windows changed to tun-rs for better performance
This commit is contained in:
parent
d0ec1e29c6
commit
e8f4498806
581
Cargo.lock
generated
581
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -50,6 +50,7 @@ default-net = "0.22.0"
|
|||||||
socket2 = "0.6.3"
|
socket2 = "0.6.3"
|
||||||
hostname = "0.4.2"
|
hostname = "0.4.2"
|
||||||
sysinfo = "0.38.4"
|
sysinfo = "0.38.4"
|
||||||
|
tun-rs = {version="2.8.5", features=["async"]}
|
||||||
# rolling-file = { path = "../rolling-file" }
|
# rolling-file = { path = "../rolling-file" }
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
|||||||
@ -1,22 +1,20 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::atomic::{Ordering};
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::config::{TCP_PING_TIME};
|
use crate::config::TCP_PING_TIME;
|
||||||
use crate::network::ipv6::run_ipv6;
|
use crate::network::ipv6::run_ipv6;
|
||||||
use crate::network::{
|
use crate::network::{get_edge, ping_to_sn, read_and_parse_packet, TunTapPacketHandler};
|
||||||
get_edge, ping_to_sn, read_and_parse_packet, TunTapPacketHandler,
|
|
||||||
};
|
|
||||||
use crate::tcp::{init_quic_conn, send_stun_request};
|
use crate::tcp::{init_quic_conn, send_stun_request};
|
||||||
use crate::utils::{send_to_sock, CommandLine};
|
use crate::utils::{send_to_sock, CommandLine};
|
||||||
use crate::{ConnectionInfo};
|
use crate::ConnectionInfo;
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use etherparse::{PacketBuilder};
|
use etherparse::PacketBuilder;
|
||||||
use sdlan_sn_rs::peer::{SdlanSock};
|
use sdlan_sn_rs::peer::SdlanSock;
|
||||||
use sdlan_sn_rs::utils::{get_current_timestamp, ip_to_string, is_multi_broadcast};
|
use sdlan_sn_rs::utils::{get_current_timestamp, ip_to_string, is_multi_broadcast};
|
||||||
use sdlan_sn_rs::utils::{Mac, Result};
|
use sdlan_sn_rs::utils::{Mac, Result};
|
||||||
use tokio::net::{UdpSocket};
|
use tokio::net::UdpSocket;
|
||||||
use tokio::sync::mpsc::{channel, Receiver, Sender};
|
use tokio::sync::mpsc::{channel, Receiver, Sender};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
@ -120,7 +118,11 @@ pub async fn async_main(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_edge_loop(eee: &'static Node, global_dns_rx: Receiver<(Vec<u8>, SocketAddr)>, cancel: CancellationToken) {
|
async fn run_edge_loop(
|
||||||
|
eee: &'static Node,
|
||||||
|
global_dns_rx: Receiver<(Vec<u8>, SocketAddr)>,
|
||||||
|
cancel: CancellationToken,
|
||||||
|
) {
|
||||||
ping_to_sn().await;
|
ping_to_sn().await;
|
||||||
{
|
{
|
||||||
let cancel2 = cancel.clone();
|
let cancel2 = cancel.clone();
|
||||||
@ -202,7 +204,7 @@ pub async fn loop_socket_v4(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn receive_dns_reply(sock: &Arc<UdpSocket>) -> Option<Vec<u8>> {
|
async fn receive_dns_reply(sock: &Arc<UdpSocket>) -> Option<Vec<u8>> {
|
||||||
let mut reply = vec![0;1024];
|
let mut reply = vec![0; 1024];
|
||||||
if let Ok((size, _from)) = sock.recv_from(&mut reply).await {
|
if let Ok((size, _from)) = sock.recv_from(&mut reply).await {
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
// closed
|
// closed
|
||||||
@ -214,7 +216,11 @@ async fn receive_dns_reply(sock: &Arc<UdpSocket>) -> Option<Vec<u8>> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn loop_tap(eee: &'static Node, mut dns_rx: Receiver<(Vec<u8>, SocketAddr)>, cancel: CancellationToken) {
|
async fn loop_tap(
|
||||||
|
eee: &'static Node,
|
||||||
|
mut dns_rx: Receiver<(Vec<u8>, SocketAddr)>,
|
||||||
|
cancel: CancellationToken,
|
||||||
|
) {
|
||||||
debug!("loop tap");
|
debug!("loop tap");
|
||||||
let (tx, mut rx) = channel(10);
|
let (tx, mut rx) = channel(10);
|
||||||
tokio::spawn(async {
|
tokio::spawn(async {
|
||||||
@ -233,10 +239,10 @@ async fn loop_tap(eee: &'static Node, mut dns_rx: Receiver<(Vec<u8>, SocketAddr)
|
|||||||
if let Ok(mut dns) = simple_dns::Packet::parse(&data.0) {
|
if let Ok(mut dns) = simple_dns::Packet::parse(&data.0) {
|
||||||
let transaction_id = dns.id();
|
let transaction_id = dns.id();
|
||||||
if let Some((ip, port, origin_transaction_id)) = eee.dns_matcher.get_client_info(transaction_id) {
|
if let Some((ip, port, origin_transaction_id)) = eee.dns_matcher.get_client_info(transaction_id) {
|
||||||
warn!("got dns reply from global 223.5.5.5, will send to {}:{}",
|
warn!("got dns reply from global 223.5.5.5, will send to {}:{}",
|
||||||
ip_to_string(&ip), port);
|
ip_to_string(&ip), port);
|
||||||
|
|
||||||
|
|
||||||
let dstmac = eee.device_config.get_mac();
|
let dstmac = eee.device_config.get_mac();
|
||||||
let srcmac = eee.device_config.dns_mac;
|
let srcmac = eee.device_config.dns_mac;
|
||||||
|
|
||||||
@ -293,23 +299,23 @@ async fn loop_tap(eee: &'static Node, mut dns_rx: Receiver<(Vec<u8>, SocketAddr)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "tun", target_os = "windows"))]
|
#[cfg(any(feature = "tun", target_os = "windows"))]
|
||||||
fn get_data_from_tun_with_layer2_zeroed(eee: &Node) -> BytesMut {
|
async fn get_data_from_tun_with_layer2_zeroed(eee: &Node) -> BytesMut {
|
||||||
let mut temp = BytesMut::zeroed(1514);
|
let mut temp = BytesMut::zeroed(1514);
|
||||||
// let mut temp = BytesMut::with_capacity(1514);
|
// let mut temp = BytesMut::with_capacity(1514);
|
||||||
let mut data_buf = temp.split_off(14);
|
let mut data_buf = temp.split_off(14);
|
||||||
|
let Ok(size) = eee.device.recv(&mut data_buf).await else {
|
||||||
let Ok(size) = eee.device.recv(&mut data_buf) else {
|
|
||||||
return BytesMut::new();
|
return BytesMut::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
data_buf.truncate(size);
|
data_buf.truncate(size);
|
||||||
temp.unsplit(data_buf);
|
temp.unsplit(data_buf);
|
||||||
temp
|
temp
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "tun"))]
|
#[cfg(not(feature = "tun"))]
|
||||||
fn get_data_from_tap_with_layer2(eee: &Node) -> BytesMut {
|
async fn get_data_from_tap_with_layer2(eee: &Node) -> BytesMut {
|
||||||
let mut buf = BytesMut::zeroed(1514);
|
let mut buf = BytesMut::zeroed(1514);
|
||||||
let Ok(size) = eee.device.recv(&mut buf) else {
|
let Ok(size) = eee.device.recv(&mut buf).await else {
|
||||||
return BytesMut::new();
|
return BytesMut::new();
|
||||||
};
|
};
|
||||||
buf.truncate(size);
|
buf.truncate(size);
|
||||||
@ -318,16 +324,14 @@ fn get_data_from_tap_with_layer2(eee: &Node) -> BytesMut {
|
|||||||
|
|
||||||
async fn get_tun_flow(eee: &'static Node, tx: Sender<BytesMut>) {
|
async fn get_tun_flow(eee: &'static Node, tx: Sender<BytesMut>) {
|
||||||
loop {
|
loop {
|
||||||
let buf = tokio::task::spawn_blocking(|| {
|
let buf = {
|
||||||
#[cfg(any(feature = "tun", target_os = "windows"))]
|
#[cfg(any(feature = "tun", target_os = "windows"))]
|
||||||
let data = get_data_from_tun_with_layer2_zeroed(eee);
|
let data = get_data_from_tun_with_layer2_zeroed(eee).await;
|
||||||
#[cfg(all(not(feature = "tun"), not(target_os="windows")))]
|
#[cfg(all(not(feature = "tun"), not(target_os = "windows")))]
|
||||||
let data = get_data_from_tap_with_layer2(eee);
|
let data = get_data_from_tap_with_layer2(eee).await;
|
||||||
|
|
||||||
data
|
data
|
||||||
})
|
};
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if buf.len() == 0 {
|
if buf.len() == 0 {
|
||||||
return;
|
return;
|
||||||
@ -360,18 +364,14 @@ async fn read_and_parse_tun_packet(eee: &'static Node, buf: BytesMut) {
|
|||||||
async fn edge_send_packet_to_net(eee: &Node, data: BytesMut) {
|
async fn edge_send_packet_to_net(eee: &Node, data: BytesMut) {
|
||||||
// debug!("edge send packet to net({} bytes): {:?}", data.len(), data);
|
// debug!("edge send packet to net({} bytes): {:?}", data.len(), data);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
let encrypt_key = eee.get_encrypt_key();
|
let encrypt_key = eee.get_encrypt_key();
|
||||||
if encrypt_key.len() == 0 {
|
if encrypt_key.len() == 0 {
|
||||||
error!("drop tun packet due to encrypt key len is 0");
|
error!("drop tun packet due to encrypt key len is 0");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
if let Err(e) = eee
|
if let Err(e) = eee.device.handle_packet_from_device(data).await {
|
||||||
.device
|
|
||||||
.handle_packet_from_device(data)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!("failed to handle packet from device: {}", e.to_string());
|
error!("failed to handle packet from device: {}", e.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,8 @@ use etherparse::{Ethernet2Header, IpHeaders, NetSlice, SlicedPacket, TransportSl
|
|||||||
use ipnet::Ipv4Net;
|
use ipnet::Ipv4Net;
|
||||||
use sdlan_sn_rs::config::SDLAN_DEFAULT_TTL;
|
use sdlan_sn_rs::config::SDLAN_DEFAULT_TTL;
|
||||||
use sdlan_sn_rs::utils::{
|
use sdlan_sn_rs::utils::{
|
||||||
BROADCAST_MAC, Result, SDLanError, aes_encrypt, ip_to_string, is_multi_broadcast, net_bit_len_to_mask
|
aes_encrypt, ip_to_string, is_multi_broadcast, net_bit_len_to_mask, Result, SDLanError,
|
||||||
|
BROADCAST_MAC,
|
||||||
};
|
};
|
||||||
use std::io::{Error, ErrorKind};
|
use std::io::{Error, ErrorKind};
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
@ -13,6 +14,7 @@ use std::process::Command;
|
|||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
use tun_rs::AsyncDevice;
|
||||||
use wintun;
|
use wintun;
|
||||||
|
|
||||||
use crate::network::{
|
use crate::network::{
|
||||||
@ -28,13 +30,130 @@ use super::device::{DeviceConfig, Mode};
|
|||||||
use super::TunTapPacketHandler;
|
use super::TunTapPacketHandler;
|
||||||
|
|
||||||
pub struct Iface {
|
pub struct Iface {
|
||||||
|
device: AsyncDevice,
|
||||||
|
if_idx: u32,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iface {
|
||||||
|
fn new(path: &str, name: &str) -> Self {
|
||||||
|
let dev = tun_rs::DeviceBuilder::new()
|
||||||
|
.wintun_file(path.to_string())
|
||||||
|
.name(name)
|
||||||
|
.layer(tun_rs::Layer::L3)
|
||||||
|
.build_async()
|
||||||
|
.expect("failed to create tun");
|
||||||
|
|
||||||
|
let idx = dev.if_index().expect("failed to get if index");
|
||||||
|
Self {
|
||||||
|
device: dev,
|
||||||
|
if_idx: idx,
|
||||||
|
name: name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iface {
|
||||||
|
pub fn get_if_idx(&self) -> u32 {
|
||||||
|
self.if_idx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn recv(&self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
self.device.recv(buf).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send(&self, content: &[u8]) -> std::io::Result<usize> {
|
||||||
|
self.device.send(content).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reload_config(&self, node: &Node, device_config: &DeviceConfig, network_domain: &str) {
|
||||||
|
let netbit = device_config.get_net_bit();
|
||||||
|
let ip = device_config.get_ip();
|
||||||
|
if netbit == 0 || ip == 0 {
|
||||||
|
error!("reload config's ip is 0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mask = net_bit_len_to_mask(netbit);
|
||||||
|
let ip = ip_to_string(&ip);
|
||||||
|
|
||||||
|
let netbit = ip_to_string(&net_bit_len_to_mask(netbit));
|
||||||
|
|
||||||
|
let mut cmd = Command::new("netsh");
|
||||||
|
debug!("name={}, addr={}, mask={}", self.name, ip, netbit);
|
||||||
|
let command = cmd
|
||||||
|
.creation_flags(0x08000000)
|
||||||
|
.arg("interface")
|
||||||
|
.arg("ip")
|
||||||
|
.arg("set")
|
||||||
|
.arg("address")
|
||||||
|
.arg(&format!("name=\"{}\"", self.name))
|
||||||
|
.arg("source=static")
|
||||||
|
.arg(&format!("addr={}", ip))
|
||||||
|
.arg(&format!("mask={}", netbit));
|
||||||
|
|
||||||
|
let res = command.status();
|
||||||
|
// let res = command.output();
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(r) => {
|
||||||
|
if r.success() {
|
||||||
|
debug!("netsh ok");
|
||||||
|
} else {
|
||||||
|
error!("failed to run netsh, returned {:?}", r.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("failed to run netsh: {}", e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = Command::new("netsh");
|
||||||
|
let command = cmd
|
||||||
|
.creation_flags(0x08000000)
|
||||||
|
.arg("interface")
|
||||||
|
.arg("ipv4")
|
||||||
|
.arg("set")
|
||||||
|
.arg("subinterface")
|
||||||
|
.arg(&format!("\"{}\"", self.name))
|
||||||
|
.arg(format!("mtu={}", device_config.mtu))
|
||||||
|
.arg("store=persistent");
|
||||||
|
|
||||||
|
let res = command.status();
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(r) => {
|
||||||
|
if r.success() {
|
||||||
|
debug!("netsh2 ok");
|
||||||
|
} else {
|
||||||
|
error!("failed to run netsh set mtu, returned {:?}", r.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("failed to run netsh2: {}", e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// let gw = ip_to_string(&default_gw);
|
||||||
|
// debug!("gw = {}", ip);
|
||||||
|
if let Err(e) = set_dns(&self.name, network_domain, &ip, self.if_idx) {
|
||||||
|
error!("failed to set dns: {:?}", e);
|
||||||
|
} else {
|
||||||
|
debug!("set dns ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
node.route_table.apply_system(self.if_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IfaceOld {
|
||||||
if_idx: u32,
|
if_idx: u32,
|
||||||
name: String,
|
name: String,
|
||||||
_adapter: Arc<wintun::Adapter>,
|
_adapter: Arc<wintun::Adapter>,
|
||||||
session: Arc<wintun::Session>,
|
session: Arc<wintun::Session>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iface {
|
impl IfaceOld {
|
||||||
pub fn get_if_idx(&self) -> u32 {
|
pub fn get_if_idx(&self) -> u32 {
|
||||||
self.if_idx
|
self.if_idx
|
||||||
}
|
}
|
||||||
@ -55,11 +174,12 @@ impl Iface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&self, content: &[u8]) -> std::io::Result<usize> {
|
pub fn send(&self, content: &[u8]) -> std::io::Result<usize> {
|
||||||
let Ok(mut pkt) = self
|
let Ok(mut pkt) = self.session.allocate_send_packet(content.len() as u16) else {
|
||||||
.session
|
error!("failed to allocate send packet");
|
||||||
.allocate_send_packet(content.len() as u16) else {
|
return Err(std::io::Error::new(
|
||||||
error!("failed to allocate send packet");
|
std::io::ErrorKind::Other,
|
||||||
return Err(std::io::Error::new(std::io::ErrorKind::Other, "failed to allocate send packet"));
|
"failed to allocate send packet",
|
||||||
|
));
|
||||||
};
|
};
|
||||||
let buf: &mut [u8] = pkt.bytes_mut();
|
let buf: &mut [u8] = pkt.bytes_mut();
|
||||||
buf.copy_from_slice(content);
|
buf.copy_from_slice(content);
|
||||||
@ -310,7 +430,7 @@ impl TunTapPacketHandler for Iface {
|
|||||||
|
|
||||||
// println!("got ip packet");
|
// println!("got ip packet");
|
||||||
// println!("got data: {:?}", rest);
|
// println!("got data: {:?}", rest);
|
||||||
match edge.device.send(rest) {
|
match edge.device.send(rest).await {
|
||||||
Ok(size) => {
|
Ok(size) => {
|
||||||
debug!("send to tun {} bytes", size);
|
debug!("send to tun {} bytes", size);
|
||||||
}
|
}
|
||||||
@ -730,13 +850,18 @@ impl TunTapPacketHandler for Iface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn create_wintun(path: &str, name: &str) -> std::io::Result<Iface> {
|
fn create_wintun(path: &str, name: &str) -> std::io::Result<Iface> {
|
||||||
|
Ok(Iface::new(path, name))
|
||||||
|
/*
|
||||||
let wt = unsafe { wintun::load_from_path(path) }.expect("failed to load wintun");
|
let wt = unsafe { wintun::load_from_path(path) }.expect("failed to load wintun");
|
||||||
|
|
||||||
let adapter = match wintun::Adapter::open(&wt, name) {
|
let adapter = match wintun::Adapter::open(&wt, name) {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
let Ok(adapt) = wintun::Adapter::create(&wt, name, "Punchnet", None) else {
|
let Ok(adapt) = wintun::Adapter::create(&wt, name, "Punchnet", None) else {
|
||||||
return Err(std::io::Error::new(std::io::ErrorKind::Other, "failed to create Punch adapter"));
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
"failed to create Punch adapter",
|
||||||
|
));
|
||||||
};
|
};
|
||||||
adapt
|
adapt
|
||||||
}
|
}
|
||||||
@ -746,7 +871,10 @@ fn create_wintun(path: &str, name: &str) -> std::io::Result<Iface> {
|
|||||||
.expect("failed to get adapter index");
|
.expect("failed to get adapter index");
|
||||||
// println!("idx = {}", idx);
|
// println!("idx = {}", idx);
|
||||||
let Ok(sess) = adapter.start_session(wintun::MAX_RING_CAPACITY) else {
|
let Ok(sess) = adapter.start_session(wintun::MAX_RING_CAPACITY) else {
|
||||||
return Err(std::io::Error::new(std::io::ErrorKind::Other, "failed to start session, maybe one process is running, or not running with admin?"));
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
"failed to start session, maybe one process is running, or not running with admin?",
|
||||||
|
));
|
||||||
};
|
};
|
||||||
let session = Arc::new(sess);
|
let session = Arc::new(sess);
|
||||||
Ok(Iface {
|
Ok(Iface {
|
||||||
@ -755,6 +883,7 @@ fn create_wintun(path: &str, name: &str) -> std::io::Result<Iface> {
|
|||||||
session,
|
session,
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_iface(name: &str, _mode: Mode) -> std::io::Result<Iface> {
|
pub fn new_iface(name: &str, _mode: Mode) -> std::io::Result<Iface> {
|
||||||
@ -778,8 +907,13 @@ pub fn set_dns(name: &str, _network_domain: &str, gw: &str, ifidx: u32) -> Resul
|
|||||||
.creation_flags(0x08000000)
|
.creation_flags(0x08000000)
|
||||||
.status()?;
|
.status()?;
|
||||||
if !res.success() {
|
if !res.success() {
|
||||||
error!("failed to add route for dns 100.100.100.100: {:?}", res.code());
|
error!(
|
||||||
return Err(SDLanError::IOError("failed to add route for dns".to_owned()));
|
"failed to add route for dns 100.100.100.100: {:?}",
|
||||||
|
res.code()
|
||||||
|
);
|
||||||
|
return Err(SDLanError::IOError(
|
||||||
|
"failed to add route for dns".to_owned(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
//println!("res1: {}", res.status.success());
|
//println!("res1: {}", res.status.success());
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user