package ipguard import ( "net" "net/http" "strings" ) // ClientIP resolves the real caller IP from the request. // When the request arrives via the Caddy reverse proxy, the real client IP is // forwarded in the X-Real-IP header (set via "header_up X-Real-IP {remote_host}" // in the Caddyfile). RemoteAddr is used as a fallback for direct connections // (local development, health checks, etc.). func ClientIP(r *http.Request) net.IP { if r == nil { return nil } // Prefer X-Real-IP set by the trusted Caddy reverse proxy. if xri := strings.TrimSpace(r.Header.Get("X-Real-IP")); xri != "" { if ip := net.ParseIP(xri); ip != nil { return ip } } raw := strings.TrimSpace(r.RemoteAddr) if raw == "" { return nil } if ip := net.ParseIP(raw); ip != nil { return ip } host, _, err := net.SplitHostPort(raw) if err != nil { return nil } return net.ParseIP(host) } func parseCIDRs(raw []string) ([]*net.IPNet, error) { blocks := make([]*net.IPNet, 0, len(raw)) for _, item := range raw { clean := strings.TrimSpace(item) if clean == "" { continue } _, block, err := net.ParseCIDR(clean) if err != nil { return nil, err } blocks = append(blocks, block) } return blocks, nil } // Allowed reports whether clientIP is allowed by configured CIDRs. // Empty CIDR list means unrestricted access. func Allowed(clientIP net.IP, cidrs []string) (bool, error) { blocks, err := parseCIDRs(cidrs) if err != nil { return false, err } if len(blocks) == 0 { return true, nil } if clientIP == nil { return false, nil } for _, block := range blocks { if block.Contains(clientIP) { return true, nil } } return false, nil }