Compare commits

...
Author SHA1 Message Date
fanyang 025605afc0 feat: support config server short links 2026-05-23 22:09:36 +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
9 changed files with 361 additions and 46 deletions
@@ -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
+53 -24
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"
@@ -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,
}
+2
View File
@@ -5,10 +5,12 @@ core_clap:
en: |+
config server address, allow format:
full url: --config-server udp://127.0.0.1:22020/admin, 'udp' can be replaced with tcp, ws, wss (when config server ws is proxied to wss)
short link: --config-server https://example.com/easytier/admin, the HTTP(S) response should redirect to the full config server URL
only user name: --config-server admin, will use official server
zh-CN: |+
配置服务器地址。允许格式:
完整URL:--config-server udp://127.0.0.1:22020/admin,udp可以根据配置服务器替换为 tcp,ws,wss(配置服务器ws被代理为wss时)
短链接:--config-server https://example.com/easytier/admin,HTTP(S) 响应应重定向到完整配置服务器 URL
仅用户名:--config-server admin,将使用官方的服务器
machine_id:
en: |+
+71 -11
View File
@@ -78,6 +78,22 @@ impl TunnelConnector for ConfigServerConnector {
}
}
fn parse_config_server_input(config_server_url_s: &str) -> Result<Url> {
match Url::parse(config_server_url_s) {
Ok(u) => Ok(u),
Err(_) => format!(
"udp://config-server.easytier.cn:22020/{}",
config_server_url_s
)
.parse()
.with_context(|| "failed to parse config server URL"),
}
}
fn is_config_server_http_short_link(url: &Url) -> bool {
matches!(url.scheme(), "http" | "https")
}
impl WebClient {
pub fn new<T: TunnelConnector + 'static, S: ToString, H: ToString>(
connector: T,
@@ -240,15 +256,7 @@ pub async fn run_web_client(
) -> Result<WebClient> {
let machine_id = resolve_machine_id(&machine_id_opts)
.with_context(|| "failed to resolve machine id for web client")?;
let config_server_url = match Url::parse(config_server_url_s) {
Ok(u) => u,
Err(_) => format!(
"udp://config-server.easytier.cn:22020/{}",
config_server_url_s
)
.parse()
.with_context(|| "failed to parse config server URL")?,
};
let config_server_url = parse_config_server_input(config_server_url_s)?;
TunnelScheme::try_from(&config_server_url).map_err(|_| {
anyhow::anyhow!(
@@ -258,7 +266,8 @@ pub async fn run_web_client(
})?;
let mut c_url = config_server_url.clone();
if !matches!(c_url.scheme(), "ws" | "wss") {
// Keep HTTP(S) paths so HttpTunnelConnector can request short links and handle their redirects.
if !matches!(c_url.scheme(), "ws" | "wss") && !is_config_server_http_short_link(&c_url) {
c_url.set_path("");
}
let token = config_server_url
@@ -305,7 +314,15 @@ pub async fn run_web_client(
mod tests {
use std::sync::{Arc, atomic::AtomicBool};
use crate::{common::MachineIdOptions, instance_manager::NetworkInstanceManager};
use crate::{
common::{MachineIdOptions, config::TomlConfigLoader, global_ctx::GlobalCtx},
instance_manager::NetworkInstanceManager,
tunnel::TunnelConnector,
};
use tokio::{
io::{AsyncReadExt as _, AsyncWriteExt as _},
net::TcpListener,
};
#[tokio::test]
async fn test_manager_wait() {
@@ -363,4 +380,47 @@ mod tests {
assert!(!client.is_connected());
drop(client);
}
#[tokio::test]
async fn config_server_short_link_uses_existing_http_redirect_connector() {
let target_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let target_addr = target_listener.local_addr().unwrap();
let target_task = tokio::spawn(async move {
let _ = target_listener.accept().await.unwrap();
});
let http_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let http_addr = http_listener.local_addr().unwrap();
let http_task = tokio::spawn(async move {
let (mut stream, _) = http_listener.accept().await.unwrap();
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).await.unwrap();
let req = String::from_utf8_lossy(&buf[..n]).to_string();
let resp = format!(
"HTTP/1.1 302 Found\r\nLocation: tcp://{}\r\nContent-Length: 0\r\n\r\n",
target_addr
);
stream.write_all(resp.as_bytes()).await.unwrap();
req
});
let config = TomlConfigLoader::default();
let global_ctx = Arc::new(GlobalCtx::new(config));
let mut flags = global_ctx.get_flags();
flags.bind_device = false;
global_ctx.set_flags(flags);
let url: url::Url = format!("http://{}/short-token", http_addr).parse().unwrap();
let mut connector = super::ConfigServerConnector { url, global_ctx };
let tunnel = connector.connect().await.unwrap();
let req = http_task.await.unwrap();
assert!(req.starts_with("GET /short-token "));
let info = tunnel.info().unwrap();
assert_eq!(
info.resolved_remote_addr.unwrap().url,
format!("tcp://{}", target_addr)
);
target_task.await.unwrap();
}
}