本文将使用OpenWRT上的natmap,设置防火墙规则开放和转发端口,然后以qBittorrent为例,将通告端口改为公网端口。

前言

一定要用OpenWRT吗?

因为路由器本身就是管理NAT的设备,管理打洞和转发逻辑上就很合适。如果不想将natmap安装在OpenWRT上,可以安装在客户端上。如果没有OpenWRT,设置防火墙可以改为使用natmap的转发模式。

为什么不使用UPnP?

UPnP假设内网开放端口的请求都是安全的,没有任何权限控制,本文倾向于手动控制防火墙规则。

端口转发结构

运营商会将打洞端口转为公网端口,本文用natmap从STUN服务器检测公网端口。STUN的检测流程可以看检测 NAT 类型 - NAT 类型初探 | Yesterday17’s Blog里的一张图,非常清晰。

+--------------------+          +-------------------+          +-------------------+
| 路由器              |          | 运营商             |          | STUN 服务器        |
|                    | -------->|                   | -------->|                   |
| [natmap打洞端口]    |          | [公网端口]         |          |                   |
+--------------------+          +-------------------+          +-------------------+

为了让BT客户端宣告的是公网端口,我们将BT监听端口改成和公网端口一样,然后让OpenWRT防火墙将传入打洞端口的流量转到BT监听端口上

                                                                                (Peer)
                                                                                  |
                                                                                  v
+----------------------------+          +--------------------+          +-------------------+ 
| BT客户端                    |          | 路由器              |          | 运营商             |
|                            |<-------- |                    |<-------- |                   |
| [监听端口(改成和公网端口一样)] |          | [natmap打洞端口]    |          | [公网端口]         |
+----------------------------+          +--------------------+          +-------------------+ 

安装natmap

opkg install natmap
# OpenWRT 22.03+
opkg install luci-app-natmap

配置natmap

uci set natmap.qbt=natmap
uci set natmap.qbt.enable='1'
# 只有IPv4需要打洞
uci set natmap.qbt.family='ipv4'
# 只需要为TCP打洞,UDP协议的uTP会基于BEP-55自己打洞
uci set natmap.qbt.udp_mode='0'
uci set natmap.qbt.interface='wan'
# BitTorrent经常有连接,不需要natmap来维持打洞时间,此处设置了一个月的保活间隔
uci set natmap.qbt.interval='2592000'
# 挑一个连接性好的公共STUN服务器,可以看讨论: https://github.com/heiher/natmap/issues/18
uci set natmap.qbt.stun_server='turn.cloudflare.com'
# 我的路由是小米的,就写了小米路由官网
uci set natmap.qbt.http_server='www.miwifi.com'
# 打洞时会用路由器的这个端口出去,可以随意挑选一个自己喜欢的数字,不要太小(1024~65535)
uci set natmap.qbt.port='<hole punch port>'
# 接下来会在这个位置创建脚本,修改防火墙规则和qBittorrent监听端口
uci set natmap.qbt.notify_script='/usr/bin/notify-firewall-and-qbt.sh'
# 保存但先不重启服务
uci commit natmap

分配静态地址

首先给运行BT的机器分配静态的IPv4地址

uci set dhcp.bt_client=host
# 运行BT的机器的MAC地址
uci set dhcp.bt_client.mac='<BT client MAC address>'
# 随意挑选喜欢的内网IPv4,比如192.168.1.2
uci set dhcp.bt_client.ip='<BT client IP address>'
# 保存并重启服务
uci commit dhcp
/etc/init.d/dnsmasq restart

新建防火墙规则

分别为IPv4 TCP、IPv4 UDP和IPv6创建防火墙规则,用来允许入站,后续会动态修改

# 转发IPv4 TCP端口
uci set firewall.qbt_ipv4_tcp=redirect
uci set firewall.qbt_ipv4_tcp.name='qBittorrent-IPv4-TCP'
uci set firewall.qbt_ipv4_tcp.src='wan'
# 设置为打洞端口
uci set firewall.qbt_ipv4_tcp.src_dport='<hole punch port>'
uci set firewall.qbt_ipv4_tcp.dest='lan'
# 给BT的机器分配的内网IPv4地址
uci set firewall.qbt_ipv4_tcp.dest_ip='<BT client IP address>'
uci set firewall.qbt_ipv4_tcp.target='DNAT'
uci set firewall.qbt_ipv4_tcp.proto='tcp'
# 之后dest_port会被修改为BT监听端口
uci set firewall.qbt_ipv4_tcp.dest_port='<hole punch port>'

