相同回环地址和端口,在不同网络命名空间中指向不同服务。

回环地址返回调用方自己的网络命名空间

回环地址让进程通过本地 IP 协议栈通信,无须把交换送到物理网络链路。IPv4 为回环保留 127.0.0.0/8,其中最熟悉的是 127.0.0.1;IPv6 使用 ::1。在 Linux 中,相关的本地网络范围是网络命名空间。因此,同一台物理机器上的两个进程,可能对“自己的回环服务”具有不同答案,不能只按机器归属判断。 坏掉的样子 容器内客户端把数据库地址配置为 127.0.0.1,却希望访问宿主或另一容器时,经常失败。客户端实际请求的是自己命名空间中的监听,地址拼写没有错,拓扑理解却错了。宿主发布端口、容器接口地址和另一进程的回环,是不同目的地。选择连接配置前,应先确定客户端运行在哪里,以及真正服务由哪个网络范围提供。 回环也不保证任何探测都成功。目标端口需要监听,预期地址族需要可用,本地网络策略还可能影响交付。IPv4 请求成功不代表服务也监听 ::1;连接被拒绝,则可能只是调用方命名空间内对应地址与端口没有服务。不能因回环无须外部网线,就把协议栈、监听状态和访问规则当成始终有效的前提。 本地交付没有消除 IP 协议栈。回环流量仍经过套接字、协议处理和本地路由决策,只是没有沿外部物理网络绕行。测量一次回环请求,不能验证交换机配置、远程路由、互联网防火墙或公共服务名称的证书。解释健康探测时,应把这些路径分开,否则内部服务正常可能被误写成所有用户都能成功访问。 快速验证 将脚本保存为 check-loopback.py,准备 Docker 后执行 python3 check-loopback.py。程序用相同固定 Python 3.11.16 镜像创建两个一次性容器,均关闭 Docker 网络接入且不发布宿主端口。容器 A 在一万九千零九十一端口启动 IPv4 和 IPv6 监听,容器 B 起初没有该端口的服务。所有测试完成后,两个容器都会被移除: import json,subprocess,time,uuid image='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84' names=['xbcatm-loop-'+uuid.uuid4().hex[:10] for _ in range(2)] server=r''' import json,os,selectors,socket,sys selector=selectors.DefaultSelector() for family,address in [(socket.AF_INET,'127.0.0.1'),(socket.AF_INET6,'::1')]: listener=socket.socket(family,socket.SOCK_STREAM) if family==socket.AF_INET6:listener.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_V6ONLY,1) listener.bind((address,19091));listener.listen(8);selector.register(listener,selectors.EVENT_READ) print('ready',flush=True) while True: for key,events in selector.select(): connection,peer=key.fileobj.accept() with connection: connection.settimeout(1) connection.sendall(json.dumps({'service':sys.argv[1],'netns':os.readlink('/proc/self/ns/net')}).encode()) ''' probe=r''' import json,os,socket answers={} for address in ['127.0.0.1','::1']: try: with socket.create_connection((address,19091),timeout=1) as stream: pieces=[] while True: piece=stream.recv(1024) if not piece:break pieces.append(piece) answers[address]=json.loads(b''.join(pieces)) except ConnectionRefusedError:answers[address]={'error':'ECONNREFUSED'} print(json.dumps({'netns':os.readlink('/proc/self/ns/net'),'interfaces':socket.if_nameindex(),'answers':answers})) ''' def docker(*args): result=subprocess.run(['docker',*args],capture_output=True,text=True,timeout=30) assert result.returncode==0,result.stderr return result.stdout def inspect(name):return json.loads(docker('exec',name,'python3','-c',probe)) def wait_for(name,identity): deadline=time.monotonic()+5 while time.monotonic()<deadline: result=inspect(name) if all(answer.get('service')==identity for answer in result['answers'].values()):return result time.sleep(.05) raise AssertionError('listener did not become ready') try: for name,command in [(names[0],['python3','-u','-c',server,'A']), (names[1],['python3','-c','import time;time.sleep(120)'])]: docker('run','-d','--rm','--name',name,'--network','none','--memory','128m','--pids-limit','64',image,*command) a=wait_for(names[0],'A');b_before=inspect(names[1]) assert a['netns']!=b_before['netns'] assert all(v=={'error':'ECONNREFUSED'} for v in b_before['answers'].values()) docker('exec','-d',names[1],'python3','-u','-c',server,'B') b_after=wait_for(names[1],'B');a_after=inspect(names[0]) assert all(v.get('service')=='A' for v in a_after['answers'].values()) for result in [a,b_after,a_after]: assert all(answer['netns']==result['netns'] for answer in result['answers'].values()) assert all(any(interface[1]=='lo' for interface in result['interfaces']) for result in [a,b_before,b_after]) print(json.dumps({'A':a,'B_before_listener':b_before,'B_after_listener':b_after,'A_still_A':a_after},sort_keys=True)) finally: for name in names:subprocess.run(['docker','rm','-f',name],capture_output=True,text=True,timeout=10) 在 A 内执行的探测访问 127.0.0.1 和 ::1,都读到服务身份 A。响应同时携带服务器的网络命名空间标识,与探测进程自身标识一致。这样把成功的应用响应与客户端所在网络范围关联起来,而不是假定宿主上所有看起来本地的地址都共享一个监听。地址、上下文和返回身份共同组成证据。 ...

十一月 17, 2026 · 2 分钟 · XB Field Notes