1 diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
2 index e5f9c673c18..e331312bdb7 100644
3 --- a/nixos/modules/module-list.nix
4 +++ b/nixos/modules/module-list.nix
6 ./services/networking/ndppd.nix
7 ./services/networking/nebula.nix
8 ./services/networking/networkmanager.nix
9 + ./services/networking/netns.nix
10 ./services/networking/nextdns.nix
11 ./services/networking/nftables.nix
12 ./services/networking/ngircd.nix
13 diff --git a/nixos/modules/services/networking/netns.nix b/nixos/modules/services/networking/netns.nix
15 index 00000000000..2e62738089d
17 +++ b/nixos/modules/services/networking/netns.nix
19 +{ pkgs, lib, config, options, ... }:
22 + cfg = config.services.netns;
23 + inherit (config) networking;
24 + # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
25 + escapeUnitName = name:
26 + lib.concatMapStrings (s: if lib.isList s then "-" else s)
27 + (builtins.split "[^a-zA-Z0-9_.\\-]+" name);
30 +options.services.netns = {
31 + namespaces = mkOption {
33 + Network namespaces to create.
35 + Other services can join a network namespace named <code>netns</code> with:
37 + PrivateNetwork=true;
38 + JoinsNamespaceOf="netns-''${netns}.service";
41 + So can <literal>iproute</literal> with:
42 + <code>ip -n ''${netns}</code>
45 + You should usually create (or update via your VPN configuration's up script)
46 + a file named <literal>/etc/netns/''${netns}/resolv.conf</literal>
47 + that will be bind-mounted by <code>ip -n ''${netns}</code> onto <literal>/etc/resolv.conf</literal>,
48 + which you'll also want to configure in the services joining this network namespace:
50 + BindReadOnlyPaths = ["/etc/netns/''${netns}/resolv.conf:/etc/resolv.conf"];
55 + type = types.attrsOf (types.submodule {
56 + options.nftables = mkOption {
57 + description = "Nftables ruleset within the network namespace.";
59 + default = networking.nftables.ruleset;
60 + defaultText = "config.networking.nftables.ruleset";
62 + options.sysctl = options.boot.kernel.sysctl // {
63 + description = "sysctl within the network namespace.";
64 + default = config.boot.kernel.sysctl;
65 + defaultText = "config.boot.kernel.sysctl";
67 + options.service = mkOption {
68 + description = "Systemd configuration specific to this netns service";
76 + systemd.services = mapAttrs' (name: c:
77 + nameValuePair "netns-${escapeUnitName name}" (mkMerge [
78 + { description = "${name} network namespace";
79 + before = [ "network.target" ];
82 + RemainAfterExit = true;
83 + # Let systemd create the netns so that PrivateNetwork=true
84 + # with JoinsNamespaceOf="netns-${name}.service" works.
85 + PrivateNetwork = true;
87 + # For convenience, register the netns to the tracking mecanism of iproute,
88 + # and make sure resolv.conf can be used in BindReadOnlyPaths=
89 + # For propagating changes in that file to the services bind mounting it,
90 + # updating must not remove the file, but only truncate it.
91 + (pkgs.writeShellScript "ip-netns-attach" ''
92 + ${pkgs.iproute}/bin/ip netns attach ${escapeShellArg name} $$
93 + mkdir -p /etc/netns/${escapeShellArg name}
94 + touch /etc/netns/${escapeShellArg name}/resolv.conf
97 + # Bringing the loopback interface is almost always a good thing.
98 + "${pkgs.iproute}/bin/ip link set dev lo up"
100 + # Use --ignore because some keys may no longer exist in that new namespace,
101 + # like net.ipv6.conf.eth0.addr_gen_mode or net.core.rmem_max
102 + ''${pkgs.procps}/bin/sysctl --ignore -p ${pkgs.writeScript "sysctl"
103 + (concatStrings (mapAttrsToList (n: v:
104 + optionalString (v != null)
105 + "${n}=${if v == false then "0" else toString v}\n"
109 + # Load the nftables ruleset of this netns.
110 + optional networking.nftables.enable (pkgs.writeScript "nftables-ruleset" ''
111 + #!${pkgs.nftables}/bin/nft -f
115 + # Unregister the netns from the tracking mecanism of iproute.
116 + ExecStop = "${pkgs.iproute}/bin/ip netns delete ${escapeShellArg name}";
122 + meta.maintainers = with lib.maintainers; [ julm ];
125 diff --git a/nixos/modules/services/networking/openvpn.nix b/nixos/modules/services/networking/openvpn.nix
126 index 492a0936fdb..adb1c8b5f0d 100644
127 --- a/nixos/modules/services/networking/openvpn.nix
128 +++ b/nixos/modules/services/networking/openvpn.nix
129 @@ -5,71 +5,216 @@ with lib;
132 cfg = config.services.openvpn;
133 + enabledServers = filterAttrs (name: srv: srv.enable) cfg.servers;
135 inherit (pkgs) openvpn;
137 + PATH = name: makeBinPath config.systemd.services."openvpn-${name}".path;
139 makeOpenVPNJob = cfg: name:
142 - path = makeBinPath (getAttr "openvpn-${name}" config.systemd.services).path;
143 + configFile = pkgs.writeText "openvpn-config-${name}" (
144 + generators.toKeyValue {
145 + mkKeyValue = key: value:
146 + if hasAttr key scripts
147 + then "${key} " + pkgs.writeShellScript "openvpn-${name}-${key}" (scripts.${key} value)
148 + else if builtins.isBool value
149 + then optionalString value key
150 + else if builtins.isPath value
151 + then "${key} ${value}"
152 + else if builtins.isList value
153 + then concatMapStringsSep "\n" (v: "${key} ${generators.mkValueStringDefault {} v}") value
154 + else "${key} ${generators.mkValueStringDefault {} value}";
160 - export PATH=${path}
164 + export PATH=${PATH name}
166 - # For convenience in client scripts, extract the remote domain
167 - # name and name server.
168 - for var in ''${!foreign_option_*}; do
170 - if [ "''${x[0]}" = dhcp-option ]; then
171 - if [ "''${x[1]}" = DOMAIN ]; then domain="''${x[2]}"
172 - elif [ "''${x[1]}" = DNS ]; then nameserver="''${x[2]}"
176 + # For convenience in client scripts, extract the remote domain
177 + # name and name server.
178 + for var in ''${!foreign_option_*}; do
180 + if [ "''${x[0]}" = dhcp-option ]; then
181 + if [ "''${x[1]}" = DOMAIN ]; then domain="''${x[2]}"
182 + elif [ "''${x[1]}" = DNS ]; then nameserver="''${x[2]}"
188 - ${optionalString cfg.updateResolvConf
189 - "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
191 + ${optionalString cfg.updateResolvConf
192 + "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
194 + # Add DNS settings given in foreign DHCP options to the resolv.conf of the netns.
195 + # Note that JoinsNamespaceOf="netns-${cfg.netns}.service" will not
196 + # BindReadOnlyPaths=["/etc/netns/${cfg.netns}/resolv.conf:/etc/resolv.conf"];
197 + # this will have to be added in each service joining the namespace.
198 + setNetNSResolvConf = ''
199 + mkdir -p /etc/netns/${cfg.netns}
200 + # This file is normally created by netns-${cfg.netns}.service,
201 + # care must be taken to not delete it but to truncate it
202 + # in order to propagate the changes to bind-mounted versions.
203 + : > /etc/netns/${cfg.netns}/resolv.conf
204 + chmod 644 /etc/netns/${cfg.netns}/resolv.conf
205 + foreign_opt_domains=
206 + process_foreign_option () {
208 + dhcp-option:DNS) echo "nameserver $3" >>/etc/netns/'${cfg.netns}'/resolv.conf ;;
209 + dhcp-option:DOMAIN) foreign_opt_domains="$foreign_opt_domains $3" ;;
214 + eval opt=\"\''${foreign_option_$i-}\"
217 + process_foreign_option $opt
220 + for d in $foreign_opt_domains; do
221 + printf '%s\n' "domain $1" "search $*" \
222 + >>/etc/netns/'${cfg.netns}'/resolv.conf
226 + if cfg.netns == null
232 + export PATH=${PATH name}
234 + ${setNetNSResolvConf}
235 + ip link set dev '${cfg.settings.dev}' up netns '${cfg.netns}' mtu "$tun_mtu"
236 + ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-up-netns.sh" ''
239 + export PATH=${PATH name}
243 - export PATH=${path}
244 - ${optionalString cfg.updateResolvConf
245 - "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
248 + ip link set dev lo up
250 - configFile = pkgs.writeText "openvpn-config-${name}"
253 - ${optionalString (cfg.up != "" || cfg.down != "" || cfg.updateResolvConf) "script-security 2"}
255 - ${optionalString (cfg.up != "" || cfg.updateResolvConf)
256 - "up ${pkgs.writeScript "openvpn-${name}-up" upScript}"}
257 - ${optionalString (cfg.down != "" || cfg.updateResolvConf)
258 - "down ${pkgs.writeScript "openvpn-${name}-down" downScript}"}
259 - ${optionalString (cfg.authUserPass != null)
260 - "auth-user-pass ${pkgs.writeText "openvpn-credentials-${name}" ''
261 - ${cfg.authUserPass.username}
262 - ${cfg.authUserPass.password}
265 + netmask4="''${ifconfig_netmask:-30}"
266 + netbits6="''${ifconfig_ipv6_netbits:-112}"
267 + if [ -n "''${ifconfig_local-}" ]; then
268 + if [ -n "''${ifconfig_remote-}" ]; then
269 + ip -4 addr replace \
270 + local "$ifconfig_local" \
271 + peer "$ifconfig_remote/$netmask4" \
272 + ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
273 + dev '${cfg.settings.dev}'
275 + ip -4 addr replace \
276 + local "$ifconfig_local/$netmask4" \
277 + ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
278 + dev '${cfg.settings.dev}'
281 + if [ -n "''${ifconfig_ipv6_local-}" ]; then
282 + if [ -n "''${ifconfig_ipv6_remote-}" ]; then
283 + ip -6 addr replace \
284 + local "$ifconfig_ipv6_local" \
285 + peer "$ifconfig_ipv6_remote/$netbits6" \
286 + dev '${cfg.settings.dev}'
288 + ip -6 addr replace \
289 + local "$ifconfig_ipv6_local/$netbits6" \
290 + dev '${cfg.settings.dev}'
298 + if cfg.netns == null
301 + export PATH=${PATH name}
303 + ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-route-up-netns" ''
304 + export PATH=${PATH name}
308 + eval net=\"\''${route_network_$i-}\"
309 + eval mask=\"\''${route_netmask_$i-}\"
310 + eval gw=\"\''${route_gateway_$i-}\"
311 + eval mtr=\"\''${route_metric_$i-}\"
314 + ip -4 route replace "$net/$mask" via "$gw" ''${mtr:+metric "$mtr"}
318 + if [ -n "''${route_vpn_gateway-}" ]; then
319 + ip -4 route replace default via "$route_vpn_gateway"
324 + # There doesn't seem to be $route_ipv6_metric_<n>
325 + # according to the manpage.
326 + eval net=\"\''${route_ipv6_network_$i-}\"
327 + eval gw=\"\''${route_ipv6_gateway_$i-}\"
330 + ip -6 route replace "$net" via "$gw" metric 100
334 + # There's no $route_vpn_gateway for IPv6. It's not
335 + # documented if OpenVPN includes default route in
336 + # $route_ipv6_*. Set default route to remote VPN
337 + # endpoint address if there is one. Use higher metric
338 + # than $route_ipv6_* routes to give preference to a
339 + # possible default route in them.
340 + if [ -n "''${ifconfig_ipv6_remote-}" ]; then
341 + ip -6 route replace default \
342 + via "$ifconfig_ipv6_remote" metric 200
349 + export PATH=${PATH name}
350 + ${optionalString cfg.updateResolvConf
351 + "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
353 + if cfg.netns == null
359 + export PATH=${PATH name}
360 + ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-down-netns.sh" ''
364 + rm -f /etc/netns/'${cfg.netns}'/resolv.conf
369 description = "OpenVPN instance ‘${name}’";
371 wantedBy = optional cfg.autoStart "multi-user.target";
372 after = [ "network.target" ];
373 + bindsTo = optional (cfg.netns != null) "netns-${cfg.netns}.service";
374 + requires = optional (cfg.netns != null) "netns-${cfg.netns}.service";
376 path = [ pkgs.iptables pkgs.iproute2 pkgs.nettools ];
378 serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
379 serviceConfig.Restart = "always";
380 + serviceConfig.RestartSec = "5s";
381 serviceConfig.Type = "notify";
387 @@ -87,30 +232,30 @@ in
388 example = literalExpression ''
393 # Simplest server configuration: https://community.openvpn.net/openvpn/wiki/StaticKeyMiniHowto
396 - ifconfig 10.8.0.1 10.8.0.2
397 - secret /root/static.key
399 - up = "ip route add ...";
400 - down = "ip route del ...";
402 + ifconfig = "10.8.0.1 10.8.0.2";
403 + secret = "/root/static.key";
404 + up = "ip route add ...";
405 + down = "ip route del ...";
412 - remote vpn.example.org
416 - ca /root/.vpn/ca.crt
417 - cert /root/.vpn/alice.crt
418 - key /root/.vpn/alice.key
420 - up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
421 - down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
424 + remote = "vpn.example.org";
426 + proto = "tcp-client";
428 + ca = "/root/.vpn/ca.crt";
429 + cert = "/root/.vpn/alice.crt";
430 + key = "/root/.vpn/alice.key";
431 + up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
432 + down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
437 @@ -124,36 +269,77 @@ in
441 - type = with types; attrsOf (submodule {
442 + type = with types; attrsOf (submodule ({name, config, options, ...}: {
446 + enable = mkEnableOption "OpenVPN server" // { default = true; };
448 - config = mkOption {
449 - type = types.lines;
450 + settings = mkOption {
451 description = lib.mdDoc ''
452 Configuration of this OpenVPN instance. See
453 {manpage}`openvpn(8)`
456 To import an external config file, use the following definition:
457 - `config = "config /path/to/config.ovpn"`
463 - type = types.lines;
464 - description = lib.mdDoc ''
465 - Shell commands executed when the instance is starting.
471 - type = types.lines;
472 - description = lib.mdDoc ''
473 - Shell commands executed when the instance is shutting down.
474 + <literal>config = /path/to/config.ovpn;</literal>
477 + type = types.submodule {
478 + freeformType = with types;
483 + (listOf (oneOf [bool int str path]))
487 + options.dev = mkOption {
490 + description = lib.mkDoc ''
491 + Shell commands executed when the instance is starting.
494 + options.down = mkOption {
496 + type = types.lines;
497 + description = lib.mkDoc ''
498 + Shell commands executed when the instance is shutting down.
501 + options.errors-to-stderr = mkOption {
504 + description = lib.mkDoc ''
505 + Output errors to stderr instead of stdout
506 + unless log output is redirected by one of the <code>--log</code> options.
509 + options.route-up = mkOption {
511 + type = types.lines;
512 + description = lib.mkDoc ''
513 + Run command after routes are added.
516 + options.up = mkOption {
518 + type = types.lines;
519 + description = lib.mkDoc ''
520 + Shell commands executed when the instance is starting.
523 + options.script-security = mkOption {
525 + type = types.enum [1 2 3];
526 + description = lib.mkDoc ''
527 + 1 — (Default) Only call built-in executables such as ifconfig, ip, route, or netsh.
528 + 2 — Allow calling of built-in executables and user-defined scripts.
529 + 3 — Allow passwords to be passed to scripts via environmental variables (potentially unsafe).
535 autoStart = mkOption {
536 @@ -162,6 +348,10 @@ in
537 description = lib.mdDoc "Whether this OpenVPN instance should be started automatically.";
541 + down = (elemAt settings.type.functor.payload.modules 0).options.down;
542 + up = (elemAt settings.type.functor.payload.modules 0).options.up;
544 updateResolvConf = mkOption {
547 @@ -195,9 +385,39 @@ in
554 + type = with types; nullOr str;
555 + description = lib.mkDoc "Network namespace.";
560 + config.settings = mkMerge
561 + [ (mkIf (config.netns != null) {
562 + # Useless to setup the interface
563 + # because moving it to the netns will reset it
564 + ifconfig-noexec = true;
565 + route-noexec = true;
566 + script-security = 2;
568 + (mkIf (config.authUserPass != null) {
569 + auth-user-pass = pkgs.writeText "openvpn-auth-user-pass-${name}" ''
570 + ${config.authUserPass.username}
571 + ${config.authUserPass.password}
574 + (mkIf config.updateResolvConf {
575 + script-security = 2;
577 + { # Aliases legacy options
578 + down = mkAliasDefinitions (mkDefault options.down or { });
579 + up = mkAliasDefinitions (mkDefault options.up or { });
588 @@ -206,9 +426,9 @@ in
590 ###### implementation
592 - config = mkIf (cfg.servers != {}) {
593 + config = mkIf (enabledServers != {}) {
595 - systemd.services = listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) cfg.servers);
596 + systemd.services = listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) enabledServers);
598 environment.systemPackages = [ openvpn ];