Kubernetes / VPC / AWS subnet を毎日触っていると
10.0.0.0/16を/24に 256 分割した範囲がパッと出ない瞬間がある。ipcalcを入れていない端末でも、ブラウザに貼って即計算できる CIDR 計算機が欲しい。JavaScript で IPv4 を扱う上で必須の>>> 0で uint32 強制、/31の RFC 3021 例外、/0のビットシフト罠 を 3 つ全部踏まずに書いた、約 350 行のツール。
🌐 デモ: https://sen.ltd/portfolio/cidr-calc/
📦 GitHub: https://github.com/sen-ltd/cidr-calc
JavaScript で IPv4 ビット演算が地味に難しい理由
JS の数値は IEEE 754 double。整数演算が完全に正確なのは 2^53 まで。32-bit 整数の bit shift は内部的に「下位 32 bit を取り出し → int32 として演算 → 結果を Number に戻す」の動作で、最上位ビットが立つと符号付きとして解釈される:
0xFFFFFFFF // → 4294967295 (uint32 として)
0xFFFFFFFF << 8 // → -256 (int32 として、上位 8 bit を捨てた)
(0xFFFFFFFF << 8) >>> 0 // → 4294967040 (uint32 強制)
「mask」「IP」「network address」全部 uint32 で扱いたいので、ビット演算の結果は必ず >>> 0 で uint32 に戻す のがこのコードベースの規律:
export function maskFor(prefix) {
if (prefix === 0) return 0;
return (0xFFFFFFFF << (32 - prefix)) >>> 0;
}
export function cidrInfo(ip, prefix) {
const mask = maskFor(prefix);
const network = (ip & mask) >>> 0; // ←
const broadcast = (network | (~mask >>> 0)) >>> 0; // ←
// ...
}
>>> 0 が無いと負数になる経路があり、intToIp の (n >>> 24) & 0xFF 等で破綻する。
/0 のビットシフト罠
「prefix 長 N の mask」を作るのは 0xFFFFFFFF << (32 - N) でいいように見えるが、N = 0 のときに << 32 になる。JS の << 演算子は ECMA 仕様で シフト量を mod 32 で取る ので、<< 32 は << 0 と同じ → mask が 0xFFFFFFFF (= 全 1 = /32) になってしまう。/0 で意図する mask は 全 0 なので、別分岐:
export function maskFor(prefix) {
if (prefix === 0) return 0;
return (0xFFFFFFFF << (32 - prefix)) >>> 0;
}
テストで両端を pin:
test("maskFor handles the boundary prefixes 0 and 32", () => {
assert.equal(maskFor(0), 0); // ← 別分岐がないと 0xFFFFFFFF になる
assert.equal(maskFor(32), 0xFFFFFFFF);
assert.equal(maskFor(24), 0xFFFFFF00);
});
/31 は RFC 3021 で例外扱い
CIDR 計算機の 正解 を出すには、prefix によって usable host 数の計算が変わる:
if (prefix === 32) {
firstUsable = network;
lastUsable = network;
usable = 1; // single-host route
} else if (prefix === 31) {
// RFC 3021: point-to-point links は network/broadcast を予約しない
firstUsable = network;
lastUsable = broadcast;
usable = 2;
} else {
firstUsable = (network + 1) >>> 0;
lastUsable = (broadcast - 1) >>> 0;
usable = total - 2;
}
-
/32はホスト 1 個のルート (loopback の127.0.0.1/32や Kubernetes Service の Cluster IP)。network = broadcast = first = last、usable = 1 -
/31は 2 アドレスとも使える。これは RFC 3021 で「point-to-point link 用に network/broadcast の予約を外す」ことが正式化されている。ルーター間リンクで/30(usable 2 / total 4) ではなく/31(usable 2 / total 2) が使われることが多い。ipcalcの古いバージョンはこの例外を実装しておらず、/31で usable=0 と出してくる -
/30以下 (/29,/28, ...) が「network + broadcast を予約する」古典的計算
テストで全パスを pin:
test("cidrInfo handles /31 per RFC 3021 (no network/broadcast reserve)", () => {
const info = cidrInfo(ipToInt("10.0.0.0"), 31);
assert.equal(info.total, 2);
assert.equal(info.usable, 2);
assert.equal(intToIp(info.firstUsable), "10.0.0.0");
assert.equal(intToIp(info.lastUsable), "10.0.0.1");
});
test("cidrInfo handles /32 as a single host", () => {
const info = cidrInfo(ipToInt("10.0.0.5"), 32);
assert.equal(info.usable, 1);
assert.equal(intToIp(info.firstUsable), "10.0.0.5");
});
「非アライン入力」を許す設計
ユーザーが 10.0.5.7/24 を貼ったとき、ナイーブには ip が host bit を含んでいるので「網内 (network) ではなく ホスト」と扱われそうだが、CIDR 表記としては prefix で網が決まり、その内側のどこかにいる IP という解釈が自然。cidrInfo 内で host bit をマスクしてから network を計算 する:
const network = (ip & mask) >>> 0; // host bit を 0 にする
これで 10.0.5.7/24 → network 10.0.5.0 / broadcast 10.0.5.255 と正しく出る。
test("cidrInfo zeros the host bits of a non-aligned input IP", () => {
const info = cidrInfo(ipToInt("10.0.5.7"), 24);
assert.equal(intToIp(info.network), "10.0.5.0");
assert.equal(intToIp(info.broadcast), "10.0.5.255");
});
subdivide — 1 万 subnet 列挙の罠
/8 を /24 に分割すると 65,536 個 の subnet。/8 を /30 に分割すると 400 万個。DOM に全部出すと当然死ぬので、limit パラメータで上限を取る:
export function subdivide(ip, prefix, newPrefix, limit = 256) {
if (newPrefix <= prefix || newPrefix > 32) {
throw new Error("newPrefix must be greater than current prefix and ≤ 32");
}
const mask = maskFor(prefix);
const network = (ip & mask) >>> 0;
const step = newPrefix === 32 ? 1 : (1 << (32 - newPrefix));
const count = 1 << (newPrefix - prefix);
const take = Math.min(count, limit);
const subnets = [];
for (let i = 0; i < take; i++) {
subnets.push({ ip: (network + i * step) >>> 0, prefix: newPrefix });
}
return { subnets, total: count, truncated: count > take };
}
UI 側は truncated: true を見て「最初の N 件のみ表示」と注記を出す。「全部見たい」ニーズは滅多にないが、scan して exhaustive に確認したいときは limit を 100000 にも引き上げられる。
newPrefix === 32 のときの step 計算で別分岐しているのは、ここでも前述の 1 << 0 罠 を避けるため。1 << (32 - 32) は 1 << 0 = 1 で偶然正しい値だが、1 << 32 経由になりそうな書き方は避けて明示的に 1 を返す。
落ちる入力に強くする
ユーザーは ありとあらゆる malformed input を貼ってくる。parseCidr と ipToInt で 全 reject の場合に null を返す ことで UI 側が分岐できる:
export function ipToInt(ip) {
if (typeof ip !== "string") return null;
const parts = ip.split(".");
if (parts.length !== 4) return null;
let n = 0;
for (const part of parts) {
if (!/^\d+$/.test(part)) return null;
const v = Number(part);
if (v < 0 || v > 255) return null;
n = (n * 256) + v;
}
return n >>> 0;
}
/^\d+$/.test(part) で +、-、空文字、小数 を全部はじく。Number(part) だけだと "010" を 10 に通して、後で intToIp で 0.0.0.10 と表示する違和感がある。テストで境界条件を全部 pin:
test("ipToInt rejects malformed input", () => {
assert.equal(ipToInt("10.0.0"), null); // 3 octets
assert.equal(ipToInt("10.0.0.0.0"), null); // 5 octets
assert.equal(ipToInt("256.0.0.0"), null); // octet > 255
assert.equal(ipToInt("10.0.0.a"), null);
assert.equal(ipToInt("10.0.0.-1"), null);
assert.equal(ipToInt(""), null);
});
まとめ
- JavaScript の bit 演算は
>>> 0で uint32 強制 が規律。これを忘れると最上位ビットが立つときに負数になってintToIpが壊れる -
/0の mask は別分岐。x << 32がx << 0になる JS の罠を避ける -
/31は RFC 3021 で usable = 2 (point-to-point links)。多くの ipcalc 実装はこれを忘れる -
subdivide は limit で truncate。
/8を/30に割ると 400 万件あるので DOM が死ぬ -
parseCidr/ipToIntは malformed → null を返して UI で分岐できるように
ソース: https://github.com/sen-ltd/cidr-calc — MIT、合計 ~350 行 (JS)、22 ユニットテスト、ビルド不要、依存ゼロ。
🛠 本記事は SEN 合同会社 が公開している小さな開発者ツール群の 1 つ。他は portfolio 一覧 から。