# 转发IPv4 UDP端口
uci set firewall.qbt_ipv4_udp=redirect
uci set firewall.qbt_ipv4_udp.name='qBittorrent-IPv4-UDP'
uci set firewall.qbt_ipv4_udp.src='wan'
uci set firewall.qbt_ipv4_udp.dest='lan'
# 给BT的机器分配的内网IPv4地址
uci set firewall.qbt_ipv4_udp.dest_ip='<BT client IP address>'
uci set firewall.qbt_ipv4_udp.target='DNAT'
uci set firewall.qbt_ipv4_udp.proto='udp'
# 之后src_dport和dest_port会被修改为BT监听端口
uci set firewall.qbt_ipv4_udp.src_dport='<hole punch port>'
uci set firewall.qbt_ipv4_udp.dest_port='<hole punch port>'

# 开放IPv6端口
uci set firewall.qbt_ipv6=rule
uci set firewall.qbt_ipv6.src='wan'
uci set firewall.qbt_ipv6.dest='lan'
uci set firewall.qbt_ipv6.target='ACCEPT'
uci set firewall.qbt_ipv6.family='ipv6'
uci set firewall.qbt_ipv6.proto='tcp udp'
uci set firewall.qbt_ipv6.name='qBittorrent-IPv6'
# 之后dest_port会被修改为BT监听端口
uci set firewall.qbt_ipv6.dest_port='<hole punch port>'

# 保存并重启服务
uci commit firewall
/etc/init.d/firewall restart

打洞完成后触发的脚本

此脚本会修改防火墙规则,并以qBittorrent为例,通过API设置监听端口新建脚本并赋予可执行权限

touch /usr/bin/notify-firewall-and-qbt.sh
chmod +x /usr/bin/notify-firewall-and-qbt.sh

安装curl

opkg install curl

写入如下内容

#!/bin/sh

# https://github.com/heiher/natmap?tab=readme-ov-file#script-arguments
PUBLIC_PORT=$2

# 给BT的机器分配的内网IPv4地址
QBT_HOST="<BT client IP address>"
# qBittorrent WebUI监听端口
QBT_PORT="7474"

# IPv4 TCP
uci set firewall.qbt_ipv4_tcp.dest_port=$PUBLIC_PORT
# IPv4 UDP
uci set firewall.qbt_ipv4_udp.src_dport=$PUBLIC_PORT
uci set firewall.qbt_ipv4_udp.dest_port=$PUBLIC_PORT
# IPv6
uci set firewall.qbt_ipv6.dest_port=$PUBLIC_PORT

uci commit firewall
/etc/init.d/firewall restart >/dev/null 2>&1
echo -e "Setting firewall to forward and open port of $PUBLIC_PORT"

QBT_API_URL="http://$QBT_HOST:$QBT_PORT/api/v2/app/setPreferences"
JSON_PAYLOAD="{\"listen_port\":$PUBLIC_PORT}"

curl -X POST "$QBT_API_URL" \
     --data-urlencode "json=$JSON_PAYLOAD" \
     -H "Content-Type: application/x-www-form-urlencoded"

if [ $? -eq 0 ]; then
    echo -e "Update qBittorrent port to $PUBLIC_PORT"
else
    echo -e "Failed to connect qBittorrent API"
fi

启动

现在启动qBittorrent,然后观察系统日志,可以用LuCI,或者:

logread -fe natmap

重启natmap

/etc/init.d/natmap restart

调试

notify-firewall-and-qbt.sh

/usr/bin/notify-firewall-and-qbt.sh 0 <port>

natmap

如果TCP已经打洞成功,又重启natmap的话,会触发错误([E] Cannot assign requested address · Issue #27 · heiher/natmap

daemon.err natmap[22007]: [E] hev_sock_client_tcp src/hev-sock.c:171 Cannot assign requested address, Please check is another instance exists or wait a minute. More: https://github.com/heiher/natmap/issues/27
daemon.err natmap[22007]: [E] tnsk_run src/hev-tnsk.c:103 Start TCP keep-alive service failed.

natmap会自动重试,忽略即可。如果很急,可以更换一个STUN服务器

uci set natmap.qbt.stun_server='turn.cloudflare.com'
uci commit natmap
/etc/init.d/natmap restart

检测打洞是否成功

telnet <公网IP> <端口>
Connected to <公网IP>

uTP BEP-55

下图展示了主动用uTP连接过我的IPv4地址:
有I和P flag的IPv4地址