The Puzzle
TLPI Exercise 59-5 poses a specific question: if you create two UDP (datagram) sockets, bind both to specific addresses, and connect socket A to socket B — what happens when a third socket tries to send a datagram to socket A using sendto()?
To answer this properly, you need to understand what connect() actually does to a UDP socket. The answer is surprising if you’ve only ever used connect() with TCP.
What Does connect() Do to a UDP Socket?
For TCP, connect() initiates a real three-way handshake and establishes a connection. For UDP, it’s completely different — there is no handshake, no connection.
| Aspect | TCP connect() | UDP connect() |
|---|---|---|
| Network activity | SYN, SYN-ACK, ACK (3-way handshake) | NONE — no packets sent |
| What it stores | Remote address in kernel, allocates buffers | Just records the peer address in the socket |
| Effect on send | Can use send() / write() (no need for sendto) | Can use send() / write(); sendto() with NULL addr |
| Effect on recv | Only receives from connected peer | Filters incoming datagrams — only accepts from peer |
| Can disconnect | No (must close and reopen) | Yes — connect() with AF_UNSPEC to “un-connect” |
The most important effect of connect() on a UDP socket is the incoming filter. The kernel will silently discard any datagram that arrives from a source other than the connected peer. This is the direct answer to the exercise question.
Setting Up the Experiment
Connected to B (:5002)
tries sendto(A)
recvfrom(), the kernel checks the source address. Since C’s address (port 5003) does NOT match A’s connected peer (port 5002), the kernel silently discards the datagram. A’s recvfrom() blocks as if nothing arrived.The Experiment — Full Code
/* udp_connect_test.c
*
* Creates three UDP sockets A, B, C:
* - A bound to :5001, connected to B (:5002)
* - B bound to :5002
* - C bound to :5003
*
* Then has C send a datagram to A.
* Demonstrates that A does NOT receive it (filtered by kernel).
*
* Then has B send a datagram to A.
* Demonstrates that A DOES receive it (from connected peer).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/* Create and bind a UDP socket to localhost:port */
static int make_udp_socket(int port)
{
int fd;
struct sockaddr_in addr;
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) { perror("socket"); exit(EXIT_FAILURE); }
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* 127.0.0.1 */
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind"); exit(EXIT_FAILURE);
}
return fd;
}
/* Make the socket non-blocking so recvfrom() doesn't block forever */
static void set_nonblocking(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main(void)
{
int fd_a, fd_b, fd_c;
struct sockaddr_in addr_b;
const char *msg_from_c = "hello from C";
const char *msg_from_b = "hello from B";
char buf[256];
ssize_t n;
/* --- Step 1: Create and bind all three sockets --- */
fd_a = make_udp_socket(5001); /* Socket A on :5001 */
fd_b = make_udp_socket(5002); /* Socket B on :5002 */
fd_c = make_udp_socket(5003); /* Socket C on :5003 */
printf("Created: A(:5001), B(:5002), C(:5003)\n\n");
/* --- Step 2: Connect A to B --- */
memset(&addr_b, 0, sizeof(addr_b));
addr_b.sin_family = AF_INET;
addr_b.sin_port = htons(5002);
addr_b.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (connect(fd_a, (struct sockaddr *)&addr_b, sizeof(addr_b)) == -1) {
perror("connect A to B"); exit(EXIT_FAILURE);
}
printf("Connected A to B\n");
printf("Effect: A will now FILTER incoming datagrams — only accepts from B\n\n");
/* Make A non-blocking so we can test without hanging */
set_nonblocking(fd_a);
/* --- Step 3: C tries to send to A --- */
struct sockaddr_in addr_a;
memset(&addr_a, 0, sizeof(addr_a));
addr_a.sin_family = AF_INET;
addr_a.sin_port = htons(5001);
addr_a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
printf("=== TEST 1: C sends datagram to A ===\n");
n = sendto(fd_c, msg_from_c, strlen(msg_from_c), 0,
(struct sockaddr *)&addr_a, sizeof(addr_a));
printf("C: sendto(A) returned %zd (datagram sent to kernel)\n", n);
/* Small delay to let kernel process it */
usleep(10000);
/* A tries to receive */
n = recvfrom(fd_a, buf, sizeof(buf) - 1, 0, NULL, NULL);
if (n == -1 && errno == EAGAIN) {
printf("A: recvfrom() returned EAGAIN — NO DATA (datagram from C was FILTERED)\n");
printf("Reason: A is connected to B(:5002); C is at :5003 — not the peer!\n\n");
} else if (n > 0) {
buf[n] = '\0';
printf("A: recvfrom() got: '%s' (unexpected!)\n\n", buf);
}
/* --- Step 4: B sends to A --- */
printf("=== TEST 2: B sends datagram to A ===\n");
n = sendto(fd_b, msg_from_b, strlen(msg_from_b), 0,
(struct sockaddr *)&addr_a, sizeof(addr_a));
printf("B: sendto(A) returned %zd\n", n);
usleep(10000);
n = recvfrom(fd_a, buf, sizeof(buf) - 1, 0, NULL, NULL);
if (n > 0) {
buf[n] = '\0';
printf("A: recvfrom() got: '%s' — RECEIVED (from connected peer B)\n\n", buf);
} else if (errno == EAGAIN) {
printf("A: recvfrom() returned EAGAIN — No data\n\n");
}
/* --- Step 5: A sends to B (using write() — no address needed after connect) --- */
printf("=== TEST 3: A sends to B using write() (no sendto needed) ===\n");
n = write(fd_a, "reply from A", 12);
printf("A: write() returned %zd\n", n);
usleep(10000);
n = recvfrom(fd_b, buf, sizeof(buf) - 1, 0, NULL, NULL);
if (n > 0) {
buf[n] = '\0';
printf("B: recvfrom() got: '%s'\n", buf);
}
printf("\n=== CONCLUSION ===\n");
printf("connect() on UDP socket:\n");
printf(" 1. Records the peer address (B:5002) — no packets sent\n");
printf(" 2. Allows send()/write() without specifying destination\n");
printf(" 3. FILTERS incoming datagrams — only from peer (B) are accepted\n");
printf(" 4. Datagrams from non-peer (C) are silently discarded\n");
close(fd_a);
close(fd_b);
close(fd_c);
return 0;
}
Build and Expected Output
gcc -Wall -o udp_connect_test udp_connect_test.c
./udp_connect_test
Created: A(:5001), B(:5002), C(:5003)
Connected A to B
Effect: A will now FILTER incoming datagrams — only accepts from B
=== TEST 1: C sends datagram to A ===
C: sendto(A) returned 12 (datagram sent to kernel)
A: recvfrom() returned EAGAIN — NO DATA (datagram from C was FILTERED)
Reason: A is connected to B(:5002); C is at :5003 — not the peer!
=== TEST 2: B sends datagram to A ===
B: sendto(A) returned 12
A: recvfrom() got: 'hello from B' — RECEIVED (from connected peer B)
=== TEST 3: A sends to B using write() (no sendto needed) ===
A: write() returned 12
B: recvfrom() got: 'reply from A'
=== CONCLUSION ===
connect() on UDP socket:
1. Records the peer address (B:5002) — no packets sent
2. Allows send()/write() without specifying destination
3. FILTERS incoming datagrams — only from peer (B) are accepted
4. Datagrams from non-peer (C) are silently discarded
Another Benefit: ICMP Error Delivery
Unconnected UDP sockets silently ignore ICMP error messages (like “port unreachable”). Connected UDP sockets receive these errors back as a return value from send() or recvfrom().
/* Unconnected UDP — ICMP errors are INVISIBLE */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
/* No connect() */
sendto(fd, buf, len, 0, &dest, sizeof(dest));
/* If dest port is closed, kernel gets ICMP "port unreachable" */
/* But without connect(), sendto() doesn't return an error! */
/* The next sendto() or recvfrom() might fail — or might not */
/* Connected UDP — ICMP errors come BACK to you */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
connect(fd, &dest, sizeof(dest)); /* No packets sent */
send(fd, buf, len, 0);
/* If dest port is closed: kernel gets ICMP "port unreachable" */
/* The NEXT send()/recvfrom() returns -1, errno == ECONNREFUSED */
/* This is how you detect "nobody is listening on that port" with UDP */
/* Example: */
ssize_t n = send(fd, "ping", 4, 0);
if (n == -1) perror("send"); /* May get ECONNREFUSED if peer not listening */
n = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
if (n == -1 && errno == ECONNREFUSED) {
printf("No one is listening on that port!\n");
}
App never knows!
next send(): ECONNREFUSED
Disconnecting a UDP Socket
Unlike TCP where you must close and reopen to disconnect, a UDP socket can be “un-connected” by calling connect() with a null address (AF_UNSPEC). After this, the socket accepts datagrams from any source again.
/* Disconnect a UDP socket (remove the peer association) */
struct sockaddr_unspec {
sa_family_t sa_family;
};
/* Method 1: Use AF_UNSPEC */
struct sockaddr addr;
memset(&addr, 0, sizeof(addr));
addr.sa_family = AF_UNSPEC;
connect(fd, &addr, sizeof(addr)); /* "Un-connect" */
/* Method 2: Simpler on Linux */
struct sockaddr_in null_addr;
memset(&null_addr, 0, sizeof(null_addr));
null_addr.sin_family = AF_UNSPEC;
connect(fd, (struct sockaddr *)&null_addr, sizeof(null_addr));
/*
* After un-connecting:
* - The socket accepts datagrams from ANY source again
* - You must use sendto() with explicit destination again
* - write()/send() without destination will fail (EDESTADDRREQ)
*
* Useful scenario: a UDP client that normally talks to server A
* but needs to temporarily talk to server B, then return to A.
* You can connect/un-connect/re-connect without closing the socket.
*/
When Should You Use Connected UDP?
write()/send() without specifying the destination each time, receives ICMP errors, and rejects datagrams from other sources (preventing spoofed packets).recvfrom() to get the client’s address, then sendto() to reply. It cannot be “connected” to a single peer.sendto() Rules on Connected Sockets
/*
* sendto() behavior on connected UDP sockets:
*
* Rule 1: sendto() with NULL destination — OK (uses connected peer)
*/
ssize_t n = sendto(fd, buf, len, 0, NULL, 0);
/*
* Rule 2: sendto() with SAME destination as connect() — OK (redundant)
*/
ssize_t n = sendto(fd, buf, len, 0,
(struct sockaddr *)&peer_addr, sizeof(peer_addr));
/* This works but is redundant; prefer write() or send() */
/*
* Rule 3: sendto() with DIFFERENT destination — ERROR on most systems
* On Linux: returns EISCONN ("Transport endpoint is already connected")
*/
struct sockaddr_in other_addr;
/* fill in different address... */
ssize_t n = sendto(fd, buf, len, 0,
(struct sockaddr *)&other_addr, sizeof(other_addr));
/* n == -1, errno == EISCONN */
printf("Error: %s\n", strerror(errno)); /* Transport endpoint is already connected */
/*
* Summary of send functions on connected UDP socket:
*
* write(fd, buf, len) — OK, uses peer from connect()
* send(fd, buf, len, flags) — OK, uses peer from connect()
* sendto(fd, buf, len, 0, NULL,0) — OK, uses peer from connect()
* sendto(fd, buf, len, 0, &peer, sizeof(peer)) — OK (same addr)
* sendto(fd, buf, len, 0, &other, sizeof(other)) — EISCONN (different addr)
*/
Interview Questions & Answers
Q1. What happens when you call connect() on a UDP socket?
No network packets are sent. The kernel simply stores the remote address (peer address) in the socket structure. This has two effects: outgoing datagrams go to this address automatically (you can use write()/send()), and incoming datagrams from any other address are silently filtered/discarded. It is purely a local kernel operation.
Q2. In Exercise 59-5: what happens when a third socket sends to a connected UDP socket?
The datagram is received by the kernel and placed in socket A’s receive queue. However, when the application calls recvfrom(), the kernel checks the source address. Since it doesn’t match A’s connected peer, the datagram is silently discarded. The recvfrom() call blocks (or returns EAGAIN on a non-blocking socket) as if nothing arrived. The sending application (C) sees no error — sendto() returns success.
Q3. What is ICMP “port unreachable” and when does it occur with UDP?
When a UDP datagram arrives at a host and no process is listening on that port, the kernel sends back an ICMP “port unreachable” message to the sender. For unconnected UDP sockets, this ICMP error is silently discarded. For connected UDP sockets, the error is converted into a return value — the next send() or recvfrom() call returns -1 with errno == ECONNREFUSED.
Q4. Why would you use connected UDP instead of TCP for a client-server application?
When low latency matters more than reliability. UDP has no connection setup (no 3-way handshake), no head-of-line blocking, no retransmission delays. For applications like DNS queries, gaming, video conferencing, or VoIP — where a slightly stale packet is better than a delayed one — UDP wins. Connected UDP is used when you always talk to the same server, giving you the convenience of write/send and ICMP error visibility.
Q5. What error does sendto() return if you try to send to a different address on a connected UDP socket?
On Linux, sendto() returns -1 with errno == EISCONN (“Transport endpoint is already connected”) if you specify a destination address that differs from the connected peer. To send to a different address, you must first disconnect the socket using connect() with AF_UNSPEC, then call sendto() with the new address.
Q6. What is the difference between SOCK_DGRAM and SOCK_STREAM?
SOCK_STREAM (TCP) provides a reliable, ordered, connection-oriented byte stream. Data is guaranteed to arrive, arrive in order, and arrive without duplicates. SOCK_DGRAM (UDP) provides unreliable, connectionless, record-oriented datagrams. Each send is an independent packet that may be lost, reordered, or duplicated. SOCK_STREAM merges data into a stream; SOCK_DGRAM preserves message boundaries.
Q7. How do you “disconnect” a connected UDP socket without closing it?
Call connect() again with a sockaddr whose family is AF_UNSPEC: addr.sa_family = AF_UNSPEC; connect(fd, &addr, sizeof(addr)); This clears the peer address from the socket. The socket then accepts datagrams from any source again, and you must use sendto() with an explicit destination.
Q8. Does connecting a UDP socket send any SYN packet or establish any state on the peer?
No. connect() on a UDP socket is entirely local. No SYN is sent, the peer is not notified, and no state is created on the remote machine. The only change is in the local kernel’s socket structure — it records the peer address. You could connect a UDP socket to a machine that is powered off and connect() would still succeed immediately.
Q9. What does recvfrom() return for the source address on a connected UDP socket?
Even on a connected UDP socket, recvfrom() fills in the source address in its src_addr parameter (if non-NULL). Since connected sockets only receive from the peer, this will always be the peer’s address. You can also use read() or recv() which don’t give you the source address at all — safe shortcut when you know it’s always from the connected peer.
Chapter 59 Complete — What You’ve Learned
| Part | Topic | Key Takeaway |
|---|---|---|
| 1 | Buffered readline | Buffer reads to avoid per-byte system calls over TCP |
| 2 | Internet socket library | inetListen/inetConnect wrap getaddrinfo boilerplate |
| 3 | UNIX domain sockets | Faster IPC, credential passing, needs unlink() cleanup |
| 4 | Name-value server | Protocol design + fork-per-client + shared state challenges |
| 5 | Connected UDP | connect() on UDP filters peers + enables ICMP errors |
Chapter 59 Complete!
Continue with the full Linux System Programming series at EmbeddedPathashala.
