使用 Phicomm N1 作为路由器

最近咕力十足,新高三狗也确实比以前忙了很多,都咕了两篇 blog 了,不过刚刚搞的这个绝对不能咕!
之前买了个 Phicomm N1,但是因为家里有服务器所以一直吃灰,近期之前的家里小米路由器 3 越来越不撑了,也不能充分利用那边的校园网资源。于是我萌生了拿 Phicomm N1 作为路由器的打算。
说干就干,于是我拿着刷了 Debian 9 stretch 的 Phicomm N1 过去了。
设备: 入户线 (静态公网 IP,无 DHCP),刷有 Debian 9 stretch 的 N1,小米路由器 3,一根网线。这里设 N1 连接的小米路由器上的 LAN 口为 LAN 1。

配置 VLAN

切入正题,首先 Phicomm N1 只有一个以太网口,虽然也许我们可以插 USB 网卡,但是不知道为什么我的多个型号的 USB 网卡都不能工作,看起来是内核驱动出了问题,而内核又一直是我的知识盲区。那么这个口就需要同时承担 LAN 和 WAN 的功能。这里我们就需要引入 VLAN。对于 VLAN 的定义不再赘述,它的功能就是在一个 interface 上划分出不同的虚拟 LAN,起到分割广播域 (广播域的定义不再赘述) 的作用。
小米路由器刷了 NATCAP 移植的 OpenWRT,因此支持 VLAN 交换。
我们的思路是,通过使用不同的 VLAN tag,使得小米路由器的 LAN 口、Wi-Fi 和 N1 处于一个广播域中,使得小米路由器的 WAN 口和 N1 处于一个广播域中,这样 N1 上的虚拟 WAN 口就可以和运营商路由器处于一个广播域中,从而互相访问。

OpenWRT 的原理

当我们插入 WAN 的网线时,其实实质上,我们是将运营商的交换机和我们路由器的 WAN 口所在的 VLAN 进行了桥接。因此,只要我们在 N1 上创建一个 VLAN 使得这个带 VLAN tag 的接口可以和路由器 WAN 口上的局域网处于一个广播域。

在小米路由器上的具体操作

在操作之前,请回顾一下 VLAN 和桥接的原理,由于时间和篇幅限制,这里不再赘述,详见大学教材《计算机网络》。
首先,必须先关闭小米路由器的 DHCP 功能,否则客户端 IP 将会混乱。
在我们内网 (VLAN ID 1) 上应当将小米路由器的 WAN 设置为 off,并且令所有 LAN 口为 untagged。这样所有在内网传输的以太帧不带 tag 且不会被发送到上级。
然后,我们创建一个 VLAN ID 为 2 的 VLAN,这里我们需要 LAN 1 和运营商处于一个广播域,因此 LAN 1 应当为 tagged。而带有我们的 VLAN ID 2 的以太帧运营商交换机不会接受,因此在发出时应当对以太帧进行去 tag。所以这里 WAN 口应当被设置为 untagged,同时设置其它接口为 off,这样发往 WAN 的以太帧不会出现在内网中。
注意,在操作过程中,由于 MT7620 方案只有一个网卡,所有网络接口都接在内部的交换机上,通过内部划分 VLAN 来识别以太帧来自哪个接口。因此,CPU 接口应当始终为 tagged,否则系统将不能识别以太帧的来源。
最终配置如图:
小米路由器 3 上的 VLAN 配置

在 Phicomm N1 上的具体操作

由于一些原因,这里使用了一些比较“不现代”的方法。
我们通过 vlan 包来管理 VLAN 接口,并且需要引入 8021q 内核模块。对于 Debian 方法如下:

sudo apt install -y vlan
sudo modprobe 8021q
echo "8021q" | sudo tee -a /etc/modules

这里我使用了服务的方式来开启 WAN interface,由于 systemd 有一个 network-online.target,作为基本网络服务的 target,我们可以创建一个这样的服务使得它能够在系统网络启动时,应用程序启动前执行。
我把这个服务叫做 wan-interface.service,服务配置文件如下:

# /etc/systemd/system/wan-interface.service
[Unit]
Description=Router WAN interface with VLAN

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/bin/sh /usr/sbin/create_wan_vlan.sh
ExecStart=/bin/sh /usr/sbin/start_wan_interface.sh
ExecStop=/bin/sh /usr/sbin/stop_wan_interface.sh
ExecStopPost=/bin/sh /usr/sbin/remove_wan_vlan.sh

[Install]
WantedBy=network-online.target

这里我们使用了 4 个 bash 脚本来集成服务功能。为了可移植性,我首先创建了配置文件 /etc/wan.conf:

