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.
- cidr_info (cidr: string) -> CidrInfo
- cidr_contains (cidr: string, address: string) -> bool
- is_cidr (text: string) -> bool
- prefix_to_netmask (prefix: int) -> string
- netmask_to_prefix (netmask: string) -> int
The type it declares, generated into your project
@dataclass(frozen=True)
class CidrInfo:
"""Everything a subnet calculator shows for one IPv4 block."""
#: the block with host bits cleared, e.g. 192.168.1.0/24
cidr: str
network: str
prefix: int
netmask: str
#: the inverse mask, as ACLs write it
wildcard: str
#: null for /31 and /32, which have none
broadcast: Optional[str]
first_host: str
last_host: str
#: usable addresses: all of them for /31 (RFC 3021) and /32
host_count: int
address_count: int
Once installed, your code imports each one from the group's module.
cidr_info throws on bad input 15 tests
def cidr_info(cidr: str) -> 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
cidr_info(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 /24cidr_info(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 droppedcidr_info(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
from fune.net.cidr import cidr_info # 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.
from .net_cidr_is_cidr import cidr_block ← isCidr, another function of this group · built into the same file, even by a slim install
from .net_cidr_types import CidrInfo
from .net_ipv4_format_ipv4 import format_ipv4
def cidr_info(cidr: str) -> CidrInfo:
"""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). A /31 is a point-to-point link with two usable addresses
and no broadcast (RFC 3021); a /32 is one host.
"""
block = cidr_block(cidr)
if block is None:
raise ValueError('"%s" is not a CIDR block' % (cidr,))
if block.version != 4:
raise ValueError('cidrInfo handles IPv4 blocks only, received "%s"' % (cidr,))
prefix = block.prefix
size = 2 ** (32 - prefix)
network = block.bits[0] - block.bits[0] % size
last = network + size - 1
point_to_point = prefix >= 31
return CidrInfo(
cidr="%s/%d" % (format_ipv4(network), prefix),
network=format_ipv4(network),
prefix=prefix,
netmask=format_ipv4(4294967296 - size),
wildcard=format_ipv4(size - 1),
broadcast=None if point_to_point else format_ipv4(last),
first_host=format_ipv4(network if point_to_point else network + 1),
last_host=format_ipv4(last if point_to_point else last - 1),
host_count=size if point_to_point else size - 2,
address_count=size,
)cidr_contains throws on bad input 18 tests
def cidr_contains(cidr: str, address: str) -> bool
| 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
cidr_contains(192.168.1.0/24, 192.168.1.200)→ true inside a /24cidr_contains(192.168.1.0/24, 192.168.2.1)→ false the next /24 is outsidecidr_contains(10.0.0.0/8, 10.255.255.255)→ true the last address of a /8
from fune.net.cidr import cidr_contains # 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.
from .net_cidr_is_cidr import cidr_block ← isCidr, another function of this group · built into the same file, even by a slim install
from .net_ipv4_parse_ipv4 import ipv4_value
from .net_ipv6_parse_ipv6 import ipv6_groups
def cidr_contains(cidr: str, address: str) -> bool:
"""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).
"""
block = cidr_block(cidr)
if block is None:
raise ValueError('"%s" is not a CIDR block' % (cidr,))
v4 = ipv4_value(address)
v6 = ipv6_groups(address) if v4 is None else None
if v4 is None and v6 is None:
raise ValueError('"%s" is not an IP address' % (address,))
if block.version == 4:
if v4 is None:
return False
size = 2 ** (32 - block.prefix)
return v4 // size == block.bits[0] // size
if v6 is None:
return False
for i in range(8):
bits = max(0, min(16, block.prefix - 16 * i))
size = 2 ** (16 - bits)
if v6[i] // size != block.bits[i] // size:
return False
return Trueis_cidr 13 tests
def is_cidr(text: str) -> bool
| text | string | any text; true for a well-formed IPv4 or IPv6 block |
| returns | bool |
For example
is_cidr(10.0.0.0/8)→ true an IPv4 blockis_cidr(2001:db8::/32)→ true an IPv6 blockis_cidr(0.0.0.0/0)→ true /0
from fune.net.cidr import is_cidr # net.cidr@^1
from typing import List, NamedTuple, Optional
from .net_ipv4_parse_ipv4 import ipv4_value
from .net_ipv6_parse_ipv6 import ipv6_groups
def is_cidr(text: str) -> bool:
"""True for a well-formed IPv4 (prefix 0-32) or IPv6 (prefix 0-128) block; never raises."""
return cidr_block(text) is not None
# Exported for the other functions of this group; not part of its contract.
class CidrBlock(NamedTuple):
version: int
bits: List[int]
prefix: int
def cidr_block(text: object) -> Optional[CidrBlock]:
"""The block, or None when the text is not address/prefix with a strict decimal prefix."""
if not isinstance(text, str) or text.count("/") != 1:
return None
address, digits = text.split("/")
if len(digits) < 1 or len(digits) > 3:
return None
for ch in digits:
if not ("0" <= ch <= "9"):
return None
if len(digits) > 1 and digits[0] == "0":
return None
prefix = int(digits)
v4 = ipv4_value(address)
if v4 is not None:
return CidrBlock(4, [v4], prefix) if prefix <= 32 else None
v6 = ipv6_groups(address)
if v6 is not None:
return CidrBlock(6, v6, prefix) if prefix <= 128 else None
return Noneprefix_to_netmask throws on bad input 12 tests
def prefix_to_netmask(prefix: int) -> str
| prefix | int | 0 to 32 |
| returns | string |
For example
prefix_to_netmask(24)→ 255.255.255.0 /24prefix_to_netmask(0)→ 0.0.0.0 /0prefix_to_netmask(8)→ 255.0.0.0 /8
from fune.net.cidr import prefix_to_netmask # net.cidr@^1
from .net_ipv4_format_ipv4 import format_ipv4
def prefix_to_netmask(prefix: int) -> str:
"""The dotted-quad mask for a prefix length: 24 is 255.255.255.0."""
if isinstance(prefix, bool) or not isinstance(prefix, int) or prefix < 0 or prefix > 32:
raise ValueError("prefix must be an integer from 0 to 32, received %r" % (prefix,))
return format_ipv4(4294967296 - 2 ** (32 - prefix))netmask_to_prefix throws on bad input 12 tests
def netmask_to_prefix(netmask: str) -> int
| netmask | string | a dotted-quad mask whose one bits are contiguous from the left |
| returns | int |
For example
netmask_to_prefix(255.255.255.0)→ 24 255.255.255.0 is /24netmask_to_prefix(0.0.0.0)→ 0 all zeros is /0netmask_to_prefix(255.255.255.255)→ 32 all ones is /32
from fune.net.cidr import netmask_to_prefix # net.cidr@^1
from .net_ipv4_parse_ipv4 import parse_ipv4
def netmask_to_prefix(netmask: str) -> int:
"""The prefix length of a dotted-quad mask; a mask with a gap in its one bits is refused."""
value = parse_ipv4(netmask)
host_part = 4294967295 - value + 1
for prefix in range(33):
if 2 ** (32 - prefix) == host_part:
return prefix
raise ValueError('"%s" is not a contiguous netmask' % (netmask,))Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Python 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 Python implementation. Install it without the registry with fune add ./net.cidr-1.0.0-python.fune, or fetch it from a terminal with fune pull net.cidr@1.0.0:python.
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).