12 cfg = config.services.openvpn;
13 enabledServers = filterAttrs (name: srv: srv.enable) cfg.servers;
15 inherit (pkgs) openvpn;
17 PATH = name: makeBinPath config.systemd.services."openvpn-${name}".path;
23 configFile = pkgs.writeText "openvpn-config-${name}" (
24 generators.toKeyValue {
27 if hasAttr key scripts then
28 "${key} " + pkgs.writeShellScript "openvpn-${name}-${key}" (scripts.${key} value)
29 else if builtins.isBool value then
30 optionalString value key
31 else if builtins.isPath value then
33 else if builtins.isList value then
34 concatMapStringsSep "\n" (v: "${key} ${generators.mkValueStringDefault { } v}") value
36 "${key} ${generators.mkValueStringDefault { } value}";
45 export PATH=${PATH name}
47 # For convenience in client scripts, extract the remote domain
48 # name and name server.
49 for var in ''${!foreign_option_*}; do
51 if [ "''${x[0]}" = dhcp-option ]; then
52 if [ "''${x[1]}" = DOMAIN ]; then domain="''${x[2]}"
53 elif [ "''${x[1]}" = DNS ]; then nameserver="''${x[2]}"
58 ${optionalString cfg.updateResolvConf "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
60 # Explanation: add DNS settings given in foreign DHCP options to the resolv.conf of the netns.
61 # Note that JoinsNamespaceOf="netns-${cfg.netns}.service" will not
62 # BindReadOnlyPaths=["/etc/netns/${cfg.netns}/resolv.conf:/etc/resolv.conf"];
63 # this will have to be added in each service joining the namespace.
64 setNetNSResolvConf = ''
65 mkdir -p /etc/netns/${cfg.netns}
66 # Explanation: this file is normally created by netns-${cfg.netns}.service,
67 # care must be taken to not delete it but to truncate it
68 # in order to propagate the changes to bind-mounted versions.
69 : > /etc/netns/${cfg.netns}/resolv.conf
70 chmod 644 /etc/netns/${cfg.netns}/resolv.conf
72 process_foreign_option () {
74 dhcp-option:DNS) echo "nameserver $3" >>/etc/netns/'${cfg.netns}'/resolv.conf ;;
75 dhcp-option:DOMAIN) foreign_opt_domains="$foreign_opt_domains $3" ;;
80 eval opt=\"\''${foreign_option_$i-}\"
83 process_foreign_option $opt
86 for d in $foreign_opt_domains; do
87 printf '%s\n' "domain $1" "search $*" \
88 >>/etc/netns/'${cfg.netns}'/resolv.conf
92 if cfg.netns == null then
99 export PATH=${PATH name}
101 ${setNetNSResolvConf}
102 ip link set dev '${cfg.settings.dev}' up netns '${cfg.netns}' mtu "$tun_mtu"
103 ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-up-netns.sh" ''
106 export PATH=${PATH name}
108 ip link set dev lo up
110 netmask4="''${ifconfig_netmask:-30}"
111 netbits6="''${ifconfig_ipv6_netbits:-112}"
112 if [ -n "''${ifconfig_local-}" ]; then
113 if [ -n "''${ifconfig_remote-}" ]; then
115 local "$ifconfig_local" \
116 peer "$ifconfig_remote/$netmask4" \
117 ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
118 dev '${cfg.settings.dev}'
121 local "$ifconfig_local/$netmask4" \
122 ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
123 dev '${cfg.settings.dev}'
126 if [ -n "''${ifconfig_ipv6_local-}" ]; then
127 if [ -n "''${ifconfig_ipv6_remote-}" ]; then
129 local "$ifconfig_ipv6_local" \
130 peer "$ifconfig_ipv6_remote/$netbits6" \
131 dev '${cfg.settings.dev}'
134 local "$ifconfig_ipv6_local/$netbits6" \
135 dev '${cfg.settings.dev}'
144 if cfg.netns == null then
148 export PATH=${PATH name}
150 ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-route-up-netns" ''
151 export PATH=${PATH name}
155 eval net=\"\''${route_network_$i-}\"
156 eval mask=\"\''${route_netmask_$i-}\"
157 eval gw=\"\''${route_gateway_$i-}\"
158 eval mtr=\"\''${route_metric_$i-}\"
161 ip -4 route replace "$net/$mask" via "$gw" ''${mtr:+metric "$mtr"}
165 if [ -n "''${route_vpn_gateway-}" ]; then
166 ip -4 route replace default via "$route_vpn_gateway"
171 # There doesn't seem to be $route_ipv6_metric_<n>
172 # according to the manpage.
173 eval net=\"\''${route_ipv6_network_$i-}\"
174 eval gw=\"\''${route_ipv6_gateway_$i-}\"
177 ip -6 route replace "$net" via "$gw" metric 100
181 # There's no $route_vpn_gateway for IPv6. It's not
182 # documented if OpenVPN includes default route in
183 # $route_ipv6_*. Set default route to remote VPN
184 # endpoint address if there is one. Use higher metric
185 # than $route_ipv6_* routes to give preference to a
186 # possible default route in them.
187 if [ -n "''${ifconfig_ipv6_remote-}" ]; then
188 ip -6 route replace default \
189 via "$ifconfig_ipv6_remote" metric 200
198 export PATH=${PATH name}
199 ${optionalString cfg.updateResolvConf "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
202 if cfg.netns == null then
209 export PATH=${PATH name}
210 ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-down-netns.sh" ''
214 rm -f /etc/netns/'${cfg.netns}'/resolv.conf
220 description = "OpenVPN instance ‘${name}’";
222 wantedBy = optional cfg.autoStart "multi-user.target";
223 after = [ "network.target" ];
224 bindsTo = optional (cfg.netns != null) "netns-${cfg.netns}.service";
225 requires = optional (cfg.netns != null) "netns-${cfg.netns}.service";
233 serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
234 serviceConfig.Restart = "always";
235 serviceConfig.RestartSec = "5s";
236 serviceConfig.Type = "notify";
239 restartService = optionalAttrs cfg.restartAfterSleep {
241 wantedBy = [ "sleep.target" ];
242 path = [ pkgs.procps ];
243 script = "pkill --signal SIGHUP --exact openvpn";
244 #SIGHUP makes openvpn process to self-exit and then it got restarted by systemd because of Restart=always
245 description = "Sends a signal to OpenVPN process to trigger a restart after return from sleep";
253 (mkRemovedOptionModule [ "services" "openvpn" "enable" ] "")
260 services.openvpn.servers = mkOption {
263 example = literalExpression ''
267 # Simplest server configuration: https://community.openvpn.net/openvpn/wiki/StaticKeyMiniHowto
270 ifconfig = "10.8.0.1 10.8.0.2";
271 secret = "/root/static.key";
272 up = "ip route add ...";
273 down = "ip route del ...";
280 remote = "vpn.example.org";
282 proto = "tcp-client";
284 ca = "/root/.vpn/ca.crt";
285 cert = "/root/.vpn/alice.crt";
286 key = "/root/.vpn/alice.key";
287 up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
288 down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
294 description = lib.mdDoc ''
295 Each attribute of this option defines a systemd service that
296 runs an OpenVPN instance. These can be OpenVPN servers or
297 clients. The name of each systemd service is
298 `openvpn-«name».service`,
299 where «name» is the corresponding
316 enable = mkEnableOption (mdDoc "OpenVPN server") // {
320 settings = mkOption {
321 description = lib.mdDoc ''
322 Configuration of this OpenVPN instance. See
323 {manpage}`openvpn(8)`
326 To import an external config file, use the following definition:
327 config = /path/to/config.ovpn;
330 type = types.submodule {
347 options.dev = mkOption {
350 description = lib.mdDoc ''
351 Shell commands executed when the instance is starting.
354 options.down = mkOption {
357 description = lib.mdDoc ''
358 Shell commands executed when the instance is shutting down.
361 options.errors-to-stderr = mkOption {
364 description = lib.mdDoc ''
365 Output errors to stderr instead of stdout
366 unless log output is redirected by one of the `--log` options.
369 options.route-up = mkOption {
372 description = lib.mdDoc ''
373 Run command after routes are added.
376 options.up = mkOption {
379 description = lib.mdDoc ''
380 Shell commands executed when the instance is starting.
383 options.script-security = mkOption {
390 description = lib.mdDoc ''
391 - 1 — (Default) Only call built-in executables such as ifconfig, ip, route, or netsh.
392 - 2 — Allow calling of built-in executables and user-defined scripts.
393 - 3 — Allow passwords to be passed to scripts via environmental variables (potentially unsafe).
399 autoStart = mkOption {
402 description = lib.mdDoc "Whether this OpenVPN instance should be started automatically.";
406 down = (elemAt settings.type.functor.payload.modules 0).options.down;
407 up = (elemAt settings.type.functor.payload.modules 0).options.up;
409 updateResolvConf = mkOption {
412 description = lib.mdDoc ''
413 Use the script from the update-resolv-conf package to automatically
414 update resolv.conf with the DNS information provided by openvpn. The
415 script will be run after the "up" commands and before the "down" commands.
419 authUserPass = mkOption {
421 description = lib.mdDoc ''
422 This option can be used to store the username / password credentials
423 with the "auth-user-pass" authentication method.
425 WARNING: Using this option will put the credentials WORLD-READABLE in the Nix store!
427 type = types.nullOr (
431 username = mkOption {
432 description = lib.mdDoc "The username to store inside the credentials file.";
436 password = mkOption {
437 description = lib.mdDoc "The password to store inside the credentials file.";
447 type = with types; nullOr str;
448 description = lib.mdDoc "Network namespace.";
452 config.settings = mkMerge [
453 (mkIf (config.netns != null) {
454 # Explanation: useless to setup the interface
455 # because moving it to the netns will reset it
456 ifconfig-noexec = true;
460 (mkIf (config.authUserPass != null) {
461 auth-user-pass = pkgs.writeText "openvpn-auth-user-pass-${name}" ''
462 ${config.authUserPass.username}
463 ${config.authUserPass.password}
466 (mkIf config.updateResolvConf {
470 # Aliases legacy options
471 down = modules.mkAliasAndWrapDefsWithPriority id (options.down or { });
472 up = modules.mkAliasAndWrapDefsWithPriority id (options.up or { });
482 services.openvpn.restartAfterSleep = mkOption {
485 description = lib.mdDoc "Whether OpenVPN client should be restarted after sleep.";
490 ###### implementation
492 config = mkIf (enabledServers != { }) {
497 name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)
502 environment.systemPackages = [ openvpn ];
504 boot.kernelModules = [ "tun" ];