Diagnose network button: raw TCP probe matrix
The Komodo probe answers a specific question (can we resolve a port from Komodo's API), but leaves ambiguity when it fails: is DNS broken, is the route blocked, is the target port unreachable, or is the target refusing? A 10-second timeout looks the same for all of those. Adds a second button, "diagnose network", that fires parallel raw TCP connect attempts (no HTTP, no auth, no bytes sent) at a fixed matrix of candidate hosts against the configured Komodo port: - 127.0.0.1 (sanity — container loopback) - host.docker.internal (bridge-gateway alias) - 172.17.0.1 (raw default bridge gateway) - the KOMODO_ADDRESS host (if different) - RCON_HOST (if different) Each row shows resolved IP, outcome (connected/refused/timeout/dns-fail/ unreachable/error), elapsed ms, and detail. Together they pinpoint which network layer the traffic dies at without guessing. New helper: src/lib/server/integrations/netProbe.ts — DNS lookup + net. createConnection with explicit timeout, classifying errors by errno (ECONNREFUSED, EHOSTUNREACH, ENOTFOUND). Runs trials in parallel so the whole diagnostic completes in one probe-timeout budget. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,29 @@
|
||||
url?: string;
|
||||
}
|
||||
|
||||
type NetOutcome = 'connected' | 'refused' | 'timeout' | 'dns-fail' | 'unreachable' | 'error';
|
||||
interface NetTrial {
|
||||
label: string;
|
||||
host: string;
|
||||
port: number;
|
||||
resolvedIp: string | null;
|
||||
outcome: NetOutcome;
|
||||
elapsedMs: number;
|
||||
detail?: string;
|
||||
}
|
||||
interface NetDiagnosis {
|
||||
ok: boolean;
|
||||
config?: {
|
||||
komodoAddress: string | null;
|
||||
komodoHost: string | null;
|
||||
komodoPort: number | null;
|
||||
rconHost: string;
|
||||
probeTimeoutMs: number;
|
||||
};
|
||||
results?: NetTrial[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface Server {
|
||||
name: string;
|
||||
komodo_type: string;
|
||||
@@ -95,6 +118,8 @@
|
||||
|
||||
let komodoProbe = $state<KomodoProbeResult | null>(null);
|
||||
let probing = $state(false);
|
||||
let netDiagnosis = $state<NetDiagnosis | null>(null);
|
||||
let diagnosing = $state(false);
|
||||
|
||||
async function probeKomodo(): Promise<void> {
|
||||
if (probing || !name) return;
|
||||
@@ -123,6 +148,22 @@
|
||||
probing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function diagnoseNetwork(): Promise<void> {
|
||||
if (diagnosing || !name) return;
|
||||
diagnosing = true;
|
||||
netDiagnosis = null;
|
||||
try {
|
||||
const res = await fetch(`/api/servers/${encodeURIComponent(name)}/diagnose-network`, {
|
||||
method: 'POST'
|
||||
});
|
||||
netDiagnosis = (await res.json()) as NetDiagnosis;
|
||||
} catch (err) {
|
||||
netDiagnosis = { ok: false, error: (err as Error).message };
|
||||
} finally {
|
||||
diagnosing = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if server}
|
||||
@@ -191,6 +232,52 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="probe-row">
|
||||
<button class="probe-btn" onclick={diagnoseNetwork} disabled={diagnosing}>
|
||||
{diagnosing ? 'diagnosing…' : 'diagnose network'}
|
||||
</button>
|
||||
<span class="probe-hint">Raw TCP reachability probe across candidate hosts.</span>
|
||||
</div>
|
||||
|
||||
{#if netDiagnosis}
|
||||
{#if netDiagnosis.ok && netDiagnosis.results}
|
||||
<table class="net-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>trial</th>
|
||||
<th>resolved</th>
|
||||
<th>outcome</th>
|
||||
<th class="num">elapsed</th>
|
||||
<th>detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each netDiagnosis.results as r (r.label)}
|
||||
<tr>
|
||||
<td class="mono">{r.label}</td>
|
||||
<td class="mono">{r.resolvedIp ?? '—'}</td>
|
||||
<td><span class="net-tag net-{r.outcome}">{r.outcome}</span></td>
|
||||
<td class="mono num">{r.elapsedMs}ms</td>
|
||||
<td class="mono detail-cell">{r.detail ?? ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{#if netDiagnosis.config}
|
||||
<div class="net-meta">
|
||||
KOMODO_ADDRESS = <code>{netDiagnosis.config.komodoAddress ?? '(unset)'}</code>
|
||||
· RCON_HOST = <code>{netDiagnosis.config.rconHost}</code>
|
||||
· probe timeout = <code>{netDiagnosis.config.probeTimeoutMs}ms</code>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="probe-result probe-bad">
|
||||
<span class="probe-tag bad">error</span>
|
||||
<span class="probe-detail">{netDiagnosis.error ?? 'network diagnostic failed'}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if komodoProbe}
|
||||
{#if komodoProbe.ok}
|
||||
<div class="probe-result probe-ok">
|
||||
@@ -536,6 +623,84 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.net-table {
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background: var(--cherry-975);
|
||||
border: 1px solid var(--cherry-700);
|
||||
border-radius: var(--r-sm);
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
}
|
||||
.net-table thead th {
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--ink-500);
|
||||
border-bottom: 1px solid var(--cherry-700);
|
||||
background: var(--cherry-925);
|
||||
}
|
||||
.net-table tbody td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid rgba(74, 15, 30, 0.4);
|
||||
color: var(--ink-100);
|
||||
vertical-align: top;
|
||||
}
|
||||
.net-table tbody tr:last-child td { border-bottom: none; }
|
||||
.net-table .mono { font-family: var(--font-mono); }
|
||||
.net-table .num { text-align: right; }
|
||||
.net-table .detail-cell {
|
||||
color: var(--ink-300);
|
||||
font-size: 11px;
|
||||
word-break: break-word;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.net-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--r-sm);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.net-connected {
|
||||
background: rgba(34, 224, 255, 0.15);
|
||||
border-color: rgba(34, 224, 255, 0.4);
|
||||
color: var(--spark-500);
|
||||
}
|
||||
.net-refused {
|
||||
background: rgba(255, 176, 32, 0.15);
|
||||
border-color: rgba(255, 176, 32, 0.4);
|
||||
color: var(--amber-500);
|
||||
}
|
||||
.net-timeout, .net-unreachable, .net-dns-fail, .net-error {
|
||||
background: rgba(255, 48, 80, 0.15);
|
||||
border-color: rgba(255, 48, 80, 0.4);
|
||||
color: var(--danger-500);
|
||||
}
|
||||
|
||||
.net-meta {
|
||||
margin-top: 10px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--ink-500);
|
||||
}
|
||||
.net-meta code {
|
||||
color: var(--cream);
|
||||
background: var(--cherry-875);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--r-sm);
|
||||
}
|
||||
|
||||
.snippet-details {
|
||||
margin-top: 10px;
|
||||
font-family: var(--font-mono);
|
||||
|
||||
Reference in New Issue
Block a user