前言
服务器通过 EdgeRouter 接入上游网络后,IPv4 正常,IPv6 只有链路本地地址和 ULA,无法访问 IPv6 外网。同一上游网络中的直连服务器可以正常使用 IPv6。
排查确认了以下问题:
- 路由器存在一条只指定出口接口、未指定下一跳的错误 IPv6 默认路由。
- 上游 DHCPv6-PD 请求返回
NoPrefixAvail,未获得用于下游网络的委派前缀。 - LAN 使用上游相同的
/64时,需要 NDP 代理处理跨接口邻居发现,并配置 WAN 到 LAN 的回程路由。
修复采用 RA/SLAAC、ndppd 和策略路由,并通过 systemd 定时同步前缀。路由器及服务器的 IPv6 连通性、入站防火墙和整机重启后的自动恢复均已验证。
以下记录配置与验证过程。地址和接口名已替换为示例;2001:db8:100::/64 为文档专用前缀,使用时需替换为实际前缀。
一、检查 IPv6 地址与路由
1.1 检查地址类型及出口连通性
在服务器上检查 IPv6 地址、路由,并发起 IPv6 HTTPS 请求:
bash1234ip -6 addr show ip -6 route show curl --noproxy '*' -6 --connect-timeout 10 --max-time 20 \ -I https://www.cloudflare.com/
fe80::/10 是链路本地地址,通常用于同一链路上的通信;fc00::/7 范围内的 ULA 也不能直接当作公网地址使用。即使拿到了全球单播地址,缺少正确路由、邻居解析失败或者被防火墙拦截,外网仍然不通。
--noproxy '*' 禁用本次请求的代理,测试服务器本身的 IPv6 出口。-6 则限制目标连接使用 IPv6;DNS 查询本身不因此保证走 IPv6。
1.2 RA、SLAAC、DHCPv6 与 NDP 的作用
相关协议的作用如下:
| 机制 | 在本次问题中的作用 |
|---|---|
| RA,路由器通告 | 告知主机默认路由器、链路前缀及相关标志 |
| SLAAC,无状态地址自动配置 | 主机根据 RA 中允许自动配置的前缀生成地址 |
| DHCPv6 地址分配 | 由 DHCPv6 服务向客户端分配地址等参数 |
| DHCPv6-PD,前缀委派 | 向下游路由器委派用于其下游网络的前缀 |
| NDP,邻居发现 | 在链路上发现邻居,并解析目标 IPv6 地址对应的链路层地址 |
默认路由器由 RA 通告,开启 DHCPv6 不等于已配置默认路由。RFC 4861 和 RFC 4862 分别描述了邻居发现与 SLAAC 的相关机制。
WAN 获得 /64 内的地址,只能证明该接口具有地址配置。下游独立子网还需要上游提供前缀委派或相应的静态路由。
二、修复默认路由并检查前缀委派
2.1 删除错误的接口默认路由
排查时,路由器存在一条只指定出口接口的 IPv6 默认路由,形式如下:
text1default dev eth0
在这次以太网接入环境中,这条路由缺少上游下一跳。对于没有更具体路由的目的地址,它会让系统尝试直接在出口链路解析目的地址,而上游实际上要求把这些流量交给网关。
删除这条错误静态配置后,由上游 RA 学习到的默认路由可以正常承担转发,其形式变为:
text1default via fe80::1 dev eth0 proto ra
这里的接口和网关均为示例。fe80::1 这样的链路本地地址作为下一跳并不异常,只要同时绑定正确接口即可。
在 EdgeOS 配置模式中,删除对应配置的命令结构如下。执行前应先确认当前配置确实存在这一项,不要把它当作通用初始化命令:
bash123456configure # eth0 为示例名称,替换为实际 WAN 接口 delete protocols static interface-route6 ::/0 next-hop-interface eth0 commit save exit
删除后,在路由器上执行 IPv6 HTTPS 测试,确认 WAN 出口恢复,再检查 LAN 转发。
2.2 DHCPv6-PD 返回 NoPrefixAvail
随后尝试 DHCPv6 前缀委派。在本次测试中,三次请求都收到了 NoPrefixAvail,状态码为 6。该状态表示服务器没有可提供给请求方的前缀,含义可查阅 DHCPv6 协议中的状态码说明。
本次接入未获得 PD,因此无法按“获取委派前缀,再分配给 LAN”的方式配置。该结果仅对应本次请求及当时的上游策略。
三、确定 NDP 代理方案
DHCPv6 relay 转发的是 DHCPv6 消息。它本身不会同时完成 RA 传递、默认路由配置和跨接口的邻居发现。即使 DHCPv6 成功分配了地址,上游依然需要知道返回流量应该发到哪里。
上游提供链路前缀 /64,WAN 与 LAN 位于不同的二层链路。方案示意如下:
text1234567上游 IPv6 链路 | WAN:获得上游 /64 内的地址,学习 RA 默认路由 | EdgeRouter:路由转发、NDP 代理、防火墙 | LAN:广播同一 /64,终端通过 SLAAC 配置地址
LAN 广播相同的 /64 后,终端可以生成地址。但上游仍将这些地址视为链路上的邻居,在 WAN 一侧发送邻居请求;LAN 主机无法直接接收并回应。NDP 代理用于处理这部分查询。
本次未获得 PD,且需要保留现有 IPv4 路由配置,因此采用共享前缀的 NDP 代理方案。桥接会改变二层结构,未采用。
四、配置 ndppd、策略路由与防火墙
4.1 部署位置与环境检查
ndppd 和同步程序安装在 EdgeRouter 上,下游服务器不安装 ndppd。 下游接口需要启用 IPv6 自动配置并接受 RA。本次服务器已有这些设置,无需修改服务器的持久网络配置。
本文使用以下示例参数,操作前替换为自己的配置:
| 参数 | 本文示例 | 用途 |
|---|---|---|
| 路由器 SSH 目标 | router-admin@192.0.2.1 |
上传及登录,替换用户名和管理地址 |
| WAN | eth0 |
连接上游的以太网接口 |
| LAN | switch0 |
EdgeOS switch 类型接口 |
| 程序目录 | /config/ipv6-proxy |
本文指定的安装目录 |
| 策略表与优先级 | 10640 |
必须未被其他策略占用 |
192.0.2.1 是文档地址。接口名也是示例,不表示所有 EdgeRouter 都使用相同端口。下方脚本默认 LAN 为 switch 类型;若使用 ethernet 或 bridge 类型接口,还需修改脚本中的 RA 配置路径,不能只替换接口名称。
在 EdgeRouter 的普通 shell 中执行:
bash1234567891011uname -a dpkg --print-architecture file /bin/bash python --version systemctl --version ip -o link show ip -6 addr show dev eth0 ip -6 route show default ip -6 rule show sudo sysctl net.ipv6.conf.all.forwarding sudo sysctl net.ipv6.conf.eth0.accept_ra
本文对应 EdgeOS 3.0.1、MIPS32 大端用户态、Python 2.7 和 systemd。IPv6 转发应为 1;转发开启时,WAN 若仍通过 RA 学习路由,Linux 的 accept_ra 应为 2。本次在原生自动配置生效后满足这些条件。若尚未满足,先修复 WAN 的自动配置与转发,再安装代理。
WAN 原生自动配置的命令结构如下,接口名按实际情况修改:
bash12345configure set interfaces ethernet eth0 ipv6 address autoconf commit save exit
执行后重新检查公网地址、accept_ra 和 default via ... dev eth0 proto ra。路由器自身的 IPv6 HTTPS 测试必须先通过。已有 WAN IPv6 本机入站防火墙时,还需要允许上游 RA 和必要的邻居发现消息。
修改前在路由器上备份原生配置和相关运行状态:
bash12345678# 在 EdgeRouter 上执行,记录命令输出的备份目录 backup_dir="/config/ipv6-before-proxy-$(date +%Y%m%d-%H%M%S)" sudo mkdir -p "$backup_dir" sudo cp /config/config.boot "$backup_dir/config.boot" ip -6 route show table all | sudo tee "$backup_dir/routes.txt" >/dev/null ip -6 rule show | sudo tee "$backup_dir/rules.txt" >/dev/null sudo ip6tables-save | sudo tee "$backup_dir/ip6tables.txt" >/dev/null printf '%s\n' "$backup_dir"
以下步骤面向首次部署。如果已存在同名 ndppd 服务、安装目录或 post-config 钩子,先备份并比较文件,不要直接覆盖。
4.2 在开发电脑上编译 ndppd
在有 Docker 和 Git 的 Linux 开发电脑上执行以下命令。这里固定构建 MIPS32 大端程序,其他架构需要更换工具链。
bash12345678910111213mkdir -p edgerouter-ipv6-build cd edgerouter-ipv6-build git clone https://github.com/DanielAdolfsson/ndppd.git cd ndppd git checkout --detach 4f0301f94345c92b723392ed2d73f04f910e6c30 docker run --rm \ -v "$PWD:/src" -w /src debian:trixie sh -ec ' apt-get update apt-get install -y --no-install-recommends make g++-mips-linux-gnu file make CXX=mips-linux-gnu-g++ LDFLAGS=-static file ndppd '
检查 file ndppd 输出应包含 ELF 32-bit、MSB、MIPS 和 statically linked。只执行 make 会默认使用构建机器的编译器,可能得到无法在路由器上运行的 x86 程序。静态链接用于避免依赖构建容器中的动态库版本。
4.3 创建同步脚本和服务文件
以下命令在 Linux 开发电脑上执行,从上一步的 ndppd 源码目录开始:
bash123mkdir -p ../ipv6-proxy-deploy cp ndppd LICENSE ../ipv6-proxy-deploy/ cd ../ipv6-proxy-deploy
先创建同步脚本。修改开头的 WAN、LAN 和 TABLE,其中 RA 的第二项是 EdgeOS 接口类型:switch 接口用 switch,以太网接口用 ethernet,桥接口用 bridge。目录统一使用本文示例的 /config/ipv6-proxy。
bash123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183cat > prefix_sync.py <<'EOF' #!/usr/bin/python """Reconcile EdgeOS NDP/SLAAC with WAN /64s. Python 2.7 and 3, stdlib only.""" from __future__ import print_function import fcntl, grp, json, os, re, shlex, socket, subprocess, sys, tempfile, time BASE='/config/ipv6-proxy' RUN='/run/ndppd' WAN='eth0' LAN='switch0' TABLE='10640' RA=['interfaces','switch',LAN,'ipv6','router-advert'] CFG='/opt/vyatta/sbin/vyatta-cfg-cmd-wrapper' API='/bin/cli-shell-api' GRACE=300 def validate_prefix(prefix): try: addr, length=prefix.split('/') packed=socket.inet_pton(socket.AF_INET6,addr) first=bytearray(packed)[0] if length!='64' or first & 0xe0 != 0x20 or packed[8:]!=b'\0'*8: raise ValueError('not a public /64 network') return socket.inet_ntop(socket.AF_INET6,packed)+'/64' except (ValueError, socket.error): raise ValueError('invalid public /64: '+prefix) def wan_prefixes(text): result=set() for line in text.splitlines(): if re.search(r'\b(tentative|dadfailed|deprecated)\b',line): continue m=re.search(r'inet6 ([0-9a-fA-F:]+)/64\b',line) life=re.search(r'preferred_lft (forever|[0-9]+sec)',line) if not m or not life or life.group(1)=='0sec': continue p=socket.inet_pton(socket.AF_INET6,m.group(1)) try: result.add(validate_prefix(socket.inet_ntop(socket.AF_INET6,p[:8]+b'\0'*8)+'/64')) except ValueError: pass return sorted(result) def plan(state, active, now): active=sorted(set(validate_prefix(p) for p in active)) retiring={validate_prefix(p):expiry for p,expiry in state.get('retiring',{}).items() if expiry>now and p not in active} for p in state.get('active',[]): if p not in active and p not in retiring: retiring[validate_prefix(p)]=now+GRACE return {'active':active,'retiring':retiring} def proxy_config(prefixes): if not prefixes: return '' blocks=['# Generated by prefix_sync.py; edit the controller, not this file.'] for iface,other,autowire in [(WAN,LAN,True),(LAN,WAN,False)]: blocks.append('proxy %s {\n router yes\n timeout 1000\n ttl 30000'%iface) if autowire: blocks.append(' autowire yes') for p in sorted(prefixes): blocks.append(' rule %s {\n iface %s\n }'%(validate_prefix(p),other)) blocks.append('}') return '\n'.join(blocks)+'\n' def run(args, check=True): p=subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE) out,err=p.communicate() out=out.decode('utf-8','replace'); err=err.decode('utf-8','replace') if check and p.returncode: raise RuntimeError('%s: %s'%(args[0],(out+err).strip()[:1500])) return out,p.returncode def read(path, fallback=''): try: with open(path) as f:return f.read() except IOError:return fallback def atomic(path, content): if read(path,None)==content:return False fd,tmp=tempfile.mkstemp(prefix='.sync-',dir=os.path.dirname(path)) try: with os.fdopen(fd,'w') as f:f.write(content) os.chmod(tmp,0o644) os.rename(tmp,path) finally: if os.path.exists(tmp):os.unlink(tmp) return True def store(path, obj):return atomic(path,json.dumps(obj,sort_keys=True)+'\n') def route_sync(prefixes, managed): rules=run(['ip','-6','rule','show'])[0] routes=run(['ip','-6','route','show','table',TABLE])[0] for p in prefixes: marker='to %s iif %s lookup %s'%(p,WAN,TABLE) if not re.search(r'^'+re.escape(p)+r' dev '+LAN+r'\b',routes,re.M): run(['ip','-6','route','replace',p,'dev',LAN,'table',TABLE]) if marker not in rules: run(['ip','-6','rule','add','pref',TABLE,'iif',WAN,'to',p,'lookup',TABLE]) for p in sorted(set(managed)-set(prefixes)): marker='to %s iif %s lookup %s'%(p,WAN,TABLE) for _ in range(rules.count(marker)): run(['ip','-6','rule','del','pref',TABLE,'iif',WAN,'to',p,'lookup',TABLE]) if re.search(r'^'+re.escape(p)+r'\b',routes,re.M): run(['ip','-6','route','del',p,'dev',LAN,'table',TABLE]) def apply_ra(desired, managed): # These wrappers require the vyattacfg group and one stable session id. os.setgid(grp.getgrnam('vyattacfg').gr_gid) os.environ['CMD_WRAPPER_SESSION_ID']=str(os.getpid()) current=shlex.split(run([API,'listActiveNodes']+RA+['prefix'])[0]) wanted=sorted(desired['active']+list(desired['retiring'])) run([CFG,'begin']) try: for p in sorted(set(current)&set(managed)-set(wanted)): run([CFG,'delete']+RA+['prefix',p]) for p in wanted: values={'autonomous-flag':'true','on-link-flag':'true', 'preferred-lifetime':'900' if p in desired['active'] else '0', 'valid-lifetime':'1800' if p in desired['active'] else str(GRACE)} for key,value in sorted(values.items()): run([CFG,'set']+RA+['prefix',p,key,value]) run([CFG,'set']+RA+['default-lifetime','1800' if desired['active'] else '0']) run([CFG,'set']+RA+['managed-flag','false']) run([CFG,'commit']) except Exception: run([CFG,'discard'],False) raise finally: run([CFG,'end'],False) def sync(dry_run=False): registry=json.loads(read(BASE+'/managed-prefixes.json','[]')) for p in registry:validate_prefix(p) old=json.loads(read(RUN+'/state.json',json.dumps({'active':registry,'retiring':{}}))) links=run(['ip','-o','link','show','dev',WAN])[0] defaults=run(['ip','-6','route','show','default'])[0] addresses=run(['ip','-6','-o','addr','show','dev',WAN,'scope','global'])[0] online='LOWER_UP' in links and re.search(r'^default via .* dev '+WAN+r'\b',defaults,re.M) active=wan_prefixes(addresses) if online else [] desired=plan(old,active,int(time.time())) prefixes=sorted(desired['active']+list(desired['retiring'])) managed=sorted(set(registry+prefixes)) if dry_run: print(json.dumps({'online':bool(online),'desired':desired,'managed':managed},sort_keys=True)) return # Remember ownership before touching config, so a partial commit is retried safely. store(BASE+'/managed-prefixes.json',managed) route_sync(prefixes,[]) proxy_changed=atomic(RUN+'/ndppd.conf',proxy_config(prefixes)) if prefixes: running=run(['systemctl','is-active','--quiet','ndppd'],False)[1]==0 if proxy_changed or not running:run(['systemctl','restart','ndppd']) changed=desired!=old or not os.path.exists(RUN+'/state.json') if changed:apply_ra(desired,managed) route_sync(prefixes,managed) if not prefixes:run(['systemctl','stop','ndppd']) store(RUN+'/state.json',desired) store(BASE+'/managed-prefixes.json',prefixes) if changed: run(['logger','-t','ndppd-sync','active=%s retiring=%s'%(desired['active'],sorted(desired['retiring']))]) print(json.dumps(desired,sort_keys=True)) def main(): os.environ['PATH']='/usr/sbin:/usr/bin:/sbin:/bin:/opt/vyatta/bin:/opt/vyatta/sbin' if not os.path.isdir(RUN):os.makedirs(RUN,0o755) with open(RUN+'/sync.lock','w') as lock: try:fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB) except IOError:return try:sync('--dry-run' in sys.argv) except Exception as e: run(['logger','-t','ndppd-sync','ERROR: '+str(e)],False) print('ERROR: '+str(e),file=sys.stderr) sys.exit(1) if __name__=='__main__':main() EOF
创建代理服务。配置文件由同步脚本生成,服务以前台方式运行:
bash1234567891011121314cat > ndppd.service <<'EOF' [Unit] Description=NDP proxy for automatically learned WAN IPv6 prefixes After=network.target ConditionPathExists=/run/ndppd/ndppd.conf [Service] Type=simple Environment=PATH=/usr/sbin:/usr/bin:/sbin:/bin ExecStartPre=/usr/bin/test -s /run/ndppd/ndppd.conf ExecStart=/config/ipv6-proxy/ndppd -c /run/ndppd/ndppd.conf Restart=on-failure RestartSec=5 EOF
创建一次性同步服务,在 EdgeOS 原生配置加载完成后运行:
bash1234567891011cat > ndppd-sync.service <<'EOF' [Unit] Description=Synchronize EdgeOS IPv6 prefixes, NDP proxy and return routing After=network.target vyatta-router.service ConditionPathExists=/run/ndppd/edgeos-ready [Service] Type=oneshot ExecStart=/usr/bin/python /config/ipv6-proxy/prefix_sync.py TimeoutStartSec=90 EOF
创建定时器,每次同步结束后约 30 秒再次执行:
bash12345678910111213cat > ndppd-sync.timer <<'EOF' [Unit] Description=Check WAN IPv6 prefix changes every 30 seconds [Timer] OnBootSec=30 OnUnitInactiveSec=30 AccuracySec=1 Unit=ndppd-sync.service [Install] WantedBy=timers.target EOF
创建开机钩子。它恢复单元链接、标记配置已就绪并启动 timer:
bash123456789101112131415cat > 50-ndppd <<'EOF' #!/bin/sh set -eu mkdir -p /run/ndppd for unit in ndppd.service ndppd-sync.service ndppd-sync.timer; do ln -sf "/config/ipv6-proxy/$unit" "/etc/systemd/system/$unit" done systemctl daemon-reload if [ -L /etc/systemd/system/multi-user.target.wants/ndppd.service ]; then rm /etc/systemd/system/multi-user.target.wants/ndppd.service fi systemctl enable ndppd-sync.timer touch /run/ndppd/edgeos-ready systemctl start ndppd-sync.timer EOF
文件创建完成后,检查 Python 与 shell 语法,再上传。以下操作仍在开发电脑执行:
bash12345678910python3 -m py_compile prefix_sync.py sh -n 50-ndppd # 替换为路由器的实际管理账号和地址 router_target=router-admin@192.0.2.1 ssh "$router_target" 'mkdir -p /tmp/ipv6-proxy-upload' scp ndppd LICENSE prefix_sync.py \ ndppd.service ndppd-sync.service ndppd-sync.timer 50-ndppd \ "$router_target:/tmp/ipv6-proxy-upload/" ssh "$router_target"
使用自定义 SSH 端口时,ssh 使用 -p,scp 使用 -P;也可以在本机 SSH 配置中设置别名。
登录 EdgeRouter 后安装文件,此时还不启动服务:
bash1234567891011sudo install -d -m 0755 /config/ipv6-proxy sudo install -m 0755 /tmp/ipv6-proxy-upload/ndppd /config/ipv6-proxy/ndppd sudo install -m 0644 /tmp/ipv6-proxy-upload/LICENSE /config/ipv6-proxy/LICENSE sudo install -m 0644 /tmp/ipv6-proxy-upload/prefix_sync.py /config/ipv6-proxy/ sudo install -m 0644 /tmp/ipv6-proxy-upload/ndppd.service /config/ipv6-proxy/ sudo install -m 0644 /tmp/ipv6-proxy-upload/ndppd-sync.service /config/ipv6-proxy/ sudo install -m 0644 /tmp/ipv6-proxy-upload/ndppd-sync.timer /config/ipv6-proxy/ sudo install -m 0755 /tmp/ipv6-proxy-upload/50-ndppd /config/ipv6-proxy/ file /config/ipv6-proxy/ndppd sudo python -m py_compile /config/ipv6-proxy/prefix_sync.py
后续启动服务时,如果执行程序时报 Exec format error,返回检查编译架构;如果程序存在却提示找不到文件,检查 ELF 解释器与动态链接依赖。不要继续执行后续启动步骤。
4.4 NDP 代理配置
同步程序会生成 /run/ndppd/ndppd.conf,内容结构如下。这段配置用于解释代理行为,无需手工保存,也不要把文档示例前缀写入路由器。 实際前缀由脚本读取 WAN 地址后生成:
conf12345678910111213141516171819# 示例配置,不可直接用于生产网络 proxy eth0 { router yes timeout 1000 ttl 30000 autowire yes rule 2001:db8:100::/64 { iface switch0 } } proxy switch0 { router yes timeout 1000 ttl 30000 rule 2001:db8:100::/64 { iface eth0 } }
WAN 一侧的规则将该前缀的邻居查询送往 LAN,LAN 一侧则能够查询 WAN 上的同前缀邻居。这里使用 iface 探测实际邻居,没有让代理无条件宣称整个 /64 的所有地址都存在。
LAN 仍由 EdgeOS 原生 RA 配置广播前缀,启用自主地址配置,managed-flag 为 false。原来的 ULA 保留,服务器通过 SLAAC 获得全球单播地址,不需要给每台服务器手工写固定公网地址。
4.5 配置 WAN 入站回程路由
测试发现,仅依靠 autowire 学习 /128 主机路由,仍存在回程转发失败。
上游可能已经缓存了邻居信息,在代理尚未学习到对应 /128 路由时,返回数据包就已经到达 WAN。此时主路由表中的共享 /64 仍指向 WAN,返回包可能再次被选到 WAN,而没有转入 LAN。
因此又增加了一张单独的路由表,并限制只有从 WAN 进入、目的地址属于共享前缀的流量查询它:
bash1234# 以下均为示例值;先检查规则与表号是否已被占用 sudo ip -6 route replace 2001:db8:100::/64 dev switch0 table 10640 sudo ip -6 rule add pref 10640 iif eth0 \ to 2001:db8:100::/64 lookup 10640
这两条命令说明同步程序所执行的操作,按本文自动部署时不必手工执行。它们是运行时配置,且 ip rule add 重复执行会产生重复规则;同步程序负责检查现状、避免重复添加,并在重启后重建。
iif eth0 将规则限制为 WAN 入站流量。若直接将主表的 /64 指向 LAN,访问上游同前缀主机的流量也会被送入 LAN。配置后的路由选择如下:
| 数据包 | 路由选择 |
|---|---|
| 从 WAN 进入,目标为共享前缀中的下游地址 | 查专用表,转入 LAN |
| 从 LAN 进入,访问上游同前缀主机 | 继续查主表,经 WAN 到达上游 |
| 目标是路由器自己的地址 | 优先由优先级为 0 的 local 表处理 |
4.6 配置 WAN 入站防火墙
服务器获得全球单播地址后,还检查了 WAN 入站转发规则。最终保留的策略是允许已建立连接及其关联流量,允许 ICMPv6,其余入站转发默认丢弃。
ICMPv6 不只有 ping,还参与邻居发现和错误报告,不能为了关闭 ping 就整体屏蔽。这里描述的是 WAN 到 LAN 的转发策略,路由器自身管理面的访问控制应另行检查。
验证时,从上游直连主机访问内网服务器的 TCP/22 超时,同时观察到对应防火墙 DROP 计数增加。确认请求命中了丢弃规则。
下面给出独立的新规则集示例。在 EdgeOS 配置模式执行,WAN 名称按实际情况修改:
bash1234567891011configure set firewall ipv6-name IPV6_PROXY_IN default-action drop set firewall ipv6-name IPV6_PROXY_IN rule 10 action accept set firewall ipv6-name IPV6_PROXY_IN rule 10 state established enable set firewall ipv6-name IPV6_PROXY_IN rule 10 state related enable set firewall ipv6-name IPV6_PROXY_IN rule 20 action accept set firewall ipv6-name IPV6_PROXY_IN rule 20 protocol ipv6-icmp set interfaces ethernet eth0 firewall in ipv6-name IPV6_PROXY_IN commit save exit
已有 WAN 入站 IPv6 规则集时,应将所需规则合并到现有规则集,不要直接替换绑定。 上面的命令适用于该方向没有既有规则集的情况。配置提交报错时先查看错误并处理,不要继续启动自动同步,更不要以手工创建 ip6tables 链代替原生配置提交。
五、自动同步前缀与启动配置
5.1 前缀检测与同步范围
为处理上游前缀变化和路由器重启,增加同步程序,统一维护 RA、代理规则和策略路由。
这次使用 Python 标准库实现同步程序,通过 systemd timer 周期运行。程序兼容设备上的 Python 2.7,也在本地 Python 3 下执行了测试。每次执行读取 WAN 地址、链路状态和默认路由,筛选可用的全球单播 /64,排除暂定、重复地址检测失败、已经弃用或首选寿命为零的地址。
只有链路已连接,并存在经 WAN 的 IPv6 下一跳默认路由时,才把这些前缀作为当前可用前缀。这里检查的是接口和路由就绪情况,没有持续探测互联网;如果上游链路仍在但远端发生黑洞,不能保证仅靠这组条件检测出来。
同步过程同时维护三处状态:
- LAN 的原生 RA 配置;
- ndppd 的运行时配置;
- WAN 入站流量使用的前缀路由与策略规则。
程序记录自己管理的前缀,删除旧配置时只处理这些前缀,保留 ULA 和其他手工配置。运行状态放在 /run,持久目录只记录必要的管理信息;没有每隔几十秒保存一次完整 EdgeOS 配置。
5.2 前缀变化与掉线处理
当前前缀的 RA 首选寿命为 900 秒,有效寿命为 1800 秒。检测到前缀退出后,将其首选寿命设为 0,并保留 300 秒的代理和路由过渡时间;过渡到期后移除对应配置。WAN 不再满足就绪条件时,LAN 广播的默认路由寿命降为 0,恢复后重新发布。
300 秒仅表示旧代理与路由配置的保留时间,不代表客户端会在 300 秒后删除旧地址。主机还受已有地址寿命及 SLAAC 的寿命更新规则影响,具体行为见 RFC 4862 第 5.5.3 节。如果旧前缀已被上游撤回,保留本地规则也无法保证旧连接继续工作。
定时器的关键配置如下:
ini12345[Timer] OnBootSec=30 OnUnitInactiveSec=30 AccuracySec=1 Unit=ndppd-sync.service
OnUnitInactiveSec=30 表示上次执行结束约 30 秒后再次运行。同步失败时保留可重试状态,由后续执行重试;并发执行通过文件锁避免重叠。状态未变化时,不重写配置,也不重启代理进程。
5.3 启动顺序与配置保存
同步程序需要在 EdgeOS 原生配置加载完成后执行。
这次通过 EdgeOS 的 post-config 钩子恢复 systemd 单元链接、设置就绪标志并启动 timer,同步服务同时声明在路由器配置服务之后执行。钩子只负责启动定时器,不在配置加载过程中阻塞等待同步完成。
保存的启动配置保留 ULA,LAN 的默认路由广播寿命初始为 0,也不固定保存当前公网前缀。等 WAN 就绪后,同步程序再提交运行时 RA 配置、启动代理并建立路由。
实现中使用 EdgeOS 自带的配置命令包装器,并为同一事务保持会话标识及 vyattacfg 组身份,完成 begin、set/delete、commit 和 end;失败时执行 discard。同步结果以配置事务提交成功为准。
公网前缀由同步程序管理。直接编辑生成文件会被后续同步覆盖;手动保存整个配置前,应确认是否把当前动态前缀一并保存了进去。
5.4 保存 LAN RA 启动基线
在 EdgeOS 配置模式设置 LAN 的启动基线。这里假定 LAN 是 switch0,ULA 沿用现有配置,不新增示例 ULA:
bash1234567configure set interfaces switch switch0 ipv6 router-advert send-advert true set interfaces switch switch0 ipv6 router-advert managed-flag false set interfaces switch switch0 ipv6 router-advert default-lifetime 0 commit save exit
如果之前手工配置过公网 RA 前缀,应先删除那一条公网前缀,再 commit 和 save;不要删除原有 ULA。命令结构为 delete interfaces switch switch0 ipv6 router-advert prefix 实际旧前缀/64。
首次启动前,公网 RA 前缀应为空,默认路由寿命为 0。同步程序在 WAN 就绪后发布真实前缀并将默认路由寿命设为 1800。不要在自动同步完成后,为了“保存 NDP”再次保存整个原生配置;程序、服务及启动钩子已位于持久目录。
5.5 安装开机钩子并首次启动
上文创建的 ndppd.service 使用以下启动命令,代理以 root 身份访问原始套接字:
ini1234567[Service] Type=simple Environment=PATH=/usr/sbin:/usr/bin:/sbin:/bin ExecStartPre=/usr/bin/test -s /run/ndppd/ndppd.conf ExecStart=/config/ipv6-proxy/ndppd -c /run/ndppd/ndppd.conf Restart=on-failure RestartSec=5
ndppd-sync.service 调用 /usr/bin/python /config/ipv6-proxy/prefix_sync.py,并在 EdgeOS 配置服务之后执行。第 4.3 节已给出完整文件,以上片段仅说明服务的启动方式。
先在 EdgeRouter 普通 shell 中执行只读规划,核对检测到的前缀:
bash1sudo python /config/ipv6-proxy/prefix_sync.py --dry-run
输出中的 online 应为 true,desired.active 应包含实际 WAN 公网 /64。这里的 dry-run 可能创建运行目录和锁文件,但不修改网络配置。若 active 为空,先检查接口参数、地址首选寿命和默认路由。
确认后安装并执行开机钩子:
bash123456789101112131415# 首次部署前确认该文件名没有被已有配置使用 sudo install -d -m 0755 /config/scripts/post-config.d sudo install -m 0755 /config/ipv6-proxy/50-ndppd \ /config/scripts/post-config.d/50-ndppd sudo /config/scripts/post-config.d/50-ndppd # 首次安装立即执行一次;后续由 timer 周期执行 sudo systemctl start ndppd-sync.service sudo systemctl show ndppd-sync.service -p Result -p ExecMainStatus sudo systemctl status ndppd.service ndppd-sync.timer --no-pager sudo cat /run/ndppd/state.json sudo cat /run/ndppd/ndppd.conf ip -6 rule show ip -6 route show table 10640 sudo cat /etc/radvd.conf
预期状态如下:
- 同步服务
Result=success、ExecMainStatus=0。 - ndppd 和 timer 均为
active。 state.json中有当前前缀,代理配置同时包含 WAN 与 LAN。- 优先级 10640 的规则带有
iif eth0,表 10640 的对应前缀指向switch0。 radvd.conf中出现实际公网前缀,允许 SLAAC,默认路由寿命为 1800。
故障日志可按以下方式查看:
bash123sudo journalctl -u ndppd.service -u ndppd-sync.service -n 80 --no-pager # 本次设备没有可用的 journal 文件,使用系统日志 sudo grep -E 'ndppd-sync|ndppd' /var/log/messages | tail -80
本方案不需要额外启用 proxy_ndp 内核静态代理;ndppd 自身负责 NDP 消息处理。不要再为每个服务器地址重复添加静态 proxy neighbor 条目。
六、连通性与重启验证
6.1 实机验证结果
实机验证结果如下:
| 检查项 | 实际结果 |
|---|---|
| 路由器 IPv6 HTTPS | 返回 HTTP 200 |
| 三台服务器的两个被测网口 | 六个网口均通过 IPv6 HTTPS 测试 |
| 下游与上游直连主机互相 ping | 双向通过 |
| 上游主动连接下游 TCP/22 | 超时,WAN 入站 DROP 计数增加 |
| 连续执行前缀同步 | 配置时间戳和代理 PID 保持不变 |
| 整台路由器重启 | 启动标识改变,代理、timer、RA 和策略路由自动恢复 |
| 重启后重复连通性测试 | 路由器及服务器均通过 |
按接口测试可以避免多网卡机器恰好走另一个正常出口:
bash12345# eno1 为示例被测接口,另一接口应单独再测一次 curl --noproxy '*' -6 --interface eno1 \ --connect-timeout 10 --max-time 20 \ -sS -o /dev/null -w 'IPv6 HTTPS %{http_code}\n' \ https://www.cloudflare.com/
整机重启前,先检查登录会话和现有入站连接,核对来源归属;确认没有其他用户正在使用后,才执行重启。恢复后没有手动启动代理,也没有手动执行同步程序,因此这次结果能够验证启动流程本身,而不只是运行期间的配置。
服务器侧应能看到新地址和默认路由:
bash1234# 在每台下游服务器执行,接口名按实际情况替换 ip -6 addr show dev eno1 ip -6 route show default sysctl net.ipv6.conf.eno1.accept_ra
如果服务器本身启用了 IPv6 转发,需要检查其网络管理器是否允许转发模式下接收 RA;仅看到路由器广播成功,并不能证明客户端会采用配置。本次客户端原有配置即可接收 RA,未额外修改。
连通性通过后,核对在线用户和入站连接,确认可以中断网络,再在路由器上执行 sudo reboot。重新登录后先重复 5.5 节的状态检查,不要手动启动服务,随后再跑各服务器的 IPv6 HTTPS 测试,以验证自动启动。
6.2 同步程序测试
同步程序的 10 项测试在本地和路由器上均通过,覆盖了前缀筛选、换号、退出与恢复、过渡期不被反复延长、提交失败后重试,以及不变状态下不重写文件、不重启进程等行为。
前缀变更和上游退出、恢复采用模拟输入测试,未执行真实上游换号。实机测试覆盖当前前缀下的双向通信、防火墙和整机重启恢复。
此外,timer 触发的一次性服务执行结束后显示 inactive 是正常现象,应结合 Result=success、退出码以及运行状态判断,不能把它当作守护进程异常退出。
七、停止与回滚
停止自动配置前,先记录当前管理的前缀,后续用于精确删除:
bash1234567# 在 EdgeRouter 上执行 sudo cat /config/ipv6-proxy/managed-prefixes.json sudo systemctl disable --now ndppd-sync.timer sudo systemctl stop ndppd-sync.service # 保留钩子备份,但移出 post-config.d,避免重启后再次执行 sudo mv /config/scripts/post-config.d/50-ndppd \ /config/ipv6-proxy/50-ndppd.disabled
随后进入 EdgeOS 配置模式,将 LAN default-lifetime 设为 0,并删除记录中的公网 RA 前缀。保留 ULA,执行 commit、save 后退出,再停止代理:
bash123456sudo systemctl stop ndppd.service # 将示例前缀和接口替换为记录中的实际值;多个前缀逐个处理 sudo ip -6 rule del pref 10640 iif eth0 \ to 2001:db8:100::/64 lookup 10640 sudo ip -6 route del 2001:db8:100::/64 dev switch0 table 10640
最后根据备份恢复此次改动的 WAN 自动配置、LAN RA 和防火墙绑定。不要清空整张 IPv6 路由表或所有防火墙规则。完整加载 config.boot 备份会覆盖备份之后的其他配置,回滚前应先比较差异。
八、适用条件
该配置适用于上游提供 SLAAC 链路前缀、允许代理邻居通信,但本次接入未获得 PD 的环境。上游地址绑定或接入策略变化后,需要重新验证。若能够获得 PD,可改为下游独立前缀的路由配置。
参考
支付宝
微信