内网穿透
在解释 内网穿透 这件事之前,我先抛出一个问题:如果让你来设计一套框架去实现两台内网主机进行通信,你会怎么做?
中继(Relaying)
一个非常直观的设计就是先准备一台拥有公网IP的服务器命名为Server,然后两台处于同内网下的客户端主机,分别为ClientA和ClientB,然后两台客户端机器分别连接服务器。这时候服务器充当一个数据中转站,把ClientA发送的数据转发给ClientB,反向同理,这就实现了两台内网的客户端之间的数据通信。
比较常见的就是平常经常使用到的frp,它就是一个典型的通过中继(也叫转发)实现的内网穿透功能。
NAT(Network Address Translation)穿透
NAT穿透,简单地讲就是要让处于不同NAT网络下的两个节点(Peer)建立直接连接,只知道自己的内网地址是肯定不行的,他们需要知道对方的公网IP和端口,然后双方互相向对方发送数据包,从而建立起连接。整个流程可以看做两个关键步骤:
- 发现自己的公网IP和Port
- 将自己的IP和Port共享给对方
而NAT有几种不同的形态:
Full Cone NAT(完全圆锥型)
这种会将同一内网地址端口192.168.1.2:4567发送至公网的所有请求都映射成同一个公网地址端口10.2.6.4.1:6666,192.168.1.2可以收到任意外部主机发送到10.2.6.4.1:6666的数据报文,也就是说,只要知道内网地址被映射成的公网IP,任何公网IP对其发送数据,这个内网下的所有端口都能接收到此数据。
这种类型的客户端局限性最低,也是最理想的情况,但是现实大部分情况都不是这种模型。
Address Restricted Cone NAT (地址限制圆锥型)
这种会将从同一内网地址端口192.168.1.2:4567发至公网的所有请求都映射成同一个公网地址端口10.2.6.4.1:6666,只有当内部主机192.168.1.2先给服务器Server 6.7.8.9发送一个数据报后,192.168.1.2才能收到服务器Server 6.7.8.9发送到公网地址端口10.2.6.4.1:6666的数据报文。
注意理清楚这里面的细节,地址限制圆锥型的意思就是说,内网地址接收到数据的这个公网IP必须是这个内网地址曾经发送过数据的IP地址(这就是一种映射规则),当前内网地址下的所有端口才能接收到此数据。
Port Restricted Cone NAT(端口限制圆锥型)
与受限制锥型基本一样,区别就是最后内网地址只有指定的端口号才能接收到数据。
这会将从同一内网地址端口192.168.1.2:4567发至公网的所有请求都映射成同一个公网地址端口10.2.6.4.1:6666,只有当内部主机192.168.1.2:4567先给服务器Server 6.7.8.9发送一个数据报后,192.168.1.2:4567才能收到服务器Server 6.7.8.9发送到公网地址端口10.2.6.4.1:6666的数据报。
对称NAT(Symmetric)
对不同的外网IP地址都会分配不同的端口号。把所有来自相同内部主机IP地址和端口号,到特定目的IP地址和端口号的请求映射到相同的外部IP地址和端口。如果同一主机使用不同的源地址和端口对,发送的目的地址不同,则使用不同的映射。只有收到了一个IP包的外部主机才能够向该内部主机发送回一个UDP包。对称的NAT不保证所有会话中的(私有地址,私有端口)和(公开IP,公开端口)之间绑定的一致性。相反,它为每个新的会话分配一个新的端口号。
如果是对称型的NAT,则不适合做内网穿透。
NAT穿透流程
- ClientA 和 ClientB分别通过NAT与服务器进行通信,服务器只存储两个客户端的公网IP地址信息。
- 服务器接收到数据时分别将对方的IP地址信息发送给双方。
- 此时,ClientA 拥有了 ClientB的公网地址,ClientA先发送一个探测数据给ClientB的公网IP,但是ClientB接收不到数据,因为根据协议,NatB中的映射没有这条规则,也就是说NatB并没有发送过数据给NatA,那就接收不到来自NatA的数据,但此时NatA添加了一条NatB信息的映射规则。
- NatB发送一条探测数据给NatA, NatA收到了。因为NatA在上一步骤中发送数据给NatB了,有了这条映射规则。
- NatA 在发送一条探测数据给NatB,NatB此时也收到了。
- 现在,UDP隧道建立成功,可以进行数据传输了。这个过程也叫做UDP打洞。
对称NAT(Symmetric)的内网穿透
根据这个流程我们会发现对称NAT(Symmetric)不适合做内网穿透。如果两个客户端都是对称NAT根据上述流程就没法穿透了,举个例子:
- ClientA 和 ClientB分别通过Nat与服务器进行通信,服务器只存储两个客户端的公网IP地址信息。
- 服务器接收到数据时分别将对方的IP地址信息发送给双方
- 此时,ClientA 拥有了 ClientB的公网地址,ClientA先发送一个探测数据给ClientB的公网IP,但是此时对于ClientB发送数据的IP已经和服务器所保存的IP不一样了,因为对称NAT对于不同的公网IP地址发送数据所对应的端口是不一样的。那后面流程就没法继续了。
那两个对成型如何穿透呢,除非你能够准确的猜出客户端每次映射的端口号是多少(这个理论上可以实现,但是稳定性很低)。
是否可穿透组合
| Peer A | Peer B | 是否可以打洞 |
|---|---|---|
| 全锥型 | 全锥型 | 是 |
| 全锥型 | 受限锥型 | 是 |
| 全锥型 | 端口受限锥型 | 是 |
| 全锥型 | 对称型 | 是 |
| 受限锥型 | 受限锥型 | 是 |
| 受限锥型 | 端口受限锥型 | 是 |
| 受限锥型 | 对称型 | 是 |
| 端口受限锥型 | 端口受限锥型 | 是 |
| 端口受限锥型 | 对称型 | 否 |
| 对称型 | 对称型 | 否 |
FRP使用记录
使用 FRP 做内网穿透,让我们能够在外网访问内网的NAS,PVE或者其他虚拟机及PC,下载 frp_x.xx.x_linux_amd64.tar.gz 最新二进制包。
frps
frps 部署在云服务器上。
bindPort = 7000
vhostHTTPPort = 8085
vhostHTTPSPort = 8086
auth.method = "token"
auth.token = "23ad6b16-881a-45f7-8ad0-e3e35cd38297"
log.to = "/root/Server/logs/frps.log"
log.level = "debug"
log.maxDays = 7
log.disablePrintColor = false
webServer.addr = "127.0.0.1"
webServer.port = 8087
# webServer.user = "amass"
# webServer.password = "xxxxxxxx"
[Unit]
Description=FRP Server Daemon
After = network.target syslog.target
Wants = network.target
[Service]
Type=simple
ExecStart=/root/HttpServer/frps -c /root/HttpServer/frps.toml
Restart=always
RestartSec=2s
LimitNOFILE=infinity
[Install]
WantedBy=multi-user.target
在服务上启动frps:
systemctl enable frps # 开机自启动
systemctl start frps
frpc
frpc 部署于PVE上,我们可以一次性代理多个内网服务,例如 TCP、HTTP、HTTPS 等。
# frpc.toml
user = "pve"
serverAddr = "amass.fun"
serverPort = 7000
auth.method = "token"
auth.token = "23ad6b16-881a-45f7-8ad0-e3e35cd38297"
log.to = "/opt/frp_0.62.0_linux_amd64/frpc.log"
log.level = "info"
log.maxDays = 7
log.disablePrintColor = false
[[proxies]]
name = "chatgpt_rw"
type = "http"
localIP = "chatgpt.custom.com"
localPort = 3000
customDomains = ["chatgpt.amass.fun"]
[[proxies]]
name = "pve"
type = "https"
localIP = "127.0.0.1"
localPort = 8006
customDomains = ["pve.amass.fun"]
[[proxies]] # 同时再代理rdp
name = "windows11_rw_rdp"
type = "tcp"
localIP = "192.168.5.6"
localPort = 3389
remotePort = 3390
[Unit]
Description=FRP Client Daemon
After = network.target syslog.target
Wants = network.target
[Service]
Type=simple
ExecStart=/opt/frp_0.62.0_linux_amd64/frpc -c /opt/frp_0.62.0_linux_amd64/frpc.toml
Restart=always
RestartSec=2s
LimitNOFILE=infinity
[Install]
WantedBy=multi-user.target
在PVE上启动frpc:
systemctl enable frpc # 开机自启动
systemctl start frpc
Nginx 反向代理 frps
最后,想直接能够以 https://pve.amass.fun 域名形式访问 HTTP、HTTPS 服务,则需要在nginx上反向代理 frps 的 vhostHTTPPort 以及 vhostHTTPSPort 端口,最终 frps 会通过 customDomains 将不用请求转发至不同内网服务,而这只需要一个 HTTP/HTTPS 端口即可完成。
upstream frp_https_proxy {
server 127.0.0.1:8085;
}
server {
listen 443 ssl;
server_name pve.amass.fun;
client_header_timeout 120s;
client_body_timeout 120s;
client_max_body_size 512m;
ssl_certificate cert/pve.amass.fun.pem;
ssl_certificate_key cert/pve.amass.fun.key;
ssl_session_timeout 5m; #缓存有效期
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; #加密算法
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #安全链接可选的加密协议
ssl_prefer_server_ciphers on; #使用服务器端的首选算法
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header x-wiz-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;
proxy_ssl_name $host;
proxy_pass https://frp_https_proxy;
}
}
在 阿里 域名解析→解析设置,添加pve.amass.fun记录,然后在 数字证书管理服务 → SSL证书 添加免费证书。
Nginx 的反向代理配置我们这里以上游服务为 HTTPS 示例。是因为 FRP 是根据 SNI(Server Name Indication)来转发 HTTPS 服务的,而 Nginx 在向上游 HTTPS 服务器转发请求时,默认是不会带上 SNI 的,需要设置 proxy_ssl_server_name、proxy_ssl_name 这两个字段。
访问RDP Windows
远程桌面服务所使用的通信协议是Microsoft定义RDP(Remote Desktop Protocol)协议,RDP协议的TCP通信端口号是3389。
serverAddr = "amass.fun"
serverPort = 7000
user = "windows11_pve"
auth.method = "token"
auth.token = "23ad6b16-881a-45f7-8ad0-e3e35cd38297"
[[proxies]]
name = "rdp"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3389
remotePort = 3389
然后我们使用WinSW创建frpc的Windows服务,下载WinSW的可执行程序WinSW-x64.exe,拷贝到frpc所在的目录下。注意目录路径不能有空格和非ASCII字符。
将WinSW-x64.exe命名成frpc-service.exe(事实上名字任意,只要和后面的xml文件同名即可),再创建配置文件:
<!-- frpc-service.xml -->
<service>
<id>frpc</id>
<name>Frpc Service</name>
<description>A fast reverse proxy to help you expose a local server to the internet.</description>
<executable>D:\Ubuntu\frp_0.62.0_windows_amd64\frpc.exe</executable>
<onfailure action="restart" delay="5 sec" />
<resetfailure>1 day</resetfailure>
<arguments>-c D:\Ubuntu\frp_0.62.0_windows_amd64\frpc.toml</arguments>
<workingdirectory>D:\Ubuntu\frp_0.62.0_windows_amd64</workingdirectory>
<priority>AboveNormal</priority>
<stoptimeout>15 sec</stoptimeout>
<stopparentprocessfirst>false</stopparentprocessfirst>
<startmode>Automatic</startmode>
<waithint>15 sec</waithint>
<sleeptime>1 sec</sleeptime>
<logpath>D:\Ubuntu\frp_0.62.0_windows_amd64\logs</logpath>
<log mode="roll-by-time">
<pattern>yyyyMMdd</pattern>
</log>
</service>
然后执行:
.\frpc-service.exe install # 安装这个service
.\frpc-service.exe start # 启动这个service
更多配置说明,可查看WinSW Github主页。
WireGuard使用记录
在 ECS 创建 WireGuard 服务端成功之后,会自动给客户端生成配置文件以方便我们配置客户端 。我们以配置 unRaid WireGuard为例:
本地私钥:
6IsckEeOr0EiFku6r+clkhKohFYR0/c7/mJNCcEB43c=
本地公钥:
y7wTVWB98OicD3vNkDq3KqW8PSoiTMdAgE0h17pkzGQ=
其他配置:
[Interface]
Address = 192.168.2.3
PrivateKey = 6IsckEeOr0EiFku6r+clkhKohFYR0/c7/mJNCcEB43c=
ListenPort = 51820
DNS = 192.168.2.1
[Peer]
PublicKey = PMRyN3v1R75YhAttfL/7RsUK+wqQsqeIHj9A001gOFc=
PresharedKey = VpB1Q8OAHycZcyRktR6kM8nfSdpjLfRwSSz7ZleWXMc=
Endpoint = amass.fun:51820
AllowedIPs = 0.0.0.0/0, ::/0
为了只让内网穿透的流量经过 该虚拟网端, 需要将 AllowedIPs 改为 192.168.2.0/24。
另外一个问题,WireGuard 是基于UDP的,而实际测试下来,国内电信运营商对UDP支持不太好......
我有两个设备,有时候其实是在同一个局域网内,有时不在同一 局域网内(其中一台设备是我的手机),它们通过wireguard连接了位于公网ECS的peer。那这两个设备通过 wireguard iP 访问,当它们在同一局域网内能够实现流量不经由网的peer转发么?
Tailscale 目前是支持的。
