You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

容器内Golang服务获取IPVS DNAT原始目标IP失败求助

解决Docker容器内Golang服务获取IPVS DNAT原始目标IP的问题

问题根源

  1. socket选项类型不匹配:你用syscall.GetsockoptIPv6Mreq处理IPv4的原始目标地址,属于类型误用,应该对应使用IPv4专属的结构体。
  2. 容器权限不足:读取SO_ORIGINAL_DST需要NET_ADMIN权限,--privileged=true权限冗余且不是必需项,添加特定网络权限即可。
  3. 网络命名空间限制: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 00:20:58