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-паникой, что в полевом крашдампе; с ним зелёный.
This commit is contained in:
Leadaxe 2026-08-05 14:53:31 +03:00
parent ffebe42860
commit 117243aa02
293 changed files with 16413 additions and 2842 deletions

View file

@ -20,17 +20,18 @@
package stack
import (
"context"
"encoding/binary"
"fmt"
"io"
"math/rand"
"sync/atomic"
"time"
"github.com/sagernet/gvisor/pkg/atomicbitops"
"github.com/sagernet/gvisor/pkg/buffer"
"github.com/sagernet/gvisor/pkg/log"
cryptorand "github.com/sagernet/gvisor/pkg/rand"
"github.com/sagernet/gvisor/pkg/sync"
"github.com/sagernet/gvisor/pkg/tcpip"
"github.com/sagernet/gvisor/pkg/tcpip/header"
"github.com/sagernet/gvisor/pkg/tcpip/ports"
@ -96,7 +97,7 @@ type Stack struct {
// +checklocks:mu
nics map[tcpip.NICID]*nic `state:"nosave"`
// +checklocks:mu
loopbackNIC *nic
loopbackNIC *nic `state:"nosave"`
// +checklocks:mu
defaultForwardingEnabled map[tcpip.NetworkProtocolNumber]struct{}
@ -121,7 +122,15 @@ type Stack struct {
tables *IPTables `state:"nosave"`
// nftables is the nftables interface for packet filtering and manipulation rules.
nftables NFTablesInterface `state:"nosave"`
// Using atomic.Pointer for RCU lock-free reads.
nftables atomic.Pointer[NFTablesInterface] `state:"nosave"`
// nftablesUpdateMu serializes concurrent netlink batch modifications to nftables.
nftablesUpdateMu sync.Mutex `state:"nosave"`
// nftablesConfigured indicates whether NFTables is configured with at
// least one rule on a chain at a network hook.
nftablesConfigured atomicbitops.Bool
// restoredEndpoints is a list of endpoints that need to be restored if the
// stack is being restored.
@ -179,8 +188,23 @@ type Stack struct {
// initialized at stack startup.
tsOffsetSecret uint32
// saveRestoreEnabled indicates whether the stack is saved and restored.
saveRestoreEnabled bool
// removeConf indicates whether to remove NICs and routes and terminate
// active connections before saving. This flag will be set to true only
// when resume is false.
removeConf bool `state:"nosave"`
// allowLiveTCPMigration allows TCP connection state to be migrated.
// If false, any connected TCP endpoints will be terminated
// during save/restore.
allowLiveTCPMigration bool `state:"nosave"`
// externalNetworkingDisabled indicates whether external networking is
// disabled. This means all non-loopback NICs are disabled.
externalNetworkingDisabled bool
// allowConnectedOnSave indicates whether connections should be
// allowed to remain connected during save.
allowConnectedOnSave bool
}
// NetworkProtocolFactory instantiates a network protocol.
@ -231,6 +255,11 @@ type Options struct {
// operations.
AllowPacketEndpointWrite bool
// AllowLiveTCPMigration allows TCP connection state to be migrated.
// If false, any connected TCP endpoints will be terminated
// during save/restore.
AllowLiveTCPMigration bool
// RandSource is an optional source to use to generate random
// numbers. If omitted it defaults to a Source seeded by the data
// returned by the stack secure RNG.
@ -398,7 +427,6 @@ func New(opts Options) *Stack {
stats: opts.Stats.FillIn(),
handleLocal: opts.HandleLocal,
tables: opts.IPTables,
nftables: opts.NFTables,
icmpRateLimiter: NewICMPRateLimiter(clock),
seed: secureRNG.Uint32(),
nudConfigs: opts.NUDConfigs,
@ -415,9 +443,11 @@ func New(opts Options) *Stack {
Default: DefaultBufferSize,
Max: DefaultMaxBufferSize,
},
tcpInvalidRateLimit: defaultTCPInvalidRateLimit,
tsOffsetSecret: secureRNG.Uint32(),
tcpInvalidRateLimit: defaultTCPInvalidRateLimit,
tsOffsetSecret: secureRNG.Uint32(),
allowLiveTCPMigration: opts.AllowLiveTCPMigration,
}
s.SetNFTables(opts.NFTables)
// Add specified network protocols.
for _, netProtoFactory := range opts.NetworkProtocols {
@ -895,8 +925,8 @@ type NICOptions struct {
// GetNICByID return a network device associated with the specified ID.
func (s *Stack) GetNICByID(id tcpip.NICID) (*nic, tcpip.Error) {
s.mu.Lock()
defer s.mu.Unlock()
s.mu.RLock()
defer s.mu.RUnlock()
n, ok := s.nics[id]
if !ok {
@ -1017,7 +1047,7 @@ func (s *Stack) CheckNIC(id tcpip.NICID) bool {
// RemoveNIC removes NIC and all related routes from the network stack.
func (s *Stack) RemoveNIC(id tcpip.NICID) tcpip.Error {
s.mu.Lock()
deferAct, err := s.removeNICLocked(id)
deferAct, err := s.removeNICLocked(id, true /* closeLinkEndpoint */)
s.mu.Unlock()
if deferAct != nil {
deferAct()
@ -1028,7 +1058,7 @@ func (s *Stack) RemoveNIC(id tcpip.NICID) tcpip.Error {
// removeNICLocked removes NIC and all related routes from the network stack.
//
// +checklocks:s.mu
func (s *Stack) removeNICLocked(id tcpip.NICID) (func(), tcpip.Error) {
func (s *Stack) removeNICLocked(id tcpip.NICID, closeLinkEndpoint bool) (func(), tcpip.Error) {
nic, ok := s.nics[id]
if !ok {
return nil, &tcpip.ErrUnknownNICID{}
@ -1056,7 +1086,19 @@ func (s *Stack) removeNICLocked(id tcpip.NICID) (func(), tcpip.Error) {
if s.loopbackNIC == nic {
s.loopbackNIC = nil
}
return nic.remove(true /* closeLinkEndpoint */)
return nic.remove(closeLinkEndpoint)
}
// GetNICCoordinatorID returns the ID of the coordinator device of a NIC.
func (s *Stack) GetNICCoordinatorID(id tcpip.NICID) (tcpip.NICID, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
if nic, ok := s.nics[id]; ok {
if nic.Primary != nil {
return nic.Primary.id, true
}
}
return 0, false
}
// SetNICCoordinator sets a coordinator device.
@ -1159,6 +1201,9 @@ type NICInfo struct {
// MulticastForwarding holds the forwarding status for each network endpoint
// that supports multicast forwarding.
MulticastForwarding map[tcpip.NetworkProtocolNumber]bool
// Primary is the index of the main controlling interface in a bonded setup.
Primary tcpip.NICID
}
// HasNIC returns true if the NICID is defined in the stack.
@ -1169,65 +1214,87 @@ func (s *Stack) HasNIC(id tcpip.NICID) bool {
return ok
}
type forwardingFn func(tcpip.NetworkProtocolNumber) (bool, tcpip.Error)
func forwardingValue(forwardingFn forwardingFn, proto tcpip.NetworkProtocolNumber, nicID tcpip.NICID, fnName string) (forward bool, ok bool) {
switch forwarding, err := forwardingFn(proto); err.(type) {
case nil:
return forwarding, true
case *tcpip.ErrUnknownProtocol:
panic(fmt.Sprintf("expected network protocol %d to be available on NIC %d", proto, nicID))
case *tcpip.ErrNotSupported:
// Not all network protocols support forwarding.
default:
panic(fmt.Sprintf("nic(id=%d).%s(%d): %s", nicID, fnName, proto, err))
}
return false, false
}
// precondition: s.mu is held.
func (s *Stack) nicInfo(nic *nic, id tcpip.NICID) *NICInfo {
flags := NICStateFlags{
Up: true, // Netstack interfaces are always up.
Running: nic.Enabled(),
Promiscuous: nic.Promiscuous(),
Loopback: nic.IsLoopback(),
}
netStats := make(map[tcpip.NetworkProtocolNumber]NetworkEndpointStats)
for proto, netEP := range nic.networkEndpoints {
netStats[proto] = netEP.Stats()
}
info := NICInfo{
Name: nic.name,
LinkAddress: nic.NetworkLinkEndpoint.LinkAddress(),
ProtocolAddresses: nic.primaryAddresses(),
Flags: flags,
MTU: nic.NetworkLinkEndpoint.MTU(),
Stats: nic.stats.local,
NetworkStats: netStats,
Context: nic.context,
ARPHardwareType: nic.NetworkLinkEndpoint.ARPHardwareType(),
Forwarding: make(map[tcpip.NetworkProtocolNumber]bool),
MulticastForwarding: make(map[tcpip.NetworkProtocolNumber]bool),
}
for proto := range s.networkProtocols {
if forwarding, ok := forwardingValue(nic.forwarding, proto, id, "forwarding"); ok {
info.Forwarding[proto] = forwarding
}
if multicastForwarding, ok := forwardingValue(nic.multicastForwarding, proto, id, "multicastForwarding"); ok {
info.MulticastForwarding[proto] = multicastForwarding
}
}
if nic.Primary != nil {
info.Primary = nic.Primary.id
}
return &info
}
// SingleNICInfo returns the NICInfo for the given NICID.
func (s *Stack) SingleNICInfo(id tcpip.NICID) (*NICInfo, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
if nic, ok := s.nics[id]; !ok {
return nil, false
} else {
return s.nicInfo(nic, id), true
}
}
// NICInfo returns a map of NICIDs to their associated information.
func (s *Stack) NICInfo() map[tcpip.NICID]NICInfo {
s.mu.RLock()
defer s.mu.RUnlock()
type forwardingFn func(tcpip.NetworkProtocolNumber) (bool, tcpip.Error)
forwardingValue := func(forwardingFn forwardingFn, proto tcpip.NetworkProtocolNumber, nicID tcpip.NICID, fnName string) (forward bool, ok bool) {
switch forwarding, err := forwardingFn(proto); err.(type) {
case nil:
return forwarding, true
case *tcpip.ErrUnknownProtocol:
panic(fmt.Sprintf("expected network protocol %d to be available on NIC %d", proto, nicID))
case *tcpip.ErrNotSupported:
// Not all network protocols support forwarding.
default:
panic(fmt.Sprintf("nic(id=%d).%s(%d): %s", nicID, fnName, proto, err))
}
return false, false
}
nics := make(map[tcpip.NICID]NICInfo)
for id, nic := range s.nics {
flags := NICStateFlags{
Up: true, // Netstack interfaces are always up.
Running: nic.Enabled(),
Promiscuous: nic.Promiscuous(),
Loopback: nic.IsLoopback(),
}
netStats := make(map[tcpip.NetworkProtocolNumber]NetworkEndpointStats)
for proto, netEP := range nic.networkEndpoints {
netStats[proto] = netEP.Stats()
}
info := NICInfo{
Name: nic.name,
LinkAddress: nic.NetworkLinkEndpoint.LinkAddress(),
ProtocolAddresses: nic.primaryAddresses(),
Flags: flags,
MTU: nic.NetworkLinkEndpoint.MTU(),
Stats: nic.stats.local,
NetworkStats: netStats,
Context: nic.context,
ARPHardwareType: nic.NetworkLinkEndpoint.ARPHardwareType(),
Forwarding: make(map[tcpip.NetworkProtocolNumber]bool),
MulticastForwarding: make(map[tcpip.NetworkProtocolNumber]bool),
}
for proto := range s.networkProtocols {
if forwarding, ok := forwardingValue(nic.forwarding, proto, id, "forwarding"); ok {
info.Forwarding[proto] = forwarding
}
if multicastForwarding, ok := forwardingValue(nic.multicastForwarding, proto, id, "multicastForwarding"); ok {
info.MulticastForwarding[proto] = multicastForwarding
}
}
nics[id] = info
nics[id] = *s.nicInfo(nic, id)
}
return nics
}
@ -1991,7 +2058,7 @@ func (s *Stack) Wait() {
for id, n := range s.nics {
// Remove NIC to ensure that qDisc goroutines are correctly
// terminated on stack teardown.
act, _ := s.removeNICLocked(id)
act, _ := s.removeNICLocked(id, true /* closeLinkEndpoint */)
n.NetworkLinkEndpoint.Wait()
if act != nil {
deferActs = append(deferActs, act)
@ -2025,31 +2092,43 @@ func (s *Stack) getNICs() map[tcpip.NICID]*nic {
return nics
}
// ResetConfig resets the stack's NICs and ID generator.
func (s *Stack) ResetConfig() {
nics := make(map[tcpip.NICID]*nic)
s.mu.Lock()
defer s.mu.Unlock()
s.nics = nics
s.loopbackNIC = nil
s.nicIDGen.Store(0)
}
// ReplaceConfig replaces config in the loaded stack.
func (s *Stack) ReplaceConfig(st *Stack) {
if st == nil {
panic("stack.Stack cannot be nil when netstack s/r is enabled")
panic("stack.Stack cannot be nil when replacing config")
}
// Update route table.
s.SetRouteTable(st.GetRouteTable())
// Update NICs.
nics := st.getNICs()
s.mu.Lock()
defer s.mu.Unlock()
s.nics = make(map[tcpip.NICID]*nic)
s.loopbackNIC = nil
// Update iptables and nftables.
s.tables = st.IPTables()
s.SetNFTables(st.NFTables())
for id, nic := range nics {
nic.stack = s
s.nics[id] = nic
if nic.IsLoopback() {
s.loopbackNIC = nic
} else if s.externalNetworkingDisabled {
nic.disable()
}
_ = s.NextNICID()
}
s.tables = st.tables
s.nftables = st.nftables
}
// Restore restarts the stack after a restore. This must be called after the
@ -2060,7 +2139,6 @@ func (s *Stack) Restore() {
s.mu.Lock()
eps := s.restoredEndpoints
s.restoredEndpoints = nil
saveRestoreEnabled := s.saveRestoreEnabled
s.mu.Unlock()
for _, e := range eps {
e.Restore(s)
@ -2070,13 +2148,9 @@ func (s *Stack) Restore() {
// protocol level background workers.
tcpip.AsyncLoading.Wait()
// Now resume any protocol level background workers.
// Now restore any protocol level background workers.
for _, p := range s.transportProtocols {
if saveRestoreEnabled {
p.proto.Restore()
} else {
p.proto.Resume()
}
p.proto.Restore()
}
}
@ -2152,6 +2226,12 @@ func (s *Stack) unregisterPacketEndpointLocked(nicID tcpip.NICID, netProto tcpip
// WritePacketToRemote writes a payload on the specified NIC using the provided
// network protocol and remote link address.
func (s *Stack) WritePacketToRemote(nicID tcpip.NICID, remote tcpip.LinkAddress, netProto tcpip.NetworkProtocolNumber, payload buffer.Buffer) tcpip.Error {
return s.WritePacketToRemoteWithMark(nicID, remote, netProto, payload, 0)
}
// WritePacketToRemoteWithMark writes a payload on the specified NIC using the
// provided network protocol, remote link address, and packet mark.
func (s *Stack) WritePacketToRemoteWithMark(nicID tcpip.NICID, remote tcpip.LinkAddress, netProto tcpip.NetworkProtocolNumber, payload buffer.Buffer, mark uint32) tcpip.Error {
s.mu.Lock()
nic, ok := s.nics[nicID]
s.mu.Unlock()
@ -2161,6 +2241,7 @@ func (s *Stack) WritePacketToRemote(nicID tcpip.NICID, remote tcpip.LinkAddress,
pkt := NewPacketBuffer(PacketBufferOptions{
ReserveHeaderBytes: int(nic.MaxHeaderLength()),
Payload: payload,
Mark: mark,
})
defer pkt.DecRef()
pkt.NetworkProtocolNumber = netProto
@ -2170,6 +2251,12 @@ func (s *Stack) WritePacketToRemote(nicID tcpip.NICID, remote tcpip.LinkAddress,
// WriteRawPacket writes data directly to the specified NIC without adding any
// headers.
func (s *Stack) WriteRawPacket(nicID tcpip.NICID, proto tcpip.NetworkProtocolNumber, payload buffer.Buffer) tcpip.Error {
return s.WriteRawPacketWithMark(nicID, proto, payload, 0)
}
// WriteRawPacketWithMark writes data directly to the specified NIC without adding any
// headers, setting the specified packet mark.
func (s *Stack) WriteRawPacketWithMark(nicID tcpip.NICID, proto tcpip.NetworkProtocolNumber, payload buffer.Buffer, mark uint32) tcpip.Error {
s.mu.RLock()
nic, ok := s.nics[nicID]
s.mu.RUnlock()
@ -2179,6 +2266,7 @@ func (s *Stack) WriteRawPacket(nicID tcpip.NICID, proto tcpip.NetworkProtocolNum
pkt := NewPacketBuffer(PacketBufferOptions{
Payload: payload,
Mark: mark,
})
defer pkt.DecRef()
pkt.NetworkProtocolNumber = proto
@ -2244,14 +2332,47 @@ func (s *Stack) IPTables() *IPTables {
return s.tables
}
// SetIPTables sets the stack's iptables.
func (s *Stack) SetIPTables(tables *IPTables) {
s.tables = tables
}
// NFTables returns the stack's nftables.
func (s *Stack) NFTables() NFTablesInterface {
return s.nftables
val := s.nftables.Load()
if val == nil {
return nil
}
return *val
}
// SetNFTables sets the stack's nftables.
func (s *Stack) SetNFTables(nft NFTablesInterface) {
s.nftables = nft
if nft == nil {
s.nftables.Store(nil)
} else {
s.nftables.Store(&nft)
}
}
// LockNFTablesUpdate locks the stack's nftables update mutex for netlink batch modification.
func (s *Stack) LockNFTablesUpdate() {
s.nftablesUpdateMu.Lock()
}
// UnlockNFTablesUpdate unlocks the stack's nftables update mutex.
func (s *Stack) UnlockNFTablesUpdate() {
s.nftablesUpdateMu.Unlock()
}
// IsNFTablesConfigured returns true if the stack has nftables configured.
func (s *Stack) IsNFTablesConfigured() bool {
return s.nftablesConfigured.Load()
}
// SetNFTablesConfigured sets whether the stack has nftables configured.
func (s *Stack) SetNFTablesConfigured(configured bool) {
s.nftablesConfigured.Store(configured)
}
// ICMPLimit returns the maximum number of ICMP messages that can be sent
@ -2460,12 +2581,11 @@ func (s *Stack) SetNICStack(id tcpip.NICID, peer *Stack) (tcpip.NICID, tcpip.Err
s.mu.Unlock()
return id, nil
}
delete(s.nics, id)
// Remove routes in-place. n tracks the number of routes written.
s.RemoveRoutes(func(r tcpip.Route) bool { return r.NIC == id })
ne := nic.NetworkLinkEndpoint.(LinkEndpoint)
deferAct, err := nic.remove(false /* closeLinkEndpoint */)
linkEp := nic.NetworkLinkEndpoint.(LinkEndpoint)
name := nic.Name()
deferAct, err := s.removeNICLocked(id, false /* closeLinkEndpoint */)
s.mu.Unlock()
if deferAct != nil {
deferAct()
@ -2475,34 +2595,71 @@ func (s *Stack) SetNICStack(id tcpip.NICID, peer *Stack) (tcpip.NICID, tcpip.Err
}
id = tcpip.NICID(peer.NextNICID())
return id, peer.CreateNICWithOptions(id, ne, NICOptions{Name: nic.Name()})
return id, peer.CreateNICWithOptions(id, linkEp, NICOptions{Name: name})
}
// EnableSaveRestore marks the saveRestoreEnabled to true.
func (s *Stack) EnableSaveRestore() {
// SetRemoveConf sets the removeConf in stack to the given value.
func (s *Stack) SetRemoveConf(removeConf bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.saveRestoreEnabled = true
s.removeConf = removeConf
}
// IsSaveRestoreEnabled returns true if save restore is enabled for the stack.
func (s *Stack) IsSaveRestoreEnabled() bool {
// GetRemoveConf gets the removeConf from stack.
func (s *Stack) GetRemoveConf() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.removeConf
}
// SetAllowConnectedOnSave sets allowConnectedOnSave in stack with the given value.
func (s *Stack) SetAllowConnectedOnSave(allowConnectedOnSave bool) {
s.mu.Lock()
defer s.mu.Unlock()
return s.saveRestoreEnabled
s.allowConnectedOnSave = allowConnectedOnSave
}
// contextID is this package's type for context.Context.Value keys.
type contextID int
const (
// CtxRestoreStack is a Context.Value key for the stack to be used in restore.
CtxRestoreStack contextID = iota
)
// RestoreStackFromContext returns the stack to be used during restore.
func RestoreStackFromContext(ctx context.Context) *Stack {
return ctx.Value(CtxRestoreStack).(*Stack)
// GetAllowConnectedOnSave gets the allowConnectedOnSave from stack.
func (s *Stack) GetAllowConnectedOnSave() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.allowConnectedOnSave
}
// AllowLiveTCPMigration returns if TCP connections can be migrated.
func (s *Stack) AllowLiveTCPMigration() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.allowLiveTCPMigration
}
// SetAllowLiveTCPMigration sets if TCP connections can be migrated.
func (s *Stack) SetAllowLiveTCPMigration(allow bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.allowLiveTCPMigration = allow
}
// DisableAllNonLoopbackNICs disables all non-loopback NICs in the stack.
func (s *Stack) DisableAllNonLoopbackNICs() {
s.mu.Lock()
defer s.mu.Unlock()
s.externalNetworkingDisabled = true
for _, nic := range s.nics {
if !nic.IsLoopback() {
nic.disable()
}
}
}
// EnableAllNonLoopbackNICs enables all non-loopback NICs in the stack.
func (s *Stack) EnableAllNonLoopbackNICs() {
s.mu.Lock()
defer s.mu.Unlock()
s.externalNetworkingDisabled = false
for _, nic := range s.nics {
if !nic.IsLoopback() {
nic.enable()
}
}
}