Skip to content

Network kits

The library ships kits for networks: a VPN with a hub, a virtual network with moons, and DNS records. A kit is a module, so a fleet writes only assemblies. See Kits batteries for kits, assemblies and roles. This is the fleet’s own words, and its assemblies:

modules/network.nix
# Network kits shipped with yen: write only assemblies. `root` is where public keys and ids are read from.
{
yen,
lib,
inputs,
...
}:
let
inherit (yen) kit machine;
inherit (yen.lib) same;
root = ../vars/per-machine;
in
{
imports = [
(inputs.yen.lib.kits.wireguard { inherit root; }) # (1)
(inputs.yen.lib.kits.zerotier { inherit root; })
inputs.yen.lib.kits.dns
];
yen.machine = {
lb.addr = "10.0.1.1";
web.addr = "10.0.1.10";
db.addr = "10.0.1.20";
};
# wireguard: lb is the hub, web and db are peers; addresses come from the prefix, in member order
yen.assembly.vpn = {
# (2)
kit = kit.wireguard;
roles = {
controller = {
at = machine.lb;
settings.prefix = "10.8.0.0/24";
};
peer = {
at = m: same m machine.web || same m machine.db; # (3)
settings = {
prefix = "10.8.0.0/24";
port = 51821;
};
};
};
};
# zerotier: one controller, up to four moons, any machine as a peer
yen.assembly.lan = {
# (4)
kit = kit.zerotier;
roles = {
controller.at = machine.lb;
moon.at = machine.web;
peer.at = machine.db;
};
};
# a kit that gives endpoints; dns reads them
yen.kit.site = {
# (5)
roles.default = { };
exports = [ "endpoints" ];
};
yen.assembly.site = {
kit = kit.site;
roles.default.at = machine.web;
};
yen.aspects.site = {
# (6)
at = yen.lib.all [
yen.kinds.member
(s: same (s.kit or null) kit.site)
];
exports = {
endpoints = [ "www.example.org" ];
};
};
}
  • (1) imports a kit that reads public values. It takes { root; }, the directory of the vars. The kit is a value in yen, not a file in your repo, so it cannot find your vars by a relative path.
  • (2) is a wireguard assembly: lb is the hub, and web and db are peers. Addresses come from the prefix, in member order.
  • (3) is a role taken by a predicate, not by a name.
  • (4) is a zerotier assembly: one controller, one moon, and any other machine as a peer.
  • (5) is a kit of the fleet that gives endpoints. (6) is the aspect that gives them. The kit dns reads them.

Each wireguard member has an address in member order, and a peer sees only the controller:

result
{
db = {
environment = {
etc = {
"zerotier/moons" = {
text = "ztmoon1";
};
};
};
networking = {
wireguard = {
interfaces = {
vpn = {
ips = [
"10.8.0.1/32"
];
peers = [
{
allowedIPs = [
"10.8.0.0/24"
];
endpoint = "10.0.1.1:51821";
publicKey = "wgLB";
}
];
privateKeyFile = "/run/secrets/vars/from-exports/wg-vpn.key";
};
};
};
};
services = {
zerotierone = {
enable = true;
joinNetworks = [
"ztnet"
];
};
};
};
lb = {
environment = {
etc = {
"zerotier/moons" = {
text = "ztmoon1";
};
};
};
networking = {
wireguard = {
interfaces = {
vpn = {
ips = [
"10.8.0.2/32"
];
listenPort = 51820;
peers = [
{
allowedIPs = [
"10.8.0.1/32"
];
publicKey = "wgDB";
}
{
allowedIPs = [
"10.8.0.3/32"
];
publicKey = "wgWEB";
}
];
privateKeyFile = "/run/secrets/vars/from-exports/wg-vpn.key";
};
};
};
};
services = {
zerotierone = {
enable = true;
joinNetworks = [
"ztnet"
];
};
};
systemd = {
tmpfiles = {
rules = [
"L+ /var/lib/zerotier-one/identity.secret - - - - /run/secrets/vars/from-exports/zt-lan.identity"
];
};
};
};
web = {
environment = {
etc = {
"zerotier/moons" = {
text = "ztmoon1";
};
};
};
networking = {
wireguard = {
interfaces = {
vpn = {
ips = [
"10.8.0.3/32"
];
peers = [
{
allowedIPs = [
"10.8.0.0/24"
];
endpoint = "10.0.1.1:51821";
publicKey = "wgLB";
}
];
privateKeyFile = "/run/secrets/vars/from-exports/wg-vpn.key";
};
};
};
};
services = {
zerotierone = {
enable = true;
joinNetworks = [
"ztnet"
];
};
};
systemd = {
tmpfiles = {
rules = [
"L+ /var/lib/zerotier-one/identity.secret - - - - /run/secrets/vars/from-exports/zt-lan-web.identity"
];
};
};
};
}

