引子
过往四五年,图个安全,一直供着VPS自建科学上网,使用Nginx做前端、博客伪装的Vmess协议,很长时间都没有遇到被墙的问题。直到去年夏天,换用了晚高峰够用且便宜许多的白丝云,把协议换成了更轻量的Vless,短短半年时间内遇到了两次443端口封禁,是线路问题还是协议问题网上众说纷纭,遇上了只能手动换端口解决,虽然操作并不复杂,但多少有些闹心。
最近折腾软路由时,读到一篇NAT转发的文章,延伸出了一个通过将大量端口转发至服务端口规避443被封禁手动更改的方法,配合订阅自动更新,遭遇端口封禁时,只需要在客户端点一下更新订阅就可以了。
后面朋友提醒,V2ray自带的任意门(Dokodemo-door)也可以实现类似功能,不过我使用的v2fly版本似乎只能指定单端口的转发。
端口转发
服务器端机器需要安装两个工具,iptables是Linux下的Netfilter控制器,而iptables-persistent则负责把iptables的临时配置持久化,首先检查服务器是否已安装iptables。
1
apt install iptables
目前我使用了trojan和hysteria两种协议进行科学上网,分别使用tcp和udp协议,需要分别映射两个端口。
hysteria是一个依靠QUIC开发的网络加速工具,实测下来延迟相较常见的tcp协议要低上许多,晚高峰频繁丢包时体验也好上不少,我已裸奔四个月未被封禁,是我目前最喜欢的翻墙协议。其缺点在于目前支持的客户端较少,经验证openwrt-passwall和Clash.Meta都是可用的。
1
2
3
4
# 映射tcp端口
iptables -t nat -A PREROUTING -p tcp --dport 40000:50000 -j REDIRECT --to-ports 443
# 映射udp端口
iptables -t nat -A PREROUTING -p udp --dport 50000:60000 -j REDIRECT --to-ports 10000
将客户端协议端口改为指定端口段内的任意端口,测试没有问题后,安装iptables-persistent保存配置,服务器重启时规则也能生效。
1
apt install iptables-persistent
订阅更新
尽管我们开启了一吨端口可供使用,但客户端只会使用指定的一个进行访问,我们需要利用客户端的订阅机制写一个脚本,每次访问时随机为客户端提供一个开放段内的端口,实现端口封禁时的快速更换。
这里我们随便用Go来撸一个用。不要吐槽脚本啰嗦,是ChatGPT写的呀~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main
import (
"encoding/base64"
"math/rand"
"net/http"
"strconv"
"strings"
)
type Config struct {
Name string
UUID string
Domain string
TcpStart int
TcpEnd int
UdpStart int
UdpEnd int
}
var c Config = Config{
Name: "name",
UUID: "uuid",
Domain: "domain",
TcpStart: 40000,
TcpEnd: 50000,
UdpStart: 50000,
UdpEnd: 60000,
}
var subTemplateString = `vless://{uuid}@{domain}:{tcpPort}?security=tls&encryption=none&host={domain}&headerType=none&type=tcp#{name}-vless
trojan://{uuid}@{domain}:{tcpPort}?peer={domain}&sni={domain}&alpn=http/1.1#{name}-trojan
hysteria://{domain}:{udpPort}?protocol=udp&auth={uuid}&peer={domain}&insecure=0&alpn=h3&upmbps=50&downmbps=200#{name}-hysteria`
var clashTemplateString = `
Your Clash conf file, Replace config info with {uuid}, {domain}, etc.
`
func getSublist(w http.ResponseWriter, r *http.Request) {
s := subTemplateString
s = strings.Replace(s, "{uuid}", c.UUID, -1)
s = strings.Replace(s, "{domain}", c.Domain, -1)
s = strings.Replace(s, "{name}", c.Name, -1)
s = strings.Replace(s, "{tcpPort}", strconv.Itoa(rand.Intn(c.TcpEnd-c.TcpStart)+c.TcpStart), -1)
s = strings.Replace(s, "{udpPort}", strconv.Itoa(rand.Intn(c.UdpEnd-c.UdpStart)+c.UdpStart), -1)
e := base64.StdEncoding.EncodeToString([]byte(s))
w.Write([]byte(e))
}
func getClash(w http.ResponseWriter, r *http.Request) {
s := clashTemplateString
s = strings.Replace(s, "{uuid}", c.UUID, -1)
s = strings.Replace(s, "{domain}", c.Domain, -1)
s = strings.Replace(s, "{name}", c.Name, -1)
s = strings.Replace(s, "{tcpPort}", strconv.Itoa(rand.Intn(c.TcpEnd-c.TcpStart)+c.TcpStart), -1)
s = strings.Replace(s, "{udpPort}", strconv.Itoa(rand.Intn(c.UdpEnd-c.UdpStart)+c.UdpStart), -1)
w.Write([]byte(s))
}
func main() {
http.HandleFunc("/"+c.UUID+"/sub", getSublist)
http.HandleFunc("/"+c.UUID+"/clash", getClash)
if err := http.ListenAndServe(":6666", nil); err != nil {
panic(err)
}
}
编译成可执行文件,并且设置自启动,访问对应的地址就能看到订阅信息了,/sub中为v2rayN、passwall等客户端可用的分享链接订阅,/clash中为带规则的Clash配置,如127.0.0.1/1a5be4ec-7ce1-4b34-884b-2e0075f68214/sub。