Compare commits

..
Author SHA1 Message Date
kurogeek f58da21d15 clanService/phonebox-global: unit tests for the directory sync logic
Add a flake check `phonebox-global-sync` that exercises phonebox-sync.py:
yggdrasil address derivation (fixed vector plus cross-check against the
yggdrasil binary), record verification (foreign address, bad or forged
signature, malformed tags, number format), collision tie-breaking, and
the files written for the dialplan including stale entry removal.
2026-09-18 09:55:21 +00:00
kurogeek f398daacb5 clanService/phonebox-global: decentralized phone network over yggdrasil
New service alongside phonebox. Boxes get a random 6 digit number
(100000-999999) signed by the machine's yggdrasil key. A serf gossip
cluster on yggdrasil port 7946 exchanges the signed records;
phonebox-sync verifies key -> address derivation and the signature and
writes the number -> address map to /run/phonebox/numbers for the
asterisk dialplan. Any yggdrasil node running the service can reach any
other; clan-core's ygg-input filter is opened for SIP, RTP and gossip.

Includes a two-node VM test proving directory convergence and a call
over yggdrasil. The existing phonebox service is untouched.
2026-09-18 02:10:05 +00:00
kurogeek 0906700ad6 deneb: add default_thai_company frappe app to poyerp
Package git.b4l.co.th/newedge/default_thai_company as a frappix app
    (pkgs/frappix/default-thai-company.nix) and expose it through a new
    frappixAppsOverlay that extends the pkgs.frappix scope. Install it on
    the poyerp.newedge.house site on deneb.
