SO_REUSEADDR & TCP Connection Identity

 

SO_REUSEADDR & TCP Connection Identity
Chapter 61 ยท Sockets: Advanced Topics โ€” Part 3 of 3
โณ TIME_WAIT state
๐Ÿ”— 4-tuple identity
๐Ÿ”ด EADDRINUSE fix
๐Ÿ“Š 2MSL timeout

The Problem: Server Won’t Restart

You have just killed your TCP server and immediately try to restart it. But instead of starting cleanly, it crashes at bind() with the error:

bind: Address already in use (EADDRINUSE)

This is frustrating and confusing โ€” you killed the server, so why is the port still in use? The answer lies in a TCP state called TIME_WAIT, and the fix is a socket option called SO_REUSEADDR. This page explains the problem from first principles and shows you exactly how to fix it.

Key Terms in This Chapter

SO_REUSEADDR EADDRINUSE TIME_WAIT 2MSL timeout 4-tuple Active close Connection incarnation Well-known port Ephemeral port

๐Ÿ”— How TCP Identifies a Connection โ€” The 4-Tuple

Every TCP connection in the kernel is uniquely identified by a combination of four values, called a 4-tuple:

TCP Connection = Unique 4-Tuple
local IP
+
local port
+
remote IP
+
remote port
TCP requires every active 4-tuple on the system to be unique

Think of this like a phone call. A call is uniquely identified by both your number and the number you called. If the same two numbers are already connected, you cannot open a second identical call โ€” there would be no way to tell them apart.

TCP enforces the same rule: no two simultaneous connections can have the same 4-tuple. This is correct and necessary. The problem is that most OS implementations go stricter than the TCP standard requires.

๐Ÿ”ด Why Linux Is Stricter Than Necessary

The TCP specification only requires the 4-tuple to be unique. A stricter interpretation that most OS implementations (including Linux) apply by default is:

Default Linux rule: A local port cannot be reused (bound with bind()) as long as any connection incarnation using that local port exists on the host โ€” even if the connection is no longer accepting data.

This means that as long as there is a TCP endpoint hanging around in TIME_WAIT state using port 55555, no new socket can bind to port 55555 โ€” even though that old endpoint cannot accept any new connection anyway.

โณ What is TIME_WAIT and Why Does it Exist?

When the side that initiates a close (the active closer) sends its final ACK, TCP does not immediately free the port. Instead, it enters TIME_WAIT and waits there for a period called 2 ร— MSL (2 ร— Maximum Segment Lifetime) โ€” typically 60 to 120 seconds on Linux.

TCP State Machine โ€” Close Sequence
ESTABLISHED
โ†’ send FIN โ†’
FIN_WAIT_1
โ†’ recv ACK โ†’
FIN_WAIT_2
โ†’ recv FIN โ†’
TIME_WAIT
waits 2ร—MSL (~60-120s)
โ†’ timer expires โ†’
CLOSED

TIME_WAIT exists for two important reasons:

  • Delayed packet safety: A stale packet from the old connection might still be wandering the network. TIME_WAIT gives it time to expire before a new connection reuses the same port. If a new connection used the same 4-tuple immediately, the stale packet could corrupt the new connection’s data.
  • Reliable connection termination: The final ACK from the active closer might be lost. TIME_WAIT allows the passive closer to retransmit its FIN, and the active closer to re-send the ACK.

๐Ÿ“‹ The Two Scenarios That Cause EADDRINUSE

There are two common situations where a server restart triggers EADDRINUSE:

Scenario 1 โ€” Server did an active close
  1. Server was serving client
  2. Server called close() or was killed by a signal
  3. Server entered TIME_WAIT on its well-known port
  4. New server instance tries to bind() the same port
  5. โ†’ EADDRINUSE
Scenario 2 โ€” Child still serving a client
  1. Server forked a child to handle a connection
  2. Parent server process exits
  3. Child is still running, using the server’s well-known port
  4. New server instance tries to bind() the same port
  5. โ†’ EADDRINUSE

Note: Clients rarely hit this because they use ephemeral ports (randomly chosen high-numbered ports like 60391) which are unlikely to be in TIME_WAIT. Only servers that bind to a specific well-known port face this.

โœ… The Fix: SO_REUSEADDR

Setting SO_REUSEADDR before calling bind() relaxes the strict Linux rule and brings it closer to what TCP actually requires. With this option set, you can bind to a port even if another connection incarnation using that port is in TIME_WAIT state.

Rule of thumb: Every TCP server should set SO_REUSEADDR on its listening socket before calling bind(). There is almost no downside to doing this.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 55555