dns reads the endpoints that site exports:

result
[
{
name = "www.example.org";
target = "web";
}
]
Kit Roles Interface Does
wireguard { root; } controller (exactly 1), peer prefix, port (51820) an address per member; keys by generators; the private key is a path, never in the store; a peer talks to the controller
zerotier { root; } controller (1), moon (0 to 4), peer none the controller’s network id and the moons’ ids are public generator files; peers join and list the moons
dns default none a record for every endpoints any kit exports

Battery addresses gives each member an address of a prefix, in member order. A repeated address, a prefix that is no IPv4 /16 to /24 and a prefix too small are error records.

A wireguard or zerotier network needs exactly one controller, zerotier allows four moons at most, and a prefix must hold its members.

variants/five-moons.nix
# zerotier allows four moons: five machines make five
{ yen, ... }:
{
yen.machine = {
m1.addr = "10.0.2.1";
m2.addr = "10.0.2.2";
m3.addr = "10.0.2.3";
m4.addr = "10.0.2.4";
m5.addr = "10.0.2.5";
};
yen.assembly.crowd = {
kit = yen.kit.zerotier;
roles = {
controller.at = yen.machine.lb;
moon.at = yen.kinds.machine;
};
};
}
result
[
"merge conflict at environment.etc.zerotier/moons.text"
"merge conflict at services.zerotierone.joinNetworks"
"merge conflict at systemd.tmpfiles.rules"
"merge conflict at environment.etc.zerotier/moons.text"
"merge conflict at services.zerotierone.joinNetworks"
"merge conflict at systemd.tmpfiles.rules"
"merge conflict at environment.etc.zerotier/moons.text"
"merge conflict at services.zerotierone.joinNetworks"
"merge conflict at systemd.tmpfiles.rules"
"zerotier: db has no zt-crowd-db.moon public file"
"zerotier: lb has no zt-crowd.nwid public file"
"zerotier: lb has no zt-crowd-lb.moon public file"
"zerotier: m1 has no zt-crowd-m1.moon public file"
"zerotier: m2 has no zt-crowd-m2.moon public file"
"zerotier: m3 has no zt-crowd-m3.moon public file"
"zerotier: m4 has no zt-crowd-m4.moon public file"
"zerotier: m5 has no zt-crowd-m5.moon public file"
"zerotier: web has no zt-crowd-web.moon public file"
"kits: assembly crowd role moon: 8 machines, at most 4"
]
result
[
"kits: assembly headless role controller: 0 machines, at least 1"
]
result
[
"merge conflict at environment.etc.zerotier/moons.text"
"merge conflict at services.zerotierone.joinNetworks"
"merge conflict at environment.etc.zerotier/moons.text"
"merge conflict at services.zerotierone.joinNetworks"
"merge conflict at systemd.tmpfiles.rules"
"merge conflict at environment.etc.zerotier/moons.text"
"merge conflict at services.zerotierone.joinNetworks"
"merge conflict at systemd.tmpfiles.rules"
"zerotier: db has no zt-twin.nwid public file"
"zerotier: lb has no zt-twin.nwid public file"
"zerotier: web has no zt-twin.nwid public file"
"kits: assembly twin role controller: 3 machines, at most 1"
]
result
[
"addresses: 10.8.0.0/30 is not an IPv4 prefix /16 to /24"
"wireguard: lb has no wg-tiny.pub public key"
]

The members of one wireguard assembly must name the same prefix, and every member needs its public file in vars. A missing file is an error record, never a text in the configuration:

variants/prefix-differs.nix
# the members of one assembly name different prefixes
{ yen, ... }:
{
yen.assembly.split = {
kit = yen.kit.wireguard;
roles = {
controller = {
at = yen.machine.lb;
settings.prefix = "10.9.0.0/24";
};
peer = {
at = yen.machine.web;
settings.prefix = "10.9.1.0/24";
};
};
};
}
result
[
"wireguard: assembly split members disagree on prefix"
"wireguard: lb has no wg-split.pub public key"
"wireguard: web has no wg-split.pub public key"
]
variants/missing-public.nix
# a public key file that is not in vars is an error record, never the text "absent"
{ yen, ... }:
{
yen.assembly.lost = {
kit = yen.kit.wireguard;
roles.controller = {
at = yen.machine.lb;
settings.prefix = "10.9.0.0/24";
};
};
}
result
[
"wireguard: lb has no wg-lost.pub public key"
]
ContributeCommunitySponsor