2026-09-15 16:18:30 +07:00
kurogeek b9302e6192 clanService/prometheus: add mirach and almach 2026-09-11 10:53:00 +07:00
kurogeek 5cb262857e inventory/wifi: retry autoconnect and auth indefinitely
Set connection.autoconnect-retries=0 and connection.auth-retries=0 on the
NetworkManager profiles generated by the clan wifi module for buna, so a
flaky AP or slow auth never leaves the connection permanently blocked.
2026-09-09 04:15:18 +00:00
kurogeek 47e536540f flake: bump nixpkgs to fix broken prettier build
prettier 3.9.6 in nixpkgs e8be781 failed to build (stale pnpm lockfile in
binding-wasm32-wasi), breaking nix fmt. Fixed upstream in 58973d7.
2026-09-08 08:35:21 +00:00
kurogeek 282a8f8287 clanService/personal-computer: supportedFilesystems ntfs 2026-09-04 14:06:30 +07:00
kurogeek a9254c424d machines/sirius: buildPlatform.system = x86_64-linux 2026-09-04 13:43:04 +07:00
kurogeek 5961254878 machines/sirius: rm common module 2026-09-04 13:42:10 +07:00
kurogeek 9403ddbf08 clanService/headscale: adjust magic dns settings 2026-09-03 17:17:36 +07:00
kurogeek 37ff958560 inventory/auto-pull-update: limited to selected machines until a build machine is present 2026-09-03 13:58:29 +07:00
kurogeek efa4b39782 libreoffice-fresh -> libreoffice-stable 2026-09-03 12:22:15 +07:00
kurogeek e2341b7c79 inputs/nixpkgs,home-manager,clan-core,frappix, bump version 2026-09-03 12:21:00 +07:00
kurogeek dc1c94796a machines/sirius: reduce write on disk 2026-09-02 11:34:40 +07:00
kurogeek 5b07b98f40 inventory/prometheus: add buna to monitored machines 2026-08-21 12:03:34 +07:00
kurogeek 88ac5cbd83 machines/buna: use wifi 2026-08-19 14:34:34 +07:00
kurogeek 969c006f97 machines/deneb: support Thai characters 2026-08-16 14:02:41 +07:00
kurogeek 1c7d0bb388 hm/emmie: librewolf -> firefox 2026-08-05 09:07:15 +07:00
kurogeek 84c3bd92bb hm/emmie: enable librewolf 2026-08-03 13:51:12 +07:00
kurogeek 3340731c8d flake/packages/installer: init 2026-07-30 17:55:41 +07:00
kurogeek cd979fb771 inventory/borgbackup: init 2026-07-29 12:38:12 +07:00
kurogeek 7f6abc1d04 clanService/apple-network: init 2026-07-23 17:33:04 +07:00
kurogeek 0946de0e46 vars: update prometheus/matrix-alertmanager-token for machine cursa 2026-07-23 15:12:16 +07:00
kurogeek a29e4611b2 inventory/auto-pull-update: init 2026-07-23 14:12:34 +07:00
kurogeek 7246ab1437 vars: secret rotation 2026-07-21 18:15:45 +07:00
kurogeek f8193425af drop vi 2026-07-20 10:31:42 +07:00
kurogeek 3c54e8d6ef inventory/internet: add tangra machine 2026-07-17 15:41:59 +07:00
kurogeek 4a7d5340f3 inventory/internet: add ramus machine 2026-07-17 15:41:44 +07:00
kurogeek 458265f96b inventory/yggdrasil-global-network: allow kurogeek laptop as extra ip 2026-07-17 15:01:09 +07:00
kurogeek 702ef6ab86 inventory/yggdrasil-global-network: enable on all machines 2026-07-17 14:55:06 +07:00
kurogeek ea4e2f03a7 nix fmt 2026-07-16 16:34:46 +07:00
kurogeek e738692558 clanService/phonebox: allow multi-clients to exist on one node 2026-07-16 16:25:51 +07:00
kurogeek ef698f8ad3 inventory/prometheus: add sirius to monitor machines 2026-07-13 10:28:47 +07:00
kurogeek 824b099ad6 Merge pull request 'clanService/wordpress: allow mutable at wp-content/ai1wm-backups' (#2) from mooyai/infra:mooyai-patch-1 into main
Reviewed-on: #2
2026-07-08 16:41:27 +07:00
mooyai 651240f5a1 clanService/wordpress: symlink upgrade-temp-backup and ai1wm-backups to state dir
WordPress writes into wp-content/upgrade-temp-backup (core rollback on
theme/plugin updates) and wp-content/ai1wm-backups (All-in-One WP
Migration). Both sit directly under wp-content, which is the read-only
Nix store path, so creation fails.

Symlink both dirs out to the writable state dir and add matching
tmpfiles rules, same pattern as themes/plugins/upgrade.

Fixes "Could not create the upgrade-temp-backup directory" on Bricks
theme update, and the ai1wm-backups permission errors.
2026-07-08 16:18:17 +07:00
kurogeek 3b8c11b096 inputs/frappix: bump version 2026-07-02 11:19:35 +07:00
kurogeek 982c6c23ca machines/deneb: frappe add posprinter app 2026-07-01 17:05:40 +07:00
kurogeek e55bbaaa6b inputs/frappix: bump version 2026-07-01 17:05:11 +07:00
kurogeek 53c98dcba8 Merge pull request 'Update modules/clan/wordpress/default.nix' (#1) from mooyai/infra:main into main
Reviewed-on: #1
2026-06-23 16:59:11 +07:00
mooyai ce5f4ff43f Update modules/clan/wordpress/default.nix
make dir to support plugin update temp dir
2026-06-23 16:44:52 +07:00
kurogeek dbb3e55cad init cursa as a prometheus server, vega and rigel are monitored 2026-06-23 15:28:35 +07:00
kurogeek d09f67a757 clanService/prometheus: init monitoring system 2026-06-23 15:27:31 +07:00
kurogeek 77b487a709 clancore bumped, zerotier migration 2026-06-16 15:01:21 +07:00
kurogeek 2b239eb162 hm/emmie: use hunspell dictionary with systemd user session variables, libreoffice -> libreoffice-qt 2026-06-16 11:20:05 +07:00
kurogeek 8e64e88d8f inputs: bump home-manager 2026-06-16 11:18:34 +07:00
kurogeek 6276d9aee0 inputs: bump nixpkgs, frappix. migrate inventree 2026-06-15 16:36:18 +07:00
kurogeek 9471d1a4e6 machines/bosona: nix fmt 2026-06-15 15:55:04 +07:00
kurogeek 77d8e42ec2 hm/emmie: add hunspellDicts en and th 2026-06-15 13:28:58 +07:00
kurogeek 0dafb8cd52 clanService/wordpress: nginx clientMaxBodySize is 128m 2026-06-12 12:22:27 +07:00
kurogeek 8b12656149 clanService/wordpress: phpfpm options and wp-config.php are configurable from inventory interface 2026-06-12 12:09:56 +07:00
kurogeek d622040d30 hm/emmie: rm DnsOverHttpsMode and DnsOverHttpsTemplates 2026-06-11 13:54:17 +07:00
kurogeek 2bc05c2d6d clanService/wordpress: test vars 2026-06-10 18:59:30 +07:00
kurogeek 5fa8444112 clanService/wordpress: security.acme.acceptTerms = true and nginx is in acme group 2026-06-10 18:59:01 +07:00
kurogeek 8874b33a5d greaterchiangmai: nodejs_20 -> nodejs 2026-06-10 18:56:48 +07:00
kurogeek 521ccdc886 clanService/personal-computer: rm services.displayManager.gdm.wayland 2026-06-10 18:53:46 +07:00
kurogeek 07b648db9a clanService/headscale: magic_dns is disabled 2026-06-10 15:11:26 +07:00
kurogeek da6be4946f machines/tangra: poyfestival.com website 2026-06-01 10:42:03 +07:00
kurogeek bc16c72707 machines/canopus: localhost -> sitename 2026-05-25 14:20:44 +07:00
1012 changed files with 18406 additions and 7591 deletions
+1
View File
@@ -3,3 +3,4 @@
result result
result-* result-*
run-vm-* run-vm-*
.nixos-test-history
Generated
+193 -36
View File
@@ -53,6 +53,69 @@
"type": "github" "type": "github"
} }
}, },
"clan-community": {
"inputs": {
"clan-core": [
"clan-core"
],
"data-mesher": [
"clan-community",
"clan-core",
"data-mesher"
],
"disko": [
"clan-community",
"clan-core",
"disko"
],
"flake-parts": [
"flake-parts"
],
"nix-darwin": [
"clan-community",
"clan-core",
"nix-darwin"
],
"nix-github-actions": "nix-github-actions",
"nix-select": [
"clan-community",
"clan-core",
"nix-select"
],
"nix-unit": "nix-unit",
"nixpkgs": [
"clan-community",
"clan-core",
"nixpkgs"
],
"sops-nix": [
"clan-community",
"clan-core",
"sops-nix"
],
"systems": [
"clan-community",
"clan-core",
"systems"
],
"treefmt-nix": [
"treefmt-nix"
]
},
"locked": {
"lastModified": 1784417092,
"narHash": "sha256-no5eDYWPUZZu4GtLufnA7b7CiZC4qAhCgf70gFiDLZk=",
"ref": "refs/heads/main",
"rev": "20371843d45f61217019e489d6857842dc8a0203",
"revCount": 73,
"type": "git",
"url": "https://git.clan.lol/clan/clan-community"
},
"original": {
"type": "git",
"url": "https://git.clan.lol/clan/clan-community"
}
},
"clan-core": { "clan-core": {
"inputs": { "inputs": {
"data-mesher": "data-mesher", "data-mesher": "data-mesher",
@@ -72,11 +135,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1772411144, "lastModified": 1788346295,
"narHash": "sha256-WhXudztwPNnKXaqGX4DOqNfHzHdBSiGCvKGHM20pscw=", "narHash": "sha256-k9Ol++FFhQuPya6ZNQwVhqZZkMvE1ytLtbdrbH5zkrI=",
"ref": "refs/heads/main", "ref": "refs/heads/main",
"rev": "92cc85bc24eb31ce5725e1e72753129810ce3fe9", "rev": "b15797faeca7494a7fe5fde2691d93affa3d3e37",
"revCount": 13201, "revCount": 15239,
"type": "git", "type": "git",
"url": "https://git.clan.lol/clan/clan-core" "url": "https://git.clan.lol/clan/clan-core"
}, },
@@ -101,11 +164,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1772273147, "lastModified": 1788308203,
"narHash": "sha256-Wzhoc6ifjTDZi8aVRH3fuLJPdd4ouNTTwwVhgoMcMek=", "narHash": "sha256-dCOb9YIJv9I2udjqukvjlGiTyOvGnO7zVbot0PxjBGk=",
"rev": "d5de7a8d9e5726e678c94e62fe8ac3a809fee5da", "rev": "644c49bbf121b3e256bc83ce10b5d97813d9181d",
"type": "tarball", "type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/d5de7a8d9e5726e678c94e62fe8ac3a809fee5da.tar.gz" "url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/644c49bbf121b3e256bc83ce10b5d97813d9181d.tar.gz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
@@ -140,11 +203,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1771881364, "lastModified": 1781152676,
"narHash": "sha256-A5uE/hMium5of/QGC6JwF5TGoDAfpNtW00T0s9u/PN8=", "narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "a4cb7bf73f264d40560ba527f9280469f1f081c6", "rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -294,11 +357,11 @@
"std": "std" "std": "std"
}, },
"locked": { "locked": {
"lastModified": 1779175997, "lastModified": 1787631179,
"narHash": "sha256-Ps/4s3jwaZdLVEpO+1cRs54VbPbgMeXJUqa4CWSPJSY=", "narHash": "sha256-l+Zt5XNTD0f0ChkzDJURfQlFAgANIMrGYrk51SZEJKU=",
"owner": "kurogeek", "owner": "kurogeek",
"repo": "frappix", "repo": "frappix",
"rev": "0f1b4bcfb8c3b976e808a57e491d10857a1a45ac", "rev": "fcb797886ae753b26a6e4415a7f2c19ba6adcf3b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -353,11 +416,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1768068402, "lastModified": 1788355065,
"narHash": "sha256-bAXnnJZKJiF7Xr6eNW6+PhBf1lg2P1aFUO9+xgWkXfA=", "narHash": "sha256-gAz5oTI7ur34CfuakQduiQd/2ZhO62Bn2XXg08nZYYQ=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "8bc5473b6bc2b6e1529a9c4040411e1199c43b4c", "rev": "d9d750e4fc11c10cab2da677bdd31e427f3a3a71",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -484,11 +547,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1772379624, "lastModified": 1786845137,
"narHash": "sha256-NG9LLTWlz4YiaTAiRGChbrzbVxBfX+Auq4Ab/SWmk4A=", "narHash": "sha256-oQFip+v0luP8NIxJzmiW4Wu8bILsbFWom5l0zonl8hQ=",
"owner": "nix-darwin", "owner": "nix-darwin",
"repo": "nix-darwin", "repo": "nix-darwin",
"rev": "52d061516108769656a8bd9c6e811c677ec5b462", "rev": "4cff07de74b50e64bdd68cd4e722ab5b6b35ee48",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -497,6 +560,49 @@
"type": "github" "type": "github"
} }
}, },
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"clan-community",
"nixpkgs"
]
},
"locked": {
"lastModified": 1737420293,
"narHash": "sha256-F1G5ifvqTpJq7fdkT34e/Jy9VCyzd5XfJ9TO8fHhJWE=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "f4158fa080ef4503c8f4c820967d946c2af31ec9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nix-github-actions_2": {
"inputs": {
"nixpkgs": [
"clan-community",
"nix-unit",
"nixpkgs"
]
},
"locked": {
"lastModified": 1737420293,
"narHash": "sha256-F1G5ifvqTpJq7fdkT34e/Jy9VCyzd5XfJ9TO8fHhJWE=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "f4158fa080ef4503c8f4c820967d946c2af31ec9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nix-select": { "nix-select": {
"locked": { "locked": {
"lastModified": 1763303120, "lastModified": 1763303120,
@@ -510,6 +616,32 @@
"url": "https://git.clan.lol/clan/nix-select/archive/main.tar.gz" "url": "https://git.clan.lol/clan/nix-select/archive/main.tar.gz"
} }
}, },
"nix-unit": {
"inputs": {
"nix-github-actions": "nix-github-actions_2",
"nixpkgs": [
"clan-community",
"nixpkgs"
],
"treefmt-nix": [
"clan-community",
"treefmt-nix"
]
},
"locked": {
"lastModified": 1779338171,
"narHash": "sha256-affUbv/bwE8SLGhuWKniDr7SVO+Lo1XEPjCZdyU5kgQ=",
"owner": "nix-community",
"repo": "nix-unit",
"rev": "6ab1f232562a01d18b40d5ed6a58718c4f3a74bc",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-unit",
"type": "github"
}
},
"nixago": { "nixago": {
"inputs": { "inputs": {
"flake-utils": "flake-utils_3", "flake-utils": "flake-utils_3",
@@ -522,11 +654,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1714086354, "lastModified": 1746801636,
"narHash": "sha256-yKVQMxL9p7zCWUhnGhDzRVT8sDgHoI3V595lBK0C2YA=", "narHash": "sha256-dlcKfIXp/eqFHzFm+DzseXAWWlpVwyk9cTvCKGtVKkw=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixago", "repo": "nixago",
"rev": "5133633e9fe6b144c8e00e3b212cdbd5a173b63d", "rev": "8cc33f973ab3a891d8a41391e73ef451a783960b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -535,6 +667,29 @@
"type": "github" "type": "github"
} }
}, },
"nixos-images": {
"inputs": {
"nixos-stable": [
"nixpkgs"
],
"nixos-unstable": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1784802994,
"narHash": "sha256-4PcD0Ibzdkh85G+70w5dLlR9YgQ2bmNIjiPPMSzO57w=",
"owner": "nix-community",
"repo": "nixos-images",
"rev": "6ece16b0c97986fe085122e796044add4cc3ff64",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-images",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1712920918, "lastModified": 1712920918,
@@ -552,11 +707,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1778458615, "lastModified": 1788775869,
"narHash": "sha256-cY07EsdhBJ8tFXPzDYevgqxRev9ZLxFonuq9wmq5kwg=", "narHash": "sha256-JRf1lSypbvKj6AzUZHpUA6YC1DirVuitZWOnRwcm+Ww=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c6e5ca3c836a5f4dd9af9f2c1fc1c38f0fac988a", "rev": "58973d74f1893afe13f5902919803a0e99da0ca4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -597,16 +752,15 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1708640854, "lastModified": 1787579760,
"narHash": "sha256-EpcAmvIS4ErqhXtVEfd2GPpU/E/s8CCRSfYzk6FZ/fY=", "narHash": "sha256-WGhIEINOICx7bhV3WrSz0QTzv8uzaaa9AXvD1clIWpc=",
"owner": "paisano-nix", "owner": "paisano-nix",
"repo": "core", "repo": "core",
"rev": "adcf742bc9463c08764ca9e6955bd5e7dcf3a3fe", "rev": "88739c84d308876714322eb9844ede6597c1a580",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "paisano-nix", "owner": "paisano-nix",
"ref": "0.2.0",
"repo": "core", "repo": "core",
"type": "github" "type": "github"
} }
@@ -653,6 +807,7 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"clan-community": "clan-community",
"clan-core": "clan-core", "clan-core": "clan-core",
"devshell": "devshell", "devshell": "devshell",
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
@@ -660,6 +815,7 @@
"home-manager": "home-manager", "home-manager": "home-manager",
"import-tree": "import-tree", "import-tree": "import-tree",
"liminix": "liminix", "liminix": "liminix",
"nixos-images": "nixos-images",
"nixpkgs": "nixpkgs_2", "nixpkgs": "nixpkgs_2",
"plasma-manager": "plasma-manager", "plasma-manager": "plasma-manager",
"treefmt-nix": "treefmt-nix" "treefmt-nix": "treefmt-nix"
@@ -673,11 +829,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1772340640, "lastModified": 1788337237,
"narHash": "sha256-1nq7+Kt5IUBD8Hu3nptVPbMf+22rNJoHT0t9L1X+GKA=", "narHash": "sha256-gkSH8VUtCo6hnysNmb9DbTuDepH2t5pv+QWjP75xKAk=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "dec4d8eac700dcd2fe3c020857d3ee220ec147f1", "rev": "fbf759290e0cb0a98dfc813a4eb7d53ad1dacb57",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -764,15 +920,16 @@
}, },
"systems": { "systems": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1774449309,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
"owner": "nix-systems", "owner": "nix-systems",
"repo": "default", "repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "rev": "c29398b59d2048c4ab79345812849c9bd15e9150",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-systems", "owner": "nix-systems",
"ref": "future-26.11",
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
+26 -1
View File
@@ -7,6 +7,12 @@
inputs.treefmt-nix.follows = "treefmt-nix"; inputs.treefmt-nix.follows = "treefmt-nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
clan-community = {
url = "git+https://git.clan.lol/clan/clan-community";
inputs.clan-core.follows = "clan-core";
inputs.flake-parts.follows = "flake-parts";
inputs.treefmt-nix.follows = "treefmt-nix";
};
devshell = { devshell = {
url = "github:numtide/devshell"; url = "github:numtide/devshell";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
@@ -39,6 +45,12 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.devshell.follows = "devshell"; inputs.devshell.follows = "devshell";
}; };
nixos-images = {
url = "github:nix-community/nixos-images";
inputs.nixos-unstable.follows = "nixpkgs";
inputs.nixos-stable.follows = "nixpkgs";
};
}; };
outputs = outputs =
{ {
@@ -56,7 +68,6 @@
./shell.nix ./shell.nix
./overlays ./overlays
./modules/nixos
./machines ./machines
./routers ./routers
./inventories ./inventories
@@ -78,6 +89,20 @@
packages.think = pkgs.think-gtcm; packages.think = pkgs.think-gtcm;
packages.think-be = pkgs.think-backend-gtcm; packages.think-be = pkgs.think-backend-gtcm;
packages.file-uploader = pkgs.gtcm-file-uploader; packages.file-uploader = pkgs.gtcm-file-uploader;
packages.installer =
(pkgs.nixos [
inputs.nixos-images.nixosModules.image-installer
{
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAB/raxJR8gASmquP63weHelbi+da2WBJR1DgzHPNz/f"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDuhpzDHBPvn8nv8RH1MRomDOaXyP4GziQm7r3MZ1Syk"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEcZ/p1Ofa9liwIzPWzNtONhJ7+FUWd2lCz33r81t8+w kurogeek@kurogeek"
"sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIOJDRQfb1+7VK5tOe8W40iryfBWYRO6Uf1r2viDjmsJtAAAABHNzaDo="
"sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIDgsWq+G/tcr6eUQYT7+sJeBtRmOMabgFiIgIV44XNc6AAAABHNzaDo="
"sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIJMi3TAuwDtIeO4MsORlBZ31HzaV5bji1fFBPcC9/tWuAAAABHNzaDo="
];
}
]).config.system.build.isoImage;
}; };
} }
); );
+303 -27
View File
@@ -19,13 +19,11 @@
w = [ "sirius" ]; w = [ "sirius" ];
b4l = [ b4l = [
"rigel" "rigel"
"neptune"
"rana" "rana"
"petra" "petra"
"alasia" "alasia"
]; ];
phonebox = [ phonebox = [
"neptune"
"rigel" "rigel"
"almach" "almach"
"alpheratz" "alpheratz"
@@ -33,25 +31,173 @@
"adhil" "adhil"
"buna" "buna"
]; ];
global-network = [
prometheus = [
"cursa"
"rigel"
"vega"
"buna"
];
dm-bootstrapper = [
"rigel"
"cursa"
"deneb"
"bosona"
"canopus"
];
dm-pull-deploy = [
"rana" "rana"
"sirius" "rigel"
"hadar" "vega"
"procyon"
"alasia"
]; ];
}; };
instances = { instances = {
borgbackup = {
module = {
name = "borgbackup";
input = "clan-core";
};
roles.client.machines."cursa".settings.destinations = {
alex = {
repo = "ssh://borg@10.0.10.225:2222/backup/cursa/backup";
rsh = "ssh -i /run/secrets/vars/borgbackup/borgbackup.ssh";
};
};
roles.client.machines."hadar".settings.destinations = {
alex = {
repo = "ssh://borg@10.0.10.225:2222/backup/hadar/backup";
rsh = "ssh -i /run/secrets/vars/borgbackup/borgbackup.ssh";
};
};
roles.client.machines."procyon".settings.destinations = {
alex = {
repo = "ssh://borg@10.0.10.225:2222/backup/procyon/backup";
rsh = "ssh -i /run/secrets/vars/borgbackup/borgbackup.ssh";
};
};
roles.client.machines."bosona".settings.destinations = {
alex = {
repo = "ssh://borg@10.0.10.225:2222/backup/bosona/backup";
rsh = "ssh -i /run/secrets/vars/borgbackup/borgbackup.ssh";
};
};
roles.client.machines."canopus".settings.destinations = {
alex = {
repo = "ssh://borg@10.0.10.225:2222/backup/canopus/backup";
rsh = "ssh -i /run/secrets/vars/borgbackup/borgbackup.ssh";
};
};
roles.client.machines."deneb".settings.destinations = {
alex = {
repo = "ssh://borg@10.0.10.225:2222/backup/deneb/backup";
rsh = "ssh -i /run/secrets/vars/borgbackup/borgbackup.ssh";
};
};
roles.client.machines."alasia".settings.destinations = {
alex = {
repo = "ssh://borg@10.0.10.225:2222/backup/alasia/backup";
rsh = "ssh -i /run/secrets/vars/borgbackup/borgbackup.ssh";
};
};
};
data-mesher = {
module = {
name = "data-mesher";
input = "clan-core";
};
roles.bootstrap.tags = [ "dm-bootstrapper" ];
roles.default.tags = [ "all" ];
roles.default.settings.interfaces = [ "ygg" ];
};
auto-pull-update = {
module = {
name = "dm-pull-deploy";
input = "clan-community";
};
roles.push.machines."rigel".settings = {
gitUrl = "https://git.b4l.co.th/newedge/infra.git";
branch = "main";
};
roles.push.extraModules = [
(
{ pkgs, config, ... }:
{
# work around until upstream is fixed
environment.systemPackages = [
(pkgs.writeShellApplication {
name = "custom-dm-send-deploy";
runtimeInputs = [
config.services.data-mesher.package
pkgs.git
pkgs.nix
pkgs.jq
];
text =
let
settings.gitUrl = "https://git.b4l.co.th/newedge/infra.git";
settings.branch = "main";
in
''
if [ $# -gt 1 ]; then
echo "Usage: dm-send-deploy [<flake-ref>]"
echo "Without arguments, sends the latest commit on '${settings.branch}' from ${settings.gitUrl}"
echo "Example: dm-send-deploy git+https://example.com/repo.git?rev=abc123..."
exit 1
fi
KEY="${config.clan.core.vars.generators.dm-pull-deploy-signing-key.files."signing.key".path}"
if [ ! -r "$KEY" ]; then
echo "Error: cannot read signing key at $KEY (are you root?)"
exit 1
fi
if [ $# -eq 1 ]; then
FLAKE_REF="$1"
else
REV=$(git ls-remote "${settings.gitUrl}" "refs/heads/${settings.branch}" | cut -f1)
if [ -z "$REV" ]; then
echo "Error: could not determine latest commit on ${settings.branch} from ${settings.gitUrl}"
exit 1
fi
FLAKE_REF="git+${settings.gitUrl}?rev=$REV"
fi
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
printf '%s' "$FLAKE_REF" > "$TMPFILE"
NETWORK_ID="${config.clan.core.vars.generators.data-mesher-network.files."network.pub".path}"
data-mesher file update "$TMPFILE" \
--url http://localhost:7331 \
--network-id "$NETWORK_ID" \
--key "$KEY" \
--name "dm_pull_deploy/target"
echo "Deployment target pushed: $FLAKE_REF"
'';
})
];
}
)
];
roles.default.tags = [ "dm-pull-deploy" ];
roles.default.settings.action = "switch";
};
sshd = { sshd = {
roles.server.tags."all" = { }; roles.server.tags."all" = { };
roles.server.settings = { roles.server.settings = {
authorizedKeys = { authorizedKeys = {
"berwn" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAB/raxJR8gASmquP63weHelbi+da2WBJR1DgzHPNz/f"; "berwn" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAB/raxJR8gASmquP63weHelbi+da2WBJR1DgzHPNz/f";
"davhau" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDuhpzDHBPvn8nv8RH1MRomDOaXyP4GziQm7r3MZ1Syk"; "davhau" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDuhpzDHBPvn8nv8RH1MRomDOaXyP4GziQm7r3MZ1Syk";
"vi" =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAmgyEGuY/r7SDlJgrzYmQqpcWS5W+fCzRi3OS59ne4W openpgp:0xFF687387";
"kurogeek" = "kurogeek" =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEcZ/p1Ofa9liwIzPWzNtONhJ7+FUWd2lCz33r81t8+w kurogeek@kurogeek"; "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEcZ/p1Ofa9liwIzPWzNtONhJ7+FUWd2lCz33r81t8+w kurogeek@kurogeek";
"matthewcroughan" = "matthewcroughan" =
@@ -102,7 +248,13 @@
name = "zerotier"; name = "zerotier";
input = "clan-core"; input = "clan-core";
}; };
roles.controller.machines."vega" = { }; roles.controller.machines."vega" = {
settings.allowedIds = [
"dbe44c0287" # Alex-gateway
"b0e0b84fd3" # Alex
"2bd36db8cc" # kurogeek-thinkpad
];
};
roles.peer.tags.glom = { }; roles.peer.tags.glom = { };
}; };
@@ -111,7 +263,13 @@
name = "zerotier"; name = "zerotier";
input = "clan-core"; input = "clan-core";
}; };
roles.controller.machines."rigel" = { }; roles.controller.machines."rigel" = {
settings.allowedIds = [
"dbe44c0287" # Alex-gateway
"b0e0b84fd3" # Alex
"2bd36db8cc" # kurogeek-thinkpad
];
};
roles.peer.tags.b4l = { }; roles.peer.tags.b4l = { };
}; };
@@ -129,12 +287,24 @@
roles.peer.tags."poy" = { }; roles.peer.tags."poy" = { };
}; };
internet = {
module.name = "internet";
roles.default.machines = {
ramus.settings.host = "5.223.63.55";
tangra.settings.host = "5.223.65.50";
};
};
yggdrasil-global-network = { yggdrasil-global-network = {
module = { module = {
name = "yggdrasil"; name = "yggdrasil";
input = "clan-core"; input = "clan-core";
}; };
roles.default.tags."global-network" = { }; roles.default.tags."all" = { };
roles.default.settings.extraYggdrasilIPs = [
# kurogeek's laptop
"200:c8db:ea9b:5bdc:44ed:ad87:462a:6bd0"
];
roles.default.settings.extraPeers = [ roles.default.settings.extraPeers = [
"tls://ygg.jjolly.dev:3443" "tls://ygg.jjolly.dev:3443"
"tls://[2602:fc24:18:7a42::1]:993" "tls://[2602:fc24:18:7a42::1]:993"
@@ -150,29 +320,30 @@
}; };
roles.server.machines."alasia".settings = { roles.server.machines."alasia".settings = {
public_url = "tailvpn.public.newedge.house"; public_url = "tailvpn.public.newedge.house";
base_domain = "tailnet.newedge.house";
advertise_routes = [ "10.0.10.0/24" ]; advertise_routes = [ "10.0.10.0/24" ];
dns = {
magic_dns = true;
base_domain = "tailvpn.newedge.house";
nameservers = [ nameservers = [
"10.0.10.82" "10.0.10.82"
"1.1.1.1" "1.1.1.1"
"8.8.8.8" "8.8.8.8"
]; ];
}; extra_records = [
}; {
name = "poyerp.newedge.house";
yggdrasil-phone-network = { type = "A";
module = { value = "10.0.10.1";
name = "yggdrasil"; }
input = "clan-core"; {
}; name = "glomerp.newedge.house";
roles.default.tags."phonebox" = { }; type = "A";
roles.default.settings.extraPeers = [ value = "10.0.10.1";
"tls://ygg.jjolly.dev:3443" }
"tls://[2602:fc24:18:7a42::1]:993"
"tcp://leo.node.3dt.net:9002"
"tcp://ygg-kcmo.incognet.io:8883"
]; ];
}; };
};
};
phonebox = { phonebox = {
module = { module = {
@@ -183,6 +354,13 @@
roles.default.machines."adhil".settings = { roles.default.machines."adhil".settings = {
ata-ethernet-iface = "end0"; ata-ethernet-iface = "end0";
}; };
roles.default.machines."rigel".settings = {
extraClientNumbers = [
"01"
"02"
];
extraFixedIPClient = { };
};
}; };
pulse-stream = { pulse-stream = {
@@ -277,6 +455,104 @@
dataDir = "/mnt/hdd/samba"; dataDir = "/mnt/hdd/samba";
}; };
}; };
wordpress = {
module = {
name = "wordpress";
input = "self";
};
roles.server.machines."tangra".settings = {
tenants = [
"poyfestival.com"
];
phpfpmOptions = ''
upload_max_filesize=64M
post_max_size=128M
'';
wpExtraConfig = ''
define('WP_MEMORY_LIMIT', '256M');
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
define('WP_DEBUG_LOG', false);
'';
};
};
prometheus-monitoring = {
module = {
name = "prometheus";
input = "self";
};
roles.server.machines."cursa".settings = {
matrix-alertmanager = {
enable = true;
homeserverUrl = "https://matrix-client.matrix.org";
matrixUser = "@nixapollo:matrix.org";
matrixRooms = [
{
receivers = [
"matrix"
];
roomId = "!rqIrWqPvsXqMgYpcNZ:matrix.org";
}
];
};
};
roles.nodes.machines = {
vega.settings = {
exporters.smartctl = { };
exporters.zfs = { };
};
rigel.settings = {
exporters.smartctl = { };
};
sirius.settings = {
exporters.smartctl = { };
exporters.zfs = { };
};
buna.settings = {
exporters.smartctl = { };
};
mirach.settings = {
exporters.smartctl = { };
};
almach.settings = {
exporters.smartctl = { };
};
};
};
wifi =
let
networks = {
home = { };
glom = { };
};
in
{
module.name = "wifi";
module.input = "clan-community";
roles.default = {
machines."buna".settings = { inherit networks; };
extraModules = [
(
{ lib, ... }:
{
# profile names match the network attr names above;
# 0 = retry forever for both (defaults: 4 autoconnect attempts, 3 auth attempts)
networking.networkmanager.ensureProfiles.profiles = lib.mapAttrs (_: _: {
connection.autoconnect-retries = 0;
connection.auth-retries = 0;
}) networks;
}
)
];
};
};
}; };
}; };
}; };
+7 -1
View File
@@ -19,7 +19,7 @@
"installedAt": 1765277591 "installedAt": 1765277591
}, },
"buna": { "buna": {
"installedAt": 1765343708 "installedAt": 1787123510
}, },
"rana": { "rana": {
"installedAt": 1773134236 "installedAt": 1773134236
@@ -47,6 +47,12 @@
}, },
"bosona": { "bosona": {
"installedAt": 1779098893 "installedAt": 1779098893
},
"tangra": {
"installedAt": 1779958921
},
"cursa": {
"installedAt": 1782187627
} }
} }
} }
+64 -215
View File
@@ -25,10 +25,7 @@
{ {
"index": 8, "index": 8,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["pci", "bridge"],
"pci",
"bridge"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -76,10 +73,7 @@
{ {
"index": 9, "index": 9,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["pci", "bridge"],
"pci",
"bridge"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -131,10 +125,7 @@
{ {
"index": 10, "index": 10,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["pci", "bridge"],
"pci",
"bridge"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -182,10 +173,7 @@
{ {
"index": 11, "index": 11,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["pci", "bridge"],
"pci",
"bridge"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -241,10 +229,7 @@
{ {
"index": 12, "index": 12,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["pci", "bridge"],
"pci",
"bridge"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -297,21 +282,14 @@
}, },
"driver": "piix4_smbus", "driver": "piix4_smbus",
"driver_module": "i2c_piix4", "driver_module": "i2c_piix4",
"drivers": [ "drivers": ["piix4_smbus"],
"piix4_smbus" "driver_modules": ["i2c_piix4"],
],
"driver_modules": [
"i2c_piix4"
],
"module_alias": "pci:v00008086d00007113sv00001AF4sd00001100bc06sc80i00" "module_alias": "pci:v00008086d00007113sv00001AF4sd00001100bc06sc80i00"
}, },
{ {
"index": 17, "index": 17,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["pci", "bridge"],
"pci",
"bridge"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -361,11 +339,7 @@
{ {
"index": 22, "index": 22,
"attached_to": 15, "attached_to": 15,
"class_list": [ "class_list": ["cdrom", "scsi", "block_device"],
"cdrom",
"scsi",
"block_device"
],
"bus_type": { "bus_type": {
"hex": "0084", "hex": "0084",
"name": "SCSI", "name": "SCSI",
@@ -422,14 +396,8 @@
"unix_device_name2": "/dev/sg1", "unix_device_name2": "/dev/sg1",
"driver": "ata_piix", "driver": "ata_piix",
"driver_module": "ata_piix", "driver_module": "ata_piix",
"drivers": [ "drivers": ["ata_piix", "sr"],
"ata_piix", "driver_modules": ["ata_piix", "sr_mod"]
"sr"
],
"driver_modules": [
"ata_piix",
"sr_mod"
]
} }
], ],
"cpu": [ "cpu": [
@@ -496,9 +464,7 @@
"spectre_v2_user", "spectre_v2_user",
"its" "its"
], ],
"power_management": [ "power_management": [""],
""
],
"bogo": 4224, "bogo": 4224,
"cache": 16384, "cache": 16384,
"page_size": 4096, "page_size": 4096,
@@ -580,9 +546,7 @@
"spectre_v2_user", "spectre_v2_user",
"its" "its"
], ],
"power_management": [ "power_management": [""],
""
],
"bogo": 4224, "bogo": 4224,
"cache": 16384, "cache": 16384,
"page_size": 4096, "page_size": 4096,
@@ -606,11 +570,7 @@
{ {
"index": 23, "index": 23,
"attached_to": 19, "attached_to": 19,
"class_list": [ "class_list": ["disk", "scsi", "block_device"],
"disk",
"scsi",
"block_device"
],
"bus_type": { "bus_type": {
"hex": "0084", "hex": "0084",
"name": "SCSI", "name": "SCSI",
@@ -674,24 +634,15 @@
], ],
"driver": "virtio_scsi", "driver": "virtio_scsi",
"driver_module": "virtio_scsi", "driver_module": "virtio_scsi",
"drivers": [ "drivers": ["sd", "virtio_scsi"],
"sd", "driver_modules": ["sd_mod", "virtio_scsi"]
"virtio_scsi"
],
"driver_modules": [
"sd_mod",
"virtio_scsi"
]
} }
], ],
"graphics_card": [ "graphics_card": [
{ {
"index": 16, "index": 16,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["graphics_card", "pci"],
"graphics_card",
"pci"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -748,12 +699,8 @@
}, },
"driver": "bochs-drm", "driver": "bochs-drm",
"driver_module": "bochs", "driver_module": "bochs",
"drivers": [ "drivers": ["bochs-drm"],
"bochs-drm" "driver_modules": ["bochs"],
],
"driver_modules": [
"bochs"
],
"module_alias": "pci:v00001234d00001111sv00001AF4sd00001100bc03sc00i00" "module_alias": "pci:v00001234d00001111sv00001AF4sd00001100bc03sc00i00"
} }
], ],
@@ -761,10 +708,7 @@
{ {
"index": 24, "index": 24,
"attached_to": 7, "attached_to": 7,
"class_list": [ "class_list": ["usb", "hub"],
"usb",
"hub"
],
"bus_type": { "bus_type": {
"hex": "0086", "hex": "0086",
"name": "USB", "name": "USB",
@@ -837,12 +781,8 @@
"hotplug": "usb", "hotplug": "usb",
"driver": "hub", "driver": "hub",
"driver_module": "usbcore", "driver_module": "usbcore",
"drivers": [ "drivers": ["hub"],
"hub" "driver_modules": ["usbcore"],
],
"driver_modules": [
"usbcore"
],
"module_alias": "usb:v1D6Bp0001d0618dc09dsc00dp00ic09isc00ip00in00" "module_alias": "usb:v1D6Bp0001d0618dc09dsc00dp00ic09isc00ip00in00"
} }
], ],
@@ -850,9 +790,7 @@
{ {
"index": 5, "index": 5,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["memory"],
"memory"
],
"base_class": { "base_class": {
"hex": "0101", "hex": "0101",
"name": "Internally Used Class", "name": "Internally Used Class",
@@ -876,9 +814,7 @@
{ {
"index": 21, "index": 21,
"attached_to": 16, "attached_to": 16,
"class_list": [ "class_list": ["monitor"],
"monitor"
],
"base_class": { "base_class": {
"hex": "0100", "hex": "0100",
"name": "Monitor", "name": "Monitor",
@@ -1024,10 +960,7 @@
{ {
"index": 25, "index": 25,
"attached_to": 24, "attached_to": 24,
"class_list": [ "class_list": ["mouse", "usb"],
"mouse",
"usb"
],
"bus_type": { "bus_type": {
"hex": "0086", "hex": "0086",
"name": "USB", "name": "USB",
@@ -1063,9 +996,7 @@
"model": "QEMU USB Tablet", "model": "QEMU USB Tablet",
"sysfs_id": "/devices/pci0000:00/0000:00:01.2/usb1/1-1/1-1:1.0", "sysfs_id": "/devices/pci0000:00/0000:00:01.2/usb1/1-1/1-1:1.0",
"sysfs_bus_id": "1-1:1.0", "sysfs_bus_id": "1-1:1.0",
"unix_device_names": [ "unix_device_names": ["/dev/input/mice"],
"/dev/input/mice"
],
"unix_device_name2": "/dev/input/mouse0", "unix_device_name2": "/dev/input/mouse0",
"resources": [ "resources": [
{ {
@@ -1106,18 +1037,11 @@
"hotplug": "usb", "hotplug": "usb",
"driver": "usbhid", "driver": "usbhid",
"driver_module": "usbhid", "driver_module": "usbhid",
"drivers": [ "drivers": ["usbhid"],
"usbhid" "driver_modules": ["usbhid"],
],
"driver_modules": [
"usbhid"
],
"driver_info": { "driver_info": {
"type": "mouse", "type": "mouse",
"db_entry_0": [ "db_entry_0": ["explorerps/2", "exps2"],
"explorerps/2",
"exps2"
],
"xf86": "explorerps/2", "xf86": "explorerps/2",
"gpm": "exps2", "gpm": "exps2",
"buttons": -1, "buttons": -1,
@@ -1130,9 +1054,7 @@
{ {
"index": 18, "index": 18,
"attached_to": 13, "attached_to": 13,
"class_list": [ "class_list": ["network_controller"],
"network_controller"
],
"bus_type": { "bus_type": {
"hex": "008f", "hex": "008f",
"name": "Virtio", "name": "Virtio",
@@ -1157,9 +1079,7 @@
"model": "Virtio Ethernet Card 0", "model": "Virtio Ethernet Card 0",
"sysfs_id": "/devices/pci0000:00/0000:00:12.0/virtio1", "sysfs_id": "/devices/pci0000:00/0000:00:12.0/virtio1",
"sysfs_bus_id": "virtio1", "sysfs_bus_id": "virtio1",
"unix_device_names": [ "unix_device_names": ["ens18"],
"ens18"
],
"resources": [ "resources": [
{ {
"type": "hwaddr", "type": "hwaddr",
@@ -1172,12 +1092,8 @@
], ],
"driver": "virtio_net", "driver": "virtio_net",
"driver_module": "virtio_net", "driver_module": "virtio_net",
"drivers": [ "drivers": ["virtio_net"],
"virtio_net" "driver_modules": ["virtio_net"],
],
"driver_modules": [
"virtio_net"
],
"module_alias": "virtio:d00000001v00001AF4" "module_alias": "virtio:d00000001v00001AF4"
} }
], ],
@@ -1185,9 +1101,7 @@
{ {
"index": 26, "index": 26,
"attached_to": 18, "attached_to": 18,
"class_list": [ "class_list": ["network_interface"],
"network_interface"
],
"base_class": { "base_class": {
"hex": "0107", "hex": "0107",
"name": "Network Interface", "name": "Network Interface",
@@ -1201,9 +1115,7 @@
"model": "Ethernet network interface", "model": "Ethernet network interface",
"sysfs_id": "/class/net/ens18", "sysfs_id": "/class/net/ens18",
"sysfs_device_link": "/devices/pci0000:00/0000:00:12.0/virtio1", "sysfs_device_link": "/devices/pci0000:00/0000:00:12.0/virtio1",
"unix_device_names": [ "unix_device_names": ["ens18"],
"ens18"
],
"resources": [ "resources": [
{ {
"type": "hwaddr", "type": "hwaddr",
@@ -1216,19 +1128,13 @@
], ],
"driver": "virtio_net", "driver": "virtio_net",
"driver_module": "virtio_net", "driver_module": "virtio_net",
"drivers": [ "drivers": ["virtio_net"],
"virtio_net" "driver_modules": ["virtio_net"]
],
"driver_modules": [
"virtio_net"
]
}, },
{ {
"index": 27, "index": 27,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["network_interface"],
"network_interface"
],
"base_class": { "base_class": {
"hex": "0107", "hex": "0107",
"name": "Network Interface", "name": "Network Interface",
@@ -1241,19 +1147,14 @@
}, },
"model": "Loopback network interface", "model": "Loopback network interface",
"sysfs_id": "/class/net/lo", "sysfs_id": "/class/net/lo",
"unix_device_names": [ "unix_device_names": ["lo"]
"lo"
]
} }
], ],
"pci": [ "pci": [
{ {
"index": 13, "index": 13,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["pci", "unknown"],
"pci",
"unknown"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -1310,21 +1211,14 @@
}, },
"driver": "virtio-pci", "driver": "virtio-pci",
"driver_module": "virtio_pci", "driver_module": "virtio_pci",
"drivers": [ "drivers": ["virtio-pci"],
"virtio-pci" "driver_modules": ["virtio_pci"],
],
"driver_modules": [
"virtio_pci"
],
"module_alias": "pci:v00001AF4d00001000sv00001AF4sd00000001bc02sc00i00" "module_alias": "pci:v00001AF4d00001000sv00001AF4sd00000001bc02sc00i00"
}, },
{ {
"index": 14, "index": 14,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["pci", "unknown"],
"pci",
"unknown"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -1380,12 +1274,8 @@
}, },
"driver": "virtio-pci", "driver": "virtio-pci",
"driver_module": "virtio_pci", "driver_module": "virtio_pci",
"drivers": [ "drivers": ["virtio-pci"],
"virtio-pci" "driver_modules": ["virtio_pci"],
],
"driver_modules": [
"virtio_pci"
],
"module_alias": "pci:v00001AF4d00001002sv00001AF4sd00000005bc00scFFi00" "module_alias": "pci:v00001AF4d00001002sv00001AF4sd00000005bc00scFFi00"
} }
], ],
@@ -1393,10 +1283,7 @@
{ {
"index": 6, "index": 6,
"attached_to": 17, "attached_to": 17,
"class_list": [ "class_list": ["storage_controller", "pci"],
"storage_controller",
"pci"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -1453,21 +1340,14 @@
}, },
"driver": "virtio-pci", "driver": "virtio-pci",
"driver_module": "virtio_pci", "driver_module": "virtio_pci",
"drivers": [ "drivers": ["virtio-pci"],
"virtio-pci" "driver_modules": ["virtio_pci"],
],
"driver_modules": [
"virtio_pci"
],
"module_alias": "pci:v00001AF4d00001004sv00001AF4sd00000008bc01sc00i00" "module_alias": "pci:v00001AF4d00001004sv00001AF4sd00000008bc01sc00i00"
}, },
{ {
"index": 15, "index": 15,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["storage_controller", "pci"],
"storage_controller",
"pci"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -1557,12 +1437,8 @@
}, },
"driver": "ata_piix", "driver": "ata_piix",
"driver_module": "ata_piix", "driver_module": "ata_piix",
"drivers": [ "drivers": ["ata_piix"],
"ata_piix" "driver_modules": ["ata_piix"],
],
"driver_modules": [
"ata_piix"
],
"module_alias": "pci:v00008086d00007010sv00001AF4sd00001100bc01sc01i80" "module_alias": "pci:v00008086d00007010sv00001AF4sd00001100bc01sc01i80"
} }
], ],
@@ -1573,9 +1449,7 @@
{ {
"index": 19, "index": 19,
"attached_to": 6, "attached_to": 6,
"class_list": [ "class_list": ["unknown"],
"unknown"
],
"base_class": { "base_class": {
"hex": "0000", "hex": "0000",
"name": "Unclassified device", "name": "Unclassified device",
@@ -1593,20 +1467,14 @@
"sysfs_bus_id": "virtio2", "sysfs_bus_id": "virtio2",
"driver": "virtio_scsi", "driver": "virtio_scsi",
"driver_module": "virtio_scsi", "driver_module": "virtio_scsi",
"drivers": [ "drivers": ["virtio_scsi"],
"virtio_scsi" "driver_modules": ["virtio_scsi"],
],
"driver_modules": [
"virtio_scsi"
],
"module_alias": "virtio:d00000008v00001AF4" "module_alias": "virtio:d00000008v00001AF4"
}, },
{ {
"index": 20, "index": 20,
"attached_to": 14, "attached_to": 14,
"class_list": [ "class_list": ["unknown"],
"unknown"
],
"base_class": { "base_class": {
"hex": "0000", "hex": "0000",
"name": "Unclassified device", "name": "Unclassified device",
@@ -1624,12 +1492,8 @@
"sysfs_bus_id": "virtio0", "sysfs_bus_id": "virtio0",
"driver": "virtio_balloon", "driver": "virtio_balloon",
"driver_module": "virtio_balloon", "driver_module": "virtio_balloon",
"drivers": [ "drivers": ["virtio_balloon"],
"virtio_balloon" "driver_modules": ["virtio_balloon"],
],
"driver_modules": [
"virtio_balloon"
],
"module_alias": "virtio:d00000005v00001AF4" "module_alias": "virtio:d00000005v00001AF4"
} }
], ],
@@ -1637,10 +1501,7 @@
{ {
"index": 7, "index": 7,
"attached_to": 0, "attached_to": 0,
"class_list": [ "class_list": ["usb_controller", "pci"],
"usb_controller",
"pci"
],
"bus_type": { "bus_type": {
"hex": "0004", "hex": "0004",
"name": "PCI", "name": "PCI",
@@ -1707,25 +1568,15 @@
}, },
"driver": "uhci_hcd", "driver": "uhci_hcd",
"driver_module": "uhci_hcd", "driver_module": "uhci_hcd",
"drivers": [ "drivers": ["uhci_hcd"],
"uhci_hcd" "driver_modules": ["uhci_hcd"],
],
"driver_modules": [
"uhci_hcd"
],
"driver_info": { "driver_info": {
"type": "module", "type": "module",
"db_entry_0": [ "db_entry_0": ["uhci-hcd"],
"uhci-hcd"
],
"active": true, "active": true,
"modprobe": true, "modprobe": true,
"names": [ "names": ["uhci-hcd"],
"uhci-hcd" "module_args": [""],
],
"module_args": [
""
],
"conf": "" "conf": ""
}, },
"module_alias": "pci:v00008086d00007020sv00001AF4sd00001100bc0Csc03i00" "module_alias": "pci:v00008086d00007020sv00001AF4sd00001100bc0Csc03i00"
@@ -1838,9 +1689,7 @@
"name": "RAM", "name": "RAM",
"value": 7 "value": 7
}, },
"memory_type_details": [ "memory_type_details": ["Other"],
"Other"
],
"speed": 0 "speed": 0
} }
], ],
+728 -606
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -50,7 +50,7 @@ in
]; ];
sites = { sites = {
"${sitename}" = { "${sitename}" = {
domains = [ "localhost" ]; domains = [ sitename ];
apps = [ apps = [
"frappe" "frappe"
"erpnext" "erpnext"
+14
View File
@@ -0,0 +1,14 @@
{
...
}:
{
clan.core.settings.machine.description =
"VM machine for collecting prometheus metrics and fire alerts";
nixpkgs.hostPlatform = {
system = "x86_64-linux";
};
system.stateVersion = "25.11";
clan.core.sops.defaultGroups = [ "admins" ];
}
+85
View File
@@ -0,0 +1,85 @@
let
hashDisk = disk: "os-${builtins.substring 0 5 (builtins.hashString "sha256" disk)}";
os = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi0";
in
{
boot.loader = {
systemd-boot = {
enable = true;
};
efi = {
canTouchEfiVariables = true;
};
};
boot.zfs.forceImportRoot = true;
disko.devices = {
disk = {
"os-${hashDisk os}" = {
type = "disk";
device = os;
content = {
type = "gpt";
partitions = {
ESP = {
size = "1G";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "nofail" ];
};
};
system = {
size = "100%";
content = {
type = "zfs";
pool = "zroot";
};
};
};
};
};
};
zpool = {
zroot = {
type = "zpool";
rootFsOptions = {
mountpoint = "none";
compression = "lz4";
acltype = "posixacl";
xattr = "sa";
"com.sun:auto-snapshot" = "true";
};
options.ashift = "12";
datasets = {
"root" = {
type = "zfs_fs";
options.mountpoint = "none";
};
"root/nixos" = {
type = "zfs_fs";
options.mountpoint = "/";
mountpoint = "/";
};
"root/home" = {
type = "zfs_fs";
options.mountpoint = "/home";
mountpoint = "/home";
};
"root/tmp" = {
type = "zfs_fs";
mountpoint = "/tmp";
options = {
mountpoint = "/tmp";
sync = "disabled";
};
};
};
};
};
};
}
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -9,7 +9,15 @@
]; ];
clan = { clan = {
meta.name = "NewEdgeClan"; meta.name = "NewEdgeClan";
machines = { }; machines = {
cursa = { ... }: { clan.core.state."vars".folders = [ "/var/lib" ]; };
hadar = { ... }: { clan.core.state."vars".folders = [ "/var/lib" ]; };
procyon = { ... }: { clan.core.state."vars".folders = [ "/var/lib" ]; };
bosona = { ... }: { clan.core.state."vars".folders = [ "/var/lib" ]; };
canopus = { ... }: { clan.core.state."vars".folders = [ "/var/lib" ]; };
deneb = { ... }: { clan.core.state."vars".folders = [ "/var/lib" ]; };
alasia = { ... }: { clan.core.state."vars".folders = [ "/var/lib" ]; };
};
secrets.age.plugins = [ secrets.age.plugins = [
"age-plugin-yubikey" "age-plugin-yubikey"
"age-plugin-fido2-hmac" "age-plugin-fido2-hmac"
+7
View File
@@ -25,8 +25,11 @@ in
inputs.self.overlays.frappixLibsOverlay inputs.self.overlays.frappixLibsOverlay
inputs.self.overlays.frappixPythonOverlay inputs.self.overlays.frappixPythonOverlay
inputs.self.overlays.frappixToolsOverlay inputs.self.overlays.frappixToolsOverlay
inputs.self.overlays.frappixAppsOverlay
]; ];
fonts.packages = [ pkgs.tlwg ];
clan.core.vars.generators.frappix = { clan.core.vars.generators.frappix = {
files = { files = {
sslCertificate.secret = false; sslCertificate.secret = false;
@@ -56,6 +59,8 @@ in
pkgs.frappix.erpnext pkgs.frappix.erpnext
pkgs.frappix.hrms pkgs.frappix.hrms
pkgs.frappix.crm pkgs.frappix.crm
pkgs.frappix.posprinter
pkgs.frappix.default_thai_company
]; ];
sites = { sites = {
"${sitename}" = { "${sitename}" = {
@@ -65,6 +70,8 @@ in
"erpnext" "erpnext"
"hrms" "hrms"
"crm" "crm"
"posprinter"
"default_thai_company"
]; ];
}; };
}; };
+3 -17
View File
@@ -9,13 +9,6 @@ let
in in
{ {
clan.core.settings.machine.description = "VM machine that host Inventree system for Poy"; clan.core.settings.machine.description = "VM machine that host Inventree system for Poy";
imports = [
inputs.self.nixosModules.inventree
];
nixpkgs.overlays = [
inputs.self.overlays.packagesOverlay
];
nixpkgs.hostPlatform = { nixpkgs.hostPlatform = {
system = "x86_64-linux"; system = "x86_64-linux";
@@ -82,19 +75,12 @@ in
services.inventree = { services.inventree = {
enable = true; enable = true;
hostName = "${domain}"; inherit domain;
config.site_url = "https://${config.services.inventree.hostName}";
secretKeyFile = config.clan.core.vars.generators.inventree.files.secret-key.path; secretKeyFile = config.clan.core.vars.generators.inventree.files.secret-key.path;
config.oidc_private_key_file = config.clan.core.vars.generators.inventree.files.oidc-key.path; adminPasswordFile = config.clan.core.vars.generators.inventree.files.admin-password.path;
config.adminPasswordFile = config.clan.core.vars.generators.inventree.files.admin-password.path; settings.INVENTREE_SITE_URL = "https://${domain}";
}; };
# services.nginx.virtualHosts."${domain}" = {
# forceSSL = true;
# sslCertificate = config.clan.core.vars.generators.nginx.files.sslCert.path;
# sslCertificateKey = config.clan.core.vars.generators.nginx.files.sslKey.path;
# };
system.stateVersion = "25.11"; system.stateVersion = "25.11";
clan.core.sops.defaultGroups = [ "admins" ]; clan.core.sops.defaultGroups = [ "admins" ];
} }
-1
View File
@@ -10,7 +10,6 @@
}; };
system.stateVersion = "25.11"; system.stateVersion = "25.11";
clan.core.sops.defaultGroups = [ "admins" ]; clan.core.sops.defaultGroups = [ "admins" ];
clan.core.networking.targetHost = "root@[${config.clan.core.vars.generators.zerotier.files.zerotier-ip.value}]";
networking.interfaces.enx00e04c106368.useDHCP = true; # recovery networking.interfaces.enx00e04c106368.useDHCP = true; # recovery
+3 -17
View File
@@ -9,13 +9,6 @@ let
in in
{ {
clan.core.settings.machine.description = "VM machine that host Inventree system for Glom"; clan.core.settings.machine.description = "VM machine that host Inventree system for Glom";
imports = [
inputs.self.nixosModules.inventree
];
nixpkgs.overlays = [
inputs.self.overlays.packagesOverlay
];
nixpkgs.hostPlatform = { nixpkgs.hostPlatform = {
system = "x86_64-linux"; system = "x86_64-linux";
@@ -82,19 +75,12 @@ in
services.inventree = { services.inventree = {
enable = true; enable = true;
hostName = "${domain}"; inherit domain;
config.site_url = "https://${config.services.inventree.hostName}";
secretKeyFile = config.clan.core.vars.generators.inventree.files.secret-key.path; secretKeyFile = config.clan.core.vars.generators.inventree.files.secret-key.path;
config.oidc_private_key_file = config.clan.core.vars.generators.inventree.files.oidc-key.path; adminPasswordFile = config.clan.core.vars.generators.inventree.files.admin-password.path;
config.adminPasswordFile = config.clan.core.vars.generators.inventree.files.admin-password.path; settings.INVENTREE_SITE_URL = "https://${domain}";
}; };
# services.nginx.virtualHosts."${domain}" = {
# forceSSL = true;
# sslCertificate = config.clan.core.vars.generators.nginx.files.sslCert.path;
# sslCertificateKey = config.clan.core.vars.generators.nginx.files.sslKey.path;
# };
system.stateVersion = "25.11"; system.stateVersion = "25.11";
clan.core.sops.defaultGroups = [ "admins" ]; clan.core.sops.defaultGroups = [ "admins" ];
} }
-1
View File
@@ -9,7 +9,6 @@
clan.core.settings.machine.description = "A Hetzner VPS machine own by Alex."; clan.core.settings.machine.description = "A Hetzner VPS machine own by Alex.";
clan.core.sops.defaultGroups = [ "admins" ]; clan.core.sops.defaultGroups = [ "admins" ];
clan.core.networking.targetHost = "root@[${config.clan.core.vars.generators.zerotier.files.zerotier-ip.value}]";
clan.core.vars.generators.acme = { clan.core.vars.generators.acme = {
share = true; share = true;
-11
View File
@@ -1,18 +1,7 @@
{ config, ... }: { config, ... }:
{ {
imports = [
(import ../../lib/auto-accept-zerotier-members.nix {
memberIds = [
"dbe44c0287" # Alex-gateway
"b0e0b84fd3" # Alex
"2bd36db8cc" # kurogeek-thinkpad
];
})
];
system.stateVersion = "25.11"; system.stateVersion = "25.11";
clan.core.sops.defaultGroups = [ "admins" ]; clan.core.sops.defaultGroups = [ "admins" ];
clan.core.networking.targetHost = "root@[${config.clan.core.vars.generators.zerotier.files.zerotier-ip.value}]";
clan.core.settings.machine.description = "Zima board computer for testing in B4L"; clan.core.settings.machine.description = "Zima board computer for testing in B4L";
} }
+22 -8
View File
@@ -5,8 +5,6 @@
}: }:
{ {
imports = [ imports = [
self.nixosModules.common
./hardware-configuration.nix ./hardware-configuration.nix
]; ];
@@ -23,15 +21,12 @@
nixpkgs.hostPlatform = { nixpkgs.hostPlatform = {
system = "aarch64-linux"; system = "aarch64-linux";
}; };
nixpkgs.buildPlatform = {
system = "x86_64-linux";
};
system.stateVersion = "25.11"; system.stateVersion = "25.11";
services.journald.extraConfig = ''
Storage=volatile
RuntimeMaxUse=30M
RuntimeMaxFileSize=10M
'';
services.udisks2.enable = false; services.udisks2.enable = false;
nix.settings.log-lines = 25; nix.settings.log-lines = 25;
@@ -54,6 +49,25 @@
"sd_mod" "sd_mod"
]; ];
boot = {
consoleLogLevel = 0;
kernel.sysctl = {
"fs.suid_dumpable" = 0;
"kernel.core_pattern" = "/dev/null";
};
tmp = {
useTmpfs = true;
};
};
services.journald.extraConfig = ''
Storage=none
'';
systemd = {
coredump.enable = false;
};
fileSystems."/mnt/hdd" = { fileSystems."/mnt/hdd" = {
device = "zdata/nas"; device = "zdata/nas";
fsType = "zfs"; fsType = "zfs";
+32
View File
@@ -0,0 +1,32 @@
{
system.stateVersion = "25.11";
nixpkgs.hostPlatform = {
system = "x86_64-linux";
};
clan.core.settings.name = "tangra";
clan.core.settings.machine.description =
"A Hetzner VPS machine own by Alex. This is a machine for poyfestival.com";
clan.core.sops.defaultGroups = [ "admins" ];
clan.core.vars.generators.acme = {
share = true;
files.email.secret = false;
prompts.email = {
type = "line";
description = "Email for ACME registeration";
};
script = ''
cat $prompts/email > $out/email
'';
};
users.users.nginx.extraGroups = [ "acme" ];
security.acme.acceptTerms = true;
imports = [ ];
}
+86
View File
@@ -0,0 +1,86 @@
{ ... }:
let
hashDisk = disk: "os-${builtins.substring 0 5 (builtins.hashString "sha256" disk)}";
os = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_119349241";
in
{
boot.loader = {
systemd-boot = {
enable = true;
};
efi = {
canTouchEfiVariables = true;
};
};
boot.zfs.forceImportRoot = true;
disko.devices = {
disk = {
"os-${hashDisk os}" = {
type = "disk";
device = os;
content = {
type = "gpt";
partitions = {
ESP = {
size = "1G";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "nofail" ];
};
};
system = {
size = "100%";
content = {
type = "zfs";
pool = "zroot";
};
};
};
};
};
};
zpool = {
zroot = {
type = "zpool";
rootFsOptions = {
mountpoint = "none";
compression = "lz4";
acltype = "posixacl";
xattr = "sa";
"com.sun:auto-snapshot" = "true";
};
options.ashift = "12";
datasets = {
"root" = {
type = "zfs_fs";
options.mountpoint = "none";
};
"root/nixos" = {
type = "zfs_fs";
options.mountpoint = "/";
mountpoint = "/";
};
"root/home" = {
type = "zfs_fs";
options.mountpoint = "/home";
mountpoint = "/home";
};
"root/tmp" = {
type = "zfs_fs";
mountpoint = "/tmp";
options = {
mountpoint = "/tmp";
sync = "disabled";
};
};
};
};
};
};
}
File diff suppressed because it is too large Load Diff
-8
View File
@@ -10,17 +10,9 @@
(inputs.import-tree ./services) (inputs.import-tree ./services)
(import ../../lib/auto-accept-zerotier-members.nix {
memberIds = [
"dbe44c0287" # Alex-gateway
"b0e0b84fd3" # Alex
"2bd36db8cc" # kurogeek-thinkpad
];
})
]; ];
clan.core.sops.defaultGroups = [ "admins" ]; clan.core.sops.defaultGroups = [ "admins" ];
clan.core.networking.targetHost = "root@[${config.clan.core.vars.generators.zerotier.files.zerotier-ip.value}]";
clan.core.settings.machine.description = "Glom NAS"; clan.core.settings.machine.description = "Glom NAS";
+3
View File
@@ -0,0 +1,3 @@
# Apple Network
This service allows atalkd instances to connect to each other over vxlan over Yggdrasil. We need to use vxlan due to the support of broadcasting it provides. The service will form a network of star-topology vxlan using the underlying Yggdrasil network. With this vxlan network, we can have a private Apple Talk network over the internet.
+139
View File
@@ -0,0 +1,139 @@
{ clanLib, ... }:
{
_class = "clan.service";
manifest.name = "apple-network";
manifest.description = "This service will create an instance of `atalkd` for each peer to talk to Apple machines over Apple Talk protocol and automatically connect each peer using vxlan over Yggdrasil network to achieve Apple Talk over internet experience";
manifest.readme = builtins.readFile ./README.md;
manifest.categories = [ "Network" ];
roles.peer = {
description = "A gateway machine that allow classic Apple machines to connect to other Apple machines over the internet";
interface =
{ lib, ... }:
{
options = {
zone_name = lib.mkOption {
type = with lib.types; str;
description = "Zone name for Apple Talk protocol";
default = "Default";
};
};
};
perInstance =
{ roles, settings, ... }:
{
nixosModule =
{
lib,
config,
pkgs,
...
}:
let
vxlanPort = 4789;
getYggdrasilIP =
machineName:
if config.clan.core.vars.generators.yggdrasil.files.address ? value then
clanLib.getPublicValue {
flake = config.clan.core.settings.directory;
machine = machineName;
generator = "yggdrasil";
file = "address";
default = null;
}
else
throw "clanService/yggdrasil is required";
noSelfPeers = builtins.filter (peerName: peerName != config.clan.core.settings.machine.name) (
lib.mapAttrsToList (machineName: _: machineName) roles.peer.machines
);
sortedPeers = builtins.sort (x: y: x < y) (
lib.mapAttrsToList (machineName: _: machineName) roles.peer.machines
);
getMachineIndex = machineName: lib.lists.findFirstIndex (x: x == machineName) null sortedPeers;
selfVXAddress = "192.168.254.${
lib.toString ((getMachineIndex config.clan.core.settings.machine.name) + 1)
}/24";
in
{
services.atalkd = {
enable = true;
interfaces = {
vxlan.config = ''
-router -phase 2 -net 1 -zone "${settings.zone_name}"
'';
};
};
networking.useNetworkd = true;
networking.firewall.interfaces."ygg".allowedUDPPorts = [ vxlanPort ];
boot.kernelModules = [ "vxlan" ];
systemd.network.netdevs =
builtins.listToAttrs (
map (
peerName:
(lib.nameValuePair "10-apl-vxlan-${peerName}" {
enable = true;
netdevConfig = {
Name = "vxlan-${peerName}";
Kind = "vxlan";
};
vxlanConfig = {
VNI = (getMachineIndex config.clan.core.settings.machine.name) + (getMachineIndex peerName);
Remote = getYggdrasilIP peerName;
DestinationPort = vxlanPort;
Independent = true;
};
})
) noSelfPeers
)
// {
"10-apl-vxlan" = {
enable = true;
netdevConfig = {
Kind = "bridge";
Name = "vxlan";
};
bridgeConfig = {
STP = true;
};
};
};
systemd.network.networks =
builtins.listToAttrs (
map (
peerName:
(lib.nameValuePair "10-apl-vxlan-${peerName}" {
enable = true;
matchConfig.Name = "vxlan-${peerName}";
networkConfig.Bridge = "vxlan";
})
) noSelfPeers
)
// {
"10-apl-vxlan" = {
enable = true;
matchConfig.Name = "vxlan";
address = [ selfVXAddress ];
};
};
environment.systemPackages = [
pkgs.netatalk
pkgs.bridge-utils
];
};
};
};
}
@@ -0,0 +1,19 @@
{ inputs, self, ... }:
let
module = ./default.nix;
in
{
clan.modules = {
apple-network = module;
};
perSystem =
{ ... }:
{
clan.nixosTests.service-apple-network = {
imports = [ ./tests/vm/default.nix ];
_module.args = { inherit self inputs; };
clan.modules."@clan/apple-network" = module;
};
};
}
@@ -0,0 +1,76 @@
{
self,
lib,
config,
hostPkgs,
...
}:
{
name = "service-apple-network";
result.update-vars =
let
relativeDir = lib.removePrefix "${self}/" (toString config.clan.directory);
in
hostPkgs.writeShellScriptBin "update-vars" ''
set -x
export PRJ_ROOT=$(git rev-parse --show-toplevel)
${
self.inputs.clan-core.packages.${hostPkgs.system}.clan-cli
}/bin/clan-generate-test-vars $PRJ_ROOT/${relativeDir} ${config.name}
'';
clan = {
directory = ./.;
test.useContainers = false;
inventory = {
meta.domain = "test.clan";
machines.peer1 = { };
machines.peer2 = { };
machines.peer3 = { };
instances = {
apple-network = {
module.name = "@clan/apple-network";
module.input = "self";
roles.peer.machines = {
peer1 = { };
peer2 = { };
peer3 = { };
};
};
yggdrasil = {
module.name = "yggdrasil";
roles.default.tags.all = { };
roles.default.settings.extraPeers = [
"tls://ygg.jjolly.dev:3443"
"tls://[2602:fc24:18:7a42::1]:993"
"tcp://leo.node.3dt.net:9002"
"tcp://ygg-kcmo.incognet.io:8883"
];
};
};
};
};
nodes = {
peer1 = { };
peer2 = { };
peer3 = { };
};
testScript = _: ''
# cannot test connectivity due to Yggdrasil's establishment
start_all()
peer1.wait_for_unit("atalkd")
peer1.succeed("systemctl status atalkd")
peer2.wait_for_unit("atalkd")
peer2.succeed("systemctl status atalkd")
peer3.wait_for_unit("atalkd")
peer3.succeed("systemctl status atalkd")
'';
}
@@ -0,0 +1,6 @@
[
{
"publickey": "age1f92zl8y70z5w9gn0l5leg2fhyvxy6m93dfh7n6ltgvuu9nyg6epsrw6t8x",
"type": "age"
}
]
@@ -0,0 +1,6 @@
[
{
"publickey": "age1u36fr87cru90vfznf5szasya9nhvvseh4atk8zsdj0rrsu0nnvwqsyt8g6",
"type": "age"
}
]
@@ -0,0 +1,6 @@
[
{
"publickey": "age1rka3368ltqc787xw99uu762lqfk7qxld9265u2t62fpxnp9xhufsc2q5rw",
"type": "age"
}
]
@@ -0,0 +1,14 @@
{
"data": "ENC[AES256_GCM,data:hC4Lle6mXuTFq//wBHUTyH3X778u/gYJSXJOiQ7xUb+gnC8beH1olxL2ZhtUiMkktzhsPM/j6cxhltPp25UTeJnUOcoYesqZcp8=,iv:WOx2P2Cu4v58xM9ePjAw1gdA0fFnGG2UqEXH2iksGoo=,tag:43n1ibVSy2AeS9f5qQBKNQ==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtT244N1Q3Zk9pRjFFQlUr\ncDExSTBwSWk5UXVyMjhWclQwSFZaYnpTRFY0CnE5YStVdzMrS3BKYzYvc3V3QVFY\nbDVwVytoNERveU1xQllqNUFGV2hYbGMKLS0tIDJSUmkwMUQzL3Y0MU5qZTJ3eFN3\nNGdwamM1UDBPdzhvSWsyZzR6anZ0eFEKKnr1/7rf3fH1i7KUoZdilLDgb44K2qzn\nI9Y+7FbV4gzQUzKWjAQeFN37Z4Qiuy0xJVA0lw8KvM+NCxSZDfh3kw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg"
}
],
"lastmodified": "2026-07-03T10:46:56Z",
"mac": "ENC[AES256_GCM,data:hmpbOtra6EyNiPg7EsA3F4elHEO87JPUr4VKVppvzUwLX1HSj67WrU6k3rEoJT+k7Hq1lxnAzDEckSG1fUOap11rk8ksp1IAGR0+yqykC/Qj/Nl8wXqP84gr49bJK6xE/DDdCRCcBff2d6IpnehyEr8fGFEZbWGygblec8U8GPo=,iv:+M1C3rL22foowr0VD80jwrNFb5xz4k5JQ/DJm4ThKHk=,tag:wabFAC5NTCVGfIQQzm0yGg==,type:str]",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../users/admin
@@ -0,0 +1,14 @@
{
"data": "ENC[AES256_GCM,data:ndWIEOqP2ybCN7Ak5g+8q3YG5rLQ7OuSv/vCbnMA8AK6Lby2JMGRkRGysqJdyEF7eh4w7PpZ/AtKPtPbUucjeTB5+8g76PPJTBE=,iv:EYy0GGqEBmIc5rSWP7Zleb5eQ0JzTz84HqeJPpCSpxk=,tag:iMgTUb9+wxE9S2kKoOEvsQ==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQTEF6OXN4K0dHdXhWWmt2\neG5wdkNIc2F0UnFEeHFLQkdTWkFpUWhTa0NBCmw2N2J1T0FJVE9JR3VBRWV0THh5\nVEZlbVZrcXAwK3BsYXFOZUF6c3lRSkUKLS0tIFRCQzNUaGFHdERWNWR4enVWRGpx\nbDEzRU9jWHgxWHovaENjbkFlZERIbkkKvAW31gTJg9izrWuAMeDHS9SWPQYtxRBY\necDCn8QORFrnDdxEusCnGJrAU4kmS3Y3CwSHCtEijs3JdIgg9wl/MA==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg"
}
],
"lastmodified": "2026-07-03T10:47:04Z",
"mac": "ENC[AES256_GCM,data:y1U2vsdqR97fPAnp6iZiLY1KbWYmgDqgZ5bwIkWPCr7Gjjrn+qV+sc2T1BTVz3Zo0JOz4ScCc+f6snVsFKdCDU/NmDVFt/sqyH4NHUPRQ3StsQim8C7IkSwUtm3EhrqpLYFD21MEjrYYdHMHbWybxrOI0qI6mDwZymHJFeCLdQA=,iv:lVvPn69eQE/huwDmwePepfpi2l9ZOPQO7W20o4AJBFQ=,tag:7VPfjKZsBVJTZxNwTcI6sg==,type:str]",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../users/admin
@@ -0,0 +1,14 @@
{
"data": "ENC[AES256_GCM,data:JPdetkhtF9eSmgEfCO2exXlGQ5KWLeIM3mnW5BfyTY8r0PxWSEGAwBskC1SiDdRERWB2VEbPiPgCNXMnGaYlDqSdZN6R9wSFvsQ=,iv:qGveNhUtgxvKnmdsPoPpffKyi5srlq0Pd6aW283B5C0=,tag:TcJsCrBlzfjXvztcRaxYFQ==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvdjNHa1RlcFJHazB5TXFu\nOUQ4REpCZm5Yc3ZhcVhwT1FqRHlvbnoxZWlFCnNrVUEyK2ZYSmlacmJ2b2QrclZl\nSDV1eWxXVmJxZlhMMXgrTWtjcjVhQlkKLS0tIHZPN3pCUVBLQmpRVGx6SDBQNDZo\nSitheGhXNGl5QW9HTGNsTTdTdWVwaE0KfuM5xZOPbR1lxkJPJUOVrtlW5Ujobemp\nid40reDqxKQxP3khJv8lf9ZoN8LAx2Iwh0DeAl+UJpsoVWgS7uI2qQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg"
}
],
"lastmodified": "2026-07-07T04:54:10Z",
"mac": "ENC[AES256_GCM,data:Tt8yqzfAnSDe2n7KDt1Z/D73hkS1w7DsqIh+s5WljLzar9PEt5vUCLb8DIvBwITyzGAoHH1ym3v3NrnJWCdxajKExTMBf4aY3MJCtykeCG37701S/KxumjTW9/fJw74gcET5I6NafTWb6H5XHsrRaQX1Xnspcs8B7uLGZZ28dSM=,iv:Fo5UPVhnjuOx6Cp4grofHOiD2pm0TXuRRuoTwvxKn1s=,tag:VQ/TF1TiyJADj7/3RwPCAg==,type:str]",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../users/admin
@@ -0,0 +1,4 @@
{
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"type": "age"
}
@@ -0,0 +1 @@
200:e6cc:c86:f02d:b0d5:fba9:9b2c:eb77
@@ -0,0 +1 @@
../../../../../../sops/machines/peer1
@@ -0,0 +1,18 @@
{
"data": "ENC[AES256_GCM,data:6YDp5S2TzEnhDgGhcz8cA3uBsfDt0Oc4JpztB1JUcg/+tDBbeaQN4MLxuZKstXn2Hb1mYtBLlX0vHw++T181Js2sIznyfC1lr68GOvQAp4NYgf23dXpdZk/CMPNUMiGOYVwKWQ/Z0/p0azJ6xV29dYQcM4Q9hys=,iv:Ijzz8hm0looCWoSpjg/rYKp9jhvzRuLboKWHrlwVeFA=,tag:kJcH/1Pu+9q9f2J68LETug==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYNDNEL3kvdEpEMjYyOFJG\nMk5adU04M3hxa21jTTZNM2dQREpmUlVES1NvCjh6eDNFZ1hzUVlTY3Nidnp3TkYr\nRVl4TFlrNUE3dVIzZzg2VWMwTFBYUXMKLS0tIFE0dVFQQmxwOFJ6YkdaQlh6dUlW\nTi8rdXdQbGR1eXpLcTdOandqQmxka3cKImbmJQo2YMYMSQD8kNTgol3VJ4YGIaoz\nl3AY4u/fobg5qlzVQwV33G0/Wf72Z2aTjSLnP4thnasQ89PdYvs7hA==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1f92zl8y70z5w9gn0l5leg2fhyvxy6m93dfh7n6ltgvuu9nyg6epsrw6t8x"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiaWw5SU9KWmJnVVVmTVpr\nL1V6RnRYUGg2UnhXR1BnVTh5eFhnNFpidGdjCmdMeU1nZkZmTTVwbkNaVElFa0Z4\nVkdGdjc4c2lPWGNrVGxocE9iQUZ3QTAKLS0tIHIzTzRRbXVkM3I4ZVRxeSs5eHV5\nTUxOaytzb2MxQ2UybkVCYWVWVmVEcUEKPeK+CC7r3CP5BJqg9thWNHWjPBp/ueHU\nRif6T79kqf+c3B34qXwR0zfyM1+a0r9GAVVC2/q+Ha/As5sPJU39ww==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg"
}
],
"lastmodified": "2026-07-03T10:46:56Z",
"mac": "ENC[AES256_GCM,data:PffiPU1GR9DojTb7TxzBkpivKV2ybmmq9h4pOOyq7p5BAIPF+JSe+BDtbItQ+Gn5LnitQRUVvhat8E2iYfz3JgNxNKcsNFU/mWtCz3PlqyiuRzUBtEL3zYfhdMi8hYYlVdfMumaK3VMk+b4uDGExR+MpURL97TFku47qRbzbILs=,iv:3//fj7WtIX2QNYB9ub1JKAFIn5vJeuZl6rqaEsX0oQc=,tag:njU+WAVq7xBgE0sna2dMKA==,type:str]",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../../../../sops/users/admin
@@ -0,0 +1 @@
8c99f9bc87e92795022b32698a440d6f9fe01d1110f867de8176bb190b92acab
@@ -0,0 +1 @@
200:eb95:7d1b:3ac3:62e7:24af:2274:3771
@@ -0,0 +1 @@
../../../../../../sops/machines/peer2
@@ -0,0 +1,18 @@
{
"data": "ENC[AES256_GCM,data:vOwMqT12A5ap5hRvCGTjTUX4sXKO3N7KE8eQ+/SCcBWndU9/b2AehiNgplWWMGcYdmeDL9qI8V2pPkgBkQ/tWH6N8DccSM/zQzrBUkXvRApl6hvNZAWuBVf/P4MW4/EbRGtMg2tEXkaPDtRn2TsDK7ygmXmqArI=,iv:FkxFx/ClAftLu85S6Uq3ZOom+YDOpeWGlYLOFBmFfnk=,tag:zprw62qiu+hmLZEZbnqYXA==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0WVB1M2ttd0FocGhTSjNo\nUDFIcmpsL051TndWbkJVakJHbUxjY3Q3NjM0CnkrbFlHS1E4bkZJQTIvMDJ3VDFv\nNW1OdmwvalBEelN2d3YwbHVqTW94OFEKLS0tIGtpOWxCM29xdXhQL2Yzb094OG1n\nT21mc25yU1dJL29vekRHdldzdDR3dEkKoZ1dPRaPg8iVsuec6IhV3cLPchokV2wv\nu6qb92hsLQuyPHDA01Oey0jxmaYMQGtAVjo2zEM+E+zrKY+f2pHIUQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArcmo5d2x6c3J0YTkwMGJE\nanphd2FiUjdGdmdNeW0rL0FpcTVSM3dublZ3CnphajNVV2YrTGl2a3ZqMTJKeU4v\nTzZRS2F5MlA5Ny84cFFoNWZQZnpBZTgKLS0tIHVKQUJWcmlPdy9GNXNBYnpJcEVC\nNmJVMlpvSzZPdk1JY2hIZVEvc3cwV2MKE/cMJU/j5g62njJ49AfnLutmxkpz/gxX\nIqWe1m7Uxl/awVFa7F8N7TUkd0jLbAGXZaonDjiBxOTwg/jr/apnuQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1u36fr87cru90vfznf5szasya9nhvvseh4atk8zsdj0rrsu0nnvwqsyt8g6"
}
],
"lastmodified": "2026-07-03T10:47:04Z",
"mac": "ENC[AES256_GCM,data:wiIkwEDwGJZe0u8qLHthPkCXubeIGyHV8yBo8CsSengLVTOtRZ877zTrFvE74ATF1pGE3DFJ5qGRrOGgKJ4VOl043LbEBs0bxZC495pPMPYszimp2UAf/XE3QWlNi9p4Ce/kbvUDd0W3SRt4hIv6HOLWApiRoqfS2qSMUnBQeW0=,iv:Hs7EysUnje/iWZRhJLlqf8wuFJnIMpWfXWHrilfmM5I=,tag:Rgj9x4ZOM2fihKKNQ4ux3g==,type:str]",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../../../../sops/users/admin
@@ -0,0 +1 @@
8a354172629e4e8c6da86ec5e447411e4238cf7bb439b798fae56509a82d7472
@@ -0,0 +1 @@
200:3dcd:9b0d:103c:f953:1e8:e3a2:ed90
@@ -0,0 +1 @@
../../../../../../sops/machines/peer3
@@ -0,0 +1,18 @@
{
"data": "ENC[AES256_GCM,data:7no9ZnfatNLEykz+JOUExxmaRzBr7B5PEcevETySJWHTCCjSULdRb2KLxbXP5OKWSuJbR8pJ+p+nZuyr7aOTyXrU+fgwf/Pff4KGaZbKf1t3HJun8wJ2q62uaFYu8ACbNFfwIzJ51SPGcO7IHxfjWn5k9zG7av4=,iv:+VNvFKAJ+HDCGZciug8SKHOcM1JZQUAbZpRT5en4xcU=,tag:3kcDHWIs9ITzTKDiyyYhnQ==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvZ0VFcGhPR0NrMGU4dStU\nZGpnMHZmbUF0R0NFNWQzNGVpZXhYQ24xNXk0CnJIYmJYMnBmeURLZytBcXNRNG5u\nZzk4cXdPdnJIU0JVam1NWFA3TkhuWFkKLS0tIDZYUEVGMUtCQWdSM3k4WEZhdUt3\naElkc3o5SXlFYTJLNUpGZ0ptNm96M2MKYKW5emktK2M+IYX0v/swCV1fgrQXQVlb\nmz11rN5fJWV5wvLcTtyU9imgZLB9MfRMyIYlHzD2HPbW+zr8Fe+bBQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZVDVxY3JrWk1TY3ZsazNz\naVl4Vk1vRUdkRTdlaDU1TE15dC96ZHlxQ1ZBCmYvSmh1QlBNRnJyRy9kcDR0cXhY\neDdPUXZ6b2FkWnBXbzA4bzU1aFo3bEUKLS0tIHMrb05Oa3FrRzh5VHN1RWRmUU9W\ndGZUYTVVQkIzM3FRRHp6NXlEdEU0VTgK7ibTaEcvbVTQjcbgeylu2s5tmI5kAadM\nFGiOgJhDFuzbHxXIEQlsCBIaXe6LlGhBo41VZbMxme5py6nJFTBHKg==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1rka3368ltqc787xw99uu762lqfk7qxld9265u2t62fpxnp9xhufsc2q5rw"
}
],
"lastmodified": "2026-07-07T04:54:10Z",
"mac": "ENC[AES256_GCM,data:gonhuGY61G3Gdtj7q23aTNYU95fjLMz2uVTsSX+Hw10LGUTEMM8LF/Phae9kr5BZKD8BYaIgH32As2+n0B9jZ4WV77A5hUe2NJze9d0A40P2MtMh9xFWWh7yEXXnx5dy1UDrthL2LBa7ebPspuaC0mFhRHdoJK3J2W1m41PGjZc=,iv:H4yNuE8i3UxVjAUIpnaFX6i8/DaO4uIlHrEY9VaSiRs=,tag:9lJtixSNv7tpox0V3ZoNnA==,type:str]",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../../../../sops/users/admin
@@ -0,0 +1 @@
e119327977e183567f0b8e2e8937c9d04708e296beed57792a1b6935eacf69d9
+47 -8
View File
@@ -19,12 +19,6 @@
description = "Public URL for accessing the instance"; description = "Public URL for accessing the instance";
}; };
base_domain = lib.mkOption {
type = with lib.types; str;
default = "";
description = "Defines the base domain to create the hostnames for MagicDNS in Headscale. `base_domain` must be a FQDN, without the trailing dot. The FQDN of the hosts will be `hostname.base_domain (e.g. myhost.tailnet.example.com)";
};
advertise_routes = lib.mkOption { advertise_routes = lib.mkOption {
type = with lib.types; listOf str; type = with lib.types; listOf str;
default = [ ]; default = [ ];
@@ -32,6 +26,13 @@
example = [ "192.168.1.0/24" ]; example = [ "192.168.1.0/24" ];
}; };
dns = {
base_domain = lib.mkOption {
type = with lib.types; str;
default = "";
description = "Defines the base domain to create the hostnames for MagicDNS in Headscale. `base_domain` must be a FQDN, without the trailing dot. The FQDN of the hosts will be `hostname.base_domain (e.g. myhost.tailnet.example.com)";
};
nameservers = lib.mkOption { nameservers = lib.mkOption {
type = with lib.types; listOf str; type = with lib.types; listOf str;
default = [ default = [
@@ -41,6 +42,42 @@
description = "List of nameservers to pass to Tailscale clients"; description = "List of nameservers to pass to Tailscale clients";
example = [ "10.0.10.1" ]; example = [ "10.0.10.1" ];
}; };
magic_dns = lib.mkOption {
type = with lib.types; bool;
default = true;
description = "Whether to enable MagicDNS";
};
extra_records = lib.mkOption {
type =
with lib.types;
nullOr (
listOf (submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = "DNS record name.";
example = "grafana.tailnet.example.com";
};
type = lib.mkOption {
type = lib.types.enum [
"A"
"AAAA"
];
description = "DNS record type.";
example = "A";
};
value = lib.mkOption {
type = lib.types.str;
description = "DNS record value (IP address).";
example = "100.64.0.3";
};
};
})
);
};
};
}; };
}; };
@@ -170,9 +207,11 @@
settings.server_url = "https://${settings.public_url}"; settings.server_url = "https://${settings.public_url}";
settings.dns = { settings.dns = {
base_domain = settings.base_domain; base_domain = settings.dns.base_domain;
override_local_dns = true; override_local_dns = true;
nameservers.global = settings.nameservers; nameservers.global = settings.dns.nameservers;
magic_dns = settings.dns.magic_dns;
extra_records = settings.dns.extra_records;
}; };
}; };
+24 -3
View File
@@ -66,8 +66,6 @@
"AutofillAddressEnabled" = false; "AutofillAddressEnabled" = false;
"AutofillCreditCardEnabled" = false; "AutofillCreditCardEnabled" = false;
"TranslateEnabled" = false; "TranslateEnabled" = false;
"DnsOverHttpsMode" = "secure";
"DnsOverHttpsTemplates" = "https://dns.adguard-dns.com/dns-query";
}; };
}; };
@@ -81,14 +79,32 @@
inputs, inputs,
... ...
}: }:
let
dictionaries =
with pkgs;
(hunspellWithDicts (
with hunspellDicts;
[
en-us-large
th-th
]
));
in
{ {
imports = [ inputs.plasma-manager.homeModules.plasma-manager ]; imports = [ inputs.plasma-manager.homeModules.plasma-manager ];
systemd.user.sessionVariables = {
DICPATH = "${dictionaries}/share/hunspell";
};
home = { home = {
homeDirectory = lib.mkForce "/home/${username}"; homeDirectory = lib.mkForce "/home/${username}";
stateVersion = osConfig.system.stateVersion; stateVersion = osConfig.system.stateVersion;
packages = with pkgs; [ packages = with pkgs; [
libreoffice-fresh libreoffice-qt6
dictionaries
element-desktop element-desktop
signal-desktop signal-desktop
brave brave
@@ -100,6 +116,11 @@
]; ];
}; };
programs.chromium.package = pkgs.brave; programs.chromium.package = pkgs.brave;
programs.firefox = {
enable = true;
};
programs.home-manager.enable = true; programs.home-manager.enable = true;
services.syncthing.tray.enable = osConfig.services.syncthing.enable; services.syncthing.tray.enable = osConfig.services.syncthing.enable;
programs.plasma.enable = true; programs.plasma.enable = true;
@@ -11,7 +11,7 @@
homeDirectory = lib.mkForce "/home/${username}"; homeDirectory = lib.mkForce "/home/${username}";
stateVersion = osConfig.system.stateVersion; stateVersion = osConfig.system.stateVersion;
packages = with pkgs; [ packages = with pkgs; [
libreoffice-fresh libreoffice-stable
element-desktop element-desktop
signal-desktop signal-desktop
brave brave
+5
View File
@@ -0,0 +1,5 @@
{
boot.supportedFilesystems = {
ntfs = true;
};
}
@@ -5,5 +5,4 @@
services.displayManager.sddm.enable = lib.mkForce false; services.displayManager.sddm.enable = lib.mkForce false;
services.displayManager.gdm.enable = true; services.displayManager.gdm.enable = true;
services.displayManager.gdm.wayland = true;
} }
+45
View File
@@ -0,0 +1,45 @@
A peer to peer phone relay network built on top of yggdrasil.
Successor of the `phonebox` service with a global, decentralized number
directory. Every box is reachable from any yggdrasil node running this
service, not only from members of the same clan. A machine runs either
`phonebox` or `phonebox-global`, not both (they share the asterisk setup).
Regenerate vars with `clan vars generate --generator phonebox-global`.
## Numbers
Each box gets a random 6 digit number (100000-999999) when its vars are
generated. The number is signed with the box's yggdrasil private key, so a
number is cryptographically bound to the yggdrasil address it belongs to;
nobody can claim a number for an address they do not own. Regenerate the
`phonebox` vars to get a new number.
Dialing from a phone plugged into the ATA:
| dialed | reaches |
| ------------- | ------------------------------------------ |
| `NNNNNN` | the phone on box `NNNNNN` (line `00`) |
| `NNNNNNXX` | line `XX` on box `NNNNNN` |
| `00`, `01`... | local lines on this box |
| `888` | fax of the current number directory |
| `000` | fax echo test |
| `999` | hello world |
Caller ID on the callee is the caller's box number followed by its line, so
calling it back works.
## Directory
There is no central registry. Each box runs a serf agent on yggdrasil port
7946 that gossips its signed number record (`number`, `key`, `sig`, `owner`
tags) to every other box it knows about. On every membership change the
`phonebox-sync` handler verifies all records and writes the resulting
`number -> yggdrasil address` map to `/run/phonebox/numbers/`, which the
asterisk dialplan reads at call time.
Bootstrapping: boxes of the same clan join each other automatically. To
connect to boxes outside the clan add one of their yggdrasil addresses to
`extraPeers`; after the first successful join the agent remembers the whole
membership in `/var/lib/phonebox/serf.snapshot` and rejoins on its own.
`serf members -rpc-addr 127.0.0.1:7373` shows the live directory.
+583
View File
@@ -0,0 +1,583 @@
{
clanLib,
...
}:
{
_class = "clan.service";
manifest.name = "phonebox-global";
manifest.description = "A peer to peer phone relay network built on top of yggdrasil.";
manifest.readme = builtins.readFile ./README.md;
manifest.categories = [ "System" ];
roles.default = {
description = "a default server role";
interface =
{ lib, ... }:
{
options.ata-ethernet-iface = lib.mkOption {
type = lib.types.str;
description = "An Ethernet interface that connect to ATA box.";
default = "enp2s0";
};
options.extraClientNumbers = lib.mkOption {
type = with lib.types; listOf str;
description = "List of client suffix number.";
default = [ ];
};
options.extraFixedIPClient = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
ip = lib.mkOption {
type = lib.types.str;
description = "IP address for this client";
};
name = lib.mkOption {
type = lib.types.str;
description = "Name of the client";
};
};
}
);
description = "Extra client to be added to pjsip config as a fixed IP auth";
default = { };
example = {
"01" = {
ip = "192.168.1.3";
name = "bob";
};
};
};
options.extraPeers = lib.mkOption {
type = with lib.types; listOf str;
description = ''
Yggdrasil addresses of phonebox nodes outside this clan to bootstrap
the number directory from. Any single reachable node is enough; the
directory is gossiped from there and remembered across restarts.
'';
default = [ ];
example = [ "200:1234:5678:9abc:def0:1234:5678:9abc" ];
};
};
perInstance =
{
roles,
settings,
...
}:
{
nixosModule =
{
lib,
config,
pkgs,
...
}:
let
asterisk = pkgs.asterisk.overrideAttrs (old: {
propagatedNativeBuildInputs = [ pkgs.spandsp3 ];
});
machineName = config.clan.core.settings.machine.name;
machines = lib.attrNames roles.default.machines;
user = "asterisk";
faxDir = "/run/asterisk/fax";
rtpPortFrom = 10000;
rtpPortTo = 20000;
ata-interface = settings.ata-ethernet-iface;
# The ATA's own line. Remote callers reach it by dialing the 6
# digit box number alone or with this extension appended.
ataLine = "00";
sipPort = 5060;
# Well-known port of the phonebox directory gossip. Every phonebox
# node worldwide must agree on it, so it is not configurable.
directoryPort = 7946;
directoryRpc = "127.0.0.1:7373";
directoryUser = "phonebox";
directoryDir = "/run/phonebox";
phoneboxVars = config.clan.core.vars.generators.phonebox-global.files;
ownNumber = phoneboxVars.number.value;
ownerName = lib.trim phoneboxVars.owner-name.value;
getYggdrasilIP =
name:
lib.trim (
clanLib.getPublicValue {
flake = config.clan.core.settings.directory;
machine = name;
generator = "yggdrasil";
file = "address";
default = "";
}
);
ownYggdrasilIP =
if config.clan.core.vars.generators.yggdrasil.files ? address then
config.clan.core.vars.generators.yggdrasil.files.address.value
else
throw "clanService/yggdrasil is required";
seedPeers = lib.unique (
lib.filter (ip: ip != "") (map getYggdrasilIP (lib.remove machineName machines))
++ settings.extraPeers
);
# Rebuilds the number -> yggdrasil address directory from the
# gossip membership. Every record is verified: the yggdrasil
# address is derived from the record's public key, so a node can
# only publish numbers for the address it actually owns, and the
# number is signed with the matching private key.
phoneboxSync = pkgs.writers.writePython3Bin "phonebox-sync" {
libraries = [ pkgs.python3Packages.cryptography ];
flakeIgnore = [ "E501" ];
} (builtins.readFile ./phonebox-sync.py);
phoneboxSyncWrapped = pkgs.writeShellApplication {
name = "phonebox-sync";
runtimeInputs = [ pkgs.serfdom ];
text = ''
exec ${lib.getExe phoneboxSync} ${directoryRpc} ${directoryDir}
'';
};
createContactListTiff = pkgs.writeShellApplication {
name = "create-contact-tiff";
text = ''
magick -background white -fill black -pointsize 20 -font DejaVu-Sans label:"$(cat ${directoryDir}/contacts.txt)" "$1"
magick "$1" -border 20x50 -bordercolor white "$1"
magick "$1" -resize 1728x -units PixelsPerInch -compress Group4 -density 204x196 -monochrome -depth 1 "$1"
'';
runtimeInputs = [ pkgs.imagemagick ];
};
genLocalSIPEndpoint =
{ localNumber }:
''
[${localNumber}](internal_endpoint)
aors=${localNumber}
auth=${localNumber}
[${localNumber}](userpass_auth)
username=${localNumber}
password=${localNumber}
[${localNumber}](dynamic_aor)
max_contacts=1
remove_existing=yes
'';
genLocalSIPEndpointV6 =
{ localNumber }:
''
[${localNumber}](internal_endpoint)
transport=transport-udp6
aors=${localNumber}
auth=${localNumber}
[${localNumber}](userpass_auth)
username=${localNumber}
password=${localNumber}
[${localNumber}](dynamic_aor)
max_contacts=1
'';
genLocalSIPIPEndpoint = number: ''
[${number}](internal_endpoint)
aors=${number}
auth=${number}
contact_deny=0.0.0.0/0
contact_deny=::/0
contact_permit=${settings.extraFixedIPClient.${number}.ip}/128
[${number}](dynamic_aor)
max_contacts=1
remove_existing=yes
[${number}](userpass_auth)
username=${number}
password=${number}
'';
genLocalExtenConf =
{ localNumber }:
''
exten => ${localNumber},1,Dial(PJSIP/${localNumber},20)
'';
localNumbers = [
ataLine
]
++ settings.extraClientNumbers
++ lib.attrNames settings.extraFixedIPClient;
in
{
assertions = [
{
assertion = !config.networking.nftables.enable;
message = "phonebox-global: opening the yggdrasil interface to non-clan nodes is only implemented for the iptables firewall backend";
}
];
clan.core.vars.generators.phonebox-global = {
files = {
number.secret = false;
public-key.secret = false;
signature.secret = false;
owner-name.secret = false;
};
dependencies = [ "yggdrasil" ];
prompts.owner-name = {
persist = true;
type = "line";
description = "The owner's name for this unit";
};
runtimeInputs = with pkgs; [
coreutils
openssl
xxd
];
# A random 6 digit number (100000-999999), bound to this
# machine's yggdrasil identity by signing it with the yggdrasil
# private key. Other nodes accept the number only if the
# signature checks out and the key derives to the yggdrasil
# address the record is gossiped from. Regenerate to re-roll.
script = ''
cat $prompts/owner-name > $out/owner-name
shuf -i 100000-999999 -n 1 | tr -d '\n' > $out/number
openssl pkey -in $in/yggdrasil/privateKey -pubout -outform DER \
| tail -c 32 | xxd -p -c 64 | tr -d '\n' > $out/public-key
# openssl needs a regular file for one-shot ed25519 signing
printf 'phonebox:%s' "$(cat $out/number)" > $out/message
openssl pkeyutl -sign -rawin -inkey $in/yggdrasil/privateKey -in $out/message \
| base64 -w0 > $out/signature
rm $out/message
'';
};
networking.interfaces = {
${ata-interface} = {
useDHCP = false;
ipv4.addresses = [
{
address = "192.168.254.1";
prefixLength = 24;
}
];
};
};
services.dnsmasq = {
enable = true;
settings = {
bind-dynamic = true;
listen-address = "192.168.254.1";
# enable-ra = true;
domain-needed = true;
domain = "localhost";
dhcp-range = [
"192.168.254.100,192.168.254.100,255.255.255.0,3m"
];
dhcp-leasefile = "/dev/null";
dhcp-option = [
"3,192.168.254.1"
];
interface = [ ata-interface ];
};
};
services.nginx = {
enable = true;
virtualHosts = {
"_" = {
locations."/" = {
proxyPass = "http://192.168.254.100";
extraConfig = ''
client_max_body_size 100M;
'';
};
};
};
};
networking.firewall.allowedUDPPortRanges = [
{
from = rtpPortFrom;
to = rtpPortTo;
}
];
networking.firewall.allowedUDPPorts = [
53
67
sipPort
];
networking.firewall.allowedTCPPorts = [
53
];
networking.firewall.interfaces = {
"zt+".allowedTCPPorts = [ 80 ];
ygg = {
allowedTCPPorts = [ directoryPort ];
allowedUDPPorts = [ directoryPort ];
};
};
# clanService/yggdrasil drops everything on the ygg interface that
# does not come from a clan member. Phonebox is a global network,
# so let SIP, RTP and the directory gossip through from anyone.
networking.firewall.extraCommands = lib.mkAfter ''
if ip6tables -n -L ygg-input >/dev/null 2>&1; then
ip6tables -I ygg-input 1 -i ygg -p udp --dport ${toString sipPort} -j RETURN
ip6tables -I ygg-input 1 -i ygg -p udp --dport ${toString rtpPortFrom}:${toString rtpPortTo} -j RETURN
ip6tables -I ygg-input 1 -i ygg -p udp --dport ${toString directoryPort} -j RETURN
ip6tables -I ygg-input 1 -i ygg -p tcp --dport ${toString directoryPort} -j RETURN
fi
'';
users.users.${directoryUser} = {
isSystemUser = true;
group = directoryUser;
};
users.groups.${directoryUser} = { };
# Decentralized number directory: a serf gossip cluster over
# yggdrasil. Each node advertises its signed number record as
# tags; membership changes trigger phonebox-sync, which writes the
# verified number -> address map to ${directoryDir}/numbers/ for
# the dialplan. The snapshot lets a node rejoin from previously
# seen members, so seeds are only needed for the very first join.
systemd.services.phonebox-directory = {
description = "Phonebox number directory (serf gossip over yggdrasil)";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [
"network-online.target"
"yggdrasil.service"
];
serviceConfig = {
User = directoryUser;
Group = directoryUser;
RuntimeDirectory = "phonebox";
RuntimeDirectoryMode = "0755";
RuntimeDirectoryPreserve = "yes";
StateDirectory = "phonebox";
Restart = "always";
RestartSec = 5;
ExecStart = lib.concatStringsSep " " (
[
(lib.getExe pkgs.serfdom)
"agent"
"-node=${machineName}-${ownNumber}"
"-bind=[::]:${toString directoryPort}"
"-advertise=[${ownYggdrasilIP}]:${toString directoryPort}"
"-rpc-addr=${directoryRpc}"
"-profile=wan"
"-snapshot=/var/lib/phonebox/serf.snapshot"
"-rejoin"
"-retry-max=0"
"-retry-interval=30s"
"-tag number=${ownNumber}"
"-tag key=${phoneboxVars.public-key.value}"
"-tag sig=${phoneboxVars.signature.value}"
"-tag ${lib.escapeShellArg "owner=${ownerName}"}"
"-event-handler=member-join,member-leave,member-failed,member-update,member-reap=${lib.getExe phoneboxSyncWrapped}"
]
++ map (ip: "-retry-join=[${ip}]:${toString directoryPort}") seedPeers
);
};
};
services.asterisk = {
enable = lib.mkDefault true;
package = lib.mkDefault asterisk;
confFiles = {
"logger.conf" = ''
[general]
dateformat = %F %T.%3q ; ISO 8601 date format with milliseconds
use_callids = yes
appendhostname = no
queue_log = yes
queue_log_to_file = no
queue_log_name = queue_log
queue_log_realtime_use_gmt = no
rotatestrategy = rotate
exec_after_rotate=gzip -9 $\{filename\}.2
[logfiles]
console => notice,warning,error
security => security
messages => notice,warning,error
full => notice,warning,error,verbose,dtmf,fax
syslog.local0 => notice,warning,error
'';
"modules.conf" = ''
[modules]
autoload=yes
load => res_fax_spandsp.so
'';
# Dial plan config
"extensions.conf" = ''
[from-internal]
exten => 999,1,Answer()
same => n,Playback(hello-world)
same => n,Hangup()
exten => 000,1,Answer()
same => n,ReceiveFAX(${faxDir}/echo-''${UNIQUEID}.tiff)
same => n,Set(FAXFILE=${faxDir}/echo-''${UNIQUEID}.tiff)
same => n,Set(FAXECHO=true)
exten => 888,1,Answer()
same => n,Set(FAXFILE=${faxDir}/contact.tiff)
same => n,System(${lib.getExe createContactListTiff} ''${FAXFILE})
same => n,Set(FAXECHO=true)
same => n,Playback(vm-goodbye)
same => n,Wait(3)
exten => h,1,GotoIf($[''${FAXECHO}]?sendfax)
same => n,Hangup()
same => n(sendfax),Originate(PJSIP/${ataLine},app,SendFAX,''${FAXFILE})
same => n,Set(FAXECHO=false)
; 6 digit box number, optionally followed by a 2 digit line
exten => _[1-9]XXXXX,1,Goto(to-yggdrasil,''${EXTEN},1)
exten => _[1-9]XXXXXXX,1,Goto(to-yggdrasil,''${EXTEN},1)
''
+ lib.concatMapStrings (number: genLocalExtenConf { localNumber = number; }) localNumbers
+ ''
; Outbound: resolve the box number through the directory and
; send the call to that node over yggdrasil.
[to-yggdrasil]
exten => _[1-9]X.,1,Set(PEER=''${FILE(${directoryDir}/numbers/''${EXTEN:0:6})})
same => n,GotoIf($["''${PEER}" = ""]?unknown)
same => n,Set(CALLERID(num)=${ownNumber}''${CALLERID(num)})
same => n,Set(CALLERID(name)=${ownerName})
same => n,Dial(PJSIP/yggdrasil/sip:''${EXTEN}@[''${PEER}]:${toString sipPort},30)
same => n,Hangup()
same => n(unknown),Playback(ss-noservice)
same => n,Hangup()
; Inbound from any yggdrasil node: only our own number is served.
[from-yggdrasil]
exten => ${ownNumber},1,Dial(PJSIP/${ataLine},20)
exten => _${ownNumber}XX,1,Dial(PJSIP/''${EXTEN:6},20)
'';
"rtp.conf" = ''
[general]
rtpstart=${toString rtpPortFrom}
rtpend=${toString rtpPortTo}
'';
"pjsip.conf" = ''
[global]
type=global
; Local lines authenticate by username; everything else from
; the yggdrasil range is a remote phonebox node.
endpoint_identifier_order=username,ip,anonymous
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0
[transport-udp6]
type=transport
protocol=udp
bind=::
[base_endpoint](!)
type=endpoint
disallow=all
allow=ulaw,alaw,g722,gsm
direct_media=no
[internal_endpoint](!,base_endpoint)
context=from-internal
[userpass_auth](!)
type=auth
auth_type=userpass
[dynamic_aor](!)
type=aor
[yggdrasil](base_endpoint)
transport=transport-udp6
context=from-yggdrasil
[yggdrasil]
type=identify
endpoint=yggdrasil
match=200::/7
''
+ (genLocalSIPEndpoint { localNumber = ataLine; })
+ lib.concatMapStrings (
number: genLocalSIPEndpointV6 { localNumber = number; }
) settings.extraClientNumbers
+ lib.concatMapStrings genLocalSIPIPEndpoint (lib.attrNames settings.extraFixedIPClient);
};
};
environment.systemPackages = [
createContactListTiff
phoneboxSyncWrapped
];
systemd.tmpfiles.rules = [
"d ${faxDir} 0755 ${user} ${user} - -"
];
systemd.services.asterisk-watcher = {
enable = true;
description = "Asterisk Configuration files watcher";
requires = [ "asterisk.service" ];
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [
inotify-tools
asterisk
];
script = ''
inotifywait -m -e move /etc/asterisk |
while read path action file; do
case "$file" in
pjsip.conf)
echo "restarting pjsip"
asterisk -rx "pjsip reload"
;;
esac
case "$file" in
extensions.conf)
echo "restarting core"
asterisk -rx "core restart now"
;;
esac
done
'';
};
};
};
};
}
@@ -0,0 +1,39 @@
{
inputs,
self,
...
}:
let
module = ./default.nix;
in
{
clan.modules = {
phonebox-global = module;
};
perSystem =
{ pkgs, ... }:
{
clan.nixosTests.service-phonebox-global = {
imports = [ ./tests/vm/default.nix ];
_module.args = { inherit self inputs; };
clan.modules."@clan/phonebox-global" = module;
};
# Unit tests for the directory sync logic (record verification,
# address derivation, collision handling, file output).
checks.phonebox-global-sync =
pkgs.runCommand "phonebox-global-sync-test"
{
nativeBuildInputs = [
(pkgs.python3.withPackages (ps: [ ps.cryptography ]))
pkgs.yggdrasil
];
PHONEBOX_SYNC = ./phonebox-sync.py;
}
''
python3 ${./tests/sync/test_sync.py} -v
touch $out
'';
};
}
@@ -0,0 +1,111 @@
"""Rebuild the phonebox number directory from serf membership.
Usage: phonebox-sync <serf rpc addr> <state dir>
Runs as a serf event handler. Every alive member carries its number record
as tags (number, key, sig, owner). A record is accepted only when the
yggdrasil address it is gossiped from derives from `key` and `sig` is a
valid ed25519 signature of "phonebox:<number>" under that key, so a number
can only ever be bound to the address of the node that signed it.
Output, consumed by the asterisk dialplan:
<state dir>/numbers/<number> the yggdrasil address, no trailing newline
<state dir>/contacts.txt "<number> : <owner>" per line, for the fax
"""
import base64
import ipaddress
import json
import os
import subprocess
import sys
import syslog
import tempfile
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
def yggdrasil_address(public_key: bytes) -> ipaddress.IPv6Address:
"""Port of yggdrasil-go address.AddrForKey.
Invert the key; the address is 0x02, the count of leading one bits,
then the bits following the first zero, truncated to 128 bits.
"""
bits = "".join(f"{b ^ 0xFF:08b}" for b in public_key)
ones = len(bits) - len(bits.lstrip("1"))
body = int(bits[ones + 1:ones + 113], 2).to_bytes(14, "big")
return ipaddress.IPv6Address(bytes([0x02, ones]) + body)
def verified_record(member: dict) -> tuple[str, str, str, str]:
"""Return (number, key hex, address, owner) or raise ValueError."""
tags = member.get("tags", {})
try:
number, key_hex, sig = tags["number"], tags["key"], tags["sig"]
key = bytes.fromhex(key_hex)
host, _ = member["addr"].rsplit(":", 1)
address = ipaddress.IPv6Address(host.strip("[]"))
signature = base64.b64decode(sig, validate=True)
except (KeyError, ValueError) as e:
raise ValueError(f"malformed record: {e}")
if not (len(number) == 6 and number.isdigit() and number[0] != "0"):
raise ValueError(f"invalid number {number!r}")
if yggdrasil_address(key) != address:
raise ValueError(f"key does not derive to address {address}")
try:
Ed25519PublicKey.from_public_bytes(key).verify(signature, b"phonebox:" + number.encode())
except (ValueError, InvalidSignature):
raise ValueError(f"bad signature for number {number}")
return number, key_hex, str(address), tags.get("owner", "")
def directory(members: list[dict]) -> dict[str, tuple[str, str, str]]:
"""number -> (key hex, address, owner); on a collision the lowest key wins."""
book = {}
for member in members:
try:
number, key, address, owner = verified_record(member)
except ValueError as e:
syslog.syslog(syslog.LOG_WARNING, f"ignoring {member.get('name')}: {e}")
continue
if number in book:
syslog.syslog(syslog.LOG_WARNING, f"number {number} claimed by keys {book[number][0]} and {key}")
if book[number][0] <= key:
continue
book[number] = (key, address, owner)
return book
def write_atomic(path: str, content: str) -> None:
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path))
with os.fdopen(fd, "w") as f:
f.write(content)
os.chmod(tmp, 0o644)
os.replace(tmp, path)
def main() -> None:
rpc_addr, state_dir = sys.argv[1:]
syslog.openlog("phonebox-sync")
out = subprocess.run(
["serf", "members", "-format=json", "-status=alive", f"-rpc-addr={rpc_addr}"],
check=True, capture_output=True, text=True,
).stdout
book = directory(json.loads(out)["members"])
numbers_dir = os.path.join(state_dir, "numbers")
os.makedirs(numbers_dir, exist_ok=True)
for number, (_, address, _) in book.items():
write_atomic(os.path.join(numbers_dir, number), address)
for stale in set(os.listdir(numbers_dir)) - book.keys():
os.unlink(os.path.join(numbers_dir, stale))
write_atomic(
os.path.join(state_dir, "contacts.txt"),
"".join(f"{number}\t\t: \t\t{book[number][2]}\n" for number in sorted(book)),
)
syslog.syslog(syslog.LOG_INFO, f"directory has {len(book)} numbers")
if __name__ == "__main__":
main()
@@ -0,0 +1,164 @@
"""Tests for phonebox-sync: record verification and directory output.
Run via the `phonebox-global-sync` flake check; needs the `cryptography`
python package and the `yggdrasil` binary on PATH.
"""
import base64
import importlib.util
import ipaddress
import json
import os
import subprocess
import sys
import tempfile
import unittest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
spec = importlib.util.spec_from_file_location("phonebox_sync", os.environ["PHONEBOX_SYNC"])
sync = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sync)
PORT = 7946
class Node:
"""A phonebox node identity: yggdrasil key, address and signed number."""
def __init__(self, number: str, owner: str = ""):
self.key = Ed25519PrivateKey.generate()
self.public = self.key.public_key().public_bytes_raw()
self.address = str(sync.yggdrasil_address(self.public))
self.number = number
self.owner = owner
def member(self, name="node", **overrides) -> dict:
tags = {
"number": self.number,
"key": self.public.hex(),
"sig": base64.b64encode(self.key.sign(b"phonebox:" + self.number.encode())).decode(),
"owner": self.owner,
}
member = {"name": name, "addr": f"[{self.address}]:{PORT}", "port": PORT, "status": "alive", "tags": tags}
for k, v in overrides.items():
(tags if k in tags else member)[k] = v
return member
def yggdrasil_binary_address(key: Ed25519PrivateKey) -> str:
private = key.private_bytes_raw() + key.public_key().public_bytes_raw()
out = subprocess.run(
["yggdrasil", "-useconf", "-address"],
input=json.dumps({"PrivateKey": private.hex()}),
check=True, capture_output=True, text=True,
)
return out.stdout.strip()
class AddressDerivation(unittest.TestCase):
def test_known_vector(self):
# A real clan machine key and the address yggdrasil assigned to it.
key = bytes.fromhex("0ec53986a2bdca8a43f74063aeab6a958fc697c528c83e7281365072926886f0")
self.assertEqual(str(sync.yggdrasil_address(key)), "204:2758:cf2b:a846:aeb7:8117:f38a:2a92")
def test_matches_yggdrasil_binary(self):
for _ in range(16):
key = Ed25519PrivateKey.generate()
expected = ipaddress.IPv6Address(yggdrasil_binary_address(key))
self.assertEqual(sync.yggdrasil_address(key.public_key().public_bytes_raw()), expected)
class RecordVerification(unittest.TestCase):
def setUp(self):
self.node = Node("482913", owner="alice")
def test_valid_record(self):
number, key, address, owner = sync.verified_record(self.node.member())
self.assertEqual((number, key, address, owner), ("482913", self.node.public.hex(), self.node.address, "alice"))
def test_record_from_foreign_address_is_rejected(self):
other = Node("111111")
with self.assertRaisesRegex(ValueError, "does not derive"):
sync.verified_record(self.node.member(addr=f"[{other.address}]:{PORT}"))
def test_signature_over_other_number_is_rejected(self):
with self.assertRaisesRegex(ValueError, "bad signature"):
sync.verified_record(self.node.member(number="999999"))
def test_garbage_signature_is_rejected(self):
with self.assertRaisesRegex(ValueError, "malformed"):
sync.verified_record(self.node.member(sig="not base64!"))
def test_missing_tags_are_rejected(self):
with self.assertRaisesRegex(ValueError, "malformed"):
sync.verified_record({"name": "foreign", "addr": f"[{self.node.address}]:{PORT}", "tags": {}})
def test_number_must_be_six_digits_without_leading_zero(self):
for bad in ["012345", "12345", "1234567", "12a456", ""]:
node = Node(bad)
with self.assertRaisesRegex(ValueError, "invalid number", msg=bad):
sync.verified_record(node.member())
def test_owner_defaults_to_empty(self):
member = self.node.member()
del member["tags"]["owner"]
self.assertEqual(sync.verified_record(member)[3], "")
class Directory(unittest.TestCase):
def test_invalid_records_are_skipped_not_fatal(self):
good = Node("482913", owner="alice")
bad = Node("555555")
members = [bad.member(sig="AAAA"), good.member(), {"name": "x", "addr": "[200::1]:7946", "tags": {}}]
self.assertEqual(sync.directory(members), {"482913": (good.public.hex(), good.address, "alice")})
def test_collision_lowest_key_wins_regardless_of_order(self):
a, b = Node("482913"), Node("482913")
winner = min((a, b), key=lambda n: n.public.hex())
for members in ([a.member("a"), b.member("b")], [b.member("b"), a.member("a")]):
self.assertEqual(sync.directory(members)["482913"][1], winner.address)
class Main(unittest.TestCase):
def run_sync(self, members: list[dict], state: str) -> None:
bindir = tempfile.mkdtemp()
with open(os.path.join(bindir, "serf"), "w") as f:
f.write("#!/bin/sh\ncat <<'EOF'\n" + json.dumps({"members": members}) + "\nEOF\n")
os.chmod(os.path.join(bindir, "serf"), 0o755)
env_path = os.environ["PATH"]
os.environ["PATH"] = bindir + os.pathsep + env_path
argv = sys.argv
sys.argv = ["phonebox-sync", "127.0.0.1:7373", state]
try:
sync.main()
finally:
sys.argv = argv
os.environ["PATH"] = env_path
def test_writes_numbers_and_contacts_and_removes_stale(self):
state = tempfile.mkdtemp()
os.makedirs(os.path.join(state, "numbers"))
with open(os.path.join(state, "numbers", "111111"), "w") as f:
f.write("200::dead")
a, b = Node("482913", owner="alice"), Node("555555", owner="bob")
self.run_sync([b.member("b"), a.member("a")], state)
numbers = os.path.join(state, "numbers")
self.assertEqual(sorted(os.listdir(numbers)), ["482913", "555555"])
with open(os.path.join(numbers, "482913")) as f:
self.assertEqual(f.read(), a.address) # no trailing newline: read by FILE() in the dialplan
with open(os.path.join(state, "contacts.txt")) as f:
self.assertEqual(f.read(), "482913\t\t: \t\talice\n555555\t\t: \t\tbob\n")
def test_empty_membership_clears_directory(self):
state = tempfile.mkdtemp()
self.run_sync([Node("482913").member()], state)
self.run_sync([], state)
self.assertEqual(os.listdir(os.path.join(state, "numbers")), [])
with open(os.path.join(state, "contacts.txt")) as f:
self.assertEqual(f.read(), "")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,77 @@
{
self,
hostPkgs,
config,
lib,
...
}:
{
name = "service-phonebox-global";
result.update-vars =
let
relativeDir = lib.removePrefix "${self}/" (toString config.clan.directory);
in
hostPkgs.writeShellScriptBin "update-vars" ''
set -x
export PRJ_ROOT=$(git rev-parse --show-toplevel)
${
self.inputs.clan-core.packages.${hostPkgs.system}.clan-cli
}/bin/clan-generate-test-vars $PRJ_ROOT/${relativeDir} ${config.name}
'';
clan = {
test.useContainers = false;
directory = ./.;
inventory = {
machines.server = { };
machines.nodeA = { };
instances = {
yggdrasil = {
module.name = "yggdrasil";
roles.default.machines.server = { };
roles.default.machines.nodeA = { };
};
phonebox-global-test = {
module.name = "@clan/phonebox-global";
module.input = "self";
roles.default.machines.server.settings.ata-ethernet-iface = "enp2s0";
roles.default.machines.nodeA.settings.ata-ethernet-iface = "enp2s0";
};
};
};
};
nodes = {
server = { };
nodeA = { };
};
testScript =
{ nodes, ... }:
let
number = node: nodes.${node}.clan.core.vars.generators.phonebox-global.files.number.value;
address = node: nodes.${node}.clan.core.vars.generators.yggdrasil.files.address.value;
in
''
start_all()
for node in [server, nodeA]:
node.wait_for_unit("asterisk.service")
node.wait_for_unit("phonebox-directory.service")
# Directory converges: each node learns the other's number and address.
server.wait_until_succeeds("test -f /run/phonebox/numbers/${number "nodeA"}", timeout=300)
nodeA.wait_until_succeeds("test -f /run/phonebox/numbers/${number "server"}", timeout=300)
assert server.succeed("cat /run/phonebox/numbers/${number "nodeA"}") == "${address "nodeA"}"
assert nodeA.succeed("cat /run/phonebox/numbers/${number "server"}") == "${address "server"}"
assert "${number "nodeA"}" in server.succeed("cat /run/phonebox/contacts.txt")
# A call from server to nodeA's number is routed over yggdrasil and
# accepted by nodeA's asterisk (no phone is registered, so it fails
# after being identified, which is enough to prove the path).
nodeA.succeed("asterisk -rx 'pjsip set logger on'")
server.succeed("asterisk -rx 'channel originate Local/${number "nodeA"}@from-internal application Wait 3'")
nodeA.wait_until_succeeds("grep -q 'INVITE sip:${number "nodeA"}@' /var/log/asterisk/full", timeout=60)
'';
}
@@ -0,0 +1,6 @@
[
{
"publickey": "age18yf7r0dalnue5vwzanxl046fxsskhu9lzvd488ajhz7rp62mu94scmw3w4",
"type": "age"
}
]
@@ -0,0 +1,6 @@
[
{
"publickey": "age1sy8xkmssttcchjj0nkapk4tu530lrzszdp4lka6nn4gzlxam6e2s3xzp2j",
"type": "age"
}
]
@@ -0,0 +1,14 @@
{
"data": "ENC[AES256_GCM,data:NkOeX6FboG7NOkdgiTgAsS+XjRoDU+AEYfMhpZXIJTYEM2dQti+PMGyizKRvr1wRnSGMp56XaX0bHdToBuf1gYruqAsEkKwqsYw=,iv:RFyO5/JybroNAKe+F3vv51l4OEu5XXNs+biTTH8poEg=,tag:zfPm0ZAjT2CC734EU+36KQ==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2RklrTFQxMll5VUxiaGVu\nNlRzeldTRnVrSG93TXYzc1YrRE9Nc1VqTHpNCjh4aHJMZmtpM2tDclNaT3d2SDhN\na1QxTXVFOHRuY3dpL1YxMTJKK3pUVmMKLS0tIHoyMGVyc01vak5EKzZONC9XUmFw\nMU5MRmx6UDROQzFiaTNuNXFiQTZoYmcK6wxypuP7vTnz//EcEt2pUNqUuKxjOaY3\nAmKToJQADJfzTsYGzpmWkkYaMHvG00v8Ja6TF6IxZr5SxUh4bdHr+w==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg"
}
],
"lastmodified": "2026-09-18T02:02:10Z",
"mac": "ENC[AES256_GCM,data:kRpbcT5+I80iHPjPUnveQE9jpa65n0xPPQR/by4p1iM80ZKk9KH0cObB5hHhXO4BD1OqI79UJ3O9O7Jhmylgx5Uh8RNbK6wf26p4GO2Bvqh9r15NqbCZtXkoW30MTLLIs58Q7NeK9O2LgeckWrXLtyEytfhnTuoTSDFo8Sh8NQY=,iv:rcNow1Z4BN7bo1XERB6c+G6Gwl3tRU1VHvYmoDaLTl4=,tag:vy/5EB/1gw/KLLqXHSEV3g==,type:str]",
"version": "3.13.3"
}
}
@@ -0,0 +1 @@
../../../users/admin
@@ -0,0 +1,14 @@
{
"data": "ENC[AES256_GCM,data:Sy9T0zEgSGZ/qkz5kaRN2qlH2X+pFRibzb8FpWuknuSI/9lActSG2Loq4LhmhqPGC5MGVkQcz2pcVFI7afUjzDzejfJf4De1ka4=,iv:JNLXguWEwJXR2+WNl0v0nUvkESVAbccfeQSUijKbj2I=,tag:/AAFKncO+B/XmUZYTQnTTw==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqOXJoSXgvcEFXK2hWTlpi\nQWhCdnZPUCt5R2xEWFYxUnpWVlc1ei93TncwCjNKaVB0MFN1VUdheEwyUTBzcmFT\nb0RjTHp2MGE4cWIraCtZME9rQXAvVEkKLS0tIFpiZDhCN0NIY3FWTnlXaFhiMmtC\nTFJMUncvMnFRSEhNdHlhaWozcjlqYWsKLDH1VFG/QS3VIMn6Iwo2NmY5kdBiOPNR\nj4sOY6xBpWP5AiGUypGoX4Bm52gLgW5AdcBRIxxhFOekIv5G8wmRaw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg"
}
],
"lastmodified": "2026-09-18T02:02:23Z",
"mac": "ENC[AES256_GCM,data:DN97Dcpy29IZkBbwFVnMxnUnWYrgLKuvbANabkMCdpOJYgI1msYODltHJlR68Lk8Fmg25sXysvdbJOnc2cMH3pgcKxeyfhdAFwlIXdie1HRtTpZql15RF0fwPBGJjwL9YYubpZU7K4cNflGbjuOCEhp8bQOUYY8Ptd2deiDGGuw=,iv:bxTcKLUu6fcz7JKzStKiakgBeNx7aWCw6M7HolthfEo=,tag:/0nWBkdCPkpcWSpcfY/TfA==,type:str]",
"version": "3.13.3"
}
}
@@ -0,0 +1 @@
../../../users/admin
@@ -0,0 +1,4 @@
{
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"type": "age"
}
@@ -0,0 +1 @@
cf28bbcd822efec197dfaa99fbe9004f78b4efa9724b4f453d05f12a61a0bb44
@@ -0,0 +1 @@
CrnzGbqfVORt00zeI99crpBRUMfGF+2uEfaHe1L3Y8k7CIojc+8TID40HPhwEgXNtC9/xr61mN5L0Vp4vpviBA==
@@ -0,0 +1 @@
200:61ae:8864:fba2:27c:d040:aacc:82d
@@ -0,0 +1 @@
../../../../../../sops/machines/nodeA
@@ -0,0 +1,18 @@
{
"data": "ENC[AES256_GCM,data:hR33SvxgsU+hZnvF2hoQxE7n0IB6kLC9MgK0wS0NXpCht5NuPjTo5p51oUE8t95Dht2qlEdXAkGO39k/h42p54vhqcfuF7u/eqVMdX6pEJyOokFmsdDmdVZziyhZghrTI/nDlctxCPcqFkeCHU4SIBZs85NS8uk=,iv:Q8k8OXSS3lFw33UO8JiZYNkrH3s+Cvh0xnAbMiBmnNA=,tag:DtNYKgDYB2nSHIjvMdvu/w==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTSzk1RUhIekVJRStQd3Er\nbXlHUmtVb201dkRteVR1VFpoMTVFcTFPSG5NCmlPcVM5cWVJQ0g0VHNXaS9rTzh2\nWTIyQnRXakVCRWRQL0xuVUdDOEV0WEUKLS0tIE43RENWL1M2STF1VzdMcHJTNThh\ndHpqNE9MMnkrMlIxN2FEcjZ6anRIbDAK3rZ1lzO42bH37lb+zoeF6rKoVgI0TBST\n9EoVwom5xOKtmVAh5jobY/z4TGSD1BqQ6P2rQ/IM1Dtw3/18Yk7TXQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age18yf7r0dalnue5vwzanxl046fxsskhu9lzvd488ajhz7rp62mu94scmw3w4"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkdDFsSTZHUG54ZWNtQVdj\nZno4enBaRVlaQU1wNTBEVDVscS9Jc1JaTXhjClA3YUNQa3B2L0JXY25SSHRGbS94\nNVVNdHZ5VzdXcWpXZG1ZS1U4T0NPUkEKLS0tIFJLVmZyazU4dWUyVnpvRDRWbHNn\nZFQ3dlhxN0lwc0loWEZIOE0yMElRV0EKeUVy2QpLHbJ8JUCl07f74V+ZnRkGfDdP\n5W28yiLuq+LYfKUaFYeRDvSVFCQo6FiYJcBPGmnPF5gg1BIUtUUBYw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg"
}
],
"lastmodified": "2026-09-18T02:02:10Z",
"mac": "ENC[AES256_GCM,data:Oq1UCi+VYuuAkMmVBife740m6fiXC+PfkaQAii+FORyv8Zpj938cPrZzE9z/LO5Ixye5cbUHGRhZr0vM1egnv9nhIFyfGinKyE/BLEOXHxOtHnAXSQOJU2bWiR6w/QzVCTy6vTTVnqD2AtEfbKndIK1PJ0ASLvxMGaRqRsA/juE=,iv:xCEQg6DHV6hbyfBk62bUKA1lnC3HgQtkn2o8i2CFjhs=,tag:Zvt9tomYGoDFDQGjLZC59A==,type:str]",
"version": "3.13.3"
}
}
@@ -0,0 +1 @@
../../../../../../sops/users/admin
@@ -0,0 +1 @@
cf28bbcd822efec197dfaa99fbe9004f78b4efa9724b4f453d05f12a61a0bb44
@@ -0,0 +1 @@
7291692f9a94948724377fc22055edde8e2773a581153317bc20bd5779736647
@@ -0,0 +1 @@
IwAOYcmClqi8K8EStthYMqWCr6WsxM3nD0CLDRvvb7I3CUyihXBjdghXLutDD/bFsHKzOkDVV/gLnTuRj9IEAg==
@@ -0,0 +1 @@
201:35ba:5b41:95ad:ade3:6f22:f7:7ea8
@@ -0,0 +1 @@
../../../../../../sops/machines/server
@@ -0,0 +1,18 @@
{
"data": "ENC[AES256_GCM,data:H1ngxnRS+cFNSxe9K7C/xNS5hzXyeTWBLZI18TPvEyT5l/MSBeRuK6FZ/G0xUGQAJw4Xo1nyX2PZEYH5wjhnG0UziKAZeM2Oe0G/ilpNbHeMS+HLVriDETlMQz2Hf4Y1J6jOZqSTgFuPMVG5armpZUYTcW+p2zo=,iv:u9sO6eiFzxtT7uImnHLXT3aYJJxgby8d5H2ckfZdNIw=,tag:ZLl7AHHRF+jmr0tnujrpIg==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzdy9HVVFoa0N5ZnBDVDhM\ncm1vQzR3MUpSbW9KSjE0YmZueUlsY1gvUVc4CmlqaTBxMTNvZzdmdkJSMmMwN01N\nVGsvUzJ5Q055Tmk2L2ZUNjRpSHp0bE0KLS0tIGJhUUNDbGh1a09aeXMzWkl4K0F0\nZ0dYY1FnNCtkVXRVTVkyeXZPbFlVbUUK3a/V6XP6m8MDYPKwlu8z0cr4inuKGGNc\nYJjQw00ou3BdM/HOPZrMjYHtjg5WdtzYiFCOl8IRkeiDaEmXcOBjiw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSbW9lcXZweHZ6b0F1eVIy\nd2RWV2lkOHpoM2RkZWxSVEFwU1h5aXNMWldnCk5lS1E4b1BDMmRYUDNHK2NhTlVT\nWGpLWm4rUnpFVWNvMHNzclJuemNGZkEKLS0tIGZxcmowODUydm1kL24yVHd4YXl4\nWVd5eWZMZ0dyS2dpTDhuNXo4S25Ha0EKvc3hwrmlP5JcH7eQeRfoq40w/QdnQL8s\n+DAwsajzRp6H3/XVTm+nDQ+Qoc3aenewZkk6Pgn+x43hAh9zwuvGkA==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1sy8xkmssttcchjj0nkapk4tu530lrzszdp4lka6nn4gzlxam6e2s3xzp2j"
}
],
"lastmodified": "2026-09-18T02:02:24Z",
"mac": "ENC[AES256_GCM,data:xrshyxQAnlSORTzxxcpt0ABaNWSCCVnwHl6oPUQ59reumF7BoAy8HmdSyWeEpIKBNo2Ane1xxXBHa62+w58r0WjcneNzy8yIBBwXr1BIRZPX83HrSz3+bENPtj6TPWdHpFZ5aDFM2jVlUvwIkn/4g+1B9FnQtoH8kuYBGe2uGCM=,iv:slMt0ag3Kp3iD89jQa/YDta4mJccKiSqcTCt8C+1Dzs=,tag:o56eg/BSHOtBUeNQqRjQDg==,type:str]",
"version": "3.13.3"
}
}
@@ -0,0 +1 @@
../../../../../../sops/users/admin
@@ -0,0 +1 @@
7291692f9a94948724377fc22055edde8e2773a581153317bc20bd5779736647
+79
View File
@@ -24,6 +24,36 @@
description = ""; description = "";
default = ""; default = "";
}; };
options.extraClientNumbers = lib.mkOption {
type = with lib.types; listOf str;
description = "List of client suffix number.";
default = [ ];
};
options.extraFixedIPClient = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
ip = lib.mkOption {
type = lib.types.str;
description = "IP address for this client";
};
name = lib.mkOption {
type = lib.types.str;
description = "Name of the client";
};
};
}
);
description = "Extra client to be added to pjsip config as a fixed IP auth";
example = {
"01" = {
ip = "192.168.1.3";
name = "bob";
};
};
};
}; };
perInstance = perInstance =
{ {
@@ -126,6 +156,41 @@
remove_existing=yes remove_existing=yes
''; '';
genLocalSIPEndpointV6 =
{ localNumber }:
''
[${localNumber}](internal_endpoint)
transport=transport-udp6
aors=${localNumber}
auth=${localNumber}
[${localNumber}](userpass_auth)
username=${localNumber}
password=${localNumber}
[${localNumber}](dynamiic_aor)
max_contacts=1
'';
genLocalSIPIPEndpoint = number: ''
[${number}](internal_endpoint)
aors=${number}
auth=${number}
contact_deny=0.0.0.0/0
contact_deny=::/0
contact_permit=${settings.extraFixedIPClient.${number}.ip}/128
[${number}](dynamiic_aor)
max_contacts=1
remove_existing=yes
[${number}](userpass_auth)
username=${number}
password=${number}
'';
genLocalExtenConf = genLocalExtenConf =
{ localNumber }: { localNumber }:
'' ''
@@ -356,6 +421,14 @@
+ (genLocalExtenConf { + (genLocalExtenConf {
localNumber = config.clan.core.vars.generators.phonebox.files.ata-local-number.value; localNumber = config.clan.core.vars.generators.phonebox.files.ata-local-number.value;
}) })
+ lib.concatStringsSep "\n" (
builtins.map (number: genLocalExtenConf { localNumber = number; }) settings.extraClientNumbers
)
+ lib.concatStringsSep "\n" (
lib.mapAttrsToList (
number: _: genLocalExtenConf { localNumber = number; }
) settings.extraFixedIPClient
)
+ serverConf; + serverConf;
"rtp.conf" = '' "rtp.conf" = ''
@@ -409,6 +482,12 @@
+ (genLocalSIPEndpoint { + (genLocalSIPEndpoint {
localNumber = config.clan.core.vars.generators.phonebox.files.ata-local-number.value; localNumber = config.clan.core.vars.generators.phonebox.files.ata-local-number.value;
}) })
+ lib.concatStringsSep "\n" (
builtins.map (number: genLocalSIPEndpointV6 { localNumber = number; }) settings.extraClientNumbers
)
+ lib.concatStringsSep "\n" (
lib.mapAttrsToList (number: _: genLocalSIPIPEndpoint number) settings.extraFixedIPClient
)
+ serverConf; + serverConf;
}; };
}; };
View File
+308
View File
@@ -0,0 +1,308 @@
{ clanLib, ... }:
{
_class = "clan.service";
manifest.name = "prometheus";
manifest.description = "The Prometheus monitoring system and time series database.";
manifest.readme = builtins.readFile ./README.md;
manifest.categories = [ "System" ];
roles.server = {
description = "Prometheus server that scraps all data from nodes";
interface =
{ lib, ... }:
{
options = {
scrape_interval = lib.mkOption {
type = with lib.types; nullOr str;
default = "1m";
description = "How often to scrape targets. Default is 1 minutes";
};
extra_rules = lib.mkOption {
type = with lib.types; listOf attrs;
default = [ ];
description = "Additional rules for Prometheus";
};
default_receiver = lib.mkOption {
type = with lib.types; attrs;
default = {
name = "default";
};
description = "Definition of a default receiver, default is doing nothing";
};
matrix-alertmanager = {
enable = lib.mkOption {
type = with lib.types; bool;
default = false;
description = "Whether to enable `services.matrix-alertmanager`";
};
homeserverUrl = lib.mkOption {
type = with lib.types; str;
default = "https://matrix-client.matrix.org";
description = "URL of the Matrix homeserver to use";
};
matrixUser = lib.mkOption {
type = with lib.types; str;
description = "Matrix user for the bot";
};
matrixRooms = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
receivers = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of receivers for this room";
};
roomId = lib.mkOption {
type = lib.types.str;
description = "Matrix room ID";
apply =
x:
assert lib.assertMsg (lib.hasPrefix "!" x) "Matrix room ID must start with a '!'. Got: ${x}";
x;
};
};
}
);
description = ''
Combination of Alertmanager receiver(s) and rooms for the bot to join.
Each Alertmanager receiver can be mapped to post to a matrix room.
Note, you must use a room ID and not a room alias/name. Room IDs start
with a "!".
'';
example = [
{
receivers = [
"receiver1"
"receiver2"
];
roomId = "!roomid@example.com";
}
{
receivers = [ "receiver3" ];
roomId = "!differentroomid@example.com";
}
];
};
};
};
};
perInstance =
{
settings,
roles,
...
}:
{
nixosModule =
{
config,
lib,
pkgs,
...
}:
let
getYggdrasilIP =
machineName:
if config.clan.core.vars.generators.yggdrasil.files.address ? value then
clanLib.getPublicValue {
flake = config.clan.core.settings.directory;
machine = machineName;
generator = "yggdrasil";
file = "address";
default = null;
}
else
throw "clanService/yggdrasil is required";
matrixRoomReceivers = lib.unique (
lib.concatMap (entry: entry.receivers) settings.matrix-alertmanager.matrixRooms
);
in
lib.mkMerge [
{
networking.firewall.allowedTCPPorts = [
9090
];
services.prometheus = {
enable = true;
globalConfig = {
scrape_interval = settings.scrape_interval;
};
alertmanagers = [
{
scheme = "http";
path_prefix = "/";
static_configs = [ { targets = [ "localhost:9093" ]; } ];
}
];
alertmanager = {
enable = true;
configuration = {
global = {
resolve_timeout = "5m";
};
route = {
receiver = "default";
routes = map (mReceiver: { receiver = mReceiver; }) matrixRoomReceivers;
};
receivers = [
{ name = "default"; }
]
++ map (mReceiver: {
name = mReceiver;
webhook_configs = [
{
url_file = config.clan.core.vars.generators.prometheus.files.matrix-alertmanager-urlfile.path;
send_resolved = true;
}
];
}) matrixRoomReceivers;
};
};
scrapeConfigs = lib.mapAttrsToList (machineName: machineVal: {
tls_config.insecure_skip_verify = true;
job_name = "${machineName}";
static_configs = lib.mapAttrsToList (
exporterName: exporterVal:
let
targetPort =
if exporterVal ? port then
exporterVal.port
else
config.services.prometheus.exporters."${exporterName}".port;
targetHost = getYggdrasilIP machineName;
in
{
targets = [ "[${targetHost}]:${lib.toString targetPort}" ];
}
) machineVal.settings.exporters;
}) roles.nodes.machines;
rules = [
(builtins.toJSON {
groups = [
{
name = "default";
rules = [
{
alert = "NodesDown";
expr = "count by (job) (up == 0) > 0";
for = "1m";
labels = {
severity = "critical";
};
annotations.summary = "Node **{{ $labels.job }}** has been down for more than 1 minutes.";
}
{
alert = "SmartCtlErrors";
expr = "smartctl_device_error_log_count > 0";
for = "5m";
labels = {
severity = "critical";
};
annotations.summary = ''
Errors occur on **{{ $labels.job }}**
Disk {{ $labels.device }} {{ $value }}
'';
}
{
alert = "ZFSPoolsHealth";
expr = "zfs_pool_health > 0";
for = "5m";
labels = {
severity = "critical";
};
annotations.summary = ''
Unhealthy Pool at **{{ $labels.job }}**
Pool {{ $labels.pool }} value {{ $value }}
'';
}
]
++ settings.extra_rules;
}
];
})
];
};
}
(lib.optionalAttrs settings.matrix-alertmanager.enable {
clan.core.vars.generators.prometheus = {
files.matrix-alertmanager-token.secret = true;
files.matrix-alertmanager-secret.secret = true;
files.matrix-alertmanager-urlfile = {
secret = true;
owner = "alertmanager";
group = "alertmanager";
};
script = ''
echo "" > $out/matrix-alertmanager-token
openssl rand -hex 32 > "$out"/matrix-alertmanager-secret
echo "http://localhost:3000/alerts?secret=$(cat $out/matrix-alertmanager-secret)" > $out/matrix-alertmanager-urlfile
'';
runtimeInputs = [
pkgs.openssl
];
};
services.matrix-alertmanager = lib.mkIf settings.matrix-alertmanager.enable {
enable = true;
tokenFile = config.clan.core.vars.generators.prometheus.files.matrix-alertmanager-token.path;
secretFile = config.clan.core.vars.generators.prometheus.files.matrix-alertmanager-secret.path;
homeserverUrl = settings.matrix-alertmanager.homeserverUrl;
matrixUser = settings.matrix-alertmanager.matrixUser;
matrixRooms = settings.matrix-alertmanager.matrixRooms;
};
})
];
};
};
roles.nodes = {
description = "A node will expose metrics for server to harvest";
interface =
{ lib, ... }:
{
options = {
exporters = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule { });
default = { };
description = "Mirror of services.prometheus.exporters";
};
};
};
perInstance =
{ settings, ... }:
let
enabledExporters = builtins.mapAttrs (
name: value:
value
// {
enable = true;
openFirewall = true;
}
) settings.exporters;
in
{
nixosModule =
{ ... }:
{
services.prometheus.exporters = enabledExporters;
};
};
};
}
+19
View File
@@ -0,0 +1,19 @@
{ self, inputs, ... }:
let
module = ./default.nix;
in
{
clan.modules = {
prometheus = module;
};
perSystem =
{ ... }:
{
clan.nixosTests.service-prometheus = {
imports = [ ./tests/vm/default.nix ];
_module.args = { inherit self inputs; };
clan.modules."@clan/prometheus" = module;
};
};
}
@@ -0,0 +1,101 @@
{
self,
hostPkgs,
config,
lib,
...
}:
{
name = "service-prometheus";
result.update-vars =
let
relativeDir = lib.removePrefix "${self}/" (toString config.clan.directory);
in
hostPkgs.writeShellScriptBin "update-vars" ''
set -x
export PRJ_ROOT=$(git rev-parse --show-toplevel)
${
self.inputs.clan-core.packages.${hostPkgs.system}.clan-cli
}/bin/clan-generate-test-vars $PRJ_ROOT/${relativeDir} ${config.name}
'';
clan = {
test.useContainers = false;
directory = ./.;
inventory = {
machines.server = { };
machines.nodeA = { };
instances = {
yggdrasil = {
module.name = "yggdrasil";
roles.default.machines.server = { };
roles.default.machines.nodeA = { };
};
prometheus = {
module.name = "@clan/prometheus";
module.input = "self";
roles.nodes.machines."nodeA".settings = {
exporters.smartctl = { };
};
roles.server.machines."server".settings = {
extra_rules = [
{
alert = "test";
expr = "zfs_pool_health > 0";
for = "5m";
labels = {
severity = "critical";
};
annotations.summary = ''
Unhealthy Pool at {{ $labels.job }}
Pool {{ $labels.pool }} value {{ $value }}
'';
}
];
matrix-alertmanager = {
enable = true;
matrixUser = "test@matrixtest.org";
matrixRooms = [
{
roomId = "!testroom";
receivers = [ "matrix" ];
}
];
};
};
};
};
};
};
nodes = {
server = { };
nodeA = { };
};
testScript =
{ nodes, ... }:
''
start_all()
server.wait_for_unit("prometheus.service")
nodeA.wait_for_unit("prometheus-smartctl-exporter.service")
nodeA.wait_for_open_port(9633)
nodeA.succeed("systemctl status prometheus-smartctl-exporter.service")
nodeA.succeed("curl http://localhost:9633/metrics")
server_ip = server.succeed("ip -4 addr show eth1 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'").strip()
nodeA_ip = nodeA.succeed("ip -4 addr show eth1 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'").strip()
server.succeed(f"ping -c 3 {nodeA_ip}")
server.succeed(f"curl -v http://{nodeA_ip}:9633/metrics")
'';
}
@@ -0,0 +1,6 @@
[
{
"publickey": "age1kxsp8pa8am6k333nxs4akjqkhht8gspznmlqz4pxn35h5dj4uv5qj6q6fl",
"type": "age"
}
]
@@ -0,0 +1,6 @@
[
{
"publickey": "age1dkrf438z3337d2qnc7ugkggua99xkh55wuf9zgun35fjrxdpnf5qkg4z6j",
"type": "age"
}
]

Some files were not shown because too many files have changed in this diff Show More