Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
95 changes: 82 additions & 13 deletions packet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
115 changes: 113 additions & 2 deletions test/packet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down