refactor: remove panic-prone unwrap and expect calls
This commit is contained in:
+11
-12
@@ -1,4 +1,4 @@
|
|||||||
fn main() {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut config = prost_build::Config::new();
|
let mut config = prost_build::Config::new();
|
||||||
|
|
||||||
match protoc_bin_vendored::protoc_bin_path() {
|
match protoc_bin_vendored::protoc_bin_path() {
|
||||||
@@ -14,15 +14,14 @@ fn main() {
|
|||||||
|
|
||||||
config.protoc_arg("--experimental_allow_proto3_optional");
|
config.protoc_arg("--experimental_allow_proto3_optional");
|
||||||
|
|
||||||
config
|
config.compile_protos(
|
||||||
.compile_protos(
|
&[
|
||||||
&[
|
"proto/control_message.proto",
|
||||||
"proto/control_message.proto",
|
"proto/rpc.proto",
|
||||||
"proto/rpc.proto",
|
"proto/client.proto",
|
||||||
"proto/client.proto",
|
"proto/fec.proto",
|
||||||
"proto/fec.proto",
|
],
|
||||||
],
|
&["proto"],
|
||||||
&["proto"],
|
)?;
|
||||||
)
|
Ok(())
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ impl NetworkManager {
|
|||||||
log::info!("绑定出口网卡: {name}");
|
log::info!("绑定出口网卡: {name}");
|
||||||
}
|
}
|
||||||
let mtu = config.mtu.unwrap_or(DEFAULT_MTU);
|
let mtu = config.mtu.unwrap_or(DEFAULT_MTU);
|
||||||
let packet_crypto = PacketCrypto::new_from_str(config.password.as_deref());
|
let packet_crypto = PacketCrypto::new_from_str(config.password.as_deref())?;
|
||||||
let packet_compression = PacketCompression::new(config.compress);
|
let packet_compression = PacketCompression::new(config.compress);
|
||||||
let (server_manager_list, tunnel_to_server, server_rpc) = create_server_tunnel(
|
let (server_manager_list, tunnel_to_server, server_rpc) = create_server_tunnel(
|
||||||
app_state.clone(),
|
app_state.clone(),
|
||||||
@@ -163,7 +163,7 @@ impl NetworkManager {
|
|||||||
EnhancedTunInbound::Nat(
|
EnhancedTunInbound::Nat(
|
||||||
internal_nat_inbound
|
internal_nat_inbound
|
||||||
.clone()
|
.clone()
|
||||||
.expect("internal_nat_inbound must be Some in no-device mode"),
|
.context("internal NAT is unavailable in no-device mode")?,
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -33,15 +33,16 @@ impl PacketCrypto {
|
|||||||
.map(|b| format!("{:02x}", b))
|
.map(|b| format!("{:02x}", b))
|
||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
}
|
}
|
||||||
pub fn new(key_bytes: [u8; 32]) -> Self {
|
pub fn new(key_bytes: [u8; 32]) -> io::Result<Self> {
|
||||||
let unbound = UnboundKey::new(&CHACHA20_POLY1305, &key_bytes).unwrap();
|
let unbound = UnboundKey::new(&CHACHA20_POLY1305, &key_bytes)
|
||||||
|
.map_err(|_| io::Error::other("failed to initialize ChaCha20-Poly1305 key"))?;
|
||||||
let key = LessSafeKey::new(unbound);
|
let key = LessSafeKey::new(unbound);
|
||||||
Self {
|
Ok(Self {
|
||||||
key,
|
key,
|
||||||
seq: Arc::new(AtomicU32::new(rand::random())),
|
seq: Arc::new(AtomicU32::new(rand::random())),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
pub fn new_from_str(s: &str) -> Self {
|
pub fn new_from_str(s: &str) -> io::Result<Self> {
|
||||||
let hash = ring::digest::digest(&ring::digest::SHA256, s.as_bytes());
|
let hash = ring::digest::digest(&ring::digest::SHA256, s.as_bytes());
|
||||||
let mut key_bytes = [0u8; 32];
|
let mut key_bytes = [0u8; 32];
|
||||||
key_bytes.copy_from_slice(hash.as_ref());
|
key_bytes.copy_from_slice(hash.as_ref());
|
||||||
@@ -161,7 +162,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_encrypt_decrypt_in_place() {
|
fn test_encrypt_decrypt_in_place() {
|
||||||
let key = [7u8; 32];
|
let key = [7u8; 32];
|
||||||
let crypto = PacketCrypto::new(key);
|
let crypto = PacketCrypto::new(key).unwrap();
|
||||||
|
|
||||||
let payload_len = 20;
|
let payload_len = 20;
|
||||||
let mut pkt = build_test_packet(payload_len);
|
let mut pkt = build_test_packet(payload_len);
|
||||||
@@ -198,7 +199,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_nonce_unique_per_packet() {
|
fn test_nonce_unique_per_packet() {
|
||||||
let crypto = PacketCrypto::new([7u8; 32]);
|
let crypto = PacketCrypto::new([7u8; 32]).unwrap();
|
||||||
|
|
||||||
let mut pkt1 = build_test_packet(20);
|
let mut pkt1 = build_test_packet(20);
|
||||||
let mut pkt2 = build_test_packet(20);
|
let mut pkt2 = build_test_packet(20);
|
||||||
@@ -227,7 +228,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_clone_shares_seq_counter() {
|
fn test_clone_shares_seq_counter() {
|
||||||
let crypto = PacketCrypto::new([9u8; 32]);
|
let crypto = PacketCrypto::new([9u8; 32]).unwrap();
|
||||||
let cloned = crypto.clone();
|
let cloned = crypto.clone();
|
||||||
|
|
||||||
let mut pkt1 = build_test_packet(8);
|
let mut pkt1 = build_test_packet(8);
|
||||||
@@ -243,9 +244,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_cross_version_compat() {
|
fn test_cross_version_compat() {
|
||||||
let key = [7u8; 32];
|
let key = [7u8; 32];
|
||||||
let crypto = PacketCrypto::new(key);
|
let crypto = PacketCrypto::new(key).unwrap();
|
||||||
// 用相同密钥的另一个实例模拟对端
|
// 用相同密钥的另一个实例模拟对端
|
||||||
let peer = PacketCrypto::new(key);
|
let peer = PacketCrypto::new(key).unwrap();
|
||||||
|
|
||||||
// 模拟旧版本发包:seq 固定为 0,nonce 直接由头部计算
|
// 模拟旧版本发包:seq 固定为 0,nonce 直接由头部计算
|
||||||
let mut pkt = build_test_packet(20);
|
let mut pkt = build_test_packet(20);
|
||||||
@@ -281,7 +282,7 @@ mod tests {
|
|||||||
/// 必须导致解密失败,而不是被静默接受。
|
/// 必须导致解密失败,而不是被静默接受。
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tampered_flags_rejected() {
|
fn test_tampered_flags_rejected() {
|
||||||
let crypto = PacketCrypto::new([7u8; 32]);
|
let crypto = PacketCrypto::new([7u8; 32]).unwrap();
|
||||||
|
|
||||||
let mut pkt = build_test_packet(20);
|
let mut pkt = build_test_packet(20);
|
||||||
crypto.encrypt_in_place(&mut pkt).expect("encrypt failed");
|
crypto.encrypt_in_place(&mut pkt).expect("encrypt failed");
|
||||||
@@ -298,7 +299,7 @@ mod tests {
|
|||||||
/// AAD 覆盖 msg_type(byte0):中间人篡改消息类型必须导致解密失败。
|
/// AAD 覆盖 msg_type(byte0):中间人篡改消息类型必须导致解密失败。
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tampered_msg_type_rejected() {
|
fn test_tampered_msg_type_rejected() {
|
||||||
let crypto = PacketCrypto::new([7u8; 32]);
|
let crypto = PacketCrypto::new([7u8; 32]).unwrap();
|
||||||
|
|
||||||
let mut pkt = build_test_packet(20);
|
let mut pkt = build_test_packet(20);
|
||||||
crypto.encrypt_in_place(&mut pkt).expect("encrypt failed");
|
crypto.encrypt_in_place(&mut pkt).expect("encrypt failed");
|
||||||
@@ -315,7 +316,7 @@ mod tests {
|
|||||||
/// 转发后 ttl 变化的包必须仍能正常解密。
|
/// 转发后 ttl 变化的包必须仍能正常解密。
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ttl_change_still_decrypts() {
|
fn test_ttl_change_still_decrypts() {
|
||||||
let crypto = PacketCrypto::new([7u8; 32]);
|
let crypto = PacketCrypto::new([7u8; 32]).unwrap();
|
||||||
|
|
||||||
let payload_len = 20;
|
let payload_len = 20;
|
||||||
let mut pkt = build_test_packet(payload_len);
|
let mut pkt = build_test_packet(payload_len);
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ impl PacketCrypto {
|
|||||||
chacha20_poly1305::PacketCrypto::key_sign(s)
|
chacha20_poly1305::PacketCrypto::key_sign(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_from_str(s: Option<&str>) -> Self {
|
pub(crate) fn new_from_str(s: Option<&str>) -> io::Result<Self> {
|
||||||
Self {
|
let crypto = s
|
||||||
crypto: s
|
.map(chacha20_poly1305::PacketCrypto::new_from_str)
|
||||||
.map(chacha20_poly1305::PacketCrypto::new_from_str)
|
.transpose()?
|
||||||
.map(Arc::new),
|
.map(Arc::new);
|
||||||
}
|
Ok(Self { crypto })
|
||||||
}
|
}
|
||||||
pub(crate) fn encrypt_reserve(&self) -> usize {
|
pub(crate) fn encrypt_reserve(&self) -> usize {
|
||||||
if self.crypto.is_some() { TAG_LEN } else { 0 }
|
if self.crypto.is_some() { TAG_LEN } else { 0 }
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ async fn create_quic_endpoint(
|
|||||||
quinn::crypto::rustls::QuicClientConfig::try_from(client_config)
|
quinn::crypto::rustls::QuicClientConfig::try_from(client_config)
|
||||||
.context("Failed to create QUIC client config")?,
|
.context("Failed to create QUIC client config")?,
|
||||||
));
|
));
|
||||||
client_config.transport_config(build_transport_config());
|
client_config.transport_config(build_transport_config()?);
|
||||||
let mut endpoint_config = EndpointConfig::default();
|
let mut endpoint_config = EndpointConfig::default();
|
||||||
endpoint_config.max_udp_payload_size(1300)?;
|
endpoint_config.max_udp_payload_size(1300)?;
|
||||||
let mut endpoint = quinn::Endpoint::new_with_abstract_socket(
|
let mut endpoint = quinn::Endpoint::new_with_abstract_socket(
|
||||||
@@ -159,14 +159,18 @@ async fn create_quic_endpoint(
|
|||||||
Ok((inbound, endpoint))
|
Ok((inbound, endpoint))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_transport_config() -> Arc<TransportConfig> {
|
fn build_transport_config() -> anyhow::Result<Arc<TransportConfig>> {
|
||||||
let mut transport = TransportConfig::default();
|
let mut transport = TransportConfig::default();
|
||||||
transport.congestion_controller_factory(Arc::new(BbrConfig::default()));
|
transport.congestion_controller_factory(Arc::new(BbrConfig::default()));
|
||||||
transport.keep_alive_interval(Some(Duration::from_secs(5)));
|
transport.keep_alive_interval(Some(Duration::from_secs(5)));
|
||||||
|
|
||||||
transport.max_idle_timeout(Some(Duration::from_secs(10).try_into().unwrap()));
|
transport.max_idle_timeout(Some(
|
||||||
|
Duration::from_secs(10)
|
||||||
|
.try_into()
|
||||||
|
.context("invalid QUIC idle timeout")?,
|
||||||
|
));
|
||||||
|
|
||||||
Arc::new(transport)
|
Ok(Arc::new(transport))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ip_stack_recv_task(
|
async fn ip_stack_recv_task(
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ async fn handle_outbound(
|
|||||||
} else {
|
} else {
|
||||||
// 创建真实 UDP socket
|
// 创建真实 UDP socket
|
||||||
let bind_addr = if dst.is_ipv4() {
|
let bind_addr = if dst.is_ipv4() {
|
||||||
"0.0.0.0:0".parse().expect("valid IPv4 bind address")
|
SocketAddr::from(([0, 0, 0, 0], 0))
|
||||||
} else {
|
} else {
|
||||||
"[::]:0".parse().expect("valid IPv6 bind address")
|
SocketAddr::from(([0; 8], 0))
|
||||||
};
|
};
|
||||||
let interface = if dst.ip().is_loopback() {
|
let interface = if dst.ip().is_loopback() {
|
||||||
None
|
None
|
||||||
@@ -223,9 +223,9 @@ where
|
|||||||
.next()
|
.next()
|
||||||
.context("UDP NAT destination resolved to no address")?;
|
.context("UDP NAT destination resolved to no address")?;
|
||||||
let bind_addr = if destination.is_ipv4() {
|
let bind_addr = if destination.is_ipv4() {
|
||||||
"0.0.0.0:0".parse().expect("valid IPv4 bind address")
|
SocketAddr::from(([0, 0, 0, 0], 0))
|
||||||
} else {
|
} else {
|
||||||
"[::]:0".parse().expect("valid IPv6 bind address")
|
SocketAddr::from(([0; 8], 0))
|
||||||
};
|
};
|
||||||
let interface = if destination.ip().is_loopback() {
|
let interface = if destination.ip().is_loopback() {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use crate::protocol::transmission::TransmissionBytes;
|
|||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use std::io;
|
use std::io;
|
||||||
use zerocopy::byteorder::{NetworkEndian, U32};
|
use zerocopy::byteorder::{NetworkEndian, U32};
|
||||||
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Ref, Unaligned};
|
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||||
|
|
||||||
#[derive(Debug, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable)]
|
#[derive(Debug, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -167,11 +167,6 @@ impl<B: AsRef<[u8]>> NetPacket<B> {
|
|||||||
}
|
}
|
||||||
Ok(NetPacket { buffer })
|
Ok(NetPacket { buffer })
|
||||||
}
|
}
|
||||||
fn header(&self) -> Ref<&[u8], NetHeader> {
|
|
||||||
// Safe: NetHeader is Unaligned and length is validated in new()
|
|
||||||
let (header, _) = Ref::<&[u8], NetHeader>::from_prefix(self.buffer.as_ref()).unwrap();
|
|
||||||
header
|
|
||||||
}
|
|
||||||
pub fn buffer(&self) -> &[u8] {
|
pub fn buffer(&self) -> &[u8] {
|
||||||
self.buffer.as_ref()
|
self.buffer.as_ref()
|
||||||
}
|
}
|
||||||
@@ -182,37 +177,40 @@ impl<B: AsRef<[u8]>> NetPacket<B> {
|
|||||||
&self.buffer
|
&self.buffer
|
||||||
}
|
}
|
||||||
pub fn msg_type(&self) -> io::Result<MsgType> {
|
pub fn msg_type(&self) -> io::Result<MsgType> {
|
||||||
self.header().msg_type().try_into()
|
(self.buffer.as_ref()[0] & 0x7F).try_into()
|
||||||
}
|
}
|
||||||
pub fn max_ttl(&self) -> u8 {
|
pub fn max_ttl(&self) -> u8 {
|
||||||
self.header().max_ttl()
|
self.buffer.as_ref()[1] >> 4
|
||||||
}
|
}
|
||||||
pub fn ttl(&self) -> u8 {
|
pub fn ttl(&self) -> u8 {
|
||||||
self.header().curr_ttl()
|
self.buffer.as_ref()[1] & 0x0F
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seq(&self) -> u32 {
|
pub fn seq(&self) -> u32 {
|
||||||
self.header().seq.get()
|
let buf = self.buffer.as_ref();
|
||||||
|
u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn src_id(&self) -> u32 {
|
pub fn src_id(&self) -> u32 {
|
||||||
self.header().src_id.get()
|
let buf = self.buffer.as_ref();
|
||||||
|
u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dest_id(&self) -> u32 {
|
pub fn dest_id(&self) -> u32 {
|
||||||
self.header().dest_id.get()
|
let buf = self.buffer.as_ref();
|
||||||
|
u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]])
|
||||||
}
|
}
|
||||||
pub fn is_compressed(&self) -> bool {
|
pub fn is_compressed(&self) -> bool {
|
||||||
(self.header().flags_byte & COMPRESSED) != 0
|
(self.buffer.as_ref()[2] & COMPRESSED) != 0
|
||||||
}
|
}
|
||||||
pub fn is_gateway(&self) -> bool {
|
pub fn is_gateway(&self) -> bool {
|
||||||
(self.header().flags_byte & GATEWAY) != 0
|
(self.buffer.as_ref()[2] & GATEWAY) != 0
|
||||||
}
|
}
|
||||||
pub fn is_fec(&self) -> bool {
|
pub fn is_fec(&self) -> bool {
|
||||||
(self.header().flags_byte & FEC) != 0
|
(self.buffer.as_ref()[2] & FEC) != 0
|
||||||
}
|
}
|
||||||
pub fn is_ethernet(&self) -> bool {
|
pub fn is_ethernet(&self) -> bool {
|
||||||
(self.header().flags_byte & ETHERNET) != 0
|
(self.buffer.as_ref()[2] & ETHERNET) != 0
|
||||||
}
|
}
|
||||||
pub fn head(&self) -> &[u8] {
|
pub fn head(&self) -> &[u8] {
|
||||||
&self.buffer.as_ref()[..HEAD_LENGTH]
|
&self.buffer.as_ref()[..HEAD_LENGTH]
|
||||||
@@ -223,47 +221,54 @@ impl<B: AsRef<[u8]>> NetPacket<B> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<B: AsRef<[u8]> + AsMut<[u8]>> NetPacket<B> {
|
impl<B: AsRef<[u8]> + AsMut<[u8]>> NetPacket<B> {
|
||||||
fn header_mut(&mut self) -> Ref<&mut [u8], NetHeader> {
|
|
||||||
// Safe: NetHeader is Unaligned and length is validated in new()
|
|
||||||
let (header, _) = Ref::<&mut [u8], NetHeader>::from_prefix(self.buffer.as_mut()).unwrap();
|
|
||||||
header
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_msg_type(&mut self, msg_type: MsgType) {
|
pub fn set_msg_type(&mut self, msg_type: MsgType) {
|
||||||
self.header_mut().set_msg_type(msg_type.into());
|
self.buffer.as_mut()[0] = (u8::from(msg_type) & 0x7F) | 0x80;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decr_ttl(&mut self) {
|
pub fn decr_ttl(&mut self) {
|
||||||
self.header_mut().decr_ttl()
|
let ttl_byte = &mut self.buffer.as_mut()[1];
|
||||||
|
let current = *ttl_byte & 0x0F;
|
||||||
|
if current != 0 {
|
||||||
|
*ttl_byte = (*ttl_byte & 0xF0) | (current - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_ttl(&mut self, ttl: u8) {
|
pub fn set_ttl(&mut self, ttl: u8) {
|
||||||
self.header_mut().set_ttl(ttl, ttl);
|
self.buffer.as_mut()[1] = (ttl << 4) | (ttl & 0x0F);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_seq(&mut self, seq: u32) {
|
pub fn set_seq(&mut self, seq: u32) {
|
||||||
self.header_mut().seq.set(seq);
|
self.buffer.as_mut()[4..8].copy_from_slice(&seq.to_be_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_src_id(&mut self, id: u32) {
|
pub fn set_src_id(&mut self, id: u32) {
|
||||||
self.header_mut().src_id.set(id);
|
self.buffer.as_mut()[8..12].copy_from_slice(&id.to_be_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_dest_id(&mut self, id: u32) {
|
pub fn set_dest_id(&mut self, id: u32) {
|
||||||
self.header_mut().dest_id.set(id);
|
self.buffer.as_mut()[12..16].copy_from_slice(&id.to_be_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_flag(&mut self, mask: u8, value: bool) {
|
||||||
|
let flags = &mut self.buffer.as_mut()[2];
|
||||||
|
if value {
|
||||||
|
*flags |= mask;
|
||||||
|
} else {
|
||||||
|
*flags &= !mask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_compressed_flag(&mut self, compressed: bool) {
|
pub fn set_compressed_flag(&mut self, compressed: bool) {
|
||||||
self.header_mut().set_flag(COMPRESSED, compressed);
|
self.set_flag(COMPRESSED, compressed);
|
||||||
}
|
}
|
||||||
pub fn set_gateway_flag(&mut self, gateway: bool) {
|
pub fn set_gateway_flag(&mut self, gateway: bool) {
|
||||||
self.header_mut().set_flag(GATEWAY, gateway);
|
self.set_flag(GATEWAY, gateway);
|
||||||
}
|
}
|
||||||
pub fn set_fec_flag(&mut self, fec: bool) {
|
pub fn set_fec_flag(&mut self, fec: bool) {
|
||||||
self.header_mut().set_flag(FEC, fec);
|
self.set_flag(FEC, fec);
|
||||||
}
|
}
|
||||||
pub fn set_ethernet_flag(&mut self, ethernet: bool) {
|
pub fn set_ethernet_flag(&mut self, ethernet: bool) {
|
||||||
self.header_mut().set_flag(ETHERNET, ethernet);
|
self.set_flag(ETHERNET, ethernet);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_payload(&mut self, data: &[u8]) -> io::Result<()> {
|
pub fn set_payload(&mut self, data: &[u8]) -> io::Result<()> {
|
||||||
|
|||||||
@@ -99,12 +99,17 @@ impl DeviceIOManager {
|
|||||||
// 保证失败时调用方状态完整、可以重试
|
// 保证失败时调用方状态完整、可以重试
|
||||||
let device_mode = device_config.device_mode;
|
let device_mode = device_config.device_mode;
|
||||||
let device = Arc::new(create_device(device_config)?);
|
let device = Arc::new(create_device(device_config)?);
|
||||||
let receiver = receiver.take().unwrap();
|
let Some(receiver_value) = receiver.take() else {
|
||||||
let enhanced_outbound = enhanced_outbound.take().unwrap();
|
bail!("device task already started");
|
||||||
|
};
|
||||||
|
let Some(enhanced_outbound) = enhanced_outbound.take() else {
|
||||||
|
*receiver = Some(receiver_value);
|
||||||
|
bail!("device task already started");
|
||||||
|
};
|
||||||
let task = create(
|
let task = create(
|
||||||
&self.task_group,
|
&self.task_group,
|
||||||
device,
|
device,
|
||||||
receiver.receiver,
|
receiver_value.receiver,
|
||||||
enhanced_outbound,
|
enhanced_outbound,
|
||||||
device_mode,
|
device_mode,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -357,7 +357,9 @@ pub(crate) async fn query_tcp_public_addr_loop(
|
|||||||
let addrs: Vec<SocketAddr> = active_connections.keys().cloned().collect();
|
let addrs: Vec<SocketAddr> = active_connections.keys().cloned().collect();
|
||||||
|
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
let (tcp_stream, _) = active_connections.get_mut(&addr).unwrap();
|
let Some((tcp_stream, _)) = active_connections.get_mut(&addr) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let mut buf = [0u8; 1024];
|
let mut buf = [0u8; 1024];
|
||||||
|
|
||||||
match tcp_stream.try_read(&mut buf) {
|
match tcp_stream.try_read(&mut buf) {
|
||||||
|
|||||||
@@ -149,8 +149,11 @@ pub async fn ping_all(
|
|||||||
ping.set_ttl(1);
|
ping.set_ttl(1);
|
||||||
ping.set_src_id(src.into());
|
ping.set_src_id(src.into());
|
||||||
ping.set_dest_id(id.into());
|
ping.set_dest_id(id.into());
|
||||||
ping.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes())
|
if let Err(error) = ping.set_payload(&crate::utils::time::now_ts_ms().to_be_bytes())
|
||||||
.unwrap();
|
{
|
||||||
|
log::warn!("failed to build route probe: {error}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let route_key = route.route_key();
|
let route_key = route.route_key();
|
||||||
if socket_manager.send_to(ping, &route_key).await.is_ok() {
|
if socket_manager.send_to(ping, &route_key).await.is_ok() {
|
||||||
packet_loss_stats.record_sent(id, route_key);
|
packet_loss_stats.record_sent(id, route_key);
|
||||||
|
|||||||
@@ -151,7 +151,10 @@ impl ServerOutbound {
|
|||||||
|
|
||||||
// 只有一个服务器,直接发送
|
// 只有一个服务器,直接发送
|
||||||
if map.len() == 1 {
|
if map.len() == 1 {
|
||||||
let (server_id, (ips, _)) = map.iter().next().expect("map has exactly one element");
|
let (server_id, (ips, _)) = map
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.context("connected server map unexpectedly became empty")?;
|
||||||
if ips.is_empty() {
|
if ips.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,11 +228,9 @@ fn bind_udp(
|
|||||||
default_interface: &Option<LocalInterface>,
|
default_interface: &Option<LocalInterface>,
|
||||||
) -> io::Result<UdpSocket> {
|
) -> io::Result<UdpSocket> {
|
||||||
let addr: SocketAddr = if name_server.is_ipv4() {
|
let addr: SocketAddr = if name_server.is_ipv4() {
|
||||||
"0.0.0.0:0"
|
SocketAddr::from(([0, 0, 0, 0], 0))
|
||||||
.parse()
|
|
||||||
.expect("valid IPv4 socket address literal")
|
|
||||||
} else {
|
} else {
|
||||||
"[::]:0".parse().expect("valid IPv6 socket address literal")
|
SocketAddr::from(([0; 8], 0))
|
||||||
};
|
};
|
||||||
let socket = rust_p2p_core::socket::bind_udp(addr, default_interface.as_ref())?;
|
let socket = rust_p2p_core::socket::bind_udp(addr, default_interface.as_ref())?;
|
||||||
UdpSocket::from_std(socket.into())
|
UdpSocket::from_std(socket.into())
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ fn toggle_main_window(app: &AppHandle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() -> Result<(), tauri::Error> {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
api_request,
|
api_request,
|
||||||
@@ -320,5 +320,4 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running VNT Desktop");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
fn main() -> Result<(), tauri::Error> {
|
||||||
vnt_desktop_lib::run();
|
vnt_desktop_lib::run()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -1,4 +1,4 @@
|
|||||||
fn main() {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut config = prost_build::Config::new();
|
let mut config = prost_build::Config::new();
|
||||||
|
|
||||||
match protoc_bin_vendored::protoc_bin_path() {
|
match protoc_bin_vendored::protoc_bin_path() {
|
||||||
@@ -13,7 +13,6 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.protoc_arg("--experimental_allow_proto3_optional");
|
config.protoc_arg("--experimental_allow_proto3_optional");
|
||||||
config
|
config.compile_protos(&["proto/local_ipc.proto"], &["proto"])?;
|
||||||
.compile_protos(&["proto/local_ipc.proto"], &["proto"])
|
Ok(())
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,5 +285,5 @@ pub fn ts_to_string(ts_secs: i64) -> String {
|
|||||||
};
|
};
|
||||||
let dt_local = dt.to_offset(local_offset);
|
let dt_local = dt.to_offset(local_offset);
|
||||||
let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
|
let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
|
||||||
dt_local.format(&format).unwrap()
|
dt_local.format(&format).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-21
@@ -5,16 +5,20 @@
|
|||||||
//! - 找不到 pnpm 时:已有产物则告警并沿用;没有产物则报错并给出指引
|
//! - 找不到 pnpm 时:已有产物则告警并沿用;没有产物则报错并给出指引
|
||||||
//! - 设置环境变量 VNT_WEB_SKIP_UI_BUILD=1 可完全跳过前端构建
|
//! - 设置环境变量 VNT_WEB_SKIP_UI_BUILD=1 可完全跳过前端构建
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
fn main() {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
|
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;
|
||||||
let manifest_dir = Path::new(&manifest_dir);
|
let manifest_dir = Path::new(&manifest_dir);
|
||||||
let ui_dir = manifest_dir.join("ui");
|
let ui_dir = manifest_dir.join("ui");
|
||||||
let static_dir = manifest_dir.join("static");
|
let static_dir = manifest_dir.join("static");
|
||||||
let workspace_root = manifest_dir.parent().expect("workspace root");
|
let workspace_root = manifest_dir
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| io::Error::other("vnt-web manifest directory has no parent"))?;
|
||||||
|
|
||||||
// UI 源码变化时重新运行本脚本
|
// UI 源码变化时重新运行本脚本
|
||||||
println!("cargo:rerun-if-changed={}", ui_dir.join("src").display());
|
println!("cargo:rerun-if-changed={}", ui_dir.join("src").display());
|
||||||
@@ -38,12 +42,12 @@ fn main() {
|
|||||||
println!("cargo:rerun-if-env-changed=VNT_WEB_SKIP_UI_BUILD");
|
println!("cargo:rerun-if-env-changed=VNT_WEB_SKIP_UI_BUILD");
|
||||||
|
|
||||||
if std::env::var("VNT_WEB_SKIP_UI_BUILD").is_ok() {
|
if std::env::var("VNT_WEB_SKIP_UI_BUILD").is_ok() {
|
||||||
ensure_static_placeholder(&static_dir);
|
ensure_static_placeholder(&static_dir)?;
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if static_is_fresh(&ui_dir, &static_dir) {
|
if static_is_fresh(&ui_dir, &static_dir) {
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(pnpm) = find_pnpm() else {
|
let Some(pnpm) = find_pnpm() else {
|
||||||
@@ -51,20 +55,22 @@ fn main() {
|
|||||||
println!(
|
println!(
|
||||||
"cargo:warning=未找到 pnpm,沿用 vnt-web/static 中已有的前端产物(可能不是最新)"
|
"cargo:warning=未找到 pnpm,沿用 vnt-web/static 中已有的前端产物(可能不是最新)"
|
||||||
);
|
);
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
panic!(
|
return Err(io::Error::other(
|
||||||
"未找到 pnpm 且 vnt-web/static 没有前端产物。\n\
|
"未找到 pnpm 且 vnt-web/static 没有前端产物。\n\
|
||||||
请安装 Node.js 与 pnpm 后重新构建(cargo 会自动完成前端构建),\n\
|
请安装 Node.js 与 pnpm 后重新构建(cargo 会自动完成前端构建),\n\
|
||||||
或从发布包中获取 static 目录放入 vnt-web/。"
|
或从发布包中获取 static 目录放入 vnt-web/。",
|
||||||
);
|
)
|
||||||
|
.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
if !ui_dir.join("node_modules").is_dir() {
|
if !ui_dir.join("node_modules").is_dir() {
|
||||||
// ui 依赖使用 workspace catalog,必须在仓库根目录安装
|
// ui 依赖使用 workspace catalog,必须在仓库根目录安装
|
||||||
run_or_panic(pnpm, &["install", "--frozen-lockfile"], workspace_root);
|
run_command(pnpm, &["install", "--frozen-lockfile"], workspace_root)?;
|
||||||
}
|
}
|
||||||
run_or_panic(pnpm, &["--filter", "vnt-web-ui", "build"], workspace_root);
|
run_command(pnpm, &["--filter", "vnt-web-ui", "build"], workspace_root)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// pnpm 命令名(Windows 上是 pnpm.cmd,由 cmd.exe 执行)
|
/// pnpm 命令名(Windows 上是 pnpm.cmd,由 cmd.exe 执行)
|
||||||
@@ -80,7 +86,7 @@ fn find_pnpm() -> Option<&'static str> {
|
|||||||
.find(|cmd| Command::new(cmd).arg("--version").output().is_ok())
|
.find(|cmd| Command::new(cmd).arg("--version").output().is_ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_or_panic(program: &str, args: &[&str], dir: &Path) {
|
fn run_command(program: &str, args: &[&str], dir: &Path) -> io::Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"cargo:warning=执行前端构建: {} {} ({})",
|
"cargo:warning=执行前端构建: {} {} ({})",
|
||||||
program,
|
program,
|
||||||
@@ -91,15 +97,21 @@ fn run_or_panic(program: &str, args: &[&str], dir: &Path) {
|
|||||||
.args(args)
|
.args(args)
|
||||||
.current_dir(dir)
|
.current_dir(dir)
|
||||||
.status()
|
.status()
|
||||||
.unwrap_or_else(|e| panic!("执行 {} 失败: {}", program, e));
|
.map_err(|error| {
|
||||||
|
io::Error::new(
|
||||||
|
error.kind(),
|
||||||
|
format!("执行 {program} 失败(目录 {}):{error}", dir.display()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
panic!(
|
return Err(io::Error::other(format!(
|
||||||
"前端构建失败: {} {} (exit: {:?})",
|
"前端构建失败: {} {} (exit: {:?})",
|
||||||
program,
|
program,
|
||||||
args.join(" "),
|
args.join(" "),
|
||||||
status.code()
|
status.code()
|
||||||
);
|
)));
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// static 产物是否比 UI 源码新
|
/// static 产物是否比 UI 源码新
|
||||||
@@ -128,16 +140,16 @@ fn newest_mtime(dir: &Path) -> Option<SystemTime> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 跳过构建时保证 static/ 存在,使 rust_embed 可以编译
|
/// 跳过构建时保证 static/ 存在,使 rust_embed 可以编译
|
||||||
fn ensure_static_placeholder(static_dir: &Path) {
|
fn ensure_static_placeholder(static_dir: &Path) -> io::Result<()> {
|
||||||
if static_dir.join("index.html").is_file() {
|
if static_dir.join("index.html").is_file() {
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
println!("cargo:warning=VNT_WEB_SKIP_UI_BUILD 已设置且 static 为空,写入占位页面");
|
println!("cargo:warning=VNT_WEB_SKIP_UI_BUILD 已设置且 static 为空,写入占位页面");
|
||||||
std::fs::create_dir_all(static_dir).expect("创建 static 目录失败");
|
std::fs::create_dir_all(static_dir)?;
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
static_dir.join("index.html"),
|
static_dir.join("index.html"),
|
||||||
"<!doctype html><html><body><p>VNT Web UI 未构建。请安装 pnpm 后重新执行 cargo build,\
|
"<!doctype html><html><body><p>VNT Web UI 未构建。请安装 pnpm 后重新执行 cargo build,\
|
||||||
或取消 VNT_WEB_SKIP_UI_BUILD。</p></body></html>",
|
或取消 VNT_WEB_SKIP_UI_BUILD。</p></body></html>",
|
||||||
)
|
)?;
|
||||||
.expect("写入占位页面失败");
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-14
@@ -21,7 +21,7 @@ use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
||||||
use time::{OffsetDateTime, format_description};
|
use time::{OffsetDateTime, macros::format_description};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
@@ -205,7 +205,7 @@ impl HttpAppState {
|
|||||||
|
|
||||||
fn timestamp() -> String {
|
fn timestamp() -> String {
|
||||||
let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
|
let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
|
||||||
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
|
let format = format_description!("[hour]:[minute]:[second]");
|
||||||
now.format(&format)
|
now.format(&format)
|
||||||
.unwrap_or_else(|_| "00:00:00".to_string())
|
.unwrap_or_else(|_| "00:00:00".to_string())
|
||||||
}
|
}
|
||||||
@@ -649,9 +649,10 @@ pub async fn run_http_server(
|
|||||||
let handle = service
|
let handle = service
|
||||||
.start_http(addr, token, cancellation.clone())
|
.start_http(addr, token, cancellation.clone())
|
||||||
.await?;
|
.await?;
|
||||||
shutdown_signal().await;
|
let shutdown_result = shutdown_signal().await;
|
||||||
cancellation.cancel();
|
cancellation.cancel();
|
||||||
handle.await??;
|
handle.await??;
|
||||||
|
shutdown_result?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,7 +752,8 @@ fn build_headers_for_path(path: &str) -> HeaderMap {
|
|||||||
};
|
};
|
||||||
headers.insert(
|
headers.insert(
|
||||||
header::CONTENT_TYPE,
|
header::CONTENT_TYPE,
|
||||||
HeaderValue::from_str(mime.as_ref()).unwrap(),
|
HeaderValue::from_str(mime.as_ref())
|
||||||
|
.unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
|
||||||
);
|
);
|
||||||
|
|
||||||
if is_gz {
|
if is_gz {
|
||||||
@@ -1309,7 +1311,7 @@ async fn save_config(Json(req): Json<SaveConfigReq>) -> Json<ApiResponse<()>> {
|
|||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap_or_default()
|
||||||
.as_millis();
|
.as_millis();
|
||||||
format!("{}.toml", now)
|
format!("{}.toml", now)
|
||||||
});
|
});
|
||||||
@@ -1455,28 +1457,29 @@ fn convert_config(cfg: StartConfig) -> anyhow::Result<CoreConfig> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shutdown_signal() {
|
async fn shutdown_signal() -> anyhow::Result<()> {
|
||||||
let ctrl_c = async {
|
let ctrl_c = async {
|
||||||
tokio::signal::ctrl_c()
|
tokio::signal::ctrl_c()
|
||||||
.await
|
.await
|
||||||
.expect("failed to install Ctrl+C handler");
|
.context("failed to install Ctrl+C handler")
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let terminate = async {
|
let terminate = async {
|
||||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
let mut signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||||
.expect("failed to install signal handler")
|
.context("failed to install terminate signal handler")?;
|
||||||
.recv()
|
signal.recv().await;
|
||||||
.await;
|
Ok::<(), anyhow::Error>(())
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
let terminate = std::future::pending::<()>();
|
let terminate = std::future::pending::<anyhow::Result<()>>();
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = ctrl_c => {},
|
result = ctrl_c => result?,
|
||||||
_ = terminate => {},
|
result = terminate => result?,
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_peers(
|
async fn get_peers(
|
||||||
|
|||||||
Reference in New Issue
Block a user