mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-03 01:25:37 +00:00
feat: stabilize mobile runtime and VPN portal (#2536)
This commit is contained in:
@@ -27,8 +27,8 @@ vpn_portal_add_client: 添加客户端
|
||||
vpn_portal_no_clients: 尚未配置客户端
|
||||
vpn_portal_client_name: 客户端名称
|
||||
vpn_portal_client_name_placeholder: 例如:alice-phone
|
||||
vpn_portal_client_virtual_ip: 虚拟网地址
|
||||
vpn_portal_client_virtual_ip_placeholder: 例如:10.126.126.10
|
||||
vpn_portal_client_virtual_ip: 虚拟网 CIDR
|
||||
vpn_portal_client_virtual_ip_placeholder: 例如:10.126.126.10/24
|
||||
vpn_portal_client_groups: ACL 组
|
||||
vpn_portal_client_groups_placeholder: 选择 ACL 组
|
||||
vpn_portal_remove_client: 删除客户端
|
||||
|
||||
@@ -27,8 +27,8 @@ vpn_portal_add_client: Add Client
|
||||
vpn_portal_no_clients: No clients configured
|
||||
vpn_portal_client_name: Client Name
|
||||
vpn_portal_client_name_placeholder: "Example: alice-phone"
|
||||
vpn_portal_client_virtual_ip: Virtual Network Address
|
||||
vpn_portal_client_virtual_ip_placeholder: "Example: 10.126.126.10"
|
||||
vpn_portal_client_virtual_ip: Virtual Network CIDR
|
||||
vpn_portal_client_virtual_ip_placeholder: "Example: 10.126.126.10/24"
|
||||
vpn_portal_client_groups: ACL Groups
|
||||
vpn_portal_client_groups_placeholder: Select ACL groups
|
||||
vpn_portal_remove_client: Remove Client
|
||||
|
||||
@@ -331,7 +331,7 @@ function makeConfig(): NetworkConfig {
|
||||
wireguard_private_key: 'portal-private-key',
|
||||
clients: [{
|
||||
name: 'phone-a',
|
||||
virtual_ip: '10.1.2.10',
|
||||
virtual_ip: '10.1.2.10/24',
|
||||
groups: ['ops'],
|
||||
}],
|
||||
},
|
||||
@@ -421,7 +421,7 @@ describe('Config.vue network config projection', () => {
|
||||
expect(input(wrapper, '#vpn_portal_wireguard_listen').value).toBe('0.0.0.0:22023')
|
||||
expect(input(wrapper, '#vpn_portal_wireguard_private_key').value).toBe('portal-private-key')
|
||||
expect(input(wrapper, '#vpn_portal_client_name_0').value).toBe('phone-a')
|
||||
expect(input(wrapper, '#vpn_portal_client_virtual_ip_0').value).toBe('10.1.2.10')
|
||||
expect(input(wrapper, '#vpn_portal_client_virtual_ip_0').value).toBe('10.1.2.10/24')
|
||||
expect(input(wrapper, '#vpn_portal_client_groups_0').value).toBe('ops')
|
||||
expect(input(wrapper, '#dev_name').value).toBe('tun-test')
|
||||
expect(input(wrapper, '#mtu').value).toBe('1280')
|
||||
@@ -455,7 +455,7 @@ describe('Config.vue network config projection', () => {
|
||||
await setInput(wrapper, '#vpn_portal_wireguard_listen', '[::]:23000')
|
||||
await setInput(wrapper, '#vpn_portal_wireguard_private_key', 'edited-private-key')
|
||||
await setInput(wrapper, '#vpn_portal_client_name_0', 'laptop-a')
|
||||
await setInput(wrapper, '#vpn_portal_client_virtual_ip_0', '10.1.2.20')
|
||||
await setInput(wrapper, '#vpn_portal_client_virtual_ip_0', '10.1.2.20/24')
|
||||
await setInput(wrapper, '#vpn_portal_client_groups_0', 'ops,admin')
|
||||
await setInput(wrapper, 'input[data-add-label="add_listener_url"]', 'tcp://0.0.0.0:13010')
|
||||
await setInput(wrapper, '#dev_name', 'tun-edited')
|
||||
@@ -489,7 +489,7 @@ describe('Config.vue network config projection', () => {
|
||||
wireguard_private_key: 'edited-private-key',
|
||||
clients: [{
|
||||
name: 'laptop-a',
|
||||
virtual_ip: '10.1.2.20',
|
||||
virtual_ip: '10.1.2.20/24',
|
||||
groups: ['ops', 'admin'],
|
||||
}],
|
||||
},
|
||||
@@ -525,7 +525,7 @@ describe('Config.vue network config projection', () => {
|
||||
wireguard_private_key: 'edited-private-key',
|
||||
clients: [{
|
||||
name: 'laptop-a',
|
||||
virtual_ip: '10.1.2.20',
|
||||
virtual_ip: '10.1.2.20/24',
|
||||
groups: ['ops', 'admin'],
|
||||
}],
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user