A VPN tab: the tunnels, and who is on them

Beside Network, per machine. Every WireGuard interface on the machine in the
switcher, with its address, listen port and public key, and under it the peers:
name, allowed addresses, last handshake, where they connect from, and what they
have moved.

Per machine is the point rather than a limitation. The tunnel devices dial into
and a tunnel a machine dials out through are different boxes here, and putting
them on one page would invite the question of which box you were looking at.

The kernel is the truth and the configuration only supplies names: wg show all
dump has the handshakes and the counters and knows nothing but public keys, so
the # comment above a peer in a wg-quick file, or a UCI section's description on
OpenWrt, is read alongside purely to label them. Whether a tunnel is dialled or
dialling is worked out from the shape of its peers, and says neither when it
cannot tell.

Online means a handshake in the last three minutes, and the tab says so in as
many words: WireGuard is silent when idle, so there is no connection to be in.

Read-only, and no secrets leave: the private key is the second column of the
dump and is read past, pre-shared keys are noted as present and never read.

The Network tab keeps listing tunnels as the interfaces they are, and hands off
to here for anything about them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-27 21:59:30 -07:00
co-authored by Claude Opus 5
parent 9c7848907b
commit 6f634bbe11
10 changed files with 686 additions and 4 deletions
+26 -3
View File
@@ -13,6 +13,7 @@ import Updates from './Updates'
import Disks from './Disks'
import Hardware from './Hardware'
import Network from './Network'
import VPN from './VPN'
import ZFS from './ZFS'
import KVM from './KVM'
import Files from './Files'
@@ -60,6 +61,14 @@ const GlobeIcon = (
</svg>
)
// A padlock-ish shield: the tab is about who may come in, and a keyhole reads at 14px where
// a padlock's shackle does not.
const ShieldIcon = (
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round" aria-hidden="true">
<path d="M8 1.6 13.2 3.5v4.2c0 3-2.1 5.4-5.2 6.7-3.1-1.3-5.2-3.7-5.2-6.7V3.5z" /><circle cx="8" cy="7.2" r="1.3" /><path d="M8 8.5v2" />
</svg>
)
const GROUPS: Group[] = [
{ id: 'procs', label: 'Processes', icon: '▚' },
{ id: 'terminal', label: 'Terminal', icon: '>_' },
@@ -68,6 +77,7 @@ const GROUPS: Group[] = [
{ id: 'disks', label: 'Disks', icon: '▤' },
{ id: 'files', label: 'Files', icon: FolderIcon },
{ id: 'network', label: 'Network', icon: GlobeIcon },
{ id: 'vpn', label: 'VPN', icon: ShieldIcon },
{ id: 'usage', label: 'Usage', icon: '▧' },
{ id: 'zfs', label: 'ZFS', icon: '⛁' },
{ id: 'docker', label: 'Docker', icon: DockerIcon },
@@ -81,8 +91,8 @@ const GROUPS: Group[] = [
{ id: 'settings', label: 'Settings', icon: '⚙' },
]
type View = 'terminal' | 'widgets' | 'docker' | 'alerts' | 'services' | 'jobs' | 'updates' | 'disks' | 'hardware' | 'network' | 'zfs' | 'usage' | 'files' | 'procs' | 'machines' | 'kvm' | 'users' | 'settings'
const VIEWS: View[] = ['terminal', 'widgets', 'docker', 'alerts', 'services', 'jobs', 'updates', 'disks', 'hardware', 'network', 'zfs', 'usage', 'files', 'procs', 'machines', 'kvm', 'users', 'settings']
type View = 'terminal' | 'widgets' | 'docker' | 'alerts' | 'services' | 'jobs' | 'updates' | 'disks' | 'hardware' | 'network' | 'vpn' | 'zfs' | 'usage' | 'files' | 'procs' | 'machines' | 'kvm' | 'users' | 'settings'
const VIEWS: View[] = ['terminal', 'widgets', 'docker', 'alerts', 'services', 'jobs', 'updates', 'disks', 'hardware', 'network', 'vpn', 'zfs', 'usage', 'files', 'procs', 'machines', 'kvm', 'users', 'settings']
const HUB_ONLY: View[] = ['machines', 'users', 'settings', 'alerts']
// URL hash is the source of truth for where you are: #/<machine|hub>/<view>. Back and
@@ -139,6 +149,18 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut:
useEffect(() => { reload() }, [reload])
// One page sending you to another — the Network tab handing a tunnel over to the VPN tab.
// A custom event rather than a hash assignment, because assigning location.hash does not
// reliably raise popstate, which is what this shell navigates by.
useEffect(() => {
const h = (e: Event) => {
const id = (e as CustomEvent<string>).detail
if (VIEWS.includes(id as View)) { setView(id as View); setDrawer(false) }
}
window.addEventListener('hamlab:view', h)
return () => window.removeEventListener('hamlab:view', h)
}, [])
// A shell opened inside a container from the Docker tab is a terminal like any other:
// pick it up, show the Terminal tab, and make it the active one.
useEffect(() => {
@@ -313,6 +335,7 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut:
{view === 'disks' && <Disks />}
{view === 'hardware' && <Hardware />}
{view === 'network' && <Network />}
{view === 'vpn' && <VPN />}
{view === 'zfs' && <ZFS />}
{view === 'files' && <Files onOpenInTerminal={runInTerminal} />}
{termView && <TermTabs terms={ordered} active={active} creating={creating}
@@ -340,7 +363,7 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut:
<span>{machine ? `${title} · ` : ''}{me.version.startsWith('v') ? me.version : 'v' + me.version}</span>
<span>up {fmtUptime(me.uptime)}</span>
<span className="spacer" />
<span className="muted">{view === 'widgets' ? 'Widgets' : view === 'docker' ? 'Docker' : view === 'alerts' ? 'Alerts' : view === 'services' ? 'Services' : view === 'jobs' ? 'Jobs' : view === 'updates' ? 'Updates' : view === 'disks' ? 'Disks' : view === 'hardware' ? 'Hardware' : view === 'network' ? 'Network' : view === 'zfs' ? 'ZFS' : view === 'usage' ? 'Usage' : view === 'files' ? 'Files' : view === 'procs' ? 'Processes' : view === 'machines' ? 'Machines' : view === 'kvm' ? 'KVM' : view === 'users' ? 'Users' : view === 'settings' ? 'Settings' : activeTerm ? activeTerm.name : ''}</span>
<span className="muted">{view === 'widgets' ? 'Widgets' : view === 'docker' ? 'Docker' : view === 'alerts' ? 'Alerts' : view === 'services' ? 'Services' : view === 'jobs' ? 'Jobs' : view === 'updates' ? 'Updates' : view === 'disks' ? 'Disks' : view === 'hardware' ? 'Hardware' : view === 'network' ? 'Network' : view === 'vpn' ? 'VPN' : view === 'zfs' ? 'ZFS' : view === 'usage' ? 'Usage' : view === 'files' ? 'Files' : view === 'procs' ? 'Processes' : view === 'machines' ? 'Machines' : view === 'kvm' ? 'KVM' : view === 'users' ? 'Users' : view === 'settings' ? 'Settings' : activeTerm ? activeTerm.name : ''}</span>
</footer>
</div>
)