本来按照上一篇结尾所预告的这一篇应该是要写我在 Linux 上用的开源灵车,不过 2023 年我所用的 Linux 也就是我的灵车 Arch NAS 和公司的 CentOS 7(...)而已,NAS 上的开源灵车又实在是太杂了,先写这一部分也不算跑题嘛!

我先招了,我也标题党了,即使使用容器来跑软路由这本质上也还是 All In One/All In BOOM,只不过本篇所要写的是用 systemd-nspawn 的高灵活度、高可玩性方案,没兴趣的可以退出了。

最开始搞这个的原因是,随着去年出现各种便宜的 N100 之类的小主机,周围朋友买小主机来跑软路由的越来越多,甚至还有买 mac mini 来跑软路由的;另外还有一帮人用 PVE 来在虚拟机上跑 NAS 系统和软路由系统实现 All In Boom。这些方案对我来说有点太重了,最大的障碍是我不知道现在的 openwrt 系统的代理软件发展到什么样的程度了,那么在购买一台专用的软路由之前,至少我得先用最低成本的方式体验一下吧?用 systemd-nspawn 体验完之后,我的看法是不用买了w

啥是 systemd-nspawn?

一种容器技术,由现代 GNU/Linux 系统事实上的基石底裤systemd提供,与 docker 有更复杂的关系,我在多年以前的文章在 Linux 上使用 qq 的 114514 种方法 中就有写用 systemd-nspawn 来跑 wine-qq 避免污染宿主机环境的方案,不过当时还不支持跑 non-systemd 的系统比如 openwrt,在后来的更新中支持了,可玩性更高了。

用来跑软路由有啥好处?
  • 可控性强,不需要去额外学习 PVE 这种比较重的虚拟化方案,容器的 rootfs 直接放在主系统的一个文件夹下,主系统不管用 Debian 还是 Arch 只要是你熟悉的最新的 GNU/Linux 发行版都可以,用你自己最熟悉方式来直接维护宿主机和容器,没有中间商赚差价,出了什么问题更好 debug;
  • 灵活性极高,可以任意使用最新的 Linux 内核和系统,想折腾/升级容器? btrfs snapshot 一下,然后随便装想要的包或者软件,搞炸了一键回滚,完全不在怕的,更不用像装在物理机上一样担心炸了还要重新刷机。想升级宿主机?pacman -Syu 我已经很久没滚炸到进不去系统过了(flag)想传输文件?直接从文件系统 copy 过去就行了,不用再曲线救国地用 sftp 或共享文件夹之类的方式往虚拟机里送东西了;
  • 与内核紧密结合,直接用功能齐全的正经 GNU/Linux 内核,如果需要玩什么花活那可比自己再编译个内核方便多了;
  • 探索还没有多少人尝试过的各种玩法,享受折腾自己的快乐
同样是容器,为什么不用 docker?
  • 最大的理由:你用的现代 GNU/Linux 发行版八成不会自带 docker,但九成九会自带 systemd,所以 systemd-nspawn 当然更加清真;
  • docker 会搞乱你的路由表,但 nspawn 不会;
  • 如果没有现成的 docker 镜像的话,从头手搓一个 docker file 的繁琐程度只会比下一个 rootfs 然后启动 nspawn 再装包的 nspawn 更高而不会更低;
  • 如果想在容器内使用 systemd,nspawn 的难度将远低于 docker

所以反过来说,如果你的系统自带了 docker/你想要的功能已经有人做了完美符合的镜像/你很熟悉 docker/你用不上 systemd 的话,你也可以直接用 docker。

如果你看完上面的好处产生了兴趣并且有折腾的动力的话,下面我们进入正题。

我的方案有两种,并且都属于单网口的单臂旁路由,这样不管怎么折腾都不会影响到主路由和其他不需要代理的机器,如果你有多个网口想做主路由的话就看着配置网卡的地方改改:

  • 基于 immortalwrt,体验各种 openwrt 下的代理插件,包括 passwall/2, openclash, ShadowsocksR Plus+, Hello World, 以及没有 luci 的 ShellClash(已更名 ShellCrash),还有顺带的 uu 加速器插件
  • 基于 Arch Linux,体验 dae(+ sing-box)

