使用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:

graph LR; A[设备A
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的网络环境
graph LR; A[设备A
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服务,如下所示:

graph LR; A[设备A
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时那么直观,但如果熟悉结构后,配置可以进一步简化,如下所示:

graph LR; A[设备A
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

套娃操作可以反复持续下去,一直到网络延迟增加至无法登录为止。