JSのビット演算でサブネット計算 — /0のシフト32と/31の扱い

IPv4のサブネット計算は、アドレスを32ビットの整数にしてマスクとAND・ORを取るだけで書ける。JavaScript ならビット演算子がそのまま使えるので数行で済むが、JavaScript のビット演算は「32ビットの符号付き整数」で行われるため、素直に書くと境界で答えがずれる。

この記事では ハシトシステムのサブネット計算機 のコードを入口に、/0 のマスク、符号、/31 と /32 のホスト数、先頭ゼロ付きのアドレスという4つの境界を Node.js で確かめる。

ツールのマスク計算は /0 だけ特別扱いしている

ツールの計算部分は次のようになっている(index.html から抜粋)。

1
2
3
4
5
6
7
8
// プレフィックス長 → 32ビット整数マスク(unsigned)
function maskInt(p){ return p === 0 ? 0 : (0xFFFFFFFF << (32 - p)) >>> 0; }

var mask = maskInt(prefix);
var wildcard = (~mask) >>> 0;
var network = (ipInt & mask) >>> 0;
var broadcast = (network | wildcard) >>> 0;
var total = Math.pow(2, 32 - prefix); // アドレス総数

マスクは「全ビット1を、ホスト部のビット数だけ左にずらす」で作る。ここで p === 0 のときだけ 0 を返す分岐が入っている。これがないと /0 のマスクが壊れる。

シフト量は32で割った余りになる

MDN の左シフト演算子の説明によると、右側のオペランド(シフト量)は符号なし32ビット整数に変換されたあと 32で割った余り が使われる。つまり実際にずらす量は常に0〜31で、x << 32 は x << 0 と同じになる。

/0 のときシフト量は 32 - 0 = 32 なので、0xFFFFFFFF << 32 は「0ビットずらす」になり、全ビット1のまま残る。結果として /0 のマスクが 0.0.0.0 ではなく 255.255.255.255(/32 と同じ)になってしまう。ツールの p === 0 の分岐は、この1点を避けるためのものである。

シフト量を32で割った余りにする動作は C や Java の32ビット整数でも同じ系統の注意点で(C では未定義動作)、「全ビットをずらし切る」計算は別扱いにするのが定石である。

>>> 0 で符号なしに戻す

JavaScript のビット演算子(<< & | ~)は結果を 符号付き 32ビット整数で返す。先頭ビットが1になるアドレス(128.0.0.0 以上)は負の数になる。192.168.1.10 を | で組み立てると -1062731510 になる。

負のままでも各オクテットの取り出し((n >>> 24) & 255 など)は正しく動くが、network + 1 のような足し算や大小比較、toString(16) で表示したときに困る。ツールが演算のたびに >>> 0 を付けているのは、ゼロビット符号なし右シフトで結果を 0〜4294967295 の範囲に戻すためである。

/31 と /32 のホスト数

通常、サブネットの先頭はネットワークアドレス、末尾はブロードキャストアドレスなので、使えるホスト数は「アドレス総数 − 2」になる。この式をそのまま当てはめると /31 は 0台、/32 は −1台になる。

  • /31: RFC 3021(2000年12月、Proposed Standard、”Using 31-Bit Prefixes on IPv4 Point-to-Point Links”)は、点対点リンクで31ビットのプレフィックスを使う場合、2つのアドレスをどちらもホストアドレスとして解釈しなければならない(MUST)と定めている。ブロードキャストはリミテッドブロードキャスト(255.255.255.255)を使う。ルーター間の接続でアドレスを半分に節約するための規定である。
  • /32: アドレスが1つだけのサブネットで、ループバックインターフェースやルーティング表の単一ホスト経路に使う。

ツールはこの2つを分岐で扱い、/31 を2ホスト(点対点)、/32 を1ホストと表示する。Python の ipaddress モジュールも同じ扱いで、ドキュメントには /31 の hosts() はネットワークアドレスとブロードキャストアドレスも含み、/32 は単一のホストアドレスを返すと書かれている。手元の Python 3.11.9 でも ip_network('192.0.2.0/31').hosts() は 192.0.2.0 と 192.0.2.1 の2つを返した。

先頭ゼロ付きのオクテットは読み方が割れる

192.168.010.1 のように、オクテットに先頭ゼロが付いた文字列は処理系によって解釈が違う。

  • WHATWG URL Standard の IPv4 パーサーは、0 で始まる数字を8進数、0x で始まる数字を16進数として読む。010 は 8 になる。
  • Node.js の net.isIPv4() は、先頭ゼロ付きを IPv4 アドレスと認めない(false)。
  • Python の ipaddress は 3.9.5 から先頭ゼロをエラーにしている(ドキュメントでは「8進表記との混同を避けるため」とされる)。

ハシトシステムのツールは各オクテットを parseInt(seg, 10) で10進数として読むので、010 は 10 として計算する。サブネット計算機としては素直な挙動だが、同じ文字列をブラウザのURLに入れると 192.168.8.1 に接続しにいく。設定ファイルやアクセス制御リストにアドレスを書くときは、先頭ゼロを付けないのが安全である。

実際に試す

Node.js 24.11.1(Windows 11)で実行した。依存パッケージは要らない。次の内容を subnet-bits.mjs として保存する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// subnet-bits.mjs : ビット演算でサブネットを計算するときの /0・符号・/31・先頭ゼロを確かめる
import net from 'node:net';

const dotted = (n) => [n >>> 24, (n >>> 16) & 255, (n >>> 8) & 255, n & 255].join('.');

