Skip to content

Subnet / CIDR Calculator — Go source

Compute IPv4 subnet details from a CIDR block - network, broadcast, host range, netmask, wildcard, host count, IP class, and private-range detection. Split a block into smaller subnets, all in your browser.

This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Package subnetcidr is the Go twin of CosmoDev's src/lib/subnet-cidr.ts (dual
// source: the web lib is TypeScript, the CLI lib is Go — kept in lock-step).
// Pure + deterministic, never panics. The table-driven tests in
// subnet-cidr_test.go share vectors with src/lib/subnet-cidr.test.ts so the two
// implementations are held to the same contract.
//
// IPv4 subnet/CIDR math. Invalid input returns a zero value with ok=false (or a
// nil slice for the splitter) so the caller can render a graceful error —
// mirroring the TS lib's null / [] returns.
package subnetcidr

import (
	"fmt"
	"math"
	"strconv"
	"strings"
)

// IPClass is the historic IPv4 class: "A", "B", "C", or "-" for everything
// else (e.g. multicast 224.0.0.0/4). Mirrors the IpClass type in the TS lib.
type IPClass string

const (
	ClassA    IPClass = "A"
	ClassB    IPClass = "B"
	ClassC    IPClass = "C"
	ClassNone IPClass = "-"
)

// SubnetInfo holds the full details of a CIDR block. Mirrors SubnetInfo in the
// TS lib field-for-field.
type SubnetInfo struct {
	Network   string
	Broadcast string
	FirstHost string
	LastHost  string
	Netmask   string
	Wildcard  string
	HostCount int64
	Prefix    int
	IPClass   IPClass
	IsPrivate bool
}

// CIDR is a parsed "ip/prefix" pair. Mirrors the { ip, prefix } returned by
// parseCidr() in the TS lib.
type CIDR struct {
	IP     string
	Prefix int
}

// Subnet is one member of a split — a network address and its prefix length.
// Mirrors the elements returned by splitSubnet() in the TS lib.
type Subnet struct {
	Network string
	Prefix  int
}

// isAllDigits reports whether s is non-empty and every byte is an ASCII digit,
// i.e. it matches the /^\d+$/ test used by ipToInt and parseCidr in the TS lib.
func isAllDigits(s string) bool {
	if s == "" {
		return false
	}
	for i := 0; i < len(s); i++ {
		if s[i] < '0' || s[i] > '9' {
			return false
		}
	}
	return true
}

// isJSInteger mirrors Number.isInteger(): true only for a finite, integral
// float64 (no NaN, no ±Inf, no fractional part).
func isJSInteger(n float64) bool {
	if math.IsNaN(n) || math.IsInf(n, 0) {
		return false
	}
	return n == math.Trunc(n)
}

// IPToInt converts a dotted-quad IPv4 string to an unsigned 32-bit integer. It
// is the Go twin of ipToInt() in src/lib/subnet-cidr.ts and returns ok=false
// for any malformed input: a wrong octet count, non-numeric octets, an octet
// greater than 255, or leading/trailing junk. Leading zeros are accepted and
// read as decimal (e.g. "010" → 10), exactly like JS Number("010").
func IPToInt(ip string) (uint32, bool) {
	parts := strings.Split(ip, ".")
	if len(parts) != 4 {
		return 0, false
	}
	var result uint32
	for _, part := range parts {
		if !isAllDigits(part) {
			return 0, false
		}
		// Base 10 on purpose: JS Number("010") === 10 (decimal, not octal).
		octet, err := strconv.ParseUint(part, 10, 32)
		if err != nil {
			// A digit string that overflows uint32 is certainly > 255, which
			// the TS lib rejects via the `octet > 255` check.
			return 0, false
		}
		if octet > 255 {
			return 0, false
		}
		result = result*256 + uint32(octet)
	}
	return result, true
}

// IntToIP converts a number to a dotted-quad IPv4 string. It is the Go twin of
// intToIp() in src/lib/subnet-cidr.ts: the TS signature is (n: number), so this
// takes a float64 to faithfully mirror it. Non-integer or oversized input is
// floor-truncated then masked to 32 bits — the Go equivalent of the TS lib's
// Math.floor(n) >>> 0.
func IntToIP(n float64) string {
	// float64 → uint64 is exact for the whole IPv4 range (and well beyond);
	// uint64 → uint32 masks the low 32 bits deterministically, matching >>> 0.
	v := uint32(uint64(math.Floor(n)))
	return fmt.Sprintf("%d.%d.%d.%d", (v>>24)&0xff, (v>>16)&0xff, (v>>8)&0xff, v&0xff)
}

// maskFromPrefix returns the 32-bit netmask for a prefix length (0–32). It
// mirrors the unexported maskFromPrefix() helper in the TS lib.
func maskFromPrefix(prefix int) uint32 {
	if prefix == 0 {
		return 0
	}
	return uint32(0xffffffff) << uint(32-prefix)
}

