diff --git a/handshake_manager.go b/handshake_manager.go index 884b892d..419f7b3e 100644 --- a/handshake_manager.go +++ b/handshake_manager.go @@ -975,7 +975,9 @@ func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostIn nb := make([]byte, 12, 12) out := make([]byte, mtu) for _, cp := range hh.packetStore { - //todo use a sendbatcher + // TODO: use a SendBatch here. Each callback lands in + // sendNoMetrics -> WriteTo: one syscall per cached packet, + // where one sendmmsg could flush the whole store. cp.callback(cp.messageType, cp.messageSubType, hostinfo, cp.packet, nb, out) } f.cachedPacketMetrics.sent.Inc(int64(len(hh.packetStore))) diff --git a/interface.go b/interface.go index 98984816..d66feca5 100644 --- a/interface.go +++ b/interface.go @@ -292,6 +292,13 @@ func (f *Interface) activate() error { return err } if len(queues) < f.routines { + // TODO: this clamp is only safe because it is unreachable when the + // udp side has multiple readers (linux Queues opens exactly n or + // errors; every other platform already clamped routines to 1 above). + // If a platform ever returns fewer queues than routines with + // SO_REUSEPORT sockets already bound, the surplus sockets get no + // listenOut and the kernel blackholes every flow it hashes to them — + // fail loudly or close the extra sockets instead. f.l.Warn("tun multiqueue is not supported on this platform, falling back to fewer routines", "requested", f.routines, "opened", len(queues)) f.routines = len(queues) diff --git a/overlay/tio/tio_poll_linux.go b/overlay/tio/tio_poll_linux.go index 8d09dc2d..50280f28 100644 --- a/overlay/tio/tio_poll_linux.go +++ b/overlay/tio/tio_poll_linux.go @@ -47,6 +47,11 @@ func (t *Poll) blockOnWrite() error { return blockOn(int32(t.fd), int32(t.shutdownFd), unix.POLLOUT) } +// TODO: port Offload's post-wake drain loop here so one poll wake amortizes +// over a burst (up to tunDrainCap packets) instead of paying a syscall and a +// wake per packet. Hosts on the TUNSETOFFLOAD-failure fallback or a tun.fd +// config currently lose that batching. blockOn and the EAGAIN plumbing are +// already shared; kept one-packet-per-Read for now to preserve behavior. func (t *Poll) Read() ([]Packet, error) { n, err := t.readOne(t.readBuf) if err != nil { diff --git a/udp/udp_darwin.go b/udp/udp_darwin.go index 1ef23d31..58c4bfe2 100644 --- a/udp/udp_darwin.go +++ b/udp/udp_darwin.go @@ -142,6 +142,9 @@ func (u *StdConn) WriteTo(b []byte, ap netip.AddrPort) error { func (u *StdConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort, _ []byte) (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. written := 0 for i, b := range bufs { if err := u.WriteTo(b, addrs[i]); err == nil { diff --git a/udp/udp_linux.go b/udp/udp_linux.go index ea7bc070..e8884ddc 100644 --- a/udp/udp_linux.go +++ b/udp/udp_linux.go @@ -313,6 +313,9 @@ func (u *StdConn) ListenOut(r EncReader, flush func()) error { for { if cmsgSpace > 0 { + // TODO: the kernel only rewrites Controllen on entries it fills, + // so resetting just the first `n` from the previous wakeup would + // save ~(batch-n) stores per wakeup on trickle traffic. for i := range msgs { setMsgControllen(&msgs[i].Hdr, cmsgSpace) } diff --git a/udp/udp_linux_writebatch.go b/udp/udp_linux_writebatch.go index 5aa5c329..dc1e1726 100644 --- a/udp/udp_linux_writebatch.go +++ b/udp/udp_linux_writebatch.go @@ -289,6 +289,12 @@ func (w *batchWriter) WriteBatch(bufs [][]byte, addrs []netip.AddrPort, ecns []b i = baseI continue } + // TODO: a transient zero-sent errno (ENOBUFS under socket-memory + // pressure, or a theoretical EINTR) lands here too and drops + // entry 0's entire run (up to 63/127 packets). The RX path + // retries EINTR; consider a bounded retry for those two before + // falling through to the per-entry drop. + // // Any other zero-sent error is a per-entry failure: // an unreachable destination, a firewall EPERM, or a PMTU shrink after a roam // (EINVAL, or EMSGSIZE since kernel 6.14, once gso_size no longer fits the path).