Skip to content

buildSACKBitmap 会污染 small packet 去重状态,导致连续小包第二包开始被丢 #2

@saba-futai

Description

@saba-futai

我在拿 NRUP 做一个简单代理转发测试时遇到一个稳定复现的问题:第一包能通,第二包
开始超时。后来定位到 buildSACKBitmap() 里调用了 smallPktSeen(),这个函数不
是只读检查,会直接把 seq 写进 seenSmall,导致 ACK 第一包时把后续 32 个 seq
都提前标记成“已收到”。

相关代码:

func (c *Conn) buildSACKBitmap(baseSeq uint32) uint32 {
    var bitmap uint32
    for i := uint32(1); i <= 32; i++ {
        if c.smallPktSeen(baseSeq + i) {
            bitmap |= 1 << (i - 1)
        }
    }
    return bitmap
}

 smallPktSeen() 里面会改状态if c.seenSmall[seq] {
    return true
}
c.seenSmall[seq] = true
return false

所以流程会变成:

  1. 收到 seq=N 的小包
  2. 回 ACK 时调用 buildSACKBitmap(N)
  3. buildSACKBitmap() 调用 smallPktSeen(N+1 ... N+32)
  4. N+1 ... N+32 被提前写进 seenSmall
  5. 下一包 seq=N+1 真正到达时,被判断成 duplicate
  6. 包被丢掉,应用层表现为第二包开始 timeout

我这边是在 localhost loopback 和真实远端机器上都复现了,所以不是防火墙/公网丢
包导致的。

实测现象

测试环境:

CN2GIA 电信美西

  • 测试方式:额外写了一个简单 NRUP 代理
    • ToU:本地 TCP -> NRUP/UDP -> 远端 TCP target
    • UoU:本地 UDP -> NRUP/UDP -> 远端 UDP target
  • RTT 测试:echo 20 次
  • 下载测试:本机相对远端回程下载

修复前:

场景 结果
TCP echo over NRUP,localhost loopback 第一包成功,第二包 timeout
UDP echo over NRUP,localhost loopback 第一包成功,后续不稳定/timeout
TCP echo over NRUP,本机 -> 远端 timeout
UDP echo over NRUP,本机 -> 远端 timeout
iperf3 -R 回程下载 无法完成

本地日志里能看到第一包确实过了,但后续读不到返回:

pipe a->b done bytes=12 err=
pipe b->a done bytes=6 err=read udp ... use of closed network connection

修复后:

场景 结果
TCP echo over NRUP,localhost loopback 20/20 成功,avg 10.75 ms
UDP echo over NRUP,localhost loopback 20/20 成功,avg 10.98 ms
ToU echo RTT,本机 -> 远端 avg 199.31 ms,p50 178.40 ms
UoU echo RTT,本机 -> 远端 avg 227.06 ms,p50 204.47 ms
ToU iperf3 -R 回程下载 24.23 Mbps
UoU 低频 UDP echo 80/80 包,0% loss

我本地验证过的修法

不要在 buildSACKBitmap() 里调用 smallPktSeen(),改成只读检查 seenSmall:

  func (c *Conn) buildSACKBitmap(baseSeq uint32) uint32 {
      var bitmap uint32

      c.writeMu.Lock()
      defer c.writeMu.Unlock()

      for i := uint32(1); i <= 32; i++ {
          if c.seenSmall != nil && c.seenSmall[baseSeq+i] {
              bitmap |= 1 << (i - 1)
          }
      }
      return bitmap
  }

另外我还发现 Dial() 里创建客户端 Conn 时没有带上 cfg:

  conn := &Conn{
      dtls: dtlsConn,
      ...
  }

这样客户端侧配置例如 SACKInterval、SmallPacketThreshold 实际上不会通过 c.cfg
生效。我本地也一起改成了:

  conn := &Conn{
      cfg:  cfg,
      dtls: dtlsConn,
      ...
  }

这两个改完后,连续小包代理就能正常跑了。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions