Lan Tian @ Blog

在 Traceroute 里膜 拜大佬

Traceroute 是常用的检查网络状况的工具之一,会显示你操作的电脑到指定服务器的网络路径上经过的每一个路由器的 IP 地址,类似于这样:

插图

可以看到后两跳的 IP 显示出了对应的域名,这个域名就是 IP 的反向解析记录。反向解析记录在 DNS 服务器中以类似 4.3.2.1.in-addr.arpa 域名的 PTR 记录形式存在。更多的信息可以参考《在 DN42 中设置 IP 反向解析》这篇文章。

然而,PTR 记录并不一定要设置成实际的域名,可以设置成任意的字符串,只要“和域名长得像”即可。利用这一点,我们可以在一段 Traceroute 中的每一跳上写一句话,整段就组成了完整的文章,类似下图:

插图

本文均在 DN42 网络中完成,如果你已经加入了 DN42 网络,可以 ping、traceroute 通文中的 IP。但本文并不局限于 DN42,如果你有可以自己控制反向解析的公网或内网 IP 段,也可以用相同的方法完成设置。

准备路由

第一步是设置一批路由器,让它们依次把某个 IP 对应的数据包层层转发下去,从而在 Traceroute 中产生一条较长、足以写文章的路径。

最原始的方法,就是找几台路由器,用网线串联起来。但是首先,我得有这么多路由器;另外,我还得把它们连入 DN42。

进一步想,Linux 也具有路由功能,也可以在一台服务器上开几台 Linux 虚拟机,分别分配 IP 地址,然后在每台上面运行:

ip route add [目标 IP]/32 via [下一个虚拟机的 IP]

目标 IP 的流量就会经过每一台虚拟机转发,产生一段路径。

接下来就要考虑怎么开这些虚拟机了。我的 Kimsufi 服务器上有 ESXi,用 Alpine Linux 开一排虚拟机也不是很占资源,但是如果这样操作,就需要手动配置好几台(本例中是 5 台)虚拟机,太麻烦了!

换一种思路:Docker 实际上就是一个对 LXC 容器的管理工具,而 LXC 容器都拥有独立的网络命名空间,可以独立设置自己的 IP、路由信息,在这个用途中完全可以代替完整的 Linux 虚拟机。

接下来就开始制作 Docker 镜像了。大致思路是基于 Alpine 镜像,然后在启动时运行如下脚本:

#!/bin/sh
echo Target IP is $TARGET_IP
THIS_IP=$(ip addr show dev eth0 | grep inet | cut -d' ' -f6 | cut -d'/' -f1)
echo My IP is $THIS_IP
NEXT_IP=$(echo $THIS_IP | awk -F. '{print $1 "." $2 "." $3 "." $4 + 1}')
if [ $THIS_IP == $NEXT_IP ]; then
    echo I\'m the target, listening
else
    echo Routing $TARGET_IP to $NEXT_IP
    ip route add $TARGET_IP/32 via $NEXT_IP
fi
ping 127.0.0.1 -q

最后一行的 ping 是让容器一直运行下去,不要退出。完整的 Dockerfile 可以在 https://github.com/xddxdd/dockerfiles/tree/master/route-next 看到。

然后是生成对应的 docker-compose.yml 以便统一管理,示例可以在上述 Repo 中看到。也可以用上述 Repo 中的 mk-compose.py 工具来快速生成 docker-compose.yml,但是生成完后仍需手工修改 network 中的网段信息。

然后把它传到服务器上 docker-compose up -d 启动这批容器。

最后,在运行 Docker 的服务器上执行这条命令:

ip route add 172.22.76.102/32 via 172.22.76.98

把流量传入第一个 Docker 容器。这时 Traceroute 一下,可以看到数据包的路径:

插图

准备文章

由于 PTR 记录只能存在英文,因此我们要先找一篇英文短文。由于 8 月 17 快到了,我选了这样一篇文章:

One should uphold his country’s interest with his life, he should not
do things just to pursue his personal gains and he should not evade
responsibilities for fear of personal loss.

由于上面数据包的路径上总共有 5 跳,因此将文章拆分成 5 段,并删除 PTR 记录中不允许存在的标点符号:

  • one should uphold his country s interest with his life
  • he should not do things
  • just to pursue his personal gains
  • and he should not evade responsibilities
  • for fear of personal loss

然后把空格全部换成英文句点:

  • one.should.uphold.his.country.s.interest.with.his.life
  • he.should.not.do.things
  • just.to.pursue.his.personal.gains
  • and.he.should.not.evade.responsibilities
  • for.fear.of.personal.loss

然后一句一句填到路径上各个 IP 的 PTR 反向解析记录里:

插图

保存,等待 DNS 生效后:

插图

你在 Traceroute 中就可以看到一篇文章了。

x86 下制作 ARM Docker 镜像,Docker Hub、Travis 自动构建

一般情况下,Docker 的镜像都是在一个已有的镜像内,一步步运行给定的命令,从而生成一个新的镜像。这样的步骤在大多数人使用的 x86 架构计算机上都不是问题,由于架构互相兼容,一台计算机上生成的镜像往往可以被直接复制到其它计算机上运行,除非镜像中的程序使用了 AVX 等较新的指令集。

但是,还有一批基于 ARM 架构的主机也可以运行 Docker,并运行专门编译的 ARM 架构的镜像。这些主机包括树莓派系列,和其它类似树莓派的主机,例如 Cubieboard,Orange Pi,Asus Tinker Board 等等。另外,Scaleway 等主机商也提供基于 ARM 架构的独立服务器。

由于 ARM 架构的系统无法在 x86 架构计算机上运行,因此无法在 x86 计算机上直接通过 Dockerfile 生成 ARM 架构的镜像,一般采用的方法是直接找一台 ARM 主机来 docker build。

但是我在为我的树莓派制作 nginx 的 Docker 镜像时发现这并不是一个很好的方法。由于树莓派的内存只有 1GB,如果开启多线程编译(make -j4 或者 make -j2),内存会不足,gcc 会被杀掉;如果单线程编译(直接 make),编译时间又非常长(几个小时)。

经过查找,另有一种方案可以解决这个问题。这个方案是在 x86 架构计算机上模拟 ARM 环境,即“虚拟机”的方式来编译镜像。虽然 x86 模拟 ARM 没有硬件加速(VT-x,AMD-V 等)支持,效率极低,但是得益于 x86 CPU 的高性能,总体效率还是高于直接在树莓派上编译。

qemu-user-static

第一步是要模拟出一个 ARM 环境。当然,我们可以用 QEMU 直接开一个 ARM 架构的完整 Linux 虚拟机,然后在里面运行 Docker 构建镜像。但是这样做的问题是,需要额外管理一个 Docker,难以将系统资源在主系统和虚拟机之间灵活分配,并且难以使用脚本自动化,即难以整合到 CI、CD 中。

