diff --git a/nixosModules/clanCore/zerotier/default.nix b/nixosModules/clanCore/zerotier/default.nix index 113fed67..fad719b7 100644 --- a/nixosModules/clanCore/zerotier/default.nix +++ b/nixosModules/clanCore/zerotier/default.nix @@ -99,6 +99,8 @@ in ${pkgs.python3.interpreter} ${./generate-network.py} "$facts/zerotier-network-id" "$secrets/zerotier-identity-secret" ''; }; + environment.etc."zerotier/network-id".text = facts.zerotier-network-id.value; + environment.systemPackages = [ config.clanCore.clanPkgs.zerotier-members ]; }) (lib.mkIf ((config.clanCore.secrets ? zerotier) && (facts.zerotier-network-id.value != null)) { clan.networking.zerotier.networkId = facts.zerotier-network-id.value; @@ -109,6 +111,11 @@ in ln -sfT ${pkgs.writeText "net.json" (builtins.toJSON networkConfig)} /var/lib/zerotier-one/controller.d/network/${cfg.networkId}.json ''}" ]; + systemd.services.zerotierone.serviceConfig.ExecStartPost = [ + "+${pkgs.writeShellScript "whitelist-controller" '' + ${config.clanCore.clanPkgs.zerotier-members}/bin/zerotier-members allow ${builtins.substring 0 10 cfg.networkId} + ''}" + ]; }) ]; } diff --git a/pkgs/flake-module.nix b/pkgs/flake-module.nix index ede366fb..52ff2084 100644 --- a/pkgs/flake-module.nix +++ b/pkgs/flake-module.nix @@ -8,6 +8,7 @@ perSystem = { pkgs, config, ... }: { packages = { tea-create-pr = pkgs.callPackage ./tea-create-pr { }; + zerotier-members = pkgs.callPackage ./zerotier-members { }; merge-after-ci = pkgs.callPackage ./merge-after-ci { inherit (config.packages) tea-create-pr; }; diff --git a/pkgs/zerotier-members/default.nix b/pkgs/zerotier-members/default.nix new file mode 100644 index 00000000..450b0793 --- /dev/null +++ b/pkgs/zerotier-members/default.nix @@ -0,0 +1,14 @@ +{ stdenv, python3, lib }: + +stdenv.mkDerivation { + name = "zerotier-members"; + src = ./.; + buildInputs = [ python3 ]; + installPhase = '' + install -Dm755 ${./zerotier-members.py} $out/bin/zerotier-members + ''; + meta = with lib; { + description = "A tool to list/allow members of a ZeroTier network"; + license = licenses.mit; + }; +} diff --git a/pkgs/zerotier-members/zerotier-members.py b/pkgs/zerotier-members/zerotier-members.py new file mode 100755 index 00000000..3bce0c12 --- /dev/null +++ b/pkgs/zerotier-members/zerotier-members.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +import argparse +import http.client +import json +import sys +from pathlib import Path + +ZEROTIER_STATE_DIR = Path("/var/lib/zerotier-one") + + +class ClanError(Exception): + pass + + +# this is managed by the nixos module +def get_network_id() -> str: + p = Path("/etc/zerotier/network-id") + if not p.exists(): + raise ClanError( + f"{p} file not found. Have you enabled the zerotier controller on this host?" + ) + return p.read_text().strip() + + +def allow_member(args: argparse.Namespace) -> None: + member_id = args.member_id + network_id = get_network_id() + token = ZEROTIER_STATE_DIR.joinpath("authtoken.secret").read_text() + conn = http.client.HTTPConnection("localhost", 9993) + conn.request( + "POST", + f"/controller/network/{network_id}/member/{member_id}", + '{"authorized": true}', + {"X-ZT1-AUTH": token}, + ) + resp = conn.getresponse() + if resp.status != 200: + raise ClanError( + f"the zerotier daemon returned this error: {resp.status} {resp.reason}" + ) + print(resp.status, resp.reason) + + +def list_members(args: argparse.Namespace) -> None: + network_id = get_network_id() + networks = ZEROTIER_STATE_DIR / "controller.d" / "network" / network_id / "member" + if not networks.exists(): + return + for member in networks.iterdir(): + with member.open() as f: + data = json.load(f) + try: + member_id = data["id"] + except KeyError: + raise ClanError(f"error: {member} does not contain an id") + print(member_id) + + +def main() -> None: + parser = argparse.ArgumentParser() + subparser = parser.add_subparsers(dest="command") + parser_allow = subparser.add_parser("allow", help="Allow a member to join") + parser_allow.add_argument("member_id") + parser_allow.set_defaults(func=allow_member) + + parser_list = subparser.add_parser("list", help="List members") + parser_list.set_defaults(func=list_members) + + args = parser.parse_args() + try: + args.func(args) + except ClanError as e: + print(e) + sys.exit(1) + + +if __name__ == "__main__": + main()