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 "auth-user-pass ${pkgs.writeText "openvpn-credentials-${name}" ''
58 ${cfg.authUserPass.username}
59 ${cfg.authUserPass.password}
60 ''}"
61 }
62 '';
63
64 in
65 {
66 description = "OpenVPN instance ‘${name}’";
67
68 wantedBy = optional cfg.autoStart "multi-user.target";
69 after = [ "network.target" ];
70
71 path = [
72 pkgs.iptables
73 pkgs.iproute2
74 pkgs.nettools
75 ];
76
77 serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
78 serviceConfig.Restart = "always";
79 serviceConfig.Type = "notify";
80 };
81
82 restartService = optionalAttrs cfg.restartAfterSleep {
83 openvpn-restart = {
84 wantedBy = [ "sleep.target" ];
85 path = [ pkgs.procps ];
86 script =
87 let
88 unitNames = map (n: "openvpn-${n}.service") (builtins.attrNames cfg.servers);
89 in
90 "systemctl try-restart ${lib.escapeShellArgs unitNames}";
91 description = "Sends a signal to OpenVPN process to trigger a restart after return from sleep";
92 };
93 };
94
95in
96
97{
98 imports = [
99 (mkRemovedOptionModule [ "services" "openvpn" "enable" ] "")
100 ];
101
102 ###### interface
103
104 options = {
105
106 services.openvpn.servers = mkOption {
107 default = { };
108
109 example = literalExpression ''
110 {
111 server = {
112 config = '''
113 # Simplest server configuration: https://community.openvpn.net/openvpn/wiki/StaticKeyMiniHowto
114 # server :
115 dev tun
116 ifconfig 10.8.0.1 10.8.0.2
117 secret /root/static.key
118 ''';
119 up = "ip route add ...";
120 down = "ip route del ...";
121 };
122
123 client = {
124 config = '''
125 client
126 remote vpn.example.org
127 dev tun
128 proto tcp-client
129 port 8080
130 ca /root/.vpn/ca.crt
131 cert /root/.vpn/alice.crt
132 key /root/.vpn/alice.key
133 ''';
134 up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
135 down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
136 };
137 }
138 '';
139
140 description = ''
141 Each attribute of this option defines a systemd service that
142 runs an OpenVPN instance. These can be OpenVPN servers or
143 clients. The name of each systemd service is
144 `openvpn-«name».service`,
145 where «name» is the corresponding
146 attribute name.
147 '';
148
149 type =
150 with types;
151 attrsOf (submodule {
152
153 options = {
154
155 config = mkOption {
156 type = types.lines;
157 description = ''
158 Configuration of this OpenVPN instance. See
159 {manpage}`openvpn(8)`
160 for details.
161
162 To import an external config file, use the following definition:
163 `config = "config /path/to/config.ovpn"`
164 '';
165 };
166
167 up = mkOption {
168 default = "";
169 type = types.lines;
170 description = ''
171 Shell commands executed when the instance is starting.
172 '';
173 };
174
175 down = mkOption {
176 default = "";
177 type = types.lines;
178 description = ''
179 Shell commands executed when the instance is shutting down.
180 '';
181 };
182
183 autoStart = mkOption {
184 default = true;
185 type = types.bool;
186 description = "Whether this OpenVPN instance should be started automatically.";
187 };
188
189 updateResolvConf = mkOption {
190 default = false;
191 type = types.bool;
192 description = ''
193 Use the script from the update-resolv-conf package to automatically
194 update resolv.conf with the DNS information provided by openvpn. The
195 script will be run after the "up" commands and before the "down" commands.
196 '';
197 };
198
199 authUserPass = mkOption {
200 default = null;
201 description = ''
202 This option can be used to store the username / password credentials
203 with the "auth-user-pass" authentication method.
204
205 WARNING: Using this option will put the credentials WORLD-READABLE in the Nix store!
206 '';
207 type = types.nullOr (
208 types.submodule {
209
210 options = {
211 username = mkOption {
212 description = "The username to store inside the credentials file.";
213 type = types.str;
214 };
215
216 password = mkOption {
217 description = "The password to store inside the credentials file.";
218 type = types.str;
219 };
220 };
221 }
222 );
223 };
224 };
225
226 });
227
228 };
229
230 services.openvpn.restartAfterSleep = mkOption {
231 default = true;
232 type = types.bool;
233 description = "Whether OpenVPN client should be restarted after sleep.";
234 };
235
236 };
237
238 ###### implementation
239
240 config = mkIf (cfg.servers != { }) {
241
242 systemd.services =
243 (listToAttrs (
244 mapAttrsToList (
245 name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)
246 ) cfg.servers
247 ))
248 // restartService;
249
250 environment.systemPackages = [ openvpn ];
251
252 boot.kernelModules = [ "tun" ];
253
254 };
255
256}