10 inherit (config.users) users groups;
11 cfg = config.services.upnpc;
16 (lib.getExe cfg.package)
18 (lib.optionals (conf.addressOrInterface != null) ([
20 conf.addressOrInterface
24 while IFS=: read -r k v; do
25 k=$(printf %s "$k" | sed -e 's/^\s*//' -e 's/\s*$//')
26 v=$(printf %s "$v" | sed -e 's/^\s*//' -e 's/\s*$//')
29 ("Local LAN ip address") localIP=$v;;
33 lib.optionalString (conf.addressOrInterface != null) "-m \"${conf.addressOrInterface}\""
39 options.services.upnpc = {
40 enable = lib.mkEnableOption "UPnP redirections";
41 package = lib.mkPackageOption pkgs "miniupnpc" { };
42 redirections = lib.mkOption {
43 description = "UPnP redirections to request.";
49 options.addressOrInterface = lib.mkOption {
51 Provide IPv4 address or interface name (IPv4 or IPv6)
52 to use for sending SSDP multicast packets.
54 Beware that under Linux multicast packets
55 are only sent on a single interface,
56 as shown by `ip route get 239.255.255.250`.
57 See https://unix.stackexchange.com/questions/719148/multicast-to-all-interfaces-how-to-set-up-the-routes
59 type = with types; nullOr str;
62 options.description = lib.mkOption {
63 description = "Description of the port mapping";
67 options.duration = lib.mkOption {
68 description = "Duration of the redirection, in seconds. 0 means indefinitely.";
72 options.externalPort = lib.mkOption {
73 description = "External port to open on the redirecting device.";
76 options.internalPort = lib.mkOption {
77 description = "Internal port, target of the redirection.";
79 default = config.externalPort;
81 options.maintainPeriod = lib.mkOption {
82 description = "Period (in seconds) between runs to maintain the redirection.";
83 type = with types; nullOr int;
84 default = if config.duration > 0 then config.duration / 2 else null;
85 defaultText = "if duration > 0 then duration / 2 else null";
87 options.override = lib.mkOption {
88 description = "Try to override the redirection in case of conflict in mapping entry.";
92 options.protocol = lib.mkOption {
93 description = "Protocol to redirect.";
102 options.service = lib.mkOption {
103 description = "Configuration specific to the systemd service handling this UPnP redirecting.";
112 config = lib.mkIf cfg.enable {
113 systemd.services = lib.listToAttrs (
116 lib.nameValuePair "upnpc-${toString (conf.addressOrInterface or "")}-${toString conf.internalPort}"
120 description = "UPnP ${toString conf.internalPort}";
121 after = [ "network-pre.target" ];
122 #wantedBy = [ "multi-user.target" ];
123 path = [ cfg.package ];
125 Type = if conf.maintainPeriod == null then "oneshot" else "simple";
126 RemainAfterExit = conf.maintainPeriod == null;
127 ExecStart = pkgs.writeShellScript "upnpc-start-${toString conf.internalPort}" ''
131 while IFS= read -r line; do
134 (*" is redirected to internal $localIP:${toString conf.internalPort}"*) result=ok ;;
135 (*ConflictInMappingEntry*) result=conflict ;;
139 lib.escapeShellArgs (
142 (lib.optionals (conf.description != "") [
144 (lib.escapeShellArg conf.description)
149 lib.optionalString (conf.description != "") "-e ${lib.escapeShellArg conf.description}"
150 } -a "$localIP" ${toString conf.internalPort} ${toString conf.externalPort} ${conf.protocol} ${toString conf.duration} 2>&1
157 ${lib.optionalString conf.override ''
158 test "$result" != conflict || {
159 ${lib.escapeShellArgs (getExe conf)} -u "$desc" -d ${toString conf.externalPort} ${conf.protocol}
165 if conf.maintainPeriod == null then "break" else "sleep " + toString conf.maintainPeriod
171 ExecStop = utils.escapeSystemdExecArgs (
176 (toString conf.externalPort)
181 Restart = "on-failure";
183 User = users."upnpc".name;
185 // lib.optionalAttrs (conf.maintainPeriod != null) {
186 RestartSec = lib.mkDefault conf.maintainPeriod;
195 environment.systemPackages = [ cfg.package ];
197 # This enables to match on the uid in the firewall.
198 users.users."upnpc" = {
200 group = groups."upnpc".name;
202 users.groups."upnpc" = { };
203 networking.nftables.ruleset = lib.concatStringsSep "\n" [
206 # A set containing the udp port(s) to which SSDP replies are allowed.
212 # Create a rule for accepting any SSDP packets going to a remembered port.
213 udp dport @upnpc-ssdp counter accept comment "SSDP answer"
216 skuid ${users.upnpc.name} \
219 comment "SSDP automatic opening"
220 skuid ${users.upnpc.name} \
221 ip daddr 239.255.255.250 udp dport ssdp \
222 set add udp sport @upnpc-ssdp \
223 comment "SSDP automatic opening"
224 skuid ${users.upnpc.name} \
225 ip daddr 239.255.255.250 udp dport ssdp \
231 (lib.optionalString config.networking.enableIPv6 ''
234 skuid ${users.upnpc.name} \
235 ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
237 set add udp sport @upnpc-ssdp \
238 comment "SSDP automatic opening"
239 skuid ${users.upnpc.name} \
240 ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
242 counter accept comment "SSDP"
248 meta.maintainers = with lib.maintainers; [ julm ];