使用frp进行内网穿透

最近想到还有块树莓派在吃灰,今天使用frp折腾了一下内网穿透,把放在家里的树莓派也可以通过外网访问。

前置需求:

  1. 具有公网IP的机器一台(具有公网IP的宽带或者VPS均可)
  2. 域名一个(非必需)

环境介绍

首先我们需要在具有公网IP的机器上配置和启动frp的服务端,我使用的我是自己的VPS,以下涉及frp服务端的操作均在我的VPS上执行,系统环境如下:

1
2
3
4
5
6
root@visionsmile:~# lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 8.9 (jessie)
Release: 8.9
Codename: jessie

下载frp

然后下载frpfrp项目提供了很多平台的二进制程序,可以在这里按需下载。
我这里需要下载的是amd64版本的:

1
$ wget https://github.com/fatedier/frp/releases/download/v0.20.0/frp_0.20.0_linux_amd64.tar.gz

下载完之后解压:

1
2
$ tar -xvzf frp_0.20.0_linux_amd64.tar.gz
$ cd frp_0.20.0_linux_amd64

frp配置和启动

此时目录下具有这样的目录结构:

1
2
root:~/downloads/frp_0.20.0_linux_amd64# ls
frpc frpc_full.ini frpc.ini frps frps_full.ini frps.ini LICENSE

frps是frp的服务端程序,frps.ini是服务端的配置。frps.ini的全部配置选项可以看这里:frps_full.ini.
当前的frp版本的frps.ini的默认配置如下:

1
2
3
$ cat frps.ini
[common]
bind_port = 7000

这里的bind_port既是后面我们使用frpc(frp客户端)连接到服务端的端口,请保证该端口被防火墙(iptables)放行。

注意:如果是阿里云的服务器,需要在网页的安全组里设置放行端口,直接在服务器上使用ufw或者iptables是没有效果的。

在这里我不写太复杂的东西,保持默认即可,更多功能可以去读项目的frp/README

然后就可以启动frp了:

1
$ ./frps -c frps.ini

执行之后如下图:

配置frps在服务器上开机启动

Linux上设置开机启动有两种方法,一种是把启动命令添加到/etc/ec.local中,另一种则是使用systemd创建服务。这里我两种都会介绍一下,但是我推荐使用systemd.

首先先在服务器上将frp拷贝到/etc目录下:

1
2
3
$ sudo mkdir /etc/frp
# 在解压frp的目录下执行
$ sudo cp * /etc/frp

使用systemd

然后新建开机启动的服务,我使用的是systemd来启动frps服务:

1
2
$ sudo cd /etc/systemd/system
$ sudo nono frps.service

将下面内容填入其中:

1
2
3
4
5
6
7
[Unit]
Description=frps daemon
[Service]
Type=idle
ExecStart=/usr/bin/frp/frps -c /usr/bin/frp/frps.ini
[Install]
WantedBy=multi-user.target

保存退出,然后修改权限:

1
$ sudo chmod 755 frps.service

现在就可以启动了:

1
2
3
$ sudo systemctl start frps
# 开机启动frps服务
$ sudo systemctl enable frps

使用/etc/rc.local

因为服务端是被动接收不需要依赖其他的服务启动顺序也可以直接写到了/etc/rc.local中,客户端有点不一样,后面详说。
添加开机启动命令(已经执行过移动frp的目录到/etc/frp):

1
2
3
4
$ sudo nano /etc/rc.local
# 将下面内容填入到exit 0之前
#开机启动frps
nohup /etc/frp/frps -c /etc/frp/frps.ini >/dev/null 2>&1 &

使用域名解析服务端地址(可选)

之所以要使用域名是因为如果我们的服务端出现了问题,客户端直接也没用了,但是我又想如果这台服务器挂掉,我可以在不修改客户端的情况下连接到另一台服务器。
如果在frpc中直接填死服务端的IP地址是没办法做到这一点的,所以我想到的是使用一个域名解析到我想要连接到的服务器地址。
所以,我们使用一个域名(或者二级域名)解析到我们想要连接的服务器地址:比如我在dnspod上将frp.imzlp.com解析A到了服务器XXX.XXX.XXX.XXX。改完之后记得ping一下看是否连通。

与上面下载frp服务端的方式相同,下载frp的客户端。注意下面的命令在客户端机器(raspberry pi)上执行。

frp客户端的默认配置如下:

1
2
3
4
5
6
7
8
9
10
$ cat frpc.ini
[common]
server_addr = 127.0.0.1
server_port = 7000

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

暂时需要修改的是server_addrserver_port,因为上面我使用了默认的端口,所以在这里只需要修改server_addr就可以了。
改为我的自定义域名(如果没有域名则也可以直接指定服务端的IP地址),如下:

1
2
3
4
5
6
7
8
9
10
$ cat frpc.ini
[common]
server_addr = frp.imzlp.com
server_port = 7000

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

[ssh]lable下的local_port的客户端的端口转发到服务端的remote_port端口。也要保证在服务端remote_port被防火墙放行,不然也是连接不上的。
然后就可以在客户机上启动frpc了:

1
$ ./frpc -c frpc.ini

尝试连接内网SSH

通过上面的操作,frps和frpc均已启动,我们可以尝试一下通过互联网连接树莓派的ssh,因为我上面使用了域名的解析,所以我可以直接使用域名:

1
$ ssh -oPort=6000 pi@frp.imzlp.com

注意:要指定ssh连接的服务端端口(即在frpc.ini)中的remote_port参数。不出意外连接成功,如下:

配置frpc在客户端上开机启动

注意:这里是Linux的介绍,Windows的脚本在后面。

1
2
3
# 执行在frp_0.20.0_linux_arm的目录(配置文件均已更改)
$ sudo mkdir /etc/frp
$ sudo cp * /etc/frp

在客户端开机启动我使用的是systemd的服务:

1
2
$ sudo cd /etc/systemd/system
$ sudo nono frpc.service

将下面内容填入其中:

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=frpc daemon

[Service]
Type=idle
ExecStartPre=/bin/sleep 5
ExecStart=/etc/frp/frpc -c /etc/frp/frpc.ini

[Install]
WantedBy=multi-user.target

请特别注意上面脚本中的这两行:

1
2
Type=idle
ExecStartPre=/bin/sleep 5

这两条语句的目的是等待系统初始化完成,如果frp直接在开机时启动会因为链接不上网络(如果是使用域名的可能会因为DNS解析不到)而错误中止。
注意:如果不希望等待,则可以把ExecStartPre去掉,但是要求在frpc的配置中把login_fail_exit设置为false

启动frpc并设置为开机启动:

1
2
$ systemctl start frpc
$ systemctl enable frpc

使用kcp协议

使用过kcp来加速ss肯定会知道,kcp加速会带来更高的传输效率和降低延迟。
frp现在的版本(>v0.12.0)的底层通信协议支持kcp,可以在配置文件中手动启用:
服务端:
在服务端的frps.ini中启用 kcp 协议支持,指定一个 udp 端口用于接收客户端请求:

1
2
3
4
5
# frps.ini
[common]
bind_port = 7000
# kcp 绑定的是 udp 端口,可以和 bind_port 一样
kcp_bind_port = 7000

客户端:
在 frpc.ini 指定需要使用的协议类型,目前只支持 tcp 和 kcp。其他代理配置不需要变更:

1
2
3
4
5
6
# frpc.ini
[common]
server_addr = x.x.x.x
# server_port 指定为 frps 的 kcp_bind_port
server_port = 7000
protocol = kcp

开启加密和流量压缩

有些局域网会做流量的特征识别进行拦截,frp也提供了流量加密和压缩(在frpc.ini中添加):

1
2
use_encryption = true
use_compression = true

