Networking in C · beginner · ~8 min

bind() — claim a local port

- By the end you can fill in a `struct sockaddr_in` correctly and call `bind()` to attach a socket to a specific local address and port. - You can explain what the kernel records when `bind()` succeeds, and why a server needs a fixed, well-known port while a client usually does not. - You can diagnose and fix the two errors every beginner hits: `EADDRINUSE` (with `SO_REUSEADDR`) and `EACCES` (privileged ports). - You can bind lab servers to `INADDR_LOOPBACK` so they are unreachable from the network, and explain why `INADDR_ANY` is a bigger exposure decision. - You can read back the actual bound port with `getsockname()` after binding to port 0.

Overview

You already know from network byte order that multi-byte numbers travelling between machines must be in big-endian order, which is why you wrap ports in htons() and addresses in htonl(). bind() is the first place that knowledge becomes load-bearing: the port and address you hand the kernel live inside a struct sockaddr_in, and if you forget to byte-swap them the kernel will faithfully bind you to the wrong port.

This lesson takes a raw socket file descriptor — an open but unattached endpoint — and gives it a local identity: a specific (address, port) pair on this machine. That identity is what makes a socket reachable. Clients dial (address, port); without a bind(), a server socket has no address to dial. After this you move on to listen()/accept(), which turn a bound socket into one that actually receives connections.

Why it matters

Every server you have ever connected to — a web server, a database, an SSH daemon — called bind() to claim its port. Getting the address argument wrong is a real security decision, not a detail: bind to INADDR_ANY and your service answers on every network interface, including the public one, so a lab tool you thought was private is suddenly exposed to the internet. Bind to INADDR_LOOPBACK and only programs on the same machine can reach it. The classic EADDRINUSE failure after a crash also teaches a durable lesson about TCP's TIME_WAIT state and why SO_REUSEADDR exists.

Core concepts

A socket starts with no address

socket() returns a file descriptor for an endpoint that exists but is anonymous — it has a protocol family and type, but no local address and no local port. bind() is the syscall that stamps a local identity onto it. Think of the socket as a phone that has been powered on but not yet assigned a number; bind() assigns the number.

  socket()                bind(fd, &addr, len)
  -------->  [ fd: TCP ]  ------------------->  [ fd: TCP, 127.0.0.1:8080 ]
             anonymous                          has a local identity

After a successful bind(), the kernel records the pair in its port table, and any packet arriving for (address, port) is routed toward this socket. For a client you normally skip bind() and let connect() auto-assign an ephemeral local port; for a server you bind on purpose so clients know where to find you.

The address structure you pass

For IPv4 you fill a struct sockaddr_in and pass its address, cast to the generic struct sockaddr *. Three fields matter, and two of them need byte-swapping:

Field Meaning How to set it
sin_family Address family AF_INET (host order — it is not sent on the wire)
sin_port Port number htons(8080) — big-endian
sin_addr.s_addr IPv4 address htonl(INADDR_LOOPBACK) or htonl(INADDR_ANY) — big-endian

Always memset() the struct to zero first, because sockaddr_in contains a padding field (sin_zero) that must be clear. The length argument is sizeof(struct sockaddr_in).

Choosing the address decides your exposure

The address you bind is a security boundary, not a formality:

Value Binds to Who can reach it
INADDR_LOOPBACK (127.0.0.1) loopback only only this machine
INADDR_ANY (0.0.0.0) every interface anyone who can route to any of your IPs
a specific interface IP that one NIC hosts on that network

For labs and defensive work, bind INADDR_LOOPBACK by default. INADDR_ANY is convenient but it means a tool you meant to test locally answers to the outside world. Treat switching to INADDR_ANY as a deliberate "I intend this to be network-reachable" decision, ideally paired with a firewall rule.

Knowledge check: you bind to htonl(INADDR_ANY) and start a debug service on port 9000 on your laptop at a coffee shop. Who can potentially connect?

