feat: add outbound interface binding
This commit is contained in:
Generated
+3
@@ -6091,6 +6091,7 @@ dependencies = [
|
||||
"getifaddrs",
|
||||
"hex",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"log",
|
||||
"lz4_flex",
|
||||
"machine-uid",
|
||||
@@ -6121,6 +6122,7 @@ dependencies = [
|
||||
"uuid",
|
||||
"widestring",
|
||||
"winapi",
|
||||
"windows-sys 0.60.2",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
@@ -6154,6 +6156,7 @@ dependencies = [
|
||||
"log",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"protoc-bin-vendored",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
|
||||
@@ -31,6 +31,7 @@ pub struct FileConfig {
|
||||
pub device_id: Option<String>,
|
||||
pub device_name: Option<String>,
|
||||
pub tun_name: Option<String>,
|
||||
pub outbound_interface: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub cert_mode: Option<String>,
|
||||
pub udp_stun: Option<Vec<String>>,
|
||||
@@ -127,6 +128,9 @@ pub struct Args {
|
||||
/// 虚拟网卡名称
|
||||
#[clap(long)]
|
||||
pub tun_name: Option<String>,
|
||||
/// 绑定对外通信 Socket 的出口网卡名称
|
||||
#[clap(long)]
|
||||
pub outbound_interface: Option<String>,
|
||||
/// 关闭内置子网NAT
|
||||
#[clap(long)]
|
||||
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())
|
||||
.unwrap_or_else(default_hostname),
|
||||
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()),
|
||||
cert_mode,
|
||||
input,
|
||||
@@ -285,6 +292,7 @@ fn build_from_args_only(args: Args) -> anyhow::Result<(Config, CtrlConfig)> {
|
||||
device_id,
|
||||
device_name: args.device_name.unwrap_or_else(default_hostname),
|
||||
tun_name: args.tun_name,
|
||||
outbound_interface: args.outbound_interface,
|
||||
password: args.password,
|
||||
cert_mode: args
|
||||
.cert_mode
|
||||
@@ -345,6 +353,7 @@ fn build_from_file_only(file: FileConfig) -> anyhow::Result<(Config, CtrlConfig)
|
||||
device_id,
|
||||
device_name: file.device_name.clone().unwrap_or_else(default_hostname),
|
||||
tun_name: file.tun_name.clone(),
|
||||
outbound_interface: file.outbound_interface.clone(),
|
||||
password: file.password.clone(),
|
||||
cert_mode,
|
||||
output: file.output.unwrap_or_default(),
|
||||
@@ -448,6 +457,9 @@ server = ["quic://1.2.3.4:29872"]
|
||||
# 虚拟网卡名称
|
||||
# tun_name = "vnt-tun"
|
||||
|
||||
# 绑定对外通信 Socket 的出口网卡名称(用于服务端通信、P2P 打洞及转发流量)
|
||||
# outbound_interface = "Ethernet"
|
||||
|
||||
# --- 安全配置 ---
|
||||
|
||||
# 加密密码 (可选)
|
||||
@@ -493,9 +505,12 @@ mod tests {
|
||||
"test-net",
|
||||
"--tunnel-port",
|
||||
"12345",
|
||||
"--outbound-interface",
|
||||
"Ethernet",
|
||||
])
|
||||
.unwrap();
|
||||
let (config, _) = build_from_args_only(args).unwrap();
|
||||
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]
|
||||
winapi = { version = "0.3.9", features = ["winreg"] }
|
||||
widestring = "1.2"
|
||||
windows-sys = { version = "0.60.2", features = ["Win32_Networking_WinSock"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.14"
|
||||
|
||||
@@ -22,6 +22,8 @@ pub struct Config {
|
||||
pub device_id: String,
|
||||
pub device_name: String,
|
||||
pub tun_name: Option<String>,
|
||||
/// 绑定 VNT 对外通信 Socket 的物理网卡名称。
|
||||
pub outbound_interface: Option<String>,
|
||||
pub ip: Option<Ipv4Addr>,
|
||||
pub password: Option<String>,
|
||||
pub no_punch: bool,
|
||||
@@ -87,7 +89,11 @@ impl Config {
|
||||
pub fn key_sign(&self) -> Option<String> {
|
||||
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 {
|
||||
server_addr: self.server_addr[index].clone(),
|
||||
cert_mode: self.cert_mode.clone(),
|
||||
@@ -97,6 +103,7 @@ impl Config {
|
||||
ip: self.ip,
|
||||
key_sign: self.key_sign(),
|
||||
ip_variable: self.ip.is_none(),
|
||||
default_interface,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +60,32 @@ impl NetworkManager {
|
||||
) -> anyhow::Result<NetworkManager> {
|
||||
let app_state = AppState::default();
|
||||
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 packet_crypto = PacketCrypto::new_from_str(config.password.as_deref());
|
||||
let packet_compression = PacketCompression::new(config.compress);
|
||||
let (server_manager_list, tunnel_to_server, server_rpc) =
|
||||
create_server_tunnel(app_state.clone(), &config, packet_crypto.clone());
|
||||
let (server_manager_list, tunnel_to_server, server_rpc) = create_server_tunnel(
|
||||
app_state.clone(),
|
||||
&config,
|
||||
packet_crypto.clone(),
|
||||
default_interface.clone(),
|
||||
);
|
||||
let device_io_manager = DeviceIOManager::new(task_group.clone());
|
||||
let allow_subnet = AllowSubnetExternalRoute::new(config.output.clone());
|
||||
|
||||
@@ -75,6 +96,8 @@ impl NetworkManager {
|
||||
tunnel_to_server.clone(),
|
||||
packet_crypto.clone(),
|
||||
config.tunnel_port,
|
||||
default_interface.clone(),
|
||||
canonical_interface_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -116,6 +139,7 @@ impl NetworkManager {
|
||||
config.no_tun,
|
||||
config.allow_port_mapping,
|
||||
app_state.network.clone(),
|
||||
default_interface.clone(),
|
||||
);
|
||||
let internal_nat_inbound = if config.no_nat && !config.no_tun {
|
||||
None
|
||||
@@ -127,6 +151,7 @@ impl NetworkManager {
|
||||
allow_subnet.clone(),
|
||||
app_state.network.clone(),
|
||||
config.no_tun,
|
||||
default_interface.clone(),
|
||||
)
|
||||
.await?;
|
||||
Some(nat_inbound)
|
||||
|
||||
@@ -5,6 +5,7 @@ use pnet_packet::Packet;
|
||||
use pnet_packet::icmp::echo_reply::{Identifier, SequenceNumber};
|
||||
use pnet_packet::icmp::{IcmpPacket, IcmpTypes};
|
||||
use pnet_packet::ipv4::Ipv4Packet;
|
||||
use rust_p2p_core::socket::LocalInterface;
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -24,6 +25,7 @@ pub async fn start_icmp_nat(
|
||||
ip_stack: &IpStack,
|
||||
no_tun: bool,
|
||||
network: SharedNetworkAddr,
|
||||
default_interface: Option<LocalInterface>,
|
||||
) -> anyhow::Result<()> {
|
||||
let net_icmp_socket = socket2::Socket::new(
|
||||
socket2::Domain::IPV4,
|
||||
@@ -31,6 +33,12 @@ pub async fn start_icmp_nat(
|
||||
Some(socket2::Protocol::ICMPV4),
|
||||
)
|
||||
.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);
|
||||
net_icmp_socket
|
||||
.bind(&socket2::SockAddr::from(addr))
|
||||
|
||||
@@ -8,6 +8,7 @@ use anyhow::Context;
|
||||
use bytes::BytesMut;
|
||||
use pnet_packet::ip::IpNextHeaderProtocol;
|
||||
use pnet_packet::ipv4::Ipv4Packet;
|
||||
use rust_p2p_core::socket::LocalInterface;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -24,6 +25,7 @@ pub(crate) struct InternalNatInbound {
|
||||
ip_stack_send: Arc<IpStackSend>,
|
||||
allow_subnet: AllowSubnetExternalRoute,
|
||||
network: SharedNetworkAddr,
|
||||
default_interface: Option<LocalInterface>,
|
||||
}
|
||||
impl InternalNatInbound {
|
||||
pub async fn create(
|
||||
@@ -33,13 +35,28 @@ impl InternalNatInbound {
|
||||
allow_subnet: AllowSubnetExternalRoute,
|
||||
network: SharedNetworkAddr,
|
||||
no_tun: bool,
|
||||
default_interface: Option<LocalInterface>,
|
||||
) -> anyhow::Result<Self> {
|
||||
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)?;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
icmp_nat::start_icmp_nat(task_group, &ip_stack, no_tun, network.clone()).await?;
|
||||
tcp_nat::start_tcp_nat(task_group, &ip_stack, no_tun, network.clone()).await?;
|
||||
udp_nat::start_udp_nat(task_group, &ip_stack).await?;
|
||||
icmp_nat::start_icmp_nat(
|
||||
task_group,
|
||||
&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 {
|
||||
if let Err(e) = ip_stack_recv_task(ip_stack_recv, hybrid_outbound).await {
|
||||
log::error!("ip stack recv task error: {e:?}");
|
||||
@@ -50,6 +67,7 @@ impl InternalNatInbound {
|
||||
ip_stack_send: Arc::new(ip_stack_send),
|
||||
allow_subnet,
|
||||
network,
|
||||
default_interface,
|
||||
})
|
||||
}
|
||||
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);
|
||||
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,
|
||||
allow_port_mapping: bool,
|
||||
network: SharedNetworkAddr,
|
||||
default_interface: Option<LocalInterface>,
|
||||
}
|
||||
|
||||
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 {
|
||||
no_tun,
|
||||
allow_port_mapping,
|
||||
network,
|
||||
default_interface,
|
||||
}
|
||||
}
|
||||
pub async fn tcp_mapping<R, W>(
|
||||
@@ -181,13 +212,25 @@ impl PortMappingManager {
|
||||
|
||||
if dest_ip == net.ip {
|
||||
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) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
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>(
|
||||
&self,
|
||||
@@ -211,12 +254,24 @@ impl PortMappingManager {
|
||||
|
||||
if dest_ip == net.ip {
|
||||
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) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
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::utils::task_control::TaskGroup;
|
||||
use anyhow::Context;
|
||||
use rust_p2p_core::socket::LocalInterface;
|
||||
use std::fmt::Debug;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use tcp_ip::IpStack;
|
||||
use tcp_ip::tcp::TcpListener;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||
use tokio::net::ToSocketAddrs;
|
||||
|
||||
pub async fn start_tcp_nat(
|
||||
task_group: &TaskGroup,
|
||||
ip_stack: &IpStack,
|
||||
no_tun: bool,
|
||||
network: SharedNetworkAddr,
|
||||
default_interface: Option<LocalInterface>,
|
||||
) -> anyhow::Result<()> {
|
||||
let tcp_listener = TcpListener::bind_all(ip_stack.clone()).await?;
|
||||
let group = task_group.clone();
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -29,6 +32,7 @@ async fn listen_task(
|
||||
mut tcp_listener: TcpListener,
|
||||
no_tun: bool,
|
||||
network: SharedNetworkAddr,
|
||||
default_interface: Option<LocalInterface>,
|
||||
) -> anyhow::Result<()> {
|
||||
loop {
|
||||
// 单次 accept/地址查询失败不能拖垮整个监听任务:
|
||||
@@ -60,8 +64,9 @@ async fn listen_task(
|
||||
local_addr.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST));
|
||||
}
|
||||
}
|
||||
let default_interface = default_interface.clone();
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -71,8 +76,9 @@ async fn listen_task(
|
||||
async fn stream_task(
|
||||
mut inner_stream: tcp_ip::tcp::TcpStream,
|
||||
addr: SocketAddr,
|
||||
default_interface: Option<&LocalInterface>,
|
||||
) -> 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?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -81,14 +87,15 @@ pub(crate) async fn stream_nat<R, W, A: ToSocketAddrs + Debug>(
|
||||
recv_stream: R,
|
||||
send_stream: W,
|
||||
addr: A,
|
||||
default_interface: Option<&LocalInterface>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
R: AsyncRead + 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
|
||||
.with_context(|| format!("error connecting to {:?}", addr))?;
|
||||
.context("error connecting to NAT destination")?;
|
||||
crate::port_mapping::tcp_port_mapping::copy_bidirectional_split(
|
||||
&mut tokio_stream,
|
||||
recv_stream,
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::utils::task_control::{SubTask, TaskGroup};
|
||||
use anyhow::Context;
|
||||
use bytes::Bytes;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use rust_p2p_core::socket::LocalInterface;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
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_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 = Arc::new(inner_socket);
|
||||
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) =
|
||||
handle_outbound(&group, &inner_socket, &nat_table, src, dst, &buf[..len]).await
|
||||
if let Err(e) = handle_outbound(
|
||||
&group,
|
||||
&inner_socket,
|
||||
&nat_table,
|
||||
src,
|
||||
dst,
|
||||
&buf[..len],
|
||||
default_interface.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::warn!("udp nat outbound error: {e:?}");
|
||||
}
|
||||
@@ -60,6 +73,7 @@ async fn handle_outbound(
|
||||
src: SocketAddr,
|
||||
dst: SocketAddr,
|
||||
packet: &[u8],
|
||||
default_interface: Option<&LocalInterface>,
|
||||
) -> anyhow::Result<()> {
|
||||
let key = (src, dst);
|
||||
|
||||
@@ -70,7 +84,17 @@ async fn handle_outbound(
|
||||
entry.socket.clone()
|
||||
} else {
|
||||
// 创建真实 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?;
|
||||
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,
|
||||
addr: A,
|
||||
default_interface: Option<&LocalInterface>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
let udp_socket = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
|
||||
udp_socket
|
||||
.connect(&addr)
|
||||
.await
|
||||
.with_context(|| format!("error connecting to {:?}", addr))?;
|
||||
let destination = tokio::net::lookup_host(addr)
|
||||
.await?
|
||||
.next()
|
||||
.context("UDP NAT destination resolved to no address")?;
|
||||
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_write = FramedWrite::new(send_stream, LengthDelimitedCodec::new());
|
||||
let mut buf = vec![0u8; 65536];
|
||||
@@ -219,7 +256,6 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::context::AppState;
|
||||
use rust_p2p_core::nat::{NatInfo, NatType};
|
||||
use rust_p2p_core::socket::LocalInterface;
|
||||
use rust_p2p_core::tunnel::SocketManager;
|
||||
use rust_p2p_core::tunnel::udp::Model;
|
||||
use std::collections::HashMap;
|
||||
@@ -9,19 +10,38 @@ use std::time::Duration;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 mut local_ipv4s = Vec::new();
|
||||
let mut local_ipv6 = Vec::new();
|
||||
match getifaddrs::getifaddrs() {
|
||||
Ok(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 {
|
||||
continue;
|
||||
};
|
||||
@@ -67,20 +87,28 @@ async fn my_nat_info_impl(app_context: &AppState, socket_manager: &SocketManager
|
||||
}
|
||||
}
|
||||
log::info!("local_ipv4s: {:?}", local_ipv4s);
|
||||
let detected = rust_p2p_core::extend::addr::local_ipv4()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::warn!("local ipv4 failed {e:?}");
|
||||
e
|
||||
})
|
||||
.ok();
|
||||
let detected = if outbound_interface_name.is_none() {
|
||||
rust_p2p_core::extend::addr::local_ipv4()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::warn!("local ipv4 failed {e:?}");
|
||||
e
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let Some((local_ipv4, merged)) = select_local_ipv4(detected, &local_ipv4s) else {
|
||||
log::warn!("未发现可用本机 IPv4 地址,跳过本次 NAT 信息更新");
|
||||
return;
|
||||
};
|
||||
// 保留网卡扫描结果,主地址排在首位
|
||||
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 addr.is_loopback()
|
||||
|| 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() {
|
||||
stun_server = default_udp_stun();
|
||||
}
|
||||
let (nat_type, public_ips, port_range) = rust_p2p_core::stun::stun_test_nat(stun_server, None)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("stun_test_nat {e:?}");
|
||||
(NatType::Cone, vec![], 0)
|
||||
});
|
||||
let (nat_type, public_ips, port_range) =
|
||||
rust_p2p_core::stun::stun_test_nat(stun_server, default_interface)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("stun_test_nat {e:?}");
|
||||
(NatType::Cone, vec![], 0)
|
||||
});
|
||||
log::info!("nat_type:{nat_type:?},public_ips:{public_ips:?},port_range={port_range}");
|
||||
nat_info.nat_type = nat_type;
|
||||
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::utils::task_control::TaskGroup;
|
||||
use rust_p2p_core::punch::Puncher;
|
||||
use rust_p2p_core::socket::LocalInterface;
|
||||
use rust_p2p_core::tunnel::{Tunnel, TunnelDispatcher, new_tunnel_component};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
@@ -24,17 +25,29 @@ pub async fn init_tunnel(
|
||||
tunnel_to_server: ServerOutbound,
|
||||
packet_crypto: PacketCrypto,
|
||||
tunnel_port: Option<u16>,
|
||||
default_interface: Option<LocalInterface>,
|
||||
outbound_interface_name: Option<String>,
|
||||
) -> anyhow::Result<(Puncher, P2pOutbound, P2pTask)> {
|
||||
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_sub_udp_count(82)
|
||||
.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,
|
||||
))
|
||||
.set_tcp_multiplexing_limit(2)
|
||||
.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()
|
||||
.set_udp_tunnel_config(udp_config)
|
||||
.set_tcp_tunnel_config(tcp_config);
|
||||
@@ -48,6 +61,8 @@ pub async fn init_tunnel(
|
||||
task_group.spawn(my_nat_info(
|
||||
app_state.clone(),
|
||||
tunnel_dispatcher.socket_manager(),
|
||||
default_interface,
|
||||
outbound_interface_name,
|
||||
));
|
||||
let manager = tunnel_dispatcher.socket_manager();
|
||||
task_group.spawn(query_udp_public_addr_loop(
|
||||
|
||||
@@ -47,13 +47,14 @@ pub(crate) fn create_server_tunnel(
|
||||
app_state: AppState,
|
||||
config: &Config,
|
||||
packet_crypto: PacketCrypto,
|
||||
default_interface: Option<rust_p2p_core::socket::LocalInterface>,
|
||||
) -> (Vec<ServerTurnManager>, ServerOutbound, ServerRPC) {
|
||||
let mut rpc_notifier: HashMap<u32, RpcNotifier> = 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_addr_list = Vec::with_capacity(config.server_addr.len());
|
||||
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;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::protocol::control_message::{RegRequestMsg, RegistrationMode};
|
||||
use crate::tls::verifier::CertValidationMode;
|
||||
use anyhow::Context;
|
||||
use rand::seq::SliceRandom;
|
||||
use rust_p2p_core::socket::LocalInterface;
|
||||
use std::fmt;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
@@ -16,6 +17,7 @@ pub(crate) struct ConnectRegConfig {
|
||||
pub ip: Option<Ipv4Addr>,
|
||||
pub key_sign: Option<String>,
|
||||
pub ip_variable: bool,
|
||||
pub default_interface: Option<LocalInterface>,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ConnectConfig {
|
||||
@@ -23,6 +25,7 @@ pub(crate) struct ConnectConfig {
|
||||
pub server_addr: SocketAddr,
|
||||
pub server_domain: String,
|
||||
pub cert_mode: CertValidationMode,
|
||||
pub default_interface: Option<LocalInterface>,
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub enum ProtocolType {
|
||||
@@ -110,7 +113,7 @@ impl ConnectRegConfig {
|
||||
let mut txt = crate::utils::dns_query::dns_query_txt(
|
||||
&self.server_addr.address,
|
||||
vec![],
|
||||
&None,
|
||||
&self.default_interface,
|
||||
)
|
||||
.await?;
|
||||
txt.shuffle(&mut rand::rng());
|
||||
@@ -121,14 +124,19 @@ impl ConnectRegConfig {
|
||||
}
|
||||
v => (v, self.server_addr.address.to_string()),
|
||||
};
|
||||
let server_addr =
|
||||
crate::utils::dns_query::dns_query_one(&server_domain, &vec![], &None).await?;
|
||||
let server_addr = crate::utils::dns_query::dns_query_one(
|
||||
&server_domain,
|
||||
&vec![],
|
||||
&self.default_interface,
|
||||
)
|
||||
.await?;
|
||||
let server_domain = strip_port(&server_domain).to_owned();
|
||||
Ok(ConnectConfig {
|
||||
protocol_type,
|
||||
server_addr,
|
||||
server_domain,
|
||||
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 bytes::Bytes;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use quinn::{ClientConfig, Endpoint, RecvStream, SendStream};
|
||||
use quinn::{ClientConfig, Endpoint, RecvStream, SendStream, TokioRuntime};
|
||||
use std::sync::Arc;
|
||||
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
|
||||
|
||||
@@ -56,14 +56,15 @@ pub async fn connect_quic(
|
||||
let server_addr = config.server_addr();
|
||||
let server_name = config.server_name();
|
||||
let quic_config = create_client_config(&config.cert_mode)?;
|
||||
let mut endpoint = match Endpoint::client((std::net::Ipv6Addr::UNSPECIFIED, 0).into()) {
|
||||
Ok(endpoint) => endpoint,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to create QUIC endpoint: {}", e);
|
||||
Endpoint::client((std::net::Ipv4Addr::UNSPECIFIED, 0).into())
|
||||
.context("Failed to create QUIC endpoint")?
|
||||
}
|
||||
let bind_addr = if server_addr.is_ipv4() {
|
||||
(std::net::Ipv4Addr::UNSPECIFIED, 0).into()
|
||||
} else {
|
||||
(std::net::Ipv6Addr::UNSPECIFIED, 0).into()
|
||||
};
|
||||
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);
|
||||
let connection = endpoint
|
||||
|
||||
@@ -58,9 +58,10 @@ pub async fn connect_tls_tcp(
|
||||
let rustls_config = config.cert_mode.create_tls_client_config()?;
|
||||
let connector = TlsConnector::from(Arc::new(rustls_config));
|
||||
|
||||
let tcp_stream = TcpStream::connect(server_addr)
|
||||
.await
|
||||
.context("Failed to establish underlying TCP connection")?;
|
||||
let tcp_stream =
|
||||
crate::utils::socket::connect_tcp(server_addr, config.default_interface.as_ref())
|
||||
.await
|
||||
.context("Failed to establish underlying TCP connection")?;
|
||||
if let Err(e) = tcp_stream.set_nodelay(true) {
|
||||
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 connector = TlsConnector::from(Arc::new(rustls_config));
|
||||
|
||||
let tcp_stream = TcpStream::connect(server_addr)
|
||||
.await
|
||||
.context("Failed to establish underlying TCP connection")?;
|
||||
let tcp_stream =
|
||||
crate::utils::socket::connect_tcp(server_addr, config.default_interface.as_ref())
|
||||
.await
|
||||
.context("Failed to establish underlying TCP connection")?;
|
||||
if let Err(e) = tcp_stream.set_nodelay(true) {
|
||||
log::error!("Failed to set TCP_NODELAY: {}", e);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,10 @@ pub async fn dns_query_one(
|
||||
default_interface: &Option<LocalInterface>,
|
||||
) -> anyhow::Result<SocketAddr> {
|
||||
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.pop().context("DNS query failed")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod device_id;
|
||||
pub(crate) mod dns_query;
|
||||
pub(crate) mod socket;
|
||||
pub mod task_control;
|
||||
pub(crate) mod time {
|
||||
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]
|
||||
prost-build = "0.14"
|
||||
protoc-bin-vendored = "3"
|
||||
@@ -1,5 +1,10 @@
|
||||
fn main() {
|
||||
let protoc_path = protoc_bin_vendored::protoc_bin_path()
|
||||
.expect("failed to find vendored protoc");
|
||||
|
||||
let mut config = prost_build::Config::new();
|
||||
|
||||
config.protoc_executable(protoc_path);
|
||||
config.protoc_arg("--experimental_allow_proto3_optional");
|
||||
config
|
||||
.compile_protos(&["proto/local_ipc.proto"], &["proto"])
|
||||
|
||||
@@ -18,6 +18,7 @@ public class VntConfig {
|
||||
private final String deviceId;
|
||||
private final String deviceName;
|
||||
private final String tunName;
|
||||
private final String outboundInterface;
|
||||
private final String ip;
|
||||
private final String certMode;
|
||||
private final boolean noPunch;
|
||||
@@ -39,6 +40,7 @@ public class VntConfig {
|
||||
this.deviceId = builder.deviceId;
|
||||
this.deviceName = builder.deviceName;
|
||||
this.tunName = builder.tunName;
|
||||
this.outboundInterface = builder.outboundInterface;
|
||||
this.ip = builder.ip;
|
||||
this.certMode = builder.certMode;
|
||||
this.noPunch = builder.noPunch;
|
||||
@@ -73,6 +75,7 @@ public class VntConfig {
|
||||
if (deviceId != null) json.put("device_id", deviceId);
|
||||
if (deviceName != null) json.put("device_name", deviceName);
|
||||
if (tunName != null) json.put("tun_name", tunName);
|
||||
if (outboundInterface != null) json.put("outbound_interface", outboundInterface);
|
||||
if (ip != null) json.put("ip", ip);
|
||||
if (certMode != null) json.put("cert_mode", certMode);
|
||||
if (mtu != null) json.put("mtu", mtu);
|
||||
@@ -124,6 +127,7 @@ public class VntConfig {
|
||||
private String deviceId;
|
||||
private String deviceName;
|
||||
private String tunName;
|
||||
private String outboundInterface;
|
||||
private String ip;
|
||||
private String certMode;
|
||||
private boolean noPunch = false;
|
||||
@@ -188,6 +192,14 @@ public class VntConfig {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定对外通信Socket的出口网卡名称(可选)
|
||||
*/
|
||||
public Builder setOutboundInterface(String outboundInterface) {
|
||||
this.outboundInterface = outboundInterface;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置固定IP(可选)
|
||||
*/
|
||||
|
||||
@@ -895,6 +895,8 @@ fn parse_config_from_json(json_str: &str) -> anyhow::Result<Config> {
|
||||
#[serde(default)]
|
||||
tun_name: Option<String>,
|
||||
#[serde(default)]
|
||||
outbound_interface: Option<String>,
|
||||
#[serde(default)]
|
||||
ip: Option<Ipv4Addr>,
|
||||
#[serde(default)]
|
||||
password: Option<String>,
|
||||
@@ -994,6 +996,7 @@ fn parse_config_from_json(json_str: &str) -> anyhow::Result<Config> {
|
||||
device_id,
|
||||
device_name,
|
||||
tun_name: cfg.tun_name,
|
||||
outbound_interface: cfg.outbound_interface,
|
||||
password: cfg.password,
|
||||
cert_mode,
|
||||
input: cfg.input,
|
||||
|
||||
@@ -254,6 +254,7 @@ pub struct StartConfig {
|
||||
pub device_id: Option<String>,
|
||||
pub device_name: Option<String>,
|
||||
pub tun_name: Option<String>,
|
||||
pub outbound_interface: Option<String>,
|
||||
pub ip: Option<Ipv4Addr>,
|
||||
pub password: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -1389,6 +1390,7 @@ fn convert_config(cfg: StartConfig) -> anyhow::Result<CoreConfig> {
|
||||
device_id,
|
||||
device_name,
|
||||
tun_name: cfg.tun_name,
|
||||
outbound_interface: cfg.outbound_interface,
|
||||
password: cfg.password,
|
||||
cert_mode,
|
||||
input: cfg.input,
|
||||
@@ -1690,6 +1692,7 @@ mod tests {
|
||||
device_id: Some("device-a".to_string()),
|
||||
device_name: None,
|
||||
tun_name: None,
|
||||
outbound_interface: None,
|
||||
ip: None,
|
||||
password: None,
|
||||
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");
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-DIvs683K.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-F22jYCcu.css">
|
||||
<script type="module" crossorigin src="/assets/index-JN_O_VQt.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DviShRor.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -19,6 +19,7 @@ export const emptyFormData = () => ({
|
||||
device_name: "",
|
||||
device_id: "",
|
||||
tun_name: "",
|
||||
outbound_interface: "",
|
||||
password: "",
|
||||
cert_mode: "skip",
|
||||
fingerprint: "",
|
||||
@@ -95,6 +96,9 @@ export const parseTomlToForm = (toml) => {
|
||||
} else if (trimmed.includes("tun_name")) {
|
||||
const match = trimmed.match(/tun_name\s*=\s*"([^"]*)"/);
|
||||
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 =")) {
|
||||
const match = trimmed.match(/password\s*=\s*"([^"]*)"/);
|
||||
if (match) data.password = match[1];
|
||||
@@ -231,6 +235,10 @@ export const formToToml = (formData) => {
|
||||
toml += "\n# 虚拟网卡名称\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";
|
||||
if (formData.password) {
|
||||
@@ -331,6 +339,9 @@ server = ["quic://1.2.3.4:29872"]
|
||||
# 虚拟网卡名称
|
||||
# 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>
|
||||
设备配置
|
||||
</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>
|
||||
<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" />
|
||||
@@ -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>
|
||||
<input v-model="formData.tun_name" type="text" placeholder="默认为vnt-tun" class="input" />
|
||||
</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user