默认端口让客户端在协议或配置已经给出数字时省略它。HTTP 常用 TCP 八十,HTTPS 常用 TCP 四百四十三,SSH 常用 TCP 二十二。这些分配减少了配置工作,却不会检查监听发送的字节,也不会建立对端身份。一个端点还包含地址和传输协议;客户端最终使用什么应用协议,必须与监听真正提供的能力一致。

坏掉的样子

把服务迁移到其他端口,不会让现有客户端自动发现变化。URL、数据库连接配置、健康探测、代理和访问规则,可能各自保存目的地。只改服务器,可能让探测仍检查旧监听,或让客户端连接一个已经无人使用的端口。需要记录预期映射,并检查所有受影响的真实调用方,不能仅凭新服务启动成功就宣布迁移完成。

熟悉的数字也容易造成错误信任。进程可以在 TCP 四百四十三提供明文 HTTP,也可以在 TCP 八十提供其他协议。选择 HTTPS 意味着实际执行 TLS 和预期证书检查,单独选择四百四十三不会增加加密。同样,TCP 建连成功既不证明下一步对话符合应用协议,也不证明对端获得了服务该身份的授权。

服务名称数据库描述映射,不管理服务生命周期。Linux 的 /etc/services 将名称映射到端口与传输协议,添加记录不会启动守护进程,也不会打开防火墙。该文件可能不同于另一台主机的数据库或当前 IANA 登记内容。把服务名称转换成端口,与把主机名称解析成地址,是连接建立之前两项不同输入,不能混为同一种发现机制。

快速验证

将脚本保存为 check-default-ports.py,准备 Docker 后执行 python3 check-default-ports.py。固定 Linux 镜像中的 Python 3.11.16 不发布宿主端口。实验有意在 TCP 四百四十三启动明文 HTTP,随后让 UDP 套接字绑定同一个数字。所有监听都可丢弃,不会替换宿主已有的生产 HTTPS 服务,也不会要求修改生产端口分配:

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)

没有指定端口时,尚未连接的 HTTPConnection 与 HTTPSConnection 对象分别选择八十和四百四十三。本地服务查询也返回 HTTP/TCP 八十与 HTTPS/TCP 四百四十三;domain 服务在 TCP 和 UDP 下都对应五十三。这些观察证明配置默认值和数据库记录,没有证明列出的每个端口都存在真实监听,更没有探测所有这些服务的健康状态。

显式指向 TCP 四百四十三的 HTTPConnection 发送普通 HTTP 请求,收到状态二百及正文 plain HTTP on TCP 443。改变端口参数没有改变这个客户端使用的协议。服务器按照自己实现的协议处理请求,不会因为所选数字通常带有另一种安全预期,就自动为连接加入 TLS。协议与位置必须一起配置和验证。

HTTPSConnection 访问相同端点时,TLS 协商失败,实测 OpenSSL 报告 WRONG_VERSION_NUMBER,因为对端实际上是明文 HTTP。这不是证书过期实验:双方尚未建立能够认证证书的有效 TLS 会话。排障应先处理协议不匹配,不能反复修复一条服务器根本没有提供的证书链。保留具体错误,才能选对调查方向。

TCP 监听保持运行时,另一个套接字仍能绑定 UDP 四百四十三,并完成数据报请求和回复。因为 TCP 与 UDP 具有不同端口命名空间,它们没有发生绑定冲突。此处 UDP 载荷只是明确的测试消息,没有验证 HTTP/3 或 QUIC。针对 TCP 的探测或防火墙规则,也不能自动代表相同数字的 UDP 路径已经覆盖。

在配置中保留完整端点

默认值有助于简化部署;拓扑或多实例确实需要时,也可以采用明确的替代端口。非标准数字可能减少部分无差别扫描噪声,却不提供可靠认证和保密性。应通过身份验证、所需加密传输、应用权限,以及适合合法调用方的访问策略建立这些属性。不要把“扫描时不容易第一眼发现”当作实际安全边界。

连接文档应记录方案或协议、地址、端口及预期服务身份。反向代理可能在公网四百四十三终止 HTTPS,再通过另一个私有端口与后端使用 HTTP。这是经过设计的两条连接,不是端口登记发生矛盾。每一跳都应按照各自协议和信任边界测试,避免把前端成功加密,误写成后端传输也已经采用同样保护。

连接失败时,先区分没有监听、网络访问被拒绝、协议不兼容、证书校验失败和应用拒绝。它们可以发生在相同数字端口,却需要不同修复。把响应或明确异常与目的地、传输协议一起记录。默认端口提供查找起点,真正证明端点履行契约的,是成功的协议交换、正确身份和符合预期的应用结果。

延伸阅读

来源