diff --git a/frontend/components/DnsLeaksTest.vue b/frontend/components/DnsLeaksTest.vue index 58c9aedb..cb47b4e6 100644 --- a/frontend/components/DnsLeaksTest.vue +++ b/frontend/components/DnsLeaksTest.vue @@ -107,13 +107,13 @@ import FitText from '@/components/widgets/FitText.vue'; import InfoBanner from '@/components/widgets/InfoBanner.vue'; import { INLINE_TIERS } from '@/composables/use-fit-text.js'; import { - ipApi, bashws, myipstack, fastly, browserleaks, surfshark, ipleak, + ipApi, bashws, myipstack, fastly, browserleaks, surfshark, ipleak, every1dns, createDnsLeakRunner, } from '@/utils/dnsleaks'; // All providers in priority order: the first SLOT_COUNT hold a card each, // the rest are standbys. New provider = file under utils/dnsleaks + append. -const PROVIDERS = [ipApi, surfshark, myipstack, fastly, bashws, browserleaks, ipleak]; +const PROVIDERS = [ipApi, every1dns, myipstack, fastly, surfshark, bashws, browserleaks, ipleak]; const SLOT_COUNT = 4; const ACTIVE = PROVIDERS.slice(0, SLOT_COUNT); diff --git a/frontend/utils/dnsleaks/every1dns.js b/frontend/utils/dnsleaks/every1dns.js new file mode 100644 index 00000000..90583241 --- /dev/null +++ b/frontend/utils/dnsleaks/every1dns.js @@ -0,0 +1,20 @@ +// every1dns.net — any fresh label on `.map.help.every1dns.net` forces a +// unique DNS lookup (the record is keyed by that label); the response echoes +// the resolver as `{ ip, protocol, dnssec, isp: { asn, name }, … }`. Only +// `ip` is used — the homepage card re-does geo via MaxMind. +import { fetchWithTimeout } from '../fetch-with-timeout.js'; + +const HOST_SUFFIX = '.map.help.every1dns.net'; + +export const every1dns = { + id: 'every1dns', + name: 'every1dns.net', + async run() { + const host = `${crypto.randomUUID()}${HOST_SUFFIX}`; + const response = await fetchWithTimeout(`https://${host}/`); + if (!response.ok) throw new Error('every1dns: response not ok'); + const data = await response.json(); + if (typeof data?.ip !== 'string') throw new Error('every1dns: missing ip'); + return { ip: data.ip }; + }, +}; diff --git a/frontend/utils/dnsleaks/index.js b/frontend/utils/dnsleaks/index.js index 2aa24de0..c9bd92f3 100644 --- a/frontend/utils/dnsleaks/index.js +++ b/frontend/utils/dnsleaks/index.js @@ -16,6 +16,7 @@ export { browserleaks } from './browserleaks.js'; export { fastly } from './fastly.js'; export { bashws } from './bashws.js'; export { myipstack } from './myipstack.js'; +export { every1dns } from './every1dns.js'; // Invoke `provider.run()` up to `attempts` times. Because each provider // regenerates its prefix internally on every call, retries trigger fresh diff --git a/tests/dnsleaks-every1dns.test.js b/tests/dnsleaks-every1dns.test.js new file mode 100644 index 00000000..375ac1fd --- /dev/null +++ b/tests/dnsleaks-every1dns.test.js @@ -0,0 +1,33 @@ +// Exercises the every1dns.net provider without contacting the upstream. +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { every1dns } from '../frontend/utils/dnsleaks/every1dns.js'; + +const SAMPLE = { + ip: '13.229.187.210', ip_version: 1, protocol: 'udp', dnssec: true, edns: 0, + client_subnet: -1, qname_minimization: true, isp: { asn: 16509, name: 'Amazon.com, Inc.' }, +}; + +describe('every1dns.net provider', () => { + it('probes a fresh UUID label per run and returns the resolver ip', async (t) => { + const urls = []; + t.mock.method(globalThis, 'fetch', async (url) => { + urls.push(url); + // Upstream answers text/plain with a JSON body. + return new Response(JSON.stringify(SAMPLE), { headers: { 'content-type': 'text/plain' } }); + }); + assert.deepEqual(await every1dns.run(), { ip: '13.229.187.210' }); + assert.deepEqual(await every1dns.run(), { ip: '13.229.187.210' }); + const label = /^https:\/\/([0-9a-f-]{36})\.map\.help\.every1dns\.net\/$/; + assert.match(urls[0], label); + assert.match(urls[1], label); + assert.notEqual(urls[0], urls[1]); + }); + + it('throws on a non-ok response or a body without ip', async (t) => { + const fetchMock = t.mock.method(globalThis, 'fetch', async () => new Response('', { status: 502 })); + await assert.rejects(every1dns.run(), /every1dns: response not ok/); + fetchMock.mock.mockImplementation(async () => Response.json({ isp: SAMPLE.isp })); + await assert.rejects(every1dns.run(), /every1dns: missing ip/); + }); +}); diff --git a/tests/dnsleaks-run.test.js b/tests/dnsleaks-run.test.js index 0d0747cd..ce94b511 100644 --- a/tests/dnsleaks-run.test.js +++ b/tests/dnsleaks-run.test.js @@ -18,7 +18,7 @@ const flush = () => new Promise(resolve => setImmediate(resolve)); const fixture = (lookupMaxmind = async () => null) => { const events = []; const timers = []; - const providers = Object.fromEntries(['ipApi', 'surfshark', 'myipstack', 'fastly', 'bashws', 'browserleaks', 'ipleak'] + const providers = Object.fromEntries(['ipApi', 'surfshark', 'myipstack', 'fastly', 'bashws', 'browserleaks', 'ipleak', 'every1dns'] .map(id => [id, { id, name: id, run: async () => ({ ip: '192.0.2.53' }) }])); const bindings = { computed, reactive, ref, createDnsLeakRunner, ...providers, @@ -65,6 +65,7 @@ test('cards share one standby request per run and refresh obtains a fresh result assert.equal(run.events.length, attempt); assert.equal(calls.browserleaks, 0); assert.equal(calls.ipleak, 0); + assert.equal(calls.every1dns, 0); } });