dns2 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ 'use strict';
2
+
3
+ // PROXY protocol parser (HAProxy/Nginx).
4
+ // Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
5
+ //
6
+ // parse(buffer) returns:
7
+ // - { header, headerLength } when a complete header is at the start of buffer
8
+ // - null when the buffer is a valid prefix but more bytes are needed
9
+ // and throws when the bytes are not a valid PROXY header.
10
+
11
+ const V2_SIGNATURE = Buffer.from([
12
+ 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D,
13
+ 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A,
14
+ ]);
15
+ const V1_PREFIX = Buffer.from('PROXY ');
16
+ const V1_MAX_LEN = 108;
17
+
18
+ const FAMILY = { 0x10: 'IPv4', 0x20: 'IPv6', 0x30: 'Unix' };
19
+ const TRANSPORT = { 0x01: 'STREAM', 0x02: 'DGRAM' };
20
+
21
+ function parse(buffer) {
22
+ if (buffer.length >= 12) {
23
+ if (buffer.slice(0, 12).equals(V2_SIGNATURE)) return parseV2(buffer);
24
+ } else if (V2_SIGNATURE.slice(0, buffer.length).equals(buffer)) {
25
+ return null;
26
+ }
27
+
28
+ if (buffer.length >= 6) {
29
+ if (buffer.slice(0, 6).equals(V1_PREFIX)) return parseV1(buffer);
30
+ } else if (V1_PREFIX.slice(0, buffer.length).equals(buffer)) {
31
+ return null;
32
+ }
33
+
34
+ throw new Error('PROXY protocol: header missing or malformed');
35
+ }
36
+
37
+ function parseV1(buffer) {
38
+ const search = buffer.slice(0, Math.min(buffer.length, V1_MAX_LEN));
39
+ const newline = search.indexOf('\r\n');
40
+ if (newline === -1) {
41
+ if (buffer.length >= V1_MAX_LEN) {
42
+ throw new Error('PROXY v1: header exceeds maximum length');
43
+ }
44
+ return null;
45
+ }
46
+ const line = buffer.slice(0, newline).toString('ascii');
47
+ const parts = line.split(' ');
48
+ if (parts[0] !== 'PROXY') throw new Error('PROXY v1: malformed header');
49
+ const headerLength = newline + 2;
50
+
51
+ if (parts[1] === 'UNKNOWN') {
52
+ return { header: { version: 1, command: 'UNKNOWN' }, headerLength };
53
+ }
54
+ if (parts.length !== 6) throw new Error('PROXY v1: malformed header');
55
+ const [ , proto, sourceAddress, destinationAddress, srcPort, dstPort ] = parts;
56
+ if (proto !== 'TCP4' && proto !== 'TCP6') {
57
+ throw new Error(`PROXY v1: unsupported protocol ${proto}`);
58
+ }
59
+ return {
60
+ header: {
61
+ version : 1,
62
+ command : 'PROXY',
63
+ family : proto === 'TCP4' ? 'IPv4' : 'IPv6',
64
+ transport : 'STREAM',
65
+ sourceAddress,
66
+ sourcePort : parseInt(srcPort, 10),
67
+ destinationAddress,
68
+ destinationPort : parseInt(dstPort, 10),
69
+ },
70
+ headerLength,
71
+ };
72
+ }
73
+
74
+ function parseV2(buffer) {
75
+ if (buffer.length < 16) return null;
76
+ const verCmd = buffer[12];
77
+ const version = verCmd >> 4;
78
+ const command = verCmd & 0x0F;
79
+ if (version !== 2) throw new Error(`PROXY v2: unsupported version ${version}`);
80
+ if (command !== 0 && command !== 1) {
81
+ throw new Error(`PROXY v2: unknown command ${command}`);
82
+ }
83
+
84
+ const famProto = buffer[13];
85
+ const addressLength = buffer.readUInt16BE(14);
86
+ const headerLength = 16 + addressLength;
87
+ if (buffer.length < headerLength) return null;
88
+
89
+ if (command === 0) {
90
+ // LOCAL — no real client info (e.g. proxy-originated health check).
91
+ return { header: { version: 2, command: 'LOCAL' }, headerLength };
92
+ }
93
+
94
+ const family = FAMILY[famProto & 0xF0];
95
+ const transport = TRANSPORT[famProto & 0x0F];
96
+
97
+ let sourceAddress, destinationAddress, sourcePort, destinationPort;
98
+ if (family === 'IPv4' && addressLength >= 12) {
99
+ sourceAddress = `${buffer[16]}.${buffer[17]}.${buffer[18]}.${buffer[19]}`;
100
+ destinationAddress = `${buffer[20]}.${buffer[21]}.${buffer[22]}.${buffer[23]}`;
101
+ sourcePort = buffer.readUInt16BE(24);
102
+ destinationPort = buffer.readUInt16BE(26);
103
+ } else if (family === 'IPv6' && addressLength >= 36) {
104
+ sourceAddress = ipv6FromBytes(buffer.slice(16, 32));
105
+ destinationAddress = ipv6FromBytes(buffer.slice(32, 48));
106
+ sourcePort = buffer.readUInt16BE(48);
107
+ destinationPort = buffer.readUInt16BE(50);
108
+ } else {
109
+ throw new Error(`PROXY v2: unsupported address family/protocol 0x${famProto.toString(16)}`);
110
+ }
111
+
112
+ return {
113
+ header: {
114
+ version : 2,
115
+ command : 'PROXY',
116
+ family,
117
+ transport,
118
+ sourceAddress,
119
+ sourcePort,
120
+ destinationAddress,
121
+ destinationPort,
122
+ },
123
+ headerLength,
124
+ };
125
+ }
126
+
127
+ function ipv6FromBytes(bytes) {
128
+ const segments = [];
129
+ for (let i = 0; i < 16; i += 2) {
130
+ segments.push(bytes.readUInt16BE(i).toString(16));
131
+ }
132
+ return segments.join(':');
133
+ }
134
+
135
+ // Test helpers — build wire-format headers used by tests and example code.
136
+ function buildV1({ family = 'TCP4', sourceAddress, destinationAddress, sourcePort, destinationPort }) {
137
+ return Buffer.from(`PROXY ${family} ${sourceAddress} ${destinationAddress} ${sourcePort} ${destinationPort}\r\n`, 'ascii');
138
+ }
139
+
140
+ function buildV2Ipv4({ sourceAddress, destinationAddress, sourcePort, destinationPort, transport = 'STREAM' }) {
141
+ const buf = Buffer.alloc(16 + 12);
142
+ V2_SIGNATURE.copy(buf, 0);
143
+ buf[12] = 0x21; // version 2 | PROXY command
144
+ buf[13] = 0x10 | (transport === 'DGRAM' ? 0x02 : 0x01); // IPv4 | STREAM/DGRAM
145
+ buf.writeUInt16BE(12, 14);
146
+ sourceAddress.split('.').forEach((o, i) => { buf[16 + i] = parseInt(o, 10); });
147
+ destinationAddress.split('.').forEach((o, i) => { buf[20 + i] = parseInt(o, 10); });
148
+ buf.writeUInt16BE(sourcePort, 24);
149
+ buf.writeUInt16BE(destinationPort, 26);
150
+ return buf;
151
+ }
152
+
153
+ module.exports = { parse, parseV1, parseV2, buildV1, buildV2Ipv4 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dns2",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "A DNS Server and Client Implementation in Pure JavaScript with no dependencies.",
5
5
  "main": "index.js",
6
6
  "types": "ts/index.d.ts",
package/packet.js CHANGED
@@ -5,10 +5,29 @@ const BufferWriter = require('./lib/writer');
5
5
 
6
6
  const debug = debuglog('dns2');
7
7
 
8
- const toIPv6 = buffer => buffer
9
- .map(part => (part > 0 ? part.toString(16) : '0'))
10
- .join(':')
11
- .replace(/\b(?:0+:){1,}/, ':');
8
+ // Canonical IPv6 text form per RFC 5952:
9
+ // - lower case hex, no leading zeros per group (handled by toString(16))
10
+ // - the longest run of >= 2 zero groups is replaced with "::"
11
+ // - on ties, the first such run is chosen
12
+ // - a single zero group is NOT compressed
13
+ const toIPv6 = buffer => {
14
+ const segments = buffer.map(part => (part > 0 ? part.toString(16) : '0'));
15
+ let bestStart = -1; let bestLen = 0;
16
+ let curStart = -1; let curLen = 0;
17
+ for (let i = 0; i < segments.length; i++) {
18
+ if (segments[i] === '0') {
19
+ if (curLen === 0) curStart = i;
20
+ curLen++;
21
+ if (curLen > bestLen) { bestLen = curLen; bestStart = curStart; }
22
+ } else {
23
+ curLen = 0;
24
+ }
25
+ }
26
+ if (bestLen < 2) return segments.join(':');
27
+ const before = segments.slice(0, bestStart).join(':');
28
+ const after = segments.slice(bestStart + bestLen).join(':');
29
+ return `${before}::${after}`;
30
+ };
12
31
 
13
32
  const fromIPv6 = (address) => {
14
33
  const digits = address.split(':');
@@ -240,8 +259,11 @@ Packet.Header = function(header) {
240
259
  this.rd = 0;
241
260
  this.ra = 0;
242
261
  this.z = 0;
262
+ this.ad = 0;
263
+ this.cd = 0;
243
264
  this.rcode = 0;
244
265
  this.qdcount = 0;
266
+ this.ancount = 0;
245
267
  this.nscount = 0;
246
268
  this.arcount = 0;
247
269
  for (const k in header) {
@@ -267,7 +289,10 @@ Packet.Header.parse = function(reader) {
267
289
  header.tc = reader.read(1);
268
290
  header.rd = reader.read(1);
269
291
  header.ra = reader.read(1);
270
- header.z = reader.read(3);
292
+ // RFC 4035 §3.2.3 repurposed the second and third Z bits as AD and CD.
293
+ header.z = reader.read(1);
294
+ header.ad = reader.read(1);
295
+ header.cd = reader.read(1);
271
296
  header.rcode = reader.read(4);
272
297
  header.qdcount = reader.read(16);
273
298
  header.ancount = reader.read(16);
@@ -289,7 +314,9 @@ Packet.Header.prototype.toBuffer = function(writer) {
289
314
  writer.write(this.tc, 1);
290
315
  writer.write(this.rd, 1);
291
316
  writer.write(this.ra, 1);
292
- writer.write(this.z, 3);
317
+ writer.write(this.z, 1);
318
+ writer.write(this.ad, 1);
319
+ writer.write(this.cd, 1);
293
320
  writer.write(this.rcode, 4);
294
321
  writer.write(this.qdcount, 16);
295
322
  writer.write(this.ancount, 16);
@@ -457,11 +484,18 @@ Packet.Name = {
457
484
  reader = new Packet.Reader(reader);
458
485
  }
459
486
  const name = []; let o; let len = reader.read(8);
487
+ // Track each pointer target we follow. A crafted packet can chain
488
+ // pointers in a cycle; without this guard, decode would loop forever.
489
+ const visited = new Set();
460
490
  while (len) {
461
491
  if ((len & Packet.Name.COPY) === Packet.Name.COPY) {
462
492
  len -= Packet.Name.COPY;
463
493
  len = len << 8;
464
494
  const pos = len + reader.read(8);
495
+ if (visited.has(pos)) {
496
+ throw new Error('Name decode: pointer cycle detected');
497
+ }
498
+ visited.add(pos);
465
499
  if (!o) o = reader.offset;
466
500
  reader.offset = pos * 8;
467
501
  len = reader.read(8);
@@ -740,19 +774,42 @@ Packet.Resource.SRV = {
740
774
  },
741
775
  };
742
776
 
743
- Packet.Resource.EDNS = function(rdata) {
777
+ // RFC 6891 §6.1.3 the OPT record's TTL field carries:
778
+ // bits 0- 7: extended RCODE (high byte of a 12-bit RCODE)
779
+ // bits 8-15: EDNS version
780
+ // bit 16: DO (DNSSEC OK)
781
+ // bits 17-31: reserved Z, must be zero
782
+ const ednsTtl = (extendedRcode, version, doFlag) =>
783
+ (((extendedRcode & 0xff) << 24) >>> 0)
784
+ | ((version & 0xff) << 16)
785
+ | (doFlag ? 0x8000 : 0);
786
+
787
+ Packet.Resource.EDNS = function(rdata, opts = {}) {
788
+ const extendedRcode = opts.extendedRcode || 0;
789
+ const version = opts.version || 0;
790
+ const doFlag = !!opts.doFlag;
791
+ const udpPayloadSize = opts.udpPayloadSize || 512;
744
792
  return {
745
793
  type : Packet.TYPE.EDNS,
746
- class : 512, // Supported UDP Payload size
747
- ttl : 0, // Extended RCODE and flags
794
+ class : udpPayloadSize,
795
+ ttl : ednsTtl(extendedRcode, version, doFlag),
796
+ extendedRcode,
797
+ version,
798
+ doFlag,
748
799
  rdata, // Objects of type Packet.Resource.EDNS.*
749
800
  };
750
801
  };
751
802
 
752
803
  Packet.Resource.EDNS.decode = function(reader, length) {
753
- this.type = Packet.TYPE.EDNS;
754
- this.class = 512;
755
- this.ttl = 0;
804
+ // When invoked through Resource.parse, this.type/class/ttl are already set
805
+ // from the wire. Direct callers (e.g. unit tests) hit defaults instead.
806
+ this.type = this.type ?? Packet.TYPE.EDNS;
807
+ this.class = this.class ?? 512;
808
+ const ttl = this.ttl ?? 0;
809
+ this.ttl = ttl;
810
+ this.extendedRcode = (ttl >>> 24) & 0xff;
811
+ this.version = (ttl >>> 16) & 0xff;
812
+ this.doFlag = !!(ttl & 0x8000);
756
813
  this.rdata = [];
757
814
 
758
815
  while (length) {
@@ -845,16 +902,47 @@ Packet.Resource.EDNS.ECS.decode = function(reader, length) {
845
902
  };
846
903
 
847
904
  Packet.Resource.EDNS.ECS.encode = function(record, writer) {
848
- const ip = record.ip.split('.').map(s => parseInt(s));
905
+ // RFC 7871 §6: the ADDRESS field carries only the leftmost
906
+ // ceil(sourcePrefixLength / 8) octets.
907
+ const octets = Math.ceil(record.sourcePrefixLength / 8);
849
908
  writer.write(record.family, 16);
850
909
  writer.write(record.sourcePrefixLength, 8);
851
910
  writer.write(record.scopePrefixLength, 8);
852
- writer.write(ip[0], 8);
853
- writer.write(ip[1], 8);
854
- writer.write(ip[2], 8);
855
- writer.write(ip[3], 8);
911
+ let bytes;
912
+ if (record.family === 1) {
913
+ bytes = record.ip.split('.').map(s => parseInt(s, 10) || 0);
914
+ } else if (record.family === 2) {
915
+ bytes = expandIPv6ToBytes(record.ip);
916
+ } else {
917
+ throw new Error(`EDNS.ECS encode: unsupported family ${record.family}`);
918
+ }
919
+ for (let i = 0; i < octets; i++) {
920
+ writer.write(bytes[i] || 0, 8);
921
+ }
856
922
  };
857
923
 
924
+ // Expand a (possibly compressed) IPv6 text address into a 16-byte array.
925
+ function expandIPv6ToBytes(address) {
926
+ let head, tail;
927
+ const idx = address.indexOf('::');
928
+ if (idx === -1) {
929
+ head = address.split(':');
930
+ tail = [];
931
+ } else {
932
+ head = address.slice(0, idx).split(':').filter(Boolean);
933
+ tail = address.slice(idx + 2).split(':').filter(Boolean);
934
+ }
935
+ const missing = 8 - head.length - tail.length;
936
+ const groups = [ ...head, ...new Array(missing).fill('0'), ...tail ];
937
+ const out = new Array(16).fill(0);
938
+ for (let g = 0; g < 8; g++) {
939
+ const n = parseInt(groups[g], 16) || 0;
940
+ out[g * 2] = (n >> 8) & 0xff;
941
+ out[g * 2 + 1] = n & 0xff;
942
+ }
943
+ return out;
944
+ }
945
+
858
946
  Packet.Resource.CAA = {
859
947
  encode: function(record, writer) {
860
948
  writer = writer || new Packet.Writer();
package/server/tcp.js CHANGED
@@ -1,17 +1,31 @@
1
1
  const tcp = require('node:net');
2
2
  const Packet = require('../packet');
3
+ const proxyProtocol = require('../lib/proxy-protocol');
3
4
 
4
5
  class Server extends tcp.Server {
5
6
  constructor(options) {
6
7
  super();
8
+ let proxyProtocolEnabled = false;
9
+ if (typeof options === 'object' && options !== null) {
10
+ proxyProtocolEnabled = options.proxyProtocol ?? false;
11
+ }
7
12
  if (typeof options === 'function') {
8
13
  this.on('request', options);
9
14
  }
15
+ this.proxyProtocol = proxyProtocolEnabled;
10
16
  this.on('connection', this.handle.bind(this));
11
17
  }
12
18
 
13
19
  async handle(client) {
14
20
  try {
21
+ if (this.proxyProtocol) {
22
+ const header = await consumeProxyHeader(client);
23
+ client.proxy = header;
24
+ if (header.command === 'PROXY') {
25
+ client.proxyAddress = header.sourceAddress;
26
+ client.proxyPort = header.sourcePort;
27
+ }
28
+ }
15
29
  const data = await Packet.readStream(client);
16
30
  const message = Packet.parse(data);
17
31
  this.emit('request', message, this.response.bind(this, client), client);
@@ -31,4 +45,61 @@ class Server extends tcp.Server {
31
45
  }
32
46
  }
33
47
 
48
+ // Read and consume the PROXY header from the front of the socket's stream.
49
+ // Any bytes that arrive past the header are unshifted back into the socket
50
+ // so the next reader (Packet.readStream) sees them.
51
+ function consumeProxyHeader(socket) {
52
+ return new Promise((resolve, reject) => {
53
+ const chunks = [];
54
+ let chunklen = 0;
55
+ let done = false;
56
+
57
+ const cleanup = () => {
58
+ socket.removeListener('readable', onReadable);
59
+ socket.removeListener('end', onEnd);
60
+ socket.removeListener('error', onError);
61
+ };
62
+ const onError = err => {
63
+ if (done) return;
64
+ done = true;
65
+ cleanup();
66
+ reject(err);
67
+ };
68
+ const onEnd = () => {
69
+ if (done) return;
70
+ done = true;
71
+ cleanup();
72
+ reject(new Error('PROXY protocol: stream ended before header complete'));
73
+ };
74
+ const onReadable = () => {
75
+ if (done) return;
76
+ let chunk;
77
+ while ((chunk = socket.read()) !== null) {
78
+ chunks.push(chunk);
79
+ chunklen += chunk.length;
80
+ }
81
+ if (chunklen === 0) return;
82
+ const buffer = Buffer.concat(chunks, chunklen);
83
+ let parsed;
84
+ try {
85
+ parsed = proxyProtocol.parse(buffer);
86
+ } catch (e) {
87
+ return onError(e);
88
+ }
89
+ if (!parsed) return;
90
+ done = true;
91
+ cleanup();
92
+ const leftover = buffer.slice(parsed.headerLength);
93
+ if (leftover.length) socket.unshift(leftover);
94
+ resolve(parsed.header);
95
+ };
96
+
97
+ socket.on('readable', onReadable);
98
+ socket.on('end', onEnd);
99
+ socket.on('error', onError);
100
+ // Drain anything already buffered before our 'readable' listener attached.
101
+ onReadable();
102
+ });
103
+ }
104
+
34
105
  module.exports = Server;
package/server/udp.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const udp = require('node:dgram');
2
2
  const Packet = require('../packet');
3
+ const proxyProtocol = require('../lib/proxy-protocol');
3
4
 
4
5
  /**
5
6
  * [Server description]
@@ -9,10 +10,13 @@ const Packet = require('../packet');
9
10
  class Server extends udp.Socket {
10
11
  constructor(options) {
11
12
  let type = 'udp4';
12
- if (typeof options === 'object') {
13
+ let proxyProtocolEnabled = false;
14
+ if (typeof options === 'object' && options !== null) {
13
15
  type = options.type ?? type;
16
+ proxyProtocolEnabled = options.proxyProtocol ?? false;
14
17
  }
15
18
  super(type);
19
+ this.proxyProtocol = proxyProtocolEnabled;
16
20
  if (typeof options === 'function') {
17
21
  this.on('request', options);
18
22
  }
@@ -21,8 +25,28 @@ class Server extends udp.Socket {
21
25
 
22
26
  handle(data, rinfo) {
23
27
  try {
28
+ // Response is always sent back to the immediate sender (the proxy when
29
+ // proxyProtocol is enabled); the parsed client info is exposed to the
30
+ // request handler so it can log/authorize against the real peer.
31
+ const responder = rinfo;
32
+ let clientInfo = rinfo;
33
+ if (this.proxyProtocol) {
34
+ const parsed = proxyProtocol.parse(data);
35
+ if (!parsed) throw new Error('PROXY protocol: incomplete header');
36
+ if (parsed.header.command === 'PROXY') {
37
+ clientInfo = {
38
+ ...rinfo,
39
+ address : parsed.header.sourceAddress,
40
+ port : parsed.header.sourcePort,
41
+ proxy : parsed.header,
42
+ };
43
+ } else {
44
+ clientInfo = { ...rinfo, proxy: parsed.header };
45
+ }
46
+ data = data.slice(parsed.headerLength);
47
+ }
24
48
  const message = Packet.parse(data);
25
- this.emit('request', message, this.response.bind(this, rinfo), rinfo);
49
+ this.emit('request', message, this.response.bind(this, responder), clientInfo);
26
50
  } catch (e) {
27
51
  this.emit('requestError', e);
28
52
  }