Anyone on the same Wi-Fi (and anyone who can route to your laptop's IP) can reach port 9000, because INADDR_ANY listens on every interface, not just loopback. If you only wanted local testing, INADDR_LOOPBACK would have kept it unreachable from the network. This is exactly why the lesson defaults to loopback.

EADDRINUSE and the TIME_WAIT state

When a TCP connection closes, the endpoint that closed first lingers in TIME_WAIT for a while (tens of seconds to a couple of minutes) so late packets can drain. During that window the (address, port) is still considered in use, so a fresh bind() to the same port fails with EADDRINUSE — you restart your server and it refuses to start.

  run #1: bind :8080  --> serve --> exit
                                     port :8080 held in TIME_WAIT ~60s
  run #2 (immediately): bind :8080  --> EADDRINUSE

The fix is the SO_REUSEADDR socket option, set before bind():

int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);

It tells the kernel "let me re-bind a port that is only held by a lingering TIME_WAIT socket." EADDRINUSE also fires when a different live process genuinely owns the port; SO_REUSEADDR does not (and should not) override that.

EACCES and privileged ports

Ports 1–1023 are "privileged": binding them requires elevated privilege (root, or a capability like CAP_NET_BIND_SERVICE on Linux). Try to bind port 80 as an ordinary user and you get EACCES. For every exercise here, pick a high port such as 8080 so you never need root — running network experiments as root is itself a bad habit.

Syntax notes

#include <sys/socket.h>
#include <netinet/in.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: the descriptor from socket().
  • addr: pointer to your struct sockaddr_in (IPv4), cast to struct sockaddr *.
  • addrlen: sizeof the concrete struct, e.g. sizeof(struct sockaddr_in).
  • Returns 0 on success, -1 on error and sets errno (EADDRINUSE, EACCES, EADDRNOTAVAIL, EINVAL). Always check the return value and use perror/strerror.
int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen);
  • Call with level = SOL_SOCKET, optname = SO_REUSEADDR, optval pointing at an int set to 1, optlen = sizeof(int). Must be done before bind().
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • After binding, fills addr with the socket's actual local address. Essential when you bind to port 0 (kernel-chosen ephemeral port). addrlen is in/out: set it to the buffer size before the call.
struct sockaddr_in {
    sa_family_t    sin_family; /* AF_INET */
    in_port_t      sin_port;   /* port, network byte order */
    struct in_addr sin_addr;   /* .s_addr = IPv4, network byte order */
    /* padding (sin_zero) — memset the struct to zero */
};

No heap allocation is involved here: the struct is a plain local variable and there is nothing to free. Do remember to close(fd) the socket when done.

Lesson

What bind() does

The call looks like this:

bind(fd, &addr, sizeof addr);

It tells the kernel: "this socket owns port N on address A." Here fd is the socket's file descriptor (the integer that identifies an open socket).

After a successful bind(), any packet sent to that (address, port) pair is routed to this socket.

Common errors

EADDRINUSE — the address is already in use.

This happens when another process is already bound to the port. It can also happen on your own program: a previous run may have left a socket in the TIME_WAIT state, which briefly holds the port.

To allow rebinding immediately, set the SO_REUSEADDR socket option:

int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);

EACCES — permission denied.

Ports below 1024 are privileged and require root. For your labs, pick a high port such as 8080.

Code examples

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

/* Bind a TCP socket to a kernel-chosen loopback port, read the port back,
   then prove that a second bind() to the SAME port fails with EADDRINUSE.
   Fully hermetic: loopback only, no root, no real network peer. */
int main(void) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) { perror("socket"); return 1; }

    int yes = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) < 0) {
        perror("setsockopt");
        return 1;
    }

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof addr);
    addr.sin_family      = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* 127.0.0.1, lab-only */
    addr.sin_port        = htons(0);               /* 0 = "kernel, pick one" */

    if (bind(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
        perror("bind");
        return 1;
    }

    /* Ask the kernel which port it actually gave us. */
    struct sockaddr_in bound;
    socklen_t blen = sizeof bound;
    if (getsockname(fd, (struct sockaddr *)&bound, &blen) < 0) {
        perror("getsockname");
        return 1;
    }
    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &bound.sin_addr, ip, sizeof ip);
    unsigned short port = ntohs(bound.sin_port);
    printf("socket 1 bound to %s:%u\n", ip, port);

    /* Now try to steal the same (address, port) with a second socket. */
    int fd2 = socket(AF_INET, SOCK_STREAM, 0);
    if (fd2 < 0) { perror("socket"); return 1; }

    struct sockaddr_in same;
    memset(&same, 0, sizeof same);
    same.sin_family      = AF_INET;
    same.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    same.sin_port        = htons(port);            /* deliberately the taken port */

    if (bind(fd2, (struct sockaddr *)&same, sizeof same) < 0) {
        printf("socket 2 bind to port %u failed as expected: %s\n",
               port, strerror(errno));
    } else {
        printf("socket 2 unexpectedly bound too\n");
    }

    close(fd2);
    close(fd);
    return 0;
}

