1{ lib, config, pkgs, ... }:
2with lib;
3let
4 cfg = config.services.wgautomesh;
5 settingsFormat = pkgs.formats.toml { };
6 configFile =
7 # Have to remove nulls manually as TOML generator will not just skip key
8 # if value is null
9 settingsFormat.generate "wgautomesh-config.toml"
10 (filterAttrs (k: v: v != null)
11 (mapAttrs
12 (k: v:
13 if k == "peers"
14 then map (e: filterAttrs (k: v: v != null) e) v
15 else v)
16 cfg.settings));
17 runtimeConfigFile =
18 if cfg.enableGossipEncryption
19 then "/run/wgautomesh/wgautomesh.toml"
20 else configFile;
21in
22{
23 options.services.wgautomesh = {
24 enable = mkEnableOption "the wgautomesh daemon";
25 logLevel = mkOption {
26 type = types.enum [ "trace" "debug" "info" "warn" "error" ];
27 default = "info";
28 description = "wgautomesh log level.";
29 };
30 enableGossipEncryption = mkOption {
31 type = types.bool;
32 default = true;
33 description = "Enable encryption of gossip traffic.";
34 };
35 gossipSecretFile = mkOption {
36 type = types.path;
37 description = ''
38 File containing the gossip secret, a shared secret key to use for gossip
39 encryption. Required if `enableGossipEncryption` is set. This file
40 may contain any arbitrary-length utf8 string. To generate a new gossip
41 secret, use a command such as `openssl rand -base64 32`.
42 '';
43 };
44 enablePersistence = mkOption {
45 type = types.bool;
46 default = true;
47 description = "Enable persistence of Wireguard peer info between restarts.";
48 };
49 openFirewall = mkOption {
50 type = types.bool;
51 default = true;
52 description = "Automatically open gossip port in firewall (recommended).";
53 };
54 settings = mkOption {
55 type = types.submodule {
56 freeformType = settingsFormat.type;
57 options = {
58
59 interface = mkOption {
60 type = types.str;
61 description = ''
62 Wireguard interface to manage (it is NOT created by wgautomesh, you
63 should use another NixOS option to create it such as
64 `networking.wireguard.interfaces.wg0 = {...};`).
65 '';
66 example = "wg0";
67 };
68 gossip_port = mkOption {
69 type = types.port;
70 description = ''
71 wgautomesh gossip port, this MUST be the same number on all nodes in
72 the wgautomesh network.
73 '';
74 default = 1666;
75 };
76 lan_discovery = mkOption {
77 type = types.bool;
78 default = true;
79 description = "Enable discovery of peers on the same LAN using UDP broadcast.";
80 };
81 upnp_forward_external_port = mkOption {
82 type = types.nullOr types.port;
83 default = null;
84 description = ''
85 Public port number to try to redirect to this machine's Wireguard
86 daemon using UPnP IGD.
87 '';
88 };
89 peers = mkOption {
90 type = types.listOf (types.submodule {
91 options = {
92 pubkey = mkOption {
93 type = types.str;
94 description = "Wireguard public key of this peer.";
95 };
96 address = mkOption {
97 type = types.str;
98 description = ''
99 Wireguard address of this peer (a single IP address, multiple
100 addresses or address ranges are not supported).
101 '';
102 example = "10.0.0.42";
103 };
104 endpoint = mkOption {
105 type = types.nullOr types.str;
106 description = ''
107 Bootstrap endpoint for connecting to this Wireguard peer if no
108 other address is known or none are working.
109 '';
110 default = null;
111 example = "wgnode.mydomain.example:51820";
112 };
113 };
114 });
115 default = [ ];
116 description = "wgautomesh peer list.";
117 };
118 };
119
120 };
121 default = { };
122 description = "Configuration for wgautomesh.";
123 };
124 };
125
126 config = mkIf cfg.enable {
127 services.wgautomesh.settings = {
128 gossip_secret_file = mkIf cfg.enableGossipEncryption "$CREDENTIALS_DIRECTORY/gossip_secret";
129 persist_file = mkIf cfg.enablePersistence "/var/lib/wgautomesh/state";
130 };
131
132 systemd.services.wgautomesh = {
133 path = [ pkgs.wireguard-tools ];
134 environment = { RUST_LOG = "wgautomesh=${cfg.logLevel}"; };
135 description = "wgautomesh";
136 serviceConfig = {
137 Type = "simple";
138
139 ExecStart = "${getExe pkgs.wgautomesh} ${runtimeConfigFile}";
140 Restart = "always";
141 RestartSec = "30";
142 LoadCredential = mkIf cfg.enableGossipEncryption [ "gossip_secret:${cfg.gossipSecretFile}" ];
143
144 ExecStartPre = mkIf cfg.enableGossipEncryption [
145 ''${pkgs.envsubst}/bin/envsubst \
146 -i ${configFile} \
147 -o ${runtimeConfigFile}''
148 ];
149
150 DynamicUser = true;
151 StateDirectory = "wgautomesh";
152 StateDirectoryMode = "0700";
153 RuntimeDirectory = "wgautomesh";
154 AmbientCapabilities = "CAP_NET_ADMIN";
155 CapabilityBoundingSet = "CAP_NET_ADMIN";
156 };
157 wantedBy = [ "multi-user.target" ];
158 };
159 networking.firewall.allowedUDPPorts =
160 mkIf cfg.openFirewall [ cfg.settings.gossip_port ];
161 };
162}
163