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. With a translated public address or movable local interface, use IPv4 wildcard 0.0.0.0. Bind to the exact local address when interface selection is part of the deployment contract.
203.23.178.228 is the service source. Enforce it in the network firewall and check again inside the application. This second check cannot substitute for the firewall, but it keeps unrelated traffic at the open port out of parsing.
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. Unless SO_RCVBUFFORCE permission exists, net.core.rmem_max caps the request. Prefer raising that 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 either 32 or 64 slots, giving every message buffer capacity of 1,228 bytes or more. Process only msg_len bytes. Never parse the unused tail.
Do not pass MSG_WAITFORONE if a sparse batch's initial packet then waits too long. 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
Give each data buffer more than 1,228 bytes, the documented maximum, 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 capacity as a dropped, truncated datagram and do not parse it.
Do not use a stream abstraction. UDP keeps each datagram boundary intact, so a successful receive returns exactly 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.
With SO_REUSEPORT, several sockets may share one address-and-port binding. 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 203.23.178.228 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. Every buffer must hold 1,228 bytes or more; 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 203.23.178.228 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.