init project
This commit is contained in:
commit
e2d0048f23
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
.rebar3
|
||||
_*
|
||||
.eunit
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
*.swp
|
||||
*.swo
|
||||
.erlang.cookie
|
||||
ebin
|
||||
log
|
||||
erl_crash.dump
|
||||
.rebar
|
||||
logs
|
||||
_build
|
||||
.idea
|
||||
*.iml
|
||||
rebar3.crashdump
|
||||
*~
|
||||
config/sys.config
|
||||
188
API.md
Normal file
188
API.md
Normal file
@ -0,0 +1,188 @@
|
||||
# SDLAN API交互文档
|
||||
|
||||
## 检测客户端版本
|
||||
|
||||
```text
|
||||
url: /api/upgrade
|
||||
method: post
|
||||
params:
|
||||
client_id: string
|
||||
version: int
|
||||
channel: string // 客户端的渠道信息
|
||||
return:
|
||||
{"result": {
|
||||
"upgrade_type": 0, // 升级类型,0: 不升级,1: 普通升级,2: 强制升级
|
||||
"upgrade_prompt": "升级提升语"
|
||||
"upgrade_address": "升级提升语"
|
||||
}}
|
||||
|
||||
{"error": {"code": 1, "message": "错误描述"}}
|
||||
|
||||
```
|
||||
|
||||
## SDL主动请求的接口
|
||||
|
||||
### 1. 获取全部的可用网络信息
|
||||
```text
|
||||
url: /api/get_all_networks
|
||||
method: get
|
||||
return:
|
||||
|
||||
{"result": [8, 9, 10]}
|
||||
|
||||
{"error": {"code": 1, "message": "错误描述"}}
|
||||
|
||||
```
|
||||
|
||||
### 2. 获取单个网络信息
|
||||
```text
|
||||
url: /api/get_network?id=:id
|
||||
method: get
|
||||
return:
|
||||
{"result":
|
||||
{
|
||||
|
||||
"id": 1,
|
||||
"name": "网络1",
|
||||
"ipaddr": "192.168.0.1/24",
|
||||
"owner_id": 1234,
|
||||
"disabled_clients": ["client_id_xyz", "client_id_xyz1"]
|
||||
}
|
||||
}
|
||||
|
||||
{"error": {"code": 1, "message": "错误描述"}}
|
||||
|
||||
```
|
||||
|
||||
### 3.token校验
|
||||
```text
|
||||
url: /api/auth_token
|
||||
method: post
|
||||
params:
|
||||
client_id: string
|
||||
token: string,
|
||||
version: int // 当前客户端版本
|
||||
return:
|
||||
|
||||
{"result":
|
||||
{
|
||||
"network_id": 8,
|
||||
"upgrade_type": 0, // 升级类型,0: 不升级,1: 普通升级,2: 强制升级
|
||||
"upgrade_prompt": "升级提升语"
|
||||
"upgrade_address": "升级提升语"
|
||||
}
|
||||
}
|
||||
|
||||
{"error": {"code": 1, "message": "错误描述"}}
|
||||
code = 1, Token does not exists
|
||||
code = 2, Client Connection Disable
|
||||
```
|
||||
|
||||
### 4.设置节点的状态
|
||||
```text
|
||||
url: /api/set_node_status
|
||||
method: post
|
||||
params:
|
||||
client_id: string
|
||||
network_id: int,
|
||||
ip_addr: string,
|
||||
status: 0 | 1, // 0: 离线,1, 在线
|
||||
return:
|
||||
|
||||
{"result": "success"}
|
||||
```
|
||||
|
||||
### 5. 节点流量汇报(每分钟汇报一次,单位为字节数)
|
||||
```text
|
||||
url: /api/flow_report
|
||||
method: post
|
||||
params:
|
||||
client_id: string
|
||||
network_id: int,
|
||||
forward_num: int,
|
||||
p2p_num: int,
|
||||
inbound_num: int
|
||||
return:
|
||||
|
||||
{"result": "success"}
|
||||
```
|
||||
|
||||
## 管理平台请求SDLAN
|
||||
|
||||
### 网络管理接口
|
||||
|
||||
#### 1. 创建新网络
|
||||
```text
|
||||
url: /network/create
|
||||
method: post
|
||||
params:
|
||||
id: int
|
||||
return:
|
||||
|
||||
{"result": "success"}
|
||||
{"error": {"code": 1, "message": "错误描述"}}
|
||||
|
||||
```
|
||||
|
||||
#### 2. 更新网络信息
|
||||
```text
|
||||
url: /network/reload
|
||||
method: post
|
||||
params:
|
||||
id: int
|
||||
return:
|
||||
|
||||
{"result": "success"}
|
||||
{"error": {"code": 1, "message": "错误描述"}}
|
||||
|
||||
```
|
||||
|
||||
#### 3. 删除网络
|
||||
```text
|
||||
url: /network/delete
|
||||
method: post
|
||||
params:
|
||||
id: int
|
||||
return:
|
||||
|
||||
{"result": "success"}
|
||||
{"error": {"code": 1, "message": "错误描述"}}
|
||||
|
||||
```
|
||||
|
||||
### 客户端节点管理
|
||||
|
||||
#### 1. 禁用节点
|
||||
|
||||
```text
|
||||
url: /node/disable
|
||||
method: post
|
||||
params:
|
||||
network_id: int
|
||||
client_id: int
|
||||
|
||||
return:
|
||||
|
||||
{"result": "success"}
|
||||
{"error": {"code": 1, "message": "错误描述"}}
|
||||
|
||||
```
|
||||
|
||||
#### 2. 迁移到新网络
|
||||
|
||||
```text
|
||||
url: /node/move
|
||||
method: post
|
||||
params:
|
||||
client_id: int
|
||||
to_network_id: int
|
||||
timeout: int
|
||||
|
||||
return:
|
||||
|
||||
{"result": "success"}
|
||||
{"error": {"code": 1, "message": "错误描述"}}
|
||||
|
||||
```
|
||||
|
||||
|
||||
191
LICENSE
Normal file
191
LICENSE
Normal file
@ -0,0 +1,191 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2024, anlicheng <244108715@qq.com>.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
347
Protocol.md
Normal file
347
Protocol.md
Normal file
@ -0,0 +1,347 @@
|
||||
# 协议说明
|
||||
|
||||
## AES加密算法说明(不同网络下的AesKey的值不一样, 服务器端网络启动的时候采用的随机生成的方式)
|
||||
|
||||
```text
|
||||
算法: AES256
|
||||
aesKey: 长度为32个字节
|
||||
iv: 长度为aesKey的前16个字节
|
||||
blockMode: cbc
|
||||
padding: pkcs7Padding
|
||||
```
|
||||
## 1. 客户端与云端的交互同时使用了TCP和UDP协议
|
||||
|
||||
### 1.1 TCP协议基础说明
|
||||
```text
|
||||
协议格式: <<Len:16, PacketId:32, PacketType:8, ProtobufData/binary>>
|
||||
|
||||
Len: tcp数据流采用2个字节长度作为分包协议, Len的长度为后面的二进制的字节数
|
||||
PacketId: 4字节用来标识包ID,用来对应请求和响应; 对于不需要返回值的命令,PacketId的值必须是0
|
||||
PacketType: 1字节命令编码,具体参考后面的说明
|
||||
ProtobufData: 所用的message采用protobuf协议进行编码和解码
|
||||
```
|
||||
### 1.2 UDP协议基础说明
|
||||
|
||||
```text
|
||||
协议格式: <<PacketType:8, ProtobufData/binary>>
|
||||
|
||||
PacketType: 1字节命令编码,具体参考后面的说明
|
||||
ProtobufData: 所用的message采用protobuf协议进行编码和解码
|
||||
```
|
||||
|
||||
## 2. protobuf消息
|
||||
参考文档`message.proto`里面的定义
|
||||
|
||||
## 3. PacketType编码说明
|
||||
|
||||
### 3.1 一级编码
|
||||
```text
|
||||
enum CommandType: UInt8 {
|
||||
// 为了建立完整的请求和响应的对应关系,部分请求没有数据返回时;服务器端返回空数据
|
||||
case empty = 0x00
|
||||
|
||||
case registerSuper = 0x01
|
||||
case registerSuperAck = 0x02
|
||||
case registerSuperNak = 0x04
|
||||
|
||||
case unregisterSuper = 0x05
|
||||
|
||||
case queryInfo = 0x06
|
||||
case peerInfo = 0x07
|
||||
|
||||
// TCP连接需要心跳机制来保持,客户端需要定时向服务器端发送心跳包
|
||||
case ping = 0x08
|
||||
case pong = 0x09
|
||||
|
||||
// 事件类型, 服务器端主动推送到客户端的事件;客户端在收到Event后,不需要向服务端发送Ack
|
||||
case event = 0x10
|
||||
|
||||
// 推送命令消息, 服务器端主动推送到客户端的命令; 需要返回值(管理后台的部分操作需要反馈信息)
|
||||
case command = 0x11
|
||||
case commandAck = 0x12
|
||||
|
||||
// 流量统计, 客户端统计的端上的流量信息;定期上报即可;服务器端收到后没有返回值
|
||||
case flowTracer = 0x15
|
||||
|
||||
// 客户端之间相互打洞
|
||||
case register = 0x20
|
||||
case registerAck = 0x21
|
||||
|
||||
// 客户端通过UDP周期性上报自己的Nat信息;需要依靠该方式保持客户端在Nat的洞不会被Nat设备关闭
|
||||
case stunRequest = 0x30
|
||||
case stunReply = 0x31
|
||||
|
||||
// 客户端通过UDP请求判断自己的Nat类型,并且在stunRequest请求中上报
|
||||
case stunProbe = 0x32
|
||||
case stunProbeReply = 0x33
|
||||
|
||||
// 数据类型
|
||||
case data = 0xFF
|
||||
}
|
||||
```
|
||||
### 3.2 二级编码(Event和Command指令存在二级编码)
|
||||
二级编码占用1个字节长度,紧跟在一级编码的后面,即: <<Len:16, PacketId:32, PacketType:8, 二级编码:8, ProtobufData/binary>>
|
||||
|
||||
```text
|
||||
Event编码
|
||||
|
||||
enum SDLEventType: UInt8 {
|
||||
// 有新的ip加入到当前网络
|
||||
case knownIp = 0x01
|
||||
// ip地址离开当前网络
|
||||
case dropIp = 0x02
|
||||
// ip地址对应的nat信息发生了编码,需要重新打洞
|
||||
case natChanged = 0x03
|
||||
// 需要发送打洞请求
|
||||
case sendRegister = 0x04
|
||||
// 网络关闭
|
||||
case networkShutdown = 0xFF
|
||||
}
|
||||
|
||||
Command编码
|
||||
|
||||
enum SDLCommandType: UInt8 {
|
||||
// 网络地址改变,当node被move的时候网络会发生改变
|
||||
case changeNetwork = 0x01
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 交互说明
|
||||
|
||||
### 4.1 基于公共类型定义
|
||||
```text
|
||||
|
||||
message SDLV4Info {
|
||||
uint32 port = 1;
|
||||
bytes v4 = 2;
|
||||
uint32 nat_type = 3;
|
||||
}
|
||||
|
||||
// ipv6信息,目前未支持!!
|
||||
message SDLV6Info {
|
||||
uint32 port = 1;
|
||||
bytes v6 = 2;
|
||||
}
|
||||
|
||||
// 设备网络地址信息
|
||||
message SDLDevAddr {
|
||||
uint32 network_id = 1;
|
||||
uint32 net_addr = 2;
|
||||
uint32 net_bit_len = 3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 5. TCP交互
|
||||
|
||||
### 5.1 客户端建立到服务端后,需要先发送RegisterSuper消息
|
||||
由于时基于tcp长连接方式,因此理论上一个连接上只需要请求一次;服务器端会绑定相关信息
|
||||
```text
|
||||
请求:
|
||||
|
||||
message SDLRegisterSuper {
|
||||
uint32 version = 1;
|
||||
string installed_channel = 2;
|
||||
string client_id = 3;
|
||||
SDLDevAddr dev_addr = 4;
|
||||
string pub_key = 5;
|
||||
string token = 6;
|
||||
}
|
||||
|
||||
响应:
|
||||
|
||||
// 服务器验证通过后返回
|
||||
message SDLRegisterSuperAck {
|
||||
SDLDevAddr dev_addr = 1;
|
||||
bytes aes_key = 2;
|
||||
bytes known_ips = 3;
|
||||
uint32 upgrade_type = 4;
|
||||
optional string upgrade_prompt = 5;
|
||||
optional string upgrade_address = 6;
|
||||
}
|
||||
|
||||
// 服务验证失败返回
|
||||
message SDLRegisterSuperNak {
|
||||
uint32 error_code = 1;
|
||||
string error_message = 2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 5.2 查询ip对应的PeerInfo
|
||||
```text
|
||||
请求:
|
||||
message SDLQueryInfo {
|
||||
uint32 dst_ip = 1;
|
||||
}
|
||||
|
||||
响应:
|
||||
成功:
|
||||
|
||||
// 目前未支持ipv6,因此SDLV6Info的值为空
|
||||
message SDLPeerInfo {
|
||||
SDLV4Info v4_info = 1;
|
||||
optional SDLV6Info v6_info = 2;
|
||||
}
|
||||
|
||||
失败:
|
||||
返回empty: <<Len:16, PacketId:32, 0x00>>
|
||||
```
|
||||
|
||||
### 5.3 客户端主动发起Ping
|
||||
服务器端无返回, 服务器在15秒内没有收到任何ping包;会关闭掉当前的tcp连接
|
||||
```text
|
||||
请求:
|
||||
<<0:32, 0x08>>
|
||||
```
|
||||
|
||||
### 5.4 主动上报当前节点的流量信息
|
||||
服务器端无返回, 请求是的packetId值必须是: 0
|
||||
```text
|
||||
请求:
|
||||
message SDLFlows {
|
||||
// 服务器转发流量
|
||||
uint32 forward_num = 1;
|
||||
// p2p直接流量
|
||||
uint32 p2p_num = 2;
|
||||
// 接收的流量
|
||||
uint32 inbound_num = 3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 5.5 命令下发
|
||||
```text
|
||||
消息格式: <<Len:16, PacketId:32, 0x11:8, Command子类型:8, ProtobufOfEvent/binary>>
|
||||
```
|
||||
|
||||
### 5.6 命令回复
|
||||
注意Ack里面的PacketId的值必须和下发命令时的PacketId值一致
|
||||
```text
|
||||
消息格式: <<Len:16, PacketId:32, 0x12:8, ProtobufOfEventAck/binary>>
|
||||
```
|
||||
|
||||
### 5.7 Event下发
|
||||
客户端在收到Event后,不需要回复Ack信息
|
||||
```text
|
||||
消息格式: <<Len:16, 0:32, 0x10:8, Event子类型:8, ProtobufOfEvent/binary>>
|
||||
```
|
||||
|
||||
### 5.8 Unregister取消注册
|
||||
无返回,服务器端收到后会关闭掉当前连接
|
||||
```text
|
||||
请求:
|
||||
<<0:32, 0x05>>
|
||||
```
|
||||
|
||||
## 6. UDP交互
|
||||
|
||||
### 6.1 StunRequest请求(10s发送一次)
|
||||
```text
|
||||
请求:
|
||||
message SDLStunRequest {
|
||||
uint32 cookie = 1;
|
||||
string client_id = 2;
|
||||
uint32 network_id = 3;
|
||||
uint32 ip = 4;
|
||||
uint32 nat_type = 5;
|
||||
}
|
||||
|
||||
响应:
|
||||
message SDLStunReply {
|
||||
uint32 cookie = 1;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 StunProbe请求
|
||||
|
||||
```text
|
||||
请求:
|
||||
message SDLStunProbe {
|
||||
uint32 cookie = 1;
|
||||
uint32 attr = 2;
|
||||
}
|
||||
|
||||
响应:
|
||||
message SDLStunProbeReply {
|
||||
uint32 cookie = 1;
|
||||
uint32 port = 2;
|
||||
uint32 ip = 3;
|
||||
}
|
||||
|
||||
Attr值的说明:
|
||||
enum SDLProbeAttr: UInt8 {
|
||||
// 正常响应
|
||||
case none = 0
|
||||
// 服务器在收到消息,用相同IP地址,但是Port不相同的Socket响应
|
||||
case port = 1
|
||||
// 服务器在收到消息,同时改变IP地址和Port的Socket响应
|
||||
case peer = 2
|
||||
}
|
||||
|
||||
Nat类型说明:
|
||||
enum NatType: UInt8, Encodable {
|
||||
case blocked = 0 // 网络不通
|
||||
case noNat = 1 // 当前设备在公网IP下
|
||||
case fullCone = 2 // 完全对称型Nat
|
||||
case portRestricted = 3 // 端口限制型
|
||||
case coneRestricted = 4 // Ip限制型
|
||||
case symmetric = 5 // 完全对称型
|
||||
}
|
||||
Nat类型的判断逻辑
|
||||
func getNatType() async -> NatType {
|
||||
let addressArray = config.stunProbeSocketAddressArray
|
||||
// step1: ip1:port1 <---- ip1:port1
|
||||
guard let natAddress1 = await getNatAddress(remoteAddress: addressArray[0][0], attr: .none) else {
|
||||
return .blocked
|
||||
}
|
||||
|
||||
// 网络没有在nat下
|
||||
if natAddress1 == self.udpHole?.localAddress {
|
||||
return .noNat
|
||||
}
|
||||
|
||||
// step2: ip2:port2 <---- ip2:port2
|
||||
guard let natAddress2 = await getNatAddress(remoteAddress: addressArray[1][1], attr: .none) else {
|
||||
return .blocked
|
||||
}
|
||||
|
||||
// 如果natAddress2 的IP地址与上次回来的IP是不一样的,它就是对称型NAT; 这次的包也一定能发成功并收到
|
||||
// 如果ip地址变了,这说明{dstIp, dstPort, srcIp, srcPort}, 其中有一个变了;则用新的ip地址
|
||||
NSLog("nat_address1: \(natAddress1), nat_address2: \(natAddress2)")
|
||||
if let ipAddress1 = natAddress1.ipAddress, let ipAddress2 = natAddress2.ipAddress, ipAddress1 != ipAddress2 {
|
||||
return .symmetric
|
||||
}
|
||||
|
||||
// step3: ip1:port1 <---- ip2:port2 (ip地址和port都变的情况)
|
||||
// 如果能收到的,说明是完全锥形 说明是IP地址限制锥型NAT,如果不能收到说明是端口限制锥型。
|
||||
if let natAddress3 = await getNatAddress(remoteAddress: addressArray[0][0], attr: .peer) {
|
||||
NSLog("nat_address1: \(natAddress1), nat_address2: \(natAddress2), nat_address3: \(natAddress3)")
|
||||
return .fullCone
|
||||
}
|
||||
|
||||
// step3: ip1:port1 <---- ip1:port2 (port改变情况)
|
||||
// 如果能收到的说明是IP地址限制锥型NAT,如果不能收到说明是端口限制锥型。
|
||||
if let natAddress4 = await getNatAddress(remoteAddress: addressArray[0][0], attr: .port) {
|
||||
NSLog("nat_address1: \(natAddress1), nat_address2: \(natAddress2), nat_address4: \(natAddress4)")
|
||||
return .coneRestricted
|
||||
} else {
|
||||
return .portRestricted
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 StunData请求
|
||||
```text
|
||||
消息体: 其中只有data字段里面的数据使用了aes加密
|
||||
|
||||
message SDLData {
|
||||
uint32 network_id = 1;
|
||||
uint32 src_ip = 2;
|
||||
uint32 dst_ip = 3;
|
||||
bool is_p2p = 4;
|
||||
uint32 ttl = 5;
|
||||
bytes data = 6;
|
||||
}
|
||||
|
||||
```
|
||||
13
README.md
Normal file
13
README.md
Normal file
@ -0,0 +1,13 @@
|
||||
sdlan
|
||||
=====
|
||||
|
||||
An OTP application
|
||||
|
||||
## proto文件的编译
|
||||
目录: /usr/local/code/tmp/gpb
|
||||
bin/protoc-erl -I . -rename msg_name:snake_case -strbin sdlan_pb.proto
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
$ rebar3 compile
|
||||
3
TODO.md
Normal file
3
TODO.md
Normal file
@ -0,0 +1,3 @@
|
||||
# 需要完善的事情
|
||||
|
||||
## 后端网络需要增加一个aes_key,采用AES256加密算法,key的长度为32个字符
|
||||
83
apps/sdlan/include/sdlan.hrl
Normal file
83
apps/sdlan/include/sdlan.hrl
Normal file
@ -0,0 +1,83 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 09. 3月 2024 14:53
|
||||
%%%-------------------------------------------------------------------
|
||||
-author("anlicheng").
|
||||
|
||||
%% 定义version
|
||||
-define(VERSION_1, 1).
|
||||
|
||||
-define(DEFAULT_PASS, <<"`encrypt!`">>).
|
||||
|
||||
-define(PACKET_EMPTY, 16#00).
|
||||
-define(PACKET_REGISTER_SUPER, 16#01).
|
||||
-define(PACKET_REGISTER_SUPER_ACK, 16#02).
|
||||
-define(PACKET_REGISTER_SUPER_ACKNOWLEDGE, 16#03).
|
||||
-define(PACKET_REGISTER_SUPER_NAK, 16#04).
|
||||
-define(PACKET_UNREGISTER, 16#05).
|
||||
|
||||
%% 信息查询
|
||||
-define(PACKET_QUERY_INFO, 16#06).
|
||||
-define(PACKET_PEER_INFO, 16#07).
|
||||
|
||||
%% 心跳机制
|
||||
-define(PACKET_PING, 16#08).
|
||||
-define(PACKET_PONG, 16#09).
|
||||
|
||||
%% 推送的事件信息, 不需要返回值
|
||||
-define(PACKET_EVENT, 16#10).
|
||||
|
||||
%% 定义事件信息
|
||||
-define(PACKET_EVENT_KNOWN_IP, 16#01).
|
||||
-define(PACKET_EVENT_DROP_IP, 16#02).
|
||||
-define(PACKET_EVENT_NAT_CHANGED, 16#03).
|
||||
-define(PACKET_EVENT_SEND_REGISTER, 16#04).
|
||||
|
||||
%% 网络关闭
|
||||
-define(PACKET_EVENT_NETWORK_SHUTDOWN, 16#FF).
|
||||
|
||||
%% 推送命令信息, 需要等待返回值
|
||||
-define(PACKET_COMMAND, 16#11).
|
||||
-define(PACKET_COMMAND_ACK, 16#12).
|
||||
|
||||
%% 网络发生变化
|
||||
-define(PACKET_COMMAND_CHANGE_NETWORK, 16#01).
|
||||
-define(PACKET_COMMAND_UPGRADE, 16#02).
|
||||
|
||||
%% 网络流量统计
|
||||
-define(PACKET_FLOW_TRACER, 16#15).
|
||||
|
||||
-define(PACKET_REGISTER, 16#20).
|
||||
-define(PACKET_REGISTER_ACK, 16#21).
|
||||
|
||||
%% stun相关的请求
|
||||
|
||||
%% 请求
|
||||
-define(PACKET_STUN_REQUEST, 16#30).
|
||||
%% 响应
|
||||
-define(PACKET_STUN_REPLY, 16#31).
|
||||
|
||||
%% stun网络类型检测
|
||||
%% 请求
|
||||
-define(PACKET_STUN_PROBE, 16#32).
|
||||
%% 响应
|
||||
-define(PACKET_STUN_PROBE_REPLY, 16#33).
|
||||
%% stun消息转发
|
||||
-define(PACKET_STUN_PROBE_RELAY, 16#3a).
|
||||
|
||||
%% stun请求的attr
|
||||
-define(STUN_ATTR_CHANGE_NONE, 0).
|
||||
-define(STUN_ATTR_CHANGE_PORT, 1).
|
||||
-define(STUN_ATTR_CHANGE_PEER, 2).
|
||||
|
||||
%% 数据转发
|
||||
-define(PACKET_STUN_DATA, 16#FF).
|
||||
|
||||
-record(id_generator, {
|
||||
tab :: atom(),
|
||||
increment_id = 0 :: integer()
|
||||
}).
|
||||
210
apps/sdlan/include/sdlan_pb.hrl
Normal file
210
apps/sdlan/include/sdlan_pb.hrl
Normal file
@ -0,0 +1,210 @@
|
||||
%% -*- coding: utf-8 -*-
|
||||
%% Automatically generated, do not edit
|
||||
%% Generated by gpb_compile version 4.21.1
|
||||
|
||||
-ifndef(sdlan_pb).
|
||||
-define(sdlan_pb, true).
|
||||
|
||||
-define(sdlan_pb_gpb_version, "4.21.1").
|
||||
|
||||
|
||||
-ifndef('SDLV_4_INFO_PB_H').
|
||||
-define('SDLV_4_INFO_PB_H', true).
|
||||
-record(sdl_v4_info,
|
||||
{port = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
v4 = <<>> :: iodata() | undefined, % = 2, optional
|
||||
nat_type = 0 :: non_neg_integer() | undefined % = 3, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLV_6_INFO_PB_H').
|
||||
-define('SDLV_6_INFO_PB_H', true).
|
||||
-record(sdl_v6_info,
|
||||
{port = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
v6 = <<>> :: iodata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_DEV_ADDR_PB_H').
|
||||
-define('SDL_DEV_ADDR_PB_H', true).
|
||||
-record(sdl_dev_addr,
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
mac = <<>> :: iodata() | undefined, % = 2, optional
|
||||
net_addr = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
net_bit_len = 0 :: non_neg_integer() | undefined % = 4, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_EMPTY_PB_H').
|
||||
-define('SDL_EMPTY_PB_H', true).
|
||||
-record(sdl_empty,
|
||||
{
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_REGISTER_SUPER_PB_H').
|
||||
-define('SDL_REGISTER_SUPER_PB_H', true).
|
||||
-record(sdl_register_super,
|
||||
{version = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
installed_channel = <<>> :: unicode:chardata() | undefined, % = 2, optional
|
||||
client_id = <<>> :: unicode:chardata() | undefined, % = 3, optional
|
||||
dev_addr = undefined :: sdlan_pb:sdl_dev_addr() | undefined, % = 4, optional
|
||||
pub_key = <<>> :: unicode:chardata() | undefined, % = 5, optional
|
||||
token = <<>> :: unicode:chardata() | undefined % = 6, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_REGISTER_SUPER_ACK_PB_H').
|
||||
-define('SDL_REGISTER_SUPER_ACK_PB_H', true).
|
||||
-record(sdl_register_super_ack,
|
||||
{dev_addr = undefined :: sdlan_pb:sdl_dev_addr() | undefined, % = 1, optional
|
||||
aes_key = <<>> :: iodata() | undefined, % = 2, optional
|
||||
upgrade_type = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
upgrade_prompt :: unicode:chardata() | undefined, % = 4, optional
|
||||
upgrade_address :: unicode:chardata() | undefined % = 5, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_REGISTER_SUPER_NAK_PB_H').
|
||||
-define('SDL_REGISTER_SUPER_NAK_PB_H', true).
|
||||
-record(sdl_register_super_nak,
|
||||
{error_code = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
error_message = <<>> :: unicode:chardata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_QUERY_INFO_PB_H').
|
||||
-define('SDL_QUERY_INFO_PB_H', true).
|
||||
-record(sdl_query_info,
|
||||
{dst_mac = <<>> :: iodata() | undefined % = 1, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_PEER_INFO_PB_H').
|
||||
-define('SDL_PEER_INFO_PB_H', true).
|
||||
-record(sdl_peer_info,
|
||||
{dst_mac = <<>> :: iodata() | undefined, % = 1, optional
|
||||
v4_info = undefined :: sdlan_pb:sdl_v4_info() | undefined, % = 2, optional
|
||||
v6_info :: sdlan_pb:sdl_v6_info() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_NAT_CHANGED_EVENT_PB_H').
|
||||
-define('SDL_NAT_CHANGED_EVENT_PB_H', true).
|
||||
-record(sdl_nat_changed_event,
|
||||
{mac = <<>> :: iodata() | undefined, % = 1, optional
|
||||
ip = 0 :: non_neg_integer() | undefined % = 2, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_SEND_REGISTER_EVENT_PB_H').
|
||||
-define('SDL_SEND_REGISTER_EVENT_PB_H', true).
|
||||
-record(sdl_send_register_event,
|
||||
{dst_mac = <<>> :: iodata() | undefined, % = 1, optional
|
||||
nat_ip = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
nat_port = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
nat_type = 0 :: non_neg_integer() | undefined, % = 4, optional, 32 bits
|
||||
v6_info :: sdlan_pb:sdl_v6_info() | undefined % = 5, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_NETWORK_SHUTDOWN_EVENT_PB_H').
|
||||
-define('SDL_NETWORK_SHUTDOWN_EVENT_PB_H', true).
|
||||
-record(sdl_network_shutdown_event,
|
||||
{message = <<>> :: unicode:chardata() | undefined % = 1, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_CHANGE_NETWORK_COMMAND_PB_H').
|
||||
-define('SDL_CHANGE_NETWORK_COMMAND_PB_H', true).
|
||||
-record(sdl_change_network_command,
|
||||
{dev_addr = undefined :: sdlan_pb:sdl_dev_addr() | undefined, % = 1, optional
|
||||
aes_key = <<>> :: iodata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_COMMAND_ACK_PB_H').
|
||||
-define('SDL_COMMAND_ACK_PB_H', true).
|
||||
-record(sdl_command_ack,
|
||||
{status = false :: boolean() | 0 | 1 | undefined, % = 1, optional
|
||||
message :: unicode:chardata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_FLOWS_PB_H').
|
||||
-define('SDL_FLOWS_PB_H', true).
|
||||
-record(sdl_flows,
|
||||
{forward_num = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
p2p_num = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
inbound_num = 0 :: non_neg_integer() | undefined % = 3, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_STUN_REQUEST_PB_H').
|
||||
-define('SDL_STUN_REQUEST_PB_H', true).
|
||||
-record(sdl_stun_request,
|
||||
{cookie = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
client_id = <<>> :: unicode:chardata() | undefined, % = 2, optional
|
||||
network_id = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
mac = <<>> :: iodata() | undefined, % = 4, optional
|
||||
ip = 0 :: non_neg_integer() | undefined, % = 5, optional, 32 bits
|
||||
nat_type = 0 :: non_neg_integer() | undefined, % = 6, optional, 32 bits
|
||||
v6_info :: sdlan_pb:sdl_v6_info() | undefined % = 7, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_STUN_REPLY_PB_H').
|
||||
-define('SDL_STUN_REPLY_PB_H', true).
|
||||
-record(sdl_stun_reply,
|
||||
{cookie = 0 :: non_neg_integer() | undefined % = 1, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_DATA_PB_H').
|
||||
-define('SDL_DATA_PB_H', true).
|
||||
-record(sdl_data,
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
src_mac = <<>> :: iodata() | undefined, % = 2, optional
|
||||
dst_mac = <<>> :: iodata() | undefined, % = 3, optional
|
||||
is_p2p = false :: boolean() | 0 | 1 | undefined, % = 4, optional
|
||||
ttl = 0 :: non_neg_integer() | undefined, % = 5, optional, 32 bits
|
||||
data = <<>> :: iodata() | undefined % = 6, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_REGISTER_PB_H').
|
||||
-define('SDL_REGISTER_PB_H', true).
|
||||
-record(sdl_register,
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
src_mac = <<>> :: iodata() | undefined, % = 2, optional
|
||||
dst_mac = <<>> :: iodata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_REGISTER_ACK_PB_H').
|
||||
-define('SDL_REGISTER_ACK_PB_H', true).
|
||||
-record(sdl_register_ack,
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
src_mac = <<>> :: iodata() | undefined, % = 2, optional
|
||||
dst_mac = <<>> :: iodata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_STUN_PROBE_PB_H').
|
||||
-define('SDL_STUN_PROBE_PB_H', true).
|
||||
-record(sdl_stun_probe,
|
||||
{cookie = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
attr = 0 :: non_neg_integer() | undefined % = 2, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_STUN_PROBE_REPLY_PB_H').
|
||||
-define('SDL_STUN_PROBE_REPLY_PB_H', true).
|
||||
-record(sdl_stun_probe_reply,
|
||||
{cookie = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
port = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
ip = 0 :: non_neg_integer() | undefined % = 3, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-endif.
|
||||
18
apps/sdlan/include/sdlan_tables.hrl
Normal file
18
apps/sdlan/include/sdlan_tables.hrl
Normal file
@ -0,0 +1,18 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 20. 1月 2025 21:35
|
||||
%%%-------------------------------------------------------------------
|
||||
-author("anlicheng").
|
||||
|
||||
%% ip的使用信息
|
||||
-record(client, {
|
||||
client_id :: binary(),
|
||||
mac :: binary(),
|
||||
ip :: integer(),
|
||||
%% 当前状态
|
||||
status = normal :: normal | disabled
|
||||
}).
|
||||
34
apps/sdlan/src/http_handler/api_handler.erl
Normal file
34
apps/sdlan/src/http_handler/api_handler.erl
Normal file
@ -0,0 +1,34 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 26. 4月 2020 3:36 下午
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(api_handler).
|
||||
-author("licheng5").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
%% 重新加载对应的主机信息
|
||||
handle_request("POST", "/test/auth_token", _, PostParams) ->
|
||||
lager:debug("[test_handler] get post params: ~p", [PostParams]),
|
||||
[Id | _] = network_bo:get_all_networks(),
|
||||
Data = #{
|
||||
<<"network_id">> => Id
|
||||
},
|
||||
{ok, 200, sdlan_util:json_data(Data)};
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
24
apps/sdlan/src/http_handler/file_handler.erl
Normal file
24
apps/sdlan/src/http_handler/file_handler.erl
Normal file
@ -0,0 +1,24 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 09. 4月 2024 14:28
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(file_handler).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, State) ->
|
||||
%% 拼接得到文件的真实路径
|
||||
FullPath = "/tmp/files/test.dmg",
|
||||
%% 使用cowboy_req:reply函数返回文件内容
|
||||
{ok, Content} = file:read_file(FullPath),
|
||||
Req1 = cowboy_req:reply(200, #{
|
||||
<<"Content-Type">> => <<"application/octet-stream">>
|
||||
}, Content, Req),
|
||||
|
||||
{ok, Req1, State}.
|
||||
86
apps/sdlan/src/http_handler/http_protocol.erl
Normal file
86
apps/sdlan/src/http_handler/http_protocol.erl
Normal file
@ -0,0 +1,86 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 26. 4月 2020 3:36 下午
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(http_protocol).
|
||||
-author("licheng5").
|
||||
|
||||
%% API
|
||||
-export([init/2]).
|
||||
|
||||
init(Req0, Opts = [Mod|_]) ->
|
||||
Method = binary_to_list(cowboy_req:method(Req0)),
|
||||
Path = binary_to_list(cowboy_req:path(Req0)),
|
||||
GetParams0 = cowboy_req:parse_qs(Req0),
|
||||
GetParams = maps:from_list(GetParams0),
|
||||
{ok, PostParams, Req1} = parse_body(Req0),
|
||||
|
||||
try Mod:handle_request(Method, Path, GetParams, PostParams) of
|
||||
{ok, StatusCode, Resp} ->
|
||||
%lager:debug("[http_protocol] request path: ~p, get_params: ~p, post_params: ~p, response: ~ts",
|
||||
% [Path, GetParams, PostParams, Resp]),
|
||||
AcceptEncoding = cowboy_req:header(<<"accept-encoding">>, Req1, <<>>),
|
||||
Req2 = case iolist_size(Resp) >= 1024 andalso supported_gzip(AcceptEncoding) of
|
||||
true ->
|
||||
Resp1 = zlib:gzip(Resp),
|
||||
cowboy_req:reply(StatusCode, #{
|
||||
<<"Content-Type">> => <<"application/json;charset=utf-8">>,
|
||||
<<"Content-Encoding">> => <<"gzip">>
|
||||
}, Resp1, Req1);
|
||||
false ->
|
||||
cowboy_req:reply(StatusCode, #{
|
||||
<<"Content-Type">> => <<"application/json;charset=utf-8">>
|
||||
}, Resp, Req1)
|
||||
end,
|
||||
{ok, Req2, Opts}
|
||||
catch
|
||||
throw:Error ->
|
||||
Req2 = cowboy_req:reply(404, #{
|
||||
<<"Content-Type">> => <<"application/json;charset=utf-8">>
|
||||
}, Error, Req1),
|
||||
{ok, Req2, Opts};
|
||||
_:Error:Stack ->
|
||||
lager:warning("[http_handler] get error: ~p, stack: ~p", [Error, Stack]),
|
||||
Req2 = cowboy_req:reply(500, #{
|
||||
<<"Content-Type">> => <<"text/html;charset=utf-8">>
|
||||
}, <<"Internal Server Error">>, Req1),
|
||||
{ok, Req2, Opts}
|
||||
end.
|
||||
|
||||
%% 判断是否支持gzip
|
||||
supported_gzip(AcceptEncoding) when is_binary(AcceptEncoding) ->
|
||||
binary:match(AcceptEncoding, <<"gzip">>) =/= nomatch.
|
||||
|
||||
parse_body(Req0) ->
|
||||
ContentType = cowboy_req:header(<<"content-type">>, Req0),
|
||||
case ContentType of
|
||||
<<"application/json", _/binary>> ->
|
||||
{ok, Body, Req1} = read_body(Req0),
|
||||
case Body /= <<>> of
|
||||
true ->
|
||||
{ok, catch jiffy:decode(Body, [return_maps]), Req1};
|
||||
false ->
|
||||
{ok, #{}, Req1}
|
||||
end;
|
||||
<<"application/x-www-form-urlencoded">> ->
|
||||
{ok, PostParams0, Req1} = cowboy_req:read_urlencoded_body(Req0),
|
||||
PostParams = maps:from_list(PostParams0),
|
||||
{ok, PostParams, Req1};
|
||||
_ ->
|
||||
{ok, #{}, Req0}
|
||||
end.
|
||||
|
||||
%% 读取请求体
|
||||
read_body(Req) ->
|
||||
read_body(Req, <<>>).
|
||||
read_body(Req, AccData) ->
|
||||
case cowboy_req:read_body(Req) of
|
||||
{ok, Data, Req1} ->
|
||||
{ok, <<AccData/binary, Data/binary>>, Req1};
|
||||
{more, Data, Req1} ->
|
||||
read_body(Req1, <<AccData/binary, Data/binary>>)
|
||||
end.
|
||||
61
apps/sdlan/src/http_handler/network_handler.erl
Normal file
61
apps/sdlan/src/http_handler/network_handler.erl
Normal file
@ -0,0 +1,61 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 09. 4月 2024 14:28
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(network_handler).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
handle_request("POST", "/network/create", _, #{<<"id">> := NetworkId}) when NetworkId > 0 ->
|
||||
case sdlan_network_sup:ensured_network_started(NetworkId) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, 200, sdlan_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:debug("[network_handler] create network: ~p, get error: ~p", [NetworkId, Reason]),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"error">>)}
|
||||
end;
|
||||
|
||||
handle_request("POST", "/network/reload", _, #{<<"id">> := NetworkId}) when NetworkId > 0 ->
|
||||
case sdlan_network:get_pid(NetworkId) of
|
||||
undefined ->
|
||||
case sdlan_network_sup:start_network(NetworkId) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
sdlan_network_sup:reallocate_bind_width(),
|
||||
{ok, 200, sdlan_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:debug("[network_handler] start network: ~p, get error: ~p", [NetworkId, Reason]),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"error">>)}
|
||||
end;
|
||||
NetworkPid when is_pid(NetworkPid) ->
|
||||
case sdlan_network:reload(NetworkPid) of
|
||||
ok ->
|
||||
{ok, 200, sdlan_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:debug("[network_handler] reload network: ~p, get error: ~p", [NetworkId, Reason]),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"error">>)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request("POST", "/network/delete", _, #{<<"id">> := NetworkId}) when NetworkId > 0 ->
|
||||
case sdlan_network:get_pid(NetworkId) of
|
||||
undefined ->
|
||||
{ok, 200, sdlan_util:json_data(<<"success">>)};
|
||||
NetworkPid when is_pid(NetworkPid) ->
|
||||
case sdlan_network_sup:delete_network(NetworkId) of
|
||||
ok ->
|
||||
{ok, 200, sdlan_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:debug("[network_handler] delete network: ~p, get error: ~p", [NetworkId, Reason]),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"error">>)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
72
apps/sdlan/src/http_handler/node_handler.erl
Normal file
72
apps/sdlan/src/http_handler/node_handler.erl
Normal file
@ -0,0 +1,72 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 09. 4月 2024 14:28
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(node_handler).
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
-include("sdlan_pb.hrl").
|
||||
-include("sdlan_tables.hrl").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
handle_request("POST", "/node/list", _, #{<<"network_id">> := NetworkId}) when NetworkId > 0 ->
|
||||
Pid = sdlan_network:get_pid(NetworkId),
|
||||
UsedMap = sdlan_network:get_used_map(Pid),
|
||||
|
||||
Clients = client_model:get_clients(NetworkId),
|
||||
ClientInfos = lists:map(fun(#client{client_id = ClientId, mac = Mac, ip = Ip, status = Status}) ->
|
||||
Info = #{
|
||||
<<"client_id">> => ClientId,
|
||||
<<"mac">> => Mac,
|
||||
<<"ip">> => sdlan_ipaddr:int_to_ipv4(Ip),
|
||||
<<"status">> => atom_to_binary(Status)
|
||||
},
|
||||
maps:merge(Info, maps:get(Mac, UsedMap, #{}))
|
||||
end, Clients),
|
||||
|
||||
{ok, 200, sdlan_util:json_data(ClientInfos)};
|
||||
|
||||
handle_request("POST", "/node/disable", _, #{<<"network_id">> := NetworkId, <<"client_id">> := ClientId}) when NetworkId > 0 ->
|
||||
case sdlan_network:get_pid(NetworkId) of
|
||||
undefined ->
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"network not found">>)};
|
||||
Pid ->
|
||||
sdlan_network:disable_client(Pid, ClientId),
|
||||
{ok, 200, sdlan_util:json_data(<<"success">>)}
|
||||
end;
|
||||
|
||||
handle_request("POST", "/node/move", _, #{<<"client_id">> := ClientId, <<"from_network_id">> := FromNetworkId, <<"to_network_id">> := ToNetworkId, <<"timeout">> := Timeout}) ->
|
||||
case {sdlan_network:get_pid(FromNetworkId), sdlan_network:get_pid(ToNetworkId)} of
|
||||
{FromPid, ToPid} when is_pid(FromPid), is_pid(ToPid) ->
|
||||
case sdlan_network:dropout_client(FromPid, ClientId) of
|
||||
{ok, ChannelPid} ->
|
||||
Ref = sdlan_channel:move_network(ChannelPid, self(), ToPid),
|
||||
receive
|
||||
{command_reply, Ref, {error, Reason}} ->
|
||||
lager:warning("[node_handler] client_id: ~p, move network from: ~p, to: ~p, get error: ~p", [ClientId, FromPid, ToPid, Reason]),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"move failed">>)};
|
||||
{command_reply, Ref, #sdl_command_ack{status = true}} ->
|
||||
{ok, 200, sdlan_util:json_data(<<"success">>)};
|
||||
{command_reply, Ref, #sdl_command_ack{status = false, message = ErrorMsg}} when is_binary(ErrorMsg) ->
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"move failed: ", ErrorMsg/binary>>)}
|
||||
after Timeout * 1000 ->
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"move timeout">>)}
|
||||
end;
|
||||
error ->
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"dropout from from_network error">>)}
|
||||
end;
|
||||
{FromPid, undefined} when is_pid(FromPid) ->
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"to_network not found">>)};
|
||||
{undefined, ToPid} when is_pid(ToPid) ->
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"from_network not found">>)}
|
||||
end;
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
77
apps/sdlan/src/http_handler/test_handler.erl
Normal file
77
apps/sdlan/src/http_handler/test_handler.erl
Normal file
@ -0,0 +1,77 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 26. 4月 2020 3:36 下午
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(test_handler).
|
||||
-author("licheng5").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
%% 重新加载对应的主机信息
|
||||
handle_request("POST", "/test/auth_token", _, PostParams) ->
|
||||
lager:debug("[test_handler] get post params: ~p", [PostParams]),
|
||||
Data = #{
|
||||
<<"network_id">> => 8,
|
||||
<<"upgrade_type">> => 0,
|
||||
<<"upgrade_prompt">> => <<"simple upgrade">>,
|
||||
<<"upgrade_address">> => <<"upgrade_address">>
|
||||
},
|
||||
{ok, 200, sdlan_util:json_data(Data)};
|
||||
|
||||
handle_request("POST", "/test/upgrade", _, PostParams) ->
|
||||
lager:debug("[test_handler] get post params: ~p", [PostParams]),
|
||||
Data = #{
|
||||
<<"upgrade_type">> => 1,
|
||||
<<"upgrade_prompt">> => <<"prompt需要升级"/utf8>>,
|
||||
<<"upgrade_address">> => <<"macappstore://apps.apple.com/app/id836500024">>
|
||||
},
|
||||
{ok, 200, sdlan_util:json_data(Data)};
|
||||
|
||||
handle_request("GET", "/test/get_all_networks", _, _) ->
|
||||
{ok, 200, sdlan_util:json_data([8, 9, 10])};
|
||||
|
||||
handle_request("GET", "/test/get_network", #{<<"id">> := Id0}, _) ->
|
||||
Id = binary_to_integer(Id0),
|
||||
Networks = #{
|
||||
8 => #{
|
||||
<<"id">> => 8,
|
||||
<<"name">> => <<"test1">>,
|
||||
<<"ipaddr">> => <<"10.211.179.0/24">>,
|
||||
<<"owner_id">> => 1234,
|
||||
<<"disabled_clients">> => []
|
||||
},
|
||||
9 => #{
|
||||
<<"id">> => 9,
|
||||
<<"name">> => <<"test2">>,
|
||||
<<"ipaddr">> => <<"10.211.180.0/24">>,
|
||||
<<"owner_id">> => 1234,
|
||||
<<"disabled_clients">> => []
|
||||
},
|
||||
10 => #{
|
||||
<<"id">> => 10,
|
||||
<<"name">> => <<"test3">>,
|
||||
<<"ipaddr">> => <<"10.211.181.0/24">>,
|
||||
<<"owner_id">> => 1234,
|
||||
<<"disabled_clients">> => []
|
||||
}
|
||||
},
|
||||
Network = maps:get(Id, Networks),
|
||||
|
||||
{ok, 200, sdlan_util:json_data(Network)};
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
134
apps/sdlan/src/mnesia/client_model.erl
Normal file
134
apps/sdlan/src/mnesia/client_model.erl
Normal file
@ -0,0 +1,134 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 04. 7月 2023 12:31
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(client_model).
|
||||
-author("aresei").
|
||||
-include("sdlan_tables.hrl").
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
|
||||
%% API
|
||||
-export([create_table/1, get_table_name/1]).
|
||||
-export([get_clients/1, delete_clients/1, delete_client/2, disable_client/2, alloc_ip/5]).
|
||||
-export([debug/1]).
|
||||
|
||||
create_table(Tab) when is_atom(Tab) ->
|
||||
mnesia:create_table(Tab, [
|
||||
{attributes, record_info(fields, client)},
|
||||
{record_name, client},
|
||||
{disc_copies, [node()]},
|
||||
{type, set}
|
||||
]).
|
||||
|
||||
-spec get_table_name(NetworkId :: integer()) -> TableName :: atom().
|
||||
get_table_name(NetworkId) when is_integer(NetworkId) ->
|
||||
list_to_atom("client_" ++ integer_to_list(NetworkId)).
|
||||
|
||||
-spec get_clients(NetworkId :: integer()) -> [Client :: #client{}].
|
||||
get_clients(NetworkId) when is_integer(NetworkId) ->
|
||||
Tab = get_table_name(NetworkId),
|
||||
case mnesia:transaction(fun() -> mnesia:foldl(fun(R, Acc0) -> [R|Acc0] end, [], Tab) end) of
|
||||
{'atomic', Items} ->
|
||||
lists:reverse(Items);
|
||||
{'aborted', _} ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec delete_clients(NetworkId :: integer()) -> ok | {error, Reason :: any()}.
|
||||
delete_clients(NetworkId) when is_integer(NetworkId) ->
|
||||
Tab = get_table_name(NetworkId),
|
||||
case mnesia:transaction(fun() -> mnesia:clear_table(Tab) end) of
|
||||
{'atomic', ok} ->
|
||||
ok;
|
||||
{'aborted', Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec delete_client(NetworkId :: integer(), ClientId :: binary()) -> ok | {error, Reason :: any()}.
|
||||
delete_client(NetworkId, ClientId) when is_integer(NetworkId), is_binary(ClientId) ->
|
||||
Tab = get_table_name(NetworkId),
|
||||
case mnesia:transaction(fun() -> mnesia:delete(Tab, ClientId, write) end) of
|
||||
{'atomic', ok} ->
|
||||
ok;
|
||||
{'aborted', Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec disable_client(NetworkId :: integer(), ClientId :: binary()) -> ok | {error, Reason :: any()}.
|
||||
disable_client(NetworkId, ClientId) when is_integer(NetworkId), is_binary(ClientId) ->
|
||||
Tab = get_table_name(NetworkId),
|
||||
Fun = fun() ->
|
||||
case mnesia:read(Tab, ClientId, read) of
|
||||
[] ->
|
||||
ok;
|
||||
[Client] ->
|
||||
mnesia:write(client, Client#client{status = disabled}, write)
|
||||
end
|
||||
end,
|
||||
|
||||
case mnesia:transaction(Fun) of
|
||||
{'atomic', ok} ->
|
||||
ok;
|
||||
{'aborted', Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
%% 分配ip地址的时候,以mac地址为唯一基准
|
||||
-spec alloc_ip(NetworkId :: integer(), Ips :: list(), ClientId :: binary(), Mac :: binary(), NetAddr0 :: integer()) ->
|
||||
{ok, Ip :: integer()} | {error, Reason :: any()}.
|
||||
alloc_ip(NetworkId, Ips, ClientId, Mac, NetAddr0) when is_binary(ClientId), is_integer(NetAddr0), is_binary(Mac) ->
|
||||
case mnesia:transaction(fun() -> alloc_ip0(NetworkId, Ips, ClientId, Mac, NetAddr0) end) of
|
||||
{'atomic', Res} ->
|
||||
{ok, Res};
|
||||
{'aborted', Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
alloc_ip0(NetworkId, Ips, ClientId, Mac, NetAddr0) ->
|
||||
Tab = get_table_name(NetworkId),
|
||||
|
||||
case mnesia:read(Tab, ClientId) of
|
||||
[Client=#client{ip = Ip, status = normal}] ->
|
||||
ok = mnesia:write(client, Client#client{mac = Mac}, write),
|
||||
Ip;
|
||||
[#client{status = disabled}] ->
|
||||
mnesia:abort(client_disabled);
|
||||
[] ->
|
||||
UsedIps = mnesia:foldl(fun(#client{ip = Ip0}, Acc) -> [Ip0|Acc] end, [], Tab),
|
||||
case lists:member(NetAddr0, Ips) andalso not lists:member(NetAddr0, UsedIps) of
|
||||
true ->
|
||||
%% 如果ip没有被占用,则分配給当前请求
|
||||
Client = #client{client_id = ClientId, mac = Mac, ip = NetAddr0},
|
||||
ok = mnesia:write(client, Client, write),
|
||||
NetAddr0;
|
||||
false ->
|
||||
case Ips -- UsedIps of
|
||||
[] ->
|
||||
mnesia:abort(no_ip);
|
||||
[Ip|_] ->
|
||||
Client = #client{client_id = ClientId, mac = Mac, ip = Ip, status = normal},
|
||||
ok = mnesia:write(client, Client, write),
|
||||
Ip
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
%%%===================================================================
|
||||
%%% helper functions
|
||||
%%%===================================================================
|
||||
|
||||
debug(NetworkId) when is_integer(NetworkId) ->
|
||||
Tab = get_table_name(NetworkId),
|
||||
F = fun() ->
|
||||
Q = qlc:q([E || E <- mnesia:table(Tab)]),
|
||||
qlc:e(Q)
|
||||
end,
|
||||
case mnesia:transaction(F) of
|
||||
{'atomic', Records} ->
|
||||
lists:foreach(fun(C) -> lager:debug("client: ~p", [C]) end, Records);
|
||||
{'aborted', Reason} ->
|
||||
lager:warning("read clients get error: ~p", [Reason])
|
||||
end.
|
||||
26
apps/sdlan/src/mnesia/mnesia_id_generator.erl
Normal file
26
apps/sdlan/src/mnesia/mnesia_id_generator.erl
Normal file
@ -0,0 +1,26 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 04. 7月 2023 12:31
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(mnesia_id_generator).
|
||||
-author("aresei").
|
||||
-include("sdlan.hrl").
|
||||
|
||||
%% API
|
||||
-export([next_id/1, create_table/0]).
|
||||
|
||||
create_table() ->
|
||||
%% id生成器
|
||||
mnesia:create_table(id_generator, [
|
||||
{attributes, record_info(fields, id_generator)},
|
||||
{record_name, id_generator},
|
||||
{disc_copies, [node()]},
|
||||
{type, ordered_set}
|
||||
]).
|
||||
|
||||
next_id(Tab) when is_atom(Tab) ->
|
||||
mnesia:dirty_update_counter(id_generator, Tab, 1).
|
||||
43
apps/sdlan/src/mnesia/mnesia_manager.erl
Normal file
43
apps/sdlan/src/mnesia/mnesia_manager.erl
Normal file
@ -0,0 +1,43 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%% TODO 数据库暂时不启用
|
||||
%%% @end
|
||||
%%% Created : 28. 3月 2024 11:01
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(mnesia_manager).
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
|
||||
%% API
|
||||
-export([init_database/0, join/1, copy_database/1]).
|
||||
|
||||
init_database() ->
|
||||
%% 清理掉以前的schema
|
||||
mnesia:stop(),
|
||||
mnesia:delete_schema([node()]),
|
||||
|
||||
%% 创建schema
|
||||
ok = mnesia:create_schema([node()]),
|
||||
ok = mnesia:start(),
|
||||
ok.
|
||||
|
||||
%% 加入集群
|
||||
join(MasterNode) when is_atom(MasterNode) ->
|
||||
net_kernel:connect_node(MasterNode).
|
||||
|
||||
%% 初始化slave数据库
|
||||
copy_database(MasterNode) when is_atom(MasterNode) ->
|
||||
%% 清理旧的schema
|
||||
mnesia:stop(),
|
||||
mnesia:delete_schema([node()]),
|
||||
%% 重新启动数据库
|
||||
mnesia:start(),
|
||||
|
||||
rpc:call(MasterNode, mnesia, change_config, [extra_db_nodes, [node()]]),
|
||||
mnesia:change_table_copy_type(schema, node(), disc_copies),
|
||||
|
||||
%% 增加表的分区复制
|
||||
% mnesia:add_table_copy(client, node(), ram_copies),
|
||||
ok.
|
||||
29
apps/sdlan/src/model/network_bo.erl
Normal file
29
apps/sdlan/src/model/network_bo.erl
Normal file
@ -0,0 +1,29 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 16. 5月 2023 12:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(network_bo).
|
||||
-author("aresei").
|
||||
-include("sdlan.hrl").
|
||||
|
||||
-define(POOL_NAME, mysql_sdlan).
|
||||
|
||||
%% API
|
||||
-export([get_all_networks/0, get_network_by_id/1]).
|
||||
|
||||
-spec get_all_networks() -> Networks :: [integer()].
|
||||
get_all_networks() ->
|
||||
case mysql_pool:get_all(?POOL_NAME, <<"SELECT id FROM network">>) of
|
||||
{ok, Networks} ->
|
||||
lists:map(fun(#{<<"id">> := Id}) -> Id end, Networks);
|
||||
{error, _} ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec get_network_by_id(Id :: integer()) -> undefined | {ok, NetworkInfo :: map()}.
|
||||
get_network_by_id(Id) when is_integer(Id) ->
|
||||
mysql_pool:get_row(?POOL_NAME, <<"SELECT * FROM network WHERE id = ? LIMIT 1">>, [Id]).
|
||||
48
apps/sdlan/src/mysql/mysql_pool.erl
Normal file
48
apps/sdlan/src/mysql/mysql_pool.erl
Normal file
@ -0,0 +1,48 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2018, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 29. 九月 2018 17:01
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(mysql_pool).
|
||||
-author("aresei").
|
||||
|
||||
%% API
|
||||
-export([get_row/2, get_row/3, get_all/2, get_all/3]).
|
||||
-export([update/4, update_by/2, update_by/3, insert/4]).
|
||||
|
||||
%% 从数据库中查找一行记录
|
||||
-spec get_row(Pool :: atom(), Sql::binary()) -> {ok, Record::map()} | undefined.
|
||||
get_row(Pool, Sql) when is_atom(Pool), is_binary(Sql) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:get_row(ConnPid, Sql) end).
|
||||
|
||||
-spec get_row(Pool :: atom(), Sql::binary(), Params::list()) -> {ok, Record::map()} | undefined.
|
||||
get_row(Pool, Sql, Params) when is_atom(Pool), is_binary(Sql), is_list(Params) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:get_row(ConnPid, Sql, Params) end).
|
||||
|
||||
-spec get_all(Pool :: atom(), Sql::binary()) -> {ok, Rows::list()} | {error, Reason :: any()}.
|
||||
get_all(Pool, Sql) when is_atom(Pool), is_binary(Sql) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:get_all(ConnPid, Sql) end).
|
||||
|
||||
-spec get_all(Pool :: atom(), Sql::binary(), Params::list()) -> {ok, Rows::list()} | {error, Reason::any()}.
|
||||
get_all(Pool, Sql, Params) when is_atom(Pool), is_binary(Sql), is_list(Params) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:get_all(ConnPid, Sql, Params) end).
|
||||
|
||||
-spec insert(Pool :: atom(), Table :: binary(), Fields :: map() | list(), boolean()) ->
|
||||
ok | {ok, InsertId :: integer()} | {error, Reason :: any()}.
|
||||
insert(Pool, Table, Fields, FetchInsertId) when is_atom(Pool), is_binary(Table), is_list(Fields); is_map(Fields), is_boolean(FetchInsertId) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:insert(ConnPid, Table, Fields, FetchInsertId) end).
|
||||
|
||||
-spec update_by(Pool :: atom(), UpdateSql :: binary()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
update_by(Pool, UpdateSql) when is_atom(Pool), is_binary(UpdateSql) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:update_by(ConnPid, UpdateSql) end).
|
||||
|
||||
-spec update_by(Pool :: atom(), UpdateSql :: binary(), Params :: list()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
update_by(Pool, UpdateSql, Params) when is_atom(Pool), is_binary(UpdateSql) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:update_by(ConnPid, UpdateSql, Params) end).
|
||||
|
||||
-spec update(Pool :: atom(), Table :: binary(), Fields :: map(), WhereFields :: map()) -> {ok, AffectedRows::integer()} | {error, Reason::any()}.
|
||||
update(Pool, Table, Fields, WhereFields) when is_atom(Pool), is_binary(Table), is_map(Fields), is_map(WhereFields) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:update(ConnPid, Table, Fields, WhereFields) end).
|
||||
144
apps/sdlan/src/mysql/mysql_provider.erl
Normal file
144
apps/sdlan/src/mysql/mysql_provider.erl
Normal file
@ -0,0 +1,144 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2018, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 29. 九月 2018 17:01
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(mysql_provider).
|
||||
-author("aresei").
|
||||
|
||||
%% API
|
||||
-export([get_row/2, get_row/3, get_all/2, get_all/3]).
|
||||
-export([update/4, update_by/2, update_by/3, insert/4]).
|
||||
|
||||
%% 从数据库中查找一行记录
|
||||
-spec get_row(ConnPid :: pid(), Sql::binary()) -> {ok, Record::map()} | undefined.
|
||||
get_row(ConnPid, Sql) when is_pid(ConnPid), is_binary(Sql) ->
|
||||
lager:debug("[mysql_client] get_row sql is: ~p", [Sql]),
|
||||
case mysql:query(ConnPid, Sql) of
|
||||
{ok, Names, [Row | _]} ->
|
||||
{ok, maps:from_list(lists:zip(Names, Row))};
|
||||
{ok, _, []} ->
|
||||
undefined;
|
||||
Error ->
|
||||
lager:warning("[mysql_client] get error: ~p", [Error]),
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec get_row(ConnPid :: pid(), Sql::binary(), Params::list()) -> {ok, Record::map()} | undefined.
|
||||
get_row(ConnPid, Sql, Params) when is_pid(ConnPid), is_binary(Sql), is_list(Params) ->
|
||||
lager:debug("[mysql_client] get_row sql is: ~p, params: ~p", [Sql, Params]),
|
||||
case mysql:query(ConnPid, Sql, Params) of
|
||||
{ok, Names, [Row | _]} ->
|
||||
{ok, maps:from_list(lists:zip(Names, Row))};
|
||||
{ok, _, []} ->
|
||||
undefined;
|
||||
Error ->
|
||||
lager:warning("[mysql_client] get error: ~p", [Error]),
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec get_all(ConnPid :: pid(), Sql::binary()) -> {ok, Rows::list()} | {error, Reason :: any()}.
|
||||
get_all(ConnPid, Sql) when is_pid(ConnPid), is_binary(Sql) ->
|
||||
lager:debug("[mysql_client] get_all sql is: ~p", [Sql]),
|
||||
case mysql:query(ConnPid, Sql) of
|
||||
{ok, Names, Rows} ->
|
||||
{ok, lists:map(fun(Row) -> maps:from_list(lists:zip(Names, Row)) end, Rows)};
|
||||
{error, Reason} ->
|
||||
lager:warning("[mysql_client] get error: ~p", [Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec get_all(ConnPid :: pid(), Sql::binary(), Params::list()) -> {ok, Rows::list()} | {error, Reason::any()}.
|
||||
get_all(ConnPid, Sql, Params) when is_pid(ConnPid), is_binary(Sql), is_list(Params) ->
|
||||
lager:debug("[mysql_client] get_all sql is: ~p, params: ~p", [Sql, Params]),
|
||||
case mysql:query(ConnPid, Sql, Params) of
|
||||
{ok, Names, Rows} ->
|
||||
{ok, lists:map(fun(Row) -> maps:from_list(lists:zip(Names, Row)) end, Rows)};
|
||||
{error, Reason} ->
|
||||
lager:warning("[mysql_client] get error: ~p", [Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec insert(ConnPid :: pid(), Table :: binary(), Fields :: map() | list(), boolean()) ->
|
||||
ok | {ok, InsertId :: integer()} | {error, Reason :: any()}.
|
||||
insert(ConnPid, Table, Fields, FetchInsertId) when is_pid(ConnPid), is_binary(Table), is_map(Fields), is_boolean(FetchInsertId) ->
|
||||
insert(ConnPid, Table, maps:to_list(Fields), FetchInsertId);
|
||||
insert(ConnPid, Table, Fields, FetchInsertId) when is_pid(ConnPid), is_binary(Table), is_list(Fields), is_boolean(FetchInsertId) ->
|
||||
{Keys, Values} = kvs(Fields),
|
||||
|
||||
FieldSql = iolist_to_binary(lists:join(<<", ">>, Keys)),
|
||||
Placeholders = lists:duplicate(length(Keys), <<"?">>),
|
||||
ValuesPlaceholder = iolist_to_binary(lists:join(<<", ">>, Placeholders)),
|
||||
|
||||
Sql = <<"INSERT INTO ", Table/binary, "(", FieldSql/binary, ") VALUES(", ValuesPlaceholder/binary, ")">>,
|
||||
lager:debug("[mysql_client] insert sql is: ~p, params: ~p", [Sql, Values]),
|
||||
case mysql:query(ConnPid, Sql, Values) of
|
||||
ok ->
|
||||
case FetchInsertId of
|
||||
true ->
|
||||
InsertId = mysql:insert_id(ConnPid),
|
||||
{ok, InsertId};
|
||||
false ->
|
||||
ok
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec update_by(ConnPid :: pid(), UpdateSql :: binary()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
update_by(ConnPid, UpdateSql) when is_pid(ConnPid), is_binary(UpdateSql) ->
|
||||
lager:debug("[mysql_client] updateBySql sql: ~p", [UpdateSql]),
|
||||
case mysql:query(ConnPid, UpdateSql) of
|
||||
ok ->
|
||||
AffectedRows = mysql:affected_rows(ConnPid),
|
||||
{ok, AffectedRows};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec update_by(ConnPid :: pid(), UpdateSql :: binary(), Params :: list()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
update_by(ConnPid, UpdateSql, Params) when is_pid(ConnPid), is_binary(UpdateSql) ->
|
||||
lager:debug("[mysql_client] updateBySql sql: ~p, params: ~p", [UpdateSql, Params]),
|
||||
case mysql:query(ConnPid, UpdateSql, Params) of
|
||||
ok ->
|
||||
AffectedRows = mysql:affected_rows(ConnPid),
|
||||
{ok, AffectedRows};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec update(ConnPid :: pid(), Sql :: binary(), Fields :: map(), WhereFields :: map()) ->
|
||||
{ok, AffectedRows::integer()} | {error, Reason::any()}.
|
||||
update(ConnPid, Table, Fields, WhereFields) when is_pid(ConnPid), is_binary(Table), is_map(Fields), is_map(WhereFields) ->
|
||||
%% 拼接set
|
||||
{SetKeys, SetVals} = kvs(Fields),
|
||||
SetKeys1 = lists:map(fun(K) when is_binary(K) -> <<"`", K/binary, "` = ?">> end, SetKeys),
|
||||
SetSql = iolist_to_binary(lists:join(<<", ">>, SetKeys1)),
|
||||
|
||||
%% 拼接where
|
||||
{WhereKeys, WhereVals} = kvs(WhereFields),
|
||||
WhereKeys1 = lists:map(fun(K) when is_binary(K) -> <<"`", K/binary, "` = ?">> end, WhereKeys),
|
||||
WhereSql = iolist_to_binary(lists:join(<<" AND ">>, WhereKeys1)),
|
||||
|
||||
Params = SetVals ++ WhereVals,
|
||||
|
||||
Sql = <<"UPDATE ", Table/binary, " SET ", SetSql/binary, " WHERE ", WhereSql/binary>>,
|
||||
lager:debug("[mysql_client] update sql is: ~p, params: ~p", [Sql, Params]),
|
||||
case mysql:query(ConnPid, Sql, Params) of
|
||||
ok ->
|
||||
AffectedRows = mysql:affected_rows(ConnPid),
|
||||
{ok, AffectedRows};
|
||||
Error ->
|
||||
lager:error("[mysql_client] update sql: ~p, params: ~p, get a error: ~p", [Sql, Params, Error]),
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec kvs(Fields :: map() | list()) -> {Keys :: list(), Values :: list()}.
|
||||
kvs(Fields) when is_map(Fields) ->
|
||||
kvs(maps:to_list(Fields));
|
||||
kvs(Fields) when is_list(Fields) ->
|
||||
{Keys0, Values0} = lists:foldl(fun({K, V}, {Acc0, Acc1}) -> {[K|Acc0], [V|Acc1]} end, {[], []}, Fields),
|
||||
{lists:reverse(Keys0), lists:reverse(Values0)}.
|
||||
31
apps/sdlan/src/sdlan.app.src
Normal file
31
apps/sdlan/src/sdlan.app.src
Normal file
@ -0,0 +1,31 @@
|
||||
{application, sdlan,
|
||||
[{description, "An OTP application"},
|
||||
{vsn, "0.1.0"},
|
||||
{registered, []},
|
||||
{mod, {sdlan_app, []}},
|
||||
{applications,
|
||||
[
|
||||
sync,
|
||||
lager,
|
||||
cowboy,
|
||||
ranch,
|
||||
poolboy,
|
||||
mysql,
|
||||
esockd,
|
||||
jiffy,
|
||||
hackney,
|
||||
gpb,
|
||||
throttle,
|
||||
parse_trans,
|
||||
mnesia,
|
||||
erts,
|
||||
kernel,
|
||||
crypto,
|
||||
stdlib
|
||||
]},
|
||||
{env,[]},
|
||||
{modules, []},
|
||||
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{links, []}
|
||||
]}.
|
||||
170
apps/sdlan/src/sdlan_api.erl
Normal file
170
apps/sdlan/src/sdlan_api.erl
Normal file
@ -0,0 +1,170 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 27. 3月 2024 16:17
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_api).
|
||||
-author("anlicheng").
|
||||
|
||||
-define(API_TOKEN, <<"wv6fGyBhl*7@AsD9">>).
|
||||
|
||||
%% API
|
||||
-export([get_all_networks/0, get_network/1]).
|
||||
-export([auth_token/3, node_online/3, node_offline/2, flow_report/5, network_forward_report/2]).
|
||||
|
||||
-spec get_all_networks() -> {ok, [NetworkId :: integer()]} | {error, Reason :: any()}.
|
||||
get_all_networks() ->
|
||||
case catch do_get("get_all_networks", []) of
|
||||
{ok, Resp} ->
|
||||
case catch jiffy:decode(Resp, [return_maps]) of
|
||||
#{<<"result">> := Networks} ->
|
||||
{ok, Networks};
|
||||
#{<<"error">> := #{<<"code">> := Code, <<"message">> := Message}} ->
|
||||
{error, {Code, Message}};
|
||||
_ ->
|
||||
{error, <<"invalid json">>}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec get_network(Id :: integer()) -> {ok, Network :: map()} | {error, Reason :: any()}.
|
||||
get_network(Id) when is_integer(Id) ->
|
||||
case catch do_get("get_network", [{<<"id">>, integer_to_binary(Id)}]) of
|
||||
{ok, Resp} ->
|
||||
case catch jiffy:decode(Resp, [return_maps]) of
|
||||
#{<<"result">> := Network} ->
|
||||
{ok, Network};
|
||||
#{<<"error">> := #{<<"code">> := Code, <<"message">> := Message}} ->
|
||||
{error, {Code, Message}};
|
||||
_ ->
|
||||
{error, <<"invalid json">>}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec auth_token(ClientId :: binary(), Token :: binary(), Version :: integer()) -> {ok, Resp :: map()} | {error, Reason :: any()}.
|
||||
auth_token(ClientId, Token, Version) when is_binary(ClientId), is_binary(Token), is_integer(Version) ->
|
||||
case catch do_post("auth_token", #{<<"client_id">> => ClientId, <<"token">> => Token, <<"version">> => Version}) of
|
||||
{ok, Resp} ->
|
||||
case catch jiffy:decode(Resp, [return_maps]) of
|
||||
Result when is_map(Result) ->
|
||||
{ok, Result};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec node_online(ClientId :: binary(), NetworkId :: integer(), IpAddr :: binary()) -> {ok, Resp :: map()} | {error, Reason :: any()}.
|
||||
node_online(ClientId, NetworkId, IpAddr) when is_binary(ClientId), is_integer(NetworkId), is_binary(IpAddr) ->
|
||||
case catch do_post("set_node_status", #{<<"client_id">> => ClientId, <<"network_id">> => NetworkId, <<"ip_addr">> => IpAddr, <<"status">> => 1}) of
|
||||
{ok, Resp} ->
|
||||
{ok, catch jiffy:decode(Resp, [return_maps])};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec node_offline(ClientId :: binary(), NetworkId :: integer()) -> {ok, Resp :: map()} | {error, Reason :: any()}.
|
||||
node_offline(ClientId, NetworkId) when is_binary(ClientId), is_integer(NetworkId) ->
|
||||
case catch do_post("set_node_status", #{<<"client_id">> => ClientId, <<"network_id">> => NetworkId, <<"status">> => 0}) of
|
||||
{ok, Resp} ->
|
||||
{ok, catch jiffy:decode(Resp, [return_maps])};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec flow_report(ClientId :: binary(), NetworkId :: integer(), ForwardNum :: integer(), P2PNum :: integer(), InboundNum :: integer()) ->
|
||||
{ok, Resp :: map()} | {error, Reason :: any()}.
|
||||
flow_report(ClientId, NetworkId, ForwardNum, P2PNum, InboundNum)
|
||||
when is_binary(ClientId), is_integer(NetworkId), is_integer(ForwardNum), is_integer(P2PNum), is_integer(InboundNum) ->
|
||||
Params = #{
|
||||
<<"client_id">> => ClientId,
|
||||
<<"network_id">> => NetworkId,
|
||||
<<"forward_num">> => ForwardNum,
|
||||
<<"p2p_num">> => P2PNum,
|
||||
<<"inbound_num">> => InboundNum
|
||||
},
|
||||
case catch do_post("client_flow_report", Params) of
|
||||
{ok, Resp} ->
|
||||
{ok, catch jiffy:decode(Resp, [return_maps])};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec network_forward_report(NetworkId :: integer(), ForwardNum :: integer()) ->
|
||||
{ok, Resp :: map()} | {error, Reason :: any()}.
|
||||
network_forward_report(NetworkId, ForwardNum) when is_integer(NetworkId), is_integer(ForwardNum) ->
|
||||
Params = #{
|
||||
<<"network_id">> => NetworkId,
|
||||
<<"forward_num">> => ForwardNum
|
||||
},
|
||||
case catch do_post("network_forward_report", Params) of
|
||||
{ok, Resp} ->
|
||||
{ok, catch jiffy:decode(Resp, [return_maps])};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec do_get(Uri :: string(), Params :: [{K :: binary(), V :: binary()}]) -> {ok, Response :: binary()} | {error, Reason :: any()}.
|
||||
do_get(Uri, Params) when is_list(Uri), is_list(Params) ->
|
||||
Token = sdlan_util:md5(<<?API_TOKEN/binary, (integer_to_binary(123))/binary, ?API_TOKEN/binary>>),
|
||||
{ok, Url0} = application:get_env(sdlan, api_url),
|
||||
|
||||
Headers = [
|
||||
{<<"content-type">>, <<"application/json">>},
|
||||
{<<"token">>, Token}
|
||||
],
|
||||
|
||||
QS0 = uri_string:compose_query(Params),
|
||||
QS = iolist_to_binary(QS0),
|
||||
|
||||
Url = Url0 ++ Uri ++ "?" ++ binary_to_list(QS),
|
||||
case catch hackney:request(get, Url, Headers, <<>>, [{pool, false}]) of
|
||||
{ok, 200, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
{ok, RespBody};
|
||||
{ok, HttpCode, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
{error, {HttpCode, RespBody}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec do_post(Uri :: string(), Params :: list() | map()) -> {ok, Resp :: binary()} | {error, Reason :: any()}.
|
||||
do_post(Uri, Params) when is_list(Uri), is_map(Params); is_list(Params) ->
|
||||
Token = sdlan_util:md5(<<?API_TOKEN/binary, (integer_to_binary(123))/binary, ?API_TOKEN/binary>>),
|
||||
{ok, Url0} = application:get_env(sdlan, api_url),
|
||||
|
||||
Headers = [
|
||||
{<<"content-type">>, <<"application/json">>},
|
||||
{<<"token">>, Token}
|
||||
],
|
||||
|
||||
Body = iolist_to_binary(jiffy:encode(Params, [force_utf8])),
|
||||
Url = Url0 ++ Uri,
|
||||
|
||||
case catch hackney:request(post, Url, Headers, Body, [{pool, false}]) of
|
||||
{ok, 200, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
{ok, RespBody};
|
||||
{ok, HttpCode, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
{error, {HttpCode, RespBody}};
|
||||
{ok, HttpCode, _} ->
|
||||
{error, {HttpCode, <<"empty response">>}};
|
||||
{ok, ClientRef} ->
|
||||
hackney:close(ClientRef),
|
||||
{error, <<"empty response">>};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
78
apps/sdlan/src/sdlan_app.erl
Normal file
78
apps/sdlan/src/sdlan_app.erl
Normal file
@ -0,0 +1,78 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%% @doc sdlan public API
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(sdlan_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
io:setopts([{encoding, unicode}]),
|
||||
%% 启动mnesia数据库
|
||||
mnesia:start(),
|
||||
%% 加速内存的回收
|
||||
erlang:system_flag(fullsweep_after, 16),
|
||||
start_http_server(),
|
||||
start_tcp_server(),
|
||||
sdlan_sup:start_link().
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
%% internal functions
|
||||
|
||||
%% 启动http服务
|
||||
start_http_server() ->
|
||||
{ok, Props} = application:get_env(sdlan, http_server),
|
||||
Acceptors = proplists:get_value(acceptors, Props, 50),
|
||||
MaxConnections = proplists:get_value(max_connections, Props, 10240),
|
||||
Backlog = proplists:get_value(backlog, Props, 1024),
|
||||
Port = proplists:get_value(port, Props),
|
||||
|
||||
Dispatcher = cowboy_router:compile([
|
||||
{'_', [
|
||||
{"/file/[...]", file_handler, []},
|
||||
{"/api/[...]", http_protocol, [api_handler]},
|
||||
{"/network/[...]", http_protocol, [network_handler]},
|
||||
{"/node/[...]", http_protocol, [node_handler]},
|
||||
{"/test/[...]", http_protocol, [test_handler]}
|
||||
]}
|
||||
]),
|
||||
|
||||
TransOpts = [
|
||||
{port, Port},
|
||||
{num_acceptors, Acceptors},
|
||||
{backlog, Backlog},
|
||||
{max_connections, MaxConnections}
|
||||
],
|
||||
|
||||
{ok, Pid} = cowboy:start_clear(http_listener, TransOpts, #{env => #{dispatch => Dispatcher}}),
|
||||
|
||||
lager:debug("[iot_app] the http server start at: ~p, pid is: ~p", [Port, Pid]).
|
||||
|
||||
%% 启动tcp服务
|
||||
start_tcp_server() ->
|
||||
{ok, Props} = application:get_env(sdlan, tcp_server),
|
||||
Acceptors = proplists:get_value(acceptors, Props, 50),
|
||||
MaxConnections = proplists:get_value(max_connections, Props, 10240),
|
||||
Backlog = proplists:get_value(backlog, Props, 1024),
|
||||
Port = proplists:get_value(port, Props),
|
||||
|
||||
TransOpts = [
|
||||
{tcp_options, [
|
||||
binary,
|
||||
{reuseaddr, true},
|
||||
{active, false},
|
||||
{packet, 2},
|
||||
{nodelay, false},
|
||||
{backlog, Backlog}
|
||||
]},
|
||||
{acceptors, Acceptors},
|
||||
{max_connections, MaxConnections}
|
||||
],
|
||||
{ok, _} = esockd:open('sdlan/tcp_server', Port, TransOpts, {sdlan_channel, start_link, []}),
|
||||
|
||||
lager:debug("[sdlan_app] the tcp server start at: ~p", [Port]).
|
||||
366
apps/sdlan/src/sdlan_channel.erl
Normal file
366
apps/sdlan/src/sdlan_channel.erl
Normal file
@ -0,0 +1,366 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 10. 12月 2020 上午11:17
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_channel).
|
||||
-author("licheng5").
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("sdlan.hrl").
|
||||
-include("sdlan_pb.hrl").
|
||||
|
||||
%% 心跳包监测机制
|
||||
-define(PING_TICKER, 15000).
|
||||
|
||||
%% 注册失败的的错误码
|
||||
|
||||
%% token不存在
|
||||
-define(NAK_INVALID_TOKEN, 1).
|
||||
%% 节点被禁用
|
||||
-define(NAK_NODE_DISABLE, 2).
|
||||
%% 没有IP地址可以用
|
||||
-define(NAK_NO_IP, 3).
|
||||
%% 网络错误
|
||||
-define(NAK_NETWORK_FAULT, 4).
|
||||
%% 内部错误
|
||||
-define(NAK_INTERNAL_FAULT, 5).
|
||||
|
||||
%% 升级策略
|
||||
-define(UPGRADE_NONE, 0).
|
||||
-define(UPGRADE_NORMAL, 1).
|
||||
-define(UPGRADE_FORCE, 2).
|
||||
|
||||
%% API
|
||||
-export([start_link/2]).
|
||||
-export([publish_command/4, send_event/3, stop/2, move_network/3]).
|
||||
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-record(state, {
|
||||
transport,
|
||||
socket,
|
||||
|
||||
client_id :: undefined | binary(),
|
||||
pub_key :: undefined | binary(),
|
||||
token :: undefined | binary(),
|
||||
|
||||
%% 分配的ip地址
|
||||
assign_ip :: undefined | integer(),
|
||||
%% 网络相关信息id
|
||||
network_pid :: undefined | pid(),
|
||||
%% mac地址
|
||||
mac :: undefined | binary(),
|
||||
|
||||
%% 标记是否已经注册
|
||||
is_registered = false,
|
||||
|
||||
%% 记录ping的次数
|
||||
ping_counter = 0,
|
||||
|
||||
%% 发送消息对应的id
|
||||
packet_id = 1 :: integer(),
|
||||
%% 请求响应的对应关系
|
||||
inflight = #{}
|
||||
}).
|
||||
|
||||
%% 向通道中写入消息
|
||||
-spec publish_command(Pid :: pid(), ReceiverPid :: pid(), CommandType :: integer(), Msg :: binary()) -> Ref :: reference().
|
||||
publish_command(Pid, ReceiverPid, CommandType, Msg) when is_pid(Pid), is_pid(ReceiverPid), is_integer(CommandType), is_binary(Msg) ->
|
||||
Ref = make_ref(),
|
||||
Pid ! {publish_command, ReceiverPid, Ref, CommandType, Msg},
|
||||
Ref.
|
||||
|
||||
%% 网络迁移是一种特殊的指令信息,需要单独处理
|
||||
-spec move_network(Pid :: pid(), ReceiverPid :: pid(), NetworkPid :: pid()) -> Ref :: reference().
|
||||
move_network(Pid, ReceiverPid, NetworkPid) when is_pid(Pid), is_pid(ReceiverPid), is_pid(NetworkPid) ->
|
||||
Ref = make_ref(),
|
||||
Pid ! {move_network, ReceiverPid, Ref, NetworkPid},
|
||||
Ref.
|
||||
|
||||
%% 向通道中写入消息
|
||||
-spec send_event(Pid :: pid(), EventType :: integer(), Event :: binary()) -> no_return().
|
||||
send_event(Pid, EventType, Event) when is_pid(Pid), is_integer(EventType), is_binary(Event) ->
|
||||
Pid ! {send_event, EventType, Event}.
|
||||
|
||||
%% 关闭方法
|
||||
-spec stop(Pid :: pid(), Reason :: any()) -> no_return().
|
||||
stop(undefined, _Reason) ->
|
||||
ok;
|
||||
stop(Pid, Reason) when is_pid(Pid) ->
|
||||
Pid ! {stop, Reason}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% esockd callback
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start_link(Transport, Sock) ->
|
||||
{ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock]])}.
|
||||
|
||||
init([Transport, Sock]) ->
|
||||
lager:debug("[sdlan_channel] get a new connection: ~p", [Sock]),
|
||||
case Transport:wait(Sock) of
|
||||
{ok, NewSock} ->
|
||||
Transport:setopts(Sock, [{active, true}]),
|
||||
erlang:start_timer(?PING_TICKER, self(), ping_ticker),
|
||||
gen_server:enter_loop(?MODULE, [], #state{transport = Transport, socket = NewSock});
|
||||
{error, Reason} ->
|
||||
{stop, Reason}
|
||||
end.
|
||||
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
handle_cast(_Msg, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
handle_info({tcp, Sock, <<PacketId:32, ?PACKET_REGISTER_SUPER, Body/binary>>}, State=#state{transport = Transport, socket = Sock}) ->
|
||||
#sdl_register_super{version = Version, client_id = ClientId, dev_addr = #sdl_dev_addr{net_addr = NetAddr0, mac = Mac}, token = Token, pub_key = PubKey} = sdlan_pb:decode_msg(Body, sdl_register_super),
|
||||
|
||||
%% 参数检查
|
||||
lager:debug("[sdlan_channel] client_id: ~p, assert0: ~p, assert1: ~p, assert2: ~p", [ClientId, Mac =/= <<>>, PubKey =/= <<>>, ClientId =/= <<>>]),
|
||||
true = (Mac =/= <<>> andalso PubKey =/= <<>> andalso ClientId =/= <<>>),
|
||||
%% Mac地址不能是广播地址
|
||||
true = not (sdlan_util:is_multicast_mac(Mac) orelse sdlan_util:is_broadcast_mac(Mac)),
|
||||
|
||||
case sdlan_api:auth_token(ClientId, Token, Version) of
|
||||
{ok, #{<<"result">> := #{<<"network_id">> := NetworkId, <<"upgrade_type">> := UpgradeType, <<"upgrade_prompt">> := UpgradePrompt, <<"upgrade_address">> := UpgradeAddress}}} when is_integer(NetworkId) ->
|
||||
lager:debug("[sdlan_channel] client_id: ~p, mac: ~p, token: ~p, version: ~p, registerd, alloc network_id: ~p", [ClientId, sdlan_util:format_mac(Mac), Token, Version, NetworkId]),
|
||||
%% 建立到network的对应关系
|
||||
case sdlan_network:get_pid(NetworkId) of
|
||||
NetworkPid when is_pid(NetworkPid) ->
|
||||
case sdlan_network:assign_ip_addr(NetworkPid, self(), ClientId, Mac, NetAddr0) of
|
||||
{ok, NetAddr, NetBitLen, AesKey} ->
|
||||
RsaPubKey = sdlan_cipher:rsa_pem_decode(PubKey),
|
||||
EncodedAesKey = rsa_encode(AesKey, RsaPubKey),
|
||||
|
||||
RegisterSuperAck = sdlan_pb:encode_msg(#sdl_register_super_ack {
|
||||
dev_addr = #sdl_dev_addr{
|
||||
network_id = NetworkId,
|
||||
net_addr = NetAddr,
|
||||
mac = Mac,
|
||||
net_bit_len = NetBitLen
|
||||
},
|
||||
aes_key = EncodedAesKey,
|
||||
upgrade_type = UpgradeType,
|
||||
upgrade_prompt = UpgradePrompt,
|
||||
upgrade_address = UpgradeAddress
|
||||
}),
|
||||
|
||||
%% 发送确认信息
|
||||
Reply = <<PacketId:32, ?PACKET_REGISTER_SUPER_ACK, RegisterSuperAck/binary>>,
|
||||
Transport:send(Sock, Reply),
|
||||
lager:debug("[sdlan_channel] client_id: ~p, mac: ~p, alloc ip: ~p, register will send ack",
|
||||
[ClientId, sdlan_util:format_mac(Mac), sdlan_ipaddr:int_to_ipv4(NetAddr)]),
|
||||
|
||||
%% 设置节点的在线状态
|
||||
Result = sdlan_api:node_online(ClientId, NetworkId, sdlan_ipaddr:int_to_ipv4(NetAddr)),
|
||||
lager:debug("[sdlan_channel] client_id: ~p, set none online, result is: ~p", [ClientId, Result]),
|
||||
case UpgradeType =:= ?UPGRADE_FORCE of
|
||||
true ->
|
||||
lager:warning("[sdlan_channel] client_id: ~p, need upgrade force!", [ClientId]),
|
||||
{stop, normal, State};
|
||||
false ->
|
||||
{noreply, State#state{client_id = ClientId, mac = Mac, assign_ip = NetAddr, network_pid = NetworkPid, pub_key = PubKey, is_registered = true}}
|
||||
end;
|
||||
{error, no_ip} ->
|
||||
lager:debug("[sdlan_channel] client_id: ~p, token: ~p, register get error: no_ip", [ClientId, Token]),
|
||||
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_NO_IP, <<"No Ip address">>)),
|
||||
{stop, normal, State};
|
||||
{error, client_disabled} ->
|
||||
lager:debug("[sdlan_channel] client_id: ~p, token: ~p, register get error: client_disabled", [ClientId, Token]),
|
||||
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_NODE_DISABLE, <<"Client Connection Disable">>)),
|
||||
{stop, normal, State}
|
||||
end;
|
||||
undefined ->
|
||||
lager:debug("[sdlan_channel] client_id: ~p, token: ~p, register get error: network not found", [ClientId, Token]),
|
||||
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_INTERNAL_FAULT, <<"Internal Error">>)),
|
||||
|
||||
{stop, normal, State}
|
||||
end;
|
||||
{ok, #{<<"error">> := #{<<"code">> := 1, <<"message">> := Message}}} ->
|
||||
lager:debug("[sdlan_channel] client_id: ~p, token: ~p, register get error: ~p, error_code: 1", [ClientId, Token, Message]),
|
||||
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_INVALID_TOKEN, Message)),
|
||||
{stop, normal, State};
|
||||
|
||||
{ok, #{<<"error">> := #{<<"code">> := 2, <<"message">> := Message}}} ->
|
||||
lager:debug("[sdlan_channel] client_id: ~p, token: ~p, register get error: ~p, error_code: 2", [ClientId, Token, Message]),
|
||||
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_NODE_DISABLE, Message)),
|
||||
{stop, normal, State};
|
||||
|
||||
{error, Reason} ->
|
||||
lager:debug("[sdlan_channel] client_id: ~p, token: ~p, register get error: ~p", [ClientId, Token, Reason]),
|
||||
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_NETWORK_FAULT, <<"Network Error">>)),
|
||||
{stop, normal, State}
|
||||
end;
|
||||
|
||||
handle_info({tcp, Sock, <<PacketId:32, ?PACKET_QUERY_INFO, Body/binary>>}, State = #state{transport = Transport, socket = Sock, network_pid = NetworkPid, mac = SrcMac, is_registered = true}) when is_pid(NetworkPid) ->
|
||||
#sdl_query_info{dst_mac = DstMac} = sdlan_pb:decode_msg(Body, sdl_query_info),
|
||||
case sdlan_network:peer_info(NetworkPid, SrcMac, DstMac) of
|
||||
error ->
|
||||
lager:debug("[sdlan_channel] query_info src_mac is: ~p, dst_mac: ~p, nat_peer not found",
|
||||
[sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
|
||||
Transport:send(Sock, <<PacketId:32, ?PACKET_EMPTY>>),
|
||||
{noreply, State};
|
||||
{ok, {NatPeer = {{Ip0, Ip1, Ip2, Ip3}, NatPort}, NatType}, V6Info} ->
|
||||
lager:debug("[sdlan_channel] query_info src_mac is: ~p, dst_mac: ~p, nat_peer: ~p",
|
||||
[sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac), NatPeer]),
|
||||
|
||||
PeerInfo = sdlan_pb:encode_msg(#sdl_peer_info{
|
||||
dst_mac = DstMac,
|
||||
v4_info = #sdl_v4_info {
|
||||
port = NatPort,
|
||||
v4 = <<Ip0, Ip1, Ip2, Ip3>>,
|
||||
nat_type = NatType
|
||||
},
|
||||
v6_info = V6Info
|
||||
}),
|
||||
Transport:send(Sock, <<PacketId:32, ?PACKET_PEER_INFO, PeerInfo/binary>>),
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
handle_info({tcp, _Sock, <<0:32, ?PACKET_PING>>}, State = #state{transport = Transport, socket = Sock, client_id = ClientId, ping_counter = PingCounter}) ->
|
||||
lager:debug("[sdlan_channel] client_id: ~p, get ping", [ClientId]),
|
||||
Transport:send(Sock, <<0:32, ?PACKET_PONG>>),
|
||||
{noreply, State#state{ping_counter = PingCounter + 1}};
|
||||
|
||||
handle_info({timeout, _, ping_ticker}, State = #state{client_id = ClientId, ping_counter = PingCounter}) ->
|
||||
%% 等待下一次的心跳检测
|
||||
erlang:start_timer(?PING_TICKER, self(), ping_ticker),
|
||||
case PingCounter > 0 of
|
||||
true ->
|
||||
{noreply, State#state{ping_counter = 0}};
|
||||
false ->
|
||||
lager:debug("[sdlan_channel] client_id: ~p, ping losted", [ClientId]),
|
||||
{stop, normal, State#state{ping_counter = 0}}
|
||||
end;
|
||||
|
||||
%% 重新加入网络
|
||||
handle_info({move_network, ReceiverPid, Ref, NetworkPid},
|
||||
State = #state{transport = Transport, socket = Sock, client_id = ClientId, mac = Mac, pub_key = PubKey, packet_id = PacketId, inflight = Inflight, is_registered = true}) ->
|
||||
|
||||
%% 建立到network的对应关系
|
||||
case sdlan_network:assign_ip_addr(NetworkPid, self(), ClientId, Mac, 0) of
|
||||
{ok, NetAddr, NetBitLen, AesKey} ->
|
||||
RsaPubKey = sdlan_cipher:rsa_pem_decode(PubKey),
|
||||
EncodedAesKey = rsa_encode(AesKey, RsaPubKey),
|
||||
|
||||
{ok, NetworkId} = sdlan_network:get_network_id(NetworkPid),
|
||||
%% 发送确认信息
|
||||
ChangeNetworkCommand = sdlan_pb:encode_msg(#sdl_change_network_command {
|
||||
dev_addr = #sdl_dev_addr {
|
||||
network_id = NetworkId,
|
||||
net_addr = NetAddr,
|
||||
net_bit_len = NetBitLen
|
||||
},
|
||||
aes_key = EncodedAesKey
|
||||
}),
|
||||
Command = <<PacketId:32, ?PACKET_COMMAND, ?PACKET_COMMAND_CHANGE_NETWORK, ChangeNetworkCommand/binary>>,
|
||||
Transport:send(Sock, Command),
|
||||
|
||||
%% 设置节点的在线状态
|
||||
sdlan_api:node_online(ClientId, NetworkId, sdlan_ipaddr:int_to_ipv4(NetAddr)),
|
||||
|
||||
lager:debug("[sdlan_channel] client_id: ~p, move_network will send command: ~p", [ClientId, Command]),
|
||||
{noreply, State#state{packet_id = PacketId + 1, assign_ip = NetAddr, network_pid = NetworkPid, inflight = maps:put(PacketId, {ReceiverPid, Ref}, Inflight)}};
|
||||
{error, Reason} ->
|
||||
lager:debug("[sdlan_channel] client_id: ~p, move_network get error: ~p", [ClientId, Reason]),
|
||||
Transport:send(Sock, register_nak_reply(0, ?NAK_NO_IP, <<"No Ip address">>)),
|
||||
ReceiverPid ! {command_reply, Ref, {error, <<"assign_ip error, no ip free">>}},
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
%% 发送指令信息
|
||||
handle_info({send_event, EventType, Event}, State = #state{transport = Transport, socket = Sock, client_id = ClientId, is_registered = true}) ->
|
||||
lager:debug("[sdlan_channel] client_id: ~p, will send eventType: ~p, event: ~p", [ClientId, EventType, Event]),
|
||||
Transport:send(Sock, <<0:32, ?PACKET_EVENT, EventType, Event/binary>>),
|
||||
{noreply, State};
|
||||
|
||||
%% 网络流量统计
|
||||
handle_info({tcp, _Sock, <<0:32, ?PACKET_FLOW_TRACER, Body/binary>>}, State = #state{client_id = ClientId, network_pid = NetworkPid, is_registered = true}) when is_pid(NetworkPid) ->
|
||||
#sdl_flows{forward_num = ForwardNum, p2p_num = P2PNum, inbound_num = InboundNum} = sdlan_pb:decode_msg(Body, sdl_flows),
|
||||
{ok, NetworkId} = sdlan_network:get_network_id(NetworkPid),
|
||||
ReportResult = sdlan_api:flow_report(ClientId, NetworkId, ForwardNum, P2PNum, InboundNum),
|
||||
lager:debug("[sdlan_channel] flow_tracer, forward: ~p, p2p: ~p, inbound: ~p, result: ~p", [ClientId, ForwardNum, P2PNum, InboundNum, ReportResult]),
|
||||
{noreply, State};
|
||||
|
||||
%% 取消注册
|
||||
handle_info({tcp, _Sock, <<0:32, ?PACKET_UNREGISTER>>}, State = #state{client_id = ClientId, network_pid = NetworkPid, is_registered = true}) when is_pid(NetworkPid) ->
|
||||
lager:warning("[sdlan_channel] unregister client_id: ~p", [ClientId]),
|
||||
% sdlan_network:unregister(NetworkPid, ClientId),
|
||||
{stop, normal, State};
|
||||
|
||||
%% 发送指令信息
|
||||
handle_info({publish_command, ReceiverPid, Ref, CommandType, Msg}, State = #state{transport = Transport, socket = Sock, client_id = ClientId, packet_id = PacketId, inflight = Inflight, is_registered = true}) ->
|
||||
lager:warning("[sdlan_channel] client_id: ~p, will publish: ~p, message: ~p", [ClientId, CommandType, Msg]),
|
||||
Transport:send(Sock, <<PacketId:32, ?PACKET_COMMAND, CommandType, Msg/binary>>),
|
||||
{noreply, State#state{packet_id = PacketId + 1, inflight = maps:put(PacketId, {ReceiverPid, Ref}, Inflight)}};
|
||||
|
||||
%% 主机端的消息响应
|
||||
handle_info({tcp, _Sock, <<PacketId:32, ?PACKET_COMMAND_ACK, Body/binary>>}, State = #state{client_id = ClientId, inflight = Inflight}) when PacketId > 0 ->
|
||||
CommandAck = #sdl_command_ack{} = sdlan_pb:decode_msg(Body, sdl_command_ack),
|
||||
|
||||
lager:debug("[sdlan_channel] client_id: ~p, get publish response message: ~p, packet_id: ~p", [ClientId, CommandAck, PacketId]),
|
||||
case maps:take(PacketId, Inflight) of
|
||||
error ->
|
||||
lager:warning("[sdlan_channel] get unknown publish response message: ~p, packet_id: ~p", [CommandAck, PacketId]),
|
||||
{ok, State};
|
||||
{{ReceiverPid, Ref}, NInflight} ->
|
||||
case is_pid(ReceiverPid) andalso is_process_alive(ReceiverPid) of
|
||||
true ->
|
||||
ReceiverPid ! {command_reply, Ref, CommandAck};
|
||||
false ->
|
||||
lager:warning("[sdlan_channel] get publish response message: ~p, packet_id: ~p, but receiver_pid is deaded", [CommandAck, PacketId])
|
||||
end,
|
||||
{noreply, State#state{inflight = NInflight}}
|
||||
end;
|
||||
|
||||
handle_info({tcp_error, Sock, Reason}, State = #state{socket = Sock, client_id = ClientId}) ->
|
||||
lager:notice("[sdlan_channel] client_id: ~p, tcp_error: ~p", [ClientId, Reason]),
|
||||
{stop, normal, State};
|
||||
|
||||
handle_info({tcp_closed, Sock}, State = #state{socket = Sock, client_id = ClientId}) ->
|
||||
lager:notice("[sdlan_channel] client_id: ~p, tcp_closed", [ClientId]),
|
||||
{stop, normal, State};
|
||||
|
||||
%% 关闭当前通道
|
||||
handle_info({stop, Reason}, State) ->
|
||||
{stop, Reason, State};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
lager:warning("[sdlan_channel] get a unknown message: ~p, channel will closed", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
terminate(Reason, #state{client_id = ClientId, network_pid = NetworkPid}) ->
|
||||
case ClientId /= undefined andalso is_pid(NetworkPid) of
|
||||
true ->
|
||||
{ok, NetworkId} = sdlan_network:get_network_id(NetworkPid),
|
||||
Result = sdlan_api:node_offline(ClientId, NetworkId),
|
||||
lager:debug("[sdlan_channel] client_id: ~p, set none offline, result is: ~p", [ClientId, Result]);
|
||||
false ->
|
||||
ok
|
||||
end,
|
||||
lager:warning("[sdlan_channel] client_id: ~p, stop with reason: ~p", [ClientId, Reason]),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
-spec register_nak_reply(PacketId :: integer(), ErrorCode :: integer(), ErrorMsg :: binary()) -> binary().
|
||||
register_nak_reply(PacketId, ErrorCode, ErrorMsg) when is_integer(PacketId), is_integer(ErrorCode), is_binary(ErrorMsg) ->
|
||||
RegisterNakReply = sdlan_pb:encode_msg(#sdl_register_super_nak {
|
||||
error_code = ErrorCode,
|
||||
error_message = ErrorMsg
|
||||
}),
|
||||
<<PacketId:32, ?PACKET_REGISTER_SUPER_NAK, RegisterNakReply/binary>>.
|
||||
|
||||
rsa_encode(PlainText, RsaPubKey) when is_binary(PlainText) ->
|
||||
iolist_to_binary(sdlan_cipher:rsa_encrypt(PlainText, RsaPubKey)).
|
||||
43
apps/sdlan/src/sdlan_cipher.erl
Normal file
43
apps/sdlan/src/sdlan_cipher.erl
Normal file
@ -0,0 +1,43 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 11. 3月 2024 11:07
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_cipher).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([rsa_encrypt/2, rsa_pem_decode/1]).
|
||||
-export([aes_encrypt/3, aes_decrypt/3]).
|
||||
-export([test/0]).
|
||||
|
||||
test() ->
|
||||
Key = <<"abcdabcdabcdabcd">>,
|
||||
X = aes_encrypt(Key, Key, <<"hello world">>),
|
||||
lager:debug("x is: ~p, raw: ~p", [X, aes_decrypt(Key, Key, X)]),
|
||||
|
||||
|
||||
ok.
|
||||
|
||||
-spec rsa_pem_decode(PubKey :: binary()) -> public_key:rsa_public_key().
|
||||
rsa_pem_decode(PubKey) when is_binary(PubKey) ->
|
||||
[PubPem] = public_key:pem_decode(PubKey),
|
||||
public_key:pem_entry_decode(PubPem).
|
||||
|
||||
%% 加密数据
|
||||
-spec rsa_encrypt(binary(), public_key:rsa_public_key()) -> binary().
|
||||
rsa_encrypt(BinData, PublicKey) when is_binary(BinData) ->
|
||||
public_key:encrypt_public(BinData, PublicKey, [{rsa_padding, rsa_pkcs1_padding}]).
|
||||
|
||||
%% 基于aes的加密算法
|
||||
-spec aes_encrypt(binary(), binary(), binary()) -> binary().
|
||||
aes_encrypt(Key, IVec, PlainText) when is_binary(Key), is_binary(IVec), is_binary(PlainText) ->
|
||||
crypto:crypto_one_time(aes_128_ofb, Key, IVec, PlainText, [{encrypt, true}, {padding, pkcs_padding}]).
|
||||
|
||||
%% 基于aes的解密算法
|
||||
-spec aes_decrypt(binary(), binary(), binary()) -> binary().
|
||||
aes_decrypt(Key, IVec, CipherText) when is_binary(Key), is_binary(IVec), is_binary(CipherText) ->
|
||||
crypto:crypto_one_time(aes_128_ofb, Key, IVec, CipherText, [{encrypt, false}, {padding, pkcs_padding}]).
|
||||
55
apps/sdlan/src/sdlan_ipaddr.erl
Normal file
55
apps/sdlan/src/sdlan_ipaddr.erl
Normal file
@ -0,0 +1,55 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 27. 3月 2024 17:43
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_ipaddr).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([ipv4_to_int/1, int_to_ipv4/1, ips/2, format_ip/1]).
|
||||
-export([ipv6_bytes_to_binary/1]).
|
||||
|
||||
format_ip(Ip) when is_integer(Ip) ->
|
||||
int_to_ipv4(Ip);
|
||||
format_ip(Ip) ->
|
||||
Ip.
|
||||
|
||||
-spec ipv4_to_int(Ip :: integer() | binary() | inet:ip4_address()) -> integer().
|
||||
ipv4_to_int(Ip) when is_integer(Ip) ->
|
||||
Ip;
|
||||
ipv4_to_int({Ip0, Ip1, Ip2, Ip3}) ->
|
||||
<<Ip:32>> = <<Ip0, Ip1, Ip2, Ip3>>,
|
||||
Ip;
|
||||
ipv4_to_int(Ip) when is_binary(Ip) ->
|
||||
Parts0 = binary:split(Ip, <<".">>, [global]),
|
||||
Parts = lists:map(fun binary_to_integer/1, Parts0),
|
||||
<<IpInt:32>> = iolist_to_binary(Parts),
|
||||
IpInt.
|
||||
|
||||
-spec int_to_ipv4(Ip :: integer()) -> binary().
|
||||
int_to_ipv4(Ip) when is_integer(Ip) ->
|
||||
<<Ip0, Ip1, Ip2, Ip3>> = <<Ip:32>>,
|
||||
<<(integer_to_binary(Ip0))/binary, $., (integer_to_binary(Ip1))/binary, $., (integer_to_binary(Ip2))/binary, $., (integer_to_binary(Ip3))/binary>>.
|
||||
|
||||
-spec ips(NetAddr :: binary(), MaskLen :: integer()) -> [Ip :: integer()].
|
||||
ips(NetAddr, MaskLen) when is_binary(NetAddr), is_integer(MaskLen) ->
|
||||
Mask = 16#FFFFFFFF bsr MaskLen,
|
||||
Net0 = ipv4_to_int(NetAddr),
|
||||
%% 防止网络地址给得不对,比如: "192.168.1.101",
|
||||
L = 32 - MaskLen,
|
||||
Net = (Net0 bsr L) bsl L,
|
||||
lists:map(fun(V) -> Net + V end, lists:seq(1, Mask - 1)).
|
||||
|
||||
-spec ipv6_bytes_to_binary(Bytes :: binary()) -> Bin :: binary().
|
||||
ipv6_bytes_to_binary(<<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>) ->
|
||||
Segments = [integer_to_list(X, 16) || X <- [A, B, C, D, E, F, G, H]],
|
||||
% 填充每个段以确保是4位
|
||||
Padded = [string:pad(S, 4, leading, $0) || S <- Segments],
|
||||
% 合并成IPv6地址格式,这里没有处理最简化形式的缩写
|
||||
iolist_to_binary(lists:flatten(string:join(Padded, ":")));
|
||||
ipv6_bytes_to_binary(_) ->
|
||||
<<"">>.
|
||||
586
apps/sdlan/src/sdlan_network.erl
Normal file
586
apps/sdlan/src/sdlan_network.erl
Normal file
@ -0,0 +1,586 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 27. 3月 2024 15:13
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_network).
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
-include("sdlan_pb.hrl").
|
||||
-include("sdlan_tables.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-define(FLOW_REPORT_INTERVAL, 60 * 1000).
|
||||
|
||||
%% broadcast, "FF-FF-FF-FF-FF-FF"
|
||||
-define(BROADCAST_MAC, <<16#FF,16#FF,16#FF,16#FF,16#FF,16#FF>>).
|
||||
|
||||
%% API
|
||||
-export([start_link/2]).
|
||||
-export([get_name/1, get_pid/1, assign_ip_addr/5, peer_info/3, unregister/3, debug_info/1, get_network_id/1, get_used_map/1]).
|
||||
-export([forward/5, update_hole/6, disable_client/2, get_channel/2, dropout_client/2, reload/1]).
|
||||
-export([test_event/1]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-record(hole, {
|
||||
peer :: {Ip :: inet:ip4_address(), Port :: integer()},
|
||||
nat_type :: integer()
|
||||
}).
|
||||
|
||||
%% ip的使用信息
|
||||
-record(host, {
|
||||
client_id :: binary(),
|
||||
mac :: binary(),
|
||||
ip :: integer(),
|
||||
channel_pid :: undefined | pid(),
|
||||
monitor_ref :: undefined | reference(),
|
||||
hole :: undefined | #hole{},
|
||||
%% 记录ip和ip_v6的映射关系, #{ip_addr :: integer() => {}}
|
||||
v6_info :: undefined | #sdl_v6_info{}
|
||||
}).
|
||||
|
||||
-record(state, {
|
||||
network_id :: integer(),
|
||||
name :: binary(),
|
||||
ipaddr :: binary(),
|
||||
mask_len :: integer(),
|
||||
owner_id :: integer(),
|
||||
|
||||
%% 设置网络带宽
|
||||
throttle_key :: atom(),
|
||||
|
||||
%% 转发流量统计
|
||||
forward_bytes = 0,
|
||||
|
||||
%% 同一个网络下公用的密钥, 采用AES-256加密算法;随机生成
|
||||
aes_key :: binary(),
|
||||
|
||||
%% ip分配器
|
||||
%% 记录当前网络下的全部ip地址
|
||||
ips = [] :: [Ip :: integer()],
|
||||
|
||||
%% 记录已经使用了的ip, #{mac :: integer() => Host :: #host{}}
|
||||
used_map = #{}
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% -- MARK: 测试函数
|
||||
test_event(Pid) ->
|
||||
gen_server:cast(Pid, test_event).
|
||||
|
||||
-spec get_pid(Id :: integer()) -> undefined | pid().
|
||||
get_pid(Id) when is_integer(Id) ->
|
||||
whereis(get_name(Id)).
|
||||
|
||||
-spec get_name(Id :: integer()) -> atom().
|
||||
get_name(Id) when is_integer(Id) ->
|
||||
list_to_atom("sdlan_network:" ++ integer_to_list(Id)).
|
||||
|
||||
-spec reload(Pid :: pid()) -> ok | {error, Reason :: any()}.
|
||||
reload(Pid) when is_pid(Pid) ->
|
||||
gen_server:call(Pid, reload).
|
||||
|
||||
-spec assign_ip_addr(Pid :: pid(), ChannelPid :: pid(), ClientId :: binary(), Mac :: binary(), NetAddr :: integer()) ->
|
||||
{ok, NetAddr :: integer(), MaskLen :: integer(), AesKey :: binary()} | {error, Reason :: any()}.
|
||||
assign_ip_addr(Pid, ChannelPid, ClientId, Mac, NetAddr) when is_pid(Pid), is_pid(ChannelPid), is_binary(ClientId), is_binary(Mac), is_integer(NetAddr) ->
|
||||
gen_server:call(Pid, {assign_ip_addr, ChannelPid, ClientId, Mac, NetAddr}).
|
||||
|
||||
-spec get_network_id(Pid :: pid()) -> {ok, NetworkId :: integer()}.
|
||||
get_network_id(Pid) when is_pid(Pid) ->
|
||||
gen_server:call(Pid, get_network_id).
|
||||
|
||||
-spec unregister(Pid :: pid(), ClientId :: binary(), Mac :: binary()) -> no_return().
|
||||
unregister(Pid, ClientId, Mac) when is_pid(Pid), is_binary(ClientId), is_binary(Mac) ->
|
||||
gen_server:cast(Pid, {unregister, ClientId, Mac}).
|
||||
|
||||
-spec peer_info(Pid :: pid(), SrcMac :: binary(), DstMac :: binary()) ->
|
||||
error | {ok, {NatPeer :: {Ip :: inet:ip4_address(), Port :: integer()}, NatType :: integer()}, V6Info :: undefined | #sdl_v6_info{}}.
|
||||
peer_info(Pid, SrcMac, DstMac) when is_pid(Pid), is_binary(SrcMac), is_binary(DstMac) ->
|
||||
gen_server:call(Pid, {peer_info, SrcMac, DstMac}).
|
||||
|
||||
-spec forward(pid(), Sock :: any(), SrcMac :: binary(), DstMac :: binary(), Packet :: binary()) -> no_return().
|
||||
forward(Pid, Sock, SrcMac, DstMac, Packet) when is_pid(Pid), is_binary(SrcMac), is_binary(DstMac), is_binary(Packet) ->
|
||||
gen_server:cast(Pid, {forward, Sock, SrcMac, DstMac, Packet}).
|
||||
|
||||
%% 更新ip地址对应的nat关系
|
||||
-spec update_hole(Pid :: pid(), ClientId :: binary(), Mac :: binary(), Peer :: tuple(), NatType :: integer(), V6Info :: undefined | #sdl_v6_info{}) -> no_return().
|
||||
update_hole(Pid, ClientId, Mac, Peer, NatType, V6Info) when is_pid(Pid), is_binary(ClientId), is_binary(Mac), is_integer(NatType) ->
|
||||
gen_server:cast(Pid, {update_hole, ClientId, Mac, Peer, NatType, V6Info}).
|
||||
|
||||
-spec disable_client(Pid :: pid(), ClientId :: binary()) -> ok | error.
|
||||
disable_client(Pid, ClientId) when is_pid(Pid), is_binary(ClientId) ->
|
||||
gen_server:call(Pid, {disable_client, ClientId}).
|
||||
|
||||
-spec get_channel(Pid :: pid(), ClientId :: binary()) -> error | {ok, ChannelPid :: pid()}.
|
||||
get_channel(Pid, ClientId) when is_pid(Pid), is_binary(ClientId) ->
|
||||
gen_server:call(Pid, {get_channel, ClientId}).
|
||||
|
||||
%% 剔除client_id,channel不关闭; channel会被重新绑定到其他的network里面
|
||||
-spec dropout_client(Pid :: pid(), ClientId :: binary()) -> {ok, ChannelPid :: pid()} | error.
|
||||
dropout_client(Pid, ClientId) when is_pid(Pid), is_binary(ClientId) ->
|
||||
gen_server:call(Pid, {dropout_client, ClientId}).
|
||||
|
||||
-spec debug_info(Pid :: pid()) -> map().
|
||||
debug_info(Pid) when is_pid(Pid) ->
|
||||
gen_server:call(Pid, debug_info).
|
||||
|
||||
-spec get_used_map(Pid :: pid()) -> map().
|
||||
get_used_map(Pid) when is_pid(Pid) ->
|
||||
gen_server:call(Pid, get_used_map);
|
||||
get_used_map(undefined) ->
|
||||
#{}.
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link(Name :: atom(), Id :: integer()) ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link(Name, Id) when is_atom(Name), is_integer(Id) ->
|
||||
gen_server:start_link({local, Name}, ?MODULE, [Id], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Initializes the server
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([Id]) when is_integer(Id) ->
|
||||
erlang:process_flag(trap_exit, true),
|
||||
case sdlan_api:get_network(Id) of
|
||||
{ok, #{<<"ipaddr">> := Null}} when Null == <<"null">>; Null == <<"NULL">> ->
|
||||
ignore;
|
||||
{ok, #{<<"id">> := Id, <<"name">> := Name, <<"ipaddr">> := IpAddr0, <<"owner_id">> := OwnerId}} ->
|
||||
{IpAddr, MaskLen} = parse_ipaddr(IpAddr0),
|
||||
Ips = sdlan_ipaddr:ips(IpAddr, MaskLen),
|
||||
AesKey = sdlan_util:rand_byte(32),
|
||||
%% 限流key
|
||||
ThrottleKey = list_to_atom("network_throttle:" ++ integer_to_list(Id)),
|
||||
%% 绑定到资源协调器
|
||||
sdlan_network_coordinator:attach(self(), ThrottleKey),
|
||||
|
||||
%% 每分钟汇报一次转发的流量
|
||||
erlang:start_timer(?FLOW_REPORT_INTERVAL, self(), flow_report_ticker),
|
||||
|
||||
%% 创建数据库表
|
||||
create_mnesia_table(Id),
|
||||
|
||||
lager:debug("[sdlan_network] network: ~p, ips: ~p", [Id, lists:map(fun sdlan_ipaddr:int_to_ipv4/1, Ips)]),
|
||||
|
||||
{ok, #state{network_id = Id, name = Name, ipaddr = IpAddr, owner_id = OwnerId, mask_len = MaskLen, ips = Ips, aes_key = AesKey, throttle_key = ThrottleKey}};
|
||||
{error, Reason} ->
|
||||
lager:warning("[sdlan_network] load network: ~p, get error: ~p", [Id, Reason]),
|
||||
ignore
|
||||
end.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
%% 重新加载网络信息
|
||||
handle_call(reload, _From, State = #state{network_id = Id, ipaddr = OldIpAddr, mask_len = OldMarkLen, used_map = UsedMap}) ->
|
||||
case sdlan_api:get_network(Id) of
|
||||
{ok, #{<<"name">> := Name, <<"ipaddr">> := IpAddr0, <<"owner_id">> := OwnerId}} ->
|
||||
{IpAddr, MaskLen} = parse_ipaddr(IpAddr0),
|
||||
case OldIpAddr =:= IpAddr andalso OldMarkLen =:= MaskLen of
|
||||
true ->
|
||||
{reply, ok, State#state{name = Name, owner_id = OwnerId}};
|
||||
false ->
|
||||
lager:debug("[sdlan_networkd] network_id: ~p, reload will close all channels", [Id]),
|
||||
Ips = sdlan_ipaddr:ips(IpAddr, MaskLen),
|
||||
%% 整个网络下的设备都需要重新连接
|
||||
maps:foreach(fun(_, #host{channel_pid = ChannelPid, monitor_ref = MRef}) ->
|
||||
is_reference(MRef) andalso demonitor(MRef),
|
||||
is_process_alive(ChannelPid) andalso sdlan_channel:stop(ChannelPid, normal)
|
||||
end, UsedMap),
|
||||
%% 清理掉数据库中的数据
|
||||
ok = client_model:delete_clients(Id),
|
||||
|
||||
{reply, ok, State#state{name = Name, ipaddr = IpAddr,
|
||||
owner_id = OwnerId, mask_len = MaskLen, ips = Ips, used_map = maps:new()}}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
lager:warning("[sdlan_network] reload network: ~p, get error: ~p", [Id, Reason]),
|
||||
{reply, {error, Reason}, State}
|
||||
end;
|
||||
|
||||
%% 给客户端分配ip地址
|
||||
handle_call({assign_ip_addr, ChannelPid, ClientId, Mac, NetAddr0}, _From,
|
||||
State = #state{network_id = NetworkId, ips = Ips, used_map = UsedMap, mask_len = MaskLen, aes_key = AesKey}) ->
|
||||
|
||||
%% 分配ip地址的时候,以mac地址为唯一基准
|
||||
case client_model:alloc_ip(NetworkId, Ips, ClientId, Mac, NetAddr0) of
|
||||
{ok, Ip} ->
|
||||
%% 关闭之前的channel
|
||||
maybe_close_channel(maps:get(Mac, UsedMap, undefined)),
|
||||
|
||||
%% 建立到新的channel之间的关系
|
||||
MRef = monitor(process, ChannelPid),
|
||||
NUsedMap = maps:put(Mac, #host{client_id = ClientId, mac = Mac, ip = Ip, channel_pid = ChannelPid, monitor_ref = MRef}, UsedMap),
|
||||
|
||||
{reply, {ok, Ip, MaskLen, AesKey}, State#state{used_map = NUsedMap}};
|
||||
{error, Reason} ->
|
||||
{reply, {error, Reason}, State}
|
||||
end;
|
||||
|
||||
handle_call(get_used_map, _From, State = #state{used_map = UsedMap}) ->
|
||||
UsedInfos = maps:map(fun(_, #host{hole = Hole, v6_info = V6Info}) ->
|
||||
HoleMap = case Hole of
|
||||
#hole{peer = {NatIp, NatPort}} ->
|
||||
#{
|
||||
<<"nat_ip">> => sdlan_ipaddr:int_to_ipv4(sdlan_ipaddr:ipv4_to_int(NatIp)),
|
||||
<<"nat_port">> => NatPort
|
||||
};
|
||||
_ ->
|
||||
#{}
|
||||
end,
|
||||
|
||||
V6Map = case V6Info of
|
||||
#sdl_v6_info{v6 = IpV6, port = Port} ->
|
||||
#{
|
||||
<<"v6_ip">> => sdlan_ipaddr:ipv6_bytes_to_binary(IpV6),
|
||||
<<"v6_port">> => Port
|
||||
};
|
||||
_ ->
|
||||
#{}
|
||||
end,
|
||||
#{<<"hole">> => HoleMap, <<"v6_info">> => V6Map}
|
||||
end, UsedMap),
|
||||
|
||||
{reply, {ok, UsedInfos}, State};
|
||||
|
||||
%% client设置为禁止状态,不允许重连
|
||||
handle_call({disable_client, ClientId}, _From, State = #state{network_id = NetworkId, used_map = UsedMap}) ->
|
||||
case lists:search(fun({_, #host{client_id = ClientId0}}) -> ClientId =:= ClientId0 end, maps:to_list(UsedMap)) of
|
||||
{value, {Mac, #host{channel_pid = ChannelPid, monitor_ref = MRef}}} ->
|
||||
is_reference(MRef) andalso demonitor(MRef),
|
||||
sdlan_channel:stop(ChannelPid, disable),
|
||||
NUsedMap = maps:remove(Mac, UsedMap),
|
||||
%% 将客户端设置为禁止状态
|
||||
client_model:disable_client(NetworkId, ClientId),
|
||||
|
||||
{reply, ok, State#state{used_map = NUsedMap}};
|
||||
false ->
|
||||
{reply, error, State}
|
||||
end;
|
||||
|
||||
handle_call({get_channel, ClientId}, _From, State = #state{used_map = UsedMap}) ->
|
||||
case lists:search(fun({_, #host{client_id = ClientId0}}) -> ClientId =:= ClientId0 end, maps:to_list(UsedMap)) of
|
||||
{value, {_Ip, #host{channel_pid = ChannelPid}}} ->
|
||||
{reply, {ok, ChannelPid}, State};
|
||||
false ->
|
||||
{reply, error, State}
|
||||
end;
|
||||
|
||||
%% 区别在于是否关闭掉channel, 这里是在网络迁移中的功能; drop的时候需要从当前网络中移除
|
||||
handle_call({dropout_client, ClientId}, _From, State = #state{network_id = NetworkId, used_map = UsedMap}) ->
|
||||
case lists:search(fun({_, #host{client_id = ClientId0}}) -> ClientId =:= ClientId0 end, maps:to_list(UsedMap)) of
|
||||
{value, {Mac, #host{channel_pid = ChannelPid, monitor_ref = MRef}}} ->
|
||||
is_reference(MRef) andalso demonitor(MRef),
|
||||
NUsedMap = maps:remove(Mac, UsedMap),
|
||||
%% 从数据库删除
|
||||
client_model:delete_client(NetworkId, ClientId),
|
||||
|
||||
{reply, {ok, ChannelPid}, State#state{used_map = NUsedMap}};
|
||||
false ->
|
||||
{reply, error, State}
|
||||
end;
|
||||
|
||||
handle_call(get_network_id, _From, State = #state{network_id = NetworkId}) ->
|
||||
{reply, {ok, NetworkId}, State};
|
||||
|
||||
%% 网络存在的nat_peer信息
|
||||
handle_call({peer_info, SrcMac, DstMac}, _From, State = #state{used_map = UsedMap}) ->
|
||||
case maps:find(DstMac, UsedMap) of
|
||||
{ok, #host{channel_pid = DstChannelPid, hole = #hole{peer = DstNatPeer, nat_type = DstNatType}, v6_info = DstV6Info}} ->
|
||||
%% 让目标服务器发送sendRegister事件(2024-06-25 新增,提高打洞的成功率)
|
||||
case maps:get(SrcMac, UsedMap, undefined) of
|
||||
#host{hole = #hole{peer = {SrcNatIp, SrcNatPort}, nat_type = NatType}, v6_info = SrcV6Info} ->
|
||||
Event = sdlan_pb:encode_msg(#sdl_send_register_event {
|
||||
dst_mac = SrcMac,
|
||||
nat_ip = sdlan_ipaddr:ipv4_to_int(SrcNatIp),
|
||||
nat_type = NatType,
|
||||
nat_port = SrcNatPort,
|
||||
v6_info = SrcV6Info
|
||||
}),
|
||||
sdlan_channel:send_event(DstChannelPid, ?PACKET_EVENT_SEND_REGISTER, Event);
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
{reply, {ok, {DstNatPeer, DstNatType}, DstV6Info}, State};
|
||||
_ ->
|
||||
{reply, error, State}
|
||||
end;
|
||||
|
||||
handle_call(debug_info, _From, State = #state{network_id = NetworkId, ipaddr = IpAddr, mask_len = MaskLen, owner_id = OwnerId, ips = Ips, used_map = UsedMap}) ->
|
||||
Reply = #{
|
||||
<<"network_id">> => NetworkId,
|
||||
<<"ipaddr">> => IpAddr,
|
||||
<<"mask_len">> => MaskLen,
|
||||
<<"owner_id">> => OwnerId,
|
||||
<<"ips">> => lists:map(fun sdlan_ipaddr:int_to_ipv4/1, Ips),
|
||||
<<"used_ips">> => lists:map(fun({_, Host}) -> format_host(Host) end, maps:to_list(UsedMap))
|
||||
},
|
||||
{reply, Reply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
%% 网络数据转发, mac地址单播
|
||||
handle_cast({forward, Sock, SrcMac, DstMac, Packet}, State = #state{network_id = NetworkId, used_map = UsedMap, throttle_key = ThrottleKey, forward_bytes = ForwardBytes})
|
||||
when is_map_key(SrcMac, UsedMap), is_map_key(DstMac, UsedMap) ->
|
||||
|
||||
PacketBytes = byte_size(Packet),
|
||||
case maps:find(DstMac, UsedMap) of
|
||||
{ok, #host{hole = #hole{peer = Peer = {Ip, Port}}}} ->
|
||||
case throttle:check(sdlan_network, ThrottleKey) of
|
||||
{ok, _RestCount, _LeftToReset} ->
|
||||
%% client和stun之间必须有心跳机制保持nat映射可用,并且通过服务转发的udp包肯定可以到达对端的nat
|
||||
lager:debug("[sdlan_network] forward data networkd_id: ~p, src_mac: ~p, dst_mac: ~p, hole: ~p",
|
||||
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac), Peer]),
|
||||
|
||||
gen_udp:send(Sock, Ip, Port, Packet),
|
||||
{noreply, State#state{forward_bytes = ForwardBytes + PacketBytes}};
|
||||
{limit_exceeded, 0, _LeftToReset} ->
|
||||
%% 尝试获取其他网络是否有让渡的资源
|
||||
case sdlan_network_coordinator:checkout() of
|
||||
ok ->
|
||||
lager:debug("[sdlan_network] use release forward data networkd_id: ~p, src_mac: ~p, dst_mac: ~p, hole: ~p",
|
||||
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac), Peer]),
|
||||
|
||||
gen_udp:send(Sock, Ip, Port, Packet),
|
||||
{noreply, State#state{forward_bytes = ForwardBytes + PacketBytes}};
|
||||
error ->
|
||||
lager:notice("[sdlan_network] networkd_id: ~p, src_mac: ~p, dst_mac: ~p, rate limited, discard",
|
||||
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
{noreply, State}
|
||||
end
|
||||
end;
|
||||
{ok, _} ->
|
||||
lager:debug("[sdlan_network] networkd_id: ~p, src_mac: ~p, dst_mac: ~p, hole not found",
|
||||
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
{noreply, State};
|
||||
error ->
|
||||
lager:debug("[sdlan_network] networkd_id: ~p, src_mac: ~p, dst_mac: ~p not found",
|
||||
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
%% 网络数据转发, ip广播或组播, 不限流
|
||||
handle_cast({forward, Sock, SrcMac, DstMac, Packet}, State = #state{network_id = NetworkId, used_map = UsedMap, forward_bytes = ForwardBytes})
|
||||
when is_map_key(SrcMac, UsedMap) ->
|
||||
%% 广播地址和组播地址,需要转发到整个网络
|
||||
case sdlan_util:is_broadcast_mac(DstMac) orelse sdlan_util:is_multicast_mac(DstMac) of
|
||||
true ->
|
||||
PacketBytes = byte_size(Packet),
|
||||
%% 消息广播
|
||||
maps:foreach(fun(Mac, #host{hole = Hole}) ->
|
||||
case {Mac =/= SrcMac, Hole} of
|
||||
{true, #hole{peer = {NatIp, NatPort}}} ->
|
||||
lager:debug("[sdlan_network] call me here"),
|
||||
gen_udp:send(Sock, NatIp, NatPort, Packet);
|
||||
_ ->
|
||||
ok
|
||||
end
|
||||
end, UsedMap),
|
||||
|
||||
%% client和stun之间必须有心跳机制保持nat映射可用,并且通过服务转发的udp包肯定可以到达对端的nat
|
||||
lager:debug("[sdlan_network] broadcast data networkd_id: ~p, src_mac: ~p, dst_mac: ~p",
|
||||
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
|
||||
{noreply, State#state{forward_bytes = ForwardBytes + PacketBytes}};
|
||||
false ->
|
||||
lager:debug("[sdlan_network] networkd_id: ~p, src_mac: ~p, dst_mac: ~p, forward discard 1",
|
||||
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
handle_cast({forward, _Sock, SrcMac, DstMac, _Packet}, State = #state{network_id = NetworkId}) ->
|
||||
lager:debug("[sdlan_network] networkd_id: ~p, src_mac: ~p, dst_mac: ~p, forward discard 2",
|
||||
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
{noreply, State};
|
||||
|
||||
%% 删除ip的占用并关闭channel
|
||||
handle_cast({unregister, _ClientId, Mac}, State = #state{network_id = NetworkId, used_map = UsedMap}) ->
|
||||
lager:debug("[sdlan_network] networkd_id: ~p, unregister Mac: ~p", [NetworkId, Mac]),
|
||||
case maps:take(Mac, UsedMap) of
|
||||
error ->
|
||||
{noreply, State};
|
||||
{#host{channel_pid = ChannelPid, monitor_ref = MRef}, NUsedMap} ->
|
||||
is_reference(MRef) andalso demonitor(MRef),
|
||||
sdlan_channel:stop(ChannelPid, normal),
|
||||
{noreply, State#state{used_map = NUsedMap}}
|
||||
end;
|
||||
|
||||
%% 需要判断,client是属于当前网络的
|
||||
handle_cast({update_hole, _ClientId, Mac, Peer, NatType, V6Info}, State = #state{used_map = UsedMap}) ->
|
||||
case maps:find(Mac, UsedMap) of
|
||||
{ok, Host0 = #host{hole = OldHole, ip = Ip}} ->
|
||||
case OldHole =:= undefined orelse (OldHole#hole.peer =/= Peer orelse OldHole#hole.nat_type =/= NatType) of
|
||||
true ->
|
||||
NatChangedEvent = sdlan_pb:encode_msg(#sdl_nat_changed_event {
|
||||
mac = Mac,
|
||||
ip = Ip
|
||||
}),
|
||||
broadcast(?PACKET_EVENT_NAT_CHANGED, NatChangedEvent, Mac, UsedMap);
|
||||
false ->
|
||||
ok
|
||||
end,
|
||||
Host = Host0#host{hole = #hole{peer = Peer, nat_type = NatType}, v6_info = V6Info},
|
||||
|
||||
{noreply, State#state{used_map = maps:put(Mac, Host, UsedMap)}};
|
||||
error ->
|
||||
{noreply, State}
|
||||
end.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({timeout, _, flow_report_ticker}, State = #state{network_id = NetworkId, forward_bytes = ForwardBytes}) ->
|
||||
erlang:start_timer(?FLOW_REPORT_INTERVAL, self(), flow_report_ticker),
|
||||
catch sdlan_api:network_forward_report(NetworkId, ForwardBytes),
|
||||
{noreply, State#state{forward_bytes = 0}};
|
||||
|
||||
handle_info({'EXIT', _Pid, shutdown}, State = #state{network_id = NetworkId, used_map = UsedMap}) ->
|
||||
lager:warning("[sdlan_network] network: ~p, get shutdown message", [NetworkId]),
|
||||
broadcast_shutdown(UsedMap),
|
||||
{stop, shutdown, State};
|
||||
%% Channel进程退出, hole里面的数据也需要清理
|
||||
handle_info({'DOWN', _MRef, process, ChannelPid, Reason}, State = #state{network_id = NetworkId, used_map = UsedMap}) ->
|
||||
lager:notice("[sdlan_network] network_id: ~p, channel_pid: ~p, close with reason: ~p", [NetworkId, ChannelPid, Reason]),
|
||||
NUsedMap = maps:filter(fun(_, #host{channel_pid = ChannelPid0}) -> ChannelPid =/= ChannelPid0 end, UsedMap),
|
||||
{noreply, State#state{used_map = NUsedMap}}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(Reason, #state{network_id = NetworkId, used_map = UsedMap}) ->
|
||||
lager:debug("[sdlan_network] network: ~p, will terminate with reason: ~p", [NetworkId, Reason]),
|
||||
broadcast_shutdown(UsedMap),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
%% 创建数据库表
|
||||
-spec create_mnesia_table(NetworkId :: integer()) -> no_return().
|
||||
create_mnesia_table(NetworkId) when is_integer(NetworkId) ->
|
||||
Tab = client_model:get_table_name(NetworkId),
|
||||
Tables = mnesia:system_info(tables),
|
||||
case lists:member(Tab, Tables) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
Res = client_model:create_table(Tab),
|
||||
lager:debug("[sdlan_network] create table result: ~p", [Res])
|
||||
end.
|
||||
|
||||
-spec maybe_close_channel(undefined | #host{}) -> no_return().
|
||||
maybe_close_channel(#host{channel_pid = ChannelPid0, monitor_ref = MRef0}) ->
|
||||
case is_pid(ChannelPid0) andalso is_process_alive(ChannelPid0) of
|
||||
true ->
|
||||
is_reference(MRef0) andalso demonitor(MRef0),
|
||||
sdlan_channel:stop(ChannelPid0, channel_rebind);
|
||||
false ->
|
||||
ok
|
||||
end;
|
||||
maybe_close_channel(_) ->
|
||||
ok.
|
||||
|
||||
-spec broadcast(EventType :: integer(), Event :: binary(), ExcludeMac :: binary(), UsedMap :: map()) -> no_return().
|
||||
broadcast(EventType, Event, ExcludeMac, UsedMap) when is_map(UsedMap), is_binary(ExcludeMac), is_integer(EventType), is_binary(Event) ->
|
||||
maps:foreach(fun(Mac, #host{channel_pid = ChannelPid}) ->
|
||||
case is_process_alive(ChannelPid) andalso ExcludeMac /= Mac of
|
||||
true ->
|
||||
sdlan_channel:send_event(ChannelPid, EventType, Event);
|
||||
false ->
|
||||
ok
|
||||
end
|
||||
end, UsedMap).
|
||||
|
||||
broadcast_shutdown(UsedMap) when is_map(UsedMap) ->
|
||||
maps:foreach(fun(_, #host{channel_pid = ChannelPid}) ->
|
||||
case is_process_alive(ChannelPid) of
|
||||
true ->
|
||||
NetworkShutdownEvent = sdlan_pb:encode_msg(#sdl_network_shutdown_event {
|
||||
message = <<"Network shutdown">>
|
||||
}),
|
||||
sdlan_channel:send_event(ChannelPid, ?PACKET_EVENT_NETWORK_SHUTDOWN, NetworkShutdownEvent),
|
||||
sdlan_channel:stop(ChannelPid, normal);
|
||||
false ->
|
||||
ok
|
||||
end
|
||||
end, UsedMap).
|
||||
|
||||
%% 解析IpAddr: <<"192.168.172/24">>
|
||||
-spec parse_ipaddr(IpAddr0 :: binary()) -> {IpAddr :: binary(), MaskLen :: integer()}.
|
||||
parse_ipaddr(IpAddr0) when is_binary(IpAddr0) ->
|
||||
case binary:split(IpAddr0, <<"/">>) of
|
||||
[IpAddr, MaskLen] ->
|
||||
MaskLen1 = binary_to_integer(MaskLen),
|
||||
{IpAddr, MaskLen1};
|
||||
_ ->
|
||||
{IpAddr0, 24}
|
||||
end.
|
||||
|
||||
-spec format_host(Host :: #host{}) -> map().
|
||||
format_host(#host{client_id = ClientId, mac = Mac, ip = Ip, hole = Hole, v6_info = V6Info}) ->
|
||||
HoleMap = case Hole of
|
||||
undefined ->
|
||||
#{};
|
||||
#hole{peer = {NatIp, NatPort}, nat_type = NatType} ->
|
||||
#{
|
||||
nat_ip => NatIp,
|
||||
nat_port => NatPort,
|
||||
nat_type => NatType
|
||||
}
|
||||
end,
|
||||
|
||||
V6InfoMap = case V6Info of
|
||||
undefined ->
|
||||
#{};
|
||||
#sdl_v6_info{v6 = V6, port = V6Port} ->
|
||||
#{v6 => V6, port => V6Port}
|
||||
end,
|
||||
|
||||
#{
|
||||
client_id => ClientId,
|
||||
mac => sdlan_util:format_mac(Mac),
|
||||
ip => sdlan_ipaddr:int_to_ipv4(Ip),
|
||||
hole_map => HoleMap,
|
||||
v6_info => V6InfoMap
|
||||
}.
|
||||
133
apps/sdlan/src/sdlan_network_coordinator.erl
Normal file
133
apps/sdlan/src/sdlan_network_coordinator.erl
Normal file
@ -0,0 +1,133 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%% 资源协调器,用来充分利用网络资源
|
||||
%%% @end
|
||||
%%% Created : 04. 6月 2024 10:55
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_network_coordinator).
|
||||
-author("anlicheng").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([checkout/0, attach/2]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {
|
||||
release_count = 0,
|
||||
network_map = #{} %% {NetworkPid => ThrottleKey :: atom()}
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec attach(NetworkPid :: pid(), ThrottleKey :: atom()) -> no_return().
|
||||
attach(NetworkPid, ThrottleKey) when is_pid(NetworkPid), is_atom(ThrottleKey) ->
|
||||
gen_server:cast(?SERVER, {attach, NetworkPid, ThrottleKey}).
|
||||
|
||||
-spec checkout() -> ok | error.
|
||||
checkout() ->
|
||||
gen_server:call(?SERVER, checkout).
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link() ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Initializes the server
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([]) ->
|
||||
%% 让渡资源定时器
|
||||
erlang:start_timer(100, self(), release_ticker),
|
||||
{ok, #state{release_count = 0}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call(checkout, _From, State = #state{release_count = Count}) ->
|
||||
case Count > 0 of
|
||||
true ->
|
||||
{reply, ok, State#state{release_count = Count - 1}};
|
||||
false ->
|
||||
{reply, error, State}
|
||||
end.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast({attach, NetworkPid, ThrottleKey}, State = #state{network_map = NetworkMap}) ->
|
||||
monitor(process, NetworkPid),
|
||||
{noreply, State#state{network_map = maps:put(NetworkPid, ThrottleKey, NetworkMap)}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({timeout, _, release_ticker}, State = #state{network_map = ChannelMap}) ->
|
||||
%% 让渡资源定时器
|
||||
erlang:start_timer(100, self(), release_ticker),
|
||||
AccReleaseCount = lists:foldl(fun(ThrottleKey, Acc) ->
|
||||
case throttle:peek(sdlan_network, ThrottleKey) of
|
||||
{ok, RestCount, LeftToReset} ->
|
||||
{ok, NetworkBindWidth} = application:get_env(sdlan, network_bind_width),
|
||||
NeedCount = erlang:ceil(NetworkBindWidth / 1000 * LeftToReset),
|
||||
Acc + max(0, RestCount - NeedCount);
|
||||
{limit_exceeded, 0, _} ->
|
||||
Acc
|
||||
end
|
||||
end, 0, maps:keys(ChannelMap)),
|
||||
% lager:debug("[sdlan_network_coordinator] can release count is: ~p", [AccReleaseCount]),
|
||||
{noreply, State#state{release_count = AccReleaseCount}};
|
||||
handle_info({'DOWN', _, process, NetworkPid, Reason}, State = #state{network_map = NetworkMap}) ->
|
||||
lager:debug("[sdlan_network_coordinator] network_pid close with reason: ~p", [Reason]),
|
||||
{noreply, State#state{network_map = maps:remove(NetworkPid, NetworkMap)}}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
117
apps/sdlan/src/sdlan_network_sup.erl
Normal file
117
apps/sdlan/src/sdlan_network_sup.erl
Normal file
@ -0,0 +1,117 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 27. 3月 2024 15:12
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_network_sup).
|
||||
-author("anlicheng").
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([ensured_network_started/1, delete_network/1, get_all_networks/0, start_network/1, reallocate_bind_width/0]).
|
||||
|
||||
%% Supervisor callbacks
|
||||
-export([init/1]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API functions
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Starts the supervisor
|
||||
-spec(start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Supervisor callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3],
|
||||
%% this function is called by the new process to find out about
|
||||
%% restart strategy, maximum restart frequency and child
|
||||
%% specifications.
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||
{ok, NetworkIds} = sdlan_api:get_all_networks(),
|
||||
Specs = lists:map(fun child_spec/1, NetworkIds),
|
||||
|
||||
set_network_bind(length(Specs)),
|
||||
|
||||
{ok, {SupFlags, Specs}}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec ensured_network_started(Id :: integer()) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||
ensured_network_started(Id) when is_integer(Id) ->
|
||||
case sdlan_network:get_pid(Id) of
|
||||
undefined ->
|
||||
case supervisor:start_child(?MODULE, child_spec(Id)) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, {'already_started', Pid}} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end;
|
||||
Pid when is_pid(Pid) ->
|
||||
{ok, Pid}
|
||||
end.
|
||||
|
||||
-spec start_network(Id :: integer()) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||
start_network(Id) when is_integer(Id) ->
|
||||
case supervisor:start_child(?MODULE, child_spec(Id)) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, {'already_started', Pid}} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
-spec get_all_networks() -> [pid()].
|
||||
get_all_networks() ->
|
||||
lists:map(fun({_Id, ChildPid, _Type, _Modules}) -> ChildPid end, supervisor:which_children(?MODULE)).
|
||||
|
||||
%% 重新分配网络带宽
|
||||
-spec reallocate_bind_width() -> no_return().
|
||||
reallocate_bind_width() ->
|
||||
ChildPids = lists:map(fun({_Id, ChildPid, _Type, _Modules}) -> ChildPid end, supervisor:which_children(?MODULE)),
|
||||
set_network_bind(length(ChildPids)).
|
||||
|
||||
-spec delete_network(NetworkId :: integer()) -> ok | {error, Reason :: any()}.
|
||||
delete_network(NetworkId) when is_integer(NetworkId) ->
|
||||
ChildId = sdlan_network:get_name(NetworkId),
|
||||
case supervisor:terminate_child(?MODULE, ChildId) of
|
||||
ok ->
|
||||
supervisor:delete_child(?MODULE, ChildId);
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec child_spec(Id :: integer()) -> map().
|
||||
child_spec(Id) when is_integer(Id) ->
|
||||
Name = sdlan_network:get_name(Id),
|
||||
#{
|
||||
id => Name,
|
||||
start => {sdlan_network, start_link, [Name, Id]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['sdlan_network']
|
||||
}.
|
||||
|
||||
set_network_bind(Count) when is_integer(Count) ->
|
||||
{ok, BindWidth} = application:get_env(sdlan, band_width),
|
||||
NetworkBindWidth = BindWidth div Count,
|
||||
application:set_env(sdlan, network_bind_width, NetworkBindWidth),
|
||||
throttle:setup(sdlan_network, NetworkBindWidth, per_second).
|
||||
3733
apps/sdlan/src/sdlan_pb.erl
Normal file
3733
apps/sdlan/src/sdlan_pb.erl
Normal file
File diff suppressed because it is too large
Load Diff
197
apps/sdlan/src/sdlan_stun.erl
Normal file
197
apps/sdlan/src/sdlan_stun.erl
Normal file
@ -0,0 +1,197 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 09. 4月 2024 17:37
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_stun).
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
-include("sdlan_pb.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/2]).
|
||||
-export([get_name/1]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {
|
||||
socket,
|
||||
stun_assist
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec get_name(Id :: integer()) -> atom().
|
||||
get_name(Id) when is_integer(Id) ->
|
||||
list_to_atom("sdlan_stun:" ++ integer_to_list(Id)).
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link(Name :: atom(), Port :: integer()) ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link(Name, Port) when is_atom(Name), is_integer(Port) ->
|
||||
gen_server:start_link({local, Name}, ?MODULE, [Port], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Initializes the server
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([Port]) ->
|
||||
%% 需要提高进程的调度优先级
|
||||
erlang:process_flag(priority, max),
|
||||
|
||||
{ok, Socket} = gen_udp:open(Port, [binary, {active, true}, {recbuf, 5 * 1024 * 1024}, {sndbuf, 5 * 1024 * 1024}]),
|
||||
inet_udp:controlling_process(Socket, self()),
|
||||
lager:debug("[sdlan_stun] start at port: ~p", [Port]),
|
||||
|
||||
case application:get_env(sdlan, stun_assist) of
|
||||
undefined ->
|
||||
{ok, #state{socket = Socket, stun_assist = undefined}};
|
||||
{ok, StunAssist} ->
|
||||
{ok, #state{socket = Socket, stun_assist = StunAssist}}
|
||||
end.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call(_Request, _From, State = #state{}) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
|
||||
%% 当前node下的转发,基于进程间的通讯
|
||||
handle_cast({stun_relay, Ip, Port, Reply}, State = #state{socket = Sock}) ->
|
||||
ok = gen_udp:send(Sock, Ip, Port, Reply),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
|
||||
handle_info({udp, Sock, Ip, Port, <<?PACKET_STUN_REQUEST:8, Body/binary>>}, State = #state{socket = Sock}) ->
|
||||
#sdl_stun_request{cookie = Cookie, client_id = ClientId, network_id = NetworkId, mac = Mac, nat_type = NatType, v6_info = V6Info} = sdlan_pb:decode_msg(Body, sdl_stun_request),
|
||||
%% 告知网络当前的ip对应的nat的映射关系
|
||||
|
||||
case sdlan_network:get_pid(NetworkId) of
|
||||
undefined ->
|
||||
lager:debug("call me here stun request 11: ~p", [NetworkId]),
|
||||
{noreply, State};
|
||||
NetworkPid when is_pid(NetworkPid) ->
|
||||
sdlan_network:update_hole(NetworkPid, ClientId, Mac, {Ip, Port}, NatType, V6Info),
|
||||
StunReply = sdlan_pb:encode_msg(#sdl_stun_reply{
|
||||
cookie = Cookie
|
||||
}),
|
||||
ok = gen_udp:send(Sock, Ip, Port, <<?PACKET_STUN_REPLY, StunReply/binary>>),
|
||||
lager:debug("call me here stun request 12"),
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
%% 网络nat类型的探测机制, 需要借助其他服务一起才能实现
|
||||
%% 辅助节点没有assist的配置,不支持attr = 2的探测
|
||||
handle_info({udp, Sock, Ip = {Ip0, Ip1, Ip2, Ip3}, Port, <<?PACKET_STUN_PROBE:8, Body/binary>>}, State = #state{socket = Sock, stun_assist = StunAssist}) ->
|
||||
#sdl_stun_probe{cookie = Cookie, attr = Attr} = sdlan_pb:decode_msg(Body, sdl_stun_probe),
|
||||
lager:debug("[sdlan_stun] get stun_probe request, att: ~p", [Attr]),
|
||||
|
||||
ProbeReply = sdlan_pb:encode_msg(#sdl_stun_probe_reply {
|
||||
cookie = Cookie,
|
||||
port = Port,
|
||||
ip = int_ip(Ip)
|
||||
}),
|
||||
Packet = <<?PACKET_STUN_PROBE_REPLY, ProbeReply/binary>>,
|
||||
|
||||
case Attr of
|
||||
?STUN_ATTR_CHANGE_NONE ->
|
||||
ok = gen_udp:send(Sock, Ip, Port, Packet);
|
||||
?STUN_ATTR_CHANGE_PORT ->
|
||||
gen_server:cast('sdlan_stun:1:2', {stun_relay, Ip, Port, Packet});
|
||||
?STUN_ATTR_CHANGE_PEER ->
|
||||
case StunAssist of
|
||||
{AssistIp, AssistPort} ->
|
||||
gen_udp:send(Sock, AssistIp, AssistPort, <<?PACKET_STUN_PROBE_RELAY, Ip0, Ip1, Ip2, Ip3, Port:16, Packet/binary>>);
|
||||
undefined ->
|
||||
ok
|
||||
end
|
||||
end,
|
||||
{noreply, State};
|
||||
|
||||
%% 转发消息, 跨服务器的stun_reply的转发通过socket来转发
|
||||
handle_info({udp, Sock, _, _, <<?PACKET_STUN_PROBE_RELAY:8, Ip0, Ip1, Ip2, Ip3, Port:16, Reply/binary>>}, State = #state{socket = Sock}) ->
|
||||
lager:debug("[sdlan_stun] get stun_probe_replay request, reply: ~p", [Reply]),
|
||||
gen_udp:send(Sock, {Ip0, Ip1, Ip2, Ip3}, Port, Reply),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({udp, _, _Ip, _Port, <<?PACKET_STUN_DATA, Body/binary>>}, State = #state{socket = Sock}) ->
|
||||
Data = #sdl_data{network_id = NetworkId, src_mac = SrcMac, dst_mac = DstMac, ttl = TTL} = sdlan_pb:decode_msg(Body, sdl_data),
|
||||
|
||||
lager:debug("[sdlan_stun] stun data, src_mac: ~p, dst_mac: ~p", [sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
|
||||
%% 重新打包数据ttl需要减1
|
||||
case sdlan_network:get_pid(NetworkId) of
|
||||
NetworkPid when is_pid(NetworkPid) ->
|
||||
NData = sdlan_pb:encode_msg(Data#sdl_data{ttl = TTL - 1, is_p2p = false}),
|
||||
sdlan_network:forward(NetworkPid, Sock, SrcMac, DstMac, <<?PACKET_STUN_DATA, NData/binary>>);
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
|
||||
{noreply, State};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
lager:error("[sdlan_stun] get a unknown message: ~p, channel will closed", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec int_ip(tuple()) -> integer().
|
||||
int_ip({Ip0, Ip1, Ip2, Ip3}) ->
|
||||
<<Ip:32>> = <<Ip0, Ip1, Ip2, Ip3>>,
|
||||
Ip.
|
||||
71
apps/sdlan/src/sdlan_sup.erl
Normal file
71
apps/sdlan/src/sdlan_sup.erl
Normal file
@ -0,0 +1,71 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%% @doc sdlan top level supervisor.
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(sdlan_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
%% sup_flags() = #{strategy => strategy(), % optional
|
||||
%% intensity => non_neg_integer(), % optional
|
||||
%% period => pos_integer()} % optional
|
||||
%% child_spec() = #{id => child_id(), % mandatory
|
||||
%% start => mfargs(), % mandatory
|
||||
%% restart => restart(), % optional
|
||||
%% shutdown => shutdown(), % optional
|
||||
%% type => worker(), % optional
|
||||
%% modules => modules()} % optional
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||
|
||||
Specs = [
|
||||
#{
|
||||
id => sdlan_network_coordinator,
|
||||
start => {sdlan_network_coordinator, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['sdlan_network_coordinator']
|
||||
},
|
||||
#{
|
||||
id => sdlan_network_sup,
|
||||
start => {sdlan_network_sup, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => supervisor,
|
||||
modules => ['sdlan_network_sup']
|
||||
}
|
||||
],
|
||||
|
||||
{ok, {SupFlags, pools() ++ Specs ++ stun_specs()}}.
|
||||
|
||||
%% internal functions
|
||||
|
||||
pools() ->
|
||||
{ok, Pools} = application:get_env(sdlan, pools),
|
||||
lists:map(fun({Name, PoolArgs, WorkerArgs}) ->
|
||||
poolboy:child_spec(Name, [{name, {local, Name}}|PoolArgs], WorkerArgs)
|
||||
end, Pools).
|
||||
|
||||
stun_specs() ->
|
||||
{ok, StunServers} = application:get_env(sdlan, stun_servers),
|
||||
lists:map(fun({Name, Port}) ->
|
||||
#{
|
||||
id => Name,
|
||||
start => {sdlan_stun, start_link, [Name, Port]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['sdlan_stun']
|
||||
}
|
||||
end, StunServers).
|
||||
18
apps/sdlan/src/sdlan_test.erl
Normal file
18
apps/sdlan/src/sdlan_test.erl
Normal file
@ -0,0 +1,18 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 09. 3月 2024 15:25
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_test).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([test/1]).
|
||||
|
||||
test(X) when X band 1 == 0 ->
|
||||
ok;
|
||||
test(_) ->
|
||||
error.
|
||||
74
apps/sdlan/src/sdlan_util.erl
Normal file
74
apps/sdlan/src/sdlan_util.erl
Normal file
@ -0,0 +1,74 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 11. 3月 2024 11:10
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_util).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([rand_byte/1, md5/1, format_mac/1, assert_call/2]).
|
||||
-export([json_data/1, json_error/2]).
|
||||
-export([is_broadcast_mac/1, is_multicast_mac/1]).
|
||||
|
||||
-spec format_mac(Mac :: binary()) -> binary().
|
||||
format_mac(Mac) when is_binary(Mac) ->
|
||||
Hex = fun
|
||||
(N) when N < 10 ->
|
||||
$0 + N;
|
||||
(N) ->
|
||||
$a + (N - 10)
|
||||
end,
|
||||
Y = [[Hex(X0), Hex(X1)] || <<X0:4, X1:4>> <= Mac],
|
||||
list_to_binary(lists:flatten(lists:join(":", Y))).
|
||||
|
||||
%% 生成随机字节
|
||||
rand_byte(Num) when is_integer(Num), Num > 0 ->
|
||||
rand_byte0(Num, <<>>).
|
||||
rand_byte0(0, Acc) ->
|
||||
Acc;
|
||||
rand_byte0(Num, Acc) ->
|
||||
Byte = ceil(rand:uniform() * 255),
|
||||
rand_byte0(Num - 1, <<Acc/binary, Byte>>).
|
||||
|
||||
%% md5哈希算法
|
||||
-spec md5(string() | binary()) -> string().
|
||||
md5(Str) when is_binary(Str) ->
|
||||
md5(binary_to_list(Str));
|
||||
md5(Str) when is_list(Str) ->
|
||||
Hash = binary_to_list(erlang:md5(Str)),
|
||||
lists:flatten([hex(I) || I <- Hash]).
|
||||
|
||||
hex(I) when I > 16#f ->
|
||||
[hex0((I band 16#f0) bsr 4), hex0(I band 16#0f)];
|
||||
hex(I) ->
|
||||
[$0, hex0(I)].
|
||||
hex0(10) -> $a;
|
||||
hex0(11) -> $b;
|
||||
hex0(12) -> $c;
|
||||
hex0(13) -> $d;
|
||||
hex0(14) -> $e;
|
||||
hex0(15) -> $f;
|
||||
hex0(I) -> $0 + I.
|
||||
|
||||
json_data(Data) ->
|
||||
jiffy:encode(#{<<"result">> => Data}, [force_utf8]).
|
||||
|
||||
json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) ->
|
||||
jiffy:encode(#{<<"error">> => #{<<"code">> => ErrCode, <<"message">> => ErrMessage}}, [force_utf8]).
|
||||
|
||||
assert_call(true, F) ->
|
||||
F();
|
||||
assert_call(false, _) ->
|
||||
ok.
|
||||
|
||||
-spec is_broadcast_mac(Mac :: binary()) -> boolean().
|
||||
is_broadcast_mac(Mac) when is_binary(Mac) ->
|
||||
Mac =:= <<16#FF,16#FF,16#FF,16#FF,16#FF,16#FF>>.
|
||||
|
||||
-spec is_multicast_mac(Mac :: binary()) -> boolean().
|
||||
is_multicast_mac(Mac) when is_binary(Mac) ->
|
||||
binary:part(Mac, 0, 3) =:= <<16#01,16#00,16#5E>>.
|
||||
101
apps/sdlan/src/test/sdlan_tcp_client.erl
Normal file
101
apps/sdlan/src/test/sdlan_tcp_client.erl
Normal file
@ -0,0 +1,101 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 29. 3月 2024 14:32
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_tcp_client).
|
||||
-author("anlicheng").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {
|
||||
socket
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link() ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Initializes the server
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([]) ->
|
||||
{ok, Socket} = gen_tcp:connect("localhost", 18083, [binary, {packet, 2}, {active, true}]),
|
||||
ok = gen_tcp:send(Socket, <<"hello world">>),
|
||||
{ok, #state{socket = Socket}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call(_Request, _From, State = #state{}) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast(_Request, State = #state{}) ->
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info(_Info, State = #state{}) ->
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
110
apps/sdlan/src/test/sdlan_udp_downloader.erl
Normal file
110
apps/sdlan/src/test/sdlan_udp_downloader.erl
Normal file
@ -0,0 +1,110 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 17. 4月 2024 10:35
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_udp_downloader).
|
||||
-author("anlicheng").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
|
||||
code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {
|
||||
socket
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link() ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Initializes the server
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([]) ->
|
||||
{ok, Socket} = gen_udp:open(22222, [binary]),
|
||||
{ok, #state{socket = Socket}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({udp, Sock, Ip, Port, <<1>>}, State = #state{socket = Sock}) ->
|
||||
{ok, Content} = file:read_file("/tmp/files/test.dmg"),
|
||||
send_file_content(Sock, Ip, Port, 1200, Content),
|
||||
{noreply, State#state{}}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
send_file_content(Sock, Ip, Port, Size, Content) when byte_size(Content) =< Size ->
|
||||
gen_udp:send(Sock, Ip, Port, Content);
|
||||
send_file_content(Sock, Ip, Port, Size, Content) ->
|
||||
<<Part:Size/binary, Rest/binary>> = Content,
|
||||
gen_udp:send(Sock, Ip, Port, Part),
|
||||
send_file_content(Sock, Ip, Port, Size, Rest).
|
||||
114
apps/sdlan/src/test/sdlan_udp_wget.erl
Normal file
114
apps/sdlan/src/test/sdlan_udp_wget.erl
Normal file
@ -0,0 +1,114 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 17. 4月 2024 10:35
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_udp_wget).
|
||||
-author("anlicheng").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([wget/0]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {
|
||||
socket,
|
||||
bytes = 0
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
wget() ->
|
||||
gen_server:call(?MODULE, wget).
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link() ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Initializes the server
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([]) ->
|
||||
{ok, Socket} = gen_udp:open(0, [binary, {active, true}]),
|
||||
inet_udp:controlling_process(Socket, self()),
|
||||
|
||||
erlang:start_timer(5000, self(), qps_ticker),
|
||||
|
||||
{ok, #state{socket = Socket}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call(wget, _From, State=#state{socket = Socket}) ->
|
||||
gen_udp:send(Socket, "127.0.0.1", 22222, <<1>>),
|
||||
{reply, ok, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({udp, Sock, _Ip, _Port, Data}, State = #state{socket = Sock, bytes = Bytes}) ->
|
||||
{noreply, State#state{bytes = Bytes + byte_size(Data)}};
|
||||
handle_info({timeout, _, qps_ticker}, State = #state{bytes = Bytes}) ->
|
||||
lager:debug("[sdlan_udp_wget] qps is: ~p(M)", [Bytes / 1024 / 1024]),
|
||||
erlang:start_timer(5000, self(), qps_ticker),
|
||||
{noreply, State#state{bytes = 0}}.
|
||||
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
208
apps/sdlan/src/test/stun_client.erl
Normal file
208
apps/sdlan/src/test/stun_client.erl
Normal file
@ -0,0 +1,208 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 08. 4月 2024 10:37
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(stun_client).
|
||||
-author("anlicheng").
|
||||
-include("sdlan_pb.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([register/1, debug_info/1]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
|
||||
code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
%% 请求
|
||||
-define(STUN_REGISTER, 3).
|
||||
%% 响应
|
||||
-define(STUN_REGISTER_ACK, 4).
|
||||
|
||||
-define(STUN_DATA, 5).
|
||||
|
||||
-record(state, {
|
||||
socket,
|
||||
tun_socket,
|
||||
client_id :: binary(),
|
||||
network_id,
|
||||
net_addr,
|
||||
mask_len,
|
||||
aes_key,
|
||||
cookie = 1,
|
||||
|
||||
sessions = #{}
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
register(Pid) when is_pid(Pid) ->
|
||||
gen_server:call(Pid, register).
|
||||
|
||||
debug_info(Pid) ->
|
||||
gen_server:call(Pid, debug_info).
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link() ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link() ->
|
||||
gen_server:start_link(?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Initializes the server
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([]) ->
|
||||
{ok, Socket} = gen_tcp:connect("localhost", 18083, [binary, {packet, 2}, {active, true}]),
|
||||
inet_tcp:controlling_process(Socket, self()),
|
||||
{ok, TunSocket} = gen_udp:open(12345, [binary, {active, true}]),
|
||||
|
||||
{ok, #state{socket = Socket, tun_socket = TunSocket, client_id = <<"22222222222222222222222222222222">>}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call(debug_info, _From, State) ->
|
||||
{reply, {ok, State}, State};
|
||||
|
||||
handle_call(register, _From, State = #state{socket = Socket, client_id = ClientId}) ->
|
||||
Req = #{
|
||||
<<"version">> => 1,
|
||||
<<"client_id">> => ClientId,
|
||||
<<"dev_addr">> => #{
|
||||
<<"net_addr">> => 0,
|
||||
<<"net_bit_len">> => 0
|
||||
},
|
||||
<<"token">> => <<"1234567890">>
|
||||
},
|
||||
|
||||
Register = #sdl_register_super {
|
||||
version = 1,
|
||||
installed_channel = <<"macos">>,
|
||||
client_id = ClientId,
|
||||
dev_addr = #sdl_dev_addr {
|
||||
network_id = 0,
|
||||
mac = <<11, 12, 13, 14, 15, 16>>,
|
||||
net_addr = 0,
|
||||
net_bit_len = 0
|
||||
},
|
||||
pub_key = <<>>,
|
||||
token = <<"1234567890">>
|
||||
},
|
||||
|
||||
lager:debug("register is: ~p", [Register]),
|
||||
|
||||
Packet = jiffy:encode(Req, [force_utf8]),
|
||||
ok = gen_tcp:send(Socket, <<1:32, 101, Packet/binary>>),
|
||||
|
||||
{reply, ok, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast(_Request, State = #state{}) ->
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({tcp, Socket, <<1:32, 5, Data/binary>>}, State = #state{socket = Socket, tun_socket = TunSocket, client_id = ClientId, cookie = Cookie}) ->
|
||||
Response = jiffy:decode(Data, [return_maps]),
|
||||
#{
|
||||
<<"dev_addr">> := #{
|
||||
<<"network_id">> := NetworkId,
|
||||
<<"net_addr">> := NetAddr,
|
||||
<<"net_bit_len">> := NetBitLen
|
||||
},
|
||||
<<"aes_key">> := AesKey,
|
||||
<<"lifetime">> := Lifetime
|
||||
} = Response,
|
||||
|
||||
lager:debug("[stun_client] get a register super response: ~p, alloc ip addr: ~p", [Response, sdlan_ipaddr:int_to_ipv4(NetAddr)]),
|
||||
|
||||
%% 开始注册自己的tun信息
|
||||
gen_udp:send(TunSocket, "localhost", 1265, <<1, Cookie:32, ClientId/binary, NetworkId:32, NetAddr:32>>),
|
||||
|
||||
{noreply, State#state{network_id = NetworkId, net_addr = NetAddr, mask_len = NetBitLen, aes_key = AesKey, cookie = Cookie + 1}};
|
||||
|
||||
handle_info({udp, _, _, _, <<2, Cookie:32, Family, Port:16, Ip0, Ip1, Ip2, Ip3>>}, State = #state{}) ->
|
||||
lager:debug("[stun_client] tun register ack, cookie: ~p, ack: ~p", [Cookie, {Family, Port, {Ip0, Ip1, Ip2, Ip3}}]),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({udp, _, Ip, Port, <<?STUN_REGISTER:8, NetworkId:32, SrcIp:32, DstIp:32>>}, State = #state{tun_socket = TunSocket, sessions = Sessions}) ->
|
||||
Packet = <<?STUN_REGISTER_ACK, NetworkId:32, DstIp:32, SrcIp:32>>,
|
||||
lager:debug("[stun_client] will send stun reply: ~p, peer: ~p", [Packet, {Ip, Port}]),
|
||||
ok = gen_udp:send(TunSocket, Ip, Port, Packet),
|
||||
|
||||
NSessions = maps:put(SrcIp, {Ip, Port}, Sessions),
|
||||
{noreply, State#state{sessions = NSessions}};
|
||||
|
||||
handle_info({udp, _, Ip, Port, <<?STUN_REGISTER_ACK, NetworkId:32, SrcIp:32, DstIp:32>>}, State = #state{sessions = Sessions}) ->
|
||||
lager:debug("[stun_client] stun_data: network_id: ~p, src: ~p, dst: ~p, register_ack!!!", [NetworkId, SrcIp, DstIp]),
|
||||
NSessions = maps:put(SrcIp, {Ip, Port}, Sessions),
|
||||
{noreply, State#state{sessions = NSessions}};
|
||||
|
||||
handle_info({udp, _, _Ip0, _Port0, <<?STUN_DATA, NetworkId:32, SrcIp:32, DstIp:32, TTL:8, Data/binary>>}, State = #state{tun_socket = TunSocket, sessions = Sessions}) ->
|
||||
lager:debug("[stun_client] stun_data: network_id: ~p, src: ~p, dst: ~p, data!!!", [NetworkId, SrcIp, DstIp]),
|
||||
case maps:is_key(SrcIp, Sessions) of
|
||||
true ->
|
||||
{Ip, Port} = maps:get(SrcIp, Sessions),
|
||||
ok = gen_udp:send(TunSocket, Ip, Port, <<?STUN_DATA, NetworkId:32, DstIp:32, SrcIp:32, 255, Data/binary>>),
|
||||
lager:debug("[stun_client] stun_data: network_id: ~p, src: ~p, dst: ~p, reply data!!!", [NetworkId, SrcIp, DstIp]);
|
||||
false ->
|
||||
lager:debug("[stun_client] stun_data: network_id: ~p, src: ~p, dst: ~p, no session", [NetworkId, SrcIp, DstIp])
|
||||
end,
|
||||
{noreply, State};
|
||||
|
||||
handle_info(Info, State = #state{}) ->
|
||||
lager:debug("[stun_client] get info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
86
config/sys-dev.config
Normal file
86
config/sys-dev.config
Normal file
@ -0,0 +1,86 @@
|
||||
[
|
||||
{sdlan, [
|
||||
|
||||
{http_server, [
|
||||
{port, 18082},
|
||||
{acceptors, 500},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
{tcp_server, [
|
||||
{port, 18083},
|
||||
{acceptors, 500},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
%% 网络带宽, 单位为: kb
|
||||
{band_width, 2048},
|
||||
|
||||
{stun_servers, [{'sdlan_stun:1:1', 1265}, {'sdlan_stun:1:2', 1266}]},
|
||||
{stun_assist, {{47,98,178,3}, 1266}},
|
||||
|
||||
% {stun_servers, [{'sdlan_stun:2:1', 1265}, {'sdlan_stun:2:2', 1266}]},
|
||||
|
||||
{pools, [
|
||||
%% mysql连接池配置
|
||||
{mysql_sdlan,
|
||||
[{size, 10}, {max_overflow, 20}, {worker_module, mysql}],
|
||||
[
|
||||
{host, {39, 98, 184, 67}},
|
||||
{port, 3306},
|
||||
{user, "sdlanuser"},
|
||||
{connect_mode, lazy},
|
||||
{keep_alive, true},
|
||||
{password, "sdlan@J1c8WGu"},
|
||||
{database, "sdlan"},
|
||||
{queries, [<<"set names utf8">>]}
|
||||
]
|
||||
}
|
||||
]},
|
||||
|
||||
{api_url, "http://127.0.0.1:18082/test/"}
|
||||
|
||||
]},
|
||||
|
||||
{throttle, [
|
||||
{driver, throttle_ets},
|
||||
{access_context, sync_transaction}
|
||||
]},
|
||||
|
||||
%% 系统日志配置,系统日志为lager, 支持日志按日期自动分割
|
||||
{lager, [
|
||||
{colored, true},
|
||||
%% Whether to write a crash log, and where. Undefined means no crash logger.
|
||||
{crash_log, "trade_hub.crash.log"},
|
||||
%% Maximum size in bytes of events in the crash log - defaults to 65536
|
||||
{crash_log_msg_size, 65536},
|
||||
%% Maximum size of the crash log in bytes, before its rotated, set
|
||||
%% to 0 to disable rotation - default is 0
|
||||
{crash_log_size, 10485760},
|
||||
%% What time to rotate the crash log - default is no time
|
||||
%% rotation. See the README for a description of this format.
|
||||
{crash_log_date, "$D0"},
|
||||
%% Number of rotated crash logs to keep, 0 means keep only the
|
||||
%% current one - default is 0
|
||||
{crash_log_count, 5},
|
||||
%% Whether to redirect error_logger messages into lager - defaults to true
|
||||
{error_logger_redirect, true},
|
||||
|
||||
%% How big the gen_event mailbox can get before it is switched into sync mode
|
||||
{async_threshold, 20},
|
||||
%% Switch back to async mode, when gen_event mailbox size decrease from `async_threshold'
|
||||
%% to async_threshold - async_threshold_window
|
||||
{async_threshold_window, 5},
|
||||
|
||||
{handlers, [
|
||||
%% debug | info | warning | error, 日志级别
|
||||
{lager_console_backend, debug},
|
||||
{lager_file_backend, [{file, "debug.log"}, {level, debug}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "notice.log"}, {level, notice}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "error.log"}, {level, error}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "info.log"}, {level, info}, {size, 314572800}]}
|
||||
]}
|
||||
]}
|
||||
].
|
||||
86
config/sys-prod.config
Normal file
86
config/sys-prod.config
Normal file
@ -0,0 +1,86 @@
|
||||
[
|
||||
{sdlan, [
|
||||
|
||||
{http_server, [
|
||||
{port, 18082},
|
||||
{acceptors, 500},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
{tcp_server, [
|
||||
{port, 18083},
|
||||
{acceptors, 500},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
%% 网络带宽, 单位为: kb
|
||||
{band_width, 2048},
|
||||
|
||||
{stun_servers, [{'sdlan_stun:1:1', 1265}, {'sdlan_stun:1:2', 1266}]},
|
||||
{stun_assist, {{47,98,178,3}, 1266}},
|
||||
|
||||
% {stun_servers, [{'sdlan_stun:2:1', 1265}, {'sdlan_stun:2:2', 1266}]},
|
||||
|
||||
{pools, [
|
||||
%% mysql连接池配置
|
||||
{mysql_sdlan,
|
||||
[{size, 10}, {max_overflow, 20}, {worker_module, mysql}],
|
||||
[
|
||||
{host, {39, 98, 184, 67}},
|
||||
{port, 3306},
|
||||
{user, "sdlanuser"},
|
||||
{connect_mode, lazy},
|
||||
{keep_alive, true},
|
||||
{password, "sdlan@J1c8WGu"},
|
||||
{database, "sdlan"},
|
||||
{queries, [<<"set names utf8">>]}
|
||||
]
|
||||
}
|
||||
]},
|
||||
|
||||
{api_url, "http://39.98.184.67:8200/api/"}
|
||||
|
||||
]},
|
||||
|
||||
{throttle, [
|
||||
{driver, throttle_ets},
|
||||
{access_context, sync_transaction}
|
||||
]},
|
||||
|
||||
%% 系统日志配置,系统日志为lager, 支持日志按日期自动分割
|
||||
{lager, [
|
||||
{colored, true},
|
||||
%% Whether to write a crash log, and where. Undefined means no crash logger.
|
||||
{crash_log, "trade_hub.crash.log"},
|
||||
%% Maximum size in bytes of events in the crash log - defaults to 65536
|
||||
{crash_log_msg_size, 65536},
|
||||
%% Maximum size of the crash log in bytes, before its rotated, set
|
||||
%% to 0 to disable rotation - default is 0
|
||||
{crash_log_size, 10485760},
|
||||
%% What time to rotate the crash log - default is no time
|
||||
%% rotation. See the README for a description of this format.
|
||||
{crash_log_date, "$D0"},
|
||||
%% Number of rotated crash logs to keep, 0 means keep only the
|
||||
%% current one - default is 0
|
||||
{crash_log_count, 5},
|
||||
%% Whether to redirect error_logger messages into lager - defaults to true
|
||||
{error_logger_redirect, true},
|
||||
|
||||
%% How big the gen_event mailbox can get before it is switched into sync mode
|
||||
{async_threshold, 20},
|
||||
%% Switch back to async mode, when gen_event mailbox size decrease from `async_threshold'
|
||||
%% to async_threshold - async_threshold_window
|
||||
{async_threshold_window, 5},
|
||||
|
||||
{handlers, [
|
||||
%% debug | info | warning | error, 日志级别
|
||||
{lager_console_backend, debug},
|
||||
{lager_file_backend, [{file, "debug.log"}, {level, debug}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "notice.log"}, {level, notice}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "error.log"}, {level, error}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "info.log"}, {level, info}, {size, 314572800}]}
|
||||
]}
|
||||
]}
|
||||
].
|
||||
6
config/vm.args
Normal file
6
config/vm.args
Normal file
@ -0,0 +1,6 @@
|
||||
-sname sdlan
|
||||
|
||||
-setcookie sdlan_cookie
|
||||
|
||||
+K true
|
||||
+A30
|
||||
152
message.proto
Normal file
152
message.proto
Normal file
@ -0,0 +1,152 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// 基础公共类型定义
|
||||
|
||||
message SDLV4Info {
|
||||
uint32 port = 1;
|
||||
bytes v4 = 2;
|
||||
uint32 nat_type = 3;
|
||||
}
|
||||
|
||||
message SDLV6Info {
|
||||
uint32 port = 1;
|
||||
bytes v6 = 2;
|
||||
}
|
||||
|
||||
// 设备网络地址信息
|
||||
message SDLDevAddr {
|
||||
uint32 network_id = 1;
|
||||
bytes mac = 2;
|
||||
uint32 net_addr = 3;
|
||||
uint32 net_bit_len = 4;
|
||||
}
|
||||
|
||||
// tcp通讯消息
|
||||
message SDLEmpty {
|
||||
|
||||
}
|
||||
|
||||
message SDLRegisterSuper {
|
||||
uint32 version = 1;
|
||||
string installed_channel = 2;
|
||||
string client_id = 3;
|
||||
SDLDevAddr dev_addr = 4;
|
||||
string pub_key = 5;
|
||||
string token = 6;
|
||||
}
|
||||
|
||||
message SDLRegisterSuperAck {
|
||||
SDLDevAddr dev_addr = 1;
|
||||
bytes aes_key = 2;
|
||||
uint32 upgrade_type = 3;
|
||||
optional string upgrade_prompt = 4;
|
||||
optional string upgrade_address = 5;
|
||||
}
|
||||
|
||||
message SDLRegisterSuperNak {
|
||||
uint32 error_code = 1;
|
||||
string error_message = 2;
|
||||
}
|
||||
|
||||
// 网络地址查询
|
||||
|
||||
message SDLQueryInfo {
|
||||
bytes dst_mac = 1;
|
||||
}
|
||||
|
||||
message SDLPeerInfo {
|
||||
bytes dst_mac = 1;
|
||||
SDLV4Info v4_info = 2;
|
||||
optional SDLV6Info v6_info = 3;
|
||||
}
|
||||
|
||||
// 事件定义
|
||||
|
||||
message SDLNatChangedEvent {
|
||||
bytes mac = 1;
|
||||
uint32 ip = 2;
|
||||
}
|
||||
|
||||
message SDLSendRegisterEvent {
|
||||
bytes dst_mac = 1;
|
||||
uint32 nat_ip = 2;
|
||||
uint32 nat_port = 3;
|
||||
uint32 nat_type = 4;
|
||||
optional SDLV6Info v6_info = 5;
|
||||
}
|
||||
|
||||
message SDLNetworkShutdownEvent {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
// 命令定义
|
||||
|
||||
message SDLChangeNetworkCommand {
|
||||
SDLDevAddr dev_addr = 1;
|
||||
bytes aes_key = 2;
|
||||
}
|
||||
|
||||
message SDLCommandAck {
|
||||
// status = true, 表示成功;status = false 表示失败,message是失败原因描述
|
||||
bool status = 1;
|
||||
optional string message = 2;
|
||||
}
|
||||
|
||||
message SDLFlows {
|
||||
// 服务器转发流量
|
||||
uint32 forward_num = 1;
|
||||
// p2p直接流量
|
||||
uint32 p2p_num = 2;
|
||||
// 接收的流量
|
||||
uint32 inbound_num = 3;
|
||||
}
|
||||
|
||||
// UDP通讯消息
|
||||
|
||||
message SDLStunRequest {
|
||||
uint32 cookie = 1;
|
||||
string client_id = 2;
|
||||
uint32 network_id = 3;
|
||||
bytes mac = 4;
|
||||
uint32 ip = 5;
|
||||
uint32 nat_type = 6;
|
||||
optional SDLV6Info v6_info = 7;
|
||||
}
|
||||
|
||||
message SDLStunReply {
|
||||
uint32 cookie = 1;
|
||||
}
|
||||
|
||||
message SDLData {
|
||||
uint32 network_id = 1;
|
||||
bytes src_mac = 2;
|
||||
bytes dst_mac = 3;
|
||||
bool is_p2p = 4;
|
||||
uint32 ttl = 5;
|
||||
bytes data = 6;
|
||||
}
|
||||
|
||||
message SDLRegister {
|
||||
uint32 network_id = 1;
|
||||
bytes src_mac = 2;
|
||||
bytes dst_mac = 3;
|
||||
}
|
||||
|
||||
message SDLRegisterAck {
|
||||
uint32 network_id = 1;
|
||||
bytes src_mac = 2;
|
||||
bytes dst_mac = 3;
|
||||
}
|
||||
|
||||
// 网络类型探测
|
||||
|
||||
message SDLStunProbe {
|
||||
uint32 cookie = 1;
|
||||
uint32 attr = 2;
|
||||
}
|
||||
|
||||
message SDLStunProbeReply {
|
||||
uint32 cookie = 1;
|
||||
uint32 port = 2;
|
||||
uint32 ip = 3;
|
||||
}
|
||||
48
rebar.config
Normal file
48
rebar.config
Normal file
@ -0,0 +1,48 @@
|
||||
{erl_opts, [debug_info]}.
|
||||
{deps, [
|
||||
{poolboy, ".*", {git, "https://github.com/devinus/poolboy.git", {tag, "1.5.1"}}},
|
||||
{hackney, ".*", {git, "https://github.com/benoitc/hackney.git", {tag, "1.16.0"}}},
|
||||
{sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}},
|
||||
{esockd, ".*", {git, "https://github.com/emqx/esockd.git", {tag, "v5.7.3"}}},
|
||||
{jiffy, ".*", {git, "https://github.com/davisp/jiffy.git", {tag, "1.1.1"}}},
|
||||
{cowboy, ".*", {git, "https://github.com/ninenines/cowboy.git", {tag, "2.12.0"}}},
|
||||
{mysql, ".*", {git, "https://github.com/mysql-otp/mysql-otp", {tag, "1.8.0"}}},
|
||||
{gpb, ".*", {git, "https://github.com/tomas-abrahamsson/gpb.git", {tag, "4.21.1"}}},
|
||||
{throttle, ".*", {git, "https://github.com/lambdaclass/throttle.git", {tag, "0.3.0"}}},
|
||||
{parse_trans, ".*", {git, "https://github.com/uwiger/parse_trans", {tag, "3.0.0"}}},
|
||||
{lager, ".*", {git,"https://github.com/erlang-lager/lager.git", {tag, "3.9.2"}}}
|
||||
]}.
|
||||
|
||||
{relx, [{release, {sdlan, "0.1.0"},
|
||||
[sdlan,
|
||||
sasl]},
|
||||
|
||||
{mode, dev},
|
||||
|
||||
%% automatically picked up if the files
|
||||
%% exist but can be set manually, which
|
||||
%% is required if the names aren't exactly
|
||||
%% sys.config and vm.args
|
||||
{sys_config, "./config/sys.config"},
|
||||
{vm_args, "./config/vm.args"}
|
||||
|
||||
%% the .src form of the configuration files do
|
||||
%% not require setting RELX_REPLACE_OS_VARS
|
||||
%% {sys_config_src, "./config/sys.config.src"},
|
||||
%% {vm_args_src, "./config/vm.args.src"}
|
||||
]}.
|
||||
|
||||
{profiles, [{prod, [{relx,
|
||||
[%% prod is the default mode when prod
|
||||
%% profile is used, so does not have
|
||||
%% to be explicitly included like this
|
||||
{mode, prod}
|
||||
|
||||
%% use minimal mode to exclude ERTS
|
||||
%% {mode, minimal}
|
||||
]
|
||||
}]}]}.
|
||||
|
||||
{erl_opts, [{parse_transform,lager_transform}]}.
|
||||
|
||||
{rebar_packages_cdn, "https://hexpm.upyun.com"}.
|
||||
81
rebar.lock
Normal file
81
rebar.lock
Normal file
@ -0,0 +1,81 @@
|
||||
{"1.2.0",
|
||||
[{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.2">>},1},
|
||||
{<<"cowboy">>,
|
||||
{git,"https://github.com/ninenines/cowboy.git",
|
||||
{ref,"3ea8395eb8f53a57acb5d3c00b99c70296e7cdbd"}},
|
||||
0},
|
||||
{<<"cowlib">>,
|
||||
{git,"https://github.com/ninenines/cowlib",
|
||||
{ref,"1eb7f4293a652adcfe43b1835d22c58d8def839f"}},
|
||||
1},
|
||||
{<<"esockd">>,
|
||||
{git,"https://github.com/emqx/esockd.git",
|
||||
{ref,"d9ce4024cc42a65e9a05001997031e743442f955"}},
|
||||
0},
|
||||
{<<"fs">>,{pkg,<<"fs">>,<<"6.1.1">>},1},
|
||||
{<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1},
|
||||
{<<"gpb">>,
|
||||
{git,"https://github.com/tomas-abrahamsson/gpb.git",
|
||||
{ref,"a53bc4909b3dc5a78b996263d92db38fed9d4782"}},
|
||||
0},
|
||||
{<<"hackney">>,
|
||||
{git,"https://github.com/benoitc/hackney.git",
|
||||
{ref,"f3e9292db22c807e73f57a8422402d6b423ddf5f"}},
|
||||
0},
|
||||
{<<"idna">>,{pkg,<<"idna">>,<<"6.0.1">>},1},
|
||||
{<<"jiffy">>,
|
||||
{git,"https://github.com/davisp/jiffy.git",
|
||||
{ref,"9ea1b35b6e60ba21dfd4adbd18e7916a831fd7d4"}},
|
||||
0},
|
||||
{<<"lager">>,
|
||||
{git,"https://github.com/erlang-lager/lager.git",
|
||||
{ref,"459a3b2cdd9eadd29e5a7ce5c43932f5ccd6eb88"}},
|
||||
0},
|
||||
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1},
|
||||
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},1},
|
||||
{<<"mysql">>,
|
||||
{git,"https://github.com/mysql-otp/mysql-otp",
|
||||
{ref,"caf5ff96c677a8fe0ce6f4082bc036c8fd27dd62"}},
|
||||
0},
|
||||
{<<"parse_trans">>,
|
||||
{git,"https://github.com/uwiger/parse_trans",
|
||||
{ref,"6f3645afb43c7c57d61b54ef59aecab288ce1013"}},
|
||||
0},
|
||||
{<<"poolboy">>,
|
||||
{git,"https://github.com/devinus/poolboy.git",
|
||||
{ref,"3bb48a893ff5598f7c73731ac17545206d259fac"}},
|
||||
0},
|
||||
{<<"ranch">>,
|
||||
{git,"https://github.com/ninenines/ranch",
|
||||
{ref,"a692f44567034dacf5efcaa24a24183788594eb7"}},
|
||||
1},
|
||||
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},1},
|
||||
{<<"sync">>,
|
||||
{git,"https://github.com/rustyio/sync.git",
|
||||
{ref,"7dc303ed4ce8d26db82e171dbbd7c41067852c65"}},
|
||||
0},
|
||||
{<<"throttle">>,
|
||||
{git,"https://github.com/lambdaclass/throttle.git",
|
||||
{ref,"4f5fb17c9d4a86ba016e7011648ae5dfe539ac01"}},
|
||||
0},
|
||||
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.5.0">>},2}]}.
|
||||
[
|
||||
{pkg_hash,[
|
||||
{<<"certifi">>, <<"B7CFEAE9D2ED395695DD8201C57A2D019C0C43ECAF8B8BCB9320B40D6662F340">>},
|
||||
{<<"fs">>, <<"9D147B944D60CFA48A349F12D06C8EE71128F610C90870BDF9A6773206452ED0">>},
|
||||
{<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>},
|
||||
{<<"idna">>, <<"1D038FB2E7668CE41FBF681D2C45902E52B3CB9E9C77B55334353B222C2EE50C">>},
|
||||
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
|
||||
{<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>},
|
||||
{<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>},
|
||||
{<<"unicode_util_compat">>, <<"8516502659002CEC19E244EBD90D312183064BE95025A319A6C7E89F4BCCD65B">>}]},
|
||||
{pkg_hash_ext,[
|
||||
{<<"certifi">>, <<"3B3B5F36493004AC3455966991EAF6E768CE9884693D9968055AEEEB1E575040">>},
|
||||
{<<"fs">>, <<"EF94E95FFE79916860649FED80AC62B04C322B0BB70F5128144C026B4D171F8B">>},
|
||||
{<<"goldrush">>, <<"99CB4128CFFCB3227581E5D4D803D5413FA643F4EB96523F77D9E6937D994CEB">>},
|
||||
{<<"idna">>, <<"A02C8A1C4FD601215BB0B0324C8A6986749F807CE35F25449EC9E69758708122">>},
|
||||
{<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
|
||||
{<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>},
|
||||
{<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>},
|
||||
{<<"unicode_util_compat">>, <<"D48D002E15F5CC105A696CF2F1BBB3FC72B4B770A184D8420C8DB20DA2674B38">>}]}
|
||||
].
|
||||
6
run
Executable file
6
run
Executable file
@ -0,0 +1,6 @@
|
||||
#! /bin/sh
|
||||
|
||||
rebar3 compile && rebar3 release
|
||||
|
||||
_build/default/rel/sdlan/bin/sdlan console
|
||||
|
||||
4
shell
Executable file
4
shell
Executable file
@ -0,0 +1,4 @@
|
||||
#! /bin/sh
|
||||
|
||||
./_build/default/rel/sdlan/bin/sdlan remote
|
||||
|
||||
58
tap.md
Normal file
58
tap.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Tap修改的点
|
||||
|
||||
## 数据结构的改变
|
||||
|
||||
### 1. 设备网络地址信息
|
||||
```text
|
||||
message SDLDevAddr {
|
||||
uint32 network_id = 1;
|
||||
uint32 net_addr = 2;
|
||||
uint32 net_bit_len = 3;
|
||||
// 增加mac地址信息,6个字节的bytes
|
||||
bytes mac = 4;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. QueryInfo的请求和响应的修改
|
||||
|
||||
```text
|
||||
message SDLQueryInfo {
|
||||
// 改为基于mac地址查询
|
||||
bytes dst_mac = 1;
|
||||
}
|
||||
|
||||
message SDLPeerInfo {
|
||||
bytes dst_mac = 1;
|
||||
SDLV4Info v4_info = 2;
|
||||
optional SDLV6Info v6_info = 3;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Data数据消息体
|
||||
|
||||
```text
|
||||
message SDLData {
|
||||
uint32 network_id = 1;
|
||||
bytes src_mac = 2;
|
||||
bytes dst_mac = 3;
|
||||
bool is_p2p = 4;
|
||||
uint32 ttl = 5;
|
||||
bytes data = 6;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.
|
||||
```text
|
||||
message SDLRegister {
|
||||
uint32 network_id = 1;
|
||||
uint32 src_ip = 2;
|
||||
uint32 dst_ip = 3;
|
||||
}
|
||||
|
||||
message SDLRegisterAck {
|
||||
uint32 network_id = 1;
|
||||
uint32 src_ip = 2;
|
||||
uint32 dst_ip = 3;
|
||||
}
|
||||
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user