]> Git — Sourcephile - julm/julm-nix.git/blob - nixos/modules/services/networking/upnpc.nix
+user/reliability(upnpc): modify service module
[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 getSlice =
13 conf: "upnpc" + lib.optionalString (conf.addressOrInterface != null) "-${conf.addressOrInterface}";
14 getExe =
15 conf:
16 lib.flatten [
17 [
18 (lib.getExe cfg.package)
19 ]
20 # Warning(correctness): -m must come first, or it will be ignored.
21 (lib.optionals (conf.addressOrInterface != null) ([
22 "-m"
23 conf.addressOrInterface
24 ]))
25 ];
26 getInfo = conf: ''
27 while IFS=: read -r k v; do
28 k=$(printf %s "$k" | sed -e 's/^\s*//' -e 's/\s*$//')
29 v=$(printf %s "$v" | sed -e 's/^\s*//' -e 's/\s*$//')
30 case $k in
31 (desc) desc=$v;;
32 ("Local LAN ip address") localIP=$v;;
33 esac
34 done <<EOF
35 $(upnpc ${
36 lib.optionalString (conf.addressOrInterface != null) "-m \"${conf.addressOrInterface}\""
37 } -s)
38 EOF
39 '';
40 in
41 {
42 options.services.upnpc = {
43 enable = lib.mkEnableOption "UPnP redirections";
44 package = lib.mkPackageOption pkgs "miniupnpc" { };
45 redirections = lib.mkOption {
46 description = "UPnP redirections to request.";
47 default = [ ];
48 type = types.listOf (
49 types.submodule (
50 { config, ... }:
51 {
52 options.addressOrInterface = lib.mkOption {
53 description = ''
54 Provide IPv4 address or interface name (IPv4 or IPv6)
55 to use for sending SSDP multicast packets.
56
57 Beware that under Linux multicast packets
58 are only sent on a single interface,
59 as shown by `ip route get 239.255.255.250`.
60 See https://unix.stackexchange.com/questions/719148/multicast-to-all-interfaces-how-to-set-up-the-routes
61 '';
62 type = with types; nullOr str;
63 default = null;
64 };
65 options.description = lib.mkOption {
66 description = "Description of the port mapping";
67 type = types.str;
68 default = "";
69 };
70 options.duration = lib.mkOption {
71 description = "Duration of the redirection, in seconds. 0 means indefinitely.";
72 type = types.int;
73 default = 0;
74 };
75 options.externalPort = lib.mkOption {
76 description = "External port to open on the redirecting device.";
77 type = types.port;
78 };
79 options.internalPort = lib.mkOption {
80 description = "Internal port, target of the redirection.";
81 type = types.port;
82 default = config.externalPort;
83 };
84 options.maintainPeriod = lib.mkOption {
85 description = "Period (in seconds) between runs to maintain the redirection.";
86 type = with types; nullOr int;
87 default = if config.duration > 0 then config.duration / 2 else null;
88 defaultText = "if duration > 0 then duration / 2 else null";
89 };
90 options.override = lib.mkOption {
91 description = "Try to override the redirection in case of conflict in mapping entry.";
92 type = types.bool;
93 default = true;
94 };
95 options.protocol = lib.mkOption {
96 description = "Protocol to redirect.";
97 type =
98 with types;
99 enum [
100 "TCP"
101 "UDP"
102 ];
103 default = "TCP";
104 };
105 options.service = lib.mkOption {
106 description = "Configuration specific to the systemd service handling this UPnP redirecting.";
107 type = types.attrs;
108 default = { };
109 };
110 }
111 )
112 );
113 };
114 };
115 config = lib.mkIf cfg.enable {
116 systemd.slices = lib.listToAttrs (
117 lib.map (
118 conf:
119 lib.nameValuePair (getSlice conf) {
120 enable = true;
121 description = "UPnP slice for ${conf.addressOrInterface or "any interface"}";
122 }
123 ) cfg.redirections
124 );
125
126 systemd.services = lib.listToAttrs (
127 lib.map (
128 conf:
129 lib.nameValuePair
130 "upnpc-${conf.addressOrInterface or ""}-${conf.protocol}-${toString conf.internalPort}"
131 (
132 lib.mkMerge ([
133 {
134 description = "UPnP for ${
135 conf.addressOrInterface or "any interface"
136 }, protocol ${conf.protocol} and port ${toString conf.internalPort}";
137 after = [ "network-pre.target" ];
138 wantedBy = lib.mkDefault [ "multi-user.target" ];
139 path = [ cfg.package ];
140 serviceConfig = {
141 Slice = "${getSlice conf}.slice";
142 Type = if conf.maintainPeriod == null then "oneshot" else "simple";
143 RemainAfterExit = conf.maintainPeriod == null;
144 ExecStart = pkgs.writeShellScript "upnpc-start-${conf.addressOrInterface or ""}-${conf.protocol}-${toString conf.internalPort}" ''
145 set -eu
146 redirect () {
147 result=
148 while IFS= read -r line; do
149 echo >&2 -E "$line"
150 case $line in
151 (*" is redirected to internal $localIP:${toString conf.internalPort}"*) result=ok ;;
152 (*ConflictInMappingEntry*) result=conflict ;;
153 esac
154 done <<EOF
155 $(${
156 lib.escapeShellArgs (
157 lib.flatten [
158 (getExe conf)
159 (lib.optionals (conf.description != "") [
160 "-e"
161 (lib.escapeShellArg conf.description)
162 ])
163 ]
164 )
165 } -u "$desc" ${
166 lib.optionalString (conf.description != "") "-e ${lib.escapeShellArg conf.description}"
167 } -a "$localIP" ${toString conf.internalPort} ${toString conf.externalPort} ${conf.protocol} ${toString conf.duration} 2>&1
168 )
169 EOF
170 }
171 while true; do
172 ${getInfo conf}
173 redirect
174 ${lib.optionalString conf.override ''
175 test "$result" != conflict || {
176 ${lib.escapeShellArgs (getExe conf)} -u "$desc" -d ${toString conf.externalPort} ${conf.protocol}
177 redirect
178 }
179 ''}
180 case $result in
181 (ok) ${
182 if conf.maintainPeriod == null then "break" else "sleep " + toString conf.maintainPeriod
183 } ;;
184 (*) exit 1 ;;
185 esac
186 done
187 '';
188 ExecStop = utils.escapeSystemdExecArgs (
189 lib.flatten [
190 (getExe conf)
191 [
192 "-d"
193 (toString conf.externalPort)
194 conf.protocol
195 ]
196 ]
197 );
198 Restart = "on-failure";
199 DynamicUser = true;
200 User = users."upnpc".name;
201 }
202 // lib.optionalAttrs (conf.maintainPeriod != null) {
203 RestartSec = lib.mkDefault conf.maintainPeriod;
204 };
205 }
206 conf.service
207 ])
208 )
209 ) cfg.redirections
210 );
211
212 environment.systemPackages = [ cfg.package ];
213
214 # This enables to match on the uid in the firewall.
215 users.users."upnpc" = {
216 isSystemUser = true;
217 group = groups."upnpc".name;
218 };
219 users.groups."upnpc" = { };
220 networking.nftables.ruleset = lib.concatStringsSep "\n" [
221 ''
222 table inet filter {
223 # A set containing the udp port(s) to which SSDP replies are allowed.
224 set upnpc-ssdp {
225 type inet_service
226 timeout 5s
227 }
228 chain input-net {
229 # Create a rule for accepting any SSDP packets going to a remembered port.
230 udp dport @upnpc-ssdp counter accept comment "SSDP answer"
231 }
232 chain output-net {
233 skuid ${users.upnpc.name} \
234 tcp dport ssdp \
235 counter accept \
236 comment "SSDP automatic opening of input-net"
237 skuid ${users.upnpc.name} \
238 ip daddr 239.255.255.250 udp dport ssdp \
239 set add udp sport @upnpc-ssdp \
240 comment "SSDP automatic opening of input-net"
241 skuid ${users.upnpc.name} \
242 ip daddr 239.255.255.250 udp dport ssdp \
243 counter accept \
244 comment "SSDP"
245 }
246 }
247 ''
248 (lib.optionalString config.networking.enableIPv6 ''
249 table inet filter {
250 chain output-net {
251 skuid ${users.upnpc.name} \
252 ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
253 udp dport ssdp \
254 set add udp sport @upnpc-ssdp \
255 comment "SSDP automatic opening of input-net"
256 skuid ${users.upnpc.name} \
257 ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
258 udp dport ssdp \
259 counter accept comment "SSDP"
260 }
261 }
262 '')
263 ];
264 };
265 meta.maintainers = with lib.maintainers; [ julm ];
266 }