使用Mstsc远程连接

在我之前的文章将树莓派打造成便携的Linux编译环境中写到过可以在树莓派上安装xrdp使其可以被windows的mstsc连接。

1
2
# 安装xrdp 使其可以使用mstsc连接
$ sudo apt-get install xrdp

通过局域网连接时这样的:

因为mstsc默认访问的端口时3389,所以在我们想要通过互联网访问时,需要将树莓派(客户端)的3389端口映射到服务器端的端口。
具体操作如下:
编辑客户端frpc.ini,添加一个lable:

1
2
3
4
5
[xvnc]
type=tcp
local_ip=127.0.0.1
local_port=3389
remote_port=6001

代表着就是,将本地的3389端口映射为服务器端的6001端口,保存后重启frpc服务即可。

1
$ systemctl restart frpc

注意:与上面提到的相同,要保证服务器端的6001端口没有被防火墙禁用。
操作完毕之后就可以使用Mstsc在外网链接了(同样我使用域名):

连接上之后就和局域网连接一样了:

登陆进去之后:

同样的方法也可以使用在windows作为客户端的远程连接上,比anydesk或者teamviewer强多了。

十分酸爽,太流畅了,就像操作本地的电脑一样。
还可以使用rdpwrap,使远程连接访问其他账户时不会把本地账户踢出(等同于WindowsServer的功能)。

重新加载配置

frp提供了一种方法从修改的配置文件读取变动并应用代理的更新(这样就不用中止再重新启动了)。
将下面的参数添加到frpc.ini

1
2
3
# frpc.ini
admin_addr = 127.0.0.1
admin_port = 7400

重新加载的命令:

1
frpc reload -c ./frpc.ini

注意:当前的frp版本(v0.20.0)只有start里的内容才会被重新加载,比如server_addr等,所以如果frpc.iniserver_addr有变动,使用reload也并不会重新连接到新的服务器。

我的frp配置文件

1
2
3
4
5
6
#frps.ini
[common]
bind_port = 7000
kcp_bind_port = 7000
token = xxxxxxx
authentication_timeout = 900
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#frpc.ini
[common]
server_addr = frp.imzlp.com
server_port = 7000
protocol = kcp
token = xxxxxxx
login_fail_exit = false
# 避免多个客户端的proxy同名造成的连接失败,在不同的客户端要有不同的userName
user = imzlp

admin_addr = 127.0.0.1
admin_port = 7400

[SSH]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 6000
use_encryption = true
use_compression = true

[XVNC]
type=tcp
local_ip=127.0.0.1
local_port=3389
remote_port=6001
use_encryption = true
use_compression = true

我写了一个py脚本来检测我域名的DNS更新,如果有更新就重启客户端的frpc服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 注意,此脚本不能在windows上用。
import socket
import time
import os
def initFrp():
print("starting frpc service.")
os.system("sudo systemctl restart frpc.service");

def gethostbyname(url="frp.imzlp.com"):
# print(url)
ip=socket.gethostbyname(url);
# print(ip)
return ip

url="frp.imzlp.com"
ip=gethostbyname(url);
initFrp()
ExecCommandIndex=0;
while True:
if(ip!=gethostbyname(url)):
print("Restarting frpc service.");
os.system("sudo systemctl restart frpc.service");
print("frpc service is Restared.");
ip=gethostbyname(url)
++ExecCommandIndex
else:
print("domain frp.imzlp.com DNS ip is",ip)
time.sleep(10)

同样地我把它加入到了systemctl的启动服务里面:

1
2
3
4
5
6
7
8
9
10
11
# /etc/systemd/system/syncfrpcdns.service
[Unit]
Description=Sync frpc Domain Server daemon

[Service]
Type=idle
ExecStartPre=/bin/sleep 10
ExecStart=/usr/bin/python3.4 /etc/frp/SyncFrpServerHost.py

[Install]
WantedBy=multi-user.target

也同样开启开机启动:

