服务器的绑定地址决定套接字可以接受哪些本地目的地址。IPv4 监听绑定到 127.0.0.1,与绑定到通配地址 0.0.0.0,具有不同匹配范围。但任何一个设置都不能独立描述完整访问策略。路由、网络命名空间、包过滤、端口发布、代理和应用认证,共同决定请求怎样到达监听,以及连接之后能够执行什么操作。
坏掉的样子 通配监听不等于互联网已经能够访问服务。进程可能位于没有外部接口的隔离命名空间,也可能受到防火墙限制。反过来,回环监听也可以被接受远程请求的反向代理或隧道间接访问。判断谁能使用服务前,需要检查这些转发路径。不能看到监听地址就把网络路径、调用者身份和业务权限合并成一个结论。
IPv6 通配地址 :: 还涉及地址族选择。在 Linux 中,IPV6_V6ONLY 控制 IPv6 套接字是否也接受映射形式的 IPv4 连接。默认值取决于环境与应用,所以监听列表里只有一条 IPv6 项,并不能证明 IPv4 不可用。服务契约要求明确地址族边界时,应显式设置这个选项,并分别建立连接验证,而不是从显示格式推测行为。
指定地址也可能让服务在启动阶段失败。地址与端口被冲突套接字占用,和地址根本没有在本地配置,是不同错误。重用选项和非本地绑定机制会改变部分场景,本例没有启用这些机制。如果把所有绑定失败都解释成端口被占用,就可能去停止无关进程,却没有修复真正缺失的地址配置。错误码应与启动参数一起保留。
快速验证 将脚本保存为 check-bind-address.py,准备 Docker 后运行 python3 check-bind-address.py。固定 Python 3.11.16 镜像在一次性容器内使用 Linux 套接字,没有发布宿主端口。实验先比较两个 IPv4 回环地址,再显式改变 IPV6_V6ONLY。它不连接公网目标,也不修改宿主防火墙;所有监听和客户端都位于同一个可丢弃网络命名空间:
import json,subprocess,uuid image="python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84" program=r""" from contextlib import ExitStack import errno,json,socket def read_exact(stream,length): result=b'' while len(result)<length: part=stream.recv(length-len(result));assert part,'unexpected end of stream' result+=part return result def listener(stack,address,family=socket.AF_INET,v6only=None): stream=stack.enter_context(socket.socket(family,socket.SOCK_STREAM)) if v6only is not None: stream.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_V6ONLY,v6only) stream.settimeout(1);stream.bind((address,0));stream.listen(4) return stream def exchange(server,address): with socket.create_connection((address,server.getsockname()[1]),timeout=1) as client: connection,peer=server.accept() with connection: connection.settimeout(1);client.sendall(b'ping');assert read_exact(connection,4)==b'ping' connection.sendall(b'pong');assert read_exact(client,4)==b'pong' return {'listener':server.getsockname(),'accepted_local':connection.getsockname(),'peer':peer} def refused(server,address): try: with socket.create_connection((address,server.getsockname()[1]),timeout=1):pass except ConnectionRefusedError:return 'ECONNREFUSED' raise AssertionError('unexpectedly connected') with ExitStack() as stack: narrow=listener(stack,'127.0.0.1') one=exchange(narrow,'127.0.0.1');other=refused(narrow,'127.0.0.2') duplicate=stack.enter_context(socket.socket()) try:duplicate.bind(narrow.getsockname());raise AssertionError('duplicate bind succeeded') except OSError as error:assert error.errno==errno.EADDRINUSE unassigned=stack.enter_context(socket.socket()) try:unassigned.bind(('192.0.2.99',0));raise AssertionError('unassigned bind succeeded') except OSError as error:assert error.errno==errno.EADDRNOTAVAIL wildcard=listener(stack,'0.0.0.0') both=[exchange(wildcard,a) for a in ['127.0.0.1','127.0.0.2']] narrow.close();wildcard.close() only6=listener(stack,'::',socket.AF_INET6,1) six=exchange(only6,'::1');four_rejected=refused(only6,'127.0.0.1') dual=listener(stack,'::',socket.AF_INET6,0) mapped=exchange(dual,'127.0.0.1');assert mapped['peer'][0].startswith('::ffff:') print(json.dumps({'narrow':one,'other_loopback':other,'duplicate':'EADDRINUSE','unassigned':'EADDRNOTAVAIL', 'wildcard':both,'ipv6_only':six,'ipv6_only_v4':four_rejected,'dual_stack':mapped})) """ 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) 指定地址监听接受发往 127.0.0.1 的请求并返回 pong,同端口发往 127.0.0.2 的连接被拒绝。两个目的地都属于回环,因此这个比较只隔离地址匹配行为,没有声称验证外部网络过滤。各监听使用端口零,让内核选择可用端口;程序随后读取实际端口再连接,避免硬编码端口恰好被其他测试占用。
...