在刷入 OpenWrt(immortalWrt)后,桥接光猫进行宽带拨号,检测 ipv4 的 nat 环境为 full cone nat,这是比较理想的 nat 类型。
在我的内网中有一台服务器,运行着 qBittorrent 实现 PT 下载,这会涉及到端口映射的问题。
国内家宽即使是 ipv4 的 full cone nat,也并不意味着公网 ip 可达,运营商有可能会做端口限制,而且外网注册的端口也不一定是你想要的端口。
例如,使用 natmap 映射本地 8080 端口到公网端口,会发现变成一个随机端口,而不是指定的 8080 端口,这样会给 qBittorrent 的外网连接带来 2 个麻烦:
- 由于公网端口是随机的且定时变化的,无法手动在 qBittorrent 中指定“外部连接端口”;
- qBittorrent 若是不在 OpenWrt 路由器上运行,而是在内网中的设备上运行,则还需要做端口转发;
进一步说明,qBittorrent 通常认为本地网络是有公网 IP 的,所以监听端口会送给 tracker 和 DHT 节点,可由于 natmap 申请到的外网端口是随机的,会导致外网 peer 连接不上。
为了解决这 2 个问题,可以借助 natmap + 自带 OpenWrt 端口转发来实现 qBittorrent 的公网可达,示意图如下:
【公网 Internet】
│
│ (1) 连接到你的随机公网端口
▼
公网IP:PORT ←─ NATMap 映射 ──→ 内网:56666
(比如 223.73.123.169:8745)
│
(NATMap 在 OP 上保持打洞 + 抢占分配端口)
│
▼
┌───────────────────────────┐
│ OP 路由器 │
│ (运行 NATMap) │
└───────────────────────────┘
│
(2) 本地端口转发:56666 → <内网 qBittorrent 设备>:8573
│
▼
┌───────────────────────────┐
│ 4750G 服务器(内网) │
│ qBittorrent 监听 8573 │
└───────────────────────────┘
│
(3) qBittorrent 正常接收外部连接
natmap 支持在端口改动之后发送通知脚本,利用这个功能来动态更新 qBittorrent 的“外部连接端口”并更新端口转发规则,从而实现上图中的 (2) 和 (3) 步骤。
首先添加一个 natmap 的规则脚本如下:

