zerotier: Migrate from clanModule to clanServices

This migrates zerotier from clanModules to clanServices.
This is a graceful migration and does not yet disables any of the
clanModule functionality.
This commit is contained in:
a-kenji
2025-06-03 09:48:06 +02:00
parent 46e3180414
commit 24cf1bcc50
6 changed files with 278 additions and 0 deletions

View File

@@ -5,5 +5,6 @@
./hello-world/flake-module.nix
./wifi/flake-module.nix
./borgbackup/flake-module.nix
./zerotier/flake-module.nix
];
}

View File

@@ -0,0 +1,44 @@
---
description = "Configures [Zerotier VPN](https://zerotier.com) secure and efficient networking within a Clan."
features = [ "inventory" ]
categories = [ "Network", "System" ]
[constraints]
roles.controller.min = 1
roles.controller.max = 1
roles.moon.max = 7
---
## Overview
This guide explains how to set up and manage a [ZeroTier VPN](https://zerotier.com) for a clan network. Each VPN requires a single controller and can support multiple peers and optional moons for better connectivity.
## Roles
### 1. Controller
The [Controller](https://docs.zerotier.com/controller/) manages network membership and is responsible for admitting new peers.
When a new node is added to the clan, the controller must be updated to ensure it has the latest member list.
- **Key Points:**
- Must be online to admit new machines to the VPN.
- Existing nodes can continue to communicate even when the controller is offline.
### 2. Moons
[Moons](https://docs.zerotier.com/roots) act as relay nodes,
providing direct connectivity to peers via their public IP addresses.
They enable devices that are not publicly reachable to join the VPN by routing through these nodes.
- **Configuration Notes:**
- Each moon must define its public IP address.
- Ensures connectivity for devices behind NAT or restrictive firewalls.
### 3. Peers
Peers are standard nodes in the VPN.
They connect to other peers, moons, and the controller as needed.
- **Purpose:**
- General role for all machines that are neither controllers nor moons.
- Ideal for most clan members' devices.

View File

@@ -0,0 +1,151 @@
{ ... }:
{
_class = "clan.service";
manifest.name = "clan-core/zerotier";
manifest.description = "Configuration of the secure and efficient Zerotier VPN";
manifest.categories = [ "Utility" ];
manifest.readme = builtins.readFile ./README.md;
roles.peer = {
perInstance =
{ instanceName, roles, ... }:
{
nixosModule =
{
config,
lib,
pkgs,
...
}:
{
imports = [
(import ./shared.nix {
inherit
instanceName
roles
config
lib
pkgs
;
})
];
};
};
};
roles.moon = {
interface =
{ lib, ... }:
{
options.zerotier.moon.stableEndpoints = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = ''
Make this machine a moon.
Other machines can join this moon by adding this moon in their config.
It will be reachable under the given stable endpoints.
'';
example = ''
[ 1.2.3.4" "10.0.0.3/9993" "2001:abcd:abcd::3/9993" ]
'';
};
};
perInstance =
{ instanceName, roles, ... }:
{
nixosModule =
{
config,
lib,
pkgs,
settings,
...
}:
{
config.clan.core.networking.zerotier.moon.stableEndpoints = settings.zerotier.moon.stableEndpoints;
imports = [
(import ./shared.nix {
inherit
instanceName
roles
config
lib
pkgs
;
})
];
};
};
};
roles.controller = {
perInstance =
{
instanceName,
roles,
...
}:
{
nixosModule =
{
config,
lib,
pkgs,
...
}:
let
uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list);
in
{
imports = [
(import ./shared.nix {
inherit
instanceName
roles
config
lib
pkgs
;
})
];
config = {
systemd.services.zerotier-inventory-autoaccept =
let
machines = uniqueStrings (
(lib.attrNames roles.moon.machines)
++ (lib.attrNames roles.controller.machines)
++ (lib.attrNames roles.peer.machines)
);
networkIps = builtins.foldl' (
ips: name:
if
builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value"
then
ips
++ [
(builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value")
]
else
ips
) [ ] machines;
allHostIPs = networkIps;
in
{
wantedBy = [ "multi-user.target" ];
after = [ "zerotierone.service" ];
path = [ config.clan.core.clanPkgs.zerotierone ];
serviceConfig.ExecStart = pkgs.writeShellScript "zerotier-inventory-autoaccept" ''
${lib.concatMapStringsSep "\n" (host: ''
${config.clan.core.clanPkgs.zerotier-members}/bin/zerotier-members allow --member-ip ${host}
'') allHostIPs}
'';
};
clan.core.networking.zerotier.controller.enable = lib.mkDefault true;
};
};
};
};
}

View File

@@ -0,0 +1,9 @@
{
lib,
...
}:
{
clan.modules = {
zerotier = lib.modules.importApply ./default.nix { };
};
}

View File

@@ -0,0 +1,72 @@
{
lib,
config,
pkgs,
roles,
instanceName,
...
}:
let
controllerMachine = builtins.head (lib.attrNames roles.controller.machines or { });
networkIdPath = "${config.clan.core.settings.directory}/vars/per-machine/${controllerMachine}/zerotier/zerotier-network-id/value";
networkId =
if builtins.pathExists networkIdPath then
builtins.readFile networkIdPath
else
builtins.throw ''
No zerotier network id found for ${controllerMachine}.
Please run `clan vars generate ${controllerMachine}` first.
'';
moons = lib.attrNames (roles.moon.machines or { });
moonIps = builtins.foldl' (
ips: name:
if
builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value"
then
ips
++ [
(builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value")
]
else
ips
) [ ] moons;
in
{
config = {
clan.core.networking.zerotier.networkId = networkId;
clan.core.networking.zerotier.name = instanceName;
systemd.services.zerotierone.serviceConfig.ExecStartPost = lib.mkIf (moonIps != [ ]) (
lib.mkAfter [
"+${pkgs.writeScript "orbit-moons-by-ip" ''
#!${pkgs.python3.interpreter}
import json
import ipaddress
import subprocess
def compute_member_id(ipv6_addr: str) -> str:
addr = ipaddress.IPv6Address(ipv6_addr)
addr_bytes = bytearray(addr.packed)
# Extract the bytes corresponding to the member_id (node_id)
node_id_bytes = addr_bytes[10:16]
node_id = int.from_bytes(node_id_bytes, byteorder="big")
member_id = format(node_id, "x").zfill(10)[-10:]
return member_id
def main() -> None:
ips = json.loads(${builtins.toJSON (builtins.toJSON moonIps)})
for ip in ips:
member_id = compute_member_id(ip)
res = subprocess.run(["zerotier-cli", "orbit", member_id, member_id])
if res.returncode != 0:
print(f"Failed to add {member_id} to orbit")
if __name__ == "__main__":
main()
''}"
]
);
};
}

View File

@@ -86,6 +86,7 @@ nav:
- reference/clanServices/borgbackup.md
- reference/clanServices/hello-world.md
- reference/clanServices/wifi.md
- reference/clanServices/zerotier.md
- Clan Modules:
- Overview: reference/clanModules/index.md
- reference/clanModules/frontmatter/index.md