1{ config
2, lib
3, options
4, pkgs
5, ...
6}:
7let
8 inherit (lib)
9 attrValues
10 concatLines
11 concatMap
12 filter
13 filterAttrsRecursive
14 flatten
15 getExe
16 mkIf
17 optional
18 ;
19
20 cfg = config.services.rosenpass;
21 opt = options.services.rosenpass;
22 settingsFormat = pkgs.formats.toml { };
23in
24{
25 options.services.rosenpass =
26 let
27 inherit (lib)
28 literalExpression
29 mkOption
30 ;
31 inherit (lib.types)
32 enum
33 listOf
34 nullOr
35 path
36 str
37 submodule
38 ;
39 in
40 {
41 enable = lib.mkEnableOption "Rosenpass";
42
43 package = lib.mkPackageOption pkgs "rosenpass" { };
44
45 defaultDevice = mkOption {
46 type = nullOr str;
47 description = "Name of the network interface to use for all peers by default.";
48 example = "wg0";
49 };
50
51 settings = mkOption {
52 type = submodule {
53 freeformType = settingsFormat.type;
54
55 options = {
56 public_key = mkOption {
57 type = path;
58 description = "Path to a file containing the public key of the local Rosenpass peer. Generate this by running {command}`rosenpass gen-keys`.";
59 };
60
61 secret_key = mkOption {
62 type = path;
63 description = "Path to a file containing the secret key of the local Rosenpass peer. Generate this by running {command}`rosenpass gen-keys`.";
64 };
65
66 listen = mkOption {
67 type = listOf str;
68 description = "List of local endpoints to listen for connections.";
69 default = [ ];
70 example = literalExpression "[ \"0.0.0.0:10000\" ]";
71 };
72
73 verbosity = mkOption {
74 type = enum [ "Verbose" "Quiet" ];
75 default = "Quiet";
76 description = "Verbosity of output produced by the service.";
77 };
78
79 peers =
80 let
81 peer = submodule {
82 freeformType = settingsFormat.type;
83
84 options = {
85 public_key = mkOption {
86 type = path;
87 description = "Path to a file containing the public key of the remote Rosenpass peer.";
88 };
89
90 endpoint = mkOption {
91 type = nullOr str;
92 default = null;
93 description = "Endpoint of the remote Rosenpass peer.";
94 };
95
96 device = mkOption {
97 type = str;
98 default = cfg.defaultDevice;
99 defaultText = literalExpression "config.${opt.defaultDevice}";
100 description = "Name of the local WireGuard interface to use for this peer.";
101 };
102
103 peer = mkOption {
104 type = str;
105 description = "WireGuard public key corresponding to the remote Rosenpass peer.";
106 };
107 };
108 };
109 in
110 mkOption {
111 type = listOf peer;
112 description = "List of peers to exchange keys with.";
113 default = [ ];
114 };
115 };
116 };
117 default = { };
118 description = "Configuration for Rosenpass, see <https://rosenpass.eu/> for further information.";
119 };
120 };
121
122 config = mkIf cfg.enable {
123 warnings =
124 let
125 # NOTE: In the descriptions below, we tried to refer to e.g.
126 # options.systemd.network.netdevs."<name>".wireguardPeers.*.PublicKey
127 # directly, but don't know how to traverse "<name>" and * in this path.
128 extractions = [
129 {
130 relevant = config.systemd.network.enable;
131 root = config.systemd.network.netdevs;
132 peer = (x: x.wireguardPeers);
133 key = (x: if x.wireguardPeerConfig ? PublicKey then x.wireguardPeerConfig.PublicKey else null);
134 description = "${options.systemd.network.netdevs}.\"<name>\".wireguardPeers.*.wireguardPeerConfig.PublicKey";
135 }
136 {
137 relevant = config.networking.wireguard.enable;
138 root = config.networking.wireguard.interfaces;
139 peer = (x: x.peers);
140 key = (x: x.publicKey);
141 description = "${options.networking.wireguard.interfaces}.\"<name>\".peers.*.publicKey";
142 }
143 rec {
144 relevant = root != { };
145 root = config.networking.wg-quick.interfaces;
146 peer = (x: x.peers);
147 key = (x: x.publicKey);
148 description = "${options.networking.wg-quick.interfaces}.\"<name>\".peers.*.publicKey";
149 }
150 ];
151 relevantExtractions = filter (x: x.relevant) extractions;
152 extract = { root, peer, key, ... }:
153 filter (x: x != null) (flatten (concatMap (x: (map key (peer x))) (attrValues root)));
154 configuredKeys = flatten (map extract relevantExtractions);
155 itemize = xs: concatLines (map (x: " - ${x}") xs);
156 descriptions = map (x: "`${x.description}`");
157 missingKeys = filter (key: !builtins.elem key configuredKeys) (map (x: x.peer) cfg.settings.peers);
158 unusual = ''
159 While this may work as expected, e.g. you want to manually configure WireGuard,
160 such a scenario is unusual. Please double-check your configuration.
161 '';
162 in
163 (optional (relevantExtractions != [ ] && missingKeys != [ ]) ''
164 You have configured Rosenpass peers with the WireGuard public keys:
165 ${itemize missingKeys}
166 But there is no corresponding active Wireguard peer configuration in any of:
167 ${itemize (descriptions relevantExtractions)}
168 ${unusual}
169 '')
170 ++
171 optional (relevantExtractions == [ ]) ''
172 You have configured Rosenpass, but you have not configured Wireguard via any of:
173 ${itemize (descriptions extractions)}
174 ${unusual}
175 '';
176
177 environment.systemPackages = [ cfg.package pkgs.wireguard-tools ];
178
179 systemd.services.rosenpass =
180 let
181 filterNonNull = filterAttrsRecursive (_: v: v != null);
182 config = settingsFormat.generate "config.toml" (
183 filterNonNull (cfg.settings
184 //
185 (
186 let
187 credentialPath = id: "$CREDENTIALS_DIRECTORY/${id}";
188 # NOTE: We would like to remove all `null` values inside `cfg.settings`
189 # recursively, since `settingsFormat.generate` cannot handle `null`.
190 # This would require to traverse both attribute sets and lists recursively.
191 # `filterAttrsRecursive` only recurses into attribute sets, but not
192 # into values that might contain other attribute sets (such as lists,
193 # e.g. `cfg.settings.peers`). Here, we just specialize on `cfg.settings.peers`,
194 # and this may break unexpectedly whenever a `null` value is contained
195 # in a list in `cfg.settings`, other than `cfg.settings.peers`.
196 peersWithoutNulls = map filterNonNull cfg.settings.peers;
197 in
198 {
199 secret_key = credentialPath "pqsk";
200 public_key = credentialPath "pqpk";
201 peers = peersWithoutNulls;
202 }
203 )
204 )
205 );
206 in
207 rec {
208 wantedBy = [ "multi-user.target" ];
209 wants = [ "network-online.target" ];
210 after = [ "network-online.target" ];
211 path = [ cfg.package pkgs.wireguard-tools ];
212
213 serviceConfig = {
214 User = "rosenpass";
215 Group = "rosenpass";
216 RuntimeDirectory = "rosenpass";
217 DynamicUser = true;
218 AmbientCapabilities = [ "CAP_NET_ADMIN" ];
219 LoadCredential = [
220 "pqsk:${cfg.settings.secret_key}"
221 "pqpk:${cfg.settings.public_key}"
222 ];
223 };
224
225 # See <https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers>
226 environment.CONFIG = "%t/${serviceConfig.RuntimeDirectory}/config.toml";
227
228 script = ''
229 ${getExe pkgs.envsubst} -i ${config} -o "$CONFIG"
230 rosenpass exchange-config "$CONFIG"
231 '';
232 };
233 };
234}