Set up the UDP receive socket
Bind and configure a UDP socket that exposes loss instead of hiding it.
Before you start
- A verified destination address
- Linux kernel access
- Rust 1.80 or newer
Configure before binding
Create an IPv4 datagram socket, set its receive buffer, enable overflow ancillary data, then bind it to the verified destination port. Bind to 0.0.0.0 when the public address is translated or may move between local interfaces. Bind to the exact local address when interface selection is part of the deployment contract.
The source is 64.130.40.90. Enforce that address in the network firewall and check it again in the application. The application check is not a substitute for the firewall, but it prevents unrelated datagrams sent to the open port from entering the parsing pipeline.
Use socket2 for portable socket creation and libc for Linux-specific options. The following function makes the actual setsockopt calls and verifies the effective receive buffer:
rust
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
use std::{io, mem, net::SocketAddr, os::fd::AsRawFd, time::Duration};
fn set_int(fd: i32, level: i32, name: i32, value: i32) -> io::Result<()> {
let rc = unsafe {
libc::setsockopt(
fd,
level,
name,
(&value as *const i32).cast(),
mem::size_of::<i32>() as libc::socklen_t,
)
};
if rc == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
}
fn open_receiver(bind: SocketAddr, requested: usize) -> io::Result<Socket> {
let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
socket.set_reuse_address(true)?;
socket.set_recv_buffer_size(requested)?;
set_int(socket.as_raw_fd(), libc::SOL_SOCKET, libc::SO_RXQ_OVFL, 1)?;
socket.set_read_timeout(Some(Duration::from_secs(1)))?;
socket.bind(&SockAddr::from(bind))?;
let effective = socket.recv_buffer_size()?;
if effective < requested {
return Err(io::Error::other("effective receive buffer below request"));
}
Ok(socket)
}Use dependencies socket2 = "0.5" and libc = "0.2". On Linux, the kernel doubles the SO_RCVBUF value for bookkeeping and returns the doubled value. A request of 8 MiB commonly reads back as 16 MiB. The request is capped by net.core.rmem_max unless the process has permission to use SO_RCVBUFFORCE. Prefer raising the host limit under configuration management and using ordinary SO_RCVBUF.
Receive batches
One recv_from call per datagram is valid and useful for the first receiver. A production loop should use Linux recvmmsg to amortize system-call cost while retaining one length and source address per datagram. Prepare 32 or 64 message slots, each with a 1,228-byte or larger buffer. Process only msg_len bytes. Never parse the unused tail.
Do not pass MSG_WAITFORONE if it creates unacceptable latency for the first packet in a sparse batch. Calling recvmmsg with no blocking flag on a nonblocking socket works well with epoll. Drain until EAGAIN, then wait again. On a dedicated blocking thread, MSG_WAITFORONE can block for the first packet and collect immediately available followers.
Read SO_RXQ_OVFL control messages returned by recvmsg or recvmmsg. Linux places a 32-bit cumulative count of packets dropped by that socket since creation in ancillary data. A jump between two received datagrams proves socket-queue loss. The global /proc/net/snmp counters remain useful, but they include other UDP sockets in the network namespace.
Detect truncation
Set each data buffer larger than the documented maximum of 1,228 bytes or request MSG_TRUNC. With MSG_TRUNC, Linux reports the original datagram length even if the supplied buffer is shorter. Treat any returned length above the buffer capacity as a dropped, truncated datagram and do not parse it.
Do not use a stream abstraction. UDP preserves datagram boundaries. One successful receive corresponds to one delivered datagram. A short datagram is not a partial read that can be completed with another call. Reject it if the shred parser cannot validate its format.
Use nonblocking mode deliberately
A single dedicated receive thread can use a blocking socket and a one-second timeout. The timeout gives the thread a chance to export health and observe shutdown. An event-loop receiver should set nonblocking mode and register EPOLLIN with epoll. Edge-triggered epoll requires draining the socket until EAGAIN; failing to drain can leave packets queued without another notification.
Do not perform signature checks, logging, allocation-heavy serialization, or RPC calls between receive operations. Copy the datagram into a preallocated object, attach Instant::now(), check the source address, and enqueue it. Keep the queue bounded. If it is full, increment receiver_channel_dropped_total and discard according to a documented policy.
Consider busy polling only after measurement
Linux SO_BUSY_POLL accepts a microsecond budget and can reduce wake-up latency on supported NIC drivers. It also consumes CPU. Set it with the same set_int helper using libc::SO_BUSY_POLL, then compare p50, p99, CPU use, and drop counters. Do not enable it by default on shared hosts.
SO_REUSEPORT allows multiple sockets to bind the same address and port. The kernel hashes a UDP flow to one socket. Because this service uses a stable source and destination tuple, ordinary reuse-port hashing can send the entire feed to one worker. Use one socket plus worker queues, or attach a tested reuse-port BPF classifier. Multiple threads calling recv on one socket also work, but scheduling and packet ownership become less predictable.
Validate the configured socket
Log the bind address, effective receive buffer, nonblocking state, and process ID once at startup. Use ss -u -n -m -p to inspect the live socket. The skmem fields show buffer limits and drops. Run tcpdump -ni any udp port PORT and src host 64.130.40.90 during commissioning. Stop packet capture after validation because full-rate capture adds I/O and can become its own failure source.
Parameters
When it goes wrong
setsockopt(SO_RCVBUF): Operation not permitted
Cause. The code used SO_RCVBUFFORCE without CAP_NET_ADMIN.
Fix. Raise net.core.rmem_max and use SO_RCVBUF, or grant the narrow capability under host policy.
bind: Address already in use
Cause. Another socket owns the destination port or SO_REUSEPORT usage is inconsistent.
Fix. Locate the owner with ss -lunp, stop it, or configure every intended listener consistently.
receiver: truncated datagram
Cause. The receive slot is smaller than the incoming UDP datagram.
Fix. Use buffers of at least 1,228 bytes and reject any receive marked MSG_TRUNC.
Questions
- Should the socket connect to the sender address?
- A connected UDP socket filters inbound datagrams to one peer and permits recv instead of recv_from. It can be appropriate when the sender source port is stable and documented. Only the source IP is fixed here, so bind normally, filter 64.130.40.90 in the firewall, and retain source-port visibility.
- Should SO_REUSEPORT be enabled?
- Not by default. Linux normally hashes one stable UDP flow to one reuse-port socket, so adding listeners may not distribute this feed. Use one receive socket feeding bounded worker queues. Enable SO_REUSEPORT only with a measured classifier or when packet source tuples provide the distribution you expect.
- Why read SO_RCVBUF back after setting it?
- The host sysctl can cap an unprivileged request, and Linux reports a bookkeeping-adjusted value. Reading the option back records what the kernel actually granted. Without that check, configuration can claim a large queue while the live socket is operating with a much smaller limit.