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