Skip to content

Commit

Permalink
feat!: rewritten api, using existing implementation of ip parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
mpetrunic committed Jan 16, 2023
1 parent 90f40bc commit cf5869c
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 391 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
dist
*.tsbuildinfo
*.tsbuildinfo
.vscode
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@chainsafe/netmask",
"version": "1.0.0",
"description": "Zero dependency typescript implementation for using netmasks with both ipv4 and ipv6",
"description": "Typescript implementation of netmask filtering",
"main": "dist/src/index.js",
"type": "module",
"author": "marin@chainsafe.io",
Expand All @@ -11,7 +11,9 @@
"build": "tsc",
"test": "cross-env NODE_OPTIONS=\"--loader ts-node/esm\" mocha --extension ts test/**/*.spec.ts"
},
"dependencies": {},
"dependencies": {
"@chainsafe/is-ip": "^2.0.1"
},
"devDependencies": {
"@chainsafe/eslint-config": "^1.1.0",
"@rushstack/eslint-patch": "^1.2.0",
Expand Down
30 changes: 10 additions & 20 deletions src/cidr.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
import {
IPv4,
IPv4Len,
IPv6,
IPv6Len,
maskIp,
parseIPv4,
parseIPv6,
} from "./ip.js";
import { parseIPv4, parseIPv6 } from "@chainsafe/is-ip/parse";
import { IPv4Len, IPv6Len, maskIp } from "./ip.js";

export type Mask = number[];
export interface IPNet {
net: IPv4 | IPv6;
mask: Mask;
}

export function parseCidr(s: string): IPNet {
export function parseCidr(s: string): {
network: Uint8Array;
mask: Uint8Array;
} {
const [address, maskString] = s.split("/");
if (!address || !maskString)
throw new Error("Failed to parse given CIDR: " + s);
let ipLength = IPv4Len;
let ip: IPv4 | IPv6 | null = parseIPv4(address);
let ip = parseIPv4(address);
if (ip == null) {
ipLength = IPv6Len;
ip = parseIPv6(address);
Expand All @@ -36,17 +26,17 @@ export function parseCidr(s: string): IPNet {
}
const mask = cidrMask(m, 8 * ipLength);
return {
net: maskIp(ip, mask),
network: maskIp(ip, mask),
mask,
};
}

export function cidrMask(ones: number, bits: number): Mask {
export function cidrMask(ones: number, bits: number): Uint8Array {
if (bits !== 8 * IPv4Len && bits !== 8 * IPv6Len)
throw new Error("Invalid CIDR mask");
if (ones < 0 || ones > bits) throw new Error("Invalid CIDR mask");
const l = bits / 8;
const m = new Array<number>(l).fill(0);
const m = new Uint8Array(l);
for (let i = 0; i < l; i++) {
if (ones >= 8) {
m[i] = 0xff;
Expand Down
49 changes: 6 additions & 43 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import { parseCidr } from "./cidr.js";
import { containsIp, parseIP } from "./ip.js";
import { IpNet } from "./ipnet.js";

export {
parseIP,
parseIPv4,
parseIPv6,
maskIp,
iPv4FromIPv6,
isIPv4mappedIPv6,
} from "./ip.js";
export type { IPv4, IPv6 } from "./ip.js";
export type { IPNet, Mask } from "./cidr.js";
export { ipToString } from "./util.js";
export { maskIp, iPv4FromIPv6, isIPv4mappedIPv6 } from "./ip.js";
export { IpNet } from "./ipnet.js";
export { parseCidr } from "./cidr.js";

/**
Expand All @@ -20,35 +12,6 @@ export { parseCidr } from "./cidr.js";
*
*/
export function cidrContains(cidr: string, ip: string): boolean {
const netIP = parseCidr(cidr);
const parsedIP = parseIP(ip);
if (parsedIP == null) {
throw new Error("Invalid ip");
}
return containsIp(netIP, parsedIP);
}
/**
* Checks if network with given mask contains IP
* @param cidr ipv4 or ipv6 formatted cidr . Example 198.51.100.14/24 or 2001:db8::/48
* @param ip ipv4 or ipv6 address Example 198.51.100.14 or 2001:db8::
*
*/
export function networkMaskContains(
networkIp: string,
mask: string,
ip: string
): boolean {
const parsedNetwork = parseIP(networkIp);
const parsedMask = parseIP(mask);
const parsedIP = parseIP(ip);
if (parsedIP == null || parsedNetwork == null || parsedMask == null) {
throw new Error("Invalid parameters");
}
return containsIp(
{
net: parsedNetwork,
mask: parsedMask,
},
parsedIP
);
const ipnet = new IpNet(cidr);
return ipnet.contains(ip);
}
175 changes: 22 additions & 153 deletions src/ip.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,20 @@
import type { IPNet, Mask } from "./cidr.js";
import { parseIP } from "@chainsafe/is-ip/parse";
import { allFF, deepEqual } from "./util.js";

export const IPv4Len = 4;
export const IPv6Len = 16;

export const maxIPv6Octet = parseInt("0xFFFF", 16);
export const ipv4Prefix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255];
export const ipv4Prefix = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255,
]);

export type IPv4 = [number, number, number, number];
export type IPv6 = [
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number
];
export interface IpNetRaw {
network: Uint8Array;
mask: Uint8Array;
}

export function maskIp(ip: IPv4 | IPv6, mask: Mask): IPv4 | IPv6 {
export function maskIp(ip: Uint8Array, mask: Uint8Array): Uint8Array {
if (mask.length === IPv6Len && ip.length === IPv4Len && allFF(mask, 0, 11)) {
mask = mask.slice(12);
}
Expand All @@ -36,163 +23,45 @@ export function maskIp(ip: IPv4 | IPv6, mask: Mask): IPv4 | IPv6 {
ip.length === IPv6Len &&
deepEqual(ip, ipv4Prefix, 0, 11)
) {
ip = ip.slice(12) as IPv4;
ip = ip.slice(12);
}
const n = ip.length;
if (n != mask.length) {
throw new Error("Failed to mask ip");
}
const out = new Array<number>(n).fill(0) as IPv4 | IPv6;
const out = new Uint8Array(n);
for (let i = 0; i < n; i++) {
out[i] = ip[i] & mask[i];
}
return out;
}

export function containsIp(net: IPNet, ip: IPv4 | IPv6 | string): boolean {
export function containsIp(
net: IpNetRaw,
ip: Uint8Array | number[] | string
): boolean {
if (typeof ip === "string") {
ip = parseIP(ip)!;
}
if (ip == null) throw new Error("Invalid ip");
if (ip.length !== net.net.length) {
if (ip.length !== net.network.length) {
return false;
}
for (let i = 0; i < ip.length; i++) {
if ((net.net[i] & net.mask[i]) !== (ip[i] & net.mask[i])) {
if ((net.network[i] & net.mask[i]) !== (ip[i] & net.mask[i])) {
return false;
}
}
return true;
}

export function parseIP(s: string): IPv4 | IPv6 | null {
for (let i = 0; i < s.length; i++) {
switch (s[i]) {
case ".":
return parseIPv4(s);
case ":":
return parseIPv6(s);
}
}
return null;
}

export function isIPv4mappedIPv6(ip: IPv6): boolean {
for (let i = 0; i < IPv6Len - IPv4Len; i++) {
if (ip[i] !== ipv4Prefix[i]) return false;
}
return true;
}

export function iPv4FromIPv6(ip: IPv6): IPv4 {
export function iPv4FromIPv6(ip: Uint8Array): Uint8Array {
if (!isIPv4mappedIPv6(ip)) {
throw new Error("Not a ipv4 mapped ipv6 address");
}
return [ip[12], ip[13], ip[14], ip[15]];
}

/**
* Parses IPv4 address, returns ip address or null if invalid
*/
export function parseIPv4(s: string): IPv4 | null {
const ip: IPv4 = [0, 0, 0, 0];
if (s.length === 0) return null;
const parts = s.split(".");
if (parts.length < IPv4Len) return null;
for (let i = 0; i < IPv4Len; i++) {
const part = parts[i];
if (part.startsWith("-")) return null;
const p = parseInt(parts[i], 10);
if (Number.isNaN(p)) return null;
if (p === 0 && part.length > 1) return null;
if (p < 0 || p > 255) return null;
if (p > 1 && parts[i].startsWith("0")) {
return null;
}
ip[i] = p;
throw new Error("Must have 0xffff prefix");
}
return ip;
return ip.slice(12);
}

/**
* parseIPv6 parses s as a literal IPv6 address described in RFC 4291
* and RFC 5952. Returns either IPV6 address or null if invalid
*/
export function parseIPv6(s: string): IPv6 | null {
const ipv6: IPv6 = new Array<number>(16).fill(0) as IPv6;
let elipsis = -1;
if (s.length > 2 && s[0] === ":" && s[1] === ":") {
elipsis = 0;
s = s.slice(2);
if (s.length === 0) {
return ipv6;
}
}
let i = 0;
while (i < IPv6Len) {
let colonIndex = s.indexOf(":");
let dotIndex = s.indexOf(".");
colonIndex = colonIndex == -1 ? Infinity : colonIndex;
dotIndex = dotIndex == -1 ? Infinity : dotIndex;
const part = s.slice(0, Math.min(colonIndex, dotIndex));
if (!part.match(/^[A-Fa-f0-9]*$/)) return null;
//parse hex
const p = parseInt(part, 16);
if (Number.isNaN(p) || p < 0 || p > maxIPv6Octet) return null;
// If followed by dot, might be in trailing IPv4.
if (s[part.length] === ".") {
if (elipsis < 0 && i != IPv6Len - IPv4Len) {
//not the right place
return null;
}
if (i + IPv4Len > IPv6Len) {
// Not enough room.
return null;
}
const ip4 = parseIPv4(s);
if (ip4 == null) return null;
ipv6[i] = ip4[0];
ipv6[i + 1] = ip4[1];
ipv6[i + 2] = ip4[2];
ipv6[i + 3] = ip4[3];
s = "";
i += IPv4Len;
break;
}
ipv6[i] = p >> 8;
ipv6[i + 1] = p & 0xff;
i += 2;
s = s.slice(part.length);
if (s.length === 0) break;
if (s[0] !== ":" || s.length === 1) {
return null;
}
s = s.slice(1);
if (s[0] === ":") {
if (elipsis >= 0) return null;
elipsis = i;
s = s.slice(1);
if (s.length === 0) break;
}
}
if (s.length !== 0) {
return null;
}
// If didn't parse enough, expand ellipsis.
if (i < IPv6Len) {
if (elipsis < 0) {
return null;
}
const n = IPv6Len - i;
for (let j = i - 1; j >= elipsis; j--) {
ipv6[j + n] = ipv6[j];
}
for (let j = elipsis + n - 1; j >= elipsis; j--) {
ipv6[j] = 0;
}
} else if (elipsis >= 0) {
// Ellipsis must represent at least one 0 group.
return null;
}
return ipv6;
export function isIPv4mappedIPv6(ip: Uint8Array | number[]): boolean {
return deepEqual(ip, ipv4Prefix, 0, 11);
}
Loading

0 comments on commit cf5869c

Please sign in to comment.