Line by line

  • socket(AF_INET, SOCK_STREAM, 0): creates an anonymous IPv4/TCP endpoint; the returned fd has no local address yet. We check < 0 because every syscall here can fail.
  • setsockopt(..., SO_REUSEADDR, ...): set before bind() so a lingering TIME_WAIT socket from a prior run won't block us with EADDRINUSE.
  • memset(&addr, 0, sizeof addr): zero the struct so the padding (sin_zero) is clean — skipping this is a classic source of subtle bugs.
  • sin_family = AF_INET: kept in host order because it's interpreted locally, never sent on the wire.
  • sin_addr.s_addr = htonl(INADDR_LOOPBACK): bind to 127.0.0.1 only, so nothing off-machine can reach this socket. htonl because the address is a wire value.
  • sin_port = htons(0): port 0 is a signal — "kernel, assign me any free ephemeral port." Great for hermetic demos and tests because it never collides.
  • bind(fd, (struct sockaddr *)&addr, sizeof addr): the star of the show; the cast to the generic sockaddr * is required by the API; the length is the size of the concrete struct.
  • getsockname(...) + ntohs: because we asked for port 0, we don't know the port until we read it back; getsockname fills in the actual bound address, and ntohs converts the port from network to host order for printing.
  • The second socket deliberately binds the same port and address; the kernel refuses with EADDRINUSE (Address already in use) because a live socket already owns it — proving that SO_REUSEADDR does not let two live sockets share the identical pair.
  • close(fd2); close(fd): release both descriptors; leaking sockets exhausts the process FD limit.

Common mistakes

1. Forgetting to byte-swap the port.

addr.sin_port = 8080;              /* WRONG: host order */

Why it breaks: on a little-endian machine 8080 (0x1F90) is stored byte-reversed, so the kernel binds port 0x901F = 36895. Your client connecting to 8080 never finds you.

addr.sin_port = htons(8080);       /* FIXED */

2. Not zeroing the struct.

struct sockaddr_in a;              /* WRONG: sin_zero/padding is garbage */
a.sin_family = AF_INET; a.sin_port = htons(8080);

Why it breaks: uninitialized padding can confuse comparisons and some stacks; it is undefined-ish and non-portable.

struct sockaddr_in a; memset(&a, 0, sizeof a);   /* FIXED */

3. Ignoring bind()'s return value.

bind(fd, (struct sockaddr *)&a, sizeof a);        /* WRONG: no check */
listen(fd, 16);                                   /* proceeds on a failed bind */

Why it breaks: if bind failed (EADDRINUSE/EACCES) you continue with an unbound socket and get baffling later errors.

if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { perror("bind"); return 1; }  /* FIXED */

4. Setting SO_REUSEADDR after bind().

bind(fd, ...);
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes); /* WRONG: too late */

Why it breaks: the option only affects the bind decision, so setting it afterward does nothing for TIME_WAIT.

setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes); /* FIXED: before bind */
bind(fd, ...);

