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 (lib.mdDoc "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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc "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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = mkOption {
112 type = package;
113 default = pkgs.yggdrasil;
114 defaultText = literalExpression "pkgs.yggdrasil";
115 description = lib.mdDoc "Yggdrasil package to use.";
116 };
117
118 persistentKeys = mkEnableOption (lib.mdDoc ''
119 If enabled then keys will be generated once and Yggdrasil
120 will retain the same IPv6 address when the service is
121 restarted. Keys are stored at ${keysPath}.
122 '');
123
124 };
125 };
126
127 config = mkIf cfg.enable (
128 let
129 binYggdrasil = "${cfg.package}/bin/yggdrasil";
130 binHjson = "${pkgs.hjson-go}/bin/hjson-cli";
131 in
132 {
133 assertions = [{
134 assertion = config.networking.enableIPv6;
135 message = "networking.enableIPv6 must be true for yggdrasil to work";
136 }];
137
138 system.activationScripts.yggdrasil = mkIf cfg.persistentKeys ''
139 if [ ! -e ${keysPath} ]
140 then
141 mkdir --mode=700 -p ${builtins.dirOf keysPath}
142 ${binYggdrasil} -genconf -json \
143 | ${pkgs.jq}/bin/jq \
144 'to_entries|map(select(.key|endswith("Key")))|from_entries' \
145 > ${keysPath}
146 fi
147 '';
148
149 systemd.services.yggdrasil = {
150 description = "Yggdrasil Network Service";
151 after = [ "network-pre.target" ];
152 wants = [ "network.target" ];
153 before = [ "network.target" ];
154 wantedBy = [ "multi-user.target" ];
155
156 # This script first prepares the config file, then it starts Yggdrasil.
157 # The preparation could also be done in ExecStartPre/preStart but only
158 # systemd versions >= v252 support reading credentials in ExecStartPre. As
159 # of February 2023, systemd v252 is not yet in the stable branch of NixOS.
160 #
161 # This could be changed in the future once systemd version v252 has
162 # reached NixOS but it does not have to be. Config file preparation is
163 # fast enough, it does not need elevated privileges, and `set -euo
164 # pipefail` should make sure that the service is not started if the
165 # preparation fails. Therefore, it is not necessary to move the
166 # preparation to ExecStartPre.
167 script = ''
168 set -euo pipefail
169
170 # prepare config file
171 ${(if settingsProvided || configFileProvided || cfg.persistentKeys then
172 "echo "
173
174 + (lib.optionalString settingsProvided
175 "'${builtins.toJSON cfg.settings}'")
176 + (lib.optionalString configFileProvided
177 "$(${binHjson} -c \"$CREDENTIALS_DIRECTORY/yggdrasil.conf\")")
178 + (lib.optionalString cfg.persistentKeys "$(cat ${keysPath})")
179 + " | ${pkgs.jq}/bin/jq -s add | ${binYggdrasil} -normaliseconf -useconf"
180 else
181 "${binYggdrasil} -genconf") + " > /run/yggdrasil/yggdrasil.conf"}
182
183 # start yggdrasil
184 ${binYggdrasil} -useconffile /run/yggdrasil/yggdrasil.conf
185 '';
186
187 serviceConfig = {
188 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
189 Restart = "always";
190
191 DynamicUser = true;
192 StateDirectory = "yggdrasil";
193 RuntimeDirectory = "yggdrasil";
194 RuntimeDirectoryMode = "0750";
195 BindReadOnlyPaths = lib.optional cfg.persistentKeys keysPath;
196 LoadCredential =
197 mkIf configFileProvided "yggdrasil.conf:${cfg.configFile}";
198
199 AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
200 CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
201 MemoryDenyWriteExecute = true;
202 ProtectControlGroups = true;
203 ProtectHome = "tmpfs";
204 ProtectKernelModules = true;
205 ProtectKernelTunables = true;
206 RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
207 RestrictNamespaces = true;
208 RestrictRealtime = true;
209 SystemCallArchitectures = "native";
210 SystemCallFilter = [ "@system-service" "~@privileged @keyring" ];
211 } // (if (cfg.group != null) then {
212 Group = cfg.group;
213 } else { });
214 };
215
216 networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces;
217 networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ];
218
219 # Make yggdrasilctl available on the command line.
220 environment.systemPackages = [ cfg.package ];
221 }
222 );
223 meta = {
224 doc = ./yggdrasil.md;
225 maintainers = with lib.maintainers; [ gazally ehmry ];
226 };
227}