配置 STUN 脚本
STUN 脚本为 OpenWrt 与 WSL2 Debian 通用
脚本内容不多,但会用较大篇幅进行说明
脚本内容
touch /usr/stun-bt.sh
chmod +x /usr/stun-bt.sh
nano /usr/stun-bt.sh
touch
创建 STUN 脚本 /usr/stun-bt.sh
chmod
赋予执行权限
nano
编辑 STUN 脚本 /usr/stun-bt.sh
粘贴以下脚本内容
- OpenWrt 下不支持中文显示,可以把注释内容删除后保存
- 由于 Windows 与 Unix 的换行符不同,使用系统记事本编辑会导致错误
- 如要使用 Windows 编辑,请选择支持 Unix 格式的文本编辑器
# 以下变量需按要求填写
IFNAME= # 指定接口,可留空;仅在多 WAN 时需要;拨号接口的格式为 "pppoe-wancm"
GWLADDR=192.168.1.1 # 主路由 LAN 的 IPv4 地址
APPADDR=192.168.1.168 # 下载设备的 IPv4 地址,允许主路由或旁路由本身运行 BT 应用
APPPORT=12345 # BT 应用的监听端口,HTTP 改包要求 5 位数端口
WANADDR=$1
WANPORT=$2
LANPORT=$4
L4PROTO=$5
OWNADDR=$6
STUNIFO=/tmp/stun-bt.info
OLDPORT=$(grep $L4PROTO $STUNIFO 2>/dev/null | awk -F ':| ' '{print$3}')
RELEASE=$(grep ^ID= /etc/os-release | awk -F '=' '{print$2}' | tr -d \")
# 判断 TCP 或 UDP 的穿透是否启用
# 清理穿透信息中没有运行的协议
touch $STUNIFO
case $RELEASE in
openwrt)
for SECTION in $(uci show natmap | grep $0 | awk -F . '{print$2}'); do
if [ "$(uci -q get natmap.$SECTION.enable)" = 1 ]; then
case $(uci get natmap.$SECTION.udp_mode) in
0) SECTTCP=$SECTION ;;
1) SECTUDP=$SECTION ;;
esac
fi
done
[ $(uci -q get natmap.$SECTTCP) ] || ( \
DISPORT="$(grep tcp $STUNIFO | awk -F ':| ' '{print$3}') tcp"; sed -i '/'tcp'/d' $STUNIFO )
[ $(uci -q get natmap.$SECTUDP) ] || ( \
DISPORT="$(grep udp $STUNIFO | awk -F ':| ' '{print$3}') udp"; sed -i '/'udp'/d' $STUNIFO )
;;
*)
ps aux | grep $0 | grep "\-h" || ( \
DISPORT="$(grep tcp $STUNIFO | awk -F ':| ' '{print$3}') tcp"; sed -i '/'tcp'/d' $STUNIFO )
ps aux | grep $0 | grep "\-u" || ( \
DISPORT="$(grep udp $STUNIFO | awk -F ':| ' '{print$3}') udp"; sed -i '/'udp'/d' $STUNIFO )
;;
esac
# 更新保存穿透信息
sed -i '/'$L4PROTO'/d' $STUNIFO
echo $L4PROTO $WANADDR:$WANPORT '->' $OWNADDR:$LANPORT '->' $APPADDR:$APPPORT $(date +%s) >>$STUNIFO
# 防止脚本同时操作 nftables 导致冲突
[ $L4PROTO = udp ] && sleep 1 && \
[ $(($(date +%s) - $(grep tcp $STUNIFO | awk '{print$NF}'))) -lt 2 ] && sleep 2
# 初始化 nftables
nft add table ip STUN
nft delete chain ip STUN BTTR 2>/dev/null
nft create chain ip STUN BTTR { type filter hook postrouting priority filter \; }
WANTCP=$(grep tcp $STUNIFO | awk -F ':| ' '{print$3}')
WANUDP=$(grep udp $STUNIFO | awk -F ':| ' '{print$3}')
if [ -n "$IFNAME" ]; then
IIFNAME="iifname $IFNAME"
OIFNAME="oifname $IFNAME"
fi
# HTTP Tracker
STRTCP=$(printf 30$(printf "$WANTCP" | xxd -p) | tail -c 10)
STRUDP=$(printf 30$(printf "$WANUDP" | xxd -p) | tail -c 10)
if [ -n "$WANTCP" ] && [ -n "$WANUDP" ]; then
SETSTR="numgen inc mod 2 map { 0 : 0x3d$STRTCP, 1 : 0x3d$STRUDP }"
elif [ -n "$WANTCP" ]; then
SETSTR=0x3d$STRTCP
elif [ -n "$WANUDP" ]; then
SETSTR=0x3d$STRUDP
fi
nft add set ip STUN BTTR_HTTP "{ type ipv4_addr . inet_service; flags dynamic; timeout 1h; }"
nft add chain ip STUN BTTR_HTTP
nft flush chain ip STUN BTTR_HTTP
nft insert rule ip STUN BTTR $OIFNAME ip saddr $APPADDR ip daddr . tcp dport @BTTR_HTTP counter goto BTTR_HTTP
nft add rule ip STUN BTTR $OIFNAME ip saddr $APPADDR meta l4proto tcp @ih,0,112 0x474554202f616e6e6f756e63653f add @BTTR_HTTP { ip daddr . tcp dport } counter goto BTTR_HTTP
for OFFSET in $(seq 768 16 1040); do
nft add rule ip STUN BTTR_HTTP @ih,$OFFSET,40 0x706f72743d @ih,$(($OFFSET+32)),48 set $SETSTR update @BTTR_HTTP { ip daddr . tcp dport } counter accept
done
# UDP Tracker
if [ -n "$WANTCP" ] && [ -n "$WANUDP" ]; then
SETNUM="numgen inc mod 2 map { 0 : $WANTCP, 1 : $WANUDP }"
elif [ -n "$WANTCP" ]; then
SETNUM=$WANTCP
elif [ -n "$WANUDP" ]; then
SETNUM=$WANUDP
fi
nft add set ip STUN BTTR_UDP "{ type ipv4_addr . inet_service; flags dynamic; timeout 1h; }"
nft add chain ip STUN BTTR_UDP
nft flush chain ip STUN BTTR_UDP
nft insert rule ip STUN BTTR $OIFNAME ip saddr $APPADDR ip daddr . udp dport @BTTR_UDP counter goto BTTR_UDP
nft add rule ip STUN BTTR $OIFNAME ip saddr $APPADDR meta l4proto udp @ih,0,64 0x41727101980 @ih,64,32 0 add @BTTR_UDP { ip daddr . udp dport } counter goto BTTR_UDP
nft add rule ip STUN BTTR_UDP @ih,64,32 1 @ih,768,16 $APPPORT @ih,768,16 set $SETNUM update @BTTR_UDP { ip daddr . udp dport } counter
# 判断脚本运行的环境,选择 DNAT 方式
# 先排除需要 UPnP 的情况
DNAT=0
for LANADDR in $(ip -4 a show dev br-lan | grep inet | awk '{print$2}' | awk -F '/' '{print$1}'); do
[ "$LANADDR" = $GWLADDR ] && DNAT=1
done
for LANADDR in $(nslookup -type=A $HOSTNAME | grep Address | grep -v :53 | awk '{print$2}'); do
[ "$LANADDR" = $GWLADDR ] && DNAT=1
done
[ $APPADDR = $GWLADDR ] && DNAT=2
# 若未排除,则尝试直连 UPnP
if [ $DNAT = 0 ]; then
[ -n "$OLDPORT" ] && upnpc -i -d $OLDPORT $L4PROTO
[ -n "$DISPORT" ] && upnpc -i -d $DISPORT
upnpc -i -e "STUN BT $L4PROTO $WANPORT->$LANPORT->$APPPORT" -a $APPADDR $APPPORT $LANPORT $L4PROTO | \
grep $APPADDR | grep $APPPORT | grep $LANPORT | grep -v failed
[ $? = 0 ] && DNAT=3
fi
# 直连失败,则尝试代理 UPnP
if [ $DNAT = 0 ]; then
PROXYCONF=/tmp/proxychains.conf
echo [ProxyList] >$PROXYCONF
echo http $APPADDR 3128 >>$PROXYCONF
[ -n "$OLDPORT" ] && proxychains -f $PROXYCONF upnpc -i -d $OLDPORT $L4PROTO
[ -n "$DISPORT" ] && proxychains -f $PROXYCONF upnpc -i -d $DISPORT
proxychains -f $PROXYCONF \
upnpc -i -e "STUN BT $L4PROTO $WANPORT->$LANPORT->$APPPORT" -a $APPADDR $APPPORT $LANPORT $L4PROTO | \
grep $APPADDR | grep $APPPORT | grep $LANPORT | grep -v failed
[ $? = 0 ] && DNAT=3
fi
# 代理失败,则启用本机 UPnP
[ $DNAT = 0 ] && (upnpc -i -e "STUN BT $L4PROTO $WANPORT->$LANPORT" -a @ $LANPORT $LANPORT $L4PROTO; DNAT=4)
# 初始化 DNAT 链
if [ $DNAT != 3 ]; then
[ -z "$WANTCP" ] && nft delete chain ip STUN BTDNAT_tcp 2>/dev/null
[ -z "$WANUDP" ] && nft delete chain ip STUN BTDNAT_udp 2>/dev/null
nft delete chain ip STUN BTDNAT_$L4PROTO 2>/dev/null
nft create chain ip STUN BTDNAT_$L4PROTO { type nat hook prerouting priority dstnat \; }
fi
# BT 应用运行在路由器下,使用 dnat
if [ $DNAT = 1 ] || [ $DNAT = 4 ]; then
nft add rule ip STUN BTDNAT_$L4PROTO $IIFNAME $L4PROTO dport $LANPORT counter dnat ip to $APPADDR:$APPPORT
[ "$RELEASE" = "openwrt" ] && \
if ! nft list chain inet fw4 forward | grep 'ct status dnat' >/dev/null; then
HANDLE=$(nft -a list chain inet fw4 forward | grep jump | awk 'NR==1{print$NF}')
nft insert rule inet fw4 forward handle $HANDLE ct status dnat counter accept
fi
fi
# BT 应用运行在路由器上,使用 redirect
if [ $DNAT = 2 ]; then
nft add rule ip STUN BTDNAT_$L4PROTO $IIFNAME $L4PROTO dport $LANPORT counter redirect to :$APPPORT
[ "$RELEASE" = "openwrt" ] && \
if ! nft list chain inet fw4 input | grep 'ct status dnat' >/dev/null; then
HANDLE=$(nft -a list chain inet fw4 input | grep jump | grep -v "tcp flags" | awk 'NR==1{print$NF}')
nft insert rule inet fw4 input handle $HANDLE ct status dnat counter accept
fi
fi
nano
下,按 Ctrl + O 键保存,Enter 键确认,Ctrl + X 键退出编辑
chmod +x /usr/stun-bt.sh
保存脚本后,运行 chmod
赋予执行权限
我在测试的时候就经常忘了这一步
以下是脚本内容的详细说明
初始化
IFNAME=
GWLADDR=192.168.1.1
APPADDR=192.168.1.168
APPPORT=12345
需要填写的基本变量
IFNAME
指定接口,可留空;仅在多 WAN 时需要;拨号接口的格式为 "pppoe-wancm"
GWLADDR
代表主路由 LAN IPv4 地址
APPADDR
代表下载设备的 IPv4 地址,允许主路由或旁路由本身运行 BT 应用
APPPORT
代表下载设备上运行的 BT 应用的监听端口,要求 5 位数端口(10000 - 65535
)
以上 4 个变量需要用户按照实际情况修改
WANADDR=$1
WANPORT=$2
LANPORT=$4
L4PROTO=$5
OWNADDR=$6
STUNIFO=/tmp/stun-bt.info
OLDPORT=$(grep $L4PROTO $STUNIFO 2>/dev/null | awk -F ':| ' '{print$3}')
RELEASE=$(grep ^ID= /etc/os-release | awk -F '=' '{print$2}' | tr -d \")
无需填写的基本变量
WANADDR
为穿透后的公网地址
WANPORT
为穿透后的公网端口
LANPORT
为穿透前的内网端口
L4PROTO
为穿透的传输层协议,TCP 或 UDP
OWNADDR
为穿透时的内网源地址
以上 5 个变量由 STUN 工具传递参数获得,使用 NATMap 以外的方案时,请注意参数顺序
STUNIFO
指定穿透信息保存的位置,默认为 /tmp/stun-bt.info
OLDPORT
从 /tmp/stun-bt.info
中读取上次穿透时的内网端口,仅用作清理 UPnP 旧规则
RELEASE
识别当前 Linux 发行版的 ID
touch $STUNIFO
case $RELEASE in
openwrt)
for SECTION in $(uci show natmap | grep $0 | awk -F . '{print$2}'); do
if [ "$(uci -q get natmap.$SECTION.enable)" = 1 ]; then
case $(uci get natmap.$SECTION.udp_mode) in
0) SECTTCP=$SECTION ;;
1) SECTUDP=$SECTION ;;
esac
fi
done
[ $(uci -q get natmap.$SECTTCP) ] || ( \
DISPORT="$(grep tcp $STUNIFO | awk -F ':| ' '{print$3}') tcp"; sed -i '/'tcp'/d' $STUNIFO )
[ $(uci -q get natmap.$SECTUDP) ] || ( \
DISPORT="$(grep udp $STUNIFO | awk -F ':| ' '{print$3}') udp"; sed -i '/'udp'/d' $STUNIFO )
;;
*)
ps aux | grep $0 | grep "\-h" || ( \
DISPORT="$(grep tcp $STUNIFO | awk -F ':| ' '{print$3}') tcp"; sed -i '/'tcp'/d' $STUNIFO )
ps aux | grep $0 | grep "\-u" || ( \
DISPORT="$(grep udp $STUNIFO | awk -F ':| ' '{print$3}') udp"; sed -i '/'udp'/d' $STUNIFO )
;;
esac
判断 TCP 或 UDP 的穿透是否启用,清理穿透信息中没有运行的协议端口
当前发行版 ID 为 openwrt
时,通过 uci
命令查看 NATMap 的配置文件
其他 Linux 发行版下,一律通过 ps aux
查看调用本脚本的进程来判断
若需要其他判断方式,请自行添加 case
OpenWrt 下,先 for
循环列出带有本通知脚本的配置段落(section)序号
若该序号的配置段落已启用,则通过 udp_mode
的选项来判断是 TCP 或 UDP,并分别把段落序号赋值到变量中,最后判断段落是否存在,记录废弃端口并清理穿透信息
以上判断式允许 OpenWrt 的 NATMap 配置使用本脚本的 TCP 及 UDP 穿透条目各一个,否则将以最后触发的一个为准
若需要为多个 BT 应用进行配置,请查看本文最后
其他 Linux 发行版下,通过 NATMap 的运行参数进行判断
-h
表示 TCP,-u
表示 UDP,分别判断并清理
注意 NATMap 允许在 UDP 模式下启用 -h
参数,虽无意义但可以执行
自行配置 NATMap 时,请考虑与脚本的耦合性
无论 OpenWrt 还是 WSL2,都以当前脚本路径作为首个判断依据
若需要同时为多个 BT 应用进行配置,请区分 STUN 脚本的保存路径,具体看文末
sed -i '/'$L4PROTO'/d' $STUNIFO
echo $L4PROTO $WANADDR:$WANPORT '->' $OWNADDR:$LANPORT '->' $APPADDR:$APPPORT $(date +%s) >>$STUNIFO
更新保存穿透信息
sed
替换上次的穿透信息,与当前协议对应
echo
保存本次的穿透信息
[ $L4PROTO = udp ] && sleep 1 && \
[ $(($(date +%s) - $(grep tcp $STUNIFO | awk '{print$NF}'))) -lt 2 ] && sleep 2
防止脚本同时操作 nftables 导致冲突
若当前脚本对应的协议为 UDP,则等待 1
秒后,若当前时间与 TCP 信息中的时间戳相差小于 2
秒,则继续等待 2
秒
也就是说,TCP 脚本不延后,而 UDP 脚本则始终延后 1-3 秒
若你发现 nftables 规则异常,可把等待时间调大
nft add table ip STUN
nft delete chain ip STUN BTTR 2>/dev/null
nft create chain ip STUN BTTR { type filter hook postrouting priority filter \; }
WANTCP=$(grep tcp $STUNIFO | awk -F ':| ' '{print$3}')
WANUDP=$(grep udp $STUNIFO | awk -F ':| ' '{print$3}')
if [ -n "$IFNAME" ]; then
IIFNAME="iifname $IFNAME"
OIFNAME="oifname $IFNAME"
fi
初始化 nftables
nft add table
在 IPv4 族
中添加名为 STUN
的规则表,重复添加不作改变
如未来有其他利用到 nftables 的 STUN 场景,可能会共用同一个表
nft delete chain
在创建规则链前,先删除原有链
nft create chain
在 IPv4 族 / STUN 规则表
下创建 BTTR 链
nftables 采用 族 / 表 / 链
的三层结构,具体规则收纳在规则链下
type filter
表示对所有包进行匹配(相对于 type nat
只对每个连接的首包匹配)
hook postrouting
表示在路由选择后拦截数据包
priority filter
表示在同一拦截点下,该规则链的优先级
该链由 HTTP 与 UDP,以及未来可能支持的其他 Tracker 公用
通过网关的流量将在此进行匹配,如识别为支持的 Tracker 汇报包,则引导其前往(goto
)修改端口信息的专用规则链中
从穿透信息中分别加载 TCP 与 UDP 的公网端口,赋值到 WANTCP
与 WANUDP
Tracker 规则中,需要对端口的表达进行加工
if
判断是否指定了接口,如有则分别为 IIFNAME
与 OIFNAME
赋值
HTTP Tracker
STRTCP=$(printf 30$(printf "$WANTCP" | xxd -p) | tail -c 10)
STRUDP=$(printf 30$(printf "$WANUDP" | xxd -p) | tail -c 10)
对端口的表达进行加工
向 HTTP Tracker 汇报端口时,在 GET
请求中传递 "port=<端口>"
的查询参数(Query)
nftables 支持二进制形式的数据包匹配和修改,而 HTTP 中的查询参数为 ASCII 编码的字符,因此需要进行转换
由于 nftables 不支持修改数据包长度,而汇报端口可能是 1025 - 65535
(1024
以下为知名端口),字符长度不同会导致数据包长度发生变化
为此,本脚本要求用户把BT应用的监听端口设置为 10000 - 65535
,使得软件汇报时,将始终发送保持 5 位数的端口长度的数据包
而当穿透的公网端口为 4 位数时,将在前面补上一个 0 后再修改(如 "port=06889"
),Tracker 可正常识别
printf
作为字符打印 TCP 与 UDP 端口,xxd
将其转换为十六进制形式,并始终在前面加上 30
(即 ASCII 中的 "0"
),最后 tail
截取尾部 10
位,即 5 个 ASCII 字
当字符为 06889
时,截取 06889
;当字符为 016889
时,截取 16889
使用 printf
不换行打印,是为了避免换行符会被 xxd
转换
if [ -n "$WANTCP" ] && [ -n "$WANUDP" ]; then
SETSTR="numgen inc mod 2 map { 0 : 0x3d$STRTCP, 1 : 0x3d$STRUDP }"
elif [ -n "$WANTCP" ]; then
SETSTR=0x3d$STRTCP
elif [ -n "$WANUDP" ]; then
SETSTR=0x3d$STRUDP
fi
if
判断当前已穿透端口是 TCP 、UDP 还是 BOTH (TCP + UDP)
当穿透 BOTH 时,TCP 与 UDP 轮流修改,否则将单一修改
numgen
表示生成数字(number generator),inc
表示递增(increase),mod
表示求余(modulo),除数为 2
当生成数字 11
时,11 mod 2 余数为 1
;下次生成数字 12
,12 mod 2 余数为 0
map
映射集指定:当余数 0
时修改为 TCP 端口号,余数 1
时修改为 UDP 端口号
0x
表示十六进制表达,3d
为 ASCII 字符等号 "="
nftables 的二进制操作以 16 bit 单位进行,当修改范围非 16 的倍数时,需要或 (OR) 运算
但 map
映射集里不允许进行运算,因此把前面字符 "port="
的等号补上来凑数
nft add set ip STUN BTTR_HTTP "{ type ipv4_addr . inet_service; flags dynamic; timeout 1h; }"
nft add chain ip STUN BTTR_HTTP
nft flush chain ip STUN BTTR_HTTP
nft add set
添加一个集,用来保存已识别的 HTTP Tracker 的 IP 地址与端口
type ipv4_addr . inet_service
表示保存类型为 IPv4地址:端口
flags dynamic
表示动态集;集在创建后,通过规则来添加,更新或删除元素
timeout 1h
表示每个元素在 1
小时后超时
nft add chain
在 IPv4
族 / STUN
表下添加 BTTR_HTTP
链,重复添加不作改变
nft flush chain
在修改规则前,先把链清空
nft insert rule ip STUN BTTR ip daddr . tcp dport @BTTR_HTTP counter goto BTTR_HTTP
nft add rule ip STUN BTTR ip saddr $APPADDR meta l4proto tcp @ih,0,112 0x474554202f616e6e6f756e63653f add @BTTR_HTTP { ip daddr . tcp dport } counter goto BTTR_HTTP
nft insert rule
插入规则到 ip STUN BTTR
链的顶端
nft add rule
添加规则到 ip STUN BTTR
链的底端
出站接口为 $OIFNAME
(若有)的 IPv4 TCP 数据包,若其目标地址与目标端口与 @BTTR_HTTP
集中的记录匹配,则直接前往 HTTP 改包专用链;没有匹配的数据包将进入下一条规则
出站接口为 $OIFNAME
(若有),IPv4 源地址为下载设备,传输协议为 TCP,0
到 112
位荷载为 0x474554202f616e6e6f756e63653f
的数据包,将记录其目标地址与目标端口到 @BTTR_HTTP
集中,计数后前往 HTTP 改包专用链
该荷载表示 "GET /announce?"
,即 HTTP Tracker 汇报信息的头部
for OFFSET in $(seq 768 16 1040); do
nft add rule ip STUN BTTR_HTTP @ih,$OFFSET,40 0x706f72743d @ih,$(($OFFSET+32)),48 set $SETSTR update @BTTR_HTTP { ip daddr . tcp dport } counter accept
done
BT 应用向 HTTP Tracker 传递的前 3 个查询参数为 info_hash
, peer_id
, port
每款 BT 软件的参数顺序都不同,但很幸运的是这 3 个的位置是固定的
由于 ASCII 转码的关系,前两个参数的比特长度并不固定,但有一定的规律
经过不同软件的抓包发现,port
参数的起点范围为 768
到 1040
位,间隔 16
位
也就是说,只需要 (1040-768)/16+1=18 条规则即可覆盖
使用 for
循环添加 HTTP 改包规则,由于 OpenWrt 不支持三表达式,这里用 seq
替代
指定 OFFSET
从 768
到 1040
,步进值 16
从 $OFFSET
的位置起匹配 40
位,内容为 0x706f72743d
,即 "port="
从 $OFFSET
的位置起后移 32
位,修改为 $SETSTR
,即 "=12345"
(参照本节开头)
改包后更新目标地址与目标端口到 @BTTR_HTTP
,计数并离开该链,无需再匹配后续规则
UDP Tracker
最初为了通用性,Tracker 使用了最流行的 HTTP 协议来传输
在 BT 下载流行后,诞生了更为轻量高效的 UDP Tracker
UDP 规则基本结构与 HTTP 规则一致,且更简洁
if [ -n "$WANTCP" ] && [ -n "$WANUDP" ]; then
SETNUM="numgen inc mod 2 map { 0 : $WANTCP, 1 : $WANUDP }"
elif [ -n "$WANTCP" ]; then
SETNUM=$WANTCP
elif [ -n "$WANUDP" ]; then
SETNUM=$WANUDP
fi
if
判断式与 HTTP Tracker 相似
唯一不同的是,UDP Tracker 用二进制而不是 ASCII 表达端口,因此无需转换
nft add set ip STUN BTTR_UDP "{ type ipv4_addr . inet_service; flags dynamic; timeout 1h; }"
nft add chain ip STUN BTTR_UDP
nft flush chain ip STUN BTTR_UDP
nft add set
添加一个集,用来保存已识别的 UDP Tracker 的 IP 地址与端口
集的属性与 HTTP Tracker 相同
nft add chain
在 IPv4
族 / STUN
表下添加 BTTR_UDP
链,重复添加不作改变
nft flush chain
在修改规则前,先把链清空
nft insert rule ip STUN BTTR $OIFNAME ip saddr $APPADDR ip daddr . udp dport @BTTR_UDP counter goto BTTR_UDP
nft add rule ip STUN BTTR $OIFNAME ip saddr $APPADDR meta l4proto udp @ih,0,64 0x41727101980 @ih,64,32 0 add @BTTR_UDP { ip daddr . udp dport } counter goto BTTR_UDP
nft insert rule
插入规则到 ip STUN BTTR
链的顶端
nft add rule
添加规则到 ip STUN BTTR
链的底端
出站接口为 $OIFNAME
(若有)的 IPv4 UDP 数据包,若其目标地址与目标端口与 @BTTR_UDP
集中的记录匹配,则直接前往 UDP 改包专用链;没有匹配的数据包将进入下一条规则
出站接口为 $OIFNAME
(若有),IPv4 源地址为下载设备,传输协议为 UDP,0
到 64
位荷载为 0x41727101980
,64
到 96
(64 + 32
)位荷载为 0
的数据包,将记录其目标地址与目标端口到 @BTTR_UDP
集中,计数后前往 UDP 改包专用链
0x41727101980
为与 UDP Tracker 建立请求时的固定连接 ID(幻方常数),后续的 0
代表连接(connect
)请求
脚本中把这两个数据区分开来,但它们的偏移量是相连的
添加规则后,nftables 会自动把它转换成 @ih,0,96 0x4172710198000000000
nft add rule ip STUN BTTR_UDP @ih,64,32 1 @ih,768,16 $APPPORT @ih,768,16 set $SETNUM update @BTTR_UDP { ip daddr . udp dport } counter
由于 BEP0015 为 UDP Tracker 规范了每个数据的偏移量,因此只需要一条规则
从偏移量 64
起匹配 32
位,数据为 1
,代表通告(announce
)请求
从偏移量 768
起匹配 16
位,数据为 BT 应用的监听端口号 $APPPORT
从偏移量 768
起修改 16
位,数据为 $SETNUM
,自动替换 TCP 或 UDP 穿透端口
改包后更新目标地址与目标端口到 @BTTR_UDP
并计数
本规则没有离开链(accept
)的操作,因为已是该链唯一的一条规则
DNAT
需要根据运行的环境,选择 DNAT 的实现方式
DNAT=0
for LANADDR in $(ip -4 a show dev br-lan | grep inet | awk '{print$2}' | awk -F '/' '{print$1}'); do
[ $DNAT = 1 ] && break
[ "$LANADDR" = $GWLADDR ] && DNAT=1
done
for LANADDR in $(nslookup -type=A $HOSTNAME | grep Address | grep -v :53 | awk '{print$2}'); do
[ $DNAT = 1 ] && break
[ "$LANADDR" = $GWLADDR ] && DNAT=1
done
[ $APPADDR = $GWLADDR ] && DNAT=2
先设置标记 DNAT
为 0
两个 for
循环分别用 ip
与 nslookup
命令判断两轮
当本机 IP $LANADDR
与主路由 IP $GWLADDR
一致时,则表示 NATMap 运行在主路由上,设置标记 DNAT
为 1
,若已设置则离开 for
循环
当下载设备的 IP $APPADDR
与主路由 IP $GWLADDR
一致时,则表示 NATMap 与 BT 应用均运行在主路由上,设置标记 DNAT
为 2
if [ $DNAT = 0 ]; then
[ -n "$OLDPORT" ] && upnpc -i -d $OLDPORT $L4PROTO
[ -n "$DISPORT" ] && upnpc -i -d $DISPORT
upnpc -i -e "STUN BT $L4PROTO $WANPORT->$LANPORT->$APPPORT" -a $APPADDR $APPPORT $LANPORT $L4PROTO | \
grep $APPADDR | grep $APPPORT | grep $LANPORT | grep -v failed
[ $? = 0 ] && DNAT=3
fi
若标记 $DNAT
为 0
,则表示 NATMap 运行在旁路由上,通过 UPnP 请求映射规则
若存在之前的穿透端口 $OLDPORT
或 $DISPORT
,则进行清理
upnpc
请求一个带有描述的映射规则,包含传输协议,公网端口 → 内网端口 → 应用端口
映射规则为内网端口到下载设备的监听端口,公网端口的映射在运营商的 NAT 网关上完成
grep
查找输出内容,若判断添加规则成功,则设置标记 DNAT
为 3
if [ $DNAT = 0 ]; then
PROXYCONF=/tmp/proxychains.conf
echo [ProxyList] >$PROXYCONF
echo http $APPADDR 3128 >>$PROXYCONF
[ -n "$OLDPORT" ] && proxychains -f $PROXYCONF upnpc -i -d $OLDPORT $L4PROTO
[ -n "$DISPORT" ] && proxychains -f $PROXYCONF upnpc -i -d $DISPORT
proxychains -f $PROXYCONF \
upnpc -i -e "STUN BT $L4PROTO $WANPORT->$LANPORT->$APPPORT" -a $APPADDR $APPPORT $LANPORT $L4PROTO | \
grep $APPADDR | grep $APPPORT | grep $LANPORT | grep -v failed
[ $? = 0 ] && DNAT=3
fi
若标记 $DNAT
为 0
,则表示 UPnP 直连请求添加规则失败
PROXYCONF
指定 proxychains
的配置文件 /tmp/proxychains.conf
echo
写入代理协议、服务器地址、端口至配置文件 /tmp/proxychains.conf
注意协议和端口,3proxy 的 proxy 代理服务器为 http
协议,默认端口为 3128
之后通过 proxychains
重复之前的操作
若主路由的 UPnP 开启了安全模式,则只允许客户端请求映射到自身地址的规则
使用旁路由时,为了减少 NAT 转发,理想情况是让主路由直接把远程传入的连接发到下载设备的监听端口上
为了自动化,该操作将由旁路由来完成,这会导致请求地址与目标地址不一致的情况
为了解决这个问题,本文采用的代理服务器的方法,经由下载设备发起 UPnP 请求
[ $DNAT = 0 ] && (upnpc -i -e "STUN BT $L4PROTO $WANPORT->$LANPORT" -a @ $LANPORT $LANPORT $L4PROTO; DNAT=4)
若标记 $DNAT
为 0
,则表示 UPnP 代理请求添加规则失败
upnpc
向主路由请求目标地址为自身的映射规则,之后再进行二次映射到下载设备
@
为本机地址,映射到本机内网端口 $LANPORT
,而非直达下载设备的 $APPPORT
最后设置标记 DNAT
为 4
if [ $DNAT != 3 ]; then
[ -z "$WANTCP" ] && nft delete chain ip STUN BTDNAT_tcp 2>/dev/null
[ -z "$WANUDP" ] && nft delete chain ip STUN BTDNAT_udp 2>/dev/null
nft delete chain ip STUN BTDNAT_$L4PROTO 2>/dev/null
nft create chain ip STUN BTDNAT_$L4PROTO { type nat hook prerouting priority dstnat \; }
fi
配置 nftables 的 DNAT 规则前,先初始化 DNAT 链
若标记 $DNAT
不为 3
,则表示需要使用 nftables 进行端口映射
这包括 NATMap 运行在主路由上,以及 NATMap 运行在旁路由上,但无法请求直达规则时
判断是否存在 TCP 或 UDP 端口,不存在则删除对应协议的规则链
nft delete chain
在创建规则链前,先删除原有链
nft create chain
在 IPv4
族 / STUN
表下创建 BTDNAT_tcp
或 BTDNAT_udp
链
type nat
表示对每个连接的首包进行匹配,后续数据包直接从连接跟踪表参照 NAT 信息
hook prerouting
表示在路由前拦截数据包
priority dstnat
表示在同一拦截点下,该规则链的优先级
该链的属性为 DNAT 的标准,独立创建只为方便管理
if [ $DNAT = 1 ] || [ $DNAT = 4 ]; then
nft add rule ip STUN BTDNAT_$L4PROTO $IIFNAME $L4PROTO dport $LANPORT counter dnat ip to $APPADDR:$APPPORT
[ "$RELEASE" = "openwrt" ] && \
if ! nft list chain inet fw4 forward | grep 'ct status dnat' >/dev/null; then
HANDLE=$(nft -a list chain inet fw4 forward | grep jump | awk 'NR==1{print$NF}')
nft insert rule inet fw4 forward handle $HANDLE ct status dnat counter accept
fi
fi
若标记 $DNAT
为 1
或 4
,则表示 NATMap 运行在主路由上,或 NATMap 运行在旁路由上,但无法请求直达规则,使用 dnat
进行端口映射
nft add rule
添加规则到 ip STUN BTDNAT_tcp
或 BTDNAT_udp
链
入站接口为 $IIFNAME
(若有)的 TCP 或 UDP 数据包,若其目标端口为内网端口 $LANPORT
,则转发至下载设备的 BT 应用 $APPADDR:$APPPORT
上
映射规则为内网端口到下载设备的监听端口,公网端口的映射在运营商的 NAT 网关上完成
若本机为 OpenWrt,由于默认规则不允许转发,需要另外放行
若系统链 inet fw4 forward
下不存在 ct status dnat
的规则,则在跳转到默认链前使其放行
nft -a list
定位到默认的跳转规则
nft insert rule
在默认的跳转规则前插入放行规则
if [ $DNAT = 2 ]; then
nft add rule ip STUN BTDNAT_$L4PROTO $IIFNAME $L4PROTO dport $LANPORT counter redirect to :$APPPORT
[ "$RELEASE" = "openwrt" ] && \
if ! nft list chain inet fw4 input | grep 'ct status dnat' >/dev/null; then
HANDLE=$(nft -a list chain inet fw4 input | grep jump | grep -v "tcp flags" | awk 'NR==1{print$NF}')
nft insert rule inet fw4 input handle $HANDLE ct status dnat counter accept
fi
fi
若标记 $DNAT
为 2
,则表示 NATMap 与 BT 应用均运行在主路由上,使用 redirect
nft add rule
添加规则到 ip STUN BTDNAT_tcp
或 **BTDNAT_udp链 入站接口为
$IIFNAME(若有)的 TCP 或 UDP 数据包,若其目标端口为内网端口
$LANPORT,重定向至本机的 BT 应用监听端口
$APPPORT`
映射规则为内网端口到下载设备的监听端口,公网端口的映射在运营商的 NAT 网关上完成
若本机为 OpenWrt,由于默认规则不允许入站,需要另外放行
若系统链 inet fw4 input
下不存在 ct status dnat
的规则,则在跳转到默认链前使其放行
nft -a list
定位到默认的跳转规则
nft insert rule
在默认的跳转规则前插入放行规则
最后
chmod +x /usr/stun-bt.sh
请记得给脚本赋予执行权限