]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/networking/openvpn.nix
nix: update patches and wip stuffs
[sourcephile-nix.git] / nixos / modules / services / networking / openvpn.nix
1 { config, lib, pkgs, ... }:
2
3 with lib;
4
5 let
6
7 cfg = config.services.openvpn;
8
9 inherit (pkgs) openvpn;
10
11 PATH = name: makeBinPath (getAttr "openvpn-${name}" config.systemd.services).path;
12
13 makeOpenVPNJob = cfg: name:
14 let
15
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;
27 } cfg.settings
28 );
29
30 scripts = {
31 up = script:
32 let init = ''
33 export PATH=${PATH name}
34
35 # For convenience in client scripts, extract the remote domain
36 # name and name server.
37 for var in ''${!foreign_option_*}; do
38 x=(''${!var})
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]}"
42 fi
43 fi
44 done
45
46 ${optionalString cfg.updateResolvConf
47 "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
48 ''; in
49 if cfg.netns == null
50 then ''
51 ${init}
52 ${script}
53 ''
54 else ''
55 export PATH=${PATH name}
56 set -eux
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" ''
59 ${init}
60 set -eux
61 export PATH=${PATH name}
62
63 ip link set dev lo up
64
65 mkdir -p /etc/netns/'${cfg.netns}'
66 foreign_opt_domains=
67 process_foreign_option () {
68 case "$1:$2" in
69 dhcp-option:DNS) echo "nameserver $3" >>/etc/netns/'${cfg.netns}'/resolv.conf ;;
70 dhcp-option:DOMAIN) foreign_opt_domains="$foreign_opt_domains $3" ;;
71 esac
72 }
73 if test ! -e /etc/netns/'${cfg.netns}'/resolv.conf; then
74 # add DNS settings if given in foreign options
75 i=1
76 while
77 eval opt=\"\''${foreign_option_$i-}\"
78 [ -n "$opt" ]
79 do
80 process_foreign_option $opt
81 i=$(( i + 1 ))
82 done
83 for d in $foreign_opt_domains; do
84 printf '%s\n' "domain $1" "search $*" \
85 >>/etc/netns/'${cfg.netns}'/resolv.conf
86 done
87 fi
88
89 netmask4="''${ifconfig_netmask:-30}"
90 netbits6="''${ifconfig_ipv6_netbits:-112}"
91 if [ -n "''${ifconfig_local-}" ]; then
92 if [ -n "''${ifconfig_remote-}" ]; then
93 ip -4 addr replace \
94 local "$ifconfig_local" \
95 peer "$ifconfig_remote/$netmask4" \
96 ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
97 dev '${cfg.settings.dev}'
98 else
99 ip -4 addr replace \
100 local "$ifconfig_local/$netmask4" \
101 ''${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
102 dev '${cfg.settings.dev}'
103 fi
104 fi
105 if [ -n "''${ifconfig_ipv6_local-}" ]; then
106 if [ -n "''${ifconfig_ipv6_remote-}" ]; then
107 ip -6 addr replace \
108 local "$ifconfig_ipv6_local" \
109 peer "$ifconfig_ipv6_remote/$netbits6" \
110 dev '${cfg.settings.dev}'
111 else
112 ip -6 addr replace \
113 local "$ifconfig_ipv6_local/$netbits6" \
114 dev '${cfg.settings.dev}'
115 fi
116 fi
117 set +eux
118 ${script}
119 ''}
120 '';
121 route-up = script:
122 if cfg.netns == null
123 then script
124 else ''
125 export PATH=${PATH name}
126 set -eux
127 ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-route-up-netns" ''
128 export PATH=${PATH name}
129 set -eux
130 i=1
131 while
132 eval net=\"\''${route_network_$i-}\"
133 eval mask=\"\''${route_netmask_$i-}\"
134 eval gw=\"\''${route_gateway_$i-}\"
135 eval mtr=\"\''${route_metric_$i-}\"
136 [ -n "$net" ]
137 do
138 ip -4 route replace "$net/$mask" via "$gw" ''${mtr:+metric "$mtr"}
139 i=$(( i + 1 ))
140 done
141
142 if [ -n "''${route_vpn_gateway-}" ]; then
143 ip -4 route replace default via "$route_vpn_gateway"
144 fi
145
146 i=1
147 while
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-}\"
152 [ -n "$net" ]
153 do
154 ip -6 route replace "$net" via "$gw" metric 100
155 i=$(( i + 1 ))
156 done
157
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
167 fi
168 ${script}
169 ''}
170 '';
171 down = script:
172 let init = ''
173 export PATH=${PATH name}
174 ${optionalString cfg.updateResolvConf
175 "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
176 ''; in
177 if cfg.netns == null
178 then ''
179 ${init}
180 ${script}
181 ''
182 else ''
183 ip netns exec '${cfg.netns}' ${pkgs.writeShellScript "openvpn-${name}-down-netns.sh" ''
184 ${init}
185 ${script}
186 ''}
187 '';
188 };
189
190 in {
191 description = "OpenVPN instance ‘${name}’";
192
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";
197
198 path = [ pkgs.iptables pkgs.iproute pkgs.nettools ];
199
200 serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
201 serviceConfig.Restart = "always";
202 serviceConfig.Type = "notify";
203 };
204 in
205
206 {
207 imports = [
208 (mkRemovedOptionModule [ "services" "openvpn" "enable" ] "")
209 ];
210
211 ###### interface
212
213 options = {
214
215 services.openvpn.servers = mkOption {
216 default = {};
217
218 example = literalExample ''
219 {
220 server = {
221 settings = {
222 # Simplest server configuration: https://community.openvpn.net/openvpn/wiki/StaticKeyMiniHowto
223 # server :
224 dev = "tun";
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 ...";
229 };
230 };
231
232 client = {
233 settings = {
234 client = true;
235 remote = "vpn.example.org";
236 dev = "tun";
237 proto = "tcp-client";
238 port = 8080;
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";
244 };
245 };
246 }
247 '';
248
249 description = ''
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
255 attribute name.
256 '';
257
258 type = with types; attrsOf (submodule ({name, config, ...}: {
259
260 options = {
261 settings = mkOption {
262 description = ''
263 Configuration of this OpenVPN instance. See
264 <citerefentry><refentrytitle>openvpn</refentrytitle><manvolnum>8</manvolnum></citerefentry>
265 for details.
266
267 To import an external config file, use the following definition:
268 <literal>config = /path/to/config.ovpn;</literal>
269 '';
270 default = {};
271 type = types.submodule {
272 freeformType = with types; attrsOf (nullOr (oneOf [path str bool int]));
273 options.dev = mkOption {
274 default = null;
275 type = types.str;
276 description = ''
277 Shell commands executed when the instance is starting.
278 '';
279 };
280 options.down = mkOption {
281 default = "";
282 type = types.lines;
283 description = ''
284 Shell commands executed when the instance is shutting down.
285 '';
286 };
287 options.errors-to-stderr = mkOption {
288 default = true;
289 type = types.bool;
290 description = ''
291 Output errors to stderr instead of stdout
292 unless log output is redirected by one of the <code>--log</code> options.
293 '';
294 };
295 options.route-up = mkOption {
296 default = "";
297 type = types.lines;
298 description = ''
299 Run command after routes are added.
300 '';
301 };
302 options.up = mkOption {
303 default = "";
304 type = types.lines;
305 description = ''
306 Shell commands executed when the instance is starting.
307 '';
308 };
309 options.script-security = mkOption {
310 default = 1;
311 type = types.enum [1 2 3];
312 description = ''
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).
316 '';
317 };
318 };
319 };
320
321 autoStart = mkOption {
322 default = true;
323 type = types.bool;
324 description = "Whether this OpenVPN instance should be started automatically.";
325 };
326
327 updateResolvConf = mkOption {
328 default = false;
329 type = types.bool;
330 description = ''
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.
334 '';
335 };
336
337 authUserPass = mkOption {
338 default = null;
339 description = ''
340 This option can be used to store the username / password credentials
341 with the "auth-user-pass" authentication method.
342
343 WARNING: Using this option will put the credentials WORLD-READABLE in the Nix store!
344 '';
345 type = types.nullOr (types.submodule {
346
347 options = {
348 username = mkOption {
349 description = "The username to store inside the credentials file.";
350 type = types.str;
351 };
352
353 password = mkOption {
354 description = "The password to store inside the credentials file.";
355 type = types.str;
356 };
357 };
358 });
359 };
360
361 netns = mkOption {
362 default = true;
363 type = with types; nullOr str;
364 description = "Network namespace.";
365 };
366 };
367
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;
373 route-noexec = true;
374 script-security = 2;
375 })
376 (mkIf (config.authUserPass != null) {
377 auth-user-pass = pkgs.writeText "openvpn-auth-user-pass-${name}" ''
378 ${config.authUserPass.username}
379 ${config.authUserPass.password}
380 '';
381 })
382 (mkIf config.updateResolvConf {
383 script-security = 2;
384 })
385 ];
386
387 }));
388
389 };
390
391 };
392
393
394 ###### implementation
395
396 config = mkIf (cfg.servers != {}) {
397
398 systemd.services = listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) cfg.servers);
399
400 environment.systemPackages = [ openvpn ];
401
402 boot.kernelModules = [ "tun" ];
403
404 };
405
406 }