// 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 { FormEvent, useCallback, useEffect, useState } from 'react'
import qrcode from 'qrcode-generator'
import { api, CreatedVPNPeer, VPNReport, VPNPeer, VPNTunnel } from './api'
import { fmtBytes } from './fmt'
import { Btn, Loading, askText, toast, useEscape } 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, iface, editable, onChanged }: { p: VPNPeer; server: boolean; iface: string; editable: boolean; onChanged: () => void }) {
const rename = async () => {
const name = await askText(`Rename ${p.name || 'this device'}`, p.name ?? '')
if (!name || name.trim() === p.name) return
try { await api.editVPNPeer(iface, p.section!, { name: name.trim() }); toast('renamed'); onChanged() }
catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') }
}
const remove = async () => {
const typed = await askText(`Remove ${p.name}? Its profile stops working at once and cannot be brought back — the device would need a new one.\n\nType its name to confirm.`, '', 'Remove')
if (typed === null) return
try { await api.removeVPNPeer(iface, p.section!, typed.trim()); toast(`${p.name} removed`); onChanged() }
catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') }
}
return (
{err === 'not found' ? "This machine's Ham Lab is older than the VPN tab. Update it from Machines." : err}
: }
// A machine running an older build answers with null rather than an empty list, and taking
// the length of that is what a blank page is made of.
const tunnels = d.tunnels ?? []
return (
{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.
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.