RAW_DEVICE="eth0"  # 裸网络接口
VLAN_ID=2 # VLAN ID
IF_ADDR="211.xxx.xxx.62" # WAN 口 IP 地址
IF_NETMASK="255.255.255.0" # WAN 口子网掩码
IF_NET="211.xxx.xxx.0" # WAN 口网络号
IF_GATEWAY="211.xxx.xxx.1" # WAN 口网关地址
IF_BCAST="211.xxx.xxx.255" # WAN 口广播地址
IF_MTU="1500" # WAN 口 MTU

这里 /usr/sbin/create_wan_vlan.sh 的内容如下:

#!/bin/sh
. /etc/wan.conf
/sbin/vconfig add $RAW_DEVICE $VLAN_ID
exit 0

这里 /usr/sbin/start_wan_interface.sh 的内容如下:

#!/bin/sh
. /etc/wan.conf
/sbin/ifconfig $RAW_DEVICE.$VLAN_ID inet $IF_ADDR netmask $IF_NETMASK broadcast $IF_BCAST mtu $IF_MTU up
/sbin/route add -net $IF_NET netmask $IF_NETMASK gw $IF_GATEWAY
/sbin/route add default gw $IF_GATEWAY

这里 /usr/sbin/stop_wan_interface.sh 的内容如下:

#!/bin/sh
. /etc/wan.conf
/sbin/ifconfig $RAW_DEVICE.$VLAN_ID down
/sbin/route del -net $IF_NET netmask $IF_NETMASK gw $IF_GATEWAY
/sbin/route del default gw $IF_GATEWAY

这里 /usr/sbin/remove_wan_vlan.sh 的内容如下:

#!/bin/sh
. /etc/wan.conf
/sbin/vconfig rem $RAW_DEVICE.$VLAN_ID
exit 0

通过 sudo systemctl enable wan-interface.service 来使它开机自启,sudo systemctl start wan-interface.service 来立即启动。
这样 VLAN 就配置完毕了,如果你像我一样使用 VLAN ID 2,你会看到一个叫做 eth0.2 的网络设备,它在活跃地发送着数据包!
这时候 ping 一下网关看看是不是通了?

路由基本功能配置

在现代的 Linux 上,配置路由功能已经非常简单。

内核包转发

首先打开 IPv4 和 IPv6 的内核包转发:

echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
echo "net.ipv6.conf.all.forwarding = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

或者手动在 /etc/sysctl.conf 中修改来开启对 IPv4、IPv6 包转发,并通过 sudo sysctl -p 来应用。

iptables 开启的 IPv4 NAT

只需两行代码:

sudo iptables -A FORWARD -i * -o eth0.2 -j ACCEPT
sudo iptables -A POSTROUTING -o eth0.2 -j MASQUERADE

假如你的 WAN 口设备名不是 eth0.2,请修改成对应的。
现代的 netfilter 子系统似乎已经不再是严格的 Symmetric NAT 了,我实地测试的结果是 Restricted Cone NAT,也许端口紧张时会跳 NAT。至于 Full Cone NAT,这不是 OpenWRT 有人写好了脚本,涉及内核模块又是我的知识盲区了,所以先咕着。
当然,我们还需要让 iptables 能够开机自动加载配置,你可以把这两行代码放到 /etc/rc.local 里,或者写一个服务开机启动来加载,反正无论如何这不是问题。

ip6tables 开启的 IPv6 NAT (尚未成功)

