feat: stabilize mobile runtime and VPN portal (#2536)

This commit is contained in:
KKRainbow
2026-08-29 13:40:56 +08:00
committed by GitHub
parent 4a10d1c2b9
commit 25f6e2dc5e
31 changed files with 1415 additions and 1302 deletions
@@ -568,15 +568,15 @@ mod tests {
fn vpn_portal_client_changes_produce_hot_patches() {
let current = config_with_vpn_portal(
vec![
portal_client("alice", "10.144.144.4"),
portal_client("carol", "10.144.144.6"),
portal_client("alice", "10.144.144.4/24"),
portal_client("carol", "10.144.144.6/24"),
],
"0.0.0.0:22121",
);
let desired = config_with_vpn_portal(
vec![
portal_client("bob", "10.144.144.5"),
portal_client("carol", "10.144.144.7"),
portal_client("bob", "10.144.144.5/24"),
portal_client("carol", "10.144.144.7/24"),
],
"0.0.0.0:22121",
);
@@ -595,16 +595,24 @@ mod tests {
],
"removals must precede additions; changed clients are remove+add"
);
let added = patch
.vpn_portal_clients
.iter()
.filter(|item| item.action == ConfigPatchAction::Add as i32)
.filter_map(|item| item.client.as_ref())
.map(|client| client.virtual_ip.as_str())
.collect::<Vec<_>>();
assert_eq!(added, ["10.144.144.5/24", "10.144.144.7/24"]);
}
#[test]
fn vpn_portal_client_no_op_produces_empty_patch_section() {
let current = config_with_vpn_portal(
vec![portal_client("alice", "10.144.144.4")],
vec![portal_client("alice", "10.144.144.4/24")],
"0.0.0.0:22121",
);
let desired = config_with_vpn_portal(
vec![portal_client("alice", "10.144.144.4")],
vec![portal_client("alice", "10.144.144.4/24")],
"0.0.0.0:22121",
);
@@ -617,11 +625,11 @@ mod tests {
#[test]
fn vpn_portal_listener_identity_change_requires_recreate() {
let current = config_with_vpn_portal(
vec![portal_client("alice", "10.144.144.4")],
vec![portal_client("alice", "10.144.144.4/24")],
"0.0.0.0:22121",
);
let desired = config_with_vpn_portal(
vec![portal_client("alice", "10.144.144.4")],
vec![portal_client("alice", "10.144.144.4/24")],
"0.0.0.0:22122",
);
assert!(
@@ -652,7 +660,7 @@ mod tests {
fn vpn_portal_enable_or_disable_requires_recreate() {
let without_portal = config_with_port_forwards(Vec::new());
let with_portal = config_with_vpn_portal(
vec![portal_client("alice", "10.144.144.4")],
vec![portal_client("alice", "10.144.144.4/24")],
"0.0.0.0:22121",
);
@@ -1087,19 +1087,33 @@ async fn mark_config_revision_applied_if_current(
let Some(data) = session_data.upgrade() else {
return RoundStatus::Stop;
};
let mut data = data.write().await;
if !SessionRpcService::runtime_heartbeat_is_current_locked(&data, &round.req) {
return RoundStatus::Ready(());
let notify = {
let mut data = data.write().await;
if !SessionRpcService::runtime_heartbeat_is_current_locked(&data, &round.req) {
return RoundStatus::Ready(());
}
if data.runtime_config_epoch != round.runtime_config_epoch {
return RoundStatus::Ready(());
}
record_applied_config_revision(&mut data, round.target_config_revision.clone())
};
if let Some(notify) = notify {
notify.notify_one();
}
if data.runtime_config_epoch != round.runtime_config_epoch {
return RoundStatus::Ready(());
}
data.applied_config_revision = round.target_config_revision.clone();
data.pending_managed_config_delta = None;
RoundStatus::Ready(())
}
fn record_applied_config_revision(
data: &mut SessionData,
revision: Option<String>,
) -> Option<std::sync::Arc<tokio::sync::Notify>> {
let changed = data.applied_config_revision != revision;
data.applied_config_revision = revision;
data.pending_managed_config_delta = None;
changed.then(|| SessionRpcService::mark_webhook_validation_dirty_locked(data))
}
#[cfg(test)]
mod tests {
use easytier::proto::api::manage::{NetworkingMethod, PortForwardConfig};
@@ -1128,6 +1142,52 @@ mod tests {
}
}
#[tokio::test]
async fn newly_applied_revision_wakes_webhook_validation() {
let storage =
crate::client_manager::storage::Storage::new(crate::db::Db::memory_db().await);
let mut data = SessionData::new(
storage.weak_ref(),
url::Url::parse("http://127.0.0.1").unwrap(),
None,
std::sync::Arc::new(crate::FeatureFlags::default()),
std::sync::Arc::new(crate::webhook::WebhookConfig::new(
None, None, None, None, None,
)),
);
let notify = record_applied_config_revision(&mut data, Some("rev-applied".to_string()))
.expect("new applied revision should wake validation");
assert_eq!(data.applied_config_revision.as_deref(), Some("rev-applied"));
assert!(data.webhook_validation_dirty);
notify.notify_one();
tokio::time::timeout(std::time::Duration::from_millis(100), notify.notified())
.await
.expect("validation worker was not notified");
}
#[tokio::test]
async fn unchanged_applied_revision_does_not_add_validation_work() {
let storage =
crate::client_manager::storage::Storage::new(crate::db::Db::memory_db().await);
let mut data = SessionData::new(
storage.weak_ref(),
url::Url::parse("http://127.0.0.1").unwrap(),
None,
std::sync::Arc::new(crate::FeatureFlags::default()),
std::sync::Arc::new(crate::webhook::WebhookConfig::new(
None, None, None, None, None,
)),
);
data.applied_config_revision = Some("rev-applied".to_string());
assert!(
record_applied_config_revision(&mut data, Some("rev-applied".to_string())).is_none()
);
assert!(!data.webhook_validation_dirty);
}
#[test]
fn patch_delete_requires_runtime_to_remove_every_requested_instance() {
let deleted_id = uuid::Uuid::new_v4();