容器内Golang服务获取IPVS DNAT原始目标IP失败求助
解决Docker容器内Golang服务获取IPVS DNAT原始目标IP的问题
问题根源
- socket选项类型不匹配:你用
syscall.GetsockoptIPv6Mreq处理IPv4的原始目标地址,属于类型误用,应该对应使用IPv4专属的结构体。 - 容器权限不足:读取
SO_ORIGINAL_DST需要NET_ADMIN权限,--privileged=true权限冗余且不是必需项,添加特定网络权限即可。 - 网络命名空间限制:Docker桥接模式下默认权限会阻止容器读取原始目标地址,需调整权限并确保内核参数正确。
解决方案
1. 修正getOriginalDst函数实现
替换原有代码,使用正确的IPv4/IPv6兼容处理逻辑:
package main import ( "context" "encoding/binary" "fmt" "net" "net/http" "syscall" "unsafe" ) const SO_ORIGINAL_DST = 80 type contextKey struct { key string } var ConnContextKey = &contextKey{"http-conn"} func SaveConnInContext(ctx context.Context, c net.Conn) context.Context { return context.WithValue(ctx, ConnContextKey, c) } func GetConn(r *http.Request) net.Conn { return r.Context().Value(ConnContextKey).(net.Conn) } func getOriginalDst(conn net.Conn) (string, int, error) { tc, ok := conn.(*net.TCPConn) if !ok { return "", 0, fmt.Errorf("redirect proxy only supports tcp") } f, err := tc.File() if err != nil { return "", 0, fmt.Errorf("get conn file error: %w", err) } defer f.Close() fd := int(f.Fd()) // 先尝试IPv4 var addr4 syscall.RawSockaddrInet4 var addrLen4 uint32 = syscall.SizeofRawSockaddrInet4 err = syscall.Getsockopt(fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, unsafe.Pointer(&addr4), &addrLen4) if err == nil { ip := net.IP(addr4.Addr[:]).String() port := int(binary.BigEndian.Uint16((*[2]byte)(unsafe.Pointer(&addr4.Port))[:])) return ip, port, nil } // 再尝试IPv6 var addr6 syscall.RawSockaddrInet6 var addrLen6 uint32 = syscall.SizeofRawSockaddrInet6 err = syscall.Getsockopt(fd, syscall.IPPROTO_IPV6, SO_ORIGINAL_DST, unsafe.Pointer(&addr6), &addrLen6) if err != nil { return "", 0, fmt.Errorf("get original dst failed: %w", err) } ip := net.IP(addr6.Addr[:]).String() port := int(binary.BigEndian.Uint16((*[2]byte)(unsafe.Pointer(&addr6.Port))[:])) return ip, port, nil } func hello(w http.ResponseWriter, req *http.Request) { conn := GetConn(req) ip, port, err := getOriginalDst(conn) if err != nil { fmt.Fprintf(w, fmt.Sprintf("get original dst failed: %s", err)) return } msg := fmt.Sprintf("source addr: %s, server addr: %s, original dst: %s:%d \n", req.RemoteAddr, req.Context().Value(http.LocalAddrContextKey), ip, port) fmt.Fprintf(w, msg) } func version(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "v4") } func main() { http.HandleFunc("/", version) http.HandleFunc("/test", hello) addr := "0.0.0.0:9090" srv := http.Server{ ConnContext: SaveConnInContext, } ln, err := net.Listen("tcp4", addr) if err != nil { panic(err) } srv.Serve(ln) }
2. 正确启动容器
添加必要的网络权限和内核参数:
docker run -d \ --cap-add=NET_ADMIN \ --sysctl net.ipv4.ip_forward=1 \ -p 9090:9090 \ your-golang-service-image
--cap-add=NET_ADMIN:授予容器读取SO_ORIGINAL_DST的必要网络权限--sysctl net.ipv4.ip_forward=1:确保IP转发功能正常,保证DNAT流量正确传递
3. 验证IPVS配置
确保IPVS规则使用masquerade模式(保留原始目标地址):
# 创建IPVS虚拟服务 ipvsadm -A -t <VIP>:<PORT> -s rr # 添加容器后端,使用-m(masquerade)模式 ipvsadm -a -t <VIP>:<PORT> -r <容器IP>:9090 -m
测试验证
启动容器后执行:
curl <VIP>:<PORT>/test
预期返回:
source addr: <客户端IP>:<端口>, server addr: <容器IP>:9090, original dst: <VIP>:<PORT>
内容的提问来源于stack exchange,提问作者firsy
相关产品推荐
相关产品推荐

