Port Numbers
Beginner to Intermediate
3 of 5
The Transport Layer’s Job
The transport layer provides end-to-end communication between applications on different hosts. The IP layer gets a packet to the right machine, but how does the operating system know which application on that machine should receive the packet? That is solved by port numbers.
A port number is a 16-bit unsigned integer (range: 0 to 65535). Combined with an IP address, it forms a socket address that uniquely identifies one endpoint of a network connection. Both TCP and UDP use port numbers for this purpose.
Port numbers are divided into three ranges, each managed differently:
| Category | Range | Assigned By | Description |
|---|---|---|---|
| Well-Known Ports | 0 – 1023 | IANA (strict) | Permanently assigned to standard services. Require an approved RFC. Privileged on Linux (root needed). |
| Registered Ports | 1024 – 41951 | IANA (less strict) | Allocated to developers on a less stringent basis. Availability is not guaranteed for registered purpose. |
| Dynamic / Private | 49152 – 65535 | Local use | Intended for ephemeral (short-lived) ports assigned by OS to client sockets automatically. |
Well-known ports are permanently reserved for specific standard services. Here are some important examples:
| Service | Protocol | Port | Purpose |
|---|---|---|---|
| FTP (data) | TCP | 20 | File Transfer Protocol — data transfer |
| FTP (control) | TCP | 21 | File Transfer Protocol — commands |
| SSH | TCP | 22 | Secure Shell — encrypted remote login |
| Telnet | TCP | 23 | Unencrypted remote terminal |
| DNS | UDP/TCP | 53 | Domain Name System — name resolution |
| HTTP | TCP | 80 | HyperText Transfer Protocol — web |
| HTTPS | TCP | 443 | HTTP over TLS — secure web |
Note that the same well-known port number is usually registered under both TCP and UDP even if a service uses only one protocol. This avoids confusion across protocols.
On Linux, ports in the range 0 to 1023 are privileged. Only a process with the CAP_NET_BIND_SERVICE capability (traditionally root) can bind to these ports. This is a security measure:
| Normal User Process
Tries to bind to port 22 (SSH) ❌ bind() fails with EACCES |
vs | Root / CAP_NET_BIND_SERVICE
Tries to bind to port 22 (SSH) ✅ bind() succeeds |
On modern Linux, you can grant just this specific capability to a binary without making it fully root: setcap cap_net_bind_service=+ep /path/to/program
When a client creates a socket and does not call bind(), the OS automatically assigns a temporary (ephemeral) port number. The client does not care which port it uses — it just needs a port for the server to send replies back.
| Client IP: 192.168.1.100 Port: 54321 (auto-assigned ephemeral) |
SYN ──────────────→
← SYN-ACK
ACK ──────────────→
|
Server IP: 10.0.0.5 Port: 80 (well-known, manually bound) |
The server knows where to reply because the client’s ephemeral port is included in every packet as the source port. On Linux, the range of ephemeral ports is controlled by:
# View current ephemeral port range on Linux
cat /proc/sys/net/ipv4/ip_local_port_range
# Typical output: 32768 60999
# Change the range (temporary, resets on reboot)
echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range
Binding to port 0 explicitly also triggers ephemeral port assignment: addr.sin_port = htons(0); — the OS picks a free port automatically.
#include <stdio.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
struct servent *svc;
/*
* getservbyname() looks up a service by name in /etc/services
* Returns struct servent with port in network byte order
*/
svc = getservbyname("ssh", "tcp");
if (svc != NULL) {
printf("Service: %-10s Port: %d Protocol: %s\n",
svc->s_name,
ntohs(svc->s_port), /* convert to host byte order */
svc->s_proto);
}
svc = getservbyname("http", "tcp");
if (svc != NULL) {
printf("Service: %-10s Port: %d Protocol: %s\n",
svc->s_name,
ntohs(svc->s_port),
svc->s_proto);
}
/*
* getservbyport() looks up a service by port number
* Port must be in NETWORK byte order
*/
svc = getservbyport(htons(443), "tcp");
if (svc != NULL) {
printf("Port 443 = Service: %s\n", svc->s_name);
}
return 0;
}
/* Output:
Service: ssh Port: 22 Protocol: tcp
Service: http Port: 80 Protocol: tcp
Port 443 = Service: https
*/
Finding Your Ephemeral Port After bind(port=0)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main() {
int fd;
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
fd = socket(AF_INET, SOCK_STREAM, 0);
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(0); /* 0 = let OS pick */
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
/*
* Use getsockname() after bind() to discover which
* ephemeral port the OS assigned.
*/
getsockname(fd, (struct sockaddr *)&addr, &addrlen);
printf("OS assigned ephemeral port: %d\n", ntohs(addr.sin_port));
close(fd);
return 0;
}
CAP_NET_BIND_SERVICE capability (traditionally superuser/root) can bind to ports 0–1023. This prevents a malicious normal-user program from impersonating a trusted service — for example, a fake SSH server on port 22 that steals passwords when users try to log in. Legitimate system daemons run as root or with specific capabilities.
bind(). The client needs a port so the server knows where to send its replies. The OS picks an unused port from the dynamic range. On Linux, this range is defined in /proc/sys/net/ipv4/ip_local_port_range (typically 32768–60999). You can also trigger this by binding to port 0 explicitly.
getsockname() system call after bind(). It fills in the sockaddr_in (or sockaddr_in6) structure with the actual address and port that the OS assigned. Read sin_port and convert with ntohs() to get the port in host byte order.
/proc/sys/net/ipv4/ip_local_port_range contains two numbers: the low and high end of the ephemeral port range. You can read it with cat and change it with echo "low high" > /proc/sys/net/ipv4/ip_local_port_range. This change is temporary (lost on reboot). For a permanent change, add the setting to /etc/sysctl.conf: net.ipv4.ip_local_port_range = 1024 65535.
Continue Learning
Next: UDP — connectionless, unreliable, and when to prefer it over TCP.
