From 1e80597b3c30bbc1fc1d64e53e46cbf408460359 Mon Sep 17 00:00:00 2001 From: Nick De Villiers Date: Tue, 23 Apr 2024 15:35:06 +0100 Subject: [PATCH] feat(utils): add util functions for IP address validation MAASENG-2980 (#5414) --- src/app/utils/subnetIpRange.test.ts | 55 ++++++++++++++++++++++ src/app/utils/subnetIpRange.ts | 72 +++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 src/app/utils/subnetIpRange.test.ts create mode 100644 src/app/utils/subnetIpRange.ts diff --git a/src/app/utils/subnetIpRange.test.ts b/src/app/utils/subnetIpRange.test.ts new file mode 100644 index 0000000000..9fe7f629e9 --- /dev/null +++ b/src/app/utils/subnetIpRange.test.ts @@ -0,0 +1,55 @@ +import { getIpRangeFromCidr, isIpInSubnet } from "./subnetIpRange"; + +describe("getIpRangeFromCidr", () => { + it("returns the start and end IP of a subnet", () => { + expect(getIpRangeFromCidr("10.0.0.0/26")).toEqual([ + "10.0.0.1", + "10.0.0.62", + ]); + + expect(getIpRangeFromCidr("10.0.0.0/25")).toEqual([ + "10.0.0.1", + "10.0.0.126", + ]); + + expect(getIpRangeFromCidr("10.0.0.0/24")).toEqual([ + "10.0.0.1", + "10.0.0.254", + ]); + + expect(getIpRangeFromCidr("10.0.0.0/23")).toEqual([ + "10.0.0.1", + "10.0.1.254", + ]); + + expect(getIpRangeFromCidr("10.0.0.0/22")).toEqual([ + "10.0.0.1", + "10.0.3.254", + ]); + }); +}); + +describe("isIpInSubnet", () => { + it("returns true if an IP is in a subnet", () => { + expect(isIpInSubnet("10.0.0.1", "10.0.0.0/24")).toBe(true); + expect(isIpInSubnet("10.0.0.254", "10.0.0.0/24")).toBe(true); + expect(isIpInSubnet("192.168.0.1", "192.168.0.0/24")).toBe(true); + expect(isIpInSubnet("192.168.0.254", "192.168.0.0/24")).toBe(true); + expect(isIpInSubnet("192.168.1.1", "192.168.0.0/23")).toBe(true); + }); + + it("returns false if an IP is not in a subnet", () => { + expect(isIpInSubnet("10.0.1.0", "10.0.0.0/24")).toBe(false); + expect(isIpInSubnet("10.1.0.0", "10.0.0.0/24")).toBe(false); + expect(isIpInSubnet("11.0.0.0", "10.0.0.0/24")).toBe(false); + expect(isIpInSubnet("192.168.1.255", "192.168.0.0/23")).toBe(false); + expect(isIpInSubnet("10.0.0.1", "192.168.0.0/24")).toBe(false); + expect(isIpInSubnet("192.168.2.1", "192.168.0.0/24")).toBe(false); + expect(isIpInSubnet("172.16.0.1", "192.168.0.0/24")).toBe(false); + }); + + it("returns false for the network and broadcast addresses", () => { + expect(isIpInSubnet("10.0.0.0", "10.0.0.0/24")).toBe(false); + expect(isIpInSubnet("10.0.0.255", "10.0.0.0/24")).toBe(false); + }); +}); diff --git a/src/app/utils/subnetIpRange.ts b/src/app/utils/subnetIpRange.ts new file mode 100644 index 0000000000..276508e5d3 --- /dev/null +++ b/src/app/utils/subnetIpRange.ts @@ -0,0 +1,72 @@ +import type { Subnet } from "../store/subnet/types"; + +/** + * Takes a subnet CIDR notation (IPv4) and returns the first and last IP of the subnet. + * The network and host addresses are excluded. + * + * @param cidr The CIDR notation of the subnet + * @returns The first and last valid IP addresses as two strings in a list. + */ +export const getIpRangeFromCidr = (cidr: Subnet["cidr"]) => { + // https://gist.github.com/binarymax/6114792 + + // Get start IP and number of valid addresses + const [startIp, mask] = cidr.split("/"); + const numberOfAddresses = (1 << (32 - parseInt(mask))) - 1; + + // IPv4 can be represented by an unsigned 32-bit integer, so we can use a Uint32Array to store the IP + const buffer = new ArrayBuffer(4); //4 octets + const int32 = new Uint32Array(buffer); + + // Convert starting IP to Uint32 and add the number of addresses to get the end IP. + // Subtract 1 from the number of addresses to exclude the broadcast address. + int32[0] = convertIpToUint32(startIp) + numberOfAddresses - 1; + + // Convert the buffer to a Uint8Array to get the octets, then convert it to an array + const arrayApplyBuffer = Array.from(new Uint8Array(buffer)); + + // Reverse the octets and join them with "." to get the end IP + const endIp = arrayApplyBuffer.reverse().join("."); + + const firstValidIp = getFirstValidIp(startIp); + + return [firstValidIp, endIp]; +}; + +const getFirstValidIp = (ip: string) => { + const buffer = new ArrayBuffer(4); //4 octets + const int32 = new Uint32Array(buffer); + + // add 1 because the first IP is the network address + int32[0] = convertIpToUint32(ip) + 1; + + const arrayApplyBuffer = Array.from(new Uint8Array(buffer)); + + return arrayApplyBuffer.reverse().join("."); +}; + +const convertIpToUint32 = (ip: string) => { + const octets = ip.split(".").map((a) => parseInt(a)); + const buffer = new ArrayBuffer(4); + const int32 = new Uint32Array(buffer); + int32[0] = + (octets[0] << 24) + (octets[1] << 16) + (octets[2] << 8) + octets[3]; + return int32[0]; +}; + +/** + * Checks if an IPv4 address is valid for the given subnet. + * + * @param ip The IPv4 address to check, as a string + * @param cidr The subnet's CIDR notation e.g. 192.168.0.0/24 + * @returns True if the IP is in the subnet, false otherwise + */ +export const isIpInSubnet = (ip: string, cidr: Subnet["cidr"]) => { + const [startIP, endIP] = getIpRangeFromCidr(cidr); + + const ipUint32 = convertIpToUint32(ip); + const startIPUint32 = convertIpToUint32(startIP); + const endIPUint32 = convertIpToUint32(endIP); + + return ipUint32 >= startIPUint32 && ipUint32 <= endIPUint32; +};