From e101758187c4b11bf510e3ad89448ea9967470f5 Mon Sep 17 00:00:00 2001 From: brent saner Date: Mon, 13 Oct 2025 15:56:07 -0400 Subject: [PATCH] v1.10.3 ADDED: * netx now has a ton of netmask conversion functions for IPv4 netmasks. (IPv6 doesn't really *have* netmasks, so it was intentionally excluded). --- netx/consts_nix.go | 13 ++ netx/consts_windows.go | 13 ++ netx/errors.go | 10 + netx/funcs.go | 410 +++++++++++++++++++++++++++++++++++++++++ netx/funcs_test.go | 134 ++++++++++++++ 5 files changed, 580 insertions(+) create mode 100644 netx/consts_nix.go create mode 100644 netx/consts_windows.go create mode 100644 netx/errors.go create mode 100644 netx/funcs.go create mode 100644 netx/funcs_test.go diff --git a/netx/consts_nix.go b/netx/consts_nix.go new file mode 100644 index 0000000..f2b0dfb --- /dev/null +++ b/netx/consts_nix.go @@ -0,0 +1,13 @@ +//go:build !windows + +package netx + +import ( + `golang.org/x/sys/unix` +) + +const ( + AFUnspec uint16 = unix.AF_UNSPEC + AFInet uint16 = unix.AF_INET + AFInet6 uint16 = unix.AF_INET6 +) diff --git a/netx/consts_windows.go b/netx/consts_windows.go new file mode 100644 index 0000000..caa93b7 --- /dev/null +++ b/netx/consts_windows.go @@ -0,0 +1,13 @@ +//go:build windows + +package netx + +import ( + `golang.org/x/sys/windows` +) + +const ( + AFUnspec uint16 = windows.AF_UNSPEC + AFInet uint16 = windows.AF_INET + AFInet6 uint16 = windows.AF_INET6 +) diff --git a/netx/errors.go b/netx/errors.go new file mode 100644 index 0000000..1971633 --- /dev/null +++ b/netx/errors.go @@ -0,0 +1,10 @@ +package netx + +import ( + `errors` +) + +var ( + ErrBadMask4Str error = errors.New("netx: unknown/bad IPv4 netmask dotted quad") + ErrBadNetFam error = errors.New("netx: unknown/bad IP network family") +) diff --git a/netx/funcs.go b/netx/funcs.go new file mode 100644 index 0000000..df43ba6 --- /dev/null +++ b/netx/funcs.go @@ -0,0 +1,410 @@ +package netx + +import ( + `math/bits` + `net` + `net/netip` + `strconv` + `strings` + + `go4.org/netipx` +) + +/* +AddrRfc returns an RFC-friendly string from an IP address ([net/netip.Addr]). + +If addr is an IPv4 address, it will simmply be the string representation (e.g. "203.0.113.1"). + +If addr is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]"). + +If the version can't be determined, rfcStr will be an empty string. +*/ +func AddrRfc(addr netip.Addr) (rfcStr string) { + + if addr.Is4() { + rfcStr = addr.String() + } else if addr.Is6() { + rfcStr = "[" + addr.String() + "]" + } + + return +} + +/* +Cidr4ToIPMask takes an IPv4 CIDR/bit size/prefix length and returns the [net.IPMask]. +It's (essentially) the inverse of [net.IPMask.Size]. + +See also: + + * [Cidr4ToMask] + * [Cidr4ToStr] + +Inverse of [IPMask4ToCidr]. +*/ +func Cidr4ToIPMask(cidr uint8) (ipMask net.IPMask, err error) { + + if cidr > 32 { + err = ErrBadNetFam + return + } + + ipMask = net.CIDRMask(int(cidr), 32) + + return +} + +/* +Cidr4ToMask takes an IPv4 CIDR/bit size/prefix length and returns the netmask *in bitmask form*. + +See also: + + * [Cidr4ToIPMask] + * [Cidr4ToStr] + +Inverse of [Mask4ToCidr]. +*/ +func Cidr4ToMask(cidr uint8) (mask uint32, err error) { + + if cidr > 32 { + err = ErrBadNetFam + return + } + + // COULD do (1 << 32) - (1 << (32 - ip.Bits())) instead but in EXTREME edge cases that could cause an overflow. + // We're basically converting the CIDR size ("number of bits"/"number of ones") to an integer mask ("number AS bits") + mask = uint32(0xffffffff) << uint32(32-cidr) + + return +} + +/* +Cidr4ToStr is a convenience wrapper around [IPMask4ToStr]([Cidr4ToMask](cidr)). + +See also: + + * [Cidr4ToIPMask] + * [Cidr4ToMask] + +Inverse of [Mask4StrToCidr]. +*/ +func Cidr4ToStr(cidr uint8) (maskStr string, err error) { + + var ipMask net.IPMask + + if ipMask, err = Cidr4ToIPMask(cidr); err != nil { + return + } + + if maskStr, err = IPMask4ToStr(ipMask); err != nil { + return + } + + return +} + +/* +GetAddrFamily returns the network family of a [net/netip.Addr]. + +See also [GetIpFamily]. + +If addr is not a "valid" IP address or the version can't be determined, family will be AFUnspec (usually 0x00/0). +*/ +func GetAddrFamily(addr netip.Addr) (family uint16) { + + family = AFUnspec + + if !addr.IsValid() { + return + } + + if addr.Is4() { + family = AFInet + } else if addr.Is6() { + family = AFInet6 + } else { + return + } + + return +} + +/* +GetIpFamily returns the network family of a [net.IP]. + +See also [GetAddrFamily]. + +If ip is not a "valid" IP address or the version can't be determined, +family will be [golang.org/x/sys/unix.AF_UNSPEC] or [golang.org/x/sys/windows.AF_UNSPEC] depending on platform (usually 0x00/0). +*/ +func GetIpFamily(ip net.IP) (family uint16) { + + var ok bool + var addr netip.Addr + + if addr, ok = netipx.FromStdIP(ip); !ok { + return + } + + family = GetAddrFamily(addr) + + return +} + +/* +IpRfc returns an RFC-friendly string from an IP address ([net.IP]). + +If ip is an IPv4 address, it will simmply be the string representation (e.g. "203.0.113.1"). + +If ip is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]"). + +If the version can't be determined, rfcStr will be an empty string. +*/ +func IpRfc(ip net.IP) (rfcStr string) { + + if ip.To4() != nil { + rfcStr = ip.To4().String() + } else if ip.To16() != nil { + rfcStr = "[" + ip.To16().String() + "]" + } + + return +} + +/* +IPMask4ToCidr returns a CIDR prefix size/bit size/bit length from a [net.IPMask]. + +See also: + + * [IPMask4ToMask] + * [IPMask4ToStr] + +Inverse of [Cidr4ToIPMask]. +*/ +func IPMask4ToCidr(ipMask net.IPMask) (cidr uint8, err error) { + + var ones int + var total int + + ones, total = ipMask.Size() + + if total != 32 { + err = ErrBadNetFam + return + } + if ones > 32 { + err = ErrBadNetFam + return + } + + cidr = uint8(ones) + + return +} + +/* +IPMask4ToMask returns the mask *in bitmask form* from a [net.IPMask]. + +See also: + + * [IPMask4ToCidr] + * [IPMask4ToStr] + +Inverse of [Mask4ToIPMask]. +*/ +func IPMask4ToMask(ipMask net.IPMask) (mask uint32, err error) { + + var cidr uint8 + + if cidr, err = IPMask4ToCidr(ipMask); err != nil { + return + } + + if mask, err = Cidr4ToMask(cidr); err != nil { + return + } + + return +} + +/* +IPMask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a [net.IPMask]. + +See also: + + * [IPMask4ToCidr] + * [IPMask4ToMask] + +Inverse of [Mask4StrToIPMask]. +*/ +func IPMask4ToStr(ipMask net.IPMask) (maskStr string, err error) { + + var idx int + var b []byte + var quads []string = make([]string, 4) + + b = []byte(ipMask) + if len(b) != 4 { + err = ErrBadNetFam + return + } + + for idx = 0; idx < len(b); idx++ { + quads[idx] = strconv.Itoa(int(b[idx])) + } + + maskStr = strings.Join(quads, ".") + + return +} + +/* +Mask4ToCidr converts an IPv4 netmask *in bitmask form* to a CIDR prefix size/bit size/bit length. + +See also: + + * [Mask4ToIPMask] + * [Mask4ToStr] + +Inverse of [Cidr4ToMask]. +*/ +func Mask4ToCidr(mask uint32) (cidr uint8, err error) { + + cidr = 32 - uint8(bits.LeadingZeros32(mask)) + + return +} + +/* +Mask4ToIPMask returns mask *in bitmask form* as a [net.IPMask]. + +See also: + + * [Mask4ToCidr] + * [Mask4ToStr] + +Inverse of [IPMask4ToMask]. +*/ +func Mask4ToIPMask(mask uint32) (ipMask net.IPMask, err error) { + + var cidr uint8 + + if cidr, err = Mask4ToCidr(mask); err != nil { + return + } + + ipMask = net.CIDRMask(int(cidr), 32) + + return +} + +/* +Mask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a netmask *in bitmask form*. + +See also: + + * [Mask4ToCidr] + * [Mask4ToIPMask] + +Inverse of [Mask4StrToMask]. +*/ +func Mask4ToStr(mask uint32) (maskStr string, err error) { + + var ipMask net.IPMask + + if ipMask, err = Mask4ToIPMask(mask); err != nil { + return + } + + if maskStr, err = IPMask4ToStr(ipMask); err != nil { + return + } + + return +} + +/* +Mask4StrToCidr parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns am IPv4 CIDR/bit size/prefix length. + +See also: + + * [Mask4StrToIPMask] + * [Mask4StrToMask] + +Inverse of [Cidr4ToMaskStr]. +*/ +func Mask4StrToCidr(maskStr string) (cidr uint8, err error) { + + var ipMask net.IPMask + + if ipMask, err = Mask4StrToIPMask(maskStr); err != nil { + return + } + + if cidr, err = IPMask4ToCidr(ipMask); err != nil { + return + } + + return +} + +/* +Mask4StrToIPMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a [net.IPMask]. + +See also: + + * [Mask4StrToCidr] + * [Mask4StrToMask] + +Inverse of [IPMask4ToStr]. +*/ +func Mask4StrToIPMask(maskStr string) (mask net.IPMask, err error) { + + var idx int + var s string + var u64 uint64 + var b []byte = make([]byte, 4) + var sl []string = strings.Split(maskStr, ".") + + if len(sl) != 4 { + err = ErrBadMask4Str + return + } + + // A net.IPMask is just a []byte. + for idx = 0; idx < len(sl); idx++ { + s = sl[idx] + if u64, err = strconv.ParseUint(s, 10, 8); err != nil { + return + } + b[idx] = byte(u64) + } + + mask = net.IPMask(b) + + return +} + +/* +Mask4StrToMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a netmask *in bitmask form*. + +See also: + + * [Mask4StrToCidr] + * [Mask4StrToIPMask] + +Inverse of [Mask4ToStr]. +*/ +func Mask4StrToMask(maskStr string) (mask uint32, err error) { + + var ipMask net.IPMask + + if ipMask, err = Mask4StrToIPMask(maskStr); err != nil { + return + } + + if mask, err = IPMask4ToMask(ipMask); err != nil { + return + } + + return +} diff --git a/netx/funcs_test.go b/netx/funcs_test.go new file mode 100644 index 0000000..5d64762 --- /dev/null +++ b/netx/funcs_test.go @@ -0,0 +1,134 @@ +package netx + +import ( + `math` + `net` + `net/netip` + "testing" +) + +func TestFuncsIP(t *testing.T) { + + var err error + var ip net.IP + var addr netip.Addr + var ipFamily uint16 + var tgtFamily uint16 + var addrFamily uint16 + + // IPv4 on even indexes, IPv6 on odd. + for idx, s := range []string{ + "203.0.113.10", + "2001:db8::203:0:113:10", + } { + if ip = net.ParseIP(s); ip == nil { + t.Fatalf("ip %s not valid", s) + } + if addr, err = netip.ParseAddr(s); err != nil { + t.Fatalf("addr %s not valid", s) + } + ipFamily = GetIpFamily(ip) + addrFamily = GetAddrFamily(addr) + if ipFamily == AFUnspec { + t.Fatalf("GetIpFamily: Failed on IP %s (unspecified family)", s) + } + if addrFamily == AFUnspec { + t.Fatalf("GetAddrFamily: Failed on IP %s (unspecified family)", s) + } + switch idx%2 == 0 { + case true: + tgtFamily = AFInet + case false: + tgtFamily = AFInet6 + } + if ipFamily != tgtFamily { + t.Fatalf("GetIpFamily: Failed on IP %s (expected %d, got %d)", s, AFInet, tgtFamily) + } + if addrFamily != tgtFamily { + t.Fatalf("GetAddrFamily: Failed on IP %s (expected %d, got %d)", s, AFInet, tgtFamily) + } + } +} + +func TestFuncsMask(t *testing.T) { + + var err error + + var cidr uint8 + var mask uint32 + var maskStr string + var ipMask net.IPMask + + var cidrTgt uint8 = 32 + var maskTgt uint32 = math.MaxUint32 + var maskStrTgt string = "255.255.255.255" + var ipMaskTgt net.IPMask = net.IPMask{255, 255, 255, 255} + + // To CIDR + if cidr, err = Mask4ToCidr(maskTgt); err != nil { + t.Fatal(err) + } else if cidr != cidrTgt { + t.Fatalf("Mask4ToCidr: cidr %d != cidrTgt %d", cidr, cidrTgt) + } + if cidr, err = IPMask4ToCidr(ipMaskTgt); err != nil { + t.Fatal(err) + } else if cidr != cidrTgt { + t.Fatalf("IPMask4ToCidr: cidr %d != cidrTgt %d", cidr, cidrTgt) + } + if cidr, err = Mask4StrToCidr(maskStrTgt); err != nil { + t.Fatal(err) + } else if cidr != cidrTgt { + t.Fatalf("Mask4StrToCidr cidr %d != cidrTgt %d", cidr, cidrTgt) + } + + // To net.IPMask + if ipMask, err = Cidr4ToIPMask(cidrTgt); err != nil { + t.Fatal(err) + } else if ipMaskTgt.String() != ipMask.String() { + t.Fatalf("Cidr4ToIPMask ipMask %s != ipMaskTgt %s", ipMask.String(), ipMaskTgt.String()) + } + if ipMask, err = Mask4ToIPMask(maskTgt); err != nil { + t.Fatal(err) + } else if ipMaskTgt.String() != ipMask.String() { + t.Fatalf("Mask4ToIPMask ipMask %s != ipMaskTgt %s", ipMask.String(), ipMaskTgt.String()) + } + if ipMask, err = Mask4StrToIPMask(maskStrTgt); err != nil { + t.Fatal(err) + } else if ipMaskTgt.String() != ipMask.String() { + t.Fatalf("Mask4StrToIPMask ipMask %s != ipMaskTgt %s", ipMask.String(), ipMaskTgt.String()) + } + + // To bitmask + if mask, err = Cidr4ToMask(cidrTgt); err != nil { + t.Fatal(err) + } else if mask != maskTgt { + t.Fatalf("Cidr4ToMask mask %d != maskTgt %d", mask, maskTgt) + } + if mask, err = IPMask4ToMask(ipMaskTgt); err != nil { + t.Fatal(err) + } else if mask != maskTgt { + t.Fatalf("IPMask4ToMask mask %d != maskTgt %d", mask, maskTgt) + } + if mask, err = Mask4StrToMask(maskStrTgt); err != nil { + t.Fatal(err) + } else if mask != maskTgt { + t.Fatalf("Mask4StrToMask mask %d != maskTgt %d", mask, maskTgt) + } + + // To string + if maskStr, err = Cidr4ToStr(cidrTgt); err != nil { + t.Fatal(err) + } else if maskStr != maskStrTgt { + t.Fatalf("Cidr4ToStr maskStr %s != maskStrTgt %s", maskStr, maskStrTgt) + } + if maskStr, err = IPMask4ToStr(ipMaskTgt); err != nil { + t.Fatal(err) + } else if maskStr != maskStrTgt { + t.Fatalf("IPMask4ToStr maskStr %s != maskStrTgt %s", maskStr, maskStrTgt) + } + if maskStr, err = Mask4ToStr(maskTgt); err != nil { + t.Fatal(err) + } else if maskStr != maskStrTgt { + t.Fatalf("Mask4ToStr maskStr %s != maskStrTgt %s", maskStr, maskStrTgt) + } +}