diff --git a/apps/bridge/src/tunnel.ts b/apps/bridge/src/tunnel.ts index 499aff1..8243f71 100644 --- a/apps/bridge/src/tunnel.ts +++ b/apps/bridge/src/tunnel.ts @@ -96,6 +96,14 @@ const dnsResolvers = [ { name: "DNSPod", server: "119.29.29.29" }, { name: "114DNS", server: "114.114.114.114" } ]; +const minimumDnsResolversRequired = Math.ceil(dnsResolvers.length / 2); + +export function hasEnoughDnsCoverage( + results: Array<{ ok: boolean }>, + required = minimumDnsResolversRequired +): boolean { + return results.filter((result) => result.ok).length >= required; +} export async function startQuickTunnel(port: number, onLog: (line: string) => void): Promise { const bin = await requireCloudflared(); @@ -211,16 +219,22 @@ async function verifyQuickTunnel(url: string, onLog: (line: string) => void): Pr }` ); } - const failedDns = dnsResults - .map((result, index) => ({ ...result, resolver: dnsResolvers[index] })) - .filter((result) => !result.ok); - if (failedDns.length) { + const namedDnsResults = dnsResults.map((result, index) => ({ ...result, resolver: dnsResolvers[index] })); + const failedDns = namedDnsResults.filter((result) => !result.ok); + if (!hasEnoughDnsCoverage(namedDnsResults)) { throw new Error( `quick tunnel DNS is not globally ready: ${failedDns .map((result) => `${result.resolver.name}/${result.resolver.server}: ${result.detail}`) .join("; ")}` ); } + if (failedDns.length) { + onLog( + `quick tunnel DNS partial failure tolerated: ${failedDns + .map((result) => `${result.resolver.name}/${result.resolver.server}: ${result.detail}`) + .join("; ")}` + ); + } const systemDns = await resolveWithSystem(hostname); onLog( diff --git a/apps/bridge/test/tunnel.test.ts b/apps/bridge/test/tunnel.test.ts index 945116a..c68791d 100644 --- a/apps/bridge/test/tunnel.test.ts +++ b/apps/bridge/test/tunnel.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { parseTryCloudflareUrl } from "../src/tunnel.js"; +import { hasEnoughDnsCoverage, parseTryCloudflareUrl } from "../src/tunnel.js"; describe("parseTryCloudflareUrl", () => { it("extracts a Quick Tunnel URL from cloudflared logs", () => { @@ -9,3 +9,17 @@ describe("parseTryCloudflareUrl", () => { ); }); }); + +describe("hasEnoughDnsCoverage", () => { + it("accepts a majority of direct public resolver checks", () => { + expect( + hasEnoughDnsCoverage([{ ok: false }, { ok: true }, { ok: true }, { ok: true }, { ok: true }]) + ).toBe(true); + }); + + it("rejects when most direct public resolver checks fail", () => { + expect( + hasEnoughDnsCoverage([{ ok: false }, { ok: true }, { ok: false }, { ok: true }, { ok: false }]) + ).toBe(false); + }); +});