remoteip/parser.go (129 lines of code) (raw):

package remoteip import ( "errors" "net" "net/http" "strings" "unicode" ) var ( ErrIPSpoofAttack = errors.New("ip spoofing attack detected") ) // Default trusted proxies include local networks commonly used by proxies. var defaultTrustedProxies = []string{ "127.0.0.0/8", // localhost IPv4 range, per RFC-3330 "10.0.0.0/8", // private IPv4 range 10.x.x.x "172.16.0.0/12", // private IPv4 range 172.16.0.0 .. 172.31.255.255 "192.168.0.0/16", // private IPv4 range 192.168.x.x "::1/128", // localhost IPv6 "fc00::/7", // private IPv6 range fc00::/7 } // RemoteIPParser extracts and validates the client's true IP address from HTTP request headers. // This is a port of the Rails code: // https://github.com/rails/rails/blob/v8.0.2/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L109-L167 type RemoteIPParser struct { // TrustedProxies is a list of IP addresses or CIDR ranges that are considered trusted proxies TrustedProxies []string // CheckIPSpoofing enables the IP spoofing check between X-Forwarded-For and Client-IP headers CheckIPSpoofing bool } // NewRemoteIPParser creates a new RemoteIPParser with the list of trusted proxies. // If the list is empty, it will use the default trusted proxies. func NewRemoteIPParser(trustedProxies []string) *RemoteIPParser { if len(trustedProxies) == 0 { trustedProxies = defaultTrustedProxies } return &RemoteIPParser{ TrustedProxies: trustedProxies, CheckIPSpoofing: true, } } // GetIP extracts the client's true IP address from the request // following a similar approach to Rails' calculate_ip method. func (p *RemoteIPParser) GetIP(r *http.Request) (string, error) { // Get the direct remote address (equivalent to REMOTE_ADDR in Rails) remoteAddr := p.parseIP(r.RemoteAddr) // Get IPs from X-Forwarded-For header(s) // In Go, r.Header.Get() returns joined values if there are multiple headers with the same name forwardedIPs := p.parseIPsFromHeader(r.Header.Get("X-Forwarded-For")) // Get IPs from Client-IP header(s) clientIPs := p.parseIPsFromHeader(r.Header.Get("Client-Ip")) // Check for potential IP spoofing if both headers are present if p.CheckIPSpoofing && len(clientIPs) > 0 && len(forwardedIPs) > 0 { // If the last client IP is not in the forwarded IPs list, it might be spoofing lastClientIP := clientIPs[len(clientIPs)-1] isInForwardedIPs := false for _, ip := range forwardedIPs { if ip == lastClientIP { isInForwardedIPs = true break } } if !isInForwardedIPs { return "", ErrIPSpoofAttack } } // Following Rails logic: // 1. Combine forwarded and client IPs (in original order) // 2. Check for non-proxies in the combined list // 3. If all IPs are proxies, use the furthest away IP (last in the list) // Combine IPs from headers (in original order) ips := make([]string, 0, len(forwardedIPs)+len(clientIPs)) ips = append(ips, forwardedIPs...) ips = append(ips, clientIPs...) // Filter out trusted proxies filteredIPs := p.filterProxies(append(ips, remoteAddr)) if len(filteredIPs) > 0 { return filteredIPs[0], nil } // If all IPs are trusted proxies, return the "furthest away" IP // (which is the last IP in the list after filtering) if len(ips) > 0 { return ips[len(ips)-1], nil } // Final fallback is the remote address return remoteAddr, nil } // parseIPsFromHeader extracts a list of IPs from a header value. func (p *RemoteIPParser) parseIPsFromHeader(header string) []string { if header == "" { return []string{} } // Trim the header and split by commas or whitespace parts := strings.FieldsFunc(strings.TrimSpace(header), func(r rune) bool { return r == ',' || unicode.IsSpace(r) }) ips := make([]string, 0, len(parts)) for _, part := range parts { // Try to parse the IP (this also handles cleaning) if ip := p.parseIP(part); ip != "" { ips = append(ips, ip) } } return ips } // parseIP parses and validates a single IP address string. func (p *RemoteIPParser) parseIP(addr string) string { // Trim whitespace addr = strings.TrimSpace(addr) if addr == "" { return "" } // Try to use SplitHostPort to handle IP:port format host := addr h, _, err := net.SplitHostPort(addr) if err == nil { host = h } else if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") { // Could be IPv6 with brackets but no port: "[IPv6]" host = addr[1 : len(addr)-1] } // Parse the IP address ip := net.ParseIP(host) if ip == nil { return "" } // Check if it has a netmask by looking for slash if strings.Contains(addr, "/") { return "" // Reject CIDR notation } return ip.String() } // filterProxies removes trusted proxy IPs from the list. func (p *RemoteIPParser) filterProxies(ips []string) []string { if len(ips) == 0 { return ips } filtered := make([]string, 0, len(ips)) for _, ip := range ips { if !p.isTrustedProxy(ip) { filtered = append(filtered, ip) } } return filtered } // isTrustedProxy checks if an IP is in the trusted proxies list. func (p *RemoteIPParser) isTrustedProxy(ip string) bool { if ip == "" { return false } parsedIP := net.ParseIP(ip) if parsedIP == nil { return false } for _, trustedProxy := range p.TrustedProxies { // Handle both CIDR ranges and specific IPs if strings.Contains(trustedProxy, "/") { _, ipNet, err := net.ParseCIDR(trustedProxy) if err == nil && ipNet.Contains(parsedIP) { return true } } else if trustedProxy == ip { return true } } return false }