Compare commits
10 Commits
b5f21063ad
...
f18ca40214
| Author | SHA1 | Date | |
|---|---|---|---|
| f18ca40214 | |||
| b7aafc68d6 | |||
| ea5d463216 | |||
| dd5610addc | |||
| f320526f2d | |||
| f9a5cc3986 | |||
| 71a10514f8 | |||
| bc63f284ff | |||
| 107a7f8161 | |||
| 1467b8ba68 |
4
Cargo.toml
Normal file → Executable file
4
Cargo.toml
Normal file → Executable file
@ -13,8 +13,10 @@ dashmap = "5.5.3"
|
|||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
# lazy_static = "1.4.0"
|
# lazy_static = "1.4.0"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
|
prost = "0.12.6"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
rolling-file = { git = "https://git.asxalex.pw/rust/rolling-file" }
|
# rolling-file = { git = "https://git.asxalex.pw/rust/rolling-file" }
|
||||||
|
rolling-file = { git = "ssh://git@git2.asxalex.pw/rust/rolling-file.git" }
|
||||||
rsa = "0.9.6"
|
rsa = "0.9.6"
|
||||||
serde = { version = "1.0.196", features = ["derive"] }
|
serde = { version = "1.0.196", features = ["derive"] }
|
||||||
serde_json = "1.0.113"
|
serde_json = "1.0.113"
|
||||||
|
|||||||
225
docs/http_api.md
Normal file
225
docs/http_api.md
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
# sdlan中心节点提供的api
|
||||||
|
sdlan中心节点需要被告知的内容主要有:
|
||||||
|
|
||||||
|
* `register_token`——节点上线需要填写的token,是一个`big int`(64位的unsigned int)
|
||||||
|
* 与`register_token`关联的信息,包括(但不一定需要)
|
||||||
|
* 过期时间
|
||||||
|
* 默认加入的网络
|
||||||
|
* 与`register_token`关联的用户信息,主要包括
|
||||||
|
* 用户的uuid,为一个`big int`(64位的unsigned int)
|
||||||
|
* 用户是否启用
|
||||||
|
* 与用户uuid相互关联的信息,比如该用户限制的连接的终端的个数等(如果有的化)
|
||||||
|
|
||||||
|
## 1.0. token创建通知
|
||||||
|
```
|
||||||
|
POST /token/created
|
||||||
|
|
||||||
|
{
|
||||||
|
"value": $unsigned_int,
|
||||||
|
|
||||||
|
// 过期时候的unix timestamp
|
||||||
|
"expire": $unsigned_int,
|
||||||
|
|
||||||
|
// 默认加入的网络的uuid
|
||||||
|
"defalut_network_id": "",
|
||||||
|
|
||||||
|
"user_info": {
|
||||||
|
// 关联的用户的id的数字
|
||||||
|
"user_id": $unsigned_int,
|
||||||
|
// 限制的节点的个数
|
||||||
|
"peer_limit": $int,
|
||||||
|
// 表明用户是否启用
|
||||||
|
"enabled": $bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
// 0表示成功,其他数字代表失败
|
||||||
|
"code": $int,
|
||||||
|
// 如果成功,message返回信息,这里只返回一个ok
|
||||||
|
"message": "ok",
|
||||||
|
// 如果code不为0,则error表示出错信息
|
||||||
|
"error": $string,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1.1. token删除
|
||||||
|
通过调用该接口,告知supernode删除一个token。
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /token/deleted
|
||||||
|
|
||||||
|
{
|
||||||
|
"value": $unsigned_int,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
// 0表示成功,其他数字代表失败
|
||||||
|
"code": $int,
|
||||||
|
// 如果成功,message返回信息,这里只返回一个ok
|
||||||
|
"message": "ok",
|
||||||
|
// 如果code不为0,则error表示出错信息
|
||||||
|
"error": $string,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1.2. 用户信息修改
|
||||||
|
用户信息修改,主要涉及到用户可以创建节点的个数,接口如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /user/updated
|
||||||
|
|
||||||
|
{
|
||||||
|
"user_id": $unsigned_int,
|
||||||
|
"peer_limit": $int,
|
||||||
|
"enabled": $bool
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
// 0表示成功,其他数字代表失败
|
||||||
|
"code": $int,
|
||||||
|
// 如果成功,message返回信息,这里只返回一个ok
|
||||||
|
"message": "ok",
|
||||||
|
// 如果code不为0,则error表示出错信息
|
||||||
|
"error": $string,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1.3. 创建网络
|
||||||
|
创建网络是supernode端进行(主要因为需要分配ip地址),supernode端提供创建网络的api。
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /network/create
|
||||||
|
|
||||||
|
{
|
||||||
|
// 网络的名称
|
||||||
|
"name": $string,
|
||||||
|
// 用户选择的ip地址段,cidr格式的ip地址,如10.20.1.0/24
|
||||||
|
// 如果不指定,则该字段不传递
|
||||||
|
"ip": $string,
|
||||||
|
// 创建的用户的id,最后会用于表明该网络属于哪个用户
|
||||||
|
"user_id": $unsigned_int,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
// 0表示成功,其他数字代表失败
|
||||||
|
"code": $int,
|
||||||
|
// 如果成功,message返回信息,这里只返回一个ok
|
||||||
|
"message": {
|
||||||
|
// 该网络的唯一标识
|
||||||
|
"uuid": $string
|
||||||
|
// 最后分配的cidr格式的ip地址,如10.20.1.0/24
|
||||||
|
"ip": $string
|
||||||
|
},
|
||||||
|
// 如果code不为0,则error表示出错信息
|
||||||
|
"error": $string,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1.4. 移动节点
|
||||||
|
移动节点用于将某个处于网络A中的节点,移动到同一个用户的另一个网络B中:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /peer/move
|
||||||
|
|
||||||
|
{
|
||||||
|
// 需要移动的节点的uuid
|
||||||
|
"peer_id": $string,
|
||||||
|
// 节点所处的以前的网络
|
||||||
|
"from_id": $string,
|
||||||
|
// 节点移动到的新网络的uuid
|
||||||
|
"to_id": $string,
|
||||||
|
// 属于哪个用户
|
||||||
|
"user_id": $unsigned_int,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// 0表示成功,其他数字代表失败
|
||||||
|
"code": $int,
|
||||||
|
// 如果成功,message返回信息,这里只返回一个ok
|
||||||
|
"message": "ok",
|
||||||
|
// 如果code不为0,则error表示出错信息
|
||||||
|
"error": $string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1.5. 授权节点
|
||||||
|
移动节点用于将某个处于未授权的节点,移动到同一个用户的某个网络中:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /peer/authorize
|
||||||
|
|
||||||
|
{
|
||||||
|
// 需要移动的节点的uuid
|
||||||
|
"peer_id": $string,
|
||||||
|
// 节点移动到的新网络的uuid
|
||||||
|
"to_id": $string,
|
||||||
|
// 属于哪个用户
|
||||||
|
"user_id": $unsigned_int,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// 0表示成功,其他数字代表失败
|
||||||
|
"code": $int,
|
||||||
|
// 如果成功,message返回信息,这里只返回一个ok
|
||||||
|
"message": "ok",
|
||||||
|
// 如果code不为0,则error表示出错信息
|
||||||
|
"error": $string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1.6. 取消节点授权
|
||||||
|
移动节点用于将某个处于某个网络的节点,取消授权,返回到为授权状态:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /peer/unauthorize
|
||||||
|
|
||||||
|
{
|
||||||
|
// 需要移动的节点的uuid
|
||||||
|
"peer_id": $string,
|
||||||
|
// 节点所处的以前的网络
|
||||||
|
"from_id": $string,
|
||||||
|
// 属于哪个用户
|
||||||
|
"user_id": $unsigned_int,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// 0表示成功,其他数字代表失败
|
||||||
|
"code": $int,
|
||||||
|
// 如果成功,message返回信息,这里只返回一个ok
|
||||||
|
"message": "ok",
|
||||||
|
// 如果code不为0,则error表示出错信息
|
||||||
|
"error": $string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# 2.0 sdlan中心节点使用的数据库结构
|
||||||
|
在中心节点收到客户端上线请求之后,如果这个节点是第一次上线,则会将相关信息插入到
|
||||||
@ -19,13 +19,14 @@ sdlan协议的总体格式如下:
|
|||||||
消息头二进制格式如下:
|
消息头二进制格式如下:
|
||||||
|
|
||||||
```
|
```
|
||||||
+---------+----+-----+------+-----+
|
+---------+----+-------+-----+------+-----+
|
||||||
| version | id | ttl | flag | pc |
|
| version | id | token | ttl | flag | pc |
|
||||||
+---------+----+-----+------+-----+
|
+---------+----+-------+-----+------+-----+
|
||||||
```
|
```
|
||||||
|
|
||||||
其中,version占用一个字节,用于标识协议版本。id用于唯一标识某个客户端,为长度为32字节的uuid,之后的ttl占用一个字节,当ttl为0,则直接丢弃该数据包。后面的flag占用2字节,用于标识数据包属性,目前拥有的标识如下:
|
其中,version占用一个字节,用于标识协议版本。id用于唯一标识某个客户端,为长度为32字节的uuid,token是用户生成的邀请码,占用8个字节(64比特的大端序整型数字)。之后的ttl占用一个字节,当ttl为0,则直接丢弃该数据包。后面的flag占用2字节,用于标识数据包属性,目前拥有的标识如下:
|
||||||
|
|
||||||
|
* `federation`: `0x0010`,表示这个数据包是从其他的supernode主动发送过来的,比如,其他supernode主动发送过来的RegisterSuper;其他supernode收到情短请求之后,主动将请求以Command的形式广播给其他supernode等。
|
||||||
* `from_sn`: `0x0020`,表示这个数据包是从服务端发送过来的。
|
* `from_sn`: `0x0020`,表示这个数据包是从服务端发送过来的。
|
||||||
* `socket`: `0x0040`,表示这个数据包里面包含了有用的socket信息(通常在服务端转发的时候有用)。
|
* `socket`: `0x0040`,表示这个数据包里面包含了有用的socket信息(通常在服务端转发的时候有用)。
|
||||||
* `v6_info`: `0x0080`,表示这个数据包里面包含了ipv6信息(专门用在packet数据包里面,目前只有这个数据包使用了该标识)。
|
* `v6_info`: `0x0080`,表示这个数据包里面包含了ipv6信息(专门用在packet数据包里面,目前只有这个数据包使用了该标识)。
|
||||||
|
|||||||
@ -6,6 +6,10 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
|
|||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum CommandType {
|
pub enum CommandType {
|
||||||
MovePeer = 1,
|
MovePeer = 1,
|
||||||
|
TokenCreated = 2,
|
||||||
|
TokenDeleted = 3,
|
||||||
|
UserChanged = 4,
|
||||||
|
UserDeleted = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@ -21,7 +25,7 @@ pub struct MovePeerCommandReq {
|
|||||||
pub peer_id: String,
|
pub peer_id: String,
|
||||||
pub new_ip: u32,
|
pub new_ip: u32,
|
||||||
pub net_bit_len: u8,
|
pub net_bit_len: u8,
|
||||||
pub user_id: u64,
|
pub user_id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|||||||
@ -46,16 +46,16 @@ impl<'a> Common<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_slice(value: &'a [u8]) -> Result<(Common, &'a [u8])> {
|
pub fn from_slice(value: &'a [u8]) -> Result<(Common, &'a [u8])> {
|
||||||
const id_len: usize = 32;
|
const ID_LEN: usize = 32;
|
||||||
const token_len: usize = 8;
|
const TOKEN_LEN: usize = 8;
|
||||||
|
|
||||||
if value.len() < 7 + id_len + token_len {
|
if value.len() < 7 + ID_LEN + TOKEN_LEN {
|
||||||
return Err("common header length error".into());
|
return Err("common header length error".into());
|
||||||
}
|
}
|
||||||
let packet_id = u16::from_be_bytes(value[0..2].try_into().unwrap());
|
let packet_id = u16::from_be_bytes(value[0..2].try_into().unwrap());
|
||||||
let version = value[2];
|
let version = value[2];
|
||||||
|
|
||||||
let v1 = &value[3..3 + id_len];
|
let v1 = &value[3..3 + ID_LEN];
|
||||||
let mut id = match std::str::from_utf8(v1) {
|
let mut id = match std::str::from_utf8(v1) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => return Err(SDLanError::ConvertError(e.to_string())),
|
Err(e) => return Err(SDLanError::ConvertError(e.to_string())),
|
||||||
@ -64,14 +64,14 @@ impl<'a> Common<'a> {
|
|||||||
id = id.trim_end_matches('\0');
|
id = id.trim_end_matches('\0');
|
||||||
|
|
||||||
let token = u64::from_be_bytes(
|
let token = u64::from_be_bytes(
|
||||||
value[3 + id_len..3 + id_len + token_len]
|
value[3 + ID_LEN..3 + ID_LEN + TOKEN_LEN]
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let ttl = value[3 + id_len + token_len];
|
let ttl = value[3 + ID_LEN + TOKEN_LEN];
|
||||||
let flags = BigEndian::read_u16(&value[4 + id_len + token_len..6 + id_len + token_len]);
|
let flags = BigEndian::read_u16(&value[4 + ID_LEN + TOKEN_LEN..6 + ID_LEN + TOKEN_LEN]);
|
||||||
let pc = value[6 + id_len + token_len];
|
let pc = value[6 + ID_LEN + TOKEN_LEN];
|
||||||
|
|
||||||
let common = Self {
|
let common = Self {
|
||||||
packet_id,
|
packet_id,
|
||||||
@ -82,7 +82,7 @@ impl<'a> Common<'a> {
|
|||||||
pc: pc.into(),
|
pc: pc.into(),
|
||||||
flags: flags,
|
flags: flags,
|
||||||
};
|
};
|
||||||
Ok((common, &value[7 + id_len + token_len..]))
|
Ok((common, &value[7 + ID_LEN + TOKEN_LEN..]))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_old_common(cmn: &'a Common) -> Self {
|
pub fn from_old_common(cmn: &'a Common) -> Self {
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
use std::{convert::From, net::AddrParseError, string::ParseError};
|
use std::{convert::From, net::AddrParseError, string::ParseError};
|
||||||
|
|
||||||
use aes::cipher::typenum::IsLessOrEqual;
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, SDLanError>;
|
pub type Result<T> = std::result::Result<T, SDLanError>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -12,6 +10,7 @@ pub enum SDLanError {
|
|||||||
SerializeError(String),
|
SerializeError(String),
|
||||||
EncryptError(String),
|
EncryptError(String),
|
||||||
DBError(String),
|
DBError(String),
|
||||||
|
PBError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SDLanError {
|
impl SDLanError {
|
||||||
@ -23,6 +22,7 @@ impl SDLanError {
|
|||||||
Self::SerializeError(ref e) => e,
|
Self::SerializeError(ref e) => e,
|
||||||
Self::EncryptError(ref e) => e,
|
Self::EncryptError(ref e) => e,
|
||||||
Self::DBError(ref e) => e,
|
Self::DBError(ref e) => e,
|
||||||
|
Self::PBError(ref e) => e,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,6 +33,12 @@ impl From<std::io::Error> for SDLanError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<prost::EncodeError> for SDLanError {
|
||||||
|
fn from(value: prost::EncodeError) -> Self {
|
||||||
|
Self::PBError(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&'static str> for SDLanError {
|
impl From<&'static str> for SDLanError {
|
||||||
fn from(value: &'static str) -> Self {
|
fn from(value: &'static str) -> Self {
|
||||||
Self::NormalError(value)
|
Self::NormalError(value)
|
||||||
|
|||||||
@ -49,7 +49,7 @@ pub fn get_current_timestamp() -> u64 {
|
|||||||
|
|
||||||
use crate::peer::SdlanSock;
|
use crate::peer::SdlanSock;
|
||||||
|
|
||||||
use super::{gen_uuid, Result};
|
use super::{gen_uuid, Mac, Result, BROADCAST_MAC, IPV6_MULTICAST_MAC, MULTICAST_MAC};
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
@ -103,21 +103,24 @@ pub fn get_sdlan_sock_from_socketaddr(addr: SocketAddr) -> Result<SdlanSock> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_broadcast(ip: u32) -> bool {
|
#[inline]
|
||||||
ip == 0xffffffff
|
pub fn is_broadcast(mac: &Mac) -> bool {
|
||||||
|
*mac == BROADCAST_MAC
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_multicast(ip: u32) -> bool {
|
#[inline]
|
||||||
let first = ((ip >> 24) & 0xff) as u8;
|
pub fn is_multicast(mac: &Mac) -> bool {
|
||||||
first >= 224 && first <= 239
|
(mac[..3] == MULTICAST_MAC[..3]) && ((mac[3] >> 7) != 0x01)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_multi_broadcast(ip: u32) -> bool {
|
#[inline]
|
||||||
if ip == 0xffffffff {
|
pub fn is_ipv6_multicast(mac: &Mac) -> bool {
|
||||||
return true;
|
mac[..2] == IPV6_MULTICAST_MAC[..2]
|
||||||
}
|
}
|
||||||
let first = ((ip >> 24) & 0xff) as u8;
|
|
||||||
first >= 224 && first <= 239
|
#[inline]
|
||||||
|
pub fn is_multi_broadcast(mac: &Mac) -> bool {
|
||||||
|
is_broadcast(mac) || is_multicast(mac) || is_ipv6_multicast(mac)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ip_to_string(ip: &u32) -> String {
|
pub fn ip_to_string(ip: &u32) -> String {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ mod error;
|
|||||||
mod helper;
|
mod helper;
|
||||||
mod myaes;
|
mod myaes;
|
||||||
mod myrsa;
|
mod myrsa;
|
||||||
|
mod mytype;
|
||||||
mod myuuid;
|
mod myuuid;
|
||||||
|
|
||||||
pub use encode_decode::*;
|
pub use encode_decode::*;
|
||||||
@ -13,6 +14,7 @@ pub use myrsa::{
|
|||||||
gen_rsa_keys, load_private_key_file, load_public_key, load_public_key_file, rsa_decrypt,
|
gen_rsa_keys, load_private_key_file, load_public_key, load_public_key_file, rsa_decrypt,
|
||||||
rsa_encrypt,
|
rsa_encrypt,
|
||||||
};
|
};
|
||||||
|
pub use mytype::*;
|
||||||
pub use myuuid::*;
|
pub use myuuid::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@ -8,7 +8,6 @@ use rsa::{Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey};
|
|||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
pub fn gen_rsa_keys(mut dirpath: &str) {
|
pub fn gen_rsa_keys(mut dirpath: &str) {
|
||||||
if dirpath.len() == 0 {
|
if dirpath.len() == 0 {
|
||||||
|
|||||||
12
src/utils/mytype.rs
Normal file
12
src/utils/mytype.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
pub type Mac = [u8; 6];
|
||||||
|
|
||||||
|
pub const BROADCAST_MAC: Mac = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
|
||||||
|
pub const MULTICAST_MAC: Mac = [0x01, 0x00, 0x5E, 0x00, 0x00, 0x00]; /* First 3 bytes are meaningful */
|
||||||
|
pub const IPV6_MULTICAST_MAC: Mac = [0x33, 0x33, 0x00, 0x00, 0x00, 0x00]; /* First 2 bytes are meaningful */
|
||||||
|
|
||||||
|
pub fn mac_to_string(mac: &Mac) -> String {
|
||||||
|
format!(
|
||||||
|
"[{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}]",
|
||||||
|
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user