Check Client IPs Against a CIDR Allowlist

Build an IP allowlist from CIDR prefixes and check IPv4 or IPv6 addresses without allocating or trusting raw strings.

GoSecurity Boundaries

Code

snippet
package main

import (
	"fmt"
	"net/netip"
)

type Allowlist struct {
	prefixes []netip.Prefix
}

func NewAllowlist(cidrs []string) (*Allowlist, error) {
	list := &Allowlist{}
	for _, cidr := range cidrs {
		prefix, err := netip.ParsePrefix(cidr)
		if err != nil {
			return nil, fmt.Errorf("parse %q: %w", cidr, err)
		}
		list.prefixes = append(list.prefixes, prefix.Masked())
	}
	return list, nil
}

func (a *Allowlist) Allows(raw string) bool {
	address, err := netip.ParseAddr(raw)
	if err != nil {
		return false
	}
	for _, prefix := range a.prefixes {
		if prefix.Contains(address) {
			return true
		}
	}
	return false
}

func main() {
	list, err := NewAllowlist([]string{"10.0.0.0/8", "192.168.1.0/24", "2001:db8::/32"})
	if err != nil {
		panic(err)
	}
	cases := map[string]bool{
		"10.2.3.4":      true,
		"192.168.1.200": true,
		"192.168.2.1":   false,
		"2001:db8::1":   true,
		"203.0.113.9":   false,
	}
	for address, want := range cases {
		got := list.Allows(address)
		fmt.Printf("%-15s allowed=%v\n", address, got)
		if got != want {
			panic("unexpected result for " + address)
		}
	}
	if _, err := NewAllowlist([]string{"10.0.0.0/33"}); err == nil {
		panic("invalid prefix should fail")
	}
}