{ pkgs, lib, config, utils, ... }: let inherit (lib) types; inherit (config.users) users groups; cfg = config.services.upnpc; getSlice = conf: "upnpc" + lib.optionalString (conf.addressOrInterface != null) "-${conf.addressOrInterface}"; getExe = conf: lib.flatten [ [ (lib.getExe cfg.package) ] # Warning(correctness): -m must come first, or it will be ignored. (lib.optionals (conf.addressOrInterface != null) ([ "-m" conf.addressOrInterface ])) ]; getInfo = conf: '' 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 = lib.mkOption { description = "Try to override the redirection in case of conflict in mapping entry."; type = types.bool; default = true; }; options.protocol = lib.mkOption { description = "Protocol to redirect."; type = with types; enum [ "TCP" "UDP" ]; default = "TCP"; }; options.service = lib.mkOption { description = "Configuration specific to the systemd service handling this UPnP redirecting."; type = types.attrs; default = { }; }; } ) ); }; }; config = lib.mkIf cfg.enable { systemd.slices = lib.listToAttrs ( lib.map ( conf: lib.nameValuePair (getSlice conf) { enable = true; description = "UPnP slice for ${conf.addressOrInterface or "any interface"}"; } ) cfg.redirections ); systemd.services = lib.listToAttrs ( lib.map ( conf: lib.nameValuePair "upnpc-${conf.addressOrInterface or ""}-${conf.protocol}-${toString conf.internalPort}" ( lib.mkMerge ([ { description = "UPnP for ${ conf.addressOrInterface or "any interface" }, protocol ${conf.protocol} and port ${toString conf.internalPort}"; after = [ "network-pre.target" ]; wantedBy = lib.mkDefault [ "multi-user.target" ]; path = [ cfg.package ]; serviceConfig = { Slice = "${getSlice conf}.slice"; Type = if conf.maintainPeriod == null then "oneshot" else "simple"; RemainAfterExit = conf.maintainPeriod == null; ExecStart = pkgs.writeShellScript "upnpc-start-${conf.addressOrInterface or ""}-${conf.protocol}-${toString conf.internalPort}" '' set -eu redirect () { result= while IFS= read -r line; do echo >&2 -E "$line" case $line in (*" is redirected to internal $localIP:${toString conf.internalPort}"*) result=ok ;; (*ConflictInMappingEntry*) result=conflict ;; esac done <&1 ) EOF } while true; do ${getInfo conf} redirect ${lib.optionalString conf.override '' test "$result" != conflict || { ${lib.escapeShellArgs (getExe conf)} -u "$desc" -d ${toString conf.externalPort} ${conf.protocol} redirect } ''} case $result in (ok) ${ if conf.maintainPeriod == null then "break" else "sleep " + toString conf.maintainPeriod } ;; (*) exit 1 ;; esac done ''; ExecStop = utils.escapeSystemdExecArgs ( lib.flatten [ (getExe conf) [ "-d" (toString conf.externalPort) conf.protocol ] ] ); Restart = "on-failure"; DynamicUser = true; User = users."upnpc".name; } // lib.optionalAttrs (conf.maintainPeriod != null) { RestartSec = lib.mkDefault conf.maintainPeriod; }; } conf.service ]) ) ) cfg.redirections ); environment.systemPackages = [ cfg.package ]; # 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.concatStringsSep "\n" [ '' 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 of input-net" skuid ${users.upnpc.name} \ ip daddr 239.255.255.250 udp dport ssdp \ set add udp sport @upnpc-ssdp \ comment "SSDP automatic opening of input-net" 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 of input-net" skuid ${users.upnpc.name} \ ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \ udp dport ssdp \ counter accept comment "SSDP" } } '') ]; }; meta.maintainers = with lib.maintainers; [ julm ]; }