Skip to content

Commit 833f228

Browse files
committed
fix: RFC correctness drive updates
- fix(packet): Name decode rejects pointer cycles (RFC 1035) - fix(packet): EDNS exposes extendedRcode/version/doFlag; udpPayloadSize configurable (RFC 6891) - fix(packet): Header initializes ancount; AD/CD bits split from Z (RFC 4035) - fix(packet): ECS encoder truncates address to ceil(prefix/8) octets, adds IPv6 family (RFC 7871)
1 parent 4325ee6 commit 833f228

3 files changed

Lines changed: 247 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
44

55
### Unreleased
66

7+
- fix(packet): IPv6 `::` compression for leading-zero address #123
8+
- fix(packet): Name decode rejects pointer cycles (RFC 1035) #124
9+
- fix(packet): EDNS exposes extendedRcode/version/doFlag #124
10+
- fix(packet): Header initializes ancount; AD/CD bits split from Z (RFC 4035) #124
11+
- fix(packet): ECS encoder truncates address, adds IPv6 (RFC 7871) #124
12+
- feat(server): PROXY protocol v1/v2 support #122
13+
714
### 2.2.1 - 2026-05-25
815

916
- fix(packet): use crypto.randomInt for Packet.uuid (RFC 5452)

packet.js

Lines changed: 105 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,29 @@ const BufferWriter = require('./lib/writer');
55

66
const debug = debuglog('dns2');
77

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+
};
1231

1332
const fromIPv6 = (address) => {
1433
const digits = address.split(':');
@@ -240,8 +259,11 @@ Packet.Header = function(header) {
240259
this.rd = 0;
241260
this.ra = 0;
242261
this.z = 0;
262+
this.ad = 0;
263+
this.cd = 0;
243264
this.rcode = 0;
244265
this.qdcount = 0;
266+
this.ancount = 0;
245267
this.nscount = 0;
246268
this.arcount = 0;
247269
for (const k in header) {
@@ -267,7 +289,10 @@ Packet.Header.parse = function(reader) {
267289
header.tc = reader.read(1);
268290
header.rd = reader.read(1);
269291
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);
271296
header.rcode = reader.read(4);
272297
header.qdcount = reader.read(16);
273298
header.ancount = reader.read(16);
@@ -289,7 +314,9 @@ Packet.Header.prototype.toBuffer = function(writer) {
289314
writer.write(this.tc, 1);
290315
writer.write(this.rd, 1);
291316
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);
293320
writer.write(this.rcode, 4);
294321
writer.write(this.qdcount, 16);
295322
writer.write(this.ancount, 16);
@@ -457,11 +484,18 @@ Packet.Name = {
457484
reader = new Packet.Reader(reader);
458485
}
459486
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();
460490
while (len) {
461491
if ((len & Packet.Name.COPY) === Packet.Name.COPY) {
462492
len -= Packet.Name.COPY;
463493
len = len << 8;
464494
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);
465499
if (!o) o = reader.offset;
466500
reader.offset = pos * 8;
467501
len = reader.read(8);
@@ -740,19 +774,42 @@ Packet.Resource.SRV = {
740774
},
741775
};
742776

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;
744792
return {
745793
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,
748799
rdata, // Objects of type Packet.Resource.EDNS.*
749800
};
750801
};
751802

752803
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);
756813
this.rdata = [];
757814

758815
while (length) {
@@ -845,16 +902,47 @@ Packet.Resource.EDNS.ECS.decode = function(reader, length) {
845902
};
846903

847904
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);
849908
writer.write(record.family, 16);
850909
writer.write(record.sourcePrefixLength, 8);
851910
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+
}
856922
};
857923

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+
858946
Packet.Resource.CAA = {
859947
encode: function(record, writer) {
860948
writer = writer || new Packet.Writer();

test/packet.js

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,28 @@ test('Package#toIPv6', function() {
8181
assert.equal(Packet.toIPv6([ 9734, 18176, 12552, 0, 0, 0, 44098, 10984 ]), '2606:4700:3108::ac42:2ae8');
8282
});
8383

84+
test('Package#toIPv6 RFC 5952 — leading-zero addresses', function() {
85+
assert.equal(Packet.toIPv6([ 0, 0, 0, 0, 0, 0, 0, 1 ]), '::1');
86+
assert.equal(Packet.toIPv6([ 0, 0, 0, 0, 0, 0, 0, 0 ]), '::');
87+
assert.equal(Packet.toIPv6([ 0, 0, 0, 0, 0, 0xffff, 0xc000, 0x0201 ]), '::ffff:c000:201');
88+
});
89+
90+
test('Package#toIPv6 RFC 5952 — trailing-zero addresses', function() {
91+
assert.equal(Packet.toIPv6([ 1, 0, 0, 0, 0, 0, 0, 0 ]), '1::');
92+
assert.equal(Packet.toIPv6([ 0x2001, 0xdb8, 0, 0, 0, 0, 0, 0 ]), '2001:db8::');
93+
});
94+
95+
test('Package#toIPv6 RFC 5952 — single zero group is not compressed', function() {
96+
// §4.2.2: "::" MUST NOT be used to shorten just one 16-bit 0 field.
97+
assert.equal(Packet.toIPv6([ 1, 0, 1, 1, 1, 1, 1, 1 ]), '1:0:1:1:1:1:1:1');
98+
});
99+
100+
test('Package#toIPv6 RFC 5952 — first run wins on tie', function() {
101+
// §4.2.3: when there is more than one run of equal maximum length,
102+
// the first is shortened.
103+
assert.equal(Packet.toIPv6([ 1, 0, 0, 1, 0, 0, 1, 1 ]), '1::1:0:0:1:1');
104+
});
105+
84106
test('Package#fromIPv6', function() {
85107
assert.deepEqual(Packet.fromIPv6('2a04:4e42:200::323'), [
86108
'2a04', '4e42', '0200', '0', '0', '0', '0', '0323' ]);
@@ -221,11 +243,13 @@ test('EDNS.ECS#encode', function() {
221243
new Packet.Resource.EDNS.ECS('10.11.12.13/24'),
222244
]);
223245

246+
// RFC 7871 §6: ADDRESS field is only ceil(sourcePrefixLength/8) octets,
247+
// so /24 writes 3 address bytes (10.11.12), not 4.
224248
const b = Packet.Resource.encode(query);
225249
assert.deepEqual(b, Buffer.from([
226250
0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x00, 0x00,
227-
0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x08, 0x00,
228-
0x01, 0x18, 0x00, 0x0a, 0x0b, 0x0c, 0x0d ]));
251+
0x00, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x07, 0x00,
252+
0x01, 0x18, 0x00, 0x0a, 0x0b, 0x0c ]));
229253
});
230254

