Overview
Port numbers are 16-bit unsigned integers (0-65535) that identify specific processes or services on a networked computer. Combined with an IP address, a port number forms a socket address that uniquely identifies a network endpoint. When data arrives at a computer, the operating system uses the destination port number to route packets to the correct application or service.
The port number system solves a fundamental networking problem: a single computer runs multiple network services simultaneously, each requiring its own stream of network traffic. Without port numbers, the operating system cannot distinguish between data intended for a web server, an SSH session, or an email client. Port numbers create logical channels within a single network interface, multiplexing many communication streams over one physical connection.
Port numbers fall into three ranges defined by IANA (Internet Assigned Numbers Authority):
Well-Known Ports (0-1023): Reserved for system services and common protocols. Opening these ports requires administrative privileges on Unix-like systems. Examples include HTTP (80), HTTPS (443), SSH (22), and FTP (21).
Registered Ports (1024-49151): Available for registration with IANA for specific services. Applications can use these ports without special permissions. Database servers commonly use registered ports: PostgreSQL (5432), MySQL (3306), MongoDB (27017).
Dynamic or Ephemeral Ports (49152-65535): Automatically assigned by the operating system for client-side connections. When a web browser connects to a server, the OS assigns an ephemeral port for that connection's source port.
Port numbers work at the transport layer (TCP and UDP) of the networking stack. A TCP connection requires four pieces of information: source IP, source port, destination IP, and destination port. This four-tuple uniquely identifies each connection, allowing a server to handle thousands of simultaneous connections to the same port number from different clients.
require 'socket'
# Server binds to port 8080
server = TCPServer.new(8080)
puts "Server listening on port #{server.local_address.ip_port}"
# => Server listening on port 8080
# Accept a connection
client = server.accept
puts "Client connected from #{client.peeraddr[3]}:#{client.peeraddr[1]}"
# => Client connected from 127.0.0.1:52341 (ephemeral port)
Key Principles
Transport Layer Multiplexing: Port numbers operate at OSI Layer 4 (Transport Layer), above the network layer (IP addresses) and below the application layer (HTTP, FTP, etc.). The transport layer protocol (TCP or UDP) includes the port number in each packet header. When the network interface card receives a packet, the operating system examines the destination port and forwards the packet to the appropriate socket buffer.
Socket Binding: Before a server can receive connections on a port, it must bind to that port. Binding associates a socket with a specific port number and IP address combination. Only one process can bind to a specific port/address pair at any time on most systems. The SO_REUSEADDR socket option modifies this behavior for TCP, allowing faster rebinding during development.
Client vs Server Ports: Servers bind to well-known or registered ports that clients know in advance. Clients use ephemeral ports assigned automatically by the operating system. The client's ephemeral port identifies the specific connection but has no semantic meaning. Multiple connections from the same client machine use different ephemeral ports.
Port Exhaustion: Each client connection consumes an ephemeral port. On a busy client making many outbound connections, the operating system can exhaust available ephemeral ports. The default ephemeral range varies by operating system: Linux typically uses 32768-60999, while Windows uses 49152-65535. Applications making frequent short-lived connections must implement connection pooling to avoid port exhaustion.
Stateful vs Stateless Protocols: TCP ports maintain connection state between endpoints. The operating system tracks each TCP connection with a control block containing sequence numbers, window sizes, and connection state. UDP ports are stateless—each datagram stands alone. This affects how applications handle port numbers: TCP servers can identify clients by connection, while UDP servers must include client identification in application data.
Port Security Model: The distinction between privileged (0-1023) and unprivileged (1024+) ports creates a security boundary. On Unix systems, only processes running as root can bind to privileged ports. This restriction prevents unprivileged users from impersonating system services. An unprivileged user cannot start a rogue SSH server on port 22.
Network Address Translation (NAT): NAT devices rewrite port numbers as packets traverse network boundaries. Home routers use NAT to share a single public IP among multiple devices. The NAT device maintains a mapping table between internal (IP:port) pairs and external (IP:port) pairs. This complicates server applications behind NAT, which must use techniques like port forwarding or UPnP.
Dual-Stack Networking: Modern systems support both IPv4 and IPv6. A server binding to a port must decide whether to accept IPv4, IPv6, or both. Ruby's socket library handles this through address family specifications. A socket bound to :: (IPv6 any address) may or may not accept IPv4 connections depending on the system's IPV6_V6ONLY socket option.
require 'socket'
# Bind to IPv4 only
ipv4_server = TCPServer.new('0.0.0.0', 9000)
# Bind to IPv6 (may also accept IPv4 on some systems)
ipv6_server = TCPServer.new('::', 9001)
# Bind to specific interface
localhost_server = TCPServer.new('127.0.0.1', 9002)
Ruby Implementation
Ruby provides comprehensive networking support through the Socket library, part of the standard library. The library offers both low-level socket primitives and high-level abstractions for common protocols.
TCPServer and TCPSocket: The most common approach for TCP networking in Ruby uses TCPServer for servers and TCPSocket for clients. TCPServer.new(port) binds to all interfaces on the specified port. TCPServer.new(hostname, port) binds to a specific interface.
require 'socket'
# Simple echo server on port 3000
server = TCPServer.new(3000)
loop do
client = server.accept
# Read data from client
data = client.gets
puts "Received: #{data}"
# Echo back to client
client.puts "Echo: #{data}"
client.close
end
Client Connection with Ephemeral Ports: When creating a client connection, Ruby automatically assigns an ephemeral port. The client does not specify its source port in normal usage.
require 'socket'
# Connect to server - Ruby assigns ephemeral port automatically
client = TCPSocket.new('localhost', 3000)
# Check the locally assigned port
local_port = client.local_address.ip_port
puts "Connected from local port: #{local_port}"
# => Connected from local port: 54321
client.puts "Hello Server"
response = client.gets
puts response
client.close
UDPSocket for Datagram Communication: UDP sockets work differently—no connection establishment occurs. The server binds to a port and receives datagrams from any source.
require 'socket'
# UDP server
udp_server = UDPSocket.new
udp_server.bind('localhost', 4000)
data, sender = udp_server.recvfrom(1024)
puts "Received from #{sender[3]}:#{sender[1]}: #{data}"
# UDP client
udp_client = UDPSocket.new
udp_client.send("Hello UDP", 0, 'localhost', 4000)
udp_client.close
Socket Options and Port Reuse: The SO_REUSEADDR option allows rebinding to a port in TIME_WAIT state, essential during development when frequently restarting servers.
require 'socket'
socket = Socket.new(:INET, :STREAM)
socket.setsockopt(:SOCKET, :REUSEADDR, true)
socket.bind(Socket.sockaddr_in(5000, '0.0.0.0'))
socket.listen(5)
# Now the port can be reused immediately after the server stops
Port Detection and Dynamic Port Assignment: Binding to port 0 tells the OS to assign an available port automatically. This technique works well for services that advertise their port through service discovery.
require 'socket'
# Let OS choose an available port
server = TCPServer.new(0)
actual_port = server.local_address.ip_port
puts "Server started on dynamic port: #{actual_port}"
# => Server started on dynamic port: 58472
Non-blocking I/O and Port Management: Ruby supports non-blocking socket operations, important for servers handling many connections without spawning threads for each client.
require 'socket'
server = TCPServer.new(6000)
server.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, true)
clients = []
loop do
# Non-blocking accept
begin
clients << server.accept_nonblock
rescue IO::WaitReadable
# No pending connection
end
# Handle existing clients
clients.each do |client|
begin
data = client.read_nonblock(1024)
client.write("Received: #{data}")
rescue IO::WaitReadable
# No data available
rescue EOFError
client.close
clients.delete(client)
end
end
sleep 0.01 # Prevent busy waiting
end
Port Forwarding and Tunneling: Ruby applications can implement port forwarding, receiving connections on one port and forwarding traffic to another destination.
require 'socket'
# Port forwarder: listen on 7000, forward to 8000
forwarder = TCPServer.new(7000)
loop do
client = forwarder.accept
Thread.new(client) do |client_conn|
target = TCPSocket.new('localhost', 8000)
# Bidirectional forwarding
[
Thread.new { IO.copy_stream(client_conn, target) },
Thread.new { IO.copy_stream(target, client_conn) }
].each(&:join)
client_conn.close
target.close
end
end
Practical Examples
HTTP Server on Standard Port: Building a minimal HTTP server demonstrates port binding for web services. Standard HTTP uses port 80, but development servers typically use ports above 1024 to avoid privilege requirements.
require 'socket'
server = TCPServer.new(8080)
puts "HTTP server running on port 8080"
loop do
client = server.accept
request = client.readpartial(2048)
# Parse request line
method, path, version = request.lines.first.split
response = "HTTP/1.1 200 OK\r\n"
response += "Content-Type: text/html\r\n"
response += "\r\n"
response += "<html><body><h1>Hello from port 8080</h1></body></html>"
client.write(response)
client.close
end
Multi-Port Service: Some applications listen on multiple ports simultaneously, each serving a different purpose. Administrative interfaces often run on separate ports from main application traffic.
require 'socket'
# Main application port
app_server = TCPServer.new(3000)
# Admin interface port
admin_server = TCPServer.new(3001)
loop do
# Use select to monitor both ports
ready = IO.select([app_server, admin_server], nil, nil, 1)
next unless ready
ready[0].each do |server|
client = server.accept
if server == app_server
client.puts "Application response"
puts "App request from #{client.peeraddr[3]}:#{client.peeraddr[1]}"
else
client.puts "Admin interface - restricted access"
puts "Admin request from #{client.peeraddr[3]}:#{client.peeraddr[1]}"
end
client.close
end
end
Database Connection Pool: Database clients maintain connection pools to reuse connections and prevent port exhaustion. Each connection consumes an ephemeral port.
require 'socket'
class ConnectionPool
def initialize(host, port, max_connections)
@host = host
@port = port
@available = []
@in_use = []
@max_connections = max_connections
end
def acquire
connection = @available.pop || create_connection
@in_use << connection
connection
end
def release(connection)
@in_use.delete(connection)
@available << connection
end
private
def create_connection
if @available.size + @in_use.size >= @max_connections
raise "Connection pool exhausted"
end
conn = TCPSocket.new(@host, @port)
puts "Created connection on local port #{conn.local_address.ip_port}"
conn
end
end
# Usage
pool = ConnectionPool.new('localhost', 5432, 10)
conn = pool.acquire
# Use connection
pool.release(conn)
Port Scanner Implementation: Port scanning tests which ports accept connections on a target host. This example shows TCP connect scanning, the most reliable method.
require 'socket'
require 'timeout'
def scan_port(host, port, timeout_sec = 1)
begin
Timeout.timeout(timeout_sec) do
socket = TCPSocket.new(host, port)
socket.close
return true
end
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Timeout::Error
return false
end
end
# Scan common web ports
host = 'localhost'
[80, 443, 8000, 8080, 8443].each do |port|
if scan_port(host, port)
puts "Port #{port} is open"
else
puts "Port #{port} is closed"
end
end
Common Patterns
Well-Known Port Assignment: Server applications bind to standardized ports that clients know in advance. HTTP servers use port 80 (HTTP) or 443 (HTTPS). Deviating from standard ports requires clients to specify the port explicitly in URLs or configuration.
require 'socket'
# Standard port - no port in URL needed
web_server = TCPServer.new(80) # Requires root/admin privileges
# Non-standard port - must specify in URL (http://example.com:3000)
dev_server = TCPServer.new(3000)
Ephemeral Port Allocation: Client applications let the operating system choose source ports automatically. Manual port specification on clients serves no purpose except for specific scenarios like source port filtering or connection tracking.
Port Range Configuration: Applications that require multiple ports often use consecutive port ranges. Load balancers distribute across multiple backend ports, monitoring systems use adjacent ports for different protocols.
require 'socket'
# Service cluster on ports 9000-9003
cluster = (9000..9003).map do |port|
TCPServer.new(port)
end
puts "Cluster running on ports 9000-9003"
Service Port Discovery: Rather than hardcoding ports, applications can implement service discovery. A central registry maps service names to host:port pairs. Clients query the registry to locate services.
# Service registry
SERVICES = {
'auth' => { host: 'localhost', port: 5001 },
'api' => { host: 'localhost', port: 5002 },
'cache' => { host: 'localhost', port: 6379 }
}
def connect_to_service(service_name)
config = SERVICES[service_name]
raise "Unknown service: #{service_name}" unless config
TCPSocket.new(config[:host], config[:port])
end
# Client code
auth_conn = connect_to_service('auth')
Zero-Configuration Port Selection: For services that don't require fixed ports, binding to port 0 enables automatic port assignment. The application then advertises its actual port through other means.
require 'socket'
server = TCPServer.new(0) # OS chooses port
actual_port = server.local_address.ip_port
# Write port to file for other processes to discover
File.write('service.port', actual_port)
puts "Service running on port #{actual_port}"
Port Multiplexing: Running multiple services on a single port requires application-layer protocol multiplexing. HTTP servers use the Host header, TLS uses SNI (Server Name Indication). The application examines initial data to route to the correct handler.
require 'socket'
server = TCPServer.new(9000)
loop do
client = server.accept
request = client.readpartial(1024)
# Route based on HTTP path
if request.include?('GET /api')
client.puts "HTTP/1.1 200 OK\r\n\r\nAPI Response"
elsif request.include?('GET /admin')
client.puts "HTTP/1.1 200 OK\r\n\r\nAdmin Interface"
else
client.puts "HTTP/1.1 404 Not Found\r\n\r\n"
end
client.close
end
Error Handling & Edge Cases
Address Already in Use (EADDRINUSE): Attempting to bind to a port already in use raises Errno::EADDRINUSE. This occurs when another process holds the port, or when the port remains in TIME_WAIT state after a recent server shutdown. The SO_REUSEADDR socket option mitigates the TIME_WAIT issue but cannot override active bindings.
require 'socket'
begin
server = TCPServer.new(8080)
rescue Errno::EADDRINUSE
puts "Port 8080 already in use"
# Retry with SO_REUSEADDR
socket = Socket.new(:INET, :STREAM)
socket.setsockopt(:SOCKET, :REUSEADDR, true)
socket.bind(Socket.sockaddr_in(8080, '0.0.0.0'))
socket.listen(5)
server = TCPServer.for_fd(socket.fileno)
end
Permission Denied (EACCES): Binding to ports below 1024 requires elevated privileges on Unix-like systems. Non-root processes receive Errno::EACCES when attempting to bind to privileged ports.
require 'socket'
begin
server = TCPServer.new(80)
rescue Errno::EACCES
puts "Cannot bind to port 80 without root privileges"
puts "Starting on unprivileged port 8080 instead"
server = TCPServer.new(8080)
end
Port Exhaustion: Systems have a finite number of ephemeral ports. Applications making many concurrent outbound connections can exhaust the ephemeral port range. The kernel returns EADDRNOTAVAIL when no ports remain available.
require 'socket'
connections = []
begin
1000.times do |i|
connections << TCPSocket.new('localhost', 8080)
puts "Created connection #{i + 1}" if (i + 1) % 100 == 0
end
rescue Errno::EADDRNOTAVAIL
puts "Exhausted ephemeral ports after #{connections.size} connections"
ensure
connections.each(&:close)
end
Connection Refused (ECONNREFUSED): Attempting to connect to a port with no listening service results in Errno::ECONNREFUSED. This indicates the host is reachable but no process accepts connections on that port.
require 'socket'
begin
client = TCPSocket.new('localhost', 9999)
rescue Errno::ECONNREFUSED
puts "No service listening on port 9999"
rescue Errno::ETIMEDOUT
puts "Connection timed out - host unreachable or port filtered"
end
IPv4 vs IPv6 Binding Conflicts: Binding to the IPv6 wildcard address :: may implicitly bind to IPv4 0.0.0.0 on some systems. This causes conflicts when attempting to bind both explicitly.
require 'socket'
# This may bind to both IPv4 and IPv6
ipv6_server = TCPServer.new('::', 7000)
begin
# This may fail if IPv6 socket already bound IPv4
ipv4_server = TCPServer.new('0.0.0.0', 7000)
rescue Errno::EADDRINUSE
puts "IPv6 socket already bound this port for IPv4 traffic"
end
File Descriptor Limits: Each socket consumes a file descriptor. Systems impose per-process file descriptor limits. Exceeding this limit causes EMFILE errors.
require 'socket'
servers = []
begin
10000.times do |i|
servers << TCPServer.new(0) # Dynamic port for each
end
rescue Errno::EMFILE
puts "Reached file descriptor limit at #{servers.size} sockets"
ensure
servers.each(&:close)
end
TIME_WAIT State: TCP connections entering TIME_WAIT state hold the port for a system-defined period (typically 2 * MSL, or 60-120 seconds). Servers that restart frequently encounter this when rebinding.
require 'socket'
# Handle TIME_WAIT gracefully
def create_server_with_reuse(port)
socket = Socket.new(:INET, :STREAM)
socket.setsockopt(:SOCKET, :REUSEADDR, true)
# Linux-specific: reuse TIME_WAIT ports
begin
socket.setsockopt(:SOCKET, :REUSEPORT, true)
rescue
# REUSEPORT not available on all systems
end
socket.bind(Socket.sockaddr_in(port, '0.0.0.0'))
socket.listen(5)
TCPServer.for_fd(socket.fileno)
end
server = create_server_with_reuse(8080)
Security Implications
Port Exposure and Attack Surface: Every open port represents a potential entry point for attackers. Minimize exposed ports by running only necessary services. Use firewalls to restrict port access to authorized networks.
Privileged Port Restrictions: The Unix privileged port system (0-1023) provides basic security by preventing non-root users from impersonating system services. However, this protection only prevents local user impersonation, not network-based attacks. After binding to a privileged port, applications should drop root privileges.
require 'socket'
require 'etc'
# Bind to privileged port as root
server = TCPServer.new(80)
# Drop privileges after binding
Process::GID.change_privilege(Etc.getgrnam('www-data').gid)
Process::UID.change_privilege(Etc.getpwnam('www-data').uid)
puts "Running as user #{Etc.getpwuid(Process.uid).name}"
# Continue serving as non-root
loop do
client = server.accept
# Handle request
client.close
end
Port Scanning Detection: Attackers scan port ranges to identify running services. Implement rate limiting and intrusion detection to identify scanning activity. Logging connection attempts helps identify reconnaissance.
require 'socket'
connection_tracker = Hash.new { |h, k| h[k] = [] }
server = TCPServer.new(8080)
loop do
client = server.accept
client_ip = client.peeraddr[3]
# Track connection attempts
connection_tracker[client_ip] << Time.now
# Remove old entries
connection_tracker[client_ip].select! { |t| Time.now - t < 60 }
# Detect scanning
if connection_tracker[client_ip].size > 10
puts "WARNING: Potential port scan from #{client_ip}"
client.close
next
end
# Handle legitimate connection
client.puts "Welcome"
client.close
end
Firewall Configuration: Firewalls control which ports accept connections from specific networks. Default-deny policies close all ports except those explicitly allowed. Application deployment requires coordinating firewall rules with required ports.
Man-in-the-Middle Attacks: Unencrypted protocols on any port are vulnerable to interception. Use TLS/SSL for sensitive data transmission regardless of port number. Port 443 conventionally indicates HTTPS, but any port can carry encrypted traffic.
require 'socket'
require 'openssl'
# TLS server on non-standard port
tcp_server = TCPServer.new(9443)
ssl_context = OpenSSL::SSL::SSLContext.new
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read('cert.pem'))
ssl_context.key = OpenSSL::PKey::RSA.new(File.read('key.pem'))
ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ssl_context)
loop do
connection = ssl_server.accept
# Encrypted communication
connection.puts "Secure connection on port 9443"
connection.close
end
Port-Based Service Identification: Attackers identify services by port number and target known vulnerabilities. Running services on non-standard ports provides minimal security—real protection comes from service hardening and patching.
Localhost Binding for Security: Binding to 127.0.0.1 restricts connections to the local machine only. Use localhost binding for services that should not accept external connections.
require 'socket'
# Accessible only from local machine
local_only = TCPServer.new('127.0.0.1', 6379)
# Accessible from network
network_accessible = TCPServer.new('0.0.0.0', 8080)
# Specific interface only
internal_only = TCPServer.new('10.0.1.5', 3000)
Reference
Port Range Categories
| Range | Name | Description | Usage |
|---|---|---|---|
| 0-1023 | Well-Known Ports | System ports requiring privileges | HTTP (80), HTTPS (443), SSH (22), FTP (21) |
| 1024-49151 | Registered Ports | IANA registered services | PostgreSQL (5432), MySQL (3306), Redis (6379) |
| 49152-65535 | Dynamic/Ephemeral | OS-assigned client ports | Automatic client source ports |
Common Service Ports
| Service | Port | Protocol | Description |
|---|---|---|---|
| HTTP | 80 | TCP | Unencrypted web traffic |
| HTTPS | 443 | TCP | Encrypted web traffic |
| SSH | 22 | TCP | Secure shell remote access |
| FTP | 21 | TCP | File transfer (control) |
| SMTP | 25 | TCP | Email transmission |
| DNS | 53 | TCP/UDP | Domain name resolution |
| DHCP | 67/68 | UDP | Dynamic IP configuration |
| Telnet | 23 | TCP | Unencrypted remote access |
| IMAP | 143 | TCP | Email retrieval |
| POP3 | 110 | TCP | Email retrieval |
| PostgreSQL | 5432 | TCP | Database server |
| MySQL | 3306 | TCP | Database server |
| Redis | 6379 | TCP | Cache/data structure server |
| MongoDB | 27017 | TCP | Document database |
| RabbitMQ | 5672 | TCP | Message queue |
Ruby Socket Methods
| Method | Purpose | Example |
|---|---|---|
| TCPServer.new(port) | Create TCP server on port | TCPServer.new(8080) |
| TCPSocket.new(host, port) | Connect TCP client to port | TCPSocket.new('localhost', 80) |
| UDPSocket.new | Create UDP socket | socket.bind('0.0.0.0', 5000) |
| Socket.new | Create low-level socket | Socket.new(:INET, :STREAM) |
| socket.bind(sockaddr) | Bind socket to address/port | socket.bind(Socket.sockaddr_in(port, addr)) |
| socket.listen(backlog) | Listen for connections | socket.listen(5) |
| server.accept | Accept incoming connection | client = server.accept |
| socket.local_address | Get local socket address | socket.local_address.ip_port |
| socket.remote_address | Get remote socket address | socket.remote_address.ip_address |
| socket.setsockopt | Set socket options | socket.setsockopt(:SOCKET, :REUSEADDR, true) |
Socket Options for Port Management
| Option | Level | Effect |
|---|---|---|
| SO_REUSEADDR | SOL_SOCKET | Allow binding to TIME_WAIT ports |
| SO_REUSEPORT | SOL_SOCKET | Allow multiple binds to same port (Linux) |
| IPV6_V6ONLY | IPPROTO_IPV6 | Disable IPv4-mapped IPv6 addresses |
| SO_LINGER | SOL_SOCKET | Control connection close behavior |
| TCP_NODELAY | IPPROTO_TCP | Disable Nagle algorithm |
Common Error Codes
| Error | Errno | Meaning | Common Cause |
|---|---|---|---|
| EADDRINUSE | 48/98 | Address already in use | Port bound by another process |
| EACCES | 13 | Permission denied | Privileged port without root |
| EADDRNOTAVAIL | 49/99 | Address not available | Port exhaustion, invalid address |
| ECONNREFUSED | 61/111 | Connection refused | No listener on port |
| ETIMEDOUT | 60/110 | Connection timed out | Host unreachable, firewall |
| EMFILE | 24 | Too many open files | File descriptor limit reached |
Port State Checklist
| State | Description | Action Required |
|---|---|---|
| LISTEN | Socket bound and listening | Normal server state |
| ESTABLISHED | Active connection | Normal connection state |
| TIME_WAIT | Connection closed, waiting | Use SO_REUSEADDR to rebind |
| CLOSE_WAIT | Remote closed, local pending | Application must close socket |
| Port in use | Bind fails with EADDRINUSE | Check for running processes, wait for TIME_WAIT |
| Port accessible | Firewall allows connections | Verify firewall rules |
| Port exhausted | No ephemeral ports available | Implement connection pooling |