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