更好的方案是 qemu-user-static,是 QEMU 虚拟机的用户态实现。它可以直接在 amd64 系统上运行 ARM、MIPS 等架构的 Linux 程序,将指令动态翻译成 x86 指令。这样 ARM 系统环境中的进程与主系统的进程一一对应,资源分配灵活,并且易于脚本自动化。

但是还有一个问题:当 ARM 进程尝试运行其它进程时,qemu-user-static 并不会接管新建的进程。如果新的进程仍然是 ARM 架构,那么 Linux 内核就无法运行它。因此,需要开启 Linux 内核的 binfmt 功能,该功能可以让 Linux 内核在检测到 ARM、MIPS 等架构的程序时,自动调用 qemu-user-static。开启该功能,并且注册 qemu-user-static 虚拟机后,运行 ARM 程序就和运行 x86 程序一样,对用户来说毫无差别。

在 x86 Docker 中运行 ARM 镜像

要在 Docker 中运行 ARM 镜像,我们要先在计算机上注册 qemu-user-static 虚拟机:

docker run --rm --privileged multiarch/qemu-user-static:register --reset

另外,Docker 镜像内必须也含有对应的 qemu-user-static 虚拟机。不过,Docker Hub 上已经有了添加 qemu-user-static 的系统镜像,可以在 https://hub.docker.com/u/multiarch/ 获取:

插图

例如,multiarch/alpine 镜像就在不同 tag 下提供了 aarch64(armv8)、armhf、amd64、i386 的镜像:

插图

如果你之前已经注册了虚拟机,那么就可以直接运行了:

docker run -it --rm multiarch/alpine:armhf-edge /bin/sh
docker run -it --rm multiarch/alpine:aarch64-edge /bin/sh

插图

修改 Dockerfile

接下来我们要在 Dockerfile 中调用 ARM 架构的镜像。如果你的 ARM 主机是 armv7l(armhf)架构(树莓派(默认),Tinker Board 等),那么把 Dockerfile 中的第一行修改成 https://hub.docker.com/u/multiarch/ 下对应的 armhf 架构镜像即可。对应关系如下:

  • alpine -> multiarch/alpine:armhf-edge
  • ubuntu:bionic -> multiarch/ubuntu-debootstrap:armhf-bionic
  • debian:stretch -> multiarch/debian-debootstrap:armhf-stretch

如果你的 ARM 主机是 aarch64(armv8)架构(树莓派 3 开始支持,但是需要特殊系统才是这个架构),那么对应关系如下:

  • alpine -> multiarch/alpine:aarch64-edge
  • ubuntu:bionic -> multiarch/ubuntu-debootstrap:arm64-bionic
  • debian:stretch -> multiarch/debian-debootstrap:arm64-stretch

改完后直接重新构建镜像,你就可以在本地生成 ARM 架构的镜像了。

Docker Hub 自动构建

Docker Hub 不仅提供镜像的存储共享服务,也提供简单的镜像自动构建服务。自动构建服务给每个用户分配了一台 2GB 内存、1 核心 CPU、30GB 硬盘的完整虚拟机运行 2 小时(来自 Docker 官方论坛),并且用户具有 root 权限。

默认的自动构建相当于是我们构建镜像时运行的 docker build 那一步,但是我们需要在这之前注册 qemu-user-static 虚拟机。我们可以用 Docker 官方提供的 hook 在构建开始前运行自定义的命令(来自 Docker Cloud 文档)。因为我们分配到的是完整的虚拟机,有 root 权限,所以我们也可以在 hook 中注册虚拟机。

如何创建这样一个 hook?在 Dockerfile 的文件夹下创建 hooks 文件夹,再在 hooks 文件夹下创建 pre_build 文件,内容如下:

#!/bin/sh
docker run --rm --privileged multiarch/qemu-user-static:register --reset

可以在我的这个 commit 中看到 hook 的示例。

Docker Hub 的自动构建服务会先运行这个脚本注册 qemu-user-static,然后再开始构建。构建完成时 push 上来的就是 ARM 架构

如果你的镜像构建时没有编译操作,构建速度应该相当快,不会比 x86 的镜像慢多少;但是如果有大量的编译操作,例如我的 nginx 镜像,很有可能就超出了 2 小时的时间限制而构建失败。在这种情况下,我们就要换其它不限制时间的自动构建服务,例如 Travis CI。

Travis CI 自动构建

Travis CI 是对开源社区免费的一款自动构建工具。只要你的 Dockerfile 传到了 GitHub 上的 Public Repository(公开代码仓库)里,就可以直接使用它。

对于构建 Docker 镜像来说,Travis 提供的配置是 7.5GB 内存、2 核心 CPU、18GB 硬盘,不限制运行时间。因此编译时可以开启 make -j4 四线程编译来提高速度。

首先到 https://travis-ci.org/ 用 GitHub 账号登录,然后开启你放 Dockerfile 仓库的自动构建功能。

插图

然后在 Settings 页面添加你的 Docker Hub 账户的用户名密码到环境变量,这样后续你就不用在自动构建配置中明文保存密码了。

插图

然后创建一个名为 .travis.yml 的文件到 git 仓库的根目录,这就是 Travis 的 “Dockerfile”,保存你的自动构建指令。

.travis.yml 的语法较复杂,你可以在我的 .travis.yml 的基础上作修改。我的 .travis.yml 可以在 https://github.com/xddxdd/dockerfiles/blob/master/.travis.yml 看到。

如果需要更复杂的修改,可以阅读 Travis 的官方文档自行学习。

编辑 .travis.yml 完成后,把它提交到 GitHub 上,Travis 就会自动开始构建你的镜像,把它们 push 到 Docker Hub 上,并且发邮件告诉你自动构建的情况。

Docker 镜像的精简

自从放弃 OpenVZ 架构的 VPS 并购买 KVM 架构的 VPS 以来,我一直使用 Docker 部署 nginx、MariaDB、PHP 等网站需要的程序,不仅方便平时单个服务的重启和配置管理(把配置目录全部用 volume 映射到一起管理),而且方便了服务的升级。

例如,我现在博客所在的 VPS 因为配置不高,内存占用最近一直在 80% 左右。我要更新 nginx 或者向 nginx 里加模块时,如果直接在这台 VPS 上编译 nginx,不仅速度慢,而且有可能因为内存不足而把网站也崩掉。使用 Docker 后,我就可以在其它的空闲资源较多的 VPS 或者在我自己的电脑上构建镜像,push 到 Docker Hub,再在 VPS 上 pull 下来运行。

不过,一直以来,我的 nginx 镜像大小都在 200 MB 左右(Docker Hub 显示大小,不包含基础镜像大小,比一般 docker image 显示的要小),明显大于它应该有的容量,不过因为 VPS 的硬盘暂时足够,而且前段时间我也没什么时间,我就没有管这个问题。现在有了空,我就仔细研究并解决了这个问题,修改了 Dockerfile,把镜像大小降到了 17 MB。

合并 RUN 命令

刚打开 Dockerfile,我就发现了一个低级错误:

FROM debian:stretch
MAINTAINER Lan Tian "lantian@lantian.pub"
ENV NGINX_VERSION=1.15.1 OPENSSL_VERSION=OpenSSL_1_1_1-pre8
RUN apt-get update -q \
    && apt-get -y upgrade \
    && apt-get -y install build-essential git autoconf automake libtool wget tar zlib1g-dev libpcre3 libpcre3-dev unzip libjemalloc-dev patch
RUN cd /tmp \
    && wget -q http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz \
    && tar xf nginx-${NGINX_VERSION}.tar.gz \
    && # 接下来是编译过程

Docker 在构建镜像时,会为 Dockerfile 中的每一条指令生成一“层”镜像,将一层层镜像叠加起来成为一“个”镜像。

在上述第一条 RUN 指令中,Docker 生成了一“层”包含了 nginx 所需的依赖包,以及 apt-get 下载下来的临时文件的镜像。后续的编译、安装过程在第二条 RUN 指令中,是在含依赖包那一层之上的一“层”操作的,完全不能影响到依赖包那一层。虽然我后续执行了 apt-get clean,但是这只是在最顶层中标记了删除这些文件,这些文件仍然在第二层含依赖包的一层留存着。

修改方法就是把两个 RUN 命令通过 && 和反斜杠连接起来,共同构成一层就可以了。

当时我这样写 Dockerfile,是因为后续的 nginx 编译过程反复出现问题,我在测试时为了防止反复下载依赖包就分离了这些指令,来重复利用下载好的依赖包。但是在测试完成后,我忘了把这里改回去!

这里修改后,Docker 镜像的大小直接小了 25 MB。虽然 175 MB 的镜像比之前已经小了一些,但是还是很大,还有精简的空间

使用 Alpine 作为基础镜像

我之前一直使用我比较熟悉的 Debian 作为基础镜像。Debian 的好处是可以使用常用的 apt-get 等工具,但是它基础镜像的体积大概在 125 MB 左右,很大。而 Alpine Linux 是一个体积只有 5 MB 左右,被广泛运用在 Docker 镜像中的 Linux 发行版。为了精简体积,我准备使用它。

首先是把 Dockerfile 开头的 FROM debian:stretch 换成 FROM alpine。我直接用了 Alpine 的最新版本,如果软件对系统版本有要求记得指定。

然后,把 apt-get 安装的依赖包在 Alpine 的包管理器 apk 中找到对应的包,并安装上去。我在 Debian 中安装依赖的命令如下:

apt-get update -q
apt-get -y install build-essential git autoconf automake libtool wget tar zlib1g-dev libpcre3 libpcre3-dev unzip libjemalloc-dev patch

把这些包一一在 apk 中找到对应的包:

apk --no-cache add build-base git autoconf automake libtool wget tar zlib-dev pcre-dev unzip jemalloc-dev patch linux-headers

其中 --no-cache 参数代表 apk 不使用本地缓存,直接去镜像源拉取软件包列表和下载,并且会自动删除下载缓存,相当于自带了 apt-get update 和 apt-get clean 两行。

同理,把最后删除编译依赖包的部分也改掉。原先在 Debian 下我是这样删的:

apt-get purge -y unzip -q git autoconf libtool automake build-essential
apt-get autoremove -y --purge

在 Alpine 下我这样删:

apk del build-base git autoconf automake wget tar unzip patch linux-headers

这样操作后,镜像的体积被直接减到了 17 MB!在 docker images 中包含 Alpine 本身显示为 39.6 MB。相比原先已经非常小了。这里应该不只是 Alpine 本身的差距,还可能和 Alpine 自带的轻量级 musl C 库文件有关。

同样操作精简其它镜像

nginx 的镜像精简完了,用类似的操作也可以精简其它的镜像。首先拿 PHP-FPM 开刀,这里因为我需要一大堆 PHP 模块所以不能直接用官方的镜像。

原先的安装命令是这样,我的 PHP-FPM 不是编译的,是直接从软件源安装的:

apt-get install -y php7.2-fpm php7.2-bz2 php7.2-curl php7.2-gd php7.2-json php7.2-mbstring php7.2-memcached php7.2-mysql php7.2-redis php7.2-sqlite3 php7.2-xml php7.2-xmlrpc php7.2-zip php7.2-intl

Alpine 的软件源也有 PHP-FPM,可以直接安装:

apk --no-cache add php7-fpm php7-bz2 php7-curl php7-gd php7-json php7-mbstring php7-memcached php7-mysqli php7-pdo_mysql php7-redis php7-sqlite3 php7-pdo_sqlite php7-xml php7-xmlrpc php7-zip php7-intl php7-ctype php7-tokenizer

其中注意 Alpine 软件源的 PHP 有点迷,默认不包含一些在其它发行版上有的包,例如 php7-ctype、php7-tokenizer,如果不装,Typecho 等程序就会报错。

换成 Alpine 后 Docker Hub 上显示的大小从 90 MB 降到了 25 MB,docker image 中的大小降到了原先的 1/4。

MariaDB 我也换成了基于 Alpine 的,不过这个镜像因为有现成的,我就没自己做,直接用了 bianjp/mariadb-alpine。它和官方基于 Debian 的镜像几乎完全兼容,我在 docker-compose.yml 里只改了 image 名就成功了,什么设置都不用动。

总结

这样操作后我的 VPS 里省了大概 1 GB 的空间,对于我的小硬盘 VPS 来说已经省出了很多的空间了。

而且以后更新程序时 push、pull 镜像的速度也大大加快了,尤其是香港 VPS 从 Docker Hub pull 镜像的速度一直不怎么样,更小的镜像意味着在更短的时间内可以 pull 下更多个版本进行测试,出现问题 pull 原来版本镜像恢复运行的速度也会更快。

RoboMaster 开发踩坑日记(2018-05-28 更新)

学校组建了一支 RoboMaster 队伍,准备参加今年的比赛。因为是新校区新学生,因此我们完全没有之前的参考资料,只能自己一个个踩坑。

以下是我们在软件开发中遇到的一些坑。

硬件版本:RoboMaster 官方开发板(信仰板)
芯片型号:STM32F427IIHx
软件系统:ChibiOS 18.2.0

信仰板 HSE 时钟频率为 12MHz 而非常见的 8MHz

最坑的是信仰板的说明书和硬件原理图上完全没有提到这事。

这个问题导致我们用 STM32CubeMX 等软件算出的时钟频率远高于额定频率,并导致了如下后果:

  • 莫名其妙的频率设置失败(设置了在合理范围内的频率,但是板子不响应了,只能短接某个电阻 Reset)
  • USART 时序错误(明明两端波特率一样,但是收发的数据就是乱码,遥控器无法使用)
  • CAN 数据无法应答(明明板子和电机电调都在发数据,示波器能解码出来,但是双方就是不 ACK)

以上问题在重新调整时钟频率后全部解决。

while(true); 发送 CAN 报文必须加延时

这是一个小问题。CAN 通信时所有设备都在一条总线上,同时只能有一台设备发送信息。如果 while(true); 发送信息不加延时,会导致其它设备没有机会发送信息,开发板自然就收不到它们的回复了。

加个 chThdSleepMilliseconds(100); 就能解决这个问题。

STM32 不能同时使用 CAN 和 USB

因为信仰板上的 UART 接口不是裸露的针,是某种接口,而我们没有线,因此我们尝试使用 ChibiOS 的 Serial over USB 功能,即通过 USB 接口模拟串口进行调试。

在尝试两个小时未果后仔细搜寻资料,发现 CAN 和 USB 不能同时使用,CAN 优先级高于 USB。

而我们必须使用 CAN 和各种电机通信,因此……我们只能继续闪灯调试了。

主线程 while(true); 堵死子线程

队友把主要逻辑(判断按键,驱动电机等)都写在另一个线程,主线程初始化完后就启动那个线程然后 while(true); 什么都不干。然后队友发现必须把逻辑线程的优先级提升到正常优先级 +2 才能正常执行,否则不运行。

我简单研究后得出结论:ChibiOS 主线程优先级是正常 +1,并且 while(true); 也在消耗 CPU,并且堵死了同等优先级或者更低优先级的其它线程。解决方法是把主线程改成 while(true) chThdSleepMilliseconds(1000);,让它去 sleep 就能解决问题。

大疆文档里某几个键顺序写反

Q、E、Shift、Ctrl 四个键,以 4 个 bit 一一对应的形式传给开发板,文档的顺序是高位到低位 Q、E、Shift、Ctrl,但实测是 Ctrl、Shift、E、Q。(差点被车撞飞)

谜之数据类型转换

队友写了机器人底盘的 PID 调速,然后发现只要轮子一动,无论哪个方向,都会全速向电机反转方向加速。

经过两个小时的调试,发现队友有类似下图的代码:

uint8_t x = 0xff; // 电机返回数据示例
uint8_t y = 0x8f; // 返回的数据分成了两个 uint8_t
int a = (x << 8 | y); // 队友的代码

此处 int 为 int32_t。

我们希望得到的值是 0xffffff8f,即 -113;运行上述代码,实际上 C 在前面补了 0,得到的 a 是 0x0000ff8f,即 65423。因此 PID 认为电机以非常快的速度在运行,并作出了诡异的调节。

C 这么做是因为 x << 8 这一步生成了一个 4 byte 的变量:

int c = sizeof(x << 8); // c = 4

然后或操作的实际对象是 0x0000ff00 和 0x8f,得 0x0000ff8f。

另外,拼接 uint16_t 时不会出现这样的现象:

uint16_t x = 0xffff;
uint16_t y = 0xff8f;
int64_t a = (x << 16 | y); // a = -113

因为 x << 16 = 0xffff0000,所以 a = 0xffffff8f = -113。

对于原来的问题,修改方法就是把 int 替换成 int16_t:

int16_t a = (x << 8 | y);

此时 a 为 0xff8f,即 -113,符合预期。

NAT64 服务器搭建

NAT64 是 IPv4 向 IPv6 过渡时出现的一项技术。它通过将 IPv4 的地址映射到一个 IPv6 地址段上,来让仅支持 IPv6 的设备同样能够访问 IPv4 网络。但由于仅支持 IPv6 的设备并不多,目前它在国内的应用主要是两个方面:

  1. 对于 IPv4 收费/限速/限流量而 IPv6 免费/不限速/不限流量的教育网用户,可以使用公共 NAT64 服务来省钱。
  2. 对于 iOS 应用开发者,用于搭建测试环境以通过 App Store 的审核。

我们也可以在自己的同时拥有 IPv4 和 IPv6 连接的路由器上安装相应的软件,来搭建 NAT64 服务器。常用的软件是 Tayga 和 Jool。其中 Tayga 年久失修,上次更新已经是 2011 年的事了,而 Jool 一直在活跃地更新,因此本文采用 Jool 来搭建。

安装 Jool

第一步是安装 Jool。Arch Linux 的 AUR 上有 Jool,而 Jool 在 Debian 和 Ubuntu 的官方源中都找不到,因此在这两个系统下需要手动编译安装。

对于 Arch Linux,直接 yaourt -S jool-dkms-git 即可。对于树莓派,需要修改 PKGBUILD,将 arch=('i686' 'x86_64') 修改成 arch=('i686' 'x86_64' 'armv7h'),即添加树莓派的架构信息,否则会提示没有对应的可以安装的架构。安装完成后,从下面的“设置内核模块开机自动加载”这一步开始做。

对于 Debian 和 Ubuntu,执行如下指令:

# 安装依赖
apt-get install build-essential dkms autoconf automake linux-headers
# 下载 Jool(本文写成时最新版本为 3.5.6)
wget https://github.com/NICMx/releases/raw/master/Jool/Jool-3.5.6.zip
unzip Jool-3.5.6.zip
cd Jool-3.5.6
# 安装内核模块
dkms install .
# 安装管理软件(可选)
cd usr
./configure && make && make install
# 设置内核模块开机自动加载
cat >/etc/modprobe.d/jool.conf <<EOF
options jool pool6=64:ff9b::/96
EOF
echo jool > /etc/modules-load.d/jool.conf
# 加载内核模块
modprobe -v jool pool6=64:ff9b::/96

安装完成后执行 jool,如果看到类似 Status: Enabled 的字样,就说明安装成功了。

测试 NAT64

IPv6 地址中专门给 NAT64 划了一个地址池,就是上文的 64:ff9b::/96。使用 64:ff9b:: 加上 IPv4 地址就可以通过 NAT64 访问了,例如如果我们的原 IP 是 8.8.8.8,那么使用 64:ff9b::8.8.8.8 就能访问。

但是在安装 Jool 的本机上直接访问这个地址是不通的,必须让本机作为路由器,让其它机器通过它才能访问 NAT64 的地址。我将树莓派作为路由器并打开 WiFi 热点,并用自己的电脑连接,可以 ping 通相应的地址就是成功了。

插图

树莓派 3B 折腾笔记:硬件看门狗

在计算机中,“看门狗”指的是一种硬件计时器,用于在计算机失去响应(死机)的时候重启计算机。计算机的系统上要运行一个程序不断和看门狗硬件通信。当通信中断经过一段预设的时间后,看门狗就会通过发送 RESET 信号或者切断再接通电源等方式强制重启,保证计算机上运行的服务不长时间中断。

在折腾树莓派的过程中,我也曾好几次让树莓派失去响应,结果不得不人工开关电源来重启。通过开启树莓派上的硬件看门狗功能,就可以减少这种情况的出现。

加载驱动

由于 Linux “万物皆文件”的特点,可以通过 ls 命令直接查看看门狗驱动的状态:

ls /dev/watchdog

如果有这个文件,可以直接跳到下一部分。如果没有,就要根据树莓派版本加载驱动:

  1. 树莓派 1代的驱动名为:bcm2708_wdog
  2. 树莓派 2代的驱动名为:bcm2709_wdog
  3. 树莓派 3代的驱动名为:bcm2835_wdt

使用 modprobe -v [驱动名] 加载驱动,然后再 ls /dev/watchdog 查看情况。如果驱动加载成功,就要将这个驱动设置为开机加载。编辑 /etc/modules,另起一行填入驱动名就可以了。

安装通信软件

前面提到,硬件看门狗需要和软件通信来确定系统的状态。在 Raspbian 下这个软件是 watchdog,可以直接 apt-get 安装:

apt-get install watchdog

然后由于显而易见的原因,要把它设置为开机启动:

systemctl enable watchdog

然后编辑配置文件 /etc/watchdog.conf,作出如下修改:

  1. 取消 #max-load-1 = 24 的注释(删除开头的 # 号),代表当系统 1 分钟内的负载高于 24(已经非常非常高了),就重启系统
  2. 取消 #watchdog-device = /dev/watchdog 的注释,设置看门狗的路径
  3. 增加一行 watchdog-timeout = 15,代表 15 秒内系统无响应就重启系统,在树莓派 3B 上这个值最高为15。注意不要设置的太小,否则可能造成系统反复重启。

保存修改,重启看门狗服务:

service watchdog restart

看门狗功能就启用了。

测试

可以通过 kill 掉看门狗服务来模拟系统死机的情况:

pkill -9 watchdog
pkill -9 wd_keepalive

过 15 秒后树莓派就会自动重启。

解决树莓派 HW CSum Failure 问题

今天登录上树莓派,习惯性 df 查看磁盘空间,发现树莓派 TF 卡上的空间所剩无几。最开始我以为我设置错误,把要挂机下载的文件下载到了 TF 卡里而不是移动硬盘里。结果排查下来,/var/log 下的日志文件居然占据了整整 18G 的空间。查看了一下日志,基本上都是类似如下的报错:

Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143274] eth0: hw csum failure
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143281] CPU: 0 PID: 1075 Comm: vncagent Not tainted 4.9.77-v7+ #1081
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143283] Hardware name: BCM2835
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143294] [<8010fa48>] (unwind_backtrace) from [<8010c058>] (show_stack+0x20/0x24)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143303] [<8010c058>] (show_stack) from [<804578e4>] (dump_stack+0xd4/0x118)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143312] [<804578e4>] (dump_stack) from [<80629704>] (netdev_rx_csum_fault+0x44/0x48)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143320] [<80629704>] (netdev_rx_csum_fault) from [<8061c2c4>] (__skb_checksum_complete+0xb4/0xb8)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143328] [<8061c2c4>] (__skb_checksum_complete) from [<806c0be8>] (nf_ip_checksum+0xd4/0x130)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143376] [<806c0be8>] (nf_ip_checksum) from [<7f5b8de0>] (tcp_error+0x1d0/0x21c [nf_conntrack])
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143463] [<7f5b8de0>] (tcp_error [nf_conntrack]) from [<7f5b3674>] (nf_conntrack_in+0xd4/0x984 [nf_conntrack])
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143515] [<7f5b3674>] (nf_conntrack_in [nf_conntrack]) from [<7f5e1488>] (ipv4_conntrack_in+0x28/0x2c [nf_conntrack_ipv4])
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143527] [<7f5e1488>] (ipv4_conntrack_in [nf_conntrack_ipv4]) from [<80663c40>] (nf_iterate+0x74/0x90)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143534] [<80663c40>] (nf_iterate) from [<80663cc4>] (nf_hook_slow+0x68/0xc0)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143541] [<80663cc4>] (nf_hook_slow) from [<8066bac8>] (ip_rcv+0x468/0x55c)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143549] [<8066bac8>] (ip_rcv) from [<806271a4>] (__netif_receive_skb_core+0x2b4/0xbc0)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143556] [<806271a4>] (__netif_receive_skb_core) from [<80629a54>] (__netif_receive_skb+0x20/0x7c)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143564] [<80629a54>] (__netif_receive_skb) from [<80629adc>] (netif_receive_skb_internal+0x2c/0xa4)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143571] [<80629adc>] (netif_receive_skb_internal) from [<80629b78>] (netif_receive_skb+0x24/0x98)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143581] [<80629b78>] (netif_receive_skb) from [<7f618514>] (ifb_ri_tasklet+0xf4/0x29c [ifb])
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143591] [<7f618514>] (ifb_ri_tasklet [ifb]) from [<80123244>] (tasklet_action+0x74/0x10c)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143599] [<80123244>] (tasklet_action) from [<8010169c>] (__do_softirq+0x18c/0x3cc)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143605] [<8010169c>] (__do_softirq) from [<80122ccc>] (irq_exit+0x10c/0x168)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143613] [<80122ccc>] (irq_exit) from [<801738f8>] (__handle_domain_irq+0x70/0xc4)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143622] [<801738f8>] (__handle_domain_irq) from [<8010150c>] (bcm2836_arm_irqchip_handle_irq+0xa8/0xac)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143630] [<8010150c>] (bcm2836_arm_irqchip_handle_irq) from [<8071c13c>] (__irq_svc+0x5c/0x7c)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143633] Exception stack(0xb13b3b60 to 0xb13b3ba8)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143639] 3b60: 00000001 39f65000 20000113 00000001 baae6a4c 80b81a3c 39f65000 00000002
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143645] 3b80: b21cbe20 80c6f440 baefcb74 b13b3c44 80c040a4 b13b3bb0 80214ae0 80214700
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143649] 3ba0: a0000113 ffffffff
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143658] [<8071c13c>] (__irq_svc) from [<80214700>] (get_page_from_freelist+0x258/0xb0c)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143668] [<80214700>] (get_page_from_freelist) from [<8021578c>] (__alloc_pages_nodemask+0xf0/0xe58)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143678] [<8021578c>] (__alloc_pages_nodemask) from [<8021c3f8>] (__do_page_cache_readahead+0xf8/0x270)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143688] [<8021c3f8>] (__do_page_cache_readahead) from [<8020efe4>] (filemap_fault+0x338/0x674)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143696] [<8020efe4>] (filemap_fault) from [<8030db3c>] (ext4_filemap_fault+0x3c/0x50)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143707] [<8030db3c>] (ext4_filemap_fault) from [<8023e2f4>] (__do_fault+0x7c/0x100)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143715] [<8023e2f4>] (__do_fault) from [<80242638>] (handle_mm_fault+0x614/0xd98)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143723] [<80242638>] (handle_mm_fault) from [<8071cbf0>] (do_page_fault+0x338/0x3bc)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143730] [<8071cbf0>] (do_page_fault) from [<801012a8>] (do_PrefetchAbort+0x44/0xa8)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143737] [<801012a8>] (do_PrefetchAbort) from [<8071c6a4>] (ret_from_exception+0x0/0x1c)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143740] Exception stack(0xb13b3fb0 to 0xb13b3ff8)
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143744] 3fa0:                                     0011ff6c 00000001 a63dc0de 0006ac00
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143750] 3fc0: 76c37000 00000001 76c374ac 00000001 00675cd8 00000000 76f2b000 00000000
Jan 25 22:51:15 lantian-rpi3 kernel: [   22.143755] 3fe0: 76f2ace4 7e9b5cc0 76b29dd4 0006ac00 60000010 ffffffff

