// VPN: this machine's WireGuard tunnels, and the devices on them. // // Per machine, like every other tab, which here is the whole point: the tunnel people dial // into and a tunnel this machine dials out through are different machines, and showing them // together would invite the question "which box is this actually on". import { useCallback, useEffect, useState } from 'react' import { api, VPNReport, VPNPeer, VPNTunnel } from './api' import { fmtBytes } from './fmt' import { Loading } from './ui' import { useRefreshMs } from './prefs' // WireGuard says nothing when it has nothing to say, so "online" is a handshake in the last // few minutes rather than a connection — there is no connection to have. const ago = (iso?: string) => { if (!iso) return 'never' const s = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000) if (s < 60) return `${Math.floor(s)}s ago` if (s < 3600) return `${Math.floor(s / 60)} min ago` if (s < 86400) return `${Math.floor(s / 3600)} h ago` return `${Math.floor(s / 86400)} d ago` } // A public key is long and nobody reads the middle of it; the ends are what you compare. const shortKey = (k: string) => (k.length > 16 ? `${k.slice(0, 8)}…${k.slice(-6)}` : k) function Peer({ p, server }: { p: VPNPeer; server: boolean }) { return (
{p.name || unnamed} {p.online && online} {!p.handshake && never connected} {(p.allowed_ips ?? []).join(', ')}
Last handshake{ago(p.handshake)}
{p.endpoint &&
{server ? 'Comes from' : 'Endpoint'}{p.endpoint}
}
Moved↓ {fmtBytes(p.rx)} · ↑ {fmtBytes(p.tx)}
{!!p.keepalive &&
Keepaliveevery {p.keepalive}s
}
Public key{shortKey(p.public_key)}
{p.psk &&
Pre-shared keyin use
}
) } function Tunnel({ t }: { t: VPNTunnel }) { const server = t.kind === 'server' const online = t.peers.filter((p) => p.online).length return ( <>

{t.name}

{server ? 'devices dial in' : t.kind === 'client' ? 'this machine dials out' : 'tunnel'} {server ? `${online} of ${t.peers.length} online` : t.exit ? `via ${t.exit}` : ''}
{!!t.addrs?.length &&
Address{t.addrs.join(', ')}
} {!!t.listen_port &&
Listening on{t.listen_port}/udp
} {!!t.mtu &&
MTU{t.mtu}
}
Public key{shortKey(t.public_key)}
{t.fwmark &&
Firewall mark{t.fwmark}
}
{t.peers.map((p) => )} {t.peers.length === 0 &&
No peers are configured on this tunnel.
} ) } export default function VPN() { const [d, setD] = useState(null) const [err, setErr] = useState('') const rate = useRefreshMs() const load = useCallback(async () => { try { setD(await api.vpn()); setErr('') } catch (e) { setErr(e instanceof Error ? e.message : 'failed') } }, []) useEffect(() => { load() }, [load]) // Handshakes and counters move; nothing here is expensive to read. useEffect(() => { const t = setInterval(() => { if (document.visibilityState === 'visible') load() }, Math.max(5000, rate)) return () => clearInterval(t) }, [load, rate]) if (!d) return
{err ?
{err === 'not found' ? "This machine's Ham Lab is older than the VPN tab. Update it from Machines." : err}
: }
return (
{d.tunnels.length === 0 && ( <>

VPN

{d.note || 'Nothing here.'}
Tunnels are the machine's own, so look on the one that carries them — the switcher at the top of the rail.
)} {d.tunnels.map((t) => )} {d.tunnels.length > 0 && (
Read from the kernel with wg show; names come from the configuration beside it. Online means a handshake in the last three minutes — WireGuard is silent when idle, so there is no connection to be in. Private and pre-shared keys are never read.
)}
) }