10 inherit (config.users) users groups;
11 cfg = config.services.upnpc;
16 (lib.getExe cfg.package)
18 # Warning(correctness): -m must come first, or it will be ignored.
19 (lib.optionals (conf.addressOrInterface != null) ([
21 conf.addressOrInterface
25 while IFS=: read -r k v; do
26 k=$(printf %s "$k" | sed -e 's/^\s*//' -e 's/\s*$//')
27 v=$(printf %s "$v" | sed -e 's/^\s*//' -e 's/\s*$//')
30 ("Local LAN ip address") localIP=$v;;
34 lib.optionalString (conf.addressOrInterface != null) "-m \"${conf.addressOrInterface}\""
40 options.services.upnpc = {
41 enable = lib.mkEnableOption "UPnP redirections";
42 package = lib.mkPackageOption pkgs "miniupnpc" { };
43 redirections = lib.mkOption {
44 description = "UPnP redirections to request.";
50 options.addressOrInterface = lib.mkOption {
52 Provide IPv4 address or interface name (IPv4 or IPv6)
53 to use for sending SSDP multicast packets.
55 Beware that under Linux multicast packets
56 are only sent on a single interface,
57 as shown by `ip route get 239.255.255.250`.
58 See https://unix.stackexchange.com/questions/719148/multicast-to-all-interfaces-how-to-set-up-the-routes
60 type = with types; nullOr str;
63 options.description = lib.mkOption {
64 description = "Description of the port mapping";
68 options.duration = lib.mkOption {
69 description = "Duration of the redirection, in seconds. 0 means indefinitely.";
73 options.externalPort = lib.mkOption {
74 description = "External port to open on the redirecting device.";
77 options.internalPort = lib.mkOption {
78 description = "Internal port, target of the redirection.";
80 default = config.externalPort;
82 options.maintainPeriod = lib.mkOption {
83 description = "Period (in seconds) between runs to maintain the redirection.";
84 type = with types; nullOr int;
85 default = if config.duration > 0 then config.duration / 2 else null;
86 defaultText = "if duration > 0 then duration / 2 else null";
88 options.override = lib.mkOption {
89 description = "Try to override the redirection in case of conflict in mapping entry.";
93 options.protocol = lib.mkOption {
94 description = "Protocol to redirect.";
103 options.service = lib.mkOption {
104 description = "Configuration specific to the systemd service handling this UPnP redirecting.";
113 config = lib.mkIf cfg.enable {
114 systemd.services = lib.listToAttrs (
117 lib.nameValuePair "upnpc-${toString (conf.addressOrInterface or "")}-${toString conf.internalPort}"
121 description = "UPnP for ${
122 toString (conf.addressOrInterface or "any interface")
123 } for port ${toString conf.internalPort}";
124 after = [ "network-pre.target" ];
125 #wantedBy = [ "multi-user.target" ];
126 path = [ cfg.package ];
128 Type = if conf.maintainPeriod == null then "oneshot" else "simple";
129 RemainAfterExit = conf.maintainPeriod == null;
130 ExecStart = pkgs.writeShellScript "upnpc-start-${toString (conf.addressOrInterface or "")}-${toString conf.internalPort}" ''
134 while IFS= read -r line; do
137 (*" is redirected to internal $localIP:${toString conf.internalPort}"*) result=ok ;;
138 (*ConflictInMappingEntry*) result=conflict ;;
142 lib.escapeShellArgs (
145 (lib.optionals (conf.description != "") [
147 (lib.escapeShellArg conf.description)
152 lib.optionalString (conf.description != "") "-e ${lib.escapeShellArg conf.description}"
153 } -a "$localIP" ${toString conf.internalPort} ${toString conf.externalPort} ${conf.protocol} ${toString conf.duration} 2>&1
160 ${lib.optionalString conf.override ''
161 test "$result" != conflict || {
162 ${lib.escapeShellArgs (getExe conf)} -u "$desc" -d ${toString conf.externalPort} ${conf.protocol}
168 if conf.maintainPeriod == null then "break" else "sleep " + toString conf.maintainPeriod
174 ExecStop = utils.escapeSystemdExecArgs (
179 (toString conf.externalPort)
184 Restart = "on-failure";
186 User = users."upnpc".name;
188 // lib.optionalAttrs (conf.maintainPeriod != null) {
189 RestartSec = lib.mkDefault conf.maintainPeriod;
198 environment.systemPackages = [ cfg.package ];
200 # This enables to match on the uid in the firewall.
201 users.users."upnpc" = {
203 group = groups."upnpc".name;
205 users.groups."upnpc" = { };
206 networking.nftables.ruleset = lib.concatStringsSep "\n" [
209 # A set containing the udp port(s) to which SSDP replies are allowed.
215 # Create a rule for accepting any SSDP packets going to a remembered port.
216 udp dport @upnpc-ssdp counter accept comment "SSDP answer"
219 skuid ${users.upnpc.name} \
222 comment "SSDP automatic opening"
223 skuid ${users.upnpc.name} \
224 ip daddr 239.255.255.250 udp dport ssdp \
225 set add udp sport @upnpc-ssdp \
226 comment "SSDP automatic opening"
227 skuid ${users.upnpc.name} \
228 ip daddr 239.255.255.250 udp dport ssdp \
234 (lib.optionalString config.networking.enableIPv6 ''
237 skuid ${users.upnpc.name} \
238 ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
240 set add udp sport @upnpc-ssdp \
241 comment "SSDP automatic opening"
242 skuid ${users.upnpc.name} \
243 ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
245 counter accept comment "SSDP"
251 meta.maintainers = with lib.maintainers; [ julm ];