FRP —— 高性能反代工具

前言

折腾过游戏服务器或一些Web服务(例如个人博客)的朋友在最初应该都遇到过一个问题:自己好不容易在本机搭建起来的服务,查到IP和端口后,开开心心将其分享给远方的朋友。结果换来的却是一个无法连接,回过头来还以为是别人的教程写错了。这实际就是一个在当今运营商层层NAT封锁下,个人用户对外提供服务面临的困境局面。

我们知道:

处于NAT环境的设备,其设备主动出站的流量是可以经网关后进入公网环境的。而设备主动访问的对端,也能顺着这条连接找进来给我们回包。从宏观上就是我们本地主动请求服务,位于公网的服务回复数据包向我们提供服务。

但是一旦身份反转,例如外面想主动连接我们的内网设备,就会因为NAT的性质——必须先由内网发起请求并由NAT记录,指定对端的包才能被网关NAT放进来;若没有内网主动发出的请求,则网关根本不知道该把包转给谁,只能直接丢弃。因此NAT死死挡住了这条由外向内的连接,这就是为什么位于不同内网中的两个设备都能上网,但是互相无法直接通信的根因。所以,不同内网设备要想建立通信,直接连接是行不通的。


这里还有一种方法——P2P连接,也就是我们常说的“打洞”。这个原理很简单:由STUN服务器作为中间人,交换双方在各自NAT上的端口映射与IP地址,随后双方同时发包命中对方与STUN维持连接的端口,如果顺利的话那么将能够打出一条直通对方设备的网络通道,届时通信速度将非常快。这也是为什么Radmin这些软件第一策略都是先尝试打洞的原因。

当然打洞不是万能的,甚至在目前来看只会越来越难。NAT有四种形态(讲过太多遍了,此处就不赘述了),其中双方都必须是锥形才有几率成功,但凡有一方是对称NAT那么打洞基本都不可能成。而现在在运营商多层CGNAT的套子下,不管是家宽也好还是什么网络,其出网网关基本都是对称型NAT,打洞很难成功。

既然直连不行,打洞也很大概率失败,那么能供稳定备选的方案就是我们的公网中继手段了,FRP就是其中一个典型例子。其本质就一句话:既然大家互相都找不到对方,那我们去找个中间人牵线搭桥不就行了?


什么是 FRP?

FRP 是一个常用于内网穿透的反向代理工具,反向代理这个概念指的是——代理服务器始终暴露在公网环境,挡在最前面,替背后的真实源站接收外部请求以及搬运数据的。在内网穿透这个场景下,反代多指由外向内打洞建立隧道后再反向复用这么一个模型。

FRP 一个常见的用途是将内网客户端的某个端口映射到公网环境中,从而实现外网对内网的访问。你可以这么理解:frp 是一根”反着插的管子”:公网服务器在明处等客(frps),家里的机器主动挖一条长连接过去(frpc),再把公网端口接进内网——外面的人看到的是公网服务器,数据实际都流进了你家。

再说得直白一些:FRP 其实就是通过在具有公网IP的中间节点上部署服务端,通过两根数据管道(其实准确来说应该是三根,为什么?下文详述)在你的本地服务与外网设备之间来回搬运数据。通过桥接在宏观上实现一种两台内网机之间的顺畅通信。


中继原理

在阐述通信原理之前首先需要明白两个概念:

  • frps(Server):作为“前台”部署在公网侧,负责服务端口暴露(门在哪)、入口流量代理登记(你是谁)、出口流量分发(要去哪)。
  • frpc(Client):作为内网“办事处”,负责搭建通信隧道、心跳保活以及事务处理。

这里Server和CLient的概念与常规意义上的有所区别,一定要分清。此处的frps并不对外提供本质上的服务,它只作为中间人负责两台内网设备之间的通信桥接,也就是说虽然叫Server但是服务器不在它这里!而frpc虽然叫Client,但是具体的服务(比如游戏服务器、语音服务器等)是跑在这里的,由frpc产生具体业务数据后通过frps进而与其它内网设备产生联系。

