{ pkgs, lib, config, ... }: with lib; let inherit (config.users) users groups; cfg = config.services.upnpc; getInfo = '' while IFS=: read -r k v; do k=$(printf %s "$k" | sed -e 's/^\s*//' -e 's/\s*$//') v=$(printf %s "$v" | sed -e 's/^\s*//' -e 's/\s*$//') case $k in (desc) desc=$v;; ("Local LAN ip address") localIP=$v;; esac done < 0 then config.duration / 2 else null; defaultText = "if duration > 0 then duration / 2 else null"; }; options.override = mkOption { description = "Try to override the redirection in case of conflict in mapping entry."; type = types.bool; default = true; }; options.service = mkOption { description = "Configuration specific to the systemd service handling this UPnP redirecting."; type = types.attrs; default = { }; }; })); }; }; config = mkIf cfg.enable { systemd.services = listToAttrs (map (r: nameValuePair "upnpc-${toString r.internalPort}" (mkMerge [ { description = "UPnP ${toString r.internalPort}"; after = [ "network-pre.target" ]; #wantedBy = [ "multi-user.target" ]; path = [ pkgs.miniupnpc ]; serviceConfig = { Type = if r.maintainPeriod == null then "oneshot" else "simple"; RemainAfterExit = r.maintainPeriod == null; ExecStart = pkgs.writeShellScript "upnpc-start-${toString r.internalPort}" '' set -eu redirect () { result= while IFS= read -r line; do echo >&2 -E "$line" case $line in (*" is redirected to internal $localIP:${toString r.internalPort}"*) result=ok ;; (*ConflictInMappingEntry*) result=conflict ;; esac done <&1) EOF } while true; do ${getInfo} redirect ${optionalString r.override '' test "$result" != conflict || { upnpc -u "$desc" -d ${toString r.externalPort} ${r.protocol} redirect } ''} case $result in (ok) ${if r.maintainPeriod == null then "break" else "sleep " + toString r.maintainPeriod} ;; (*) exit 1 ;; esac done ''; ExecStop = "${pkgs.miniupnpc}/bin/upnpc -d ${toString r.externalPort} ${r.protocol}"; Restart = "on-failure"; DynamicUser = true; User = users."upnpc".name; } // lib.optionalAttrs (r.maintainPeriod != null) { RestartSec = mkDefault r.maintainPeriod; }; } r.service ]) ) cfg.redirections); environment.systemPackages = [ pkgs.miniupnpc ]; # This enables to match on the uid in the firewall. users.users."upnpc" = { isSystemUser = true; group = groups."upnpc".name; }; users.groups."upnpc" = { }; networking.nftables.ruleset = lib.optionalString (cfg.redirections != [ ]) '' table inet filter { # A set containing the udp port(s) to which SSDP replies are allowed. set upnpc-ssdp { type inet_service timeout 5s } chain input-net { # Create a rule for accepting any SSDP packets going to a remembered port. udp dport @upnpc-ssdp counter accept comment "SSDP answer" } chain output-net { skuid ${users.upnpc.name} \ tcp dport ssdp \ counter accept \ comment "SSDP automatic opening" skuid ${users.upnpc.name} \ ip daddr 239.255.255.250 udp dport ssdp \ set add udp sport @upnpc-ssdp \ comment "SSDP automatic opening" skuid ${users.upnpc.name} \ ip daddr 239.255.255.250 udp dport ssdp \ counter accept \ comment "SSDP" } } '' + lib.optionalString config.networking.enableIPv6 '' table inet filter { chain output-net { skuid ${users.upnpc.name} \ ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \ udp dport ssdp \ set add udp sport @upnpc-ssdp \ comment "SSDP automatic opening" skuid ${users.upnpc.name} \ ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \ udp dport ssdp \ counter accept comment "SSDP" } } ''; }; meta.maintainers = with maintainers; [ julm ]; }