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