// 1. シフト量は 32 で割った余りになる
console.log('1 << 32 =', 1 << 32, ' 0xFFFFFFFF << 32 =', 0xFFFFFFFF << 32);

// 2. 素朴なマスク計算と、/0 を特別扱いした計算
const naive = (p) => (0xFFFFFFFF << (32 - p)) >>> 0;
const maskInt = (p) => (p === 0 ? 0 : (0xFFFFFFFF << (32 - p)) >>> 0);
for (const p of [0, 1, 24, 31, 32]) {
console.log(`/${p}`.padEnd(4), 'naive', dotted(naive(p)).padEnd(16), 'fixed', dotted(maskInt(p)));
}

// 3. >>> 0 を付けないと負の数になる
const ip = (192 << 24) | (168 << 16) | (1 << 8) | 10;
console.log('192.168.1.10 =', ip, '→ >>> 0 で', ip >>> 0);
console.log('ip & mask =', ip & maskInt(24), '→ >>> 0 で', (ip & maskInt(24)) >>> 0, dotted((ip & maskInt(24)) >>> 0));

// 4. ネットワーク・ブロードキャスト・使えるホスト数
function subnet(ipInt, p) {
const mask = maskInt(p);
const network = (ipInt & mask) >>> 0;
const broadcast = (network | (~mask >>> 0)) >>> 0;
const total = 2 ** (32 - p);
const usable = p === 32 ? 1 : p === 31 ? 2 : total - 2; // /31 は RFC 3021
return `${dotted(network)}/${p} total=${total} usable=${usable} last=${dotted(broadcast)}`;
}
const base = 0xC0000201; // 192.0.2.1
for (const p of [0, 24, 30, 31, 32]) console.log(subnet(base, p));

// 5. 先頭ゼロ付きのオクテット
const parseDecimal = (s) => s.split('.').reduce((n, seg) => n * 256 + parseInt(seg, 10), 0) >>> 0;
const s = '192.168.010.1';
console.log(s, '10進で読む ', dotted(parseDecimal(s)));
console.log(s, 'URL ', new URL(`http://${s}/`).hostname);
console.log(s, 'net.isIPv4 ', net.isIPv4(s));
console.log('Node', process.version);
1
node subnet-bits.mjs

出力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1 << 32 = 1  0xFFFFFFFF << 32 = -1
/0 naive 255.255.255.255 fixed 0.0.0.0
/1 naive 128.0.0.0 fixed 128.0.0.0
/24 naive 255.255.255.0 fixed 255.255.255.0
/31 naive 255.255.255.254 fixed 255.255.255.254
/32 naive 255.255.255.255 fixed 255.255.255.255
192.168.1.10 = -1062731510 → >>> 0 で 3232235786
ip & mask = -1062731520 → >>> 0 で 3232235776 192.168.1.0
0.0.0.0/0 total=4294967296 usable=4294967294 last=255.255.255.255
192.0.2.0/24 total=256 usable=254 last=192.0.2.255
192.0.2.0/30 total=4 usable=2 last=192.0.2.3
192.0.2.0/31 total=2 usable=2 last=192.0.2.1
192.0.2.1/32 total=1 usable=1 last=192.0.2.1
192.168.010.1 10進で読む 192.168.10.1
192.168.010.1 URL 192.168.8.1
192.168.010.1 net.isIPv4 false
Node v24.11.1

読み方:

  • 1行目: 1 << 32 は 1、0xFFFFFFFF << 32 は −1(全ビット1)のまま。シフト量32は0として扱われる。
  • 2〜6行目: 素朴な実装は /0 だけ 255.255.255.255 になり、/1〜/32 は正しい。分岐を入れた実装は /0 が 0.0.0.0 になる。
  • 7〜8行目: | と & の結果は負の数になり、>>> 0 で符号なしに戻る。
  • 9〜13行目: /30 までは「総数 − 2」、/31 は総数と同じ2、/32 は1になる。アドレス総数は Math.pow(2, 32 - p)(または 2 ** (32 - p))で求めているので、/0 の 4294967296 もビット演算の範囲外で正しく出る。
  • 14〜16行目: 同じ 192.168.010.1 が、10進で読むと 192.168.10.1、URL では 192.168.8.1、net.isIPv4 では不正になる。

Python で /31 と /32 を確かめるなら次の2行でよい(Python 3.11.9 で確認)。

1
2
3
import ipaddress
print([str(h) for h in ipaddress.ip_network("192.0.2.0/31").hosts()], [str(h) for h in ipaddress.ip_network("192.0.2.1/32").hosts()])
# ['192.0.2.0', '192.0.2.1'] ['192.0.2.1']

まとめ

  • JavaScript のシフト量は32で割った余りになるので、0xFFFFFFFF << (32 - p) は /0 だけ全ビット1を返す。/0 は分岐で 0 にする。
  • ビット演算の結果は符号付き32ビット整数なので、>>> 0 で符号なしに戻してから足し算・比較・表示に使う。
  • 使えるホスト数は /30 までが「総数 − 2」、/31 は RFC 3021 で2、/32 は1。
  • 192.168.010.1 のような先頭ゼロは、URL では8進数、Node.js の net.isIPv4 と Python 3.9.5 以降の ipaddress では不正として扱われる。

IPv6アドレスの短縮表記と正規化は「IPv6アドレスをRFC 5952で正規化する」で、cron の日と曜日の照合は「cronの日と曜日はOR照合」で扱った。

ツール: サブネット計算機(ハシトシステム)。IPv4アドレスとプレフィックス長から、ネットワークアドレス・ブロードキャストアドレス・ホスト範囲・ホスト数を計算する。/31 と /32 にも対応し、処理はブラウザ内で完結する。