mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 14:36:52 +01:00
udp/darwin: send to busy established peers on connected sockets
On darwin an unconnected sendto runs the address and MAC policy checks and the EndpointSecurity send check for every datagram, and while xprotectd throttles it each send takes about 185us, which caps a tunnel's upload and, through its ACKs, its download. A connected socket runs those checks once, at connect, and it is the only UDP send that reports the interface queue's flow control, as ENOBUFS. Only the current direct remote of an established tunnel is eligible. udp.EstablishedPeerConn is an optional Conn interface, implemented on darwin; establishedRemotes refcounts remotes over the main hostmap and tells each writer on the first and last reference: handshake completion adds one, deleting or retiring a hostinfo removes it, SetRemote (roaming, SetRemoteIfPreferred, control, ssh) moves it. Pending and relayed hostinfos hold none, and a rehandshake's two hostinfos share one. An eligible peer that Nebula sends NEBULA_CONNSOCK_RUN (64) datagrams to within NEBULA_CONNSOCK_WINDOW (1s) gets a connected socket: SO_REUSEPORT, bound to the source address the listener's route to the peer picks on the listener's port, carrying the listener's IP_BOUND_IF. The peer sees the same address and port as from the listener, and the socket's reader delivers through the listener's reader. The listener itself never sets SO_REUSEPORT (listen routines still do), so no other uid can bind the port; with a specific listen.host and no routines connected sockets stay off, since sharing an exact address needs SO_REUSEPORT on both. All routines listening on one address share one set of connected sockets: at most NEBULA_CONNSOCKS (8), one per peer, with opens from a bucket that refills one per NEBULA_CONNSOCK_OPEN_EVERY (1s). A socket closes when its peer stops being eligible, after NEBULA_CONNSOCK_IDLE (30s) without traffic, on Rebind (of every routine) and Close, and on an error that means the connected path is gone. Closing never runs under the hostmap's or the eligibility lock; the peer can't reopen until the close finishes. A send that gets ENOBUFS or EAGAIN waits within one 10ms budget shared by every writer since the last send that went through, then drops; nothing waits in the netpoller without a deadline. When nothing is eligible or open, a send costs one atomic load before the listener path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7cfa47d569
commit
3bb5918e7c
12 files changed
+2349
-13
No files matched your search
@@ -48,6 +48,14 @@ type Conn interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// EstablishedPeerConn is a Conn that treats the remotes of established tunnels differently from other
|
||||
// destinations, as darwin's gives busy ones connected sockets.
|
||||
type EstablishedPeerConn interface {
|
||||
// SetEstablishedPeer reports whether dst is the current remote of at least one established tunnel. Calls for
|
||||
// one dst must not race each other.
|
||||
SetEstablishedPeer(dst netip.AddrPort, established bool)
|
||||
}
|
||||
|
||||
type NoopConn struct{}
|
||||
|
||||
func (NoopConn) Rebind() error {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
//go:build !e2e_testing
|
||||
|
||||
package udp
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// benchConnSocks opens a wildcard listener with ListenOut running, with the connected socket knobs pinned to
|
||||
// their defaults and then cfg applied, and a loopback peer that never reads, so its sends cost only the sender.
|
||||
func benchConnSocks(b *testing.B, s Settings, cfg func(*connSockConfig)) (*StdConn, netip.AddrPort) {
|
||||
b.Helper()
|
||||
for k, v := range map[string]string{
|
||||
"NEBULA_CONNSOCKS": "8",
|
||||
"NEBULA_CONNSOCK_RUN": "64",
|
||||
"NEBULA_CONNSOCK_WINDOW": "1s",
|
||||
"NEBULA_CONNSOCK_IDLE": "30s",
|
||||
"NEBULA_CONNSOCK_OPEN_EVERY": "1s",
|
||||
} {
|
||||
b.Setenv(k, v)
|
||||
}
|
||||
c, err := NewListener(slog.New(slog.DiscardHandler), s)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.Cleanup(func() { _ = c.Close() })
|
||||
u := c.(*StdConn)
|
||||
if cfg != nil {
|
||||
cfg(&u.socks.cfg)
|
||||
}
|
||||
go func() { _ = u.ListenOut(func(netip.AddrPort, []byte) {}, func() {}) }()
|
||||
for u.reader.Load() == nil {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
pc, err := net.ListenPacket("udp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.Cleanup(func() { _ = pc.Close() })
|
||||
return u, pc.LocalAddr().(*net.UDPAddr).AddrPort()
|
||||
}
|
||||
|
||||
// BenchmarkConnSockWriteTo measures one WriteTo of a full-size datagram down each path: the listener with
|
||||
// connected sockets off, to a destination that is no tunnel's remote while another is, to an established peer
|
||||
// that never gets busy, and on a connected socket.
|
||||
func BenchmarkConnSockWriteTo(b *testing.B) {
|
||||
pkt := make([]byte, 1400)
|
||||
cases := []struct {
|
||||
name string
|
||||
cfg func(*connSockConfig)
|
||||
setup func(b *testing.B, u *StdConn, dst netip.AddrPort)
|
||||
}{
|
||||
{name: "listener/off", cfg: func(c *connSockConfig) { c.max = 0 }},
|
||||
{name: "listener/ineligible", setup: func(b *testing.B, u *StdConn, dst netip.AddrPort) {
|
||||
u.SetEstablishedPeer(netip.AddrPortFrom(dst.Addr(), dst.Port()+1), true)
|
||||
}},
|
||||
{name: "listener/eligible-not-busy", cfg: func(c *connSockConfig) { c.run = 1 << 30 },
|
||||
setup: func(b *testing.B, u *StdConn, dst netip.AddrPort) { u.SetEstablishedPeer(dst, true) }},
|
||||
{name: "connected", setup: func(b *testing.B, u *StdConn, dst netip.AddrPort) {
|
||||
u.SetEstablishedPeer(dst, true)
|
||||
for range u.socks.cfg.run {
|
||||
_ = u.WriteTo(pkt, dst)
|
||||
}
|
||||
if u.connSockFor(dst) == nil {
|
||||
b.Fatal("no connected socket")
|
||||
}
|
||||
}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
b.Run(c.name, func(b *testing.B) {
|
||||
u, dst := benchConnSocks(b, Settings{Listen: netip.MustParseAddrPort("0.0.0.0:0")}, c.cfg)
|
||||
if c.setup != nil {
|
||||
c.setup(b, u, dst)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
_ = u.WriteTo(pkt, dst)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkConnSockDispatch measures what WriteTo adds before the listener's sendto, without the syscall: a v4
|
||||
// listener refuses a v6 destination before sending, with connected sockets off or with another peer established.
|
||||
func BenchmarkConnSockDispatch(b *testing.B) {
|
||||
pkt := make([]byte, 1400)
|
||||
dst := netip.MustParseAddrPort("[::1]:4242")
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
cfg func(*connSockConfig)
|
||||
establish bool
|
||||
}{
|
||||
{name: "off", cfg: func(c *connSockConfig) { c.max = 0 }},
|
||||
{name: "ineligible", establish: true},
|
||||
} {
|
||||
b.Run(c.name, func(b *testing.B) {
|
||||
u, peer := benchConnSocks(b, Settings{Listen: netip.MustParseAddrPort("127.0.0.1:0"), Multi: true}, c.cfg)
|
||||
if c.establish {
|
||||
u.SetEstablishedPeer(peer, true)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
if u.WriteTo(pkt, dst) == nil {
|
||||
b.Fatal("sent a v6 datagram on a v4 listener")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,803 @@
|
||||
//go:build !e2e_testing
|
||||
|
||||
package udp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// A connected socket is a UDP socket connected to one busy peer, on the listener's port and bound to the source
|
||||
// address the listener's route to that peer picks, so the peer sees the same address and port from either. A send
|
||||
// on a connected socket skips the per-datagram address and MAC policy checks, and the EndpointSecurity send check
|
||||
// that xprotectd can hold every unconnected sendto on, and it is the only UDP send that reports the interface
|
||||
// queue's flow control, as ENOBUFS. A connected socket wins xnu's lookup for its peer's datagrams; it never sees
|
||||
// anyone else's.
|
||||
//
|
||||
// A connected socket sets SO_REUSEPORT and binds a specific address beside a wildcard listener, which sets
|
||||
// SO_REUSEPORT only for listen routines. xnu refuses a wildcard SO_REUSEPORT bind beside a holder without the
|
||||
// option, and refuses another uid's bind to an address a socket holds specifically, so no other user can join the
|
||||
// port while connected sockets open and close: uid nobody got EADDRINUSE on every bind against a root listener
|
||||
// with connected sockets in the 09-25 probes. A connected socket is closed, never disconnected, because xnu widens
|
||||
// a disconnected socket's local address to the wildcard.
|
||||
//
|
||||
// Only the current remote of an established tunnel may get one: Nebula names those with SetEstablishedPeer, so an
|
||||
// unauthenticated sender can't earn a connected socket by drawing replies such as recv_error or handshake
|
||||
// responses.
|
||||
type connSock struct {
|
||||
dst netip.AddrPort
|
||||
peer *connSockPeer
|
||||
conn *net.UDPConn
|
||||
rc syscall.RawConn
|
||||
// used is the monotonic time in nanoseconds of the connected socket's last send or receive.
|
||||
used atomic.Int64
|
||||
// stallAt is the monotonic time of the first ENOBUFS or EAGAIN since a send last went through, or 0, and
|
||||
// dropping is set once that stall has dropped a datagram, so each stall logs its drops once.
|
||||
stallAt atomic.Int64
|
||||
dropping atomic.Bool
|
||||
// parkMu serializes the writes that wait for a full send buffer, since the write deadline bounding each wait
|
||||
// is the descriptor's, not the writer's.
|
||||
parkMu sync.Mutex
|
||||
// sys writes one datagram on the descriptor: writeFd, or a stand-in for the interface in tests.
|
||||
sys func(fd int, b []byte) error
|
||||
}
|
||||
|
||||
func writeFd(fd int, b []byte) error {
|
||||
_, err := syscall.Write(fd, b)
|
||||
return err
|
||||
}
|
||||
|
||||
// errStallSpent is a write that found the connected socket's stall budget spent, so its datagram is dropped.
|
||||
var errStallSpent = errors.New("connected socket stall budget spent")
|
||||
|
||||
// connSockWriter carries one write through RawConn.Write. They are pooled with fn bound once, since a closure
|
||||
// passed through the RawConn interface escapes and would allocate on every send.
|
||||
type connSockWriter struct {
|
||||
s *connSock
|
||||
b []byte
|
||||
park bool
|
||||
err error
|
||||
fn func(fd uintptr) bool
|
||||
}
|
||||
|
||||
var connSockWriters = sync.Pool{New: func() any {
|
||||
w := &connSockWriter{}
|
||||
w.fn = w.write
|
||||
return w
|
||||
}}
|
||||
|
||||
func (w *connSockWriter) write(fd uintptr) bool {
|
||||
for {
|
||||
w.err = w.s.sys(int(fd), w.b)
|
||||
switch {
|
||||
case w.err == syscall.EINTR:
|
||||
case w.err == syscall.EAGAIN && w.park:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try writes b on s once and returns nil, the write's errno, os.ErrDeadlineExceeded, or net.ErrClosed. With park
|
||||
// set, EAGAIN waits in the netpoller until the socket is writable or the write deadline passes; without it, EAGAIN
|
||||
// is returned.
|
||||
func (s *connSock) try(b []byte, park bool) error {
|
||||
w := connSockWriters.Get().(*connSockWriter)
|
||||
w.s, w.b, w.park = s, b, park
|
||||
rerr := s.rc.Write(w.fn)
|
||||
err := w.err
|
||||
*w = connSockWriter{fn: w.fn}
|
||||
connSockWriters.Put(w)
|
||||
switch {
|
||||
case rerr == nil:
|
||||
return err
|
||||
case errors.Is(rerr, os.ErrDeadlineExceeded):
|
||||
return os.ErrDeadlineExceeded
|
||||
default:
|
||||
return net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
// connSockConfig holds the knobs. connSockConfigFromEnv reads all but backoff and enobufsMax from the environment,
|
||||
// so one binary can be tuned without a rebuild.
|
||||
type connSockConfig struct {
|
||||
// max is how many connected sockets the listen routines may have open at once; 0 turns connected sockets off.
|
||||
max int
|
||||
// run is how many datagrams to one destination within one window earn it a connected socket. darwin's tun
|
||||
// hands over one packet per read, so a peer's sends arrive one WriteTo or one-datagram batch at a time and
|
||||
// busyness has to be counted across calls.
|
||||
run int
|
||||
window time.Duration
|
||||
// idle closes a connected socket after it has neither sent nor received for this long.
|
||||
idle time.Duration
|
||||
// backoff keeps a destination whose connected socket failed on the listener for this long.
|
||||
backoff time.Duration
|
||||
// openEvery is how often the open bucket earns back an open once max are spent, which bounds connected socket
|
||||
// churn however fast senders earn connected sockets and errors close them.
|
||||
openEvery time.Duration
|
||||
// enobufsMax is how long a connected socket's sends may wait out ENOBUFS without one going through before they
|
||||
// drop instead, so a suspended interface costs one write per datagram rather than stalling the tun reader.
|
||||
enobufsMax time.Duration
|
||||
}
|
||||
|
||||
func connSockConfigFromEnv() connSockConfig {
|
||||
c := connSockConfig{
|
||||
max: 8,
|
||||
run: 64,
|
||||
window: time.Second,
|
||||
idle: 30 * time.Second,
|
||||
backoff: 30 * time.Second,
|
||||
openEvery: time.Second,
|
||||
enobufsMax: 10 * time.Millisecond,
|
||||
}
|
||||
if v, err := strconv.Atoi(os.Getenv("NEBULA_CONNSOCKS")); err == nil {
|
||||
c.max = v
|
||||
}
|
||||
if v, err := strconv.Atoi(os.Getenv("NEBULA_CONNSOCK_RUN")); err == nil && v > 0 {
|
||||
c.run = v
|
||||
}
|
||||
if v, err := time.ParseDuration(os.Getenv("NEBULA_CONNSOCK_WINDOW")); err == nil && v > 0 {
|
||||
c.window = v
|
||||
}
|
||||
if v, err := time.ParseDuration(os.Getenv("NEBULA_CONNSOCK_IDLE")); err == nil && v > 0 {
|
||||
c.idle = v
|
||||
}
|
||||
if v, err := time.ParseDuration(os.Getenv("NEBULA_CONNSOCK_OPEN_EVERY")); err == nil && v > 0 {
|
||||
c.openEvery = v
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// connSockGroups holds the connSocks that the listen routines bound to one address share, so the limits, the
|
||||
// connected sockets and Rebind cover every routine, and a peer has at most one connected socket.
|
||||
var connSockGroups = struct {
|
||||
sync.Mutex
|
||||
m map[netip.AddrPort]*connSocks
|
||||
}{m: map[netip.AddrPort]*connSocks{}}
|
||||
|
||||
// joinConnSocks returns the connSocks u shares with the other listen routines bound to la, and makes it for the
|
||||
// first.
|
||||
func joinConnSocks(u *StdConn, la netip.AddrPort, s Settings) *connSocks {
|
||||
g := &connSockGroups
|
||||
g.Lock()
|
||||
defer g.Unlock()
|
||||
if cs := g.m[la]; s.Multi && cs != nil {
|
||||
cs.mu.Lock()
|
||||
cs.members = append(cs.members, u)
|
||||
cs.mu.Unlock()
|
||||
return cs
|
||||
}
|
||||
cs := &connSocks{cfg: connSockConfigFromEnv(), key: la, members: []*StdConn{u}, clock: monoNow, sleep: time.Sleep}
|
||||
if cs.cfg.max > 0 && !u.listenHost.IsUnspecified() && !s.Multi {
|
||||
// A connected socket on a specific listen.host binds the listener's own address, which xnu allows only when
|
||||
// both sockets set SO_REUSEPORT, and a listener with it lets another uid bind a dual-stack wildcard on the
|
||||
// port.
|
||||
cs.off.Store(true)
|
||||
u.l.Debug("connected sockets: off for a specific listen.host", "addr", s.Listen)
|
||||
}
|
||||
if s.Multi {
|
||||
g.m[la] = cs
|
||||
}
|
||||
return cs
|
||||
}
|
||||
|
||||
type connSocks struct {
|
||||
cfg connSockConfig
|
||||
// key is the listeners' bound address, which connSockGroups files this under.
|
||||
key netip.AddrPort
|
||||
// off keeps connected sockets from opening, when the listener's address can't be shared with them.
|
||||
off atomic.Bool
|
||||
// table maps each destination that is the remote of an established tunnel, and that a connected socket could
|
||||
// reach, to its peer; it is nil when there are none, so a send with nothing to consider costs one load. Every
|
||||
// send reads it without a lock, and mu guards copying it. An open that holds mu and still finds its peer here
|
||||
// can't outlive the tunnel.
|
||||
table atomic.Pointer[map[netip.AddrPort]*connSockPeer]
|
||||
// reserved counts the connected sockets that are open, opening or closing, against cfg.max; mu guards changing it.
|
||||
reserved atomic.Int32
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
// members are the listen routines' listeners that share these connected sockets.
|
||||
members []*StdConn
|
||||
// pending holds each destination whose connected socket is opening or closing, and not on its peer. xnu
|
||||
// refuses to connect a 4-tuple that is still bound, so a destination in pending can't open another.
|
||||
pending map[netip.AddrPort]struct{}
|
||||
// rebinds counts Rebinds, so an open that copied the listener's interface before one is discarded.
|
||||
rebinds uint64
|
||||
// closers tracks the goroutines that close detached connected sockets.
|
||||
closers sync.WaitGroup
|
||||
// done stops the reaper; it is made when ListenOut starts the connected sockets.
|
||||
done chan struct{}
|
||||
// opens bounds connected socket opens to cfg.max at once and one per cfg.openEvery after.
|
||||
opens gcra
|
||||
// logs bounds connected socket log lines at info or warn; see connSockLogBurst.
|
||||
logs gcra
|
||||
// clock returns monotonic nanoseconds and sleep waits out ENOBUFS: monoNow and time.Sleep, or stand-ins in
|
||||
// tests.
|
||||
clock func() int64
|
||||
sleep func(time.Duration)
|
||||
// dialHook and closeHook, when set, run just before an open's dials and a close's Close, outside mu; tests
|
||||
// use them to hold an open or close in flight.
|
||||
dialHook, closeHook func(dst netip.AddrPort)
|
||||
}
|
||||
|
||||
// connSockPeer is what connSocks keeps about an established tunnel's remote: its connected socket, how busy it is,
|
||||
// and how long a failed connected socket keeps it on the listener.
|
||||
type connSockPeer struct {
|
||||
sock atomic.Pointer[connSock]
|
||||
// windowMu guards start, the monotonic time the current window began, and n, the datagrams sent in it, so a
|
||||
// window's reset and its counts can't interleave with another sender's.
|
||||
windowMu sync.Mutex
|
||||
start int64
|
||||
n int
|
||||
// retryAt is the monotonic time before which a peer whose connected socket failed stays on the listener;
|
||||
// connSocks.mu guards it.
|
||||
retryAt int64
|
||||
}
|
||||
|
||||
// busy counts n more datagrams to p and reports whether it has now sent cfg.run within one window. A busy peer's
|
||||
// count starts over, so a peer that can't get a connected socket retries at most once per cfg.run sends.
|
||||
func (p *connSockPeer) busy(now int64, n int, cfg *connSockConfig) bool {
|
||||
p.windowMu.Lock()
|
||||
defer p.windowMu.Unlock()
|
||||
if time.Duration(now-p.start) > cfg.window {
|
||||
p.start, p.n = now, 0
|
||||
}
|
||||
p.n += n
|
||||
if p.n < cfg.run {
|
||||
return false
|
||||
}
|
||||
p.n = 0
|
||||
return true
|
||||
}
|
||||
|
||||
// connSockLogBurst and connSockLogEvery size the bucket that lets connected socket opens, closes and failures log
|
||||
// at info or warn; past it they log at debug, so churn can't flood the log.
|
||||
const (
|
||||
connSockLogBurst = 32
|
||||
connSockLogEvery = 10 * time.Second
|
||||
)
|
||||
|
||||
var monoStart = time.Now()
|
||||
|
||||
func monoNow() int64 { return int64(time.Since(monoStart)) }
|
||||
|
||||
// gcra is a token bucket kept as the monotonic time it is next full (the generic cell rate algorithm), so it needs
|
||||
// no refill tick.
|
||||
type gcra struct{ tat int64 }
|
||||
|
||||
// take spends a token from a bucket of burst that earns one back per every, and reports whether there was one to
|
||||
// spend.
|
||||
func (g *gcra) take(now int64, burst int, every time.Duration) bool {
|
||||
e := int64(every)
|
||||
tat := max(g.tat, now)
|
||||
if tat-now > int64(burst-1)*e {
|
||||
return false
|
||||
}
|
||||
g.tat = tat + e
|
||||
return true
|
||||
}
|
||||
|
||||
// logLevelLocked returns level while the log bucket has a token, and debug once it is spent.
|
||||
func (cs *connSocks) logLevelLocked(level slog.Level) slog.Level {
|
||||
if cs.logs.take(cs.clock(), connSockLogBurst, connSockLogEvery) {
|
||||
return level
|
||||
}
|
||||
return slog.LevelDebug
|
||||
}
|
||||
|
||||
// startConnSocks starts the idle reaper.
|
||||
func (u *StdConn) startConnSocks() {
|
||||
cs := u.socks
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
if cs.closed || cs.done != nil || cs.cfg.max <= 0 || cs.off.Load() {
|
||||
return
|
||||
}
|
||||
cs.done = make(chan struct{})
|
||||
go u.reapConnSocks(cs.done)
|
||||
}
|
||||
|
||||
// stopConnSocks closes every connected socket and keeps new ones from opening, once any listen routine's listener
|
||||
// closes, as they do together at shutdown. It returns once their descriptors are closed.
|
||||
func (u *StdConn) stopConnSocks() {
|
||||
cs := u.socks
|
||||
cs.mu.Lock()
|
||||
cs.members = slices.DeleteFunc(cs.members, func(m *StdConn) bool { return m == u })
|
||||
if !cs.closed {
|
||||
cs.closed = true
|
||||
if cs.done != nil {
|
||||
close(cs.done)
|
||||
}
|
||||
u.detachAllLocked("close")
|
||||
}
|
||||
cs.mu.Unlock()
|
||||
connSockGroups.Lock()
|
||||
if connSockGroups.m[cs.key] == cs {
|
||||
delete(connSockGroups.m, cs.key)
|
||||
}
|
||||
connSockGroups.Unlock()
|
||||
cs.closers.Wait()
|
||||
}
|
||||
|
||||
// siblings returns the other listen routines' listeners.
|
||||
func (cs *connSocks) siblings(u *StdConn) []*StdConn {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
return slices.DeleteFunc(slices.Clone(cs.members), func(m *StdConn) bool { return m == u })
|
||||
}
|
||||
|
||||
// connSockFor returns dst's connected socket, or nil.
|
||||
func (u *StdConn) connSockFor(dst netip.AddrPort) *connSock {
|
||||
if p := u.socks.peer(dst); p != nil {
|
||||
return p.sock.Load()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *connSocks) peer(dst netip.AddrPort) *connSockPeer {
|
||||
if t := cs.table.Load(); t != nil {
|
||||
return (*t)[dst]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetEstablishedPeer makes dst eligible for a connected socket while it is the remote of an established tunnel,
|
||||
// and closes the one it has once it no longer is. Nebula calls it for every listen routine under the hostmap's
|
||||
// write lock, so repeats are no-ops and it makes no syscalls: the descriptor is closed by another goroutine.
|
||||
func (u *StdConn) SetEstablishedPeer(dst netip.AddrPort, established bool) {
|
||||
cs := u.socks
|
||||
if cs.cfg.max <= 0 || cs.off.Load() || !u.connSockable(dst) {
|
||||
return
|
||||
}
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
t := cs.table.Load()
|
||||
p := cs.peer(dst)
|
||||
if cs.closed || (p != nil) == established {
|
||||
return
|
||||
}
|
||||
next := map[netip.AddrPort]*connSockPeer{}
|
||||
if t != nil {
|
||||
next = maps.Clone(*t)
|
||||
}
|
||||
if established {
|
||||
p = &connSockPeer{start: cs.clock()}
|
||||
next[dst] = p
|
||||
} else {
|
||||
delete(next, dst)
|
||||
if s := p.sock.Load(); s != nil {
|
||||
u.detachLocked(s, "tunnel gone", false)
|
||||
}
|
||||
}
|
||||
cs.storeTableLocked(next)
|
||||
}
|
||||
|
||||
// storeTableLocked publishes t, or nil for an empty one.
|
||||
func (cs *connSocks) storeTableLocked(t map[netip.AddrPort]*connSockPeer) {
|
||||
if len(t) == 0 {
|
||||
cs.table.Store(nil)
|
||||
return
|
||||
}
|
||||
cs.table.Store(&t)
|
||||
}
|
||||
|
||||
// connSockable reports whether a connected socket to dst could send what the listener would. A v4 listener can't
|
||||
// reach a v6 destination at all, and a connected socket bound to a specific listen.host must share its family.
|
||||
func (u *StdConn) connSockable(dst netip.AddrPort) bool {
|
||||
if !dst.IsValid() || dst.Port() == 0 {
|
||||
return false
|
||||
}
|
||||
if u.isV4 {
|
||||
return dst.Addr().Is4()
|
||||
}
|
||||
return u.listenHost.IsUnspecified() || u.listenHost.Unmap().Is4() == dst.Addr().Unmap().Is4()
|
||||
}
|
||||
|
||||
// connSockRun sends bufs, all to p's destination, on p's connected socket, opening one if they make p busy. It
|
||||
// returns how many went out; when that is short of len(bufs), err is the failure of bufs[sent], or nil to leave
|
||||
// bufs[sent:] to the listener.
|
||||
func (u *StdConn) connSockRun(p *connSockPeer, dst netip.AddrPort, bufs [][]byte) (sent int, err error) {
|
||||
s := p.sock.Load()
|
||||
if s == nil {
|
||||
if s = u.maybeOpenConnSock(p, dst, len(bufs)); s == nil {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
sent, errno, closed := u.connSockWrite(s, bufs)
|
||||
switch {
|
||||
case sent == len(bufs):
|
||||
case connSockDead(errno):
|
||||
u.closeConnSock(s, errno.Error(), true)
|
||||
case !closed:
|
||||
return sent, &net.OpError{Op: "write", Err: errno}
|
||||
}
|
||||
return sent, nil
|
||||
}
|
||||
|
||||
// maybeOpenConnSock counts n datagrams to p, and opens a connected socket to it once it is busy and nothing rules
|
||||
// one out.
|
||||
func (u *StdConn) maybeOpenConnSock(p *connSockPeer, dst netip.AddrPort, n int) *connSock {
|
||||
cs := u.socks
|
||||
if n <= 0 || u.reader.Load() == nil || int(cs.reserved.Load()) >= cs.cfg.max || !p.busy(cs.clock(), n, &cs.cfg) {
|
||||
return nil
|
||||
}
|
||||
cs.mu.Lock()
|
||||
if cs.closed || cs.peer(dst) != p {
|
||||
cs.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
if s := p.sock.Load(); s != nil {
|
||||
cs.mu.Unlock()
|
||||
return s
|
||||
}
|
||||
now := cs.clock()
|
||||
if _, ok := cs.pending[dst]; ok || int(cs.reserved.Load()) >= cs.cfg.max || now < p.retryAt ||
|
||||
!cs.opens.take(now, cs.cfg.max, cs.cfg.openEvery) {
|
||||
cs.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
if cs.pending == nil {
|
||||
cs.pending = map[netip.AddrPort]struct{}{}
|
||||
}
|
||||
cs.pending[dst] = struct{}{}
|
||||
cs.reserved.Add(1)
|
||||
rebinds := cs.rebinds
|
||||
cs.mu.Unlock()
|
||||
|
||||
if cs.dialHook != nil {
|
||||
cs.dialHook(dst)
|
||||
}
|
||||
s, err := u.dialConnSock(p, dst)
|
||||
|
||||
cs.mu.Lock()
|
||||
if err != nil {
|
||||
delete(cs.pending, dst)
|
||||
cs.reserved.Add(-1)
|
||||
p.retryAt = cs.clock() + int64(cs.cfg.backoff)
|
||||
level := cs.logLevelLocked(slog.LevelWarn)
|
||||
cs.mu.Unlock()
|
||||
u.l.Log(context.Background(), level, "connected socket: open failed, staying on the listener", "udpAddr", dst, "error", err)
|
||||
return nil
|
||||
}
|
||||
if cs.closed || cs.rebinds != rebinds || cs.peer(dst) != p {
|
||||
// The tunnel went away, or a Rebind or shutdown ran, while the socket was opening; it stays in pending until
|
||||
// it is closed.
|
||||
cs.mu.Unlock()
|
||||
u.finishClose(s)
|
||||
u.l.Debug("connected socket: discarded, its tunnel or route changed while it opened", "udpAddr", dst)
|
||||
return nil
|
||||
}
|
||||
delete(cs.pending, dst)
|
||||
p.sock.Store(s)
|
||||
go u.readConnSock(s)
|
||||
level := cs.logLevelLocked(slog.LevelInfo)
|
||||
cs.mu.Unlock()
|
||||
u.l.Log(context.Background(), level, "connected socket: opened", "udpAddr", dst, "local", s.conn.LocalAddr(), "open", cs.reserved.Load())
|
||||
return s
|
||||
}
|
||||
|
||||
// dialConnSock opens a socket on the listener's port with SO_REUSEPORT, bound to the configured listen.host or,
|
||||
// for a wildcard listener, to the source address the route to dst picks, and connected to dst. It carries the
|
||||
// listener's IP_BOUND_IF, so both route the same way.
|
||||
func (u *StdConn) dialConnSock(p *connSockPeer, dst netip.AddrPort) (*connSock, error) {
|
||||
raddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(dst.Addr().Unmap(), dst.Port()))
|
||||
network := "udp6"
|
||||
if raddr.IP.To4() != nil {
|
||||
network = "udp4"
|
||||
}
|
||||
ifindex, err := u.boundIf()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
local := &net.UDPAddr{IP: u.listenHost.AsSlice(), Zone: u.listenHost.Zone()}
|
||||
if u.listenHost.IsUnspecified() {
|
||||
// A connected socket gets the source address xnu would pick for an unconnected send to dst.
|
||||
d := net.Dialer{Control: connSockControl(false, ifindex)}
|
||||
probe, err := d.Dial(network, raddr.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
local = probe.LocalAddr().(*net.UDPAddr)
|
||||
_ = probe.Close()
|
||||
}
|
||||
local.Port = int(u.port)
|
||||
d := net.Dialer{LocalAddr: local, Control: connSockControl(true, ifindex)}
|
||||
c, err := d.Dial(network, raddr.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uc := c.(*net.UDPConn)
|
||||
rc, err := uc.SyscallConn()
|
||||
if err != nil {
|
||||
_ = uc.Close()
|
||||
return nil, err
|
||||
}
|
||||
s := &connSock{dst: dst, peer: p, conn: uc, rc: rc, sys: writeFd}
|
||||
s.used.Store(u.socks.clock())
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// connSockControl sets SO_REUSEPORT when reuse is set, and IP_BOUND_IF or IPV6_BOUND_IF when ifindex isn't 0,
|
||||
// before the socket binds.
|
||||
func connSockControl(reuse bool, ifindex int) func(network, address string, c syscall.RawConn) error {
|
||||
return func(network, _ string, c syscall.RawConn) error {
|
||||
var serr error
|
||||
err := c.Control(func(fd uintptr) {
|
||||
if reuse {
|
||||
serr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||
}
|
||||
if serr == nil && ifindex != 0 {
|
||||
if network == "udp4" {
|
||||
serr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifindex)
|
||||
} else {
|
||||
serr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifindex)
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return serr
|
||||
}
|
||||
}
|
||||
|
||||
// boundIf returns the interface index the listener is scoped to, or 0. An error keeps the peer on the listener,
|
||||
// since a connected socket opened without the listener's scope could route out an interface the listener can't.
|
||||
func (u *StdConn) boundIf() (int, error) {
|
||||
level, opt := unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF
|
||||
if u.isV4 {
|
||||
level, opt = unix.IPPROTO_IP, unix.IP_BOUND_IF
|
||||
}
|
||||
v, err := unix.GetsockoptInt(int(u.sysFd), level, opt)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("reading the listener's bound interface: %w", err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// closeConnSock closes s if it is still open. With backoff set, the peer stays on the listener until the backoff
|
||||
// runs out.
|
||||
func (u *StdConn) closeConnSock(s *connSock, why string, backoff bool) {
|
||||
u.socks.mu.Lock()
|
||||
u.detachLocked(s, why, backoff)
|
||||
u.socks.mu.Unlock()
|
||||
}
|
||||
|
||||
// detachLocked takes s off its peer, if it is still there, and closes it on another goroutine, since callers such
|
||||
// as SetEstablishedPeer hold locks that no socket syscall should run under. s.dst stays in pending until the close
|
||||
// completes.
|
||||
func (u *StdConn) detachLocked(s *connSock, why string, backoff bool) {
|
||||
cs := u.socks
|
||||
if !s.peer.sock.CompareAndSwap(s, nil) {
|
||||
return
|
||||
}
|
||||
if backoff {
|
||||
s.peer.retryAt = cs.clock() + int64(cs.cfg.backoff)
|
||||
}
|
||||
if cs.pending == nil {
|
||||
cs.pending = map[netip.AddrPort]struct{}{}
|
||||
}
|
||||
cs.pending[s.dst] = struct{}{}
|
||||
level := cs.logLevelLocked(slog.LevelInfo)
|
||||
cs.closers.Add(1)
|
||||
go func() {
|
||||
defer cs.closers.Done()
|
||||
open := u.finishClose(s)
|
||||
u.l.Log(context.Background(), level, "connected socket: closed", "udpAddr", s.dst, "reason", why, "open", open)
|
||||
}()
|
||||
}
|
||||
|
||||
// finishClose closes s, whose destination is in pending, then lets the destination open again, and returns how
|
||||
// many connected sockets are left. Closing waits only for a write already in a syscall: a write parked on a full
|
||||
// buffer is woken, and one waiting out ENOBUFS holds no reference on the descriptor.
|
||||
func (u *StdConn) finishClose(s *connSock) int32 {
|
||||
cs := u.socks
|
||||
if cs.closeHook != nil {
|
||||
cs.closeHook(s.dst)
|
||||
}
|
||||
_ = s.conn.Close()
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
delete(cs.pending, s.dst)
|
||||
return cs.reserved.Add(-1)
|
||||
}
|
||||
|
||||
// detachAllLocked detaches every connected socket, for a network change or shutdown.
|
||||
func (u *StdConn) detachAllLocked(why string) {
|
||||
if t := u.socks.table.Load(); t != nil {
|
||||
for _, p := range *t {
|
||||
if s := p.sock.Load(); s != nil {
|
||||
u.detachLocked(s, why, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// closeConnSocks detaches every connected socket and discards the ones still opening, for a network change.
|
||||
func (u *StdConn) closeConnSocks(why string) {
|
||||
u.socks.mu.Lock()
|
||||
u.socks.rebinds++
|
||||
u.detachAllLocked(why)
|
||||
u.socks.mu.Unlock()
|
||||
}
|
||||
|
||||
func (u *StdConn) reapConnSocks(done <-chan struct{}) {
|
||||
cs := u.socks
|
||||
idle := cs.cfg.idle
|
||||
tick := time.NewTicker(max(min(idle/4, 5*time.Second), time.Millisecond))
|
||||
defer tick.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-tick.C:
|
||||
}
|
||||
t := cs.table.Load()
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
now := cs.clock()
|
||||
for _, p := range *t {
|
||||
if s := p.sock.Load(); s != nil && time.Duration(now-s.used.Load()) > idle {
|
||||
u.closeConnSock(s, "idle", false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// connSockDead reports whether a connected socket send or receive error means the connected path to the peer is
|
||||
// gone, so its traffic belongs back on the listener. A connected UDP socket reports a peer's ICMP unreachable as
|
||||
// ECONNREFUSED on its next call, which the unconnected listener never sees.
|
||||
func connSockDead(errno syscall.Errno) bool {
|
||||
switch errno {
|
||||
case unix.ECONNREFUSED, unix.EHOSTUNREACH, unix.EHOSTDOWN, unix.ENETUNREACH, unix.ENETDOWN,
|
||||
unix.EADDRNOTAVAIL, unix.EPIPE, unix.ENOTCONN, unix.EDESTADDRREQ:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// enobufsWait is how long a connected socket send sleeps when the interface queue is flow controlled. The kernel
|
||||
// posts no wakeup when the advisory lifts, so this polls; 50us kept a gigabit link full without spinning a core in
|
||||
// the 09-25 probes.
|
||||
const enobufsWait = 50 * time.Microsecond
|
||||
|
||||
// connSockWrite writes bufs on s, one datagram per write. A full socket buffer parks the caller in the netpoller
|
||||
// until the socket is writable, and a flow-controlled interface makes it wait out ENOBUFS, so a busy link slows
|
||||
// the tun reader instead of dropping. Once the connected socket has waited cfg.enobufsMax without a send going
|
||||
// through, either one drops the datagram instead, and a dropped datagram counts as sent, as the listener's
|
||||
// unreported interface drops do; it never falls back to the listener, which would reorder it. closed reports that
|
||||
// the connected socket was closed before bufs[sent] went out; any other error stops the run and is returned with
|
||||
// how many went out.
|
||||
func (u *StdConn) connSockWrite(s *connSock, bufs [][]byte) (sent int, errno syscall.Errno, closed bool) {
|
||||
cs := u.socks
|
||||
defer func() {
|
||||
if sent > 0 {
|
||||
s.used.Store(cs.clock())
|
||||
}
|
||||
}()
|
||||
for sent < len(bufs) {
|
||||
err := s.try(bufs[sent], false)
|
||||
if err == syscall.EAGAIN || err == os.ErrDeadlineExceeded {
|
||||
// os.ErrDeadlineExceeded here is another writer's parked deadline, passed but not yet cleared.
|
||||
err = u.parkedWrite(s, bufs[sent])
|
||||
}
|
||||
switch err {
|
||||
case nil:
|
||||
sent++
|
||||
if s.stallAt.Load() != 0 {
|
||||
s.stallAt.Store(0)
|
||||
s.dropping.Store(false)
|
||||
}
|
||||
continue
|
||||
case errStallSpent:
|
||||
u.dropStalled(s)
|
||||
sent++
|
||||
continue
|
||||
}
|
||||
errno, ok := err.(syscall.Errno)
|
||||
if !ok {
|
||||
return sent, 0, true
|
||||
}
|
||||
if errno != syscall.ENOBUFS {
|
||||
return sent, errno, false
|
||||
}
|
||||
// The wait holds no reference on the descriptor, so a close doesn't wait for it.
|
||||
if s.stalledFor(cs.clock()) < cs.cfg.enobufsMax {
|
||||
cs.sleep(enobufsWait)
|
||||
} else {
|
||||
u.dropStalled(s)
|
||||
sent++
|
||||
}
|
||||
}
|
||||
return sent, 0, false
|
||||
}
|
||||
|
||||
// parkedWrite writes b after EAGAIN, parked in the netpoller for what is left of s's stall budget, and returns
|
||||
// errStallSpent once none is. golang/go#73919: a darwin UDP write parked on EAGAIN can wait forever for a
|
||||
// writability event that never comes, so the wait always has a deadline.
|
||||
func (u *StdConn) parkedWrite(s *connSock, b []byte) error {
|
||||
cs := u.socks
|
||||
s.parkMu.Lock()
|
||||
defer s.parkMu.Unlock()
|
||||
left := cs.cfg.enobufsMax - s.stalledFor(cs.clock())
|
||||
if left <= 0 {
|
||||
return errStallSpent
|
||||
}
|
||||
if err := s.conn.SetWriteDeadline(time.Now().Add(left)); err != nil {
|
||||
return net.ErrClosed
|
||||
}
|
||||
err := s.try(b, true)
|
||||
_ = s.conn.SetWriteDeadline(time.Time{})
|
||||
if err == os.ErrDeadlineExceeded {
|
||||
return errStallSpent
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// dropStalled logs the first datagram a stall drops.
|
||||
func (u *StdConn) dropStalled(s *connSock) {
|
||||
if !s.dropping.Swap(true) {
|
||||
u.l.Debug("connected socket: sends stalled, dropping datagrams until one goes through", "udpAddr", s.dst)
|
||||
}
|
||||
}
|
||||
|
||||
// stalledFor records an ENOBUFS or EAGAIN on s at now and returns how long its sends have stalled: since the first
|
||||
// ENOBUFS or EAGAIN after a send last went through. Every writer measures from that one time, so concurrent writers
|
||||
// spend the budget no faster than one, and a send going through ends the stall for every writer, including one
|
||||
// asleep in it.
|
||||
func (s *connSock) stalledFor(now int64) time.Duration {
|
||||
if s.stallAt.CompareAndSwap(0, now) {
|
||||
return 0
|
||||
}
|
||||
at := s.stallAt.Load()
|
||||
if at == 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(now - at)
|
||||
}
|
||||
|
||||
// readConnSock delivers a connected socket's datagrams through the listener's reader, serialized with ListenOut,
|
||||
// until the connected socket is closed. Any receive error closes it: a connected socket reports its peer's
|
||||
// unreachables here, and retrying an unexpected error could spin.
|
||||
func (u *StdConn) readConnSock(s *connSock) {
|
||||
rd := u.reader.Load()
|
||||
buf := make([]byte, MTU)
|
||||
for {
|
||||
n, from, err := s.conn.ReadFromUDPAddrPort(buf)
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
why := err.Error()
|
||||
var errno syscall.Errno
|
||||
if errors.As(err, &errno) && connSockDead(errno) {
|
||||
why = errno.Error()
|
||||
} else {
|
||||
u.l.Warn("connected socket: unexpected receive error", "udpAddr", s.dst, "error", err)
|
||||
}
|
||||
u.closeConnSock(s, why, true)
|
||||
return
|
||||
}
|
||||
s.used.Store(u.socks.clock())
|
||||
u.readMu.Lock()
|
||||
rd.r(netip.AddrPortFrom(from.Addr().Unmap(), from.Port()), buf[:n:n])
|
||||
rd.flush()
|
||||
u.readMu.Unlock()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,6 @@
|
||||
//go:build !race && !e2e_testing
|
||||
|
||||
package udp
|
||||
|
||||
// raceEnabled reports a -race build, whose sync.Pool drops a random share of what is put back.
|
||||
const raceEnabled = false
|
||||
@@ -0,0 +1,6 @@
|
||||
//go:build race && !e2e_testing
|
||||
|
||||
package udp
|
||||
|
||||
// raceEnabled reports a -race build, whose sync.Pool drops a random share of what is put back.
|
||||
const raceEnabled = true
|
||||
+108
-11
@@ -10,6 +10,8 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
@@ -22,9 +24,26 @@ type StdConn struct {
|
||||
isV4 bool
|
||||
sysFd uintptr
|
||||
l *slog.Logger
|
||||
|
||||
// listenHost and port are the configured address and the bound port, which a connected socket shares.
|
||||
listenHost netip.Addr
|
||||
port uint16
|
||||
// rc reaches the listener's descriptor for another listen routine's Rebind.
|
||||
rc syscall.RawConn
|
||||
socks *connSocks
|
||||
// reader is ListenOut's reader, which connected socket readers share; readMu serializes them, since an
|
||||
// EncReader isn't safe for concurrent use.
|
||||
reader atomic.Pointer[readerPair]
|
||||
readMu sync.Mutex
|
||||
}
|
||||
|
||||
type readerPair struct {
|
||||
r EncReader
|
||||
flush func()
|
||||
}
|
||||
|
||||
var _ Conn = &StdConn{}
|
||||
var _ EstablishedPeerConn = &StdConn{}
|
||||
|
||||
func NewListener(l *slog.Logger, s Settings) (Conn, error) {
|
||||
lc := NewListenConfig(s.Multi)
|
||||
@@ -34,12 +53,13 @@ func NewListener(l *slog.Logger, s Settings) (Conn, error) {
|
||||
}
|
||||
|
||||
if uc, ok := pc.(*net.UDPConn); ok {
|
||||
c := &StdConn{UDPConn: uc, l: l}
|
||||
c := &StdConn{UDPConn: uc, l: l, listenHost: s.Listen.Addr()}
|
||||
|
||||
rc, err := uc.SyscallConn()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open udp socket: %w", err)
|
||||
}
|
||||
c.rc = rc
|
||||
|
||||
err = rc.Control(func(fd uintptr) {
|
||||
c.sysFd = fd
|
||||
@@ -53,6 +73,8 @@ func NewListener(l *slog.Logger, s Settings) (Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
c.isV4 = la.Addr().Is4()
|
||||
c.port = la.Port()
|
||||
c.socks = joinConnSocks(c, la, s)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@@ -89,6 +111,19 @@ func NewListenConfig(multi bool) net.ListenConfig {
|
||||
func sendto(s int, buf []byte, flags int, to unsafe.Pointer, addrlen int32) (err error)
|
||||
|
||||
func (u *StdConn) WriteTo(b []byte, ap netip.AddrPort) error {
|
||||
if t := u.socks.table.Load(); t != nil {
|
||||
if p := (*t)[ap]; p != nil {
|
||||
bufs := [1][]byte{b}
|
||||
if sent, err := u.connSockRun(p, ap, bufs[:]); sent == 1 || err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return u.writeTo(b, ap)
|
||||
}
|
||||
|
||||
// writeTo sends b through the listener.
|
||||
func (u *StdConn) writeTo(b []byte, ap netip.AddrPort) error {
|
||||
var sa unsafe.Pointer
|
||||
var addrLen int32
|
||||
|
||||
@@ -141,15 +176,47 @@ func (u *StdConn) WriteTo(b []byte, ap netip.AddrPort) error {
|
||||
|
||||
func (u *StdConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort) (int, error) {
|
||||
// An un-sendable destination costs its own packet, never the ones behind it in the batch.
|
||||
// TODO: WriteTo maps EWOULDBLOCK to an error, so a full send buffer
|
||||
// silently drops the rest of a burst (linux blocks instead). Poll for
|
||||
// writability on EAGAIN before giving up on the remainder.
|
||||
// TODO: writeTo maps EWOULDBLOCK to an error, so a full listener send buffer
|
||||
// silently drops those packets (linux blocks instead). Poll for
|
||||
// writability on EAGAIN before giving up on them.
|
||||
written := 0
|
||||
for i, b := range bufs {
|
||||
if err := u.WriteTo(b, addrs[i]); err == nil {
|
||||
written++
|
||||
} else {
|
||||
u.l.Debug("failed to write packet in batch", "udpAddr", addrs[i], "error", err)
|
||||
t := u.socks.table.Load()
|
||||
if t == nil {
|
||||
for i, b := range bufs {
|
||||
if err := u.writeTo(b, addrs[i]); err == nil {
|
||||
written++
|
||||
} else {
|
||||
u.l.Debug("failed to write packet in batch", "udpAddr", addrs[i], "error", err)
|
||||
}
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
for off := 0; off < len(bufs); {
|
||||
// A run of datagrams to one peer goes out together, on its connected socket if it has or earns one.
|
||||
dst := addrs[off]
|
||||
end := off + 1
|
||||
for end < len(bufs) && addrs[end] == dst {
|
||||
end++
|
||||
}
|
||||
if p := (*t)[dst]; p != nil {
|
||||
for off < end {
|
||||
sent, err := u.connSockRun(p, dst, bufs[off:end])
|
||||
written += sent
|
||||
off += sent
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
u.l.Debug("failed to write packet in batch", "udpAddr", dst, "error", err)
|
||||
off++
|
||||
}
|
||||
}
|
||||
// The listener sends what no connected socket took.
|
||||
for ; off < end; off++ {
|
||||
if err := u.writeTo(bufs[off], dst); err == nil {
|
||||
written++
|
||||
} else {
|
||||
u.l.Debug("failed to write packet in batch", "udpAddr", dst, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return written, nil
|
||||
@@ -182,6 +249,8 @@ func NewUDPStatsEmitter(udpConns []Conn) func() {
|
||||
|
||||
func (u *StdConn) ListenOut(r EncReader, flush func()) error {
|
||||
buffer := make([]byte, MTU)
|
||||
u.reader.Store(&readerPair{r: r, flush: flush})
|
||||
u.startConnSocks()
|
||||
|
||||
for {
|
||||
// Just read one packet at a time
|
||||
@@ -194,18 +263,29 @@ func (u *StdConn) ListenOut(r EncReader, flush func()) error {
|
||||
continue
|
||||
}
|
||||
|
||||
u.readMu.Lock()
|
||||
r(netip.AddrPortFrom(rua.Addr().Unmap(), rua.Port()), buffer[:n:n])
|
||||
flush()
|
||||
u.readMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the connected sockets of every listen routine, then the listener.
|
||||
func (u *StdConn) Close() error {
|
||||
u.stopConnSocks()
|
||||
return u.UDPConn.Close()
|
||||
}
|
||||
|
||||
func (u *StdConn) SupportsMultipleReaders() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Rebind clears the interface the kernel scoped this socket to, so that sends are routed against the current
|
||||
// routing table instead of the interface we happened to be on when the socket was created. Darwin pins sockets
|
||||
// this way on its own, which is what strands us after the underlying network changes.
|
||||
// this way on its own, which is what strands us after the underlying network changes. Control.RebindUDPServer
|
||||
// rebinds only the first writer, so this clears the other listen routines' listeners too. Connected sockets are
|
||||
// closed after the listeners are cleared, and later ones copy the cleared scope and pick their source addresses
|
||||
// afresh.
|
||||
func (u *StdConn) Rebind() error {
|
||||
var err error
|
||||
if u.isV4 {
|
||||
@@ -213,6 +293,23 @@ func (u *StdConn) Rebind() error {
|
||||
} else {
|
||||
err = syscall.SetsockoptInt(int(u.sysFd), syscall.IPPROTO_IPV6, syscall.IPV6_BOUND_IF, 0)
|
||||
}
|
||||
|
||||
for _, m := range u.socks.siblings(u) {
|
||||
err = errors.Join(err, m.clearBoundIf())
|
||||
}
|
||||
u.closeConnSocks("rebind")
|
||||
return err
|
||||
}
|
||||
|
||||
// clearBoundIf clears the interface the listener is scoped to through its RawConn, which fails on a listener that
|
||||
// closed concurrently instead of reaching whatever reused its descriptor number.
|
||||
func (u *StdConn) clearBoundIf() error {
|
||||
level, opt := syscall.IPPROTO_IPV6, syscall.IPV6_BOUND_IF
|
||||
if u.isV4 {
|
||||
level, opt = syscall.IPPROTO_IP, syscall.IP_BOUND_IF
|
||||
}
|
||||
var serr error
|
||||
if err := u.rc.Control(func(fd uintptr) { serr = syscall.SetsockoptInt(int(fd), level, opt, 0) }); err != nil {
|
||||
return err
|
||||
}
|
||||
return serr
|
||||
}
|
||||
Reference in new issue
Block a user