背景
自分のツールサイトに、IPアドレスとプレフィックス長からネットワークアドレスやホスト数を出すページを置いています。
https://hashitosystem.com/tools/subnet/
今朝その中身を読み返していたら、マスクを作る1行に p === 0 ? 0 : ... という特別扱いが入っていました。
自分で書いたはずなのに理由を覚えていなかったので、外したらどうなるか試しました(こういうのをコメントに残さないのが私の悪い癖です…)。
プレフィックス長とマスク
前提です。
IPv4 のアドレスは32ビットの数で、192.168.1.10/24 の /24 はプレフィックス長と呼ばれる。
先頭から何ビットがネットワーク部かを表していて、/24 なら上位24ビットが1のマスク、つまり 255.255.255.0 になります。
/0 は「全部ホスト部」なのでマスクは 0.0.0.0、/32 は 255.255.255.255 です。
素直に書いたマスク
全ビットが1の 0xFFFFFFFF を、ホスト部のビット数(32 − p)だけ左にずらせばマスクになる、と考えて書いたのが下記です。
dotted は32ビットの数を a.b.c.d の文字列にする関数で、ツールのものをそのまま使っています。
function dotted(n) {
return [(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255].join('.');
}
const naiveMask = p => (0xFFFFFFFF << (32 - p)) >>> 0;
console.log(naiveMask(24), dotted(naiveMask(24))); // 4294967040 255.255.255.0
console.log(naiveMask(1), dotted(naiveMask(1))); // 2147483648 128.0.0.0
console.log(naiveMask(0), dotted(naiveMask(0))); // 4294967295 255.255.255.255
Node.js 24 で動かすと、/24 と /1 は合っているのに、/0 だけ 255.255.255.255 になりました。
シフト量は5ビットしか見ない
原因は、JavaScript のシフト演算子(<< >> >>>)が、シフト量の下位5ビットだけを使う仕様だからだ。
32 は2進数で 100000 なので、下位5ビットは 0 になり、x << 32 は x << 0、つまり何もずらさない計算になります。
実際に 1 << 32 は 1、1 << 33 は 2 でした。
32ビット以上ずらす場面を、JavaScript は「32で割った余りだけずらす」と決めている、と理解しています。
なのでツールでは、下記のように /0 だけ先に分けていました。
function maskInt(p) {
return p === 0 ? 0 : (0xFFFFFFFF << (32 - p)) >>> 0;
}
p が1〜32なら、シフト量は0〜31に収まるので普通に計算できます。
特別扱いが必要なのは /0 の1か所だけです。
もう一つの罠、負の数
同じ関数の末尾についている >>> 0 にも理由がありました。
JavaScript のビット演算(& | ~ <<)は、結果を符号付きの32ビット整数で返すので、最上位ビットが1のアドレスは負の数になる。
192 は2進数で 11000000 なので、192.168.x.x はすべてこれに当たります。
下記は 192.168.1.10 を数にした ip と /24 のマスクで、ネットワークアドレスを出した例です。
const ip = ((192 * 256 + 168) * 256 + 1) * 256 + 10; // 3232235786
const mask = 0xFFFFFF00; // /24
console.log(ip & mask); // -1062731520
console.log((ip & mask) >>> 0); // 3232235776
console.log(dotted(ip & mask)); // 192.168.1.0
console.log((ip & mask) === 3232235776); // false
dotted の中では >>> を使っているので、負の数でも文字列にすると 192.168.1.0 と正しく出ます。
画面の表示は合っているのに、=== で比べると false になる、という壊れ方です。
>>> 0 は「0ビットだけ符号なしで右にずらす」演算で、値はそのままに、符号なしの32ビット整数(0〜4294967295)として読み直す、という意味になります。
どう付き合うか
JavaScript で IPv4 のマスクを作るときは、/0 だけ分岐で 0 を返し、ビット演算の結果には毎回 >>> 0 を付けて符号なしに戻す。
この2つを関数の中で済ませておけば、呼び出す側は普通の数として比べたり足したりできます。
本記事はAI補助で執筆した、個人開発の紹介記事です。