immortalwrt

immortalwrt 号称是最适合中国人(唉)的 openwrt 发行版,主要就是自带的软件源包含了各种代理插件和相关依赖,足够我们都体验一遍了。

首先安装容器和配置网桥,可以从 https://downloads.immortalwrt.org/ 直接下载他们提供的 rootfs, 例如:

$ machinectl pull-tar https://downloads.immortalwrt.org/releases/21.02.7/targets/x86/64/immort
alwrt-21.02.7-x86-64-rootfs.tar.gz immortalwrt --verify=no

#设置一下账号密码,以防启动了登陆不上去:
$ cd /var/lib/machines
$ systemd-nspawn -D immortalwrt
$ passwd
#改完退出回宿主机

#配置一下网桥,用于让宿主机可以直接访问到容器,这里基于 systemd-networkd, 如果不需要给宿主机透明代理的话不建网桥也没关系,后面设置一下端口映射就行了:
$ vim /etc/systemd/network/br-openwrt.netdev
# /etc/systemd/network/br-openwrt.netdev

[NetDev]
Name=br-openwrt
Kind=bridge

$ vim /etc/systemd/network/br-openwrt.network
# /etc/systemd/network/br-openwrt.network
[Match]
Name=br-openwrt

[Network]
DHCP=yes

$ vim /etc/systemd/network/br-openwrt.network
# /etc/systemd/network/br-openwrt.network

[Match]
Name=br-openwrt

[Network]
Address=10.10.0.2/24

#重启一下 systemd-networkd 载入网桥配置
$ systemctl restart systemd-networkd

然后配置一下 nspawn, 修改以下几个文件,enp4s0 是我的网卡需要改成你自己的:

# /etc/systemd/nspawn/immortalwrt.nspawn

[Exec]
PrivateUsers=yes

[Network]
MACVLAN=enp4s0:eth0
#MACVLAN=enp4s0:eth1 如果你想要用 uu 加速器的话就再加一个macvlan,我也不知道为什么,来源 https://github.com/mritd/NetEaseUU
#IPVLAN=wlan0:eth0
Bridge=br-openwrt
# /var/lib/machines/immortalwrt/etc/config/network
# 也就是 openwrt 容器里的 network 设置

config device    
    option name 'br-lan'
    option type 'bridge'
    list ports 'host0'  

config interface 'lan'  
    option proto 'static'
    option device 'br-lan'
    list ipaddr '10.10.0.1/16'  
    option broadcast '10.10.0.255'

config interface 'wan'  
        option device 'eth0'
        option proto 'dhcp'

config interface 'wan6'  
        option device 'eth0'
        option proto 'dhcpv6'
# /var/lib/machines/immortalwrt/etc/config/network
# 跟前一个同个文件,如果想用 uu 的话下面这么写是可以的

config interface 'loopback'
        option device 'lo'
        option proto 'static'
        option ipaddr '127.0.0.1'
        option netmask '255.0.0.0'

config interface 'wan'
        option device 'eth0'
        option proto 'dhcp'

config device
        option name 'br-lan'
        option type 'bridge'
        list ports 'eth1'

config interface 'lan'
        option device 'br-lan'
        option proto 'dhcp'
        option defaultroute '0'
# /var/lib/machines/immortalwrt/etc/config/firewall
# 最后提前配一下做软路由用的防火墙设置, 我放在内网做旁路由就不担心安全问题了直接全开了

...
config zone  # 找到这部分
    option name 'wan'
    option input 'ACCEPT'  # 改这里
    option output 'ACCEPT'
    option forward 'ACCEPT' #改这里
    option mtu_fix '1'
    option masq6 '1'
    option masq '1'
    list network 'wan'
    list network 'wan6'
...

改好这几个文件之后,就可以启动了!

