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
}