Networking for beginners. Part 9: UDP

All parts of the series “Networking for beginners”

Over eight parts we have learned to deliver a packet from machine to machine: Ethernet to the neighbour, IP through routers, ARP to translate addresses and ICMP for error messages. The packet has arrived at the machine with the right IP address. But who gets it next? There are dozens of programs on the machine, and IP knows nothing about them: its header has a Protocol field, but it only says "UDP inside" or "TCP inside".

The transport layer solves exactly this problem — delivering data not to a machine but to a process on it — and, if needed, adds what IP lacks: reliability, ordering, rate control. The internet has two transports. Let's start with the one that adds the bare minimum to IP: UDP, the User Datagram Protocol, RFC 768. The whole standard is two pages from 1980.


Ports

A process's address at the transport layer is a port, a 16-bit number from 0 to 65535. A process that wants to receive data asks the OS for a port and listens on it; the sender puts the recipient's port in the header. The pair "IP address, port" uniquely identifies a point on the whole planet where data can be sent, and this pair together with the protocol is called a socket.

$ ss -ulnp
State   Recv-Q  Send-Q   Local Address:Port    Peer Address:Port  Process
UNCONN  0       0            127.0.0.53%lo:53         0.0.0.0:*      users:(("systemd-resolve",pid=600,fd=13))
UNCONN  0       0                  0.0.0.0:68         0.0.0.0:*      users:(("dhclient",pid=700,fd=6))
UNCONN  0       0                  0.0.0.0:123        0.0.0.0:*      users:(("ntpd",pid=800,fd=16))

Three processes listen on UDP: the DNS resolver on port 53, only on the address 127.0.0.53; the DHCP client on 68 and the time daemon on 123 — on 0.0.0.0, that is, on all of the machine's addresses. When a datagram arrives, the stack looks at the destination port (and destination address) and hands the data to the socket listening on it. This is the last step of the demultiplexing from the first part: EtherType → Protocol → port.

The range is divided by convention:

Ports Name What
0–1023 well-known assigned to well-known services: 53 DNS, 67/68 DHCP, 123 NTP, 443 HTTPS. On Unix only root can listen on them
1024–49151 registered registered to applications, but without privileges: 3389 RDP, 5060 SIP, 4789 VXLAN
49152–65535 ephemeral temporary: the OS gives them to a sender that does not care about its own port, as long as the reply comes back

In practice Linux takes ephemeral ports from 32768–60999 (net.ipv4.ip_local_port_range). When a browser opens a connection or dig sends a query, its source port is a random one from this range, and the destination port is a well-known one. The reply comes back to that random port, and the stack uses it to find the right process.

Port numbering is per protocol: UDP 53 and TCP 53 are different sockets, and different processes can listen on them.


The header

Eight bytes:

 bytes:    2            2            2          2
        ┌────────────┬────────────┬──────────┬──────────┐
        │ Source     │ Destination│ Length   │ Checksum │   then data
        │ Port       │ Port       │          │          │
        └────────────┴────────────┴──────────┴──────────┘
  • Source Port — the sender's port, where to send the reply. It may be 0 if no reply is expected.
  • Destination Port — to whom.
  • Length — the length of the header and data, from 8 to 65,535. The maximum data in one datagram is 65,507 bytes (65,535 minus 20 bytes of IP and 8 of UDP), but more on that below.
  • Checksum — and there is a subtlety here. It is computed not only over the UDP header and data but also over a pseudo-header: the source and destination addresses, the protocol number and the length from the IP header. The pseudo-header is not transmitted anywhere; it just takes part in the sum. The point is to catch a packet delivered to the wrong machine: if the destination address got corrupted along the way, the checksum will not match, even though the UDP header is intact. We have already seen a side effect: NAT, when changing an address in the IP header, must recompute the UDP checksum as well. In IPv4 the checksum may be skipped by writing 0; in IPv6 it is mandatory.

That is all. No sequence numbers, no acknowledgements, no flags, no state. Sending a datagram means prepending eight bytes to the data and handing it to IP.


"Connectionless" — what it means in practice

UDP has no notion of a connection. Each datagram is a self-contained event: the sender builds it, the stack sends it, and that is where UDP's involvement ends. Consequences:

  • No handshake. The very first datagram carries data. For a request-response exchange this means one packet there and one back, with no preliminary exchange.
  • No acknowledgements or retransmissions. Lost is lost. The sender will not find out, unless the application itself waits for a reply and retries.
  • No ordering. Two datagrams may arrive in reverse order if they took different paths. Numbering them is the application's job.
  • No rate control. An application can send faster than the network or the receiver can keep up, and the excess is simply dropped. UDP will not slow down in any way.
  • Message boundaries are preserved. This is a plus: one sendto() call — one datagram — one recvfrom() call with the same bytes. TCP, as we will see in the next part, is a stream of bytes without boundaries, and the application has to mark where a message ends itself.
  • One socket for all peers. A DNS server holds one socket on port 53 and receives datagrams from all clients at once through it; the address in each one says who it is from. A TCP server creates a socket per client.