# 启动容器
$ machinectl start immortalwrt
# 查看状态和 ip
$ machinectl status immortalwrt
immortalwrt(32698c***)
           Since: Sun 2024-03-10 20:22:11 CST; 11min ago
          Leader: 820540 (procd)
         Service: systemd-nspawn; class container
            Root: /var/lib/machines/immortalwrt
           Iface: br-openwrt
         Address: 192.168.31.121
                  192.168.31.188
                  fe80::84b6:cbff:fee6:4631%2
                  fe80::b030:7dff:fe18:9a02%2
              OS: ImmortalWrt 21.02.7

然后你就可以用显示的 ip 从 web 访问 immortalwrt 了,都到这一步了后面安装软件包之类的肯定不需要我讲了:

image

装上你想体验的代理软件,然后在手机等设备上用手动 ip 的方式指定网关到旁路由容器,然后就使用吧!(如果用 uu 的话网关需要使用 br-lan 获取的 ip)

接下来简单锐评一下几个比较热门的 openwrt 代理软件:

ShadowsocksR Plus+ / hello world
image

从名字就知道是从古老年代修修补补到现在的插件了,功能不多但依赖复杂,核心主要还是 xray,只能说凑合能用,都 2024 年了我还是想用点新的。 hello world 基本上是一样的。

OpenClash
image

听说很多人在用而让我本来很期待的本次主要体验对象之一,但实际体验下来我要点名批评了,配置过于复杂,光是设置规则的地方就有好几个出了问题都不好排查是哪里设置的不对,明明有 webui 但还是得用 yacd 面板去切节点那我要你这 luci 干嘛呢,折腾一通最后也没有比别人好用。

Passwall/2
image
image

设置相对 OpenClash 来说要清晰很多,至少规则怎么自定义、规则优先级都一目了然,缺点是黑名单模式得用曲线救国的形式才能实现,节点测速不能排序也不能自动选择低延迟节点,每次要切节点的话不太方便。相对来说更适合个人用户而不是机场用户,节点一多体验可能还是蛮差的。

Passwall 2 支持了 sing-box 内核,可以玩更多花活,不过缺点还是一样。

ShellClash(现名ShellCrash)
image

点名表扬,折腾程度只是需要用到命令行而已,但是配置在哪里都很清晰,配完以后切节点再用 webui 的面板就可以了。支持多个内核,包括 clash meta 和 sing-box,还是很合理的。不过让我纠结的点在于因为并不需要 luci,甚至在小米路由官方固件上就能使用,导致跑容器的折腾程度不那么划算。。。

Dae + sing-box on Arch Linux Subsystem for Arch Linux

dae 是由 V2rayA 重做的高性能新兴透明代理软件,利用 eBPF 进行内核级的流量转发,与其他代理的区别在于极高的直连性能。不要让我解释细节,因为我也不懂捏,作为用户体验一下就完事了。

因为原理的关系,dae 需要用到特定的内核功能,而且因为是比较新的工具的关系在最新版的 immortalwrt 中才有包但也没有能用的 luci,并且不知道用容器跑会不会还有额外的坑,所以我们干脆用 Arch 容器来跑吧,当然你想用 Debian 也可以。因为是用 Arch 来跑 Arch 容器,我给这个容器起个名字叫 ASA (Arch Linux Subsystem for Arch Linux)

# 一样先拉一个 rootfs 下来,nspawn.org 提供了各大 systemd 发行版用的镜像,可以直接用,当然你想用 pacstrap 或者 debootstrap 装个更干净的 rootfs 也可以
$ machinectl pull-tar https://hub.nspawn.org/storage/archlinux/archlinux/tar/image.ta
r.xz ASA

# 一样的先改个密码
$ systemd-nspawn -D ASA
$ passwd

然后关键的来了,为了让 dae 不报错,我们的 nspawn 文件需要一点特殊配置,必须用 SystemCallFilter=@privileged 不然 dae 会在初始化时试图移除 MemLock 然后失败退出,然后需要单独 bind mount /sys/fs/bpf,如果像 docker 一样直接 bind /sys 的话又会影响到 cgourp 之类的东西让容器起不来:

# /etc/systemd/nspawn/ASA.nspawn

