IPv6 Mask / Prefix Conversion

IPv6 Mask / Prefix Conversion

Functions to convert and validate IPv6 Mask

·

4 min read

In our SDWAN solution, I have just started to implement IPv6 Support for the upcoming release. For all IP validations I'm using ip-address.js NPM library, however it doesn't have all the functionalities.

So I had to extend it and build our own NPM library so that it could be consumed by our products.

One of the main functionality which was missing was prefix - mask conversion and validation. As usual, I googled it, Unfortunately I didn't find any solution. So I had to implement it on my own and i thought it could be useful for the community.

Prefix to Mask Conversion

Input will be a number between 0 and 128 and the function should return the corresponding IPv6 Mask.

Explanation:

  1. Create a 128 bit binary string
  2. Fill the first n (prefix) bits with 1s and the remaining with 0s.
  3. Chunk them with a size of 16 bits each
  4. Convert each chuck to a hex value and join them with (:)

For Example: 118 => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:fc00'


const HEX = 16;
const BINARY = 2;
const MAX_PREFIX = 128;
const MIN_PREFIX = 0;

  static isValidPrefix(prefix: number): boolean {
    return Number.isInteger(prefix) && prefix >= MIN_PREFIX && prefix <= MAX_PREFIX;
  }
  /**
   *
   * @param prefix
   * @returns ipv6 netmask address
   *
   * Fill an array with 1s for given number of prefix bits
   * Fill the remaining bits with 0s
   * chunk it with 16 elements and covert each chunk to hex
   *
   */
  static getNetmaskForPrefix(prefix: number): string {
    if (!IPV6.isValidPrefix(prefix)) {
      throw new TypeError('Invalid IPv6 Prefix');
    }

    const prefixArr: number[] = new Array(prefix).fill(1);
    const chunkArr = Array.from({
      length: Math.ceil(prefixArr.length / HEX),
    }, (_v, i) => prefixArr.slice(i * HEX, i * HEX + HEX));

    // Converting from binary to hex
    let subnet = chunkArr.map((item) => {
      return parseInt(item.join('').padEnd(HEX, '0'), BINARY).toString(HEX);
    }).join(':');

    if (subnet.length < 35) {
      subnet = `${subnet}::`;
    }

    return new Address6(subnet).canonicalForm();
  }

Mask to Prefix Conversion

This is the opposite of the previous function. With a given mask it should return the corresponding prefix.

Ex: expect(IPV6.getPrefixForNetmask('ffff:ffff:ffff:ffff:ffff:fffc:0000:0000')).toBe(94);

Explanation:

  1. Convert the mask to binary using ip-address.js function.
  2. Count the number of 1's and return the count as result.

/**
   *
   * @param {string} netmask
   * @returns {number} prefix
   *
   * Returns exception if its an invalid mask
   */
  static getPrefixForNetmask(netmask: string): number {

      if (IPV6.isValidMask(netmask)) {
        const bits = new Address6(netmask).getBitsBase2(0, 128); // Note : Address6 from ip-address.js 
        const masked = bits.match(/1/g);

        return masked ? masked.length : 0;
      }

      throw new TypeError('Invalid IPv6 Mask');
  }

Valid Mask

But before attempting to get the prefix out of the mask, we also need to check whether it is a valid mask, else it might return the wrong results.

Ideally, a valid mask can have a maximum of one flip bit in it and starts with 1 with trailing 0s or starts with 0 and no trailing 1s.

Example: 1100 => valid // starts with 1 with trailing 0s 0000 => valid // with 0 and no trailing 1s 1010 => Invalid 0001 => Invalid

Explanation:

  1. Convert the mask to binary string
  2. Iterate through each bit to count the flips.
  3. If the count is more than 1, then it's not a valid mask.

static isValidMask(mask: string): boolean {
    try {
      const bits = new Address6(mask).getBitsBase2(0, 128);
      let flips = 0;
      let oldBit = '1';

      for (let i = 0; i < bits.length; i++) {
        if (oldBit !== bits.charAt(i)) {
          flips += 1;
          oldBit = bits.charAt(i);
        }

        if (flips > 1) {
          return false;
        }
      }

      return flips <= 1;
    } catch (e) {
      return false;
    }
  }

Bonus : Random IPv6 Address Generator ( used for testing)

Sometimes I needed a random IPv6 address for testing.

Explanation:

  1. Create 7 * 4 chars chunks of individual sets with Math.random() and valid characters from IPv6 char set [0-9][a-f].
  2. Append them with the given prefix and join it with the colon(:).

/**
     *
     * @param ipPrefix defaults to 2000:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
     * @returns a random IPv6 address with the given prefix
     */
    static generateRandomIPv6Address(ipPrefix = '2000'): string {
        const items = '0123456789abcdef'.split('');
        const randomised = [];

        for (let i = 0; i < 28; i++) {
            randomised[i] = items[Math.floor(Math.random() * items.length)];
        }

        const ipBlock: string[][] = [];

        for (let i = 0; i < randomised.length; i++) {
            const blockIndex = Math.floor(i / 4);

            if (!Array.isArray(ipBlock[blockIndex])) {
                ipBlock.push([]);
            }

            if (!(ipBlock[blockIndex].length === 0) || randomised[i] !== '0') {
                ipBlock[blockIndex].push(randomised[i]);
            }
        }

        const ip: string[][] = [];

        for (let i = 0; i < ipBlock.length; i++) {
            const ipIndex = Math.floor(i / 8);

            if (!Array.isArray(ip[ipIndex])) {
                ip.push([]);
            }

            ip[ipIndex].push(ipBlock[i].join(''));
        }

        let suffixIp = '';

        for (let i = 0; i < ip.length; i++) {
            suffixIp += ip[i].join(':');
        }

        return `${ipPrefix}:${suffixIp}`;
    }

Did you find this article valuable?

Support Sedhu by becoming a sponsor. Any amount is appreciated!