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
 
   7  nixos/modules/services/networking/openvpn.nix | 509 +++++++++++++-----
 
   8  1 file changed, 382 insertions(+), 127 deletions(-)
 
  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;
 
  17    cfg = config.services.openvpn;
 
  18 +  enabledServers = filterAttrs (name: srv: srv.enable) cfg.servers;
 
  20    inherit (pkgs) openvpn;
 
  22 +  PATH = name: makeBinPath config.systemd.services."openvpn-${name}".path;
 
  28 -      path = makeBinPath (getAttr "openvpn-${name}" config.systemd.services).path;
 
  29 +      configFile = pkgs.writeText "openvpn-config-${name}" (
 
  30 +        generators.toKeyValue {
 
  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
 
  39 +            else if builtins.isList value then
 
  40 +              concatMapStringsSep "\n" (v: "${key} ${generators.mkValueStringDefault { } v}") value
 
  42 +              "${key} ${generators.mkValueStringDefault { } value}";
 
  53 +              export PATH=${PATH name}
 
  55 -        # For convenience in client scripts, extract the remote domain
 
  56 -        # name and name server.
 
  57 -        for var in ''${!foreign_option_*}; do
 
  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]}"
 
  65 +              # For convenience in client scripts, extract the remote domain
 
  66 +              # name and name server.
 
  67 +              for var in ''${!foreign_option_*}; do
 
  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]}"
 
  77 -        ${optionalString cfg.updateResolvConf "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
 
  79 +              ${optionalString cfg.updateResolvConf "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
 
  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 () {
 
  95 +                  dhcp-option:DNS) echo "nameserver $3" >>/etc/netns/'${cfg.netns}'/resolv.conf ;;
 
  96 +                  dhcp-option:DOMAIN) foreign_opt_domains="$foreign_opt_domains $3" ;;
 
 101 +                eval opt=\"\''${foreign_option_$i-}\"
 
 104 +                process_foreign_option $opt
 
 107 +              for d in $foreign_opt_domains; do
 
 108 +                printf '%s\n' "domain $1" "search $*" \
 
 109 +                  >>/etc/netns/'${cfg.netns}'/resolv.conf
 
 113 +          if cfg.netns == null then
 
 120 +              export PATH=${PATH name}
 
 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" ''
 
 127 +                export PATH=${PATH name}
 
 130 -        export PATH=${path}
 
 131 -        ${optionalString cfg.updateResolvConf "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
 
 134 +                ip link set dev lo up
 
 136 -      configFile = pkgs.writeText "openvpn-config-${name}" ''
 
 138 -        ${optionalString (cfg.up != "" || cfg.down != "" || cfg.updateResolvConf) "script-security 2"}
 
 141 -          cfg.up != "" || cfg.updateResolvConf
 
 142 -        ) "up ${pkgs.writeShellScript "openvpn-${name}-up" upScript}"}
 
 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}
 
 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}'
 
 163 +                    ip -4 addr replace \
 
 164 +                      local "$ifconfig_local/$netmask4" \
 
 165 +                      ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
 
 166 +                      dev '${cfg.settings.dev}'
 
 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}'
 
 176 +                    ip -6 addr replace \
 
 177 +                      local "$ifconfig_ipv6_local/$netbits6" \
 
 178 +                      dev '${cfg.settings.dev}'
 
 187 +          if cfg.netns == null then
 
 191 +              export PATH=${PATH name}
 
 193 +              ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-route-up-netns" ''
 
 194 +                export PATH=${PATH name}
 
 198 +                  eval net=\"\''${route_network_$i-}\"
 
 199 +                  eval mask=\"\''${route_netmask_$i-}\"
 
 200 +                  eval gw=\"\''${route_gateway_$i-}\"
 
 201 +                  eval mtr=\"\''${route_metric_$i-}\"
 
 204 +                  ip -4 route replace "$net/$mask" via "$gw" ''${mtr:+metric "$mtr"}
 
 208 +                if [ -n "''${route_vpn_gateway-}" ]; then
 
 209 +                  ip -4 route replace default via "$route_vpn_gateway"
 
 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-}\"
 
 220 +                  ip -6 route replace  "$net"  via "$gw"  metric 100
 
 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
 
 241 +              export PATH=${PATH name}
 
 242 +              ${optionalString cfg.updateResolvConf "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
 
 245 +          if cfg.netns == null then
 
 252 +              export PATH=${PATH name}
 
 253 +              ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-down-netns.sh" ''
 
 257 +              rm -f /etc/netns/'${cfg.netns}'/resolv.conf
 
 263 @@ -67,6 +221,8 @@ let
 
 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";
 
 272 @@ -76,6 +232,7 @@ let
 
 274        serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
 
 275        serviceConfig.Restart = "always";
 
 276 +      serviceConfig.RestartSec = "5s";
 
 277        serviceConfig.Type = "notify";
 
 280 @@ -109,30 +266,30 @@ in
 
 281        example = literalExpression ''
 
 286                # Simplest server configuration: https://community.openvpn.net/openvpn/wiki/StaticKeyMiniHowto
 
 289 -              ifconfig 10.8.0.1 10.8.0.2
 
 290 -              secret /root/static.key
 
 292 -            up = "ip route add ...";
 
 293 -            down = "ip route del ...";
 
 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 ...";
 
 305 -              remote vpn.example.org
 
 309 -              ca /root/.vpn/ca.crt
 
 310 -              cert /root/.vpn/alice.crt
 
 311 -              key /root/.vpn/alice.key
 
 313 -            up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
 
 314 -            down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
 
 317 +              remote = "vpn.example.org";
 
 319 +              proto = "tcp-client";
 
 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";
 
 330 @@ -148,82 +305,180 @@ in
 
 334 -        attrsOf (submodule {
 
 347 +                enable = mkEnableOption "OpenVPN server" // {
 
 351 -            config = mkOption {
 
 352 -              type = types.lines;
 
 354 -                Configuration of this OpenVPN instance.  See
 
 355 -                {manpage}`openvpn(8)`
 
 357 +                settings = mkOption {
 
 359 +                    Configuration of this OpenVPN instance.  See
 
 360 +                    {manpage}`openvpn(8)`
 
 363 -                To import an external config file, use the following definition:
 
 364 -                `config = "config /path/to/config.ovpn"`
 
 370 -              type = types.lines;
 
 372 -                Shell commands executed when the instance is starting.
 
 378 -              type = types.lines;
 
 380 -                Shell commands executed when the instance is shutting down.
 
 384 -            autoStart = mkOption {
 
 387 -              description = "Whether this OpenVPN instance should be started automatically.";
 
 390 -            updateResolvConf = mkOption {
 
 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.
 
 400 -            authUserPass = mkOption {
 
 403 -                This option can be used to store the username / password credentials
 
 404 -                with the "auth-user-pass" authentication method.
 
 406 -                WARNING: Using this option will put the credentials WORLD-READABLE in the Nix store!
 
 408 -              type = types.nullOr (
 
 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;
 
 418 +                  type = types.submodule {
 
 435 +                    options.dev = mkOption {
 
 439 +                        Shell commands executed when the instance is starting.
 
 443 -                    password = mkOption {
 
 444 -                      description = "The password to store inside the credentials file.";
 
 446 +                    options.down = mkOption {
 
 448 +                      type = types.lines;
 
 450 +                        Shell commands executed when the instance is shutting down.
 
 453 +                    options.errors-to-stderr = mkOption {
 
 457 +                        Output errors to stderr instead of stdout
 
 458 +                        unless log output is redirected by one of the `--log` options.
 
 461 +                    options.route-up = mkOption {
 
 463 +                      type = types.lines;
 
 465 +                        Run command after routes are added.
 
 468 +                    options.up = mkOption {
 
 470 +                      type = types.lines;
 
 472 +                        Shell commands executed when the instance is starting.
 
 475 +                    options.script-security = mkOption {
 
 477 +                      type = types.enum [
 
 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).
 
 496 +                autoStart = mkOption {
 
 499 +                  description = "Whether this OpenVPN instance should be started automatically.";
 
 503 +                down = (elemAt settings.type.functor.payload.modules 0).options.down;
 
 504 +                up = (elemAt settings.type.functor.payload.modules 0).options.up;
 
 506 +                updateResolvConf = mkOption {
 
 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.
 
 516 +                authUserPass = mkOption {
 
 519 +                    This option can be used to store the username / password credentials
 
 520 +                    with the "auth-user-pass" authentication method.
 
 522 +                    WARNING: Using this option will put the credentials WORLD-READABLE in the Nix store!
 
 524 +                  type = types.nullOr (
 
 528 +                        username = mkOption {
 
 529 +                          description = "The username to store inside the credentials file.";
 
 533 +                        password = mkOption {
 
 534 +                          description = "The password to store inside the credentials file.";
 
 544 +                  type = with types; nullOr str;
 
 545 +                  description = "Network namespace.";
 
 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;
 
 557 +                (mkIf (config.authUserPass != null) {
 
 558 +                  auth-user-pass = pkgs.writeText "openvpn-auth-user-pass-${name}" ''
 
 559 +                    ${config.authUserPass.username}
 
 560 +                    ${config.authUserPass.password}
 
 563 +                (mkIf config.updateResolvConf {
 
 564 +                  script-security = 2;
 
 567 +                  # Aliases legacy options
 
 568 +                  down = modules.mkAliasAndWrapDefsWithPriority id (options.down or { });
 
 569 +                  up = modules.mkAliasAndWrapDefsWithPriority id (options.up or { });
 
 579 @@ -237,13 +492,13 @@ in
 
 581    ###### implementation
 
 583 -  config = mkIf (cfg.servers != { }) {
 
 584 +  config = mkIf (enabledServers != { }) {
 
 589            name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)