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