|
|
|
|
@ -15,6 +15,7 @@ import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
|
|
|
|
"net/netip"
|
|
|
|
|
"runtime"
|
|
|
|
|
"slices"
|
|
|
|
|
"strconv"
|
|
|
|
|
"sync"
|
|
|
|
|
@ -66,10 +67,10 @@ type Server struct {
|
|
|
|
|
bindLifetime time.Duration
|
|
|
|
|
steadyStateLifetime time.Duration
|
|
|
|
|
bus *eventbus.Bus
|
|
|
|
|
uc4 batching.Conn // always non-nil
|
|
|
|
|
uc4Port uint16 // always nonzero
|
|
|
|
|
uc6 batching.Conn // may be nil if IPv6 bind fails during initialization
|
|
|
|
|
uc6Port uint16 // may be zero if IPv6 bind fails during initialization
|
|
|
|
|
uc4 []batching.Conn // length is always nonzero
|
|
|
|
|
uc4Port uint16 // always nonzero
|
|
|
|
|
uc6 []batching.Conn // length may be zero if udp6 bind fails
|
|
|
|
|
uc6Port uint16 // zero if len(uc6) is zero, otherwise nonzero
|
|
|
|
|
closeOnce sync.Once
|
|
|
|
|
wg sync.WaitGroup
|
|
|
|
|
closeCh chan struct{}
|
|
|
|
|
@ -337,37 +338,51 @@ func NewServer(logf logger.Logf, port uint16, onlyStaticAddrPorts bool) (s *Serv
|
|
|
|
|
Logf: logger.WithPrefix(logf, "netcheck: "),
|
|
|
|
|
SendPacket: func(b []byte, addrPort netip.AddrPort) (int, error) {
|
|
|
|
|
if addrPort.Addr().Is4() {
|
|
|
|
|
return s.uc4.WriteToUDPAddrPort(b, addrPort)
|
|
|
|
|
} else if s.uc6 != nil {
|
|
|
|
|
return s.uc6.WriteToUDPAddrPort(b, addrPort)
|
|
|
|
|
return s.uc4[0].WriteToUDPAddrPort(b, addrPort)
|
|
|
|
|
} else if len(s.uc6) > 0 {
|
|
|
|
|
return s.uc6[0].WriteToUDPAddrPort(b, addrPort)
|
|
|
|
|
} else {
|
|
|
|
|
return 0, errors.New("IPv6 socket is not bound")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = s.listenOn(port)
|
|
|
|
|
err = s.bindSockets(port)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
s.startPacketReaders()
|
|
|
|
|
|
|
|
|
|
if !s.onlyStaticAddrPorts {
|
|
|
|
|
s.wg.Add(1)
|
|
|
|
|
go s.addrDiscoveryLoop()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.wg.Add(1)
|
|
|
|
|
go s.packetReadLoop(s.uc4, s.uc6, true)
|
|
|
|
|
if s.uc6 != nil {
|
|
|
|
|
s.wg.Add(1)
|
|
|
|
|
go s.packetReadLoop(s.uc6, s.uc4, false)
|
|
|
|
|
}
|
|
|
|
|
s.wg.Add(1)
|
|
|
|
|
go s.endpointGCLoop()
|
|
|
|
|
|
|
|
|
|
return s, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) startPacketReaders() {
|
|
|
|
|
for i, uc := range s.uc4 {
|
|
|
|
|
var other batching.Conn
|
|
|
|
|
if len(s.uc6) > 0 {
|
|
|
|
|
other = s.uc6[min(len(s.uc6)-1, i)]
|
|
|
|
|
}
|
|
|
|
|
s.wg.Add(1)
|
|
|
|
|
go s.packetReadLoop(uc, other, true)
|
|
|
|
|
}
|
|
|
|
|
for i, uc := range s.uc6 {
|
|
|
|
|
var other batching.Conn
|
|
|
|
|
if len(s.uc4) > 0 {
|
|
|
|
|
other = s.uc4[min(len(s.uc4)-1, i)]
|
|
|
|
|
}
|
|
|
|
|
s.wg.Add(1)
|
|
|
|
|
go s.packetReadLoop(uc, other, false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) addrDiscoveryLoop() {
|
|
|
|
|
defer s.wg.Done()
|
|
|
|
|
|
|
|
|
|
@ -514,70 +529,108 @@ func trySetUDPSocketOptions(pconn nettype.PacketConn, logf logger.Logf) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// listenOn binds an IPv4 and IPv6 socket to port. We consider it successful if
|
|
|
|
|
// we manage to bind the IPv4 socket.
|
|
|
|
|
// bindSockets binds udp4 and udp6 sockets to desiredPort. We consider it
|
|
|
|
|
// successful if we manage to bind at least one udp4 socket. Multiple sockets
|
|
|
|
|
// may be bound per address family, e.g. SO_REUSEPORT, depending on platform.
|
|
|
|
|
//
|
|
|
|
|
// The requested port may be zero, in which case port selection is left up to
|
|
|
|
|
// the host networking stack. We make no attempt to bind a consistent port
|
|
|
|
|
// across IPv4 and IPv6 if the requested port is zero.
|
|
|
|
|
// desiredPort may be zero, in which case port selection is left up to the host
|
|
|
|
|
// networking stack. We make no attempt to bind a consistent port between udp4
|
|
|
|
|
// and udp6 if the requested port is zero, but a consistent port is used
|
|
|
|
|
// across multiple sockets within a given address family if SO_REUSEPORT is
|
|
|
|
|
// supported.
|
|
|
|
|
//
|
|
|
|
|
// TODO: make these "re-bindable" in similar fashion to magicsock as a means to
|
|
|
|
|
// deal with EDR software closing them. http://go/corp/30118. We could re-use
|
|
|
|
|
// [magicsock.RebindingConn], which would also remove the need for
|
|
|
|
|
// [singlePacketConn], as [magicsock.RebindingConn] also handles fallback to
|
|
|
|
|
// single packet syscall operations.
|
|
|
|
|
func (s *Server) listenOn(port uint16) error {
|
|
|
|
|
func (s *Server) bindSockets(desiredPort uint16) error {
|
|
|
|
|
// maxSocketsPerAF is a conservative starting point, but is somewhat
|
|
|
|
|
// arbitrary.
|
|
|
|
|
const maxSocketsPerAF = 16
|
|
|
|
|
listenConfig := &net.ListenConfig{
|
|
|
|
|
Control: listenControl,
|
|
|
|
|
}
|
|
|
|
|
for _, network := range []string{"udp4", "udp6"} {
|
|
|
|
|
uc, err := net.ListenUDP(network, &net.UDPAddr{Port: int(port)})
|
|
|
|
|
if err != nil {
|
|
|
|
|
SocketsLoop:
|
|
|
|
|
for i := 0; i < maxSocketsPerAF && i < runtime.NumCPU(); i++ {
|
|
|
|
|
if i > 0 {
|
|
|
|
|
// Use a consistent port per address family if the user-supplied
|
|
|
|
|
// port was zero, and we are binding multiple sockets.
|
|
|
|
|
if network == "udp4" {
|
|
|
|
|
desiredPort = s.uc4Port
|
|
|
|
|
} else {
|
|
|
|
|
desiredPort = s.uc6Port
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
uc, boundPort, err := s.bindSocketTo(listenConfig, network, desiredPort)
|
|
|
|
|
if err != nil {
|
|
|
|
|
switch {
|
|
|
|
|
case i == 0 && network == "udp4":
|
|
|
|
|
// At least one udp4 socket is required.
|
|
|
|
|
return err
|
|
|
|
|
case i == 0 && network == "udp6":
|
|
|
|
|
// A udp6 socket is not required.
|
|
|
|
|
s.logf("ignoring IPv6 bind failure: %v", err)
|
|
|
|
|
break SocketsLoop
|
|
|
|
|
default: // i > 0
|
|
|
|
|
// Reusable sockets are not required.
|
|
|
|
|
s.logf("ignoring reusable (index=%d network=%v) socket bind failure: %v", i, network, err)
|
|
|
|
|
break SocketsLoop
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pc := batching.TryUpgradeToConn(uc, network, batching.IdealBatchSize)
|
|
|
|
|
bc, ok := pc.(batching.Conn)
|
|
|
|
|
if !ok {
|
|
|
|
|
bc = &singlePacketConn{uc}
|
|
|
|
|
}
|
|
|
|
|
if network == "udp4" {
|
|
|
|
|
return err
|
|
|
|
|
s.uc4 = append(s.uc4, bc)
|
|
|
|
|
s.uc4Port = boundPort
|
|
|
|
|
} else {
|
|
|
|
|
s.logf("ignoring IPv6 bind failure: %v", err)
|
|
|
|
|
break
|
|
|
|
|
s.uc6 = append(s.uc6, bc)
|
|
|
|
|
s.uc6Port = boundPort
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
trySetUDPSocketOptions(uc, s.logf)
|
|
|
|
|
// TODO: set IP_PKTINFO sockopt
|
|
|
|
|
_, boundPortStr, err := net.SplitHostPort(uc.LocalAddr().String())
|
|
|
|
|
if err != nil {
|
|
|
|
|
uc.Close()
|
|
|
|
|
if s.uc4 != nil {
|
|
|
|
|
s.uc4.Close()
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
portUint, err := strconv.ParseUint(boundPortStr, 10, 16)
|
|
|
|
|
if err != nil {
|
|
|
|
|
uc.Close()
|
|
|
|
|
if s.uc4 != nil {
|
|
|
|
|
s.uc4.Close()
|
|
|
|
|
if !isReusableSocket(uc) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
pc := batching.TryUpgradeToConn(uc, network, batching.IdealBatchSize)
|
|
|
|
|
bc, ok := pc.(batching.Conn)
|
|
|
|
|
if !ok {
|
|
|
|
|
bc = &singlePacketConn{uc}
|
|
|
|
|
}
|
|
|
|
|
if network == "udp4" {
|
|
|
|
|
s.uc4 = bc
|
|
|
|
|
s.uc4Port = uint16(portUint)
|
|
|
|
|
} else {
|
|
|
|
|
s.uc6 = bc
|
|
|
|
|
s.uc6Port = uint16(portUint)
|
|
|
|
|
}
|
|
|
|
|
s.logf("listening on %s:%d", network, portUint)
|
|
|
|
|
}
|
|
|
|
|
s.logf("listening on udp4:%d sockets=%d", s.uc4Port, len(s.uc4))
|
|
|
|
|
if len(s.uc6) > 0 {
|
|
|
|
|
s.logf("listening on udp6:%d sockets=%d", s.uc6Port, len(s.uc6))
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) bindSocketTo(listenConfig *net.ListenConfig, network string, port uint16) (*net.UDPConn, uint16, error) {
|
|
|
|
|
lis, err := listenConfig.ListenPacket(context.Background(), network, fmt.Sprintf(":%d", port))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
}
|
|
|
|
|
uc := lis.(*net.UDPConn)
|
|
|
|
|
trySetUDPSocketOptions(uc, s.logf)
|
|
|
|
|
_, boundPortStr, err := net.SplitHostPort(uc.LocalAddr().String())
|
|
|
|
|
if err != nil {
|
|
|
|
|
uc.Close()
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
}
|
|
|
|
|
portUint, err := strconv.ParseUint(boundPortStr, 10, 16)
|
|
|
|
|
if err != nil {
|
|
|
|
|
uc.Close()
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
}
|
|
|
|
|
return uc, uint16(portUint), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close closes the server.
|
|
|
|
|
func (s *Server) Close() error {
|
|
|
|
|
s.closeOnce.Do(func() {
|
|
|
|
|
s.uc4.Close()
|
|
|
|
|
if s.uc6 != nil {
|
|
|
|
|
s.uc6.Close()
|
|
|
|
|
for _, uc4 := range s.uc4 {
|
|
|
|
|
uc4.Close()
|
|
|
|
|
}
|
|
|
|
|
for _, uc6 := range s.uc6 {
|
|
|
|
|
uc6.Close()
|
|
|
|
|
}
|
|
|
|
|
close(s.closeCh)
|
|
|
|
|
s.wg.Wait()
|
|
|
|
|
|