Skip to content

Commit

Permalink
feat(utils): add util functions for IP address validation MAASENG-2980 (
Browse files Browse the repository at this point in the history
  • Loading branch information
ndv99 authored Apr 23, 2024
1 parent b981bfa commit 1e80597
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 0 deletions.
55 changes: 55 additions & 0 deletions src/app/utils/subnetIpRange.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
72 changes: 72 additions & 0 deletions src/app/utils/subnetIpRange.ts
Original file line number Diff line number Diff line change
@@ -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;
};

0 comments on commit 1e80597

Please sign in to comment.