// ParseCIDR parses "ip/prefix" into a CIDR with prefix in 0–32. It is the Go
// twin of parseCidr() in src/lib/subnet-cidr.ts and returns ok=false for a
// missing slash, a second slash, an empty/non-numeric/out-of-range prefix, or a
// bad IP. The IP is re-emitted through IntToIP so it is canonical dotted-quad
// (the host bits are preserved verbatim here; subnetDetails masks them).
func ParseCIDR(cidr string) (CIDR, bool) {
	ipPart, prefixPart, found := strings.Cut(cidr, "/")
	if !found {
		return CIDR{}, false
	}
	// Reject multiple slashes ("a.b.c.d/24/16").
	if strings.Contains(prefixPart, "/") {
		return CIDR{}, false
	}
	if !isAllDigits(prefixPart) {
		return CIDR{}, false
	}
	prefixU, err := strconv.ParseUint(prefixPart, 10, 64)
	if err != nil {
		// Huge digit string: JS Number() yields a finite value > 32 → null.
		return CIDR{}, false
	}
	if prefixU > 32 {
		return CIDR{}, false
	}
	ipInt, ok := IPToInt(ipPart)
	if !ok {
		return CIDR{}, false
	}
	return CIDR{IP: IntToIP(float64(ipInt)), Prefix: int(prefixU)}, true
}

// SubnetDetails returns the full details of a CIDR block. It is the Go twin of
// subnetDetails() in src/lib/subnet-cidr.ts and returns ok=false for an invalid
// CIDR. /31 (point-to-point) and /32 (single host) have no host/broadcast split
// and report hostCount 2 and 1 respectively, matching the TS edge cases.
func SubnetDetails(cidr string) (SubnetInfo, bool) {
	parsed, ok := ParseCIDR(cidr)
	if !ok {
		return SubnetInfo{}, false
	}
	prefix := parsed.Prefix

	mask := maskFromPrefix(prefix)
	wildcard := ^mask
	ipInt, _ := IPToInt(parsed.IP)
	network := ipInt & mask
	broadcast := network | wildcard

	// /31 and /32 have no host/broadcast split.
	var firstHost, lastHost uint32
	if prefix >= 31 {
		firstHost = network
		lastHost = broadcast
	} else {
		firstHost = network + 1
		lastHost = broadcast - 1
	}

	var hostCount int64
	switch {
	case prefix >= 32:
		hostCount = 1
	case prefix == 31:
		hostCount = 2
	default:
		hostCount = int64(1)<<uint(32-prefix) - 2
	}

	firstOctet := (network >> 24) & 0xff
	var ipClass IPClass
	switch {
	case firstOctet < 128:
		ipClass = ClassA
	case firstOctet < 192:
		ipClass = ClassB
	case firstOctet < 224:
		ipClass = ClassC
	default:
		ipClass = ClassNone
	}

	secondOctet := (network >> 16) & 0xff
	isPrivate := firstOctet == 10 ||
		(firstOctet == 172 && secondOctet >= 16 && secondOctet <= 31) ||
		(firstOctet == 192 && secondOctet == 168)

	return SubnetInfo{
		Network:   IntToIP(float64(network)),
		Broadcast: IntToIP(float64(broadcast)),
		FirstHost: IntToIP(float64(firstHost)),
		LastHost:  IntToIP(float64(lastHost)),
		Netmask:   IntToIP(float64(mask)),
		Wildcard:  IntToIP(float64(wildcard)),
		HostCount: hostCount,
		Prefix:    prefix,
		IPClass:   ipClass,
		IsPrivate: isPrivate,
	}, true
}

// SplitSubnet splits a CIDR block into smaller subnets of newPrefix bits. It is
// the Go twin of splitSubnet() in src/lib/subnet-cidr.ts and returns nil for an
// invalid CIDR or a newPrefix outside (prefix, 32] (also for a non-integer
// newPrefix). The base IP is normalized to the network address before
// splitting, matching the TS lib.
func SplitSubnet(cidr string, newPrefix float64) []Subnet {
	parsed, ok := ParseCIDR(cidr)
	if !ok {
		return nil
	}
	if !isJSInteger(newPrefix) {
		return nil
	}
	np := int64(newPrefix)
	if np < int64(parsed.Prefix) || np > 32 {
		return nil
	}

	mask := maskFromPrefix(parsed.Prefix)
	ipInt, _ := IPToInt(parsed.IP)
	network := ipInt & mask

	count := int64(1) << uint(np-int64(parsed.Prefix))
	blockSize := uint32(1) << uint(32-np)

	out := make([]Subnet, 0, int(count))
	for i := range count {
		addr := network + uint32(i)*blockSize
		out = append(out, Subnet{Network: IntToIP(float64(addr)), Prefix: int(np)})
	}
	return out
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →