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