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:
parent
ffebe42860
commit
117243aa02
293 changed files with 16413 additions and 2842 deletions
|
|
@ -18,7 +18,6 @@ import (
|
|||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -45,6 +44,31 @@ const (
|
|||
unestablishedTimeout time.Duration = 120 * time.Second
|
||||
)
|
||||
|
||||
// ConnTrackState represents the state of a connection.
|
||||
type ConnTrackState int
|
||||
|
||||
const (
|
||||
// ConnTrackStateInvalid is the invalid connection tracking state.
|
||||
ConnTrackStateInvalid ConnTrackState = -1
|
||||
// ConnTrackStateEstablished represents an established connection.
|
||||
ConnTrackStateEstablished ConnTrackState = 0
|
||||
// ConnTrackStateNew represents a new connection.
|
||||
ConnTrackStateNew ConnTrackState = 2
|
||||
// ConnTrackStateEstablishedReply represents an established connection
|
||||
// in the reply direction.
|
||||
ConnTrackStateEstablishedReply ConnTrackState = 3
|
||||
)
|
||||
|
||||
// ConnTrackDirection represents the direction of a connection.
|
||||
type ConnTrackDirection uint8
|
||||
|
||||
const (
|
||||
// ConnTrackDirectionOriginal represents the original direction.
|
||||
ConnTrackDirectionOriginal ConnTrackDirection = 0
|
||||
// ConnTrackDirectionReply represents the reply direction.
|
||||
ConnTrackDirectionReply ConnTrackDirection = 1
|
||||
)
|
||||
|
||||
// tuple holds a connection's identifying and manipulating data in one
|
||||
// direction. It is immutable.
|
||||
//
|
||||
|
|
@ -161,6 +185,10 @@ type conn struct {
|
|||
//
|
||||
// +checklocks:stateMu
|
||||
lastUsed tcpip.MonotonicTime
|
||||
// replySeen indicates whether a packet in the reply direction has been seen.
|
||||
//
|
||||
// +checklocks:stateMu
|
||||
replySeen bool
|
||||
}
|
||||
|
||||
// timedOut returns whether the connection timed out based on its state.
|
||||
|
|
@ -177,6 +205,27 @@ func (cn *conn) timedOut(now tcpip.MonotonicTime) bool {
|
|||
return now.Sub(cn.lastUsed) > unestablishedTimeout
|
||||
}
|
||||
|
||||
// expiresIn returns the duration from now until the connection times out.
|
||||
func (cn *conn) expiresIn() time.Duration {
|
||||
var timeout time.Duration
|
||||
var lastUsed tcpip.MonotonicTime
|
||||
cn.stateMu.RLock()
|
||||
state := cn.tcb.State()
|
||||
lastUsed = cn.lastUsed
|
||||
cn.stateMu.RUnlock()
|
||||
if state == tcpconntrack.ResultAlive {
|
||||
timeout = establishedTimeout
|
||||
} else {
|
||||
timeout = unestablishedTimeout
|
||||
}
|
||||
now := cn.ct.clock.NowMonotonic()
|
||||
expires := timeout - now.Sub(lastUsed)
|
||||
if expires < 0 {
|
||||
return 0
|
||||
}
|
||||
return expires
|
||||
}
|
||||
|
||||
// update the connection tracking state.
|
||||
func (cn *conn) update(pkt *PacketBuffer, reply bool) {
|
||||
cn.stateMu.Lock()
|
||||
|
|
@ -184,6 +233,9 @@ func (cn *conn) update(pkt *PacketBuffer, reply bool) {
|
|||
|
||||
// Mark the connection as having been used recently so it isn't reaped.
|
||||
cn.lastUsed = cn.ct.clock.NowMonotonic()
|
||||
if reply {
|
||||
cn.replySeen = true
|
||||
}
|
||||
|
||||
if pkt.TransportProtocolNumber != header.TCPProtocolNumber {
|
||||
return
|
||||
|
|
@ -206,6 +258,10 @@ func (cn *conn) update(pkt *PacketBuffer, reply bool) {
|
|||
}
|
||||
}
|
||||
|
||||
type connTrackRNG interface {
|
||||
Uint32() uint32
|
||||
}
|
||||
|
||||
// ConnTrack tracks all connections created for NAT rules. Most users are
|
||||
// expected to only call handlePacket, insertRedirectConn, and maybeInsertNoop.
|
||||
//
|
||||
|
|
@ -225,12 +281,26 @@ type ConnTrack struct {
|
|||
// seed is a one-time random value initialized at stack startup
|
||||
// and is used in the calculation of hash keys for the list of buckets.
|
||||
// It is immutable.
|
||||
//
|
||||
// TODO(gvisor.dev/issue/4595): When Stack.tables becomes savable and
|
||||
// ConnTrack flows into checkpoint state, this seed must be redrawn
|
||||
// from secureRNG during restore AND the entries in buckets must be
|
||||
// rehashed under the new seed. bucket_index = jenkins.Sum32(seed) %
|
||||
// len(buckets) couples the seed value to bucket layout; redrawing the
|
||||
// seed without rehashing leaves restored entries unreachable by
|
||||
// Lookup. Persisting the pre-checkpoint seed extends the brute-force
|
||||
// window across save boundaries.
|
||||
seed uint32
|
||||
|
||||
// nftIDSeed is a one-time random value initialized at stack startup
|
||||
// and is used in the calculation of tuple IDs for nftables.
|
||||
// It is immutable.
|
||||
nftIDSeed uint32
|
||||
|
||||
// clock provides timing used to determine conntrack reapings.
|
||||
clock tcpip.Clock
|
||||
// TODO(b/341946753): Restore when netstack is savable.
|
||||
rand *rand.Rand `state:"nosave"`
|
||||
rng connTrackRNG `state:"nosave"`
|
||||
|
||||
mu connTrackRWMutex `state:"nosave"`
|
||||
// mu protects the buckets slice, but not buckets' contents. Only take
|
||||
|
|
@ -271,99 +341,6 @@ func v6NetAndTransHdr(icmpPayload []byte, minTransHdrLen int) (header.Network, [
|
|||
return netHdr, transHdr[:minTransHdrLen]
|
||||
}
|
||||
|
||||
func getEmbeddedNetAndTransHeaders(pkt *PacketBuffer, netHdrLength int, getNetAndTransHdr netAndTransHeadersFunc, transProto tcpip.TransportProtocolNumber) (header.Network, header.ChecksummableTransport, bool) {
|
||||
switch transProto {
|
||||
case header.TCPProtocolNumber:
|
||||
if netAndTransHeader, ok := pkt.Data().PullUp(netHdrLength + header.TCPMinimumSize); ok {
|
||||
netHeader, transHeaderBytes := getNetAndTransHdr(netAndTransHeader, header.TCPMinimumSize)
|
||||
return netHeader, header.TCP(transHeaderBytes), true
|
||||
}
|
||||
case header.UDPProtocolNumber:
|
||||
if netAndTransHeader, ok := pkt.Data().PullUp(netHdrLength + header.UDPMinimumSize); ok {
|
||||
netHeader, transHeaderBytes := getNetAndTransHdr(netAndTransHeader, header.UDPMinimumSize)
|
||||
return netHeader, header.UDP(transHeaderBytes), true
|
||||
}
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
func getHeaders(pkt *PacketBuffer) (netHdr header.Network, transHdr header.Transport, isICMPError bool, ok bool) {
|
||||
switch pkt.TransportProtocolNumber {
|
||||
case header.TCPProtocolNumber:
|
||||
if tcpHeader := header.TCP(pkt.TransportHeader().Slice()); len(tcpHeader) >= header.TCPMinimumSize {
|
||||
return pkt.Network(), tcpHeader, false, true
|
||||
}
|
||||
return nil, nil, false, false
|
||||
case header.UDPProtocolNumber:
|
||||
if udpHeader := header.UDP(pkt.TransportHeader().Slice()); len(udpHeader) >= header.UDPMinimumSize {
|
||||
return pkt.Network(), udpHeader, false, true
|
||||
}
|
||||
return nil, nil, false, false
|
||||
case header.ICMPv4ProtocolNumber:
|
||||
icmpHeader := header.ICMPv4(pkt.TransportHeader().Slice())
|
||||
if len(icmpHeader) < header.ICMPv4MinimumSize {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
|
||||
switch icmpType := icmpHeader.Type(); icmpType {
|
||||
case header.ICMPv4Echo, header.ICMPv4EchoReply:
|
||||
return pkt.Network(), icmpHeader, false, true
|
||||
case header.ICMPv4DstUnreachable, header.ICMPv4TimeExceeded, header.ICMPv4ParamProblem:
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected ICMPv4 type = %d", icmpType))
|
||||
}
|
||||
|
||||
h, ok := pkt.Data().PullUp(header.IPv4MinimumSize)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("should have a valid IPv4 packet; only have %d bytes, want at least %d bytes", pkt.Data().Size(), header.IPv4MinimumSize))
|
||||
}
|
||||
|
||||
if header.IPv4(h).HeaderLength() > header.IPv4MinimumSize {
|
||||
// TODO(https://gvisor.dev/issue/6765): Handle IPv4 options.
|
||||
panic("should have dropped packets with IPv4 options")
|
||||
}
|
||||
|
||||
if netHdr, transHdr, ok := getEmbeddedNetAndTransHeaders(pkt, header.IPv4MinimumSize, v4NetAndTransHdr, pkt.tuple.tupleID.transProto); ok {
|
||||
return netHdr, transHdr, true, true
|
||||
}
|
||||
return nil, nil, false, false
|
||||
case header.ICMPv6ProtocolNumber:
|
||||
icmpHeader := header.ICMPv6(pkt.TransportHeader().Slice())
|
||||
if len(icmpHeader) < header.ICMPv6MinimumSize {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
|
||||
switch icmpType := icmpHeader.Type(); icmpType {
|
||||
case header.ICMPv6EchoRequest, header.ICMPv6EchoReply:
|
||||
return pkt.Network(), icmpHeader, false, true
|
||||
case header.ICMPv6DstUnreachable, header.ICMPv6PacketTooBig, header.ICMPv6TimeExceeded, header.ICMPv6ParamProblem:
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected ICMPv6 type = %d", icmpType))
|
||||
}
|
||||
|
||||
h, ok := pkt.Data().PullUp(header.IPv6MinimumSize)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("should have a valid IPv6 packet; only have %d bytes, want at least %d bytes", pkt.Data().Size(), header.IPv6MinimumSize))
|
||||
}
|
||||
|
||||
// We do not support extension headers in ICMP errors so the next header
|
||||
// in the IPv6 packet should be a tracked protocol if we reach this point.
|
||||
//
|
||||
// TODO(https://gvisor.dev/issue/6789): Support extension headers.
|
||||
transProto := pkt.tuple.tupleID.transProto
|
||||
if got := header.IPv6(h).TransportProtocol(); got != transProto {
|
||||
panic(fmt.Sprintf("got TransportProtocol() = %d, want = %d", got, transProto))
|
||||
}
|
||||
|
||||
if netHdr, transHdr, ok := getEmbeddedNetAndTransHeaders(pkt, header.IPv6MinimumSize, v6NetAndTransHdr, transProto); ok {
|
||||
return netHdr, transHdr, true, true
|
||||
}
|
||||
return nil, nil, false, false
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected transport protocol = %d", pkt.TransportProtocolNumber))
|
||||
}
|
||||
}
|
||||
|
||||
func getTupleIDForRegularPacket(netHdr header.Network, netProto tcpip.NetworkProtocolNumber, transHdr header.Transport, transProto tcpip.TransportProtocolNumber) tupleID {
|
||||
return tupleID{
|
||||
srcAddr: netHdr.SourceAddress(),
|
||||
|
|
@ -376,7 +353,7 @@ func getTupleIDForRegularPacket(netHdr header.Network, netProto tcpip.NetworkPro
|
|||
}
|
||||
|
||||
func getTupleIDForPacketInICMPError(pkt *PacketBuffer, getNetAndTransHdr netAndTransHeadersFunc, netProto tcpip.NetworkProtocolNumber, netLen int, transProto tcpip.TransportProtocolNumber) (tupleID, bool) {
|
||||
if netHdr, transHdr, ok := getEmbeddedNetAndTransHeaders(pkt, netLen, getNetAndTransHdr, transProto); ok {
|
||||
if netHdr, transHdr, ok := pkt.GetEmbeddedNetAndTransHeaders(netLen, getNetAndTransHdr, transProto); ok {
|
||||
return tupleID{
|
||||
srcAddr: netHdr.DestinationAddress(),
|
||||
srcPortOrEchoRequestIdent: transHdr.DestinationPort(),
|
||||
|
|
@ -602,6 +579,11 @@ func (ct *ConnTrack) getConnAndUpdate(pkt *PacketBuffer, skipChecksumValidation
|
|||
return t
|
||||
}
|
||||
|
||||
// GetConnAndUpdatePkt gets the connection for the packet and also sets the packet's tuple.
|
||||
func (ct *ConnTrack) GetConnAndUpdatePkt(pkt *PacketBuffer, skipChecksumValidation bool) {
|
||||
pkt.tuple = ct.getConnAndUpdate(pkt, skipChecksumValidation)
|
||||
}
|
||||
|
||||
func (ct *ConnTrack) connForTID(tid tupleID) *tuple {
|
||||
ct.mu.RLock()
|
||||
bkt := &ct.buckets[ct.bucket(tid)]
|
||||
|
|
@ -610,6 +592,114 @@ func (ct *ConnTrack) connForTID(tid tupleID) *tuple {
|
|||
return bkt.connForTID(tid, ct.clock.NowMonotonic())
|
||||
}
|
||||
|
||||
// ConnTrackInfo holds connection tracking information for a packet.
|
||||
type ConnTrackInfo struct {
|
||||
State ConnTrackState
|
||||
Direction ConnTrackDirection
|
||||
SrcAddr tcpip.Address
|
||||
DstAddr tcpip.Address
|
||||
SrcPort uint16
|
||||
DstPort uint16
|
||||
NetProto tcpip.NetworkProtocolNumber
|
||||
TransProto tcpip.TransportProtocolNumber
|
||||
Expiration time.Duration
|
||||
PseudoID uint32
|
||||
Bytes uint64
|
||||
Packets uint64
|
||||
}
|
||||
|
||||
// ConnTrackInfoOpts holds options for GetConnTrackInfo.
|
||||
type ConnTrackInfoOpts struct {
|
||||
FillState bool
|
||||
UseReplyDir bool
|
||||
FillPseudoID bool
|
||||
FillExpiration bool
|
||||
}
|
||||
|
||||
// getTCPConnTrackState converts the TCB state to ConnTrackState.
|
||||
func (cn *conn) getTCPConnTrackState(useReplyDir bool) ConnTrackState {
|
||||
state := ConnTrackStateInvalid
|
||||
cn.stateMu.RLock()
|
||||
tcbState := cn.tcb.State()
|
||||
cn.stateMu.RUnlock()
|
||||
switch tcbState {
|
||||
case tcpconntrack.ResultConnecting:
|
||||
state = ConnTrackStateNew
|
||||
|
||||
case tcpconntrack.ResultAlive, tcpconntrack.ResultReset,
|
||||
tcpconntrack.ResultClosedByOriginator, tcpconntrack.ResultClosedByResponder:
|
||||
|
||||
if useReplyDir {
|
||||
state = ConnTrackStateEstablishedReply
|
||||
} else {
|
||||
state = ConnTrackStateEstablished
|
||||
}
|
||||
case tcpconntrack.ResultDrop:
|
||||
state = ConnTrackStateInvalid
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// getConnTrackState returns the connection tracking state for the connection.
|
||||
func (cn *conn) getConnTrackState(useReplyDir bool) ConnTrackState {
|
||||
state := ConnTrackStateInvalid
|
||||
// TCP connections have their own state machine in the TCB.
|
||||
if cn.original.tupleID.transProto == header.TCPProtocolNumber {
|
||||
return cn.getTCPConnTrackState(useReplyDir)
|
||||
}
|
||||
// For non-TCP connections, fill the info based on the reply.
|
||||
cn.stateMu.RLock()
|
||||
replySeen := cn.replySeen
|
||||
cn.stateMu.RUnlock()
|
||||
if useReplyDir {
|
||||
state = ConnTrackStateEstablishedReply
|
||||
} else if replySeen {
|
||||
state = ConnTrackStateEstablished
|
||||
} else {
|
||||
state = ConnTrackStateNew
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// FillConnTrackInfo fills connection tracking information for the connection.
|
||||
func (cn *conn) FillConnTrackInfo(opts ConnTrackInfoOpts, info *ConnTrackInfo) bool {
|
||||
state := ConnTrackStateInvalid
|
||||
if opts.FillState {
|
||||
state = cn.getConnTrackState(opts.UseReplyDir)
|
||||
}
|
||||
|
||||
dir := ConnTrackDirectionOriginal
|
||||
t := &cn.original
|
||||
if opts.UseReplyDir {
|
||||
t = &cn.reply
|
||||
dir = ConnTrackDirectionReply
|
||||
}
|
||||
tID := t.tupleID
|
||||
|
||||
pID := uint32(0)
|
||||
if opts.FillPseudoID {
|
||||
// Generate a pseudo-ID similar to Linux nf_ct_get_id
|
||||
pID = tupleHash(cn.original.tupleID, cn.ct.nftIDSeed)
|
||||
}
|
||||
|
||||
var expires time.Duration
|
||||
if opts.FillExpiration {
|
||||
expires = cn.expiresIn()
|
||||
}
|
||||
|
||||
info.State = state
|
||||
info.Direction = dir
|
||||
info.SrcAddr = tID.srcAddr
|
||||
info.DstAddr = tID.dstAddr
|
||||
info.SrcPort = tID.srcPortOrEchoRequestIdent
|
||||
info.DstPort = tID.dstPortOrEchoReplyIdent
|
||||
info.NetProto = tID.netProto
|
||||
info.TransProto = tID.transProto
|
||||
info.Expiration = expires
|
||||
info.PseudoID = pID
|
||||
return true
|
||||
}
|
||||
|
||||
func (bkt *bucket) connForTID(tid tupleID, now tcpip.MonotonicTime) *tuple {
|
||||
bkt.mu.RLock()
|
||||
defer bkt.mu.RUnlock()
|
||||
|
|
@ -697,325 +787,14 @@ func (cn *conn) finalize() bool {
|
|||
}
|
||||
}
|
||||
|
||||
// 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 (cn *conn) maybePerformNoopNAT(pkt *PacketBuffer, hook Hook, r *Route, dnat bool) {
|
||||
cn.mu.Lock()
|
||||
var manip *manipType
|
||||
if dnat {
|
||||
manip = &cn.destinationManip
|
||||
} else {
|
||||
manip = &cn.sourceManip
|
||||
}
|
||||
if *manip != manipNotPerformed {
|
||||
cn.mu.Unlock()
|
||||
_ = cn.handlePacket(pkt, hook, r)
|
||||
return
|
||||
}
|
||||
if dnat {
|
||||
*manip = manipPerformedNoop
|
||||
cn.mu.Unlock()
|
||||
_ = cn.handlePacket(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 */)
|
||||
}
|
||||
|
||||
type portOrIdentRange struct {
|
||||
start uint16
|
||||
size uint32
|
||||
}
|
||||
|
||||
// performNAT 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.
|
||||
func (cn *conn) performNAT(pkt *PacketBuffer, hook Hook, r *Route, portsOrIdents portOrIdentRange, natAddress tcpip.Address, dnat, changePort, changeAddress bool) {
|
||||
lastPortOrIdent := func() uint16 {
|
||||
lastPortOrIdent := uint32(portsOrIdents.start) + portsOrIdents.size - 1
|
||||
if lastPortOrIdent > math.MaxUint16 {
|
||||
panic(fmt.Sprintf("got lastPortOrIdent = %d, want <= MaxUint16(=%d); portsOrIdents=%#v", lastPortOrIdent, math.MaxUint16, portsOrIdents))
|
||||
}
|
||||
return uint16(lastPortOrIdent)
|
||||
}()
|
||||
|
||||
// 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.
|
||||
_ = cn.handlePacket(pkt, hook, r)
|
||||
}()
|
||||
|
||||
cn.mu.Lock()
|
||||
defer cn.mu.Unlock()
|
||||
|
||||
var manip *manipType
|
||||
var address *tcpip.Address
|
||||
var portOrIdent *uint16
|
||||
if 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
|
||||
}
|
||||
*manip = manipPerformed
|
||||
if changeAddress {
|
||||
*address = natAddress
|
||||
}
|
||||
|
||||
// Everything below here is port-fiddling.
|
||||
if !changePort {
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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.rand.Uint32()
|
||||
|
||||
for i := uint32(0); i < maxAttempts; i++ {
|
||||
newPortOrIdentU32 := uint32(portsOrIdents.start) + (randOffset+i)%portsOrIdents.size
|
||||
if newPortOrIdentU32 > math.MaxUint16 {
|
||||
panic(fmt.Sprintf("got newPortOrIdentU32 = %d, want <= MaxUint16(=%d); portsOrIdents=%#v, randOffset=%d", newPortOrIdentU32, math.MaxUint16, portsOrIdents, randOffset))
|
||||
}
|
||||
|
||||
*portOrIdent = uint16(newPortOrIdentU32)
|
||||
|
||||
if other := cn.ct.connForTID(cn.reply.tupleID); other == nil {
|
||||
// We found a unique tuple!
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if maxAttempts == portsOrIdents.size {
|
||||
// We already tried all the ports/idents in the range so no need to keep
|
||||
// trying.
|
||||
return
|
||||
}
|
||||
|
||||
if maxAttempts < minAttemptsToContinue {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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).
|
||||
}
|
||||
|
||||
// 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 (cn *conn) handlePacket(pkt *PacketBuffer, hook Hook, rt *Route) bool {
|
||||
netHdr, transHdr, isICMPError, ok := getHeaders(pkt)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
fullChecksum := false
|
||||
updatePseudoHeader := false
|
||||
natDone := &pkt.snatDone
|
||||
dnat := false
|
||||
switch hook {
|
||||
case Prerouting:
|
||||
// Packet came from outside the stack so it must have a checksum set
|
||||
// already.
|
||||
fullChecksum = true
|
||||
updatePseudoHeader = true
|
||||
|
||||
natDone = &pkt.dnatDone
|
||||
dnat = true
|
||||
case Input:
|
||||
case Forward:
|
||||
panic("should not handle packet in the forwarding hook")
|
||||
case Output:
|
||||
natDone = &pkt.dnatDone
|
||||
dnat = true
|
||||
fallthrough
|
||||
case Postrouting:
|
||||
if pkt.TransportProtocolNumber == header.TCPProtocolNumber && pkt.GSOOptions.Type != GSONone && pkt.GSOOptions.NeedsCsum {
|
||||
updatePseudoHeader = true
|
||||
} else if rt.RequiresTXTransportChecksum() {
|
||||
fullChecksum = true
|
||||
updatePseudoHeader = true
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized hook = %d", hook))
|
||||
}
|
||||
|
||||
if *natDone {
|
||||
panic(fmt.Sprintf("packet already had NAT(dnat=%t) performed at hook=%s; pkt=%#v", dnat, hook, 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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
rewritePacket(
|
||||
netHdr,
|
||||
transHdr,
|
||||
!dnat != isICMPError,
|
||||
fullChecksum,
|
||||
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
|
||||
}
|
||||
|
||||
// bucket gets the conntrack bucket for a tupleID.
|
||||
// +checklocksread:ct.mu
|
||||
func (ct *ConnTrack) bucket(id tupleID) int {
|
||||
return ct.bucketWithTableLength(id, len(ct.buckets))
|
||||
}
|
||||
|
||||
func (ct *ConnTrack) bucketWithTableLength(id tupleID, tableLength int) int {
|
||||
h := jenkins.Sum32(ct.seed)
|
||||
func tupleHash(id tupleID, seed uint32) uint32 {
|
||||
h := jenkins.Sum32(seed)
|
||||
h.Write(id.srcAddr.AsSlice())
|
||||
h.Write(id.dstAddr.AsSlice())
|
||||
shortBuf := make([]byte, 2)
|
||||
|
|
@ -1027,7 +806,12 @@ func (ct *ConnTrack) bucketWithTableLength(id tupleID, tableLength int) int {
|
|||
h.Write([]byte(shortBuf))
|
||||
binary.LittleEndian.PutUint16(shortBuf, uint16(id.netProto))
|
||||
h.Write([]byte(shortBuf))
|
||||
return int(h.Sum32()) % tableLength
|
||||
return h.Sum32()
|
||||
}
|
||||
|
||||
func (ct *ConnTrack) bucketWithTableLength(id tupleID, tableLength int) int {
|
||||
h := tupleHash(id, ct.seed)
|
||||
return int(h) % tableLength
|
||||
}
|
||||
|
||||
// reapUnused deletes timed out entries from the conntrack map. The rules for
|
||||
|
|
@ -1167,3 +951,53 @@ func (ct *ConnTrack) originalDst(epID TransportEndpointID, netProto tcpip.Networ
|
|||
id := t.conn.original.tupleID
|
||||
return id.dstAddr, id.dstPortOrEchoReplyIdent, nil
|
||||
}
|
||||
|
||||
// NewConnTrack creates and initializes a new ConnTrack object.
|
||||
func NewConnTrack(clock tcpip.Clock, rng connTrackRNG, seed *uint32) *ConnTrack {
|
||||
if seed == nil {
|
||||
r := rng.Uint32()
|
||||
seed = &r
|
||||
}
|
||||
ct := &ConnTrack{
|
||||
clock: clock,
|
||||
rng: rng,
|
||||
seed: *seed,
|
||||
nftIDSeed: rng.Uint32(),
|
||||
}
|
||||
ct.init()
|
||||
return ct
|
||||
}
|
||||
|
||||
// NewConnTrackWithReaper creates and initializes a new ConnTrack and reaper.
|
||||
// Reaper garbage collects unused connections.
|
||||
func NewConnTrackWithReaper(clock tcpip.Clock, rng connTrackRNG, seed *uint32) (*ConnTrack, tcpip.Timer) {
|
||||
ct := NewConnTrack(clock, rng, seed)
|
||||
var reaper tcpip.Timer
|
||||
bucket := 0
|
||||
interval := 1 * time.Second
|
||||
reaper = ct.clock.AfterFunc(interval, func() {
|
||||
bucket, interval = ct.reapUnused(bucket, interval)
|
||||
reaper.Reset(interval)
|
||||
})
|
||||
return ct, reaper
|
||||
}
|
||||
|
||||
// NfConnTrackPriority returns the priority of the conntrack hook.
|
||||
// Check `ipv4/ipv6_conntrack_ops` in nf_conntrack_proto.c.
|
||||
func NfConnTrackPriority(hook NFHook) (int, bool) {
|
||||
switch hook {
|
||||
case NFPrerouting:
|
||||
// NF_IP_PRI_CONNTRACK
|
||||
return -200, true
|
||||
case NFInput:
|
||||
// NF_IP_PRI_CONNTRACK_CONFIRM
|
||||
return math.MaxInt32, true
|
||||
case NFPostrouting:
|
||||
// NF_IP_PRI_CONNTRACK_CONFIRM
|
||||
return math.MaxInt32, true
|
||||
case NFOutput:
|
||||
// NF_IP_PRI_CONNTRACK
|
||||
return -200, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue