From 04091593270cdcec1f985edebc56953284751dd3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 21:22:06 +0000 Subject: [PATCH] Make mesh verification universal (CIDR check, not NetBird-specific) Replace the NetBird-adapter-based "reachable" check with a vendor-agnostic one: the admin supplies the mesh's IP range (CIDR), and verification just confirms this host has an address inside it. Works identically for NetBird, WireGuard, ZeroTier, Tailscale, or any other mesh tech, with no integration record or vendor API call required. --- backend/src/routes/system.ts | 80 +++++++++++++++++++----------------- src/lib/api.ts | 6 +-- src/pages/MeshGate.tsx | 28 +++++-------- 3 files changed, 55 insertions(+), 59 deletions(-) diff --git a/backend/src/routes/system.ts b/backend/src/routes/system.ts index e357f2a..f2e9fa7 100644 --- a/backend/src/routes/system.ts +++ b/backend/src/routes/system.ts @@ -1,27 +1,34 @@ import type { FastifyInstance } from 'fastify' import { networkInterfaces } from 'node:os' import { z } from 'zod' -import { db, getConfig, setConfig, logEvent } from '../db/index.js' -import { loadSecrets } from '../db/secrets.js' -import { adapterRegistry } from '../integrations/registry.js' -import type { IntegrationType } from '../integrations/types.js' +import { getConfig, setConfig, logEvent } from '../db/index.js' -interface IntegrationRow { - id: number - type: string - name: string - config_json: string +/** Parses "a.b.c.d/n" into a 32-bit base int + prefix length. */ +function parseCidr(cidr: string): { base: number; prefix: number } | null { + const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(cidr.trim()) + if (!match) return null + const octets = match.slice(1, 5).map(Number) + const prefix = Number(match[5]) + if (octets.some((o) => o < 0 || o > 255) || prefix < 0 || prefix > 32) return null + const base = (octets[0]! << 24) | (octets[1]! << 16) | (octets[2]! << 8) | octets[3]! + return { base, prefix } } -/** NetBird's default CGNAT mesh range (100.64.0.0/10) — informational only (DECIDE B2). */ -function detectHostMeshIp(): string | null { +function ipToInt(ip: string): number | null { + const parts = ip.split('.').map(Number) + if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return null + return (parts[0]! << 24) | (parts[1]! << 16) | (parts[2]! << 8) | parts[3]! +} + +/** Finds this host's own IPv4 address that falls within the given mesh CIDR, if any. */ +function findHostIpInCidr(cidr: { base: number; prefix: number }): string | null { + const mask = cidr.prefix === 0 ? 0 : (-1 << (32 - cidr.prefix)) >>> 0 for (const addrs of Object.values(networkInterfaces())) { for (const addr of addrs ?? []) { if (addr.family !== 'IPv4') continue - const parts = addr.address.split('.').map(Number) - if (parts[0] === 100 && parts[1] !== undefined && parts[1] >= 64 && parts[1] <= 127) { - return addr.address - } + const ipInt = ipToInt(addr.address) + if (ipInt === null) continue + if (((ipInt & mask) >>> 0) === ((cidr.base & mask) >>> 0)) return addr.address } } return null @@ -29,56 +36,53 @@ function detectHostMeshIp(): string | null { function meshStatusPayload() { const required = getConfig('mesh.required') === 'true' - const meshIntegrationId = getConfig('mesh.integrationId') + const cidr = getConfig('mesh.cidr') ?? null const verifiedAt = getConfig('mesh.verifiedAt') const overridden = getConfig('mesh.overrideUntil') === 'permanent' + const parsed = cidr ? parseCidr(cidr) : null return { required, verified: !!verifiedAt, verifiedAt: verifiedAt ?? null, overridden, - meshIntegrationId: meshIntegrationId ? Number(meshIntegrationId) : null, - hostMeshIp: detectHostMeshIp(), + cidr, + hostMeshIp: parsed ? findHostIpInCidr(parsed) : null, } } -const verifySchema = z.object({ integrationId: z.number().int() }) +const verifySchema = z.object({ cidr: z.string().min(1) }) export async function systemRoutes(app: FastifyInstance) { app.get('/api/system/mesh-status', { onRequest: [app.authenticate] }, async () => { return meshStatusPayload() }) + // Verification is mesh-tech-agnostic: any overlay network (NetBird, WireGuard, + // ZeroTier, Tailscale...) assigns this host an IP in its own range. We just check + // the host has an address inside the admin-supplied CIDR — no vendor API needed. app.post('/api/system/mesh/verify', { onRequest: [app.requireAdmin] }, async (req, reply) => { const parsed = verifySchema.safeParse(req.body) if (!parsed.success) { return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' }) } - const row = db.prepare('SELECT * FROM integrations WHERE id = ? AND type = ?').get( - parsed.data.integrationId, - 'netbird' - ) as IntegrationRow | undefined - if (!row) return reply.code(404).send({ error: 'NetBird integration not found' }) + const cidr = parseCidr(parsed.data.cidr) + if (!cidr) return reply.code(400).send({ error: 'Invalid CIDR — expected format like 100.64.0.0/10' }) - const adapter = adapterRegistry[row.type as IntegrationType] - const config = JSON.parse(row.config_json) - const secrets = loadSecrets(row.id) - const result = await adapter.testConnection(config, secrets) + const hostMeshIp = findHostIpInCidr(cidr) + setConfig('mesh.cidr', parsed.data.cidr) - db.prepare("UPDATE integrations SET status = ?, last_checked_at = datetime('now') WHERE id = ?").run( - result.ok ? 'connected' : 'error', - row.id - ) - - if (result.ok) { - setConfig('mesh.integrationId', String(row.id)) + if (hostMeshIp) { setConfig('mesh.verifiedAt', new Date().toISOString()) - logEvent('mesh_verified', `Mesh verified via ${row.name}`, row.type) + logEvent('mesh_verified', `Mesh verified — host has ${hostMeshIp} in ${parsed.data.cidr}`) } else { - logEvent('mesh_verify_failed', `Mesh verification failed via ${row.name}: ${result.message}`, row.type) + logEvent('mesh_verify_failed', `Mesh verification failed — no host IP found in ${parsed.data.cidr}`) } - return { ...result, hostMeshIp: detectHostMeshIp() } + return { + ok: !!hostMeshIp, + message: hostMeshIp ? `Host is on the mesh (${hostMeshIp})` : 'No host IP found in that range', + hostMeshIp, + } }) app.post('/api/system/mesh/override', { onRequest: [app.requireAdmin] }, async (req) => { diff --git a/src/lib/api.ts b/src/lib/api.ts index 1e4242d..5985007 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -59,10 +59,10 @@ export const api = { listLoginEvents: (limit = 20) => apiFetch<{ events: LoginEvent[] }>(`/auth/login-events?limit=${limit}`), getMeshStatus: () => apiFetch('/system/mesh-status'), - verifyMesh: (integrationId: number) => + verifyMesh: (cidr: string) => apiFetch<{ ok: boolean; message: string; hostMeshIp: string | null }>('/system/mesh/verify', { method: 'POST', - body: JSON.stringify({ integrationId }), + body: JSON.stringify({ cidr }), }), overrideMesh: () => apiFetch('/system/mesh/override', { method: 'POST' }), setMeshRequired: (required: boolean) => @@ -252,7 +252,7 @@ export interface MeshStatus { verified: boolean verifiedAt: string | null overridden: boolean - meshIntegrationId: number | null + cidr: string | null hostMeshIp: string | null } diff --git a/src/pages/MeshGate.tsx b/src/pages/MeshGate.tsx index 1281fb7..7bd0e4f 100644 --- a/src/pages/MeshGate.tsx +++ b/src/pages/MeshGate.tsx @@ -43,8 +43,7 @@ const goldButton: React.CSSProperties = { export default function MeshGate() { const { refreshMeshStatus } = useAuth() - const [baseUrl, setBaseUrl] = useState('') - const [apiKey, setApiKey] = useState('') + const [cidr, setCidr] = useState('') const [error, setError] = useState(null) const [testResult, setTestResult] = useState<{ ok: boolean; message: string; hostMeshIp: string | null } | null>(null) const [submitting, setSubmitting] = useState(false) @@ -56,13 +55,7 @@ export default function MeshGate() { setTestResult(null) setSubmitting(true) try { - const { integration } = await api.createIntegration({ - type: 'netbird', - name: 'NetBird', - config: baseUrl ? { baseUrl } : {}, - secrets: apiKey ? { apiKey } : {}, - }) - const result = await api.verifyMesh(integration.id) + const result = await api.verifyMesh(cidr) setTestResult(result) if (result.ok) await refreshMeshStatus() } catch (err) { @@ -103,21 +96,20 @@ export default function MeshGate() {

- ArchNest expects a verified NetBird mesh before the rest of the app can be configured. - Connect your mesh below to continue. + ArchNest expects this host to be on a private mesh network before the rest of the app + can be configured. Works with any mesh — NetBird, WireGuard, ZeroTier, Tailscale, etc. + Enter the mesh's IP range below; we verify by checking this host has an address in it.

- + setBaseUrl(e.target.value)} - placeholder="https://api.netbird.io" + value={cidr} + onChange={(e) => setCidr(e.target.value)} + placeholder="e.g. 100.64.0.0/10 (NetBird/Tailscale CGNAT range)" + required /> - - setApiKey(e.target.value)} required /> - {error &&

{error}

} {testResult && (