About size. Formally a datagram can carry 65,507 bytes, but IP will then cut it into 45 fragments of 1500, and the loss of any one of them kills the whole datagram (part 6). That is why UDP applications keep datagrams within the MTU: 1472 bytes of data for Ethernet without options, and cautious protocols 1200–1232, leaving headroom for tunnels. For this reason DNS originally limited responses to 512 bytes and asked for anything larger over TCP.

What happens if nobody listens on the port? The recipient's stack replies with ICMP Port Unreachable (type 3 code 3) — the very message traceroute used to learn it had arrived. If the sender's socket is "connected" to an address with connect(), it will get an ECONNREFUSED error on the next operation; if not, the stack discards the error and the application simply never gets a reply.


Who it suits

It may seem that a protocol without guarantees is only needed where nobody cares about the data. In fact UDP is chosen when TCP's guarantees get in the way or when it is simpler to build them yourself.

  • DNS (part 12): one query, one response, usually in a single datagram. A TCP handshake would cost more than the exchange itself. The resolver handles a loss by retrying a second later.
  • DHCP (part 11): the client has no IP address yet and sends its request to the broadcast address. There is nobody to establish a connection with and nothing to establish it from: TCP is impossible here in principle.
  • NTP — time synchronisation: the client sends a packet with a timestamp, the server returns it with its own timestamps, and the clock offset is computed from the four timestamps. Retransmission via TCP would introduce delay, which matters more than the data itself.
  • Real-time voice and video: a packet that arrives late is useless; it is better to lose a frame than to wait for it to be retransmitted. For this there is RTP over UDP — a header with a sequence number and timestamp the receiver uses to restore order and notice losses, without asking for retransmission.
  • Games — for the same reason: what matters is the latest state, not all the intermediate ones.
  • syslog, SNMP, metrics: "fire and forget"; losing one record is tolerable, while the overhead of connections from thousands of devices is not.
  • QUIC — a transport over UDP on which HTTP/3 runs, the third version of the protocol a browser uses to request pages. It does, by itself, in user space, everything TCP does: acknowledgements, retransmissions, congestion control, plus built-in encryption and several independent streams in one connection, so that losing a packet of one stream does not stall the others. On top of UDP rather than with a new protocol number, because NATs and firewalls would not let a new protocol through, while everyone lets UDP through.
  • Tunnels: VXLAN (part 18) and IPsec through NAT (part 19) wrap other packets in UDP, because UDP adds no logic of its own and does not break the logic of what is inside.

The general rule: UDP is used where each datagram is self-sufficient, where latency matters more than completeness, where broadcast or multicast is needed (TCP does not exist for them: it always has exactly two parties), and where the application knows better than the stack what to do about a loss.

The flip side of having no handshake: nobody verifies the source address. You can send a DNS server a query on behalf of a victim, and the response, tens of times larger than the query, goes to the victim. That is how amplification attacks work, and the defence against them lies not in UDP but with ISPs: not letting packets with other networks' source addresses out of their own network.


UDP behind NAT

Back to NAT from the sixth part. There the translation table was filled on the first outgoing packet, and TCP, to jump ahead, has an explicit start and end of a connection, from which NAT understands when to create an entry and when to remove it. UDP has neither. So NAT creates an entry on the first outgoing datagram and removes it on a timer: in Linux after 30 seconds if traffic went only outwards, and after 180 if there were replies too. An application that waits longer than that for datagrams from a server must send something itself so that the entry does not die. That is why VPN clients and voice apps send keepalives every 20–25 seconds: not for the server, but for the NAT along the way.

Two devices behind different NATs

