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 作为 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 口。如无特别说明,后面的操作均按照这个来。

DNS

DNS 用于将主机名解析为 IP 地址。由于传统 DNS 查询是明文的,可以被中间人任意截留修改,部分运营商会进行 DNS 污染,以达到加广告或封锁境外网站的目的。因此,使用 DoH(DHS-over-HTTPS)和 DoT(DNS-over-TLS),并对国内和国外域名进行分流,就有其意义。此处使用 Telescope DNS 负责监听 53 端口,为局域网内设备进行解析,并根据条件将其发送至不同的 DNS 服务器。

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

Armbian 默认使用 systemd-resolved 解析 DNS 并占用 25 端口,由于其仅能监听本机请求而不能接受局域网内其他机器的请求2,需要将其禁用。此外需要将 /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 解析器。

*实际上,Tailscale 自带一个 DNS 服务器,如果使用 Tailscale 且配置得当,让 MagicDNS 接管也是一个好选择(即下图中的 override local DNS)。

Tailscale Magic DNS
Tailscale Magic DNS

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。考察了 GitHub 上许多 DHCP 服务器实现,de-facto 的 Dnsmasq 配置起来相对有点太复杂了,manual 也太长。Kea 又太大,Dora 则硬是没编译出来(不得不感叹 Go 的交叉编译简单程度)。

照例是配置文件:

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 官方文档 3 给了我们一个很好的基础配置,我也知道其他高级用途也需要依托 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 使其生效。这样,软路由就有了将不属于它的包转发出去的能力。

进阶应用

按照上面的步骤配置完毕后,如无意外,软路由本身和接入其的设备应该已经可以上网了。然后是一些相比 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 即可4

基本防火墙

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

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 NAT6

后记

以上均为个人在试图使用 Armbian 作为路由器系统时摸爬滚打得出的一些经验,如果你看到这段文字,则表明我大概率还在摸索一个舒服的网络配置。