UDP, IPv4, IPv6, fragmentation, and checksums

Compare UDP and IP delivery, fragmentation and checksum handling.

15 min readChapter 51 of 168Intermediate → advanced
In this chapter

Before you start#

Read TCP internals and sockets and the network stack. The source baseline is upstream Linux v6.12, a historical teaching version. The lab sends only between sockets bound to numeric loopback addresses in one process; IPv6 may be unavailable or disabled.

Learning goals#

Trace an IPv4 UDP datagram through send, IP output and receive, explain datagram truncation and zero-length payloads, distinguish IPv4 and IPv6 fragmentation and checksums, and identify error and lifetime boundaries.

Source walkthrough#

UDP, IPv4, IPv6, fragmentation, and checksums
  1. UDP send

    Address, port and route select a datagram path.

  2. IP output

    Local hooks and destination determine continuation.

  3. UDP receive

    Validate header and checksum; look up and queue to a socket.

  4. Application read

    Buffer limits can truncate a datagram; zero payload remains a datagram.

Conceptual model. Linux v6.12 conceptual source model; conditional paths and values are illustrative, not a captured trace.

UDP provides datagrams with source and destination ports. It preserves each datagram boundary at the socket API, but it does not provide TCP's ordered reliable byte stream, retransmission or connection handshake. Datagrams can be lost, duplicated or reordered. A successful local send does not establish remote receipt. A socket can be bound to a local address and port; an unbound socket can receive an ephemeral local port as part of sending under the applicable path. Binding conflicts, wildcard addresses, reuse options, namespaces and privileges affect port selection. SO_REUSEPORT can distribute packets among an eligible group; it does not by itself guarantee equal distribution or delivery to a specific process.

For an IPv4 UDP socket, udp_sendmsg determines the destination from the message or connected socket, processes control and socket options, and obtains a route as needed. It constructs or appends data under its corking rules, then pushes a datagram to IP output. A connected UDP socket records a default peer and changes filtering and error behavior; it does not exchange a SYN or turn UDP into TCP. The implementation rejects an excessively large length and unsupported flags on the relevant branches. A route lookup, memory allocation, permission check or MTU constraint can also fail. MSG_MORE or UDP corking can affect how data is accumulated; do not assume every individual send call necessarily becomes an independently transmitted datagram when those options are used.

An IPv4 output step is ip_local_out, Linux v6.12, GPL-2.0-only:

c
	err = __ip_local_out(net, sk, skb);
	if (likely(err == 1))
		err = dst_output(net, sk, skb);

The first call performs the local-output stage including the configured hook path; only its special continuation result of 1 causes this function to call the destination's output method. Other results are returned rather than blindly issuing the skb to the next stage. Routing, netfilter, device output and errors depend on configuration. This excerpt is one stage, not a universal guarantee that a packet reached a NIC or wire.

On receive, IPv4's ip_rcv handles an incoming packet and, for local delivery, IP protocol dispatch can select UDP. __udp4_lib_rcv checks that the header is accessible, validates UDP length, initializes checksum processing, and looks up an eligible socket before queueing. An skb is a packet descriptor with its own reference and payload lifetime. If validation fails or no matching socket is found, the stack handles or drops the skb through the appropriate error path. Queue pressure and socket memory limits can discard datagrams before the application reads them. An skb arriving at a host is not equivalent to a datagram successfully copied to userspace.

In __udp4_lib_rcv, v6.12, GPL-2.0-or-later (socket lookup and checksum setup omitted), the first validation steps are:

c
	if (!pskb_may_pull(skb, sizeof(struct udphdr)))
		goto drop;		/* No space for header. */

	uh   = udp_hdr(skb);
	ulen = ntohs(uh->len);
	saddr = ip_hdr(skb)->saddr;
	daddr = ip_hdr(skb)->daddr;

	if (ulen > skb->len)
		goto short_packet;

	if (proto == IPPROTO_UDP) {
		/* UDP validates ulen. */
		if (ulen < sizeof(*uh) || pskb_trim_rcsum(skb, ulen))
			goto short_packet;
		uh = udp_hdr(skb);
	}

