commit 4c0982f854c994b2cf663f48204eb4f075ac2b0f Author: Dan Head Date: Sat Mar 21 13:54:16 2026 +0000 chore: initial repo setup with baseline config backup - Pull current config from router (OpenWRT 24.10.2) - Add backup, safe-apply, and push-all scripts - Add CLAUDE.md with workflow rules and context - Add network-map.md with current topology and planned VLANs Co-Authored-By: Claude Sonnet 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c52d037 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Never commit sensitive credentials +.env +*.pem +*.key +*.crt +*.bin diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ac41586 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,61 @@ +# OpenWRT Router Management + +## Hardware + +**Primary router:** +- **Device:** TP-Link Archer AX23 v1 +- **OpenWRT:** 24.10.2 (ramips/mt7621) +- **Router IP:** 10.0.0.1 +- **SSH:** `ssh openwrt` +- **No USB port** — no USB WAN option + +**WAN failover device:** +- **Device:** GL-XE300 (Puli) +- **Firmware:** GL.iNet 4.3.27 (OpenWRT 22.03.4) +- **Current IP:** 192.168.8.1 (to be changed to 10.0.100.1 before wiring in) +- **SSH:** `ssh openwrtwan` + +## Repository Layout +``` +config/ UCI config files pulled from /etc/config/ on the router +scripts/ Backup, push, and safe-apply helpers +docs/ Network map, VLAN plan, change log +``` + +## Workflow Rules +1. **Never edit the router directly for anything non-trivial.** Edit `config/` files here, then push. +2. **All network/firewall/wireless changes go through `safe-apply.sh`** — it sets an auto-revert so a bad config can't permanently lock us out. +3. **Run `backup.sh` before starting any work session** to ensure `config/` reflects the current router state. +4. **Commit after every successful change.** The git log is the change history. + +## Scripts +```bash +scripts/backup.sh # Pull config from router → config/, prompt to commit +scripts/safe-apply.sh # Push one config file with auto-revert safety net +scripts/push-all.sh # Push all configs (low-risk bulk changes only) +``` + +## Safe-Apply Pattern +```bash +# Edit config/network in this repo, then: +./scripts/safe-apply.sh network 5 # 5-minute auto-revert window +# Test connectivity — if working, confirm at the prompt +``` + +## Config Files +| File | Controls | +|------|---------| +| `network` | Interfaces, VLANs, WAN, bridges | +| `wireless` | SSIDs, radios, encryption | +| `firewall` | Zones, rules, forwarding, NAT | +| `dhcp` | DHCP pools, static leases, DNS | +| `system` | Hostname, timezone, logging | +| `dropbear` | SSH daemon | + +## Network Overview +See `docs/network-map.md` for full topology, IP allocations, and device inventory. + +## Planned Features (not yet implemented) +- [ ] VLAN segmentation (trusted / servers / IoT / guest) +- [ ] Multiple SSIDs mapped to VLANs +- [ ] Failover WAN via ethernet-connected 4G device diff --git a/README.md b/README.md new file mode 100644 index 0000000..176e6b4 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# OpenWRT Router Config + +Version-controlled configuration for a home network running OpenWRT, managed from this repo rather than the router directly. + +## Hardware + +| Device | Role | IP | SSH | +|------------------------------------------|-----------------|------------|------------------| +| TP-Link Archer AX23 v1 (OpenWRT 24.10.2) | Primary router | 10.0.0.1 | `ssh openwrt` | +| GL-XE300 Puli (GL.iNet 4.3.27) | 4G WAN failover | 10.0.100.1 | `ssh openwrtwan` | + +WAN: full fibre, 1 Gbps down / 100 Mbps up. Failover via 4G LTE handled by `mwan3`. + +## Repository Layout + +``` +config/ UCI config files pulled from /etc/config/ on the router +scripts/ Backup, push, and safe-apply helpers +docs/ Network map, VLAN plan, change log +files/ Supporting config files (e.g. avahi-daemon.conf) +``` + +## Workflow + +**Never edit the router directly for anything non-trivial.** The pattern is: + +1. Run `scripts/backup.sh` at the start of any work session +2. Edit files in `config/` +3. Push with `scripts/safe-apply.sh` - this sets an auto-revert window so a bad config can't permanently lock you out +4. Confirm the change at the prompt to cancel the revert +5. Commit - the git log is the change history + +```bash +# Pull current config from the router and optionally commit +./scripts/backup.sh + +# Edit a config file, then push it with a 5-minute revert window +./scripts/safe-apply.sh network 5 + +# Push all configs (low-risk bulk changes only) +./scripts/push-all.sh +``` + +## Config Files + +| File | Controls | +|------------|---------------------------------| +| `network` | Interfaces, VLANs, WAN, bridges | +| `wireless` | SSIDs, radios, encryption | +| `firewall` | Zones, rules, forwarding, NAT | +| `dhcp` | DHCP pools, static leases, DNS | +| `system` | Hostname, timezone, logging | +| `dropbear` | SSH daemon | + +## Network + +See [`docs/network-map.md`](docs/network-map.md) for the full topology, IP allocations, port forwards and planned VLAN layout. + +### Planned VLANs (not yet implemented) + +| VLAN | Name | Subnet | SSID | +|------|---------|--------------|-----------------| +| 1 | trusted | 10.0.1.0/24 | Moonshield | +| 10 | servers | 10.0.10.0/24 | wired only | +| 20 | iot | 10.0.20.0/24 | Cloud Connected | +| 30 | media | 10.0.30.0/24 | Pinball Map | +| 40 | guest | 10.0.40.0/24 | Passenger | + +Full device inventory, static DHCP leases and cross-VLAN firewall rules are in [`docs/vlan-requirements.md`](docs/vlan-requirements.md). The implementation plan is in [`docs/implementation-plan.md`](docs/implementation-plan.md). diff --git a/config/dhcp b/config/dhcp new file mode 100644 index 0000000..03a6ef1 --- /dev/null +++ b/config/dhcp @@ -0,0 +1,80 @@ + +config dnsmasq + option domainneeded '1' + option localise_queries '1' + option rebind_protection '1' + option rebind_localhost '1' + option local '/lan/' + option domain 'lan' + option expandhosts '1' + option cachesize '1000' + option authoritative '1' + option readethers '1' + option leasefile '/tmp/dhcp.leases' + option resolvfile '/tmp/resolv.conf.d/resolv.conf.auto' + option localservice '1' + option ednspacket_max '1232' + +config dhcp 'lan' + option interface 'lan' + option start '100' + option limit '150' + option leasetime '12h' + option dhcpv4 'server' + option dhcpv6 'server' + option ra 'server' + list ra_flags 'managed-config' + list ra_flags 'other-config' + +config dhcp 'wan' + option interface 'wan' + option ignore '1' + +config odhcpd 'odhcpd' + option maindhcp '0' + option leasefile '/tmp/hosts/odhcpd' + option leasetrigger '/usr/sbin/odhcpd-update' + option loglevel '4' + +config host + option name 'everlost.lan' + list mac '2C:CF:67:22:B0:52' + option ip '10.0.0.2' + option leasetime 'infinite' + +config host + option name 'homeassistant.lan' + list mac '2C:CF:67:71:81:82' + option ip '10.0.0.11' + option leasetime 'infinite' + +config host + option name 'doorbell.lan' + list mac 'D0:76:02:1B:0E:26' + option ip '10.0.0.41' + option leasetime 'infinite' + +config host + option name 'frigate.lan' + list mac '2C:CF:67:71:91:F0' + option ip '10.0.0.12' + option leasetime 'infinite' + +config host + option name 'jester.lan' + list mac '10:C3:7B:4E:B2:3F' + option ip '10.0.0.21' + option leasetime 'infinite' + +config host + option name 'wayfaerer.lan' + list mac 'B8:27:EB:F1:F4:FC' + option ip '10.0.0.22' + option leasetime 'infinite' + +config dhcp 'guest' + option interface 'guest' + option start '100' + option limit '150' + option leasetime '12h' + diff --git a/config/dropbear b/config/dropbear new file mode 100644 index 0000000..362a366 --- /dev/null +++ b/config/dropbear @@ -0,0 +1,7 @@ + +config dropbear 'main' + option enable '1' + option PasswordAuth '0' + option RootPasswordAuth '0' + option Port '22' + diff --git a/config/firewall b/config/firewall new file mode 100644 index 0000000..94b8926 --- /dev/null +++ b/config/firewall @@ -0,0 +1,259 @@ + +config defaults + option input 'REJECT' + option output 'ACCEPT' + option forward 'REJECT' + option synflood_protect '1' + option flow_offloading '1' + option flow_offloading_hw '1' + +config zone + option name 'lan' + option input 'ACCEPT' + option output 'ACCEPT' + option forward 'ACCEPT' + list network 'lan' + +config zone + option name 'wan' + option input 'REJECT' + option output 'ACCEPT' + option forward 'REJECT' + option masq '1' + option mtu_fix '1' + list network 'wan' + +config forwarding + option src 'lan' + option dest 'wan' + +config rule + option name 'Allow-DHCP-Renew' + option src 'wan' + option proto 'udp' + option dest_port '68' + option target 'ACCEPT' + option family 'ipv4' + +config rule + option name 'Allow-Ping' + option src 'wan' + option proto 'icmp' + option icmp_type 'echo-request' + option family 'ipv4' + option target 'ACCEPT' + +config rule + option name 'Allow-IGMP' + option src 'wan' + option proto 'igmp' + option family 'ipv4' + option target 'ACCEPT' + +config rule + option name 'Allow-DHCPv6' + option src 'wan' + option proto 'udp' + option dest_port '546' + option family 'ipv6' + option target 'ACCEPT' + +config rule + option name 'Allow-MLD' + option src 'wan' + option proto 'icmp' + option src_ip 'fe80::/10' + list icmp_type '130/0' + list icmp_type '131/0' + list icmp_type '132/0' + list icmp_type '143/0' + option family 'ipv6' + option target 'ACCEPT' + +config rule + option name 'Allow-ICMPv6-Input' + option src 'wan' + option proto 'icmp' + list icmp_type 'echo-request' + list icmp_type 'echo-reply' + list icmp_type 'destination-unreachable' + list icmp_type 'packet-too-big' + list icmp_type 'time-exceeded' + list icmp_type 'bad-header' + list icmp_type 'unknown-header-type' + list icmp_type 'router-solicitation' + list icmp_type 'neighbour-solicitation' + list icmp_type 'router-advertisement' + list icmp_type 'neighbour-advertisement' + option limit '1000/sec' + option family 'ipv6' + option target 'ACCEPT' + +config rule + option name 'Allow-ICMPv6-Forward' + option src 'wan' + option dest '*' + option proto 'icmp' + list icmp_type 'echo-request' + list icmp_type 'echo-reply' + list icmp_type 'destination-unreachable' + list icmp_type 'packet-too-big' + list icmp_type 'time-exceeded' + list icmp_type 'bad-header' + list icmp_type 'unknown-header-type' + option limit '1000/sec' + option family 'ipv6' + option target 'ACCEPT' + +config rule + option name 'Allow-IPSec-ESP' + option src 'wan' + option dest 'lan' + option proto 'esp' + option target 'ACCEPT' + +config rule + option name 'Allow-ISAKMP' + option src 'wan' + option dest 'lan' + option dest_port '500' + option proto 'udp' + option target 'ACCEPT' + +config redirect + option dest 'lan' + option target 'DNAT' + option name 'HTTP' + list proto 'tcp' + option src 'wan' + option src_dport '80' + option dest_ip '10.0.0.2' + option dest_port '80' + +config redirect + option dest 'lan' + option target 'DNAT' + option name 'HTTPS' + list proto 'tcp' + option src 'wan' + option src_dport '443' + option dest_ip '10.0.0.2' + option dest_port '443' + +config redirect + option dest 'lan' + option target 'DNAT' + option name 'SSH - Everlost' + list proto 'tcp' + option src 'wan' + option src_dport '22563' + option dest_ip '10.0.0.2' + option dest_port '22563' + +config redirect + option dest 'lan' + option target 'DNAT' + option name 'SSH - Home Assistant' + list proto 'tcp' + option src 'wan' + option src_dport '22553' + option dest_ip '10.0.0.11' + option dest_port '22553' + +config redirect + option dest 'lan' + option target 'DNAT' + option name 'SSH - Frigate' + list proto 'tcp' + option src 'wan' + option src_dport '22583' + option dest_ip '10.0.0.12' + option dest_port '22583' + +config redirect + option dest 'lan' + option target 'DNAT' + option name 'SSH - Jester' + list proto 'tcp' + option src 'wan' + option src_dport '22573' + option dest_ip '10.0.0.21' + option dest_port '22573' + +config redirect + option dest 'lan' + option target 'DNAT' + option name 'SSH - Wayfaerer' + list proto 'tcp' + option src 'wan' + option src_dport '22593' + option dest_ip '10.0.0.22' + option dest_port '22593' + +config redirect + option dest 'lan' + option target 'DNAT' + option name 'Wireguard' + list proto 'udp' + option src 'wan' + option src_dport '51820' + option dest_ip '10.0.0.2' + option dest_port '51820' + +config redirect + option dest 'lan' + option target 'DNAT' + option name 'Plex - Jester' + list proto 'tcp' + option src 'wan' + option src_dport '32400' + option dest_ip '10.0.0.21' + option dest_port '32400' + +config redirect + option dest 'lan' + option target 'DNAT' + option name 'Plex - Wayfaerer' + list proto 'tcp' + option src 'wan' + option src_dport '32450' + option dest_ip '10.0.0.22' + option dest_port '32450' + +config zone + option name 'guest' + option input 'ACCEPT' + option output 'ACCEPT' + option forward 'ACCEPT' + list network 'guest' + option masq '1' + +config forwarding + option src 'guest' + option dest 'wan' + +config rule + option src 'guest' + option name 'Guest DHCP and DNS' + option dest_port '53 67 68' + option target 'ACCEPT' + +config rule + option src 'guest' + option dest 'lan' + option name 'Guest Pihole access' + option src_port '53' + list dest_ip '10.0.0.2' + option dest_port '54' + option target 'ACCEPT' + +config redirect + option dest 'lan' + option target 'DNAT' + option name 'SSH - Gitea' + list proto 'tcp' + option src 'wan' + option src_dport '2222' + option dest_ip '10.0.0.2' + option dest_port '2222' + diff --git a/config/network b/config/network new file mode 100644 index 0000000..840a296 --- /dev/null +++ b/config/network @@ -0,0 +1,59 @@ + +config interface 'loopback' + option device 'lo' + option proto 'static' + option ipaddr '127.0.0.1' + option netmask '255.0.0.0' + +config globals 'globals' + option ula_prefix 'fde4:b048:39cd::/48' + option packet_steering '1' + +config device + option name 'br-lan' + option type 'bridge' + list ports 'lan1' + list ports 'lan2' + list ports 'lan3' + list ports 'lan4' + option ipv6 '1' + +config interface 'lan' + option device 'br-lan' + option proto 'static' + option ipaddr '10.0.0.1' + option netmask '255.255.255.0' + option ip6assign '60' + list dns '10.0.0.2' + +config interface 'wan' + option device 'wan' + option proto 'pppoe' + option username 'suburbanme@plusdsl.net' + option password 'Fo4oD7naqzHpEdnO' + option ipv6 '0' + option force_link '1' + option sourcefilter '0' + option delegate '0' + +config device + option name 'pppoe-wan' + option ipv6 '0' + +config device + option name 'eth0' + +config device + option type 'bridge' + option name 'br-guest' + option bridge_empty '1' + option ipv6 '0' + +config interface 'guest' + option proto 'static' + option device 'br-guest' + option ipaddr '10.10.10.1' + option netmask '255.255.255.0' + list dns '10.0.0.2' + option delegate '0' + diff --git a/config/system b/config/system new file mode 100644 index 0000000..e9b6013 --- /dev/null +++ b/config/system @@ -0,0 +1,31 @@ + +config system + option hostname 'OpenWrt' + option timezone 'UTC' + option ttylogin '0' + option log_size '128' + option urandom_seed '0' + option compat_version '1.1' + +config timeserver 'ntp' + option enabled '1' + option enable_server '0' + list server '0.openwrt.pool.ntp.org' + list server '1.openwrt.pool.ntp.org' + list server '2.openwrt.pool.ntp.org' + list server '3.openwrt.pool.ntp.org' + +config led 'led_lan' + option name 'LAN' + option sysfs 'green:lan' + option trigger 'netdev' + option mode 'link tx rx' + option dev 'br-lan' + +config led 'led_wan' + option name 'WAN' + option sysfs 'green:wan' + option trigger 'netdev' + option mode 'link tx rx' + option dev 'wan' + diff --git a/config/wireless b/config/wireless new file mode 100644 index 0000000..942c574 --- /dev/null +++ b/config/wireless @@ -0,0 +1,57 @@ + +config wifi-device 'radio0' + option type 'mac80211' + option path '1e140000.pcie/pci0000:00/0000:00:01.0/0000:02:00.0' + option band '2g' + option channel 'auto' + option htmode 'HE20' + option country 'GB' + option cell_density '0' + option disabled '0' + +config wifi-device 'radio1' + option type 'mac80211' + option path '1e140000.pcie/pci0000:00/0000:00:01.0/0000:02:00.0+1' + option band '5g' + option channel 'auto' + option htmode 'HE80' + option country 'GB' + option cell_density '0' + option disabled '0' + +config wifi-iface 'wifinet0' + option device 'radio0' + option mode 'ap' + option ssid 'Moonshield' + option encryption 'sae-mixed' + option key 'lun4rstr41n' + option ocv '0' + option wpa_disable_eapol_key_retries '1' + option network 'lan' + +config wifi-iface 'wifinet1' + option device 'radio1' + option mode 'ap' + option ssid 'Moonshield' + option encryption 'sae-mixed' + option key 'lun4rstr41n' + option ocv '0' + option wpa_disable_eapol_key_retries '1' + option network 'lan' + +config wifi-iface 'wifinet2' + option device 'radio0' + option mode 'ap' + option ssid 'Stow on the Wireless' + option encryption 'psk2' + option key 'BanburyCross81' + option network 'guest' + +config wifi-iface 'wifinet3' + option device 'radio1' + option mode 'ap' + option ssid 'Stow on the Wireless' + option encryption 'psk2' + option key 'BanburyCross81' + option network 'guest' + diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md new file mode 100644 index 0000000..b1c64a5 --- /dev/null +++ b/docs/implementation-plan.md @@ -0,0 +1,547 @@ +# VLAN Implementation Plan + +## Guiding Principles + +- **Every risky change goes through `safe-apply.sh`** with a revert window +- **Build alongside, then cut over** — new VLANs and SSIDs are created while the existing flat network stays up; the cutover is a single planned step +- **Servers migrate before clients** — HA and other services need stable IPs before IoT/media devices reconnect to them +- **Have a fallback** — keep a phone on mobile data during the cutover so you can SSH into the router if WiFi drops and doesn't recover + +--- + +## Prerequisites (Complete Before Any Router Changes) + +- [x] Fill in all MAC addresses in `vlan-requirements.md` +- [x] Note Shield TV's current hostname/IP from LuCI +- [x] Document all current port forwards (see `docs/network-map.md` → Port Forwards) +- [x] Note any hardcoded IPs in Home Assistant — Frigate (`10.0.0.12`) and Enphase Envoy (`10.0.0.144`); Frigate also has doorbell camera IP (`10.0.0.41`) hardcoded in its config +- [x] DNS records confirmed — managed in router `config/dhcp`, not PiHole (no local DNS records in PiHole UI or pihole.toml) +- [ ] Add PiHole Local DNS records (Settings → Local DNS → DNS Records) for split-horizon DNS — internal clients resolve service hostnames to everlost's internal IP directly, bypassing hairpin and keeping services reachable during WAN outages: + - `jester.danielhead.com` → `10.0.0.2` + - `wayfaerer.danielhead.com` → `10.0.0.2` + - `wg0.danielhead.com` → `10.0.0.2` + - (add any future service subdomains here too) +- [x] Push updated `config/dhcp` to remove now-redundant dnsmasq domain entries: `./scripts/safe-apply.sh dhcp 5` +- [x] Collect MAC addresses for internet-allowed IoT devices from LuCI → Network → DHCP Leases (Hypervolt, OCTO-CADLITE, HP printer, Alarmo, Envoy) — fill into `vlan-requirements.md` +- [x] Complete the br-guest port assignment test (see `docs/pre-implementation-findings.md` → Pending Validation Test) +- [ ] Push updated `config/network` to remove LAN4 from br-guest +- [ ] Run `./scripts/backup.sh` to snapshot current working config + +--- + +## Phase 0 — Upgrade router to openwrt-25.12.2 + +Upgrade OpenWRT to latest stable version using sysupgrade. The ramips/mt7621 target supports config-preserving upgrades but this must be explicitly requested — without the `-k` flag sysupgrade will factory reset the router. + +**Pre-flight:** + +```bash +# Snapshot current config into the repo first +./scripts/backup.sh + +# Verify the backup looks correct before proceeding +git diff config/ +``` + +**Copy firmware to the router and verify checksum:** + +```bash +# Check available space first +ssh openwrt "df -h /tmp" + +# Copy the firmware binary from this repo to the router +scp openwrt-25.12.2-ramips-mt7621-tplink_archer-ax23-v1-squashfs-sysupgrade.bin openwrt:/tmp/ + +# Verify checksum matches the value on downloads.openwrt.org +ssh openwrt "sha256sum /tmp/openwrt-25.12.2-ramips-mt7621-tplink_archer-ax23-v1-squashfs-sysupgrade.bin" +``` + +**Apply the upgrade:** + +```bash +# -k preserves /etc/config/* — without this it factory resets +ssh openwrt "sysupgrade -k /tmp/openwrt-25.12.2-ramips-mt7621-tplink_archer-ax23-v1-squashfs-sysupgrade.bin" +``` + +The router will reboot. Reconnect after ~2 minutes. + +**Verify:** + +```bash +ssh openwrt "cat /etc/openwrt_release" # confirm new version +ssh openwrt "uci show network.lan.ipaddr" # confirm LAN IP intact +./scripts/backup.sh # confirm config still matches repo +``` + +> **Rollback:** sysupgrade does not support automatic rollback. If the router becomes unreachable after upgrading, connect via ethernet and access it at `192.168.1.1` (default IP after a reset). Restore config using the Clean Restore steps at the bottom of this document. + +## Phase 1 — Install Required Packages + +Low risk. Packages are additive, nothing changes until configured. + +```bash +ssh openwrt "opkg update && opkg install avahi-daemon kmod-bridge" +``` + +- `avahi-daemon` — mDNS reflection across VLANs +- `kmod-bridge` — kernel bridging support for VLAN interfaces (may already be present) + +**Verify:** `ssh openwrt "avahi-daemon --version"` + +--- + +## Phase 2 — Create VLAN Interfaces (network config) + +Edit `config/network` to add VLAN bridge interfaces alongside the existing `br-lan`. + +**New interfaces to add:** +| Interface | Bridge | Subnet | VLAN ID | +|---------------|--------------|--------------|---------| +| `lan_trusted` | `br-trusted` | 10.0.1.1/24 | 1 | +| `lan_servers` | `br-servers` | 10.0.10.1/24 | 10 | +| `lan_iot` | `br-iot` | 10.0.20.1/24 | 20 | +| `lan_media` | `br-media` | 10.0.30.1/24 | 30 | +| `lan_guest` | `br-guest` | 10.0.40.1/24 | 40 | + +The existing flat `br-lan` (10.0.0.1/24) stays untouched until cutover. + +```bash +./scripts/safe-apply.sh network 10 +``` + +**Verify:** `ssh openwrt "ip addr show"` — new bridge interfaces should appear +**Rollback:** If router becomes unreachable, it auto-reverts in 10 minutes + +--- + +## Phase 3 — Configure DHCP Pools + +Edit `config/dhcp` to add a pool for each new VLAN interface. Each pool advertises: +- Gateway: the router's IP on that VLAN (e.g. `10.0.1.1`) +- DNS: PiHole (`10.0.10.2`) +- Static leases for servers, Shield TV, and doorbell camera + +```bash +./scripts/safe-apply.sh dhcp 5 +``` + +**Verify:** Connect a test device to the router via ethernet, manually set IP to e.g. `10.0.1.100/24` gateway `10.0.1.1` — confirm it can ping the gateway. + +--- + +## Phase 4 — Configure Firewall Zones and Rules + +Edit `config/firewall` to add zones for each VLAN and the cross-VLAN rules from `vlan-requirements.md`. The existing `lan` zone stays in place. + +Key rules to implement: +- `trusted → internet` allow +- `trusted → media` allow (Cast ports + Sonos ports) +- `trusted → servers` allow (SSH + Nginx) +- `servers → iot` allow all +- `servers → media` allow all +- `media → servers` allow (Plex TCP 32400, Jellyfin TCP 8096) +- `iot → internet` **block by default** — set IoT zone forward policy to REJECT +- `iot → internet` explicit allows for: Hypervolt (`10.0.20.2`), OCTO-CADLITE (`10.0.20.3`), HP printer (`10.0.20.4`), Alarmo (`10.0.20.5`), Envoy (`10.0.20.6`) +- `guest → internet` allow only +- DNS hijack: redirect all outbound TCP/UDP 53 to PiHole (`10.0.10.2`) + +> **Note:** The per-device IoT allow rules depend on static leases being in place (Phase 3) so those devices have predictable IPs. Verify static leases are active before applying firewall rules. + +```bash +./scripts/safe-apply.sh firewall 10 +``` + +**Verify:** Zones appear in LuCI → Network → Firewall + +--- + +## Phase 5 — Add New SSIDs + +Edit `config/wireless` to add new SSIDs mapped to VLAN bridge interfaces. **Do not change Moonshield yet** — it stays on the flat `br-lan` for now. + +New SSIDs to add: +| SSID | Interface | Band | +|-----------------|------------|---------------| +| Cloud Connected | `br-iot` | 2.4GHz | +| Pinball Map | `br-media` | 5GHz + 2.4GHz | +| Passenger | `br-guest` | 2.4GHz | + +```bash +./scripts/safe-apply.sh wireless 5 +``` + +**Verify:** New SSIDs appear on a phone. Connect a test device to each and confirm it gets an IP in the right subnet (e.g. Passenger → 10.0.40.x). + +--- + +## Phase 6 — Migrate Servers (Maintenance Window Begins) + +> From this point, brief outages are expected. Ensure your phone is on mobile data. + +Update static DHCP leases in `config/dhcp` to assign new IPs (10.0.10.x) to server devices. Move them from the flat `br-lan` DHCP to the `lan_servers` DHCP. + +**For each server (everlost, homeassistant, frigate, jester, wayfaerer):** +1. Push updated DHCP config +2. SSH into the server and run `sudo dhclient -r && sudo dhclient` (or reboot) to renew its lease +3. Confirm it gets its new `10.0.10.x` IP + +**After all servers have new IPs:** + +Update `config/firewall` port forwards to reflect new server IPs: + +| Name | Proto | WAN Port | Old Dest IP | New Dest IP | +|----------------------|-------|----------|-------------|-------------| +| HTTP | TCP | 80 | 10.0.0.2 | 10.0.10.2 | +| HTTPS | TCP | 443 | 10.0.0.2 | 10.0.10.2 | +| Wireguard | UDP | 51820 | 10.0.0.2 | 10.0.10.2 | +| SSH - Everlost | TCP | 22563 | 10.0.0.2 | 10.0.10.2 | +| SSH - Home Assistant | TCP | 22553 | 10.0.0.11 | 10.0.10.3 | +| SSH - Frigate | TCP | 22583 | 10.0.0.12 | 10.0.10.4 | +| SSH - Jester | TCP | 22573 | 10.0.0.21 | 10.0.10.10 | +| SSH - Wayfaerer | TCP | 22593 | 10.0.0.22 | 10.0.10.11 | +| Plex - Jester | TCP | 32400 | 10.0.0.21 | 10.0.10.10 | +| Plex - Wayfaerer | TCP | 32450 | 10.0.0.22 | 10.0.10.11 | + +```bash +./scripts/safe-apply.sh firewall 5 +``` + +- Update hardcoded IPs in Home Assistant integrations: + - **Frigate** (Settings → Integrations → Frigate): change host from `10.0.0.12` → `10.0.10.4` +- Confirm PiHole dashboard is reachable at `10.0.10.2` + +**Update PiHole Local DNS records** (Settings → Local DNS → DNS Records) to point to everlost's new IP: + +| Name | Old IP | New IP | +|------|--------|--------| +| jester.danielhead.com | 10.0.0.2 | 10.0.10.2 | +| wayfaerer.danielhead.com | 10.0.0.2 | 10.0.10.2 | +| wg0.danielhead.com | 10.0.0.2 | 10.0.10.2 | + +**Update WireGuard config on everlost:** +1. Update wg-easy client DNS setting from `10.0.0.2` → `10.0.10.2` and regenerate client configs +2. Verify from a WG-connected device: `nslookup homeassistant.danielhead.com` should return `10.0.10.2` +3. Verify WireGuard-connected devices can still reach proxied services + +**Verify:** Home Assistant loads, all integrations show as connected, Nginx proxy still routes external traffic correctly, WireGuard clients can reach internal services. + +**Add temporary `lan → servers` firewall rule:** + +IoT and media devices are still on Moonshield (`br-lan`, 10.0.0.x) and need to keep reaching HA, Frigate etc. while you migrate them at your own pace. Add a temporary allow-all forwarding rule from the `lan` zone to the `servers` zone: + +```bash +uci add firewall rule +uci set firewall.@rule[-1].name='temp_lan_to_servers' +uci set firewall.@rule[-1].src='lan' +uci set firewall.@rule[-1].dest='servers' +uci set firewall.@rule[-1].target='ACCEPT' +uci commit firewall +./scripts/safe-apply.sh firewall 5 +``` + +> **Remember to remove this rule after Phase 7** — once all IoT and media devices have migrated off Moonshield, this rule is no longer needed and leaves an unintended hole. + +--- + +## Phase 7 — Migrate IoT Devices + +1. Connect each IoT device to **Cloud Connected** SSID + - ESPHome devices: forget current WiFi in ESPHome config and re-provision, or just update SSID in the ESPHome dashboard + - Other devices: reconnect via their app or settings +2. Devices will get IPs in `10.0.20.x` +3. HA should rediscover ESPHome devices automatically via mDNS within a few minutes +4. Confirm each device shows as available in HA + +**After IoT devices have new IPs:** + +- Update hardcoded IPs in Home Assistant integrations: + - **Enphase Envoy** (Settings → Integrations → Enphase Envoy): change host from `10.0.0.144` → `10.0.20.2` +- Update doorbell camera IP in Frigate's config: change from `10.0.0.41` → `10.0.20.1`, then restart Frigate + +**Remove the temporary `lan → servers` rule** (added at end of Phase 6) once all IoT and media devices are off Moonshield: + +```bash +# Find and delete the rule by name +uci delete firewall.$(uci show firewall | grep 'temp_lan_to_servers' | cut -d. -f2) +uci commit firewall +./scripts/safe-apply.sh firewall 5 +``` + +**Verify:** All ESPHome entities, voice assistants, blinds, and sensors show as available in Home Assistant. Test a blind, a sensor reading, and a voice command. Confirm Frigate shows the doorbell camera stream. + +--- + +## Phase 8 — Migrate Media Devices + +1. Connect Shield TV to **Pinball Map** SSID + - It will get `10.0.30.2` (static lease) + - Open Plex and Jellyfin — update server address to `10.0.10.21` (jester.lan) if not auto-discovered +2. Connect consoles and speakers to **Pinball Map** SSID +3. Test casting from a phone (still on flat network at this point) to speakers and Shield + +**Verify:** Plex/Jellyfin plays content, Cast works from phone, Music Assistant in HA can control speakers, HA Shield integration shows as connected. + +--- + +## Phase 9 — Cutover: Move Moonshield to Trusted VLAN + +This is the final disruptive step. Moonshield will briefly drop all connected devices while it moves to `br-trusted`. + +**Before starting:** Plug your laptop into **LAN 3** (reserved for trusted VLAN). This gives you a wired fallback — if Moonshield doesn't come back up cleanly, you keep your connection to the router and can intervene. + +Edit `config/wireless` — change Moonshield's interface from `br-lan` to `br-trusted`. + +```bash +./scripts/safe-apply.sh wireless 5 +``` + +All phones and laptops on Moonshield will disconnect and immediately reconnect to the same SSID — they'll get new IPs in `10.0.1.x`. This typically takes 5–15 seconds. + +**Verify:** Phone reconnects to Moonshield, gets `10.0.1.x` IP, internet works, can cast to speakers/Shield, can reach Nginx-proxied services. + +--- + +## Phase 10 — DNS Hijacking + +Confirm DNS hijacking rule is active: + +```bash +ssh openwrt "nft list ruleset | grep -A2 'dns'" +``` + +Test it's working by temporarily setting a device's DNS to `8.8.8.8` — it should still resolve via PiHole (check PiHole query logs). + +--- + +## Phase 11 — avahi-daemon (mDNS Reflection) + +Reflects mDNS across trusted, servers, media and IoT VLANs so that: +- Phones (trusted) can discover Cast devices and speakers (media) +- HA (servers) can discover IoT and media devices +- Phones (trusted) can discover the HP printer (IoT) via AirPrint + +The config is stored at `files/avahi-daemon.conf` in this repo. It is **not** a UCI file — it must be pushed manually and is not covered by `safe-apply.sh`. + +```bash +# Install package (if not already done in Phase 1) +ssh openwrt "opkg update && opkg install avahi-daemon" + +# Push config +scp files/avahi-daemon.conf openwrt:/etc/avahi/avahi-daemon.conf + +# Enable and restart +ssh openwrt "/etc/init.d/avahi-daemon enable && /etc/init.d/avahi-daemon restart" +``` + +> **Note:** There is no auto-revert safety net for this file. If avahi causes problems, disable it with `ssh openwrt "/etc/init.d/avahi-daemon stop"` — it is not load-bearing for routing or connectivity. + +**Verify:** Cast devices (speakers, Shield) appear in Google Home app and in Music Assistant from a phone on Moonshield (trusted). Confirm the HP printer is discoverable via AirPrint from a phone. + +--- + +## Phase 12 — Clean Up Flat Network + +Once everything is verified on the new VLANs, remove the old flat `br-lan` interface and its DHCP pool from the config. + +```bash +./scripts/safe-apply.sh network 10 +./scripts/safe-apply.sh dhcp 5 +``` + +Run `./scripts/backup.sh` to commit the final clean state. + +--- + +## Phase 13 — WAN Failover (Separate Session) + +Once VLANs are stable and bedded in, tackle failover as a standalone change: + +**Device:** GL-XE300 (Puli) 4G router, currently at `192.168.8.1` running GL.iNet 4.3.27 (OpenWRT 22.03.4). + +**Pre-flight: reconfigure XE300 subnet** + +Before wiring it in, change the XE300's LAN subnet from `192.168.8.0/24` to a `10.0.x.x` range consistent with the VLAN layout. A sensible choice is `10.0.100.0/24` (XE300 at `10.0.100.1`). Do this via the GL.iNet web UI (Network → LAN IP) before connecting it to the main router. + +**Steps:** + +1. Install `mwan3` package +2. Repurpose a LAN port as WAN2 (network config change) +3. Connect XE300 LAN port to that repurposed port +4. Configure `mwan3` health checks and failover policy +5. Test by temporarily unplugging the primary WAN + +**XE300 management access** + +By default, LAN devices cannot reach the XE300 web UI or SSH because WAN interfaces are in the untrusted firewall zone. To retain management access from the trusted VLAN, add to the main router config: + +- A static route for `10.0.100.0/24` via the WAN2 interface (OpenWRT may add this automatically when the interface comes up) +- A firewall rule: `trusted → 10.0.100.1` allow TCP 22, 80, 443 + +Without this, the only way to reach the XE300 is via SSH on the main router itself (which is directly on the `10.0.50.x` subnet via WAN2). + +--- + +### DDNS — WireGuard Endpoint on Failover + +When WAN2 takes over, the public IP changes. The only service that needs to remain reachable externally during a failover is WireGuard — once connected to the VPN, split-horizon DNS handles everything else internally. + +**Pre-flight: dedicated WireGuard hostname** + +Create a Cloudflare A record for a dedicated WireGuard endpoint hostname (e.g. `wg0.danielhead.com`) pointing to the current fibre WAN IP. Set TTL to 60 seconds. Update all WireGuard client configs to use this hostname as their endpoint if they don't already. + +**Pre-flight: Cloudflare API token** + +In Cloudflare dashboard → My Profile → API Tokens, create a token with: +- Permission: `Zone → DNS → Edit` +- Zone: `danielhead.com` only + +**Steps:** + +1. Install packages: + ```bash + ssh openwrt "opkg update && opkg install ddns-scripts ddns-scripts-cloudflare" + ``` + +2. Add to `config/ddns` (create file if it doesn't exist): + ``` + config ddns 'wg_endpoint' + option service_name 'cloudflare.com-v4' + option enabled '1' + option lookup_host 'wg0.danielhead.com' + option domain 'wg0.danielhead.com' + option zone 'danielhead.com' + option username 'Bearer' + option password '' + option ip_source 'web' + option ip_url 'https://checkip.amazonaws.com https://icanhazip.com https://ifconfig.me' + option check_interval '5' + option unit_check 'minutes' + option force_interval '72' + option unit_force 'hours' + ``` + + `ip_source web` queries an external service to get the current public IP regardless of which WAN interface is active — the correct approach for mwan3 setups where the active interface changes dynamically. + + > **Credentials:** `CLOUDFLARE_API_TOKEN` is in `.env` (gitignored). When applying, substitute the value manually — do not commit the token into `config/ddns`. + +3. Enable and start the ddns service: + ```bash + ssh openwrt "/etc/init.d/ddns enable && /etc/init.d/ddns start" + ``` + +4. Push config: + ```bash + ./scripts/safe-apply.sh ddns 5 + ``` + +**Behaviour:** + +- ddns polls every 5 minutes via `ifconfig.me` +- While WAN1 is up, the public IP matches the Cloudflare record — no update +- When WAN2 takes over, within 5 minutes ddns detects the new IP and updates `wg0.danielhead.com` in Cloudflare +- WireGuard clients re-resolve the hostname (within ~60s due to TTL) and reconnect +- When WAN1 recovers and mwan3 fails back, the record is updated back to the fibre IP within 5 minutes + +**Verify:** + +Simulate a failover by unplugging the primary WAN. After 5 minutes check that `wg0.danielhead.com` has updated to the 4G IP: +```bash +nslookup wg0.danielhead.com 9.9.9.9 +``` +Confirm a WireGuard client can reconnect after the DNS TTL expires. + +--- + +## Future: Managed Switch Migration + +When a managed switch is added, the migration is a `config/network`-only change. Firewall zones, DHCP pools and wireless config are all unaffected - the VLAN identities and IP ranges stay identical. + +**Current approach - one physical port per VLAN:** + +``` +config device + option name 'br-servers' + option type 'bridge' + list ports 'lan2' + +config device + option name 'br-iot' + option type 'bridge' + list ports 'lan3' + +config interface 'lan_servers' + option device 'br-servers' + ... + +config interface 'lan_iot' + option device 'br-iot' + ... +``` + +**With managed switch - single trunk port, 802.1Q VLAN filtering:** + +``` +config device + option name 'br-trunk' + option type 'bridge' + list ports 'lan2' # single cable to managed switch + option vlan_filtering '1' + +config bridge-vlan + option device 'br-trunk' + option vlan '10' # servers VLAN ID + list ports 'lan2:t' # tagged on trunk + +config bridge-vlan + option device 'br-trunk' + option vlan '20' # IoT VLAN ID + list ports 'lan2:t' + +config interface 'lan_servers' + option device 'br-trunk.10' # was: 'br-servers' + ... + +config interface 'lan_iot' + option device 'br-trunk.20' # was: 'br-iot' + ... +``` + +On the managed switch side, set the uplink port as a tagged trunk for VLANs 10, 20, 30 etc., and set each downstream port as an untagged access port for whichever VLAN it belongs to. + +--- + +## Rollback Reference + +| Situation | Action | +|---------------------------------------------|--------------------------------------------------------------------------------------------------------| +| Router unreachable after a change | Wait for auto-revert (5–10 min window set in safe-apply.sh) | +| Rolled back but want to retry | Fix the config file, run safe-apply.sh again | +| Something subtle is broken after confirming | `git diff config/` to see what changed, `./scripts/safe-apply.sh ` to re-push a previous version | +| Complete disaster | SSH in and run `firstboot` (factory reset) — then restore from git using the sequence below | + +--- + +## Clean Restore from Git + +Use this after a factory reset (`firstboot`) or a clean firmware flash. After either, the router is at its default IP `192.168.1.1` - `ssh openwrt` won't work until the network config is pushed first. + +**Requirements:** laptop connected via ethernet to a LAN port on the router. + +```bash +# 1. Push network config to restore the correct LAN IP (10.0.0.1) +ssh root@192.168.1.1 "cat > /etc/config/network" < config/network +ssh root@192.168.1.1 "uci commit network && reload_config" + +# 2. Wait a few seconds for the interface to come back, then push everything else +./scripts/push-all.sh + +# 3. Reinstall packages (adjust list to what was installed at time of restore) +ssh openwrt "opkg update && opkg install avahi-daemon kmod-bridge" +``` + +**What the repo covers:** all six UCI config files (`dhcp`, `dropbear`, `firewall`, `network`, `system`, `wireless`). + +**What it does not cover:** +- Packages - must be reinstalled manually (see step 3) +- `/etc/avahi/avahi-daemon.conf` - not a UCI file, push manually with `scp files/avahi-daemon.conf openwrt:/etc/avahi/avahi-daemon.conf` (config stored in `files/` in this repo) +- SSH host keys - regenerated on clean flash; first reconnect will show a `known_hosts` warning, clear with `ssh-keygen -R openwrt` diff --git a/docs/network-map.md b/docs/network-map.md new file mode 100644 index 0000000..62bf297 --- /dev/null +++ b/docs/network-map.md @@ -0,0 +1,70 @@ +# Network Map + +## Router +| Item | Value | +|------------|-------------------------------------| +| Device | TP-Link Archer AX23 v1 | +| OpenWRT | 24.10.2 | +| LAN IP | 10.0.0.1 | +| LAN Subnet | 10.0.0.0/24 (pre-VLAN) | +| WAN | Full fibre, 1gbps down / 100mbps up | +| SSH | `ssh openwrt` | + +## Current SSIDs +| SSID | Band | Status | +|----------------------|---------------|---------------------------------------------------| +| Moonshield | 2.4GHz + 5GHz | Main network | +| Stow on the Wireless | 2.4GHz | Unused — will become IoT SSID ("Cloud Connected") | + +## Planned VLAN Layout +| VLAN ID | Name | Subnet | Purpose | +|---------|---------|--------------|--------------------------------| +| 1 | trusted | 10.0.1.0/24 | Phones, laptops | +| 10 | servers | 10.0.10.0/24 | NAS, Pis, HA, Frigate, PiHole | +| 20 | iot | 10.0.20.0/24 | Smart devices, cameras | +| 30 | media | 10.0.30.0/24 | Shield TV, consoles, smart TVs | +| 40 | guest | 10.0.40.0/24 | Guest WiFi | + +## Planned SSID → VLAN Mapping +| SSID | VLAN | Notes | +|-----------------|---------|-------------------------------------| +| Moonshield | trusted | Existing main SSID | +| Cloud Connected | iot | Renamed from "Stow on the Wireless" | +| Pinball Map | media | New SSID for Shield + consoles | +| Passenger | guest | New — optional | + +## External Access + +Ports forwarded to `everlost.lan` (10.0.0.2), which runs Nginx + Letsencrypt + auth before proxying to internal services. + +### Port Forwards +| Name | Proto | WAN Port | Dest IP | Dest Port | +|----------------------|-------|----------|-----------|-----------| +| HTTP | TCP | 80 | 10.0.0.2 | 80 | +| HTTPS | TCP | 443 | 10.0.0.2 | 443 | +| SSH - Everlost | TCP | 22563 | 10.0.0.2 | 22563 | +| SSH - Home Assistant | TCP | 22553 | 10.0.0.11 | 22553 | +| SSH - Frigate | TCP | 22583 | 10.0.0.12 | 22583 | +| SSH - Jester | TCP | 22573 | 10.0.0.21 | 22573 | +| SSH - Wayfaerer | TCP | 22593 | 10.0.0.22 | 22593 | +| SSH - Gitea | TCP | 2222 | 10.0.0.2 | 2222 | +| Wireguard | UDP | 51820 | 10.0.0.2 | 51820 | +| Plex - Jester | TCP | 32400 | 10.0.0.21 | 32400 | +| Plex - Wayfaerer | TCP | 32450 | 10.0.0.22 | 32450 | + +## Planned WAN2 (Failover) +| Item | Value | +|----------|---------------------------------------------------------------| +| Device | GL-XE300 (Puli) | +| Firmware | GL.iNet 4.3.27 (based on OpenWRT 22.03.4) | +| LAN IP | 10.0.100.1 (change from default 192.168.8.1 before wiring in) | +| Subnet | 10.0.100.0/24 | +| WAN | 4G LTE via M.2 modem | +| SSH | `ssh openwrtwan` | + +`mwan3` on the main router handles automatic failover. A firewall rule on the main router allows management access from the trusted VLAN to `10.0.100.1` on ports 22/80/443. + +--- + +> For full device inventory, static DHCP leases, and cross-VLAN firewall requirements see: +> [`vlan-requirements.md`](vlan-requirements.md) diff --git a/docs/pre-implementation-findings.md b/docs/pre-implementation-findings.md new file mode 100644 index 0000000..5dfe421 --- /dev/null +++ b/docs/pre-implementation-findings.md @@ -0,0 +1,60 @@ +# Pre-Implementation Findings + +Notes from live router investigation before VLAN implementation begins. + +--- + +## DSA and Bridge Architecture + +**Concern raised:** OpenWRT 24.10+ uses DSA (Distributed Switch Architecture) on the MT7621. The implementation plan needed to be validated against the actual router interface naming and bridge support before work begins. + +**Finding: bridge-per-VLAN approach is confirmed valid.** + +`ip link show` output from the live router: + +``` +1: lo +2: eth0 — uplink to MT7621 switch ASIC +3: wan — WAN port (PPPoE) +4: lan1@eth0 — UP — LAN port 1 (wired, device connected) +5: lan2@eth0 — NO-CARRIER — LAN port 2 (nothing plugged in) +6: lan3@eth0 — UP — LAN port 3 (Sonos Connect) +7: lan4@eth0 — NO-CARRIER — LAN port 4 (nothing plugged in) +8: br-guest — guest bridge (already live) +9: br-lan — main flat LAN bridge +10: pppoe-wan +11: phy0-ap0 → br-lan — Moonshield (2.4GHz) +12: phy1-ap0 → br-lan — Moonshield (5GHz) +13: phy0-ap1 → br-guest — guest SSID (2.4GHz) — already attached +14: phy1-ap1 → br-guest — guest SSID (5GHz) — already attached +``` + +**Key conclusions:** + +- DSA port names are `lan1`–`lan4` — use these in `list ports` when defining bridge devices in `config/network`. +- `br-guest` is already running in production with two wireless VAPs attached — the bridge-per-VLAN pattern is proven on this hardware. +- Both radios support `AP` and `AP/VLAN` modes with up to ~4 VAPs per radio, so adding Cloud Connected and Pinball Map SSIDs will not hit hardware limits. +- The `config/network` entry for `br-guest` currently has no `list ports` entry (bridge_empty '1'), confirming that wireless-only bridges work fine without a wired port. + +--- + +## Pending Validation Test + +**Test: assign a physical port to `br-guest`, connect a device, verify it gets a `10.10.10.x` IP.** + +This validates the full stack — port assignment, bridge isolation and DHCP — using a live but low-risk interface before touching the main VLAN work. + +**Steps (when physically at the router):** + +1. Edit `config/network` — add `list ports 'lan4'` to the `br-guest` device block +2. `./scripts/safe-apply.sh network 5` +3. Plug a device into LAN 4 +4. Confirm it gets an IP in `10.10.10.100`–`10.10.10.249` (pool: start 100, limit 150) +5. Verify on router: `ssh openwrt "cat /tmp/dhcp.leases"` +6. **Clean up:** remove `list ports 'lan4'` and re-push before starting real implementation + +**Status: complete — passed 2026-04-02.** + +Result: device on LAN 4 was issued `10.10.10.101` (within pool `10.10.10.100`–`10.10.10.249`). Port assignment, bridge isolation and DHCP all confirmed working end-to-end. + +**Next step:** remove `list ports 'lan4'` from the `br-guest` device block and re-push before starting VLAN implementation. diff --git a/docs/vlan-requirements.md b/docs/vlan-requirements.md new file mode 100644 index 0000000..d62f42f --- /dev/null +++ b/docs/vlan-requirements.md @@ -0,0 +1,165 @@ +# VLAN Requirements + +This document captures everything needed to implement VLANs. + +--- + +## 1. Device Inventory + +Static DHCP leases (MAC address required): servers VLAN devices, cameras, and Shield TV. +All other devices use dynamic DHCP — no MAC address needed. + +### Servers VLAN (10.0.10.0/24) +Managed Linux devices. Full internet. Can reach IoT and media VLANs. + +| Hostname | Current IP | MAC Address | New IP | Notes | +|-------------------|------------|-------------------|------------|--------------------------------------------| +| everlost.lan | 10.0.0.2 | 2C:CF:67:22:B0:52 | 10.0.10.2 | PiHole + Nginx reverse proxy + Letsencrypt | +| homeassistant.lan | 10.0.0.11 | 2C:CF:67:71:81:82 | 10.0.10.3 | Home Assistant + Music Assistant | +| frigate.lan | 10.0.0.12 | 2C:CF:67:71:91:F0 | 10.0.10.4 | Frigate NVR | +| jester.lan | 10.0.0.21 | 10:C3:7B:4E:B2:3F | 10.0.10.10 | NAS | +| wayfaerer.lan | 10.0.0.22 | B8:27:EB:F1:F4:FC | 10.0.10.11 | TVHeadend | + +### Media VLAN (10.0.30.0/24) +Media, gaming, and speaker devices. Full internet. No access to IoT or trusted. + +**Static DHCP lease (HA needs to find Shield by IP):** + +| Hostname | Current IP | MAC Address | New IP | Notes | +|------------|------------|-------------------|-----------|---------------------------------------------------| +| shield.lan | 10.0.0.119 | 00:04:4B:E4:5A:1B | 10.0.30.2 | Nvidia Shield TV — Plex, Jellyfin, HA integration | + +**Dynamic DHCP — no static lease needed:** + +| Device | Current IP | Connection | Notes | +|----------------------|------------|---------------|---------------------------------------------------------------------------| +| Google-Nest-Mini | 10.0.0.210 | WiFi | Google speaker (Cast) | +| Google-Home-Mini.lan | 10.0.0.228 | WiFi | Google speaker (Cast) | +| Sonos Connect | 10.0.0.157 | Wired (LAN 3) | Wired Sonos bridge — wireless Sonos speakers mesh through it via SonosNet | +| Nintendo Switch | - | WiFi | Games console | +| Nintendo Switch 2 | - | WiFi | Games console | +| PS4 | - | WiFi | Games console | + +### IoT VLAN (10.0.20.0/24) +Sensors, cameras, and embedded devices only. + +**Firewall strategy: block all IoT → WAN by default. Explicit per-device allows for cloud-dependent devices only.** + +#### Internet-blocked devices (dynamic DHCP, no static lease needed) + +These devices only need to reach HA/Frigate on the servers VLAN (covered by cross-VLAN rule 5). No internet access. + +| Hostname | Current IP | Notes | +|-------------------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| doorbell.lan | 10.0.0.41 | See static lease below — RTSP to Frigate only | +| esphome-web-647abc.lan | 10.0.0.238 | Bed occupancy sensor | +| esphome-web-642198.lan | 10.0.0.191 | Flexispot desk controller | +| everything-presence-lite-2c68f4.lan | 10.0.0.213 | Kitchen multisensor | +| everything-presence-lite-9232dc.lan | 10.0.0.237 | Office multisensor | +| everything-presence-lite-934e54.lan | 10.0.0.229 | Lounge multisensor | +| home-assistant-voice-0aac99.lan | 10.0.0.102 | Office voice assistant — managed via HA, no direct internet needed | +| home-assistant-voice-09e888.lan | 10.0.0.220 | Kitchen voice assistant — managed via HA, no direct internet needed | +| Brel_1968.lan | 10.0.0.113 | Blinds controller. Watchdog-resets on cloud loss but settles within ~2 mins and HA (motionblinds integration) works throughout. Blocking prevents unwanted firmware updates breaking the integration. | + +#### Internet-allowed devices (static DHCP lease required) + +These devices require cloud connectivity. Static leases give them predictable IPs so firewall allow rules can be written against them. + +| Hostname | Current IP | MAC Address | New IP | Notes | +|------------------|------------|-------------------|-----------|---------------------------------------------| +| envoy | 10.0.0.144 | 44:EE:14:F9:3A:C3 | 10.0.20.2 | Enphase Envoy solar controller — Enlighten | +| Hypervolt.lan | 10.0.0.201 | 6C:0F:61:A9:30:90 | 10.0.20.3 | Car charger — cloud app + load balancing | +| OCTO-CADLITE.lan | 10.0.0.217 | 94:54:C5:53:EB:24 | 10.0.20.4 | Octopus Energy hub — reports usage data | +| HP83F0BD.lan | 10.0.0.164 | 38:CA:84:83:F0:BD | 10.0.20.5 | HP printer — cloud print + firmware updates | +| CLO_bc744b7ff139 | 10.0.0.118 | BC:74:4B:7F:F1:39 | 10.0.20.6 | Nintendo Alarmo — firmware updates | + +#### Static DHCP lease (internet-blocked) + +| Hostname | Current IP | MAC Address | New IP | Notes | +|--------------|------------|-------------------|-----------|--------------------------------------------------------| +| doorbell.lan | 10.0.0.41 | D0:76:02:1B:0E:26 | 10.0.20.1 | Doorbell IP camera — no internet, RTSP to Frigate only | + +### Trusted VLAN (10.0.1.0/24) +Phones, laptops, personal devices. Full internet. Can cast to media VLAN. +Dynamic DHCP only — no static leases needed. Any device connecting to Moonshield gets a `10.0.1.x` IP automatically. + +### Guest VLAN (10.0.40.0/24) +Internet only. No access to any other VLAN. DHCP pool, no static leases. + +--- + +## 2. Cross-VLAN Access Rules + +| # | Source | Destination | Protocol / Port | Reason | +|------|----------------|--------------------|---------------------------------|---------------------------------------------------------------| +| 1 | trusted | media | TCP 8008, 8009 | Phone → Google speakers + Shield (Cast) | +| 2 | trusted | media | TCP 1400, 3400, 3401 / UDP 1900 | Phone → Sonos (control + SSDP) | +| 3 | trusted | servers | TCP 22 | SSH into servers from laptop/NAS | +| 4 | trusted | servers (everlost) | TCP 80, 443 | Internal services via Nginx | +| 5a | trusted | servers (jester) | TCP 8096 | Jellyfin direct access (local only, not proxied) | +| 5 | servers | iot | allow all | HA, Frigate, Music Assistant → IoT devices | +| 6 | servers (HA) | media | allow all | HA → Shield (Android TV) + Music Assistant → speakers | +| 7 | media (Shield) | servers (NAS) | TCP 32400, 8096 | Plex and Jellyfin → NAS | +| 8 | any | servers (everlost) | TCP/UDP 53 | PiHole DNS for all VLANs | +| 9 | media (Shield) | servers (wayfaerer) | TCP 9981, 9982 | TVHeadend web UI and HTSP streaming | +| 10 | trusted | iot (printer) | TCP 9100, 631 | Raw printing and IPP from laptops/phones | +| 11 | trusted | iot (printer) | TCP 80, 443 | Printer web UI / config | + +> **Guest VLAN:** internet only — no casting to speakers or Shield from guest devices. +> +> **mDNS:** `avahi-daemon` reflects mDNS across trusted, servers, media and iot. +> Speakers and Shield are discoverable from phones (trusted) and HA (servers) via mDNS reflection. +> The printer (IoT) is discoverable from phones and laptops (trusted) via mDNS reflection (AirPrint). +> No firewall rules needed for discovery — only for the data connections above. + +--- + +## 3. Internet Access per VLAN + +| VLAN | Internet | Notes | +|---------|----------|-------------------------------------------------| +| trusted | Yes | Unrestricted | +| servers | Yes | Unrestricted | +| media | Yes | Unrestricted — consoles need online gaming | +| iot | Partial | Blocked by default. Explicit allows for Hypervolt, OCTO-CADLITE, HP printer, Alarmo, Envoy | +| guest | Yes | Internet only — no access to any internal VLAN | + +--- + +## 4. DNS + +- PiHole on `everlost.lan` (10.0.10.2) handles DNS for all VLANs +- DHCP on each VLAN advertises PiHole (`10.0.10.2`) as the DNS server +- DNS hijacking enabled: all outbound DNS (TCP/UDP 53) intercepted and redirected to PiHole, preventing devices hardcoding `8.8.8.8` or similar from bypassing it +- PiHole upstream DNS: Quad9 `9.9.9.9` — filtered, DNSSEC, IPv4 only + +--- + +## 5. Physical Port Assignment + +| Port | Device | VLAN | Notes | +|-------|-------------------------|---------|-------------------------------------------------------| +| WAN | Primary fibre | — | Unchanged | +| LAN 1 | 4G failover device | WAN2 | Repurposed as second WAN | +| LAN 2 | Servers switch | servers | Wired servers, including wayfaerer.lan (TVHeadend Pi) | +| LAN 3 | Sonos Connect (lounge) | media | Wired Sonos bridge | +| LAN 4 | Laptop (during cutover) | trusted | Reserved — ensures reliable connection during Phase 9 | + +--- + +## 6. Open Questions + +- [x] MAC addresses: servers VLAN devices (`ip link show eth0`) + doorbell + Shield TV (LuCI DHCP leases) +- [x] Shield TV current hostname/IP on the network +- [x] Console models (for the device inventory record) +- [x] Any smart TVs to add to media VLAN? No +- [x] Sonos: how many speakers, which models? They're all connected to SonosNet via the Connect +- [x] Any other IoT/smart devices not listed? Yes, added +- [x] Any devices that should stay on the flat 10.0.0.0/24 and NOT move to a VLAN? I don't believe so, I've added all known devices and annotated if they need internet access +- [x] Brel blinds controller: HA-initiated via UDP 32100 (servers → iot, covered by rule 5). Brel maintains a persistent cloud connection so needs internet access. +- [x] TVHeadend (wayfaerer): Shield streams via TCP 9981/9982 (media → servers); jester SSH is intra-VLAN, no rule needed +- [x] PiHole upstream DNS servers currently configured? Quad9 +- [x] Which LAN port is physically convenient for the 4G device: LAN 1 +- [x] 4G device model: GL.iNet XE300C6 Puli +- [x] SSID name for media VLAN: **Pinball Map** +- [x] SSID name for guest VLAN: **Passenger** diff --git a/files/avahi-daemon.conf b/files/avahi-daemon.conf new file mode 100644 index 0000000..fc9cf92 --- /dev/null +++ b/files/avahi-daemon.conf @@ -0,0 +1,22 @@ +[server] +use-ipv4=yes +use-ipv6=no +allow-interfaces=br-trusted,br-servers,br-media,br-iot +deny-interfaces=br-guest + +[wide-area] +enable-wide-area=no + +[publish] +disable-publishing=no + +[reflector] +enable-reflector=yes + +[rlimits] +rlimit-core=0 +rlimit-data=4194304 +rlimit-fsize=0 +rlimit-nofile=768 +rlimit-stack=4194304 +rlimit-nproc=3 diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..d30eaa5 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Pull current router config into config/ and commit if there are changes. +set -euo pipefail + +ROUTER="${ROUTER:-openwrt}" +CONFIGS=(dhcp dropbear firewall network system wireless) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_DIR="$SCRIPT_DIR/../config" + +echo "Pulling config from $ROUTER..." +for f in "${CONFIGS[@]}"; do + ssh "$ROUTER" "cat /etc/config/$f" > "$CONFIG_DIR/$f" + echo " $f" +done + +cd "$SCRIPT_DIR/.." +if git diff --quiet && git diff --cached --quiet; then + echo "No changes — config is up to date." +else + echo "" + git diff --stat config/ + echo "" + read -rp "Commit these changes? [y/N] " answer + if [[ "${answer,,}" == "y" ]]; then + git add config/ + git commit -m "backup: pull config from router $(date '+%Y-%m-%d %H:%M')" + echo "Committed." + fi +fi diff --git a/scripts/push-all.sh b/scripts/push-all.sh new file mode 100755 index 0000000..f60f241 --- /dev/null +++ b/scripts/push-all.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Push ALL config files to router and reload. +# WARNING: Use safe-apply.sh for individual risky changes (network, firewall, wireless). +# This script is for bulk pushes of low-risk configs (dhcp, system, dropbear). +set -euo pipefail + +ROUTER="${ROUTER:-openwrt}" +CONFIGS=(dhcp dropbear firewall network system wireless) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_DIR="$SCRIPT_DIR/../config" + +echo "WARNING: This will push all configs and reload. Use safe-apply.sh for network/firewall changes." +read -rp "Continue? [y/N] " answer +[[ "${answer,,}" == "y" ]] || exit 0 + +for f in "${CONFIGS[@]}"; do + echo " pushing $f..." + ssh "$ROUTER" "cat > /etc/config/$f" < "$CONFIG_DIR/$f" +done + +ssh "$ROUTER" "uci commit && reload_config" +echo "Done." diff --git a/scripts/safe-apply.sh b/scripts/safe-apply.sh new file mode 100755 index 0000000..28925d5 --- /dev/null +++ b/scripts/safe-apply.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Push a single config file to the router with an automatic revert safety net. +# +# Usage: ./safe-apply.sh [revert-minutes] +# config-name: e.g. "network", "wireless", "firewall" +# revert-minutes: how long before auto-revert fires (default: 5) +# +# The router will automatically reboot (and revert to its saved config) after +# REVERT_MINS minutes unless you explicitly confirm the change is working. +# On confirmation, the pending reboot is cancelled and the config is committed. + +set -euo pipefail + +CONFIG_NAME="${1:-}" +REVERT_MINS="${2:-5}" +ROUTER="${ROUTER:-openwrt}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="$SCRIPT_DIR/../config/$CONFIG_NAME" + +if [[ -z "$CONFIG_NAME" ]]; then + echo "Usage: $0 [revert-minutes]" + exit 1 +fi + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Error: $CONFIG_FILE not found." + exit 1 +fi + +echo "==> Staging auto-revert in ${REVERT_MINS} minutes on router..." +ssh "$ROUTER" "echo 'reboot' | at now + ${REVERT_MINS} minutes 2>/dev/null || { sleep ${REVERT_MINS}m && reboot; } &" + +echo "==> Pushing $CONFIG_NAME..." +ssh "$ROUTER" "cat > /etc/config/$CONFIG_NAME" < "$CONFIG_FILE" + +echo "==> Reloading service..." +case "$CONFIG_NAME" in + network) ssh "$ROUTER" "/etc/init.d/network restart" ;; + wireless) ssh "$ROUTER" "/etc/init.d/network restart" ;; + firewall) ssh "$ROUTER" "/etc/init.d/firewall restart" ;; + dhcp) ssh "$ROUTER" "/etc/init.d/dnsmasq restart" ;; + *) ssh "$ROUTER" "uci commit && reload_config" ;; +esac + +echo "" +echo "Config applied. You have ${REVERT_MINS} minutes to confirm." +echo "Test your connection, then come back here." +echo "" +read -rp "Is everything working? Confirm to cancel revert [y/N] " answer + +if [[ "${answer,,}" == "y" ]]; then + ssh "$ROUTER" "kill \$(atq 2>/dev/null | awk '{print \$1}' | xargs -I{} at -l {} 2>/dev/null | grep -l reboot | xargs) 2>/dev/null; killall -q sleep 2>/dev/null || true" + ssh "$ROUTER" "uci commit" + echo "Confirmed. Revert cancelled, config committed on router." +else + echo "Reverting — router will reboot in remaining window." +fi