int main(void)
{
    int sfd;
    struct sockaddr_in addr;

    sfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sfd == -1) { perror("socket"); exit(EXIT_FAILURE); }

    /* ============================================
     * CRITICAL: Set SO_REUSEADDR BEFORE bind()
     * Without this, restarting the server within
     * the TIME_WAIT period causes EADDRINUSE.
     * ============================================ */
    int optval = 1;
    if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR,
                   &optval, sizeof(optval)) == -1) {
        perror("setsockopt SO_REUSEADDR");
        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_ANY);

    /* This will now succeed even if a previous server
     * left a TIME_WAIT connection on this port */
    if (bind(sfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    if (listen(sfd, 10) == -1) { perror("listen"); exit(EXIT_FAILURE); }

    printf("Server listening on port %d (SO_REUSEADDR enabled)\n", PORT);

    /* Accept loop ... */
    int cfd;
    while ((cfd = accept(sfd, NULL, NULL)) != -1) {
        printf("Client connected\n");
        /* Handle client ... */
        close(cfd);
    }

    close(sfd);
    return 0;
}

โš  What SO_REUSEADDR Does NOT Allow

SO_REUSEADDR is not a free pass to reuse any port at any time. It specifically allows binding to a port that has a connection in TIME_WAIT. It does not allow:

  • Two sockets to simultaneously listen on the same port with the same local address. That would cause real ambiguity about which socket should receive new connections.
  • Binding to a port that has an active ESTABLISHED connection with exactly the same 4-tuple (same local IP, local port, remote IP, remote port). The 4-tuple uniqueness requirement still holds.

What makes TIME_WAIT safe to bypass is that those old connections cannot accept new data โ€” they are just waiting out a timer. The new server’s connections will have different 4-tuples (different remote IP/port from any new client) so there is no real ambiguity.

๐Ÿ“ž The Telephone Switchboard Analogy

The book uses a helpful analogy for understanding how accept() and TCP connections work:

๐Ÿ“ž Phone world
  • Company has one switchboard number
  • Operator answers external calls
  • Transfers each call to an internal extension
  • Multiple external calls identified by: caller number + switchboard number
๐Ÿ’ป TCP world
  • Server has one listening socket (well-known port)
  • accept() handles incoming connections
  • Creates a new connected socket for each client
  • Multiple connections identified by 4-tuple: local addr + remote addr

Just as you can tell two phone calls apart by knowing which number called which extension, TCP tells connections apart by the combination of all four endpoint identifiers. This is why all accepted sockets can share the same local port number โ€” they are distinguished by their different client addresses.

๐Ÿ”Ž Viewing TIME_WAIT Connections in Linux

You can see TIME_WAIT connections with the ss command (modern replacement for netstat):

/* Show all TCP connections in TIME_WAIT state */
$ ss -tan state time-wait

/* Show all listening TCP sockets */
$ ss -tlnp

/* Show all TCP sockets (any state) */
$ ss -tan

Example output showing a TIME_WAIT entry on port 55555:

State    Recv-Q Send-Q Local Address:Port Peer Address:Port
TIME-WAIT 0      0     127.0.0.1:55555  127.0.0.1:60391

This is what causes EADDRINUSE when you try to restart your server. With SO_REUSEADDR, the new bind() call succeeds even when this entry exists.

๐Ÿ’ป Production-Ready TCP Server Template

This template incorporates all the best practices from this chapter:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/tcp.h>

#define PORT      8080
#define BACKLOG   10
#define BUF_SIZE  4096

/* Create and configure a listening TCP socket */
static int create_listening_socket(int port)
{
    int sfd;
    struct sockaddr_in addr;

    sfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sfd == -1) { perror("socket"); return -1; }

    /* Allow reuse of port in TIME_WAIT โ€” always do this for servers */
    int reuse = 1;
    if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR,
                   &reuse, sizeof(reuse)) == -1) {
        perror("setsockopt SO_REUSEADDR");
        close(sfd); return -1;
    }

    /* Optional: disable Nagle for low-latency servers */
    int nodelay = 1;
    if (setsockopt(sfd, IPPROTO_TCP, TCP_NODELAY,
                   &nodelay, sizeof(nodelay)) == -1) {
        perror("setsockopt TCP_NODELAY");
        /* non-fatal; continue anyway */
    }

    memset(&addr, 0, sizeof(addr));
    addr.sin_family      = AF_INET;
    addr.sin_port        = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(sfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("bind");
        close(sfd); return -1;
    }

    if (listen(sfd, BACKLOG) == -1) {
        perror("listen");
        close(sfd); return -1;
    }

    return sfd;
}

