Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

subnet-calc

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'
// }

Why

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

Install

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>

API

calc(ip, prefix)

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()

parse(input, { defaultPrefix })

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(ip, prefix, newPrefix, limit = 1024)

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(cidr, ip)

contains('172.16.0.0/12', '172.31.255.1');  // true
contains('172.16.0.0/12', '172.32.0.1');    // false

classify(ip)

Returns one of public, private (RFC 1918), shared (CGNAT, RFC 6598), loopback, link-local, documentation (RFC 5737), benchmark (RFC 2544), multicast, reserved, broadcast, unspecified.

Helpers

parseIPv4(str) → integer or null · formatIPv4(int) → string · prefixToMask(prefix) → integer · maskToPrefix(maskStringOrInt) → prefix or null


How it's tested

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 /0 to /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's subnets()
  • containment checks and invalid addresses ipaddress rejects
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.


Licence

MIT — see LICENSE.

Built by ExamineIP. There's a plain-English guide to subnetting and more free network tools at tools.examineip.com.

About

IPv4 subnet arithmetic with /0, /31, /32 and mask edge cases done properly. One dependency-free JS file, tested against Python's ipaddress.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages