众所周知,LAN 内网(即常见家用宽带)通常带有 NAT 网络地址转换设备。这可以提升网络安全,并减少公网 IPv4 分配(尤其是国内环境,IPv4 地址少的可怜)。
缺点是 NAT 限制了两台设备间的端到端连接,使得内网设备无法直接暴露在公网。
举几个常见的例子,没有公网IP,你无法直接地:

  • 访问内网域的打印机
  • 存取内网域的 NAS 存储设备
  • 控制内网域的电脑

我在这里所说的无法访问是指的无法直接访问捏
您当然可以通过常见的 C/S 架构,即客户端到服务端访问到你的设备,不过这属于间接访问了。

要知道在国内带宽资源可是寸土寸金的,有公网 IPv4 的宽带少之又少,家用宽带又基本很难申请到,于是内网穿透就应运而生了。

国内带宽资源真的很贵吗?

如图:这是 阿⭕️云 的 100Mbps 带宽价格
此图仅供参考,不对时效性和准确性承担任何责任desu

什么是内网穿透

  • 在 NAT 的影响下,外网设备是无法直接访问内网设备的。在 NAT 影响下只有在内网设备向外网设备通信后才能建立连接
  • 我们可以用带 IPv4 的公网设备进行中继,将内网的设备通过中继映射到外网 中。
  • 这样,我们能够通过具有 IPv4 的公网中继设备,这就是内网穿透。

FRP 内网穿透教程

FRP 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。

在众多内网穿透软件中,使用 FRP 的好处是在用户侧可以无需安装任何软件即可访问内网设备

我们将使用 FRP 作为内网穿透,实现下图的功能。
即,在一个内网下只使用一台设备(我们将此设备称为跳板机),将同一内网子网不同内网 IP下的 HTTP 网页服务,HTTPS 网页服务,SSH 远程开发服务及远程桌面服务等映射到外网。

环境准备

这里只为实现上述功能进行基本的讲解,如您是新手上路,您可前往 FRP 的官方教程和示例网站查看相关内容

  1. 你需要掌握基本的 Unix 操作命令,教程详见 Bash命令及Linux常用操作
  2. 您需要一台能够访问外网的机器,您可前往各大云服务器厂商购买。
  1. 您可以从 GitHub 的 Release 页面中下载 frp 最新版本的客户端和服务器二进制文件。所有文件都打包在一个压缩包中(后缀 tar.gz 或 zip),如果你有安全担忧请注意校验 MD5 值。
    不知道选择下载哪个文件?
    架构名称操作系统架构解释
    android_arm64Android适用于64位ARM架构的Android设备。
    darwin_amd64macOS适用于Intel 64位架构的macOS系统。
    darwin_arm64macOS适用于Apple Silicon 64位ARM架构的macOS系统。
    freebsd_amd64FreeBSD适用于64位AMD或Intel架构的FreeBSD系统。
    linux_amd64Linux适用于64位AMD或Intel架构的Linux系统。
    linux_armLinux适用于32位ARM架构的Linux设备。
    linux_arm64Linux适用于64位ARM架构的Linux设备。
    linux_arm_hfLinux适用于ARM硬浮点架构的Linux设备。
    linux_loong64Linux适用于龙芯64位架构的Linux设备。
    linux_mipsLinux适用于32位MIPS架构的Linux设备。
    linux_mips64Linux适用于64位MIPS架构的Linux设备。
    linux_mips64leLinux适用于64位小端MIPS架构的Linux设备。
    linux_mipsleLinux适用于32位小端MIPS架构的Linux设备。
    linux_riscv64Linux适用于64位RISC-V架构的Linux设备。
    windows_amd64Windows适用于64位AMD或Intel架构的Windows系统。
    windows_arm64Windows适用于64位ARM架构的Windows系统。
    解压后您会看到下面 4 个文件:
    1
    2
    3
    4
    frpc       # 这是客户端应用    您应该将其放在具有内网的机器上
    frpc.toml # 这是客户端配置文件 您应该将其放在具有内网的机器上
    frps # 这是服务端应用 您应该将其放在具有公网的机器上
    frps.toml # 这是服务端配置文件 您应该将其放在具有公网的机器上

服务端的基础配置

您需要修改 frps.toml 配置文件

1
2
3
# frps.toml
bindPort = 7000 # 这是外网与内网通信的端口,您可自行修改
auth.token = "mypassword" # 这里可以配置密码保护,你同样需要在 frpc.toml 设置此密码
对于云服务器厂商(如阿里云、腾讯云等)用户,您需要自行开放在 bindPort 设置的端口并允许 UDP 及 TCP 协议连接

请在编写好配置文件后再使用以下命令启动服务器

$

客户端的基础配置

您需要修改 frpc.toml 配置文件,您需要知道服务端的公网 IP 地址以进行内网机器的配置

1
2
3
serverAddr = "145.14.191.10"   # 这里需要填写服务端的公网 IP 地址
serverPort = 7000 # 这里需和 frps.toml 中的 bindPort 保持一致
auth.token = "mypassword" # 这里需和 frps.toml 中的 auth.token 保持一致

请在编写好配置文件后再使用以下命令启动服务器

$

如果与服务端连接成功,你将看到类似如下日志信息:

1
2
3
[I] [sub/root.go:142] start frpc service for config file [./frpc.toml]
[I] [client/service.go:295] try to connect to server...
[I] [client/service.go:287] [abcdefghi] login to server success, get run id [abcdefghi]

至此,您已完成全部的 FRP 基础配置。

为 SSH 服务进行远程穿透

此步骤可以将内网开启 SSH 服务的设备暴露在公网中。

假设服务端公网 IP 为 145.14.191.10, 我们开放服务端的端口 11451。而我们将进行穿透的内网设备 IP 为 192.168.1.101,设备的 SSH 端口号为 22

对于云服务器厂商(如阿里云、腾讯云等)用户,您需要自行开放在此设置的端口并允许 TCP 协议连接

我们只需为 frpc.toml 添加一个 proxies 配置,详细解释请查看注释内容

1
2
3
4
5
6
7
8
9
# frpc.toml 追加的内容
[[proxies]]
name = "my-local-computor-ssh" # 起个名字捏,命名不能重复
type = "tcp" # SSH 使用的是 TCP 协议
localIP = "192.168.1.101" # 内网设备 SSH 的内网 IP 地址
localPort = 22 # 内网设备 SSH 的端口号,一般为 22
remotePort = 11451 # 在服务端开放的远程端口号
transport.useEncryption = true # 传输时进行数据加密
transport.useCompression = true # 传输时进行数据压缩

重启 frpc 服务,如果设置成功,你将看到类似如下 frpc 的日志信息:

1
2
3
4
[I] [client/service.go:294] try to connect to server...
[I] [client/service.go:286] [abcdefjhi] login to server success, get run id [abcdefjhi]
[I] [proxy/proxy_manager.go:173] [abcdefjhi] proxy added: [my-local-computor-ssh]
[I] [client/control.go:168] [abcdefjhi] [my-local-computor-ssh] start proxy success

此时访问 SSH 服务时只需输入如下命令

$

示例如下

$

为 HTTP 网页服务进行内网穿透

HTTP 服务穿透可以达到的效果是,当访问自定义域名网站如 web.example.com,可以穿透到内网域指定的任意网络服务。

HTTP 内网穿透原理

我将解释 HTTP 内网穿透的原理。

  1. HTTP 解析基础知识
    • HTTP 使用 80 作为默认的访问端口,如果您访问 `web.example.com`,实际上访问的是此域名指向服务器IP的端口,即 `web.example.com:80`。
      • 我们可以在 frps 服务端上开放 80 端口,以避免输入网站域名时加端口号。
      • 如果您设置自定义端口号,如 4321, 那么您需要加上端口号才能访问到您的服务,即,web.example.com:4321
  2. DNS 解析基础知识
    • 在访问一个网站域名时,如 web.example.com,会首先访问 DNS 服务器(域名解析服务器)。
    • DNS 查找记录,并将您的域名解析成一个指向服务器的IP地址,这个在 DNS 服务器上的记录称为 A 记录。
    • 用户设备接收到来自 DNS 的 A 记录,里面包含了服务器的 IP 地址,此时用户设备将访问此 IP 地址的某个端口
  3. frp 的设计
    • 首先,我们需要了解的是 HTTP 协议发送的请求内容都包含 Host 字段,即域名名称,例如web.example.com这个名称能被 frps 截获
    • frps(即 frp 服务端)对 HTTP 协议端口进行了复用(例如,A域名和B域名同时访问了 80 端口),frp 称其为 vHost
    • 因此,多个域名进行端口访问时,frps 能够知道其域名名称,也就能够将其转向某个内网服务,我们将用到 vHostHTTPPort 属性配置 frps。
    • 此外,在 frpc(即 frp 客户端)中,我们将用到一个 frpc 客户端自带的插件 http2http,用以访问内网域非本机的服务
    frpc 还有很多插件用于访问内网域非本机的服务,如 https2http, http2https, https2https 等
    您可前往 GitHub 查看全部配置示例

配置 HTTP 内网穿透

您需要拥有一个属于自己的域名,如果没有的话:

  • 您可以去各大云服务器厂商购买域名
  • 您可以使用 frp服务提供商 提供的二级自定义域名
  • 您可以使用 公网IP地址加端口 的形式进行 HTTP 内网穿透,但这一章节的教程并不适用于您。
    • 使用 TCP 协议,并在 frps 设置 vHostHTTPPort 属性,使用 http://<服务端 IP 地址>:<端口号> 的形式进行访问。

此外,您需要在域名的DNS解析服务上新增一条 A 记录,并指向服务器 IP 地址

首先,您需要在部署 frps 的服务端上修改 frps.toml 配置文件。
只需新增 vHostHTTPPort 属性

1
2
# frps.toml 追加的内容
vHostHTTPPort: 80 # 配置 HTTP 复用的端口

之后您需要在部署 frpc 的内网设备上修改 frpc.toml 配置文件。

如果您穿透的内网服务协议是 HTTP,您需要 http2http 插件,在 localAddr 属性的端口号是 80
如果您穿透的内网服务协议是 HTTPS,您需要 http2https 插件,在 localAddr 属性的端口号是 443
您可在内网用浏览器查看您的服务,浏览器链接栏左侧的 http:// 和 https:// 即是协议名称

1
2
3
4
5
6
7
8
9
10
11
12
# frpc.toml 追加的内容
[[proxies]]
name = "http-web-service" # 起个名字捏,命名不能重复
type = "http" # 类型是 HTTP
customDomains = ["web.example.com"] # 您自己的外网域名名称

[proxies.plugin]
type = "http2http" # 使用插件 http2http 以进行非本机(localhost)设备的内网转发
localAddr = "192.168.1.102:80" # 转发的地址,需加上端口号
hostHeaderRewrite = "192.168.2.102" # 重写 Header 中的域名,即告诉此服务是通过重写的地址 192.168.2.102 访问的,
# 而非外网,这样可以避免 nginx 等程序错误解析域名。
requestHeaders.set.x-from-where = "frp" # 用以识别流量来源

重启 frps 和 frpc 服务,如果设置成功,你将看到 frps 和 frpc 的类似如下的日志信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# frps 的日志
[I] [frps/root.go:105] frps uses config file: /frp/frps.toml
[I] [server/service.go:237] frps tcp listen on 0.0.0.0:7000
[I] [server/service.go:319] http service listen on 0.0.0.0:80
[I] [frps/root.go:114] frps started successfully
[I] [server/service.go:576] [abcdefjhi] client login info: ip [145.14.191.10:10662] version [linux] arch [amd64]
[I] [server/control.go:399] [abcdefjhi] new proxy [http-web-service] type [http] success
[I] [proxy/http.go] [abcdefjhi] [http-web-service] http proxy listen for host [web.example.com]

# frpc 的日志
[I] [client/service.go:294] try to connect to server...
[I] [client/service.go:286] [abcdefjhi] login to server success, get run id [abcdefjhi]
[I] [proxy/proxy_manager.go:173] [abcdefjhi] proxy added: [http-web-service]
[I] [client/control.go:168] [abcdefjhi] [http-web-service] start proxy success

为 HTTPS 网页服务进行远程穿透

请先完成获取个人域名及为域名设置服务器IP地址 DNS 解析的操作

详见 配置 HTTP 内网穿透

您还需要申请一个您个人域名的 SSL 证书,如果您持有此域名,最简单的方法是使用 Let’s Encrypt 申请免费证书,此处略过。

实际上,HTTPS 和 HTTP 设置方法是一样的,只多添加了 SSL 安全协议所需的证书,不过 HTTPS 的默认端口号是 443

HTTPS 服务穿透可以达到的效果是,当访问自定义域名网站如 nas.example.com,服务是SSL加密的,并可以穿透到内网域指定的任意网络服务。

首先,您需要在部署 frps 的服务端上修改 frps.toml 配置文件。
只需新增 vHostHTTPSPort 属性

1
2
# frps.toml 追加的内容
vHostHTTPSPort: 443 # 配置 HTTPS 复用的端口

之后您需要在部署 frpc 的内网设备上修改 frpc.toml 配置文件。

如果您穿透的内网服务协议是 HTTP,您需要 https2http 插件,在 localAddr 属性的端口号是 80
如果您穿透的内网服务协议是 HTTPS,您需要 https2https 插件,在 localAddr 属性的端口号是 443
您可在内网用浏览器查看您的服务,浏览器链接栏左侧的 http:// 和 https:// 即是协议名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# frpc.toml 追加的内容
[[proxies]]
name = "nas-web-service" # 起个名字捏,命名不能重复
type = "https" # 类型是 HTTP
customDomains = ["nas.example.com"] # 您自己的外网域名名称

[proxies.plugin]
type = "https2https" # 使用插件 http2http 以进行非本机(localhost)设备的内网转发
localAddr = "192.168.1.103:443" # 转发的地址,需加上端口号
hostHeaderRewrite = "192.168.2.103" # 重写 Header 中的域名,即告诉此服务是通过重写的地址 192.168.2.103 访问的,
# 而非外网,这样可以避免 nginx 等程序错误解析域名。
crtPath = "/ssl_cert/nas.example.com.cer"# 您的证书文件(Certificate)存放位置
keyPath = "/ssl_cert/nas.example.com.key"# 您的私钥文件(Private Key)存放位置
requestHeaders.set.x-from-where = "frp" # 用以识别流量来源

使用 FRP 打洞建立 P2P 连接

这里才是使用 frp 最有趣的地方!您可以通过 打洞 的方式使两台设备直接连接,中继服务器仅提供了打洞所需的铲子。
打洞成功后,两台设备的数据传输不再需要中继服务器的转发,这就是点对点连接(P2P, i.e. Peer-to-peer)。

什么是打洞?

引用自维基百科:
打洞指的是一种普遍使用的NAT穿越技术。
让位于NAT后的两台主机都与处于公共地址空间的、已知IP地址的第三台服务器相连,一旦NAT设备建立好状态信息就转为直接通信,并让NAT设备在从另外一个主机传送过来时仍然保持当前状态。
这项技术需要使用圆锥型NAT设备,对称型NAT不能使用这项技术。

NAT 类型检测及打洞的原理

NAT 一共有 4 种类型,其中 NAT 1-4:

  • NAT1: 完全圆锥型:最开放,允许任意外部主机访问公网地址端口映射的数据。
  • NAT2: 地址限制圆锥型:限制了外部主机需由内网设备主动联系才能回应,具备一定安全性。
  • NAT3: 端口限制圆锥型:进一步限制在端口层级,需要外部端口与私网端口一致。
  • NAT4: 对称型:对每个连接映射不同端口,使得端口不固定,不适合直接进行 P2P 穿透,只能通过服务器中转。
更详细的 NAT 描述
NAT 类型点对点穿透 (P2P)描述
Full Cone (完全圆锥型)支持私网地址和端口映射到一个固定的公网地址和端口,
任何外部主机都可以通过该公网地址端口访问私网设备。
Address Restricted Cone
(地址限制圆锥型)
支持仅允许内网设备先发送过数据的外部主机在任何端口回复数据。
外部主机在被联系后可以回复到相同端口。
Port Restricted Cone
(端口限制圆锥型)
支持仅允许内网设备先发送数据的外部主机在指定端口回复数据。
端口需与私网端口一致,才可传输到内网设备。
Symmetric NAT
(对称型 NAT)
不支持每个外部主机的映射端口不同,即每个新连接都会有新的映射端口,
无法通过相同端口接收其他外部主机数据。

实际上 frp 将 NAT 内网设备间的公网IP地址及端口的映射模式分发给了另一方的内网设备,双方设备中的用户(访问者)方尝试进行连接。

搭建 FRP 打洞服务建立 P2P 连接

请注意当前 frp 只支持打洞后双方设备进行 TCP 连接

下面我们将搭建一个的 SSH 服务,以演示 P2P 连接。
对于其他应用,在搭建好下面演示的方法后您只需要更改端口号即可使用其他服务,如文件传输,远程桌面等,P2P 的传输速率只取决于两台设备的上行和下行带宽

