diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index e5f9c673c18..e331312bdb7 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -862,6 +862,7 @@
./services/networking/ndppd.nix
./services/networking/nebula.nix
./services/networking/networkmanager.nix
+ ./services/networking/netns.nix
./services/networking/nextdns.nix
./services/networking/nftables.nix
./services/networking/ngircd.nix
diff --git a/nixos/modules/services/networking/netns.nix b/nixos/modules/services/networking/netns.nix
new file mode 100644
index 00000000000..2e62738089d
--- /dev/null
+++ b/nixos/modules/services/networking/netns.nix
@@ -0,0 +1,106 @@
+{ pkgs, lib, config, options, ... }:
+with lib;
+let
+ cfg = config.services.netns;
+ inherit (config) networking;
+ # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
+ escapeUnitName = name:
+ lib.concatMapStrings (s: if lib.isList s then "-" else s)
+ (builtins.split "[^a-zA-Z0-9_.\\-]+" name);
+in
+{
+options.services.netns = {
+ namespaces = mkOption {
+ description = ''
+ Network namespaces to create.
+
+ Other services can join a network namespace named netns
with:
+
+ PrivateNetwork=true;
+ JoinsNamespaceOf="netns-''${netns}.service";
+
+
+ So can iproute with:
+ ip -n ''${netns}
+
+
+ You should usually create (or update via your VPN configuration's up script)
+ a file named /etc/netns/''${netns}/resolv.conf
+ that will be bind-mounted by ip -n ''${netns}
onto /etc/resolv.conf,
+ which you'll also want to configure in the services joining this network namespace:
+
+ BindReadOnlyPaths = ["/etc/netns/''${netns}/resolv.conf:/etc/resolv.conf"];
+
+
+ '';
+ default = {};
+ type = types.attrsOf (types.submodule {
+ options.nftables = mkOption {
+ description = "Nftables ruleset within the network namespace.";
+ type = types.lines;
+ default = networking.nftables.ruleset;
+ defaultText = "config.networking.nftables.ruleset";
+ };
+ options.sysctl = options.boot.kernel.sysctl // {
+ description = "sysctl within the network namespace.";
+ default = config.boot.kernel.sysctl;
+ defaultText = "config.boot.kernel.sysctl";
+ };
+ options.service = mkOption {
+ description = "Systemd configuration specific to this netns service";
+ type = types.attrs;
+ default = {};
+ };
+ });
+ };
+};
+config = {
+ systemd.services = mapAttrs' (name: c:
+ nameValuePair "netns-${escapeUnitName name}" (mkMerge [
+ { description = "${name} network namespace";
+ before = [ "network.target" ];
+ serviceConfig = {
+ Type = "oneshot";
+ RemainAfterExit = true;
+ # Let systemd create the netns so that PrivateNetwork=true
+ # with JoinsNamespaceOf="netns-${name}.service" works.
+ PrivateNetwork = true;
+ ExecStart = [
+ # For convenience, register the netns to the tracking mecanism of iproute,
+ # and make sure resolv.conf can be used in BindReadOnlyPaths=
+ # For propagating changes in that file to the services bind mounting it,
+ # updating must not remove the file, but only truncate it.
+ (pkgs.writeShellScript "ip-netns-attach" ''
+ ${pkgs.iproute}/bin/ip netns attach ${escapeShellArg name} $$
+ mkdir -p /etc/netns/${escapeShellArg name}
+ touch /etc/netns/${escapeShellArg name}/resolv.conf
+ '')
+
+ # Bringing the loopback interface is almost always a good thing.
+ "${pkgs.iproute}/bin/ip link set dev lo up"
+
+ # Use --ignore because some keys may no longer exist in that new namespace,
+ # like net.ipv6.conf.eth0.addr_gen_mode or net.core.rmem_max
+ ''${pkgs.procps}/bin/sysctl --ignore -p ${pkgs.writeScript "sysctl"
+ (concatStrings (mapAttrsToList (n: v:
+ optionalString (v != null)
+ "${n}=${if v == false then "0" else toString v}\n"
+ ) c.sysctl))}
+ ''
+ ] ++
+ # Load the nftables ruleset of this netns.
+ optional networking.nftables.enable (pkgs.writeScript "nftables-ruleset" ''
+ #!${pkgs.nftables}/bin/nft -f
+ flush ruleset
+ ${c.nftables}
+ '');
+ # Unregister the netns from the tracking mecanism of iproute.
+ ExecStop = "${pkgs.iproute}/bin/ip netns delete ${escapeShellArg name}";
+ };
+ }
+ c.service
+ ]
+ )) cfg.namespaces;
+ meta.maintainers = with lib.maintainers; [ julm ];
+};
+}
diff --git a/nixos/modules/services/networking/openvpn.nix b/nixos/modules/services/networking/openvpn.nix
index 492a0936fdb..adb1c8b5f0d 100644
--- a/nixos/modules/services/networking/openvpn.nix
+++ b/nixos/modules/services/networking/openvpn.nix
@@ -5,71 +5,216 @@ with lib;
let
cfg = config.services.openvpn;
+ enabledServers = filterAttrs (name: srv: srv.enable) cfg.servers;
inherit (pkgs) openvpn;
+ PATH = name: makeBinPath config.systemd.services."openvpn-${name}".path;
+
makeOpenVPNJob = cfg: name:
let
- path = makeBinPath (getAttr "openvpn-${name}" config.systemd.services).path;
+ configFile = pkgs.writeText "openvpn-config-${name}" (
+ generators.toKeyValue {
+ mkKeyValue = key: value:
+ if hasAttr key scripts
+ then "${key} " + pkgs.writeShellScript "openvpn-${name}-${key}" (scripts.${key} value)
+ else if builtins.isBool value
+ then optionalString value key
+ else if builtins.isPath value
+ then "${key} ${value}"
+ else if builtins.isList value
+ then concatMapStringsSep "\n" (v: "${key} ${generators.mkValueStringDefault {} v}") value
+ else "${key} ${generators.mkValueStringDefault {} value}";
+ } cfg.settings
+ );
- upScript = ''
- #! /bin/sh
- export PATH=${path}
+ scripts = {
+ up = script:
+ let init = ''
+ export PATH=${PATH name}
- # For convenience in client scripts, extract the remote domain
- # name and name server.
- for var in ''${!foreign_option_*}; do
- x=(''${!var})
- if [ "''${x[0]}" = dhcp-option ]; then
- if [ "''${x[1]}" = DOMAIN ]; then domain="''${x[2]}"
- elif [ "''${x[1]}" = DNS ]; then nameserver="''${x[2]}"
- fi
- fi
- done
+ # For convenience in client scripts, extract the remote domain
+ # name and name server.
+ for var in ''${!foreign_option_*}; do
+ x=(''${!var})
+ if [ "''${x[0]}" = dhcp-option ]; then
+ if [ "''${x[1]}" = DOMAIN ]; then domain="''${x[2]}"
+ elif [ "''${x[1]}" = DNS ]; then nameserver="''${x[2]}"
+ fi
+ fi
+ done
- ${cfg.up}
- ${optionalString cfg.updateResolvConf
- "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
- '';
+ ${optionalString cfg.updateResolvConf
+ "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
+ '';
+ # Add DNS settings given in foreign DHCP options to the resolv.conf of the netns.
+ # Note that JoinsNamespaceOf="netns-${cfg.netns}.service" will not
+ # BindReadOnlyPaths=["/etc/netns/${cfg.netns}/resolv.conf:/etc/resolv.conf"];
+ # this will have to be added in each service joining the namespace.
+ setNetNSResolvConf = ''
+ mkdir -p /etc/netns/${cfg.netns}
+ # This file is normally created by netns-${cfg.netns}.service,
+ # care must be taken to not delete it but to truncate it
+ # in order to propagate the changes to bind-mounted versions.
+ : > /etc/netns/${cfg.netns}/resolv.conf
+ chmod 644 /etc/netns/${cfg.netns}/resolv.conf
+ foreign_opt_domains=
+ process_foreign_option () {
+ case "$1:$2" in
+ dhcp-option:DNS) echo "nameserver $3" >>/etc/netns/'${cfg.netns}'/resolv.conf ;;
+ dhcp-option:DOMAIN) foreign_opt_domains="$foreign_opt_domains $3" ;;
+ esac
+ }
+ i=1
+ while
+ eval opt=\"\''${foreign_option_$i-}\"
+ [ -n "$opt" ]
+ do
+ process_foreign_option $opt
+ i=$(( i + 1 ))
+ done
+ for d in $foreign_opt_domains; do
+ printf '%s\n' "domain $1" "search $*" \
+ >>/etc/netns/'${cfg.netns}'/resolv.conf
+ done
+ '';
+ in
+ if cfg.netns == null
+ then ''
+ ${init}
+ ${script}
+ ''
+ else ''
+ export PATH=${PATH name}
+ set -eux
+ ${setNetNSResolvConf}
+ ip link set dev '${cfg.settings.dev}' up netns '${cfg.netns}' mtu "$tun_mtu"
+ ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-up-netns.sh" ''
+ ${init}
+ set -eux
+ export PATH=${PATH name}
- downScript = ''
- #! /bin/sh
- export PATH=${path}
- ${optionalString cfg.updateResolvConf
- "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
- ${cfg.down}
- '';
+ ip link set dev lo up
- configFile = pkgs.writeText "openvpn-config-${name}"
- ''
- errors-to-stderr
- ${optionalString (cfg.up != "" || cfg.down != "" || cfg.updateResolvConf) "script-security 2"}
- ${cfg.config}
- ${optionalString (cfg.up != "" || cfg.updateResolvConf)
- "up ${pkgs.writeScript "openvpn-${name}-up" upScript}"}
- ${optionalString (cfg.down != "" || cfg.updateResolvConf)
- "down ${pkgs.writeScript "openvpn-${name}-down" downScript}"}
- ${optionalString (cfg.authUserPass != null)
- "auth-user-pass ${pkgs.writeText "openvpn-credentials-${name}" ''
- ${cfg.authUserPass.username}
- ${cfg.authUserPass.password}
- ''}"}
- '';
+ netmask4="''${ifconfig_netmask:-30}"
+ netbits6="''${ifconfig_ipv6_netbits:-112}"
+ if [ -n "''${ifconfig_local-}" ]; then
+ if [ -n "''${ifconfig_remote-}" ]; then
+ ip -4 addr replace \
+ local "$ifconfig_local" \
+ peer "$ifconfig_remote/$netmask4" \
+ ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
+ dev '${cfg.settings.dev}'
+ else
+ ip -4 addr replace \
+ local "$ifconfig_local/$netmask4" \
+ ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
+ dev '${cfg.settings.dev}'
+ fi
+ fi
+ if [ -n "''${ifconfig_ipv6_local-}" ]; then
+ if [ -n "''${ifconfig_ipv6_remote-}" ]; then
+ ip -6 addr replace \
+ local "$ifconfig_ipv6_local" \
+ peer "$ifconfig_ipv6_remote/$netbits6" \
+ dev '${cfg.settings.dev}'
+ else
+ ip -6 addr replace \
+ local "$ifconfig_ipv6_local/$netbits6" \
+ dev '${cfg.settings.dev}'
+ fi
+ fi
+ set +eux
+ ${script}
+ ''}
+ '';
+ route-up = script:
+ if cfg.netns == null
+ then script
+ else ''
+ export PATH=${PATH name}
+ set -eux
+ ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-route-up-netns" ''
+ export PATH=${PATH name}
+ set -eux
+ i=1
+ while
+ eval net=\"\''${route_network_$i-}\"
+ eval mask=\"\''${route_netmask_$i-}\"
+ eval gw=\"\''${route_gateway_$i-}\"
+ eval mtr=\"\''${route_metric_$i-}\"
+ [ -n "$net" ]
+ do
+ ip -4 route replace "$net/$mask" via "$gw" ''${mtr:+metric "$mtr"}
+ i=$(( i + 1 ))
+ done
+
+ if [ -n "''${route_vpn_gateway-}" ]; then
+ ip -4 route replace default via "$route_vpn_gateway"
+ fi
+
+ i=1
+ while
+ # There doesn't seem to be $route_ipv6_metric_
+ # according to the manpage.
+ eval net=\"\''${route_ipv6_network_$i-}\"
+ eval gw=\"\''${route_ipv6_gateway_$i-}\"
+ [ -n "$net" ]
+ do
+ ip -6 route replace "$net" via "$gw" metric 100
+ i=$(( i + 1 ))
+ done
+
+ # There's no $route_vpn_gateway for IPv6. It's not
+ # documented if OpenVPN includes default route in
+ # $route_ipv6_*. Set default route to remote VPN
+ # endpoint address if there is one. Use higher metric
+ # than $route_ipv6_* routes to give preference to a
+ # possible default route in them.
+ if [ -n "''${ifconfig_ipv6_remote-}" ]; then
+ ip -6 route replace default \
+ via "$ifconfig_ipv6_remote" metric 200
+ fi
+ ${script}
+ ''}
+ '';
+ down = script:
+ let init = ''
+ export PATH=${PATH name}
+ ${optionalString cfg.updateResolvConf
+ "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
+ ''; in
+ if cfg.netns == null
+ then ''
+ ${init}
+ ${script}
+ ''
+ else ''
+ export PATH=${PATH name}
+ ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-down-netns.sh" ''
+ ${init}
+ ${script}
+ ''}
+ rm -f /etc/netns/'${cfg.netns}'/resolv.conf
+ '';
+ };
in {
description = "OpenVPN instance ‘${name}’";
wantedBy = optional cfg.autoStart "multi-user.target";
after = [ "network.target" ];
+ bindsTo = optional (cfg.netns != null) "netns-${cfg.netns}.service";
+ requires = optional (cfg.netns != null) "netns-${cfg.netns}.service";
path = [ pkgs.iptables pkgs.iproute2 pkgs.nettools ];
serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
serviceConfig.Restart = "always";
+ serviceConfig.RestartSec = "5s";
serviceConfig.Type = "notify";
};
-
in
{
@@ -87,30 +232,30 @@ in
example = literalExpression ''
{
server = {
- config = '''
+ settings = {
# Simplest server configuration: https://community.openvpn.net/openvpn/wiki/StaticKeyMiniHowto
# server :
- dev tun
- ifconfig 10.8.0.1 10.8.0.2
- secret /root/static.key
- ''';
- up = "ip route add ...";
- down = "ip route del ...";
+ dev = "tun";
+ ifconfig = "10.8.0.1 10.8.0.2";
+ secret = "/root/static.key";
+ up = "ip route add ...";
+ down = "ip route del ...";
+ };
};
client = {
- config = '''
- client
- remote vpn.example.org
- dev tun
- proto tcp-client
- port 8080
- ca /root/.vpn/ca.crt
- cert /root/.vpn/alice.crt
- key /root/.vpn/alice.key
- ''';
- up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
- down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
+ settings = {
+ client = true;
+ remote = "vpn.example.org";
+ dev = "tun";
+ proto = "tcp-client";
+ port = 8080;
+ ca = "/root/.vpn/ca.crt";
+ cert = "/root/.vpn/alice.crt";
+ key = "/root/.vpn/alice.key";
+ up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
+ down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
+ };
};
}
'';
@@ -124,36 +269,77 @@ in
attribute name.
'';
- type = with types; attrsOf (submodule {
+ type = with types; attrsOf (submodule ({name, config, options, ...}: {
- options = {
+ options = rec {
+ enable = mkEnableOption "OpenVPN server" // { default = true; };
- config = mkOption {
- type = types.lines;
+ settings = mkOption {
description = lib.mdDoc ''
Configuration of this OpenVPN instance. See
{manpage}`openvpn(8)`
for details.
To import an external config file, use the following definition:
- `config = "config /path/to/config.ovpn"`
- '';
- };
-
- up = mkOption {
- default = "";
- type = types.lines;
- description = lib.mdDoc ''
- Shell commands executed when the instance is starting.
- '';
- };
-
- down = mkOption {
- default = "";
- type = types.lines;
- description = lib.mdDoc ''
- Shell commands executed when the instance is shutting down.
+ config = /path/to/config.ovpn;
'';
+ default = {};
+ type = types.submodule {
+ freeformType = with types;
+ attrsOf (
+ nullOr (
+ oneOf [
+ bool int str path
+ (listOf (oneOf [bool int str path]))
+ ]
+ )
+ );
+ options.dev = mkOption {
+ default = null;
+ type = types.str;
+ description = lib.mkDoc ''
+ Shell commands executed when the instance is starting.
+ '';
+ };
+ options.down = mkOption {
+ default = "";
+ type = types.lines;
+ description = lib.mkDoc ''
+ Shell commands executed when the instance is shutting down.
+ '';
+ };
+ options.errors-to-stderr = mkOption {
+ default = true;
+ type = types.bool;
+ description = lib.mkDoc ''
+ Output errors to stderr instead of stdout
+ unless log output is redirected by one of the --log
options.
+ '';
+ };
+ options.route-up = mkOption {
+ default = "";
+ type = types.lines;
+ description = lib.mkDoc ''
+ Run command after routes are added.
+ '';
+ };
+ options.up = mkOption {
+ default = "";
+ type = types.lines;
+ description = lib.mkDoc ''
+ Shell commands executed when the instance is starting.
+ '';
+ };
+ options.script-security = mkOption {
+ default = 1;
+ type = types.enum [1 2 3];
+ description = lib.mkDoc ''
+ 1 — (Default) Only call built-in executables such as ifconfig, ip, route, or netsh.
+ 2 — Allow calling of built-in executables and user-defined scripts.
+ 3 — Allow passwords to be passed to scripts via environmental variables (potentially unsafe).
+ '';
+ };
+ };
};
autoStart = mkOption {
@@ -162,6 +348,10 @@ in
description = lib.mdDoc "Whether this OpenVPN instance should be started automatically.";
};
+ # Legacy options
+ down = (elemAt settings.type.functor.payload.modules 0).options.down;
+ up = (elemAt settings.type.functor.payload.modules 0).options.up;
+
updateResolvConf = mkOption {
default = false;
type = types.bool;
@@ -195,9 +385,39 @@ in
};
});
};
+
+ netns = mkOption {
+ default = true;
+ type = with types; nullOr str;
+ description = lib.mkDoc "Network namespace.";
+ };
};
- });
+ config.settings = mkMerge
+ [ (mkIf (config.netns != null) {
+ # Useless to setup the interface
+ # because moving it to the netns will reset it
+ ifconfig-noexec = true;
+ route-noexec = true;
+ script-security = 2;
+ })
+ (mkIf (config.authUserPass != null) {
+ auth-user-pass = pkgs.writeText "openvpn-auth-user-pass-${name}" ''
+ ${config.authUserPass.username}
+ ${config.authUserPass.password}
+ '';
+ })
+ (mkIf config.updateResolvConf {
+ script-security = 2;
+ })
+ { # Aliases legacy options
+ down = mkAliasDefinitions (mkDefault options.down or { });
+ up = mkAliasDefinitions (mkDefault options.up or { });
+ }
+ ];
+
+
+ }));
};
@@ -206,9 +426,9 @@ in
###### implementation
- config = mkIf (cfg.servers != {}) {
+ config = mkIf (enabledServers != {}) {
- systemd.services = listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) cfg.servers);
+ systemd.services = listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) enabledServers);
environment.systemPackages = [ openvpn ];