Compare commits
52 Commits
main
...
202601_v2_
| Author | SHA1 | Date | |
|---|---|---|---|
| 16555f1520 | |||
| b3a51adc95 | |||
| 79eded8c96 | |||
| 6f2322174d | |||
| 83361ef1b2 | |||
| f6494df5d1 | |||
| 39a32ec73a | |||
| 08942fe73d | |||
| 86d60c9ff0 | |||
| 588b05b24b | |||
| 42dbb48e18 | |||
| 48721a0578 | |||
| 1f0cade371 | |||
| 37d46e5bbc | |||
| 11ac0a5b8c | |||
| 9403259513 | |||
| 160fb4d92b | |||
| 672da8d7b1 | |||
| a4efafd484 | |||
| d272258312 | |||
| 388096a29a | |||
| 8bdc5868c3 | |||
| 6cce8f7e5e | |||
| c9adbb5e16 | |||
| d64cb4235f | |||
| 75fa239ae9 | |||
| eb35558fe7 | |||
| eb11c05ba5 | |||
| a4f2ed8428 | |||
| f5a07e41f7 | |||
| 7cd645eb17 | |||
| abf6e11a34 | |||
| c1f196fa96 | |||
| ce69bfd730 | |||
| ab74389f07 | |||
| 771896045b | |||
| 715103bd1f | |||
| 92b3256646 | |||
| 97019a87de | |||
| 2394db892c | |||
| c1676115a6 | |||
| a17453022d | |||
| 775ec28485 | |||
| 609bcd6cd5 | |||
| 1ae54ab276 | |||
| e2fd4f18cb | |||
| a597ecbf2e | |||
| ef8ded9f35 | |||
| d7db914e88 | |||
| 8d94244689 | |||
| be855d6985 | |||
| c89091c205 |
128
API.md
128
API.md
@ -1,10 +1,30 @@
|
||||
# SDLAN 交互文档
|
||||
# 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: get_all_networks
|
||||
url: /api/get_all_networks
|
||||
method: get
|
||||
return:
|
||||
|
||||
@ -16,7 +36,7 @@ return:
|
||||
|
||||
### 2. 获取单个网络信息
|
||||
```text
|
||||
url: get_network?id=:id
|
||||
url: /api/get_network?id=:id
|
||||
method: get
|
||||
return:
|
||||
{"result":
|
||||
@ -27,7 +47,6 @@ return:
|
||||
"ipaddr": "192.168.0.1/24",
|
||||
"domain": "punchnet.cn",
|
||||
"owner_id": 1234,
|
||||
"algorithm": "aes|chacha20",
|
||||
"disabled_clients": ["client_id_xyz", "client_id_xyz1"]
|
||||
}
|
||||
}
|
||||
@ -36,30 +55,58 @@ return:
|
||||
|
||||
```
|
||||
|
||||
### 3. access_token校验
|
||||
### 3.token校验
|
||||
```text
|
||||
url: auth/access_token
|
||||
url: /api/auth_token
|
||||
method: post
|
||||
params:
|
||||
network_id: int,
|
||||
client_id: string
|
||||
mac: string,
|
||||
ip: string,
|
||||
mask_len: int,
|
||||
hostname: string,
|
||||
access_token: string,
|
||||
token: string,
|
||||
version: int // 当前客户端版本
|
||||
return:
|
||||
|
||||
{"result": "ok"}
|
||||
{"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
|
||||
```
|
||||
|
||||
### 3.网络校验
|
||||
```text
|
||||
url: /api/check_network
|
||||
method: post
|
||||
params:
|
||||
client_id: string
|
||||
code: 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: set_node_status
|
||||
url: /api/set_node_status
|
||||
method: post
|
||||
params:
|
||||
client_id: string
|
||||
@ -71,6 +118,21 @@ 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
|
||||
|
||||
### 网络管理接口
|
||||
@ -114,23 +176,6 @@ return:
|
||||
|
||||
```
|
||||
|
||||
#### 3. 设置网络出口
|
||||
```text
|
||||
url: /network/exit_node_control
|
||||
method: post
|
||||
params:
|
||||
id: int, //网络id
|
||||
action: int // 0: 关闭,1: 开启
|
||||
client_id: string // 目标节点的mac地址
|
||||
remark: string // 下发的标签,用于跟踪测试问题,可为空
|
||||
timeout: int // 超时设置,单位为秒
|
||||
return:
|
||||
|
||||
{"result": "success"}
|
||||
{"error": {"code": 1, "message": "错误描述"}}
|
||||
|
||||
```
|
||||
|
||||
### 客户端节点管理
|
||||
|
||||
#### 1. 禁用节点
|
||||
@ -147,4 +192,23 @@ 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": "错误描述"}}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
189
Register.md
189
Register.md
@ -1,189 +0,0 @@
|
||||
# 注册流程
|
||||
|
||||
## 1. 发送验证码
|
||||
url: /register/sendVerfiyCode
|
||||
method: POST
|
||||
params: {
|
||||
username: $username,
|
||||
}
|
||||
|
||||
result:
|
||||
success: {code: 0, data: {"session_id": int}}
|
||||
error: {code: -1, message: "操作失败"}
|
||||
|
||||
## 2. 提交验证码
|
||||
url: /register/verfiyCode
|
||||
method: POST
|
||||
params: {
|
||||
session_id: $session_id,
|
||||
code: int
|
||||
}
|
||||
|
||||
result:
|
||||
success: {code: 0, data: "ok"}
|
||||
error: {code: -1, message: "验证码错误"}
|
||||
|
||||
## 3. 注册
|
||||
url: /register/submit
|
||||
method: POST
|
||||
params: {
|
||||
session_id: $session_id,
|
||||
password: $password
|
||||
}
|
||||
|
||||
result:
|
||||
success: {code: 0, data: "ok"}
|
||||
error: {code: -1, message: "注册失败"}
|
||||
|
||||
|
||||
# 找回密码流程
|
||||
|
||||
## 1. 发送验证码
|
||||
url: /password/sendVerfiyCode
|
||||
method: POST
|
||||
params: {
|
||||
username: $username,
|
||||
}
|
||||
|
||||
result:
|
||||
success: {code: 0, data: {"session_id": int}}
|
||||
error: {code: -1, message: "操作失败"}
|
||||
|
||||
## 2. 提交验证码
|
||||
url: /password/verfiyCode
|
||||
method: POST
|
||||
params: {
|
||||
session_id: $session_id,
|
||||
code: int
|
||||
}
|
||||
|
||||
result:
|
||||
success: {code: 0, data: "ok"}
|
||||
error: {code: -1, message: "验证码错误"}
|
||||
|
||||
## 3. 重置
|
||||
url: /password/reset
|
||||
method: POST
|
||||
params: {
|
||||
session_id: $session_id,
|
||||
new_password: $new_password
|
||||
}
|
||||
|
||||
result:
|
||||
success: {code: 0, data: "ok"}
|
||||
error: {code: -1, message: "注册失败"}
|
||||
|
||||
# 注册和找回密码公用的session的结构
|
||||
|
||||
```text
|
||||
session:
|
||||
{
|
||||
session_id,
|
||||
username,
|
||||
code,
|
||||
verified,
|
||||
expire_at,
|
||||
used
|
||||
}
|
||||
|
||||
✅ 3. 必须有过期时间
|
||||
|
||||
建议:
|
||||
|
||||
验证码:5分钟
|
||||
|
||||
session:10分钟
|
||||
|
||||
✅ 4. 限流(强烈建议)
|
||||
sendVerifyCode
|
||||
|
||||
每个 username:60 秒一次
|
||||
|
||||
每个 IP:限制
|
||||
|
||||
✅ 5. 防枚举(重要)
|
||||
|
||||
错误返回统一:
|
||||
|
||||
"操作失败"
|
||||
|
||||
不要区分:
|
||||
|
||||
用户存在
|
||||
|
||||
用户不存在
|
||||
```
|
||||
|
||||
# app相关的接口
|
||||
|
||||
## 1. 检查版本
|
||||
url: /app/checkUpdate
|
||||
method: POST
|
||||
|
||||
params: {
|
||||
app_id: "your_app", // 应用标识
|
||||
platform: "macos", // 平台
|
||||
version: "1.2.3", // 当前版本
|
||||
build: 123, // 构建号(非常重要)
|
||||
channel: "appstore|direct", // 渠道(可选)
|
||||
}
|
||||
|
||||
result:
|
||||
success: {
|
||||
code: 0,
|
||||
data: {
|
||||
has_update: true,
|
||||
latest_version: "1.3.0",
|
||||
latest_build: 150,
|
||||
|
||||
force_update: false,
|
||||
|
||||
download_url: "https://xxx.com/app.pkg",
|
||||
release_notes: "修复了一些问题",
|
||||
|
||||
min_supported_version: "1.0.0",
|
||||
|
||||
publish_time: 1710000000
|
||||
}
|
||||
}
|
||||
|
||||
error: {
|
||||
code: -1,
|
||||
message: "检查失败"
|
||||
}
|
||||
|
||||
## 2. app当前隐私政策和服务条款
|
||||
url: /app/policies
|
||||
method: POST
|
||||
params: {
|
||||
platform: "macos"
|
||||
}
|
||||
|
||||
result:
|
||||
{
|
||||
code: 0,
|
||||
data: {
|
||||
privacy_policy_url: "https://xxx.com/privacy",
|
||||
terms_of_service_url: "https://xxx.com/terms",
|
||||
|
||||
privacy_policy_version: "2026-01-01",
|
||||
terms_version: "2026-01-01"
|
||||
}
|
||||
}
|
||||
|
||||
## 3. 用户反馈
|
||||
url: /app/issue
|
||||
method: POST
|
||||
params: {
|
||||
username: $username,
|
||||
// 联系方式
|
||||
contact: $contact,
|
||||
platform: "macos",
|
||||
content: $content
|
||||
}
|
||||
|
||||
result:
|
||||
{
|
||||
code: 0,
|
||||
data: "ok"
|
||||
}
|
||||
@ -13,10 +13,7 @@
|
||||
|
||||
-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_NAK, 16#04).
|
||||
@ -26,19 +23,17 @@
|
||||
-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_COMMAND, 16#11).
|
||||
-define(PACKET_COMMAND_ACK, 16#12).
|
||||
%% 定义事件信息
|
||||
-define(PACKET_EVENT_DROP_MACS, 16#02).
|
||||
-define(PACKET_EVENT_NAT_CHANGED, 16#03).
|
||||
-define(PACKET_EVENT_SEND_REGISTER, 16#04).
|
||||
-define(PACKET_EVENT_REFRESH_AUTH, 16#05).
|
||||
|
||||
%% 网络流量统计
|
||||
-define(PACKET_FLOW_TRACER, 16#15).
|
||||
%% 网络关闭
|
||||
-define(PACKET_EVENT_NETWORK_SHUTDOWN, 16#FF).
|
||||
|
||||
-define(PACKET_REGISTER, 16#20).
|
||||
-define(PACKET_REGISTER_ACK, 16#21).
|
||||
@ -46,8 +41,6 @@
|
||||
%% stun相关的请求
|
||||
%% 请求
|
||||
-define(PACKET_STUN_REQUEST, 16#30).
|
||||
%% 响应
|
||||
-define(PACKET_STUN_REPLY, 16#31).
|
||||
|
||||
%% stun网络类型检测
|
||||
%% 请求
|
||||
@ -57,21 +50,23 @@
|
||||
%% stun消息转发
|
||||
-define(PACKET_STUN_PROBE_RELAY, 16#3a).
|
||||
|
||||
%% 权限控制
|
||||
-define(PACKET_POLICY_REQUEST, 16#b0).
|
||||
-define(PACKET_POLICY_REPLY, 16#b1).
|
||||
|
||||
%% 欢迎消息
|
||||
-define(PACKET_WELCOME, 16#4F).
|
||||
|
||||
%% ARP查询
|
||||
-define(PACKET_ARP_REQUEST, 16#50).
|
||||
-define(PACKET_ARP_RESPONSE, 16#51).
|
||||
%% 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).
|
||||
|
||||
%% stun请求的attr
|
||||
-define(STUN_ATTR_CHANGE_NONE, 0).
|
||||
-define(STUN_ATTR_CHANGE_PORT, 1).
|
||||
-define(STUN_ATTR_CHANGE_PEER, 2).
|
||||
%% arp查询 request -> response
|
||||
-define(PACKET_ARP_REQUEST, 16#a0).
|
||||
-define(PACKET_ARP_RESPONSE, 16#a1).
|
||||
|
||||
%% 权限请求
|
||||
-define(PACKET_POLICY_REQUEST, 16#b0).
|
||||
-define(PACKET_POLICY_RESPONSE, 16#b1).
|
||||
|
||||
-record(id_generator, {
|
||||
tab :: atom(),
|
||||
increment_id = 0 :: integer()
|
||||
}).
|
||||
251
apps/sdlan/include/sdlan_pb.hrl
Normal file
251
apps/sdlan/include/sdlan_pb.hrl
Normal file
@ -0,0 +1,251 @@
|
||||
%% -*- 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_EMPTY_PB_H').
|
||||
-define('SDL_EMPTY_PB_H', true).
|
||||
-record(sdl_empty,
|
||||
{pkt_id = 0 :: non_neg_integer() | undefined % = 1, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_REGISTER_SUPER_PB_H').
|
||||
-define('SDL_REGISTER_SUPER_PB_H', true).
|
||||
-record(sdl_register_super,
|
||||
{pkt_id = 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
|
||||
mask_len = 0 :: non_neg_integer() | undefined, % = 6, optional, 32 bits
|
||||
hostname = <<>> :: unicode:chardata() | undefined, % = 7, optional
|
||||
pub_key = <<>> :: unicode:chardata() | undefined, % = 8, optional
|
||||
access_token = <<>> :: unicode:chardata() | undefined % = 9, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_REGISTER_SUPER_ACK_PB_H').
|
||||
-define('SDL_REGISTER_SUPER_ACK_PB_H', true).
|
||||
-record(sdl_register_super_ack,
|
||||
{pkt_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
aes_key = <<>> :: iodata() | undefined, % = 2, optional
|
||||
session_token = <<>> :: iodata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_REGISTER_SUPER_NAK_PB_H').
|
||||
-define('SDL_REGISTER_SUPER_NAK_PB_H', true).
|
||||
-record(sdl_register_super_nak,
|
||||
{pkt_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
error_code = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
error_message = <<>> :: unicode:chardata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_QUERY_INFO_PB_H').
|
||||
-define('SDL_QUERY_INFO_PB_H', true).
|
||||
-record(sdl_query_info,
|
||||
{pkt_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
network_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
src_mac = <<>> :: iodata() | undefined, % = 3, optional
|
||||
dst_mac = <<>> :: iodata() | undefined, % = 4, optional
|
||||
session_token = <<>> :: iodata() | undefined % = 5, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_PEER_INFO_PB_H').
|
||||
-define('SDL_PEER_INFO_PB_H', true).
|
||||
-record(sdl_peer_info,
|
||||
{pkt_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
network_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
dst_mac = <<>> :: iodata() | undefined, % = 3, optional
|
||||
v4_info = undefined :: sdlan_pb:sdl_v4_info() | undefined, % = 4, optional
|
||||
v6_info :: sdlan_pb:sdl_v6_info() | undefined % = 5, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_EMPTY_PEER_INFO_PB_H').
|
||||
-define('SDL_EMPTY_PEER_INFO_PB_H', true).
|
||||
-record(sdl_empty_peer_info,
|
||||
{pkt_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
network_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
dst_mac = <<>> :: iodata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_NAT_CHANGED_EVENT_PB_H').
|
||||
-define('SDL_NAT_CHANGED_EVENT_PB_H', true).
|
||||
-record(sdl_nat_changed_event,
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
mac = <<>> :: iodata() | undefined, % = 2, optional
|
||||
ip = 0 :: non_neg_integer() | undefined % = 3, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_DROP_MACS_EVENT_PB_H').
|
||||
-define('SDL_DROP_MACS_EVENT_PB_H', true).
|
||||
-record(sdl_drop_macs_event,
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
macs = [] :: [iodata()] | undefined % = 2, repeated
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_REFRESH_AUTH_EVENT_PB_H').
|
||||
-define('SDL_REFRESH_AUTH_EVENT_PB_H', true).
|
||||
-record(sdl_refresh_auth_event,
|
||||
{network_id = 0 :: non_neg_integer() | undefined % = 1, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_SEND_REGISTER_EVENT_PB_H').
|
||||
-define('SDL_SEND_REGISTER_EVENT_PB_H', true).
|
||||
-record(sdl_send_register_event,
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
dst_mac = <<>> :: iodata() | undefined, % = 2, optional
|
||||
nat_ip = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
nat_port = 0 :: non_neg_integer() | undefined, % = 4, optional, 32 bits
|
||||
nat_type = 0 :: non_neg_integer() | undefined, % = 5, optional, 32 bits
|
||||
v6_info :: sdlan_pb:sdl_v6_info() | undefined % = 6, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_NETWORK_SHUTDOWN_EVENT_PB_H').
|
||||
-define('SDL_NETWORK_SHUTDOWN_EVENT_PB_H', true).
|
||||
-record(sdl_network_shutdown_event,
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
message = <<>> :: unicode:chardata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_STUN_REQUEST_PB_H').
|
||||
-define('SDL_STUN_REQUEST_PB_H', true).
|
||||
-record(sdl_stun_request,
|
||||
{client_id = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
network_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
mac = <<>> :: iodata() | undefined, % = 3, optional
|
||||
ip = 0 :: non_neg_integer() | undefined, % = 4, optional, 32 bits
|
||||
nat_type = 0 :: non_neg_integer() | undefined, % = 5, optional, 32 bits
|
||||
v6_info :: sdlan_pb:sdl_v6_info() | undefined, % = 6, optional
|
||||
session_token = <<>> :: iodata() | undefined % = 7, optional
|
||||
}).
|
||||
-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
|
||||
session_token = <<>> :: iodata() | undefined, % = 7, optional
|
||||
identity_id = 0 :: non_neg_integer() | undefined % = 8, optional, 32 bits
|
||||
}).
|
||||
-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
|
||||
step = 0 :: non_neg_integer() | undefined % = 3, 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
|
||||
step = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
port = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
ip = 0 :: non_neg_integer() | undefined % = 4, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_ARP_REQUEST_PB_H').
|
||||
-define('SDL_ARP_REQUEST_PB_H', true).
|
||||
-record(sdl_arp_request,
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
target_ip = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
session_token = <<>> :: iodata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_ARP_RESPONSE_PB_H').
|
||||
-define('SDL_ARP_RESPONSE_PB_H', true).
|
||||
-record(sdl_arp_response,
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
target_ip = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
target_mac = <<>> :: iodata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_POLICY_REQUEST_PB_H').
|
||||
-define('SDL_POLICY_REQUEST_PB_H', true).
|
||||
-record(sdl_policy_request,
|
||||
{client_id = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
network_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
mac = <<>> :: iodata() | undefined, % = 3, optional
|
||||
src_identity_id = 0 :: non_neg_integer() | undefined, % = 4, optional, 32 bits
|
||||
dst_identity_id = 0 :: non_neg_integer() | undefined, % = 5, optional, 32 bits
|
||||
version = 0 :: non_neg_integer() | undefined, % = 6, optional, 32 bits
|
||||
session_token = <<>> :: iodata() | undefined % = 7, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDL_POLICY_RESPONSE_PB_H').
|
||||
-define('SDL_POLICY_RESPONSE_PB_H', true).
|
||||
-record(sdl_policy_response,
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
src_identity_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
dst_identity_id = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
version = 0 :: non_neg_integer() | undefined, % = 4, optional, 32 bits
|
||||
total_num = 0 :: non_neg_integer() | undefined, % = 5, optional, 32 bits
|
||||
index = 0 :: non_neg_integer() | undefined, % = 6, optional, 32 bits
|
||||
rules = <<>> :: iodata() | undefined % = 7, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-endif.
|
||||
19
apps/sdlan/include/sdlan_tables.hrl
Normal file
19
apps/sdlan/include/sdlan_tables.hrl
Normal file
@ -0,0 +1,19 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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(),
|
||||
host_name :: binary(),
|
||||
%% 当前状态
|
||||
status = normal :: normal | disabled
|
||||
}).
|
||||
59
apps/sdlan/src/dns_proxy/dns_cache.erl
Normal file
59
apps/sdlan/src/dns_proxy/dns_cache.erl
Normal file
@ -0,0 +1,59 @@
|
||||
-module(dns_cache).
|
||||
-include_lib("dns_proxy.hrl").
|
||||
-include_lib("dns_erlang/include/dns.hrl").
|
||||
-include_lib("dns_erlang/include/dns_records.hrl").
|
||||
-include_lib("dns_erlang/include/dns_terms.hrl").
|
||||
|
||||
-export([init/0, lookup/1, insert/2]).
|
||||
|
||||
-define(TABLE, dns_cache).
|
||||
|
||||
init() ->
|
||||
ets:new(?TABLE, [named_table, set, public, {keypos, 2}, {read_concurrency, true}]).
|
||||
|
||||
lookup(#dns_query{name = Qname, type = QType, class = QClass}) ->
|
||||
Key = {Qname, QType, QClass},
|
||||
case ets:lookup(?TABLE, Key) of
|
||||
[Cache = #dns_cache{expire_at = ExpireAt}] ->
|
||||
Now = os:system_time(second),
|
||||
case ExpireAt > Now of
|
||||
true ->
|
||||
{hit, Cache};
|
||||
false ->
|
||||
true = ets:delete(?TABLE, Key),
|
||||
miss
|
||||
end;
|
||||
[] ->
|
||||
miss
|
||||
end.
|
||||
|
||||
insert(#dns_query{name = Qname, type = QType, class = QClass},
|
||||
#dns_message{answers = Answers, authority = Authority, additional = Additional, rc = RCode, aa = AA}) ->
|
||||
TTLs = lists:foldl(fun(Term, Acc) ->
|
||||
case Term of
|
||||
#dns_rr{ttl = TTL} ->
|
||||
[TTL|Acc];
|
||||
_ ->
|
||||
Acc
|
||||
end
|
||||
end, [], Answers ++ Authority ++ Additional),
|
||||
case length(TTLs) > 0 of
|
||||
true ->
|
||||
TTL = lists:min(TTLs),
|
||||
ExpireAt = os:system_time(second) + TTL,
|
||||
logger:debug("min ttl is: ~p, expire_at: ~p", [TTL, ExpireAt]),
|
||||
Key = {Qname, QType, QClass},
|
||||
Cache = #dns_cache{
|
||||
key = Key,
|
||||
answers = Answers,
|
||||
authority = Authority,
|
||||
additional = Additional,
|
||||
rc = RCode,
|
||||
flags = #{aa => AA},
|
||||
% unix time
|
||||
expire_at = ExpireAt
|
||||
},
|
||||
true = ets:insert(?TABLE, Cache);
|
||||
false ->
|
||||
ok
|
||||
end.
|
||||
263
apps/sdlan/src/dns_proxy/dns_handler.erl
Normal file
263
apps/sdlan/src/dns_proxy/dns_handler.erl
Normal file
@ -0,0 +1,263 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 03. 12月 2025 23:00
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(dns_handler).
|
||||
-author("anlicheng").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include_lib("dns_erlang/include/dns.hrl").
|
||||
-include_lib("pkt/include/pkt.hrl").
|
||||
-include("dns_proxy.hrl").
|
||||
|
||||
%% 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]).
|
||||
-export([handle_ip_packet/5]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
-define(RESOLVER_POOL, dns_resolver_pool).
|
||||
|
||||
%% 协议部分
|
||||
-define(TCP_PROTOCOL, 6).
|
||||
-define(UDP_PROTOCOL, 17).
|
||||
|
||||
-record(state, {}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
start_link() ->
|
||||
gen_server:start_link(?MODULE, [], []).
|
||||
|
||||
handle_ip_packet(Pid, Sock, SrcIp, SrcPort, Packet) when is_pid(Pid) ->
|
||||
gen_server:cast(Pid, {handle_ip_packet, Sock, SrcIp, SrcPort, Packet}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% 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, #state{}}.
|
||||
|
||||
%% @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({handle_ip_packet, Sock, SrcIp, SrcPort, IpPacket}, State) ->
|
||||
{#ipv4{saddr = ReqSAddr, daddr = ReqDAddr, p = Protocol}, ReqIpPayload} = pkt:ipv4(IpPacket),
|
||||
case Protocol =:= ?UDP_PROTOCOL of
|
||||
true ->
|
||||
{#udp{sport = ReqSPort, dport = ReqDPort}, UdpPayload} = pkt:udp(ReqIpPayload),
|
||||
case resolver(UdpPayload) of
|
||||
{ok, DnsResp} ->
|
||||
RespIpPacket = build_ip_packet(ReqDAddr, ReqSAddr, ReqDPort, ReqSPort, DnsResp),
|
||||
gen_udp:send(Sock, SrcIp, SrcPort, RespIpPacket);
|
||||
{error, Reason} ->
|
||||
logger:notice("[dns_handler] resolver get error: ~p", [Reason])
|
||||
end;
|
||||
false ->
|
||||
logger:notice("[dns_handler] resolver invalid protocol: ~p", [Protocol])
|
||||
end,
|
||||
{stop, normal, 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) ->
|
||||
{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 resolver(Packet :: binary()) -> {ok, Resp :: binary()} | {error, Reason :: any()}.
|
||||
resolver(Packet) when is_binary(Packet) ->
|
||||
resolver0(Packet, dns:decode_message(Packet)).
|
||||
resolver0(Packet, QueryMsg = #dns_message{qc = 1, questions = [Question = #dns_query{name = QName, type = QType, class = QClass}|_]}) ->
|
||||
%% 查找是否是内置的域名
|
||||
case sdlan_hostname_regedit:lookup(QName) of
|
||||
{ok, Ip} ->
|
||||
Answer = #dns_rr {
|
||||
name = QName,
|
||||
type = QType,
|
||||
class = QClass,
|
||||
ttl = 300,
|
||||
data = #dns_rrdata_a {
|
||||
ip = Ip
|
||||
}
|
||||
},
|
||||
RespMsg = QueryMsg#dns_message{
|
||||
qr = true,
|
||||
ra = true,
|
||||
anc = 1,
|
||||
auc = 0,
|
||||
adc = 0,
|
||||
answers = [Answer],
|
||||
authority = [],
|
||||
additional = []
|
||||
},
|
||||
logger:debug("[dns_handler] punchnet inbuilt qnanme: ~p, ip: ~p", [QName, Ip]),
|
||||
{ok, dns:encode_message(RespMsg)};
|
||||
error ->
|
||||
%% 是否命中内部的域名后缀
|
||||
EmptyDnsResp = dns:encode_message(build_nxdomain_response(QueryMsg)),
|
||||
case sdlan_domain_regedit:maybe_domain(QName) of
|
||||
true ->
|
||||
logger:debug("[dns_handler] punchnet inbuilt qnanme: ~p, nxdomain", [QName]),
|
||||
{ok, EmptyDnsResp};
|
||||
false ->
|
||||
case dns_cache:lookup(Question) of
|
||||
{hit, Cache} ->
|
||||
logger:debug("[dns_handler] qname: ~p, hit cache answers: ~p", [QName, Cache#dns_cache.answers]),
|
||||
RespMsg = build_response(QueryMsg, Cache),
|
||||
{ok, dns:encode_message(RespMsg)};
|
||||
miss ->
|
||||
Ref = make_ref(),
|
||||
forward_to_upstream(Ref, Packet, QueryMsg),
|
||||
logger:debug("[dns_handler] cache is miss, forward_to_upstream"),
|
||||
receive
|
||||
{dns_resolver_reply, Ref, Resp} ->
|
||||
case dns:decode_message(Resp) of
|
||||
RespMsg = #dns_message{answers = Answers} ->
|
||||
logger:debug("[dns_handler] get a response answers: ~p", [Answers]),
|
||||
dns_cache:insert(Question, RespMsg),
|
||||
{ok, Resp};
|
||||
Error ->
|
||||
logger:debug("[dns_handler] parse reply get error: ~p", [Error]),
|
||||
{ok, EmptyDnsResp}
|
||||
end
|
||||
after 5000 ->
|
||||
logger:debug("[dns_handler] forward_to_upstream timeout"),
|
||||
{ok, EmptyDnsResp}
|
||||
end
|
||||
end
|
||||
end
|
||||
end;
|
||||
resolver0(_, Error) ->
|
||||
logger:warning("[dns_handler] decode dns_query get error: ~p", [Error]),
|
||||
{error, Error}.
|
||||
|
||||
-spec forward_to_upstream(Ref :: reference(), Request :: binary(), QueryMsg :: #dns_message{}) -> no_return().
|
||||
forward_to_upstream(Ref, Request, QueryMsg) ->
|
||||
ReceiverPid = self(),
|
||||
poolboy:transaction(?RESOLVER_POOL, fun(Pid) -> dns_resolver:forward(Pid, ReceiverPid, Ref, Request, QueryMsg) end).
|
||||
|
||||
-spec build_response(QueryMsg :: #dns_message{}, Dns_cache :: #dns_cache{}) -> RespMsg :: #dns_message{}.
|
||||
build_response(QueryMsg, #dns_cache{expire_at = ExpireAt, answers = Answers, authority = Authority, additional = Additional, rc = RCode, flags = #{aa := AA}}) ->
|
||||
Now = os:system_time(second),
|
||||
RemainingTTL = ExpireAt - Now,
|
||||
|
||||
Answers2 = [adjust_ttl(RR, RemainingTTL) || RR <- Answers],
|
||||
Authority2 = [adjust_ttl(RR, RemainingTTL) || RR <- Authority],
|
||||
Additional2 = [adjust_ttl(RR, RemainingTTL) || RR <- Additional],
|
||||
|
||||
QueryMsg#dns_message{
|
||||
qr = true,
|
||||
ra = true,
|
||||
aa = AA,
|
||||
rc = RCode,
|
||||
anc = length(Answers2),
|
||||
auc = length(Authority2),
|
||||
adc = length(Additional2),
|
||||
answers = Answers2,
|
||||
authority = Authority2,
|
||||
additional = Additional2
|
||||
}.
|
||||
|
||||
-spec adjust_ttl(RR :: any(), RemainingTTL :: integer()) -> any().
|
||||
adjust_ttl(RR = #dns_rr{}, RemainingTTL) ->
|
||||
RR#dns_rr{ttl = max(0, RemainingTTL)};
|
||||
adjust_ttl(RR, _RemainingTTL) ->
|
||||
RR.
|
||||
|
||||
-spec build_nxdomain_response(QueryMsg :: #dns_message{}) -> EmptyResp :: #dns_message{}.
|
||||
build_nxdomain_response(QueryMsg) ->
|
||||
QueryMsg#dns_message{
|
||||
qr = true,
|
||||
aa = true,
|
||||
ra = true,
|
||||
rc = ?DNS_RCODE_NXDOMAIN,
|
||||
anc = 0,
|
||||
auc = 0,
|
||||
adc = 0,
|
||||
answers = [],
|
||||
authority = [],
|
||||
additional = []
|
||||
}.
|
||||
|
||||
-spec build_ip_packet(SAddr :: inet:ip4_address(), DAddr :: inet:ip4_address(), SPort :: integer(), DPort :: integer(), Payload :: binary()) -> IpPacket :: binary().
|
||||
build_ip_packet(SAddr, DAddr, SPort, DPort, UdpPayload) when is_integer(SPort), is_integer(DPort), is_binary(UdpPayload) ->
|
||||
ULen = 8 + byte_size(UdpPayload),
|
||||
RespUdpHeader = pkt:udp(#udp{
|
||||
sport = SPort,
|
||||
dport = DPort,
|
||||
ulen = ULen,
|
||||
sum = dns_utils:udp_checksum(SAddr, DAddr, SPort, DPort, UdpPayload)
|
||||
}),
|
||||
IpPayload = <<RespUdpHeader/binary, UdpPayload/binary>>,
|
||||
|
||||
IpPacket0 = #ipv4{
|
||||
len = 20 + ULen,
|
||||
ttl = 64,
|
||||
off = 0,
|
||||
mf = 0,
|
||||
sum = 0,
|
||||
p = ?UDP_PROTOCOL,
|
||||
saddr = SAddr,
|
||||
daddr = DAddr,
|
||||
opt = <<>>
|
||||
},
|
||||
IpCheckSum = dns_utils:ip_checksum(IpPacket0),
|
||||
IpHeader = pkt:ipv4(IpPacket0#ipv4{sum = IpCheckSum}),
|
||||
|
||||
<<IpHeader/binary, IpPayload/binary>>.
|
||||
@ -1,22 +1,22 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 13. 2月 2026 18:00
|
||||
%%% Created : 03. 12月 2025 17:29
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_quic_channel_sup).
|
||||
-module(dns_handler_sup).
|
||||
-author("anlicheng").
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([start_channel/2]).
|
||||
|
||||
%% Supervisor callbacks
|
||||
-export([init/1]).
|
||||
-export([start_handler/0]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
@ -38,26 +38,29 @@ start_link() ->
|
||||
%% this function is called by the new process to find out about
|
||||
%% restart strategy, maximum restart frequency and child
|
||||
%% specifications.
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, {SupFlags :: supervisor:sup_flags(), [ChildSpec :: supervisor:child_spec()]}}
|
||||
| ignore).
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => simple_one_for_one, intensity => 0, period => 1},
|
||||
|
||||
AChild = #{
|
||||
id => sdlan_quic_transport,
|
||||
start => {'sdlan_quic_transport', start_link, []},
|
||||
Spec = #{
|
||||
id => dns_handler,
|
||||
start => {'dns_handler', start_link, []},
|
||||
restart => temporary,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['sdlan_quic_transport']
|
||||
modules => ['dns_handler']
|
||||
},
|
||||
{ok, {SupFlags, [AChild]}}.
|
||||
|
||||
{ok, {SupFlags, [Spec]}}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_channel(NConn :: quicer:connection_handle(), Limits :: proplists:proplist()) -> supervisor:startchild_ret().
|
||||
start_channel(NConn, Limits) when is_list(Limits) ->
|
||||
supervisor:start_child(?MODULE, [NConn, Limits]).
|
||||
start_handler() ->
|
||||
case supervisor:start_child(?MODULE, []) of
|
||||
{ok, Pid} ->
|
||||
{ok, Pid};
|
||||
{error, {already_started, Pid}} ->
|
||||
{ok, Pid};
|
||||
StartError ->
|
||||
StartError
|
||||
end.
|
||||
64
apps/sdlan/src/dns_proxy/dns_pending_wheel.erl
Normal file
64
apps/sdlan/src/dns_proxy/dns_pending_wheel.erl
Normal file
@ -0,0 +1,64 @@
|
||||
-module(dns_pending_wheel).
|
||||
|
||||
-export([start/0, insert/2, lookup/1, delete/1]).
|
||||
|
||||
-define(TTL, 5).
|
||||
-define(TICK_MS, 1000).
|
||||
-define(WHEEL_SIZE, (?TTL + 1)).
|
||||
|
||||
%%% =====================================================
|
||||
%%% Public API
|
||||
%%% =====================================================
|
||||
|
||||
start() ->
|
||||
ets:new(dns_pending_data, [ordered_set, public, named_table, {read_concurrency, true}, {write_concurrency, true}]),
|
||||
ets:new(dns_pending_wheel, [bag, public, named_table, {read_concurrency, true}, {write_concurrency, true}]),
|
||||
start_scanner().
|
||||
|
||||
-spec insert(Key :: any(), Val :: any()) -> ok.
|
||||
insert(Key, Val) ->
|
||||
Tick = now_tick(),
|
||||
Slot = Tick rem ?WHEEL_SIZE,
|
||||
ets:insert(dns_pending_data, {Key, {Val, Tick}}),
|
||||
ets:insert(dns_pending_wheel, {Slot, {Key, Tick}}),
|
||||
ok.
|
||||
|
||||
-spec lookup(Key :: any()) -> [term()].
|
||||
lookup(Key) ->
|
||||
ets:lookup(dns_pending_data, Key).
|
||||
|
||||
-spec delete(Key :: any()) -> ok.
|
||||
delete(Key) ->
|
||||
ets:delete(dns_pending_data, Key),
|
||||
ok.
|
||||
|
||||
%%% =====================================================
|
||||
%%% Internal
|
||||
%%% =====================================================
|
||||
|
||||
start_scanner() ->
|
||||
{ok, spawn_link(fun tick_loop/0)}.
|
||||
|
||||
%% 当前插入数据是在Tick, 而清理是从 Tick + 1 开始的,没有问题
|
||||
tick_loop() ->
|
||||
Tick = now_tick(),
|
||||
CleanSlot = (Tick + 1) rem ?WHEEL_SIZE,
|
||||
spawn(fun() -> clean_slot(CleanSlot) end),
|
||||
timer:sleep(?TICK_MS),
|
||||
tick_loop().
|
||||
|
||||
clean_slot(Slot) ->
|
||||
Items = ets:lookup(dns_pending_wheel, Slot),
|
||||
true = ets:delete(dns_pending_wheel, Slot),
|
||||
lists:foreach(fun({_, {Key, InsertTick}}) ->
|
||||
case ets:lookup(dns_pending_data, Key) of
|
||||
[{Key, {_Val, InsertTick}}] ->
|
||||
ets:delete(dns_pending_data, Key);
|
||||
_ ->
|
||||
ok
|
||||
end
|
||||
end, Items).
|
||||
|
||||
-spec now_tick() -> integer().
|
||||
now_tick() ->
|
||||
erlang:system_time(second).
|
||||
@ -3,7 +3,7 @@
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(dns_server_sup).
|
||||
-module(dns_proxy_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-spec start_link() -> supervisor:startlink_ret().
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
@ -24,23 +23,28 @@ start_link() ->
|
||||
%% restart => restart(), % optional
|
||||
%% shutdown => shutdown(), % optional
|
||||
%% type => worker(), % optional
|
||||
-spec init(Args :: term()) ->
|
||||
{ok, {SupFlags :: supervisor:sup_flags(), [ChildSpec :: supervisor:child_spec()]}}
|
||||
| ignore.
|
||||
%% modules => modules()} % optional
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||
Port = 15353,
|
||||
AcceptorNum = 10,
|
||||
|
||||
Specs = lists:map(fun(Id) ->
|
||||
Name = dns_server:get_name(Id),
|
||||
Port = 15353,
|
||||
Specs = [
|
||||
#{
|
||||
id => Name,
|
||||
start => {dns_server, start_link, [Name, Port]},
|
||||
id => dns_handler_sup,
|
||||
start => {dns_handler_sup, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => supervisor,
|
||||
modules => ['dns_handler_sup']
|
||||
},
|
||||
#{
|
||||
id => dns_server,
|
||||
start => {dns_server, start_link, [Port]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['dns_server']
|
||||
}
|
||||
end, lists:seq(1, AcceptorNum)),
|
||||
{ok, {SupFlags, Specs}}.
|
||||
],
|
||||
|
||||
{ok, {SupFlags, Specs}}.
|
||||
141
apps/sdlan/src/dns_proxy/dns_resolver.erl
Normal file
141
apps/sdlan/src/dns_proxy/dns_resolver.erl
Normal file
@ -0,0 +1,141 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 03. 12月 2025 18:26
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(dns_resolver).
|
||||
-author("anlicheng").
|
||||
-include_lib("dns_erlang/include/dns.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/1]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-export([forward/5]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {
|
||||
socket,
|
||||
idx :: integer(),
|
||||
dns_servers = []
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
forward(Pid, ReceiverPid, Ref, Request, QueryMsg) ->
|
||||
gen_server:cast(Pid, {forward, ReceiverPid, Ref, Request, QueryMsg}).
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link(Args :: list()) ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link(Args) when is_list(Args) ->
|
||||
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, DnsServers} = application:get_env(sdlan, public_dns_servers),
|
||||
{ok, Sock} = gen_udp:open(0, [binary, {active, true}]),
|
||||
Idx = erlang:unique_integer([monotonic, positive]),
|
||||
{ok, #state{socket = Sock, idx = Idx, dns_servers = DnsServers}}.
|
||||
|
||||
%% @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({forward, ReceiverPid, Ref, Request, #dns_message{id = TxId, questions = [#dns_query{name = QName, type = QType, class = QClass}|_]}},
|
||||
State = #state{socket = Socket, idx = Idx, dns_servers = DnsServers}) ->
|
||||
|
||||
lists:foreach(fun({DnsIp, DnsPort}) ->
|
||||
ok = gen_udp:send(Socket, DnsIp, DnsPort, Request),
|
||||
Key = {Idx, TxId, DnsIp, DnsPort, QName, QType, QClass},
|
||||
logger:debug("[dns_resolver] key: ~p, send to: ~p, packet: ~p", [Key, {DnsIp, DnsPort}, Request]),
|
||||
dns_pending_wheel:insert(Key, {Ref, ReceiverPid})
|
||||
end, DnsServers),
|
||||
|
||||
{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, Socket, TargetIp, TargetPort, Resp}, State = #state{socket = Socket, idx = Idx}) ->
|
||||
try dns:decode_message(Resp) of
|
||||
#dns_message{id = TxId, questions = [#dns_query{name = QName, type = QType, class = QClass}|_]} ->
|
||||
Key = {Idx, TxId, TargetIp, TargetPort, QName, QType, QClass},
|
||||
Records = dns_pending_wheel:lookup(Key),
|
||||
dns_pending_wheel:delete(Key),
|
||||
resolver_reply(Records, Resp);
|
||||
_ ->
|
||||
ok
|
||||
catch error:_ ->
|
||||
ok
|
||||
end,
|
||||
{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 resolver_reply(list(), Resp :: binary()) -> no_return().
|
||||
resolver_reply(Records, Resp) when is_binary(Resp) ->
|
||||
lists:foreach(fun({_, {Ref, ReceiverPid}}) ->
|
||||
case is_process_alive(ReceiverPid) of
|
||||
true ->
|
||||
ReceiverPid ! {dns_resolver_reply, Ref, Resp};
|
||||
false ->
|
||||
ok
|
||||
end
|
||||
end, Records).
|
||||
24
apps/sdlan/src/dns_proxy/dns_server.erl
Normal file
24
apps/sdlan/src/dns_proxy/dns_server.erl
Normal file
@ -0,0 +1,24 @@
|
||||
-module(dns_server).
|
||||
-export([start_link/1, init/1]).
|
||||
|
||||
start_link(Port) when is_integer(Port) ->
|
||||
{ok, spawn_link(?MODULE, init, [Port])}.
|
||||
|
||||
init(Port) ->
|
||||
dns_cache:init(),
|
||||
{ok, Sock} = gen_udp:open(Port, [binary, {active, true}]),
|
||||
logger:debug("[dns_server] DNS Forwarder started on UDP port ~p~n", [Port]),
|
||||
loop(Sock).
|
||||
|
||||
loop(Sock) ->
|
||||
receive
|
||||
{udp, Sock, Ip, Port, Packet} ->
|
||||
logger:debug("[dns_server] ip: ~p, get a packet: ~p", [{Ip, Port}, Packet]),
|
||||
case dns_handler_sup:start_handler() of
|
||||
{ok, HandlerPid} ->
|
||||
dns_handler:handle_ip_packet(HandlerPid, Sock, Ip, Port, Packet);
|
||||
Error ->
|
||||
logger:debug("[dns_server] start handler get error: ~p", [Error])
|
||||
end,
|
||||
loop(Sock)
|
||||
end.
|
||||
@ -112,7 +112,6 @@ ip_checksum(#ipv4{hl = HL, tos = ToS, len = Len,
|
||||
CheckSum
|
||||
end.
|
||||
|
||||
-spec test() -> term().
|
||||
test() ->
|
||||
%Bin = <<69,0,0,77,48,179,0,0,64,17,28,168,100,123,0,2,100,100,100,100,252,230,0,53,0,57,6,92,152,24,1,0,0,1,0,0,0,0,0,0,2,100,98,7,95,100,110,115,45,115,100,4,95,117,100,112,8,112,117,110,99,104,110,101,116,2,116,115,3,110,101,116,0,0,12,0,1>>,
|
||||
Bin = <<69,0,0,93,0,0,0,0,64,6,77,86,100,100,100,100,100,123,0,2,0,53,196,102,0,73,39,7,215,192,129,128,0,1,0,1,0,0,0,0,2,108,98,7,95,100,110,115,45,115,100,4,95,117,100,112,8,112,117,110,99,104,110,101,116,2,116,115,3,110,101,116,0,0,12,0,1,192,12,0,12,0,1,0,0,1,44,0,4,192,168,1,101>>,
|
||||
@ -129,4 +128,4 @@ test() ->
|
||||
|
||||
logger:debug("ip_sum: ~p, =: ~p, udp: ~p, checkSum: ~p, =: ~p", [IpSum, ip_checksum(IPPacket), UDP, CheckSum, X]),
|
||||
|
||||
dns:decode_message(UDPPayload).
|
||||
dns:decode_message(UDPPayload).
|
||||
24
apps/sdlan/src/http_handler/file_handler.erl
Normal file
24
apps/sdlan/src/http_handler/file_handler.erl
Normal file
@ -0,0 +1,24 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 09. 4月 2024 14:28
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(file_handler).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, State) ->
|
||||
%% 拼接得到文件的真实路径
|
||||
FullPath = "/tmp/files/test.dmg",
|
||||
%% 使用cowboy_req:reply函数返回文件内容
|
||||
{ok, Content} = file:read_file(FullPath),
|
||||
Req1 = cowboy_req:reply(200, #{
|
||||
<<"Content-Type">> => <<"application/octet-stream">>
|
||||
}, Content, Req),
|
||||
|
||||
{ok, Req1, State}.
|
||||
@ -12,7 +12,6 @@
|
||||
%% API
|
||||
-export([init/2]).
|
||||
|
||||
-spec init(Req :: cowboy_req:req(), Opts :: [module()]) -> {ok, cowboy_req:req(), [module()]}.
|
||||
init(Req0, Opts = [Mod|_]) ->
|
||||
Method = binary_to_list(cowboy_req:method(Req0)),
|
||||
Path = binary_to_list(cowboy_req:path(Req0)),
|
||||
@ -20,12 +19,10 @@ init(Req0, Opts = [Mod|_]) ->
|
||||
GetParams = maps:from_list(GetParams0),
|
||||
{ok, PostParams, Req1} = parse_body(Req0),
|
||||
|
||||
Headers = cowboy_req:headers(Req0),
|
||||
logger:debug("[http_protocol] request path: ~p, get_params: ~p, post_params: ~p, headers: ~p",
|
||||
[Path, GetParams, PostParams, Headers]),
|
||||
|
||||
try Mod:handle_request(Method, Path, GetParams, PostParams) of
|
||||
{ok, StatusCode, Resp} ->
|
||||
%logger: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 ->
|
||||
@ -48,10 +45,9 @@ init(Req0, Opts = [Mod|_]) ->
|
||||
{ok, Req2, Opts};
|
||||
_:Error:Stack ->
|
||||
logger:warning("[http_handler] get error: ~p, stack: ~p", [Error, Stack]),
|
||||
ErrorBin = sdlan_util:term_to_binary(Error),
|
||||
Req2 = cowboy_req:reply(500, #{
|
||||
<<"Content-Type">> => <<"text/html;charset=utf-8">>
|
||||
}, <<"Internal Server Error: ", ErrorBin/binary>>, Req1),
|
||||
}, <<"Internal Server Error">>, Req1),
|
||||
{ok, Req2, Opts}
|
||||
end.
|
||||
|
||||
@ -66,7 +62,7 @@ parse_body(Req0) ->
|
||||
{ok, Body, Req1} = read_body(Req0),
|
||||
case Body /= <<>> of
|
||||
true ->
|
||||
{ok, catch json:decode(Body), Req1};
|
||||
{ok, catch jiffy:decode(Body, [return_maps]), Req1};
|
||||
false ->
|
||||
{ok, #{}, Req1}
|
||||
end;
|
||||
@ -87,4 +83,4 @@ read_body(Req, AccData) ->
|
||||
{ok, <<AccData/binary, Data/binary>>, Req1};
|
||||
{more, Data, Req1} ->
|
||||
read_body(Req1, <<AccData/binary, Data/binary>>)
|
||||
end.
|
||||
end.
|
||||
40
apps/sdlan/src/http_handler/network_handler.erl
Normal file
40
apps/sdlan/src/http_handler/network_handler.erl
Normal file
@ -0,0 +1,40 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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/start", _, #{<<"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} ->
|
||||
logger:debug("[network_handler] create network: ~p, get error: ~p", [NetworkId, Reason]),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"error">>)}
|
||||
end;
|
||||
|
||||
handle_request("POST", "/network/stop", _, #{<<"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} ->
|
||||
logger: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">>)}.
|
||||
@ -8,12 +8,13 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(node_handler).
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
-include("sdlan_pb.hrl").
|
||||
-include("sdlan_tables.hrl").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
-spec handle_request(Method :: string(), Path :: string(), GetParams :: map(), PostParams :: map()) ->
|
||||
{ok, StatusCode :: non_neg_integer(), Body :: iodata()}.
|
||||
handle_request("POST", "/node/disable", _, #{<<"network_id">> := NetworkId, <<"client_id">> := ClientId}) when NetworkId > 0 ->
|
||||
case sdlan_network:get_pid(NetworkId) of
|
||||
undefined ->
|
||||
@ -25,4 +26,4 @@ handle_request("POST", "/node/disable", _, #{<<"network_id">> := NetworkId, <<"c
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
82
apps/sdlan/src/http_handler/test_handler.erl
Normal file
82
apps/sdlan/src/http_handler/test_handler.erl
Normal file
@ -0,0 +1,82 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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) ->
|
||||
logger:debug("[test_handler] get post params: ~p", [PostParams]),
|
||||
{ok, 200, sdlan_util:json_data(<<"ok">>)};
|
||||
|
||||
handle_request("POST", "/test/upgrade", _, PostParams) ->
|
||||
logger: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("POST", "/test/login_with_account", _, PostParams) ->
|
||||
logger:debug("[test_handler] get post params: ~p", [PostParams]),
|
||||
Data = #{
|
||||
<<"access_token">> => <<"cookie:1234">>,
|
||||
<<"network_id">> => 1234
|
||||
},
|
||||
{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,
|
||||
<<"domain">> => <<"punchnet8.net">>,
|
||||
<<"disabled_clients">> => []
|
||||
},
|
||||
9 => #{
|
||||
<<"id">> => 9,
|
||||
<<"name">> => <<"test2">>,
|
||||
<<"ipaddr">> => <<"10.211.180.0/24">>,
|
||||
<<"owner_id">> => 1234,
|
||||
<<"domain">> => <<"punchnet9.net">>,
|
||||
<<"disabled_clients">> => []
|
||||
},
|
||||
10 => #{
|
||||
<<"id">> => 10,
|
||||
<<"name">> => <<"test3">>,
|
||||
<<"ipaddr">> => <<"10.211.181.0/24">>,
|
||||
<<"owner_id">> => 1234,
|
||||
<<"domain">> => <<"punchnet10.net">>,
|
||||
<<"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
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
176
apps/sdlan/src/net/arp_packet.erl
Normal file
176
apps/sdlan/src/net/arp_packet.erl
Normal file
@ -0,0 +1,176 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc ARP packet parser / builder (Ethernet + IPv4)
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(arp_packet).
|
||||
|
||||
-export([parse/1, marshal/1, is_broadcast_mac/1, arp_request/3, arp_response/3]).
|
||||
-export([opcode/1, target_ip/1]).
|
||||
|
||||
-define(ETHERNET, 16#0001).
|
||||
-define(IPV4, 16#0800).
|
||||
|
||||
-define(ARP_REQUEST, 1).
|
||||
-define(ARP_RESPONSE, 2).
|
||||
|
||||
-define(BROADCAST_MAC, <<255,255,255,255,255,255>>).
|
||||
|
||||
-record(arp_packet, {
|
||||
hardware_type,
|
||||
protocol_type,
|
||||
hardware_size,
|
||||
protocol_size,
|
||||
opcode,
|
||||
sender_mac,
|
||||
sender_ip,
|
||||
target_mac,
|
||||
target_ip
|
||||
}).
|
||||
|
||||
-type arp_packet() :: #arp_packet{
|
||||
hardware_type :: integer(),
|
||||
protocol_type :: integer(),
|
||||
hardware_size :: integer(),
|
||||
protocol_size :: integer(),
|
||||
opcode :: atom() | integer(),
|
||||
sender_mac :: binary(),
|
||||
sender_ip :: integer(),
|
||||
target_mac :: binary(),
|
||||
target_ip :: integer()
|
||||
}.
|
||||
|
||||
-export_type([arp_packet/0]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Parsing
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec parse(binary()) ->
|
||||
{ok, #arp_packet{}} | {error, invalid_length | invalid_opcode}.
|
||||
parse(Bin) when is_binary(Bin) ->
|
||||
case Bin of
|
||||
<<
|
||||
HType:16/big,
|
||||
PType:16/big,
|
||||
HLen:8,
|
||||
PLen:8,
|
||||
Op:16/big,
|
||||
SMac:6/binary,
|
||||
SIP:32/big,
|
||||
TMac:6/binary,
|
||||
TIP:32/big,
|
||||
_/binary
|
||||
>> ->
|
||||
case decode_opcode(Op) of
|
||||
{ok, Opcode} ->
|
||||
{ok, #arp_packet{
|
||||
hardware_type = HType,
|
||||
protocol_type = PType,
|
||||
hardware_size = HLen,
|
||||
protocol_size = PLen,
|
||||
opcode = Opcode,
|
||||
sender_mac = SMac,
|
||||
sender_ip = SIP,
|
||||
target_mac = TMac,
|
||||
target_ip = TIP
|
||||
}};
|
||||
error ->
|
||||
{error, invalid_opcode}
|
||||
end;
|
||||
_ ->
|
||||
{error, invalid_length}
|
||||
end.
|
||||
|
||||
-spec opcode(Arp_packet :: #arp_packet{}) -> atom().
|
||||
opcode(#arp_packet{opcode = Opcode}) ->
|
||||
Opcode.
|
||||
|
||||
-spec target_ip(Arp_packet :: #arp_packet{}) -> TargetIp :: integer().
|
||||
target_ip(#arp_packet{target_ip = TargetIp}) ->
|
||||
TargetIp.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Marshal
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec marshal(#arp_packet{}) -> binary().
|
||||
marshal(#arp_packet{
|
||||
hardware_type = HType,
|
||||
protocol_type = PType,
|
||||
hardware_size = HLen,
|
||||
protocol_size = PLen,
|
||||
opcode = Opcode,
|
||||
sender_mac = SMac,
|
||||
sender_ip = SIP,
|
||||
target_mac = TMac,
|
||||
target_ip = TIP
|
||||
}) ->
|
||||
Op = encode_opcode(Opcode),
|
||||
<<
|
||||
HType:16/big,
|
||||
PType:16/big,
|
||||
HLen:8,
|
||||
PLen:8,
|
||||
Op:16/big,
|
||||
SMac/binary,
|
||||
SIP:32/big,
|
||||
TMac/binary,
|
||||
TIP:32/big
|
||||
>>.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Opcode helpers
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
decode_opcode(?ARP_REQUEST) -> {ok, request};
|
||||
decode_opcode(?ARP_RESPONSE) -> {ok, response};
|
||||
decode_opcode(_) -> error.
|
||||
|
||||
encode_opcode(request) -> ?ARP_REQUEST;
|
||||
encode_opcode(response) -> ?ARP_RESPONSE.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Utils
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec is_broadcast_mac(binary()) -> boolean().
|
||||
is_broadcast_mac(?BROADCAST_MAC) -> true;
|
||||
is_broadcast_mac(_) -> false.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Builders (对齐 Swift 扩展)
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec arp_request(integer(), binary(), integer()) -> #arp_packet{}.
|
||||
arp_request(SenderIP, SenderMAC, TargetIP) ->
|
||||
#arp_packet{
|
||||
hardware_type = ?ETHERNET,
|
||||
protocol_type = ?IPV4,
|
||||
hardware_size = 6,
|
||||
protocol_size = 4,
|
||||
opcode = request,
|
||||
sender_mac = SenderMAC,
|
||||
sender_ip = SenderIP,
|
||||
target_mac = <<0,0,0,0,0,0>>,
|
||||
target_ip = TargetIP
|
||||
}.
|
||||
|
||||
-spec arp_response(#arp_packet{}, binary(), integer()) -> #arp_packet{}.
|
||||
arp_response(#arp_packet{
|
||||
hardware_type = HType,
|
||||
protocol_type = PType,
|
||||
hardware_size = HLen,
|
||||
protocol_size = PLen,
|
||||
sender_mac = ReqMac,
|
||||
sender_ip = ReqIP
|
||||
}, Mac, IP) ->
|
||||
#arp_packet{
|
||||
hardware_type = HType,
|
||||
protocol_type = PType,
|
||||
hardware_size = HLen,
|
||||
protocol_size = PLen,
|
||||
opcode = response,
|
||||
sender_mac = Mac,
|
||||
sender_ip = IP,
|
||||
target_mac = ReqMac,
|
||||
target_ip = ReqIP
|
||||
}.
|
||||
96
apps/sdlan/src/net/layer_packet.erl
Normal file
96
apps/sdlan/src/net/layer_packet.erl
Normal file
@ -0,0 +1,96 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Ethernet II layer packet parser
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(layer_packet).
|
||||
-export([parse/1, marshal/1, is_broadcast_mac/1, is_multicast_mac/1]).
|
||||
-export([packet_type/1, payload/1]).
|
||||
|
||||
-define(ETH_ARP, 16#0806).
|
||||
-define(ETH_IPV4, 16#0800).
|
||||
-define(ETH_IPV6, 16#86DD).
|
||||
-define(ETH_VLAN, 16#8100).
|
||||
|
||||
-record(layer_packet, {
|
||||
dst_mac,
|
||||
src_mac,
|
||||
type,
|
||||
payload
|
||||
}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec parse(binary()) -> {ok, #layer_packet{}} | {error, invalid_length | invalid_type}.
|
||||
parse(Bin) when is_binary(Bin) ->
|
||||
case Bin of
|
||||
<<Dst:6/binary, Src:6/binary, Type:16, Payload/binary>> ->
|
||||
case decode_type(Type) of
|
||||
{ok, T} ->
|
||||
{ok, #layer_packet{
|
||||
dst_mac = Dst,
|
||||
src_mac = Src,
|
||||
type = T,
|
||||
payload = Payload
|
||||
}};
|
||||
error ->
|
||||
{error, invalid_type}
|
||||
end;
|
||||
_ ->
|
||||
{error, invalid_length}
|
||||
end.
|
||||
|
||||
-spec packet_type(Layer_packet :: #layer_packet{}) -> atom().
|
||||
packet_type(#layer_packet{type = T}) ->
|
||||
T.
|
||||
|
||||
-spec payload(Layer_packet :: #layer_packet{}) -> binary().
|
||||
payload(#layer_packet{payload = Payload}) ->
|
||||
Payload.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec marshal(#layer_packet{}) -> binary().
|
||||
marshal(#layer_packet{dst_mac = Dst, src_mac = Src, type = Type, payload = Payload}) ->
|
||||
TypeBin = encode_type(Type),
|
||||
<<Dst/binary, Src/binary, TypeBin:16, Payload/binary>>.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% EtherType mapping
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
decode_type(?ETH_ARP) ->
|
||||
{ok, arp};
|
||||
decode_type(?ETH_IPV4) ->
|
||||
{ok, ipv4};
|
||||
decode_type(?ETH_IPV6) ->
|
||||
{ok, ipv6};
|
||||
decode_type(?ETH_VLAN) ->
|
||||
{ok, tagged_frame};
|
||||
decode_type(_) ->
|
||||
error.
|
||||
|
||||
encode_type(arp) ->
|
||||
?ETH_ARP;
|
||||
encode_type(ipv4) ->
|
||||
?ETH_IPV4;
|
||||
encode_type(ipv6) ->
|
||||
?ETH_IPV6;
|
||||
encode_type(tagged_frame) ->
|
||||
?ETH_VLAN.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% MAC helpers (对齐 Swift MacAddress)
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec is_broadcast_mac(binary()) -> boolean().
|
||||
is_broadcast_mac(<<255,255,255,255,255,255>>) ->
|
||||
true;
|
||||
is_broadcast_mac(_) ->
|
||||
false.
|
||||
|
||||
-spec is_multicast_mac(binary()) -> boolean().
|
||||
is_multicast_mac(<<1,0,94,_/binary>>) ->
|
||||
true;
|
||||
is_multicast_mac(_) ->
|
||||
false.
|
||||
@ -10,11 +10,14 @@
|
||||
ranch,
|
||||
poolboy,
|
||||
mysql,
|
||||
jiffy,
|
||||
hackney,
|
||||
gpb,
|
||||
throttle,
|
||||
dns_erlang,
|
||||
pkt,
|
||||
quicer,
|
||||
parse_trans,
|
||||
mnesia,
|
||||
erts,
|
||||
public_key,
|
||||
ssl,
|
||||
199
apps/sdlan/src/sdlan_api.erl
Normal file
199
apps/sdlan/src/sdlan_api.erl
Normal file
@ -0,0 +1,199 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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_access_token/1, node_online/3, node_offline/2, flow_report/5, network_forward_report/2]).
|
||||
|
||||
-export([assign_ip_address/3]).
|
||||
|
||||
-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_access_token(Params :: map()) -> {ok, Resp :: map()} | {error, Reason :: any()}.
|
||||
auth_access_token(Params) when is_map(Params) ->
|
||||
case catch do_post("auth_token", Params) 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.
|
||||
|
||||
%% 请求ip地址的分配
|
||||
-spec assign_ip_address(NetworkId :: integer(), ClientId :: binary(), Mac :: binary()) -> {ok, map()} | {error, Reason :: any()}.
|
||||
assign_ip_address(NetworkId, ClientId, Mac) when is_integer(NetworkId), is_binary(ClientId), is_binary(Mac) ->
|
||||
Params = #{
|
||||
<<"network_id">> => NetworkId,
|
||||
<<"client_id">> => ClientId,
|
||||
<<"mac">> => Mac
|
||||
},
|
||||
case catch do_post("assign_ip_address", Params) of
|
||||
{ok, Resp} ->
|
||||
case catch jiffy:decode(Resp, [return_maps]) of
|
||||
Json when is_map(Json) ->
|
||||
case Json of
|
||||
#{<<"result">> := Result} ->
|
||||
{ok, Result};
|
||||
#{<<"error">> := #{<<"message">> := Message}} ->
|
||||
{error, Message};
|
||||
_ ->
|
||||
{error, <<"invalid response">>}
|
||||
end;
|
||||
{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.
|
||||
@ -1,20 +1,36 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 03. 5月 2026 15:06
|
||||
%% @doc sdlan public API
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_http_server).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([start/0]).
|
||||
-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),
|
||||
|
||||
%% 启动注册表
|
||||
sdlan_hostname_regedit:init(),
|
||||
sdlan_domain_regedit:init(),
|
||||
dns_pending_wheel:start(),
|
||||
|
||||
start_http_server(),
|
||||
sdlan_sup:start_link().
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
%% internal functions
|
||||
|
||||
%% 启动http服务
|
||||
-spec start() -> ok.
|
||||
start() ->
|
||||
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),
|
||||
@ -23,8 +39,8 @@ start() ->
|
||||
|
||||
Dispatcher = cowboy_router:compile([
|
||||
{'_', [
|
||||
{"/file/[...]", file_handler, []},
|
||||
{"/api/[...]", http_protocol, [api_handler]},
|
||||
{"/binlog", http_protocol, [binlog_handler]},
|
||||
{"/network/[...]", http_protocol, [network_handler]},
|
||||
{"/node/[...]", http_protocol, [node_handler]},
|
||||
{"/test/[...]", http_protocol, [test_handler]}
|
||||
@ -37,5 +53,7 @@ start() ->
|
||||
{backlog, Backlog},
|
||||
{max_connections, MaxConnections}
|
||||
],
|
||||
|
||||
{ok, Pid} = cowboy:start_clear(http_listener, TransOpts, #{env => #{dispatch => Dispatcher}}),
|
||||
logger:debug("[iot_app] the http server start at: ~p, pid is: ~p", [Port, Pid]).
|
||||
|
||||
logger:debug("[iot_app] the http server start at: ~p, pid is: ~p", [Port, Pid]).
|
||||
@ -8,18 +8,12 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_cipher).
|
||||
-author("anlicheng").
|
||||
-include_lib("public_key/include/public_key.hrl").
|
||||
|
||||
%% API
|
||||
-export([rsa_encrypt/2, rsa_pem_decode/1]).
|
||||
-export([rsa_encrypt/2, rsa_pem_decode/1, rsa_decrypt/2]).
|
||||
-export([aes_encrypt/3, aes_decrypt/3]).
|
||||
-export([test/0, test_chacha20/0]).
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
Key = <<"abcdabcdabcdabcd">>,
|
||||
X = aes_encrypt(Key, Key, <<"hello world">>),
|
||||
logger:debug("x is: ~p, raw: ~p", [X, aes_decrypt(Key, Key, X)]),
|
||||
ok.
|
||||
-export([rsa_generate_key/0]).
|
||||
|
||||
-spec rsa_pem_decode(PubKey :: binary()) -> public_key:rsa_public_key().
|
||||
rsa_pem_decode(PubKey) when is_binary(PubKey) ->
|
||||
@ -31,6 +25,23 @@ rsa_pem_decode(PubKey) when is_binary(PubKey) ->
|
||||
rsa_encrypt(BinData, PublicKey) when is_binary(BinData) ->
|
||||
public_key:encrypt_public(BinData, PublicKey, [{rsa_padding, rsa_pkcs1_padding}]).
|
||||
|
||||
-spec rsa_decrypt(EncData :: binary(), PrivateKey :: public_key:rsa_private_key()) -> binary().
|
||||
rsa_decrypt(EncData, PrivateKey) when is_binary(EncData) ->
|
||||
public_key:decrypt_private(EncData, PrivateKey, [{rsa_padding, rsa_pkcs1_padding}]).
|
||||
|
||||
rsa_generate_key() ->
|
||||
PrivateKey = public_key:generate_key({rsa, 2048, 65537}),
|
||||
PublicKey = #'RSAPublicKey'{
|
||||
modulus = PrivateKey#'RSAPrivateKey'.modulus,
|
||||
publicExponent = PrivateKey#'RSAPrivateKey'.publicExponent
|
||||
},
|
||||
|
||||
PemBin = public_key:pem_encode([
|
||||
public_key:pem_entry_encode('RSAPublicKey', PublicKey)
|
||||
]),
|
||||
PK = binary_to_list(PemBin),
|
||||
{PK, PrivateKey}.
|
||||
|
||||
%% 基于aes的加密算法
|
||||
-spec aes_encrypt(binary(), binary(), binary()) -> binary().
|
||||
aes_encrypt(Key, IVec, PlainText) when is_binary(Key), is_binary(IVec), is_binary(PlainText) ->
|
||||
@ -41,43 +52,3 @@ aes_encrypt(Key, IVec, PlainText) when is_binary(Key), is_binary(IVec), is_binar
|
||||
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}]).
|
||||
|
||||
|
||||
-spec test_chacha20() -> ok.
|
||||
test_chacha20() ->
|
||||
Key = crypto:strong_rand_bytes(32),
|
||||
Nonce = crypto:strong_rand_bytes(12),
|
||||
PlainText = <<"hello world">>,
|
||||
|
||||
Enc = chacha20_encrypt(Key, Nonce, PlainText),
|
||||
|
||||
Ex = chacha20_decrypt(Key, Enc),
|
||||
|
||||
logger:debug("yes ex is: ~p", [Ex]),
|
||||
ok.
|
||||
|
||||
|
||||
chacha20_encrypt(Key, Nonce, Plain) ->
|
||||
AAD = <<>>,
|
||||
{Cipher, Tag} = crypto:crypto_one_time_aead(
|
||||
chacha20_poly1305,
|
||||
Key,
|
||||
Nonce,
|
||||
Plain,
|
||||
AAD,
|
||||
true
|
||||
),
|
||||
<<Nonce/binary, Cipher/binary, Tag/binary>>.
|
||||
|
||||
chacha20_decrypt(Key, <<Nonce:12/binary, Rest/binary>>) ->
|
||||
AAD = <<>>,
|
||||
CipherLen = byte_size(Rest) - 16,
|
||||
<<Cipher:CipherLen/binary, Tag:16/binary>> = Rest,
|
||||
crypto:crypto_one_time_aead(
|
||||
chacha20_poly1305,
|
||||
Key,
|
||||
Nonce,
|
||||
Cipher,
|
||||
AAD,
|
||||
Tag,
|
||||
false
|
||||
).
|
||||
@ -14,7 +14,6 @@
|
||||
|
||||
-define(TABLE, sdlan_domain_regedit).
|
||||
|
||||
-spec init() -> ets:tab().
|
||||
init() ->
|
||||
ets:new(?TABLE, [named_table, ordered_set, public, {read_concurrency, true}, {write_concurrency, true}]).
|
||||
|
||||
@ -32,6 +31,6 @@ maybe_domain(QName) when is_binary(QName) ->
|
||||
false
|
||||
end.
|
||||
|
||||
-spec insert(Domain :: binary()) -> true.
|
||||
-spec insert(Domain :: binary()) -> no_return().
|
||||
insert(Domain) when is_binary(Domain) ->
|
||||
true = ets:insert(?TABLE, {Domain}).
|
||||
true = ets:insert(?TABLE, {Domain}).
|
||||
@ -10,29 +10,30 @@
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([init/0, lookup/1, insert/3]).
|
||||
-export([init/0, lookup/1, insert/2, insert/3]).
|
||||
|
||||
-define(TABLE, sdlan_hostname_regedit).
|
||||
|
||||
-spec init() -> ets:tab().
|
||||
init() ->
|
||||
ets:new(?TABLE, [named_table, set, public, {read_concurrency, true}, {write_concurrency, true}]).
|
||||
|
||||
-spec lookup(FullHostname :: binary()) -> error | {ok, Ip :: inet:ip4_address()}.
|
||||
lookup(FullHostname) when is_binary(FullHostname) ->
|
||||
LowerFullHostname = string:lowercase(FullHostname),
|
||||
case ets:lookup(?TABLE, LowerFullHostname) of
|
||||
case ets:lookup(?TABLE, FullHostname) of
|
||||
[{_, Ip}] ->
|
||||
{ok, Ip};
|
||||
[] ->
|
||||
error
|
||||
end.
|
||||
|
||||
-spec insert(any(), Domain :: binary(), Ip :: integer()) -> true | ok.
|
||||
-spec insert(any(), Domain :: binary(), Ip :: integer()) -> no_return().
|
||||
insert(HostName, Domain, Ip) when is_binary(HostName), is_binary(Domain), is_integer(Ip), HostName /= <<>> ->
|
||||
FullHostname = <<HostName/binary, ".", Domain/binary>>,
|
||||
LowerFullHostname = string:lowercase(FullHostname),
|
||||
<<Ip0, Ip1, Ip2, Ip3>> = <<Ip:32>>,
|
||||
true = ets:insert(?TABLE, {LowerFullHostname, {Ip0, Ip1, Ip2, Ip3}});
|
||||
insert(FullHostname, Ip);
|
||||
insert(_, _, _) ->
|
||||
ok.
|
||||
|
||||
-spec insert(FullHostname :: binary(), Ip :: integer()) -> no_return().
|
||||
insert(FullHostname, Ip) when is_binary(FullHostname), is_integer(Ip) ->
|
||||
<<Ip0, Ip1, Ip2, Ip3>> = <<Ip:32>>,
|
||||
true = ets:insert(?TABLE, {FullHostname, {Ip0, Ip1, Ip2, Ip3}}).
|
||||
55
apps/sdlan/src/sdlan_ipaddr.erl
Normal file
55
apps/sdlan/src/sdlan_ipaddr.erl
Normal file
@ -0,0 +1,55 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 27. 3月 2024 17:43
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_ipaddr).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([ipv4_to_int/1, int_to_ipv4/1, ips/2, format_ip/1]).
|
||||
-export([ipv6_bytes_to_binary/1]).
|
||||
|
||||
format_ip(Ip) when is_integer(Ip) ->
|
||||
int_to_ipv4(Ip);
|
||||
format_ip(Ip) ->
|
||||
Ip.
|
||||
|
||||
-spec ipv4_to_int(Ip :: integer() | binary() | inet:ip4_address()) -> integer().
|
||||
ipv4_to_int(Ip) when is_integer(Ip) ->
|
||||
Ip;
|
||||
ipv4_to_int({Ip0, Ip1, Ip2, Ip3}) ->
|
||||
<<Ip:32>> = <<Ip0, Ip1, Ip2, Ip3>>,
|
||||
Ip;
|
||||
ipv4_to_int(Ip) when is_binary(Ip) ->
|
||||
Parts0 = binary:split(Ip, <<".">>, [global]),
|
||||
Parts = lists:map(fun binary_to_integer/1, Parts0),
|
||||
<<IpInt:32>> = iolist_to_binary(Parts),
|
||||
IpInt.
|
||||
|
||||
-spec int_to_ipv4(Ip :: integer()) -> binary().
|
||||
int_to_ipv4(Ip) when is_integer(Ip) ->
|
||||
<<Ip0, Ip1, Ip2, Ip3>> = <<Ip:32>>,
|
||||
<<(integer_to_binary(Ip0))/binary, $., (integer_to_binary(Ip1))/binary, $., (integer_to_binary(Ip2))/binary, $., (integer_to_binary(Ip3))/binary>>.
|
||||
|
||||
-spec ips(NetAddr :: binary(), MaskLen :: integer()) -> [Ip :: integer()].
|
||||
ips(NetAddr, MaskLen) when is_binary(NetAddr), is_integer(MaskLen) ->
|
||||
Mask = 16#FFFFFFFF bsr MaskLen,
|
||||
Net0 = ipv4_to_int(NetAddr),
|
||||
%% 防止网络地址给得不对,比如: "192.168.1.101",
|
||||
L = 32 - MaskLen,
|
||||
Net = (Net0 bsr L) bsl L,
|
||||
lists:map(fun(V) -> Net + V end, lists:seq(1, Mask - 1)).
|
||||
|
||||
-spec ipv6_bytes_to_binary(Bytes :: binary()) -> Bin :: binary().
|
||||
ipv6_bytes_to_binary(<<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>) ->
|
||||
Segments = [integer_to_list(X, 16) || X <- [A, B, C, D, E, F, G, H]],
|
||||
% 填充每个段以确保是4位
|
||||
Padded = [string:pad(S, 4, leading, $0) || S <- Segments],
|
||||
% 合并成IPv6地址格式,这里没有处理最简化形式的缩写
|
||||
iolist_to_binary(lists:flatten(string:join(Padded, ":")));
|
||||
ipv6_bytes_to_binary(_) ->
|
||||
<<"">>.
|
||||
631
apps/sdlan/src/sdlan_network.erl
Normal file
631
apps/sdlan/src/sdlan_network.erl
Normal file
@ -0,0 +1,631 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 27. 3月 2024 15:13
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_network).
|
||||
-feature(maybe_expr, enable).
|
||||
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
-include("sdlan_pb.hrl").
|
||||
-include("sdlan_tables.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-define(FLOW_REPORT_INTERVAL, 60_000).
|
||||
-define(ENDPOINT_GC_INTERVAL, 30_000).
|
||||
|
||||
%% 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, lookup_pid/1, attach/6, peer_info/4, unregister/3, debug_info/1, get_network_id/1, arp_query/2]).
|
||||
-export([forward/5, stun_request/4, policy_request/4, disable_client/2, dropout_client/2]).
|
||||
-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的使用信息, 记录Node的运行时状态信息
|
||||
-record(endpoint, {
|
||||
client_id :: binary(),
|
||||
mac :: binary(),
|
||||
ip :: integer(),
|
||||
hostname :: binary(),
|
||||
hole :: #hole{},
|
||||
%% 记录ip和ip_v6的映射关系, #{ip_addr :: integer() => {}}
|
||||
v6_info :: undefined | #sdl_v6_info{},
|
||||
session_token :: binary(),
|
||||
last_seen :: integer() %% monotonic_time(second),
|
||||
}).
|
||||
|
||||
-record(state, {
|
||||
network_id :: integer(),
|
||||
name :: binary(),
|
||||
domain :: binary(),
|
||||
ipaddr :: binary(),
|
||||
mask_len :: integer(),
|
||||
%% 设置网络带宽
|
||||
throttle_key :: atom(),
|
||||
%% 转发流量统计
|
||||
forward_bytes = 0,
|
||||
%% 同一个网络下公用的密钥, 采用AES-256加密算法;随机生成
|
||||
aes_key :: binary(),
|
||||
%% 记录已经使用了的ip, #{mac => #endpoint{}}
|
||||
endpoints = #{} :: 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 lookup_pid(Id :: integer()) -> error | {ok, Pid :: pid()}.
|
||||
lookup_pid(Id) when is_integer(Id) ->
|
||||
case whereis(get_name(Id)) of
|
||||
Pid when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
undefined ->
|
||||
error
|
||||
end.
|
||||
|
||||
-spec get_name(Id :: integer()) -> atom().
|
||||
get_name(Id) when is_integer(Id) ->
|
||||
list_to_atom("sdlan_network:" ++ integer_to_list(Id)).
|
||||
|
||||
-spec attach(Pid :: pid(), Peer :: {Ip :: inet:ip4_address(), Port :: integer()}, ClientId :: binary(), Mac :: binary(), Ip :: integer(), HostName :: binary()) ->
|
||||
{ok, AesKey :: binary(), SessionToken :: binary()}.
|
||||
attach(Pid, Peer, ClientId, Mac, Ip, HostName) when is_pid(Pid), is_binary(ClientId), is_binary(Mac), is_integer(Ip) ->
|
||||
gen_server:call(Pid, {attach, Peer, ClientId, Mac, Ip, HostName}).
|
||||
|
||||
-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 arp_query(Pid :: pid(), TargetIp :: integer()) -> {ok, TargetMac :: binary()} | error.
|
||||
arp_query(Pid, TargetIp) when is_pid(Pid), is_integer(TargetIp) ->
|
||||
gen_server:call(Pid, {arp_query, TargetIp}).
|
||||
|
||||
-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(), Sock :: inet:socket(), Peer :: {inet:ip4_address(), integer()}, Query :: #sdl_query_info{}) -> no_return().
|
||||
peer_info(Pid, Sock, ClientPeer, Query) when is_pid(Pid) ->
|
||||
gen_server:cast(Pid, {peer_info, Sock, ClientPeer, Query}).
|
||||
|
||||
-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 stun_request(Pid :: pid(), Sock :: inet:socket(), ClientPeer :: {inet:ip4_address(), integer()}, StunRequest :: #sdl_stun_request{}) -> no_return().
|
||||
stun_request(Pid, Sock, ClientPeer, StunRequest) when is_pid(Pid) ->
|
||||
gen_server:cast(Pid, {stun_request, Sock, ClientPeer, StunRequest}).
|
||||
|
||||
-spec policy_request(Pid :: pid(), Sock :: inet:socket(), ClientPeer :: {inet:ip4_address(), integer()}, PolicyRequest :: #sdl_policy_request{}) -> no_return().
|
||||
policy_request(Pid, Sock, ClientPeer, PolicyRequest) when is_pid(Pid) ->
|
||||
gen_server:cast(Pid, {policy_request, Sock, ClientPeer, PolicyRequest}).
|
||||
|
||||
-spec disable_client(Pid :: pid(), ClientId :: binary()) -> ok.
|
||||
disable_client(Pid, ClientId) when is_pid(Pid), is_binary(ClientId) ->
|
||||
gen_server:call(Pid, {disable_client, ClientId}).
|
||||
|
||||
%% 剔除client_id,channel不关闭; channel会被重新绑定到其他的network里面
|
||||
-spec dropout_client(Pid :: pid(), ClientId :: binary()) -> {ok, ChannelPid :: pid(), HostName :: binary()} | 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).
|
||||
|
||||
%% @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) ->
|
||||
case sdlan_api:get_network(Id) of
|
||||
{ok, #{<<"ipaddr">> := Null}} when Null == <<"null">>; Null == <<"NULL">> ->
|
||||
ignore;
|
||||
{ok, #{<<"id">> := Id, <<"name">> := Name, <<"domain">> := Domain, <<"ipaddr">> := IpAddr0}} ->
|
||||
{IpAddr, MaskLen} = parse_ipaddr(IpAddr0),
|
||||
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),
|
||||
|
||||
%% endpoint定时扫描器
|
||||
erlang:start_timer(?ENDPOINT_GC_INTERVAL, self(), endpoint_gc),
|
||||
|
||||
sdlan_domain_regedit:insert(Domain),
|
||||
|
||||
{ok, #state{network_id = Id, name = Name, domain = Domain, ipaddr = IpAddr, mask_len = MaskLen, aes_key = AesKey, throttle_key = ThrottleKey}};
|
||||
{error, Reason} ->
|
||||
logger: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{}}).
|
||||
%% 给客户端分配ip地址
|
||||
handle_call({attach, Peer, ClientId, Mac, Ip, Hostname}, _From,
|
||||
State = #state{network_id = NetworkId, domain = Domain, endpoints = Endpoints, aes_key = AesKey}) ->
|
||||
%% 分配ip地址的时候,以mac地址为唯一基准
|
||||
logger:debug("[sdlan_network] alloc_ip, network_id: ~p, client_id: ~p, mac: ~p, ip_addr: ~p",
|
||||
[NetworkId, ClientId, sdlan_util:format_mac(Mac), sdlan_ipaddr:int_to_ipv4(Ip)]),
|
||||
%% 添加域名->ip的映射关系
|
||||
sdlan_hostname_regedit:insert(Hostname, Domain, Ip),
|
||||
|
||||
%% mac对应的Endpoint存在,并且对应的ip变了,需要通知端上清理arp
|
||||
maybe
|
||||
{ok, #endpoint{ip = OldIp}} ?= maps:find(Mac, Endpoints),
|
||||
true ?= OldIp =/= Ip,
|
||||
NatChangedEvent = sdlan_pb:encode_msg(#sdl_nat_changed_event{
|
||||
mac = Mac,
|
||||
ip = Ip
|
||||
}),
|
||||
EventPacket = <<?PACKET_EVENT, ?PACKET_EVENT_NAT_CHANGED, NatChangedEvent/binary>>,
|
||||
|
||||
EndpointPeers = endpoint_peers([Mac], Endpoints),
|
||||
sdlan_stun_pool:send_packets(EndpointPeers, EventPacket)
|
||||
end,
|
||||
|
||||
SessionToken = gen_session_token(),
|
||||
Endpoint = #endpoint{client_id = ClientId, mac = Mac, ip = Ip, hostname = Hostname, session_token = SessionToken,
|
||||
hole = #hole{peer = Peer, nat_type = 0}, last_seen = erlang:monotonic_time(second)},
|
||||
|
||||
{reply, {ok, AesKey, SessionToken}, State#state{endpoints = maps:put(Mac, Endpoint, Endpoints)}};
|
||||
|
||||
%% client设置为禁止状态,不允许重连
|
||||
handle_call({disable_client, ClientId}, _From, State = #state{endpoints = Endpoints}) ->
|
||||
case search_endpoint(fun(_, #endpoint{client_id = ClientId0}) -> ClientId =:= ClientId0 end, Endpoints) of
|
||||
{ok, Mac, _} ->
|
||||
{reply, ok, State#state{endpoints = maps:remove(Mac, Endpoints)}};
|
||||
error ->
|
||||
{reply, ok, State}
|
||||
end;
|
||||
|
||||
handle_call(get_network_id, _From, State = #state{network_id = NetworkId}) ->
|
||||
{reply, {ok, NetworkId}, State};
|
||||
|
||||
%% arp查询
|
||||
handle_call({arp_query, TargetIp}, _From, State = #state{endpoints = Endpoints}) ->
|
||||
case search_endpoint(fun(_, #endpoint{ip = Ip0}) -> TargetIp =:= Ip0 end, Endpoints) of
|
||||
{ok, TargetMac, _} ->
|
||||
{reply, {ok, TargetMac}, State};
|
||||
error ->
|
||||
{reply, error, State}
|
||||
end;
|
||||
|
||||
handle_call(debug_info, _From, State = #state{network_id = NetworkId, ipaddr = IpAddr, mask_len = MaskLen, endpoints = Endpoints}) ->
|
||||
Reply = #{
|
||||
<<"network_id">> => NetworkId,
|
||||
<<"ipaddr">> => IpAddr,
|
||||
<<"mask_len">> => MaskLen,
|
||||
<<"used_ips">> => lists:map(fun format_endpoint/1, maps:to_list(Endpoints))
|
||||
},
|
||||
{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{}}).
|
||||
|
||||
%% 网络存在的nat_peer信息
|
||||
%% TODO SrcMac不存在的时候需要重新校验
|
||||
handle_cast({peer_info, Sock, {ClientIp, ClientPort}, #sdl_query_info{pkt_id = PktId, src_mac = SrcMac, dst_mac = DstMac}},
|
||||
State = #state{network_id = NetworkId, endpoints = Endpoints}) ->
|
||||
|
||||
logger:debug("[sdlan_network] network_id: ~p, query_info, src_mac: ~p, dst_mac: ~p",
|
||||
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
|
||||
case maps:find(DstMac, Endpoints) of
|
||||
{ok, #endpoint{hole = #hole{peer = DstNatPeer = {{Ip0, Ip1, Ip2, Ip3}, DstNatPort}, nat_type = DstNatType}, v6_info = DstV6Info}} ->
|
||||
%% 让目标服务器发送sendRegister事件(2024-06-25 新增,提高打洞的成功率)
|
||||
maybe_preload_hole(DstNatPeer, maps:get(SrcMac, Endpoints, undefined)),
|
||||
|
||||
PeerInfo = sdlan_pb:encode_msg(#sdl_peer_info{
|
||||
pkt_id = PktId,
|
||||
network_id = NetworkId,
|
||||
dst_mac = DstMac,
|
||||
v4_info = #sdl_v4_info {
|
||||
port = DstNatPort,
|
||||
v4 = <<Ip0, Ip1, Ip2, Ip3>>,
|
||||
nat_type = DstNatType
|
||||
},
|
||||
v6_info = DstV6Info
|
||||
}),
|
||||
PeerPacket = <<?PACKET_PEER_INFO, PeerInfo/binary>>,
|
||||
gen_udp:send(Sock, ClientIp, ClientPort, PeerPacket);
|
||||
_ ->
|
||||
EmptyPeerInfo = sdlan_pb:encode_msg(#sdl_empty_peer_info{
|
||||
pkt_id = PktId,
|
||||
network_id = NetworkId,
|
||||
dst_mac = DstMac
|
||||
}),
|
||||
EmptyPeerPacket = <<?PACKET_PEER_INFO, EmptyPeerInfo/binary>>,
|
||||
gen_udp:send(Sock, ClientIp, ClientPort, EmptyPeerPacket)
|
||||
end,
|
||||
{noreply, State};
|
||||
|
||||
%% 网络数据转发, mac地址单播
|
||||
handle_cast({forward, Sock, SrcMac, DstMac, Packet}, State = #state{network_id = NetworkId, endpoints = Endpoints, throttle_key = ThrottleKey, forward_bytes = ForwardBytes})
|
||||
when is_map_key(SrcMac, Endpoints), is_map_key(DstMac, Endpoints) ->
|
||||
|
||||
PacketBytes = byte_size(Packet),
|
||||
case maps:find(DstMac, Endpoints) of
|
||||
{ok, #endpoint{hole = #hole{peer = Peer = {Ip, Port}}}} ->
|
||||
case limiting_check(ThrottleKey) of
|
||||
pass ->
|
||||
%% client和stun之间必须有心跳机制保持nat映射可用,并且通过服务转发的udp包肯定可以到达对端的nat
|
||||
logger: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}};
|
||||
denied ->
|
||||
logger: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;
|
||||
{ok, _} ->
|
||||
logger: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 ->
|
||||
logger: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, endpoints = Endpoints, forward_bytes = ForwardBytes})
|
||||
when is_map_key(SrcMac, Endpoints) ->
|
||||
%% 广播地址和组播地址,需要转发到整个网络
|
||||
case sdlan_util:is_broadcast_mac(DstMac) orelse sdlan_util:is_multicast_mac(DstMac) of
|
||||
true ->
|
||||
PacketBytes = byte_size(Packet),
|
||||
%% 消息广播
|
||||
broadcast(fun(#endpoint{hole = #hole{peer = {NatIp, NatPort}}}) ->
|
||||
gen_udp:send(Sock, NatIp, NatPort, Packet)
|
||||
end, [SrcMac], Endpoints),
|
||||
%% client和stun之间必须有心跳机制保持nat映射可用,并且通过服务转发的udp包肯定可以到达对端的nat
|
||||
logger: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 ->
|
||||
logger: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}) ->
|
||||
logger: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, endpoints = Endpoints}) ->
|
||||
logger:debug("[sdlan_network] networkd_id: ~p, unregister Mac: ~p", [NetworkId, sdlan_util:format_mac(Mac)]),
|
||||
{noreply, State#state{endpoints = maps:remove(Mac, Endpoints)}};
|
||||
|
||||
%% 需要判断,client是属于当前网络的
|
||||
%% 被拒绝的endpoint需要通知其重新验证
|
||||
handle_cast({stun_request, Sock, Peer = {ClientIp, ClientPort}, #sdl_stun_request{mac = Mac, client_id = ClientId, nat_type = NatType, v6_info = V6Info, session_token = ST}},
|
||||
State = #state{network_id = NetworkId, endpoints = Endpoints}) ->
|
||||
|
||||
case maps:find(Mac, Endpoints) of
|
||||
%% ClientId =:= ClientId0, SessionToken =:= SessionToken0
|
||||
{ok, Endpoint0 = #endpoint{ip = Ip, client_id = ClientId, hole = OldHole, session_token = ST}} ->
|
||||
NHole = #hole{peer = Peer, nat_type = NatType},
|
||||
maybe
|
||||
%% hole changed -> notify peers
|
||||
true ?= not same_hole(OldHole, NHole),
|
||||
NatChangedEvent = sdlan_pb:encode_msg(#sdl_nat_changed_event{
|
||||
mac = Mac,
|
||||
ip = Ip
|
||||
}),
|
||||
EventPacket = <<?PACKET_EVENT, ?PACKET_EVENT_NAT_CHANGED, NatChangedEvent/binary>>,
|
||||
EndpointPeers = endpoint_peers([Mac], Endpoints),
|
||||
logger:debug("[sdlan_network] hole changed, mac: ~p, ip: ~p, notify peers: ~p", [Mac, Ip, EndpointPeers]),
|
||||
|
||||
sdlan_stun_pool:send_packets(EndpointPeers, EventPacket)
|
||||
end,
|
||||
NEndpoint = Endpoint0#endpoint{hole = NHole, v6_info = V6Info, last_seen = erlang:monotonic_time(second)},
|
||||
logger:debug("[sdlan_network] mac: ~p, ip: ~p, endpoint is: ~p", [Mac, Ip, NEndpoint]),
|
||||
|
||||
{noreply, State#state{endpoints = maps:put(Mac, NEndpoint, Endpoints)}};
|
||||
_ ->
|
||||
%% 客户端需要重新校验
|
||||
RefreshAuthEvent = sdlan_pb:encode_msg(#sdl_refresh_auth_event{
|
||||
network_id = NetworkId
|
||||
}),
|
||||
EventPacket = <<?PACKET_EVENT, ?PACKET_EVENT_REFRESH_AUTH, RefreshAuthEvent/binary>>,
|
||||
gen_udp:send(Sock, ClientIp, ClientPort, EventPacket),
|
||||
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
%% 处理权限逻辑
|
||||
handle_cast({policy_request, Sock, {ClientIp, ClientPort}, #sdl_policy_request{client_id = ClientId, version = Version, mac = Mac, src_identity_id = SrcIdentityId, dst_identity_id = DstIdentityId, session_token = ST}},
|
||||
State = #state{network_id = NetworkId, endpoints = Endpoints}) ->
|
||||
logger:debug("[sdlan_network] policy_request, src_identity: ~p, dst_identity: ~p, version: ~p", [SrcIdentityId, DstIdentityId, Version]),
|
||||
|
||||
%% 分片逻辑,index必须从0开始
|
||||
maybe
|
||||
{ok, #endpoint{client_id = ClientId, session_token = ST}} ?= maps:find(Mac, Endpoints),
|
||||
|
||||
%% todo
|
||||
Bin = <<1, 80:16, 2, 9090:16>>,
|
||||
RulesBin = iolist_to_binary(lists:map(fun(_Id) -> Bin end, lists:seq(1, 1000))),
|
||||
case byte_size(RulesBin) > 1200 of
|
||||
true ->
|
||||
%% 分组
|
||||
Groups = chunk_rules(RulesBin, 1200),
|
||||
TotalNum = length(Groups),
|
||||
Fragments = lists:zip(lists:seq(0, TotalNum - 1), Groups),
|
||||
[begin
|
||||
FragmentPolicyResponse = sdlan_pb:encode_msg(#sdl_policy_response {
|
||||
network_id = NetworkId,
|
||||
src_identity_id = SrcIdentityId,
|
||||
dst_identity_id = DstIdentityId,
|
||||
version = Version,
|
||||
total_num = TotalNum,
|
||||
index = Idx,
|
||||
rules = ChunkRulesBin
|
||||
}),
|
||||
FragmentPolicyResponsePkt = <<?PACKET_POLICY_RESPONSE, FragmentPolicyResponse/binary>>,
|
||||
gen_udp:send(Sock, ClientIp, ClientPort, FragmentPolicyResponsePkt)
|
||||
end || {Idx, ChunkRulesBin} <- Fragments];
|
||||
false ->
|
||||
%% 小于1200字节不分组
|
||||
PolicyResponse = sdlan_pb:encode_msg(#sdl_policy_response {
|
||||
network_id = NetworkId,
|
||||
src_identity_id = SrcIdentityId,
|
||||
dst_identity_id = DstIdentityId,
|
||||
version = Version,
|
||||
total_num = 1,
|
||||
index = 0,
|
||||
rules = RulesBin
|
||||
}),
|
||||
PolicyResponsePkt = <<?PACKET_POLICY_RESPONSE, PolicyResponse/binary>>,
|
||||
logger:debug("[sdlan_network] will send policy response: ~p", [PolicyResponsePkt]),
|
||||
gen_udp:send(Sock, ClientIp, ClientPort, PolicyResponsePkt)
|
||||
end
|
||||
end,
|
||||
{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({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({timeout, _, endpoint_gc}, State = #state{network_id = NetworkId, endpoints = Endpoints}) ->
|
||||
Now = erlang:monotonic_time(second),
|
||||
{AliveEndpoints, ExpiredEndpoints} = maps:fold(fun(Mac, Ep = #endpoint{last_seen = Last}, {Alive, Expired}) ->
|
||||
case Now - Last =< ?ENDPOINT_GC_INTERVAL of
|
||||
true ->
|
||||
{maps:put(Mac, Ep, Alive), Expired};
|
||||
false ->
|
||||
{Alive, maps:put(Mac, Ep, Expired)}
|
||||
end
|
||||
end,
|
||||
{#{}, #{}}, Endpoints),
|
||||
|
||||
%% 通知活着的Endpoints
|
||||
maybe
|
||||
true ?= maps:size(ExpiredEndpoints) > 0,
|
||||
DropMacsEvent = sdlan_pb:encode_msg(#sdl_drop_macs_event {
|
||||
network_id = NetworkId,
|
||||
macs = maps:keys(ExpiredEndpoints)
|
||||
}),
|
||||
EventPacket = <<?PACKET_EVENT, ?PACKET_EVENT_DROP_MACS, DropMacsEvent/binary>>,
|
||||
|
||||
EndpointPeers = endpoint_peers([], AliveEndpoints),
|
||||
sdlan_stun_pool:send_packets(EndpointPeers, EventPacket)
|
||||
end,
|
||||
erlang:start_timer(?ENDPOINT_GC_INTERVAL, self(), endpoint_gc),
|
||||
|
||||
{noreply, State#state{endpoints = AliveEndpoints}}.
|
||||
|
||||
%% @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{}) -> no_return()).
|
||||
terminate(Reason, #state{network_id = NetworkId, endpoints = Endpoints}) ->
|
||||
logger:debug("[sdlan_network] network: ~p, will terminate with reason: ~p", [NetworkId, Reason]),
|
||||
|
||||
NetworkShutdownEvent = sdlan_pb:encode_msg(#sdl_network_shutdown_event {
|
||||
message = <<"Network shutdown">>
|
||||
}),
|
||||
EventPacket = <<?PACKET_EVENT, ?PACKET_EVENT_NETWORK_SHUTDOWN, NetworkShutdownEvent/binary>>,
|
||||
EndpointPeers = endpoint_peers([], Endpoints),
|
||||
sdlan_stun_pool:send_packets(EndpointPeers, EventPacket),
|
||||
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 maybe_preload_hole(DstNatPeer :: {Ip :: inet:ip4_address(), Port :: integer()}, any()) -> no_return().
|
||||
maybe_preload_hole(DstNatPeer, #endpoint{mac = SrcMac, 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
|
||||
}),
|
||||
EventPacket = <<?PACKET_EVENT, ?PACKET_EVENT_SEND_REGISTER, Event/binary>>,
|
||||
sdlan_stun_pool:send_packet(DstNatPeer, EventPacket);
|
||||
maybe_preload_hole(_, _) ->
|
||||
ok.
|
||||
|
||||
-spec limiting_check(ThrottleKey :: any()) -> pass | denied.
|
||||
limiting_check(ThrottleKey) ->
|
||||
case throttle:check(sdlan_network, ThrottleKey) of
|
||||
{ok, _RestCount, _LeftToReset} ->
|
||||
pass;
|
||||
{limit_exceeded, 0, _LeftToReset} ->
|
||||
%% 尝试获取其他网络是否有让渡的资源
|
||||
case sdlan_network_coordinator:checkout() of
|
||||
ok ->
|
||||
pass;
|
||||
error ->
|
||||
denied
|
||||
end
|
||||
end.
|
||||
|
||||
-spec broadcast(Fun :: fun((#endpoint{}) -> no_return()), ExcludeMacs :: [binary()], Endpoints :: map()) -> no_return().
|
||||
broadcast(Fun, ExcludeMacs, Endpoints) when is_function(Fun, 1), is_map(Endpoints), is_list(ExcludeMacs) ->
|
||||
maps:foreach(fun(Mac, Endpoint) ->
|
||||
case lists:member(Mac, ExcludeMacs) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
Fun(Endpoint)
|
||||
end
|
||||
end, Endpoints).
|
||||
|
||||
%% 解析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_endpoint({Mac :: binary(), Host :: #endpoint{}}) -> map().
|
||||
format_endpoint({Mac, #endpoint{client_id = ClientId, ip = Ip, hole = #hole{peer = {NatIp, NatPort}, nat_type = NatType}, v6_info = V6Info}}) ->
|
||||
HoleMap = #{
|
||||
nat_ip => NatIp,
|
||||
nat_port => NatPort,
|
||||
nat_type => NatType
|
||||
},
|
||||
|
||||
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
|
||||
}.
|
||||
|
||||
-spec search_endpoint(F :: fun((term(), term()) -> boolean()), Endpoints :: map()) -> error | {ok, Key :: any(), Val :: any()}.
|
||||
search_endpoint(F, Endpoints) when is_function(F, 2), is_map(Endpoints) ->
|
||||
search_endpoint0(F, maps:iterator(Endpoints)).
|
||||
search_endpoint0(F, Iter) when is_function(F, 2) ->
|
||||
case maps:next(Iter) of
|
||||
{Key, Value, NextIter} ->
|
||||
case F(Key, Value) of
|
||||
true ->
|
||||
{ok, Key, Value};
|
||||
false ->
|
||||
search_endpoint0(F, NextIter)
|
||||
end;
|
||||
'none' ->
|
||||
error
|
||||
end.
|
||||
|
||||
-spec same_hole(Hole :: #hole{}, Hole :: #hole{}) -> boolean().
|
||||
same_hole(#hole{peer = OldPeer, nat_type = OldNatType}, #hole{peer = Peer, nat_type = NatType}) when OldPeer =:= Peer, OldNatType =:= NatType ->
|
||||
true;
|
||||
same_hole(_, _) ->
|
||||
false.
|
||||
|
||||
-spec endpoint_peers(ExcludeMacs :: list(), Endpoints :: map()) -> Peers :: list().
|
||||
endpoint_peers(ExcludeMacs, Endpoints) when is_list(ExcludeMacs), is_map(Endpoints) ->
|
||||
maps:values(maps:filtermap(fun(Mac, #endpoint{hole = #hole{peer = Peer}}) ->
|
||||
case lists:member(Mac, ExcludeMacs) of
|
||||
true ->
|
||||
false;
|
||||
false ->
|
||||
{true, Peer}
|
||||
end
|
||||
end, Endpoints)).
|
||||
|
||||
-spec gen_session_token() -> binary().
|
||||
gen_session_token() ->
|
||||
Bytes = crypto:strong_rand_bytes(32),
|
||||
base64:encode(Bytes).
|
||||
|
||||
%% 对rules进行分组
|
||||
chunk_rules(<<>>, _) ->
|
||||
[];
|
||||
chunk_rules(Bin, Size) when byte_size(Bin) =< Size ->
|
||||
[Bin];
|
||||
chunk_rules(Bin, Size) ->
|
||||
<<Head:Size/binary, Tail/binary>> = Bin,
|
||||
[Head | chunk_rules(Tail, Size)].
|
||||
@ -29,7 +29,7 @@
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec attach(NetworkPid :: pid(), ThrottleKey :: atom()) -> ok.
|
||||
-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}).
|
||||
|
||||
@ -91,7 +91,7 @@ handle_cast({attach, NetworkPid, ThrottleKey}, State = #state{network_map = Netw
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({timeout, _, release_ticker}, State = #state{network_map = NetworkMap}) ->
|
||||
handle_info({timeout, _, release_ticker}, State = #state{network_map = ChannelMap}) ->
|
||||
%% 让渡资源定时器
|
||||
erlang:start_timer(100, self(), release_ticker),
|
||||
AccReleaseCount = lists:foldl(fun(ThrottleKey, Acc) ->
|
||||
@ -103,7 +103,7 @@ handle_info({timeout, _, release_ticker}, State = #state{network_map = NetworkMa
|
||||
{limit_exceeded, 0, _} ->
|
||||
Acc
|
||||
end
|
||||
end, 0, maps:values(NetworkMap)),
|
||||
end, 0, maps:keys(ChannelMap)),
|
||||
% logger: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}) ->
|
||||
117
apps/sdlan/src/sdlan_network_sup.erl
Normal file
117
apps/sdlan/src/sdlan_network_sup.erl
Normal file
@ -0,0 +1,117 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 27. 3月 2024 15:12
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_network_sup).
|
||||
-author("anlicheng").
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([ensured_network_started/1, delete_network/1, get_all_networks/0, start_network/1, reallocate_bind_width/0]).
|
||||
|
||||
%% Supervisor callbacks
|
||||
-export([init/1]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API functions
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Starts the supervisor
|
||||
-spec(start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Supervisor callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3],
|
||||
%% this function is called by the new process to find out about
|
||||
%% restart strategy, maximum restart frequency and child
|
||||
%% specifications.
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||
{ok, NetworkIds} = sdlan_api:get_all_networks(),
|
||||
Specs = lists:map(fun child_spec/1, NetworkIds),
|
||||
|
||||
set_network_bind(length(Specs)),
|
||||
|
||||
{ok, {SupFlags, Specs}}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec ensured_network_started(Id :: integer()) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||
ensured_network_started(Id) when is_integer(Id) ->
|
||||
case sdlan_network:get_pid(Id) of
|
||||
undefined ->
|
||||
case supervisor:start_child(?MODULE, child_spec(Id)) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, {'already_started', Pid}} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end;
|
||||
Pid when is_pid(Pid) ->
|
||||
{ok, Pid}
|
||||
end.
|
||||
|
||||
-spec start_network(Id :: integer()) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||
start_network(Id) when is_integer(Id) ->
|
||||
case supervisor:start_child(?MODULE, child_spec(Id)) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, {'already_started', Pid}} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
-spec get_all_networks() -> [pid()].
|
||||
get_all_networks() ->
|
||||
lists:map(fun({_Id, ChildPid, _Type, _Modules}) -> ChildPid end, supervisor:which_children(?MODULE)).
|
||||
|
||||
%% 重新分配网络带宽
|
||||
-spec reallocate_bind_width() -> no_return().
|
||||
reallocate_bind_width() ->
|
||||
ChildPids = lists:map(fun({_Id, ChildPid, _Type, _Modules}) -> ChildPid end, supervisor:which_children(?MODULE)),
|
||||
set_network_bind(length(ChildPids)).
|
||||
|
||||
-spec delete_network(NetworkId :: integer()) -> ok | {error, Reason :: any()}.
|
||||
delete_network(NetworkId) when is_integer(NetworkId) ->
|
||||
ChildId = sdlan_network:get_name(NetworkId),
|
||||
case supervisor:terminate_child(?MODULE, ChildId) of
|
||||
ok ->
|
||||
supervisor:delete_child(?MODULE, ChildId);
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec child_spec(Id :: integer()) -> map().
|
||||
child_spec(Id) when is_integer(Id) ->
|
||||
Name = sdlan_network:get_name(Id),
|
||||
#{
|
||||
id => Name,
|
||||
start => {sdlan_network, start_link, [Name, Id]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['sdlan_network']
|
||||
}.
|
||||
|
||||
set_network_bind(Count) when is_integer(Count) ->
|
||||
{ok, BindWidth} = application:get_env(sdlan, band_width),
|
||||
NetworkBindWidth = BindWidth div Count,
|
||||
application:set_env(sdlan, network_bind_width, NetworkBindWidth),
|
||||
throttle:setup(sdlan_network, NetworkBindWidth, per_second).
|
||||
4680
apps/sdlan/src/sdlan_pb.erl
Normal file
4680
apps/sdlan/src/sdlan_pb.erl
Normal file
File diff suppressed because it is too large
Load Diff
103
apps/sdlan/src/sdlan_register_worker.erl
Normal file
103
apps/sdlan/src/sdlan_register_worker.erl
Normal file
@ -0,0 +1,103 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 23. 1月 2026 15:20
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_register_worker).
|
||||
-author("anlicheng").
|
||||
-include("sdlan_pb.hrl").
|
||||
-include("sdlan.hrl").
|
||||
|
||||
%% 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).
|
||||
%% hostname被占用
|
||||
-define(NAK_HOSTNAME_USED, 6).
|
||||
|
||||
%% API
|
||||
-export([start_link/4, do_register/4]).
|
||||
|
||||
-spec start_link(Sock :: inet:socket(), Ip :: inet:ip4_address(), Port :: integer(), Packet :: #sdl_register_super{}) -> {ok, pid()}.
|
||||
start_link(Sock, Ip, Port, Packet) ->
|
||||
{ok, erlang:spawn_link(?MODULE, do_register, [Sock, Ip, Port, Packet])}.
|
||||
|
||||
do_register(Sock, ClientIp, ClientPort, #sdl_register_super{
|
||||
pkt_id = PktId, client_id = ClientId, network_id = NetworkId, mac = Mac, ip = Ip, mask_len = MaskLen,
|
||||
hostname = HostName, pub_key = PubKey, access_token = AccessToken}) ->
|
||||
|
||||
true = (Mac =/= <<>> andalso PubKey =/= <<>> andalso ClientId =/= <<>>),
|
||||
%% Mac地址不能是广播地址
|
||||
true = not (sdlan_util:is_multicast_mac(Mac) orelse sdlan_util:is_broadcast_mac(Mac)),
|
||||
|
||||
MacBinStr = sdlan_util:format_mac(Mac),
|
||||
IpAddr = sdlan_ipaddr:int_to_ipv4(Ip),
|
||||
Params = #{
|
||||
<<"network_id">> => NetworkId,
|
||||
<<"client_id">> => ClientId,
|
||||
<<"mac">> => MacBinStr,
|
||||
<<"ip">> => IpAddr,
|
||||
<<"mask_len">> => MaskLen,
|
||||
<<"hostname">> => HostName,
|
||||
<<"access_token">> => AccessToken
|
||||
},
|
||||
%% 参数检查
|
||||
logger:debug("[sdlan_register_worker] client_id: ~p, ip: ~p, mac: ~p, host_name: ~p, access_token: ~p, network_id: ~p",
|
||||
[ClientId, Ip, Mac, HostName, AccessToken, NetworkId]),
|
||||
|
||||
case sdlan_api:auth_access_token(Params) of
|
||||
{ok, #{<<"result">> := <<"ok">>}} ->
|
||||
%% 建立到network的对应关系
|
||||
case sdlan_network:get_pid(NetworkId) of
|
||||
NetworkPid when is_pid(NetworkPid) ->
|
||||
{ok, AesKey, SessionToken} = sdlan_network:attach(NetworkPid, {ClientIp, ClientPort}, ClientId, Mac, Ip, HostName),
|
||||
RsaPubKey = sdlan_cipher:rsa_pem_decode(PubKey),
|
||||
EncodedAesKey = rsa_encode(AesKey, RsaPubKey),
|
||||
|
||||
RegisterSuperAck = sdlan_pb:encode_msg(#sdl_register_super_ack {
|
||||
pkt_id = PktId,
|
||||
aes_key = EncodedAesKey,
|
||||
session_token = SessionToken
|
||||
}),
|
||||
|
||||
%% 发送确认信息
|
||||
Reply = <<?PACKET_REGISTER_SUPER_ACK, RegisterSuperAck/binary>>,
|
||||
gen_udp:send(Sock, ClientIp, ClientPort, Reply),
|
||||
|
||||
%% 设置节点的在线状态
|
||||
Result = sdlan_api:node_online(ClientId, NetworkId, sdlan_ipaddr:int_to_ipv4(Ip)),
|
||||
logger:debug("[sdlan_register_worker] client_id: ~p, set none online result is: ~p", [ClientId, Result]);
|
||||
undefined ->
|
||||
logger:warning("[sdlan_register_worker] client_id: ~p, register get error: network not found", [ClientId]),
|
||||
gen_udp:send(Sock, ClientIp, ClientPort, register_nak_reply(PktId, ?NAK_INTERNAL_FAULT, <<"Internal Error">>))
|
||||
end;
|
||||
{ok, #{<<"error">> := #{<<"code">> := Code, <<"message">> := Message}}} ->
|
||||
logger:warning("[sdlan_register_worker] network_id: ~p, client_id: ~p, register get error: ~ts, error_code: ~p", [NetworkId, ClientId, Message, Code]),
|
||||
gen_udp:send(Sock, ClientIp, ClientPort, register_nak_reply(PktId, Code, Message));
|
||||
{error, Reason} ->
|
||||
logger:warning("[sdlan_register_worker] network_id: ~p, client_id: ~p, register get error: ~p", [NetworkId, ClientId, Reason]),
|
||||
gen_udp:send(Sock, ClientIp, ClientPort, register_nak_reply(PktId, ?NAK_NETWORK_FAULT, <<"Network Error">>))
|
||||
end,
|
||||
exit(normal).
|
||||
|
||||
-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 {
|
||||
pkt_id = PacketId,
|
||||
error_code = ErrorCode,
|
||||
error_message = ErrorMsg
|
||||
}),
|
||||
<<?PACKET_REGISTER_SUPER_NAK, RegisterNakReply/binary>>.
|
||||
|
||||
-spec rsa_encode(PlainText :: binary(), RsaPubKey :: public_key:rsa_public_key()) -> binary().
|
||||
rsa_encode(PlainText, RsaPubKey) when is_binary(PlainText) ->
|
||||
iolist_to_binary(sdlan_cipher:rsa_encrypt(PlainText, RsaPubKey)).
|
||||
66
apps/sdlan/src/sdlan_register_worker_sup.erl
Normal file
66
apps/sdlan/src/sdlan_register_worker_sup.erl
Normal file
@ -0,0 +1,66 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 23. 1月 2026 15:19
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_register_worker_sup).
|
||||
-author("anlicheng").
|
||||
-include("sdlan_pb.hrl").
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([start_worker/4]).
|
||||
|
||||
%% 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.
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, {SupFlags :: supervisor:sup_flags(), [ChildSpec :: supervisor:child_spec()]}}
|
||||
| ignore | {error, Reason :: term()}).
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => simple_one_for_one, intensity => 1000, period => 3600},
|
||||
|
||||
ChildSpec = #{
|
||||
id => sdlan_register_worker,
|
||||
start => {sdlan_register_worker, start_link, []},
|
||||
restart => temporary,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => [sdlan_register_worker]
|
||||
},
|
||||
|
||||
{ok, {SupFlags, [ChildSpec]}}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_worker(Sock :: inet:socket(), Ip :: inet:ip4_address(), Port :: integer(), Packet :: #sdl_register_super{}) -> supervisor:startchild_ret().
|
||||
start_worker(Sock, Ip, Port, Packet) ->
|
||||
%% Start a temporary worker under the supervisor
|
||||
supervisor:start_child(?SERVER, [Sock, Ip, Port, Packet]).
|
||||
235
apps/sdlan/src/sdlan_stun.erl
Normal file
235
apps/sdlan/src/sdlan_stun.erl
Normal file
@ -0,0 +1,235 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 09. 4月 2024 17:37
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_stun).
|
||||
-feature(maybe_expr, enable).
|
||||
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
-include("sdlan_pb.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/1]).
|
||||
-export([send_packets/3]).
|
||||
|
||||
%% 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 :: inet:socket()
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec send_packets(Pid :: pid(), Peers :: [{Ip :: inet:ip4_address(), Port :: integer()}], Packet :: binary()) -> no_return().
|
||||
send_packets(Pid, Peers, Packet) when is_pid(Pid), is_list(Peers), is_binary(Packet) ->
|
||||
gen_server:cast(Pid, {send_packets, Peers, Packet}).
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link(Port :: integer()) ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link(Port) ->
|
||||
gen_server:start_link(?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]) ->
|
||||
Opts = [
|
||||
binary,
|
||||
{reuseaddr, true},
|
||||
{reuseport, true},
|
||||
{active, true},
|
||||
{recbuf, 5 * 1024 * 1024},
|
||||
{sndbuf, 5 * 1024 * 1024}
|
||||
],
|
||||
|
||||
{ok, Socket} = gen_udp:open(Port, Opts),
|
||||
%% 需要提高进程的调度优先级
|
||||
erlang:process_flag(priority, high),
|
||||
inet_udp:controlling_process(Socket, self()),
|
||||
{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({send_packets, Peers, Packet}, State = #state{socket = Sock}) ->
|
||||
lists:foreach(fun({Ip, Port}) -> gen_udp:send(Sock, Ip, Port, Packet) end, Peers),
|
||||
{noreply, 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, PeerIp, PeerPort, Packet}, State = #state{socket = Sock}) ->
|
||||
handle_packet(Sock, PeerIp, PeerPort, Packet),
|
||||
{noreply, State};
|
||||
handle_info({udp_error , Sock, Reason}, State = #state{socket = Sock}) ->
|
||||
{stop, Reason, State};
|
||||
handle_info(Info, State) ->
|
||||
logger:error("[sdlan_stun] get a unknown message?: ~p, channel will closed, state: ~p", [Info, 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
|
||||
%%%===================================================================
|
||||
|
||||
-spec handle_packet(Sock :: inet:socket(), Ip :: inet:ip4_address(), Port :: integer(), Packet :: binary()) -> no_return().
|
||||
%% 带上token或者网络id来注册
|
||||
handle_packet(Sock, Ip, Port, <<?PACKET_REGISTER_SUPER, Body/binary>>) ->
|
||||
maybe
|
||||
Register = catch sdlan_pb:decode_msg(Body, sdl_register_super),
|
||||
#sdl_register_super{} ?= Register,
|
||||
sdlan_register_worker_sup:start_worker(Sock, Ip, Port, Register)
|
||||
end;
|
||||
|
||||
%% 查询信息
|
||||
handle_packet(Sock, ClientIp, ClientPort, <<?PACKET_QUERY_INFO, Body/binary>>) ->
|
||||
maybe
|
||||
Query = catch sdlan_pb:decode_msg(Body, sdl_query_info),
|
||||
#sdl_query_info{network_id = NetworkId} ?= Query,
|
||||
|
||||
{ok, NetworkPid} ?= sdlan_network:lookup_pid(NetworkId),
|
||||
sdlan_network:peer_info(NetworkPid, Sock, {ClientIp, ClientPort}, Query)
|
||||
end;
|
||||
|
||||
%% 处理心跳逻辑
|
||||
handle_packet(Sock, ClientIp, ClientPort, <<?PACKET_STUN_REQUEST:8, Body/binary>>) ->
|
||||
maybe
|
||||
StunRequest = catch sdlan_pb:decode_msg(Body, sdl_stun_request),
|
||||
#sdl_stun_request{network_id = NetworkId} ?= StunRequest,
|
||||
{ok, NetworkPid} ?= sdlan_network:lookup_pid(NetworkId),
|
||||
%% 告知网络当前的ip对应的nat的映射关系
|
||||
sdlan_network:stun_request(NetworkPid, Sock, {ClientIp, ClientPort}, StunRequest)
|
||||
end;
|
||||
|
||||
%% 处理网络的权限请求
|
||||
handle_packet(Sock, ClientIp, ClientPort, <<?PACKET_POLICY_REQUEST:8, Body/binary>>) ->
|
||||
maybe
|
||||
PolicyRequest = catch sdlan_pb:decode_msg(Body, sdl_policy_request),
|
||||
#sdl_policy_request{network_id = NetworkId} ?= PolicyRequest,
|
||||
logger:debug("[sdlan_stun] get policy request, network_id: ~p", [NetworkId]),
|
||||
{ok, NetworkPid} ?= sdlan_network:lookup_pid(NetworkId),
|
||||
sdlan_network:policy_request(NetworkPid, Sock, {ClientIp, ClientPort}, PolicyRequest)
|
||||
end;
|
||||
|
||||
%% 网络nat类型的探测机制, 需要借助其他服务一起才能实现
|
||||
%% 辅助节点没有assist的配置,不支持attr = 2的探测
|
||||
handle_packet(Sock, Ip, Port, <<?PACKET_STUN_PROBE:8, Body/binary>>) ->
|
||||
maybe
|
||||
#sdl_stun_probe{cookie = Cookie, step = Step, attr = Attr} ?= sdlan_pb:decode_msg(Body, sdl_stun_probe),
|
||||
logger:debug("[sdlan_stun] get stun_probe request, cookie: ~p", [Cookie]),
|
||||
|
||||
ProbeReplyPacket = sdlan_pb:encode_msg(#sdl_stun_probe_reply {
|
||||
cookie = Cookie,
|
||||
ip = int_ip(Ip),
|
||||
port = Port,
|
||||
step = Step
|
||||
}),
|
||||
|
||||
case Attr of
|
||||
?STUN_ATTR_CHANGE_NONE ->
|
||||
%% 什么不都改变
|
||||
gen_udp:send(Sock, Ip, Port, <<?PACKET_STUN_PROBE_REPLY, ProbeReplyPacket/binary>>);
|
||||
?STUN_ATTR_CHANGE_PEER ->
|
||||
%% 切换端口和ip
|
||||
sdlan_stun_peer_assist:stun_relay(Ip, Port, ProbeReplyPacket);
|
||||
?STUN_ATTR_CHANGE_PORT ->
|
||||
%% 切换端口返回
|
||||
sdlan_stun_port_assist:stun_relay(Ip, Port, ProbeReplyPacket)
|
||||
end
|
||||
end;
|
||||
|
||||
%% 转发消息, 跨服务器的stun_reply的转发通过socket来转发
|
||||
handle_packet(Sock, _, _, <<?PACKET_STUN_PROBE_RELAY:8, Ip0, Ip1, Ip2, Ip3, Port:16, Reply/binary>>) ->
|
||||
logger:debug("[sdlan_stun] get stun_probe_replay request, reply: ~p", [Reply]),
|
||||
gen_udp:send(Sock, {Ip0, Ip1, Ip2, Ip3}, Port, Reply);
|
||||
|
||||
handle_packet(Sock, _Ip, _Port, <<?PACKET_STUN_DATA, Body/binary>>) ->
|
||||
maybe
|
||||
Data = catch sdlan_pb:decode_msg(Body, sdl_data),
|
||||
#sdl_data{network_id = NetworkId, src_mac = SrcMac, dst_mac = DstMac, ttl = TTL} ?= Data,
|
||||
{ok, NetworkPid} ?= sdlan_network:lookup_pid(NetworkId),
|
||||
logger:debug("[sdlan_stun] stun data, src_mac: ~p, dst_mac: ~p", [sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
%% 重新打包数据ttl需要减1
|
||||
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>>)
|
||||
end;
|
||||
|
||||
%% 执行arp查询
|
||||
handle_packet(Sock, ClientIp, ClientPort, <<?PACKET_ARP_REQUEST, Body/binary>>) ->
|
||||
maybe
|
||||
#sdl_arp_request{network_id = NetworkId, target_ip = TargetIp} ?= sdlan_pb:decode_msg(Body, sdl_arp_request),
|
||||
logger:debug("[sdlan_stun] stun sdl_arp_request, network_id: ~p, target_ip: ~p", [NetworkId, TargetIp]),
|
||||
{ok, NetworkPid} ?= sdlan_network:lookup_pid(NetworkId),
|
||||
{ok, TargetMac} ?= sdlan_network:arp_query(NetworkPid, TargetIp),
|
||||
ArpResponse = sdlan_pb:encode_msg(#sdl_arp_response{
|
||||
network_id = NetworkId,
|
||||
target_ip = TargetIp,
|
||||
target_mac = TargetMac
|
||||
}),
|
||||
gen_udp:send(Sock, ClientIp, ClientPort, ArpResponse)
|
||||
end.
|
||||
|
||||
-spec int_ip(tuple()) -> integer().
|
||||
int_ip({Ip0, Ip1, Ip2, Ip3}) ->
|
||||
<<Ip:32>> = <<Ip0, Ip1, Ip2, Ip3>>,
|
||||
Ip.
|
||||
@ -8,7 +8,6 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_stun_peer_assist).
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
@ -31,7 +30,7 @@
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec stun_relay(Ip :: inet:ip_address(), Port :: integer(), Reply :: binary()) -> ok.
|
||||
-spec stun_relay(Ip :: inet:ip_address(), Port :: integer(), Reply :: binary()) -> no_return().
|
||||
stun_relay(Ip, Port, Reply) when is_integer(Port), is_binary(Reply) ->
|
||||
gen_server:cast(?SERVER, {stun_relay, Ip, Port, Reply}).
|
||||
|
||||
@ -59,6 +58,7 @@ init([]) ->
|
||||
{ok, Socket} = gen_udp:open(0, Opts),
|
||||
inet_udp:controlling_process(Socket, self()),
|
||||
logger:debug("[sdlan_stun_peer_assist] started"),
|
||||
|
||||
{ok, #state{socket = Socket, assist_ip = AssistIp, assist_port = AssistPort}}.
|
||||
|
||||
%% @private
|
||||
@ -81,7 +81,7 @@ handle_call(_Request, _From, State = #state{}) ->
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast({stun_relay, _Ip = {Ip0, Ip1, Ip2, Ip3}, Port, Reply}, State = #state{socket = Sock, assist_ip = AssistIp, assist_port = AssistPort}) ->
|
||||
Packet = <<?PACKET_STUN_PROBE_RELAY, Ip0, Ip1, Ip2, Ip3, Port:16, Reply/binary>>,
|
||||
Packet = <<Ip0, Ip1, Ip2, Ip3, Port:16, Reply/binary>>,
|
||||
ok = gen_udp:send(Sock, AssistIp, AssistPort, Packet),
|
||||
{noreply, State}.
|
||||
|
||||
@ -6,7 +6,9 @@
|
||||
%%% @end
|
||||
%%% Created : 09. 4月 2024 17:37
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(dns_server).
|
||||
-module(sdlan_stun_pool).
|
||||
-feature(maybe_expr, enable).
|
||||
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
-include("sdlan_pb.hrl").
|
||||
@ -14,8 +16,7 @@
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/2]).
|
||||
-export([get_name/1]).
|
||||
-export([start_link/0, send_packets/2, send_packet/2]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
@ -23,22 +24,28 @@
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {
|
||||
socket
|
||||
workers :: tuple(),
|
||||
idx = 1,
|
||||
num = 0
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec get_name(Id :: integer()) -> atom().
|
||||
get_name(Id) when is_integer(Id) ->
|
||||
list_to_atom("dns_server:" ++ integer_to_list(Id)).
|
||||
-spec send_packet(Peer :: {Ip :: inet:ip4_address(), Port :: integer()}, Packet :: binary()) -> no_return().
|
||||
send_packet(Peer, Packet) ->
|
||||
gen_server:cast(?SERVER, {send_packets, [Peer], Packet}).
|
||||
|
||||
-spec send_packets(Peers :: [{Ip :: inet:ip4_address(), Port :: integer()}], Packet :: binary()) -> no_return().
|
||||
send_packets(Peers, Packet) when is_list(Peers), is_binary(Packet) ->
|
||||
gen_server:cast(?SERVER, {send_packets, Peers, Packet}).
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link(Name :: atom(), Port :: integer()) ->
|
||||
-spec(start_link() ->
|
||||
{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], []).
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
@ -49,22 +56,16 @@ start_link(Name, Port) when is_atom(Name), is_integer(Port) ->
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([Port]) ->
|
||||
%% 需要提高进程的调度优先级
|
||||
erlang:process_flag(priority, high),
|
||||
Opts = [
|
||||
binary,
|
||||
{reuseaddr, true},
|
||||
{reuseport, true},
|
||||
{active, true},
|
||||
{recbuf, 5 * 1024 * 1024},
|
||||
{sndbuf, 5 * 1024 * 1024}
|
||||
],
|
||||
{ok, Socket} = gen_udp:open(Port, Opts),
|
||||
inet_udp:controlling_process(Socket, self()),
|
||||
init([]) ->
|
||||
{ok, StunServerProps} = application:get_env(sdlan, stun_servers),
|
||||
Port = proplists:get_value(port, StunServerProps),
|
||||
AcceptorNums = proplists:get_value(acceptor_nums, StunServerProps),
|
||||
|
||||
logger:debug("[sdlan_stun] start at port: ~p", [Port]),
|
||||
{ok, #state{socket = Socket}}.
|
||||
Workers = lists:map(fun(_Id) ->
|
||||
{ok, Pid} = sdlan_stun:start_link(Port),
|
||||
Pid
|
||||
end, lists:seq(1, AcceptorNums)),
|
||||
{ok, #state{workers = list_to_tuple(Workers), idx = 1, num = length(Workers)}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
@ -86,6 +87,11 @@ handle_call(_Request, _From, State = #state{}) ->
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
%% 当前node下的转发,基于进程间的通讯
|
||||
handle_cast({send_packets, Peers, Packet}, State = #state{workers = Workers, idx = Idx, num = Num}) ->
|
||||
WorkerPid = element(Idx, Workers),
|
||||
sdlan_stun:send_packets(WorkerPid, Peers, Packet),
|
||||
NewIdx = (Idx rem Num) + 1,
|
||||
{noreply, State#state{idx = NewIdx}};
|
||||
handle_cast(_Request, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
@ -95,16 +101,8 @@ handle_cast(_Request, State) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({udp, Sock, Ip, Port, IpPacket}, State = #state{socket = Sock}) ->
|
||||
case dns_resolver:resolve(IpPacket) of
|
||||
{ok, RespIpPacket} ->
|
||||
gen_udp:send(Sock, Ip, Port, RespIpPacket);
|
||||
{error, Reason} ->
|
||||
logger:debug("[dns_server] resolve dns query error: ~p", [Reason])
|
||||
end,
|
||||
{noreply, State};
|
||||
handle_info(Info, State) ->
|
||||
logger:error("[sdlan_stun] get a unknown message: ~p, channel will closed", [Info]),
|
||||
logger:error("[sdlan_stun] get a unknown message:xx ~p, channel will closed", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
@ -8,7 +8,6 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_stun_port_assist).
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
@ -29,7 +28,7 @@
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec stun_relay(Ip :: inet:ip_address(), Port :: integer(), Reply :: binary()) -> ok.
|
||||
-spec stun_relay(Ip :: inet:ip_address(), Port :: integer(), Reply :: binary()) -> no_return().
|
||||
stun_relay(Ip, Port, Reply) when is_integer(Port), is_binary(Reply) ->
|
||||
gen_server:cast(?SERVER, {stun_relay, Ip, Port, Reply}).
|
||||
|
||||
@ -88,10 +87,6 @@ handle_cast({stun_relay, Ip, Port, Reply}, State = #state{socket = Sock}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({udp, Sock, _Ip, _Port, <<?PACKET_STUN_PROBE_RELAY, Ip0, Ip1, Ip2, Ip3, ClientPort:16, Reply/binary>>},
|
||||
State = #state{socket = Sock}) ->
|
||||
ok = gen_udp:send(Sock, {Ip0, Ip1, Ip2, Ip3}, ClientPort, Reply),
|
||||
{noreply, State};
|
||||
handle_info(_Info, State = #state{}) ->
|
||||
{noreply, State}.
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-spec start_link() -> supervisor:startlink_ret().
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
@ -24,12 +23,19 @@ start_link() ->
|
||||
%% restart => restart(), % optional
|
||||
%% shutdown => shutdown(), % optional
|
||||
%% type => worker(), % optional
|
||||
-spec init(Args :: term()) ->
|
||||
{ok, {SupFlags :: supervisor:sup_flags(), [ChildSpec :: supervisor:child_spec()]}}
|
||||
| ignore.
|
||||
%% modules => modules()} % optional
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||
Specs = [
|
||||
#{
|
||||
id => sdlan_stun_pool,
|
||||
start => {sdlan_stun_pool, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['sdlan_stun_pool']
|
||||
},
|
||||
|
||||
#{
|
||||
id => sdlan_stun_port_assist,
|
||||
start => {sdlan_stun_port_assist, start_link, []},
|
||||
@ -48,21 +54,4 @@ init([]) ->
|
||||
}
|
||||
],
|
||||
|
||||
{ok, {SupFlags, Specs ++ stun_acceptors()}}.
|
||||
|
||||
stun_acceptors() ->
|
||||
{ok, StunServers} = application:get_env(sdlan, stun_servers),
|
||||
Port = proplists:get_value(port, StunServers),
|
||||
AcceptorNums = proplists:get_value(acceptor_nums, StunServers),
|
||||
|
||||
lists:map(fun(Id) ->
|
||||
Name = sdlan_stun:get_name(Id),
|
||||
#{
|
||||
id => Name,
|
||||
start => {sdlan_stun, start_link, [Name, Port]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['sdlan_stun']
|
||||
}
|
||||
end, lists:seq(1, AcceptorNums)).
|
||||
{ok, {SupFlags, Specs}}.
|
||||
@ -13,7 +13,6 @@
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-spec start_link() -> supervisor:startlink_ret().
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
@ -25,29 +24,18 @@ start_link() ->
|
||||
%% restart => restart(), % optional
|
||||
%% shutdown => shutdown(), % optional
|
||||
%% type => worker(), % optional
|
||||
-spec init(Args :: term()) ->
|
||||
{ok, {SupFlags :: supervisor:sup_flags(), [ChildSpec :: supervisor:child_spec()]}}
|
||||
| ignore.
|
||||
%% modules => modules()} % optional
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||
|
||||
Specs = [
|
||||
%% 暂时注释掉dns的逻辑
|
||||
#{
|
||||
id => dns_server_sup,
|
||||
start => {dns_server_sup, start_link, []},
|
||||
id => dns_proxy_sup,
|
||||
start => {dns_proxy_sup, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => supervisor,
|
||||
modules => ['dns_server_sup']
|
||||
},
|
||||
#{
|
||||
id => ipv6_assist_server_sup,
|
||||
start => {ipv6_assist_server_sup, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => supervisor,
|
||||
modules => ['ipv6_assist_server_sup']
|
||||
modules => ['dns_proxy_sup']
|
||||
},
|
||||
#{
|
||||
id => sdlan_network_coordinator,
|
||||
@ -57,6 +45,7 @@ init([]) ->
|
||||
type => worker,
|
||||
modules => ['sdlan_network_coordinator']
|
||||
},
|
||||
|
||||
#{
|
||||
id => sdlan_network_sup,
|
||||
start => {sdlan_network_sup, start_link, []},
|
||||
@ -66,6 +55,15 @@ init([]) ->
|
||||
modules => ['sdlan_network_sup']
|
||||
},
|
||||
|
||||
#{
|
||||
id => sdlan_register_worker_sup,
|
||||
start => {sdlan_register_worker_sup, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => supervisor,
|
||||
modules => ['sdlan_register_worker_sup']
|
||||
},
|
||||
|
||||
#{
|
||||
id => sdlan_stun_sup,
|
||||
start => {sdlan_stun_sup, start_link, []},
|
||||
@ -73,42 +71,6 @@ init([]) ->
|
||||
shutdown => 2000,
|
||||
type => supervisor,
|
||||
modules => ['sdlan_stun_sup']
|
||||
},
|
||||
|
||||
#{
|
||||
id => sdlan_quic_channel_sup,
|
||||
start => {sdlan_quic_channel_sup, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => supervisor,
|
||||
modules => ['sdlan_quic_channel_sup']
|
||||
},
|
||||
|
||||
#{
|
||||
id => sdlan_quic_server,
|
||||
start => {sdlan_quic_server, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['sdlan_quic_server']
|
||||
},
|
||||
|
||||
#{
|
||||
id => sdlan_sync_mysql,
|
||||
start => {sdlan_sync_mysql, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['sdlan_sync_mysql']
|
||||
},
|
||||
|
||||
#{
|
||||
id => maxwell_redis_server,
|
||||
start => {maxwell_redis_server, start_link, [16379]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['maxwell_redis_server']
|
||||
}
|
||||
],
|
||||
|
||||
@ -120,4 +82,4 @@ 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).
|
||||
end, Pools).
|
||||
74
apps/sdlan/src/sdlan_util.erl
Normal file
74
apps/sdlan/src/sdlan_util.erl
Normal file
@ -0,0 +1,74 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 11. 3月 2024 11:10
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_util).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([rand_byte/1, md5/1, format_mac/1, assert_call/2]).
|
||||
-export([json_data/1, json_error/2]).
|
||||
-export([is_broadcast_mac/1, is_multicast_mac/1]).
|
||||
|
||||
-spec format_mac(Mac :: binary()) -> binary().
|
||||
format_mac(Mac) when is_binary(Mac) ->
|
||||
Hex = fun
|
||||
(N) when N < 10 ->
|
||||
$0 + N;
|
||||
(N) ->
|
||||
$a + (N - 10)
|
||||
end,
|
||||
Y = [[Hex(X0), Hex(X1)] || <<X0:4, X1:4>> <= Mac],
|
||||
list_to_binary(lists:flatten(lists:join(":", Y))).
|
||||
|
||||
%% 生成随机字节
|
||||
rand_byte(Num) when is_integer(Num), Num > 0 ->
|
||||
rand_byte0(Num, <<>>).
|
||||
rand_byte0(0, Acc) ->
|
||||
Acc;
|
||||
rand_byte0(Num, Acc) ->
|
||||
Byte = ceil(rand:uniform() * 255),
|
||||
rand_byte0(Num - 1, <<Acc/binary, Byte>>).
|
||||
|
||||
%% md5哈希算法
|
||||
-spec md5(string() | binary()) -> string().
|
||||
md5(Str) when is_binary(Str) ->
|
||||
md5(binary_to_list(Str));
|
||||
md5(Str) when is_list(Str) ->
|
||||
Hash = binary_to_list(erlang:md5(Str)),
|
||||
lists:flatten([hex(I) || I <- Hash]).
|
||||
|
||||
hex(I) when I > 16#f ->
|
||||
[hex0((I band 16#f0) bsr 4), hex0(I band 16#0f)];
|
||||
hex(I) ->
|
||||
[$0, hex0(I)].
|
||||
hex0(10) -> $a;
|
||||
hex0(11) -> $b;
|
||||
hex0(12) -> $c;
|
||||
hex0(13) -> $d;
|
||||
hex0(14) -> $e;
|
||||
hex0(15) -> $f;
|
||||
hex0(I) -> $0 + I.
|
||||
|
||||
json_data(Data) ->
|
||||
jiffy:encode(#{<<"result">> => Data}, [force_utf8]).
|
||||
|
||||
json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) ->
|
||||
jiffy:encode(#{<<"error">> => #{<<"code">> => ErrCode, <<"message">> => ErrMessage}}, [force_utf8]).
|
||||
|
||||
assert_call(true, F) ->
|
||||
F();
|
||||
assert_call(false, _) ->
|
||||
ok.
|
||||
|
||||
-spec is_broadcast_mac(Mac :: binary()) -> boolean().
|
||||
is_broadcast_mac(Mac) when is_binary(Mac) ->
|
||||
Mac =:= <<16#FF,16#FF,16#FF,16#FF,16#FF,16#FF>>.
|
||||
|
||||
-spec is_multicast_mac(Mac :: binary()) -> boolean().
|
||||
is_multicast_mac(Mac) when is_binary(Mac) ->
|
||||
binary:part(Mac, 0, 3) =:= <<16#01,16#00,16#5E>>.
|
||||
215
apps/sdlan/src/test/stun_client.erl
Normal file
215
apps/sdlan/src/test/stun_client.erl
Normal file
@ -0,0 +1,215 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 08. 4月 2024 10:37
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(stun_client).
|
||||
-author("anlicheng").
|
||||
-include("sdlan_pb.hrl").
|
||||
-include("sdlan.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([register/1, debug_info/1]).
|
||||
-export([test/0]).
|
||||
|
||||
%% 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).
|
||||
|
||||
-define(SUPER_HOST, "127.0.0.1").
|
||||
-define(SUPER_PORT, 1265).
|
||||
|
||||
-record(state, {
|
||||
tun_socket,
|
||||
client_id :: binary(),
|
||||
mac :: binary(),
|
||||
network_id,
|
||||
ip,
|
||||
mask_len,
|
||||
aes_key,
|
||||
cookie = 1,
|
||||
|
||||
session_token :: binary(),
|
||||
|
||||
private_key,
|
||||
|
||||
sessions = #{}
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
test() ->
|
||||
{ok, Pid} = start_link(),
|
||||
register(Pid),
|
||||
ok.
|
||||
|
||||
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, TunSocket} = gen_udp:open(0, [binary, {active, true}]),
|
||||
Ip = sdlan_ipaddr:ipv4_to_int({10, 211, 179, 1}),
|
||||
|
||||
{ok, #state{tun_socket = TunSocket, network_id = 8, mac = <<11, 12, 13, 14, 15, 16>>, ip = Ip, mask_len = 24, 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{tun_socket = Socket, network_id = NId, client_id = ClientId, ip = Ip, mac = Mac, mask_len = MaskLen}) ->
|
||||
{PublicKeyBin, PrivateKey} = sdlan_cipher:rsa_generate_key(),
|
||||
|
||||
Register = #sdl_register_super {
|
||||
pkt_id = 1,
|
||||
client_id = ClientId,
|
||||
network_id = NId,
|
||||
mac = Mac,
|
||||
ip = Ip,
|
||||
mask_len = MaskLen,
|
||||
hostname = <<"mysql1">>,
|
||||
pub_key = PublicKeyBin,
|
||||
access_token = <<"access_token1234">>
|
||||
},
|
||||
logger:debug("register is: ~p", [Register]),
|
||||
|
||||
Pkt = sdlan_pb:encode_msg(Register),
|
||||
ok = gen_udp:send(Socket, ?SUPER_HOST, ?SUPER_PORT, <<?PACKET_REGISTER_SUPER, Pkt/binary>>),
|
||||
|
||||
{reply, ok, State#state{private_key = PrivateKey}}.
|
||||
|
||||
%% @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({udp, _Socket, _ServerIp, _ServerPort, <<?PACKET_REGISTER_SUPER_ACK, Data/binary>>}, State = #state{private_key = PrivateKey}) ->
|
||||
SuperAck = #sdl_register_super_ack{session_token = SessionToken, aes_key = EncAesKey} = sdlan_pb:decode_msg(Data, sdl_register_super_ack),
|
||||
|
||||
AesKey = sdlan_cipher:rsa_decrypt(EncAesKey, PrivateKey),
|
||||
logger:debug("[stun_client] get a register super ack: ~p, aes_key: ~p", [SuperAck, AesKey]),
|
||||
|
||||
erlang:start_timer(5000, self(), stun_request_ticker),
|
||||
|
||||
{noreply, State#state{session_token = SessionToken}};
|
||||
|
||||
handle_info({udp, _Socket, _ServerIp, _ServerPort, <<?PACKET_REGISTER_SUPER_NAK, Data/binary>>}, State) ->
|
||||
SuperNak = #sdl_register_super_nak{} = sdlan_pb:decode_msg(Data, sdl_register_super_nak),
|
||||
logger:debug("[stun_client] get a register super nak: ~p", [SuperNak]),
|
||||
{noreply, State#state{}};
|
||||
|
||||
%% 错误的事件处理
|
||||
handle_info({udp, _Socket, _ServerIp, _ServerPort, <<?PACKET_EVENT, ?PACKET_EVENT_REFRESH_AUTH, Data/binary>>}, State) ->
|
||||
Event = #sdl_refresh_auth_event{} = sdlan_pb:decode_msg(Data, sdl_refresh_auth_event),
|
||||
logger:debug("[stun_client] get a refresh_auth_event: ~p", [Event]),
|
||||
{stop, refresh_auth, State};
|
||||
|
||||
handle_info({udp, _Socket, _ServerIp, _ServerPort, <<?PACKET_PEER_INFO, Data/binary>>}, State) ->
|
||||
PeerInfo = #sdl_peer_info{} = sdlan_pb:decode_msg(Data, sdl_peer_info),
|
||||
logger:debug("[stun_client] get peer info: ~p", [PeerInfo]),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({udp, _Socket, _ServerIp, _ServerPort, <<?PACKET_EVENT, EventType:8, Event/binary>>}, State) ->
|
||||
logger:debug("[stun_client] get event_type: ~p, event: ~p", [EventType, Event]),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({timeout, _, stun_request_ticker},
|
||||
State = #state{tun_socket = TunSocket, network_id = NetworkId, client_id = ClientId, mac = Mac, ip = Ip, session_token = SessionToken}) ->
|
||||
|
||||
Packet = sdlan_pb:encode_msg(#sdl_stun_request{
|
||||
client_id = ClientId,
|
||||
network_id = 8,
|
||||
mac = Mac,
|
||||
ip = Ip,
|
||||
nat_type = 0,
|
||||
session_token = SessionToken
|
||||
}),
|
||||
logger:debug("timer ticker will send packet: ~p", [Packet]),
|
||||
|
||||
gen_udp:send(TunSocket, ?SUPER_HOST, ?SUPER_PORT, <<?PACKET_STUN_REQUEST, Packet/binary>>),
|
||||
erlang:start_timer(5000, self(), stun_request_ticker),
|
||||
|
||||
QueryPacket = sdlan_pb:encode_msg(#sdl_query_info{
|
||||
pkt_id = 10,
|
||||
network_id = NetworkId,
|
||||
src_mac = Mac,
|
||||
dst_mac = Mac,
|
||||
session_token = SessionToken
|
||||
}),
|
||||
gen_udp:send(TunSocket, ?SUPER_HOST, ?SUPER_PORT, <<?PACKET_QUERY_INFO, QueryPacket/binary>>),
|
||||
logger:debug("[stun_client] will send query packet: ~p", [QueryPacket]),
|
||||
|
||||
{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
|
||||
%%%===================================================================
|
||||
@ -8,74 +8,34 @@
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
%% 证书配置
|
||||
{certificate, [
|
||||
{certfile, "/usr/local/code/erl/sdlan/priv/ssl/server.crt"},
|
||||
{keyfile, "/usr/local/code/erl/sdlan/priv/ssl/server.key"}
|
||||
]},
|
||||
|
||||
{quic_server, [
|
||||
{port, 443},
|
||||
{acceptors, 10},
|
||||
{alpn, ["punchnet/1.0"]},
|
||||
{limits, [
|
||||
{max_packet_size, 16384},
|
||||
{stream_active_n, 100},
|
||||
%% 单位为秒
|
||||
{heartbeat_sec, 15}
|
||||
]}
|
||||
]},
|
||||
|
||||
{ssl_server, [
|
||||
{port, 1443},
|
||||
{acceptors, 500},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240},
|
||||
{limits, [
|
||||
{max_packet_size, 16384},
|
||||
%% 单位为秒
|
||||
{heartbeat_sec, 15}
|
||||
]}
|
||||
]},
|
||||
|
||||
%% 网络带宽, 单位为: kb
|
||||
{band_width, 2048},
|
||||
|
||||
%% stun类型探测相当于有个类型
|
||||
{stun_servers, [
|
||||
{port, 1365},
|
||||
{acceptor_nums, 5}
|
||||
{port, 1265},
|
||||
{acceptor_nums, 1}
|
||||
]},
|
||||
|
||||
{stun_port_assist, [
|
||||
{port, 1366}
|
||||
{port, 1266}
|
||||
]},
|
||||
|
||||
{stun_peer_assist, [
|
||||
{ip, {47,98,178,3}},
|
||||
{port, 1366}
|
||||
{port, 1266}
|
||||
]},
|
||||
|
||||
{ipv6_assist, [
|
||||
{port, 1367},
|
||||
{acceptor_nums, 5},
|
||||
{global_addr6, "2408:4005:318:4900:164a:b5c6:9023:ca8d"}
|
||||
%% 公共的dns域名解析服务
|
||||
{public_dns_servers, [
|
||||
{{114, 114, 114, 114}, 53},
|
||||
{{8,8,8,8}, 53}
|
||||
]},
|
||||
|
||||
{pools, [
|
||||
%% mysql连接池配置
|
||||
{mysql_sdlan,
|
||||
[{size, 10}, {max_overflow, 20}, {worker_module, mysql}],
|
||||
[
|
||||
{host, {118, 178, 229, 213}},
|
||||
{port, 3306},
|
||||
{user, "punchnetuser"},
|
||||
{connect_mode, lazy},
|
||||
{keep_alive, true},
|
||||
{password, "punchnet@J1c8WGu"},
|
||||
{database, "punchnet_v2"},
|
||||
{queries, [<<"set names utf8">>]}
|
||||
]
|
||||
{dns_resolver_pool,
|
||||
[{size, 20}, {max_overflow, 100}, {worker_module, dns_resolver}],
|
||||
[]
|
||||
}
|
||||
]},
|
||||
|
||||
@ -103,15 +63,14 @@
|
||||
{handler, disk, logger_disk_log_h,
|
||||
#{
|
||||
level => debug,
|
||||
config => #{
|
||||
file => "log/debug.log",
|
||||
max_no_files => 10,
|
||||
max_no_bytes => 524288000
|
||||
},
|
||||
config => #{
|
||||
file => "log/debug.log",
|
||||
max_no_files => 10,
|
||||
max_no_bytes => 524288000
|
||||
},
|
||||
formatter => {logger_formatter, #{template => [time, " [", level, "] ", msg, "\n"]}}
|
||||
}
|
||||
}
|
||||
|
||||
]}
|
||||
]}
|
||||
].
|
||||
|
||||
@ -3,39 +3,9 @@
|
||||
|
||||
{http_server, [
|
||||
{port, 18082},
|
||||
{acceptors, 50},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
%% 证书配置
|
||||
{certificate, [
|
||||
{certfile, "fullchain.cer"},
|
||||
{keyfile, "root.punchsky.com.key"}
|
||||
]},
|
||||
|
||||
{quic_server, [
|
||||
{port, 443},
|
||||
{acceptors, 10},
|
||||
{alpn, ["punchnet/1.0"]},
|
||||
{limits, [
|
||||
{max_packet_size, 16384},
|
||||
{stream_active_n, 100},
|
||||
%% 单位为秒
|
||||
{heartbeat_sec, 15}
|
||||
]}
|
||||
]},
|
||||
|
||||
{ssl_server, [
|
||||
{port, 1443},
|
||||
{acceptors, 500},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240},
|
||||
{limits, [
|
||||
{max_packet_size, 16384},
|
||||
%% 单位为秒
|
||||
{heartbeat_sec, 15}
|
||||
]}
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
%% 网络带宽, 单位为: kb
|
||||
@ -43,42 +13,34 @@
|
||||
|
||||
%% stun类型探测相当于有个类型
|
||||
{stun_servers, [
|
||||
{port, 1365},
|
||||
{port, 1265},
|
||||
{acceptor_nums, 5}
|
||||
]},
|
||||
|
||||
{stun_port_assist, [
|
||||
{port, 1366}
|
||||
{port, 1266}
|
||||
]},
|
||||
|
||||
{stun_peer_assist, [
|
||||
{ip, {47,98,178,3}},
|
||||
{port, 1366}
|
||||
{port, 1266}
|
||||
]},
|
||||
|
||||
{ipv6_assist, [
|
||||
{port, 1367},
|
||||
{acceptor_nums, 5},
|
||||
{global_addr6, "2408:4005:318:4900:164a:b5c6:9023:ca8d"}
|
||||
%% 公共的dns域名解析服务
|
||||
{public_dns_servers, [
|
||||
{{114, 114, 114, 114}, 53},
|
||||
{{8,8,8,8}, 53}
|
||||
]},
|
||||
|
||||
{pools, [
|
||||
%% mysql连接池配置
|
||||
{mysql_sdlan,
|
||||
[{size, 10}, {max_overflow, 20}, {worker_module, mysql}],
|
||||
[
|
||||
{host, {118, 178, 229, 213}},
|
||||
{port, 3306},
|
||||
{user, "punchnetuser"},
|
||||
{keep_alive, true},
|
||||
{password, "punchnet@J1c8WGu"},
|
||||
{database, "punchnet_v2"},
|
||||
{queries, [<<"set names utf8">>]}
|
||||
]
|
||||
{dns_resolver_pool,
|
||||
[{size, 20}, {max_overflow, 100}, {worker_module, dns_resolver}],
|
||||
[]
|
||||
}
|
||||
|
||||
]},
|
||||
|
||||
{api_url, "https://root.punchsky.com/api/"}
|
||||
{api_url, "https://punchnet.aioe.tech/api/"}
|
||||
|
||||
]},
|
||||
|
||||
@ -103,14 +65,15 @@
|
||||
#{
|
||||
level => debug,
|
||||
config => #{
|
||||
file => "log/debug.log",
|
||||
max_no_files => 10,
|
||||
max_no_bytes => 524288000
|
||||
},
|
||||
file => "log/debug.log",
|
||||
max_no_files => 10,
|
||||
max_no_bytes => 524288000
|
||||
},
|
||||
formatter => {logger_formatter, #{template => [time, " [", level, "] ", msg, "\n"]}}
|
||||
}
|
||||
}
|
||||
|
||||
]}
|
||||
]}
|
||||
|
||||
].
|
||||
|
||||
386
docs/api.md
386
docs/api.md
@ -1,386 +0,0 @@
|
||||
# HTTP API 接口文档
|
||||
|
||||
本文档根据 `src/http` 下各 handler 的 `handle_request/4` 实现整理。
|
||||
|
||||
## 通用说明
|
||||
|
||||
- HTTP 服务端口由应用配置 `sdlan.http_server.port` 决定。
|
||||
- 响应 `Content-Type` 为 `application/json;charset=utf-8`。
|
||||
- 当响应体大小大于等于 1024 字节且请求头 `Accept-Encoding` 包含 `gzip` 时,响应会启用 gzip 压缩。
|
||||
- GET 参数来自 URL query string。
|
||||
- POST 请求体支持两种格式:
|
||||
- `Content-Type: application/json`:请求体会按 JSON object 解析,数字保持为 JSON number。
|
||||
- `Content-Type: application/x-www-form-urlencoded`:请求体会按表单解析,参数值为 binary/string;涉及整型参数的接口建议使用 JSON 请求体。
|
||||
- 统一成功返回格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": "<any>"
|
||||
}
|
||||
```
|
||||
|
||||
- 统一失败返回格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": -1,
|
||||
"message": "错误描述"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- handler 未匹配到 URL、Method 或参数结构时,通常返回 HTTP 200,并返回如下错误格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": -1,
|
||||
"message": "url: /path not found"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## binlog_handler
|
||||
|
||||
路由配置:`/binlog`
|
||||
|
||||
### POST /binlog
|
||||
|
||||
接收 binlog 通知,目前只记录请求参数并返回固定结果。
|
||||
|
||||
**Method**:`POST`
|
||||
|
||||
**参数**:无必填参数。请求体可为 JSON object 或 form。
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 任意字段 | body | object/string/number/boolean/array | 否 | 当前实现不读取具体字段,仅打印完整请求体参数。 |
|
||||
|
||||
**成功返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| result | string | 固定为 `"ok"`。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
## test_handler
|
||||
|
||||
路由配置:`/test/[...]`
|
||||
|
||||
### POST /test/auth_access_token
|
||||
|
||||
测试用 access token 校验接口,目前返回固定结果。
|
||||
|
||||
**Method**:`POST`
|
||||
|
||||
**参数**:无必填参数。请求体可为 JSON object 或 form。
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 任意字段 | body | object/string/number/boolean/array | 否 | 当前实现不读取具体字段。 |
|
||||
|
||||
**成功返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| result | string | 固定为 `"ok"`。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
### GET /test/get_all_networks
|
||||
|
||||
测试用获取全部网络 ID。
|
||||
|
||||
**Method**:`GET`
|
||||
|
||||
**参数**:无。
|
||||
|
||||
**成功返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| result | array<integer> | 网络 ID 列表。当前实现固定返回 `[8]`。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": [8]
|
||||
}
|
||||
```
|
||||
|
||||
### GET /test/get_network
|
||||
|
||||
测试用获取单个网络详情。
|
||||
|
||||
**Method**:`GET`
|
||||
|
||||
**参数**:
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | query | string(integer) | 是 | 网络 ID。实现中会通过 `binary_to_integer/1` 转为 integer;当前仅内置 ID `8`。 |
|
||||
|
||||
**成功返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| result | object | 网络详情对象。 |
|
||||
| result.id | integer | 网络 ID。 |
|
||||
| result.name | string | 网络名称。 |
|
||||
| result.domain | string | 网络域名。 |
|
||||
| result.ipaddr | string | 网络地址段,CIDR 格式。 |
|
||||
| result.owner_id | integer | 所属用户 ID。 |
|
||||
| result.algorithm | string | 加密算法。 |
|
||||
| result.disabled_clients | array<string> | 禁用的客户端 ID 列表。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"id": 8,
|
||||
"name": "test1",
|
||||
"domain": "punchnet.cn",
|
||||
"ipaddr": "10.211.179.0/24",
|
||||
"owner_id": 1234,
|
||||
"algorithm": "chacha20",
|
||||
"disabled_clients": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## api_handler
|
||||
|
||||
路由配置:`/api/[...]`
|
||||
|
||||
注意:当前 `api_handler` 的实现匹配路径为 `/test/auth_token`,但 Cowboy 路由只会把 `/api/[...]` 请求分发给 `api_handler`。因此按当前路由配置,该接口不可达;直接请求 `/test/auth_token` 会被分发到 `test_handler`,也不会命中此实现。
|
||||
|
||||
### POST /test/auth_token
|
||||
|
||||
获取测试 auth token 信息,返回第一个网络 ID。
|
||||
|
||||
**Method**:`POST`
|
||||
|
||||
**当前可达性**:不可达,原因见本节说明。
|
||||
|
||||
**参数**:无必填参数。请求体可为 JSON object 或 form。
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 任意字段 | body | object/string/number/boolean/array | 否 | 当前实现不读取具体字段,仅打印完整请求体参数。 |
|
||||
|
||||
**成功返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| result | object | 返回数据对象。 |
|
||||
| result.network_id | integer | `network_bo:get_all_networks/0` 返回列表中的第一个网络 ID。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"network_id": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## network_handler
|
||||
|
||||
路由配置:`/network/[...]`
|
||||
|
||||
### POST /network/create
|
||||
|
||||
启动或确保指定网络已启动。
|
||||
|
||||
**Method**:`POST`
|
||||
|
||||
**参数**:
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | body | integer | 是 | 网络 ID,需大于 0。建议使用 JSON number。 |
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 8
|
||||
}
|
||||
```
|
||||
|
||||
**成功返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| result | string | 固定为 `"success"`。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": "success"
|
||||
}
|
||||
```
|
||||
|
||||
**失败返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| error.code | integer | 固定为 `-1`。 |
|
||||
| error.message | string | 固定为 `"error"`。 |
|
||||
|
||||
### POST /network/delete
|
||||
|
||||
删除指定网络;如果网络未启动,也视为删除成功。
|
||||
|
||||
**Method**:`POST`
|
||||
|
||||
**参数**:
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | body | integer | 是 | 网络 ID,需大于 0。建议使用 JSON number。 |
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 8
|
||||
}
|
||||
```
|
||||
|
||||
**成功返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| result | string | 固定为 `"success"`。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": "success"
|
||||
}
|
||||
```
|
||||
|
||||
**失败返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| error.code | integer | 固定为 `-1`。 |
|
||||
| error.message | string | 固定为 `"error"`。 |
|
||||
|
||||
### POST /network/exit_node_control
|
||||
|
||||
向指定客户端下发出口节点控制命令,并等待客户端 ACK。
|
||||
|
||||
**Method**:`POST`
|
||||
|
||||
**参数**:
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | body | integer | 是 | 网络 ID,需大于 0。建议使用 JSON number。 |
|
||||
| action | body | integer | 是 | 出口节点动作,传入 `SDLCommand.ExitNodeControl.action`。历史文档约定:`0` 表示关闭,`1` 表示开启。 |
|
||||
| client_id | body | string | 是 | 目标客户端 ID。 |
|
||||
| remark | body | string | 是 | 下发备注,用于跟踪命令;可传空字符串。 |
|
||||
| timeout | body | integer | 是 | 等待 ACK 的超时时间,单位秒。实现中会转换为毫秒。 |
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 8,
|
||||
"action": 1,
|
||||
"client_id": "client-id",
|
||||
"remark": "trace remark",
|
||||
"timeout": 10
|
||||
}
|
||||
```
|
||||
|
||||
**成功返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| result | string | 正常 ACK 时固定为 `"success"`。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": "success"
|
||||
}
|
||||
```
|
||||
|
||||
**其他返回**:
|
||||
|
||||
| 场景 | 返回格式 | 字段类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 网络不存在 | `{"result":"network not found"}` | result: string | 当前实现将网络不存在作为 result 返回,而不是 error。 |
|
||||
| 命令发送失败 | `{"error":{"code":-1,"message":"..."}}` | code: integer, message: string | message 为底层返回的 Reason,需为可 JSON 编码值。 |
|
||||
| 客户端 ACK 失败 | `{"error":{"code":<Code>,"message":"<Message>"}}` | code: integer, message: string | Code 和 Message 来自 `SDLCommandAck`。 |
|
||||
| 等待 ACK 超时 | `{"error":{"code":-1,"message":"任务执行超时"}}` | code: integer, message: string | 超过 `timeout` 秒未收到 ACK。 |
|
||||
|
||||
## node_handler
|
||||
|
||||
路由配置:`/node/[...]`
|
||||
|
||||
### POST /node/disable
|
||||
|
||||
禁用指定网络中的客户端节点。
|
||||
|
||||
**Method**:`POST`
|
||||
|
||||
**参数**:
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| network_id | body | integer | 是 | 网络 ID,需大于 0。建议使用 JSON number。 |
|
||||
| client_id | body | string | 是 | 客户端 ID。 |
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"network_id": 8,
|
||||
"client_id": "client-id"
|
||||
}
|
||||
```
|
||||
|
||||
**成功返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| result | string | 固定为 `"success"`。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": "success"
|
||||
}
|
||||
```
|
||||
|
||||
**失败返回**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| error.code | integer | 固定为 `-1`。 |
|
||||
| error.message | string | 当前实现中网络不存在时为 `"network not found"`。 |
|
||||
@ -1,2 +0,0 @@
|
||||
## 查看当前系统的dns
|
||||
scutil --dns
|
||||
@ -1,40 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 28. 2月 2026 15:25
|
||||
%%%-------------------------------------------------------------------
|
||||
-author("anlicheng").
|
||||
|
||||
-record(identity, {
|
||||
identity_id :: integer(),
|
||||
network_id :: integer(),
|
||||
subject_type,
|
||||
created_at :: integer(),
|
||||
expired_at :: integer()
|
||||
}).
|
||||
|
||||
-record(identity_policy, {
|
||||
identity_id :: integer(),
|
||||
policy_id :: integer()
|
||||
}).
|
||||
|
||||
-record(policy, {
|
||||
policy_id :: integer(),
|
||||
network_id :: integer(),
|
||||
name :: binary(),
|
||||
created_at = 0 :: integer()
|
||||
}).
|
||||
|
||||
-record(rule, {
|
||||
rule_id :: integer(),
|
||||
network_id :: integer(),
|
||||
src_policy_id :: integer(),
|
||||
dst_policy_id :: integer(),
|
||||
proto :: integer(),
|
||||
port :: integer(),
|
||||
action = allow :: allow | deny,
|
||||
created_at = 0 :: integer()
|
||||
}).
|
||||
@ -1,270 +0,0 @@
|
||||
%% -*- coding: utf-8 -*-
|
||||
%% Automatically generated, do not edit
|
||||
%% Generated by gpb_compile version 4.21.7
|
||||
|
||||
-ifndef(sdlan_pb).
|
||||
-define(sdlan_pb, true).
|
||||
|
||||
-define(sdlan_pb_gpb_version, "4.21.7").
|
||||
|
||||
|
||||
-ifndef('SDLV4INFO_PB_H').
|
||||
-define('SDLV4INFO_PB_H', true).
|
||||
-record('SDLV4Info',
|
||||
{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('SDLV6INFO_PB_H').
|
||||
-define('SDLV6INFO_PB_H', true).
|
||||
-record('SDLV6Info',
|
||||
{port = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
v6 = <<>> :: iodata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLV6ASSISTPROBE_PB_H').
|
||||
-define('SDLV6ASSISTPROBE_PB_H', true).
|
||||
-record('SDLV6AssistProbe',
|
||||
{pkt_id = 0 :: non_neg_integer() | undefined % = 1, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLV6ASSISTPROBEREPLY_PB_H').
|
||||
-define('SDLV6ASSISTPROBEREPLY_PB_H', true).
|
||||
-record('SDLV6AssistProbeReply',
|
||||
{pkt_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
v6_info = undefined :: sdlan_pb:'SDLV6Info'() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLWELCOME_PB_H').
|
||||
-define('SDLWELCOME_PB_H', true).
|
||||
-record('SDLWelcome',
|
||||
{version = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
max_bidi_streams = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
max_packet_size = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
heartbeat_sec = 0 :: non_neg_integer() | undefined, % = 4, optional, 32 bits
|
||||
ipv6_assist = undefined :: sdlan_pb:'SDLV6Info'() | undefined % = 5, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLREGISTERSUPER_PB_H').
|
||||
-define('SDLREGISTERSUPER_PB_H', true).
|
||||
-record('SDLRegisterSuper',
|
||||
{client_id = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
network_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
mac = <<>> :: iodata() | undefined, % = 3, optional
|
||||
ip = 0 :: non_neg_integer() | undefined, % = 4, optional, 32 bits
|
||||
mask_len = 0 :: non_neg_integer() | undefined, % = 5, optional, 32 bits
|
||||
hostname = <<>> :: unicode:chardata() | undefined, % = 6, optional
|
||||
pub_key = <<>> :: unicode:chardata() | undefined, % = 7, optional
|
||||
access_token = <<>> :: unicode:chardata() | undefined % = 8, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLREGISTERSUPERACK_PB_H').
|
||||
-define('SDLREGISTERSUPERACK_PB_H', true).
|
||||
-record('SDLRegisterSuperAck',
|
||||
{algorithm = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
key = <<>> :: iodata() | undefined, % = 2, optional
|
||||
region_id = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
session_token = <<>> :: iodata() | undefined % = 4, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLREGISTERSUPERNAK_PB_H').
|
||||
-define('SDLREGISTERSUPERNAK_PB_H', true).
|
||||
-record('SDLRegisterSuperNak',
|
||||
{error_code = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
error_message = <<>> :: unicode:chardata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLQUERYINFO_PB_H').
|
||||
-define('SDLQUERYINFO_PB_H', true).
|
||||
-record('SDLQueryInfo',
|
||||
{dst_mac = <<>> :: iodata() | undefined % = 1, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLPEERINFO_PB_H').
|
||||
-define('SDLPEERINFO_PB_H', true).
|
||||
-record('SDLPeerInfo',
|
||||
{dst_mac = <<>> :: iodata() | undefined, % = 1, optional
|
||||
v4_info :: sdlan_pb:'SDLV4Info'() | undefined, % = 2, optional
|
||||
v6_info :: sdlan_pb:'SDLV6Info'() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLARPREQUEST_PB_H').
|
||||
-define('SDLARPREQUEST_PB_H', true).
|
||||
-record('SDLArpRequest',
|
||||
{target_ip = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
origin_ip = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
context = <<>> :: iodata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLARPRESPONSE_PB_H').
|
||||
-define('SDLARPRESPONSE_PB_H', true).
|
||||
-record('SDLArpResponse',
|
||||
{target_ip = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
target_mac = <<>> :: iodata() | undefined, % = 2, optional
|
||||
origin_ip = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
context = <<>> :: iodata() | undefined % = 4, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLPOLICYREQUEST_PB_H').
|
||||
-define('SDLPOLICYREQUEST_PB_H', true).
|
||||
-record('SDLPolicyRequest',
|
||||
{src_identity_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
dst_identity_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
version = 0 :: non_neg_integer() | undefined % = 3, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLPOLICYRESPONSE_PB_H').
|
||||
-define('SDLPOLICYRESPONSE_PB_H', true).
|
||||
-record('SDLPolicyResponse',
|
||||
{src_identity_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
dst_identity_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
version = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
rules = <<>> :: iodata() | undefined % = 4, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLEVENT.NATCHANGED_PB_H').
|
||||
-define('SDLEVENT.NATCHANGED_PB_H', true).
|
||||
-record('SDLEvent.NatChanged',
|
||||
{mac = <<>> :: iodata() | undefined, % = 1, optional
|
||||
ip = 0 :: non_neg_integer() | undefined % = 2, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLEVENT.SENDREGISTER_PB_H').
|
||||
-define('SDLEVENT.SENDREGISTER_PB_H', true).
|
||||
-record('SDLEvent.SendRegister',
|
||||
{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:'SDLV6Info'() | undefined % = 5, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLEVENT.NETWORKSHUTDOWN_PB_H').
|
||||
-define('SDLEVENT.NETWORKSHUTDOWN_PB_H', true).
|
||||
-record('SDLEvent.NetworkShutdown',
|
||||
{message = <<>> :: unicode:chardata() | undefined % = 1, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLEVENT_PB_H').
|
||||
-define('SDLEVENT_PB_H', true).
|
||||
-record('SDLEvent',
|
||||
{event :: {nat_changed, sdlan_pb:'SDLEvent.NatChanged'()} | {send_register, sdlan_pb:'SDLEvent.SendRegister'()} | {shutdown, sdlan_pb:'SDLEvent.NetworkShutdown'()} | undefined % oneof
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLCOMMAND.EXITNODECONTROL_PB_H').
|
||||
-define('SDLCOMMAND.EXITNODECONTROL_PB_H', true).
|
||||
-record('SDLCommand.ExitNodeControl',
|
||||
{action = 0 :: integer() | undefined, % = 1, optional, 32 bits
|
||||
remark = <<>> :: unicode:chardata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLCOMMAND_PB_H').
|
||||
-define('SDLCOMMAND_PB_H', true).
|
||||
-record('SDLCommand',
|
||||
{pkt_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
command :: {exit_node, sdlan_pb:'SDLCommand.ExitNodeControl'()} | undefined % oneof
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLCOMMANDACK_PB_H').
|
||||
-define('SDLCOMMANDACK_PB_H', true).
|
||||
-record('SDLCommandAck',
|
||||
{pkt_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
code = 0 :: integer() | undefined, % = 2, optional, 32 bits
|
||||
message = <<>> :: unicode:chardata() | undefined, % = 3, optional
|
||||
data = <<>> :: iodata() | undefined % = 4, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLSTUNREQUEST_PB_H').
|
||||
-define('SDLSTUNREQUEST_PB_H', true).
|
||||
-record('SDLStunRequest',
|
||||
{client_id = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
network_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
mac = <<>> :: iodata() | undefined, % = 3, optional
|
||||
ip = 0 :: non_neg_integer() | undefined, % = 4, optional, 32 bits
|
||||
nat_type = 0 :: non_neg_integer() | undefined, % = 5, optional, 32 bits
|
||||
v6_info :: sdlan_pb:'SDLV6Info'() | undefined, % = 6, optional
|
||||
session_token = <<>> :: iodata() | undefined % = 7, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLSTUNREPLY_PB_H').
|
||||
-define('SDLSTUNREPLY_PB_H', true).
|
||||
-record('SDLStunReply',
|
||||
{
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLDATA_PB_H').
|
||||
-define('SDLDATA_PB_H', true).
|
||||
-record('SDLData',
|
||||
{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
|
||||
session_token = <<>> :: iodata() | undefined, % = 7, optional
|
||||
identity_id = 0 :: non_neg_integer() | undefined % = 8, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLSTUNPROBE_PB_H').
|
||||
-define('SDLSTUNPROBE_PB_H', true).
|
||||
-record('SDLStunProbe',
|
||||
{cookie = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
attr = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
step = 0 :: non_neg_integer() | undefined % = 3, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLSTUNPROBEREPLY_PB_H').
|
||||
-define('SDLSTUNPROBEREPLY_PB_H', true).
|
||||
-record('SDLStunProbeReply',
|
||||
{cookie = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
step = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
port = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
|
||||
ip = 0 :: non_neg_integer() | undefined % = 4, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('SDLREGISTER_PB_H').
|
||||
-define('SDLREGISTER_PB_H', true).
|
||||
-record('SDLRegister',
|
||||
{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('SDLREGISTERACK_PB_H').
|
||||
-define('SDLREGISTERACK_PB_H', true).
|
||||
-record('SDLRegisterAck',
|
||||
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
src_mac = <<>> :: iodata() | undefined, % = 2, optional
|
||||
dst_mac = <<>> :: iodata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-endif.
|
||||
48
maxwell.md
48
maxwell.md
@ -1,48 +0,0 @@
|
||||
## maxwell配置
|
||||
|
||||
## docker-compose.yml配置
|
||||
```yaml
|
||||
|
||||
services:
|
||||
maxwell:
|
||||
image: docker.1ms.run/zendesk/maxwell:latest
|
||||
container_name: maxwell
|
||||
network_mode: "host"
|
||||
|
||||
restart: always
|
||||
entrypoint: ["bin/maxwell"]
|
||||
|
||||
command:
|
||||
- --host=127.0.0.1
|
||||
- --port=3306
|
||||
- --user=punchnetuser
|
||||
- --password=punchnet@J1c8WGu
|
||||
- --producer=redis
|
||||
- --redis_host=127.0.0.1
|
||||
- --redis_port=16379
|
||||
- --redis_type=stream
|
||||
- --redis_stream=maxwell_stream
|
||||
- --log_level=info
|
||||
```
|
||||
|
||||
## mysql权限配置
|
||||
|
||||
```sql
|
||||
|
||||
# 创建maxwell依赖的数据库
|
||||
|
||||
CREATE DATABASE maxwell;
|
||||
GRANT ALL ON maxwell.* TO 'punchnetuser'@'%';
|
||||
|
||||
-- 1️⃣ 允许读取数据
|
||||
GRANT SELECT ON *.* TO 'punchnetuser'@'%';
|
||||
|
||||
-- 2️⃣ 允许复制权限(关键)
|
||||
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'punchnetuser'@'%';
|
||||
|
||||
-- 3️⃣ 如果还需要创建 maxwell 库
|
||||
GRANT ALL ON maxwell.* TO 'punchnetuser'@'%';
|
||||
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
```
|
||||
209
message.proto
Normal file
209
message.proto
Normal file
@ -0,0 +1,209 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// 注意
|
||||
// 1. network_id, ip地址等分配的逻辑已经迁移到后端https服务
|
||||
// 2. 端的强制升级逻辑也迁移到了https服务
|
||||
// 3. 假设一个udp包的最大有效负载为1400字节;因此虚拟网卡启动的时候mtu需要控制一下,因为每个包里面都有自己的元数据信息, 端上的mtu值先统一为:1250
|
||||
|
||||
// 基础公共类型定义
|
||||
|
||||
message SDLV4Info {
|
||||
uint32 port = 1;
|
||||
bytes v4 = 2;
|
||||
uint32 nat_type = 3;
|
||||
}
|
||||
|
||||
message SDLV6Info {
|
||||
uint32 port = 1;
|
||||
bytes v6 = 2;
|
||||
}
|
||||
|
||||
message SDLEmpty {
|
||||
uint32 pkt_id = 1;
|
||||
}
|
||||
|
||||
// 这里修改成了扁平的结构, 否则有些字段不好找放的位置
|
||||
message SDLRegisterSuper {
|
||||
// 所有需要建立请求和响应对应关系的,都是通过4字节的pktId来对应
|
||||
uint32 pkt_id = 1;
|
||||
string client_id = 2;
|
||||
// 网络地址信息已经有https请求分配了
|
||||
// 注册的时候需要带上(network_id, mac, ip, mask_len, hostname)
|
||||
uint32 network_id = 3;
|
||||
bytes mac = 4;
|
||||
uint32 ip = 5;
|
||||
uint32 mask_len = 6;
|
||||
string hostname = 7;
|
||||
|
||||
string pub_key = 8;
|
||||
// 客户端使用http协议请求后端,通过token或者账号密码登录时, 统一返回一个access_token;
|
||||
// RegisterSuper的时候,验证凭证是否合法 (access_token)
|
||||
string access_token = 9;
|
||||
}
|
||||
|
||||
message SDLRegisterSuperAck {
|
||||
uint32 pkt_id = 1;
|
||||
bytes aes_key = 2;
|
||||
// 验证通过后,返回session_token; 后续通讯的合法行需哟啊通过session_token来保证
|
||||
// 在SDLQueryInfo,SDLStunRequest, SDLData, SDLArpRequest等需要服务器端介入的地方都增加了session_token的验证(端和端之间的SDLData不需要校验)
|
||||
bytes session_token = 3;
|
||||
}
|
||||
|
||||
// 注册失败时候的消息体
|
||||
message SDLRegisterSuperNak {
|
||||
uint32 pkt_id = 1;
|
||||
uint32 error_code = 2;
|
||||
string error_message = 3;
|
||||
}
|
||||
|
||||
// 网络地址查询
|
||||
message SDLQueryInfo {
|
||||
uint32 pkt_id = 1;
|
||||
uint32 network_id = 2;
|
||||
bytes src_mac = 3;
|
||||
bytes dst_mac = 4;
|
||||
bytes session_token = 5;
|
||||
}
|
||||
|
||||
message SDLPeerInfo {
|
||||
uint32 pkt_id = 1;
|
||||
uint32 network_id = 2;
|
||||
bytes dst_mac = 3;
|
||||
SDLV4Info v4_info = 4;
|
||||
optional SDLV6Info v6_info = 5;
|
||||
}
|
||||
|
||||
message SDLEmptyPeerInfo {
|
||||
uint32 pkt_id = 1;
|
||||
uint32 network_id = 2;
|
||||
bytes dst_mac = 3;
|
||||
}
|
||||
|
||||
// 事件定义, 下面的事件是服务器主动推送的,不需要响应
|
||||
|
||||
message SDLNatChangedEvent {
|
||||
uint32 network_id = 1;
|
||||
bytes mac = 2;
|
||||
uint32 ip = 3;
|
||||
}
|
||||
|
||||
// 被清理掉的Endpoints
|
||||
// 协议改成udp后,服务端只能通过定时器来集中清理掉线的端(通过心跳机制SDLStunRequest)
|
||||
message SDLDropMacsEvent {
|
||||
uint32 network_id = 1;
|
||||
repeated bytes macs = 2;
|
||||
}
|
||||
|
||||
// 通知端上必须重新校验
|
||||
// 服务器端认为端已经掉线了,但是这个时候还在向服务端发送消息;服务端要求端上重新校验(SDLRegisterSuper)
|
||||
message SDLRefreshAuthEvent {
|
||||
uint32 network_id = 1;
|
||||
}
|
||||
|
||||
message SDLSendRegisterEvent {
|
||||
uint32 network_id = 1;
|
||||
bytes dst_mac = 2;
|
||||
uint32 nat_ip = 3;
|
||||
uint32 nat_port = 4;
|
||||
uint32 nat_type = 5;
|
||||
optional SDLV6Info v6_info = 6;
|
||||
}
|
||||
|
||||
message SDLNetworkShutdownEvent {
|
||||
uint32 network_id = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
// UDP通讯消息
|
||||
message SDLStunRequest {
|
||||
string client_id = 1;
|
||||
uint32 network_id = 2;
|
||||
bytes mac = 3;
|
||||
uint32 ip = 4;
|
||||
uint32 nat_type = 5;
|
||||
optional SDLV6Info v6_info = 6;
|
||||
bytes session_token = 7;
|
||||
}
|
||||
|
||||
message SDLData {
|
||||
uint32 network_id = 1;
|
||||
bytes src_mac = 2;
|
||||
bytes dst_mac = 3;
|
||||
bool is_p2p = 4;
|
||||
uint32 ttl = 5;
|
||||
bytes data = 6;
|
||||
bytes session_token = 7;
|
||||
// 端通过https登录的时候,服务端会分配该端对应的权限标识
|
||||
// 后续的请求过程中需要带上这个值,对端通过这个值要判断对数据包是否放行
|
||||
uint32 identity_id = 8;
|
||||
}
|
||||
|
||||
// 这个是客户端之间的相互打洞的数据
|
||||
|
||||
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;
|
||||
// 增加step是为了方便端上判断,收到的请求和响应之间的映射关系;服务器端原样返回
|
||||
uint32 step = 3;
|
||||
}
|
||||
|
||||
message SDLStunProbeReply {
|
||||
uint32 cookie = 1;
|
||||
// 增加step是为了方便端上判断,收到的请求和响应之间的映射关系;服务器端原样返回
|
||||
uint32 step = 2;
|
||||
uint32 port = 3;
|
||||
uint32 ip = 4;
|
||||
}
|
||||
|
||||
// ARP查询相关
|
||||
// 真实的arp请求是通过广播的形式获取到的,但是针对于macos这种tun的实现;是能够分析出arp请求包的;对于当前网络来说,服务端是知道mac对应的ip地址的,因此没有必要广播;直接通过服务器端返回
|
||||
message SDLArpRequest {
|
||||
uint32 network_id = 1;
|
||||
uint32 target_ip = 2;
|
||||
bytes session_token = 3;
|
||||
}
|
||||
|
||||
message SDLArpResponse {
|
||||
uint32 network_id = 1;
|
||||
uint32 target_ip = 2;
|
||||
bytes target_mac = 3;
|
||||
}
|
||||
|
||||
// 权限请求查询相关
|
||||
message SDLPolicyRequest {
|
||||
string client_id = 1;
|
||||
uint32 network_id = 2;
|
||||
bytes mac = 3;
|
||||
uint32 src_identity_id = 4;
|
||||
uint32 dst_identity_id = 5;
|
||||
uint32 version = 6;
|
||||
bytes session_token = 7;
|
||||
}
|
||||
|
||||
message SDLPolicyResponse {
|
||||
uint32 network_id = 1;
|
||||
uint32 src_identity_id = 2;
|
||||
uint32 dst_identity_id = 3;
|
||||
// 版本号,客户端需要比较版本号确定是否覆盖
|
||||
uint32 version = 4;
|
||||
// 总包数
|
||||
uint32 total_num = 5;
|
||||
// 当前分片
|
||||
uint32 index = 6;
|
||||
// 4+1+2 的稀疏序列化规则
|
||||
bytes rules = 7;
|
||||
}
|
||||
101
policy.sql
101
policy.sql
@ -1,43 +1,64 @@
|
||||
CREATE TABLE `identity` (
|
||||
`identity_id` int NOT NULL AUTO_INCREMENT,
|
||||
`network_id` int NOT NULL,
|
||||
`subject_type` enum('token','instance','user','service') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`subject_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`created_at` int NOT NULL DEFAULT '0',
|
||||
`expired_at` int NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`identity_id`),
|
||||
UNIQUE KEY `uk_subject` (`network_id`,`subject_type`,`subject_id`),
|
||||
KEY `idx_network_id` (`network_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `identity_policy` (
|
||||
`identity_id` int NOT NULL,
|
||||
`policy_id` int NOT NULL,
|
||||
PRIMARY KEY (`identity_id`,`policy_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
CREATE TABLE identity (
|
||||
identity_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
network_id INT NOT NULL,
|
||||
-- 业务侧来源
|
||||
subject_type ENUM('token', 'instance', 'user', 'service') NOT NULL,
|
||||
subject_id VARCHAR(128) NOT NULL,
|
||||
|
||||
CREATE TABLE `policy` (
|
||||
`policy_id` int NOT NULL AUTO_INCREMENT,
|
||||
`network_id` int NOT NULL,
|
||||
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`created_at` int NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`policy_id`),
|
||||
KEY `idx_network_id` (`network_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
created_at INT(10) NOT NULL DEFAULT 0,
|
||||
expired_at INT(10) NOT NULL DEFAULT 0,
|
||||
|
||||
UNIQUE KEY uk_subject (network_id, subject_type, subject_id)
|
||||
);
|
||||
|
||||
-- policy 表(权限集合,可复用)
|
||||
CREATE TABLE policy (
|
||||
policy_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
network_id INT NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
description VARCHAR(255),
|
||||
created_at INT(10) NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- identity_policy(多对多关系)
|
||||
CREATE TABLE identity_policy (
|
||||
identity_id INT NOT NULL,
|
||||
policy_id INT NOT NULL,
|
||||
PRIMARY KEY (identity_id, policy_id)
|
||||
);
|
||||
|
||||
CREATE TABLE rule (
|
||||
rule_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
network_id INT NOT NULL,
|
||||
-- 来源限制(可选)
|
||||
src_policy_id INT NOT NULL,
|
||||
-- 目标限制(可选,允许 NULL 表示 any)
|
||||
dst_policy_id INT NULL,
|
||||
-- 6=TCP, 17=UDP
|
||||
proto TINYINT NOT NULL,
|
||||
-- 0~65535
|
||||
port INT NOT NULL,
|
||||
action ENUM('allow', 'deny') NOT NULL,
|
||||
created_at INT(10) NOT NULL DEFAULT 0,
|
||||
INDEX idx_src (src_policy_id),
|
||||
INDEX idx_dst (dst_policy_id)
|
||||
);
|
||||
|
||||
-- 实际操作逻辑
|
||||
-- 1. 通过Token获取user+password的方式找到对应的identity_id,每个端都会有一个对应的identity_id值
|
||||
-- 2. 数据访问的时候SDLData结构会携带一个identity_id, 被访问端会先查找自身的cache是否有对应identity_id(src_identity_id)的规则
|
||||
|
||||
|
||||
-- 难点
|
||||
-- 通过src_identity_id, dst_identity_id 查找到对应的rules
|
||||
|
||||
-- 查找来源对应的rules
|
||||
-- $src_policy_ids = select * from identity_policy where identity_id = $src_identity_id
|
||||
-- $drt_policy_ids = select * from identity_policy where identity_id = $dst_identity_id
|
||||
|
||||
-- 来源 src_policy_id 可以是Any (UNION {0});目标 dst_policy_id不能是Any
|
||||
-- select * from rule where src_policy_id in ($src_policy_ids UNION {0}) and dst_policy_id in $drt_policy_ids
|
||||
|
||||
-- 然后合并全部的rules
|
||||
|
||||
CREATE TABLE `rule` (
|
||||
`rule_id` int NOT NULL AUTO_INCREMENT,
|
||||
`network_id` int NOT NULL,
|
||||
`access_rule_id` int NOT NULL,
|
||||
`src_policy_id` int NOT NULL,
|
||||
`dst_policy_id` int NOT NULL,
|
||||
`proto` tinyint NOT NULL,
|
||||
`port` int NOT NULL,
|
||||
`action` enum('allow','deny') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`created_at` int NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`rule_id`),
|
||||
KEY `idx_src` (`src_policy_id`),
|
||||
KEY `idx_network_id` (`network_id`),
|
||||
KEY `idx_dst` (`dst_policy_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=85 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
@ -1,5 +0,0 @@
|
||||
# 提取公钥 -> 转换为 RSA 原始格式 (PKCS#1) -> 转为 DER 二进制 -> 计算哈希
|
||||
openssl x509 -in cert.pem -pubkey -noout | \
|
||||
openssl rsa -pubin -RSAPublicKey_out -outform DER | \
|
||||
openssl dgst -sha256 -binary | \
|
||||
base64
|
||||
@ -1,23 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDwzCCAqugAwIBAgIUMhNyuVo6MieWs2I9LsyTTiDKtYgwDQYJKoZIhvcNAQEL
|
||||
BQAwgYkxCzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdC
|
||||
ZWlqaW5nMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV
|
||||
BAMMCWFubGljaGVuZzEfMB0GCSqGSIb3DQEJARYQMjQ0MTA4NzE1QHFxLmNvbTAe
|
||||
Fw0yNTA0MjEwMzUyNTlaFw0yNjA0MjEwMzUyNTlaMIGJMQswCQYDVQQGEwJDTjEQ
|
||||
MA4GA1UECAwHQmVpamluZzEQMA4GA1UEBwwHQmVpamluZzEhMB8GA1UECgwYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlhbmxpY2hlbmcxHzAdBgkq
|
||||
hkiG9w0BCQEWEDI0NDEwODcxNUBxcS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||
DwAwggEKAoIBAQDdxxyYG0zV2KzeiVH5AGj1X1h7vLAHVS8FGkPL2QBBqW5/PrJ/
|
||||
z/sv3QJz6dB3ElOnk14GsY8lWk8uX/fjgNIPawN1G2/bCdMrlPOCEJEIOqZvzQWg
|
||||
PLRPGjoZx+wjCM/H7h47KVr4GNbo8MakLJeg6QB3rEEIiPrhQIoq9N7AhcjbEvWR
|
||||
NIxIQrpSqMCE2A5RoKUxAMum9rEoe/6PBw2GgEsQl5E6suRZw3wavg3aUU+6MMyx
|
||||
iiKEA6fCD5gDqgSo/xjgDUWJIOTTrmW6RBTcWP9iBUjWnCovQQ9zc6CzDXRgfsz9
|
||||
7uXQt9fuQEi++lcGnrsePjC99PnirCvmj/C/AgMBAAGjITAfMB0GA1UdDgQWBBT6
|
||||
n4jUTWQNcastC5jPHuU7CDzLFjANBgkqhkiG9w0BAQsFAAOCAQEAZJ9fY2Z+4vKr
|
||||
bBqwHmfBjEnGgS7L2mC7uS2x/x2meBRKlAlw5+nKdaUBccyI2baI3P1mh/iV72Wr
|
||||
OTcwUwSS6gIOB7JeWSB0UT7KbEOJ1tM/1HYs5F9tOT94P4Adm2gcKY81UlJMfSNQ
|
||||
WyWFLjWOk/5fUP42BmgbUIafTT9p+LeP6NOyEs6b4hGpF3q5L1QDwMUfASpOWtHn
|
||||
O7VyBvFyCGkVchnorWJ3ZXPaa7hy+2ULOK/d9zH3xxq4LKRclAS5XAMWHuw6/+tu
|
||||
CoxA+RPnaqHexPSzYlEElOT286FDyZHadjUDD0q/0Um92NUM3r+UrNEs/OHp7/nU
|
||||
M3BVM7H6LQ==
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,18 +0,0 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICzzCCAbcCAQAwgYkxCzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAw
|
||||
DgYDVQQHDAdCZWlqaW5nMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBM
|
||||
dGQxEjAQBgNVBAMMCWFubGljaGVuZzEfMB0GCSqGSIb3DQEJARYQMjQ0MTA4NzE1
|
||||
QHFxLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3HHJgbTNXY
|
||||
rN6JUfkAaPVfWHu8sAdVLwUaQ8vZAEGpbn8+sn/P+y/dAnPp0HcSU6eTXgaxjyVa
|
||||
Ty5f9+OA0g9rA3Ubb9sJ0yuU84IQkQg6pm/NBaA8tE8aOhnH7CMIz8fuHjspWvgY
|
||||
1ujwxqQsl6DpAHesQQiI+uFAiir03sCFyNsS9ZE0jEhCulKowITYDlGgpTEAy6b2
|
||||
sSh7/o8HDYaASxCXkTqy5FnDfBq+DdpRT7owzLGKIoQDp8IPmAOqBKj/GOANRYkg
|
||||
5NOuZbpEFNxY/2IFSNacKi9BD3NzoLMNdGB+zP3u5dC31+5ASL76Vwaeux4+ML30
|
||||
+eKsK+aP8L8CAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBHdTrwcS0Ip5XO/zJ/
|
||||
wFZvxe8wcdMBaHjSRx09Lgy3V1L9d0DPOfuUP4LP8UCxniucA2JLugJO1wr/5nZA
|
||||
FS9GcyCgbMKX/EskFkpuz72EA11WhyqFp/9nWZsxZB0t1cs3bUFeoFd2SE6QIo4N
|
||||
UY+guZCcF8hVwppceOxyaUQ9cudAClH9JSMR1XoIwvv3X7FJkPhM12DvcsLr062S
|
||||
fwG7/1h+VoSpAM/UYtBHBkU0MIVn1Gw6lJvkabUV0oYYBzl9ejsq1Qo2nUMdOEQY
|
||||
SS/VhVLONMyMCIERt3/GJTXdrJ12VqawJACFwv8g2i6NxbQvbZpF7TpS8amduO8+
|
||||
aRE6
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdxxyYG0zV2Kze
|
||||
iVH5AGj1X1h7vLAHVS8FGkPL2QBBqW5/PrJ/z/sv3QJz6dB3ElOnk14GsY8lWk8u
|
||||
X/fjgNIPawN1G2/bCdMrlPOCEJEIOqZvzQWgPLRPGjoZx+wjCM/H7h47KVr4GNbo
|
||||
8MakLJeg6QB3rEEIiPrhQIoq9N7AhcjbEvWRNIxIQrpSqMCE2A5RoKUxAMum9rEo
|
||||
e/6PBw2GgEsQl5E6suRZw3wavg3aUU+6MMyxiiKEA6fCD5gDqgSo/xjgDUWJIOTT
|
||||
rmW6RBTcWP9iBUjWnCovQQ9zc6CzDXRgfsz97uXQt9fuQEi++lcGnrsePjC99Pni
|
||||
rCvmj/C/AgMBAAECggEAM4j6RwpI/4RbH1cvmjoTKbmfORmumfWceIYS7QKfAaMa
|
||||
jy0Fk5fD2ep0kHTrwU+b6tvexJVsGxTyQ2d/lfkwVu7aHdNjWbXdwUnakAXDffML
|
||||
C/3LaeHRUHRavfTsFXQNvHrDwaGphu9WuUiCEFJgZb7fIfAAKLiT+9XghXzjaj79
|
||||
nGANBB++kfBGacfj2NMedCxsbZ++pD7DbTG4utnHmzgwB3WEEm2kIbt0XpuDSZqe
|
||||
/tCA1gCOADjV17C31booT6xdoj+s/Se0YxY91JaZFkI5wTSDIhB9scZzh8piy8YQ
|
||||
QaEaRUUYNbzdI1MyvgewT6wQLONNlytOOc9hYnPgAQKBgQDyzaublY2q+yzqCQ68
|
||||
JSKNTCSFRwumJ1kmdfz/0QwzS363l7LFnfIb55h27q3vuiagPfRbi0GK/tVXhOe+
|
||||
8mntNMoJJQJzowpy4ANqKfGndojXejE1s1ExpuGyqWV4MyFGiwAnnzyWRENw4laS
|
||||
roY0OcynNuq++isgrVMPlDLAvwKBgQDp1OTh2wcSfhsaVbMjNVCOGD82OFK057JB
|
||||
3C62Li2lCybFwcooypCyZthUdskY2Gf/mDdnpLSLvXr/7bO3IyLopA3WpnYqMBt+
|
||||
7sP/VQJMoyobLFMnJh+N+CxwkhRNAib64054WKwiJcccJ5fplUvNjOLBgGx3a/3N
|
||||
OIxx+L7QAQKBgQC+EK3zPuEFJVYFZk24jkE75oz4H6NIz6iD6PzBrH0mckShpwh0
|
||||
la1+lo7NGw3hiRDPg3ATcTE/gziyKAHZgZ3V5+r3uZbvuoNlZWKG6oqWkr2QH8EB
|
||||
znsSqRYoa15Y931m4Uyft5EP+CPq6+LlM+UuYMiJZP3hvaehDszkdxg7tQKBgGjv
|
||||
vMPLCpJ2+2zHFvxu+ba7FOsdPain7ix2RpRFhwBxT7Yh8Lp7pZIaa20EXd0DiTCA
|
||||
PCUMGmY345IlN6ixYQIsVXWGALOQIVbGijj1CnIgK05EhxCjoDeTL0ZZmDizBZFE
|
||||
HzwM9zrf30o4TolqEbmuRzj1jDfPw/esMAMZ0XABAoGBANpjvUN0Qvy9WmLyPe6U
|
||||
DBzIAUyyIL3mnCuHLCQ+dE6nBf5l20sVjTdIl/eLlvmD7dwT/D9Zsg32/c2NBCdY
|
||||
DltKKZl2u9nuSJ1g8DWTZUQHkYSTeKPOoAl/5mbCgyTxzmmmtZF6MySKuif0rzwS
|
||||
X2Rwou9zTpwgsEEmQ/RH4G94
|
||||
-----END PRIVATE KEY-----
|
||||
@ -1,229 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
//package message;
|
||||
|
||||
// 基础公共类型定义
|
||||
|
||||
message SDLV4Info {
|
||||
uint32 port = 1;
|
||||
bytes v4 = 2;
|
||||
uint32 nat_type = 3;
|
||||
}
|
||||
|
||||
message SDLV6Info {
|
||||
uint32 port = 1;
|
||||
bytes v6 = 2;
|
||||
}
|
||||
|
||||
// ipv6 assist相关
|
||||
message SDLV6AssistProbe {
|
||||
uint32 pkt_id = 1;
|
||||
}
|
||||
|
||||
message SDLV6AssistProbeReply {
|
||||
uint32 pkt_id = 1;
|
||||
SDLV6Info v6_info = 2;
|
||||
}
|
||||
|
||||
// 和super之间采用了quic协议通讯
|
||||
// 传输层采用: <<Len:16, PacketType:8, Payload/binary>>
|
||||
|
||||
message SDLWelcome {
|
||||
uint32 version = 1;
|
||||
// 服务器允许的最大双向流
|
||||
uint32 max_bidi_streams = 2;
|
||||
// 服务器允许的最大包
|
||||
uint32 max_packet_size = 3;
|
||||
// 心跳包的间隔
|
||||
uint32 heartbeat_sec = 4;
|
||||
// ipv6辅助器地址
|
||||
SDLV6Info ipv6_assist = 5;
|
||||
}
|
||||
|
||||
// 这里修改成了扁平的结构, 否则有些字段不好找放的位置
|
||||
message SDLRegisterSuper {
|
||||
string client_id = 1;
|
||||
// 网络地址信息已经有https请求分配了
|
||||
// 注册的时候需要带上(network_id, mac, ip, mask_len, hostname)
|
||||
uint32 network_id = 2;
|
||||
bytes mac = 3;
|
||||
uint32 ip = 4;
|
||||
uint32 mask_len = 5;
|
||||
string hostname = 6;
|
||||
|
||||
string pub_key = 7;
|
||||
// 客户端使用http协议请求后端,通过token或者账号密码登录时, 统一返回一个access_token;
|
||||
// RegisterSuper的时候,验证凭证是否合法 (access_token)
|
||||
string access_token = 8;
|
||||
}
|
||||
|
||||
// 客户端的升级逻辑,在https的接口里面去完成
|
||||
// 部分逻辑会脱离quic去通讯,增加session_token校验
|
||||
message SDLRegisterSuperAck {
|
||||
// 目前支持aes, chacha20
|
||||
string algorithm = 1;
|
||||
bytes key = 2;
|
||||
// 逻辑分段,chacha20加密算法需要使用该字段
|
||||
uint32 region_id = 3;
|
||||
bytes session_token = 4;
|
||||
}
|
||||
|
||||
message SDLRegisterSuperNak {
|
||||
uint32 error_code = 1;
|
||||
string error_message = 2;
|
||||
}
|
||||
|
||||
// 网络地址查询
|
||||
message SDLQueryInfo {
|
||||
bytes dst_mac = 1;
|
||||
}
|
||||
|
||||
message SDLPeerInfo {
|
||||
bytes dst_mac = 1;
|
||||
optional SDLV4Info v4_info = 2;
|
||||
optional SDLV6Info v6_info = 3;
|
||||
}
|
||||
|
||||
// ARP查询相关
|
||||
// 真实的arp请求是通过广播的形式获取到的,但是针对于macos这种tun的实现;是能够分析出arp请求包的;
|
||||
// 对于当前网络来说,服务端是知道mac对应的ip地址的,因此没有必要广播;直接通过服务器端返回
|
||||
message SDLArpRequest {
|
||||
uint32 target_ip = 1;
|
||||
uint32 origin_ip = 2;
|
||||
bytes context = 3;
|
||||
}
|
||||
|
||||
message SDLArpResponse {
|
||||
uint32 target_ip = 1;
|
||||
bytes target_mac = 2;
|
||||
uint32 origin_ip = 3;
|
||||
bytes context = 4;
|
||||
}
|
||||
|
||||
// 权限请求查询相关
|
||||
message SDLPolicyRequest {
|
||||
uint32 src_identity_id = 1;
|
||||
uint32 dst_identity_id = 2;
|
||||
uint32 version = 3;
|
||||
}
|
||||
|
||||
// 基于quic通讯,rules部分已经没有了长度限制
|
||||
message SDLPolicyResponse {
|
||||
uint32 src_identity_id = 1;
|
||||
uint32 dst_identity_id = 2;
|
||||
// 版本号,客户端需要比较版本号确定是否覆盖; 请求端自己去管理版本号,服务端只是原样回写
|
||||
uint32 version = 3;
|
||||
// 1 + 2稀疏序列化规则, 按照: <<Proto:8, Port:16>> 这个格式序列号所有的规则信息; 下发的数据默认都是allow,deny规则的服务器端已经屏蔽
|
||||
bytes rules = 4;
|
||||
}
|
||||
|
||||
// 事件定义
|
||||
|
||||
message SDLEvent {
|
||||
// nat映射变化
|
||||
message NatChanged {
|
||||
bytes mac = 1;
|
||||
uint32 ip = 2;
|
||||
}
|
||||
|
||||
// 发送register消息
|
||||
message SendRegister {
|
||||
bytes dst_mac = 1;
|
||||
uint32 nat_ip = 2;
|
||||
uint32 nat_port = 3;
|
||||
uint32 nat_type = 4;
|
||||
optional SDLV6Info v6_info = 5;
|
||||
}
|
||||
|
||||
// 网络关闭
|
||||
message NetworkShutdown {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
oneof event {
|
||||
NatChanged nat_changed = 1;
|
||||
SendRegister send_register = 2;
|
||||
NetworkShutdown shutdown = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Command指令
|
||||
message SDLCommand {
|
||||
uint32 pkt_id = 1;
|
||||
// 出口节点控制
|
||||
message ExitNodeControl {
|
||||
int32 action = 1; // 必选:操作类型
|
||||
string remark = 2; // 可选:备注(方便日志/调试)
|
||||
}
|
||||
|
||||
oneof command {
|
||||
ExitNodeControl exit_node = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message SDLCommandAck {
|
||||
uint32 pkt_id = 1;
|
||||
int32 code = 2;
|
||||
string message = 3;
|
||||
bytes data = 4;
|
||||
}
|
||||
|
||||
// UDP通讯消息
|
||||
|
||||
// client和stun之间的心跳包,客户端需要和super的udp之间的存活逻辑
|
||||
message SDLStunRequest {
|
||||
string client_id = 1;
|
||||
uint32 network_id = 2;
|
||||
bytes mac = 3;
|
||||
uint32 ip = 4;
|
||||
uint32 nat_type = 5;
|
||||
optional SDLV6Info v6_info = 6;
|
||||
bytes session_token = 7;
|
||||
}
|
||||
|
||||
message SDLStunReply {
|
||||
}
|
||||
|
||||
message SDLData {
|
||||
uint32 network_id = 1;
|
||||
bytes src_mac = 2;
|
||||
bytes dst_mac = 3;
|
||||
bool is_p2p = 4;
|
||||
uint32 ttl = 5;
|
||||
bytes data = 6;
|
||||
bytes session_token = 7;
|
||||
// 端通过https登录的时候,服务端会分配该端对应的权限标识
|
||||
// 后续的请求过程中需要带上这个值,对端通过这个值要判断对数据包是否放行
|
||||
uint32 identity_id = 8;
|
||||
}
|
||||
|
||||
// 网络类型探测
|
||||
|
||||
message SDLStunProbe {
|
||||
uint32 cookie = 1;
|
||||
uint32 attr = 2;
|
||||
// 增加step是为了方便端上判断,收到的请求和响应之间的映射关系;服务器端原样返回
|
||||
uint32 step = 3;
|
||||
}
|
||||
|
||||
message SDLStunProbeReply {
|
||||
uint32 cookie = 1;
|
||||
// 增加step是为了方便端上判断,收到的请求和响应之间的映射关系;服务器端原样返回
|
||||
uint32 step = 2;
|
||||
uint32 port = 3;
|
||||
uint32 ip = 4;
|
||||
}
|
||||
|
||||
// Node-Node之间的握手逻辑, 是基于udp传输的
|
||||
|
||||
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;
|
||||
}
|
||||
69
rebar.config
69
rebar.config
@ -2,51 +2,48 @@
|
||||
debug_info
|
||||
]}.
|
||||
|
||||
{plugins, [
|
||||
{rebar3_gpb_plugin, ".*", {git, "https://github.com/lrascao/rebar3_gpb_plugin.git", {tag, "2.23.8"}}}
|
||||
]}.
|
||||
|
||||
% ======================
|
||||
% 核心:强制指定 proto 路径 + 输出到你的 apps/sdlan
|
||||
% ======================
|
||||
{gpb_opts, [
|
||||
{i, "proto"}, % proto 文件路径
|
||||
{src_dirs, ["proto"]}, % 源码目录(必须)
|
||||
recursive, % 递归查找 proto 文件
|
||||
{module_name_suffix, "_pb"}, % 生成模块后缀
|
||||
{o_erl, "src"}, % .erl 输出目录
|
||||
{o_hrl, "include"}, % .hrl 输出目录
|
||||
include_as_lib, % gpb.hrl 通过 -include_lib("gpb/include/gpb.hrl")
|
||||
{strings_as_binaries, true}, % proto string → Erlang binary
|
||||
type_specs, % 生成 type specs
|
||||
report, % 编译报告
|
||||
verbose % 打印详细信息
|
||||
]}.
|
||||
|
||||
{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"}}},
|
||||
{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"}}},
|
||||
{dns_erlang, ".*", {git, "https://github.com/dnsimple/dns_erlang.git", {tag, "v4.4.0"}}},
|
||||
{quicer, ".*", {git, "https://github.com/emqx/quic.git", {tag, "0.4.0"}}},
|
||||
{pkt, ".*", {git, "https://github.com/msantos/pkt.git", {tag, "0.6.0"}}},
|
||||
{sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}}
|
||||
]}.
|
||||
|
||||
{provider_hooks, [
|
||||
{pre, [
|
||||
{compile, {protobuf, compile}},
|
||||
{clean, {protobuf, clean}}
|
||||
]}
|
||||
{sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}},
|
||||
{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},
|
||||
{sys_config, "./config/sys.config"},
|
||||
{vm_args, "./config/vm.args"}]}.
|
||||
[sdlan,
|
||||
sasl]},
|
||||
|
||||
{profiles, [{prod, [{relx, [{mode, prod}]}]}]}.
|
||||
{rebar_packages_cdn, "https://hexpm.upyun.com"}.
|
||||
{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}
|
||||
]
|
||||
}]}]}.
|
||||
{rebar_packages_cdn, "https://hexpm.upyun.com"}.
|
||||
37
rebar.lock
37
rebar.lock
@ -1,5 +1,5 @@
|
||||
{"1.2.0",
|
||||
[{<<"base32">>,{pkg,<<"base32">>,<<"1.0.0">>},1},
|
||||
[{<<"base32">>,{pkg,<<"base32">>,<<"1.0.0">>},2},
|
||||
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.2">>},1},
|
||||
{<<"cowboy">>,
|
||||
{git,"https://github.com/ninenines/cowboy.git",
|
||||
@ -12,37 +12,52 @@
|
||||
{<<"dns_erlang">>,
|
||||
{git,"https://github.com/dnsimple/dns_erlang.git",
|
||||
{ref,"e1149a2dd6f49c6560aa245bc6f3d40a5cbe70e1"}},
|
||||
1},
|
||||
{<<"dns_proxy">>,
|
||||
{git,"https://gitea.s5s8.com/anlicheng/dns_proxy.git",
|
||||
{ref,"5ed832359e42ec99d148bf2ada1729b540017ab8"}},
|
||||
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">>,{pkg,<<"parse_trans">>,<<"3.3.0">>},1},
|
||||
{<<"parse_trans">>,
|
||||
{git,"https://github.com/uwiger/parse_trans",
|
||||
{ref,"6f3645afb43c7c57d61b54ef59aecab288ce1013"}},
|
||||
0},
|
||||
{<<"pkt">>,
|
||||
{git,"https://github.com/msantos/pkt.git",
|
||||
{ref,"67a4a14f596fded5ad5f2d8f94318faa8ad2c288"}},
|
||||
0},
|
||||
1},
|
||||
{<<"poolboy">>,
|
||||
{git,"https://github.com/devinus/poolboy.git",
|
||||
{ref,"3bb48a893ff5598f7c73731ac17545206d259fac"}},
|
||||
0},
|
||||
{<<"quicer">>,
|
||||
{git,"https://github.com/emqx/quic.git",
|
||||
{ref,"c2962fd732caa29d513de7c645ea307cf89aaa9c"}},
|
||||
0},
|
||||
{<<"ranch">>,
|
||||
{git,"https://github.com/ninenines/ranch",
|
||||
{ref,"a692f44567034dacf5efcaa24a24183788594eb7"}},
|
||||
1},
|
||||
{<<"snabbkaffe">>,{pkg,<<"snabbkaffe">>,<<"1.0.10">>},1},
|
||||
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},1},
|
||||
{<<"sync">>,
|
||||
{git,"https://github.com/rustyio/sync.git",
|
||||
@ -58,22 +73,20 @@
|
||||
{<<"base32">>, <<"1AB331F812FCC254C8F7D4348E1E5A6F2B9B32B7A260BF2BC3358E3BF14C841A">>},
|
||||
{<<"certifi">>, <<"B7CFEAE9D2ED395695DD8201C57A2D019C0C43ECAF8B8BCB9320B40D6662F340">>},
|
||||
{<<"fs">>, <<"9D147B944D60CFA48A349F12D06C8EE71128F610C90870BDF9A6773206452ED0">>},
|
||||
{<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>},
|
||||
{<<"idna">>, <<"1D038FB2E7668CE41FBF681D2C45902E52B3CB9E9C77B55334353B222C2EE50C">>},
|
||||
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
|
||||
{<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>},
|
||||
{<<"parse_trans">>, <<"09765507A3C7590A784615CFD421D101AEC25098D50B89D7AA1D66646BC571C1">>},
|
||||
{<<"snabbkaffe">>, <<"9BE2F54F61FC6862391B666B2B5B76C3FA53598E2989A17CEF1B48CF347A8A63">>},
|
||||
{<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>},
|
||||
{<<"unicode_util_compat">>, <<"8516502659002CEC19E244EBD90D312183064BE95025A319A6C7E89F4BCCD65B">>}]},
|
||||
{pkg_hash_ext,[
|
||||
{<<"base32">>, <<"0449285348ED0C4CD83C7198E76C5FD5A0451C4EF18695B9FD43792A503E551A">>},
|
||||
{<<"certifi">>, <<"3B3B5F36493004AC3455966991EAF6E768CE9884693D9968055AEEEB1E575040">>},
|
||||
{<<"fs">>, <<"EF94E95FFE79916860649FED80AC62B04C322B0BB70F5128144C026B4D171F8B">>},
|
||||
{<<"goldrush">>, <<"99CB4128CFFCB3227581E5D4D803D5413FA643F4EB96523F77D9E6937D994CEB">>},
|
||||
{<<"idna">>, <<"A02C8A1C4FD601215BB0B0324C8A6986749F807CE35F25449EC9E69758708122">>},
|
||||
{<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
|
||||
{<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>},
|
||||
{<<"parse_trans">>, <<"17EF63ABDE837AD30680EA7F857DD9E7CED9476CDD7B0394432AF4BFC241B960">>},
|
||||
{<<"snabbkaffe">>, <<"70A98DF36AE756908D55B5770891D443D63C903833E3E87D544036E13D4FAC26">>},
|
||||
{<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>},
|
||||
{<<"unicode_util_compat">>, <<"D48D002E15F5CC105A696CF2F1BBB3FC72B4B770A184D8420C8DB20DA2674B38">>}]}
|
||||
].
|
||||
|
||||
@ -1,147 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 03. 12月 2025 23:00
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(dns_resolver).
|
||||
-author("anlicheng").
|
||||
|
||||
-include_lib("dns_erlang/include/dns.hrl").
|
||||
-include_lib("pkt/include/pkt.hrl").
|
||||
-include("dns_proxy.hrl").
|
||||
|
||||
-export([resolve/1]).
|
||||
|
||||
%% 协议部分
|
||||
-define(TCP_PROTOCOL, 6).
|
||||
-define(UDP_PROTOCOL, 17).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec resolve(IpPacket :: binary()) -> {ok, RespIpPacket :: binary()} | {error, Reason :: term()}.
|
||||
resolve(IpPacket) when is_binary(IpPacket) ->
|
||||
try
|
||||
{#ipv4{saddr = ReqSAddr, daddr = ReqDAddr, p = Protocol}, ReqIpPayload} = pkt:ipv4(IpPacket),
|
||||
case Protocol of
|
||||
?UDP_PROTOCOL ->
|
||||
{#udp{sport = ReqSPort, dport = ReqDPort}, UdpPayload} = pkt:udp(ReqIpPayload),
|
||||
DnsQueryMessage = dns:decode_message(UdpPayload),
|
||||
case resolve0(DnsQueryMessage) of
|
||||
{ok, DnsResp} ->
|
||||
RespIpPacket = build_ip_packet(ReqDAddr, ReqSAddr, ReqDPort, ReqSPort, DnsResp),
|
||||
{ok, RespIpPacket};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
_ ->
|
||||
{error, invalid_packet}
|
||||
end
|
||||
catch error:_ ->
|
||||
{error, invalid_packet}
|
||||
end.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec resolve0(Packet :: #dns_message{}) -> {ok, Resp :: binary()} | {error, Reason :: any()}.
|
||||
resolve0(QueryMsg = #dns_message{qc = 1, questions = [#dns_query{name = QName, type = QType, class = QClass}|_]}) ->
|
||||
%% 查找是否是内置的域名
|
||||
case sdlan_hostname_regedit:lookup(QName) of
|
||||
{ok, Ip} when QType =:= ?DNS_TYPE_A, QClass =:= ?DNS_CLASS_IN ->
|
||||
Answer = #dns_rr {
|
||||
name = QName,
|
||||
type = QType,
|
||||
class = QClass,
|
||||
ttl = 300,
|
||||
data = #dns_rrdata_a {
|
||||
ip = Ip
|
||||
}
|
||||
},
|
||||
RespMsg = QueryMsg#dns_message{
|
||||
qr = true,
|
||||
ra = true,
|
||||
anc = 1,
|
||||
auc = 0,
|
||||
adc = 0,
|
||||
answers = [Answer],
|
||||
authority = [],
|
||||
additional = []
|
||||
},
|
||||
logger:debug("[dns_resolver] punchnet inbuilt qnanme: ~p, ip: ~p", [QName, Ip]),
|
||||
{ok, dns:encode_message(RespMsg)};
|
||||
{ok, _Ip} ->
|
||||
{ok, dns:encode_message(build_empty_response(QueryMsg))};
|
||||
error ->
|
||||
case sdlan_domain_regedit:maybe_domain(QName) of
|
||||
true ->
|
||||
EmptyDnsResp = dns:encode_message(build_nxdomain_response(QueryMsg)),
|
||||
{ok, EmptyDnsResp};
|
||||
false ->
|
||||
{error, not_supported}
|
||||
end
|
||||
end;
|
||||
resolve0(Error) ->
|
||||
{error, Error}.
|
||||
|
||||
-spec build_empty_response(QueryMsg :: #dns_message{}) -> EmptyResp :: #dns_message{}.
|
||||
build_empty_response(QueryMsg) ->
|
||||
QueryMsg#dns_message{
|
||||
qr = true,
|
||||
aa = true,
|
||||
ra = true,
|
||||
rc = ?DNS_RCODE_NOERROR,
|
||||
anc = 0,
|
||||
auc = 0,
|
||||
adc = 0,
|
||||
answers = [],
|
||||
authority = [],
|
||||
additional = []
|
||||
}.
|
||||
|
||||
-spec build_nxdomain_response(QueryMsg :: #dns_message{}) -> EmptyResp :: #dns_message{}.
|
||||
build_nxdomain_response(QueryMsg) ->
|
||||
QueryMsg#dns_message{
|
||||
qr = true,
|
||||
aa = true,
|
||||
ra = true,
|
||||
rc = ?DNS_RCODE_NXDOMAIN,
|
||||
anc = 0,
|
||||
auc = 0,
|
||||
adc = 0,
|
||||
answers = [],
|
||||
authority = [],
|
||||
additional = []
|
||||
}.
|
||||
|
||||
-spec build_ip_packet(SAddr :: inet:ip4_address(), DAddr :: inet:ip4_address(), SPort :: integer(), DPort :: integer(), Payload :: binary()) -> IpPacket :: binary().
|
||||
build_ip_packet(SAddr, DAddr, SPort, DPort, UdpPayload) when is_integer(SPort), is_integer(DPort), is_binary(UdpPayload) ->
|
||||
ULen = 8 + byte_size(UdpPayload),
|
||||
RespUdpHeader = pkt:udp(#udp{
|
||||
sport = SPort,
|
||||
dport = DPort,
|
||||
ulen = ULen,
|
||||
sum = dns_utils:udp_checksum(SAddr, DAddr, SPort, DPort, UdpPayload)
|
||||
}),
|
||||
IpPayload = <<RespUdpHeader/binary, UdpPayload/binary>>,
|
||||
|
||||
IpPacket0 = #ipv4{
|
||||
len = 20 + ULen,
|
||||
ttl = 64,
|
||||
off = 0,
|
||||
mf = 0,
|
||||
sum = 0,
|
||||
p = ?UDP_PROTOCOL,
|
||||
saddr = SAddr,
|
||||
daddr = DAddr,
|
||||
opt = <<>>
|
||||
},
|
||||
IpCheckSum = dns_utils:ip_checksum(IpPacket0),
|
||||
IpHeader = pkt:ipv4(IpPacket0#ipv4{sum = IpCheckSum}),
|
||||
|
||||
<<IpHeader/binary, IpPayload/binary>>.
|
||||
@ -1,36 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
%% 重新加载对应的主机信息
|
||||
-spec handle_request(Method :: string(), Path :: string(), GetParams :: map(), PostParams :: map()) ->
|
||||
{ok, StatusCode :: non_neg_integer(), Body :: iodata()}.
|
||||
handle_request("POST", "/test/auth_token", _, PostParams) ->
|
||||
logger: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
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
@ -1,32 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 26. 4月 2020 3:36 下午
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(binlog_handler).
|
||||
-author("licheng5").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
%% 重新加载对应的主机信息
|
||||
-spec handle_request(Method :: string(), Path :: string(), GetParams :: map(), PostParams :: map()) ->
|
||||
{ok, StatusCode :: non_neg_integer(), Body :: iodata()}.
|
||||
handle_request("POST", "/binlog", _, PostParams) ->
|
||||
logger:debug("[binlog_handler] get post params: ~p", [PostParams]),
|
||||
{ok, 200, sdlan_util:json_data(<<"ok">>)};
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
@ -1,72 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 09. 4月 2024 14:28
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(network_handler).
|
||||
-author("anlicheng").
|
||||
-include_lib("sdlan_pb.hrl").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
-spec handle_request(Method :: string(), Path :: string(), GetParams :: map(), PostParams :: map()) ->
|
||||
{ok, StatusCode :: non_neg_integer(), Body :: iodata()}.
|
||||
handle_request("POST", "/network/create", _, #{<<"id">> := NetworkId}) when NetworkId > 0 ->
|
||||
case sdlan_network_sup:ensure_network_started(NetworkId) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, 200, sdlan_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
logger:debug("[network_handler] create network: ~p, get error: ~p", [NetworkId, Reason]),
|
||||
ReasonBin = sdlan_util:term_to_binary(Reason),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"error: ", ReasonBin/binary>>)}
|
||||
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} ->
|
||||
logger:debug("[network_handler] delete network: ~p, get error: ~p", [NetworkId, Reason]),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"error">>)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request("POST", "/network/exit_node_control", _, #{<<"id">> := NetworkId, <<"action">> := Action, <<"client_id">> := ClientId, <<"remark">> := Remark, <<"timeout">> := Timeout}) when NetworkId > 0 ->
|
||||
case sdlan_network:get_pid(NetworkId) of
|
||||
undefined ->
|
||||
{ok, 200, sdlan_util:json_data(<<"network not found">>)};
|
||||
NetworkPid when is_pid(NetworkPid) ->
|
||||
ReceiverPid = self(),
|
||||
SubCommand = {exit_node, #'SDLCommand.ExitNodeControl'{
|
||||
action = Action,
|
||||
remark = Remark
|
||||
}},
|
||||
case sdlan_network:command(NetworkPid, ReceiverPid, ClientId, SubCommand) of
|
||||
{error, Reason} ->
|
||||
{ok, 200, sdlan_util:json_error(-1, Reason)};
|
||||
{ok, Ref} ->
|
||||
case sdlan_network:wait_command_ack(Ref, Timeout * 1000) of
|
||||
{ok, #'SDLCommandAck'{code = Code, message = Message}} ->
|
||||
case Code == 0 of
|
||||
true ->
|
||||
{ok, 200, sdlan_util:json_data(<<"success">>)};
|
||||
false ->
|
||||
{ok, 200, sdlan_util:json_error(Code, Message)}
|
||||
end;
|
||||
{error, timeout} ->
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"任务执行超时"/utf8>>)}
|
||||
end
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, sdlan_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
@ -1,51 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
%% 重新加载对应的主机信息
|
||||
-spec handle_request(Method :: string(), Path :: string(), GetParams :: map(), PostParams :: map()) ->
|
||||
{ok, StatusCode :: non_neg_integer(), Body :: iodata()}.
|
||||
handle_request("POST", "/test/auth_access_token", _, _PostParams) ->
|
||||
{ok, 200, sdlan_util:json_data(<<"ok">>)};
|
||||
|
||||
handle_request("GET", "/test/get_all_networks", _, _) ->
|
||||
{ok, 200, sdlan_util:json_data([8])};
|
||||
|
||||
handle_request("GET", "/test/get_network", #{<<"id">> := Id0}, _) ->
|
||||
Id = binary_to_integer(Id0),
|
||||
Networks = #{
|
||||
8 => #{
|
||||
<<"id">> => 8,
|
||||
<<"name">> => <<"test1">>,
|
||||
<<"domain">> => <<"punchnet.cn">>,
|
||||
<<"ipaddr">> => <<"10.211.179.0/24">>,
|
||||
<<"owner_id">> => 1234,
|
||||
<<"algorithm">> => <<"chacha20">>,
|
||||
<<"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
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
@ -1,120 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc IPv6 UDP listener for connectivity checks.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ipv6_assist_server).
|
||||
|
||||
-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]).
|
||||
|
||||
-record(state, {
|
||||
socket
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec get_name(Id :: integer()) -> atom().
|
||||
get_name(Id) when is_integer(Id) ->
|
||||
list_to_atom("ipv6_assistor_server:" ++ integer_to_list(Id)).
|
||||
|
||||
-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
|
||||
%%%===================================================================
|
||||
|
||||
-spec init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore.
|
||||
init([Port]) ->
|
||||
erlang:process_flag(priority, high),
|
||||
Opts = [
|
||||
binary,
|
||||
inet6,
|
||||
{reuseaddr, true},
|
||||
{reuseport, true},
|
||||
{active, true},
|
||||
{recbuf, 5 * 1024 * 1024},
|
||||
{sndbuf, 5 * 1024 * 1024}
|
||||
],
|
||||
{ok, Socket} = gen_udp:open(Port, Opts),
|
||||
inet_udp:controlling_process(Socket, self()),
|
||||
|
||||
logger:debug("[ipv6_assistor_server] start at port: ~p", [Port]),
|
||||
{ok, #state{socket = Socket}}.
|
||||
|
||||
-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}.
|
||||
|
||||
-spec handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}.
|
||||
handle_cast(_Request, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
-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, Body}, State = #state{socket = Sock}) ->
|
||||
maybe
|
||||
ThrottleKey = {Ip, Port},
|
||||
ok ?= limit_check(ThrottleKey),
|
||||
#'SDLV6AssistProbe'{pkt_id = PktId} ?= catch sdlan_pb:decode_msg(Body, 'SDLV6AssistProbe'),
|
||||
V6Bytes = sdlan_util:ipv6_to_bytes(Ip),
|
||||
ReplyBin = sdlan_pb:encode_msg(#'SDLV6AssistProbeReply'{
|
||||
pkt_id = PktId,
|
||||
v6_info = #'SDLV6Info'{
|
||||
v6 = V6Bytes,
|
||||
port = Port
|
||||
}
|
||||
}),
|
||||
ok ?= gen_udp:send(Sock, Ip, Port, ReplyBin)
|
||||
end,
|
||||
{noreply, State};
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
-spec terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term().
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
ok.
|
||||
|
||||
-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}.
|
||||
|
||||
%% 访问频率限制
|
||||
-spec limit_check(ThrottleKey :: any()) -> ok | limited.
|
||||
limit_check(ThrottleKey) ->
|
||||
case throttle:check(sdlan_ipv6_assist, ThrottleKey) of
|
||||
{ok, _RestCount, _LeftToReset} ->
|
||||
ok;
|
||||
{limit_exceeded, 0, _LeftToReset} ->
|
||||
limited
|
||||
end.
|
||||
@ -1,39 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%% @doc ipv6_assist top level supervisor.
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(ipv6_assist_server_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]).
|
||||
-export([init/1]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-spec start_link() -> supervisor:startlink_ret().
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
-spec init(Args :: term()) ->
|
||||
{ok, {SupFlags :: supervisor:sup_flags(), [ChildSpec :: supervisor:child_spec()]}}
|
||||
| ignore.
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||
{ok, Props} = application:get_env(sdlan, ipv6_assist),
|
||||
Port = proplists:get_value(port, Props, 1367),
|
||||
AcceptorNum = proplists:get_value(acceptor_nums, Props, 5),
|
||||
|
||||
Specs = lists:map(fun(Id) ->
|
||||
Name = ipv6_assist_server:get_name(Id),
|
||||
#{
|
||||
id => Name,
|
||||
start => {ipv6_assist_server, start_link, [Name, Port]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['ipv6_assist_server']
|
||||
}
|
||||
end, lists:seq(1, AcceptorNum)),
|
||||
{ok, {SupFlags, Specs}}.
|
||||
@ -1,29 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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]).
|
||||
@ -1,48 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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).
|
||||
@ -1,144 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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) ->
|
||||
logger: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 ->
|
||||
logger: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) ->
|
||||
logger: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 ->
|
||||
logger: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) ->
|
||||
logger: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} ->
|
||||
logger: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) ->
|
||||
logger: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} ->
|
||||
logger: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, ")">>,
|
||||
logger: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) ->
|
||||
logger: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) ->
|
||||
logger: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>>,
|
||||
logger: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 ->
|
||||
logger: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)}.
|
||||
@ -1,33 +0,0 @@
|
||||
-module(identity_policy_ets).
|
||||
-include("policy.hrl").
|
||||
|
||||
-export([init/0]).
|
||||
-export([get_policies/1, insert/1, delete/1, update/2]).
|
||||
|
||||
-spec init() -> ets:tab().
|
||||
init() ->
|
||||
ets:new(identity_policy, [named_table, bag, public, {keypos, 2}, {read_concurrency, true}]).
|
||||
|
||||
-spec get_policies(IdentityId :: integer()) -> [PolicyId :: integer()].
|
||||
get_policies(IdentityId) when is_integer(IdentityId) ->
|
||||
Records = ets:lookup(identity_policy, IdentityId),
|
||||
lists:map(fun(#identity_policy{policy_id = PolicyId}) -> PolicyId end, Records).
|
||||
|
||||
-spec insert(map() | #identity_policy{}) -> true.
|
||||
insert(#{<<"identity_id">> := IdentityId, <<"policy_id">> := PolicyId}) ->
|
||||
insert(#identity_policy{identity_id = IdentityId, policy_id = PolicyId});
|
||||
insert(IdentityPolicy=#identity_policy{}) ->
|
||||
true = ets:insert(identity_policy, IdentityPolicy).
|
||||
|
||||
-spec delete(map() | #identity_policy{}) -> true.
|
||||
delete(#{<<"identity_id">> := IdentityId, <<"policy_id">> := PolicyId}) ->
|
||||
ets:delete_object(identity_policy, #identity_policy{identity_id = IdentityId, policy_id = PolicyId});
|
||||
delete(IdentityPolicy = #identity_policy{}) ->
|
||||
true = ets:delete_object(identity_policy, IdentityPolicy).
|
||||
|
||||
-spec update(NewData :: map(), OldData :: map()) -> true.
|
||||
update(NewData=#{<<"identity_id">> := IdentityId, <<"policy_id">> := PolicyId}, OldData) ->
|
||||
%% 清理老的数据
|
||||
#{<<"identity_id">> := OldIdentityId, <<"policy_id">> := OldPolicyId} = maps:merge(NewData, OldData),
|
||||
ets:delete_object(identity_policy, #identity_policy{identity_id = OldIdentityId, policy_id = OldPolicyId}),
|
||||
ets:insert(identity_policy, #identity_policy{identity_id = IdentityId, policy_id = PolicyId}).
|
||||
@ -1,143 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 28. 2月 2026 22:07
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(maxwell_redis_channel).
|
||||
-author("licheng5").
|
||||
-include("policy.hrl").
|
||||
|
||||
%% API
|
||||
-export([start/1, loop/1]).
|
||||
|
||||
-record(command, {
|
||||
data = <<>>,
|
||||
stage = parse_arg_num,
|
||||
arg_num = 0,
|
||||
args = []
|
||||
}).
|
||||
|
||||
-record(state, {
|
||||
socket,
|
||||
command
|
||||
}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% esockd callback
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec start(Socket :: inet:socket()) -> pid().
|
||||
start(Socket) ->
|
||||
spawn(?MODULE, loop, [#state{socket = Socket, command = #command{}}]).
|
||||
|
||||
-spec loop(State :: #state{}) -> no_return().
|
||||
loop(State=#state{socket = Socket, command = Command = #command{data = Data}}) ->
|
||||
inet:setopts(Socket, [{active, once}]),
|
||||
receive
|
||||
{tcp, _, Packet} ->
|
||||
NData = <<Data/binary, Packet/binary>>,
|
||||
case parse(Command#command{data = NData}) of
|
||||
{ok, #command{args = Args}} ->
|
||||
{reply, Reply} = handle_command(Args),
|
||||
gen_tcp:send(Socket, Reply),
|
||||
loop(State#state{command = #command{}});
|
||||
{more_data, NCommand} ->
|
||||
%% 请求的数据包过大,一次接受不完整
|
||||
loop(State#state{command = NCommand})
|
||||
end;
|
||||
{tcp_error, _} ->
|
||||
exit(normal);
|
||||
{tcp_closed, _} ->
|
||||
logger:debug("[maxwell_redis_channel] channel closed"),
|
||||
exit(normal)
|
||||
end.
|
||||
|
||||
%% PING命令
|
||||
handle_command([<<"PING">>]) ->
|
||||
{reply, encode({single_line, <<"PONG">>})};
|
||||
|
||||
handle_command([<<"PUBLISH">>, _Channel, Msg]) ->
|
||||
case catch json:decode(Msg) of
|
||||
M when is_map(M) ->
|
||||
handle_data(M);
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
{reply, encode(1)};
|
||||
|
||||
handle_command(_) ->
|
||||
{reply, encode({error, <<"Unsuported Command">>})}.
|
||||
|
||||
handle_data(#{<<"database">> := <<"punchnet_v2">>, <<"table">> := <<"identity_policy">>, <<"type">> := <<"insert">>, <<"data">> := Data}) ->
|
||||
identity_policy_ets:insert(Data);
|
||||
handle_data(#{<<"database">> := <<"punchnet_v2">>, <<"table">> := <<"identity_policy">>, <<"type">> := <<"delete">>, <<"data">> := Data}) ->
|
||||
identity_policy_ets:delete(Data);
|
||||
handle_data(#{<<"database">> := <<"punchnet_v2">>, <<"table">> := <<"identity_policy">>, <<"type">> := <<"update">>, <<"data">> := Data, <<"old">> := Old}) ->
|
||||
identity_policy_ets:update(Data, Old);
|
||||
%% 处理rule
|
||||
handle_data(#{<<"database">> := <<"punchnet_v2">>, <<"table">> := <<"rule">>, <<"type">> := <<"insert">>, <<"data">> := Data}) ->
|
||||
rule_ets:insert(Data);
|
||||
handle_data(#{<<"database">> := <<"punchnet_v2">>, <<"table">> := <<"rule">>, <<"type">> := <<"delete">>, <<"data">> := Data}) ->
|
||||
rule_ets:delete(Data);
|
||||
handle_data(#{<<"database">> := <<"punchnet_v2">>, <<"table">> := <<"rule">>, <<"type">> := <<"update">>, <<"data">> := Data, <<"old">> := Old}) ->
|
||||
rule_ets:update(Data, Old);
|
||||
handle_data(_Json) ->
|
||||
ok.
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
%% 解析请求的包, 支持请求不在一个包里面的情况, 基于状态机
|
||||
parse(Command = #command{stage = parse_arg_num, data = <<$*, Rest/binary>>}) ->
|
||||
case binary:split(Rest, <<$\r, $\n>>) of
|
||||
[ArgNum0, ArgBin] ->
|
||||
ArgNum = binary_to_integer(ArgNum0),
|
||||
parse(Command#command{arg_num = ArgNum, data = ArgBin, stage = parse_arg});
|
||||
_ ->
|
||||
{more_data, Command}
|
||||
end;
|
||||
%% 解析请求的参数
|
||||
parse(Command = #command{stage = parse_arg, args = Args, arg_num = 0, data = <<>>}) ->
|
||||
{ok, Command#command{args = lists:reverse(Args)}};
|
||||
parse(Command = #command{stage = parse_arg, args = Args, arg_num = ArgNum, data = ArgBin}) ->
|
||||
case binary:split(ArgBin, <<$\r, $\n>>) of
|
||||
[<<"$", ArgLen0/binary>>, RestArgBin] ->
|
||||
ArgLen = binary_to_integer(ArgLen0),
|
||||
case RestArgBin of
|
||||
<<Arg:ArgLen/binary, $\r, $\n, RestArgBin1/binary>> ->
|
||||
parse(Command#command{arg_num = ArgNum - 1, args = [Arg | Args], data = RestArgBin1});
|
||||
_ ->
|
||||
{more_data, Command}
|
||||
end;
|
||||
_ ->
|
||||
{more_data, Command}
|
||||
end.
|
||||
|
||||
%% redis数据返回格式化
|
||||
-spec encode(tuple() | binary() | integer() | list()) -> iolist().
|
||||
encode({single_line, Arg}) when is_binary(Arg) ->
|
||||
[<<$+>>, Arg, <<$\r, $\n>>];
|
||||
encode({error, Arg}) when is_binary(Arg) ->
|
||||
[<<$->>, Arg, <<$\r, $\n>>];
|
||||
encode(Arg) when is_integer(Arg) ->
|
||||
[<<$:>>, integer_to_list(Arg), <<$\r, $\n>>];
|
||||
encode(Arg) when is_binary(Arg) ->
|
||||
[<<$$>>, integer_to_list(iolist_size(Arg)), <<$\r, $\n>>, Arg, <<$\r, $\n>>];
|
||||
encode(Args) when is_list(Args) ->
|
||||
ArgCount = [<<$*>>, integer_to_list(length(Args)), <<$\r, $\n>>],
|
||||
ArgsBin = lists:map(fun encode/1, lists:map(fun to_binary/1, Args)),
|
||||
[ArgCount, ArgsBin].
|
||||
|
||||
%% 将数据转换成binary
|
||||
to_binary(X) when is_list(X) ->
|
||||
unicode:characters_to_binary(X);
|
||||
to_binary(X) when is_atom(X) ->
|
||||
list_to_binary(atom_to_list(X));
|
||||
to_binary(X) when is_binary(X) ->
|
||||
X;
|
||||
to_binary(X) when is_integer(X) ->
|
||||
list_to_binary(integer_to_list(X)).
|
||||
@ -1,47 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 28. 2月 2026 22:07
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(maxwell_redis_server).
|
||||
-author("licheng5").
|
||||
|
||||
%% API
|
||||
-export([start_link/1, init/1]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% esockd callback
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec start_link(Port :: inet:port_number()) -> {ok, pid()}.
|
||||
start_link(Port) when is_integer(Port) ->
|
||||
{ok, spawn_link(?MODULE, init, [Port])}.
|
||||
|
||||
-spec init(Port :: inet:port_number()) -> no_return().
|
||||
init(Port) ->
|
||||
{ok, LSocket} = gen_tcp:listen(Port, [
|
||||
binary,
|
||||
{packet, 0},
|
||||
{reuseaddr, true},
|
||||
{backlog, 1024},
|
||||
{active, false}
|
||||
]),
|
||||
accept_loop(LSocket).
|
||||
|
||||
accept_loop(LSocket) ->
|
||||
case gen_tcp:accept(LSocket) of
|
||||
{ok, Socket} ->
|
||||
logger:debug("accept socket: ~p", [Socket]),
|
||||
%% 每个连接一个进程
|
||||
Pid = maxwell_redis_channel:start(Socket),
|
||||
ok = gen_tcp:controlling_process(Socket, Pid),
|
||||
accept_loop(LSocket);
|
||||
{error, closed} ->
|
||||
exit(tcp_closed);
|
||||
{error, Reason} ->
|
||||
logger:debug("[maxwell_redis_server] Accept error: ~p~n", [Reason]),
|
||||
accept_loop(LSocket)
|
||||
end.
|
||||
@ -1,78 +0,0 @@
|
||||
-module(rule_ets).
|
||||
-include("policy.hrl").
|
||||
|
||||
-export([init/0]).
|
||||
-export([insert/1, get_rules/2, delete/1, update/2]).
|
||||
|
||||
-spec init() -> ets:tab().
|
||||
init() ->
|
||||
ets:new(rule_table, [named_table, ordered_set, public, {keypos, 2}, {read_concurrency, true}]),
|
||||
ets:new(rule_index, [named_table, bag, public, {read_concurrency, true}]).
|
||||
|
||||
-spec get_rules(SrcPolicyIds :: any(), DstPolicyIds :: any()) -> {ok, [{Proto :: integer(), Port :: integer()}]}.
|
||||
get_rules(SrcPolicyIds, DstPolicyIds) when is_list(SrcPolicyIds), is_list(DstPolicyIds) ->
|
||||
MatchKeys = [{S, D, '_'} || S <- SrcPolicyIds, D <- DstPolicyIds],
|
||||
Records = lists:flatmap(fun({S, D, _}) -> ets:match_object(rule_index, {S, D, '_'}) end, MatchKeys),
|
||||
Rules = lists:flatmap(fun({_, _, RuleId}) -> ets:lookup(rule_table, RuleId) end, Records),
|
||||
|
||||
S = lists:foldl(fun(Rule, S) ->
|
||||
case Rule of
|
||||
#rule{action = allow, proto = Proto, port = Port} ->
|
||||
sets:add_element({Proto, Port}, S);
|
||||
_ ->
|
||||
S
|
||||
end
|
||||
end, sets:new(), Rules),
|
||||
|
||||
{ok, sets:to_list(S)}.
|
||||
|
||||
-spec insert(Data :: map()) -> true.
|
||||
insert(#{<<"rule_id">> := RuleId, <<"network_id">> := NetworkId,
|
||||
<<"src_policy_id">> := SrcPolicyId, <<"dst_policy_id">> := DstPolicyId, <<"proto">> := Proto,
|
||||
<<"port">> := Port, <<"action">> := Action, <<"created_at">> := CreatedAt}) ->
|
||||
Rule = #rule{
|
||||
rule_id = RuleId,
|
||||
network_id = NetworkId,
|
||||
src_policy_id = SrcPolicyId,
|
||||
dst_policy_id = DstPolicyId,
|
||||
proto = Proto,
|
||||
port = Port,
|
||||
action = format_action(Action),
|
||||
created_at = CreatedAt
|
||||
},
|
||||
ets:insert(rule_table, Rule),
|
||||
ets:insert(rule_index, {SrcPolicyId, DstPolicyId, RuleId}).
|
||||
|
||||
-spec update(NewData :: map(), OldData :: map()) -> true.
|
||||
update(NewData = #{<<"rule_id">> := RuleId, <<"network_id">> := NetworkId,
|
||||
<<"src_policy_id">> := SrcPolicyId, <<"dst_policy_id">> := DstPolicyId, <<"proto">> := Proto,
|
||||
<<"port">> := Port, <<"action">> := Action, <<"created_at">> := CreatedAt}, OldData) ->
|
||||
|
||||
%% rule_id是主键,直接覆盖
|
||||
Rule = #rule{
|
||||
rule_id = RuleId,
|
||||
network_id = NetworkId,
|
||||
src_policy_id = SrcPolicyId,
|
||||
dst_policy_id = DstPolicyId,
|
||||
proto = Proto,
|
||||
port = Port,
|
||||
action = format_action(Action),
|
||||
created_at = CreatedAt
|
||||
},
|
||||
ets:insert(rule_table, Rule),
|
||||
%% index老的数据可能要清理掉, 用就的数据覆盖新的数据,得到的是Old的Record
|
||||
#{<<"src_policy_id">> := OldSrcPolicyId, <<"dst_policy_id">> := OldDstPolicyId} = maps:merge(NewData, OldData),
|
||||
ets:delete_object(rule_index, {OldSrcPolicyId, OldDstPolicyId, RuleId}),
|
||||
%% 建立信息的索引
|
||||
ets:insert(rule_index, {SrcPolicyId, DstPolicyId, RuleId}).
|
||||
|
||||
-spec delete(Data :: map()) -> true.
|
||||
delete(#{<<"rule_id">> := RuleId, <<"src_policy_id">> := SrcPolicyId, <<"dst_policy_id">> := DstPolicyId}) ->
|
||||
ets:delete(rule_table, RuleId),
|
||||
ets:delete_object(rule_index, {SrcPolicyId, DstPolicyId, RuleId}).
|
||||
|
||||
-spec format_action(binary()) -> atom().
|
||||
format_action(<<"allow">>) ->
|
||||
allow;
|
||||
format_action(_) ->
|
||||
deny.
|
||||
@ -1,48 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%% Compatibility facade for the QUIC channel API.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_quic_channel).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([start_link/2]).
|
||||
-export([accept_stream/1, send_event/2, command/4, stop/2, debug_info/1]).
|
||||
-export([test_rules/2]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% 测试规则函数
|
||||
-spec test_rules(SrcIdentityId :: integer(), DstIdentityId :: integer()) -> binary().
|
||||
test_rules(SrcIdentityId, DstIdentityId) when is_integer(SrcIdentityId), is_integer(DstIdentityId) ->
|
||||
sdlan_session:test_rules(SrcIdentityId, DstIdentityId).
|
||||
|
||||
-spec send_event(Pid :: pid(), Event :: binary()) -> ok.
|
||||
send_event(Pid, ProtobufEvent) when is_pid(Pid), is_binary(ProtobufEvent) ->
|
||||
sdlan_quic_transport:send_event(Pid, ProtobufEvent).
|
||||
|
||||
-spec command(Pid :: pid(), Ref :: reference(), ReceiverPid :: pid(), {Tag :: atom(), SubCommand :: any()}) -> ok.
|
||||
command(Pid, Ref, ReceiverPid, SubCommand) when is_pid(Pid), is_pid(ReceiverPid) ->
|
||||
sdlan_quic_transport:command(Pid, Ref, ReceiverPid, SubCommand).
|
||||
|
||||
-spec accept_stream(Pid :: pid()) -> ok.
|
||||
accept_stream(Pid) when is_pid(Pid) ->
|
||||
sdlan_quic_transport:accept_stream(Pid).
|
||||
|
||||
-spec stop(Pid :: pid(), Reason :: term()) -> ok.
|
||||
stop(Pid, Reason) when is_pid(Pid) ->
|
||||
sdlan_quic_transport:stop(Pid, Reason).
|
||||
|
||||
-spec debug_info(Pid :: pid()) -> map().
|
||||
debug_info(Pid) when is_pid(Pid) ->
|
||||
sdlan_quic_transport:debug_info(Pid).
|
||||
|
||||
%% @doc Creates a transport process. Kept for existing supervisor specs.
|
||||
-spec start_link(Conn :: quicer:connection_handle(), Limits :: proplists:proplist()) -> gen_statem:start_ret().
|
||||
start_link(Conn, Limits) when is_list(Limits) ->
|
||||
sdlan_quic_transport:start_link(Conn, Limits).
|
||||
@ -1,93 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 11. 2月 2026 21:26
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_quic_server).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([start_link/0, init/0]).
|
||||
|
||||
-spec start_link() -> {ok, pid()}.
|
||||
start_link() ->
|
||||
{ok, spawn_link(?MODULE, init, [])}.
|
||||
|
||||
-spec init() -> ok.
|
||||
init() ->
|
||||
process_flag(trap_exit, true),
|
||||
{ok, Props} = application:get_env(sdlan, quic_server),
|
||||
Port = proplists:get_value(port, Props),
|
||||
Alpn = proplists:get_value(alpn, Props),
|
||||
Limits = proplists:get_value(limits, Props),
|
||||
AcceptorCount = proplists:get_value(acceptors, Props, 10),
|
||||
|
||||
%% 证书配置
|
||||
{CertFile, KeyFile} = sdlan_util:certificate_config(),
|
||||
|
||||
LOptions = #{
|
||||
% 必选:QUIC/TLS证书配置
|
||||
certfile => CertFile,
|
||||
keyfile => KeyFile,
|
||||
alpn => Alpn,
|
||||
peer_bidi_stream_count => 1,
|
||||
conn_acceptors => AcceptorCount
|
||||
},
|
||||
ListenAddr = "0.0.0.0:" ++ integer_to_list(Port),
|
||||
case quicer:listen(ListenAddr, LOptions) of
|
||||
{ok, L} ->
|
||||
logger:notice("[sdlan_quic_server] listen on ~s, acceptors: ~p", [ListenAddr, AcceptorCount]),
|
||||
Pids = [spawn_link(fun() -> loop_accept(L, Limits, I) end) || I <- lists:seq(1, AcceptorCount)],
|
||||
wait_acceptors(L, Pids);
|
||||
Error ->
|
||||
exit(Error)
|
||||
end.
|
||||
|
||||
wait_acceptors(L, Pids) ->
|
||||
receive
|
||||
{'EXIT', Pid, Reason} ->
|
||||
logger:warning("[sdlan_quic_server] acceptor ~p exit with reason: ~p", [Pid, Reason]),
|
||||
quicer:close_listener(L),
|
||||
exit({acceptor_exit, Pid, Reason, Pids});
|
||||
stop ->
|
||||
quicer:close_listener(L),
|
||||
ok;
|
||||
Info ->
|
||||
logger:notice("[sdlan_quic_server] get unexpected info: ~p", [Info]),
|
||||
wait_acceptors(L, Pids)
|
||||
end.
|
||||
|
||||
loop_accept(L, Limits, AcceptorId) ->
|
||||
case quicer:accept(L, #{}, infinity) of
|
||||
{ok, Conn} ->
|
||||
logger:debug("[sdlan_quic_server] acceptor: ~p, accept a new connection: ~p", [AcceptorId, Conn]),
|
||||
case quicer:handshake(Conn) of
|
||||
{ok, NConn} ->
|
||||
case sdlan_quic_channel_sup:start_channel(NConn, Limits) of
|
||||
{ok, ChannelPid} ->
|
||||
logger:debug("[sdlan_quic_server] conn: ~p, handshake success, channel pid: ~p", [NConn, ChannelPid]),
|
||||
case quicer:controlling_process(NConn, ChannelPid) of
|
||||
ok ->
|
||||
sdlan_quic_transport:accept_stream(ChannelPid);
|
||||
{error, Reason} ->
|
||||
logger:warning("[sdlan_quic_server] conn: ~p, controlling_process failed: ~p", [NConn, Reason]),
|
||||
sdlan_quic_transport:stop(ChannelPid, {controlling_process_failed, Reason}),
|
||||
quicer:close_connection(NConn)
|
||||
end;
|
||||
Error ->
|
||||
quicer:close_connection(NConn),
|
||||
logger:notice("[sdlan_quic_server] start channel get error: ~p", [Error])
|
||||
end,
|
||||
loop_accept(L, Limits, AcceptorId);
|
||||
{error, Reason} ->
|
||||
logger:debug("[sdlan_quic_server] acceptor: ~p, handshake failed: ~p", [AcceptorId, Reason]),
|
||||
quicer:close_connection(Conn),
|
||||
loop_accept(L, Limits, AcceptorId)
|
||||
end;
|
||||
{error, Reason} ->
|
||||
logger:debug("[sdlan_quic_server] acceptor: ~p, accept failed: ~p", [AcceptorId, Reason]),
|
||||
loop_accept(L, Limits, AcceptorId)
|
||||
end.
|
||||
@ -1,301 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%% QUIC transport for sdlan sessions.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_quic_transport).
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
-include("sdlan_pb.hrl").
|
||||
|
||||
-behaviour(gen_statem).
|
||||
|
||||
%% 心跳包监测机制
|
||||
-define(STREAM_ACTIVE_N, 100).
|
||||
|
||||
%% API
|
||||
-export([start_link/2]).
|
||||
-export([accept_stream/1, send_event/2, command/4, stop/2, debug_info/1]).
|
||||
|
||||
%% gen_statem callbacks
|
||||
-export([init/1, handle_event/4, terminate/3, code_change/4, callback_mode/0]).
|
||||
|
||||
-record(state, {
|
||||
conn :: quicer:connection_handle(),
|
||||
%% 最大包大小
|
||||
max_packet_size = 16384,
|
||||
%% 心跳间隔
|
||||
heartbeat_sec = 15,
|
||||
stream_active_n = ?STREAM_ACTIVE_N,
|
||||
|
||||
stream :: undefined | quicer:stream_handle(),
|
||||
%% 累积器,用于处理协议framing的解析
|
||||
buf = <<>>,
|
||||
|
||||
session :: sdlan_session:session(),
|
||||
|
||||
close_reason = undefined
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec send_event(Pid :: pid(), Event :: binary()) -> ok.
|
||||
send_event(Pid, ProtobufEvent) when is_pid(Pid), is_binary(ProtobufEvent) ->
|
||||
gen_statem:cast(Pid, {send_event, ProtobufEvent}).
|
||||
|
||||
-spec command(Pid :: pid(), Ref :: reference(), ReceiverPid :: pid(), {Tag :: atom(), SubCommand :: any()}) -> ok.
|
||||
command(Pid, Ref, ReceiverPid, SubCommand) when is_pid(Pid), is_pid(ReceiverPid) ->
|
||||
gen_statem:cast(Pid, {command, Ref, ReceiverPid, SubCommand}).
|
||||
|
||||
-spec accept_stream(Pid :: pid()) -> ok.
|
||||
accept_stream(Pid) when is_pid(Pid) ->
|
||||
gen_statem:cast(Pid, accept_stream).
|
||||
|
||||
-spec stop(Pid :: pid(), Reason :: term()) -> ok.
|
||||
stop(Pid, Reason) when is_pid(Pid) ->
|
||||
gen_statem:stop(Pid, Reason, 2000).
|
||||
|
||||
-spec debug_info(Pid :: pid()) -> map().
|
||||
debug_info(Pid) when is_pid(Pid) ->
|
||||
gen_statem:call(Pid, debug_info).
|
||||
|
||||
%% @doc Creates a gen_statem process which calls Module:init/1 to
|
||||
%% initialize. To ensure a synchronized start-up procedure, this
|
||||
%% function does not return until Module:init/1 has returned.
|
||||
-spec start_link(Conn :: quicer:connection_handle(), Limits :: proplists:proplist()) -> gen_statem:start_ret().
|
||||
start_link(Conn, Limits) when is_list(Limits) ->
|
||||
gen_statem:start_link(?MODULE, [Conn, Limits], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_statem callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
|
||||
%% gen_statem:start_link/[3,4], this function is called by the new
|
||||
%% process to initialize.
|
||||
-spec init(Args :: term()) -> gen_statem:init_result(atom(), #state{}).
|
||||
init([Conn, Limits]) ->
|
||||
MaxPacketSize = proplists:get_value(max_packet_size, Limits, 16384),
|
||||
HeartbeatSec = proplists:get_value(heartbeat_sec, Limits, 15),
|
||||
StreamActiveN = proplists:get_value(stream_active_n, Limits, ?STREAM_ACTIVE_N),
|
||||
Session = sdlan_session:new(HeartbeatSec, ?MODULE),
|
||||
{ok, initializing, #state{
|
||||
conn = Conn,
|
||||
max_packet_size = MaxPacketSize,
|
||||
heartbeat_sec = HeartbeatSec,
|
||||
stream_active_n = StreamActiveN,
|
||||
session = Session
|
||||
}}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_statem when it needs to find out
|
||||
%% the callback mode of the callback module.
|
||||
-spec callback_mode() -> gen_statem:callback_mode_result().
|
||||
callback_mode() ->
|
||||
handle_event_function.
|
||||
|
||||
%% @private
|
||||
%% @doc If callback_mode is handle_event_function, then whenever a
|
||||
%% gen_statem receives an event from call/2, cast/2, or as a normal
|
||||
%% process message, this function is called.
|
||||
|
||||
-spec handle_event(EventType :: gen_statem:event_type(), EventContent :: term(),
|
||||
StateName :: atom(), State :: #state{}) -> gen_statem:event_handler_result(atom(), #state{}).
|
||||
handle_event(cast, accept_stream, initializing, State = #state{conn = Conn, stream_active_n = StreamActiveN}) ->
|
||||
logger:debug("[sdlan_quic_transport] call do_init of conn: ~p", [Conn]),
|
||||
case quicer:async_accept_stream(Conn, #{active => StreamActiveN}) of
|
||||
{ok, _} ->
|
||||
{next_state, waiting_stream, State};
|
||||
{error, Reason} ->
|
||||
{stop, {accept_stream_failed, Reason}, State}
|
||||
end;
|
||||
|
||||
%% 处理收到的quic消息
|
||||
handle_event(info, {quic, dgram_state_changed, Conn, Opts = #{dgram_send_enabled := true}}, _, State = #state{conn = Conn}) ->
|
||||
logger:debug("[sdlan_quic_transport] dgram_state_changed, opts: ~p", [Opts]),
|
||||
{keep_state, State};
|
||||
|
||||
handle_event(info, {quic, new_stream, Stream, Opts}, waiting_stream, State = #state{max_packet_size = MaxPacketSize, heartbeat_sec = HeartbeatSec}) ->
|
||||
logger:debug("[sdlan_quic_transport] call new_stream: ~p, opts: ~p", [Stream, Opts]),
|
||||
%% 发送欢迎消息
|
||||
quic_send(Stream, sdlan_session:welcome_packet(MaxPacketSize, HeartbeatSec)),
|
||||
logger:debug("[sdlan_quic_transport] get stream: ~p, send welcome", [Stream]),
|
||||
|
||||
{next_state, initialized, State#state{stream = Stream}};
|
||||
|
||||
handle_event(info, {quic, new_stream, Stream, Opts}, _StateName, State) ->
|
||||
logger:warning("[sdlan_quic_transport] reject unexpected stream: ~p, opts: ~p", [Stream, Opts]),
|
||||
quicer:close_stream(Stream, 1000),
|
||||
{keep_state, State};
|
||||
|
||||
handle_event(info, {quic, stream_closed, Stream, Props}, _StateName, State = #state{stream = Stream}) ->
|
||||
expected_stop({stream_closed, Props}, State);
|
||||
|
||||
handle_event(info, {quic, peer_send_shutdown, Stream, _Props}, _StateName, State = #state{stream = Stream}) ->
|
||||
expected_stop(peer_send_shutdown, State);
|
||||
|
||||
handle_event(info, {quic, peer_send_aborted, Stream, ErrorCode}, _StateName, State = #state{stream = Stream}) ->
|
||||
expected_stop({peer_send_aborted, ErrorCode}, State);
|
||||
|
||||
handle_event(info, {quic, peer_receive_aborted, Stream, ErrorCode}, _StateName, State = #state{stream = Stream}) ->
|
||||
expected_stop({peer_receive_aborted, ErrorCode}, State);
|
||||
|
||||
handle_event(info, {quic, send_shutdown_complete, Stream, _Props}, _StateName, State = #state{stream = Stream}) ->
|
||||
expected_stop(connection_shutdown, State);
|
||||
|
||||
handle_event(info, {quic, passive, Stream, _Props}, _StateName, State = #state{stream = Stream, stream_active_n = StreamActiveN}) ->
|
||||
ok = quicer:setopt(Stream, active, StreamActiveN),
|
||||
{keep_state, State};
|
||||
|
||||
handle_event(info, {quic, closed, Conn, Props}, _StateName, State = #state{conn = Conn}) ->
|
||||
expected_stop({connection_closed, Props}, State);
|
||||
|
||||
handle_event(info, {quic, transport_shutdown, Conn, Props}, _StateName, State = #state{conn = Conn}) ->
|
||||
expected_stop({transport_shutdown, Props}, State);
|
||||
|
||||
handle_event(info, {quic, shutdown, Conn, ErrorCode}, _StateName, State = #state{conn = Conn}) ->
|
||||
expected_stop({connection_shutdown_by_peer, ErrorCode}, State);
|
||||
|
||||
%% 处理quicer相关的信息, 需要转换成内部能够识别的frame消息
|
||||
handle_event(info, {quic, Data, Stream, _Props}, _StateName,
|
||||
State = #state{stream = Stream, buf = Buf, max_packet_size = MaxPacketSize}) when is_binary(Data) ->
|
||||
case decode_frames(<<Buf/binary, Data/binary>>, MaxPacketSize) of
|
||||
{error, Reason} ->
|
||||
{stop, Reason, State};
|
||||
{ok, NBuf, Frames} ->
|
||||
Actions = [{next_event, internal, {frame, Frame}} || Frame <- Frames],
|
||||
{keep_state, State#state{buf = NBuf}, Actions}
|
||||
end;
|
||||
|
||||
%% 处理内部的包消息
|
||||
handle_event(internal, {frame, Frame}, _StateName, State = #state{stream = Stream, session = Session}) ->
|
||||
case sdlan_session:handle_frame(Frame, Session) of
|
||||
{ok, NStateName, NSession, Packets} ->
|
||||
quic_send(Stream, Packets),
|
||||
{next_state, NStateName, State#state{session = NSession}};
|
||||
{stop, Reason, _NStateName, NSession, Packets} ->
|
||||
quic_send(Stream, Packets),
|
||||
{stop, Reason, State#state{session = NSession}}
|
||||
end;
|
||||
|
||||
handle_event(info, {timeout, TimerRef, ping_ticker}, _StateName, State = #state{session = Session}) ->
|
||||
case sdlan_session:handle_timeout(TimerRef, Session) of
|
||||
{ok, NStateName, NSession} ->
|
||||
{next_state, NStateName, State#state{session = NSession}};
|
||||
{stop, Reason, _NStateName, NSession} ->
|
||||
expected_stop(Reason, State#state{session = NSession})
|
||||
end;
|
||||
|
||||
%% 发送指令信息
|
||||
handle_event(cast, {send_event, Event}, _StateName, State = #state{stream = Stream, session = Session}) ->
|
||||
case sdlan_session:send_event(Event, Session) of
|
||||
{ok, NStateName, NSession, Packets} ->
|
||||
quic_send(Stream, Packets),
|
||||
{next_state, NStateName, State#state{session = NSession}};
|
||||
{error, not_registered} ->
|
||||
keep_state_and_data
|
||||
end;
|
||||
|
||||
%% 发送命令信息
|
||||
handle_event(cast, {command, Ref, ReceiverPid, SubCommand}, _StateName, State = #state{stream = Stream, session = Session}) ->
|
||||
case sdlan_session:command(Ref, ReceiverPid, SubCommand, Session) of
|
||||
{ok, NStateName, NSession, Packets} ->
|
||||
quic_send(Stream, Packets),
|
||||
{next_state, NStateName, State#state{session = NSession}};
|
||||
{error, not_registered} ->
|
||||
keep_state_and_data
|
||||
end;
|
||||
|
||||
handle_event({call, From}, debug_info, StateName, State) ->
|
||||
{keep_state, State, [{reply, From, debug_info(StateName, State)}]};
|
||||
|
||||
handle_event(info, {'EXIT', _, _}, _StateName, State) ->
|
||||
expected_stop(connection_closed, State);
|
||||
|
||||
handle_event(EventType, Info, StateName, State) ->
|
||||
logger:notice("[sdlan_quic_transport] state: ~p, state_name: ~p, event_type: ~p, info: ~p", [State, StateName, EventType, Info]),
|
||||
keep_state_and_data.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_statem 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_statem terminates with
|
||||
%% Reason. The return value is ignored.
|
||||
-spec terminate(Reason :: term(), StateName :: atom(), State :: #state{}) -> ok.
|
||||
terminate(Reason, _StateName, _State = #state{conn = Conn, stream = Stream, session = Session, close_reason = CloseReason}) ->
|
||||
Stream /= undefined andalso quicer:close_stream(Stream, 1000),
|
||||
quicer:close_connection(Conn),
|
||||
logger:notice("[sdlan_quic_transport] terminate closed with reason: ~p, close_reason: ~p", [Reason, CloseReason]),
|
||||
sdlan_session:close(Session),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec code_change(OldVsn :: term(), StateName :: atom(), State :: #state{}, Extra :: term()) ->
|
||||
{ok, atom(), #state{}}.
|
||||
code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
|
||||
{ok, StateName, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
%% 有2种情况
|
||||
%% 1. 收到了多个完整的请求
|
||||
%% 2. 不完整,则不处理
|
||||
-spec decode_frames(Buf :: binary(), MaxPacketSize :: integer()) -> {ok, RestBin::binary(), Frames :: list()} | {error, Reason :: any()}.
|
||||
decode_frames(Buf, MaxPacketSize) when is_binary(Buf) ->
|
||||
decode_frames0(Buf, MaxPacketSize, []).
|
||||
decode_frames0(<<Len:16, _/binary>>, MaxPacketSize, _Frames) when Len > MaxPacketSize ->
|
||||
{error, frame_too_large};
|
||||
decode_frames0(<<Len:16, Frame:Len/binary, Rest/binary>>, MaxPacketSize, Frames) ->
|
||||
decode_frames0(Rest, MaxPacketSize, [Frame|Frames]);
|
||||
decode_frames0(Rest, _MaxPacketSize, Frames) ->
|
||||
{ok, Rest, lists:reverse(Frames)}.
|
||||
|
||||
-spec quic_send(Stream :: quicer:stream_handle(), Data :: iodata()) -> ok.
|
||||
quic_send(_Stream, []) ->
|
||||
ok;
|
||||
quic_send(Stream, Data) ->
|
||||
Len = iolist_size(Data),
|
||||
true = Len =< 65535,
|
||||
case quicer:send(Stream, [<<Len:16>>, Data]) of
|
||||
{ok, _} ->
|
||||
incr_counter(quic_frames_sent, 1),
|
||||
incr_counter(quic_bytes_sent, Len + 2),
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
exit({quic_send_failed, Reason})
|
||||
end.
|
||||
|
||||
expected_stop(Reason, State) ->
|
||||
logger:notice("[sdlan_quic_transport] expected close: ~p", [Reason]),
|
||||
{stop, normal, State#state{close_reason = Reason}}.
|
||||
|
||||
debug_info(StateName, #state{session = Session, stream_active_n = StreamActiveN, heartbeat_sec = HeartbeatSec}) ->
|
||||
ProcInfo = maps:from_list(process_info(self(), [message_queue_len, memory, reductions])),
|
||||
SessionInfo = sdlan_session:debug_info(Session),
|
||||
maps:merge(SessionInfo, ProcInfo#{
|
||||
state => StateName,
|
||||
session => SessionInfo,
|
||||
frames_sent => get_counter(quic_frames_sent),
|
||||
bytes_sent => get_counter(quic_bytes_sent),
|
||||
stream_active_n => StreamActiveN,
|
||||
heartbeat_sec => HeartbeatSec
|
||||
}).
|
||||
|
||||
incr_counter(Key, Inc) ->
|
||||
erlang:put(Key, get_counter(Key) + Inc).
|
||||
|
||||
get_counter(Key) ->
|
||||
case erlang:get(Key) of
|
||||
undefined ->
|
||||
0;
|
||||
Value when is_integer(Value) ->
|
||||
Value
|
||||
end.
|
||||
@ -1,219 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 27. 3月 2024 16:17
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_api).
|
||||
-author("anlicheng").
|
||||
|
||||
-define(API_TOKEN, <<"H6p*2RfEu4ITcL">>).
|
||||
|
||||
%% API
|
||||
-export([get_all_networks/0, get_network/1]).
|
||||
-export([flow_report/5, network_forward_report/2, auth_access_token/1, set_node_status/1]).
|
||||
|
||||
-spec get_all_networks() -> {ok, [NetworkId :: integer()]} | {error, Reason :: any()}.
|
||||
get_all_networks() ->
|
||||
case safe_request(fun() -> do_get("get_all_networks", []) end) of
|
||||
{ok, Resp} ->
|
||||
case decode_json(Resp) of
|
||||
{ok, #{<<"result">> := Networks}} ->
|
||||
{ok, Networks};
|
||||
{ok, #{<<"error">> := #{<<"code">> := _Code, <<"message">> := Message}}} ->
|
||||
{error, Message};
|
||||
{ok, _} ->
|
||||
{error, <<"invalid json">>};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
{error, _Reason} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec get_network(Id :: integer()) -> {ok, Network :: map()} | {error, Reason :: any()}.
|
||||
get_network(Id) when is_integer(Id) ->
|
||||
case safe_request(fun() -> do_get("get_network", [{<<"id">>, integer_to_binary(Id)}]) end) of
|
||||
{ok, Resp} ->
|
||||
case decode_json(Resp) of
|
||||
{ok, #{<<"result">> := Network}} ->
|
||||
{ok, Network};
|
||||
{ok, #{<<"error">> := #{<<"code">> := _Code, <<"message">> := Message}}} ->
|
||||
{error, Message};
|
||||
{ok, _} ->
|
||||
{error, <<"invalid json">>};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
{error, _Reason} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec auth_access_token(Params :: map()) -> {ok, Resp :: map()} | {error, Reason :: any()}.
|
||||
auth_access_token(Params) when is_map(Params) ->
|
||||
case safe_request(fun() -> do_post("auth/access_token", Params) end) of
|
||||
{ok, Resp} ->
|
||||
case decode_json(Resp) of
|
||||
{ok, Result} ->
|
||||
{ok, Result};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
{error, _Reason} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec set_node_status(Params :: map()) -> {ok, Resp :: map()} | {error, Reason :: any()}.
|
||||
set_node_status(Params) when is_map(Params) ->
|
||||
case safe_request(fun() -> do_post("set_node_status", Params) end) of
|
||||
{ok, Resp} ->
|
||||
decode_json(Resp);
|
||||
{error, _Reason} = 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 safe_request(fun() -> do_post("client_flow_report", Params) end) of
|
||||
{ok, Resp} ->
|
||||
decode_json(Resp);
|
||||
{error, _Reason} = 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 safe_request(fun() -> do_post("network_forward_report", Params) end) of
|
||||
{ok, Resp} ->
|
||||
decode_json(Resp);
|
||||
{error, _Reason} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec safe_request(Fun :: fun(() -> {ok, binary()} | {error, any()})) -> {ok, binary()} | {error, any()}.
|
||||
safe_request(Fun) when is_function(Fun, 0) ->
|
||||
case catch Fun() of
|
||||
{ok, _Resp} = Ok ->
|
||||
Ok;
|
||||
{error, _Reason} = Error ->
|
||||
Error;
|
||||
{'EXIT', Reason} ->
|
||||
{error, Reason};
|
||||
Error ->
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
-spec decode_json(Resp :: binary()) -> {ok, map()} | {error, any()}.
|
||||
decode_json(Resp) when is_binary(Resp) ->
|
||||
try json:decode(Resp) of
|
||||
Result when is_map(Result) ->
|
||||
{ok, Result};
|
||||
Other ->
|
||||
{error, {invalid_json, Other}}
|
||||
catch error:Reason ->
|
||||
{error, Reason}
|
||||
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) ->
|
||||
{ok, Url0} = application:get_env(sdlan, api_url),
|
||||
|
||||
Headers = [
|
||||
{<<"Content-Type">>, <<"application/json">>},
|
||||
{<<"X-sign">>, sign_params(Params)}
|
||||
],
|
||||
|
||||
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 :: map()) -> {ok, Resp :: binary()} | {error, Reason :: any()}.
|
||||
do_post(Uri, Params) when is_list(Uri), is_map(Params) ->
|
||||
{ok, Url0} = application:get_env(sdlan, api_url),
|
||||
|
||||
Headers = [
|
||||
{<<"content-type">>, <<"application/json">>},
|
||||
{<<"X-sign">>, sign_params(Params)}
|
||||
],
|
||||
|
||||
Body = iolist_to_binary(json:encode(Params)),
|
||||
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.
|
||||
|
||||
-spec sign_params(Params :: any()) -> binary().
|
||||
sign_params(Params) when is_list(Params) ->
|
||||
%% 签名逻辑
|
||||
Keys = lists:map(fun({K, _}) -> K end, Params),
|
||||
Parts = lists:map(fun(K) ->
|
||||
V = as_binary(proplists:get_value(K, Params, <<"">>)),
|
||||
<<K/binary, "=", V/binary>>
|
||||
end, lists:sort(Keys)),
|
||||
SignBin = iolist_to_binary(lists:join(<<"&">>, Parts)),
|
||||
logger:debug("sign: ~p, params: ~p", [SignBin, Params]),
|
||||
|
||||
list_to_binary(sdlan_util:hmac(?API_TOKEN, SignBin));
|
||||
sign_params(Params) when is_map(Params) ->
|
||||
SortedKeys = lists:sort(maps:keys(Params)),
|
||||
Parts = lists:map(fun(K) ->
|
||||
V = as_binary(maps:get(K, Params, <<"">>)),
|
||||
<<K/binary, "=", V/binary>>
|
||||
end, SortedKeys),
|
||||
SignBin = iolist_to_binary(lists:join(<<"&">>, Parts)),
|
||||
logger:debug("sign: ~p, params: ~p", [SignBin, Params]),
|
||||
list_to_binary(sdlan_util:hmac(?API_TOKEN, SignBin)).
|
||||
|
||||
-spec as_binary(Bin :: any()) -> binary().
|
||||
as_binary(Bin) when is_binary(Bin) ->
|
||||
Bin;
|
||||
as_binary(I) when is_integer(I) ->
|
||||
integer_to_binary(I);
|
||||
as_binary(F) when is_float(F) ->
|
||||
float_to_binary(F, [short]);
|
||||
as_binary(Str) when is_list(Str) ->
|
||||
list_to_binary(Str).
|
||||
@ -1,47 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%% @doc sdlan public API
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(sdlan_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
-spec start(StartType :: application:start_type(), StartArgs :: term()) ->
|
||||
{ok, pid()} | {ok, pid(), term()} | {error, term()}.
|
||||
start(_StartType, _StartArgs) ->
|
||||
io:setopts([{encoding, unicode}]),
|
||||
%% 加速内存的回收
|
||||
erlang:system_flag(fullsweep_after, 16),
|
||||
|
||||
%% 设置环境变量
|
||||
case sdlan_util:ipv6_assist_info() of
|
||||
{ok, V6Info} ->
|
||||
application:set_env(sdlan, ipv6_assist_info, V6Info);
|
||||
undefined ->
|
||||
ok
|
||||
end,
|
||||
|
||||
%% 现在ipv6探测工具的使用频率
|
||||
throttle:setup(sdlan_ipv6_assist, 60, per_minute),
|
||||
|
||||
%% 启动注册表
|
||||
sdlan_hostname_regedit:init(),
|
||||
sdlan_domain_regedit:init(),
|
||||
|
||||
%% 权限的数据管理
|
||||
identity_policy_ets:init(),
|
||||
rule_ets:init(),
|
||||
|
||||
sdlan_http_server:start(),
|
||||
sdlan_ssl_server:start(),
|
||||
|
||||
sdlan_sup:start_link().
|
||||
|
||||
-spec stop(State :: term()) -> ok.
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
%% internal functions
|
||||
@ -1,725 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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_lib("stdlib/include/ms_transform.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% 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, lookup_pid/1, peer_info/3, unregister/3, debug_info/1, get_network_id/1, attach/6, arp_request/2]).
|
||||
-export([forward_by_ets/5, update_hole/7, disable_client/2, get_channel/2]).
|
||||
-export([command/4, wait_command_ack/2]).
|
||||
|
||||
%% 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的使用信息, 记录Node的运行时状态信息
|
||||
-record(endpoint, {
|
||||
channel_pid :: undefined | pid(),
|
||||
channel_ref :: undefined | reference(),
|
||||
|
||||
client_id :: binary(),
|
||||
mac :: binary(),
|
||||
ip :: integer(),
|
||||
hostname :: binary(),
|
||||
hole :: undefined | #hole{},
|
||||
%% 记录ip和ip_v6的映射关系, #{ip_addr :: integer() => {}}
|
||||
v6_info :: undefined | #'SDLV6Info'{},
|
||||
session_token :: binary(),
|
||||
last_seen :: integer() %% monotonic_time(second),
|
||||
}).
|
||||
|
||||
-record(state, {
|
||||
network_id :: integer(),
|
||||
name :: binary(),
|
||||
domain :: binary(),
|
||||
ipaddr :: binary(),
|
||||
mask_len :: integer(),
|
||||
owner_id :: integer(),
|
||||
|
||||
%% 加密算法, 默认为chacha20
|
||||
algorithm :: binary(),
|
||||
%% 同一个网络下公用的密钥, 采用AES-256加密算法;随机生成
|
||||
key :: binary(),
|
||||
|
||||
%% 设置网络带宽
|
||||
throttle_key :: atom(),
|
||||
endpoint_table :: ets:tid()
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec get_pid(Id :: integer()) -> undefined | pid().
|
||||
get_pid(Id) when is_integer(Id) ->
|
||||
whereis(get_name(Id)).
|
||||
|
||||
-spec lookup_pid(Id :: integer()) -> {ok, Pid :: pid()} | error.
|
||||
lookup_pid(Id) when is_integer(Id) ->
|
||||
case whereis(get_name(Id)) of
|
||||
undefined ->
|
||||
error;
|
||||
Pid ->
|
||||
{ok, Pid}
|
||||
end.
|
||||
|
||||
-spec get_name(Id :: integer()) -> atom().
|
||||
get_name(Id) when is_integer(Id) ->
|
||||
list_to_atom("sdlan_network:" ++ integer_to_list(Id)).
|
||||
|
||||
-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 attach(Pid :: pid(), ChannelPid :: pid(), ClientId :: binary(), Mac :: binary(), Ip :: integer(), Hostname :: binary()) ->
|
||||
{ok, Algorithm :: binary(), Key :: binary(), RegionId :: integer(), SessionToken :: binary()}.
|
||||
attach(Pid, ChannelPid, ClientId, Mac, Ip, Hostname) when is_pid(Pid), is_pid(ChannelPid), is_binary(ClientId), is_binary(Mac), is_integer(Ip), is_binary(Hostname) ->
|
||||
gen_server:call(Pid, {attach, ChannelPid, ClientId, Mac, Ip, Hostname}).
|
||||
|
||||
-spec unregister(Pid :: pid(), ClientId :: binary(), Mac :: binary()) -> ok.
|
||||
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 | #'SDLV6Info'{}}.
|
||||
peer_info(Pid, SrcMac, DstMac) when is_pid(Pid), is_binary(SrcMac), is_binary(DstMac) ->
|
||||
gen_server:call(Pid, {peer_info, SrcMac, DstMac}).
|
||||
|
||||
-spec arp_request(Pid :: pid(), TargetIp :: integer()) -> error | {ok, Mac :: binary()}.
|
||||
arp_request(Pid, TargetIp) when is_pid(Pid), is_integer(TargetIp) ->
|
||||
gen_server:call(Pid, {arp_request, TargetIp}).
|
||||
|
||||
-spec command(Pid :: pid(), ReceiverPid :: pid(), ClientId :: binary(), {Tag :: atom(), SubCommand :: any()}) ->
|
||||
{error, Reason :: binary()} | {ok, Ref :: reference()}.
|
||||
command(Pid, ReceiverPid, ClientId, SubCommand) when is_pid(Pid), is_pid(ReceiverPid), is_binary(ClientId) ->
|
||||
gen_server:call(Pid, {command, ReceiverPid, ClientId, SubCommand}).
|
||||
|
||||
-spec wait_command_ack(Ref :: reference(), Timeout :: integer()) -> {error, timeout} | {ok, CommandAck :: #'SDLCommandAck'{}}.
|
||||
wait_command_ack(Ref, Timeout) when is_reference(Ref), is_integer(Timeout) ->
|
||||
receive
|
||||
{quic_command_ack, Ref, CommandAck} ->
|
||||
{ok, CommandAck}
|
||||
after Timeout ->
|
||||
{error, timeout}
|
||||
end.
|
||||
|
||||
-spec forward_by_ets(NetworkId :: integer(), Sock :: any(), SrcMac :: binary(), DstMac :: binary(), Packet :: binary()) ->
|
||||
{ok, ForwardBytes :: integer()} | {error, Reason :: any()}.
|
||||
forward_by_ets(NetworkId, Sock, SrcMac, DstMac, Packet) when is_integer(NetworkId), is_binary(SrcMac), is_binary(DstMac), is_binary(Packet) ->
|
||||
case endpoint_existing_table_name(NetworkId) of
|
||||
{ok, Table} ->
|
||||
case lookup_endpoint(Table, SrcMac) of
|
||||
#endpoint{} ->
|
||||
case sdlan_util:is_broadcast_mac(DstMac) orelse sdlan_util:is_multicast_mac(DstMac) of
|
||||
true ->
|
||||
forward_broadcast_by_ets(NetworkId, Sock, SrcMac, DstMac, Packet);
|
||||
false ->
|
||||
forward_unicast_by_ets(NetworkId, Sock, SrcMac, DstMac, Packet)
|
||||
end;
|
||||
undefined ->
|
||||
logger:debug("[sdlan_network] networkd_id: ~p, src_mac: ~p, dst_mac: ~p, forward discard, src not found",
|
||||
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
{error, src_not_found}
|
||||
end;
|
||||
error ->
|
||||
{error, table_not_found}
|
||||
end.
|
||||
|
||||
%% 更新ip地址对应的nat关系
|
||||
-spec update_hole(Pid :: pid(), SessionToken :: binary(), ClientId :: binary(), Mac :: binary(), Peer :: tuple(), NatType :: integer(), V6Info :: undefined | #'SDLV6Info'{}) -> ok.
|
||||
update_hole(Pid, SessionToken, ClientId, Mac, Peer, NatType, V6Info) when is_pid(Pid), is_binary(ClientId), is_binary(Mac), is_integer(NatType) ->
|
||||
gen_server:cast(Pid, {update_hole, SessionToken, 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}).
|
||||
|
||||
-spec debug_info(Pid :: pid()) -> map().
|
||||
debug_info(Pid) when is_pid(Pid) ->
|
||||
gen_server:call(Pid, debug_info).
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link(Name :: atom(), NetworkData :: tuple()) ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link(Name, NetworkData = {network, Id, _NetworkName, _Domain, _IpAddr, _MaskLen, _OwnerId, _Algorithm})
|
||||
when is_atom(Name), is_integer(Id) ->
|
||||
gen_server:start_link({local, Name}, ?MODULE, [NetworkData], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% 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([{network, Id, Name, Domain, IpAddr, MaskLen, OwnerId, Algorithm0}]) ->
|
||||
erlang:process_flag(trap_exit, true),
|
||||
%% 限流key
|
||||
ThrottleKey = list_to_atom("network_throttle:" ++ integer_to_list(Id)),
|
||||
%% 绑定到资源协调器
|
||||
sdlan_network_coordinator:attach(self(), ThrottleKey),
|
||||
|
||||
sdlan_domain_regedit:insert(Domain),
|
||||
|
||||
%% 处理加密算法
|
||||
Algorithm = normalization_algorithm(Algorithm0),
|
||||
Key = gen_key(Algorithm),
|
||||
EndpointTable = new_endpoint_table(Id),
|
||||
|
||||
{ok, #state{network_id = Id, name = Name, domain = Domain, ipaddr = IpAddr, algorithm = Algorithm,
|
||||
owner_id = OwnerId, mask_len = MaskLen, key = Key, throttle_key = ThrottleKey, endpoint_table = EndpointTable}}.
|
||||
|
||||
%% @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{}}).
|
||||
%% 给客户端分配ip地址
|
||||
handle_call({attach, ChannelPid, ClientId, Mac, Ip, Hostname}, _From,
|
||||
State = #state{network_id = NetworkId, domain = Domain, algorithm = Algorithm, key = Key, endpoint_table = EndpointTable}) ->
|
||||
%% 分配ip地址的时候,以mac地址为唯一基准
|
||||
logger:debug("[sdlan_network] alloc_ip, network_id: ~p, client_id: ~p, mac: ~p, ip_addr: ~p",
|
||||
[NetworkId, ClientId, sdlan_util:format_mac(Mac), sdlan_util:int_to_ipv4(Ip)]),
|
||||
%% 添加域名->ip的映射关系
|
||||
sdlan_hostname_regedit:insert(Hostname, Domain, Ip),
|
||||
|
||||
%% mac对应的Endpoint存在,并且对应的ip变了,需要通知端上清理arp
|
||||
maybe_nat_changed(ChannelPid, Mac, Ip, EndpointTable),
|
||||
|
||||
ChannelRef = monitor(process, ChannelPid),
|
||||
SessionToken = gen_session_token(),
|
||||
Endpoint = #endpoint{
|
||||
channel_pid = ChannelPid,
|
||||
channel_ref = ChannelRef,
|
||||
client_id = ClientId,
|
||||
mac = Mac,
|
||||
ip = Ip,
|
||||
hostname = Hostname,
|
||||
session_token = SessionToken,
|
||||
last_seen = erlang:monotonic_time(second)
|
||||
},
|
||||
insert_endpoint(EndpointTable, Mac, Endpoint),
|
||||
|
||||
%% 生成对应的分区id
|
||||
RegionId = gen_region_id(Ip),
|
||||
|
||||
{reply, {ok, Algorithm, Key, RegionId, SessionToken}, State};
|
||||
|
||||
%% client设置为禁止状态,不允许重连
|
||||
handle_call({disable_client, ClientId}, _From, State = #state{endpoint_table = EndpointTable}) ->
|
||||
MatchSpec = ets:fun2ms(fun(Object = {_Mac, #endpoint{client_id = ClientId0}}) when ClientId0 =:= ClientId ->
|
||||
Object
|
||||
end),
|
||||
case select_endpoint(EndpointTable, MatchSpec) of
|
||||
{ok, Mac, Endpoint} ->
|
||||
cleanup_endpoint(Endpoint, undefined, disabled),
|
||||
delete_endpoint(EndpointTable, Mac),
|
||||
{reply, ok, State};
|
||||
error ->
|
||||
{reply, ok, State}
|
||||
end;
|
||||
|
||||
handle_call({get_channel, ClientId}, _From, State = #state{endpoint_table = EndpointTable}) ->
|
||||
MatchSpec = ets:fun2ms(fun(Object = {_Mac, #endpoint{client_id = ClientId0}}) when ClientId0 =:= ClientId ->
|
||||
Object
|
||||
end),
|
||||
case select_endpoint(EndpointTable, MatchSpec) of
|
||||
{ok, _, #endpoint{channel_pid = ChannelPid}} ->
|
||||
{reply, {ok, ChannelPid}, State};
|
||||
error ->
|
||||
{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{endpoint_table = EndpointTable}) ->
|
||||
case lookup_endpoint(EndpointTable, DstMac) of
|
||||
#endpoint{channel_pid = DstChannelPid, hole = #hole{peer = DstNatPeer, nat_type = DstNatType}, v6_info = DstV6Info} ->
|
||||
%% 让目标服务器发送sendRegister事件(2024-06-25 新增,提高打洞的成功率)
|
||||
maybe
|
||||
#endpoint{hole = #hole{peer = {SrcNatIp, SrcNatPort}, nat_type = SrcNatType}, v6_info = SrcV6Info} ?= lookup_endpoint(EndpointTable, SrcMac),
|
||||
|
||||
RegisterEvent = sdlan_pb:encode_msg(#'SDLEvent' {
|
||||
event = {send_register, #'SDLEvent.SendRegister'{
|
||||
dst_mac = SrcMac,
|
||||
nat_ip = sdlan_util:ipv4_to_int(SrcNatIp),
|
||||
nat_type = SrcNatType,
|
||||
nat_port = SrcNatPort,
|
||||
v6_info = SrcV6Info
|
||||
}}
|
||||
}),
|
||||
logger:debug("Event: send_register, for peer_info"),
|
||||
|
||||
sdlan_quic_channel:send_event(DstChannelPid, RegisterEvent)
|
||||
end,
|
||||
{reply, {ok, {DstNatPeer, DstNatType}, DstV6Info}, State};
|
||||
_ ->
|
||||
{reply, error, State}
|
||||
end;
|
||||
|
||||
%% arp查询
|
||||
handle_call({arp_request, TargetIp}, _From, State = #state{endpoint_table = EndpointTable}) ->
|
||||
MatchSpec = ets:fun2ms(fun(Object = {_Mac, #endpoint{ip = Ip}}) when Ip =:= TargetIp ->
|
||||
Object
|
||||
end),
|
||||
case select_endpoint(EndpointTable, MatchSpec) of
|
||||
error ->
|
||||
{reply, error, State};
|
||||
{ok, Mac, _} ->
|
||||
{reply, {ok, Mac}, State}
|
||||
end;
|
||||
|
||||
%% 发送命令
|
||||
handle_call({command, ReceiverPid, ClientId, SubCommand}, _From, State = #state{endpoint_table = EndpointTable}) ->
|
||||
MatchSpec = ets:fun2ms(fun(Object = {_Mac, #endpoint{client_id = ClientId0}}) when ClientId0 =:= ClientId ->
|
||||
Object
|
||||
end),
|
||||
case select_endpoint(EndpointTable, MatchSpec) of
|
||||
{ok, _Mac, #endpoint{channel_pid = ChannelPid}} ->
|
||||
Ref = make_ref(),
|
||||
sdlan_quic_channel:command(ChannelPid, Ref, ReceiverPid, SubCommand),
|
||||
{reply, {ok, Ref}, State};
|
||||
error ->
|
||||
{reply, {error, <<"目标Node不在线"/utf8>>}, State}
|
||||
end;
|
||||
|
||||
handle_call(debug_info, _From, State = #state{network_id = NetworkId, ipaddr = IpAddr, mask_len = MaskLen, owner_id = OwnerId, endpoint_table = EndpointTable}) ->
|
||||
Reply = #{
|
||||
<<"network_id">> => NetworkId,
|
||||
<<"ipaddr">> => IpAddr,
|
||||
<<"mask_len">> => MaskLen,
|
||||
<<"owner_id">> => OwnerId,
|
||||
<<"metrics">> => network_metrics(State),
|
||||
<<"used_ips">> => lists:map(fun format_endpoint/1, list_endpoints(EndpointTable))
|
||||
},
|
||||
{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{}}).
|
||||
%% 删除ip的占用并关闭channel
|
||||
handle_cast({unregister, ClientId, Mac}, State = #state{network_id = NetworkId, endpoint_table = EndpointTable}) ->
|
||||
logger:debug("[sdlan_network] networkd_id: ~p, unregister client_id: ~p, Mac: ~p",
|
||||
[NetworkId, ClientId, sdlan_util:format_mac(Mac)]),
|
||||
case lookup_endpoint(EndpointTable, Mac) of
|
||||
Endpoint = #endpoint{client_id = ClientId} ->
|
||||
cleanup_endpoint(Endpoint, Endpoint#endpoint.channel_pid, unregister),
|
||||
delete_endpoint(EndpointTable, Mac),
|
||||
{noreply, State};
|
||||
undefined ->
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
%% 需要判断,client是属于当前网络的
|
||||
handle_cast({update_hole, SessionToken, ClientId, Mac, Peer, NatType, V6Info}, State = #state{endpoint_table = EndpointTable}) ->
|
||||
case lookup_endpoint(EndpointTable, Mac) of
|
||||
%% ClientId =:= ClientId0, SessionToken =:= SessionToken0
|
||||
Endpoint0 = #endpoint{ip = Ip, client_id = ClientId, hole = OldHole, session_token = SessionToken} ->
|
||||
NHole = #hole{peer = Peer, nat_type = NatType},
|
||||
maybe
|
||||
true ?= not same_hole(OldHole, NHole),
|
||||
|
||||
NatChangedEvent = sdlan_pb:encode_msg(#'SDLEvent' {
|
||||
event = {nat_changed, #'SDLEvent.NatChanged'{
|
||||
mac = Mac,
|
||||
ip = Ip
|
||||
}}
|
||||
}),
|
||||
|
||||
logger:debug("[sdlan_network] Event: nat_changed, update_hole, client_id: ~p(~p), hole changed", [ClientId, Ip]),
|
||||
broadcast(fun(#endpoint{channel_pid = ChannelPid}) ->
|
||||
sdlan_quic_channel:send_event(ChannelPid, NatChangedEvent)
|
||||
end, [Mac], EndpointTable)
|
||||
end,
|
||||
NEndpoint = Endpoint0#endpoint{hole = NHole, v6_info = V6Info, last_seen = erlang:monotonic_time(second)},
|
||||
insert_endpoint(EndpointTable, Mac, NEndpoint),
|
||||
logger:debug("[sdlan_network] mac: ~p, ip: ~p, endpoint is: ~p", [Mac, Ip, NEndpoint]),
|
||||
|
||||
{noreply, State};
|
||||
_ ->
|
||||
{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{}}).
|
||||
%% Channel进程退出, hole里面的数据也需要清理
|
||||
handle_info({'DOWN', _MRef, process, ChannelPid, Reason}, State = #state{network_id = NetworkId, endpoint_table = EndpointTable}) ->
|
||||
logger:notice("[sdlan_network] network_id: ~p, channel_pid: ~p, close with reason: ~p", [NetworkId, ChannelPid, Reason]),
|
||||
remove_channel_endpoints(ChannelPid, EndpointTable),
|
||||
{noreply, State};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
logger:debug("[sdlan_network] get unknown 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{}) -> ok).
|
||||
terminate(Reason, #state{network_id = NetworkId, endpoint_table = EndpointTable}) ->
|
||||
broadcast(fun(#endpoint{channel_pid = ChannelPid}) ->
|
||||
case is_pid(ChannelPid) andalso is_process_alive(ChannelPid) of
|
||||
true ->
|
||||
NetworkShutdownEvent = sdlan_pb:encode_msg(#'SDLEvent'{
|
||||
event = {shutdown, #'SDLEvent.NetworkShutdown'{
|
||||
message = <<"Network shutdown">>
|
||||
}}
|
||||
}),
|
||||
|
||||
logger:debug("[sdlan_network] Event: shutdown"),
|
||||
|
||||
sdlan_quic_channel:send_event(ChannelPid, NetworkShutdownEvent),
|
||||
sdlan_quic_channel:stop(ChannelPid, normal);
|
||||
false ->
|
||||
ok
|
||||
end
|
||||
end, [], EndpointTable),
|
||||
logger:debug("[sdlan_network] network: ~p, will terminate with reason: ~p", [NetworkId, Reason]),
|
||||
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 limiting_check(ThrottleKey :: any()) -> pass | denied.
|
||||
limiting_check(ThrottleKey) ->
|
||||
case throttle:check(sdlan_network, ThrottleKey) of
|
||||
{ok, _RestCount, _LeftToReset} ->
|
||||
pass;
|
||||
{limit_exceeded, 0, _LeftToReset} ->
|
||||
%% 尝试获取其他网络是否有让渡的资源
|
||||
case sdlan_network_coordinator:checkout() of
|
||||
ok ->
|
||||
pass;
|
||||
error ->
|
||||
denied
|
||||
end
|
||||
end.
|
||||
|
||||
-spec endpoint_table_name(NetworkId :: integer()) -> atom().
|
||||
endpoint_table_name(NetworkId) when is_integer(NetworkId) ->
|
||||
list_to_atom(lists:concat(["sdlan_network_endpoint:", NetworkId])).
|
||||
|
||||
-spec endpoint_existing_table_name(NetworkId :: integer()) -> {ok, atom()} | error.
|
||||
endpoint_existing_table_name(NetworkId) when is_integer(NetworkId) ->
|
||||
try
|
||||
Atom = list_to_existing_atom(lists:concat(["sdlan_network_endpoint:", NetworkId])),
|
||||
{ok, Atom}
|
||||
catch error:_ ->
|
||||
error
|
||||
end.
|
||||
|
||||
-spec throttle_key(NetworkId :: integer()) -> atom().
|
||||
throttle_key(NetworkId) when is_integer(NetworkId) ->
|
||||
list_to_atom("network_throttle:" ++ integer_to_list(NetworkId)).
|
||||
|
||||
-spec new_endpoint_table(NetworkId :: integer()) -> ets:tid().
|
||||
new_endpoint_table(NetworkId) when is_integer(NetworkId) ->
|
||||
ets:new(endpoint_table_name(NetworkId), [
|
||||
named_table,
|
||||
protected,
|
||||
set,
|
||||
{read_concurrency, true},
|
||||
{write_concurrency, true}
|
||||
]).
|
||||
|
||||
-spec insert_endpoint(Table :: ets:tid(), Mac :: binary(), Endpoint :: #endpoint{}) -> true.
|
||||
insert_endpoint(Table, Mac, Endpoint = #endpoint{}) when is_binary(Mac) ->
|
||||
ets:insert(Table, {Mac, Endpoint}).
|
||||
|
||||
-spec delete_endpoint(Table :: ets:tid(), Mac :: binary()) -> true.
|
||||
delete_endpoint(Table, Mac) when is_binary(Mac) ->
|
||||
ets:delete(Table, Mac).
|
||||
|
||||
-spec lookup_endpoint(atom() | ets:tid(), Mac :: binary()) -> #endpoint{} | undefined.
|
||||
lookup_endpoint(Table, Mac) when is_binary(Mac) ->
|
||||
try ets:lookup(Table, Mac) of
|
||||
[{Mac, Endpoint = #endpoint{}}] ->
|
||||
Endpoint;
|
||||
[] ->
|
||||
undefined
|
||||
catch error:_ ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec list_endpoints(atom() | ets:tid()) -> [{binary(), #endpoint{}}].
|
||||
list_endpoints(Table) ->
|
||||
try ets:tab2list(Table) of
|
||||
Endpoints when is_list(Endpoints) ->
|
||||
Endpoints
|
||||
catch error:_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec remove_channel_endpoints(ChannelPid :: pid(), Table :: ets:tid()) -> ok.
|
||||
remove_channel_endpoints(ChannelPid, Table) when is_pid(ChannelPid) ->
|
||||
lists:foreach(fun
|
||||
({Mac, #endpoint{channel_pid = ChannelPid0}}) when ChannelPid =:= ChannelPid0 ->
|
||||
delete_endpoint(Table, Mac);
|
||||
(_) ->
|
||||
ok
|
||||
end, list_endpoints(Table)).
|
||||
|
||||
-spec forward_unicast_by_ets(NetworkId :: integer(), Sock :: any(), SrcMac :: binary(), DstMac :: binary(), Packet :: binary()) ->
|
||||
{ok, integer()} | {error, any()}.
|
||||
forward_unicast_by_ets(NetworkId, Sock, SrcMac, DstMac, Packet) ->
|
||||
PacketBytes = byte_size(Packet),
|
||||
case endpoint_existing_table_name(NetworkId) of
|
||||
{ok, Table} ->
|
||||
case lookup_endpoint(Table, DstMac) of
|
||||
#endpoint{hole = #hole{peer = Peer = {NatIp, NatPort}}} ->
|
||||
case limiting_check(throttle_key(NetworkId)) of
|
||||
pass ->
|
||||
logger:debug("[sdlan_network] forward data by ets 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, NatIp, NatPort, Packet),
|
||||
{ok, PacketBytes};
|
||||
denied ->
|
||||
logger: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)]),
|
||||
{error, rate_limited}
|
||||
end;
|
||||
#endpoint{} ->
|
||||
logger: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)]),
|
||||
{error, hole_not_found};
|
||||
undefined ->
|
||||
logger: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)]),
|
||||
{error, dst_not_found}
|
||||
end;
|
||||
error ->
|
||||
{error, table_not_found}
|
||||
end.
|
||||
|
||||
-spec forward_broadcast_by_ets(NetworkId :: integer(), Sock :: any(), SrcMac :: binary(), DstMac :: binary(), Packet :: binary()) ->
|
||||
{ok, integer()}.
|
||||
forward_broadcast_by_ets(NetworkId, Sock, SrcMac, DstMac, Packet) ->
|
||||
Table = endpoint_table_name(NetworkId),
|
||||
lists:foreach(fun
|
||||
({Mac, #endpoint{hole = #hole{peer = {NatIp, NatPort}}}}) when Mac =/= SrcMac ->
|
||||
gen_udp:send(Sock, NatIp, NatPort, Packet);
|
||||
(_) ->
|
||||
ok
|
||||
end, list_endpoints(Table)),
|
||||
logger:debug("[sdlan_network] broadcast data by ets networkd_id: ~p, src_mac: ~p, dst_mac: ~p",
|
||||
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
{ok, byte_size(Packet)}.
|
||||
|
||||
maybe_nat_changed(ChannelPid, Mac, Ip, EndpointTable) ->
|
||||
OldEndpoint = lookup_endpoint(EndpointTable, Mac),
|
||||
%% mac对应的Endpoint存在,并且对应的ip变了,需要通知端上清理arp
|
||||
maybe
|
||||
#endpoint{ip = OldIp} ?= OldEndpoint,
|
||||
true ?= OldIp =/= Ip,
|
||||
|
||||
Event = sdlan_pb:encode_msg(#'SDLEvent'{
|
||||
event = {nat_changed, #'SDLEvent.NatChanged' {
|
||||
mac = Mac,
|
||||
ip = Ip
|
||||
}}
|
||||
}),
|
||||
logger:debug("Event: nat_changed, for attach"),
|
||||
|
||||
broadcast(fun(#endpoint{channel_pid = ChannelPid0}) ->
|
||||
sdlan_quic_channel:send_event(ChannelPid0, Event)
|
||||
end, [Mac], EndpointTable)
|
||||
end,
|
||||
%% 重复attach需要清理之前的绑定信息;即使IP未变化,也不能保留旧channel。
|
||||
cleanup_endpoint(OldEndpoint, ChannelPid, rebind).
|
||||
|
||||
cleanup_endpoint(undefined, _KeepChannelPid, _Reason) ->
|
||||
ok;
|
||||
cleanup_endpoint(#endpoint{channel_ref = ChannelRef, channel_pid = ChannelPid}, KeepChannelPid, Reason) ->
|
||||
is_reference(ChannelRef) andalso erlang:demonitor(ChannelRef, [flush]),
|
||||
case should_stop_channel(ChannelPid, KeepChannelPid) of
|
||||
true ->
|
||||
catch sdlan_quic_channel:stop(ChannelPid, Reason),
|
||||
ok;
|
||||
false ->
|
||||
ok
|
||||
end.
|
||||
|
||||
should_stop_channel(ChannelPid, KeepChannelPid) when is_pid(ChannelPid), ChannelPid =/= KeepChannelPid ->
|
||||
is_process_alive(ChannelPid);
|
||||
should_stop_channel(_, _) ->
|
||||
false.
|
||||
|
||||
-spec broadcast(Fun :: fun((#endpoint{}) -> term()), ExcludeMacs :: [binary()], Table :: ets:tid()) -> ok.
|
||||
broadcast(Fun, ExcludeMacs, Table) when is_function(Fun, 1), is_list(ExcludeMacs) ->
|
||||
lists:foreach(fun({Mac, Endpoint}) ->
|
||||
case lists:member(Mac, ExcludeMacs) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
Fun(Endpoint)
|
||||
end
|
||||
end, list_endpoints(Table)).
|
||||
|
||||
-spec format_endpoint({Mac :: binary(), Host :: #endpoint{}}) -> map().
|
||||
format_endpoint({Mac, #endpoint{client_id = ClientId, ip = Ip, hole = #hole{peer = {NatIp, NatPort}, nat_type = NatType}, v6_info = V6Info}}) ->
|
||||
HoleMap = #{
|
||||
nat_ip => NatIp,
|
||||
nat_port => NatPort,
|
||||
nat_type => NatType
|
||||
},
|
||||
|
||||
V6InfoMap = case V6Info of
|
||||
undefined ->
|
||||
#{};
|
||||
#'SDLV6Info'{v6 = V6, port = V6Port} ->
|
||||
#{v6 => V6, port => V6Port}
|
||||
end,
|
||||
#{
|
||||
client_id => ClientId,
|
||||
mac => sdlan_util:format_mac(Mac),
|
||||
ip => sdlan_util:int_to_ipv4(Ip),
|
||||
hole_map => HoleMap,
|
||||
v6_info => V6InfoMap
|
||||
};
|
||||
format_endpoint({Mac, #endpoint{client_id = ClientId, ip = Ip, hole = undefined, v6_info = V6Info}}) ->
|
||||
V6InfoMap = case V6Info of
|
||||
undefined ->
|
||||
#{};
|
||||
#'SDLV6Info'{v6 = V6, port = V6Port} ->
|
||||
#{v6 => V6, port => V6Port}
|
||||
end,
|
||||
#{
|
||||
client_id => ClientId,
|
||||
mac => sdlan_util:format_mac(Mac),
|
||||
ip => sdlan_util:int_to_ipv4(Ip),
|
||||
hole_map => undefined,
|
||||
v6_info => V6InfoMap
|
||||
}.
|
||||
|
||||
network_metrics(#state{network_id = NetworkId, endpoint_table = EndpointTable}) ->
|
||||
ProcInfo = maps:from_list(process_info(self(), [message_queue_len, memory, reductions])),
|
||||
ProcInfo#{
|
||||
network_id => NetworkId,
|
||||
endpoint_count => endpoint_count(EndpointTable),
|
||||
channel_metrics => channel_metrics(EndpointTable)
|
||||
}.
|
||||
|
||||
endpoint_count(Table) ->
|
||||
case catch ets:info(Table, size) of
|
||||
Size when is_integer(Size) ->
|
||||
Size;
|
||||
_ ->
|
||||
0
|
||||
end.
|
||||
|
||||
channel_metrics(Table) ->
|
||||
lists:foldl(fun({_Mac, #endpoint{channel_pid = ChannelPid}}, Acc) ->
|
||||
case is_pid(ChannelPid) andalso is_process_alive(ChannelPid) of
|
||||
true ->
|
||||
ProcInfo = maps:from_list(process_info(ChannelPid, [message_queue_len, memory])),
|
||||
[ProcInfo#{pid => ChannelPid} | Acc];
|
||||
false ->
|
||||
Acc
|
||||
end
|
||||
end, [], list_endpoints(Table)).
|
||||
|
||||
-spec select_endpoint(Table :: ets:tid(), MatchSpec :: ets:match_spec()) -> error | {ok, Mac :: binary(), Endpoint :: #endpoint{}}.
|
||||
select_endpoint(Table, MatchSpec) ->
|
||||
case catch ets:select(Table, MatchSpec, 1) of
|
||||
{[], _Continuation} ->
|
||||
error;
|
||||
{[{Mac, Endpoint = #endpoint{}}], _Continuation} ->
|
||||
{ok, Mac, Endpoint};
|
||||
'$end_of_table' ->
|
||||
error;
|
||||
{'EXIT', _} ->
|
||||
error
|
||||
end.
|
||||
|
||||
-spec same_hole(Hole :: #hole{}, Hole :: #hole{}) -> boolean().
|
||||
same_hole(#hole{peer = OldPeer, nat_type = OldNatType}, #hole{peer = Peer, nat_type = NatType}) when OldPeer =:= Peer, OldNatType =:= NatType ->
|
||||
true;
|
||||
same_hole(_, _) ->
|
||||
false.
|
||||
|
||||
-spec gen_session_token() -> binary().
|
||||
gen_session_token() ->
|
||||
Bytes = crypto:strong_rand_bytes(32),
|
||||
base64:encode(Bytes).
|
||||
|
||||
-spec normalization_algorithm(any()) -> binary().
|
||||
normalization_algorithm(<<"aes">>) ->
|
||||
<<"aes">>;
|
||||
normalization_algorithm(<<"chacha20">>) ->
|
||||
<<"chacha20">>;
|
||||
normalization_algorithm(_) ->
|
||||
<<"chacha20">>.
|
||||
|
||||
-spec gen_key(Algorithm :: binary()) -> Key :: binary().
|
||||
gen_key(<<"aes">>) ->
|
||||
sdlan_util:rand_byte(32);
|
||||
gen_key(<<"chacha20">>) ->
|
||||
sdlan_util:rand_byte(32).
|
||||
|
||||
-spec gen_region_id(IpInt :: integer()) -> integer().
|
||||
gen_region_id(IpInt) ->
|
||||
%% 把整数 IP 转成字符串
|
||||
IpStr = integer_to_list(IpInt),
|
||||
%% 拼接盐
|
||||
FullStr = "salt_fG7xQp2BzH9L" ++ IpStr,
|
||||
time33(FullStr, 5381).
|
||||
|
||||
%% 核心Time33算法
|
||||
-spec time33(string(), integer()) -> integer().
|
||||
time33([], Hash) ->
|
||||
Hash band 16#FFFFFFFF; % 32位
|
||||
time33([C|Rest], Hash) ->
|
||||
%% hash = hash * 33 + char
|
||||
NewHash = ((Hash bsl 5) + Hash) + C,
|
||||
time33(Rest, NewHash).
|
||||
@ -1,191 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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([ensure_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.
|
||||
-spec init(Args :: term()) ->
|
||||
{ok, {SupFlags :: supervisor:sup_flags(), [ChildSpec :: supervisor:child_spec()]}}
|
||||
| ignore
|
||||
| {stop, Reason :: term()}.
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||
case sdlan_api:get_all_networks() of
|
||||
{ok, NetworkIds} ->
|
||||
Specs = lists:filtermap(fun load_child_spec/1, NetworkIds),
|
||||
set_network_bind(length(Specs)),
|
||||
|
||||
{ok, {SupFlags, Specs}};
|
||||
{error, Reason} ->
|
||||
logger:warning("[sdlan_network_sup] load all networks error: ~p", [Reason]),
|
||||
{stop, {load_networks_failed, Reason}}
|
||||
end.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec ensure_network_started(Id :: integer()) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||
ensure_network_started(Id) when is_integer(Id) ->
|
||||
case sdlan_network:get_pid(Id) of
|
||||
undefined ->
|
||||
case child_spec(Id) of
|
||||
{ok, Spec} ->
|
||||
start_child(Spec);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
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 child_spec(Id) of
|
||||
{ok, Spec} ->
|
||||
start_child(Spec);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec start_child(Spec :: map()) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||
start_child(Spec) ->
|
||||
case supervisor:start_child(?MODULE, Spec) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{ok, undefined} ->
|
||||
{error, ignored};
|
||||
{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() -> term().
|
||||
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()) -> {ok, map()} | {error, Reason :: any()}.
|
||||
child_spec(Id) when is_integer(Id) ->
|
||||
case load_network_data(Id) of
|
||||
{ok, NetworkData} ->
|
||||
Name = sdlan_network:get_name(Id),
|
||||
{ok, #{
|
||||
id => Name,
|
||||
start => {sdlan_network, start_link, [Name, NetworkData]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['sdlan_network']
|
||||
}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec load_child_spec(Id :: integer()) -> {true, map()} | false.
|
||||
load_child_spec(Id) ->
|
||||
case child_spec(Id) of
|
||||
{ok, Spec} ->
|
||||
{true, Spec};
|
||||
{error, Reason} ->
|
||||
logger:warning("[sdlan_network_sup] skip network: ~p, reason: ~p", [Id, Reason]),
|
||||
false
|
||||
end.
|
||||
|
||||
-spec load_network_data(Id :: integer()) -> {ok, tuple()} | {error, Reason :: any()}.
|
||||
load_network_data(Id) when is_integer(Id) ->
|
||||
case sdlan_api:get_network(Id) of
|
||||
{ok, #{<<"ipaddr">> := Null}} when Null == <<"null">>; Null == <<"NULL">> ->
|
||||
{error, <<"ipaddrr is null">>};
|
||||
{ok, #{<<"id">> := NetworkId}} when NetworkId =/= Id ->
|
||||
{error, {network_id_mismatch, Id, NetworkId}};
|
||||
{ok, NetworkInfo = #{<<"id">> := Id, <<"name">> := Name, <<"domain">> := Domain,
|
||||
<<"algorithm">> := Algorithm, <<"ipaddr">> := IpAddr0, <<"owner_id">> := OwnerId}} ->
|
||||
logger:debug("[sdlan_network_sup] load network info: ~p", [NetworkInfo]),
|
||||
case parse_ipaddr(IpAddr0) of
|
||||
{ok, IpAddr, MaskLen} ->
|
||||
{ok, {network, Id, Name, Domain, IpAddr, MaskLen, OwnerId, Algorithm}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
{ok, NetworkInfo} ->
|
||||
{error, {invalid_network_info, NetworkInfo}};
|
||||
{error, Reason} ->
|
||||
logger:warning("[sdlan_network_sup] load network: ~p, get error: ~p", [Id, Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
%% 解析IpAddr: <<"192.168.172/24">>
|
||||
-spec parse_ipaddr(IpAddr0 :: binary()) -> {ok, IpAddr :: binary(), MaskLen :: integer()} | {error, Reason :: any()}.
|
||||
parse_ipaddr(IpAddr0) when is_binary(IpAddr0) ->
|
||||
try
|
||||
case binary:split(IpAddr0, <<"/">>) of
|
||||
[IpAddr, MaskLen] ->
|
||||
{ok, IpAddr, binary_to_integer(MaskLen)};
|
||||
_ ->
|
||||
{ok, IpAddr0, 24}
|
||||
end
|
||||
catch
|
||||
_:_ ->
|
||||
{error, {invalid_ipaddr, IpAddr0}}
|
||||
end;
|
||||
parse_ipaddr(IpAddr0) ->
|
||||
{error, {invalid_ipaddr, IpAddr0}}.
|
||||
|
||||
set_network_bind(0) ->
|
||||
ok;
|
||||
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).
|
||||
4849
src/sdlan_pb.erl
4849
src/sdlan_pb.erl
File diff suppressed because it is too large
Load Diff
@ -1,379 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%% Transport independent sdlan session logic.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_session).
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
-include("sdlan_pb.hrl").
|
||||
|
||||
%% API
|
||||
-export([new/2, handle_frame/2, handle_timeout/2]).
|
||||
-export([send_event/2, command/4, close/1, debug_info/1]).
|
||||
-export([welcome_packet/2]).
|
||||
-export([test_rules/2]).
|
||||
-export_type([session/0]).
|
||||
|
||||
%% 心跳包监测机制
|
||||
-define(PING_TICKER, 15000).
|
||||
|
||||
%% 注册失败的的错误码
|
||||
|
||||
%% 网络错误
|
||||
-define(NAK_NETWORK_FAULT, 4).
|
||||
%% 内部错误
|
||||
-define(NAK_INTERNAL_FAULT, 5).
|
||||
|
||||
-record(session, {
|
||||
transport :: atom(),
|
||||
status = initialized :: initialized | registered,
|
||||
%% 心跳间隔
|
||||
heartbeat_sec = 15,
|
||||
ping_timer :: undefined | reference(),
|
||||
|
||||
client_id :: undefined | binary(),
|
||||
network_id = 0 :: integer(),
|
||||
%% 网络相关信息id
|
||||
network_pid :: undefined | pid(),
|
||||
%% mac地址
|
||||
mac :: undefined | binary(),
|
||||
ip = 0 :: integer(),
|
||||
|
||||
%% 建立请求和响应的对应关系
|
||||
pkt_id = 1,
|
||||
%% #{pkt_id => {Ref, ReceiverPid}}
|
||||
pending_commands = #{},
|
||||
|
||||
ping_counter = 0,
|
||||
|
||||
%% 离线回调函数
|
||||
offline_cb :: undefined | fun()
|
||||
}).
|
||||
|
||||
-type session() :: #session{}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec new(HeartbeatSec :: integer(), Transport :: atom()) -> #session{}.
|
||||
new(HeartbeatSec, Transport) ->
|
||||
#session{heartbeat_sec = HeartbeatSec, transport = Transport}.
|
||||
|
||||
%% 测试规则函数
|
||||
-spec test_rules(SrcIdentityId :: integer(), DstIdentityId :: integer()) -> binary().
|
||||
test_rules(SrcIdentityId, DstIdentityId) when is_integer(SrcIdentityId), is_integer(DstIdentityId) ->
|
||||
{ok, Rules} = get_rules(SrcIdentityId, DstIdentityId),
|
||||
logger:debug("[sdlan_session] test_rules policy_request src_identity_id: ~p, dst_identity_id: ~p, rules: ~p", [SrcIdentityId, DstIdentityId, Rules]),
|
||||
iolist_to_binary(lists:map(fun({Proto, Port}) -> <<Proto:8, Port:16>> end, Rules)).
|
||||
|
||||
-spec welcome_packet(MaxPacketSize :: integer(), HeartbeatSec :: integer()) -> binary().
|
||||
welcome_packet(MaxPacketSize, HeartbeatSec) ->
|
||||
Ipv6Assist = case application:get_env(sdlan, ipv6_assist_info) of
|
||||
{ok, {V6Bytes, Port}} ->
|
||||
#'SDLV6Info'{v6 = V6Bytes, port = Port};
|
||||
_ ->
|
||||
undefined
|
||||
end,
|
||||
WelcomePkt = sdlan_pb:encode_msg(#'SDLWelcome'{
|
||||
version = 1,
|
||||
max_bidi_streams = 1,
|
||||
max_packet_size = MaxPacketSize,
|
||||
heartbeat_sec = HeartbeatSec,
|
||||
ipv6_assist = Ipv6Assist
|
||||
}),
|
||||
<<?PACKET_WELCOME, WelcomePkt/binary>>.
|
||||
|
||||
-spec handle_frame(Frame :: binary(), Session :: #session{}) ->
|
||||
{ok, StateName :: initialized | registered, NewSession :: #session{}, Packets :: [binary()]} |
|
||||
{stop, Reason :: term(), StateName :: initialized | registered, NewSession :: #session{}, Packets :: [binary()]}.
|
||||
handle_frame(<<?PACKET_REGISTER_SUPER, Body/binary>>, Session = #session{status = initialized, transport = Transport}) ->
|
||||
#'SDLRegisterSuper'{
|
||||
client_id = ClientId, network_id = NetworkId, mac = Mac, ip = Ip, mask_len = MaskLen,
|
||||
hostname = HostName, pub_key = PubKey, access_token = AccessToken} = sdlan_pb:decode_msg(Body, 'SDLRegisterSuper'),
|
||||
|
||||
true = (Mac =/= <<>> andalso PubKey =/= <<>> andalso ClientId =/= <<>>),
|
||||
%% Mac地址不能是广播地址
|
||||
true = not (sdlan_util:is_multicast_mac(Mac) orelse sdlan_util:is_broadcast_mac(Mac)),
|
||||
|
||||
MacBinStr = sdlan_util:format_mac(Mac),
|
||||
IpAddr = sdlan_util:int_to_ipv4(Ip),
|
||||
Params = #{
|
||||
<<"network_id">> => NetworkId,
|
||||
<<"client_id">> => ClientId,
|
||||
<<"mac">> => MacBinStr,
|
||||
<<"ip">> => IpAddr,
|
||||
<<"mask_len">> => MaskLen,
|
||||
<<"hostname">> => HostName,
|
||||
<<"access_token">> => AccessToken
|
||||
},
|
||||
%% 参数检查
|
||||
logger:debug("[sdlan_session] transport: ~p, client_id: ~p, ip: ~p, mac: ~p, host_name: ~p, access_token: ~p, network_id: ~p",
|
||||
[Transport, ClientId, Ip, Mac, HostName, AccessToken, NetworkId]),
|
||||
|
||||
case sdlan_api:auth_access_token(Params) of
|
||||
{ok, #{<<"result">> := <<"ok">>}} ->
|
||||
%% 建立到network的对应关系
|
||||
case sdlan_network:get_pid(NetworkId) of
|
||||
NetworkPid when is_pid(NetworkPid) ->
|
||||
{ok, Algorithm, Key, RegionId, SessionToken} = sdlan_network:attach(NetworkPid, self(), ClientId, Mac, Ip, HostName),
|
||||
RsaPubKey = sdlan_cipher:rsa_pem_decode(PubKey),
|
||||
RegisterSuperAck = sdlan_pb:encode_msg(#'SDLRegisterSuperAck'{
|
||||
algorithm = Algorithm,
|
||||
key = rsa_encode(Key, RsaPubKey),
|
||||
region_id = RegionId,
|
||||
session_token = SessionToken
|
||||
}),
|
||||
|
||||
%% 设置节点的在线状态
|
||||
Result = sdlan_api:set_node_status(#{
|
||||
<<"network_id">> => NetworkId,
|
||||
<<"client_id">> => ClientId,
|
||||
<<"access_token">> => AccessToken,
|
||||
<<"status">> => 1
|
||||
}),
|
||||
logger:debug("[sdlan_session] tranport: ~p, client_id: ~p, set none online result is: ~p", [Transport, ClientId, Result]),
|
||||
|
||||
OfflineCb = fun() ->
|
||||
sdlan_api:set_node_status(#{
|
||||
<<"network_id">> => NetworkId,
|
||||
<<"client_id">> => ClientId,
|
||||
<<"access_token">> => AccessToken,
|
||||
<<"status">> => 0
|
||||
})
|
||||
end,
|
||||
NSession = schedule_ping(Session#session{
|
||||
status = registered,
|
||||
network_id = NetworkId,
|
||||
network_pid = NetworkPid,
|
||||
client_id = ClientId,
|
||||
mac = Mac,
|
||||
ip = Ip,
|
||||
offline_cb = OfflineCb
|
||||
}),
|
||||
ok_result(NSession, [<<?PACKET_REGISTER_SUPER_ACK, RegisterSuperAck/binary>>]);
|
||||
undefined ->
|
||||
logger:warning("[sdlan_session] transport: ~p, client_id: ~p, register get error: network not found", [Transport, ClientId]),
|
||||
stop_result(normal, Session, [register_nak_reply(?NAK_INTERNAL_FAULT, <<"Internal Error">>)])
|
||||
end;
|
||||
{ok, #{<<"error">> := #{<<"code">> := Code, <<"message">> := Message}}} ->
|
||||
logger:warning("[sdlan_session] transport: ~p, network_id: ~p, client_id: ~p, register get error: ~ts, error_code: ~p", [Transport, NetworkId, ClientId, Message, Code]),
|
||||
stop_result(normal, Session, [register_nak_reply(Code, Message)]);
|
||||
{error, Reason} ->
|
||||
logger:warning("[sdlan_session] transport: ~p, network_id: ~p, client_id: ~p, register get error: ~p", [Transport, NetworkId, ClientId, Reason]),
|
||||
stop_result(normal, Session, [register_nak_reply(?NAK_NETWORK_FAULT, <<"Network Error">>)])
|
||||
end;
|
||||
|
||||
handle_frame(<<?PACKET_QUERY_INFO, Body/binary>>, Session = #session{status = registered, network_pid = NetworkPid, mac = SrcMac, transport = Transport}) when is_pid(NetworkPid) ->
|
||||
#'SDLQueryInfo'{dst_mac = DstMac} = sdlan_pb:decode_msg(Body, 'SDLQueryInfo'),
|
||||
case sdlan_network:peer_info(NetworkPid, SrcMac, DstMac) of
|
||||
error ->
|
||||
logger:debug("[sdlan_session] transport: ~p, query_info src_mac is: ~p, dst_mac: ~p, nat_peer not found",
|
||||
[Transport, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
|
||||
|
||||
EmptyResponse = sdlan_pb:encode_msg(#'SDLPeerInfo'{
|
||||
dst_mac = DstMac,
|
||||
v4_info = undefined,
|
||||
v6_info = undefined
|
||||
}),
|
||||
ok_result(Session, [<<?PACKET_PEER_INFO, EmptyResponse/binary>>]);
|
||||
{ok, {NatPeer = {{Ip0, Ip1, Ip2, Ip3}, NatPort}, NatType}, V6Info} ->
|
||||
logger:debug("[sdlan_session] transport: ~p, query_info src_mac is: ~p, dst_mac: ~p, nat_peer: ~p",
|
||||
[Transport, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac), NatPeer]),
|
||||
|
||||
PeerInfo = sdlan_pb:encode_msg(#'SDLPeerInfo'{
|
||||
dst_mac = DstMac,
|
||||
v4_info = #'SDLV4Info' {
|
||||
port = NatPort,
|
||||
v4 = <<Ip0, Ip1, Ip2, Ip3>>,
|
||||
nat_type = NatType
|
||||
},
|
||||
v6_info = V6Info
|
||||
}),
|
||||
ok_result(Session, [<<?PACKET_PEER_INFO, PeerInfo/binary>>])
|
||||
end;
|
||||
|
||||
%% arp查询
|
||||
handle_frame(<<?PACKET_ARP_REQUEST, Body/binary>>,
|
||||
Session = #session{status = registered, network_id = NetworkId, network_pid = NetworkPid, transport = Transport}) when is_pid(NetworkPid) ->
|
||||
|
||||
#'SDLArpRequest'{target_ip = TargetIp, origin_ip = OriginIp, context = Context} = sdlan_pb:decode_msg(Body, 'SDLArpRequest'),
|
||||
case sdlan_network:arp_request(NetworkPid, TargetIp) of
|
||||
error ->
|
||||
logger:debug("[sdlan_session] transport: ~p, network: ~p, arp_request target_ip: ~p, mac not found",
|
||||
[Transport, NetworkId, sdlan_util:int_to_ipv4(TargetIp)]),
|
||||
|
||||
EmptyArpResponsePkt = sdlan_pb:encode_msg(#'SDLArpResponse'{
|
||||
target_ip = TargetIp,
|
||||
target_mac = <<>>,
|
||||
origin_ip = OriginIp,
|
||||
context = Context
|
||||
}),
|
||||
ok_result(Session, [<<?PACKET_ARP_RESPONSE, EmptyArpResponsePkt/binary>>]);
|
||||
{ok, Mac} ->
|
||||
logger:debug("[sdlan_session] transport: ~p, network: ~p, arp_request target_ip: ~p, mac: ~p",
|
||||
[Transport, NetworkId, sdlan_util:int_to_ipv4(TargetIp), sdlan_util:format_mac(Mac)]),
|
||||
ArpResponsePkt = sdlan_pb:encode_msg(#'SDLArpResponse'{
|
||||
target_ip = TargetIp,
|
||||
target_mac = Mac,
|
||||
origin_ip = OriginIp,
|
||||
context = Context
|
||||
}),
|
||||
ok_result(Session, [<<?PACKET_ARP_RESPONSE, ArpResponsePkt/binary>>])
|
||||
end;
|
||||
|
||||
handle_frame(<<?PACKET_POLICY_REQUEST, Body/binary>>, Session = #session{status = registered, network_pid = NetworkPid, transport = Transport}) when is_pid(NetworkPid) ->
|
||||
Packets = maybe
|
||||
#'SDLPolicyRequest'{src_identity_id = SrcIdentityId, dst_identity_id = DstIdentityId, version = Version} ?= sdlan_pb:decode_msg(Body, 'SDLPolicyRequest'),
|
||||
|
||||
{ok, Rules} = get_rules(SrcIdentityId, DstIdentityId),
|
||||
logger:debug("[sdlan_session] transport: ~p, policy_request src_identity_id: ~p, dst_identity_id: ~p, rules: ~p",
|
||||
[Transport, SrcIdentityId, DstIdentityId, Rules]),
|
||||
|
||||
RuleBin = iolist_to_binary(lists:map(fun({Proto, Port}) -> <<Proto:8, Port:16>> end, Rules)),
|
||||
PolicyResponsePkt = sdlan_pb:encode_msg(#'SDLPolicyResponse'{
|
||||
src_identity_id = SrcIdentityId,
|
||||
dst_identity_id = DstIdentityId,
|
||||
version = Version,
|
||||
rules = RuleBin
|
||||
}),
|
||||
[<<?PACKET_POLICY_REPLY, PolicyResponsePkt/binary>>]
|
||||
else _ ->
|
||||
[]
|
||||
end,
|
||||
ok_result(Session, Packets);
|
||||
|
||||
%% 处理命令的响应逻辑
|
||||
handle_frame(<<?PACKET_COMMAND_ACK, Body/binary>>, Session = #session{status = registered, pending_commands = PendingCommands, transport = Transport}) ->
|
||||
logger:debug("[sdlan_session] transport: ~p command_ack", [Transport]),
|
||||
maybe
|
||||
CommandAck = sdlan_pb:decode_msg(Body, 'SDLCommandAck'),
|
||||
#'SDLCommandAck'{pkt_id = PktId} ?= CommandAck,
|
||||
|
||||
{{Ref, ReceiverPid}, RestPendingCommands} ?= maps:take(PktId, PendingCommands),
|
||||
case is_process_alive(ReceiverPid) of
|
||||
true ->
|
||||
ReceiverPid ! {quic_command_ack, Ref, CommandAck};
|
||||
false ->
|
||||
ok
|
||||
end,
|
||||
ok_result(Session#session{pending_commands = RestPendingCommands}, [])
|
||||
else _ ->
|
||||
ok_result(Session, [])
|
||||
end;
|
||||
|
||||
handle_frame(<<?PACKET_PING>>, Session = #session{ping_counter = PingCounter, client_id = ClientId, transport = Transport}) ->
|
||||
logger:warning("[sdlan_session] transport: ~p, get ping: ~p", [Transport, ClientId]),
|
||||
ok_result(Session#session{ping_counter = PingCounter + 1}, [<<?PACKET_PONG>>]);
|
||||
|
||||
%% 取消注册
|
||||
handle_frame(<<?PACKET_UNREGISTER>>, Session = #session{status = registered, client_id = ClientId, mac = Mac, network_pid = NetworkPid, transport = Transport})
|
||||
when is_pid(NetworkPid) ->
|
||||
|
||||
logger:warning("[sdlan_session] transport: ~p, unregister client_id: ~p", [Transport, ClientId]),
|
||||
sdlan_network:unregister(NetworkPid, ClientId, Mac),
|
||||
stop_result(normal, Session, []);
|
||||
|
||||
handle_frame(Frame, Session = #session{transport = Transport}) ->
|
||||
logger:notice("[sdlan_session] transport: ~p, unexpected frame: ~p, status: ~p", [Transport, Frame, Session#session.status]),
|
||||
ok_result(Session, []).
|
||||
|
||||
-spec handle_timeout(TimerRef :: reference(), Session :: #session{}) ->
|
||||
{ok, StateName :: initialized | registered, NewSession :: #session{}} |
|
||||
{stop, Reason :: term(), StateName :: initialized | registered, NewSession :: #session{}}.
|
||||
handle_timeout(TimerRef, Session = #session{ping_timer = TimerRef, client_id = ClientId, ping_counter = PingCounter, transport = Transport}) ->
|
||||
case PingCounter > 0 of
|
||||
true ->
|
||||
ok_timeout_result(schedule_ping(Session#session{ping_counter = 0, ping_timer = undefined}));
|
||||
false ->
|
||||
logger:debug("[sdlan_session] transport: ~p, client_id: ~p, ping losted", [Transport, ClientId]),
|
||||
stop_timeout_result(heartbeat_timeout, Session#session{ping_counter = 0, ping_timer = undefined})
|
||||
end;
|
||||
handle_timeout(_TimerRef, Session) ->
|
||||
ok_timeout_result(Session).
|
||||
|
||||
-spec send_event(Event :: binary(), Session :: #session{}) ->
|
||||
{ok, StateName :: initialized | registered, NewSession :: #session{}, Packets :: [binary()]} | {error, not_registered}.
|
||||
send_event(Event, Session = #session{status = registered}) when is_binary(Event) ->
|
||||
ok_result(Session, [<<?PACKET_EVENT, Event/binary>>]);
|
||||
send_event(_Event, _Session) ->
|
||||
{error, not_registered}.
|
||||
|
||||
-spec command(Ref :: reference(), ReceiverPid :: pid(), {Tag :: atom(), SubCommand :: any()}, Session :: #session{}) ->
|
||||
{ok, StateName :: initialized | registered, NewSession :: #session{}, Packets :: [binary()]} | {error, not_registered}.
|
||||
command(Ref, ReceiverPid, SubCommand, Session = #session{status = registered, pkt_id = PktId, pending_commands = PendingCommands, client_id = ClientId, transport = Transport})
|
||||
when is_reference(Ref), is_pid(ReceiverPid) ->
|
||||
CommandPkt = sdlan_pb:encode_msg(#'SDLCommand'{
|
||||
pkt_id = PktId,
|
||||
command = SubCommand
|
||||
}),
|
||||
logger:debug("[sdlan_session] transport: ~p, client_id: ~p, will send Command: ~p", [Transport, ClientId, SubCommand]),
|
||||
|
||||
ok_result(Session#session{pkt_id = PktId + 1, pending_commands = maps:put(PktId, {Ref, ReceiverPid}, PendingCommands)},
|
||||
[<<?PACKET_COMMAND, CommandPkt/binary>>]);
|
||||
command(_Ref, _ReceiverPid, _SubCommand, _Session) ->
|
||||
{error, not_registered}.
|
||||
|
||||
-spec close(Session :: #session{}) -> ok.
|
||||
close(#session{offline_cb = OfflineCb}) ->
|
||||
%% 触发客户端的离线逻辑
|
||||
is_function(OfflineCb) andalso OfflineCb(),
|
||||
ok.
|
||||
|
||||
-spec debug_info(Session :: #session{}) -> map().
|
||||
debug_info(#session{ status = Status, client_id = ClientId, network_id = NetworkId, mac = Mac, ip = Ip,
|
||||
pending_commands = PendingCommands, heartbeat_sec = HeartbeatSec}) ->
|
||||
|
||||
#{
|
||||
session_state => Status,
|
||||
client_id => ClientId,
|
||||
network_id => NetworkId,
|
||||
mac => Mac,
|
||||
ip => Ip,
|
||||
pending_commands => maps:size(PendingCommands),
|
||||
heartbeat_sec => HeartbeatSec
|
||||
}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec register_nak_reply(ErrorCode :: integer(), ErrorMsg :: binary()) -> binary().
|
||||
register_nak_reply(ErrorCode, ErrorMsg) when is_integer(ErrorCode), is_binary(ErrorMsg) ->
|
||||
RegisterNakReply = sdlan_pb:encode_msg(#'SDLRegisterSuperNak'{
|
||||
error_code = ErrorCode,
|
||||
error_message = ErrorMsg
|
||||
}),
|
||||
<<?PACKET_REGISTER_SUPER_NAK, RegisterNakReply/binary>>.
|
||||
|
||||
rsa_encode(PlainText, RsaPubKey) when is_binary(PlainText) ->
|
||||
iolist_to_binary(sdlan_cipher:rsa_encrypt(PlainText, RsaPubKey)).
|
||||
|
||||
-spec get_rules(SrcIdentityId :: integer(), DstIdentityId :: integer()) -> {ok, [{Proto :: integer(), Port :: integer()}]}.
|
||||
get_rules(SrcIdentityId, DstIdentityId) when is_integer(SrcIdentityId), is_integer(DstIdentityId) ->
|
||||
SrcPolicyIds = identity_policy_ets:get_policies(SrcIdentityId),
|
||||
DstPolicyIds = identity_policy_ets:get_policies(DstIdentityId),
|
||||
rule_ets:get_rules(SrcPolicyIds, DstPolicyIds).
|
||||
|
||||
schedule_ping(Session = #session{heartbeat_sec = HeartbeatSec}) ->
|
||||
Session#session{ping_timer = erlang:start_timer(heartbeat_ms(HeartbeatSec), self(), ping_ticker)}.
|
||||
|
||||
ok_result(Session = #session{status = StateName}, Packets) ->
|
||||
{ok, StateName, Session, Packets}.
|
||||
|
||||
stop_result(Reason, Session = #session{status = StateName}, Packets) ->
|
||||
{stop, Reason, StateName, Session, Packets}.
|
||||
|
||||
ok_timeout_result(Session = #session{status = StateName}) ->
|
||||
{ok, StateName, Session}.
|
||||
|
||||
stop_timeout_result(Reason, Session = #session{status = StateName}) ->
|
||||
{stop, Reason, StateName, Session}.
|
||||
|
||||
heartbeat_ms(HeartbeatSec) when is_integer(HeartbeatSec), HeartbeatSec > 0 ->
|
||||
HeartbeatSec * 1000;
|
||||
heartbeat_ms(_) ->
|
||||
?PING_TICKER.
|
||||
@ -1,198 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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).
|
||||
-define(FLOW_REPORT_INTERVAL, 60 * 1000).
|
||||
|
||||
-record(state, {
|
||||
socket,
|
||||
flow_counters = #{}
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% 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, high),
|
||||
Opts = [
|
||||
binary,
|
||||
{reuseaddr, true},
|
||||
{reuseport, true},
|
||||
{active, true},
|
||||
{recbuf, 5 * 1024 * 1024},
|
||||
{sndbuf, 5 * 1024 * 1024}
|
||||
],
|
||||
{ok, Socket} = gen_udp:open(Port, Opts),
|
||||
inet_udp:controlling_process(Socket, self()),
|
||||
erlang:start_timer(?FLOW_REPORT_INTERVAL, self(), flow_report_ticker),
|
||||
|
||||
logger:debug("[sdlan_stun] start at port: ~p", [Port]),
|
||||
{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{}}).
|
||||
%% 当前node下的转发,基于进程间的通讯
|
||||
handle_cast(_Request, 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, <<?PACKET_STUN_REQUEST:8, Body/binary>>}, State = #state{socket = Sock}) ->
|
||||
StunRequest = catch sdlan_pb:decode_msg(Body, 'SDLStunRequest'),
|
||||
%% 告知网络当前的ip对应的nat的映射关系
|
||||
maybe
|
||||
#'SDLStunRequest'{session_token = SessionToken, client_id = ClientId, network_id = NetworkId, mac = Mac, nat_type = NatType, v6_info = V6Info} ?= StunRequest,
|
||||
{ok, NetworkPid} ?= sdlan_network:lookup_pid(NetworkId),
|
||||
sdlan_network:update_hole(NetworkPid, SessionToken, ClientId, Mac, {Ip, Port}, NatType, V6Info),
|
||||
StunReply = sdlan_pb:encode_msg(#'SDLStunReply'{}),
|
||||
ok = gen_udp:send(Sock, Ip, Port, <<?PACKET_STUN_REPLY:8, StunReply/binary>>),
|
||||
logger:debug("[sdlan_stun] stun_request network_id: ~p, client_id: ~p, mac: ~p, hole: ~p, replied", [NetworkId, ClientId, sdlan_util:format_mac(Mac), {Ip, Port}])
|
||||
end,
|
||||
{noreply, State};
|
||||
|
||||
%% 网络nat类型的探测机制, 需要借助其他服务一起才能实现
|
||||
%% 辅助节点没有assist的配置,不支持attr = 2的探测
|
||||
handle_info({udp, Sock, ClientIp, ClientPort, <<?PACKET_STUN_PROBE:8, Body/binary>>}, State = #state{socket = Sock}) ->
|
||||
StunProbe = catch sdlan_pb:decode_msg(Body, 'SDLStunProbe'),
|
||||
maybe
|
||||
#'SDLStunProbe'{cookie = Cookie, attr = Attr} ?= StunProbe,
|
||||
logger:debug("[sdlan_stun] get stun_probe request, att: ~p", [Attr]),
|
||||
ProbeReplyPkt = sdlan_pb:encode_msg(#'SDLStunProbeReply' {
|
||||
cookie = Cookie,
|
||||
port = ClientPort,
|
||||
ip = int_ip(ClientIp)
|
||||
}),
|
||||
ProbeReply = <<?PACKET_STUN_PROBE_REPLY, ProbeReplyPkt/binary>>,
|
||||
case Attr of
|
||||
?STUN_ATTR_CHANGE_NONE ->
|
||||
ok = gen_udp:send(Sock, ClientIp, ClientPort, ProbeReply);
|
||||
?STUN_ATTR_CHANGE_PORT ->
|
||||
%% 切换端口和ip
|
||||
sdlan_stun_peer_assist:stun_relay(ClientIp, ClientPort, ProbeReply);
|
||||
?STUN_ATTR_CHANGE_PEER ->
|
||||
%% 切换端口返回
|
||||
sdlan_stun_port_assist:stun_relay(ClientIp, ClientPort, ProbeReply)
|
||||
end
|
||||
end,
|
||||
{noreply, State};
|
||||
|
||||
handle_info({udp, _, _Ip, _Port, <<?PACKET_STUN_DATA, Body/binary>>}, State = #state{socket = Sock, flow_counters = FlowCounters}) ->
|
||||
Data = catch sdlan_pb:decode_msg(Body, 'SDLData'),
|
||||
NFlowCounters = maybe
|
||||
#'SDLData'{network_id = NetworkId, src_mac = SrcMac, dst_mac = DstMac, ttl = TTL} ?= Data,
|
||||
logger:debug("[sdlan_stun] forward data, network_id: ~p", [NetworkId]),
|
||||
%% 重新打包数据ttl需要减1
|
||||
NData = sdlan_pb:encode_msg(Data#'SDLData'{ttl = TTL - 1, is_p2p = false}),
|
||||
case sdlan_network:forward_by_ets(NetworkId, Sock, SrcMac, DstMac, <<?PACKET_STUN_DATA, NData/binary>>) of
|
||||
{ok, ForwardBytes} ->
|
||||
incr_flow_counter(NetworkId, ForwardBytes, FlowCounters);
|
||||
{error, _Reason} ->
|
||||
FlowCounters
|
||||
end
|
||||
else _ ->
|
||||
FlowCounters
|
||||
end,
|
||||
{noreply, State#state{flow_counters = NFlowCounters}};
|
||||
|
||||
handle_info({timeout, _, flow_report_ticker}, State = #state{flow_counters = FlowCounters}) ->
|
||||
erlang:start_timer(?FLOW_REPORT_INTERVAL, self(), flow_report_ticker),
|
||||
maps:foreach(fun(NetworkId, ForwardBytes) ->
|
||||
catch sdlan_api:network_forward_report(NetworkId, ForwardBytes)
|
||||
end, FlowCounters),
|
||||
{noreply, State#state{flow_counters = #{}}};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
logger: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.
|
||||
|
||||
-spec incr_flow_counter(NetworkId :: integer(), ForwardBytes :: integer(), Counters :: map()) -> map().
|
||||
incr_flow_counter(NetworkId, ForwardBytes, Counters) when is_integer(NetworkId), is_integer(ForwardBytes), is_map(Counters) ->
|
||||
maps:update_with(NetworkId, fun(V) -> V + ForwardBytes end, ForwardBytes, Counters).
|
||||
@ -1,117 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 11. 2月 2026 23:00
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_sync_mysql).
|
||||
-author("anlicheng").
|
||||
-include("policy.hrl").
|
||||
|
||||
-behaviour(gen_statem).
|
||||
|
||||
%% 心跳包监测机制
|
||||
-define(PING_TICKER, 15000).
|
||||
|
||||
%% 注册失败的的错误码
|
||||
|
||||
%% 网络错误
|
||||
-define(NAK_NETWORK_FAULT, 4).
|
||||
%% 内部错误
|
||||
-define(NAK_INTERNAL_FAULT, 5).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
|
||||
%% gen_statem callbacks
|
||||
-export([init/1, handle_event/4, terminate/3, code_change/4, callback_mode/0]).
|
||||
|
||||
-record(state, {
|
||||
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Creates a gen_statem process which calls Module:init/1 to
|
||||
%% initialize. To ensure a synchronized start-up procedure, this
|
||||
%% function does not return until Module:init/1 has returned.
|
||||
-spec start_link() -> gen_statem:start_ret().
|
||||
start_link() ->
|
||||
gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_statem callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
|
||||
%% gen_statem:start_link/[3,4], this function is called by the new
|
||||
%% process to initialize.
|
||||
-spec init(Args :: term()) -> gen_statem:init_result(atom(), #state{}).
|
||||
init([]) ->
|
||||
{ok, initializing, #state{}, [{next_event, internal, do_sync}]}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_statem when it needs to find out
|
||||
%% the callback mode of the callback module.
|
||||
-spec callback_mode() -> gen_statem:callback_mode_result().
|
||||
callback_mode() ->
|
||||
handle_event_function.
|
||||
|
||||
%% @private
|
||||
%% @doc If callback_mode is handle_event_function, then whenever a
|
||||
%% gen_statem receives an event from call/2, cast/2, or as a normal
|
||||
%% process message, this function is called.
|
||||
|
||||
-spec handle_event(EventType :: gen_statem:event_type(), EventContent :: term(),
|
||||
StateName :: atom(), State :: #state{}) -> gen_statem:event_handler_result(atom(), #state{}).
|
||||
handle_event(internal, do_sync, initializing, State=#state{}) ->
|
||||
sync_identity_policy(),
|
||||
sync_rule(),
|
||||
{next_state, initialized, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_statem 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_statem terminates with
|
||||
%% Reason. The return value is ignored.
|
||||
-spec terminate(Reason :: term(), StateName :: atom(), State :: #state{}) -> ok.
|
||||
terminate(Reason, _StateName, _State) ->
|
||||
logger:debug("[sdlan_sync_mysql] terminate with reason: ~p", [Reason]),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec code_change(OldVsn :: term(), StateName :: atom(), State :: #state{}, Extra :: term()) ->
|
||||
{ok, atom(), #state{}}.
|
||||
code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
|
||||
{ok, StateName, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
sync_identity_policy() ->
|
||||
{ok, Rows} = mysql_pool:get_all(mysql_sdlan, <<"select * from identity_policy">>),
|
||||
lists:map(fun(R) -> identity_policy_ets:insert(R) end, Rows).
|
||||
|
||||
sync_rule() ->
|
||||
sync_rule0(0).
|
||||
sync_rule0(RuleIdOffset) ->
|
||||
{ok, Rows} = mysql_pool:get_all(mysql_sdlan, <<"select * from rule where rule_id > ? order by rule_id asc limit 5000">>, [RuleIdOffset]),
|
||||
logger:debug("rule rows: ~p", [Rows]),
|
||||
case length(Rows) > 0 of
|
||||
true ->
|
||||
RuleIds = lists:map(fun(R = #{<<"rule_id">> := RuleId}) ->
|
||||
rule_ets:insert(R),
|
||||
RuleId
|
||||
end, Rows),
|
||||
LastRuleOffset = lists:max(RuleIds),
|
||||
sync_rule0(LastRuleOffset);
|
||||
false ->
|
||||
ok
|
||||
end.
|
||||
@ -1,183 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @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, mac_str_to_bin/1]).
|
||||
-export([json_data/1, json_error/2]).
|
||||
-export([is_broadcast_mac/1, is_multicast_mac/1]).
|
||||
-export([ipv4_to_int/1, int_to_ipv4/1, ips/2, format_ip/1]).
|
||||
-export([ipv6_to_bytes/1, ipv6_bytes_to_binary/1, ipv6_assist_info/0]).
|
||||
-export([hmac/2, term_to_binary/1]).
|
||||
-export([certificate_config/0]).
|
||||
|
||||
-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))).
|
||||
|
||||
-spec mac_str_to_bin(MacBin :: binary()) -> binary().
|
||||
mac_str_to_bin(MacBin) when is_binary(MacBin) ->
|
||||
% 过滤掉 : 和 -,只保留十六进制字符
|
||||
HexBin = binary:replace(MacBin, <<":">>, <<>>, [global]),
|
||||
HexBin2 = binary:replace(HexBin, <<"-">>, <<>>, [global]),
|
||||
% 解码为 6 字节 MAC 二进制
|
||||
binary:decode_hex(HexBin2).
|
||||
|
||||
%% 生成随机字节
|
||||
-spec rand_byte(Num :: pos_integer()) -> binary().
|
||||
rand_byte(Num) when is_integer(Num), Num > 0 ->
|
||||
crypto:strong_rand_bytes(Num).
|
||||
|
||||
%% 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.
|
||||
|
||||
-spec json_data(Data :: term()) -> iodata().
|
||||
json_data(Data) ->
|
||||
json:encode(#{<<"result">> => Data}).
|
||||
|
||||
-spec json_error(ErrCode :: integer(), ErrMessage :: binary()) -> iodata().
|
||||
json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) ->
|
||||
json:encode(#{<<"error">> => #{<<"code">> => ErrCode, <<"message">> => ErrMessage}}).
|
||||
|
||||
-spec assert_call(Condition :: boolean(), F :: fun(() -> T)) -> T | ok.
|
||||
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>>.
|
||||
|
||||
|
||||
-spec format_ip(Ip :: term()) -> term().
|
||||
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_to_bytes(Address :: binary() | string() | inet:ip6_address()) -> binary().
|
||||
ipv6_to_bytes(Address) when is_binary(Address) ->
|
||||
ipv6_to_bytes(binary_to_list(Address));
|
||||
ipv6_to_bytes({A, B, C, D, E, F, G, H}) ->
|
||||
<<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>;
|
||||
ipv6_to_bytes(Address) when is_list(Address) ->
|
||||
case inet:parse_ipv6strict_address(string:trim(Address)) of
|
||||
{ok, Ip6Address} ->
|
||||
ipv6_to_bytes(Ip6Address);
|
||||
{error, _} ->
|
||||
<<"">>
|
||||
end;
|
||||
ipv6_to_bytes(_) ->
|
||||
<<"">>.
|
||||
|
||||
-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(_) ->
|
||||
<<"">>.
|
||||
|
||||
-spec ipv6_assist_info() -> undefined | {ok, {binary(), integer()}}.
|
||||
ipv6_assist_info() ->
|
||||
case application:get_env(sdlan, ipv6_assist) of
|
||||
{ok, Props} ->
|
||||
Port = proplists:get_value(port, Props, 0),
|
||||
GlobalAddr6 = proplists:get_value(global_addr6, Props, <<"">>),
|
||||
case {Port, ipv6_to_bytes(GlobalAddr6)} of
|
||||
{Port0, V6Bytes = <<_:128>>} when is_integer(Port0), Port0 > 0 ->
|
||||
{ok, {V6Bytes, Port0}};
|
||||
_ ->
|
||||
undefined
|
||||
end;
|
||||
_ ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec hmac(Key :: binary(), Data :: binary()) -> string().
|
||||
hmac(Key, Data) when is_binary(Key), is_binary(Data) ->
|
||||
Digest = crypto:mac(hmac, md5, Key, Data),
|
||||
%% 转成十六进制小写字符串,和 PHP hash_hmac 一致
|
||||
lists:flatten([io_lib:format("~2.16.0b", [B]) || B <- binary:bin_to_list(Digest)]).
|
||||
|
||||
-spec term_to_binary(Term :: term()) -> binary().
|
||||
term_to_binary(Term) ->
|
||||
iolist_to_binary(io_lib:format("~p", [Term])).
|
||||
|
||||
-spec certificate_config() -> {CertFile :: string(), KeyFile :: string()}.
|
||||
certificate_config() ->
|
||||
{ok, Props} = application:get_env(sdlan, certificate),
|
||||
CertFile = proplists:get_value(certfile, Props),
|
||||
KeyFile = proplists:get_value(keyfile, Props),
|
||||
|
||||
%% 获取环境变量
|
||||
case os:getenv("QUIC_CERT_PATH") of
|
||||
false ->
|
||||
{CertFile, KeyFile};
|
||||
Path ->
|
||||
{Path ++ "/" ++ CertFile, Path ++ "/" ++ KeyFile}
|
||||
end.
|
||||
@ -1,52 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 03. 5月 2026 13:31
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_ssl_server).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([start/0]).
|
||||
|
||||
%% 启动ssl服务
|
||||
-spec start() -> ok.
|
||||
start() ->
|
||||
{ok, Props} = application:get_env(sdlan, ssl_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),
|
||||
Limits = proplists:get_value(limits, Props, []),
|
||||
|
||||
%% 证书配置
|
||||
{CertFile, KeyFile} = sdlan_util:certificate_config(),
|
||||
|
||||
TransOpts = #{
|
||||
max_connections => MaxConnections,
|
||||
num_acceptors => Acceptors,
|
||||
shutdown => 2000,
|
||||
socket_opts => [
|
||||
binary,
|
||||
{active, false},
|
||||
{nodelay, true},
|
||||
{backlog, Backlog},
|
||||
{port, Port},
|
||||
{send_timeout, 30000},
|
||||
{send_timeout_close, true},
|
||||
{certfile, CertFile},
|
||||
{keyfile, KeyFile},
|
||||
{verify, verify_none},
|
||||
{alpn_preferred_protocols, [<<"punchnet/1.0">>]},
|
||||
{versions, ['tlsv1.3', 'tlsv1.2']},
|
||||
{secure_renegotiate, true},
|
||||
{reuse_sessions, true},
|
||||
{honor_cipher_order, true},
|
||||
{client_renegotiation, false}
|
||||
]
|
||||
},
|
||||
{ok, Pid} = ranch:start_listener(ssl_server, ranch_ssl, TransOpts, sdlan_ssl_transport, Limits),
|
||||
logger:debug("[sdlan_ssl_server] the ssl server start at: ~p, pid is: ~p", [Port, Pid]).
|
||||
@ -1,186 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%% SSL transport for sdlan sessions.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(sdlan_ssl_transport).
|
||||
-author("anlicheng").
|
||||
-include("sdlan.hrl").
|
||||
-include("sdlan_pb.hrl").
|
||||
|
||||
-behaviour(gen_statem).
|
||||
-behaviour(ranch_protocol).
|
||||
|
||||
%% Ranch protocol callback
|
||||
-export([start_link/4]).
|
||||
|
||||
%% gen_statem callbacks
|
||||
-export([init/1, handle_event/4, terminate/3, code_change/4, callback_mode/0]).
|
||||
|
||||
-record(state, {
|
||||
ref :: ranch:ref(),
|
||||
socket :: undefined | ssl:sslsocket(),
|
||||
transport :: module(),
|
||||
|
||||
max_packet_size = 16384,
|
||||
heartbeat_sec = 15,
|
||||
|
||||
session :: sdlan_session:session(),
|
||||
|
||||
close_reason = undefined
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Ranch protocol callback
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_link(Ref :: ranch:ref(), Socket :: inet:socket(), Transport :: module(),
|
||||
Limits :: proplists:proplist()) -> gen_statem:start_ret().
|
||||
start_link(Ref, Socket, Transport, Limits) ->
|
||||
gen_statem:start_link(?MODULE, [Ref, Socket, Transport, Limits], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_statem callbacks
|
||||
%%%===================================================================
|
||||
|
||||
-spec init(Args :: term()) -> gen_statem:init_result(atom(), #state{}).
|
||||
init([Ref, Socket, Transport, Limits]) ->
|
||||
MaxPacketSize = proplists:get_value(max_packet_size, Limits, 16384),
|
||||
HeartbeatSec = proplists:get_value(heartbeat_sec, Limits, 15),
|
||||
Session = sdlan_session:new(HeartbeatSec, ?MODULE),
|
||||
{ok, handshaking, #state{
|
||||
ref = Ref,
|
||||
socket = Socket,
|
||||
transport = Transport,
|
||||
max_packet_size = MaxPacketSize,
|
||||
heartbeat_sec = HeartbeatSec,
|
||||
session = Session
|
||||
}}.
|
||||
|
||||
-spec callback_mode() -> gen_statem:callback_mode_result().
|
||||
callback_mode() ->
|
||||
handle_event_function.
|
||||
|
||||
-spec handle_event(EventType :: gen_statem:event_type(), EventContent :: term(),
|
||||
StateName :: atom(), State :: #state{}) -> gen_statem:event_handler_result(atom(), #state{}).
|
||||
handle_event(info, {handshake, Ref, Transport, Socket, Timeout}, handshaking,
|
||||
State = #state{ref = Ref, transport = Transport, max_packet_size = MaxPacketSize, heartbeat_sec = HeartbeatSec}) ->
|
||||
case Transport:handshake(Socket, [], Timeout) of
|
||||
{ok, SslSocket} ->
|
||||
ok = Transport:setopts(SslSocket, [{mode, binary}, {packet, 2}, {active, true}]),
|
||||
ssl_send(Transport, SslSocket, sdlan_session:welcome_packet(MaxPacketSize, HeartbeatSec)),
|
||||
logger:debug("[sdlan_ssl_transport] ssl handshake ok, send welcome"),
|
||||
{next_state, initialized, State#state{socket = SslSocket}};
|
||||
{error, Reason} ->
|
||||
{stop, {ssl_handshake_failed, Reason}, State}
|
||||
end;
|
||||
|
||||
handle_event(info, {ssl, Socket, Frame}, _StateName,
|
||||
State = #state{socket = Socket, transport = Transport, session = Session}) when is_binary(Frame) ->
|
||||
case sdlan_session:handle_frame(Frame, Session) of
|
||||
{ok, NStateName, NSession, Packets} ->
|
||||
ssl_send(Transport, Socket, Packets),
|
||||
{next_state, NStateName, State#state{session = NSession}};
|
||||
{stop, Reason, _NStateName, NSession, Packets} ->
|
||||
ssl_send(Transport, Socket, Packets),
|
||||
{stop, Reason, State#state{session = NSession}}
|
||||
end;
|
||||
|
||||
handle_event(info, {ssl_closed, Socket}, _StateName, State = #state{socket = Socket}) ->
|
||||
expected_stop(socket_closed, State);
|
||||
|
||||
handle_event(info, {ssl_error, Socket, Reason}, _StateName, State = #state{socket = Socket}) ->
|
||||
expected_stop({socket_error, Reason}, State);
|
||||
|
||||
handle_event(info, {timeout, TimerRef, ping_ticker}, _StateName, State = #state{session = Session}) ->
|
||||
case sdlan_session:handle_timeout(TimerRef, Session) of
|
||||
{ok, NStateName, NSession} ->
|
||||
{next_state, NStateName, State#state{session = NSession}};
|
||||
{stop, Reason, _NStateName, NSession} ->
|
||||
expected_stop(Reason, State#state{session = NSession})
|
||||
end;
|
||||
|
||||
%% 发送事件信息
|
||||
handle_event(cast, {send_event, Event}, _StateName, State = #state{socket = Socket, transport = Transport, session = Session}) ->
|
||||
case sdlan_session:send_event(Event, Session) of
|
||||
{ok, NStateName, NSession, Packets} ->
|
||||
ssl_send(Transport, Socket, Packets),
|
||||
{next_state, NStateName, State#state{session = NSession}};
|
||||
{error, not_registered} ->
|
||||
keep_state_and_data
|
||||
end;
|
||||
|
||||
%% 发送命令信息
|
||||
handle_event(cast, {command, Ref, ReceiverPid, SubCommand}, _StateName, State = #state{socket = Socket, transport = Transport, session = Session}) ->
|
||||
case sdlan_session:command(Ref, ReceiverPid, SubCommand, Session) of
|
||||
{ok, NStateName, NSession, Packets} ->
|
||||
ssl_send(Transport, Socket, Packets),
|
||||
{next_state, NStateName, State#state{session = NSession}};
|
||||
{error, not_registered} ->
|
||||
keep_state_and_data
|
||||
end;
|
||||
|
||||
handle_event({call, From}, debug_info, StateName, State) ->
|
||||
{keep_state, State, [{reply, From, debug_info(StateName, State)}]};
|
||||
|
||||
handle_event(EventType, Info, StateName, State) ->
|
||||
logger:notice("[sdlan_ssl_transport] state: ~p, state_name: ~p, event_type: ~p, info: ~p", [State, StateName, EventType, Info]),
|
||||
keep_state_and_data.
|
||||
|
||||
-spec terminate(Reason :: term(), StateName :: atom(), State :: #state{}) -> ok.
|
||||
terminate(Reason, _StateName, #state{socket = Socket, transport = Transport, session = Session, close_reason = CloseReason}) ->
|
||||
Socket =/= undefined andalso catch Transport:close(Socket),
|
||||
logger:notice("[sdlan_ssl_transport] terminate closed with reason: ~p, close_reason: ~p", [Reason, CloseReason]),
|
||||
sdlan_session:close(Session),
|
||||
ok.
|
||||
|
||||
-spec code_change(OldVsn :: term(), StateName :: atom(), State :: #state{}, Extra :: term()) ->
|
||||
{ok, atom(), #state{}}.
|
||||
code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
|
||||
{ok, StateName, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
ssl_send(_Transport, _Socket, []) ->
|
||||
ok;
|
||||
ssl_send(Transport, Socket, Data) ->
|
||||
Len = iolist_size(Data),
|
||||
true = Len =< 65535,
|
||||
case Transport:send(Socket, Data) of
|
||||
ok ->
|
||||
incr_counter(ssl_frames_sent, 1),
|
||||
incr_counter(ssl_bytes_sent, Len + 2),
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
exit({ssl_send_failed, Reason})
|
||||
end.
|
||||
|
||||
expected_stop(Reason, State) ->
|
||||
logger:notice("[sdlan_ssl_transport] expected close: ~p", [Reason]),
|
||||
{stop, normal, State#state{close_reason = Reason}}.
|
||||
|
||||
debug_info(StateName, #state{session = Session, heartbeat_sec = HeartbeatSec}) ->
|
||||
ProcInfo = maps:from_list(process_info(self(), [message_queue_len, memory, reductions])),
|
||||
SessionInfo = sdlan_session:debug_info(Session),
|
||||
maps:merge(SessionInfo, ProcInfo#{
|
||||
state => StateName,
|
||||
session => SessionInfo,
|
||||
frames_sent => get_counter(ssl_frames_sent),
|
||||
bytes_sent => get_counter(ssl_bytes_sent),
|
||||
heartbeat_sec => HeartbeatSec
|
||||
}).
|
||||
|
||||
incr_counter(Key, Inc) ->
|
||||
erlang:put(Key, get_counter(Key) + Inc).
|
||||
|
||||
get_counter(Key) ->
|
||||
case erlang:get(Key) of
|
||||
undefined ->
|
||||
0;
|
||||
Value when is_integer(Value) ->
|
||||
Value
|
||||
end.
|
||||
@ -1,27 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 16. 4月 2026 20:53
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(test_ipv6_udp).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([test/0]).
|
||||
|
||||
-spec test() -> ok.
|
||||
test() ->
|
||||
Opts = [
|
||||
binary,
|
||||
inet6,
|
||||
{reuseaddr, true},
|
||||
{reuseport, true},
|
||||
{active, true},
|
||||
{recbuf, 5 * 1024 * 1024},
|
||||
{sndbuf, 5 * 1024 * 1024}
|
||||
],
|
||||
{ok, Socket} = gen_udp:open(0, Opts),
|
||||
ok = gen_udp:send(Socket, "2408:4005:318:4900:164a:b5c6:9023:ca8d", 1367, <<"hello world">>).
|
||||
12
swift_pb.sh
12
swift_pb.sh
@ -1,12 +0,0 @@
|
||||
#! /bin/sh
|
||||
|
||||
rm -rf tmp
|
||||
mkdir tmp
|
||||
|
||||
cp proto/sdlan.proto tmp/sdlan_pb.proto
|
||||
|
||||
cd tmp
|
||||
|
||||
protoc sdlan_pb.proto --swift_out=.
|
||||
cp sdlan_pb.pb.swift SDLMessage.pb.swift
|
||||
cp SDLMessage.pb.swift /usr/local/code/macos/punchnet/Tun/Punchnet/Protobuf/
|
||||
29
tun.proto
29
tun.proto
@ -1,29 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// 定义App发送给NE的事件
|
||||
message AppRequest {
|
||||
|
||||
message ChangeExitNodeRequest {
|
||||
// 空字符串表示清除出口节点
|
||||
string ip = 1;
|
||||
}
|
||||
|
||||
oneof command {
|
||||
ChangeExitNodeRequest change_exit_node = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message TunnelResponse {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
|
||||
// Tunnel产生的事件
|
||||
|
||||
message TunnelEvent {
|
||||
string id = 1;
|
||||
uint64 timestamp_ms = 2;
|
||||
int32 code = 3;
|
||||
string message = 4;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user