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