The task: two phones, each behind its own home router with NAT, want to exchange voice directly without routing it through a server. Neither can start: an incoming datagram with no entry in the table is dropped. The solution is hole punching, and it rests on two facts: an entry is created by an outgoing datagram, and for most home NATs that entry does not depend on whom the datagram is addressed to.

          A  192.168.1.10:5000                               B  10.0.0.7:5000
          │                                                        │
    NAT-A │ 198.51.100.2:40001                    203.0.113.5:40002│ NAT-B
          │                                                        │
          │ (1) datagram to server S ──►  S  ◄── datagram to S     │
          │ (2) S tells A: "you appear as 198.51.100.2:40001;      │
          │     B is 203.0.113.5:40002", and vice versa for B      │
          │                                                        │
          │ (3) A sends to 203.0.113.5:40002 ─────────────────────►│  NAT-B doesn't know A yet — drops it
          │◄─────────────────────── B sends to 198.51.100.2:40001  │  NAT-A already has an entry for B: gets through
          │                                                        │
          │ (4) A again ──────────────────────────────────────────►│  now NAT-B lets it through too
  1. Both send a datagram to an intermediary server S, which has a public address. Each NAT creates an entry and rewrites the address.
  2. The server sees where the datagrams came from — the public addresses and ports after translation — and tells both sides. This part is called STUN: a protocol in which the server simply returns to the client the address and port from which it received the client's datagram.
  3. Both sides simultaneously send datagrams to each other's public address. The first datagram from A to B hits NAT-B, which has no entry for A yet. But it has also created an entry in NAT-A: "A:5000 ↔ 40001, for 203.0.113.5:40002". The datagram from B, arriving at 40001, matches that entry and gets through.
  4. The next datagram from A gets through NAT-B as well: it already has an entry from B's outgoing datagram.

The hole is punched, and from then on both sides talk directly, refreshing the entries with keepalives.

This does not work with every NAT. If a NAT assigns different external ports for different destinations (such a NAT is called symmetric; CGNAT often behaves this way), the port the server saw will not match the port used for the peer, and it cannot be predicted. Then what remains is TURN — a relay server through which all traffic flows, and the call becomes more expensive and slower. The procedure that tries the candidates "directly, via STUN, via TURN" and picks the best working path is called ICE, and that is exactly what a browser does in the first seconds of a video call.


Seeing it for yourself

Who listens on UDP and who is talking right now:

$ ss -ulnp          # listening
$ ss -uanp          # all, including clients' "connected" sockets

The most accessible UDP exchange is a DNS query. Capture in one window, dig example.com in the other:

$ sudo tcpdump -i eth0 -n udp port 53
12:00:00.000000 IP 192.168.1.10.40321 > 192.168.1.1.53: 12345+ A? example.com. (29)
12:00:00.012345 IP 192.168.1.1.53 > 192.168.1.10.40321: 12345 1/0/0 A 203.0.113.10 (45)

Source port 40321 is ephemeral, destination port 53, and the reply came back to 40321. 29 and 45 are the UDP data lengths. Nothing between the query and the reply: no handshake, no acknowledgements.

Send a datagram by hand and see what happens with a closed port:

$ echo hello | nc -u -w1 192.168.1.1 9999

and in a tcpdump -n 'udp port 9999 or icmp' capture:

IP 192.168.1.10.51000 > 192.168.1.1.9999: UDP, length 6
IP 192.168.1.1 > 192.168.1.10: ICMP 192.168.1.1 udp port 9999 unreachable, length 42

The datagram arrived, nobody was listening, the stack replied with ICMP. If no ICMP arrived but the port is definitely closed, there is a firewall between you that drops it (part 8). UDP port scanning is based on the same thing: no reply means the port is "open or filtered", and the scanner cannot give an unambiguous answer.

You can find out how you appear from the internet with any STUN client, for example stunclient stun.example.com from the stuntman package, or simply by opening a WebRTC test page in the browser (WebRTC is the browsers' built-in calling mechanism, which performs ICE): it will show your public address and port after NAT.


Key takeaways

  • The transport layer delivers data to a process: the process's address is a port, and the pair "address, port" plus the protocol is a socket. The sender takes an ephemeral port, the recipient listens on a well-known one.
  • The UDP header is 8 bytes: two ports, a length, and a checksum with a pseudo-header made of the IP addresses (which is why NAT recomputes it).
  • Connectionless: no handshake, no retransmissions, no ordering, no throttling, but message boundaries are kept and one socket serves everyone. Datagrams are kept within the MTU.
  • UDP is chosen when latency matters more than completeness, when broadcast or multicast is needed, when the application itself knows what to do about a loss, or when it builds its own transport on top (QUIC).
  • Behind NAT an entry lives on a timer, hence keepalives; hole punching via STUN opens a direct path between two NATs, and if that fails, a TURN relay.

Next — Part 10: TCP, the longest part of the series: the header, sequence numbers, the three-step connection setup and four-step teardown, how reliability is achieved, what the window and slow start are and why losing one packet cuts the speed, and one whole connection in Wireshark from SYN to FIN.

Only registered users can leave a comment.

Sign in or register to leave a comment.