-
Notifications
You must be signed in to change notification settings - Fork 107
Description
Hi Matt,
I'd like to propose adding scatter-gather I/O support for sending multiple netlink messages, which would allow sending large message batches without requiring a single contiguous memory allocation.
Background
The current SendMessages implementation marshals all messages into a single []byte buffer before calling sendmsg():
// Current approach (simplified from conn_linux.go)
func (c *conn) SendMessages(messages []Message) error {
var buf []byte
for _, m := range messages {
b, err := m.MarshalBinary()
buf = append(buf, b...) // Single growing buffer
}
_, err := c.s.Sendmsg(ctx, buf, nil, sa, 0)
return err
}This works well for typical use cases, but creates a hard ceiling when sending very large batches. For context, the nft CLI (userspace tool for nftables) uses scatter-gather I/O via sendmsg() with an iovec array to send arbitrarily large atomic ruleset updates. From the kernel mailing list (Pablo Neira Ayuso, nftables maintainer, 2013):
"While discussing atomic rule-set for nftables with Patrick McHardy, we decided to put all rule-set updates that need to be applied atomically in one single batch to simplify the existing approach. However, as explained above, the existing netlink code limits us to a maximum of ~20000 rules that fit in one single batch without hitting ENOBUFS."
The kernel still receives everything as a single datagram—scatter-gather just avoids the userspace contiguous allocation requirement.
Proposal
Add a new method that uses Go's unix.SendmsgBuffers() (available since Go 1.20, unix.SendmsgBuffers ) to send pre-marshaled message buffers via scatter-gather:
1. In mdlayher/socket — new method on *Conn:
// SendmsgBuffers wraps sendmsg(2) with scatter-gather I/O support.
// Each buffer in the slice becomes an iovec entry, sent as a single datagram.
func (c *Conn) SendmsgBuffers(ctx context.Context, buffers [][]byte, oob []byte, to unix.Sockaddr, flags int) (int, error) {
// Implementation using unix.SendmsgBuffers
}2. In mdlayher/netlink — new method on *Conn:
// SendMessagesScatter sends multiple messages using scatter-gather I/O.
// Each message is marshaled to its own buffer, avoiding a single large
// contiguous allocation. All messages are sent as one datagram.
func (c *Conn) SendMessagesScatter(messages []Message) error {
buffers := make([][]byte, len(messages))
for i, m := range messages {
b, err := m.MarshalBinary()
if err != nil {
return err
}
buffers[i] = b
}
_, err := c.sock.SendmsgBuffers(ctx, buffers, nil, sa, 0)
return err
}Why a new method rather than modifying SendMessages?
I'd suggest keeping this as a separate opt-in method initially:
- Zero risk to existing users — current behavior is unchanged
- Different memory characteristics — scatter-gather trades one large allocation for many small ones; existing users may prefer current behavior
- Easier to review and merge — purely additive change
- Can consolidate later — if proven stable, could hypothetically-theoretically become the default implementation in a future major version
Implementation Notes
- Requires Go 1.20+ for
unix.SendmsgBuffers()(aligns with your two-recent-versions policy) - The socket-level change would need to land in
mdlayher/socketfirst - No changes to the netlink protocol or message format—just the syscall mechanics
Offer to Help
I'm happy to prepare the PRs for both repositories if you're open to this feature. I can also write tests and benchmarks comparing the two approaches across various batch sizes.
Let me know your thoughts, and thanks for maintaining this excellent library!