starlink: dish route + status card on the Van Router page
The Starlink RTL8153 gets a declarative NM profile in 50-van-wan.yaml (replaces the auto 'Wired connection 1') with a 192.168.100.1/32 link route, so the dish's management address stays reachable from the router and — via the existing !br0 masquerade — from the van LAN, regardless of which WAN holds the default route. New Cockpit "Starlink" card queries the dish's gRPC API (:9200, get_status via grpcurl — not packaged in apt, deploy.sh warns when missing) inside the existing single batched spawn: online/obstructed/outage pill, alerts, uptime, sw version, PoP latency, down/up throughput, obstruction %. Sentinels distinguish adapter-absent / no-grpcurl / dish-unreachable. Gotcha captured while cutting over: a WAN profile without ipv4.route-metric makes NM re-assert its DHCP default (metric ~101) against van-failover's enforce_route pruning every cycle; the profile metric must match (failover's set_profile_metric does this on health transitions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2da97d26f4
commit
751facd511
+15
-3
@@ -1,7 +1,8 @@
|
|||||||
# /etc/netplan/50-van-wan.yaml — replaces cloud-init's 50-cloud-init.yaml.
|
# /etc/netplan/50-van-wan.yaml — replaces cloud-init's 50-cloud-init.yaml.
|
||||||
# Both onboard interfaces are WANs handed to NetworkManager so van-failover can
|
# Both onboard interfaces plus the Starlink USB NIC are WANs handed to
|
||||||
# steer them (mirrors wayback where NM owns all WANs): wlan0 = wifi uplink
|
# NetworkManager so van-failover can steer them (mirrors wayback where NM owns
|
||||||
# (Wapana at home / campsite wifi), eth0 = ethernet uplink. The AP radios and
|
# all WANs): wlan0 = wifi uplink (Wapana at home / campsite wifi), eth0 =
|
||||||
|
# ethernet uplink, enxd8ec5eeb3512 = Starlink. The AP radios and
|
||||||
# the wired LAN port (USB dongles, wlx*/enx* MAC-named) are deliberately absent:
|
# the wired LAN port (USB dongles, wlx*/enx* MAC-named) are deliberately absent:
|
||||||
# systemd-networkd/hostapd own them, and van-ap-unmanaged.conf hides them from NM.
|
# systemd-networkd/hostapd own them, and van-ap-unmanaged.conf hides them from NM.
|
||||||
# Apply once by hand: netplan generate && netplan apply (flaps both uplinks).
|
# Apply once by hand: netplan generate && netplan apply (flaps both uplinks).
|
||||||
@@ -12,6 +13,17 @@ network:
|
|||||||
renderer: NetworkManager
|
renderer: NetworkManager
|
||||||
optional: true
|
optional: true
|
||||||
dhcp4: true
|
dhcp4: true
|
||||||
|
# Starlink dish uplink (RTL8153 USB NIC, MAC-named — travels with the
|
||||||
|
# adapter). The /32 link route keeps the dish's management address reachable
|
||||||
|
# no matter which WAN holds the default route: the dish answers on
|
||||||
|
# 192.168.100.1 (gRPC :9200) even while the uplink sits behind CGNAT.
|
||||||
|
enxd8ec5eeb3512:
|
||||||
|
renderer: NetworkManager
|
||||||
|
optional: true
|
||||||
|
dhcp4: true
|
||||||
|
routes:
|
||||||
|
- to: 192.168.100.1/32
|
||||||
|
scope: link
|
||||||
wifis:
|
wifis:
|
||||||
wlan0:
|
wlan0:
|
||||||
renderer: NetworkManager
|
renderer: NetworkManager
|
||||||
|
|||||||
@@ -35,6 +35,11 @@
|
|||||||
<div id="failover"></div>
|
<div id="failover"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Starlink</h3>
|
||||||
|
<div id="starlink"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>WAN / Uplinks</h3>
|
<h3>WAN / Uplinks</h3>
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ const APS = [
|
|||||||
{ iface: "wlxd8ec5e2faa8c", unit: "hostapd-2g", band: "2.4GHz" }, // RTL8822BU
|
{ iface: "wlxd8ec5e2faa8c", unit: "hostapd-2g", band: "2.4GHz" }, // RTL8822BU
|
||||||
];
|
];
|
||||||
const PREFER_FILE = "/run/van-failover/prefer"; // van-failover reads this to pick the preferred WAN
|
const PREFER_FILE = "/run/van-failover/prefer"; // van-failover reads this to pick the preferred WAN
|
||||||
|
// Starlink dish: gRPC status API on the fixed management IP (reached via the /32
|
||||||
|
// link route the netplan profile installs on the RTL8153 uplink).
|
||||||
|
const STARLINK = { iface: "enxd8ec5eeb3512", dish: "192.168.100.1:9200" };
|
||||||
|
|
||||||
function run(args, opts) {
|
function run(args, opts) {
|
||||||
return cockpit.spawn(args, Object.assign({ err: "message" }, opts || {}));
|
return cockpit.spawn(args, Object.assign({ err: "message" }, opts || {}));
|
||||||
@@ -43,6 +46,13 @@ const STATUS_SCRIPT = (() => {
|
|||||||
add("neigh", "ip -j neigh show dev br0");
|
add("neigh", "ip -j neigh show dev br0");
|
||||||
add("leases", "cat /var/lib/misc/dnsmasq.leases");
|
add("leases", "cat /var/lib/misc/dnsmasq.leases");
|
||||||
add("thermal", "cat /run/van-thermal/state.json");
|
add("thermal", "cat /run/van-thermal/state.json");
|
||||||
|
// Sentinel words (ABSENT/NOGRPCURL/UNREACHABLE) let the renderer tell the
|
||||||
|
// three failure modes apart; anything starting with '{' is dish status JSON.
|
||||||
|
add("starlink",
|
||||||
|
`if ! ip link show ${STARLINK.iface} >/dev/null 2>&1; then echo ABSENT; ` +
|
||||||
|
`elif ! command -v grpcurl >/dev/null 2>&1; then echo NOGRPCURL; ` +
|
||||||
|
`else timeout 4 grpcurl -plaintext -max-time 3 -d '{"get_status":{}}' ` +
|
||||||
|
`${STARLINK.dish} SpaceX.API.Device.Device/Handle || echo UNREACHABLE; fi`);
|
||||||
add("battery", "cat /run/van-battery/state.json");
|
add("battery", "cat /run/van-battery/state.json");
|
||||||
add("failover", "cat /run/van-failover/state.json");
|
add("failover", "cat /run/van-failover/state.json");
|
||||||
add("devices", "nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status");
|
add("devices", "nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status");
|
||||||
@@ -169,6 +179,64 @@ function renderThermal(th) {
|
|||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Starlink (dish gRPC get_status via grpcurl) ---------- */
|
||||||
|
|
||||||
|
function fmtMbps(bps) {
|
||||||
|
return bps == null ? "—" : (bps / 1e6).toFixed(1) + " Mbps";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtUptime(s) {
|
||||||
|
s = parseInt(s, 10);
|
||||||
|
if (isNaN(s)) return "—";
|
||||||
|
const d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60);
|
||||||
|
return (d ? `${d}d ` : "") + (d || h ? `${h}h ` : "") + `${m}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStarlink(raw) {
|
||||||
|
const el = document.getElementById("starlink");
|
||||||
|
const t = (raw || "").trim();
|
||||||
|
if (!t || t === "ABSENT") {
|
||||||
|
el.innerHTML = `<p class="muted">Starlink adapter (<code>${esc(STARLINK.iface)}</code>) not plugged in.</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (t === "NOGRPCURL") {
|
||||||
|
el.innerHTML = `<p class="muted">grpcurl not installed — the dish status API is gRPC. ` +
|
||||||
|
`arm64 binary: <code>github.com/fullstorydev/grpcurl/releases</code></p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (t === "UNREACHABLE" || t[0] !== "{") {
|
||||||
|
el.innerHTML = `<p><span class="pill bad">dish unreachable</span> ` +
|
||||||
|
`<span class="muted">adapter present but 192.168.100.1 not answering ` +
|
||||||
|
`(dish booting / unpowered / route missing?)</span></p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const st = (parseJSON(t, {}) || {}).dishGetStatus || {};
|
||||||
|
const obs = st.obstructionStats || {};
|
||||||
|
const alerts = Object.keys(st.alerts || {}).filter(k => st.alerts[k]);
|
||||||
|
|
||||||
|
let state;
|
||||||
|
if (st.outage)
|
||||||
|
state = `<span class="pill bad">${esc(st.outage.cause || "OUTAGE")}</span>`;
|
||||||
|
else if (obs.currentlyObstructed)
|
||||||
|
state = `<span class="pill warn">obstructed</span>`;
|
||||||
|
else
|
||||||
|
state = `<span class="pill ok">online</span>`;
|
||||||
|
|
||||||
|
let html = `<p>${state}` +
|
||||||
|
(alerts.length ? ` <span class="pill warn">alerts: ${esc(alerts.join(", "))}</span>` : "") +
|
||||||
|
` <span class="muted">uptime ${esc(fmtUptime((st.deviceState || {}).uptimeS))}` +
|
||||||
|
` · sw ${esc((st.deviceInfo || {}).softwareVersion || "—")}</span></p>`;
|
||||||
|
html += `<table><thead><tr><th>Latency (PoP)</th><th>Down</th><th>Up</th><th>Obstructed</th></tr></thead><tbody>`;
|
||||||
|
html += `<tr><td>${st.popPingLatencyMs == null || st.popPingLatencyMs < 0 ? "—" : esc(st.popPingLatencyMs.toFixed(0)) + " ms"}</td>` +
|
||||||
|
`<td>${esc(fmtMbps(st.downlinkThroughputBps))}</td>` +
|
||||||
|
`<td>${esc(fmtMbps(st.uplinkThroughputBps))}</td>` +
|
||||||
|
`<td>${obs.fractionObstructed == null ? "—" : esc((obs.fractionObstructed * 100).toFixed(1)) + " %"}</td></tr>`;
|
||||||
|
html += `</tbody></table>`;
|
||||||
|
html += `<p class="muted">Dish web UI: <code>http://192.168.100.1</code> (from the van LAN)</p>`;
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Battery / power source (van-battery daemon state) ---------- */
|
/* ---------- Battery / power source (van-battery daemon state) ---------- */
|
||||||
|
|
||||||
function renderBattery(b) {
|
function renderBattery(b) {
|
||||||
@@ -320,6 +388,7 @@ async function refresh() {
|
|||||||
const secs = parseSections(await sh(STATUS_SCRIPT));
|
const secs = parseSections(await sh(STATUS_SCRIPT));
|
||||||
renderAPs(APS.map((a, i) => parseAP(a, i, secs)), parseClientDir(secs));
|
renderAPs(APS.map((a, i) => parseAP(a, i, secs)), parseClientDir(secs));
|
||||||
renderThermal(parseJSON(secs.thermal, null));
|
renderThermal(parseJSON(secs.thermal, null));
|
||||||
|
renderStarlink(secs.starlink);
|
||||||
renderBattery(parseJSON(secs.battery, null));
|
renderBattery(parseJSON(secs.battery, null));
|
||||||
renderFailover(parseJSON(secs.failover, null));
|
renderFailover(parseJSON(secs.failover, null));
|
||||||
renderWAN(parseWAN(secs));
|
renderWAN(parseWAN(secs));
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ install -d /usr/share/cockpit/vanrouter
|
|||||||
install -m0644 cockpit/vanrouter/* /usr/share/cockpit/vanrouter/
|
install -m0644 cockpit/vanrouter/* /usr/share/cockpit/vanrouter/
|
||||||
# Bridge fd headroom (Python bridge frees spawn pipes only at GC; 1024 is too tight)
|
# Bridge fd headroom (Python bridge frees spawn pipes only at GC; 1024 is too tight)
|
||||||
install -D -m0644 cockpit/cockpit-session-nofile.conf /etc/systemd/system/cockpit-session@.service.d/nofile.conf
|
install -D -m0644 cockpit/cockpit-session-nofile.conf /etc/systemd/system/cockpit-session@.service.d/nofile.conf
|
||||||
|
# The Starlink card queries the dish's gRPC API; grpcurl isn't packaged in apt.
|
||||||
|
command -v grpcurl >/dev/null 2>&1 \
|
||||||
|
|| echo " -> grpcurl missing (Starlink card will say so): install linux_arm64 binary from github.com/fullstorydev/grpcurl/releases"
|
||||||
|
|
||||||
echo "== thermal monitor =="
|
echo "== thermal monitor =="
|
||||||
install -D -m0755 power/van-thermal /usr/local/sbin/van-thermal
|
install -D -m0755 power/van-thermal /usr/local/sbin/van-thermal
|
||||||
|
|||||||
Reference in New Issue
Block a user