231255
test('EDNS#decode', function() {
@@ -772,6 +796,115 @@ test('Packet.uuid exercises the full 16-bit range with high diversity', function
772796
}
773797
});
774798

799+
test('Name decode rejects a pointer cycle (no infinite loop)', function() {
800+
// Hand-built packet header (12 bytes) followed by a name that points to
801+
// itself: byte 12 = 0xC0 (pointer high), byte 13 = 0x0C (offset = 12).
802+
// Without cycle detection this would loop forever.
803+
const buf = Buffer.alloc(14);
804+
buf[12] = 0xC0;
805+
buf[13] = 0x0C;
806+
const reader = new Packet.Reader(buf);
807+
reader.offset = 8 * 12;
808+
assert.throws(() => Packet.Name.decode(reader), /pointer cycle/);
809+
});
810+
811+
test('Name decode rejects a two-step pointer cycle', function() {
812+
// Two pointers pointing at each other: bytes 12-13 = C0 0E, bytes 14-15 = C0 0C.
813+
const buf = Buffer.alloc(16);
814+
buf[12] = 0xC0; buf[13] = 0x0E;
815+
buf[14] = 0xC0; buf[15] = 0x0C;
816+
const reader = new Packet.Reader(buf);
817+
reader.offset = 8 * 12;
818+
assert.throws(() => Packet.Name.decode(reader), /pointer cycle/);
819+
});
820+
821+
test('Header default constructor initializes ancount/ad/cd', function() {
822+
const header = new Packet.Header();
823+
assert.equal(header.ancount, 0);
824+
assert.equal(header.ad, 0);
825+
assert.equal(header.cd, 0);
826+
});
827+
828+
test('Header#parse exposes AD and CD bits (RFC 4035)', function() {
829+
// Second header word with AD=1, CD=1, all other flags zero.
830+
// Layout: qr(1) opcode(4) aa(1) tc(1) rd(1) ra(1) z(1) ad(1) cd(1) rcode(4)
831+
// bits : 0 0000 0 0 0 0 0 1 1 0000 => 0000 0000 0011 0000 = 0x0030
832+
const buf = Buffer.from([
833+
0x00, 0x01, // id
834+
0x00, 0x30, // flags: AD=1, CD=1
835+
0x00, 0x00, 0x00, 0x00, // counts
836+
0x00, 0x00, 0x00, 0x00,
837+
]);
838+
const header = Packet.Header.parse(buf);
839+
assert.equal(header.z, 0);
840+
assert.equal(header.ad, 1);
841+
assert.equal(header.cd, 1);
842+
});
843+
844+
test('Header#toBuffer round-trips AD and CD bits', function() {
845+
const header = new Packet.Header({ id: 0x4242, ad: 1, cd: 1 });
846+
const parsed = Packet.Header.parse(header.toBuffer());
847+
assert.equal(parsed.id, 0x4242);
848+
assert.equal(parsed.ad, 1);
849+
assert.equal(parsed.cd, 1);
850+
assert.equal(parsed.z, 0);
851+
});
852+
853+
test('EDNS exposes extendedRcode / version / doFlag', function() {
854+
const opt = new Packet.Resource.EDNS([], { extendedRcode: 16, version: 0, doFlag: true });
855+
assert.equal(opt.extendedRcode, 16);
856+
assert.equal(opt.version, 0);
857+
assert.equal(opt.doFlag, true);
858+
// ttl wire encoding: ext rcode in top byte, DO at bit 15 of low half.
859+
assert.equal(opt.ttl, (16 << 24) | 0x8000);
860+
});
861+
862+
test('EDNS round-trip preserves DO bit and extended RCODE', function() {
863+
const opt = new Packet.Resource.EDNS([], { extendedRcode: 23, version: 0, doFlag: true });
864+
const parsed = Packet.Resource.decode(Packet.Resource.encode(opt));
865+
assert.equal(parsed.extendedRcode, 23);
866+
assert.equal(parsed.doFlag, true);
867+
});
868+
869+
test('EDNS udpPayloadSize is configurable (RFC 6891 §6.2.3)', function() {
870+
const opt = new Packet.Resource.EDNS([], { udpPayloadSize: 4096 });
871+
assert.equal(opt.class, 4096);
872+
const parsed = Packet.Resource.decode(Packet.Resource.encode(opt));
873+
assert.equal(parsed.class, 4096);
874+
});
875+
876+
test('EDNS.ECS#encode truncates IPv4 address to prefix length (RFC 7871)', function() {
877+
// /8 → 1 octet, /17 → 3 octets (ceil)
878+
for (const [ cidr, expectedOctets ] of [
879+
[ '10.0.0.0/8', 1 ],
880+
[ '10.20.0.0/16', 2 ],
881+
[ '10.20.30.0/24', 3 ],
882+
[ '10.20.30.0/17', 3 ],
883+
[ '10.20.30.40/32', 4 ],
884+
]) {
885+
const query = new Packet.Resource.EDNS([ new Packet.Resource.EDNS.ECS(cidr) ]);
886+
const buf = Packet.Resource.encode(query);
887+
// Layout: name(1) type(2) class(2) ttl(4) rdlength(2) optionCode(2)
888+
// optionLength(2) → optionLength sits at offset 13. Address byte count =
889+
// optionLength - 4 (family + src prefix + scope prefix headers).
890+
const optionLength = buf.readUInt16BE(13);
891+
assert.equal(optionLength - 4, expectedOctets, `cidr ${cidr}`);
892+
}
893+
});
894+
895+
test('EDNS.ECS#encode supports IPv6 family', function() {
896+
// family=2 (IPv6), /32 prefix → 4 leading octets of the address.
897+
const ecs = Packet.Resource.EDNS.ECS('2001:db8::/32');
898+
ecs.family = 2; // factory currently hard-codes family 1; opt into IPv6
899+
const opt = new Packet.Resource.EDNS([ ecs ]);
900+
const buf = Packet.Resource.encode(opt);
901+
const parsed = Packet.Resource.decode(buf);
902+
assert.equal(parsed.rdata[0].family, 2);
903+
assert.equal(parsed.rdata[0].sourcePrefixLength, 32);
904+
// The decoder pads truncated IPv6 to 8 segments; '2001:db8' followed by 6 zero segments.
905+
assert.equal(parsed.rdata[0].ip, '2001:db8:0:0:0:0:0:0');
906+
});
907+
775908
test('Packet.parse tolerates multiple questions', function() {
776909
const request = new Packet();
777910
request.header.id = 0x9999;

0 commit comments

Comments
 (0)