最近自己拉了一条联通的千兆宽带,并成功地找官方客服要到了公网 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等管理器。具体配置如下,其中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 能够正确进行。dhcp6c.service内容如下:

[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;

                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