Notify script 的内容如下:
#!/bin/sh
# NATMap notify script
# Purpose:
# NATMap already maps <external_ip:external_port> to <router:LOCAL_PORT>.
# This script creates a firewall redirect so that:
# router:LOCAL_PORT -> 192.168.2.1:8573
# Requirements:
# - Only touch "config redirect" in /etc/config/firewall
# - Do NOT modify communication rules (config rule)
# ===== 请根据 qBittorrent 的实际情况修改 =====
TARGET_IP="192.168.2.1"
TARGET_PORT="$2"
QBT_HOST="192.168.2.1" # qBittorrent 运行的 IP
QBT_PORT="18080" # qBittorrent WebUI 端口
# ===== 固定配置部分,通常不修改 =====
LOGTAG="natmap-redirect"
EXT_IP="$1" # external IP
EXT_PORT="$2" # external port(要写入 qBittorrent 配置)
LOCAL_IP6="$3" # local IPv6 (unused)
LOCAL_PORT="$4" # local port on router (we use this!)
PROTO="$5" # tcp / udp
LOCAL_IP4="$6" # local IPv4 (unused)
logger -t "$LOGTAG" "called: ext_ip=$EXT_IP ext_port=$EXT_PORT local_v6=$LOCAL_IP6 local_port=$LOCAL_PORT proto=$PROTO local_v4=$LOCAL_IP4"
# Basic checks
if [ -z "$LOCAL_PORT" ]; then
logger -t "$LOGTAG" "ERROR: LOCAL_PORT (arg4) is empty, abort"
exit 1
fi
if [ -z "$PROTO" ]; then
PROTO="tcp"
logger -t "$LOGTAG" "PROTO is empty, default to tcp"
fi
case "$PROTO" in
tcp|udp) ;;
*)
logger -t "$LOGTAG" "WARN: unsupported proto '$PROTO', force tcp"
PROTO="tcp"
;;
esac
# ==== 在添加转发规则前:调用 qBittorrent,更改监听端口为 EXT_PORT ====
if [ -z "$EXT_PORT" ]; then
logger -t "$LOGTAG" "WARN: EXT_PORT (arg2) is empty, skip qBittorrent port update"
else
SETPREF_RESP=$(
curl -v -X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'json={"listen_port":"'"$EXT_PORT"'"}' \
"http://$QBT_HOST:$QBT_PORT/api/v2/app/setPreferences"
)
logger -t "$LOGTAG" "qBittorrent setPreferences response: '$SETPREF_RESP'"
fi
# Rule name rule: NATMAP_{TARGET_PORT}_{PROTO}
RULE_NAME="NATMAP_${TARGET_PORT}_${PROTO}"
logger -t "$LOGTAG" "effective mapping: router:$LOCAL_PORT -> ${TARGET_IP}:${TARGET_PORT} ($PROTO), rule name=$RULE_NAME"
# Delete previous redirect with the same name, if any
OLD_SECTIONS="$(uci show firewall 2>/dev/null | grep "name='${RULE_NAME}'" | cut -d. -f2 | cut -d= -f1)"
if [ -n "$OLD_SECTIONS" ]; then
for SID in $OLD_SECTIONS; do
logger -t "$LOGTAG" "found existing redirect with name ${RULE_NAME}: firewall.${SID}, deleting it"
uci delete firewall."$SID"
done
uci commit firewall
logger -t "$LOGTAG" "old redirect(s) with name ${RULE_NAME} removed and firewall config committed"
else
logger -t "$LOGTAG" "no existing redirect with name ${RULE_NAME} found, nothing to delete"
fi
# Create new redirect
SID="$(uci add firewall redirect)"
if [ -z "$SID" ]; then
logger -t "$LOGTAG" "ERROR: uci add firewall redirect failed"
exit 1
fi
logger -t "$LOGTAG" "created new redirect section: firewall.${SID}"
uci set firewall."$SID".name="$RULE_NAME"
uci set firewall."$SID".src='wan'
uci set firewall."$SID".src_dport="$LOCAL_PORT"
uci set firewall."$SID".proto="$PROTO"
uci set firewall."$SID".dest='lan'
uci set firewall."$SID".dest_ip="$TARGET_IP"
uci set firewall."$SID".dest_port="$TARGET_PORT"
uci set firewall."$SID".target='DNAT'
# Log pending config for this section
uci show firewall."$SID" | logger -t "$LOGTAG"
uci commit firewall
logger -t "$LOGTAG" "firewall config committed for redirect section firewall.${SID}"
# Reload firewall
/etc/init.d/firewall reload
RC=$?
if [ $RC -ne 0 ]; then
logger -t "$LOGTAG" "ERROR: firewall reload failed, rc=$RC"
exit $RC
fi
logger -t "$LOGTAG" "firewall reload success, redirect ${RULE_NAME} should now be active"
exit 0
如何运行这个脚本?
- 运行环境:OpenWrt/ImmortalWrt(含
uci、logger、curl、/etc/init.d/firewall)。 - 通常你只需要更改脚本开头的
TARGET_IP、TARGET_PORT、QBT_HOST和QBT_PORT变量为你的实际值。 - qBittorrent 的白名单允许路由器 IP 访问无需认证的 WebUI API。
这个脚本做了什么?
- 由 NATMap 触发时接收外部端口信息;
- 调用 qBittorrent API 动态更新监听端口为映射出的 EXT_PORT;
- 通过 UCI 删除旧 firewall redirect 并新建 DNAT 规则,将
路由器:LOCAL_PORT→192.168.2.1:TARGET_PORT; - 提交并重载防火墙以立即生效。
这样,每次 NATMap 映射端口变化时,qBittorrent 的监听端口和路由器的端口转发规则都会自动更新,实现 qBittorrent 的公网可达。