IPv4 subnet arithmetic with the edge cases done properly — /0, /31, /32, non-contiguous masks,
leading zeros, and the top of the address space — tested against Python's ipaddress module.
One dependency-free file. Works in Node and in the browser. It powers the Subnet Calculator on ExamineIP.
const { calc } = require('./subnet-calc');
calc('192.168.1.130', 26);
// {
// address: '192.168.1.130', prefix: 26, cidr: '192.168.1.128/26',
// network: '192.168.1.128', broadcast: '192.168.1.191', lastAddress: '192.168.1.191',
// firstHost: '192.168.1.129', lastHost: '192.168.1.190',
// mask: '255.255.255.192', wildcard: '0.0.0.63',
// totalAddresses: 64, usableHosts: 62, type: 'private'
// }Subnet calculators are easy to write and easy to get subtly wrong. The usual bugs:
| Bug | Cause | What subnet-calc does |
|---|---|---|
| Negative numbers for addresses above 128.0.0.0 | JavaScript bitwise operators work on signed 32-bit integers | Unsigned arithmetic throughout (>>> 0), tested up to 255.255.255.255 |
/0 gives the wrong mask |
x << 32 is a no-op in JavaScript, so 0xFFFFFFFF << 32 is still all ones |
/0 handled explicitly |
/31 reports 0 usable hosts |
Treating every network as having a network and broadcast address | /31 is a point-to-point link with 2 usable hosts and no broadcast (RFC 3021) |
/32 reports −1 usable hosts |
Same formula, 2^0 − 2 |
/32 is a single host |
255.0.255.0 accepted as a mask |
Counting 1 bits instead of checking they are contiguous | Non-contiguous masks are rejected |
010.0.0.1 read as 10.0.0.1 or 8.0.0.1 |
Leading zeros are octal on some systems | Rejected as ambiguous, like Python's ipaddress |
Copy subnet-calc.js into your project, or clone the repo. It is not published to npm.
Node
const subnet = require('./subnet-calc');Browser
<script src="subnet-calc.js"></script>
<script>
const net = subnetCalc.calc('10.20.30.40', 16);
</script>ip is a dotted-quad string or an unsigned integer; prefix is 0–32. Returns null on invalid input.
| Field | Example (10.20.30.40/16) |
Notes |
|---|---|---|
cidr |
10.20.0.0/16 |
|
network |
10.20.0.0 |
|
broadcast |
10.20.255.255 |
null for /31 and /32 |
lastAddress |
10.20.255.255 |
The last address in the block, always set |
firstHost / lastHost |
10.20.0.1 / 10.20.255.254 |
/31: both addresses. /32: the address itself |
mask / wildcard |
255.255.0.0 / 0.0.255.255 |
|
totalAddresses |
65536 |
|
usableHosts |
65534 |
2 for /31, 1 for /32 |
type |
private |
See classify() |
Turns user input into { ip, prefix }, or { error }:
parse('192.168.1.10/24'); // { ip: '192.168.1.10', prefix: 24 }
parse('192.168.1.10/255.255.255.0'); // { ip: '192.168.1.10', prefix: 24 }
parse('192.168.1.10 255.255.255.0'); // { ip: '192.168.1.10', prefix: 24 }
parse('192.168.1.10'); // { ip: '192.168.1.10', prefix: 32 }
parse('192.168.1.10', { defaultPrefix: 24 });
parse('192.168.1.10 255.0.255.0'); // { error: 'Subnet mask must be a contiguous run of 1 bits: ...' }split('10.0.0.0', 24, 26).map(n => n.cidr);
// [ '10.0.0.0/26', '10.0.0.64/26', '10.0.0.128/26', '10.0.0.192/26' ]Returns at most limit children. A /8 split into /32s is 16.7 million networks — check
2 ** (newPrefix - prefix) before asking.
contains('172.16.0.0/12', '172.31.255.1'); // true
contains('172.16.0.0/12', '172.32.0.1'); // falseReturns one of public, private (RFC 1918), shared (CGNAT, RFC 6598), loopback, link-local,
documentation (RFC 5737), benchmark (RFC 2544), multicast, reserved, broadcast, unspecified.
parseIPv4(str) → integer or null · formatIPv4(int) → string ·
prefixToMask(prefix) → integer · maskToPrefix(maskStringOrInt) → prefix or null
The expected values are not written by hand. tests/generate-vectors.py uses Python's standard
ipaddress module — an independent implementation — to produce tests/vectors.json:
- every prefix from
/0to/32, at random addresses and at the edges of the address space (0.0.0.0,127.255.255.255,128.0.0.0,255.255.255.255) - network, last address, mask, wildcard, total size, and first/last usable host
- mask ↔ prefix conversion for all 33 prefixes, plus a non-contiguous (one flipped bit) variant of each mask
- splits compared against
ipaddress'ssubnets() - containment checks and invalid addresses
ipaddressrejects
node tests/run.js
CI runs the tests on Node 18, 20 and 22, and separately regenerates the vectors with Python and fails if they differ from the committed file — so the expected answers can't drift from the reference.
MIT — see LICENSE.
Built by ExamineIP. There's a plain-English guide to subnetting and more free network tools at tools.examineip.com.