1{ config, lib, pkgs, ... }:
2with lib;
3let
4 keysPath = "/var/lib/yggdrasil/keys.json";
5
6 cfg = config.services.yggdrasil;
7 settingsProvided = cfg.settings != { };
8 configFileProvided = cfg.configFile != null;
9
10 format = pkgs.formats.json { };
11in
12{
13 imports = [
14 (mkRenamedOptionModule
15 [ "services" "yggdrasil" "config" ]
16 [ "services" "yggdrasil" "settings" ])
17 ];
18
19 options = with types; {
20 services.yggdrasil = {
21 enable = mkEnableOption "the yggdrasil system service";
22
23 settings = mkOption {
24 type = format.type;
25 default = { };
26 example = {
27 Peers = [
28 "tcp://aa.bb.cc.dd:eeeee"
29 "tcp://[aaaa:bbbb:cccc:dddd::eeee]:fffff"
30 ];
31 Listen = [
32 "tcp://0.0.0.0:xxxxx"
33 ];
34 };
35 description = ''
36 Configuration for yggdrasil, as a Nix attribute set.
37
38 Warning: this is stored in the WORLD-READABLE Nix store!
39 Therefore, it is not appropriate for private keys. If you
40 wish to specify the keys, use {option}`configFile`.
41
42 If the {option}`persistentKeys` is enabled then the
43 keys that are generated during activation will override
44 those in {option}`settings` or
45 {option}`configFile`.
46
47 If no keys are specified then ephemeral keys are generated
48 and the Yggdrasil interface will have a random IPv6 address
49 each time the service is started. This is the default.
50
51 If both {option}`configFile` and {option}`settings`
52 are supplied, they will be combined, with values from
53 {option}`configFile` taking precedence.
54
55 You can use the command `nix-shell -p yggdrasil --run "yggdrasil -genconf"`
56 to generate default configuration values with documentation.
57 '';
58 };
59
60 configFile = mkOption {
61 type = nullOr path;
62 default = null;
63 example = "/run/keys/yggdrasil.conf";
64 description = ''
65 A file which contains JSON or HJSON configuration for yggdrasil. See
66 the {option}`settings` option for more information.
67
68 Note: This file must not be larger than 1 MB because it is passed to
69 the yggdrasil process via systemd‘s LoadCredential mechanism. For
70 details, see <https://systemd.io/CREDENTIALS/> and `man 5
71 systemd.exec`.
72 '';
73 };
74
75 group = mkOption {
76 type = types.nullOr types.str;
77 default = null;
78 example = "wheel";
79 description = "Group to grant access to the Yggdrasil control socket. If `null`, only root can access the socket.";
80 };
81
82 openMulticastPort = mkOption {
83 type = bool;
84 default = false;
85 description = ''
86 Whether to open the UDP port used for multicast peer discovery. The
87 NixOS firewall blocks link-local communication, so in order to make
88 incoming local peering work you will also need to configure
89 `MulticastInterfaces` in your Yggdrasil configuration
90 ({option}`settings` or {option}`configFile`). You will then have to
91 add the ports that you configure there to your firewall configuration
92 ({option}`networking.firewall.allowedTCPPorts` or
93 {option}`networking.firewall.interfaces.<name>.allowedTCPPorts`).
94 '';
95 };
96
97 denyDhcpcdInterfaces = mkOption {
98 type = listOf str;
99 default = [ ];
100 example = [ "tap*" ];
101 description = ''
102 Disable the DHCP client for any interface whose name matches
103 any of the shell glob patterns in this list. Use this
104 option to prevent the DHCP client from broadcasting requests
105 on the yggdrasil network. It is only necessary to do so
106 when yggdrasil is running in TAP mode, because TUN
107 interfaces do not support broadcasting.
108 '';
109 };
110
111 package = mkPackageOption pkgs "yggdrasil" { };
112
113 persistentKeys = mkEnableOption ''
114 persistent keys. If enabled then keys will be generated once and Yggdrasil
115 will retain the same IPv6 address when the service is
116 restarted. Keys are stored at ${keysPath}
117 '';
118
119 extraArgs = mkOption {
120 type = listOf str;
121 default = [ ];
122 example = [ "-loglevel" "info" ];
123 description = "Extra command line arguments.";
124 };
125
126 };
127 };
128
129 config = mkIf cfg.enable (
130 let
131 binYggdrasil = "${cfg.package}/bin/yggdrasil";
132 binHjson = "${pkgs.hjson-go}/bin/hjson-cli";
133 in
134 {
135 assertions = [{
136 assertion = config.networking.enableIPv6;
137 message = "networking.enableIPv6 must be true for yggdrasil to work";
138 }];
139
140 # This needs to be a separate service. The yggdrasil service fails if
141 # this is put into its preStart.
142 systemd.services.yggdrasil-persistent-keys = lib.mkIf cfg.persistentKeys {
143 wantedBy = [ "multi-user.target" ];
144 before = [ "yggdrasil.service" ];
145 serviceConfig.Type = "oneshot";
146 serviceConfig.RemainAfterExit = true;
147 script = ''
148 if [ ! -e ${keysPath} ]
149 then
150 mkdir --mode=700 -p ${builtins.dirOf keysPath}
151 ${binYggdrasil} -genconf -json \
152 | ${pkgs.jq}/bin/jq \
153 'to_entries|map(select(.key|endswith("Key")))|from_entries' \
154 > ${keysPath}
155 fi
156 '';
157 };
158
159 systemd.services.yggdrasil = {
160 description = "Yggdrasil Network Service";
161 after = [ "network-pre.target" ];
162 wants = [ "network.target" ];
163 before = [ "network.target" ];
164 wantedBy = [ "multi-user.target" ];
165
166 # This script first prepares the config file, then it starts Yggdrasil.
167 # The preparation could also be done in ExecStartPre/preStart but only
168 # systemd versions >= v252 support reading credentials in ExecStartPre. As
169 # of February 2023, systemd v252 is not yet in the stable branch of NixOS.
170 #
171 # This could be changed in the future once systemd version v252 has
172 # reached NixOS but it does not have to be. Config file preparation is
173 # fast enough, it does not need elevated privileges, and `set -euo
174 # pipefail` should make sure that the service is not started if the
175 # preparation fails. Therefore, it is not necessary to move the
176 # preparation to ExecStartPre.
177 script = ''
178 set -euo pipefail
179
180 # prepare config file
181 ${(if settingsProvided || configFileProvided || cfg.persistentKeys then
182 "echo "
183
184 + (lib.optionalString settingsProvided
185 "'${builtins.toJSON cfg.settings}'")
186 + (lib.optionalString configFileProvided
187 "$(${binHjson} -c \"$CREDENTIALS_DIRECTORY/yggdrasil.conf\")")
188 + (lib.optionalString cfg.persistentKeys "$(cat ${keysPath})")
189 + " | ${pkgs.jq}/bin/jq -s add | ${binYggdrasil} -normaliseconf -useconf"
190 else
191 "${binYggdrasil} -genconf") + " > /run/yggdrasil/yggdrasil.conf"}
192
193 # start yggdrasil
194 ${binYggdrasil} -useconffile /run/yggdrasil/yggdrasil.conf ${lib.strings.escapeShellArgs cfg.extraArgs}
195 '';
196
197 serviceConfig = {
198 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
199 Restart = "always";
200
201 DynamicUser = true;
202 StateDirectory = "yggdrasil";
203 RuntimeDirectory = "yggdrasil";
204 RuntimeDirectoryMode = "0750";
205 BindReadOnlyPaths = lib.optional cfg.persistentKeys keysPath;
206 LoadCredential =
207 mkIf configFileProvided "yggdrasil.conf:${cfg.configFile}";
208
209 AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
210 CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
211 MemoryDenyWriteExecute = true;
212 ProtectControlGroups = true;
213 ProtectHome = "tmpfs";
214 ProtectKernelModules = true;
215 ProtectKernelTunables = true;
216 RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
217 RestrictNamespaces = true;
218 RestrictRealtime = true;
219 SystemCallArchitectures = "native";
220 SystemCallFilter = [ "@system-service" "~@privileged @keyring" ];
221 } // (if (cfg.group != null) then {
222 Group = cfg.group;
223 } else { });
224 };
225
226 networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces;
227 networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ];
228
229 # Make yggdrasilctl available on the command line.
230 environment.systemPackages = [ cfg.package ];
231 }
232 );
233 meta = {
234 doc = ./yggdrasil.md;
235 maintainers = with lib.maintainers; [ gazally ehmry ];
236 };
237}