最近要重构 Light App Engine的Frp这一块,记录一下Frp自定义认证过程。
相关链接
Frps 服务端配置
[common]
bind_port = 7000
token = your_token
[common]
dashboard_port =7500
dashboard_user = admin
dashboard_pwd = admin
#[plugin.user-manager]
#addr = http://tunnel.test/
#path = handler
#ops = Login
[plugin.port-manager]
addr = http://tunnel.test/
path = handler
ops = NewProxy, Ping
别看有两个plugin,实际上我们只需要用到下面的”port-manager”,”user-manager”如果你有需要,可以加上。
“ops”代表这个插件要发送的”握手(?”,当发生NewProxy时,将POST数据到http://tunnel.test/handler(addr+path)
获取数据
我们先来看一个基本的请求。
当 NewProxy 发生时,后端将收到一个类似下方的请求:
{
"content": {
"user": {
"user": <string>,
"metas": map<string>string
"run_id": <string>
},
"proxy_name": <string>,
"proxy_type": <string>,
"use_encryption": <bool>,
"use_compression": <bool>,
"group": <string>,
"group_key": <string>,
// tcp and udp only
"remote_port": <int>,
// http and https only
"custom_domains": []<string>,
"subdomain": <string>,
"locations": <string>,
"http_user": <string>,
"http_pwd": <string>,
"host_header_rewrite": <string>,
"headers": map<string>string,
// stcp only
"sk": <string>,
// tcpmux only
"multiplexer": <string>
"metas": map<string>string
}
}
不用管那么多,我们只需要下面的这些请求,其余请求我们可以暂时忽略。
- op // 用于判断是什么类型的请求
- content[‘user’] // 代理的用户
- content[‘proxy_name’] // 代理的名称
- content[‘proxy_type’] // 代理协议(tcp/udp之类的)
逻辑与处理
在本质上,我们只需要content[‘proxy_name’]就可以分辨出隧道在哪个服务器,属于哪个用户之类的,因为Frp的隧道名称支持部分符号,比如“server1|user1|tunnel_token”。这样你就可以通过分割字符串,来判断隧道是属于哪个服务器,哪个用户,并且查找隧道的ID来鉴权。
但是
他也是有缺陷的。
如果你要精确到每个用户的流量,包括隧道的心跳(Ping),你需要user字段。
示例客户端配置文件
# 这是你的配置文件,请将它填入frpc.ini
[common]
server_addr = server.test
server_port = 7000
user = 1ec2698b-1080-6a08-af80-5525f8d9c476
token = test_config
# Test项目 的 123 于服务器 server.test
[3|5|1ec2698b-1080-6a08-af80-5525f8d9c476]
type = udp
local_ip = 127.0.0.1
local_port = 80
remote_port = 12415
可能细心的你已经发现了。如果还没有,请看下方。
- “3” 代表 服务器ID
- “5” 代表 平台用户ID
- “1ec2698b-1080-6a08-af80-5525f8d9c476” 代表 代理ID
在实战时,你不必遵循示例配置文件的格式,而是可以自由发挥(不要坑了自己)。
实战代码
完整代码看这里:LAE TunnelController
我将在这里使用Laravel框架,并且假设服务器和模型都存在且正常工作。
获取 op
我们要先通过op来判断是什么类型的请求,这里我们只需要一个NewProxy。
if ($request->op == 'NewProxy') {
// code here
}
我建议加一个判断,防止发生除NewProxy之外的情况,比如恶意请求。
查询代理 ID 是否存在
try {
// 分割字符串 // proxy_type // $request->user['user]
$client = explode('|', $request->content['proxy_name']);
// 0: 服务器ID 1: 代理ID
// $sid = explode('.', $client[0])[1];
$tid = $client[1];
$token = $client[2];
} catch (Exception $e) {
unset($e);
return response()->json([
"reject" => true,
"reject_reason" => "不要啦,这不是正确的信息!",
"unchange" => true,
]);
}
首先,我们先尝试分割proxy_name,因为当用户随意输入时,他将无法解析并抛出异常。
然后依次赋值给变量,方便我们接下来处理。
你可以清晰的看见,当解析失败的时候,将返回一个 JSON 响应,其中 REJECT 为 true,这代表拒绝用户连接隧道,随后frps将会拒绝frpc的连接。
REJECT REASON 是拒绝原因。你可以在这里填写拒绝的理由。
UNCHANGE 一般情况下都要设置为true,这代表不修改用户传入的配置。如果为false,你需要给出修改后的配置。
检查代理信息是否正确
接下来,我们将从数据库中检查信息,并核对frps给出的信息,然后返回结果,最后再决定是否拒绝还是同意客户端连接。
// 检查是否存在
$tunnel_where = $tunnel->where('server_id', $request->route('id'))->where('id', $tid)->with(['project']);
if ($tunnel_where->where('client_token', $token)->exists()) {
// 检查端口之类的是否相等
$tunnel_info = $tunnel_where->firstOrFail();
if ($request->content['proxy_type'] == 'tcp' || $request->content['proxy_type'] == 'udp') {
if ($request->content['proxy_type'] == $tunnel_info->proxy_type || $request->content['remote_port'] != $tunnel_info->remote_port || $tunnel_info->remote_port < 1024) {
return response()->json(array(
"reject" => true,
"reject_reason" => '配置文件错了,请检查配置文件或者到我们的平台重新下载一份。',
"unchange" => true,
));
}
} elseif ($request->content['proxy_type'] == 'http' || $request->content['proxy_type'] == 'https') {
if ($request->content['proxy_type'] == $tunnel_info->proxy_type || $request->content['custom_domains'][0] != $tunnel_info->custom_domain) {
return response()->json(array(
"reject" => true,
"reject_reason" => '可能域名填写错误。',
"unchange" => true,
));
}
}
return response()->json(array(
"reject" => false,
"unchange" => true,
));
} else {
return response()->json(array(
"reject" => true,
"reject_reason" => "代理不存在",
"unchange" => true,
));
}
想必到这边,你可能清楚了整个流程。
总结
验证其实并不是很难,所以我就先写到这里了(要下课了)
这篇文章还没有完善,如有任何问题,你可以在下方评论,我会尽我所能的回复。
😉
awsl~
awsl~