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
9 changes: 5 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ 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)
- fix(packet): IPv6 `::` compression for leading-zero address #123
- fix(packet): Name decode rejects pointer cycles (RFC 1035) #124
- fix(packet): EDNS exposes extendedRcode/version/doFlag #124
- fix(packet): Header initializes ancount; AD/CD bits split from Z (RFC 4035) #124
- fix(packet): ECS encoder truncates address, adds IPv6 (RFC 7871) #124
- feat(server): PROXY protocol v1/v2 support #122

### 2.2.1 - 2026-05-25
Expand Down
27 changes: 23 additions & 4 deletions packet.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,29 @@ const BufferWriter = require('./lib/writer');

const debug = debuglog('dns2');

const toIPv6 = buffer => buffer
.map(part => (part > 0 ? part.toString(16) : '0'))
.join(':')
.replace(/\b(?:0+:){1,}/, ':');
// Canonical IPv6 text form per RFC 5952:
// - lower case hex, no leading zeros per group (handled by toString(16))
// - the longest run of >= 2 zero groups is replaced with "::"
// - on ties, the first such run is chosen
// - a single zero group is NOT compressed
const toIPv6 = buffer => {
const segments = buffer.map(part => (part > 0 ? part.toString(16) : '0'));
let bestStart = -1; let bestLen = 0;
let curStart = -1; let curLen = 0;
for (let i = 0; i < segments.length; i++) {
if (segments[i] === '0') {
if (curLen === 0) curStart = i;
curLen++;
if (curLen > bestLen) { bestLen = curLen; bestStart = curStart; }
} else {
curLen = 0;
}
}
if (bestLen < 2) return segments.join(':');
const before = segments.slice(0, bestStart).join(':');
const after = segments.slice(bestStart + bestLen).join(':');
return `${before}::${after}`;
};

const fromIPv6 = (address) => {
const digits = address.split(':');
Expand Down
22 changes: 22 additions & 0 deletions test/packet.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ test('Package#toIPv6', function() {
assert.equal(Packet.toIPv6([ 9734, 18176, 12552, 0, 0, 0, 44098, 10984 ]), '2606:4700:3108::ac42:2ae8');
});

test('Package#toIPv6 RFC 5952 — leading-zero addresses', function() {
assert.equal(Packet.toIPv6([ 0, 0, 0, 0, 0, 0, 0, 1 ]), '::1');
assert.equal(Packet.toIPv6([ 0, 0, 0, 0, 0, 0, 0, 0 ]), '::');
assert.equal(Packet.toIPv6([ 0, 0, 0, 0, 0, 0xffff, 0xc000, 0x0201 ]), '::ffff:c000:201');
});

test('Package#toIPv6 RFC 5952 — trailing-zero addresses', function() {
assert.equal(Packet.toIPv6([ 1, 0, 0, 0, 0, 0, 0, 0 ]), '1::');
assert.equal(Packet.toIPv6([ 0x2001, 0xdb8, 0, 0, 0, 0, 0, 0 ]), '2001:db8::');
});

test('Package#toIPv6 RFC 5952 — single zero group is not compressed', function() {
// §4.2.2: "::" MUST NOT be used to shorten just one 16-bit 0 field.
assert.equal(Packet.toIPv6([ 1, 0, 1, 1, 1, 1, 1, 1 ]), '1:0:1:1:1:1:1:1');
});

test('Package#toIPv6 RFC 5952 — first run wins on tie', function() {
// §4.2.3: when there is more than one run of equal maximum length,
// the first is shortened.
assert.equal(Packet.toIPv6([ 1, 0, 0, 1, 0, 0, 1, 1 ]), '1::1:0:0:1:1');
});

test('Package#fromIPv6', function() {
assert.deepEqual(Packet.fromIPv6('2a04:4e42:200::323'), [
'2a04', '4e42', '0200', '0', '0', '0', '0', '0323' ]);
Expand Down