这你可能会问:为什么不直接把服务跑在云端,这样不就不需要frp技术了吗?理论上这完全成立,但是需要考虑实际成本。对于游戏服务器等一些计算量大的工程,需要云端服务器具备比较高的配置,而云端服务器是非常贵的,我们手头的个人PC普遍配置较高,因此使用frp并不是多此一举,而是对于个人服务提供的一笔经济账。

扯远了,回来。现在来看看frpc+frps这个框架是怎么运作的:

内网frpc客户端首先向公网frps服务端主动发起通信建立通信隧道,而后双方通过共享Token来进行握手验证,若握手失败服务端则主动断开连接;握手成功后,frpc会向frps注册代理——Token握手以及代理注册这些行为是一套frp的应用层私有协议,此处不展开讨论。只需要知道frpc会要求frps监听在指定端口,并会指定代理的协议类型(tcp/udp等)。

  • 关于Token认证:

    Token就是安全密钥,在frps的配置文件中由用户自主进行配置,同时还需要配置frpc端的Token,通信双方的Token必须保持一致。在TCP连接建立后,在frpc侧会主动将自己的Token打包成特定协议格式发往frps服务端,服务端收到后验证密钥是否与自己配置的相同,相同则承认该客户端,若不同则主动断开连接。

  • 为什么需要Token验证?

    因为frps服务端是公网环境,具备公网IP,也就意味着全世界都能连过来。假如没有验证,只要有人扫到你的端口,就可以写一个frpc连接上了,以你的机器为跳板隐藏自身源IP去发动攻击行为。

  • 心跳保活:

    当frpc与frps之间连接正式建立并验证Token之后,frpc客户端会定时发送心跳探测包给云端,云端收到后会回复该包,这就是FRP应用层的Ping-Pong连接保活机制,目的是保证通信隧道正常存活。因为FRP本质上是依赖内网设备主动出网并且在网关NAT会话表上留下的记录来进行双端通信的,而网关NAT会话表是有超时时间的,因此为了保持通道一直存活,在超时之前会通过探测包来刷新时间,维持长连接。

FRP内网穿透实战:Dayz联机中继

前言

最近突然心血来潮打算和朋友玩玩Dayz这款老游戏。这游戏联机模式比较特殊,因为游戏地图大对象多、服务器端运算量十分大,因此并不能像别的大部分联机游戏一样房主创建房间后直连,而是必须自行搭建游戏服务器。

关于服务器搭建,由于我的云端公网服务器配置不够(常规云应用服务器,非专门的游戏服务器),所以服务器进程不能够跑在云端,那么也就只有我本地机来做运算了,服务器开在我的本地。但是这又引入一个问题——服务器跑在本地很简单,因为官方提供了完整的一键启动模板,但是这么一来我的服务器也只能被局域网段看到。也就是说朋友必须得和我处于同一网段才行,这自然是不现实的。于是核心问题就落在了:我本地产生的UDP游戏数据流怎么和远端的朋友主机产生交互。

P2P 打洞落败

为了能够快速玩上,一开始我们决定使用Radmin直接P2P打洞。提到打洞,这里必须涉及到NAT的几种形态:全锥形NAT、受限锥形NAT、端口受限锥形NAT、对称NAT。本篇文章不详细讲述这些NAT形态。

  • 通信双方均为锥形:
    这是P2P打洞最简单的一种,A、B机双方在STUN中转服务器拿到对方各自开在公网网关的端口后,各自互相针对指定端口发个包就能将NAT捅开,成功打洞。

  • 一方锥形、一方对称形/双方对称形:

    这个方式打洞的难度就超级大了,因为对称形NAT只要发现目标的IP/端口不一致,就会采用随机端口映射分发一个随机端口给这个新连接的工作流。而对端拿到的端口是从STUN服务器来的旧端口,这就好比你拿着A门的钥匙想去开B门,B门自然不会认。见下图。