Debugging tips

  • perror/strerror(errno) first. bind returning -1 with Address already in use (EADDRINUSE), Permission denied (EACCES), or Cannot assign requested address (EADDRNOTAVAIL — you tried an IP no local interface has) each points at a distinct fix.
  • See who owns a port. lsof -i :8080 (macOS/Linux) or ss -ltnp / netstat -anv | grep 8080 shows the process holding it and whether it's in TIME_WAIT.
  • Confirm the actual bound port with getsockname() (as in the demo) rather than assuming; this catches byte-order bugs immediately (you'll see a nonsense port like 36895).
  • Trace the syscall. strace -e trace=bind,setsockopt ./server on Linux (or dtruss on macOS) shows the exact args and errno the kernel returned — invaluable when the C looks right but behaves wrong.
  • Under gdb, break on bind, then print *(struct sockaddr_in *)addr to inspect family/port/addr as the kernel will see them.

Memory safety

  • The length you pass must match the struct you pass: use sizeof(struct sockaddr_in) for an IPv4 sockaddr_in. Passing a too-large or mismatched length can make the kernel read past your struct (EINVAL at best, garbage at worst).
  • The generic struct sockaddr is smaller than sockaddr_in; only ever take the address of a real sockaddr_in (or sockaddr_storage) and cast the pointer — never declare a bare struct sockaddr and write port/address into it, which would overflow it.
  • For getsockname, initialize the in/out addrlen to the buffer size before the call; leaving it uninitialized is undefined behavior and may truncate or overflow.
  • Sockets are a finite resource: every socket() needs a matching close(). Leaking descriptors eventually yields EMFILE (too many open files). There is no heap allocation here, so there is nothing to free, but the FD leak is the analogous hazard.
  • bind() itself is thread-safe per-socket, but never share and bind the same fd from two threads without synchronization; the port table update and the descriptor's state are shared.

Real-world uses

  • Every network server: web servers (nginx, Apache), databases (PostgreSQL binds 5432), SSH, and message brokers all bind() their listening port at startup. Production servers commonly bind INADDR_ANY deliberately and rely on firewalls; internal-only services often bind 127.0.0.1.
  • Local-only admin/metrics endpoints: tools expose a debug or Prometheus endpoint bound to 127.0.0.1 so it is reachable by a local agent but not the internet — the exact defensive pattern this lesson teaches.
  • Test harnesses: binding to port 0 and reading the port back with getsockname() lets test suites spin up servers on collision-free ephemeral ports.
  • Best practice: set SO_REUSEADDR before bind() on long-running servers so restarts are instant; default to the narrowest address that works; keep servers off privileged ports (put a reverse proxy or CAP_NET_BIND_SERVICE in front instead of running as root).

Practice tasks

  1. Bind and report. Modify the example to bind a fixed port 8080 on INADDR_LOOPBACK, print success, and confirm with lsof -i :8080 (or ss -ltn) in another terminal while the program sleeps for 20 seconds before closing.
  2. Reproduce EADDRINUSE. Remove the SO_REUSEADDR line, run a small server that binds 8080 and exits, then immediately run it again. Capture the exact errno message, then add SO_REUSEADDR back and show the second run now succeeds.
  3. Trigger EACCES safely. Attempt to bind port 80 as a normal user, catch the error, print a clear message, and then fall back to binding 8080 automatically.
  4. Ephemeral-port helper. Write a function int bind_ephemeral(void) that creates a socket, binds 127.0.0.1:0, uses getsockname() to discover the port, prints it, and returns the fd — the pattern real test harnesses use.
  5. Exposure audit. Write a program that binds INADDR_ANY:9000, then use getsockname and a comment to explain which interfaces it now answers on; change it to INADDR_LOOPBACK and explain in a printed line what an outside host would now see (a refused connection). Do all testing on your own machine only.

Summary

  • bind() gives an anonymous socket a local identity: a specific (address, port) pair the kernel routes packets to.
  • Fill a zeroed struct sockaddr_in: sin_family = AF_INET, sin_port = htons(port), sin_addr.s_addr = htonl(addr) — byte-swap the port and address.
  • Always check bind()'s return value; EADDRINUSE means the port is taken (often a TIME_WAIT leftover), EACCES means a privileged port (< 1024).
  • Set SO_REUSEADDR before bind() to survive TIME_WAIT on restart.
  • Prefer INADDR_LOOPBACK for lab/local services — INADDR_ANY exposes you on every interface, a deliberate security choice.
  • Bind port 0 plus getsockname() to get a collision-free ephemeral port; use a high port like 8080 to avoid needing root; close() every socket.

Practice with these exercises