Gentoo Linux 手搓路由器 —— 使用 odhcpd 支持 IPv6
继之前的几篇文章:
- 《使用 Gentoo Linux 搭建简单路由器并支持IPTV功能》
- 《IPTV单线复用——在Linux系统上配置》
- 《使用 Gentoo Linux 手搓路由器之二 -- 透明代理》
- 《使用 Gentoo Linux 手搓路由器之三 -- 国内访问加速》
- 《使用Gentoo Linux手搓路由器之四 -- 支持 IPv6》
本文继续折腾 Gentoo Linux 手搓路由器的 IPv6 支持。上一篇 IPv6 的方案使用的是 bridge + ebtables,让 RA / NDP 这类 IPv6 必要报文在 WAN 和 LAN 之间通过网桥转发,再把真正的数据流量拉回三层路由处理。
这个方案能跑,而且思路也挺有意思。但是内核升级到 Linux 6.17 之后,legacy ebtables 相关支持有了变化,直接照抄旧文会踩坑。所以这篇换一个思路:把 IPv6 的 RA / DHCPv6 / NDP relay 交给 OpenWrt 里成熟使用的 odhcpd。
背景
先回顾一下之前的做法。旧文《使用Gentoo Linux手搓路由器之四 -- 支持 IPv6》里,我大概用了三步:
- 创建一个只用于
IPv6中继的网桥。 - 用
ebtables控制哪些报文经过网桥,哪些报文被拉回三层。 - 让
RA / RS / NS / NA这些ICMPv6报文通过,从而实现一个类似NDP Relay的效果。
核心规则里会用到这些东西:
1ebtables -t broute -A BROUTING ...
2ebtables -t nat -A PREROUTING ...
3ebtables -t filter -A OUTPUT ...
这个方案的问题是,它对 ebtables 的依赖比较重。到了 Linux 6.17 之后,内核里与 legacy x_tables / ebtables 有关的配置项不再像以前一样顺手。例如桥接防火墙里的这些选项:
1CONFIG_NETFILTER_XTABLES_LEGACY
2CONFIG_BRIDGE_EBT_BROUTE
3CONFIG_BRIDGE_EBT_T_NAT
4CONFIG_BRIDGE_EBT_T_FILTER
如果你用的是自己编译的内核,当然可以回头把这些选项重新打开。可问题在于,这条路越来越像是在维护一个历史包袱:能用,但不优雅;能修,但每次升级内核都要惦记一下。
所以我准备换成 odhcpd。它本来就是 OpenWrt 用来处理 DHCP / DHCPv6 / RA / NDP 的组件,尤其适合我这种“家庭宽带没有拿到可路由 PD,但是又想让内网设备正常获得 IPv6”的场景。
odhcpd 是什么
odhcpd 是 OpenWrt 项目里的一个守护进程,项目地址在这里:
它的完整定位大概是:
DHCPv4serverRouter Advertisementserver / relayDHCPv6server / relayPrefix DelegationNDP Proxy
对本文来说,重点不是把 Gentoo 变成 OpenWrt,也不是引入整套 netifd / ubus / uci 生态,而是单独借用 odhcpd 解决一个问题:
在没有可分配
IPv6 Prefix Delegation的情况下,把上游的RA / DHCPv6 / NDP在WAN和LAN之间 relay 起来。
odhcpd 官方 README 里也明确提到,它可以在没有下发 delegated prefix 的情况下,在 routed、non-bridged 的接口之间 relay RA / DHCPv6 / NDP。这正好对应我现在的需求。
编译 odhcpd
odhcpd 使用 cmake 构建。官方 README 里给出的构建方式很简单:
1git clone https://github.com/openwrt/odhcpd.git
2cd odhcpd
3
4cmake -B build -S .
5cmake --build build
6cmake --install build
不过它毕竟是 OpenWrt 项目,依赖并不是普通发行版上一定已经准备好的。大概会涉及这些库:
libucilibuboxlibnl-tinyjson-cubus
其中 ubus 主要是为了和 OpenWrt 的运行时通信。如果只是像本文这样写死配置文件,本机跑一个 odhcpd,可以先把 ubus 相关能力放到后面再研究。
在 gentoo 中直接使用下面的命令
1bash scripts/devel-build.sh
编译结果会放到 build/ 目录里
odhcpd 参数详解
odhcpd 默认使用 OpenWrt UCI 风格配置文件,路径是:
1/etc/config/dhcp
这个配置文件里主要有两类 section:
config odhcpd:全局配置。config dhcp:每一个接口上的DHCP / DHCPv6 / RA / NDP配置。
全局参数
1config odhcpd 'odhcpd'
2 option maindhcp '0'
3 option leasefile '/var/lib/odhcpd/dhcp.leases'
4 option leasetrigger '/usr/lib/odhcpd/odhcpd-update'
5 option hostsdir '/var/lib/odhcpd/hosts'
6 option loglevel '6'
7 option piodir '/var/lib/odhcpd/pio'
常用参数说明:
| 参数 | 作用 |
|---|---|
maindhcp |
是否把 odhcpd 作为主要的 DHCPv4 服务。本文只关心 IPv6,一般保持 0。 |
leasefile |
保存 DHCPv4 / DHCPv6 租约的文件。 |
leasetrigger |
租约变化时触发的脚本。家庭路由器简单场景可以先不配。 |
hostsdir |
生成 hosts 文件的目录。 |
loglevel |
日志级别,0-7,默认 6。调试时可以先保持默认。 |
piodir |
保存 IPv6 Prefix Information 的目录,用于识别 stale prefix。 |
接口基础参数
1config dhcp 'lan'
2 option ifname 'enp1s0f0'
| 参数 | 作用 |
|---|---|
interface |
OpenWrt 里的逻辑接口名称。纯 Gentoo 场景不依赖 netifd 时,可以优先使用 ifname。 |
ifname |
物理网络接口名称,比如 enp1s0f0。 |
networkid |
ifname 的兼容别名。 |
master |
relay 模式下的主接口。一般 WAN 设置为 1,LAN 不设置。 |
master 这个参数很关键。做 relay 的时候,需要告诉 odhcpd 哪一侧是上游。我的场景里,上游当然是 WAN。
IPv6 核心参数
1option ra 'relay'
2option dhcpv6 'relay'
3option ndp 'relay'
| 参数 | 作用 |
|---|---|
ra |
Router Advertisement 服务。可选值:disabled、server、relay、hybrid。 |
dhcpv6 |
DHCPv6 服务。可选值:disabled、server、relay、hybrid。 |
ndp |
Neighbor Discovery Proxy。可选值:disabled、relay、hybrid。 |
几个模式可以先这样理解:
| 模式 | 理解 |
|---|---|
disabled |
不启用。 |
server |
自己作为服务器给下游发配置。适合已经拿到可分配前缀的场景。 |
relay |
在上下游之间中继报文。本文主线就是这个。 |
hybrid |
混合模式,通常给更复杂的 OpenWrt 场景使用,先不展开。 |
RA 参数
这些参数主要影响 Router Advertisement 里怎么向客户端宣告路由、DNS、MTU、SLAAC 等信息。
| 参数 | 作用 |
|---|---|
ra_default |
是否宣告默认路由。0 是默认行为,1/2 可用于忽略某些无公网前缀的情况。 |
ra_flags |
RA 里的标志位,比如 managed-config、other-config、none。 |
ra_slaac |
是否允许客户端通过 SLAAC 自动配置地址。 |
ra_mtu |
通过 RA 宣告 MTU。遇到 IPv6 MTU 问题时可以考虑。 |
ra_dns |
是否通过 RA 宣告 DNS,也就是 RDNSS。 |
ra_preference |
默认路由优先级:medium、high、low。 |
ra_mininterval |
主动发送 RA 的最小间隔。 |
ra_maxinterval |
主动发送 RA 的最大间隔。 |
ra_lifetime |
RA 中 Router Lifetime 字段的值。 |
如果只做 relay,很多 RA 参数可以先不配,让上游的 RA 信息中继过来。
DHCPv6 参数
| 参数 | 作用 |
|---|---|
dhcpv6_na |
是否分配 IA_NA,也就是普通 IPv6 地址。 |
dhcpv6_pd |
是否分配 IA_PD,也就是 Prefix Delegation。 |
dhcpv6_assignall |
stateful 模式下是否分配所有可用地址。 |
dhcpv6_relay_servers |
指定要 relay 到的 DHCPv6 server 地址。 |
家庭宽带里最麻烦的就是 PD。如果运营商愿意给你下发一个 /60 或 /56,那路由器自己做 server 会更舒服。但我这里先按“没有可用 PD”的情况写,也就是用 relay。
NDP Proxy 参数
| 参数 | 作用 |
|---|---|
ndproxy_routing |
是否从 NDP 学习路由。 |
ndproxy_slave |
标记外部 slave 接口,避免错误 proxy 某些 NDP。 |
ndp_from_link_local |
NDP 操作是否使用 link-local 源地址,对兼容性有帮助。 |
NDP 这部分是 IPv6 relay 能不能正常工作的关键。客户端拿到地址之后,上游还得知道这个地址在哪个二层邻居后面,否则地址看起来有了,实际包不一定回来。
宣告信息
| 参数 | 作用 |
|---|---|
dns |
宣告给客户端的 DNS 服务器,可以是 IPv4 或 IPv6。 |
domain |
搜索域。 |
ntp |
NTP 服务器。 |
prefix_filter |
只宣告指定范围内的 on-link prefix。 |
如果希望客户端优先使用路由器自己的 DNS,可以在 lan 上加:
1list dns 'fd00::1'
不过 relay 模式下,我倾向于第一版先不做太多重写,先让上游信息原样跑通,再慢慢加自己的策略。
odhcpd 配置
下面用旧文里的接口名举例:
1export WAN=enp1s0f1
2export LAN=enp1s0f0
创建配置目录:
1mkdir -p /etc/config
2mkdir -p /var/lib/odhcpd
编辑配置:
1vim /etc/config/dhcp
第一版可以先这样写:
1config odhcpd 'odhcpd'
2 option loglevel '6'
3 option leasefile '/var/lib/odhcpd/dhcp.leases'
4 option piodir '/var/lib/odhcpd/pio'
5
6config dhcp 'wan6'
7 option ifname 'enp1s0f1'
8 option master '1'
9 option ra 'relay'
10 option dhcpv6 'relay'
11 option ndp 'relay'
12
13config dhcp 'lan'
14 option ifname 'enp1s0f0'
15 option ra 'relay'
16 option dhcpv6 'relay'
17 option ndp 'relay'
这个配置的意思是:
wan6是上游接口,所以设置master '1'。lan是下游接口,不设置master。RA / DHCPv6 / NDP全部使用relay。
内核参数也要配一下:
1sysctl -w net.ipv6.conf.all.forwarding=1
2sysctl -w net.ipv6.conf.enp1s0f1.accept_ra=2
如果要写入 /etc/sysctl.conf:
1net.ipv6.conf.all.forwarding = 1
2net.ipv6.conf.enp1s0f1.accept_ra = 2
accept_ra=2的原因是,路由器开启 forwarding 之后默认不再像普通主机一样接收 RA。但本文这个场景里,路由器本身仍然需要从上游获得 IPv6 路由信息。
启动方式先用前台调试:
1odhcpd -f -l 7
确认没有明显报错之后,再写成 OpenRC 服务:
1# 我个人比较喜欢写到 /etc/local.d/ 目录里
2vim /etc/local.d/ipv6.start
1nohub odhcpd -f -l 7 1>&2 &
验证
先看地址和路由:
1ip -6 addr
2ip -6 route
再抓 RS / RA:
1tcpdump -i enp1s0f0 -vvvvn icmp6 and 'ip6[40] = 133 or ip6[40] = 134'
其中:
133是Router Solicitation134是Router Advertisement
抓 NS / NA:
1tcpdump -i enp1s0f1 -vvvvn icmp6 and 'ip6[40] = 135 or ip6[40] = 136'
其中:
135是Neighbor Solicitation136是Neighbor Advertisement
在客户端上可以检查:
1ip -6 addr
2ip -6 route
3ping -6 240c::6666
4curl -6 https://ifconfig.co
如果客户端已经拿到了公网 IPv6 地址,但访问外网不通,优先检查这几件事:
- 路由器有没有
IPv6 default route。 - 上游能不能看到客户端地址的
NDP。 - 防火墙有没有放行
ICMPv6。 accept_ra=2是否只配在了错误接口上。
和旧方案的差异
旧方案的核心是把一部分 IPv6 控制报文留在二层桥里,让它们自然穿过去;新方案是让 odhcpd 明确承担 relay 工作。
| 对比项 | bridge + ebtables | odhcpd relay |
|---|---|---|
| 依赖 | ebtables、bridge netfilter、legacy x_tables |
odhcpd、UCI 配置文件 |
| 内核升级影响 | 比较敏感 | 相对小 |
| 可读性 | 规则比较绕 | 配置语义更清晰 |
| 调试方式 | 看 ebtables 命中和抓包 | 看 odhcpd 日志和抓包 |
| 适合场景 | 想控制二层转发细节 | 只想稳定支持 IPv6 relay |
我个人现在更倾向于 odhcpd。毕竟这件事情 OpenWrt 已经验证了很多年,没必要继续用一堆桥接规则手搓。
参考资料
Posts in this series
- Gentoo Linux Kernel 从 6.12 升级至 6.18
- Gentoo Linux 手搓路由器 —— 使用 odhcpd 支持 IPv6
- 使用Gentoo Linux手搓路由器之四 -- 支持 IPv6
- 使用 Gentoo Linux 手搓路由器之三 -- 国内访问加速
- 使用 Gentoo Linux 手搓路由器之二 -- 透明代理
- IPTV单线复用——在Linux系统上配置
- 使用 Gentoo Linux 搭建简单路由器并支持IPTV功能