Armbian,更适合 R2S 软路由的系统

OpenWrt 的困境

人们安装 OpenWrt,是因为他们觉得它比路由器或者嵌入式设备的原厂固件更好用。

——《使用 OpenWrt 的理由》

不错,作为一个路由器系统来说 OpenWrt 确实实现了相对官方固件更好的可玩性。实际用久了自然会发现 OpenWrt 作为一个 Linux 来说是非常难用的,这点突出体现在其包管理上。官方软件源各种残缺,第三方软件源也充斥着野包,让我想起了用 CentOS 的时候。开发者倾向于为传统大发行版打包,相比较而言极少看到过 opkg。OpenWrt 官方甚至不建议升级软件包 1,因为包管理器并没有能力升级 OpenWrt 本身……这对 Tailscale 和 Xray-core 等需要经常更新的软件来说很致命。

整个 OpenWrt 给人的印象也是能省则省,因为它需要照顾到大量低性能的设备,甚至低至 8MB Flash、32M RAM 的设备;前文提到的包管理器区别是因为 opkg 不支持 ABI 兼容性监测;对于普通的 Linux 发行版也有诸多其他不同。而对于 R2S 这种 1GB RAM 的设备来说,倒没必要这么节省。更何况即使在 Armbian 上,日常占用 RAM 也仅有 300MB,这里面还有 100MB 以上是 Xray-core 的缓存。

我并没有说 OpenWrt 不适合路由器使用,它的大部分配置都能点几下就完成设置,也有更多的中文资料,还有很多开箱即用的网络优化,在社区活跃开发者的帮助下,它十分适合路由器使用。但若是想要跳出插件开发者设定的笼子,尝试稍高级一点的玩法,OpenWrt 就会显得各种别扭。而 Armbian 基于 Debian 或者 Ubuntu,都是许多 Linux 用户再熟悉不过的发行版,因此使用 Armbian 不是找罪受,反而能够利用已有的其他 Linux 使用习惯和资料,是一个偷懒的选择。

包含 Armbian 信息的 neofetch

因此,本文希望为读者提供另一种软路由的思路,让 R2S 兼有传统主路由的功能,又有更熟悉的 Linux 发行版体验

我的网络拓扑大概如下(无线路由器连接的各个设备仅为示意)。R2S 作为主路由使用,Redmi AC2100 (已升级为小米 AX3000)作为 AP,不存在旁路由。

网络拓扑
网络拓扑

安装 Armbian

安装 Armbian 的第一步就像 OpenWrt 一样,从 官方网站 下载对应的 img(本文使用基于 Ubuntu 的 Jammy),然后用你的工具烧录进 SD 卡。将 R2S 接入电源,指示灯应该闪烁。

初始设置需要将其 WAN 或 LAN 口接到另一台 已经启用 DHCP 的路由器上,想办法找到其 IP 地址(比如进管理员后台),然后使用 SSH 登录,初始用户名和密码分别为 root1234。注意其此时不能作为一个开箱即用的路由器,也就是说不能直接接入电脑。

登入后有一个初始引导,跟着做就行了,没什么好说的。然后这就是一个标准的 Linux 系统了,基本可以直接遵循其他 Linux 的操作方法。但国内许多用户会到手换源,由于 Armbian 是基于其他发行版做的,所以换源不完全一样。其软件源有两个位置,均需要更改:

  • /etc/apt/sources.list 与你安装的版本所基于的发行版有关,如 Armbian Jammy 基于 Ubuntu Jammy,就换对应的源(如 清华源,注意 Ubuntu 使用 Ports 源);
  • /etc/apt/sources.list.d/armbian.list 是 Armbian 专属软件源,也要换(仍然如 清华源)。

配置路由器功能

OpenWRT 说白了也是个 Linux,作为一个路由器所需要的功能也有许多方案可以通过后期手动安装实现。

顺着走到这一步,执行 ip l,一台 R2S 的显示应该类似于下面这样:

plaintext
1
2
3
4
5
6
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000
    link/ether aa:aa:aa:aa:aa:aa brd ff:ff:ff:ff:ff:ff
4: lan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether aa:aa:aa:aa:aa:aa brd ff:ff:ff:ff:ff:ff

此处我把外部网络(相对,如上层路由器)插到了 lan0 上,局域网设备则均连接至 eth0,相当于许多 OpenWRT 包所做的反转 WAN 和 LAN 口。如无特别说明,后面的操作均按照这个来。

R2S 的 LAN 是 USB 3.0 转接的网卡,而 WAN 是 PCIe 网卡。进行高速传输时,USB 造成的中断会大量消耗性能,所以对于内网流量,将其扔给性能更高的 WAN 口。R2S 在小包较多时速度明显下降,也是基于同样的原因。

DNS

DNS 用于将主机名解析为 IP 地址。由于传统 DNS 查询是明文的,可以被中间人任意截留修改,部分运营商会进行 DNS 污染,以达到加广告或封锁境外网站的目的。因此,使用 DoH(DHS-over-HTTPS)和 DoT(DNS-over-TLS),并对国内和国外域名进行分流,就有其意义。

下文介绍了几种 DNS 转发器的的配置方案。其中,EasyMosdns 自带了较完善的方案,而 TelescopeDNS 的配置文件较为简明,都能有效对抗 DNS 污染。

TelescopeDNS

此处使用 Telescope DNS 负责监听 53 端口,为局域网内设备进行解析,并根据条件将其发送至不同的 DNS 服务器。

虽然我有透明代理的需求,但是本着解耦的原则(说白了就是怕 Xray-core 挂掉全都上不了网),还是由单独的 DNS 转发器处理局域网查询吧。

Armbian 默认使用 systemd-resolved 解析 DNS 并占用 25 端口,由于其仅能监听本机请求而不能接受局域网内其他机器的请求[^7],需要将其禁用。此外需要将 /etc/resolv.conf 指向 127.0.0.1 以将本机的 DNS 查询转发至 Telescope DNS。

以下是我的配置文件,读者可以参考。

toml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
listen = "192.168.0.1:53"
disable_qtypes = ["AAAA"] # 禁用 IPv6 解析

[cache]
size = 4096

[hosts] # 为 DoH 域名指定 IP 地址,防止回环解析(指解析所必需的 DoH 域名本身也需要解析)
"doh.pub" = "1.12.12.12"
"cloudflare-dns.com" = "1.0.0.1"

[groups]
  [groups.clean] # 境内域名
  doh = ["https://doh.pub/dns-query"] # 腾讯 DNSPod DoH
  dns = ["114.114.114.114"]
  concurrent = true
  no_cookie = true # DNSPod 可能需要
  redirector = "oversea_ip2dirty" # 按照下方同名 redirector 设置重定向

  [groups.dirty] # 境外域名
  dns = ["208.67.222.222:5353", "176.103.130.130:5353"]
  doh = ["https://cloudflare-dns.com/dns-query"] # Cloudflare DoH
  gfwlist_file = "/etc/ts-dns/gfwlist.txt"

[redirectors]
  [redirectors.oversea_ip2dirty]
  # 解析后如发现ip地址不匹配cnip,则重定向到dirty组解析
  type = "mismatch_cidr"
  rules_file = "/etc/ts-dns/cnip.txt"
  dst_group = "dirty"

为了让本机的 DNS 解析通过本机的服务器,需要更改默认的 DNS 解析器。

EasyMosdns

Mosdns 以强大的功能而闻名,但对于不想花时间配置的人来说,使用别人配好的方案也能十分舒适,比如 EasyMosdns

因为它和 Mosdns 都没有被大部分发行版打包,所以在 Armbian 上,需要先手动安装对应版本的 Mosdns,然后将上述 repo 内的文件丢进对应目录。至于 Systemd 单元等文件,可以参见 AUR

默认监听 53 端口,可以在 /etc/mosdns/config.yaml 中设置。

dnsmasq 整合

确实 dnsmasq 可以当 DNS 转发器啊,但因为它对 DNS 污染抗性比较差,所以我肯定不会单独用它的。不过可以把它套娃,在本机上做两层转发。

如果你希望做两层转发,那么需要将嵌套的转发器监听端口改一下,比如 5353,将这个地址改为 dnsmasq 的上游 DNS,并且把 dnsmasq 的 cache size 设为 0(否则第一次查询大概率失败);如果你希望把 dnsmasq 的 DNS 禁掉,那么这三个设置都不用动,然后给 dnsmasq 加个 -p0 启动参数就可以了。

DHCP

DHCP 用于为局域网内的设备分配各自的 IP 地址,同时也会传递网关、默认 DNS 等信息。在我的网络拓扑中,由软路由充当 DHCP 服务器,其他设备则仅需自带的 DHCP 客户端。

