Compare commits

...
Author SHA1 Message Date
fanyang 3b2fd5d471 fix(gateway): route local virtual IP proxy targets to loopback
Avoid proxy loops when the target is this node's virtual IP.

- Normalize local virtual IP destinations to loopback
- Apply the behavior to TCP, KCP, and QUIC proxy paths
- Add macOS utun repro tests and normalization coverage
2026-05-28 23:44:56 +08:00
fanyang 8b1a2b5e80 fix(tunnel): silence macOS BPF unsafe warnings
Make macOS BPF ioctl wrappers explicit about unsafe calls.

- Wrap libc ioctl calls in unsafe blocks
- Keep the existing error handling unchanged
2026-05-28 23:44:56 +08:00
fanyang 00957e5f9d feat: add Docker healthcheck (#2279) 2026-05-24 23:12:51 +08:00
fanyang bfa3383aaa chore: update kcp-sys (#2277) 2026-05-24 23:11:16 +08:00
fanyang 73bea01f40 fix: support env vars for easytier-web (#2280)
* fix: support env vars for easytier-web
* fix: hide sensitive web env values
2026-05-22 23:59:16 +08:00
韩嘉乐 0378191783 feat: add the management of config_store_snapshot (#2271) 2026-05-22 01:54:43 +08:00
ParkGarden d5fa6a608d fix: Magisk module incorrectly matches the lookup main rule in Android 15 (#2259)
Fix the issue where the Magisk module incorrectly matches the lookup main rule in Android 15's cellular network rules, causing data plane connectivity failure
2026-05-18 12:51:49 +08:00
14 changed files with 437 additions and 56 deletions
+3
View File
@@ -42,4 +42,7 @@ EXPOSE 11011/tcp
# wss
EXPOSE 11012/tcp
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
CMD ["/usr/local/bin/easytier-cli", "--rpc-portal", "127.0.0.1:15888", "--output", "json", "node", "info"]
ENTRYPOINT ["/sbin/tini", "--", "easytier-core"]
Generated
+1 -1
View File
@@ -4623,7 +4623,7 @@ dependencies = [
[[package]]
name = "kcp-sys"
version = "0.1.0"
source = "git+https://github.com/EasyTier/kcp-sys?rev=94964794caaed5d388463137da59b97499619e5f#94964794caaed5d388463137da59b97499619e5f"
source = "git+https://github.com/EasyTier/kcp-sys?rev=d7427c22d764deb1860a7d37acc446ed5033464c#d7427c22d764deb1860a7d37acc446ed5033464c"
dependencies = [
"anyhow",
"auto_impl",
@@ -99,7 +99,7 @@ while true; do
# 启动后的扫尾工作
if pgrep -f "${EASYTIER}" >/dev/null; then
if ! ip rule show | grep -q "lookup main"; then
if ! ip rule show | grep -qE '^[0-9]+:[[:space:]]+from all lookup main$'; then
ip rule add from all lookup main
fi
@@ -109,4 +109,4 @@ while true; do
fi
sleep 10s
done
done
+54 -25
View File
@@ -156,6 +156,17 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-lock"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
@@ -655,6 +666,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
@@ -1181,7 +1201,6 @@ dependencies = [
"derivative",
"derive_builder",
"derive_more",
"easytier-rpc-build",
"encoding",
"flume",
"forwarded-header-value",
@@ -1205,6 +1224,7 @@ dependencies = [
"itertools 0.14.0",
"kcp-sys",
"machine-uid",
"moka",
"multimap",
"natpmp",
"netlink-packet-core",
@@ -1222,6 +1242,7 @@ dependencies = [
"pin-project-lite",
"pnet",
"prefix-trie",
"proc-macro2",
"prost",
"prost-build",
"prost-reflect",
@@ -1231,6 +1252,7 @@ dependencies = [
"prost-wkt-types",
"quinn",
"quinn-plaintext",
"quote",
"rand 0.8.5",
"rcgen",
"regex",
@@ -1263,7 +1285,6 @@ dependencies = [
"tokio-util",
"tokio-websockets",
"toml",
"tonic-build",
"tracing",
"tracing-subscriber",
"tun-easytier",
@@ -1311,14 +1332,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "easytier-rpc-build"
version = "0.1.0"
dependencies = [
"heck 0.5.0",
"prost-build",
]
[[package]]
name = "either"
version = "1.15.0"
@@ -1469,6 +1482,27 @@ dependencies = [
"arrayvec",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
@@ -2561,7 +2595,7 @@ dependencies = [
[[package]]
name = "kcp-sys"
version = "0.1.0"
source = "git+https://github.com/EasyTier/kcp-sys?rev=94964794caaed5d388463137da59b97499619e5f#94964794caaed5d388463137da59b97499619e5f"
source = "git+https://github.com/EasyTier/kcp-sys?rev=d7427c22d764deb1860a7d37acc446ed5033464c#d7427c22d764deb1860a7d37acc446ed5033464c"
dependencies = [
"anyhow",
"auto_impl",
@@ -2817,9 +2851,12 @@ version = "0.12.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
dependencies = [
"async-lock",
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"event-listener",
"futures-util",
"loom",
"parking_lot",
"portable-atomic",
@@ -3236,6 +3273,12 @@ dependencies = [
"unicode-width 0.1.11",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.4"
@@ -4991,20 +5034,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tonic-build"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11"
dependencies = [
"prettyplease",
"proc-macro2",
"prost-build",
"prost-types",
"quote",
"syn 2.0.106",
]
[[package]]
name = "tower"
version = "0.5.2"
@@ -1,7 +1,7 @@
use crate::config::types::stored_config::{StoredConfigList, StoredConfigMeta};
use ohos_hilog_binding::{hilog_debug, hilog_error};
use rusqlite::{Connection, OptionalExtension, params};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -101,6 +101,110 @@ fn load_meta_record(conn: &Connection, config_id: &str) -> Option<StoredConfigMe
.flatten()
}
fn validate_snapshot_schema(conn: &Connection) -> bool {
let has_stored_configs = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'stored_configs'",
[],
|row| row.get::<_, i64>(0),
)
.optional()
.ok()
.flatten()
.is_some();
let has_stored_fields = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'stored_config_fields'",
[],
|row| row.get::<_, i64>(0),
)
.optional()
.ok()
.flatten()
.is_some();
has_stored_configs && has_stored_fields
}
fn copy_snapshot_tables(src: &Connection, dst: &mut Connection) -> rusqlite::Result<()> {
let mut meta_rows = Vec::<StoredConfigMetaRecord>::new();
{
let mut stmt = src.prepare(
"SELECT config_id, display_name, created_at, updated_at, favorite, temporary
FROM stored_configs",
)?;
let rows = stmt.query_map([], row_to_meta)?;
for row in rows {
meta_rows.push(row?);
}
}
let mut field_rows = Vec::<(String, String, String, String)>::new();
{
let mut stmt = src.prepare(
"SELECT config_id, field_name, field_json, updated_at
FROM stored_config_fields",
)?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
))
})?;
for row in rows {
field_rows.push(row?);
}
}
let tx = dst.unchecked_transaction()?;
tx.execute("DELETE FROM stored_config_fields", [])?;
tx.execute("DELETE FROM stored_configs", [])?;
for row in meta_rows {
tx.execute(
"INSERT INTO stored_configs (
config_id, display_name, created_at, updated_at, favorite, temporary
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![
row.config_id,
row.display_name,
row.created_at,
row.updated_at,
if row.favorite { 1 } else { 0 },
if row.temporary { 1 } else { 0 }
],
)?;
}
for (config_id, field_name, field_json, updated_at) in field_rows {
tx.execute(
"INSERT INTO stored_config_fields (config_id, field_name, field_json, updated_at)
VALUES (?1, ?2, ?3, ?4)",
params![config_id, field_name, field_json, updated_at],
)?;
}
tx.commit()
}
fn ensure_parent_dir(path: &Path) -> bool {
match path.parent() {
Some(parent) => match std::fs::create_dir_all(parent) {
Ok(_) => true,
Err(e) => {
hilog_error!(
"[Rust] failed to create snapshot parent {}: {}",
parent.display(),
e
);
false
}
},
None => true,
}
}
fn to_meta(record: StoredConfigMetaRecord) -> StoredConfigMeta {
StoredConfigMeta {
config_id: record.config_id,
@@ -142,6 +246,79 @@ pub fn init_config_meta_store(root_dir: String) -> bool {
true
}
pub fn export_config_store_snapshot(target_path: String) -> bool {
let target = PathBuf::from(target_path);
if !ensure_parent_dir(&target) {
return false;
}
let Some(src) = open_db() else {
return false;
};
let mut dst = match Connection::open(&target) {
Ok(conn) => conn,
Err(e) => {
hilog_error!(
"[Rust] failed to open snapshot target {}: {}",
target.display(),
e
);
return false;
}
};
if let Err(e) = init_schema(&dst) {
hilog_error!(
"[Rust] failed to init snapshot schema {}: {}",
target.display(),
e
);
return false;
}
match copy_snapshot_tables(&src, &mut dst) {
Ok(_) => true,
Err(e) => {
hilog_error!(
"[Rust] failed to export snapshot {}: {}",
target.display(),
e
);
false
}
}
}
pub fn import_config_store_snapshot(source_path: String) -> bool {
let source = PathBuf::from(source_path);
let src = match Connection::open(&source) {
Ok(conn) => conn,
Err(e) => {
hilog_error!(
"[Rust] failed to open snapshot source {}: {}",
source.display(),
e
);
return false;
}
};
if !validate_snapshot_schema(&src) {
hilog_error!("[Rust] invalid snapshot schema {}", source.display());
return false;
}
let Some(mut dst) = open_db() else {
return false;
};
match copy_snapshot_tables(&src, &mut dst) {
Ok(_) => true,
Err(e) => {
hilog_error!(
"[Rust] failed to import snapshot {}: {}",
source.display(),
e
);
false
}
}
}
pub fn list_config_meta_entries() -> StoredConfigList {
let Some(conn) = open_db() else {
return StoredConfigList { configs: vec![] };
@@ -44,3 +44,11 @@ pub(crate) fn import_toml(toml_text: String, display_name: Option<String>) -> Op
pub(crate) fn export_toml(config_id: String) -> Option<String> {
config::repository::export_config_toml(&config_id).map(|ret| ret.toml_text)
}
pub(crate) fn export_config_store_snapshot(target_path: String) -> bool {
config::storage::config_meta::export_config_store_snapshot(target_path)
}
pub(crate) fn import_config_store_snapshot(source_path: String) -> bool {
config::storage::config_meta::import_config_store_snapshot(source_path)
}
+10
View File
@@ -363,6 +363,16 @@ pub fn export_toml(config_id: String) -> Option<String> {
exports::config_api::export_toml(config_id)
}
#[napi]
pub fn export_config_store_snapshot(target_path: String) -> bool {
exports::config_api::export_config_store_snapshot(target_path)
}
#[napi]
pub fn import_config_store_snapshot(source_path: String) -> bool {
exports::config_api::import_config_store_snapshot(source_path)
}
#[napi]
pub fn start_kernel(config_id: String) -> bool {
exports::runtime_api::start_kernel(config_id, start_kernel_with_config_id)
+1
View File
@@ -53,6 +53,7 @@ clap = { version = "4.4.8", features = [
"unicode",
"derive",
"wrap_help",
"env",
] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
+36 -8
View File
@@ -41,23 +41,32 @@ rust_i18n::i18n!("locales", fallback = "en");
#[derive(Parser, Debug)]
#[command(name = "easytier-web", author, version = EASYTIER_VERSION , about, long_about = None)]
struct Cli {
#[arg(short, long, default_value = "et.db", help = t!("cli.db").to_string())]
#[arg(
short,
long,
env = "ET_WEB_DB",
default_value = "et.db",
help = t!("cli.db").to_string()
)]
db: String,
#[arg(
long,
env = "ET_WEB_CONSOLE_LOG_LEVEL",
help = t!("cli.console_log_level").to_string(),
)]
console_log_level: Option<String>,
#[arg(
long,
env = "ET_WEB_FILE_LOG_LEVEL",
help = t!("cli.file_log_level").to_string(),
)]
file_log_level: Option<String>,
#[arg(
long,
env = "ET_WEB_FILE_LOG_DIR",
help = t!("cli.file_log_dir").to_string(),
)]
file_log_dir: Option<String>,
@@ -65,6 +74,7 @@ struct Cli {
#[arg(
long,
short='c',
env = "ET_CONFIG_SERVER_PORT",
default_value = "22020",
help = t!("cli.config_server_port").to_string(),
)]
@@ -73,6 +83,7 @@ struct Cli {
#[arg(
long,
short='p',
env = "ET_CONFIG_SERVER_PROTOCOL",
default_value = "udp",
help = t!("cli.config_server_protocol").to_string(),
)]
@@ -81,6 +92,7 @@ struct Cli {
#[arg(
long,
short='a',
env = "ET_API_SERVER_PORT",
default_value = "11211",
help = t!("cli.api_server_port").to_string(),
)]
@@ -88,6 +100,7 @@ struct Cli {
#[arg(
long,
env = "ET_API_SERVER_ADDR",
default_value = "0.0.0.0",
help = t!("cli.api_server_addr").to_string(),
)]
@@ -95,6 +108,7 @@ struct Cli {
#[arg(
long,
env = "ET_GEOIP_DB",
help = t!("cli.geoip_db").to_string(),
)]
geoip_db: Option<String>,
@@ -103,6 +117,7 @@ struct Cli {
#[arg(
long,
short='l',
env = "ET_WEB_SERVER_PORT",
help = t!("cli.web_server_port").to_string(),
)]
web_server_port: Option<u16>,
@@ -110,6 +125,7 @@ struct Cli {
#[cfg(feature = "embed")]
#[arg(
long,
env = "ET_WEB_SERVER_ADDR",
default_value = "0.0.0.0",
help = t!("cli.web_server_addr").to_string(),
)]
@@ -118,6 +134,7 @@ struct Cli {
#[cfg(feature = "embed")]
#[arg(
long,
env = "ET_NO_WEB",
help = t!("cli.no_web").to_string(),
default_value = "false"
)]
@@ -126,6 +143,7 @@ struct Cli {
#[cfg(feature = "embed")]
#[arg(
long,
env = "ET_API_HOST",
help = t!("cli.api_host").to_string()
)]
api_host: Option<url::Url>,
@@ -144,35 +162,45 @@ struct Cli {
pub struct WebhookOptions {
/// Base URL of the webhook endpoint for token validation and event delivery.
/// When set, incoming tokens are validated via this webhook before local fallback.
#[arg(long)]
#[arg(long, env = "ET_WEBHOOK_URL")]
pub webhook_url: Option<String>,
/// Shared secret used to authenticate outbound webhook calls.
#[arg(long)]
#[arg(long, env = "ET_WEBHOOK_SECRET", hide_env_values = true)]
pub webhook_secret: Option<String>,
/// Token for X-Internal-Auth header. When set, API requests with this header
/// bypass session authentication.
#[arg(long)]
#[arg(long, env = "ET_INTERNAL_AUTH_TOKEN", hide_env_values = true)]
pub internal_auth_token: Option<String>,
/// Stable identifier for this easytier-web instance when routing webhook callbacks.
#[arg(long)]
#[arg(long, env = "ET_WEB_INSTANCE_ID")]
pub web_instance_id: Option<String>,
/// Reachable base URL for this easytier-web instance's internal REST API.
#[arg(long)]
#[arg(long, env = "ET_WEB_INSTANCE_API_BASE_URL")]
pub web_instance_api_base_url: Option<String>,
}
#[derive(Debug, Clone, Default, clap::Args)]
pub struct FeatureFlags {
/// Whether user registration via the web UI is disabled.
#[arg(long, default_value = "false", help = t!("cli.disable_registration").to_string())]
#[arg(
long,
env = "ET_DISABLE_REGISTRATION",
default_value = "false",
help = t!("cli.disable_registration").to_string()
)]
pub disable_registration: bool,
/// Whether to auto-create users when they connect via heartbeat with an unknown token.
#[arg(long, default_value = "false", help = t!("cli.allow_auto_create_user").to_string())]
#[arg(
long,
env = "ET_ALLOW_AUTO_CREATE_USER",
default_value = "false",
help = t!("cli.allow_auto_create_user").to_string()
)]
pub allow_auto_create_user: bool,
}
+1 -1
View File
@@ -224,7 +224,7 @@ service-manager = { git = "https://github.com/EasyTier/service-manager-rs.git",
zstd = { version = "0.13", optional = true }
kcp-sys = { git = "https://github.com/EasyTier/kcp-sys", rev = "94964794caaed5d388463137da59b97499619e5f", optional = true }
kcp-sys = { git = "https://github.com/EasyTier/kcp-sys", rev = "d7427c22d764deb1860a7d37acc446ed5033464c", optional = true }
prost-reflect = { version = "0.14.5", default-features = false, features = [
"derive",
+4 -4
View File
@@ -19,7 +19,9 @@ use tokio::task::JoinSet;
use super::{
CidrSet,
tcp_proxy::{NatDstConnector, NatDstTcpConnector, TcpProxy},
tcp_proxy::{
NatDstConnector, NatDstTcpConnector, TcpProxy, normalize_dst_for_local_virtual_ip,
},
};
use crate::utils::task::HedgeExt;
use crate::{
@@ -369,9 +371,7 @@ impl KcpProxyDst {
}
let send_to_self = global_ctx.is_ip_local_virtual_ip(&dst_ip);
if send_to_self && global_ctx.no_tun() {
dst_socket = format!("127.0.0.1:{}", dst_socket.port()).parse().unwrap();
}
dst_socket = normalize_dst_for_local_virtual_ip(&global_ctx, dst_socket);
let acl_handler = ProxyAclHandler {
acl_filter: global_ctx.get_acl_filter().clone(),
+2 -4
View File
@@ -2,7 +2,7 @@ use crate::common::PeerId;
use crate::common::acl_processor::PacketInfo;
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtx};
use crate::gateway::CidrSet;
use crate::gateway::tcp_proxy::{NatDstConnector, TcpProxy};
use crate::gateway::tcp_proxy::{NatDstConnector, TcpProxy, normalize_dst_for_local_virtual_ip};
use crate::gateway::wrapped_proxy::{ProxyAclHandler, TcpProxyForWrappedSrcTrait};
use crate::peers::PeerPacketFilter;
use crate::peers::peer_manager::PeerManager;
@@ -748,9 +748,7 @@ impl QuicStreamReceiver {
}
let send_to_self = global_ctx.is_ip_local_virtual_ip(&dst_ip);
if send_to_self && global_ctx.no_tun() {
dst_socket = format!("127.0.0.1:{}", dst_socket.port()).parse()?;
}
dst_socket = normalize_dst_for_local_virtual_ip(&global_ctx, dst_socket);
let acl_handler = ProxyAclHandler {
acl_filter: global_ctx.get_acl_filter().clone(),
+135 -8
View File
@@ -9,7 +9,7 @@ use pnet::packet::ip::IpNextHeaderProtocols;
use pnet::packet::ipv4::{Ipv4Packet, MutableIpv4Packet};
use pnet::packet::tcp::{MutableTcpPacket, TcpPacket, ipv4_checksum};
use socket2::{SockRef, TcpKeepalive};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4};
use std::sync::atomic::{AtomicBool, AtomicU16};
use std::sync::{Arc, Weak};
use std::time::{Duration, Instant};
@@ -40,6 +40,20 @@ use super::CidrSet;
#[cfg(feature = "smoltcp")]
use super::tokio_smoltcp::{self, Net, NetConfig, channel_device};
pub(crate) fn normalize_dst_for_local_virtual_ip(
global_ctx: &GlobalCtx,
dst: SocketAddr,
) -> SocketAddr {
if !global_ctx.is_ip_local_virtual_ip(&dst.ip()) {
return dst;
}
match dst {
SocketAddr::V4(addr) => SocketAddr::new(Ipv4Addr::LOCALHOST.into(), addr.port()),
SocketAddr::V6(addr) => SocketAddr::new(Ipv6Addr::LOCALHOST.into(), addr.port()),
}
}
#[async_trait::async_trait]
pub(crate) trait NatDstConnector: Send + Sync + Clone + 'static {
type DstStream: AsyncRead + AsyncWrite + Unpin + Send;
@@ -762,13 +776,7 @@ impl<C: NatDstConnector> TcpProxy<C> {
return;
}
let nat_dst = if global_ctx.is_ip_local_virtual_ip(&nat_entry.real_dst.ip()) {
format!("127.0.0.1:{}", nat_entry.real_dst.port())
.parse()
.unwrap()
} else {
nat_entry.real_dst
};
let nat_dst = normalize_dst_for_local_virtual_ip(&global_ctx, nat_entry.real_dst);
global_ctx
.stats_manager()
@@ -1033,3 +1041,122 @@ impl<C: NatDstConnector> TcpProxyRpcService<C> {
}
}
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, SocketAddr};
use super::normalize_dst_for_local_virtual_ip;
#[tokio::test]
async fn normalize_dst_for_local_virtual_ip_maps_to_loopback() {
let global_ctx = crate::common::global_ctx::tests::get_mock_global_ctx();
global_ctx.set_ipv4(Some("10.254.229.6/24".parse().unwrap()));
let local_virtual = SocketAddr::from(([10, 254, 229, 6], 22));
let normalized = normalize_dst_for_local_virtual_ip(&global_ctx, local_virtual);
assert_eq!(normalized, SocketAddr::from((Ipv4Addr::LOCALHOST, 22)));
let remote_virtual = SocketAddr::from(([10, 254, 229, 7], 22));
assert_eq!(
normalize_dst_for_local_virtual_ip(&global_ctx, remote_virtual),
remote_virtual
);
}
}
#[cfg(all(test, target_os = "macos", feature = "tun"))]
mod macos_utun_tests {
use std::{net::SocketAddr, sync::Arc, time::Duration};
use tokio::net::{TcpListener, TcpStream};
use super::{NatDstConnector as _, NatDstTcpConnector, normalize_dst_for_local_virtual_ip};
use crate::{
common::config::{ConfigLoader, TomlConfigLoader},
instance::instance::Instance,
};
async fn run_instance_with_utun(ipv4: &str, enable_kcp: bool, enable_quic: bool) -> Instance {
let config = TomlConfigLoader::default();
config.set_inst_name(format!("macos-utun-tcp-proxy-repro-{ipv4}"));
config.set_ipv4(Some(ipv4.parse().unwrap()));
config.set_ipv6(None);
config.set_listeners(Vec::new());
let mut flags = config.get_flags();
flags.enable_kcp_proxy = enable_kcp;
flags.enable_quic_proxy = enable_quic;
flags.use_smoltcp = false;
config.set_flags(flags);
let mut instance = Instance::new(config);
instance.run().await.expect(
"failed to create macOS utun device; run this ignored reproducer with root privileges",
);
instance
}
async fn assert_wildcard_listener_reachable_via_local_virtual_ip(
ipv4: &str,
enable_kcp: bool,
enable_quic: bool,
) {
let mut instance = run_instance_with_utun(ipv4, enable_kcp, enable_quic).await;
let virtual_ip = instance.get_global_ctx().get_ipv4().unwrap().address();
let listener = Arc::new(TcpListener::bind("0.0.0.0:0").await.unwrap());
let port = listener.local_addr().unwrap().port();
let baseline_listener = listener.clone();
let baseline_accept = tokio::spawn(async move { baseline_listener.accept().await });
let baseline_connect = TcpStream::connect((std::net::Ipv4Addr::LOCALHOST, port));
let (baseline_connect, baseline_accept) = tokio::join!(baseline_connect, baseline_accept);
baseline_connect.unwrap();
baseline_accept.unwrap().unwrap();
let test_listener = listener.clone();
let mut accept = tokio::spawn(async move { test_listener.accept().await });
let dst = SocketAddr::new(virtual_ip.into(), port);
let dst = normalize_dst_for_local_virtual_ip(&instance.get_global_ctx(), dst);
assert_eq!(dst.ip(), std::net::Ipv4Addr::LOCALHOST);
let connect = NatDstTcpConnector {}.connect("0.0.0.0:0".parse().unwrap(), dst);
let connect = tokio::time::timeout(Duration::from_secs(3), connect).await;
let accept_result = tokio::time::timeout(Duration::from_secs(1), &mut accept).await;
if accept_result.is_err() {
accept.abort();
}
instance.clear_resources().await;
match connect {
Ok(Ok(_)) => {}
Ok(Err(error)) => panic!("connect to local EasyTier virtual IP failed: {error:?}"),
Err(_) => panic!("connect to local EasyTier virtual IP timed out"),
}
match accept_result {
Ok(Ok(Ok(_))) => {}
Ok(Ok(Err(error))) => panic!("listener accept failed: {error:?}"),
Ok(Err(error)) => panic!("listener task failed: {error:?}"),
Err(_) => panic!(
"listener did not accept connection to local EasyTier virtual IP {virtual_ip}:{port}"
),
}
}
#[tokio::test]
#[ignore = "requires root and a real macOS utun device; covers issue #2296"]
async fn macos_utun_kcp_proxy_dst_local_virtual_ip_reaches_wildcard_listener() {
assert_wildcard_listener_reachable_via_local_virtual_ip("10.254.229.6/24", true, false)
.await;
}
#[tokio::test]
#[ignore = "requires root and a real macOS utun device; covers issue #2296"]
async fn macos_utun_quic_proxy_dst_local_virtual_ip_reaches_wildcard_listener() {
assert_wildcard_listener_reachable_via_local_virtual_ip("10.254.230.6/24", false, true)
.await;
}
}
@@ -232,7 +232,7 @@ fn iowr<T>(group: u8, num: u8) -> libc::c_ulong {
}
unsafe fn ioctl_ptr<T>(fd: libc::c_int, req: libc::c_ulong, arg: *mut T) -> io::Result<()> {
let ret = libc::ioctl(fd, req, arg);
let ret = unsafe { libc::ioctl(fd, req, arg) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
@@ -240,7 +240,7 @@ unsafe fn ioctl_ptr<T>(fd: libc::c_int, req: libc::c_ulong, arg: *mut T) -> io::
}
unsafe fn ioctl_void(fd: libc::c_int, req: libc::c_ulong) -> io::Result<()> {
let ret = libc::ioctl(fd, req);
let ret = unsafe { libc::ioctl(fd, req) };
if ret < 0 {
return Err(io::Error::last_os_error());
}