diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index b33b5cb..a497f09 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "log/slog" + "strconv" + "strings" "sync/atomic" ttl_map "github.com/leprosus/golang-ttl-map" @@ -27,8 +29,17 @@ const ( CacheMiss = "cache:miss" // CacheUnreachable error string when cache is unreachable. CacheUnreachable = "cache:unreachable" - // cidrPrefix store all cidr within the same key. + // cidrPrefix namespaces a CIDR decision, one cache key per CIDR. cidrPrefix = "cidr:" + // cidrPrefixLensKey holds the prefix lengths that have a decision, so a lookup probes + // only those. Its absence means no CIDR decision was ever stored. + cidrPrefixLensKey = "cidrprefixlens" + // cidrPrefixLensSeparator separates the prefix lengths in cidrPrefixLensKey. + cidrPrefixLensSeparator = "," + // cidrPrefixLensDuration has to outlive every decision it describes, so it is + // effectively infinite. It cannot be zero: the local cache ignores a zero duration + // and redis rejects a non positive EX. + cidrPrefixLensDuration = 10 * 365 * 24 * 60 * 60 ) //nolint:gochecknoglobals @@ -160,14 +171,15 @@ func (c *Client) DeleteCIDR(cidr string) { } // GetCIDR checks if an IP matches a CIDR decision in the cache. +// Only probes the prefix lengths that have a decision: it is on the request path. func (c *Client) GetCIDR(ipStr string) (string, error) { - keys := ip.CIDRKeys(ipStr) - if keys == nil { - return "", errors.New(CacheMiss) + prefixLens, err := c.cache.get(cidrPrefixLensKey) + if err != nil { + return "", err } - for _, key := range keys { - value, err := c.cache.get(cidrPrefix + key) - if err == nil { + for _, key := range ip.CIDRLookupKeys(ipStr, parsePrefixLens(prefixLens)) { + value, getErr := c.cache.get(cidrPrefix + key) + if getErr == nil { return value, nil } } @@ -176,10 +188,43 @@ func (c *Client) GetCIDR(ipStr string) (string, error) { // SetCIDR stores a CIDR decision in the cache. func (c *Client) SetCIDR(cidr, value string, duration int64) { - cidr = ip.NormalizeCIDR(cidr) - if cidr == "" { + normalized := ip.NormalizeCIDR(cidr) + prefixLen := ip.CIDRPrefixLen(cidr) + if normalized == "" || prefixLen < 0 { return } - c.cache.set(cidrPrefix+cidr, value, duration) - c.log.Debug(fmt.Sprintf("cache:SetCIDR cidr:%v value:%v duration:%vs", cidr, value, duration)) + // Publish the length first, or a concurrent lookup misses the decision. + c.addCIDRPrefixLen(prefixLen) + c.cache.set(cidrPrefix+normalized, value, duration) + c.log.Debug(fmt.Sprintf("cache:SetCIDR cidr:%v value:%v duration:%vs", normalized, value, duration)) +} + +// addCIDRPrefixLen records a prefix length in the set probed on lookup. The set only grows: +// a stale length costs one extra read, dropping one too early leaves decisions unmatched. +func (c *Client) addCIDRPrefixLen(prefixLen int) { + prefixLens, err := c.cache.get(cidrPrefixLensKey) + if err == nil { + for _, known := range parsePrefixLens(prefixLens) { + if known == prefixLen { + return + } + } + prefixLens += cidrPrefixLensSeparator + strconv.Itoa(prefixLen) + } else { + prefixLens = strconv.Itoa(prefixLen) + } + c.cache.set(cidrPrefixLensKey, prefixLens, cidrPrefixLensDuration) + c.log.Debug(fmt.Sprintf("cache:addCIDRPrefixLen prefixLens:%v", prefixLens)) +} + +// parsePrefixLens decodes the set of prefix lengths stored in cidrPrefixLensKey. +func parsePrefixLens(value string) []int { + fields := strings.Split(value, cidrPrefixLensSeparator) + prefixLens := make([]int, 0, len(fields)) + for _, field := range fields { + if prefixLen, err := strconv.Atoi(field); err == nil { + prefixLens = append(prefixLens, prefixLen) + } + } + return prefixLens } diff --git a/pkg/ip/cidr.go b/pkg/ip/cidr.go index c30e189..bf9ead4 100644 --- a/pkg/ip/cidr.go +++ b/pkg/ip/cidr.go @@ -5,6 +5,11 @@ import ( "strings" ) +const ( + maxIPv4PrefixLen = 32 + maxIPv6PrefixLen = 128 +) + // CIDRKeys returns all possible CIDR prefixes of an IP, from the most specific (/32 for IPv4, /128 for IPv6) to the least specific (/0). func CIDRKeys(ipStr string) []string { parsed, maxBits := parseForPrefix(ipStr) @@ -18,6 +23,28 @@ func CIDRKeys(ipStr string) []string { return keys } +// CIDRLookupKeys returns the keys of the CIDRs containing an IP for the given prefix lengths +// only, most specific first. Duplicates and lengths of the other family are skipped. +func CIDRLookupKeys(ipStr string, prefixLens []int) []string { + parsed, maxBits := parseForPrefix(ipStr) + if parsed == nil { + return nil + } + var wanted [maxIPv6PrefixLen + 1]bool + for _, bits := range prefixLens { + if bits >= 0 && bits <= maxBits { + wanted[bits] = true + } + } + keys := make([]string, 0, len(prefixLens)) + for bits := maxBits; bits >= 0; bits-- { + if wanted[bits] { + keys = append(keys, cidrKey(parsed, bits, maxBits)) + } + } + return keys +} + // NormalizeCIDR parses a CIDR string and returns its normalized form, or an empty string if invalid. func NormalizeCIDR(cidrStr string) string { _, ipNet, err := net.ParseCIDR(strings.TrimSpace(cidrStr)) @@ -27,6 +54,16 @@ func NormalizeCIDR(cidrStr string) string { return ipNet.String() } +// CIDRPrefixLen returns the prefix length of a CIDR, or -1 if it is not a valid CIDR. +func CIDRPrefixLen(cidrStr string) int { + _, ipNet, err := net.ParseCIDR(strings.TrimSpace(cidrStr)) + if err != nil { + return -1 + } + prefixLen, _ := ipNet.Mask.Size() + return prefixLen +} + // parseForPrefix returns the IP in the native form of its family, and that family's bit length. func parseForPrefix(ipStr string) (net.IP, int) { parsed := net.ParseIP(ipStr) @@ -34,9 +71,9 @@ func parseForPrefix(ipStr string) (net.IP, int) { return nil, 0 } if parsed4 := parsed.To4(); parsed4 != nil { - return parsed4, 32 + return parsed4, maxIPv4PrefixLen } - return parsed.To16(), 128 + return parsed.To16(), maxIPv6PrefixLen } // cidrKey builds the key of the CIDR of bits length containing the IP.