由于这里我使用了清华 ISATAP 隧道,因此需要使用 IPv6 NAT,不过不知道为什么我还没有成功。以后再具体研究。(就是咕,可能咕几天到十几天,也可能咕到高考后

DHCPv4 服务的配置

怀着对 BusyBox 的热爱,我使用了 BusyBox 中内嵌的 udhcpd。
只需修改 /etc/udhcpd.conf 即可自定义 DHCPv4 功能。以下是我的配置,由于配置非常简单,这里就不作说明了。注意如果你的斐讯 N1 不作为 DNS 那么需要在 DNS 处删掉 N1 的 IP 地址。

start       192.168.15.20
end     192.168.15.200
interface   eth0
option  subnet  255.255.255.0
opt router  192.168.15.250
opt dns 192.168.15.250 101.6.6.6
option  domain  lan
option  lease   864000      

然后通过 systemctl 设置开机启动以及启动服务即可。

IPv6 Router Advertisement 的配置

由于我使用了清华隧道,需要给内网的设备分配私网 IPv6。这里使用了 radvd。如果上级网络有原生 IPv6 也可以使用 6relayd。
配置如下:

interface eth0 {
        AdvSendAdvert on;
        MinRtrAdvInterval 3;
        MaxRtrAdvInterval 10;
        prefix fd23:dead:beef::/64{
        };
};

附加功能的配置

DNS 服务器

首先我们要关闭 systemd-resolved 来释放 53 端口:

sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved

区别于使用 dnsmasq,我采用了 Go 语言编写的 overture。这个“船新”的 DNS 服务器要比 dnsmasq 强大的多,还可以配置上游 DNS 为非 53 端口来使用“干净的 DNS”。
overture 的配置文件也非常简单,这是我的配置文件:

{
  "BindAddress": ":53",
  "PrimaryDNS": [
    {
      "Name": "Tsinghua TUNA",
      "Address": "101.6.6.6:53",
      "Protocol": "udp",
      "SOCKS5Address": "",
      "Timeout": 6,
      "EDNSClientSubnet": {
        "Policy": "manual",
        "ExternalIP": "211.xx.xxx.62"
      }
    }
  ],
  "AlternativeDNS": [
    {
      "Name": "USTC LUG",
      "Address": "202.141.162.123:5353",
      "Protocol": "udp",
      "SOCKS5Address": "",
      "Timeout": 6,
      "EDNSClientSubnet": {
        "Policy": "manual",
        "ExternalIP": "211.xx.xxx.62"
      }
    }
  ],
  "OnlyPrimaryDNS": true,
  "IPv6UseAlternativeDNS": false,
  "WhenPrimaryDNSAnswerNoneUse": "PrimaryDNS",
  "IPNetworkFile": {
    "Primary": "./ip_network_primary_sample",
    "Alternative": "./ip_network_alternative_sample"
  },
  "DomainFile": {
    "Primary": "./domain_primary_sample",
    "Alternative": "./domain_alternative_sample"
  },
  "HostsFile": "./hosts_sample",
  "MinimumTTL": 0,
  "DomainTTLFile" : "./domain_ttl_sample",
  "CacheSize" : 0,
  "RejectQType": [255]
}

其中我设置的工作目录为 /opt/overture,对于自定义的 Hosts,可以修改 hosts_sample 文件。

然后我们发现系统本身似乎失去了解析 DNS 的能力。我们当然可以通过一些其它方法来解决,但我选择了一个非常简单粗暴的方法。在 /etc/rc.local 中加入 echo 'nameserver 127.0.0.1' >> /etc/resolv.conf

Dr.COM 宽带认证客户端

这里我直接复用了许多我之前基于 drcom_generic 项目的 Hiwifi-Drcom 的许多代码。
首先安装 Python 2。这个非常简单,不多说了。假定 drcom.py 位于 /usr/local/share/drcom/ 下,我们需要创建服务文件:

# /etc/systemd/system/drcom.service
[Unit]
Description=Dr.COM Client
After=wan-interface.service

[Service]
Type=simple
User=nobody
Group=daemon
ExecStart=/usr/bin/python /usr/local/share/drcom/drcom.py
Restart=on-failure

[Install]
WantedBy=network-online.target

这里注意,我们需要对 drcom.py 进行修改。由于我们使用现代的 systemd,记录 PID 已经不再必要,而且也不利于我们低权限启动。找到 def daemon(): 这一句,将其修改为:

def daemon():
    pass

其它配置详见 drcom_generic 项目和 Hiwifi-Drcom 项目的文档。

清华大学 ISATAP 隧道

对于有公网 IP 而没有 IPv6 的网络,使用清华隧道是一个很好的解决方案。
这里直接抄了 TUNA 官方给的代码,首先创建服务配置文件 (注意根据自己情况修改):

# /etc/systemd/system/thu-isatap.service
[Unit]
Description=ISATAP via Tsinghua
After=drcom.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh /usr/sbin/start_thu_isatap.sh
ExecStop=/bin/sh /usr/sbin/stop_thu_isatap.sh

[Install]
WantedBy=network-online.target

相应的,我创建了两个脚本来配置 ISATAP,start 和 stop 脚本都是抄的 TUNA 代码

UPnP 配置

MiniUPnPd 的 Debian 包里面内含的 /etc/init.d/miniupnpd 是坏的,不能用,删了它。
创建服务文件如下:

# /etc/systemd/system/miniupnpd.service
[Unit]
Description=MiniUPNP Daemon
After=wan-interface.service

[Service]
Type=forking
ExecStart=/bin/sh /usr/sbin/start_miniupnpd.sh

[Install]
WantedBy=network-online.target

相应的,/usr/sbin/start_miniupnpd.sh 内容如下:

#!/bin/sh
IFACE_INT="eth0"  # 请将 eth0 改为你内网网卡设备名,环境和我一样就不用动
UUID="$(uuidgen)"  # 建议手动通过 uuidgen 生成一个填入
IFACE4=`ip route show|grep default|sed -e 's/^default.*dev \([^ ]\+\).*$/\1/'`
IP4=`ip addr show dev $IFACE4 | grep -m 1 'inet\ ' | sed -e 's/^.*inet \([^ \\]\+\)\/.*$/\1/'`
INT_IP4=`ip addr show dev $IFACE_INT | grep -m 1 'inet\ ' | sed -e 's/^.*inet \([^ \\]\+\)\/.*$/\1/'`
/usr/sbin/miniupnpd -i $IFACE4 -o $IP4 -a $IFACE_INT -N -u $UUID -S

服务的操作不再赘述。
另外,之前漏掉了一点,UPnP 需要配置 iptables:

iptables -N MINIUPNPD
iptables -I FORWARD -i eth0.2 -o eth0 -j MINIUPNPD
iptables -t nat -N MINIUPNPD
iptables -t nat -I PREROUTING -i eth0.2 -j MINIUPNPD

请记得保存 iptables 配置。

网络保活

我第一天配好 N1 离开之后发现 N1 撑不到十二分钟就挂了,内网外网都连不进去,推测是 kernel panic 但是 watchdog 没有自动重启。今天抱着显示器去实地考察发现并不是 kernel panic。似乎是内核网卡驱动有 bug。那么这又涉及到我的知识盲区了……高考完一定得学一下 Linux 内核的知识。
我的方法是写一个脚本来保活,这里参考了一个 GitHub Gist 上的实现,改进了一下使得它更适合我的环境:

#!/bin/bash

interface=eth0
WATCHER="192.168.15.1"
## check if lan wire connected
if [[ `cat /sys/class/net/$interface/carrier` -eq 0 ]]; then
    logger -i -t "Network-Holder" "[ERROR] LAN wire is unplugged."
    exit 0
fi
## check if device operating
if [[ `cat /sys/class/net/$interface/operstate` = "down" ]]; then
    ifup $interface
    systemctl restart wan-interface.service
    logger -i -t "Network-Holder" "[INFO] $interface was restarted"
fi
## check if network connected
ping -c 1 -w 1 -q $WATCHER >/dev/null && exit 0
## if not, restart the interface
/sbin/ifdown $interface
/sbin/ifup $interface
systemctl restart wan-interface.service
logger -i -t "Network-Holder" "[INFO] Network was reconnected."
exit 0

其中 WATCHER 是网络中一个可以确保存活的设备,这里我把它设置为小米路由器的 IP,因为只要小米路由器还活着,这个保活就还有意义,否则整个 N1 就已经断网了保活也没意义了。
INTERFACE 为裸设备名,我们用裸设备来接内网。
我把这个脚本放在了 /usr/sbin/network-holder.sh 处。
关于自动执行,我选择使用 systemd 的 timer 机制,我们需要分别创建 network-holder.timer,内容如下:

# /etc/systemd/system/network-holder.timer
[Unit]
Description=Making sure that the network is alive.
After=wan-interface.service

[Timer]
OnBootSec=1min
OnUnitActiveSec=15sec
Unit=network-holder.service

[Install]
WantedBy=network-online.target

然后需要创建对应的 network-holder.service 来执行具体动作:

# /etc/systemd/system/network-holder.service
[Unit]
Description=The network holder service.
After=wan-interface.service

[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/tmp
ExecStart=/bin/bash /usr/sbin/network-holder.sh
Restart=on-failure

然后我们只要 sudo systemctl enable network-holder.timer 来设置定时器开机自启,sudo systemctl start network-holder.timer 来启动定时器。就可以实现每 15 秒检测一下网络进行保活了。这样处理后一晚上都没炸。

结语

这样一番配置之后 N1 就可以作为一个基础的纯路由器使用了。虽然单线复用会导致局域网内非 N1 的主机访问广播域之外的主机时链路带宽降低 50%,但就目前老校园网速度被压到了上行 15Mbps 下行 20Mbps 的情况下,这个影响并不大。

当然这一篇文章也不仅仅适用于斐讯 N1 这一款设备,更像一种软路由的通用化指南。文章就在这里了,至于怎么用就看你了。

3 评论

  1. 我觉得你需要使用最新版的Armbian,不会断网。

    还有我想了解一下这个UPNP到底是怎么实现的…

    我那天基于Ubuntu弄了半天都无法实现upnp

    我的QQ是1787074172

    如果可以就加一下吧qwq

    1. 我当时赶时间没有测试 UPnP 的可用性,只是照着 OpenWRT UPnP 的方案去做的。按理说只要 miniupnpd 正常跑起来了 UPnP 就能用了。

      网卡断流问题似乎确实是 Armbian 版本问题,以后有时间去升级一下。

      另外,QQ 好友申请已发送,很高兴认识你qwq

发表评论

电子邮件地址不会被公开。 必填项已用*标注


由于移动端输入法兼容性问题,Markdown 编辑器已停用,但您仍可以使用 Markdown 语法。