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