Add central firewall and tunnel status screen

This commit is contained in:
archipelago
2026-10-07 09:12:42 -04:00
parent b50bf6159a
commit ef6c10f06e
3 changed files with 123 additions and 2 deletions
+5
View File
@@ -190,6 +190,11 @@ const router = createRouter({
name: 'openwrt-gateway',
component: () => import('../views/server/OpenWrtGateway.vue'),
},
{
path: 'server/firewall-tunnels',
name: 'firewall-tunnels',
component: () => import('../views/server/FirewallTunnelSettings.vue'),
},
{
path: 'monitoring',
name: 'monitoring',
+14 -2
View File
@@ -94,13 +94,18 @@
</svg>
Refreshing network...
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<router-link
to="/dashboard/server/firewall-tunnels"
class="flex items-center justify-between p-3 bg-white/5 rounded-lg hover:bg-white/10 transition-colors text-left"
aria-label="Open firewall and tunnel settings"
>
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
<span class="text-white/80 text-sm">Firewall Active</span>
</div>
<span class="text-green-400 text-sm font-medium">Protected</span>
</div>
<svg class="w-4 h-4 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</router-link>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" /></svg>
@@ -159,6 +164,13 @@
</div>
</template>
</div>
<router-link
to="/dashboard/server/firewall-tunnels"
class="mt-4 w-full flex items-center justify-center gap-2 p-3 rounded-lg bg-white/10 hover:bg-white/15 transition-colors text-sm text-white/90"
>
Firewall &amp; tunnels
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</router-link>
</div>
<FipsNetworkCard />
@@ -0,0 +1,104 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { RouterLink } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
interface TunnelStatus {
connected?: boolean
provider?: string
ip_address?: string
wg_ip?: string
}
interface FipsStatus {
installed?: boolean
service_active?: boolean
anchor_connected?: boolean
authenticated_peer_count?: number
}
interface Forward { [key: string]: unknown }
const loading = ref(true)
const error = ref('')
const tunnel = ref<TunnelStatus | null>(null)
const fips = ref<FipsStatus | null>(null)
const forwards = ref<Forward[]>([])
const tunnelLabel = computed(() => {
if (!tunnel.value) return 'Unknown'
return tunnel.value.connected ? 'Connected' : 'Not connected'
})
const firewallLabel = computed(() => {
if (!fips.value) return 'Unknown'
if (fips.value.service_active === false) return 'Inactive'
return 'Protected'
})
async function refresh() {
loading.value = true
error.value = ''
const results = await Promise.allSettled([
rpcClient.vpnStatus(),
rpcClient.call<FipsStatus>({ method: 'fips.status', dedup: true, maxRetries: 1 }),
rpcClient.call<{ forwards?: Forward[] }>({ method: 'router.list-forwards', dedup: true, maxRetries: 1 }),
])
const failures = results.filter((result) => result.status === 'rejected')
if (results[0]?.status === 'fulfilled') tunnel.value = results[0].value
if (results[1]?.status === 'fulfilled') fips.value = results[1].value
if (results[2]?.status === 'fulfilled') forwards.value = results[2].value.forwards ?? []
if (failures.length === results.length) error.value = 'Could not load network protection status'
else if (failures.length) error.value = 'Some checks are unavailable; verify each route before changing it'
loading.value = false
}
onMounted(refresh)
</script>
<template>
<main class="max-w-5xl mx-auto px-4 sm:px-6 py-6">
<div class="flex items-start justify-between gap-4 mb-6">
<div>
<RouterLink to="/dashboard/server" class="text-xs text-white/50 hover:text-white">← Network</RouterLink>
<h1 class="text-2xl font-semibold text-white mt-2">Firewall &amp; tunnels</h1>
<p class="text-sm text-white/60 mt-1">Review reachability before exposing a service. Transport access and app authentication are separate controls.</p>
</div>
<button class="glass-button px-3 py-2 text-sm shrink-0" :disabled="loading" @click="refresh">
<span v-if="loading" class="inline-flex items-center gap-2"><span class="h-3.5 w-3.5 rounded-full border-2 border-white/30 border-t-white animate-spin" /> Checking…</span>
<span v-else>Refresh checks</span>
</button>
</div>
<div v-if="error" class="mb-4 rounded-lg border border-orange-400/30 bg-orange-500/10 px-4 py-3 text-sm text-orange-200">{{ error }}</div>
<section class="grid gap-4 md:grid-cols-2 mb-6">
<div class="glass-card p-5">
<h2 class="text-lg font-medium text-white">Firewall</h2>
<p class="text-sm text-white/60 mt-1">Node protection status and mesh reachability.</p>
<div class="mt-4 flex items-center justify-between"><span class="text-white/70">Node firewall</span><span :class="firewallLabel === 'Protected' ? 'text-green-300' : 'text-orange-300'">{{ firewallLabel }}</span></div>
<div class="mt-2 flex items-center justify-between"><span class="text-white/70">Authenticated mesh peers</span><span class="text-white">{{ fips?.authenticated_peer_count ?? 'Unknown' }}</span></div>
</div>
<div class="glass-card p-5">
<h2 class="text-lg font-medium text-white">Tunnel</h2>
<p class="text-sm text-white/60 mt-1">Connection and handshake status.</p>
<div class="mt-4 flex items-center justify-between"><span class="text-white/70">Tunnel</span><span :class="tunnel?.connected ? 'text-green-300' : 'text-orange-300'">{{ tunnelLabel }}</span></div>
<div class="mt-2 flex items-center justify-between"><span class="text-white/70">Address</span><span class="text-white font-mono text-xs">{{ tunnel?.wg_ip || tunnel?.ip_address || 'Unknown' }}</span></div>
</div>
</section>
<section class="glass-card p-5 mb-6">
<div class="flex items-start justify-between gap-3"><div><h2 class="text-lg font-medium text-white">Public access checks</h2><p class="text-sm text-white/60 mt-1">Existing router forwards are shown for review. No route is changed from this screen yet.</p></div><span class="text-sm text-white/60">{{ forwards.length }} forward{{ forwards.length === 1 ? '' : 's' }}</span></div>
<div class="mt-4 grid gap-2 sm:grid-cols-2">
<div v-for="(forward, index) in forwards" :key="index" class="rounded-lg bg-white/5 p-3 text-xs text-white/70 font-mono break-all">{{ JSON.stringify(forward) }}</div>
<div v-if="!forwards.length" class="text-sm text-white/45">No router forwards reported.</div>
</div>
<p class="text-xs text-white/45 mt-4">Public forwarding, node firewall, service listener and external reachability are independent checks. A local pass does not claim public access.</p>
</section>
<section class="glass-card p-5">
<h2 class="text-lg font-medium text-white">App exposure</h2>
<p class="text-sm text-white/60 mt-1">App manifests and owned exposure rules will be added here after validation and rollback support are qualified.</p>
<div class="mt-4 rounded-lg border border-white/10 bg-white/5 p-3 text-sm text-white/60">No changes are available yet. Existing app access remains untouched.</div>
</section>
</main>
</template>