]> Git — Sourcephile - julm/julm-nix.git/blob - nixos/modules/services/networking/openvpn.nix
+security+access(nan2gua1/openvpn/riseup): enable
[julm/julm-nix.git] / nixos / modules / services / networking / openvpn.nix
1 {
2 config,
3 lib,
4 pkgs,
5 ...
6 }:
7
8 with lib;
9
10 let
11
12 cfg = config.services.openvpn;
13 enabledServers = filterAttrs (name: srv: srv.enable) cfg.servers;
14
15 inherit (pkgs) openvpn;
16
17 PATH = name: makeBinPath config.systemd.services."openvpn-${name}".path;
18
19 makeOpenVPNJob =
20 cfg: name:
21 let
22
23 configFile = pkgs.writeText "openvpn-config-${name}" (
24 generators.toKeyValue {
25 mkKeyValue =
26 key: value:
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
32 "${key} ${value}"
33 else if builtins.isList value then
34 concatMapStringsSep "\n" (v: "${key} ${generators.mkValueStringDefault { } v}") value
35 else
36 "${key} ${generators.mkValueStringDefault { } value}";
37 } cfg.settings
38 );
39
40 scripts = {
41 up =
42 script:
43 let
44 init = ''
45 export PATH=${PATH name}
46
47 # For convenience in client scripts, extract the remote domain
48 # name and name server.
49 for var in ''${!foreign_option_*}; do
50 x=(''${!var})
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]}"
54 fi
55 fi
56 done
57
58 ${optionalString cfg.updateResolvConf "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
59 '';
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
71 foreign_opt_domains=
72 process_foreign_option () {
73 case "$1:$2" in
74 dhcp-option:DNS) echo "nameserver $3" >>/etc/netns/'${cfg.netns}'/resolv.conf ;;
75 dhcp-option:DOMAIN) foreign_opt_domains="$foreign_opt_domains $3" ;;
76 esac
77 }
78 i=1
79 while
80 eval opt=\"\''${foreign_option_$i-}\"
81 [ -n "$opt" ]
82 do
83 process_foreign_option $opt
84 i=$(( i + 1 ))
85 done
86 for d in $foreign_opt_domains; do
87 printf '%s\n' "domain $1" "search $*" \
88 >>/etc/netns/'${cfg.netns}'/resolv.conf
89 done
90 '';
91 in
92 if cfg.netns == null then
93 ''
94 ${init}
95 ${script}
96 ''
97 else
98 ''
99 export PATH=${PATH name}
100 set -eux
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" ''
104 ${init}
105 set -eux
106 export PATH=${PATH name}
107
108 ip link set dev lo up
109
110 netmask4="''${ifconfig_netmask:-30}"
111 netbits6="''${ifconfig_ipv6_netbits:-112}"
112 if [ -n "''${ifconfig_local-}" ]; then
113 if [ -n "''${ifconfig_remote-}" ]; then
114 ip -4 addr replace \
115 local "$ifconfig_local" \
116 peer "$ifconfig_remote/$netmask4" \
117 ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
118 dev '${cfg.settings.dev}'
119 else
120 ip -4 addr replace \
121 local "$ifconfig_local/$netmask4" \
122 ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
123 dev '${cfg.settings.dev}'
124 fi
125 fi
126 if [ -n "''${ifconfig_ipv6_local-}" ]; then
127 if [ -n "''${ifconfig_ipv6_remote-}" ]; then
128 ip -6 addr replace \
129 local "$ifconfig_ipv6_local" \
130 peer "$ifconfig_ipv6_remote/$netbits6" \
131 dev '${cfg.settings.dev}'
132 else
133 ip -6 addr replace \
134 local "$ifconfig_ipv6_local/$netbits6" \
135 dev '${cfg.settings.dev}'
136 fi
137 fi
138 set +eux
139 ${script}
140 ''}
141 '';
142 route-up =
143 script:
144 if cfg.netns == null then
145 script
146 else
147 ''
148 export PATH=${PATH name}
149 set -eux
150 ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-route-up-netns" ''
151 export PATH=${PATH name}
152 set -eux
153 i=1
154 while
155 eval net=\"\''${route_network_$i-}\"
156 eval mask=\"\''${route_netmask_$i-}\"
157 eval gw=\"\''${route_gateway_$i-}\"
158 eval mtr=\"\''${route_metric_$i-}\"
159 [ -n "$net" ]
160 do
161 ip -4 route replace "$net/$mask" via "$gw" ''${mtr:+metric "$mtr"}
162 i=$(( i + 1 ))
163 done
164
165 if [ -n "''${route_vpn_gateway-}" ]; then
166 ip -4 route replace default via "$route_vpn_gateway"
167 fi
168
169 i=1
170 while
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-}\"
175 [ -n "$net" ]
176 do
177 ip -6 route replace "$net" via "$gw" metric 100
178 i=$(( i + 1 ))
179 done
180
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
190 fi
191 ${script}
192 ''}
193 '';
194 down =
195 script:
196 let
197 init = ''
198 export PATH=${PATH name}
199 ${optionalString cfg.updateResolvConf "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
200 '';
201 in
202 if cfg.netns == null then
203 ''
204 ${init}
205 ${script}
206 ''
207 else
208 ''
209 export PATH=${PATH name}
210 ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-down-netns.sh" ''
211 ${init}
212 ${script}
213 ''}
214 rm -f /etc/netns/'${cfg.netns}'/resolv.conf
215 '';
216 };
217
218 in
219 {
220 description = "OpenVPN instance ‘${name}’";
221
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";
226
227 path = [
228 pkgs.iptables
229 pkgs.iproute2
230 pkgs.nettools
231 ];
232
233 serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
234 serviceConfig.Restart = "always";
235 serviceConfig.RestartSec = "5s";
236 serviceConfig.Type = "notify";
237 };
238
239 restartService = optionalAttrs cfg.restartAfterSleep {
240 openvpn-restart = {
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";
246 };
247 };
248
249 in
250
251 {
252 imports = [
253 (mkRemovedOptionModule [ "services" "openvpn" "enable" ] "")
254 ];
255
256 ###### interface
257
258 options = {
259
260 services.openvpn.servers = mkOption {
261 default = { };
262
263 example = literalExpression ''
264 {
265 server = {
266 settings = {
267 # Simplest server configuration: https://community.openvpn.net/openvpn/wiki/StaticKeyMiniHowto
268 # server :
269 dev = "tun";
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 ...";
274 };
275 };
276
277 client = {
278 settings = {
279 client = true;
280 remote = "vpn.example.org";
281 dev = "tun";
282 proto = "tcp-client";
283 port = 8080;
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";
289 };
290 };
291 }
292 '';
293
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
300 attribute name.
301 '';
302
303 type =
304 with types;
305 attrsOf (
306 submodule (
307 {
308 name,
309 config,
310 options,
311 ...
312 }:
313 {
314
315 options = rec {
316 enable = mkEnableOption (mdDoc "OpenVPN server") // {
317 default = true;
318 };
319
320 settings = mkOption {
321 description = lib.mdDoc ''
322 Configuration of this OpenVPN instance. See
323 {manpage}`openvpn(8)`
324 for details.
325
326 To import an external config file, use the following definition:
327 config = /path/to/config.ovpn;
328 '';
329 default = { };
330 type = types.submodule {
331 freeformType =
332 with types;
333 attrsOf (
334 nullOr (oneOf [
335 bool
336 int
337 str
338 path
339 (listOf (oneOf [
340 bool
341 int
342 str
343 path
344 ]))
345 ])
346 );
347 options.dev = mkOption {
348 default = null;
349 type = types.str;
350 description = lib.mdDoc ''
351 Shell commands executed when the instance is starting.
352 '';
353 };
354 options.down = mkOption {
355 default = "";
356 type = types.lines;
357 description = lib.mdDoc ''
358 Shell commands executed when the instance is shutting down.
359 '';
360 };
361 options.errors-to-stderr = mkOption {
362 default = true;
363 type = types.bool;
364 description = lib.mdDoc ''
365 Output errors to stderr instead of stdout
366 unless log output is redirected by one of the `--log` options.
367 '';
368 };
369 options.route-up = mkOption {
370 default = "";
371 type = types.lines;
372 description = lib.mdDoc ''
373 Run command after routes are added.
374 '';
375 };
376 options.up = mkOption {
377 default = "";
378 type = types.lines;
379 description = lib.mdDoc ''
380 Shell commands executed when the instance is starting.
381 '';
382 };
383 options.script-security = mkOption {
384 default = 1;
385 type = types.enum [
386 1
387 2
388 3
389 ];
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).
394 '';
395 };
396 };
397 };
398
399 autoStart = mkOption {
400 default = true;
401 type = types.bool;
402 description = lib.mdDoc "Whether this OpenVPN instance should be started automatically.";
403 };
404
405 # Legacy options
406 down = (elemAt settings.type.functor.payload.modules 0).options.down;
407 up = (elemAt settings.type.functor.payload.modules 0).options.up;
408
409 updateResolvConf = mkOption {
410 default = false;
411 type = types.bool;
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.
416 '';
417 };
418
419 authUserPass = mkOption {
420 default = null;
421 description = lib.mdDoc ''
422 This option can be used to store the username / password credentials
423 with the "auth-user-pass" authentication method.
424
425 WARNING: Using this option will put the credentials WORLD-READABLE in the Nix store!
426 '';
427 type = types.nullOr (
428 types.submodule {
429
430 options = {
431 username = mkOption {
432 description = lib.mdDoc "The username to store inside the credentials file.";
433 type = types.str;
434 };
435
436 password = mkOption {
437 description = lib.mdDoc "The password to store inside the credentials file.";
438 type = types.str;
439 };
440 };
441 }
442 );
443 };
444
445 netns = mkOption {
446 default = null;
447 type = with types; nullOr str;
448 description = lib.mdDoc "Network namespace.";
449 };
450 };
451
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;
457 route-noexec = true;
458 script-security = 2;
459 })
460 (mkIf (config.authUserPass != null) {
461 auth-user-pass = pkgs.writeText "openvpn-auth-user-pass-${name}" ''
462 ${config.authUserPass.username}
463 ${config.authUserPass.password}
464 '';
465 })
466 (mkIf config.updateResolvConf {
467 script-security = 2;
468 })
469 {
470 # Aliases legacy options
471 down = modules.mkAliasAndWrapDefsWithPriority id (options.down or { });
472 up = modules.mkAliasAndWrapDefsWithPriority id (options.up or { });
473 }
474 ];
475
476 }
477 )
478 );
479
480 };
481
482 services.openvpn.restartAfterSleep = mkOption {
483 default = true;
484 type = types.bool;
485 description = lib.mdDoc "Whether OpenVPN client should be restarted after sleep.";
486 };
487
488 };
489
490 ###### implementation
491
492 config = mkIf (enabledServers != { }) {
493
494 systemd.services =
495 (listToAttrs (
496 mapAttrsFlatten (
497 name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)
498 ) enabledServers
499 ))
500 // restartService;
501
502 environment.systemPackages = [ openvpn ];
503
504 boot.kernelModules = [ "tun" ];
505
506 };
507
508 }