Identifying a Process on the Network
IP addresses identify a machine on the network. But a machine runs many processes β web server, SSH server, database, email β all at the same time. How does the OS know which process to deliver a packet to?
The answer is port numbers. Together, an (IP address + port number) uniquely identifies a communication endpoint. This is called a socket address. Two socket addresses (source + destination) uniquely identify a connection β called a socket pair.
A port number is a 16-bit unsigned integer (0β65535). It identifies a specific service or process within a host. Port numbers are divided into three ranges:
| Range | Name | Who Uses It | Examples |
|---|---|---|---|
| 0 β 1023 | Well-Known / Privileged Ports | System services (root only on Linux) | HTTP:80, HTTPS:443, SSH:22, DNS:53 |
| 1024 β 49151 | Registered Ports | Applications (IANA registered) | MySQL:3306, PostgreSQL:5432, Redis:6379 |
| 49152 β 65535 | Ephemeral / Dynamic Ports | OS assigns automatically to client sockets | Your browser’s outgoing port |
cat /proc/sys/net/ipv4/ip_local_port_rangeDefault: typically 32768 β 60999 on Linux (not exactly 49152+ β Linux uses its own range).
| Port | Protocol | Service | Transport |
|---|---|---|---|
| 20/21 | FTP | File Transfer Protocol (data/control) | TCP |
| 22 | SSH | Secure Shell | TCP |
| 23 | Telnet | Remote terminal (insecure) | TCP |
| 25 | SMTP | Email sending | TCP |
| 53 | DNS | Domain Name System | UDP (+ TCP for large) |
| 67/68 | DHCP | Dynamic Host Configuration | UDP |
| 80 | HTTP | Web (unencrypted) | TCP |
| 110 | POP3 | Email retrieval | TCP |
| 123 | NTP | Network Time Protocol | UDP |
| 443 | HTTPS | Secure Web (TLS) | TCP |
| 3306 | MySQL | MySQL database | TCP |
cat /etc/services β maps service names to port numbersgetservbyname("http", "tcp") β C function to look up port by nameIn C, socket addresses are represented as structures. The key ones are:
#include <sys/socket.h>
#include <netinet/in.h>
/* ---- Generic socket address (base type used in syscalls) ---- */
struct sockaddr {
sa_family_t sa_family; /* Address family: AF_INET, AF_INET6, AF_UNIX */
char sa_data[14]; /* Address data (varies by family) */
};
/* ---- IPv4 socket address ---- */
struct sockaddr_in {
sa_family_t sin_family; /* = AF_INET */
in_port_t sin_port; /* Port number in NETWORK BYTE ORDER */
struct in_addr sin_addr; /* IPv4 address in NETWORK BYTE ORDER */
char sin_zero[8]; /* Padding to match sockaddr size */
};
struct in_addr {
in_addr_t s_addr; /* 32-bit IPv4 address (network byte order) */
};
/* ---- IPv6 socket address ---- */
struct sockaddr_in6 {
sa_family_t sin6_family; /* = AF_INET6 */
in_port_t sin6_port; /* Port (network byte order) */
uint32_t sin6_flowinfo; /* IPv6 flow info (usually 0) */
struct in6_addr sin6_addr; /* 128-bit IPv6 address */
uint32_t sin6_scope_id; /* Scope ID for link-local addresses */
};
/* ---- Generic storage (holds any address type) ---- */
struct sockaddr_storage {
sa_family_t ss_family; /* Address family */
/* ... enough space for any address type (128 bytes total) ... */
};
/* Use this when you don't know if you'll get IPv4 or IPv6 */
Socket system calls like
bind(), connect(), accept() all take a struct sockaddr *. But you fill in a struct sockaddr_in (IPv4) or struct sockaddr_in6 (IPv6). You cast the specific type to the generic type when calling. This is C’s way of doing polymorphism.#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
int main(void) {
/* -------- IPv4 address setup -------- */
struct sockaddr_in addr4;
memset(&addr4, 0, sizeof(addr4)); /* Zero out all fields first! */
addr4.sin_family = AF_INET;
addr4.sin_port = htons(8080); /* hostβnetwork byte order */
/* Method 1: specific IP string */
inet_pton(AF_INET, "192.168.1.100", &addr4.sin_addr);
/* Method 2: bind to all interfaces (server typically does this) */
addr4.sin_addr.s_addr = INADDR_ANY; /* = 0.0.0.0 */
/* Method 3: loopback */
addr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* = 127.0.0.1 */
/* -------- IPv6 address setup -------- */
struct sockaddr_in6 addr6;
memset(&addr6, 0, sizeof(addr6));
addr6.sin6_family = AF_INET6;
addr6.sin6_port = htons(8080);
addr6.sin6_flowinfo = 0;
addr6.sin6_scope_id = 0;
/* Listen on all IPv6 interfaces */
addr6.sin6_addr = in6addr_any; /* = :: */
/* Or loopback */
addr6.sin6_addr = in6addr_loopback; /* = ::1 */
/* Or specific IPv6 address */
inet_pton(AF_INET6, "2001:db8::1", &addr6.sin6_addr);
printf("Setup complete.\n");
return 0;
}
/* Use in bind() β cast to (struct sockaddr *) */
/* IPv4: */
bind(sockfd, (struct sockaddr *)&addr4, sizeof(addr4));
/* IPv6: */
bind(sockfd, (struct sockaddr *)&addr6, sizeof(addr6));
/* Generic (when you have sockaddr_storage): */
struct sockaddr_storage ss;
/* ... fill ss ... */
bind(sockfd, (struct sockaddr *)&ss, sizeof(ss));
A single TCP connection is uniquely identified by a 5-tuple:
TCP
192.168.1.5
54321
93.184.216.34
80
This 5-tuple is unique per connection. The OS uses it to demultiplex incoming packets to the right socket. This is why a web server on port 80 can handle thousands of simultaneous connections β each has a different source IP and/or source port.
/* See active connections and their 5-tuples on Linux: */
$ ss -tnp
/* Example output:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 192.168.1.5:54321 93.184.216.34:80 users:(("curl",pid=1234))
ESTAB 0 0 192.168.1.5:54322 93.184.216.34:80 users:(("curl",pid=1235))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=999))
*/
A: 0β65535 (16-bit). Three ranges: 0β1023 = well-known/privileged (require root to bind on Linux), 1024β49151 = registered (IANA-assigned for applications), 49152β65535 = ephemeral/dynamic (OS assigns to clients automatically).
A: struct sockaddr is the generic base type used in socket system call parameters. struct sockaddr_in is the IPv4-specific structure you actually fill in. struct sockaddr_in6 is for IPv6. struct sockaddr_storage is large enough to hold any address family β use it when writing address-family-agnostic code (e.g., a server that handles both IPv4 and IPv6). You cast the specific struct to (struct sockaddr *) when calling socket APIs.
A: INADDR_ANY (value = 0, i.e., 0.0.0.0) means “bind to all available network interfaces.” Servers typically use it so they accept connections on any network interface (eth0, wlan0, lo, etc.) on the specified port. If you bind to a specific IP (e.g., 192.168.1.1), only connections arriving on that interface are accepted.
A: Using the 5-tuple: (protocol, source IP, source port, destination IP, destination port). When a packet arrives, the kernel looks up this 5-tuple in its socket table. For a LISTEN socket, it matches on (protocol, destination IP, destination port) only. For ESTABLISHED sockets, all 5 fields must match. This is why thousands of connections to the same server port (e.g., :80) can coexist β they differ in source IP or source port.
A: Ports below 1024 are “privileged ports.” On Linux, only processes with the CAP_NET_BIND_SERVICE capability (traditionally root) can bind to them. This prevents unprivileged users from hijacking system services. Workarounds: run as root, use setcap CAP_NET_BIND_SERVICE on the binary, use a reverse proxy (nginx on port 80 forwards to your app on port 8080), or use iptables port redirect.
