parent
3cc9bcb338
commit
b8cb8c39d6
@ -0,0 +1,181 @@ |
||||
{ config, lib, pkgs, ... }: |
||||
with lib; |
||||
let |
||||
cfg = config.services.yggdrasil; |
||||
configProvided = (cfg.config != {}); |
||||
configAsFile = (if configProvided then |
||||
toString (pkgs.writeTextFile { |
||||
name = "yggdrasil-conf"; |
||||
text = builtins.toJSON cfg.config; |
||||
}) |
||||
else null); |
||||
configFileProvided = (cfg.configFile != null); |
||||
generateConfig = ( |
||||
if configProvided && configFileProvided then |
||||
"${pkgs.jq}/bin/jq -s add /run/yggdrasil/configFile.json ${configAsFile}" |
||||
else if configProvided then |
||||
"cat ${configAsFile}" |
||||
else if configFileProvided then |
||||
"cat /run/yggdrasil/configFile.json" |
||||
else |
||||
"${cfg.package}/bin/yggdrasil -genconf" |
||||
); |
||||
|
||||
in { |
||||
options = with types; { |
||||
services.yggdrasil = { |
||||
enable = mkEnableOption "the yggdrasil system service"; |
||||
|
||||
configFile = mkOption { |
||||
type = nullOr str; |
||||
default = null; |
||||
example = "/run/keys/yggdrasil.conf"; |
||||
description = '' |
||||
A file which contains JSON configuration for yggdrasil. |
||||
|
||||
You do not have to supply a complete configuration, as |
||||
yggdrasil will use default values for anything which is |
||||
omitted. If the encryption and signing keys are omitted, |
||||
yggdrasil will generate new ones each time the service is |
||||
started, resulting in a random IPv6 address on the yggdrasil |
||||
network each time. |
||||
|
||||
If both this option and <option>config</option> are |
||||
supplied, they will be combined, with values from |
||||
<option>config</option> taking precedence. |
||||
|
||||
You can use the command <code>nix-shell -p yggdrasil --run |
||||
"yggdrasil -genconf -json"</code> to generate a default |
||||
JSON configuration. |
||||
''; |
||||
}; |
||||
|
||||
config = mkOption { |
||||
type = attrs; |
||||
default = {}; |
||||
example = { |
||||
Peers = [ |
||||
"tcp://aa.bb.cc.dd:eeeee" |
||||
"tcp://[aaaa:bbbb:cccc:dddd::eeee]:fffff" |
||||
]; |
||||
Listen = [ |
||||
"tcp://0.0.0.0:xxxxx" |
||||
]; |
||||
}; |
||||
description = '' |
||||
Configuration for yggdrasil, as a Nix attribute set. |
||||
|
||||
Warning: this is stored in the WORLD-READABLE Nix store! |
||||
Therefore, it is not appropriate for private keys. If you |
||||
do not specify the keys, yggdrasil will generate a new set |
||||
each time the service is started, creating a random IPv6 |
||||
address on the yggdrasil network each time. |
||||
|
||||
If you wish to specify the keys, use |
||||
<option>configFile</option>. If both |
||||
<option>configFile</option> and <option>config</option> are |
||||
supplied, they will be combined, with values from |
||||
<option>config</option> taking precedence. |
||||
|
||||
You can use the command <code>nix-shell -p yggdrasil --run |
||||
"yggdrasil -genconf"</code> to generate default |
||||
configuration values with documentation. |
||||
''; |
||||
}; |
||||
|
||||
openMulticastPort = mkOption { |
||||
type = bool; |
||||
default = false; |
||||
description = '' |
||||
Whether to open the UDP port used for multicast peer |
||||
discovery. The NixOS firewall blocks link-local |
||||
communication, so in order to make local peering work you |
||||
will also need to set <code>LinkLocalTCPPort</code> in your |
||||
yggdrasil configuration (<option>config</option> or |
||||
<option>configFile</option>) to a port number other than 0, |
||||
and then add that port to |
||||
<option>networking.firewall.allowedTCPPorts</option>. |
||||
''; |
||||
}; |
||||
|
||||
denyDhcpcdInterfaces = mkOption { |
||||
type = listOf str; |
||||
default = []; |
||||
example = [ "tap*" ]; |
||||
description = '' |
||||
Disable the DHCP client for any interface whose name matches |
||||
any of the shell glob patterns in this list. Use this |
||||
option to prevent the DHCP client from broadcasting requests |
||||
on the yggdrasil network. It is only necessary to do so |
||||
when yggdrasil is running in TAP mode, because TUN |
||||
interfaces do not support broadcasting. |
||||
''; |
||||
}; |
||||
|
||||
package = mkOption { |
||||
type = package; |
||||
default = pkgs.yggdrasil; |
||||
defaultText = "pkgs.yggdrasil"; |
||||
description = "Yggdrasil package to use."; |
||||
}; |
||||
}; |
||||
}; |
||||
|
||||
config = mkIf cfg.enable { |
||||
assertions = [ |
||||
{ assertion = config.networking.enableIPv6; |
||||
message = "networking.enableIPv6 must be true for yggdrasil to work"; |
||||
} |
||||
]; |
||||
|
||||
environment.etc."yggdrasil.conf" = { |
||||
enable = true; |
||||
mode = "symlink"; |
||||
source = "/run/yggdrasil/yggdrasil.conf"; |
||||
}; |
||||
|
||||
systemd.services.yggdrasil = { |
||||
description = "Yggdrasil Network Service"; |
||||
path = [ cfg.package ] ++ optional (configProvided && configFileProvided) pkgs.jq; |
||||
bindsTo = [ "network-online.target" ]; |
||||
after = [ "network-online.target" ]; |
||||
wantedBy = [ "multi-user.target" ]; |
||||
|
||||
preStart = '' |
||||
${generateConfig} | yggdrasil -normaliseconf -useconf > /run/yggdrasil/yggdrasil.conf |
||||
''; |
||||
|
||||
serviceConfig = { |
||||
ExecStart = "${cfg.package}/bin/yggdrasil -useconffile /etc/yggdrasil.conf"; |
||||
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; |
||||
Restart = "always"; |
||||
|
||||
RuntimeDirectory = "yggdrasil"; |
||||
RuntimeDirectoryMode = "0700"; |
||||
BindReadOnlyPaths = mkIf configFileProvided |
||||
[ "${cfg.configFile}:/run/yggdrasil/configFile.json" ]; |
||||
|
||||
DynamicUser = true; |
||||
AmbientCapabilities = "CAP_NET_ADMIN"; |
||||
CapabilityBoundingSet = "CAP_NET_ADMIN"; |
||||
MemoryDenyWriteExecute = true; |
||||
ProtectControlGroups = true; |
||||
ProtectHome = "tmpfs"; |
||||
ProtectKernelModules = true; |
||||
ProtectKernelTunables = true; |
||||
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; |
||||
RestrictNamespaces = true; |
||||
RestrictRealtime = true; |
||||
SystemCallArchitectures = "native"; |
||||
SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @resources"; |
||||
}; |
||||
}; |
||||
|
||||
networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces; |
||||
networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ]; |
||||
|
||||
# Make yggdrasilctl available on the command line. |
||||
environment.systemPackages = [ cfg.package ]; |
||||
}; |
||||
meta.maintainers = with lib.maintainers; [ gazally ]; |
||||
} |
@ -0,0 +1,123 @@ |
||||
let |
||||
aliceIp6 = "200:3b91:b2d8:e708:fbf3:f06:fdd5:90d0"; |
||||
aliceKeys = { |
||||
EncryptionPublicKey = "13e23986fe76bc3966b42453f479bc563348b7ff76633b7efcb76e185ec7652f"; |
||||
EncryptionPrivateKey = "9f86947b15e86f9badac095517a1982e39a2db37ca726357f95987b898d82208"; |
||||
SigningPublicKey = "e2c43349083bc1e998e4ec4535b4c6a8f44ca9a5a8e07336561267253b2be5f4"; |
||||
SigningPrivateKey = "fe3add8da35316c05f6d90d3ca79bd2801e6ccab6d37e5339fef4152589398abe2c43349083bc1e998e4ec4535b4c6a8f44ca9a5a8e07336561267253b2be5f4"; |
||||
}; |
||||
bobIp6 = "201:ebbd:bde9:f138:c302:4afa:1fb6:a19a"; |
||||
bobConfig = { |
||||
InterfacePeers = { |
||||
eth1 = [ "tcp://192.168.1.200:12345" ]; |
||||
}; |
||||
MulticastInterfaces = [ "eth1" ]; |
||||
LinkLocalTCPPort = 54321; |
||||
EncryptionPublicKey = "c99d6830111e12d1b004c52fe9e5a2eef0f6aefca167aca14589a370b7373279"; |
||||
EncryptionPrivateKey = "2e698a53d3fdce5962d2ff37de0fe77742a5c8b56cd8259f5da6aa792f6e8ba3"; |
||||
SigningPublicKey = "de111da0ec781e45bf6c63ecb45a78c24d7d4655abfaeea83b26c36eb5c0fd5b"; |
||||
SigningPrivateKey = "2a6c21550f3fca0331df50668ffab66b6dce8237bcd5728e571e8033b363e247de111da0ec781e45bf6c63ecb45a78c24d7d4655abfaeea83b26c36eb5c0fd5b"; |
||||
}; |
||||
|
||||
in import ./make-test.nix ({ pkgs, ...} : { |
||||
name = "yggdrasil"; |
||||
meta = with pkgs.stdenv.lib.maintainers; { |
||||
maintainers = [ gazally ]; |
||||
}; |
||||
|
||||
nodes = rec { |
||||
# Alice is listening for peerings on a specified port, |
||||
# but has multicast peering disabled. Alice has part of her |
||||
# yggdrasil config in Nix and part of it in a file. |
||||
alice = |
||||
{ ... }: |
||||
{ |
||||
networking = { |
||||
interfaces.eth1.ipv4.addresses = [{ |
||||
address = "192.168.1.200"; |
||||
prefixLength = 24; |
||||
}]; |
||||
firewall.allowedTCPPorts = [ 80 12345 ]; |
||||
}; |
||||
services.httpd.enable = true; |
||||
services.httpd.adminAddr = "foo@example.org"; |
||||
|
||||
services.yggdrasil = { |
||||
enable = true; |
||||
config = { |
||||
Listen = ["tcp://0.0.0.0:12345"]; |
||||
MulticastInterfaces = [ ]; |
||||
}; |
||||
configFile = toString (pkgs.writeTextFile { |
||||
name = "yggdrasil-alice-conf"; |
||||
text = builtins.toJSON aliceKeys; |
||||
}); |
||||
}; |
||||
}; |
||||
|
||||
# Bob is set up to peer with Alice, and also to do local multicast |
||||
# peering. Bob's yggdrasil config is in a file. |
||||
bob = |
||||
{ ... }: |
||||
{ |
||||
networking.firewall.allowedTCPPorts = [ 54321 ]; |
||||
services.yggdrasil = { |
||||
enable = true; |
||||
openMulticastPort = true; |
||||
configFile = toString (pkgs.writeTextFile { |
||||
name = "yggdrasil-bob-conf"; |
||||
text = builtins.toJSON bobConfig; |
||||
}); |
||||
}; |
||||
}; |
||||
|
||||
# Carol only does local peering. Carol's yggdrasil config is all Nix. |
||||
carol = |
||||
{ ... }: |
||||
{ |
||||
networking.firewall.allowedTCPPorts = [ 43210 ]; |
||||
services.yggdrasil = { |
||||
enable = true; |
||||
denyDhcpcdInterfaces = [ "ygg0" ]; |
||||
config = { |
||||
IfTAPMode = true; |
||||
IFName = "ygg0"; |
||||
MulticastInterfaces = [ "eth1" ]; |
||||
LinkLocalTCPPort = 43210; |
||||
}; |
||||
}; |
||||
}; |
||||
}; |
||||
|
||||
testScript = |
||||
'' |
||||
# Give Alice a head start so she is ready when Bob calls. |
||||
$alice->start; |
||||
$alice->waitForUnit("yggdrasil.service"); |
||||
|
||||
$bob->start; |
||||
$carol->start; |
||||
$bob->waitForUnit("yggdrasil.service"); |
||||
$carol->waitForUnit("yggdrasil.service"); |
||||
|
||||
$carol->waitUntilSucceeds("[ `ip -o -6 addr show dev ygg0 scope global | grep -v tentative | wc -l` -ge 1 ]"); |
||||
my $carolIp6 = (split /[ \/]+/, $carol->succeed("ip -o -6 addr show dev ygg0 scope global"))[3]; |
||||
|
||||
# If Alice can talk to Carol, then Bob's outbound peering and Carol's |
||||
# local peering have succeeded and everybody is connected. |
||||
$alice->waitUntilSucceeds("ping -c 1 $carolIp6"); |
||||
$alice->succeed("ping -c 1 ${bobIp6}"); |
||||
|
||||
$bob->succeed("ping -c 1 ${aliceIp6}"); |
||||
$bob->succeed("ping -c 1 $carolIp6"); |
||||
|
||||
$carol->succeed("ping -c 1 ${aliceIp6}"); |
||||
$carol->succeed("ping -c 1 ${bobIp6}"); |
||||
|
||||
$carol->fail("journalctl -u dhcpcd | grep ygg0"); |
||||
|
||||
$alice->waitForUnit("httpd.service"); |
||||
$carol->succeed("curl --fail -g http://[${aliceIp6}]"); |
||||
|
||||
''; |
||||
}) |
Loading…
Reference in new issue