Обновление снапшота с 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-паникой, что в полевом крашдампе; с ним зелёный.
1140 lines
32 KiB
Go
1140 lines
32 KiB
Go
// Copyright 2021 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 network provides facilities to support tcpip.Endpoints that operate
|
|
// at the network layer or above.
|
|
package network
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/sagernet/gvisor/pkg/atomicbitops"
|
|
"github.com/sagernet/gvisor/pkg/buffer"
|
|
"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/stack"
|
|
"github.com/sagernet/gvisor/pkg/tcpip/transport"
|
|
"github.com/sagernet/gvisor/pkg/waiter"
|
|
)
|
|
|
|
// Endpoint is a datagram-based endpoint. It only supports sending datagrams to
|
|
// a peer.
|
|
//
|
|
// +stateify savable
|
|
type Endpoint struct {
|
|
// The following fields must only be set once then never changed.
|
|
stack *stack.Stack
|
|
ops *tcpip.SocketOptions
|
|
netProto tcpip.NetworkProtocolNumber
|
|
transProto tcpip.TransportProtocolNumber
|
|
waiterQueue *waiter.Queue
|
|
|
|
mu sync.RWMutex `state:"nosave"`
|
|
// +checklocks:mu
|
|
wasBound bool
|
|
// owner is the owner of transmitted packets.
|
|
//
|
|
// +checklocks:mu
|
|
owner tcpip.PacketOwner
|
|
// +checklocks:mu
|
|
writeShutdown bool
|
|
// +checklocks:mu
|
|
effectiveNetProto tcpip.NetworkProtocolNumber
|
|
// +checklocks:mu
|
|
connectedRoute *stack.Route `state:"nosave"`
|
|
// +checklocks:mu
|
|
multicastMemberships map[multicastMembership]struct{}
|
|
// +checklocks:mu
|
|
ipv4TTL uint8
|
|
// +checklocks:mu
|
|
ipv6HopLimit int16
|
|
// TODO(https://gvisor.dev/issue/6389): Use different fields for IPv4/IPv6.
|
|
// +checklocks:mu
|
|
multicastTTL uint8
|
|
// TODO(https://gvisor.dev/issue/6389): Use different fields for IPv4/IPv6.
|
|
// +checklocks:mu
|
|
multicastAddr tcpip.Address
|
|
// +checklocks:mu
|
|
multicastNICID tcpip.NICID
|
|
// +checklocks:mu
|
|
ipv6MulticastNICID tcpip.NICID
|
|
// +checklocks:mu
|
|
ipv4TOS uint8
|
|
// +checklocks:mu
|
|
ipv6TClass uint8
|
|
// +checklocks:mu
|
|
pmtud tcpip.PMTUDStrategy
|
|
|
|
// Lock ordering: mu > infoMu.
|
|
infoMu sync.RWMutex `state:"nosave"`
|
|
// info has a dedicated mutex so that we can avoid lock ordering violations
|
|
// when reading the endpoint's info. If we used mu, we need to guarantee
|
|
// that any lock taken while mu is held is not held when calling Info()
|
|
// which is not true as of writing (we hold mu while registering transport
|
|
// endpoints (taking the transport demuxer lock but we also hold the demuxer
|
|
// lock when delivering packets/errors to endpoints).
|
|
//
|
|
// Writes must be performed through setInfo.
|
|
//
|
|
// +checklocks:infoMu
|
|
info stack.TransportEndpointInfo
|
|
|
|
// state holds a transport.DatagramBasedEndpointState.
|
|
//
|
|
// state must be accessed with atomics so that we can avoid lock ordering
|
|
// violations when reading the state. If we used mu, we need to guarantee
|
|
// that any lock taken while mu is held is not held when calling State()
|
|
// which is not true as of writing (we hold mu while registering transport
|
|
// endpoints (taking the transport demuxer lock but we also hold the demuxer
|
|
// lock when delivering packets/errors to endpoints).
|
|
//
|
|
// Writes must be performed through setEndpointState.
|
|
state atomicbitops.Uint32
|
|
|
|
// Callers should not attempt to obtain sendBufferSizeInUseMu while holding
|
|
// another lock on Endpoint.
|
|
sendBufferSizeInUseMu sync.RWMutex `state:"nosave"`
|
|
// sendBufferSizeInUse keeps track of the bytes in use by in-flight packets.
|
|
//
|
|
// +checklocks:sendBufferSizeInUseMu
|
|
sendBufferSizeInUse int64 `state:"nosave"`
|
|
}
|
|
|
|
// +stateify savable
|
|
type multicastMembership struct {
|
|
nicID tcpip.NICID
|
|
multicastAddr tcpip.Address
|
|
}
|
|
|
|
// Init initializes the endpoint.
|
|
func (e *Endpoint) Init(s *stack.Stack, netProto tcpip.NetworkProtocolNumber, transProto tcpip.TransportProtocolNumber, ops *tcpip.SocketOptions, waiterQueue *waiter.Queue) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
if e.multicastMemberships != nil {
|
|
panic(fmt.Sprintf("endpoint is already initialized; got e.multicastMemberships = %#v, want = nil", e.multicastMemberships))
|
|
}
|
|
|
|
switch netProto {
|
|
case header.IPv4ProtocolNumber, header.IPv6ProtocolNumber:
|
|
default:
|
|
panic(fmt.Sprintf("invalid protocol number = %d", netProto))
|
|
}
|
|
|
|
e.stack = s
|
|
e.ops = ops
|
|
e.netProto = netProto
|
|
e.transProto = transProto
|
|
e.waiterQueue = waiterQueue
|
|
e.infoMu.Lock()
|
|
e.info = stack.TransportEndpointInfo{
|
|
NetProto: netProto,
|
|
TransProto: transProto,
|
|
}
|
|
e.infoMu.Unlock()
|
|
e.effectiveNetProto = netProto
|
|
e.ipv4TTL = tcpip.UseDefaultIPv4TTL
|
|
e.ipv6HopLimit = tcpip.UseDefaultIPv6HopLimit
|
|
|
|
// Linux defaults to TTL=1.
|
|
e.multicastTTL = 1
|
|
e.multicastMemberships = make(map[multicastMembership]struct{})
|
|
e.setEndpointState(transport.DatagramEndpointStateInitial)
|
|
}
|
|
|
|
// NetProto returns the network protocol the endpoint was initialized with.
|
|
func (e *Endpoint) NetProto() tcpip.NetworkProtocolNumber {
|
|
return e.netProto
|
|
}
|
|
|
|
// setEndpointState sets the state of the endpoint.
|
|
//
|
|
// e.mu must be held to synchronize changes to state with the rest of the
|
|
// endpoint.
|
|
//
|
|
// +checklocks:e.mu
|
|
func (e *Endpoint) setEndpointState(state transport.DatagramEndpointState) {
|
|
e.state.Store(uint32(state))
|
|
}
|
|
|
|
// State returns the state of the endpoint.
|
|
func (e *Endpoint) State() transport.DatagramEndpointState {
|
|
return transport.DatagramEndpointState(e.state.Load())
|
|
}
|
|
|
|
// Close cleans the endpoint's resources and leaves the endpoint in a closed
|
|
// state.
|
|
func (e *Endpoint) Close() {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if e.State() == transport.DatagramEndpointStateClosed {
|
|
return
|
|
}
|
|
|
|
for mem := range e.multicastMemberships {
|
|
proto, err := e.multicastNetProto(mem.multicastAddr)
|
|
if err != nil {
|
|
panic("non multicast address in an existing membership")
|
|
}
|
|
e.stack.LeaveGroup(proto, mem.nicID, mem.multicastAddr)
|
|
}
|
|
e.multicastMemberships = nil
|
|
|
|
if e.connectedRoute != nil {
|
|
e.connectedRoute.Release()
|
|
e.connectedRoute = nil
|
|
}
|
|
|
|
e.setEndpointState(transport.DatagramEndpointStateClosed)
|
|
}
|
|
|
|
// SetOwner sets the owner of transmitted packets.
|
|
func (e *Endpoint) SetOwner(owner tcpip.PacketOwner) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
e.owner = owner
|
|
}
|
|
|
|
// +checklocksread:e.mu
|
|
func (e *Endpoint) calculateTTL(route *stack.Route) uint8 {
|
|
remoteAddress := route.RemoteAddress()
|
|
if header.IsV4MulticastAddress(remoteAddress) || header.IsV6MulticastAddress(remoteAddress) {
|
|
return e.multicastTTL
|
|
}
|
|
|
|
switch netProto := route.NetProto(); netProto {
|
|
case header.IPv4ProtocolNumber:
|
|
if e.ipv4TTL == 0 {
|
|
return route.DefaultTTL()
|
|
}
|
|
return e.ipv4TTL
|
|
case header.IPv6ProtocolNumber:
|
|
if e.ipv6HopLimit == -1 {
|
|
return route.DefaultTTL()
|
|
}
|
|
return uint8(e.ipv6HopLimit)
|
|
default:
|
|
panic(fmt.Sprintf("invalid protocol number = %d", netProto))
|
|
}
|
|
}
|
|
|
|
// WriteContext holds the context for a write.
|
|
type WriteContext struct {
|
|
e *Endpoint
|
|
route *stack.Route
|
|
ttl uint8
|
|
tos uint8
|
|
df bool
|
|
}
|
|
|
|
func (c *WriteContext) MTU() uint32 {
|
|
return c.route.MTU()
|
|
}
|
|
|
|
// Release releases held resources.
|
|
func (c *WriteContext) Release() {
|
|
c.route.Release()
|
|
*c = WriteContext{}
|
|
}
|
|
|
|
// WritePacketInfo is the properties of a packet that may be written.
|
|
type WritePacketInfo struct {
|
|
NetProto tcpip.NetworkProtocolNumber
|
|
LocalAddress, RemoteAddress tcpip.Address
|
|
MaxHeaderLength uint16
|
|
RequiresTXTransportChecksum bool
|
|
}
|
|
|
|
// PacketInfo returns the properties of a packet that will be written.
|
|
func (c *WriteContext) PacketInfo() WritePacketInfo {
|
|
return WritePacketInfo{
|
|
NetProto: c.route.NetProto(),
|
|
LocalAddress: c.route.LocalAddress(),
|
|
RemoteAddress: c.route.RemoteAddress(),
|
|
MaxHeaderLength: c.route.MaxHeaderLength(),
|
|
RequiresTXTransportChecksum: c.route.RequiresTXTransportChecksum(),
|
|
}
|
|
}
|
|
|
|
// TryNewPacketBuffer returns a new packet buffer iff the endpoint's send buffer
|
|
// is not full.
|
|
//
|
|
// If this method returns nil, the caller should wait for the endpoint to become
|
|
// writable.
|
|
func (c *WriteContext) TryNewPacketBuffer(reserveHdrBytes int, data buffer.Buffer) *stack.PacketBuffer {
|
|
e := c.e
|
|
|
|
e.sendBufferSizeInUseMu.Lock()
|
|
defer e.sendBufferSizeInUseMu.Unlock()
|
|
|
|
if !e.hasSendSpaceRLocked() {
|
|
return nil
|
|
}
|
|
|
|
mark := e.ops.GetMark()
|
|
return c.newPacketBufferLocked(reserveHdrBytes, data, mark)
|
|
}
|
|
|
|
// TryNewPacketBufferFromPayloader returns a new packet buffer if the endpoint's send buffer
|
|
// is not full. Otherwise, data from `payloader` isn't read.
|
|
func (c *WriteContext) TryNewPacketBufferFromPayloader(reserveHdrBytes int, payloader tcpip.Payloader) (*stack.PacketBuffer, tcpip.Error) {
|
|
e := c.e
|
|
|
|
e.sendBufferSizeInUseMu.Lock()
|
|
defer e.sendBufferSizeInUseMu.Unlock()
|
|
|
|
if !e.hasSendSpaceRLocked() {
|
|
return nil, &tcpip.ErrWouldBlock{}
|
|
}
|
|
var data buffer.Buffer
|
|
if _, err := data.WriteFromReader(payloader, int64(payloader.Len())); err != nil {
|
|
data.Release()
|
|
return nil, &tcpip.ErrBadBuffer{}
|
|
}
|
|
mark := e.ops.GetMark()
|
|
return c.newPacketBufferLocked(reserveHdrBytes, data, mark), nil
|
|
}
|
|
|
|
// +checklocks:c.e.sendBufferSizeInUseMu
|
|
func (c *WriteContext) newPacketBufferLocked(reserveHdrBytes int, data buffer.Buffer, mark uint32) *stack.PacketBuffer {
|
|
e := c.e
|
|
// Note that we allow oversubscription - if there is any space at all in the
|
|
// send buffer, we accept the full packet which may be larger than the space
|
|
// available. This is because if the endpoint reports that it is writable,
|
|
// a write operation should succeed.
|
|
//
|
|
// This matches Linux behaviour:
|
|
// https://github.com/torvalds/linux/blob/38d741cb70b/include/net/sock.h#L2519
|
|
// https://github.com/torvalds/linux/blob/38d741cb70b/net/core/sock.c#L2588
|
|
var expOptVal uint16
|
|
if nic, err := c.e.stack.GetNICByID(c.route.OutgoingNIC()); err == nil && nic.GetExperimentIPOptionEnabled() {
|
|
expOptVal = c.e.ops.GetExperimentOptionValue()
|
|
}
|
|
if c.route.NetProto() == header.IPv6ProtocolNumber && expOptVal != 0 {
|
|
reserveHdrBytes += header.IPv6ExperimentHdrLength
|
|
}
|
|
pktSize := int64(reserveHdrBytes) + int64(data.Size())
|
|
e.sendBufferSizeInUse += pktSize
|
|
|
|
return stack.NewPacketBuffer(stack.PacketBufferOptions{
|
|
ReserveHeaderBytes: reserveHdrBytes,
|
|
Payload: data,
|
|
Mark: mark,
|
|
OnRelease: func() {
|
|
e.sendBufferSizeInUseMu.Lock()
|
|
if got := e.sendBufferSizeInUse; got < pktSize {
|
|
e.sendBufferSizeInUseMu.Unlock()
|
|
panic(fmt.Sprintf("e.sendBufferSizeInUse=(%d) < pktSize(=%d)", got, pktSize))
|
|
}
|
|
e.sendBufferSizeInUse -= pktSize
|
|
signal := e.hasSendSpaceRLocked()
|
|
e.sendBufferSizeInUseMu.Unlock()
|
|
|
|
// Let waiters know if we now have space in the send buffer.
|
|
if signal {
|
|
e.waiterQueue.Notify(waiter.WritableEvents)
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
// WritePacket attempts to write the packet.
|
|
func (c *WriteContext) WritePacket(pkt *stack.PacketBuffer, headerIncluded bool) tcpip.Error {
|
|
c.e.mu.RLock()
|
|
pkt.Owner = c.e.owner
|
|
c.e.mu.RUnlock()
|
|
|
|
if headerIncluded {
|
|
return c.route.WriteHeaderIncludedPacket(pkt)
|
|
}
|
|
|
|
var expOptVal uint16
|
|
if nic, err := c.e.stack.GetNICByID(c.route.OutgoingNIC()); err == nil && nic.GetExperimentIPOptionEnabled() {
|
|
expOptVal = c.e.ops.GetExperimentOptionValue()
|
|
}
|
|
|
|
err := c.route.WritePacket(stack.NetworkHeaderParams{
|
|
Protocol: c.e.transProto,
|
|
TTL: c.ttl,
|
|
TOS: c.tos,
|
|
DF: c.df,
|
|
ExperimentOptionValue: expOptVal,
|
|
}, pkt)
|
|
|
|
if _, ok := err.(*tcpip.ErrNoBufferSpace); ok {
|
|
var recvErr bool
|
|
switch netProto := c.route.NetProto(); netProto {
|
|
case header.IPv4ProtocolNumber:
|
|
recvErr = c.e.ops.GetIPv4RecvError()
|
|
case header.IPv6ProtocolNumber:
|
|
recvErr = c.e.ops.GetIPv6RecvError()
|
|
default:
|
|
panic(fmt.Sprintf("unhandled network protocol number = %d", netProto))
|
|
}
|
|
|
|
// Linux only returns ENOBUFS to the caller if IP{,V6}_RECVERR is set.
|
|
//
|
|
// https://github.com/torvalds/linux/blob/3e71713c9e75c/net/ipv4/udp.c#L969
|
|
// https://github.com/torvalds/linux/blob/3e71713c9e75c/net/ipv6/udp.c#L1260
|
|
if !recvErr {
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// MaybeSignalWritable signals waiters with writable events if the send buffer
|
|
// has space.
|
|
func (e *Endpoint) MaybeSignalWritable() {
|
|
e.sendBufferSizeInUseMu.RLock()
|
|
signal := e.hasSendSpaceRLocked()
|
|
e.sendBufferSizeInUseMu.RUnlock()
|
|
|
|
if signal {
|
|
e.waiterQueue.Notify(waiter.WritableEvents)
|
|
}
|
|
}
|
|
|
|
// HasSendSpace returns whether or not the send buffer has space.
|
|
func (e *Endpoint) HasSendSpace() bool {
|
|
e.sendBufferSizeInUseMu.RLock()
|
|
defer e.sendBufferSizeInUseMu.RUnlock()
|
|
return e.hasSendSpaceRLocked()
|
|
}
|
|
|
|
// +checklocksread:e.sendBufferSizeInUseMu
|
|
func (e *Endpoint) hasSendSpaceRLocked() bool {
|
|
return e.ops.GetSendBufferSize() > e.sendBufferSizeInUse
|
|
}
|
|
|
|
// AcquireContextForWrite acquires a WriteContext.
|
|
func (e *Endpoint) AcquireContextForWrite(opts tcpip.WriteOptions) (WriteContext, tcpip.Error) {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
// MSG_MORE is unimplemented. This also means that MSG_EOR is a no-op.
|
|
if opts.More {
|
|
return WriteContext{}, &tcpip.ErrInvalidOptionValue{}
|
|
}
|
|
|
|
if e.State() == transport.DatagramEndpointStateClosed {
|
|
return WriteContext{}, &tcpip.ErrInvalidEndpointState{}
|
|
}
|
|
|
|
if e.writeShutdown {
|
|
return WriteContext{}, &tcpip.ErrClosedForSend{}
|
|
}
|
|
|
|
ipv6PktInfoValid := e.effectiveNetProto == header.IPv6ProtocolNumber && opts.ControlMessages.HasIPv6PacketInfo
|
|
|
|
route := e.connectedRoute
|
|
to := opts.To
|
|
info := e.Info()
|
|
switch to {
|
|
case nil:
|
|
// If the user doesn't specify a destination, they should have
|
|
// connected to another address.
|
|
if e.State() != transport.DatagramEndpointStateConnected {
|
|
return WriteContext{}, &tcpip.ErrDestinationRequired{}
|
|
}
|
|
|
|
if !ipv6PktInfoValid {
|
|
route.Acquire()
|
|
break
|
|
}
|
|
|
|
// We are connected and the caller did not specify the destination but
|
|
// we have an IPv6 packet info structure which may change our local
|
|
// interface/address used to send the packet so we need to construct
|
|
// a new route instead of using the connected route.
|
|
//
|
|
// Construct a destination matching the remote the endpoint is connected
|
|
// to.
|
|
to = &tcpip.FullAddress{
|
|
// RegisterNICID is set when the endpoint is connected. It is usually
|
|
// only set for link-local addresses or multicast addresses if the
|
|
// multicast interface was specified (see e.multicastNICID,
|
|
// e.connectRouteRLocked and e.ConnectAndThen).
|
|
NIC: info.RegisterNICID,
|
|
Addr: info.ID.RemoteAddress,
|
|
}
|
|
fallthrough
|
|
default:
|
|
// Reject destination address if it goes through a different
|
|
// NIC than the endpoint was bound to.
|
|
nicID := to.NIC
|
|
if nicID == 0 {
|
|
nicID = tcpip.NICID(e.ops.GetBindToDevice())
|
|
}
|
|
|
|
var localAddr tcpip.Address
|
|
if ipv6PktInfoValid {
|
|
// Uphold strong-host semantics since (as of writing) the stack follows
|
|
// the strong host model.
|
|
|
|
pktInfoNICID := opts.ControlMessages.IPv6PacketInfo.NIC
|
|
pktInfoAddr := opts.ControlMessages.IPv6PacketInfo.Addr
|
|
|
|
if pktInfoNICID != 0 {
|
|
// If we are bound to an interface or specified the destination
|
|
// interface (usually when using link-local addresses), make sure the
|
|
// interface matches the specified local interface.
|
|
if nicID != 0 && nicID != pktInfoNICID {
|
|
return WriteContext{}, &tcpip.ErrHostUnreachable{}
|
|
}
|
|
|
|
// If a local address is not specified, then we need to make sure the
|
|
// bound address belongs to the specified local interface.
|
|
if pktInfoAddr.BitLen() == 0 {
|
|
// If the bound interface is different from the specified local
|
|
// interface, the bound address obviously does not belong to the
|
|
// specified local interface.
|
|
//
|
|
// The bound interface is usually only set for link-local addresses.
|
|
if info.BindNICID != 0 && info.BindNICID != pktInfoNICID {
|
|
return WriteContext{}, &tcpip.ErrHostUnreachable{}
|
|
}
|
|
if info.ID.LocalAddress.BitLen() != 0 && e.stack.CheckLocalAddress(pktInfoNICID, header.IPv6ProtocolNumber, info.ID.LocalAddress) == 0 {
|
|
return WriteContext{}, &tcpip.ErrBadLocalAddress{}
|
|
}
|
|
}
|
|
|
|
nicID = pktInfoNICID
|
|
}
|
|
|
|
if pktInfoAddr.BitLen() != 0 {
|
|
// The local address must belong to the stack. If an outgoing interface
|
|
// is specified as a result of binding the endpoint to a device, or
|
|
// specifying the outgoing interface in the destination address/pkt info
|
|
// structure, the address must belong to that interface.
|
|
if e.stack.CheckLocalAddress(nicID, header.IPv6ProtocolNumber, pktInfoAddr) == 0 {
|
|
return WriteContext{}, &tcpip.ErrBadLocalAddress{}
|
|
}
|
|
|
|
localAddr = pktInfoAddr
|
|
}
|
|
} else {
|
|
if info.BindNICID != 0 {
|
|
if nicID != 0 && nicID != info.BindNICID {
|
|
return WriteContext{}, &tcpip.ErrHostUnreachable{}
|
|
}
|
|
|
|
nicID = info.BindNICID
|
|
}
|
|
if nicID == 0 {
|
|
nicID = info.RegisterNICID
|
|
}
|
|
}
|
|
|
|
dst, netProto, err := e.checkV4Mapped(*to, false /* bind */)
|
|
if err != nil {
|
|
return WriteContext{}, err
|
|
}
|
|
|
|
route, _, err = e.connectRouteRLocked(nicID, localAddr, dst, netProto)
|
|
if err != nil {
|
|
return WriteContext{}, err
|
|
}
|
|
}
|
|
|
|
if !e.ops.GetBroadcast() && route.IsOutboundBroadcast() {
|
|
route.Release()
|
|
return WriteContext{}, &tcpip.ErrBroadcastDisabled{}
|
|
}
|
|
|
|
var tos uint8
|
|
var ttl uint8
|
|
switch netProto := route.NetProto(); netProto {
|
|
case header.IPv4ProtocolNumber:
|
|
tos = e.ipv4TOS
|
|
if opts.ControlMessages.HasTTL {
|
|
ttl = opts.ControlMessages.TTL
|
|
} else {
|
|
ttl = e.calculateTTL(route)
|
|
}
|
|
case header.IPv6ProtocolNumber:
|
|
tos = e.ipv6TClass
|
|
if opts.ControlMessages.HasHopLimit {
|
|
ttl = opts.ControlMessages.HopLimit
|
|
} else {
|
|
ttl = e.calculateTTL(route)
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("invalid protocol number = %d", netProto))
|
|
}
|
|
|
|
// Set the DF (Don't Fragment) bit based on the PMTUD strategy,
|
|
// matching TCP behavior in connect.go.
|
|
// Note: In gVisor, WANT and DO are treated identically (both set DF).
|
|
// Linux kernel differentiates them (WANT allows local fragmentation,
|
|
// DO returns EMSGSIZE), but gVisor's IPv4 layer always allows local
|
|
// fragmentation for locally-generated packets regardless of DF
|
|
// (see gvisor.dev/issue/5919).
|
|
//
|
|
// PROBE also sets DF, matching Linux ip_dont_fragment(). In Linux,
|
|
// PROBE differs from DO only in that it ignores incoming ICMP
|
|
// "Fragmentation Needed" messages (i.e. does not update the cached
|
|
// route PMTU). Since gVisor does not implement ICMP-based PMTU
|
|
// feedback for transport sockets, PROBE and DO are functionally
|
|
// equivalent here.
|
|
df := e.pmtud == tcpip.PMTUDiscoveryWant || e.pmtud == tcpip.PMTUDiscoveryDo || e.pmtud == tcpip.PMTUDiscoveryProbe
|
|
|
|
return WriteContext{
|
|
e: e,
|
|
route: route,
|
|
ttl: ttl,
|
|
tos: tos,
|
|
df: df,
|
|
}, nil
|
|
}
|
|
|
|
// Disconnect disconnects the endpoint from its peer.
|
|
func (e *Endpoint) Disconnect() {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if e.State() != transport.DatagramEndpointStateConnected {
|
|
return
|
|
}
|
|
|
|
info := e.Info()
|
|
// Exclude ephemerally bound endpoints.
|
|
if e.wasBound {
|
|
info.ID = stack.TransportEndpointID{
|
|
LocalAddress: info.BindAddr,
|
|
}
|
|
e.setEndpointState(transport.DatagramEndpointStateBound)
|
|
} else {
|
|
info.ID = stack.TransportEndpointID{}
|
|
e.setEndpointState(transport.DatagramEndpointStateInitial)
|
|
}
|
|
e.setInfo(info)
|
|
|
|
e.connectedRoute.Release()
|
|
e.connectedRoute = nil
|
|
}
|
|
|
|
// connectRouteRLocked establishes a route to the specified interface or the
|
|
// configured multicast interface if no interface is specified and the
|
|
// specified address is a multicast address.
|
|
//
|
|
// +checklocksread:e.mu
|
|
func (e *Endpoint) connectRouteRLocked(nicID tcpip.NICID, localAddr tcpip.Address, addr tcpip.FullAddress, netProto tcpip.NetworkProtocolNumber) (*stack.Route, tcpip.NICID, tcpip.Error) {
|
|
if localAddr.BitLen() == 0 {
|
|
localAddr = e.Info().ID.LocalAddress
|
|
if e.isBroadcastOrMulticast(nicID, netProto, localAddr) {
|
|
// A packet can only originate from a unicast address (i.e., an interface).
|
|
localAddr = tcpip.Address{}
|
|
}
|
|
|
|
if header.IsV4MulticastAddress(addr.Addr) {
|
|
if nicID == 0 {
|
|
nicID = e.multicastNICID
|
|
}
|
|
if localAddr == (tcpip.Address{}) && nicID == 0 {
|
|
localAddr = e.multicastAddr
|
|
}
|
|
}
|
|
if header.IsV6MulticastAddress(addr.Addr) && nicID == 0 {
|
|
nicID = e.ipv6MulticastNICID
|
|
}
|
|
}
|
|
|
|
// Find a route to the desired destination.
|
|
r, err := e.stack.FindRoute(nicID, localAddr, addr.Addr, netProto, e.ops.GetMulticastLoop())
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return r, nicID, nil
|
|
}
|
|
|
|
// Connect connects the endpoint to the address.
|
|
func (e *Endpoint) Connect(addr tcpip.FullAddress) tcpip.Error {
|
|
return e.ConnectAndThen(addr, func(_ tcpip.NetworkProtocolNumber, _, _ stack.TransportEndpointID) tcpip.Error {
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// ConnectAndThen connects the endpoint to the address and then calls the
|
|
// provided function.
|
|
//
|
|
// If the function returns an error, the endpoint's state does not change. The
|
|
// function will be called with the network protocol used to connect to the peer
|
|
// and the source and destination addresses that will be used to send traffic to
|
|
// the peer.
|
|
func (e *Endpoint) ConnectAndThen(addr tcpip.FullAddress, f func(netProto tcpip.NetworkProtocolNumber, previousID, nextID stack.TransportEndpointID) tcpip.Error) tcpip.Error {
|
|
addr.Port = 0
|
|
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
info := e.Info()
|
|
nicID := addr.NIC
|
|
switch e.State() {
|
|
case transport.DatagramEndpointStateInitial:
|
|
case transport.DatagramEndpointStateBound, transport.DatagramEndpointStateConnected:
|
|
if info.BindNICID == 0 {
|
|
break
|
|
}
|
|
|
|
if nicID != 0 && nicID != info.BindNICID {
|
|
return &tcpip.ErrInvalidEndpointState{}
|
|
}
|
|
|
|
nicID = info.BindNICID
|
|
default:
|
|
return &tcpip.ErrInvalidEndpointState{}
|
|
}
|
|
|
|
addr, netProto, err := e.checkV4Mapped(addr, false /* bind */)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r, nicID, err := e.connectRouteRLocked(nicID, tcpip.Address{}, addr, netProto)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
id := stack.TransportEndpointID{
|
|
LocalAddress: info.ID.LocalAddress,
|
|
RemoteAddress: r.RemoteAddress(),
|
|
}
|
|
if e.State() == transport.DatagramEndpointStateInitial {
|
|
id.LocalAddress = r.LocalAddress()
|
|
}
|
|
|
|
if err := f(r.NetProto(), info.ID, id); err != nil {
|
|
r.Release()
|
|
return err
|
|
}
|
|
|
|
if e.connectedRoute != nil {
|
|
// If the endpoint was previously connected then release any previous route.
|
|
e.connectedRoute.Release()
|
|
}
|
|
e.connectedRoute = r
|
|
info.ID = id
|
|
info.RegisterNICID = nicID
|
|
e.setInfo(info)
|
|
e.effectiveNetProto = netProto
|
|
e.setEndpointState(transport.DatagramEndpointStateConnected)
|
|
return nil
|
|
}
|
|
|
|
// Shutdown shutsdown the endpoint.
|
|
func (e *Endpoint) Shutdown() tcpip.Error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
switch state := e.State(); state {
|
|
case transport.DatagramEndpointStateInitial, transport.DatagramEndpointStateClosed:
|
|
return &tcpip.ErrNotConnected{}
|
|
case transport.DatagramEndpointStateBound, transport.DatagramEndpointStateConnected:
|
|
e.writeShutdown = true
|
|
return nil
|
|
default:
|
|
panic(fmt.Sprintf("unhandled state = %s", state))
|
|
}
|
|
}
|
|
|
|
// checkV4MappedRLocked determines the effective network protocol and converts
|
|
// addr to its canonical form.
|
|
func (e *Endpoint) checkV4Mapped(addr tcpip.FullAddress, bind bool) (tcpip.FullAddress, tcpip.NetworkProtocolNumber, tcpip.Error) {
|
|
info := e.Info()
|
|
unwrapped, netProto, err := info.AddrNetProtoLocked(addr, e.ops.GetV6Only(), bind)
|
|
if err != nil {
|
|
return tcpip.FullAddress{}, 0, err
|
|
}
|
|
return unwrapped, netProto, nil
|
|
}
|
|
|
|
func (e *Endpoint) isBroadcastOrMulticast(nicID tcpip.NICID, netProto tcpip.NetworkProtocolNumber, addr tcpip.Address) bool {
|
|
return addr == header.IPv4Broadcast || header.IsV4MulticastAddress(addr) || header.IsV6MulticastAddress(addr) || e.stack.IsSubnetBroadcast(nicID, netProto, addr)
|
|
}
|
|
|
|
// Bind binds the endpoint to the address.
|
|
func (e *Endpoint) Bind(addr tcpip.FullAddress) tcpip.Error {
|
|
return e.BindAndThen(addr, func(tcpip.NetworkProtocolNumber, tcpip.Address) tcpip.Error {
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// BindAndThen binds the endpoint to the address and then calls the provided
|
|
// function.
|
|
//
|
|
// If the function returns an error, the endpoint's state does not change. The
|
|
// function will be called with the bound network protocol and address.
|
|
func (e *Endpoint) BindAndThen(addr tcpip.FullAddress, f func(tcpip.NetworkProtocolNumber, tcpip.Address) tcpip.Error) tcpip.Error {
|
|
addr.Port = 0
|
|
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
// Don't allow binding once endpoint is not in the initial state
|
|
// anymore.
|
|
if e.State() != transport.DatagramEndpointStateInitial {
|
|
return &tcpip.ErrInvalidEndpointState{}
|
|
}
|
|
|
|
addr, netProto, err := e.checkV4Mapped(addr, true /* bind */)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nicID := addr.NIC
|
|
if addr.Addr.BitLen() != 0 && !e.isBroadcastOrMulticast(addr.NIC, netProto, addr.Addr) {
|
|
nicID = e.stack.CheckLocalAddress(nicID, netProto, addr.Addr)
|
|
if nicID == 0 {
|
|
return &tcpip.ErrBadLocalAddress{}
|
|
}
|
|
}
|
|
|
|
if err := f(netProto, addr.Addr); err != nil {
|
|
return err
|
|
}
|
|
|
|
e.wasBound = true
|
|
|
|
info := e.Info()
|
|
info.ID = stack.TransportEndpointID{
|
|
LocalAddress: addr.Addr,
|
|
}
|
|
info.BindNICID = addr.NIC
|
|
info.RegisterNICID = nicID
|
|
info.BindAddr = addr.Addr
|
|
e.setInfo(info)
|
|
e.effectiveNetProto = netProto
|
|
e.setEndpointState(transport.DatagramEndpointStateBound)
|
|
return nil
|
|
}
|
|
|
|
// WasBound returns true iff the endpoint was ever bound.
|
|
func (e *Endpoint) WasBound() bool {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
return e.wasBound
|
|
}
|
|
|
|
// GetLocalAddress returns the address that the endpoint is bound to.
|
|
func (e *Endpoint) GetLocalAddress() tcpip.FullAddress {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
info := e.Info()
|
|
addr := info.BindAddr
|
|
if e.State() == transport.DatagramEndpointStateConnected {
|
|
addr = e.connectedRoute.LocalAddress()
|
|
}
|
|
|
|
return tcpip.FullAddress{
|
|
NIC: info.RegisterNICID,
|
|
Addr: addr,
|
|
}
|
|
}
|
|
|
|
// GetRemoteAddress returns the address that the endpoint is connected to.
|
|
func (e *Endpoint) GetRemoteAddress() (tcpip.FullAddress, bool) {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
if e.State() != transport.DatagramEndpointStateConnected {
|
|
return tcpip.FullAddress{}, false
|
|
}
|
|
|
|
return tcpip.FullAddress{
|
|
Addr: e.connectedRoute.RemoteAddress(),
|
|
NIC: e.Info().RegisterNICID,
|
|
}, true
|
|
}
|
|
|
|
// SetSockOptInt sets the socket option.
|
|
func (e *Endpoint) SetSockOptInt(opt tcpip.SockOptInt, v int) tcpip.Error {
|
|
switch opt {
|
|
case tcpip.MTUDiscoverOption:
|
|
// Store PMTU discovery settings. The DF bit on outgoing
|
|
// packets is set accordingly in AcquireContextForWrite.
|
|
// PROBE is accepted alongside DO/WANT/DONT. In Linux,
|
|
// PROBE sets DF but ignores ICMP-based PMTU updates;
|
|
// since gVisor lacks ICMP PMTU feedback, it behaves
|
|
// identically to DO.
|
|
switch tcpip.PMTUDStrategy(v) {
|
|
case tcpip.PMTUDiscoveryWant, tcpip.PMTUDiscoveryDont, tcpip.PMTUDiscoveryDo, tcpip.PMTUDiscoveryProbe:
|
|
e.mu.Lock()
|
|
e.pmtud = tcpip.PMTUDStrategy(v)
|
|
e.mu.Unlock()
|
|
default:
|
|
return &tcpip.ErrNotSupported{}
|
|
}
|
|
|
|
case tcpip.MulticastTTLOption:
|
|
e.mu.Lock()
|
|
e.multicastTTL = uint8(v)
|
|
e.mu.Unlock()
|
|
|
|
case tcpip.IPv4TTLOption:
|
|
e.mu.Lock()
|
|
e.ipv4TTL = uint8(v)
|
|
e.mu.Unlock()
|
|
|
|
case tcpip.IPv6HopLimitOption:
|
|
e.mu.Lock()
|
|
e.ipv6HopLimit = int16(v)
|
|
e.mu.Unlock()
|
|
|
|
case tcpip.IPv4TOSOption:
|
|
e.mu.Lock()
|
|
e.ipv4TOS = uint8(v)
|
|
e.mu.Unlock()
|
|
|
|
case tcpip.IPv6TrafficClassOption:
|
|
e.mu.Lock()
|
|
e.ipv6TClass = uint8(v)
|
|
e.mu.Unlock()
|
|
|
|
case tcpip.IPv6MulticastInterfaceOption:
|
|
if v != 0 && !e.stack.CheckNIC(tcpip.NICID(v)) {
|
|
return &tcpip.ErrUnknownNICID{}
|
|
}
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
nic := tcpip.NICID(v)
|
|
if info := e.Info(); info.BindNICID != 0 && info.BindNICID != nic {
|
|
return &tcpip.ErrInvalidEndpointState{}
|
|
}
|
|
e.ipv6MulticastNICID = nic
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetSockOptInt returns the socket option.
|
|
func (e *Endpoint) GetSockOptInt(opt tcpip.SockOptInt) (int, tcpip.Error) {
|
|
switch opt {
|
|
case tcpip.MTUDiscoverOption:
|
|
e.mu.Lock()
|
|
v := int(e.pmtud)
|
|
e.mu.Unlock()
|
|
return v, nil
|
|
|
|
case tcpip.MulticastTTLOption:
|
|
e.mu.Lock()
|
|
v := int(e.multicastTTL)
|
|
e.mu.Unlock()
|
|
return v, nil
|
|
|
|
case tcpip.IPv4TTLOption:
|
|
e.mu.Lock()
|
|
v := int(e.ipv4TTL)
|
|
e.mu.Unlock()
|
|
return v, nil
|
|
|
|
case tcpip.IPv6HopLimitOption:
|
|
e.mu.Lock()
|
|
v := int(e.ipv6HopLimit)
|
|
e.mu.Unlock()
|
|
return v, nil
|
|
|
|
case tcpip.IPv4TOSOption:
|
|
e.mu.RLock()
|
|
v := int(e.ipv4TOS)
|
|
e.mu.RUnlock()
|
|
return v, nil
|
|
|
|
case tcpip.IPv6TrafficClassOption:
|
|
e.mu.RLock()
|
|
v := int(e.ipv6TClass)
|
|
e.mu.RUnlock()
|
|
return v, nil
|
|
|
|
case tcpip.IPv6MulticastInterfaceOption:
|
|
e.mu.RLock()
|
|
v := int(e.ipv6MulticastNICID)
|
|
e.mu.RUnlock()
|
|
return v, nil
|
|
|
|
default:
|
|
return -1, &tcpip.ErrUnknownProtocolOption{}
|
|
}
|
|
}
|
|
|
|
// multicastNetProto returns the network protocol of a given multicast address.
|
|
// Returns an error if the address is not a multicast address.
|
|
func (e *Endpoint) multicastNetProto(addr tcpip.Address) (tcpip.NetworkProtocolNumber, tcpip.Error) {
|
|
switch {
|
|
case header.IsV4MulticastAddress(addr):
|
|
return header.IPv4ProtocolNumber, nil
|
|
case header.IsV6MulticastAddress(addr):
|
|
return header.IPv6ProtocolNumber, nil
|
|
default:
|
|
return 0, &tcpip.ErrInvalidOptionValue{}
|
|
}
|
|
}
|
|
|
|
// SetSockOpt sets the socket option.
|
|
func (e *Endpoint) SetSockOpt(opt tcpip.SettableSocketOption) tcpip.Error {
|
|
switch v := opt.(type) {
|
|
case *tcpip.MulticastInterfaceOption:
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
fa := tcpip.FullAddress{Addr: v.InterfaceAddr}
|
|
fa, netProto, err := e.checkV4Mapped(fa, true /* bind */)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nic := v.NIC
|
|
addr := fa.Addr
|
|
|
|
if nic == 0 && addr == (tcpip.Address{}) {
|
|
e.multicastAddr = tcpip.Address{}
|
|
e.multicastNICID = 0
|
|
break
|
|
}
|
|
|
|
if nic != 0 {
|
|
if !e.stack.CheckNIC(nic) {
|
|
return &tcpip.ErrBadLocalAddress{}
|
|
}
|
|
} else {
|
|
nic = e.stack.CheckLocalAddress(0, netProto, addr)
|
|
if nic == 0 {
|
|
return &tcpip.ErrBadLocalAddress{}
|
|
}
|
|
}
|
|
|
|
if info := e.Info(); info.BindNICID != 0 && info.BindNICID != nic {
|
|
return &tcpip.ErrInvalidEndpointState{}
|
|
}
|
|
|
|
e.multicastNICID = nic
|
|
e.multicastAddr = addr
|
|
|
|
case *tcpip.AddMembershipOption:
|
|
// Allowing IP_ADD_MEMBERSHIP on an ipv6 socket matches Linux behavior:
|
|
// https://github.com/torvalds/linux/blob/cec1e6e5d1a/net/ipv6/ipv6_sockglue.c#L964
|
|
proto, err := e.multicastNetProto(v.MulticastAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nicID := v.NIC
|
|
if v.InterfaceAddr.Unspecified() {
|
|
if nicID == 0 {
|
|
if r, err := e.stack.FindRoute(0, tcpip.Address{}, v.MulticastAddr, proto, false /* multicastLoop */); err == nil {
|
|
nicID = r.NICID()
|
|
r.Release()
|
|
}
|
|
}
|
|
} else {
|
|
nicID = e.stack.CheckLocalAddress(nicID, proto, v.InterfaceAddr)
|
|
}
|
|
if nicID == 0 {
|
|
return &tcpip.ErrUnknownDevice{}
|
|
}
|
|
|
|
memToInsert := multicastMembership{nicID: nicID, multicastAddr: v.MulticastAddr}
|
|
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if _, ok := e.multicastMemberships[memToInsert]; ok {
|
|
return &tcpip.ErrPortInUse{}
|
|
}
|
|
|
|
if err := e.stack.JoinGroup(proto, nicID, v.MulticastAddr); err != nil {
|
|
return err
|
|
}
|
|
|
|
e.multicastMemberships[memToInsert] = struct{}{}
|
|
|
|
case *tcpip.RemoveMembershipOption:
|
|
proto, err := e.multicastNetProto(v.MulticastAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nicID := v.NIC
|
|
if v.InterfaceAddr.Unspecified() {
|
|
if nicID == 0 {
|
|
if r, err := e.stack.FindRoute(0, tcpip.Address{}, v.MulticastAddr, proto, false /* multicastLoop */); err == nil {
|
|
nicID = r.NICID()
|
|
r.Release()
|
|
}
|
|
}
|
|
} else {
|
|
nicID = e.stack.CheckLocalAddress(nicID, proto, v.InterfaceAddr)
|
|
}
|
|
if nicID == 0 {
|
|
return &tcpip.ErrUnknownDevice{}
|
|
}
|
|
|
|
memToRemove := multicastMembership{nicID: nicID, multicastAddr: v.MulticastAddr}
|
|
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if _, ok := e.multicastMemberships[memToRemove]; !ok {
|
|
return &tcpip.ErrBadLocalAddress{}
|
|
}
|
|
|
|
if err := e.stack.LeaveGroup(proto, nicID, v.MulticastAddr); err != nil {
|
|
return err
|
|
}
|
|
|
|
delete(e.multicastMemberships, memToRemove)
|
|
|
|
case *tcpip.SocketDetachFilterOption:
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetSockOpt returns the socket option.
|
|
func (e *Endpoint) GetSockOpt(opt tcpip.GettableSocketOption) tcpip.Error {
|
|
switch o := opt.(type) {
|
|
case *tcpip.MulticastInterfaceOption:
|
|
e.mu.Lock()
|
|
*o = tcpip.MulticastInterfaceOption{
|
|
NIC: e.multicastNICID,
|
|
InterfaceAddr: e.multicastAddr,
|
|
}
|
|
e.mu.Unlock()
|
|
|
|
default:
|
|
return &tcpip.ErrUnknownProtocolOption{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Info returns a copy of the endpoint info.
|
|
func (e *Endpoint) Info() stack.TransportEndpointInfo {
|
|
e.infoMu.RLock()
|
|
defer e.infoMu.RUnlock()
|
|
return e.info
|
|
}
|
|
|
|
// setInfo sets the endpoint's info.
|
|
//
|
|
// e.mu must be held to synchronize changes to info with the rest of the
|
|
// endpoint.
|
|
//
|
|
// +checklocks:e.mu
|
|
func (e *Endpoint) setInfo(info stack.TransportEndpointInfo) {
|
|
e.infoMu.Lock()
|
|
defer e.infoMu.Unlock()
|
|
e.info = info
|
|
}
|