前言
最近在用 gvisor 这个优秀的 project 重构我的项目,在使用过程中出现了一个诡异的问题,发现 gvisor 之后,tcp 的握手失败了。很奇怪,这里简单追踪记录一下
环境
Pod 基础镜像:ubuntu:latest Gvisor 版本:gvisor.dev/gvisor v0.0.0-20220208035940-56a131734b85
复现步骤
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:/
* Trying 223.254.254.1:9080...
* TCP_NODELAY set
* Connected to 223.254.254.1 (223.254.254.1) port 9080 (
> 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/gvisor@v0.0.0-20220208035940-56a131734b85/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 握手成功了!
其他的尝试
securityContext:
capabilities:
add:
- NET_ADMIN
- NET_BIND_SERVICE
- NET_BROADCAST
- NET_RAW
privileged: true
- 尝试修改 pod 时区
定位到是 timestamp option 后,脑海中第一个出现的 idea 就是是不是时区不对导致的,果然时区设置的不对,但是修改了正确的时区后,问题依旧没有解决,排除
后记
其实这才是第一步,还是做实验验证想法,只是第一步就遇到一个大难题,真是头大大,重构路漫漫啊,路漫漫其修远兮,吾将上下而求索~
|