Common Ports and Protocols
Sixteen Bits That Pick the Application
An IP address gets a packet to a machine; a port gets it to the right program on that machine. TCP and UDP headers each carry a 16-bit source and destination port, so there are 65,536 of each, and TCP port 53 and UDP port 53 are completely separate. A connection is identified by the four-tuple of source IP, source port, destination IP and destination port, which is how one server port can carry thousands of simultaneous clients. The transport details are in TCP vs UDP; this lesson is about which numbers mean what, how to see who is using them, and why the numbers themselves protect nothing.
The Three Ranges
IANA divides the space by convention:
| Range | Name | Used for |
|---|---|---|
| 0-1023 | well-known (system) | standard services: SSH, DNS, HTTP, HTTPS |
| 1024-49151 | registered | applications that registered a number: databases, RDP, many vendor tools |
| 49152-65535 | dynamic / private | ephemeral client ports, by IANA's recommendation |
Operating systems do not all follow the last row. Linux hands out ephemeral ports from 32768-60999 by default (sysctl net.ipv4.ip_local_port_range); Windows follows IANA, which you can confirm in PowerShell:
PS> Get-NetTCPSetting -SettingName Internet | Select DynamicPortRangeStartPort,DynamicPortRangeNumberOfPorts
DynamicPortRangeStartPort DynamicPortRangeNumberOfPorts
------------------------- -----------------------------
49152 16384
On Linux, binding a port below 1024 requires root or the CAP_NET_BIND_SERVICE capability (the threshold is net.ipv4.ip_unprivileged_port_start). This is why web servers start as root, bind 80 and 443, then drop privileges, and why development servers default to 8000 or 8080. Windows has no such restriction.
The Ports You Will Actually Meet
| Port | Proto | Service | Notes |
|---|---|---|---|
| 22 | TCP | SSH, SFTP | brute-forced constantly if public |
| 23 | TCP | Telnet | cleartext; should not exist on modern networks |
| 25 | TCP | SMTP server-to-server | many ISPs and clouds block outbound 25 |
| 53 | UDP + TCP | DNS | TCP for large answers and zone transfers |
| 67 / 68 | UDP | DHCP server / client | |
| 80 | TCP | HTTP | usually just redirects to 443 |
| 123 | UDP | NTP | |
| 143 / 993 | TCP | IMAP / IMAP over TLS | |
| 161 / 162 | UDP | SNMP / SNMP traps | v1/v2c community strings are passwords in cleartext |
| 389 / 636 | TCP | LDAP / LDAPS | |
| 443 | TCP + UDP | HTTPS; UDP 443 is HTTP/3 (QUIC) | see HTTP/2 and HTTP/3 |
| 445 | TCP | SMB file sharing | never expose to the internet |
| 465 / 587 | TCP | SMTP submission (implicit TLS / STARTTLS) | where mail clients send |
| 500 / 4500 | UDP | IPsec IKE / NAT traversal | |
| 853 | TCP | DNS over TLS | |
| 1194 | UDP | OpenVPN default | |
| 1433 | TCP | Microsoft SQL Server | |
| 3306 | TCP | MySQL / MariaDB | |
| 3389 | TCP + UDP | RDP | a favourite ransomware entry point |
| 5353 | UDP | mDNS (.local names) |
|
| 5432 | TCP | PostgreSQL | |
| 6379 | TCP | Redis | historically no authentication by default |
| 6443 | TCP | Kubernetes API server | |
| 8080 / 8443 | TCP | alternative HTTP / HTTPS | dev servers, proxies, admin panels |
| 9200 | TCP | Elasticsearch HTTP API | frequent source of data leaks |
| 27017 | TCP | MongoDB | |
| 51820 | UDP | WireGuard (common default) |
Two patterns are worth memorising. Protocols that added TLS later often got a second port for the encrypted version (143/993, 389/636), while newer designs upgrade in place with STARTTLS or are encrypted from the start. And a port being listed as TCP tells you nothing about UDP: DNS, RDP and HTTPS use both.
The Services Database Is Just a Text File
The names tools print next to port numbers come from a local file, /etc/services on Unix and C:\Windows\System32\drivers\etc\services on Windows. Python reads the same file:
import socket
for name, proto in [("ssh", "tcp"), ("https", "tcp"), ("domain", "udp"), ("postgresql", "tcp")]:
try:
print(f"{name}/{proto} -> {socket.getservbyname(name, proto)}")
except OSError as e:
print(f"{name}/{proto} -> {e}")
print(socket.getservbyport(3389, "tcp"))
On Windows:
ssh/tcp -> 22
https/tcp -> 443
domain/udp -> 53
postgresql/tcp -> service/proto not found
ms-wbt-server
Most Linux distributions do list postgresql; Windows' file is shorter. The lesson is that these labels are local guesses. When netstat or a firewall log says https, it means "port 443", not "this traffic was HTTPS".
What Is Listening on This Machine?
An open port is almost always a program that called bind() (and listen() for TCP; see Socket Programming in Python). Finding it is the first step of every hardening job:
sudo ss -tulpn # Linux: TCP+UDP, listening, with process, numeric
sudo lsof -nP -iTCP -sTCP:LISTEN # Linux/macOS: listeners per process
sudo lsof -nP -i :5432 # who owns this one port?
netstat -ano | findstr LISTENING # Windows: PID in the last column
A typical ss -tulpn on a small Linux web server:
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=612,fd=13))
tcp LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=901,fd=3))
tcp LISTEN 0 244 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=1022,fd=6))
tcp LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=1200,fd=6))
tcp LISTEN 0 4096 *:9100 *:* users:(("node_exporter",pid=1310,fd=3))
tcp LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=901,fd=4))
The local address column matters as much as the port:
| Bound to | Reachable from |
|---|---|
127.0.0.1, ::1, 127.0.0.53%lo |
this machine only |
a specific interface address (10.0.1.5) |
networks that reach that interface |
0.0.0.0 |
every IPv4 interface |
[::] |
every IPv6 interface (IPv4 as well only if the socket is dual-stack) |
* (in ss) |
a dual-stack socket: every IPv4 and IPv6 interface |
So PostgreSQL above is safe by binding alone, while node_exporter on *:9100 answers anyone the firewall lets through. The PowerShell equivalent resolves process names for you:
PS> Get-NetTCPConnection -State Listen | Select LocalAddress,LocalPort,OwningProcess,
@{n='Process';e={(Get-Process -Id $_.OwningProcess).ProcessName}}
LocalAddress LocalPort OwningProcess Process
------------ --------- ------------- -------
0.0.0.0 135 1592 svchost
:: 135 1592 svchost
:: 445 4 System
Checking from the machine shows what is listening; checking from another host shows what is reachable. Do both: nmap -Pn -p- host from outside, on systems you are authorised to scan, catches the firewall rule you forgot.
Ephemeral Ports: The Other Half of Every Connection
A client connecting to 203.0.113.10:443 gets a source port picked from the ephemeral range, and the reply comes back to that high port. This has practical consequences:
- Stateless filters need a return rule. A stateful firewall remembers the outbound connection and allows the reply automatically. A stateless one, such as an AWS network ACL, must explicitly allow inbound traffic to the ephemeral range, or every outbound connection silently hangs.
- Exhaustion. A proxy or scraper opening many short connections to one destination can run out of source ports; the cause and fix are in TCP Handshake and Control Flags.
- Conflicts. A service configured to listen on a port inside the ephemeral range may fail to start at boot because an outgoing connection happened to grab that port first. Keep server ports out of it, or reserve them (
net.ipv4.ip_local_reserved_portson Linux). - NAT rewrites them. Your home router changes source ports as it translates, so the port a server logs is not the one your laptop chose.
Ports Are a Convention, Not a Security Control
Nothing forces a program on port 443 to speak HTTPS, or an SSH server to use 22. This local demo runs HTTP on 2222 and an SSH-style greeting on 8080, then identifies each by what it actually says:
import socket, threading
from http.server import HTTPServer, SimpleHTTPRequestHandler
class Quiet(SimpleHTTPRequestHandler):
def log_message(self, *args): pass
# Two local services on misleading ports: HTTP on 2222, an SSH greeting on 8080.
http = HTTPServer(("127.0.0.1", 2222), Quiet)
threading.Thread(target=http.serve_forever, daemon=True).start()
fake_ssh = socket.create_server(("127.0.0.1", 8080))
def greet():
while True:
conn, _ = fake_ssh.accept()
conn.sendall(b"SSH-2.0-OpenSSH_9.6\r\n") # SSH servers speak first
conn.close()
threading.Thread(target=greet, daemon=True).start()
def identify(host, port):
with socket.create_connection((host, port), timeout=3) as s:
s.settimeout(1.5)
try:
banner = s.recv(200) # server-speaks-first: SSH, SMTP, FTP
except TimeoutError:
s.sendall(b"HEAD / HTTP/1.0\r\n\r\n") # silent: try a client-speaks-first protocol
banner = s.recv(200)
return banner.split(b"\r\n")[0].decode(errors="replace")
for port, usual in [(2222, "SSH"), (8080, "HTTP")]:
print(f"port {port} (usually {usual}) answered: {identify('127.0.0.1', port)}")
port 2222 (usually SSH) answered: HTTP/1.0 200 OK
port 8080 (usually HTTP) answered: SSH-2.0-OpenSSH_9.6
The trick is the difference between protocols where the server speaks first (SSH, SMTP, FTP send a greeting on connect) and those where the client speaks first (HTTP, TLS). nmap -sV automates the same idea with a large probe database, and internet-wide scanners run it against every port. Three conclusions follow:
- Moving SSH to a high port cuts log noise from lazy bots but does not stop anyone who scans all ports. It is not a substitute for key-only authentication.
- "Allow TCP 443 outbound" allows anything that runs on 443. VPNs and tunnels use it precisely because it is always open. Controlling applications needs identification by content (next-generation firewalls, TLS inspection, or proxies), as the Network Security lesson explains.
- A port on its standard number can still be something else. Treat port-based labels in logs as hints and confirm with the protocol.
Real protection comes from binding services to the right interface, default-deny firewalls, authentication, and encryption. Databases, caches, SMB, RDP and admin APIs should never be reachable from the internet at all; put them behind a VPN or bastion.
Practice
On a machine you control, run ss -tulpn (or the PowerShell command) and account for every listener: which program, why it runs, and whether its bind address is wider than it needs to be. Then scan the same machine from another host with nmap -Pn -p- -sV, and explain every difference between the two lists.