1
$ sudo systemctl enable syncfrpcdns.service

上上面的脚本是只能在Linux下跑的,我也写了一个功能相同的可以在Win下跑的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# SyncFrpServerHost.py
# 在Windows上可用
import socket
import time
import os
import subprocess
def GetHotsByName(url="frp.imzlp.com"):
ip=socket.gethostbyname(url);
return ip
def FrpcIsRuning():
findTaskProcess=os.popen('tasklist|find \"frpc.exe\"')
findTaskOut=findTaskProcess.read()
return findTaskOut.find("frpc.exe")!=-1

def StartFrp(frpcDir,frpcIniDir,FrpcStartUpInfo):
frpProcess=subprocess.Popen([frpcDir,"-c",frpcIniDir],stdout = subprocess.PIPE,startupinfo=FrpcStartUpInfo)
while not FrpcIsRuning():
print("Starting Frpc Process Again...")
frpProcess=subprocess.Popen([frpcDir,"-c",frpcIniDir],stdout = subprocess.PIPE,startupinfo=FrpcStartUpInfo)
print("Frpc Process is strting.")
return frpProcess

def main():
frpcDir="C:\\Program Files\\BuildPath\\UserTools\\frp\\frpc.exe"
frpcIniDir="C:\\Program Files\\BuildPath\\UserTools\\frp\\frpc.ini"
frpcDomainUrl="frp.imzlp.com"
UpdateSleepTime=10

# 隐藏启动的fcp窗口
FrpcStartUpInfo=subprocess.STARTUPINFO()
FrpcStartUpInfo.dwFlags = subprocess.CREATE_NEW_CONSOLE | subprocess.STARTF_USESHOWWINDOW
FrpcStartUpInfo.wShowWindow=subprocess.SW_HIDE

if FrpcIsRuning():
print("Restarting Frpc Process...")
os.system("taskkill /f /t /im frpc.exe")
frpProcess=StartFrp(frpcDir,frpcIniDir,FrpcStartUpInfo)
ip=GetHotsByName(frpcDomainUrl);
ExecCommandIndex=0;
while True:
if FrpcIsRuning() is False:
frpProcess=StartFrp(frpcDir,frpcIniDir,FrpcStartUpInfo)
if(ip!=GetHotsByName(frpcDomainUrl)):
frpProcess.kill()
print("Restarting frpc service.");
frpProcess=StartFrp(frpcDir,frpcIniDir,FrpcStartUpInfo)
print("frpc service is Restared.");
ip=GetHotsByName(frpcDomainUrl)
++ExecCommandIndex
else:
print("domain frp.imzlp.com DNS ip is",ip)
time.sleep(UpdateSleepTime)

main()

注意:该脚本可以作为Frpc的开机启动程序,不用再单独设置Frpc的开机的开机启动。最好把frpc.ini中的login_fail_exit设置为false
开机启动且隐藏窗口可以写一个简单的vbs来实现(其实也可以使用pythonw):

1
2
3
DIM objShell
set objShell=wscript.createObject("wscript.shell")
iReturn=objShell.Run("python SyncFrpServerHost.py", 0, TRUE)

将其快捷方式放在C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp下即可。

这样当我切换frp.imzlpe.me的服务器时,客户端检测到DNS变化也会更新连接到新的服务器的。

结语

操作时最重要的一点是要检查端口是否被防火墙放行,因为连接不上很有可能是端口被禁止了而不是frp配置的问题。
frp的更多使用方法可以看这里:frp/README

Update

2018.11.10 16:19

  • 增加服务端上使用systemd来启动frps.
  • 修改部分措辞
  • 增加阿里云的安全组介绍
全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:使用frp进行内网穿透
文章作者:查利鹏
发布时间:2018年06月09日 11时40分
更新时间:2018年11月10日 16时19分
本文字数:本文一共有3.2k字
原始链接:https://imzlp.com/posts/5050/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!