]> Git — Sourcephile - julm/julm-nix.git/blob - nixos/modules/services/networking/upnpc.nix
fixup! +user/correctness(upnpc): use correct interface
[julm/julm-nix.git] / nixos / modules / services / networking / upnpc.nix
1 {
2 pkgs,
3 lib,
4 config,
5 utils,
6 ...
7 }:
8 let
9 inherit (lib) types;
10 inherit (config.users) users groups;
11 cfg = config.services.upnpc;
12 getExe =
13 conf:
14 lib.flatten [
15 [
16 (lib.getExe cfg.package)
17 ]
18 # Warning(correctness): -m must come first, or it will be ignored.
19 (lib.optionals (conf.addressOrInterface != null) ([
20 "-m"
21 conf.addressOrInterface
22 ]))
23 ];
24 getInfo = conf: ''
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*$//')
28 case $k in
29 (desc) desc=$v;;
30 ("Local LAN ip address") localIP=$v;;
31 esac
32 done <<EOF
33 $(upnpc ${
34 lib.optionalString (conf.addressOrInterface != null) "-m \"${conf.addressOrInterface}\""
35 } -s)
36 EOF
37 '';
38 in
39 {
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.";
45 default = [ ];
46 type = types.listOf (
47 types.submodule (
48 { config, ... }:
49 {
50 options.addressOrInterface = lib.mkOption {
51 description = ''
52 Provide IPv4 address or interface name (IPv4 or IPv6)
53 to use for sending SSDP multicast packets.
54
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
59 '';
60 type = with types; nullOr str;
61 default = null;
62 };
63 options.description = lib.mkOption {
64 description = "Description of the port mapping";
65 type = types.str;
66 default = "";
67 };
68 options.duration = lib.mkOption {
69 description = "Duration of the redirection, in seconds. 0 means indefinitely.";
70 type = types.int;
71 default = 0;
72 };
73 options.externalPort = lib.mkOption {
74 description = "External port to open on the redirecting device.";
75 type = types.port;
76 };
77 options.internalPort = lib.mkOption {
78 description = "Internal port, target of the redirection.";
79 type = types.port;
80 default = config.externalPort;
81 };
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";
87 };
88 options.override = lib.mkOption {
89 description = "Try to override the redirection in case of conflict in mapping entry.";
90 type = types.bool;
91 default = true;
92 };
93 options.protocol = lib.mkOption {
94 description = "Protocol to redirect.";
95 type =
96 with types;
97 enum [
98 "TCP"
99 "UDP"
100 ];
101 default = "TCP";
102 };
103 options.service = lib.mkOption {
104 description = "Configuration specific to the systemd service handling this UPnP redirecting.";
105 type = types.attrs;
106 default = { };
107 };
108 }
109 )
110 );
111 };
112 };
113 config = lib.mkIf cfg.enable {
114 systemd.services = lib.listToAttrs (
115 lib.map (
116 conf:
117 lib.nameValuePair "upnpc-${toString (conf.addressOrInterface or "")}-${toString conf.internalPort}"
118 (
119 lib.mkMerge [
120 {
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 ];
127 serviceConfig = {
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}" ''
131 set -eu
132 redirect () {
133 result=
134 while IFS= read -r line; do
135 echo >&2 -E "$line"
136 case $line in
137 (*" is redirected to internal $localIP:${toString conf.internalPort}"*) result=ok ;;
138 (*ConflictInMappingEntry*) result=conflict ;;
139 esac
140 done <<EOF
141 $(${
142 lib.escapeShellArgs (
143 lib.flatten [
144 (getExe conf)
145 (lib.optionals (conf.description != "") [
146 "-e"
147 (lib.escapeShellArg conf.description)
148 ])
149 ]
150 )
151 } -u "$desc" ${
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
154 )
155 EOF
156 }
157 while true; do
158 ${getInfo conf}
159 redirect
160 ${lib.optionalString conf.override ''
161 test "$result" != conflict || {
162 ${lib.escapeShellArgs (getExe conf)} -u "$desc" -d ${toString conf.externalPort} ${conf.protocol}
163 redirect
164 }
165 ''}
166 case $result in
167 (ok) ${
168 if conf.maintainPeriod == null then "break" else "sleep " + toString conf.maintainPeriod
169 } ;;
170 (*) exit 1 ;;
171 esac
172 done
173 '';
174 ExecStop = utils.escapeSystemdExecArgs (
175 lib.flatten [
176 (getExe conf)
177 [
178 "-d"
179 (toString conf.externalPort)
180 conf.protocol
181 ]
182 ]
183 );
184 Restart = "on-failure";
185 DynamicUser = true;
186 User = users."upnpc".name;
187 }
188 // lib.optionalAttrs (conf.maintainPeriod != null) {
189 RestartSec = lib.mkDefault conf.maintainPeriod;
190 };
191 }
192 conf.service
193 ]
194 )
195 ) cfg.redirections
196 );
197
198 environment.systemPackages = [ cfg.package ];
199
200 # This enables to match on the uid in the firewall.
201 users.users."upnpc" = {
202 isSystemUser = true;
203 group = groups."upnpc".name;
204 };
205 users.groups."upnpc" = { };
206 networking.nftables.ruleset = lib.concatStringsSep "\n" [
207 ''
208 table inet filter {
209 # A set containing the udp port(s) to which SSDP replies are allowed.
210 set upnpc-ssdp {
211 type inet_service
212 timeout 5s
213 }
214 chain input-net {
215 # Create a rule for accepting any SSDP packets going to a remembered port.
216 udp dport @upnpc-ssdp counter accept comment "SSDP answer"
217 }
218 chain output-net {
219 skuid ${users.upnpc.name} \
220 tcp dport ssdp \
221 counter accept \
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 \
229 counter accept \
230 comment "SSDP"
231 }
232 }
233 ''
234 (lib.optionalString config.networking.enableIPv6 ''
235 table inet filter {
236 chain output-net {
237 skuid ${users.upnpc.name} \
238 ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
239 udp dport ssdp \
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 } \
244 udp dport ssdp \
245 counter accept comment "SSDP"
246 }
247 }
248 '')
249 ];
250 };
251 meta.maintainers = with lib.maintainers; [ julm ];
252 }