Skip to content

Commit 668f171

Browse files
feat: Deduplicate DNS records in resolveAll
Co-authored-by: jake <jake@jarv.is>
1 parent fcd47af commit 668f171

File tree

2 files changed

+60
-5
lines changed

2 files changed

+60
-5
lines changed

server/services/dns.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,34 @@ function dohAnswer(
4545
}
4646

4747
describe("resolveAll", () => {
48+
it("deduplicates records when DoH provider returns duplicates", async () => {
49+
const { resolveAll } = await import("./dns");
50+
// Simulate a DoH provider returning duplicate A records (same IP twice)
51+
const fetchMock = vi
52+
.spyOn(global, "fetch")
53+
.mockResolvedValueOnce(
54+
dohAnswer([
55+
{ name: "example.com.", TTL: 60, data: "1.2.3.4" },
56+
{ name: "example.com.", TTL: 60, data: "1.2.3.4" }, // duplicate!
57+
{ name: "example.com.", TTL: 60, data: "5.6.7.8" },
58+
]),
59+
)
60+
.mockResolvedValueOnce(dohAnswer([])) // AAAA
61+
.mockResolvedValueOnce(dohAnswer([])) // MX
62+
.mockResolvedValueOnce(dohAnswer([])) // TXT
63+
.mockResolvedValueOnce(dohAnswer([])); // NS
64+
65+
const out = await resolveAll("example.com");
66+
const aRecords = out.records.filter((r) => r.type === "A");
67+
68+
// Should have exactly 2 A records (duplicate removed), not 3
69+
expect(aRecords).toHaveLength(2);
70+
expect(aRecords[0]?.value).toBe("1.2.3.4");
71+
expect(aRecords[1]?.value).toBe("5.6.7.8");
72+
73+
fetchMock.mockRestore();
74+
});
75+
4876
it("normalizes records and returns combined results", async () => {
4977
const { resolveAll } = await import("./dns");
5078
// The code calls DoH for A, AAAA, MX, TXT, NS in parallel and across providers; we just return A for both A and AAAA etc.

server/services/dns.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,12 @@ export async function resolveAll(domain: string): Promise<DnsResolveResult> {
251251
isCloudflare: r.isCloudflare ?? undefined,
252252
})),
253253
);
254-
const merged = sortDnsRecordsByType(
255-
[...cachedFresh, ...fetchedStale],
256-
types,
257-
);
254+
// Deduplicate before sorting to prevent duplicates in merged results
255+
const deduplicated = deduplicateDnsRecords([
256+
...cachedFresh,
257+
...fetchedStale,
258+
]);
259+
const merged = sortDnsRecordsByType(deduplicated, types);
258260
const counts = (types as DnsType[]).reduce(
259261
(acc, t) => {
260262
acc[t] = merged.filter((r) => r.type === t).length;
@@ -370,8 +372,10 @@ export async function resolveAll(domain: string): Promise<DnsResolveResult> {
370372
console.info(
371373
`[dns] ok ${registrable} counts=${JSON.stringify(counts)} resolver=${resolverUsed} durations=${JSON.stringify(durationByProvider)}`,
372374
);
375+
// Deduplicate records before returning (same logic as replaceDns uses for DB persistence)
376+
const deduplicated = deduplicateDnsRecords(flat);
373377
// Sort records deterministically to match cache-path ordering
374-
const sorted = sortDnsRecordsByType(flat, types);
378+
const sorted = sortDnsRecordsByType(deduplicated, types);
375379
return { records: sorted, resolver: resolverUsed } as DnsResolveResult;
376380
} catch (err) {
377381
console.warn(
@@ -466,6 +470,29 @@ function trimQuotes(s: string) {
466470
return s.replace(/^"|"$/g, "");
467471
}
468472

473+
/**
474+
* Deduplicate DNS records using the same logic as replaceDns.
475+
* Records are considered duplicates if they have the same type, name, value, and priority.
476+
* Case-insensitive comparison for name and value.
477+
*/
478+
function deduplicateDnsRecords(records: DnsRecord[]): DnsRecord[] {
479+
const seen = new Set<string>();
480+
const deduplicated: DnsRecord[] = [];
481+
482+
for (const r of records) {
483+
// Use case-insensitive comparison, same as replaceDns
484+
const priorityPart = r.priority != null ? `|${r.priority}` : "";
485+
const key = `${r.type}|${r.name.trim().toLowerCase()}|${r.value.trim().toLowerCase()}${priorityPart}`;
486+
487+
if (!seen.has(key)) {
488+
seen.add(key);
489+
deduplicated.push(r);
490+
}
491+
}
492+
493+
return deduplicated;
494+
}
495+
469496
function sortDnsRecordsByType(
470497
records: DnsRecord[],
471498
order: readonly DnsType[],

0 commit comments

Comments
 (0)