dotfiles/flake-modules/clan.nix
DannyDannyDanny e2cf93e7d6 feat(foreign-port): add WiFi-only laptop as clan machine
Mirrors the distant-shore pattern: clan-managed (no standalone
flake-module), wired into zerotier/data-mesher/dm-pull-deploy with the
generated vars. WiFi via NetworkManager (PSK from /etc/secrets/nm.env);
locally-signed boot chain (shim chain-loads sbsign-signed systemd-boot
+ kernel, refreshed every nixos-rebuild). targetHost is the LAN IP for
the first push, switch to ZT IPv6 once on the mesh. buildHost =
sunken-ship to avoid self-SSH on the closure copy.
2026-06-07 21:44:14 +02:00

254 lines
10 KiB
Nix

# clan.lol wiring for the homelab.
#
# Declares `sunken-ship` and `phantom-ship` as clan machines. Each machine's
# `imports` list is the NixOS module set that used to live in its own
# flake-module. clan-core produces `flake.nixosConfigurations.<name>` from
# these, which is why the old per-host flake-modules were removed.
#
# The mac stays outside the clan — admin only, uses `clan machines update`
# to push to the servers.
{ config, inputs, ... }:
let
lib = inputs.nixpkgs.lib;
hmModule = { user, homeDirectory, stateVersion ? null, userImports ? [ ] }:
import ../lib/home-manager-user.nix {
inherit lib user homeDirectory stateVersion userImports;
};
# ZT IPv6 addresses of the two clan machines. Clan publishes these as
# generated vars at vars/per-machine/<host>/zerotier/zerotier-ip/value;
# duplicated here so we can drop them into /etc/hosts at module-eval time.
sunkenShipZTv6 = "fdd5:53a2:de33:d269:6499:93d5:53a2:de33";
phantomShipZTv6 = "fdd5:53a2:de33:d269:6499:936c:48a:bbdc";
vpsRelayZTv6 = "fdd5:53a2:de33:d269:6499:9305:339f:2ed3";
distantShoreZTv6 = "fdd5:53a2:de33:d269:6499:93b6:ef1a:c3b3";
foreignPortZTv6 = "fdd5:53a2:de33:d269:6499:9389:9b18:6c52";
# Shared across both servers: /etc/hosts entries so data-mesher's
# libp2p /dns/<machine>.clan/... bootstrap multiaddrs resolve over ZT.
clanHostsModule = {
networking.hosts = {
"${sunkenShipZTv6}" = [ "sunken-ship.clan" ];
"${phantomShipZTv6}" = [ "phantom-ship.clan" ];
"${vpsRelayZTv6}" = [ "vps-relay.clan" ];
"${distantShoreZTv6}" = [ "distant-shore.clan" ];
"${foreignPortZTv6}" = [ "foreign-port.clan" ];
};
};
in {
imports = [ inputs.clan-core.flakeModules.default ];
clan = {
meta.name = "homelab";
# data-mesher uses `<machine>.${domain}` as a libp2p /dns/ multiaddr.
# We don't run a DNS server for "clan" — per-machine networking.hosts
# entries (via clanHostsModule) resolve it to the host's ZT IPv6.
meta.domain = "clan";
# Inventory machines — required for `inventory.instances` role bindings
# to resolve. Host-specific NixOS config lives under `machines.<name>`
# below.
inventory.machines.sunken-ship = { };
inventory.machines.phantom-ship = { };
inventory.machines.vps-relay = { };
inventory.machines.distant-shore = { };
inventory.machines.foreign-port = { };
# ZeroTier mesh VPN. sunken-ship is the controller (manages network
# membership); phantom-ship is a peer. The mac joins manually as an
# external ZT client and is authorized on the controller by node ID.
inventory.instances.zerotier = {
module.name = "zerotier";
module.input = "clan-core";
roles.controller.machines.sunken-ship = { };
roles.peer.machines.phantom-ship = { };
roles.peer.machines.sunken-ship = { };
roles.peer.machines.vps-relay = { };
roles.peer.machines.distant-shore = { };
roles.peer.machines.foreign-port = { };
};
# data-mesher — signed-file gossip protocol over libp2p (port 7946).
# Underpins dm-pull-deploy below. Files are registered + their allowed
# signers managed automatically via clan service exports.
# sunken-ship is the bootstrap node; phantom-ship joins via its
# /dns/sunken-ship.clan/... multiaddr (resolved via /etc/hosts).
inventory.instances.data-mesher = {
module.name = "data-mesher";
module.input = "clan-core";
roles.default.machines.sunken-ship = { };
roles.default.machines.phantom-ship = { };
roles.default.machines.distant-shore = { };
roles.default.machines.foreign-port = { };
roles.bootstrap.machines.sunken-ship = { };
};
# dm-pull-deploy — pull-based NixOS deploy via data-mesher gossip.
# Our clan-community input is pinned to the branch that sanitizes
# machine.name for the status file name (upstream PR pending).
# sunken-ship is the push node; both servers run the default watcher
# with action="switch".
inventory.instances.dm-pull-deploy = {
module.name = "dm-pull-deploy";
module.input = "clan-community";
roles.push.machines.sunken-ship.settings = {
gitUrl = "https://github.com/DannyDannyDanny/dotfiles.git";
branch = "main";
};
roles.default.machines.sunken-ship.settings.action = "switch";
roles.default.machines.phantom-ship.settings.action = "switch";
roles.default.machines.distant-shore.settings.action = "switch";
roles.default.machines.foreign-port.settings.action = "switch";
};
# `clan machines update` connection target. Priority 2000 > ZT's 900
# and overrides the ZT service's root@ default. Using the ZT IPv6 as
# the host makes updates work regardless of LAN DNS / mDNS state.
inventory.instances.internet = {
module.name = "internet";
module.input = "clan-core";
roles.default.machines.sunken-ship.settings = {
host = "fdd5:53a2:de33:d269:6499:93d5:53a2:de33";
user = "danny";
};
roles.default.machines.phantom-ship.settings = {
host = "fdd5:53a2:de33:d269:6499:936c:48a:bbdc";
user = "danny";
};
# Using public IPv4 while ZT identity is being bootstrapped on the
# VPS. Swap to ZT IPv6 (fdd5:53a2:de33:d269:6499:9305:339f:2ed3)
# after the first clan update uploads SOPS keys and zerotierone
# restarts with the clan-managed identity.
roles.default.machines.vps-relay.settings = {
host = "89.167.39.251";
user = "danny";
};
# distant-shore: LAN IP for the first update (not yet on ZT). Swap to
# its generated ZT IPv6 after it joins the mesh, like the others.
roles.default.machines.distant-shore.settings = {
host = "192.168.1.182";
user = "danny";
};
# foreign-port: WiFi-only LAN IP for the first update; swap to its
# generated ZT IPv6 after it joins the mesh.
roles.default.machines.foreign-port.settings = {
host = "192.168.1.223";
user = "danny";
};
};
# Preserve current network / init stack (no systemd-networkd/resolved,
# no boot.initrd.systemd, no extra debug packages). Revisit per-service
# in later stages rather than flipping this fleet-wide.
machines.sunken-ship = {
imports = [
{
clan.core.enableRecommendedDefaults = false;
clan.core.networking.targetHost = "danny@[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]";
clan.core.networking.buildHost = "danny@[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]";
}
clanHostsModule
../nixos/hosts/sunken-ship.nix
config.flake.nixosModules.server-debug-tools
config.flake.nixosModules.monitoring-node-exporter
config.flake.nixosModules.monitoring-prometheus-server
inputs.home-manager.nixosModules.home-manager
(hmModule {
user = "danny";
homeDirectory = "/home/danny";
stateVersion = "25.11";
})
];
};
machines.vps-relay = {
imports = [
{
clan.core.enableRecommendedDefaults = false;
# Initial install uses --target-host override; subsequent
# updates go over ZT IPv6 (set once generated, via the
# internet instance above).
}
clanHostsModule
../nixos/hosts/vps-relay.nix
config.flake.nixosModules.monitoring-node-exporter
inputs.home-manager.nixosModules.home-manager
(hmModule {
user = "danny";
homeDirectory = "/home/danny";
stateVersion = "25.11";
})
];
};
# distant-shore — ThinkPad X13 Gen 2, WiFi, Secure Boot via shim+MOK
# (installed standalone, then migrated into clan). targetHost is the LAN
# IP for the first `clan machines update`; switch to its ZT IPv6 once the
# mesh is up. buildHost = sunken-ship: it's an x86_64 builder whose key is
# already in distant-shore's authorized_keys, so the closure copy works
# (building on distant-shore itself needs a fragile self-SSH).
machines.distant-shore = {
imports = [
{
clan.core.enableRecommendedDefaults = false;
clan.core.networking.targetHost = "danny@192.168.1.182";
clan.core.networking.buildHost = "danny@sunken-ship";
}
clanHostsModule
../nixos/hosts/distant-shore.nix
config.flake.nixosModules.monitoring-node-exporter
inputs.home-manager.nixosModules.home-manager
(hmModule {
user = "danny";
homeDirectory = "/home/danny";
stateVersion = "25.11";
})
];
};
# foreign-port — WiFi-only laptop server, locally-signed boot chain
# (installed standalone, migrated into clan). targetHost is the LAN IP
# for the first `clan machines update`; switch to its ZT IPv6 once the
# mesh is up. buildHost = sunken-ship for the closure copy (avoids
# self-SSH).
machines.foreign-port = {
imports = [
{
clan.core.enableRecommendedDefaults = false;
clan.core.networking.targetHost = "danny@192.168.1.223";
clan.core.networking.buildHost = "danny@sunken-ship";
}
clanHostsModule
../nixos/hosts/foreign-port.nix
config.flake.nixosModules.monitoring-node-exporter
inputs.home-manager.nixosModules.home-manager
(hmModule {
user = "danny";
homeDirectory = "/home/danny";
stateVersion = "25.11";
})
];
};
machines.phantom-ship = {
imports = [
{
clan.core.enableRecommendedDefaults = false;
clan.core.networking.targetHost = "danny@[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]";
clan.core.networking.buildHost = "danny@[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]";
}
clanHostsModule
inputs.nix-openclaw.nixosModules.openclaw-gateway
../nixos/hosts/phantom-ship.nix
config.flake.nixosModules.server-debug-tools
config.flake.nixosModules.monitoring-node-exporter
inputs.home-manager.nixosModules.home-manager
(hmModule {
user = "danny";
homeDirectory = "/home/danny";
stateVersion = "25.11";
})
];
};
};
}