使用V2ray反向代理实现内网穿透
Overview
1. 前言
笔者在很久之前感叹过:在IPv6成为基础设施标配前,NAT依旧会是常态,这个常态已经持续几十年了。没想到现在的IPv6带着NAT一起来,内网穿透的玩法也更多了。上周wireguard突然失效,笔者以为是哪里配置出了问题,仔细排查后发现,国内访问VPS的UDP端口已经被封。
笔者的wireguard网络已经稳定运行了很长时间,但最后还是逃不过屏蔽,在国内的环境下,只有合规的流量才能大隐隐于网。笔者在对比了几个可用方案:OpenVPN、frp、inlets和v2ray之后,选择相对熟悉的v2ray来实现内网穿透,这里稍作记录。
2. 内网穿透架构
这里我们不会考虑对NAT动手脚,而是选择使用一个带公网IP的中心节点完成流量转发,以笔者的三个设备为例:
- 设备A:MacBook Air,放置在公司内
- 设备B:MacBook Pro,放置在家里
- 设备C:VPS,托管在云厂商
如果需要在公司访问家里的设备,并执行一些ssh登录,vscode远程开发等,需要设备B主动连接设备C,建立一个传输隧道,当设备A需要访问家里的服务时,请求经过设备C转发到达设备B:
Macbook Air] B[设备B
MacBook Pro] C[设备C
VPS] A--->|公网访问|C B--->|公网隧道|C C--->|公网隧道|B
这个结构在使用wireguard与v2ray时都是一致的,区别在于:
- wireguard使用无连接状态的UDP构建三层网络,在逻辑上所有设备都处于一个子网下,设备之间使用内网IP互相访问
- v2ray使用有连接状态的TCP构建隧道,利用流量探测和路由规则在应用层实现内网穿透,设备之间需要经过代理才能互相访问
v2ray相比wireguard会有更多的开销,但好处在于v2ray属于应用层实现,只要在Go语言支持的OS和硬件下都可以运行,而且WebSocket能受益于TLS加密和CDN加速。
3. v2ray配置
v2ray定义了reverse对象来设置关于反向代理的参数。以Air访问Pro为例:
- 我们使用vless作为传输协议,ws+tls作为传输方式
- B与C之间通过 pro.v2ray.com 域名标记隧道流量
- 子路径 ProTunnel 用于B与C之间建立传输隧道
- 子路径 ProForward 供A连接C来使用B的网络环境
Macbook Air] B[设备B
MacBook Pro] C[设备C
VPS] A--->|test.example.com/ProForward|C B--->|pro.v2ray.com + test.example.com/ProTunnel|C C--->|pro.v2ray.com + test.example.com/ProTunnel|B
并且为了更好利用WebSocket特性,减少端口暴露,在C上部署了nginx作为前端,处理TLS流量,并将访问子路径的请求转发到对应v2ray服务,如下所示:
Macbook Air] B[设备B
MacBook Pro] A--->|https://test.example.com/ProForward|C B--->|https://test.example.com/ProTunnel|C subgraph 设备C: VPS C[nginx] D[v2ray] D1[http://127.0.0.1:9090/ProForward] D2[http://127.0.0.1:9091/ProTunnel] C--->D D--->D1 D--->D2 end
设备A、B、C的V2ray配置文件如下:
设备A
这里只是简单将输入端的 pro-forward 路由到输出端的 pro-forward。
1{
2 "log": {
3 "loglevel": "debug"
4 },
5 "inbounds": [
6 {
7 "tag": "pro-forward",
8 "port": 1081,
9 "listen": "127.0.0.1",
10 "protocol": "socks",
11 "settings": {
12 "udp": true
13 }
14 }
15 ],
16 "outbounds": [
17 {
18 "tag": "pro-forward",
19 "protocol": "vless",
20 "settings": {
21 "vnext": [
22 {
23 "address": "test.example.com",
24 "port": 443,
25 "users": [
26 {
27 "id": "897244b5-0170-44a3-9f76-672fa6734c9b",
28 "encryption": "none",
29 "level": 0
30 }
31 ]
32 }
33 ]
34 },
35 "mux": {
36 "enabled": true,
37 "concurrency": 1
38 },
39 "streamSettings": {
40 "network": "ws",
41 "security": "tls",
42 "tlsSettings": {
43 "serverName": "test.example.com"
44 },
45 "wsSettings": {
46 "path": "/ProForward",
47 "headers": {
48 "Host": "test.example.com"
49 }
50 }
51 }
52 }
53 ],
54 "routing": {
55 "rules": [
56 {
57 "type": "field",
58 "inboundTag": [
59 "pro-forward"
60 ],
61 "outboundTag": "pro-forward"
62 }
63 ]
64 }
65}
设备B
这里定义了一个reverse对象的bridges字段来标记隧道流量,在routing规则中:
- 从 pro 输入的流量中,只允许匹配预定义的 pro.v2ray.com 域名流量,即隧道流量,输出到 pro-tunnel
- 其余从 pro 输入的流量,全部输出到 direct 来使用宿主机的网络
1{
2 "log": {
3 "loglevel": "debug"
4 },
5 "reverse": {
6 "bridges": [
7 {
8 "tag": "pro",
9 "domain": "pro.v2ray.com"
10 }
11 ]
12 },
13 "inbounds": [],
14 "outbounds": [
15 {
16 "tag": "pro-tunnel",
17 "protocol": "vless",
18 "settings": {
19 "vnext": [
20 {
21 "address": "test.example.com",
22 "port": 443,
23 "users": [
24 {
25 "id": "897244b5-0170-44a3-9f76-672fa6734c9b",
26 "encryption": "none",
27 "level": 0
28 }
29 ]
30 }
31 ]
32 },
33 "mux": {
34 "enabled": false,
35 "concurrency": 1
36 },
37 "streamSettings": {
38 "network": "ws",
39 "security": "tls",
40 "tlsSettings": {
41 "serverName": "test.example.com"
42 },
43 "wsSettings": {
44 "path": "/ProTunnel",
45 "headers": {
46 "Host": "test.example.com"
47 }
48 }
49 }
50 },
51 {
52 "tag": "direct",
53 "protocol": "freedom"
54 }
55 ],
56 "routing": {
57 "rules": [
58 {
59 "type": "field",
60 "inboundTag": [
61 "pro"
62 ],
63 "domain": [
64 "full:pro.v2ray.com"
65 ],
66 "outboundTag": "pro-tunnel"
67 },
68 {
69 "type": "field",
70 "inboundTag": [
71 "pro"
72 ],
73 "outboundTag": "direct"
74 }
75 ]
76 }
77}
设备C
这里定义了一个reverse对象的portals字段来标记隧道流量,与需要被访问的B中的bridges域名一致,并在inboounds中启用流量嗅探,在routing规则中:
- 令 pro-forward 输出到 pro,让经过该代理的请求可以转发到内网的设备B,进而使用B的网络
- 从 pro-tunnel 输入的流量中,只允许匹配预定义的 pro.v2ray.com 域名流量,即隧道流量,从 pro 输出
1{
2 "log": {
3 "loglevel": "debug"
4 },
5 "reverse": {
6 "portals": [
7 {
8 "tag": "pro",
9 "domain": "pro.v2ray.com"
10 }
11 ]
12 },
13 "inbounds": [
14 {
15 "tag": "pro-forward",
16 "listen": "127.0.0.1",
17 "port": 9090,
18 "protocol": "vless",
19 "settings": {
20 "clients": [
21 {
22 "id": "897244b5-0170-44a3-9f76-672fa6734c9b"
23 }
24 ],
25 "decryption": "none"
26 },
27 "streamSettings": {
28 "network": "ws",
29 "wsSettings": {
30 "path": "/ProForward"
31 }
32 },
33 "sniffing": {
34 "enabled": true,
35 "destOverride": [
36 "http",
37 "tls"
38 ],
39 "metadataOnly": false
40 }
41 },
42 {
43 "tag": "pro-tunnel",
44 "listen": "127.0.0.1",
45 "port": 9091,
46 "protocol": "vless",
47 "settings": {
48 "clients": [
49 {
50 "id": "897244b5-0170-44a3-9f76-672fa6734c9b"
51 }
52 ],
53 "decryption": "none"
54 },
55 "streamSettings": {
56 "network": "ws",
57 "wsSettings": {
58 "path": "/ProTunnel"
59 }
60 },
61 "sniffing": {
62 "enabled": true,
63 "destOverride": [
64 "http",
65 "tls"
66 ],
67 "metadataOnly": false
68 }
69 }
70 ],
71 "routing": {
72 "rules": [
73 {
74 "type": "field",
75 "inboundTag": [
76 "pro-forward"
77 ],
78 "outboundTag": "pro"
79 },
80 {
81 "type": "field",
82 "inboundTag": [
83 "pro-tunnel"
84 ],
85 "domain": [
86 "full:pro.v2ray.com"
87 ],
88 "outboundTag": "pro"
89 }
90 ]
91 }
92}
4. 通过代理访问内网服务
需要访问设备B的内网服务时,只需要设置代理即可。
笔者的两台内网设备都已经启用了ssh登录,如果在需要设备A上通过登录设备B,则操作如下:
1➜ ~ ssh [email protected] -o ProxyCommand='nc -x 127.0.0.1:1086 %h %p'
2Last login: Thu Mar 31 23:13:13
3➜ ~
使用桌面浏览器时,例如Chrome配合SwitchyOmega插件,就能像访问国外网站一样经过代理访问内网网站。使用手机时,也可以通过编辑代理工具的自定义规则来分流,实现按域名或者IP访问内网服务。
5. 更进一步
由于是在应用层通过路由规则进行转发,v2ray的内网穿透架构不像使用VPN时那么直观,但如果熟悉结构后,配置可以进一步简化,如下所示:
Macbook Air] B[设备B
MacBook Pro] U[用户] subgraph 设备C: VPS C[nginx] D[v2ray] D1[http://127.0.0.1:8080/Tunnel] D2[http://127.0.0.1:9090/AirFoward] D3[http://127.0.0.1:9091/ProFoward] C--->D D--->D1 D--->D2 D--->D3 end U--->|test.example.com/AirForward|C U--->|test.example.com/ProForward|C A--->|air.v2ray.com + test.example.com/Tunnel|C B--->|pro.v2ray.com + test.example.com/Tunnel|C
- 设备A和设备B通过公共的Tunnel子路径和内部域名与设备C建立长连接
- 用户通过AirForward子路径连接代理时,所有流量导向设备A
- 用户通过ProForward子路径连接代理时,所有流量导向设备B
通过这种方式我们可以固定隧道入口,要暴露新设备到公网时,只需添加内部域名和转发路径即可,设备C此时的配置文件如下:
- 从 tunnel 输入的流量中
- 匹配预定义的 air.v2ray.com 域名流量,从 air 输出
- 匹配预定义的 pro.v2ray.com 域名流量,从 pro 输出
- 令 air-forward 输出到 air,让经过该代理的请求可以转发到内网的设备A,进而使用A的网络
- 令 pro-forward 输出到 pro,让经过该代理的请求可以转发到内网的设备B,进而使用B的网络
1{
2 "log": {
3 "loglevel": "debug"
4 },
5 "reverse": {
6 "portals": [
7 {
8 "tag": "air",
9 "domain": "air.v2ray.com"
10 },
11 {
12 "tag": "pro",
13 "domain": "pro.v2ray.com"
14 }
15 ]
16 },
17 "inbounds": [
18 {
19 "tag": "tunnel",
20 "listen": "127.0.0.1",
21 "port": 8080,
22 "protocol": "vless",
23 "settings": {
24 "clients": [
25 {
26 "id": "897244b5-0170-44a3-9f76-672fa6734c9b"
27 }
28 ],
29 "decryption": "none"
30 },
31 "streamSettings": {
32 "network": "ws",
33 "wsSettings": {
34 "path": "/Tunnel"
35 }
36 },
37 "sniffing": {
38 "enabled": true,
39 "destOverride": [
40 "http",
41 "tls"
42 ],
43 "metadataOnly": false
44 }
45 },
46 {
47 "tag": "air-forward",
48 "listen": "127.0.0.1",
49 "port": 9090,
50 "protocol": "vless",
51 "settings": {
52 "clients": [
53 {
54 "id": "897244b5-0170-44a3-9f76-672fa6734c9b"
55 }
56 ],
57 "decryption": "none"
58 },
59 "streamSettings": {
60 "network": "ws",
61 "wsSettings": {
62 "path": "/AirForward"
63 }
64 },
65 "sniffing": {
66 "enabled": true,
67 "destOverride": [
68 "http",
69 "tls"
70 ],
71 "metadataOnly": false
72 }
73 },
74 {
75 "tag": "pro-forward",
76 "listen": "127.0.0.1",
77 "port": 9091,
78 "protocol": "vless",
79 "settings": {
80 "clients": [
81 {
82 "id": "897244b5-0170-44a3-9f76-672fa6734c9b"
83 }
84 ],
85 "decryption": "none"
86 },
87 "streamSettings": {
88 "network": "ws",
89 "wsSettings": {
90 "path": "/ProForward"
91 }
92 },
93 "sniffing": {
94 "enabled": true,
95 "destOverride": [
96 "http",
97 "tls"
98 ],
99 "metadataOnly": false
100 }
101 }
102 ],
103 "routing": {
104 "rules": [
105 {
106 "type": "field",
107 "inboundTag": [
108 "tunnel"
109 ],
110 "domain": [
111 "full:air.v2ray.com"
112 ],
113 "outboundTag": "air"
114 },
115 {
116 "type": "field",
117 "inboundTag": [
118 "tunnel"
119 ],
120 "domain": [
121 "full:pro.v2ray.com"
122 ],
123 "outboundTag": "pro"
124 },
125 {
126 "type": "field",
127 "inboundTag": [
128 "air-forward"
129 ],
130 "outboundTag": "air"
131 },
132 {
133 "type": "field",
134 "inboundTag": [
135 "pro-forward"
136 ],
137 "outboundTag": "pro"
138 }
139 ]
140 }
141}
笔者测试时使用了xray的镜像,进入容器后,可以看到设备A(MacBook Air)与设备B(MacBook Pro)建立的长连接,本地端口为8080,对应上述tunnel的配置:
1~ # netstat -atupn
2Active Internet connections (servers and established)
3Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
4tcp 0 0 :::9090 :::* LISTEN 1/xray
5tcp 0 0 :::9091 :::* LISTEN 1/xray
6tcp 0 0 :::8080 :::* LISTEN 1/xray
7tcp 0 0 ::ffff:10.16.0.150:8080 ::ffff:10.16.0.102:58662 ESTABLISHED 1/xray
8tcp 0 0 ::ffff:10.16.0.150:8080 ::ffff:10.16.0.102:59158 ESTABLISHED 1/xray
当我们从MacBook Pro使用AirForward子路径代理登录到Macbook Air时,可以看到新增了本地端口为9090的长连接,对应上述air-foward的配置:
1~ # netstat -atupn
2Active Internet connections (servers and established)
3Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
4tcp 0 0 :::9090 :::* LISTEN 1/xray
5tcp 0 0 :::9091 :::* LISTEN 1/xray
6tcp 0 0 :::8080 :::* LISTEN 1/xray
7tcp 0 0 ::ffff:10.16.0.150:8080 ::ffff:10.16.0.102:58662 ESTABLISHED 1/xray
8tcp 0 0 ::ffff:10.16.0.150:8080 ::ffff:10.16.0.102:59158 ESTABLISHED 1/xray
9tcp 0 0 ::ffff:10.16.0.150:9090 ::ffff:10.16.0.102:43826 ESTABLISHED 1/xray
接着进行套娃操作,在MacBook Air使用ProForward子路径代理登录MacBook Pro,可以看到又新增了一个长连接,本地端口为9091,对应上述pro-forward的配置:
1~ # netstat -atupn
2Active Internet connections (servers and established)
3Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
4tcp 0 0 :::9090 :::* LISTEN 1/xray
5tcp 0 0 :::9091 :::* LISTEN 1/xray
6tcp 0 0 :::8080 :::* LISTEN 1/xray
7tcp 0 0 ::ffff:10.16.0.150:8080 ::ffff:10.16.0.102:58662 ESTABLISHED 1/xray
8tcp 0 0 ::ffff:10.16.0.150:8080 ::ffff:10.16.0.102:59158 ESTABLISHED 1/xray
9tcp 0 0 ::ffff:10.16.0.150:9090 ::ffff:10.16.0.102:43826 ESTABLISHED 1/xray
10tcp 0 0 ::ffff:10.16.0.150:9091 ::ffff:10.16.0.102:42312 ESTABLISHED 1/xray
套娃操作可以反复持续下去,一直到网络延迟增加至无法登录为止。