Skip to content

Instantly share code, notes, and snippets.

@birjj
Last active February 2, 2023 16:25
Show Gist options
  • Save birjj/886807f5c428aaa32e9f4d99630b2c17 to your computer and use it in GitHub Desktop.
Save birjj/886807f5c428aaa32e9f4d99630b2c17 to your computer and use it in GitHub Desktop.
CIDR helpers for our Azure DevOps wiki
/** Class that represents an IPv4 CIDR range (or a single IP, i.e. a CIDR range with /32 mask) */
class IPv4CIDR {
ip = 0n;
mask = 0n;
constructor(ip, mask = 32) {
this.mask = ~BigInt("0b"+([...new Array(32 - mask)].fill("1").join("") || "0"));
this.ip = BigInt("0b"+(ip.split(".").map((v,i) => (+v).toString(2).padStart(8, "0")).join("")));
}
get start() { return this.ip & this.mask; }
get end() { return (this.ip & this.mask) + (~this.mask); }
get isSingular() { return !(~this.mask); }
firstIP() {
return new IPv4CIDR(IPv4CIDR.formatIP(this.start), 32);
}
lastIP() {
return new IPv4CIDR(IPv4CIDR.formatIP(this.end), 32);
}
overlaps(other) {
if (other.end < this.start || other.start > this.end) {
return false;
}
return true;
}
static formatIP(ip) {
let outp = "";
ip = ip.toString(2).padStart(32, "0");
for (let i = 0; i < ip.length; i += 8) {
if (outp) { outp += "."; }
outp += parseInt(ip.substring(i, i+8), 2);
}
return outp;
}
static formatCIDR(ip, mask) {
return IPv4CIDR.formatIP(ip)+"/"+(32 - (~mask).toString(2).length);
}
toString() {
if (this.isSingular) {
return IPv4CIDR.formatIP(this.ip);
}
return IPv4CIDR.formatCIDR(this.ip, this.mask);
}
}
/** Logs a warning to console if any CIDRs overlap */
const checkCIDROverlaps = (entries) => {
const stringifyEntry = entry => `"${entry.name}" ${entry.cidr.toString()} (${entry.cidr.firstIP().toString()} => ${entry.cidr.lastIP().toString()})`;
for (let i = 0; i < entries.length; ++i) {
for (let j = i+1; j < entries.length; ++j) {
if (entries[i].cidr.overlaps(entries[j].cidr)) {
console.warn("Overlap between:\n ", stringifyEntry(entries[i]), "\n ", stringifyEntry(entries[j]));
}
}
}
};
/** Expects other files to be available */
if (document.getElementById("cidr-visualizer")) {
const $tmp = document.getElementById("cidr-visualizer");
$tmp.parentNode.removeChild($tmp);
}
document.body.appendChild(visualizeCIDRs(getCIDRs()));
/** Returns { name: string, ranges: string[] }[] */
const getCIDRs = () => {
const $th = [...document.querySelectorAll("th")].find($th => /IP Address Range/i.test($th.textContent));
const $table = $th.closest("table");
const index = [...$th.parentNode.children].indexOf($th);
const $rows = [...$table.querySelectorAll("tbody tr")];
const outps = [];
$rows.forEach($row => {
const name = $row.children[0]?.textContent || "";
[...$row.children[index].childNodes]
.filter($n => $n.nodeType === Node.TEXT_NODE)
.forEach($t => {
const range = $t.textContent.trim();
if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/.test(range)) {
console.warn("Invalid CIDR range", range);
return;
}
const [ip,mask] = range.split("/");
const cidr = new IPv4CIDR(ip, +mask);
outps.push({ cidr, name });
});
});
return outps;
};
/** Takes output of getCIDRs(), returns SVGSVGElement */
const visualizeCIDRs = (cidrs) => {
const ROW_HEIGHT = 16;
const ROW_WIDTH = 256;
let $VISUALIZER = null;
const visualize = (entry) => {
if (!$VISUALIZER) { console.warn("Attempting to visualize", entry, "without $VISUALIZER"); return; }
while ($VISUALIZER.firstChild) {
$VISUALIZER.removeChild($VISUALIZER.firstChild);
}
if (!entry) { return; }
const $text = document.createElementNS("http://www.w3.org/2000/svg", "text");
$text.textContent = `${entry.name} (${entry.cidr.toString()})`;
$text.setAttribute("x", "0"); $text.setAttribute("y", ROW_HEIGHT - 2);
$text.setAttribute("fill", "#000");
$text.style.fontSize = "8px";
$VISUALIZER.appendChild($text);
};
const data = {};
cidrs.forEach(entry => {
const { name, cidr } = entry;
const [a,b,c,d] = cidr.toString().split(".");
const bucket = `${a}.${b}`;
data[bucket] = data[bucket] || [];
data[bucket].push(entry);
});
class Elm {
padding = [0, 0, 0, 0];
type = "";
children = [];
$elm = null;
constructor(type) {
this.type = type;
}
get x() { return 0; } /* relative to parent */
get y() { return 0; } /* relative to parent */
get width() {
return this.padding[3] + this.children.reduce((p,child) => {
return Math.max(p, child.x + child.width);
}, 0) + this.padding[1];
}
get height() {
return this.padding[0] + this.children.reduce((p,child) => {
return Math.max(p, child.y + child.height);
}, 0) + this.padding[2];
}
addChild(child) { this.children.push(child); }
render() {
const $elm = document.createElementNS("http://www.w3.org/2000/svg", this.type);
$elm.setAttribute("transform", `translate(${this.x}, ${this.y})`);
this.children.forEach($child => $elm.appendChild($child.render()));
this.$elm = $elm;
return $elm;
}
}
class SVGColumnElm extends Elm {
padding = [8, 8, 8, 32];
constructor() { super("svg"); }
get width() {
return this.padding[3] + this.children.reduce((p, child) => {
return Math.max(p, child.width);
}, 0) + this.padding[1];
}
get height() {
return this.padding[0] + this.children.reduce((p, child) => {
return p + child.height;
}, 0) + this.padding[2];
}
render() {
const $svg = super.render();
$svg.setAttribute("viewBox", `${this.x} ${this.y} ${this.width} ${this.height}`);
let curY = this.padding[0];
this.children.forEach((child, i) => {
const $elm = child.$elm;
$elm.setAttribute("transform", `translate(${this.padding[3]}, ${curY})`);
curY += child.height;
});
return $svg;
}
}
class IPRowElm extends Elm {
padding = [0, 0, 4, 0];
constructor(name, cidrs) {
super("g");
this.name = name;
cidrs.forEach(entry => this.addChild(new CidrElm(entry.name, entry.cidr)));
}
get width() { return this.padding[3] + ROW_WIDTH + this.padding[1]; }
get height() { return this.padding[0] + ROW_HEIGHT + this.padding[2]; }
render() {
const $g = super.render();
$g.setAttribute("data-row", this.name);
const $row = document.createElementNS("http://www.w3.org/2000/svg", "rect");
$row.setAttribute("x", this.padding[3]); $row.setAttribute("y", this.padding[0]);
$row.setAttribute("width", ROW_WIDTH); $row.setAttribute("height", ROW_HEIGHT);
$row.setAttribute("fill", "#fff"); $row.setAttribute("stroke", "#aaa");
const $text = document.createElementNS("http://www.w3.org/2000/svg", "text");
$text.setAttribute("y", this.padding[0] + (ROW_HEIGHT/2));
$text.setAttribute("x", -4);
$text.style.textAnchor = "end";
$text.style.dominantBaseline = "middle";
$text.style.fontSize = "8px";
$text.style.color = "#aaa";
$text.textContent = this.name;
$g.prepend($text);
$g.prepend($row);
return $g;
}
}
class CidrElm extends Elm {
constructor(name, cidr) {
super("g");
this.cidr = cidr;
this.name = name;
}
get x() {
const index = Number(this.cidr.start % 65536n);
return (index / 65536) * ROW_WIDTH;
}
get width() {
const n = Math.min(Number(this.cidr.end - this.cidr.start), 65536);
return (n / 65536) * ROW_WIDTH;
}
get height() { return ROW_HEIGHT; }
render() {
const $g = super.render();
$g.setAttribute("data-entry", this.name);
$g.setAttribute("data-cidr", this.cidr.toString());
const $rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
$rect.setAttribute("x", 0); $rect.setAttribute("y", 0);
$rect.setAttribute("width", this.width); $rect.setAttribute("height", this.height);
$rect.setAttribute("fill", "#000000aa"); $rect.setAttribute("stroke", "none");
$g.addEventListener("mouseenter", () => {
visualize({ name: this.name, cidr: this.cidr });
$rect.setAttribute("fill", "#0000ffaa");
});
$g.addEventListener("mouseleave", () => $rect.setAttribute("fill", "#000000aa"));
$g.appendChild($rect);
const $text = document.createElementNS("http://www.w3.org/2000/svg", "text");
$text.textContent = this.name;
$text.setAttribute("x", 2); $text.setAttribute("y", ROW_HEIGHT - 2);
$text.setAttribute("fill", "#fff");
$text.style.fontSize = "4px";
$g.appendChild($text);
return $g;
}
}
class VisualizerElm extends Elm {
constructor() {
super("g");
}
get height() { return ROW_HEIGHT + 6; }
render() {
const $g = super.render();
$VISUALIZER = $g;
return $g;
}
}
const svg = new SVGColumnElm();
console.log("Creating from", data);
svg.addChild(new VisualizerElm());
Object.keys(data).sort((a,b) => {
const [i,j] = a.split(".");
const [x,y] = b.split(".");
if (i - x === 0) { return j - y; }
return i - x;
}).forEach(bucket => {
const row = new IPRowElm(bucket, data[bucket]);
svg.addChild(row);
});
const $svg = svg.render();
$svg.setAttribute("id", "cidr-visualizer");
$svg.setAttribute("style", "position:fixed;left:0;top:0;width:100vw;height:100vh;background:#fff;z-index:999");
return $svg;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment