前言
最近上一个公选课的时候听老师说我这边校园网也有IPv6了,感觉折腾的时候又来了。要知道几个月前,可是压根不行的。IPv6有了后,下BT也能通过IPv6链接上更多分享者,还有说不定能用公网地址整活。如果是走教育网的路由话,可能链接我的甲骨文小鸡延迟会更低。
有网,没网?
直连电脑拨号能用
电脑直接连接网口 PPPoE 拨号后,能够顺利获取到 240c:... 开头的公网 IPv6 地址,访问 test-ipv6.com 测试也是10/10满分通过。简单测试了一下,这边防火墙似乎屏蔽掉了外部对公网IPv6地址的访问。😭😭😭呜呜呜,不能整花活了。
当然,我肯定是不能直接连电脑的,我还有另外的电脑和手机需要上网。我用的是老掉牙的斐讯K2P呵呵呵。
连上路由器——终端处IPv6不通
我们校园网的IPv6环境颇具代表性:通过PPPoE拨号后,OpenWrt的DHCPv6 wan6接口可以获取到一个/64的公网IPv6地址段(例如240c:xxxx:xxxx:xxxx::/64)。但关键在于,没有前缀委派(PD)!这意味着路由器无法从上游获取一个更大的地址池来分割并分配给内网使用。我这边似乎使用的是ABMS 8.0 的系统。
- /64 前缀:通常是分配给一个末端局域网段(如一个家庭网络或一个小型办公室网络)的最小 IPv6 子网单位。
- 前缀委派 (PD):ISP 通常会分配一个比 /64 更大的前缀(例如 /56 或 /60)给用户的路由器。路由器随后可以将这个大前缀分割成多个 /64 的“小段”,分配给其下的不同局域网(例如主LAN、访客LAN等),使得这些局域网内的设备都能获得公网 IPv6 地址 (GUA) 并实现端到端通信。
- 没有PD,只有/64意味着:我的 OpenWrt 路由器无法从上游获取一个“可供支配”的大段 IPv6 地址池,也就很难通过标准方式为我的内网设备分发独立的、可全局路由的公网 IPv6 地址。
通过查询和询问群友,我打消了原本想直接直接上NAT 6的念头:我这边显然没有极端到只分配/128,即单个IPv6地址的情况,/64足足有2的64次方个地址,这是足够下游分配的。使用NAT6会因v6地址的巨长导致消耗,实际性能肯定不如直接分配。
目前我可以先尝试DHCPv6 Relay (中继模式)。
在这种情况下:
- LAN侧设备通过中继的RA/SLAAC获取到WAN侧那个/64段内的公网IPv6地址(GUA)。
- 它们的默认网关指向ISP的上游网关。
- OpenWrt负责将LAN客户端发出的GUA包正确转发到WAN口。
果然,连接在 OpenWrt 下的电脑也顺利获取到了 240c:... 的公网 IPv6 地址(这些地址与路由器 WAN 口获得的 /64 前缀属于同一段),并且能够 Ping 通由校园网分配的 IPv6 网关。
然而,当我尝试 Ping 外部 IPv6 地址(test6.ustc.edu.cn)时,却发现全都超时了。
于是我分别在LAN口侧和WAN口侧进行了抓包。
- WAN 口 (pppoe-wan) 抓包:
tcpdump -i pppoe-wan -n -vv 'ip6 and icmp6 and host <电脑临时GUA> and host <外部目标IP>'
在 pppoe-wan 口,我同样看到了这些来自LAN客户端(源IP是其GUA)的 ICMPv6 Echo Request 包被转发了出去!Hop Limit (hlim) 也从客户端发出的 128 正常地变成了 127。这无可辩驳地证明 OpenWrt 确实把包送出去了!然而,这些请求包的回复却石沉大海,从未能返回到LAN客户端手中。吊诡的是,OpenWrt路由器自身使用其WAN口的GUA与外部进行IPv6通信却是正常的。
这几乎可以断定:上游ISP设备(校园网网关)在处理回复包时,只“认”与PPPoE会话直接关联的那个路由器WAN口GUA,而不愿意或无法将回复包正确地路由给通过Relay方式共享这个/64段的其他内网客户端GUA。
二、Hotplug脚本强制路由
原先呢,我是准备妥协了,无奈下选择ULA+NAT6的方案,牺牲性能换取可用性。(详细的步骤可以参考2楼)
不过呢,后来我在搜索中找到一篇分析类似校园网环境的博客文章:odhcpd 中继模式原理、局限以及解决方案 中获得了一个极具创意的灵感——通过Hotplug脚本,在OpenWrt获取到WAN口的IPv6网络信息后,强制将整个WAN口的/64公网IPv6前缀的路由指向br-lan(我的局域网接口)!
这个方案的核心逻辑是:
既然上游ISP会将发往我WAN口整个/64段内所有IP的流量都送到我的路由器WAN口,那么我就在路由器层面告诉系统:“嘿,这个/64段的所有IP其实都在我的LAN里面,所有发给它们的包都往LAN口送就对了!”
这样,当回复包到达OpenWrt的WAN口时,OpenWrt会根据这条“霸道”的路由规则,将回复包直接转发到LAN,从而送达目标客户端。
三、方案实施步骤详解
步骤1:安装依赖包 owipcalc
此脚本需要owipcalc来从接口的IP地址/掩码计算出网络地址。
opkg update
opkg install owipcalc
步骤2:创建Hotplug脚本
在OpenWrt的/etc/hotplug.d/iface/目录下创建一个名为80-reset-route6(或其他你喜欢的名字,确保以数字开头以便正确执行顺序)的脚本文件。
vi /etc/hotplug.d/iface/80-reset-route6
将以下脚本内容粘贴进去:
#!/bin/sh
# WAN6是你获取公网IPv6地址的逻辑接口名 (uci中定义的wan6)
wan_dev="wan6"
# 确保只在接口事件时执行,并且是针对我们关心的wan6接口
[ "$HOTPLUG_TYPE" = "iface" ] || exit 0
[ "$INTERFACE" = "$wan_dev" ] || exit 0
# 设置一个路由度量值,可以根据需要调整
RTMETRIC=127
# 引入OpenWrt网络功能库
. /lib/functions/network.sh
# 获取LAN接口的实际物理设备名 (通常是 br-lan)
network_get_physdev lan_dev lan || exit 0
# 定义接口UP时的回调函数
ifup_cb() {
local _lan_dev="$1" # 传入的lan设备名 (br-lan)
local _metric="$2" # 传入的路由度量值
local wan_subnet # 用于存储wan6接口的子网信息 (如 240c::xxxx/64)
# 从wan6接口获取其IPv6子网信息
network_get_subnet6 wan_subnet "$wan_dev" || {
logger -t "hotplug-ipv6-route" "Failed to get IPv6 subnet for $wan_dev"
return 1
}
# 使用owipcalc从子网信息计算出网络地址 (如 240c::/64)
# 注意:owipcalc $wan_subnet network 会输出如 240c:xx:xx:xx:: (不带/64)
# 我们需要的是带/64的完整网络地址,或者直接用$wan_subnet(如果它就是网络地址/长度格式)
# 博客原文的_wan_network=$(owipcalc "${wan_subnet}" network)可能只取了网络号,
# 更稳妥的做法是直接使用 wan_subnet 如果它已经是 prefix/length 格式,或者确保获取正确的网络地址和长度
# 根据实际情况,如果network_get_subnet6返回的是地址/长度(如240c:1:2:3::abc/64),
# 我们需要的是网络/长度 (如240c:1:2:3::/64)。
# 一个更健壮的方式可能是直接用wan_subnet如果它包含了掩码长度
local _wan_network_prefix
_wan_network_prefix=$(echo "$wan_subnet" | cut -d'/' -f1) # IP部分
local _wan_network_length
_wan_network_length=$(echo "$wan_subnet" | cut -d'/' -f2) # 长度部分
# 使用ipcalc(或类似owipcalc的功能)获取准确的网络地址
# 这里简化处理,假设network_get_subnet6获取的就是正确的网络前缀/长度
# 如果上面获取的wan_subnet已经是正确的网络前缀/长度,可以直接用
# 例如,如果wan_subnet是"240c:cd22:cd22:2000::/64" (通常需要手动或从PD获取并设置)
# 但对于仅/64的情况,wan_subnet可能是接口IP/64,如 "240c:cd22:cd22:2000:aaaa:bbbb:cccc:dddd/64"
# 此时,$(owipcalc "${wan_subnet}" network)/$_wan_network_length 才是正确的网络/长度
local network_address_with_length
network_address_with_length="$(owipcalc "${wan_subnet}" network)/$_wan_network_length"
if [ -z "$_wan_network_length" ] || [ "$_wan_network_length" -eq "128" ]; then
logger -t "hotplug-ipv6-route" "Invalid or /128 prefix on $wan_dev, not adding route to $lan_dev. Subnet: $wan_subnet"
return 1
fi
logger -t "hotplug-ipv6-route" "Adding IPv6 route for $network_address_with_length via $lan_dev metric $_metric"
# 替换或添加一条路由,将整个wan_network_prefix 指向 lan_dev (br-lan)
ip -6 route replace "$network_address_with_length" dev "$_lan_dev" metric "$_metric"
}
# 定义接口DOWN时的回调函数
ifdown_cb() {
local _lan_dev="$1"
local _metric="$2"
# 尝试获取wan_subnet以精确删除 (如果接口已关闭,可能获取不到)
local wan_subnet
network_get_subnet6 wan_subnet "$wan_dev" # 此时接口可能已无地址,获取会失败
# 更可靠的做法是在ifup时记录下添加的路由,ifdown时精确删除
# 但简单起见,我们先尝试根据已知信息删除,如果失败问题不大,因为接口关闭后路由通常会失效
# 或者,我们可以假设要删除的路由就是之前添加的那个metric和dev的
# 这里我们假设需要一个之前添加的prefix信息来删除,但这在ifdown时可能不可靠
# 为了简单且符合博客思路,我们尝试用一种方式(如果能获取到wan_subnet)
if [ -n "$wan_subnet" ]; then
local _wan_network_prefix
_wan_network_prefix=$(echo "$wan_subnet" | cut -d'/' -f1)
local _wan_network_length
_wan_network_length=$(echo "$wan_subnet" | cut -d'/' -f2)
local network_address_with_length="$(owipcalc "${wan_subnet}" network)/$_wan_network_length"
if [ -n "$_wan_network_length" ] && [ "$_wan_network_length" -ne "128" ]; then
logger -t "hotplug-ipv6-route" "Deleting IPv6 route for $network_address_with_length via $lan_dev metric $_metric"
ip -6 route del "$network_address_with_length" dev "$_lan_dev" metric "$_metric"
fi
else
# 如果无法获取wan_subnet,可能需要一个更通用的删除方式,或者依赖路由自动失效
logger -t "hotplug-ipv6-route" "Could not get subnet for $wan_dev on ifdown, route may persist or be auto-removed."
fi
}
# 根据不同的接口动作执行回调
case "$ACTION" in
ifup)
ifup_cb "$lan_dev" "$RTMETRIC"
;;
ifdown)
ifdown_cb "$lan_dev" "$RTMETRIC"
;;
ifupdate) # 当接口配置更新时 (例如 DHCPv6 续租并可能改变了地址/前缀)
# 先尝试移除旧的(如果能获取到旧信息的话)
ifdown_cb "$lan_dev" "$RTMETRIC"
sleep 1 # 等待一下,确保旧路由清理
# 再添加新的
ifup_cb "$lan_dev" "$RTMETRIC"
;;
*)
;;
esac
exit 0
赋予脚本执行权限:
chmod +x /etc/hotplug.d/iface/80-reset-route6
步骤3:配置OpenWrt网络 (/etc/config/network)
globals 部分:可以不配置ULA,因为此方案专注于公网GUA。
config globals 'globals'
option packet_steering '1'
# option ula_prefix 'auto' # 本方案可以不使用ULA
lan 接口部分:保持静态IPv4,ip6assign '0'。
config interface 'lan'
option device 'br-lan'
option proto 'static'
option ipaddr '10.0.0.1' # 你的 IPv4 地址
option netmask '255.255.255.0' # 你的 IPv4 子网掩码
option ip6assign '0' # 不请求/分配GUA前缀给LAN接口自身
wan 接口部分:保持PPPoE配置,option ipv6 '0'。
config interface 'wan'
option proto 'pppoe'
option device 'wan' # 确认这是你的物理WAN口设备名
option username '你的拨号用户名'
option password '你的拨号密码'
option ipv6 '0'
wan6 接口部分:通过DHCPv6获取地址,必须请求前缀(即使只给/64,脚本也需要这个信息),关闭“IPv6源路由”以获取通用默认路由。
config interface 'wan6'
option proto 'dhcpv6'
option device '@wan' # 或者 'pppoe-wan'
option reqaddress 'try'
option reqprefix 'auto' # 必须请求前缀,脚本依赖此信息
option defaultroute '1' # 获取通用默认路由
# 确保在LuCI中关闭了“IPv6 源路由”选项
步骤4:配置DHCP/RA服务 (/etc/config/dhcp)
将LAN侧和WAN6侧都配置为Relay模式,并把WAN6设置为Master。
* lan 区域 (config dhcp 'lan'):
config dhcp 'lan'
option interface 'lan'
# ... IPv4 DHCP 设置 ...
option dhcpv6 'relay'
option ra 'relay'
option ndp 'relay' # odhcpd的relay模式通常会处理NDP
# option master '0' # LuCI中设置relay时通常会自动设为slave
* wan6 区域 (config dhcp 'wan6'):
config dhcp 'wan6'
option interface 'wan6'
option dhcpv6 'disabled' # WAN6自身是DHCPv6客户端
option ra 'relay' # 作为Relay的Master端
option ndp 'relay'
option master '1' # 标记为中继的主接口
LuCI界面操作对应:
- LAN接口的DHCP服务器 -> IPv6设置:RA、DHCPv6、NDP Proxy均设为 relay。不勾选 Designated master interface。
- WAN6接口的DHCP服务器 -> IPv6设置:RA、DHCPv6(设为disabled)、NDP Proxy均设为 relay。勾选 Designated master interface。
步骤5:配置防火墙 (/etc/config/firewall)
- 关闭IPv6 NAT (masq6):
在 config zone 'wan' 中,确保 option masq6 '0' 或此行不存在。
- 确保转发允许:
lan 到 wan6 的 forward 策略为 ACCEPT。
步骤6:重启并验证
- 保存所有配置文件。
- 执行 reboot 重启路由器。
- 路由器启动后:
- 检查 wan6 状态:ifstatus wan6 (确认获取到/64地址段信息)。
- 检查路由表:ip -6 route show
- 应该有一条通用的默认IPv6路由 (无 from <prefix> 限制)。
- 关键:应该看到hotplug脚本添加的路由,例如:240c:xxxx:xxxx:xxxx::/64 dev br-lan metric 127。
- 检查 br-lan 地址:ip -6 addr show dev br-lan (应只有链路本地)。
- 客户端IPv6配置:客户端应通过SLAAC获取到240c:...的GUA,网关指向ISP上游网关。
步骤7:测试客户端IPv6连通性
- 从客户端电脑 ping test6.ustc.edu.cn。
- 访问 test-ipv6.com。
四、方案原理解析与优势
除了owipcalc计算子网地址重设路由表之外,原博客文章也提供 伪装前缀代理 把下发的 /64 伪装为PD的方法,感兴趣可以去 原博客了解。https://blog.icpz.dev/articles/notes/odhcpd-relay-mode-discuss/
本方案的核心在于,通过Hotplug脚本在OpenWrt的路由表中“声明”了整个从校园网获取到的/64公网IPv6前缀都位于br-lan(局域网)之后。
当外部IPv6网络的回复包(目标是你局域网内某台电脑的公网IPv6地址)到达OpenWrt的pppoe-wan接口时:
- OpenWrt查询路由表。
- 由于Hotplug脚本添加的这条高优先级(metric 127通常较高,但具体要看其他路由)或者说特定性更强的路由(/64段比默认路由::/0更具体),OpenWrt会认为这个目标地址就在br-lan后面。
- 于是,OpenWrt将这个回复包正确地转发到br-lan接口,最终送达目标电脑。
优势:
- 实现了LAN客户端使用公网IPv6地址(GUA)上网,更符合IPv6的设计初衷(相比NAT6)。
- 解决了上游ISP不给中继客户端GUA回复包的问题,通过在OpenWrt层面主动“认领”这些回复包并将其导向内网。
- 配置相对固定后,维护成本可能比复杂的NDP代理调试要低。
潜在注意事项:
- 校园网/64段内其他用户:如博客作者所述,此方法假定校园网分配给你的/64段是你独占的,或者你不在意无法访问这个/64段内、但不在你路由器后面的其他设备。
- 脚本的健壮性:Hotplug脚本的执行依赖于wan6接口状态的正确变化和network_get_subnet6等函数的准确返回值。
- 安全性:客户端直接暴露公网IP,需要客户端自身防火墙和OpenWrt入站防火墙策略的配合。
参考
锐捷校园网IPv6的/64内网中继配置:ndp-proxy、relay
odhcpd 中继模式原理、局限以及解决方案
【已解决】 odhcpd 配置校园网 LAN 口能得到 IPv6 但客户端无法获得 IPv6 地址