最近自己拉了一条联通的千兆宽带,并成功地找官方客服要到了公网 IPv4。为了有效利用这个公网 IP,同时避免重演之前在房东的光猫上的血泪史,我决定将拨号等各项网络功能全部交由软路由实现,光猫仅保留光电转换功能。这篇文章用于记录我的配置过程,防止以后在软路由配置上重复踩坑,也为需要的人提供参考。

准备工作

在开始配置软路由之前,需要安装一系列工具,并对系统的一些参数进行修改。我使用的工具包括:

  1. pppoeconf,用于宽带拨号
  2. wide-dhcpv6-client,用于获取 IPv6 PD 前缀并配置到接口
  3. dnsmasq,用于为 LAN 提供 DNS 服务、DHCP 服务和 IPv6 路由通告服务
  4. nftables,用于提供 IPv4 NAT 和防火墙功能

以上四个工具均可通过apt直接安装。 同时,为了保证系统能够正确获取 IPv6 地址和转发数据包,还需要向/etc/sysctl.conf中添加以下内容:

net.ipv4.ip_forward=1

net.ipv6.conf.all.disable_ipv6=0
net.ipv6.conf.default.disable_ipv6=0
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.default.forwarding=1
net.ipv6.conf.all.accept_ra=2
net.ipv6.conf.default.accept_ra=2

其中,第一条用于启用对 IPv4 数据包的转发,后面六条分别对应启用 IPv6 功能、启用对 IPv6 数据包的转发和接受 IPv6 的路由器通告。带default的配置用于系统对接口的默认设置,如果未对其进行修改,可能导致新添加的接口与 all 的配置不一致,从而产生问题。(在我的场景中,拨号后新增的ppp0接口的accept_ra项为1,无法正确从 ISP 的 RA 通告中获取并设置 IPv6 地址,导致其只有公网 IPv4 地址和 FE80 打头的 IPv6 link local 地址,没有 GUA,即公网 IPv6 地址。)

配置网络接口

在进行后续工作前,首先需要正确配置软路由 LAN 口和 WAN 口的地址。方便起见,我直接使用 Debian 12 自带的networking服务进行网络配置,不使用NetworkManager等管理器。在 Debian12 中,networking服务使用的配置文件位于/etc/network/interfaces,具体配置如下所示。其中,enp1s0为 LAN 口,LAN 的 IPv4 网段为10.20.0.0/24enp4s0为 WAN 口,连接光猫,IPv4 地址与光猫处于同一网段,方便后续对光猫进行操作。

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

auto enp1s0
allow-hotplug enp1s0
iface enp1s0 inet static
    address 10.20.0.1/24

auto enp4s0
allow-hotplug enp4s0
iface enp4s0 inet static
     address 192.168.1.99/24

拨号上网

在作为路由器之前,软路由自身首先需要能够上网。为了使用软路由进行拨号,必须保证光猫处于桥接模式,这个可以在装宽带时让师傅进行修改,或是通过各种手段获取光猫的超级密码后进入管理员后台进行修改,这里不作陈述。 在 Debian 12 上进行拨号,我选择的方案是pppoeconf这个工具,可以直接使用 apt 进行安装。在安装完成后,直接执行sudo pppoeconf,其会自行检测可用于 pppoe 拨号的网口。按提示输入宽带账号和密码后,其便会自动生成/etc/ppp/peers/dsl-provider配置文件用于实际拨号,并且会提醒是否立即进行拨号。需要注意的第一点是,pppoeconf默认生成的配置文件并未启用 IPv6,需要手动在dsl-provider文件的最后添加一行+ipv6以启用。在添加后,需要使用sudo systemctl restart networking重启网络服务确保配置生效。 dsl-provider文件内容如下,其中enp4s0为软路由连接光猫的接口:

# Minimalistic default options file for DSL/PPPoE connections

noipdefault
defaultroute
replacedefaultroute
hide-password
#lcp-echo-interval 30
#lcp-echo-failure 4
noauth
persist
#mtu 1492
#persist
#maxfail 0
#holdoff 20
plugin rp-pppoe.so
nic-enp4s0
user "********"
+ipv6

完成拨号后,理论上就能够看到ppp0接口已经可用,同时具备 ISP 下发的 IPv4 与 IPv6 地址。 需要注意的第二点是,pppoeconf在配置过程中自动生成的 mss clamping 设置脚本位于/etc/ppp/ip-up.d/0clampmss中,但该脚本仅设置了对 TCP over IPv4 连接的 mss clamping,而没有对 IPv6 连接进行设置,进而导致部分网站的 IPv6 数据包因 MTU 问题丢包。为了解决这个问题,需要将/etc/ppp/ip-up.d/0clampmss复制一份到/etc/ppp/ipv6-up.d/0clampmss,并将脚本中的iptables命令改为ip6tables。如此即可实现对 TCP over IPv6 连接的 mss clamping。

IPv6 获取 PD

