0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザで IPv4 CIDR 計算機を書く — `>>> 0` で uint32 を扱う、`/31` の RFC 3021 を間違えない、`/0` のシフト罠を踏まない

0
Posted at

Kubernetes / VPC / AWS subnet を毎日触っていると 10.0.0.0/16/24 に 256 分割した範囲がパッと出ない瞬間がある。ipcalc を入れていない端末でも、ブラウザに貼って即計算できる CIDR 計算機が欲しい。JavaScript で IPv4 を扱う上で必須の >>> 0 で uint32 強制/31 の RFC 3021 例外/0 のビットシフト罠 を 3 つ全部踏まずに書いた、約 350 行のツール。

cidr-calc の画面: 上に CIDR 入力 "10.0.0.0/16"、下に 9 行の情報テーブル (network 10.0.0.0 / broadcast 10.0.255.255 / first usable 10.0.0.1 / last usable 10.0.255.254 / total 65,536 / usable 65,534 / subnet mask 255.255.0.0 / wildcard 0.0.255.255)、さらに下に "/24 prefix" を指定して 256 個の subnet がグリッドで列挙されている

🌐 デモ: 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
  • /312 アドレスとも使える。これは 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 を貼ってくる。parseCidripToInt全 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 に通して、後で intToIp0.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 << 32x << 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 一覧 から。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?