Hey there! In our previous articles, we covered Addr and AddrPort types. Today, we're diving into the Prefix type, which represents an IP network in CIDR notation. If you've ever worked with subnetting or network configuration, you know how important this is. Let's make it less intimidating and more practical.
Understanding Prefix
A Prefix represents an IP address range using CIDR (Classless Inter-Domain Routing) notation. For example:
- 192.168.1.0/24 represents 256 IPv4 addresses (192.168.1.0 to 192.168.1.255)
- 2001:db8::/32 represents a large IPv6 network
Creating and Parsing Prefixes
Let's start with the basics:
package main
import (
"fmt"
"net/netip"
)
func main() {
// Parse from CIDR string
prefix, err := netip.ParsePrefix("192.168.1.0/24")
if err != nil {
panic(err)
}
// Create from Addr and bits
addr := netip.MustParseAddr("192.168.1.0")
prefix2 := netip.PrefixFrom(addr, 24)
fmt.Printf("From string: %v\nFrom components: %v\n", prefix, prefix2)
}
Some important validation rules:
- Bits must be valid (0-32 for IPv4, 0-128 for IPv6)
- The host portion of the address should be zero
- The address must be valid
Deep Dive into Prefix Methods
Let's explore all the key methods you'll need when working with Prefix.
Basic Properties
func explorePrefix(p netip.Prefix) {
// Get the network address
addr := p.Addr()
fmt.Printf("Network address: %v\n", addr)
// Get the prefix length (bits)
bits := p.Bits()
fmt.Printf("Prefix length: %d\n", bits)
// Check if it's IPv4 or IPv6
fmt.Printf("Is IPv4: %v\n", p.Addr().Is4())
fmt.Printf("Is IPv6: %v\n", p.Addr().Is6())
// Check if it represents a single IP
fmt.Printf("Is single IP: %v\n", p.IsSingleIP())
}
Network Operations
Here's where Prefix really shines - lets look at containment and overlap checks:
func networkOperations() {
network := netip.MustParsePrefix("192.168.1.0/24")
// Check if an IP is in the network
ip := netip.MustParseAddr("192.168.1.100")
fmt.Printf("Contains IP? %v\n", network.Contains(ip))
// Check if a smaller network is contained
subnet := netip.MustParsePrefix("192.168.1.0/25")
fmt.Printf("Contains subnet? %v\n", network.Contains(subnet.Addr()))
// Check if networks overlap
other := netip.MustParsePrefix("192.168.1.128/25")
fmt.Printf("Overlaps? %v\n", network.Overlaps(other))
}
Real-World Applications
Let's look at some practical applications of Prefix.
1. IPAM (IP Address Management) System
Here's a simple IPAM system that manages IP allocation within a network:
type IPAM struct {
network netip.Prefix
used map[netip.Addr]bool
mu sync.Mutex
}
func NewIPAM(network string) (*IPAM, error) {
prefix, err := netip.ParsePrefix(network)
if err != nil {
return nil, fmt.Errorf("invalid network: %w", err)
}
return &IPAM{
network: prefix,
used: make(map[netip.Addr]bool),
}, nil
}
func (ip *IPAM) Allocate() (netip.Addr, error) {
ip.mu.Lock()
defer ip.mu.Unlock()
// Start from network address + 1
addr := ip.network.Addr()
for {
addr = addr.Next()
// Check if we're still in network
if !ip.network.Contains(addr) {
return netip.Addr{}, fmt.Errorf("no addresses available")
}
// Skip if already used
if ip.used[addr] {
continue
}
// Found an available address
ip.used[addr] = true
return addr, nil
}
}
func (ip *IPAM) Release(addr netip.Addr) error {
ip.mu.Lock()
defer ip.mu.Unlock()
if !ip.network.Contains(addr) {
return fmt.Errorf("address %v not in network %v", addr, ip.network)
}
delete(ip.used, addr)
return nil
}
2. Subnet Calculator
Here's a useful tool for network planning:
type SubnetInfo struct {
Network netip.Prefix
NumIPs int
FirstUsable netip.Addr
LastUsable netip.Addr
Broadcast netip.Addr // IPv4 only
}
func AnalyzeSubnet(prefix netip.Prefix) SubnetInfo {
info := SubnetInfo{
Network: prefix,
}
if prefix.Addr().Is4() {
// IPv4 calculations
bits := 32 - prefix.Bits()
info.NumIPs = 1 << bits
first := prefix.Addr()
last := first
for i := 0; i < info.NumIPs-1; i++ {
last = last.Next()
}
// For IPv4, first and last addresses are network and broadcast
if info.NumIPs > 2 {
info.FirstUsable = first.Next()
info.LastUsable = last.Prev()
info.Broadcast = last
} else {
info.FirstUsable = first
info.LastUsable = last
}
} else {
// IPv6 calculations
bits := 128 - prefix.Bits()
if bits < 64 {
info.NumIPs = 1 << bits
} else {
info.NumIPs = -1 // Too large to represent
}
info.FirstUsable = prefix.Addr()
// Last address calculation omitted for brevity
// (would need big.Int for proper calculation)
}
return info
}
3. Firewall Rule Manager
Here's how you might use Prefix in a simple firewall rule system:
type Action string
const (
Allow Action = "allow"
Deny Action = "deny"
)
type Rule struct {
Network netip.Prefix
Action Action
}
type Firewall struct {
rules []Rule
}
func NewFirewall() *Firewall {
return &Firewall{}
}
func (fw *Firewall) AddRule(network string, action Action) error {
prefix, err := netip.ParsePrefix(network)
if err != nil {
return fmt.Errorf("invalid network: %w", err)
}
fw.rules = append(fw.rules, Rule{
Network: prefix,
Action: action,
})
return nil
}
func (fw *Firewall) CheckAccess(addr netip.Addr) Action {
// Check rules in order (last matching rule wins)
result := Allow // default
for _, rule := range fw.rules {
if rule.Network.Contains(addr) {
result = rule.Action
}
}
return result
}
Advanced Operations
1. Subnet Division
Here's how to divide a network into smaller subnets:
func divideNetwork(network netip.Prefix, newBits int) ([]netip.Prefix, error) {
if newBits <= network.Bits() {
return nil, fmt.Errorf("new prefix length must be larger than current")
}
var subnets []netip.Prefix
current := network.Addr()
// Calculate number of subnets
numSubnets := 1 << (newBits - network.Bits())
for i := 0; i < numSubnets; i++ {
subnet := netip.PrefixFrom(current, newBits)
subnets = append(subnets, subnet)
// Skip to next subnet
for j := 0; j < 1<<(32-newBits); j++ {
current = current.Next()
}
}
return subnets, nil
}
2. Network Aggregation
This function tries to combine adjacent networks when possible:
func aggregateNetworks(networks []netip.Prefix) []netip.Prefix {
if len(networks) < 2 {
return networks
}
// Sort networks first
sort.Slice(networks, func(i, j int) bool {
return networks[i].Addr().Less(networks[j].Addr())
})
var result []netip.Prefix
current := networks[0]
for i := 1; i < len(networks); i++ {
// Check if networks can be combined
if canCombine(current, networks[i]) {
// Create a new network with one less bit
current = netip.PrefixFrom(current.Addr(), current.Bits()-1)
} else {
result = append(result, current)
current = networks[i]
}
}
result = append(result, current)
return result
}
func canCombine(a, b netip.Prefix) bool {
// Networks must have same prefix length
if a.Bits() != b.Bits() {
return false
}
// Networks must be adjacent
bits := a.Bits() - 1
parentPrefix := netip.PrefixFrom(a.Addr(), bits)
return parentPrefix.Contains(a.Addr()) && parentPrefix.Contains(b.Addr())
}
Best Practices
- Always Validate Input
func validateNetwork(input string) error {
prefix, err := netip.ParsePrefix(input)
if err != nil {
return fmt.Errorf("invalid network %q: %w", input, err)
}
// Ensure host bits are zero
if prefix.Addr().String() != prefix.Addr().Masking(prefix.Bits()).String() {
return fmt.Errorf("network address has non-zero host bits")
}
return nil
}
- Handle IPv4 and IPv6 Appropriately
func getNetworkType(prefix netip.Prefix) string {
addr := prefix.Addr()
if addr.Is4() {
return fmt.Sprintf("IPv4 /%d", prefix.Bits())
}
return fmt.Sprintf("IPv6 /%d", prefix.Bits())
}
- Use Contains() for Network Membership
func isInNetwork(network netip.Prefix, addrs ...netip.Addr) bool {
for _, addr := range addrs {
if !network.Contains(addr) {
return false
}
}
return true
}
What's Next?
In our next article, we'll explore IP address set operations, which build upon everything we've learned about Addr and Prefix to handle complex network range manipulations.
Until then, happy networking! Remember, CIDR might seem complex at first, but with net/netip's Prefix type, it becomes much more manageable. Take your time experimenting with these examples, and don't hesitate to modify them for your specific needs.
Top comments (0)