The length in the UDP header is checked against the skb before any socket lookup, and a bad length takes the short_packet path. The packet is not delivered with a length error to a reader, which is consistent with the no-per-packet-error point above. The excerpt does not show udp4_csum_init, the socket lookup, or what short_packet frees.

The IPv6 path uses its own input and UDP implementation: ipv6_rcv and udpv6_rcv participate in relevant IPv6 delivery. Socket matching includes address family, local/remote addresses and ports, namespace and reuse rules. These are distinct code paths, even though applications can use analogous socket calls.

One IPv4 UDP datagram end to end
One IPv4 UDP datagram end to end1. Sender socket to udp_sendmsg: sendto(datagram). 2. udp_sendmsg to ip_local_out: route and hand skb to IP output. 3. ip_local_out to __udp4_lib_rcv: local delivery reaches UDP receive. 4. __udp4_lib_rcv to Receiver socket: queue skb for the socket. 5. Receiver socket to __udp4_lib_rcv (return): recvmsg consumes or truncates.Sender socketudp_sendmsgip_local_out__udp4_lib_rcvReceiver socketsendto(datagram)route and hand skbto IP outputlocal deliveryreaches UDP receivequeue skb for thesocketrecvmsg consumes ortruncates
  1. Sender socketudp_sendmsg

    sendto(datagram)

  2. udp_sendmsgip_local_out

    route and hand skb to IP output

  3. ip_local_out__udp4_lib_rcv

    local delivery reaches UDP receive

  4. __udp4_lib_rcvReceiver socket

    queue skb for the socket

  5. Receiver socket__udp4_lib_rcv

    Return. recvmsg consumes or truncates

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Sender socket → udp_sendmsg: sendto(datagram)
  2. udp_sendmsg → ip_local_out: route and hand skb to IP output
  3. ip_local_out → __udp4_lib_rcv: local delivery reaches UDP receive
  4. __udp4_lib_rcv → Receiver socket: queue skb for the socket
  5. Receiver socket → __udp4_lib_rcv (return): recvmsg consumes or truncates
Source map. Source map of Linux v6.12 IPv4 UDP send and receive functions, not a captured trace; routing, netfilter and device choices change the middle stages.

Receive length and a concrete datagram#

In udp_recvmsg, Linux v6.12, GPL-2.0-or-later, the received skb's UDP payload length is compared with the user's buffer:

c
	ulen = udp_skb_len(skb);
	copied = len;
	if (copied > ulen - off)
		copied = ulen - off;
	else if (copied < ulen)
		msg->msg_flags |= MSG_TRUNC;

ulen is the datagram payload length in this helper, len is the requested copy length, and off reflects peek handling. The first condition limits copying to available data. The second marks truncation when the requested copy is shorter than the datagram. The full function also checks checksums, handles copy faults, fills address/control information and frees or consumes the skb as appropriate. A copy error can drop the received skb and return an error; it is not a successful partial application message.

Imagine a sender transmits one eight-byte datagram abcdefgh. A receiver uses a four-byte buffer with recvmsg: it obtains abcd and MSG_TRUNC in the returned flags, while the remainder of that datagram is discarded on a normal consuming receive. A second receive waits for a new datagram; it does not obtain efgh from the old one. On Linux, passing MSG_TRUNC as an input flag for supported Internet datagram sockets can request a return value reflecting the full datagram length; distinguish that option from the output truncation flag and verify the specific API (recvmsg(2)). A zero-length UDP datagram is a valid datagram. Receiving b'' from a datagram socket can therefore mean a zero-length message, not stream EOF (Linux recv(2)).

An 8-byte datagram read with a 4-byte buffer

