vivekstills/ipspace

★ 1Forks 0TypeScriptGitHub ↗Compare

README

ipspace

treat IPv4 and IPv6 address space as sets. union, intersect, subtract, and complement them, and always get back the minimal list of CIDR blocks that covers the result.

this is the math you reach for when you want "this big block, except these few": punching holes in a firewall rule, reconciling two blocklists, working out exactly which subnets are left after you carve some out. doing it by hand is error prone and doing it with strings is worse, so this does it with interval arithmetic and hands you clean CIDRs.

install

npm install ipspace

examples

subtract a CIDR from a block and see what is left:

import { IpSet } from "ipspace";

IpSet.from("10.0.0.0/8").difference(IpSet.from("10.10.0.0/16")).toCIDRs();
// [
//   "10.0.0.0/13", "10.8.0.0/15", "10.11.0.0/16", "10.12.0.0/14",
//   "10.16.0.0/12", "10.32.0.0/11", "10.64.0.0/10", "10.128.0.0/9"
// ]

intersect two lists, mixing CIDRs and explicit ranges:

IpSet.from(["198.51.100.0/24", "203.0.113.0/24"])
  .intersection(IpSet.from("198.51.100.128-203.0.113.127"))
  .toCIDRs();
// ["198.51.100.128/25", "203.0.113.0/25"]

complement a set over the whole v4 space (here, everything that is not RFC 1918 private space):

IpSet.from(["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]).complement(4).toCIDRs();
// ["0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", ...]

an arbitrary range becomes the fewest CIDRs that exactly cover it:

IpSet.from("192.168.1.5-192.168.1.50").toCIDRs();
// [
//   "192.168.1.5/32", "192.168.1.6/31", "192.168.1.8/29", "192.168.1.16/28",
//   "192.168.1.32/28", "192.168.1.48/31", "192.168.1.50/32"
// ]

it understands :: compression and embedded v4, and v6 counts are returned as bigint because they blow past Number:

IpSet.from("2001:db8::/32").size();
// 79228162514264337593543950336n

why bigint

an address is just a number, but a v6 address is a 128-bit one, so every address, range bound, and set size is a bigint. no octet arrays, no string parsing in the parts that do the work.

the one design decision worth knowing

an IpSet is always in canonical form: its ranges are sorted, pairwise disjoint, and adjacent ranges are merged (if one ends right where the next begins, they become one). every operation restores this form before returning, and v4 and v6 ranges never interact. because the representation is canonical, equals is a straight structural comparison, toCIDRs is always minimal, and contains is a binary search. you never have to normalize anything yourself.

api

construct

  • IpSet.from(input) where input is a string, an array of strings, or another IpSet. accepts a single address, a CIDR (host bits are masked off, not rejected), or an explicit start-end range.
  • IpSet.empty()

algebra, each returns a new set and never mutates

  • union(other)
  • intersection(other)
  • difference(other)
  • symmetricDifference(other)
  • complement(version?), over the full space of the given version, or every version present if omitted. an empty set needs the version argument since there is nothing to infer.

query

  • contains(addr), membership for one address via binary search
  • containsAll(other), overlaps(other), equals(other), isEmpty()
  • size(), total address count as a bigint

output

  • toCIDRs(), the minimal list of CIDR strings
  • toRanges(), start-end strings
  • addresses(), a lazy generator over every address, never materialized
  • the set is iterable over its ranges

license

MIT

ipspace

Issues