用 Python 检查 CIDR 网段重叠:部署前发现重复与包含
网段清单里同时写了 192.0.2.0/24 和 192.0.2.128/25,两行语法都合法,却分配了重复的地址范围。等容器、VPN 或测试环境部署以后再排查,往往要把配置重新翻一遍。
本文面向初学者到中级读者,用 Python 标准库做一个只读的 CIDR 重叠检查器:检查本地清单,报告第一对冲突及行号,用退出码阻止后续操作。不扫描网络,不修改 Linux 路由、防火墙或服务配置。
1. 方法:检查范围,不逐个枚举地址
CIDR 写成“网络地址/前缀长度”,例如 /24 表示 IPv4 地址的前 24 位属于网络部分。如果还分不清单个地址与网络,可先看Python 验证 IPv4/IPv6 地址;这里检查的是整段地址的交集,不是刷题中的字符串规则。
清单采用以下规则:
- 每行一个 IPv4 或 IPv6 网段,必须显式写数字前缀;允许空行和
#后的注释。 - 网络地址必须对齐前缀,拒绝
192.0.2.9/24这类主机位不为零的输入,不自动改成另一个范围。 - 相同网段、包含关系都视为重叠;相邻但没有交集的两个网段允许通过。IPv4、IPv6 分开比较。
- 先校验完整份文件,再按输入顺序查第一对冲突。空清单不是“通过”,而是输入错误。
ip_network() 官方文档说明了严格解析;overlaps()用于判断网络交集。本例不展开主机列表,即使包含 IPv6 大网段也不会枚举每个地址。
重叠不一定是业务错误。 路由汇总、允许列表中的包含关系可能是有意的。本工具适合“同一规划范围内,网段必须互不相交”的清单,不应直接拿来判定任意路由表是否正确。
2. 准备:新建独立演示目录
需要 Python 3.9 或更新版本,无第三方依赖。本文命令在 Linux、Python 3.11.4 下测试,适用于 bash、zsh。
python3 --version
mkdir cidr-check-demo
cd cidr-check-demo
把下一节保存为当前目录的 check_cidrs.py,也可下载同一份完整脚本。后面的重定向只用于演示文件,会覆盖同名文件;重复实验请新建目录。
3. 实现:严格解析、按版本比较
"""Find the first overlap in a small local CIDR inventory. Python 3.9+."""
import argparse
import ipaddress
from pathlib import Path
import sys
MAX_BYTES = 128 * 1024
MAX_NETWORKS = 1000
def read_networks(path):
if not path.is_file():
raise ValueError("input must be a regular file")
with path.open("rb") as stream:
raw = stream.read(MAX_BYTES + 1)
if len(raw) > MAX_BYTES:
raise ValueError(f"input exceeds {MAX_BYTES} bytes")
text = raw.decode("utf-8")
if text.startswith("\ufeff"):
raise ValueError("remove the UTF-8 BOM first")
rows = []
for line_number, line in enumerate(text.splitlines(), 1):
token = line.split("#", 1)[0].strip()
if not token:
continue
if len(rows) >= MAX_NETWORKS:
raise ValueError(f"more than {MAX_NETWORKS} networks")
if token.count("/") != 1 or "%" in token:
raise ValueError(f"line {line_number}: expected unscoped IP/prefix")
prefix = token.split("/", 1)[1]
if not prefix.isascii() or not prefix.isdecimal():
raise ValueError(f"line {line_number}: prefix must be decimal digits")
try:
network = ipaddress.ip_network(token, strict=True)
except ValueError as exc:
raise ValueError(f"line {line_number}: {exc}") from exc
rows.append((line_number, network))
if not rows:
raise ValueError("no networks found")
return rows
def first_overlap(rows):
for index, (left_line, left) in enumerate(rows):
for right_line, right in rows[index + 1:]:
if left.version == right.version and left.overlaps(right):
return left_line, left, right_line, right
return None
def main():
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("file", type=Path, help="UTF-8 file: one CIDR per line")
args = parser.parse_args()
try:
rows = read_networks(args.file)
conflict = first_overlap(rows)
except (OSError, ValueError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
if conflict:
left_line, left, right_line, right = conflict
print(f"OVERLAP: line {left_line} {left} <-> line {right_line} {right}")
return 1
print(f"OK: {len(rows)} networks; no overlaps within each IP version")
return 0
if __name__ == "__main__":
raise SystemExit(main())
脚本只读取可信本地目录中、已经停止写入的小型普通文件:最多 128 KiB、1000 个网段,符号链接会跟随目标。不用于恶意路径、并发替换的文件或可能挂起的网络文件系统;读取上限不等于超时保护。
数字前缀、必须有 /、拒绝 % 作用域标识,都是本工具额外定义的输入约束。不要因为库接受某种写法,就默认库存清单也应该接受它。
Python 官方入门说明展示了 strict=False 会清掉主机位,ip_interface() 则适合表示“某个主机地址及其网络”。本工具保留 strict=True,要求维护者先确认意图再修正清单;不能为了消除错误就静默扩大范围。
4. 运行:区分冲突与检查失败
- 查看帮助,再创建含有重叠的测试清单:
python3 check_cidrs.py --help
cat > networks.txt <<'CIDRS'
# planned subnets
192.0.2.0/24
192.0.2.128/25
2001:db8:1::/64
CIDRS
if python3 check_cidrs.py networks.txt; then
rc=0
else
rc=$?
fi
printf 'exit=%s\n' "$rc"
帮助信息之后,检查命令输出:
OVERLAP: line 2 192.0.2.0/24 <-> line 3 192.0.2.128/25
exit=1
行号包含注释和空行。if 分支保留非零退出码,因此即使 shell 启用了 set -e,也能看到冲突结果。
- 如果真实意图是两个互不重叠的
/25,创建修正后的另一份清单:
cat > networks-fixed.txt <<'CIDRS'
192.0.2.0/25
192.0.2.128/25
2001:db8:1::/64
CIDRS
python3 check_cidrs.py networks-fixed.txt
OK: 3 networks; no overlaps within each IP version
这是示例规划,不是对真实网络的改动建议。先确认 /24 是否本就要包含后面的子网,再决定改哪一行。
| 退出码 | 含义 | 输出位置 |
|---|---|---|
| 0 | 完整清单合法,未发现重叠 | 标准输出 OK: |
| 1 | 找到第一对重叠网段 | 标准输出 OVERLAP: |
| 2 | 文件、编码、格式、规模或参数错误 | 标准错误;没有成功报告 |
--help 输出帮助并退出 0。自动化中应显式传递失败状态;把下一段放在独立脚本里运行,失败时会结束那个脚本:
if python3 check_cidrs.py networks-fixed.txt; then
printf 'NEXT: inventory passed; review the deployment separately\n'
else
rc=$?
printf 'STOP: CIDR preflight exit=%s\n' "$rc" >&2
exit "$rc"
fi
这里只允许进入人工审阅后的下一步,并不真正部署。网段检查通过也不代表连通性正常:域名问题继续看系统 DNS 解析排查,服务可达性则用TCP 端口检查单独验证。
5. 验证:边界、IPv6 与输入不变
在同一目录执行自检。测试只创建自己的临时目录,检查实际退出码和输出类别,并确认输入 SHA-256 没有变化:
python3 - <<'PY_CHECK'
import hashlib
from pathlib import Path
import subprocess
import sys
import tempfile
cases = [
("adjacent", "192.0.2.0/25\n192.0.2.128/25\n", 0),
("nested", "192.0.2.0/24\n192.0.2.128/25\n", 1),
("duplicate", "192.0.2.0/24\n192.0.2.0/24\n", 1),
("ipv6", "2001:db8::/32\n2001:db8:1::/48\n", 1),
("mixed", "192.0.2.0/24\n2001:db8::/32\n", 0),
("host-bits", "192.0.2.9/24\n", 2),
("empty", "# nothing planned yet\n", 2),
]
with tempfile.TemporaryDirectory(prefix="cidr-tests-", dir=".") as folder:
path = Path(folder) / "input.txt"
for label, text, expected in cases:
path.write_text(text, encoding="utf-8")
before = hashlib.sha256(path.read_bytes()).digest()
result = subprocess.run(
[sys.executable, "check_cidrs.py", str(path)],
capture_output=True, text=True, timeout=10,
)
assert result.returncode == expected, (label, result)
if expected == 2:
assert not result.stdout and result.stderr.startswith("ERROR:")
else:
assert not result.stderr
assert result.stdout.startswith("OVERLAP:" if expected else "OK:")
assert hashlib.sha256(path.read_bytes()).digest() == before
print(f"PASS: {label}; exit={result.returncode}; input unchanged")
PY_CHECK
预期打印 7 行 PASS::相邻网段和混合版本通过;包含、重复、IPv6 包含返回 1;主机位错误和空清单返回 2。任一断言失败都会停止,而不是继续报告通过。
还可以复现主机位错误,注意诊断写到标准错误:
printf '192.0.2.9/24\n' > host-bits.txt
if python3 check_cidrs.py host-bits.txt; then
rc=0
else
rc=$?
fi
printf 'exit=%s\n' "$rc"
ERROR: line 1: 192.0.2.9/24 has host bits set
exit=2
6. 排错:不要把错误清单当成安全结果
| 现象 | 直接处理 |
|---|---|
has host bits set |
确认是网络还是接口地址;网络清单应填写对齐后的网络地址,不要盲目切换 strict=False。 |
expected unscoped IP/prefix |
显式填写 /24、/64 等数字前缀;该清单不接受主机名或 %eth0。 |
prefix must be decimal digits |
使用 /24,不要使用 /255.255.255.0 或行末额外文字;注释放在 # 后。 |
| 编码错误或 BOM 提示 | 将原文件另存为无 BOM 的 UTF-8,保留原始副本后重新检查。 |
| 只显示一个冲突 | 这是“第一对”检查器;人工修正该冲突后重跑,不是全部冲突报告。 |
| 文件过大或网段太多 | 不要按块独立检查并宣称整体无冲突;跨块仍可能重叠。应改用能覆盖全量输入的算法。 |
最多 1000 个网段时,成对比较最多 499500 次,复杂度为 O(n²)。这个限制适合小型配置审阅,不是大型地址管理平台。工具不判断地址是否能在公网路由,也不替你检查实际路由、防火墙或地址占用。
7. 总结
先把网段清单严格解析,再检查同一 IP 版本内的交集,可以在部署前发现包含和重复分配。保留行号、区分退出码,并用故障样本验证检查器,才能把它接到自动化入口。检查通过只代表这份清单满足互不重叠的规则,实际网络仍需单独验证。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-cidr-overlap-check.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。