Synthetic teaching data, not a device capture.

  1. One datagram

    Eight-byte payload abcdefgh queued as a single datagram.

  2. recvmsg with 4 bytes

    Copies abcd and sets MSG_TRUNC in msg_flags.

  3. Remainder

    Discarded on a normal consuming receive; efgh is not kept.

  4. Next receive

    Waits for a new datagram rather than returning efgh.

Synthetic teaching data. Synthetic teaching data of udp_recvmsg truncation for the worked example, not a captured trace; Linux v6.12 udp_recvmsg is the reference.

A connected UDP sender can receive asynchronous network errors under relevant conditions; Linux supports extended error queues when enabled. An ICMP error can refer to an earlier datagram, and not every dropped datagram generates a useful error. An application requiring acknowledgement, retransmission, ordering or duplicate suppression must implement an appropriate protocol or use a transport that supplies those properties. A datagram queue can fill when the receiver is slow; increasing a buffer may reduce some drops but does not convert UDP into reliable delivery.

IPv4 and IPv6 packet boundaries#

IPv4 has a variable-length header (20–60 octets) that can include options (RFC 791, §3.1). Its header includes an identification field, fragment offset and flags. An IPv4 sender or, where permitted by the packet and path rules, a router can fragment a packet to fit a smaller MTU. Fragments are reassembled at the destination; loss of one fragment prevents reassembly of the original datagram. Fragmentation is therefore not a way to make a large UDP message reliable. Firewalls, reassembly timeouts and resource limits can affect delivery. The Don't Fragment setting and path-MTU discovery alter the outcome; an oversized send can fail with EMSGSIZE rather than producing fragments.

IPv6 has a fixed 40-octet base header, which can be followed by extension headers (RFC 8200, §3). IPv6 routers do not fragment packets in transit; they discard an over-MTU packet and send ICMPv6 Packet Too Big. A source may use an IPv6 Fragment header when source fragmentation is appropriate (RFC 8200, §§4.5, 5; RFC 8201). ICMPv6 Packet Too Big messages are central to path-MTU discovery. Neighbor Discovery uses ICMPv6 for link-layer neighbor and related discovery functions; it is not IPv4 ARP with a different address length. Blocking essential ICMPv6 can break more than diagnostic pings. Extension headers, tunnel paths and local policy affect the effective MTU and delivery path.

Generic segmentation offload (GSO) and related hardware offload can represent several eventual packets in one larger skb at an observation point. UDP segmentation support and limits are configuration and API dependent. A large locally captured skb is not necessarily one oversized IP packet on the wire, and IP fragmentation is not the same operation as UDP segmentation offload. Checksums shown in local packet captures can also appear incomplete because the NIC or later software is expected to finish them. An observation must say where in the stack it was captured.

IPv4 UDP permits a zero checksum field to indicate that no UDP checksum was supplied (RFC 768); a nonzero checksum is validated under the relevant rules. IPv6 UDP normally requires a checksum; zero-checksum use is permitted only for specific tunnel modes under defined conditions (RFC 8085, §3.4; RFC 6935). The IPv6 UDP source checks zero-checksum acceptance using per-socket settings; do not treat those exceptions as a default for ordinary application traffic. A checksum detects some accidental corruption and covers protocol pseudo-header information; it is not encryption, authentication or a proof that an application received the message.

Address families and error handling#

An IPv6 socket can be configured with IPV6_V6ONLY to restrict it to IPv6, while dual-stack behavior and the default depend on kernel and system configuration. An IPv4-mapped IPv6 address is an API representation, not evidence that an IPv4 packet became an IPv6 packet on the wire. Explicitly record socket family, bound address and V6ONLY state in tests. IPv6 loopback ::1 may be unavailable in a restricted environment even when the machine's kernel contains IPv6 source code.

For a hypothetical UDP send of 2000 payload bytes across a path with a 1500-byte MTU, headers, options and offload state determine whether the send is fragmented, rejected, segmented under an explicitly enabled mechanism, or handled differently. The bare numbers do not prove one outcome. If path-MTU discovery returns an error, the application can reduce its payload or adapt its protocol rather than assuming the kernel will always fragment. If a UDP receiver's queue is full, a datagram may be dropped; later data can still arrive because UDP has no built-in retransmission or stream gap repair.