一般来说 Linux 的网络连接由 systemd-networkd 或 NetworkManager 之类的东西管理(包含 DHCP 客户端),而在基于 Ubuntu 的系统上有个好东西叫做 Netplan,用于在前述两者等一系列工具之上再构建一层抽象,以求简化其配置。

该机型 Armbian 的 Netplan 配置文件位于 /etc/netplan/armbian-default.yaml。未经修改的文件类似于:

yaml
1
2
3
network:
  version: 2
  renderer: NetworkManager

要将软路由作为 DHCP 服务器,需要先将对应接口的 DHCP 客户端关掉(路由器一般当然不能从局域网其他主机取得 IP 地址了),然后再为其单独安排一个 IP 地址。再次提醒,我计划把局域网设备插在 eth0

yaml
1
2
3
4
5
6
7
network:
  version: 2
  renderer: NetworkManager
  ethernets:
    eth0:
      dhcp4: false # 这个 false 是关掉 DHCP 客户端
      addresses: [192.168.0.1/24] # 网段自定,后面对应修改

保存后不要忘了使用 sudo netplan try 测试配置文件是不是写对了,没问题的话就确认。

至于 DHCP 服务器,我起初选择了 coredhcp。其并不支持 SLAAC 宣告,故对 IPv6 环境并不友好;而 dnsmasq 虽然看起来麻烦,但实际上只需要动几个配置项就行了,而且对 IPv6 比较友好。

照例是配置文件:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
server4:
    listen:
        - "%eth0" # 只监听 eth0 接口的请求
    plugins:
        - lease_time: 3600s
        - server_id: 192.168.0.1 # 填路由器 IP 就行
        - dns: 192.168.0.1 # 重要,设置为路由器 IP 以指向路由器上的 DNS
        - router: 192.168.0.1 # 网关,主路由兼具代理功能,所以这里也是路由器 IP
        - netmask: 255.255.255.0 # 子网掩码
        - range: /etc/coredhcp/leases.txt 192.168.0.2 192.168.0.254 12h # 分配地址文件,起始地址,结束地址,租约时间

NAT

NAT 对局域网内 IP 和端口号二元组与外网 IP 和端口号二元组之间建立联系,并进行转换。此处使用的工具是 nftables。依托其强大的规则体系,我们可以自由选择转发暴露的端口,以及内外能访问的服务等。

nftables 官方文档 2 给了我们一个很好的基础配置,我也知道其他高级用途也需要依托 nftables,此处按下不表。但要做到 NAT,只需要用一张 nat 表的 masquerade 规则。

perl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/usr/sbin/nft -f

flush ruleset

define WANLINK = lan0

table ip nat {
        chain  prerouting {
                type nat hook prerouting priority -100;
        }
        chain postrouting {
                type nat hook postrouting priority 100; policy accept;
                oif $WANLINK masquerade
        }
}

要暂时应用 Nftables 规则,可以使用 sudo nft -f <filename>,永久应用则需要将其保存到 /etc/nftables.conf,并设置权限 755。

启用 IP 转发

只有启用代理转发,流量才能进入 Netfilter 的 forward 链,从而不经本地用户程序而向外转发。

/etc/sysctl.d/ 下新建一个文件,名字随意(如 98-ip-forwarding.conf),内容为:

plaintext
1
2
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1

然后运行 sudo sysctl -p 使其生效。这样,软路由就有了将不属于它的包转发出去的能力。

PPPoE

安装 pppoeconf,然后运行它就可以了。它的底层是 pppd,所以修改配置也需要到 /etc/ppp/ 里面修改。

使用 PPPoE 上网后,会生成一个 ppp0 虚拟网卡,它充当了 WAN 口的作用;使用原来的 WAN 接口是无法直接联网的。使用透明代理等需要指定接口的应用时请务必注意。

进阶应用

按照上面的步骤配置完毕后,如无意外,软路由本身和接入其的设备应该已经可以上网了。然后是一些相比 OpenWrt,Armbian 没有 out-of-box 支持的。

透明代理

请阅读 Armbian + R2S 番外篇:透明代理 一文。

另外,如果使用 Tailscale,实测透明代理可能会与 Tailscale 规则冲突(实测 23/8/26 有此现象),从而导致上不了网的严重问题,所幸 1.48.0 开始 Tailscale 增加了 Nftables 支持,将其启用似乎可以解决这个问题。在 /etc/default/tailscaled 中加入 TS_DEBUG_FIREWALL_MODE=nftables 即可3

基本防火墙

