feat: add TAP device mode and Ethernet interoperability
This commit is contained in:
@@ -64,12 +64,22 @@ pnpm dev:desktop
|
||||
2. 提升流量稳定性,支持使用quic代理流量,支持FEC冗余传输
|
||||
3. 简化操作,去除了大量vnt1.0的重复和无用的配置参数
|
||||
4. vnt-link、vnt合二为一
|
||||
5. 支持有tun模式、无tun模式、端口映射
|
||||
5. 支持无网卡、TUN(三层)和 TAP(二层)模式及端口映射;三种模式的 IPv4 流量可互通
|
||||
6. 全功能的情况下,减少程序体积
|
||||
7. 性能提升,支持linux-offload
|
||||
8. 更规范的api接入,可以轻松自定义客户端
|
||||
9. 支持同时连接多个服务端,可以容灾和负载均衡
|
||||
|
||||
## 虚拟网卡模式
|
||||
|
||||
配置文件使用 `device_mode = "no|tun|tap"`,默认值为 `tun`;命令行可用 `--device-mode` 覆盖。旧的 `no_tun` 配置已移除,程序会提示迁移而不会静默按 TUN 启动。
|
||||
|
||||
- `no`:不创建虚拟网卡,只提供流量出口和端口映射。
|
||||
- `tun`:创建三层网卡,网卡收发 IPv4 包。
|
||||
- `tap`:创建二层网卡,完整透传 Ethernet 帧,并与 TUN/NO 节点转换 IPv4、兼容 ARP。
|
||||
|
||||
Linux 和 macOS 使用系统提供的 TUN/TAP 能力。Windows 的 TUN 模式使用随程序提供的 `wintun.dll`;TAP 模式需要管理员权限并预先安装 `tap-windows`(硬件 ID `tap0901`)。Android VpnService 仅支持 TUN。
|
||||
|
||||
# 说明
|
||||
|
||||
vnt2.0整体重构了一遍,和1.0不兼容,同时也可能引入新的bug,欢迎反馈
|
||||
|
||||
+70
-9
@@ -4,7 +4,7 @@ use ipnet::Ipv4Net;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use vnt_core::context::config::Config;
|
||||
use vnt_core::context::config::{Config, DeviceMode};
|
||||
use vnt_core::nat::NetInput;
|
||||
use vnt_core::tls::verifier::CertValidationMode;
|
||||
use vnt_core::tunnel_core::server::transport::config::ProtocolAddress;
|
||||
@@ -23,7 +23,9 @@ pub struct FileConfig {
|
||||
pub input: Option<Vec<NetInput>>,
|
||||
pub output: Option<Vec<Ipv4Net>>,
|
||||
pub no_nat: Option<bool>,
|
||||
pub no_tun: Option<bool>,
|
||||
pub device_mode: Option<DeviceMode>,
|
||||
#[serde(rename = "no_tun", skip_serializing)]
|
||||
pub legacy_no_tun: Option<bool>,
|
||||
pub mtu: Option<u16>,
|
||||
pub ctrl_port: Option<u16>,
|
||||
pub port_mapping: Option<Vec<String>>,
|
||||
@@ -134,9 +136,9 @@ pub struct Args {
|
||||
/// 关闭内置子网NAT
|
||||
#[clap(long)]
|
||||
pub no_nat: bool,
|
||||
/// 禁用tun,禁用后只能充当流量出口或者进行端口映射,无需管理员权限
|
||||
/// 虚拟网卡模式:no(无网卡)、tun(三层网卡)、tap(二层网卡)
|
||||
#[clap(long)]
|
||||
pub no_tun: bool,
|
||||
pub device_mode: Option<DeviceMode>,
|
||||
/// 端口映射,格式为:协议://本地监听地址-目标虚拟IP-目标映射地址
|
||||
#[clap(long)]
|
||||
pub port_mapping: Vec<PortMapping>,
|
||||
@@ -177,6 +179,14 @@ pub fn build_config_from_args_and_file(
|
||||
args: Option<Args>,
|
||||
file: Option<FileConfig>,
|
||||
) -> anyhow::Result<(Config, CtrlConfig)> {
|
||||
if file
|
||||
.as_ref()
|
||||
.is_some_and(|config| config.legacy_no_tun.is_some())
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"configuration key 'no_tun' was removed; use device_mode = \"no|tun|tap\""
|
||||
));
|
||||
}
|
||||
match (args, file) {
|
||||
(Some(args), Some(file)) => build_from_args_and_file(args, file),
|
||||
(Some(args), None) => build_from_args_only(args),
|
||||
@@ -258,7 +268,7 @@ fn build_from_args_and_file(args: Args, file: FileConfig) -> anyhow::Result<(Con
|
||||
input,
|
||||
output,
|
||||
no_nat: args.no_nat || file.no_nat.unwrap_or(false),
|
||||
no_tun: args.no_tun || file.no_tun.unwrap_or(false),
|
||||
device_mode: args.device_mode.or(file.device_mode).unwrap_or_default(),
|
||||
mtu: args.mtu.or(file.mtu),
|
||||
port_mapping,
|
||||
allow_port_mapping: args.allow_mapping || file.allow_mapping.unwrap_or(false),
|
||||
@@ -299,7 +309,7 @@ fn build_from_args_only(args: Args) -> anyhow::Result<(Config, CtrlConfig)> {
|
||||
.unwrap_or(CertValidationMode::InsecureSkipVerification),
|
||||
output: args.output,
|
||||
no_nat: args.no_nat,
|
||||
no_tun: args.no_tun,
|
||||
device_mode: args.device_mode.unwrap_or_default(),
|
||||
mtu: args.mtu,
|
||||
port_mapping: args.port_mapping,
|
||||
allow_port_mapping: args.allow_mapping,
|
||||
@@ -313,6 +323,11 @@ fn build_from_args_only(args: Args) -> anyhow::Result<(Config, CtrlConfig)> {
|
||||
}
|
||||
|
||||
fn build_from_file_only(file: FileConfig) -> anyhow::Result<(Config, CtrlConfig)> {
|
||||
if file.legacy_no_tun.is_some() {
|
||||
return Err(anyhow!(
|
||||
"configuration key 'no_tun' was removed; use device_mode = \"no|tun|tap\""
|
||||
));
|
||||
}
|
||||
let server_addr = file.to_server_addr()?;
|
||||
let port_mapping = file.to_port_mapping()?;
|
||||
|
||||
@@ -358,7 +373,7 @@ fn build_from_file_only(file: FileConfig) -> anyhow::Result<(Config, CtrlConfig)
|
||||
cert_mode,
|
||||
output: file.output.unwrap_or_default(),
|
||||
no_nat: file.no_nat.unwrap_or(false),
|
||||
no_tun: file.no_tun.unwrap_or(false),
|
||||
device_mode: file.device_mode.unwrap_or_default(),
|
||||
mtu: file.mtu,
|
||||
port_mapping,
|
||||
allow_port_mapping: file.allow_mapping.unwrap_or(false),
|
||||
@@ -422,8 +437,8 @@ server = ["quic://1.2.3.4:29872"]
|
||||
# 是否关闭内置子网NAT,关闭(设为true)后需要配置网卡转发,否则无法使用点对网。通常关闭内置子网NAT,使用系统的网卡转发,点对网性能会更好
|
||||
# no_nat = false
|
||||
|
||||
# 是否关闭TUN虚拟网卡,关闭(设为true)后只能充当流量出口或者进行端口映射,关闭后无需管理员权限
|
||||
# no_tun = false
|
||||
# 虚拟网卡模式:no(无网卡)、tun(三层网卡,默认)、tap(二层网卡)
|
||||
# device_mode = "tun"
|
||||
|
||||
# 端口映射,格式为:协议://本地监听地址-目标虚拟IP-目标映射地址
|
||||
# 端口映射用于在本地监听指定端口,并将收到的网络流量经由指定虚拟节点转发到目标地址,从而实现跨网络或内网服务访问
|
||||
@@ -512,4 +527,50 @@ mod tests {
|
||||
assert_eq!(config.tunnel_port, Some(12345));
|
||||
assert_eq!(config.outbound_interface.as_deref(), Some("Ethernet"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_mode_cli_and_legacy_rejection() {
|
||||
let args = Args::try_parse_from([
|
||||
"vnt",
|
||||
"-s",
|
||||
"quic://127.0.0.1:29872",
|
||||
"-n",
|
||||
"test-net",
|
||||
"--device-mode",
|
||||
"tap",
|
||||
])
|
||||
.unwrap();
|
||||
let (config, _) = build_from_args_only(args).unwrap();
|
||||
assert_eq!(config.device_mode, DeviceMode::Tap);
|
||||
|
||||
let legacy: FileConfig = toml::from_str("no_tun = true").unwrap();
|
||||
let error = match build_config_from_args_and_file(None, Some(legacy)) {
|
||||
Err(error) => error,
|
||||
Ok(_) => panic!("legacy no_tun must be rejected"),
|
||||
};
|
||||
assert!(error.to_string().contains("device_mode"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_mode_cli_overrides_file_and_file_defaults() {
|
||||
let file: FileConfig = toml::from_str("device_mode = \"no\"").unwrap();
|
||||
let args = Args::try_parse_from(["vnt", "-s", "quic://127.0.0.1:29872", "-n", "test-net"])
|
||||
.unwrap();
|
||||
let (config, _) = build_config_from_args_and_file(Some(args), Some(file)).unwrap();
|
||||
assert_eq!(config.device_mode, DeviceMode::No);
|
||||
|
||||
let file: FileConfig = toml::from_str("device_mode = \"no\"").unwrap();
|
||||
let args = Args::try_parse_from([
|
||||
"vnt",
|
||||
"-s",
|
||||
"quic://127.0.0.1:29872",
|
||||
"-n",
|
||||
"test-net",
|
||||
"--device-mode",
|
||||
"tap",
|
||||
])
|
||||
.unwrap();
|
||||
let (config, _) = build_config_from_args_and_file(Some(args), Some(file)).unwrap();
|
||||
assert_eq!(config.device_mode, DeviceMode::Tap);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-6
@@ -104,18 +104,26 @@ async fn main0() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
};
|
||||
if !network_manager.is_no_tun() {
|
||||
log::info!("启动网络:{}/{}", reg_msg.ip, reg_msg.prefix_len);
|
||||
network_manager.start_tun().await.context("start tun")?;
|
||||
if network_manager.device_mode().has_device() {
|
||||
log::info!(
|
||||
"启动网络:{}/{} ({})",
|
||||
reg_msg.ip,
|
||||
reg_msg.prefix_len,
|
||||
network_manager.device_mode()
|
||||
);
|
||||
network_manager
|
||||
.set_tun_network_ip(reg_msg.ip, reg_msg.prefix_len)
|
||||
.start_device()
|
||||
.await
|
||||
.context("start device")?;
|
||||
network_manager
|
||||
.set_device_network_ip(reg_msg.ip, reg_msg.prefix_len)
|
||||
.await
|
||||
.context("set network ip")?;
|
||||
if !sub_input.is_empty() {
|
||||
let if_index = network_manager
|
||||
.tun_if_index()
|
||||
.device_if_index()
|
||||
.await
|
||||
.context("tun_if_index")?;
|
||||
.context("device_if_index")?;
|
||||
let mut route_manager = route_manager::RouteManager::new()?;
|
||||
for x in sub_input {
|
||||
let route = Route::new(x.net.network().into(), x.net.prefix_len())
|
||||
|
||||
@@ -89,11 +89,13 @@ mod tests {
|
||||
|
||||
// --- 构造原始包 ---
|
||||
let payload = vec![1u8; 200];
|
||||
let original = make_packet(&payload);
|
||||
let mut original = make_packet(&payload);
|
||||
original.set_ethernet_flag(true);
|
||||
|
||||
// --- 压缩 ---
|
||||
let compressed = lz.compress(original, 0).unwrap();
|
||||
assert!(compressed.is_compressed());
|
||||
assert!(compressed.is_ethernet());
|
||||
|
||||
// 压缩后的 payload 应变小
|
||||
assert!(
|
||||
@@ -106,11 +108,14 @@ mod tests {
|
||||
|
||||
// 标志应清除
|
||||
assert!(!decompressed.is_compressed());
|
||||
assert!(decompressed.is_ethernet());
|
||||
|
||||
// HEAD 不变
|
||||
let mut expected_head = [0u8; HEAD_LENGTH];
|
||||
expected_head[2] = 0x10;
|
||||
assert_eq!(
|
||||
&decompressed.buffer()[..HEAD_LENGTH],
|
||||
&[0u8; HEAD_LENGTH][..],
|
||||
&expected_head,
|
||||
"HEAD 必须保持不变"
|
||||
);
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ use crate::tunnel_core::server::transport::config::{ConnectRegConfig, ProtocolAd
|
||||
use anyhow::bail;
|
||||
use ipnet::Ipv4Net;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub const MAX_NETWORK_CODE_LEN: usize = 32;
|
||||
pub const MAX_DEVICE_ID_LEN: usize = 64;
|
||||
@@ -14,6 +16,44 @@ pub const MAX_NAME_LEN: usize = 128;
|
||||
pub const MAX_VERSION_LEN: usize = 32;
|
||||
pub const MAX_MTU: u16 = 1500;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DeviceMode {
|
||||
No,
|
||||
#[default]
|
||||
Tun,
|
||||
Tap,
|
||||
}
|
||||
|
||||
impl DeviceMode {
|
||||
pub fn has_device(self) -> bool {
|
||||
!matches!(self, Self::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DeviceMode {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::No => "no",
|
||||
Self::Tun => "tun",
|
||||
Self::Tap => "tap",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DeviceMode {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"no" => Ok(Self::No),
|
||||
"tun" => Ok(Self::Tun),
|
||||
"tap" => Ok(Self::Tap),
|
||||
_ => bail!("invalid device_mode '{value}', expected one of: no, tun, tap"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Config {
|
||||
pub server_addr: Vec<ProtocolAddress>,
|
||||
@@ -33,7 +73,7 @@ pub struct Config {
|
||||
pub input: Vec<NetInput>,
|
||||
pub output: Vec<Ipv4Net>,
|
||||
pub no_nat: bool,
|
||||
pub no_tun: bool,
|
||||
pub device_mode: DeviceMode,
|
||||
pub mtu: Option<u16>,
|
||||
pub port_mapping: Vec<PortMapping>,
|
||||
pub allow_port_mapping: bool,
|
||||
@@ -43,6 +83,10 @@ pub struct Config {
|
||||
}
|
||||
impl Config {
|
||||
pub fn check(&self) -> anyhow::Result<()> {
|
||||
#[cfg(any(target_os = "android", target_os = "ios", target_os = "tvos"))]
|
||||
if self.device_mode == DeviceMode::Tap {
|
||||
bail!("TAP mode is not supported on mobile VPN interfaces");
|
||||
}
|
||||
if self.server_addr.is_empty() {
|
||||
bail!("服务器地址不能为空");
|
||||
}
|
||||
@@ -107,3 +151,22 @@ impl Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn device_mode_parse_and_display() {
|
||||
for (text, mode) in [
|
||||
("no", DeviceMode::No),
|
||||
("tun", DeviceMode::Tun),
|
||||
("tap", DeviceMode::Tap),
|
||||
] {
|
||||
assert_eq!(text.parse::<DeviceMode>().unwrap(), mode);
|
||||
assert_eq!(mode.to_string(), text);
|
||||
}
|
||||
assert!("bridge".parse::<DeviceMode>().is_err());
|
||||
assert_eq!(DeviceMode::default(), DeviceMode::Tun);
|
||||
}
|
||||
}
|
||||
|
||||
+49
-24
@@ -1,6 +1,6 @@
|
||||
use crate::api::VntApi;
|
||||
use crate::compression::PacketCompression;
|
||||
use crate::context::config::Config;
|
||||
use crate::context::config::{Config, DeviceMode};
|
||||
use crate::context::{AppState, NetworkAddr, NetworkRoute};
|
||||
use crate::crypto::PacketCrypto;
|
||||
use crate::enhanced_tunnel::enhanced_ipv4_tunnel;
|
||||
@@ -21,7 +21,7 @@ use crate::tunnel_core::server::connection_manager::{
|
||||
};
|
||||
use crate::tunnel_core::server::rpc::ServerRPC;
|
||||
use crate::utils::task_control::TaskGroup;
|
||||
use anyhow::bail;
|
||||
use anyhow::{Context, bail};
|
||||
use ipnet::Ipv4Net;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use std::net::Ipv4Addr;
|
||||
@@ -137,12 +137,12 @@ impl NetworkManager {
|
||||
fec_encoder,
|
||||
);
|
||||
let port_mapping_manager = PortMappingManager::new(
|
||||
config.no_tun,
|
||||
config.device_mode == DeviceMode::No,
|
||||
config.allow_port_mapping,
|
||||
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.device_mode != DeviceMode::No {
|
||||
None
|
||||
} else {
|
||||
let nat_inbound = InternalNatInbound::create(
|
||||
@@ -151,26 +151,32 @@ impl NetworkManager {
|
||||
hybrid_outbound.clone(),
|
||||
allow_subnet.clone(),
|
||||
app_state.network.clone(),
|
||||
config.no_tun,
|
||||
config.device_mode == DeviceMode::No,
|
||||
default_interface.clone(),
|
||||
)
|
||||
.await?;
|
||||
Some(nat_inbound)
|
||||
};
|
||||
|
||||
let (enhanced_tun_inbound, tun_receiver) = if config.no_tun {
|
||||
(
|
||||
let (enhanced_tun_inbound, tun_receiver) = match config.device_mode {
|
||||
DeviceMode::No => (
|
||||
EnhancedTunInbound::Nat(
|
||||
internal_nat_inbound
|
||||
.clone()
|
||||
.expect("internal_nat_inbound must be Some when no_tun is true"),
|
||||
.expect("internal_nat_inbound must be Some in no-device mode"),
|
||||
),
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
let (tun_inbound, tun_receiver) = tun_channel();
|
||||
let tun_data_sender = TunDataInbound::new(tun_inbound, allow_subnet.clone());
|
||||
(EnhancedTunInbound::Tun(tun_data_sender), Some(tun_receiver))
|
||||
),
|
||||
mode @ (DeviceMode::Tun | DeviceMode::Tap) => {
|
||||
let (tun_inbound, tun_receiver) = tun_channel();
|
||||
let tun_data_sender = TunDataInbound::new(tun_inbound, allow_subnet.clone(), mode);
|
||||
let inbound = if mode == DeviceMode::Tap {
|
||||
EnhancedTunInbound::Tap(tun_data_sender)
|
||||
} else {
|
||||
EnhancedTunInbound::Tun(tun_data_sender)
|
||||
};
|
||||
(inbound, Some(tun_receiver))
|
||||
}
|
||||
};
|
||||
|
||||
let (enhanced_inbound, enhanced_outbound) = enhanced_ipv4_tunnel(
|
||||
@@ -182,6 +188,7 @@ impl NetworkManager {
|
||||
password: config.password.clone(),
|
||||
open_quic_client: config.rtx,
|
||||
port_mapping: config.port_mapping.clone(),
|
||||
device_mode: config.device_mode,
|
||||
},
|
||||
crate::enhanced_tunnel::TunnelComponents {
|
||||
hybrid_outbound: hybrid_outbound.clone(),
|
||||
@@ -325,16 +332,25 @@ impl NetworkManager {
|
||||
Ok(RegisterResponse::Success(network_addr))
|
||||
}
|
||||
|
||||
pub fn is_no_tun(&self) -> bool {
|
||||
self.config.no_tun
|
||||
pub fn device_mode(&self) -> DeviceMode {
|
||||
self.config.device_mode
|
||||
}
|
||||
|
||||
pub async fn start_tun(&mut self) -> anyhow::Result<()> {
|
||||
pub async fn start_device(&mut self) -> anyhow::Result<()> {
|
||||
if self.tun_receiver.is_none() || self.enhanced_outbound.is_none() {
|
||||
bail!("start_tun can only be called once");
|
||||
bail!("start_device requires tun/tap mode and can only be called once");
|
||||
}
|
||||
let mut config = DeviceConfig::default();
|
||||
config = config.set_mtu(self.config.mtu.unwrap_or(DEFAULT_MTU));
|
||||
config = config
|
||||
.set_device_mode(self.config.device_mode)
|
||||
.set_mtu(self.config.mtu.unwrap_or(DEFAULT_MTU));
|
||||
if self.config.device_mode == DeviceMode::Tap {
|
||||
let net = self
|
||||
.app_state
|
||||
.get_network()
|
||||
.context("network is not registered")?;
|
||||
config = config.set_mac_addr(crate::ethernet::mac_from_ip(net.ip));
|
||||
}
|
||||
if let Some(tun_name) = self.config.tun_name.clone() {
|
||||
config = config.set_tun_name(tun_name);
|
||||
}
|
||||
@@ -344,11 +360,20 @@ impl NetworkManager {
|
||||
.await
|
||||
}
|
||||
#[cfg(unix)]
|
||||
pub async fn start_tun_fd(&mut self, tun_fd: Option<i32>) -> anyhow::Result<()> {
|
||||
pub async fn start_device_fd(&mut self, tun_fd: Option<i32>) -> anyhow::Result<()> {
|
||||
if self.tun_receiver.is_none() || self.enhanced_outbound.is_none() {
|
||||
bail!("start_tun_fd can only be called once");
|
||||
bail!("start_device_fd requires tun/tap mode and can only be called once");
|
||||
}
|
||||
let mut config = DeviceConfig::default()
|
||||
.set_device_mode(self.config.device_mode)
|
||||
.set_mtu(self.config.mtu.unwrap_or(DEFAULT_MTU));
|
||||
if self.config.device_mode == DeviceMode::Tap {
|
||||
let net = self
|
||||
.app_state
|
||||
.get_network()
|
||||
.context("network is not registered")?;
|
||||
config = config.set_mac_addr(crate::ethernet::mac_from_ip(net.ip));
|
||||
}
|
||||
let mut config = DeviceConfig::default();
|
||||
if let Some(tun_fd) = tun_fd {
|
||||
config = config.set_tun_fd(tun_fd);
|
||||
}
|
||||
@@ -360,7 +385,7 @@ impl NetworkManager {
|
||||
.await
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub async fn set_tun_network_ip(&self, ip: Ipv4Addr, prefix_len: u8) -> anyhow::Result<()> {
|
||||
pub async fn set_device_network_ip(&self, ip: Ipv4Addr, prefix_len: u8) -> anyhow::Result<()> {
|
||||
self.device_io_manager.set_network(ip, prefix_len).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -370,8 +395,8 @@ impl NetworkManager {
|
||||
self.app_state.stop_network();
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub async fn tun_if_index(&self) -> anyhow::Result<u32> {
|
||||
self.device_io_manager.tun_if_index().await
|
||||
pub async fn device_if_index(&self) -> anyhow::Result<u32> {
|
||||
self.device_io_manager.device_if_index().await
|
||||
}
|
||||
pub async fn wait_all_stopped(&mut self) {
|
||||
self.task_group.wait_all_stopped().await;
|
||||
|
||||
@@ -74,7 +74,7 @@ impl PacketCrypto {
|
||||
|
||||
/// AAD 承担"认证"职责:覆盖传输中不变、但不参与 nonce 的头部字节
|
||||
/// byte0(msg_type)/byte2(flags)/byte3(reserved)。
|
||||
/// msg_type 与 flags(COMPRESSED/FEC/GATEWAY)只由发送方设置、
|
||||
/// msg_type 与 flags(COMPRESSED/FEC/GATEWAY/ETHERNET)只由发送方设置、
|
||||
/// 传输中不会被修改,必须纳入认证,否则中间人可翻转造成不可检测的
|
||||
/// 丢包/语义篡改;ttl(byte1) 在中继转发时会递减,不能纳入 AAD。
|
||||
fn make_aad<B: AsRef<[u8]>>(pkt: &NetPacket<B>) -> [u8; 3] {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::context::config::DeviceMode;
|
||||
use crate::context::{NetworkAddr, TrafficStats};
|
||||
use crate::enhanced_tunnel::quic_over::quic_inbound::EnhancedQuicInbound;
|
||||
use crate::ethernet::{ETHERTYPE_IPV4, build_arp_reply, parse_arp_ipv4, parse_frame};
|
||||
use crate::nat::internal_nat::InternalNatInbound;
|
||||
use crate::protocol::ip_packet_protocol::{HEAD_LENGTH, MsgType, NetPacket};
|
||||
use crate::protocol::transmission::TransmissionBytes;
|
||||
use crate::tun::enhanced_tun::EnhancedTunInbound;
|
||||
use crate::tunnel_core::outbound::HybridOutbound;
|
||||
use anyhow::{Context, bail};
|
||||
use pnet_packet::ipv4::Ipv4Packet;
|
||||
use std::net::Ipv4Addr;
|
||||
@@ -14,6 +17,8 @@ pub(crate) struct EnhancedInbound {
|
||||
quic_inbound: EnhancedQuicInbound,
|
||||
internal_nat_inbound: Option<InternalNatInbound>,
|
||||
traffic_stats: TrafficStats,
|
||||
device_mode: DeviceMode,
|
||||
hybrid_outbound: HybridOutbound,
|
||||
}
|
||||
|
||||
impl EnhancedInbound {
|
||||
@@ -22,12 +27,16 @@ impl EnhancedInbound {
|
||||
quic_inbound: EnhancedQuicInbound,
|
||||
internal_nat_inbound: Option<InternalNatInbound>,
|
||||
traffic_stats: TrafficStats,
|
||||
device_mode: DeviceMode,
|
||||
hybrid_outbound: HybridOutbound,
|
||||
) -> Self {
|
||||
Self {
|
||||
tun_data_inbound,
|
||||
quic_inbound,
|
||||
internal_nat_inbound,
|
||||
traffic_stats,
|
||||
device_mode,
|
||||
hybrid_outbound,
|
||||
}
|
||||
}
|
||||
pub async fn inbound(
|
||||
@@ -37,26 +46,65 @@ impl EnhancedInbound {
|
||||
src: Ipv4Addr,
|
||||
packet: NetPacket<TransmissionBytes>,
|
||||
) -> anyhow::Result<()> {
|
||||
let ethernet = packet.is_ethernet();
|
||||
let mut buf = packet.into_buffer();
|
||||
self.traffic_stats.record_rx(src, buf.len() as u64);
|
||||
buf.advance_head(HEAD_LENGTH)?;
|
||||
|
||||
if ethernet && parse_frame(buf.as_ref()).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
if ethernet && self.device_mode != DeviceMode::Tap {
|
||||
if let Some(arp) = parse_arp_ipv4(buf.as_ref())
|
||||
&& arp.operation == 1
|
||||
&& arp.target_ip == network_addr.ip
|
||||
{
|
||||
if let Some(reply) = build_arp_reply(buf.as_ref(), network_addr.ip) {
|
||||
self.hybrid_outbound
|
||||
.ethernet_unicast_outbound(*network_addr, src, reply)
|
||||
.await?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if parse_frame(buf.as_ref()).is_none_or(|frame| frame.ethertype != ETHERTYPE_IPV4) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
match msg_type {
|
||||
MsgType::Turn => {
|
||||
if let Some(internal_nat_inbound) = self.internal_nat_inbound.as_ref() {
|
||||
let Some(ipv4) = Ipv4Packet::new(&buf) else {
|
||||
let ip_data = if ethernet {
|
||||
let Some(frame) = parse_frame(buf.as_ref()) else {
|
||||
return Ok(());
|
||||
};
|
||||
if frame.ethertype != ETHERTYPE_IPV4 {
|
||||
self.tun_data_inbound
|
||||
.inbound(buf, network_addr, src, true)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
&buf[frame.payload_offset..]
|
||||
} else {
|
||||
buf.as_ref()
|
||||
};
|
||||
let Some(ipv4) = Ipv4Packet::new(ip_data) else {
|
||||
bail!("EnhancedInbound not ipv4")
|
||||
};
|
||||
let dest = ipv4.get_destination();
|
||||
if dest != network_addr.ip && !network_addr.network().contains(&dest) {
|
||||
internal_nat_inbound.send(&buf, network_addr).await?;
|
||||
internal_nat_inbound.send(ip_data, network_addr).await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
self.tun_data_inbound.inbound(buf, network_addr).await?;
|
||||
self.tun_data_inbound
|
||||
.inbound(buf, network_addr, src, ethernet)
|
||||
.await?;
|
||||
}
|
||||
MsgType::Broadcast | MsgType::ExcludeBroadcast => {
|
||||
self.tun_data_inbound.inbound(buf, network_addr).await?;
|
||||
self.tun_data_inbound
|
||||
.inbound(buf, network_addr, src, ethernet)
|
||||
.await?;
|
||||
}
|
||||
MsgType::Quic => {
|
||||
let payload = buf.into_bytes().freeze();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::context::AppState;
|
||||
use crate::context::config::DeviceMode;
|
||||
use crate::enhanced_tunnel::inbound::EnhancedInbound;
|
||||
use crate::enhanced_tunnel::outbound::EnhancedOutbound;
|
||||
use crate::nat::SubnetExternalRoute;
|
||||
@@ -18,6 +19,7 @@ pub(crate) struct TunnelConfig {
|
||||
pub password: Option<String>,
|
||||
pub open_quic_client: bool,
|
||||
pub port_mapping: Vec<PortMapping>,
|
||||
pub device_mode: DeviceMode,
|
||||
}
|
||||
|
||||
pub(crate) struct TunnelComponents {
|
||||
@@ -36,7 +38,7 @@ pub(crate) async fn enhanced_ipv4_tunnel(
|
||||
) -> anyhow::Result<(EnhancedInbound, Option<EnhancedOutbound>)> {
|
||||
let password = config.password.unwrap_or_else(|| "password".to_string());
|
||||
let tun = match &tun_data_sender {
|
||||
EnhancedTunInbound::Tun(tun) => Some(tun.clone()),
|
||||
EnhancedTunInbound::Tun(tun) | EnhancedTunInbound::Tap(tun) => Some(tun.clone()),
|
||||
EnhancedTunInbound::Nat(_) => None,
|
||||
};
|
||||
let (inbound, outbound) = quic_over::boot::quic_tunnel_start(
|
||||
@@ -62,6 +64,8 @@ pub(crate) async fn enhanced_ipv4_tunnel(
|
||||
inbound,
|
||||
components.internal_nat_inbound,
|
||||
app_state.traffic_stats.clone(),
|
||||
config.device_mode,
|
||||
components.hybrid_outbound.clone(),
|
||||
);
|
||||
|
||||
let enhanced_outbound = outbound.map(|outbound| {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use crate::context::SharedNetworkAddr;
|
||||
use crate::enhanced_tunnel::quic_over::quic_outbound::EnhancedQuicOutbound;
|
||||
use crate::ethernet::{
|
||||
ETHERTYPE_ARP, ETHERTYPE_IPV4, build_arp_reply, ip_from_mac, is_broadcast_or_multicast,
|
||||
parse_arp_ipv4, parse_frame,
|
||||
};
|
||||
use crate::protocol::transmission::TransmissionBytes;
|
||||
use crate::tunnel_core::outbound::HybridOutbound;
|
||||
use pnet_packet::ipv4::Ipv4Packet;
|
||||
@@ -30,6 +34,74 @@ impl EnhancedOutbound {
|
||||
log::warn!("EnhancedOutbound error: {:?}", e);
|
||||
}
|
||||
}
|
||||
pub async fn ethernet_outbound(&self, data: TransmissionBytes) -> Option<TransmissionBytes> {
|
||||
match self.ethernet_outbound_impl(data).await {
|
||||
Ok(reply) => reply,
|
||||
Err(e) => {
|
||||
log::warn!("EnhancedOutbound Ethernet error: {e:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn ethernet_outbound_impl(
|
||||
&self,
|
||||
data: TransmissionBytes,
|
||||
) -> anyhow::Result<Option<TransmissionBytes>> {
|
||||
let Some(frame) = parse_frame(data.as_ref()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(net) = self.network.get() else {
|
||||
return Ok(None);
|
||||
};
|
||||
match frame.ethertype {
|
||||
ETHERTYPE_IPV4 => {
|
||||
let Some(ipv4) = Ipv4Packet::new(&data[frame.payload_offset..]) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let src = ipv4.get_source();
|
||||
let dest = ipv4.get_destination();
|
||||
if dest == src || dest.is_unspecified() {
|
||||
return Ok(None);
|
||||
}
|
||||
self.hybrid_outbound
|
||||
.ethernet_ipv4_outbound(net, data, dest)
|
||||
.await?;
|
||||
}
|
||||
ETHERTYPE_ARP => {
|
||||
let Some(arp) = parse_arp_ipv4(data.as_ref()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if arp.operation == 1 && arp.target_ip == net.gateway {
|
||||
return Ok(build_arp_reply(data.as_ref(), net.gateway));
|
||||
}
|
||||
if arp.operation == 2 {
|
||||
let dest = ip_from_mac(frame.destination).unwrap_or(arp.target_ip);
|
||||
self.hybrid_outbound
|
||||
.ethernet_unicast_outbound(net, dest, data)
|
||||
.await?;
|
||||
} else {
|
||||
self.hybrid_outbound
|
||||
.ethernet_broadcast_outbound(net, data)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if !is_broadcast_or_multicast(frame.destination)
|
||||
&& let Some(dest) = ip_from_mac(frame.destination)
|
||||
&& net.network().contains(&dest)
|
||||
{
|
||||
self.hybrid_outbound
|
||||
.ethernet_unicast_outbound(net, dest, data)
|
||||
.await?;
|
||||
} else {
|
||||
self.hybrid_outbound
|
||||
.ethernet_broadcast_outbound(net, data)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
async fn ipv4_outbound_impl(&self, data: TransmissionBytes) -> anyhow::Result<()> {
|
||||
let Some(ipv4) = Ipv4Packet::new(data.as_ref()) else {
|
||||
return Ok(());
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::tun::TunDataInbound;
|
||||
use crate::tunnel_core::outbound::HybridOutbound;
|
||||
use crate::utils::task_control::TaskGroup;
|
||||
use anyhow::Context;
|
||||
use pnet_packet::ipv4::Ipv4Packet;
|
||||
use quinn::congestion::BbrConfig;
|
||||
use quinn::crypto::rustls::QuicServerConfig;
|
||||
use quinn::{ClientConfig, Endpoint, EndpointConfig, TransportConfig, default_runtime};
|
||||
@@ -186,7 +187,13 @@ async fn ip_stack_recv_task(
|
||||
log::error!("not network");
|
||||
break;
|
||||
};
|
||||
match tun_data_sender.send((&buf[..len]).into(), &net).await {
|
||||
let Some(ipv4) = Ipv4Packet::new(&buf[..len]) else {
|
||||
continue;
|
||||
};
|
||||
match tun_data_sender
|
||||
.send_ip((&buf[..len]).into(), &net, ipv4.get_source())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("IP stack send error: {:?}", e);
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
use crate::context::NetworkAddr;
|
||||
use crate::protocol::ip_packet_protocol::HEAD_LENGTH;
|
||||
use crate::protocol::transmission::TransmissionBytes;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
pub const ETHERNET_HEADER_LEN: usize = 14;
|
||||
pub const ETHERTYPE_IPV4: u16 = 0x0800;
|
||||
pub const ETHERTYPE_ARP: u16 = 0x0806;
|
||||
const ETHERTYPE_VLAN: u16 = 0x8100;
|
||||
const ETHERTYPE_QINQ: u16 = 0x88a8;
|
||||
const ETHERTYPE_VLAN_9100: u16 = 0x9100;
|
||||
const ARP_IPV4_LEN: usize = 28;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct FrameInfo {
|
||||
pub destination: [u8; 6],
|
||||
pub source: [u8; 6],
|
||||
pub ethertype: u16,
|
||||
pub payload_offset: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct ArpIpv4 {
|
||||
pub operation: u16,
|
||||
pub sender_mac: [u8; 6],
|
||||
pub sender_ip: Ipv4Addr,
|
||||
pub target_mac: [u8; 6],
|
||||
pub target_ip: Ipv4Addr,
|
||||
}
|
||||
|
||||
pub fn mac_from_ip(ip: Ipv4Addr) -> [u8; 6] {
|
||||
let octets = ip.octets();
|
||||
[0x02, 0x00, octets[0], octets[1], octets[2], octets[3]]
|
||||
}
|
||||
|
||||
pub fn ip_from_mac(mac: [u8; 6]) -> Option<Ipv4Addr> {
|
||||
(mac[0] == 0x02 && mac[1] == 0x00).then(|| Ipv4Addr::new(mac[2], mac[3], mac[4], mac[5]))
|
||||
}
|
||||
|
||||
pub fn parse_frame(frame: &[u8]) -> Option<FrameInfo> {
|
||||
if frame.len() < ETHERNET_HEADER_LEN {
|
||||
return None;
|
||||
}
|
||||
let destination = frame[0..6].try_into().ok()?;
|
||||
let source = frame[6..12].try_into().ok()?;
|
||||
let mut ethertype = u16::from_be_bytes(frame[12..14].try_into().ok()?);
|
||||
let mut payload_offset = ETHERNET_HEADER_LEN;
|
||||
// Support stacked VLAN tags. Four tags is already beyond normal Q-in-Q usage
|
||||
// and bounds work performed for an untrusted frame.
|
||||
for _ in 0..4 {
|
||||
if !matches!(
|
||||
ethertype,
|
||||
ETHERTYPE_VLAN | ETHERTYPE_QINQ | ETHERTYPE_VLAN_9100
|
||||
) {
|
||||
break;
|
||||
}
|
||||
if frame.len() < payload_offset + 4 {
|
||||
return None;
|
||||
}
|
||||
ethertype = u16::from_be_bytes(
|
||||
frame[payload_offset + 2..payload_offset + 4]
|
||||
.try_into()
|
||||
.ok()?,
|
||||
);
|
||||
payload_offset += 4;
|
||||
}
|
||||
(frame.len() >= payload_offset).then_some(FrameInfo {
|
||||
destination,
|
||||
source,
|
||||
ethertype,
|
||||
payload_offset,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_arp_ipv4(frame: &[u8]) -> Option<ArpIpv4> {
|
||||
let info = parse_frame(frame)?;
|
||||
if info.ethertype != ETHERTYPE_ARP || frame.len() < info.payload_offset + ARP_IPV4_LEN {
|
||||
return None;
|
||||
}
|
||||
let arp = &frame[info.payload_offset..];
|
||||
if u16::from_be_bytes(arp[0..2].try_into().ok()?) != 1
|
||||
|| u16::from_be_bytes(arp[2..4].try_into().ok()?) != ETHERTYPE_IPV4
|
||||
|| arp[4] != 6
|
||||
|| arp[5] != 4
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(ArpIpv4 {
|
||||
operation: u16::from_be_bytes(arp[6..8].try_into().ok()?),
|
||||
sender_mac: arp[8..14].try_into().ok()?,
|
||||
sender_ip: Ipv4Addr::from(<[u8; 4]>::try_from(&arp[14..18]).ok()?),
|
||||
target_mac: arp[18..24].try_into().ok()?,
|
||||
target_ip: Ipv4Addr::from(<[u8; 4]>::try_from(&arp[24..28]).ok()?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_arp_reply(request: &[u8], own_ip: Ipv4Addr) -> Option<TransmissionBytes> {
|
||||
let info = parse_frame(request)?;
|
||||
let arp = parse_arp_ipv4(request)?;
|
||||
if arp.operation != 1 || arp.target_ip != own_ip {
|
||||
return None;
|
||||
}
|
||||
let frame_len = request.len().max(info.payload_offset + ARP_IPV4_LEN);
|
||||
let mut bytes = TransmissionBytes::with_capacity(HEAD_LENGTH, HEAD_LENGTH + frame_len);
|
||||
bytes.put(request).ok()?;
|
||||
if bytes.len() < frame_len {
|
||||
bytes.extend_end(frame_len - bytes.len());
|
||||
}
|
||||
let own_mac = mac_from_ip(own_ip);
|
||||
bytes[0..6].copy_from_slice(&arp.sender_mac);
|
||||
bytes[6..12].copy_from_slice(&own_mac);
|
||||
let payload = info.payload_offset;
|
||||
bytes[payload + 6..payload + 8].copy_from_slice(&2u16.to_be_bytes());
|
||||
bytes[payload + 8..payload + 14].copy_from_slice(&own_mac);
|
||||
bytes[payload + 14..payload + 18].copy_from_slice(&own_ip.octets());
|
||||
bytes[payload + 18..payload + 24].copy_from_slice(&arp.sender_mac);
|
||||
bytes[payload + 24..payload + 28].copy_from_slice(&arp.sender_ip.octets());
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
pub fn strip_ipv4(mut frame: TransmissionBytes) -> Option<TransmissionBytes> {
|
||||
let info = parse_frame(frame.as_ref())?;
|
||||
if info.ethertype != ETHERTYPE_IPV4 {
|
||||
return None;
|
||||
}
|
||||
frame.advance_head(info.payload_offset).ok()?;
|
||||
Some(frame)
|
||||
}
|
||||
|
||||
pub fn wrap_ipv4(
|
||||
mut packet: TransmissionBytes,
|
||||
src_node: Ipv4Addr,
|
||||
net: &NetworkAddr,
|
||||
) -> Option<TransmissionBytes> {
|
||||
let ipv4 = pnet_packet::ipv4::Ipv4Packet::new(packet.as_ref())?;
|
||||
let destination_ip = ipv4.get_destination();
|
||||
let destination_mac = if destination_ip.is_broadcast() || destination_ip == net.broadcast {
|
||||
[0xff; 6]
|
||||
} else if destination_ip.is_multicast() {
|
||||
let octets = destination_ip.octets();
|
||||
[0x01, 0x00, 0x5e, octets[1] & 0x7f, octets[2], octets[3]]
|
||||
} else {
|
||||
mac_from_ip(net.ip)
|
||||
};
|
||||
packet.retreat_head(ETHERNET_HEADER_LEN).ok()?;
|
||||
packet[0..6].copy_from_slice(&destination_mac);
|
||||
packet[6..12].copy_from_slice(&mac_from_ip(src_node));
|
||||
packet[12..14].copy_from_slice(ÐERTYPE_IPV4.to_be_bytes());
|
||||
Some(packet)
|
||||
}
|
||||
|
||||
pub fn is_broadcast_or_multicast(mac: [u8; 6]) -> bool {
|
||||
mac == [0xff; 6] || mac[0] & 1 != 0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn node_mac_round_trip() {
|
||||
let ip = Ipv4Addr::new(10, 26, 1, 9);
|
||||
assert_eq!(mac_from_ip(ip), [2, 0, 10, 26, 1, 9]);
|
||||
assert_eq!(ip_from_mac(mac_from_ip(ip)), Some(ip));
|
||||
assert_eq!(ip_from_mac([0; 6]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_vlan_ipv4() {
|
||||
let mut frame = vec![0u8; 18 + 20];
|
||||
frame[12..14].copy_from_slice(ÐERTYPE_VLAN.to_be_bytes());
|
||||
frame[16..18].copy_from_slice(ÐERTYPE_IPV4.to_be_bytes());
|
||||
let info = parse_frame(&frame).unwrap();
|
||||
assert_eq!(info.ethertype, ETHERTYPE_IPV4);
|
||||
assert_eq!(info.payload_offset, 18);
|
||||
assert!(parse_frame(&[0u8; 13]).is_none());
|
||||
let mut truncated_vlan = vec![0u8; 16];
|
||||
truncated_vlan[12..14].copy_from_slice(ÐERTYPE_VLAN.to_be_bytes());
|
||||
assert!(parse_frame(&truncated_vlan).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_arp_reply_for_node_ip() {
|
||||
let sender_ip = Ipv4Addr::new(10, 26, 0, 8);
|
||||
let target_ip = Ipv4Addr::new(10, 26, 0, 9);
|
||||
let sender_mac = mac_from_ip(sender_ip);
|
||||
let mut request = vec![0u8; 42];
|
||||
request[0..6].copy_from_slice(&[0xff; 6]);
|
||||
request[6..12].copy_from_slice(&sender_mac);
|
||||
request[12..14].copy_from_slice(ÐERTYPE_ARP.to_be_bytes());
|
||||
request[14..16].copy_from_slice(&1u16.to_be_bytes());
|
||||
request[16..18].copy_from_slice(ÐERTYPE_IPV4.to_be_bytes());
|
||||
request[18] = 6;
|
||||
request[19] = 4;
|
||||
request[20..22].copy_from_slice(&1u16.to_be_bytes());
|
||||
request[22..28].copy_from_slice(&sender_mac);
|
||||
request[28..32].copy_from_slice(&sender_ip.octets());
|
||||
request[38..42].copy_from_slice(&target_ip.octets());
|
||||
|
||||
let reply = build_arp_reply(&request, target_ip).unwrap();
|
||||
let arp = parse_arp_ipv4(reply.as_ref()).unwrap();
|
||||
assert_eq!(arp.operation, 2);
|
||||
assert_eq!(arp.sender_ip, target_ip);
|
||||
assert_eq!(arp.sender_mac, mac_from_ip(target_ip));
|
||||
assert_eq!(arp.target_ip, sender_ip);
|
||||
assert_eq!(arp.target_mac, sender_mac);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wraps_ipv4_for_tap_and_strips_it_again() {
|
||||
let net = NetworkAddr {
|
||||
gateway: Ipv4Addr::new(10, 26, 0, 1),
|
||||
broadcast: Ipv4Addr::new(10, 26, 0, 255),
|
||||
ip: Ipv4Addr::new(10, 26, 0, 9),
|
||||
prefix_len: 24,
|
||||
};
|
||||
let src = Ipv4Addr::new(10, 26, 0, 8);
|
||||
let mut packet = TransmissionBytes::with_capacity(HEAD_LENGTH, HEAD_LENGTH + 20);
|
||||
packet.put(&[0u8; 20]).unwrap();
|
||||
packet[0] = 0x45;
|
||||
packet[12..16].copy_from_slice(&src.octets());
|
||||
packet[16..20].copy_from_slice(&net.ip.octets());
|
||||
let original = packet.as_ref().to_vec();
|
||||
|
||||
let frame = wrap_ipv4(packet, src, &net).unwrap();
|
||||
let info = parse_frame(frame.as_ref()).unwrap();
|
||||
assert_eq!(info.source, mac_from_ip(src));
|
||||
assert_eq!(info.destination, mac_from_ip(net.ip));
|
||||
assert_eq!(strip_ipv4(frame).unwrap().as_ref(), original);
|
||||
}
|
||||
}
|
||||
@@ -436,6 +436,18 @@ mod tests {
|
||||
shard
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_original_fec_packet_preserves_ethernet_flag() {
|
||||
let decoder = FecDecoder::new();
|
||||
let packets = decoder
|
||||
.receive(build_data_packet(99, 0, 0x81, 0x10, &[1, 2, 3]))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(packets.len(), 1);
|
||||
assert!(packets[0].is_ethernet());
|
||||
assert!(!packets[0].is_fec());
|
||||
}
|
||||
|
||||
/// 变长批次:丢一个数据包,靠校验包必须能恢复(修复前必报 IncorrectShardSize)
|
||||
#[test]
|
||||
fn test_reconstruct_variable_length_batch() {
|
||||
|
||||
@@ -2,6 +2,7 @@ pub(crate) mod compression;
|
||||
pub mod context;
|
||||
pub mod core;
|
||||
pub mod crypto;
|
||||
pub(crate) mod ethernet;
|
||||
pub(crate) mod fec;
|
||||
pub mod nat;
|
||||
pub mod protocol;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
0 15 31
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| 1 | msg_type(7) |max ttl(4) |curr ttl(4)| C | G | R | reserve(13) |
|
||||
| 1 | msg_type(7) |max ttl(4) |curr ttl(4)| C | G | F | E | reserve(12) |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| seq(32) |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
@@ -27,7 +27,7 @@ pub struct NetHeader {
|
||||
pub type_byte: u8,
|
||||
/// Byte 1: high 4 = max ttl, low 4 = curr ttl
|
||||
pub ttl_byte: u8,
|
||||
/// Byte 2: C(0x80) | G(0x40) | reserve
|
||||
/// Byte 2: C(0x80) | G(0x40) | F(0x20) | ETHERNET(0x10) | reserve
|
||||
pub flags_byte: u8,
|
||||
/// Byte 3: reserve
|
||||
pub _reserved: u8,
|
||||
@@ -39,6 +39,7 @@ pub struct NetHeader {
|
||||
const COMPRESSED: u8 = 0x80;
|
||||
const GATEWAY: u8 = 0x40;
|
||||
const FEC: u8 = 0x20;
|
||||
const ETHERNET: u8 = 0x10;
|
||||
impl NetHeader {
|
||||
#[inline]
|
||||
pub fn msg_type(&self) -> u8 {
|
||||
@@ -210,6 +211,9 @@ impl<B: AsRef<[u8]>> NetPacket<B> {
|
||||
pub fn is_fec(&self) -> bool {
|
||||
(self.header().flags_byte & FEC) != 0
|
||||
}
|
||||
pub fn is_ethernet(&self) -> bool {
|
||||
(self.header().flags_byte & ETHERNET) != 0
|
||||
}
|
||||
pub fn head(&self) -> &[u8] {
|
||||
&self.buffer.as_ref()[..HEAD_LENGTH]
|
||||
}
|
||||
@@ -258,6 +262,9 @@ impl<B: AsRef<[u8]> + AsMut<[u8]>> NetPacket<B> {
|
||||
pub fn set_fec_flag(&mut self, fec: bool) {
|
||||
self.header_mut().set_flag(FEC, fec);
|
||||
}
|
||||
pub fn set_ethernet_flag(&mut self, ethernet: bool) {
|
||||
self.header_mut().set_flag(ETHERNET, ethernet);
|
||||
}
|
||||
|
||||
pub fn set_payload(&mut self, data: &[u8]) -> io::Result<()> {
|
||||
let buf = self.buffer.as_mut();
|
||||
@@ -345,6 +352,19 @@ mod tests {
|
||||
assert!(MsgType::try_from(20u8).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ethernet_flag_round_trip() {
|
||||
let mut packet = NetPacket::new(BytesMut::from(&[0u8; HEAD_LENGTH][..])).unwrap();
|
||||
assert!(!packet.is_ethernet());
|
||||
packet.set_ethernet_flag(true);
|
||||
assert!(packet.is_ethernet());
|
||||
packet.set_fec_flag(true);
|
||||
assert!(packet.is_ethernet());
|
||||
packet.set_ethernet_flag(false);
|
||||
assert!(!packet.is_ethernet());
|
||||
assert!(packet.is_fec());
|
||||
}
|
||||
|
||||
/// 中继转发语义:包每经过一跳 curr_ttl 减 1,curr_ttl >= 1 时才继续转发,
|
||||
/// 接收方以 metric = max_ttl - curr_ttl 计算路由距离。
|
||||
#[test]
|
||||
|
||||
@@ -1,18 +1,134 @@
|
||||
use crate::context::NetworkAddr;
|
||||
use crate::ethernet::strip_ipv4;
|
||||
use crate::nat::internal_nat::InternalNatInbound;
|
||||
use crate::protocol::transmission::TransmissionBytes;
|
||||
use crate::tun::TunDataInbound;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum EnhancedTunInbound {
|
||||
Tun(TunDataInbound),
|
||||
Tap(TunDataInbound),
|
||||
Nat(InternalNatInbound),
|
||||
}
|
||||
impl EnhancedTunInbound {
|
||||
pub async fn inbound(&self, data: TransmissionBytes, net: &NetworkAddr) -> anyhow::Result<()> {
|
||||
pub async fn inbound(
|
||||
&self,
|
||||
data: TransmissionBytes,
|
||||
net: &NetworkAddr,
|
||||
src_node: Ipv4Addr,
|
||||
ethernet: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
match self {
|
||||
EnhancedTunInbound::Tun(tun) => tun.send(data, net).await,
|
||||
EnhancedTunInbound::Nat(nat) => nat.send(&data, net).await,
|
||||
EnhancedTunInbound::Tun(tun) => {
|
||||
let data = if ethernet {
|
||||
let Some(ip) = strip_ipv4(data) else {
|
||||
return Ok(());
|
||||
};
|
||||
ip
|
||||
} else {
|
||||
data
|
||||
};
|
||||
tun.send_ip(data, net, src_node).await
|
||||
}
|
||||
EnhancedTunInbound::Tap(tap) => {
|
||||
if ethernet {
|
||||
tap.send_frame(data).await
|
||||
} else {
|
||||
tap.send_ip(data, net, src_node).await
|
||||
}
|
||||
}
|
||||
EnhancedTunInbound::Nat(nat) => {
|
||||
let data = if ethernet {
|
||||
let Some(ip) = strip_ipv4(data) else {
|
||||
return Ok(());
|
||||
};
|
||||
ip
|
||||
} else {
|
||||
data
|
||||
};
|
||||
nat.send(&data, net).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::context::config::DeviceMode;
|
||||
use crate::ethernet::{ETHERTYPE_IPV4, parse_frame, wrap_ipv4};
|
||||
use crate::nat::AllowSubnetExternalRoute;
|
||||
use crate::protocol::ip_packet_protocol::HEAD_LENGTH;
|
||||
use crate::tun::{TunDataInbound, tun_channel};
|
||||
|
||||
fn network() -> NetworkAddr {
|
||||
NetworkAddr {
|
||||
gateway: Ipv4Addr::new(10, 26, 0, 1),
|
||||
broadcast: Ipv4Addr::new(10, 26, 0, 255),
|
||||
ip: Ipv4Addr::new(10, 26, 0, 9),
|
||||
prefix_len: 24,
|
||||
}
|
||||
}
|
||||
|
||||
fn ipv4(src: Ipv4Addr, dest: Ipv4Addr) -> TransmissionBytes {
|
||||
let mut packet = TransmissionBytes::with_capacity(HEAD_LENGTH, HEAD_LENGTH + 20);
|
||||
packet.put(&[0u8; 20]).unwrap();
|
||||
packet[0] = 0x45;
|
||||
packet[12..16].copy_from_slice(&src.octets());
|
||||
packet[16..20].copy_from_slice(&dest.octets());
|
||||
packet
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tap_wraps_ip_and_tun_strips_ethernet() {
|
||||
let net = network();
|
||||
let src = Ipv4Addr::new(10, 26, 0, 8);
|
||||
|
||||
let (tap_tx, mut tap_rx) = tun_channel();
|
||||
let tap = EnhancedTunInbound::Tap(TunDataInbound::new(
|
||||
tap_tx,
|
||||
AllowSubnetExternalRoute::new(vec![]),
|
||||
DeviceMode::Tap,
|
||||
));
|
||||
tap.inbound(ipv4(src, net.ip), &net, src, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let frame = tap_rx.receiver.recv().await.unwrap();
|
||||
assert_eq!(
|
||||
parse_frame(frame.as_ref()).unwrap().ethertype,
|
||||
ETHERTYPE_IPV4
|
||||
);
|
||||
|
||||
let (tun_tx, mut tun_rx) = tun_channel();
|
||||
let tun = EnhancedTunInbound::Tun(TunDataInbound::new(
|
||||
tun_tx,
|
||||
AllowSubnetExternalRoute::new(vec![]),
|
||||
DeviceMode::Tun,
|
||||
));
|
||||
let frame = wrap_ipv4(ipv4(src, net.ip), src, &net).unwrap();
|
||||
tun.inbound(frame, &net, src, true).await.unwrap();
|
||||
let packet = tun_rx.receiver.recv().await.unwrap();
|
||||
assert_eq!(packet[0] >> 4, 4);
|
||||
assert_eq!(&packet[16..20], &net.ip.octets());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tap_keeps_arbitrary_ethernet_frame() {
|
||||
let net = network();
|
||||
let src = Ipv4Addr::new(10, 26, 0, 8);
|
||||
let (tap_tx, mut tap_rx) = tun_channel();
|
||||
let tap = EnhancedTunInbound::Tap(TunDataInbound::new(
|
||||
tap_tx,
|
||||
AllowSubnetExternalRoute::new(vec![]),
|
||||
DeviceMode::Tap,
|
||||
));
|
||||
let mut raw = vec![0u8; 32];
|
||||
raw[0..6].copy_from_slice(&[0xff; 6]);
|
||||
raw[12..14].copy_from_slice(&0x88b5u16.to_be_bytes());
|
||||
tap.inbound(raw.as_slice().into(), &net, src, true)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(tap_rx.receiver.recv().await.unwrap().as_ref(), raw);
|
||||
}
|
||||
}
|
||||
|
||||
+67
-20
@@ -1,3 +1,4 @@
|
||||
use crate::context::config::DeviceMode;
|
||||
use crate::enhanced_tunnel::outbound::EnhancedOutbound;
|
||||
use crate::protocol::ip_packet_protocol::HEAD_LENGTH;
|
||||
use crate::protocol::transmission::TransmissionBytes;
|
||||
@@ -10,9 +11,9 @@ use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tun_rs::AsyncDevice;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use tun_rs::DeviceBuilder;
|
||||
use tun_rs::async_framed::{Decoder, DeviceFramedRead, DeviceFramedWrite, Encoder};
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use tun_rs::{DeviceBuilder, Layer};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DeviceIOManager {
|
||||
@@ -27,10 +28,12 @@ pub struct DeviceTask {
|
||||
}
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DeviceConfig {
|
||||
pub device_mode: DeviceMode,
|
||||
pub tun_name: Option<String>,
|
||||
#[cfg(unix)]
|
||||
pub tun_fd: Option<i32>,
|
||||
pub mtu: Option<u16>,
|
||||
pub mac_addr: Option<[u8; 6]>,
|
||||
}
|
||||
|
||||
impl DeviceConfig {
|
||||
@@ -47,6 +50,14 @@ impl DeviceConfig {
|
||||
self.mtu = Some(mtu);
|
||||
self
|
||||
}
|
||||
pub fn set_device_mode(mut self, device_mode: DeviceMode) -> Self {
|
||||
self.device_mode = device_mode;
|
||||
self
|
||||
}
|
||||
pub fn set_mac_addr(mut self, mac_addr: [u8; 6]) -> Self {
|
||||
self.mac_addr = Some(mac_addr);
|
||||
self
|
||||
}
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct TunInbound {
|
||||
@@ -54,7 +65,7 @@ pub struct TunInbound {
|
||||
}
|
||||
|
||||
pub struct TunReceiver {
|
||||
receiver: Receiver<TransmissionBytes>,
|
||||
pub(crate) receiver: Receiver<TransmissionBytes>,
|
||||
}
|
||||
pub fn tun_channel() -> (TunInbound, TunReceiver) {
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(1024);
|
||||
@@ -84,9 +95,10 @@ impl DeviceIOManager {
|
||||
bail!("device task already started");
|
||||
}
|
||||
self.stop_task().await;
|
||||
// 先执行可能失败的 tun 设备创建,成功后才消费 receiver/outbound,
|
||||
// 先执行可能失败的 TUN/TAP 设备创建,成功后才消费 receiver/outbound,
|
||||
// 保证失败时调用方状态完整、可以重试
|
||||
let device = Arc::new(create_tun(device_config)?);
|
||||
let device_mode = device_config.device_mode;
|
||||
let device = Arc::new(create_device(device_config)?);
|
||||
let receiver = receiver.take().unwrap();
|
||||
let enhanced_outbound = enhanced_outbound.take().unwrap();
|
||||
let task = create(
|
||||
@@ -94,12 +106,13 @@ impl DeviceIOManager {
|
||||
device,
|
||||
receiver.receiver,
|
||||
enhanced_outbound,
|
||||
device_mode,
|
||||
);
|
||||
self.device.lock().await.0.replace(task);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub async fn tun_if_index(&self) -> anyhow::Result<u32> {
|
||||
pub async fn device_if_index(&self) -> anyhow::Result<u32> {
|
||||
let guard = self.device.lock().await;
|
||||
if let Some(v) = &guard.0 {
|
||||
Ok(v.device.if_index()?)
|
||||
@@ -111,7 +124,7 @@ impl DeviceIOManager {
|
||||
pub async fn set_network(&self, ip: Ipv4Addr, prefix_len: u8) -> anyhow::Result<()> {
|
||||
let mut guard = self.device.lock().await;
|
||||
let Some(dev) = guard.0.as_ref() else {
|
||||
bail!("未启动tun")
|
||||
bail!("虚拟网卡尚未启动")
|
||||
};
|
||||
if let Some(v) = guard.1.as_ref()
|
||||
&& v.0 == ip
|
||||
@@ -127,7 +140,7 @@ impl DeviceIOManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_tun(config: DeviceConfig) -> anyhow::Result<AsyncDevice> {
|
||||
fn create_device(config: DeviceConfig) -> anyhow::Result<AsyncDevice> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let fd = config
|
||||
@@ -146,21 +159,45 @@ fn create_tun(config: DeviceConfig) -> anyhow::Result<AsyncDevice> {
|
||||
unsafe { return Ok(AsyncDevice::from_fd(fd)?) }
|
||||
}
|
||||
let mut builder = DeviceBuilder::new();
|
||||
builder = builder.layer(match config.device_mode {
|
||||
DeviceMode::Tap => Layer::L2,
|
||||
DeviceMode::Tun => Layer::L3,
|
||||
DeviceMode::No => bail!("cannot create a device in no mode"),
|
||||
});
|
||||
if let Some(tun_name) = config.tun_name {
|
||||
builder = builder.name(tun_name);
|
||||
}
|
||||
if let Some(mtu) = config.mtu {
|
||||
builder = builder.mtu(mtu);
|
||||
}
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "linux",
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "macos",
|
||||
target_os = "netbsd"
|
||||
))]
|
||||
if let Some(mac_addr) = config.mac_addr {
|
||||
builder = builder.mac_addr(mac_addr);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
builder = builder.metric(1);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if config.device_mode == DeviceMode::Tun {
|
||||
builder = builder.offload(true);
|
||||
}
|
||||
let dev = builder.build_async().context("创建tun失败")?;
|
||||
let dev = builder.build_async().with_context(|| {
|
||||
if config.device_mode == DeviceMode::Tap && cfg!(windows) {
|
||||
"创建 TAP 失败;Windows TAP 模式需要预先安装 tap-windows (tap0901) 驱动"
|
||||
} else if config.device_mode == DeviceMode::Tap {
|
||||
"创建 TAP 失败"
|
||||
} else {
|
||||
"创建 TUN 失败"
|
||||
}
|
||||
})?;
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
_ = dev.set_tx_queue_len(1000);
|
||||
@@ -173,26 +210,28 @@ fn create(
|
||||
device: Arc<AsyncDevice>,
|
||||
receiver: Receiver<TransmissionBytes>,
|
||||
enhanced_outbound: EnhancedOutbound,
|
||||
device_mode: DeviceMode,
|
||||
) -> DeviceTask {
|
||||
let device_framed_read = DeviceFramedRead::new(device.clone(), BytesCodec::new());
|
||||
let device_framed_write = DeviceFramedWrite::new(device.clone(), BytesCodec::new());
|
||||
let outbound_device = device.clone();
|
||||
|
||||
// 读写两个方向合并为一个任务:任一方向结束(出错或设备关闭)即
|
||||
// 通过 select! 取消另一方向,避免单侧失败后另一侧继续运行的半开状态
|
||||
let task = task_group.spawn(async move {
|
||||
tokio::select! {
|
||||
rs = in_tun_loop(receiver, device_framed_write) => {
|
||||
rs = in_device_loop(receiver, device_framed_write) => {
|
||||
if let Err(e) = rs {
|
||||
log::error!("in_tun_loop error, stopping out_tun_loop: {e:?}");
|
||||
log::error!("in_device_loop error, stopping out_device_loop: {e:?}");
|
||||
} else {
|
||||
log::warn!("in_tun_loop exited, stopping out_tun_loop");
|
||||
log::warn!("in_device_loop exited, stopping out_device_loop");
|
||||
}
|
||||
}
|
||||
rs = out_tun_loop(device_framed_read, enhanced_outbound) => {
|
||||
rs = out_device_loop(device_framed_read, outbound_device, enhanced_outbound, device_mode) => {
|
||||
if let Err(e) = rs {
|
||||
log::error!("out_tun_loop error, stopping in_tun_loop: {e:?}");
|
||||
log::error!("out_device_loop error, stopping in_device_loop: {e:?}");
|
||||
} else {
|
||||
log::warn!("out_tun_loop exited, stopping in_tun_loop");
|
||||
log::warn!("out_device_loop exited, stopping in_device_loop");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +240,7 @@ fn create(
|
||||
DeviceTask { device, task }
|
||||
}
|
||||
|
||||
async fn in_tun_loop(
|
||||
async fn in_device_loop(
|
||||
mut receiver: Receiver<TransmissionBytes>,
|
||||
mut device_framed_write: DeviceFramedWrite<BytesCodec, Arc<AsyncDevice>>,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -209,7 +248,7 @@ async fn in_tun_loop(
|
||||
match device_framed_write.send(data).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("send to tun error: {:?}", e);
|
||||
log::error!("send to virtual device error: {:?}", e);
|
||||
return Err(anyhow::anyhow!(e));
|
||||
}
|
||||
}
|
||||
@@ -217,13 +256,21 @@ async fn in_tun_loop(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn out_tun_loop(
|
||||
async fn out_device_loop(
|
||||
mut device_framed_read: DeviceFramedRead<BytesCodec, Arc<AsyncDevice>>,
|
||||
device: Arc<AsyncDevice>,
|
||||
enhanced_outbound: EnhancedOutbound,
|
||||
device_mode: DeviceMode,
|
||||
) -> anyhow::Result<()> {
|
||||
while let Some(rs) = device_framed_read.next().await {
|
||||
let bytes_mut = rs?;
|
||||
enhanced_outbound.ipv4_outbound(bytes_mut).await;
|
||||
if device_mode == DeviceMode::Tap {
|
||||
if let Some(reply) = enhanced_outbound.ethernet_outbound(bytes_mut).await {
|
||||
device.send(reply.as_ref()).await?;
|
||||
}
|
||||
} else {
|
||||
enhanced_outbound.ipv4_outbound(bytes_mut).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
use crate::context::NetworkAddr;
|
||||
use crate::context::config::DeviceMode;
|
||||
use crate::ethernet::wrap_ipv4;
|
||||
use crate::nat::AllowSubnetExternalRoute;
|
||||
use crate::protocol::transmission::TransmissionBytes;
|
||||
use crate::tun::TunInbound;
|
||||
use pnet_packet::ipv4::Ipv4Packet;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TunDataInbound {
|
||||
allow_subnet: AllowSubnetExternalRoute,
|
||||
tun_inbound: TunInbound,
|
||||
device_mode: DeviceMode,
|
||||
}
|
||||
impl TunDataInbound {
|
||||
pub fn new(tun_inbound: TunInbound, allow_subnet: AllowSubnetExternalRoute) -> Self {
|
||||
pub fn new(
|
||||
tun_inbound: TunInbound,
|
||||
allow_subnet: AllowSubnetExternalRoute,
|
||||
device_mode: DeviceMode,
|
||||
) -> Self {
|
||||
Self {
|
||||
allow_subnet,
|
||||
tun_inbound,
|
||||
device_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TunDataInbound {
|
||||
pub async fn send(&self, data: TransmissionBytes, net: &NetworkAddr) -> anyhow::Result<()> {
|
||||
pub async fn send_ip(
|
||||
&self,
|
||||
data: TransmissionBytes,
|
||||
net: &NetworkAddr,
|
||||
src_node: Ipv4Addr,
|
||||
) -> anyhow::Result<()> {
|
||||
if data.is_empty() || data[0] >> 4 != 4 {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -33,10 +47,23 @@ impl TunDataInbound {
|
||||
|| dest.is_multicast()
|
||||
|| self.allow_subnet.allow(&dest)
|
||||
{
|
||||
let data = if self.device_mode == DeviceMode::Tap {
|
||||
let Some(frame) = wrap_ipv4(data, src_node, net) else {
|
||||
return Ok(());
|
||||
};
|
||||
frame
|
||||
} else {
|
||||
data
|
||||
};
|
||||
self.tun_inbound.sender.send(data).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_frame(&self, data: TransmissionBytes) -> anyhow::Result<()> {
|
||||
self.tun_inbound.sender.send(data).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -44,7 +71,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::nat::AllowSubnetExternalRoute;
|
||||
use crate::tun::tun_channel;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
fn test_net() -> NetworkAddr {
|
||||
NetworkAddr {
|
||||
@@ -59,17 +85,29 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_send_empty_or_short_packet_does_not_panic() {
|
||||
let (tun_inbound, _receiver) = tun_channel();
|
||||
let inbound = TunDataInbound::new(tun_inbound, AllowSubnetExternalRoute::new(vec![]));
|
||||
let inbound = TunDataInbound::new(
|
||||
tun_inbound,
|
||||
AllowSubnetExternalRoute::new(vec![]),
|
||||
DeviceMode::Tun,
|
||||
);
|
||||
|
||||
// 零载荷包(头部被剥离后为空)
|
||||
inbound
|
||||
.send(TransmissionBytes::zeroed(0), &test_net())
|
||||
.send_ip(
|
||||
TransmissionBytes::zeroed(0),
|
||||
&test_net(),
|
||||
Ipv4Addr::new(10, 26, 0, 3),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 过短的包(不足 IPv4 头)
|
||||
inbound
|
||||
.send(TransmissionBytes::zeroed(3), &test_net())
|
||||
.send_ip(
|
||||
TransmissionBytes::zeroed(3),
|
||||
&test_net(),
|
||||
Ipv4Addr::new(10, 26, 0, 3),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -234,6 +234,60 @@ impl HybridOutbound {
|
||||
self.traffic_stats.record_tx(dest, len);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn ethernet_ipv4_outbound(
|
||||
&self,
|
||||
net: NetworkAddr,
|
||||
data: TransmissionBytes,
|
||||
mut dest: Ipv4Addr,
|
||||
) -> anyhow::Result<()> {
|
||||
if dest == net.gateway {
|
||||
let Some(ip) = crate::ethernet::strip_ipv4(data) else {
|
||||
return Ok(());
|
||||
};
|
||||
return self.ipv4_gateway_outbound(net, ip).await;
|
||||
}
|
||||
if dest.is_multicast() || dest == net.broadcast || dest.is_broadcast() {
|
||||
return self.ethernet_broadcast_outbound(net, data).await;
|
||||
}
|
||||
if !net.network().contains(&dest) {
|
||||
if let Some(route) = self.external_route.route(&dest) {
|
||||
dest = route;
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
self.ethernet_unicast_outbound(net, dest, data).await
|
||||
}
|
||||
|
||||
pub async fn ethernet_unicast_outbound(
|
||||
&self,
|
||||
net: NetworkAddr,
|
||||
dest: Ipv4Addr,
|
||||
mut data: TransmissionBytes,
|
||||
) -> anyhow::Result<()> {
|
||||
let len = data.len() as u64;
|
||||
data.retreat_head(HEAD_LENGTH)?;
|
||||
let mut packet = NetPacket::new(data)?;
|
||||
packet.set_msg_type(MsgType::Turn);
|
||||
packet.set_src_id(net.ip.into());
|
||||
packet.set_dest_id(dest.into());
|
||||
packet.set_ttl(5);
|
||||
packet.set_ethernet_flag(true);
|
||||
let packet = self
|
||||
.packet_compression
|
||||
.compress(packet, self.basic_outbound.encrypt_reserve())?;
|
||||
let packet = if let Some(fec_encoder) = &self.fec_encoder {
|
||||
fec_encoder.encode(packet)?
|
||||
} else {
|
||||
packet
|
||||
};
|
||||
self.basic_outbound
|
||||
.send_encrypted_packet(dest, packet)
|
||||
.await?;
|
||||
self.traffic_stats.record_tx(dest, len);
|
||||
Ok(())
|
||||
}
|
||||
pub async fn ipv4_gateway_outbound(
|
||||
&self,
|
||||
net: NetworkAddr,
|
||||
@@ -279,6 +333,37 @@ impl HybridOutbound {
|
||||
.send_raw_broadcast(exclude_ips, packet_bytes)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn ethernet_broadcast_outbound(
|
||||
&self,
|
||||
net: NetworkAddr,
|
||||
mut data: TransmissionBytes,
|
||||
) -> anyhow::Result<()> {
|
||||
data.retreat_head(HEAD_LENGTH)?;
|
||||
let mut packet = NetPacket::new(data)?;
|
||||
packet.set_msg_type(MsgType::Broadcast);
|
||||
packet.set_src_id(net.ip.into());
|
||||
packet.set_dest_id(Ipv4Addr::BROADCAST.into());
|
||||
packet.set_ttl(5);
|
||||
packet.set_ethernet_flag(true);
|
||||
let mut packet = self
|
||||
.packet_compression
|
||||
.compress(packet, self.basic_outbound.encrypt_reserve())?;
|
||||
self.basic_outbound.encrypt_in_place(&mut packet)?;
|
||||
let packet_bytes = packet.into_bytes();
|
||||
let list = self.server_info.client_online_ips();
|
||||
let exclude_ips = self
|
||||
.basic_outbound
|
||||
.p2p_broadcast_transmission(&list, 16, &packet_bytes);
|
||||
if let Some(exclude_ips) = &exclude_ips
|
||||
&& exclude_ips.len() == list.len()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
self.basic_outbound
|
||||
.send_raw_broadcast(exclude_ips, packet_bytes)
|
||||
.await
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn has_route(&self, dest: &Ipv4Addr) -> bool {
|
||||
self.basic_outbound.exists_route(dest)
|
||||
|
||||
@@ -51,7 +51,7 @@ $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD=""
|
||||
pnpm build:desktop
|
||||
```
|
||||
|
||||
Windows 使用虚拟网卡模式时,可能需要以管理员身份运行。
|
||||
Windows 使用虚拟网卡模式时,可能需要以管理员身份运行;TAP(二层)模式还需要预先安装 `tap-windows`(`tap0901`)驱动,内置的 `wintun.dll` 只用于 TUN(三层)模式。
|
||||
|
||||
### 发布桌面更新
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class VntConfig {
|
||||
private final boolean rtx;
|
||||
private final boolean fec;
|
||||
private final boolean noNat;
|
||||
private final boolean noTun;
|
||||
private final String deviceMode;
|
||||
private final Integer mtu;
|
||||
private final boolean allowMapping;
|
||||
private final List<String> portMapping;
|
||||
@@ -48,7 +48,7 @@ public class VntConfig {
|
||||
this.rtx = builder.rtx;
|
||||
this.fec = builder.fec;
|
||||
this.noNat = builder.noNat;
|
||||
this.noTun = builder.noTun;
|
||||
this.deviceMode = builder.deviceMode;
|
||||
this.mtu = builder.mtu;
|
||||
this.allowMapping = builder.allowMapping;
|
||||
this.portMapping = builder.portMapping;
|
||||
@@ -86,7 +86,7 @@ public class VntConfig {
|
||||
json.put("rtx", rtx);
|
||||
json.put("fec", fec);
|
||||
json.put("no_nat", noNat);
|
||||
json.put("no_tun", noTun);
|
||||
json.put("device_mode", deviceMode);
|
||||
json.put("allow_mapping", allowMapping);
|
||||
|
||||
// 数组
|
||||
@@ -135,7 +135,7 @@ public class VntConfig {
|
||||
private boolean rtx = false;
|
||||
private boolean fec = false;
|
||||
private boolean noNat = false;
|
||||
private boolean noTun = false;
|
||||
private String deviceMode = "tun";
|
||||
private Integer mtu;
|
||||
private boolean allowMapping = false;
|
||||
private List<String> portMapping = new ArrayList<>();
|
||||
@@ -258,10 +258,13 @@ public class VntConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* 无TUN模式(默认false)
|
||||
* 设置虚拟网卡模式:no、tun(默认)或 tap。
|
||||
*/
|
||||
public Builder setNoTun(boolean noTun) {
|
||||
this.noTun = noTun;
|
||||
public Builder setDeviceMode(String deviceMode) {
|
||||
if (!"no".equals(deviceMode) && !"tun".equals(deviceMode) && !"tap".equals(deviceMode)) {
|
||||
throw new IllegalArgumentException("deviceMode must be no, tun, or tap");
|
||||
}
|
||||
this.deviceMode = deviceMode;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
+17
-10
@@ -8,7 +8,7 @@ use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use tokio::runtime::Runtime;
|
||||
use vnt_core::api::VntApi;
|
||||
use vnt_core::context::config::Config;
|
||||
use vnt_core::context::config::{Config, DeviceMode};
|
||||
use vnt_core::core::{NetworkManager, RegisterResponse};
|
||||
use vnt_core::nat::NetInput;
|
||||
use vnt_core::port_mapping::PortMapping;
|
||||
@@ -269,13 +269,13 @@ pub extern "system" fn Java_com_vnt_VntNetwork_nativeStartTun(
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let tun_fd = if tun_fd < 0 { None } else { Some(tun_fd) };
|
||||
runtime.block_on(async { manager.start_tun_fd(tun_fd).await })?;
|
||||
runtime.block_on(async { manager.start_device_fd(tun_fd).await })?;
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = tun_fd; // 避免未使用警告
|
||||
runtime.block_on(async { manager.start_tun().await })?;
|
||||
runtime.block_on(async { manager.start_device().await })?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -331,7 +331,9 @@ pub extern "system" fn Java_com_vnt_VntNetwork_nativeSetNetworkIp<'local>(
|
||||
.as_ref()
|
||||
.context("Network manager already destroyed")?;
|
||||
runtime.block_on(async {
|
||||
manager.set_tun_network_ip(ip_addr, prefix_len as u8).await
|
||||
manager
|
||||
.set_device_network_ip(ip_addr, prefix_len as u8)
|
||||
.await
|
||||
})?;
|
||||
Ok(())
|
||||
})();
|
||||
@@ -410,19 +412,19 @@ pub extern "system" fn Java_com_vnt_VntNetwork_nativeIsNoTun(
|
||||
.as_ref()
|
||||
.context("Network manager already destroyed")?;
|
||||
|
||||
Ok(manager.is_no_tun())
|
||||
Ok(manager.device_mode() == DeviceMode::No)
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(is_no_tun) => {
|
||||
if is_no_tun {
|
||||
Ok(is_no_device) => {
|
||||
if is_no_device {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = env.throw(format!("Failed to check no_tun: {:?}", e));
|
||||
let _ = env.throw(format!("Failed to check device mode: {:?}", e));
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -914,7 +916,9 @@ fn parse_config_from_json(json_str: &str) -> anyhow::Result<Config> {
|
||||
#[serde(default)]
|
||||
no_nat: bool,
|
||||
#[serde(default)]
|
||||
no_tun: bool,
|
||||
device_mode: DeviceMode,
|
||||
#[serde(default, rename = "no_tun")]
|
||||
legacy_no_tun: Option<bool>,
|
||||
#[serde(default)]
|
||||
mtu: Option<u16>,
|
||||
#[serde(default)]
|
||||
@@ -930,6 +934,9 @@ fn parse_config_from_json(json_str: &str) -> anyhow::Result<Config> {
|
||||
}
|
||||
|
||||
let cfg: ConfigJson = serde_json::from_str(json_str)?;
|
||||
if cfg.legacy_no_tun.is_some() {
|
||||
anyhow::bail!("configuration key 'no_tun' was removed; use device_mode = \"no|tun|tap\"");
|
||||
}
|
||||
|
||||
let server_addrs: Vec<ProtocolAddress> = cfg
|
||||
.server
|
||||
@@ -999,7 +1006,7 @@ fn parse_config_from_json(json_str: &str) -> anyhow::Result<Config> {
|
||||
input: cfg.input,
|
||||
output: cfg.output,
|
||||
no_nat: cfg.no_nat,
|
||||
no_tun: cfg.no_tun,
|
||||
device_mode: cfg.device_mode,
|
||||
mtu: cfg.mtu,
|
||||
port_mapping,
|
||||
allow_port_mapping: cfg.allow_mapping,
|
||||
|
||||
+26
-7
@@ -18,9 +18,18 @@ fn main() {
|
||||
|
||||
// UI 源码变化时重新运行本脚本
|
||||
println!("cargo:rerun-if-changed={}", ui_dir.join("src").display());
|
||||
println!("cargo:rerun-if-changed={}", ui_dir.join("index.html").display());
|
||||
println!("cargo:rerun-if-changed={}", ui_dir.join("vite.config.js").display());
|
||||
println!("cargo:rerun-if-changed={}", ui_dir.join("package.json").display());
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
ui_dir.join("index.html").display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
ui_dir.join("vite.config.js").display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
ui_dir.join("package.json").display()
|
||||
);
|
||||
// 产物被删除时也要重新运行
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
@@ -53,9 +62,9 @@ fn main() {
|
||||
|
||||
if !ui_dir.join("node_modules").is_dir() {
|
||||
// ui 依赖使用 workspace catalog,必须在仓库根目录安装
|
||||
run_or_panic(&pnpm, &["install", "--frozen-lockfile"], workspace_root);
|
||||
run_or_panic(pnpm, &["install", "--frozen-lockfile"], workspace_root);
|
||||
}
|
||||
run_or_panic(&pnpm, &["--filter", "vnt-web-ui", "build"], workspace_root);
|
||||
run_or_panic(pnpm, &["--filter", "vnt-web-ui", "build"], workspace_root);
|
||||
}
|
||||
|
||||
/// pnpm 命令名(Windows 上是 pnpm.cmd,由 cmd.exe 执行)
|
||||
@@ -72,14 +81,24 @@ fn find_pnpm() -> Option<&'static str> {
|
||||
}
|
||||
|
||||
fn run_or_panic(program: &str, args: &[&str], dir: &Path) {
|
||||
println!("cargo:warning=执行前端构建: {} {} ({})", program, args.join(" "), dir.display());
|
||||
println!(
|
||||
"cargo:warning=执行前端构建: {} {} ({})",
|
||||
program,
|
||||
args.join(" "),
|
||||
dir.display()
|
||||
);
|
||||
let status = Command::new(program)
|
||||
.args(args)
|
||||
.current_dir(dir)
|
||||
.status()
|
||||
.unwrap_or_else(|e| panic!("执行 {} 失败: {}", program, e));
|
||||
if !status.success() {
|
||||
panic!("前端构建失败: {} {} (exit: {:?})", program, args.join(" "), status.code());
|
||||
panic!(
|
||||
"前端构建失败: {} {} (exit: {:?})",
|
||||
program,
|
||||
args.join(" "),
|
||||
status.code()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+50
-12
@@ -28,7 +28,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use vnt_core::api::VntApi;
|
||||
use vnt_core::context::config::Config as CoreConfig;
|
||||
use vnt_core::context::config::{Config as CoreConfig, DeviceMode};
|
||||
use vnt_core::core::{DEFAULT_MTU, NetworkManager, RegisterResponse};
|
||||
use vnt_core::nat::NetInput;
|
||||
use vnt_core::port_mapping::PortMapping;
|
||||
@@ -271,7 +271,9 @@ pub struct StartConfig {
|
||||
#[serde(default)]
|
||||
pub no_nat: bool,
|
||||
#[serde(default)]
|
||||
pub no_tun: bool,
|
||||
pub device_mode: DeviceMode,
|
||||
#[serde(default, rename = "no_tun", skip_serializing)]
|
||||
pub legacy_no_tun: Option<bool>,
|
||||
pub mtu: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub port_mapping: Vec<String>,
|
||||
@@ -284,6 +286,15 @@ pub struct StartConfig {
|
||||
pub tunnel_port: Option<u16>,
|
||||
}
|
||||
|
||||
impl StartConfig {
|
||||
fn reject_legacy_no_tun(&self) -> anyhow::Result<()> {
|
||||
if self.legacy_no_tun.is_some() {
|
||||
bail!("configuration key 'no_tun' was removed; use device_mode = \"no|tun|tap\"")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SaveConfigReq {
|
||||
file_name: Option<String>,
|
||||
@@ -982,19 +993,20 @@ async fn start_vnt_network(
|
||||
format!("注册成功 {}/{}", reg_msg.ip, reg_msg.prefix_len),
|
||||
);
|
||||
log::info!("Network Started: {}/{}", reg_msg.ip, reg_msg.prefix_len);
|
||||
if !network_manager.is_no_tun() {
|
||||
state.record_log(&file_name, "正在创建 TUN 虚拟网卡");
|
||||
network_manager.start_tun().await?;
|
||||
if network_manager.device_mode().has_device() {
|
||||
let mode = network_manager.device_mode();
|
||||
state.record_log(&file_name, format!("正在创建 {} 虚拟网卡", mode));
|
||||
network_manager.start_device().await?;
|
||||
|
||||
state.record_log(&file_name, "创建 TUN 虚拟网卡成功,设置 IP");
|
||||
state.record_log(&file_name, format!("创建 {} 虚拟网卡成功,设置 IP", mode));
|
||||
network_manager
|
||||
.set_tun_network_ip(reg_msg.ip, reg_msg.prefix_len)
|
||||
.set_device_network_ip(reg_msg.ip, reg_msg.prefix_len)
|
||||
.await?;
|
||||
state.record_log(&file_name, "设置 IP 成功");
|
||||
|
||||
// 配置子网路由
|
||||
if !sub_input.is_empty()
|
||||
&& let Ok(if_index) = network_manager.tun_if_index().await
|
||||
&& let Ok(if_index) = network_manager.device_if_index().await
|
||||
&& let Ok(mut route_manager) = route_manager::RouteManager::new()
|
||||
{
|
||||
state.record_log(&file_name, "配置子网路由");
|
||||
@@ -1011,6 +1023,8 @@ async fn start_vnt_network(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.record_log(&file_name, "device_mode=no,不创建虚拟网卡");
|
||||
}
|
||||
|
||||
state.starting_to_running(&file_name);
|
||||
@@ -1278,7 +1292,13 @@ async fn list_configs() -> Json<ApiResponse<Vec<ConfigSummary>>> {
|
||||
|
||||
async fn save_config(Json(req): Json<SaveConfigReq>) -> Json<ApiResponse<()>> {
|
||||
// 验证配置格式
|
||||
if let Err(e) = toml::from_str::<StartConfig>(&req.config) {
|
||||
let parsed = toml::from_str::<StartConfig>(&req.config).and_then(|config| {
|
||||
config
|
||||
.reject_legacy_no_tun()
|
||||
.map(|_| config)
|
||||
.map_err(serde::de::Error::custom)
|
||||
});
|
||||
if let Err(e) = parsed {
|
||||
log::warn!("Failed to parse configuration: {:?}", e);
|
||||
return Json(ApiResponse::error(format!("Invalid TOML format: {}", e)));
|
||||
}
|
||||
@@ -1358,6 +1378,7 @@ async fn delete_config(
|
||||
}
|
||||
|
||||
fn convert_config(cfg: StartConfig) -> anyhow::Result<CoreConfig> {
|
||||
cfg.reject_legacy_no_tun()?;
|
||||
let server_addrs: Vec<ProtocolAddress> = cfg
|
||||
.server
|
||||
.iter()
|
||||
@@ -1423,7 +1444,7 @@ fn convert_config(cfg: StartConfig) -> anyhow::Result<CoreConfig> {
|
||||
input: cfg.input,
|
||||
output: cfg.output,
|
||||
no_nat: cfg.no_nat,
|
||||
no_tun: cfg.no_tun,
|
||||
device_mode: cfg.device_mode,
|
||||
mtu: cfg.mtu,
|
||||
port_mapping,
|
||||
allow_port_mapping: cfg.allow_mapping,
|
||||
@@ -1750,8 +1771,9 @@ mod tests {
|
||||
input: Vec::new(),
|
||||
output: Vec::new(),
|
||||
no_nat: false,
|
||||
// 默认 no_tun,避免无关用例意外触发 tun_name 冲突
|
||||
no_tun: true,
|
||||
// 默认无网卡,避免无关用例意外触发 tun_name 冲突
|
||||
device_mode: DeviceMode::No,
|
||||
legacy_no_tun: None,
|
||||
mtu: None,
|
||||
port_mapping: Vec::new(),
|
||||
allow_mapping: false,
|
||||
@@ -1761,6 +1783,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_mode_config_and_legacy_rejection() {
|
||||
let base = r#"server = ["quic://127.0.0.1:29872"]
|
||||
network_code = "test"
|
||||
"#;
|
||||
let default_cfg: StartConfig = toml::from_str(base).unwrap();
|
||||
assert_eq!(default_cfg.device_mode, DeviceMode::Tun);
|
||||
|
||||
let tap_cfg: StartConfig =
|
||||
toml::from_str(&format!("{base}device_mode = \"tap\"\n")).unwrap();
|
||||
assert_eq!(tap_cfg.device_mode, DeviceMode::Tap);
|
||||
|
||||
let legacy: StartConfig = toml::from_str(&format!("{base}no_tun = true\n")).unwrap();
|
||||
assert!(legacy.reject_legacy_no_tun().is_err());
|
||||
}
|
||||
|
||||
/// 两个实例同时处于 Starting 互不影响
|
||||
#[test]
|
||||
fn test_two_instances_starting_independent() {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const emptyFormData = () => ({
|
||||
input: [],
|
||||
output: [],
|
||||
no_nat: false,
|
||||
no_tun: false,
|
||||
device_mode: "tun",
|
||||
port_mapping: [],
|
||||
allow_mapping: false,
|
||||
device_name: "",
|
||||
@@ -78,7 +78,13 @@ export const parseTomlToForm = (toml) => {
|
||||
} else if (trimmed.match(/^no_nat\s*=/)) {
|
||||
data.no_nat = trimmed.includes("true");
|
||||
} else if (trimmed.match(/^no_tun\s*=/)) {
|
||||
data.no_tun = trimmed.includes("true");
|
||||
throw new Error('配置项 no_tun 已移除,请改用 device_mode = "no|tun|tap"');
|
||||
} else if (trimmed.match(/^device_mode\s*=/)) {
|
||||
const match = trimmed.match(/device_mode\s*=\s*"([^"]*)"/);
|
||||
if (!match || !["no", "tun", "tap"].includes(match[1])) {
|
||||
throw new Error('device_mode 必须是 "no"、"tun" 或 "tap"');
|
||||
}
|
||||
data.device_mode = match[1];
|
||||
} else if (trimmed.startsWith("port_mapping")) {
|
||||
const match = trimmed.match(/port_mapping\s*=\s*\[(.*)\]/);
|
||||
if (match) {
|
||||
@@ -196,10 +202,8 @@ export const formToToml = (formData) => {
|
||||
toml += "no_nat = true\n";
|
||||
}
|
||||
|
||||
if (formData.no_tun) {
|
||||
toml += "\n# 是否关闭TUN虚拟网卡,关闭后只能充当流量出口或者进行端口映射,关闭后无需管理员权限\n";
|
||||
toml += "no_tun = true\n";
|
||||
}
|
||||
toml += "\n# 虚拟网卡模式:no(无网卡)、tun(三层网卡)、tap(二层网卡)\n";
|
||||
toml += `device_mode = "${formData.device_mode || "tun"}"\n`;
|
||||
|
||||
const portMappings = formData.port_mapping.filter((s) => s.trim());
|
||||
if (portMappings.length > 0) {
|
||||
@@ -310,8 +314,9 @@ server = ["quic://1.2.3.4:29872"]
|
||||
# 是否关闭内置子网NAT,关闭(设为true)后需要配置网卡转发,否则无法使用点对网。通常关闭内置子网NAT,使用系统的网卡转发,点对网性能会更好
|
||||
# no_nat = false
|
||||
|
||||
# 是否关闭TUN虚拟网卡,关闭(设为true)后只能充当流量出口或者进行端口映射,关闭后无需管理员权限
|
||||
# no_tun = false
|
||||
# 虚拟网卡模式:no(无网卡)、tun(三层网卡,默认)、tap(二层网卡)
|
||||
# Windows 的 tap 模式需要预先安装 tap-windows (tap0901) 驱动
|
||||
device_mode = "tun"
|
||||
|
||||
# 端口映射,格式为:协议://本地监听地址-目标虚拟IP-目标映射地址
|
||||
# 端口映射用于在本地监听指定端口,并将收到的网络流量经由指定虚拟节点转发到目标地址,从而实现跨网络或内网服务访问
|
||||
|
||||
@@ -26,10 +26,15 @@ const hasFormChanges = ref(false);
|
||||
const isParsingToml = ref(false);
|
||||
const formData = ref(emptyFormData());
|
||||
const certificateModeOptions = [
|
||||
{ value: "skip", label: "跳过验证(默认)" },
|
||||
{ value: "skip", label: "跳过验证(默认)" },
|
||||
{ value: "standard", label: "系统证书验证" },
|
||||
{ value: "finger", label: "证书指纹验证" },
|
||||
];
|
||||
const deviceModeOptions = [
|
||||
{ value: "no", label: "无虚拟网卡" },
|
||||
{ value: "tun", label: "TUN(三层)" },
|
||||
{ value: "tap", label: "TAP(二层)" },
|
||||
];
|
||||
|
||||
// 打开时加载内容
|
||||
watch(
|
||||
@@ -68,13 +73,17 @@ watch(
|
||||
// 切换到表单模式
|
||||
const switchToFormMode = () => {
|
||||
if (editMode.value === "toml") {
|
||||
editMode.value = "form";
|
||||
// 从TOML解析到表单
|
||||
isParsingToml.value = true;
|
||||
formData.value = parseTomlToForm(editorContent.value);
|
||||
nextTick(() => {
|
||||
isParsingToml.value = false;
|
||||
});
|
||||
try {
|
||||
const parsed = parseTomlToForm(editorContent.value);
|
||||
editMode.value = "form";
|
||||
isParsingToml.value = true;
|
||||
formData.value = parsed;
|
||||
nextTick(() => {
|
||||
isParsingToml.value = false;
|
||||
});
|
||||
} catch (e) {
|
||||
ui.toast.error("配置解析失败: " + e.message);
|
||||
}
|
||||
} else {
|
||||
editMode.value = "form";
|
||||
}
|
||||
@@ -440,13 +449,13 @@ const sectionTitleClass = "text-md mb-4 flex items-center font-bold text-slate-9
|
||||
</div>
|
||||
<input v-model="formData.no_nat" type="checkbox" :class="checkboxClass" />
|
||||
</label>
|
||||
<label :class="toggleLabelClass">
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3 dark:border-slate-700 dark:bg-slate-800/50">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium text-slate-800 dark:text-white">关闭TUN网卡</div>
|
||||
<div class="text-xs text-slate-400 mt-0.5">仅作流量出口或端口映射</div>
|
||||
<div class="text-sm font-medium text-slate-800 dark:text-white">虚拟网卡模式</div>
|
||||
<div class="text-xs text-slate-400 mt-0.5">TAP 为二层网卡;Windows 需要 tap-windows 驱动</div>
|
||||
</div>
|
||||
<input v-model="formData.no_tun" type="checkbox" :class="checkboxClass" />
|
||||
</label>
|
||||
<AppSelect v-model="formData.device_mode" :options="deviceModeOptions" class="mt-2" aria-label="虚拟网卡模式" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user