Last active
February 2, 2023 16:25
-
-
Save birjj/886807f5c428aaa32e9f4d99630b2c17 to your computer and use it in GitHub Desktop.
CIDR helpers for our Azure DevOps wiki
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 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])); | |
} | |
} | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 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())); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 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; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** 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