为了让 LAN 能够正确配置 IPv6 GUA 地址,需要在软路由上向 ISP 请求一个可用的 PD 并下发给 LAN 的所有设备。这里我使用wide-dhcpv6-client包来实现这一功能,其配置文件位于/etc/wide-dhcpv6/dhcp6c.conf,内容如下:

interface ppp0 {
    # 请求 PD
    send ia-pd 0;
};

# 请求到的序号为 0 的 PD 用于如下
id-assoc pd 0 {
    # 用该 PD 配置 enp1s0 接口
    prefix-interface enp1s0 {
        # 该接口在前缀中的 id 序号,从 0 开始
        sla-id 1;
        # 该接口使用前缀长度,如果 PD 下发的是/60,这里是 4,则最后接口的前缀为 60+4=/64
        sla-len 4;
    };
};

完成上述后,可以通过sudo systemctl restart wide-dhcpv6-client来检查是否能够正确获取 PD 并设置 LAN 接口的 IPv6 地址。 由于开机后需要等待拨号ppp0接口才会出现,所以wide-dhcpv6-client服务的自启动往往会失败。为了实现正确的自启动,这里我自行构建了一个dhcp6c.service服务,并设置失败重启,确保在ppp0接口启用后 DHCPv6 能够正确进行。按照systemd的惯例,将dhcp6c.service放置在/etc/systemd/system目录下,其内容如下:

[Unit]
Description=WIDE DHCPv6 Client
Wants=network-online.target
Before=dnsmasq.service
After=network-online.target

[Service]
EnvironmentFile=/etc/default/wide-dhcpv6-client
ExecStart=/usr/sbin/dhcp6c -f $INTERFACES
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -TERM $MAINPID
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

启用 DNS 和 DHCP 服务

我使用dnsmasq来同时提供 DNS 和 DHCP 服务,配置文件如下所示:

# = DNS 服务器设置 =
interface=enp1s0
port=53
server=119.29.29.29
server=223.5.5.5
# = DHCP 服务器设置 =
# 启用路由通告
enable-ra
log-dhcp
# DHCPv4 的地址范围
dhcp-range=10.20.0.100,10.20.0.199,12h
# DHCPv6 仅用于通告 DNS,使用 enp1s0 的前缀进行路由通告,让 LAN 机器通过 SLAAC 获取 IPv6 地址
dhcp-range=::,constructor:enp1s0,ra-stateless,12h
# DHCPv4 的路由器地址和 DNS 服务器选项
dhcp-option=option:router,10.20.0.1
dhcp-option=option:dns-server,10.20.0.1
# DHCPv6 的 DNS 服务器选项,按路由器接口地址构造
dhcp-option=option6:dns-server,[::]

设置防火墙

为了避免干扰上述配置过程,我将防火墙的设置放在了最后。防火墙的防护重点如下:

  1. 保护软路由不受公网攻击
  2. 保护 LAN 不受公网攻击

除此以外,IPv4 的 NAT 功能也借由nftables实现。 为了实现上述功能,我的nftables配置如下:

#!/usr/sbin/nft -f

flush ruleset

# 防火墙功能
table inet filter {
        chain input {
                type filter hook input priority filter; policy drop;
                iifname "lo" accept comment "Accept any localhost traffic"

                ct state invalid drop comment "Drop invalid connections"
                ct state established,related accept comment "Accept traffic originated from us"
                meta l4proto ipv6-icmp accept comment "Accept ICMPv6"
                meta l4proto icmp accept comment "Accept ICMP"
                ip protocol igmp accept comment "Accept IGMP"

                udp dport mdns ip6 daddr ff02::fb accept comment "Accept mDNS"
                udp dport mdns ip daddr 224.0.0.251 accept comment "Accept mDNS"

                iifname "enp1s0" accept comment "Accept LAN to Router"

                # 放行 DHCPv6 的端口,确保 wide-dhcpv6-client 能够正确获取 PD
                udp dport {546,547} accept
        }
        chain forward {
                type filter hook forward priority filter; policy drop;

                # 由于 pppoe 的协议封装需要占用 8 个字节,导致 MTU 降低,这里根据 ppp0 接口的 MTU
                # 修改 tcp 协议的 MSS 来避免数据包因为超过 MTU 而被丢弃
                iifname "ppp0" tcp flags syn counter tcp option maxseg size set rt mtu
                oifname "ppp0" tcp flags syn counter tcp option maxseg size set rt mtu

                iifname enp1s0 accept comment "Accept LAN Outgoing"
                ct state established,related accept comment "Accept traffic orininated from LAN"

                meta l4proto ipv6-icmp accept comment "Accept ICMPv6"
                meta l4proto icmp accept comment "Accept ICMP"
        }
        chain output {
                type filter hook output priority filter;
        }
}

# NAT 功能
table inet nat {
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        ip saddr 10.20.0.0/24 oifname "ppp0" masquerade
    }
}

将上述内容保存到/etc/nftables.conf中即可在 boot 后自动将上述规则添加至nftables中。

参考

  1. X86 软路由配置 IPv6 踩坑小记
  2. 使用 Debian 作为路由器
  3. dnsmasq - ArchWiki
  4. Nftables/Examples - Gentoo wiki