1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.openvpn;
8
9 inherit (pkgs) openvpn;
10
11 makeOpenVPNJob = cfg: name:
12 let
13
14 path = makeBinPath (getAttr "openvpn-${name}" config.systemd.services).path;
15
16 upScript = ''
17 export PATH=${path}
18
19 # For convenience in client scripts, extract the remote domain
20 # name and name server.
21 for var in ''${!foreign_option_*}; do
22 x=(''${!var})
23 if [ "''${x[0]}" = dhcp-option ]; then
24 if [ "''${x[1]}" = DOMAIN ]; then domain="''${x[2]}"
25 elif [ "''${x[1]}" = DNS ]; then nameserver="''${x[2]}"
26 fi
27 fi
28 done
29
30 ${cfg.up}
31 ${optionalString cfg.updateResolvConf
32 "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
33 '';
34
35 downScript = ''
36 export PATH=${path}
37 ${optionalString cfg.updateResolvConf
38 "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
39 ${cfg.down}
40 '';
41
42 configFile = pkgs.writeText "openvpn-config-${name}"
43 ''
44 errors-to-stderr
45 ${optionalString (cfg.up != "" || cfg.down != "" || cfg.updateResolvConf) "script-security 2"}
46 ${cfg.config}
47 ${optionalString (cfg.up != "" || cfg.updateResolvConf)
48 "up ${pkgs.writeShellScript "openvpn-${name}-up" upScript}"}
49 ${optionalString (cfg.down != "" || cfg.updateResolvConf)
50 "down ${pkgs.writeShellScript "openvpn-${name}-down" downScript}"}
51 ${optionalString (cfg.authUserPass != null)
52 "auth-user-pass ${pkgs.writeText "openvpn-credentials-${name}" ''
53 ${cfg.authUserPass.username}
54 ${cfg.authUserPass.password}
55 ''}"}
56 '';
57
58 in
59 {
60 description = "OpenVPN instance ‘${name}’";
61
62 wantedBy = optional cfg.autoStart "multi-user.target";
63 after = [ "network.target" ];
64
65 path = [ pkgs.iptables pkgs.iproute2 pkgs.nettools ];
66
67 serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
68 serviceConfig.Restart = "always";
69 serviceConfig.Type = "notify";
70 };
71
72 restartService = optionalAttrs cfg.restartAfterSleep {
73 openvpn-restart = {
74 wantedBy = [ "sleep.target" ];
75 path = [ pkgs.procps ];
76 script = "pkill --signal SIGHUP --exact openvpn";
77 #SIGHUP makes openvpn process to self-exit and then it got restarted by systemd because of Restart=always
78 description = "Sends a signal to OpenVPN process to trigger a restart after return from sleep";
79 };
80 };
81
82in
83
84{
85 imports = [
86 (mkRemovedOptionModule [ "services" "openvpn" "enable" ] "")
87 ];
88
89 ###### interface
90
91 options = {
92
93 services.openvpn.servers = mkOption {
94 default = { };
95
96 example = literalExpression ''
97 {
98 server = {
99 config = '''
100 # Simplest server configuration: https://community.openvpn.net/openvpn/wiki/StaticKeyMiniHowto
101 # server :
102 dev tun
103 ifconfig 10.8.0.1 10.8.0.2
104 secret /root/static.key
105 ''';
106 up = "ip route add ...";
107 down = "ip route del ...";
108 };
109
110 client = {
111 config = '''
112 client
113 remote vpn.example.org
114 dev tun
115 proto tcp-client
116 port 8080
117 ca /root/.vpn/ca.crt
118 cert /root/.vpn/alice.crt
119 key /root/.vpn/alice.key
120 ''';
121 up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
122 down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
123 };
124 }
125 '';
126
127 description = lib.mdDoc ''
128 Each attribute of this option defines a systemd service that
129 runs an OpenVPN instance. These can be OpenVPN servers or
130 clients. The name of each systemd service is
131 `openvpn-«name».service`,
132 where «name» is the corresponding
133 attribute name.
134 '';
135
136 type = with types; attrsOf (submodule {
137
138 options = {
139
140 config = mkOption {
141 type = types.lines;
142 description = lib.mdDoc ''
143 Configuration of this OpenVPN instance. See
144 {manpage}`openvpn(8)`
145 for details.
146
147 To import an external config file, use the following definition:
148 `config = "config /path/to/config.ovpn"`
149 '';
150 };
151
152 up = mkOption {
153 default = "";
154 type = types.lines;
155 description = lib.mdDoc ''
156 Shell commands executed when the instance is starting.
157 '';
158 };
159
160 down = mkOption {
161 default = "";
162 type = types.lines;
163 description = lib.mdDoc ''
164 Shell commands executed when the instance is shutting down.
165 '';
166 };
167
168 autoStart = mkOption {
169 default = true;
170 type = types.bool;
171 description = lib.mdDoc "Whether this OpenVPN instance should be started automatically.";
172 };
173
174 updateResolvConf = mkOption {
175 default = false;
176 type = types.bool;
177 description = lib.mdDoc ''
178 Use the script from the update-resolv-conf package to automatically
179 update resolv.conf with the DNS information provided by openvpn. The
180 script will be run after the "up" commands and before the "down" commands.
181 '';
182 };
183
184 authUserPass = mkOption {
185 default = null;
186 description = lib.mdDoc ''
187 This option can be used to store the username / password credentials
188 with the "auth-user-pass" authentication method.
189
190 WARNING: Using this option will put the credentials WORLD-READABLE in the Nix store!
191 '';
192 type = types.nullOr (types.submodule {
193
194 options = {
195 username = mkOption {
196 description = lib.mdDoc "The username to store inside the credentials file.";
197 type = types.str;
198 };
199
200 password = mkOption {
201 description = lib.mdDoc "The password to store inside the credentials file.";
202 type = types.str;
203 };
204 };
205 });
206 };
207 };
208
209 });
210
211 };
212
213 services.openvpn.restartAfterSleep = mkOption {
214 default = true;
215 type = types.bool;
216 description = lib.mdDoc "Whether OpenVPN client should be restarted after sleep.";
217 };
218
219 };
220
221
222 ###### implementation
223
224 config = mkIf (cfg.servers != { }) {
225
226 systemd.services = (listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) cfg.servers))
227 // restartService;
228
229 environment.systemPackages = [ openvpn ];
230
231 boot.kernelModules = [ "tun" ];
232
233 };
234
235}