[Exec]
PrivateUsers=no
#LimitMEMLOCK=infinity
#LimitNOFILE=infinity
SystemCallFilter=@privileged

[Network]
MACVLAN=enp4s0:eth0
Bridge=br-openwrt

[Files]
Bind=/sys/fs/bpf

接下来启动容器做初始配置

$ machinectl start ASA

# 进入容器的 shell
$ machinectl login ASA

# 用 systemd-networkd 通过 dhcp 获取 ip
$ vim /etc/systemd/network/20-wired.network
# /etc/systemd/network/20-wired.network
[Match]
Name=eth0

[Network]
DHCP=yes
MulticastDNS=yes
SendHostname=true

[DHCPv4]
UseDomains=true

[Link]
Multicast=yes

# 重启容器的 systemd-networkd
$ systemctl restart systemd-networkd

# 配置软件源,并添加 archlinuxcn 的软件源,不用我教了吧
# 然后安装 daed 顺带 sing-box
$ pacman -S daed sing-box

这里装的 daed 是 dae 的 web 面板,不用面板的话就得手写 dae 的配置文件,愿意折腾的可以直接装 dae,我就偷懒了。

# 启动 daed
$ systemctl start daed

然后 daed 的 web 面板就可以用 ip:2023 来访问,大概长这样:

image

基础使用的话需要修改的地方不多,只要在右边 Subscription 处导入订阅或者在中间 Node 处填入 http/socks 代理,然后将节点托到左边的 Group 里就可以,默认会用 proxy 那个 group 然后在其中选择一个延迟最低的。其他的细节建议我就提一个可以把 global 配置中的 Wan Interface 去掉来不代理本机流量,可以减少大量的错误日志(主要我也懒得 debug)。然后,打开右上角的开关就进行代理了,把手机指向网关就能用了。dae 自带的代理后端支持的协议可以看官方说明,还是不少的。

我用的机场所用的协议是 trojan,在我测试时感觉 dae 的兼容性还是不太好很多正常的节点连接不上,而且这个 web 面板说实话还是不适合手动切节点(它不会把测速延迟展示出来,而且只能拖来拖去的),还有些朋友用的奇奇怪怪的协议很可能 sing-box 也不支持,所以我决定还是将代理的工作交给 sing-box.

这也是我第一次在命令行使用 sing-box,配置还是蛮简单的,随便找个订阅转换网站把机场订阅转换为 sing-box 格式的配置文件然后放到 /etc/sing-box/config.json ,我们不需要 sing-box 的分流功能所以只需要一个这样的配置文件:

{
  "outbounds": [
   ...
  ],
  "experimental": {
    "clash_api": {
      "external_controller": "0.0.0.0:9090",
      "external_ui": "ui",
      "secret": "",
      "external_ui_download_url": "https://github.com/MetaCubeX/metacubexd/archive/gh-pages.zip",
      "external_ui_download_detour": "proxy",
      "default_mode": "rule"
    },
    "cache_file": {
      "enabled": true,
      "store_fakeip": false
    }
  },
  "inbounds": [
    {
      "type": "mixed",
      "listen": "0.0.0.0",
      "listen_port": 7890,
      "sniff": false,
      "users": []
    }
  ],
  "route": {
    "auto_detect_interface": true,
    "final": "proxy"
  }
}

中间的 clash_api 让我们可以在启动后通过 ip:9090 打开 sing-box 面板测速和选择想要的节点。然后,把 sock://127.0.0.1:7890 加入 daed 的 Node 就可以了。

这一套折腾下来总算用上了先进的 dae,但是说实话对于一般用户来说可能还是体会不出它跟一般代理的性能差距的。。。

总的来说本方案还是以体验为主,实用价值未来可期,至少 webui 能改进的用户体验还是不少的。推荐给想尝鲜的人。

总结一下,本文推荐 ShellCrash。那么,下次再见。

Reference:

  • 群友番茄蛋的大量建议
  • https://github.com/67au/everything-in-nspawn/tree/master/openwrt
  • https://github.com/mritd/NetEaseUU