某天你要确认一个自建服务到底有没有对外暴露。打开配置文件一看,明明写着 bind 127.0.0.1,你安心了。这个安心可能是错的。
现象
某个服务的配置文件里,绑定地址一栏清清楚楚写着 127.0.0.1。但实测发现它监听在 *:<端口>,从公网可以直接访问。
同类的事情还有另一例:某个服务的启动脚本里压根没有传地址参数,于是程序必然绑全部接口。你翻配置文件翻半天什么也找不到——因为配置不是问题所在。
这两种情况的外在表现一模一样:配置看起来没问题,监听却敞着。区别只在于,一个是被程序忽略了,一个是根本没有生效的配置。
根因
两种机制,都很常见。
第一种是"配置写了但没被遵守"。 某些程序(尤其是自带二进制的服务)会忽略配置里的绑定字段,自己绑到 ::(IPv6 通配),理由是"优先双栈"。这种情况下配置文件只是一份"意图声明",和实际行为脱节。常见的诱因是:程序读取的是另一个位置的配置文件、命令行参数的优先级高于配置文件、或者配置项的名字在这个版本里已经改了而程序选择了静默忽略。
第二种是"配置根本不存在"。 如果启动命令里没有提供绑定地址参数,而程序内部的默认值又是通配地址,那么你不写就等于全开。默认值通常是最宽松的那个,因为这样"开箱即用"最不容易出问题——代价就是安全性。
两种情况的共同教训是:配置文件表达的是意图,监听状态才是事实。
解决
核心原则:以实际监听状态为准,不要以配置为准。
ss -tln
# 或
lsof -nP -iTCP -sTCP:LISTEN
看输出里的本地地址(Local Address)列:
127.0.0.1:<端口>—— 真的只绑本机,安全;0.0.0.0:<端口>/*:<端口>—— 所有 IPv4 接口;[::]:<端口>—— 所有 IPv6 接口(通常也意味着公网可达)。
这里有个易踩的细节:* 在 ss 里表示通配地址,并不等于"任意来源",但效果就是所有接口可达。另外 :: 在开启双栈的机器上往往同时接受 IPv4 映射地址,所以看到 [::] 不要以为"只影响 IPv6"。
如果确认某个服务无法通过配置来限制绑定,就换一层思路:交给防火墙策略处理。 只放行来自内网网段的访问,剩下的全部丢弃。这比和程序的默认行为较劲要可靠得多,而且对后续升级也友好——不管程序怎么改默认值,防火墙那层不动。
# 只允许内网网段访问该端口
sudo iptables -A INPUT -p tcp --dport <端口> -s 192.168.0.0/16 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport <端口> -j DROP
排查时如果 ss -tln 没显示进程名,加上 -p 参数(需要足够权限):
sudo ss -tlnp
就能看到是哪个进程占着这个端口。如果连进程都认不出来,再补一手对照 PID:
sudo ss -tlnp | grep ':<端口> '
延伸与预防
把"核实监听地址"作为上线检查的固定动作,不管配置文件写的是什么。
更进一步,可以写一个定时脚本,定期扫描本机所有 LISTEN 状态的端口,输出监听地址"既不是回环、也不是指定内网 IP"的项,有新增就告警:
ss -tln | awk 'NR>1 && $4 !~ /^127\.0\.0\.1:/ && $4 !~ /^192\.168\./ {print $4}'
这样新起的服务一旦没收敛,你能在自己发现之前先收到提醒。第一次跑出来的结果往往会吓你一跳——原来有这么多服务默认绑在所有接口上。
这条规则的适用范围很广:任何"配置文件写了什么"和"实际行为是什么"可能脱节的地方(绑定地址、超时、并发数、缓存大小),都应该以实测为准——ss、curl、日志输出,才是可靠的证据。
顺带一提,ss -tln 只看 TCP 的监听;如果服务还用了 UDP(比如某些发现协议、日志上报),要再用 ss -uln 看一遍,别漏了另一半暴露面。