// Package netutil 提供网络相关的通用工具函数。 // // 该文件包含客户端 IP 提取相关的工具函数, // 从 HTTP 请求中提取真实的客户端 IP 地址。 // // 作者:xfy package netutil import ( "net" "strings" "sync" "github.com/valyala/fasthttp" ) // ExtractClientIP 从请求上下文中提取客户端 IP 地址(返回字符串)。 // // 该函数按以下顺序提取 IP: // 1. X-Forwarded-For 请求头的第一个 IP(最左侧) // 2. X-Real-IP 请求头 // 3. RemoteAddr // // 注意:此函数不进行可信代理验证,适用于非安全场景(如日志记录)。 // 对于安全场景(如访问控制),应使用特定模块的安全实现。 // // 参数: // - ctx: FastHTTP 请求上下文 // // 返回值: // - string: 客户端 IP 地址字符串 func ExtractClientIP(ctx *fasthttp.RequestCtx) string { // 首先检查 X-Forwarded-For 请求头 if xff := ctx.Request.Header.Peek("X-Forwarded-For"); len(xff) > 0 { ips := strings.Split(string(xff), ",") if len(ips) > 0 { return strings.TrimSpace(ips[0]) } } // 检查 X-Real-IP 请求头 if xri := ctx.Request.Header.Peek("X-Real-IP"); len(xri) > 0 { return string(xri) } // 回退到 RemoteAddr if addr := ctx.RemoteAddr(); addr != nil { if tcpAddr, ok := addr.(*net.TCPAddr); ok { return tcpAddr.IP.String() } return addr.String() } return "" } // ExtractClientIPNet 从请求上下文中提取客户端 IP 地址(返回 net.IP)。 // // 该函数与 ExtractClientIP 功能相同,但返回 net.IP 类型, // 便于后续进行 IP 网络操作(如 CIDR 匹配)。 // // 参数: // - ctx: FastHTTP 请求上下文 // // 返回值: // - net.IP: 客户端 IP 地址,无法解析时返回 nil func ExtractClientIPNet(ctx *fasthttp.RequestCtx) net.IP { // 首先检查 X-Forwarded-For 请求头 if xff := ctx.Request.Header.Peek("X-Forwarded-For"); len(xff) > 0 { ips := strings.Split(string(xff), ",") if len(ips) > 0 { ipStr := strings.TrimSpace(ips[0]) if ip := net.ParseIP(ipStr); ip != nil { return ip } } } // 检查 X-Real-IP 请求头 if xri := ctx.Request.Header.Peek("X-Real-IP"); len(xri) > 0 { if ip := net.ParseIP(string(xri)); ip != nil { return ip } } // 回退到 RemoteAddr if addr := ctx.RemoteAddr(); addr != nil { if tcpAddr, ok := addr.(*net.TCPAddr); ok { return tcpAddr.IP } } return nil } // GetRemoteAddrIP 从 RemoteAddr 提取 IP 地址。 // // 这是一个辅助函数,直接从连接的远程地址获取 IP, // 不检查任何代理头。 // // 参数: // - ctx: FastHTTP 请求上下文 // // 返回值: // - net.IP: 客户端 IP 地址,无法获取时返回 nil func GetRemoteAddrIP(ctx *fasthttp.RequestCtx) net.IP { if addr := ctx.RemoteAddr(); addr != nil { if tcpAddr, ok := addr.(*net.TCPAddr); ok { return tcpAddr.IP } } return nil } // remoteAddrCache 缓存 RemoteAddr 字符串化结果,避免重复的 net.TCPAddr.String() 分配。 type remoteAddrCache struct { mu sync.RWMutex entries map[string]string maxSize int } var globalRemoteAddrCache = &remoteAddrCache{ entries: make(map[string]string, 1024), maxSize: 1024, } // FormatRemoteAddr 使用缓存格式化 RemoteAddr,避免重复的地址字符串分配。 // 优先使用 ctx.RemoteIP() 获取 IP,对 IPv4 直接零分配格式化,IPv6 回退到 addr.String()。 func FormatRemoteAddr(ctx *fasthttp.RequestCtx) string { ip := ctx.RemoteIP() if ip == nil { addr := ctx.RemoteAddr() if addr == nil { return "-" } return addr.String() } // 优先尝试 IPv4 快速路径(零分配) if ipv4 := ip.To4(); ipv4 != nil { return formatIPv4(ipv4) } // IPv6:尝试缓存 ipStr := ip.String() globalRemoteAddrCache.mu.RLock() if cached, ok := globalRemoteAddrCache.entries[ipStr]; ok { globalRemoteAddrCache.mu.RUnlock() return cached } globalRemoteAddrCache.mu.RUnlock() // 未命中缓存,回退到 addr.String() addr := ctx.RemoteAddr() if addr == nil { return "-" } result := addr.String() globalRemoteAddrCache.mu.Lock() if len(globalRemoteAddrCache.entries) < globalRemoteAddrCache.maxSize { globalRemoteAddrCache.entries[ipStr] = result } globalRemoteAddrCache.mu.Unlock() return result } // formatIPv4 将 4 字节 IPv4 地址格式化为字符串(零分配)。 func formatIPv4(ip net.IP) string { var buf [15]byte n := 0 for i := 0; i < 4; i++ { if i > 0 { buf[n] = '.' n++ } n += writeUint8(buf[n:], ip[i]) } return string(buf[:n]) } // writeUint8 将 uint8 写入 buf,返回写入的字节数。 func writeUint8(buf []byte, v byte) int { if v >= 100 { buf[0] = byte('0' + v/100) buf[1] = byte('0' + (v/10)%10) buf[2] = byte('0' + v%10) return 3 } if v >= 10 { buf[0] = byte('0' + v/10) buf[1] = byte('0' + v%10) return 2 } buf[0] = byte('0' + v) return 1 }