feat: add TAP device mode and Ethernet interoperability

This commit is contained in:
lbl
2026-08-22 21:47:49 +08:00
parent 26a44e64bd
commit de0183139d
26 changed files with 1073 additions and 139 deletions
+11 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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())
+7 -2
View File
@@ -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 必须保持不变"
);
+64 -1
View File
@@ -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
View File
@@ -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;
+1 -1
View File
@@ -74,7 +74,7 @@ impl PacketCrypto {
/// AAD 承担"认证"职责:覆盖传输中不变、但不参与 nonce 的头部字节
/// byte0(msg_type)/byte2(flags)/byte3(reserved)。
/// msg_type 与 flagsCOMPRESSED/FEC/GATEWAY)只由发送方设置、
/// msg_type 与 flagsCOMPRESSED/FEC/GATEWAY/ETHERNET)只由发送方设置、
/// 传输中不会被修改,必须纳入认证,否则中间人可翻转造成不可检测的
/// 丢包/语义篡改;ttl(byte1) 在中继转发时会递减,不能纳入 AAD。
fn make_aad<B: AsRef<[u8]>>(pkt: &NetPacket<B>) -> [u8; 3] {
+52 -4
View File
@@ -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();
+5 -1
View File
@@ -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| {
+72
View File
@@ -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);
+231
View File
@@ -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(&ETHERTYPE_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(&ETHERTYPE_VLAN.to_be_bytes());
frame[16..18].copy_from_slice(&ETHERTYPE_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(&ETHERTYPE_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(&ETHERTYPE_ARP.to_be_bytes());
request[14..16].copy_from_slice(&1u16.to_be_bytes());
request[16..18].copy_from_slice(&ETHERTYPE_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);
}
}
+12
View File
@@ -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() {
+1
View File
@@ -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;
+22 -2
View File
@@ -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 减 1curr_ttl >= 1 时才继续转发,
/// 接收方以 metric = max_ttl - curr_ttl 计算路由距离。
#[test]
+119 -3
View File
@@ -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
View File
@@ -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(())
}
+44 -6
View File
@@ -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();
}
+85
View File
@@ -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)
+1 -1
View File
@@ -51,7 +51,7 @@ $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD=""
pnpm build:desktop
```
Windows 使用虚拟网卡模式时,可能需要以管理员身份运行。
Windows 使用虚拟网卡模式时,可能需要以管理员身份运行TAP(二层)模式还需要预先安装 `tap-windows``tap0901`)驱动,内置的 `wintun.dll` 只用于 TUN(三层)模式
### 发布桌面更新
+10 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 -8
View File
@@ -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-目标映射地址
# 端口映射用于在本地监听指定端口,并将收到的网络流量经由指定虚拟节点转发到目标地址,从而实现跨网络或内网服务访问
+22 -13
View File
@@ -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>