Compare commits

...

124 Commits

Author SHA1 Message Date
c54a5b35ad 简化逻辑 2026-04-16 16:48:54 +08:00
9dd2bd3380 fix 2026-04-16 16:35:36 +08:00
83e226aaf6 fix 2026-04-16 15:40:08 +08:00
7a5cb71bd4 fix 2026-04-16 15:27:18 +08:00
0dd32a5410 增加ipv6辅助信息的返回 2026-04-16 14:56:41 +08:00
88a2d5281e fix 2026-04-16 14:42:35 +08:00
08b31c8279 fix rebar.config 2026-04-16 14:25:03 +08:00
a8943c2940 fix 2026-04-16 11:36:25 +08:00
55a115d9ff rename 2026-04-16 11:20:08 +08:00
6001ec227c add ipv6 support 2026-04-16 11:16:22 +08:00
40c0f92735 add Tun 2026-04-15 20:38:04 +08:00
5bc5b5ebe3 fix 2026-04-13 15:12:01 +08:00
e6163c06cc fix dns 2026-04-13 15:05:13 +08:00
98f2078dea fix 2026-04-13 15:02:34 +08:00
2ec2380a54 fix 2026-04-13 14:51:17 +08:00
d009bba5b6 简化dns解析 2026-04-13 14:39:46 +08:00
59b6607d4c 简化dns解析 2026-04-13 14:38:24 +08:00
d77f61a5ac fix debug 2026-04-10 20:11:27 +08:00
f29da93cf9 fix debug 2026-04-10 19:57:32 +08:00
df7a096366 fix debug 2026-04-10 19:52:49 +08:00
a1ad437c84 fix debug 2026-04-10 19:51:36 +08:00
7c88fa3839 fix 2026-04-09 16:25:15 +08:00
d41992099d fix 2026-04-09 16:22:35 +08:00
85e856ff1b fix 2026-04-09 16:10:55 +08:00
ab4527a8e7 remove hole 2026-04-09 16:07:29 +08:00
1467e7f724 add debug 2026-04-09 15:12:11 +08:00
d0a764ac48 fix 2026-04-09 14:26:40 +08:00
46a7bcca87 fix 2026-04-09 11:30:13 +08:00
39dd454be0 fix 2026-04-02 17:34:12 +08:00
32fc75648b fix 2026-04-02 17:09:31 +08:00
08e4a9cbcb fix 2026-04-02 16:41:31 +08:00
681ac70d10 fix 2026-04-02 16:34:59 +08:00
86effab931 fix 2026-04-02 16:34:24 +08:00
2f341e6319 fix command 2026-04-02 16:17:28 +08:00
6ce69900f6 fix 2026-04-02 15:46:01 +08:00
32a7151f7d 修改项目结构 2026-04-02 14:26:48 +08:00
e01916fabf change protoc-erl to gpb plugin 2026-04-02 13:32:23 +08:00
2132f45dae fix Event 2026-04-02 13:11:51 +08:00
3ccc8a1666 fix 2026-03-31 14:39:32 +08:00
c5c3c59094 fix config 2026-03-31 12:33:55 +08:00
b49f20d26f fix prod config 2026-03-31 12:18:01 +08:00
3298f714fc fix prod config 2026-03-31 12:17:15 +08:00
c7036567e3 解决证书路径的问题 2026-03-31 12:13:06 +08:00
bf9be7ebc5 fix 2026-03-27 11:29:22 +08:00
e0afccfced fix message proto 2026-03-27 11:00:17 +08:00
adbced1cad 增加 2026-03-24 21:56:59 +08:00
95ff0dcab9 add md 2026-03-20 11:08:19 +08:00
25466727ad add md 2026-03-20 10:59:50 +08:00
a2795d3db8 add md 2026-03-20 10:58:23 +08:00
7b834f46d8 解决数据加密问题 2026-03-18 17:29:37 +08:00
6c55a905d2 解决数据加密问题 2026-03-18 17:27:59 +08:00
d39b35ac39 fix 2026-03-18 16:03:39 +08:00
c5abfd698a fix network 2026-03-17 15:11:02 +08:00
55204a4204 支持chacha20算法 2026-03-17 15:04:31 +08:00
d6a9e6a3ac add chacha20 2026-03-17 14:38:45 +08:00
c28699bc08 fix 2026-03-11 16:08:43 +08:00
9602609347 remove debug 2026-03-11 15:54:02 +08:00
ace1b832f1 fix 2026-03-11 15:37:45 +08:00
d735f29c2f fix 2026-03-11 15:18:36 +08:00
24cbbe0940 arp request 2026-03-06 11:26:23 +08:00
899561e94a fix 2026-03-05 11:16:48 +08:00
0f41304871 support arp request 2026-03-04 21:23:52 +08:00
92a5998868 support arp request 2026-03-04 21:23:01 +08:00
b009a324bd test rules 2026-03-04 16:17:27 +08:00
9351cabdee add debug 2026-03-04 16:01:03 +08:00
dd8330deed fix api 2026-03-03 16:49:35 +08:00
224e9343f7 fix api 2026-03-03 16:31:36 +08:00
4f6df41b04 fix 2026-03-03 16:06:39 +08:00
785a84802f fix quic channel 2026-03-03 14:17:31 +08:00
2b764cac77 fix quic channel 2026-03-03 14:14:58 +08:00
c19a3063dd fix quic channel 2026-03-03 14:05:59 +08:00
63706d9cd8 fix quic channel 2026-03-03 11:59:31 +08:00
d72a2221e5 fix maxwell.md 2026-02-28 23:55:05 +08:00
609c067d5b fix channel 2026-02-28 23:50:35 +08:00
3f15398c47 fix channel 2026-02-28 23:49:56 +08:00
2a7b8da245 fix mysql binlog 2026-02-28 23:44:32 +08:00
5615a97e14 fix mysql binlog 2026-02-28 23:34:05 +08:00
6f8d936dfc fix 2026-02-28 22:42:41 +08:00
17532cb729 fix redis 2026-02-28 22:35:24 +08:00
eb793e913b fix redis_server 2026-02-28 22:25:13 +08:00
be053ef8b4 fix rules 2026-02-28 17:22:41 +08:00
2c4ce65e7b fix 2026-02-28 16:35:15 +08:00
73f3d58286 add sql 2026-02-28 15:52:30 +08:00
9f39cf4852 fix 2026-02-28 15:44:53 +08:00
fef8247944 fix binlog 2026-02-28 13:49:22 +08:00
bcfcbcbd1d fix mysql 2026-02-28 13:37:56 +08:00
ed0ad499dd fix quic channel 2026-02-24 17:07:13 +08:00
a1e768695b fix ping 2026-02-24 16:47:07 +08:00
890f203609 fix ping 2026-02-24 16:44:48 +08:00
3b83b7dfee fix 2026-02-24 15:50:43 +08:00
e8a1b15f8f 修改dns的继续逻辑 2026-02-24 15:37:32 +08:00
2f7373b69b 修改dns的继续逻辑 2026-02-24 15:36:13 +08:00
6a044f155f 简化dns的解析逻辑 2026-02-24 15:28:04 +08:00
5a9b680d2e fix policy 2026-02-21 23:40:38 +08:00
f9b30f0afd fix 2026-02-21 23:01:18 +08:00
159586da72 fix 2026-02-21 22:44:48 +08:00
2cc9bdadc1 fix 2026-02-21 22:42:01 +08:00
92ced0cfa0 fix 2026-02-21 22:40:00 +08:00
ba430e76ad fix 2026-02-21 22:33:53 +08:00
60516e83e0 fix udp 2026-02-21 21:43:35 +08:00
9429518aa2 fix message type 2026-02-21 21:21:42 +08:00
c1f28aeb55 fix accept stream 2026-02-19 22:43:26 +08:00
73d86101ca fix 2026-02-19 22:38:32 +08:00
c5a0c25b8c fix 2026-02-19 00:28:26 +08:00
7c74e07c51 fix 2026-02-19 00:18:34 +08:00
a6d140afaa fix 2026-02-19 00:03:46 +08:00
66935409c9 fix 2026-02-18 23:57:55 +08:00
1ad809aa94 fix 2026-02-18 23:48:04 +08:00
9c81a7c2be fix 2026-02-18 23:45:55 +08:00
a6ebd84272 fix quic channel 2026-02-15 00:15:34 +08:00
de30e7e520 fix PktId 2026-02-15 00:02:49 +08:00
06e52f18ce fix proto 2026-02-14 23:58:09 +08:00
77ddabbf43 fix proto 2026-02-13 19:12:42 +08:00
f69ff60703 fix channel 2026-02-13 18:41:43 +08:00
c820f62a53 fix 2026-02-13 17:52:20 +08:00
f45b5e4ed4 fix warning 2026-02-13 17:32:23 +08:00
7a4ab39bc3 fix stun 2026-02-13 16:50:29 +08:00
9dad8b6ee8 remove tcp_channel 2026-02-13 16:39:11 +08:00
c6cb4eb49e fix quic_channel 2026-02-13 16:37:21 +08:00
21e26c3ab2 fix proto.message 2026-02-13 14:11:44 +08:00
2ff9cf51ce decode frames 2026-02-13 13:40:24 +08:00
2a8ead5f01 fix quic server 2026-02-13 12:55:58 +08:00
8a095a9b45 fix quicer 2026-02-11 23:38:27 +08:00
0f15f1da57 init quic server 2026-02-11 23:22:41 +08:00
82 changed files with 8266 additions and 7136 deletions

128
API.md
View File