请看上图,要想进行 P2P 连接,需要满足以下条件:

  • 用户(也即访问者)也同样需要安装 frpc 软件,并连接到 frps 服务端用以完成 P2P 的建立。
  • 在不同内网的两个设备需要均满足 NAT 的设备类型不为对称型 NAT 才可打洞,一般家用宽带是均为圆锥型 NAT。
    我如何知道我的 NAT 设备是什么类型?

    您可以使用 pystun3 对 NAT 类型进行探测,你需要安装 python3
    在安装 python3 的情况下,使用如下命令安装 pystun3

    $
    之后您只需运行如下命令,我们在国内可以使用 miwifi 配置好的 stun 的服务端
    $
    您将看到类似如下信息,其中 NAT Type 即为您的 NAT 类型
    1
    2
    3
    NAT Type: Full Cone
    External IP: 123.123.123.123
    External Port: 11451

首先,您需要在图中 LAN1 FRP 服务设备上修改 frpc.toml 配置文件。

1
2
3
4
5
6
7
# 图中 LAN1 FRP 服务设备的 frpc.toml 追加的内容
[[proxies]]
name = "ssh-p2p-service" # 起个名字捏,命名不能重复
type = "xtcp" # 类型是 XTCP,目前不支持 XUDP
secretKey = "yourpassword" # 设置密码
localIP = "192.168.1.104" # 子网域内的提供 SSH 服务的 IP 地址
localPort = 22 # 提供 SSH 服务的端口

之后您需要在访问设备上,即图中的 LAN2 访问者设备上修改 frpc.toml 配置文件。
您需要在访问设备上也安装 frpc 软件。

1
2
3
4
5
6
7
8
9
10
# 图中 LAN2 访问者设备的 frpc.toml 追加的内容
[[visitors]]
name = "ssh-p2p-visitor" # 这个名字是访问者的名称
type = "xtcp" # 类型是 XTCP,目前不支持 XUDP

serverName = "ssh-p2p-service" # 这个名字是 LAN1 FRP 服务设备上的名称
secretKey = "yourpassword" # 密码需要一致

bindAddr = "127.0.0.1" # 您将通过本机访问到此对方 LAN1 的设备
bindPort = 2200 # 设置本机地址的端口号

此时重启 LAN1 和 LAN2 下的 frpc,您将看到类似如下日志内容。

1
2
3
4
5
6
7
8
9
10
11
# LAN1 设备 frpc 的日志信息
[client/service.go:294] try to connect to server...
[client/service.go:286] [abcdefjhi] login to server success, get run id [abcdefjhi]
[proxy/proxy_manager.go:173] [abcdefjhi] proxy added: [ssh-p2p-service]
[client/control.go:168] [abcdefjhi] [ssh-p2p-service] start proxy success
# LAN2 设备 frpc 的日志信息
[sub/root.go:142] start frpc service for config file [./frpc.toml]
[client/service.go:295] try to connect to server...
[client/service.go:287] [abcedfghi] login to server success, get run id [abcedfghi]
[visitor/visitor_manager.go:121] [abcedfghi] start visitor success
[visitor/visitor_manager.go:172] [abcedfghi] visitor added: [ssh-p2p-visitor]

现在将尝试在 LAN2 的设备上通过 SSH 连接时到 LAN1 子网上的 SSH 服务,命令示例如下:

$

稍等片刻,您可以观察 frpc 的日志信息。

当您成功在 LAN2 的设备上通过 SSH 连接时到 LAN1 子网上的 SSH 服务时,您将看到类似如下日志内容。

1
2
3
4
# LAN2 设备 frpc 的日志信息
[visitor/xtcp.go:283] [abcdefjhi] [ssh-p2p-visitor] nathole prepare success, nat type: EasyNAT, behavior: BehaviorNoChange, addresses: [... ...], assistedAddresses: [... ... ... ...]
[visitor/xtcp.go:309] [abcdefjhi] [ssh-p2p-visitor] get natHoleRespMsg, sid [abcdefjhi], protocol [quic], candidate address [...], assisted address [...], detectBehavior: {Role:receiver Mode:0 TTL:7 SendDelayMs:0 ReadTimeoutMs:5000 CandidatePorts:[] SendRandomPorts:0 ListenRandomPorts:0}
[visitor/xtcp.go:320] [abcdefjhi] [ssh-p2p-visitor] establishing nat hole connection successful, sid [abcdefjhi], remoteAddr [...]

The End.