OpenWrt 上用 NATMap 动态更新端口以实现 qBittorrent 公网可达

在 OpenWrt/immortalWrt 环境下,利用 NATMap 的通知脚本自动更新 qBittorrent 的外部监听端口并通过 UCI 动态调整防火墙 DNAT 转发,实现内网运行的 qBittorrent 在公网端口随机分配情况下仍能稳定被访问。包含脚本示例、工作原理与部署注意事项。
2025-12-03
5000 字 · 约 13 分钟阅读

在刷入 OpenWrt(immortalWrt)后,桥接光猫进行宽带拨号,检测 ipv4 的 nat 环境为 full cone nat,这是比较理想的 nat 类型。

在我的内网中有一台服务器,运行着 qBittorrent 实现 PT 下载,这会涉及到端口映射的问题。

国内家宽即使是 ipv4 的 full cone nat,也并不意味着公网 ip 可达,运营商有可能会做端口限制,而且外网注册的端口也不一定是你想要的端口。

例如,使用 natmap 映射本地 8080 端口到公网端口,会发现变成一个随机端口,而不是指定的 8080 端口,这样会给 qBittorrent 的外网连接带来 2 个麻烦:

  1. 由于公网端口是随机的且定时变化的,无法手动在 qBittorrent 中指定“外部连接端口”;
  2. 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(含 uciloggercurl/etc/init.d/firewall)。
  • 通常你只需要更改脚本开头的 TARGET_IPTARGET_PORTQBT_HOSTQBT_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 的公网可达。

留言

发表留言