将这些规则加入之前的 nftables 配置,可以对内外网进行基本的隔离。修改自 4

perl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
table inet filter {
        chain inbound {
                type filter hook input priority 0; policy drop;
                ct state vmap { established : accept, related : accept, invalid : accept } counter
                ip protocol icmp icmp type { destination-unreachable, echo-reply, echo-request, source-quench, time-exceeded } limit rate 5/second accept
                tcp dport 22 accept
                iifname vmap { lo: accept, $WANLINK : jump inbound_world, $LANLINK : jump inbound_private }
        }
        chain inbound_world {
                ip saddr { $RESERVED_IP } drop
        }
        chain inbound_private {
                ip protocol . th dport vmap { tcp . 22 : accept, udp . 53 : accept, tcp . 53 : accept, udp . 67 : accept }
        }
        chain forward {
                type filter hook forward priority 0; policy accept;
        }
}

UPnP / NAT-PMP

UPnP 和 NAT-PMP 本质上是类似的东西,类似于一个自动的路由器端口映射。可以使用 MiniUPnPd 来实现。

Ubuntu 和 Debian 已经打包了 miniupnpdminiupnpd-nftables 两个包,安装时会自动进入向导,分别填入内网接口(eth0)和外网接口(lan0),并选择启用即可。

Full Cone NAT

Nftables 的 NAT 行为 masquerade 是 Symmetric NAT。打游戏的应该会需要 Full Cone NAT,可以参考一下 这个实现。需要编译对应的 libnftnlnftablesnft,以及内核模块。

当然如果你和我一样已经配好了 Xray-core 透明代理,那么它已经帮你用神奇的方式实现了 Full Cone NAT5

MSS Clamping

我不只一次见网友说自己搭的软路由访问某些网站非常慢,而换回硬路由就正常。这是因为多数家用路由器默认对 IPv4 下的 TCP 开启了 MSS (maximum segment size) Clamping。

《开启 IPv6 后网速变得很慢?可能是 PMTU 黑洞的问题》

简而言之就是家用硬路由会自动帮你设置 MSS,而软路由不会,造成被路径上丢包只能重传。进行 MSS Clamping 后,终于能跑满 300M 宽带了。

在 Nftables 中添加以下内容:

perl
1
2
3
4
5
6
table inet filter {
    chain forward {
        type filter hook forward priority 0; policy accept;
        tcp flags syn tcp option maxseg size set rt mtu
    }
}

IPv6

以网上资料的细碎程度,IPv6 其实也是一个进阶应用;但是感觉对自己来说,即使没有全局梯也不能没有 IPv6,所以就挺拧巴的,这也是我把它放在最后的原因。如果你对 IPv6 没有那么熟悉,我强烈建议你展开下面的内容看点更拧巴的现象和我的理解,防止配完黑人问号。

常用的路由器 IPv6 部署方案解析

IPv6 的情况与 IPv4 不同,路由器较少使用 DHCPv6 来为局域网设备分配 IP 地址(而且支持也像屎一样 [^13]),更常用的叫做无状态地址自动配置(SLAAC,StateLess Address Auto Configuration),一般来说路由器会选择 LAN 上前缀的一个 /64 地址块,向局域网内进行宣告,然后各个主机通过某种方式,自己在这个 /64 里选择一个地址(并非一个设备直接分走一个 /64,所以运营商只给 /60 而不是 /56 并没有什么问题;感谢 V2EX @dalaoshu 的纠正)。因为一个 /64 实在是太巨大了,所以一般也不会冲突。

诶,看起来拧巴的事情来了:本人使用的长沙联通宽带,光猫已改为桥接路由器拨号;而对于 PPPoE,协议本身会给路由器 ppp0 指定一个 /64 的地址 [^14]。不知细心的你是否发现了, 我们没说过 ppp0 在委托的那个子网里面——事实上也确实不在。当执行 ip -6 addr 时,就会得到以下看起来十分抽象实际上却又合理的输出(节选):

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ ip -6 addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 2408:aaaa:450:3650:4c43:81ff:fe4d:9cac/60 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::4c43:81ff:fe4d:9cac/64 scope link
       valid_lft forever preferred_lft forever
