normcap: fix on GNOME wayland when used via keybind or alt-f2 (#351763)
[NixPkgs.git] / nixos / modules / services / networking / rosenpass.nix
blob2ad2196179f8a7d7a62226622968b2debe01d33e
2   config,
3   lib,
4   options,
5   pkgs,
6   ...
7 }:
8 let
9   inherit (lib)
10     attrValues
11     concatLines
12     concatMap
13     filter
14     filterAttrsRecursive
15     flatten
16     getExe
17     mkIf
18     optional
19     ;
21   cfg = config.services.rosenpass;
22   opt = options.services.rosenpass;
23   settingsFormat = pkgs.formats.toml { };
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";
44       package = lib.mkPackageOption pkgs "rosenpass" { };
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       };
52       settings = mkOption {
53         type = submodule {
54           freeformType = settingsFormat.type;
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             };
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             };
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             };
74             verbosity = mkOption {
75               type = enum [
76                 "Verbose"
77                 "Quiet"
78               ];
79               default = "Quiet";
80               description = "Verbosity of output produced by the service.";
81             };
83             peers =
84               let
85                 peer = submodule {
86                   freeformType = settingsFormat.type;
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                     };
94                     endpoint = mkOption {
95                       type = nullOr str;
96                       default = null;
97                       description = "Endpoint of the remote Rosenpass peer.";
98                     };
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                     };
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     };
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       '';
186     environment.systemPackages = [
187       cfg.package
188       pkgs.wireguard-tools
189     ];
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         ];
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         };
240         # See <https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers>
241         environment.CONFIG = "%t/${serviceConfig.RuntimeDirectory}/config.toml";
243         script = ''
244           ${getExe pkgs.envsubst} -i ${config} -o "$CONFIG"
245           rosenpass exchange-config "$CONFIG"
246         '';
247       };
248   };