access.go

81 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
package router

import (
	"log"
	"net/http"
	"net/netip"
	"strings"
)

// IpBlockList denies any request whose client IP falls within one of the given
// CIDRs or bare IPs, responding 403; all others pass. Entries are parsed once
// at construction — a malformed entry panics, a boot-time programming error
// caught before the server starts.
//
//	router.IpBlockList("203.0.113.0/24", "198.51.100.7")
func IpBlockList(entries ...string) Handler {
	set := parsePrefixes(entries)
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if set.contains(clientAddr(r)) {
				http.Error(w, "Forbidden", http.StatusForbidden)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

// IpAllowList is the inverse of IpBlockList: only requests whose client IP falls
// within one of the given CIDRs or bare IPs pass; all others get 403.
//
//	router.IpAllowList("10.0.0.0/8")
func IpAllowList(entries ...string) Handler {
	set := parsePrefixes(entries)
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if !set.contains(clientAddr(r)) {
				http.Error(w, "Forbidden", http.StatusForbidden)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

// prefixSet is a parsed list of CIDRs. A bare IP becomes a host-length prefix.
type prefixSet []netip.Prefix

func parsePrefixes(entries []string) prefixSet {
	set := make(prefixSet, 0, len(entries))
	for _, e := range entries {
		e = strings.TrimSpace(e)
		if strings.Contains(e, "/") {
			p, err := netip.ParsePrefix(e)
			if err != nil {
				log.Panicf("router: invalid CIDR %q: %v", e, err)
			}
			set = append(set, p.Masked())
			continue
		}
		addr, err := netip.ParseAddr(e)
		if err != nil {
			log.Panicf("router: invalid IP %q: %v", e, err)
		}
		addr = addr.Unmap()
		set = append(set, netip.PrefixFrom(addr, addr.BitLen()))
	}
	return set
}

func (s prefixSet) contains(addr netip.Addr) bool {
	if !addr.IsValid() {
		return false
	}
	for _, p := range s {
		if p.Contains(addr) {
			return true
		}
	}
	return false
}