一劳永逸解决端口封禁问题

引子

过往四五年,图个安全,一直供着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-passwallClash.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

加载评论