Ownership is also conditional. Once the stack accepts an skb for a later stage, the prior layer follows that stage's ownership contract; a drop path frees or reports the skb under its own rules. Receive lookup may use RCU and socket references; a returned socket pointer cannot be retained beyond the protection actually acquired. Protocol locks and queue accounting protect state independently from skb data references. Read the target branch before adding an asynchronous callback that stores either pointer.

Socket queues, peeking, and errors#

A UDP receive queue holds datagrams, each with metadata and payload, until the application consumes them or the kernel drops them. If the reader is slower than arrivals, the socket's memory accounting and queue limits can cause drops. The kernel does not retransmit those lost datagrams on behalf of UDP. A larger receive buffer changes a resource limit, not the reliability contract. If an application uses sequence numbers to detect gaps, it must also account for reordering and duplicates rather than concluding that every missing sequence number is permanently lost immediately.

MSG_PEEK changes consumption behavior: it permits inspecting queued data without consuming it under the interface's rules. The off variable in the quoted udp_recvmsg code participates in that handling, so a zero-offset normal receive and a peek are not identical. The standard eight-byte example assumes a normal consuming receive and no special peek behavior. A too-small ancillary buffer is another kind of truncation, distinct from payload truncation; applications using control messages must inspect the corresponding flags and close any received resources when relevant.

Connected UDP stores a default destination and can constrain received peer traffic, but it still sends datagrams. The kernel can associate an asynchronous error with a socket or its extended error queue when enabled; an error observed on a later call need not mean that the immediately preceding datagram was the only possible cause. A timeout waiting for a UDP response does not prove whether the request was lost, the reply was lost, or the peer chose not to respond. The application needs a bounded retry and deduplication design if duplicate requests are harmful.

Checksums and reassembly in context#

The UDP checksum includes a pseudo-header derived from IP-layer fields as well as the UDP header and data. That ties the checksum to addresses and protocol information, but it remains an error-detection mechanism rather than a cryptographic message authentication code. In the IPv4 receive implementation, the stack first checks that the UDP header is present and that its length is consistent before accepting the packet for later delivery. A bad length or checksum can cause a drop; an application waiting on its socket does not necessarily receive a descriptive error for each dropped incoming packet.

Fragments of one IP packet need enough consistent identifying information for the destination's reassembly logic to associate them. Reassembly consumes time and memory and is subject to resource limits; losing a fragment can cause the whole datagram to be unavailable to UDP. IPv6 extension headers and IPv4 options can make header parsing and filtering more complicated, and implementations can have configured limits. A large application payload should therefore be designed with a path-MTU strategy instead of assuming that fragmentation will always work. The exact maximum UDP payload depends on IP version and protocol details, and local send limits do not guarantee that every intermediate network supports the resulting packet.

For the lab, all packets remain on loopback and are intentionally tiny. It demonstrates a socket-level truncation flag and a zero-length datagram; it does not demonstrate IP fragmentation, checksum offload or packet loss. A source-only reading of udp_recvmsg still teaches the returned flag when the IPv6 loopback address is unavailable. An application should report that environmental limitation rather than trying to change host IPv6 settings to make the test pass.

Port lookup and a missing receiver#

A UDP destination port is only one input to socket lookup. The network namespace, destination address, source address and port for a connected socket, bound device, address-family behavior and reuse-port grouping can all influence the selected receiver. A packet can pass IP validation but fail to find an eligible socket. Depending on address type, policy and protocol path, the stack may generate an ICMP error, drop the packet or handle it differently; the sender cannot depend on receiving an error for every missing receiver. A connected UDP socket can surface some asynchronous errors, but that does not provide an acknowledgement protocol.