@ -1,30 +1,10 @@
# 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": "错误描述"}}
```
# SDLAN 交互文档
## SDL主动请求的接口
### 1. 获取全部的可用网络信息
```text
url: /api/get_all_networks
url: get_all_networks
method: get
return:
@ -36,7 +16,7 @@ return:
### 2. 获取单个网络信息
```text
url: /api/get_network?id=:id
url: get_network?id=:id
method: get
return:
{"result":
@ -47,6 +27,7 @@ return:
"ipaddr": "192.168.0.1/24",
"domain": "punchnet.cn",
"owner_id": 1234,
"algorithm": "aes|chacha20",
"disabled_clients": ["client_id_xyz", "client_id_xyz1"]
}
}
@ -55,58 +36,30 @@ return:
```
### 3.token校验
### 3. access_token校验
```text
url: /api/auth_token
url: auth/access_token
method: post
params:
network_id: int,
client_id: string
token: string,
version: int // 当前客户端版本
mac: string,
ip: string,
mask_len: int,
hostname: string,
access_token: string,
return:
{"result":
{
"network_id": 8,
"upgrade_type": 0, // 升级类型0: 不升级1: 普通升级2: 强制升级
"upgrade_prompt": "升级提升语"
"upgrade_address": "升级提升语"
}
}
{"result": "ok"}
{"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: /api/set_node_status
url: set_node_status
method: post
params:
client_id: string
@ -118,21 +71,6 @@ 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
### 网络管理接口
@ -176,6 +114,23 @@ 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. 禁用节点
@ -192,23 +147,4 @@ 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 Normal file
View File

@ -0,0 +1,189 @@
# 注册流程
## 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分钟
session10分钟
✅ 4. 限流(强烈建议)
sendVerifyCode
每个 username60 秒一次
每个 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"
}

View File

@ -1,213 +0,0 @@
%% -*- coding: utf-8 -*-
%% Automatically generated, do not edit
%% Generated by gpb_compile version 4.21.1
-ifndef(sdlan_pb).
-define(sdlan_pb, true).
-define(sdlan_pb_gpb_version, "4.21.1").
-ifndef('SDLV_4_INFO_PB_H').
-define('SDLV_4_INFO_PB_H', true).
-record(sdl_v4_info,
{port = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
v4 = <<>> :: iodata() | undefined, % = 2, optional
nat_type = 0 :: non_neg_integer() | undefined % = 3, optional, 32 bits
}).
-endif.
-ifndef('SDLV_6_INFO_PB_H').
-define('SDLV_6_INFO_PB_H', true).
-record(sdl_v6_info,
{port = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
v6 = <<>> :: iodata() | undefined % = 2, optional
}).
-endif.
-ifndef('SDL_DEV_ADDR_PB_H').
-define('SDL_DEV_ADDR_PB_H', true).
-record(sdl_dev_addr,
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
mac = <<>> :: iodata() | undefined, % = 2, optional
net_addr = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
net_bit_len = 0 :: non_neg_integer() | undefined, % = 4, optional, 32 bits
network_domain = <<>> :: unicode:chardata() | undefined % = 5, optional
}).
-endif.
-ifndef('SDL_EMPTY_PB_H').
-define('SDL_EMPTY_PB_H', true).
-record(sdl_empty,
{
}).
-endif.
-ifndef('SDL_REGISTER_SUPER_PB_H').
-define('SDL_REGISTER_SUPER_PB_H', true).
-record(sdl_register_super,
{version = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
installed_channel = <<>> :: unicode:chardata() | undefined, % = 2, optional
client_id = <<>> :: unicode:chardata() | undefined, % = 3, optional
dev_addr = undefined :: sdlan_pb:sdl_dev_addr() | undefined, % = 4, optional
pub_key = <<>> :: unicode:chardata() | undefined, % = 5, optional
token = <<>> :: unicode:chardata() | undefined, % = 6, optional
network_code = <<>> :: unicode:chardata() | undefined, % = 7, optional
hostname = <<>> :: unicode:chardata() | undefined % = 8, optional
}).
-endif.
-ifndef('SDL_REGISTER_SUPER_ACK_PB_H').
-define('SDL_REGISTER_SUPER_ACK_PB_H', true).
-record(sdl_register_super_ack,
{dev_addr = undefined :: sdlan_pb:sdl_dev_addr() | undefined, % = 1, optional
aes_key = <<>> :: iodata() | undefined, % = 2, optional
upgrade_type = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
upgrade_prompt :: unicode:chardata() | undefined, % = 4, optional
upgrade_address :: unicode:chardata() | undefined % = 5, optional
}).
-endif.
-ifndef('SDL_REGISTER_SUPER_NAK_PB_H').
-define('SDL_REGISTER_SUPER_NAK_PB_H', true).
-record(sdl_register_super_nak,
{error_code = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
error_message = <<>> :: unicode:chardata() | undefined % = 2, optional
}).
-endif.
-ifndef('SDL_QUERY_INFO_PB_H').
-define('SDL_QUERY_INFO_PB_H', true).
-record(sdl_query_info,
{dst_mac = <<>> :: iodata() | undefined % = 1, optional
}).
-endif.
-ifndef('SDL_PEER_INFO_PB_H').
-define('SDL_PEER_INFO_PB_H', true).
-record(sdl_peer_info,
{dst_mac = <<>> :: iodata() | undefined, % = 1, optional
v4_info = undefined :: sdlan_pb:sdl_v4_info() | undefined, % = 2, optional
v6_info :: sdlan_pb:sdl_v6_info() | undefined % = 3, optional
}).
-endif.
-ifndef('SDL_NAT_CHANGED_EVENT_PB_H').
-define('SDL_NAT_CHANGED_EVENT_PB_H', true).
-record(sdl_nat_changed_event,
{mac = <<>> :: iodata() | undefined, % = 1, optional
ip = 0 :: non_neg_integer() | undefined % = 2, optional, 32 bits
}).
-endif.
-ifndef('SDL_SEND_REGISTER_EVENT_PB_H').
-define('SDL_SEND_REGISTER_EVENT_PB_H', true).
-record(sdl_send_register_event,
{dst_mac = <<>> :: iodata() | undefined, % = 1, optional
nat_ip = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
nat_port = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
nat_type = 0 :: non_neg_integer() | undefined, % = 4, optional, 32 bits
v6_info :: sdlan_pb:sdl_v6_info() | undefined % = 5, optional
}).
-endif.
-ifndef('SDL_NETWORK_SHUTDOWN_EVENT_PB_H').
-define('SDL_NETWORK_SHUTDOWN_EVENT_PB_H', true).
-record(sdl_network_shutdown_event,
{message = <<>> :: unicode:chardata() | undefined % = 1, optional
}).
-endif.
-ifndef('SDL_CHANGE_NETWORK_COMMAND_PB_H').
-define('SDL_CHANGE_NETWORK_COMMAND_PB_H', true).
-record(sdl_change_network_command,
{dev_addr = undefined :: sdlan_pb:sdl_dev_addr() | undefined, % = 1, optional
aes_key = <<>> :: iodata() | undefined % = 2, optional
}).
-endif.
-ifndef('SDL_COMMAND_ACK_PB_H').
-define('SDL_COMMAND_ACK_PB_H', true).
-record(sdl_command_ack,
{status = false :: boolean() | 0 | 1 | undefined, % = 1, optional
message :: unicode:chardata() | undefined % = 2, optional
}).
-endif.
-ifndef('SDL_FLOWS_PB_H').
-define('SDL_FLOWS_PB_H', true).
-record(sdl_flows,
{forward_num = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
p2p_num = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
inbound_num = 0 :: non_neg_integer() | undefined % = 3, optional, 32 bits
}).
-endif.
-ifndef('SDL_STUN_REQUEST_PB_H').
-define('SDL_STUN_REQUEST_PB_H', true).
-record(sdl_stun_request,
{cookie = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
client_id = <<>> :: unicode:chardata() | undefined, % = 2, optional
network_id = 0 :: non_neg_integer() | undefined, % = 3, optional, 32 bits
mac = <<>> :: iodata() | undefined, % = 4, optional
ip = 0 :: non_neg_integer() | undefined, % = 5, optional, 32 bits
nat_type = 0 :: non_neg_integer() | undefined, % = 6, optional, 32 bits
v6_info :: sdlan_pb:sdl_v6_info() | undefined % = 7, optional
}).
-endif.
-ifndef('SDL_STUN_REPLY_PB_H').
-define('SDL_STUN_REPLY_PB_H', true).
-record(sdl_stun_reply,
{cookie = 0 :: non_neg_integer() | undefined % = 1, optional, 32 bits
}).
-endif.
-ifndef('SDL_DATA_PB_H').
-define('SDL_DATA_PB_H', true).
-record(sdl_data,
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
src_mac = <<>> :: iodata() | undefined, % = 2, optional
dst_mac = <<>> :: iodata() | undefined, % = 3, optional
is_p2p = false :: boolean() | 0 | 1 | undefined, % = 4, optional
ttl = 0 :: non_neg_integer() | undefined, % = 5, optional, 32 bits
data = <<>> :: iodata() | undefined % = 6, optional
}).
-endif.
-ifndef('SDL_REGISTER_PB_H').
-define('SDL_REGISTER_PB_H', true).
-record(sdl_register,
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
src_mac = <<>> :: iodata() | undefined, % = 2, optional
dst_mac = <<>> :: iodata() | undefined % = 3, optional
}).
-endif.
-ifndef('SDL_REGISTER_ACK_PB_H').
-define('SDL_REGISTER_ACK_PB_H', true).
-record(sdl_register_ack,
{network_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
src_mac = <<>> :: iodata() | undefined, % = 2, optional
dst_mac = <<>> :: iodata() | undefined % = 3, optional
}).
-endif.
-ifndef('SDL_STUN_PROBE_PB_H').
-define('SDL_STUN_PROBE_PB_H', true).
-record(sdl_stun_probe,
{cookie = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
attr = 0 :: non_neg_integer() | undefined % = 2, optional, 32 bits
}).
-endif.
-ifndef('SDL_STUN_PROBE_REPLY_PB_H').
-define('SDL_STUN_PROBE_REPLY_PB_H', true).
-record(sdl_stun_probe_reply,
{cookie = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
port = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
ip = 0 :: non_neg_integer() | undefined % = 3, optional, 32 bits
}).
-endif.
-endif.

View File

@ -1,19 +0,0 @@
%%%-------------------------------------------------------------------
%%% @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
}).

View File

@ -1,59 +0,0 @@
-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.

View File

@ -1,263 +0,0 @@
%%%-------------------------------------------------------------------
%%% @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>>.

View File

@ -1,64 +0,0 @@
-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).

View File

@ -1,141 +0,0 @@
%%%-------------------------------------------------------------------
%%% @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).

View File

@ -1,24 +0,0 @@
-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.

View File

@ -1,24 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 09. 4 2024 14:28
%%%-------------------------------------------------------------------
-module(file_handler).
-author("anlicheng").
%% API
-export([init/2]).
init(Req, State) ->
%%
FullPath = "/tmp/files/test.dmg",
%% 使cowboy_req:reply函数返回文件内容
{ok, Content} = file:read_file(FullPath),
Req1 = cowboy_req:reply(200, #{
<<"Content-Type">> => <<"application/octet-stream">>
}, Content, Req),
{ok, Req1, State}.

View File

@ -1,72 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 09. 4 2024 14:28
%%%-------------------------------------------------------------------
-module(node_handler).
-author("anlicheng").
-include("sdlan.hrl").
-include("sdlan_pb.hrl").
-include("sdlan_tables.hrl").
%% API
-export([handle_request/4]).
handle_request("POST", "/node/list", _, #{<<"network_id">> := NetworkId}) when NetworkId > 0 ->
Pid = sdlan_network:get_pid(NetworkId),
UsedMap = sdlan_network:get_used_map(Pid),
Clients = client_model:get_clients(NetworkId),
ClientInfos = lists:map(fun(#client{client_id = ClientId, mac = Mac, ip = Ip, status = Status}) ->
Info = #{
<<"client_id">> => ClientId,
<<"mac">> => Mac,
<<"ip">> => sdlan_ipaddr:int_to_ipv4(Ip),
<<"status">> => atom_to_binary(Status)
},
maps:merge(Info, maps:get(Mac, UsedMap, #{}))
end, Clients),
{ok, 200, sdlan_util:json_data(ClientInfos)};
handle_request("POST", "/node/disable", _, #{<<"network_id">> := NetworkId, <<"client_id">> := ClientId}) when NetworkId > 0 ->
case sdlan_network:get_pid(NetworkId) of
undefined ->
{ok, 200, sdlan_util:json_error(-1, <<"network not found">>)};
Pid ->
sdlan_network:disable_client(Pid, ClientId),
{ok, 200, sdlan_util:json_data(<<"success">>)}
end;
handle_request("POST", "/node/move", _, #{<<"client_id">> := ClientId, <<"from_network_id">> := FromNetworkId, <<"to_network_id">> := ToNetworkId, <<"timeout">> := Timeout}) ->
case {sdlan_network:get_pid(FromNetworkId), sdlan_network:get_pid(ToNetworkId)} of
{FromPid, ToPid} when is_pid(FromPid), is_pid(ToPid) ->
case sdlan_network:dropout_client(FromPid, ClientId) of
{ok, ChannelPid, HostName} ->
Ref = sdlan_channel:move_network(ChannelPid, self(), ToPid, HostName),
receive
{command_reply, Ref, {error, Reason}} ->
logger:warning("[node_handler] client_id: ~p, move network from: ~p, to: ~p, get error: ~p", [ClientId, FromPid, ToPid, Reason]),
{ok, 200, sdlan_util:json_error(-1, <<"move failed">>)};
{command_reply, Ref, #sdl_command_ack{status = true}} ->
{ok, 200, sdlan_util:json_data(<<"success">>)};
{command_reply, Ref, #sdl_command_ack{status = false, message = ErrorMsg}} when is_binary(ErrorMsg) ->
{ok, 200, sdlan_util:json_error(-1, <<"move failed: ", ErrorMsg/binary>>)}
after Timeout * 1000 ->
{ok, 200, sdlan_util:json_error(-1, <<"move timeout">>)}
end;
error ->
{ok, 200, sdlan_util:json_error(-1, <<"dropout from from_network error">>)}
end;
{FromPid, undefined} when is_pid(FromPid) ->
{ok, 200, sdlan_util:json_error(-1, <<"to_network not found">>)};
{undefined, ToPid} when is_pid(ToPid) ->
{ok, 200, sdlan_util:json_error(-1, <<"from_network not found">>)}
end;
handle_request(_, Path, _, _) ->
Path1 = list_to_binary(Path),
{ok, 200, sdlan_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.

View File

@ -1,162 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 04. 7 2023 12:31
%%%-------------------------------------------------------------------
-module(client_model).
-author("aresei").
-include("sdlan_tables.hrl").
-include_lib("stdlib/include/qlc.hrl").
%% API
-export([create_table/1, get_table_name/1]).
-export([get_clients/1, get_client/2, delete_clients/1, delete_client/2, disable_client/2, alloc_ip/6]).
-export([debug/1]).
create_table(Tab) when is_atom(Tab) ->
mnesia:create_table(Tab, [
{attributes, record_info(fields, client)},
{record_name, client},
{disc_copies, [node()]},
{type, set}
]).
-spec get_table_name(NetworkId :: integer()) -> TableName :: atom().
get_table_name(NetworkId) when is_integer(NetworkId) ->
list_to_atom("client_" ++ integer_to_list(NetworkId)).
-spec get_client(NetworkId :: integer(), ClientId :: binary()) -> error | {ok, Client :: #client{}} .
get_client(NetworkId, ClientId) when is_integer(NetworkId), is_binary(ClientId) ->
Tab = get_table_name(NetworkId),
case mnesia:dirty_read(Tab, ClientId) of
[] ->
error;
[Client|_] ->
{ok, Client}
end.
-spec get_clients(NetworkId :: integer()) -> [Client :: #client{}].
get_clients(NetworkId) when is_integer(NetworkId) ->
Tab = get_table_name(NetworkId),
case mnesia:transaction(fun() -> mnesia:foldl(fun(R, Acc0) -> [R|Acc0] end, [], Tab) end) of
{'atomic', Items} ->
lists:reverse(Items);
{'aborted', _} ->
[]
end.
-spec delete_clients(NetworkId :: integer()) -> ok | {error, Reason :: any()}.
delete_clients(NetworkId) when is_integer(NetworkId) ->
Tab = get_table_name(NetworkId),
case mnesia:transaction(fun() -> mnesia:clear_table(Tab) end) of
{'atomic', ok} ->
ok;
{'aborted', Reason} ->
{error, Reason}
end.
-spec delete_client(NetworkId :: integer(), ClientId :: binary()) -> {ok, Client :: #client{}} | {error, Reason :: any()}.
delete_client(NetworkId, ClientId) when is_integer(NetworkId), is_binary(ClientId) ->
Tab = get_table_name(NetworkId),
Fun = fun() ->
case mnesia:read(Tab, ClientId, write) of
[] ->
mnesia:abort(not_found);
[Record] ->
mnesia:delete(Tab, ClientId, write),
{ok, Record}
end
end,
case mnesia:transaction(Fun) of
{'atomic', {ok, Client}} ->
{ok, Client};
{'aborted', Reason} ->
{error, Reason}
end.
-spec disable_client(NetworkId :: integer(), ClientId :: binary()) -> ok | {error, Reason :: any()}.
disable_client(NetworkId, ClientId) when is_integer(NetworkId), is_binary(ClientId) ->
Tab = get_table_name(NetworkId),
Fun = fun() ->
case mnesia:read(Tab, ClientId, read) of
[] ->
ok;
[Client] ->
mnesia:write(Tab, Client#client{status = disabled}, write)
end
end,
case mnesia:transaction(Fun) of
{'atomic', ok} ->
ok;
{'aborted', Reason} ->
{error, Reason}
end.
%% ip地址的时候mac地址为唯一基准
-spec alloc_ip(NetworkId :: integer(), Ips :: list(), ClientId :: binary(), Mac :: binary(), NetAddr0 :: integer(), HostName :: binary()) ->
{ok, Ip :: integer()} | {error, Reason :: any()}.
alloc_ip(NetworkId, Ips, ClientId, Mac, NetAddr0, HostName) when is_binary(ClientId), is_integer(NetAddr0), is_binary(Mac), is_binary(HostName) ->
case mnesia:transaction(fun() -> alloc_ip0(NetworkId, Ips, ClientId, Mac, NetAddr0, HostName) end) of
{'atomic', Res} ->
{ok, Res};
{'aborted', Reason} ->
{error, Reason}
end.
alloc_ip0(NetworkId, Ips, ClientId, Mac, NetAddr0, HostName) ->
Tab = get_table_name(NetworkId),
case mnesia:read(Tab, ClientId) of
[Client=#client{ip = Ip, status = normal}] ->
ok = mnesia:write(Tab, Client#client{mac = Mac}, write),
Ip;
[#client{status = disabled}] ->
mnesia:abort(client_disabled);
[] ->
{UsedIps, UsedHostNames} = mnesia:foldl(fun(#client{ip = Ip0, host_name = HostName0}, {IpAcc, HostNameAcc}) ->
{[Ip0|IpAcc], [HostName0|HostNameAcc]}
end, {[], []}, Tab),
case HostName =/= <<>> andalso lists:member(HostName, UsedHostNames) of
true ->
mnesia:abort(host_name_used);
false ->
case lists:member(NetAddr0, Ips) andalso not lists:member(NetAddr0, UsedIps) of
true ->
%% ip没有被占用
Client = #client{client_id = ClientId, mac = Mac, ip = NetAddr0, host_name = HostName, status = normal},
ok = mnesia:write(Tab, Client, write),
NetAddr0;
false ->
case Ips -- UsedIps of
[] ->
mnesia:abort(no_ip);
[Ip|_] ->
Client = #client{client_id = ClientId, mac = Mac, ip = Ip, host_name = HostName, status = normal},
ok = mnesia:write(Tab, Client, write),
Ip
end
end
end
end.
%%%===================================================================
%%% helper functions
%%%===================================================================
debug(NetworkId) when is_integer(NetworkId) ->
Tab = get_table_name(NetworkId),
F = fun() ->
Q = qlc:q([E || E <- mnesia:table(Tab)]),
qlc:e(Q)
end,
case mnesia:transaction(F) of
{'atomic', Records} ->
lists:foreach(fun(C) -> logger:debug("client: ~p", [C]) end, Records);
{'aborted', Reason} ->
logger:warning("read clients get error: ~p", [Reason])
end.

View File

@ -1,26 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 04. 7 2023 12:31
%%%-------------------------------------------------------------------
-module(mnesia_id_generator).
-author("aresei").
-include("sdlan.hrl").
%% API
-export([next_id/1, create_table/0]).
create_table() ->
%% id生成器
mnesia:create_table(id_generator, [
{attributes, record_info(fields, id_generator)},
{record_name, id_generator},
{disc_copies, [node()]},
{type, ordered_set}
]).
next_id(Tab) when is_atom(Tab) ->
mnesia:dirty_update_counter(id_generator, Tab, 1).

View File

@ -1,43 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%% TODO
%%% @end
%%% Created : 28. 3 2024 11:01
%%%-------------------------------------------------------------------
-module(mnesia_manager).
-author("anlicheng").
-include("sdlan.hrl").
%% API
-export([init_database/0, join/1, copy_database/1]).
init_database() ->
%% schema
mnesia:stop(),
mnesia:delete_schema([node()]),
%% schema
ok = mnesia:create_schema([node()]),
ok = mnesia:start(),
ok.
%%
join(MasterNode) when is_atom(MasterNode) ->
net_kernel:connect_node(MasterNode).
%% slave数据库
copy_database(MasterNode) when is_atom(MasterNode) ->
%% schema
mnesia:stop(),
mnesia:delete_schema([node()]),
%%
mnesia:start(),
rpc:call(MasterNode, mnesia, change_config, [extra_db_nodes, [node()]]),
mnesia:change_table_copy_type(schema, node(), disc_copies),
%%
% mnesia:add_table_copy(client, node(), ram_copies),
ok.

View File

@ -1,382 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author licheng5
%%% @copyright (C) 2020, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 10. 12 2020 11:17
%%%-------------------------------------------------------------------
-module(sdlan_channel).
-author("licheng5").
-behaviour(gen_server).
-include("sdlan.hrl").
-include("sdlan_pb.hrl").
%%
-define(PING_TICKER, 15000).
%%
%% token不存在
-define(NAK_INVALID_TOKEN, 1).
%%
-define(NAK_NODE_DISABLE, 2).
%% IP地址可以用
-define(NAK_NO_IP, 3).
%%
-define(NAK_NETWORK_FAULT, 4).
%%
-define(NAK_INTERNAL_FAULT, 5).
%% hostname被占用
-define(NAK_HOSTNAME_USED, 6).
%%
-define(UPGRADE_NONE, 0).
-define(UPGRADE_NORMAL, 1).
-define(UPGRADE_FORCE, 2).
%% API
-export([start_link/4]).
-export([publish_command/4, send_event/3, stop/2, move_network/4]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-record(state, {
transport,
socket,
client_id :: undefined | binary(),
pub_key :: undefined | binary(),
token :: undefined | binary(),
%% ip地址
assign_ip :: undefined | integer(),
%% id
network_pid :: undefined | pid(),
%% mac地址
mac :: undefined | binary(),
%%
is_registered = false,
%% ping的次数
ping_counter = 0,
%% id
packet_id = 1 :: integer(),
%%
inflight = #{}
}).
%%
-spec publish_command(Pid :: pid(), ReceiverPid :: pid(), CommandType :: integer(), Msg :: binary()) -> Ref :: reference().
publish_command(Pid, ReceiverPid, CommandType, Msg) when is_pid(Pid), is_pid(ReceiverPid), is_integer(CommandType), is_binary(Msg) ->
Ref = make_ref(),
Pid ! {publish_command, ReceiverPid, Ref, CommandType, Msg},
Ref.
%%
-spec move_network(Pid :: pid(), ReceiverPid :: pid(), NetworkPid :: pid(), HostName :: binary()) -> Ref :: reference().
move_network(Pid, ReceiverPid, NetworkPid, HostName) when is_pid(Pid), is_pid(ReceiverPid), is_pid(NetworkPid), is_binary(HostName) ->
Ref = make_ref(),
Pid ! {move_network, ReceiverPid, Ref, NetworkPid, HostName},
Ref.
%%
-spec send_event(Pid :: pid(), EventType :: integer(), Event :: binary()) -> no_return().
send_event(Pid, EventType, Event) when is_pid(Pid), is_integer(EventType), is_binary(Event) ->
Pid ! {send_event, EventType, Event}.
%%
-spec stop(Pid :: pid(), Reason :: any()) -> no_return().
stop(undefined, _Reason) ->
ok;
stop(Pid, Reason) when is_pid(Pid) ->
Pid ! {stop, Reason}.
%%--------------------------------------------------------------------
%% esockd callback
%%--------------------------------------------------------------------
start_link(Ref, _Socket, Transport, Opts) ->
{ok, proc_lib:spawn_link(?MODULE, init, [[Ref, Transport, Opts]])}.
init([Ref, Transport, _Opts = []]) ->
{ok, Socket} = ranch:handshake(Ref),
logger:debug("[sdlan_channel] get a new connection: ~p", [Socket]),
Transport:setopts(Socket, [{active, true}, {packet, 2}]),
erlang:start_timer(?PING_TICKER, self(), ping_ticker),
gen_server:enter_loop(?MODULE, [], #state{transport = Transport, socket = Socket}).
handle_call(_Request, _From, State) ->
{reply, ok, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
%% token或者网络id来注册
handle_info({tcp, Sock, <<PacketId:32, ?PACKET_REGISTER_SUPER, Body/binary>>}, State=#state{transport = Transport, socket = Sock}) ->
#sdl_register_super{version = Version, client_id = ClientId, hostname = HostName, dev_addr = #sdl_dev_addr{net_addr = NetAddr0, mac = Mac}, network_code = NetworkCode, token = Token, pub_key = PubKey} = sdlan_pb:decode_msg(Body, sdl_register_super),
%%
logger:debug("[sdlan_channel] client_id: ~p, public_key: ~p, token: ~p, network_code: ~p", [ClientId, PubKey, Token, NetworkCode]),
true = (Mac =/= <<>> andalso PubKey =/= <<>> andalso ClientId =/= <<>>),
%% Mac地址不能是广播地址
true = not (sdlan_util:is_multicast_mac(Mac) orelse sdlan_util:is_broadcast_mac(Mac)),
AuthResult = if
Token /= <<>> ->
logger:debug("[sdlan_channel] auth token: ~p", [Token]),
sdlan_api:auth_token(ClientId, Token, Version);
NetworkCode /= <<>> ->
logger:debug("[sdlan_channel] auth network code: ~p", [NetworkCode]),
sdlan_api:auth_network_code(ClientId, NetworkCode, Version)
end,
case AuthResult of
{ok, #{<<"result">> := #{<<"network_id">> := NetworkId, <<"upgrade_type">> := UpgradeType, <<"upgrade_prompt">> := UpgradePrompt, <<"upgrade_address">> := UpgradeAddress}}} when is_integer(NetworkId) ->
logger:debug("[sdlan_channel] client_id: ~p, mac: ~p, token: ~p, version: ~p, registerd, alloc network_id: ~p", [ClientId, sdlan_util:format_mac(Mac), Token, Version, NetworkId]),
%% network的对应关系
case sdlan_network:get_pid(NetworkId) of
NetworkPid when is_pid(NetworkPid) ->
try sdlan_network:assign_ip_addr(NetworkPid, self(), ClientId, Mac, NetAddr0, HostName) of
{ok, Domain, NetAddr, NetBitLen, AesKey} ->
RsaPubKey = sdlan_cipher:rsa_pem_decode(PubKey),
EncodedAesKey = rsa_encode(AesKey, RsaPubKey),
RegisterSuperAck = sdlan_pb:encode_msg(#sdl_register_super_ack {
dev_addr = #sdl_dev_addr{
network_id = NetworkId,
net_addr = NetAddr,
mac = Mac,
net_bit_len = NetBitLen,
network_domain = Domain
},
aes_key = EncodedAesKey,
upgrade_type = UpgradeType,
upgrade_prompt = UpgradePrompt,
upgrade_address = UpgradeAddress
}),
%%
Reply = <<PacketId:32, ?PACKET_REGISTER_SUPER_ACK, RegisterSuperAck/binary>>,
Transport:send(Sock, Reply),
logger:debug("[sdlan_channel] client_id: ~p, mac: ~p, alloc ip: ~p, register will send ack, aes_key: ~p, enc_aes_key: ~p",
[ClientId, sdlan_util:format_mac(Mac), sdlan_ipaddr:int_to_ipv4(NetAddr), AesKey, EncodedAesKey]),
%% 线
Result = sdlan_api:node_online(ClientId, NetworkId, sdlan_ipaddr:int_to_ipv4(NetAddr)),
logger:debug("[sdlan_channel] client_id: ~p, set none online, result is: ~p", [ClientId, Result]),
case UpgradeType =:= ?UPGRADE_FORCE of
true ->
logger:warning("[sdlan_channel] client_id: ~p, need upgrade force!", [ClientId]),
{stop, normal, State};
false ->
{noreply, State#state{client_id = ClientId, mac = Mac, assign_ip = NetAddr, network_pid = NetworkPid, pub_key = PubKey, is_registered = true}}
end;
{error, no_ip} ->
logger:warning("[sdlan_channel] client_id: ~p, token: ~p, register get error: no_ip", [ClientId, Token]),
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_NO_IP, <<"No Ip address">>)),
{stop, normal, State};
{error, host_name_used} ->
logger:warning("[sdlan_channel] client_id: ~p, token: ~p, register get error: host_name_used", [ClientId, Token]),
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_HOSTNAME_USED, <<"Host Name Used">>)),
{stop, normal, State};
{error, client_disabled} ->
logger:warning("[sdlan_channel] client_id: ~p, token: ~p, register get error: client_disabled", [ClientId, Token]),
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_NODE_DISABLE, <<"Client Connection Disable">>)),
{stop, normal, State}
catch _:Error:Stack ->
logger:warning("[sdlan_channel] get error: ~p, stack: ~p", [Error, Stack])
end;
undefined ->
logger:warning("[sdlan_channel] client_id: ~p, token: ~p, register get error: network not found", [ClientId, Token]),
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_INTERNAL_FAULT, <<"Internal Error">>)),
{stop, normal, State}
end;
{ok, #{<<"error">> := #{<<"code">> := 1, <<"message">> := Message}}} ->
logger:warning("[sdlan_channel] client_id: ~p, token: ~p, register get error: ~ts, error_code: 1", [ClientId, Token, Message]),
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_INVALID_TOKEN, Message)),
{stop, normal, State};
{ok, #{<<"error">> := #{<<"code">> := 2, <<"message">> := Message}}} ->
logger:warning("[sdlan_channel] client_id: ~p, token: ~p, register get error: ~p, error_code: 2", [ClientId, Token, Message]),
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_NODE_DISABLE, Message)),
{stop, normal, State};
{error, Reason} ->
logger:warning("[sdlan_channel] client_id: ~p, token: ~p, register get error: ~p", [ClientId, Token, Reason]),
Transport:send(Sock, register_nak_reply(PacketId, ?NAK_NETWORK_FAULT, <<"Network Error">>)),
{stop, normal, State}
end;
handle_info({tcp, Sock, <<PacketId:32, ?PACKET_QUERY_INFO, Body/binary>>}, State = #state{transport = Transport, socket = Sock, network_pid = NetworkPid, mac = SrcMac, is_registered = true}) when is_pid(NetworkPid) ->
#sdl_query_info{dst_mac = DstMac} = sdlan_pb:decode_msg(Body, sdl_query_info),
case sdlan_network:peer_info(NetworkPid, SrcMac, DstMac) of
error ->
logger:debug("[sdlan_channel] query_info src_mac is: ~p, dst_mac: ~p, nat_peer not found",
[sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
Transport:send(Sock, <<PacketId:32, ?PACKET_EMPTY>>),
{noreply, State};
{ok, {NatPeer = {{Ip0, Ip1, Ip2, Ip3}, NatPort}, NatType}, V6Info} ->
logger:debug("[sdlan_channel] query_info src_mac is: ~p, dst_mac: ~p, nat_peer: ~p",
[sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac), NatPeer]),
PeerInfo = sdlan_pb:encode_msg(#sdl_peer_info{
dst_mac = DstMac,
v4_info = #sdl_v4_info {
port = NatPort,
v4 = <<Ip0, Ip1, Ip2, Ip3>>,
nat_type = NatType
},
v6_info = V6Info
}),
Transport:send(Sock, <<PacketId:32, ?PACKET_PEER_INFO, PeerInfo/binary>>),
{noreply, State}
end;
handle_info({tcp, _Sock, <<0:32, ?PACKET_PING>>}, State = #state{transport = Transport, socket = Sock, ping_counter = PingCounter}) ->
%logger:debug("[sdlan_channel] client_id: ~p, get ping", [ClientId]),
Transport:send(Sock, <<0:32, ?PACKET_PONG>>),
{noreply, State#state{ping_counter = PingCounter + 1}};
handle_info({timeout, _, ping_ticker}, State = #state{client_id = ClientId, ping_counter = PingCounter}) ->
%%
erlang:start_timer(?PING_TICKER, self(), ping_ticker),
case PingCounter > 0 of
true ->
{noreply, State#state{ping_counter = 0}};
false ->
logger:debug("[sdlan_channel] client_id: ~p, ping losted", [ClientId]),
{stop, normal, State#state{ping_counter = 0}}
end;
%%
handle_info({move_network, ReceiverPid, Ref, NetworkPid, HostName},
State = #state{transport = Transport, socket = Sock, client_id = ClientId, mac = Mac, pub_key = PubKey, packet_id = PacketId, inflight = Inflight, is_registered = true}) ->
%% network的对应关系
case sdlan_network:assign_ip_addr(NetworkPid, self(), ClientId, Mac, 0, HostName) of
{ok, Domain, NetAddr, NetBitLen, AesKey} ->
RsaPubKey = sdlan_cipher:rsa_pem_decode(PubKey),
EncodedAesKey = rsa_encode(AesKey, RsaPubKey),
{ok, NetworkId} = sdlan_network:get_network_id(NetworkPid),
%%
ChangeNetworkCommand = sdlan_pb:encode_msg(#sdl_change_network_command {
dev_addr = #sdl_dev_addr {
network_id = NetworkId,
net_addr = NetAddr,
net_bit_len = NetBitLen,
network_domain = Domain
},
aes_key = EncodedAesKey
}),
Command = <<PacketId:32, ?PACKET_COMMAND, ?PACKET_COMMAND_CHANGE_NETWORK, ChangeNetworkCommand/binary>>,
Transport:send(Sock, Command),
%% 线
sdlan_api:node_online(ClientId, NetworkId, sdlan_ipaddr:int_to_ipv4(NetAddr)),
logger:debug("[sdlan_channel] client_id: ~p, move_network will send command: ~p", [ClientId, Command]),
{noreply, State#state{packet_id = PacketId + 1, assign_ip = NetAddr, network_pid = NetworkPid, inflight = maps:put(PacketId, {ReceiverPid, Ref}, Inflight)}};
{error, Reason} ->
logger:debug("[sdlan_channel] client_id: ~p, move_network get error: ~p", [ClientId, Reason]),
Transport:send(Sock, register_nak_reply(0, ?NAK_NO_IP, <<"No Ip address">>)),
ReceiverPid ! {command_reply, Ref, {error, <<"assign_ip error, no ip free">>}},
{noreply, State}
end;
%%
handle_info({send_event, EventType, Event}, State = #state{transport = Transport, socket = Sock, client_id = ClientId, is_registered = true}) ->
logger:debug("[sdlan_channel] client_id: ~p, will send eventType: ~p, event: ~p", [ClientId, EventType, Event]),
Transport:send(Sock, <<0:32, ?PACKET_EVENT, EventType, Event/binary>>),
{noreply, State};
%%
handle_info({tcp, _Sock, <<0:32, ?PACKET_FLOW_TRACER, Body/binary>>}, State = #state{client_id = ClientId, network_pid = NetworkPid, is_registered = true}) when is_pid(NetworkPid) ->
#sdl_flows{forward_num = ForwardNum, p2p_num = P2PNum, inbound_num = InboundNum} = sdlan_pb:decode_msg(Body, sdl_flows),
{ok, NetworkId} = sdlan_network:get_network_id(NetworkPid),
ReportResult = sdlan_api:flow_report(ClientId, NetworkId, ForwardNum, P2PNum, InboundNum),
logger:debug("[sdlan_channel] flow_tracer, forward: ~p, p2p: ~p, inbound: ~p, result: ~p", [ClientId, ForwardNum, P2PNum, InboundNum, ReportResult]),
{noreply, State};
%%
handle_info({tcp, _Sock, <<0:32, ?PACKET_UNREGISTER>>}, State = #state{client_id = ClientId, network_pid = NetworkPid, is_registered = true}) when is_pid(NetworkPid) ->
logger:warning("[sdlan_channel] unregister client_id: ~p", [ClientId]),
% sdlan_network:unregister(NetworkPid, ClientId),
{stop, normal, State};
%%
handle_info({publish_command, ReceiverPid, Ref, CommandType, Msg}, State = #state{transport = Transport, socket = Sock, client_id = ClientId, packet_id = PacketId, inflight = Inflight, is_registered = true}) ->
logger:warning("[sdlan_channel] client_id: ~p, will publish: ~p, message: ~p", [ClientId, CommandType, Msg]),
Transport:send(Sock, <<PacketId:32, ?PACKET_COMMAND, CommandType, Msg/binary>>),
{noreply, State#state{packet_id = PacketId + 1, inflight = maps:put(PacketId, {ReceiverPid, Ref}, Inflight)}};
%%
handle_info({tcp, _Sock, <<PacketId:32, ?PACKET_COMMAND_ACK, Body/binary>>}, State = #state{client_id = ClientId, inflight = Inflight}) when PacketId > 0 ->
CommandAck = #sdl_command_ack{} = sdlan_pb:decode_msg(Body, sdl_command_ack),
logger:debug("[sdlan_channel] client_id: ~p, get publish response message: ~p, packet_id: ~p", [ClientId, CommandAck, PacketId]),
case maps:take(PacketId, Inflight) of
error ->
logger:warning("[sdlan_channel] get unknown publish response message: ~p, packet_id: ~p", [CommandAck, PacketId]),
{ok, State};
{{ReceiverPid, Ref}, NInflight} ->
case is_pid(ReceiverPid) andalso is_process_alive(ReceiverPid) of
true ->
ReceiverPid ! {command_reply, Ref, CommandAck};
false ->
logger:warning("[sdlan_channel] get publish response message: ~p, packet_id: ~p, but receiver_pid is deaded", [CommandAck, PacketId])
end,
{noreply, State#state{inflight = NInflight}}
end;
handle_info({tcp_error, Sock, Reason}, State = #state{socket = Sock, client_id = ClientId}) ->
logger:notice("[sdlan_channel] client_id: ~p, tcp_error: ~p", [ClientId, Reason]),
{stop, normal, State};
handle_info({tcp_closed, Sock}, State = #state{socket = Sock, client_id = ClientId}) ->
logger:notice("[sdlan_channel] client_id: ~p, tcp_closed", [ClientId]),
{stop, normal, State};
%%
handle_info({stop, Reason}, State) ->
{stop, Reason, State};
handle_info(Info, State) ->
logger:warning("[sdlan_channel] get a unknown message: ~p, channel will closed", [Info]),
{noreply, State}.
terminate(Reason, #state{client_id = ClientId, network_pid = NetworkPid}) ->
case ClientId /= undefined andalso is_pid(NetworkPid) of
true ->
{ok, NetworkId} = sdlan_network:get_network_id(NetworkPid),
Result = sdlan_api:node_offline(ClientId, NetworkId),
logger:debug("[sdlan_channel] client_id: ~p, set none offline, result is: ~p", [ClientId, Result]);
false ->
ok
end,
logger:warning("[sdlan_channel] client_id: ~p, stop with reason: ~p", [ClientId, Reason]),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% helper methods
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec register_nak_reply(PacketId :: integer(), ErrorCode :: integer(), ErrorMsg :: binary()) -> binary().
register_nak_reply(PacketId, ErrorCode, ErrorMsg) when is_integer(PacketId), is_integer(ErrorCode), is_binary(ErrorMsg) ->
RegisterNakReply = sdlan_pb:encode_msg(#sdl_register_super_nak {
error_code = ErrorCode,
error_message = ErrorMsg
}),
<<PacketId:32, ?PACKET_REGISTER_SUPER_NAK, RegisterNakReply/binary>>.
rsa_encode(PlainText, RsaPubKey) when is_binary(PlainText) ->
iolist_to_binary(sdlan_cipher:rsa_encrypt(PlainText, RsaPubKey)).

View File

@ -1,55 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 27. 3 2024 17:43
%%%-------------------------------------------------------------------
-module(sdlan_ipaddr).
-author("anlicheng").
%% API
-export([ipv4_to_int/1, int_to_ipv4/1, ips/2, format_ip/1]).
-export([ipv6_bytes_to_binary/1]).
format_ip(Ip) when is_integer(Ip) ->
int_to_ipv4(Ip);
format_ip(Ip) ->
Ip.
-spec ipv4_to_int(Ip :: integer() | binary() | inet:ip4_address()) -> integer().
ipv4_to_int(Ip) when is_integer(Ip) ->
Ip;
ipv4_to_int({Ip0, Ip1, Ip2, Ip3}) ->
<<Ip:32>> = <<Ip0, Ip1, Ip2, Ip3>>,
Ip;
ipv4_to_int(Ip) when is_binary(Ip) ->
Parts0 = binary:split(Ip, <<".">>, [global]),
Parts = lists:map(fun binary_to_integer/1, Parts0),
<<IpInt:32>> = iolist_to_binary(Parts),
IpInt.
-spec int_to_ipv4(Ip :: integer()) -> binary().
int_to_ipv4(Ip) when is_integer(Ip) ->
<<Ip0, Ip1, Ip2, Ip3>> = <<Ip:32>>,
<<(integer_to_binary(Ip0))/binary, $., (integer_to_binary(Ip1))/binary, $., (integer_to_binary(Ip2))/binary, $., (integer_to_binary(Ip3))/binary>>.
-spec ips(NetAddr :: binary(), MaskLen :: integer()) -> [Ip :: integer()].
ips(NetAddr, MaskLen) when is_binary(NetAddr), is_integer(MaskLen) ->
Mask = 16#FFFFFFFF bsr MaskLen,
Net0 = ipv4_to_int(NetAddr),
%% : "192.168.1.101",
L = 32 - MaskLen,
Net = (Net0 bsr L) bsl L,
lists:map(fun(V) -> Net + V end, lists:seq(1, Mask - 1)).
-spec ipv6_bytes_to_binary(Bytes :: binary()) -> Bin :: binary().
ipv6_bytes_to_binary(<<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>) ->
Segments = [integer_to_list(X, 16) || X <- [A, B, C, D, E, F, G, H]],
% 4
Padded = [string:pad(S, 4, leading, $0) || S <- Segments],
% IPv6地址格式
iolist_to_binary(lists:flatten(string:join(Padded, ":")));
ipv6_bytes_to_binary(_) ->
<<"">>.

View File

@ -1,609 +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("sdlan_tables.hrl").
-behaviour(gen_server).
-define(FLOW_REPORT_INTERVAL, 60 * 1000).
%% broadcast, "FF-FF-FF-FF-FF-FF"
-define(BROADCAST_MAC, <<16#FF,16#FF,16#FF,16#FF,16#FF,16#FF>>).
%% API
-export([start_link/2]).
-export([get_name/1, get_pid/1, assign_ip_addr/6, peer_info/3, unregister/3, debug_info/1, get_network_id/1, get_used_map/1]).
-export([forward/5, update_hole/6, disable_client/2, get_channel/2, dropout_client/2, reload/1]).
-export([test_event/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-record(hole, {
peer :: {Ip :: inet:ip4_address(), Port :: integer()},
nat_type :: integer()
}).
%% ip的使用信息,
-record(host, {
client_id :: binary(),
channel_pid :: undefined | pid(),
monitor_ref :: undefined | reference(),
hole :: undefined | #hole{},
%% ip和ip_v6的映射关系, #{ip_addr :: integer() => {}}
v6_info :: undefined | #sdl_v6_info{}
}).
-record(state, {
network_id :: integer(),
name :: binary(),
domain :: binary(),
ipaddr :: binary(),
mask_len :: integer(),
owner_id :: integer(),
%%
throttle_key :: atom(),
%%
forward_bytes = 0,
%% , AES-256
aes_key :: binary(),
%% ip分配器
%% ip地址
ips = [] :: [Ip :: integer()],
%% 使ip, #{mac :: integer() => Host :: #host{}}
used_map = #{}
}).
%%%===================================================================
%%% API
%%%===================================================================
%% -- MARK:
test_event(Pid) ->
gen_server:cast(Pid, test_event).
-spec get_pid(Id :: integer()) -> undefined | pid().
get_pid(Id) when is_integer(Id) ->
whereis(get_name(Id)).
-spec get_name(Id :: integer()) -> atom().
get_name(Id) when is_integer(Id) ->
list_to_atom("sdlan_network:" ++ integer_to_list(Id)).
-spec reload(Pid :: pid()) -> ok | {error, Reason :: any()}.
reload(Pid) when is_pid(Pid) ->
gen_server:call(Pid, reload).
-spec assign_ip_addr(Pid :: pid(), ChannelPid :: pid(), ClientId :: binary(), Mac :: binary(), NetAddr :: integer(), HostName :: binary()) ->
{ok, Domain :: binary(), NetAddr :: integer(), MaskLen :: integer(), AesKey :: binary()} | {error, Reason :: any()}.
assign_ip_addr(Pid, ChannelPid, ClientId, Mac, NetAddr, HostName) when is_pid(Pid), is_pid(ChannelPid), is_binary(ClientId), is_binary(Mac), is_integer(NetAddr) ->
gen_server:call(Pid, {assign_ip_addr, ChannelPid, ClientId, Mac, NetAddr, 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 unregister(Pid :: pid(), ClientId :: binary(), Mac :: binary()) -> no_return().
unregister(Pid, ClientId, Mac) when is_pid(Pid), is_binary(ClientId), is_binary(Mac) ->
gen_server:cast(Pid, {unregister, ClientId, Mac}).
-spec peer_info(Pid :: pid(), SrcMac :: binary(), DstMac :: binary()) ->
error | {ok, {NatPeer :: {Ip :: inet:ip4_address(), Port :: integer()}, NatType :: integer()}, V6Info :: undefined | #sdl_v6_info{}}.
peer_info(Pid, SrcMac, DstMac) when is_pid(Pid), is_binary(SrcMac), is_binary(DstMac) ->
gen_server:call(Pid, {peer_info, SrcMac, DstMac}).
-spec forward(pid(), Sock :: any(), SrcMac :: binary(), DstMac :: binary(), Packet :: binary()) -> no_return().
forward(Pid, Sock, SrcMac, DstMac, Packet) when is_pid(Pid), is_binary(SrcMac), is_binary(DstMac), is_binary(Packet) ->
gen_server:cast(Pid, {forward, Sock, SrcMac, DstMac, Packet}).
%% ip地址对应的nat关系
-spec update_hole(Pid :: pid(), ClientId :: binary(), Mac :: binary(), Peer :: tuple(), NatType :: integer(), V6Info :: undefined | #sdl_v6_info{}) -> no_return().
update_hole(Pid, ClientId, Mac, Peer, NatType, V6Info) when is_pid(Pid), is_binary(ClientId), is_binary(Mac), is_integer(NatType) ->
gen_server:cast(Pid, {update_hole, ClientId, Mac, Peer, NatType, V6Info}).
-spec disable_client(Pid :: pid(), ClientId :: binary()) -> ok | error.
disable_client(Pid, ClientId) when is_pid(Pid), is_binary(ClientId) ->
gen_server:call(Pid, {disable_client, ClientId}).
-spec get_channel(Pid :: pid(), ClientId :: binary()) -> error | {ok, ChannelPid :: pid()}.
get_channel(Pid, ClientId) when is_pid(Pid), is_binary(ClientId) ->
gen_server:call(Pid, {get_channel, ClientId}).
%% client_idchannel不关闭; channel会被重新绑定到其他的network里面
-spec dropout_client(Pid :: pid(), ClientId :: binary()) -> {ok, ChannelPid :: pid(), 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).
-spec get_used_map(Pid :: pid()) -> map().
get_used_map(Pid) when is_pid(Pid) ->
gen_server:call(Pid, get_used_map);
get_used_map(undefined) ->
#{}.
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link(Name :: atom(), Id :: integer()) ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link(Name, Id) when is_atom(Name), is_integer(Id) ->
gen_server:start_link({local, Name}, ?MODULE, [Id], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([Id]) when is_integer(Id) ->
erlang:process_flag(trap_exit, true),
case sdlan_api:get_network(Id) of
{ok, #{<<"ipaddr">> := Null}} when Null == <<"null">>; Null == <<"NULL">> ->
ignore;
{ok, #{<<"id">> := Id, <<"name">> := Name, <<"domain">> := Domain, <<"ipaddr">> := IpAddr0, <<"owner_id">> := OwnerId}} ->
{IpAddr, MaskLen} = parse_ipaddr(IpAddr0),
Ips = sdlan_ipaddr:ips(IpAddr, MaskLen),
AesKey = sdlan_util:rand_byte(32),
%% key
ThrottleKey = list_to_atom("network_throttle:" ++ integer_to_list(Id)),
%%
sdlan_network_coordinator:attach(self(), ThrottleKey),
%%
erlang:start_timer(?FLOW_REPORT_INTERVAL, self(), flow_report_ticker),
%%
create_mnesia_table(Id),
logger:debug("[sdlan_network] network: ~p, ips: ~p", [Id, lists:map(fun sdlan_ipaddr:int_to_ipv4/1, Ips)]),
sdlan_domain_regedit:insert(Domain),
{ok, #state{network_id = Id, name = Name, domain = Domain, ipaddr = IpAddr, owner_id = OwnerId, mask_len = MaskLen, ips = Ips, 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{}}).
%%
handle_call(reload, _From, State = #state{network_id = Id, ipaddr = OldIpAddr, mask_len = OldMarkLen, used_map = UsedMap}) ->
case sdlan_api:get_network(Id) of
{ok, #{<<"name">> := Name, <<"ipaddr">> := IpAddr0, <<"owner_id">> := OwnerId}} ->
{IpAddr, MaskLen} = parse_ipaddr(IpAddr0),
case OldIpAddr =:= IpAddr andalso OldMarkLen =:= MaskLen of
true ->
{reply, ok, State#state{name = Name, owner_id = OwnerId}};
false ->
logger:debug("[sdlan_networkd] network_id: ~p, reload will close all channels", [Id]),
Ips = sdlan_ipaddr:ips(IpAddr, MaskLen),
%%
maps:foreach(fun(_, #host{channel_pid = ChannelPid, monitor_ref = MRef}) ->
is_reference(MRef) andalso demonitor(MRef),
is_process_alive(ChannelPid) andalso sdlan_channel:stop(ChannelPid, normal)
end, UsedMap),
%%
ok = client_model:delete_clients(Id),
{reply, ok, State#state{name = Name, ipaddr = IpAddr,
owner_id = OwnerId, mask_len = MaskLen, ips = Ips, used_map = maps:new()}}
end;
{error, Reason} ->
logger:warning("[sdlan_network] reload network: ~p, get error: ~p", [Id, Reason]),
{reply, {error, Reason}, State}
end;
%% ip地址
handle_call({assign_ip_addr, ChannelPid, ClientId, Mac, NetAddr0, HostName}, _From,
State = #state{network_id = NetworkId, domain = Domain, ips = Ips, used_map = UsedMap, mask_len = MaskLen, aes_key = AesKey}) ->
%% ip地址的时候mac地址为唯一基准
logger:debug("[sdlan_network] alloc_ip, network_id: ~p, ips: ~p, client_id: ~p, mac: ~p, net_addr: ~p",
[NetworkId, Ips, ClientId, sdlan_util:format_mac(Mac), sdlan_ipaddr:int_to_ipv4(NetAddr0)]),
case client_model:alloc_ip(NetworkId, Ips, ClientId, Mac, NetAddr0, HostName) of
{ok, Ip} ->
%% channel
maybe_close_channel(maps:get(Mac, UsedMap, undefined)),
%% ->ip的映射关系
case HostName =/= <<>> of
true ->
FullHostname = <<HostName/binary, ".", Domain/binary>>,
sdlan_hostname_regedit:insert(FullHostname, Ip);
false ->
ok
end,
%% channel之间的关系
MRef = monitor(process, ChannelPid),
NUsedMap = maps:put(Mac, #host{client_id = ClientId, channel_pid = ChannelPid, monitor_ref = MRef}, UsedMap),
{reply, {ok, Domain, Ip, MaskLen, AesKey}, State#state{used_map = NUsedMap}};
{error, Reason} ->
{reply, {error, Reason}, State}
end;
handle_call(get_used_map, _From, State = #state{used_map = UsedMap}) ->
UsedInfos = maps:map(fun(_, #host{hole = Hole, v6_info = V6Info}) ->
HoleMap = case Hole of
#hole{peer = {NatIp, NatPort}} ->
#{
<<"nat_ip">> => sdlan_ipaddr:int_to_ipv4(sdlan_ipaddr:ipv4_to_int(NatIp)),
<<"nat_port">> => NatPort
};
_ ->
#{}
end,
V6Map = case V6Info of
#sdl_v6_info{v6 = IpV6, port = Port} ->
#{
<<"v6_ip">> => sdlan_ipaddr:ipv6_bytes_to_binary(IpV6),
<<"v6_port">> => Port
};
_ ->
#{}
end,
#{<<"hole">> => HoleMap, <<"v6_info">> => V6Map}
end, UsedMap),
{reply, {ok, UsedInfos}, State};
%% client设置为禁止状态
handle_call({disable_client, ClientId}, _From, State = #state{network_id = NetworkId, used_map = UsedMap}) ->
case lists:search(fun({_, #host{client_id = ClientId0}}) -> ClientId =:= ClientId0 end, maps:to_list(UsedMap)) of
{value, {Mac, #host{channel_pid = ChannelPid, monitor_ref = MRef}}} ->
is_reference(MRef) andalso demonitor(MRef),
sdlan_channel:stop(ChannelPid, disable),
NUsedMap = maps:remove(Mac, UsedMap),
%%
client_model:disable_client(NetworkId, ClientId),
{reply, ok, State#state{used_map = NUsedMap}};
false ->
{reply, error, State}
end;
handle_call({get_channel, ClientId}, _From, State = #state{used_map = UsedMap}) ->
case lists:search(fun({_, #host{client_id = ClientId0}}) -> ClientId =:= ClientId0 end, maps:to_list(UsedMap)) of
{value, {_Ip, #host{channel_pid = ChannelPid}}} ->
{reply, {ok, ChannelPid}, State};
false ->
{reply, error, State}
end;
%% channel, ; drop的时候需要从当前网络中移除
handle_call({dropout_client, ClientId}, _From, State = #state{network_id = NetworkId, used_map = UsedMap}) ->
case lists:search(fun({_, #host{client_id = ClientId0}}) -> ClientId =:= ClientId0 end, maps:to_list(UsedMap)) of
{value, {Mac, #host{channel_pid = ChannelPid, monitor_ref = MRef}}} ->
is_reference(MRef) andalso demonitor(MRef),
NUsedMap = maps:remove(Mac, UsedMap),
%%
case client_model:delete_client(NetworkId, ClientId) of
{ok, #client{host_name = HostName}} ->
{reply, {ok, ChannelPid, HostName}, State#state{used_map = NUsedMap}};
{error, _} ->
{reply, error, State}
end;
false ->
{reply, error, State}
end;
handle_call(get_network_id, _From, State = #state{network_id = NetworkId}) ->
{reply, {ok, NetworkId}, State};
%% nat_peer信息
handle_call({peer_info, SrcMac, DstMac}, _From, State = #state{used_map = UsedMap}) ->
case maps:find(DstMac, UsedMap) of
{ok, #host{channel_pid = DstChannelPid, hole = #hole{peer = DstNatPeer, nat_type = DstNatType}, v6_info = DstV6Info}} ->
%% sendRegister事件(2024-06-25 )
case maps:get(SrcMac, UsedMap, undefined) of
#host{hole = #hole{peer = {SrcNatIp, SrcNatPort}, nat_type = NatType}, v6_info = SrcV6Info} ->
Event = sdlan_pb:encode_msg(#sdl_send_register_event {
dst_mac = SrcMac,
nat_ip = sdlan_ipaddr:ipv4_to_int(SrcNatIp),
nat_type = NatType,
nat_port = SrcNatPort,
v6_info = SrcV6Info
}),
sdlan_channel:send_event(DstChannelPid, ?PACKET_EVENT_SEND_REGISTER, Event);
_ ->
ok
end,
{reply, {ok, {DstNatPeer, DstNatType}, DstV6Info}, State};
_ ->
{reply, error, State}
end;
handle_call(debug_info, _From, State = #state{network_id = NetworkId, ipaddr = IpAddr, mask_len = MaskLen, owner_id = OwnerId, ips = Ips, used_map = UsedMap}) ->
Reply = #{
<<"network_id">> => NetworkId,
<<"ipaddr">> => IpAddr,
<<"mask_len">> => MaskLen,
<<"owner_id">> => OwnerId,
<<"ips">> => lists:map(fun sdlan_ipaddr:int_to_ipv4/1, Ips),
<<"used_ips">> => lists:map(fun({_, Host = #host{client_id = ClientId}}) ->
case client_model:get_client(NetworkId, ClientId) of
error ->
#{};
{ok, #client{mac = Mac, ip = Ip}} ->
format_host(Host, Ip, Mac)
end
end, maps:to_list(UsedMap))
},
{reply, Reply, State}.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
%% , mac地址单播
handle_cast({forward, Sock, SrcMac, DstMac, Packet}, State = #state{network_id = NetworkId, used_map = UsedMap, throttle_key = ThrottleKey, forward_bytes = ForwardBytes})
when is_map_key(SrcMac, UsedMap), is_map_key(DstMac, UsedMap) ->
PacketBytes = byte_size(Packet),
case maps:find(DstMac, UsedMap) of
{ok, #host{hole = #hole{peer = Peer = {Ip, Port}}}} ->
case throttle:check(sdlan_network, ThrottleKey) of
{ok, _RestCount, _LeftToReset} ->
%% client和stun之间必须有心跳机制保持nat映射可用udp包肯定可以到达对端的nat
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}};
{limit_exceeded, 0, _LeftToReset} ->
%%
case sdlan_network_coordinator:checkout() of
ok ->
logger:debug("[sdlan_network] use release forward data networkd_id: ~p, src_mac: ~p, dst_mac: ~p, hole: ~p",
[NetworkId, sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac), Peer]),
gen_udp:send(Sock, Ip, Port, Packet),
{noreply, State#state{forward_bytes = ForwardBytes + PacketBytes}};
error ->
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
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, used_map = UsedMap, forward_bytes = ForwardBytes})
when is_map_key(SrcMac, UsedMap) ->
%% 广
case sdlan_util:is_broadcast_mac(DstMac) orelse sdlan_util:is_multicast_mac(DstMac) of
true ->
PacketBytes = byte_size(Packet),
%% 广
maps:foreach(fun(Mac, #host{hole = Hole}) ->
case {Mac =/= SrcMac, Hole} of
{true, #hole{peer = {NatIp, NatPort}}} ->
logger:debug("[sdlan_network] call me here"),
gen_udp:send(Sock, NatIp, NatPort, Packet);
_ ->
ok
end
end, UsedMap),
%% 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, used_map = UsedMap}) ->
logger:debug("[sdlan_network] networkd_id: ~p, unregister Mac: ~p", [NetworkId, sdlan_util:format_mac(Mac)]),
case maps:take(Mac, UsedMap) of
error ->
{noreply, State};
{#host{channel_pid = ChannelPid, monitor_ref = MRef}, NUsedMap} ->
is_reference(MRef) andalso demonitor(MRef),
sdlan_channel:stop(ChannelPid, normal),
{noreply, State#state{used_map = NUsedMap}}
end;
%% client是属于当前网络的
handle_cast({update_hole, ClientId, Mac, Peer, NatType, V6Info}, State = #state{network_id = NetworkId, used_map = UsedMap}) ->
case {maps:find(Mac, UsedMap), client_model:get_client(NetworkId, ClientId)} of
{{ok, Host0 = #host{client_id = ClientId0, hole = OldHole}}, {ok, #client{ip = Ip}}} when ClientId =:= ClientId0 ->
case OldHole =:= undefined orelse (OldHole#hole.peer =/= Peer orelse OldHole#hole.nat_type =/= NatType) of
true ->
NatChangedEvent = sdlan_pb:encode_msg(#sdl_nat_changed_event{
mac = Mac,
ip = Ip
}),
broadcast(?PACKET_EVENT_NAT_CHANGED, NatChangedEvent, Mac, UsedMap);
false ->
ok
end,
Host = Host0#host{hole = #hole{peer = Peer, nat_type = NatType}, v6_info = V6Info},
{noreply, State#state{used_map = maps:put(Mac, Host, UsedMap)}};
_ ->
{noreply, State}
end.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info({timeout, _, flow_report_ticker}, State = #state{network_id = NetworkId, forward_bytes = ForwardBytes}) ->
erlang:start_timer(?FLOW_REPORT_INTERVAL, self(), flow_report_ticker),
catch sdlan_api:network_forward_report(NetworkId, ForwardBytes),
{noreply, State#state{forward_bytes = 0}};
handle_info({'EXIT', _Pid, shutdown}, State = #state{network_id = NetworkId, used_map = UsedMap}) ->
logger:warning("[sdlan_network] network: ~p, get shutdown message", [NetworkId]),
broadcast_shutdown(UsedMap),
{stop, shutdown, State};
%% Channel进程退出, hole里面的数据也需要清理
handle_info({'DOWN', _MRef, process, ChannelPid, Reason}, State = #state{network_id = NetworkId, used_map = UsedMap}) ->
logger:notice("[sdlan_network] network_id: ~p, channel_pid: ~p, close with reason: ~p", [NetworkId, ChannelPid, Reason]),
NUsedMap = maps:filter(fun(_, #host{channel_pid = ChannelPid0}) -> ChannelPid =/= ChannelPid0 end, UsedMap),
{noreply, State#state{used_map = NUsedMap}}.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(Reason, #state{network_id = NetworkId, used_map = UsedMap}) ->
logger:debug("[sdlan_network] network: ~p, will terminate with reason: ~p", [NetworkId, Reason]),
broadcast_shutdown(UsedMap),
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
%%
-spec create_mnesia_table(NetworkId :: integer()) -> no_return().
create_mnesia_table(NetworkId) when is_integer(NetworkId) ->
Tab = client_model:get_table_name(NetworkId),
Tables = mnesia:system_info(tables),
case lists:member(Tab, Tables) of
true ->
ok;
false ->
Res = client_model:create_table(Tab),
logger:debug("[sdlan_network] create table result: ~p", [Res])
end.
-spec maybe_close_channel(undefined | #host{}) -> no_return().
maybe_close_channel(#host{channel_pid = ChannelPid0, monitor_ref = MRef0}) ->
case is_pid(ChannelPid0) andalso is_process_alive(ChannelPid0) of
true ->
is_reference(MRef0) andalso demonitor(MRef0),
sdlan_channel:stop(ChannelPid0, channel_rebind);
false ->
ok
end;
maybe_close_channel(_) ->
ok.
-spec broadcast(EventType :: integer(), Event :: binary(), ExcludeMac :: binary(), UsedMap :: map()) -> no_return().
broadcast(EventType, Event, ExcludeMac, UsedMap) when is_map(UsedMap), is_binary(ExcludeMac), is_integer(EventType), is_binary(Event) ->
maps:foreach(fun(Mac, #host{channel_pid = ChannelPid}) ->
case is_process_alive(ChannelPid) andalso ExcludeMac /= Mac of
true ->
sdlan_channel:send_event(ChannelPid, EventType, Event);
false ->
ok
end
end, UsedMap).
broadcast_shutdown(UsedMap) when is_map(UsedMap) ->
maps:foreach(fun(_, #host{channel_pid = ChannelPid}) ->
case is_process_alive(ChannelPid) of
true ->
NetworkShutdownEvent = sdlan_pb:encode_msg(#sdl_network_shutdown_event {
message = <<"Network shutdown">>
}),
sdlan_channel:send_event(ChannelPid, ?PACKET_EVENT_NETWORK_SHUTDOWN, NetworkShutdownEvent),
sdlan_channel:stop(ChannelPid, normal);
false ->
ok
end
end, UsedMap).
%% IpAddr: <<"192.168.172/24">>
-spec parse_ipaddr(IpAddr0 :: binary()) -> {IpAddr :: binary(), MaskLen :: integer()}.
parse_ipaddr(IpAddr0) when is_binary(IpAddr0) ->
case binary:split(IpAddr0, <<"/">>) of
[IpAddr, MaskLen] ->
MaskLen1 = binary_to_integer(MaskLen),
{IpAddr, MaskLen1};
_ ->
{IpAddr0, 24}
end.
-spec format_host(Host :: #host{}, Ip :: integer(), Mac :: binary()) -> map().
format_host(#host{client_id = ClientId, hole = Hole, v6_info = V6Info}, Ip, Mac) when is_integer(Ip), is_binary(Mac) ->
HoleMap = case Hole of
undefined ->
#{};
#hole{peer = {NatIp, NatPort}, nat_type = NatType} ->
#{
nat_ip => NatIp,
nat_port => NatPort,
nat_type => NatType
}
end,
V6InfoMap = case V6Info of
undefined ->
#{};
#sdl_v6_info{v6 = V6, port = V6Port} ->
#{v6 => V6, port => V6Port}
end,
#{
client_id => ClientId,
mac => sdlan_util:format_mac(Mac),
ip => sdlan_ipaddr:int_to_ipv4(Ip),
hole_map => HoleMap,
v6_info => V6InfoMap
}.

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 09. 3 2024 15:25
%%%-------------------------------------------------------------------
-module(sdlan_test).
-author("anlicheng").
%% API
-export([test/1]).
test(X) when X band 1 == 0 ->
ok;
test(_) ->
error.

View File

@ -1,74 +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]).
-export([json_data/1, json_error/2]).
-export([is_broadcast_mac/1, is_multicast_mac/1]).
-spec format_mac(Mac :: binary()) -> binary().
format_mac(Mac) when is_binary(Mac) ->
Hex = fun
(N) when N < 10 ->
$0 + N;
(N) ->
$a + (N - 10)
end,
Y = [[Hex(X0), Hex(X1)] || <<X0:4, X1:4>> <= Mac],
list_to_binary(lists:flatten(lists:join(":", Y))).
%%
rand_byte(Num) when is_integer(Num), Num > 0 ->
rand_byte0(Num, <<>>).
rand_byte0(0, Acc) ->
Acc;
rand_byte0(Num, Acc) ->
Byte = ceil(rand:uniform() * 255),
rand_byte0(Num - 1, <<Acc/binary, Byte>>).
%% md5哈希算法
-spec md5(string() | binary()) -> string().
md5(Str) when is_binary(Str) ->
md5(binary_to_list(Str));
md5(Str) when is_list(Str) ->
Hash = binary_to_list(erlang:md5(Str)),
lists:flatten([hex(I) || I <- Hash]).
hex(I) when I > 16#f ->
[hex0((I band 16#f0) bsr 4), hex0(I band 16#0f)];
hex(I) ->
[$0, hex0(I)].
hex0(10) -> $a;
hex0(11) -> $b;
hex0(12) -> $c;
hex0(13) -> $d;
hex0(14) -> $e;
hex0(15) -> $f;
hex0(I) -> $0 + I.
json_data(Data) ->
jiffy:encode(#{<<"result">> => Data}, [force_utf8]).
json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) ->
jiffy:encode(#{<<"error">> => #{<<"code">> => ErrCode, <<"message">> => ErrMessage}}, [force_utf8]).
assert_call(true, F) ->
F();
assert_call(false, _) ->
ok.
-spec is_broadcast_mac(Mac :: binary()) -> boolean().
is_broadcast_mac(Mac) when is_binary(Mac) ->
Mac =:= <<16#FF,16#FF,16#FF,16#FF,16#FF,16#FF>>.
-spec is_multicast_mac(Mac :: binary()) -> boolean().
is_multicast_mac(Mac) when is_binary(Mac) ->
binary:part(Mac, 0, 3) =:= <<16#01,16#00,16#5E>>.

View File

@ -1,20 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2026, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 11. 2 2026 15:49
%%%-------------------------------------------------------------------
-module(sdlan_quicer_test).
-author("anlicheng").
%% API
-export([test/0]).
test() ->
{ok, Conn} = quicer:connect("http3.is", 443, [{alpn, ["h3"]},
{verify, verify_peer},
{peer_unidi_stream_count, 3}], 5000),
logger:debug("conn is: ~p", [Conn]),
quicer:shutdown_connection(Conn).

View File

@ -1,110 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 17. 4 2024 10:35
%%%-------------------------------------------------------------------
-module(sdlan_udp_downloader).
-author("anlicheng").
-behaviour(gen_server).
%% API
-export([start_link/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-define(SERVER, ?MODULE).
-record(state, {
socket
}).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link() ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([]) ->
{ok, Socket} = gen_udp:open(22222, [binary]),
{ok, #state{socket = Socket}}.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_call(_Request, _From, State) ->
{reply, ok, State}.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_cast(_Info, State) ->
{noreply, State}.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info({udp, Sock, Ip, Port, <<1>>}, State = #state{socket = Sock}) ->
{ok, Content} = file:read_file("/tmp/files/test.dmg"),
send_file_content(Sock, Ip, Port, 1200, Content),
{noreply, State#state{}}.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) ->
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
send_file_content(Sock, Ip, Port, Size, Content) when byte_size(Content) =< Size ->
gen_udp:send(Sock, Ip, Port, Content);
send_file_content(Sock, Ip, Port, Size, Content) ->
<<Part:Size/binary, Rest/binary>> = Content,
gen_udp:send(Sock, Ip, Port, Part),
send_file_content(Sock, Ip, Port, Size, Rest).

View File

@ -1,114 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 17. 4 2024 10:35
%%%-------------------------------------------------------------------
-module(sdlan_udp_wget).
-author("anlicheng").
-behaviour(gen_server).
%% API
-export([start_link/0]).
-export([wget/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-record(state, {
socket,
bytes = 0
}).
%%%===================================================================
%%% API
%%%===================================================================
wget() ->
gen_server:call(?MODULE, wget).
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link() ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([]) ->
{ok, Socket} = gen_udp:open(0, [binary, {active, true}]),
inet_udp:controlling_process(Socket, self()),
erlang:start_timer(5000, self(), qps_ticker),
{ok, #state{socket = Socket}}.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_call(wget, _From, State=#state{socket = Socket}) ->
gen_udp:send(Socket, "127.0.0.1", 22222, <<1>>),
{reply, ok, State}.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_cast(_Info, State) ->
{noreply, State}.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info({udp, Sock, _Ip, _Port, Data}, State = #state{socket = Sock, bytes = Bytes}) ->
{noreply, State#state{bytes = Bytes + byte_size(Data)}};
handle_info({timeout, _, qps_ticker}, State = #state{bytes = Bytes}) ->
logger:debug("[sdlan_udp_wget] qps is: ~p(M)", [Bytes / 1024 / 1024]),
erlang:start_timer(5000, self(), qps_ticker),
{noreply, State#state{bytes = 0}}.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) ->
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================

View File

@ -1,208 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 08. 4 2024 10:37
%%%-------------------------------------------------------------------
-module(stun_client).
-author("anlicheng").
-include("sdlan_pb.hrl").
-behaviour(gen_server).
%% API
-export([start_link/0]).
-export([register/1, debug_info/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-define(SERVER, ?MODULE).
%%
-define(STUN_REGISTER, 3).
%%
-define(STUN_REGISTER_ACK, 4).
-define(STUN_DATA, 5).
-record(state, {
socket,
tun_socket,
client_id :: binary(),
network_id,
net_addr,
mask_len,
aes_key,
cookie = 1,
sessions = #{}
}).
%%%===================================================================
%%% API
%%%===================================================================
register(Pid) when is_pid(Pid) ->
gen_server:call(Pid, register).
debug_info(Pid) ->
gen_server:call(Pid, debug_info).
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link() ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
gen_server:start_link(?MODULE, [], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([]) ->
{ok, Socket} = gen_tcp:connect("localhost", 18083, [binary, {packet, 2}, {active, true}]),
inet_tcp:controlling_process(Socket, self()),
{ok, TunSocket} = gen_udp:open(12345, [binary, {active, true}]),
{ok, #state{socket = Socket, tun_socket = TunSocket, client_id = <<"22222222222222222222222222222222">>}}.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_call(debug_info, _From, State) ->
{reply, {ok, State}, State};
handle_call(register, _From, State = #state{socket = Socket, client_id = ClientId}) ->
Req = #{
<<"version">> => 1,
<<"client_id">> => ClientId,
<<"dev_addr">> => #{
<<"net_addr">> => 0,
<<"net_bit_len">> => 0
},
<<"token">> => <<"1234567890">>
},
Register = #sdl_register_super {
version = 1,
installed_channel = <<"macos">>,
client_id = ClientId,
dev_addr = #sdl_dev_addr {
network_id = 0,
mac = <<11, 12, 13, 14, 15, 16>>,
net_addr = 0,
net_bit_len = 0
},
pub_key = <<>>,
token = <<"1234567890">>
},
logger:debug("register is: ~p", [Register]),
Packet = jiffy:encode(Req, [force_utf8]),
ok = gen_tcp:send(Socket, <<1:32, 101, Packet/binary>>),
{reply, ok, State}.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_cast(_Request, State = #state{}) ->
{noreply, State}.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info({tcp, Socket, <<1:32, 5, Data/binary>>}, State = #state{socket = Socket, tun_socket = TunSocket, client_id = ClientId, cookie = Cookie}) ->
Response = jiffy:decode(Data, [return_maps]),
#{
<<"dev_addr">> := #{
<<"network_id">> := NetworkId,
<<"net_addr">> := NetAddr,
<<"net_bit_len">> := NetBitLen
},
<<"aes_key">> := AesKey,
<<"lifetime">> := Lifetime
} = Response,
logger:debug("[stun_client] get a register super response: ~p, alloc ip addr: ~p", [Response, sdlan_ipaddr:int_to_ipv4(NetAddr)]),
%% tun信息
gen_udp:send(TunSocket, "localhost", 1265, <<1, Cookie:32, ClientId/binary, NetworkId:32, NetAddr:32>>),
{noreply, State#state{network_id = NetworkId, net_addr = NetAddr, mask_len = NetBitLen, aes_key = AesKey, cookie = Cookie + 1}};
handle_info({udp, _, _, _, <<2, Cookie:32, Family, Port:16, Ip0, Ip1, Ip2, Ip3>>}, State = #state{}) ->
logger:debug("[stun_client] tun register ack, cookie: ~p, ack: ~p", [Cookie, {Family, Port, {Ip0, Ip1, Ip2, Ip3}}]),
{noreply, State};
handle_info({udp, _, Ip, Port, <<?STUN_REGISTER:8, NetworkId:32, SrcIp:32, DstIp:32>>}, State = #state{tun_socket = TunSocket, sessions = Sessions}) ->
Packet = <<?STUN_REGISTER_ACK, NetworkId:32, DstIp:32, SrcIp:32>>,
logger:debug("[stun_client] will send stun reply: ~p, peer: ~p", [Packet, {Ip, Port}]),
ok = gen_udp:send(TunSocket, Ip, Port, Packet),
NSessions = maps:put(SrcIp, {Ip, Port}, Sessions),
{noreply, State#state{sessions = NSessions}};
handle_info({udp, _, Ip, Port, <<?STUN_REGISTER_ACK, NetworkId:32, SrcIp:32, DstIp:32>>}, State = #state{sessions = Sessions}) ->
logger:debug("[stun_client] stun_data: network_id: ~p, src: ~p, dst: ~p, register_ack!!!", [NetworkId, SrcIp, DstIp]),
NSessions = maps:put(SrcIp, {Ip, Port}, Sessions),
{noreply, State#state{sessions = NSessions}};
handle_info({udp, _, _Ip0, _Port0, <<?STUN_DATA, NetworkId:32, SrcIp:32, DstIp:32, TTL:8, Data/binary>>}, State = #state{tun_socket = TunSocket, sessions = Sessions}) ->
logger:debug("[stun_client] stun_data: network_id: ~p, src: ~p, dst: ~p, data!!!", [NetworkId, SrcIp, DstIp]),
case maps:is_key(SrcIp, Sessions) of
true ->
{Ip, Port} = maps:get(SrcIp, Sessions),
ok = gen_udp:send(TunSocket, Ip, Port, <<?STUN_DATA, NetworkId:32, DstIp:32, SrcIp:32, 255, Data/binary>>),
logger:debug("[stun_client] stun_data: network_id: ~p, src: ~p, dst: ~p, reply data!!!", [NetworkId, SrcIp, DstIp]);
false ->
logger:debug("[stun_client] stun_data: network_id: ~p, src: ~p, dst: ~p, no session", [NetworkId, SrcIp, DstIp])
end,
{noreply, State};
handle_info(Info, State = #state{}) ->
logger:debug("[stun_client] get info: ~p", [Info]),
{noreply, State}.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) ->
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================

View File

@ -3,16 +3,21 @@
{http_server, [
{port, 18082},
{acceptors, 500},
{acceptors, 1},
{max_connections, 10240},
{backlog, 10240}
]},
{tcp_server, [
{port, 18083},
{acceptors, 500},
{max_connections, 10240},
{backlog, 10240}
{quic_server, [
{port, 443},
{alpn, ["punchnet/1.0"]},
{certfile, "cert.pem"},
{keyfile, "key.pem"},
{limits, [
{max_packet_size, 16384},
%% 单位为秒
{heartbeat_sec, 5}
]}
]},
%% 网络带宽, 单位为: kb
@ -20,23 +25,23 @@
%% stun类型探测相当于有个类型
{stun_servers, [
{port, 1265},
{port, 1365},
{acceptor_nums, 5}
]},
{stun_port_assist, [
{port, 1266}
{port, 1366}
]},
{stun_peer_assist, [
{ip, {47,98,178,3}},
{port, 1266}
{port, 1366}
]},
%% 公共的dns域名解析服务
{public_dns_servers, [
{{114, 114, 114, 114}, 53},
{{8,8,8,8}, 53}
{ipv6_assist, [
{port, 1367},
{acceptor_nums, 5},
{global_addr6, "2408:4005:318:4900:164a:b5c6:9023:ca8d"}
]},
{pools, [
@ -44,20 +49,15 @@
{mysql_sdlan,
[{size, 10}, {max_overflow, 20}, {worker_module, mysql}],
[
{host, {39, 98, 184, 67}},
{host, {118, 178, 229, 213}},
{port, 3306},
{user, "sdlanuser"},
{user, "punchnetuser"},
{connect_mode, lazy},
{keep_alive, true},
{password, "sdlan@J1c8WGu"},
{database, "sdlan"},
{password, "punchnet@J1c8WGu"},
{database, "punchnet_v2"},
{queries, [<<"set names utf8">>]}
]
},
{dns_resolver_pool,
[{size, 20}, {max_overflow, 100}, {worker_module, dns_resolver}],
[]
}
]},

View File

@ -3,16 +3,21 @@
{http_server, [
{port, 18082},
{acceptors, 500},
{acceptors, 50},
{max_connections, 10240},
{backlog, 10240}
]},
{tcp_server, [
{port, 18083},
{acceptors, 500},
{max_connections, 10240},
{backlog, 10240}
{quic_server, [
{port, 443},
{alpn, ["punchnet/1.0"]},
{certfile, "fullchain.cer"},
{keyfile, "root.punchsky.com.key"},
{limits, [
{max_packet_size, 16384},
%% 单位为秒
{heartbeat_sec, 5}
]}
]},
%% 网络带宽, 单位为: kb
@ -20,23 +25,23 @@
%% stun类型探测相当于有个类型
{stun_servers, [
{port, 1265},
{acceptor_nums, 1}
{port, 1365},
{acceptor_nums, 5}
]},
{stun_port_assist, [
{port, 1266}
{port, 1366}
]},
{stun_peer_assist, [
{ip, {47,98,178,3}},
{port, 1266}
{port, 1366}
]},
%% 公共的dns域名解析服务
{public_dns_servers, [
{{114, 114, 114, 114}, 53},
{{8,8,8,8}, 53}
{ipv6_assist, [
{port, 1367},
{acceptor_nums, 5},
{global_addr6, "2408:4005:318:4900:164a:b5c6:9023:ca8d"}
]},
{pools, [
@ -46,23 +51,16 @@
[
{host, {118, 178, 229, 213}},
{port, 3306},
{user, "sdlanuser"},
{connect_mode, lazy},
{user, "punchnetuser"},
{keep_alive, true},
{password, "sdlan@J1c8WGu"},
{database, "sdlan"},
{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://punchnet.aioe.tech/api/"}
{api_url, "https://root.punchsky.com/api/"}
]},

2
docs/mac.md Normal file
View File

@ -0,0 +1,2 @@
## 查看当前系统的dns
scutil --dns

40
include/policy.hrl Normal file
View File

@ -0,0 +1,40 @@
%%%-------------------------------------------------------------------
%%% @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()
}).

View File

@ -13,10 +13,12 @@
-define(DEFAULT_PASS, <<"`encrypt!`">>).
%%
-define(PACKET_EMPTY, 16#00).
%%
-define(PACKET_REGISTER_SUPER, 16#01).
-define(PACKET_REGISTER_SUPER_ACK, 16#02).
-define(PACKET_REGISTER_SUPER_ACKNOWLEDGE, 16#03).
-define(PACKET_REGISTER_SUPER_NAK, 16#04).
-define(PACKET_UNREGISTER, 16#05).
@ -31,23 +33,10 @@
%% ,
-define(PACKET_EVENT, 16#10).
%%
-define(PACKET_EVENT_KNOWN_IP, 16#01).
-define(PACKET_EVENT_DROP_IP, 16#02).
-define(PACKET_EVENT_NAT_CHANGED, 16#03).
-define(PACKET_EVENT_SEND_REGISTER, 16#04).
%%
-define(PACKET_EVENT_NETWORK_SHUTDOWN, 16#FF).
%% ,
-define(PACKET_COMMAND, 16#11).
-define(PACKET_COMMAND_ACK, 16#12).
%%
-define(PACKET_COMMAND_CHANGE_NETWORK, 16#01).
-define(PACKET_COMMAND_UPGRADE, 16#02).
%%
-define(PACKET_FLOW_TRACER, 16#15).
@ -55,7 +44,6 @@
-define(PACKET_REGISTER_ACK, 16#21).
%% stun相关的请求
%%
-define(PACKET_STUN_REQUEST, 16#30).
%%
@ -69,15 +57,21 @@
%% stun消息转发
-define(PACKET_STUN_PROBE_RELAY, 16#3a).
%% stun请求的attr
-define(STUN_ATTR_CHANGE_NONE, 0).
-define(STUN_ATTR_CHANGE_PORT, 1).
-define(STUN_ATTR_CHANGE_PEER, 2).
%%
-define(PACKET_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).
%%
-define(PACKET_STUN_DATA, 16#FF).
-record(id_generator, {
tab :: atom(),
increment_id = 0 :: integer()
}).
%% stun请求的attr
-define(STUN_ATTR_CHANGE_NONE, 0).
-define(STUN_ATTR_CHANGE_PORT, 1).
-define(STUN_ATTR_CHANGE_PEER, 2).

270
include/sdlan_pb.hrl Normal file
View File

@ -0,0 +1,270 @@
%% -*- 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 Normal file
View File

@ -0,0 +1,48 @@
## 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;
```

