net.cidr
CIDR blocks: network, broadcast, first and last host, host count, netmask and prefix, and membership.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 70 tests, run in TypeScript, Python and Rust.cidrInfo 15 · cidrContains 18 · isCidr 13 · prefixToNetmask 12 · netmaskToPrefix 12
What it does
A subnet calculator as five functions that only make sense together: describe an IPv4 block (`cidrInfo`), test membership for IPv4 or IPv6 (`cidrContains`), check a block without raising (`isCidr`), and convert between prefix lengths and dotted netmasks.
## Decisions
The functions
A group: 5 functions that work together, each in its own file, each pinned by its own tests in TypeScript, Python and Rust. A project can install only the ones it calls.
- cidrInfo (cidr: string) -> CidrInfo
- cidrContains (cidr: string, address: string) -> bool
- isCidr (text: string) -> bool
- prefixToNetmask (prefix: int) -> string
- netmaskToPrefix (netmask: string) -> int
The type it declares, generated into your project
/** Everything a subnet calculator shows for one IPv4 block. */
export interface CidrInfo {
/** the block with host bits cleared, e.g. 192.168.1.0/24 */
readonly cidr: string;
readonly network: string;
readonly prefix: number;
readonly netmask: string;
/** the inverse mask, as ACLs write it */
readonly wildcard: string;
/** null for /31 and /32, which have none */
readonly broadcast: string | null;
readonly firstHost: string;
readonly lastHost: string;
/** usable addresses: all of them for /31 (RFC 3021) and /32 */
readonly hostCount: number;
readonly addressCount: number;
}
Once installed, your code imports each one from the group's module.
cidrInfo throws on bad input 15 tests
export function cidrInfo(cidr: string): CidrInfo
| cidr | string | an IPv4 block, address/prefix, e.g. 192.168.1.0/24; host bits may be set and are dropped |
| returns | CidrInfo |
For example
cidrInfo(192.168.1.0/24)→ cidr 192.168.1.0/24, network 192.168.1.0, prefix 24, netmask 255.255.255.0, wildcard 0.0.0.255, broadcast 192.168.1.255, first host 192.168.1.1, last host 192.168.1.254, host coun… a /24cidrInfo(192.168.1.77/24)→ cidr 192.168.1.0/24, network 192.168.1.0, prefix 24, netmask 255.255.255.0, wildcard 0.0.0.255, broadcast 192.168.1.255, first host 192.168.1.1, last host 192.168.1.254, host coun… an interface address: host bits are droppedcidrInfo(10.0.0.0/8)→ cidr 10.0.0.0/8, network 10.0.0.0, prefix 8, netmask 255.0.0.0, wildcard 0.255.255.255, broadcast 10.255.255.255, first host 10.0.0.1, last host 10.255.255.254, host count 16,777,… a /8
import { cidrInfo } from "#fune/net.cidr@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { formatIpv4 } from "./net_ipv4_format_ipv4.ts";
import { cidrBlock } from "./net_cidr_is_cidr.ts"; ← isCidr, another function of this group · built into the same file, even by a slim install
import { type CidrInfo } from "./net_cidr_types.ts";
/**
* Everything a subnet calculator shows for one IPv4 block. Host bits in the
* input are dropped (192.168.1.77/24 describes 192.168.1.0/24), since that is
* how an interface address is usually written. A /31 is a point-to-point link
* with two usable addresses and no broadcast (RFC 3021); a /32 is one host.
*/
export function cidrInfo(cidr: string): CidrInfo {
const block = cidrBlock(cidr);
if (block === null) throw new Error(`"${String(cidr)}" is not a CIDR block`);
if (block.version !== 4) throw new Error(`cidrInfo handles IPv4 blocks only, received "${cidr}"`);
const prefix = block.prefix;
const size = 2 ** (32 - prefix);
// Arithmetic rather than bit masks: JavaScript's & is signed 32-bit.
const network = block.bits[0] - (block.bits[0] % size);
const last = network + size - 1;
const pointToPoint = prefix >= 31;
return {
cidr: `${formatIpv4(network)}/${prefix}`,
network: formatIpv4(network),
prefix,
netmask: formatIpv4(4294967296 - size),
wildcard: formatIpv4(size - 1),
broadcast: pointToPoint ? null : formatIpv4(last),
firstHost: formatIpv4(pointToPoint ? network : network + 1),
lastHost: formatIpv4(pointToPoint ? last : last - 1),
hostCount: pointToPoint ? size : size - 2,
addressCount: size,
};
}cidrContains throws on bad input 18 tests
export function cidrContains(cidr: string, address: string): boolean
| cidr | string | an IPv4 or IPv6 block |
| address | string | an IPv4 or IPv6 address; one of the other family is never contained |
| returns | bool |
For example
cidrContains(192.168.1.0/24, 192.168.1.200)→ true inside a /24cidrContains(192.168.1.0/24, 192.168.2.1)→ false the next /24 is outsidecidrContains(10.0.0.0/8, 10.255.255.255)→ true the last address of a /8
import { cidrContains } from "#fune/net.cidr@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { ipv4Value } from "./net_ipv4_parse_ipv4.ts";
import { ipv6Groups } from "./net_ipv6_parse_ipv6.ts";
import { cidrBlock } from "./net_cidr_is_cidr.ts"; ← isCidr, another function of this group · built into the same file, even by a slim install
/**
* Whether an address lies inside a block, IPv4 or IPv6. An address of the
* other family is never inside (an IPv4-mapped IPv6 address is not treated
* as its IPv4 address; map it yourself if you mean that).
*/
export function cidrContains(cidr: string, address: string): boolean {
const block = cidrBlock(cidr);
if (block === null) throw new Error(`"${String(cidr)}" is not a CIDR block`);
const v4 = ipv4Value(address);
const v6 = v4 === null ? ipv6Groups(address) : null;
if (v4 === null && v6 === null) throw new Error(`"${String(address)}" is not an IP address`);
if (block.version === 4) {
if (v4 === null) return false;
const size = 2 ** (32 - block.prefix);
return Math.floor(v4 / size) === Math.floor(block.bits[0] / size);
}
if (v6 === null) return false;
for (let i = 0; i < 8; i++) {
const bits = Math.max(0, Math.min(16, block.prefix - 16 * i));
const size = 2 ** (16 - bits);
if (Math.floor(v6[i] / size) !== Math.floor(block.bits[i] / size)) return false;
}
return true;
}isCidr 13 tests
export function isCidr(text: string): boolean
| text | string | any text; true for a well-formed IPv4 or IPv6 block |
| returns | bool |
For example
isCidr(10.0.0.0/8)→ true an IPv4 blockisCidr(2001:db8::/32)→ true an IPv6 blockisCidr(0.0.0.0/0)→ true /0
import { isCidr } from "#fune/net.cidr@^1";
import { ipv4Value } from "./net_ipv4_parse_ipv4.ts";
import { ipv6Groups } from "./net_ipv6_parse_ipv6.ts";
/** True for a well-formed IPv4 (prefix 0-32) or IPv6 (prefix 0-128) block; never throws. */
export function isCidr(text: string): boolean {
return cidrBlock(text) !== null;
}
// Exported for the other functions of this group; not part of its contract.
/** A parsed block: version 4 with bits [address], or version 6 with the eight groups. */
export interface CidrBlock {
version: number;
bits: number[];
prefix: number;
}
/** The block, or null when the text is not address/prefix with a strict decimal prefix. */
export function cidrBlock(text: unknown): CidrBlock | null {
if (typeof text !== "string") return null;
const slash = text.indexOf("/");
if (slash < 0 || text.indexOf("/", slash + 1) >= 0) return null;
const address = text.slice(0, slash);
const digits = text.slice(slash + 1);
if (digits.length < 1 || digits.length > 3) return null;
for (const ch of digits) if (ch < "0" || ch > "9") return null;
if (digits.length > 1 && digits[0] === "0") return null;
const prefix = Number(digits);
const v4 = ipv4Value(address);
if (v4 !== null) return prefix <= 32 ? { version: 4, bits: [v4], prefix } : null;
const v6 = ipv6Groups(address);
if (v6 !== null) return prefix <= 128 ? { version: 6, bits: v6, prefix } : null;
return null;
}prefixToNetmask throws on bad input 12 tests
export function prefixToNetmask(prefix: number): string
| prefix | int | 0 to 32 |
| returns | string |
For example
prefixToNetmask(24)→ 255.255.255.0 /24prefixToNetmask(0)→ 0.0.0.0 /0prefixToNetmask(8)→ 255.0.0.0 /8
import { prefixToNetmask } from "#fune/net.cidr@^1";
import { formatIpv4 } from "./net_ipv4_format_ipv4.ts";
/** The dotted-quad mask for a prefix length: 24 is 255.255.255.0. */
export function prefixToNetmask(prefix: number): string {
if (typeof prefix !== "number" || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) {
throw new Error(`prefix must be an integer from 0 to 32, received ${prefix}`);
}
return formatIpv4(4294967296 - 2 ** (32 - prefix));
}netmaskToPrefix throws on bad input 12 tests
export function netmaskToPrefix(netmask: string): number
| netmask | string | a dotted-quad mask whose one bits are contiguous from the left |
| returns | int |
For example
netmaskToPrefix(255.255.255.0)→ 24 255.255.255.0 is /24netmaskToPrefix(0.0.0.0)→ 0 all zeros is /0netmaskToPrefix(255.255.255.255)→ 32 all ones is /32
import { netmaskToPrefix } from "#fune/net.cidr@^1";
import { parseIpv4 } from "./net_ipv4_parse_ipv4.ts";
/** The prefix length of a dotted-quad mask; a mask with a gap in its one bits is refused. */
export function netmaskToPrefix(netmask: string): number {
const value = parseIpv4(netmask);
const hostPart = 4294967295 - value + 1;
for (let prefix = 0; prefix <= 32; prefix++) {
if (2 ** (32 - prefix) === hostPart) return prefix;
}
throw new Error(`"${netmask}" is not a contiguous netmask`);
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add net.cidr
That builds the whole group. To build only what you call, and whatever it uses inside the group:
fune add net.cidr --only cidrInfo
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./net.cidr-1.0.0-typescript.fune, or fetch it from a terminal with fune pull net.cidr@1.0.0:typescript.
The whole function, every language, is one file too: net.cidr-1.0.0.fune, 36,244 bytes, sha256 bbc5cdb055ae733750b3c2cfb9061a67690545d83a9deae6e546ea7de67c4510. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before net.cidr.cidrInfo
// fune: before net.cidr.cidrContains
// fune: before net.cidr.isCidr
// fune: before net.cidr.prefixToNetmask
// fune: before net.cidr.netmaskToPrefix
after — your function gets the result and the arguments, and returns the final result.
// fune: after net.cidr.cidrInfo
// fune: after net.cidr.cidrContains
// fune: after net.cidr.isCidr
// fune: after net.cidr.prefixToNetmask
// fune: after net.cidr.netmaskToPrefix
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace net.ipv4 in net.cidr
// fune: replace net.ipv6 in net.cidr
step — your function runs at a numbered point inside a function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show net.cidr --steps.
// fune: step net.cidr.<fn> after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
cidrInfo 15 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a /24 | 192.168.1.0/24 | → | cidr 192.168.1.0/24, network 192.168.1.0, prefix 24, netmask 255.255.255.0, wildcard 0.0.0.255, broadcast 192.168.1.255, first host 192.168.1.1, last host 192.168.1.254, host coun… |
| an interface address: host bits are dropped | 192.168.1.77/24 | → | cidr 192.168.1.0/24, network 192.168.1.0, prefix 24, netmask 255.255.255.0, wildcard 0.0.0.255, broadcast 192.168.1.255, first host 192.168.1.1, last host 192.168.1.254, host coun… |
| a /8 | 10.0.0.0/8 | → | cidr 10.0.0.0/8, network 10.0.0.0, prefix 8, netmask 255.0.0.0, wildcard 0.255.255.255, broadcast 10.255.255.255, first host 10.0.0.1, last host 10.255.255.254, host count 16,777,… |
| a /12 that does not fall on an octet: 172.16-172.31 | 172.16.5.4/12 | → | cidr 172.16.0.0/12, network 172.16.0.0, prefix 12, netmask 255.240.0.0, wildcard 0.15.255.255, broadcast 172.31.255.255, first host 172.16.0.1, last host 172.31.255.254, host coun… |
| a /26 in the middle of a /24 | 203.0.113.64/26 | → | cidr 203.0.113.64/26, network 203.0.113.64, prefix 26, netmask 255.255.255.192, wildcard 0.0.0.63, broadcast 203.0.113.127, first host 203.0.113.65, last host 203.0.113.126, host … |
| a /30 has two hosts | 192.168.1.0/30 | → | cidr 192.168.1.0/30, network 192.168.1.0, prefix 30, netmask 255.255.255.252, wildcard 0.0.0.3, broadcast 192.168.1.3, first host 192.168.1.1, last host 192.168.1.2, host count 2,… |
| a /31 point-to-point link: both addresses usable, no broadcast (RFC 3021) | 198.51.100.7/31 | → | cidr 198.51.100.6/31, network 198.51.100.6, prefix 31, netmask 255.255.255.254, wildcard 0.0.0.1, broadcast —, first host 198.51.100.6, last host 198.51.100.7, host count 2, addre… |
| a /32 is one host | 192.0.2.9/32 | → | cidr 192.0.2.9/32, network 192.0.2.9, prefix 32, netmask 255.255.255.255, wildcard 0.0.0.0, broadcast —, first host 192.0.2.9, last host 192.0.2.9, host count 1, address count 1 |
| the whole IPv4 space | 0.0.0.0/0 | → | cidr 0.0.0.0/0, network 0.0.0.0, prefix 0, netmask 0.0.0.0, wildcard 255.255.255.255, broadcast 255.255.255.255, first host 0.0.0.1, last host 255.255.255.254, host count 4,294,96… |
| prefix above 32 | 192.168.1.0/33 | → | error: is not a CIDR block |
Show the other 5 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| no prefix | 192.168.1.0 | → | error: is not a CIDR block |
| a leading zero in the prefix | 192.168.1.0/024 | → | error: is not a CIDR block |
| a trailing newline | 192.168.1.0/24 | → | error: is not a CIDR block |
| a non-ASCII digit in the prefix | 10.0.0.0/٨ | → | error: is not a CIDR block |
| IPv6 blocks are not described | 2001:db8::/32 | → | error: cidrInfo handles IPv4 blocks only |
cidrContains 18 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| inside a /24 | 192.168.1.0/24, 192.168.1.200 | → | true |
| the next /24 is outside | 192.168.1.0/24, 192.168.2.1 | → | false |
| the last address of a /8 | 10.0.0.0/8, 10.255.255.255 | → | true |
| 172.31 is inside 172.16.0.0/12 | 172.16.0.0/12, 172.31.0.1 | → | true |
| 172.32 is not (a naive /16 reading gets 172.16-only) | 172.16.0.0/12, 172.32.0.1 | → | false |
| /0 contains everything of its family | 0.0.0.0/0, 203.0.113.5 | → | true |
| /32 contains only itself | 192.0.2.9/32, 192.0.2.10 | → | false |
| IPv6 inside a /32 | 2001:db8::/32, 2001:db8:ffff::1 | → | true |
| IPv6 just outside a /32 | 2001:db8::/32, 2001:db9::1 | → | false |
| link-local /10 reaches febf | fe80::/10, febf::1 | → | true |
Show the other 8 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| link-local /10 stops before fec0 | fe80::/10, fec0::1 | → | false |
| a /33 splits a group: 7fff is in the lower half | 2001:db8::/33, 2001:db8:7fff::1 | → | true |
| a /33 splits a group: 8000 is in the upper half | 2001:db8::/33, 2001:db8:8000::1 | → | false |
| an IPv4-mapped IPv6 address is not in an IPv4 block | 10.0.0.0/8, ::ffff:10.0.0.1 | → | false |
| an IPv4 address is not in ::/0 | ::/0, 10.0.0.1 | → | false |
| a malformed address | 10.0.0.0/8, 10.0.0.256 | → | error: is not an IP address |
| a block without a prefix | 10.0.0.0, 10.0.0.1 | → | error: is not a CIDR block |
| an IPv6 prefix above 128 | 2001:db8::/129, 2001:db8::1 | → | error: is not a CIDR block |
isCidr 13 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| an IPv4 block | 10.0.0.0/8 | → | true |
| an IPv6 block | 2001:db8::/32 | → | true |
| /0 | 0.0.0.0/0 | → | true |
| IPv6 /128 | ::/128 | → | true |
| host bits set are still a block | 192.168.1.77/24 | → | true |
| IPv4 prefix 33 | 10.0.0.0/33 | → | false |
| IPv6 prefix 129 | ::/129 | → | false |
| a bare address | 10.0.0.0 | → | false |
| two slashes | 10.0.0.0/8/8 | → | false |
| a leading zero in the prefix | 10.0.0.0/08 | → | false |
Show the other 3 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a trailing newline | 10.0.0.0/8 | → | false |
| a zone id | fe80::1%eth0/64 | → | false |
| empty text | → | false |
prefixToNetmask 12 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| /24 | 24 | → | 255.255.255.0 |
| /0 | 0 | → | 0.0.0.0 |
| /8 | 8 | → | 255.0.0.0 |
| /12 | 12 | → | 255.240.0.0 |
| /20 | 20 | → | 255.255.240.0 |
| /25 | 25 | → | 255.255.255.128 |
| /30 | 30 | → | 255.255.255.252 |
| /31 | 31 | → | 255.255.255.254 |
| /32 | 32 | → | 255.255.255.255 |
| 33 is too long | 33 | → | error: prefix must be an integer from 0 to 32 |
Show the other 2 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| negative | -1 | → | error: prefix must be an integer from 0 to 32 |
| a fraction | 24.5 | → | error: prefix must be an integer from 0 to 32 |
netmaskToPrefix 12 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| 255.255.255.0 is /24 | 255.255.255.0 | → | 24 |
| all zeros is /0 | 0.0.0.0 | → | 0 |
| all ones is /32 | 255.255.255.255 | → | 32 |
| 255.255.254.0 is /23 | 255.255.254.0 | → | 23 |
| 255.240.0.0 is /12 | 255.240.0.0 | → | 12 |
| 255.255.255.128 is /25 | 255.255.255.128 | → | 25 |
| 128.0.0.0 is /1 | 128.0.0.0 | → | 1 |
| 255.255.255.252 is /30 | 255.255.255.252 | → | 30 |
| a gap in the one bits | 255.0.255.0 | → | error: is not a contiguous netmask |
| a wildcard mask is not a netmask | 0.255.255.255 | → | error: is not a contiguous netmask |
Show the other 2 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a stray low bit | 255.255.255.1 | → | error: is not a contiguous netmask |
| not a dotted quad | 255.255.256.0 | → | error: is not a dotted-quad IPv4 address |
More from the author
- **Host bits are allowed and dropped.** `192.168.1.77/24` is how an interface address is written, and it describes `192.168.1.0/24`; `cidrInfo` returns the cleaned-up block in `cidr`. (Python's `ipaddress.ip_network` refuses it unless `strict=False`; this behaves like `strict=False`.) - **/31 and /32.** A /31 is a point-to-point link: RFC 3021 makes both addresses usable, so `hostCount` is 2, the hosts are the two addresses and `broadcast` is null. A /32 is one host, also with no broadcast. Everything from /0 to /30 has network and broadcast addresses reserved, so `hostCount` is `addressCount - 2`. - **Families do not mix.** `cidrContains("10.0.0.0/8", "::ffff:10.0.0.1")` is false: an IPv4-mapped IPv6 address is a different address as far as a socket is concerned. Convert it first if you mean the IPv4 address. - **Strict text.** Addresses follow `net.ipv4` and `net.ipv6` (no leading zeros, no zone ids); the prefix is ASCII decimal with no leading zero, 0-32 for IPv4 and 0-128 for IPv6. - `cidrInfo` is IPv4 only: an IPv6 /64 has 2^64 addresses, which no language here holds in an `int`, and "broadcast" has no IPv6 meaning. - Netmasks must be contiguous: `255.0.255.0` is refused, and so is a wildcard mask such as `0.0.0.255` (write the netmask, `255.255.255.0`).
`cidrBlock` (`cidr_block`) is exported for the group's own functions; it is not part of the contract.
Sources: RFC 4632 (CIDR) section 3.1; RFC 3021 (Using 31-Bit Prefixes on IPv4 Point-to-Point Links); RFC 4291 section 2.3 (IPv6 prefix notation).