Compare commits
24 Commits
docs
...
e47e30df19
| Author | SHA1 | Date | |
|---|---|---|---|
| e47e30df19 | |||
| e077c6e30c | |||
| 6f8d499ec2 | |||
| d0859d4c5c | |||
| 9b582bec7b | |||
| 8c4223af18 | |||
| b95992735e | |||
| 0d1562d150 | |||
| 49de78d47f | |||
| 832d0b5542 | |||
| 4d4c05ea70 | |||
| 0bd08c4a91 | |||
| a86888a2c7 | |||
| dc1ec3bf5a | |||
| 823ed83252 | |||
| 300103f2ba | |||
| d33f32ce5b | |||
| 14d9bba4eb | |||
| e63cee7b09 | |||
| a8318d3de2 | |||
| aab4a193ae | |||
| 541102f683 | |||
| 805590a705 | |||
| f8dbd99a7b |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(nix eval:*)",
|
||||
"Bash(nix flake check:*)",
|
||||
"Bash(nix build:*)",
|
||||
"Bash(check-system:*)",
|
||||
"Bash(build-system:*)",
|
||||
"Bash(build-home:*)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git show:*)",
|
||||
"Bash(drill:*)",
|
||||
"Bash(ping:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,4 @@ result*
|
||||
!/.vms/.gitkeep
|
||||
/.keys/*.key
|
||||
*.swp
|
||||
/.claude/settings.local.json
|
||||
|
||||
@@ -2,11 +2,23 @@
|
||||
|
||||
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. `home-switches.md`), not in agent memory. AGENTS.md is versioned and shared; memory is not.
|
||||
|
||||
Claude Code permissions live in two files: `.claude/settings.json` (versioned, shared — the
|
||||
committed allow list of safe-to-auto-approve commands) and `.claude/settings.local.json` (personal,
|
||||
gitignored — where interactive "always allow" grants accumulate). Put durable, generally-safe
|
||||
commands in the shared file; leave one-off or machine-specific grants in the local one.
|
||||
|
||||
## Overview
|
||||
|
||||
Personal Nix flake managing NixOS systems and home-manager configurations for a fleet of
|
||||
machines (servers, home boxes, routers). It is built around a **custom module system** layered
|
||||
on top of NixOS/home-manager, not the stock flake `nixosConfigurations` pattern.
|
||||
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
|
||||
|
||||
@@ -28,10 +40,16 @@ Common ones:
|
||||
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).
|
||||
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`).
|
||||
(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.
|
||||
@@ -40,6 +58,12 @@ 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`.
|
||||
|
||||
For DNS lookups use **`drill`** (ldns) — `dig` isn't installed in this environment (it fails with
|
||||
exit 127, which is easy to miss if stderr is redirected). E.g. `drill -Q @<resolver> <name> A`.
|
||||
|
||||
For privilege escalation use **`doas`**, not `sudo` — the boxes don't install `sudo` (it fails with
|
||||
`command not found`). E.g. `doas ip link set <if> up`.
|
||||
|
||||
## Architecture
|
||||
|
||||
### The custom module system
|
||||
@@ -91,6 +115,51 @@ Per-host configs live under `nixos/boxes/<host>` (some are single `.nix` files,
|
||||
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.
|
||||
|
||||
### 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 **`home-switches.md`** at the repo root — 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; `home-switches.md` documents the switch layout and per-switch config for the WAN design.
|
||||
|
||||
### Home wireless APs (`vibe` / `wave`)
|
||||
The home Wi-Fi APs are also **not** managed by this flake: `vibe` (MikroTik cAP ax, RouterOS) and
|
||||
`wave` (Cudy AX3000 running OpenWrt/UCI). They are dumb APs — bridge clients onto the right VLAN,
|
||||
routers do DHCP/RA/firewall. The trunk/VLAN design, the OpenWrt flash + config for `wave`, and the
|
||||
per-AP management addressing live in **`home-aps.md`** at the repo root — read it before touching AP
|
||||
config, and update it when an AP changes. Only the DNS records live in the flake
|
||||
(`routing-common/dns.nix`).
|
||||
|
||||
## Secrets
|
||||
|
||||
age-encrypted secrets in `secrets/`, managed with **ragenix**. Each module declares
|
||||
@@ -103,6 +172,12 @@ 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.*`.
|
||||
@@ -111,3 +186,6 @@ private keys) is required for editing secrets, deploying, and running dev VMs.
|
||||
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.
|
||||
|
||||
Generated
+130
-4
@@ -8,7 +8,7 @@
|
||||
"ragenix",
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_7"
|
||||
"systems": "systems_8"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1761656077,
|
||||
@@ -67,6 +67,34 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"bun2nix": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"nixpkgs": [
|
||||
"pi-agent",
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": [
|
||||
"pi-agent",
|
||||
"systems"
|
||||
],
|
||||
"treefmt-nix": "treefmt-nix_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1778446047,
|
||||
"narHash": "sha256-oQvcadh2BCkrog+SGrG6YffKJrveYpjj3TdQJWaKhaM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "bun2nix",
|
||||
"rev": "f2bc12af1a6369648aac41041ceeaa0b866599c6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "2.1.0",
|
||||
"repo": "bun2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"copyparty": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_5",
|
||||
@@ -256,6 +284,28 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"pi-agent",
|
||||
"bun2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1777988971,
|
||||
"narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
@@ -276,7 +326,7 @@
|
||||
},
|
||||
"flake-utils_10": {
|
||||
"inputs": {
|
||||
"systems": "systems_9"
|
||||
"systems": "systems_10"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709126324,
|
||||
@@ -294,7 +344,7 @@
|
||||
},
|
||||
"flake-utils_11": {
|
||||
"inputs": {
|
||||
"systems": "systems_10"
|
||||
"systems": "systems_11"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705309234,
|
||||
@@ -444,7 +494,7 @@
|
||||
},
|
||||
"flake-utils_9": {
|
||||
"inputs": {
|
||||
"systems": "systems_8"
|
||||
"systems": "systems_9"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
@@ -586,6 +636,21 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"jail-nix": {
|
||||
"locked": {
|
||||
"lastModified": 1776230864,
|
||||
"narHash": "sha256-YsEjjdOsGEzTeD+iT7ONh071BqWAOQWpzYVei3okAXE=",
|
||||
"owner": "~alexdavid",
|
||||
"repo": "jail.nix",
|
||||
"rev": "404e7da9da5ab9aa643666682b2ba1312fa5fbe8",
|
||||
"type": "sourcehut"
|
||||
},
|
||||
"original": {
|
||||
"owner": "~alexdavid",
|
||||
"repo": "jail.nix",
|
||||
"type": "sourcehut"
|
||||
}
|
||||
},
|
||||
"libnetRepo": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@@ -795,6 +860,29 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pi-agent": {
|
||||
"inputs": {
|
||||
"bun2nix": "bun2nix",
|
||||
"jail-nix": "jail-nix",
|
||||
"nixpkgs": [
|
||||
"nixpkgs-unstable"
|
||||
],
|
||||
"systems": "systems_7"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784984457,
|
||||
"narHash": "sha256-y7jv+RTP0TkcJaDm7/D2xIFF9JDWKc6g5L3X3EUu3Eo=",
|
||||
"owner": "lukasl-dev",
|
||||
"repo": "pi.nix",
|
||||
"rev": "fd2c62853f66a5803dae621769570f1165e59b80",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "lukasl-dev",
|
||||
"repo": "pi.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pyproject-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -860,6 +948,7 @@
|
||||
"nixpkgs-mine-stable": "nixpkgs-mine-stable",
|
||||
"nixpkgs-stable": "nixpkgs-stable",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||
"pi-agent": "pi-agent",
|
||||
"ragenix": "ragenix",
|
||||
"sharry": "sharry"
|
||||
}
|
||||
@@ -957,6 +1046,21 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_11": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
@@ -1098,6 +1202,28 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"pi-agent",
|
||||
"bun2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775636079,
|
||||
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
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 =
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
# Home wireless APs
|
||||
|
||||
Reference for the home Wi-Fi access points. Like the switches (`home-switches.md`), these are **not**
|
||||
managed by this flake — they are configured on-device (RouterOS on the MikroTik, OpenWrt/UCI on the
|
||||
Cudy). This file documents the shared VLAN/trunk design and each AP.
|
||||
|
||||
Only the DNS records live in the flake (`nixos/boxes/home/routing-common/dns.nix`, `h.nul.ie` zone).
|
||||
Everything else here is applied by hand on the device.
|
||||
|
||||
## The APs
|
||||
|
||||
| | vibe | wave |
|
||||
|---|---|---|
|
||||
| Model | MikroTik **cAP ax** (`cAPGi-5HaxD2HaxD`) | Cudy **AX3000** (OpenWrt id `cudy,ap3000-v1`) |
|
||||
| OS | RouterOS 7.x | OpenWrt 25.12.x (MT7981B / Filogic 820) |
|
||||
| Radio | 2×2 both bands | 2×2 both bands (2 spatial streams) |
|
||||
| Uplink | trunk (multi-port; `ether2` is a wired LAN port) | single **2.5 GbE** trunk |
|
||||
| Management | core/hi/lo `.15` | core/lo `.14` (**not** `hi`; see below) |
|
||||
|
||||
Both mirror the same two SSIDs. `wave` replaced an older AP of the same name; the new one is the
|
||||
Cudy running OpenWrt.
|
||||
|
||||
> **Note on "AX3000":** MT7981B is 2×2 (2 spatial streams). The "3000" is *aggregate* Mbps —
|
||||
> 574 (2.4 GHz 2ss) + 2402 (5 GHz 2ss @ **160 MHz**) — not three streams. `iw` confirms 2×2
|
||||
> (`Available Antennas TX/RX 0x3`; "3 streams: not supported"). 160 MHz is what earns the "3000".
|
||||
|
||||
## Shared design (dumb AP)
|
||||
|
||||
Every AP is a **dumb AP**: it bridges wireless clients onto the right VLAN and does **no** routing,
|
||||
DHCP, RA or firewalling. The home routers (`river`/`stream`) own DHCP/RA/gateway (per-VLAN VRRP VIPs)
|
||||
and firewalling. The uplink is a **tagged trunk**:
|
||||
|
||||
| VLAN | `lib.my.c.home.vlans` | Role | AP use |
|
||||
|---|---|---|---|
|
||||
| — (native) | `core` | switch/fabric management (1500) | backup management (untagged) |
|
||||
| 100 | `hi` | trusted LAN, **high-MTU** (jumbo 9000) | `vibe` management (it's jumbo-capable) |
|
||||
| 110 | `lo` | trusted LAN (1500) | main SSID `wlan0`; `wave` management |
|
||||
| 120 | `untrusted` | guest network | guest SSID `wlan1` |
|
||||
|
||||
`hi` and `lo` are both **trusted** client VLANs — the only difference is MTU (`hi` carries jumbo
|
||||
9000, `lo` is standard 1500). An AP puts its own management on whichever it can do: `vibe` (jumbo)
|
||||
sits on `hi`, `wave` (eth0 capped at 2026) sits on `lo`. The main SSID lands on `lo` because Wi-Fi
|
||||
clients are 1500 regardless.
|
||||
|
||||
### SSIDs
|
||||
|
||||
| SSID | Bands | Security | VLAN |
|
||||
|---|---|---|---|
|
||||
| `wlan0` (main) | 5 GHz + 2.4 GHz | WPA2/WPA3-PSK (`sae-mixed`) | 110 (`lo`) |
|
||||
| `wlan1` (guest) | 2.4 GHz | WPA2-PSK (`psk2`) | 120 (`untrusted`) |
|
||||
|
||||
**Passphrases are never stored in this repo.** `vibe` is the source of truth; read them out-of-band
|
||||
with `ssh admin@vibe '/interface wifi export show-sensitive'` (`.passphrase=` prints **unquoted**).
|
||||
|
||||
## vibe (MikroTik cAP ax)
|
||||
|
||||
RouterOS, one hardware-offloaded bridge `main` with `vlan-filtering=yes`. Access: `ssh admin@vibe`
|
||||
— key auth for `admin` is installed (`~/.ssh/id_rsa`), with `admin`/`admin` as a fallback.
|
||||
|
||||
- **Uplink `ether1`** — trunk, tagged VLANs 100/110/120; native/untagged is the default VLAN 1
|
||||
(PVID, no IP). `ether2` is a wired **access port** on VLAN 110 (`lo`). `l2mtu 9214`.
|
||||
- **Radios** — `wifi1` (5 GHz, 20/40/80) + `wifi2` (2.4 GHz, 20/40) both broadcast `wlan0`
|
||||
(WPA2/WPA3-PSK), untagged onto VLAN 110. `wifi3` is a virtual AP on `wifi2` broadcasting `wlan1`
|
||||
(WPA2-PSK), untagged onto VLAN 120. `country=Ireland`.
|
||||
- **Bridge VLANs** — 100 tagged `main,ether1`; 110 tagged `main,ether1` + untagged
|
||||
`ether2,wifi1,wifi2`; 120 tagged `main,ether1` + untagged `wifi3`.
|
||||
- **Management** — `jim`/`dave`-style (core/hi/lo), on host `.15`: `192.168.64.15` on core
|
||||
(native/untagged, backup), `192.168.68.15/22` + `2a0e:97c0:4d0:1::1:6` on the `hi` VLAN-100
|
||||
interface (holds the default route, via the hi VIP `192.168.71.254`), and
|
||||
`192.168.72.15/21` + `2a0e:97c0:4d0:2::1:6` on `lo` VLAN 110. No IP on `untrusted`.
|
||||
`l2mtu 9214`, so `hi` carries jumbo (9000) here — `vibe` sits on `hi` because it *can* jumbo,
|
||||
unlike `wave` (see its MTU note).
|
||||
- **Roaming** — 802.11k/v via a `/interface wifi steering` profile (`rrm=yes wnm=yes`,
|
||||
`neighbor-group=home-aps`) assigned to `wifi1`/`wifi2`/`wifi3`.
|
||||
- **Resolver** — the hi VIP `192.168.71.254` / `2a0e:97c0:4d0:1::ffff`.
|
||||
|
||||
## wave (Cudy AX3000, OpenWrt)
|
||||
|
||||
Single-port AP, so the port is a VLAN **trunk** carrying management + both SSIDs.
|
||||
|
||||
### Management addressing
|
||||
|
||||
`wave` takes host `.14`, on **`lo` (primary) and `core` (backup)** — deliberately **not** `hi`,
|
||||
unlike the switches and `vibe`. `hi` is the jumbo (9000) VLAN, but `wave`'s eth0 caps at 2026 (see
|
||||
MTU note), so there's no reason to put it there; `lo` is 1500 with a proper VRRP VIP for the default
|
||||
route + resolver, and `core` has no VIP/v6 so it can only be a backup. No IP on `untrusted`. Records
|
||||
in `dns.nix`:
|
||||
|
||||
| Name | VLAN | Address |
|
||||
|---|---|---|
|
||||
| `wave-core` | core (native/untagged) | `192.168.64.14/24` — backup, like the switches (no VIP → backup only) |
|
||||
| `wave` | lo 110 | `192.168.72.14/21`, `2a0e:97c0:4d0:2::1:5` — primary; holds the default route + resolver (lo VIP `192.168.79.254` / `2a0e:97c0:4d0:2::ffff`) |
|
||||
|
||||
**Firewall:** management (SSH/LuCI) reachable from `core`/`lo` only; `untrusted` is a separate
|
||||
zone with `input REJECT` (and `wave` has no IP there) — **no management via the guest VLAN**.
|
||||
|
||||
### brian switch port
|
||||
|
||||
`wave` hangs off **brian** (UniFi). Its port is a **trunk**: tagged VLAN **110/120** (`lo` + guest),
|
||||
and **native/untagged = core** (the fabric's management VLAN, carrying `wave-core`). VLAN 100 (`hi`)
|
||||
is **not** needed here — `wave` isn't on `hi` (see Management addressing). Configure via the UniFi
|
||||
controller (brian has no CLI); see `home-switches.md`.
|
||||
|
||||
### Flashing OpenWrt (Cudy AX3000 / `cudy_ap3000-v1`)
|
||||
|
||||
Hardware: MT7981B, 512 MB RAM, 256 MB SPI-NAND, 1× 2.5 GbE (RTL8221B), 2×2 WiFi 6.
|
||||
|
||||
> ⚠️ **Serial caveat:** units with a serial starting `2543…` (post ~Nov 2025) use a different flash
|
||||
> chip and can brick with older firmware. Match firmware to the unit.
|
||||
|
||||
OpenWrt can't be flashed directly over stock. Two-stage, via a Cudy **transition** firmware (Cudy
|
||||
OpenWrt download page / `support@cudy.com`; `warnning.txt` in that bundle has the steps):
|
||||
1. Stock Cudy UI: update to **≥ 2.4.7** (adds TFTP `recovery.bin` recovery), then flash the Cudy
|
||||
**intermediate** firmware (`cudy_ap3000-v1-sysupgrade_*.bin`), "keep settings" **unchecked**.
|
||||
It reboots into an OpenWrt-based build at `192.168.1.1` (SSH `root`, empty password).
|
||||
2. From there, `sysupgrade -n` to vanilla OpenWrt (`…-cudy_ap3000-v1-squashfs-sysupgrade.bin` from
|
||||
`downloads.openwrt.org`; this release ships **no** factory image — sysupgrade only).
|
||||
|
||||
Stock default (out of box) is a DHCP client falling back to **`192.168.10.254`**; the stock UI is a
|
||||
customised LuCI (only 80/443, no SSH) with a first-boot "create admin password" wizard — so the
|
||||
stock-side flashing is done from a browser, not headless.
|
||||
|
||||
### On-device config notes
|
||||
|
||||
- Package manager is **`apk`** (not `opkg`). WiFi runs **`wpad-mbedtls`** (full — swapped from the
|
||||
default `wpad-basic-mbedtls`, which lacks 802.11v). **802.11k + 802.11v** (`ieee80211k` +
|
||||
`bss_transition`) are enabled on all SSIDs. ⚠️ Swapping wpad **live** leaves the mac80211 vifs
|
||||
stuck in a start→teardown loop (`nl80211 ... No such device`); a `wifi reload`/`network restart`
|
||||
won't recover it — **reboot** after `apk add wpad-mbedtls`.
|
||||
- Radios: `radio0` = 2.4 GHz, `radio1` = 5 GHz (keyed by `band`, don't assume). 5 GHz is pinned to
|
||||
**channel 36 / HE160** (any 160 MHz block in IE is DFS; ch36 has the shortest ~60 s CAC).
|
||||
- Bridge: `br-lan` with `vlan_filtering`, single port `eth0` — tagged `110/120`, untagged/PVID
|
||||
VLAN 1 (= native/core). SSIDs attach via `network` = `lo`/`untrusted` (= `br-lan.110`/`.120`).
|
||||
- Dumb-AP: no DHCP pools, `odhcpd.maindhcp=0`, `delegate=0` on the L3 interfaces.
|
||||
- **MTU:** all interfaces are **1500**. The `mtk_eth_soc` 2.5 GbE (`eth0`) caps at **2026 bytes**
|
||||
(`ip link set eth0 mtu 9000` → `SIOCSIFMTU: Invalid argument`), so `wave` can't join `hi`'s jumbo
|
||||
(9000) fabric like `vibe` does — which is precisely **why `wave` is managed on `lo`, not `hi`**
|
||||
(see Management addressing). Nothing on `wave` needs > 1500.
|
||||
- **LuCI:** enabled, login `root` / `admin`. **SSH:** key-only (`PasswordAuth`/`RootPasswordAuth off`).
|
||||
- `iperf3` installed for throughput testing.
|
||||
|
||||
### Access
|
||||
|
||||
- SSH: `ssh root@wave` (key-only; `wave`/`wave-core` resolve once `dns.nix` is deployed).
|
||||
- LuCI: `http://192.168.72.14/` (or `http://wave/`), `root` / `admin`.
|
||||
@@ -8,6 +8,8 @@ let
|
||||
inherit (lib.my) mkOpt' dummyOption;
|
||||
in
|
||||
{
|
||||
imports = [ inputs.pi-agent.homeModules.default ];
|
||||
|
||||
options = with lib.types; {
|
||||
my = {
|
||||
isStandalone = mkOption {
|
||||
@@ -27,6 +29,7 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkMerge [
|
||||
{
|
||||
my = {
|
||||
@@ -213,6 +216,7 @@ in
|
||||
jq
|
||||
yq-go
|
||||
nix-tree
|
||||
treemd
|
||||
];
|
||||
|
||||
sessionVariables = {
|
||||
@@ -229,6 +233,7 @@ in
|
||||
inputs.deploy-rs.overlays.default
|
||||
inputs.boardie.overlays.default
|
||||
inputs.nixGL.overlays.default
|
||||
inputs.pi-agent.overlays.default
|
||||
];
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
|
||||
@@ -71,8 +71,10 @@ in
|
||||
|
||||
python3Packages.python-lsp-server
|
||||
nil # nix language server
|
||||
nixd # another nix language server
|
||||
zls # zig language server
|
||||
rust-analyzer
|
||||
pyright
|
||||
|
||||
cowsay
|
||||
fortune
|
||||
@@ -85,6 +87,17 @@ in
|
||||
|
||||
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 ]}
|
||||
'';
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
# 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 switches
|
||||
|
||||
| | jim | dave | brian |
|
||||
|---|---|---|---|
|
||||
| Identity | `jim-sw` | `dave-sw` | (UniFi) |
|
||||
| Model | CRS326-24G-2S+ | CRS504-4XQ | Ubiquiti Switch Pro XG 8 PoE |
|
||||
| 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 | 8×10GBASE-T PoE + 2×25G SFP28 |
|
||||
| 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: fort, pronter, laptop-dock, palace-kvm, │
|
||||
│ ups, ether15-20, wan1/wan2/wan-in │
|
||||
│ wan-pon-in = sfp-sfpplus2 (spare SFP+) │
|
||||
└───────┬──────────────────────────────────────────────┘
|
||||
│ dave-uplink = sfp-sfpplus1 (10G trunk)
|
||||
│ also: palace, stream (1G secondaries),
|
||||
│ castle (2.5G, normally down)
|
||||
┌───────┴──────────────────────────────────────────────┐
|
||||
│ dave CRS504-4XQ (Marvell 98DX4310) │
|
||||
│ jim-downlink = qsfp28-3-1 │
|
||||
└──┬──────────────────┬───────────────────┬────────────┘
|
||||
│ palace │ castle │ brian-downlink
|
||||
│ = qsfp28-1-1 │ (100G) │ 802.3ad LAG
|
||||
│ (100G) │ │ (brian1 + brian2)
|
||||
│ … │
|
||||
┌───────┴────────┐ ┌───────┴─────────────┐
|
||||
│ palace host │ │ brian │
|
||||
│ └ river (VM) │ │ Switch Pro XG 8 PoE │
|
||||
└────────────────┘ └───────┬─────────────┘
|
||||
river WAN + LAN ride the 100G link │ hosts the ONT
|
||||
│
|
||||
┌───────┴─────────────┐
|
||||
│ ONT (Digiweb) │ untagged mgmt
|
||||
│ PPPoE via ONT │ 192.168.100.1
|
||||
└─────────────────────┘ + VLAN 10; PVID 140
|
||||
```
|
||||
|
||||
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).
|
||||
- **castle** is dual-homed but **not** via STP: its primary uplink is dave's **100G** `castle` port
|
||||
(`et100g`, active), and it has a secondary **2.5G** link to jim's `castle` edge port (`et2.5g`,
|
||||
**normally down** — no live failover). ⚠️ **castle's root disk is NVMe-oF over the fabric** (via
|
||||
`et100g`→dave), so rebooting **dave** — or downing castle's `et100g` — freezes castle mid-I/O.
|
||||
Do dave maintenance (upgrades/reboots) from a host that doesn't depend on dave for storage or
|
||||
network, or with castle cleanly powered off; don't drive it from castle.
|
||||
- **brian** is a Ubiquiti **Switch Pro XG 8 PoE** (8×10GBASE-T), 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` 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`: `jim` → hi `.10`, `dave` → hi
|
||||
`.11`, `brian` → core `.13`). From a box on the home network just `ssh admin@jim` / `admin@dave`.
|
||||
|
||||
**Key auth** for `admin` is installed on jim/dave (and the `vibe` AP) — `ssh -i ~/.ssh/id_rsa
|
||||
admin@jim` works keyless (imported via `/user ssh-keys import`). Password `admin`/`admin` remains as
|
||||
a fallback. Non-interactive password pattern (avoids the ssh-agent hang) if the key isn't available:
|
||||
|
||||
```
|
||||
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) | — | — |
|
||||
+14
-1
@@ -312,6 +312,16 @@ rec {
|
||||
lo = 110;
|
||||
untrusted = 120;
|
||||
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 home-switches.md).
|
||||
pon-isp = 10;
|
||||
wan-pon-ont = 140;
|
||||
wan-pon-isp = 141;
|
||||
};
|
||||
hiMTU = 9000;
|
||||
routers = [
|
||||
@@ -319,7 +329,7 @@ rec {
|
||||
"stream"
|
||||
];
|
||||
routersPubV4 = [
|
||||
"109.255.108.88"
|
||||
"84.203.124.128" # river: Digiweb static
|
||||
"109.255.108.121"
|
||||
];
|
||||
|
||||
@@ -327,6 +337,9 @@ rec {
|
||||
modem = {
|
||||
v4 = "192.168.0.0/24";
|
||||
};
|
||||
ont = {
|
||||
v4 = "192.168.100.0/24";
|
||||
};
|
||||
all = {
|
||||
v4 = "192.168.64.0/18";
|
||||
v6 = "2a0e:97c0:4d0::/60";
|
||||
|
||||
+4
-3
@@ -175,11 +175,11 @@ rec {
|
||||
};
|
||||
|
||||
vm = rec {
|
||||
lvmDisk' = name: lv: {
|
||||
lvmDisk'' = name: vg: lv: {
|
||||
inherit name;
|
||||
backend = {
|
||||
driver = "host_device";
|
||||
filename = "/dev/main/${lv}";
|
||||
filename = "/dev/${vg}/${lv}";
|
||||
# It appears this needs to be set on the backend _and_ the format
|
||||
discard = "unmap";
|
||||
};
|
||||
@@ -189,7 +189,8 @@ rec {
|
||||
};
|
||||
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}";
|
||||
};
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ in
|
||||
shell = pkgs.bashInteractive;
|
||||
openssh.authorizedKeys.keyFiles = [
|
||||
lib.my.c.sshKeyFiles.harmonia
|
||||
lib.my.c.sshKeyFiles.me
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ in
|
||||
};
|
||||
};
|
||||
});
|
||||
kernelModules = [ "kvm-amd" ];
|
||||
kernelModules = [ "dm-raid" "kvm-amd" ];
|
||||
kernelParams = [ "amd_iommu=on" ];
|
||||
initrd = {
|
||||
availableKernelModules = [ "xhci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" "sr_mod" ];
|
||||
@@ -126,9 +126,9 @@ in
|
||||
};
|
||||
linkConfig.Name = "et1g0";
|
||||
};
|
||||
"10-lan-core" = {
|
||||
"10-lan-core-phy" = {
|
||||
matchConfig.PermanentMACAddress = "e0:d5:5e:68:0c:70";
|
||||
linkConfig.Name = "lan-core";
|
||||
linkConfig.Name = "lan-core-phy";
|
||||
};
|
||||
"10-et100g" = {
|
||||
matchConfig = {
|
||||
@@ -145,6 +145,12 @@ in
|
||||
netdevs = mkMerge [
|
||||
(mkVLAN "lan-hi" vlans.hi)
|
||||
(mkVLAN "lan-lo-phy" vlans.lo)
|
||||
{
|
||||
"25-lan-core".netdevConfig = {
|
||||
Name = "lan-core";
|
||||
Kind = "bridge";
|
||||
};
|
||||
}
|
||||
{
|
||||
"25-lan-lo".netdevConfig = {
|
||||
Name = "lan-lo";
|
||||
@@ -199,6 +205,12 @@ in
|
||||
};
|
||||
"60-lan-hi" = networkdAssignment "lan-hi" assignments.hi;
|
||||
|
||||
"50-lan-core-phy" = {
|
||||
matchConfig.Name = "lan-core-phy";
|
||||
networkConfig = {
|
||||
Bridge = "lan-core";
|
||||
} // networkd.noL3;
|
||||
};
|
||||
"50-lan-lo-phy" = {
|
||||
matchConfig.Name = "lan-lo-phy";
|
||||
networkConfig = {
|
||||
|
||||
@@ -172,12 +172,23 @@
|
||||
};
|
||||
memory = 32768;
|
||||
cleanShutdown.timeout = 120;
|
||||
networks.netboot = {
|
||||
bridge = "lan-lo";
|
||||
waitOnline = "carrier";
|
||||
mac = "52:54:00:a5:7e:93";
|
||||
extraOptions.bootindex = 1;
|
||||
networks = {
|
||||
netboot = {
|
||||
bridge = "lan-lo";
|
||||
waitOnline = "carrier";
|
||||
mac = "52:54:00:a5:7e:93";
|
||||
extraOptions.bootindex = 1;
|
||||
};
|
||||
core = {
|
||||
bridge = "lan-core";
|
||||
ifname = "vm-sfh-core";
|
||||
waitOnline = "carrier";
|
||||
mac = "52:54:00:72:67:51";
|
||||
};
|
||||
};
|
||||
drives = [
|
||||
(vm.lvmDisk' "hdds" "frigate")
|
||||
];
|
||||
hostDevices = {
|
||||
et100g0vf2 = {
|
||||
index = 0;
|
||||
|
||||
@@ -8,9 +8,23 @@
|
||||
|
||||
configuration = { lib, modulesPath, pkgs, config, assignments, allAssignments, ... }:
|
||||
let
|
||||
inherit (lib.my) networkdAssignment mkVLAN;
|
||||
inherit (builtins) elemAt;
|
||||
inherit (lib) mkForce mkMerge;
|
||||
inherit (lib.my) net networkdAssignment mkVLAN;
|
||||
inherit (lib.my.c) networkd;
|
||||
inherit (lib.my.c.home) vlans domain prefixes roceBootModules;
|
||||
inherit (lib.my.c.home) vlans domain prefixes roceBootModules routersPubV4;
|
||||
|
||||
# river reaches the ONT over its 100G `lan` uplink to the dave switch (which downlinks to
|
||||
# brian, where the ONT lands). Digiweb delivers the ISP VLAN (pon-isp, 10) single-tagged at
|
||||
# the ONT alongside the ONT's untagged management traffic. With a single ONT there's no VLAN
|
||||
# collision, so the switches simply trunk the ISP's VLAN 10 straight through to river (PPPoE
|
||||
# runs directly on it) and PVID the ONT's untagged management port onto wan-pon-ont (140).
|
||||
# river takes .100 in the ONT's /24 (matching stream's modem-mgmt .100) to reach its web
|
||||
# UI at 192.168.100.1. (See home-switches.md for the switch side and the multi-ONT plan.)
|
||||
ontV4 = net.cidr.host 100 prefixes.ont.v4;
|
||||
|
||||
# river is routing-common index 0; the Digiweb static IP we request via IPCP
|
||||
pubV4 = elemAt routersPubV4 0;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
@@ -71,11 +85,75 @@
|
||||
dmeventd.enable = true;
|
||||
};
|
||||
fstrim.enable = true;
|
||||
|
||||
# TODO: re-enable once scheduling is tested
|
||||
networkd-dispatcher.enable = mkForce false;
|
||||
|
||||
pppd = {
|
||||
enable = true;
|
||||
peers.digiweb = {
|
||||
autostart = true;
|
||||
enable = true;
|
||||
# Password is shared across all Digiweb customers, so no need for a secret
|
||||
config = ''
|
||||
plugin pppoe.so wan-pon-isp
|
||||
name "digiweb@nga.digiweb.ie"
|
||||
password "digiweb"
|
||||
# request our static IP as the local address in IPCP (local:remote, remote left open)
|
||||
${pubV4}:
|
||||
# no usepeerdns: we ignore Digiweb's resolvers and use the local recursive resolver
|
||||
lcp-echo-interval 1
|
||||
lcp-echo-failure 4
|
||||
noauth
|
||||
persist
|
||||
maxfail 0
|
||||
holdoff 5
|
||||
mtu 1500
|
||||
mru 1500
|
||||
noaccomp
|
||||
default-asyncmap
|
||||
ifname wan
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# PPPoE WAN (Digiweb): pppd owns the `wan` interface on top of wan-pon-isp (the switch's
|
||||
# swap of the ISP's VLAN 10), and its ip-up/ip-down hooks toggle the shared
|
||||
# wan-online.target. Nothing else Wants the target, so it stays inactive until the link
|
||||
# is actually up.
|
||||
systemd.targets.wan-online.unitConfig.DefaultDependencies = false;
|
||||
|
||||
environment.etc = {
|
||||
ppp-up = {
|
||||
target = "ppp/ip-up";
|
||||
mode = "0755";
|
||||
text = ''
|
||||
#!${pkgs.runtimeShell}
|
||||
${pkgs.iproute2}/bin/ip route add default dev wan scope link metric 100
|
||||
${config.systemd.package}/bin/systemctl --no-block start wan-online.target
|
||||
'';
|
||||
};
|
||||
ppp-down = {
|
||||
target = "ppp/ip-down";
|
||||
mode = "0755";
|
||||
text = ''
|
||||
#!${pkgs.runtimeShell}
|
||||
${config.systemd.package}/bin/systemctl --no-block stop wan-online.target
|
||||
${pkgs.iproute2}/bin/ip route del default dev wan scope link metric 100
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
systemd.network = {
|
||||
netdevs = mkMerge [
|
||||
(mkVLAN "wan-pon-ont" vlans.wan-pon-ont)
|
||||
# The ISP VLAN is trunked through untranslated, so this is the raw pon-isp (10)
|
||||
(mkVLAN "wan-pon-isp" vlans.pon-isp)
|
||||
];
|
||||
|
||||
links = {
|
||||
"10-wan" = {
|
||||
"10-wan-old" = {
|
||||
matchConfig = {
|
||||
# Matching against MAC address seems to break VLAN interfaces
|
||||
# (since they share the same MAC address)
|
||||
@@ -83,7 +161,7 @@
|
||||
PermanentMACAddress = "e0:d5:5e:68:0c:6e";
|
||||
};
|
||||
linkConfig = {
|
||||
Name = "wan";
|
||||
Name = "wan-old";
|
||||
RxBufferSize = 4096;
|
||||
TxBufferSize = 4096;
|
||||
};
|
||||
@@ -101,8 +179,38 @@
|
||||
};
|
||||
};
|
||||
|
||||
# So we don't drop the IP we use to connect to NVMe-oF!
|
||||
networks."60-lan-hi".networkConfig.KeepConfiguration = "static";
|
||||
networks = {
|
||||
"55-lan" = {
|
||||
# both WAN VLANs arrive single-tagged on the 100G uplink to dave: wan-pon-ont (140,
|
||||
# the ONT's management, PVID-tagged at the brian edge) and the ISP's VLAN 10, trunked
|
||||
# straight through
|
||||
vlan = [ "wan-pon-ont" "wan-pon-isp" ];
|
||||
};
|
||||
# So we don't drop the IP we use to connect to NVMe-oF!
|
||||
"60-lan-hi".networkConfig.KeepConfiguration = "static";
|
||||
|
||||
# ONT management: the brian edge PVIDs the ONT's untagged port onto wan-pon-ont, so give
|
||||
# ourselves an address in its /24 to reach the ONT's web UI at 192.168.100.1.
|
||||
"70-wan-pon-ont" = {
|
||||
matchConfig.Name = "wan-pon-ont";
|
||||
address = [ "${ontV4}/24" ];
|
||||
linkConfig = {
|
||||
RequiredForOnline = "no";
|
||||
MTUBytes = "1500";
|
||||
};
|
||||
};
|
||||
# pppd attaches PPPoE to this; just needs to be up with no L3. This is the ISP's
|
||||
# VLAN 10 trunked straight through from the ONT (no switch translation; see "55-lan").
|
||||
"71-wan-pon-isp" = {
|
||||
matchConfig.Name = "wan-pon-isp";
|
||||
linkConfig = {
|
||||
RequiredForOnline = "no";
|
||||
# baby jumbo: PPPoE's 8B overhead leaves a clean 1500 on ppp
|
||||
MTUBytes = "1508";
|
||||
};
|
||||
networkConfig = networkd.noL3;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
my = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
imports = [
|
||||
# ./unifi.nix
|
||||
./unifi.nix
|
||||
./hass.nix
|
||||
];
|
||||
}
|
||||
|
||||
@@ -24,6 +24,15 @@ in
|
||||
address = net.cidr.host (65536*5+1) prefixes.hi.v6;
|
||||
};
|
||||
};
|
||||
core = {
|
||||
inherit domain;
|
||||
name = "unifi-ctr-core";
|
||||
mtu = 1500;
|
||||
ipv4 = {
|
||||
address = net.cidr.host 21 prefixes.core.v4;
|
||||
gateway = null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
configuration = { lib, config, pkgs, assignments, ... }:
|
||||
@@ -48,7 +57,10 @@ in
|
||||
};
|
||||
|
||||
systemd = {
|
||||
network.networks."80-container-host0" = networkdAssignment "host0" assignments.hi;
|
||||
network.networks = {
|
||||
"80-container-host0" = networkdAssignment "host0" assignments.hi;
|
||||
"80-lan-core" = networkdAssignment "lan-core" assignments.core;
|
||||
};
|
||||
};
|
||||
|
||||
services = {
|
||||
|
||||
@@ -72,6 +72,10 @@ in
|
||||
fsType = "ext4";
|
||||
neededForBoot = true;
|
||||
};
|
||||
"/mnt/frigate" = {
|
||||
device = "/dev/disk/by-label/frigate";
|
||||
fsType = "ext4";
|
||||
};
|
||||
};
|
||||
|
||||
networking = { inherit domain; };
|
||||
@@ -111,6 +115,13 @@ in
|
||||
MTUBytes = toString lib.my.c.home.hiMTU;
|
||||
};
|
||||
};
|
||||
"10-lan-core-ctrs" = {
|
||||
matchConfig = {
|
||||
Driver = "virtio_net";
|
||||
PermanentMACAddress = "52:54:00:72:67:51";
|
||||
};
|
||||
linkConfig.Name = "lan-core-ctrs";
|
||||
};
|
||||
"10-lan-lo-ctrs" = {
|
||||
matchConfig = {
|
||||
Driver = "virtio_net";
|
||||
@@ -131,6 +142,11 @@ in
|
||||
linkConfig.RequiredForOnline = "no";
|
||||
networkConfig = networkd.noL3;
|
||||
};
|
||||
"30-lan-core-ctrs" = {
|
||||
matchConfig.Name = "lan-core-ctrs";
|
||||
linkConfig.RequiredForOnline = "no";
|
||||
networkConfig = networkd.noL3;
|
||||
};
|
||||
"30-lan-lo-ctrs" = {
|
||||
matchConfig.Name = "lan-lo-ctrs";
|
||||
linkConfig.RequiredForOnline = "no";
|
||||
@@ -145,6 +161,11 @@ in
|
||||
MACVLAN = mkForce "lan-hi-ctrs:host0 lan-lo-ctrs:lan-lo";
|
||||
};
|
||||
};
|
||||
unifi = {
|
||||
networkConfig = {
|
||||
MACVLAN = mkForce "lan-hi-ctrs:host0 lan-core-ctrs:lan-core";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services = {
|
||||
@@ -174,9 +195,13 @@ in
|
||||
containers.instances =
|
||||
let
|
||||
instances = {
|
||||
# unifi = {};
|
||||
unifi = {};
|
||||
hass = {
|
||||
bindMounts = {
|
||||
"/mnt/frigate" = {
|
||||
mountPoint = "/var/lib/frigate";
|
||||
readOnly = false;
|
||||
};
|
||||
"/dev/bus/usb/001/002".readOnly = false;
|
||||
"/dev/video0".readOnly = false;
|
||||
"/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_ce549704fe38ef11a2c2e5d154516304-if00-port0" = {
|
||||
|
||||
@@ -11,12 +11,6 @@ in
|
||||
{
|
||||
nixos.systems."${name}" = {
|
||||
assignments = {
|
||||
modem = {
|
||||
ipv4 = {
|
||||
address = net.cidr.host (254 - index) prefixes.modem.v4;
|
||||
gateway = null;
|
||||
};
|
||||
};
|
||||
core = {
|
||||
name = "${name}-core";
|
||||
inherit domain;
|
||||
@@ -100,9 +94,11 @@ in
|
||||
|
||||
configuration = { lib, pkgs, config, assignments, allAssignments, ... }:
|
||||
let
|
||||
inherit (lib) mkIf mkMerge mkForce;
|
||||
inherit (lib.my) networkdAssignment;
|
||||
inherit (lib) mkIf mkMerge mkForce optionalString concatStringsSep;
|
||||
inherit (lib.my) mkOpt' networkdAssignment;
|
||||
inherit (lib.my.c) networkd;
|
||||
|
||||
cfg = config.my.homeRouter;
|
||||
in
|
||||
{
|
||||
imports = map (m: import m index) [
|
||||
@@ -112,6 +108,20 @@ in
|
||||
./kea.nix
|
||||
];
|
||||
|
||||
# Per-box WAN-management specifics: the Virgin Media modem on stream lives on the `wan`
|
||||
# interface itself, whereas river's ONT sits on its own interface. Declared as options the
|
||||
# box sets so routing-common itself carries no modem/ONT knowledge.
|
||||
options.my.homeRouter = with lib.types; {
|
||||
dns.wanSkipBroadcasts = mkOpt' (listOf str) [ ] ''
|
||||
Broadcast addresses to exclude when auto-selecting the router's own `wan` A record,
|
||||
for extra static subnets that share the `wan` interface.
|
||||
'';
|
||||
firewall.untrustedRejectV4 = mkOpt' (listOf str) [ ] ''
|
||||
IPv4 prefixes untrusted clients must be explicitly rejected from reaching. Only needed
|
||||
for subnets sharing the `wan` interface, since `wan` egress is otherwise accepted.
|
||||
'';
|
||||
};
|
||||
|
||||
config = {
|
||||
environment = {
|
||||
systemPackages = with pkgs; [
|
||||
@@ -152,26 +162,34 @@ in
|
||||
|
||||
networking = { inherit domain; };
|
||||
|
||||
systemd.services =
|
||||
let
|
||||
waitOnline = "systemd-networkd-wait-online@wan.service";
|
||||
in
|
||||
{
|
||||
# Uniform "WAN is up" gate. Consumers attach to this target (via wantedBy +
|
||||
# partOf) rather than depending on it, so it is never pulled in / prematurely
|
||||
# activated. Each box wires up how the target actually gets reached: stream
|
||||
# gates it on networkd's wait-online, river drives it from the pppd hooks.
|
||||
systemd.targets.wan-online.description = "WAN is online";
|
||||
|
||||
systemd.services = {
|
||||
ipsec = {
|
||||
after = [ waitOnline ];
|
||||
requires = [ waitOnline ];
|
||||
after = [ "wan-online.target" ];
|
||||
# strongswan/libreswan force wantedBy=multi-user.target; drop it so the
|
||||
# target is a true gate rather than mere ordering. This matters most on
|
||||
# river, where the target is hook-driven and not in the boot transaction,
|
||||
# so plain ordering wouldn't hold ipsec back at all. partOf re-loads ipsec
|
||||
# (re-orienting its connections) whenever the WAN drops and returns.
|
||||
wantedBy = mkForce [ "wan-online.target" ];
|
||||
partOf = [ "wan-online.target" ];
|
||||
};
|
||||
|
||||
ipv6-clear-default-route = {
|
||||
description = "Clear IPv6 RA default route";
|
||||
after = [ waitOnline ];
|
||||
requires = [ waitOnline ];
|
||||
after = [ "wan-online.target" ];
|
||||
wantedBy = [ "wan-online.target" ];
|
||||
partOf = [ "wan-online.target" ];
|
||||
script = ''
|
||||
# Seems like we can sometimes pick up a default route somehow...
|
||||
${pkgs.iproute2}/bin/ip -6 route del default via fe80::1 || true
|
||||
'';
|
||||
serviceConfig.Type = "oneshot";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -220,41 +238,6 @@ in
|
||||
in
|
||||
mkMerge [
|
||||
{
|
||||
"50-wan-ifb" = {
|
||||
matchConfig.Name = "wan-ifb";
|
||||
networkConfig = networkd.noL3;
|
||||
extraConfig = ''
|
||||
[CAKE]
|
||||
Bandwidth=490M
|
||||
RTTSec=50ms
|
||||
PriorityQueueingPreset=besteffort
|
||||
# DOCSIS preset
|
||||
OverheadBytes=18
|
||||
MPUBytes=64
|
||||
CompensationMode=none
|
||||
'';
|
||||
};
|
||||
"50-wan" = mkMerge [
|
||||
(networkdAssignment "wan" assignments.modem)
|
||||
{
|
||||
matchConfig.Name = "wan";
|
||||
DHCP = "ipv4";
|
||||
dns = [ "127.0.0.1" "::1" ];
|
||||
dhcpV4Config.UseDNS = false;
|
||||
|
||||
qdiscConfig = {
|
||||
Parent = "ingress";
|
||||
Handle = "0xffff";
|
||||
};
|
||||
extraConfig = ''
|
||||
[CAKE]
|
||||
Parent=root
|
||||
Bandwidth=48M
|
||||
RTTSec=50ms
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
"55-lan" = {
|
||||
matchConfig.Name = "lan";
|
||||
vlan = [ "lan-hi" "lan-lo" "lan-untrusted" "wan-tunnel" ];
|
||||
@@ -365,7 +348,7 @@ in
|
||||
return
|
||||
}
|
||||
chain filter-untrusted {
|
||||
ip daddr ${prefixes.modem.v4} reject
|
||||
${optionalString (cfg.firewall.untrustedRejectV4 != [ ]) "ip daddr { ${concatStringsSep ", " cfg.firewall.untrustedRejectV4} } reject"}
|
||||
oifname wan accept
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ let
|
||||
in
|
||||
{
|
||||
config = {
|
||||
# Let pdns-recursor bind the VRRP VIPs even on the backup, where the addresses
|
||||
# aren't present locally
|
||||
boot.kernel.sysctl = {
|
||||
"net.ipv4.ip_nonlocal_bind" = 1;
|
||||
"net.ipv6.ip_nonlocal_bind" = 1;
|
||||
};
|
||||
|
||||
my = {
|
||||
secrets.files = {
|
||||
"home/pdns/auth.conf" = {
|
||||
@@ -40,6 +47,10 @@ in
|
||||
"127.0.0.1" "::1"
|
||||
assignments.hi.ipv4.address assignments.hi.ipv6.address
|
||||
assignments.lo.ipv4.address assignments.lo.ipv6.address
|
||||
# VRRP VIPs: DNS follows the master, so clients only ever have one
|
||||
# (always-live) resolver address and never hang on a dead router
|
||||
vips.hi.v4 vips.hi.v6
|
||||
vips.lo.v4 vips.lo.v6
|
||||
];
|
||||
allow_from = [
|
||||
"127.0.0.0/8" "::1/128"
|
||||
@@ -53,7 +64,13 @@ in
|
||||
};
|
||||
|
||||
outgoing = {
|
||||
source_address = [ "0.0.0.0" "::" ];
|
||||
# Query authoritative servers over IPv4 only. Our IPv6 default route runs over the
|
||||
# as211024 mesh (`ip -6 route show default`), a proto-static route that isn't
|
||||
# withdrawn when the mesh flaps (e.g. during ipsec churn) — it just blackholes. With
|
||||
# "::" here the recursor keeps picking IPv6 to reach NS, stalls on timeouts, and
|
||||
# takes recursion down with it. IPv4 upstream goes out the WAN directly and stays up;
|
||||
# we still serve AAAA records regardless of transport.
|
||||
source_address = [ "0.0.0.0" ];
|
||||
};
|
||||
|
||||
recursor = {
|
||||
@@ -189,7 +206,7 @@ in
|
||||
${name} IN LUA ${lib.my.dns.ifaceA {
|
||||
inherit pkgs;
|
||||
iface = "wan";
|
||||
skipBroadcasts = [ (lib.my.netBroadcast prefixes.modem.v4) ];
|
||||
skipBroadcasts = config.my.homeRouter.dns.wanSkipBroadcasts;
|
||||
}}
|
||||
${otherName} IN LUA ${lib.my.dns.lookupIP {
|
||||
inherit pkgs;
|
||||
@@ -223,11 +240,16 @@ in
|
||||
dave-lo IN AAAA ${net.cidr.host (65536+2) prefixes.lo.v6}
|
||||
|
||||
shytzel IN A ${net.cidr.host 12 prefixes.core.v4}
|
||||
brian IN A ${net.cidr.host 13 prefixes.core.v4}
|
||||
|
||||
wave IN A ${net.cidr.host 12 prefixes.hi.v4}
|
||||
; wave IN AAAA ${net.cidr.host (65536+3) prefixes.hi.v6}
|
||||
vibe IN A ${net.cidr.host 13 prefixes.hi.v4}
|
||||
vibe IN AAAA ${net.cidr.host (65536+4) prefixes.hi.v6}
|
||||
vibe-core IN A ${net.cidr.host 15 prefixes.core.v4}
|
||||
vibe IN A ${net.cidr.host 15 prefixes.hi.v4}
|
||||
vibe IN AAAA ${net.cidr.host (65536+6) prefixes.hi.v6}
|
||||
vibe-lo IN A ${net.cidr.host 15 prefixes.lo.v4}
|
||||
vibe-lo IN AAAA ${net.cidr.host (65536+6) prefixes.lo.v6}
|
||||
wave-core IN A ${net.cidr.host 14 prefixes.core.v4}
|
||||
wave IN A ${net.cidr.host 14 prefixes.lo.v4}
|
||||
wave IN AAAA ${net.cidr.host (65536+5) prefixes.lo.v6}
|
||||
|
||||
ups IN A ${net.cidr.host 20 prefixes.lo.v4}
|
||||
palace-kvm IN A ${net.cidr.host 21 prefixes.lo.v4}
|
||||
|
||||
@@ -33,7 +33,7 @@ def main():
|
||||
|
||||
print(f'Updating {args.record} -> {address}')
|
||||
cf.dns.records.edit(
|
||||
zone_id=zone.id, dns_record_id=record.id, name=args.record,
|
||||
zone_id=zone.id, dns_record_id=record.id, name=args.record, ttl=60,
|
||||
type='A', content=address)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -83,7 +83,8 @@ in
|
||||
}
|
||||
{
|
||||
name = "domain-name-servers";
|
||||
data = "${net.cidr.host 1 prefixes.hi.v4}, ${net.cidr.host 2 prefixes.hi.v4}";
|
||||
# VRRP VIP so DNS follows the master and clients never hit a dead router
|
||||
data = vips.hi.v4;
|
||||
}
|
||||
{
|
||||
name = "interface-mtu";
|
||||
@@ -116,7 +117,8 @@ in
|
||||
}
|
||||
{
|
||||
name = "domain-name-servers";
|
||||
data = "${net.cidr.host 1 prefixes.lo.v4}, ${net.cidr.host 2 prefixes.lo.v4}";
|
||||
# VRRP VIP so DNS follows the master and clients never hit a dead router
|
||||
data = vips.lo.v4;
|
||||
}
|
||||
];
|
||||
pools = [
|
||||
|
||||
@@ -3,11 +3,14 @@ let
|
||||
# TODO: Move into nixpkgs
|
||||
mstpd = pkgs.mstpd.overrideAttrs {
|
||||
patches = [ ./mstpd.patch ];
|
||||
# Delete postInstall since it nukes the bridge-stp script we need
|
||||
postInstall = "";
|
||||
};
|
||||
in
|
||||
{
|
||||
environment = {
|
||||
systemPackages = [
|
||||
# For kernel to call bridge-stp (see ./pkgs/os-specific/linux/kernel/bridge-stp-helper.patch)
|
||||
mstpd
|
||||
];
|
||||
etc = {
|
||||
@@ -39,8 +42,8 @@ in
|
||||
before = [ "network-pre.target" ];
|
||||
serviceConfig = {
|
||||
Type = "forking";
|
||||
ExecStart = "${mstpd}/sbin/bridge-stp restart";
|
||||
ExecReload = "${mstpd}/sbin/bridge-stp restart_config";
|
||||
ExecStart = "${mstpd}/bin/bridge-stp restart";
|
||||
ExecReload = "${mstpd}/bin/bridge-stp restart_config";
|
||||
PIDFile = "/run/mstpd.pid";
|
||||
Restart = "always";
|
||||
PrivateTmp = true;
|
||||
|
||||
@@ -2,7 +2,14 @@ index: { lib, pkgs, ... }:
|
||||
let
|
||||
inherit (lib) mkForce concatMapStringsSep;
|
||||
inherit (lib.my) net;
|
||||
inherit (lib.my.c.home) domain prefixes;
|
||||
inherit (lib.my.c.home) domain prefixes vips;
|
||||
|
||||
# untrusted uses external (Cloudflare) resolvers, matching the v4 kea config;
|
||||
# trusted VLANs use the internal recursor via its floating VRRP VIP
|
||||
rdnss = name:
|
||||
if name == "untrusted"
|
||||
then "2606:4700:4700::1111 2606:4700:4700::1001"
|
||||
else vips."${name}".v6;
|
||||
|
||||
mkInterface = name: ''
|
||||
interface lan-${name} {
|
||||
@@ -10,7 +17,7 @@ let
|
||||
AdvRASrcAddress { fe80::1; };
|
||||
AdvLinkMTU ${toString prefixes."${name}".mtu};
|
||||
prefix ${prefixes."${name}".v6} {};
|
||||
RDNSS ${net.cidr.host 1 prefixes."${name}".v6} ${net.cidr.host 2 prefixes."${name}".v6} {};
|
||||
RDNSS ${rdnss name} {};
|
||||
DNSSL ${domain} dyn.${domain} ${lib.my.c.colony.domain} ${lib.my.c.britway.domain} {};
|
||||
};
|
||||
'';
|
||||
|
||||
@@ -8,7 +8,14 @@
|
||||
|
||||
configuration = { lib, pkgs, config, ... }:
|
||||
let
|
||||
inherit (lib);
|
||||
inherit (lib) mkMerge;
|
||||
inherit (lib.my) net;
|
||||
inherit (lib.my.c) networkd;
|
||||
inherit (lib.my.c.home) prefixes;
|
||||
|
||||
# Static address on the Virgin Media modem's management subnet. Kept as a plain interface
|
||||
# address (not a network assignment) since it's local to this box's WAN uplink.
|
||||
modemV4 = net.cidr.host 100 prefixes.modem.v4;
|
||||
in
|
||||
{
|
||||
imports = [ ./routing-common/mstpd.nix ];
|
||||
@@ -75,6 +82,32 @@
|
||||
};
|
||||
};
|
||||
|
||||
# wan carries a permanent static modem-management address (modemV4)
|
||||
# alongside the DHCP public IP, so wait-online@wan reports "online" as soon as
|
||||
# the static address is up - before the DHCP lease arrives. ipsec's left= is the
|
||||
# public IP, so gating on wait-online lets it start unoriented and never connect.
|
||||
# Gate instead on the DHCP default route, which only exists once the public lease
|
||||
# is up (the static modem address has no gateway).
|
||||
systemd.services.wan-wait-online = {
|
||||
description = "Wait for the wan default route (public DHCP lease)";
|
||||
after = [ "systemd-networkd.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
TimeoutStartSec = "300";
|
||||
};
|
||||
script = ''
|
||||
until [ -n "$(${pkgs.iproute2}/bin/ip -4 route show default dev wan)" ]; do
|
||||
sleep 1
|
||||
done
|
||||
'';
|
||||
};
|
||||
systemd.targets.wan-online = {
|
||||
requires = [ "wan-wait-online.service" ];
|
||||
after = [ "wan-wait-online.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
|
||||
systemd.network = {
|
||||
netdevs = {
|
||||
"25-lan" = {
|
||||
@@ -147,6 +180,43 @@
|
||||
matchConfig.Name = "lan-dave";
|
||||
networkConfig.Bridge = "lan";
|
||||
};
|
||||
|
||||
"50-wan-ifb" = {
|
||||
matchConfig.Name = "wan-ifb";
|
||||
networkConfig = networkd.noL3;
|
||||
extraConfig = ''
|
||||
[CAKE]
|
||||
Bandwidth=490M
|
||||
RTTSec=50ms
|
||||
PriorityQueueingPreset=besteffort
|
||||
# DOCSIS preset
|
||||
OverheadBytes=18
|
||||
MPUBytes=64
|
||||
CompensationMode=none
|
||||
'';
|
||||
};
|
||||
"50-wan" = {
|
||||
matchConfig.Name = "wan";
|
||||
# Static modem-management address alongside the DHCP public lease. It has no
|
||||
# gateway, so the wan-wait-online gate keys off the DHCP default route instead.
|
||||
address = [ "${modemV4}/24" ];
|
||||
DHCP = "ipv4";
|
||||
dns = [ "127.0.0.1" "::1" ];
|
||||
dhcpV4Config.UseDNS = false;
|
||||
# IPv4-only WAN (public IPv6 arrives over the tunnel, not this link).
|
||||
networkConfig.IPv6AcceptRA = false;
|
||||
|
||||
qdiscConfig = {
|
||||
Parent = "ingress";
|
||||
Handle = "0xffff";
|
||||
};
|
||||
extraConfig = ''
|
||||
[CAKE]
|
||||
Parent=root
|
||||
Bandwidth=48M
|
||||
RTTSec=50ms
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -155,6 +225,12 @@
|
||||
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPYTB4zeAqotrEJ8M+AiGm/s9PFsWlAodz3hYSROGuDb";
|
||||
};
|
||||
server.enable = true;
|
||||
# The modem's management subnet shares the `wan` interface: skip its address when
|
||||
# picking our own wan A record, and reject untrusted clients from reaching it.
|
||||
homeRouter = {
|
||||
dns.wanSkipBroadcasts = [ (lib.my.netBroadcast prefixes.modem.v4) ];
|
||||
firewall.untrustedRejectV4 = [ prefixes.modem.v4 ];
|
||||
};
|
||||
# deploy.node.hostname = "192.168.68.2";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -83,6 +83,7 @@ in
|
||||
inputs.boardie.overlays.default
|
||||
inputs.copyparty.overlays.default
|
||||
inputs.hass-west-wood.overlays.default
|
||||
inputs.pi-agent.overlays.default
|
||||
];
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
|
||||
@@ -8,7 +8,7 @@ let
|
||||
tftpRoot = pkgs.linkFarm "tftp-root" [
|
||||
{
|
||||
name = "ipxe-x86_64.efi";
|
||||
path = "${pkgs.ipxe}/ipxe.efi";
|
||||
path = "${pkgs.ipxe}/snp.efi";
|
||||
}
|
||||
];
|
||||
menuFile = pkgs.runCommand "menu.ipxe" {
|
||||
|
||||
Reference in New Issue
Block a user