gvisor-lx/pkg/tcpip/transport/tcp/rack.go
Leadaxe 117243aa02 snapshot: sagernet/gvisor v0.0.0-20260727.0-sing-box-mod.1 + SPEC 048 guard
Обновление снапшота с v0.0.0-20250811.0 на пин, которого требует
sing-box после мержа 235 коммитов (upstream d620bbbf2 "Update gvisor to
20260727.0"). Прежний снапшот был взят 2026-08-04 ровно с той версии,
на которой тогда стоял апстрим; разрыв возник 2026-08-05 вместе с его
бампом.

За год апстрим-gvisor изменил ~14 000 строк в 292 файлах. Значимое для
нас — сетевой стек: tcp/connect.go (PMTU-discovery + исправление
начального RTT/RTO: раньше задержка ACK внутри стека завышала стартовый
таймаут на несколько RTT), tcp/snd.go, tcp/rcv.go, stack/conntrack.go,
stack/packet_buffer.go. Всего 30 файлов в TCP и 37 в stack.

Баг SPEC 048 апстрим НЕ исправил — проверено по коду новой версии:
handleConnecting по-прежнему проверяет состояние endpoint'а, но не ep.h,
а performHandshake так же зануляет h и отпускает мьютекс до Close().
Поэтому guard перенесён (12 строк) вместе со своим тестом (45 строк).

Red/green проверен на новой базе: без guard'а тест падает с той же
nil-паникой, что в полевом крашдампе; с ним зелёный.
2026-08-05 14:53:31 +03:00

477 lines
15 KiB
Go

// Copyright 2020 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcp
import (
"time"
"github.com/sagernet/gvisor/pkg/tcpip"
"github.com/sagernet/gvisor/pkg/tcpip/seqnum"
)
const (
// wcDelayedACKTimeout is the recommended maximum delayed ACK timer
// value as defined in the RFC. It stands for worst case delayed ACK
// timer (WCDelAckT). When FlightSize is 1, PTO is inflated by
// WCDelAckT time to compensate for a potential long delayed ACK timer
// at the receiver.
// See: https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-7.5.
wcDelayedACKTimeout = 200 * time.Millisecond
// tcpRACKRecoveryThreshold is the number of loss recoveries for which
// the reorder window is inflated and after that the reorder window is
// reset to its initial value of minRTT/4.
// See: https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-7.2.
tcpRACKRecoveryThreshold = 16
)
// RACK is a loss detection algorithm used in TCP to detect packet loss and
// reordering using transmission timestamp of the packets instead of packet or
// sequence counts. To use RACK, SACK should be enabled on the connection.
// rackControl stores the rack related fields.
// See: https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-6.1
//
// +stateify savable
type rackControl struct {
TCPRACKState
// exitedRecovery indicates if the connection is exiting loss recovery.
// This flag is set if the sender is leaving the recovery after
// receiving an ACK and is reset during updating of reorder window.
exitedRecovery bool
// minRTT is the estimated minimum RTT of the connection.
minRTT time.Duration
// tlpRxtOut indicates whether there is an unacknowledged
// TLP retransmission.
tlpRxtOut bool
// tlpHighRxt the value of sender.sndNxt at the time of sending
// a TLP retransmission.
tlpHighRxt seqnum.Value
// snd is a reference to the sender.
snd *sender
}
// init initializes RACK specific fields.
func (rc *rackControl) init(snd *sender, iss seqnum.Value) {
rc.FACK = iss
rc.ReoWndIncr = 1
rc.snd = snd
}
// update will update the RACK related fields when an ACK has been received.
// See: https://tools.ietf.org/html/draft-ietf-tcpm-rack-09#section-6.2
func (rc *rackControl) update(seg *segment, ackSeg *segment) {
// Compute the RTT sample against the time the ACK was received at ingress
// (ackSeg.rcvdTime), not the current clock. The two differ when the ACK was
// delayed inside the stack before being processed (e.g. it sat in the
// endpoint's segment queue while the application held the endpoint lock
// during a Write that synchronously flushed a window). Using the processing
// time would inflate the RTT by that internal delay, corrupting RACK.RTT,
// RACK.minRTT and the reorder window, and causing spurious loss detection.
// detectLoss already uses ackSeg.rcvdTime; this keeps update consistent.
rtt := ackSeg.rcvdTime.Sub(seg.xmitTime)
// If the ACK is for a retransmitted packet, do not update if it is a
// spurious inference which is determined by below checks:
// 1. When Timestamping option is available, if the TSVal is less than
// the transmit time of the most recent retransmitted packet.
// 2. When RTT calculated for the packet is less than the smoothed RTT
// for the connection.
// See: https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-7.2
// step 2
if seg.xmitCount > 1 {
if ackSeg.parsedOptions.TS && ackSeg.parsedOptions.TSEcr != 0 {
if ackSeg.parsedOptions.TSEcr < rc.snd.ep.tsVal(seg.xmitTime) {
return
}
}
if rtt < rc.minRTT {
return
}
}
rc.RTT = rtt
// The sender can either track a simple global minimum of all RTT
// measurements from the connection, or a windowed min-filtered value
// of recent RTT measurements. This implementation keeps track of the
// simple global minimum of all RTTs for the connection.
if rtt < rc.minRTT || rc.minRTT == 0 {
rc.minRTT = rtt
}
// Update rc.xmitTime and rc.endSequence to the transmit time and
// ending sequence number of the packet which has been acknowledged
// most recently.
endSeq := seg.sequenceNumber.Add(seqnum.Size(seg.payloadSize()))
if rc.XmitTime.Before(seg.xmitTime) || (seg.xmitTime == rc.XmitTime && rc.EndSequence.LessThan(endSeq)) {
rc.XmitTime = seg.xmitTime
rc.EndSequence = endSeq
}
}
// detectReorder detects if packet reordering has been observed.
// See: https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-7.2
// - Step 3: Detect data segment reordering.
// To detect reordering, the sender looks for original data segments being
// delivered out of order. To detect such cases, the sender tracks the
// highest sequence selectively or cumulatively acknowledged in the RACK.fack
// variable. The name "fack" stands for the most "Forward ACK" (this term is
// adopted from [FACK]). If a never retransmitted segment that's below
// RACK.fack is (selectively or cumulatively) acknowledged, it has been
// delivered out of order. The sender sets RACK.reord to TRUE if such segment
// is identified.
func (rc *rackControl) detectReorder(seg *segment) {
endSeq := seg.sequenceNumber.Add(seqnum.Size(seg.payloadSize()))
if rc.FACK.LessThan(endSeq) {
rc.FACK = endSeq
return
}
if endSeq.LessThan(rc.FACK) && seg.xmitCount == 1 {
rc.Reord = true
}
}
func (rc *rackControl) setDSACKSeen(dsackSeen bool) {
rc.DSACKSeen = dsackSeen
}
// shouldSchedulePTO dictates whether we should schedule a PTO or not.
// See https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-7.5.1.
func (s *sender) shouldSchedulePTO() bool {
// Schedule PTO only if RACK loss detection is enabled.
return s.ep.tcpRecovery&tcpip.TCPRACKLossDetection != 0 &&
// The connection supports SACK.
s.ep.SACKPermitted &&
// The connection is not in loss recovery.
(s.state != tcpip.RTORecovery && s.state != tcpip.SACKRecovery) &&
// The connection has no SACKed sequences in the SACK scoreboard.
s.ep.scoreboard.Sacked() == 0
}
// schedulePTO schedules the probe timeout as defined in
// https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-7.5.1.
//
// +checklocks:s.ep.mu
func (s *sender) schedulePTO() {
pto := time.Second
s.rtt.Lock()
if s.rtt.TCPRTTState.SRTTInited && s.rtt.TCPRTTState.SRTT > 0 {
pto = s.rtt.TCPRTTState.SRTT * 2
if s.Outstanding == 1 {
pto += wcDelayedACKTimeout
}
}
s.rtt.Unlock()
now := s.ep.stack.Clock().NowMonotonic()
if s.resendTimer.enabled() {
if now.Add(pto).After(s.resendTimer.target) {
pto = s.resendTimer.target.Sub(now)
}
s.resendTimer.disable()
}
s.probeTimer.enable(pto)
}
// probeTimerExpired is the same as TLP_send_probe() as defined in
// https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-7.5.2.
//
// +checklocks:s.ep.mu
func (s *sender) probeTimerExpired() tcpip.Error {
if s.probeTimer.isUninitialized() || !s.probeTimer.checkExpiration() {
return nil
}
var dataSent bool
if s.writeNext != nil && s.writeNext.xmitCount == 0 && s.Outstanding < s.SndCwnd {
dataSent = s.maybeSendSegment(s.writeNext, int(s.ep.scoreboard.SMSS()), s.SndUna.Add(s.SndWnd))
if dataSent {
s.Outstanding += s.pCount(s.writeNext, s.MaxPayloadSize)
s.updateWriteNext(s.writeNext.Next())
}
}
if !dataSent && !s.rc.tlpRxtOut {
var highestSeqXmit *segment
for highestSeqXmit = s.writeList.Front(); highestSeqXmit != nil; highestSeqXmit = highestSeqXmit.Next() {
if highestSeqXmit.xmitCount == 0 {
// Nothing in writeList is transmitted, no need to send a probe.
highestSeqXmit = nil
break
}
if highestSeqXmit.Next() == nil || highestSeqXmit.Next().xmitCount == 0 {
// Either everything in writeList has been transmitted or the next
// sequence has not been transmitted. Either way this is the highest
// sequence segment that was transmitted.
break
}
}
if highestSeqXmit != nil {
dataSent = s.maybeSendSegment(highestSeqXmit, int(s.ep.scoreboard.SMSS()), s.SndUna.Add(s.SndWnd))
if dataSent {
s.rc.tlpRxtOut = true
s.rc.tlpHighRxt = s.SndNxt
}
}
}
// Whether or not the probe was sent, the sender must arm the resend timer,
// not the probe timer. This ensures that the sender does not send repeated,
// back-to-back tail loss probes.
s.postXmit(dataSent, false /* shouldScheduleProbe */)
return nil
}
// detectTLPRecovery detects if recovery was accomplished by the loss probes
// and updates TLP state accordingly.
// See https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-7.6.3.
//
// +checklocks:s.ep.mu
func (s *sender) detectTLPRecovery(ack seqnum.Value, rcvdSeg *segment) {
if !(s.ep.SACKPermitted && s.rc.tlpRxtOut) {
return
}
// Step 1.
if s.isDupAck(rcvdSeg) && ack == s.rc.tlpHighRxt {
var sbAboveTLPHighRxt bool
for _, sb := range rcvdSeg.parsedOptions.SACKBlocks {
if s.rc.tlpHighRxt.LessThan(sb.End) {
sbAboveTLPHighRxt = true
break
}
}
if !sbAboveTLPHighRxt {
// TLP episode is complete.
s.rc.tlpRxtOut = false
}
}
if s.rc.tlpRxtOut && s.rc.tlpHighRxt.LessThanEq(ack) {
// TLP episode is complete.
s.rc.tlpRxtOut = false
if !checkDSACK(rcvdSeg) {
// Step 2. Either the original packet or the retransmission (in the
// form of a probe) was lost. Invoke a congestion control response
// equivalent to fast recovery.
s.cc.HandleLossDetected()
s.enterRecovery()
s.leaveRecovery()
}
}
}
// updateRACKReorderWindow updates the reorder window.
// See: https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-7.2
// - Step 4: Update RACK reordering window
// To handle the prevalent small degree of reordering, RACK.reo_wnd serves as
// an allowance for settling time before marking a packet lost. RACK starts
// initially with a conservative window of min_RTT/4. If no reordering has
// been observed RACK uses reo_wnd of zero during loss recovery, in order to
// retransmit quickly, or when the number of DUPACKs exceeds the classic
// DUPACKthreshold.
//
// +checklocks:rc.snd.ep.mu
func (rc *rackControl) updateRACKReorderWindow() {
dsackSeen := rc.DSACKSeen
snd := rc.snd
// React to DSACK once per round trip.
// If SND.UNA < RACK.rtt_seq:
// RACK.dsack = false
if snd.SndUna.LessThan(rc.RTTSeq) {
dsackSeen = false
}
// If RACK.dsack:
// RACK.reo_wnd_incr += 1
// RACK.dsack = false
// RACK.rtt_seq = SND.NXT
// RACK.reo_wnd_persist = 16
if dsackSeen {
rc.ReoWndIncr++
dsackSeen = false
rc.RTTSeq = snd.SndNxt
rc.ReoWndPersist = tcpRACKRecoveryThreshold
} else if rc.exitedRecovery {
// Else if exiting loss recovery:
// RACK.reo_wnd_persist -= 1
// If RACK.reo_wnd_persist <= 0:
// RACK.reo_wnd_incr = 1
rc.ReoWndPersist--
if rc.ReoWndPersist <= 0 {
rc.ReoWndIncr = 1
}
rc.exitedRecovery = false
}
// Reorder window is zero during loss recovery, or when the number of
// DUPACKs exceeds the classic DUPACKthreshold.
// If RACK.reord is FALSE:
// If in loss recovery: (If in fast or timeout recovery)
// RACK.reo_wnd = 0
// Return
// Else if RACK.pkts_sacked >= RACK.dupthresh:
// RACK.reo_wnd = 0
// return
if !rc.Reord {
if snd.state == tcpip.RTORecovery || snd.state == tcpip.SACKRecovery {
rc.ReoWnd = 0
return
}
if snd.SackedOut >= nDupAckThreshold {
rc.ReoWnd = 0
return
}
}
// Calculate reorder window.
// RACK.reo_wnd = RACK.min_RTT / 4 * RACK.reo_wnd_incr
// RACK.reo_wnd = min(RACK.reo_wnd, SRTT)
snd.rtt.Lock()
srtt := snd.rtt.TCPRTTState.SRTT
snd.rtt.Unlock()
rc.ReoWnd = time.Duration((int64(rc.minRTT) / 4) * int64(rc.ReoWndIncr))
if srtt < rc.ReoWnd {
rc.ReoWnd = srtt
}
}
func (rc *rackControl) exitRecovery() {
rc.exitedRecovery = true
}
// detectLoss marks the segment as lost if the reordering window has elapsed
// and the ACK is not received. It will also arm the reorder timer.
// See: https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-7.2 Step 5.
//
// +checklocks:rc.snd.ep.mu
func (rc *rackControl) detectLoss(rcvTime tcpip.MonotonicTime) int {
var timeout time.Duration
numLost := 0
for seg := rc.snd.writeList.Front(); seg != nil && seg.xmitCount != 0; seg = seg.Next() {
if rc.snd.ep.scoreboard.IsSACKED(seg.sackBlock()) {
continue
}
if seg.lost && seg.xmitCount == 1 {
numLost++
continue
}
endSeq := seg.sequenceNumber.Add(seqnum.Size(seg.payloadSize()))
if seg.xmitTime.Before(rc.XmitTime) || (seg.xmitTime == rc.XmitTime && rc.EndSequence.LessThan(endSeq)) {
timeRemaining := seg.xmitTime.Sub(rcvTime) + rc.RTT + rc.ReoWnd
if timeRemaining <= 0 {
seg.lost = true
numLost++
} else if timeRemaining > timeout {
timeout = timeRemaining
}
}
}
if timeout != 0 && !rc.snd.reorderTimer.enabled() {
rc.snd.reorderTimer.enable(timeout)
}
return numLost
}
// reorderTimerExpired will retransmit the segments which have not been acked
// before the reorder timer expired.
//
// +checklocks:rc.snd.ep.mu
func (rc *rackControl) reorderTimerExpired() tcpip.Error {
if rc.snd.reorderTimer.isUninitialized() || !rc.snd.reorderTimer.checkExpiration() {
return nil
}
// Evaluate loss as of the time the reorder timer was scheduled to fire (its
// target), not the current clock. The timer is armed for the instant a
// segment's reorder window elapses, but the timer callback itself can run
// arbitrarily late under load (the processor goroutine is busy, or the
// endpoint lock is held while a Write flushes a window). Using the
// (possibly much later) processing time would make timeRemaining strongly
// negative and mark not-yet-lost segments as lost, triggering spurious
// retransmits and recovery on a path with little or no real loss
// (gvisor#9707/#9778). detectLoss arms the timer from rcvdTime-derived
// values, so evaluating it at the timer target keeps the two consistent.
numLost := rc.detectLoss(rc.snd.reorderTimer.target)
if numLost == 0 {
return nil
}
fastRetransmit := false
if !rc.snd.FastRecovery.Active {
rc.snd.cc.HandleLossDetected()
rc.snd.enterRecovery()
fastRetransmit = true
}
rc.DoRecovery(nil, fastRetransmit)
return nil
}
// DoRecovery implements lossRecovery.DoRecovery.
//
// +checklocks:rc.snd.ep.mu
func (rc *rackControl) DoRecovery(_ *segment, fastRetransmit bool) {
snd := rc.snd
if fastRetransmit {
snd.resendSegment()
}
var dataSent bool
// Iterate the writeList and retransmit the segments which are marked
// as lost by RACK.
for seg := snd.writeList.Front(); seg != nil && seg.xmitCount > 0; seg = seg.Next() {
if seg == snd.writeNext {
break
}
if !seg.lost {
continue
}
// Reset seg.lost as it is already SACKed.
if snd.ep.scoreboard.IsSACKED(seg.sackBlock()) {
seg.lost = false
continue
}
// Check the congestion window after entering recovery.
if snd.Outstanding >= snd.SndCwnd {
break
}
if sent := snd.maybeSendSegment(seg, int(snd.ep.scoreboard.SMSS()), snd.SndUna.Add(snd.SndWnd)); !sent {
break
}
dataSent = true
snd.Outstanding += snd.pCount(seg, snd.MaxPayloadSize)
}
snd.postXmit(dataSent, true /* shouldScheduleProbe */)
}