IPv4 前缀长度表示地址中有多少前导位标识网络。对于 192.0.2.5/24,网段是 192.0.2.0/24,普通广播地址是 192.0.2.255。配置接口通常会建立相应直连路由,但前缀不是完整路由算法。内核还要依据路由策略与路由表,为具体目的地选择路径,不能只看地址是否位于某个网段就推断全部行为。
坏掉的样子
前缀太短,会让主机把原本远端的目的地当成直接相连。保留地址 192.0.2.5,却把 /24 改成 /16,就把 192.0.3.9 纳入直连范围。在真实以太网中,这种错误可能让主机尝试解析一个根本不在当前链路上的邻居。于是地址看起来正确,下一跳决策却已经改变;反复核对地址字符串本身无法发现问题。
“不在自己的子网就走默认网关”也不完整。更具体的路由可以选择其他网关或接口;在已经选择的路由表内,更长的匹配前缀优先。明确的主机路由甚至可以覆盖本来位于接口子网内的目的地。应检查实际查询结果,而不是仅凭接口地址重新猜测内核会怎么做。默认路由只处理没有更具体匹配的相应流量。
地址数量计算也需要上下文。普通 /24 的网络地址与定向广播地址通常不用于主机分配,但“减二”不是统一公式。RFC 3021 允许点到点 /31 链路的两个地址都用作端点,/32 则标识单个地址。这些规则解释地址语义,没有证明配置中的两端能够通过当前物理拓扑交换数据。可用地址集合和实际通信仍须区分。
快速验证
将脚本保存为 check-subnet.py,准备 Docker 后执行 python3 check-subnet.py。程序给固定 Python 镜像安装 iproute2 6.1.0,再只向一次性容器授予 NET_ADMIN;容器没有外部网络接入。脚本创建虚拟 dummy 接口并修改该容器的路由,不改变宿主接口、路由或生产网关。构建下载与运行时路由实验也处于不同阶段:
from pathlib import Path
from tempfile import TemporaryDirectory
import json,subprocess,uuid
recipe='''FROM python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84
RUN apt-get update && apt-get install -y --no-install-recommends iproute2=6.1.0-3 && rm -rf /var/lib/apt/lists/*
'''
program=r'''
from ipaddress import IPv4Address,IPv4Interface,IPv4Network
import json,subprocess
def ip(*args):
result=subprocess.run(['ip',*args],capture_output=True,text=True,timeout=3)
assert result.returncode==0,result.stderr
return result.stdout
def route(address):return json.loads(ip('-j','route','get',address))[0]
interface=IPv4Interface('192.0.2.5/24');network=interface.network
assert str(network.network_address)=='192.0.2.0' and str(network.broadcast_address)=='192.0.2.255'
assert IPv4Address('192.0.2.77') in network and IPv4Address('192.0.3.9') not in network
try:IPv4Network('192.0.2.5/24');raise AssertionError('host bits accepted in strict network')
except ValueError:strict_rejected=True
point_to_point=[str(a) for a in IPv4Network('192.0.2.0/31').hosts()]
assert point_to_point==['192.0.2.0','192.0.2.1']
ip('link','add','lab0','type','dummy');ip('link','set','lab0','up')
ip('address','add','192.0.2.5/24','dev','lab0');ip('route','add','default','via','192.0.2.1','dev','lab0')
ip('route','add','203.0.113.0/24','via','192.0.2.2','dev','lab0')
direct=route('192.0.2.77');specific=route('203.0.113.9');default=route('198.51.100.9');before=route('192.0.3.9')
assert direct['dev']=='lab0' and 'gateway' not in direct
assert specific['gateway']=='192.0.2.2' and default['gateway']==before['gateway']=='192.0.2.1'
ip('route','add','192.0.2.77/32','via','192.0.2.1','dev','lab0');overridden=route('192.0.2.77')
assert overridden['gateway']=='192.0.2.1'
ip('address','del','192.0.2.5/24','dev','lab0');ip('address','add','192.0.2.5/16','dev','lab0')
ip('route','replace','default','via','192.0.2.1','dev','lab0');wrong=route('192.0.3.9')
assert wrong['dev']=='lab0' and 'gateway' not in wrong
print(json.dumps({'interface':str(interface),'network':str(network),'broadcast':str(network.broadcast_address),
'strict_host_bits_rejected':strict_rejected,'point_to_point_31_hosts':point_to_point,
'connected':direct,'more_specific_route':specific,'default':default,'host_route_overrides_subnet':overridden,
'destination_before_widening':before,'destination_after_wrong_16':wrong,'iproute_version':ip('-V').strip()}))
'''
name='xbcatm-subnet-'+uuid.uuid4().hex[:10]
def docker(*args,**kwargs):return subprocess.run(['docker',*args],capture_output=True,text=True,timeout=300,**kwargs)
try:
with TemporaryDirectory(prefix=name+'-') as directory:
root=Path(directory);(root/'Dockerfile').write_text(recipe)
build=docker('build','-t',name,str(root));assert build.returncode==0,build.stderr
result=docker('run','--rm','-i','--name',name,'--network','none','--cap-add','NET_ADMIN',
'--memory','128m','--pids-limit','64',name,'python3','-',input=program)
assert result.returncode==0,result.stderr
print(json.dumps(json.loads(result.stdout),sort_keys=True))
finally:
docker('rm','-f',name);docker('image','rm',name)
Python 区分接口 192.0.2.5/24 与其网段 192.0.2.0/24。严格网络解析拒绝前者,因为主机位非零;/31 示例则列出 192.0.2.0 和 192.0.2.1 两个可用端点。这个计算确认地址集合,实验没有配置可工作的点到点链路,也没有测试这两个地址之间的转发。不能把库返回两个值,写成真实链路已经验证成功。
dummy 接口配置 192.0.2.5/24,默认路由经过 192.0.2.1;203.0.113.0/24 的具体路由经过 192.0.2.2。真实 ip route get 对 192.0.2.77 不选择网关,对 203.0.113.9 选择后者,对 198.51.100.9 选择默认网关。输出同时记录接口和首选源地址,让下一跳选择可以与本机发送身份一起检查。
添加经过 192.0.2.1 的 192.0.2.77/32 路由后,目标虽然仍属于直连 /24,查询却转而选择网关。这一受控覆盖说明子网归属与最终路由决策必须分开。选中的路由表对这个单独地址具有更具体指令,并不需要为了表达例外而更换接口地址。直接把“同网段”当成永远不经网关,会遗漏这种合法配置。
扩大前缀前,192.0.3.9 使用默认网关;保留 192.0.2.5、将 /24 替换为 /16 后,查询直接选择 dummy 接口,不再包含网关。脚本断言真实内核决策发生改变,没有声称成功交付数据、观察到 ARP 失败,或者虚拟接口之外存在可工作的路由器。实验验证选择逻辑,链路通信需要另外的环境和证据。
修改掩码前先检查真实路由
检查生效的地址与前缀、相关策略规则,以及失败目的地的路由查询。多接口系统重现路径时,还应包含预期源地址和其他策略输入。存在来源路由、标记或不同路由域时,仅按目的地查询可能不同于应用实际路径。应保留诊断上下文,不能拿另一个进程或另一种来源条件下的结果覆盖真实调用方。
在以太网中,区分解析目标自身的链路地址,还是解析选中网关的链路地址。网关需要符合配置的链路和路由规则,路由文件里出现一个看起来合理的数字还不够。确认选路之后,再比较邻居状态与真实报文交付。显示下一跳只说明计划怎样发送,不能证明下一跳已经收到、转发或允许该应用流量。
规划前缀变更时,应一起检查它将创建或移除的直连路由及受影响目的地。变更后验证代表性的本地、具体路由和默认路由流量。掩码是路由与地址解释的一项输入,不是独立可达性承诺。这个区别既能解释意外扩大链路范围,也能解释更具体路由有意建立的例外,而不把正常路径选择误判成地址配置错误。
来源
- RFC 4632 CIDR,无类别前缀与最长匹配转发,已核验 2026-09-12。
- RFC 3021 point-to-point /31 links,点到点 /31 链路两端地址的使用,已核验 2026-09-12。
- Linux ip-route(8),路由查询、下一跳及明确主机路由,已核验 2026-09-12。
- Python 3.11 ipaddress,接口、网段及主机地址的区分,已核验 2026-09-12。