From 0e6d5dc3689f572e9b7d00a11b851d7afac727d3 Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Mon, 18 Aug 2025 13:23:13 +0200 Subject: [PATCH] Add utility for converting CIDR ranges to IP ranges (#26904) * Add utility for converting CIDR ranges to IP ranges * Add CLI support for IP matcher ranges script GitOrigin-RevId: 1432bf3efa269c0e8e9d58fce1575bb01d694b2f --- services/web/scripts/ip_matcher_ranges.mjs | 80 +++++++++++++++++++ .../unit/src/Scripts/IpMatcherRange.test.mjs | 19 +++++ 2 files changed, 99 insertions(+) create mode 100644 services/web/scripts/ip_matcher_ranges.mjs create mode 100644 services/web/test/unit/src/Scripts/IpMatcherRange.test.mjs diff --git a/services/web/scripts/ip_matcher_ranges.mjs b/services/web/scripts/ip_matcher_ranges.mjs new file mode 100644 index 0000000000..66a170761a --- /dev/null +++ b/services/web/scripts/ip_matcher_ranges.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env node + +// @ts-check + +import minimist from 'minimist' +import { fileURLToPath } from 'node:url' + +/** + * Converts an integer to its corresponding IPv4 address string representation + * + * @param {number} int + * @returns {string} + */ +const intToIp = int => + [ + (int >>> 24) & 0xff, + (int >>> 16) & 0xff, + (int >>> 8) & 0xff, + int & 0xff, + ].join('.') + +/** + * Convert CIDR to IP range + * + * @param {string} cidr + * @returns {{min: string, max: string}} + */ +const cidrToRange = cidr => { + const [ip, prefixLength] = cidr.split('/') + const prefix = parseInt(prefixLength) + + // Convert IP to 32-bit integer + const ipParts = ip.split('.').map(part => parseInt(part)) + const ipInt = + (ipParts[0] << 24) + (ipParts[1] << 16) + (ipParts[2] << 8) + ipParts[3] + + // Calculate network mask + const mask = (0xffffffff << (32 - prefix)) >>> 0 + + // Calculate network and broadcast addresses + const network = (ipInt & mask) >>> 0 + const broadcast = (network | (0xffffffff >>> prefix)) >>> 0 + + return { + min: intToIp(network), + max: intToIp(broadcast), + } +} + +/** + * Converts an array of CIDR ranges into a single string representation. + * Each CIDR range is converted into its corresponding minimum and maximum IP range, + * formatted as "min..max". All resultant ranges are joined by a comma. + * + * @param {string[]} cidrRanges - An array of CIDR range strings to be converted. + * @returns {string} A string representation of the converted ranges where each + * range is formatted as "min..max" and joined by commas. + */ +export const convertCidrRanges = cidrRanges => + cidrRanges + .map(cidr => { + const range = cidrToRange(cidr) + return `${range.min}..${range.max}` + }) + .join(',') + +// Only run CLI if this file is executed directly +if (fileURLToPath(import.meta.url) === process.argv[1]) { + const argv = minimist(process.argv.slice(2)) + + if (argv._.length === 0) { + console.log('Usage: node scripts/ip_matcher_ranges.mjs [cidr2] ...') + console.log( + 'Example: node scripts/ip_matcher_ranges.mjs 192.168.1.0/24 10.0.0.0/8' + ) + process.exit(1) + } + + console.log(convertCidrRanges(argv._)) +} diff --git a/services/web/test/unit/src/Scripts/IpMatcherRange.test.mjs b/services/web/test/unit/src/Scripts/IpMatcherRange.test.mjs new file mode 100644 index 0000000000..91594e1bcc --- /dev/null +++ b/services/web/test/unit/src/Scripts/IpMatcherRange.test.mjs @@ -0,0 +1,19 @@ +import { convertCidrRanges } from '../../../../scripts/ip_matcher_ranges.mjs' + +describe('IpMatcherRange', function () { + it('returns IP ranges from CIDR notation', function () { + const ranges = convertCidrRanges(['192.168.1.0/24']) + expect(ranges).to.deep.equal('192.168.1.0..192.168.1.255') + }) + + it('returns IP ranges from a variation CIDR notation', function () { + const ranges = convertCidrRanges([ + '192.168.0.0/24', + '10.0.0.0/8', + '172.16.0.0/12', + ]) + expect(ranges).to.deep.equal( + '192.168.0.0..192.168.0.255,10.0.0.0..10.255.255.255,172.16.0.0..172.31.255.255' + ) + }) +})