Pod 中运行 gvisor tcp 三次握手失败问题分析

Pod 中运行 gvisor tcp 三次握手失败问题分析,第1张

前言

最近在用 gvisor 这个优秀的 project 重构我的项目,在使用过程中出现了一个诡异的问题,发现 gvisor 之后,tcp 的握手失败了。


很奇怪,这里简单追踪记录一下

环境

Pod 基础镜像:ubuntu:latest
Gvisor 版本:gvisor.dev/gvisor v0.0.0-20220208035940-56a131734b85

复现步骤
  • 使用 tun2socks 绑定在 tun 上,
tun2socks -device tun://utun9  -proxy socks://127.0.0.1:1080
  • 设置路由,
ip link set dev utun9 mtu 65535
ip address add 223.254.254.100 dev utun9
ip link set dev utun9 up
ip route add 223.254.254.0/24 dev utun9
  • 然后随便 curl 一个非自身网卡地址的 IP,就会出现卡住的问题
root@kubevpn:/# curl 223.254.254.1:9080/health -iv
*   Trying 223.254.254.1:9080...
* TCP_NODELAY set
* Connected to 223.254.254.1 (223.254.254.1) port 9080 (#0)
> GET /health HTTP/1.1
> Host: 223.254.254.1:9080
> User-Agent: curl/7.68.0
> Accept: */*
>

问题分析 单部调试的威力

可以从 curl 中看到,已经成功链接了,但是总是卡住,这里翻看了一下 tun2socks 的源码,使用单部调试,最终确定在这里

...
return func(s *Stack) error {
		tcpForwarder := tcp.NewForwarder(s.Stack, defaultWndSize, maxConnAttempts, func(r *tcp.ForwarderRequest) {
			var wq waiter.Queue
			ep, err := r.CreateEndpoint(&wq) // 这里卡住!!!
			if err != nil {
				// RST: prevent potential half-open TCP connection leak.
				r.Complete(true)
				return
			}
			conn := gonet.NewTCPConn(&wq, ep)
			handleTCP(conn)
			...
}

大体含义:

  • 与请求发送方建立 TCP 链接(此例中是 curl )
  • 建立完成后,拿到 conn
  • 网络协议栈处理这个 conn,本地请求或者是走代理,自己的逻辑

一步步追源码,发现 CreateEndpoint 是 tcp 三次握手的逻辑

// CreateEndpoint creates a TCP endpoint for the connection request, performing
// the 3-way handshake in the process.
func (r *ForwarderRequest) CreateEndpoint(queue *waiter.Queue) (tcpip.Endpoint, tcpip.Error) {
	r.mu.Lock()
	defer r.mu.Unlock()

	if r.segment == nil {
		return nil, &tcpip.ErrInvalidEndpointState{}
	}

	f := r.forwarder
	ep, err := f.listen.performHandshake(r.segment, header.TCPSynOptions{
		MSS:           r.synOptions.MSS,
		WS:            r.synOptions.WS,
		TS:            r.synOptions.TS,
		TSVal:         r.synOptions.TSVal,
		TSEcr:         r.synOptions.TSEcr,
		SACKPermitted: r.synOptions.SACKPermitted,
	}, queue, nil)
	if err != nil {
		return nil, err
	}

	// Start the protocol goroutine. Note that the endpoint is returned
	// from performHandshake locked.
	ep.startAcceptedLoop() // +checklocksforce

	return ep, nil
}

继续追源码,发现会卡在这的 for 循环中,也就是握手一直没有完成:

// complete completes the TCP 3-way handshake initiated by h.start().
// +checklocks:h.ep.mu
func (h *handshake) complete() tcpip.Error {
...
	for h.state != handshakeCompleted {
		// Unlock before blocking, and reacquire again afterwards (h.ep.mu is held
		// throughout handshake processing).
		h.ep.mu.Unlock()
		w := s.Fetch(true /* block */)
		h.ep.mu.Lock()
		switch w {
		case &resendWaker:
			if err := timer.reset(); err != nil {
				return err
			}
			// Resend the SYN/SYN-ACK only if the following conditions hold.
			//  - It's an active handshake (deferAccept does not apply)
			//  - It's a passive handshake and we have not yet got the final-ACK.
			//  - It's a passive handshake and we got an ACK but deferAccept is
			//    enabled and we are now past the deferAccept duration.
			// The last is required to provide a way for the peer to complete
			// the connection with another ACK or data (as ACKs are never
			// retransmitted on their own).
			if h.active || !h.acked || h.deferAccept != 0 && h.ep.stack.Clock().NowMonotonic().Sub(h.startTime) > h.deferAccept {
				h.ep.sendSynTCP(h.ep.route, tcpFields{
					id:     h.ep.TransportEndpointInfo.ID,
					ttl:    calculateTTL(h.ep.route, h.ep.ipv4TTL, h.ep.ipv6HopLimit),
					tos:    h.ep.sendTOS,
					flags:  h.flags,
					seq:    h.iss,
					ack:    h.ackNum,
					rcvWnd: h.rcvWnd,
				}, h.sendSYNOpts)
				// If we have ever retransmitted the SYN-ACK or
				// SYN segment, we should only measure RTT if
				// TS option is present.
				h.sampleRTTWithTSOnly = true
			}

		case &h.ep.notificationWaker:
			n := h.ep.fetchNotifications()
			if (n¬ifyClose)|(n¬ifyAbort) != 0 {
				return &tcpip.ErrAborted{}
			}
			if n¬ifyShutdown != 0 {
				return &tcpip.ErrConnectionReset{}
			}
			if n¬ifyDrain != 0 {
				for !h.ep.segmentQueue.empty() {
					s := h.ep.segmentQueue.dequeue()
					err := h.handleSegment(s)
					s.decRef()
					if err != nil {
						return err
					}
					if h.state == handshakeCompleted {
						return nil
					}
				}
				close(h.ep.drainDone)
				h.ep.mu.Unlock()
				<-h.ep.undrain
				h.ep.mu.Lock()
			}
			// Check for any ICMP errors notified to us.
			if n¬ifyError != 0 {
				if err := h.ep.lastErrorLocked(); err != nil {
					return err
				}
				// Flag the handshake failure as aborted if the lastError is
				// cleared because of a socket layer call.
				return &tcpip.ErrConnectionAborted{}
			}
		case &h.ep.newSegmentWaker:
			if err := h.processSegments(); err != nil {
				return err
			}
		}
	}

	return nil
}

