DEV Community

Rez Moss
Rez Moss

Posted on

In-depth Guide to net/netip Prefix Methods 7/7

Hey there! We've made it to our final deep dive into net/netip's core types. Today we're focusing on the Prefix type and its methods. If you've worked with networks, you're familiar with CIDR notation (like 192.168.1.0/24). That's exactly what Prefix handles, and we're going to explore every method you can use with it.

Core Method Exploration

Let's start by looking at all the ways to create and work with Prefix.

Creation and Parsing

package main

import (
    "fmt"
    "net/netip"
)

func demoPrefixCreation() {
    // From CIDR string
    prefix1, _ := netip.ParsePrefix("192.168.1.0/24")

    // From Addr and bits
    addr := netip.MustParseAddr("192.168.1.0")
    prefix2 := netip.PrefixFrom(addr, 24)

    fmt.Printf("From string: %v\n", prefix1)
    fmt.Printf("From components: %v\n", prefix2)

    // Parsing with validation
    prefixes := []string{
        "192.168.1.1/24",    // Host bits set
        "192.168.1.0/24",    // Correct form
        "2001:db8::/32",     // IPv6
        "invalid/24",        // Invalid
        "192.168.1.0/33",    // Invalid mask
    }

    for _, p := range prefixes {
        if prefix, err := netip.ParsePrefix(p); err != nil {
            fmt.Printf("%s - Error: %v\n", p, err)
        } else {
            fmt.Printf("%s - Valid: %v\n", p, prefix)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Core Methods

Let's explore the essential methods every Prefix provides:

func explorePrefixMethods(p netip.Prefix) {
    // Basic properties
    fmt.Printf("Prefix: %v\n", p)
    fmt.Printf("Address: %v\n", p.Addr())
    fmt.Printf("Bits: %d\n", p.Bits())

    // Network properties
    fmt.Printf("Contains own address? %v\n", p.Contains(p.Addr()))
    fmt.Printf("Is single IP? %v\n", p.IsSingleIP())

    // String representation
    fmt.Printf("String form: %s\n", p.String())

    // Masking
    fmt.Printf("Masked address: %v\n", p.Addr().Masking(p.Bits()))
}
Enter fullscreen mode Exit fullscreen mode

Real-World Applications

1. IPAM (IP Address Management) System

A comprehensive IPAM system using Prefix:

type IPRange struct {
    Network netip.Prefix
    Used    map[netip.Addr]bool
    mu      sync.RWMutex
}

type IPAM struct {
    ranges map[string]*IPRange  // Key is range name
    mu     sync.RWMutex
}

func NewIPAM() *IPAM {
    return &IPAM{
        ranges: make(map[string]*IPRange),
    }
}

func (ipam *IPAM) AddRange(name string, cidr string) error {
    prefix, err := netip.ParsePrefix(cidr)
    if err != nil {
        return fmt.Errorf("invalid CIDR: %w", err)
    }

    ipam.mu.Lock()
    defer ipam.mu.Unlock()

    if _, exists := ipam.ranges[name]; exists {
        return fmt.Errorf("range %q already exists", name)
    }

    ipam.ranges[name] = &IPRange{
        Network: prefix,
        Used:    make(map[netip.Addr]bool),
    }

    return nil
}

func (ipam *IPAM) AllocateIP(rangeName string) (netip.Addr, error) {
    ipam.mu.RLock()
    r, exists := ipam.ranges[rangeName]
    ipam.mu.RUnlock()

    if !exists {
        return netip.Addr{}, fmt.Errorf("range %q not found", rangeName)
    }

    r.mu.Lock()
    defer r.mu.Unlock()

    addr := r.Network.Addr()
    maxHosts := uint64(1) << (32 - r.Network.Bits())  // For IPv4

    for i := uint64(1); i < maxHosts-1; i++ {  // Skip network and broadcast
        addr = addr.Next()
        if !r.Used[addr] {
            r.Used[addr] = true
            return addr, nil
        }
    }

    return netip.Addr{}, fmt.Errorf("no available addresses in range %q", rangeName)
}

func (ipam *IPAM) ReleaseIP(rangeName string, addr netip.Addr) error {
    ipam.mu.RLock()
    r, exists := ipam.ranges[rangeName]
    ipam.mu.RUnlock()

    if !exists {
        return fmt.Errorf("range %q not found", rangeName)
    }

    r.mu.Lock()
    defer r.mu.Unlock()

    if !r.Network.Contains(addr) {
        return fmt.Errorf("address %v is not in range %q", addr, rangeName)
    }

    delete(r.Used, addr)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

2. Subnet Calculator

A tool for network planning and analysis:

type SubnetInfo struct {
    Network       netip.Prefix
    FirstUsable   netip.Addr
    LastUsable    netip.Addr
    NumHosts      uint64
    BroadcastAddr netip.Addr  // IPv4 only
}

func AnalyzeSubnet(prefix netip.Prefix) (SubnetInfo, error) {
    info := SubnetInfo{Network: prefix}

    if !prefix.IsValid() {
        return info, fmt.Errorf("invalid prefix")
    }

    if prefix.Addr().Is4() {
        // IPv4 calculations
        bits := 32 - prefix.Bits()
        info.NumHosts = (1 << bits) - 2  // Subtract network and broadcast

        network := prefix.Addr()
        info.FirstUsable = network.Next()

        // Calculate broadcast address
        broadcast := network
        for i := 0; i < 1<<bits-1; i++ {
            broadcast = broadcast.Next()
        }
        info.BroadcastAddr = broadcast
        info.LastUsable = broadcast.Prev()
    } else {
        // IPv6 calculations
        bits := 128 - prefix.Bits()
        if bits > 64 {
            info.NumHosts = 0  // Too large to represent
        } else {
            info.NumHosts = 1 << bits
        }

        info.FirstUsable = prefix.Addr()
        // IPv6 doesn't use broadcast addresses
        info.LastUsable = info.FirstUsable
        for i := 0; i < 1<<bits-1; i++ {
            info.LastUsable = info.LastUsable.Next()
        }
    }

    return info, nil
}

func SubnetNetwork(prefix netip.Prefix, newBits int) ([]netip.Prefix, error) {
    if newBits <= prefix.Bits() {
        return nil, fmt.Errorf("new prefix must be larger than current")
    }

    if prefix.Addr().Is4() && newBits > 32 {
        return nil, fmt.Errorf("invalid IPv4 prefix length")
    }
    if prefix.Addr().Is6() && newBits > 128 {
        return nil, fmt.Errorf("invalid IPv6 prefix length")
    }

    numSubnets := 1 << (newBits - prefix.Bits())
    subnets := make([]netip.Prefix, 0, numSubnets)

    current := prefix.Addr()
    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
}
Enter fullscreen mode Exit fullscreen mode

3. Network ACL Manager

A system for managing network access control lists:

type Action string

const (
    Allow Action = "allow"
    Deny  Action = "deny"
)

type ACLRule struct {
    Network netip.Prefix
    Action  Action
    Order   int
}

type NetworkACL struct {
    rules []ACLRule
    mu    sync.RWMutex
}

func NewNetworkACL() *NetworkACL {
    return &NetworkACL{}
}

func (acl *NetworkACL) AddRule(cidr string, action Action, order int) error {
    prefix, err := netip.ParsePrefix(cidr)
    if err != nil {
        return fmt.Errorf("invalid CIDR: %w", err)
    }

    acl.mu.Lock()
    defer acl.mu.Unlock()

    // Check for duplicate rules
    for _, rule := range acl.rules {
        if rule.Network == prefix && rule.Action == action {
            return fmt.Errorf("duplicate rule")
        }
    }

    acl.rules = append(acl.rules, ACLRule{
        Network: prefix,
        Action:  action,
        Order:   order,
    })

    // Sort rules by order
    sort.Slice(acl.rules, func(i, j int) bool {
        return acl.rules[i].Order < acl.rules[j].Order
    })

    return nil
}

func (acl *NetworkACL) CheckAccess(addr netip.Addr) Action {
    acl.mu.RLock()
    defer acl.mu.RUnlock()

    // Check rules in order
    for _, rule := range acl.rules {
        if rule.Network.Contains(addr) {
            return rule.Action
        }
    }

    return Deny  // Default deny
}

func (acl *NetworkACL) OptimizeRules() {
    acl.mu.Lock()
    defer acl.mu.Unlock()

    // Group rules by action
    allowRules := make(map[netip.Prefix]bool)
    denyRules := make(map[netip.Prefix]bool)

    for _, rule := range acl.rules {
        if rule.Action == Allow {
            allowRules[rule.Network] = true
        } else {
            denyRules[rule.Network] = true
        }
    }

    // TODO: Implement rule optimization logic
    // This could include:
    // - Merging adjacent networks
    // - Removing redundant rules
    // - Detecting conflicts
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Validate Prefix Creation
   func validatePrefix(prefix string) error {
       p, err := netip.ParsePrefix(prefix)
       if err != nil {
           return fmt.Errorf("invalid prefix: %w", err)
       }

       // Ensure network address is properly masked
       if p.Addr().String() != p.Addr().Masking(p.Bits()).String() {
           return fmt.Errorf("prefix contains host bits")
       }

       return nil
   }
Enter fullscreen mode Exit fullscreen mode
  1. Handle IPv4 and IPv6 Appropriately
   func getAddressFamily(p netip.Prefix) string {
       if p.Addr().Is4() {
           return fmt.Sprintf("IPv4/%d", p.Bits())
       }
       return fmt.Sprintf("IPv6/%d", p.Bits())
   }
Enter fullscreen mode Exit fullscreen mode
  1. Use Contains Efficiently
   func isInRange(networks []netip.Prefix, addr netip.Addr) bool {
       for _, net := range networks {
           if net.Contains(addr) {
               return true
           }
       }
       return false
   }
Enter fullscreen mode Exit fullscreen mode

Performance Tips

  1. Cache Parsed Prefixes
   type NetworkCache struct {
       prefixes map[string]netip.Prefix
       mu       sync.RWMutex
   }

   func (nc *NetworkCache) GetPrefix(cidr string) (netip.Prefix, error) {
       nc.mu.RLock()
       if prefix, ok := nc.prefixes[cidr]; ok {
           nc.mu.RUnlock()
           return prefix, nil
       }
       nc.mu.RUnlock()

       prefix, err := netip.ParsePrefix(cidr)
       if err != nil {
           return netip.Prefix{}, err
       }

       nc.mu.Lock()
       nc.prefixes[cidr] = prefix
       nc.mu.Unlock()

       return prefix, nil
   }
Enter fullscreen mode Exit fullscreen mode
  1. Efficient Network Checks
   // Bad: Converting to string unnecessarily
   if prefix.String() == "192.168.1.0/24" {
       // ...
   }

   // Good: Direct comparison
   if prefix == netip.MustParsePrefix("192.168.1.0/24") {
       // ...
   }
Enter fullscreen mode Exit fullscreen mode
  1. Batch Operations
   func processNetworks(prefixes []netip.Prefix) {
       // Process in chunks for better performance
       const chunkSize = 100
       for i := 0; i < len(prefixes); i += chunkSize {
           end := i + chunkSize
           if end > len(prefixes) {
               end = len(prefixes)
           }
           processChunk(prefixes[i:end])
       }
   }
Enter fullscreen mode Exit fullscreen mode

Series Conclusion

This concludes our deep dive into the net/netip package! We've covered:

  • Addr type and its methods
  • AddrPort for handling IP:port combinations
  • Prefix for working with CIDR networks

These types work together to provide a robust foundation for network programming in Go. The key benefits of using net/netip include:

  • Type safety
  • Memory efficiency
  • Clear semantics
  • Comprehensive functionality

Remember to check the Go documentation for updates and new features. The package continues to evolve with the language.

Keep exploring and building great networking applications with Go!

Top comments (0)