上一篇,我们聊了内网穿透的原理,这一篇我们聊一聊内网穿透的实际搭建步骤以及一些典型的应用。对于个人而言,内网穿透是基础的基础,没了内网穿透,很多事情想做也做不成。包括我们现在所熟知的各种智能设备,都是需要使用到内网穿透技术的。

内网穿透真的做到了“让天下没有难以访问的内网设备”。有了内网穿透,你也就可以随时随地控制自己的电脑;有了内网穿透,你也就可以把你的任意一台电脑变成一个服务器。想要搭建自己的博客网站?想要搭建自己的视频网站?想要搭建自己的图床?想要给自己开发的app部署一个后端服务器?有了内网穿透,你都可以轻松做到。

1. 软件下载

如果你对花生壳之类的商业软件感兴趣的话,无需继续读下去,直接联系其官方客服即可。本篇以frp为例讲解内网穿透的实际实现方法。如果你更喜欢nps,等搞明白了frp后,可以很轻松的根据其项目网站的文档依葫芦画瓢地搭建出来。frp和nps在客户端没有任何区别,只是配置方式和管理端界面有些不同而已。它们的下载地址为:

软件 项目地址 特点
frp https://github.com/fatedier/frp 开发活跃,配置简单
nps https://github.com/ehang-io/nps 支持硬件和系统更多,甚至支持安卓。

无论是frp还是nps,通常找最新版本下载即可。这两个软件的稳定性都是非常不错的。另外,服务端和客户端可以是不同的硬件架构,比如服务器是x64,客户端是arm64,只要版本相同,就可以互联互通。版本不同的话,要看一下官方说明哈。

2. 服务端

服务端必须部署在具有公网IP的服务器上面的。你可能会有疑问,既然我已经有了一个有公网IP的服务器了,为啥还需要内网穿透呢?比如我想搭建一个网站,直接把网站搭建在这个服务器上面不就好了。关于这个疑问,我们稍后作答。

想要满足这个条件有以下几种办法:

  • 方法一:跟宽带服务商斗智斗勇,突破层层障碍,拿到公网IP(具体方法请移步百度哈)。如果你用的话,电信或者联通的宽带,还是有可能做到的。如果是用的是移动的宽带,可能可以拿到IPv6的公网IP,但是,IPv4就不用想了。如果不是这三家,大概率啥都拿不到哈。

    拿到公网IP以后,再使用openwrt或者Padavan系统的路由器,让内网中的某台设备暴露在互联网中。那这台设备便是一个具有公网IP的服务器了。

    需要特别留意的是,设备暴露在互联网上面是一个蛮危险的行为。建议关闭ssh服务,关闭不必要的端口,以便增强安全性。设备最好是一些性能比较差的,比如arm小盒子之类的设备。即使被攻破,也因为性能差或者指令集原因,而被嫌弃。至少不会被人拿来挖矿,白白浪费自己家里的电。

  • 方法二:花点小钱买个云主机。比如每年双11时,各大云服务厂商都会高新用户特价活动,1折的价格还是可以接受的。需要留意的是,对于咱们这个需求而言,ECS主机和轻量应用服务器没有区别。一般来说,轻量应用服务器性价比会更高一些。

    这个方法的好处是实现起来简单。但是,需要花钱不说,带宽还会受到蛮大限制的。云服务商的带宽是很贵的。

  • 方法三:去网上找免费的frp服务端,比如https://freefrp.net/。至于安全和稳定性,自己考量哈。

现在,回答一下本章节开始的那个疑问。

如果你是通过方法一拿到的公网IP,那么你暴露在公网上的设备一定会是低性能的设备。而且,该设备的安全系数是比较低的。所以,不推荐将应用直接部署在这台设备上面。而是将这台设备当成DMZ区使用,做好防护隔离,让自己的高性能设备免受互联网的侵害。

如果你是通过方法二拿到的公网IP,大概率也是低性能的云主机。尤其是特价云主机通常有时间限制,过期后老用户续费往往会非常贵。下一年也就可能在换个身份购买1折云主机了。如果拿来部署服务,每年需要迁移不说,性能还很差。一旦,你需要GPU、高性能、大内存,价格是非常非常昂贵的。

所以,无论如何,你都需要内网穿透技术,来省钱拓展。

这儿假设,你已经通过方法一或者方法二,准备好了一个具有公网IP的服务器。接下来你需要去下载,对应硬件架构的包。

$ wget https://github.com/fatedier/frp/releases/download/v0.46.1/frp_0.46.1_linux_amd64.tar.gz
$ tar -xf frp_0.46.1_linux_amd64.tar.gz
$ sudo mv frp_0.46.1_linux_amd64 /opt
$ tree frp_0.46.1_linux_amd64 
frp_0.46.1_linux_amd64
├── frpc
├── frpc_full.ini
├── frpc.ini
├── frps
├── frps_full.ini
├── frps.ini
└── LICENSE

0 directories, 7 files

其中,frps就是服务端,frps.ini是服务端的配置文件。运行方法很简单,下面的命令即可:

./frps -c frps.ini

但是,默认给的frps.ini中没有任何安全性。这样子运行,等于把frps开放给所有人使用。所以,可以做以下配置:

[common]
# 改掉默认端口,以尽量防止定向攻击。
bind_port = 7171
bind_udp_port = 7172
vhost_https_port=7173
vhost_http_port=7174

# 记录日志,以便出现问题时排查
log_file = /var/log/frps/frps.log
# trace, debug, info, warn, error
log_level = info
log_max_days = 30

# 设置token,这样只有客户端配置了正确的token才可以连接上来。注意,token一定要是难以猜到且比较复杂。
token=12345678
# 如果你有域名的话,可以在这儿设置一个域名。这样网站可以使用多级子域名。
subdomain_host = test.mydata.top
# 允许客户端使用的端口号
allow_ports=6000-9000

# 限制允许的连接数,以防服务端扛不住哈
max_ports_per_client=50
max_pool_count=10

另外,由于frps需要7*24小时运行。所以,一般不要直接通过命令启动它,而是写一个进程守护脚本启动它。例如,启动脚本/opt/frp_0.46.1_linux_amd64/start.sh,内容如下:

#!/bin/bash

