1 { config, lib, pkgs, ... }:
7 cfg = config.services.openvpn;
9 inherit (pkgs) openvpn;
11 PATH = name: makeBinPath (getAttr "openvpn-${name}" config.systemd.services).path;
13 makeOpenVPNJob = cfg: name:
16 configFile = pkgs.writeText "openvpn-config-${name}" (
17 generators.toKeyValue {
18 mkKeyValue = key: value:
19 if hasAttr key scripts
20 then "${key} " + pkgs.writeShellScript "openvpn-${name}-${key}" (scripts.${key} value)
21 else if builtins.isBool value
22 then optionalString value key
23 else if builtins.isPath value
24 then "${key} ${toString value}"
25 else "${key} ${generators.mkValueStringDefault {} value}";
26 listsAsDuplicateKeys = true;
33 export PATH=${PATH name}
35 # For convenience in client scripts, extract the remote domain
36 # name and name server.
37 for var in ''${!foreign_option_*}; do
39 if [ "''${x[0]}" = dhcp-option ]; then
40 if [ "''${x[1]}" = DOMAIN ]; then domain="''${x[2]}"
41 elif [ "''${x[1]}" = DNS ]; then nameserver="''${x[2]}"
46 ${optionalString cfg.updateResolvConf
47 "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
55 export PATH=${PATH name}
57 ip link set dev '${cfg.settings.dev}' up netns '${cfg.netns}' mtu "$tun_mtu"
58 ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-up-netns.sh" ''
61 export PATH=${PATH name}
65 mkdir -p /etc/netns/'${cfg.netns}'
67 process_foreign_option () {
69 dhcp-option:DNS) echo "nameserver $3" >>/etc/netns/'${cfg.netns}'/resolv.conf ;;
70 dhcp-option:DOMAIN) foreign_opt_domains="$foreign_opt_domains $3" ;;
73 if test ! -e /etc/netns/'${cfg.netns}'/resolv.conf; then
74 # add DNS settings if given in foreign options
77 eval opt=\"\''${foreign_option_$i-}\"
80 process_foreign_option $opt
83 for d in $foreign_opt_domains; do
84 printf '%s\n' "domain $1" "search $*" \
85 >>/etc/netns/'${cfg.netns}'/resolv.conf
89 netmask4="''${ifconfig_netmask:-30}"
90 netbits6="''${ifconfig_ipv6_netbits:-112}"
91 if [ -n "''${ifconfig_local-}" ]; then
92 if [ -n "''${ifconfig_remote-}" ]; then
94 local "$ifconfig_local" \
95 peer "$ifconfig_remote/$netmask4" \
96 ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
97 dev '${cfg.settings.dev}'
100 local "$ifconfig_local/$netmask4" \
101 ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
102 dev '${cfg.settings.dev}'
105 if [ -n "''${ifconfig_ipv6_local-}" ]; then
106 if [ -n "''${ifconfig_ipv6_remote-}" ]; then
108 local "$ifconfig_ipv6_local" \
109 peer "$ifconfig_ipv6_remote/$netbits6" \
110 dev '${cfg.settings.dev}'
113 local "$ifconfig_ipv6_local/$netbits6" \
114 dev '${cfg.settings.dev}'
125 export PATH=${PATH name}
127 ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-route-up-netns" ''
128 export PATH=${PATH name}
132 eval net=\"\''${route_network_$i-}\"
133 eval mask=\"\''${route_netmask_$i-}\"
134 eval gw=\"\''${route_gateway_$i-}\"
135 eval mtr=\"\''${route_metric_$i-}\"
138 ip -4 route replace "$net/$mask" via "$gw" ''${mtr:+metric "$mtr"}
142 if [ -n "''${route_vpn_gateway-}" ]; then
143 ip -4 route replace default via "$route_vpn_gateway"
148 # There doesn't seem to be $route_ipv6_metric_<n>
149 # according to the manpage.
150 eval net=\"\''${route_ipv6_network_$i-}\"
151 eval gw=\"\''${route_ipv6_gateway_$i-}\"
154 ip -6 route replace "$net" via "$gw" metric 100
158 # There's no $route_vpn_gateway for IPv6. It's not
159 # documented if OpenVPN includes default route in
160 # $route_ipv6_*. Set default route to remote VPN
161 # endpoint address if there is one. Use higher metric
162 # than $route_ipv6_* routes to give preference to a
163 # possible default route in them.
164 if [ -n "''${ifconfig_ipv6_remote-}" ]; then
165 ip -6 route replace default \
166 via "$ifconfig_ipv6_remote" metric 200
173 export PATH=${PATH name}
174 ${optionalString cfg.updateResolvConf
175 "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
183 ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-down-netns.sh" ''
191 description = "OpenVPN instance ‘${name}’";
193 wantedBy = optional cfg.autoStart "multi-user.target";
194 after = [ "network.target" ];
195 bindsTo = optional (cfg.netns != null) "netns-${cfg.netns}.service";
196 requires = optional (cfg.netns != null) "netns-${cfg.netns}.service";
198 path = [ pkgs.iptables pkgs.iproute pkgs.nettools ];
200 serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
201 serviceConfig.Restart = "always";
202 serviceConfig.Type = "notify";
208 (mkRemovedOptionModule [ "services" "openvpn" "enable" ] "")
215 services.openvpn.servers = mkOption {
218 example = literalExample ''
222 # Simplest server configuration: https://community.openvpn.net/openvpn/wiki/StaticKeyMiniHowto
225 ifconfig = "10.8.0.1 10.8.0.2";
226 secret = "/root/static.key";
227 up = "ip route add ...";
228 down = "ip route del ...";
235 remote = "vpn.example.org";
237 proto = "tcp-client";
239 ca = "/root/.vpn/ca.crt";
240 cert = "/root/.vpn/alice.crt";
241 key = "/root/.vpn/alice.key";
242 up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
243 down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
250 Each attribute of this option defines a systemd service that
251 runs an OpenVPN instance. These can be OpenVPN servers or
252 clients. The name of each systemd service is
253 <literal>openvpn-<replaceable>name</replaceable>.service</literal>,
254 where <replaceable>name</replaceable> is the corresponding
258 type = with types; attrsOf (submodule ({name, config, ...}: {
261 settings = mkOption {
263 Configuration of this OpenVPN instance. See
264 <citerefentry><refentrytitle>openvpn</refentrytitle><manvolnum>8</manvolnum></citerefentry>
267 To import an external config file, use the following definition:
268 <literal>config = /path/to/config.ovpn;</literal>
271 type = types.submodule {
272 freeformType = with types; attrsOf (nullOr (oneOf [path str bool int]));
273 options.dev = mkOption {
277 Shell commands executed when the instance is starting.
280 options.down = mkOption {
284 Shell commands executed when the instance is shutting down.
287 options.errors-to-stderr = mkOption {
291 Output errors to stderr instead of stdout
292 unless log output is redirected by one of the <code>--log</code> options.
295 options.route-up = mkOption {
299 Run command after routes are added.
302 options.up = mkOption {
306 Shell commands executed when the instance is starting.
309 options.script-security = mkOption {
311 type = types.enum [1 2 3];
313 1 — (Default) Only call built-in executables such as ifconfig, ip, route, or netsh.
314 2 — Allow calling of built-in executables and user-defined scripts.
315 3 — Allow passwords to be passed to scripts via environmental variables (potentially unsafe).
321 autoStart = mkOption {
324 description = "Whether this OpenVPN instance should be started automatically.";
327 updateResolvConf = mkOption {
331 Use the script from the update-resolv-conf package to automatically
332 update resolv.conf with the DNS information provided by openvpn. The
333 script will be run after the "up" commands and before the "down" commands.
337 authUserPass = mkOption {
340 This option can be used to store the username / password credentials
341 with the "auth-user-pass" authentication method.
343 WARNING: Using this option will put the credentials WORLD-READABLE in the Nix store!
345 type = types.nullOr (types.submodule {
348 username = mkOption {
349 description = "The username to store inside the credentials file.";
353 password = mkOption {
354 description = "The password to store inside the credentials file.";
363 type = with types; nullOr str;
364 description = "Network namespace.";
368 config.settings = mkMerge
369 [ (mkIf (config.netns != null) {
370 # Useless to setup the interface
371 # because moving it to the netns will reset it
372 ifconfig-noexec = true;
376 (mkIf (config.authUserPass != null) {
377 auth-user-pass = pkgs.writeText "openvpn-auth-user-pass-${name}" ''
378 ${config.authUserPass.username}
379 ${config.authUserPass.password}
382 (mkIf config.updateResolvConf {
394 ###### implementation
396 config = mkIf (cfg.servers != {}) {
398 systemd.services = listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) cfg.servers);
400 environment.systemPackages = [ openvpn ];
402 boot.kernelModules = [ "tun" ];