When a matching socket exists, packet ownership moves to or through its receive-queue machinery under socket memory limits. If queueing fails, the implementation accounts and frees the skb through its failure path. Once udp_recvmsg retrieves an skb, it must verify or complete required checksum work and copy to the iterator; a userspace-copy fault can fail the receive. MSG_PEEK changes whether a successful operation consumes the datagram. For the worked eight-byte example, the no-peek assumption is essential: a truncated ordinary receive consumes that datagram, while a separate peek experiment has different state transitions.

Practice#

Source-only route:

sh
rg -n -A35 'int udp_sendmsg\(' "$KERNEL_SRC/net/ipv4/udp.c"
rg -n -A48 'int udp_recvmsg\(' "$KERNEL_SRC/net/ipv4/udp.c"
rg -n -A45 'int __udp4_lib_rcv\(' "$KERNEL_SRC/net/ipv4/udp.c"
rg -n -A12 'int ip_local_out\(' "$KERNEL_SRC/net/ipv4/ip_output.c"

Verify the checkout tag is v6.12. Identify the length/flag errors, truncation flag, UDP header validation and IP output continuation. Separately inspect udpv6_rcv in net/ipv6/udp.c; lack of an IPv6 runtime route does not prevent that source exercise.

Run this bounded own-process experiment on IPv4 loopback, then IPv6 loopback if available:

sh
python3 - <<'PY'
import socket
for family, address in [(socket.AF_INET, '127.0.0.1'), (socket.AF_INET6, '::1')]:
    r = s = None
    try:
        r = socket.socket(family, socket.SOCK_DGRAM); r.settimeout(1)
        s = socket.socket(family, socket.SOCK_DGRAM); s.settimeout(1)
        r.bind((address, 0)); s.sendto(b'abcdefgh', r.getsockname())
        data, anc, flags, peer = r.recvmsg(4)
        print(address, repr(data), 'truncated:', bool(flags & socket.MSG_TRUNC))
        s.sendto(b'', r.getsockname())
        data, anc, flags, peer = r.recvmsg(4)
        print(address, 'zero datagram:', repr(data))
        r.setblocking(False)
        try: r.recv(1)
        except BlockingIOError: print(address, 'empty queue: EAGAIN')
    except OSError as e:
        print(address, 'unavailable:', e)
    finally:
        if s is not None: s.close()
        if r is not None: r.close()
PY

For each available family, expect b'abcd' truncated: True, then zero datagram: b'', then empty queue: EAGAIN. The final nonblocking receive shows the discarded remainder is not a second message. A socket-creation, bind or send failure is reported for that family; no external destination or DNS is used. Deliver the output or unavailability and a diagram of one datagram through validation, queue and receive truncation.

Knowledge check#

  1. What happens to the unreturned four bytes in the eight-byte UDP example?
  2. Why is b'' from a UDP receive not necessarily EOF?
  3. What does IPv6 do when a router encounters a packet larger than the next link's MTU?
  4. What does a zero UDP checksum mean for IPv4, and what is the ordinary IPv6 rule?
  5. Why is a connected UDP socket still not TCP?

Answers#

  1. A normal consuming truncated receive discards the rest of that datagram; the next receive waits for another datagram.
  2. It can be a valid zero-length datagram; stream EOF has different semantics.
  3. An IPv6 router does not fragment it in transit; path-MTU handling can use ICMPv6 Packet Too Big and source-side adaptation.
  4. In IPv4 a zero field can indicate no UDP checksum; IPv6 UDP normally requires one, with narrow explicitly configured tunnel exceptions.
  5. UDP connect sets a peer and affects socket behavior but does not add TCP's handshake, ordered stream, acknowledgements or retransmissions.

Checkpoint#

Save the loopback results and the source diagram. Pass when you distinguish datagram truncation from a stream short read, IPv4 fragmentation from IPv6 path MTU, and checksum validation from authentication. If IPv6 is unavailable, document that result and complete the source-only comparison. This is practice, not certification.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.