即,树莓派的有线网卡部分出现了大面积的报错,内核不断的打 Stacktrace 导致日志文件暴涨。而我的树莓派因为用来挂校内 PT 站,常常有 5MB/S 以上的上传下载,日志量可想而知。

报错的“HW CSum”功能全称为“Hardware Checksum Offloading”,即将网络数据包的校验转交给网卡芯片,从而降低 CPU 占用的功能。为了排查问题,我尝试用 ethtool 关闭该功能:

apt-get install ethtool
ethtool --offload eth0 rx off tx off

关闭后 dmesg 中不再出现类似报错。再尝试打开该功能:

ethtool --offload eth0 rx on tx on

dmesg 中再次出现大面积报错,证明该问题由 HW CSum 产生。

当然,关闭 HW CSum 仅是权宜之计,这个问题应该通过更新内核和/或驱动的方式解决。但是我尝试 raspi-update,更新内核和驱动后问题仍未解决,因此只能继续停用 HW CSum。

编辑 /etc/network/interfaces.d/eth0(如果没有就创建),加入以下代码:

allow-hotplug eth0
iface eth0 inet dhcp
    offload-rx off
    offload-tx off

这样之后系统启动时就会自动调用 ethtool 禁用 HW CSum 功能。

让 ASF 帮你在 Steam 中挂“贪玩蓝月”

最近贪玩蓝月因为其洗脑的广告而流行了起来,也出现了许多通过在 Steam 中添加自定义程序后重命名为“贪玩蓝月”,达到显示自己在玩贪玩蓝月效果的教程。不过这么做需要把那个自定义程序一直开着,有些时候还是比较麻烦的。

ASF(ArchiSteamFarm)则是一个模拟用户玩游戏,从而刷 Steam 交易卡掉落的程序。因为它能模拟用户玩游戏,自然也能模拟玩“贪玩蓝月”,在自己电脑上什么都不用设置的情况下达到如图效果:

插图

实现这个效果,在你运行 ASF 的主机上修改 ASF 的配置即可。打开 config/[BOT 名称].json,找到下面三行并修改成对应的参数:

"CustomGamePlayedWhileFarming": "贪玩蓝月",
"CustomGamePlayedWhileIdle": "贪玩蓝月",
"FarmOffline": false,

第一项是在模拟玩 Steam 游戏时显示的名称,第二项是不模拟时(也就是交易卡挂完后)显示的名称。第三项是“离线挂卡”功能,就是模拟玩游戏时不在好友列表中显示上线,这项必须关闭,否则好友根本看不到你的状态。

改完重启 ASF 就有了贪玩蓝月的效果了。

树莓派 3B 折腾笔记:硬件随机数发生器

随机数在计算机中有着十分重要的应用,例如常用的 SSL 加密算法就非常依赖随机数。如果随机数不够随机,就很有可能被攻击者猜到,相应的加密验证体系也就土崩瓦解。但是由于计算机说零是零、说一是一的特点,它没有办法产生真正的随机数,只能通过复杂的算法去尽可能模拟随机数。

在 Linux 系统上,由于 Linux “万物皆文件”的特点,可以从 /dev/random 读取到由 Linux 内核综合大量数据生成的随机数。但是因为 Linux 基于“安全第一”的原则综合了大量数据,随机数的产生速度很慢。用 rng-tools 软件包中的 rngtest 工具就可以看到:

lantian@lantian-rpi3:~ $ cat /dev/random | rngtest -c 1000
rngtest 2-unofficial-mt.14
Copyright (c) 2004 by Henrique de Moraes Holschuh
This is free software; see the source for copying conditions.  There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

rngtest: starting FIPS tests...
rngtest: bits received from input: 20000032
rngtest: FIPS 140-2 successes: 999
rngtest: FIPS 140-2 failures: 1
rngtest: FIPS 140-2(2001-10-10) Monobit: 0
rngtest: FIPS 140-2(2001-10-10) Poker: 1
rngtest: FIPS 140-2(2001-10-10) Runs: 0
rngtest: FIPS 140-2(2001-10-10) Long run: 0
rngtest: FIPS 140-2(2001-10-10) Continuous run: 0
rngtest: input channel speed: (min=167.862; avg=361.389; max=4358.681)Kibits/s
rngtest: FIPS tests speed: (min=2.087; avg=13.116; max=14.309)Mibits/s
rngtest: Program run time: 55507560 microseconds

/dev/random 的读取速度仅仅 361.389 Kbit/s(注意不是千字节)。在需要大量随机数的场景,程序就不得不等待 Linux 产生更多的随机数,造成严重的延迟卡顿。

但是很多时候我们并不需要如此随机的随机数。Linux 内核还提供了 /dev/urandom,它的算法更加简单,相比 /dev/random 有几万倍的速度加成:

