init project

This commit is contained in:
anlicheng 2025-05-12 11:54:24 +08:00
commit e2d0048f23
48 changed files with 8583 additions and 0 deletions

20
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
# 需要完善的事情
## 后端网络需要增加一个aes_key采用AES256加密算法key的长度为32个字符

View 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()
}).

View 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.

View 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
}).

View 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
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

View 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}.

View 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.

View 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">>)}.

View 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">>)}.

View 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
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

View 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.

View 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).

View 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.

View 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]).

View 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).

View 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)}.

View 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, []}
]}.

View 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.

View 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]).

View 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)).

View 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}]).

View 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(_) ->
<<"">>.

View 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_idchannel不关闭; 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
}.

View 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
%%%===================================================================

View 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

File diff suppressed because it is too large Load Diff

View 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.

View 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).

View 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.

View 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>>.

View 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
%%%===================================================================

View 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).

View 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
%%%===================================================================

View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
-sname sdlan
-setcookie sdlan_cookie
+K true
+A30

152
message.proto Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
#! /bin/sh
rebar3 compile && rebar3 release
_build/default/rel/sdlan/bin/sdlan console

4
shell Executable file
View File

@ -0,0 +1,4 @@
#! /bin/sh
./_build/default/rel/sdlan/bin/sdlan remote

58
tap.md Normal file
View 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;
}
```