Merge pull request 'Add inventory module for zerotier' (#2108) from init/zerotier-inventory into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/2108
This commit is contained in:
@@ -36,5 +36,6 @@
|
|||||||
xfce = ./xfce;
|
xfce = ./xfce;
|
||||||
zerotier-static-peers = ./zerotier-static-peers;
|
zerotier-static-peers = ./zerotier-static-peers;
|
||||||
zt-tcp-relay = ./zt-tcp-relay;
|
zt-tcp-relay = ./zt-tcp-relay;
|
||||||
|
zerotier = ./zerotier;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
38
clanModules/zerotier/README.md
Normal file
38
clanModules/zerotier/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
description = "Configures [Zerotier VPN](https://zerotier.com) secure and efficient networking within a Clan.."
|
||||||
|
features = [ "inventory" ]
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
2
clanModules/zerotier/default.nix
Normal file
2
clanModules/zerotier/default.nix
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# TODO: only kept this file to not break documentation generation.
|
||||||
|
{ }
|
||||||
47
clanModules/zerotier/roles/controller.nix
Normal file
47
clanModules/zerotier/roles/controller.nix
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
instanceNames = builtins.attrNames config.clan.inventory.services.zerotier;
|
||||||
|
instanceName = builtins.head instanceNames;
|
||||||
|
zeroTierInstance = config.clan.inventory.services.zerotier.${instanceName};
|
||||||
|
roles = zeroTierInstance.roles;
|
||||||
|
stringSet = list: builtins.attrNames (builtins.groupBy lib.id list);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../shared.nix
|
||||||
|
];
|
||||||
|
config = {
|
||||||
|
systemd.services.zerotier-inventory-autoaccept =
|
||||||
|
let
|
||||||
|
machines = stringSet (roles.moon.machines ++ roles.controller.machines ++ roles.peer.machines);
|
||||||
|
networkIps = builtins.foldl' (
|
||||||
|
ips: name:
|
||||||
|
if builtins.pathExists "${config.clan.core.clanDir}/machines/${name}/facts/zerotier-ip" then
|
||||||
|
ips
|
||||||
|
++ [
|
||||||
|
(builtins.readFile "${config.clan.core.clanDir}/machines/${name}/facts/zerotier-ip")
|
||||||
|
]
|
||||||
|
else
|
||||||
|
ips
|
||||||
|
) [ ] machines;
|
||||||
|
allHostIPs = config.clan.zerotier.networkIps ++ 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
17
clanModules/zerotier/roles/moon.nix
Normal file
17
clanModules/zerotier/roles/moon.nix
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../shared.nix
|
||||||
|
];
|
||||||
|
options.clan.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.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
# TODO, we want to remove these options from clanCore
|
||||||
|
config.clan.core.networking.zerotier.moon.stableEndpoints =
|
||||||
|
config.clan.zerotier.moon.stableEndpoints;
|
||||||
|
}
|
||||||
5
clanModules/zerotier/roles/peer.nix
Normal file
5
clanModules/zerotier/roles/peer.nix
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../shared.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
95
clanModules/zerotier/shared.nix
Normal file
95
clanModules/zerotier/shared.nix
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
instanceNames = builtins.attrNames config.clan.inventory.services.zerotier;
|
||||||
|
instanceName = builtins.head instanceNames;
|
||||||
|
zeroTierInstance = config.clan.inventory.services.zerotier.${instanceName};
|
||||||
|
roles = zeroTierInstance.roles;
|
||||||
|
controllerMachine = builtins.head roles.controller.machines;
|
||||||
|
networkIdPath = "${config.clan.core.clanDir}/machines/${controllerMachine}/facts/zerotier-network-id";
|
||||||
|
networkId = if builtins.pathExists networkIdPath then builtins.readFile networkIdPath else null;
|
||||||
|
moons = roles.moon.machines;
|
||||||
|
moonIps = builtins.foldl' (
|
||||||
|
ips: name:
|
||||||
|
if builtins.pathExists "${config.clan.core.clanDir}/machines/${name}/facts/zerotier-ip" then
|
||||||
|
ips
|
||||||
|
++ [
|
||||||
|
(builtins.readFile "${config.clan.core.clanDir}/machines/${name}/facts/zerotier-ip")
|
||||||
|
]
|
||||||
|
else
|
||||||
|
ips
|
||||||
|
) [ ] moons;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.clan.zerotier = {
|
||||||
|
excludeHosts = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ config.clan.core.machineName ];
|
||||||
|
description = "Hosts that should be excluded";
|
||||||
|
};
|
||||||
|
networkIps = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = "Extra zerotier network Ips that should be accepted";
|
||||||
|
};
|
||||||
|
networkIds = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = "Extra zerotier network Ids that should be accepted";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = builtins.length instanceNames == 1;
|
||||||
|
message = "The zerotier module currently only supports one instance per machine, but found ${builtins.toString instanceNames}";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = builtins.length roles.controller.machines == 1;
|
||||||
|
message = "The zerotier module requires exactly one controller, but found ${builtins.toString roles.controller.machines}";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
clan.core.networking.zerotier.networkId = networkId;
|
||||||
|
clan.core.networking.zerotier.name = instanceName;
|
||||||
|
|
||||||
|
# TODO: in future we want to have the node id of our moons in our facts
|
||||||
|
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()
|
||||||
|
''}"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -66,22 +66,23 @@ nav:
|
|||||||
- Reference:
|
- Reference:
|
||||||
- Overview: reference/index.md
|
- Overview: reference/index.md
|
||||||
- Clan Modules:
|
- Clan Modules:
|
||||||
- reference/clanModules/index.md
|
|
||||||
- reference/clanModules/admin.md
|
- reference/clanModules/admin.md
|
||||||
|
|
||||||
- reference/clanModules/borgbackup-static.md
|
- reference/clanModules/borgbackup-static.md
|
||||||
- reference/clanModules/borgbackup.md
|
- reference/clanModules/borgbackup.md
|
||||||
- reference/clanModules/deltachat.md
|
- reference/clanModules/deltachat.md
|
||||||
- reference/clanModules/dyndns.md
|
|
||||||
- reference/clanModules/disk-id.md
|
- reference/clanModules/disk-id.md
|
||||||
|
- reference/clanModules/dyndns.md
|
||||||
- reference/clanModules/ergochat.md
|
- reference/clanModules/ergochat.md
|
||||||
- reference/clanModules/garage.md
|
- reference/clanModules/garage.md
|
||||||
- reference/clanModules/golem-provider.md
|
- reference/clanModules/golem-provider.md
|
||||||
- reference/clanModules/heisenbridge.md
|
- reference/clanModules/heisenbridge.md
|
||||||
|
- reference/clanModules/index.md
|
||||||
- reference/clanModules/iwd.md
|
- reference/clanModules/iwd.md
|
||||||
- reference/clanModules/localbackup.md
|
- reference/clanModules/localbackup.md
|
||||||
- reference/clanModules/localsend.md
|
- reference/clanModules/localsend.md
|
||||||
- reference/clanModules/matrix-synapse.md
|
|
||||||
- reference/clanModules/machine-id.md
|
- reference/clanModules/machine-id.md
|
||||||
|
- reference/clanModules/matrix-synapse.md
|
||||||
- reference/clanModules/moonlight.md
|
- reference/clanModules/moonlight.md
|
||||||
- reference/clanModules/mumble.md
|
- reference/clanModules/mumble.md
|
||||||
- reference/clanModules/nginx.md
|
- reference/clanModules/nginx.md
|
||||||
@@ -101,9 +102,11 @@ nav:
|
|||||||
- reference/clanModules/vaultwarden.md
|
- reference/clanModules/vaultwarden.md
|
||||||
- reference/clanModules/xfce.md
|
- reference/clanModules/xfce.md
|
||||||
- reference/clanModules/zerotier-static-peers.md
|
- reference/clanModules/zerotier-static-peers.md
|
||||||
|
- reference/clanModules/zerotier.md
|
||||||
- reference/clanModules/zt-tcp-relay.md
|
- reference/clanModules/zt-tcp-relay.md
|
||||||
- CLI:
|
- CLI:
|
||||||
- reference/cli/index.md
|
- reference/cli/index.md
|
||||||
|
|
||||||
- reference/cli/backups.md
|
- reference/cli/backups.md
|
||||||
- reference/cli/facts.md
|
- reference/cli/facts.md
|
||||||
- reference/cli/flakes.md
|
- reference/cli/flakes.md
|
||||||
@@ -117,6 +120,7 @@ nav:
|
|||||||
- reference/cli/vms.md
|
- reference/cli/vms.md
|
||||||
- Clan Core:
|
- Clan Core:
|
||||||
- reference/clan-core/index.md
|
- reference/clan-core/index.md
|
||||||
|
|
||||||
- reference/clan-core/backups.md
|
- reference/clan-core/backups.md
|
||||||
- reference/clan-core/facts.md
|
- reference/clan-core/facts.md
|
||||||
- reference/clan-core/sops.md
|
- reference/clan-core/sops.md
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ in
|
|||||||
rules = [
|
rules = [
|
||||||
{
|
{
|
||||||
not = false;
|
not = false;
|
||||||
or = false;
|
"or" = false;
|
||||||
type = "ACTION_ACCEPT";
|
type = "ACTION_ACCEPT";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ def get_network_id() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def allow_member(args: argparse.Namespace) -> None:
|
def allow_member(args: argparse.Namespace) -> None:
|
||||||
member_id = args.member_id
|
|
||||||
if args.member_ip:
|
if args.member_ip:
|
||||||
member_ip = args.member_id
|
member_id = compute_member_id(args.member_id)
|
||||||
member_id = compute_member_id(member_ip)
|
else:
|
||||||
|
member_id = args.member_id
|
||||||
network_id = get_network_id()
|
network_id = get_network_id()
|
||||||
token = ZEROTIER_STATE_DIR.joinpath("authtoken.secret").read_text()
|
token = ZEROTIER_STATE_DIR.joinpath("authtoken.secret").read_text()
|
||||||
conn = http.client.HTTPConnection("localhost", 9993)
|
conn = http.client.HTTPConnection("localhost", 9993)
|
||||||
@@ -90,6 +90,8 @@ def list_members(args: argparse.Namespace) -> None:
|
|||||||
networks = ZEROTIER_STATE_DIR / "controller.d" / "network" / network_id / "member"
|
networks = ZEROTIER_STATE_DIR / "controller.d" / "network" / network_id / "member"
|
||||||
if not networks.exists():
|
if not networks.exists():
|
||||||
return
|
return
|
||||||
|
if not args.no_headers:
|
||||||
|
print(f"{'Member ID':<10} {'Ipv6 Address':<39} {'Authorized'}")
|
||||||
for member in networks.iterdir():
|
for member in networks.iterdir():
|
||||||
with member.open() as f:
|
with member.open() as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
@@ -98,16 +100,14 @@ def list_members(args: argparse.Namespace) -> None:
|
|||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
msg = f"error: {member} does not contain an id"
|
msg = f"error: {member} does not contain an id"
|
||||||
raise ClanError(msg) from e
|
raise ClanError(msg) from e
|
||||||
print(
|
ip = str(compute_zerotier_ip(network_id, member_id))
|
||||||
member_id,
|
authorized = str(data.get("authorized", False))
|
||||||
compute_zerotier_ip(network_id, data["id"]),
|
print(f"{member_id:<10} {ip:<39} {authorized}")
|
||||||
data["authorized"] or "false",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser(description="Manage zerotier members")
|
||||||
subparser = parser.add_subparsers(dest="command")
|
subparser = parser.add_subparsers(dest="command", required=True)
|
||||||
parser_allow = subparser.add_parser("allow", help="Allow a member to join")
|
parser_allow = subparser.add_parser("allow", help="Allow a member to join")
|
||||||
parser_allow.add_argument(
|
parser_allow.add_argument(
|
||||||
"--member-ip",
|
"--member-ip",
|
||||||
@@ -118,6 +118,9 @@ def main() -> None:
|
|||||||
parser_allow.set_defaults(func=allow_member)
|
parser_allow.set_defaults(func=allow_member)
|
||||||
|
|
||||||
parser_list = subparser.add_parser("list", help="List members")
|
parser_list = subparser.add_parser("list", help="List members")
|
||||||
|
parser_list.add_argument(
|
||||||
|
"--no-headers", action="store_true", help="Do not print headers"
|
||||||
|
)
|
||||||
parser_list.set_defaults(func=list_members)
|
parser_list.set_defaults(func=list_members)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|||||||
Reference in New Issue
Block a user