Files
9router/src/lib/tunnel/networkProbe.js
T
decolua bee8dad946 feat(db): migrate from lowdb to SQLite with repos pattern
- Add modular DB layer (adapters, migrations, repos, helpers)
- Replace localDb/usageDb/requestDetailsDb monoliths with repos
- Add Tailscale tunnel integration & status check API
- Add /api/cli-tools/all-statuses aggregated endpoint
- Add settingsStore (Zustand) and mitm/dbReader
- Add DB unit tests (benchmark, concurrent, migration, vs-lowdb)
2026-05-09 17:48:20 +07:00

69 lines
2.4 KiB
JavaScript

import net from "net";
import dns from "dns";
import { INTERNET_CHECK, HEALTH_CHECK } from "./tunnelConfig.js";
// Force public DNS to bypass OS negative cache (mDNSResponder holds NXDOMAIN)
const resolver = new dns.promises.Resolver();
resolver.setServers(["1.1.1.1", "1.0.0.1", "8.8.8.8"]);
export function checkInternet() {
return new Promise((resolve) => {
const socket = new net.Socket();
let done = false;
const finish = (ok) => {
if (done) return;
done = true;
try { socket.destroy(); } catch { /* ignore */ }
resolve(ok);
};
socket.setTimeout(INTERNET_CHECK.timeoutMs);
socket.once("connect", () => finish(true));
socket.once("timeout", () => finish(false));
socket.once("error", () => finish(false));
try { socket.connect(INTERNET_CHECK.port, INTERNET_CHECK.host); }
catch { finish(false); }
});
}
async function resolveDns(hostname, timeoutMs) {
// Try custom public DNS first (bypasses negative-cached NXDOMAIN on macOS).
// Fall back to OS resolver for hostnames blocked or unsupported by Cloudflare DNS
// (e.g. *.ts.net not always resolvable via 1.1.1.1).
const tryResolver = (fn) => Promise.race([
fn(),
new Promise((_, rej) => setTimeout(() => rej(new Error("dns timeout")), timeoutMs)),
]).then(() => true).catch(() => false);
if (await tryResolver(() => resolver.resolve4(hostname))) return true;
return tryResolver(() => dns.promises.resolve4(hostname));
}
// Single health probe: DNS via 1.1.1.1 → fetch /api/health
export async function probeUrlAlive(url) {
if (!url) return false;
let hostname;
try { hostname = new URL(url).hostname; } catch { return false; }
if (!await resolveDns(hostname, HEALTH_CHECK.dnsTimeoutMs)) return false;
try {
const res = await fetch(`${url}/api/health`, {
signal: AbortSignal.timeout(HEALTH_CHECK.fetchTimeoutMs),
});
return res.ok;
} catch {
return false;
}
}
// Poll until tunnel responds /api/health, or timeout. Cancellable via token.
export async function waitForHealth(url, cancelToken = { cancelled: false }) {
const start = Date.now();
while (Date.now() - start < HEALTH_CHECK.timeoutMs) {
if (cancelToken.cancelled) throw new Error("cancelled");
if (await probeUrlAlive(url)) return true;
await new Promise((r) => setTimeout(r, HEALTH_CHECK.intervalMs));
}
throw new Error(`Health check timeout after ${HEALTH_CHECK.timeoutMs}ms`);
}