1{ config, lib, options, pkgs, ... }:
2with lib;
3let
4 cfg = config.networking.openconnect;
5 openconnect = cfg.package;
6 pkcs11 = types.strMatching "pkcs11:.+" // {
7 name = "pkcs11";
8 description = "PKCS#11 URI";
9 };
10 interfaceOptions = {
11 options = {
12 autoStart = mkOption {
13 default = true;
14 description = lib.mdDoc "Whether this VPN connection should be started automatically.";
15 type = types.bool;
16 };
17
18 gateway = mkOption {
19 description = lib.mdDoc "Gateway server to connect to.";
20 example = "gateway.example.com";
21 type = types.str;
22 };
23
24 protocol = mkOption {
25 description = lib.mdDoc "Protocol to use.";
26 example = "anyconnect";
27 type =
28 types.enum [ "anyconnect" "array" "nc" "pulse" "gp" "f5" "fortinet" ];
29 };
30
31 user = mkOption {
32 description = lib.mdDoc "Username to authenticate with.";
33 example = "example-user";
34 type = types.nullOr types.str;
35 default = null;
36 };
37
38 # Note: It does not make sense to provide a way to declaratively
39 # set an authentication cookie, because they have to be requested
40 # for every new connection and would only work once.
41 passwordFile = mkOption {
42 description = lib.mdDoc ''
43 File containing the password to authenticate with. This
44 is passed to `openconnect` via the
45 `--passwd-on-stdin` option.
46 '';
47 default = null;
48 example = "/var/lib/secrets/openconnect-passwd";
49 type = types.nullOr types.path;
50 };
51
52 certificate = mkOption {
53 description = lib.mdDoc "Certificate to authenticate with.";
54 default = null;
55 example = "/var/lib/secrets/openconnect_certificate.pem";
56 type = with types; nullOr (either path pkcs11);
57 };
58
59 privateKey = mkOption {
60 description = lib.mdDoc "Private key to authenticate with.";
61 example = "/var/lib/secrets/openconnect_private_key.pem";
62 default = null;
63 type = with types; nullOr (either path pkcs11);
64 };
65
66 extraOptions = mkOption {
67 description = lib.mdDoc ''
68 Extra config to be appended to the interface config. It should
69 contain long-format options as would be accepted on the command
70 line by `openconnect`
71 (see https://www.infradead.org/openconnect/manual.html).
72 Non-key-value options like `deflate` can be used by
73 declaring them as booleans, i. e. `deflate = true;`.
74 '';
75 default = { };
76 example = {
77 compression = "stateless";
78
79 no-http-keepalive = true;
80 no-dtls = true;
81 };
82 type = with types; attrsOf (either str bool);
83 };
84 };
85 };
86 generateExtraConfig = extra_cfg:
87 strings.concatStringsSep "\n" (attrsets.mapAttrsToList
88 (name: value: if (value == true) then name else "${name}=${value}")
89 (attrsets.filterAttrs (_: value: value != false) extra_cfg));
90 generateConfig = name: icfg:
91 pkgs.writeText "config" ''
92 interface=${name}
93 ${optionalString (icfg.protocol != null) "protocol=${icfg.protocol}"}
94 ${optionalString (icfg.user != null) "user=${icfg.user}"}
95 ${optionalString (icfg.passwordFile != null) "passwd-on-stdin"}
96 ${optionalString (icfg.certificate != null)
97 "certificate=${icfg.certificate}"}
98 ${optionalString (icfg.privateKey != null) "sslkey=${icfg.privateKey}"}
99
100 ${generateExtraConfig icfg.extraOptions}
101 '';
102 generateUnit = name: icfg: {
103 description = "OpenConnect Interface - ${name}";
104 requires = [ "network-online.target" ];
105 after = [ "network.target" "network-online.target" ];
106 wantedBy = optional icfg.autoStart "multi-user.target";
107
108 serviceConfig = {
109 Type = "simple";
110 ExecStart = "${openconnect}/bin/openconnect --config=${
111 generateConfig name icfg
112 } ${icfg.gateway}";
113 StandardInput = lib.mkIf (icfg.passwordFile != null) "file:${icfg.passwordFile}";
114
115 ProtectHome = true;
116 };
117 };
118in {
119 options.networking.openconnect = {
120 package = mkPackageOptionMD pkgs "openconnect" { };
121
122 interfaces = mkOption {
123 description = lib.mdDoc "OpenConnect interfaces.";
124 default = { };
125 example = {
126 openconnect0 = {
127 gateway = "gateway.example.com";
128 protocol = "anyconnect";
129 user = "example-user";
130 passwordFile = "/var/lib/secrets/openconnect-passwd";
131 };
132 };
133 type = with types; attrsOf (submodule interfaceOptions);
134 };
135 };
136
137 config = {
138 systemd.services = mapAttrs' (name: value: {
139 name = "openconnect-${name}";
140 value = generateUnit name value;
141 }) cfg.interfaces;
142 };
143
144 meta.maintainers = with maintainers; [ alyaeanyx ];
145}