Technical note
将IP网段批量转换为RouterOS Address List
RouterOS的Address List可以被Filter、Mangle和NAT规则重复引用,很适合保存地区、运营商或云厂商的网段。公开IP地址库通常只提供一行一个CIDR的文本文件,数量多时,手工改成RouterOS命令既费事,也很难及时发现坏数据。
最简单的写法是用Python直接拼接命令,遇到导入错误就用on-error={}跳过。这样生成得快,问题也很明显:非法地址、IPv4和IPv6混用、重复网段都会被留到路由器处理,导入结束后甚至不知道漏了哪些条目。更稳妥的做法是在生成.rsc之前完成严格校验、去重和合并,RouterOS只负责执行已经检查过的命令。
输入文件只保留CIDR
输入文件使用一行一个网段的形式,空行和#之后的注释会被忽略。例如:
# documentation networks
192.0.2.0/25
192.0.2.128/25
198.51.100.0/24
198.51.100.0/24
这里故意放了两个可以合并的相邻网段和一个重复项。生成时,Python标准库ipaddress会把它们压缩成:
192.0.2.0/24
198.51.100.0/24
脚本使用严格模式解析CIDR。像192.0.2.1/24这样带有主机位的写法会直接报错,而不是悄悄改成192.0.2.0/24。对于要进入防火墙的外部数据,明确失败比自动猜测更容易发现上游变化。
生成RouterOS导入文件
下面的脚本只使用Python标准库,要求Python 3.10或更高版本:
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ipaddress
import re
from pathlib import Path
Network = ipaddress.IPv4Network | ipaddress.IPv6Network
LIST_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+$")
def read_networks(path: Path, family: int) -> list[Network]:
networks: list[Network] = []
errors: list[str] = []
for line_number, raw_line in enumerate(
path.read_text(encoding="utf-8").splitlines(), start=1
):
value = raw_line.split("#", maxsplit=1)[0].strip()
if not value:
continue
try:
network = ipaddress.ip_network(value, strict=True)
except ValueError as error:
errors.append(f"第{line_number}行:{value}({error})")
continue
if network.version != family:
errors.append(
f"第{line_number}行:{value}(期望IPv{family})"
)
continue
networks.append(network)
if errors:
raise SystemExit("输入文件存在错误:\n" + "\n".join(errors))
if not networks:
raise SystemExit("输入文件中没有可用网段")
return list(ipaddress.collapse_addresses(networks))
def render_routeros(
networks: list[Network], list_name: str, family: int
) -> str:
menu = (
"/ip firewall address-list"
if family == 4
else "/ipv6 firewall address-list"
)
marker = f"managed:{list_name}"
lines = [
f'/log info "Loading {list_name} address list"',
menu,
f'remove [find list="{list_name}" comment="{marker}"]',
]
lines.extend(
f'add address={network.with_prefixlen} '
f'list="{list_name}" comment="{marker}"'
for network in networks
)
lines.append(f'/log info "Loaded {list_name} address list"')
return "\n".join(lines) + "\n"
def main() -> None:
parser = argparse.ArgumentParser(
description="Convert CIDR lines to a RouterOS address-list script"
)
parser.add_argument("input", type=Path)
parser.add_argument("output", type=Path)
parser.add_argument("--list-name", required=True)
parser.add_argument(
"--family", choices=("ipv4", "ipv6"), default="ipv4"
)
args = parser.parse_args()
if not LIST_NAME_PATTERN.fullmatch(args.list_name):
raise SystemExit(
"列表名只能包含英文字母、数字、点、下划线和连字符"
)
if args.output.exists():
raise SystemExit(f"输出文件已存在:{args.output}")
family = 4 if args.family == "ipv4" else 6
networks = read_networks(args.input, family)
content = render_routeros(networks, args.list_name, family)
args.output.write_text(content, encoding="utf-8")
print(f"已生成{args.output},共{len(networks)}个网段")
if __name__ == "__main__":
main()
假设脚本保存为cidr_to_routeros.py,输入文件为networks.txt,生成IPv4列表:
python3 cidr_to_routeros.py \
networks.txt \
region_ipv4-20251105.rsc \
--list-name region_ipv4
需要生成IPv6列表时显式指定地址族:
python3 cidr_to_routeros.py \
networks-v6.txt \
region_ipv6-20251105.rsc \
--list-name region_ipv6 \
--family ipv6
列表名被限制在字母、数字、点、下划线和连字符内,是为了避免未经转义的名称破坏RouterOS脚本语法。输出文件已经存在时也会停止,避免一次重新生成覆盖掉仍需要对比的旧版本。
只替换脚本管理的条目
生成的.rsc大致如下:
/log info "Loading region_ipv4 address list"
/ip firewall address-list
remove [find list="region_ipv4" comment="managed:region_ipv4"]
add address=192.0.2.0/24 list="region_ipv4" comment="managed:region_ipv4"
add address=198.51.100.0/24 list="region_ipv4" comment="managed:region_ipv4"
/log info "Loaded region_ipv4 address list"
脚本没有直接删除region_ipv4中的所有记录,只删除comment="managed:region_ipv4"的条目。这样,同名列表中临时增加的手工记录和RouterOS动态生成的记录不会被顺手清掉。
这仍然不是事务更新:旧的托管条目会先删除,新条目再逐行加入。地址列表被实时规则引用时,中间会有一个很短的空窗;如果导入在中途失败,还可能只留下部分新数据。对连续性要求高的规则,应该生成带日期的新列表名,完整导入并检查后,再把引用它的Filter或Mangle规则切换到新名称。
先Dry Run再正式导入
把.rsc上传到RouterOS的Files后,先备份当前列表。RouterOS 7.13及以后可以只导出指定Address List:
/ip firewall address-list export \
where list=region_ipv4 file=region_ipv4-before
正式执行前使用Verbose模式的Dry Run检查语法:
/import file-name=region_ipv4-20251105.rsc verbose=yes dry-run=yes
Dry Run通过以后再导入:
/import file-name=region_ipv4-20251105.rsc verbose=yes
最后确认条目数量,并抽查合并后的网段:
/ip firewall address-list print count-only where list=region_ipv4
/ip firewall address-list print where list=region_ipv4
Dry Run只能检查脚本执行和语法问题,不能证明外部IP分类准确,也不能判断某条防火墙规则是否应该使用这份列表。Address List本身不会改变流量,真正的放行、丢弃、标记或分流仍由引用它的Filter、Mangle、NAT和路由配置决定。
外部列表要保留来源和更新时间
metowolf/iplist提供国家、地区、运营商和云厂商等分类的CIDR文本,并持续更新。使用这类列表时,最好同时保留原始下载文件和生成日期,而不是只留下最终.rsc。当条目数量突然大幅变化时,先比较新旧输入,再决定是否导入。
IP归属数据天然会变化,也可能存在分类误差。它适合做分流、统计或辅助匹配,不适合作为唯一的安全身份依据。尤其是白名单场景,不能因为某个地址出现在第三方地区列表里,就跳过认证和更精确的访问控制。