Files
drip/internal/shared/netutil/ipaccess.go
Gouryella 852dbb2ee6 feat(netutil): extend IP access checker blacklist from single IP to CIDR ranges
Rename denyIPs field to denyNets, supporting blacklist configuration with CIDR ranges. Now supports both individual IP addresses and CIDR subnet masks as deny rules, with IPv4 automatically converted to /32 and IPv6 to /128, using the Contains method for more flexible subnet matching.
2026-01-12 11:50:34 +08:00

133 lines
3.2 KiB
Go

package netutil
import (
"net"
"strings"
)
// IPAccessChecker checks if an IP address is allowed based on whitelist/blacklist rules.
type IPAccessChecker struct {
allowNets []*net.IPNet // Allowed CIDR ranges (whitelist)
denyNets []*net.IPNet // Denied CIDR ranges (blacklist)
hasAllow bool // Whether whitelist is configured
hasDeny bool // Whether blacklist is configured
}
// NewIPAccessChecker creates a new IP access checker from CIDR and IP lists.
// allowCIDRs: list of CIDR ranges to allow (e.g., "192.168.1.0/24", "10.0.0.0/8")
// denyIPs: list of CIDR ranges or IP addresses to deny (e.g., "192.168.0.0/16", "1.2.3.4")
func NewIPAccessChecker(allowCIDRs, denyIPs []string) *IPAccessChecker {
checker := &IPAccessChecker{}
// Parse allowed CIDRs
for _, cidr := range allowCIDRs {
cidr = strings.TrimSpace(cidr)
if cidr == "" {
continue
}
// If no "/" in the string, treat it as a single IP (/32 for IPv4, /128 for IPv6)
if !strings.Contains(cidr, "/") {
ip := net.ParseIP(cidr)
if ip != nil {
if ip.To4() != nil {
cidr = cidr + "/32"
} else {
cidr = cidr + "/128"
}
}
}
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
continue
}
checker.allowNets = append(checker.allowNets, ipNet)
}
checker.hasAllow = len(checker.allowNets) > 0
// Parse denied IPs/CIDRs
for _, ipStr := range denyIPs {
ipStr = strings.TrimSpace(ipStr)
if ipStr == "" {
continue
}
// If no "/" in the string, treat it as a single IP (/32 for IPv4, /128 for IPv6)
if !strings.Contains(ipStr, "/") {
ip := net.ParseIP(ipStr)
if ip != nil {
if ip.To4() != nil {
ipStr = ipStr + "/32"
} else {
ipStr = ipStr + "/128"
}
}
}
_, ipNet, err := net.ParseCIDR(ipStr)
if err != nil {
continue
}
checker.denyNets = append(checker.denyNets, ipNet)
}
checker.hasDeny = len(checker.denyNets) > 0
return checker
}
// IsAllowed checks if the given IP address is allowed.
// Rules:
// 1. If IP is in deny list, reject
// 2. If whitelist is configured and IP is not in whitelist, reject
// 3. Otherwise, allow
func (c *IPAccessChecker) IsAllowed(ipStr string) bool {
if c == nil || (!c.hasAllow && !c.hasDeny) {
return true // No rules configured, allow all
}
ip := net.ParseIP(ipStr)
if ip == nil {
return false // Invalid IP, reject
}
// Check deny list first (blacklist takes priority)
if c.hasDeny {
for _, denyNet := range c.denyNets {
if denyNet.Contains(ip) {
return false
}
}
}
// Check allow list (whitelist)
if c.hasAllow {
for _, allowNet := range c.allowNets {
if allowNet.Contains(ip) {
return true
}
}
return false // Whitelist configured but IP not in it
}
return true // No whitelist, and not in blacklist
}
// HasRules returns true if any access control rules are configured.
func (c *IPAccessChecker) HasRules() bool {
return c != nil && (c.hasAllow || c.hasDeny)
}
// ExtractIP extracts the IP address from a remote address string (e.g., "192.168.1.1:12345").
func ExtractIP(remoteAddr string) string {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
// Maybe it's just an IP without port
if ip := net.ParseIP(remoteAddr); ip != nil {
return remoteAddr
}
return ""
}
return host
}