Skip to content

Commit 19f93e4

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 19f93e4

3 files changed

Lines changed: 201 additions & 15 deletions

File tree

CHANGELOG.md

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

55
### Unreleased
66

7+
- fix(packet): Name decode rejects pointer cycles (RFC 1035)
8+
- fix(packet): EDNS exposes extendedRcode/version/doFlag; udpPayloadSize configurable (RFC 6891)
9+
- fix(packet): Header initializes ancount; AD/CD bits split from Z (RFC 4035)
10+
- fix(packet): ECS encoder truncates address to ceil(prefix/8) octets, adds IPv6 family (RFC 7871)
11+
- feat(server): PROXY protocol v1/v2 support #122
12+
713
### 2.2.1 - 2026-05-25
814

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

packet.js

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,11 @@ Packet.Header = function(header) {
240240
this.rd = 0;
241241
this.ra = 0;
242242
this.z = 0;
243+
this.ad = 0;
244+
this.cd = 0;
243245
this.rcode = 0;
244246
this.qdcount = 0;
247+
this.ancount = 0;
245248
this.nscount = 0;
246249
this.arcount = 0;
247250
for (const k in header) {
@@ -267,7 +270,10 @@ Packet.Header.parse = function(reader) {
267270
header.tc = reader.read(1);
268271
header.rd = reader.read(1);
269272
header.ra = reader.read(1);
270-
header.z = reader.read(3);
273+
// RFC 4035 §3.2.3 repurposed the second and third Z bits as AD and CD.
274+
header.z = reader.read(1);
275+
header.ad = reader.read(1);
276+
header.cd = reader.read(1);
271277
header.rcode = reader.read(4);
272278
header.qdcount = reader.read(16);
273279
header.ancount = reader.read(16);
@@ -289,7 +295,9 @@ Packet.Header.prototype.toBuffer = function(writer) {
289295
writer.write(this.tc, 1);
290296
writer.write(this.rd, 1);
291297
writer.write(this.ra, 1);
292-
writer.write(this.z, 3);
298+
writer.write(this.z, 1);
299+
writer.write(this.ad, 1);
300+
writer.write(this.cd, 1);
293301
writer.write(this.rcode, 4);
294302
writer.write(this.qdcount, 16);
295303
writer.write(this.ancount, 16);
@@ -457,11 +465,18 @@ Packet.Name = {
457465
reader = new Packet.Reader(reader);
458466
}
459467
const name = []; let o; let len = reader.read(8);
468+
// Track each pointer target we follow. A crafted packet can chain
469+
// pointers in a cycle; without this guard, decode would loop forever.
470+
const visited = new Set();
460471
while (len) {
461472
if ((len & Packet.Name.COPY) === Packet.Name.COPY) {
462473
len -= Packet.Name.COPY;
463474
len = len << 8;
464475
const pos = len + reader.read(8);
476+
if (visited.has(pos)) {
477+
throw new Error('Name decode: pointer cycle detected');
478+
}
479+
visited.add(pos);
465480
if (!o) o = reader.offset;
466481
reader.offset = pos * 8;
467482
len = reader.read(8);
@@ -740,19 +755,42 @@ Packet.Resource.SRV = {
740755
},
741756
};
742757

743-
Packet.Resource.EDNS = function(rdata) {
758+
// RFC 6891 §6.1.3 — the OPT record's TTL field carries:
759+
// bits 0- 7: extended RCODE (high byte of a 12-bit RCODE)
760+
// bits 8-15: EDNS version
761+
// bit 16: DO (DNSSEC OK)
762+
// bits 17-31: reserved Z, must be zero
763+
const ednsTtl = (extendedRcode, version, doFlag) =>
764+
(((extendedRcode & 0xff) << 24) >>> 0)
765+
| ((version & 0xff) << 16)
766+
| (doFlag ? 0x8000 : 0);
767+
768+
Packet.Resource.EDNS = function(rdata, opts = {}) {
769+
const extendedRcode = opts.extendedRcode || 0;
770+
const version = opts.version || 0;
771+
const doFlag = !!opts.doFlag;
772+
const udpPayloadSize = opts.udpPayloadSize || 512;
744773
return {
745774
type : Packet.TYPE.EDNS,
746-
class : 512, // Supported UDP Payload size
747-
ttl : 0, // Extended RCODE and flags
775+
class : udpPayloadSize,
776+
ttl : ednsTtl(extendedRcode, version, doFlag),
777+
extendedRcode,
778+
version,
779+
doFlag,
748780
rdata, // Objects of type Packet.Resource.EDNS.*
749781
};
750782
};
751783

752784
Packet.Resource.EDNS.decode = function(reader, length) {
753-
this.type = Packet.TYPE.EDNS;
754-
this.class = 512;
755-
this.ttl = 0;
785+
// When invoked through Resource.parse, this.type/class/ttl are already set
786+
// from the wire. Direct callers (e.g. unit tests) hit defaults instead.
787+
this.type = this.type ?? Packet.TYPE.EDNS;
788+
this.class = this.class ?? 512;
789+
const ttl = this.ttl ?? 0;
790+
this.ttl = ttl;
791+
this.extendedRcode = (ttl >>> 24) & 0xff;
792+
this.version = (ttl >>> 16) & 0xff;
793+
this.doFlag = !!(ttl & 0x8000);
756794
this.rdata = [];
757795

758796
while (length) {
@@ -845,16 +883,47 @@ Packet.Resource.EDNS.ECS.decode = function(reader, length) {
845883
};
846884

847885
Packet.Resource.EDNS.ECS.encode = function(record, writer) {
848-
const ip = record.ip.split('.').map(s => parseInt(s));
886+
// RFC 7871 §6: the ADDRESS field carries only the leftmost
887+
// ceil(sourcePrefixLength / 8) octets.
888+
const octets = Math.ceil(record.sourcePrefixLength / 8);
849889
writer.write(record.family, 16);
850890
writer.write(record.sourcePrefixLength, 8);
851891
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);
892+
let bytes;
893+
if (record.family === 1) {
894+
bytes = record.ip.split('.').map(s => parseInt(s, 10) || 0);
895+
} else if (record.family === 2) {
896+
bytes = expandIPv6ToBytes(record.ip);
897+
} else {
898+
throw new Error(`EDNS.ECS encode: unsupported family ${record.family}`);
899+
}
900+
for (let i = 0; i < octets; i++) {
901+
writer.write(bytes[i] || 0, 8);
902+
}
856903
};
857904

905+
// Expand a (possibly compressed) IPv6 text address into a 16-byte array.
906+
function expandIPv6ToBytes(address) {
907+
let head, tail;
908+
const idx = address.indexOf('::');
909+
if (idx === -1) {
910+
head = address.split(':');
911+
tail = [];
912+
} else {
913+
head = address.slice(0, idx).split(':').filter(Boolean);
914+
tail = address.slice(idx + 2).split(':').filter(Boolean);
915+
}
916+
const missing = 8 - head.length - tail.length;
917+
const groups = [ ...head, ...new Array(missing).fill('0'), ...tail ];
918+
const out = new Array(16).fill(0);
919+
for (let g = 0; g < 8; g++) {
920+
const n = parseInt(groups[g], 16) || 0;
921+
out[g * 2] = (n >> 8) & 0xff;
922+
out[g * 2 + 1] = n & 0xff;
923+
}
924+
return out;
925+
}
926+
858927
Packet.Resource.CAA = {
859928
encode: function(record, writer) {
860929
writer = writer || new Packet.Writer();

test/packet.js

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,11 +221,13 @@ test('EDNS.ECS#encode', function() {
221221
new Packet.Resource.EDNS.ECS('10.11.12.13/24'),
222222
]);
223223

224+
// RFC 7871 §6: ADDRESS field is only ceil(sourcePrefixLength/8) octets,
225+
// so /24 writes 3 address bytes (10.11.12), not 4.
224226
const b = Packet.Resource.encode(query);
225227
assert.deepEqual(b, Buffer.from([
226228
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 ]));
229+
0x00, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x07, 0x00,
230+
0x01, 0x18, 0x00, 0x0a, 0x0b, 0x0c ]));
229231
});
230232

231233
test('EDNS#decode', function() {
@@ -772,6 +774,115 @@ test('Packet.uuid exercises the full 16-bit range with high diversity', function
772774
}
773775
});
774776

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

0 commit comments

Comments
 (0)