Skip to content

Commit f53af5a

Browse files
committed
filter senders, add timeout, 16-bit txn ids
1 parent 6d05ac4 commit f53af5a

2 files changed

Lines changed: 121 additions & 9 deletions

File tree

client/udp.js

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
const udp = require('dgram');
2+
const net = require('net');
3+
const crypto = require('crypto');
24
const Packet = require('../packet');
3-
const { equal } = require('assert');
45
const { debuglog } = require('util');
56

67
const debug = debuglog('dns2');
78

8-
module.exports = ({ dns = '8.8.8.8', port = 53, socketType = 'udp4' } = {}) => {
9+
module.exports = ({
10+
dns = '8.8.8.8',
11+
port = 53,
12+
socketType = 'udp4',
13+
timeout = 10000,
14+
} = {}) => {
915
return (name, type = 'A', cls = Packet.CLASS.IN, options = {}) => {
1016
const { clientIp, recursive = true } = options;
1117
const query = new Packet();
12-
query.header.id = (Math.random() * 1e4) | 0;
18+
query.header.id = crypto.randomInt(0x10000);
1319
// see https://github.com/song940/node-dns/issues/29
1420
if (recursive) {
1521
query.header.rd = 1;
@@ -25,15 +31,66 @@ module.exports = ({ dns = '8.8.8.8', port = 53, socketType = 'udp4' } = {}) => {
2531
type : Packet.TYPE[type],
2632
});
2733
const client = new udp.Socket(socketType);
34+
// Only enforce a strict source-address check when `dns` is an IP literal;
35+
// hostnames would require an extra resolve to compare against.
36+
const expectedAddress = net.isIP(dns) ? dns : null;
2837
return new Promise((resolve, reject) => {
29-
client.once('message', function onMessage(message) {
30-
client.close();
31-
const response = Packet.parse(message);
32-
equal(response.header.id, query.header.id);
38+
let settled = false;
39+
let timer;
40+
const cleanup = () => {
41+
if (settled) return;
42+
settled = true;
43+
if (timer) clearTimeout(timer);
44+
client.removeListener('message', onMessage);
45+
client.removeListener('error', onError);
46+
try { client.close(); } catch (_) { /* already closed */ }
47+
};
48+
function onMessage(message, rinfo) {
49+
// Drop packets that didn't come from the configured resolver.
50+
if (rinfo.port !== port || (expectedAddress && rinfo.address !== expectedAddress)) {
51+
debug('udp: dropping packet from unexpected sender %s:%d', rinfo.address, rinfo.port);
52+
return;
53+
}
54+
let response;
55+
try {
56+
response = Packet.parse(message);
57+
} catch (e) {
58+
debug('udp: dropping unparseable packet: %s', e.message);
59+
return;
60+
}
61+
// Stray / late reply from a reused ephemeral port — keep listening.
62+
if (response.header.id !== query.header.id) {
63+
debug('udp: dropping response with mismatched id %d (expected %d)',
64+
response.header.id, query.header.id);
65+
return;
66+
}
67+
cleanup();
3368
resolve(response);
34-
});
69+
}
70+
function onError(err) {
71+
cleanup();
72+
reject(err);
73+
}
74+
client.on('message', onMessage);
75+
client.on('error', onError);
76+
77+
if (timeout > 0) {
78+
timer = setTimeout(() => {
79+
cleanup();
80+
const err = new Error(`DNS query timed out after ${timeout}ms`);
81+
err.code = 'ETIMEDOUT';
82+
reject(err);
83+
}, timeout);
84+
timer.unref();
85+
}
86+
3587
debug('send', dns, query.toBuffer());
36-
client.send(query.toBuffer(), port, dns, err => err && reject(err));
88+
client.send(query.toBuffer(), port, dns, err => {
89+
if (err) {
90+
cleanup();
91+
reject(err);
92+
}
93+
});
3794
});
3895
};
3996
};

test/index.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,61 @@ test('server/udp-tcp#simple-request-async-response', async() => {
401401
await server.close();
402402
});
403403

404+
test('client/udp ignores stray response and resolves on matching id', async() => {
405+
// Simulate the scenario from upstream issue #100: a stray UDP packet (e.g.
406+
// late reply on a reused ephemeral port) arrives before the real response.
407+
// The client must drop it and keep listening rather than asserting/crashing.
408+
const server = udp.createSocket('udp4');
409+
await new Promise(resolve => server.bind(0, '127.0.0.1', resolve));
410+
const { port: serverPort } = server.address();
411+
412+
server.on('message', (msg, rinfo) => {
413+
const request = Packet.parse(msg);
414+
415+
// Stray packet: same socket, but a different (wrong) transaction id.
416+
const stray = new Packet();
417+
stray.header.id = (request.header.id + 1) & 0xffff;
418+
stray.header.qr = 1;
419+
server.send(stray.toBuffer(), rinfo.port, rinfo.address);
420+
421+
// Real reply, slightly delayed so the stray definitely lands first.
422+
const response = Packet.createResponseFromRequest(request);
423+
response.answers.push({
424+
name : request.questions[0].name,
425+
type : Packet.TYPE.A,
426+
class : Packet.CLASS.IN,
427+
ttl : 300,
428+
address : '1.2.3.4',
429+
});
430+
setTimeout(() => server.send(response.toBuffer(), rinfo.port, rinfo.address), 5);
431+
});
432+
433+
const query = UDPClient({ dns: '127.0.0.1', port: serverPort, timeout: 2000 });
434+
const reply = await query('stray.test');
435+
assert.equal(reply.answers.length, 1);
436+
assert.equal(reply.answers[0].address, '1.2.3.4');
437+
await new Promise(resolve => server.close(resolve));
438+
});
439+
440+
test('client/udp times out when no matching response arrives', async() => {
441+
// Server replies with only stray packets; client must time out, not hang.
442+
const server = udp.createSocket('udp4');
443+
await new Promise(resolve => server.bind(0, '127.0.0.1', resolve));
444+
const { port: serverPort } = server.address();
445+
446+
server.on('message', (msg, rinfo) => {
447+
const request = Packet.parse(msg);
448+
const stray = new Packet();
449+
stray.header.id = (request.header.id + 1) & 0xffff;
450+
stray.header.qr = 1;
451+
server.send(stray.toBuffer(), rinfo.port, rinfo.address);
452+
});
453+
454+
const query = UDPClient({ dns: '127.0.0.1', port: serverPort, timeout: 100 });
455+
await assert.rejects(query('timeout.test'), err => err.code === 'ETIMEDOUT');
456+
await new Promise(resolve => server.close(resolve));
457+
});
458+
404459
test('server/all#invalid-request', async() => {
405460
const server = createServer({
406461
doh : true,

0 commit comments

Comments
 (0)