fix(vpn): backport VPN fixes from production debugging

- Fix _commit_and_sync infinite recursion
- Use admin session for subnet_index allocation (bypass RLS)
- Auto-set VPN endpoint from CORS_ORIGINS hostname
- Remove server address field from VPN setup UI
- Add DELETE endpoint and button for VPN config removal
- Add wg-reload watcher for reliable config hot-reload via wg syncconf
- Add wg_status.json writer for live peer handshake status in UI
- Per-tenant SNAT for poller-to-device routing through VPN
- Restrict VPN→eth0 forwarding to Docker networks only (block exit node abuse)
- Use 10.10.0.0/16 allowed-address in RouterOS commands
- Fix structlog event= conflict (use audit=True)
- Export backup_scheduler proxy for firmware/upgrade imports
This commit is contained in:
Jason Staack
2026-03-14 20:59:14 -05:00
parent b5f9bf14df
commit 2ad0367c91
7 changed files with 194 additions and 31 deletions

View File

@@ -134,6 +134,16 @@ export function VpnPage() {
},
})
const deleteMutation = useMutation({
mutationFn: () => vpnApi.deleteConfig(tenantId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['vpn-config'] })
queryClient.invalidateQueries({ queryKey: ['vpn-peers'] })
toast({ title: 'VPN configuration deleted' })
},
onError: (e: any) => toast({ title: e?.response?.data?.detail || 'Failed to delete VPN', variant: 'destructive' }),
})
// ── Helpers ──
const connectedPeerIds = new Set(peers.map((p) => p.device_id))
@@ -195,22 +205,6 @@ export function VpnPage() {
</p>
</div>
<div className="space-y-3 text-left">
<Label htmlFor="endpoint" className="text-text-secondary">
Server Address <span className="text-text-muted">(optional)</span>
</Label>
<Input
id="endpoint"
placeholder="your-server.example.com:51820"
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
className="text-center"
/>
<p className="text-xs text-text-muted">
The public hostname or IP where devices will connect. You can set this later.
</p>
</div>
{writable && (
<Button
onClick={() => setupMutation.mutate()}
@@ -264,6 +258,19 @@ export function VpnPage() {
<Button size="sm" onClick={() => setShowAddDevice(true)}>
<Plus className="h-4 w-4 mr-1" /> Add Device
</Button>
<Button
variant="outline"
size="sm"
className="text-red-400 border-red-800 hover:bg-red-900/30"
onClick={() => {
if (confirm('Delete VPN configuration? All peers will be removed.')) {
deleteMutation.mutate()
}
}}
disabled={deleteMutation.isPending}
>
Delete VPN
</Button>
</>
)}
</div>

View File

@@ -1136,6 +1136,9 @@ export const vpnApi = {
updateConfig: (tenantId: string, data: { endpoint?: string; is_enabled?: boolean }) =>
api.patch<VpnConfigResponse>(`/api/tenants/${tenantId}/vpn`, data).then((r) => r.data),
deleteConfig: (tenantId: string) =>
api.delete(`/api/tenants/${tenantId}/vpn`),
listPeers: (tenantId: string) =>
api.get<VpnPeerResponse[]>(`/api/tenants/${tenantId}/vpn/peers`).then((r) => r.data),