]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/networking/upnpc.nix
mermet: creds: reencrypt
[sourcephile-nix.git] / nixos / modules / services / networking / upnpc.nix
1 { pkgs, lib, config, ... }:
2 with lib;
3 let
4 inherit (config.users) users groups;
5 cfg = config.services.upnpc;
6 getInfo = ''
7 while IFS=: read -r k v; do
8 k=$(printf %s "$k" | sed -e 's/^\s*//' -e 's/\s*$//')
9 v=$(printf %s "$v" | sed -e 's/^\s*//' -e 's/\s*$//')
10 case $k in
11 (desc) desc=$v;;
12 ("Local LAN ip address") localIP=$v;;
13 esac
14 done <<EOF
15 $(upnpc -s)
16 EOF
17 '';
18 in
19 {
20 options.services.upnpc = {
21 redirections = mkOption {
22 description = "UPnP redirections to request.";
23 default = [ ];
24 type = types.listOf (types.submodule ({ config, ... }: {
25 options.externalPort = mkOption {
26 description = "External port to open on the redirecting device.";
27 type = types.port;
28 };
29 options.internalPort = mkOption {
30 description = "Internal port, target of the redirection.";
31 type = types.port;
32 default = config.externalPort;
33 };
34 options.protocol = mkOption {
35 description = "Protocol to redirect.";
36 type = with types; enum [ "TCP" "UDP" ];
37 default = "TCP";
38 };
39 options.description = mkOption {
40 description = "Description of the port mapping";
41 type = types.str;
42 default = "";
43 };
44 options.duration = mkOption {
45 description = "Duration of the redirection, in seconds. 0 means indefinitely.";
46 type = types.int;
47 default = 0;
48 };
49 options.maintainPeriod = mkOption {
50 description = "Period (in seconds) between runs to maintain the redirection.";
51 type = with types; nullOr int;
52 default = if config.duration > 0 then config.duration / 2 else null;
53 defaultText = "if duration > 0 then duration / 2 else null";
54 };
55 options.override = mkOption {
56 description = "Try to override the redirection in case of conflict in mapping entry.";
57 type = types.bool;
58 default = true;
59 };
60 options.service = mkOption {
61 description = "Configuration specific to the systemd service handling this UPnP redirecting.";
62 type = types.attrs;
63 default = { };
64 };
65 }));
66 };
67 };
68 config = {
69 systemd.services = listToAttrs (map
70 (r:
71 nameValuePair "upnpc-${toString r.internalPort}" (mkMerge [
72 {
73 description = "UPnP ${toString r.internalPort}";
74 after = [ "network-pre.target" ];
75 #wantedBy = [ "multi-user.target" ];
76 path = [ pkgs.miniupnpc ];
77 serviceConfig = {
78 Type = if r.maintainPeriod == null then "oneshot" else "simple";
79 RemainAfterExit = r.maintainPeriod == null;
80 ExecStart = pkgs.writeShellScript "upnpc-start-${toString r.internalPort}" ''
81 set -eu
82 redirect () {
83 result=
84 while IFS= read -r line; do
85 echo >&2 -E "$line"
86 case $line in
87 (*" is redirected to internal $localIP:${toString r.internalPort}"*) result=ok ;;
88 (*ConflictInMappingEntry*) result=conflict ;;
89 esac
90 done <<EOF
91 $(upnpc -u "$desc" ${optionalString (r.description != "") "-e \"${r.description}\""} \
92 -a "$localIP" ${toString r.internalPort} ${toString r.externalPort} ${r.protocol} ${toString r.duration} 2>&1)
93 EOF
94 }
95 while true; do
96 ${getInfo}
97 redirect
98 ${optionalString r.override ''
99 test "$result" != conflict || {
100 upnpc -u "$desc" -d ${toString r.externalPort} ${r.protocol}
101 redirect
102 }
103 ''}
104 case $result in
105 (ok) ${if r.maintainPeriod == null then "break" else "sleep " + toString r.maintainPeriod} ;;
106 (*) exit 1 ;;
107 esac
108 done
109 '';
110 ExecStop = "${pkgs.miniupnpc}/bin/upnpc -d ${toString r.externalPort} ${r.protocol}";
111 Restart = "on-failure";
112 RestartSec = mkDefault r.maintainPeriod;
113 DynamicUser = true;
114 User = users."upnpc".name;
115 };
116 }
117 r.service
118 ])
119 )
120 cfg.redirections);
121
122 # This enables to match on the uid in the firewall.
123 users.users."upnpc" = {
124 isSystemUser = true;
125 group = groups."upnpc".name;
126 };
127 users.groups."upnpc" = { };
128 networking.nftables.ruleset =
129 lib.optionalString (cfg.redirections != [ ]) ''
130 table inet filter {
131 # A set containing the udp port(s) to which SSDP replies are allowed.
132 set upnpc-ssdp {
133 type inet_service
134 timeout 5s
135 }
136 chain input-net {
137 # Create a rule for accepting any SSDP packets going to a remembered port.
138 udp dport @upnpc-ssdp counter accept comment "SSDP answer"
139 }
140 chain output-net {
141 skuid ${users.upnpc.name} \
142 tcp dport ssdp \
143 counter accept \
144 comment "SSDP automatic opening"
145 skuid ${users.upnpc.name} \
146 ip daddr 239.255.255.250 udp dport ssdp \
147 set add udp sport @upnpc-ssdp \
148 comment "SSDP automatic opening"
149 skuid ${users.upnpc.name} \
150 ip daddr 239.255.255.250 udp dport ssdp \
151 counter accept \
152 comment "SSDP"
153 }
154 }
155 '' + lib.optionalString config.networking.enableIPv6 ''
156 table inet filter {
157 chain output-net {
158 skuid ${users.upnpc.name} \
159 ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
160 udp dport ssdp \
161 set add udp sport @upnpc-ssdp \
162 comment "SSDP automatic opening"
163 skuid ${users.upnpc.name} \
164 ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
165 udp dport ssdp \
166 counter accept comment "SSDP"
167 }
168 }
169 '';
170 };
171 meta.maintainers = with maintainers; [ julm ];
172 }