diff options
| author | github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> | 2026-05-08 03:46:02 +0000 |
|---|---|---|
| committer | github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> | 2026-05-08 03:46:02 +0000 |
| commit | f08627448d96ce07cac829c4192349c5077d2dfc (patch) | |
| tree | 15af7840a3d104095136285673e1763a103c19ab | |
| parent | bump dependencies (diff) | |
| download | wireguard-config-f08627448d96ce07cac829c4192349c5077d2dfc.tar.gz wireguard-config-f08627448d96ce07cac829c4192349c5077d2dfc.zip | |
bump dependencies
| -rw-r--r-- | Gemfile.lock | 2 | ||||
| -rw-r--r-- | assets/js/bundle.js | 747 | ||||
| -rw-r--r-- | package-lock.json | 14 |
3 files changed, 560 insertions, 203 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index f7cb9f9..f3edf9d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -67,7 +67,7 @@ GEM jekyll jekyll-watch (2.2.1) listen (~> 3.0) - json (2.19.4) + json (2.19.5) kramdown (2.5.2) rexml (>= 3.4.4) kramdown-parser-gfm (1.1.0) diff --git a/assets/js/bundle.js b/assets/js/bundle.js index d0138ed..da8f679 100644 --- a/assets/js/bundle.js +++ b/assets/js/bundle.js @@ -16,9 +16,11 @@ exports.AddressError = AddressError; Object.defineProperty(exports, "__esModule", { value: true }); exports.isInSubnet = isInSubnet; exports.isCorrect = isCorrect; +exports.prefixLengthFromMask = prefixLengthFromMask; exports.numberToPaddedHex = numberToPaddedHex; exports.stringToPaddedHex = stringToPaddedHex; exports.testBit = testBit; +const address_error_1 = require("./address-error"); function isInSubnet(address) { if (this.subnetMask < address.subnetMask) { return false; @@ -39,6 +41,25 @@ function isCorrect(defaultBits) { return this.parsedSubnet === String(this.subnetMask); }; } +/** + * Returns the prefix length (number of leading 1 bits) of a contiguous + * subnet mask. Throws `AddressError` if the mask is non-contiguous (e.g. + * `255.0.255.0`). + */ +function prefixLengthFromMask(value, totalBits) { + const binary = value.toString(2).padStart(totalBits, '0'); + if (binary.length > totalBits) { + throw new address_error_1.AddressError('Invalid subnet mask.'); + } + const firstZero = binary.indexOf('0'); + if (firstZero === -1) { + return totalBits; + } + if (binary.slice(firstZero).includes('1')) { + throw new address_error_1.AddressError('Invalid subnet mask.'); + } + return firstZero; +} function numberToPaddedHex(number) { return number.toString(16).padStart(2, '0'); } @@ -58,7 +79,7 @@ function testBit(binaryValue, position) { return binaryValue.substring(positionInString, positionInString + 1) === '1'; } -},{}],3:[function(require,module,exports){ +},{"./address-error":1}],3:[function(require,module,exports){ "use strict"; /* eslint-disable no-param-reassign */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { @@ -89,9 +110,9 @@ exports.Address4 = void 0; const common = __importStar(require("./common")); const constants = __importStar(require("./v4/constants")); const address_error_1 = require("./address-error"); +const isCorrect4 = common.isCorrect(constants.BITS); /** * Represents an IPv4 address - * @class Address4 * @param {string} address - An IPv4 address string */ class Address4 { @@ -104,15 +125,11 @@ class Address4 { this.v4 = true; /** * Returns true if the address is correct, false otherwise - * @memberof Address4 - * @instance * @returns {Boolean} */ - this.isCorrect = common.isCorrect(constants.BITS); + this.isCorrect = isCorrect4; /** * Returns true if the given address is in the subnet of the current address - * @memberof Address4 - * @instance * @returns {boolean} */ this.isInSubnet = common.isInSubnet; @@ -130,6 +147,13 @@ class Address4 { this.addressMinusSuffix = address; this.parsedAddress = this.parse(address); } + /** + * Returns true if the given string is a valid IPv4 address (with optional + * CIDR subnet), false otherwise. Host bits in the subnet portion are + * allowed (e.g. `192.168.1.5/24` is valid); for strict network-address + * validation compare `correctForm()` to `startAddress().correctForm()`, + * or use `networkForm()`. + */ static isValid(address) { try { // eslint-disable-next-line no-new @@ -140,8 +164,11 @@ class Address4 { return false; } } - /* - * Parses a v4 address + /** + * Parses an IPv4 address string into its four octet groups and stores the + * result on `this.parsedAddress`. Called automatically by the constructor; + * you typically don't need to call it directly. Throws `AddressError` if + * the input is not a valid IPv4 address. */ parse(address) { const groups = address.split('.'); @@ -151,45 +178,110 @@ class Address4 { return groups; } /** - * Returns the correct form of an address - * @memberof Address4 - * @instance - * @returns {String} + * Returns the address in correct form: octets joined with `.` and any + * leading zeros stripped (e.g. `192.168.1.1`). For IPv4 this matches the + * canonical dotted-decimal representation. */ correctForm() { return this.parsedAddress.map((part) => parseInt(part, 10)).join('.'); } /** - * Converts a hex string to an IPv4 address object - * @memberof Address4 - * @static + * Construct an `Address4` from an address and a dotted-decimal subnet + * mask given as separate strings (e.g. as returned by Node's + * `os.networkInterfaces()`). Throws `AddressError` if the mask is + * non-contiguous (e.g. `255.0.255.0`). + * @example + * var address = Address4.fromAddressAndMask('192.168.1.1', '255.255.255.0'); + * address.subnetMask; // 24 + */ + static fromAddressAndMask(address, mask) { + const bits = common.prefixLengthFromMask(new Address4(mask).bigInt(), constants.BITS); + return new Address4(`${address}/${bits}`); + } + /** + * Construct an `Address4` from an address and a Cisco-style wildcard mask + * given as separate strings (e.g. `0.0.0.255` for a `/24`). The wildcard + * mask is the bitwise inverse of the subnet mask. Throws `AddressError` + * if the mask is non-contiguous (e.g. `0.255.0.255`). + * @example + * var address = Address4.fromAddressAndWildcardMask('10.0.0.1', '0.0.0.255'); + * address.subnetMask; // 24 + */ + static fromAddressAndWildcardMask(address, wildcardMask) { + const wildcard = new Address4(wildcardMask).bigInt(); + const allOnes = (BigInt(1) << BigInt(constants.BITS)) - BigInt(1); + // eslint-disable-next-line no-bitwise + const mask = wildcard ^ allOnes; + const bits = common.prefixLengthFromMask(mask, constants.BITS); + return new Address4(`${address}/${bits}`); + } + /** + * Construct an `Address4` from a wildcard pattern with trailing `*` + * octets. The number of trailing wildcards determines the prefix + * length: each `*` represents 8 bits. + * + * Only trailing whole-octet wildcards are supported. Partial-octet + * wildcards (e.g. `192.168.0.1*`) and interior wildcards (e.g. + * `192.*.0.1`) throw `AddressError`. + * @example + * Address4.fromWildcard('192.168.0.*').subnet; // '/24' + * Address4.fromWildcard('192.168.*.*').subnet; // '/16' + * Address4.fromWildcard('*.*.*.*').subnet; // '/0' + */ + static fromWildcard(input) { + const groups = input.split('.'); + if (groups.length !== constants.GROUPS) { + throw new address_error_1.AddressError('Wildcard pattern must have 4 octets'); + } + let firstWildcard = -1; + for (let i = 0; i < groups.length; i++) { + if (groups[i] === '*') { + if (firstWildcard === -1) { + firstWildcard = i; + } + } + else if (firstWildcard !== -1) { + throw new address_error_1.AddressError('Wildcard `*` must only appear in trailing octets (e.g. `192.168.0.*`)'); + } + } + const trailing = firstWildcard === -1 ? 0 : groups.length - firstWildcard; + const replaced = groups.map((g) => (g === '*' ? '0' : g)); + const subnetBits = constants.BITS - trailing * 8; + return new Address4(`${replaced.join('.')}/${subnetBits}`); + } + /** + * Converts a hex string to an IPv4 address object. Accepts 8 hex digits + * with optional `:` separators (e.g. `'7f000001'` or `'7f:00:00:01'`). + * Throws `AddressError` for any other length or for non-hex characters. * @param {string} hex - a hex string to convert * @returns {Address4} */ static fromHex(hex) { - const padded = hex.replace(/:/g, '').padStart(8, '0'); + const stripped = hex.replace(/:/g, ''); + if (!/^[0-9a-fA-F]{8}$/.test(stripped)) { + throw new address_error_1.AddressError('IPv4 hex must be exactly 8 hex digits'); + } const groups = []; - let i; - for (i = 0; i < 8; i += 2) { - const h = padded.slice(i, i + 2); - groups.push(parseInt(h, 16)); + for (let i = 0; i < 8; i += 2) { + groups.push(parseInt(stripped.slice(i, i + 2), 16)); } return new Address4(groups.join('.')); } /** - * Converts an integer into a IPv4 address object - * @memberof Address4 - * @static + * Converts an integer into a IPv4 address object. The integer must be a + * non-negative safe integer in the range `[0, 2**32 - 1]`; otherwise + * `AddressError` is thrown. * @param {integer} integer - a number to convert * @returns {Address4} */ static fromInteger(integer) { - return Address4.fromHex(integer.toString(16)); + if (!Number.isInteger(integer) || integer < 0 || integer > 0xffffffff) { + throw new address_error_1.AddressError('IPv4 integer must be in the range 0 to 2**32 - 1'); + } + return Address4.fromHex(integer.toString(16).padStart(8, '0')); } /** * Return an address from in-addr.arpa form - * @memberof Address4 - * @static * @param {string} arpaFormAddress - an 'in-addr.arpa' form ipv4 address * @returns {Adress4} * @example @@ -204,17 +296,15 @@ class Address4 { } /** * Converts an IPv4 address object to a hex string - * @memberof Address4 - * @instance * @returns {String} */ toHex() { return this.parsedAddress.map((part) => common.stringToPaddedHex(part)).join(':'); } /** - * Converts an IPv4 address object to an array of bytes - * @memberof Address4 - * @instance + * Converts an IPv4 address object to an array of bytes. + * + * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toArray())`. * @returns {Array} */ toArray() { @@ -222,8 +312,6 @@ class Address4 { } /** * Converts an IPv4 address object to an IPv6 address group - * @memberof Address4 - * @instance * @returns {String} */ toGroup6() { @@ -236,8 +324,6 @@ class Address4 { } /** * Returns the address as a `bigint` - * @memberof Address4 - * @instance * @returns {bigint} */ bigInt() { @@ -245,8 +331,6 @@ class Address4 { } /** * Helper function getting start address. - * @memberof Address4 - * @instance * @returns {bigint} */ _startAddress() { @@ -255,8 +339,6 @@ class Address4 { /** * The first address in the range given by this address' subnet. * Often referred to as the Network Address. - * @memberof Address4 - * @instance * @returns {Address4} */ startAddress() { @@ -265,8 +347,6 @@ class Address4 { /** * The first host address in the range given by this address's subnet ie * the first address after the Network Address - * @memberof Address4 - * @instance * @returns {Address4} */ startAddressExclusive() { @@ -275,8 +355,6 @@ class Address4 { } /** * Helper function getting end address. - * @memberof Address4 - * @instance * @returns {bigint} */ _endAddress() { @@ -285,8 +363,6 @@ class Address4 { /** * The last address in the range given by this address' subnet * Often referred to as the Broadcast - * @memberof Address4 - * @instance * @returns {Address4} */ endAddress() { @@ -295,8 +371,6 @@ class Address4 { /** * The last host address in the range given by this address's subnet ie * the last address prior to the Broadcast Address - * @memberof Address4 - * @instance * @returns {Address4} */ endAddressExclusive() { @@ -304,19 +378,47 @@ class Address4 { return Address4.fromBigInt(this._endAddress() - adjust); } /** - * Converts a BigInt to a v4 address object - * @memberof Address4 - * @static + * The dotted-decimal form of the subnet mask, e.g. `255.255.240.0` for + * a `/20`. Returns an `Address4`; call `.correctForm()` for the string. + * @returns {Address4} + */ + subnetMaskAddress() { + return Address4.fromBigInt(BigInt(`0b${'1'.repeat(this.subnetMask)}${'0'.repeat(constants.BITS - this.subnetMask)}`)); + } + /** + * The Cisco-style wildcard mask, e.g. `0.0.0.255` for a `/24`. This is + * the bitwise inverse of `subnetMaskAddress()`. Returns an `Address4`; + * call `.correctForm()` for the string. + * @returns {Address4} + */ + wildcardMask() { + return Address4.fromBigInt(BigInt(`0b${'0'.repeat(this.subnetMask)}${'1'.repeat(constants.BITS - this.subnetMask)}`)); + } + /** + * The network address in CIDR string form, e.g. `192.168.1.0/24` for + * `192.168.1.5/24`. For an address with no explicit subnet the prefix is + * `/32`, e.g. `networkForm()` on `192.168.1.5` returns `192.168.1.5/32`. + * @returns {string} + */ + networkForm() { + return `${this.startAddress().correctForm()}/${this.subnetMask}`; + } + /** + * Converts a BigInt to a v4 address object. The value must be in the + * range `[0, 2**32 - 1]`; otherwise `AddressError` is thrown. * @param {bigint} bigInt - a BigInt to convert * @returns {Address4} */ static fromBigInt(bigInt) { - return Address4.fromHex(bigInt.toString(16)); + if (bigInt < 0n || bigInt > 0xffffffffn) { + throw new address_error_1.AddressError('IPv4 BigInt must be in the range 0 to 2**32 - 1'); + } + return Address4.fromHex(bigInt.toString(16).padStart(8, '0')); } /** - * Convert a byte array to an Address4 object - * @memberof Address4 - * @static + * Convert a byte array to an Address4 object. + * + * To convert from a Node.js `Buffer`, spread it: `Address4.fromByteArray([...buf])`. * @param {Array<number>} bytes - an array of 4 bytes (0-255) * @returns {Address4} */ @@ -334,8 +436,6 @@ class Address4 { } /** * Convert an unsigned byte array to an Address4 object - * @memberof Address4 - * @static * @param {Array<number>} bytes - an array of 4 unsigned bytes (0-255) * @returns {Address4} */ @@ -349,8 +449,6 @@ class Address4 { /** * Returns the first n bits of the address, defaulting to the * subnet mask - * @memberof Address4 - * @instance * @returns {String} */ mask(mask) { @@ -361,8 +459,6 @@ class Address4 { } /** * Returns the bits in the given range as a base-2 string - * @memberof Address4 - * @instance * @returns {string} */ getBitsBase2(start, end) { @@ -370,10 +466,8 @@ class Address4 { } /** * Return the reversed ip6.arpa form of the address - * @memberof Address4 * @param {Object} options * @param {boolean} options.omitSuffix - omit the "in-addr.arpa" suffix - * @instance * @returns {String} */ reverseForm(options) { @@ -388,21 +482,62 @@ class Address4 { } /** * Returns true if the given address is a multicast address - * @memberof Address4 - * @instance * @returns {boolean} */ isMulticast() { - return this.isInSubnet(new Address4('224.0.0.0/4')); + return this.isInSubnet(MULTICAST_V4); + } + /** + * Returns true if the address is in one of the [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private address ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`). + * @returns {boolean} + */ + isPrivate() { + return PRIVATE_V4.some((subnet) => this.isInSubnet(subnet)); + } + /** + * Returns true if the address is in the loopback range `127.0.0.0/8` ([RFC 1122](https://datatracker.ietf.org/doc/html/rfc1122)). + * @returns {boolean} + */ + isLoopback() { + return this.isInSubnet(LOOPBACK_V4); + } + /** + * Returns true if the address is in the link-local range `169.254.0.0/16` ([RFC 3927](https://datatracker.ietf.org/doc/html/rfc3927)). + * @returns {boolean} + */ + isLinkLocal() { + return this.isInSubnet(LINK_LOCAL_V4); + } + /** + * Returns true if the address is the unspecified address `0.0.0.0`. + * @returns {boolean} + */ + isUnspecified() { + return this.isInSubnet(UNSPECIFIED_V4); + } + /** + * Returns true if the address is the limited broadcast address `255.255.255.255` ([RFC 919](https://datatracker.ietf.org/doc/html/rfc919)). + * @returns {boolean} + */ + isBroadcast() { + return this.isInSubnet(BROADCAST_V4); + } + /** + * Returns true if the address is in the carrier-grade NAT range `100.64.0.0/10` ([RFC 6598](https://datatracker.ietf.org/doc/html/rfc6598)). + * @returns {boolean} + */ + isCGNAT() { + return this.isInSubnet(CGNAT_V4); } /** * Returns a zero-padded base-2 string representation of the address - * @memberof Address4 - * @instance * @returns {string} */ binaryZeroPad() { - return this.bigInt().toString(2).padStart(constants.BITS, '0'); + if (this._binaryZeroPad === undefined) { + this._binaryZeroPad = this.bigInt().toString(2).padStart(constants.BITS, '0'); + } + return this._binaryZeroPad; } /** * Groups an IPv4 address for inclusion at the end of an IPv6 address @@ -418,6 +553,17 @@ class Address4 { } } exports.Address4 = Address4; +const MULTICAST_V4 = new Address4('224.0.0.0/4'); +const PRIVATE_V4 = [ + new Address4('10.0.0.0/8'), + new Address4('172.16.0.0/12'), + new Address4('192.168.0.0/16'), +]; +const LOOPBACK_V4 = new Address4('127.0.0.0/8'); +const LINK_LOCAL_V4 = new Address4('169.254.0.0/16'); +const UNSPECIFIED_V4 = new Address4('0.0.0.0/32'); +const BROADCAST_V4 = new Address4('255.255.255.255/32'); +const CGNAT_V4 = new Address4('100.64.0.0/10'); },{"./address-error":1,"./common":2,"./v4/constants":5}],4:[function(require,module,exports){ "use strict"; @@ -456,6 +602,7 @@ const ipv4_1 = require("./ipv4"); const regular_expressions_1 = require("./v6/regular-expressions"); const address_error_1 = require("./address-error"); const common_1 = require("./common"); +const isCorrect6 = common.isCorrect(constants6.BITS); function assert(condition) { if (!condition) { throw new Error('Assertion failed.'); @@ -499,7 +646,6 @@ function unsignByte(b) { } /** * Represents an IPv6 address - * @class Address6 * @param {string} address - An IPv6 address string * @param {number} [groups=8] - How many octets to parse * @example @@ -516,18 +662,14 @@ class Address6 { // #region Attributes /** * Returns true if the given address is in the subnet of the current address - * @memberof Address6 - * @instance * @returns {boolean} */ this.isInSubnet = common.isInSubnet; /** * Returns true if the address is correct, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ - this.isCorrect = common.isCorrect(constants6.BITS); + this.isCorrect = isCorrect6; if (optionalGroups === undefined) { this.groups = constants6.GROUPS; } @@ -558,6 +700,13 @@ class Address6 { this.addressMinusSuffix = address; this.parsedAddress = this.parse(this.addressMinusSuffix); } + /** + * Returns true if the given string is a valid IPv6 address (with optional + * CIDR subnet and zone identifier), false otherwise. Host bits in the + * subnet portion are allowed (e.g. `2001:db8::1/32` is valid); for strict + * network-address validation compare `correctForm()` to + * `startAddress().correctForm()`, or use `networkForm()`. + */ static isValid(address) { try { // eslint-disable-next-line no-new @@ -569,9 +718,8 @@ class Address6 { } } /** - * Convert a BigInt to a v6 address object - * @memberof Address6 - * @static + * Convert a BigInt to a v6 address object. The value must be in the + * range `[0, 2**128 - 1]`; otherwise `AddressError` is thrown. * @param {bigint} bigInt - a BigInt to convert * @returns {Address6} * @example @@ -580,19 +728,21 @@ class Address6 { * address.correctForm(); // '::e8:d4a5:1000' */ static fromBigInt(bigInt) { + if (bigInt < 0n || bigInt > (1n << BigInt(constants6.BITS)) - 1n) { + throw new address_error_1.AddressError('IPv6 BigInt must be in the range 0 to 2**128 - 1'); + } const hex = bigInt.toString(16).padStart(32, '0'); const groups = []; - let i; - for (i = 0; i < constants6.GROUPS; i++) { + for (let i = 0; i < constants6.GROUPS; i++) { groups.push(hex.slice(i * 4, (i + 1) * 4)); } return new Address6(groups.join(':')); } /** - * Convert a URL (with optional port number) to an address object - * @memberof Address6 - * @static - * @param {string} url - a URL with optional port number + * Parse a URL (with optional bracketed host and port) into an address and + * port. Returns either `{ address, port }` on success or + * `{ error, address: null, port: null }` if the URL could not be parsed. + * Ports are returned as numbers (or `null` if absent or out of range). * @example * var addressAndPort = Address6.fromURL('http://[ffff::]:8080/foo/'); * addressAndPort.address.correctForm(); // 'ffff::' @@ -652,9 +802,91 @@ class Address6 { }; } /** + * Construct an `Address6` from an address and a hex subnet mask given as + * separate strings (e.g. as returned by Node's `os.networkInterfaces()`). + * Throws `AddressError` if the mask is non-contiguous (e.g. + * `ffff::ffff`). + * @example + * var address = Address6.fromAddressAndMask('fe80::1', 'ffff:ffff:ffff:ffff::'); + * address.subnetMask; // 64 + */ + static fromAddressAndMask(address, mask) { + const bits = common.prefixLengthFromMask(new Address6(mask).bigInt(), constants6.BITS); + return new Address6(`${address}/${bits}`); + } + /** + * Construct an `Address6` from an address and a Cisco-style wildcard mask + * given as separate strings (e.g. `::ffff:ffff:ffff:ffff` for a `/64`). + * The wildcard mask is the bitwise inverse of the subnet mask. Throws + * `AddressError` if the mask is non-contiguous. + * @example + * var address = Address6.fromAddressAndWildcardMask('fe80::1', '::ffff:ffff:ffff:ffff'); + * address.subnetMask; // 64 + */ + static fromAddressAndWildcardMask(address, wildcardMask) { + const wildcard = new Address6(wildcardMask).bigInt(); + const allOnes = (BigInt(1) << BigInt(constants6.BITS)) - BigInt(1); + // eslint-disable-next-line no-bitwise + const mask = wildcard ^ allOnes; + const bits = common.prefixLengthFromMask(mask, constants6.BITS); + return new Address6(`${address}/${bits}`); + } + /** + * Construct an `Address6` from a wildcard pattern with trailing `*` + * groups. The number of trailing wildcards determines the prefix + * length: each `*` represents 16 bits. `::` is expanded to zero groups + * (not wildcards) before evaluating trailing wildcards. + * + * Only trailing whole-group wildcards are supported. Partial-group + * wildcards (e.g. `2001:db8::0*`) and interior wildcards (e.g. + * `*::1`) throw `AddressError`. + * @example + * Address6.fromWildcard('2001:db8:*:*:*:*:*:*').subnet; // '/32' + * Address6.fromWildcard('2001:db8::*').subnet; // '/112' + * Address6.fromWildcard('*:*:*:*:*:*:*:*').subnet; // '/0' + */ + static fromWildcard(input) { + if (input.includes('%') || input.includes('/')) { + throw new address_error_1.AddressError('Wildcard pattern must not include a zone or CIDR suffix'); + } + const halves = input.split('::'); + if (halves.length > 2) { + throw new address_error_1.AddressError("Wildcard pattern cannot contain more than one '::'"); + } + let groups; + if (halves.length === 2) { + const left = halves[0] === '' ? [] : halves[0].split(':'); + const right = halves[1] === '' ? [] : halves[1].split(':'); + const remaining = constants6.GROUPS - left.length - right.length; + if (remaining < 1) { + throw new address_error_1.AddressError("Wildcard pattern with '::' has too many groups"); + } + groups = [...left, ...new Array(remaining).fill('0'), ...right]; + } + else { + groups = input.split(':'); + } + if (groups.length !== constants6.GROUPS) { + throw new address_error_1.AddressError('Wildcard pattern must have 8 groups'); + } + let firstWildcard = -1; + for (let i = 0; i < groups.length; i++) { + if (groups[i] === '*') { + if (firstWildcard === -1) { + firstWildcard = i; + } + } + else if (firstWildcard !== -1) { + throw new address_error_1.AddressError('Wildcard `*` must only appear in trailing groups (e.g. `2001:db8:*:*:*:*:*:*`)'); + } + } + const trailing = firstWildcard === -1 ? 0 : groups.length - firstWildcard; + const replaced = groups.map((g) => (g === '*' ? '0' : g)); + const subnetBits = constants6.BITS - trailing * 16; + return new Address6(`${replaced.join(':')}/${subnetBits}`); + } + /** * Create an IPv6-mapped address given an IPv4 address - * @memberof Address6 - * @static * @param {string} address - An IPv4 address string * @returns {Address6} * @example @@ -669,8 +901,6 @@ class Address6 { } /** * Return an address from ip6.arpa form - * @memberof Address6 - * @static * @param {string} arpaFormAddress - an 'ip6.arpa' form address * @returns {Adress6} * @example @@ -695,8 +925,6 @@ class Address6 { } /** * Return the Microsoft UNC transcription of the address - * @memberof Address6 - * @instance * @returns {String} the Microsoft UNC transcription of the address */ microsoftTranscription() { @@ -704,8 +932,6 @@ class Address6 { } /** * Return the first n bits of the address, defaulting to the subnet mask - * @memberof Address6 - * @instance * @param {number} [mask=subnet] - the number of bits to mask * @returns {String} the first n bits of the address as a string */ @@ -714,8 +940,6 @@ class Address6 { } /** * Return the number of possible subnets of a given size in the address - * @memberof Address6 - * @instance * @param {number} [subnetSize=128] - the subnet size * @returns {String} */ @@ -731,8 +955,6 @@ class Address6 { } /** * Helper function getting start address. - * @memberof Address6 - * @instance * @returns {bigint} */ _startAddress() { @@ -741,8 +963,6 @@ class Address6 { /** * The first address in the range given by this address' subnet * Often referred to as the Network Address. - * @memberof Address6 - * @instance * @returns {Address6} */ startAddress() { @@ -751,8 +971,6 @@ class Address6 { /** * The first host address in the range given by this address's subnet ie * the first address after the Network Address - * @memberof Address6 - * @instance * @returns {Address6} */ startAddressExclusive() { @@ -761,8 +979,6 @@ class Address6 { } /** * Helper function getting end address. - * @memberof Address6 - * @instance * @returns {bigint} */ _endAddress() { @@ -771,8 +987,6 @@ class Address6 { /** * The last address in the range given by this address' subnet * Often referred to as the Broadcast - * @memberof Address6 - * @instance * @returns {Address6} */ endAddress() { @@ -781,8 +995,6 @@ class Address6 { /** * The last host address in the range given by this address's subnet ie * the last address prior to the Broadcast Address - * @memberof Address6 - * @instance * @returns {Address6} */ endAddressExclusive() { @@ -790,36 +1002,73 @@ class Address6 { return Address6.fromBigInt(this._endAddress() - adjust); } /** - * Return the scope of the address - * @memberof Address6 - * @instance + * The hex form of the subnet mask, e.g. `ffff:ffff:ffff:ffff::` for a + * `/64`. Returns an `Address6`; call `.correctForm()` for the string. + * @returns {Address6} + */ + subnetMaskAddress() { + return Address6.fromBigInt(BigInt(`0b${'1'.repeat(this.subnetMask)}${'0'.repeat(constants6.BITS - this.subnetMask)}`)); + } + /** + * The Cisco-style wildcard mask, e.g. `::ffff:ffff:ffff:ffff` for a + * `/64`. This is the bitwise inverse of `subnetMaskAddress()`. Returns + * an `Address6`; call `.correctForm()` for the string. + * @returns {Address6} + */ + wildcardMask() { + return Address6.fromBigInt(BigInt(`0b${'0'.repeat(this.subnetMask)}${'1'.repeat(constants6.BITS - this.subnetMask)}`)); + } + /** + * The network address in CIDR string form, e.g. `2001:db8::/32` for + * `2001:db8::1/32`. For an address with no explicit subnet the prefix + * is `/128`, e.g. `networkForm()` on `2001:db8::1` returns + * `2001:db8::1/128`. + * @returns {string} + */ + networkForm() { + return `${this.startAddress().correctForm()}/${this.subnetMask}`; + } + /** + * Return the scope of the address. The 4-bit scope field + * ([RFC 4291 §2.7](https://datatracker.ietf.org/doc/html/rfc4291#section-2.7)) + * is only defined for multicast addresses; for unicast addresses the scope + * is derived from the address type per + * [RFC 4007 §6](https://datatracker.ietf.org/doc/html/rfc4007#section-6). * @returns {String} */ getScope() { - let scope = constants6.SCOPES[parseInt(this.getBits(12, 16).toString(10), 10)]; - if (this.getType() === 'Global unicast' && scope !== 'Link local') { - scope = 'Global'; + const type = this.getType(); + if (type === 'Multicast' || type.startsWith('Multicast ')) { + const scope = constants6.SCOPES[parseInt(this.getBits(12, 16).toString(10), 10)]; + return scope || 'Unknown'; + } + // RFC 4291 §2.5.3: the loopback address is treated as having Link-Local + // scope. (Multicast scope 1, "Interface-Local", is a different concept + // used only for loopback transmission of multicast.) + if (type === 'Link-local unicast' || type === 'Loopback') { + return 'Link local'; + } + // RFC 4007 §6: the unspecified address has no scope. + if (type === 'Unspecified') { + return 'Unknown'; } - return scope || 'Unknown'; + return 'Global'; } /** * Return the type of the address - * @memberof Address6 - * @instance * @returns {String} */ getType() { - for (const subnet of Object.keys(constants6.TYPES)) { - if (this.isInSubnet(new Address6(subnet))) { - return constants6.TYPES[subnet]; + for (let i = 0; i < TYPE_SUBNETS.length; i++) { + const entry = TYPE_SUBNETS[i]; + if (this.isInSubnet(entry[0])) { + return entry[1]; } } return 'Global unicast'; } /** * Return the bits in the given range as a BigInt - * @memberof Address6 - * @instance * @returns {bigint} */ getBits(start, end) { @@ -827,8 +1076,6 @@ class Address6 { } /** * Return the bits in the given range as a base-2 string - * @memberof Address6 - * @instance * @returns {String} */ getBitsBase2(start, end) { @@ -836,8 +1083,6 @@ class Address6 { } /** * Return the bits in the given range as a base-16 string - * @memberof Address6 - * @instance * @returns {String} */ getBitsBase16(start, end) { @@ -851,8 +1096,6 @@ class Address6 { } /** * Return the bits that are set past the subnet mask length - * @memberof Address6 - * @instance * @returns {String} */ getBitsPastSubnet() { @@ -860,10 +1103,8 @@ class Address6 { } /** * Return the reversed ip6.arpa form of the address - * @memberof Address6 * @param {Object} options * @param {boolean} options.omitSuffix - omit the "ip6.arpa" suffix - * @instance * @returns {String} */ reverseForm(options) { @@ -889,10 +1130,10 @@ class Address6 { return 'ip6.arpa.'; } /** - * Return the correct form of the address - * @memberof Address6 - * @instance - * @returns {String} + * Returns the address in correct form, per + * [RFC 5952](https://datatracker.ietf.org/doc/html/rfc5952): leading zeros + * stripped, the longest run of zero groups collapsed to `::`, and hex digits + * lowercased (e.g. `2001:db8::1`). This is the recommended form for display. */ correctForm() { let i; @@ -936,8 +1177,6 @@ class Address6 { } /** * Return a zero-padded base-2 string representation of the address - * @memberof Address6 - * @instance * @returns {String} * @example * var address = new Address6('2001:4860:4001:803::1011'); @@ -946,10 +1185,22 @@ class Address6 { * // 0000000000000000000000000000000000000000000000000001000000010001' */ binaryZeroPad() { - return this.bigInt().toString(2).padStart(constants6.BITS, '0'); + if (this._binaryZeroPad === undefined) { + this._binaryZeroPad = this.bigInt().toString(2).padStart(constants6.BITS, '0'); + } + return this._binaryZeroPad; } + /** + * Parses a v4-in-v6 string (e.g. `::ffff:192.168.0.1`) by extracting the + * trailing IPv4 address into `this.address4` / `this.parsedAddress4` and + * returning the address with the v4 portion converted to two v6 groups. + * Used internally by `parse()`. + */ // TODO: Improve the semantics of this helper function parse4in6(address) { + if (address.indexOf('.') === -1) { + return address; + } const groups = address.split(':'); const lastGroup = groups.slice(-1)[0]; const address4 = lastGroup.match(constants4.RE_ADDRESS); @@ -972,6 +1223,13 @@ class Address6 { } return address; } + /** + * Parses an IPv6 address string into its 8 hexadecimal groups (expanding + * any `::` elision and any trailing v4-in-v6 portion) and stores the result + * on `this.parsedAddress`. Called automatically by the constructor; you + * typically don't need to call it directly. Throws `AddressError` if the + * input is malformed. + */ // TODO: Make private? parse(address) { address = this.parse4in6(address); @@ -1021,18 +1279,16 @@ class Address6 { return groups; } /** - * Return the canonical form of the address - * @memberof Address6 - * @instance - * @returns {String} + * Returns the canonical (fully expanded) form of the address: all 8 groups, + * each padded to 4 hex digits, with no `::` collapsing + * (e.g. `2001:0db8:0000:0000:0000:0000:0000:0001`). Useful for sorting and + * byte-exact comparison. */ canonicalForm() { return this.parsedAddress.map(paddedHex).join(':'); } /** * Return the decimal form of the address - * @memberof Address6 - * @instance * @returns {String} */ decimal() { @@ -1040,8 +1296,6 @@ class Address6 { } /** * Return the address as a BigInt - * @memberof Address6 - * @instance * @returns {bigint} */ bigInt() { @@ -1049,8 +1303,6 @@ class Address6 { } /** * Return the last two groups of this address as an IPv4 address string - * @memberof Address6 - * @instance * @returns {Address4} * @example * var address = new Address6('2001:4860:4001::1825:bf11'); @@ -1058,12 +1310,10 @@ class Address6 { */ to4() { const binary = this.binaryZeroPad().split(''); - return ipv4_1.Address4.fromHex(BigInt(`0b${binary.slice(96, 128).join('')}`).toString(16)); + return ipv4_1.Address4.fromHex(BigInt(`0b${binary.slice(96, 128).join('')}`).toString(16).padStart(8, '0')); } /** * Return the v4-in-v6 form of the address - * @memberof Address6 - * @instance * @returns {String} */ to4in6() { @@ -1077,10 +1327,10 @@ class Address6 { return correct + infix + address4.address; } /** - * Return an object containing the Teredo properties of the address - * @memberof Address6 - * @instance - * @returns {Object} + * Decodes the Teredo tunneling fields embedded in this address. Returns the + * Teredo prefix, server IPv4, client IPv4, raw flag bits, cone-NAT flag, + * UDP port, and Microsoft-format flag breakdown (reserved, universal/local, + * group/individual, nonce). Only meaningful for addresses in `2001::/32`. */ inspectTeredo() { /* @@ -1111,7 +1361,7 @@ class Address6 { const server4 = ipv4_1.Address4.fromHex(this.getBitsBase16(32, 64)); const bitsForClient4 = this.getBits(96, 128); // eslint-disable-next-line no-bitwise - const client4 = ipv4_1.Address4.fromHex((bitsForClient4 ^ BigInt('0xffffffff')).toString(16)); + const client4 = ipv4_1.Address4.fromHex((bitsForClient4 ^ BigInt('0xffffffff')).toString(16).padStart(8, '0')); const flagsBase2 = this.getBitsBase2(64, 80); const coneNat = (0, common_1.testBit)(flagsBase2, 15); const reserved = (0, common_1.testBit)(flagsBase2, 14); @@ -1134,10 +1384,9 @@ class Address6 { }; } /** - * Return an object containing the 6to4 properties of the address - * @memberof Address6 - * @instance - * @returns {Object} + * Decodes the 6to4 tunneling fields embedded in this address. Returns the + * 6to4 prefix and the embedded IPv4 gateway address. Only meaningful for + * addresses in `2002::/16`. */ inspect6to4() { /* @@ -1153,8 +1402,6 @@ class Address6 { } /** * Return a v6 6to4 address from a v6 v4inv6 address - * @memberof Address6 - * @instance * @returns {Address6} */ to6to4() { @@ -1171,9 +1418,80 @@ class Address6 { return new Address6(addr6to4); } /** - * Return a byte array - * @memberof Address6 - * @instance + * Embed an IPv4 address into a NAT64 IPv6 address using the encoding + * defined by [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052). + * The default prefix is the well-known prefix `64:ff9b::/96`. The prefix + * length must be one of 32, 40, 48, 56, 64, or 96; for prefixes shorter + * than /64 the IPv4 octets are split around the reserved bits 64–71. + * @example + * Address6.fromAddress4Nat64('192.0.2.33').correctForm(); // '64:ff9b::c000:221' + * Address6.fromAddress4Nat64('192.0.2.33', '2001:db8::/32').correctForm(); // '2001:db8:c000:221::' + */ + static fromAddress4Nat64(address, prefix = '64:ff9b::/96') { + const v4 = new ipv4_1.Address4(address); + const prefix6 = new Address6(prefix); + const pl = prefix6.subnetMask; + if (pl !== 32 && pl !== 40 && pl !== 48 && pl !== 56 && pl !== 64 && pl !== 96) { + throw new address_error_1.AddressError('NAT64 prefix length must be 32, 40, 48, 56, 64, or 96'); + } + const prefixBits = prefix6.binaryZeroPad(); + const v4Bits = v4.binaryZeroPad(); + let bits; + if (pl === 96) { + bits = prefixBits.slice(0, 96) + v4Bits; + } + else { + const beforeU = 64 - pl; + bits = + prefixBits.slice(0, pl) + + v4Bits.slice(0, beforeU) + + '00000000' + + v4Bits.slice(beforeU) + + '0'.repeat(128 - 72 - (32 - beforeU)); + } + const hex = BigInt(`0b${bits}`).toString(16).padStart(32, '0'); + const groups = []; + for (let i = 0; i < 8; i++) { + groups.push(hex.slice(i * 4, (i + 1) * 4)); + } + return new Address6(groups.join(':')); + } + /** + * Extract the embedded IPv4 address from a NAT64 IPv6 address using the + * encoding defined by [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052). + * The default prefix is the well-known prefix `64:ff9b::/96`. Returns + * `null` if this address is not contained within the given prefix. + * @example + * new Address6('64:ff9b::c000:221').toAddress4Nat64()!.correctForm(); // '192.0.2.33' + */ + toAddress4Nat64(prefix = '64:ff9b::/96') { + const prefix6 = new Address6(prefix); + const pl = prefix6.subnetMask; + if (pl !== 32 && pl !== 40 && pl !== 48 && pl !== 56 && pl !== 64 && pl !== 96) { + throw new address_error_1.AddressError('NAT64 prefix length must be 32, 40, 48, 56, 64, or 96'); + } + if (!this.isInSubnet(prefix6)) { + return null; + } + const bits = this.binaryZeroPad(); + let v4Bits; + if (pl === 96) { + v4Bits = bits.slice(96, 128); + } + else { + const beforeU = 64 - pl; + v4Bits = bits.slice(pl, pl + beforeU) + bits.slice(72, 72 + (32 - beforeU)); + } + const octets = []; + for (let i = 0; i < 4; i++) { + octets.push(parseInt(v4Bits.slice(i * 8, (i + 1) * 8), 2).toString()); + } + return new ipv4_1.Address4(octets.join('.')); + } + /** + * Return a byte array. + * + * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toByteArray())`. * @returns {Array} */ toByteArray() { @@ -1187,27 +1505,27 @@ class Address6 { return bytes; } /** - * Return an unsigned byte array - * @memberof Address6 - * @instance + * Return an unsigned byte array. + * + * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toUnsignedByteArray())`. * @returns {Array} */ toUnsignedByteArray() { return this.toByteArray().map(unsignByte); } /** - * Convert a byte array to an Address6 object - * @memberof Address6 - * @static + * Convert a byte array to an Address6 object. + * + * To convert from a Node.js `Buffer`, spread it: `Address6.fromByteArray([...buf])`. * @returns {Address6} */ static fromByteArray(bytes) { return this.fromUnsignedByteArray(bytes.map(unsignByte)); } /** - * Convert an unsigned byte array to an Address6 object - * @memberof Address6 - * @static + * Convert an unsigned byte array to an Address6 object. + * + * To convert from a Node.js `Buffer`, spread it: `Address6.fromUnsignedByteArray([...buf])`. * @returns {Address6} */ static fromUnsignedByteArray(bytes) { @@ -1222,8 +1540,6 @@ class Address6 { } /** * Returns true if the address is in the canonical form, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ isCanonical() { @@ -1231,8 +1547,6 @@ class Address6 { } /** * Returns true if the address is a link local address, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ isLinkLocal() { @@ -1245,53 +1559,81 @@ class Address6 { } /** * Returns true if the address is a multicast address, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ isMulticast() { - return this.getType() === 'Multicast'; + const type = this.getType(); + return type === 'Multicast' || type.startsWith('Multicast '); } /** - * Returns true if the address is a v4-in-v6 address, false otherwise - * @memberof Address6 - * @instance + * Returns true if the address was written in v4-in-v6 dotted-quad notation + * (e.g. `::ffff:127.0.0.1`), false otherwise. This is a notation-level flag + * and does not reflect whether the address bits lie in the IPv4-mapped + * (`::ffff:0:0/96`) subnet — for that, see {@link isMapped4}. * @returns {boolean} */ is4() { return this.v4; } /** + * Returns true if the address is an IPv4-mapped IPv6 address in + * `::ffff:0:0/96` ([RFC 4291 §2.5.5.2](https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2)), + * false otherwise. Unlike {@link is4}, this checks the underlying address + * bits rather than the textual notation, so `::ffff:127.0.0.1` and + * `::ffff:7f00:1` both return true. + * @returns {boolean} + */ + isMapped4() { + return this.isInSubnet(IPV4_MAPPED_SUBNET); + } + /** * Returns true if the address is a Teredo address, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ isTeredo() { - return this.isInSubnet(new Address6('2001::/32')); + return this.isInSubnet(TEREDO_SUBNET); } /** * Returns true if the address is a 6to4 address, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ is6to4() { - return this.isInSubnet(new Address6('2002::/16')); + return this.isInSubnet(SIX_TO_FOUR_SUBNET); } /** * Returns true if the address is a loopback address, false otherwise - * @memberof Address6 - * @instance * @returns {boolean} */ isLoopback() { return this.getType() === 'Loopback'; } + /** + * Returns true if the address is a Unique Local Address in `fc00::/7` ([RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193)). ULAs are the IPv6 equivalent of IPv4 [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private addresses. + * @returns {boolean} + */ + isULA() { + return this.isInSubnet(ULA_SUBNET); + } + /** + * Returns true if the address is the unspecified address `::`. + * @returns {boolean} + */ + isUnspecified() { + return this.getType() === 'Unspecified'; + } + /** + * Returns true if the address is in the documentation prefix `2001:db8::/32` ([RFC 3849](https://datatracker.ietf.org/doc/html/rfc3849)). + * @returns {boolean} + */ + isDocumentation() { + return this.isInSubnet(DOCUMENTATION_SUBNET); + } // #endregion // #region HTML /** - * @returns {String} the address in link form with a default port of 80 + * Returns the address as an HTTP URL with the host bracketed, e.g. + * `http://[2001:db8::1]/`. If `optionalPort` is provided it is appended, + * e.g. `http://[2001:db8::1]:8080/`. */ href(optionalPort) { if (optionalPort === undefined) { @@ -1303,7 +1645,12 @@ class Address6 { return `http://[${this.correctForm()}]${optionalPort}/`; } /** - * @returns {String} a link suitable for conveying the address via a URL hash + * Returns an HTML `<a>` element whose `href` encodes the address in a URL + * hash fragment (default prefix `/#address=`). Useful for linking between + * pages of an address-inspector UI. + * @param options.className - CSS class for the rendered `<a>` element + * @param options.prefix - hash prefix prepended to the address (default `/#address=`) + * @param options.v4 - when true, render the address in v4-in-v6 form */ link(options) { if (!options) { @@ -1374,8 +1721,6 @@ class Address6 { /** * Generate a regular expression string that can be used to find or validate * all variations of this address - * @memberof Address6 - * @instance * @param {boolean} substringSearch * @returns {string} */ @@ -1420,8 +1765,6 @@ class Address6 { /** * Generate a regular expression that can be used to find or validate all * variations of this address. - * @memberof Address6 - * @instance * @param {boolean} substringSearch * @returns {RegExp} */ @@ -1430,6 +1773,15 @@ class Address6 { } } exports.Address6 = Address6; +const TYPE_SUBNETS = Object.keys(constants6.TYPES).map((subnet) => [ + new Address6(subnet), + constants6.TYPES[subnet], +]); +const TEREDO_SUBNET = new Address6('2001::/32'); +const SIX_TO_FOUR_SUBNET = new Address6('2002::/16'); +const ULA_SUBNET = new Address6('fc00::/7'); +const DOCUMENTATION_SUBNET = new Address6('2001:db8::/32'); +const IPV4_MAPPED_SUBNET = new Address6('::ffff:0:0/96'); },{"./address-error":1,"./common":2,"./ipv4":3,"./v4/constants":5,"./v6/constants":6,"./v6/helpers":7,"./v6/regular-expressions":8}],5:[function(require,module,exports){ "use strict"; @@ -1489,6 +1841,11 @@ exports.TYPES = { '::1/128': 'Loopback', 'ff00::/8': 'Multicast', 'fe80::/10': 'Link-local unicast', + 'fc00::/7': 'Unique local', + '2002::/16': '6to4', + '2001:db8::/32': 'Documentation', + '64:ff9b::/96': 'NAT64 (well-known)', + '64:ff9b:1::/48': 'NAT64 (local-use)', }; /** * A regular expression that matches bad characters in an IPv6 address diff --git a/package-lock.json b/package-lock.json index e5043cd..b02bac4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1034,9 +1034,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.1.tgz", - "integrity": "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -1077,12 +1077,12 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" |
