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 →