很不幸的是,我们之间有一方正是对称NAT,Radmin打洞无效只能走TURN中继服务器。而Radmin的中继服务器在海外,意味着我们的游戏数据从我本地发出后需要出国兜一圈再回到我朋友手里,那个延迟自然很大,很多时候根本无法正常游戏。


FRP 云端流量中继服务

没办法,便利的方式用不了,自然只能利用手里这个低配云服务器搭一个国内中转了。这里要先简单介绍一下FRP是什么:FRP全称(Fast Reverse Proxy)。快速反向代理工具,最核心的用途就是拿来内网穿透。什么是内网穿透?说白了就是利用一台具有公网IP的设备把你内网设备(不具备公网IP)的某个服务端口暴露到公网环境中。本案例中我的本地主机和朋友的主机都在内网中,打洞失败的情况下,双方不管谁连谁都不可能连上。

FRP原生支持TCP/UDP的代理,这正是我们需要的。只要成功代理,我们本地的流量就可以直接在国内中转,延迟将会大大下降,提高游戏体验。当然这篇文章我只是梳理整个架构,不讲部署。

整个通信流程如下:

  • 本地:游戏服务器跑在本机A,监听在本地127.0.0.1:2302环路。并负责产生原始游戏数据(也就是流通在整个信道上的数据流)。同时本机A也运行一个frp客户端frpc,配置了将流量全部转发到本地的2302端口。frpc启动时会主动去连接云端frps服务,并在token认证成功后,向云端声明:我目前有个udp代理需求,请监听公网2302端口,并将进入该端口的流量全部通过隧道转发给我。
  • 云端:云端frps收到代理声明后,开始监听UDP/2302端口。
  • 数据出站(本地服务器->中转站->对端主机):本地服务器运算完毕并产生数据后,若当前无任何连接,则静默不进行任何动作。若当前有连接,则将数据封装成UDP报文后发回给本地frpc,再由frpc将服务器UDP数据使用TCP(FRP默认使用TCP作为底层隧道协议)进行封装后通过隧道发回给云端。云端frps收到后解包还原出UDP数据,再发给目标主机(朋友们)。
  • 数据入站(朋友们->中转站->本地服务器):当朋友的游戏客户端向云端 中转站公网IP:2302 发送UDP游戏报文后,frps由于在监听云端2302端口,所以会对收到的UDP游戏报文使用TCP进行封装。而后通过反向长连接通道将数据送回给本机A的frpc,frpc将隧道里过来的TCP报文解包还原出UDP游戏数据报文后,路由给本机127.0.0.1:2302,也就是Dayz服务器监听的地址。服务器就这样收到了远端的游戏数据。

其它

这里需要注意两点,首先是Dayz服务器的查询端口。如果你只代理了2302/UDP游戏端口,那么玩家可以通过IP直连进入游戏,但是在游戏登录客户端里面是找不到你服务器的信息的。为了解决这个问题,我们还需要代理27016/UDP(游戏默认服务器查询端口)端口,同时正确设置防火墙和出入站规则。

然后就是关于隧道的传输协议。默认是TCP协议搭建的隧道,除此之外官方也提供了QUIC、KCP等方式来进行传输控制,各有优劣,这里不展开。

Ps:这里聊个题外话——前面我们提到如果什么都不做,那么服务器只能在局域网段运行。如此的话本地网段的主机又是怎么发现我们的呢?

这其实很简单:点击客户端的LAN按钮后,客户端会发送一个基于UDP的Steam A2S信息查询包。该包被广播到网段中其它设备,其它设备底层网络栈收包后,如果没有应用层监听该端口,内核会静默丢弃该包;即便有其他应用误绑了查询端口(例如DayZ默认27016/UDP),由于无法解析A2S协议头,也不会产生响应;只有正确实现了A2S协议的 DayZ服务端,才会解析请求并单播回复服务器元数据。然后我们收到服务器的回复后,在页面上就会显示当前网段有哪个服务器正在运行,以及服务器的一些基础元信息了。


END

音乐
0:000:00