View File

@ -1,155 +0,0 @@
syntax = "proto3";
//
message SDLV4Info {
uint32 port = 1;
bytes v4 = 2;
uint32 nat_type = 3;
}
message SDLV6Info {
uint32 port = 1;
bytes v6 = 2;
}
//
message SDLDevAddr {
uint32 network_id = 1;
bytes mac = 2;
uint32 net_addr = 3;
uint32 net_bit_len = 4;
string network_domain = 5;
}
// tcp通讯消息
message SDLEmpty {
}
message SDLRegisterSuper {
uint32 version = 1;
string installed_channel = 2;
string client_id = 3;
SDLDevAddr dev_addr = 4;
string pub_key = 5;
string token = 6;
string network_code = 7;
string hostname = 8;
}
message SDLRegisterSuperAck {
SDLDevAddr dev_addr = 1;
bytes aes_key = 2;
uint32 upgrade_type = 3;
optional string upgrade_prompt = 4;
optional string upgrade_address = 5;
}
message SDLRegisterSuperNak {
uint32 error_code = 1;
string error_message = 2;
}
//
message SDLQueryInfo {
bytes dst_mac = 1;
}
message SDLPeerInfo {
bytes dst_mac = 1;
SDLV4Info v4_info = 2;
optional SDLV6Info v6_info = 3;
}
//
message SDLNatChangedEvent {
bytes mac = 1;
uint32 ip = 2;
}
message SDLSendRegisterEvent {
bytes dst_mac = 1;
uint32 nat_ip = 2;
uint32 nat_port = 3;
uint32 nat_type = 4;
optional SDLV6Info v6_info = 5;
}
message SDLNetworkShutdownEvent {
string message = 1;
}
//
message SDLChangeNetworkCommand {
SDLDevAddr dev_addr = 1;
bytes aes_key = 2;
}
message SDLCommandAck {
// status = true, status = false message是失败原因描述
bool status = 1;
optional string message = 2;
}
message SDLFlows {
//
uint32 forward_num = 1;
// p2p直接流量
uint32 p2p_num = 2;
//
uint32 inbound_num = 3;
}
// UDP通讯消息
message SDLStunRequest {
uint32 cookie = 1;
string client_id = 2;
uint32 network_id = 3;
bytes mac = 4;
uint32 ip = 5;
uint32 nat_type = 6;
optional SDLV6Info v6_info = 7;
}
message SDLStunReply {
uint32 cookie = 1;
}
message SDLData {
uint32 network_id = 1;
bytes src_mac = 2;
bytes dst_mac = 3;
bool is_p2p = 4;
uint32 ttl = 5;
bytes data = 6;
}
message SDLRegister {
uint32 network_id = 1;
bytes src_mac = 2;
bytes dst_mac = 3;
}
message SDLRegisterAck {
uint32 network_id = 1;
bytes src_mac = 2;
bytes dst_mac = 3;
}
//
message SDLStunProbe {
uint32 cookie = 1;
uint32 attr = 2;
}
message SDLStunProbeReply {
uint32 cookie = 1;
uint32 port = 2;
uint32 ip = 3;
}

