Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f89ad33c26 | |||
| d51f2d62b6 | |||
| 9b582bec7b | |||
| 8c4223af18 | |||
| b95992735e | |||
| 0d1562d150 | |||
| 49de78d47f | |||
| 832d0b5542 | |||
| 4d4c05ea70 | |||
| 0bd08c4a91 | |||
| a86888a2c7 | |||
| dc1ec3bf5a | |||
| 823ed83252 | |||
| 300103f2ba | |||
| d33f32ce5b | |||
| 14d9bba4eb | |||
| e63cee7b09 | |||
| a8318d3de2 | |||
| aab4a193ae | |||
| 541102f683 | |||
| 805590a705 | |||
| f8dbd99a7b | |||
| 90cc2d53f1 | |||
| b044504938 | |||
| 98ccc23ef5 | |||
| d7e8ca52a0 | |||
| f1cc0fa25c | |||
| 43828ad34c | |||
| 36d7e4a7e3 | |||
| 2f7d5d3908 | |||
| f3a0ca3f57 | |||
| 023b3f6605 | |||
| ecd3a46591 | |||
| 6bcca599e2 | |||
| 93529c578b | |||
| bb32784962 | |||
| 6ffa35c414 | |||
| 3cc5448410 | |||
| ee4178ee2d | |||
| 2bf18319c9 | |||
| a394b9124a | |||
| 5bc48d33a3 | |||
| 365ef5d49d | |||
| 0206d52fa2 | |||
| 5526e07e65 | |||
| dde682390f | |||
| 4ec59a64ce | |||
| c9c788e261 | |||
| 21c24216b4 | |||
| 2ecd350fcc | |||
| 3aa873d52a | |||
| 0a8ad18de8 | |||
| b6b94fea5a | |||
| de9762b272 | |||
| 8d1f4d51d0 | |||
| f1dc04ec4b | |||
| 7951c777cb | |||
| b031840f81 | |||
| 45861bef08 | |||
| 4433395125 | |||
| 3306aaab5e | |||
| 4480302c65 | |||
| be7fb5a243 | |||
| 25758bae08 | |||
| 7bfe9ad697 | |||
| 7db5e18974 | |||
| 20b7da74bf | |||
| adaf8b6a83 | |||
| 1f145334f3 | |||
| abf9f1b465 | |||
| f0740741a4 | |||
| 0c0b66b8db | |||
| bdf3c04595 | |||
| 02795a6ee4 | |||
| 8fa4a7ee60 | |||
| 773674d879 | |||
| 12c5ca126d | |||
| b38a2a07e2 | |||
| 0dc474887f | |||
| c8bd63ec3e | |||
| d7522f3f97 | |||
| 58c76f822f | |||
| 31bcde23b8 | |||
| fc2fa0666e | |||
| 854cc48479 | |||
| 85a4b124e5 | |||
| f322f3ebac | |||
| bc74fb4968 | |||
| 584abd4991 | |||
| 05074a1fd9 | |||
| 69060dfbff | |||
| 8e288a9e2a | |||
| bb03b6fa76 | |||
| fd92cfae6e | |||
| 25267d09a2 | |||
| f02f538ab2 | |||
| d319657680 | |||
| dff5a4e6d8 | |||
| 2a8ced0fec | |||
| 36c7096120 | |||
| adfcf2f848 | |||
| a3870a4293 | |||
| 8f4b61fc2b | |||
| 44e3a3011a | |||
| 45c972cca9 | |||
| 7bd5b8cbdf | |||
| d1eb9cc981 | |||
| 7a2ebf6872 | |||
| 72b8bd089c | |||
| cff229f487 | |||
| f3ac3cd67f | |||
| 820bb2de5b | |||
| 7d3ad52a44 | |||
| 2cdb98e898 | |||
| b717b1ceb4 | |||
| f31ce61c2b | |||
| aec22942f7 | |||
| fc8676c3bb | |||
| 2915e42a1d | |||
| 5783d3a51e | |||
| 2fe94bba23 | |||
| 4b42960d26 | |||
| 56e9abf945 | |||
| 4e2c2f92f0 | |||
| caa208b288 | |||
| 9e6f885c17 | |||
| d8ca87bfd8 | |||
| e9467e0cc7 | |||
| 6c98ef8944 | |||
| 18981e240b | |||
| df7e5953eb | |||
| 71d1c3f9c2 | |||
| 1453a755c3 | |||
| 970af805e9 | |||
| 383e9a9b1e | |||
| 26a16d0629 | |||
| 208de7654e | |||
| f577e7d58a | |||
| 6130ee73be | |||
| 5d827aa00c | |||
| 173ffc0044 | |||
| b113f2f48d | |||
| 7c67eaff21 | |||
| d1f1b84e82 | |||
| e3cb2adbb6 | |||
| 736c406eb5 | |||
| 8e9b750ac8 | |||
| 51c5578840 | |||
| e174af45f6 | |||
| 198e7188bd | |||
| 571f8f1504 | |||
| 64c3fe682c | |||
| 7c05b6158f | |||
| c9ab90547f | |||
| 63d929c8e8 | |||
| bbb87a2d69 | |||
| e5d5847b89 | |||
| 9e7294e871 | |||
| 69216c6b4c | |||
| 1ea172e690 | |||
| b7be45715e | |||
| 3522a7078b | |||
| b44f0e74e8 | |||
| 7c57f00b27 | |||
| c9d36ec65b | |||
| d8f97b9316 | |||
| d5bb2f6787 | |||
| ced82fc002 | |||
| 3535d2fd90 | |||
| 4e207c3397 | |||
| bc4e75a6a5 | |||
| 2ae922f3e8 | |||
| f263fdca3e | |||
| 1232e9cb30 | |||
| fbb29162ca | |||
| 7ab57a12b7 | |||
| 4e947d4b1e | |||
| b68e82ae03 | |||
| 91489551b9 | |||
| 86c99c2cbb | |||
| 7e2dfc21c6 | |||
| 9ac63220d5 | |||
| ffa5d19854 | |||
| 19fb29213e | |||
| f9870abc9e | |||
| 84ca556c47 | |||
| 9f2651e352 | |||
| bce876ec42 | |||
| bc8adcecad | |||
| 8878ce56c4 | |||
| dd9439b7fa | |||
| bc9f266ef0 | |||
| 1b083d298b | |||
| 83ba26735e | |||
| 50bd96ccdf | |||
| a133cfb189 | |||
| 051e68254e | |||
| 3fa8ab43ef | |||
| c6720f87c1 | |||
| 55ecdddadb | |||
| 9b5173a587 | |||
| 73f5a690bb | |||
| 54db751e23 | |||
| 85299b65dc | |||
| 45bda5b588 | |||
| 9114f5ce74 | |||
| 3925c1090e | |||
| e74538a1a9 | |||
| b8ee21b6e8 | |||
| 41fd54cfad | |||
| 1df34e0515 | |||
| ce0c194761 | |||
| a5e51ddd6b | |||
| 746e0b9dc4 | |||
| 77600a64fc | |||
| c6d5705097 | |||
| 6eefe97764 | |||
| 4bc4fe3ee8 | |||
| 57ec2bfc1b | |||
| d9d1150feb | |||
| 92896d8e52 | |||
| 477ffca33e | |||
| fdc65c544e | |||
| 945302b7c0 | |||
| 5ccf19cab8 | |||
| 7b61dd7f03 | |||
| 682865a0e1 | |||
| a0e4cf2479 | |||
| a5880d66f4 | |||
| 27a4583879 | |||
| fdbf5f8aca | |||
| 40c491aa14 | |||
| 1a8740fb9c | |||
| f857e751b5 | |||
| b420f2377c | |||
| 7d90b5ecb8 | |||
| ace979c226 | |||
| f540edb361 | |||
| 6bc5cd79da | |||
| 5ec77dfde6 | |||
| 52623d458e | |||
| 23b29f0707 | |||
| 338902497f | |||
| 977846991a | |||
| 0e8aec58fb | |||
| 0f1de58917 | |||
| 32183bd331 | |||
| 1813ca1927 | |||
| 51d44e472a | |||
| 44e87aa387 | |||
| f90deabb50 |
+26
-18
@@ -6,11 +6,11 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
name: Check, build and cache Nix flake
|
name: Check, build and cache nixfiles
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: cachix/install-nix-action@v23
|
- uses: cachix/install-nix-action@v31
|
||||||
with:
|
with:
|
||||||
# Gitea will supply a token in GITHUB_TOKEN, which this action will
|
# Gitea will supply a token in GITHUB_TOKEN, which this action will
|
||||||
# try to pass to Nix when downloading from GitHub
|
# try to pass to Nix when downloading from GitHub
|
||||||
@@ -18,22 +18,30 @@ jobs:
|
|||||||
extra_nix_config: |
|
extra_nix_config: |
|
||||||
# Make sure we're using sandbox
|
# Make sure we're using sandbox
|
||||||
sandbox-fallback = false
|
sandbox-fallback = false
|
||||||
|
# Big C++ projects fill up memory...
|
||||||
|
cores = 6
|
||||||
|
|
||||||
extra-substituters = https://nix-cache.nul.ie/main
|
extra-substituters = https://nix-cache.nul.ie
|
||||||
extra-trusted-public-keys = main:mMChkG8LwXrFirVfudqjSHasK1jV31OVElYD3eImYl8=
|
extra-trusted-public-keys = nix-cache.nul.ie-1:BzH5yMfF4HbzY1C977XzOxoPhEc9Zbu39ftPkUbH+m4=
|
||||||
- name: Set up attic
|
|
||||||
run: |
|
|
||||||
nix run .#nixpkgs.mine.x86_64-linux.attic-client -- \
|
|
||||||
login --set-default colony https://nix-cache.nul.ie "${{ secrets.NIX_CACHE_TOKEN }}"
|
|
||||||
|
|
||||||
- name: Check flake
|
- name: Check flake
|
||||||
run: nix flake check
|
run: nix flake check --no-build
|
||||||
- name: Build the world
|
|
||||||
|
- name: Build (and cache) the world
|
||||||
id: build
|
id: build
|
||||||
|
env:
|
||||||
|
HARMONIA_SSH_KEY: ${{ secrets.HARMONIA_SSH_KEY }}
|
||||||
run: |
|
run: |
|
||||||
path=$(nix build --no-link .#ci.x86_64-linux --json | jq -r .[0].outputs.out)
|
nix eval --json --apply "builtins.attrNames" .#ci.x86_64-linux | jq -cr '.[]' | while read job; do
|
||||||
echo "path=$path" >> "$GITHUB_OUTPUT"
|
echo "::group::Build $job"
|
||||||
- name: Push to cache
|
nix build --no-link .#ci.x86_64-linux."$job"
|
||||||
run: |
|
echo "::endgroup::"
|
||||||
nix run .#nixpkgs.mine.x86_64-linux.attic-client -- \
|
|
||||||
push main ${{ steps.build.outputs.path }}
|
echo "::group::Cache $job"
|
||||||
|
ci/push-to-cache.sh "$(nix eval --raw .#ci.x86_64-linux."$job")"
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Building and caching CI derivation"
|
||||||
|
nix build --no-link .#ciDrv.x86_64-linux
|
||||||
|
UPDATE_PROFILE=1 ci/push-to-cache.sh "$(nix eval --raw .#ciDrv.x86_64-linux)"
|
||||||
|
|||||||
@@ -14,22 +14,20 @@ jobs:
|
|||||||
uses: https://github.com/actions/setup-go@v4
|
uses: https://github.com/actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '>=1.20.1'
|
go-version: '>=1.20.1'
|
||||||
- uses: cachix/install-nix-action@v23
|
- uses: cachix/install-nix-action@v27
|
||||||
with:
|
with:
|
||||||
github_access_token: ${{ secrets.GH_PULL_TOKEN }}
|
github_access_token: ${{ secrets.GH_PULL_TOKEN }}
|
||||||
extra_nix_config: |
|
extra_nix_config: |
|
||||||
# Make sure we're using sandbox
|
# Make sure we're using sandbox
|
||||||
sandbox-fallback = false
|
sandbox-fallback = false
|
||||||
|
|
||||||
extra-substituters = https://nix-cache.nul.ie/main
|
extra-substituters = https://nix-cache.nul.ie
|
||||||
extra-trusted-public-keys = main:mMChkG8LwXrFirVfudqjSHasK1jV31OVElYD3eImYl8=
|
extra-trusted-public-keys = nix-cache.nul.ie-1:BzH5yMfF4HbzY1C977XzOxoPhEc9Zbu39ftPkUbH+m4=
|
||||||
- name: Set up attic
|
|
||||||
|
- name: Set up vars
|
||||||
id: setup
|
id: setup
|
||||||
run: |
|
run: |
|
||||||
nix run .#nixpkgs.mine.x86_64-linux.attic-client -- \
|
|
||||||
login --set-default colony https://nix-cache.nul.ie "${{ secrets.NIX_CACHE_TOKEN }}"
|
|
||||||
echo "short_rev=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
echo "short_rev=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Build installer ISO
|
- name: Build installer ISO
|
||||||
run: |
|
run: |
|
||||||
nix build .#nixfiles.config.nixos.systems.installer.configuration.config.my.buildAs.iso
|
nix build .#nixfiles.config.nixos.systems.installer.configuration.config.my.buildAs.iso
|
||||||
@@ -39,12 +37,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
nix build .#nixfiles.config.nixos.systems.installer.configuration.config.my.buildAs.netbootArchive
|
nix build .#nixfiles.config.nixos.systems.installer.configuration.config.my.buildAs.netbootArchive
|
||||||
ln -s "$(readlink result)" \
|
ln -s "$(readlink result)" \
|
||||||
jackos-installer-netboot-${{ steps.setup.outputs.short_rev }}.tar
|
jackos-installer-netboot-${{ steps.setup.outputs.short_rev }}.tar.zst
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: https://gitea.com/actions/release-action@main
|
uses: https://gitea.com/actions/release-action@main
|
||||||
with:
|
with:
|
||||||
title: Latest installer
|
title: Latest installer
|
||||||
|
api_key: '${{ secrets.RELEASE_TOKEN }}'
|
||||||
files: |
|
files: |
|
||||||
jackos-installer-${{ steps.setup.outputs.short_rev }}.iso
|
jackos-installer-${{ steps.setup.outputs.short_rev }}.iso
|
||||||
jackos-installer-netboot-${{ steps.setup.outputs.short_rev }}.tar
|
jackos-installer-netboot-${{ steps.setup.outputs.short_rev }}.tar.zst
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: Update docs assignments
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [docs-kimi-new]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update:
|
||||||
|
if: "!contains(github.event.head_commit.message, 'docs: update assignment tables')"
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v31
|
||||||
|
with:
|
||||||
|
github_access_token: ${{ secrets.GH_PULL_TOKEN }}
|
||||||
|
extra_nix_config: |
|
||||||
|
extra-substituters = https://nix-cache.nul.ie
|
||||||
|
extra-trusted-public-keys = nix-cache.nul.ie-1:BzH5yMfF4HbzY1C977XzOxoPhEc9Zbu39ftPkUbH+m4=
|
||||||
|
|
||||||
|
- name: Update assignment tables
|
||||||
|
run: nix run .#update-docs-assignments
|
||||||
|
|
||||||
|
- name: Commit and push if changed
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
REPO_URL: ${{ gitea.repositoryUrl }}
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git remote set-url origin "${REPO_URL/https:\/\//https:\/\/oauth2:${GITEA_TOKEN}@}"
|
||||||
|
git add docs/
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
git commit -m "docs: update assignment tables"
|
||||||
|
git push
|
||||||
|
fi
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKXRXkYnBf2opIjN+bXE7HmhUpa4hyXJUGmBT+MRccT4 harmonia
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to coding agents when working with code in this repository.
|
||||||
|
|
||||||
|
`CLAUDE.md` at the repo root is a symlink to this file — edit `AGENTS.md`, not the symlink (some
|
||||||
|
tools refuse to write through a symlink and will error on `CLAUDE.md`).
|
||||||
|
|
||||||
|
**Prefer this file over agent memory.** When you learn something durable about this repo — a
|
||||||
|
convention, a workflow gotcha, a design rationale — record it here (or in a repo doc this file points
|
||||||
|
to, e.g. `docs/sites/home/switches.md`), not in agent memory. AGENTS.md is versioned and shared; memory is not.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Personal Nix flake managing NixOS systems and home-manager configurations for a set of
|
||||||
|
machines — always called **"boxes"**, never "fleet". It is built around a **custom module
|
||||||
|
system** layered on top of NixOS/home-manager, not the stock flake `nixosConfigurations` pattern.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
This repo provides a `numtide/devshell` (entered via `direnv` / `use flake`). The shell defines
|
||||||
|
named commands — prefer them over raw `nix` invocations. Run a bare command name with no args to
|
||||||
|
see its help, or browse `devshell/commands.nix` / `devshell/install.nix` / `devshell/vm-tasks.nix`.
|
||||||
|
|
||||||
|
Common ones:
|
||||||
|
- `fmt` — format Nix with `nixpkgs-fmt` (the canonical formatter here).
|
||||||
|
- `build-system <host> [nix args]` — build a NixOS system's `toplevel`.
|
||||||
|
- `build-n-switch <args>` — wraps `doas nixos-rebuild --flake .`.
|
||||||
|
- `build-home <name>` / `home-switch` — build / switch a home-manager config.
|
||||||
|
- `run-vm <host>` — build & boot a system as a dev VM (installs `.keys/dev.key` into the VM).
|
||||||
|
- `build-iso` / `build-kexec` / `build-netboot <host>` — alternate build outputs via `config.my.buildAs.*`.
|
||||||
|
- `check-system <host> [nix args]` — evaluate a system (catches eval errors without a full build).
|
||||||
|
**Prefer this over `build-system` to validate a config change** — evaluation surfaces module/option
|
||||||
|
errors quickly and cheaply; only do a full build when you specifically need the built artifact.
|
||||||
|
- `deploy <host>` and `deploy-multi <hosts...>` — deploy-rs deployment (uses `.keys/deploy.key`, `--skip-checks`).
|
||||||
|
Pass the flake-qualified node, e.g. `deploy .#git`. The deploy node name is **always** the system
|
||||||
|
name (`deploy-rs.nix` keys nodes directly off `nixos.systems` / `home-manager.homes`); a system is
|
||||||
|
only a deploy target when `config.my.deploy.enable` is true (defaults true; auto-disabled for dev
|
||||||
|
VMs and containers). Pass `--boot` to stage a config as the boot default **without** live-switching
|
||||||
|
(`deploy --boot .#<host>`) — the box keeps running its current generation until it reboots. Use this
|
||||||
|
when a live `switch` would break connectivity mid-change (e.g. a router's WAN VLAN rework), then
|
||||||
|
reboot to cut over.
|
||||||
|
- `ssh-machine <name> [cmd]` — SSH to a NixOS system or home-manager config by name. Resolves the
|
||||||
|
target and ssh options (identity, port) from its deploy-rs node, so it needs `my.deploy.enable`
|
||||||
|
(same gate as `deploy`). Boxes default to the `fish` login shell, so pipe multi-statement remote
|
||||||
|
scripts through `bash` (e.g. `ssh-machine <name> bash -s < script.sh`) rather than `&&`/`for`.
|
||||||
|
If outbound SSH hangs at the publickey step (flaky `ssh-agent`), disable the agent for the call:
|
||||||
|
`SSH_AUTH_SOCK= ssh-machine …` (or add `-o IdentityAgent=none` to a raw `ssh`).
|
||||||
|
- `ragenix` — edit age secrets using `.keys/dev.key` as identity (see Secrets).
|
||||||
|
- `repl` — `nix repl .#`.
|
||||||
|
- `update-nixpkgs` / `update-home-manager` — bump pinned inputs.
|
||||||
|
|
||||||
|
Check everything (what CI runs): `nix flake check --no-build`.
|
||||||
|
CI builds each attr of `.#ci.x86_64-linux` (systems, homes, packages, shell) and pushes to the
|
||||||
|
Harmonia binary cache; see `.gitea/workflows/ci.yaml` and `ci/push-to-cache.sh`. A separate
|
||||||
|
workflow (`.gitea/workflows/update-docs.yaml`) regenerates the network-assignment tables in `docs/`
|
||||||
|
via `nix run .#update-docs-assignments`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The mechanics in this section have expanded human-readable write-ups under `docs/`:
|
||||||
|
`docs/architecture.md` (module system), `docs/networking.md` (assignments, topology, meshes) and
|
||||||
|
`docs/deployment.md` (deploy-rs, devshell, secrets, CI). This section stays the terse agent
|
||||||
|
version; consult those for depth.
|
||||||
|
|
||||||
|
### The custom module system
|
||||||
|
`flake.nix` does **not** call `nixosSystem` per host directly. Instead it `evalModules` over
|
||||||
|
`./nixos`, `./home-manager`, `./deploy-rs.nix`, and the per-host files listed in the `configs`
|
||||||
|
list in `flake.nix`. That evaluation produces a top-level config (`self.nixfiles`) from which the
|
||||||
|
real flake outputs are derived:
|
||||||
|
- `nixos.systems.<name>` → `nixosConfigurations.<name>`
|
||||||
|
- `home-manager.homes.<name>` → `homeConfigurations.<name>`
|
||||||
|
- `nixos.modules` / `home-manager.modules` → `nixosModules` / `homeModules`
|
||||||
|
- `deploy-rs.rendered` → `deploy`
|
||||||
|
|
||||||
|
`nixos/default.nix` and `home-manager/default.nix` define the `systemOpts` / `homeOpts` submodules
|
||||||
|
and the `mkSystem` / `mkHome` functions that actually invoke `eval-config.nix` /
|
||||||
|
`homeManagerConfiguration`. **To add a new host:** create a box file that sets
|
||||||
|
`nixos.systems.<name> = { ... }`, then add its path to the `configs` list in `flake.nix`.
|
||||||
|
|
||||||
|
### Multiple nixpkgs channels
|
||||||
|
Four pkgs sets are threaded everywhere as `pkgsFlakes` / `pkgs'` (and `hmFlakes` for home-manager):
|
||||||
|
`unstable`, `stable`, `mine` (a personal nixpkgs fork), `mine-stable`. Each system/home picks its
|
||||||
|
channel via `nixpkgs` / `home-manager` / `hmNixpkgs` options (e.g. `nixpkgs = "mine-stable"`).
|
||||||
|
Modules receive `pkgs'` = an attrset of all channels for the current system.
|
||||||
|
|
||||||
|
### `lib.my` and the `my` option namespace
|
||||||
|
`lib/default.nix` extends `lib` with a `my` attrset (helpers like `mkOpt'`, `mkBoolOpt'`,
|
||||||
|
`mkDefault'`, `inlineModule'`, `mkDefaultSystemsPkgs`, `homeStateVersion`). It also pulls in:
|
||||||
|
- `lib.my.net` — network/CIDR helpers from the `libnetRepo` input. Used heavily for IP math.
|
||||||
|
- `lib.my.c` — shared constants from `lib/constants.nix` (UIDs/GIDs, kernel package selection,
|
||||||
|
nginx snippets, per-network domains/prefixes, etc.). Reuse these rather than hardcoding.
|
||||||
|
- `lib.my.dns` — DNS helpers (`lib/dns.nix`).
|
||||||
|
|
||||||
|
Custom modules add options under the `my.*` namespace (e.g. `my.secrets`, `my.build`,
|
||||||
|
`my.tmproot`, `my.server`). Use `mkOpt'`/`mkBoolOpt'` for option declarations to match style.
|
||||||
|
|
||||||
|
### Modules and module lists
|
||||||
|
Module sets are registered in `nixos/modules/_list.nix` and `home-manager/modules/_list.nix`
|
||||||
|
(name → path), which become `nixos.modules` / `home-manager.modules` and are applied to every
|
||||||
|
system/home. To add a shared module, drop the file in `nixos/modules/` (or `home-manager/modules/`)
|
||||||
|
and add an entry to the relevant `_list.nix`.
|
||||||
|
|
||||||
|
### Network assignments
|
||||||
|
Each system declares `assignments.<name>` (in its `nixos.systems.<host>` block) with IPv4/IPv6
|
||||||
|
addresses, gateways, domains, MTU, etc. These are aggregated into `allAssignments` (passed to every
|
||||||
|
module) and there is an assertion that fails on duplicate IPs. Host networking
|
||||||
|
(`networking.hostName`, `domain`) defaults from the `internal` assignment.
|
||||||
|
|
||||||
|
### Hosts / "boxes"
|
||||||
|
Per-host configs live under `nixos/boxes/<host>` (some are single `.nix` files, some directories
|
||||||
|
with nested VMs/containers under e.g. `colony/vms`). Many "systems" are VMs or containers managed
|
||||||
|
via the `vms` / `containers` modules and the `l2mesh` VXLAN module.
|
||||||
|
|
||||||
|
For a human-readable map of what is actually deployed (per-box roles, services and networking),
|
||||||
|
see `README.md` and `docs/` (index at `docs/README.md`; box pages under `docs/sites/`,
|
||||||
|
`docs/remote/`, `docs/mobile/`). Keep these in sync when adding, removing or repurposing a box or
|
||||||
|
service. The network-assignment tables in box pages are CI-generated from `allAssignments` (the
|
||||||
|
`<!-- assignments: <name> -->` markers) — write the prose and let the updater refresh the tables.
|
||||||
|
|
||||||
|
### Home routers (`nixos/boxes/home/routing-common`)
|
||||||
|
The two home routers, `river` and `stream`, share `routing-common`, which is a **function of an
|
||||||
|
`index`** (`import ../../routing-common 0` for river, `1` for stream). The index derives per-box
|
||||||
|
addresses, keepalived VRRP priorities/state, DNS `ns` numbering, etc., so the two boxes are an
|
||||||
|
active/backup HA pair from one definition. They differ where hardware/uplink differ: `stream` has a
|
||||||
|
DHCP WAN, `river` runs PPPoE (`services.pppd`, Digiweb) — box-specific bits live in the respective
|
||||||
|
box file, not `routing-common`.
|
||||||
|
|
||||||
|
- **HA is VRRP (`keepalived`).** Per-VLAN floating **VIPs** (`lib.my.c.home.vips`) are what clients
|
||||||
|
use as both gateway *and* DNS server. `kea` (DHCP) and `radvd` (RAs; started only on the master)
|
||||||
|
hand out the VIP, and `pdns-recursor` binds the VIPs (with `net.ipv*.ip_nonlocal_bind` so the
|
||||||
|
backup can pre-bind). Point client-facing services at the VIP, not a box's real address, so
|
||||||
|
failover follows the master instead of relying on client resolver timeouts.
|
||||||
|
- **`wan-online.target`** is a shared abstract target meaning "the public WAN/IPv4 route is up".
|
||||||
|
`routing-common` only declares it; each box wires *how it is reached* (`stream`: a oneshot that
|
||||||
|
waits for the DHCP default route; `river`: the pppd `ip-up`/`ip-down` hooks). Services that need
|
||||||
|
the WAN attach **to** it via `wantedBy` + `partOf` + `after` (not `requires`/`wants`), so an empty
|
||||||
|
target is never pulled in and prematurely activated, and they re-load on WAN flap.
|
||||||
|
- networkd helpers used heavily here: `lib.my.networkdAssignment` and `lib.my.mkVLAN` live under
|
||||||
|
**`lib.my`**, while networkd snippet constants like `networkd.noL3` live under **`lib.my.c`** —
|
||||||
|
easy to mix up. Set an interface MTU via the `.network`'s `linkConfig.MTUBytes` (`[Link]`), not
|
||||||
|
`netdevConfig` (`[NetDev]` rejects `MTUBytes`).
|
||||||
|
|
||||||
|
### Home switches (`jim` / `dave` / `brian`)
|
||||||
|
The home boxes and the Digiweb WAN hang off hand-configured switches that are **not** managed by
|
||||||
|
this flake: `jim` and `dave` (MikroTik, RouterOS) and `brian` (Ubiquiti, UniFi). The full topology,
|
||||||
|
VLAN map, and the ONT/WAN path live in **`docs/sites/home/switches.md`** — read it before
|
||||||
|
touching anything WAN/VLAN-related, and update it when the switch layout changes.
|
||||||
|
- **Access:** the switches resolve by **short hostname** on the home network (the routers serve
|
||||||
|
their records in the home zone — `routing-common/dns.nix`: `jim`/`dave`/`brian`). From a home box,
|
||||||
|
SSH to the MikroTiks as `admin`/`admin` (e.g. `ssh admin@jim`); `brian` is configured via the
|
||||||
|
UniFi controller, not a CLI.
|
||||||
|
- **Changing switch config is out-of-band and hard to revert — always confirm before applying:**
|
||||||
|
print the affected menu, make the change, then re-verify. The nix config and the switches must
|
||||||
|
agree on VLAN numbering (e.g. `lib.my.c.home.vlans`), so a switch-side change usually pairs with a
|
||||||
|
box change; `docs/sites/home/switches.md` documents the switch layout and per-switch config for the WAN design.
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
age-encrypted secrets in `secrets/`, managed with **ragenix**. Each module declares
|
||||||
|
`my.secrets.files.<name>` and `my.secrets.key` (the host pubkey to encrypt for). `secrets.nix`
|
||||||
|
(the ragenix rules file) is generated by reading every system's declared secrets and computing the
|
||||||
|
recipient key list (always including `.keys/dev.pub`). Edit secrets with the `ragenix` devshell
|
||||||
|
command, which supplies `.keys/dev.key` as the identity. The `.keys/` directory (dev + deploy
|
||||||
|
private keys) is required for editing secrets, deploying, and running dev VMs.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Format with `nixpkgs-fmt` (`fmt`). 2-space indent, `inherit (...)` blocks at the top of `let`.
|
||||||
|
**Ask before running `fmt`** — some files aren't canonically formatted, so `fmt` can reindent a
|
||||||
|
whole file and bury a logical change in whitespace churn. Match the surrounding style by hand and
|
||||||
|
leave formatting to the user unless they ask.
|
||||||
|
- Comment where it genuinely aids understanding, but not for trivial/obvious code — match the file's
|
||||||
|
existing (fairly sparse) comment density. When adding something general, comment its general
|
||||||
|
purpose, not the specific change or one-off reason it was introduced for.
|
||||||
|
- Prefer `lib.my` helpers (`mkOpt'`, `mkBoolOpt'`, `mkDefault'`) and `lib.my.c` constants over
|
||||||
|
reimplementing.
|
||||||
|
- New shared functionality → a module in `*/modules/` + entry in `_list.nix`, options under `my.*`.
|
||||||
|
- New host → box file under `nixos/boxes/` + entry in the `configs` list in `flake.nix`.
|
||||||
|
- Custom packages live in `pkgs/` and are registered in `pkgs/default.nix`; the overlay is exposed
|
||||||
|
as `overlays.default`.
|
||||||
|
- In prose and commit messages, quote code-like identifiers (commands, options, paths, package and
|
||||||
|
attribute names) in backticks.
|
||||||
|
- Call the machines **"boxes"**, never "fleet".
|
||||||
|
- Commit subjects follow `area/scope: Capitalized summary` (e.g. `nixos/home: ...`); keep logically
|
||||||
|
distinct changes in separate commits.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# nixfiles
|
||||||
|
|
||||||
|
Personal Nix flake managing every machine I run: hosted servers, home
|
||||||
|
infrastructure, routers, a remote site, VPSes and personal workstations. It is
|
||||||
|
built around a **custom module system** layered on top of NixOS and
|
||||||
|
home-manager rather than the stock per-host `nixosConfigurations` pattern.
|
||||||
|
|
||||||
|
For day-to-day commands and a deeper explanation of the module system,
|
||||||
|
conventions and secrets, see [`AGENTS.md`](AGENTS.md). This README is the map of
|
||||||
|
**what is actually deployed**; the per-machine details live under [`docs/`](docs)
|
||||||
|
(start at [`docs/README.md`](docs/README.md)).
|
||||||
|
|
||||||
|
> **Note:** This documentation (the README and everything under `docs/`) is a
|
||||||
|
> work in progress and was **agent-generated** from the repository. It may be
|
||||||
|
> incomplete or out of date — treat the Nix configuration as the source of truth.
|
||||||
|
|
||||||
|
## The boxes at a glance
|
||||||
|
|
||||||
|
Machines are grouped by deployment/location. Each group has its own directory
|
||||||
|
under `docs/` with a `README.md` overview and one file per machine.
|
||||||
|
|
||||||
|
| Group | What it is | Docs |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **colony** | Hosted dedicated server in Amsterdam (`ams1`). A VM host running the public-facing infrastructure: routing, web, git, media, object storage, chat, game servers. | [`docs/sites/colony/`](docs/sites/colony) |
|
||||||
|
| **home** | Home network: a VM host (`palace`), the home routers, storage, Home Assistant, and personal desktops — plus the hand-configured switches tying it together. | [`docs/sites/home/`](docs/sites/home) |
|
||||||
|
| **remote** | Edge VPSes (`britway`, `britnet`) and the remote `kelder` site. | [`docs/remote/`](docs/remote) |
|
||||||
|
| **mobile** | The `tower` laptop. | [`docs/mobile/`](docs/mobile) |
|
||||||
|
|
||||||
|
The custom installer image is documented at [`docs/misc/installer.md`](docs/misc/installer.md).
|
||||||
|
Cross-cutting topics — the module system, the network, deployment — live next to
|
||||||
|
the index: [`docs/architecture.md`](docs/architecture.md),
|
||||||
|
[`docs/networking.md`](docs/networking.md), [`docs/deployment.md`](docs/deployment.md).
|
||||||
|
|
||||||
|
## The "big machine" pattern
|
||||||
|
|
||||||
|
The larger sites (`colony`, `home`) all follow the same shape:
|
||||||
|
|
||||||
|
```
|
||||||
|
physical host (VM host)
|
||||||
|
├── VM ── thing impractical to containerise (router, storage, podman host, …)
|
||||||
|
├── VM ── container host ──┬── NixOS container ── application
|
||||||
|
│ ├── NixOS container ──┬── application
|
||||||
|
│ │ ├── application
|
||||||
|
│ │ └── application
|
||||||
|
│ └── …
|
||||||
|
└── VM ── …
|
||||||
|
```
|
||||||
|
|
||||||
|
- A **physical host** (`colony`, `palace`) does little itself beyond running
|
||||||
|
**VMs** via the custom `my.vms` module (QEMU + systemd units, LVM-backed
|
||||||
|
disks).
|
||||||
|
- **VMs** exist for things that are impractical to put in a container — kernel
|
||||||
|
features, separate networking, podman/OCI workloads, foreign OSes.
|
||||||
|
- One VM is usually a **container host** (`shill` on colony, `sfh` on home; and
|
||||||
|
`kelder` directly). It runs **NixOS containers** via the custom `my.containers`
|
||||||
|
module (systemd-nspawn based, each with its own address on a bridge).
|
||||||
|
- **Most applications live in those NixOS containers.** A container isn't limited
|
||||||
|
to a single application — it commonly hosts a **group of related applications**
|
||||||
|
that belong together (e.g. `jackflix` runs Jellyfin, the *arr stack,
|
||||||
|
Transmission, PhotoPrism and copyparty; `object` runs MinIO, Harmonia, HedgeDoc
|
||||||
|
and wastebin). Each container is a first-class entry in the docs.
|
||||||
|
|
||||||
|
Networking between everything is largely defined by per-system `assignments`
|
||||||
|
(IPs/prefixes) plus an L2 VXLAN mesh (`my.vpns.l2`, AS211024) that ties the edge
|
||||||
|
routers together. See [`docs/networking.md`](docs/networking.md) for the full
|
||||||
|
picture and [`AGENTS.md`](AGENTS.md) for the mechanics.
|
||||||
|
|
||||||
|
## Repo layout
|
||||||
|
|
||||||
|
```
|
||||||
|
README.md <- you are here
|
||||||
|
nixos/
|
||||||
|
boxes/ per-machine configuration ("boxes")
|
||||||
|
colony/ colony host + its VMs (vms/) + shill's containers
|
||||||
|
home/ palace host + its VMs, routing-common, plus stream, castle
|
||||||
|
britway/ britnet.nix, kelder/, tower/, installer.nix …
|
||||||
|
modules/ shared NixOS modules (my.* options); registered in _list.nix
|
||||||
|
home-manager/ home-manager modules + configs
|
||||||
|
lib/ lib.my helpers, constants (lib.my.c), net/dns helpers
|
||||||
|
pkgs/ custom packages (overlays.default)
|
||||||
|
secrets/ age-encrypted secrets (ragenix)
|
||||||
|
devshell/ devshell commands (build/deploy/check/ssh helpers)
|
||||||
|
ci/ CI helpers (binary-cache push, docs assignment-table updater)
|
||||||
|
docs/ deployment documentation (index at docs/README.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
A machine is wired into the flake by adding its box file to the `configs` list
|
||||||
|
in `flake.nix`. See [`AGENTS.md`](AGENTS.md#architecture) for how `evalModules`
|
||||||
|
turns these into `nixosConfigurations`, `homeConfigurations` and `deploy` nodes.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
object-ctr.ams1.int.nul.ie ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFdHbZErWLmTPO/aEWB1Fup/aGMf31Un5Wk66FJwTz/8
|
||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
REMOTE_STORE=/var/lib/harmonia
|
||||||
|
SSH_HOST="harmonia@object-ctr.ams1.int.nul.ie"
|
||||||
|
SSH_KEY=/tmp/harmonia.key
|
||||||
|
STORE_URI="ssh-ng://$SSH_HOST?ssh-key=$SSH_KEY&remote-store=$REMOTE_STORE"
|
||||||
|
|
||||||
|
remote_cmd() {
|
||||||
|
ssh -i "$SSH_KEY" "$SSH_HOST" env HOME=/run/harmonia NIX_REMOTE="$REMOTE_STORE" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
umask_old=$(umask)
|
||||||
|
umask 0066
|
||||||
|
echo "$HARMONIA_SSH_KEY" | base64 -d > "$SSH_KEY"
|
||||||
|
umask $umask_old
|
||||||
|
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
cp ci/known_hosts ~/.ssh/
|
||||||
|
path="$1"
|
||||||
|
|
||||||
|
echo "Pushing $path to cache..."
|
||||||
|
nix copy --no-check-sigs --to "$STORE_URI" "$path"
|
||||||
|
|
||||||
|
if [ -n "$UPDATE_PROFILE" ]; then
|
||||||
|
echo "Updating profile..."
|
||||||
|
remote_cmd nix-env -p "$REMOTE_STORE"/nix/var/nix/profiles/nixfiles --set "$path"
|
||||||
|
|
||||||
|
echo "Collecting garbage..."
|
||||||
|
remote_cmd nix-collect-garbage --delete-older-than 60d
|
||||||
|
fi
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Update assignment tables in docs/ from nixos.allAssignments."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DOCS_DIR = Path("docs")
|
||||||
|
HEADER = "| Name | Assignment | IPv4 | IPv6 | Domain | Notes |"
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_ip(ip: dict) -> str:
|
||||||
|
addr = ip.get("address")
|
||||||
|
if addr is None:
|
||||||
|
return "—"
|
||||||
|
mask = ip.get("mask")
|
||||||
|
gateway = ip.get("gateway")
|
||||||
|
parts = [f"{addr}/{mask}"]
|
||||||
|
if gateway is not None:
|
||||||
|
parts.append(f"gw {gateway}")
|
||||||
|
return f"`{' '.join(parts)}`"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_notes(lines: list[str]) -> dict[str, str]:
|
||||||
|
"""Extract existing Notes indexed by Assignment from a marked table."""
|
||||||
|
notes: dict[str, str] = {}
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped.startswith("|"):
|
||||||
|
continue
|
||||||
|
cells = [c.strip() for c in stripped.split("|")]
|
||||||
|
# Splitting "| a | b |" produces ['', 'a', 'b', '']
|
||||||
|
if len(cells) < 8:
|
||||||
|
continue
|
||||||
|
key = cells[2]
|
||||||
|
if key == "Assignment":
|
||||||
|
continue
|
||||||
|
notes[key] = cells[6]
|
||||||
|
return notes
|
||||||
|
|
||||||
|
|
||||||
|
def render_table(assignments: dict, notes: dict[str, str]) -> list[str]:
|
||||||
|
lines = [
|
||||||
|
HEADER,
|
||||||
|
"|---|---|---|---|---|---|",
|
||||||
|
]
|
||||||
|
for key, a in assignments.items():
|
||||||
|
name = a.get("name", key)
|
||||||
|
alt = ", ".join(a.get("altNames", []))
|
||||||
|
if alt:
|
||||||
|
name = f"{name} ({alt})"
|
||||||
|
lines.append(
|
||||||
|
"| "
|
||||||
|
+ " | ".join(
|
||||||
|
[
|
||||||
|
name,
|
||||||
|
key,
|
||||||
|
fmt_ip(a.get("ipv4", {})),
|
||||||
|
fmt_ip(a.get("ipv6", {})),
|
||||||
|
a.get("domain") or "—",
|
||||||
|
notes.get(key, ""),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ " |"
|
||||||
|
)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def process_file(path: Path, all_assignments: dict) -> bool:
|
||||||
|
box_name = path.stem
|
||||||
|
if box_name not in all_assignments:
|
||||||
|
return False
|
||||||
|
|
||||||
|
text = path.read_text()
|
||||||
|
lines = text.splitlines()
|
||||||
|
|
||||||
|
marker_re = re.compile(r"^<!--\s*assignments:\s*(\S+)\s*-->$")
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
m = marker_re.match(line.strip())
|
||||||
|
if m and m.group(1) == box_name:
|
||||||
|
end_idx = None
|
||||||
|
for j in range(i + 1, len(lines)):
|
||||||
|
if lines[j].strip() == "<!-- assignments-end -->":
|
||||||
|
end_idx = j
|
||||||
|
break
|
||||||
|
if end_idx is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
old_inner = lines[i + 1 : end_idx]
|
||||||
|
notes = parse_notes(old_inner)
|
||||||
|
new_table = render_table(all_assignments[box_name], notes)
|
||||||
|
new_lines = (
|
||||||
|
lines[: i + 1]
|
||||||
|
+ ["<!-- assignments-start -->"]
|
||||||
|
+ new_table
|
||||||
|
+ ["<!-- assignments-end -->"]
|
||||||
|
+ lines[end_idx + 1 :]
|
||||||
|
)
|
||||||
|
if new_lines != lines:
|
||||||
|
path.write_text("\n".join(new_lines) + "\n")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
result = subprocess.run(
|
||||||
|
["nix", "eval", ".#nixfiles.config.nixos.allAssignments", "--json"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
all_assignments = json.loads(result.stdout)
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
for path in sorted(DOCS_DIR.rglob("*.md")):
|
||||||
|
if process_file(path, all_assignments):
|
||||||
|
print(f"updated {path}")
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
return 1 if changed else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+51
-19
@@ -20,7 +20,7 @@ in
|
|||||||
[ -e "${homeFlake}" ] && echo "${homeFlake} already exists" && exit 1
|
[ -e "${homeFlake}" ] && echo "${homeFlake} already exists" && exit 1
|
||||||
|
|
||||||
mkdir -p "$(dirname "${homeFlake}")"
|
mkdir -p "$(dirname "${homeFlake}")"
|
||||||
ln -s "$(pwd)/flake.nix" "${homeFlake}"
|
ln -sf "$(pwd)/flake.nix" "${homeFlake}"
|
||||||
echo "Installed link to $(pwd)/flake.nix at ${homeFlake}"
|
echo "Installed link to $(pwd)/flake.nix at ${homeFlake}"
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
@@ -48,11 +48,30 @@ in
|
|||||||
help = "Print the ed25519 pubkey for a host";
|
help = "Print the ed25519 pubkey for a host";
|
||||||
command = "${pkgs.openssh}/bin/ssh-keyscan -t ed25519 \"$1\" 2> /dev/null | awk '{ print $2 \" \" $3 }'";
|
command = "${pkgs.openssh}/bin/ssh-keyscan -t ed25519 \"$1\" 2> /dev/null | awk '{ print $2 \" \" $3 }'";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
name = "ssh-machine";
|
||||||
|
category = "utilities";
|
||||||
|
help = "SSH to a machine by NixOS system or home-manager config name";
|
||||||
|
command = ''
|
||||||
|
name="$1"
|
||||||
|
shift
|
||||||
|
# Run from the project root so deploy's relative identity path (.keys/deploy.key) resolves
|
||||||
|
cd "$PRJ_ROOT"
|
||||||
|
# deploy-rs node names are the system/home name, with `@` mangled to `-at-`
|
||||||
|
node="''${name//@/-at-}"
|
||||||
|
# Single eval: resolve `user@host` plus merged (global + node) deploy-rs ssh options
|
||||||
|
info="$(nix eval --raw .#deploy --apply 'd: let n = d.nodes."'"$node"'"; in "''${n.sshUser}@''${n.hostname} ''${builtins.concatStringsSep " " (d.sshOpts ++ n.sshOpts)}"')"
|
||||||
|
target="''${info%% *}"
|
||||||
|
opts="''${info#* }"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
exec ${pkgs.openssh}/bin/ssh $opts "$target" "$@"
|
||||||
|
'';
|
||||||
|
}
|
||||||
{
|
{
|
||||||
name = "json2nix";
|
name = "json2nix";
|
||||||
category = "utilities";
|
category = "utilities";
|
||||||
help = "Convert JSON to formatted Nix";
|
help = "Convert JSON to formatted Nix";
|
||||||
command = "nix eval --impure --expr 'builtins.fromJSON (builtins.readFile /dev/stdin)' | ${pkgs.nixfmt}/bin/nixfmt";
|
command = "nix eval --impure --expr 'builtins.fromJSON (builtins.readFile /dev/stdin)' | ${pkgs.nixfmt}/bin/nixfmt -";
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -73,11 +92,22 @@ in
|
|||||||
help = "Build NixOS configuration";
|
help = "Build NixOS configuration";
|
||||||
command = ''nix build "''${@:2}" ".#nixosConfigurations.\"$1\".config.system.build.toplevel"'';
|
command = ''nix build "''${@:2}" ".#nixosConfigurations.\"$1\".config.system.build.toplevel"'';
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
name = "check-system";
|
||||||
|
category = "utilities";
|
||||||
|
help = "Evaluate NixOS configuration (check validity without building)";
|
||||||
|
command = ''nix eval "''${@:2}" ".#nixosConfigurations.\"$1\".config.system.build.toplevel.drvPath"'';
|
||||||
|
}
|
||||||
{
|
{
|
||||||
name = "build-n-switch";
|
name = "build-n-switch";
|
||||||
category = "tasks";
|
category = "tasks";
|
||||||
help = "Shortcut to nixos-rebuild for this flake";
|
help = "Shortcut to nixos-rebuild for this flake";
|
||||||
command = ''doas nixos-rebuild --flake . "$@"'';
|
command = ''
|
||||||
|
# HACK: Upstream changes in Git + Nix makes this necessary
|
||||||
|
# https://github.com/NixOS/nix/issues/10202
|
||||||
|
doas git config --global --add safe.directory "$PWD"
|
||||||
|
doas nixos-rebuild --flake . "$@"
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "run-vm";
|
name = "run-vm";
|
||||||
@@ -106,8 +136,8 @@ in
|
|||||||
{
|
{
|
||||||
name = "build-netboot";
|
name = "build-netboot";
|
||||||
category = "tasks";
|
category = "tasks";
|
||||||
help = "Build NixOS configuration as netboot archive";
|
help = "Build NixOS configuration as netboot tree";
|
||||||
command = ''nix build "''${@:2}" ".#nixfiles.config.nixos.systems.\"$1\".configuration.config.my.buildAs.netbootArchive"'';
|
command = ''nix build "''${@:2}" ".#nixfiles.config.nixos.systems.\"$1\".configuration.config.my.buildAs.netbootTree"'';
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "build-home";
|
name = "build-home";
|
||||||
@@ -115,29 +145,17 @@ in
|
|||||||
help = "Build home-manager configuration";
|
help = "Build home-manager configuration";
|
||||||
command = ''nix build "''${@:2}" ".#homeConfigurations.\"$1\".activationPackage"'';
|
command = ''nix build "''${@:2}" ".#homeConfigurations.\"$1\".activationPackage"'';
|
||||||
}
|
}
|
||||||
{
|
|
||||||
name = "update-inputs";
|
|
||||||
category = "tasks";
|
|
||||||
help = "Update flake inputs";
|
|
||||||
command = ''
|
|
||||||
args=()
|
|
||||||
for f in "$@"; do
|
|
||||||
args+=(--update-input "$f")
|
|
||||||
done
|
|
||||||
nix flake lock "''${args[@]}"
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
name = "update-nixpkgs";
|
name = "update-nixpkgs";
|
||||||
category = "tasks";
|
category = "tasks";
|
||||||
help = "Update nixpkgs flake inputs";
|
help = "Update nixpkgs flake inputs";
|
||||||
command = ''update-inputs nixpkgs-{unstable,stable,mine,mine-stable}'';
|
command = ''nix flake update nixpkgs-{unstable,stable,mine,mine-stable}'';
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "update-home-manager";
|
name = "update-home-manager";
|
||||||
category = "tasks";
|
category = "tasks";
|
||||||
help = "Update home-manager flake inputs";
|
help = "Update home-manager flake inputs";
|
||||||
command = ''update-inputs home-manager-{unstable,stable}'';
|
command = ''nix flake update home-manager-{unstable,stable}'';
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "update-installer";
|
name = "update-installer";
|
||||||
@@ -145,5 +163,19 @@ in
|
|||||||
help = "Update installer tag (to trigger new release)";
|
help = "Update installer tag (to trigger new release)";
|
||||||
command = ''git tag -f installer && git push -f origin installer'';
|
command = ''git tag -f installer && git push -f origin installer'';
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
name = "deploy-multi";
|
||||||
|
category = "tasks";
|
||||||
|
help = "Deploy multiple flakes at once";
|
||||||
|
command = ''
|
||||||
|
for f in $@; do
|
||||||
|
if [ -n "''${O:-}" ]; then
|
||||||
|
deploy "$O" $f
|
||||||
|
else
|
||||||
|
deploy $f
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ in
|
|||||||
|
|
||||||
NIX_USER_CONF_FILES = toString (pkgs.writeText "nix.conf"
|
NIX_USER_CONF_FILES = toString (pkgs.writeText "nix.conf"
|
||||||
''
|
''
|
||||||
experimental-features = nix-command flakes ca-derivations repl-flake
|
experimental-features = nix-command flakes ca-derivations
|
||||||
connect-timeout = 5
|
connect-timeout = 5
|
||||||
fallback = true
|
fallback = true
|
||||||
${lib.my.c.nix.cache.conf}
|
${lib.my.c.nix.cache.conf}
|
||||||
@@ -24,10 +24,10 @@ in
|
|||||||
coreutils
|
coreutils
|
||||||
nixVersions.stable
|
nixVersions.stable
|
||||||
rage
|
rage
|
||||||
|
wireguard-tools
|
||||||
(pkgs.writeShellScriptBin "deploy" ''
|
(pkgs.writeShellScriptBin "deploy" ''
|
||||||
exec ${deploy-rs.deploy-rs}/bin/deploy --skip-checks "$@"
|
exec ${deploy-rs.deploy-rs}/bin/deploy --skip-checks "$@"
|
||||||
'')
|
'')
|
||||||
home-manager
|
home-manager
|
||||||
attic-client
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Deployment documentation
|
||||||
|
|
||||||
|
> **Note:** these pages are a work in progress and were **agent-generated** from the repository.
|
||||||
|
> They may be incomplete or out of date — treat the Nix configuration as the source of truth.
|
||||||
|
|
||||||
|
This directory documents the boxes managed by this flake: their roles, network assignments,
|
||||||
|
hierarchy, and the services they run. For the mechanics of the repo itself (conventions, module
|
||||||
|
system internals for contributors, agent guidance), see [`AGENTS.md`](../AGENTS.md).
|
||||||
|
|
||||||
|
The two big sites follow the pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
physical host (VM host)
|
||||||
|
└── VM (for things impractical to containerise)
|
||||||
|
└── container host VM
|
||||||
|
└── NixOS containers (one per application group)
|
||||||
|
```
|
||||||
|
|
||||||
|
Not every box fits this pattern, but **colony** and **home** are organised this way.
|
||||||
|
|
||||||
|
## Cross-cutting topics
|
||||||
|
|
||||||
|
- [`architecture.md`](architecture.md) — the custom module system, `my.*` namespace, multiple
|
||||||
|
nixpkgs channels, shared module inventory.
|
||||||
|
- [`networking.md`](networking.md) — network assignments, domains, site topologies, router HA,
|
||||||
|
the AS211024 L2 mesh, BGP, WireGuard, Tailscale.
|
||||||
|
- [`deployment.md`](deployment.md) — deploy-rs, devshell commands, secrets workflow, CI.
|
||||||
|
|
||||||
|
## Site: colony (Amsterdam)
|
||||||
|
|
||||||
|
Physical host and public-infrastructure hub — see [`sites/colony/README.md`](sites/colony/README.md).
|
||||||
|
|
||||||
|
```
|
||||||
|
colony (physical VM host, ams1)
|
||||||
|
├── estuary ── edge router: WAN, firewall/NAT, DNS, BGP (AS211024), WireGuard
|
||||||
|
├── shill ──── NixOS container host ──┬── middleman (reverse proxy, ACME, nginx-sso, librespeed)
|
||||||
|
│ ├── vaultwarden (password manager)
|
||||||
|
│ ├── colony-psql (shared PostgreSQL)
|
||||||
|
│ ├── chatterbox (Matrix Synapse + bridges)
|
||||||
|
│ ├── jackflix (media stack)
|
||||||
|
│ ├── object (MinIO, Harmonia Nix cache, Sharry, HedgeDoc, wastebin)
|
||||||
|
│ ├── toot (Bluesky PDS; Mastodon disabled)
|
||||||
|
│ ├── waffletail (Tailscale subnet router / exit node)
|
||||||
|
│ ├── qclk (WireGuard management appliance)
|
||||||
|
│ ├── gam (Terraria server)
|
||||||
|
│ └── jam (raw nspawn customer container)
|
||||||
|
├── whale2 ─── podman/OCI host for game servers
|
||||||
|
├── git ────── Gitea + Gitea Actions runner
|
||||||
|
├── mail ───── Debian VM running mailcow (not NixOS)
|
||||||
|
└── darts ──── third-party/customer VM (opaque, not NixOS)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Site: home
|
||||||
|
|
||||||
|
Redundant routers, VM host, storage, IoT containers and the workstation — see
|
||||||
|
[`sites/home/README.md`](sites/home/README.md). The hand-configured switch fabric (jim/dave/brian)
|
||||||
|
and the Digiweb WAN path are documented in [`sites/home/switches.md`](sites/home/switches.md).
|
||||||
|
|
||||||
|
```
|
||||||
|
h.nul.ie
|
||||||
|
├── palace (physical VM host — AMD, 100G, SR-IOV)
|
||||||
|
│ ├── river ── primary router VM (PPPoE / Digiweb WAN)
|
||||||
|
│ ├── cellar ─ NVMe-oF / SPDK storage target VM
|
||||||
|
│ └── sfh ──── container host VM ("services for home")
|
||||||
|
│ ├── hass ── Home Assistant + Frigate + MQTT (container)
|
||||||
|
│ └── unifi ─ UniFi controller (container)
|
||||||
|
├── stream (physical secondary router — Virgin Media WAN)
|
||||||
|
└── castle (workstation / gaming desktop — netboot, NVMe-oF root)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Remote boxes
|
||||||
|
|
||||||
|
Edge VPSes and the remote `kelder` site — see [`remote/README.md`](remote/README.md).
|
||||||
|
|
||||||
|
- [`remote/britway.md`](remote/britway.md) — London VPS: headscale control plane, Tailscale exit
|
||||||
|
node, BGP edge.
|
||||||
|
- [`remote/britnet.md`](remote/britnet.md) — Birmingham VPS: Tailscale exit node, WireGuard hub.
|
||||||
|
- [`remote/kelder.md`](remote/kelder.md) — secondary home server: container host, Nextcloud,
|
||||||
|
Samba, WireGuard to `estuary`.
|
||||||
|
- [`remote/kelder-acquisition.md`](remote/kelder-acquisition.md) — media acquisition container.
|
||||||
|
- [`remote/kelder-spoder.md`](remote/kelder-spoder.md) — Nextcloud + nginx container.
|
||||||
|
|
||||||
|
## Mobile boxes
|
||||||
|
|
||||||
|
- [`mobile/tower.md`](mobile/tower.md) — laptop workstation.
|
||||||
|
|
||||||
|
## Misc
|
||||||
|
|
||||||
|
- [`misc/installer.md`](misc/installer.md) — the custom NixOS installer image.
|
||||||
|
|
||||||
|
## A note on the assignment tables
|
||||||
|
|
||||||
|
The "Network assignments" tables in box pages (between `<!-- assignments: … -->` markers) are
|
||||||
|
**generated from the flake** (`nixos.allAssignments`) by `nix run .#update-docs-assignments` —
|
||||||
|
CI refreshes them on push. Hand-written prose and the table's Notes column are preserved; don't
|
||||||
|
hand-edit the other cells.
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
This flake does **not** use the stock pattern of calling `nixosSystem` once per host in
|
||||||
|
[`flake.nix`](../flake.nix). Instead it runs a single `lib.evalModules` evaluation over its own
|
||||||
|
module tree, producing one big top-level config (`self.nixfiles`) from which the real flake
|
||||||
|
outputs (`nixosConfigurations`, `homeConfigurations`, `nixosModules`, `deploy`, …) are derived.
|
||||||
|
Per-host files declare *options* (`nixos.systems.<name>`); the machinery in
|
||||||
|
[`nixos/default.nix`](../nixos/default.nix) and
|
||||||
|
[`home-manager/default.nix`](../home-manager/default.nix) turns those into evaluated NixOS /
|
||||||
|
home-manager configurations.
|
||||||
|
|
||||||
|
## The top-level evaluation
|
||||||
|
|
||||||
|
`flake.nix`'s `outputs` builds a `nixfiles` attrset via `evalModules` over:
|
||||||
|
|
||||||
|
- An inline module that seeds `_module.args` (`lib`, `pkgsFlakes`, `hmFlakes`, `self`, `inputs`,
|
||||||
|
`pkgs'`), sets `nixos.secretsPath = ./secrets`, and sets the global deploy-rs SSH option
|
||||||
|
`deploy-rs.deploy.sshOpts = [ "-i" ".keys/deploy.key" ]`.
|
||||||
|
- `nixos/modules/misc/assertions.nix` from the unstable nixpkgs (so the top-level evaluation has
|
||||||
|
the standard `assertions` / `warnings` options).
|
||||||
|
- [`nixos/`](../nixos/default.nix) — defines `nixos.*` options (`systems`, `modules`,
|
||||||
|
`allAssignments`, `vpns`, `secretsPath`) and `mkSystem`.
|
||||||
|
- [`home-manager/`](../home-manager/default.nix) — defines `home-manager.*` options (`homes`,
|
||||||
|
`modules`) and `mkHome`.
|
||||||
|
- [`deploy-rs.nix`](../deploy-rs.nix) — defines `deploy-rs.*` and renders the deploy config.
|
||||||
|
- Every file in the `configs` list — the boxes themselves
|
||||||
|
([`nixos/boxes/`](../nixos/boxes) plus [`nixos/installer.nix`](../nixos/installer.nix); the
|
||||||
|
home-manager entries are currently commented out, see [Home-manager](#home-manager)).
|
||||||
|
|
||||||
|
The resulting `nixfiles.config` is mapped onto flake outputs:
|
||||||
|
|
||||||
|
| Top-level option (`nixfiles.config`) | Flake output |
|
||||||
|
|---|---|
|
||||||
|
| `nixos.systems.<name>.rendered` | `nixosConfigurations.<name>` |
|
||||||
|
| `home-manager.homes.<name>.configuration` | `homeConfigurations.<name>` |
|
||||||
|
| `nixos.modules` | `nixosModules` |
|
||||||
|
| `home-manager.modules` | `homeModules` |
|
||||||
|
| `deploy-rs.rendered` | `deploy` |
|
||||||
|
|
||||||
|
`nixfiles` itself is also a flake output, so anything in the top-level config can be addressed
|
||||||
|
directly — e.g. `build-iso` builds
|
||||||
|
`.#nixfiles.config.nixos.systems."<host>".configuration.config.my.buildAs.iso`. The flake also
|
||||||
|
exposes `lib` (the extended unstable nixpkgs lib, including `lib.my`), `inputs`, `nixpkgs`
|
||||||
|
(the `pkgs'` channel sets), and `overlays.default` (the custom packages from
|
||||||
|
[`pkgs/`](../pkgs/default.nix)).
|
||||||
|
|
||||||
|
Per platform (`eachDefaultSystem`), the flake produces:
|
||||||
|
|
||||||
|
- `packages` — everything from `pkgs/`, flattened.
|
||||||
|
- `checks` — every `homeConfigurations.*.activationPackage` plus deploy-rs's own `deployChecks`.
|
||||||
|
- `devShells.default` — the `numtide/devshell` from [`devshell/`](../devshell) (see
|
||||||
|
[deployment.md](deployment.md)).
|
||||||
|
- `ci.<system>` — one attr per buildable thing (`system-<name>`, `home-<name>` with `@` mangled
|
||||||
|
to `-at-`, `package-<name>`, plus `shell`), consumed by CI; `ciDrv` is a `linkFarm` of all of
|
||||||
|
them.
|
||||||
|
|
||||||
|
## Systems: `systemOpts` and `mkSystem`
|
||||||
|
|
||||||
|
Each entry of `nixos.systems` is a submodule defined by `systemOpts` in
|
||||||
|
[`nixos/default.nix`](../nixos/default.nix):
|
||||||
|
|
||||||
|
| Option | Type / default | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `system` | enum of `defaultSystems` | Nix platform string, e.g. `"x86_64-linux"`. |
|
||||||
|
| `nixpkgs` | one of `unstable`/`stable`/`mine`/`mine-stable`, default `"unstable"` | nixpkgs channel for the system. |
|
||||||
|
| `home-manager` | same enum, defaults to `nixpkgs` | home-manager channel. |
|
||||||
|
| `hmNixpkgs` | same enum, defaults to `nixpkgs` | nixpkgs channel used for home-manager's own pkgs when it doesn't share the system's. |
|
||||||
|
| `docCustom` | bool, default `false` | Include nixfiles' custom modules in the generated NixOS manual (slow). |
|
||||||
|
| `assignments` | attrsOf `assignmentOpts` | The box's network assignments (see [networking.md](networking.md)). |
|
||||||
|
| `extraAssignments` | attrsOf attrsOf `assignmentOpts` | Extra assignments for things not on the box itself (e.g. the routers' floating VIP entries). |
|
||||||
|
| `configuration` | custom merged type | The actual NixOS configuration module(s); merging runs `mkSystem`. |
|
||||||
|
| `rendered` | unspecified, default `configuration` | What ends up in `nixosConfigurations.<name>` — overridden for boxes built as something other than a plain system (e.g. `installer` renders `config.my.asISO`, containers render `config.my.asContainer`). |
|
||||||
|
|
||||||
|
`mkSystem` does the real work:
|
||||||
|
|
||||||
|
1. It selects the channel's nixpkgs flake (`pkgsFlakes.${config'.nixpkgs}`) and imports
|
||||||
|
`nixos/lib/eval-config.nix` **by hand**, because the flake force-sets `lib` and
|
||||||
|
`eval-config.nix` would otherwise import its own unextended one. The lib it passes is the
|
||||||
|
channel's `pkgs.lib` extended with `lib.my` plus a `versionOverlay` that stamps
|
||||||
|
`system.nixos` with the flake revision and channel.
|
||||||
|
2. `specialArgs` gives every module access to `self`, `inputs`, `pkgsFlakes`, the box's own
|
||||||
|
`pkgsFlake`, `allAssignments` (every box's assignments), and `systems` (all of
|
||||||
|
`nixos.systems`). Passing these via `specialArgs` (rather than module `imports`) avoids
|
||||||
|
infinite recursion.
|
||||||
|
3. The module list is: the home-manager channel's `nixosModules.default`, every module in
|
||||||
|
`nixos.modules` (the shared modules, see below), an inline module, and the box's own
|
||||||
|
`configuration` definitions (wrapped with `inlineModule'` so error messages keep file
|
||||||
|
provenance).
|
||||||
|
4. The inline module wires the plumbing:
|
||||||
|
- `_module.args`: `secretsPath`, `vpns` (the flake-level `nixos.vpns`), the box's own
|
||||||
|
`assignments`, and `pkgs'` — an attrset of **all four nixpkgs channels** for this box's
|
||||||
|
platform, so a module can grab a package from another channel (e.g. `pkgs'.stable.foo`).
|
||||||
|
- `system.name` is the attribute name of the box.
|
||||||
|
- `networking.hostName` / `networking.domain` default from the `internal` assignment (see
|
||||||
|
[networking.md](networking.md)).
|
||||||
|
- `nixpkgs.system` is set and the overlays computed at flake level (lib overlay + custom
|
||||||
|
packages) are passed through, so `pkgs` is imported modularly with config and overlays
|
||||||
|
applied.
|
||||||
|
- home-manager integration: `useGlobalPkgs` defaults to true when the system and
|
||||||
|
home-manager channels match (a warning is emitted when they deliberately differ);
|
||||||
|
`sharedModules` is every module in `home-manager.modules` plus an inline module that
|
||||||
|
passes `pkgsPath` / `pkgs'` into home-manager, disables the release check, and pins
|
||||||
|
`home.stateVersion` via `homeStateVersion` (currently `22.11` for stable-flavoured
|
||||||
|
channels, `23.05` otherwise).
|
||||||
|
5. Finally `applyAssertions` throws with all failed assertion messages (and shows warnings)
|
||||||
|
when the merged configuration is forced.
|
||||||
|
|
||||||
|
## The four nixpkgs channels
|
||||||
|
|
||||||
|
`flake.nix` builds `pkgsFlakes` (nixpkgs) and `hmFlakes` (home-manager):
|
||||||
|
|
||||||
|
| Channel | nixpkgs input | home-manager input |
|
||||||
|
|---|---|---|
|
||||||
|
| `unstable` | `nixpkgs/nixos-unstable` | `home-manager` (master) |
|
||||||
|
| `stable` | `nixpkgs/nixos-26.05` | `home-manager/release-26.05` |
|
||||||
|
| `mine` | `github:devplayer0/nixpkgs/devplayer0` (personal fork) | alias of `unstable` (no fork exists) |
|
||||||
|
| `mine-stable` | `github:devplayer0/nixpkgs/devplayer0-stable` | alias of `stable` |
|
||||||
|
|
||||||
|
Each channel's `lib` is extended with the `libOverlay` (`lib.my` + flake-utils). Two package
|
||||||
|
sets are built per channel:
|
||||||
|
|
||||||
|
- `pkgs'` — with the devshell/ragenix/deploy-rs/home-manager overlays; used for the dev shell
|
||||||
|
and `packages`, and exposed as the flake's `nixpkgs` output.
|
||||||
|
- `configPkgs'` — with just the lib + custom-packages overlays and an `allowUnfreePredicate`
|
||||||
|
(Widevine / Chromium); this is the `pkgs'` threaded into the top-level evaluation, from
|
||||||
|
which each box's `pkgs` and per-channel `pkgs'` module args (and home-manager's base
|
||||||
|
`pkgs`) are taken.
|
||||||
|
|
||||||
|
A box picks its channel with `nixpkgs = "mine"` (most boxes), `"mine-stable"` (e.g. `colony`),
|
||||||
|
etc. Inside a module, `pkgs` is the selected channel and `pkgs'` is the attrset of all four
|
||||||
|
(`pkgs'.<channel>.<attr>`).
|
||||||
|
|
||||||
|
## `lib.my` and the `my.*` namespace
|
||||||
|
|
||||||
|
[`lib/default.nix`](../lib/default.nix) extends nixpkgs `lib` with a `my` attrset. The
|
||||||
|
flake-level `lib` (`pkgsFlakes.unstable.lib` so extended) is for platform-independent flake
|
||||||
|
use only; each box gets its own channel's lib extended the same way. Contents:
|
||||||
|
|
||||||
|
- **Option helpers** — `mkOpt'`, `mkBoolOpt'`, `nullOrOpt'`, `mkDefault'` (`mkOverride 900`,
|
||||||
|
slightly stronger than `mkDefault`), `mkVMOverride'`, `inlineModule'` / `inlineModule`
|
||||||
|
(attach `_file` provenance), `commonOpts` (the shared `system`/`nixpkgs`/`home-manager`
|
||||||
|
options and the `moduleType` used for exported modules), `applyAssertions`, `duplicates`.
|
||||||
|
- **`lib.my.net`** — CIDR/IP math (`net.cidr.host`, `net.cidr.subnet`, `net.types.ipv4`, …)
|
||||||
|
from the `libnetRepo` input (`oddlama/nixos-extra-modules`' `netu.nix`). Used for nearly all
|
||||||
|
address arithmetic; addresses are almost never written literally.
|
||||||
|
- **`lib.my.c`** — shared constants from [`lib/constants.nix`](../lib/constants.nix): static
|
||||||
|
UID/GID assignments, kernel package selection (`kernel.lts`/`kernel.latest`), nginx config
|
||||||
|
snippets, networkd snippets (`networkd.noL3`), the binary-cache settings (`nix.cache`),
|
||||||
|
and the per-site domains/prefixes/VIPs described in [networking.md](networking.md).
|
||||||
|
- **`lib.my.dns`** — zone-generation helpers from [`lib/dns.nix`](../lib/dns.nix): `fwdRecords`
|
||||||
|
/ `ptrRecords` / `ptr6Records` rendered from `allAssignments`, plus the LUA-record helpers
|
||||||
|
(`ifaceA`, `lookupIP`) used by the home routers.
|
||||||
|
- **networkd helpers** — `networkdAssignment` (assignment → `systemd.network` network, see
|
||||||
|
[networking.md](networking.md)), `mkVLAN`, `dockerNetAssignment`.
|
||||||
|
- **Misc** — `mkDefaultSystemsPkgs`, `flakePackageOverlay` (wrap another flake's package as an
|
||||||
|
overlay), `isIPv6` / `parseIPPort` / `netBroadcast`, `nft` chain-name helpers,
|
||||||
|
`systemdAwaitPostgres`, `vm.*` LVM disk descriptors for the `vms` module, the typed
|
||||||
|
`deploy-rs` option definitions (`lib.my.deploy-rs`), `netbootKeaClientClasses` (iPXE/EFI
|
||||||
|
DHCP client classes), and `homeStateVersion`.
|
||||||
|
|
||||||
|
Custom NixOS / home-manager modules declare their options under the `my.*` namespace (the root
|
||||||
|
`options.my` is declared in `nixos/modules/common.nix` / `home-manager/modules/common.nix`) —
|
||||||
|
e.g. `my.secrets`, `my.build`, `my.tmproot`, `my.firewall`, `my.server`, `my.deploy`,
|
||||||
|
`my.vms`, `my.containers`.
|
||||||
|
|
||||||
|
## Shared modules
|
||||||
|
|
||||||
|
Registered in [`nixos/modules/_list.nix`](../nixos/modules/_list.nix) and applied to **every**
|
||||||
|
system (box files opt in per-feature via `my.*` options):
|
||||||
|
|
||||||
|
| Module | Provides |
|
||||||
|
|---|---|
|
||||||
|
| `common` | Baseline for all boxes: imports the impermanence, ragenix (age), sharry, copyparty and harmonia NixOS modules; pins `system.stateVersion`; `doas` instead of `sudo`; immutable users; nix settings (flakes, `ca-derivations`, the `nix-cache.nul.ie` substituter); declares the `my` option root. |
|
||||||
|
| `user` | `my.user` — the primary user: `users.users` + matching `home-manager.users` entry, wheel/doas, SSH authorized key from `.keys/me.pub`, shell taken from the home config, home persistence under tmproot. |
|
||||||
|
| `build` | `my.build` — alternate build targets via `extendModules`: `my.buildAs.devVM` (QEMU dev VM), `iso`, `container`, `kexecTree`, `netbootTree`/`netbootArchive`; `my.build.isDevVM` marker; `allHardware` profile toggle. |
|
||||||
|
| `dynamic-motd` | `my.dynamic-motd` — runs a script via `pam_exec` to generate the MOTD on login/ssh. |
|
||||||
|
| `tmproot` | `my.tmproot` — tmpfs `/` plus impermanence persistence (`persistence.dir`), with a `tmproot-unsaved` helper that walks the root tmpfs and lists files not covered by persistence. |
|
||||||
|
| `firewall` | `my.firewall` — nftables firewall: `tcp.allowed`/`udp.allowed` port lists, `trustedInterfaces`, NAT with `forwardPorts`, `extraRules` escape hatch; sane ICMP/ICMPv6/PMTUD rules baked in. |
|
||||||
|
| `server` | `my.server.enable` — server commonalities: getty autologin, LLMNR off, tightened fstrim timers, GUI and NixOS documentation off. |
|
||||||
|
| `deploy-rs` | `my.deploy` — per-box deploy-rs node/profile generation and the `deploy` user; see [deployment.md](deployment.md). |
|
||||||
|
| `secrets` | `my.secrets` — ragenix/agenix wiring: `files` to decrypt from `secrets/`, host `key` to encrypt for, identity paths derived from the OpenSSH host keys (persistence-aware), dev-key identity inside dev VMs. |
|
||||||
|
| `containers` | `my.containers.instances` — `systemd-nspawn` NixOS containers whose systems come from `nixos.systems` rendered `asContainer`, with bridge/macvlan networking, bind mounts, `hotReload` (reload instead of reboot) and a placeholder "dummy" init before first deploy. |
|
||||||
|
| `vms` | `my.vms.instances` — QEMU/KVM VMs as systemd units: TAP/bridge networking, LVM-backed disks, VFIO host-device passthrough (with udev tagging), UEFI, SPICE/TTY/QMP unix sockets under `/run/vms/`, clean shutdown via QMP `system_powerdown`. |
|
||||||
|
| `network` | Baseline networking: networkd only (`useDHCP = false`), IPv6 on, resolved domain/negative-cache settings; dev VMs get DHCP on `eth0` and an SSH port forward. |
|
||||||
|
| `pdns` | `my.pdns` — PowerDNS: authoritative server driven by BIND-style zone files (with templating and serial management) and recursor extra-settings wiring. |
|
||||||
|
| `nginx-sso` | `my.nginx-sso` — runs `nginx-sso` (instead of the stock NixOS module) and generates per-instance nginx `auth_request` include files. |
|
||||||
|
| `gui` | `my.gui.enable` (default on) — desktop baseline: graphics, polkit, swaylock PAM entry, Android udev rules, screenshot tmpdir. |
|
||||||
|
| `l2mesh` | Consumes `nixos.vpns.l2` — builds the VXLAN + IPsec layer-2 meshes between edge routers; see [networking.md](networking.md). |
|
||||||
|
| `borgthin` | `my.borgthin.jobs` — borg backups of thin-LVM snapshots to local or SSH repos on a systemd timer, with pruning. |
|
||||||
|
| `nvme` | `my.nvme` — sets the NVMe host NQN/hostid and, optionally, NVMe-oF-over-RDMA boot in the initrd. |
|
||||||
|
| `spdk` | `my.spdk` — SPDK target configuration (JSON RPC-driven) plus `spdk-rpc`/`spdk-setup`/`spdk-debug` helper tools. |
|
||||||
|
| `librespeed` | `my.librespeed` — a LibreSpeed speedtest: generated frontend (from the `librespeed-go` package in `pkgs/`) plus backend settings. |
|
||||||
|
| `netboot` | `my.netboot` — iPXE netboot server (TFTP/HTTP, menu) and the client-side loader that installs boot entries. |
|
||||||
|
|
||||||
|
Home-manager modules, registered in
|
||||||
|
[`home-manager/modules/_list.nix`](../home-manager/modules/_list.nix) and applied to every
|
||||||
|
home (including the per-user homes attached to systems via `my.user.homeConfig`):
|
||||||
|
|
||||||
|
| Module | Provides |
|
||||||
|
|---|---|
|
||||||
|
| `common` | Home baseline: `my.shell`, `my.ssh.authKeys`, `my.isStandalone` (home-manager running standalone vs inside NixOS), fish setup and completions generation, common programs. |
|
||||||
|
| `gui` | `my.gui` — the sway-based desktop: waybar, notifications, lock/saver plumbing (including the "doomsaver"), terminal and fonts. |
|
||||||
|
| `deploy-rs` | `my.deploy` for standalone homes — generates a `home` profile (home-manager activation) and deploy node; auto-disabled for NixOS-attached homes. |
|
||||||
|
| `swaync` | `my.swaync` — typed configuration module for Sway Notification Center. |
|
||||||
|
|
||||||
|
## Adding a box
|
||||||
|
|
||||||
|
1. Create a file (or directory with a `default.nix`) under [`nixos/boxes/`](../nixos/boxes)
|
||||||
|
that sets `nixos.systems.<name> = { system = "x86_64-linux"; nixpkgs = "mine"; assignments =
|
||||||
|
{ … }; configuration = { … }: { … }; };`. Nested boxes (VMs, containers) live under their
|
||||||
|
host's directory (e.g. `nixos/boxes/colony/vms/estuary/`).
|
||||||
|
2. Add the path to the `configs` list in [`flake.nix`](../flake.nix).
|
||||||
|
3. Give the box an `internal` assignment (name/domain) if it gets one — `hostName`/`domain`
|
||||||
|
default from it — and declare `my.secrets.key` if it has secrets.
|
||||||
|
4. Evaluate it with `check-system <name>` (cheap) before `build-system <name>`.
|
||||||
|
|
||||||
|
## Adding a shared module
|
||||||
|
|
||||||
|
1. Drop the file in [`nixos/modules/`](../nixos/modules) (or
|
||||||
|
[`home-manager/modules/`](../home-manager/modules)) with options under `my.*`, declared via
|
||||||
|
the `lib.my` helpers (`mkOpt'`, `mkBoolOpt'`).
|
||||||
|
2. Register it in the corresponding `_list.nix` (name → path). It is then applied to every
|
||||||
|
box and exported as `nixosModules.<name>` / `homeModules.<name>`.
|
||||||
|
|
||||||
|
## Home-manager
|
||||||
|
|
||||||
|
Home-manager has the same two-level structure: `homeOpts` (in
|
||||||
|
[`home-manager/default.nix`](../home-manager/default.nix)) defines `system`, `nixpkgs`,
|
||||||
|
`home-manager`, `homeDirectory`, `username`, and `configuration`; `mkHome` calls the selected
|
||||||
|
channel's `lib.homeManagerConfiguration`, passing the channel's `pkgs'` (with `config` emptied
|
||||||
|
so home-manager applies overlays/config modularly itself), `extraSpecialArgs` (`inputs`,
|
||||||
|
`pkgsFlakes`, `pkgsFlake`), every module in `home-manager.modules`, an inline module that sets
|
||||||
|
`home.homeDirectory` / `home.username` and provides `pkgs'` (all channels) as a module arg,
|
||||||
|
and the pinned `home.stateVersion`.
|
||||||
|
|
||||||
|
Standalone homes live in [`home-manager/configs/`](../home-manager/configs) and are named
|
||||||
|
`<user>@<host>` (deploy-rs mangles the `@` to `-at-`). `macsimum` is a home-manager-only
|
||||||
|
config — an `x86_64-darwin` macOS box with no NixOS side at all. Note that **no homes are
|
||||||
|
currently wired into the flake**: the `home-manager/configs/macsimum.nix` entry in `configs` is
|
||||||
|
commented out, and `home-manager/configs/castle.nix` exists on disk but is not listed either,
|
||||||
|
so `nixfiles.config.home-manager.homes` currently evaluates to `{}`. Most day-to-day user
|
||||||
|
configuration instead rides along with systems through `my.user.homeConfig`.
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# Deployment and workflows
|
||||||
|
|
||||||
|
How boxes get deployed, the devshell commands that drive everyday work, secrets, dev VMs, and
|
||||||
|
CI.
|
||||||
|
|
||||||
|
## deploy-rs
|
||||||
|
|
||||||
|
### Rendering
|
||||||
|
|
||||||
|
The top-level [`deploy-rs.nix`](../deploy-rs.nix) renders the flake's `deploy` output:
|
||||||
|
|
||||||
|
- It collects `nixos.systems` and `home-manager.homes` (mangling `@` in home names to
|
||||||
|
`-at-`, since deploy-rs node names can't contain `@`). An assertion rejects name collisions
|
||||||
|
between systems and homes.
|
||||||
|
- A system/home becomes a deploy node only when `configuration.config.my.deploy.enable` is
|
||||||
|
true. The node config is the *definitions* of the box's `my.deploy.node` option, re-imported
|
||||||
|
as a module so deploy-rs's submodule merging still applies.
|
||||||
|
- `autoRollback` and `magicRollback` are off globally; global `sshOpts` are
|
||||||
|
`[ "-i" ".keys/deploy.key" ]` (set in [`flake.nix`](../flake.nix)).
|
||||||
|
- The result is passed through `lib.my.deploy-rs.filterOpts`, which strips nulls so unset
|
||||||
|
options don't override deploy-rs defaults, and exposed as `deploy = deploy-rs.rendered`.
|
||||||
|
|
||||||
|
### Per-box nodes
|
||||||
|
|
||||||
|
The shared [`nixos/modules/deploy-rs.nix`](../nixos/modules/deploy-rs.nix) module provides
|
||||||
|
`my.deploy`:
|
||||||
|
|
||||||
|
- `my.deploy.enable` defaults to true, but is **automatically forced off for dev VMs and
|
||||||
|
NixOS containers** (`my.build.isDevVM` / `boot.isContainer` — containers are deployed
|
||||||
|
through their host instead, see below). The installer also disables it explicitly.
|
||||||
|
- Node defaults: `hostname` = the box's FQDN, `sshUser = "deploy"`, `user = "root"`,
|
||||||
|
`sudo = "doas -u"` (or `sudo -u`), `sshOpts` = the box's first OpenSSH port. The module
|
||||||
|
creates the `deploy` system user (bash shell, wheel, keys from `my.deploy.authorizedKeys`,
|
||||||
|
which defaults to `.keys/deploy.pub`).
|
||||||
|
- Generated profiles:
|
||||||
|
- **`system`** — activates `config.system.build.toplevel` with
|
||||||
|
`switch-to-configuration switch`, with workarounds for the `/tmp` cwd issue
|
||||||
|
(NixOS/nixpkgs#73404) and the systemd-boot `loader.conf` default-entry issue
|
||||||
|
(deploy-rs#31), then prunes old generations (`nix-env --delete-generations +10`,
|
||||||
|
tunable via `keepGenerations`, 0 disables).
|
||||||
|
- **`container-<name>`** — one per `my.containers.instances` entry, activating the
|
||||||
|
container's `my.buildAs.container` toplevel into
|
||||||
|
`/nix/var/nix/profiles/per-container/<name>/system`. With `hotReload` (default) the
|
||||||
|
profile *reloads* `systemd-nspawn@<name>` (restarting only if the container is down or
|
||||||
|
still running the placeholder "dummy" init); otherwise it restarts it. Generation cleanup
|
||||||
|
applies here too.
|
||||||
|
- Profiles deploy in order `system`, then the containers.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
- `deploy .#<host>` — the devshell's `deploy` is a wrapper that adds `--skip-checks`. Node
|
||||||
|
names are the system names (`deploy .#git`).
|
||||||
|
- `deploy-multi <hosts...>` — loops `deploy` over several nodes (extra args via `$O`).
|
||||||
|
- `deploy --boot .#<host>` — stages the config as the boot default **without** live-switching
|
||||||
|
(deploy-rs's `--boot` maps to the custom activation's `boot` phase, which also does the
|
||||||
|
generation cleanup). Use it when a live `switch` would cut the box off mid-change (e.g. a
|
||||||
|
router WAN rework), then reboot to cut over.
|
||||||
|
- `ssh-machine <name> [cmd]` — resolves `user@host` and the merged global+node `sshOpts` from
|
||||||
|
`.#deploy` (same `my.deploy.enable` gate as `deploy`, same `@` → `-at-` mangling), then
|
||||||
|
execs `ssh`. Boxes default to the `fish` login shell; pipe multi-statement remote scripts
|
||||||
|
through `bash` (`ssh-machine <name> bash -s < script.sh`). If outbound SSH hangs at the
|
||||||
|
publickey step (flaky agent), use `SSH_AUTH_SOCK= ssh-machine …`.
|
||||||
|
- `nix flake check` includes deploy-rs's own `deployChecks` for the whole `deploy` attrset.
|
||||||
|
|
||||||
|
## Devshell commands
|
||||||
|
|
||||||
|
The repo ships a `numtide/devshell` ([`devshell/`](../devshell), entered via `direnv`). Run a
|
||||||
|
command with no arguments for its help. From
|
||||||
|
[`devshell/commands.nix`](../devshell/commands.nix):
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `check-system <host> [nix args]` | Evaluates `.#nixosConfigurations."<host>".config.system.build.toplevel.drvPath` — catches module/option errors without building. Prefer this to validate a config change. |
|
||||||
|
| `build-system <host> [nix args]` | Builds the system's `toplevel` (extra args pass through to `nix build`). |
|
||||||
|
| `build-n-switch <args>` | `doas nixos-rebuild --flake .` (adds the repo as a git `safe.directory` for root first). |
|
||||||
|
| `build-home <name> [nix args]` | Builds `.#homeConfigurations."<name>".activationPackage`. |
|
||||||
|
| `home-switch [args]` | `home-manager switch --flake .`. |
|
||||||
|
| `deploy [args]` | `deploy-rs --skip-checks` (wrapper package in `devshell/default.nix`). |
|
||||||
|
| `deploy-multi <nodes...>` | Deploys several nodes in sequence. |
|
||||||
|
| `ssh-machine <name> [cmd]` | SSH to a system or home by name, resolving target/options from its deploy-rs node (see above). |
|
||||||
|
| `run-vm <host>` | Boots a system as a dev VM: installs `.keys/dev.key` into a temp `xchg/`, then `nix run`s `config.my.buildAs.devVM`. |
|
||||||
|
| `build-iso <host>` | Builds `config.my.buildAs.iso`. |
|
||||||
|
| `build-kexec <host>` | Builds `config.my.buildAs.kexecTree`. |
|
||||||
|
| `build-netboot <host>` | Builds `config.my.buildAs.netbootTree`. |
|
||||||
|
| `ragenix [args]` | `ragenix --identity .keys/dev.key` (see [Secrets](#secrets)). |
|
||||||
|
| `repl` | `nix repl .#`. |
|
||||||
|
| `fmt [args]` | `nixpkgs-fmt` (the canonical formatter). |
|
||||||
|
| `update-nixpkgs` | `nix flake update nixpkgs-{unstable,stable,mine,mine-stable}`. |
|
||||||
|
| `update-home-manager` | `nix flake update home-manager-{unstable,stable}`. |
|
||||||
|
| `update-installer` | Force-pushes the `installer` tag to trigger the installer release workflow. |
|
||||||
|
| `home-link` / `home-unlink` | Symlink (or remove) this `flake.nix` at `~/.config/home-manager/flake.nix` for standalone `home-manager` use. |
|
||||||
|
| `qemu-genmac` | Prints a random QEMU-suitable MAC (`52:54:00:xx:xx:xx`). |
|
||||||
|
| `ssh-get-ed25519 <host>` | Prints a host's ed25519 pubkey via `ssh-keyscan`. |
|
||||||
|
| `json2nix` | Converts JSON on stdin to formatted Nix. |
|
||||||
|
|
||||||
|
From [`devshell/install.nix`](../devshell/install.nix) (driven by `$INSTALLER`, the address of
|
||||||
|
a running custom installer, SSHing as root with `.keys/deploy.key`):
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `installer-shell [cmd]` | Runs a command (default: a shell) inside the installer. |
|
||||||
|
| `do-install [--no-bootloader] [--no-substitute] <system>` | Builds the system's `toplevel`, `nix copy`s it into the installer's target store, sets the system profile, and activates it with `switch-to-configuration boot` (with `NIXOS_INSTALL_BOOTLOADER=1` unless `--no-bootloader`). |
|
||||||
|
|
||||||
|
From [`devshell/vm-tasks.nix`](../devshell/vm-tasks.nix) (remote VM consoles; they forward the
|
||||||
|
VM's unix sockets from `/run/vms/<vm>/` on `<host>` over SSH):
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `vm-tty <host> <vm>` | Serial TTY of a VM in `minicom`. |
|
||||||
|
| `vm-monitor <host> <vm>` | QEMU monitor socket in `minicom`. |
|
||||||
|
| `vm-viewer <host> <vm>` | SPICE display in `virt-viewer` (not on Darwin). |
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
Secrets are age-encrypted files in [`secrets/`](../secrets), managed with **ragenix** (a fork
|
||||||
|
with a rekey flag, from the flake inputs).
|
||||||
|
|
||||||
|
- Each box declares `my.secrets.key` (the host public key its secrets encrypt to) and
|
||||||
|
`my.secrets.files.<name>` (files to decrypt; attribute values merge into the agenix secret,
|
||||||
|
e.g. `owner`/`mode`). At runtime the `secrets` module decrypts via
|
||||||
|
`age.secrets."<name>".path`, with identity paths derived from the box's OpenSSH host keys —
|
||||||
|
read from the tmproot persistence dir when there is one, since agenix runs before persisted
|
||||||
|
keys would otherwise be available.
|
||||||
|
- [`secrets.nix`](../secrets.nix) (the ragenix rules file at the repo root) is **generated**:
|
||||||
|
it evaluates the flake, collects every system's `my.secrets.files` + `key`, and emits each
|
||||||
|
secret with its recipient list — always including `.keys/dev.pub` so the dev key can open
|
||||||
|
everything. Re-running `ragenix -r` re-keys after adding a box or secret.
|
||||||
|
- The `ragenix` devshell command wraps `ragenix --identity .keys/dev.key`; use it to
|
||||||
|
edit/rekey secrets.
|
||||||
|
- The `.keys/` directory holds the dev key (`dev.key`/`dev.pub`), the deploy key
|
||||||
|
(`deploy.key`/`deploy.pub`, authorized on every box's `deploy` user), and assorted other
|
||||||
|
keys referenced by `lib.my.c.sshKeyFiles`. It is required for editing secrets, deploying,
|
||||||
|
and running dev VMs.
|
||||||
|
|
||||||
|
## Dev VMs
|
||||||
|
|
||||||
|
Any system can be built as a throwaway QEMU VM via `config.my.buildAs.devVM` (the `build`
|
||||||
|
module extends the config with `qemu-vm.nix` and sets `my.build.isDevVM`). `run-vm <host>`
|
||||||
|
creates a temp dir, installs `.keys/dev.key` as `xchg/dev.key`, and runs the VM; inside, the
|
||||||
|
`secrets` module switches `age.identityPaths` to that dev key (`my.secrets.vmKeyPath`,
|
||||||
|
default `/tmp/xchg/dev.key`), so dev VMs can decrypt the boxes' secrets without the real host
|
||||||
|
keys. Dev VMs also get DHCP on `eth0`, an SSH port forward (host 2222 → guest 22), and are
|
||||||
|
automatically excluded from deploy targets.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
GitHub/Gitea Actions workflows live in [`.gitea/workflows/`](../.gitea/workflows):
|
||||||
|
|
||||||
|
- **`ci.yaml`** (push to `master`): `nix flake check --no-build`, then for every attribute of
|
||||||
|
`.#ci.x86_64-linux` (each system as `system-<name>`, each home as `home-<name>` with `@` →
|
||||||
|
`-at-`, each package as `package-<name>`, plus the dev `shell`) it builds and pushes the
|
||||||
|
result to the Harmonia binary cache with [`ci/push-to-cache.sh`](../ci/push-to-cache.sh).
|
||||||
|
Finally it builds `.#ciDrv.x86_64-linux` (a `linkFarm` of all CI attrs) and pushes it with
|
||||||
|
`UPDATE_PROFILE=1`, which updates the `nixfiles` profile on the cache box and garbage
|
||||||
|
collects paths older than 60 days. The cache is `ssh-ng://harmonia@object-ctr.ams1.int.nul.ie`
|
||||||
|
(remote store `/var/lib/harmonia`), keyed by the `HARMONIA_SSH_KEY` secret with a pinned
|
||||||
|
`ci/known_hosts`; clients consume it as `https://nix-cache.nul.ie` (see `lib.my.c.nix.cache`).
|
||||||
|
- **`installer.yaml`** (push of the `installer` tag; `update-installer` refreshes it): builds
|
||||||
|
the installer's `my.buildAs.iso` and `my.buildAs.netbootArchive` and attaches both to a
|
||||||
|
release.
|
||||||
|
- **`update-docs.yaml`** (push to the docs branch, skipping its own commits): runs
|
||||||
|
`nix run .#update-docs-assignments` and commits any changes as
|
||||||
|
`docs: update assignment tables`.
|
||||||
|
|
||||||
|
### The docs assignment-table updater
|
||||||
|
|
||||||
|
`update-docs-assignments` is registered in [`pkgs/default.nix`](../pkgs/default.nix) (a
|
||||||
|
`writeShellScriptBin` wrapping [`ci/update-docs-assignments.py`](../ci/update-docs-assignments.py)).
|
||||||
|
It evaluates `.#nixfiles.config.nixos.allAssignments` to JSON, walks `docs/**/*.md`, and for
|
||||||
|
each file looks for a marker line `<!-- assignments: <box> -->` where `<box>` is the **file's
|
||||||
|
own stem** (e.g. `river.md` → `river`) and names a real box. The generated table (columns
|
||||||
|
`Name | Assignment | IPv4 | IPv6 | Domain | Notes`) is (re)written between the marker and a
|
||||||
|
closing `<!-- assignments-end -->` line, with an `<!-- assignments-start -->` line inserted
|
||||||
|
after the marker on the first run. Hand-written text in the **Notes** column is preserved
|
||||||
|
across runs (keyed by the Assignment cell), so notes survive regeneration. Pages that want a
|
||||||
|
table only need the marker + end marker; pages without markers (including the three top-level
|
||||||
|
docs) are left alone. The script exits non-zero when it changed something, which is how the
|
||||||
|
workflow knows whether to commit.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# installer
|
||||||
|
|
||||||
|
The custom NixOS installer image used to bootstrap new boxes.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/installer.nix`](../../nixos/installer.nix)
|
||||||
|
- **Host:** — (a build target, not a deployed box)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Defines `nixos.systems.installer`, a minimal server system rendered as a bootable ISO via
|
||||||
|
`config.my.buildAs.iso` (`my.asISO`); the same base can also be built as a kexec or netboot
|
||||||
|
tree.
|
||||||
|
- Build it with the devshell commands: `build-iso installer` (or `build-kexec installer` /
|
||||||
|
`build-netboot installer`). The `update-installer` command force-tags `installer` to
|
||||||
|
trigger a release rebuild in CI.
|
||||||
|
|
||||||
|
## Image contents
|
||||||
|
|
||||||
|
- Broad hardware support (`my.build.allHardware` pulls in the nixpkgs all-hardware profile);
|
||||||
|
EFI- and USB-bootable, zstd-compressed squashfs. Volume ID
|
||||||
|
`jackos-<release>-<arch>`, menu label "/dev/player0 Installer", image base name
|
||||||
|
`jackos-installer`.
|
||||||
|
- Root SSH with the deploy key authorized (`PermitRootLogin prohibit-password`); a random
|
||||||
|
`installer-<hex>` hostname is set at boot.
|
||||||
|
- `INSTALL_ROOT=/mnt` in the session environment, plus a `show-hw-config` alias wrapping
|
||||||
|
`nixos-generate-config --show-hardware-config --root $INSTALL_ROOT`.
|
||||||
|
- NixOS documentation enabled, `wpa_supplicant` available but not started, aggressive GC
|
||||||
|
(`GC_INITIAL_HEAP_SIZE=1M`) and `vm.overcommit_memory=1` for low-memory targets, LVM thin
|
||||||
|
and NFS support.
|
||||||
|
- No regular user (`my.user.enable = false`), no tmpfs-root management, no NAT, and not a
|
||||||
|
deploy target (`my.deploy.enable = false`).
|
||||||
|
|
||||||
|
## Installing a box
|
||||||
|
|
||||||
|
The devshell's installer commands ([`devshell/install.nix`](../../devshell/install.nix)) drive
|
||||||
|
an install over SSH against a booted installer reachable at `$INSTALLER`:
|
||||||
|
|
||||||
|
- `installer-shell` — get a shell on the installer.
|
||||||
|
- `do-install <system>` — builds the system's toplevel, `nix copy`s the closure to the
|
||||||
|
installer's `$INSTALL_ROOT` remote store, sets the system profile, touches `/etc/NIXOS`,
|
||||||
|
and runs `switch-to-configuration boot` with `NIXOS_INSTALL_BOOTLOADER=1` (skip the
|
||||||
|
bootloader with `--no-bootloader`, skip substitution with `--no-substitute`).
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Mobile boxes
|
||||||
|
|
||||||
|
Portable workstations that move between networks — currently just `tower`. Mobile boxes have
|
||||||
|
no static network assignments; they use DHCP/NetworkManager and reach other boxes over the
|
||||||
|
tailnet.
|
||||||
|
|
||||||
|
| Box | What it is | Docs |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `tower` | Framework Laptop 13 workstation | [tower.md](tower.md) |
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# tower
|
||||||
|
|
||||||
|
Portable workstation — a Framework Laptop 13 (Intel), running the full GUI environment.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/tower/default.nix`](../../nixos/boxes/tower/default.nix)
|
||||||
|
- **Host:** physical (laptop)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Personal portable workstation: `my.gui.enable`, with Sway managed by home-manager.
|
||||||
|
- Joins the tailnet through the headscale on [`britway`](../remote/britway.md) (fish abbr
|
||||||
|
`tsup` = `doas tailscale up --login-server=https://hs.nul.ie --accept-routes`).
|
||||||
|
|
||||||
|
## Hardware / platform
|
||||||
|
|
||||||
|
- Intel platform: microcode updates, `kvm-intel`, `intel_iommu=on`, `intel-media-driver` for
|
||||||
|
graphics; latest kernel (`lib.my.c.kernel.latest`).
|
||||||
|
- Thunderbolt security (`bolt`), Bluetooth (`blueman` + tray applet), fingerprint reader
|
||||||
|
(`fprintd`; `doas` persists auth for `wheel`).
|
||||||
|
- `tlp` power management, including battery charge thresholds (start 90% / stop 97%).
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
- Two LUKS-encrypted partitions, `persist` and `home` (both `allowDiscards`); `/nix` is a
|
||||||
|
separate ext4 filesystem, `/boot` is vfat.
|
||||||
|
- Persistent `/home` (`my.user.tmphome = false`) with an 8G tmpfs root
|
||||||
|
(`my.tmproot.size = "8G"`).
|
||||||
|
|
||||||
|
## Networking / services
|
||||||
|
|
||||||
|
- NetworkManager (`wpa_supplicant` backend) with `systemd-resolved`; networkd `wait-online`
|
||||||
|
is disabled. The Wi-Fi interface is renamed to `wifi` by MAC.
|
||||||
|
- Steam and Wireshark enabled; `fstrim`, LVM thin provisioning.
|
||||||
|
- `nix.gc.automatic = false` — GC is run manually on the laptop.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/tower/default.nix`](../../nixos/boxes/tower/default.nix) — the whole box (single file).
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
# Networking
|
||||||
|
|
||||||
|
This page describes how addressing works across the boxes: the assignment mechanism, the
|
||||||
|
per-site domains and prefixes, the home router HA pair, and the overlays/tunnels that tie the
|
||||||
|
sites together. Switch-level home topology (jim/dave/brian, the ONT path) lives in
|
||||||
|
[sites/home/switches.md](sites/home/switches.md).
|
||||||
|
|
||||||
|
## Assignments
|
||||||
|
|
||||||
|
Every box declares `nixos.systems.<name>.assignments`, an attrset of *assignments* (one per
|
||||||
|
network the box is attached to). The option shape (`assignmentOpts` in
|
||||||
|
[`nixos/default.nix`](../nixos/default.nix)):
|
||||||
|
|
||||||
|
- `name` (defaults to the attribute name) and `altNames` — DNS names for the assignment.
|
||||||
|
- `visible` (default `true`) — whether DNS helpers include it.
|
||||||
|
- `domain` — DNS suffix for this assignment.
|
||||||
|
- `mtu` — interface MTU (applied via the network's `linkConfig.MTUBytes`).
|
||||||
|
- `ipv4.address` / `ipv4.mask` (default 24) / `ipv4.gateway` (defaults to host 1 of the
|
||||||
|
prefix; set explicitly to `null` when there is no gateway) / `ipv4.genPTR`.
|
||||||
|
- `ipv6.address` (nullable — an assignment can be v4-only) / `ipv6.mask` (default 64) /
|
||||||
|
`ipv6.iid` (SLAAC static token instead of a full address) / `ipv6.gateway` / `ipv6.genPTR`.
|
||||||
|
|
||||||
|
`extraAssignments` is a second, nested level for addresses that belong *to* a network but not
|
||||||
|
to any single box — the home routers use it for their floating VIP entries (`router-hi`,
|
||||||
|
`router-lo`, `router-ut`).
|
||||||
|
|
||||||
|
All assignments are aggregated into `nixos.allAssignments` — every system's `assignments`
|
||||||
|
merged with every system's `extraAssignments` — and passed to every module as the
|
||||||
|
`allAssignments` argument, so any box can route to any other box's addresses without
|
||||||
|
hardcoding. A flake-wide assertion fails evaluation if any IPv4 or IPv6 address appears in
|
||||||
|
more than one assignment. Each box also receives its own assignments as the `assignments`
|
||||||
|
module argument.
|
||||||
|
|
||||||
|
Two pieces of machinery consume assignments:
|
||||||
|
|
||||||
|
- `lib.my.networkdAssignment` ([`lib/default.nix`](../lib/default.nix)) renders an assignment
|
||||||
|
as a `systemd.network` network: static `address`/`gateway`, MTU, LLDP, and IPv6 RA handling
|
||||||
|
(`IPv6AcceptRA` when there's no static gateway or a static `iid` is set, with
|
||||||
|
`Token = static:<iid>`).
|
||||||
|
- `mkSystem` defaults `networking.hostName` to `assignments.internal.name` (falling back to
|
||||||
|
the system name) and `networking.domain` to `assignments.internal.domain`. The shared
|
||||||
|
`network` module sets a fallback domain of `int.nul.ie` for boxes without one.
|
||||||
|
|
||||||
|
## Domains
|
||||||
|
|
||||||
|
The public domain is `nul.ie` (`lib.my.c.pubDomain`). Each site has its own internal domain
|
||||||
|
(constants in [`lib/constants.nix`](../lib/constants.nix)):
|
||||||
|
|
||||||
|
| Site | Domain |
|
||||||
|
|---|---|
|
||||||
|
| colony | `ams1.int.nul.ie` |
|
||||||
|
| home | `h.nul.ie` |
|
||||||
|
| britway | `lon1.int.nul.ie` |
|
||||||
|
| britnet | `bhx1.int.nul.ie` |
|
||||||
|
| kelder | `hentai.engineer` |
|
||||||
|
|
||||||
|
## colony
|
||||||
|
|
||||||
|
The colony box is a hosted server in Amsterdam (`ams1`); its public edge is the `estuary` VM
|
||||||
|
(`94.142.240.44`, `2a02:898:0:20::329:1`), which NATs and filters for everything behind it.
|
||||||
|
The internal prefixes (`lib.my.c.colony.prefixes`) are carved from `10.100.0.0/16` and
|
||||||
|
`2a0e:97c0:4d2:10::/60`:
|
||||||
|
|
||||||
|
| Network | IPv4 | IPv6 | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `base` | `10.100.0.0/24` | `2a0e:97c0:4d2:10::/64` | Base/management LAN (bridge on the host; estuary is `.1`) |
|
||||||
|
| `vms` | `10.100.1.0/24` | `2a0e:97c0:4d2:11::/64` | VM network (host is `.1`, hands out RAs) |
|
||||||
|
| `ctrs` | `10.100.2.0/24` | `2a0e:97c0:4d2:12::/64` | `systemd-nspawn` containers on the `shill` VM |
|
||||||
|
| `oci` | `10.100.3.0/24` | `2a0e:97c0:4d2:13::/64` | Podman/OCI workloads on the `whale2` VM |
|
||||||
|
| `qclk` | `10.100.4.0/24` | — | WireGuard endpoint instances in the `qclk` container |
|
||||||
|
|
||||||
|
On top of that: `p2pTunnels` (`10.100.5.0/24`) holds point-to-point tunnel /30s (see
|
||||||
|
[WireGuard tunnels](#wireguard-point-to-point-tunnels)); the `as211024` mesh gets
|
||||||
|
`10.100.50.0/24` + `2a0e:97c0:4df::/64` (see [the L2 mesh](#the-as211024-l2-mesh)); and the
|
||||||
|
`cust` block (`10.100.100.0/24`, `2a0e:97c0:4d2:2000::/56`) plus the `vip1`/`vip2`/`vip3`
|
||||||
|
public blocks and the per-customer `mail` / `darts` / `jam` prefixes carry customer-facing
|
||||||
|
services with their own public addresses (announced by BGP, routed via the host).
|
||||||
|
|
||||||
|
## home
|
||||||
|
|
||||||
|
The home site prefixes (`lib.my.c.home.prefixes`) come from `192.168.64.0/18` and
|
||||||
|
`2a0e:97c0:4d0::/60`, with VLAN IDs from `lib.my.c.home.vlans`:
|
||||||
|
|
||||||
|
| Network | VLAN | IPv4 | IPv6 | MTU | Purpose |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `core` | — (macvlan) | `192.168.64.0/24` | — | 1500 | Router-to-router/core link |
|
||||||
|
| `hi` | 100 | `192.168.68.0/22` | `2a0e:97c0:4d0:1::/64` | 9000 | High-speed LAN (jumbo frames) |
|
||||||
|
| `lo` | 110 | `192.168.72.0/21` | `2a0e:97c0:4d0:2::/64` | 1500 | General LAN |
|
||||||
|
| `untrusted` | 120 | `192.168.80.0/24` | `2a0e:97c0:4d0:3::/64` | 1500 | Untrusted / IoT |
|
||||||
|
| `modem` | 130 (`wan`) | `192.168.0.0/24` | — | — | Virgin Media modem management (stream) |
|
||||||
|
| `ont` | 140 (`wan-pon-ont`) | `192.168.100.0/24` | — | — | Digiweb ONT management (river) |
|
||||||
|
|
||||||
|
Two more WAN-side VLANs exist: `pon-isp` (10), the ISP VLAN Digiweb delivers at the ONT and
|
||||||
|
which is trunked untranslated to river, and `wan-pon-isp` (141), reserved for a future
|
||||||
|
multi-ONT translation scheme — see
|
||||||
|
[sites/home/switches.md](sites/home/switches.md) for the fabric side.
|
||||||
|
|
||||||
|
The routers themselves (`river` = host 1, `stream` = host 2 in each prefix) are built from one
|
||||||
|
definition, [`nixos/boxes/home/routing-common`](../nixos/boxes/home/routing-common/default.nix),
|
||||||
|
parameterised by an index (`0` = river, `1` = stream) that derives per-box addresses, DHCP
|
||||||
|
pool splits, VRRP state/priority and DNS `ns` numbering.
|
||||||
|
|
||||||
|
### Router VIPs
|
||||||
|
|
||||||
|
Clients never use a router's real address: each client VLAN has a floating VIP
|
||||||
|
(`lib.my.c.home.vips`) that follows the VRRP master. The VIPs are also declared as
|
||||||
|
`extraAssignments` (`router-hi`/`router-lo`/`router-ut`) so they appear in `allAssignments`
|
||||||
|
and DNS:
|
||||||
|
|
||||||
|
| Assignment | IPv4 | IPv6 |
|
||||||
|
|---|---|---|
|
||||||
|
| `router-hi` | `192.168.71.254/22` | `2a0e:97c0:4d0:1::ffff/64` |
|
||||||
|
| `router-lo` | `192.168.79.254/21` | `2a0e:97c0:4d0:2::ffff/64` |
|
||||||
|
| `router-ut` | `192.168.80.254/24` | `2a0e:97c0:4d0:3::ffff/64` |
|
||||||
|
|
||||||
|
There is also a mesh-side VIP (`as211024`): `10.100.50.4` and `2a0e:97c0:4df:0:1::ffff`,
|
||||||
|
which the other sites use as their next-hop into the home prefixes.
|
||||||
|
|
||||||
|
### Router HA
|
||||||
|
|
||||||
|
- **VRRP via `keepalived`** ([`routing-common/keepalived.nix`](../nixos/boxes/home/routing-common/keepalived.nix)):
|
||||||
|
separate v4/v6 instances (router IDs 51/52) on the `lan-core` link; index 0 (river) starts
|
||||||
|
as `MASTER`, priorities are `255 - index`. Track scripts ping public v4/v6 anycast targets
|
||||||
|
and demote a router whose WAN is down. All VIPs of a family move together.
|
||||||
|
- **Clients get the VIP as gateway *and* DNS.** `kea` hands out `vips.<vlan>.v4` as both
|
||||||
|
`routers` and `domain-name-servers`, with the two routers serving disjoint pool halves;
|
||||||
|
`radvd` advertises the v6 VIP as RDNSS (the `untrusted` VLAN gets Cloudflare instead) and
|
||||||
|
is started/stopped by keepalived's `notify_master`/`notify_backup` hooks so only the master
|
||||||
|
sends RAs.
|
||||||
|
- **`pdns-recursor` binds the VIPs directly** ([`routing-common/dns.nix`](../nixos/boxes/home/routing-common/dns.nix)),
|
||||||
|
with `net.ipv4.ip_nonlocal_bind` / `net.ipv6.ip_nonlocal_bind` so the backup can listen on
|
||||||
|
addresses it doesn't currently hold — failover doesn't depend on client resolver timeouts.
|
||||||
|
The recursor forwards the site's zones to the local authoritative PowerDNS on `127.0.0.1:5353`.
|
||||||
|
- **`wan-online.target`** is a shared, initially-inert systemd target meaning "the public
|
||||||
|
IPv4 WAN route is up". `routing-common` only declares it; each box wires how it's reached —
|
||||||
|
stream gates it on a oneshot that waits for the DHCP default route on `wan`, river's `pppd`
|
||||||
|
`ip-up`/`ip-down` hooks start/stop it. Consumers (e.g. `ipsec`, the RA-default-route
|
||||||
|
cleanup) attach **to** it with `wantedBy` + `partOf` + `after`, never `requires`/`wants`,
|
||||||
|
so the target is never pulled in early and services re-load on WAN flap.
|
||||||
|
|
||||||
|
### WAN paths (summary)
|
||||||
|
|
||||||
|
- **river** (a VM on `palace`): PPPoE to Digiweb via `services.pppd`, running directly on the
|
||||||
|
ISP's VLAN 10 (`wan-pon-isp`, trunked untranslated through the switches; baby-jumbo MTU 1508
|
||||||
|
so the PPP session is a clean 1500). The static IP is requested in IPCP; the pppd hooks own
|
||||||
|
`wan-online.target`. The ONT's management subnet (`192.168.100.0/24`) is reached on
|
||||||
|
`wan-pon-ont` (VLAN 140, PVID'd at the brian switch), where river takes `.100`.
|
||||||
|
- **stream** (bare metal): DHCP on the Virgin Media cable modem (VLAN 130) on `wan`, with a
|
||||||
|
static modem-management address (`192.168.0.100/24`) alongside the public lease, and CAKE
|
||||||
|
egress/ingress shaping via `wan-ifb`.
|
||||||
|
|
||||||
|
The full fabric story — which switch port carries what, why VLAN 10 is trunked untranslated,
|
||||||
|
and the multi-ONT plan — is in [sites/home/switches.md](sites/home/switches.md); the
|
||||||
|
`my.homeRouter.*` options (`dns.wanSkipBroadcasts`, `firewall.untrustedRejectV4`) let each box
|
||||||
|
tell `routing-common` about subnets sharing its WAN interface.
|
||||||
|
|
||||||
|
## The AS211024 L2 mesh
|
||||||
|
|
||||||
|
The edge routers are joined by a layer-2 mesh, defined once as `nixos.vpns.l2.as211024` in
|
||||||
|
[`nixos/boxes/colony/vms/estuary/default.nix`](../nixos/boxes/colony/vms/estuary/default.nix)
|
||||||
|
and realised on each member by the [`l2mesh` module](../nixos/modules/l2mesh.nix):
|
||||||
|
|
||||||
|
- **Members**: `estuary`, `river`, `stream`, `britway`, peering on their public addresses.
|
||||||
|
- **Transport**: VXLAN (VNI 211024, UDP port 4789) with static per-peer FDB entries and
|
||||||
|
UDP-encapsulated IPsec in transport mode via Libreswan (authentication-only by default;
|
||||||
|
`security.encrypt` would switch ESP from `null-sha256` to AES-GCM). The PSK is the shared
|
||||||
|
`l2mesh/as211024.key` secret, expanded into `/run/l2mesh.secrets` at `ipsec` start.
|
||||||
|
- **Overlay addressing**: `10.100.50.0/24` / `2a0e:97c0:4df::/64`; the interface MTU is
|
||||||
|
computed from the physical MTU minus VXLAN/UDP/IPsec overhead. Each router holds
|
||||||
|
`10.100.50.<n>` (estuary `.1`, river `.2`, stream `.3`, britway `.5`).
|
||||||
|
- **Routing over it**: the home routers route the colony prefixes via estuary and the
|
||||||
|
Tailscale prefixes via britway; estuary and britway route the home prefixes via the
|
||||||
|
`10.100.50.4` VIP. The home IPv6 **default** route also runs over the mesh, via britway
|
||||||
|
(hence the recursor's IPv4-only upstream pinning noted in
|
||||||
|
[`routing-common/dns.nix`](../nixos/boxes/home/routing-common/dns.nix)). The `nftTrust`
|
||||||
|
snippet in `lib.my.c.as211024` lets trusted inter-site traffic (colony, home, mesh,
|
||||||
|
Tailscale prefixes) through the edge firewalls.
|
||||||
|
|
||||||
|
## BGP
|
||||||
|
|
||||||
|
Both edge routers run `bird2` as AS211024:
|
||||||
|
|
||||||
|
- **estuary** ([`bgp.nix`](../nixos/boxes/colony/vms/estuary/bgp.nix)): full table from
|
||||||
|
ColoClue (AS8283) over two sessions per family, plus IPv6 transit from iFog and Hurricane
|
||||||
|
Electric; peering at the Frys-IX, NL-ix and FogIXP route servers and with various networks
|
||||||
|
(Meta, Cloudflare, Apple, LUJE.net, …); a `bgp.tools` monitoring session. Static routes pull
|
||||||
|
the customer VIP blocks out of the `base` network and the internal/home IPv6 prefixes out of
|
||||||
|
the mesh.
|
||||||
|
- **britway** ([`bgp.nix`](../nixos/boxes/britway/bgp.nix)): Vultr transit (AS64515,
|
||||||
|
MD5-passworded from a secret) and a `bgp.tools` session, originating the internal, colony
|
||||||
|
and home IPv6 prefixes.
|
||||||
|
|
||||||
|
## WireGuard point-to-point tunnels
|
||||||
|
|
||||||
|
Separate from the mesh, estuary terminates several networkd-managed WireGuard tunnels (private
|
||||||
|
keys in per-box secrets):
|
||||||
|
|
||||||
|
| Tunnel | Port | Remote / prefix | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| estuary ↔ kelder | 51820 | kelder holds `94.142.242.254/32` | kelder's public presence is a colony /32 routed over the tunnel |
|
||||||
|
| estuary ↔ hillcrest | 51822 | `10.100.5.0/30` | point-to-point /30 out of `p2pTunnels` |
|
||||||
|
| estuary ↔ john-valorant | 51823 | `10.100.5.4/30` | point-to-point /30 out of `p2pTunnels` |
|
||||||
|
|
||||||
|
Additionally, the `qclk` container on `shill` runs its own WireGuard instances on port 51821
|
||||||
|
out of `10.100.4.0/24`, and `britnet` hosts a road-warrior style WireGuard VPN on port 51820
|
||||||
|
serving `10.200.0.0/24` / `fdfb:5ebf:6e84::/64`.
|
||||||
|
|
||||||
|
## Tailscale / headscale
|
||||||
|
|
||||||
|
Tailscale runs against a self-hosted **headscale** control plane on britway
|
||||||
|
([`tailscale.nix`](../nixos/boxes/britway/tailscale.nix)) at `https://hs.nul.ie`: Google OIDC
|
||||||
|
login, SQLite state, MagicDNS under `ts.nul.ie`, and split DNS that resolves the colony and
|
||||||
|
home internal domains through their site resolvers. The tailnet prefixes are
|
||||||
|
`100.64.0.0/10` / `fd7a:115c:a1e0::/48`.
|
||||||
|
|
||||||
|
Notable nodes:
|
||||||
|
|
||||||
|
- **waffletail** (container on `shill`) — the colony subnet router: advertises the colony
|
||||||
|
prefixes, acts as an exit node, and SNATs tailnet traffic into the colony networks.
|
||||||
|
- **britway** — advertises the home prefixes (routed via the mesh) and is also an exit node.
|
||||||
|
- Other boxes join with the shared `tailscale-auth.key` auth-key secret.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Remote boxes
|
||||||
|
|
||||||
|
The "remote" group covers the boxes that live outside the `colony` and `home` sites: the two
|
||||||
|
edge VPSes (`britway` in London, `britnet` in Birmingham) and the `kelder` site — a secondary
|
||||||
|
server at a remote location, linked back to colony over WireGuard and acting as a NixOS
|
||||||
|
container host.
|
||||||
|
|
||||||
|
| Box | What it is | Docs |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `britway` | Vultr VPS (London, `lon1`): Headscale control plane, Tailscale exit node, BGP edge, nginx | [britway.md](britway.md) |
|
||||||
|
| `britnet` | VPS (Birmingham, `bhx1`): Tailscale exit node / WireGuard hub | [britnet.md](britnet.md) |
|
||||||
|
| `kelder` | Secondary home server (`hentai.engineer`): container host, Samba, DDNS | [kelder.md](kelder.md) |
|
||||||
|
| `kelder-acquisition` | Media stack container on `kelder` (Transmission over VPN, *arr, Jellyfin) | [kelder-acquisition.md](kelder-acquisition.md) |
|
||||||
|
| `kelder-spoder` | Web container on `kelder` (Nextcloud + nginx reverse proxy) | [kelder-spoder.md](kelder-spoder.md) |
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# britnet
|
||||||
|
|
||||||
|
A small VPS in Birmingham (`bhx1`) acting as a second Tailscale/WireGuard egress point — a
|
||||||
|
narrower gateway role than [`britway`](britway.md) (no control plane, no BGP).
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/britnet.nix`](../../nixos/boxes/britnet.nix)
|
||||||
|
- **Host:** VPS (Birmingham, `bhx1`; provider uplink assignment `allhost`)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **Tailscale exit node** — logs into the headscale on [`britway`](britway.md)
|
||||||
|
(`--login-server=https://hs.nul.ie`) with `--advertise-exit-node`.
|
||||||
|
- **WireGuard hub** — `wg0` listens on UDP 51820 on the `vpn` network
|
||||||
|
(`10.200.0.0/24` / `fdfb:5ebf:6e84::/64`), with a single peer at `10.200.0.10` /
|
||||||
|
`fdfb:5ebf:6e84::10`.
|
||||||
|
- **NAT gateway** — traffic arriving on `tailscale0`/`wg0` is forwarded out `veth0` and SNATed
|
||||||
|
to the `allhost` v4/v6 addresses.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: britnet -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| britnet | allhost | `77.74.199.67/24 gw 77.74.199.1` | `2a12:ab46:5344:99::a/64 gw 2a12:ab46:5344::1` | bhx1.int.nul.ie | |
|
||||||
|
| britnet | vpn | `10.200.0.1/24` | `fdfb:5ebf:6e84::1/64` | — | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
- The provider interface is renamed to `veth0` by MAC. The v6 default gateway
|
||||||
|
(`2a12:ab46:5344::1`) sits off-subnet, so a link-scope route is added to reach it.
|
||||||
|
- `wg0` is a networkd WireGuard netdev keyed from `britnet/wg.key`; RA is disabled on it.
|
||||||
|
- Upstream DNS is hardcoded to Cloudflare (`1.1.1.1` / `1.0.0.1`).
|
||||||
|
- `iperf3` runs with an open port for bandwidth testing.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/britnet.nix`](../../nixos/boxes/britnet.nix) — the whole box (single file).
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# britway
|
||||||
|
|
||||||
|
A Vultr VPS in London (`lon1`) acting as the network edge node: the self-hosted Tailscale
|
||||||
|
control plane, a tailnet exit node, and the BGP speaker for AS211024.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/britway/`](../../nixos/boxes/britway)
|
||||||
|
- **Host:** VPS at Vultr (London, `lon1`)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **Headscale** — the self-hosted Tailscale control plane at `hs.nul.ie`; every other box's
|
||||||
|
`tailscaled` logs in here (`--login-server=https://hs.nul.ie`). Google OIDC for auth,
|
||||||
|
SQLite state, MagicDNS under `ts.nul.ie`, and split DNS pointing the colony/home domains
|
||||||
|
at their internal resolvers.
|
||||||
|
- **Tailscale exit node** — advertises `--advertise-exit-node` plus routes to the home v4/v6
|
||||||
|
prefixes; tailnet traffic is SNATed out `veth0` (v4 to the Vultr public IP, v6 to the
|
||||||
|
`as211024` mesh address).
|
||||||
|
- **BGP edge** — `bird2` speaks BGP as AS211024 to Vultr transit (AS64515, separate v4/v6
|
||||||
|
sessions authenticated with a password from `britway/bgp-password-vultr.conf`) and exports
|
||||||
|
everything to a `bgp.tools` monitoring session. It originates the internal, colony and home
|
||||||
|
v6 prefixes (`2a0e:97c0:4df::/48`, `2a0e:97c0:4d2::/48`, `2a0e:97c0:4d0::/48`).
|
||||||
|
- **nginx** — reverse proxy fronting headscale (`hs.nul.ie` → `localhost` headscale port),
|
||||||
|
with a wildcard ACME cert for `nul.ie` issued via Cloudflare DNS.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: britway -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| britway | as211024 | `10.100.50.5/24` | `2a0e:97c0:4df:0:2::1/64` | — | |
|
||||||
|
| britway | vultr | `45.76.141.188/23 gw 45.76.140.1` | `2001:19f0:7402:128b::1/64` | lon1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
- Two assignments: `vultr` on the provider interface `veth0` (renamed by MAC), and `as211024`
|
||||||
|
on the l2mesh VXLAN interface (`my.vpns.l2`, a mesh shared with `estuary` and the home
|
||||||
|
routers `river`/`stream`).
|
||||||
|
- Static routes steer colony/home v4 traffic over the `as211024` mesh. A separate `ts-extra`
|
||||||
|
routing table (1337) with a policy rule on `tailscale0` ingress sends Tailscale-sourced v6
|
||||||
|
traffic for colony via `estuary`, while the box's own v6 uses WAN.
|
||||||
|
- The firewall trusts the `as211024` prefixes (`lib.my.c.as211024.nftTrust`) and
|
||||||
|
`tailscale0`; `iperf3` runs with an open port for bandwidth testing.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/britway/default.nix`](../../nixos/boxes/britway/default.nix) — system, assignments, networkd, firewall/SNAT.
|
||||||
|
- [`nixos/boxes/britway/bgp.nix`](../../nixos/boxes/britway/bgp.nix) — `bird2` config (Vultr transit, `bgp.tools`).
|
||||||
|
- [`nixos/boxes/britway/nginx.nix`](../../nixos/boxes/britway/nginx.nix) — nginx vhosts + ACME.
|
||||||
|
- [`nixos/boxes/britway/tailscale.nix`](../../nixos/boxes/britway/tailscale.nix) — headscale + the tailnet node itself.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# kelder-acquisition
|
||||||
|
|
||||||
|
The media acquisition stack for the kelder site — Transmission (forced over VPN), the *arrs
|
||||||
|
and Jellyfin in one NixOS container.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/kelder/containers/acquisition/`](../../nixos/boxes/kelder/containers/acquisition)
|
||||||
|
- **Host:** NixOS container on [`kelder`](kelder.md)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **Transmission** (`transmission_4`) — BitTorrent client bound to the VPN
|
||||||
|
(`bindsTo systemd-networkd-wait-online@vpn.service`); peer port 26180 (forwarded in the
|
||||||
|
AirVPN config), 20 MiB/s down / 1 MiB/s up limits, ratio limit 2.0. Downloads land in
|
||||||
|
`/mnt/media/downloads/torrents` (`/mnt/media` is bind-mounted from the host's
|
||||||
|
`/mnt/storage/media`).
|
||||||
|
- **Jackett, Radarr, Sonarr** — indexer + media managers, in the shared `media` group with
|
||||||
|
`UMask 0002`.
|
||||||
|
- **Jellyfin** — streaming with Intel hardware transcoding: `/dev/dri` is bind-mounted from
|
||||||
|
the host, `intel-vaapi-driver`/`intel-ocl` are installed and the `jellyfin` user is in the
|
||||||
|
`render` group.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: kelder-acquisition -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| acquisition-ctr | internal | `172.16.64.2/24 gw 172.16.64.1` | — | hentai.engineer | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
- `internal` assignment (name `acquisition-ctr`) on the host's `ctrs` bridge, MTU 1460 to
|
||||||
|
match the site WAN.
|
||||||
|
- All non-site traffic goes over an AirVPN WireGuard tunnel (`vpn` netdev, MTU 1320, AirVPN
|
||||||
|
IE endpoint) using fwmark 42 / route table 51820: policy rules keep traffic to and from the
|
||||||
|
kelder prefixes on the main table and push everything else via the VPN.
|
||||||
|
- An nftables input chain drops new TCP connections from the VPN interface except the
|
||||||
|
Transmission peer port; the web UI ports (9091 Transmission, 9117 Jackett, 7878 Radarr,
|
||||||
|
8989 Sonarr, 8096 Jellyfin) are accepted from the site. When built as a dev VM, those ports
|
||||||
|
are forwarded to the host.
|
||||||
|
- Sonarr still needs the EOL .NET 6 runtime, allowed via
|
||||||
|
`nixpkgs.config.permittedInsecurePackages`.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/kelder/containers/acquisition/default.nix`](../../nixos/boxes/kelder/containers/acquisition/default.nix) — services and users.
|
||||||
|
- [`nixos/boxes/kelder/containers/acquisition/networking.nix`](../../nixos/boxes/kelder/containers/acquisition/networking.nix) — AirVPN tunnel + firewall.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# kelder-spoder
|
||||||
|
|
||||||
|
The web container for the kelder site: Nextcloud plus an nginx (OpenResty) reverse proxy for
|
||||||
|
the site's services.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/kelder/containers/spoder/`](../../nixos/boxes/kelder/containers/spoder)
|
||||||
|
- **Host:** NixOS container on [`kelder`](kelder.md)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **Nextcloud** (`nextcloud32`) at `cloud.hentai.engineer` (trusted alias
|
||||||
|
`cloud-local.hentai.engineer`), SQLite backend, data in `/mnt/storage/nextcloud`
|
||||||
|
(`/mnt/storage` is bind-mounted from the host).
|
||||||
|
- **nginx reverse proxy** (`openresty`) terminating TLS for the site's public vhosts, with a
|
||||||
|
wildcard ACME cert for `hentai.engineer` via Cloudflare DNS. The kelder host forwards
|
||||||
|
`http`/`https` to this container; uploads are unlimited (`clientMaxBodySize = 0`) for
|
||||||
|
Nextcloud's sake.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: kelder-spoder -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| spoder-ctr | internal | `172.16.64.3/24 gw 172.16.64.1` | — | hentai.engineer | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Proxy vhosts
|
||||||
|
|
||||||
|
All under `hentai.engineer`, each with a `*-local` alias:
|
||||||
|
|
||||||
|
| vhost | Target | Auth |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `monitor` | `netdata` on the kelder host (:19999) | `htpasswd` |
|
||||||
|
| `kontent` | Jellyfin on `kelder-acquisition` (:8096, incl. websocket) | — |
|
||||||
|
| `torrents` | Transmission on `kelder-acquisition` (:9091) | `htpasswd` |
|
||||||
|
| `jackett` | Jackett on `kelder-acquisition` (:9117) | `htpasswd` |
|
||||||
|
| `radarr` | Radarr on `kelder-acquisition` (:7878) | `htpasswd` |
|
||||||
|
| `sonarr` | Sonarr on `kelder-acquisition` (:8989) | `htpasswd` |
|
||||||
|
| `cloud` | Nextcloud (local) | — |
|
||||||
|
|
||||||
|
An `init_worker_by_lua` timer polls `v4.ident.me` every 5 minutes to track the site's public
|
||||||
|
IP; it feeds `localRedirect` rewrites (bounce public-IP clients to the `*-local` name) that
|
||||||
|
are currently **disabled** (commented out — Virgin Media filters DNS answers containing local
|
||||||
|
IPs, so the split doesn't work as intended).
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
- `internal` assignment (name `spoder-ctr`) on the host's `ctrs` bridge, MTU 1420.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/kelder/containers/spoder/default.nix`](../../nixos/boxes/kelder/containers/spoder/default.nix) — Nextcloud + ACME.
|
||||||
|
- [`nixos/boxes/kelder/containers/spoder/nginx.nix`](../../nixos/boxes/kelder/containers/spoder/nginx.nix) — reverse proxy vhosts.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# kelder
|
||||||
|
|
||||||
|
Secondary home server at a remote site, domain `hentai.engineer`. Linked back to colony over
|
||||||
|
WireGuard and acting as a NixOS container host (like `shill`/`sfh`).
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/kelder/`](../../nixos/boxes/kelder)
|
||||||
|
- **Host:** physical (Intel; LTS kernel, `kvm-intel`, IOMMU on)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **Container host** — runs two NixOS containers on the `ctrs` bridge
|
||||||
|
(`my.containers.instances`): `kelder-acquisition` and `kelder-spoder` (below).
|
||||||
|
- **Public services via colony** — a WireGuard tunnel (`estuary` netdev) connects to colony's
|
||||||
|
`estuary` box, which DNATs public traffic to kelder's `/32`; policy routing (table 51820 +
|
||||||
|
connmark) sends replies back through the tunnel while ordinary traffic uses the LAN.
|
||||||
|
kelder's own NAT forwards `http`/`https` on to `kelder-spoder`.
|
||||||
|
- **Nextcloud host** — served from the `kelder-spoder` container.
|
||||||
|
- **Samba** — the `storage` share backed by `/mnt/storage`, with `nmbd` and `samba-wsdd` for
|
||||||
|
Windows discovery.
|
||||||
|
- **DDNS** — a `ddns-update` timer (every 10 min) runs `dns_update.py` to sync the
|
||||||
|
`hentai.engineer` and `kelder-local.hentai.engineer` Cloudflare records with the address on
|
||||||
|
`et1g0`.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: kelder -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| kelder-ctrs | ctrs | `172.16.64.1/24` | — | hentai.engineer | |
|
||||||
|
| kelder | estuary | `94.142.242.254/32` | — | — | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Containers
|
||||||
|
|
||||||
|
| Container | Role | Docs |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `kelder-acquisition` | Media stack (Transmission over AirVPN, Jackett/Radarr/Sonarr, Jellyfin) | [kelder-acquisition.md](kelder-acquisition.md) |
|
||||||
|
| `kelder-spoder` | Nextcloud + nginx reverse proxy | [kelder-spoder.md](kelder-spoder.md) |
|
||||||
|
|
||||||
|
The containers are not deploy targets (`my.deploy.enable = false`); they're managed through
|
||||||
|
the host.
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
- LAN on `et1g0` (renamed by MAC) with DHCP and MTU 1460 (`lib.my.c.kelder.ipv4MTU`); the
|
||||||
|
kelder v4 prefixes are masqueraded out of it.
|
||||||
|
- The `estuary` WireGuard peer (`AllowedIPs = 0.0.0.0/0`, keepalive 25s) is combined with
|
||||||
|
rules that keep LAN traffic on the main table and only route tunnel-marked/owned traffic
|
||||||
|
via table 51820.
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
- `netdata` (proxied as `monitor.hentai.engineer` by `kelder-spoder`), `smartd`, `fstrim`,
|
||||||
|
LVM thin provisioning.
|
||||||
|
- `minecraft-server` is present but **disabled** (`enable = false`); the firewall still opens
|
||||||
|
25565 tcp/udp.
|
||||||
|
- Primary user `kontent` (in the `storage`/`media` groups).
|
||||||
|
- Sets `system.nixos.distroName = "KelderOS"`, a custom Plymouth theme and an `amogus-beep`
|
||||||
|
boot jingle ([`boot.nix`](../../nixos/boxes/kelder/boot.nix)).
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/kelder/default.nix`](../../nixos/boxes/kelder/default.nix) — system, assignments, tunnel, NAT, containers.
|
||||||
|
- [`nixos/boxes/kelder/boot.nix`](../../nixos/boxes/kelder/boot.nix) — Plymouth theme + boot beep.
|
||||||
|
- [`nixos/boxes/kelder/containers/`](../../nixos/boxes/kelder/containers) — the two container definitions.
|
||||||
|
- [`nixos/boxes/kelder/dns_update.py`](../../nixos/boxes/kelder/dns_update.py) — Cloudflare DDNS script.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# colony
|
||||||
|
|
||||||
|
The hosted dedicated server in Amsterdam (`ams1`) and the public-facing half of
|
||||||
|
the boxes: almost everything reachable from the internet lives here.
|
||||||
|
|
||||||
|
- **Internal domain:** `ams1.int.nul.ie` (`lib.my.c.colony.domain`)
|
||||||
|
- **Public domain:** `nul.ie` — public services are published as `*.nul.ie`
|
||||||
|
- **Source:** [`nixos/boxes/colony/`](../../../nixos/boxes/colony)
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
`colony` is the physical VM host. It runs the VMs below; `shill` is itself a
|
||||||
|
NixOS container host where most applications run.
|
||||||
|
|
||||||
|
```
|
||||||
|
colony (physical VM host, ams1)
|
||||||
|
├── estuary ── edge router: WAN, firewall/NAT, DNS, BGP (AS211024), WireGuard
|
||||||
|
├── shill ──── NixOS container host ──┬── middleman (reverse proxy, ACME, nginx-sso, librespeed)
|
||||||
|
│ ├── vaultwarden (password manager)
|
||||||
|
│ ├── colony-psql (shared PostgreSQL)
|
||||||
|
│ ├── chatterbox (Matrix Synapse + bridges)
|
||||||
|
│ ├── jackflix (media stack)
|
||||||
|
│ ├── object (MinIO, Harmonia Nix cache, Sharry, HedgeDoc, wastebin)
|
||||||
|
│ ├── toot (Bluesky PDS; Mastodon disabled)
|
||||||
|
│ ├── waffletail (Tailscale subnet router / exit node)
|
||||||
|
│ ├── qclk (WireGuard management appliance)
|
||||||
|
│ ├── gam (Terraria server)
|
||||||
|
│ └── jam (raw nspawn customer container)
|
||||||
|
├── whale2 ─── podman/OCI host for game servers
|
||||||
|
├── git ────── Gitea + Gitea Actions runner
|
||||||
|
├── mail ───── Debian VM running mailcow (not NixOS)
|
||||||
|
└── darts ──── third-party/customer VM (opaque, not NixOS)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Networks
|
||||||
|
|
||||||
|
All internal space is carved out of `10.100.0.0/16` and `2a0e:97c0:4d2:10::/60`
|
||||||
|
(`lib.my.c.colony.prefixes`):
|
||||||
|
|
||||||
|
| Network | IPv4 CIDR | IPv6 CIDR | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `base` | `10.100.0.0/24` | `2a0e:97c0:4d2:10::/64` | Base LAN shared by `colony` and `estuary` |
|
||||||
|
| `vms` | `10.100.1.0/24` | `2a0e:97c0:4d2:11::/64` | VM network |
|
||||||
|
| `ctrs` | `10.100.2.0/24` | `2a0e:97c0:4d2:12::/64` | `shill` container network |
|
||||||
|
| `oci` | `10.100.3.0/24` | `2a0e:97c0:4d2:13::/64` | `whale2` podman network |
|
||||||
|
|
||||||
|
Public addressing — the WAN /24 (`94.142.240.44`), the `vip*` ranges shared by
|
||||||
|
the VMs, and the customer /32s for `mail` / `darts` — terminates on `estuary`;
|
||||||
|
see [estuary.md](estuary.md).
|
||||||
|
|
||||||
|
## Machines
|
||||||
|
|
||||||
|
| Machine | Role | Page |
|
||||||
|
|---|---|---|
|
||||||
|
| `colony` | Physical VM host (AMD, KVM, LVM-thin, `borgthin` backups → rsync.net) | [colony.md](colony.md) |
|
||||||
|
| `estuary` | Edge router: WAN, firewall/NAT, DNS, BGP (AS211024), WireGuard | [estuary.md](estuary.md) |
|
||||||
|
| `shill` | NixOS container host (most applications) | [shill.md](shill.md) |
|
||||||
|
| `whale2` | podman/OCI game-server host | [whale2.md](whale2.md) |
|
||||||
|
| `git` | Gitea + Gitea Actions runner | [git.md](git.md) |
|
||||||
|
| `mail` | Debian VM running mailcow (not NixOS) | [mail.md](mail.md) |
|
||||||
|
| `darts` | Third-party/customer VM (not NixOS) | [darts.md](darts.md) |
|
||||||
|
|
||||||
|
### `shill` containers
|
||||||
|
|
||||||
|
Each has its own page under `shill/containers/`:
|
||||||
|
|
||||||
|
| Container | Role | Page |
|
||||||
|
|---|---|---|
|
||||||
|
| `middleman` | Front-end nginx reverse proxy, ACME, nginx-sso, librespeed | [middleman](shill/containers/middleman.md) |
|
||||||
|
| `vaultwarden` | Vaultwarden password manager | [vaultwarden](shill/containers/vaultwarden.md) |
|
||||||
|
| `colony-psql` | Shared PostgreSQL for colony services | [colony-psql](shill/containers/colony-psql.md) |
|
||||||
|
| `chatterbox` | Matrix homeserver + bridges | [chatterbox](shill/containers/chatterbox.md) |
|
||||||
|
| `jackflix` | Media stack (Jellyfin, *arr, Transmission, PhotoPrism, copyparty) | [jackflix](shill/containers/jackflix.md) |
|
||||||
|
| `object` | MinIO (S3), Harmonia Nix cache, Sharry, HedgeDoc, wastebin | [object](shill/containers/object.md) |
|
||||||
|
| `toot` | Bluesky PDS (Mastodon disabled) | [toot](shill/containers/toot.md) |
|
||||||
|
| `waffletail` | Tailscale subnet router / exit node | [waffletail](shill/containers/waffletail.md) |
|
||||||
|
| `qclk` | WireGuard management appliance | [qclk](shill/containers/qclk.md) |
|
||||||
|
| `gam` | Terraria server | [gam](shill/containers/gam.md) |
|
||||||
|
|
||||||
|
## Non-NixOS VMs
|
||||||
|
|
||||||
|
Two VMs are declared in `colony`'s `my.vms.instances` — so `colony` runs them
|
||||||
|
and routes/firewalls their traffic — but they are **not** managed as NixOS
|
||||||
|
systems by this repo:
|
||||||
|
|
||||||
|
- **`mail`** — a Debian VM running [mailcow](https://mailcow.email/)
|
||||||
|
(`mail.nul.ie`). ACME certificates are pushed to it from `middleman`.
|
||||||
|
See [mail.md](mail.md).
|
||||||
|
- **`darts`** — an opaque third-party/customer VM, given a routed public /32
|
||||||
|
and IPv6 /64 and otherwise left alone. See [darts.md](darts.md).
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# colony
|
||||||
|
|
||||||
|
The physical dedicated server in Amsterdam (`ams1`) and the VM host for
|
||||||
|
everything at the colony site.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/colony/default.nix`](../../../nixos/boxes/colony/default.nix)
|
||||||
|
(VM instances in [`nixos/boxes/colony/vms/default.nix`](../../../nixos/boxes/colony/vms/default.nix))
|
||||||
|
- **Host:** bare metal (this *is* the physical box)
|
||||||
|
- **nixpkgs:** `mine-stable`
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
Bare-metal AMD host. It does little application work itself — its job is to run
|
||||||
|
the VMs and provide them with storage, networking and backups.
|
||||||
|
|
||||||
|
- **Virtualisation:** QEMU/KVM (`kvm-amd`, IOMMU on) driven by the `my.vms`
|
||||||
|
module: each entry in `my.vms.instances` becomes a `vm@<name>` systemd
|
||||||
|
service running `qemu-kvm` with UEFI, a QMP/monitor socket under
|
||||||
|
`/run/vms/<name>/`, TAP networking and optional PCI passthrough
|
||||||
|
(`hostDevices`, bound to `vfio-pci`). `estuary` gets the WAN NIC this way.
|
||||||
|
- **Storage:** LVM-thin (`services.lvm.boot.thin`) in the `main` VG; VM disks
|
||||||
|
are logical volumes (`vm-<name>-<disk>`, see the `lib.my.vm.disk` /
|
||||||
|
`lvmDisk` helpers). `/persist` holds host state, `/mnt/backup` the local
|
||||||
|
borg repo. Only the boot-critical LVs are activated in the initrd; the rest
|
||||||
|
come up via `lvm-activate-main.service`.
|
||||||
|
- **Backups:** `my.borgthin` job `main` snapshots the persist/data LVs of the
|
||||||
|
host and its VMs into `/mnt/backup/main`; `borgthin-rsync.service` then
|
||||||
|
rsyncs the repo to rsync.net and `rsync-lvm-meta.service` ships the LVM
|
||||||
|
metadata alongside (both run idle-priority, after the borg job).
|
||||||
|
- **Monitoring/health:** netdata (freeipmi, ignoring the VCCM sensor), `smartd`
|
||||||
|
(logging to `/var/log/smartd`), `rasdaemon`, `fstrim` at 04:45 (before the
|
||||||
|
05:00 backup).
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: colony -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| colony (vm) | internal | `94.142.241.224/32` | `2a0e:97c0:4d2:10::2/64` | ams1.int.nul.ie | |
|
||||||
|
| colony-routing | routing | `10.100.0.2/24 gw 10.100.0.1` | — | ams1.int.nul.ie | |
|
||||||
|
| colony-vms | vms | `10.100.1.1/24` | `2a0e:97c0:4d2:11::1/64` | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
- Two bridges: `base` (the colony base network, shared with `estuary`) and
|
||||||
|
`vms` (the VM network). Dummy interfaces (`base0`, `vms0`) keep the bridges
|
||||||
|
up in networkd's eyes so dependent VMs can start.
|
||||||
|
- `colony` sends RAs on `vms` (DNS = `estuary`'s base address) and carries
|
||||||
|
static routes for the downstream prefixes: `ctrs` via `shill`, `oci` via
|
||||||
|
`whale2`, plus the Tailscale, `qclk` and `jam` prefixes via `shill`.
|
||||||
|
- `estuary` is the default gateway (via the `base` bridge); `colony`'s own
|
||||||
|
public-facing address is its `internal` assignment (a `vip1` /32, alt name
|
||||||
|
`vm`).
|
||||||
|
- The customer VMs attach to dedicated TAP devices (`vm-mail`, `vm-darts`)
|
||||||
|
which are **not** bridged: networkd puts the point-to-point /32
|
||||||
|
(`lib.my.c.colony.custRouting`) and the customer's IPv6 /64 on each, sends
|
||||||
|
RAs, and link-routes the customer's public /32 down the tap.
|
||||||
|
- `my.firewall` trusts the `vms` bridge, DNATs the shared
|
||||||
|
`lib.my.c.colony.firewallForwards` list for traffic addressed to `estuary`'s
|
||||||
|
public IP (so the port forwards also work from inside), and forwards the
|
||||||
|
customer prefixes through with minimal filtering ("trust for now").
|
||||||
|
|
||||||
|
## VMs
|
||||||
|
|
||||||
|
Declared in `my.vms.instances` (`cpus`/`threads` are QEMU `smp` values):
|
||||||
|
|
||||||
|
| VM | Cores | Threads | Memory | MAC | Disks |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `estuary` | 2 | 2 | 3 GiB | `52:54:00:15:1a:53` (`base`) | `esp` / `nix` / `persist` LVs + WAN NIC passthrough |
|
||||||
|
| `shill` | 12 | 2 | 40 GiB | `52:54:00:27:3d:5c` (`vms`) | `esp` / `nix` / `persist` + `media` / `minio` / `nix-cache` / `jam` LVs |
|
||||||
|
| `whale2` | 8 | 2 | 16 GiB | `52:54:00:d5:d9:c6` (`vms`) | `esp` / `nix` / `persist` + `oci` LV |
|
||||||
|
| `git` | 12 | 2 | 40 GiB | `52:54:00:75:78:a8` (`vms`) | `esp` / `nix` / `persist` / `oci` + `git` / `gitea-actions-cache` LVs |
|
||||||
|
| `mail` | 3 | 2 | 6 GiB | `52:54:00:a8:d1:03` (`vm-mail` tap) | `root` / `data` LVs |
|
||||||
|
| `darts` | 4 | 2 | 16 GiB | `52:54:00:a8:29:cd` (`vm-darts` tap) | `root` LV + `darts-media` / `darts-ext` LVs |
|
||||||
|
|
||||||
|
`estuary`, `shill`, `whale2` and `git` are NixOS systems with their own pages
|
||||||
|
(see the [README](README.md#machines)); `mail` and `darts` are not (see
|
||||||
|
[Non-NixOS VMs](README.md#non-nixos-vms)).
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/default.nix`](../../../nixos/boxes/colony/default.nix) — host hardware, networkd, firewall, backups.
|
||||||
|
- [`nixos/boxes/colony/vms/default.nix`](../../../nixos/boxes/colony/vms/default.nix) — `my.vms.instances` for all six VMs.
|
||||||
|
- [`nixos/modules/vms.nix`](../../../nixos/modules/vms.nix) — the `my.vms` module itself.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# darts
|
||||||
|
|
||||||
|
An opaque third-party/customer VM. Declared in `colony`'s `my.vms.instances`
|
||||||
|
but **not a NixOS system**: this repo knows nothing about what runs inside it
|
||||||
|
and doesn't manage it.
|
||||||
|
|
||||||
|
- **Source (host-side only):** the `darts` instance in
|
||||||
|
[`nixos/boxes/colony/vms/default.nix`](../../../nixos/boxes/colony/vms/default.nix)
|
||||||
|
and the `90-vm-darts` network in
|
||||||
|
[`nixos/boxes/colony/default.nix`](../../../nixos/boxes/colony/default.nix)
|
||||||
|
- **Host:** VM on `colony`
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Customer/dedicated VM, left alone beyond hosting and connectivity.
|
||||||
|
- 4 cores, 16 GiB RAM; a `root` LV plus `darts-media` and `darts-ext` LVs from
|
||||||
|
the `media`/`ext` volume groups.
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
- Same customer-VM pattern as [`mail`](mail.md): dedicated unbridged TAP
|
||||||
|
(`vm-darts`, MAC `52:54:00:a8:29:cd`), point-to-point address
|
||||||
|
(`custRouting.darts-vm`) on the host side, link-routed public /32
|
||||||
|
`94.142.242.255`, and the IPv6 /64 `2a0e:97c0:4d2:2001::/64` with RAs.
|
||||||
|
- DNS: `darts-cust.ams1.int.nul.ie`. Like the other customer prefixes, its
|
||||||
|
inbound traffic is accepted by `estuary` without per-port filtering and
|
||||||
|
forwarded on by `colony`.
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# estuary
|
||||||
|
|
||||||
|
The colony edge router and firewall — the box that holds colony's public IPs
|
||||||
|
and connects everything else at the site to the internet.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/colony/vms/estuary/`](../../../nixos/boxes/colony/vms/estuary)
|
||||||
|
(`default.nix`, `bgp.nix`, `dns.nix`, `bandwidth.nix`)
|
||||||
|
- **Host:** VM on `colony` (gets the WAN NIC by PCI passthrough)
|
||||||
|
- **nixpkgs:** `mine`
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **Edge routing / firewall / NAT:** owns the colony public IPv4/IPv6
|
||||||
|
(`94.142.240.44/24`, `2a02:898:0:20::329:1/64`), NATs outbound traffic, and
|
||||||
|
port-forwards inbound services (`my.firewall.nat.forwardPorts` driven by the
|
||||||
|
shared `lib.my.c.colony.firewallForwards` list): HTTP/S and Matrix
|
||||||
|
federation to `middleman`, git to `git`, game ports to the OCI servers on
|
||||||
|
`whale2` and to `gam`, Tailscale to `waffletail`, and the `qclk` WireGuard
|
||||||
|
port.
|
||||||
|
- **DNS:** PowerDNS authoritative server *and* recursor (see below).
|
||||||
|
- **BGP:** BIRD2 speaking AS211024 with upstreams, IXP route servers and
|
||||||
|
direct peers (see below).
|
||||||
|
- **VPNs:** member of the `as211024` L2 VXLAN mesh and endpoint for three
|
||||||
|
point-to-point WireGuard tunnels (see below).
|
||||||
|
- **Misc:** `iperf3` server, netdata.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: estuary -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| estuary | as211024 | `10.100.50.1/24` | `2a0e:97c0:4df::1/64` | — | |
|
||||||
|
| estuary-vm-base | base | `10.100.0.1/24` | `2a0e:97c0:4d2:10::1/64` | ams1.int.nul.ie | |
|
||||||
|
| estuary-vm (fw) | internal | `94.142.240.44/24 gw 94.142.240.254` | `2a02:898:0:20::329:1/64 gw 2a02:898:0:20::1` | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## WAN and IXP VLANs
|
||||||
|
|
||||||
|
- `wan` — the passed-through `igb` NIC (9000 MTU, enlarged rings). It carries
|
||||||
|
the plain upstream uplink (static v4/v6 with gateways from the `internal`
|
||||||
|
assignment) plus the tagged `ifog` VLAN.
|
||||||
|
- `ifog` (VLAN 409) is an iFog QinQ transport that carries the IXP VLANs as
|
||||||
|
nested tags:
|
||||||
|
|
||||||
|
| Interface | VLAN | IPv4 | IPv6 | Purpose |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `frys-ix` | 701 | `185.1.160.196/23` | `2001:7f8:10f::3:3850:196/64` | Frys-IX peering LAN |
|
||||||
|
| `nl-ix` | 1845 | `193.239.116.145/22` | `2001:7f8:13::a521:1024:1/64` | NL-ix peering LAN |
|
||||||
|
| `fogixp` | 1147 | `185.1.147.159/24` | `2001:7f8:ca:1::159/64` | FogIXP peering LAN |
|
||||||
|
| `ifog-transit` | 702 | — | `2a0c:9a40:100f:370::2/64` | iFog IPv6 transit |
|
||||||
|
|
||||||
|
The IXP interfaces run at 1500 MTU with DHCP/RA/LLDP off; an nftables `ixp`
|
||||||
|
chain rejects non-IP/ARP ethertypes in both directions.
|
||||||
|
|
||||||
|
- `base` — colony base network; sends RAs and serves DNS to the site, and
|
||||||
|
routes the `vms`/`ctrs`/`oci`, Tailscale, `qclk`, `vip*` and customer
|
||||||
|
prefixes back via `colony`.
|
||||||
|
- `as211024` — the L2 mesh interface (see VPNs).
|
||||||
|
|
||||||
|
## Firewall and NAT
|
||||||
|
|
||||||
|
`my.firewall` (nftables). Besides the port forwards, `extraRules` defines:
|
||||||
|
|
||||||
|
- `routing-tcp` / `routing-udp` chains — the inbound allow-list for new
|
||||||
|
connections from `wan`/`as211024`/IXPs towards internal services (SSH
|
||||||
|
anywhere, otherwise per-service v4/v6 rules mirroring `firewallForwards`).
|
||||||
|
- `filter-routing` — applied to `wan`/`as211024`/IXPs → `base` forwards;
|
||||||
|
customer prefixes (`mail`/`darts` v4, `cust.v6`) are accepted wholesale, the
|
||||||
|
rest goes through the `routing-*` chains.
|
||||||
|
- SNAT: everything from `prefixes.all.v4` leaving non-`as211024` interfaces is
|
||||||
|
NATed to the public IP; the WireGuard tunnel prefixes get their own SNAT
|
||||||
|
addresses.
|
||||||
|
- DNS redirect: DNS traffic arriving at estuary's own public addresses is
|
||||||
|
redirected to port 5353 (the authoritative server) — see below.
|
||||||
|
|
||||||
|
## DNS
|
||||||
|
|
||||||
|
Both halves are PowerDNS ([`dns.nix`](../../../nixos/boxes/colony/vms/estuary/dns.nix)):
|
||||||
|
|
||||||
|
- **Authoritative** (`my.pdns.auth`) listens on `0.0.0.0:5353` / `[::]:5353` as
|
||||||
|
primary for `ams1.int.nul.ie`, `100.10.in-addr.arpa` and the
|
||||||
|
`2a0e:97c0:4d2::/48` reverse zone. Zone contents are largely generated from
|
||||||
|
`allAssignments` (`lib.my.dns.fwdRecords` / `ptrRecords` / `ptr6Records`);
|
||||||
|
`ALIAS` records (with `expand-alias`) point the zone apex at estuary itself.
|
||||||
|
AXFR is allowed to HE.net's secondary (`216.218.133.2` / `2001:470:600::2`),
|
||||||
|
and `_acme-challenge` is a LUA `TXT` record answered from a file (used for
|
||||||
|
DNS-01 issuance). Public DNS reaches it via the NAT redirect of port 53 to
|
||||||
|
5353; the `base` side also accepts DNS directly.
|
||||||
|
- **Recursor** (`my.pdns.recursor`, `pdns-recursor`) listens on localhost and
|
||||||
|
the `base` addresses, serving `prefixes.all` and the Tailscale prefixes. The
|
||||||
|
authoritative zones are forwarded back to `127.0.0.1:5353` (with NOTIFY
|
||||||
|
support so changes show up immediately), and a small Lua `preresolve` hook
|
||||||
|
rewrites `nix-cache.nul.ie` to `http.ams1.int.nul.ie` so cache traffic stays
|
||||||
|
on-site.
|
||||||
|
|
||||||
|
## BGP
|
||||||
|
|
||||||
|
BIRD2 ([`bgp.nix`](../../../nixos/boxes/colony/vms/estuary/bgp.nix)) speaking **AS211024**:
|
||||||
|
|
||||||
|
- **Upstreams:** ColoClue (AS8283, `euNetworks` 2/3, v4+v6); iFog IPv6 transit
|
||||||
|
(AS34927); Hurricane Electric IPv6 over Frys-IX (AS6939).
|
||||||
|
- **IXP route servers:** Frys-IX (AS56393), NL-ix (AS34307, depref'd by 1),
|
||||||
|
FogIXP (AS47498).
|
||||||
|
- **Direct peers:** LUJE.net (AS212855, on ColoClue/Frys-IX/FogIXP + multihop
|
||||||
|
labs), jurrian (AS212635), Meta (AS32934, Frys-IX/NL-ix), Cloudflare
|
||||||
|
(AS13335, Frys-IX), Apple (AS714, NL-ix), HE (AS6939, Frys-IX v4).
|
||||||
|
- **Monitoring:** a multihop session to the bgp.tools collector (AS212232)
|
||||||
|
exporting everything with `add paths`.
|
||||||
|
- Statics originate the site prefixes: the `vip*` ranges via `base`, the
|
||||||
|
ams1/internal v6 prefixes via `base`/`as211024`, and the home /48 towards
|
||||||
|
the home routers' `as211024` VIP.
|
||||||
|
|
||||||
|
Currently disabled (commented out): efero transit over FogIXP ("not working so
|
||||||
|
well lately") and the NL-ix Cloudflare sessions.
|
||||||
|
|
||||||
|
## VPNs
|
||||||
|
|
||||||
|
- **`as211024` L2 mesh** (`my.vpns.l2`, the `l2mesh` module): VXLAN (VNI
|
||||||
|
211024, UDP-encapsulated) secured with libreswan IPsec, meshing estuary with
|
||||||
|
the home routers `river`/`stream` and `britway`. This carries the AS211024
|
||||||
|
anycast-ish internal address space between sites.
|
||||||
|
- **WireGuard endpoints** (networkd `wireguard` netdevs, keys from agenix):
|
||||||
|
- `kelder` — tunnel to the remote `kelder` site, port `51820`.
|
||||||
|
- `hillcrest` — port `51822`, point-to-point /32 pair out of
|
||||||
|
`prefixes.hillcrest.v4`.
|
||||||
|
- `john-valorant` — port `51823`, same pattern out of
|
||||||
|
`prefixes.john-valorant.v4`.
|
||||||
|
|
||||||
|
## Bandwidth management
|
||||||
|
|
||||||
|
[`bandwidth.nix`](../../../nixos/boxes/colony/vms/estuary/bandwidth.nix)
|
||||||
|
implements a ~95% WAN shaper: a 245 Mbit token-bucket filter on `wan`
|
||||||
|
(outbound) and on an `ifb-wan` IFB device that ingress traffic is mirrored
|
||||||
|
into (inbound), with [`bandwidth.py`](../../../nixos/boxes/colony/vms/estuary/bandwidth.py)
|
||||||
|
as a `bandwidth-limiter` service that watches/utilises the link and can adjust
|
||||||
|
the rate. **Currently disabled** — the file is not in estuary's `imports`
|
||||||
|
(only `dns.nix` and `bgp.nix` are), so no shaping is applied.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/estuary/default.nix`](../../../nixos/boxes/colony/vms/estuary/default.nix) — system, networkd, firewall, WireGuard, mesh membership.
|
||||||
|
- [`nixos/boxes/colony/vms/estuary/bgp.nix`](../../../nixos/boxes/colony/vms/estuary/bgp.nix) — BIRD2 config.
|
||||||
|
- [`nixos/boxes/colony/vms/estuary/dns.nix`](../../../nixos/boxes/colony/vms/estuary/dns.nix) — PowerDNS auth + recursor.
|
||||||
|
- [`nixos/boxes/colony/vms/estuary/bandwidth.nix`](../../../nixos/boxes/colony/vms/estuary/bandwidth.nix) — WAN shaper (disabled, not imported).
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# git
|
||||||
|
|
||||||
|
The Gitea VM — source hosting and CI for the boxes (`git.nul.ie`).
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/colony/vms/git/`](../../../nixos/boxes/colony/vms/git)
|
||||||
|
(`default.nix`, `gitea.nix`, `gitea-actions.nix`)
|
||||||
|
- **Host:** VM on `colony`
|
||||||
|
- **nixpkgs:** `mine`
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **Gitea** ([`gitea.nix`](../../../nixos/boxes/colony/vms/git/gitea.nix)) — the
|
||||||
|
Git forge at `git.nul.ie` (self-registration disabled). Backed by PostgreSQL
|
||||||
|
on `colony-psql` (waiting on it via `lib.my.systemdAwaitPostgres`), LFS
|
||||||
|
enabled, with all object storage (incl. LFS and packages) on MinIO at
|
||||||
|
`s3.nul.ie` (bucket `gitea`, on `object`; the secret is spliced into
|
||||||
|
`app.ini` at startup). Mail goes out via `mail.nul.ie`, including the
|
||||||
|
issue-reply incoming-mail poller.
|
||||||
|
- **Gitea Actions runner**
|
||||||
|
([`gitea-actions.nix`](../../../nixos/boxes/colony/vms/git/gitea-actions.nix)) —
|
||||||
|
one Docker-mode instance (`main-docker`) on podman (privileged, `podman`
|
||||||
|
network), with labels for `node:24-trixie` and the self-built
|
||||||
|
`git.nul.ie/dev/actions-ubuntu:26.04` images. Runs as a fixed
|
||||||
|
`gitea-runner` user (not `DynamicUser`) so it can read its token; 8 h job
|
||||||
|
timeout; the action cache lives on a dedicated disk
|
||||||
|
(`/var/cache/gitea-runner`). This runner executes the repo's own
|
||||||
|
`.gitea/workflows/ci.yaml`.
|
||||||
|
- **nginx** — terminates TLS for `git.nul.ie` (and a default vhost) and
|
||||||
|
proxies to Gitea on `:3000`. ACME (Let's Encrypt, production) issues
|
||||||
|
`nul.ie` + `*.nul.ie` via the Cloudflare DNS-01 challenge.
|
||||||
|
- **podman** — local container backend for the runner;
|
||||||
|
`/var/lib/containers` is an XFS data disk, and the default
|
||||||
|
`10.88.0.0/16` podman subnet is allowed to forward.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: git -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| git-vm | internal | `94.142.241.117/32` | `2a0e:97c0:4d2:11::4/64` | ams1.int.nul.ie | |
|
||||||
|
| git-vm-routing | routing | `10.100.1.4/24 gw 10.100.1.1` | — | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
- `/var/lib/gitea` — the `git` LV (repositories, config).
|
||||||
|
- `/var/cache/gitea-runner` — the `gitea-actions-cache` LV.
|
||||||
|
- `/var/lib/containers` — the `oci` LV (XFS with project quotas). Despite the
|
||||||
|
name this is local to the `git` VM and unrelated to `whale2`'s `oci`
|
||||||
|
network.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/git/default.nix`](../../../nixos/boxes/colony/vms/git/default.nix) — VM config, nginx + ACME, podman, firewall.
|
||||||
|
- [`nixos/boxes/colony/vms/git/gitea.nix`](../../../nixos/boxes/colony/vms/git/gitea.nix) — Gitea itself.
|
||||||
|
- [`nixos/boxes/colony/vms/git/gitea-actions.nix`](../../../nixos/boxes/colony/vms/git/gitea-actions.nix) — the Actions runner.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# mail
|
||||||
|
|
||||||
|
A Debian VM running [mailcow](https://mailcow.email/) (`mail.nul.ie`) — the
|
||||||
|
mail server for `nul.ie`. Declared in `colony`'s `my.vms.instances` but **not
|
||||||
|
a NixOS system**: everything inside the VM is configured out of band.
|
||||||
|
|
||||||
|
- **Source (host-side only):** the `mail` instance in
|
||||||
|
[`nixos/boxes/colony/vms/default.nix`](../../../nixos/boxes/colony/vms/default.nix)
|
||||||
|
and the `90-vm-mail` network in
|
||||||
|
[`nixos/boxes/colony/default.nix`](../../../nixos/boxes/colony/default.nix)
|
||||||
|
- **Host:** VM on `colony`
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Runs the full mailcow stack (Postfix/Dovecot/SOGo/Rspamd) for `nul.ie`.
|
||||||
|
Other colony services send through it as `mail.nul.ie` (e.g. Gitea, and the
|
||||||
|
disabled Mastodon config).
|
||||||
|
- 3 cores, 6 GiB RAM; `root` and `data` LVM disks (the `vm-mail-data` LV is
|
||||||
|
included in `colony`'s `borgthin` backups).
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
- Attaches to a dedicated, unbridged TAP (`vm-mail`, MAC
|
||||||
|
`52:54:00:a8:d1:03`). `colony` puts the point-to-point address
|
||||||
|
(`custRouting.mail-vm`) on the host side, link-routes the public /32
|
||||||
|
`94.142.241.227` down the tap, and serves the IPv6 /64
|
||||||
|
`2a0e:97c0:4d2:2000::/64` with RAs.
|
||||||
|
- DNS: `mail-vm.ams1.int.nul.ie` (and `mail.nul.ie` publicly, incl. the PTR in
|
||||||
|
estuary's reverse zone). `estuary` accepts traffic to the customer prefixes
|
||||||
|
without per-port filtering; `colony` forwards it on ("trust for now").
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- ACME certificates are issued on `middleman` and pushed to the VM over SSH
|
||||||
|
(`acme@mail.nul.ie mailcow-ssl-reload`, key `middleman/mailcow-ssh.key`);
|
||||||
|
the VM's SSH host key is pinned at `.keys/mail-vm-host.pub`.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# shill
|
||||||
|
|
||||||
|
The colony NixOS container host — most colony applications run as
|
||||||
|
`systemd-nspawn` containers on this VM.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/colony/vms/shill/`](../../../nixos/boxes/colony/vms/shill)
|
||||||
|
(`default.nix`, `containers-ext.nix`, `containers/`)
|
||||||
|
- **Host:** VM on `colony` (large: 12 cores, 40 GiB RAM)
|
||||||
|
- **nixpkgs:** `mine`
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Runs the colony containers via `my.containers.instances`, each attached to
|
||||||
|
the `ctrs` bridge with its own address. The containers are full NixOS
|
||||||
|
systems rendered via `my.asContainer` and deployed as container profiles on
|
||||||
|
`shill` (`my.deploy.enable = false` — they are not standalone deploy
|
||||||
|
targets); the `my.containers` module wires up the nspawn units, `/persist`
|
||||||
|
bind mounts and store binds.
|
||||||
|
- Provides shared data volumes to containers via bind mounts from LVM-backed
|
||||||
|
disks: `/mnt/media` (→ `middleman` read-only, `jackflix` read-write),
|
||||||
|
`/mnt/minio` and `/mnt/nix-cache` (→ `object`, both read-write).
|
||||||
|
- Routes between the `vms` network and the `ctrs` container network: sends RAs
|
||||||
|
on `ctrs` (DNS = `estuary`'s base address) and routes the Tailscale prefixes
|
||||||
|
via `waffletail` and the `qclk` prefix via `qclk`. Applies the shared
|
||||||
|
`firewallForwards` DNAT for traffic addressed to `estuary`'s public IP, with
|
||||||
|
an nftables `ct mark 0x1337` SNAT hack so forwarded return traffic stays
|
||||||
|
symmetric.
|
||||||
|
- Tuned for high connection counts (larger conntrack table, wider ephemeral
|
||||||
|
port range); netdata on 19999.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: shill -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| shill-vm-ctrs | ctrs | `10.100.2.1/24` | `2a0e:97c0:4d2:12::1/64` | ams1.int.nul.ie | |
|
||||||
|
| shill-vm (ctr) | internal | `94.142.241.225/32` | `2a0e:97c0:4d2:11::2/64` | ams1.int.nul.ie | |
|
||||||
|
| shill-vm-routing | routing | `10.100.1.2/24 gw 10.100.1.1` | — | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Containers
|
||||||
|
|
||||||
|
Defined under
|
||||||
|
[`shill/containers/`](../../../nixos/boxes/colony/vms/shill/containers) and
|
||||||
|
wired up in `shill`'s `my.containers.instances`. Each has its own page:
|
||||||
|
|
||||||
|
| Container | IPv4 | IPv6 | Role | Page |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `middleman` | `10.100.2.2` | `2a0e:97c0:4d2:12::2` | Reverse proxy, ACME, nginx-sso, librespeed | [middleman](shill/containers/middleman.md) |
|
||||||
|
| `vaultwarden` | `10.100.2.3` | `2a0e:97c0:4d2:12::3` | Password manager | [vaultwarden](shill/containers/vaultwarden.md) |
|
||||||
|
| `colony-psql` | `10.100.2.4` | `2a0e:97c0:4d2:12::4` | Shared PostgreSQL (14) | [colony-psql](shill/containers/colony-psql.md) |
|
||||||
|
| `chatterbox` | `10.100.2.5` | `2a0e:97c0:4d2:12::5` | Matrix Synapse + bridges | [chatterbox](shill/containers/chatterbox.md) |
|
||||||
|
| `jackflix` | `10.100.2.6` | `2a0e:97c0:4d2:12::6` | Media stack | [jackflix](shill/containers/jackflix.md) |
|
||||||
|
| `object` | `10.100.2.7` | `2a0e:97c0:4d2:12::7` | MinIO, Harmonia Nix cache, Sharry, HedgeDoc, wastebin | [object](shill/containers/object.md) |
|
||||||
|
| `toot` | `10.100.2.8` | `2a0e:97c0:4d2:12::8` | Bluesky PDS (Mastodon disabled) | [toot](shill/containers/toot.md) |
|
||||||
|
| `waffletail` | `10.100.2.9` | `2a0e:97c0:4d2:12::9` | Tailscale subnet router / exit node | [waffletail](shill/containers/waffletail.md) |
|
||||||
|
| `qclk` | `10.100.2.10` | `2a0e:97c0:4d2:12::a` | WireGuard management appliance | [qclk](shill/containers/qclk.md) |
|
||||||
|
| `gam` | `10.100.2.11` | `2a0e:97c0:4d2:12::b` | Terraria server | [gam](shill/containers/gam.md) |
|
||||||
|
|
||||||
|
### `jam`
|
||||||
|
|
||||||
|
A one-off: [`containers-ext.nix`](../../../nixos/boxes/colony/vms/shill/containers-ext.nix)
|
||||||
|
runs a raw `systemd-nspawn` container (not a `my.containers` instance, not
|
||||||
|
NixOS) with its root on the `jam` LV, private user namespaces and a `ve-jam`
|
||||||
|
veth. It gets the `jam` customer prefix (`prefixes.jam`, `jam-cust` in DNS)
|
||||||
|
and SSH is forwarded to it from `shill`'s public IP port 60022.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `nix.settings.substituters` is forced to just `https://cache.nixos.org` —
|
||||||
|
`shill` sits next to the S3 cache on `object`, so it doesn't use it.
|
||||||
|
- [`hercules.nix`](../../../nixos/boxes/colony/vms/shill/hercules.nix)
|
||||||
|
(Hercules CI agent + the `nix-cache-gc` timer for the S3 binary cache)
|
||||||
|
exists but is **currently disabled**: the file is not imported by
|
||||||
|
`shill/default.nix`.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/shill/default.nix`](../../../nixos/boxes/colony/vms/shill/default.nix) — VM config, networkd, firewall, `my.containers.instances`.
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/default.nix`](../../../nixos/boxes/colony/vms/shill/containers/default.nix) — container imports.
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers-ext.nix`](../../../nixos/boxes/colony/vms/shill/containers-ext.nix) — the `jam` nspawn container.
|
||||||
|
- [`nixos/modules/containers.nix`](../../../nixos/modules/containers.nix) — the `my.containers` module.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# chatterbox
|
||||||
|
|
||||||
|
The Matrix homeserver for `nul.ie` (Synapse) and its bridges to other chat networks.
|
||||||
|
[middleman](middleman.md) fronts it as `matrix.nul.ie` for clients and on `:8448` for
|
||||||
|
federation.
|
||||||
|
|
||||||
|
- **Source:** [`shill/containers/chatterbox.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/chatterbox.nix)
|
||||||
|
- **Host:** NixOS container on [`shill`](../../shill.md)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **matrix-synapse** — `server_name = "nul.ie"`, `public_baseurl = https://matrix.nul.ie`,
|
||||||
|
Element at `element.nul.ie` as the web client. Listens on `[::]:8008` (client + federation
|
||||||
|
resources, `x_forwarded`) with a localhost manhole on `:9000`. Registration and guest access
|
||||||
|
are disabled; uploads up to 1024M with dynamic thumbnails and URL previews enabled (previews
|
||||||
|
are limited to [middleman](middleman.md)'s addresses as the fetch proxy).
|
||||||
|
- **heisenbridge** — IRC bridge, owner `@dev:nul.ie`, exclusive `@irc_*` user namespace.
|
||||||
|
- **mautrix-whatsapp** — WhatsApp bridge (appservice `whatsapp2`, `!wa` commands).
|
||||||
|
- **mautrix-meta** — two instances, `messenger` (`fbm2_*`, `!fbm`) and `instagram` (`ig_*`,
|
||||||
|
`!ig`), both with backfill enabled.
|
||||||
|
- All three mautrix bridges use Postgres on [colony-psql](colony-psql.md) (URIs in their secret
|
||||||
|
env files), require end-to-end encryption by default, and double-puppet onto `nul.ie` via the
|
||||||
|
shared `doublepuppet.yaml` appservice registration (an age secret).
|
||||||
|
- The firewall allows `8008` (Synapse) and `8009` besides netdata.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: chatterbox -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| chatterbox-ctr | internal | `10.100.2.5/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::5/64` | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Synapse's real database config lives in the `chatterbox/synapse.yaml` age secret — options
|
||||||
|
only merge at the top level, so the base config carries a dummy `sqlite3` block to satisfy
|
||||||
|
the module defaults. The signing key is also an age secret.
|
||||||
|
- `olm-3.2.16` is allowed via `permittedInsecurePackages` (a nixpkgs E2EE library issue).
|
||||||
|
- The bridge services get `ffmpeg` on their `PATH` for GIF→video conversion.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/chatterbox.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/chatterbox.nix) — container definition, Synapse settings and all bridge configuration
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# colony-psql
|
||||||
|
|
||||||
|
The shared PostgreSQL instance for colony services. Rather than each service running its own
|
||||||
|
database, the containers (and the `git` VM) connect here over the `ctrs` network.
|
||||||
|
|
||||||
|
- **Source:** [`shill/containers/colony-psql.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/colony-psql.nix)
|
||||||
|
- **Host:** NixOS container on [`shill`](../../shill.md)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **PostgreSQL 14** with TCP/IP enabled, reachable from the whole colony (`10.100.0.0/16` and
|
||||||
|
`2a0e:97c0:4d2:10::/60`, `md5` auth). The firewall allows `5432`.
|
||||||
|
- Local `peer` auth maps `postgres`, `root`, `netdata` and `dev` to the `postgres` superuser via
|
||||||
|
the ident map.
|
||||||
|
- **netdata** with the Python PostgreSQL collector.
|
||||||
|
- Consumers wait for the database to accept connections with the `lib.my.systemdAwaitPostgres`
|
||||||
|
helper (e.g. `sharry`, `atticd`, `mastodon-init-db`, and `middleman`'s nginx as a DNS
|
||||||
|
bootstrap hack).
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: colony-psql -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| colony-psql-ctr (colony-psql) | internal | `10.100.2.4/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::4/64` | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
The assignment also has the alt name `colony-psql` (no `-ctr` suffix), which is what consumers
|
||||||
|
use as the database hostname.
|
||||||
|
|
||||||
|
## Consumers
|
||||||
|
|
||||||
|
- [object](object.md) — `sharry` and `hedgedoc` (and `atticd` when enabled) over
|
||||||
|
`colony-psql:5432`
|
||||||
|
- [toot](toot.md) — Mastodon's database (Mastodon currently disabled)
|
||||||
|
- [chatterbox](chatterbox.md) — the mautrix bridges (WhatsApp, Messenger, Instagram) via
|
||||||
|
Postgres URIs in their secret env files
|
||||||
|
- `git` VM — Gitea
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/colony-psql.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/colony-psql.nix) — container definition and PostgreSQL configuration
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# gam
|
||||||
|
|
||||||
|
A game-server container — the lightweight counterpart to the OCI game servers on `whale2`,
|
||||||
|
running servers directly as NixOS services. Currently it runs a single Terraria server.
|
||||||
|
|
||||||
|
- **Source:** [`shill/containers/gam.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/gam.nix)
|
||||||
|
- **Host:** NixOS container on [`shill`](../../shill.md)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **terraria** — dedicated server (`services.terraria`): world at
|
||||||
|
`/var/lib/terraria/NotWorld.wld`, auto-created large worlds, MOTD "sup gamers", UPnP off.
|
||||||
|
Extra settings (e.g. password) come from the `gam/terraria.conf` age secret used as the config
|
||||||
|
file. `openFirewall` is on, and `estuary` port-forwards TCP and UDP `7777` to this container.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: gam -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| gam-ctr | internal | `10.100.2.11/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::b/64` | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/gam.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/gam.nix) — container definition and the Terraria service
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# jackflix
|
||||||
|
|
||||||
|
The media stack — acquisition, library, streaming and photos. Torrent traffic is routed through
|
||||||
|
an AirVPN WireGuard tunnel so downloads only flow while the VPN is up.
|
||||||
|
|
||||||
|
- **Source:** [`shill/containers/jackflix/`](../../../../../nixos/boxes/colony/vms/shill/containers/jackflix)
|
||||||
|
(`default.nix`, `networking.nix`)
|
||||||
|
- **Host:** NixOS container on [`shill`](../../shill.md) (bind-mounts `/mnt/media` read-write)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
| Service | Port | Purpose |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Jellyfin | `8096` | streaming, published as `jackflix.nul.ie` |
|
||||||
|
| Transmission | `9091` | BitTorrent client (`transmission_4`), published as `torrents.nul.ie` (SSO) |
|
||||||
|
| Jackett | `9117` | indexer aggregator, `jackett.nul.ie` (SSO) |
|
||||||
|
| FlareSolverr | — | Cloudflare challenge solver for Jackett |
|
||||||
|
| Radarr | `7878` | movies, `radarr.nul.ie` (SSO) |
|
||||||
|
| Sonarr | `8989` | TV, `sonarr.nul.ie` (SSO) |
|
||||||
|
| Jellyseerr (`seerr`) | `5055` | request portal, `gib.nul.ie` (`openFirewall` on) |
|
||||||
|
| PhotoPrism | `2342` | photos, `photos.nul.ie`; password auth, sqlite DB, originals/import under `/mnt/media/photoprism` |
|
||||||
|
| copyparty | `3923` | file sharing, `stuff.nul.ie`; serves `/mnt/media/public` (read-only to everyone) and `/priv` → `/mnt/media/stuff` (admin for `dev`), share creation, indexing (`e2dsa`/`e2t`), file-magic checks |
|
||||||
|
|
||||||
|
All published through [middleman](middleman.md) as shown. A shared `media` group (gid 2000) plus
|
||||||
|
`UMask=0002` on Radarr/Sonarr gives the apps coordinated access to the media volume.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: jackflix -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| jackflix-ctr | internal | `10.100.2.6/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::6/64` | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## VPN download path
|
||||||
|
|
||||||
|
[`networking.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/jackflix/networking.nix)
|
||||||
|
defines a `vpn` WireGuard netdev to **AirVPN NL** (key + PSK from age secrets, MTU 1320 per
|
||||||
|
AirVPN, fwmark `42`, route table `51820`):
|
||||||
|
|
||||||
|
- Policy routing keeps colony traffic on the main table (from/to `prefixes.all` v4/v6 rules at
|
||||||
|
priority 100, plus a default-route suppression) while everything else falls through to the VPN
|
||||||
|
table — so the services stay reachable on the `ctrs` network while outbound torrent traffic
|
||||||
|
exits via AirVPN. `DNSDefaultRoute` is disabled on `host0`; the VPN provides DNS.
|
||||||
|
- `transmission` and `jackett` `bindsTo` `systemd-networkd-wait-online@vpn.service` — they only
|
||||||
|
run while the tunnel is up.
|
||||||
|
- AirVPN forwards peer port `28457` to Transmission (`peer-port`); the firewall accepts it and
|
||||||
|
drops other new inbound TCP from `vpn`, while non-VPN input is limited to the service ports
|
||||||
|
(netdata, Transmission, Jackett, Radarr, Sonarr, Jellyfin, PhotoPrism) plus copyparty's `3923`
|
||||||
|
from the base config and Jellyseerr's `5055`.
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Media lives on the shared `/mnt/media` volume (bind-mounted read-write from `shill`); Transmission
|
||||||
|
downloads into `/mnt/media/downloads/torrents` with a `.incomplete` dir, 28 MB/s up/down limits
|
||||||
|
and a seed ratio limit of 2.0.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/jackflix/default.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/jackflix/default.nix) — container definition and the media services
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/jackflix/networking.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/jackflix/networking.nix) — AirVPN WireGuard netdev, policy routing and VPN firewall rules
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# middleman
|
||||||
|
|
||||||
|
The front-end reverse proxy for the colony's public web services — the single ingress that
|
||||||
|
`estuary` DNATs HTTP/HTTPS (and Matrix federation on `:8448`) to. Terminates TLS with wildcard
|
||||||
|
certificates it issues itself, provides nginx-sso for gated vhosts, and runs a librespeed
|
||||||
|
backend.
|
||||||
|
|
||||||
|
- **Source:** [`shill/containers/middleman/`](../../../../../nixos/boxes/colony/vms/shill/containers/middleman)
|
||||||
|
(`default.nix`, `vhosts.nix`)
|
||||||
|
- **Host:** NixOS container on [`shill`](../../shill.md) (`my.containers` ephemeral nspawn on the
|
||||||
|
`ctrs` bridge; bind-mounts `/mnt/media` read-only for the static file vhosts)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **nginx** reverse proxy with the `vts` and `fancyindex` modules, Brotli, kTLS, a proxy cache,
|
||||||
|
and a dynamic resolver pointed at `estuary` (`proxyResolveWhileRunning`) so upstreams re-resolve
|
||||||
|
at runtime. All vhosts are declared in
|
||||||
|
[`vhosts.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/middleman/vhosts.nix);
|
||||||
|
upstreams are addressed by their internal container/VM hostnames under `ams1.int.nul.ie`.
|
||||||
|
nginx is made to wait for `colony-psql` (`systemdAwaitPostgres`) before starting — a hack for
|
||||||
|
it getting stuck on DNS early in boot.
|
||||||
|
- **ACME** — issues the wildcard certificates its own vhosts are served with (it is not a shared
|
||||||
|
CA for the other boxes):
|
||||||
|
- `ams1.int.nul.ie` + `*.ams1.int.nul.ie` via a lego `exec` DNS challenge that SSHes to
|
||||||
|
`pdns-file-records@estuary-vm` to add/remove records in the internal PowerDNS zone. This is
|
||||||
|
the default cert (`useACMEHost`) for vhosts on the internal domain.
|
||||||
|
- `nul.ie` + `*.nul.ie` + `*.s3.nul.ie` via the Cloudflare DNS provider. A `postRun` hook
|
||||||
|
`scp`s the renewed key/cert to the `mail` (Mailcow) VM and runs `mailcow-ssl-reload` there.
|
||||||
|
- Certs reload `nginx` on renewal; the `acme` group owns the secret files and `nginx` is a
|
||||||
|
member.
|
||||||
|
- **nginx-sso** — single sign-on at `sso.nul.ie` (one instance, `generic`), with Google OAuth as
|
||||||
|
the default method and a simple username/password provider. Cookie domain is `.nul.ie`.
|
||||||
|
SSO-gated vhosts below include `server-generic.conf` / `location-generic.conf` from
|
||||||
|
`/etc/nginx/includes/sso/`.
|
||||||
|
- **librespeed** — speed test, backend + frontend, published as `speed.nul.ie` and
|
||||||
|
`librespeed.ams1.int.nul.ie` (both proxy to `localhost:8989`).
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: middleman -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| middleman-ctr | internal | `10.100.2.2/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::2/64` | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
The firewall allows `http`, `https` and `8448` (Matrix federation). A small nftables SNAT rule
|
||||||
|
rewrites outbound IPv6 to the container's own address on `host0`.
|
||||||
|
|
||||||
|
## Published vhosts
|
||||||
|
|
||||||
|
Everything is under `*.nul.ie` with the public wildcard cert unless noted; defaults applied to
|
||||||
|
all vhosts are `onlySSL`, kTLS and HTTP/2. "SSO" = gated behind nginx-sso (`generic` instance).
|
||||||
|
|
||||||
|
| Host | Upstream | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `nul.ie` (`_`, default) | static | landing page (`index.html`, CV PDF, SSH pubkey); serves Matrix `.well-known`s and redirects `webfinger`/`nodeinfo`/`host-meta` → `toot.nul.ie`, `atproto-did` → `pds.nul.ie`; `forceSSL` (plain HTTP redirects to HTTPS) |
|
||||||
|
| `localhost` | — | loopback-only VTS status page at `/status` (scraped by netdata); plain HTTP |
|
||||||
|
| `sso.nul.ie` | `localhost:8082` | nginx-sso endpoint |
|
||||||
|
| `netdata-colony.nul.ie` | `<host>.ams1.int.nul.ie:19999` | netdata fan-out over `vm`, `fw`, `ctr`, `oci`, `http`, `jackflix-ctr`, `chatterbox-ctr`, `colony-psql-ctr`; **SSO** |
|
||||||
|
| `pass.nul.ie` | `vaultwarden-ctr:8080` | [vaultwarden](vaultwarden.md); `/notifications/hub` proxied with websockets |
|
||||||
|
| `matrix.nul.ie` | `chatterbox-ctr:8008` | [chatterbox](chatterbox.md) Synapse client + federation; also listens on `:8448` as federation `default_server`; `= /` redirects to Element; serves Matrix `.well-known`s |
|
||||||
|
| `element.nul.ie` | static `element-web` | Element configured for the `nul.ie` homeserver |
|
||||||
|
| `torrents.nul.ie` | `jackflix-ctr:9091` | Transmission ([jackflix](jackflix.md)); **SSO** |
|
||||||
|
| `jackett.nul.ie` | `jackflix-ctr:9117` | **SSO** |
|
||||||
|
| `radarr.nul.ie` | `jackflix-ctr:7878` | **SSO**; websockets |
|
||||||
|
| `sonarr.nul.ie` | `jackflix-ctr:8989` | **SSO**; websockets |
|
||||||
|
| `gib.nul.ie` | `jackflix-ctr:5055` | Jellyseerr requests |
|
||||||
|
| `jackflix.nul.ie` | `jackflix-ctr:8096` | Jellyfin; `/socket` websockets; `/` redirects to `/web/` |
|
||||||
|
| `toot.nul.ie` | `toot-ctr:80` | Mastodon — **upstream currently disabled**, see [toot](toot.md) |
|
||||||
|
| `pds.nul.ie` | `toot-ctr:3000` | Bluesky PDS ([toot](toot.md)); websockets |
|
||||||
|
| `share.nul.ie` | `object-ctr:9090` | Sharry ([object](object.md)); websockets |
|
||||||
|
| `stuff.nul.ie` | `jackflix-ctr:3923` | copyparty |
|
||||||
|
| `public.nul.ie` (+ alias `p.nul.ie`) | static `/mnt/media/public` | fancyindex file listing; `addSSL` so plain HTTP also works |
|
||||||
|
| `mc-map.nul.ie` | `simpcraft-oci:8100` | Minecraft map (OCI container on `whale2`) |
|
||||||
|
| `mc-rail.nul.ie` | `simpcraft-oci:3876` | Minecraft railway map |
|
||||||
|
| `mc-map-kink.nul.ie` | `kinkcraft-oci:8100` | Minecraft map |
|
||||||
|
| `speed.nul.ie` | `localhost:8989` | librespeed |
|
||||||
|
| `librespeed.ams1.int.nul.ie` | `localhost:8989` | librespeed on the internal domain (internal wildcard cert) |
|
||||||
|
| `md.nul.ie` | `object-ctr:3000` | HedgeDoc; websockets |
|
||||||
|
| `pb.nul.ie` | `object-ctr:8088` | wastebin |
|
||||||
|
| `photos.nul.ie` | `jackflix-ctr:2342` | PhotoPrism; websockets |
|
||||||
|
| `pront.nul.ie` | `stream-hi.h.nul.ie:5000` | OctoPrint on the home network; `/webcam/` → `:5050`; **SSO** |
|
||||||
|
| `hass.nul.ie` | `hass-ctr.h.nul.ie:8123` | Home Assistant (home network); websockets |
|
||||||
|
| `hass-john.nul.ie` | `john-valorant-tun.ams1.int.nul.ie:8123` | remote Home Assistant over the point-to-point tunnel; websockets |
|
||||||
|
| `minio.nul.ie` | `object-ctr:9001` | MinIO console; `/ws` websockets |
|
||||||
|
| `s3.nul.ie` (+ `*.s3.nul.ie`) | `object-ctr:9000` | MinIO S3 API (virtual-host style via the `*.s3` wildcard cert); `/gitea/packages/` has a hack forcing the correct `Content-Type` for Docker image manifests |
|
||||||
|
| `nix-cache.nul.ie` | `object-ctr:5000` | Harmonia Nix binary cache; `.narinfo`/`nar/`/`serve/` paths get immutable `Cache-Control`/`Expires` headers |
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/middleman/default.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/middleman/default.nix) — container definition: nginx, ACME, nginx-sso, librespeed, secrets
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/middleman/vhosts.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/middleman/vhosts.nix) — all virtual hosts, the SSO include helpers, and the `.well-known` tree
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/middleman/default.html`](../../../../../nixos/boxes/colony/vms/shill/containers/middleman/default.html) — default vhost landing page
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# object
|
||||||
|
|
||||||
|
Object storage and the Nix binary cache, plus a few small self-hosted web apps (Sharry,
|
||||||
|
HedgeDoc, wastebin).
|
||||||
|
|
||||||
|
- **Source:** [`shill/containers/object.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/object.nix)
|
||||||
|
- **Host:** NixOS container on [`shill`](../../shill.md) (bind-mounts `/mnt/minio` and
|
||||||
|
`/mnt/nix-cache` read-write)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
| Service | Port | Purpose |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| MinIO | `9000` (S3) / `9001` (console) | S3-compatible object storage, `s3.nul.ie` + `*.s3.nul.ie` (virtual-host style via `MINIO_DOMAIN`), console at `minio.nul.ie`; region `eu-central-1`; data on the `/mnt/minio` XFS volume |
|
||||||
|
| Harmonia | `5000` | Nix binary cache at `nix-cache.nul.ie` — `harmonia-dev` cache serves `shill`'s `/nix/store` out of a dedicated store view rooted at `/var/lib/harmonia` (bind-mounted from `/mnt/nix-cache`), signed with the `nix-cache.key` secret; a `harmonia` user with authorized keys exists for cache pushes; priority 30 |
|
||||||
|
| Sharry | `9090` | file sharing at `share.nul.ie`; Postgres on [colony-psql](colony-psql.md), files stored in the `share` MinIO bucket; fixed `dev` account + invite signup; mail via `mail.nul.ie`; max share size 128G |
|
||||||
|
| HedgeDoc | `3000` | collaborative markdown notes at `md.nul.ie`; Postgres on [colony-psql](colony-psql.md); anonymous edits but no anonymous notes, email login, no open email registration |
|
||||||
|
| wastebin | `8088` | pastebin at `pb.nul.ie` |
|
||||||
|
| atticd | `8069` | **currently disabled** (`services.atticd.enable = false`) — an alternative Nix cache that would store locally and sit behind `nix-cache.nul.ie`; config (including the `object/atticd.env` secret) is kept around |
|
||||||
|
|
||||||
|
Everything public is fronted by [middleman](middleman.md) (see its vhost table). The
|
||||||
|
`minio-client` is installed and the user's `~/.mc/config.json` points at an age-secret config.
|
||||||
|
`minio-2025-10-15T17-29-55Z` is allowlisted via `permittedInsecurePackages` (flagged as a TODO).
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: object -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| object-ctr | internal | `10.100.2.7/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::7/64` | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Backing services
|
||||||
|
|
||||||
|
- [colony-psql](colony-psql.md) — Sharry and HedgeDoc databases (atticd too, when enabled).
|
||||||
|
- MinIO buckets back other boxes' services: Gitea LFS/packages (with the `middleman` MIME hack
|
||||||
|
for Docker manifests), Mastodon's `mastodon` bucket and the Bluesky PDS `pds` bucket on
|
||||||
|
[toot](toot.md), and Sharry's `share` bucket.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/object.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/object.nix) — container definition and all services
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# qclk
|
||||||
|
|
||||||
|
A WireGuard management appliance for the `qclk` network — it terminates the `management`
|
||||||
|
tunnel and routes/NATs the `qclk` prefix. No service daemon is currently defined in the config;
|
||||||
|
the container provides the network plumbing and opens the API port.
|
||||||
|
|
||||||
|
- **Source:** [`shill/containers/qclk/`](../../../../../nixos/boxes/colony/vms/shill/containers/qclk)
|
||||||
|
(`default.nix`)
|
||||||
|
- **Host:** NixOS container on [`shill`](../../shill.md)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **WireGuard `management` interface** — listens on UDP `51821` (`lib.my.c.colony.qclk.wgPort`,
|
||||||
|
allowed through the firewall; `estuary` port-forwards it here) with the private key from the
|
||||||
|
`qclk/wg.key` age secret. Managed devices are static peers, each pinned to its own address in
|
||||||
|
the `qclk` prefix (`10.100.4.0/24`); the peer list currently has a single entry (host 2).
|
||||||
|
- **Routing/NAT** — the container itself is host 1 of the `qclk` prefix. `shill` routes
|
||||||
|
`10.100.4.0/24` to this container, and outbound traffic from `host0` into `management` is
|
||||||
|
SNATed to the container's `qclk` address. Forwarding into `management` is accepted from the
|
||||||
|
AS211024 trusted IPv4 ranges (`lib.my.c.as211024.trusted.v4`).
|
||||||
|
- **API port** — TCP `8080` is accepted on the `management` interface (`apiPort`), but note
|
||||||
|
`services = { }`: whatever serves the qclk API is not defined in this configuration today.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: qclk -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| qclk-ctr | internal | `10.100.2.10/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::a/64` | ams1.int.nul.ie | |
|
||||||
|
| qclk | qclk | `10.100.4.1/24` | — | — | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
Two assignments: `internal` on the `ctrs` network like the other containers, and `qclk` — host 1
|
||||||
|
of the `qclk` prefix on the `management` WireGuard interface (IPv4 only, no DNS name).
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/qclk/default.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/qclk/default.nix) — container definition: WireGuard netdev, peer list and firewall rules
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# toot
|
||||||
|
|
||||||
|
Federated-social container. Despite the name, the only service actually running is a **Bluesky
|
||||||
|
PDS** — the Mastodon instance ("toots") is **currently disabled**.
|
||||||
|
|
||||||
|
- **Source:** [`shill/containers/toot.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/toot.nix)
|
||||||
|
- **Host:** NixOS container on [`shill`](../../shill.md)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **bluesky-pds** — the active service. `pds.nul.ie` on port `3000`, fronted by
|
||||||
|
[middleman](middleman.md) (which also redirects `/.well-known/atproto-did` here). Invites
|
||||||
|
required; blob store is the `pds` bucket on [object](object.md)'s MinIO (`s3.nul.ie`,
|
||||||
|
`eu-central-1`), upload limit 50 MiB; federation settings point at the stock Bluesky
|
||||||
|
infrastructure (`plc.directory`, `api.bsky.app`, `mod.bsky.app`, `bsky.network` crawlers).
|
||||||
|
Secrets (including the S3 credentials) come from the `toot/pds.env` age secret. Email from
|
||||||
|
`pds@nul.ie`.
|
||||||
|
- **mastodon** — **disabled** (`services.mastodon.enable = false`). The full config is still
|
||||||
|
present: `LOCAL_DOMAIN = nul.ie` with `WEB_DOMAIN = toot.nul.ie`, Postgres on
|
||||||
|
[colony-psql](colony-psql.md), local Redis, SMTP via `mail.nul.ie`, media in the `mastodon`
|
||||||
|
MinIO bucket (`S3_ALIAS_HOST = mastodon.s3.nul.ie`), 4 streaming processes, and media
|
||||||
|
auto-cleanup after 30 days. [middleman](middleman.md) still proxies `toot.nul.ie` →
|
||||||
|
`toot-ctr:80` and redirects the `webfinger`/`nodeinfo`/`host-meta` well-knowns there, but with
|
||||||
|
the service off those endpoints are dead. The config notes the removed `otpSecretFile` option
|
||||||
|
would need addressing before Mastodon can come back.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: toot -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| toot-ctr | internal | `10.100.2.8/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::8/64` | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
The firewall allows `http` (the Mastodon nginx vhost) and the PDS port `3000` besides netdata.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The local nginx still carries the Mastodon virtual host (`toot.nul.ie`) with proxy-header
|
||||||
|
overrides for being behind `middleman` — part of the preserved-but-disabled Mastodon setup.
|
||||||
|
- `mastodon-init-dirs` appends the S3 secret key to Mastodon's `.secrets_env` (the module has no
|
||||||
|
option for a secret-key file), and `mastodon-init-db` waits for `colony-psql` — moot while
|
||||||
|
Mastodon is disabled.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/toot.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/toot.nix) — container definition; active PDS config and the preserved (disabled) Mastodon config
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# vaultwarden
|
||||||
|
|
||||||
|
[Vaultwarden](https://github.com/dani-garcia/vaultwarden), a Bitwarden-compatible password
|
||||||
|
manager, published as `pass.nul.ie` through [middleman](middleman.md).
|
||||||
|
|
||||||
|
- **Source:** [`shill/containers/vaultwarden.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/vaultwarden.nix)
|
||||||
|
- **Host:** NixOS container on [`shill`](../../shill.md)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **vaultwarden** — HTTP on `[::]:8080`, WebSocket notifications on `3012` (both allowed through
|
||||||
|
the firewall). Web vault enabled, signups disabled, Bitwarden push notifications enabled
|
||||||
|
(`PUSH_ENABLED`). `DOMAIN` is `https://pass.nul.ie`.
|
||||||
|
- **SMTP** via `mail.nul.ie:587` (STARTTLS) as `pass@nul.ie`; credentials and other sensitive
|
||||||
|
settings come from the `vaultwarden/config.env` age secret.
|
||||||
|
- **Backups** — a `borgbackup` job pushes `/var/lib/vaultwarden` to rsync.net
|
||||||
|
(`zh2855@zh2855.rsync.net:borg/vaultwarden2`), repokey-encrypted (passphrase and SSH key from
|
||||||
|
secrets), `zstd,10` compression, keeping 7 daily / 4 weekly / all monthly archives.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: vaultwarden -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| vaultwarden-ctr | internal | `10.100.2.3/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::3/64` | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
`/var/lib/vaultwarden` is persisted through `my.tmproot.persistence` — like the other
|
||||||
|
`shill` containers the root is ephemeral and real state lives under `/persist` (bind-mounted
|
||||||
|
from the host's `/persist/containers/vaultwarden`).
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/vaultwarden.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/vaultwarden.nix) — container definition, service config and the borgbackup job
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# waffletail
|
||||||
|
|
||||||
|
The colony Tailscale node: a subnet router and exit node that advertises the colony prefixes
|
||||||
|
into the tailnet.
|
||||||
|
|
||||||
|
- **Source:** [`shill/containers/waffletail.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/waffletail.nix)
|
||||||
|
- **Host:** NixOS container on [`shill`](../../shill.md)
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **tailscale** — joins via an auth key from secrets (`tailscale-auth.key`) against the
|
||||||
|
self-hosted Headscale control plane at `hs.nul.ie`. Runs with `--netfilter-mode=off` (firewall
|
||||||
|
is managed by the repo's own nftables rules), `--advertise-exit-node`, and
|
||||||
|
`--advertise-routes` covering the whole colony — `10.100.0.0/16` and `2a0e:97c0:4d2:10::/60`.
|
||||||
|
Does **not** accept routes itself. Listens on UDP `41641` (`openFirewall`), which `estuary`
|
||||||
|
port-forwards to this container.
|
||||||
|
- `shill` routes the Tailscale prefixes (`100.64.0.0/10`, `fd7a:115c:a1e0::/48`) to this
|
||||||
|
container, so colony hosts can reach tailnet clients and vice versa.
|
||||||
|
- nftables: `tailscale0` is a trusted interface; forwarding from `host0` into Tailscale is
|
||||||
|
allowed for the colony source ranges, and tailnet-sourced traffic leaving via `host0` is
|
||||||
|
SNATed to the container's colony addresses (except when destined to the colony ranges
|
||||||
|
themselves).
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: waffletail -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| waffletail-ctr | internal | `10.100.2.9/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::9/64` | ams1.int.nul.ie | |
|
||||||
|
| waffletail | tailscale | `100.64.0.5/32` | `fd7a:115c:a1e0::5/128` | — | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
Two assignments: `internal` on the `ctrs` network like the other containers, and `tailscale` —
|
||||||
|
its addresses on the tailnet itself (host 5 of `100.64.0.0/10` and `fd7a:115c:a1e0::/48`; no
|
||||||
|
DNS name).
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/shill/containers/waffletail.nix`](../../../../../nixos/boxes/colony/vms/shill/containers/waffletail.nix) — container definition, Tailscale setup and forward/NAT rules
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# whale2
|
||||||
|
|
||||||
|
The colony podman/OCI host, dedicated to game servers (kept off `shill` so
|
||||||
|
container churn and resource use stay isolated).
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/colony/vms/whale2/`](../../../nixos/boxes/colony/vms/whale2)
|
||||||
|
(`default.nix`, `valheim.nix`, `minecraft/`, `enshrouded.nix`)
|
||||||
|
- **Host:** VM on `colony`
|
||||||
|
- **nixpkgs:** `mine`
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Runs OCI containers via podman (`virtualisation.oci-containers`, netavark
|
||||||
|
backend, `firewall_driver = "none"` so podman doesn't fight `my.firewall`).
|
||||||
|
- Each game server gets its own routable address on the `colony` netavark
|
||||||
|
network (defined in `/etc/containers/networks/colony.json`), which is backed
|
||||||
|
by the `oci` interface and the `prefixes.oci` v4/v6 ranges; per-game
|
||||||
|
addresses come from `extraAssignments` (`valheim-oci`, `simpcraft-oci`, …)
|
||||||
|
and are passed to podman with `--network=colony:ip=…` (`lib.my.dockerNetAssignment`).
|
||||||
|
- `estuary` forwards the game ports in (see `firewallForwards`), so the
|
||||||
|
servers are reachable on the public IP as well as directly over IPv6.
|
||||||
|
- `/var/lib/containers` is a dedicated XFS disk (project quotas).
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: whale2 -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| whale-vm (oci) | internal | `94.142.241.226/32` | `2a0e:97c0:4d2:11::3/64` | ams1.int.nul.ie | |
|
||||||
|
| whale-vm-oci | oci | `10.100.3.1/24` | `2a0e:97c0:4d2:13::1/64` | ams1.int.nul.ie | |
|
||||||
|
| whale-vm-routing | routing | `10.100.1.3/24 gw 10.100.1.1` | — | ams1.int.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Game servers
|
||||||
|
|
||||||
|
The OCI containers are documented here (they have no pages of their own).
|
||||||
|
Addresses are the per-container `extraAssignments` on the `oci` network;
|
||||||
|
ports are the public ones forwarded by `estuary`.
|
||||||
|
|
||||||
|
| Container | Address (v4 / v6 host) | Ports | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `valheim` | `10.100.3.2` / `2a0e:97c0:4d2:13::2` | `2456-2457`/udp | running |
|
||||||
|
| `simpcraft` | `10.100.3.3` / `2a0e:97c0:4d2:13::3` | `25565` tcp+udp | running |
|
||||||
|
| `simpcraft-staging` | `10.100.3.4` / `2a0e:97c0:4d2:13::4` | `25566` tcp | **disabled** (commented out) |
|
||||||
|
| `enshrouded` | `10.100.3.5` / `2a0e:97c0:4d2:13::5` | `15636-15637`/udp | **disabled** (`enshrouded.nix` not imported) |
|
||||||
|
| `kevcraft` | `10.100.3.6` / `2a0e:97c0:4d2:13::6` | `25567` tcp+udp | running |
|
||||||
|
| `kinkcraft` | `10.100.3.7` / `2a0e:97c0:4d2:13::7` | `25568` tcp+udp | running |
|
||||||
|
| `graeme` | `10.100.3.8` / `2a0e:97c0:4d2:13::8` | `25569` tcp+udp | running |
|
||||||
|
|
||||||
|
- **valheim** ([`valheim.nix`](../../../nixos/boxes/colony/vms/whale2/valheim.nix)) —
|
||||||
|
`lloesche/valheim-server`, public server "amogus sus", world `simpland2`,
|
||||||
|
allow-listed Steam IDs, password from agenix.
|
||||||
|
- **simpcraft** ([`minecraft/`](../../../nixos/boxes/colony/vms/whale2/minecraft)) —
|
||||||
|
`itzg/minecraft-server` (self-built `git.nul.ie/dev/craftblock` image),
|
||||||
|
Modrinth "Simpcraft" modpack, whitelist + ops, 8 GiB heap.
|
||||||
|
**simpcraft-staging** is the same setup pinned to an older pack version,
|
||||||
|
currently commented out.
|
||||||
|
- **kevcraft** — vanilla Minecraft 1.20.1, 4 GiB heap, extra op.
|
||||||
|
- **kinkcraft** — same Simpcraft modpack as `simpcraft`, 6 GiB heap.
|
||||||
|
- **graeme** — vanilla Minecraft on hard difficulty with its own whitelist.
|
||||||
|
- **enshrouded** ([`enshrouded.nix`](../../../nixos/boxes/colony/vms/whale2/enshrouded.nix)) —
|
||||||
|
`sknnr/enshrouded-dedicated-server` ("UWUshrouded"); the file exists but is
|
||||||
|
commented out of `whale2`'s `imports`, so the server is down (its forwards
|
||||||
|
and DNS records remain).
|
||||||
|
|
||||||
|
The Minecraft containers share one whitelist/ops list and agenix env file
|
||||||
|
(`whale2/simpcraft.env`, which also carries the RCON password).
|
||||||
|
|
||||||
|
## Backups
|
||||||
|
|
||||||
|
A local borg job (`services.borgbackup.jobs.simpcraft`) archives the
|
||||||
|
`simpcraft` world every ~15 minutes (offset from the usual 5-minute autosave
|
||||||
|
ticks) into `/var/lib/containers/backup/simpcraft`, using `mcrcon` to
|
||||||
|
`save-off`/`save-on` around each run. Retention is short (12 h + 48 hourly) —
|
||||||
|
this is for quick world rollback, not disaster recovery (the `oci` LV itself
|
||||||
|
is covered by `colony`'s `borgthin`).
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/colony/vms/whale2/default.nix`](../../../nixos/boxes/colony/vms/whale2/default.nix) — VM config, podman/netavark setup, `extraAssignments`.
|
||||||
|
- [`nixos/boxes/colony/vms/whale2/valheim.nix`](../../../nixos/boxes/colony/vms/whale2/valheim.nix) — Valheim server.
|
||||||
|
- [`nixos/boxes/colony/vms/whale2/minecraft/default.nix`](../../../nixos/boxes/colony/vms/whale2/minecraft/default.nix) — the Minecraft servers + world backup job.
|
||||||
|
- [`nixos/boxes/colony/vms/whale2/enshrouded.nix`](../../../nixos/boxes/colony/vms/whale2/enshrouded.nix) — Enshrouded server (disabled, not imported).
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Home site
|
||||||
|
|
||||||
|
The home network (domain `h.nul.ie`): a redundant pair of routers in front of a VM host, an
|
||||||
|
NVMe-oF storage target, an IoT container host, and a workstation. The two routers — `river` (a VM)
|
||||||
|
and `stream` (a physical box) — are built from one shared
|
||||||
|
[`routing-common`](../../../nixos/boxes/home/routing-common) definition as an active/backup VRRP
|
||||||
|
pair, and everything clients touch (gateway, DNS) is a floating VIP that follows the master.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/home/`](../../../nixos/boxes/home)
|
||||||
|
|
||||||
|
## Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
h.nul.ie
|
||||||
|
├── palace (physical VM host — AMD, 100G, SR-IOV)
|
||||||
|
│ ├── river ── primary router VM (PPPoE / Digiweb WAN)
|
||||||
|
│ ├── cellar ─ NVMe-oF / SPDK storage target VM
|
||||||
|
│ └── sfh ──── container host VM ("services for home")
|
||||||
|
│ ├── hass ── Home Assistant + Frigate + MQTT (container)
|
||||||
|
│ └── unifi ─ UniFi controller (container)
|
||||||
|
├── stream (physical secondary router — Virgin Media WAN)
|
||||||
|
└── castle (workstation / gaming desktop — netboot, NVMe-oF root)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Machines
|
||||||
|
|
||||||
|
| Box | Role | Host | Page |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `palace` | VM host | physical | [palace.md](palace.md) |
|
||||||
|
| `river` | Primary router (VRRP pair with `stream`) | VM on `palace` | [river.md](river.md) |
|
||||||
|
| `stream` | Secondary router (VRRP pair with `river`) | physical | [stream.md](stream.md) |
|
||||||
|
| `cellar` | NVMe-oF / SPDK storage target | VM on `palace` | [cellar.md](cellar.md) |
|
||||||
|
| `sfh` | NixOS container host | VM on `palace` | [sfh.md](sfh.md) |
|
||||||
|
| `castle` | Workstation / gaming desktop | physical | [castle.md](castle.md) |
|
||||||
|
| `hass` | Home Assistant + Frigate + MQTT | container on `sfh` | [sfh/containers/hass.md](sfh/containers/hass.md) |
|
||||||
|
| `unifi` | UniFi controller | container on `sfh` | [sfh/containers/unifi.md](sfh/containers/unifi.md) |
|
||||||
|
|
||||||
|
## Router VIPs
|
||||||
|
|
||||||
|
Clients never use a router's real address: `kea` (DHCP) and `radvd` (RAs) hand out the per-VLAN
|
||||||
|
floating VIPs as both gateway and DNS server, and `keepalived` moves them between `river` and
|
||||||
|
`stream`. They are declared as pseudo-systems (`router-hi` / `router-lo` / `router-ut`) in
|
||||||
|
`routing-common`'s `extraAssignments`, with addresses from `lib.my.c.home.vips`
|
||||||
|
([`lib/constants.nix`](../../../lib/constants.nix)); `pdns-recursor` binds them on both routers
|
||||||
|
(`ip_nonlocal_bind`) so DNS follows the master instead of relying on client resolver timeouts.
|
||||||
|
|
||||||
|
| Pseudo-system | Network | IPv4 | IPv6 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `router-hi` | `hi` (VLAN 100) | `192.168.71.254/22` | `2a0e:97c0:4d0:1::ffff/64` |
|
||||||
|
| `router-lo` | `lo` (VLAN 110) | `192.168.79.254/21` | `2a0e:97c0:4d0:2::ffff/64` |
|
||||||
|
| `router-ut` | `untrusted` (VLAN 120) | `192.168.80.254/24` | `2a0e:97c0:4d0:3::ffff/64` |
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- The IPv6 gateway clients learn from RAs is the link-local `fe80::1` on each VLAN (a
|
||||||
|
`virtual_ipaddress_excluded` VIP; `radvd` advertises from it), not the global VIP above.
|
||||||
|
- `keepalived` also floats a VIP on the `as211024` mesh interface (`10.100.50.4`,
|
||||||
|
`2a0e:97c0:4df:0:1::ffff`) — it has no pseudo-system because nothing client-facing uses it.
|
||||||
|
- The `untrusted` VLAN is an exception to "DNS follows the master": its DHCP/RA options hand out
|
||||||
|
Cloudflare resolvers (`1.1.1.1` / `2606:4700:4700::1111`), not the VIP.
|
||||||
|
|
||||||
|
## Networks
|
||||||
|
|
||||||
|
| Network | VLAN | IPv4 | IPv6 | MTU |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `core` | native | `192.168.64.0/24` | — | 1500 |
|
||||||
|
| `hi` | 100 | `192.168.68.0/22` | `2a0e:97c0:4d0:1::/64` | 9000 |
|
||||||
|
| `lo` | 110 | `192.168.72.0/21` | `2a0e:97c0:4d0:2::/64` | 1500 |
|
||||||
|
| `untrusted` | 120 | `192.168.80.0/24` | `2a0e:97c0:4d0:3::/64` | 1500 |
|
||||||
|
| `wan` (stream) | 130 | DHCP public lease + modem mgmt `192.168.0.0/24` | — | 1500 |
|
||||||
|
| `pon-isp` (river) | 10 | PPPoE transport (no L3) | — | 1508 |
|
||||||
|
| `wan-pon-ont` (river) | 140 | ONT mgmt `192.168.100.0/24` | — | 1500 |
|
||||||
|
|
||||||
|
The logical network map lives in [networking.md](../../networking.md).
|
||||||
|
|
||||||
|
## Switch fabric
|
||||||
|
|
||||||
|
The boxes hang off three hand-configured switches — `jim` and `dave` (MikroTik, RouterOS) and
|
||||||
|
`brian` (Ubiquiti, UniFi) — which are **not** managed by this flake. The physical topology, VLAN
|
||||||
|
map, the Digiweb WAN path (trunked VLAN 10 + PVID 140 at the ONT edge), and the multi-ONT plan are
|
||||||
|
documented in [switches.md](switches.md).
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# castle
|
||||||
|
|
||||||
|
The home workstation / gaming desktop. Diskless-style: it netboots from `river` and keeps its
|
||||||
|
root storage on NVMe-oF volumes from `cellar`.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/home/castle/`](../../../nixos/boxes/home/castle) (`default.nix`)
|
||||||
|
- **Host:** physical
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- AMD desktop running the GUI stack (`my.gui.enable`, Sway/Wayland via home-manager), PipeWire
|
||||||
|
(low-latency `quantum 128`, EasyEffects, jacktrip), Bluetooth, Thunderbolt (`bolt`).
|
||||||
|
- **Netboot client** (`my.netboot.client.enable`): the firmware iPXE-boots off the 2.5G NIC —
|
||||||
|
kea's client-class for `castle` matches `et2.5g`'s MAC (`c8:7f:54:6e:17:0f`) and points at
|
||||||
|
`boot.h.nul.ie` on [`river`](river.md).
|
||||||
|
- **Root on NVMe-oF**: `/nix`, `/persist` and `/home` are `/dev/nvmeof/*` LVs on the
|
||||||
|
`nqn.2016-06.io.spdk:castle` namespace from [`cellar`](cellar.md) (`my.nvme.boot`,
|
||||||
|
`192.168.68.80`, RDMA). The initrd brings up `et100g`/`lan-hi` plus `roceBootModules` to reach
|
||||||
|
it, and the running system keeps `KeepConfiguration=static` on `lan-hi` so networkd never drops
|
||||||
|
the NVMe-oF address. The root itself is a 24 GiB tmpfs (`my.tmproot`).
|
||||||
|
- Local virtualisation: `libvirtd` + `virt-manager` are enabled and the IOMMU is on
|
||||||
|
(`amd_iommu=on`), but no VFIO/GPU-passthrough is configured in the box config today.
|
||||||
|
- Both firewalls are off (`networking.firewall.enable` and `my.firewall.enable` — it's a trusted
|
||||||
|
desktop on `hi`).
|
||||||
|
- Misc: `binfmt` emulation for `aarch64-linux`/`armv7l-linux`, `recursive-nix`, Wireshark,
|
||||||
|
`rdma-core`/`qperf` for the RoCE link. A `drm-amd-display` flicker patch sits commented out in
|
||||||
|
`kernelPatches`.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: castle -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| castle | hi | `192.168.68.40/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::3:1/64` | h.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
- `et100g` (100G, MTU 9000) carries `lan-hi` (the `hi` assignment, statically `.40` — also pinned
|
||||||
|
by a kea reservation on its MAC) and `lan-lo`.
|
||||||
|
- `lan-lo` is a secondary leg: DHCPv4 with `UseGateway`/`UseDNS` off and RAs accepted with
|
||||||
|
gateway/DNS use off — present for reaching `lo` devices, never a default route.
|
||||||
|
- `et2.5g` (netboot) and `et10g` are renamed but carry no network config.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/home/castle/default.nix`](../../../nixos/boxes/home/castle/default.nix) — box
|
||||||
|
config: netboot/NVMe-oF boot, 100G networking, GUI/audio, virtualisation.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# cellar
|
||||||
|
|
||||||
|
The home storage target. A VM on `palace` that drives three passed-through NVMe disks with SPDK
|
||||||
|
and exports them over NVMe-oF/RDMA — `river`, `sfh` and `castle` all run their root storage off
|
||||||
|
it.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/home/palace/vms/cellar/`](../../../nixos/boxes/home/palace/vms/cellar)
|
||||||
|
(`default.nix`, `spdk.nix`)
|
||||||
|
- **Host:** VM on `palace`
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Runs an **SPDK userspace target** (`my.spdk`, [`spdk.nix`](../../../nixos/boxes/home/palace/vms/cellar/spdk.nix)):
|
||||||
|
the kernel `nvme` driver is blacklisted so SPDK can claim the three NVMe controllers directly
|
||||||
|
(host BDFs `41:00.0`–`43:00.0`, attached in the guest as `02:00.0`–`04:00.0`).
|
||||||
|
- Builds a **RAID-0** (`NVMeRaid`, 64 KiB strip) across the three drives and exports one
|
||||||
|
partition per consumer as an **NVMe-oF subsystem over RDMA** (port 4420) on the `hi` network:
|
||||||
|
|
||||||
|
| Bdev | NQN | Consumer |
|
||||||
|
|---|---|---|
|
||||||
|
| `NVMeRaidp1` | `nqn.2016-06.io.spdk:river` | [`river`](river.md) |
|
||||||
|
| `NVMeRaidp2` | `nqn.2016-06.io.spdk:castle` | [`castle`](castle.md) |
|
||||||
|
| `NVMeRaidp3` | `nqn.2016-06.io.spdk:sfh` | [`sfh`](sfh.md) |
|
||||||
|
|
||||||
|
Each subsystem is pinned to its consumer's `hostnqn` (the `my.nvme.uuid` on the client side).
|
||||||
|
- `spdk-tgt` is ordered after `lan-hi` is online; the RDMA listener binds the `hi` address
|
||||||
|
(`192.168.68.80:4420`). The VM itself is pinned to NUMA node 1 on `palace` and gets SR-IOV VF 0.
|
||||||
|
- `netdata` (port 19999 allowed in the firewall) and `fstrim`.
|
||||||
|
- Deploy address `192.168.68.80` (`my.deploy.node.hostname`).
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: cellar -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| cellar | hi | `192.168.68.80/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::4:1/64` | h.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `ublk_*` calls in `my.spdk.debugCommands` are only a debugging aid — they create a local
|
||||||
|
ublk device so the RAID can be mounted and inspected on `cellar` itself. Client exports are the
|
||||||
|
`nvmf` subsystems above.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/home/palace/vms/cellar/default.nix`](../../../nixos/boxes/home/palace/vms/cellar/default.nix) —
|
||||||
|
box config (assignment, networking, netdata).
|
||||||
|
- [`nixos/boxes/home/palace/vms/cellar/spdk.nix`](../../../nixos/boxes/home/palace/vms/cellar/spdk.nix) —
|
||||||
|
SPDK target: RAID-0, NVMe-oF/RDMA subsystems.
|
||||||
|
- [`nixos/boxes/home/palace/vms/default.nix`](../../../nixos/boxes/home/palace/vms/default.nix) —
|
||||||
|
the VM definition on `palace` (VF 0, NVMe passthrough, NUMA pinning).
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# palace
|
||||||
|
|
||||||
|
The physical VM host for the home site. Runs the `river`, `cellar` and `sfh` VMs and feeds them
|
||||||
|
SR-IOV VFs, PCI NVMe drives and LVM disks.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/home/palace/default.nix`](../../../nixos/boxes/home/palace/default.nix)
|
||||||
|
(VM definitions in [`palace/vms/default.nix`](../../../nixos/boxes/home/palace/vms/default.nix))
|
||||||
|
- **Host:** physical
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Home hypervisor: VMs are declared in `my.vms.instances`
|
||||||
|
([`palace/vms/default.nix`](../../../nixos/boxes/home/palace/vms/default.nix)); disks are LVs in
|
||||||
|
the `main` thin pool (`services.lvm.boot.thin.enable`).
|
||||||
|
- AMD box (`kvm-amd`, `amd_iommu=on`, microcode updates); the kernel is built with
|
||||||
|
`ACPI_APEI_PCIEAER`/`PCIEAER` for the PCIe passthrough work below.
|
||||||
|
- Deploy address `192.168.68.22` (`my.deploy.node.hostname`).
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: palace -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| palace-core | core | `192.168.64.20/24` | — | h.nul.ie | |
|
||||||
|
| palace | hi | `192.168.68.22/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::2:1/64` | h.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
- 100G `et100g` (mlx5, MTU 9000) uplinks to the `dave` switch and carries `lan-hi` (VLAN 100,
|
||||||
|
the `hi` assignment). A udev rule creates **4 SR-IOV VFs** on the PF, distributed in
|
||||||
|
`50-et100g`'s `[SR-IOV]` sections: VF 0 → `cellar` (VLAN hi), VF 1 → `river` (no VLAN — river
|
||||||
|
tags all of its own VLANs, including both WAN VLANs, on this trunk), VF 2 → `sfh` (VLAN hi),
|
||||||
|
VF 3 → `sfh`'s container MACVLAN parent (VLAN hi).
|
||||||
|
- `lan-core` is a bridge with the `core` assignment (`192.168.64.20`, no gateway); the 1G
|
||||||
|
`lan-core-phy` and the `lan-lo-phy` VLAN ride on it. `lan-lo` is a second, L3-less bridge used
|
||||||
|
for VM netboot and `lo` clients.
|
||||||
|
- The 1G `et1g0` (igb) is exported to `river` as a passthru-mode macvtap (`vm-et1g0`) — river sees
|
||||||
|
it as `wan-old`.
|
||||||
|
|
||||||
|
## VMs
|
||||||
|
|
||||||
|
| VM | vCPUs | RAM | Passthrough | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `cellar` | 8c × 2t | 16 GiB | VF 0 (`44:00.1`); NVMe `41:00.0`–`43:00.0` | pinned to NUMA node 1; split IRQ chip + vIOMMU |
|
||||||
|
| `river` | 3c × 2t | 4 GiB | VF 1 (`44:00.2`); macvtap `vm-et1g0` | only an ESP local disk (plus an installer ISO) — root is NVMe-oF from `cellar` |
|
||||||
|
| `sfh` | 8c × 2t | 32 GiB | VF 2 (`44:00.3`), VF 3 (`44:00.4`); two USB host ports | no boot disk — netboots; gets the `hdds/frigate` LV |
|
||||||
|
|
||||||
|
Boot ordering is enforced with systemd dependencies: `vm@river` waits for `cellar`'s SSH port (and
|
||||||
|
for the `vm-et1g0` device), and `vm@sfh` waits for `river` — storage first, then the router, then
|
||||||
|
everything that boots off both.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/home/palace/default.nix`](../../../nixos/boxes/home/palace/default.nix) — host
|
||||||
|
hardware, networkd (links/bridges/SR-IOV), LVM.
|
||||||
|
- [`nixos/boxes/home/palace/vms/default.nix`](../../../nixos/boxes/home/palace/vms/default.nix) —
|
||||||
|
VM instances and boot ordering.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# river
|
||||||
|
|
||||||
|
The primary home router. A VM on `palace`, built from `routing-common` at index 0; it runs PPPoE
|
||||||
|
to Digiweb directly on the ISP's VLAN 10 and normally holds the master side of the VRRP pair with
|
||||||
|
[`stream`](stream.md).
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/home/palace/vms/river.nix`](../../../nixos/boxes/home/palace/vms/river.nix)
|
||||||
|
(shared router config: [`routing-common`](../../../nixos/boxes/home/routing-common), index 0)
|
||||||
|
- **Host:** VM on `palace`
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Primary of the router pair: `routing-common` index 0 → keepalived starts `MASTER` (priority
|
||||||
|
255), kea serves the lower-half DHCP pools, the zone's SOA/`ns1` point here.
|
||||||
|
- Everything from `routing-common`: keepalived/VRRP ([`keepalived.nix`](../../../nixos/boxes/home/routing-common/keepalived.nix)),
|
||||||
|
PowerDNS recursor + authoritative with a blocklist ([`dns.nix`](../../../nixos/boxes/home/routing-common/dns.nix)),
|
||||||
|
kea DHCP + DDNS ([`kea.nix`](../../../nixos/boxes/home/routing-common/kea.nix)), radvd
|
||||||
|
([`radvd.nix`](../../../nixos/boxes/home/routing-common/radvd.nix)), NAT/firewall, the
|
||||||
|
`as211024` L2 mesh link back to colony, `iperf3`, `nginx`. See
|
||||||
|
[networking.md](../../networking.md) for the logical view and [README.md](README.md#router-vips)
|
||||||
|
for the floating VIPs.
|
||||||
|
- **Netboot server** for `sfh` and `castle` (`my.netboot.server`): iPXE/TFTP at
|
||||||
|
`boot.h.nul.ie` (a CNAME to `river-hi`), served from the `lo` address `192.168.72.1` and
|
||||||
|
restricted to the hi/lo prefixes.
|
||||||
|
- **NVMe-oF client of `cellar`**: the VM's only local disk is an ESP (an installer ISO is also
|
||||||
|
still attached); `/nix` and `/persist` are LVs on the `nqn.2016-06.io.spdk:river` namespace
|
||||||
|
exported by `cellar` (`192.168.68.80`, RDMA).
|
||||||
|
The initrd brings up `lan-hi` with the RoCE modules (`roceBootModules`), and
|
||||||
|
`KeepConfiguration=static` on `lan-hi` stops networkd from dropping the NVMe-oF address on
|
||||||
|
reconfigure.
|
||||||
|
- **SR-IOV VF passthrough**: the 100G `lan` NIC is VF 1 of palace's `et100g` (MAC
|
||||||
|
`52:54:00:8a:8a:f2`, MTU 9000). All router VLANs — hi/lo/untrusted plus both WAN VLANs — are
|
||||||
|
tagged on top of it (`55-lan`).
|
||||||
|
- Also carries a macvtap passthrough of palace's 1G `et1g0`, renamed `wan-old` — the pre-100G WAN
|
||||||
|
path, kept around with no L3 config today.
|
||||||
|
- Deploy address `192.168.68.1` (`my.deploy.node.hostname`).
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: river -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| river | as211024 | `10.100.50.2/24` | `2a0e:97c0:4df:0:1::1/64 gw 2a0e:97c0:4df:0:2::1` | — | |
|
||||||
|
| river-core | core | `192.168.64.1/24` | — | h.nul.ie | |
|
||||||
|
| river-hi | hi | `192.168.68.1/22` | `2a0e:97c0:4d0:1::1/64` | h.nul.ie | |
|
||||||
|
| river-lo | lo | `192.168.72.1/21` | `2a0e:97c0:4d0:2::1/64` | h.nul.ie | |
|
||||||
|
| river-ut | untrusted | `192.168.80.1/24` | `2a0e:97c0:4d0:3::1/64` | h.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## WAN (Digiweb PPPoE)
|
||||||
|
|
||||||
|
- `services.pppd` peer `digiweb` attaches PPPoE directly to `wan-pon-isp`
|
||||||
|
(`plugin pppoe.so wan-pon-isp`) — the **raw ISP VLAN 10** (`vlans.pon-isp`), trunked
|
||||||
|
untranslated from the ONT through `brian` and `dave` (the fabric side is in
|
||||||
|
[switches.md](switches.md)). The netdev is up with no L3; `MTUBytes=1508` (baby jumbo) absorbs
|
||||||
|
PPPoE's 8-byte overhead so the `wan` ppp interface gets a clean `mtu`/`mru 1500`.
|
||||||
|
- The Digiweb static IP (`84.203.124.128`, `elemAt routersPubV4 0`) is requested as the local
|
||||||
|
address in IPCP. Auth is the across-all-customers shared `digiweb@nga.digiweb.ie` / `digiweb`
|
||||||
|
(deliberately not a secret); `persist`, `maxfail 0`, 1s LCP echoes. `usepeerdns` is absent on
|
||||||
|
purpose — Digiweb's resolvers are ignored in favour of the local recursor.
|
||||||
|
- **ONT management**: `wan-pon-ont` (VLAN 140, PVID'd at the `brian` edge) holds
|
||||||
|
`192.168.100.100/24` so the ONT web UI at `192.168.100.1` is reachable — the `.100` mirrors
|
||||||
|
stream's modem-management convention.
|
||||||
|
- **`wan-online.target`** is the shared "public WAN is up" gate declared by `routing-common`;
|
||||||
|
here it is driven by the pppd hooks (`DefaultDependencies=false`, so nothing pulls it in
|
||||||
|
early): `ip-up` installs `default dev wan scope link metric 100` and starts the target,
|
||||||
|
`ip-down` stops it and deletes the route. Consumers (`ipsec`, `ipv6-clear-default-route`)
|
||||||
|
attach with `wantedBy` + `partOf`, so they re-load on every WAN flap.
|
||||||
|
- The `wan-ifb` ingress-shaping pieces from `routing-common` are inert on this box: the CAKE
|
||||||
|
config itself is stream's, and `networkd-dispatcher` (which installs the `tc` mirror) is
|
||||||
|
`mkForce false` here pending scheduling testing.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/home/palace/vms/river.nix`](../../../nixos/boxes/home/palace/vms/river.nix) — box
|
||||||
|
config: pppd, WAN VLANs, `wan-online.target` hooks, netboot server, NVMe-oF boot.
|
||||||
|
- [`nixos/boxes/home/routing-common/default.nix`](../../../nixos/boxes/home/routing-common/default.nix) —
|
||||||
|
shared router definition (assignments, firewall/NAT, `as211024`).
|
||||||
|
- [`nixos/boxes/home/palace/vms/default.nix`](../../../nixos/boxes/home/palace/vms/default.nix) —
|
||||||
|
the VM definition on `palace` (VF 1, macvtap, ESP disk).
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# sfh
|
||||||
|
|
||||||
|
"Services for home" — the NixOS container host for the home site. A VM on `palace` that netboots
|
||||||
|
from `river` and runs its root off NVMe-oF from `cellar`.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/home/palace/vms/sfh/`](../../../nixos/boxes/home/palace/vms/sfh)
|
||||||
|
(`default.nix`, `containers/`)
|
||||||
|
- **Host:** VM on `palace`
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Runs the home NixOS containers via `my.containers.instances` (systemd-nspawn); each container is
|
||||||
|
its own `nixos.systems.*` entry rendered through `my.asContainer`.
|
||||||
|
- **Netboot client** (`my.netboot.client.enable`): the VM has no boot disk — its `netboot` NIC
|
||||||
|
(MAC `52:54:00:a5:7e:93`, on palace's `lan-lo` bridge, `bootindex=1`) is matched by a kea
|
||||||
|
client-class on [`river`](river.md) and iPXE-boots from `boot.h.nul.ie`.
|
||||||
|
- **Root on NVMe-oF**: `my.nvme.boot` connects to `nqn.2016-06.io.spdk:sfh` at `192.168.68.80`
|
||||||
|
([`cellar`](cellar.md), RDMA) from the initrd (`lan-hi` up + `roceBootModules`); `/nix` and
|
||||||
|
`/persist` are LVs on that volume. `KeepConfiguration=static` on `lan-hi` protects the
|
||||||
|
NVMe-oF address from networkd reconfigures.
|
||||||
|
- **Frigate footage disk**: palace passes the `hdds/frigate` LVM LV through as a virtio disk;
|
||||||
|
sfh mounts it at `/mnt/frigate` (by label) and bind-mounts it into the `hass` container at
|
||||||
|
`/var/lib/frigate`.
|
||||||
|
- USB: two host ports are passed to the VM (qemu flags) for the Zigbee coordinator and webcam used
|
||||||
|
by `hass`; the nspawn unit gets `DeviceAllow` for `char-ttyUSB` and `char-video4linux`.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: sfh -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| sfh | hi | `192.168.68.81/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::4:2/64` | h.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
Four NICs, all MTU 9000 where jumbo-capable:
|
||||||
|
|
||||||
|
- `lan-hi` — SR-IOV VF 2, the box's own `hi` assignment (`192.168.68.81`).
|
||||||
|
- `lan-hi-ctrs` — SR-IOV VF 3, no L3: the MACVLAN parent for the containers' `hi` legs
|
||||||
|
(`host0` inside each container).
|
||||||
|
- `lan-core-ctrs` / `lan-lo-ctrs` — virtio NICs (bridged to palace's `lan-core` / `lan-lo`), no
|
||||||
|
L3: MACVLAN parents for containers that need a `core` or `lo` leg.
|
||||||
|
|
||||||
|
The per-container MACVLAN wiring lives in `systemd.nspawn.*.networkConfig` in
|
||||||
|
[`sfh/default.nix`](../../../nixos/boxes/home/palace/vms/sfh/default.nix).
|
||||||
|
|
||||||
|
## Containers
|
||||||
|
|
||||||
|
| Container | Role | Page |
|
||||||
|
|---|---|---|
|
||||||
|
| `hass` | Home Assistant + Frigate + MQTT | [sfh/containers/hass.md](sfh/containers/hass.md) |
|
||||||
|
| `unifi` | UniFi controller | [sfh/containers/unifi.md](sfh/containers/unifi.md) |
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/home/palace/vms/sfh/default.nix`](../../../nixos/boxes/home/palace/vms/sfh/default.nix) —
|
||||||
|
box config: netboot/NVMe-oF boot, container instances, MACVLAN plumbing, Frigate disk.
|
||||||
|
- [`nixos/boxes/home/palace/vms/sfh/containers/`](../../../nixos/boxes/home/palace/vms/sfh/containers) —
|
||||||
|
the container system definitions.
|
||||||
|
- [`nixos/boxes/home/palace/vms/default.nix`](../../../nixos/boxes/home/palace/vms/default.nix) —
|
||||||
|
the VM definition on `palace` (VFs, USB passthrough, netboot NIC, `hdds/frigate` disk).
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# hass
|
||||||
|
|
||||||
|
Home automation container: Home Assistant plus its supporting services (MQTT, camera restreaming,
|
||||||
|
Frigate NVR), running on [`sfh`](../../sfh.md).
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/home/palace/vms/sfh/containers/hass.nix`](../../../../../nixos/boxes/home/palace/vms/sfh/containers/hass.nix)
|
||||||
|
- **Host:** NixOS container on `sfh`
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **Home Assistant** (`services.home-assistant`) — declarative config (`configWritable = false`),
|
||||||
|
components `esphome`, `zha`, `denonavr`, `webostv`, `androidtv_remote`, `heos`, `mqtt`, `wled`,
|
||||||
|
`met`, `google_translate`; custom components `alarmo`, `frigate`, `west_wood_club`; a custom
|
||||||
|
Irish Rail sensor (Glenageary ↔ Dublin Connolly). A `hass-cli` wrapper is on the box, wired to
|
||||||
|
the local server with a token from `my.secrets`.
|
||||||
|
- **mosquitto** — MQTT broker (anonymous local listener; port 1883 allowed, alongside HTTP).
|
||||||
|
- **go2rtc** — restreams the Reolink living-room camera (RTSP from `reolink-living-room`, on the
|
||||||
|
`lo` leg) and the office USB webcam (`/dev/video0` via ffmpeg).
|
||||||
|
- **Frigate** (`services.frigate`, `frigate.h.nul.ie` — the `frigate` alt name on the `hi`
|
||||||
|
assignment) — records both restreamed cameras with 1-day retention; detection is disabled.
|
||||||
|
- External access is via `https://hass.nul.ie` through the `middleman` reverse proxy
|
||||||
|
(`trusted_proxies`); internally it's `hass-ctr.h.nul.ie`.
|
||||||
|
- Not a deploy-rs target (`my.deploy.enable = false`) — it's rendered via `my.asContainer` and
|
||||||
|
started by `sfh`'s `my.containers.instances`.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: hass -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| hass-ctr (frigate) | hi | `192.168.68.103/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::5:3/64` | h.nul.ie | |
|
||||||
|
| hass-ctr-lo | lo | `192.168.72.103/21` | `2a0e:97c0:4d0:2::5:3/64` | h.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Frigate footage lives on a **separate HDD LV**: `palace` passes the `hdds/frigate` LVM LV to the
|
||||||
|
`sfh` VM, sfh mounts it at `/mnt/frigate`, and the container bind-mounts it at `/var/lib/frigate`
|
||||||
|
(read-write). This keeps recording churn off the NVMe-oF root.
|
||||||
|
|
||||||
|
## Devices
|
||||||
|
|
||||||
|
Passed through from `sfh` (USB host ports on `palace`):
|
||||||
|
|
||||||
|
- Nabu Casa Connect ZBT-1 Zigbee coordinator → `/dev/ttyUSB0` (used by `zha`).
|
||||||
|
- USB webcam → `/dev/video0` (go2rtc's `webcam_office` stream).
|
||||||
|
- `/dev/bus/usb/001/002` (raw USB device node).
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
MACVLAN legs created from `sfh`'s container NICs: `host0` on `lan-hi-ctrs` (the `hi` assignment,
|
||||||
|
alt name `frigate`, default gateway via the VIP) and `lan-lo` on `lan-lo-ctrs` (the `lo`
|
||||||
|
assignment, no gateway) — the `lo` leg reaches the IoT devices (the Reolink camera lives there).
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/home/palace/vms/sfh/containers/hass.nix`](../../../../../nixos/boxes/home/palace/vms/sfh/containers/hass.nix) —
|
||||||
|
container system: Home Assistant, Frigate, mosquitto, go2rtc.
|
||||||
|
- [`nixos/boxes/home/palace/vms/sfh/default.nix`](../../../../../nixos/boxes/home/palace/vms/sfh/default.nix) —
|
||||||
|
the `sfh` side: bind mounts, MACVLAN wiring, `DeviceAllow`.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# unifi
|
||||||
|
|
||||||
|
The UniFi network controller, running as a container on [`sfh`](../../sfh.md). It manages the
|
||||||
|
home UniFi switch `brian` (see [switches.md](../../switches.md)).
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/home/palace/vms/sfh/containers/unifi.nix`](../../../../../nixos/boxes/home/palace/vms/sfh/containers/unifi.nix)
|
||||||
|
- **Host:** NixOS container on `sfh`
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Currently enabled.** The container spent a while disabled — its import was commented out of
|
||||||
|
[`containers/default.nix`](../../../../../nixos/boxes/home/palace/vms/sfh/containers/default.nix)
|
||||||
|
while there was no UniFi gear to manage — and was re-enabled when the UniFi switch `brian` was
|
||||||
|
added, gaining a `core` leg (`unifi-ctr-core`) at the same time so it can reach the switch on its
|
||||||
|
management network. It is imported, listed in `sfh`'s `my.containers.instances`, and
|
||||||
|
`services.unifi.enable = true`.
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- **UniFi controller** (`services.unifi`, `pkgs.unifi` on `mongodb-7_0`, firewall open; TCP 8443
|
||||||
|
allowed).
|
||||||
|
- Not a deploy-rs target (`my.deploy.enable = false`) — it's rendered via `my.asContainer` and
|
||||||
|
started by `sfh`'s `my.containers.instances`.
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: unifi -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| unifi-ctr-core | core | `192.168.64.21/24` | — | h.nul.ie | |
|
||||||
|
| unifi-ctr | hi | `192.168.68.100/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::5:1/64` | h.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
Two MACVLAN legs from `sfh`'s container NICs: `host0` on `lan-hi-ctrs` (the `hi` assignment —
|
||||||
|
`unifi-ctr`, `192.168.68.100`, default gateway via the VIP) and `lan-core` on `lan-core-ctrs`
|
||||||
|
(the `core` assignment — `unifi-ctr-core`, `192.168.64.21/24`, no gateway). The `core` leg is
|
||||||
|
how the controller talks to the switches: `brian` lives at `192.168.64.13` on `core`.
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/home/palace/vms/sfh/containers/unifi.nix`](../../../../../nixos/boxes/home/palace/vms/sfh/containers/unifi.nix) —
|
||||||
|
container system: UniFi service, assignments.
|
||||||
|
- [`nixos/boxes/home/palace/vms/sfh/default.nix`](../../../../../nixos/boxes/home/palace/vms/sfh/default.nix) —
|
||||||
|
the `sfh` side: container instance, MACVLAN wiring.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# stream
|
||||||
|
|
||||||
|
The secondary home router. A physical Intel box, built from `routing-common` at index 1; its WAN
|
||||||
|
is a DHCP lease from the Virgin Media cable modem, and it is dual-homed to both switches.
|
||||||
|
|
||||||
|
- **Source:** [`nixos/boxes/home/stream.nix`](../../../nixos/boxes/home/stream.nix) (shared router
|
||||||
|
config: [`routing-common`](../../../nixos/boxes/home/routing-common), index 1)
|
||||||
|
- **Host:** physical
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
- Backup of the router pair: `routing-common` index 1 → keepalived starts `BACKUP` (priority
|
||||||
|
254), kea serves the upper-half DHCP pools, the zone's `ns2` points here. Takes over all VIPs
|
||||||
|
when [`river`](river.md) is down — see [README.md](README.md#router-vips).
|
||||||
|
- Runs the same `routing-common` services as river: keepalived/VRRP, PowerDNS recursor +
|
||||||
|
authoritative, kea DHCP + DDNS, radvd, NAT/firewall, the `as211024` L2 mesh, `iperf3`, `nginx`.
|
||||||
|
- Intel box (`kvm-intel`, `intel_iommu=on`, microcode updates).
|
||||||
|
- `octoprint` and `mjpg-streamer` (3D-printer services) are defined but **disabled**
|
||||||
|
(`enable = false`).
|
||||||
|
- `my.deploy.node.hostname` is currently commented out (it was `192.168.68.2`).
|
||||||
|
|
||||||
|
## Network assignments
|
||||||
|
|
||||||
|
<!-- assignments: stream -->
|
||||||
|
<!-- assignments-start -->
|
||||||
|
| Name | Assignment | IPv4 | IPv6 | Domain | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| stream | as211024 | `10.100.50.3/24` | `2a0e:97c0:4df:0:1::2/64 gw 2a0e:97c0:4df:0:2::1` | — | |
|
||||||
|
| stream-core | core | `192.168.64.2/24` | — | h.nul.ie | |
|
||||||
|
| stream-hi | hi | `192.168.68.2/22` | `2a0e:97c0:4d0:1::2/64` | h.nul.ie | |
|
||||||
|
| stream-lo | lo | `192.168.72.2/21` | `2a0e:97c0:4d0:2::2/64` | h.nul.ie | |
|
||||||
|
| stream-ut | untrusted | `192.168.80.2/24` | `2a0e:97c0:4d0:3::2/64` | h.nul.ie | |
|
||||||
|
<!-- assignments-end -->
|
||||||
|
|
||||||
|
## WAN (Virgin Media DHCP)
|
||||||
|
|
||||||
|
- `wan` is a renamed igc NIC (`00:f0:cb:ee:ca:dd`) towards the cable modem. The modem segment is
|
||||||
|
switch **VLAN 130** — tagging is handled on the switch (`jim`), so the box interface itself is
|
||||||
|
untagged. The fabric side is in [switches.md](switches.md).
|
||||||
|
- `DHCP=ipv4` pulls the public lease; `dhcpV4Config.UseDNS=false` and the interface DNS points at
|
||||||
|
the local recursor. `IPv6AcceptRA=false` — this is an IPv4-only WAN (public IPv6 arrives over
|
||||||
|
the tunnel, not this link).
|
||||||
|
- A **static modem-management address** (`192.168.0.100/24`, host `.100` of `prefixes.modem.v4`)
|
||||||
|
sits on `wan` alongside the DHCP lease so the modem's web UI stays reachable; it has no
|
||||||
|
gateway.
|
||||||
|
- **`wan-online.target`** wiring: `wan-wait-online.service` (a oneshot) polls until the DHCP
|
||||||
|
default route exists, and the target `requires`/`after`s it (`wantedBy multi-user.target`).
|
||||||
|
The route — not networkd's wait-online — is the gate because the permanent static modem address
|
||||||
|
would otherwise report "online" before the public lease arrives, letting `ipsec` start
|
||||||
|
unoriented (`left=` is the public IP) and never connect.
|
||||||
|
- **CAKE QoS**: egress is shaped at the `wan` root qdisc (`Bandwidth=48M`); ingress is redirected
|
||||||
|
by `tc` (`mirred`, installed by the `networkd-dispatcher` rule in `routing-common`) into the
|
||||||
|
`wan-ifb` IFB at `Bandwidth=490M` with the DOCSIS overhead preset.
|
||||||
|
|
||||||
|
### Modem specifics (de-shared from `routing-common`)
|
||||||
|
|
||||||
|
The modem's management subnet shares the `wan` interface, which `routing-common` itself knows
|
||||||
|
nothing about — it declares two per-box options
|
||||||
|
([`routing-common/default.nix`](../../../nixos/boxes/home/routing-common/default.nix)) that this
|
||||||
|
box sets:
|
||||||
|
|
||||||
|
- `my.homeRouter.dns.wanSkipBroadcasts = [ 192.168.0.255 ]` — skip the modem subnet when
|
||||||
|
auto-selecting the router's own `wan` A record for the zone's LUA record.
|
||||||
|
- `my.homeRouter.firewall.untrustedRejectV4 = [ 192.168.0.0/24 ]` — reject untrusted clients from
|
||||||
|
reaching the modem subnet (needed only because it shares `wan`; WAN egress is otherwise
|
||||||
|
accepted).
|
||||||
|
|
||||||
|
## Switching (STP)
|
||||||
|
|
||||||
|
`stream` is dual-homed to both switches: `lan-jim` (igc) and `lan-dave` (mlx4_en), both MTU 9000,
|
||||||
|
are enslaved to the `lan` bridge with `STP=true`. [`routing-common/mstpd.nix`](../../../nixos/boxes/home/routing-common/mstpd.nix)
|
||||||
|
runs a patched `mstpd` and forces RSTP on `lan` once it's routable, so exactly one uplink carries
|
||||||
|
traffic at a time. (The remaining NICs are renamed `et2`/`et5` and left unconfigured.)
|
||||||
|
|
||||||
|
## Notable config files
|
||||||
|
|
||||||
|
- [`nixos/boxes/home/stream.nix`](../../../nixos/boxes/home/stream.nix) — box config: DHCP WAN,
|
||||||
|
modem management, CAKE, `wan-online.target` gate, STP bridge.
|
||||||
|
- [`nixos/boxes/home/routing-common/default.nix`](../../../nixos/boxes/home/routing-common/default.nix) —
|
||||||
|
shared router definition (index 1).
|
||||||
|
- [`nixos/boxes/home/routing-common/mstpd.nix`](../../../nixos/boxes/home/routing-common/mstpd.nix) —
|
||||||
|
RSTP on the `lan` bridge.
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
# Home switches
|
||||||
|
|
||||||
|
Reference for the two MikroTik switches on the home network — **jim** and **dave** — plus the
|
||||||
|
Ubiquiti switch **brian**, and how the home boxes and the Digiweb WAN hang off them. These switches
|
||||||
|
are **not** managed by this flake; they are configured by hand (RouterOS on jim/dave, UniFi on
|
||||||
|
brian). It covers the physical topology, the VLAN map, and how the Digiweb WAN reaches river.
|
||||||
|
|
||||||
|
In short: the Digiweb ISP VLAN (10) is trunked straight through to river (which runs PPPoE on it),
|
||||||
|
and the ONT's untagged management is PVID'd onto VLAN 140 at brian, its edge switch. VLAN 10 is
|
||||||
|
carried untranslated because a single ONT makes it unique on the fabric — see
|
||||||
|
[the WAN path](#the-digiweb-wan-path-trunked-vlan-10--pvid-140) and
|
||||||
|
[why not translation](#why-not-translation-for-one-ont). The router side lives in
|
||||||
|
[river.md](river.md); the logical network map in [networking.md](../../networking.md).
|
||||||
|
|
||||||
|
## The switches
|
||||||
|
|
||||||
|
| | jim | dave | brian |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Identity | `jim-sw` | `dave-sw` | (UniFi) |
|
||||||
|
| Model | CRS326-24G-2S+ | CRS504-4XQ | Ubiquiti 10GBASE-T |
|
||||||
|
| Switch chip | Marvell 98DX3236 | Marvell 98DX4310 (+ Atheros 8227 for the 1G mgmt port) | — |
|
||||||
|
| OS | RouterOS 7.18 | RouterOS 7.18 | UniFi |
|
||||||
|
| Ports | 24×1G + 2×SFP+ | 4×QSFP28 (100G, breakout-capable) + 1G mgmt | 10GBASE-T |
|
||||||
|
| Bridge | `main`, `vlan-filtering=yes` | `main`, `vlan-filtering=yes` | UniFi VLAN profiles |
|
||||||
|
|
||||||
|
jim and dave run a single hardware-offloaded bridge (`main`) with VLAN filtering. Access to the
|
||||||
|
MikroTiks is SSH as `admin` / `admin` by short hostname (see [Accessing the switches](#accessing-the-switches)).
|
||||||
|
Only jim and dave can do hardware VLAN translation (`/interface ethernet switch rule` on the Marvell
|
||||||
|
chips); brian cannot rewrite tags, only trunk/PVID them.
|
||||||
|
|
||||||
|
## Physical topology
|
||||||
|
|
||||||
|
The ONT terminates on brian; jim's `wan-pon-in` (`sfp-sfpplus2`) is a spare SFP+ port.
|
||||||
|
|
||||||
|
```
|
||||||
|
Virgin Media cable modem
|
||||||
|
│
|
||||||
|
│ (VLAN 130)
|
||||||
|
│ wan1/wan2/wan-in
|
||||||
|
┌──────────────────────────────────────────────────┴──┐
|
||||||
|
│ jim CRS326-24G-2S+ (Marvell 98DX3236) │
|
||||||
|
│ 1G edge ports: castle, fort, pronter, laptop-dock, │
|
||||||
|
│ palace-kvm, ups, ether15-20, wan1/wan2/wan-in │
|
||||||
|
│ wan-pon-in (= sfp-sfpplus2) ← spare SFP+ │
|
||||||
|
└───────┬───────────────────────────────────┬─────────┘
|
||||||
|
dave-uplink ┤ (= sfp-sfpplus1) │ stream, palace (1G secondaries)
|
||||||
|
10G trunk│ └── stream (box, dual-homed)
|
||||||
|
┌─────────┴───────────────────────────────────────────────┐
|
||||||
|
│ dave CRS504-4XQ (Marvell 98DX4310) │
|
||||||
|
│ jim-downlink = qsfp28-3-1 │
|
||||||
|
└──┬───────────┬──────────┬───────────────────┬────────────┘
|
||||||
|
palace(100G) │ castle │ stream │ brian-downlink
|
||||||
|
= qsfp28-1-1 │ │ │ (802.3ad LAG: brian1+brian2)
|
||||||
|
┌────────────────┴─┐ … │
|
||||||
|
│ palace host │ Digiweb (PPPoE) via ONT
|
||||||
|
│ └ river (VM) │ │
|
||||||
|
└──────────────────┘ ┌─────┴─────┐
|
||||||
|
↑ river's WAN + LAN ride the 100G link │ ONT │ untagged mgmt 192.168.100.1
|
||||||
|
└─────┬─────┘ + tagged VLAN 10 (ISP)
|
||||||
|
│
|
||||||
|
┌────────┴────────┐
|
||||||
|
│ brian (UniFi) │ PVID 140 on the ONT port,
|
||||||
|
│ 10GBASE-T │ tagged VLAN 10 allowed through
|
||||||
|
└────────┬────────┘
|
||||||
|
└── brian-downlink LAG up to dave
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- **river** runs as a VM on the **palace** host; its uplink is dave's 100G `palace` port. jim also
|
||||||
|
has 1G `palace`/`stream` ports, but those are secondary links and do **not** carry the WAN.
|
||||||
|
- **stream** (the second router box) is dual-homed to both jim and dave (STP picks the active path).
|
||||||
|
- **brian** is a Ubiquiti 10GBASE-T switch, downlinked from dave over an **802.3ad LAG**
|
||||||
|
(`brian-downlink` = `brian1` + `brian2`, layer-2 hash). It hosts the ONT.
|
||||||
|
|
||||||
|
## VLANs
|
||||||
|
|
||||||
|
| VLAN | Name | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| — (native) | core | Switch management, `192.168.64.0/24` (jim `.10`, dave `.11`, brian `.13`) |
|
||||||
|
| 100 | hi | High-performance / jumbo network (MTU 9000) |
|
||||||
|
| 110 | lo | Standard LAN |
|
||||||
|
| 120 | untrusted | Guest / untrusted network |
|
||||||
|
| 130 | wan | **stream's WAN** — Virgin Media cable modem (untagged on jim's `wan1`/`wan2`/`wan-in`) |
|
||||||
|
| 140 | wan-pon-ont | ONT management, `192.168.100.0/24` (PVID'd at the ONT edge) |
|
||||||
|
| 10 | pon-isp | Digiweb ISP transport — **trunked straight through** to river, PPPoE runs on it |
|
||||||
|
| 141 | wan-pon-isp | **Reserved** — the translated ISP VLAN for the future multi-ONT design |
|
||||||
|
|
||||||
|
Switch L3 presence (`/interface vlan` on `main`) exists **only** for VLANs the switch is managed
|
||||||
|
from — `hi` (100) and `lo` (110), plus native core. WAN and guest VLANs deliberately have no switch
|
||||||
|
L3 interface.
|
||||||
|
|
||||||
|
## The Digiweb WAN path (trunked VLAN 10 + PVID 140)
|
||||||
|
|
||||||
|
The ONT presents two things on one wire:
|
||||||
|
- **untagged** management traffic (`192.168.100.x`), and
|
||||||
|
- **tagged VLAN 10** carrying the Digiweb ISP session (the BRAS requires VLAN 10).
|
||||||
|
|
||||||
|
With a **single ONT** there's no reason to translate anything — VLAN 10 is unique on the fabric, so
|
||||||
|
we just carry it end to end and let river run PPPoE directly on it:
|
||||||
|
|
||||||
|
1. **Untagged mgmt → VLAN 140, at the ONT's edge switch (brian).** brian sets the ONT port's PVID to
|
||||||
|
140 so the untagged management traffic becomes VLAN 140, and allows tagged VLAN 10 through the
|
||||||
|
same port. river takes `192.168.100.100/24` on VLAN 140 (matching stream's modem-mgmt `.100`) to
|
||||||
|
reach the ONT web UI at `192.168.100.1`. Doing the PVID at the ONT-facing edge keeps it clean —
|
||||||
|
the untagged frames never share a domain with anything else.
|
||||||
|
|
||||||
|
2. **VLAN 10 (ISP) trunked straight through, untranslated.** brian → dave → palace carry tagged
|
||||||
|
VLAN 10 by ordinary bridge-VLAN membership. No `/interface ethernet switch rule`, no pinning, no
|
||||||
|
asymmetric-learning issues — it's just a normal tagged VLAN. river attaches PPPoE to VLAN 10
|
||||||
|
directly (`wan-pon-isp` netdev = VLAN `pon-isp` = 10; baby-jumbo MTU 1508 so PPP nets a clean
|
||||||
|
1500).
|
||||||
|
|
||||||
|
Net result: **river runs PPPoE single-tagged on VLAN 10 and holds a VLAN 140 address to reach the
|
||||||
|
ONT.** See [`nixos/boxes/home/palace/vms/river.nix`](../../../nixos/boxes/home/palace/vms/river.nix)
|
||||||
|
for the river side.
|
||||||
|
|
||||||
|
```
|
||||||
|
ONT ──(untagged + VLAN10)── brian ──(VLAN140 + VLAN10)── dave ──(VLAN140 + VLAN10)── river
|
||||||
|
ONT port │ PVID140 + tagged 10 │ plain bridging
|
||||||
|
└─ brian-downlink LAG ── dave ┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why not translation (for one ONT)?
|
||||||
|
|
||||||
|
Translation would swap VLAN 10 → 141 with two pinned hardware ACL rules to keep VLAN 10 off the rest
|
||||||
|
of the fabric. That buys nothing with a single ONT — VLAN 10 is already unique, so trunking it is
|
||||||
|
simpler and rule-free. Translation only earns its keep when **two** ONTs both deliver VLAN 10 and
|
||||||
|
would collide (below).
|
||||||
|
|
||||||
|
## Switch configuration
|
||||||
|
|
||||||
|
How each switch is set up for the Digiweb WAN path. **Confirm any change on the box before applying**
|
||||||
|
(see [Accessing the switches](#accessing-the-switches)).
|
||||||
|
|
||||||
|
**brian (UniFi)** — hosts the ONT:
|
||||||
|
- The ONT port has **native/untagged network = VLAN 140** (PVID) and is a **tagged member of VLAN 10**,
|
||||||
|
so the ONT's untagged management lands on 140 and its tagged ISP frames pass through.
|
||||||
|
- The `brian-downlink` LAG up to dave trunks **tagged 140 + tagged 10** (alongside the LAN VLANs).
|
||||||
|
|
||||||
|
**dave (RouterOS)** — trunks both WAN-pon VLANs to `brian-downlink` and `palace`. The ISP VLAN 10 row:
|
||||||
|
```
|
||||||
|
/interface bridge vlan add bridge=main vlan-ids=10 tagged=brian-downlink,palace
|
||||||
|
```
|
||||||
|
VLAN 140 also spans `brian-downlink,palace` (it carries a few other members too). No switch rules —
|
||||||
|
this is plain tagged bridging.
|
||||||
|
|
||||||
|
**jim (RouterOS)** — carries **none** of the Digiweb WAN path: no translation rules, and no VLAN
|
||||||
|
10/140/141 rows. `wan-pon-in` (`sfp-sfpplus2`) sits at `pvid=1` as a spare port. jim only handles
|
||||||
|
stream's VLAN-130 WAN and the LAN VLANs.
|
||||||
|
|
||||||
|
## Future: multiple ONTs (per-port VLAN translation)
|
||||||
|
|
||||||
|
If a second ONT arrives (e.g. a Digiweb line for stream, or a second river), trunking breaks: both
|
||||||
|
ONTs deliver **tagged VLAN 10**, and plain bridge-VLAN filtering can't tell them apart. That's when
|
||||||
|
translation earns its place — a switch rule matches on the **ingress port**, so each ONT's VLAN 10
|
||||||
|
becomes a *distinct* fabric VLAN:
|
||||||
|
|
||||||
|
- ONT-A port: VLAN 10 → **141** (→ river)
|
||||||
|
- ONT-B port: VLAN 10 → **142** (→ stream / second river)
|
||||||
|
- mgmt: PVID each ONT port onto its own VLAN (140, 143, …) so both ONTs' `192.168.100.1` stay in
|
||||||
|
separate L2/L3 domains.
|
||||||
|
|
||||||
|
The forward direction isolates naturally (each ONT maps to a different fabric VLAN). The **return**
|
||||||
|
direction is where port targeting is mandatory: both translate *back* to VLAN 10, so bridge VLAN 10
|
||||||
|
now has two members and a plain FDB-miss flood would leak one ONT's upstream to the other. Each
|
||||||
|
return must be pinned to its port with `new-dst-ports`:
|
||||||
|
```
|
||||||
|
# ONT-A: 141 in on palace → 10, forced out ONT-A's port
|
||||||
|
# ONT-B: 142 in on stream → 10, forced out ONT-B's port
|
||||||
|
```
|
||||||
|
Each ONT port must also be a tagged member of bridge VLAN 10 for correct egress tagging (the missing
|
||||||
|
piece that otherwise shows up as pppd "Timeout waiting for PADO"). The pins bypass the FDB, so the
|
||||||
|
two ISP sessions never mix.
|
||||||
|
|
||||||
|
**Why a new switch:** jim (the only box with spare SFP+ *and* the translation feature) has just
|
||||||
|
**one** free SFP+ port, so it can't host two ONTs. The plan is a dedicated
|
||||||
|
**CRS305-1G-4S+** (4×SFP+, same Marvell rule support) to land multiple ONTs and do the per-port
|
||||||
|
translation there, feeding distinct fabric VLANs up to dave.
|
||||||
|
|
||||||
|
## Accessing the switches
|
||||||
|
|
||||||
|
The switches resolve by **short hostname** on the home network — the home routers serve their
|
||||||
|
records in the home zone
|
||||||
|
([`nixos/boxes/home/routing-common/dns.nix`](../../../nixos/boxes/home/routing-common/dns.nix):
|
||||||
|
`jim` → hi `.10`, `dave` → hi `.11`, `brian` → core `.13`). From a box on the home network just
|
||||||
|
`ssh admin@jim` / `admin@dave`. Non-interactive pattern (password auth, avoids the ssh-agent hang):
|
||||||
|
|
||||||
|
```
|
||||||
|
sshpass -p admin ssh -o IdentityAgent=none -o PubkeyAuthentication=no \
|
||||||
|
-o PreferredAuthentications=password -o StrictHostKeyChecking=accept-new \
|
||||||
|
-o UserKnownHostsFile=/tmp/sw_known_hosts admin@jim
|
||||||
|
```
|
||||||
|
|
||||||
|
**Always confirm config changes on the switch** (print the affected menu, apply, re-verify). brian
|
||||||
|
is UniFi — configured through its controller, not RouterOS CLI.
|
||||||
|
|
||||||
|
## Management IPs
|
||||||
|
|
||||||
|
| | core (`192.168.64.0/24`) | hi (`192.168.68.0/22`) | lo (`192.168.72.0/21`) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| jim | `.10` (on `main`) | `.10` | `.10` |
|
||||||
|
| dave | `.11` (on `management`, the 1G Atheros port) | `.11` | `.11` |
|
||||||
|
| brian | `.13` (core) | — | — |
|
||||||
Generated
+697
-314
File diff suppressed because it is too large
Load Diff
@@ -3,40 +3,54 @@
|
|||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
# libnet.url = "github:reo101/nix-lib-net";
|
||||||
|
libnetRepo = {
|
||||||
|
url = "github:oddlama/nixos-extra-modules";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
devshell.url = "github:numtide/devshell";
|
devshell.url = "github:numtide/devshell";
|
||||||
devshell.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
devshell.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||||
|
|
||||||
nixpkgs-unstable.url = "nixpkgs/nixos-unstable";
|
nixpkgs-unstable.url = "nixpkgs/nixos-unstable";
|
||||||
nixpkgs-stable.url = "nixpkgs/nixos-23.11";
|
nixpkgs-stable.url = "nixpkgs/nixos-26.05";
|
||||||
nixpkgs-mine.url = "github:devplayer0/nixpkgs/devplayer0";
|
nixpkgs-mine.url = "github:devplayer0/nixpkgs/devplayer0";
|
||||||
nixpkgs-mine-stable.url = "github:devplayer0/nixpkgs/devplayer0-stable";
|
nixpkgs-mine-stable.url = "github:devplayer0/nixpkgs/devplayer0-stable";
|
||||||
|
|
||||||
home-manager-unstable.url = "home-manager";
|
home-manager-unstable.url = "home-manager";
|
||||||
home-manager-unstable.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
home-manager-unstable.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||||
home-manager-stable.url = "home-manager/release-23.11";
|
home-manager-stable.url = "home-manager/release-26.05";
|
||||||
home-manager-stable.inputs.nixpkgs.follows = "nixpkgs-stable";
|
home-manager-stable.inputs.nixpkgs.follows = "nixpkgs-stable";
|
||||||
|
|
||||||
# Stuff used by the flake for build / deployment
|
# Stuff used by the flake for build / deployment
|
||||||
ragenix.url = "github:yaxitech/ragenix";
|
# ragenix.url = "github:yaxitech/ragenix";
|
||||||
|
ragenix.url = "github:devplayer0/ragenix/add-rekey-one-flag";
|
||||||
ragenix.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
ragenix.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||||
deploy-rs.url = "github:serokell/deploy-rs";
|
deploy-rs.url = "github:serokell/deploy-rs";
|
||||||
deploy-rs.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
deploy-rs.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||||
|
|
||||||
# Stuff used by systems
|
# Stuff used by systems
|
||||||
impermanence.url = "github:nix-community/impermanence";
|
impermanence.url = "github:nix-community/impermanence";
|
||||||
|
impermanence.inputs.home-manager.follows = "home-manager-unstable";
|
||||||
boardie.url = "github:devplayer0/boardie";
|
boardie.url = "github:devplayer0/boardie";
|
||||||
boardie.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
boardie.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||||
nixGL.url = "github:nix-community/nixGL";
|
nixGL.url = "github:nix-community/nixGL";
|
||||||
nixGL.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
nixGL.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||||
|
harmonia.url = "github:nix-community/harmonia";
|
||||||
|
# harmonia.url = "github:devplayer0/harmonia/cache-config-daemon-store";
|
||||||
|
harmonia.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||||
|
|
||||||
# Packages not in nixpkgs
|
# Packages not in nixpkgs
|
||||||
sharry.url = "github:eikek/sharry";
|
sharry.url = "github:eikek/sharry";
|
||||||
sharry.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
sharry.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||||
borgthin.url = "github:devplayer0/borg";
|
borgthin.url = "github:devplayer0/borg";
|
||||||
borgthin.inputs.nixpkgs.follows = "nixpkgs-mine";
|
# TODO: Update borgthin so this works
|
||||||
attic.url = "github:zhaofengli/attic";
|
# borgthin.inputs.nixpkgs.follows = "nixpkgs-mine";
|
||||||
attic.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
copyparty.url = "github:9001/copyparty";
|
||||||
attic.inputs.nixpkgs-stable.follows = "nixpkgs-stable";
|
copyparty.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||||
|
hass-west-wood.url = "github:devplayer0/hass-west-wood";
|
||||||
|
hass-west-wood.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||||
|
pi-agent.url = "github:lukasl-dev/pi.nix";
|
||||||
|
pi-agent.inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
@@ -51,7 +65,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
inherit (builtins) mapAttrs replaceStrings;
|
inherit (builtins) mapAttrs replaceStrings elem;
|
||||||
inherit (lib) mapAttrs' filterAttrs nameValuePair recurseIntoAttrs evalModules;
|
inherit (lib) mapAttrs' filterAttrs nameValuePair recurseIntoAttrs evalModules;
|
||||||
inherit (lib.flake) flattenTree eachDefaultSystem;
|
inherit (lib.flake) flattenTree eachDefaultSystem;
|
||||||
inherit (lib.my) mkDefaultSystemsPkgs flakePackageOverlay;
|
inherit (lib.my) mkDefaultSystemsPkgs flakePackageOverlay;
|
||||||
@@ -59,7 +73,7 @@
|
|||||||
# Extend a lib with extras that _must not_ internally reference private nixpkgs. flake-utils doesn't, but many
|
# Extend a lib with extras that _must not_ internally reference private nixpkgs. flake-utils doesn't, but many
|
||||||
# other flakes (e.g. home-manager) probably do internally.
|
# other flakes (e.g. home-manager) probably do internally.
|
||||||
libOverlay = final: prev: {
|
libOverlay = final: prev: {
|
||||||
my = import ./lib { lib = final; };
|
my = import ./lib { inherit inputs; lib = final; };
|
||||||
flake = flake-utils.lib;
|
flake = flake-utils.lib;
|
||||||
};
|
};
|
||||||
pkgsLibOverlay = final: prev: { lib = prev.lib.extend libOverlay; };
|
pkgsLibOverlay = final: prev: { lib = prev.lib.extend libOverlay; };
|
||||||
@@ -90,12 +104,12 @@
|
|||||||
(_: path: mkDefaultSystemsPkgs path (system: {
|
(_: path: mkDefaultSystemsPkgs path (system: {
|
||||||
overlays = [
|
overlays = [
|
||||||
pkgsLibOverlay
|
pkgsLibOverlay
|
||||||
|
|
||||||
myPkgsOverlay
|
myPkgsOverlay
|
||||||
inputs.devshell.overlays.default
|
inputs.devshell.overlays.default
|
||||||
inputs.ragenix.overlays.default
|
inputs.ragenix.overlays.default
|
||||||
inputs.deploy-rs.overlay
|
inputs.deploy-rs.overlays.default
|
||||||
(flakePackageOverlay inputs.home-manager-unstable system)
|
(flakePackageOverlay inputs.home-manager-unstable system)
|
||||||
inputs.attic.overlays.default
|
|
||||||
];
|
];
|
||||||
}))
|
}))
|
||||||
pkgsFlakes;
|
pkgsFlakes;
|
||||||
@@ -105,8 +119,19 @@
|
|||||||
(_: path: mkDefaultSystemsPkgs path (_: {
|
(_: path: mkDefaultSystemsPkgs path (_: {
|
||||||
overlays = [
|
overlays = [
|
||||||
pkgsLibOverlay
|
pkgsLibOverlay
|
||||||
|
|
||||||
myPkgsOverlay
|
myPkgsOverlay
|
||||||
];
|
];
|
||||||
|
|
||||||
|
config = {
|
||||||
|
# RMS forgive me...
|
||||||
|
# Normally this is set modularly, but sometimes we need to use other pkgs
|
||||||
|
allowUnfreePredicate = p: elem (lib.getName p) [
|
||||||
|
"widevine-cdm"
|
||||||
|
"chromium-unwrapped"
|
||||||
|
"chromium"
|
||||||
|
];
|
||||||
|
};
|
||||||
}))
|
}))
|
||||||
pkgsFlakes;
|
pkgsFlakes;
|
||||||
|
|
||||||
@@ -115,10 +140,11 @@
|
|||||||
nixos/installer.nix
|
nixos/installer.nix
|
||||||
nixos/boxes/colony
|
nixos/boxes/colony
|
||||||
nixos/boxes/tower
|
nixos/boxes/tower
|
||||||
nixos/boxes/castle
|
|
||||||
nixos/boxes/home/stream.nix
|
nixos/boxes/home/stream.nix
|
||||||
nixos/boxes/home/palace
|
nixos/boxes/home/palace
|
||||||
|
nixos/boxes/home/castle
|
||||||
nixos/boxes/britway
|
nixos/boxes/britway
|
||||||
|
nixos/boxes/britnet.nix
|
||||||
nixos/boxes/kelder
|
nixos/boxes/kelder
|
||||||
|
|
||||||
# Homes
|
# Homes
|
||||||
@@ -149,7 +175,7 @@
|
|||||||
# Platform independent stuff
|
# Platform independent stuff
|
||||||
{
|
{
|
||||||
nixpkgs = pkgs';
|
nixpkgs = pkgs';
|
||||||
inherit lib nixfiles;
|
inherit inputs lib nixfiles;
|
||||||
|
|
||||||
overlays.default = myPkgsOverlay;
|
overlays.default = myPkgsOverlay;
|
||||||
|
|
||||||
@@ -197,8 +223,9 @@
|
|||||||
systems' = mapAttrs' (n: v: nameValuePair "system-${n}" v) systems;
|
systems' = mapAttrs' (n: v: nameValuePair "system-${n}" v) systems;
|
||||||
packages' = mapAttrs' (n: v: nameValuePair "package-${n}" v) packages;
|
packages' = mapAttrs' (n: v: nameValuePair "package-${n}" v) packages;
|
||||||
in
|
in
|
||||||
pkgs.linkFarm "ci" (homes' // systems' // packages' // {
|
homes' // systems' // packages' // {
|
||||||
inherit shell;
|
inherit shell;
|
||||||
});
|
};
|
||||||
|
ciDrv = pkgs.linkFarm "ci" ci;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ let
|
|||||||
inherit (lib.my) mkOpt' dummyOption;
|
inherit (lib.my) mkOpt' dummyOption;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
imports = [ inputs.pi-agent.homeModules.default ];
|
||||||
|
|
||||||
options = with lib.types; {
|
options = with lib.types; {
|
||||||
my = {
|
my = {
|
||||||
isStandalone = mkOption {
|
isStandalone = mkOption {
|
||||||
@@ -27,6 +29,7 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkMerge [
|
config = mkMerge [
|
||||||
{
|
{
|
||||||
my = {
|
my = {
|
||||||
@@ -66,7 +69,7 @@ in
|
|||||||
|
|
||||||
lsd = {
|
lsd = {
|
||||||
enable = mkDefault true;
|
enable = mkDefault true;
|
||||||
enableAliases = mkDefault true;
|
enableFishIntegration = mkDefault true;
|
||||||
};
|
};
|
||||||
|
|
||||||
starship = {
|
starship = {
|
||||||
@@ -132,38 +135,38 @@ in
|
|||||||
|
|
||||||
ssh = {
|
ssh = {
|
||||||
enable = mkDefault true;
|
enable = mkDefault true;
|
||||||
matchBlocks = {
|
enableDefaultConfig = false;
|
||||||
|
settings = {
|
||||||
nix-dev-vm = {
|
nix-dev-vm = {
|
||||||
user = "dev";
|
User = "dev";
|
||||||
hostname = "localhost";
|
HostName = "localhost";
|
||||||
port = 2222;
|
Port = 2222;
|
||||||
extraOptions = {
|
|
||||||
StrictHostKeyChecking = "no";
|
StrictHostKeyChecking = "no";
|
||||||
UserKnownHostsFile = "/dev/null";
|
UserKnownHostsFile = "/dev/null";
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
"rsync.net" = {
|
"rsync.net" = {
|
||||||
host = "rsyncnet";
|
Host = "rsyncnet";
|
||||||
user = "16413";
|
User = "16413";
|
||||||
hostname = "ch-s010.rsync";
|
HostName = "ch-s010.rsync";
|
||||||
};
|
};
|
||||||
|
|
||||||
shoe = {
|
shoe = {
|
||||||
host = "shoe.netsoc.tcd.ie shoe";
|
Host = "shoe.netsoc.tcd.ie shoe";
|
||||||
user = "netsoc";
|
User = "netsoc";
|
||||||
};
|
};
|
||||||
netsocBoxes = {
|
netsocBoxes = {
|
||||||
host = "cube spoon napalm gandalf saruman";
|
Host = "cube spoon napalm gandalf saruman";
|
||||||
user = "root";
|
User = "root";
|
||||||
|
};
|
||||||
|
|
||||||
|
"*" = {
|
||||||
|
IdentityFile = [
|
||||||
|
"~/.ssh/id_rsa"
|
||||||
|
"~/.ssh/borg"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
extraConfig =
|
|
||||||
''
|
|
||||||
IdentityFile ~/.ssh/id_rsa
|
|
||||||
IdentityFile ~/.ssh/netsoc
|
|
||||||
IdentityFile ~/.ssh/borg
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
direnv = {
|
direnv = {
|
||||||
@@ -199,17 +202,20 @@ in
|
|||||||
file
|
file
|
||||||
tree
|
tree
|
||||||
pwgen
|
pwgen
|
||||||
|
|
||||||
|
minicom
|
||||||
iperf3
|
iperf3
|
||||||
mosh
|
mosh
|
||||||
wget
|
wget
|
||||||
hyx
|
|
||||||
whois
|
whois
|
||||||
ldns
|
ldns
|
||||||
minicom
|
|
||||||
mtr
|
mtr
|
||||||
|
|
||||||
|
hyx
|
||||||
ncdu
|
ncdu
|
||||||
jq
|
jq
|
||||||
yq-go
|
yq-go
|
||||||
|
nix-tree
|
||||||
];
|
];
|
||||||
|
|
||||||
sessionVariables = {
|
sessionVariables = {
|
||||||
@@ -223,9 +229,10 @@ in
|
|||||||
# Note: If globalPkgs mode is on, then these will be overridden by the NixOS equivalents of these options
|
# Note: If globalPkgs mode is on, then these will be overridden by the NixOS equivalents of these options
|
||||||
nixpkgs = {
|
nixpkgs = {
|
||||||
overlays = [
|
overlays = [
|
||||||
inputs.deploy-rs.overlay
|
inputs.deploy-rs.overlays.default
|
||||||
inputs.boardie.overlays.default
|
inputs.boardie.overlays.default
|
||||||
inputs.nixGL.overlays.default
|
inputs.nixGL.overlays.default
|
||||||
|
inputs.pi-agent.overlays.default
|
||||||
];
|
];
|
||||||
config = {
|
config = {
|
||||||
allowUnfree = true;
|
allowUnfree = true;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# XTerm's default colors
|
||||||
|
|
||||||
|
# Default colors
|
||||||
|
[colors.primary]
|
||||||
|
background = '#000000'
|
||||||
|
foreground = '#ffffff'
|
||||||
|
|
||||||
|
# Normal colors
|
||||||
|
[colors.normal]
|
||||||
|
black = '#000000'
|
||||||
|
red = '#cd0000'
|
||||||
|
green = '#00cd00'
|
||||||
|
yellow = '#cdcd00'
|
||||||
|
blue = '#0000ee'
|
||||||
|
magenta = '#cd00cd'
|
||||||
|
cyan = '#00cdcd'
|
||||||
|
white = '#e5e5e5'
|
||||||
|
|
||||||
|
# Bright colors
|
||||||
|
[colors.bright]
|
||||||
|
black = '#7f7f7f'
|
||||||
|
red = '#ff0000'
|
||||||
|
green = '#00ff00'
|
||||||
|
yellow = '#ffff00'
|
||||||
|
blue = '#5c5cff'
|
||||||
|
magenta = '#ff00ff'
|
||||||
|
cyan = '#00ffff'
|
||||||
|
white = '#ffffff'
|
||||||
+520
@@ -0,0 +1,520 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Source: https://github.com/daniel3303/ClaudeCodeStatusLine
|
||||||
|
# Single line: Model | tokens | %used | %remain | think | 5h bar @reset | 7d bar @reset | extra
|
||||||
|
|
||||||
|
set -f # disable globbing
|
||||||
|
VERSION="1.4.4"
|
||||||
|
|
||||||
|
input=$(cat)
|
||||||
|
|
||||||
|
if [ -z "$input" ]; then
|
||||||
|
printf "Claude"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ANSI colors matching oh-my-posh theme
|
||||||
|
blue='\033[38;2;0;153;255m'
|
||||||
|
orange='\033[38;2;255;176;85m'
|
||||||
|
green='\033[38;2;0;160;0m'
|
||||||
|
cyan='\033[38;2;46;149;153m'
|
||||||
|
red='\033[38;2;255;85;85m'
|
||||||
|
yellow='\033[38;2;230;200;0m'
|
||||||
|
purple='\033[38;2;167;139;250m'
|
||||||
|
white='\033[38;2;220;220;220m'
|
||||||
|
dim='\033[2m'
|
||||||
|
reset='\033[0m'
|
||||||
|
|
||||||
|
# Format token counts (e.g., 50k / 200k)
|
||||||
|
format_tokens() {
|
||||||
|
local num=$1
|
||||||
|
if [ "$num" -ge 1000000 ]; then
|
||||||
|
awk "BEGIN {v=sprintf(\"%.1f\",$num/1000000)+0; if(v==int(v)) printf \"%dm\",v; else printf \"%.1fm\",v}"
|
||||||
|
elif [ "$num" -ge 1000 ]; then
|
||||||
|
awk "BEGIN {printf \"%.0fk\", $num / 1000}"
|
||||||
|
else
|
||||||
|
printf "%d" "$num"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Format number with commas (e.g., 134,938)
|
||||||
|
format_commas() {
|
||||||
|
printf "%'d" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Return color escape based on usage percentage
|
||||||
|
# Usage: usage_color <pct>
|
||||||
|
usage_color() {
|
||||||
|
local pct=$1
|
||||||
|
if [ "$pct" -ge 90 ]; then echo "$red"
|
||||||
|
elif [ "$pct" -ge 70 ]; then echo "$orange"
|
||||||
|
elif [ "$pct" -ge 50 ]; then echo "$yellow"
|
||||||
|
else echo "$green"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve config directory: CLAUDE_CONFIG_DIR (set by alias) or default ~/.claude
|
||||||
|
claude_config_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
||||||
|
|
||||||
|
# Return 0 (true) if $1 > $2 using semantic versioning
|
||||||
|
version_gt() {
|
||||||
|
local a="${1#v}" b="${2#v}"
|
||||||
|
local IFS='.'
|
||||||
|
read -r a1 a2 a3 <<< "$a"
|
||||||
|
read -r b1 b2 b3 <<< "$b"
|
||||||
|
a1=${a1:-0}; a2=${a2:-0}; a3=${a3:-0}
|
||||||
|
b1=${b1:-0}; b2=${b2:-0}; b3=${b3:-0}
|
||||||
|
[ "$a1" -gt "$b1" ] 2>/dev/null && return 0
|
||||||
|
[ "$a1" -lt "$b1" ] 2>/dev/null && return 1
|
||||||
|
[ "$a2" -gt "$b2" ] 2>/dev/null && return 0
|
||||||
|
[ "$a2" -lt "$b2" ] 2>/dev/null && return 1
|
||||||
|
[ "$a3" -gt "$b3" ] 2>/dev/null && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
# ===== Extract data from JSON =====
|
||||||
|
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
||||||
|
model_name=$(echo "$model_name" | sed 's/ *(\([0-9.]*[kKmM]*\) context)/ \1/') # "(1M context)" → "1M"
|
||||||
|
|
||||||
|
# Context window
|
||||||
|
size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
|
||||||
|
[ "$size" -eq 0 ] 2>/dev/null && size=200000
|
||||||
|
|
||||||
|
# Token usage
|
||||||
|
input_tokens=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0')
|
||||||
|
cache_create=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0')
|
||||||
|
cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
|
||||||
|
current=$(( input_tokens + cache_create + cache_read ))
|
||||||
|
|
||||||
|
used_tokens=$(format_tokens $current)
|
||||||
|
total_tokens=$(format_tokens $size)
|
||||||
|
|
||||||
|
if [ "$size" -gt 0 ]; then
|
||||||
|
pct_used=$(( current * 100 / size ))
|
||||||
|
else
|
||||||
|
pct_used=0
|
||||||
|
fi
|
||||||
|
pct_remain=$(( 100 - pct_used ))
|
||||||
|
|
||||||
|
used_comma=$(format_commas $current)
|
||||||
|
remain_comma=$(format_commas $(( size - current )))
|
||||||
|
|
||||||
|
settings_path="$claude_config_dir/settings.json"
|
||||||
|
effort_level=""
|
||||||
|
stdin_effort=$(echo "$input" | jq -r '.effort.level // empty' 2>/dev/null)
|
||||||
|
if [ -n "$stdin_effort" ]; then
|
||||||
|
effort_level="$stdin_effort"
|
||||||
|
elif [ -n "$CLAUDE_CODE_EFFORT_LEVEL" ]; then
|
||||||
|
effort_level="$CLAUDE_CODE_EFFORT_LEVEL"
|
||||||
|
elif [ -f "$settings_path" ]; then
|
||||||
|
effort_val=$(jq -r '.effortLevel // empty' "$settings_path" 2>/dev/null)
|
||||||
|
[ -n "$effort_val" ] && effort_level="$effort_val"
|
||||||
|
fi
|
||||||
|
[ -z "$effort_level" ] && effort_level="medium"
|
||||||
|
|
||||||
|
# ===== Claude CLI version (cached, 1h TTL) =====
|
||||||
|
cli_version_cache="/tmp/claude/statusline-cli-version"
|
||||||
|
cli_version=""
|
||||||
|
cli_version_max_age=3600
|
||||||
|
|
||||||
|
if [ -f "$cli_version_cache" ]; then
|
||||||
|
cv_mtime=$(stat -c %Y "$cli_version_cache" 2>/dev/null || stat -f %m "$cli_version_cache" 2>/dev/null)
|
||||||
|
cv_now=$(date +%s)
|
||||||
|
cv_age=$(( cv_now - cv_mtime ))
|
||||||
|
if [ "$cv_age" -lt "$cli_version_max_age" ]; then
|
||||||
|
cli_version=$(cat "$cli_version_cache" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$cli_version" ]; then
|
||||||
|
cli_version=$(claude --version 2>/dev/null | awk '{print $1}')
|
||||||
|
if [ -n "$cli_version" ]; then
|
||||||
|
mkdir -p /tmp/claude 2>/dev/null
|
||||||
|
echo "$cli_version" > "$cli_version_cache"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Build single-line output =====
|
||||||
|
out=""
|
||||||
|
out+="${blue}${model_name}${reset}"
|
||||||
|
|
||||||
|
# Current working directory
|
||||||
|
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
||||||
|
if [ -n "$cwd" ]; then
|
||||||
|
display_dir="${cwd##*/}"
|
||||||
|
git_branch=$(git -C "${cwd}" rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||||
|
out+=" ${dim}|${reset} "
|
||||||
|
out+="${cyan}${display_dir}${reset}"
|
||||||
|
if [ -n "$git_branch" ]; then
|
||||||
|
out+="${dim}@${reset}${green}${git_branch}${reset}"
|
||||||
|
git_stat=$(git -C "${cwd}" diff --numstat 2>/dev/null | awk '{a+=$1; d+=$2} END {if (a+d>0) printf "+%d -%d", a, d}')
|
||||||
|
[ -n "$git_stat" ] && out+=" ${dim}(${reset}${green}${git_stat%% *}${reset} ${red}${git_stat##* }${reset}${dim})${reset}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
out+=" ${dim}|${reset} "
|
||||||
|
out+="${orange}${used_tokens}/${total_tokens}${reset} ${dim}(${reset}${green}${pct_used}%${reset}${dim})${reset}"
|
||||||
|
out+=" ${dim}|${reset} "
|
||||||
|
out+="effort: "
|
||||||
|
case "$effort_level" in
|
||||||
|
low) out+="${dim}${effort_level}${reset}" ;;
|
||||||
|
medium) out+="${orange}med${reset}" ;;
|
||||||
|
high) out+="${green}${effort_level}${reset}" ;;
|
||||||
|
xhigh) out+="${purple}${effort_level}${reset}" ;;
|
||||||
|
max) out+="${red}${effort_level}${reset}" ;;
|
||||||
|
*) out+="${green}${effort_level}${reset}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ===== Cross-platform OAuth token resolution (from statusline.sh) =====
|
||||||
|
# Tries credential sources in order: env var → macOS Keychain → Linux creds file → GNOME Keyring
|
||||||
|
get_oauth_token() {
|
||||||
|
local token=""
|
||||||
|
|
||||||
|
# 1. Explicit env var override
|
||||||
|
if [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
|
||||||
|
echo "$CLAUDE_CODE_OAUTH_TOKEN"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. macOS Keychain (Claude Code appends a SHA256 hash of CLAUDE_CONFIG_DIR to the service name)
|
||||||
|
if command -v security >/dev/null 2>&1; then
|
||||||
|
local keychain_svc="Claude Code-credentials"
|
||||||
|
if [ -n "$CLAUDE_CONFIG_DIR" ]; then
|
||||||
|
local dir_hash
|
||||||
|
dir_hash=$(echo -n "$CLAUDE_CONFIG_DIR" | shasum -a 256 | cut -c1-8)
|
||||||
|
keychain_svc="Claude Code-credentials-${dir_hash}"
|
||||||
|
fi
|
||||||
|
local blob
|
||||||
|
blob=$(security find-generic-password -s "$keychain_svc" -w 2>/dev/null)
|
||||||
|
if [ -n "$blob" ]; then
|
||||||
|
token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
|
||||||
|
if [ -n "$token" ] && [ "$token" != "null" ]; then
|
||||||
|
echo "$token"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Linux credentials file
|
||||||
|
local creds_file="${claude_config_dir}/.credentials.json"
|
||||||
|
if [ -f "$creds_file" ]; then
|
||||||
|
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$creds_file" 2>/dev/null)
|
||||||
|
if [ -n "$token" ] && [ "$token" != "null" ]; then
|
||||||
|
echo "$token"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. GNOME Keyring via secret-tool
|
||||||
|
if command -v secret-tool >/dev/null 2>&1; then
|
||||||
|
local blob
|
||||||
|
blob=$(timeout 2 secret-tool lookup service "Claude Code-credentials" 2>/dev/null)
|
||||||
|
if [ -n "$blob" ]; then
|
||||||
|
token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
|
||||||
|
if [ -n "$token" ] && [ "$token" != "null" ]; then
|
||||||
|
echo "$token"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===== LINE 2 & 3: Usage limits with progress bars =====
|
||||||
|
# First, try to use rate_limits data provided directly by Claude Code in the JSON input.
|
||||||
|
# This is the most reliable source — no OAuth token or API call required.
|
||||||
|
builtin_five_hour_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
|
||||||
|
builtin_five_hour_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
|
||||||
|
builtin_seven_day_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
|
||||||
|
builtin_seven_day_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
|
||||||
|
|
||||||
|
use_builtin=false
|
||||||
|
if [ -n "$builtin_five_hour_pct" ] || [ -n "$builtin_seven_day_pct" ]; then
|
||||||
|
use_builtin=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cache setup — shared across all Claude Code instances to avoid rate limits
|
||||||
|
claude_config_dir_hash=$(echo -n "$claude_config_dir" | shasum -a 256 2>/dev/null || echo -n "$claude_config_dir" | sha256sum 2>/dev/null)
|
||||||
|
claude_config_dir_hash=$(echo "$claude_config_dir_hash" | cut -c1-8)
|
||||||
|
cache_file="/tmp/claude/statusline-usage-cache-${claude_config_dir_hash}.json"
|
||||||
|
cache_max_age=60 # seconds between API calls
|
||||||
|
mkdir -p /tmp/claude
|
||||||
|
|
||||||
|
needs_refresh=true
|
||||||
|
usage_data=""
|
||||||
|
|
||||||
|
# Always load cache — used as primary source for API path, and as fallback when builtin reports zero
|
||||||
|
if [ -f "$cache_file" ] && [ -s "$cache_file" ]; then
|
||||||
|
cache_mtime=$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null)
|
||||||
|
now=$(date +%s)
|
||||||
|
cache_age=$(( now - cache_mtime ))
|
||||||
|
if [ "$cache_age" -lt "$cache_max_age" ]; then
|
||||||
|
needs_refresh=false
|
||||||
|
fi
|
||||||
|
usage_data=$(cat "$cache_file" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# When builtin values are all zero AND reset timestamps are missing, it likely indicates
|
||||||
|
# an API failure on Claude's side — fall through to cached data instead of displaying
|
||||||
|
# misleading 0%. Genuine zero responses (after a billing reset) still include valid
|
||||||
|
# resets_at timestamps, so we trust those.
|
||||||
|
effective_builtin=false
|
||||||
|
if $use_builtin; then
|
||||||
|
# Trust builtin if any percentage is non-zero
|
||||||
|
if { [ -n "$builtin_five_hour_pct" ] && [ "$(printf '%.0f' "$builtin_five_hour_pct" 2>/dev/null)" != "0" ]; } || \
|
||||||
|
{ [ -n "$builtin_seven_day_pct" ] && [ "$(printf '%.0f' "$builtin_seven_day_pct" 2>/dev/null)" != "0" ]; }; then
|
||||||
|
effective_builtin=true
|
||||||
|
fi
|
||||||
|
# Also trust if reset timestamps are present — genuine zero responses include valid reset times
|
||||||
|
if ! $effective_builtin; then
|
||||||
|
if { [ -n "$builtin_five_hour_reset" ] && [ "$builtin_five_hour_reset" != "null" ] && [ "$builtin_five_hour_reset" != "0" ]; } || \
|
||||||
|
{ [ -n "$builtin_seven_day_reset" ] && [ "$builtin_seven_day_reset" != "null" ] && [ "$builtin_seven_day_reset" != "0" ]; }; then
|
||||||
|
effective_builtin=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Refresh API cache when stale — runs regardless of builtin rate_limits because
|
||||||
|
# extra_usage is only exposed through the OAuth usage endpoint (not stdin JSON).
|
||||||
|
# Throttled to cache_max_age and stampede-locked via touch for shared panes.
|
||||||
|
if $needs_refresh; then
|
||||||
|
touch "$cache_file" # stampede lock: prevent parallel panes from fetching simultaneously
|
||||||
|
token=$(get_oauth_token)
|
||||||
|
if [ -n "$token" ] && [ "$token" != "null" ]; then
|
||||||
|
response=$(curl -s --max-time 10 \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H "anthropic-beta: oauth-2025-04-20" \
|
||||||
|
-H "User-Agent: claude-code/2.1.34" \
|
||||||
|
"https://api.anthropic.com/api/oauth/usage" 2>/dev/null)
|
||||||
|
# Only cache valid usage responses (not error/rate-limit JSON)
|
||||||
|
if [ -n "$response" ] && echo "$response" | jq -e '.five_hour' >/dev/null 2>&1; then
|
||||||
|
usage_data="$response"
|
||||||
|
echo "$response" > "$cache_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Remove the stampede sentinel if the fetch failed to produce valid JSON —
|
||||||
|
# otherwise an empty cache file would suppress retries for a full cache_max_age window.
|
||||||
|
[ -f "$cache_file" ] && [ ! -s "$cache_file" ] && rm -f "$cache_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cross-platform ISO to epoch conversion
|
||||||
|
# Converts ISO 8601 timestamp (e.g. "2025-06-15T12:30:00Z" or "2025-06-15T12:30:00.123+00:00") to epoch seconds.
|
||||||
|
# Properly handles UTC timestamps and converts to local time.
|
||||||
|
iso_to_epoch() {
|
||||||
|
local iso_str="$1"
|
||||||
|
|
||||||
|
# Try GNU date first (Linux) — handles ISO 8601 format automatically
|
||||||
|
local epoch
|
||||||
|
epoch=$(date -d "${iso_str}" +%s 2>/dev/null)
|
||||||
|
if [ -n "$epoch" ]; then
|
||||||
|
echo "$epoch"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# BSD date (macOS) - handle various ISO 8601 formats
|
||||||
|
local stripped="${iso_str%%.*}" # Remove fractional seconds (.123456)
|
||||||
|
stripped="${stripped%%Z}" # Remove trailing Z
|
||||||
|
stripped="${stripped%%+*}" # Remove timezone offset (+00:00)
|
||||||
|
stripped="${stripped%%-[0-9][0-9]:[0-9][0-9]}" # Remove negative timezone offset
|
||||||
|
|
||||||
|
# Check if timestamp is UTC (has Z or +00:00 or -00:00)
|
||||||
|
if [[ "$iso_str" == *"Z"* ]] || [[ "$iso_str" == *"+00:00"* ]] || [[ "$iso_str" == *"-00:00"* ]]; then
|
||||||
|
# For UTC timestamps, parse with timezone set to UTC
|
||||||
|
epoch=$(env TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null)
|
||||||
|
else
|
||||||
|
epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$epoch" ]; then
|
||||||
|
echo "$epoch"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Format ISO reset time to compact local time
|
||||||
|
# Usage: format_reset_time <iso_string> <style: time|datetime|date>
|
||||||
|
format_reset_time() {
|
||||||
|
local iso_str="$1"
|
||||||
|
local style="$2"
|
||||||
|
{ [ -z "$iso_str" ] || [ "$iso_str" = "null" ]; } && return
|
||||||
|
|
||||||
|
# Parse ISO datetime and convert to local time (cross-platform)
|
||||||
|
local epoch
|
||||||
|
epoch=$(iso_to_epoch "$iso_str")
|
||||||
|
[ -z "$epoch" ] && return
|
||||||
|
|
||||||
|
# Format based on style
|
||||||
|
# Try GNU date first (Linux), then BSD date (macOS)
|
||||||
|
# Previous implementation piped BSD date through sed/tr, which always returned
|
||||||
|
# exit code 0 from the last pipe stage, preventing the GNU date fallback from
|
||||||
|
# ever executing on Linux.
|
||||||
|
local formatted=""
|
||||||
|
case "$style" in
|
||||||
|
time)
|
||||||
|
formatted=$(date -d "@$epoch" +"%H:%M" 2>/dev/null) || \
|
||||||
|
formatted=$(date -j -r "$epoch" +"%H:%M" 2>/dev/null)
|
||||||
|
;;
|
||||||
|
datetime)
|
||||||
|
formatted=$(date -d "@$epoch" +"%a %b %-d, %H:%M" 2>/dev/null) || \
|
||||||
|
formatted=$(date -j -r "$epoch" +"%a %b %-d, %H:%M" 2>/dev/null)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
formatted=$(date -d "@$epoch" +"%b %-d" 2>/dev/null) || \
|
||||||
|
formatted=$(date -j -r "$epoch" +"%b %-d" 2>/dev/null)
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
[ -n "$formatted" ] && echo "$formatted"
|
||||||
|
}
|
||||||
|
|
||||||
|
sep=" ${dim}|${reset} "
|
||||||
|
|
||||||
|
# Render extra_usage segment from API usage data (not available via stdin rate_limits).
|
||||||
|
# Appends to the global $out. No-op when data is missing or is_enabled is false.
|
||||||
|
render_extra_usage() {
|
||||||
|
local data="$1"
|
||||||
|
[ -z "$data" ] && return
|
||||||
|
local enabled
|
||||||
|
enabled=$(echo "$data" | jq -r '.extra_usage.is_enabled // false' 2>/dev/null)
|
||||||
|
[ "$enabled" != "true" ] && return
|
||||||
|
|
||||||
|
local pct used limit
|
||||||
|
pct=$(echo "$data" | jq -r '.extra_usage.utilization // 0' | awk '{printf "%.0f", $1}')
|
||||||
|
used=$(echo "$data" | jq -r '.extra_usage.used_credits // 0' | LC_NUMERIC=C awk '{printf "%.2f", $1/100}')
|
||||||
|
limit=$(echo "$data" | jq -r '.extra_usage.monthly_limit // 0' | LC_NUMERIC=C awk '{printf "%.2f", $1/100}')
|
||||||
|
|
||||||
|
if [ -n "$used" ] && [ -n "$limit" ] && [[ "$used" != *'$'* ]] && [[ "$limit" != *'$'* ]]; then
|
||||||
|
local color
|
||||||
|
color=$(usage_color "$pct")
|
||||||
|
out+="${sep}${white}extra${reset} ${color}\$${used}/\$${limit}${reset}"
|
||||||
|
else
|
||||||
|
out+="${sep}${white}extra${reset} ${green}enabled${reset}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if $effective_builtin; then
|
||||||
|
# ---- Use rate_limits data provided directly by Claude Code in JSON input ----
|
||||||
|
# resets_at values are Unix epoch integers in this source
|
||||||
|
if [ -n "$builtin_five_hour_pct" ]; then
|
||||||
|
five_hour_pct=$(printf "%.0f" "$builtin_five_hour_pct")
|
||||||
|
five_hour_color=$(usage_color "$five_hour_pct")
|
||||||
|
out+="${sep}${white}5h${reset} ${five_hour_color}${five_hour_pct}%${reset}"
|
||||||
|
if [ -n "$builtin_five_hour_reset" ] && [ "$builtin_five_hour_reset" != "null" ]; then
|
||||||
|
five_hour_reset=$(date -j -r "$builtin_five_hour_reset" +"%H:%M" 2>/dev/null || date -d "@$builtin_five_hour_reset" +"%H:%M" 2>/dev/null)
|
||||||
|
[ -n "$five_hour_reset" ] && out+=" ${dim}@${five_hour_reset}${reset}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$builtin_seven_day_pct" ]; then
|
||||||
|
seven_day_pct=$(printf "%.0f" "$builtin_seven_day_pct")
|
||||||
|
seven_day_color=$(usage_color "$seven_day_pct")
|
||||||
|
out+="${sep}${white}7d${reset} ${seven_day_color}${seven_day_pct}%${reset}"
|
||||||
|
if [ -n "$builtin_seven_day_reset" ] && [ "$builtin_seven_day_reset" != "null" ]; then
|
||||||
|
seven_day_reset=$(date -j -r "$builtin_seven_day_reset" +"%a %b %-d, %H:%M" 2>/dev/null || date -d "@$builtin_seven_day_reset" +"%a %b %-d, %H:%M" 2>/dev/null)
|
||||||
|
[ -n "$seven_day_reset" ] && out+=" ${dim}@${seven_day_reset}${reset}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Render extra_usage from API cache (stdin rate_limits doesn't expose it)
|
||||||
|
render_extra_usage "$usage_data"
|
||||||
|
|
||||||
|
# Cache builtin values so they're available as fallback when API is unavailable.
|
||||||
|
# Convert epoch resets_at to ISO 8601 for compatibility with the API-format cache parser.
|
||||||
|
# Preserve extra_usage from prior API response so we don't clobber it.
|
||||||
|
_fh_reset_json="null"
|
||||||
|
if [ -n "$builtin_five_hour_reset" ] && [ "$builtin_five_hour_reset" != "null" ] && [ "$builtin_five_hour_reset" != "0" ]; then
|
||||||
|
_fh_iso=$(date -u -r "$builtin_five_hour_reset" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
|
||||||
|
date -u -d "@$builtin_five_hour_reset" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null)
|
||||||
|
[ -n "$_fh_iso" ] && _fh_reset_json="\"$_fh_iso\""
|
||||||
|
fi
|
||||||
|
_sd_reset_json="null"
|
||||||
|
if [ -n "$builtin_seven_day_reset" ] && [ "$builtin_seven_day_reset" != "null" ] && [ "$builtin_seven_day_reset" != "0" ]; then
|
||||||
|
_sd_iso=$(date -u -r "$builtin_seven_day_reset" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
|
||||||
|
date -u -d "@$builtin_seven_day_reset" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null)
|
||||||
|
[ -n "$_sd_iso" ] && _sd_reset_json="\"$_sd_iso\""
|
||||||
|
fi
|
||||||
|
_extra_json=$(echo "$usage_data" | jq -c '.extra_usage // null' 2>/dev/null)
|
||||||
|
[ -z "$_extra_json" ] && _extra_json="null"
|
||||||
|
printf '{"five_hour":{"utilization":%s,"resets_at":%s},"seven_day":{"utilization":%s,"resets_at":%s},"extra_usage":%s}' \
|
||||||
|
"${builtin_five_hour_pct:-0}" "$_fh_reset_json" \
|
||||||
|
"${builtin_seven_day_pct:-0}" "$_sd_reset_json" \
|
||||||
|
"$_extra_json" > "$cache_file" 2>/dev/null
|
||||||
|
elif [ -n "$usage_data" ] && echo "$usage_data" | jq -e '.five_hour' >/dev/null 2>&1; then
|
||||||
|
# ---- Fall back: API-fetched usage data ----
|
||||||
|
# ---- 5-hour (current) ----
|
||||||
|
five_hour_pct=$(echo "$usage_data" | jq -r '.five_hour.utilization // 0' | awk '{printf "%.0f", $1}')
|
||||||
|
five_hour_reset_iso=$(echo "$usage_data" | jq -r '.five_hour.resets_at // empty')
|
||||||
|
five_hour_reset=$(format_reset_time "$five_hour_reset_iso" "time")
|
||||||
|
five_hour_color=$(usage_color "$five_hour_pct")
|
||||||
|
|
||||||
|
out+="${sep}${white}5h${reset} ${five_hour_color}${five_hour_pct}%${reset}"
|
||||||
|
[ -n "$five_hour_reset" ] && out+=" ${dim}@${five_hour_reset}${reset}"
|
||||||
|
|
||||||
|
# ---- 7-day (weekly) ----
|
||||||
|
seven_day_pct=$(echo "$usage_data" | jq -r '.seven_day.utilization // 0' | awk '{printf "%.0f", $1}')
|
||||||
|
seven_day_reset_iso=$(echo "$usage_data" | jq -r '.seven_day.resets_at // empty')
|
||||||
|
seven_day_reset=$(format_reset_time "$seven_day_reset_iso" "datetime")
|
||||||
|
seven_day_color=$(usage_color "$seven_day_pct")
|
||||||
|
|
||||||
|
out+="${sep}${white}7d${reset} ${seven_day_color}${seven_day_pct}%${reset}"
|
||||||
|
[ -n "$seven_day_reset" ] && out+=" ${dim}@${seven_day_reset}${reset}"
|
||||||
|
|
||||||
|
render_extra_usage "$usage_data"
|
||||||
|
else
|
||||||
|
# No valid usage data — show placeholders
|
||||||
|
out+="${sep}${white}5h${reset} ${dim}-${reset}"
|
||||||
|
out+="${sep}${white}7d${reset} ${dim}-${reset}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Update check (cached, 24h TTL) =====
|
||||||
|
# Set STATUSLINE_CHECK_UPDATES=false to disable the update check (no network calls).
|
||||||
|
update_line=""
|
||||||
|
if [ "${STATUSLINE_CHECK_UPDATES:-true}" != "false" ]; then
|
||||||
|
version_cache_file="/tmp/claude/statusline-version-cache.json"
|
||||||
|
version_cache_max_age=86400 # 24 hours
|
||||||
|
|
||||||
|
version_needs_refresh=true
|
||||||
|
version_data=""
|
||||||
|
|
||||||
|
if [ -f "$version_cache_file" ]; then
|
||||||
|
vc_mtime=$(stat -c %Y "$version_cache_file" 2>/dev/null || stat -f %m "$version_cache_file" 2>/dev/null)
|
||||||
|
vc_now=$(date +%s)
|
||||||
|
vc_age=$(( vc_now - vc_mtime ))
|
||||||
|
if [ "$vc_age" -lt "$version_cache_max_age" ]; then
|
||||||
|
version_needs_refresh=false
|
||||||
|
fi
|
||||||
|
version_data=$(cat "$version_cache_file" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $version_needs_refresh; then
|
||||||
|
touch "$version_cache_file" 2>/dev/null
|
||||||
|
vc_response=$(curl -s --max-time 5 \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
"https://api.github.com/repos/daniel3303/ClaudeCodeStatusLine/releases/latest" 2>/dev/null)
|
||||||
|
if [ -n "$vc_response" ] && echo "$vc_response" | jq -e '.tag_name' >/dev/null 2>&1; then
|
||||||
|
version_data="$vc_response"
|
||||||
|
echo "$vc_response" > "$version_cache_file"
|
||||||
|
elif [ ! -s "$version_cache_file" ]; then
|
||||||
|
rm -f "$version_cache_file" 2>/dev/null
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$version_data" ]; then
|
||||||
|
latest_tag=$(echo "$version_data" | jq -r '.tag_name // empty')
|
||||||
|
if [ -n "$latest_tag" ] && version_gt "$latest_tag" "$VERSION"; then
|
||||||
|
update_line="\n${dim}Update available: ${latest_tag} → Tell Claude: \"Find my installed status bar and update it\"${reset}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Append CLI version as last segment
|
||||||
|
if [ -n "$cli_version" ]; then
|
||||||
|
out+=" ${dim}|${reset} ${orange}v${cli_version}${reset}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Output
|
||||||
|
printf "%b" "$out$update_line"
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
{ lib, pkgs, config, ... }:
|
{ lib, pkgs', pkgs, config, ... }:
|
||||||
let
|
let
|
||||||
inherit (lib) genAttrs mkIf mkMerge mkForce;
|
inherit (lib) genAttrs mkIf mkMerge mkForce mapAttrs mkOptionDefault mkDefault;
|
||||||
inherit (lib.my) mkBoolOpt';
|
inherit (lib.my) mkOpt' mkBoolOpt';
|
||||||
|
inherit (lib.my.c) pubDomain;
|
||||||
|
|
||||||
cfg = config.my.gui;
|
cfg = config.my.gui;
|
||||||
|
|
||||||
@@ -10,33 +11,93 @@ let
|
|||||||
name = "Monocraft";
|
name = "Monocraft";
|
||||||
size = 10;
|
size = 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
doomWad = pkgs.fetchurl {
|
||||||
|
url = "https://distro.ibiblio.org/slitaz/sources/packages/d/doom1.wad";
|
||||||
|
hash = "sha256-HX1DvlAeZ9kn5BXguPPinDvzMHXoWXIYFvZSpSbKx3E=";
|
||||||
|
};
|
||||||
|
subwaySurfers = pkgs.fetchurl {
|
||||||
|
url = "https://p.${pubDomain}/video/subway-surfers-smol.mkv";
|
||||||
|
hash = "sha256-fMe7TDRNTymRHIJOi7qG3trzu4GP8a3gCDz+FMkX1dY=";
|
||||||
|
};
|
||||||
|
minecraftParkour = pkgs.fetchurl {
|
||||||
|
url = "https://p.${pubDomain}/video/minecraft-parkour-smol.mkv";
|
||||||
|
hash = "sha256-723pRm4AsIjY/WFUyAHzTJp+JvH4Pn5hvzF9wHTnOPA=";
|
||||||
|
};
|
||||||
|
|
||||||
|
genLipsum = pkgs.writeScript "lipsum" ''
|
||||||
|
#!${pkgs.python3.withPackages (ps: [ ps.python-lorem ])}/bin/python
|
||||||
|
import lorem
|
||||||
|
print(lorem.get_paragraph(count=5, sep='\n\n'))
|
||||||
|
'';
|
||||||
|
doomsaver' = brainrotTextCommand: pkgs.runCommand "doomsaver" {
|
||||||
|
inherit (pkgs) windowtolayer tmux terminaltexteffects;
|
||||||
|
chocoDoom = pkgs.chocolate-doom2xx;
|
||||||
|
ffmpeg = pkgs.ffmpeg-full;
|
||||||
|
python = pkgs.python3.withPackages (ps: [ ps.filelock ]);
|
||||||
|
|
||||||
|
inherit doomWad;
|
||||||
|
enojy = ./enojy.jpg;
|
||||||
|
inherit brainrotTextCommand subwaySurfers minecraftParkour;
|
||||||
|
} ''
|
||||||
|
mkdir -p "$out"/bin
|
||||||
|
substituteAll ${./screensaver.py} "$out"/bin/doomsaver
|
||||||
|
chmod +x "$out"/bin/doomsaver
|
||||||
|
'';
|
||||||
|
doomsaver = doomsaver' cfg.screensaver.brainrotTextCommand;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.my.gui = {
|
options.my.gui = with lib.types; {
|
||||||
enable = mkBoolOpt' true "Enable settings and packages meant for graphical systems";
|
enable = mkBoolOpt' true "Enable settings and packages meant for graphical systems";
|
||||||
manageGraphical = mkBoolOpt' false "Configure the graphical session";
|
manageGraphical = mkBoolOpt' false "Configure the graphical session";
|
||||||
standalone = mkBoolOpt' false "Enable settings for fully Nix managed systems";
|
standalone = mkBoolOpt' false "Enable settings for fully Nix managed systems";
|
||||||
|
screensaver.brainrotTextCommand = mkOpt' (either path str) genLipsum "Command to generate brainrot text.";
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable (mkMerge [
|
config = mkIf cfg.enable (mkMerge [
|
||||||
{
|
{
|
||||||
home = {
|
home = {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
|
xdg-utils
|
||||||
|
|
||||||
font.package
|
font.package
|
||||||
(nerdfonts.override {
|
nerd-fonts.sauce-code-pro
|
||||||
fonts = [ "DroidSansMono" "SourceCodePro" ];
|
nerd-fonts.droid-sans-mono
|
||||||
})
|
noto-fonts-color-emoji
|
||||||
noto-fonts-emoji
|
|
||||||
|
|
||||||
grim
|
grim
|
||||||
slurp
|
slurp
|
||||||
swappy
|
swappy
|
||||||
|
|
||||||
python310Packages.python-lsp-server
|
python3Packages.python-lsp-server
|
||||||
nil # nix language server
|
nil # nix language server
|
||||||
|
nixd # another nix language server
|
||||||
zls # zig language server
|
zls # zig language server
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
neofetch
|
pyright
|
||||||
|
|
||||||
|
cowsay
|
||||||
|
fortune
|
||||||
|
jp2a
|
||||||
|
terminaltexteffects
|
||||||
|
screenfetch
|
||||||
|
fastfetch
|
||||||
|
cmatrix
|
||||||
|
doomsaver
|
||||||
|
|
||||||
|
ffmpeg-full
|
||||||
|
xournalpp
|
||||||
|
|
||||||
|
(pkgs.symlinkJoin {
|
||||||
|
name = "pi-coding-agent";
|
||||||
|
buildInputs = [ pkgs.makeWrapper ];
|
||||||
|
paths = [ pkgs.pi-coding-agent-bun ];
|
||||||
|
postBuild = ''
|
||||||
|
wrapProgram $out/bin/pi \
|
||||||
|
--set NPM_CONFIG_PREFIX ${config.home.homeDirectory}/.pi/npm/ \
|
||||||
|
--prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.nodejs_latest ]}
|
||||||
|
'';
|
||||||
|
})
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +112,15 @@ in
|
|||||||
alacritty = {
|
alacritty = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings = {
|
settings = {
|
||||||
font.normal.family = font.name;
|
general.import = [ ./alacritty-xterm.toml ];
|
||||||
|
|
||||||
|
font = {
|
||||||
|
size = font.size;
|
||||||
|
normal = {
|
||||||
|
family = font.name;
|
||||||
|
style = "Regular";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,9 +128,23 @@ in
|
|||||||
enable = true;
|
enable = true;
|
||||||
inherit font;
|
inherit font;
|
||||||
settings = {
|
settings = {
|
||||||
background_opacity = "0.8";
|
background_opacity = "0.65";
|
||||||
tab_bar_edge = "top";
|
tab_bar_edge = "top";
|
||||||
shell_integration = "no-sudo";
|
shell_integration = "no-sudo";
|
||||||
|
font_features = "${font.name} -liga";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
foot = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
main = {
|
||||||
|
font = "${font.name}:size=${toString font.size}";
|
||||||
|
};
|
||||||
|
colors = {
|
||||||
|
alpha = 0.8;
|
||||||
|
background = "000000";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,6 +168,19 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
claude-code = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
model = "opus";
|
||||||
|
theme = "auto";
|
||||||
|
statusLine = {
|
||||||
|
type = "command";
|
||||||
|
command = ./claude-statusline.sh;
|
||||||
|
padding = 0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +204,19 @@ in
|
|||||||
};
|
};
|
||||||
Install.RequiredBy = [ "sway-session.target" ];
|
Install.RequiredBy = [ "sway-session.target" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
activate-linux = {
|
||||||
|
Unit = {
|
||||||
|
Description = "Linux activation watermark";
|
||||||
|
After = "graphical-session.target";
|
||||||
|
PartOf = "graphical-session.target";
|
||||||
|
};
|
||||||
|
Service = {
|
||||||
|
Type = "simple";
|
||||||
|
ExecStart = "${pkgs.activate-linux}/bin/activate-linux";
|
||||||
|
};
|
||||||
|
Install.RequiredBy = [ "graphical-session.target" ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,6 +226,7 @@ in
|
|||||||
wl-clipboard
|
wl-clipboard
|
||||||
wev
|
wev
|
||||||
wdisplays
|
wdisplays
|
||||||
|
swaysome
|
||||||
|
|
||||||
pavucontrol
|
pavucontrol
|
||||||
libsecret
|
libsecret
|
||||||
@@ -126,10 +236,11 @@ in
|
|||||||
];
|
];
|
||||||
|
|
||||||
pointerCursor = {
|
pointerCursor = {
|
||||||
package = pkgs.vanilla-dmz;
|
package = pkgs.posy-cursors;
|
||||||
name = "Vanilla-DMZ";
|
name = "Posy_Cursor";
|
||||||
size = 16;
|
size = 32;
|
||||||
gtk.enable = true;
|
gtk.enable = true;
|
||||||
|
x11.enable = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,9 +249,36 @@ in
|
|||||||
xsession.preferStatusNotifierItems = true;
|
xsession.preferStatusNotifierItems = true;
|
||||||
wayland = {
|
wayland = {
|
||||||
windowManager = {
|
windowManager = {
|
||||||
sway = {
|
sway =
|
||||||
|
let
|
||||||
|
cfg = config.wayland.windowManager.sway.config;
|
||||||
|
mod = cfg.modifier;
|
||||||
|
|
||||||
|
renameWs = pkgs.writeShellScript "sway-rename-ws" ''
|
||||||
|
focused_ws="$(swaymsg -t get_workspaces | jq ".[] | select(.focused)")"
|
||||||
|
focused_num="$(jq -r ".num" <<< "$focused_ws")"
|
||||||
|
focused_name="$(jq -r ".name" <<< "$focused_ws")"
|
||||||
|
placeholder="$(sed -E 's/[0-9]+: //' <<< "$focused_name")"
|
||||||
|
|
||||||
|
name="$(rofi -dmenu -p "rename ws $focused_num" -theme+entry+placeholder "\"$placeholder\"")"
|
||||||
|
if [ -n "$name" ]; then
|
||||||
|
swaymsg rename workspace "$focused_name" to "$focused_num: $name"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
clearWsName = pkgs.writeShellScript "sway-clear-ws-name" ''
|
||||||
|
focused_ws="$(swaymsg -t get_workspaces | jq ".[] | select(.focused)")"
|
||||||
|
focused_num="$(jq -r ".num" <<< "$focused_ws")"
|
||||||
|
focused_name="$(jq -r ".name" <<< "$focused_ws")"
|
||||||
|
|
||||||
|
swaymsg rename workspace "$focused_name" to "$focused_num"
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
enable = true;
|
enable = true;
|
||||||
xwayland = true;
|
xwayland = true;
|
||||||
|
extraConfigEarly = ''
|
||||||
|
set $mod ${mod}
|
||||||
|
'';
|
||||||
config = {
|
config = {
|
||||||
input = {
|
input = {
|
||||||
"type:touchpad" = {
|
"type:touchpad" = {
|
||||||
@@ -155,23 +293,87 @@ in
|
|||||||
|
|
||||||
modifier = "Mod4";
|
modifier = "Mod4";
|
||||||
terminal = "kitty";
|
terminal = "kitty";
|
||||||
keybindings =
|
keybindings = mapAttrs (k: mkOptionDefault) {
|
||||||
let
|
"${mod}+Left" = "focus left";
|
||||||
cfg = config.wayland.windowManager.sway.config;
|
"${mod}+Down" = "focus down";
|
||||||
mod = cfg.modifier;
|
"${mod}+Up" = "focus up";
|
||||||
in
|
"${mod}+Right" = "focus right";
|
||||||
lib.mkOptionDefault {
|
|
||||||
|
"${mod}+Shift+Left" = "move left";
|
||||||
|
"${mod}+Shift+Down" = "move down";
|
||||||
|
"${mod}+Shift+Up" = "move up";
|
||||||
|
"${mod}+Shift+Right" = "move right";
|
||||||
|
|
||||||
|
"${mod}+b" = "splith";
|
||||||
|
"${mod}+v" = "splitv";
|
||||||
|
"${mod}+f" = "fullscreen toggle";
|
||||||
|
"${mod}+a" = "focus parent";
|
||||||
|
|
||||||
|
"${mod}+s" = "layout stacking";
|
||||||
|
"${mod}+w" = "layout tabbed";
|
||||||
|
"${mod}+e" = "layout toggle split";
|
||||||
|
|
||||||
|
"${mod}+Shift+space" = "floating toggle";
|
||||||
|
"${mod}+space" = "focus mode_toggle";
|
||||||
|
|
||||||
|
"${mod}+1" = "workspace number 1";
|
||||||
|
"${mod}+2" = "workspace number 2";
|
||||||
|
"${mod}+3" = "workspace number 3";
|
||||||
|
"${mod}+4" = "workspace number 4";
|
||||||
|
"${mod}+5" = "workspace number 5";
|
||||||
|
"${mod}+6" = "workspace number 6";
|
||||||
|
"${mod}+7" = "workspace number 7";
|
||||||
|
"${mod}+8" = "workspace number 8";
|
||||||
|
"${mod}+9" = "workspace number 9";
|
||||||
|
"${mod}+0" = "workspace number 10";
|
||||||
|
|
||||||
|
"${mod}+Shift+1" =
|
||||||
|
"move container to workspace number 1";
|
||||||
|
"${mod}+Shift+2" =
|
||||||
|
"move container to workspace number 2";
|
||||||
|
"${mod}+Shift+3" =
|
||||||
|
"move container to workspace number 3";
|
||||||
|
"${mod}+Shift+4" =
|
||||||
|
"move container to workspace number 4";
|
||||||
|
"${mod}+Shift+5" =
|
||||||
|
"move container to workspace number 5";
|
||||||
|
"${mod}+Shift+6" =
|
||||||
|
"move container to workspace number 6";
|
||||||
|
"${mod}+Shift+7" =
|
||||||
|
"move container to workspace number 7";
|
||||||
|
"${mod}+Shift+8" =
|
||||||
|
"move container to workspace number 8";
|
||||||
|
"${mod}+Shift+9" =
|
||||||
|
"move container to workspace number 9";
|
||||||
|
"${mod}+Shift+0" =
|
||||||
|
"move container to workspace number 10";
|
||||||
|
|
||||||
|
"${mod}+Shift+minus" = "move scratchpad";
|
||||||
|
"${mod}+minus" = "scratchpad show";
|
||||||
|
|
||||||
|
"${mod}+Return" = "exec ${cfg.terminal}";
|
||||||
|
"${mod}+r" = "mode resize";
|
||||||
"${mod}+d" = null;
|
"${mod}+d" = null;
|
||||||
"${mod}+l" = "exec swaylock -i ${./lock.png} -s stretch";
|
"${mod}+l" = "exec ${doomsaver}/bin/doomsaver";
|
||||||
|
"${mod}+q" = "kill";
|
||||||
|
"${mod}+Shift+c" = "reload";
|
||||||
|
"${mod}+Shift+q" = "exec swaynag -t warning -m 'bruh you really wanna kill sway?' -b 'ye' 'systemctl --user stop graphical-session.target && swaymsg exit'";
|
||||||
|
|
||||||
|
# rofi
|
||||||
"${mod}+x" = "exec ${cfg.menu}";
|
"${mod}+x" = "exec ${cfg.menu}";
|
||||||
"${mod}+Shift+x" = "exec rofi -show drun";
|
"${mod}+Shift+x" = "exec rofi -show drun";
|
||||||
"${mod}+q" = "kill";
|
|
||||||
"${mod}+Shift+q" = "exec swaynag -t warning -m 'bruh you really wanna kill sway?' -b 'ye' 'systemctl --user stop graphical-session.target && swaymsg exit'";
|
|
||||||
"${mod}+Shift+d" = ''exec grim - | swappy -f -'';
|
|
||||||
"${mod}+Shift+s" = ''exec grim -g "$(slurp)" - | swappy -f -'';
|
|
||||||
"${mod}+Shift+e" = "exec rofi -show emoji";
|
"${mod}+Shift+e" = "exec rofi -show emoji";
|
||||||
# Config for this doesn't seem to work :/
|
# Config for this doesn't seem to work :/
|
||||||
"${mod}+c" = ''exec rofi -show calc -calc-command "echo -n '{result}' | ${pkgs.wl-clipboard}/bin/wl-copy"'';
|
"${mod}+c" = ''exec rofi -show calc -calc-command "echo -n '{result}' | ${pkgs.wl-clipboard}/bin/wl-copy"'';
|
||||||
|
"${mod}+Shift+r" = "exec ${renameWs}";
|
||||||
|
"${mod}+Shift+n" = "exec ${clearWsName}";
|
||||||
|
|
||||||
|
# Screenshots
|
||||||
|
"${mod}+Shift+d" = ''exec grim - | swappy -f -'';
|
||||||
|
"${mod}+Shift+s" = ''exec grim -g "$(slurp)" - | swappy -f -'';
|
||||||
|
|
||||||
|
"XF86MonBrightnessDown" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%-";
|
||||||
|
"XF86MonBrightnessUp" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set +5%";
|
||||||
|
|
||||||
"XF86AudioRaiseVolume" = "exec ${pkgs.pamixer}/bin/pamixer -i 5";
|
"XF86AudioRaiseVolume" = "exec ${pkgs.pamixer}/bin/pamixer -i 5";
|
||||||
"XF86AudioLowerVolume" = "exec ${pkgs.pamixer}/bin/pamixer -d 5";
|
"XF86AudioLowerVolume" = "exec ${pkgs.pamixer}/bin/pamixer -d 5";
|
||||||
@@ -188,6 +390,9 @@ in
|
|||||||
menu = "rofi -show run";
|
menu = "rofi -show run";
|
||||||
bars = mkForce [ ];
|
bars = mkForce [ ];
|
||||||
};
|
};
|
||||||
|
extraConfig = ''
|
||||||
|
include ${./swaysome.conf}
|
||||||
|
'';
|
||||||
|
|
||||||
swaynag = {
|
swaynag = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -202,6 +407,10 @@ in
|
|||||||
name = "Numix";
|
name = "Numix";
|
||||||
package = pkgs.numix-gtk-theme;
|
package = pkgs.numix-gtk-theme;
|
||||||
};
|
};
|
||||||
|
gtk4.theme = {
|
||||||
|
name = "Numix";
|
||||||
|
package = pkgs.numix-gtk-theme;
|
||||||
|
};
|
||||||
iconTheme = {
|
iconTheme = {
|
||||||
name = "Numix";
|
name = "Numix";
|
||||||
package = pkgs.numix-icon-theme;
|
package = pkgs.numix-icon-theme;
|
||||||
@@ -210,17 +419,10 @@ in
|
|||||||
};
|
};
|
||||||
qt = {
|
qt = {
|
||||||
enable = true;
|
enable = true;
|
||||||
platformTheme = "gtk";
|
platformTheme.name = "gtk";
|
||||||
};
|
};
|
||||||
|
|
||||||
services = {
|
services = {
|
||||||
swaync = {
|
|
||||||
enable = true;
|
|
||||||
settings = {
|
|
||||||
widgets = [ "title" "dnd" "mpris" "notifications" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
playerctld.enable = true;
|
playerctld.enable = true;
|
||||||
spotifyd = {
|
spotifyd = {
|
||||||
enable = false;
|
enable = false;
|
||||||
@@ -237,18 +439,45 @@ in
|
|||||||
device_type = "computer";
|
device_type = "computer";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
easyeffects = {
|
||||||
|
enable = mkDefault false;
|
||||||
|
preset = mkDefault "moar-bass";
|
||||||
|
extraPresets = {
|
||||||
|
moar-bass = {
|
||||||
|
output = {
|
||||||
|
"bass_enhancer#0" = {
|
||||||
|
amount = 3;
|
||||||
|
blend = 0;
|
||||||
|
bypass = false;
|
||||||
|
floor = 20;
|
||||||
|
floor-active = false;
|
||||||
|
harmonics = 8.5;
|
||||||
|
input-gain = 0;
|
||||||
|
output-gain = 0;
|
||||||
|
scope = 100;
|
||||||
|
};
|
||||||
|
blocklist = [ ];
|
||||||
|
plugins_order = [ "bass_enhancer#0" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
programs = {
|
programs = {
|
||||||
git = {
|
git = {
|
||||||
enable = true;
|
enable = true;
|
||||||
diff-so-fancy.enable = true;
|
settings = {
|
||||||
userEmail = "jackos1998@gmail.com";
|
user = {
|
||||||
userName = "Jack O'Sullivan";
|
email = "jackos1998@gmail.com";
|
||||||
extraConfig = {
|
name = "Jack O'Sullivan";
|
||||||
|
};
|
||||||
pull.rebase = true;
|
pull.rebase = true;
|
||||||
};
|
};
|
||||||
|
lfs.enable = true;
|
||||||
};
|
};
|
||||||
|
diff-so-fancy.enable = true;
|
||||||
|
|
||||||
waybar = import ./waybar.nix { inherit lib pkgs config font; };
|
waybar = import ./waybar.nix { inherit lib pkgs config font; };
|
||||||
rofi = {
|
rofi = {
|
||||||
@@ -271,7 +500,7 @@ in
|
|||||||
|
|
||||||
chromium = {
|
chromium = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = (pkgs.chromium.override { enableWideVine = true; }).overrideAttrs (old: {
|
package = (pkgs'.unstable.chromium.override { enableWideVine = true; }).overrideAttrs (old: {
|
||||||
buildCommand = ''
|
buildCommand = ''
|
||||||
${old.buildCommand}
|
${old.buildCommand}
|
||||||
|
|
||||||
@@ -297,6 +526,15 @@ in
|
|||||||
] (_: "chromium-browser.desktop");
|
] (_: "chromium-browser.desktop");
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
my = {
|
||||||
|
swaync = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
widgets = [ "title" "dnd" "mpris" "notifications" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
(mkIf (cfg.standalone && !pkgs.stdenv.isDarwin) {
|
(mkIf (cfg.standalone && !pkgs.stdenv.isDarwin) {
|
||||||
@@ -304,6 +542,7 @@ in
|
|||||||
userDirs = {
|
userDirs = {
|
||||||
enable = true;
|
enable = true;
|
||||||
createDirectories = true;
|
createDirectories = true;
|
||||||
|
setSessionVariables = true;
|
||||||
desktop = "$HOME/desktop";
|
desktop = "$HOME/desktop";
|
||||||
documents = "$HOME/documents";
|
documents = "$HOME/documents";
|
||||||
download = "$HOME/downloads";
|
download = "$HOME/downloads";
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
Executable
+257
@@ -0,0 +1,257 @@
|
|||||||
|
#!@python@/bin/python
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import filelock
|
||||||
|
|
||||||
|
class Screensaver:
|
||||||
|
def __init__(self, cmd, env=None, weight=1):
|
||||||
|
self.cmd = cmd
|
||||||
|
self.weight = weight
|
||||||
|
|
||||||
|
if env is not None:
|
||||||
|
self.env = os.environ.copy()
|
||||||
|
for k, v in env.items():
|
||||||
|
self.env[k] = v
|
||||||
|
else:
|
||||||
|
self.env = None
|
||||||
|
self.proc = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
assert self.proc is None
|
||||||
|
self.proc = subprocess.Popen(self.cmd, env=self.env)
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
assert self.proc is not None
|
||||||
|
self.proc.wait()
|
||||||
|
|
||||||
|
def stop(self, kill=False):
|
||||||
|
assert self.proc is not None
|
||||||
|
if kill:
|
||||||
|
self.proc.kill()
|
||||||
|
else:
|
||||||
|
self.proc.terminate()
|
||||||
|
|
||||||
|
class DoomSaver(Screensaver):
|
||||||
|
wad = '@doomWad@'
|
||||||
|
|
||||||
|
def __init__(self, demo_index, weight=1.5):
|
||||||
|
super().__init__(
|
||||||
|
['@chocoDoom@/bin/chocolate-doom',
|
||||||
|
'-iwad', self.wad,
|
||||||
|
'-demoloopi', str(demo_index)],
|
||||||
|
env={
|
||||||
|
'SDL_AUDIODRIVER': 'null',
|
||||||
|
'SDL_VIDEODRIVER': 'caca',
|
||||||
|
'CACA_DRIVER': 'ncurses',
|
||||||
|
},
|
||||||
|
weight=weight,
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
super().stop(kill=True)
|
||||||
|
|
||||||
|
class TTESaver(Screensaver):
|
||||||
|
effects = (
|
||||||
|
'beams,binarypath,blackhole,bouncyballs,bubbles,burn,colorshift,crumble,'
|
||||||
|
'decrypt,errorcorrect,expand,fireworks,middleout,orbittingvolley,overflow,'
|
||||||
|
'pour,print,rain,randomsequence,rings,scattered,slice,slide,spotlights,'
|
||||||
|
'spray,swarm,synthgrid,unstable,vhstape,waves,wipe'
|
||||||
|
).split(',')
|
||||||
|
|
||||||
|
def __init__(self, cmd, env=None, weight=1):
|
||||||
|
super().__init__(cmd, env=env, weight=weight)
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
while self.running:
|
||||||
|
effect_cmd = ['@terminaltexteffects@/bin/tte', random.choice(self.effects)]
|
||||||
|
print(f"$ {self.cmd} | {' '.join(effect_cmd)}")
|
||||||
|
content = subprocess.check_output(self.cmd, shell=True, env=self.env, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
self.proc = subprocess.Popen(effect_cmd, stdin=subprocess.PIPE)
|
||||||
|
self.proc.stdin.write(content)
|
||||||
|
self.proc.stdin.close()
|
||||||
|
self.proc.wait()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
self.proc.terminate()
|
||||||
|
|
||||||
|
class FFmpegCACASaver(Screensaver):
|
||||||
|
@staticmethod
|
||||||
|
def command(video, size):
|
||||||
|
return ['@ffmpeg@/bin/ffmpeg', '-hide_banner', '-loglevel', 'error',
|
||||||
|
'-stream_loop', '-1', '-i', video,
|
||||||
|
'-pix_fmt', 'rgb24', '-window_size', f'{size}x{size}',
|
||||||
|
'-f', 'caca', '-']
|
||||||
|
|
||||||
|
def __init__(self, video, weight=2):
|
||||||
|
cols, lines = os.get_terminal_size()
|
||||||
|
# IDK if it's reasonable to do this as "1:1"
|
||||||
|
size = lines - 4
|
||||||
|
super().__init__(
|
||||||
|
self.command(video, size),
|
||||||
|
env={'CACA_DRIVER': 'ncurses'},
|
||||||
|
weight=weight,
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
super().stop(kill=True)
|
||||||
|
|
||||||
|
class BrainrotStorySaver(Screensaver):
|
||||||
|
def __init__(self, video, text_command, weight=2):
|
||||||
|
cols, lines = os.get_terminal_size()
|
||||||
|
video_size = lines - 1
|
||||||
|
video_command = ' '.join(FFmpegCACASaver.command(video, video_size))
|
||||||
|
text_command = (
|
||||||
|
f'while true; do {text_command} | '
|
||||||
|
f'@terminaltexteffects@/bin/tte --wrap-text --canvas-width=80 --canvas-height={video_size//2} --anchor-canvas=c '
|
||||||
|
'print --final-gradient-stops=ffffff; clear; done' )
|
||||||
|
self.tmux_session = f'screensaver-{os.urandom(4).hex()}'
|
||||||
|
super().__init__(
|
||||||
|
['@tmux@/bin/tmux', 'new-session', '-s', self.tmux_session, '-n', 'brainrot',
|
||||||
|
text_command, ';', 'split-window', '-hbl', str(lines), video_command],
|
||||||
|
# ['sh', '-c', text_command],
|
||||||
|
env={
|
||||||
|
'CACA_DRIVER': 'ncurses',
|
||||||
|
'SHELL': '/bin/sh',
|
||||||
|
},
|
||||||
|
weight=weight,
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
subprocess.check_call(['@tmux@/bin/tmux', 'kill-session', '-t', self.tmux_session])
|
||||||
|
|
||||||
|
class MultiSaver:
|
||||||
|
savers = [
|
||||||
|
DoomSaver(0),
|
||||||
|
DoomSaver(1),
|
||||||
|
DoomSaver(2),
|
||||||
|
|
||||||
|
Screensaver(['cmatrix']),
|
||||||
|
|
||||||
|
TTESaver('screenfetch -N'),
|
||||||
|
TTESaver('fortune | cowsay'),
|
||||||
|
TTESaver('top -bn1 | head -n50'),
|
||||||
|
TTESaver('ss -nltu'),
|
||||||
|
TTESaver('ss -ntu'),
|
||||||
|
TTESaver('jp2a --width=100 @enojy@'),
|
||||||
|
|
||||||
|
BrainrotStorySaver('@subwaySurfers@', '@brainrotTextCommand@'),
|
||||||
|
BrainrotStorySaver('@minecraftParkour@', '@brainrotTextCommand@'),
|
||||||
|
]
|
||||||
|
state_filename = 'screensaver.json'
|
||||||
|
|
||||||
|
def __init__(self, select=None):
|
||||||
|
self.state_path = os.path.join(f'/run/user/{os.geteuid()}', self.state_filename)
|
||||||
|
self.lock = filelock.FileLock(f'{self.state_path}.lock')
|
||||||
|
|
||||||
|
if select is not None:
|
||||||
|
assert select >= 0 and select < len(self.savers), 'Invalid screensaver index'
|
||||||
|
self.selected = self.savers[select]
|
||||||
|
else:
|
||||||
|
self.selected = None
|
||||||
|
self.cleaned_up = False
|
||||||
|
|
||||||
|
def select(self):
|
||||||
|
with self.lock:
|
||||||
|
if not os.path.exists(self.state_path):
|
||||||
|
state = {'instances': []}
|
||||||
|
else:
|
||||||
|
with open(self.state_path) as f:
|
||||||
|
state = json.load(f)
|
||||||
|
|
||||||
|
if self.selected is None:
|
||||||
|
available = set(range(len(self.savers)))
|
||||||
|
new_instances = []
|
||||||
|
for instance in state['instances']:
|
||||||
|
if not os.path.exists(f"/proc/{instance['pid']}"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_instances.append(instance)
|
||||||
|
i = instance['saver']
|
||||||
|
assert i in available
|
||||||
|
available.remove(i)
|
||||||
|
assert available, 'No screensavers left'
|
||||||
|
available = list(available)
|
||||||
|
|
||||||
|
weights = []
|
||||||
|
for i in available:
|
||||||
|
weights.append(self.savers[i].weight)
|
||||||
|
selected_i = random.choices(available, weights=weights)[0]
|
||||||
|
|
||||||
|
new_instances.append({'pid': os.getpid(), 'saver': selected_i})
|
||||||
|
state['instances'] = new_instances
|
||||||
|
|
||||||
|
# print(f'Selected saver {selected_i}')
|
||||||
|
self.selected = self.savers[selected_i]
|
||||||
|
|
||||||
|
with open(self.state_path, 'w') as f:
|
||||||
|
json.dump(state, f)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
if self.cleaned_up:
|
||||||
|
return
|
||||||
|
self.cleaned_up = True
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
with open(self.state_path) as f:
|
||||||
|
state = json.load(f)
|
||||||
|
|
||||||
|
for i, instance in enumerate(state['instances']):
|
||||||
|
if instance['pid'] == os.getpid():
|
||||||
|
del state['instances'][i]
|
||||||
|
|
||||||
|
with open(self.state_path, 'w') as f:
|
||||||
|
json.dump(state, f)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
assert self.selected is not None
|
||||||
|
self.selected.start()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, self._sighandler)
|
||||||
|
signal.signal(signal.SIGTERM, self._sighandler)
|
||||||
|
signal.signal(signal.SIGHUP, self._sighandler)
|
||||||
|
self.selected.wait()
|
||||||
|
self.cleanup()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
assert self.selected is not None
|
||||||
|
print('Shutting down')
|
||||||
|
self.selected.stop()
|
||||||
|
self.cleanup()
|
||||||
|
def _sighandler(self, signum, frame):
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='Wayland terminal-based lock screen')
|
||||||
|
parser.add_argument('-l', '--locker-cmd', default='swaylock-plugin', help='swaylock-plugin command to use')
|
||||||
|
parser.add_argument('-t', '--terminal', default='alacritty', help='Terminal emulator to use')
|
||||||
|
parser.add_argument('-i', '--instance', action='store_true', help='Run as instance')
|
||||||
|
parser.add_argument('-s', '--screensaver', type=int, help='Force use of specific screensaver')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not args.instance:
|
||||||
|
cmd = [
|
||||||
|
args.locker_cmd, '--command-each',
|
||||||
|
f'@windowtolayer@/bin/windowtolayer -- {args.terminal} -e {sys.argv[0]} --instance']
|
||||||
|
if args.screensaver is not None:
|
||||||
|
cmd[-1] += f' --screensaver {args.screensaver}'
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
return
|
||||||
|
|
||||||
|
ms = MultiSaver(select=args.screensaver)
|
||||||
|
ms.select()
|
||||||
|
ms.run()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 251 KiB |
@@ -0,0 +1,66 @@
|
|||||||
|
# Use (un)bindcode or (un)bindsym, depending on what you used in your main sway config file.
|
||||||
|
# The `--no-warn` setting is only added to shortcuts that exist in the default config. You may want to add or remove
|
||||||
|
# that flag on some bindings depending on your config.
|
||||||
|
|
||||||
|
|
||||||
|
# Change focus between workspaces
|
||||||
|
bindsym $mod+Alt+1 exec "swaysome focus 1"
|
||||||
|
bindsym $mod+Alt+2 exec "swaysome focus 2"
|
||||||
|
bindsym $mod+Alt+3 exec "swaysome focus 3"
|
||||||
|
bindsym $mod+Alt+4 exec "swaysome focus 4"
|
||||||
|
bindsym $mod+Alt+5 exec "swaysome focus 5"
|
||||||
|
bindsym $mod+Alt+6 exec "swaysome focus 6"
|
||||||
|
bindsym $mod+Alt+7 exec "swaysome focus 7"
|
||||||
|
bindsym $mod+Alt+8 exec "swaysome focus 8"
|
||||||
|
bindsym $mod+Alt+9 exec "swaysome focus 9"
|
||||||
|
bindsym $mod+Alt+0 exec "swaysome focus 0"
|
||||||
|
|
||||||
|
# Focus workspace groups
|
||||||
|
bindsym --no-warn $mod+1 exec "swaysome focus-group 1"
|
||||||
|
bindsym --no-warn $mod+2 exec "swaysome focus-group 2"
|
||||||
|
bindsym --no-warn $mod+3 exec "swaysome focus-group 3"
|
||||||
|
bindsym --no-warn $mod+4 exec "swaysome focus-group 4"
|
||||||
|
bindsym --no-warn $mod+5 exec "swaysome focus-group 5"
|
||||||
|
bindsym --no-warn $mod+6 exec "swaysome focus-group 6"
|
||||||
|
bindsym --no-warn $mod+7 exec "swaysome focus-group 7"
|
||||||
|
bindsym --no-warn $mod+8 exec "swaysome focus-group 8"
|
||||||
|
bindsym --no-warn $mod+9 exec "swaysome focus-group 9"
|
||||||
|
bindsym --no-warn $mod+0 exec "swaysome focus-group 0"
|
||||||
|
|
||||||
|
# Move containers between workspaces
|
||||||
|
bindsym $mod+Alt+Shift+1 exec "swaysome move 1"
|
||||||
|
bindsym $mod+Alt+Shift+2 exec "swaysome move 2"
|
||||||
|
bindsym $mod+Alt+Shift+3 exec "swaysome move 3"
|
||||||
|
bindsym $mod+Alt+Shift+4 exec "swaysome move 4"
|
||||||
|
bindsym $mod+Alt+Shift+5 exec "swaysome move 5"
|
||||||
|
bindsym $mod+Alt+Shift+6 exec "swaysome move 6"
|
||||||
|
bindsym $mod+Alt+Shift+7 exec "swaysome move 7"
|
||||||
|
bindsym $mod+Alt+Shift+8 exec "swaysome move 8"
|
||||||
|
bindsym $mod+Alt+Shift+9 exec "swaysome move 9"
|
||||||
|
bindsym $mod+Alt+Shift+0 exec "swaysome move 0"
|
||||||
|
|
||||||
|
# Move containers to other workspace groups
|
||||||
|
bindsym --no-warn $mod+Shift+1 exec "swaysome move-to-group 1"
|
||||||
|
bindsym --no-warn $mod+Shift+2 exec "swaysome move-to-group 2"
|
||||||
|
bindsym --no-warn $mod+Shift+3 exec "swaysome move-to-group 3"
|
||||||
|
bindsym --no-warn $mod+Shift+4 exec "swaysome move-to-group 4"
|
||||||
|
bindsym --no-warn $mod+Shift+5 exec "swaysome move-to-group 5"
|
||||||
|
bindsym --no-warn $mod+Shift+6 exec "swaysome move-to-group 6"
|
||||||
|
bindsym --no-warn $mod+Shift+7 exec "swaysome move-to-group 7"
|
||||||
|
bindsym --no-warn $mod+Shift+8 exec "swaysome move-to-group 8"
|
||||||
|
bindsym --no-warn $mod+Shift+9 exec "swaysome move-to-group 9"
|
||||||
|
bindsym --no-warn $mod+Shift+0 exec "swaysome move-to-group 0"
|
||||||
|
|
||||||
|
# Move focused container to next output
|
||||||
|
bindsym $mod+Alt+Right exec "swaysome next-output"
|
||||||
|
# Move focused container to previous output
|
||||||
|
bindsym $mod+Alt+Left exec "swaysome prev-output"
|
||||||
|
|
||||||
|
# Move focused workspace group to next output
|
||||||
|
bindsym $mod+Shift+Alt+Right exec "swaysome workspace-group-next-output"
|
||||||
|
# Move focused workspace group to previous output
|
||||||
|
bindsym $mod+Shift+Alt+Left exec "swaysome workspace-group-prev-output"
|
||||||
|
|
||||||
|
# Init workspaces for every screen
|
||||||
|
exec "swaysome init 1"
|
||||||
|
|
||||||
@@ -146,9 +146,9 @@ in
|
|||||||
dnd-none = "";
|
dnd-none = "";
|
||||||
};
|
};
|
||||||
return-type = "json";
|
return-type = "json";
|
||||||
exec = "${config.services.swaync.package}/bin/swaync-client -swb";
|
exec = "${config.my.swaync.package}/bin/swaync-client -swb";
|
||||||
on-click = "${config.services.swaync.package}/bin/swaync-client -t -sw";
|
on-click = "${config.my.swaync.package}/bin/swaync-client -t -sw";
|
||||||
on-click-right = "${config.services.swaync.package}/bin/swaync-client -d -sw";
|
on-click-right = "${config.my.swaync.package}/bin/swaync-client -d -sw";
|
||||||
escape = true;
|
escape = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ let
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
cfg = config.services.swaync;
|
cfg = config.my.swaync;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.swaync = with lib.types; {
|
options.my.swaync = with lib.types; {
|
||||||
enable = mkEnableOption "Sway Notification Center";
|
enable = mkEnableOption "Sway Notification Center";
|
||||||
package = mkOption {
|
package = mkOption {
|
||||||
type = package;
|
type = package;
|
||||||
|
|||||||
+141
-9
@@ -11,6 +11,9 @@ rec {
|
|||||||
jellyseerr = 402;
|
jellyseerr = 402;
|
||||||
atticd = 403;
|
atticd = 403;
|
||||||
kea = 404;
|
kea = 404;
|
||||||
|
keepalived_script = 405;
|
||||||
|
photoprism = 406;
|
||||||
|
copyparty = 408;
|
||||||
};
|
};
|
||||||
gids = {
|
gids = {
|
||||||
matrix-syncv3 = 400;
|
matrix-syncv3 = 400;
|
||||||
@@ -18,12 +21,16 @@ rec {
|
|||||||
jellyseerr = 402;
|
jellyseerr = 402;
|
||||||
atticd = 403;
|
atticd = 403;
|
||||||
kea = 404;
|
kea = 404;
|
||||||
|
keepalived_script = 405;
|
||||||
|
photoprism = 406;
|
||||||
|
adbusers = 407;
|
||||||
|
copyparty = 408;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
kernel = {
|
kernel = {
|
||||||
lts = pkgs: pkgs.linuxKernel.packages.linux_6_1;
|
lts = pkgs: pkgs.linuxKernel.packages.linux_6_18;
|
||||||
latest = pkgs: pkgs.linuxKernel.packages.linux_6_6;
|
latest = pkgs: pkgs.linuxKernel.packages.linux_7_0;
|
||||||
};
|
};
|
||||||
|
|
||||||
nginx = rec {
|
nginx = rec {
|
||||||
@@ -94,10 +101,10 @@ rec {
|
|||||||
nix = {
|
nix = {
|
||||||
cache = rec {
|
cache = rec {
|
||||||
substituters = [
|
substituters = [
|
||||||
"https://nix-cache.${pubDomain}/main"
|
"https://nix-cache.${pubDomain}"
|
||||||
];
|
];
|
||||||
keys = [
|
keys = [
|
||||||
"main:mMChkG8LwXrFirVfudqjSHasK1jV31OVElYD3eImYl8="
|
"nix-cache.nul.ie-1:BzH5yMfF4HbzY1C977XzOxoPhEc9Zbu39ftPkUbH+m4="
|
||||||
];
|
];
|
||||||
conf = ''
|
conf = ''
|
||||||
extra-substituters = ${concatStringsSep " " substituters}
|
extra-substituters = ${concatStringsSep " " substituters}
|
||||||
@@ -107,7 +114,7 @@ rec {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pubDomain = "nul.ie";
|
pubDomain = "nul.ie";
|
||||||
colony = {
|
colony = rec {
|
||||||
domain = "ams1.int.${pubDomain}";
|
domain = "ams1.int.${pubDomain}";
|
||||||
pubV4 = "94.142.240.44";
|
pubV4 = "94.142.240.44";
|
||||||
prefixes = with lib.my.net.cidr; rec {
|
prefixes = with lib.my.net.cidr; rec {
|
||||||
@@ -131,6 +138,19 @@ rec {
|
|||||||
v4 = subnet 8 3 all.v4;
|
v4 = subnet 8 3 all.v4;
|
||||||
v6 = subnet 4 3 all.v6;
|
v6 = subnet 4 3 all.v6;
|
||||||
};
|
};
|
||||||
|
qclk = {
|
||||||
|
v4 = subnet 8 4 all.v4;
|
||||||
|
};
|
||||||
|
|
||||||
|
p2pTunnels = {
|
||||||
|
v4 = subnet 8 5 all.v4;
|
||||||
|
};
|
||||||
|
hillcrest = {
|
||||||
|
v4 = subnet 6 0 p2pTunnels.v4;
|
||||||
|
};
|
||||||
|
john-valorant = {
|
||||||
|
v4 = subnet 6 1 p2pTunnels.v4;
|
||||||
|
};
|
||||||
|
|
||||||
cust = {
|
cust = {
|
||||||
v4 = subnet 8 100 all.v4; # single ip for routing only
|
v4 = subnet 8 100 all.v4; # single ip for routing only
|
||||||
@@ -144,6 +164,10 @@ rec {
|
|||||||
v4 = "94.142.242.255/32";
|
v4 = "94.142.242.255/32";
|
||||||
v6 = subnet 8 1 cust.v6;
|
v6 = subnet 8 1 cust.v6;
|
||||||
};
|
};
|
||||||
|
jam = {
|
||||||
|
v4 = subnet 8 4 cust.v4;
|
||||||
|
v6 = subnet 8 2 cust.v6;
|
||||||
|
};
|
||||||
|
|
||||||
vip1 = "94.142.241.224/30";
|
vip1 = "94.142.241.224/30";
|
||||||
vip2 = "94.142.242.254/31";
|
vip2 = "94.142.242.254/31";
|
||||||
@@ -156,6 +180,16 @@ rec {
|
|||||||
home.v6 = "2a0e:97c0:4d0::/48";
|
home.v6 = "2a0e:97c0:4d0::/48";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
custRouting = with lib.my.net.cidr; {
|
||||||
|
mail-vm = host 1 prefixes.cust.v4;
|
||||||
|
darts-vm = host 2 prefixes.cust.v4;
|
||||||
|
jam-ctr = host 3 prefixes.cust.v4;
|
||||||
|
};
|
||||||
|
|
||||||
|
qclk = {
|
||||||
|
wgPort = 51821;
|
||||||
|
};
|
||||||
|
|
||||||
firewallForwards = aa: [
|
firewallForwards = aa: [
|
||||||
{
|
{
|
||||||
port = "http";
|
port = "http";
|
||||||
@@ -169,6 +203,7 @@ rec {
|
|||||||
port = 8448;
|
port = 8448;
|
||||||
dst = aa.middleman.internal.ipv4.address;
|
dst = aa.middleman.internal.ipv4.address;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
port = 25565;
|
port = 25565;
|
||||||
dst = aa.simpcraft-oci.internal.ipv4.address;
|
dst = aa.simpcraft-oci.internal.ipv4.address;
|
||||||
@@ -178,8 +213,27 @@ rec {
|
|||||||
dst = aa.simpcraft-staging-oci.internal.ipv4.address;
|
dst = aa.simpcraft-staging-oci.internal.ipv4.address;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
port = 25575;
|
port = 25567;
|
||||||
dst = aa.simpcraft-oci.internal.ipv4.address;
|
dst = aa.kevcraft-oci.internal.ipv4.address;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
port = 25568;
|
||||||
|
dst = aa.kinkcraft-oci.internal.ipv4.address;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
port = 25569;
|
||||||
|
dst = aa.graeme-oci.internal.ipv4.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
# RCON... unsafe?
|
||||||
|
# {
|
||||||
|
# port = 25575;
|
||||||
|
# dst = aa.simpcraft-oci.internal.ipv4.address;
|
||||||
|
# }
|
||||||
|
|
||||||
|
{
|
||||||
|
port = 7777;
|
||||||
|
dst = aa.gam.internal.ipv4.address;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -204,6 +258,44 @@ rec {
|
|||||||
dst = aa.simpcraft-oci.internal.ipv4.address;
|
dst = aa.simpcraft-oci.internal.ipv4.address;
|
||||||
proto = "udp";
|
proto = "udp";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
port = 25567;
|
||||||
|
dst = aa.kevcraft-oci.internal.ipv4.address;
|
||||||
|
proto = "udp";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
port = 25568;
|
||||||
|
dst = aa.kinkcraft-oci.internal.ipv4.address;
|
||||||
|
proto = "udp";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
port = 25569;
|
||||||
|
dst = aa.graeme-oci.internal.ipv4.address;
|
||||||
|
proto = "udp";
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
port = 15636;
|
||||||
|
dst = aa.enshrouded-oci.internal.ipv4.address;
|
||||||
|
proto = "udp";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
port = 15637;
|
||||||
|
dst = aa.enshrouded-oci.internal.ipv4.address;
|
||||||
|
proto = "udp";
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
port = qclk.wgPort;
|
||||||
|
dst = aa.qclk.internal.ipv4.address;
|
||||||
|
proto = "udp";
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
port = 7777;
|
||||||
|
dst = aa.gam.internal.ipv4.address;
|
||||||
|
proto = "udp";
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
fstrimConfig = {
|
fstrimConfig = {
|
||||||
@@ -220,6 +312,16 @@ rec {
|
|||||||
lo = 110;
|
lo = 110;
|
||||||
untrusted = 120;
|
untrusted = 120;
|
||||||
wan = 130;
|
wan = 130;
|
||||||
|
|
||||||
|
# Digiweb delivers the ISP VLAN (pon-isp, 10) single-tagged at the ONT alongside the ONT's
|
||||||
|
# own untagged management traffic. With a single ONT we trunk pon-isp (10) straight through
|
||||||
|
# the switches to river (PPPoE runs directly on it), and the switch at the ONT edge PVIDs the
|
||||||
|
# untagged management port onto wan-pon-ont (140). wan-pon-isp (141) is reserved for the
|
||||||
|
# future multi-ONT case, where per-port VLAN translation on a dedicated switch swaps each
|
||||||
|
# ONT's VLAN 10 to a distinct fabric VLAN (see docs/sites/home/switches.md).
|
||||||
|
pon-isp = 10;
|
||||||
|
wan-pon-ont = 140;
|
||||||
|
wan-pon-isp = 141;
|
||||||
};
|
};
|
||||||
hiMTU = 9000;
|
hiMTU = 9000;
|
||||||
routers = [
|
routers = [
|
||||||
@@ -227,14 +329,17 @@ rec {
|
|||||||
"stream"
|
"stream"
|
||||||
];
|
];
|
||||||
routersPubV4 = [
|
routersPubV4 = [
|
||||||
"109.255.1.246"
|
"84.203.124.128" # river: Digiweb static
|
||||||
"109.255.252.63"
|
"109.255.108.121"
|
||||||
];
|
];
|
||||||
|
|
||||||
prefixes = with lib.my.net.cidr; rec {
|
prefixes = with lib.my.net.cidr; rec {
|
||||||
modem = {
|
modem = {
|
||||||
v4 = "192.168.0.0/24";
|
v4 = "192.168.0.0/24";
|
||||||
};
|
};
|
||||||
|
ont = {
|
||||||
|
v4 = "192.168.100.0/24";
|
||||||
|
};
|
||||||
all = {
|
all = {
|
||||||
v4 = "192.168.64.0/18";
|
v4 = "192.168.64.0/18";
|
||||||
v6 = "2a0e:97c0:4d0::/60";
|
v6 = "2a0e:97c0:4d0::/60";
|
||||||
@@ -277,6 +382,8 @@ rec {
|
|||||||
v6 = host ((1*65536*65536*65536) + 65535) prefixes.as211024.v6;
|
v6 = host ((1*65536*65536*65536) + 65535) prefixes.as211024.v6;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
roceBootModules = [ "ib_core" "ib_uverbs" "mlx5_core" "mlx5_ib" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
britway = {
|
britway = {
|
||||||
@@ -292,6 +399,20 @@ rec {
|
|||||||
assignedV6 = "2001:19f0:7402:128b:5400:04ff:feac:6e06";
|
assignedV6 = "2001:19f0:7402:128b:5400:04ff:feac:6e06";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
britnet = {
|
||||||
|
domain = "bhx1.int.${pubDomain}";
|
||||||
|
pubV4 = "77.74.199.67";
|
||||||
|
vpn = {
|
||||||
|
port = 51820;
|
||||||
|
};
|
||||||
|
prefixes = with lib.my.net.cidr; rec {
|
||||||
|
vpn = {
|
||||||
|
v4 = "10.200.0.0/24";
|
||||||
|
v6 = "fdfb:5ebf:6e84::/64";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
tailscale = {
|
tailscale = {
|
||||||
prefix = {
|
prefix = {
|
||||||
v4 = "100.64.0.0/10";
|
v4 = "100.64.0.0/10";
|
||||||
@@ -327,6 +448,7 @@ rec {
|
|||||||
};
|
};
|
||||||
|
|
||||||
domain = "hentai.engineer";
|
domain = "hentai.engineer";
|
||||||
|
ipv4MTU = 1460;
|
||||||
vpn = {
|
vpn = {
|
||||||
port = 51820;
|
port = 51820;
|
||||||
};
|
};
|
||||||
@@ -335,11 +457,21 @@ rec {
|
|||||||
ctrs.v4 = subnet 4 0 all.v4;
|
ctrs.v4 = subnet 4 0 all.v4;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
hillcrest = {
|
||||||
|
vpn.port = 51822;
|
||||||
|
};
|
||||||
|
|
||||||
|
john-valorant = {
|
||||||
|
vpn.port = 51823;
|
||||||
|
};
|
||||||
|
|
||||||
sshKeyFiles = {
|
sshKeyFiles = {
|
||||||
me = ../.keys/me.pub;
|
me = ../.keys/me.pub;
|
||||||
deploy = ../.keys/deploy.pub;
|
deploy = ../.keys/deploy.pub;
|
||||||
rsyncNet = ../.keys/zh2855.rsync.net.pub;
|
rsyncNet = ../.keys/zh2855.rsync.net.pub;
|
||||||
mailcowAcme = ../.keys/mailcow-acme.pub;
|
mailcowAcme = ../.keys/mailcow-acme.pub;
|
||||||
|
harmonia = ../.keys/harmonia.pub;
|
||||||
};
|
};
|
||||||
sshHostKeys = {
|
sshHostKeys = {
|
||||||
mail-vm = ../.keys/mail-vm-host.pub;
|
mail-vm = ../.keys/mail-vm-host.pub;
|
||||||
|
|||||||
+38
-9
@@ -1,11 +1,11 @@
|
|||||||
{ lib }:
|
{ inputs, lib }:
|
||||||
let
|
let
|
||||||
inherit (builtins) length match elemAt filter replaceStrings substring;
|
inherit (builtins) length match elemAt filter replaceStrings substring;
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
genAttrs mapAttrsToList filterAttrsRecursive nameValuePair types
|
genAttrs mapAttrsToList filterAttrsRecursive nameValuePair types
|
||||||
mkOption mkOverride mkForce mkIf mergeEqualOption optional
|
mkOption mkOverride mkForce mkIf mergeEqualOption optional
|
||||||
showWarnings concatStringsSep flatten unique optionalAttrs
|
showWarnings concatStringsSep flatten unique optionalAttrs
|
||||||
mkBefore;
|
mkBefore toLower splitString last;
|
||||||
inherit (lib.flake) defaultSystems;
|
inherit (lib.flake) defaultSystems;
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
@@ -23,7 +23,7 @@ rec {
|
|||||||
|
|
||||||
attrsToNVList = mapAttrsToList nameValuePair;
|
attrsToNVList = mapAttrsToList nameValuePair;
|
||||||
|
|
||||||
inherit (import ./net.nix { inherit lib; }) net;
|
inherit ((import "${inputs.libnetRepo}/lib/netu.nix" { inherit lib; }).lib) net;
|
||||||
dns = import ./dns.nix { inherit lib; };
|
dns = import ./dns.nix { inherit lib; };
|
||||||
c = import ./constants.nix { inherit lib; };
|
c = import ./constants.nix { inherit lib; };
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ rec {
|
|||||||
in mkApp "${app}/bin/${app.meta.mainProgram}";
|
in mkApp "${app}/bin/${app.meta.mainProgram}";
|
||||||
flakePackageOverlay' = flake: pkg: system: (final: prev:
|
flakePackageOverlay' = flake: pkg: system: (final: prev:
|
||||||
let
|
let
|
||||||
pkg' = if pkg != null then flake.packages.${system}.${pkg} else flake.defaultPackage.${system};
|
pkg' = if pkg != null then flake.packages.${system}.${pkg} else flake.packages.${system}.default;
|
||||||
name = if pkg != null then pkg else pkg'.name;
|
name = if pkg != null then pkg else pkg'.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -175,11 +175,11 @@ rec {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vm = rec {
|
vm = rec {
|
||||||
lvmDisk' = name: lv: {
|
lvmDisk'' = name: vg: lv: {
|
||||||
inherit name;
|
inherit name;
|
||||||
backend = {
|
backend = {
|
||||||
driver = "host_device";
|
driver = "host_device";
|
||||||
filename = "/dev/main/${lv}";
|
filename = "/dev/${vg}/${lv}";
|
||||||
# It appears this needs to be set on the backend _and_ the format
|
# It appears this needs to be set on the backend _and_ the format
|
||||||
discard = "unmap";
|
discard = "unmap";
|
||||||
};
|
};
|
||||||
@@ -189,7 +189,8 @@ rec {
|
|||||||
};
|
};
|
||||||
frontend = "virtio-blk";
|
frontend = "virtio-blk";
|
||||||
};
|
};
|
||||||
lvmDisk = lv: lvmDisk' lv lv;
|
lvmDisk' = vg: lv: lvmDisk'' lv vg lv;
|
||||||
|
lvmDisk = lvmDisk' "main";
|
||||||
disk = vm: lv: lvmDisk' lv "vm-${vm}-${lv}";
|
disk = vm: lv: lvmDisk' lv "vm-${vm}-${lv}";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -248,10 +249,38 @@ rec {
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
trivial = prev.trivial // {
|
trivial = prev.trivial // {
|
||||||
release = "23.12:u-${prev.trivial.release}";
|
release = "26.06:u-${prev.trivial.release}";
|
||||||
codeName = "Amogus";
|
codeName = "Irritating";
|
||||||
revisionWithDefault = default: self.rev or default;
|
revisionWithDefault = default: self.rev or default;
|
||||||
versionSuffix = ".${date}.${revCode self}:u-${revCode pkgsFlake}";
|
versionSuffix = ".${date}.${revCode self}:u-${revCode pkgsFlake}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
upstreamRelease = last (splitString "-" lib.trivial.release);
|
||||||
|
|
||||||
|
netbootKeaClientClasses = { tftpIP, hostname, systems }:
|
||||||
|
let
|
||||||
|
testIPXE = "substring(option[user-class].hex, 0, 4) == 'iPXE'";
|
||||||
|
in
|
||||||
|
(mapAttrsToList (name: mac: {
|
||||||
|
name = "nixos-${name}";
|
||||||
|
test = "(${testIPXE}) and (hexstring(pkt4.mac, ':') == '${toLower mac}')";
|
||||||
|
next-server = tftpIP;
|
||||||
|
server-hostname = hostname;
|
||||||
|
boot-file-name = "http://${hostname}/systems/${name}/menu.ipxe";
|
||||||
|
}) systems) ++ [
|
||||||
|
{
|
||||||
|
name = "ipxe";
|
||||||
|
test = testIPXE;
|
||||||
|
next-server = tftpIP;
|
||||||
|
server-hostname = hostname;
|
||||||
|
boot-file-name = "http://${hostname}/boot.ipxe";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "efi-x86_64";
|
||||||
|
test = "option[client-system].hex == 0x0007";
|
||||||
|
next-server = tftpIP;
|
||||||
|
server-hostname = hostname;
|
||||||
|
boot-file-name = "ipxe-x86_64.efi";
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
-1322
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,191 @@
|
|||||||
|
{ lib, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib.my) net;
|
||||||
|
inherit (lib.my.c) pubDomain;
|
||||||
|
inherit (lib.my.c.britnet) domain pubV4 prefixes;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
nixos.systems.britnet = {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
nixpkgs = "mine";
|
||||||
|
|
||||||
|
assignments = {
|
||||||
|
allhost = {
|
||||||
|
inherit domain;
|
||||||
|
ipv4 = {
|
||||||
|
address = pubV4;
|
||||||
|
mask = 24;
|
||||||
|
gateway = "77.74.199.1";
|
||||||
|
};
|
||||||
|
ipv6 = {
|
||||||
|
address = "2a12:ab46:5344:99::a";
|
||||||
|
gateway = "2a12:ab46:5344::1";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
vpn = {
|
||||||
|
ipv4 = {
|
||||||
|
address = net.cidr.host 1 prefixes.vpn.v4;
|
||||||
|
gateway = null;
|
||||||
|
};
|
||||||
|
ipv6.address = net.cidr.host 1 prefixes.vpn.v6;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
configuration = { lib, pkgs, modulesPath, config, assignments, allAssignments, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib) mkMerge mkForce;
|
||||||
|
inherit (lib.my) networkdAssignment;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
"${modulesPath}/profiles/qemu-guest.nix"
|
||||||
|
];
|
||||||
|
|
||||||
|
config = mkMerge [
|
||||||
|
{
|
||||||
|
boot = {
|
||||||
|
initrd.availableKernelModules = [
|
||||||
|
"ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "ahci" "sr_mod" "virtio_blk"
|
||||||
|
];
|
||||||
|
loader = {
|
||||||
|
systemd-boot.enable = false;
|
||||||
|
grub = {
|
||||||
|
enable = true;
|
||||||
|
device = "/dev/vda";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems = {
|
||||||
|
"/boot" = {
|
||||||
|
device = "/dev/disk/by-uuid/457444a1-81dd-4934-960c-650ad16c92b5";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
"/nix" = {
|
||||||
|
device = "/dev/disk/by-uuid/992c0c79-5be6-45b6-bc30-dc82e3ec082a";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
"/persist" = {
|
||||||
|
device = "/dev/disk/by-uuid/f020a955-54d5-4098-98ba-d3615781d96a";
|
||||||
|
fsType = "ext4";
|
||||||
|
neededForBoot = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
systemPackages = with pkgs; [
|
||||||
|
wireguard-tools
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
iperf3 = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
tailscale = {
|
||||||
|
enable = true;
|
||||||
|
authKeyFile = config.age.secrets."tailscale-auth.key".path;
|
||||||
|
openFirewall = true;
|
||||||
|
interfaceName = "tailscale0";
|
||||||
|
extraUpFlags = [
|
||||||
|
"--operator=${config.my.user.config.name}"
|
||||||
|
"--login-server=https://hs.nul.ie"
|
||||||
|
"--netfilter-mode=off"
|
||||||
|
"--advertise-exit-node"
|
||||||
|
"--accept-routes=false"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking = { inherit domain; };
|
||||||
|
|
||||||
|
systemd.network = {
|
||||||
|
netdevs = {
|
||||||
|
"30-wg0" = {
|
||||||
|
netdevConfig = {
|
||||||
|
Name = "wg0";
|
||||||
|
Kind = "wireguard";
|
||||||
|
};
|
||||||
|
wireguardConfig = {
|
||||||
|
PrivateKeyFile = config.age.secrets."britnet/wg.key".path;
|
||||||
|
ListenPort = lib.my.c.britnet.vpn.port;
|
||||||
|
};
|
||||||
|
wireguardPeers = [
|
||||||
|
{
|
||||||
|
PublicKey = "EfPwREfZ/q3ogHXBIqFZh4k/1NRJRyq4gBkBXtegNkE=";
|
||||||
|
AllowedIPs = [
|
||||||
|
(net.cidr.host 10 prefixes.vpn.v4)
|
||||||
|
(net.cidr.host 10 prefixes.vpn.v6)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
links = {
|
||||||
|
"10-veth0" = {
|
||||||
|
matchConfig.PermanentMACAddress = "00:db:d9:62:68:1a";
|
||||||
|
linkConfig.Name = "veth0";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networks = {
|
||||||
|
"20-veth0" = mkMerge [
|
||||||
|
(networkdAssignment "veth0" assignments.allhost)
|
||||||
|
{
|
||||||
|
dns = [ "1.1.1.1" "1.0.0.1" ];
|
||||||
|
routes = [
|
||||||
|
{
|
||||||
|
# Gateway is on a different network for some reason...
|
||||||
|
Destination = "2a12:ab46:5344::1";
|
||||||
|
Scope = "link";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
"30-wg0" = mkMerge [
|
||||||
|
(networkdAssignment "wg0" assignments.vpn)
|
||||||
|
{
|
||||||
|
networkConfig.IPv6AcceptRA = mkForce false;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
my = {
|
||||||
|
server.enable = true;
|
||||||
|
secrets = {
|
||||||
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJIEx+1EC/lN6WKIaOB+O5LJgVHRK962YpZEPQg/m78O";
|
||||||
|
files = {
|
||||||
|
"tailscale-auth.key" = {};
|
||||||
|
"britnet/wg.key" = {
|
||||||
|
owner = "systemd-network";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
firewall = {
|
||||||
|
udp.allowed = [ lib.my.c.britnet.vpn.port ];
|
||||||
|
trustedInterfaces = [ "tailscale0" ];
|
||||||
|
extraRules = ''
|
||||||
|
table inet filter {
|
||||||
|
chain forward {
|
||||||
|
iifname wg0 oifname veth0 accept
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table inet nat {
|
||||||
|
chain postrouting {
|
||||||
|
iifname { tailscale0, wg0 } oifname veth0 snat ip to ${assignments.allhost.ipv4.address}
|
||||||
|
iifname { tailscale0, wg0 } oifname veth0 snat ip6 to ${assignments.allhost.ipv6.address}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,23 +11,24 @@ in
|
|||||||
config = {
|
config = {
|
||||||
my = {
|
my = {
|
||||||
secrets.files."britway/bgp-password-vultr.conf" = {
|
secrets.files."britway/bgp-password-vultr.conf" = {
|
||||||
owner = "bird2";
|
owner = "bird";
|
||||||
group = "bird2";
|
group = "bird";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
environment.etc."bird/vultr-password.conf".source = config.age.secrets."britway/bgp-password-vultr.conf".path;
|
environment.etc."bird/vultr-password.conf".source = config.age.secrets."britway/bgp-password-vultr.conf".path;
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
services.bird2.after = [ "systemd-networkd-wait-online@veth0.service" ];
|
services.bird.after = [ "systemd-networkd-wait-online@veth0.service" ];
|
||||||
network = {
|
network = {
|
||||||
config.networkConfig.ManageForeignRoutes = false;
|
config.networkConfig.ManageForeignRoutes = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
services = {
|
services = {
|
||||||
bird2 = {
|
bird = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
package = pkgs.bird2;
|
||||||
preCheckConfig = ''
|
preCheckConfig = ''
|
||||||
echo '"dummy"' > vultr-password.conf
|
echo '"dummy"' > vultr-password.conf
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ in
|
|||||||
{
|
{
|
||||||
matchConfig.Name = "as211024";
|
matchConfig.Name = "as211024";
|
||||||
networkConfig.IPv6AcceptRA = mkForce false;
|
networkConfig.IPv6AcceptRA = mkForce false;
|
||||||
routes = map (r: { routeConfig = r; }) [
|
routes = [
|
||||||
{
|
{
|
||||||
Destination = lib.my.c.colony.prefixes.all.v4;
|
Destination = lib.my.c.colony.prefixes.all.v4;
|
||||||
Gateway = allAssignments.estuary.as211024.ipv4.address;
|
Gateway = allAssignments.estuary.as211024.ipv4.address;
|
||||||
@@ -123,7 +123,7 @@ in
|
|||||||
Table = "ts-extra";
|
Table = "ts-extra";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
routingPolicyRules = map (r: { routingPolicyRuleConfig = r; }) [
|
routingPolicyRules = [
|
||||||
{
|
{
|
||||||
IncomingInterface = "tailscale0";
|
IncomingInterface = "tailscale0";
|
||||||
To = lib.my.c.colony.prefixes.all.v6;
|
To = lib.my.c.colony.prefixes.all.v6;
|
||||||
|
|||||||
@@ -9,11 +9,6 @@ in
|
|||||||
config = {
|
config = {
|
||||||
my = {
|
my = {
|
||||||
secrets.files = {
|
secrets.files = {
|
||||||
"dhparams.pem" = {
|
|
||||||
owner = "acme";
|
|
||||||
group = "acme";
|
|
||||||
mode = "440";
|
|
||||||
};
|
|
||||||
"britway/cloudflare-credentials.conf" = {
|
"britway/cloudflare-credentials.conf" = {
|
||||||
owner = "acme";
|
owner = "acme";
|
||||||
group = "acme";
|
group = "acme";
|
||||||
@@ -45,7 +40,7 @@ in
|
|||||||
"*.${pubDomain}"
|
"*.${pubDomain}"
|
||||||
];
|
];
|
||||||
dnsProvider = "cloudflare";
|
dnsProvider = "cloudflare";
|
||||||
credentialsFile = config.age.secrets."britway/cloudflare-credentials.conf".path;
|
environmentFile = config.age.secrets."britway/cloudflare-credentials.conf".path;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -58,7 +53,6 @@ in
|
|||||||
logError = "stderr info";
|
logError = "stderr info";
|
||||||
recommendedTlsSettings = true;
|
recommendedTlsSettings = true;
|
||||||
serverTokens = true;
|
serverTokens = true;
|
||||||
sslDhparam = config.age.secrets."dhparams.pem".path;
|
|
||||||
|
|
||||||
# Based on recommended*Settings, but probably better to be explicit about these
|
# Based on recommended*Settings, but probably better to be explicit about these
|
||||||
appendHttpConfig = ''
|
appendHttpConfig = ''
|
||||||
@@ -80,7 +74,7 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
"ts.${pubDomain}" = {
|
"hs.${pubDomain}" = {
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://localhost:${toString config.services.headscale.port}";
|
proxyPass = "http://localhost:${toString config.services.headscale.port}";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
|
|||||||
@@ -4,20 +4,6 @@ let
|
|||||||
inherit (lib.my.c) pubDomain;
|
inherit (lib.my.c) pubDomain;
|
||||||
inherit (lib.my.c.britway) prefixes domain;
|
inherit (lib.my.c.britway) prefixes domain;
|
||||||
|
|
||||||
# Can't use overrideAttrs because we need to override `vendorHash` within `buildGoModule`
|
|
||||||
headscale = pkgs.headscale.override {
|
|
||||||
buildGoModule = args: pkgs.buildGoModule (args // rec {
|
|
||||||
version = "0.23.0-alpha2";
|
|
||||||
src = pkgs.fetchFromGitHub {
|
|
||||||
owner = "juanfont";
|
|
||||||
repo = "headscale";
|
|
||||||
rev = "v${version}";
|
|
||||||
hash = "sha256-sz+uQyyq/5YYDe5I44x5x2nvd48swAhNlInB8KZYvDo=";
|
|
||||||
};
|
|
||||||
vendorHash = "sha256-u9AmJguQ5dnJpfhOeLN43apvMHuraOrJhvlEIp9RoIc=";
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
advRoutes = concatStringsSep "," [
|
advRoutes = concatStringsSep "," [
|
||||||
lib.my.c.home.prefixes.all.v4
|
lib.my.c.home.prefixes.all.v4
|
||||||
lib.my.c.home.prefixes.all.v6
|
lib.my.c.home.prefixes.all.v6
|
||||||
@@ -39,19 +25,21 @@ in
|
|||||||
services = {
|
services = {
|
||||||
headscale = {
|
headscale = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = headscale;
|
|
||||||
settings = {
|
settings = {
|
||||||
disable_check_updates = true;
|
disable_check_updates = true;
|
||||||
unix_socket_permission = "0770";
|
unix_socket_permission = "0770";
|
||||||
server_url = "https://ts.${pubDomain}";
|
server_url = "https://hs.${pubDomain}";
|
||||||
db_type = "sqlite3";
|
database = {
|
||||||
db_path = "/var/lib/headscale/db.sqlite3";
|
type = "sqlite3";
|
||||||
|
sqlite.path = "/var/lib/headscale/db.sqlite3";
|
||||||
|
};
|
||||||
noise.private_key_path = "/var/lib/headscale/noise_private.key";
|
noise.private_key_path = "/var/lib/headscale/noise_private.key";
|
||||||
ip_prefixes = with lib.my.c.tailscale.prefix; [ v4 v6 ];
|
prefixes = with lib.my.c.tailscale.prefix; { inherit v4 v6; };
|
||||||
dns_config = {
|
dns = {
|
||||||
|
override_local_dns = false;
|
||||||
# Use IPs that will route inside the VPN to prevent interception
|
# Use IPs that will route inside the VPN to prevent interception
|
||||||
# (e.g. DNS rebinding filtering)
|
# (e.g. DNS rebinding filtering)
|
||||||
restricted_nameservers = {
|
nameservers.split = {
|
||||||
"${domain}" = pubNameservers;
|
"${domain}" = pubNameservers;
|
||||||
"${lib.my.c.colony.domain}" = with allAssignments.estuary.base; [
|
"${lib.my.c.colony.domain}" = with allAssignments.estuary.base; [
|
||||||
ipv4.address ipv6.address
|
ipv4.address ipv6.address
|
||||||
@@ -65,7 +53,6 @@ in
|
|||||||
};
|
};
|
||||||
magic_dns = true;
|
magic_dns = true;
|
||||||
base_domain = "ts.${pubDomain}";
|
base_domain = "ts.${pubDomain}";
|
||||||
override_local_dns = false;
|
|
||||||
};
|
};
|
||||||
oidc = {
|
oidc = {
|
||||||
only_start_if_oidc_is_available = true;
|
only_start_if_oidc_is_available = true;
|
||||||
@@ -85,7 +72,7 @@ in
|
|||||||
interfaceName = "tailscale0";
|
interfaceName = "tailscale0";
|
||||||
extraUpFlags = [
|
extraUpFlags = [
|
||||||
"--operator=${config.my.user.config.name}"
|
"--operator=${config.my.user.config.name}"
|
||||||
"--login-server=https://ts.nul.ie"
|
"--login-server=https://hs.nul.ie"
|
||||||
"--netfilter-mode=off"
|
"--netfilter-mode=off"
|
||||||
"--advertise-exit-node"
|
"--advertise-exit-node"
|
||||||
"--advertise-routes=${advRoutes}"
|
"--advertise-routes=${advRoutes}"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
let
|
||||||
inherit (lib.my) net;
|
inherit (lib.my) net;
|
||||||
inherit (lib.my.c.colony) domain prefixes firewallForwards;
|
inherit (lib.my.c.colony) domain prefixes custRouting firewallForwards;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [ ./vms ];
|
imports = [ ./vms ];
|
||||||
@@ -60,8 +60,8 @@ in
|
|||||||
kernelPackages = (lib.my.c.kernel.lts pkgs).extend (self: super: {
|
kernelPackages = (lib.my.c.kernel.lts pkgs).extend (self: super: {
|
||||||
kernel = super.kernel.override {
|
kernel = super.kernel.override {
|
||||||
structuredExtraConfig = with lib.kernel; {
|
structuredExtraConfig = with lib.kernel; {
|
||||||
#SOME_OPT = yes;
|
ACPI_APEI_PCIEAER = yes;
|
||||||
#A_MOD = module;
|
PCIEAER = yes;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -150,12 +150,12 @@ in
|
|||||||
"serial-getty@ttyS1".enable = true;
|
"serial-getty@ttyS1".enable = true;
|
||||||
lvm-activate-main = {
|
lvm-activate-main = {
|
||||||
description = "Activate remaining LVs";
|
description = "Activate remaining LVs";
|
||||||
before = [ "local-fs-pre.target" ];
|
unitConfig.DefaultDependencies = false;
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
ExecStart = "${pkgs.lvm2.bin}/bin/vgchange -aay main";
|
ExecStart = "${pkgs.lvm2.bin}/bin/vgchange -aay main";
|
||||||
};
|
};
|
||||||
wantedBy = [ "sysinit.target" ];
|
wantedBy = [ "local-fs-pre.target" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
rsync-lvm-meta = {
|
rsync-lvm-meta = {
|
||||||
@@ -252,10 +252,10 @@ in
|
|||||||
};
|
};
|
||||||
ipv6Prefixes = [
|
ipv6Prefixes = [
|
||||||
{
|
{
|
||||||
ipv6PrefixConfig.Prefix = prefixes.vms.v6;
|
Prefix = prefixes.vms.v6;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
routes = map (r: { routeConfig = r; }) [
|
routes = [
|
||||||
{
|
{
|
||||||
Destination = prefixes.ctrs.v4;
|
Destination = prefixes.ctrs.v4;
|
||||||
Gateway = allAssignments.shill.routing.ipv4.address;
|
Gateway = allAssignments.shill.routing.ipv4.address;
|
||||||
@@ -264,10 +264,12 @@ in
|
|||||||
Destination = prefixes.ctrs.v6;
|
Destination = prefixes.ctrs.v6;
|
||||||
Gateway = allAssignments.shill.internal.ipv6.address;
|
Gateway = allAssignments.shill.internal.ipv6.address;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
Destination = allAssignments.shill.internal.ipv4.address;
|
Destination = allAssignments.shill.internal.ipv4.address;
|
||||||
Gateway = allAssignments.shill.routing.ipv4.address;
|
Gateway = allAssignments.shill.routing.ipv4.address;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
Destination = lib.my.c.tailscale.prefix.v4;
|
Destination = lib.my.c.tailscale.prefix.v4;
|
||||||
Gateway = allAssignments.shill.routing.ipv4.address;
|
Gateway = allAssignments.shill.routing.ipv4.address;
|
||||||
@@ -276,6 +278,15 @@ in
|
|||||||
Destination = lib.my.c.tailscale.prefix.v6;
|
Destination = lib.my.c.tailscale.prefix.v6;
|
||||||
Gateway = allAssignments.shill.internal.ipv6.address;
|
Gateway = allAssignments.shill.internal.ipv6.address;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
Destination = prefixes.qclk.v4;
|
||||||
|
Gateway = allAssignments.shill.routing.ipv4.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Destination = prefixes.jam.v6;
|
||||||
|
Gateway = allAssignments.shill.internal.ipv6.address;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
Destination = prefixes.oci.v4;
|
Destination = prefixes.oci.v4;
|
||||||
@@ -307,7 +318,7 @@ in
|
|||||||
"90-vm-mail" = {
|
"90-vm-mail" = {
|
||||||
matchConfig.Name = "vm-mail";
|
matchConfig.Name = "vm-mail";
|
||||||
address = [
|
address = [
|
||||||
(net.cidr.subnet 8 1 prefixes.cust.v4)
|
"${custRouting.mail-vm}/32"
|
||||||
prefixes.mail.v6
|
prefixes.mail.v6
|
||||||
];
|
];
|
||||||
networkConfig = {
|
networkConfig = {
|
||||||
@@ -316,10 +327,10 @@ in
|
|||||||
};
|
};
|
||||||
ipv6Prefixes = [
|
ipv6Prefixes = [
|
||||||
{
|
{
|
||||||
ipv6PrefixConfig.Prefix = prefixes.mail.v6;
|
Prefix = prefixes.mail.v6;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
routes = map (r: { routeConfig = r; }) [
|
routes = [
|
||||||
{
|
{
|
||||||
Destination = prefixes.mail.v4;
|
Destination = prefixes.mail.v4;
|
||||||
Scope = "link";
|
Scope = "link";
|
||||||
@@ -330,7 +341,7 @@ in
|
|||||||
"90-vm-darts" = {
|
"90-vm-darts" = {
|
||||||
matchConfig.Name = "vm-darts";
|
matchConfig.Name = "vm-darts";
|
||||||
address = [
|
address = [
|
||||||
(net.cidr.subnet 8 2 prefixes.cust.v4)
|
"${custRouting.darts-vm}/32"
|
||||||
prefixes.darts.v6
|
prefixes.darts.v6
|
||||||
];
|
];
|
||||||
networkConfig = {
|
networkConfig = {
|
||||||
@@ -339,10 +350,10 @@ in
|
|||||||
};
|
};
|
||||||
ipv6Prefixes = [
|
ipv6Prefixes = [
|
||||||
{
|
{
|
||||||
ipv6PrefixConfig.Prefix = prefixes.darts.v6;
|
Prefix = prefixes.darts.v6;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
routes = map (r: { routeConfig = r; }) [
|
routes = [
|
||||||
{
|
{
|
||||||
Destination = prefixes.darts.v4;
|
Destination = prefixes.darts.v4;
|
||||||
Scope = "link";
|
Scope = "link";
|
||||||
|
|||||||
@@ -29,6 +29,9 @@
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
# Kernel Same-Page Merging to attempt memory usage reduction
|
||||||
|
hardware.ksm.enable = false;
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
network = {
|
network = {
|
||||||
links = {
|
links = {
|
||||||
@@ -130,7 +133,8 @@
|
|||||||
|
|
||||||
(vm.lvmDisk "media")
|
(vm.lvmDisk "media")
|
||||||
(vm.lvmDisk "minio")
|
(vm.lvmDisk "minio")
|
||||||
(vm.lvmDisk "nix-atticd")
|
(vm.lvmDisk "nix-cache")
|
||||||
|
(vm.lvmDisk "jam")
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,6 +213,7 @@
|
|||||||
drives = [
|
drives = [
|
||||||
(mkMerge [ (vm.disk "darts" "root") { frontendOpts.bootindex = 0; } ])
|
(mkMerge [ (vm.disk "darts" "root") { frontendOpts.bootindex = 0; } ])
|
||||||
(vm.lvmDisk' "media" "darts-media")
|
(vm.lvmDisk' "media" "darts-media")
|
||||||
|
(vm.lvmDisk' "ext" "darts-ext")
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ in
|
|||||||
{
|
{
|
||||||
config = {
|
config = {
|
||||||
services = {
|
services = {
|
||||||
bird2 = {
|
bird = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
package = pkgs.bird2;
|
||||||
# TODO: Clean up and modularise
|
# TODO: Clean up and modularise
|
||||||
config = ''
|
config = ''
|
||||||
define OWNAS = 211024;
|
define OWNAS = 211024;
|
||||||
@@ -232,59 +233,88 @@ in
|
|||||||
neighbor 2a07:cd40:1::9 as 202413;
|
neighbor 2a07:cd40:1::9 as 202413;
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol bgp ixp4_frysix_rs1 from ixp_bgp4 {
|
protocol bgp ixp4_frysix_rs3 from ixp_bgp4 {
|
||||||
description "Frys-IX route server 1 (IPv4)";
|
description "Frys-IX route server 3 (IPv4)";
|
||||||
neighbor 185.1.203.253 as 56393;
|
neighbor 185.1.160.255 as 56393;
|
||||||
}
|
}
|
||||||
protocol bgp ixp6_frysix_rs1 from ixp_bgp6 {
|
protocol bgp ixp6_frysix_rs3 from ixp_bgp6 {
|
||||||
description "Frys-IX route server 1 (IPv6)";
|
description "Frys-IX route server 3 (IPv6)";
|
||||||
neighbor 2001:7f8:10f::dc49:253 as 56393;
|
neighbor 2001:7f8:10f::dc49:1 as 56393;
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol bgp ixp4_frysix_rs2 from ixp_bgp4 {
|
protocol bgp ixp4_frysix_rs4 from ixp_bgp4 {
|
||||||
description "Frys-IX route server 2 (IPv4)";
|
description "Frys-IX route server 4 (IPv4)";
|
||||||
neighbor 185.1.203.254 as 56393;
|
neighbor 185.1.161.0 as 56393;
|
||||||
}
|
}
|
||||||
protocol bgp ixp6_frysix_rs2 from ixp_bgp6 {
|
protocol bgp ixp6_frysix_rs4 from ixp_bgp6 {
|
||||||
description "Frys-IX route server 2 (IPv6)";
|
description "Frys-IX route server 4 (IPv6)";
|
||||||
neighbor 2001:7f8:10f::dc49:254 as 56393;
|
neighbor 2001:7f8:10f::dc49:2 as 56393;
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol bgp peer4_frysix_luje from peer_bgp4 {
|
protocol bgp peer4_frysix_luje from peer_bgp4 {
|
||||||
description "LUJE.net (on Frys-IX, IPv4)";
|
description "LUJE.net (on Frys-IX, IPv4)";
|
||||||
neighbor 185.1.203.152 as 212855;
|
neighbor 185.1.160.152 as 212855;
|
||||||
}
|
}
|
||||||
protocol bgp peer6_frysix_luje from peer_bgp6 {
|
protocol bgp peer6_frysix_luje from peer_bgp6 {
|
||||||
description "LUJE.net (on Frys-IX, IPv6)";
|
description "LUJE.net (on Frys-IX, IPv6)";
|
||||||
neighbor 2001:7f8:10f::3:3f95:152 as 212855;
|
neighbor 2001:7f8:10f::3:3f95:152 as 212855;
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol bgp peer4_frysix_he from peer_bgp4 {
|
protocol bgp peer4_frysix_he from peer_bgp4 {
|
||||||
description "Hurricane Electric (on Frys-IX, IPv4)";
|
description "Hurricane Electric (on Frys-IX, IPv4)";
|
||||||
neighbor 185.1.203.154 as 6939;
|
neighbor 185.1.160.154 as 6939;
|
||||||
}
|
}
|
||||||
protocol bgp peer4_frysix_cloudflare from peer_bgp4 {
|
|
||||||
description "Cloudflare (on Frys-IX, IPv4)";
|
protocol bgp peer4_frysix_cloudflare1 from peer_bgp4 {
|
||||||
neighbor 185.1.203.217 as 13335;
|
description "Cloudflare 1 (on Frys-IX, IPv4)";
|
||||||
|
neighbor 185.1.160.217 as 13335;
|
||||||
}
|
}
|
||||||
protocol bgp peer6_frysix_cloudflare from peer_bgp6 {
|
protocol bgp peer4_frysix_cloudflare2 from peer_bgp4 {
|
||||||
description "Cloudflare (on Frys-IX, IPv6)";
|
description "Cloudflare 2 (on Frys-IX, IPv4)";
|
||||||
|
neighbor 185.1.160.109 as 13335;
|
||||||
|
}
|
||||||
|
protocol bgp peer6_frysix_cloudflare1 from peer_bgp6 {
|
||||||
|
description "Cloudflare 1 (on Frys-IX, IPv6)";
|
||||||
neighbor 2001:7f8:10f::3417:217 as 13335;
|
neighbor 2001:7f8:10f::3417:217 as 13335;
|
||||||
}
|
}
|
||||||
|
protocol bgp peer6_frysix_cloudflare2 from peer_bgp6 {
|
||||||
|
description "Cloudflare 2 (on Frys-IX, IPv6)";
|
||||||
|
neighbor 2001:7f8:10f::3417:109 as 13335;
|
||||||
|
}
|
||||||
|
|
||||||
protocol bgp peer4_frysix_jurrian from peer_bgp4 {
|
protocol bgp peer4_frysix_jurrian from peer_bgp4 {
|
||||||
description "AS212635 aka jurrian (on Frys-IX, IPv4)";
|
description "AS212635 aka jurrian (on Frys-IX, IPv4)";
|
||||||
neighbor 185.1.203.134 as 212635;
|
neighbor 185.1.160.134 as 212635;
|
||||||
}
|
}
|
||||||
protocol bgp peer6_frysix_jurrian from peer_bgp6 {
|
protocol bgp peer6_frysix_jurrian from peer_bgp6 {
|
||||||
description "AS212635 aka jurrian (on Frys-IX, IPv6)";
|
description "AS212635 aka jurrian (on Frys-IX, IPv6)";
|
||||||
neighbor 2001:7f8:10f::3:3e9b:134 as 212635;
|
neighbor 2001:7f8:10f::3:3e9b:134 as 212635;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol bgp peer4_nlix_meta1 from peer_bgp4 {
|
||||||
|
description "Meta 1 (on NL-ix, IPv4)";
|
||||||
|
neighbor 193.239.116.147 as 32934;
|
||||||
|
}
|
||||||
|
protocol bgp peer4_nlix_meta2 from peer_bgp4 {
|
||||||
|
description "Meta 2 (on NL-ix, IPv4)";
|
||||||
|
neighbor 193.239.116.148 as 32934;
|
||||||
|
}
|
||||||
|
protocol bgp peer6_nlix_meta1 from peer_bgp6 {
|
||||||
|
description "Meta 1 (on NL-ix, IPv6)";
|
||||||
|
neighbor 2001:7f8:13::a503:2934:1 as 32934;
|
||||||
|
}
|
||||||
|
protocol bgp peer6_nlix_meta2 from peer_bgp6 {
|
||||||
|
description "Meta 2 (on NL-ix, IPv6)";
|
||||||
|
neighbor 2001:7f8:13::a503:2934:2 as 32934;
|
||||||
|
}
|
||||||
|
|
||||||
protocol bgp peer4_frysix_meta1 from peer_bgp4 {
|
protocol bgp peer4_frysix_meta1 from peer_bgp4 {
|
||||||
description "Meta 1 (on Frys-IX, IPv4)";
|
description "Meta 1 (on Frys-IX, IPv4)";
|
||||||
neighbor 185.1.203.225 as 32934;
|
neighbor 185.1.160.225 as 32934;
|
||||||
}
|
}
|
||||||
protocol bgp peer4_frysix_meta2 from peer_bgp4 {
|
protocol bgp peer4_frysix_meta2 from peer_bgp4 {
|
||||||
description "Meta 2 (on Frys-IX, IPv4)";
|
description "Meta 2 (on Frys-IX, IPv4)";
|
||||||
neighbor 185.1.203.226 as 32934;
|
neighbor 185.1.160.226 as 32934;
|
||||||
}
|
}
|
||||||
protocol bgp peer6_frysix_meta1 from peer_bgp6 {
|
protocol bgp peer6_frysix_meta1 from peer_bgp6 {
|
||||||
description "Meta 1 (on Frys-IX, IPv6)";
|
description "Meta 1 (on Frys-IX, IPv6)";
|
||||||
@@ -317,36 +347,36 @@ in
|
|||||||
ipv6 { preference (PREFIXP-1); };
|
ipv6 { preference (PREFIXP-1); };
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol bgp peer4_nlix_cloudflare1 from peer_bgp4 {
|
# protocol bgp peer4_nlix_cloudflare1 from peer_bgp4 {
|
||||||
description "Cloudflare NL-ix 1 (IPv4)";
|
# description "Cloudflare NL-ix 1 (IPv4)";
|
||||||
neighbor 193.239.117.14 as 13335;
|
# neighbor 193.239.117.14 as 13335;
|
||||||
ipv4 { preference (PREFPEER-1); };
|
# ipv4 { preference (PREFPEER-1); };
|
||||||
}
|
# }
|
||||||
protocol bgp peer4_nlix_cloudflare2 from peer_bgp4 {
|
# protocol bgp peer4_nlix_cloudflare2 from peer_bgp4 {
|
||||||
description "Cloudflare NL-ix 2 (IPv4)";
|
# description "Cloudflare NL-ix 2 (IPv4)";
|
||||||
neighbor 193.239.117.114 as 13335;
|
# neighbor 193.239.117.114 as 13335;
|
||||||
ipv4 { preference (PREFPEER-1); };
|
# ipv4 { preference (PREFPEER-1); };
|
||||||
}
|
# }
|
||||||
protocol bgp peer4_nlix_cloudflare3 from peer_bgp4 {
|
# protocol bgp peer4_nlix_cloudflare3 from peer_bgp4 {
|
||||||
description "Cloudflare NL-ix 3 (IPv4)";
|
# description "Cloudflare NL-ix 3 (IPv4)";
|
||||||
neighbor 193.239.118.138 as 13335;
|
# neighbor 193.239.118.138 as 13335;
|
||||||
ipv4 { preference (PREFPEER-1); };
|
# ipv4 { preference (PREFPEER-1); };
|
||||||
}
|
# }
|
||||||
protocol bgp peer6_nlix_cloudflare1 from peer_bgp6 {
|
# protocol bgp peer6_nlix_cloudflare1 from peer_bgp6 {
|
||||||
description "Cloudflare NL-ix 1 (IPv6)";
|
# description "Cloudflare NL-ix 1 (IPv6)";
|
||||||
neighbor 2001:7f8:13::a501:3335:1 as 13335;
|
# neighbor 2001:7f8:13::a501:3335:1 as 13335;
|
||||||
ipv6 { preference (PREFPEER-1); };
|
# ipv6 { preference (PREFPEER-1); };
|
||||||
}
|
# }
|
||||||
protocol bgp peer6_nlix_cloudflare2 from peer_bgp6 {
|
# protocol bgp peer6_nlix_cloudflare2 from peer_bgp6 {
|
||||||
description "Cloudflare NL-ix 2 (IPv6)";
|
# description "Cloudflare NL-ix 2 (IPv6)";
|
||||||
neighbor 2001:7f8:13::a501:3335:2 as 13335;
|
# neighbor 2001:7f8:13::a501:3335:2 as 13335;
|
||||||
ipv6 { preference (PREFPEER-1); };
|
# ipv6 { preference (PREFPEER-1); };
|
||||||
}
|
# }
|
||||||
protocol bgp peer6_nlix_cloudflare3 from peer_bgp6 {
|
# protocol bgp peer6_nlix_cloudflare3 from peer_bgp6 {
|
||||||
description "Cloudflare NL-ix 3 (IPv6)";
|
# description "Cloudflare NL-ix 3 (IPv6)";
|
||||||
neighbor 2001:7f8:13::a501:3335:3 as 13335;
|
# neighbor 2001:7f8:13::a501:3335:3 as 13335;
|
||||||
ipv6 { preference (PREFPEER-1); };
|
# ipv6 { preference (PREFPEER-1); };
|
||||||
}
|
# }
|
||||||
protocol bgp peer4_nlix_jurrian from peer_bgp4 {
|
protocol bgp peer4_nlix_jurrian from peer_bgp4 {
|
||||||
description "AS212635 aka jurrian (on NL-ix, IPv4)";
|
description "AS212635 aka jurrian (on NL-ix, IPv4)";
|
||||||
neighbor 193.239.117.55 as 212635;
|
neighbor 193.239.117.55 as 212635;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ in
|
|||||||
vpns = {
|
vpns = {
|
||||||
l2 = {
|
l2 = {
|
||||||
as211024 = {
|
as211024 = {
|
||||||
|
udpEncapsulation = true;
|
||||||
vni = 211024;
|
vni = 211024;
|
||||||
security.enable = true;
|
security.enable = true;
|
||||||
peers = {
|
peers = {
|
||||||
@@ -103,11 +104,9 @@ in
|
|||||||
lvm = {
|
lvm = {
|
||||||
dmeventd.enable = true;
|
dmeventd.enable = true;
|
||||||
};
|
};
|
||||||
resolved = {
|
resolved.settings.Resolve = {
|
||||||
llmnr = "false";
|
LLMNR = false;
|
||||||
extraConfig = ''
|
MulticastDNS = false;
|
||||||
MulticastDNS=false
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
netdata.enable = true;
|
netdata.enable = true;
|
||||||
|
|
||||||
@@ -163,12 +162,48 @@ in
|
|||||||
};
|
};
|
||||||
wireguardPeers = [
|
wireguardPeers = [
|
||||||
{
|
{
|
||||||
wireguardPeerConfig = {
|
|
||||||
PublicKey = "7N9YdQaCMWWIwAnW37vrthm9ZpbnG4Lx3gheHeRYz2E=";
|
PublicKey = "7N9YdQaCMWWIwAnW37vrthm9ZpbnG4Lx3gheHeRYz2E=";
|
||||||
AllowedIPs = [ allAssignments.kelder.estuary.ipv4.address ];
|
AllowedIPs = [ allAssignments.kelder.estuary.ipv4.address ];
|
||||||
PersistentKeepalive = 25;
|
PersistentKeepalive = 25;
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
"30-hillcrest" = {
|
||||||
|
netdevConfig = {
|
||||||
|
Name = "hillcrest";
|
||||||
|
Kind = "wireguard";
|
||||||
|
};
|
||||||
|
wireguardConfig = {
|
||||||
|
PrivateKeyFile = config.age.secrets."estuary/hillcrest-wg.key".path;
|
||||||
|
ListenPort = lib.my.c.hillcrest.vpn.port;
|
||||||
|
};
|
||||||
|
wireguardPeers = [
|
||||||
|
{
|
||||||
|
PublicKey = "+67Ks+ZRk1ssNCfg5BFKmIE9NtLasAxRE6XMqufx5GY=";
|
||||||
|
AllowedIPs = [ (net.cidr.host 2 prefixes.hillcrest.v4) ];
|
||||||
|
PersistentKeepalive = 25;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"30-john-valorant" = {
|
||||||
|
netdevConfig = {
|
||||||
|
Name = "john-valorant";
|
||||||
|
Kind = "wireguard";
|
||||||
|
};
|
||||||
|
wireguardConfig = {
|
||||||
|
PrivateKeyFile = config.age.secrets."estuary/john-valorant-wg.key".path;
|
||||||
|
ListenPort = lib.my.c.john-valorant.vpn.port;
|
||||||
|
};
|
||||||
|
wireguardPeers = [
|
||||||
|
{
|
||||||
|
PublicKey = "xyqKF0yOAv1bObN1paL2vATFh77pdFfvN+JmuAxaTCk=";
|
||||||
|
AllowedIPs = [ (net.cidr.host 2 prefixes.john-valorant.v4) ];
|
||||||
|
PersistentKeepalive = 25;
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -219,7 +254,7 @@ in
|
|||||||
in
|
in
|
||||||
mkMerge
|
mkMerge
|
||||||
[
|
[
|
||||||
(mkIXPConfig "frys-ix" "185.1.203.196/24" "2001:7f8:10f::3:3850:196/64")
|
(mkIXPConfig "frys-ix" "185.1.160.196/23" "2001:7f8:10f::3:3850:196/64")
|
||||||
(mkIXPConfig "nl-ix" "193.239.116.145/22" "2001:7f8:13::a521:1024:1/64")
|
(mkIXPConfig "nl-ix" "193.239.116.145/22" "2001:7f8:13::a521:1024:1/64")
|
||||||
(mkIXPConfig "fogixp" "185.1.147.159/24" "2001:7f8:ca:1::159/64")
|
(mkIXPConfig "fogixp" "185.1.147.159/24" "2001:7f8:ca:1::159/64")
|
||||||
{
|
{
|
||||||
@@ -277,11 +312,10 @@ in
|
|||||||
};
|
};
|
||||||
ipv6Prefixes = [
|
ipv6Prefixes = [
|
||||||
{
|
{
|
||||||
ipv6PrefixConfig.Prefix = prefixes.base.v6;
|
Prefix = prefixes.base.v6;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
routes = map (r: { routeConfig = r; }) (flatten
|
routes = flatten ([
|
||||||
([
|
|
||||||
{
|
{
|
||||||
Destination = prefixes.vip1;
|
Destination = prefixes.vip1;
|
||||||
Gateway = allAssignments.colony.routing.ipv4.address;
|
Gateway = allAssignments.colony.routing.ipv4.address;
|
||||||
@@ -307,6 +341,11 @@ in
|
|||||||
Destination = lib.my.c.tailscale.prefix.v6;
|
Destination = lib.my.c.tailscale.prefix.v6;
|
||||||
Gateway = allAssignments.colony.internal.ipv6.address;
|
Gateway = allAssignments.colony.internal.ipv6.address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Destination = prefixes.qclk.v4;
|
||||||
|
Gateway = allAssignments.colony.routing.ipv4.address;
|
||||||
|
}
|
||||||
] ++
|
] ++
|
||||||
(map (pName: [
|
(map (pName: [
|
||||||
{
|
{
|
||||||
@@ -317,7 +356,7 @@ in
|
|||||||
Destination = prefixes."${pName}".v6;
|
Destination = prefixes."${pName}".v6;
|
||||||
Gateway = allAssignments.colony.internal.ipv6.address;
|
Gateway = allAssignments.colony.internal.ipv6.address;
|
||||||
}
|
}
|
||||||
]) [ "vms" "ctrs" "oci" ])));
|
]) [ "vms" "ctrs" "oci" ]));
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -326,7 +365,7 @@ in
|
|||||||
{
|
{
|
||||||
matchConfig.Name = "as211024";
|
matchConfig.Name = "as211024";
|
||||||
networkConfig.IPv6AcceptRA = mkForce false;
|
networkConfig.IPv6AcceptRA = mkForce false;
|
||||||
routes = map (r: { routeConfig = r; }) [
|
routes = [
|
||||||
{
|
{
|
||||||
Destination = lib.my.c.home.prefixes.all.v4;
|
Destination = lib.my.c.home.prefixes.all.v4;
|
||||||
Gateway = lib.my.c.home.vips.as211024.v4;
|
Gateway = lib.my.c.home.vips.as211024.v4;
|
||||||
@@ -338,10 +377,28 @@ in
|
|||||||
matchConfig.Name = "kelder";
|
matchConfig.Name = "kelder";
|
||||||
routes = [
|
routes = [
|
||||||
{
|
{
|
||||||
routeConfig = {
|
|
||||||
Destination = allAssignments.kelder.estuary.ipv4.address;
|
Destination = allAssignments.kelder.estuary.ipv4.address;
|
||||||
Scope = "link";
|
Scope = "link";
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
"95-hillcrest" = {
|
||||||
|
matchConfig.Name = "hillcrest";
|
||||||
|
address = [ "${net.cidr.host 1 prefixes.hillcrest.v4}/32" ];
|
||||||
|
routes = [
|
||||||
|
{
|
||||||
|
Destination = net.cidr.host 2 prefixes.hillcrest.v4;
|
||||||
|
Scope = "link";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"95-john-valorant" = {
|
||||||
|
matchConfig.Name = "john-valorant";
|
||||||
|
address = [ "${net.cidr.host 1 prefixes.john-valorant.v4}/32" ];
|
||||||
|
routes = [
|
||||||
|
{
|
||||||
|
Destination = net.cidr.host 2 prefixes.john-valorant.v4;
|
||||||
|
Scope = "link";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -355,6 +412,12 @@ in
|
|||||||
"estuary/kelder-wg.key" = {
|
"estuary/kelder-wg.key" = {
|
||||||
owner = "systemd-network";
|
owner = "systemd-network";
|
||||||
};
|
};
|
||||||
|
"estuary/hillcrest-wg.key" = {
|
||||||
|
owner = "systemd-network";
|
||||||
|
};
|
||||||
|
"estuary/john-valorant-wg.key" = {
|
||||||
|
owner = "systemd-network";
|
||||||
|
};
|
||||||
"l2mesh/as211024.key" = {};
|
"l2mesh/as211024.key" = {};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -366,7 +429,13 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
firewall = {
|
firewall = {
|
||||||
udp.allowed = [ 5353 lib.my.c.kelder.vpn.port ];
|
udp.allowed = [
|
||||||
|
5353
|
||||||
|
|
||||||
|
lib.my.c.kelder.vpn.port
|
||||||
|
lib.my.c.hillcrest.vpn.port
|
||||||
|
lib.my.c.john-valorant.vpn.port
|
||||||
|
];
|
||||||
tcp.allowed = [ 5353 "bgp" ];
|
tcp.allowed = [ 5353 "bgp" ];
|
||||||
nat = {
|
nat = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -393,16 +462,28 @@ in
|
|||||||
# Safe enough to allow all SSH
|
# Safe enough to allow all SSH
|
||||||
tcp dport ssh accept
|
tcp dport ssh accept
|
||||||
|
|
||||||
|
# jam-ctr forwards
|
||||||
|
ip daddr ${aa.shill.internal.ipv4.address} tcp dport 60022 accept
|
||||||
|
|
||||||
ip6 daddr ${aa.middleman.internal.ipv6.address} tcp dport { http, https, 8448 } accept
|
ip6 daddr ${aa.middleman.internal.ipv6.address} tcp dport { http, https, 8448 } accept
|
||||||
${matchInet "tcp dport { http, https } accept" "git"}
|
${matchInet "tcp dport { http, https } accept" "git"}
|
||||||
ip6 daddr ${aa.simpcraft-oci.internal.ipv6.address} tcp dport { 25565, 25575 } accept
|
ip6 daddr ${aa.simpcraft-oci.internal.ipv6.address} tcp dport 25565 accept
|
||||||
ip6 daddr ${aa.simpcraft-staging-oci.internal.ipv6.address} tcp dport 25565 accept
|
ip6 daddr ${aa.simpcraft-staging-oci.internal.ipv6.address} tcp dport 25565 accept
|
||||||
|
ip6 daddr ${aa.kevcraft-oci.internal.ipv6.address} tcp dport 25567 accept
|
||||||
|
ip6 daddr ${aa.kinkcraft-oci.internal.ipv6.address} tcp dport 25568 accept
|
||||||
|
ip6 daddr ${aa.graeme-oci.internal.ipv6.address} tcp dport 25569 accept
|
||||||
|
ip6 daddr ${aa.gam.internal.ipv6.address} tcp dport 7777 accept
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
chain routing-udp {
|
chain routing-udp {
|
||||||
ip6 daddr ${aa.valheim-oci.internal.ipv6.address} udp dport { 2456-2457 } accept
|
ip6 daddr ${aa.valheim-oci.internal.ipv6.address} udp dport { 2456-2457 } accept
|
||||||
ip6 daddr ${aa.waffletail.internal.ipv6.address} udp dport 41641 accept
|
ip6 daddr ${aa.waffletail.internal.ipv6.address} udp dport 41641 accept
|
||||||
ip6 daddr ${aa.simpcraft-oci.internal.ipv6.address} udp dport 25565 accept
|
ip6 daddr ${aa.simpcraft-oci.internal.ipv6.address} udp dport 25565 accept
|
||||||
|
ip6 daddr ${aa.enshrouded-oci.internal.ipv6.address} udp dport { 15636-15637 } accept
|
||||||
|
ip6 daddr ${aa.kevcraft-oci.internal.ipv6.address} udp dport 25567 accept
|
||||||
|
ip6 daddr ${aa.kinkcraft-oci.internal.ipv6.address} udp dport 25568 accept
|
||||||
|
ip6 daddr ${aa.graeme-oci.internal.ipv6.address} udp dport 25569 accept
|
||||||
|
ip6 daddr ${aa.gam.internal.ipv6.address} udp dport 7777 accept
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
chain filter-routing {
|
chain filter-routing {
|
||||||
@@ -423,7 +504,7 @@ in
|
|||||||
iifname { wan, as211024, $ixps } oifname base jump filter-routing
|
iifname { wan, as211024, $ixps } oifname base jump filter-routing
|
||||||
oifname $ixps jump ixp
|
oifname $ixps jump ixp
|
||||||
iifname base oifname { base, wan, $ixps } accept
|
iifname base oifname { base, wan, $ixps } accept
|
||||||
oifname { as211024, kelder } accept
|
oifname { as211024, kelder, hillcrest, john-valorant } accept
|
||||||
}
|
}
|
||||||
chain output {
|
chain output {
|
||||||
oifname ifog ether type != vlan reject
|
oifname ifog ether type != vlan reject
|
||||||
@@ -435,6 +516,8 @@ in
|
|||||||
${matchInet "meta l4proto { udp, tcp } th dport domain redirect to :5353" "estuary"}
|
${matchInet "meta l4proto { udp, tcp } th dport domain redirect to :5353" "estuary"}
|
||||||
}
|
}
|
||||||
chain postrouting {
|
chain postrouting {
|
||||||
|
oifname hillcrest snat ip to ${net.cidr.host 1 prefixes.hillcrest.v4}
|
||||||
|
oifname john-valorant snat ip to ${net.cidr.host 1 prefixes.john-valorant.v4}
|
||||||
ip saddr ${prefixes.all.v4} oifname != as211024 snat to ${assignments.internal.ipv4.address}
|
ip saddr ${prefixes.all.v4} oifname != as211024 snat to ${assignments.internal.ipv4.address}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
let
|
let
|
||||||
inherit (builtins) attrNames;
|
inherit (builtins) attrNames;
|
||||||
inherit (lib.my) net;
|
inherit (lib.my) net;
|
||||||
inherit (lib.my.c.colony) prefixes;
|
inherit (lib.my.c.colony) prefixes custRouting;
|
||||||
|
|
||||||
authZones = attrNames config.my.pdns.auth.bind.zones;
|
authZones = attrNames config.my.pdns.auth.bind.zones;
|
||||||
in
|
in
|
||||||
@@ -14,7 +14,7 @@ in
|
|||||||
owner = "pdns";
|
owner = "pdns";
|
||||||
group = "pdns";
|
group = "pdns";
|
||||||
};
|
};
|
||||||
"estuary/pdns/recursor.conf" = {
|
"estuary/pdns/recursor.yml" = {
|
||||||
owner = "pdns-recursor";
|
owner = "pdns-recursor";
|
||||||
group = "pdns-recursor";
|
group = "pdns-recursor";
|
||||||
};
|
};
|
||||||
@@ -31,7 +31,7 @@ in
|
|||||||
|
|
||||||
pdns.recursor = {
|
pdns.recursor = {
|
||||||
enable = true;
|
enable = true;
|
||||||
extraSettingsFile = config.age.secrets."estuary/pdns/recursor.conf".path;
|
extraSettingsFile = config.age.secrets."estuary/pdns/recursor.yml".path;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,34 +44,37 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
pdns-recursor = {
|
pdns-recursor = {
|
||||||
dns = {
|
settings = {
|
||||||
address = [
|
incoming = {
|
||||||
|
listen = [
|
||||||
"127.0.0.1" "::1"
|
"127.0.0.1" "::1"
|
||||||
assignments.base.ipv4.address assignments.base.ipv6.address
|
assignments.base.ipv4.address assignments.base.ipv6.address
|
||||||
];
|
];
|
||||||
allowFrom = [
|
allow_from = [
|
||||||
"127.0.0.0/8" "::1/128"
|
"127.0.0.0/8" "::1/128"
|
||||||
prefixes.all.v4 prefixes.all.v6
|
prefixes.all.v4 prefixes.all.v6
|
||||||
] ++ (with lib.my.c.tailscale.prefix; [ v4 v6 ]);
|
] ++ (with lib.my.c.tailscale.prefix; [ v4 v6 ]);
|
||||||
|
|
||||||
|
# DNS NOTIFY messages override TTL
|
||||||
|
allow_notify_for = authZones;
|
||||||
|
allow_notify_from = [ "127.0.0.0/8" "::1/128" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
settings = {
|
outgoing = {
|
||||||
query-local-address = [
|
source_address = [
|
||||||
assignments.internal.ipv4.address
|
assignments.internal.ipv4.address
|
||||||
assignments.internal.ipv6.address
|
assignments.internal.ipv6.address
|
||||||
assignments.base.ipv6.address
|
assignments.base.ipv6.address
|
||||||
];
|
];
|
||||||
forward-zones = map (z: "${z}=127.0.0.1:5353") authZones;
|
};
|
||||||
|
|
||||||
# DNS NOTIFY messages override TTL
|
recursor = {
|
||||||
allow-notify-for = authZones;
|
forward_zones = map (z: {
|
||||||
allow-notify-from = [ "127.0.0.0/8" "::1/128" ];
|
zone = z;
|
||||||
|
forwarders = [ "127.0.0.1:5353" ];
|
||||||
|
}) authZones;
|
||||||
|
|
||||||
webserver = true;
|
lua_dns_script = pkgs.writeText "pdns-script.lua" ''
|
||||||
webserver-address = "::";
|
|
||||||
webserver-allow-from = [ "127.0.0.1" "::1" ];
|
|
||||||
|
|
||||||
lua-dns-script = pkgs.writeText "pdns-script.lua" ''
|
|
||||||
function preresolve(dq)
|
function preresolve(dq)
|
||||||
if dq.qname:equal("nix-cache.nul.ie") then
|
if dq.qname:equal("nix-cache.nul.ie") then
|
||||||
dq:addAnswer(pdns.CNAME, "http.${config.networking.domain}.")
|
dq:addAnswer(pdns.CNAME, "http.${config.networking.domain}.")
|
||||||
@@ -84,6 +87,13 @@ in
|
|||||||
end
|
end
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
webservice = {
|
||||||
|
webserver = true;
|
||||||
|
address = "::";
|
||||||
|
allow_from = [ "127.0.0.1" "::1" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -153,6 +163,15 @@ in
|
|||||||
simpcraft IN AAAA ${allAssignments.simpcraft-oci.internal.ipv6.address}
|
simpcraft IN AAAA ${allAssignments.simpcraft-oci.internal.ipv6.address}
|
||||||
simpcraft-staging IN A ${assignments.internal.ipv4.address}
|
simpcraft-staging IN A ${assignments.internal.ipv4.address}
|
||||||
simpcraft-staging IN AAAA ${allAssignments.simpcraft-staging-oci.internal.ipv6.address}
|
simpcraft-staging IN AAAA ${allAssignments.simpcraft-staging-oci.internal.ipv6.address}
|
||||||
|
enshrouded IN A ${assignments.internal.ipv4.address}
|
||||||
|
kevcraft IN A ${assignments.internal.ipv4.address}
|
||||||
|
kevcraft IN AAAA ${allAssignments.kevcraft-oci.internal.ipv6.address}
|
||||||
|
kinkcraft IN A ${assignments.internal.ipv4.address}
|
||||||
|
kinkcraft IN AAAA ${allAssignments.kinkcraft-oci.internal.ipv6.address}
|
||||||
|
graeme IN A ${assignments.internal.ipv4.address}
|
||||||
|
graeme IN AAAA ${allAssignments.graeme-oci.internal.ipv6.address}
|
||||||
|
terraria IN A ${assignments.internal.ipv4.address}
|
||||||
|
terraria IN AAAA ${allAssignments.gam.internal.ipv6.address}
|
||||||
|
|
||||||
mail-vm IN A ${net.cidr.host 0 prefixes.mail.v4}
|
mail-vm IN A ${net.cidr.host 0 prefixes.mail.v4}
|
||||||
mail-vm IN AAAA ${net.cidr.host 1 prefixes.mail.v6}
|
mail-vm IN AAAA ${net.cidr.host 1 prefixes.mail.v6}
|
||||||
@@ -162,6 +181,13 @@ in
|
|||||||
|
|
||||||
andrey-cust IN A ${allAssignments.kelder.estuary.ipv4.address}
|
andrey-cust IN A ${allAssignments.kelder.estuary.ipv4.address}
|
||||||
|
|
||||||
|
jam-cust IN A ${net.cidr.host 0 prefixes.jam.v4}
|
||||||
|
jam-fwd IN A ${allAssignments.shill.internal.ipv4.address}
|
||||||
|
jam-cust IN AAAA ${net.cidr.host 1 prefixes.jam.v6}
|
||||||
|
|
||||||
|
hillcrest-tun IN A ${net.cidr.host 2 prefixes.hillcrest.v4}
|
||||||
|
john-valorant-tun IN A ${net.cidr.host 2 prefixes.john-valorant.v4}
|
||||||
|
|
||||||
$TTL 3
|
$TTL 3
|
||||||
_acme-challenge IN LUA TXT @@FILE@@
|
_acme-challenge IN LUA TXT @@FILE@@
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ let
|
|||||||
inherit (lib) mkMerge mkDefault;
|
inherit (lib) mkMerge mkDefault;
|
||||||
inherit (lib.my) net;
|
inherit (lib.my) net;
|
||||||
inherit (lib.my.c) pubDomain;
|
inherit (lib.my.c) pubDomain;
|
||||||
inherit (lib.my.c.colony) domain prefixes;
|
inherit (lib.my.c.colony) domain prefixes firewallForwards;
|
||||||
inherit (lib.my.c.nginx) baseHttpConfig proxyHeaders;
|
inherit (lib.my.c.nginx) baseHttpConfig proxyHeaders;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -95,7 +95,7 @@ in
|
|||||||
"*.${pubDomain}"
|
"*.${pubDomain}"
|
||||||
];
|
];
|
||||||
dnsProvider = "cloudflare";
|
dnsProvider = "cloudflare";
|
||||||
credentialsFile = config.age.secrets."middleman/cloudflare-credentials.conf".path;
|
environmentFile = config.age.secrets."middleman/cloudflare-credentials.conf".path;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -111,7 +111,6 @@ in
|
|||||||
recommendedTlsSettings = true;
|
recommendedTlsSettings = true;
|
||||||
clientMaxBodySize = "0";
|
clientMaxBodySize = "0";
|
||||||
serverTokens = true;
|
serverTokens = true;
|
||||||
sslDhparam = config.age.secrets."dhparams.pem".path;
|
|
||||||
|
|
||||||
# Based on recommended*Settings, but probably better to be explicit about these
|
# Based on recommended*Settings, but probably better to be explicit about these
|
||||||
appendHttpConfig = ''
|
appendHttpConfig = ''
|
||||||
@@ -182,11 +181,6 @@ in
|
|||||||
secrets = {
|
secrets = {
|
||||||
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP+KINpHLMduBuW96JzfSRDLUzkI+XaCBghu5/wHiW5R";
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP+KINpHLMduBuW96JzfSRDLUzkI+XaCBghu5/wHiW5R";
|
||||||
files = {
|
files = {
|
||||||
"dhparams.pem" = {
|
|
||||||
owner = "acme";
|
|
||||||
group = "acme";
|
|
||||||
mode = "440";
|
|
||||||
};
|
|
||||||
"middleman/cloudflare-credentials.conf" = {
|
"middleman/cloudflare-credentials.conf" = {
|
||||||
owner = "acme";
|
owner = "acme";
|
||||||
group = "acme";
|
group = "acme";
|
||||||
@@ -197,6 +191,7 @@ in
|
|||||||
|
|
||||||
firewall = {
|
firewall = {
|
||||||
tcp.allowed = [ 19999 "http" "https" ];
|
tcp.allowed = [ 19999 "http" "https" ];
|
||||||
|
nat.forwardPorts."${allAssignments.estuary.internal.ipv4.address}" = firewallForwards allAssignments;
|
||||||
extraRules = ''
|
extraRules = ''
|
||||||
table inet filter {
|
table inet filter {
|
||||||
chain forward {
|
chain forward {
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
{ lib, pkgs, config, ... }:
|
{ lib, pkgs, config, ... }:
|
||||||
let
|
let
|
||||||
inherit (builtins) toJSON;
|
|
||||||
inherit (lib) mkForce;
|
inherit (lib) mkForce;
|
||||||
inherit (lib.my.c) pubDomain;
|
inherit (lib.my.c) pubDomain;
|
||||||
|
|
||||||
cfgFile = pkgs.writeText "gitea-actions-runner.yaml" (toJSON {
|
|
||||||
container = {
|
|
||||||
network = "podman";
|
|
||||||
privileged = true;
|
|
||||||
};
|
|
||||||
cache = {
|
|
||||||
enabled = true;
|
|
||||||
dir = "/var/cache/gitea-runner";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = {
|
config = {
|
||||||
@@ -30,11 +18,24 @@ in
|
|||||||
enable = true;
|
enable = true;
|
||||||
name = "main-docker";
|
name = "main-docker";
|
||||||
labels = [
|
labels = [
|
||||||
"debian-node-bullseye:docker://node:18-bullseye"
|
"debian-node-trixie:docker://node:24-trixie"
|
||||||
"ubuntu-22.04:docker://git.nul.ie/dev/actions-ubuntu:22.04"
|
"ubuntu-26.04:docker://git.nul.ie/dev/actions-ubuntu:26.04"
|
||||||
];
|
];
|
||||||
url = "https://git.${pubDomain}";
|
url = "https://git.${pubDomain}";
|
||||||
tokenFile = config.age.secrets."gitea/actions-runner.env".path;
|
tokenFile = config.age.secrets."gitea/actions-runner.env".path;
|
||||||
|
settings = {
|
||||||
|
runner = {
|
||||||
|
timeout = "8h";
|
||||||
|
};
|
||||||
|
container = {
|
||||||
|
network = "podman";
|
||||||
|
privileged = true;
|
||||||
|
};
|
||||||
|
cache = {
|
||||||
|
enabled = true;
|
||||||
|
dir = "/var/cache/gitea-runner";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -61,7 +62,6 @@ in
|
|||||||
DynamicUser = mkForce false;
|
DynamicUser = mkForce false;
|
||||||
User = "gitea-runner";
|
User = "gitea-runner";
|
||||||
Group = "gitea-runner";
|
Group = "gitea-runner";
|
||||||
ExecStart = mkForce "${config.services.gitea-actions-runner.package}/bin/act_runner -c ${cfgFile} daemon";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
{ lib, pkgs, assignments, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib.my) net;
|
||||||
|
inherit (lib.my.c.colony) prefixes custRouting;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
fileSystems = {
|
||||||
|
"/mnt/jam" = {
|
||||||
|
device = "/dev/disk/by-label/jam";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
|
||||||
|
"/var/lib/machines/jam" = {
|
||||||
|
device = "/mnt/jam";
|
||||||
|
options = [ "bind" ];
|
||||||
|
fsType = "none";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd = {
|
||||||
|
nspawn = {
|
||||||
|
jam = {
|
||||||
|
enable = true;
|
||||||
|
execConfig = {
|
||||||
|
Boot = true;
|
||||||
|
PrivateUsers = "pick";
|
||||||
|
LinkJournal = false;
|
||||||
|
};
|
||||||
|
networkConfig = {
|
||||||
|
Private = true;
|
||||||
|
VirtualEthernet = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
network.networks = {
|
||||||
|
"50-ve-jam" = {
|
||||||
|
matchConfig = {
|
||||||
|
Kind = "veth";
|
||||||
|
Name = "ve-jam";
|
||||||
|
};
|
||||||
|
address = [
|
||||||
|
custRouting.jam-ctr
|
||||||
|
prefixes.jam.v6
|
||||||
|
];
|
||||||
|
networkConfig = {
|
||||||
|
IPv6AcceptRA = false;
|
||||||
|
IPv6SendRA = true;
|
||||||
|
};
|
||||||
|
ipv6Prefixes = [
|
||||||
|
{
|
||||||
|
Prefix = prefixes.jam.v6;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
routes = [
|
||||||
|
{
|
||||||
|
Destination = prefixes.jam.v4;
|
||||||
|
Scope = "link";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
services = {
|
||||||
|
"systemd-nspawn@jam" = {
|
||||||
|
overrideStrategy = "asDropin";
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
CPUQuota = "400%";
|
||||||
|
MemoryHigh = "infinity";
|
||||||
|
MemoryMax = "4G";
|
||||||
|
};
|
||||||
|
|
||||||
|
wantedBy = [ "machines.target" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
my = {
|
||||||
|
firewall =
|
||||||
|
let
|
||||||
|
jamIP = net.cidr.host 0 prefixes.jam.v4;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
nat.forwardPorts."${assignments.internal.ipv4.address}" = [
|
||||||
|
{
|
||||||
|
port = 60022;
|
||||||
|
dst = jamIP;
|
||||||
|
dstPort = "ssh";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
extraRules = ''
|
||||||
|
table inet filter {
|
||||||
|
chain forward {
|
||||||
|
iifname { ve-jam } oifname vms accept
|
||||||
|
iifname vms oifname { ve-jam } accept
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table inet nat {
|
||||||
|
chain postrouting {
|
||||||
|
ip saddr ${jamIP} snat to ${assignments.internal.ipv4.address}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ in
|
|||||||
|
|
||||||
configuration = { lib, pkgs, config, assignments, allAssignments, ... }:
|
configuration = { lib, pkgs, config, assignments, allAssignments, ... }:
|
||||||
let
|
let
|
||||||
inherit (lib) mkMerge mkIf mkForce;
|
inherit (lib) genAttrs mkMerge mkIf mkForce;
|
||||||
inherit (lib.my) networkdAssignment;
|
inherit (lib.my) networkdAssignment;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -45,9 +45,22 @@ in
|
|||||||
owner = "matrix-synapse";
|
owner = "matrix-synapse";
|
||||||
group = "matrix-synapse";
|
group = "matrix-synapse";
|
||||||
};
|
};
|
||||||
"chatterbox/syncv3.env" = {
|
"chatterbox/doublepuppet.yaml" = {
|
||||||
owner = "matrix-syncv3";
|
owner = "matrix-synapse";
|
||||||
group = "matrix-syncv3";
|
group = "matrix-synapse";
|
||||||
|
};
|
||||||
|
|
||||||
|
"chatterbox/mautrix-whatsapp.env" = {
|
||||||
|
owner = "mautrix-whatsapp";
|
||||||
|
group = "mautrix-whatsapp";
|
||||||
|
};
|
||||||
|
"chatterbox/mautrix-messenger.env" = {
|
||||||
|
owner = "mautrix-meta-messenger";
|
||||||
|
group = "mautrix-meta";
|
||||||
|
};
|
||||||
|
"chatterbox/mautrix-instagram.env" = {
|
||||||
|
owner = "mautrix-meta-instagram";
|
||||||
|
group = "mautrix-meta";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -59,29 +72,24 @@ in
|
|||||||
|
|
||||||
users = with lib.my.c.ids; {
|
users = with lib.my.c.ids; {
|
||||||
users = {
|
users = {
|
||||||
matrix-syncv3 = {
|
matrix-synapse.extraGroups = [
|
||||||
isSystemUser = true;
|
"mautrix-whatsapp"
|
||||||
uid = uids.matrix-syncv3;
|
];
|
||||||
group = "matrix-syncv3";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
groups = {
|
|
||||||
matrix-syncv3.gid = gids.matrix-syncv3;
|
|
||||||
};
|
};
|
||||||
|
groups = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
network.networks."80-container-host0" = networkdAssignment "host0" assignments.internal;
|
network.networks."80-container-host0" = networkdAssignment "host0" assignments.internal;
|
||||||
services = {
|
services = { } // (genAttrs [ "mautrix-whatsapp" "mautrix-meta-messenger" "mautrix-meta-instagram" ] (_: {
|
||||||
matrix-sliding-sync.serviceConfig = {
|
# ffmpeg needed to convert GIFs to video
|
||||||
# Needs to be able to read its secrets
|
path = with pkgs; [ ffmpeg ];
|
||||||
DynamicUser = mkForce false;
|
}));
|
||||||
User = "matrix-syncv3";
|
|
||||||
Group = "matrix-syncv3";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# TODO/FIXME: https://github.com/NixOS/nixpkgs/issues/336052
|
||||||
|
nixpkgs.config.permittedInsecurePackages = [ "olm-3.2.16" ];
|
||||||
|
|
||||||
services = {
|
services = {
|
||||||
netdata.enable = true;
|
netdata.enable = true;
|
||||||
matrix-synapse = {
|
matrix-synapse = {
|
||||||
@@ -168,18 +176,10 @@ in
|
|||||||
|
|
||||||
app_service_config_files = [
|
app_service_config_files = [
|
||||||
"/var/lib/heisenbridge/registration.yml"
|
"/var/lib/heisenbridge/registration.yml"
|
||||||
|
config.age.secrets."chatterbox/doublepuppet.yaml".path
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
sliding-sync = {
|
|
||||||
enable = true;
|
|
||||||
createDatabase = false;
|
|
||||||
environmentFile = config.age.secrets."chatterbox/syncv3.env".path;
|
|
||||||
settings = {
|
|
||||||
SYNCV3_BINDADDR = "[::]:8009";
|
|
||||||
SYNCV3_SERVER = "http://localhost:8008";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
heisenbridge = {
|
heisenbridge = {
|
||||||
@@ -195,6 +195,181 @@ in
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mautrix-whatsapp = {
|
||||||
|
enable = true;
|
||||||
|
# package = pkgs.mautrix-whatsapp.overrideAttrs (o: rec {
|
||||||
|
# version = "26.05";
|
||||||
|
# tag = "v0.2605.0";
|
||||||
|
# src = pkgs.fetchFromGitHub {
|
||||||
|
# owner = "mautrix";
|
||||||
|
# repo = "whatsapp";
|
||||||
|
# inherit tag;
|
||||||
|
# hash = "sha256-WlVfGQoP9e/wl98hUJei8O2JMcOKijoEY8XuU/z69Qk=";
|
||||||
|
# };
|
||||||
|
# vendorHash = "sha256-Hi/dZHJHoTTCnxLXgbkcYzuzis4fl5kxb5wMd9fKTY8=";
|
||||||
|
# });
|
||||||
|
environmentFile = config.age.secrets."chatterbox/mautrix-whatsapp.env".path;
|
||||||
|
settings = {
|
||||||
|
database = {
|
||||||
|
type = "postgres";
|
||||||
|
uri = "$MAU_WAPP_PSQL_URI";
|
||||||
|
};
|
||||||
|
homeserver = {
|
||||||
|
address = "http://localhost:8008";
|
||||||
|
domain = "nul.ie";
|
||||||
|
};
|
||||||
|
appservice = {
|
||||||
|
id = "whatsapp2";
|
||||||
|
bot = {
|
||||||
|
username = "whatsapp2";
|
||||||
|
displayname = "WhatsApp Bridge Bot";
|
||||||
|
};
|
||||||
|
username_template = "wapp2_{{.}}";
|
||||||
|
};
|
||||||
|
bridge = {
|
||||||
|
personal_filtering_spaces = true;
|
||||||
|
command_prefix = "!wa";
|
||||||
|
permissions = {
|
||||||
|
"@dev:nul.ie" = "admin";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
double_puppet = {
|
||||||
|
secrets."nul.ie" = "$MAU_WAPP_DOUBLE_PUPPET_TOKEN";
|
||||||
|
};
|
||||||
|
encryption = {
|
||||||
|
allow = true;
|
||||||
|
default = true;
|
||||||
|
require = true;
|
||||||
|
pickle_key = "maunium.net/go/mautrix-whatsapp";
|
||||||
|
};
|
||||||
|
matrix = {
|
||||||
|
delivery_receipts = true;
|
||||||
|
};
|
||||||
|
network = {
|
||||||
|
displayname_template = ''{{or .BusinessName .PushName .FullName .Phone "Unknown user"}} (WA)'';
|
||||||
|
url_previews = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# mautrix-meta.package = pkgs.mautrix-meta.overrideAttrs (o: rec {
|
||||||
|
# version = "26.05.1";
|
||||||
|
# tag = "v0.2605.1";
|
||||||
|
# src = pkgs.fetchFromGitHub {
|
||||||
|
# owner = "mautrix";
|
||||||
|
# repo = "meta";
|
||||||
|
# inherit tag;
|
||||||
|
# hash = "sha256-zpolDtwGulDTiojJPnkj9O0D5b4rgPYQX6A28rvuvM0=";
|
||||||
|
# };
|
||||||
|
# vendorHash = "sha256-+i45bXBhlXPXX24VMS9IJLLX+i4VPnqy5RAH4j88sTA=";
|
||||||
|
# });
|
||||||
|
mautrix-meta.instances = {
|
||||||
|
messenger = {
|
||||||
|
enable = true;
|
||||||
|
registerToSynapse = true;
|
||||||
|
dataDir = "mautrix-messenger";
|
||||||
|
environmentFile = config.age.secrets."chatterbox/mautrix-messenger.env".path;
|
||||||
|
settings = {
|
||||||
|
database = {
|
||||||
|
type = "postgres";
|
||||||
|
uri = "$MAU_FBM_PSQL_URI";
|
||||||
|
};
|
||||||
|
homeserver = {
|
||||||
|
address = "http://localhost:8008";
|
||||||
|
domain = "nul.ie";
|
||||||
|
};
|
||||||
|
appservice = {
|
||||||
|
id = "fbm2";
|
||||||
|
bot = {
|
||||||
|
username = "messenger2";
|
||||||
|
displayname = "Messenger Bridge Bot";
|
||||||
|
avatar = "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak";
|
||||||
|
};
|
||||||
|
username_template = "fbm2_{{.}}";
|
||||||
|
};
|
||||||
|
network = {
|
||||||
|
mode = "messenger";
|
||||||
|
displayname_template = ''{{or .DisplayName .Username "Unknown user"}} (FBM)'';
|
||||||
|
};
|
||||||
|
bridge = {
|
||||||
|
personal_filtering_spaces = true;
|
||||||
|
# management_room_text.welcome = "Hello, I'm a Messenger bridge bot.";
|
||||||
|
command_prefix = "!fbm";
|
||||||
|
backfill = {
|
||||||
|
enabled = true;
|
||||||
|
};
|
||||||
|
permissions = {
|
||||||
|
"@dev:nul.ie" = "admin";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
double_puppet = {
|
||||||
|
secrets."nul.ie" = "$MAU_FBM_DOUBLE_PUPPET_TOKEN";
|
||||||
|
};
|
||||||
|
encryption = {
|
||||||
|
allow = true;
|
||||||
|
default = true;
|
||||||
|
require = true;
|
||||||
|
};
|
||||||
|
matrix = {
|
||||||
|
delivery_receipts = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
instagram = {
|
||||||
|
enable = true;
|
||||||
|
registerToSynapse = true;
|
||||||
|
dataDir = "mautrix-instagram";
|
||||||
|
environmentFile = config.age.secrets."chatterbox/mautrix-instagram.env".path;
|
||||||
|
settings = {
|
||||||
|
database = {
|
||||||
|
type = "postgres";
|
||||||
|
uri = "$MAU_IG_PSQL_URI";
|
||||||
|
};
|
||||||
|
homeserver = {
|
||||||
|
address = "http://localhost:8008";
|
||||||
|
domain = "nul.ie";
|
||||||
|
};
|
||||||
|
appservice = {
|
||||||
|
id = "instagram";
|
||||||
|
bot = {
|
||||||
|
username = "instagram";
|
||||||
|
displayname = "Instagram Bridge Bot";
|
||||||
|
avatar = "mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv";
|
||||||
|
};
|
||||||
|
username_template = "ig_{{.}}";
|
||||||
|
};
|
||||||
|
network = {
|
||||||
|
mode = "instagram";
|
||||||
|
displayname_template = ''{{or .DisplayName .Username "Unknown user"}} (IG)'';
|
||||||
|
};
|
||||||
|
bridge = {
|
||||||
|
personal_filtering_spaces = true;
|
||||||
|
# management_room_text.welcome = "Hello, I'm an Instagram bridge bot.";
|
||||||
|
command_prefix = "!ig";
|
||||||
|
backfill = {
|
||||||
|
enabled = true;
|
||||||
|
};
|
||||||
|
permissions = {
|
||||||
|
"@dev:nul.ie" = "admin";
|
||||||
|
"@adzerq:nul.ie" = "user";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
double_puppet = {
|
||||||
|
secrets."nul.ie" = "$MAU_IG_DOUBLE_PUPPET_TOKEN";
|
||||||
|
};
|
||||||
|
encryption = {
|
||||||
|
allow = true;
|
||||||
|
default = true;
|
||||||
|
require = true;
|
||||||
|
};
|
||||||
|
matrix = {
|
||||||
|
delivery_receipts = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
(mkIf config.my.build.isDevVM {
|
(mkIf config.my.build.isDevVM {
|
||||||
|
|||||||
@@ -8,5 +8,7 @@
|
|||||||
./object.nix
|
./object.nix
|
||||||
./toot.nix
|
./toot.nix
|
||||||
./waffletail.nix
|
./waffletail.nix
|
||||||
|
./qclk
|
||||||
|
./gam.nix
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
{ lib, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib.my) net;
|
||||||
|
inherit (lib.my.c) pubDomain;
|
||||||
|
inherit (lib.my.c.colony) domain prefixes;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
nixos.systems.gam = { config, ... }: {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
nixpkgs = "mine";
|
||||||
|
rendered = config.configuration.config.my.asContainer;
|
||||||
|
|
||||||
|
assignments = {
|
||||||
|
internal = {
|
||||||
|
name = "gam-ctr";
|
||||||
|
inherit domain;
|
||||||
|
ipv4.address = net.cidr.host 11 prefixes.ctrs.v4;
|
||||||
|
ipv6 = {
|
||||||
|
iid = "::11";
|
||||||
|
address = net.cidr.host 11 prefixes.ctrs.v6;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
configuration = { lib, pkgs, config, assignments, allAssignments, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib) mkMerge mkIf mkForce;
|
||||||
|
inherit (lib.my) networkdAssignment;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = mkMerge [
|
||||||
|
{
|
||||||
|
my = {
|
||||||
|
deploy.enable = false;
|
||||||
|
server.enable = true;
|
||||||
|
|
||||||
|
secrets = {
|
||||||
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAvDlH3nT1kve741gBluYmn5KQs8yz7FAEt8qLt+f0K6";
|
||||||
|
files = {
|
||||||
|
"gam/terraria.conf" = {
|
||||||
|
owner = "terraria";
|
||||||
|
group = "terraria";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd = {
|
||||||
|
network.networks."80-container-host0" = networkdAssignment "host0" assignments.internal;
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
terraria = {
|
||||||
|
enable = true;
|
||||||
|
noUPnP = true;
|
||||||
|
messageOfTheDay = "sup gamers";
|
||||||
|
autoCreatedWorldSize = "large";
|
||||||
|
worldPath = "/var/lib/terraria/NotWorld.wld";
|
||||||
|
configFile = config.age.secrets."gam/terraria.conf".path;
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(mkIf config.my.build.isDevVM {
|
||||||
|
virtualisation = {
|
||||||
|
forwardPorts = [ ];
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
let
|
||||||
|
inherit (lib) concatStringsSep;
|
||||||
inherit (lib.my) net;
|
inherit (lib.my) net;
|
||||||
|
inherit (lib.my.c) pubDomain;
|
||||||
inherit (lib.my.c.colony) domain prefixes;
|
inherit (lib.my.c.colony) domain prefixes;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -21,7 +23,7 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
configuration = { lib, pkgs, config, ... }:
|
configuration = { lib, pkgs, config, allAssignments, ... }:
|
||||||
let
|
let
|
||||||
inherit (lib) mkForce;
|
inherit (lib) mkForce;
|
||||||
in
|
in
|
||||||
@@ -35,6 +37,19 @@ in
|
|||||||
|
|
||||||
secrets = {
|
secrets = {
|
||||||
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPUv1ntVrZv5ripsKpcOAnyDQX2PHjowzyhqWK10Ml53";
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPUv1ntVrZv5ripsKpcOAnyDQX2PHjowzyhqWK10Ml53";
|
||||||
|
files = {
|
||||||
|
"jackflix/photoprism-pass.txt" = {};
|
||||||
|
"jackflix/copyparty-pass.txt" = {
|
||||||
|
owner = "copyparty";
|
||||||
|
group = "copyparty";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
firewall = {
|
||||||
|
tcp.allowed = [
|
||||||
|
3923
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,15 +60,26 @@ in
|
|||||||
transmission.extraGroups = [ "media" ];
|
transmission.extraGroups = [ "media" ];
|
||||||
radarr.extraGroups = [ "media" ];
|
radarr.extraGroups = [ "media" ];
|
||||||
sonarr.extraGroups = [ "media" ];
|
sonarr.extraGroups = [ "media" ];
|
||||||
jellyseerr = {
|
seerr = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
uid = uids.jellyseerr;
|
uid = uids.jellyseerr;
|
||||||
group = "jellyseerr";
|
group = "seerr";
|
||||||
|
};
|
||||||
|
photoprism = {
|
||||||
|
isSystemUser = true;
|
||||||
|
uid = uids.photoprism;
|
||||||
|
group = "photoprism";
|
||||||
|
};
|
||||||
|
copyparty = {
|
||||||
|
uid = uids.copyparty;
|
||||||
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
groups = {
|
groups = {
|
||||||
media.gid = 2000;
|
media.gid = 2000;
|
||||||
jellyseerr.gid = gids.jellyseerr;
|
seerr.gid = gids.jellyseerr;
|
||||||
|
photoprism.gid = gids.photoprism;
|
||||||
|
copyparty.gid = gids.copyparty;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,13 +88,15 @@ in
|
|||||||
jackett.bindsTo = [ "systemd-networkd-wait-online@vpn.service" ];
|
jackett.bindsTo = [ "systemd-networkd-wait-online@vpn.service" ];
|
||||||
transmission.bindsTo = [ "systemd-networkd-wait-online@vpn.service" ];
|
transmission.bindsTo = [ "systemd-networkd-wait-online@vpn.service" ];
|
||||||
|
|
||||||
radarr.serviceConfig.UMask = "0002";
|
radarr.serviceConfig.UMask = mkForce "0002";
|
||||||
sonarr.serviceConfig.UMask = "0002";
|
radarr.path = with pkgs; [ ffmpeg ];
|
||||||
jellyseerr.serviceConfig = {
|
sonarr.serviceConfig.UMask = mkForce "0002";
|
||||||
|
sonarr.path = with pkgs; [ ffmpeg ];
|
||||||
|
seerr.serviceConfig = {
|
||||||
# Needs to be able to read its secrets
|
# Needs to be able to read its secrets
|
||||||
DynamicUser = mkForce false;
|
DynamicUser = mkForce false;
|
||||||
User = "jellyseerr";
|
User = "seerr";
|
||||||
Group = "jellyseerr";
|
Group = "seerr";
|
||||||
};
|
};
|
||||||
|
|
||||||
# https://github.com/NixOS/nixpkgs/issues/258793#issuecomment-1748168206
|
# https://github.com/NixOS/nixpkgs/issues/258793#issuecomment-1748168206
|
||||||
@@ -76,6 +104,10 @@ in
|
|||||||
RootDirectoryStartOnly = lib.mkForce false;
|
RootDirectoryStartOnly = lib.mkForce false;
|
||||||
RootDirectory = lib.mkForce "";
|
RootDirectory = lib.mkForce "";
|
||||||
};
|
};
|
||||||
|
photoprism.serviceConfig = {
|
||||||
|
# Needs to be able to access its data
|
||||||
|
DynamicUser = mkForce false;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,6 +116,7 @@ in
|
|||||||
|
|
||||||
transmission = {
|
transmission = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
package = pkgs.transmission_4;
|
||||||
downloadDirPermissions = null;
|
downloadDirPermissions = null;
|
||||||
performanceNetParameters = true;
|
performanceNetParameters = true;
|
||||||
settings = {
|
settings = {
|
||||||
@@ -108,15 +141,78 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
flaresolverr.enable = true;
|
||||||
jackett.enable = true;
|
jackett.enable = true;
|
||||||
radarr.enable = true;
|
radarr.enable = true;
|
||||||
sonarr.enable = true;
|
sonarr.enable = true;
|
||||||
jellyseerr = {
|
seerr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
jellyfin.enable = true;
|
jellyfin.enable = true;
|
||||||
|
|
||||||
|
photoprism = {
|
||||||
|
enable = true;
|
||||||
|
address = "[::]";
|
||||||
|
port = 2342;
|
||||||
|
originalsPath = "/mnt/media/photoprism/originals";
|
||||||
|
importPath = "/mnt/media/photoprism/import";
|
||||||
|
passwordFile = config.age.secrets."jackflix/photoprism-pass.txt".path;
|
||||||
|
settings = {
|
||||||
|
PHOTOPRISM_AUTH_MODE = "password";
|
||||||
|
PHOTOPRISM_ADMIN_USER = "dev";
|
||||||
|
PHOTOPRISM_APP_NAME = "/dev/player0 Photos";
|
||||||
|
PHOTOPRISM_SITE_URL = "https://photos.${pubDomain}/";
|
||||||
|
PHOTOPRISM_SITE_TITLE = "/dev/player0 Photos";
|
||||||
|
PHOTOPRISM_TRUSTED_PROXY = concatStringsSep "," (with prefixes.ctrs; [ v4 v6 ]);
|
||||||
|
PHOTOPRISM_DATABASE_DRIVER = "sqlite";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
copyparty = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.copyparty.override {
|
||||||
|
withMagic = true;
|
||||||
|
};
|
||||||
|
settings = {
|
||||||
|
name = "dev-stuff";
|
||||||
|
no-reload = true;
|
||||||
|
j = 8; # cores
|
||||||
|
http-only = true;
|
||||||
|
xff-src =
|
||||||
|
with allAssignments.middleman.internal;
|
||||||
|
[ "${ipv4.address}/32" prefixes.ctrs.v6 ];
|
||||||
|
rproxy = 1; # get if from x-forwarded-for
|
||||||
|
magic = true; # enable checking file magic on upload
|
||||||
|
hist = "/var/cache/copyparty";
|
||||||
|
shr = "/share"; # enable share creation
|
||||||
|
ed = true; # enable dotfiles
|
||||||
|
chmod-f = "664";
|
||||||
|
chmod-d = "775";
|
||||||
|
e2dsa = true; # file indexing
|
||||||
|
e2t = true; # metadata indexing
|
||||||
|
og-ua = "(Discord|Twitter|Slack)bot"; # embeds
|
||||||
|
theme = 6;
|
||||||
|
};
|
||||||
|
accounts.dev.passwordFile = config.age.secrets."jackflix/copyparty-pass.txt".path;
|
||||||
|
volumes = {
|
||||||
|
"/" = {
|
||||||
|
path = "/mnt/media/public";
|
||||||
|
access = {
|
||||||
|
A = "dev";
|
||||||
|
"r." = "*";
|
||||||
|
};
|
||||||
|
flags = {
|
||||||
|
shr_who = "no"; # no reason to have shares here
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"/priv" = {
|
||||||
|
path = "/mnt/media/stuff";
|
||||||
|
access.A = "dev"; # dev has admin access
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ let
|
|||||||
};
|
};
|
||||||
|
|
||||||
# Forwarded in AirVPN config
|
# Forwarded in AirVPN config
|
||||||
transmissionPeerPort = 47016;
|
transmissionPeerPort = 28457;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = mkMerge [
|
config = mkMerge [
|
||||||
@@ -37,7 +37,7 @@ in
|
|||||||
tcp dport ${toString transmissionPeerPort} accept
|
tcp dport ${toString transmissionPeerPort} accept
|
||||||
iifname vpn return
|
iifname vpn return
|
||||||
|
|
||||||
tcp dport { 19999, 9091, 9117, 7878, 8989, 8096 } accept
|
tcp dport { 19999, 9091, 9117, 7878, 8989, 8096, 2342 } accept
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
chain input {
|
chain input {
|
||||||
@@ -71,14 +71,12 @@ in
|
|||||||
RouteTable = routeTable;
|
RouteTable = routeTable;
|
||||||
};
|
};
|
||||||
wireguardPeers = [
|
wireguardPeers = [
|
||||||
{
|
|
||||||
# AirVPN NL
|
# AirVPN NL
|
||||||
wireguardPeerConfig = {
|
{
|
||||||
Endpoint = "2a00:1678:1337:2329:e5f:35d4:4404:ef9f:1637";
|
Endpoint = "2a00:1678:1337:2329:e5f:35d4:4404:ef9f:1637";
|
||||||
PublicKey = "PyLCXAQT8KkM4T+dUsOQfn+Ub3pGxfGlxkIApuig+hk=";
|
PublicKey = "PyLCXAQT8KkM4T+dUsOQfn+Ub3pGxfGlxkIApuig+hk=";
|
||||||
PresharedKeyFile = config.age.secrets."${pskFile}".path;
|
PresharedKeyFile = config.age.secrets."${pskFile}".path;
|
||||||
AllowedIPs = [ "0.0.0.0/0" "::/0" ];
|
AllowedIPs = [ "0.0.0.0/0" "::/0" ];
|
||||||
};
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -94,7 +92,7 @@ in
|
|||||||
matchConfig.Name = "vpn";
|
matchConfig.Name = "vpn";
|
||||||
address = [ "10.182.97.37/32" "fd7d:76ee:e68f:a993:735d:ef5e:6907:b122/128" ];
|
address = [ "10.182.97.37/32" "fd7d:76ee:e68f:a993:735d:ef5e:6907:b122/128" ];
|
||||||
dns = [ "10.128.0.1" "fd7d:76ee:e68f:a993::1" ];
|
dns = [ "10.128.0.1" "fd7d:76ee:e68f:a993::1" ];
|
||||||
routingPolicyRules = map (r: { routingPolicyRuleConfig = r; }) [
|
routingPolicyRules = [
|
||||||
{
|
{
|
||||||
Family = "both";
|
Family = "both";
|
||||||
SuppressPrefixLength = 0;
|
SuppressPrefixLength = 0;
|
||||||
|
|||||||
@@ -40,11 +40,6 @@ in
|
|||||||
secrets = {
|
secrets = {
|
||||||
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAQM9U1e/XcUCyMJITrpAHjAGahpqkZCmtX6pJkYzuks";
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAQM9U1e/XcUCyMJITrpAHjAGahpqkZCmtX6pJkYzuks";
|
||||||
files = {
|
files = {
|
||||||
"dhparams.pem" = {
|
|
||||||
owner = "acme";
|
|
||||||
group = "acme";
|
|
||||||
mode = "440";
|
|
||||||
};
|
|
||||||
"pdns-file-records.key" = {
|
"pdns-file-records.key" = {
|
||||||
owner = "acme";
|
owner = "acme";
|
||||||
group = "acme";
|
group = "acme";
|
||||||
@@ -72,6 +67,13 @@ in
|
|||||||
|
|
||||||
firewall = {
|
firewall = {
|
||||||
tcp.allowed = [ "http" "https" 8448 ];
|
tcp.allowed = [ "http" "https" 8448 ];
|
||||||
|
extraRules = ''
|
||||||
|
table inet nat {
|
||||||
|
chain postrouting {
|
||||||
|
oifname host0 snat ip6 to ${assignments.internal.ipv6.address}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
nginx-sso = {
|
nginx-sso = {
|
||||||
@@ -169,7 +171,7 @@ in
|
|||||||
"*.${config.networking.domain}"
|
"*.${config.networking.domain}"
|
||||||
];
|
];
|
||||||
dnsProvider = "exec";
|
dnsProvider = "exec";
|
||||||
credentialsFile =
|
environmentFile =
|
||||||
let
|
let
|
||||||
script = pkgs.writeShellScript "lego-update-int.sh" ''
|
script = pkgs.writeShellScript "lego-update-int.sh" ''
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -200,7 +202,7 @@ in
|
|||||||
"*.s3.${pubDomain}"
|
"*.s3.${pubDomain}"
|
||||||
];
|
];
|
||||||
dnsProvider = "cloudflare";
|
dnsProvider = "cloudflare";
|
||||||
credentialsFile = config.age.secrets."middleman/cloudflare-credentials.conf".path;
|
environmentFile = config.age.secrets."middleman/cloudflare-credentials.conf".path;
|
||||||
postRun =
|
postRun =
|
||||||
let
|
let
|
||||||
sshKey = config.age.secrets."middleman/mailcow-ssh.key".path;
|
sshKey = config.age.secrets."middleman/mailcow-ssh.key".path;
|
||||||
@@ -239,6 +241,9 @@ in
|
|||||||
];
|
];
|
||||||
|
|
||||||
recommendedTlsSettings = true;
|
recommendedTlsSettings = true;
|
||||||
|
recommendedBrotliSettings = true;
|
||||||
|
# Uh so nginx is hanging with zstd enabled... maybe let's not for now
|
||||||
|
# recommendedZstdSettings = true;
|
||||||
clientMaxBodySize = "0";
|
clientMaxBodySize = "0";
|
||||||
serverTokens = true;
|
serverTokens = true;
|
||||||
resolver = {
|
resolver = {
|
||||||
@@ -246,8 +251,10 @@ in
|
|||||||
valid = "5s";
|
valid = "5s";
|
||||||
};
|
};
|
||||||
proxyResolveWhileRunning = true;
|
proxyResolveWhileRunning = true;
|
||||||
sslDhparam = config.age.secrets."dhparams.pem".path;
|
|
||||||
|
|
||||||
|
appendConfig = ''
|
||||||
|
worker_processes auto;
|
||||||
|
'';
|
||||||
# Based on recommended*Settings, but probably better to be explicit about these
|
# Based on recommended*Settings, but probably better to be explicit about these
|
||||||
appendHttpConfig = ''
|
appendHttpConfig = ''
|
||||||
${baseHttpConfig}
|
${baseHttpConfig}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
let
|
let
|
||||||
inherit (builtins) mapAttrs toJSON;
|
inherit (builtins) mapAttrs toJSON;
|
||||||
inherit (lib) mkMerge mkDefault genAttrs flatten concatStringsSep;
|
inherit (lib) mkMerge mkDefault genAttrs flatten concatStringsSep;
|
||||||
inherit (lib.my.c) pubDomain;
|
inherit (lib.my.c) pubDomain home;
|
||||||
inherit (lib.my.c.nginx) proxyHeaders;
|
inherit (lib.my.c.nginx) proxyHeaders;
|
||||||
inherit (config.networking) domain;
|
inherit (config.networking) domain;
|
||||||
|
|
||||||
@@ -35,7 +35,6 @@ let
|
|||||||
# For clients
|
# For clients
|
||||||
(mkWellKnown "matrix/client" (toJSON {
|
(mkWellKnown "matrix/client" (toJSON {
|
||||||
"m.homeserver".base_url = "https://matrix.nul.ie";
|
"m.homeserver".base_url = "https://matrix.nul.ie";
|
||||||
"org.matrix.msc3575.proxy".url = "https://matrix-syncv3.nul.ie";
|
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -50,6 +49,7 @@ let
|
|||||||
"/.well-known/webfinger".return = "301 https://toot.nul.ie$request_uri";
|
"/.well-known/webfinger".return = "301 https://toot.nul.ie$request_uri";
|
||||||
"/.well-known/nodeinfo".return = "301 https://toot.nul.ie$request_uri";
|
"/.well-known/nodeinfo".return = "301 https://toot.nul.ie$request_uri";
|
||||||
"/.well-known/host-meta".return = "301 https://toot.nul.ie$request_uri";
|
"/.well-known/host-meta".return = "301 https://toot.nul.ie$request_uri";
|
||||||
|
"/.well-known/atproto-did".return = "301 https://pds.nul.ie$request_uri";
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -80,6 +80,10 @@ in
|
|||||||
sha256 = "018wh6ps19n7323fi44njzj9yd4wqslc90dykbwfyscv7bgxhlar";
|
sha256 = "018wh6ps19n7323fi44njzj9yd4wqslc90dykbwfyscv7bgxhlar";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
name = "ssh.pub";
|
||||||
|
path = lib.my.c.sshKeyFiles.me;
|
||||||
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
wellKnown
|
wellKnown
|
||||||
@@ -145,7 +149,7 @@ in
|
|||||||
|
|
||||||
"pass.${pubDomain}" =
|
"pass.${pubDomain}" =
|
||||||
let
|
let
|
||||||
upstream = "http://vaultwarden-ctr.${domain}";
|
upstream = "http://vaultwarden-ctr.${domain}:8080";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
locations = {
|
locations = {
|
||||||
@@ -182,10 +186,6 @@ in
|
|||||||
];
|
];
|
||||||
useACMEHost = pubDomain;
|
useACMEHost = pubDomain;
|
||||||
};
|
};
|
||||||
"matrix-syncv3.${pubDomain}" = {
|
|
||||||
locations."/".proxyPass = "http://chatterbox-ctr.${domain}:8009";
|
|
||||||
useACMEHost = pubDomain;
|
|
||||||
};
|
|
||||||
|
|
||||||
"element.${pubDomain}" =
|
"element.${pubDomain}" =
|
||||||
let
|
let
|
||||||
@@ -206,7 +206,8 @@ in
|
|||||||
# Currently it seems like single quotes aren't escaped like they should be...
|
# Currently it seems like single quotes aren't escaped like they should be...
|
||||||
conf = {
|
conf = {
|
||||||
brand = "/dev/player0 Matrix";
|
brand = "/dev/player0 Matrix";
|
||||||
showLabsSettings = true;
|
show_labs_settings = true;
|
||||||
|
default_country_code = "IE";
|
||||||
disable_guests = true;
|
disable_guests = true;
|
||||||
default_server_config = {
|
default_server_config = {
|
||||||
"m.homeserver" = {
|
"m.homeserver" = {
|
||||||
@@ -214,9 +215,8 @@ in
|
|||||||
server_name = "nul.ie";
|
server_name = "nul.ie";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
roomDirectory.servers = [
|
room_directory.servers = [
|
||||||
"nul.ie"
|
"nul.ie"
|
||||||
"netsoc.ie"
|
|
||||||
"matrix.org"
|
"matrix.org"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -327,6 +327,15 @@ in
|
|||||||
useACMEHost = pubDomain;
|
useACMEHost = pubDomain;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
"pds.nul.ie" = {
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://toot-ctr.${domain}:3000";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = proxyHeaders;
|
||||||
|
};
|
||||||
|
useACMEHost = pubDomain;
|
||||||
|
};
|
||||||
|
|
||||||
"share.${pubDomain}" = {
|
"share.${pubDomain}" = {
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://object-ctr.${domain}:9090";
|
proxyPass = "http://object-ctr.${domain}:9090";
|
||||||
@@ -338,16 +347,13 @@ in
|
|||||||
|
|
||||||
"stuff.${pubDomain}" = {
|
"stuff.${pubDomain}" = {
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
basicAuthFile = config.age.secrets."middleman/htpasswd".path;
|
proxyPass = "http://jackflix-ctr.${domain}:3923";
|
||||||
root = "/mnt/media/stuff";
|
|
||||||
extraConfig = ''
|
|
||||||
fancyindex on;
|
|
||||||
fancyindex_show_dotfiles on;
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
useACMEHost = pubDomain;
|
useACMEHost = pubDomain;
|
||||||
};
|
};
|
||||||
"public.${pubDomain}" = {
|
"public.${pubDomain}" = {
|
||||||
|
onlySSL = false;
|
||||||
|
addSSL = true;
|
||||||
serverAliases = [ "p.${pubDomain}" ];
|
serverAliases = [ "p.${pubDomain}" ];
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
root = "/mnt/media/public";
|
root = "/mnt/media/public";
|
||||||
@@ -364,7 +370,12 @@ in
|
|||||||
useACMEHost = pubDomain;
|
useACMEHost = pubDomain;
|
||||||
};
|
};
|
||||||
"mc-rail.${pubDomain}" = {
|
"mc-rail.${pubDomain}" = {
|
||||||
locations."/".proxyPass = "http://simpcraft-staging-oci.${domain}:3876";
|
locations."/".proxyPass = "http://simpcraft-oci.${domain}:3876";
|
||||||
|
useACMEHost = pubDomain;
|
||||||
|
};
|
||||||
|
|
||||||
|
"mc-map-kink.${pubDomain}" = {
|
||||||
|
locations."/".proxyPass = "http://kinkcraft-oci.${domain}:8100";
|
||||||
useACMEHost = pubDomain;
|
useACMEHost = pubDomain;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -384,6 +395,56 @@ in
|
|||||||
};
|
};
|
||||||
useACMEHost = pubDomain;
|
useACMEHost = pubDomain;
|
||||||
};
|
};
|
||||||
|
"pb.${pubDomain}" = {
|
||||||
|
locations."/".proxyPass = "http://object-ctr.${domain}:8088";
|
||||||
|
useACMEHost = pubDomain;
|
||||||
|
};
|
||||||
|
"photos.${pubDomain}" = {
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://jackflix-ctr.${domain}:2342";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = proxyHeaders;
|
||||||
|
};
|
||||||
|
useACMEHost = pubDomain;
|
||||||
|
};
|
||||||
|
|
||||||
|
"pront.${pubDomain}" = mkMerge [
|
||||||
|
{
|
||||||
|
locations."/" = mkMerge [
|
||||||
|
{
|
||||||
|
proxyPass = "http://stream-hi.${home.domain}:5000";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = proxyHeaders;
|
||||||
|
}
|
||||||
|
(ssoLoc "generic")
|
||||||
|
];
|
||||||
|
locations."~* ^/webcam/(.*)" = mkMerge [
|
||||||
|
{
|
||||||
|
proxyPass = "http://stream-hi.${home.domain}:5050/$1$is_args$args";
|
||||||
|
extraConfig = proxyHeaders;
|
||||||
|
}
|
||||||
|
(ssoLoc "generic")
|
||||||
|
];
|
||||||
|
useACMEHost = pubDomain;
|
||||||
|
}
|
||||||
|
(ssoServer "generic")
|
||||||
|
];
|
||||||
|
"hass.${pubDomain}" = {
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://hass-ctr.${home.domain}:8123";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = proxyHeaders;
|
||||||
|
};
|
||||||
|
useACMEHost = pubDomain;
|
||||||
|
};
|
||||||
|
"hass-john.${pubDomain}" = {
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://john-valorant-tun.${domain}:8123";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = proxyHeaders;
|
||||||
|
};
|
||||||
|
useACMEHost = pubDomain;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
minio =
|
minio =
|
||||||
@@ -395,10 +456,13 @@ in
|
|||||||
ignore_invalid_headers off;
|
ignore_invalid_headers off;
|
||||||
'';
|
'';
|
||||||
|
|
||||||
nixCacheableRegex = ''^\/(\S+\.narinfo|nar\/\S+\.nar\.\S+)$'';
|
nixCacheableRegex = ''^\/(\S+\.narinfo|nar\/\S+\.nar.*|serve\/.+)$'';
|
||||||
nixCacheHeaders = ''
|
nixCacheHeaders = ''
|
||||||
add_header Cache-Control $nix_cache_control;
|
add_header Cache-Control $nix_cache_control;
|
||||||
add_header Expires $nix_expires;
|
add_header Expires $nix_expires;
|
||||||
|
|
||||||
|
brotli on;
|
||||||
|
brotli_types application/x-nix-archive;
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -440,9 +504,11 @@ in
|
|||||||
|
|
||||||
"nix-cache.${pubDomain}" = {
|
"nix-cache.${pubDomain}" = {
|
||||||
locations = {
|
locations = {
|
||||||
"/".proxyPass = "http://${host}:8069";
|
"/" = {
|
||||||
|
proxyPass = "http://${host}:5000";
|
||||||
|
};
|
||||||
"~ ${nixCacheableRegex}" = {
|
"~ ${nixCacheableRegex}" = {
|
||||||
proxyPass = "http://${host}:8069";
|
proxyPass = "http://${host}:5000";
|
||||||
extraConfig = nixCacheHeaders;
|
extraConfig = nixCacheHeaders;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ in
|
|||||||
{
|
{
|
||||||
config = mkMerge [
|
config = mkMerge [
|
||||||
{
|
{
|
||||||
|
fileSystems = {
|
||||||
|
"/var/lib/harmonia" = {
|
||||||
|
device = "/mnt/nix-cache";
|
||||||
|
options = [ "bind" ];
|
||||||
|
fsType = "none";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
my = {
|
my = {
|
||||||
deploy.enable = false;
|
deploy.enable = false;
|
||||||
server.enable = true;
|
server.enable = true;
|
||||||
@@ -48,7 +56,9 @@ in
|
|||||||
group = config.my.user.config.group;
|
group = config.my.user.config.group;
|
||||||
};
|
};
|
||||||
"object/atticd.env" = {};
|
"object/atticd.env" = {};
|
||||||
|
"nix-cache.key" = {};
|
||||||
"object/hedgedoc.env" = {};
|
"object/hedgedoc.env" = {};
|
||||||
|
"object/wastebin.env" = {};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,7 +67,9 @@ in
|
|||||||
9000 9001
|
9000 9001
|
||||||
config.services.sharry.config.bind.port
|
config.services.sharry.config.bind.port
|
||||||
8069
|
8069
|
||||||
|
5000
|
||||||
config.services.hedgedoc.settings.port
|
config.services.hedgedoc.settings.port
|
||||||
|
8088
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,14 +78,32 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
users = with lib.my.c.ids; let inherit (config.services.atticd) user group; in {
|
users = with lib.my.c.ids; mkMerge [
|
||||||
|
(let inherit (config.services.atticd) user group; in {
|
||||||
users."${user}" = {
|
users."${user}" = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
uid = uids.atticd;
|
uid = uids.atticd;
|
||||||
group = group;
|
group = group;
|
||||||
};
|
};
|
||||||
groups."${user}".gid = gids.atticd;
|
groups."${user}".gid = gids.atticd;
|
||||||
|
})
|
||||||
|
{
|
||||||
|
users = {
|
||||||
|
harmonia = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "harmonia";
|
||||||
|
shell = pkgs.bashInteractive;
|
||||||
|
openssh.authorizedKeys.keyFiles = [
|
||||||
|
lib.my.c.sshKeyFiles.harmonia
|
||||||
|
lib.my.c.sshKeyFiles.me
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
groups = {
|
||||||
|
harmonia = { };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
network.networks."80-container-host0" = networkdAssignment "host0" assignments.internal;
|
network.networks."80-container-host0" = networkdAssignment "host0" assignments.internal;
|
||||||
@@ -91,7 +121,9 @@ in
|
|||||||
MINIO_BROWSER_REDIRECT_URL = "https://minio.nul.ie";
|
MINIO_BROWSER_REDIRECT_URL = "https://minio.nul.ie";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
sharry = awaitPostgres;
|
sharry = awaitPostgres;
|
||||||
|
|
||||||
atticd = mkMerge [
|
atticd = mkMerge [
|
||||||
awaitPostgres
|
awaitPostgres
|
||||||
{
|
{
|
||||||
@@ -102,6 +134,26 @@ in
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
harmonia-dev = {
|
||||||
|
# environment.RUST_LOG = mkForce "trace";
|
||||||
|
# serviceConfig = {
|
||||||
|
# StateDirectory = "harmonia";
|
||||||
|
# DynamicUser = mkForce false;
|
||||||
|
# };
|
||||||
|
};
|
||||||
|
harmonia-daemon = {
|
||||||
|
# environment.RUST_LOG = mkForce "trace";
|
||||||
|
preStart = ''
|
||||||
|
${config.nix.package}/bin/nix store info --store /var/lib/harmonia
|
||||||
|
'';
|
||||||
|
serviceConfig = {
|
||||||
|
User = "harmonia";
|
||||||
|
Group = "harmonia";
|
||||||
|
StateDirectory = "harmonia";
|
||||||
|
DynamicUser = mkForce false;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,6 +163,9 @@ in
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# TODO/FIXME: this is bad...
|
||||||
|
nixpkgs.config.permittedInsecurePackages = [ "minio-2025-10-15T17-29-55Z" ];
|
||||||
|
|
||||||
services = {
|
services = {
|
||||||
minio = {
|
minio = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -181,8 +236,8 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
atticd = {
|
atticd = {
|
||||||
enable = true;
|
enable = false;
|
||||||
credentialsFile = config.age.secrets."object/atticd.env".path;
|
environmentFile = config.age.secrets."object/atticd.env".path;
|
||||||
settings = {
|
settings = {
|
||||||
listen = "[::]:8069";
|
listen = "[::]:8069";
|
||||||
allowed-hosts = [ "nix-cache.${pubDomain}" ];
|
allowed-hosts = [ "nix-cache.${pubDomain}" ];
|
||||||
@@ -201,6 +256,23 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
harmonia-dev = {
|
||||||
|
daemon = {
|
||||||
|
enable = true;
|
||||||
|
storeDir = "/nix/store";
|
||||||
|
dbPath = "/var/lib/harmonia/nix/var/nix/db/db.sqlite";
|
||||||
|
};
|
||||||
|
cache = {
|
||||||
|
enable = true;
|
||||||
|
signKeyPaths = [ config.age.secrets."nix-cache.key".path ];
|
||||||
|
settings = {
|
||||||
|
priority = 30;
|
||||||
|
virtual_nix_store = "/nix/store";
|
||||||
|
real_nix_store = "/var/lib/harmonia/nix/store";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
hedgedoc = {
|
hedgedoc = {
|
||||||
enable = true;
|
enable = true;
|
||||||
environmentFile = config.age.secrets."object/hedgedoc.env".path;
|
environmentFile = config.age.secrets."object/hedgedoc.env".path;
|
||||||
@@ -220,6 +292,15 @@ in
|
|||||||
allowEmailRegister = false;
|
allowEmailRegister = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
wastebin = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
WASTEBIN_MAX_BODY_SIZE = 67108864; # 16 MiB
|
||||||
|
WASTEBIN_PASSWORD_SALT = "TeGhaemeer0Siez3";
|
||||||
|
};
|
||||||
|
secretFile = config.age.secrets."object/wastebin.env".path;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
(mkIf config.my.build.isDevVM {
|
(mkIf config.my.build.isDevVM {
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
{ lib, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib.my) net;
|
||||||
|
inherit (lib.my.c.colony) domain prefixes qclk;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
nixos.systems.qclk = { config, ... }: {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
nixpkgs = "mine";
|
||||||
|
rendered = config.configuration.config.my.asContainer;
|
||||||
|
|
||||||
|
assignments = {
|
||||||
|
internal = {
|
||||||
|
name = "qclk-ctr";
|
||||||
|
inherit domain;
|
||||||
|
ipv4.address = net.cidr.host 10 prefixes.ctrs.v4;
|
||||||
|
ipv6 = {
|
||||||
|
iid = "::a";
|
||||||
|
address = net.cidr.host 10 prefixes.ctrs.v6;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
qclk = {
|
||||||
|
ipv4 = {
|
||||||
|
address = net.cidr.host 1 prefixes.qclk.v4;
|
||||||
|
gateway = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
configuration = { lib, pkgs, config, assignments, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib) concatStringsSep mkMerge mkIf mkForce;
|
||||||
|
inherit (lib.my) networkdAssignment;
|
||||||
|
|
||||||
|
apiPort = 8080;
|
||||||
|
|
||||||
|
instances = [
|
||||||
|
{
|
||||||
|
host = 2;
|
||||||
|
wgKey = "D7z1FhcdxpnrGCE0wBW5PZb5BKuhCu6tcZ/5ZaYxdwQ=";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
ipFor = i: net.cidr.host i.host prefixes.qclk.v4;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = {
|
||||||
|
environment = {
|
||||||
|
systemPackages = with pkgs; [
|
||||||
|
wireguard-tools
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
my = {
|
||||||
|
deploy.enable = false;
|
||||||
|
server.enable = true;
|
||||||
|
|
||||||
|
secrets = {
|
||||||
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC1kcfvahYmSk8IJKaUIcGkhxf/8Yse2XnU7Qqgcglyq";
|
||||||
|
files = {
|
||||||
|
"qclk/wg.key" = {
|
||||||
|
group = "systemd-network";
|
||||||
|
mode = "440";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
firewall = {
|
||||||
|
udp.allowed = [ qclk.wgPort ];
|
||||||
|
extraRules = ''
|
||||||
|
table inet filter {
|
||||||
|
chain input {
|
||||||
|
iifname management tcp dport ${toString apiPort} accept
|
||||||
|
}
|
||||||
|
chain forward {
|
||||||
|
iifname host0 oifname management ip saddr { ${concatStringsSep ", " lib.my.c.as211024.trusted.v4} } accept
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table inet nat {
|
||||||
|
chain postrouting {
|
||||||
|
iifname host0 oifname management snat ip to ${assignments.qclk.ipv4.address}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd = {
|
||||||
|
network = {
|
||||||
|
netdevs."30-management" = {
|
||||||
|
netdevConfig = {
|
||||||
|
Name = "management";
|
||||||
|
Kind = "wireguard";
|
||||||
|
};
|
||||||
|
wireguardConfig = {
|
||||||
|
PrivateKeyFile = config.age.secrets."qclk/wg.key".path;
|
||||||
|
ListenPort = qclk.wgPort;
|
||||||
|
};
|
||||||
|
wireguardPeers = map (i: {
|
||||||
|
PublicKey = i.wgKey;
|
||||||
|
AllowedIPs = [ (ipFor i) ];
|
||||||
|
}) instances;
|
||||||
|
};
|
||||||
|
networks = {
|
||||||
|
"30-container-host0" = networkdAssignment "host0" assignments.internal;
|
||||||
|
|
||||||
|
"30-management" = networkdAssignment "management" assignments.qclk;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services = { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ in
|
|||||||
let
|
let
|
||||||
inherit (lib) mkMerge mkIf genAttrs;
|
inherit (lib) mkMerge mkIf genAttrs;
|
||||||
inherit (lib.my) networkdAssignment systemdAwaitPostgres;
|
inherit (lib.my) networkdAssignment systemdAwaitPostgres;
|
||||||
|
|
||||||
|
pdsPort = 3000;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = mkMerge [
|
config = mkMerge [
|
||||||
@@ -36,7 +38,7 @@ in
|
|||||||
|
|
||||||
secrets = {
|
secrets = {
|
||||||
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILSslLkDe54AKYzxdtKD70zcU72W0EpYsfbdJ6UFq0QK";
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILSslLkDe54AKYzxdtKD70zcU72W0EpYsfbdJ6UFq0QK";
|
||||||
files = genAttrs
|
files = (genAttrs
|
||||||
(map (f: "toot/${f}") [
|
(map (f: "toot/${f}") [
|
||||||
"postgres-password.txt"
|
"postgres-password.txt"
|
||||||
"secret-key.txt"
|
"secret-key.txt"
|
||||||
@@ -48,7 +50,12 @@ in
|
|||||||
(_: with config.services.mastodon; {
|
(_: with config.services.mastodon; {
|
||||||
owner = user;
|
owner = user;
|
||||||
inherit group;
|
inherit group;
|
||||||
});
|
})) // {
|
||||||
|
"toot/pds.env" = {
|
||||||
|
owner = "pds";
|
||||||
|
group = "pds";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
firewall = {
|
firewall = {
|
||||||
@@ -56,6 +63,7 @@ in
|
|||||||
19999
|
19999
|
||||||
|
|
||||||
"http"
|
"http"
|
||||||
|
pdsPort
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -79,7 +87,7 @@ in
|
|||||||
netdata.enable = true;
|
netdata.enable = true;
|
||||||
mastodon = mkMerge [
|
mastodon = mkMerge [
|
||||||
rec {
|
rec {
|
||||||
enable = true;
|
enable = false;
|
||||||
localDomain = extraConfig.WEB_DOMAIN; # for nginx config
|
localDomain = extraConfig.WEB_DOMAIN; # for nginx config
|
||||||
extraConfig = {
|
extraConfig = {
|
||||||
LOCAL_DOMAIN = "nul.ie";
|
LOCAL_DOMAIN = "nul.ie";
|
||||||
@@ -87,7 +95,9 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
secretKeyBaseFile = config.age.secrets."toot/secret-key.txt".path;
|
secretKeyBaseFile = config.age.secrets."toot/secret-key.txt".path;
|
||||||
otpSecretFile = config.age.secrets."toot/otp-secret.txt".path;
|
# TODO: This was removed at some point.
|
||||||
|
# If we want to bring Mastodon back, this will probably need to be addressd.
|
||||||
|
# otpSecretFile = config.age.secrets."toot/otp-secret.txt".path;
|
||||||
vapidPrivateKeyFile = config.age.secrets."toot/vapid-key.txt".path;
|
vapidPrivateKeyFile = config.age.secrets."toot/vapid-key.txt".path;
|
||||||
vapidPublicKeyFile = toString (pkgs.writeText
|
vapidPublicKeyFile = toString (pkgs.writeText
|
||||||
"vapid-pubkey.txt"
|
"vapid-pubkey.txt"
|
||||||
@@ -155,6 +165,32 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bluesky-pds = {
|
||||||
|
enable = true;
|
||||||
|
environmentFiles = [ config.age.secrets."toot/pds.env".path ];
|
||||||
|
settings = {
|
||||||
|
PDS_HOSTNAME = "pds.nul.ie";
|
||||||
|
PDS_PORT = pdsPort;
|
||||||
|
|
||||||
|
PDS_BLOBSTORE_DISK_LOCATION = null;
|
||||||
|
PDS_BLOBSTORE_S3_BUCKET = "pds";
|
||||||
|
PDS_BLOBSTORE_S3_ENDPOINT = "https://s3.nul.ie/";
|
||||||
|
PDS_BLOBSTORE_S3_REGION = "eu-central-1";
|
||||||
|
PDS_BLOBSTORE_S3_ACCESS_KEY_ID = "pds";
|
||||||
|
PDS_BLOB_UPLOAD_LIMIT = "52428800";
|
||||||
|
|
||||||
|
PDS_EMAIL_FROM_ADDRESS = "pds@nul.ie";
|
||||||
|
|
||||||
|
PDS_DID_PLC_URL = "https://plc.directory";
|
||||||
|
PDS_INVITE_REQUIRED = "true";
|
||||||
|
PDS_BSKY_APP_VIEW_URL = "https://api.bsky.app";
|
||||||
|
PDS_BSKY_APP_VIEW_DID = "did:web:api.bsky.app";
|
||||||
|
PDS_REPORT_SERVICE_URL = "https://mod.bsky.app";
|
||||||
|
PDS_REPORT_SERVICE_DID = "did:plc:ar7c4by46qjdydhdevvrndac";
|
||||||
|
PDS_CRAWLERS = "https://bsky.network";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
(mkIf config.my.build.isDevVM {
|
(mkIf config.my.build.isDevVM {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ in
|
|||||||
DOMAIN = "https://pass.${lib.my.c.pubDomain}";
|
DOMAIN = "https://pass.${lib.my.c.pubDomain}";
|
||||||
|
|
||||||
ROCKET_ADDRESS = "::";
|
ROCKET_ADDRESS = "::";
|
||||||
ROCKET_PORT = 80;
|
ROCKET_PORT = 8080;
|
||||||
|
|
||||||
SMTP_HOST = "mail.nul.ie";
|
SMTP_HOST = "mail.nul.ie";
|
||||||
SMTP_FROM = "pass@nul.ie";
|
SMTP_FROM = "pass@nul.ie";
|
||||||
@@ -99,6 +99,8 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
borgbackup.jobs.vaultwarden = {
|
borgbackup.jobs.vaultwarden = {
|
||||||
|
readWritePaths = [ "/var/lib/borgbackup" "/var/cache/borgbackup" ];
|
||||||
|
|
||||||
paths = [ vwData ];
|
paths = [ vwData ];
|
||||||
repo = "zh2855@zh2855.rsync.net:borg/vaultwarden2";
|
repo = "zh2855@zh2855.rsync.net:borg/vaultwarden2";
|
||||||
doInit = true;
|
doInit = true;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ in
|
|||||||
interfaceName = "tailscale0";
|
interfaceName = "tailscale0";
|
||||||
extraUpFlags = [
|
extraUpFlags = [
|
||||||
"--operator=${config.my.user.config.name}"
|
"--operator=${config.my.user.config.name}"
|
||||||
"--login-server=https://ts.nul.ie"
|
"--login-server=https://hs.nul.ie"
|
||||||
"--netfilter-mode=off"
|
"--netfilter-mode=off"
|
||||||
"--advertise-exit-node"
|
"--advertise-exit-node"
|
||||||
"--advertise-routes=${advRoutes}"
|
"--advertise-routes=${advRoutes}"
|
||||||
|
|||||||
@@ -49,7 +49,11 @@ in
|
|||||||
inherit (lib.my) networkdAssignment;
|
inherit (lib.my) networkdAssignment;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [ "${modulesPath}/profiles/qemu-guest.nix" ];
|
imports = [
|
||||||
|
"${modulesPath}/profiles/qemu-guest.nix"
|
||||||
|
|
||||||
|
./containers-ext.nix
|
||||||
|
];
|
||||||
|
|
||||||
config = mkMerge [
|
config = mkMerge [
|
||||||
{
|
{
|
||||||
@@ -90,8 +94,8 @@ in
|
|||||||
device = "/dev/disk/by-label/minio";
|
device = "/dev/disk/by-label/minio";
|
||||||
fsType = "xfs";
|
fsType = "xfs";
|
||||||
};
|
};
|
||||||
"/mnt/atticd" = {
|
"/mnt/nix-cache" = {
|
||||||
device = "/dev/disk/by-label/atticd";
|
device = "/dev/disk/by-label/nix-cache";
|
||||||
fsType = "ext4";
|
fsType = "ext4";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -136,10 +140,10 @@ in
|
|||||||
};
|
};
|
||||||
ipv6Prefixes = [
|
ipv6Prefixes = [
|
||||||
{
|
{
|
||||||
ipv6PrefixConfig.Prefix = prefixes.ctrs.v6;
|
Prefix = prefixes.ctrs.v6;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
routes = map (r: { routeConfig = r; }) [
|
routes = [
|
||||||
{
|
{
|
||||||
Destination = lib.my.c.tailscale.prefix.v4;
|
Destination = lib.my.c.tailscale.prefix.v4;
|
||||||
Gateway = allAssignments.waffletail.internal.ipv4.address;
|
Gateway = allAssignments.waffletail.internal.ipv4.address;
|
||||||
@@ -148,6 +152,11 @@ in
|
|||||||
Destination = lib.my.c.tailscale.prefix.v6;
|
Destination = lib.my.c.tailscale.prefix.v6;
|
||||||
Gateway = allAssignments.waffletail.internal.ipv6.address;
|
Gateway = allAssignments.waffletail.internal.ipv6.address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Destination = prefixes.qclk.v4;
|
||||||
|
Gateway = allAssignments.qclk.internal.ipv4.address;
|
||||||
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -202,11 +211,13 @@ in
|
|||||||
object = {
|
object = {
|
||||||
bindMounts = {
|
bindMounts = {
|
||||||
"/mnt/minio".readOnly = false;
|
"/mnt/minio".readOnly = false;
|
||||||
"/mnt/atticd".readOnly = false;
|
"/mnt/nix-cache".readOnly = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
toot = {};
|
toot = {};
|
||||||
waffletail = {};
|
waffletail = {};
|
||||||
|
qclk = {};
|
||||||
|
gam = {};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
mkMerge [
|
mkMerge [
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ in
|
|||||||
valheim-oci = 2;
|
valheim-oci = 2;
|
||||||
simpcraft-oci = 3;
|
simpcraft-oci = 3;
|
||||||
simpcraft-staging-oci = 4;
|
simpcraft-staging-oci = 4;
|
||||||
|
enshrouded-oci = 5;
|
||||||
|
kevcraft-oci = 6;
|
||||||
|
kinkcraft-oci = 7;
|
||||||
|
graeme-oci = 8;
|
||||||
};
|
};
|
||||||
|
|
||||||
configuration = { lib, pkgs, modulesPath, config, assignments, allAssignments, ... }:
|
configuration = { lib, pkgs, modulesPath, config, assignments, allAssignments, ... }:
|
||||||
@@ -66,6 +70,7 @@ in
|
|||||||
|
|
||||||
./valheim.nix
|
./valheim.nix
|
||||||
./minecraft
|
./minecraft
|
||||||
|
# ./enshrouded.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
config = mkMerge [
|
config = mkMerge [
|
||||||
@@ -108,46 +113,31 @@ in
|
|||||||
oci-containers = {
|
oci-containers = {
|
||||||
backend = "podman";
|
backend = "podman";
|
||||||
};
|
};
|
||||||
# NixOS has switched to using netavark, which is native to podman. It's currently missing an option to
|
containers.containersConf.settings.network = {
|
||||||
# disable iptables rules generation, which is very annoying.
|
network_backend = "netavark";
|
||||||
containers.containersConf.settings.network.network_backend = mkForce "cni";
|
firewall_driver = mkForce "none";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
etc = {
|
etc = {
|
||||||
"cni/net.d/90-colony.conflist".text = toJSON {
|
"containers/networks/colony.json".text = toJSON {
|
||||||
cniVersion = "0.4.0";
|
|
||||||
name = "colony";
|
name = "colony";
|
||||||
plugins = [
|
id = "0000000000000000000000000000000000000000000000000000000000000001";
|
||||||
{
|
driver = "bridge";
|
||||||
type = "bridge";
|
network_interface = "oci";
|
||||||
bridge = "oci";
|
ipv6_enabled = true;
|
||||||
isGateway = true;
|
internal = false;
|
||||||
ipMasq = false;
|
dns_enabled = false;
|
||||||
hairpinMode = true;
|
subnets = [
|
||||||
ipam = {
|
|
||||||
type = "host-local";
|
|
||||||
routes = [
|
|
||||||
{ dst = "0.0.0.0/0"; }
|
|
||||||
{ dst = "::/0"; }
|
|
||||||
];
|
|
||||||
ranges = [
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
subnet = prefixes.oci.v4;
|
subnet = prefixes.oci.v4;
|
||||||
gateway = net.cidr.host 1 prefixes.oci.v4;
|
gateway = net.cidr.host 1 prefixes.oci.v4;
|
||||||
}
|
}
|
||||||
]
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
subnet = prefixes.oci.v6;
|
subnet = prefixes.oci.v6;
|
||||||
gateway = net.cidr.host 1 prefixes.oci.v6;
|
gateway = net.cidr.host 1 prefixes.oci.v6;
|
||||||
}
|
}
|
||||||
]
|
|
||||||
];
|
|
||||||
};
|
|
||||||
capabilities.ips = true;
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{ lib, config, allAssignments, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib) concatStringsSep;
|
||||||
|
inherit (lib.my) dockerNetAssignment;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = {
|
||||||
|
virtualisation.oci-containers.containers = {
|
||||||
|
enshrouded = {
|
||||||
|
image = "sknnr/enshrouded-dedicated-server@sha256:f163e8ba9caa2115d8a0a7b16c3696968242fb6fba82706d9a77a882df083497";
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
SERVER_NAME = "UWUshrouded";
|
||||||
|
# SERVER_IP = "::"; # no IPv6?? :(
|
||||||
|
TZ = "Europe/Dublin";
|
||||||
|
};
|
||||||
|
environmentFiles = [ config.age.secrets."whale2/enshrouded.env".path ];
|
||||||
|
|
||||||
|
volumes = [
|
||||||
|
"enshrouded:/home/steam/enshrouded/savegame"
|
||||||
|
];
|
||||||
|
|
||||||
|
extraOptions = [
|
||||||
|
''--network=colony:${dockerNetAssignment allAssignments "enshrouded-oci"}''
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
my = {
|
||||||
|
secrets.files = {
|
||||||
|
"whale2/enshrouded.env" = {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
{ lib, config, allAssignments, ... }:
|
{ lib, pkgs, config, allAssignments, ... }:
|
||||||
let
|
let
|
||||||
inherit (lib) concatStringsSep;
|
inherit (lib) concatStringsSep;
|
||||||
inherit (lib.my) dockerNetAssignment;
|
inherit (lib.my) dockerNetAssignment;
|
||||||
|
|
||||||
# devplayer0
|
# devplayer0
|
||||||
op = "6d7d971b-ce10-435b-85c5-c99c0d8d288c";
|
op = "6d7d971b-ce10-435b-85c5-c99c0d8d288c";
|
||||||
|
kev = "703b378a-09f9-4c1d-9876-1c9305728c49";
|
||||||
whitelist = concatStringsSep "," [
|
whitelist = concatStringsSep "," [
|
||||||
op
|
op
|
||||||
"dcd2ecb9-2b5e-49cb-9d4f-f5a76162df56" # Elderlypug
|
"dcd2ecb9-2b5e-49cb-9d4f-f5a76162df56" # Elderlypug
|
||||||
"fcb26db2-c3ce-41aa-b588-efec79d37a8a" # Jesthral_
|
"fcb26db2-c3ce-41aa-b588-efec79d37a8a" # Jesthral_
|
||||||
"1d366062-12c0-4e29-aba7-6ab5d8c6bb05" # shr3kas0ras
|
"1d366062-12c0-4e29-aba7-6ab5d8c6bb05" # shr3kas0ras
|
||||||
"703b378a-09f9-4c1d-9876-1c9305728c49" # OROURKEIRE
|
kev
|
||||||
"f105bbe6-eda6-4a13-a8cf-894e77cab77b" # Adzerq
|
"f105bbe6-eda6-4a13-a8cf-894e77cab77b" # Adzerq
|
||||||
"1fc94979-41fb-497a-81e9-34ae24ca537a" # johnnyscrims
|
"1fc94979-41fb-497a-81e9-34ae24ca537a" # johnnyscrims
|
||||||
"d53c91df-b6e6-4463-b106-e8427d7a8d01" # BossLonus
|
"d53c91df-b6e6-4463-b106-e8427d7a8d01" # BossLonus
|
||||||
@@ -18,18 +19,27 @@ let
|
|||||||
"d6ec4c91-5da2-44eb-b89d-71dc8fe017a0" # Eefah98
|
"d6ec4c91-5da2-44eb-b89d-71dc8fe017a0" # Eefah98
|
||||||
"096a7348-fabe-4b2d-93fc-fd1fd5608fb0" # ToTheMoonStar
|
"096a7348-fabe-4b2d-93fc-fd1fd5608fb0" # ToTheMoonStar
|
||||||
];
|
];
|
||||||
|
|
||||||
|
fastback = {
|
||||||
|
gitConfig = pkgs.writeText "git-config" ''
|
||||||
|
[user]
|
||||||
|
email = "simpcraft@nul.ie"
|
||||||
|
name = "Simpcraft bot"
|
||||||
|
'';
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = {
|
config = {
|
||||||
virtualisation.oci-containers.containers = {
|
virtualisation.oci-containers.containers = {
|
||||||
simpcraft = {
|
simpcraft = {
|
||||||
image = "ghcr.io/itzg/minecraft-server:2023.12.2-java17-alpine";
|
image = "git.nul.ie/dev/craftblock:2024.1.0-java17-alpine";
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
TYPE = "MODRINTH";
|
TYPE = "MODRINTH";
|
||||||
|
|
||||||
EULA = "true";
|
EULA = "true";
|
||||||
ENABLE_QUERY = "true";
|
ENABLE_QUERY = "true";
|
||||||
|
ENABLE_RCON = "true";
|
||||||
MOTD = "§4§k----- §9S§ai§bm§cp§dc§er§fa§6f§5t §4§k-----";
|
MOTD = "§4§k----- §9S§ai§bm§cp§dc§er§fa§6f§5t §4§k-----";
|
||||||
ICON = "/ext/icon.png";
|
ICON = "/ext/icon.png";
|
||||||
|
|
||||||
@@ -41,15 +51,17 @@ in
|
|||||||
SPAWN_PROTECTION = "0";
|
SPAWN_PROTECTION = "0";
|
||||||
VIEW_DISTANCE = "20";
|
VIEW_DISTANCE = "20";
|
||||||
|
|
||||||
MAX_MEMORY = "6G";
|
MAX_MEMORY = "8G";
|
||||||
MODRINTH_MODPACK = "https://cdn.modrinth.com/data/CIYf3Hk8/versions/cdj2bSKg/Simpcraft-0.1.2.mrpack";
|
MODRINTH_MODPACK = "https://cdn.modrinth.com/data/CIYf3Hk8/versions/NGutsQSd/Simpcraft-0.2.1.mrpack";
|
||||||
|
|
||||||
TZ = "Europe/Dublin";
|
TZ = "Europe/Dublin";
|
||||||
};
|
};
|
||||||
|
environmentFiles = [ config.age.secrets."whale2/simpcraft.env".path ];
|
||||||
|
|
||||||
volumes = [
|
volumes = [
|
||||||
"minecraft_data:/data"
|
"minecraft_data:/data"
|
||||||
"${./icon.png}:/ext/icon.png:ro"
|
"${./icon.png}:/ext/icon.png:ro"
|
||||||
|
"${fastback.gitConfig}:/data/.config/git/config:ro"
|
||||||
];
|
];
|
||||||
|
|
||||||
extraOptions = [
|
extraOptions = [
|
||||||
@@ -57,16 +69,96 @@ in
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
simpcraft-staging = {
|
# simpcraft-staging = {
|
||||||
image = "git.nul.ie/dev/craftblock:2024.1.0-java17-alpine";
|
# image = "git.nul.ie/dev/craftblock:2024.1.0-java17-alpine";
|
||||||
|
|
||||||
|
# environment = {
|
||||||
|
# TYPE = "MODRINTH";
|
||||||
|
|
||||||
|
# EULA = "true";
|
||||||
|
# ENABLE_QUERY = "true";
|
||||||
|
# ENABLE_RCON = "true";
|
||||||
|
# MOTD = "§4§k----- §9S§ai§bm§cp§dc§er§fa§6f§5t [staging] §4§k-----";
|
||||||
|
# ICON = "/ext/icon.png";
|
||||||
|
|
||||||
|
# EXISTING_WHITELIST_FILE = "SYNCHRONIZE";
|
||||||
|
# WHITELIST = whitelist;
|
||||||
|
# EXISTING_OPS_FILE = "SYNCHRONIZE";
|
||||||
|
# OPS = op;
|
||||||
|
# DIFFICULTY = "normal";
|
||||||
|
# SPAWN_PROTECTION = "0";
|
||||||
|
# VIEW_DISTANCE = "20";
|
||||||
|
|
||||||
|
# MAX_MEMORY = "4G";
|
||||||
|
# MODRINTH_MODPACK = "https://cdn.modrinth.com/data/CIYf3Hk8/versions/Ym3sIi6H/Simpcraft-0.2.0.mrpack";
|
||||||
|
|
||||||
|
# TZ = "Europe/Dublin";
|
||||||
|
# };
|
||||||
|
# environmentFiles = [ config.age.secrets."whale2/simpcraft.env".path ];
|
||||||
|
|
||||||
|
# volumes = [
|
||||||
|
# "minecraft_staging_data:/data"
|
||||||
|
# "${./icon.png}:/ext/icon.png:ro"
|
||||||
|
# ];
|
||||||
|
|
||||||
|
# extraOptions = [
|
||||||
|
# ''--network=colony:${dockerNetAssignment allAssignments "simpcraft-staging-oci"}''
|
||||||
|
# ];
|
||||||
|
# };
|
||||||
|
|
||||||
|
kevcraft = {
|
||||||
|
# 2025.2.1-java21-alpine
|
||||||
|
image = "itzg/minecraft-server@sha256:57e319c15e9fee63f61029a65a33acc3de85118b21a2b4bb29f351cf4a915027";
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
TYPE = "MODRINTH";
|
TYPE = "VANILLA";
|
||||||
|
VERSION = "1.20.1";
|
||||||
|
SERVER_PORT = "25567";
|
||||||
|
QUERY_PORT = "25567";
|
||||||
|
|
||||||
EULA = "true";
|
EULA = "true";
|
||||||
ENABLE_QUERY = "true";
|
ENABLE_QUERY = "true";
|
||||||
ENABLE_RCON = "true";
|
ENABLE_RCON = "true";
|
||||||
MOTD = "§4§k----- §9S§ai§bm§cp§dc§er§fa§6f§5t [staging] §4§k-----";
|
MOTD = "§4§k----- §9K§ae§bv§cc§dr§ea§ff§6t §4§k-----";
|
||||||
|
ICON = "/ext/icon.png";
|
||||||
|
|
||||||
|
EXISTING_WHITELIST_FILE = "SYNCHRONIZE";
|
||||||
|
WHITELIST = whitelist;
|
||||||
|
EXISTING_OPS_FILE = "SYNCHRONIZE";
|
||||||
|
OPS = concatStringsSep "," [ op kev ];
|
||||||
|
DIFFICULTY = "normal";
|
||||||
|
SPAWN_PROTECTION = "0";
|
||||||
|
# VIEW_DISTANCE = "20";
|
||||||
|
|
||||||
|
MAX_MEMORY = "4G";
|
||||||
|
|
||||||
|
TZ = "Europe/Dublin";
|
||||||
|
};
|
||||||
|
environmentFiles = [ config.age.secrets."whale2/simpcraft.env".path ];
|
||||||
|
|
||||||
|
volumes = [
|
||||||
|
"kevcraft_data:/data"
|
||||||
|
"${./kev.png}:/ext/icon.png:ro"
|
||||||
|
];
|
||||||
|
|
||||||
|
extraOptions = [
|
||||||
|
''--network=colony:${dockerNetAssignment allAssignments "kevcraft-oci"}''
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
kinkcraft = {
|
||||||
|
# 2025.5.1-java21-alpine
|
||||||
|
image = "itzg/minecraft-server@sha256:de26c7128e3935f3be48fd30283f0b5a6da1b3d9f1a10c9f92502ee1ba072f7b";
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
TYPE = "MODRINTH";
|
||||||
|
SERVER_PORT = "25568";
|
||||||
|
QUERY_PORT = "25568";
|
||||||
|
|
||||||
|
EULA = "true";
|
||||||
|
ENABLE_QUERY = "true";
|
||||||
|
ENABLE_RCON = "true";
|
||||||
|
MOTD = "§4§k----- §9K§ai§bn§ck§dc§er§fa§6f§5t §4§k-----";
|
||||||
ICON = "/ext/icon.png";
|
ICON = "/ext/icon.png";
|
||||||
|
|
||||||
EXISTING_WHITELIST_FILE = "SYNCHRONIZE";
|
EXISTING_WHITELIST_FILE = "SYNCHRONIZE";
|
||||||
@@ -77,22 +169,97 @@ in
|
|||||||
SPAWN_PROTECTION = "0";
|
SPAWN_PROTECTION = "0";
|
||||||
VIEW_DISTANCE = "20";
|
VIEW_DISTANCE = "20";
|
||||||
|
|
||||||
MAX_MEMORY = "4G";
|
MAX_MEMORY = "6G";
|
||||||
MODRINTH_MODPACK = "https://cdn.modrinth.com/data/CIYf3Hk8/versions/Ym3sIi6H/Simpcraft-0.2.0.mrpack";
|
MODRINTH_MODPACK = "https://cdn.modrinth.com/data/CIYf3Hk8/versions/NGutsQSd/Simpcraft-0.2.1.mrpack";
|
||||||
|
|
||||||
TZ = "Europe/Dublin";
|
TZ = "Europe/Dublin";
|
||||||
};
|
};
|
||||||
environmentFiles = [ config.age.secrets."whale2/simpcraft.env".path ];
|
environmentFiles = [ config.age.secrets."whale2/simpcraft.env".path ];
|
||||||
|
|
||||||
volumes = [
|
volumes = [
|
||||||
"minecraft_staging_data:/data"
|
"kinkcraft_data:/data"
|
||||||
"${./icon.png}:/ext/icon.png:ro"
|
"${./icon.png}:/ext/icon.png:ro"
|
||||||
];
|
];
|
||||||
|
|
||||||
extraOptions = [
|
extraOptions = [
|
||||||
''--network=colony:${dockerNetAssignment allAssignments "simpcraft-staging-oci"}''
|
''--network=colony:${dockerNetAssignment allAssignments "kinkcraft-oci"}''
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
graeme = {
|
||||||
|
# 2026.2.1-java21-alpine
|
||||||
|
image = "itzg/minecraft-server@sha256:82adaddfe0156f07c34228f1c1065cdbd298abc174de0a9961abb068b11beebb";
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
TYPE = "VANILLA";
|
||||||
|
SERVER_PORT = "25569";
|
||||||
|
QUERY_PORT = "25569";
|
||||||
|
|
||||||
|
EULA = "true";
|
||||||
|
ENABLE_QUERY = "true";
|
||||||
|
ENABLE_RCON = "false";
|
||||||
|
MOTD = "§4§k----- §9G§ar§ba§ce§dm§ee §4§k-----";
|
||||||
|
ICON = "/ext/icon.png";
|
||||||
|
|
||||||
|
EXISTING_WHITELIST_FILE = "SYNCHRONIZE";
|
||||||
|
WHITELIST = concatStringsSep "," [
|
||||||
|
op
|
||||||
|
"fffa146c-0bc8-421c-9e3a-3635c0aca2ea" # Scarlehh
|
||||||
|
"1ea05f48-76cc-4034-bcd3-2fa1fc5a7375" # Dario
|
||||||
|
"4bf837b1-01db-4491-a0e0-700d98542833" # JoeSpencer
|
||||||
|
"d07a9554-1b05-4b0b-b558-27e4a86e1f53" # AmyClover
|
||||||
|
];
|
||||||
|
EXISTING_OPS_FILE = "SYNCHRONIZE";
|
||||||
|
OPS = op;
|
||||||
|
DIFFICULTY = "hard";
|
||||||
|
SPAWN_PROTECTION = "0";
|
||||||
|
VIEW_DISTANCE = "20";
|
||||||
|
|
||||||
|
MAX_MEMORY = "4G";
|
||||||
|
|
||||||
|
TZ = "Europe/Dublin";
|
||||||
|
};
|
||||||
|
|
||||||
|
volumes = [
|
||||||
|
"graeme_data:/data"
|
||||||
|
"${./graeme.png}:/ext/icon.png:ro"
|
||||||
|
];
|
||||||
|
|
||||||
|
extraOptions = [
|
||||||
|
''--network=colony:${dockerNetAssignment allAssignments "graeme-oci"}''
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
borgbackup.jobs.simpcraft =
|
||||||
|
let
|
||||||
|
rconCommand = cmd: ''${pkgs.mcrcon}/bin/mcrcon -H simpcraft-oci -p "$RCON_PASSWORD" "${cmd}"'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
paths = [ "/var/lib/containers/storage/volumes/minecraft_data/_data/world" ];
|
||||||
|
repo = "/var/lib/containers/backup/simpcraft";
|
||||||
|
doInit = true;
|
||||||
|
encryption.mode = "none";
|
||||||
|
compression = "zstd,10";
|
||||||
|
# every ~15 minutes offset from 5 minute intervals (Minecraft seems to save at precise times?)
|
||||||
|
startAt = "*:03,17,33,47";
|
||||||
|
prune.keep = {
|
||||||
|
within = "12H";
|
||||||
|
hourly = 48;
|
||||||
|
};
|
||||||
|
readWritePaths = [ "/var/lib/borgbackup" "/var/cache/borgbackup" ];
|
||||||
|
|
||||||
|
# Avoid Minecraft poking the files while we back up
|
||||||
|
preHook = rconCommand "save-off";
|
||||||
|
postHook = rconCommand "save-on";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd = {
|
||||||
|
services = {
|
||||||
|
borgbackup-job-simpcraft.serviceConfig.EnvironmentFile = [ config.age.secrets."whale2/simpcraft.env".path ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
my = {
|
my = {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user