feat: add outbound interface binding
This commit is contained in:
Generated
+3
@@ -6091,6 +6091,7 @@ dependencies = [
|
|||||||
"getifaddrs",
|
"getifaddrs",
|
||||||
"hex",
|
"hex",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"lz4_flex",
|
"lz4_flex",
|
||||||
"machine-uid",
|
"machine-uid",
|
||||||
@@ -6121,6 +6122,7 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
"widestring",
|
"widestring",
|
||||||
"winapi",
|
"winapi",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6154,6 +6156,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"prost",
|
"prost",
|
||||||
"prost-build",
|
"prost-build",
|
||||||
|
"protoc-bin-vendored",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ pub struct FileConfig {
|
|||||||
pub device_id: Option<String>,
|
pub device_id: Option<String>,
|
||||||
pub device_name: Option<String>,
|
pub device_name: Option<String>,
|
||||||
pub tun_name: Option<String>,
|
pub tun_name: Option<String>,
|
||||||
|
pub outbound_interface: Option<String>,
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub cert_mode: Option<String>,
|
pub cert_mode: Option<String>,
|
||||||
pub udp_stun: Option<Vec<String>>,
|
pub udp_stun: Option<Vec<String>>,
|
||||||
@@ -127,6 +128,9 @@ pub struct Args {
|
|||||||
/// 虚拟网卡名称
|
/// 虚拟网卡名称
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub tun_name: Option<String>,
|
pub tun_name: Option<String>,
|
||||||
|
/// 绑定对外通信 Socket 的出口网卡名称
|
||||||
|
#[clap(long)]
|
||||||
|
pub outbound_interface: Option<String>,
|
||||||
/// 关闭内置子网NAT
|
/// 关闭内置子网NAT
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub no_nat: bool,
|
pub no_nat: bool,
|
||||||
@@ -246,6 +250,9 @@ fn build_from_args_and_file(args: Args, file: FileConfig) -> anyhow::Result<(Con
|
|||||||
.or_else(|| file.device_name.clone())
|
.or_else(|| file.device_name.clone())
|
||||||
.unwrap_or_else(default_hostname),
|
.unwrap_or_else(default_hostname),
|
||||||
tun_name: args.tun_name.or_else(|| file.tun_name.clone()),
|
tun_name: args.tun_name.or_else(|| file.tun_name.clone()),
|
||||||
|
outbound_interface: args
|
||||||
|
.outbound_interface
|
||||||
|
.or_else(|| file.outbound_interface.clone()),
|
||||||
password: args.password.or_else(|| file.password.clone()),
|
password: args.password.or_else(|| file.password.clone()),
|
||||||
cert_mode,
|
cert_mode,
|
||||||
input,
|
input,
|
||||||
@@ -285,6 +292,7 @@ fn build_from_args_only(args: Args) -> anyhow::Result<(Config, CtrlConfig)> {
|
|||||||
device_id,
|
device_id,
|
||||||
device_name: args.device_name.unwrap_or_else(default_hostname),
|
device_name: args.device_name.unwrap_or_else(default_hostname),
|
||||||
tun_name: args.tun_name,
|
tun_name: args.tun_name,
|
||||||
|
outbound_interface: args.outbound_interface,
|
||||||
password: args.password,
|
password: args.password,
|
||||||
cert_mode: args
|
cert_mode: args
|
||||||
.cert_mode
|
.cert_mode
|
||||||
@@ -345,6 +353,7 @@ fn build_from_file_only(file: FileConfig) -> anyhow::Result<(Config, CtrlConfig)
|
|||||||
device_id,
|
device_id,
|
||||||
device_name: file.device_name.clone().unwrap_or_else(default_hostname),
|
device_name: file.device_name.clone().unwrap_or_else(default_hostname),
|
||||||
tun_name: file.tun_name.clone(),
|
tun_name: file.tun_name.clone(),
|
||||||
|
outbound_interface: file.outbound_interface.clone(),
|
||||||
password: file.password.clone(),
|
password: file.password.clone(),
|
||||||
cert_mode,
|
cert_mode,
|
||||||
output: file.output.unwrap_or_default(),
|
output: file.output.unwrap_or_default(),
|
||||||
@@ -448,6 +457,9 @@ server = ["quic://1.2.3.4:29872"]
|
|||||||
# 虚拟网卡名称
|
# 虚拟网卡名称
|
||||||
# tun_name = "vnt-tun"
|
# tun_name = "vnt-tun"
|
||||||
|
|
||||||
|
# 绑定对外通信 Socket 的出口网卡名称(用于服务端通信、P2P 打洞及转发流量)
|
||||||
|
# outbound_interface = "Ethernet"
|
||||||
|
|
||||||
# --- 安全配置 ---
|
# --- 安全配置 ---
|
||||||
|
|
||||||
# 加密密码 (可选)
|
# 加密密码 (可选)
|
||||||
@@ -493,9 +505,12 @@ mod tests {
|
|||||||
"test-net",
|
"test-net",
|
||||||
"--tunnel-port",
|
"--tunnel-port",
|
||||||
"12345",
|
"12345",
|
||||||
|
"--outbound-interface",
|
||||||
|
"Ethernet",
|
||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let (config, _) = build_from_args_only(args).unwrap();
|
let (config, _) = build_from_args_only(args).unwrap();
|
||||||
assert_eq!(config.tunnel_port, Some(12345));
|
assert_eq!(config.tunnel_port, Some(12345));
|
||||||
|
assert_eq!(config.outbound_interface.as_deref(), Some("Ethernet"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ socket2 = { version = "0.6.1", features = ["all"] }
|
|||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
winapi = { version = "0.3.9", features = ["winreg"] }
|
winapi = { version = "0.3.9", features = ["winreg"] }
|
||||||
widestring = "1.2"
|
widestring = "1.2"
|
||||||
|
windows-sys = { version = "0.60.2", features = ["Win32_Networking_WinSock"] }
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
prost-build = "0.14"
|
prost-build = "0.14"
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ pub struct Config {
|
|||||||
pub device_id: String,
|
pub device_id: String,
|
||||||
pub device_name: String,
|
pub device_name: String,
|
||||||
pub tun_name: Option<String>,
|
pub tun_name: Option<String>,
|
||||||
|
/// 绑定 VNT 对外通信 Socket 的物理网卡名称。
|
||||||
|
pub outbound_interface: Option<String>,
|
||||||
pub ip: Option<Ipv4Addr>,
|
pub ip: Option<Ipv4Addr>,
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub no_punch: bool,
|
pub no_punch: bool,
|
||||||
@@ -87,7 +89,11 @@ impl Config {
|
|||||||
pub fn key_sign(&self) -> Option<String> {
|
pub fn key_sign(&self) -> Option<String> {
|
||||||
self.password.as_ref().map(|p| PacketCrypto::key_sign(p))
|
self.password.as_ref().map(|p| PacketCrypto::key_sign(p))
|
||||||
}
|
}
|
||||||
pub(crate) fn to_connect_config(&self, index: usize) -> ConnectRegConfig {
|
pub(crate) fn to_connect_config(
|
||||||
|
&self,
|
||||||
|
index: usize,
|
||||||
|
default_interface: Option<rust_p2p_core::socket::LocalInterface>,
|
||||||
|
) -> ConnectRegConfig {
|
||||||
ConnectRegConfig {
|
ConnectRegConfig {
|
||||||
server_addr: self.server_addr[index].clone(),
|
server_addr: self.server_addr[index].clone(),
|
||||||
cert_mode: self.cert_mode.clone(),
|
cert_mode: self.cert_mode.clone(),
|
||||||
@@ -97,6 +103,7 @@ impl Config {
|
|||||||
ip: self.ip,
|
ip: self.ip,
|
||||||
key_sign: self.key_sign(),
|
key_sign: self.key_sign(),
|
||||||
ip_variable: self.ip.is_none(),
|
ip_variable: self.ip.is_none(),
|
||||||
|
default_interface,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,11 +60,32 @@ impl NetworkManager {
|
|||||||
) -> anyhow::Result<NetworkManager> {
|
) -> anyhow::Result<NetworkManager> {
|
||||||
let app_state = AppState::default();
|
let app_state = AppState::default();
|
||||||
config.check()?;
|
config.check()?;
|
||||||
|
let outbound_interface_name = config
|
||||||
|
.outbound_interface
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|name| !name.is_empty())
|
||||||
|
.map(str::to_owned);
|
||||||
|
let resolved_interface =
|
||||||
|
crate::utils::socket::resolve_interface(outbound_interface_name.as_deref())?;
|
||||||
|
let default_interface = resolved_interface
|
||||||
|
.as_ref()
|
||||||
|
.map(|interface| interface.socket_interface.clone());
|
||||||
|
let canonical_interface_name = resolved_interface
|
||||||
|
.as_ref()
|
||||||
|
.map(|interface| interface.name.clone());
|
||||||
|
if let Some(name) = canonical_interface_name.as_deref() {
|
||||||
|
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) =
|
let (server_manager_list, tunnel_to_server, server_rpc) = create_server_tunnel(
|
||||||
create_server_tunnel(app_state.clone(), &config, packet_crypto.clone());
|
app_state.clone(),
|
||||||
|
&config,
|
||||||
|
packet_crypto.clone(),
|
||||||
|
default_interface.clone(),
|
||||||
|
);
|
||||||
let device_io_manager = DeviceIOManager::new(task_group.clone());
|
let device_io_manager = DeviceIOManager::new(task_group.clone());
|
||||||
let allow_subnet = AllowSubnetExternalRoute::new(config.output.clone());
|
let allow_subnet = AllowSubnetExternalRoute::new(config.output.clone());
|
||||||
|
|
||||||
@@ -75,6 +96,8 @@ impl NetworkManager {
|
|||||||
tunnel_to_server.clone(),
|
tunnel_to_server.clone(),
|
||||||
packet_crypto.clone(),
|
packet_crypto.clone(),
|
||||||
config.tunnel_port,
|
config.tunnel_port,
|
||||||
|
default_interface.clone(),
|
||||||
|
canonical_interface_name,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -116,6 +139,7 @@ impl NetworkManager {
|
|||||||
config.no_tun,
|
config.no_tun,
|
||||||
config.allow_port_mapping,
|
config.allow_port_mapping,
|
||||||
app_state.network.clone(),
|
app_state.network.clone(),
|
||||||
|
default_interface.clone(),
|
||||||
);
|
);
|
||||||
let internal_nat_inbound = if config.no_nat && !config.no_tun {
|
let internal_nat_inbound = if config.no_nat && !config.no_tun {
|
||||||
None
|
None
|
||||||
@@ -127,6 +151,7 @@ impl NetworkManager {
|
|||||||
allow_subnet.clone(),
|
allow_subnet.clone(),
|
||||||
app_state.network.clone(),
|
app_state.network.clone(),
|
||||||
config.no_tun,
|
config.no_tun,
|
||||||
|
default_interface.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Some(nat_inbound)
|
Some(nat_inbound)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use pnet_packet::Packet;
|
|||||||
use pnet_packet::icmp::echo_reply::{Identifier, SequenceNumber};
|
use pnet_packet::icmp::echo_reply::{Identifier, SequenceNumber};
|
||||||
use pnet_packet::icmp::{IcmpPacket, IcmpTypes};
|
use pnet_packet::icmp::{IcmpPacket, IcmpTypes};
|
||||||
use pnet_packet::ipv4::Ipv4Packet;
|
use pnet_packet::ipv4::Ipv4Packet;
|
||||||
|
use rust_p2p_core::socket::LocalInterface;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -24,6 +25,7 @@ pub async fn start_icmp_nat(
|
|||||||
ip_stack: &IpStack,
|
ip_stack: &IpStack,
|
||||||
no_tun: bool,
|
no_tun: bool,
|
||||||
network: SharedNetworkAddr,
|
network: SharedNetworkAddr,
|
||||||
|
default_interface: Option<LocalInterface>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let net_icmp_socket = socket2::Socket::new(
|
let net_icmp_socket = socket2::Socket::new(
|
||||||
socket2::Domain::IPV4,
|
socket2::Domain::IPV4,
|
||||||
@@ -31,6 +33,12 @@ pub async fn start_icmp_nat(
|
|||||||
Some(socket2::Protocol::ICMPV4),
|
Some(socket2::Protocol::ICMPV4),
|
||||||
)
|
)
|
||||||
.context("new Socket RAW ICMPV4 failed")?;
|
.context("new Socket RAW ICMPV4 failed")?;
|
||||||
|
crate::utils::socket::bind_socket_to_interface(
|
||||||
|
&net_icmp_socket,
|
||||||
|
default_interface.as_ref(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.context("bind ICMP socket to outbound interface failed")?;
|
||||||
let addr: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0);
|
let addr: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0);
|
||||||
net_icmp_socket
|
net_icmp_socket
|
||||||
.bind(&socket2::SockAddr::from(addr))
|
.bind(&socket2::SockAddr::from(addr))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use anyhow::Context;
|
|||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use pnet_packet::ip::IpNextHeaderProtocol;
|
use pnet_packet::ip::IpNextHeaderProtocol;
|
||||||
use pnet_packet::ipv4::Ipv4Packet;
|
use pnet_packet::ipv4::Ipv4Packet;
|
||||||
|
use rust_p2p_core::socket::LocalInterface;
|
||||||
use std::net::{Ipv4Addr, SocketAddr};
|
use std::net::{Ipv4Addr, SocketAddr};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -24,6 +25,7 @@ pub(crate) struct InternalNatInbound {
|
|||||||
ip_stack_send: Arc<IpStackSend>,
|
ip_stack_send: Arc<IpStackSend>,
|
||||||
allow_subnet: AllowSubnetExternalRoute,
|
allow_subnet: AllowSubnetExternalRoute,
|
||||||
network: SharedNetworkAddr,
|
network: SharedNetworkAddr,
|
||||||
|
default_interface: Option<LocalInterface>,
|
||||||
}
|
}
|
||||||
impl InternalNatInbound {
|
impl InternalNatInbound {
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
@@ -33,13 +35,28 @@ impl InternalNatInbound {
|
|||||||
allow_subnet: AllowSubnetExternalRoute,
|
allow_subnet: AllowSubnetExternalRoute,
|
||||||
network: SharedNetworkAddr,
|
network: SharedNetworkAddr,
|
||||||
no_tun: bool,
|
no_tun: bool,
|
||||||
|
default_interface: Option<LocalInterface>,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
let ip_stack_config = IpStackConfig::builder().mtu(mtu).build();
|
let ip_stack_config = IpStackConfig::builder().mtu(mtu).build();
|
||||||
let (ip_stack, ip_stack_send, ip_stack_recv) = tcp_ip::ip_stack(ip_stack_config)?;
|
let (ip_stack, ip_stack_send, ip_stack_recv) = tcp_ip::ip_stack(ip_stack_config)?;
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
icmp_nat::start_icmp_nat(task_group, &ip_stack, no_tun, network.clone()).await?;
|
icmp_nat::start_icmp_nat(
|
||||||
tcp_nat::start_tcp_nat(task_group, &ip_stack, no_tun, network.clone()).await?;
|
task_group,
|
||||||
udp_nat::start_udp_nat(task_group, &ip_stack).await?;
|
&ip_stack,
|
||||||
|
no_tun,
|
||||||
|
network.clone(),
|
||||||
|
default_interface.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
tcp_nat::start_tcp_nat(
|
||||||
|
task_group,
|
||||||
|
&ip_stack,
|
||||||
|
no_tun,
|
||||||
|
network.clone(),
|
||||||
|
default_interface.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
udp_nat::start_udp_nat(task_group, &ip_stack, default_interface.clone()).await?;
|
||||||
task_group.spawn(async move {
|
task_group.spawn(async move {
|
||||||
if let Err(e) = ip_stack_recv_task(ip_stack_recv, hybrid_outbound).await {
|
if let Err(e) = ip_stack_recv_task(ip_stack_recv, hybrid_outbound).await {
|
||||||
log::error!("ip stack recv task error: {e:?}");
|
log::error!("ip stack recv task error: {e:?}");
|
||||||
@@ -50,6 +67,7 @@ impl InternalNatInbound {
|
|||||||
ip_stack_send: Arc::new(ip_stack_send),
|
ip_stack_send: Arc::new(ip_stack_send),
|
||||||
allow_subnet,
|
allow_subnet,
|
||||||
network,
|
network,
|
||||||
|
default_interface,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub async fn send(&self, data: &[u8], net: &NetworkAddr) -> anyhow::Result<()> {
|
pub async fn send(&self, data: &[u8], net: &NetworkAddr) -> anyhow::Result<()> {
|
||||||
@@ -140,7 +158,13 @@ impl InternalNatInbound {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let dst = SocketAddr::new(dest_ip.into(), dest_port);
|
let dst = SocketAddr::new(dest_ip.into(), dest_port);
|
||||||
tcp_nat::stream_nat(recv_stream, send_stream, dst).await
|
tcp_nat::stream_nat(
|
||||||
|
recv_stream,
|
||||||
|
send_stream,
|
||||||
|
dst,
|
||||||
|
self.default_interface.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,14 +173,21 @@ pub(crate) struct PortMappingManager {
|
|||||||
no_tun: bool,
|
no_tun: bool,
|
||||||
allow_port_mapping: bool,
|
allow_port_mapping: bool,
|
||||||
network: SharedNetworkAddr,
|
network: SharedNetworkAddr,
|
||||||
|
default_interface: Option<LocalInterface>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PortMappingManager {
|
impl PortMappingManager {
|
||||||
pub fn new(no_tun: bool, allow_port_mapping: bool, network: SharedNetworkAddr) -> Self {
|
pub fn new(
|
||||||
|
no_tun: bool,
|
||||||
|
allow_port_mapping: bool,
|
||||||
|
network: SharedNetworkAddr,
|
||||||
|
default_interface: Option<LocalInterface>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
no_tun,
|
no_tun,
|
||||||
allow_port_mapping,
|
allow_port_mapping,
|
||||||
network,
|
network,
|
||||||
|
default_interface,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub async fn tcp_mapping<R, W>(
|
pub async fn tcp_mapping<R, W>(
|
||||||
@@ -181,13 +212,25 @@ impl PortMappingManager {
|
|||||||
|
|
||||||
if dest_ip == net.ip {
|
if dest_ip == net.ip {
|
||||||
let dst = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), dest_port);
|
let dst = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), dest_port);
|
||||||
return tcp_nat::stream_nat(recv_stream, send_stream, dst).await;
|
return tcp_nat::stream_nat(
|
||||||
|
recv_stream,
|
||||||
|
send_stream,
|
||||||
|
dst,
|
||||||
|
self.default_interface.as_ref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
} else if net.network().contains(&dest_ip) {
|
} else if net.network().contains(&dest_ip) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let dst = format!("{}:{}", dest, dest_port);
|
let dst = format!("{}:{}", dest, dest_port);
|
||||||
tcp_nat::stream_nat(recv_stream, send_stream, dst).await
|
tcp_nat::stream_nat(
|
||||||
|
recv_stream,
|
||||||
|
send_stream,
|
||||||
|
dst,
|
||||||
|
self.default_interface.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
pub async fn udp_mapping<R, W>(
|
pub async fn udp_mapping<R, W>(
|
||||||
&self,
|
&self,
|
||||||
@@ -211,12 +254,24 @@ impl PortMappingManager {
|
|||||||
|
|
||||||
if dest_ip == net.ip {
|
if dest_ip == net.ip {
|
||||||
let dst = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), dest_port);
|
let dst = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), dest_port);
|
||||||
return udp_nat::stream_nat(recv_stream, send_stream, dst).await;
|
return udp_nat::stream_nat(
|
||||||
|
recv_stream,
|
||||||
|
send_stream,
|
||||||
|
dst,
|
||||||
|
self.default_interface.as_ref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
} else if net.network().contains(&dest_ip) {
|
} else if net.network().contains(&dest_ip) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let dst = format!("{}:{}", dest, dest_port);
|
let dst = format!("{}:{}", dest, dest_port);
|
||||||
udp_nat::stream_nat(recv_stream, send_stream, dst).await
|
udp_nat::stream_nat(
|
||||||
|
recv_stream,
|
||||||
|
send_stream,
|
||||||
|
dst,
|
||||||
|
self.default_interface.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
use crate::context::SharedNetworkAddr;
|
use crate::context::SharedNetworkAddr;
|
||||||
use crate::utils::task_control::TaskGroup;
|
use crate::utils::task_control::TaskGroup;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use rust_p2p_core::socket::LocalInterface;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
use tcp_ip::IpStack;
|
use tcp_ip::IpStack;
|
||||||
use tcp_ip::tcp::TcpListener;
|
use tcp_ip::tcp::TcpListener;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
use tokio::net::ToSocketAddrs;
|
||||||
|
|
||||||
pub async fn start_tcp_nat(
|
pub async fn start_tcp_nat(
|
||||||
task_group: &TaskGroup,
|
task_group: &TaskGroup,
|
||||||
ip_stack: &IpStack,
|
ip_stack: &IpStack,
|
||||||
no_tun: bool,
|
no_tun: bool,
|
||||||
network: SharedNetworkAddr,
|
network: SharedNetworkAddr,
|
||||||
|
default_interface: Option<LocalInterface>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let tcp_listener = TcpListener::bind_all(ip_stack.clone()).await?;
|
let tcp_listener = TcpListener::bind_all(ip_stack.clone()).await?;
|
||||||
let group = task_group.clone();
|
let group = task_group.clone();
|
||||||
task_group.spawn(async move {
|
task_group.spawn(async move {
|
||||||
if let Err(e) = listen_task(&group, tcp_listener, no_tun, network).await {
|
if let Err(e) = listen_task(&group, tcp_listener, no_tun, network, default_interface).await
|
||||||
|
{
|
||||||
log::error!("listen task error: {:?}", e);
|
log::error!("listen task error: {:?}", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -29,6 +32,7 @@ async fn listen_task(
|
|||||||
mut tcp_listener: TcpListener,
|
mut tcp_listener: TcpListener,
|
||||||
no_tun: bool,
|
no_tun: bool,
|
||||||
network: SharedNetworkAddr,
|
network: SharedNetworkAddr,
|
||||||
|
default_interface: Option<LocalInterface>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
loop {
|
loop {
|
||||||
// 单次 accept/地址查询失败不能拖垮整个监听任务:
|
// 单次 accept/地址查询失败不能拖垮整个监听任务:
|
||||||
@@ -60,8 +64,9 @@ async fn listen_task(
|
|||||||
local_addr.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST));
|
local_addr.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let default_interface = default_interface.clone();
|
||||||
task_group.spawn(async move {
|
task_group.spawn(async move {
|
||||||
if let Err(e) = stream_task(stream, local_addr).await {
|
if let Err(e) = stream_task(stream, local_addr, default_interface.as_ref()).await {
|
||||||
log::error!("stream task Error: {:?},{peer_addr}->{local_addr}", e);
|
log::error!("stream task Error: {:?},{peer_addr}->{local_addr}", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -71,8 +76,9 @@ async fn listen_task(
|
|||||||
async fn stream_task(
|
async fn stream_task(
|
||||||
mut inner_stream: tcp_ip::tcp::TcpStream,
|
mut inner_stream: tcp_ip::tcp::TcpStream,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
|
default_interface: Option<&LocalInterface>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut tokio_stream = TcpStream::connect(addr).await?;
|
let mut tokio_stream = crate::utils::socket::connect_tcp(addr, default_interface).await?;
|
||||||
tokio::io::copy_bidirectional(&mut inner_stream, &mut tokio_stream).await?;
|
tokio::io::copy_bidirectional(&mut inner_stream, &mut tokio_stream).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -81,14 +87,15 @@ pub(crate) async fn stream_nat<R, W, A: ToSocketAddrs + Debug>(
|
|||||||
recv_stream: R,
|
recv_stream: R,
|
||||||
send_stream: W,
|
send_stream: W,
|
||||||
addr: A,
|
addr: A,
|
||||||
|
default_interface: Option<&LocalInterface>,
|
||||||
) -> anyhow::Result<()>
|
) -> anyhow::Result<()>
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin,
|
R: AsyncRead + Unpin,
|
||||||
W: AsyncWrite + Unpin,
|
W: AsyncWrite + Unpin,
|
||||||
{
|
{
|
||||||
let mut tokio_stream = TcpStream::connect(&addr)
|
let mut tokio_stream = crate::utils::socket::connect_tcp_resolved(addr, default_interface)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("error connecting to {:?}", addr))?;
|
.context("error connecting to NAT destination")?;
|
||||||
crate::port_mapping::tcp_port_mapping::copy_bidirectional_split(
|
crate::port_mapping::tcp_port_mapping::copy_bidirectional_split(
|
||||||
&mut tokio_stream,
|
&mut tokio_stream,
|
||||||
recv_stream,
|
recv_stream,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::utils::task_control::{SubTask, TaskGroup};
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use rust_p2p_core::socket::LocalInterface;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
@@ -25,7 +26,11 @@ type NatTable = Arc<Mutex<HashMap<(SocketAddr, SocketAddr), NatEntry>>>;
|
|||||||
const NAT_IDLE_TIMEOUT: Duration = Duration::from_secs(60 * 5);
|
const NAT_IDLE_TIMEOUT: Duration = Duration::from_secs(60 * 5);
|
||||||
const NAT_GC_INTERVAL: Duration = Duration::from_secs(60);
|
const NAT_GC_INTERVAL: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
pub async fn start_udp_nat(task_group: &TaskGroup, ip_stack: &IpStack) -> anyhow::Result<()> {
|
pub async fn start_udp_nat(
|
||||||
|
task_group: &TaskGroup,
|
||||||
|
ip_stack: &IpStack,
|
||||||
|
default_interface: Option<LocalInterface>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let inner_socket = tcp_ip::udp::UdpSocket::bind_all(ip_stack.clone()).await?;
|
let inner_socket = tcp_ip::udp::UdpSocket::bind_all(ip_stack.clone()).await?;
|
||||||
let inner_socket = Arc::new(inner_socket);
|
let inner_socket = Arc::new(inner_socket);
|
||||||
let nat_table: NatTable = Arc::new(Mutex::new(HashMap::new()));
|
let nat_table: NatTable = Arc::new(Mutex::new(HashMap::new()));
|
||||||
@@ -42,8 +47,16 @@ pub async fn start_udp_nat(task_group: &TaskGroup, ip_stack: &IpStack) -> anyhow
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) =
|
if let Err(e) = handle_outbound(
|
||||||
handle_outbound(&group, &inner_socket, &nat_table, src, dst, &buf[..len]).await
|
&group,
|
||||||
|
&inner_socket,
|
||||||
|
&nat_table,
|
||||||
|
src,
|
||||||
|
dst,
|
||||||
|
&buf[..len],
|
||||||
|
default_interface.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
log::warn!("udp nat outbound error: {e:?}");
|
log::warn!("udp nat outbound error: {e:?}");
|
||||||
}
|
}
|
||||||
@@ -60,6 +73,7 @@ async fn handle_outbound(
|
|||||||
src: SocketAddr,
|
src: SocketAddr,
|
||||||
dst: SocketAddr,
|
dst: SocketAddr,
|
||||||
packet: &[u8],
|
packet: &[u8],
|
||||||
|
default_interface: Option<&LocalInterface>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let key = (src, dst);
|
let key = (src, dst);
|
||||||
|
|
||||||
@@ -70,7 +84,17 @@ async fn handle_outbound(
|
|||||||
entry.socket.clone()
|
entry.socket.clone()
|
||||||
} else {
|
} else {
|
||||||
// 创建真实 UDP socket
|
// 创建真实 UDP socket
|
||||||
let sock = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
|
let bind_addr = if dst.is_ipv4() {
|
||||||
|
"0.0.0.0:0".parse().expect("valid IPv4 bind address")
|
||||||
|
} else {
|
||||||
|
"[::]:0".parse().expect("valid IPv6 bind address")
|
||||||
|
};
|
||||||
|
let interface = if dst.ip().is_loopback() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
default_interface
|
||||||
|
};
|
||||||
|
let sock = crate::utils::socket::bind_udp(bind_addr, interface)?;
|
||||||
sock.connect(dst).await?;
|
sock.connect(dst).await?;
|
||||||
let sock = Arc::new(sock);
|
let sock = Arc::new(sock);
|
||||||
|
|
||||||
@@ -184,19 +208,32 @@ fn spawn_nat_gc(task_group: &TaskGroup, nat: NatTable) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn stream_nat<R, W, A: ToSocketAddrs + Debug>( recv_stream: R,
|
pub(crate) async fn stream_nat<R, W, A: ToSocketAddrs + Debug>(
|
||||||
|
recv_stream: R,
|
||||||
send_stream: W,
|
send_stream: W,
|
||||||
addr: A,
|
addr: A,
|
||||||
|
default_interface: Option<&LocalInterface>,
|
||||||
) -> anyhow::Result<()>
|
) -> anyhow::Result<()>
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin,
|
R: AsyncRead + Unpin,
|
||||||
W: AsyncWrite + Unpin,
|
W: AsyncWrite + Unpin,
|
||||||
{
|
{
|
||||||
let udp_socket = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
|
let destination = tokio::net::lookup_host(addr)
|
||||||
udp_socket
|
.await?
|
||||||
.connect(&addr)
|
.next()
|
||||||
.await
|
.context("UDP NAT destination resolved to no address")?;
|
||||||
.with_context(|| format!("error connecting to {:?}", addr))?;
|
let bind_addr = if destination.is_ipv4() {
|
||||||
|
"0.0.0.0:0".parse().expect("valid IPv4 bind address")
|
||||||
|
} else {
|
||||||
|
"[::]:0".parse().expect("valid IPv6 bind address")
|
||||||
|
};
|
||||||
|
let interface = if destination.ip().is_loopback() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
default_interface
|
||||||
|
};
|
||||||
|
let udp_socket = crate::utils::socket::bind_udp(bind_addr, interface)?;
|
||||||
|
udp_socket.connect(destination).await?;
|
||||||
let mut framed_read = FramedRead::new(recv_stream, LengthDelimitedCodec::new());
|
let mut framed_read = FramedRead::new(recv_stream, LengthDelimitedCodec::new());
|
||||||
let mut framed_write = FramedWrite::new(send_stream, LengthDelimitedCodec::new());
|
let mut framed_write = FramedWrite::new(send_stream, LengthDelimitedCodec::new());
|
||||||
let mut buf = vec![0u8; 65536];
|
let mut buf = vec![0u8; 65536];
|
||||||
@@ -219,7 +256,6 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::context::AppState;
|
use crate::context::AppState;
|
||||||
use rust_p2p_core::nat::{NatInfo, NatType};
|
use rust_p2p_core::nat::{NatInfo, NatType};
|
||||||
|
use rust_p2p_core::socket::LocalInterface;
|
||||||
use rust_p2p_core::tunnel::SocketManager;
|
use rust_p2p_core::tunnel::SocketManager;
|
||||||
use rust_p2p_core::tunnel::udp::Model;
|
use rust_p2p_core::tunnel::udp::Model;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -9,19 +10,38 @@ use std::time::Duration;
|
|||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
|
||||||
pub async fn my_nat_info(app_context: AppState, socket_manager: SocketManager) {
|
pub async fn my_nat_info(
|
||||||
|
app_context: AppState,
|
||||||
|
socket_manager: SocketManager,
|
||||||
|
default_interface: Option<LocalInterface>,
|
||||||
|
outbound_interface_name: Option<String>,
|
||||||
|
) {
|
||||||
loop {
|
loop {
|
||||||
my_nat_info_impl(&app_context, &socket_manager).await;
|
my_nat_info_impl(
|
||||||
|
&app_context,
|
||||||
|
&socket_manager,
|
||||||
|
default_interface.as_ref(),
|
||||||
|
outbound_interface_name.as_deref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
tokio::time::sleep(Duration::from_secs(60 * 30)).await;
|
tokio::time::sleep(Duration::from_secs(60 * 30)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn my_nat_info_impl(app_context: &AppState, socket_manager: &SocketManager) {
|
async fn my_nat_info_impl(
|
||||||
|
app_context: &AppState,
|
||||||
|
socket_manager: &SocketManager,
|
||||||
|
default_interface: Option<&LocalInterface>,
|
||||||
|
outbound_interface_name: Option<&str>,
|
||||||
|
) {
|
||||||
let network = app_context.network.network();
|
let network = app_context.network.network();
|
||||||
let mut local_ipv4s = Vec::new();
|
let mut local_ipv4s = Vec::new();
|
||||||
let mut local_ipv6 = Vec::new();
|
let mut local_ipv6 = Vec::new();
|
||||||
match getifaddrs::getifaddrs() {
|
match getifaddrs::getifaddrs() {
|
||||||
Ok(addrs) => {
|
Ok(addrs) => {
|
||||||
for x in addrs {
|
for x in addrs {
|
||||||
|
if outbound_interface_name.is_some_and(|name| x.name != name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let Some(ip) = x.address.ip_addr() else {
|
let Some(ip) = x.address.ip_addr() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -67,20 +87,28 @@ async fn my_nat_info_impl(app_context: &AppState, socket_manager: &SocketManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
log::info!("local_ipv4s: {:?}", local_ipv4s);
|
log::info!("local_ipv4s: {:?}", local_ipv4s);
|
||||||
let detected = rust_p2p_core::extend::addr::local_ipv4()
|
let detected = if outbound_interface_name.is_none() {
|
||||||
.await
|
rust_p2p_core::extend::addr::local_ipv4()
|
||||||
.map_err(|e| {
|
.await
|
||||||
log::warn!("local ipv4 failed {e:?}");
|
.map_err(|e| {
|
||||||
e
|
log::warn!("local ipv4 failed {e:?}");
|
||||||
})
|
e
|
||||||
.ok();
|
})
|
||||||
|
.ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let Some((local_ipv4, merged)) = select_local_ipv4(detected, &local_ipv4s) else {
|
let Some((local_ipv4, merged)) = select_local_ipv4(detected, &local_ipv4s) else {
|
||||||
log::warn!("未发现可用本机 IPv4 地址,跳过本次 NAT 信息更新");
|
log::warn!("未发现可用本机 IPv4 地址,跳过本次 NAT 信息更新");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// 保留网卡扫描结果,主地址排在首位
|
// 保留网卡扫描结果,主地址排在首位
|
||||||
local_ipv4s = merged;
|
local_ipv4s = merged;
|
||||||
let mut ipv6 = rust_p2p_core::extend::addr::local_ipv6().await.ok();
|
let mut ipv6 = if outbound_interface_name.is_none() {
|
||||||
|
rust_p2p_core::extend::addr::local_ipv6().await.ok()
|
||||||
|
} else {
|
||||||
|
local_ipv6.first().cloned()
|
||||||
|
};
|
||||||
if let Some(addr) = ipv6 {
|
if let Some(addr) = ipv6 {
|
||||||
if addr.is_loopback()
|
if addr.is_loopback()
|
||||||
|| addr.is_unique_local()
|
|| addr.is_unique_local()
|
||||||
@@ -134,12 +162,13 @@ async fn my_nat_info_impl(app_context: &AppState, socket_manager: &SocketManager
|
|||||||
if stun_server.is_empty() {
|
if stun_server.is_empty() {
|
||||||
stun_server = default_udp_stun();
|
stun_server = default_udp_stun();
|
||||||
}
|
}
|
||||||
let (nat_type, public_ips, port_range) = rust_p2p_core::stun::stun_test_nat(stun_server, None)
|
let (nat_type, public_ips, port_range) =
|
||||||
.await
|
rust_p2p_core::stun::stun_test_nat(stun_server, default_interface)
|
||||||
.unwrap_or_else(|e| {
|
.await
|
||||||
log::warn!("stun_test_nat {e:?}");
|
.unwrap_or_else(|e| {
|
||||||
(NatType::Cone, vec![], 0)
|
log::warn!("stun_test_nat {e:?}");
|
||||||
});
|
(NatType::Cone, vec![], 0)
|
||||||
|
});
|
||||||
log::info!("nat_type:{nat_type:?},public_ips:{public_ips:?},port_range={port_range}");
|
log::info!("nat_type:{nat_type:?},public_ips:{public_ips:?},port_range={port_range}");
|
||||||
nat_info.nat_type = nat_type;
|
nat_info.nat_type = nat_type;
|
||||||
nat_info.public_ips = public_ips;
|
nat_info.public_ips = public_ips;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use crate::tunnel_core::p2p::transport::punch::{PunchTaskContext, punch_task};
|
|||||||
use crate::tunnel_core::server::outbound::ServerOutbound;
|
use crate::tunnel_core::server::outbound::ServerOutbound;
|
||||||
use crate::utils::task_control::TaskGroup;
|
use crate::utils::task_control::TaskGroup;
|
||||||
use rust_p2p_core::punch::Puncher;
|
use rust_p2p_core::punch::Puncher;
|
||||||
|
use rust_p2p_core::socket::LocalInterface;
|
||||||
use rust_p2p_core::tunnel::{Tunnel, TunnelDispatcher, new_tunnel_component};
|
use rust_p2p_core::tunnel::{Tunnel, TunnelDispatcher, new_tunnel_component};
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -24,17 +25,29 @@ pub async fn init_tunnel(
|
|||||||
tunnel_to_server: ServerOutbound,
|
tunnel_to_server: ServerOutbound,
|
||||||
packet_crypto: PacketCrypto,
|
packet_crypto: PacketCrypto,
|
||||||
tunnel_port: Option<u16>,
|
tunnel_port: Option<u16>,
|
||||||
|
default_interface: Option<LocalInterface>,
|
||||||
|
outbound_interface_name: Option<String>,
|
||||||
) -> anyhow::Result<(Puncher, P2pOutbound, P2pTask)> {
|
) -> anyhow::Result<(Puncher, P2pOutbound, P2pTask)> {
|
||||||
let tunnel_port = tunnel_port.unwrap_or(0);
|
let tunnel_port = tunnel_port.unwrap_or(0);
|
||||||
let udp_config = rust_p2p_core::tunnel::config::UdpTunnelConfig::default()
|
let mut udp_config = rust_p2p_core::tunnel::config::UdpTunnelConfig::default()
|
||||||
.set_main_udp_count(2)
|
.set_main_udp_count(2)
|
||||||
.set_sub_udp_count(82)
|
.set_sub_udp_count(82)
|
||||||
.set_simple_udp_port(tunnel_port);
|
.set_simple_udp_port(tunnel_port);
|
||||||
let tcp_config = rust_p2p_core::tunnel::config::TcpTunnelConfig::new(Box::new(
|
let mut tcp_config = rust_p2p_core::tunnel::config::TcpTunnelConfig::new(Box::new(
|
||||||
rust_p2p_core::tunnel::tcp::LengthPrefixedInitCodec,
|
rust_p2p_core::tunnel::tcp::LengthPrefixedInitCodec,
|
||||||
))
|
))
|
||||||
.set_tcp_multiplexing_limit(2)
|
.set_tcp_multiplexing_limit(2)
|
||||||
.set_tcp_port(tunnel_port);
|
.set_tcp_port(tunnel_port);
|
||||||
|
if let Some(interface) = default_interface.clone() {
|
||||||
|
// rust-p2p-core 当前的接口绑定实现针对 IPv4;指定出口网卡时关闭
|
||||||
|
// 未绑定的 IPv6 Socket,避免流量绕过所选网卡。
|
||||||
|
udp_config = udp_config
|
||||||
|
.set_default_interface(interface.clone())
|
||||||
|
.set_use_v6(false);
|
||||||
|
tcp_config = tcp_config
|
||||||
|
.set_default_interface(interface)
|
||||||
|
.set_use_v6(false);
|
||||||
|
}
|
||||||
let config = rust_p2p_core::tunnel::config::TunnelConfig::empty()
|
let config = rust_p2p_core::tunnel::config::TunnelConfig::empty()
|
||||||
.set_udp_tunnel_config(udp_config)
|
.set_udp_tunnel_config(udp_config)
|
||||||
.set_tcp_tunnel_config(tcp_config);
|
.set_tcp_tunnel_config(tcp_config);
|
||||||
@@ -48,6 +61,8 @@ pub async fn init_tunnel(
|
|||||||
task_group.spawn(my_nat_info(
|
task_group.spawn(my_nat_info(
|
||||||
app_state.clone(),
|
app_state.clone(),
|
||||||
tunnel_dispatcher.socket_manager(),
|
tunnel_dispatcher.socket_manager(),
|
||||||
|
default_interface,
|
||||||
|
outbound_interface_name,
|
||||||
));
|
));
|
||||||
let manager = tunnel_dispatcher.socket_manager();
|
let manager = tunnel_dispatcher.socket_manager();
|
||||||
task_group.spawn(query_udp_public_addr_loop(
|
task_group.spawn(query_udp_public_addr_loop(
|
||||||
|
|||||||
@@ -47,13 +47,14 @@ pub(crate) fn create_server_tunnel(
|
|||||||
app_state: AppState,
|
app_state: AppState,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
packet_crypto: PacketCrypto,
|
packet_crypto: PacketCrypto,
|
||||||
|
default_interface: Option<rust_p2p_core::socket::LocalInterface>,
|
||||||
) -> (Vec<ServerTurnManager>, ServerOutbound, ServerRPC) {
|
) -> (Vec<ServerTurnManager>, ServerOutbound, ServerRPC) {
|
||||||
let mut rpc_notifier: HashMap<u32, RpcNotifier> = HashMap::new();
|
let mut rpc_notifier: HashMap<u32, RpcNotifier> = HashMap::new();
|
||||||
let mut sender_map: HashMap<u32, Sender<(Bytes, Instant)>> = HashMap::new();
|
let mut sender_map: HashMap<u32, Sender<(Bytes, Instant)>> = HashMap::new();
|
||||||
let mut server_manager_list = Vec::with_capacity(config.server_addr.len());
|
let mut server_manager_list = Vec::with_capacity(config.server_addr.len());
|
||||||
let mut server_addr_list = Vec::with_capacity(config.server_addr.len());
|
let mut server_addr_list = Vec::with_capacity(config.server_addr.len());
|
||||||
for (index, server_addr) in config.server_addr.iter().enumerate() {
|
for (index, server_addr) in config.server_addr.iter().enumerate() {
|
||||||
let connect_reg_config = config.to_connect_config(index);
|
let connect_reg_config = config.to_connect_config(index, default_interface.clone());
|
||||||
|
|
||||||
let server_id = index as u32;
|
let server_id = index as u32;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::protocol::control_message::{RegRequestMsg, RegistrationMode};
|
|||||||
use crate::tls::verifier::CertValidationMode;
|
use crate::tls::verifier::CertValidationMode;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
|
use rust_p2p_core::socket::LocalInterface;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::net::{Ipv4Addr, SocketAddr};
|
use std::net::{Ipv4Addr, SocketAddr};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -16,6 +17,7 @@ pub(crate) struct ConnectRegConfig {
|
|||||||
pub ip: Option<Ipv4Addr>,
|
pub ip: Option<Ipv4Addr>,
|
||||||
pub key_sign: Option<String>,
|
pub key_sign: Option<String>,
|
||||||
pub ip_variable: bool,
|
pub ip_variable: bool,
|
||||||
|
pub default_interface: Option<LocalInterface>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ConnectConfig {
|
pub(crate) struct ConnectConfig {
|
||||||
@@ -23,6 +25,7 @@ pub(crate) struct ConnectConfig {
|
|||||||
pub server_addr: SocketAddr,
|
pub server_addr: SocketAddr,
|
||||||
pub server_domain: String,
|
pub server_domain: String,
|
||||||
pub cert_mode: CertValidationMode,
|
pub cert_mode: CertValidationMode,
|
||||||
|
pub default_interface: Option<LocalInterface>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||||
pub enum ProtocolType {
|
pub enum ProtocolType {
|
||||||
@@ -110,7 +113,7 @@ impl ConnectRegConfig {
|
|||||||
let mut txt = crate::utils::dns_query::dns_query_txt(
|
let mut txt = crate::utils::dns_query::dns_query_txt(
|
||||||
&self.server_addr.address,
|
&self.server_addr.address,
|
||||||
vec![],
|
vec![],
|
||||||
&None,
|
&self.default_interface,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
txt.shuffle(&mut rand::rng());
|
txt.shuffle(&mut rand::rng());
|
||||||
@@ -121,14 +124,19 @@ impl ConnectRegConfig {
|
|||||||
}
|
}
|
||||||
v => (v, self.server_addr.address.to_string()),
|
v => (v, self.server_addr.address.to_string()),
|
||||||
};
|
};
|
||||||
let server_addr =
|
let server_addr = crate::utils::dns_query::dns_query_one(
|
||||||
crate::utils::dns_query::dns_query_one(&server_domain, &vec![], &None).await?;
|
&server_domain,
|
||||||
|
&vec![],
|
||||||
|
&self.default_interface,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let server_domain = strip_port(&server_domain).to_owned();
|
let server_domain = strip_port(&server_domain).to_owned();
|
||||||
Ok(ConnectConfig {
|
Ok(ConnectConfig {
|
||||||
protocol_type,
|
protocol_type,
|
||||||
server_addr,
|
server_addr,
|
||||||
server_domain,
|
server_domain,
|
||||||
cert_mode: self.cert_mode.clone(),
|
cert_mode: self.cert_mode.clone(),
|
||||||
|
default_interface: self.default_interface.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::tunnel_core::server::transport::config::ConnectConfig;
|
|||||||
use anyhow::{Context, bail};
|
use anyhow::{Context, bail};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
use quinn::{ClientConfig, Endpoint, RecvStream, SendStream};
|
use quinn::{ClientConfig, Endpoint, RecvStream, SendStream, TokioRuntime};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
|
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
|
||||||
|
|
||||||
@@ -56,14 +56,15 @@ pub async fn connect_quic(
|
|||||||
let server_addr = config.server_addr();
|
let server_addr = config.server_addr();
|
||||||
let server_name = config.server_name();
|
let server_name = config.server_name();
|
||||||
let quic_config = create_client_config(&config.cert_mode)?;
|
let quic_config = create_client_config(&config.cert_mode)?;
|
||||||
let mut endpoint = match Endpoint::client((std::net::Ipv6Addr::UNSPECIFIED, 0).into()) {
|
let bind_addr = if server_addr.is_ipv4() {
|
||||||
Ok(endpoint) => endpoint,
|
(std::net::Ipv4Addr::UNSPECIFIED, 0).into()
|
||||||
Err(e) => {
|
} else {
|
||||||
log::warn!("Failed to create QUIC endpoint: {}", e);
|
(std::net::Ipv6Addr::UNSPECIFIED, 0).into()
|
||||||
Endpoint::client((std::net::Ipv4Addr::UNSPECIFIED, 0).into())
|
|
||||||
.context("Failed to create QUIC endpoint")?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
let socket = crate::utils::socket::bind_udp(bind_addr, config.default_interface.as_ref())?;
|
||||||
|
let socket = socket.into_std()?;
|
||||||
|
let mut endpoint = Endpoint::new(Default::default(), None, socket, Arc::new(TokioRuntime))
|
||||||
|
.context("Failed to create QUIC endpoint")?;
|
||||||
|
|
||||||
endpoint.set_default_client_config(quic_config);
|
endpoint.set_default_client_config(quic_config);
|
||||||
let connection = endpoint
|
let connection = endpoint
|
||||||
|
|||||||
@@ -58,9 +58,10 @@ pub async fn connect_tls_tcp(
|
|||||||
let rustls_config = config.cert_mode.create_tls_client_config()?;
|
let rustls_config = config.cert_mode.create_tls_client_config()?;
|
||||||
let connector = TlsConnector::from(Arc::new(rustls_config));
|
let connector = TlsConnector::from(Arc::new(rustls_config));
|
||||||
|
|
||||||
let tcp_stream = TcpStream::connect(server_addr)
|
let tcp_stream =
|
||||||
.await
|
crate::utils::socket::connect_tcp(server_addr, config.default_interface.as_ref())
|
||||||
.context("Failed to establish underlying TCP connection")?;
|
.await
|
||||||
|
.context("Failed to establish underlying TCP connection")?;
|
||||||
if let Err(e) = tcp_stream.set_nodelay(true) {
|
if let Err(e) = tcp_stream.set_nodelay(true) {
|
||||||
log::error!("Failed to set TCP_NODELAY: {}", e);
|
log::error!("Failed to set TCP_NODELAY: {}", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,9 +71,10 @@ pub async fn connect_wss(config: &ConnectConfig) -> anyhow::Result<WssStream> {
|
|||||||
let rustls_config = config.cert_mode.create_tls_client_config()?;
|
let rustls_config = config.cert_mode.create_tls_client_config()?;
|
||||||
let connector = TlsConnector::from(Arc::new(rustls_config));
|
let connector = TlsConnector::from(Arc::new(rustls_config));
|
||||||
|
|
||||||
let tcp_stream = TcpStream::connect(server_addr)
|
let tcp_stream =
|
||||||
.await
|
crate::utils::socket::connect_tcp(server_addr, config.default_interface.as_ref())
|
||||||
.context("Failed to establish underlying TCP connection")?;
|
.await
|
||||||
|
.context("Failed to establish underlying TCP connection")?;
|
||||||
if let Err(e) = tcp_stream.set_nodelay(true) {
|
if let Err(e) = tcp_stream.set_nodelay(true) {
|
||||||
log::error!("Failed to set TCP_NODELAY: {}", e);
|
log::error!("Failed to set TCP_NODELAY: {}", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ pub async fn dns_query_one(
|
|||||||
default_interface: &Option<LocalInterface>,
|
default_interface: &Option<LocalInterface>,
|
||||||
) -> anyhow::Result<SocketAddr> {
|
) -> anyhow::Result<SocketAddr> {
|
||||||
let mut vec = dns_query_all(domain, name_servers, default_interface).await?;
|
let mut vec = dns_query_all(domain, name_servers, default_interface).await?;
|
||||||
|
if default_interface.is_some() {
|
||||||
|
// 出口网卡绑定目前应用于 IPv4 Socket;优先且只使用可绑定的 IPv4 地址。
|
||||||
|
vec.retain(SocketAddr::is_ipv4);
|
||||||
|
}
|
||||||
vec.shuffle(&mut rand::rng());
|
vec.shuffle(&mut rand::rng());
|
||||||
vec.pop().context("DNS query failed")
|
vec.pop().context("DNS query failed")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod device_id;
|
pub mod device_id;
|
||||||
pub(crate) mod dns_query;
|
pub(crate) mod dns_query;
|
||||||
|
pub(crate) mod socket;
|
||||||
pub mod task_control;
|
pub mod task_control;
|
||||||
pub(crate) mod time {
|
pub(crate) mod time {
|
||||||
pub fn now_ts_ms() -> i64 {
|
pub fn now_ts_ms() -> i64 {
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use rust_p2p_core::socket::LocalInterface;
|
||||||
|
use socket2::{Domain, Protocol, Socket, Type};
|
||||||
|
use std::io;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct ResolvedInterface {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) socket_interface: LocalInterface,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将配置中的网卡名称解析为底层 Socket 所需的接口标识。
|
||||||
|
/// Linux/Android 使用名称绑定,Windows/macOS 使用接口索引。
|
||||||
|
pub(crate) fn resolve_interface(name: Option<&str>) -> anyhow::Result<Option<ResolvedInterface>> {
|
||||||
|
let Some(name) = name.map(str::trim).filter(|name| !name.is_empty()) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let (canonical_name, index) = match getifaddrs::if_nametoindex(name) {
|
||||||
|
Ok(index) => (
|
||||||
|
getifaddrs::if_indextoname(index).unwrap_or_else(|_| name.to_owned()),
|
||||||
|
index,
|
||||||
|
),
|
||||||
|
Err(_) => {
|
||||||
|
let interface = getifaddrs::getifaddrs()
|
||||||
|
.context("读取本机网卡列表失败")?
|
||||||
|
.find(|interface| {
|
||||||
|
if interface.name.eq_ignore_ascii_case(name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
interface.description.eq_ignore_ascii_case(name)
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("找不到出口网卡 '{name}'"))?;
|
||||||
|
let index = interface
|
||||||
|
.index
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("出口网卡 '{}' 没有可用索引", interface.name))?;
|
||||||
|
(interface.name, index)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||||
|
let interface = LocalInterface::new(canonical_name.clone());
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||||
|
let interface = LocalInterface::new(index);
|
||||||
|
|
||||||
|
// Linux/Android 只用名称,但仍执行 if_nametoindex 来提前校验配置。
|
||||||
|
let _ = index;
|
||||||
|
Ok(Some(ResolvedInterface {
|
||||||
|
name: canonical_name,
|
||||||
|
socket_interface: interface,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait SocketTrait {
|
||||||
|
fn set_ip_unicast_if(&self, _interface: &LocalInterface) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
impl SocketTrait for Socket {
|
||||||
|
fn set_ip_unicast_if(&self, interface: &LocalInterface) -> io::Result<()> {
|
||||||
|
use std::os::windows::io::AsRawSocket;
|
||||||
|
use windows_sys::Win32::Networking::WinSock::{
|
||||||
|
IP_UNICAST_IF, IPPROTO_IP, SOCKET_ERROR, htonl, setsockopt,
|
||||||
|
};
|
||||||
|
|
||||||
|
let raw_socket = self.as_raw_socket();
|
||||||
|
let result = unsafe {
|
||||||
|
let best_interface = htonl(interface.index);
|
||||||
|
setsockopt(
|
||||||
|
raw_socket as usize,
|
||||||
|
IPPROTO_IP,
|
||||||
|
IP_UNICAST_IF,
|
||||||
|
&best_interface as *const _ as *const u8,
|
||||||
|
std::mem::size_of_val(&best_interface) as i32,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if result == SOCKET_ERROR {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||||
|
impl SocketTrait for Socket {
|
||||||
|
fn set_ip_unicast_if(&self, interface: &LocalInterface) -> io::Result<()> {
|
||||||
|
self.bind_device(Some(interface.name.as_bytes()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
|
impl SocketTrait for Socket {
|
||||||
|
fn set_ip_unicast_if(&self, interface: &LocalInterface) -> io::Result<()> {
|
||||||
|
self.bind_device_by_index_v4(std::num::NonZeroU32::new(interface.index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
impl SocketTrait for Socket {}
|
||||||
|
|
||||||
|
pub(crate) fn bind_socket_to_interface(
|
||||||
|
socket: &Socket,
|
||||||
|
interface: Option<&LocalInterface>,
|
||||||
|
is_ipv4: bool,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
if is_ipv4 && let Some(interface) = interface {
|
||||||
|
socket.set_ip_unicast_if(interface)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bind_udp(
|
||||||
|
addr: SocketAddr,
|
||||||
|
interface: Option<&LocalInterface>,
|
||||||
|
) -> io::Result<tokio::net::UdpSocket> {
|
||||||
|
let socket = rust_p2p_core::socket::bind_udp(addr, interface)?;
|
||||||
|
tokio::net::UdpSocket::from_std(socket.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn connect_tcp(
|
||||||
|
addr: SocketAddr,
|
||||||
|
interface: Option<&LocalInterface>,
|
||||||
|
) -> io::Result<tokio::net::TcpStream> {
|
||||||
|
let socket = Socket::new(Domain::for_address(addr), Type::STREAM, Some(Protocol::TCP))?;
|
||||||
|
bind_socket_to_interface(
|
||||||
|
&socket,
|
||||||
|
interface,
|
||||||
|
addr.is_ipv4() && !addr.ip().is_loopback(),
|
||||||
|
)?;
|
||||||
|
socket.set_nonblocking(true)?;
|
||||||
|
socket.set_tcp_nodelay(true)?;
|
||||||
|
|
||||||
|
match socket.connect(&addr.into()) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(ref error) if error.kind() == io::ErrorKind::WouldBlock => {}
|
||||||
|
#[cfg(unix)]
|
||||||
|
Err(ref error) if error.raw_os_error() == Some(libc::EINPROGRESS) => {}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = tokio::net::TcpStream::from_std(socket.into())?;
|
||||||
|
stream.writable().await?;
|
||||||
|
if let Some(error) = stream.take_error()? {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
Ok(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn connect_tcp_resolved<A: tokio::net::ToSocketAddrs>(
|
||||||
|
addr: A,
|
||||||
|
interface: Option<&LocalInterface>,
|
||||||
|
) -> io::Result<tokio::net::TcpStream> {
|
||||||
|
let addrs = tokio::net::lookup_host(addr).await?.collect::<Vec<_>>();
|
||||||
|
let mut last_error = None;
|
||||||
|
for addr in addrs {
|
||||||
|
match connect_tcp(addr, interface).await {
|
||||||
|
Ok(stream) => return Ok(stream),
|
||||||
|
Err(error) => last_error = Some(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(last_error.unwrap_or_else(|| io::Error::other("目标地址解析结果为空")))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_interface_name_disables_binding() {
|
||||||
|
assert!(resolve_interface(None).unwrap().is_none());
|
||||||
|
assert!(resolve_interface(Some(" ")).unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_interface_name_is_rejected() {
|
||||||
|
let name = "vnt-interface-that-must-not-exist";
|
||||||
|
assert!(resolve_interface(Some(name)).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn existing_interface_name_resolves() {
|
||||||
|
let interface = getifaddrs::getifaddrs()
|
||||||
|
.unwrap()
|
||||||
|
.find(|interface| interface.index.is_some())
|
||||||
|
.expect("at least one indexed interface");
|
||||||
|
let resolved = resolve_interface(Some(&interface.name)).unwrap().unwrap();
|
||||||
|
assert_eq!(resolved.name, interface.name);
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||||
|
assert_eq!(resolved.socket_interface.index, interface.index.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[test]
|
||||||
|
fn windows_friendly_name_resolves() {
|
||||||
|
let interface = getifaddrs::getifaddrs()
|
||||||
|
.unwrap()
|
||||||
|
.find(|interface| interface.index.is_some() && !interface.description.is_empty())
|
||||||
|
.expect("at least one described interface");
|
||||||
|
let resolved = resolve_interface(Some(&interface.description))
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resolved.socket_interface.index, interface.index.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,3 +18,4 @@ cli-table = "0.5.0"
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
prost-build = "0.14"
|
prost-build = "0.14"
|
||||||
|
protoc-bin-vendored = "3"
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
|
let protoc_path = protoc_bin_vendored::protoc_bin_path()
|
||||||
|
.expect("failed to find vendored protoc");
|
||||||
|
|
||||||
let mut config = prost_build::Config::new();
|
let mut config = prost_build::Config::new();
|
||||||
|
|
||||||
|
config.protoc_executable(protoc_path);
|
||||||
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"])
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class VntConfig {
|
|||||||
private final String deviceId;
|
private final String deviceId;
|
||||||
private final String deviceName;
|
private final String deviceName;
|
||||||
private final String tunName;
|
private final String tunName;
|
||||||
|
private final String outboundInterface;
|
||||||
private final String ip;
|
private final String ip;
|
||||||
private final String certMode;
|
private final String certMode;
|
||||||
private final boolean noPunch;
|
private final boolean noPunch;
|
||||||
@@ -39,6 +40,7 @@ public class VntConfig {
|
|||||||
this.deviceId = builder.deviceId;
|
this.deviceId = builder.deviceId;
|
||||||
this.deviceName = builder.deviceName;
|
this.deviceName = builder.deviceName;
|
||||||
this.tunName = builder.tunName;
|
this.tunName = builder.tunName;
|
||||||
|
this.outboundInterface = builder.outboundInterface;
|
||||||
this.ip = builder.ip;
|
this.ip = builder.ip;
|
||||||
this.certMode = builder.certMode;
|
this.certMode = builder.certMode;
|
||||||
this.noPunch = builder.noPunch;
|
this.noPunch = builder.noPunch;
|
||||||
@@ -73,6 +75,7 @@ public class VntConfig {
|
|||||||
if (deviceId != null) json.put("device_id", deviceId);
|
if (deviceId != null) json.put("device_id", deviceId);
|
||||||
if (deviceName != null) json.put("device_name", deviceName);
|
if (deviceName != null) json.put("device_name", deviceName);
|
||||||
if (tunName != null) json.put("tun_name", tunName);
|
if (tunName != null) json.put("tun_name", tunName);
|
||||||
|
if (outboundInterface != null) json.put("outbound_interface", outboundInterface);
|
||||||
if (ip != null) json.put("ip", ip);
|
if (ip != null) json.put("ip", ip);
|
||||||
if (certMode != null) json.put("cert_mode", certMode);
|
if (certMode != null) json.put("cert_mode", certMode);
|
||||||
if (mtu != null) json.put("mtu", mtu);
|
if (mtu != null) json.put("mtu", mtu);
|
||||||
@@ -124,6 +127,7 @@ public class VntConfig {
|
|||||||
private String deviceId;
|
private String deviceId;
|
||||||
private String deviceName;
|
private String deviceName;
|
||||||
private String tunName;
|
private String tunName;
|
||||||
|
private String outboundInterface;
|
||||||
private String ip;
|
private String ip;
|
||||||
private String certMode;
|
private String certMode;
|
||||||
private boolean noPunch = false;
|
private boolean noPunch = false;
|
||||||
@@ -188,6 +192,14 @@ public class VntConfig {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定对外通信Socket的出口网卡名称(可选)
|
||||||
|
*/
|
||||||
|
public Builder setOutboundInterface(String outboundInterface) {
|
||||||
|
this.outboundInterface = outboundInterface;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置固定IP(可选)
|
* 设置固定IP(可选)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -895,6 +895,8 @@ fn parse_config_from_json(json_str: &str) -> anyhow::Result<Config> {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
tun_name: Option<String>,
|
tun_name: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
outbound_interface: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
ip: Option<Ipv4Addr>,
|
ip: Option<Ipv4Addr>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
@@ -994,6 +996,7 @@ fn parse_config_from_json(json_str: &str) -> anyhow::Result<Config> {
|
|||||||
device_id,
|
device_id,
|
||||||
device_name,
|
device_name,
|
||||||
tun_name: cfg.tun_name,
|
tun_name: cfg.tun_name,
|
||||||
|
outbound_interface: cfg.outbound_interface,
|
||||||
password: cfg.password,
|
password: cfg.password,
|
||||||
cert_mode,
|
cert_mode,
|
||||||
input: cfg.input,
|
input: cfg.input,
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ pub struct StartConfig {
|
|||||||
pub device_id: Option<String>,
|
pub device_id: Option<String>,
|
||||||
pub device_name: Option<String>,
|
pub device_name: Option<String>,
|
||||||
pub tun_name: Option<String>,
|
pub tun_name: Option<String>,
|
||||||
|
pub outbound_interface: Option<String>,
|
||||||
pub ip: Option<Ipv4Addr>,
|
pub ip: Option<Ipv4Addr>,
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -1389,6 +1390,7 @@ fn convert_config(cfg: StartConfig) -> anyhow::Result<CoreConfig> {
|
|||||||
device_id,
|
device_id,
|
||||||
device_name,
|
device_name,
|
||||||
tun_name: cfg.tun_name,
|
tun_name: cfg.tun_name,
|
||||||
|
outbound_interface: cfg.outbound_interface,
|
||||||
password: cfg.password,
|
password: cfg.password,
|
||||||
cert_mode,
|
cert_mode,
|
||||||
input: cfg.input,
|
input: cfg.input,
|
||||||
@@ -1690,6 +1692,7 @@ mod tests {
|
|||||||
device_id: Some("device-a".to_string()),
|
device_id: Some("device-a".to_string()),
|
||||||
device_name: None,
|
device_name: None,
|
||||||
tun_name: None,
|
tun_name: None,
|
||||||
|
outbound_interface: None,
|
||||||
ip: None,
|
ip: None,
|
||||||
password: None,
|
password: None,
|
||||||
no_punch: false,
|
no_punch: false,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -19,8 +19,8 @@
|
|||||||
if (dark) document.documentElement.classList.add("dark");
|
if (dark) document.documentElement.classList.add("dark");
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-DIvs683K.js"></script>
|
<script type="module" crossorigin src="/assets/index-JN_O_VQt.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-F22jYCcu.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DviShRor.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const emptyFormData = () => ({
|
|||||||
device_name: "",
|
device_name: "",
|
||||||
device_id: "",
|
device_id: "",
|
||||||
tun_name: "",
|
tun_name: "",
|
||||||
|
outbound_interface: "",
|
||||||
password: "",
|
password: "",
|
||||||
cert_mode: "skip",
|
cert_mode: "skip",
|
||||||
fingerprint: "",
|
fingerprint: "",
|
||||||
@@ -95,6 +96,9 @@ export const parseTomlToForm = (toml) => {
|
|||||||
} else if (trimmed.includes("tun_name")) {
|
} else if (trimmed.includes("tun_name")) {
|
||||||
const match = trimmed.match(/tun_name\s*=\s*"([^"]*)"/);
|
const match = trimmed.match(/tun_name\s*=\s*"([^"]*)"/);
|
||||||
if (match) data.tun_name = match[1];
|
if (match) data.tun_name = match[1];
|
||||||
|
} else if (trimmed.includes("outbound_interface")) {
|
||||||
|
const match = trimmed.match(/outbound_interface\s*=\s*"([^"]*)"/);
|
||||||
|
if (match) data.outbound_interface = match[1];
|
||||||
} else if (trimmed.includes("password =")) {
|
} else if (trimmed.includes("password =")) {
|
||||||
const match = trimmed.match(/password\s*=\s*"([^"]*)"/);
|
const match = trimmed.match(/password\s*=\s*"([^"]*)"/);
|
||||||
if (match) data.password = match[1];
|
if (match) data.password = match[1];
|
||||||
@@ -231,6 +235,10 @@ export const formToToml = (formData) => {
|
|||||||
toml += "\n# 虚拟网卡名称\n";
|
toml += "\n# 虚拟网卡名称\n";
|
||||||
toml += `tun_name = "${formData.tun_name}"\n`;
|
toml += `tun_name = "${formData.tun_name}"\n`;
|
||||||
}
|
}
|
||||||
|
if (formData.outbound_interface) {
|
||||||
|
toml += "\n# 绑定对外通信 Socket 的出口网卡名称(用于服务端通信、P2P 打洞及转发流量)\n";
|
||||||
|
toml += `outbound_interface = "${formData.outbound_interface}"\n`;
|
||||||
|
}
|
||||||
|
|
||||||
toml += "\n# --- 安全配置 ---\n";
|
toml += "\n# --- 安全配置 ---\n";
|
||||||
if (formData.password) {
|
if (formData.password) {
|
||||||
@@ -331,6 +339,9 @@ server = ["quic://1.2.3.4:29872"]
|
|||||||
# 虚拟网卡名称
|
# 虚拟网卡名称
|
||||||
# tun_name = "vnt-tun"
|
# tun_name = "vnt-tun"
|
||||||
|
|
||||||
|
# 绑定对外通信 Socket 的出口网卡名称(例如 Ethernet、Wi-Fi、eth0)
|
||||||
|
# outbound_interface = "Ethernet"
|
||||||
|
|
||||||
# --- 安全配置 ---
|
# --- 安全配置 ---
|
||||||
|
|
||||||
# 加密密码 (可选)
|
# 加密密码 (可选)
|
||||||
|
|||||||
@@ -515,7 +515,7 @@ const sectionTitleClass = "text-md mb-4 flex items-center font-bold text-slate-9
|
|||||||
</svg>
|
</svg>
|
||||||
设备配置
|
设备配置
|
||||||
</h4>
|
</h4>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">设备名称</label>
|
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">设备名称</label>
|
||||||
<input v-model="formData.device_name" type="text" placeholder="默认为主机名" class="input" />
|
<input v-model="formData.device_name" type="text" placeholder="默认为主机名" class="input" />
|
||||||
@@ -528,6 +528,16 @@ const sectionTitleClass = "text-md mb-4 flex items-center font-bold text-slate-9
|
|||||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">虚拟网卡名</label>
|
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">虚拟网卡名</label>
|
||||||
<input v-model="formData.tun_name" type="text" placeholder="默认为vnt-tun" class="input" />
|
<input v-model="formData.tun_name" type="text" placeholder="默认为vnt-tun" class="input" />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">绑定出口网卡</label>
|
||||||
|
<input
|
||||||
|
v-model="formData.outbound_interface"
|
||||||
|
type="text"
|
||||||
|
placeholder="例如 Ethernet、Wi-Fi、eth0"
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
<p class="mt-1.5 text-xs leading-5 text-slate-400">服务端通信、P2P 打洞及转发流量将使用此网卡</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user