而 h.state 的状态一直是 handshakeSynRcvd,翻看一下定义

回想一下 tcp 三次握手过程,这里也就是处于第二阶段,收到 server 端的回包,但是没有进行确定,也就是没有发送第三次确认包

这又是为什么尼?继续追源码,终于追到一个关键步骤,
/Users/naison/go/pkg/mod/gvisor.dev/[email protected]/pkg/tcpip/transport/tcp/connect.go:318

每一次都是从这里 return 出去了,从注释可以看出:
如果 timestamp option 是需要协商的,但是回复的段 segment 中没有包含这个 timestamp option ,那么此段 segment 需要被丢弃
但是为什么回复的段没有这个 timestamp option 尼?

新问题产生

虽然已经定位到问题了,即:
tcp 握手第二次回段中,没有这个 timestamp option,从而导致 tcp 握手一直无法完成,一直转圈圈,死循环

不会了当然找 Google,果然搜到一条线索


可以看见,这个 commit 删除了校验 timestamp option 这段儿逻辑,原因是 network stack 需要和 Linux 的兼容,那么也就是 Linux 自己的 network stack 可能就没有实现这个约定,

可以看见,这里是建议收到这样的包后丢弃的,也就是 gvisor 的做法是符合规范的,但实际上不符合规范也是可以的。


可能的方法
  • 关闭这个特性(最优方案)
  • 使 Linux 的网络协议栈也发送这个 timestamp option (最优方案)
  • 注释这段校验代码(一定可行,但 ugly )

然后一通 *** 作,找到个可以关闭这个特性的命令:

sysctl -w net.ipv4.tcp_timestamps=0

执行了命令后,tcp 握手成功了!

其他的尝试
  • Gvisor Linux 环境支持的不好?
    但在本地 Linux 机器上是好的,排除

  • 网络特性导致的?
    在本地 Linux 机器上是好的,但是 pod 中工作不正确,可能是 pod 的网络特性导致的,于是加了所有的网络特性,问题依旧没有解决,排除

    securityContext:
      capabilities:
        add:
        - NET_ADMIN
        - NET_BIND_SERVICE
        - NET_BROADCAST
        - NET_RAW
      privileged: true
  • 尝试修改 pod 时区
    定位到是 timestamp option 后,脑海中第一个出现的 idea 就是是不是时区不对导致的,果然时区设置的不对,但是修改了正确的时区后,问题依旧没有解决,排除
后记

其实这才是第一步,还是做实验验证想法,只是第一步就遇到一个大难题,真是头大大,重构路漫漫啊,路漫漫其修远兮,吾将上下而求索~

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/584424.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-12
下一篇 2022-04-12

发表评论

登录后才能评论

评论列表(0条)

保存