SOURCE="$0"
while [ -h "$SOURCE"  ]; do
        DIR="$( cd -P "$( dirname "$SOURCE"  )" && pwd  )"
        SOURCE="$(readlink "$SOURCE")"
        [[ $SOURCE != /*  ]] && SOURCE="$DIR/$SOURCE"
done
SRC_PATH="$( cd -P "$( dirname "$SOURCE"  )" && pwd  )"
cd $SRC_PATH

if [ ! -e /var/log/frps ]; then
    mkdir -p /var/log/frps
fi

while [ true ]; do
    ./frps -c frps.ini
    sleep 10
done

然后,使用以下命令启动该脚本:

nohup bash ./start.sh >/dev/null &

这样启动后,即使frps因为某些原因崩溃了,也会在10秒后被自动拉起来。但是,如果系统被重启了,frps还是无法自动启动。所以,我们需要把它变成一个service。让系统启动时自动启动frps。

首先,我们创建文件/usr/lib/systemd/system/frps.service,内容为:

[Unit]
Description=frp server
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
Type=simple
ExecStart=/opt/frp_0.46.1_linux_amd64/start.sh 
# ExecReload=target_dir/restart.sh 
# ExecStop=target_dir/shutdown.sh
SuccessExitStatus=0

[Install]
WantedBy=multi-user.target

然后,执行下面的命令:

sudo systemctl daemon-reload # 重新加载配置
sudo systemctl enable frps   # 让frps服务开机自动启动
sudo systemctl start frps    # 立即启动frps服务

3. 客户端

相比服务端,客户端就没有什么要求了。可以是frp或者nps支持的任意系统或架构。当然,肯定不需要有公网IP了。如果客户端的设备有公网IP的话,谁还用内网穿透啊,直接访问不就好了。

3.1 基本使用方法

接下来,我们可以通过frpc来连接我们的客户端了。同样的,我们可以简单的使用,./frpc -c frpc.ini运行客户端。但是,因为我们的服务端配置了token,也改了端口号,所以,这儿会报错。

$ wget https://github.com/fatedier/frp/releases/download/v0.46.1/frp_0.46.1_linux_arm64.tar.gz
$ ./frpc -c frpc.ini
2023/01/10 23:57:39 [W] [service.go:133] login to server failed: dial tcp 127.0.0.1:7000: connect: connection refused
dial tcp test.mydata.top:7000: connect: connection refused

即使,把端口号改正确了,也会报错。

2023/01/10 23:58:48 [E] [service.go:289] token in login doesn't match token from configuration
2023/01/10 23:58:48 [W] [service.go:133] login to server failed: token in login doesn't match token from configuration
token in login doesn't match token from configuration

正确的配置,至少应该是:

[common]
server_addr = test.mydata.top
server_port = 7171
token = 12345678

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 6000

其中,token需要跟服务端设置的token完全相同。接下来再运行,就能看到正确的信息了:

$ ./frpc -c frpc.ini 
2023/01/11 00:00:15 [I] [service.go:298] [f37327838287a046] login to server success, get run id [f37327838287a046], server udp port [7172]
2023/01/11 00:00:15 [I] [proxy_manager.go:142] [f37327838287a046] proxy added: [ssh]
2023/01/11 00:00:15 [I] [control.go:172] [f37327838287a046] [ssh] start proxy success

现在,你就可以在任意一台联网的设备(这台设备被称为:访客端)上通过下面命令连接这台电脑了。

$ ssh -p 6000 username@test.mydata.top

实际上,你是先连接了frps进程的6000端口,然后,再通过6000端口连接上了22端口。

在上一节中,我通过frp对ssh的端口实现了内网穿透。实际上,通过这种方式,我们可以对任意基于TCP协议的端口实现内网穿透。甚至包括http协议。

另外,为了增强安全性,可以还可以针对端口进行加密。为了减少带宽占用,还可以对数据包进行压缩。还可以防止中断,增加心跳检查。另外,不仅仅支持tcp协议,还支持udp、http、https等各种协议。配置方法在安装包的frpc_full.ini里面都有。通常我们依葫芦画瓢即可。

不过,需要注意的是加密和压缩都只存在于frps和frpc的通信过程中,访客端与frps之间的通信既不加密也不压缩的。

通过frps转发时的时序图为:

sequenceDiagram participant a as 访客端(用户的客户端程序,如ssh、浏览器等) participant s as 服务端(frps) participant c as 客户端(frpc) participant c1 as 客户端(用户的服务器程序) c-->>s: 建立连接 a-->>+s: 发送数据包(不可加密、不可压缩) s-->>c: 转发数据包(可加密、可压缩) c->>+c1: 转发数据包(本机通信) c1->>c1: 处理数据包 c1->>-c: 回复数据包(本机通信) c-->>s: 转发数据包(可加密、可压缩) s-->>-a: 转发数据包(不可加密、不可压缩)

3.2 安全连接

由于在普通模式下,只有服务端和客户端之间可以加密和压缩。访客端和服务端无法进行加密和压缩。所以,安全性不高,通信效率也偏低。于是,frp提供了一种安全连接的方式来解决这个问题。那就是在访客端再运行一个frpc,这样访客端的用户程序只是跟访客端的frpc通信。而访客端的frpc和frps之间也可以进行加密和压缩了。这样一来,访客端、服务端、客户端之间的所有通信就都是加密的了。

这种模式下的通信时序图为:

sequenceDiagram participant a1 as 访客端(用户的客户端程序,如ssh、浏览器等) participant a as 访客端(frpc) participant s as 服务端(frps) participant c as 客户端(frpc) participant c1 as 客户端(用户的服务器程序) c->>s: 建立连接 a->>s: 建立连接 a1->>+a: 发送数据包(本机通信) a->>s: 转发数据包(可加密、可压缩) s->>c: 转发数据包(可加密、可压缩) c->>+c1: 转发数据包(本机通信) c1->c1: 处理数据包 c1-->>-c: 回复数据包(本机通信) c-->>s: 转发数据包(可加密、可压缩) s-->>a: 转发数据包(可加密、可压缩) a-->>-a1: 转发数据包(本机通信)
  • 客户端的配置方法

    [secret_tcp]
    type = stcp
    sk = abcdefg
    local_ip = 127.0.0.1
    local_port = 22
    use_encryption = true
    use_compression = true
  • 服务器端不需要特别配置。

  • 访客端的配置方法

    [secret_tcp_visitor]
    role = visitor
    type = stcp
    server_name = secret_tcp
    sk = abcdefg
    bind_addr = 127.0.0.1
    bind_port = 2022
    use_encryption = true
    use_compression = true

接下来,我们在访客端打开一个终端程序,执行:

ssh -p 2022 username@127.0.0.1

即可连接到客户端,而且全程通信都是安全的。

3.3 点对点直连

默认情况下,我们所有的数据包都是通过frps服务器转发的。也就是说,frps是个代理。这就导致速度慢一倍。如果服务器的带宽有限的话,那就更是个问题了。有没有方法让数据包不通过frps走呢?答案是肯定的。frp提供了一种p2p模式,让双方不再通过frps转手,而是直接发送给对方。frps只是个介绍人,在建立连接期间,让双方都认识对方,这样双方也就可以直接通信了。没有中间商,效率也就高效多了。

在p2p模式下,通信时序图为:

sequenceDiagram participant a1 as 访客端(用户的客户端程序,如ssh、浏览器等) participant a as 访客端(frpc) participant s as 服务端(frps) participant c as 客户端(frpc) participant c1 as 客户端(用户的服务器程序) c->>s: 建立连接 a->>s: 建立连接 s->>a: 发送frpc(内网设备)的连接信息 a->>c: 使用frps发送过来的信息,建立连接 a1->>+a: 发送数据包(本机通信) a->>c: 转发数据包(可加密、可压缩) c->>+c1: 转发数据包(本机通信) c1->c1: 处理数据包(本机通信) c1-->>-c: 回复数据包 c-->>a: 转发数据包(可加密、可压缩) a-->>-a1: 转发数据包(本机通信)

在这种方式下,访客端也需要运行一个frpc,以便跟客户端的frpc建立连接。从时序图上看,访客端的frpc扮演了frps转发数据包的功能。但是,因为访客端的frpc跟访客端的程序在同一台设备上,不需要走互联网。速度和带宽都是非常大的,延时也可以忽略不计。而访客端的frpc与客户端的frpc是点对点直连的,所以,速度要比frps转发快很多。

  • 客户端的配置方法

    [p2p_tcp]
    type = xtcp
    sk = abcdefg
    local_ip = 127.0.0.1
    local_port = 22
    use_encryption = true
    use_compression = true

    其中,sk是一个安全码,客户端与连接端需要匹配,可以进一步提高安全性。跟tcp协议不同的是,因为是直连,这儿不需要指定一个frps侧的端口号。

  • 服务器端不需要特别配置。

  • 访客端的配置方法

    [p2p_tcp_visitor]
    role = visitor
    type = xtcp
    server_name = p2p_tcp
    sk = abcdefg
    bind_addr = 127.0.0.1
    bind_port = 2022
    use_encryption = true
    use_compression = true

    需要留意的是,访客端是通过server_name来到frps上取连接信息的。

接来下,我们在访客端打开一个终端,执行:

ssh -p 2022 username@127.0.0.1

即可连接到客户端了。因为是直连,所以,这种通信要比普通模式和安全连接模式都快很多。缺点就是,某些网络环境下可能会连接失败。

4. 仪表盘

frps是有一个web仪表盘,可以在web页面上查看frps上面的一些信息。只需要在frps.ini中配置一下:

[common]
...
dashboard_port = 7071 # 端口号
dashboard_user = admin # 用户名
dashboard_pwd = 1qaz-7ujm # 密码
...

然后,就可以在浏览器中查看frps中的各种端口信息了。