43
policy.sql Normal file
View File

@ -0,0 +1,43 @@
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 `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
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

21
priv/cert.pem Normal file
View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUEhYvMYhAARHRpKd1EOTR7HiRIVkwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNjAyMTEwOTMyMjlaFw0yNzAy
MTEwOTMyMjlaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDWKHFkwDMH/XjY3joBwT3zzgh5WkqG7dKuj7YV7WdX
JjOc46iqNsvyieHTfuJBuPirLDAX2hGzU0OheeQBg/a9pFQxxyRBN2UZUFwTHPTG
TBBvdQGcC2vMQ3HnJwUoPzJRCYPBXQZ3JSmlq+y1uJzpLQfYeiRtowfGrRrd0jHJ
Xt5amitoN7m9VshG7KCg2K8AVriP/X5oiyNJ0s8kVdMFclUNekvWbuxik98VLWbF
dcB+kTaFUQkMj7y2ks6b6gWhw1wxPU4kWDEBaQMIICM2nZ+sTSUisKPBqpqlvC8N
NFFUhA+QW5SQuTa8t4iUxWsfIeuvPSrb1E1QzPwvld8PAgMBAAGjUzBRMB0GA1Ud
DgQWBBRKr5Ulk3xjskqncFCS22VxK2tCmTAfBgNVHSMEGDAWgBRKr5Ulk3xjskqn
cFCS22VxK2tCmTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAc
p/59bzORWJCKGJJ/GFrBIKKB9F1T1GoKy5rajxZeUCbCL22FaZ6VDmi8uUb4dYBU
5bwKOea76+J/cM1/Irt7n8c0d5nhIXm/ZdqERYjtv3F/MPi/X8Q4TP8uY6bA+dJr
enDaATGg+Jy2Iq6A6EGhYSmyxabgDkN2MNPtIwyekXoUMrA8D4jBGCQznjS3f1OW
9DMpVyb1qoz7WyAftyZkhIcwrBoTZ+C7e7ys9L8Q9uu7dKvdsYoJ73JQn4Rsgk6Y
jMGbxn6Td+z9m7aI6oPtY6UlxeA4scepX5Cx610cP0xnGJhf/o13e7jCxnr2/pTD
TTtahnT+G0FeQmlUswb9
-----END CERTIFICATE-----

28
priv/key.pem Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDWKHFkwDMH/XjY
3joBwT3zzgh5WkqG7dKuj7YV7WdXJjOc46iqNsvyieHTfuJBuPirLDAX2hGzU0Oh
eeQBg/a9pFQxxyRBN2UZUFwTHPTGTBBvdQGcC2vMQ3HnJwUoPzJRCYPBXQZ3JSml
q+y1uJzpLQfYeiRtowfGrRrd0jHJXt5amitoN7m9VshG7KCg2K8AVriP/X5oiyNJ
0s8kVdMFclUNekvWbuxik98VLWbFdcB+kTaFUQkMj7y2ks6b6gWhw1wxPU4kWDEB
aQMIICM2nZ+sTSUisKPBqpqlvC8NNFFUhA+QW5SQuTa8t4iUxWsfIeuvPSrb1E1Q
zPwvld8PAgMBAAECggEAQ5DB6cP7ta8iI+XEzk3t4lAj+0lhzv0UZa+Ahp1+Z4/t
Y7etvHoKUUxwG35iGtMlXTfVOok54WZJJZZjuZitTXqdd5D5Hrw/4MMqMXuGvFM+
MjPrnJQ739d9hayZY2/Ay7FhSK21PvzSDWtXBKQomSZ0Xbd3a8GuT9/IZKiOvZVl
Caf9IpWsfBCTJQCO0IpNrktTzxEHPNHZLKBd7HmpRmb7SNdPPaDM5Sl9B5N8pV1j
VafAsJjOcv2L0kTEc4Kq/VWO4fc625JTb7Zn3Q4VmQGdq/o+3Or1ojLME3GNz1YF
j+5cKjJf+ezAdvXYLCYqRaWS8UuxhxR++S0B8tGI/QKBgQD84U+YRKB+bURUjKz3
6OsSY6q78OeGiQjLZBBLoA/724xIPElstpy4j3wylmhV+AW696Dp8+PMMqSpdhS8
ts9QAtvrqFchu5aIhs5ZltYrNioinRri7F/FrFeDpdmt+YcNnY5p2QNI+5Tt01oG
i67qtxuVE+UOlUxDSaW+XpG2KwKBgQDYzNVKSX+IgB3aQV5SyRZJmEUsB8bxAnQ8
K4vFBXtmAtOZibaJvokHe1Rp48NZHE4xlDKm4nfKx46XBW1EEPtaoyzL4gxwlSQN
LL80PhZM0wQBhL6Ya6TZPYPLSqv+KMuIGL9OFmavrlDroQMJbiUy9UJS+xFaouHt
EoaRq5tMrQKBgDErgjWCSo6qolmqTMubf3HA7WbDzdDr/kjF+SErS1BWfS1ig4he
7ZQ5WhXgBwOISVz0X1Z+NLH0uu20Zw3WofLVy3tD7UVC219Kjv7+hEA8tO6sC5lK
Csk93HpdmjjoxujP1Owh5TCgsnGX4e5Z5LYAyp0vFB/EyeJfhJnCe6SnAoGAfU5O
zSQUAVpDZt23XdP5/Ml02ZEZLD3F3u0wWMzlWL2zfZ+6EH0/CEMBND6/ruaMT12f
tRNaN6sFwEYTtG64SNfdUW4y0HNzJeZCETj9fKPOQe5ulvxIiINkhICBTmJX2S6s
i76o1UvEW5xxe+bcu0pEbl/M1P0l5fd6LgHovfUCgYADQHISMiW1JqCnM/Mu0WhD
i++BWMnUCb9BqFk/IPnKJUZUPLNKQ3izuvrNKpvd5HwSm7278h6SX2LaU+1D+Pcw
vvQGMhK7FDhO0E7qOu/pNDRtaNnKIeG76GoYAkGdm2dc3kY8mxlLIgegL/ateuf0
AXBDlbmJJZgDYNioytFKnw==
-----END PRIVATE KEY-----

View File

@ -0,0 +1,5 @@
# 提取公钥 -> 转换为 RSA 原始格式 (PKCS#1) -> 转为 DER 二进制 -> 计算哈希
openssl x509 -in cert.pem -pubkey -noout | \
openssl rsa -pubin -RSAPublicKey_out -outform DER | \
openssl dgst -sha256 -binary | \
base64

229
proto/sdlan.proto Normal file
View File

@ -0,0 +1,229 @@
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>> ; allowdeny规则的服务器端已经屏蔽
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;
}

View File

@ -1,13 +1,34 @@
{erl_opts, [
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"}}},
@ -15,34 +36,18 @@
{sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}}
]}.
{relx, [{release, {sdlan, "0.1.0"},
[sdlan,
sasl]},
{mode, dev},
%% automatically picked up if the files
%% exist but can be set manually, which
%% is required if the names aren't exactly
%% sys.config and vm.args
{sys_config, "./config/sys.config"},
{vm_args, "./config/vm.args"}
%% the .src form of the configuration files do
%% not require setting RELX_REPLACE_OS_VARS
%% {sys_config_src, "./config/sys.config.src"},
%% {vm_args_src, "./config/vm.args.src"}
{provider_hooks, [
{pre, [
{compile, {protobuf, compile}},
{clean, {protobuf, clean}}
]}
]}.
{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}
{relx, [{release, {sdlan, "0.1.0"},
[sdlan, sasl]},
{mode, dev},
{sys_config, "./config/sys.config"},
{vm_args, "./config/vm.args"}]}.
%% use minimal mode to exclude ERTS
%% {mode, minimal}
]
}]}]}.
{rebar_packages_cdn, "https://hexpm.upyun.com"}.
{profiles, [{prod, [{relx, [{mode, prod}]}]}]}.
{rebar_packages_cdn, "https://hexpm.upyun.com"}.

View File

@ -1,5 +1,5 @@
{"1.2.0",
[{<<"base32">>,{pkg,<<"base32">>,<<"1.0.0">>},2},
[{<<"base32">>,{pkg,<<"base32">>,<<"1.0.0">>},1},
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.2">>},1},
{<<"cowboy">>,
{git,"https://github.com/ninenines/cowboy.git",
@ -12,17 +12,8 @@
{<<"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"}},
@ -32,24 +23,17 @@
{git,"https://github.com/davisp/jiffy.git",
{ref,"9ea1b35b6e60ba21dfd4adbd18e7916a831fd7d4"}},
0},
{<<"lager">>,
{git,"https://github.com/erlang-lager/lager.git",
{ref,"459a3b2cdd9eadd29e5a7ce5c43932f5ccd6eb88"}},
0},
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1},
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},1},
{<<"mysql">>,
{git,"https://github.com/mysql-otp/mysql-otp",
{ref,"caf5ff96c677a8fe0ce6f4082bc036c8fd27dd62"}},
0},
{<<"parse_trans">>,
{git,"https://github.com/uwiger/parse_trans",
{ref,"6f3645afb43c7c57d61b54ef59aecab288ce1013"}},
0},
{<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.0">>},1},
{<<"pkt">>,
{git,"https://github.com/msantos/pkt.git",
{ref,"67a4a14f596fded5ad5f2d8f94318faa8ad2c288"}},
1},
0},
{<<"poolboy">>,
{git,"https://github.com/devinus/poolboy.git",
{ref,"3bb48a893ff5598f7c73731ac17545206d259fac"}},
@ -78,10 +62,10 @@
{<<"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">>}]},
@ -89,10 +73,10 @@
{<<"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">>}]}

View File

@ -0,0 +1,147 @@
%%%-------------------------------------------------------------------
%%% @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 :: binary()) -> {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>>.

View File

@ -4,15 +4,18 @@
%%% @doc
%%%
%%% @end
%%% Created : 29. 3 2024 14:32
%%% Created : 09. 4 2024 17:37
%%%-------------------------------------------------------------------
-module(sdlan_tcp_client).
-module(dns_server).
-author("anlicheng").
-include("sdlan.hrl").
-include("sdlan_pb.hrl").
-behaviour(gen_server).
%% API
-export([start_link/0]).
-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]).
@ -27,11 +30,15 @@
%%% API
%%%===================================================================
-spec get_name(Id :: integer()) -> atom().
get_name(Id) when is_integer(Id) ->
list_to_atom("dns_server:" ++ integer_to_list(Id)).
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link() ->
-spec(start_link(Name :: atom(), Port :: integer()) ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
start_link(Name, Port) when is_atom(Name), is_integer(Port) ->
gen_server:start_link({local, Name}, ?MODULE, [Port], []).
%%%===================================================================
%%% gen_server callbacks
@ -42,9 +49,21 @@ start_link() ->
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([]) ->
{ok, Socket} = gen_tcp:connect("localhost", 18083, [binary, {packet, 2}, {active, true}]),
ok = gen_tcp:send(Socket, <<"hello world">>),
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()),
logger:debug("[sdlan_stun] start at port: ~p", [Port]),
{ok, #state{socket = Socket}}.
%% @private
@ -66,7 +85,8 @@ handle_call(_Request, _From, State = #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_cast(_Request, State = #state{}) ->
%% node下的转发
handle_cast(_Request, State) ->
{noreply, State}.
%% @private
@ -75,7 +95,16 @@ handle_cast(_Request, State = #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info(_Info, State = #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]),
{noreply, State}.
%% @private
@ -98,4 +127,4 @@ code_change(_OldVsn, State = #state{}, _Extra) ->
%%%===================================================================
%%% Internal functions
%%%===================================================================
%%%===================================================================

View File

@ -3,7 +3,7 @@
%% @end
%%%-------------------------------------------------------------------
-module(dns_proxy_sup).
-module(dns_server_sup).
-behaviour(supervisor).
@ -26,25 +26,18 @@ start_link() ->
%% modules => modules()} % optional
init([]) ->
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
Port = 15353,
Specs = [
AcceptorNum = 10,
Specs = lists:map(fun(Id) ->
Name = dns_server:get_name(Id),
#{
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]},
id => Name,
start => {dns_server, start_link, [Name, Port]},
restart => permanent,
shutdown => 2000,
type => worker,
modules => ['dns_server']
}
],
end, lists:seq(1, AcceptorNum)),
{ok, {SupFlags, Specs}}.

View File

@ -0,0 +1,30 @@
%%%-------------------------------------------------------------------
%%% @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
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
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
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

View File

@ -8,6 +8,7 @@
%%%-------------------------------------------------------------------
-module(network_handler).
-author("anlicheng").
-include_lib("sdlan_pb.hrl").
%% API
-export([handle_request/4]).
@ -21,27 +22,6 @@ handle_request("POST", "/network/create", _, #{<<"id">> := NetworkId}) when Netw
{ok, 200, sdlan_util:json_error(-1, <<"error">>)}
end;
handle_request("POST", "/network/reload", _, #{<<"id">> := NetworkId}) when NetworkId > 0 ->
case sdlan_network:get_pid(NetworkId) of
undefined ->
case sdlan_network_sup:start_network(NetworkId) of
{ok, Pid} when is_pid(Pid) ->
sdlan_network_sup:reallocate_bind_width(),
{ok, 200, sdlan_util:json_data(<<"success">>)};
{error, Reason} ->
logger:debug("[network_handler] start network: ~p, get error: ~p", [NetworkId, Reason]),
{ok, 200, sdlan_util:json_error(-1, <<"error">>)}
end;
NetworkPid when is_pid(NetworkPid) ->
case sdlan_network:reload(NetworkPid) of
ok ->
{ok, 200, sdlan_util:json_data(<<"success">>)};
{error, Reason} ->
logger:debug("[network_handler] reload network: ~p, get error: ~p", [NetworkId, Reason]),
{ok, 200, sdlan_util:json_error(-1, <<"error">>)}
end
end;
handle_request("POST", "/network/delete", _, #{<<"id">> := NetworkId}) when NetworkId > 0 ->
case sdlan_network:get_pid(NetworkId) of
undefined ->
@ -56,6 +36,34 @@ handle_request("POST", "/network/delete", _, #{<<"id">> := NetworkId}) when Netw
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">>)}.

View File

@ -0,0 +1,26 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 09. 4 2024 14:28
%%%-------------------------------------------------------------------
-module(node_handler).
-author("anlicheng").
%% API
-export([handle_request/4]).
handle_request("POST", "/node/disable", _, #{<<"network_id">> := NetworkId, <<"client_id">> := ClientId}) when NetworkId > 0 ->
case sdlan_network:get_pid(NetworkId) of
undefined ->
{ok, 200, sdlan_util:json_error(-1, <<"network not found">>)};
Pid ->
sdlan_network:disable_client(Pid, ClientId),
{ok, 200, sdlan_util:json_data(<<"success">>)}
end;
handle_request(_, Path, _, _) ->
Path1 = list_to_binary(Path),
{ok, 200, sdlan_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.

View File

@ -17,27 +17,11 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
handle_request("POST", "/test/auth_token", _, PostParams) ->
logger:debug("[test_handler] get post params: ~p", [PostParams]),
Data = #{
<<"network_id">> => 8,
<<"upgrade_type">> => 0,
<<"upgrade_prompt">> => <<"simple upgrade">>,
<<"upgrade_address">> => <<"upgrade_address">>
},
{ok, 200, sdlan_util:json_data(Data)};
handle_request("POST", "/test/upgrade", _, PostParams) ->
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/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, 9, 10])};
{ok, 200, sdlan_util:json_data([8])};
handle_request("GET", "/test/get_network", #{<<"id">> := Id0}, _) ->
Id = binary_to_integer(Id0),
@ -45,22 +29,10 @@ handle_request("GET", "/test/get_network", #{<<"id">> := Id0}, _) ->
8 => #{
<<"id">> => 8,
<<"name">> => <<"test1">>,
<<"domain">> => <<"punchnet.cn">>,
<<"ipaddr">> => <<"10.211.179.0/24">>,
<<"owner_id">> => 1234,
<<"disabled_clients">> => []
},
9 => #{
<<"id">> => 9,
<<"name">> => <<"test2">>,
<<"ipaddr">> => <<"10.211.180.0/24">>,
<<"owner_id">> => 1234,
<<"disabled_clients">> => []
},
10 => #{
<<"id">> => 10,
<<"name">> => <<"test3">>,
<<"ipaddr">> => <<"10.211.181.0/24">>,
<<"owner_id">> => 1234,
<<"algorithm">> => <<"chacha20">>,
<<"disabled_clients">> => []
}
},

View File

@ -0,0 +1,120 @@
%%%-------------------------------------------------------------------
%%% @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.

View File

@ -0,0 +1,35 @@
%%%-------------------------------------------------------------------
%% @doc ipv6_assist top level supervisor.
%% @end
%%%-------------------------------------------------------------------
-module(ipv6_assist_server_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
-define(SERVER, ?MODULE).
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
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}}.

View File

@ -0,0 +1,29 @@
-module(identity_policy_ets).
-include("policy.hrl").
-export([init/0]).
-export([get_policies/1, insert/1, delete/1, update/2]).
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).
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).
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).
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}).

View File

@ -0,0 +1,137 @@
%%%-------------------------------------------------------------------
%%% @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
%%--------------------------------------------------------------------
start(Socket) ->
spawn(?MODULE, loop, [#state{socket = Socket, command = #command{}}]).
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 jiffy:decode(Msg, [return_maps]) 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>>}) ->
[ArgNum0, ArgBin] = binary:split(Rest, <<$\r, $\n>>),
ArgNum = binary_to_integer(ArgNum0),
parse(Command#command{arg_num = ArgNum, data = ArgBin, stage = parse_arg});
%%
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() | 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)).

View File

@ -0,0 +1,45 @@
%%%-------------------------------------------------------------------
%%% @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
%%--------------------------------------------------------------------
start_link(Port) when is_integer(Port) ->
{ok, spawn_link(?MODULE, init, [Port])}.
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.

74
src/policy/rule_ets.erl Normal file
View File

@ -0,0 +1,74 @@
-module(rule_ets).
-include("policy.hrl").
-export([init/0]).
-export([insert/1, get_rules/2, delete/1, update/2]).
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)}.
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}).
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}).
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.

View File

@ -0,0 +1,441 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2026, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 11. 2 2026 23:00
%%%-------------------------------------------------------------------
-module(sdlan_quic_channel).
-author("anlicheng").
-include("sdlan.hrl").
-include("sdlan_pb.hrl").
-behaviour(gen_statem).
%%
-define(PING_TICKER, 15000).
%%
%%
-define(NAK_NETWORK_FAULT, 4).
%%
-define(NAK_INTERNAL_FAULT, 5).
%% API
-export([start_link/2]).
-export([send_event/2, command/4, stop/2]).
-export([test_rules/2]).
%% 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 = 10,
stream :: undefined | quicer:stream_handle(),
%% framing的解析
buf = <<>>,
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()
}).
%%%===================================================================
%%% API
%%%===================================================================
%%
test_rules(SrcIdentityId, DstIdentityId) when is_integer(SrcIdentityId), is_integer(DstIdentityId) ->
{ok, Rules} = get_rules(SrcIdentityId, DstIdentityId),
logger:debug("[sdlan_channel] 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 send_event(Pid :: pid(), Event :: binary()) -> no_return().
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()}) -> no_return().
command(Pid, Ref, ReceiverPid, SubCommand) when is_pid(Pid), is_pid(ReceiverPid) ->
gen_statem:cast(Pid, {command, Ref, ReceiverPid, SubCommand}).
-spec stop(Pid :: pid(), Reason :: term()) -> ok.
stop(Pid, Reason) when is_pid(Pid) ->
gen_statem:stop(Pid, Reason, 2000).
%% @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.
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.
init([Conn, Limits]) ->
MaxPacketSize = proplists:get_value(max_packet_size, Limits, 16384),
HeartbeatSec = proplists:get_value(heartbeat_sec, Limits, 10),
{ok, initializing, #state{conn = Conn, max_packet_size = MaxPacketSize, heartbeat_sec = HeartbeatSec}, [{next_event, internal, do_init}]}.
%% @private
%% @doc This function is called by a gen_statem when it needs to find out
%% the callback mode of the callback module.
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.
handle_event(internal, do_init, initializing, State=#state{conn = Conn}) ->
logger:debug("[sdlan_quic_channel] call do_init of conn: ~p", [Conn]),
{ok, _} = quicer:async_accept_stream(Conn, #{active => true}),
{next_state, waiting_stream, State};
%% quic消息
handle_event(info, {quic, dgram_state_changed, Conn, Opts = #{dgram_send_enabled := true}}, _, State=#state{conn = Conn}) ->
logger:debug("[sdlan_quic_channel] 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_channel] call new_stream: ~p, opts: ~p", [Stream, Opts]),
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
}),
quic_send(Stream, <<?PACKET_WELCOME, WelcomePkt/binary>>),
logger:debug("[sdlan_quic_channel] get stream: ~p, send welcome", [Stream]),
{next_state, initialized, State#state{stream = Stream}};
handle_event(info, {quic, closed, Stream, _Props}, _StateName, State = #state{stream = Stream}) ->
{stop, connection_closed, State};
handle_event(info, {quic, send_shutdown_complete, Stream, _Props}, _StateName, State = #state{stream = Stream}) ->
{stop, connection_shutdown, State};
handle_event(info, {quic, transport_shutdown, Stream, _Props}, _StateName, State = #state{stream = Stream}) ->
{stop, transport_shutdown, 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],
%logger:debug("[sdlan_quic_channel] get frames: ~p", [Frames]),
{keep_state, State#state{buf = NBuf}, Actions}
end;
%%
handle_event(internal, {frame, <<?PACKET_REGISTER_SUPER, Body/binary>>}, initialized, State=#state{stream = Stream}) ->
#'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_quic_channel] 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, 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
}),
%%
quic_send(Stream, <<?PACKET_REGISTER_SUPER_ACK, RegisterSuperAck/binary>>),
%% 线
Result = sdlan_api:set_node_status(#{
<<"network_id">> => NetworkId,
<<"client_id">> => ClientId,
<<"access_token">> => AccessToken,
<<"status">> => 1
}),
logger:debug("[sdlan_quic_channel] client_id: ~p, set none online result is: ~p", [ClientId, Result]),
OfflineCb = fun() ->
Result = sdlan_api:set_node_status(#{
<<"network_id">> => NetworkId,
<<"client_id">> => ClientId,
<<"access_token">> => AccessToken,
<<"status">> => 0
})
end,
{next_state, registered, State#state{network_id = NetworkId, network_pid = NetworkPid, client_id = ClientId, mac = Mac, ip = Ip, offline_cb = OfflineCb}};
undefined ->
logger:warning("[sdlan_quic_channel] client_id: ~p, register get error: network not found", [ClientId]),
quic_send(Stream, register_nak_reply(?NAK_INTERNAL_FAULT, <<"Internal Error">>)),
{stop, normal, State}
end;
{ok, #{<<"error">> := #{<<"code">> := Code, <<"message">> := Message}}} ->
logger:warning("[sdlan_quic_channel] network_id: ~p, client_id: ~p, register get error: ~ts, error_code: ~p", [NetworkId, ClientId, Message, Code]),
quic_send(Stream, register_nak_reply(Code, Message)),
{stop, normal, State};
{error, Reason} ->
logger:warning("[sdlan_quic_channel] network_id: ~p, client_id: ~p, register get error: ~p", [NetworkId, ClientId, Reason]),
quic_send(Stream, register_nak_reply(?NAK_NETWORK_FAULT, <<"Network Error">>)),
{stop, normal, State}
end;
handle_event(internal, {frame, <<?PACKET_QUERY_INFO, Body/binary>>}, registered, #state{stream = Stream, network_pid = NetworkPid, mac = SrcMac}) 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_channel] query_info src_mac is: ~p, dst_mac: ~p, nat_peer not found",
[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
}),
quic_send(Stream, <<?PACKET_PEER_INFO, EmptyResponse/binary>>),
keep_state_and_data;
{ok, {NatPeer = {{Ip0, Ip1, Ip2, Ip3}, NatPort}, NatType}, V6Info} ->
logger:debug("[sdlan_channel] query_info src_mac is: ~p, dst_mac: ~p, nat_peer: ~p",
[sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac), NatPeer]),
PeerInfo = sdlan_pb:encode_msg(#'SDLPeerInfo'{
dst_mac = DstMac,
v4_info = #'SDLV4Info' {
port = NatPort,
v4 = <<Ip0, Ip1, Ip2, Ip3>>,
nat_type = NatType
},
v6_info = V6Info
}),
quic_send(Stream, <<?PACKET_PEER_INFO, PeerInfo/binary>>),
keep_state_and_data
end;
%% arp查询
handle_event(internal, {frame, <<?PACKET_ARP_REQUEST, Body/binary>>}, registered, #state{stream = Stream, network_id = NetworkId, network_pid = NetworkPid}) 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_channel] network: ~p, arp_request target_ip: ~p, mac not found", [NetworkId, sdlan_util:int_to_ipv4(TargetIp)]),
EmptyArpResponsePkt = sdlan_pb:encode_msg(#'SDLArpResponse'{
target_ip = TargetIp,
target_mac = <<>>,
origin_ip = OriginIp,
context = Context
}),
quic_send(Stream, <<?PACKET_ARP_RESPONSE, EmptyArpResponsePkt/binary>>),
keep_state_and_data;
{ok, Mac} ->
logger:debug("[sdlan_channel] network: ~p, arp_request target_ip: ~p, mac: ~p", [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
}),
quic_send(Stream, <<?PACKET_ARP_RESPONSE, ArpResponsePkt/binary>>),
keep_state_and_data
end;
handle_event(internal, {frame, <<?PACKET_POLICY_REQUEST, Body/binary>>}, registered, #state{stream = Stream, network_pid = NetworkPid}) when is_pid(NetworkPid) ->
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_channel] policy_request src_identity_id: ~p, dst_identity_id: ~p, rules: ~p", [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
}),
quic_send(Stream, <<?PACKET_POLICY_REPLY, PolicyResponsePkt/binary>>)
end,
keep_state_and_data;
%%
handle_event(internal, {frame, <<?PACKET_COMMAND_ACK, Body/binary>>}, registered, State=#state{pending_commands = PendingCommands}) ->
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,
{keep_state, State#state{pending_commands = RestPendingCommands}}
else _ ->
keep_state_and_data
end;
handle_event(internal, {frame, <<?PACKET_PING>>}, _StateName, State = #state{stream = Stream, ping_counter = PingCounter}) ->
quic_send(Stream, <<?PACKET_PONG>>),
{keep_state, State#state{ping_counter = PingCounter + 1}};
%%
handle_event(internal, {frame, <<?PACKET_UNREGISTER>>}, registered, State=#state{client_id = ClientId, mac = Mac, network_pid = NetworkPid}) when is_pid(NetworkPid) ->
logger:warning("[sdlan_channel] unregister client_id: ~p", [ClientId]),
sdlan_network:unregister(NetworkPid, ClientId, Mac),
{stop, normal, State};
handle_event(info, {timeout, _, ping_ticker}, _, State = #state{client_id = ClientId, ping_counter = PingCounter}) ->
%%
erlang:start_timer(?PING_TICKER, self(), ping_ticker),
case PingCounter > 0 of
true ->
{keep_state, State#state{ping_counter = 0}};
false ->
logger:debug("[sdlan_channel] client_id: ~p, ping losted", [ClientId]),
{stop, normal, State#state{ping_counter = 0}}
end;
%%
handle_event(cast, {send_event, Event}, registered, #state{stream = Stream}) ->
quic_send(Stream, <<?PACKET_EVENT, Event/binary>>),
keep_state_and_data;
%%
handle_event(cast, {command, Ref, ReceiverPid, SubCommand}, registered, State=#state{stream = Stream, pkt_id = PktId, pending_commands = PendingCommands, client_id = ClientId}) ->
CommandPkt = sdlan_pb:encode_msg(#'SDLCommand'{
pkt_id = PktId,
command = SubCommand
}),
logger:debug("[sdlan_channel] client_id: ~p, will send Command: ~p", [ClientId, SubCommand]),
quic_send(Stream, <<?PACKET_COMMAND, CommandPkt/binary>>),
{keep_state, State#state{pkt_id = PktId + 1, pending_commands = maps:put(PktId, {Ref, ReceiverPid}, PendingCommands)}};
handle_event(info, {'EXIT', _, _}, _StateName, State) ->
{stop, connection_closed, State};
handle_event(EventType, Info, StateName, State) ->
logger:notice("[sdlan_quic_channel] 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.
terminate(Reason, _StateName, _State = #state{conn = Conn, stream = Stream, offline_cb = OfflineCb}) ->
Stream /= undefined andalso quicer:close_stream(Stream),
quicer:close_connection(Conn),
logger:warning("[sdlan_quic_conn] terminate closed with reason: ~p", [Reason]),
%% 线
is_function(OfflineCb) andalso OfflineCb(),
ok.
%% @private
%% @doc Convert process state when code is changed
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 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 quic_send(Stream :: quicer:stream_handle(), Packet :: binary()) -> no_return().
quic_send(Stream, Packet) when is_binary(Packet) ->
Len = byte_size(Packet),
case quicer:send(Stream, <<Len:16, Packet/binary>>) of
{ok, _} ->
ok;
{error, Reason} ->
exit({quic_send_failed, Reason})
end.
-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).

View File

@ -1,22 +1,22 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @copyright (C) 2026, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 03. 12 2025 17:29
%%% Created : 13. 2 2026 18:00
%%%-------------------------------------------------------------------
-module(dns_handler_sup).
-module(sdlan_quic_channel_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,29 +38,28 @@ 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 :: {RestartStrategy :: supervisor:strategy(),
MaxR :: non_neg_integer(), MaxT :: non_neg_integer()},
[ChildSpec :: supervisor:child_spec()]}}
| ignore | {error, Reason :: term()}).
init([]) ->
SupFlags = #{strategy => simple_one_for_one, intensity => 0, period => 1},
Spec = #{
id => dns_handler,
start => {'dns_handler', start_link, []},
AChild = #{
id => sdlan_quic_channel,
start => {'sdlan_quic_channel', start_link, []},
restart => temporary,
shutdown => 2000,
type => worker,
modules => ['dns_handler']
modules => ['sdlan_quic_channel']
},
{ok, {SupFlags, [Spec]}}.
{ok, {SupFlags, [AChild]}}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
start_handler() ->
case supervisor:start_child(?MODULE, []) of
{ok, Pid} ->
{ok, Pid};
{error, {already_started, Pid}} ->
{ok, Pid};
StartError ->
StartError
end.
-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]).

View File

@ -0,0 +1,68 @@
%%%-------------------------------------------------------------------
%%% @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]).
start_link() ->
{ok, spawn_link(?MODULE, init, [])}.
init() ->
{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),
CertFile = proplists:get_value(certfile, Props),
KeyFile = proplists:get_value(keyfile, Props),
%%
Path = os:getenv("QUIC_CERT_PATH", code:priv_dir(sdlan)),
LOptions = #{
addr_family => inet,
% QUIC/TLS证书配置
certfile => Path ++ "/" ++ CertFile,
keyfile => Path ++ "/" ++ KeyFile,
alpn => Alpn,
peer_bidi_stream_count => 1,
conn_acceptors => 10
},
ListenAddr = "0.0.0.0:" ++ integer_to_list(Port),
case quicer:listen(ListenAddr, LOptions) of
{ok, L} ->
loop_accept(L, Limits);
Error ->
exit(Error)
end.
loop_accept(L, Limits) ->
case quicer:accept(L, #{}, infinity) of
{ok, Conn} ->
logger:debug("[sdlan_quic_server] accept a new connection: ~p", [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]),
quicer:controlling_process(NConn, ChannelPid);
Error ->
quicer:close_connection(NConn),
logger:notice("[sdlan_quic_server] start channel get error: ~p", [Error])
end,
loop_accept(L, Limits);
{error, _} ->
quicer:close_connection(Conn),
loop_accept(L, Limits)
end;
{error, Reason} ->
logger:debug("[sdlan_quic_server] accept failed: ~p", [Reason]),
loop_accept(L, Limits)
end.

View File

@ -17,7 +17,6 @@
dns_erlang,
pkt,
quicer,
mnesia,
erts,
public_key,
ssl,

View File

@ -9,11 +9,11 @@
-module(sdlan_api).
-author("anlicheng").
-define(API_TOKEN, <<"wv6fGyBhl*7@AsD9">>).
-define(API_TOKEN, <<"H6p*2RfEu4ITcL">>).
%% API
-export([get_all_networks/0, get_network/1]).
-export([auth_token/3, node_online/3, node_offline/2, flow_report/5, network_forward_report/2, auth_network_code/3]).
-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() ->
@ -47,9 +47,9 @@ get_network(Id) when is_integer(Id) ->
Error
end.
-spec auth_token(ClientId :: binary(), Token :: binary(), Version :: integer()) -> {ok, Resp :: map()} | {error, Reason :: any()}.
auth_token(ClientId, Token, Version) when is_binary(ClientId), is_binary(Token), is_integer(Version) ->
case catch do_post("auth_token", #{<<"client_id">> => ClientId, <<"token">> => Token, <<"version">> => Version}) of
-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/access_token", Params) of
{ok, Resp} ->
case catch jiffy:decode(Resp, [return_maps]) of
Result when is_map(Result) ->
@ -61,32 +61,9 @@ auth_token(ClientId, Token, Version) when is_binary(ClientId), is_binary(Token),
Error
end.
-spec auth_network_code(ClientId :: binary(), NetworkCode :: binary(), Version :: integer()) -> {ok, Resp :: map()} | {error, Reason :: any()}.
auth_network_code(ClientId, NetworkCode, Version) when is_binary(ClientId), is_binary(NetworkCode), is_integer(Version) ->
case catch do_post("check_network", #{<<"client_id">> => ClientId, <<"code">> => NetworkCode, <<"version">> => Version}) of
{ok, Resp} ->
case catch jiffy:decode(Resp, [return_maps]) of
Result when is_map(Result) ->
{ok, Result};
{error, Reason} ->
{error, Reason}
end;
Error ->
Error
end.
-spec node_online(ClientId :: binary(), NetworkId :: integer(), IpAddr :: binary()) -> {ok, Resp :: map()} | {error, Reason :: any()}.
node_online(ClientId, NetworkId, IpAddr) when is_binary(ClientId), is_integer(NetworkId), is_binary(IpAddr) ->
case catch do_post("set_node_status", #{<<"client_id">> => ClientId, <<"network_id">> => NetworkId, <<"ip_addr">> => IpAddr, <<"status">> => 1}) of
{ok, Resp} ->
{ok, catch jiffy:decode(Resp, [return_maps])};
Error ->
Error
end.
-spec node_offline(ClientId :: binary(), NetworkId :: integer()) -> {ok, Resp :: map()} | {error, Reason :: any()}.
node_offline(ClientId, NetworkId) when is_binary(ClientId), is_integer(NetworkId) ->
case catch do_post("set_node_status", #{<<"client_id">> => ClientId, <<"network_id">> => NetworkId, <<"status">> => 0}) of
-spec set_node_status(Params :: map()) -> {ok, Resp :: map()} | {error, Reason :: any()}.
set_node_status(Params) when is_map(Params) ->
case catch do_post("set_node_status", Params) of
{ok, Resp} ->
{ok, catch jiffy:decode(Resp, [return_maps])};
Error ->
@ -127,12 +104,11 @@ network_forward_report(NetworkId, ForwardNum) when is_integer(NetworkId), is_int
-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}
{<<"Content-Type">>, <<"application/json">>},
{<<"X-sign">>, sign_params(Params)}
],
QS0 = uri_string:compose_query(Params),
@ -152,14 +128,13 @@ do_get(Uri, Params) when is_list(Uri), is_list(Params) ->
{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>>),
-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">>},
{<<"token">>, Token}
{<<"X-sign">>, sign_params(Params)}
],
Body = iolist_to_binary(jiffy:encode(Params, [force_utf8])),
@ -181,4 +156,36 @@ do_post(Uri, Params) when is_list(Uri), is_map(Params); is_list(Params) ->
{error, <<"empty response">>};
{error, Reason} ->
{error, Reason}
end.
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).

View File

@ -11,18 +11,29 @@
start(_StartType, _StartArgs) ->
io:setopts([{encoding, unicode}]),
%% mnesia数据库
mnesia:start(),
%%
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(),
dns_pending_wheel:start(),
%%
identity_policy_ets:init(),
rule_ets:init(),
start_http_server(),
start_tcp_server(),
sdlan_sup:start_link().
stop(_State) ->
@ -40,8 +51,8 @@ start_http_server() ->
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]}
@ -54,28 +65,5 @@ start_http_server() ->
{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]).
%% tcp服务
start_tcp_server() ->
{ok, Props} = application:get_env(sdlan, tcp_server),
Acceptors = proplists:get_value(acceptors, Props, 50),
MaxConnections = proplists:get_value(max_connections, Props, 10240),
Backlog = proplists:get_value(backlog, Props, 1024),
Port = proplists:get_value(port, Props),
TransOpts = #{
num_acceptors => Acceptors,
max_connections => MaxConnections,
socket_opts => [
{port, Port},
{nodelay, false},
{backlog, Backlog}
]
},
{ok, _} = ranch:start_listener('sdlan/tcp_server', ranch_tcp, TransOpts, sdlan_channel, []),
logger:debug("[sdlan_app] the tcp server start at: ~p", [Port]).

View File

@ -12,14 +12,12 @@
%% API
-export([rsa_encrypt/2, rsa_pem_decode/1]).
-export([aes_encrypt/3, aes_decrypt/3]).
-export([test/0]).
-export([test/0, test_chacha20/0]).
test() ->
Key = <<"abcdabcdabcdabcd">>,
X = aes_encrypt(Key, Key, <<"hello world">>),
logger:debug("x is: ~p, raw: ~p", [X, aes_decrypt(Key, Key, X)]),
ok.
-spec rsa_pem_decode(PubKey :: binary()) -> public_key:rsa_public_key().
@ -40,4 +38,44 @@ aes_encrypt(Key, IVec, PlainText) when is_binary(Key), is_binary(IVec), is_binar
%% aes的解密算法
-spec aes_decrypt(binary(), binary(), binary()) -> binary().
aes_decrypt(Key, IVec, CipherText) when is_binary(Key), is_binary(IVec), is_binary(CipherText) ->
crypto:crypto_one_time(aes_128_ofb, Key, IVec, CipherText, [{encrypt, false}, {padding, pkcs_padding}]).
crypto:crypto_one_time(aes_128_ofb, Key, IVec, CipherText, [{encrypt, false}, {padding, pkcs_padding}]).
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
).

View File

@ -10,7 +10,7 @@
-author("anlicheng").
%% API
-export([init/0, lookup/1, insert/2]).
-export([init/0, lookup/1, insert/3]).
-define(TABLE, sdlan_hostname_regedit).
@ -19,14 +19,19 @@ init() ->
-spec lookup(FullHostname :: binary()) -> error | {ok, Ip :: inet:ip4_address()}.
lookup(FullHostname) when is_binary(FullHostname) ->
case ets:lookup(?TABLE, FullHostname) of
LowerFullHostname = string:lowercase(FullHostname),
case ets:lookup(?TABLE, LowerFullHostname) of
[{_, Ip}] ->
{ok, Ip};
[] ->
error
end.
-spec insert(FullHostname :: binary(), Ip :: integer()) -> no_return().
insert(FullHostname, Ip) when is_binary(FullHostname), is_integer(Ip) ->
-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, {FullHostname, {Ip0, Ip1, Ip2, Ip3}}).
true = ets:insert(?TABLE, {LowerFullHostname, {Ip0, Ip1, Ip2, Ip3}});
insert(_, _, _) ->
ok.

602
src/sdlan_network.erl Normal file
View File

@ -0,0 +1,602 @@
%%%-------------------------------------------------------------------
%%% @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").
-behaviour(gen_server).
-define(FLOW_REPORT_INTERVAL, 60 * 1000).
%% broadcast, "FF-FF-FF-FF-FF-FF"
-define(BROADCAST_MAC, <<16#FF,16#FF,16#FF,16#FF,16#FF,16#FF>>).
%% API
-export([start_link/2]).
-export([get_name/1, get_pid/1, lookup_pid/1, peer_info/3, unregister/3, debug_info/1, get_network_id/1, attach/6, arp_request/2]).
-export([forward/5, update_hole/7, disable_client/2, get_channel/2]).
-export([command/4, wait_command_ack/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, {
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(),
%%
forward_bytes = 0,
%% 使ip, #{mac :: integer() => Host :: #endpoint{}}
endpoints = #{}
}).
%%%===================================================================
%%% 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()) -> {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()) -> no_return().
unregister(Pid, ClientId, Mac) when is_pid(Pid), is_binary(ClientId), is_binary(Mac) ->
gen_server:cast(Pid, {unregister, ClientId, Mac}).
-spec peer_info(Pid :: pid(), SrcMac :: binary(), DstMac :: binary()) ->
error | {ok, {NatPeer :: {Ip :: inet:ip4_address(), Port :: integer()}, NatType :: integer()}, V6Info :: undefined | #'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(pid(), Sock :: any(), SrcMac :: binary(), DstMac :: binary(), Packet :: binary()) -> no_return().
forward(Pid, Sock, SrcMac, DstMac, Packet) when is_pid(Pid), is_binary(SrcMac), is_binary(DstMac), is_binary(Packet) ->
gen_server:cast(Pid, {forward, Sock, SrcMac, DstMac, Packet}).
%% ip地址对应的nat关系
-spec update_hole(Pid :: pid(), SessionToken :: binary(), ClientId :: binary(), Mac :: binary(), Peer :: tuple(), NatType :: integer(), V6Info :: undefined | #'SDLV6Info'{}) -> no_return().
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(), Id :: integer()) ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link(Name, Id) when is_atom(Name), is_integer(Id) ->
gen_server:start_link({local, Name}, ?MODULE, [Id], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([Id]) when is_integer(Id) ->
erlang:process_flag(trap_exit, true),
case sdlan_api:get_network(Id) of
{ok, #{<<"ipaddr">> := Null}} when Null == <<"null">>; Null == <<"NULL">> ->
ignore;
{ok, NetworkInfo = #{<<"id">> := Id, <<"name">> := Name, <<"domain">> := Domain, <<"algorithm">> := Algorithm0, <<"ipaddr">> := IpAddr0, <<"owner_id">> := OwnerId}} ->
logger:debug("[sdlan_network] load network info: ~p", [NetworkInfo]),
{IpAddr, MaskLen} = parse_ipaddr(IpAddr0),
%% 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),
sdlan_domain_regedit:insert(Domain),
%%
Algorithm = normalization_algorithm(Algorithm0),
Key = gen_key(Algorithm),
{ok, #state{network_id = Id, name = Name, domain = Domain, ipaddr = IpAddr, algorithm = Algorithm,
owner_id = OwnerId, mask_len = MaskLen, key = Key, 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, ChannelPid, ClientId, Mac, Ip, Hostname}, _From,
State = #state{network_id = NetworkId, domain = Domain, endpoints = Endpoints, algorithm = Algorithm, key = Key}) ->
%% 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
%% attach需要清理之前的绑定信息
maybe
{ok, #endpoint{ip = OldIp, channel_pid = OldChannelPid, channel_ref = OldChannelRef}} ?= maps:find(Mac, Endpoints),
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], Endpoints),
%%
is_reference(OldChannelRef) andalso demonitor(OldChannelRef),
case OldChannelPid /= undefined andalso is_process_alive(OldChannelPid) of
true ->
sdlan_quic_channel:stop(OldChannelPid, rebind);
false ->
ok
end
end,
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)},
%% id
RegionId = gen_region_id(Ip),
{reply, {ok, Algorithm, Key, RegionId, 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_channel, ClientId}, _From, State = #state{endpoints = Endpoints}) ->
case search_endpoint(fun(_, #endpoint{client_id = ClientId0}) -> ClientId =:= ClientId0 end, Endpoints) 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{endpoints = Endpoints}) ->
case maps:find(DstMac, Endpoints) of
{ok, #endpoint{channel_pid = DstChannelPid, hole = #hole{peer = DstNatPeer, nat_type = DstNatType}, v6_info = DstV6Info}} ->
%% sendRegister事件(2024-06-25 )
maybe
{ok, #endpoint{hole = #hole{peer = {SrcNatIp, SrcNatPort}, nat_type = SrcNatType}, v6_info = SrcV6Info}} ?= maps:find(SrcMac, Endpoints),
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{endpoints = Endpoints}) ->
case search_endpoint(fun(_, #endpoint{ip = Ip0}) -> Ip0 =:= TargetIp end, Endpoints) of
error ->
{reply, error, State};
{ok, Mac, _} ->
{reply, {ok, Mac}, State}
end;
%%
handle_call({command, ReceiverPid, ClientId, SubCommand}, _From, State = #state{endpoints = Endpoints}) ->
case search_endpoint(fun(_, #endpoint{client_id = ClientId0}) -> ClientId =:= ClientId0 end, Endpoints) 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>>}}
end;
handle_call(debug_info, _From, State = #state{network_id = NetworkId, ipaddr = IpAddr, mask_len = MaskLen, owner_id = OwnerId, endpoints = Endpoints}) ->
Reply = #{
<<"network_id">> => NetworkId,
<<"ipaddr">> => IpAddr,
<<"mask_len">> => MaskLen,
<<"owner_id">> => OwnerId,
<<"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{}}).
%% , 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 = {NatIp, NatPort}}}} ->
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, NatIp, NatPort, 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是属于当前网络的
handle_cast({update_hole, SessionToken, ClientId, Mac, Peer, NatType, V6Info}, State = #state{endpoints = Endpoints}) ->
case maps:find(Mac, Endpoints) of
%% ClientId =:= ClientId0, SessionToken =:= SessionToken0
{ok, 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], Endpoints)
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)}};
_ ->
{noreply, State}
end.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info({timeout, _, flow_report_ticker}, State = #state{network_id = NetworkId, forward_bytes = ForwardBytes}) ->
erlang:start_timer(?FLOW_REPORT_INTERVAL, self(), flow_report_ticker),
catch sdlan_api:network_forward_report(NetworkId, ForwardBytes),
{noreply, State#state{forward_bytes = 0}};
%% Channel进程退出, hole里面的数据也需要清理
handle_info({'DOWN', _MRef, process, ChannelPid, Reason}, State = #state{network_id = NetworkId, endpoints = Endpoints}) ->
logger:notice("[sdlan_network] network_id: ~p, channel_pid: ~p, close with reason: ~p", [NetworkId, ChannelPid, Reason]),
NEndpoints = maps:filter(fun(_, #endpoint{channel_pid = ChannelPid0}) -> ChannelPid =/= ChannelPid0 end, Endpoints),
{noreply, State#state{endpoints = NEndpoints}}.
%% @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{}) -> none()).
terminate(Reason, #state{network_id = NetworkId, endpoints = Endpoints}) ->
broadcast(fun(#endpoint{channel_pid = ChannelPid}) ->
case 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, [], Endpoints),
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
%%%===================================================================
%% 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 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).
-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
}.
-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 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).

View File

@ -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 = ChannelMap}) ->
handle_info({timeout, _, release_ticker}, State = #state{network_map = NetworkMap}) ->
%%
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 = ChannelMa
{limit_exceeded, 0, _} ->
Acc
end
end, 0, maps:keys(ChannelMap)),
end, 0, maps:values(NetworkMap)),
% 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}) ->

View File

@ -41,8 +41,7 @@ start_link() ->
init([]) ->
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
{ok, NetworkIds} = sdlan_api:get_all_networks(),
Specs = lists:map(fun child_spec/1, []),
Specs = lists:map(fun child_spec/1, NetworkIds),
set_network_bind(length(Specs)),
{ok, {SupFlags, Specs}}.

4849
src/sdlan_pb.erl Normal file

File diff suppressed because it is too large Load Diff

View File

@ -85,10 +85,8 @@ handle_call(_Request, _From, State = #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
%% node下的转发
handle_cast({stun_relay, Ip, Port, Reply}, State = #state{socket = Sock}) ->
ok = gen_udp:send(Sock, Ip, Port, Reply),
handle_cast(_Request, State) ->
{noreply, State}.
%% @private
@ -97,69 +95,54 @@ 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_REQUEST:8, Body/binary>>}, State = #state{socket = Sock}) ->
#sdl_stun_request{cookie = Cookie, client_id = ClientId, network_id = NetworkId, mac = Mac, nat_type = NatType, v6_info = V6Info} = sdlan_pb:decode_msg(Body, sdl_stun_request),
StunRequest = catch sdlan_pb:decode_msg(Body, 'SDLStunRequest'),
%% ip对应的nat的映射关系
case sdlan_network:get_pid(NetworkId) of
undefined ->
logger:debug("[sdlan_stun] stun_request network_id: ~p, client_id: ~p, not found", [NetworkId, ClientId]),
{noreply, State};
NetworkPid when is_pid(NetworkPid) ->
sdlan_network:update_hole(NetworkPid, ClientId, Mac, {Ip, Port}, NatType, V6Info),
StunReply = sdlan_pb:encode_msg(#sdl_stun_reply{
cookie = Cookie
}),
ok = gen_udp:send(Sock, Ip, Port, <<?PACKET_STUN_REPLY, StunReply/binary>>),
logger:debug("[sdlan_stun] stun_request network_id: ~p, client_id: ~p, hole: ~p", [NetworkId, ClientId, {Ip, Port}]),
{noreply, State}
end;
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}) ->
#sdl_stun_probe{cookie = Cookie, attr = Attr} = sdlan_pb:decode_msg(Body, sdl_stun_probe),
logger:debug("[sdlan_stun] get stun_probe request, att: ~p", [Attr]),
ProbeReplyPkt = sdlan_pb:encode_msg(#sdl_stun_probe_reply {
cookie = Cookie,
port = ClientPort,
ip = int_ip(ClientIp)
}),
case Attr of
?STUN_ATTR_CHANGE_NONE ->
ok = gen_udp:send(Sock, ClientIp, ClientPort, <<?PACKET_STUN_PROBE_REPLY, ProbeReplyPkt/binary>>);
?STUN_ATTR_CHANGE_PORT ->
%% ip
sdlan_stun_peer_assist:stun_relay(ClientIp, ClientPort, ProbeReplyPkt);
?STUN_ATTR_CHANGE_PEER ->
%%
sdlan_stun_port_assist:stun_relay(ClientIp, ClientPort, ProbeReplyPkt)
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)
}),
case Attr of
?STUN_ATTR_CHANGE_NONE ->
ok = gen_udp:send(Sock, ClientIp, ClientPort, <<?PACKET_STUN_PROBE_REPLY, ProbeReplyPkt/binary>>);
?STUN_ATTR_CHANGE_PORT ->
%% ip
sdlan_stun_peer_assist:stun_relay(ClientIp, ClientPort, ProbeReplyPkt);
?STUN_ATTR_CHANGE_PEER ->
%%
sdlan_stun_port_assist:stun_relay(ClientIp, ClientPort, ProbeReplyPkt)
end
end,
{noreply, State};
%% , stun_reply的转发通过socket来转发
handle_info({udp, Sock, _, _, <<?PACKET_STUN_PROBE_RELAY:8, Ip0, Ip1, Ip2, Ip3, Port:16, Reply/binary>>}, State = #state{socket = Sock}) ->
logger:debug("[sdlan_stun] get stun_probe_replay request, reply: ~p", [Reply]),
gen_udp:send(Sock, {Ip0, Ip1, Ip2, Ip3}, Port, Reply),
{noreply, State};
handle_info({udp, _, _Ip, _Port, <<?PACKET_STUN_DATA, Body/binary>>}, State = #state{socket = Sock}) ->
Data = #sdl_data{network_id = NetworkId, src_mac = SrcMac, dst_mac = DstMac, ttl = TTL} = sdlan_pb:decode_msg(Body, sdl_data),
logger:debug("[sdlan_stun] stun data, src_mac: ~p, dst_mac: ~p", [sdlan_util:format_mac(SrcMac), sdlan_util:format_mac(DstMac)]),
%% ttl需要减1
case sdlan_network:get_pid(NetworkId) of
NetworkPid when is_pid(NetworkPid) ->
NData = sdlan_pb:encode_msg(Data#sdl_data{ttl = TTL - 1, is_p2p = false}),
sdlan_network:forward(NetworkPid, Sock, SrcMac, DstMac, <<?PACKET_STUN_DATA, NData/binary>>);
_ ->
ok
Data = catch sdlan_pb:decode_msg(Body, 'SDLData'),
maybe
#'SDLData'{network_id = NetworkId, src_mac = SrcMac, dst_mac = DstMac, ttl = TTL} ?= Data,
logger:debug("[sdlan_stun] forward data, network_id: ~p", [NetworkId]),
{ok, NetworkPid} ?= sdlan_network:lookup_pid(NetworkId),
%% ttl需要减1
NData = sdlan_pb:encode_msg(Data#'SDLData'{ttl = TTL - 1, is_p2p = false}),
sdlan_network:forward(NetworkPid, Sock, SrcMac, DstMac, <<?PACKET_STUN_DATA, NData/binary>>)
end,
{noreply, State};
handle_info(Info, State) ->

View File

@ -58,7 +58,6 @@ 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

View File

@ -29,13 +29,22 @@ init([]) ->
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
Specs = [
%% dns的逻辑
#{
id => dns_proxy_sup,
start => {dns_proxy_sup, start_link, []},
id => dns_server_sup,
start => {dns_server_sup, start_link, []},
restart => permanent,
shutdown => 2000,
type => supervisor,
modules => ['dns_proxy_sup']
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']
},
#{
id => sdlan_network_coordinator,
@ -61,6 +70,42 @@ 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']
}
],
@ -72,4 +117,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).

109
src/sdlan_sync_mysql.erl Normal file
View File

@ -0,0 +1,109 @@
%%%-------------------------------------------------------------------
%%% @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.
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.
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.
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.
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.
terminate(Reason, _StateName, _State) ->
logger:debug("[sdlan_sync_mysql] terminate with reason: ~p", [Reason]),
ok.
%% @private
%% @doc Convert process state when code is changed
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.

164
src/sdlan_util.erl Normal file
View File

@ -0,0 +1,164 @@
%%%-------------------------------------------------------------------
%%% @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]).
-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).
%%
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>>.
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)]).

36
src/test/quic_client.erl Normal file
View File

@ -0,0 +1,36 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2026, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 13. 2 2026 12:46
%%%-------------------------------------------------------------------
-module(quic_client).
-author("anlicheng").
%% API
-export([start/0]).
-export([do_start/0]).
start() ->
erlang:spawn(?MODULE, do_start, []).
do_start() ->
Port = 1365,
{ok, Conn} = quicer:connect("localhost", Port, [{alpn, ["punchnet/1.0"]}, {verify, none}], 5000),
{ok, Stm} = quicer:start_stream(Conn, []),
receive
Info ->
logger:debug("get info: ~p", [Info])
end,
Data = <<"ping">>,
Len = byte_size(Data),
Payload = <<Len:16, Data/binary, Len:16, Data/binary>>,
{ok, SendLen} = quicer:send(Stm, Payload),
logger:debug("[quic_client] client send success: ~p", [SendLen]),
receive {quic, <<"pong">>, Stm, _Props} -> ok end,
ok = quicer:close_connection(Conn).

12
swift_pb.sh Executable file
View File

@ -0,0 +1,12 @@
#! /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 Normal file
View File

@ -0,0 +1,29 @@
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;
}