9.8 KiB
Host-local configuration for multi-host vanlink
Date: 2026-09-23. Status: approved by user; revised 2026-09-23 (LAN_IFACE rename, symlinked host config).
Problem
Two hosts run vanlink from this repo: the Pi 5 (10.42.0.1) and the FriendlyElec
R5C (10.42.1.1). Host-specific values (interface names, addressing, storage
devices, which optional services to run) live in deploy.conf, which is tracked in git
and was hand-edited per host. The hosts drifted: each has its own uncommitted
deploy.conf, and the R5C runs a non-git copy of the repo. On 2026-09-23 a deploy on
the R5C failed because:
van-ap-dnsmasq.confand21-van-br0.networkcontain@TOKEN@placeholders butdeploy.shinstalled them with plaininstall, so they reached/etcunrendered (dnsmasq failed to parse, br0 got no address);nftables.confon the host was an unrendered copy for the same reason;- Armbian's netplan-generated
10-netplan-all-eth-interfaces.networksorts before22-van-lan.network, so networkd never applied the bridge config to the LAN port.
Other host-specific values are hardcoded outside deploy.conf: van-br0-setup.service
(enP2p33s0, wlp1s0), failover/config.json and router-status/config.json
(wlan0), ap/50-van-wan.yaml (wlan0), ap/van-ap-unmanaged.conf (eth0),
ap/23-van-lan-eth0.network, power/nvme-watch-config.json (NVMe device),
failover/sync-usb-backup.sh (/dev/sda2, NFS target), and failover/sync-sd-backup.sh
(SD UUID).
Decisions already made
- One git repo, cloned on every host, same branch; no per-host copies or branches.
- All host-specific values live in one file per host under
hosts/in the repo, and/etc/vanlink.confon each host is a symlink to that host's file. (Revised from "outside the repo": one versioned source of truth per host, no drift,git pullcarries config changes.) - Per-host optional services are controlled by explicit feature flags in that file.
deploy.shkeeps rendering@TOKEN@placeholders at deploy time (approach A); services do not read/etc/vanlink.confat runtime, except the two backup shell scripts.
Design
1. Host config file and the /etc/vanlink.conf symlink
Each host has hosts/<hostname>.conf (named by hostname -s, so hosts/van.conf
for the R5C and hosts/wan.conf for the Pi 5; a hardware swap does not rename a
host). /etc/vanlink.conf is an absolute symlink to it:
sudo ln -sfn "$PWD/hosts/$(hostname -s).conf" /etc/vanlink.conf. Everything reads
/etc/vanlink.conf, so there is one fixed path. Consequences, accepted:
- Editing a host's values means committing to
hosts/<hostname>.conf(on the host or the laptop) and pulling; there is no invisible per-host drift, but a dirty checkout on a host is still possible anddeploy.shreports it (see 3). - Root
sources a file writable by the checkout's owner. Fine on these single-user boxes (that user has sudo anyway); it is also why the file must never hold secrets. - If the checkout is moved the symlink dangles;
deploy.shsays so and prints thelncommand.
Plain KEY=value lines, comments allowed, no secrets (it is in git and read by
root). It is valid shell,
so deploy.sh and the backup scripts source it. Every variable below is required
unless marked optional: deploy.sh aborts naming any missing one, so a variable added
in a later release fails loudly on hosts that have not adopted it instead of silently
defaulting.
| Group | Variable | Meaning / used by |
|---|---|---|
| Interfaces | WIFI_5G_IFACE, WIFI_2G_IFACE |
AP radios (hostapd, networkd, br0-setup) |
LAN_IFACE |
wired LAN port bridged into br0 (renamed from LAN_USB_IFACE; the R5C's is PCIe) |
|
STARLINK_IFACE |
Starlink uplink (netplan, failover, status) | |
WIFI_WAN_IFACE (new) |
Wi-Fi uplink, currently hardcoded wlan0 |
|
ONBOARD_LAN_IFACE (new, optional) |
extra onboard LAN port bridged into br0 (Pi: eth0); empty = none |
|
| Addressing | GATEWAY_IP, GATEWAY_PREFIX, GATEWAY_NETWORK, DHCP_RANGE_START, DHCP_RANGE_END |
br0, dnsmasq, nftables |
DNS_RESOLVERS |
fixed upstream resolvers | |
| Hardware | MODEM_USB_VENDOR |
modem watch / udev |
NVME_DEVICE, NVME_FS_DEVICE (new) |
NVMe watchdog; only read when ENABLE_NVME_WATCH=1 |
|
| Backup | USB_BACKUP_ROOT_PART, USB_BACKUP_MOUNT (new) |
sync-usb-backup.sh |
SD_BACKUP_UUID (new) |
sync-sd-backup.sh |
|
| Features | ENABLE_COCKPIT (replaces SKIP_COCKPIT) |
Cockpit plugin |
ENABLE_LI3 (replaces the Pi's uncommitted SKIP_LI3) |
Li3 battery service | |
ENABLE_NVME_WATCH |
NVMe watchdog | |
ENABLE_HA |
podman quadlets: Home Assistant + ESPHome; adds podman to required packages |
|
ENABLE_FRIGATE |
Frigate quadlet |
Backup variables are only validated by the script that uses them, not by deploy.sh,
because a host uses one backup method or neither.
2. Repo changes
deploy.confis deleted.hosts/vanlink.conf.example: every variable with a comment (the reference).hosts/van.conf(R5C),hosts/wan.conf(Pi 5): the real, versioned values for the two hosts, also the test input for--check.deploy.shnever creates the symlink or edits these files.- New tokens:
@WIFI_WAN_IFACE@,@ONBOARD_LAN_IFACE@,@NVME_DEVICE@,@NVME_FS_DEVICE@.van-br0-setup.service,failover/config.json,router-status/config.json,ap/50-van-wan.yaml,ap/van-ap-unmanaged.conf,power/nvme-watch-config.jsonandap/nftables.confbecome templates (nftables currently hardcodes10.42.1.0/24, which is wrong on the Pi 5). README.mdheader ("Pi 4 port") updated to describe the shared model.
3. deploy.sh behaviour
- Config path is
${VANLINK_CONF:-/etc/vanlink.conf}. Missing or dangling symlink: abort with thelncommand for this host. Missing/empty required variable: abort naming it. It prints the resolved file it is using, and warns if that file has uncommitted changes (git status) so a deploy from unreviewed values is visible. install_renderedrefuses to install any output that still matches@[A-Z][A-Z0-9_]*@and aborts before anything is restarted. Every file containing a token must be installed through it (enforced by the test below).ap/22-van-lan.networkis installed as05-van-lan.network(sorts before the netplan-generated10-file) and any old22-van-lan.networkis removed. Same fix for23-van-lan-eth0.networkwhenONBOARD_LAN_IFACEis set; when empty it is neither installed nor left behind.- Feature-flagged sections: install,
enableandrestartare all skipped when the flag is 0 (todaysystemctl enable/restartnames van-li3-battery unconditionally). ./deploy.sh --check [conf]: no root needed, no installs. Loads the conf (default/etc/vanlink.conf), validates variables, renders every token-bearing file to a temp dir, and reports leftover tokens. Exit non-zero on any problem../deploy.sh render <file>keeps working.- Failure while restarting
nftablesorvan-ap-dnsmasqis reported as a warning in the summary (it is today printed and lost).
4. Backup scripts
sync-usb-backup.sh and sync-sd-backup.sh source /etc/vanlink.conf and use
${VAR:?set in /etc/vanlink.conf}. Hardcoded devices, UUIDs and NFS targets are
removed. The failover/dormant-boot.scr logic of sync-sd-backup.sh is unchanged.
Testing
tests/check-templates.sh (plain bash, run from the repo root, no root needed):
bash -n deploy.shand the backup scripts.- Every repo file containing
@TOKEN@is installed bydeploy.shviainstall_rendered(fails on the class of bug from 2026-09-23). - For each
hosts/*.conf:./deploy.sh --check hosts/x.confpasses, and rendering all templates leaves no@…@. hosts/vanlink.conf.exampledefines exactly the variablesdeploy.shrequires (no drift between docs and code).
On the hosts: --check first, then a real deploy. The R5C deploy is done at a console
or with the cabling settled (see Migration); a deploy restarts systemd-networkd.
Migration
- Implement and merge on
wan, run the test script, push. - R5C: create
hosts/van.conffrom its currentdeploy.conf(interfaces as currently swapped) in the repo, commit and push; on the R5Cgit clonenext to the stale copy, keep the old directory asvanlink.old, make the symlink, run--check, then deploy. - Pi 5: create
hosts/wan.conffrom itsdeploy.conf(its real interfaces,10.42.0.x,ENABLE_LI3=0,ENABLE_COCKPIT=0,ENABLE_HAetc. as it is today); copy its 4 modified files to a backup directory;git checkout -- .and fast-forward to the sharedwan; make the symlink;--check; deploy. Its existing behaviour must not change:diffthe rendered files against what is currently in/etcbefore restarting anything. mainis brought up to date withwanafterwards (separate decision; hosts followwan).
Rollback per host: restore the old directory and, if needed, the previous files from
/etc (deploy.sh does not currently back them up; the pre-deploy diff in step 3 is
the safety net for the Pi 5).
Out of scope / follow-ups
ap/50-van-wan.yamlcontains the Wi-Fi PSK hash for "Wapana", committed to git. Should move to the host (and the value be considered exposed); separate change.power/thermal-config.jsonhardcodes Pi sensor names (rp1_adc,nvme); the R5C needs its own sensor list. A per-host mechanism for it is deferred until wanted.- Netplan (
ap/50-van-wan.yaml) remains a manual step; the drift check stays as is. - The
.bakfiles inap/andfailover/are unrelated clutter.
Open questions for review
None open. Resolved: LAN_USB_IFACE becomes LAN_IFACE (the lan_usb_iface key in
router-status/config.json is a consumer-facing name and stays); host files live in
the repo and are reached through the /etc/vanlink.conf symlink.