|
1 | 1 | // The HTTP half of the DoH resolver. |
| 2 | +import http from "node:http"; |
2 | 3 | import test from "node:test"; |
3 | 4 | import assert from "node:assert/strict"; |
4 | 5 |
|
@@ -159,3 +160,49 @@ test("a server built with defaults actually enforces them", async (t) => { |
159 | 160 | const body = Buffer.from(await limited.arrayBuffer()); |
160 | 161 | assert.equal(body.readUInt16BE(2) & 0x000f, 5, "REFUSED once the burst is spent"); |
161 | 162 | }); |
| 163 | + |
| 164 | +test("overriding one guard keeps the guards you did not mention", async (t) => { |
| 165 | + // A caller mounting this programmatically may tune a single guard and leave |
| 166 | + // the rest at their defaults. The un-mentioned guards have to stay the safe |
| 167 | + // defaults `server.guards` reports, not silently fall to the handler's own |
| 168 | + // off-by-default null/null/0 — a resolver that says it rate limits and does |
| 169 | + // not is exactly the open resolver these guards exist to prevent. |
| 170 | + const registry = http.createServer((req, res) => { |
| 171 | + // The only thing the resolver needs from the registry here is that our |
| 172 | + // ending is ours, so a query for it reaches the handler as an answerable |
| 173 | + // name (NXDOMAIN) rather than a not-ours REFUSED, which rate limiting also |
| 174 | + // returns and would mask the very thing under test. |
| 175 | + if ((req.url || "").startsWith("/api/moshpit/tlds")) { |
| 176 | + res.writeHead(200, { "content-type": "application/json" }); |
| 177 | + res.end(JSON.stringify({ tlds: ["moshtest"], total: 1 })); |
| 178 | + } else { |
| 179 | + res.writeHead(404, { "content-type": "application/json" }); |
| 180 | + res.end("{}"); |
| 181 | + } |
| 182 | + }); |
| 183 | + await new Promise((r) => registry.listen(0, "127.0.0.1", r)); |
| 184 | + t.after(() => new Promise((r) => registry.close(r))); |
| 185 | + const registryBase = `http://127.0.0.1:${registry.address().port}`; |
| 186 | + |
| 187 | + // Override only maxResponseBytes; rateLimit is left unmentioned. |
| 188 | + const server = await createDohServer({ port: 0, registryBase, maxResponseBytes: 4096 }); |
| 189 | + t.after(() => server.close()); |
| 190 | + assert.deepEqual(server.guards.rateLimit, { perSecond: 20, burst: 40 }, |
| 191 | + "reports the default rate limit is in force"); |
| 192 | + |
| 193 | + const ask = () => fetch(`http://127.0.0.1:${server.port}${DOH_PATH}`, { |
| 194 | + method: "POST", |
| 195 | + headers: { "content-type": DNS_MESSAGE, "x-forwarded-for": "198.51.100.9" }, |
| 196 | + body: query("x.moshtest"), |
| 197 | + }); |
| 198 | + // Default burst is 40. Fire well past it at once so token refill during the |
| 199 | + // run cannot stand in for a limiter that was dropped. |
| 200 | + const rcodes = await Promise.all( |
| 201 | + Array.from({ length: 120 }, () => ask().then(async (r) => |
| 202 | + Buffer.from(await r.arrayBuffer()).readUInt16BE(2) & 0x000f)), |
| 203 | + ); |
| 204 | + assert.ok(rcodes.some((c) => c !== 5), |
| 205 | + "an ours-name below the limit is answered, not REFUSED"); |
| 206 | + assert.ok(rcodes.some((c) => c === 5), |
| 207 | + "the unmentioned default rate limit still REFUSES once the burst is spent"); |
| 208 | +}); |
0 commit comments