Add ExclusionChecker implementation for validating and filtering IP ranges by exclusion rules
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExclusionChecker is an interface for checking if an IP is excluded.
|
||||
type ExclusionChecker interface {
|
||||
IsExcluded(ip string) (excluded bool, ips []string, err error)
|
||||
}
|
||||
|
||||
type ipRange struct {
|
||||
start netip.Addr
|
||||
end netip.Addr
|
||||
isV4 bool
|
||||
}
|
||||
|
||||
type exclusionChecker struct {
|
||||
ipRanges []ipRange
|
||||
}
|
||||
|
||||
// NewExclusionChecker creates a new exclusion checker.
|
||||
func NewExclusionChecker(ips []string) (ExclusionChecker, error) {
|
||||
ipRanges := make([]ipRange, 0, len(ips))
|
||||
|
||||
for _, ip := range ips {
|
||||
ipRange, err := parseSpec(ip)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid exclusion IP: %s", ip)
|
||||
}
|
||||
|
||||
ipRanges = append(ipRanges, ipRange)
|
||||
}
|
||||
|
||||
return &exclusionChecker{
|
||||
ipRanges: ipRanges,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsExcluded checks if the given IP is excluded.
|
||||
func (e *exclusionChecker) IsExcluded(ip string) (excluded bool, ips []string, err error) {
|
||||
inIPRange, err := parseSpec(ip)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
pending := []ipRange{inIPRange}
|
||||
matchedAny := false
|
||||
|
||||
for _, baseRange := range e.ipRanges {
|
||||
// Different IP families do not overlap.
|
||||
if baseRange.isV4 != inIPRange.isV4 {
|
||||
continue
|
||||
}
|
||||
|
||||
nextPending := make([]ipRange, 0, len(pending))
|
||||
for _, candidate := range pending {
|
||||
matched, remains := subtractRange(candidate, baseRange)
|
||||
if matched {
|
||||
matchedAny = true
|
||||
}
|
||||
nextPending = append(nextPending, remains...)
|
||||
}
|
||||
|
||||
pending = nextPending
|
||||
if len(pending) == 0 {
|
||||
// Completely excluded the range.
|
||||
return true, []string{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
if !matchedAny {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(pending))
|
||||
for _, r := range pending {
|
||||
out = append(out, formatRange(r))
|
||||
}
|
||||
return true, out, nil
|
||||
}
|
||||
|
||||
func parseSpec(s string) (ipRange, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ipRange{}, fmt.Errorf("empty value")
|
||||
}
|
||||
|
||||
// range: a-b
|
||||
if strings.Contains(s, "-") {
|
||||
parts := strings.Split(s, "-")
|
||||
if len(parts) != 2 {
|
||||
return ipRange{}, fmt.Errorf("invalid range format")
|
||||
}
|
||||
a, err := parseAddr(strings.TrimSpace(parts[0]))
|
||||
if err != nil {
|
||||
return ipRange{}, err
|
||||
}
|
||||
b, err := parseAddr(strings.TrimSpace(parts[1]))
|
||||
if err != nil {
|
||||
return ipRange{}, err
|
||||
}
|
||||
if a.Is4() != b.Is4() {
|
||||
return ipRange{}, fmt.Errorf("mixed ip families in range")
|
||||
}
|
||||
if compareAddr(a, b) > 0 {
|
||||
return ipRange{}, fmt.Errorf("range start > end")
|
||||
}
|
||||
return ipRange{start: a, end: b, isV4: a.Is4()}, nil
|
||||
}
|
||||
|
||||
// cidr
|
||||
if strings.Contains(s, "/") {
|
||||
p, err := netip.ParsePrefix(s)
|
||||
if err != nil {
|
||||
return ipRange{}, fmt.Errorf("invalid cidr: %w", err)
|
||||
}
|
||||
p = p.Masked()
|
||||
start := p.Addr()
|
||||
|
||||
var end netip.Addr
|
||||
if start.Is4() {
|
||||
u := ip4ToU32(start)
|
||||
hostBits := uint32(32 - p.Bits())
|
||||
var mask uint32
|
||||
if hostBits == 32 {
|
||||
mask = ^uint32(0)
|
||||
} else {
|
||||
mask = (uint32(1) << hostBits) - 1
|
||||
}
|
||||
end = u32ToIP4(u | mask)
|
||||
return ipRange{start: start, end: end, isV4: true}, nil
|
||||
}
|
||||
|
||||
u := ip16ToBig(start)
|
||||
hostBits := 128 - p.Bits()
|
||||
ones := new(big.Int).Lsh(big.NewInt(1), uint(hostBits))
|
||||
ones.Sub(ones, big.NewInt(1))
|
||||
u.Or(u, ones)
|
||||
end = bigToIP16(u)
|
||||
return ipRange{start: start, end: end, isV4: false}, nil
|
||||
}
|
||||
|
||||
// single ip
|
||||
a, err := parseAddr(s)
|
||||
if err != nil {
|
||||
return ipRange{}, err
|
||||
}
|
||||
return ipRange{start: a, end: a, isV4: a.Is4()}, nil
|
||||
}
|
||||
|
||||
func parseAddr(s string) (netip.Addr, error) {
|
||||
a, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("invalid ip: %w", err)
|
||||
}
|
||||
return a.Unmap(), nil // important for ::ffff:1.2.3.4
|
||||
}
|
||||
|
||||
func compareAddr(a, b netip.Addr) int {
|
||||
if a.Is4() && b.Is4() {
|
||||
ua := ip4ToU32(a)
|
||||
ub := ip4ToU32(b)
|
||||
switch {
|
||||
case ua < ub:
|
||||
return -1
|
||||
case ua > ub:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
aa := a.As16()
|
||||
bb := b.As16()
|
||||
for i := 0; i < 16; i++ {
|
||||
if aa[i] < bb[i] {
|
||||
return -1
|
||||
}
|
||||
if aa[i] > bb[i] {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func ip4ToU32(a netip.Addr) uint32 {
|
||||
v := a.As4()
|
||||
return binary.BigEndian.Uint32(v[:])
|
||||
}
|
||||
|
||||
func u32ToIP4(u uint32) netip.Addr {
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], u)
|
||||
return netip.AddrFrom4(b)
|
||||
}
|
||||
|
||||
func ip16ToBig(a netip.Addr) *big.Int {
|
||||
v := a.As16()
|
||||
return new(big.Int).SetBytes(v[:])
|
||||
}
|
||||
|
||||
func bigToIP16(x *big.Int) netip.Addr {
|
||||
var b [16]byte
|
||||
raw := x.Bytes()
|
||||
copy(b[16-len(raw):], raw)
|
||||
return netip.AddrFrom16(b)
|
||||
}
|
||||
|
||||
func subtractRange(incoming, base ipRange) (bool, []ipRange) {
|
||||
// No intersection
|
||||
if compareAddr(base.end, incoming.start) < 0 || compareAddr(base.start, incoming.end) > 0 {
|
||||
return false, []ipRange{incoming}
|
||||
}
|
||||
|
||||
// Full incoming coverage
|
||||
if compareAddr(base.start, incoming.start) <= 0 && compareAddr(base.end, incoming.end) >= 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
remains := make([]ipRange, 0, 2)
|
||||
|
||||
// Left side
|
||||
if compareAddr(base.start, incoming.start) > 0 {
|
||||
leftEnd, ok := prevIP(base.start)
|
||||
if ok && compareAddr(incoming.start, leftEnd) <= 0 {
|
||||
remains = append(remains, ipRange{
|
||||
start: incoming.start,
|
||||
end: leftEnd,
|
||||
isV4: incoming.isV4,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Right side
|
||||
if compareAddr(base.end, incoming.end) < 0 {
|
||||
rightStart, ok := nextIP(base.end)
|
||||
if ok && compareAddr(rightStart, incoming.end) <= 0 {
|
||||
remains = append(remains, ipRange{
|
||||
start: rightStart,
|
||||
end: incoming.end,
|
||||
isV4: incoming.isV4,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return true, remains
|
||||
}
|
||||
|
||||
func nextIP(a netip.Addr) (netip.Addr, bool) {
|
||||
if a.Is4() {
|
||||
u := ip4ToU32(a)
|
||||
if u == ^uint32(0) {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
return u32ToIP4(u + 1), true
|
||||
}
|
||||
|
||||
u := ip16ToBig(a)
|
||||
max := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
max.Sub(max, big.NewInt(1))
|
||||
if u.Cmp(max) == 0 {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
u.Add(u, big.NewInt(1))
|
||||
return bigToIP16(u), true
|
||||
}
|
||||
|
||||
func prevIP(a netip.Addr) (netip.Addr, bool) {
|
||||
if a.Is4() {
|
||||
u := ip4ToU32(a)
|
||||
if u == 0 {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
return u32ToIP4(u - 1), true
|
||||
}
|
||||
|
||||
u := ip16ToBig(a)
|
||||
if u.Sign() == 0 {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
u.Sub(u, big.NewInt(1))
|
||||
return bigToIP16(u), true
|
||||
}
|
||||
|
||||
func formatRange(r ipRange) string {
|
||||
if r.start == r.end {
|
||||
return r.start.String()
|
||||
}
|
||||
return r.start.String() + "-" + r.end.String()
|
||||
}
|
||||
Reference in New Issue
Block a user