Add batched UDP I/O on Darwin via sendmsg_x/recvmsg_x

On a connected socket sendmsg_x sends a whole batch in one syscall;
msg_name is unsupported there, so batched sends require connecting to
the peer, which loses roaming and is therefore opt-in via
SetSinglePeerMode for single fixed-peer binds. recvmsg_x fills in
per-message source addresses, so batched receive works on unconnected
sockets too. Any unexpected errno permanently falls back to the generic
paths. iOS is excluded: in the Network Extension recvmsg_x on
unconnected UDP sockets delivers no data, and connected sockets stop
passing traffic after a rebind.
This commit is contained in:
世界 2026-07-06 21:06:55 +08:00
parent fcbb7c473b
commit 57baac9504
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
3 changed files with 401 additions and 2 deletions

View file

@ -39,6 +39,8 @@ type StdNetBind struct {
ipv6 *net.UDPConn
ipv4PC *ipv4.PacketConn // will be nil on non-Linux
ipv6PC *ipv6.PacketConn // will be nil on non-Linux
ipv4RC syscall.RawConn // will be nil on non-Darwin
ipv6RC syscall.RawConn // will be nil on non-Darwin
ipv4TxOffload bool
ipv4RxOffload bool
ipv6TxOffload bool
@ -48,6 +50,8 @@ type StdNetBind struct {
udpAddrPool sync.Pool
msgsPool sync.Pool
msgx msgXState
blackhole4 bool
blackhole6 bool
}
@ -175,6 +179,7 @@ func (s *StdNetBind) Open(uport uint16) ([]ReceiveFunc, uint16, error) {
if s.ipv4 != nil || s.ipv6 != nil {
return nil, 0, ErrBindAlreadyOpen
}
s.msgx.reset()
// Attempt to open ipv4 and ipv6 listeners on the same port.
// If uport is 0, we can retry on failure.
@ -207,7 +212,22 @@ again:
v4pc = ipv4.NewPacketConn(v4conn)
s.ipv4PC = v4pc
}
fns = append(fns, s.makeReceiveIPv4(v4pc, v4conn, s.ipv4RxOffload))
if supportsMsgX {
var receiveFn ReceiveFunc
receiveFn, err = s.makeReceiveMsgX(v4conn, false)
if err != nil {
v4conn.Close()
return nil, 0, err
}
s.ipv4RC, err = v4conn.SyscallConn()
if err != nil {
v4conn.Close()
return nil, 0, err
}
fns = append(fns, receiveFn)
} else {
fns = append(fns, s.makeReceiveIPv4(v4pc, v4conn, s.ipv4RxOffload))
}
s.ipv4 = v4conn
}
if v6conn != nil {
@ -216,7 +236,22 @@ again:
v6pc = ipv6.NewPacketConn(v6conn)
s.ipv6PC = v6pc
}
fns = append(fns, s.makeReceiveIPv6(v6pc, v6conn, s.ipv6RxOffload))
if supportsMsgX {
var receiveFn ReceiveFunc
receiveFn, err = s.makeReceiveMsgX(v6conn, true)
if err != nil {
v6conn.Close()
return nil, 0, err
}
s.ipv6RC, err = v6conn.SyscallConn()
if err != nil {
v6conn.Close()
return nil, 0, err
}
fns = append(fns, receiveFn)
} else {
fns = append(fns, s.makeReceiveIPv6(v6pc, v6conn, s.ipv6RxOffload))
}
s.ipv6 = v6conn
}
if len(fns) == 0 {
@ -326,6 +361,9 @@ func (s *StdNetBind) BatchSize() int {
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
return IdealBatchSize
}
if supportsMsgX {
return msgXBatchSize
}
return 1
}
@ -467,6 +505,12 @@ func (s *StdNetBind) send(conn *net.UDPConn, pc batchWriter, msgs []ipv6.Message
start += n
}
} else {
if supportsMsgX {
handled, sendErr := s.sendMsgX(conn, msgs)
if handled {
return sendErr
}
}
for _, msg := range msgs {
_, _, err = conn.WriteMsgUDP(msg.Buffers[0], msg.OOB, msg.Addr.(*net.UDPAddr))
if err != nil {