A default port lets a client omit a number when a protocol or configuration already supplies it. HTTP commonly uses TCP 80, HTTPS TCP 443, and SSH TCP 22. These assignments reduce configuration, but they do not inspect a listener’s bytes or establish its identity. An endpoint includes an address and transport as well as a port, and its application protocol still has to match the caller.
When it breaks
Moving a service to another port does not make its clients discover the change. URLs, database connection settings, probes, proxies, and access rules may each carry their own destination. Updating only the server can leave the health check aimed at an old listener or a client failing against an unused port. Record the intended mapping and test the actual callers affected by the change.
A familiar number can also create false confidence. A process can speak cleartext HTTP on TCP 443, or a different protocol on TCP 80. Selecting HTTPS means performing TLS, including the intended certificate checks; selecting port 443 alone does not add encryption. Likewise, a successful TCP connection proves neither that the expected application speaks next nor that the peer is authorized to serve its identity.
Service-name databases are descriptions, not service managers. Linux /etc/services maps names to numbers and transport protocols, but adding an entry does not start a daemon or open a firewall. Its contents can differ from another host’s database and from the current IANA registry. Name resolution for a service number and hostname resolution for an address are separate inputs to a connection.
Check it quickly
Save this as check-default-ports.py and run python3 check-default-ports.py with Docker. Python 3.11.16 runs in a pinned Linux container with no published ports. The fixture deliberately starts a plain HTTP server on TCP 443 and later binds a UDP socket to the same number. These listeners are disposable and cannot replace a host’s production HTTPS server:
import json,subprocess,uuid
image="python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84"
program=r"""
from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
import http.client,json,socket,ssl,threading
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body=b'plain HTTP on TCP 443\n';self.send_response(200);self.send_header('Content-Length',str(len(body)))
self.end_headers();self.wfile.write(body)
def log_message(self,*args):pass
server=ThreadingHTTPServer(('127.0.0.1',443),Handler)
thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start()
try:
plain=http.client.HTTPConnection('127.0.0.1',443,timeout=2)
try:
plain.request('GET','/');response=plain.getresponse();body=response.read()
assert response.status==200 and body==b'plain HTTP on TCP 443\n'
finally:plain.close()
encrypted=http.client.HTTPSConnection('127.0.0.1',443,timeout=2)
try:
encrypted.request('GET','/');raise AssertionError('plain server spoke TLS')
except ssl.SSLError as error:tls_error=error.reason
finally:encrypted.close()
with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as udp,socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as client:
udp.bind(('127.0.0.1',443));udp.settimeout(1);client.settimeout(1)
client.sendto(b'UDP 443',udp.getsockname());data,peer=udp.recvfrom(64);assert data==b'UDP 443'
udp.sendto(b'UDP reply',peer);assert client.recvfrom(64)[0]==b'UDP reply'
concurrent=udp.getsockname()[1]==server.server_port
defaults={'http':http.client.HTTPConnection('127.0.0.1').port,'https':http.client.HTTPSConnection('127.0.0.1').port}
registry={'http_tcp':socket.getservbyname('http','tcp'),'https_tcp':socket.getservbyname('https','tcp'),
'domain_tcp':socket.getservbyname('domain','tcp'),'domain_udp':socket.getservbyname('domain','udp')}
assert defaults=={'http':80,'https':443} and registry=={'http_tcp':80,'https_tcp':443,'domain_tcp':53,'domain_udp':53}
print(json.dumps({'client_defaults':defaults,'local_services':registry,'explicit_http_status':response.status,
'explicit_http_body':body.decode().strip(),'https_failure':tls_error,'tcp_udp_same_number':concurrent}))
finally:
server.shutdown();server.server_close();thread.join(timeout=2);assert not thread.is_alive()
"""
name='xbcatm-socket-'+uuid.uuid4().hex[:10]
try:
result=subprocess.run(['docker','run','--rm','-i','--name',name,'--network','none',
'--memory','128m','--pids-limit','64',image,'python3','-'],input=program,
capture_output=True,text=True,timeout=60)
assert result.returncode==0,result.stderr
print(json.dumps(json.loads(result.stdout),ensure_ascii=False,sort_keys=True))
finally:
subprocess.run(['docker','rm','-f',name],capture_output=True,text=True,timeout=10)
The client’s unconnected HTTPConnection and HTTPSConnection objects select 80 and 443 when no port is supplied. Local service lookups also return HTTP/TCP 80 and HTTPS/TCP 443. For the domain service, the database returns 53 for both TCP and UDP. These observations show the configured defaults and registry entries; they do not establish that any service is listening on the listed ports.
An HTTPConnection explicitly pointed at TCP 443 sends a normal HTTP request and receives status 200 with plain HTTP on TCP 443. Changing the port argument did not change that client’s protocol. The server handles exactly what it implements, regardless of the number traditionally associated with a different transport security expectation.
An HTTPSConnection to that same endpoint fails during TLS negotiation. In the recorded environment, OpenSSL reports WRONG_VERSION_NUMBER because the peer is a plain HTTP server. This is not an expired-certificate test: no valid TLS session was established to authenticate a certificate. Diagnose the protocol mismatch before trying to repair a trust chain that the server never supplied.
While the TCP listener remains active, another socket binds UDP 443 and completes a datagram request and reply. There is no bind conflict because TCP and UDP have separate port namespaces. This does not demonstrate HTTP/3 or QUIC; the UDP payload is explicitly an arbitrary test message. A firewall rule or probe for TCP 443 does not by itself cover UDP 443.
Keep the complete endpoint in configuration
Use defaults where they simplify a deployment, and use explicit alternatives where topology or multiple instances require them. Nonstandard ports can reduce some indiscriminate traffic, but they do not provide meaningful authentication or confidentiality. Base those properties on verified identities, encrypted transport where required, application permissions, and an access policy appropriate to the service’s legitimate callers.
Include the scheme or protocol, address, port, and expected server identity in connection documentation. A reverse proxy may terminate public HTTPS on 443 and speak HTTP to a private backend on another port. That is a deliberate pair of connections, not a contradiction in the port registry. Test each hop according to its own protocol and trust boundary.
When a connection fails, first distinguish no listener, rejected network access, incompatible protocol, certificate failure, and application rejection. They can occur at the same numeric port while requiring different fixes. Keep the response or specific exception alongside the destination and transport. Port conventions help find the right starting point; successful protocol and identity checks establish whether the endpoint fulfills its contract.
Further reading
- Content-Type should describe the bytes you actually send
- HTTP methods are a promise about side effects
Sources
- IANA service and port registry, registered service names, numbers and transport protocols, checked 2026-09-12.
- Linux services(5), the local service-name database and its lack of service activation, checked 2026-09-12.
- Python 3.11 socket, service-name lookups and separate transport sockets, checked 2026-09-12.