From 08751359b4f27698cd0a689de2f11ef9e8189ffa Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Mon, 25 May 2026 17:58:48 -0700 Subject: [PATCH] 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) --- CHANGELOG.md | 6 +++ packet.js | 95 ++++++++++++++++++++++++++++++++++------ test/packet.js | 115 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 201 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cac0ee7..3917ab5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Unreleased +- 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) +- feat(server): PROXY protocol v1/v2 support #122 + ### 2.2.1 - 2026-05-25 - fix(packet): use crypto.randomInt for Packet.uuid (RFC 5452) diff --git a/packet.js b/packet.js index cdc1f15..7a55695 100644 --- a/packet.js +++ b/packet.js @@ -240,8 +240,11 @@ Packet.Header = function(header) { this.rd = 0; this.ra = 0; this.z = 0; + this.ad = 0; + this.cd = 0; this.rcode = 0; this.qdcount = 0; + this.ancount = 0; this.nscount = 0; this.arcount = 0; for (const k in header) { @@ -267,7 +270,10 @@ Packet.Header.parse = function(reader) { header.tc = reader.read(1); header.rd = reader.read(1); header.ra = reader.read(1); - header.z = reader.read(3); + // RFC 4035 §3.2.3 repurposed the second and third Z bits as AD and CD. + header.z = reader.read(1); + header.ad = reader.read(1); + header.cd = reader.read(1); header.rcode = reader.read(4); header.qdcount = reader.read(16); header.ancount = reader.read(16); @@ -289,7 +295,9 @@ Packet.Header.prototype.toBuffer = function(writer) { writer.write(this.tc, 1); writer.write(this.rd, 1); writer.write(this.ra, 1); - writer.write(this.z, 3); + writer.write(this.z, 1); + writer.write(this.ad, 1); + writer.write(this.cd, 1); writer.write(this.rcode, 4); writer.write(this.qdcount, 16); writer.write(this.ancount, 16); @@ -457,11 +465,18 @@ Packet.Name = { reader = new Packet.Reader(reader); } const name = []; let o; let len = reader.read(8); + // Track each pointer target we follow. A crafted packet can chain + // pointers in a cycle; without this guard, decode would loop forever. + const visited = new Set(); while (len) { if ((len & Packet.Name.COPY) === Packet.Name.COPY) { len -= Packet.Name.COPY; len = len << 8; const pos = len + reader.read(8); + if (visited.has(pos)) { + throw new Error('Name decode: pointer cycle detected'); + } + visited.add(pos); if (!o) o = reader.offset; reader.offset = pos * 8; len = reader.read(8); @@ -740,19 +755,42 @@ Packet.Resource.SRV = { }, }; -Packet.Resource.EDNS = function(rdata) { +// RFC 6891 §6.1.3 — the OPT record's TTL field carries: +// bits 0- 7: extended RCODE (high byte of a 12-bit RCODE) +// bits 8-15: EDNS version +// bit 16: DO (DNSSEC OK) +// bits 17-31: reserved Z, must be zero +const ednsTtl = (extendedRcode, version, doFlag) => + (((extendedRcode & 0xff) << 24) >>> 0) + | ((version & 0xff) << 16) + | (doFlag ? 0x8000 : 0); + +Packet.Resource.EDNS = function(rdata, opts = {}) { + const extendedRcode = opts.extendedRcode || 0; + const version = opts.version || 0; + const doFlag = !!opts.doFlag; + const udpPayloadSize = opts.udpPayloadSize || 512; return { type : Packet.TYPE.EDNS, - class : 512, // Supported UDP Payload size - ttl : 0, // Extended RCODE and flags + class : udpPayloadSize, + ttl : ednsTtl(extendedRcode, version, doFlag), + extendedRcode, + version, + doFlag, rdata, // Objects of type Packet.Resource.EDNS.* }; }; Packet.Resource.EDNS.decode = function(reader, length) { - this.type = Packet.TYPE.EDNS; - this.class = 512; - this.ttl = 0; + // When invoked through Resource.parse, this.type/class/ttl are already set + // from the wire. Direct callers (e.g. unit tests) hit defaults instead. + this.type = this.type ?? Packet.TYPE.EDNS; + this.class = this.class ?? 512; + const ttl = this.ttl ?? 0; + this.ttl = ttl; + this.extendedRcode = (ttl >>> 24) & 0xff; + this.version = (ttl >>> 16) & 0xff; + this.doFlag = !!(ttl & 0x8000); this.rdata = []; while (length) { @@ -845,16 +883,47 @@ Packet.Resource.EDNS.ECS.decode = function(reader, length) { }; Packet.Resource.EDNS.ECS.encode = function(record, writer) { - const ip = record.ip.split('.').map(s => parseInt(s)); + // RFC 7871 §6: the ADDRESS field carries only the leftmost + // ceil(sourcePrefixLength / 8) octets. + const octets = Math.ceil(record.sourcePrefixLength / 8); writer.write(record.family, 16); writer.write(record.sourcePrefixLength, 8); writer.write(record.scopePrefixLength, 8); - writer.write(ip[0], 8); - writer.write(ip[1], 8); - writer.write(ip[2], 8); - writer.write(ip[3], 8); + let bytes; + if (record.family === 1) { + bytes = record.ip.split('.').map(s => parseInt(s, 10) || 0); + } else if (record.family === 2) { + bytes = expandIPv6ToBytes(record.ip); + } else { + throw new Error(`EDNS.ECS encode: unsupported family ${record.family}`); + } + for (let i = 0; i < octets; i++) { + writer.write(bytes[i] || 0, 8); + } }; +// Expand a (possibly compressed) IPv6 text address into a 16-byte array. +function expandIPv6ToBytes(address) { + let head, tail; + const idx = address.indexOf('::'); + if (idx === -1) { + head = address.split(':'); + tail = []; + } else { + head = address.slice(0, idx).split(':').filter(Boolean); + tail = address.slice(idx + 2).split(':').filter(Boolean); + } + const missing = 8 - head.length - tail.length; + const groups = [ ...head, ...new Array(missing).fill('0'), ...tail ]; + const out = new Array(16).fill(0); + for (let g = 0; g < 8; g++) { + const n = parseInt(groups[g], 16) || 0; + out[g * 2] = (n >> 8) & 0xff; + out[g * 2 + 1] = n & 0xff; + } + return out; +} + Packet.Resource.CAA = { encode: function(record, writer) { writer = writer || new Packet.Writer(); diff --git a/test/packet.js b/test/packet.js index 8410a86..5f49675 100644 --- a/test/packet.js +++ b/test/packet.js @@ -221,11 +221,13 @@ test('EDNS.ECS#encode', function() { new Packet.Resource.EDNS.ECS('10.11.12.13/24'), ]); + // RFC 7871 §6: ADDRESS field is only ceil(sourcePrefixLength/8) octets, + // so /24 writes 3 address bytes (10.11.12), not 4. const b = Packet.Resource.encode(query); assert.deepEqual(b, Buffer.from([ 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x08, 0x00, - 0x01, 0x18, 0x00, 0x0a, 0x0b, 0x0c, 0x0d ])); + 0x00, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x07, 0x00, + 0x01, 0x18, 0x00, 0x0a, 0x0b, 0x0c ])); }); test('EDNS#decode', function() { @@ -772,6 +774,115 @@ test('Packet.uuid exercises the full 16-bit range with high diversity', function } }); +test('Name decode rejects a pointer cycle (no infinite loop)', function() { + // Hand-built packet header (12 bytes) followed by a name that points to + // itself: byte 12 = 0xC0 (pointer high), byte 13 = 0x0C (offset = 12). + // Without cycle detection this would loop forever. + const buf = Buffer.alloc(14); + buf[12] = 0xC0; + buf[13] = 0x0C; + const reader = new Packet.Reader(buf); + reader.offset = 8 * 12; + assert.throws(() => Packet.Name.decode(reader), /pointer cycle/); +}); + +test('Name decode rejects a two-step pointer cycle', function() { + // Two pointers pointing at each other: bytes 12-13 = C0 0E, bytes 14-15 = C0 0C. + const buf = Buffer.alloc(16); + buf[12] = 0xC0; buf[13] = 0x0E; + buf[14] = 0xC0; buf[15] = 0x0C; + const reader = new Packet.Reader(buf); + reader.offset = 8 * 12; + assert.throws(() => Packet.Name.decode(reader), /pointer cycle/); +}); + +test('Header default constructor initializes ancount/ad/cd', function() { + const header = new Packet.Header(); + assert.equal(header.ancount, 0); + assert.equal(header.ad, 0); + assert.equal(header.cd, 0); +}); + +test('Header#parse exposes AD and CD bits (RFC 4035)', function() { + // Second header word with AD=1, CD=1, all other flags zero. + // Layout: qr(1) opcode(4) aa(1) tc(1) rd(1) ra(1) z(1) ad(1) cd(1) rcode(4) + // bits : 0 0000 0 0 0 0 0 1 1 0000 => 0000 0000 0011 0000 = 0x0030 + const buf = Buffer.from([ + 0x00, 0x01, // id + 0x00, 0x30, // flags: AD=1, CD=1 + 0x00, 0x00, 0x00, 0x00, // counts + 0x00, 0x00, 0x00, 0x00, + ]); + const header = Packet.Header.parse(buf); + assert.equal(header.z, 0); + assert.equal(header.ad, 1); + assert.equal(header.cd, 1); +}); + +test('Header#toBuffer round-trips AD and CD bits', function() { + const header = new Packet.Header({ id: 0x4242, ad: 1, cd: 1 }); + const parsed = Packet.Header.parse(header.toBuffer()); + assert.equal(parsed.id, 0x4242); + assert.equal(parsed.ad, 1); + assert.equal(parsed.cd, 1); + assert.equal(parsed.z, 0); +}); + +test('EDNS exposes extendedRcode / version / doFlag', function() { + const opt = new Packet.Resource.EDNS([], { extendedRcode: 16, version: 0, doFlag: true }); + assert.equal(opt.extendedRcode, 16); + assert.equal(opt.version, 0); + assert.equal(opt.doFlag, true); + // ttl wire encoding: ext rcode in top byte, DO at bit 15 of low half. + assert.equal(opt.ttl, (16 << 24) | 0x8000); +}); + +test('EDNS round-trip preserves DO bit and extended RCODE', function() { + const opt = new Packet.Resource.EDNS([], { extendedRcode: 23, version: 0, doFlag: true }); + const parsed = Packet.Resource.decode(Packet.Resource.encode(opt)); + assert.equal(parsed.extendedRcode, 23); + assert.equal(parsed.doFlag, true); +}); + +test('EDNS udpPayloadSize is configurable (RFC 6891 §6.2.3)', function() { + const opt = new Packet.Resource.EDNS([], { udpPayloadSize: 4096 }); + assert.equal(opt.class, 4096); + const parsed = Packet.Resource.decode(Packet.Resource.encode(opt)); + assert.equal(parsed.class, 4096); +}); + +test('EDNS.ECS#encode truncates IPv4 address to prefix length (RFC 7871)', function() { + // /8 → 1 octet, /17 → 3 octets (ceil) + for (const [ cidr, expectedOctets ] of [ + [ '10.0.0.0/8', 1 ], + [ '10.20.0.0/16', 2 ], + [ '10.20.30.0/24', 3 ], + [ '10.20.30.0/17', 3 ], + [ '10.20.30.40/32', 4 ], + ]) { + const query = new Packet.Resource.EDNS([ new Packet.Resource.EDNS.ECS(cidr) ]); + const buf = Packet.Resource.encode(query); + // Layout: name(1) type(2) class(2) ttl(4) rdlength(2) optionCode(2) + // optionLength(2) → optionLength sits at offset 13. Address byte count = + // optionLength - 4 (family + src prefix + scope prefix headers). + const optionLength = buf.readUInt16BE(13); + assert.equal(optionLength - 4, expectedOctets, `cidr ${cidr}`); + } +}); + +test('EDNS.ECS#encode supports IPv6 family', function() { + // family=2 (IPv6), /32 prefix → 4 leading octets of the address. + const ecs = Packet.Resource.EDNS.ECS('2001:db8::/32'); + ecs.family = 2; // factory currently hard-codes family 1; opt into IPv6 + const opt = new Packet.Resource.EDNS([ ecs ]); + const buf = Packet.Resource.encode(opt); + const parsed = Packet.Resource.decode(buf); + assert.equal(parsed.rdata[0].family, 2); + assert.equal(parsed.rdata[0].sourcePrefixLength, 32); + // The decoder pads truncated IPv6 to 8 segments; '2001:db8' followed by 6 zero segments. + assert.equal(parsed.rdata[0].ip, '2001:db8:0:0:0:0:0:0'); +}); + test('Packet.parse tolerates multiple questions', function() { const request = new Packet(); request.header.id = 0x9999;