lantian@lantian-rpi3:~ $ cat /dev/urandom | rngtest -c 1000
rngtest 2-unofficial-mt.14
Copyright (c) 2004 by Henrique de Moraes Holschuh
This is free software; see the source for copying conditions.  There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

rngtest: starting FIPS tests...
rngtest: bits received from input: 20000032
rngtest: FIPS 140-2 successes: 1000
rngtest: FIPS 140-2 failures: 0
rngtest: FIPS 140-2(2001-10-10) Monobit: 0
rngtest: FIPS 140-2(2001-10-10) Poker: 0
rngtest: FIPS 140-2(2001-10-10) Runs: 0
rngtest: FIPS 140-2(2001-10-10) Long run: 0
rngtest: FIPS 140-2(2001-10-10) Continuous run: 0
rngtest: input channel speed: (min=334.623; avg=2492.940; max=9536.743)Mibits/s
rngtest: FIPS tests speed: (min=5.508; avg=12.871; max=14.245)Mibits/s
rngtest: Program run time: 1492273 microseconds

速度是 2492.940 Mbit/s,相比 /dev/random 不知高到哪里去了。但是这个随机数不够随机,存在被攻击者猜到的可能。

如果无法在软件上解决这个问题,那么就可以加入硬件来解决。现在的主板基本上都内置了硬件随机数产生器,它们一般是通过主板的电气信号来产生随机数。由于主板上数据流量很大,电气信号一般很难预测,因此硬件随机数一般被认为是安全的。

树莓派 3B 的 Broadcom 芯片组也有硬件随机数产生器,可以在 /dev/hwrng 看到。它的性能如下:

lantian@lantian-rpi3:~ $ cat /dev/hwrng | rngtest -c 1000
rngtest 2-unofficial-mt.14
Copyright (c) 2004 by Henrique de Moraes Holschuh
This is free software; see the source for copying conditions.  There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

rngtest: starting FIPS tests...
rngtest: bits received from input: 20000032
rngtest: FIPS 140-2 successes: 999
rngtest: FIPS 140-2 failures: 1
rngtest: FIPS 140-2(2001-10-10) Monobit: 0
rngtest: FIPS 140-2(2001-10-10) Poker: 0
rngtest: FIPS 140-2(2001-10-10) Runs: 0
rngtest: FIPS 140-2(2001-10-10) Long run: 1
rngtest: FIPS 140-2(2001-10-10) Continuous run: 0
rngtest: input channel speed: (min=18.251; avg=1103.150; max=9765625.000)Kibits/s
rngtest: FIPS tests speed: (min=2.416; avg=13.004; max=14.341)Mibits/s
rngtest: Program run time: 20213577 microseconds

1103.150 Kbit/s,是 /dev/random 的三倍,反映到需要随机数的程序上就有了很大的性能提升。但是一般程序只认 /dev/random,不会去管硬件随机数发生器,怎么办?

前面说到,/dev/random 的速度很慢。Linux 为了解决这个问题,引入了一个“随机数池”:在不需要随机数的时候,Linux 也在后台根据运行的指令等等慢慢地收集随机数,放入 /dev/random 这个“池子”里;在突然需要大量随机数的时候就从池子里取,以应付短时突发的需求而不会卡顿。因此我们只要把 /dev/hwrng 的随机数也放进 /dev/random 这个池子里就可以了。需要的程序在 rng-tools 软件包里就有:

sudo apt-get install rng-tools
sudo nano /etc/default/rng-tools
# 添加以下内容
HRNGDEVICE=/dev/hwrng
RNGDOPTIONS="--fill-watermark=50% --feed-interval=1"
# 保存,最后运行以下命令
sudo service rng-tools restart

HRNGDEVICE 一行的意思是设置额外随机数的来源,这里我们指定到硬件随机数发生器。有些教程建议设置成 /dev/urandom,但是如我们之前所说,urandom 不够安全,这样设置会影响系统的安全性。

RNGDOPTIONS 中 --fill-watermark 是在池子里随机数不足时,一口气将随机数补到给定的容量,这里设置成 50%。不设置成 100%,是因为各家硬件厂商实现随机数发生器的方式不同,并且一般均不公开,存在硬件随机数产生器中含有不为人知的后门的可能性。如果填满,相当于完全信任了硬件随机数发生器,可能会有安全隐患。

--feed-interval 是池子到达指定容量后,缓慢填充池子的间隔时间,一般设为 1 就好。

这样设置后 /dev/random 产生随机数的效率就会大幅提升了。

树莓派 3B 折腾笔记:BT 下载与策略路由

这次就直接进入正题。(其实是不知道该拿什么开头)

安装 Transmission 挂 PT

作为一个可以自由连接各种传感器的小型电脑,树莓派的可玩性相当高。即使你不想在 GPIO 上接一大堆传感器(或者像我一样觉得另外的传感器暂时没什么用),你也可以利用它低功耗的特点,让它 24 小时运行,做一些不需要大量 CPU 运算,但是因为其它因素需要较长时间才能完成的任务,例如……挂机下载。

我所在的大学有一个内网的 PT(Private Tracker)站。PT 站就是一个 BT 种子的发布网站,但是它在传统 BT 的基础上增加了用户管理功能,并且通过限制客户端种类、强制要求上传率等方式,解决了传统 BT 下各类客户端吸血(只下载不上传,例如迅雷)和种子很快失效(因为一段时间后就没人继续上传了)的问题。

但因为有了这些要求,PT 站用户往往需要长时间挂机上传下载,而这刚好是树莓派擅长的事。

上篇文章里,我弄好了一个简单的 NAS。在此基础上装一个 BT 下载软件就可以挂 PT 了。这个 PT 站在 Linux 下仅允许 Deluge,Rufus,Transmission 和 rTorrent。我一开始准备用 Deluge,但是 PT 站提示不允许使用 Raspbian 软件源内最新的 Deluge 1.3.13。该站推荐旧版本的 Deluge 1.3.3,可以在 Debian 7 软件源内找到,但是可能是因为 Debian 9 基于 systemd,我装上旧版后无法启动。

总之我选择了 Transmission。首先 apt-get:

apt-get install transmission-daemon

然后编辑 /etc/transmission-daemon/settings.json

# 屏蔽吸血客户端
"blocklist-enabled": true,
"blocklist-url": "http://john.bitsurge.net/public/biglist.p2p.gz",
# 修改默认下载位置
"download-dir": "/mnt/usb/Transmission",
# 用 PT 站一定要把这几项都关掉,如果用传统 BT 这里不用改
"dht-enabled": false,
"lpd-enabled": false,
"pex-enabled": false,
# 远程 Web 管理
"rpc-enabled": true,
"rpc-authentication-required": true,
"rpc-username": "用户名",
"rpc-password": "密码,启动 Transmission 后会被自动加密",

