diff --git a/CHANGELOG.md b/CHANGELOG.md index 3917ab5..d510084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packet.js b/packet.js index 7a55695..3658086 100644 --- a/packet.js +++ b/packet.js @@ -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(':'); diff --git a/test/packet.js b/test/packet.js index 5f49675..b8b8d09 100644 --- a/test/packet.js +++ b/test/packet.js @@ -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' ]);