From 03cad6f6864c1a5415045c92ffaa005a538b8074 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Mon, 25 May 2026 15:55:51 -0700 Subject: [PATCH] feat(server): PROXY protocol v1/v2 support --- lib/proxy-protocol.js | 153 ++++++++++++++++++++++++++ server/tcp.js | 71 ++++++++++++ server/udp.js | 28 ++++- test/proxy-protocol.js | 119 ++++++++++++++++++++ test/server.js | 240 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 609 insertions(+), 2 deletions(-) create mode 100644 lib/proxy-protocol.js create mode 100644 test/proxy-protocol.js diff --git a/lib/proxy-protocol.js b/lib/proxy-protocol.js new file mode 100644 index 0000000..594f73f --- /dev/null +++ b/lib/proxy-protocol.js @@ -0,0 +1,153 @@ +'use strict'; + +// PROXY protocol parser (HAProxy/Nginx). +// Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt +// +// parse(buffer) returns: +// - { header, headerLength } when a complete header is at the start of buffer +// - null when the buffer is a valid prefix but more bytes are needed +// and throws when the bytes are not a valid PROXY header. + +const V2_SIGNATURE = Buffer.from([ + 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, + 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, +]); +const V1_PREFIX = Buffer.from('PROXY '); +const V1_MAX_LEN = 108; + +const FAMILY = { 0x10: 'IPv4', 0x20: 'IPv6', 0x30: 'Unix' }; +const TRANSPORT = { 0x01: 'STREAM', 0x02: 'DGRAM' }; + +function parse(buffer) { + if (buffer.length >= 12) { + if (buffer.slice(0, 12).equals(V2_SIGNATURE)) return parseV2(buffer); + } else if (V2_SIGNATURE.slice(0, buffer.length).equals(buffer)) { + return null; + } + + if (buffer.length >= 6) { + if (buffer.slice(0, 6).equals(V1_PREFIX)) return parseV1(buffer); + } else if (V1_PREFIX.slice(0, buffer.length).equals(buffer)) { + return null; + } + + throw new Error('PROXY protocol: header missing or malformed'); +} + +function parseV1(buffer) { + const search = buffer.slice(0, Math.min(buffer.length, V1_MAX_LEN)); + const newline = search.indexOf('\r\n'); + if (newline === -1) { + if (buffer.length >= V1_MAX_LEN) { + throw new Error('PROXY v1: header exceeds maximum length'); + } + return null; + } + const line = buffer.slice(0, newline).toString('ascii'); + const parts = line.split(' '); + if (parts[0] !== 'PROXY') throw new Error('PROXY v1: malformed header'); + const headerLength = newline + 2; + + if (parts[1] === 'UNKNOWN') { + return { header: { version: 1, command: 'UNKNOWN' }, headerLength }; + } + if (parts.length !== 6) throw new Error('PROXY v1: malformed header'); + const [ , proto, sourceAddress, destinationAddress, srcPort, dstPort ] = parts; + if (proto !== 'TCP4' && proto !== 'TCP6') { + throw new Error(`PROXY v1: unsupported protocol ${proto}`); + } + return { + header: { + version : 1, + command : 'PROXY', + family : proto === 'TCP4' ? 'IPv4' : 'IPv6', + transport : 'STREAM', + sourceAddress, + sourcePort : parseInt(srcPort, 10), + destinationAddress, + destinationPort : parseInt(dstPort, 10), + }, + headerLength, + }; +} + +function parseV2(buffer) { + if (buffer.length < 16) return null; + const verCmd = buffer[12]; + const version = verCmd >> 4; + const command = verCmd & 0x0F; + if (version !== 2) throw new Error(`PROXY v2: unsupported version ${version}`); + if (command !== 0 && command !== 1) { + throw new Error(`PROXY v2: unknown command ${command}`); + } + + const famProto = buffer[13]; + const addressLength = buffer.readUInt16BE(14); + const headerLength = 16 + addressLength; + if (buffer.length < headerLength) return null; + + if (command === 0) { + // LOCAL — no real client info (e.g. proxy-originated health check). + return { header: { version: 2, command: 'LOCAL' }, headerLength }; + } + + const family = FAMILY[famProto & 0xF0]; + const transport = TRANSPORT[famProto & 0x0F]; + + let sourceAddress, destinationAddress, sourcePort, destinationPort; + if (family === 'IPv4' && addressLength >= 12) { + sourceAddress = `${buffer[16]}.${buffer[17]}.${buffer[18]}.${buffer[19]}`; + destinationAddress = `${buffer[20]}.${buffer[21]}.${buffer[22]}.${buffer[23]}`; + sourcePort = buffer.readUInt16BE(24); + destinationPort = buffer.readUInt16BE(26); + } else if (family === 'IPv6' && addressLength >= 36) { + sourceAddress = ipv6FromBytes(buffer.slice(16, 32)); + destinationAddress = ipv6FromBytes(buffer.slice(32, 48)); + sourcePort = buffer.readUInt16BE(48); + destinationPort = buffer.readUInt16BE(50); + } else { + throw new Error(`PROXY v2: unsupported address family/protocol 0x${famProto.toString(16)}`); + } + + return { + header: { + version : 2, + command : 'PROXY', + family, + transport, + sourceAddress, + sourcePort, + destinationAddress, + destinationPort, + }, + headerLength, + }; +} + +function ipv6FromBytes(bytes) { + const segments = []; + for (let i = 0; i < 16; i += 2) { + segments.push(bytes.readUInt16BE(i).toString(16)); + } + return segments.join(':'); +} + +// Test helpers — build wire-format headers used by tests and example code. +function buildV1({ family = 'TCP4', sourceAddress, destinationAddress, sourcePort, destinationPort }) { + return Buffer.from(`PROXY ${family} ${sourceAddress} ${destinationAddress} ${sourcePort} ${destinationPort}\r\n`, 'ascii'); +} + +function buildV2Ipv4({ sourceAddress, destinationAddress, sourcePort, destinationPort, transport = 'STREAM' }) { + const buf = Buffer.alloc(16 + 12); + V2_SIGNATURE.copy(buf, 0); + buf[12] = 0x21; // version 2 | PROXY command + buf[13] = 0x10 | (transport === 'DGRAM' ? 0x02 : 0x01); // IPv4 | STREAM/DGRAM + buf.writeUInt16BE(12, 14); + sourceAddress.split('.').forEach((o, i) => { buf[16 + i] = parseInt(o, 10); }); + destinationAddress.split('.').forEach((o, i) => { buf[20 + i] = parseInt(o, 10); }); + buf.writeUInt16BE(sourcePort, 24); + buf.writeUInt16BE(destinationPort, 26); + return buf; +} + +module.exports = { parse, parseV1, parseV2, buildV1, buildV2Ipv4 }; diff --git a/server/tcp.js b/server/tcp.js index 2c83cd0..098ef51 100644 --- a/server/tcp.js +++ b/server/tcp.js @@ -1,17 +1,31 @@ const tcp = require('node:net'); const Packet = require('../packet'); +const proxyProtocol = require('../lib/proxy-protocol'); class Server extends tcp.Server { constructor(options) { super(); + let proxyProtocolEnabled = false; + if (typeof options === 'object' && options !== null) { + proxyProtocolEnabled = options.proxyProtocol ?? false; + } if (typeof options === 'function') { this.on('request', options); } + this.proxyProtocol = proxyProtocolEnabled; this.on('connection', this.handle.bind(this)); } async handle(client) { try { + if (this.proxyProtocol) { + const header = await consumeProxyHeader(client); + client.proxy = header; + if (header.command === 'PROXY') { + client.proxyAddress = header.sourceAddress; + client.proxyPort = header.sourcePort; + } + } const data = await Packet.readStream(client); const message = Packet.parse(data); this.emit('request', message, this.response.bind(this, client), client); @@ -31,4 +45,61 @@ class Server extends tcp.Server { } } +// Read and consume the PROXY header from the front of the socket's stream. +// Any bytes that arrive past the header are unshifted back into the socket +// so the next reader (Packet.readStream) sees them. +function consumeProxyHeader(socket) { + return new Promise((resolve, reject) => { + const chunks = []; + let chunklen = 0; + let done = false; + + const cleanup = () => { + socket.removeListener('readable', onReadable); + socket.removeListener('end', onEnd); + socket.removeListener('error', onError); + }; + const onError = err => { + if (done) return; + done = true; + cleanup(); + reject(err); + }; + const onEnd = () => { + if (done) return; + done = true; + cleanup(); + reject(new Error('PROXY protocol: stream ended before header complete')); + }; + const onReadable = () => { + if (done) return; + let chunk; + while ((chunk = socket.read()) !== null) { + chunks.push(chunk); + chunklen += chunk.length; + } + if (chunklen === 0) return; + const buffer = Buffer.concat(chunks, chunklen); + let parsed; + try { + parsed = proxyProtocol.parse(buffer); + } catch (e) { + return onError(e); + } + if (!parsed) return; + done = true; + cleanup(); + const leftover = buffer.slice(parsed.headerLength); + if (leftover.length) socket.unshift(leftover); + resolve(parsed.header); + }; + + socket.on('readable', onReadable); + socket.on('end', onEnd); + socket.on('error', onError); + // Drain anything already buffered before our 'readable' listener attached. + onReadable(); + }); +} + module.exports = Server; diff --git a/server/udp.js b/server/udp.js index 3cd67e7..805bcf4 100644 --- a/server/udp.js +++ b/server/udp.js @@ -1,5 +1,6 @@ const udp = require('node:dgram'); const Packet = require('../packet'); +const proxyProtocol = require('../lib/proxy-protocol'); /** * [Server description] @@ -9,10 +10,13 @@ const Packet = require('../packet'); class Server extends udp.Socket { constructor(options) { let type = 'udp4'; - if (typeof options === 'object') { + let proxyProtocolEnabled = false; + if (typeof options === 'object' && options !== null) { type = options.type ?? type; + proxyProtocolEnabled = options.proxyProtocol ?? false; } super(type); + this.proxyProtocol = proxyProtocolEnabled; if (typeof options === 'function') { this.on('request', options); } @@ -21,8 +25,28 @@ class Server extends udp.Socket { handle(data, rinfo) { try { + // Response is always sent back to the immediate sender (the proxy when + // proxyProtocol is enabled); the parsed client info is exposed to the + // request handler so it can log/authorize against the real peer. + const responder = rinfo; + let clientInfo = rinfo; + if (this.proxyProtocol) { + const parsed = proxyProtocol.parse(data); + if (!parsed) throw new Error('PROXY protocol: incomplete header'); + if (parsed.header.command === 'PROXY') { + clientInfo = { + ...rinfo, + address : parsed.header.sourceAddress, + port : parsed.header.sourcePort, + proxy : parsed.header, + }; + } else { + clientInfo = { ...rinfo, proxy: parsed.header }; + } + data = data.slice(parsed.headerLength); + } const message = Packet.parse(data); - this.emit('request', message, this.response.bind(this, rinfo), rinfo); + this.emit('request', message, this.response.bind(this, responder), clientInfo); } catch (e) { this.emit('requestError', e); } diff --git a/test/proxy-protocol.js b/test/proxy-protocol.js new file mode 100644 index 0000000..7c2844d --- /dev/null +++ b/test/proxy-protocol.js @@ -0,0 +1,119 @@ +const assert = require('node:assert'); +const test = require('./test'); +const proxy = require('../lib/proxy-protocol'); + +test('proxy v1: TCP4 header parses', function() { + const buf = Buffer.from('PROXY TCP4 203.0.113.5 198.51.100.1 56324 53\r\n', 'ascii'); + const { header, headerLength } = proxy.parse(buf); + assert.equal(header.version, 1); + assert.equal(header.command, 'PROXY'); + assert.equal(header.family, 'IPv4'); + assert.equal(header.sourceAddress, '203.0.113.5'); + assert.equal(header.sourcePort, 56324); + assert.equal(header.destinationAddress, '198.51.100.1'); + assert.equal(header.destinationPort, 53); + assert.equal(headerLength, buf.length); +}); + +test('proxy v1: TCP6 header parses', function() { + const buf = Buffer.from( + 'PROXY TCP6 2001:db8::1 2001:db8::2 49152 53\r\n', 'ascii'); + const { header } = proxy.parse(buf); + assert.equal(header.family, 'IPv6'); + assert.equal(header.sourceAddress, '2001:db8::1'); + assert.equal(header.destinationAddress, '2001:db8::2'); + assert.equal(header.sourcePort, 49152); +}); + +test('proxy v1: UNKNOWN protocol parses without address info', function() { + const buf = Buffer.from('PROXY UNKNOWN\r\n', 'ascii'); + const { header, headerLength } = proxy.parse(buf); + assert.equal(header.version, 1); + assert.equal(header.command, 'UNKNOWN'); + assert.equal(header.sourceAddress, undefined); + assert.equal(headerLength, buf.length); +}); + +test('proxy v1: payload after header is preserved via headerLength', function() { + const header = 'PROXY TCP4 1.2.3.4 5.6.7.8 1024 53\r\n'; + const payload = Buffer.from([ 0x00, 0x01, 0x02, 0x03 ]); + const buf = Buffer.concat([ Buffer.from(header, 'ascii'), payload ]); + const { headerLength } = proxy.parse(buf); + assert.deepEqual(buf.slice(headerLength), payload); +}); + +test('proxy v1: incomplete header (no \\r\\n yet) returns null', function() { + const buf = Buffer.from('PROXY TCP4 1.2.3.4', 'ascii'); + assert.equal(proxy.parse(buf), null); +}); + +test('proxy v1: oversized header without terminator throws', function() { + // V1 max line length is 108; build something past that with no \r\n. + const buf = Buffer.from('PROXY ' + 'x'.repeat(200), 'ascii'); + assert.throws(() => proxy.parse(buf), /exceeds maximum length/); +}); + +test('proxy v2: IPv4 PROXY header parses', function() { + const buf = proxy.buildV2Ipv4({ + sourceAddress : '203.0.113.99', + destinationAddress : '198.51.100.1', + sourcePort : 51000, + destinationPort : 53, + }); + const { header, headerLength } = proxy.parse(buf); + assert.equal(header.version, 2); + assert.equal(header.command, 'PROXY'); + assert.equal(header.family, 'IPv4'); + assert.equal(header.transport, 'STREAM'); + assert.equal(header.sourceAddress, '203.0.113.99'); + assert.equal(header.sourcePort, 51000); + assert.equal(header.destinationAddress, '198.51.100.1'); + assert.equal(header.destinationPort, 53); + assert.equal(headerLength, 28); +}); + +test('proxy v2: incomplete header (signature only) returns null', function() { + const sig = Buffer.from([ + 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, + 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, + ]); + assert.equal(proxy.parse(sig), null); +}); + +test('proxy v2: payload after header is preserved via headerLength', function() { + const header = proxy.buildV2Ipv4({ + sourceAddress : '10.0.0.1', + destinationAddress : '10.0.0.2', + sourcePort : 12345, + destinationPort : 53, + }); + const payload = Buffer.from([ 0xAB, 0xCD, 0xEF ]); + const buf = Buffer.concat([ header, payload ]); + const parsed = proxy.parse(buf); + assert.deepEqual(buf.slice(parsed.headerLength), payload); +}); + +test('proxy v2: LOCAL command is recognized without address info', function() { + const buf = Buffer.alloc(16); + Buffer.from([ + 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, + 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, + ]).copy(buf, 0); + buf[12] = 0x20; // version 2 | LOCAL command (0) + buf[13] = 0x00; // AF_UNSPEC + buf.writeUInt16BE(0, 14); + const { header } = proxy.parse(buf); + assert.equal(header.version, 2); + assert.equal(header.command, 'LOCAL'); +}); + +test('proxy: unrelated bytes throw "header missing or malformed"', function() { + const buf = Buffer.from('GET / HTTP/1.1\r\n', 'ascii'); + assert.throws(() => proxy.parse(buf), /header missing or malformed/); +}); + +test('proxy: empty buffer needs more (returns null via prefix match)', function() { + // V1_PREFIX.slice(0,0).equals(Buffer.alloc(0)) is true, so empty bytes + // are treated as "incomplete v1 header" rather than malformed. + assert.equal(proxy.parse(Buffer.alloc(0)), null); +}); diff --git a/test/server.js b/test/server.js index 695efb0..8161378 100644 --- a/test/server.js +++ b/test/server.js @@ -508,3 +508,243 @@ test('server/all#maxConcurrent - excess requests receive SERVFAIL', async() => { await server.close(); }); + +// --------------------------------------------------------------------------- +// PROXY protocol (issue #81) — UDP and TCP servers expose the real client IP +// when sitting behind an L4 proxy (Nginx stream, HAProxy, etc). +// --------------------------------------------------------------------------- + +const proxyProtocol = require('../lib/proxy-protocol'); + +test('server/udp#proxyProtocol exposes real client address (v2 IPv4)', async() => { + const server = createUDPServer({ proxyProtocol: true }); + let observedClient; + server.on('request', (request, send, info) => { + observedClient = info; + const response = Packet.createResponseFromRequest(request); + response.answers.push({ + name : request.questions[0].name, + type : Packet.TYPE.A, + class : Packet.CLASS.IN, + ttl : 60, + address : '127.0.0.1', + }); + send(response); + }); + await server.listen(0, '127.0.0.1'); + const { port: serverPort } = server.address(); + + // Build a DNS query and prepend a PROXY v2 IPv4 header naming a fake client. + const query = new Packet(); + query.header.id = 0x4321; + query.header.rd = 1; + query.questions.push({ name: 'proxied.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + const header = proxyProtocol.buildV2Ipv4({ + sourceAddress : '203.0.113.77', + destinationAddress : '127.0.0.1', + sourcePort : 50001, + destinationPort : serverPort, + transport : 'DGRAM', + }); + const datagram = Buffer.concat([ header, query.toBuffer() ]); + + const sender = udp.createSocket('udp4'); + const reply = await new Promise((resolve, reject) => { + sender.on('message', msg => resolve(Packet.parse(msg))); + sender.on('error', reject); + sender.send(datagram, serverPort, '127.0.0.1'); + }); + await new Promise(resolve => sender.close(resolve)); + + assert.equal(reply.header.id, 0x4321); + assert.equal(reply.answers[0].address, '127.0.0.1'); + assert.equal(observedClient.address, '203.0.113.77'); + assert.equal(observedClient.port, 50001); + assert.equal(observedClient.proxy.version, 2); + assert.equal(observedClient.proxy.transport, 'DGRAM'); + await new Promise(resolve => server.close(resolve)); +}); + +test('server/udp#proxyProtocol with missing header emits requestError', async() => { + const server = createUDPServer({ proxyProtocol: true }); + let captured; + server.on('requestError', e => { captured = e; }); + await server.listen(0, '127.0.0.1'); + const { port: serverPort } = server.address(); + + const query = new Packet(); + query.header.id = 1; + query.questions.push({ name: 'noheader.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + + const sender = udp.createSocket('udp4'); + await new Promise(resolve => sender.send(query.toBuffer(), serverPort, '127.0.0.1', resolve)); + // Give the server a moment to handle the datagram. + await new Promise(resolve => setTimeout(resolve, 20)); + await new Promise(resolve => sender.close(resolve)); + + assert.ok(captured, 'expected requestError to fire'); + assert.match(captured.message, /PROXY/); + await new Promise(resolve => server.close(resolve)); +}); + +test('server/tcp#proxyProtocol v1 exposes real client address', async() => { + const server = createTCPServer({ proxyProtocol: true }); + let observed; + server.on('request', (request, send, client) => { + observed = { address: client.proxyAddress, port: client.proxyPort, proxy: client.proxy }; + const response = Packet.createResponseFromRequest(request); + response.answers.push({ + name : request.questions[0].name, + type : Packet.TYPE.A, + class : Packet.CLASS.IN, + ttl : 60, + address : '127.0.0.1', + }); + send(response); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const { port: serverPort } = server.address(); + + const query = new Packet(); + query.header.id = 0x1111; + query.questions.push({ name: 'proxied-tcp.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + const dnsMessage = query.toBuffer(); + const length = Buffer.alloc(2); + length.writeUInt16BE(dnsMessage.length); + const proxyHeader = proxyProtocol.buildV1({ + family : 'TCP4', + sourceAddress : '198.51.100.42', + destinationAddress : '127.0.0.1', + sourcePort : 51515, + destinationPort : serverPort, + }); + + const reply = await new Promise((resolve, reject) => { + const sock = tcp.connect(serverPort, '127.0.0.1', () => { + sock.write(Buffer.concat([ proxyHeader, length, dnsMessage ])); + }); + const chunks = []; + sock.on('data', c => chunks.push(c)); + sock.on('end', () => { + const buf = Buffer.concat(chunks); + resolve(Packet.parse(buf.slice(2))); + }); + sock.on('error', reject); + }); + + assert.equal(reply.header.id, 0x1111); + assert.equal(reply.answers[0].address, '127.0.0.1'); + assert.equal(observed.address, '198.51.100.42'); + assert.equal(observed.port, 51515); + assert.equal(observed.proxy.version, 1); + await new Promise(resolve => server.close(resolve)); +}); + +test('server/tcp#proxyProtocol v2 exposes real client address', async() => { + const server = createTCPServer({ proxyProtocol: true }); + let observed; + server.on('request', (request, send, client) => { + observed = { address: client.proxyAddress, port: client.proxyPort, version: client.proxy.version }; + const response = Packet.createResponseFromRequest(request); + response.answers.push({ + name : request.questions[0].name, + type : Packet.TYPE.A, + class : Packet.CLASS.IN, + ttl : 60, + address : '127.0.0.1', + }); + send(response); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const { port: serverPort } = server.address(); + + const query = new Packet(); + query.header.id = 0x2222; + query.questions.push({ name: 'proxied-tcp-v2.test', type: Packet.TYPE.A, class: Packet.CLASS.IN }); + const dnsMessage = query.toBuffer(); + const length = Buffer.alloc(2); + length.writeUInt16BE(dnsMessage.length); + const proxyHeader = proxyProtocol.buildV2Ipv4({ + sourceAddress : '198.51.100.99', + destinationAddress : '127.0.0.1', + sourcePort : 52525, + destinationPort : serverPort, + }); + + const reply = await new Promise((resolve, reject) => { + const sock = tcp.connect(serverPort, '127.0.0.1', () => { + sock.write(Buffer.concat([ proxyHeader, length, dnsMessage ])); + }); + const chunks = []; + sock.on('data', c => chunks.push(c)); + sock.on('end', () => resolve(Packet.parse(Buffer.concat(chunks).slice(2)))); + sock.on('error', reject); + }); + + assert.equal(reply.header.id, 0x2222); + assert.equal(observed.address, '198.51.100.99'); + assert.equal(observed.port, 52525); + assert.equal(observed.version, 2); + await new Promise(resolve => server.close(resolve)); +}); + +test('server/tcp#proxyProtocol with garbage prefix emits requestError', async() => { + const server = createTCPServer({ proxyProtocol: true }); + let captured; + server.on('requestError', e => { captured = e; }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const { port: serverPort } = server.address(); + + await new Promise((resolve, reject) => { + const sock = tcp.connect(serverPort, '127.0.0.1', () => { + sock.end(Buffer.from('GET / HTTP/1.1\r\n\r\n', 'ascii')); + }); + sock.on('close', resolve); + sock.on('error', reject); + }); + // Give the server an event-loop tick to surface the error. + await new Promise(resolve => setTimeout(resolve, 20)); + + assert.ok(captured, 'expected requestError to fire'); + assert.match(captured.message, /PROXY/); + await new Promise(resolve => server.close(resolve)); +}); + +test('server/udp/tcp without proxyProtocol still work normally', async() => { + // Regression guard: enabling the option is opt-in; default behavior unchanged. + const udpServer = createUDPServer(); + udpServer.on('request', (request, send) => { + const response = Packet.createResponseFromRequest(request); + response.answers.push({ + name : request.questions[0].name, + type : Packet.TYPE.A, + class : Packet.CLASS.IN, + ttl : 60, + address : '10.0.0.1', + }); + send(response); + }); + await udpServer.listen(0, '127.0.0.1'); + const udpQuery = UDPClient({ dns: '127.0.0.1', port: udpServer.address().port }); + const udpReply = await udpQuery('plain.test'); + assert.equal(udpReply.answers[0].address, '10.0.0.1'); + await new Promise(resolve => udpServer.close(resolve)); + + const tcpServer = createTCPServer(); + tcpServer.on('request', (request, send) => { + const response = Packet.createResponseFromRequest(request); + response.answers.push({ + name : request.questions[0].name, + type : Packet.TYPE.A, + class : Packet.CLASS.IN, + ttl : 60, + address : '10.0.0.2', + }); + send(response); + }); + await new Promise(resolve => tcpServer.listen(0, '127.0.0.1', resolve)); + const tcpQuery = TCPClient({ dns: '127.0.0.1', port: tcpServer.address().port }); + const tcpReply = await tcpQuery('plain.test'); + assert.equal(tcpReply.answers[0].address, '10.0.0.2'); + await new Promise(resolve => tcpServer.close(resolve)); +});