最后 service transmission-daemon start 启动后,访问 [树莓派 IP]:9091 可以查看 Web 管理界面,可以上传种子、调整限速等。

针对学校网络环境的调整

上篇文章中我提到过,学校提供了一个有线网和两个 Wi-Fi,它们各自有如下特点:

  1. 有线网,限速 1.5M,网页方式登录,其它设备局域网内可访问
  2. 无线网,限速 1.5M,网页方式登录,其它设备若通过无线网连接则无法访问(Wi-Fi 设备隔离,通过有线网仍可访问)
  3. eduroam,不限速,WPA2 企业级登录,其它设备若通过无线网连接则无法访问(Wi-Fi 设备隔离,通过有线网仍可访问)

由于树莓派有一个有线网卡和一个无线网卡,因此最佳的方案是,树莓派平时通过 eduroam 进行 PT 下载,我用自己电脑访问树莓派有线网卡的 IP 进行管理和访问文件。

但是如果你直接把有线网和无线网都连上,你会发现只有一张网卡有流量,另外一张网卡甚至无法 ping 通。这是因为 Linux 内核在收到连接请求(例如 TCP SYN)时,并不是“从哪来回哪去”,直接从连接请求来源网卡继续建立连接(TCP ACK)。相反,Linux 会根据内核的路由表来确定从哪张网卡回复。这样就会造成从有线网卡进入的请求被从无线网卡回复,然后回复因为设备隔离策略或是来源 IP 与网卡 IP 不符而被丢弃。

因此我们需要设置一下 Linux 的策略路由功能,做到“从哪来回哪去”。策略路由可以指定符合某些条件的数据包(例如从某张网卡进入的数据包)不经主路由表处理,而是单独开一张路由表处理它。这个功能早在《OpenVZ 配置 Hurricane Electric IPv6 隧道,开启整个地址池并与原生 IPv6 共同使用》里我就用过了。虽然当时是用在了 IPv6 地址上,但是 Linux 下 IPv4 和 IPv6 的网络命令都大致相同,因此稍微改一下就可以使用。

首先输入 route 命令查看路由表,你会看到这样的输出:

Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         10.106.65.1     0.0.0.0         UG    202    0        0 eth0
default         10.107.128.1    0.0.0.0         UG    303    0        0 wlan0
10.106.65.0     0.0.0.0         255.255.255.0   U     400    0        0 eth0
10.107.128.0    0.0.0.0         255.255.240.0   U     303    0        0 wlan0

此处可以看到,有两条 Destination(目标)为 default(默认)的路由,一条对应有线网卡(eth0),一条对应无线网卡(wlan0)。它们的网关分别是 10.106.65.1 和 10.107.128.1,记录好它们。

再输入 ip addr 看一下现在的 IP。你会看到类似如下输出:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP group default qlen 1000
    link/ether b8:27:eb:7c:5b:07 brd ff:ff:ff:ff:ff:ff
    inet 10.106.65.213/24 brd 10.106.65.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::62f6:bc87:f1f5:533a/64 scope link
       valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP group default qlen 1000
    link/ether b8:27:eb:29:0e:52 brd ff:ff:ff:ff:ff:ff
    inet 10.107.134.208/20 brd 10.107.143.255 scope global wlan0
       valid_lft forever preferred_lft forever
    inet6 fe80::f013:e96e:8451:7a94/64 scope link
       valid_lft forever preferred_lft forever

很长?其实重要的就几句话。我们只关心各张网卡的地址:

2: eth0: [...]
    inet 10.106.65.213/24 [...]
3: wlan0: [...]
    inet 10.107.134.208/20 [...]

这样就可以清楚地看到,有线网卡的地址是 10.106.65.213/24。因为 IPv4 的地址用 32 bit 表示,所以把最后(32-24=)8 bit 清零,获得 10.106.65.0/24。也就是说,你 DHCP 自动获取到的 IP 必然在 10.106.65.0/24 这个 IP 段以内。同理,无线网卡的地址是 10.107.134.208/20,清掉最后(32-20=)12 bit,获得 10.107.128.0/20。记好,等会也要用。

然后修改 /etc/iproute2/rt_tables,这里记录了路由表的列表,我们需要在这里加两张路由表,分别给两张网卡使用:

#
# reserved values
#
255    local
254    main
253    default
0    unspec
#
# local
#
#1    inr.ruhep

# 上面的都是 Raspbian 默认的设置,在文件末尾添加这两行:
100    university_eth
101    university_wlan

然后设置策略路由:

# 对于 university_eth 这张路由表,设置流量默认从 10.106.65.1(就是有线网卡的网关)走
ip route add default via 10.106.65.1 dev eth0 table university_eth
# 对于 university_wlan 这张路由表,设置默认走 10.107.128.1(无线网卡网关)
ip route add default via 10.107.128.1 dev wlan0 table university_wlan
# 对于来自 10.106.65.0/24(也就是刚才算出的有线网卡的 IP 段),走 university_eth 路由表
ip rule add from 10.106.65.0/24 table university_eth
# 对于来自 10.107.128.0/20(也就是刚才算出的无线网卡的 IP 段),走 university_wlan 路由表
ip rule add from 10.107.128.0/20 table university_wlan

输完这些命令,你的两张网卡就都能 ping 通了,“从哪来到哪去”完工。你可以把上面这四条命令加入 /etc/rc.local 以开机自动设置。如果开机时没有生效,在四行命令上面再加一行 sleep 5 就可以了——开机的时候有可能 Linux 网络功能还没启动完,因此先等 5 秒让网络功能启动了再继续设置。

最后一个问题,在同时连接了有线和无线的情况下,Linux 的 DHCP 客户端会给有线网卡设置更低的 Metric,使得流量优先从有线网卡走。Metric 就是 Linux 下每条路由的优先级,Metric 越低,优先级越高。但是我希望优先使用无线网卡,因此就要修改 DHCP 客户端的设置,给有线网卡更高的 Metric。

编辑 /etc/dhcpcd.conf

interface eth0    # 对于网卡 eth0(有线网卡)
metric 400        # 将它的 Metric 设置成 400(无线网卡默认是 303,任何比 303 大的数都可以)

保存重启,然后网络请求就都优先通过无线网卡发送了。

最终效果

树莓派下所有的下载软件均通过不限速的 eduroam Wi-Fi 进行下载。我用自己的电脑通过连接较慢的有线网卡的 IP 操作 SSH、Transmission,访问共享等,不影响 Wi-Fi 的传输速度,同时 1.5M 的限速也足以观看 1080p 分辨率的视频(特别高清的除外)。