Обновление снапшота с 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-паникой, что в полевом крашдампе; с ним зелёный.
581 lines
16 KiB
Go
581 lines
16 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 stack
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
|
|
"github.com/sagernet/gvisor/pkg/log"
|
|
"github.com/sagernet/gvisor/pkg/tcpip"
|
|
"github.com/sagernet/gvisor/pkg/tcpip/header"
|
|
)
|
|
|
|
// NATType represents the type of NAT.
|
|
type NATType int
|
|
|
|
const (
|
|
// SNAT is source NAT.
|
|
SNAT NATType = iota
|
|
// DNAT is destination NAT.
|
|
DNAT
|
|
// NATUnknown is unknown NAT type.
|
|
NATUnknown
|
|
)
|
|
|
|
// ToNATType converts a uint8 to a NATType.
|
|
func ToNATType(t uint8) NATType {
|
|
switch t {
|
|
case 0:
|
|
return SNAT
|
|
case 1:
|
|
return DNAT
|
|
}
|
|
return NATUnknown
|
|
}
|
|
|
|
func (natType NATType) String() string {
|
|
switch natType {
|
|
case SNAT:
|
|
return "SNAT"
|
|
case DNAT:
|
|
return "DNAT"
|
|
default:
|
|
return "NATUnknown"
|
|
}
|
|
}
|
|
|
|
// NfNATPriority returns the priority of the NAT hook.
|
|
// Check `ipv4/ipv6_nat_ops` in nf_nat_proto.c.
|
|
func NfNATPriority(hook NFHook) (int, bool) {
|
|
switch hook {
|
|
case NFPrerouting:
|
|
// NF_IP_PRI_NAT_DST
|
|
return -100, true
|
|
case NFPostrouting:
|
|
// NF_IP_PRI_NAT_SRC
|
|
return 100, true
|
|
case NFOutput:
|
|
// NF_IP_PRI_NAT_DST
|
|
return -100, true
|
|
case NFInput:
|
|
// NF_IP_PRI_NAT_SRC
|
|
return 100, true
|
|
}
|
|
// NAT is not supported for other hooks.
|
|
return 0, false
|
|
}
|
|
|
|
// NfHookToNATType returns the applicable NAT type
|
|
// for the given netfilter hook.
|
|
func NfHookToNATType(hook NFHook) NATType {
|
|
switch hook {
|
|
case NFPrerouting, NFOutput:
|
|
return DNAT
|
|
case NFInput, NFPostrouting:
|
|
return SNAT
|
|
}
|
|
return NATUnknown
|
|
}
|
|
|
|
// handlePacketOpts contains the options for handlePacket.
|
|
type handlePacketOpts struct {
|
|
fullChecksum bool
|
|
updatePseudoHeader bool
|
|
natType NATType
|
|
}
|
|
|
|
// handlePacket attempts to handle a packet and perform NAT if the connection
|
|
// has had NAT performed on it.
|
|
//
|
|
// Returns true if the packet can skip the NAT table.
|
|
func handlePacket(pkt *PacketBuffer, opts *handlePacketOpts) bool {
|
|
if opts == nil || opts.natType == NATUnknown {
|
|
return false
|
|
}
|
|
netHdr, transHdr, isICMPError, ok := pkt.GetHeaders()
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
natDone := &pkt.snatDone
|
|
dnat := false
|
|
if opts.natType == DNAT {
|
|
natDone = &pkt.dnatDone
|
|
dnat = true
|
|
}
|
|
|
|
if *natDone {
|
|
panic(fmt.Sprintf("packet already had NAT: %s performed; pkt=%#v", opts.natType, pkt))
|
|
}
|
|
|
|
// TODO(gvisor.dev/issue/5748): TCP checksums on inbound packets should be
|
|
// validated if checksum offloading is off. It may require IP defrag if the
|
|
// packets are fragmented.
|
|
|
|
reply := pkt.tuple.reply
|
|
cn := pkt.tuple.conn
|
|
|
|
tid, manip := func() (tupleID, manipType) {
|
|
cn.mu.RLock()
|
|
defer cn.mu.RUnlock()
|
|
|
|
if reply {
|
|
tid := cn.original.tupleID
|
|
|
|
if dnat {
|
|
return tid, cn.sourceManip
|
|
}
|
|
return tid, cn.destinationManip
|
|
}
|
|
|
|
tid := cn.reply.tupleID
|
|
if dnat {
|
|
return tid, cn.destinationManip
|
|
}
|
|
return tid, cn.sourceManip
|
|
}()
|
|
switch manip {
|
|
case manipNotPerformed:
|
|
return false
|
|
case manipPerformedNoop:
|
|
*natDone = true
|
|
return true
|
|
case manipPerformed:
|
|
default:
|
|
panic(fmt.Sprintf("unhandled manip = %d", manip))
|
|
}
|
|
|
|
newPort := tid.dstPortOrEchoReplyIdent
|
|
newAddr := tid.dstAddr
|
|
if dnat {
|
|
newPort = tid.srcPortOrEchoRequestIdent
|
|
newAddr = tid.srcAddr
|
|
}
|
|
|
|
UpdateHeaders(
|
|
netHdr,
|
|
transHdr,
|
|
!dnat != isICMPError,
|
|
opts.fullChecksum,
|
|
opts.updatePseudoHeader,
|
|
newPort,
|
|
newAddr,
|
|
)
|
|
|
|
*natDone = true
|
|
|
|
if !isICMPError {
|
|
return true
|
|
}
|
|
|
|
// We performed NAT on (erroneous) packet that triggered an ICMP response, but
|
|
// not the ICMP packet itself.
|
|
switch pkt.TransportProtocolNumber {
|
|
case header.ICMPv4ProtocolNumber:
|
|
icmp := header.ICMPv4(pkt.TransportHeader().Slice())
|
|
// TODO(https://gvisor.dev/issue/6788): Incrementally update ICMP checksum.
|
|
icmp.SetChecksum(0)
|
|
icmp.SetChecksum(header.ICMPv4Checksum(icmp, pkt.Data().Checksum()))
|
|
|
|
network := header.IPv4(pkt.NetworkHeader().Slice())
|
|
if dnat {
|
|
network.SetDestinationAddressWithChecksumUpdate(tid.srcAddr)
|
|
} else {
|
|
network.SetSourceAddressWithChecksumUpdate(tid.dstAddr)
|
|
}
|
|
case header.ICMPv6ProtocolNumber:
|
|
network := header.IPv6(pkt.NetworkHeader().Slice())
|
|
srcAddr := network.SourceAddress()
|
|
dstAddr := network.DestinationAddress()
|
|
if dnat {
|
|
dstAddr = tid.srcAddr
|
|
} else {
|
|
srcAddr = tid.dstAddr
|
|
}
|
|
|
|
icmp := header.ICMPv6(pkt.TransportHeader().Slice())
|
|
// TODO(https://gvisor.dev/issue/6788): Incrementally update ICMP checksum.
|
|
icmp.SetChecksum(0)
|
|
payload := pkt.Data()
|
|
icmp.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
|
|
Header: icmp,
|
|
Src: srcAddr,
|
|
Dst: dstAddr,
|
|
PayloadCsum: payload.Checksum(),
|
|
PayloadLen: payload.Size(),
|
|
}))
|
|
|
|
if dnat {
|
|
network.SetDestinationAddress(dstAddr)
|
|
} else {
|
|
network.SetSourceAddress(srcAddr)
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// IPTHandlePacket handles and applies NAT to the packet if required.
|
|
func IPTHandlePacket(pkt *PacketBuffer, hook Hook, r *Route) bool {
|
|
opts := handlePacketOpts{
|
|
fullChecksum: false,
|
|
updatePseudoHeader: false,
|
|
natType: SNAT,
|
|
}
|
|
requiresTXTransportChecksum := false
|
|
if r != nil {
|
|
requiresTXTransportChecksum = r.RequiresTXTransportChecksum()
|
|
}
|
|
switch hook {
|
|
case Prerouting:
|
|
opts.fullChecksum = true
|
|
opts.updatePseudoHeader = true
|
|
opts.natType = DNAT
|
|
case Input:
|
|
case Forward:
|
|
panic("should not handle packet in the forwarding hook")
|
|
case Output:
|
|
opts.natType = DNAT
|
|
fallthrough
|
|
case Postrouting:
|
|
if pkt.TransportProtocolNumber == header.TCPProtocolNumber && pkt.GSOOptions.Type != GSONone && pkt.GSOOptions.NeedsCsum {
|
|
opts.updatePseudoHeader = true
|
|
} else if requiresTXTransportChecksum {
|
|
opts.fullChecksum = true
|
|
opts.updatePseudoHeader = true
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("unrecognized hook = %d", hook))
|
|
}
|
|
|
|
return handlePacket(pkt, &opts)
|
|
}
|
|
|
|
// PortOrIdentRange represents a range of ports or idents
|
|
// range to use for NAT.
|
|
type PortOrIdentRange struct {
|
|
Start uint16
|
|
Size uint32
|
|
}
|
|
|
|
// ConfigureNAT setups up the connection for the specified NAT and rewrites the
|
|
// packet.
|
|
//
|
|
// If NAT has already been performed on the connection, then the packet will
|
|
// be rewritten with the NAT performed on the connection, ignoring the passed
|
|
// address and port range.
|
|
//
|
|
// Generally, only the first packet of a connection reaches this method; other
|
|
// packets will be manipulated without needing to modify the connection.
|
|
//
|
|
// Returns whether the NAT was configured or not.
|
|
func (cn *conn) ConfigureNAT(portsOrIdents PortOrIdentRange, natAddress tcpip.Address, natType NATType, changePort, changeAddress bool) bool {
|
|
lastPortOrIdentU32 := uint32(portsOrIdents.Start) + portsOrIdents.Size - 1
|
|
if lastPortOrIdentU32 > math.MaxUint16 {
|
|
log.Warningf("got lastPortOrIdent = %d, want <= MaxUint16(=%d); portsOrIdents=%#v", lastPortOrIdentU32, math.MaxUint16, portsOrIdents)
|
|
return false
|
|
}
|
|
lastPortOrIdent := uint16(lastPortOrIdentU32)
|
|
|
|
cn.mu.Lock()
|
|
defer cn.mu.Unlock()
|
|
|
|
var manip *manipType
|
|
var address *tcpip.Address
|
|
var portOrIdent *uint16
|
|
if natType == DNAT {
|
|
manip = &cn.destinationManip
|
|
address = &cn.reply.tupleID.srcAddr
|
|
portOrIdent = &cn.reply.tupleID.srcPortOrEchoRequestIdent
|
|
} else {
|
|
manip = &cn.sourceManip
|
|
address = &cn.reply.tupleID.dstAddr
|
|
portOrIdent = &cn.reply.tupleID.dstPortOrEchoReplyIdent
|
|
}
|
|
|
|
if *manip != manipNotPerformed {
|
|
return true
|
|
}
|
|
*manip = manipPerformed
|
|
if changeAddress {
|
|
*address = natAddress
|
|
}
|
|
|
|
// Everything below here is port-fiddling.
|
|
if !changePort {
|
|
return true
|
|
}
|
|
|
|
// Does the current port/ident fit in the range?
|
|
if portsOrIdents.Start <= *portOrIdent && *portOrIdent <= lastPortOrIdent {
|
|
// Yes, is the current reply tuple unique?
|
|
//
|
|
// Or, does the reply tuple refer to the same connection as the current one that
|
|
// we are NATing? This would apply, for example, to a self-connected socket,
|
|
// where the original and reply tuples are identical.
|
|
other := cn.ct.connForTID(cn.reply.tupleID)
|
|
if other == nil || other.conn == cn {
|
|
// Yes! No need to change the port.
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Try our best to find a port/ident that results in a unique reply tuple.
|
|
//
|
|
// We limit the number of attempts to find a unique tuple to not waste a lot
|
|
// of time looking for a unique tuple.
|
|
//
|
|
// Matches linux behaviour introduced in
|
|
// https://github.com/torvalds/linux/commit/a504b703bb1da526a01593da0e4be2af9d9f5fa8.
|
|
const maxAttemptsForInitialRound uint32 = 128
|
|
const minAttemptsToContinue = 16
|
|
|
|
allowedInitialAttempts := maxAttemptsForInitialRound
|
|
if allowedInitialAttempts > portsOrIdents.Size {
|
|
allowedInitialAttempts = portsOrIdents.Size
|
|
}
|
|
|
|
for maxAttempts := allowedInitialAttempts; ; maxAttempts /= 2 {
|
|
// Start reach round with a random initial port/ident offset.
|
|
randOffset := cn.ct.rng.Uint32()
|
|
|
|
for i := uint32(0); i < maxAttempts; i++ {
|
|
newPortOrIdentU32 := uint32(portsOrIdents.Start) + (randOffset+i)%portsOrIdents.Size
|
|
if newPortOrIdentU32 > math.MaxUint16 {
|
|
log.Warningf("got newPortOrIdentU32 = %d, want <= MaxUint16(=%d); portsOrIdents=%#v", newPortOrIdentU32, math.MaxUint16, portsOrIdents)
|
|
continue
|
|
}
|
|
|
|
*portOrIdent = uint16(newPortOrIdentU32)
|
|
|
|
if other := cn.ct.connForTID(cn.reply.tupleID); other == nil {
|
|
// We found a unique tuple!
|
|
return true
|
|
}
|
|
}
|
|
|
|
if maxAttempts == portsOrIdents.Size {
|
|
// We already tried all the ports/idents in the range so no need to keep
|
|
// trying.
|
|
return false
|
|
}
|
|
|
|
if maxAttempts < minAttemptsToContinue {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// We did not find a unique tuple, use the last used port anyways.
|
|
// TODO(https://gvisor.dev/issue/6850): Handle not finding a unique tuple
|
|
// better (e.g. remove the connection and drop the packet).
|
|
}
|
|
|
|
// IPTPerformNAT performs NAT on the packet and updates the connection.
|
|
// Used by IPTables.
|
|
func IPTPerformNAT(pkt *PacketBuffer, hook Hook, r *Route, portsOrIdents PortOrIdentRange, natAddress tcpip.Address, dnat, changePort, changeAddress bool) {
|
|
// Make sure the packet is re-written after performing NAT.
|
|
defer func() {
|
|
// handlePacket returns true if the packet may skip the NAT table as the
|
|
// connection is already NATed, but if we reach this point we must be in the
|
|
// NAT table, so the return value is useless for us.
|
|
_ = IPTHandlePacket(pkt, hook, r)
|
|
}()
|
|
cn := pkt.tuple.conn
|
|
natType := SNAT
|
|
if dnat {
|
|
natType = DNAT
|
|
}
|
|
_ = cn.ConfigureNAT(portsOrIdents, natAddress, natType, changePort, changeAddress)
|
|
}
|
|
|
|
// IPTMaybePerformNoopNAT can apply NAT or configure a no-op NAT.
|
|
// If NAT has not been configured for this connection, either mark the
|
|
// connection as configured for "no-op NAT", in the case of DNAT, or, in the
|
|
// case of SNAT, perform source port remapping so that source ports used by
|
|
// locally-generated traffic do not conflict with ports occupied by existing NAT
|
|
// bindings.
|
|
//
|
|
// Note that in the typical case this is also a no-op, because `snatAction`
|
|
// will do nothing if the original tuple is already unique.
|
|
func IPTMaybePerformNoopNAT(pkt *PacketBuffer, hook Hook, r *Route, dnat bool) {
|
|
cn := pkt.tuple.conn
|
|
cn.mu.Lock()
|
|
var manip *manipType
|
|
if dnat {
|
|
manip = &cn.destinationManip
|
|
} else {
|
|
manip = &cn.sourceManip
|
|
}
|
|
if *manip != manipNotPerformed {
|
|
cn.mu.Unlock()
|
|
_ = IPTHandlePacket(pkt, hook, r)
|
|
return
|
|
}
|
|
if dnat {
|
|
*manip = manipPerformedNoop
|
|
cn.mu.Unlock()
|
|
_ = IPTHandlePacket(pkt, hook, r)
|
|
return
|
|
}
|
|
cn.mu.Unlock()
|
|
|
|
// At this point, we know that NAT has not yet been performed on this
|
|
// connection, and the DNAT case has been handled with a no-op. For SNAT, we
|
|
// simply perform source port remapping to ensure that source ports for
|
|
// locally generated traffic do not clash with ports used by existing NAT
|
|
// bindings.
|
|
_, _ = snatAction(pkt, hook, r, 0, tcpip.Address{}, true /* changePort */, false /* changeAddress */)
|
|
}
|
|
|
|
// NFTApplyNAT applies NAT to the packet and updates the connection.
|
|
// Similar to IPTHandlePacket but for NFTables hooks.
|
|
func NFTApplyNAT(pkt *PacketBuffer, hook NFHook, rt *Route) bool {
|
|
requiresTXTransportChecksum := false
|
|
if rt != nil {
|
|
requiresTXTransportChecksum = rt.RequiresTXTransportChecksum()
|
|
}
|
|
opts := handlePacketOpts{
|
|
fullChecksum: false,
|
|
updatePseudoHeader: false,
|
|
natType: SNAT,
|
|
}
|
|
switch hook {
|
|
case NFPrerouting:
|
|
opts.fullChecksum = true
|
|
opts.updatePseudoHeader = true
|
|
opts.natType = DNAT
|
|
case NFInput:
|
|
case NFForward:
|
|
panic("should not handle packet in the forwarding hook")
|
|
case NFOutput:
|
|
opts.natType = DNAT
|
|
fallthrough
|
|
case NFPostrouting:
|
|
if pkt.TransportProtocolNumber == header.TCPProtocolNumber && pkt.GSOOptions.Type != GSONone && pkt.GSOOptions.NeedsCsum {
|
|
opts.updatePseudoHeader = true
|
|
} else if requiresTXTransportChecksum {
|
|
opts.fullChecksum = true
|
|
opts.updatePseudoHeader = true
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("unrecognized hook = %d", hook))
|
|
}
|
|
|
|
return handlePacket(pkt, &opts)
|
|
}
|
|
|
|
// IsNATConfigured returns whether NAT has been configured for the given NAT type.
|
|
func (cn *conn) IsNATConfigured(natType NATType) bool {
|
|
cn.mu.RLock()
|
|
defer cn.mu.RUnlock()
|
|
switch natType {
|
|
case SNAT:
|
|
return cn.sourceManip != manipNotPerformed
|
|
case DNAT:
|
|
return cn.destinationManip != manipNotPerformed
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ConfigureNoopNAT configures the connection for no-op NAT.
|
|
// Similar to the func `IPTMaybePerformNoopNAT` except that this one only configures NO-OP NAT and is independent of IPTables.
|
|
func (cn *conn) ConfigureNoopNAT(pkt *PacketBuffer, natType NATType) bool {
|
|
cn.mu.Lock()
|
|
var manip *manipType
|
|
if natType == DNAT {
|
|
manip = &cn.destinationManip
|
|
} else {
|
|
manip = &cn.sourceManip
|
|
}
|
|
|
|
if *manip != manipNotPerformed {
|
|
cn.mu.Unlock()
|
|
return true
|
|
}
|
|
|
|
if natType == DNAT {
|
|
*manip = manipPerformedNoop
|
|
cn.mu.Unlock()
|
|
return true
|
|
}
|
|
cn.mu.Unlock()
|
|
|
|
// At this point, we know that NAT has not yet been performed on this
|
|
// connection, and the DNAT case has been handled with a no-op. For SNAT, we
|
|
// simply perform source port remapping to ensure that source ports for
|
|
// locally generated traffic do not clash with ports used by existing NAT
|
|
// bindings.
|
|
|
|
portsOrIdents := PortOrIdentRange{Start: 0, Size: math.MaxUint16 + 1}
|
|
|
|
// However, we need to extract the port from packet.
|
|
var port uint16
|
|
switch pkt.TransportProtocolNumber {
|
|
case header.UDPProtocolNumber:
|
|
port = header.UDP(pkt.TransportHeader().Slice()).SourcePort()
|
|
case header.TCPProtocolNumber:
|
|
port = header.TCP(pkt.TransportHeader().Slice()).SourcePort()
|
|
}
|
|
|
|
if port != 0 {
|
|
portsOrIdents = targetPortRangeForTCPAndUDP(port)
|
|
}
|
|
|
|
return cn.ConfigureNAT(portsOrIdents, tcpip.Address{}, natType, true /* changePort */, false /* changeAddress */)
|
|
}
|
|
|
|
// ConfigureMasquerade configures the connection for masquerade.
|
|
func (cn *conn) configureMasquerade(pkt *PacketBuffer, route *Route, stk *Stack, ports PortOrIdentRange, changePort bool) bool {
|
|
srcAddr := pkt.Network().SourceAddress()
|
|
if srcAddr == header.IPv4Any || srcAddr == header.IPv6Any {
|
|
return false
|
|
}
|
|
// Masquerade is only supported for postrouting.
|
|
if route == nil {
|
|
return false
|
|
}
|
|
|
|
// Get the network endpoint for the outgoing interface to find its primary address.
|
|
netEP, err := stk.GetNetworkEndpoint(route.NICID(), route.NetProto())
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
addressEP, ok := netEP.(AddressableEndpoint)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Ref: net/netfilter/nf_nat_masquerade.c:nf_nat_masquerade_ipv[4|6]()
|
|
// Use the next hop address as the destination address if it is set.
|
|
nh := route.NextHop()
|
|
if nh.Len() == 0 {
|
|
nh = pkt.Network().DestinationAddress()
|
|
}
|
|
|
|
// addressEP is expected to be set for the postrouting hook.
|
|
// Find the outgoing primary address for the destination address.
|
|
ep := addressEP.AcquireOutgoingPrimaryAddress(nh, tcpip.Address{} /* srcHint */, false /* allowExpired */)
|
|
if ep == nil {
|
|
// No address exists that we can use as a source address.
|
|
return false
|
|
}
|
|
address := ep.AddressWithPrefix().Address
|
|
ep.DecRef()
|
|
|
|
// Configure NAT for the packet to change the source address.
|
|
return cn.ConfigureNAT(ports, address, SNAT, changePort, true /* changeAddress */)
|
|
}
|