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

引子

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