move zerotier secret generation into nixos module
This commit is contained in:
@@ -45,8 +45,8 @@ in
|
||||
{
|
||||
options.clan.networking.zerotier = {
|
||||
networkId = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
type = lib.types.str;
|
||||
default = config.clanCore.secrets.zerotier.facts."zerotier-network-id".value;
|
||||
description = ''
|
||||
zerotier networking id
|
||||
'';
|
||||
@@ -63,6 +63,11 @@ in
|
||||
};
|
||||
};
|
||||
config = lib.mkMerge [
|
||||
({
|
||||
# Override license so that we can build zerotierone without
|
||||
# having to re-import nixpkgs.
|
||||
services.zerotierone.package = lib.mkDefault (pkgs.zerotierone.overrideAttrs (_old: { meta = { }; }));
|
||||
})
|
||||
(lib.mkIf (cfg.networkId != null) {
|
||||
systemd.network.networks.zerotier = {
|
||||
matchConfig.Name = "zt*";
|
||||
@@ -85,24 +90,20 @@ in
|
||||
# only the controller needs to have the key in the repo, the other clients can be dynamic
|
||||
# we generate the zerotier code manually for the controller, since it's part of the bootstrap command
|
||||
clanCore.secrets.zerotier = {
|
||||
facts."zerotier.network.id" = { };
|
||||
secrets."zerotier.identity.secret" = { };
|
||||
facts."zerotier-network-id" = { };
|
||||
secrets."zerotier-identity-secret" = { };
|
||||
generator = ''
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
${config.clanCore.clanPkgs.clan-cli}/bin/clan zerotier --outpath "$TMPDIR"
|
||||
cp "$TMPDIR"/network.id "$facts"/zerotier.network.id
|
||||
cp "$TMPDIR"/identity.secret "$secrets"/zerotier.identity.secret
|
||||
export PATH=${lib.makeBinPath [ config.services.zerotierone.package pkgs.fakeroot ]}
|
||||
${pkgs.python3.interpreter} ${./generate-network.py} "$facts/zerotier-network-id" "$secrets/zerotier-identity-secret"
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.zerotierone.serviceConfig.ExecStartPre = [
|
||||
"+${pkgs.writeShellScript "init_zerotier" ''
|
||||
cp /etc/secrets/zerotier.identity.secret /var/lib/zerotier-one/identity.secret
|
||||
ln -sfT ${pkgs.writeText "net.json" (builtins.toJSON networkConfig)} /var/lib/zerotier-one/controller.d/network/${cfg.networkId}.json
|
||||
''}"
|
||||
"+${pkgs.writeShellScript "init-zerotier" ''
|
||||
cp ${config.clanCore.secrets.zerotier.secrets.zerotier-identity-secret.path} /var/lib/zerotier-one/identity.secret
|
||||
ln -sfT ${pkgs.writeText "net.json" (builtins.toJSON networkConfig)} /var/lib/zerotier-one/controller.d/network/${cfg.networkId}.json
|
||||
''}"
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
142
nixosModules/clanCore/zerotier/generate-network.py
Normal file
142
nixosModules/clanCore/zerotier/generate-network.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import argparse
|
||||
import json
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any, Iterator, Optional
|
||||
|
||||
|
||||
class ClanError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def try_bind_port(port: int) -> bool:
|
||||
tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
with tcp, udp:
|
||||
try:
|
||||
tcp.bind(("127.0.0.1", port))
|
||||
udp.bind(("127.0.0.1", port))
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def try_connect_port(port: int) -> bool:
|
||||
sock = socket.socket(socket.AF_INET)
|
||||
result = sock.connect_ex(("127.0.0.1", port))
|
||||
sock.close()
|
||||
return result == 0
|
||||
|
||||
|
||||
def find_free_port(port_range: range) -> Optional[int]:
|
||||
for port in port_range:
|
||||
if try_bind_port(port):
|
||||
return port
|
||||
return None
|
||||
|
||||
|
||||
class ZerotierController:
|
||||
def __init__(self, port: int, home: Path) -> None:
|
||||
self.port = port
|
||||
self.home = home
|
||||
self.authtoken = (home / "authtoken.secret").read_text()
|
||||
self.secret = (home / "identity.secret").read_text()
|
||||
|
||||
def _http_request(
|
||||
self,
|
||||
path: str,
|
||||
method: str = "GET",
|
||||
headers: dict[str, str] = {},
|
||||
data: Optional[dict[str, Any]] = None,
|
||||
) -> dict[str, Any]:
|
||||
body = None
|
||||
headers = headers.copy()
|
||||
if data is not None:
|
||||
body = json.dumps(data).encode("ascii")
|
||||
headers["Content-Type"] = "application/json"
|
||||
headers["X-ZT1-AUTH"] = self.authtoken
|
||||
url = f"http://127.0.0.1:{self.port}{path}"
|
||||
req = urllib.request.Request(url, headers=headers, method=method, data=body)
|
||||
resp = urllib.request.urlopen(req)
|
||||
return json.load(resp)
|
||||
|
||||
def status(self) -> dict[str, Any]:
|
||||
return self._http_request("/status")
|
||||
|
||||
def create_network(self, data: dict[str, Any] = {}) -> dict[str, Any]:
|
||||
identity = (self.home / "identity.public").read_text()
|
||||
node_id = identity.split(":")[0]
|
||||
return self._http_request(
|
||||
f"/controller/network/{node_id}______", method="POST", data=data
|
||||
)
|
||||
|
||||
def get_network(self, id: str) -> dict[str, Any]:
|
||||
return self._http_request(f"/controller/network/{id}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def zerotier_controller() -> Iterator[ZerotierController]:
|
||||
# This check could be racy but it's unlikely in practice
|
||||
controller_port = find_free_port(range(10000, 65535))
|
||||
if controller_port is None:
|
||||
raise ClanError("cannot find a free port for zerotier controller")
|
||||
|
||||
with TemporaryDirectory() as d:
|
||||
tempdir = Path(d)
|
||||
home = tempdir / "zerotier-one"
|
||||
home.mkdir()
|
||||
cmd = [
|
||||
"fakeroot",
|
||||
"--",
|
||||
"zerotier-one",
|
||||
f"-p{controller_port}",
|
||||
str(home),
|
||||
]
|
||||
with subprocess.Popen(cmd) as p:
|
||||
try:
|
||||
print(
|
||||
f"wait for controller to be started on 127.0.0.1:{controller_port}...",
|
||||
)
|
||||
while not try_connect_port(controller_port):
|
||||
status = p.poll()
|
||||
if status is not None:
|
||||
raise ClanError(
|
||||
f"zerotier-one has been terminated unexpected with {status}"
|
||||
)
|
||||
time.sleep(0.1)
|
||||
print()
|
||||
|
||||
yield ZerotierController(controller_port, home)
|
||||
finally:
|
||||
p.kill()
|
||||
p.wait()
|
||||
|
||||
|
||||
# TODO: allow merging more network configuration here
|
||||
def create_network() -> dict:
|
||||
with zerotier_controller() as controller:
|
||||
network = controller.create_network()
|
||||
return {
|
||||
"secret": controller.secret,
|
||||
"networkid": network["nwid"],
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("network_id")
|
||||
parser.add_argument("identity_secret")
|
||||
args = parser.parse_args()
|
||||
|
||||
zerotier = create_network()
|
||||
Path(args.network_id).write_text(zerotier["networkid"])
|
||||
Path(args.identity_secret).write_text(zerotier["secret"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
35
nixosModules/clanCore/zerotier/pyproject.toml
Normal file
35
nixosModules/clanCore/zerotier/pyproject.toml
Normal file
@@ -0,0 +1,35 @@
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
warn_redundant_casts = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
exclude = "clan_cli.nixpkgs"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
|
||||
select = [ "E", "F", "I", "U", "N"]
|
||||
ignore = [ "E501" ]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = [ "py310" ]
|
||||
include = "\\.pyi?$"
|
||||
exclude = '''
|
||||
/(
|
||||
\.git
|
||||
| \.hg
|
||||
| \.mypy_cache
|
||||
| \.tox
|
||||
| \.venv
|
||||
| _build
|
||||
| buck-out
|
||||
| build
|
||||
| dist
|
||||
# The following are specific to Black, you probably don't want those.
|
||||
| blib2to3
|
||||
| tests/data
|
||||
| profiling
|
||||
)/
|
||||
'''
|
||||
Reference in New Issue
Block a user