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 imports = [
13 (mkRenamedOptionModule
14 [ "services" "yggdrasil" "config" ]
15 [ "services" "yggdrasil" "settings" ])
16 ];
17
18 options = with types; {
19 services.yggdrasil = {
20 enable = mkEnableOption (lib.mdDoc "the yggdrasil system service");
21
22 settings = mkOption {
23 type = format.type;
24 default = {};
25 example = {
26 Peers = [
27 "tcp://aa.bb.cc.dd:eeeee"
28 "tcp://[aaaa:bbbb:cccc:dddd::eeee]:fffff"
29 ];
30 Listen = [
31 "tcp://0.0.0.0:xxxxx"
32 ];
33 };
34 description = lib.mdDoc ''
35 Configuration for yggdrasil, as a Nix attribute set.
36
37 Warning: this is stored in the WORLD-READABLE Nix store!
38 Therefore, it is not appropriate for private keys. If you
39 wish to specify the keys, use {option}`configFile`.
40
41 If the {option}`persistentKeys` is enabled then the
42 keys that are generated during activation will override
43 those in {option}`settings` or
44 {option}`configFile`.
45
46 If no keys are specified then ephemeral keys are generated
47 and the Yggdrasil interface will have a random IPv6 address
48 each time the service is started, this is the default.
49
50 If both {option}`configFile` and {option}`settings`
51 are supplied, they will be combined, with values from
52 {option}`configFile` taking precedence.
53
54 You can use the command `nix-shell -p yggdrasil --run "yggdrasil -genconf"`
55 to generate default configuration values with documentation.
56 '';
57 };
58
59 configFile = mkOption {
60 type = nullOr path;
61 default = null;
62 example = "/run/keys/yggdrasil.conf";
63 description = lib.mdDoc ''
64 A file which contains JSON configuration for yggdrasil.
65 See the {option}`settings` option for more information.
66 '';
67 };
68
69 group = mkOption {
70 type = types.nullOr types.str;
71 default = null;
72 example = "wheel";
73 description = lib.mdDoc "Group to grant access to the Yggdrasil control socket. If `null`, only root can access the socket.";
74 };
75
76 openMulticastPort = mkOption {
77 type = bool;
78 default = false;
79 description = lib.mdDoc ''
80 Whether to open the UDP port used for multicast peer
81 discovery. The NixOS firewall blocks link-local
82 communication, so in order to make local peering work you
83 will also need to set `LinkLocalTCPPort` in your
84 yggdrasil configuration ({option}`settings` or
85 {option}`configFile`) to a port number other than 0,
86 and then add that port to
87 {option}`networking.firewall.allowedTCPPorts`.
88 '';
89 };
90
91 denyDhcpcdInterfaces = mkOption {
92 type = listOf str;
93 default = [];
94 example = [ "tap*" ];
95 description = lib.mdDoc ''
96 Disable the DHCP client for any interface whose name matches
97 any of the shell glob patterns in this list. Use this
98 option to prevent the DHCP client from broadcasting requests
99 on the yggdrasil network. It is only necessary to do so
100 when yggdrasil is running in TAP mode, because TUN
101 interfaces do not support broadcasting.
102 '';
103 };
104
105 package = mkOption {
106 type = package;
107 default = pkgs.yggdrasil;
108 defaultText = literalExpression "pkgs.yggdrasil";
109 description = lib.mdDoc "Yggdrasil package to use.";
110 };
111
112 persistentKeys = mkEnableOption (lib.mdDoc ''
113 If enabled then keys will be generated once and Yggdrasil
114 will retain the same IPv6 address when the service is
115 restarted. Keys are stored at ${keysPath}.
116 '');
117
118 };
119 };
120
121 config = mkIf cfg.enable (let binYggdrasil = cfg.package + "/bin/yggdrasil";
122 in {
123 assertions = [{
124 assertion = config.networking.enableIPv6;
125 message = "networking.enableIPv6 must be true for yggdrasil to work";
126 }];
127
128 system.activationScripts.yggdrasil = mkIf cfg.persistentKeys ''
129 if [ ! -e ${keysPath} ]
130 then
131 mkdir --mode=700 -p ${builtins.dirOf keysPath}
132 ${binYggdrasil} -genconf -json \
133 | ${pkgs.jq}/bin/jq \
134 'to_entries|map(select(.key|endswith("Key")))|from_entries' \
135 > ${keysPath}
136 fi
137 '';
138
139 systemd.services.yggdrasil = {
140 description = "Yggdrasil Network Service";
141 after = [ "network-pre.target" ];
142 wants = [ "network.target" ];
143 before = [ "network.target" ];
144 wantedBy = [ "multi-user.target" ];
145
146 preStart =
147 (if settingsProvided || configFileProvided || cfg.persistentKeys then
148 "echo "
149
150 + (lib.optionalString settingsProvided
151 "'${builtins.toJSON cfg.settings}'")
152 + (lib.optionalString configFileProvided "$(cat ${cfg.configFile})")
153 + (lib.optionalString cfg.persistentKeys "$(cat ${keysPath})")
154 + " | ${pkgs.jq}/bin/jq -s add | ${binYggdrasil} -normaliseconf -useconf"
155 else
156 "${binYggdrasil} -genconf") + " > /run/yggdrasil/yggdrasil.conf";
157
158 serviceConfig = {
159 ExecStart =
160 "${binYggdrasil} -useconffile /run/yggdrasil/yggdrasil.conf";
161 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
162 Restart = "always";
163
164 DynamicUser = true;
165 StateDirectory = "yggdrasil";
166 RuntimeDirectory = "yggdrasil";
167 RuntimeDirectoryMode = "0750";
168 BindReadOnlyPaths = lib.optional configFileProvided cfg.configFile
169 ++ lib.optional cfg.persistentKeys keysPath;
170 ReadWritePaths = "/run/yggdrasil";
171
172 AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
173 CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
174 MemoryDenyWriteExecute = true;
175 ProtectControlGroups = true;
176 ProtectHome = "tmpfs";
177 ProtectKernelModules = true;
178 ProtectKernelTunables = true;
179 RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
180 RestrictNamespaces = true;
181 RestrictRealtime = true;
182 SystemCallArchitectures = "native";
183 SystemCallFilter = [ "@system-service" "~@privileged @keyring" ];
184 } // (if (cfg.group != null) then {
185 Group = cfg.group;
186 } else {});
187 };
188
189 networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces;
190 networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ];
191
192 # Make yggdrasilctl available on the command line.
193 environment.systemPackages = [ cfg.package ];
194 });
195 meta = {
196 doc = ./yggdrasil.xml;
197 maintainers = with lib.maintainers; [ gazally ehmry ];
198 };
199}