6: ppp0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1492 state UNKNOWN qlen 3
    inet6 2408:bbbb:451:50b6:7a88:efdb:db94:4b77/64 scope global temporary dynamic
       valid_lft 259001sec preferred_lft 85804sec
    inet6 2408:bbbb:451:50b6:4e43:81ff:fe4d:9cac/64 scope global dynamic mngtmpaddr
       valid_lft 259001sec preferred_lft 172601sec
    inet6 fe80::4e43:81ff:fe4d:9cac peer fe80::16eb:8ff:feb2:b27d/128 scope link
       valid_lft forever preferred_lft forever

说它抽象吧,是因为在我的直觉里,似乎 IPv6 不会分配私有 IP,所以大家都是平等的设备,也理所应当属于一个子网;说它合理吧,是既然 IP 转发启用了,那么路由器就可以将其送达该有的目的地,路径也并不需要跟着网段走。事实上,WAN 口的 /64 是通过 PPPoE 的 SLAAC 获得的,这与 LAN 口的 DHCPv6-PD 获得的前缀来源不同,所以才会出现两个地址不在同一网段的情况。

其实类比 IPv4 也是很好理解的:运营商给一个普通路由器的 WAN 口分配了一个公网 IP;同时路由器通过 DHCP 将内网的私有地址分配给各个主机,LAN 口也获得了一个该网段下的私有地址。IPv6 情况其实差不多,只是上面的私有地址段变成了通过 DHCPv6-PD 获得的公网地址段,通过某个公网地址能够直接路由到特定的主机,无需经过 NAT 而已;子网内的 DHCPv4 也变成了 SLAAC,不过结果上没区别,都是主机获得了自己的 IP。

评论区有人提到了 IPv6 的一些奇怪情况;此外关于 PD 和 SLAAC,也可以多阅读 这篇文章 作为参考。

好的,逼叨完了,结果很明了了:我们需要配置的有

  1. 一个能进行 DHCPv6-PD 的客户端,
  2. 一个能进行 SLAAC 宣告的程序,和
  3. 让路由器本身获得 IPv6。

第一个事好说,用得很广泛的是 wide-dhcpv6-client。安装后编辑 /etc/wide-dhcpv6/dhcp6c.conf。以下给出一个示例,它从运营商请求一个非临时的前缀,然后将这个前缀委托给 eth0,让 eth0 获得一个 /64 地址。

perl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface ppp0 { # 通过 ppp0 进行 DHCPv6
    send ia-pd 0; # 发送 Prefix Delegation 消息
    send ia-na 0; # 请求非临时地址(non-temporary address)
    script "/etc/wide-dhcpv6/dhcp6c-script";
};

id-assoc pd 0 { # PD 0 的配置
    prefix-interface eth0 { # 将前缀委托给 eth0
        sla-id 0; # SLA ID,即网段编号
        sla-len 0; # SLA 长度,即用掉整个 PD
    };
};

id-assoc na 0 {}; # NA 0 的配置,留空就行

这是一个很基础的配置,如果想整更多花活,建议读 manual

重启一下服务,如果看到 eth0(或自己定义的 LAN)上有了一个大于 /64 的非私有 IP,那么就是成功了,不出意外的话 route 也会为你配好;否则可以自行使用 dhcp6c -Df -c /path/to/conf <interface> 进行 debug。

现在连上设备并不能用 SLAAC,因为我们还没有配置路由宣告。如果你已经使用 dnsmasq 作为 DHCP 服务器,那么宣告非常简单:仅需要在配置文件中加类似下面这一行。

plaintext
1
dhcp-range=::1000,::1fff,constructor:eth0,slaac,ra-names

我来详细解释一下:

  • 即使向运营商申请了非临时前缀,也可能变,所以前缀不能写死,要自动从接口获取
  • constructor:eth0 指的是分配给 eth0 的 IP 段
  • ::1000,::1fff 指的是所有 IPv6 地址,当然不能给 IPv4 做 SLAAC 不是嘛
  • slaac 指的是选择外部分配的前缀,而非 fe80 开头的内网 IP 段
  • ra-names 代表进行 SLAAC 宣告,不进行 DHCPv6,并将主机添加到 DNS

第三个事情看起来很玄学。在远古的 Linux 内核中,当开启 IPv6 forwarding 的时候,即使 net.ipv6.conf.all.accept_ra 不为 0,其也会自作主张地停止进行 SLAAC,导致路由器 WAN (eth0)接口无法获取 IPv6 地址;而在不那么老的内核上,将其设为 2,就可以同时开启 IPv6 forwarding 和进行 SLAAC6

这样,局域网下的设备就可以通过 IPv6 上网了,没有邪道 DHCPv6。