]> Git — Sourcephile - julm/julm-nix.git/blob - nixpkgs/patches/openvpn/openvpn-netns.patch
+maint/update: bump to nixos-25.11
[julm/julm-nix.git] / nixpkgs / patches / openvpn / openvpn-netns.patch
1 From 14ef78b30a3100dc5e4621bb8c3601a7604b730a Mon Sep 17 00:00:00 2001
2 From: Julien Moutinho <julm+nixpkgs@sourcephile.fr>
3 Date: Sun, 17 Jan 2021 15:28:25 +0100
4 Subject: [PATCH] nixos/openvpn: add netns support
5
6 ---
7 nixos/modules/services/networking/openvpn.nix | 459 ++++++++++++++++++++--------
8 1 file changed, 342 insertions(+), 117 deletions(-)
9
10 diff --git a/nixos/modules/services/networking/openvpn.nix b/nixos/modules/services/networking/openvpn.nix
11 index cea5b180a3..0145fdd9fe 100644
12 --- a/nixos/modules/services/networking/openvpn.nix
13 +++ b/nixos/modules/services/networking/openvpn.nix
14 @@ -10,57 +10,205 @@
15 let
16
17 cfg = config.services.openvpn;
18 -
19 - makeOpenVPNJob =
20 - cfg: name:
21 + enabledServers = filterAttrs (name: srv: srv.enable) cfg.servers;
22 +
23 + inherit (pkgs) openvpn;
24 +
25 + PATH = name: makeBinPath config.systemd.services."openvpn-${name}".path;
26 +
27 + makeOpenVPNJob = cfg: name:
28 let
29
30 - path = makeBinPath (getAttr "openvpn-${name}" config.systemd.services).path;
31 -
32 - upScript = ''
33 - export PATH=${path}
34 -
35 - # For convenience in client scripts, extract the remote domain
36 - # name and name server.
37 - for var in ''${!foreign_option_*}; do
38 - x=(''${!var})
39 - if [ "''${x[0]}" = dhcp-option ]; then
40 - if [ "''${x[1]}" = DOMAIN ]; then domain="''${x[2]}"
41 - elif [ "''${x[1]}" = DNS ]; then nameserver="''${x[2]}"
42 - fi
43 - fi
44 - done
45 -
46 - ${cfg.up}
47 - ${optionalString cfg.updateResolvConf "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
48 - '';
49 -
50 - downScript = ''
51 - export PATH=${path}
52 - ${optionalString cfg.updateResolvConf "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
53 - ${cfg.down}
54 - '';
55 -
56 - configFile = pkgs.writeText "openvpn-config-${name}" ''
57 - errors-to-stderr
58 - ${optionalString (cfg.up != "" || cfg.down != "" || cfg.updateResolvConf) "script-security 2"}
59 - ${cfg.config}
60 - ${optionalString (
61 - cfg.up != "" || cfg.updateResolvConf
62 - ) "up ${pkgs.writeShellScript "openvpn-${name}-up" upScript}"}
63 - ${optionalString (
64 - cfg.down != "" || cfg.updateResolvConf
65 - ) "down ${pkgs.writeShellScript "openvpn-${name}-down" downScript}"}
66 - ${optionalString (cfg.authUserPass != null) (
67 - if isAttrs cfg.authUserPass then
68 - "auth-user-pass ${pkgs.writeText "openvpn-credentials-${name}" ''
69 - ${cfg.authUserPass.username}
70 - ${cfg.authUserPass.password}
71 - ''}"
72 - else
73 - "auth-user-pass ${cfg.authUserPass}"
74 - )}
75 - '';
76 + configFile = pkgs.writeText "openvpn-config-${name}" (
77 + generators.toKeyValue
78 + {
79 + mkKeyValue = key: value:
80 + if hasAttr key scripts
81 + then "${key} " + pkgs.writeShellScript "openvpn-${name}-${key}" (scripts.${key} value)
82 + else if builtins.isBool value
83 + then optionalString value key
84 + else if builtins.isPath value
85 + then "${key} ${value}"
86 + else if builtins.isList value
87 + then concatMapStringsSep "\n" (v: "${key} ${generators.mkValueStringDefault {} v}") value
88 + else "${key} ${generators.mkValueStringDefault {} value}";
89 + }
90 + cfg.settings
91 + );
92 +
93 + scripts = {
94 + up = script:
95 + let
96 + init = ''
97 + export PATH=${PATH name}
98 +
99 + # For convenience in client scripts, extract the remote domain
100 + # name and name server.
101 + for var in ''${!foreign_option_*}; do
102 + x=(''${!var})
103 + if [ "''${x[0]}" = dhcp-option ]; then
104 + if [ "''${x[1]}" = DOMAIN ]; then domain="''${x[2]}"
105 + elif [ "''${x[1]}" = DNS ]; then nameserver="''${x[2]}"
106 + fi
107 + fi
108 + done
109 +
110 + ${optionalString cfg.updateResolvConf
111 + "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
112 + '';
113 + # Add DNS settings given in foreign DHCP options to the resolv.conf of the netns.
114 + # Note that JoinsNamespaceOf="netns-${cfg.netns}.service" will not
115 + # BindReadOnlyPaths=["/etc/netns/${cfg.netns}/resolv.conf:/etc/resolv.conf"];
116 + # this will have to be added in each service joining the namespace.
117 + setNetNSResolvConf = ''
118 + mkdir -p /etc/netns/${cfg.netns}
119 + # This file is normally created by netns-${cfg.netns}.service,
120 + # care must be taken to not delete it but to truncate it
121 + # in order to propagate the changes to bind-mounted versions.
122 + : > /etc/netns/${cfg.netns}/resolv.conf
123 + chmod 644 /etc/netns/${cfg.netns}/resolv.conf
124 + foreign_opt_domains=
125 + process_foreign_option () {
126 + case "$1:$2" in
127 + dhcp-option:DNS) echo "nameserver $3" >>/etc/netns/'${cfg.netns}'/resolv.conf ;;
128 + dhcp-option:DOMAIN) foreign_opt_domains="$foreign_opt_domains $3" ;;
129 + esac
130 + }
131 + i=1
132 + while
133 + eval opt=\"\''${foreign_option_$i-}\"
134 + [ -n "$opt" ]
135 + do
136 + process_foreign_option $opt
137 + i=$(( i + 1 ))
138 + done
139 + for d in $foreign_opt_domains; do
140 + printf '%s\n' "domain $1" "search $*" \
141 + >>/etc/netns/'${cfg.netns}'/resolv.conf
142 + done
143 + '';
144 + in
145 + if cfg.netns == null
146 + then ''
147 + ${init}
148 + ${script}
149 + ''
150 + else ''
151 + export PATH=${PATH name}
152 + set -eux
153 + ${setNetNSResolvConf}
154 + ip link set dev '${cfg.settings.dev}' up netns '${cfg.netns}' mtu "$tun_mtu"
155 + ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-up-netns.sh" ''
156 + ${init}
157 + set -eux
158 + export PATH=${PATH name}
159 +
160 + ip link set dev lo up
161 +
162 + netmask4="''${ifconfig_netmask:-30}"
163 + netbits6="''${ifconfig_ipv6_netbits:-112}"
164 + if [ -n "''${ifconfig_local-}" ]; then
165 + if [ -n "''${ifconfig_remote-}" ]; then
166 + ip -4 addr replace \
167 + local "$ifconfig_local" \
168 + peer "$ifconfig_remote/$netmask4" \
169 + ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
170 + dev '${cfg.settings.dev}'
171 + else
172 + ip -4 addr replace \
173 + local "$ifconfig_local/$netmask4" \
174 + ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
175 + dev '${cfg.settings.dev}'
176 + fi
177 + fi
178 + if [ -n "''${ifconfig_ipv6_local-}" ]; then
179 + if [ -n "''${ifconfig_ipv6_remote-}" ]; then
180 + ip -6 addr replace \
181 + local "$ifconfig_ipv6_local" \
182 + peer "$ifconfig_ipv6_remote/$netbits6" \
183 + dev '${cfg.settings.dev}'
184 + else
185 + ip -6 addr replace \
186 + local "$ifconfig_ipv6_local/$netbits6" \
187 + dev '${cfg.settings.dev}'
188 + fi
189 + fi
190 + set +eux
191 + ${script}
192 + ''}
193 + '';
194 + route-up = script:
195 + if cfg.netns == null
196 + then script
197 + else ''
198 + export PATH=${PATH name}
199 + set -eux
200 + ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-route-up-netns" ''
201 + export PATH=${PATH name}
202 + set -eux
203 + i=1
204 + while
205 + eval net=\"\''${route_network_$i-}\"
206 + eval mask=\"\''${route_netmask_$i-}\"
207 + eval gw=\"\''${route_gateway_$i-}\"
208 + eval mtr=\"\''${route_metric_$i-}\"
209 + [ -n "$net" ]
210 + do
211 + ip -4 route replace "$net/$mask" via "$gw" ''${mtr:+metric "$mtr"}
212 + i=$(( i + 1 ))
213 + done
214 +
215 + if [ -n "''${route_vpn_gateway-}" ]; then
216 + ip -4 route replace default via "$route_vpn_gateway"
217 + fi
218 +
219 + i=1
220 + while
221 + # There doesn't seem to be $route_ipv6_metric_<n>
222 + # according to the manpage.
223 + eval net=\"\''${route_ipv6_network_$i-}\"
224 + eval gw=\"\''${route_ipv6_gateway_$i-}\"
225 + [ -n "$net" ]
226 + do
227 + ip -6 route replace "$net" via "$gw" metric 100
228 + i=$(( i + 1 ))
229 + done
230 +
231 + # There's no $route_vpn_gateway for IPv6. It's not
232 + # documented if OpenVPN includes default route in
233 + # $route_ipv6_*. Set default route to remote VPN
234 + # endpoint address if there is one. Use higher metric
235 + # than $route_ipv6_* routes to give preference to a
236 + # possible default route in them.
237 + if [ -n "''${ifconfig_ipv6_remote-}" ]; then
238 + ip -6 route replace default \
239 + via "$ifconfig_ipv6_remote" metric 200
240 + fi
241 + ${script}
242 + ''}
243 + '';
244 + down = script:
245 + let
246 + init = ''
247 + export PATH=${PATH name}
248 + ${optionalString cfg.updateResolvConf
249 + "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
250 + '';
251 + in
252 + if cfg.netns == null
253 + then ''
254 + ${init}
255 + ${script}
256 + ''
257 + else ''
258 + export PATH=${PATH name}
259 + ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-down-netns.sh" ''
260 + ${init}
261 + ${script}
262 + ''}
263 + rm -f /etc/netns/'${cfg.netns}'/resolv.conf
264 + '';
265 + };
266
267 in
268 {
269 @@ -68,6 +216,8 @@
270
271 wantedBy = optional cfg.autoStart "multi-user.target";
272 after = [ "network.target" ];
273 + bindsTo = optional (cfg.netns != null) "netns-${cfg.netns}.service";
274 + requires = optional (cfg.netns != null) "netns-${cfg.netns}.service";
275
276 path = [
277 pkgs.iptables
278 @@ -77,6 +227,7 @@
279
280 serviceConfig.ExecStart = "@${config.services.openvpn.package}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
281 serviceConfig.Restart = "always";
282 + serviceConfig.RestartSec = "5s";
283 serviceConfig.Type = "notify";
284 };
285
286 @@ -88,7 +239,7 @@
287 unitNames = map (n: "openvpn-${n}.service") (builtins.attrNames cfg.servers);
288 in
289 "systemctl try-restart ${lib.escapeShellArgs unitNames}";
290 - description = "Restart system OpenVPN connections when returning from sleep";
291 + description = "Sends a signal to OpenVPN process to trigger a restart after return from sleep";
292 };
293 };
294
295 @@ -110,30 +261,30 @@
296 example = literalExpression ''
297 {
298 server = {
299 - config = '''
300 + settings = {
301 # Simplest server configuration: https://community.openvpn.net/openvpn/wiki/StaticKeyMiniHowto
302 # server :
303 - dev tun
304 - ifconfig 10.8.0.1 10.8.0.2
305 - secret /root/static.key
306 - ''';
307 - up = "ip route add ...";
308 - down = "ip route del ...";
309 + dev = "tun";
310 + ifconfig = "10.8.0.1 10.8.0.2";
311 + secret = "/root/static.key";
312 + up = "ip route add ...";
313 + down = "ip route del ...";
314 + };
315 };
316
317 client = {
318 - config = '''
319 - client
320 - remote vpn.example.org
321 - dev tun
322 - proto tcp-client
323 - port 8080
324 - ca /root/.vpn/ca.crt
325 - cert /root/.vpn/alice.crt
326 - key /root/.vpn/alice.key
327 - ''';
328 - up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
329 - down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
330 + settings = {
331 + client = true;
332 + remote = "vpn.example.org";
333 + dev = "tun";
334 + proto = "tcp-client";
335 + port = 8080;
336 + ca = "/root/.vpn/ca.crt";
337 + cert = "/root/.vpn/alice.crt";
338 + key = "/root/.vpn/alice.key";
339 + up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
340 + down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
341 + };
342 };
343 }
344 '';
345 @@ -147,55 +298,102 @@
346 attribute name.
347 '';
348
349 - type =
350 - with types;
351 - attrsOf (submodule {
352 + type = with types; attrsOf (submodule ({ name, config, options, ... }: {
353
354 - options = {
355 + options = rec {
356 + enable = mkEnableOption "OpenVPN server" // { default = true; };
357
358 - config = mkOption {
359 - type = types.lines;
360 - description = ''
361 - Configuration of this OpenVPN instance. See
362 - {manpage}`openvpn(8)`
363 - for details.
364 + settings = mkOption {
365 + description = ''
366 + Configuration of this OpenVPN instance. See
367 + {manpage}`openvpn(8)`
368 + for details.
369
370 To import an external config file, use the following definition:
371 `config = "config /path/to/config.ovpn"`
372 '';
373 - };
374
375 - up = mkOption {
376 - default = "";
377 - type = types.lines;
378 - description = ''
379 - Shell commands executed when the instance is starting.
380 - '';
381 + default = { };
382 + type = types.submodule {
383 + freeformType = with types;
384 + attrsOf (
385 + nullOr (
386 + oneOf [
387 + bool
388 + int
389 + str
390 + path
391 + (listOf (oneOf [ bool int str path ]))
392 + ]
393 + )
394 + );
395 + options.dev = mkOption {
396 + default = null;
397 + type = types.str;
398 + description = ''
399 + Shell commands executed when the instance is starting.
400 + '';
401 + };
402 + options.down = mkOption {
403 + default = "";
404 + type = types.lines;
405 + description = ''
406 + Shell commands executed when the instance is shutting down.
407 + '';
408 + };
409 + options.errors-to-stderr = mkOption {
410 + default = true;
411 + type = types.bool;
412 + description = ''
413 + Output errors to stderr instead of stdout
414 + unless log output is redirected by one of the `--log` options.
415 + '';
416 + };
417 + options.route-up = mkOption {
418 + default = "";
419 + type = types.lines;
420 + description = ''
421 + Run command after routes are added.
422 + '';
423 + };
424 + options.up = mkOption {
425 + default = "";
426 + type = types.lines;
427 + description = ''
428 + Shell commands executed when the instance is starting.
429 + '';
430 + };
431 + options.script-security = mkOption {
432 + default = 1;
433 + type = types.enum [ 1 2 3 ];
434 + description = ''
435 + - 1 — (Default) Only call built-in executables such as ifconfig, ip, route, or netsh.
436 + - 2 — Allow calling of built-in executables and user-defined scripts.
437 + - 3 — Allow passwords to be passed to scripts via environmental variables (potentially unsafe).
438 + '';
439 + };
440 };
441 + };
442
443 - down = mkOption {
444 - default = "";
445 - type = types.lines;
446 - description = ''
447 - Shell commands executed when the instance is shutting down.
448 - '';
449 - };
450 + autoStart = mkOption {
451 + default = true;
452 + type = types.bool;
453 + description = "Whether this OpenVPN instance should be started automatically.";
454 + };
455
456 - autoStart = mkOption {
457 - default = true;
458 - type = types.bool;
459 - description = "Whether this OpenVPN instance should be started automatically.";
460 - };
461 + # Legacy options
462 + down = (elemAt settings.type.functor.payload.modules 0).options.down;
463 + up = (elemAt settings.type.functor.payload.modules 0).options.up;
464
465 - updateResolvConf = mkOption {
466 - default = false;
467 - type = types.bool;
468 - description = ''
469 - Use the script from the update-resolv-conf package to automatically
470 - update resolv.conf with the DNS information provided by openvpn. The
471 - script will be run after the "up" commands and before the "down" commands.
472 - '';
473 - };
474 + updateResolvConf = mkOption {
475 + default = false;
476 + type = types.bool;
477 + description = ''
478 + Use the script from the update-resolv-conf package to automatically
479 + update resolv.conf with the DNS information provided by openvpn. The
480 + script will be run after the "up" commands and before the "down" commands.
481 + '';
482 + };
483
484 authUserPass = mkOption {
485 default = null;
486 @@ -227,9 +425,39 @@
487 ]
488 );
489 };
490 - };
491 -
492 - });
493 +
494 + netns = mkOption {
495 + default = null;
496 + type = with types; nullOr str;
497 + description = "Network namespace.";
498 + };
499 + };
500 +
501 + config.settings = mkMerge
502 + [
503 + (mkIf (config.netns != null) {
504 + # Useless to setup the interface
505 + # because moving it to the netns will reset it
506 + ifconfig-noexec = true;
507 + route-noexec = true;
508 + script-security = 2;
509 + })
510 + (mkIf (config.authUserPass != null) {
511 + auth-user-pass = pkgs.writeText "openvpn-auth-user-pass-${name}" ''
512 + ${config.authUserPass.username}
513 + ${config.authUserPass.password}
514 + '';
515 + })
516 + (mkIf config.updateResolvConf {
517 + script-security = 2;
518 + })
519 + {
520 + # Aliases legacy options
521 + down = modules.mkAliasAndWrapDefsWithPriority id (options.down or { });
522 + up = modules.mkAliasAndWrapDefsWithPriority id (options.up or { });
523 + }
524 + ];
525 + }));
526
527 };
528
529 @@ -243,14 +471,11 @@
530
531 ###### implementation
532
533 - config = mkIf (cfg.servers != { }) {
534 + config = mkIf (enabledServers != { }) {
535
536 - systemd.services =
537 - (listToAttrs (
538 - mapAttrsToList (
539 - name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)
540 - ) cfg.servers
541 - ))
542 + systemd.services = (listToAttrs (mapAttrsToList
543 + (name: value: nameValuePair "openvpn-${name}"
544 + (makeOpenVPNJob value name)) enabledServers))
545 // restartService;
546
547 environment.systemPackages = [ cfg.package ];