/* Handle one client connection โ€” echo server */
static void handle_client(int cfd)
{
    char buf[BUF_SIZE];
    ssize_t nr;

    while ((nr = read(cfd, buf, sizeof(buf))) > 0) {
        if (write(cfd, buf, nr) != nr) {
            fprintf(stderr, "write: short or failed\n");
            break;
        }
    }

    if (nr == -1)
        perror("read");

    close(cfd);
}

int main(void)
{
    int sfd = create_listening_socket(PORT);
    if (sfd == -1) exit(EXIT_FAILURE);

    printf("Echo server listening on port %d\n", PORT);
    printf("Restart it immediately after kill โ€” no EADDRINUSE!\n");

    int cfd;
    while ((cfd = accept(sfd, NULL, NULL)) != -1) {
        printf("New client on fd %d\n", cfd);
        handle_client(cfd);
    }

    if (errno != EINTR)
        perror("accept");

    close(sfd);
    return 0;
}

To verify SO_REUSEADDR works:

/* Terminal 1: run the server */
$ gcc -o echo_server echo_server.c && ./echo_server

/* Terminal 2: connect a client, send data, disconnect */
$ echo "hello" | nc localhost 8080

/* Terminal 1: kill the server (Ctrl+C) then immediately restart */
$ ./echo_server
/* Should start without EADDRINUSE even within 60 seconds of first kill */

๐ŸŽ“ Interview Questions

Q1. What is the TIME_WAIT state in TCP and why does it exist?

TIME_WAIT is the state the active closer enters after sending its final ACK. It lasts for 2ร—MSL (typically 60โ€“120 seconds). It exists for two reasons: (1) to let any delayed packets from the old connection expire before the port is reused, and (2) to allow retransmission of the final ACK if the remote side’s FIN is lost.

Q2. Why does a TCP server get EADDRINUSE when restarted quickly?

The previous server instance may have been the active closer, leaving a TCP endpoint in TIME_WAIT on the server’s well-known port. Linux by default prevents binding a port that has any active connection incarnation, including those in TIME_WAIT. The new server’s bind() call fails with EADDRINUSE.

Q3. What does SO_REUSEADDR do exactly?

It relaxes the default Linux rule about port reuse. With it set, you can call bind() to claim a local port even if a connection in TIME_WAIT is using that local port. It brings Linux’s behaviour closer to the minimal requirement in the TCP specification, which only requires uniqueness of the complete 4-tuple.

Q4. When must SO_REUSEADDR be set โ€” before or after bind()?

Always before bind(). Setting it after bind() has no effect on the current socket. The socket options must be configured on the socket before it is bound to an address.

Q5. What is a TCP 4-tuple?

It is the combination of four values that uniquely identifies a TCP connection: {local IP address, local port, remote IP address, remote port}. TCP requires each active connection to have a unique 4-tuple.

Q6. Does SO_REUSEADDR allow two servers to listen on the same port at the same time?

No. SO_REUSEADDR allows binding to a port that has old TIME_WAIT connections, but it does not allow two sockets to simultaneously listen on the same local address and port. That would create genuine ambiguity about which socket receives new incoming connections.

Q7. Why don’t TCP clients usually see EADDRINUSE?

Clients typically connect from ephemeral ports โ€” randomly chosen high-numbered ports assigned by the OS. These are unique per session and are extremely unlikely to collide with any port currently in TIME_WAIT. Only servers that repeatedly use the same well-known port (like 80 or 55555) run into this issue.

Q8. How do you check whether a port is in TIME_WAIT in Linux?

$ ss -tan state time-wait | grep :55555

This uses ss to show all TCP sockets in TIME_WAIT and filters for port 55555.

Q9. What is 2MSL?

MSL stands for Maximum Segment Lifetime โ€” the maximum time an IP packet (and therefore a TCP segment) can wander the network before it is discarded. The TIME_WAIT period is defined as 2ร—MSL to ensure that any stale packet from the old connection in either direction has expired. On Linux the default is typically 60 seconds (representing 2ร—30s MSL).

Q10. You are writing a TCP server. List three socket options you should always consider setting.

  • SO_REUSEADDR โ€” always set this before bind() to avoid EADDRINUSE on restart.
  • SO_KEEPALIVE โ€” enables the OS to send keepalive probes to detect dead connections when no application-level heartbeat exists.
  • TCP_NODELAY โ€” consider this for low-latency applications where the Nagle algorithm’s coalescing delay is undesirable.

Chapter 61 Complete!
You have covered tcpdump, socket options, and SO_REUSEADDR.

โ† Part 1: tcpdump โ† Part 2: Socket Options

Leave a Reply

Your email address will not be published. Required fields are marked *