mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-26 08:00:12 +00:00
Add a native EasyTier proof-of-concept binary with TCP and UDP transports, TUN, UDP hole punching, AES-GCM, and a read-only RPC portal. Introduce a release-derived mini profile and musl linker policy so x86_64, big-endian MIPS, and little-endian MIPS stay below the strict 5,000,000-byte target without UPX.
2512 lines
88 KiB
Rust
2512 lines
88 KiB
Rust
use std::sync::Arc;
|
|
|
|
use async_trait::async_trait;
|
|
|
|
use super::*;
|
|
use crate::{
|
|
config::toml::ConfigLoader as _,
|
|
listener::transport::TransportListenerConfig,
|
|
socket::{
|
|
SocketContext, SocketListener,
|
|
udp::{UdpSessionAcceptKind, UdpSessionProtocol},
|
|
},
|
|
};
|
|
|
|
struct TestServerProtocol;
|
|
|
|
#[async_trait]
|
|
impl ServerProtocolUpgrader<()> for TestServerProtocol {
|
|
fn supports_scheme(&self, scheme: &str) -> bool {
|
|
matches!(scheme, "ws" | "wss" | "wg" | "quic" | "faketcp" | "unix")
|
|
}
|
|
|
|
async fn upgrade_tcp(
|
|
&self,
|
|
_socket: (),
|
|
_local_url: Url,
|
|
) -> anyhow::Result<crate::connectivity::protocol::ServerProtocolUpgrade> {
|
|
unreachable!()
|
|
}
|
|
|
|
async fn upgrade_udp(
|
|
&self,
|
|
_session: crate::socket::udp::UdpSession,
|
|
_local_url: Url,
|
|
_admission: Option<crate::connectivity::protocol::ServerProtocolAdmission>,
|
|
) -> anyhow::Result<crate::connectivity::protocol::ServerProtocolUpgrade> {
|
|
unreachable!()
|
|
}
|
|
|
|
async fn upgrade_byte_stream(
|
|
&self,
|
|
_socket: (),
|
|
_local_url: Url,
|
|
_remote_url: Option<Url>,
|
|
) -> anyhow::Result<crate::connectivity::protocol::ServerProtocolUpgrade> {
|
|
unreachable!()
|
|
}
|
|
}
|
|
|
|
struct TestExternalListenerFactory;
|
|
|
|
impl ExternalListenerFactory<()> for TestExternalListenerFactory {
|
|
fn supports_scheme(&self, scheme: &str) -> bool {
|
|
matches!(scheme, "faketcp" | "unix")
|
|
}
|
|
|
|
fn create(&self, _request: ExternalListenerRequest) -> Box<dyn SocketListener<Accepted = ()>> {
|
|
unreachable!()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_updates_retain_core_owned_peer_identity() {
|
|
let mut snapshot = Arc::new(PeerRuntimeSnapshot::default());
|
|
Arc::make_mut(&mut snapshot).runtime.core.node.peer_id = Some(17);
|
|
Arc::make_mut(&mut snapshot).runtime.core.node.instance_id = Some([1; 16]);
|
|
let submitted = snapshot.clone();
|
|
|
|
retain_core_peer_identity(&mut snapshot, 23, Some([2; 16]));
|
|
|
|
assert_eq!(snapshot.runtime.core.node.peer_id, Some(23));
|
|
assert_eq!(snapshot.runtime.core.node.instance_id, Some([2; 16]));
|
|
assert_eq!(submitted.runtime.core.node.peer_id, Some(17));
|
|
assert_eq!(submitted.runtime.core.node.instance_id, Some([1; 16]));
|
|
}
|
|
|
|
#[test]
|
|
fn core_plans_transport_and_external_listener_capabilities() {
|
|
let self_id = uuid::Uuid::new_v4();
|
|
let config = ListenerRuntimeConfig::new(
|
|
[
|
|
"tcp://127.0.0.1:1",
|
|
"udp://127.0.0.1:2",
|
|
"ws://127.0.0.1:3",
|
|
"wg://127.0.0.1:4",
|
|
"quic://127.0.0.1:5",
|
|
"faketcp://127.0.0.1:6",
|
|
"unix:///tmp/easytier-test",
|
|
"http://127.0.0.1:7",
|
|
]
|
|
.into_iter()
|
|
.map(str::parse)
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.unwrap(),
|
|
false,
|
|
SocketContext::default().with_socket_mark(Some(7)),
|
|
);
|
|
|
|
let plan = prepare_listener_plan::<(), ()>(
|
|
Some(&config),
|
|
self_id,
|
|
Some(&TestServerProtocol),
|
|
Some(&TestExternalListenerFactory),
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(plan.transports.len(), 6);
|
|
assert_eq!(plan.external.len(), 2);
|
|
assert_eq!(plan.failures.len(), 1);
|
|
assert_eq!(
|
|
plan.transports[0].url(),
|
|
&crate::listener::plan::ring_listener_url(self_id)
|
|
);
|
|
assert!(matches!(
|
|
&plan.transports[4],
|
|
TransportListenerConfig::Udp {
|
|
accept_kind: UdpSessionAcceptKind::Classified(UdpSessionProtocol::WireGuard),
|
|
..
|
|
}
|
|
));
|
|
assert!(matches!(
|
|
&plan.transports[5],
|
|
TransportListenerConfig::Udp {
|
|
accept_kind: UdpSessionAcceptKind::Classified(UdpSessionProtocol::Quic),
|
|
..
|
|
}
|
|
));
|
|
assert_eq!(plan.external[0].0.url.scheme(), "faketcp");
|
|
assert_eq!(plan.external[0].1.socket_mark, Some(7));
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_protocol_listener_becomes_a_plan_failure() {
|
|
let config = ListenerRuntimeConfig::new(
|
|
vec!["wg://127.0.0.1:11011".parse().unwrap()],
|
|
false,
|
|
SocketContext::default(),
|
|
);
|
|
|
|
let plan =
|
|
prepare_listener_plan::<(), ()>(Some(&config), uuid::Uuid::new_v4(), None, None).unwrap();
|
|
|
|
assert_eq!(plan.transports.len(), 1);
|
|
assert!(plan.external.is_empty());
|
|
assert_eq!(plan.failures.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn raw_unix_listener_does_not_require_a_server_protocol() {
|
|
let config = ListenerRuntimeConfig::new(
|
|
vec!["unix:///tmp/easytier-test".parse().unwrap()],
|
|
false,
|
|
SocketContext::default(),
|
|
);
|
|
|
|
let plan = prepare_listener_plan::<(), ()>(
|
|
Some(&config),
|
|
uuid::Uuid::new_v4(),
|
|
None,
|
|
Some(&TestExternalListenerFactory),
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(plan.transports.len(), 1);
|
|
assert_eq!(plan.external.len(), 1);
|
|
assert!(plan.failures.is_empty());
|
|
assert_eq!(plan.external[0].0.url.scheme(), "unix");
|
|
}
|
|
|
|
#[test]
|
|
fn core_instance_config_round_trips_as_normalized_json() {
|
|
let mut core = crate::config::CoreConfig::default();
|
|
core.peer_policy.encryption_required = false;
|
|
core.peer_policy.p2p_enabled = false;
|
|
let peer = crate::peers::peer_manager::PortablePeerManagerConfig::new(
|
|
crate::config::peers::PeerRuntimeConfig {
|
|
core,
|
|
network_identity: crate::config::NetworkIdentity {
|
|
network_name: "default".to_owned(),
|
|
network_secret: Some("test".to_owned()),
|
|
network_secret_digest: None,
|
|
},
|
|
stun_info: crate::proto::common::StunInfo::default(),
|
|
feature_flags: crate::proto::common::PeerFeatureFlag::default(),
|
|
secure_mode: None,
|
|
host_routing: crate::config::peers::HostRoutingPolicy::default(),
|
|
},
|
|
);
|
|
let config = CoreInstanceConfig {
|
|
instance_name: String::new(),
|
|
peer,
|
|
connectivity: CoreConnectivityConfig::default(),
|
|
};
|
|
|
|
let mut config = config;
|
|
config.connectivity.direct.testing = true;
|
|
let encoded = serde_json::to_value(&config).unwrap();
|
|
assert!(encoded["connectivity"]["direct"].get("testing").is_none());
|
|
let decoded: CoreInstanceConfig = serde_json::from_value(encoded.clone()).unwrap();
|
|
|
|
assert!(!decoded.connectivity.direct.testing);
|
|
assert!(decoded.connectivity.startup_plan.gateway);
|
|
assert_eq!(serde_json::to_value(&decoded).unwrap(), encoded);
|
|
|
|
let mut legacy = encoded;
|
|
legacy.as_object_mut().unwrap().remove("instance_name");
|
|
let decoded: CoreInstanceConfig = serde_json::from_value(legacy).unwrap();
|
|
assert_eq!(decoded.instance_name, "default");
|
|
}
|
|
|
|
#[test]
|
|
fn wasi_create_config_uses_shared_toml() {
|
|
let fixture = include_bytes!("../../testdata/wasi_core_instance_create.json");
|
|
let mut create =
|
|
serde_json::from_slice::<crate::wasi::schema::WasiCoreInstanceCreateConfig>(fixture)
|
|
.unwrap();
|
|
create.validate().unwrap();
|
|
let config = create.parse_config().unwrap();
|
|
let normalized = CoreInstanceConfig::from_toml(&config).unwrap();
|
|
|
|
assert_eq!(
|
|
normalized.peer.snapshot.runtime.core.node.instance_id,
|
|
Some(*config.get_id().as_bytes())
|
|
);
|
|
assert!(normalized.peer.snapshot.flags.disable_p2p);
|
|
create.version += 1;
|
|
assert!(create.validate().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn core_instance_config_validation_rejects_invalid_acl_whitelist() {
|
|
let peer = crate::peers::peer_manager::PortablePeerManagerConfig::new(
|
|
crate::config::peers::PeerRuntimeConfig {
|
|
core: crate::config::CoreConfig::default(),
|
|
network_identity: crate::config::NetworkIdentity {
|
|
network_name: "default".to_owned(),
|
|
network_secret: Some("test".to_owned()),
|
|
network_secret_digest: None,
|
|
},
|
|
stun_info: crate::proto::common::StunInfo::default(),
|
|
feature_flags: crate::proto::common::PeerFeatureFlag::default(),
|
|
secure_mode: None,
|
|
host_routing: crate::config::peers::HostRoutingPolicy::default(),
|
|
},
|
|
);
|
|
let mut config = CoreInstanceConfig {
|
|
instance_name: String::new(),
|
|
peer,
|
|
connectivity: CoreConnectivityConfig::default(),
|
|
};
|
|
config.connectivity.runtime.acl.tcp_whitelist = vec!["9000-8000".to_owned()];
|
|
|
|
let error = validate_core_instance_config(&config).unwrap_err();
|
|
|
|
assert!(error.to_string().contains("Start port must be <= end port"));
|
|
}
|
|
|
|
mod portable_runtime {
|
|
use std::{
|
|
net::{IpAddr, Ipv6Addr, SocketAddr},
|
|
sync::{
|
|
Arc,
|
|
atomic::{AtomicBool, AtomicUsize, Ordering},
|
|
},
|
|
time::Duration,
|
|
};
|
|
|
|
use tokio::sync::Notify;
|
|
use tokio_util::task::AbortOnDropHandle;
|
|
|
|
#[cfg(feature = "proxy-packet")]
|
|
use std::sync::Mutex as StdMutex;
|
|
|
|
use super::*;
|
|
use crate::{
|
|
config::peers::{HostRoutingPolicy, PeerRuntimeConfig},
|
|
config::runtime::CoreInstanceRuntimeConfig,
|
|
config::{
|
|
CoreConfig, IpPrefix, NetworkIdentity, ProxyNetworkConfig, gateway::PortForwardConfig,
|
|
},
|
|
connectivity::manual::{ManualConnectorHost, ManualInterfaceAddrs},
|
|
gateway::proxy::wrapped_transport::{
|
|
WrappedTransportEngine, WrappedTransportEngineStart, WrappedTransportEngines,
|
|
WrappedTransportRole,
|
|
},
|
|
host::testkit::{TestDns, TestHost, TestTcpSocket},
|
|
listener::transport::AcceptedTransport,
|
|
peers::peer_manager::PortablePeerManagerConfig,
|
|
proto::{common::StunInfo, peer_rpc::GetIpListResponse},
|
|
socket::{SocketContext, udp::PreferredIpv6Source},
|
|
};
|
|
|
|
#[cfg(feature = "proxy-packet")]
|
|
use crate::gateway::proxy::wrapped_transport::WrappedTransportKind;
|
|
|
|
#[async_trait]
|
|
impl ManualConnectorHost for TestHost {
|
|
async fn local_addr_for_remote(
|
|
&self,
|
|
remote_addr: SocketAddr,
|
|
_context: SocketContext,
|
|
) -> anyhow::Result<SocketAddr> {
|
|
Ok(match remote_addr {
|
|
SocketAddr::V4(_) => "127.0.0.1:0".parse().unwrap(),
|
|
SocketAddr::V6(_) => "[::1]:0".parse().unwrap(),
|
|
})
|
|
}
|
|
|
|
async fn interface_addrs(&self) -> anyhow::Result<ManualInterfaceAddrs> {
|
|
Ok(ManualInterfaceAddrs {
|
|
interface_ipv4s: vec![],
|
|
interface_ipv6s: vec![],
|
|
public_ipv6: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl DirectConnectorHost for TestHost {
|
|
async fn collect_ip_addrs(&self, _context: &SocketContext) -> GetIpListResponse {
|
|
GetIpListResponse::default()
|
|
}
|
|
|
|
fn mapped_listeners(&self) -> Vec<Url> {
|
|
Vec::new()
|
|
}
|
|
|
|
fn is_local_ip(&self, _ip: &IpAddr) -> bool {
|
|
false
|
|
}
|
|
|
|
async fn preferred_ipv6_source(
|
|
&self,
|
|
_ip: Ipv6Addr,
|
|
_context: SocketContext,
|
|
) -> Option<PreferredIpv6Source> {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn test_config(network_name: &str) -> CoreInstanceConfig {
|
|
let mut core = CoreConfig::default();
|
|
core.node.network_name = network_name.to_owned();
|
|
core.peer_policy.encryption_required = false;
|
|
let peer = PortablePeerManagerConfig::new(PeerRuntimeConfig {
|
|
core,
|
|
network_identity: NetworkIdentity {
|
|
network_name: network_name.to_owned(),
|
|
network_secret: Some(String::new()),
|
|
network_secret_digest: None,
|
|
},
|
|
stun_info: StunInfo::default(),
|
|
feature_flags: Default::default(),
|
|
secure_mode: None,
|
|
host_routing: HostRoutingPolicy::default(),
|
|
});
|
|
let connectivity = CoreConnectivityConfig::default();
|
|
CoreInstanceConfig {
|
|
instance_name: network_name.to_owned(),
|
|
peer,
|
|
connectivity,
|
|
}
|
|
}
|
|
|
|
fn runtime_snapshot(config: &CoreInstanceConfig) -> CoreInstanceRuntimeConfig {
|
|
CoreInstanceRuntimeConfig {
|
|
services: config.connectivity.runtime.clone(),
|
|
peer: Arc::new(config.peer.snapshot.clone()),
|
|
}
|
|
}
|
|
|
|
fn proxy_network(real: &str, mapped: Option<&str>) -> ProxyNetworkConfig {
|
|
fn prefix(value: &str) -> IpPrefix {
|
|
let (address, prefix_len) = value.split_once('/').unwrap();
|
|
IpPrefix {
|
|
address: address.parse().unwrap(),
|
|
prefix_len: prefix_len.parse().unwrap(),
|
|
}
|
|
}
|
|
|
|
ProxyNetworkConfig {
|
|
real: prefix(real),
|
|
mapped: mapped.map(prefix),
|
|
}
|
|
}
|
|
|
|
fn adapters(
|
|
external_listener_factory: Option<
|
|
Arc<dyn ExternalListenerFactory<AcceptedTransport<TestTcpSocket>>>,
|
|
>,
|
|
packet_sink: Arc<dyn PacketSink>,
|
|
) -> CoreHostAdapters<TestHost> {
|
|
adapters_with_process_runtime(
|
|
external_listener_factory,
|
|
packet_sink,
|
|
CoreProcessRuntime::new(),
|
|
)
|
|
}
|
|
|
|
fn adapters_with_process_runtime(
|
|
external_listener_factory: Option<
|
|
Arc<dyn ExternalListenerFactory<AcceptedTransport<TestTcpSocket>>>,
|
|
>,
|
|
packet_sink: Arc<dyn PacketSink>,
|
|
process_runtime: Arc<CoreProcessRuntime>,
|
|
) -> CoreHostAdapters<TestHost> {
|
|
adapters_with_host_and_process_runtime(
|
|
Arc::new(TestHost::default()),
|
|
external_listener_factory,
|
|
packet_sink,
|
|
process_runtime,
|
|
)
|
|
}
|
|
|
|
#[cfg(any(feature = "proxy-packet", feature = "proxy-smoltcp-stack"))]
|
|
fn adapters_with_host(
|
|
host: Arc<TestHost>,
|
|
external_listener_factory: Option<
|
|
Arc<dyn ExternalListenerFactory<AcceptedTransport<TestTcpSocket>>>,
|
|
>,
|
|
packet_sink: Arc<dyn PacketSink>,
|
|
) -> CoreHostAdapters<TestHost> {
|
|
adapters_with_host_and_process_runtime(
|
|
host,
|
|
external_listener_factory,
|
|
packet_sink,
|
|
CoreProcessRuntime::new(),
|
|
)
|
|
}
|
|
|
|
fn adapters_with_host_and_process_runtime(
|
|
host: Arc<TestHost>,
|
|
external_listener_factory: Option<
|
|
Arc<dyn ExternalListenerFactory<AcceptedTransport<TestTcpSocket>>>,
|
|
>,
|
|
packet_sink: Arc<dyn PacketSink>,
|
|
process_runtime: Arc<CoreProcessRuntime>,
|
|
) -> CoreHostAdapters<TestHost> {
|
|
let dns = Arc::new(TestDns);
|
|
let mut adapters = CoreHostAdapters::new(host, dns, packet_sink, process_runtime);
|
|
adapters.external_listener_factory = external_listener_factory;
|
|
adapters
|
|
}
|
|
|
|
fn build_with_engines(
|
|
config: CoreInstanceConfig,
|
|
engines: WrappedTransportEngines,
|
|
) -> anyhow::Result<Arc<CoreInstance<TestHost>>> {
|
|
build_with_engines_and_listener(config, engines, None)
|
|
}
|
|
|
|
fn build_with_engines_and_listener(
|
|
config: CoreInstanceConfig,
|
|
engines: WrappedTransportEngines,
|
|
external_listener_factory: Option<
|
|
Arc<dyn ExternalListenerFactory<AcceptedTransport<TestTcpSocket>>>,
|
|
>,
|
|
) -> anyhow::Result<Arc<CoreInstance<TestHost>>> {
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
let mut adapters = adapters(external_listener_factory, Arc::new(packet_sink));
|
|
adapters.wrapped_transports = engines;
|
|
CoreInstance::new(config, adapters)
|
|
}
|
|
|
|
fn build_instance(config: CoreInstanceConfig) -> anyhow::Result<Arc<CoreInstance<TestHost>>> {
|
|
build_with_engines(config, WrappedTransportEngines::default())
|
|
}
|
|
|
|
#[cfg(feature = "dhcp-ipv4")]
|
|
#[tokio::test]
|
|
async fn runtime_update_preserves_dhcp_owned_ipv4() {
|
|
let mut initial = test_config("dhcp-runtime-update");
|
|
initial.connectivity.runtime.dhcp_ipv4 = true;
|
|
let instance = build_instance(initial).unwrap();
|
|
let lease = IpPrefix {
|
|
address: "10.126.126.7".parse().unwrap(),
|
|
prefix_len: 24,
|
|
};
|
|
instance.runtime_config.update_peer_with(|peer| {
|
|
peer.runtime.core.routes.ipv4 = Some(lease.clone());
|
|
});
|
|
|
|
let mut replacement = test_config("dhcp-runtime-update");
|
|
replacement.connectivity.runtime.dhcp_ipv4 = true;
|
|
instance
|
|
.update_runtime_config(runtime_snapshot(&replacement))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
instance
|
|
.runtime_config
|
|
.snapshot()
|
|
.peer
|
|
.runtime
|
|
.core
|
|
.routes
|
|
.ipv4,
|
|
Some(lease)
|
|
);
|
|
|
|
let static_replacement = test_config("dhcp-runtime-update");
|
|
instance
|
|
.update_runtime_config(runtime_snapshot(&static_replacement))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
instance
|
|
.runtime_config
|
|
.snapshot()
|
|
.peer
|
|
.runtime
|
|
.core
|
|
.routes
|
|
.ipv4,
|
|
None
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn core_instance_is_a_direct_managed_record() {
|
|
let instance = build_instance(test_config("managed-directly")).unwrap();
|
|
|
|
assert_eq!(instance.instance_name(), "managed-directly");
|
|
assert_eq!(
|
|
instance.instance_id(),
|
|
crate::instance::manager::ManagedInstance::instance_id(instance.as_ref())
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn instance_start_ignores_configured_peer_id() {
|
|
let mut config = test_config("fresh-peer-id");
|
|
let instance_id = uuid::Uuid::from_bytes([7; 16]);
|
|
config.peer.snapshot.runtime.core.node.instance_id = Some(*instance_id.as_bytes());
|
|
config.peer.snapshot.runtime.core.node.peer_id = Some(0);
|
|
|
|
let instance = build_instance(config).unwrap();
|
|
let generated_peer_id = instance.peer_id();
|
|
|
|
assert_eq!(instance.instance_id(), instance_id);
|
|
assert_ne!(generated_peer_id, 0);
|
|
assert_eq!(
|
|
instance
|
|
.runtime_config
|
|
.snapshot()
|
|
.peer
|
|
.runtime
|
|
.core
|
|
.node
|
|
.peer_id,
|
|
Some(generated_peer_id)
|
|
);
|
|
}
|
|
|
|
#[cfg(all(feature = "proxy-packet", feature = "management"))]
|
|
#[tokio::test]
|
|
async fn process_management_rpc_resolves_and_calls_core_instance_directly() {
|
|
use crate::{
|
|
config::toml::TomlConfig,
|
|
instance::manager::{InstanceFactory, InstanceManager},
|
|
management::InstanceManagementRpc,
|
|
};
|
|
use easytier_proto::{
|
|
api::config::{ConfigRpc, GetConfigRequest, InstanceConfigPatch, PatchConfigRequest},
|
|
api::instance::{
|
|
PeerManageRpc, ShowNodeInfoRequest,
|
|
instance_identifier::{InstanceSelector, Selector},
|
|
},
|
|
rpc_types::controller::BaseController,
|
|
};
|
|
|
|
struct ManagementTestFactory;
|
|
|
|
impl InstanceFactory for ManagementTestFactory {
|
|
type Instance = CoreInstance<TestHost>;
|
|
type CreateContext = ();
|
|
type Error = anyhow::Error;
|
|
|
|
fn create(
|
|
&self,
|
|
config: TomlConfig,
|
|
(): Self::CreateContext,
|
|
) -> Result<Arc<Self::Instance>, Self::Error> {
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
let mut adapters = adapters(None, Arc::new(packet_sink));
|
|
adapters.config.force_exit_node = true;
|
|
adapters.config.public_ipv6_provider_supported = true;
|
|
adapters.config.easytier_version = "host-version".to_owned();
|
|
CoreInstance::from_toml(config, adapters)
|
|
}
|
|
}
|
|
|
|
let manager = Arc::new(InstanceManager::new(ManagementTestFactory, None));
|
|
let config = TomlConfig::new_from_str(
|
|
r#"
|
|
instance_name = "managed-by-name"
|
|
hostname = "core-owned-config"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let instance = manager.create(config, ()).unwrap();
|
|
instance.start().await.unwrap();
|
|
let rpc = InstanceManagementRpc::<ManagementTestFactory>::new(manager);
|
|
|
|
let response = rpc
|
|
.show_node_info(
|
|
BaseController::default(),
|
|
ShowNodeInfoRequest {
|
|
instance: Some(easytier_proto::api::instance::InstanceIdentifier {
|
|
selector: Some(Selector::InstanceSelector(InstanceSelector {
|
|
name: Some("managed-by-name".to_owned()),
|
|
})),
|
|
}),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let node = response.node_info.unwrap();
|
|
assert_eq!(node.hostname, "core-owned-config");
|
|
assert!(node.config.contains("instance_name = \"managed-by-name\""));
|
|
|
|
let selector = || easytier_proto::api::instance::InstanceIdentifier {
|
|
selector: Some(Selector::InstanceSelector(InstanceSelector {
|
|
name: Some("managed-by-name".to_owned()),
|
|
})),
|
|
};
|
|
rpc.patch_config(
|
|
BaseController::default(),
|
|
PatchConfigRequest {
|
|
patch: Some(InstanceConfigPatch {
|
|
hostname: Some("patched-in-core".to_owned()),
|
|
..Default::default()
|
|
}),
|
|
instance: Some(selector()),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let response = rpc
|
|
.get_config(
|
|
BaseController::default(),
|
|
GetConfigRequest {
|
|
instance: Some(selector()),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
response.config.unwrap().hostname.as_deref(),
|
|
Some("patched-in-core")
|
|
);
|
|
let runtime = instance.runtime_config.snapshot();
|
|
assert!(runtime.services.proxy.enable_exit_node);
|
|
assert!(runtime.services.public_ipv6_provider.provider_supported);
|
|
assert_eq!(runtime.peer.easytier_version, "host-version");
|
|
}
|
|
|
|
#[cfg(feature = "management")]
|
|
#[tokio::test]
|
|
async fn config_patch_rejects_instance_before_host_startup_is_complete() {
|
|
let instance = build_instance(test_config("not-ready-for-patch")).unwrap();
|
|
|
|
let error = crate::management::apply_config_patch(
|
|
&instance,
|
|
easytier_proto::api::config::InstanceConfigPatch {
|
|
hostname: Some("too-early".to_owned()),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
.unwrap_err();
|
|
|
|
assert!(error.to_string().contains("instance is not ready"));
|
|
}
|
|
|
|
#[cfg(all(feature = "management", not(feature = "proxy-smoltcp-stack")))]
|
|
#[tokio::test]
|
|
async fn unavailable_gateway_patch_does_not_commit_shared_toml() {
|
|
use easytier_proto::{
|
|
api::config::{ConfigPatchAction, InstanceConfigPatch, PortForwardPatch},
|
|
common::{PortForwardConfigPb, SocketType},
|
|
};
|
|
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
let instance = CoreInstance::from_toml(
|
|
crate::config::toml::TomlConfig::new_from_str(
|
|
"instance_name = \"rejected-gateway-patch\"",
|
|
)
|
|
.unwrap(),
|
|
adapters(None, Arc::new(packet_sink)),
|
|
)
|
|
.unwrap();
|
|
instance.start().await.unwrap();
|
|
|
|
let error = crate::management::apply_config_patch(
|
|
&instance,
|
|
InstanceConfigPatch {
|
|
port_forwards: vec![PortForwardPatch {
|
|
action: ConfigPatchAction::Add as i32,
|
|
cfg: Some(PortForwardConfigPb {
|
|
bind_addr: Some(
|
|
"127.0.0.1:18080"
|
|
.parse::<std::net::SocketAddr>()
|
|
.unwrap()
|
|
.into(),
|
|
),
|
|
dst_addr: Some(
|
|
"10.144.144.2:8080"
|
|
.parse::<std::net::SocketAddr>()
|
|
.unwrap()
|
|
.into(),
|
|
),
|
|
socket_type: SocketType::Tcp as i32,
|
|
}),
|
|
}],
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
.unwrap_err();
|
|
|
|
assert!(
|
|
error
|
|
.to_string()
|
|
.contains("does not include the smoltcp gateway"),
|
|
"unexpected patch error: {error:#}"
|
|
);
|
|
assert!(
|
|
instance
|
|
.toml_config()
|
|
.unwrap()
|
|
.get_port_forwards()
|
|
.is_empty()
|
|
);
|
|
assert!(
|
|
instance
|
|
.runtime_config
|
|
.snapshot()
|
|
.services
|
|
.gateway
|
|
.port_forwards
|
|
.is_empty()
|
|
);
|
|
instance.stop().await;
|
|
}
|
|
|
|
#[cfg(feature = "web-client")]
|
|
#[tokio::test]
|
|
async fn ignored_gateway_patch_remains_in_toml_config() {
|
|
use easytier_proto::{
|
|
api::config::{ConfigPatchAction, InstanceConfigPatch, PortForwardPatch, UrlPatch},
|
|
common::{PortForwardConfigPb, SocketType},
|
|
};
|
|
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
let toml_config = crate::config::toml::TomlConfig::new_from_str(
|
|
"instance_name = \"ignored-gateway-patch\"",
|
|
)
|
|
.unwrap();
|
|
let mut host_adapters = adapters(None, Arc::new(packet_sink));
|
|
host_adapters.config.ignore_unsupported_config = true;
|
|
host_adapters.config.gateway_enabled = false;
|
|
host_adapters.config.endpoint_protocols = vec!["tcp".to_owned(), "udp".to_owned()];
|
|
let instance = CoreInstance::from_toml(toml_config, host_adapters).unwrap();
|
|
instance.start().await.unwrap();
|
|
|
|
crate::management::apply_config_patch(
|
|
&instance,
|
|
InstanceConfigPatch {
|
|
port_forwards: vec![PortForwardPatch {
|
|
action: ConfigPatchAction::Add as i32,
|
|
cfg: Some(PortForwardConfigPb {
|
|
bind_addr: Some(
|
|
"127.0.0.1:18080"
|
|
.parse::<std::net::SocketAddr>()
|
|
.unwrap()
|
|
.into(),
|
|
),
|
|
dst_addr: Some(
|
|
"10.144.144.2:8080"
|
|
.parse::<std::net::SocketAddr>()
|
|
.unwrap()
|
|
.into(),
|
|
),
|
|
socket_type: SocketType::Tcp as i32,
|
|
}),
|
|
}],
|
|
connectors: vec![UrlPatch {
|
|
action: ConfigPatchAction::Add as i32,
|
|
url: Some("quic://127.0.0.1:11010".parse::<url::Url>().unwrap().into()),
|
|
}],
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(instance.toml_config().unwrap().get_port_forwards().len(), 1);
|
|
assert!(
|
|
instance
|
|
.runtime_config
|
|
.snapshot()
|
|
.services
|
|
.gateway
|
|
.port_forwards
|
|
.is_empty()
|
|
);
|
|
assert!(instance.list_connectors().is_empty());
|
|
instance.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn dropping_core_instance_requests_host_shutdown() {
|
|
struct DropAwareRuntimeHost(Arc<AtomicBool>);
|
|
|
|
#[async_trait]
|
|
impl InstanceRuntimeHost for DropAwareRuntimeHost {
|
|
async fn prepare(
|
|
&self,
|
|
_packet_plane: Arc<CorePacketPlane>,
|
|
) -> anyhow::Result<Option<Arc<dyn DhcpIpv4Host>>> {
|
|
Ok(None)
|
|
}
|
|
|
|
async fn shutdown(&self) {}
|
|
|
|
fn request_shutdown(&self) {
|
|
self.0.store(true, Ordering::Release);
|
|
}
|
|
}
|
|
|
|
let shutdown_requested = Arc::new(AtomicBool::new(false));
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
let mut adapters = adapters(None, Arc::new(packet_sink));
|
|
adapters.instance_runtime = Arc::new(DropAwareRuntimeHost(shutdown_requested.clone()));
|
|
let instance = CoreInstance::new(test_config("drop-cleanup"), adapters).unwrap();
|
|
|
|
drop(instance);
|
|
|
|
assert!(shutdown_requested.load(Ordering::Acquire));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn host_prepare_failure_runs_unified_cleanup() {
|
|
#[derive(Default)]
|
|
struct FailingRuntimeHost {
|
|
shutdown_calls: AtomicUsize,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl InstanceRuntimeHost for FailingRuntimeHost {
|
|
async fn prepare(
|
|
&self,
|
|
_packet_plane: Arc<CorePacketPlane>,
|
|
) -> anyhow::Result<Option<Arc<dyn DhcpIpv4Host>>> {
|
|
anyhow::bail!("host prepare failed")
|
|
}
|
|
|
|
async fn shutdown(&self) {
|
|
self.shutdown_calls.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
let runtime_host = Arc::new(FailingRuntimeHost::default());
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
let mut adapters = adapters(None, Arc::new(packet_sink));
|
|
adapters.instance_runtime = runtime_host.clone();
|
|
let instance = CoreInstance::new(test_config("host-prepare-failure"), adapters).unwrap();
|
|
|
|
let error = instance.start().await.unwrap_err();
|
|
|
|
assert!(error.to_string().contains("host prepare failed"));
|
|
assert_eq!(instance.state(), CoreInstanceState::Stopped);
|
|
assert!(!instance.is_ready());
|
|
assert_eq!(runtime_host.shutdown_calls.load(Ordering::Relaxed), 1);
|
|
assert!(
|
|
instance
|
|
.latest_error()
|
|
.unwrap()
|
|
.contains("host prepare failed")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn aborting_host_prepare_runs_unified_cleanup() {
|
|
#[derive(Default)]
|
|
struct BlockingPrepareRuntimeHost {
|
|
prepare_started: Notify,
|
|
prepare_release: Notify,
|
|
shutdown_calls: AtomicUsize,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl InstanceRuntimeHost for BlockingPrepareRuntimeHost {
|
|
async fn prepare(
|
|
&self,
|
|
_packet_plane: Arc<CorePacketPlane>,
|
|
) -> anyhow::Result<Option<Arc<dyn DhcpIpv4Host>>> {
|
|
self.prepare_started.notify_one();
|
|
self.prepare_release.notified().await;
|
|
Ok(None)
|
|
}
|
|
|
|
async fn shutdown(&self) {
|
|
self.shutdown_calls.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
let runtime_host = Arc::new(BlockingPrepareRuntimeHost::default());
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
let mut adapters = adapters(None, Arc::new(packet_sink));
|
|
adapters.instance_runtime = runtime_host.clone();
|
|
let instance = CoreInstance::new(test_config("aborted-host-prepare"), adapters).unwrap();
|
|
let start = tokio::spawn({
|
|
let instance = instance.clone();
|
|
async move { instance.start().await }
|
|
});
|
|
runtime_host.prepare_started.notified().await;
|
|
|
|
start.abort();
|
|
assert!(start.await.unwrap_err().is_cancelled());
|
|
tokio::time::timeout(Duration::from_secs(2), async {
|
|
while instance.state() != CoreInstanceState::Stopped {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("aborted Host prepare should stop the instance");
|
|
|
|
assert!(!instance.is_ready());
|
|
assert_eq!(runtime_host.shutdown_calls.load(Ordering::Relaxed), 1);
|
|
}
|
|
|
|
#[cfg(feature = "management")]
|
|
#[tokio::test]
|
|
async fn cancelled_delete_finishes_stop_and_keeps_wait_blocked() {
|
|
use std::{path::Path, path::PathBuf};
|
|
|
|
use crate::{
|
|
config::toml::TomlConfig,
|
|
instance::manager::InstanceFactory,
|
|
management::{
|
|
ConfigFileControl, ConfigFilePermission, ConfigFileStorage, InstanceManager,
|
|
InstanceMutationHooks, ProcessManagementRpc,
|
|
},
|
|
};
|
|
use easytier_proto::{
|
|
api::manage::{DeleteNetworkInstanceRequest, WebClientService},
|
|
rpc_types::controller::BaseController,
|
|
};
|
|
|
|
#[derive(Default)]
|
|
struct BlockingRuntimeHost {
|
|
shutdown_started: Notify,
|
|
shutdown_release: Notify,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl InstanceRuntimeHost for BlockingRuntimeHost {
|
|
async fn prepare(
|
|
&self,
|
|
_packet_plane: Arc<CorePacketPlane>,
|
|
) -> anyhow::Result<Option<Arc<dyn DhcpIpv4Host>>> {
|
|
Ok(None)
|
|
}
|
|
|
|
async fn shutdown(&self) {
|
|
self.shutdown_started.notify_one();
|
|
self.shutdown_release.notified().await;
|
|
}
|
|
}
|
|
|
|
struct BlockingFactory {
|
|
runtime_host: Arc<BlockingRuntimeHost>,
|
|
process_runtime: Arc<CoreProcessRuntime>,
|
|
}
|
|
|
|
impl InstanceFactory for BlockingFactory {
|
|
type Instance = CoreInstance<TestHost>;
|
|
type CreateContext = ();
|
|
type Error = anyhow::Error;
|
|
|
|
fn create(
|
|
&self,
|
|
config: TomlConfig,
|
|
(): Self::CreateContext,
|
|
) -> Result<Arc<Self::Instance>, Self::Error> {
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
let mut adapters = adapters_with_process_runtime(
|
|
None,
|
|
Arc::new(packet_sink),
|
|
self.process_runtime.clone(),
|
|
);
|
|
adapters.instance_runtime = self.runtime_host.clone();
|
|
CoreInstance::from_toml(config, adapters)
|
|
}
|
|
}
|
|
|
|
impl crate::management::ProcessRuntimeProvider for BlockingFactory {
|
|
fn process_runtime(&self) -> Arc<CoreProcessRuntime> {
|
|
self.process_runtime.clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct RecordingStorage(AtomicBool);
|
|
|
|
#[async_trait]
|
|
impl ConfigFileStorage for RecordingStorage {
|
|
async fn inspect(&self, path: &Path) -> ConfigFileControl {
|
|
ConfigFileControl::new(Some(path.to_owned()), ConfigFilePermission::default())
|
|
}
|
|
|
|
async fn read(&self, _path: &Path) -> anyhow::Result<Option<Vec<u8>>> {
|
|
Ok(None)
|
|
}
|
|
|
|
async fn write(&self, _path: &Path, _contents: &[u8]) -> anyhow::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn remove(&self, _path: &Path) -> anyhow::Result<()> {
|
|
self.0.store(true, Ordering::Release);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct RecordingHooks(AtomicBool);
|
|
|
|
#[async_trait]
|
|
impl InstanceMutationHooks for RecordingHooks {
|
|
async fn post_remove_network_instances(
|
|
&self,
|
|
_instance_ids: &[uuid::Uuid],
|
|
) -> Result<(), String> {
|
|
self.0.store(true, Ordering::Release);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
let runtime_host = Arc::new(BlockingRuntimeHost::default());
|
|
let process_runtime = CoreProcessRuntime::new();
|
|
let instances = Arc::new(InstanceManager::new(
|
|
BlockingFactory {
|
|
runtime_host: runtime_host.clone(),
|
|
process_runtime,
|
|
},
|
|
Some(tokio::runtime::Handle::current()),
|
|
));
|
|
let config = TomlConfig::default();
|
|
config.set_listeners(Vec::new());
|
|
let instance_id = config.get_id();
|
|
instances
|
|
.run_network_instance(
|
|
config,
|
|
ConfigFileControl::new(
|
|
Some(PathBuf::from("cancelled-delete.toml")),
|
|
ConfigFilePermission::default(),
|
|
),
|
|
)
|
|
.unwrap();
|
|
let storage = Arc::new(RecordingStorage::default());
|
|
let hooks = Arc::new(RecordingHooks::default());
|
|
let rpc = ProcessManagementRpc::<BlockingFactory>::new(
|
|
instances.clone(),
|
|
hooks.clone(),
|
|
storage.clone(),
|
|
);
|
|
|
|
let deletion = tokio::spawn(async move {
|
|
rpc.delete_network_instance(
|
|
BaseController::default(),
|
|
DeleteNetworkInstanceRequest {
|
|
inst_ids: vec![instance_id.into()],
|
|
},
|
|
)
|
|
.await
|
|
});
|
|
runtime_host.shutdown_started.notified().await;
|
|
deletion.abort();
|
|
assert!(deletion.await.unwrap_err().is_cancelled());
|
|
|
|
let mut wait = tokio::spawn({
|
|
let instances = instances.clone();
|
|
async move { instances.wait().await }
|
|
});
|
|
assert!(
|
|
tokio::time::timeout(Duration::from_millis(20), &mut wait)
|
|
.await
|
|
.is_err()
|
|
);
|
|
|
|
runtime_host.shutdown_release.notify_one();
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
|
while !storage.0.load(Ordering::Acquire) {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.unwrap();
|
|
tokio::time::timeout(Duration::from_secs(1), wait)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert!(hooks.0.load(Ordering::Acquire));
|
|
assert!(instances.instances().is_empty());
|
|
}
|
|
|
|
#[cfg(feature = "web-client")]
|
|
#[tokio::test]
|
|
async fn process_management_rpc_owns_instance_create_list_and_delete() {
|
|
use crate::{
|
|
config::toml::TomlConfig,
|
|
instance::manager::InstanceFactory,
|
|
management::{
|
|
InstanceManager, ProcessManagementRpc, UnsupportedConfigFileStorage,
|
|
register_web_client_rpc,
|
|
},
|
|
rpc::service_registry::ServiceRegistry,
|
|
};
|
|
use easytier_proto::{
|
|
api::manage::{
|
|
DeleteNetworkInstanceRequest, ListNetworkInstanceRequest, NetworkConfig,
|
|
NetworkingMethod, RunNetworkInstanceRequest, WebClientService,
|
|
},
|
|
common::RpcDescriptor,
|
|
rpc_types::controller::BaseController,
|
|
};
|
|
|
|
struct ManagementTestFactory(Arc<CoreProcessRuntime>);
|
|
|
|
impl InstanceFactory for ManagementTestFactory {
|
|
type Instance = CoreInstance<TestHost>;
|
|
type CreateContext = ();
|
|
type Error = anyhow::Error;
|
|
|
|
fn create(
|
|
&self,
|
|
config: TomlConfig,
|
|
(): Self::CreateContext,
|
|
) -> Result<Arc<Self::Instance>, Self::Error> {
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
CoreInstance::from_toml(
|
|
config,
|
|
adapters_with_process_runtime(None, Arc::new(packet_sink), self.0.clone()),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl crate::management::ProcessRuntimeProvider for ManagementTestFactory {
|
|
fn process_runtime(&self) -> Arc<CoreProcessRuntime> {
|
|
self.0.clone()
|
|
}
|
|
}
|
|
|
|
let instances = Arc::new(InstanceManager::new(
|
|
ManagementTestFactory(CoreProcessRuntime::new()),
|
|
Some(tokio::runtime::Handle::current()),
|
|
));
|
|
let registry = ServiceRegistry::new();
|
|
register_web_client_rpc(
|
|
instances.clone(),
|
|
®istry,
|
|
Arc::new(()),
|
|
Arc::new(UnsupportedConfigFileStorage),
|
|
);
|
|
assert_eq!(
|
|
registry.get_method_name(&RpcDescriptor {
|
|
domain_name: String::new(),
|
|
service_name: "ConfigRpc".to_owned(),
|
|
proto_name: "ConfigRpc".to_owned(),
|
|
method_index: 2,
|
|
}),
|
|
Some("get_config".to_owned())
|
|
);
|
|
assert_eq!(
|
|
registry.get_method_name(&RpcDescriptor {
|
|
domain_name: String::new(),
|
|
service_name: "WebClientService".to_owned(),
|
|
proto_name: "WebClientService".to_owned(),
|
|
method_index: 1,
|
|
}),
|
|
Some("validate_config".to_owned())
|
|
);
|
|
let rpc = ProcessManagementRpc::<ManagementTestFactory>::new(
|
|
instances.clone(),
|
|
Arc::new(()),
|
|
Arc::new(UnsupportedConfigFileStorage),
|
|
);
|
|
let created = rpc
|
|
.run_network_instance(
|
|
BaseController::default(),
|
|
RunNetworkInstanceRequest {
|
|
config: Some(NetworkConfig {
|
|
network_name: Some("managed-process-rpc".to_owned()),
|
|
networking_method: Some(NetworkingMethod::Standalone.into()),
|
|
..Default::default()
|
|
}),
|
|
overwrite: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.inst_id
|
|
.unwrap();
|
|
|
|
let listed = rpc
|
|
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(listed.inst_ids, vec![created]);
|
|
|
|
let deleted = rpc
|
|
.delete_network_instance(
|
|
BaseController::default(),
|
|
DeleteNetworkInstanceRequest {
|
|
inst_ids: vec![created],
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert!(deleted.remain_inst_ids.is_empty());
|
|
assert!(instances.instances().is_empty());
|
|
}
|
|
|
|
#[cfg(feature = "management")]
|
|
#[tokio::test]
|
|
async fn owned_selection_and_cleanup_share_the_canonical_transaction() {
|
|
use crate::{
|
|
config::toml::TomlConfig,
|
|
instance::manager::InstanceFactory,
|
|
management::{
|
|
ConfigFileControl, InstanceManager, InstanceMutationHooks, ProcessManagement,
|
|
UnsupportedConfigFileStorage,
|
|
},
|
|
};
|
|
|
|
struct ManagementTestFactory(Arc<CoreProcessRuntime>);
|
|
|
|
impl InstanceFactory for ManagementTestFactory {
|
|
type Instance = CoreInstance<TestHost>;
|
|
type CreateContext = ();
|
|
type Error = anyhow::Error;
|
|
|
|
fn create(
|
|
&self,
|
|
config: TomlConfig,
|
|
(): Self::CreateContext,
|
|
) -> Result<Arc<Self::Instance>, Self::Error> {
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
CoreInstance::from_toml(
|
|
config,
|
|
adapters_with_process_runtime(None, Arc::new(packet_sink), self.0.clone()),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl crate::management::ProcessRuntimeProvider for ManagementTestFactory {
|
|
fn process_runtime(&self) -> Arc<CoreProcessRuntime> {
|
|
self.0.clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct BlockingRemovalHook {
|
|
entered: Notify,
|
|
release: Notify,
|
|
removed: std::sync::Mutex<Vec<Vec<uuid::Uuid>>>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl InstanceMutationHooks for BlockingRemovalHook {
|
|
async fn post_remove_network_instances(
|
|
&self,
|
|
instance_ids: &[uuid::Uuid],
|
|
) -> Result<(), String> {
|
|
self.removed.lock().unwrap().push(instance_ids.to_vec());
|
|
self.entered.notify_one();
|
|
self.release.notified().await;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
let instances = Arc::new(InstanceManager::new(
|
|
ManagementTestFactory(CoreProcessRuntime::new()),
|
|
Some(tokio::runtime::Handle::current()),
|
|
));
|
|
let config = TomlConfig::default();
|
|
config.set_listeners(Vec::new());
|
|
let instance_id = config.get_id();
|
|
instances
|
|
.run_network_instance(config, ConfigFileControl::STATIC_CONFIG)
|
|
.unwrap();
|
|
let hooks = Arc::new(BlockingRemovalHook::default());
|
|
let management = ProcessManagement::<ManagementTestFactory>::new(
|
|
instances.clone(),
|
|
hooks.clone(),
|
|
Arc::new(UnsupportedConfigFileStorage),
|
|
);
|
|
|
|
let deletion_management = management.clone();
|
|
let deletion = tokio::spawn(async move {
|
|
deletion_management
|
|
.delete_owned_network_instances(vec![instance_id, uuid::Uuid::new_v4()])
|
|
.await
|
|
});
|
|
hooks.entered.notified().await;
|
|
assert!(instances.mutation_lock().try_lock().is_err());
|
|
assert_eq!(
|
|
hooks.removed.lock().unwrap().as_slice(),
|
|
&[vec![instance_id]]
|
|
);
|
|
hooks.release.notify_one();
|
|
|
|
let result = deletion.await.unwrap().unwrap();
|
|
assert_eq!(result.removed_instance_ids, vec![instance_id]);
|
|
assert_eq!(hooks.removed.lock().unwrap().len(), 1);
|
|
|
|
let old_name = format!("old-{instance_id}");
|
|
let new_name = format!("new-{instance_id}");
|
|
let old_config = TomlConfig::default();
|
|
old_config.set_id(instance_id);
|
|
old_config.set_inst_name(old_name.clone());
|
|
old_config.set_listeners(Vec::new());
|
|
instances
|
|
.run_network_instance(old_config, ConfigFileControl::STATIC_CONFIG)
|
|
.unwrap();
|
|
|
|
let mutation_guard = instances.mutation_lock().lock_owned().await;
|
|
let deletion = management.delete_owned_network_instances_by_name(vec![old_name]);
|
|
tokio::pin!(deletion);
|
|
assert!(matches!(
|
|
futures::poll!(deletion.as_mut()),
|
|
std::task::Poll::Pending
|
|
));
|
|
|
|
instances
|
|
.delete_network_instances([instance_id])
|
|
.await
|
|
.unwrap();
|
|
let new_config = TomlConfig::default();
|
|
new_config.set_id(instance_id);
|
|
new_config.set_inst_name(new_name.clone());
|
|
new_config.set_listeners(Vec::new());
|
|
instances
|
|
.run_network_instance(new_config, ConfigFileControl::STATIC_CONFIG)
|
|
.unwrap();
|
|
|
|
drop(mutation_guard);
|
|
hooks.release.notify_one();
|
|
let result = deletion.await.unwrap();
|
|
assert!(result.removed_instance_ids.is_empty());
|
|
assert_eq!(
|
|
instances
|
|
.instance(instance_id)
|
|
.map(|instance| instance.instance_name().to_owned())
|
|
.as_deref(),
|
|
Some(new_name.as_str())
|
|
);
|
|
assert_eq!(
|
|
hooks.removed.lock().unwrap().as_slice(),
|
|
&[vec![instance_id], Vec::new()]
|
|
);
|
|
instances
|
|
.delete_network_instances([instance_id])
|
|
.await
|
|
.unwrap();
|
|
|
|
let selection_ran = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
let selection_ran_for_call = selection_ran.clone();
|
|
let mutation_guard = instances.mutation_lock().lock_owned().await;
|
|
let deletion = management.delete_owned_network_instances_selected_by(move || {
|
|
selection_ran_for_call.store(true, std::sync::atomic::Ordering::Release);
|
|
Vec::new()
|
|
});
|
|
tokio::pin!(deletion);
|
|
assert!(matches!(
|
|
futures::poll!(deletion.as_mut()),
|
|
std::task::Poll::Pending
|
|
));
|
|
assert!(!selection_ran.load(std::sync::atomic::Ordering::Acquire));
|
|
drop(mutation_guard);
|
|
hooks.release.notify_one();
|
|
deletion.await.unwrap();
|
|
assert!(selection_ran.load(std::sync::atomic::Ordering::Acquire));
|
|
}
|
|
|
|
#[cfg(feature = "management")]
|
|
#[tokio::test]
|
|
async fn process_management_rpc_rolls_back_instance_and_file_on_hook_failure() {
|
|
use std::{
|
|
collections::HashMap,
|
|
path::{Path, PathBuf},
|
|
sync::Mutex as StdMutex,
|
|
};
|
|
|
|
use crate::{
|
|
config::toml::TomlConfig,
|
|
instance::manager::InstanceFactory,
|
|
management::{
|
|
ConfigFileControl, ConfigFilePermission, ConfigFileStorage, InstanceManager,
|
|
InstanceMutationHooks, ProcessManagementRpc,
|
|
},
|
|
};
|
|
use easytier_proto::{
|
|
api::manage::{
|
|
NetworkConfig, NetworkingMethod, RunNetworkInstanceRequest, WebClientService,
|
|
},
|
|
rpc_types::controller::BaseController,
|
|
};
|
|
|
|
struct ManagementTestFactory(Arc<CoreProcessRuntime>);
|
|
|
|
impl InstanceFactory for ManagementTestFactory {
|
|
type Instance = CoreInstance<TestHost>;
|
|
type CreateContext = ();
|
|
type Error = anyhow::Error;
|
|
|
|
fn create(
|
|
&self,
|
|
config: TomlConfig,
|
|
(): Self::CreateContext,
|
|
) -> Result<Arc<Self::Instance>, Self::Error> {
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
CoreInstance::from_toml(
|
|
config,
|
|
adapters_with_process_runtime(None, Arc::new(packet_sink), self.0.clone()),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl crate::management::ProcessRuntimeProvider for ManagementTestFactory {
|
|
fn process_runtime(&self) -> Arc<CoreProcessRuntime> {
|
|
self.0.clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct MemoryStorage(StdMutex<HashMap<PathBuf, Vec<u8>>>);
|
|
|
|
#[async_trait]
|
|
impl ConfigFileStorage for MemoryStorage {
|
|
async fn inspect(&self, path: &Path) -> ConfigFileControl {
|
|
ConfigFileControl::new(Some(path.to_owned()), ConfigFilePermission::default())
|
|
}
|
|
|
|
async fn read(&self, path: &Path) -> anyhow::Result<Option<Vec<u8>>> {
|
|
Ok(self.0.lock().unwrap().get(path).cloned())
|
|
}
|
|
|
|
async fn write(&self, path: &Path, contents: &[u8]) -> anyhow::Result<()> {
|
|
self.0
|
|
.lock()
|
|
.unwrap()
|
|
.insert(path.to_owned(), contents.to_vec());
|
|
Ok(())
|
|
}
|
|
|
|
async fn remove(&self, path: &Path) -> anyhow::Result<()> {
|
|
self.0.lock().unwrap().remove(path);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct RejectPostRun;
|
|
|
|
#[async_trait]
|
|
impl InstanceMutationHooks for RejectPostRun {
|
|
fn manages_remote_config_instances(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
async fn post_run_network_instance(
|
|
&self,
|
|
_instance_id: &uuid::Uuid,
|
|
) -> Result<(), String> {
|
|
Err("rejected for rollback test".to_owned())
|
|
}
|
|
}
|
|
|
|
let instances = Arc::new(InstanceManager::new(
|
|
ManagementTestFactory(CoreProcessRuntime::new()),
|
|
Some(tokio::runtime::Handle::current()),
|
|
));
|
|
let storage = Arc::new(MemoryStorage::default());
|
|
let instance_id = uuid::Uuid::new_v4();
|
|
let config_path = PathBuf::from("managed-process-rpc.toml");
|
|
let original_file = b"original configuration".to_vec();
|
|
storage
|
|
.0
|
|
.lock()
|
|
.unwrap()
|
|
.insert(config_path.clone(), original_file.clone());
|
|
let original = TomlConfig::default();
|
|
original.set_id(instance_id);
|
|
original.set_inst_name("original-instance".to_owned());
|
|
original.set_listeners(Vec::new());
|
|
instances
|
|
.run_network_instance(
|
|
original,
|
|
ConfigFileControl::new(Some(config_path.clone()), ConfigFilePermission::default()),
|
|
)
|
|
.unwrap();
|
|
let rpc = ProcessManagementRpc::<ManagementTestFactory>::new(
|
|
instances.clone(),
|
|
Arc::new(RejectPostRun),
|
|
storage.clone(),
|
|
);
|
|
|
|
let result = rpc
|
|
.run_network_instance(
|
|
BaseController::default(),
|
|
RunNetworkInstanceRequest {
|
|
inst_id: Some(instance_id.into()),
|
|
config: Some(NetworkConfig {
|
|
network_name: Some("replacement".to_owned()),
|
|
networking_method: Some(NetworkingMethod::Standalone.into()),
|
|
..Default::default()
|
|
}),
|
|
overwrite: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
assert_eq!(
|
|
instances
|
|
.instance(instance_id)
|
|
.map(|instance| instance.instance_name().to_owned())
|
|
.as_deref(),
|
|
Some("original-instance")
|
|
);
|
|
assert_eq!(
|
|
storage.0.lock().unwrap().get(&config_path),
|
|
Some(&original_file)
|
|
);
|
|
instances
|
|
.delete_network_instances([instance_id])
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn packet_plane_does_not_retain_core_instance() {
|
|
let instance = build_instance(test_config("packet-plane-ownership")).unwrap();
|
|
let weak = Arc::downgrade(&instance);
|
|
let packet_plane = instance.packet_plane();
|
|
|
|
drop(instance);
|
|
|
|
assert!(weak.upgrade().is_none());
|
|
drop(packet_plane);
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct RecordingProxyService {
|
|
start_calls: AtomicUsize,
|
|
stop_calls: AtomicUsize,
|
|
start_gate: Option<Arc<ProxyStartGate>>,
|
|
#[cfg(feature = "proxy-packet")]
|
|
destination_ingress: StdMutex<
|
|
Option<crate::gateway::proxy::wrapped_transport::WrappedTransportDestinationIngress>,
|
|
>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct ProxyStartGate {
|
|
entered: Notify,
|
|
release: Notify,
|
|
}
|
|
|
|
impl RecordingProxyService {
|
|
fn blocking() -> (Arc<Self>, Arc<ProxyStartGate>) {
|
|
let gate = Arc::new(ProxyStartGate::default());
|
|
(
|
|
Arc::new(Self {
|
|
start_gate: Some(gate.clone()),
|
|
..Default::default()
|
|
}),
|
|
gate,
|
|
)
|
|
}
|
|
|
|
#[cfg(feature = "proxy-packet")]
|
|
fn destination_ingress(
|
|
&self,
|
|
) -> Option<crate::gateway::proxy::wrapped_transport::WrappedTransportDestinationIngress>
|
|
{
|
|
self.destination_ingress.lock().unwrap().clone()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl WrappedTransportEngine for RecordingProxyService {
|
|
async fn prepare(&self, options: WrappedTransportEngineStart) -> anyhow::Result<()> {
|
|
self.start_calls.fetch_add(1, Ordering::Relaxed);
|
|
#[cfg(feature = "proxy-packet")]
|
|
{
|
|
*self.destination_ingress.lock().unwrap() = options.destination_ingress;
|
|
}
|
|
#[cfg(not(feature = "proxy-packet"))]
|
|
let _ = options;
|
|
if let Some(gate) = &self.start_gate {
|
|
gate.entered.notify_one();
|
|
gate.release.notified().await;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn activate(&self) -> anyhow::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn inject_peer_datagram(
|
|
&self,
|
|
_role: WrappedTransportRole,
|
|
_from_peer_id: u32,
|
|
_payload: bytes::Bytes,
|
|
) -> anyhow::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "proxy-packet")]
|
|
async fn connect_source(
|
|
&self,
|
|
_request: crate::gateway::proxy::wrapped_transport::WrappedTransportConnect,
|
|
) -> anyhow::Result<Box<dyn crate::gateway::proxy::traits::TcpProxyStream>> {
|
|
anyhow::bail!("recording engine does not open streams")
|
|
}
|
|
|
|
async fn stop(&self) {
|
|
self.stop_calls.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct BlockingListenerState {
|
|
start_entered: Notify,
|
|
drop_calls: AtomicUsize,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct BlockingSocketListener {
|
|
url: Url,
|
|
state: Arc<BlockingListenerState>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl SocketListener for BlockingSocketListener {
|
|
type Accepted = AcceptedTransport<TestTcpSocket>;
|
|
|
|
async fn listen(&mut self) -> anyhow::Result<()> {
|
|
self.state.start_entered.notify_one();
|
|
std::future::pending().await
|
|
}
|
|
|
|
async fn accept(&mut self) -> anyhow::Result<Self::Accepted> {
|
|
std::future::pending().await
|
|
}
|
|
|
|
fn local_url(&self) -> Url {
|
|
self.url.clone()
|
|
}
|
|
}
|
|
|
|
impl Drop for BlockingSocketListener {
|
|
fn drop(&mut self) {
|
|
self.state.drop_calls.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
struct BlockingExternalListenerFactory {
|
|
state: Arc<BlockingListenerState>,
|
|
}
|
|
|
|
impl ExternalListenerFactory<AcceptedTransport<TestTcpSocket>> for BlockingExternalListenerFactory {
|
|
fn supports_scheme(&self, scheme: &str) -> bool {
|
|
scheme == "unix"
|
|
}
|
|
|
|
fn create(
|
|
&self,
|
|
request: ExternalListenerRequest,
|
|
) -> Box<dyn SocketListener<Accepted = AcceptedTransport<TestTcpSocket>>> {
|
|
Box::new(BlockingSocketListener {
|
|
url: request.url,
|
|
state: self.state.clone(),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ReadySocketListener(Url);
|
|
|
|
#[async_trait]
|
|
impl SocketListener for ReadySocketListener {
|
|
type Accepted = AcceptedTransport<TestTcpSocket>;
|
|
|
|
async fn listen(&mut self) -> anyhow::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn accept(&mut self) -> anyhow::Result<Self::Accepted> {
|
|
std::future::pending().await
|
|
}
|
|
|
|
fn local_url(&self) -> Url {
|
|
self.0.clone()
|
|
}
|
|
}
|
|
|
|
struct ReadyExternalListenerFactory;
|
|
|
|
impl ExternalListenerFactory<AcceptedTransport<TestTcpSocket>> for ReadyExternalListenerFactory {
|
|
fn supports_scheme(&self, scheme: &str) -> bool {
|
|
scheme == "unix"
|
|
}
|
|
|
|
fn create(
|
|
&self,
|
|
request: ExternalListenerRequest,
|
|
) -> Box<dyn SocketListener<Accepted = AcceptedTransport<TestTcpSocket>>> {
|
|
Box::new(ReadySocketListener(request.url))
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn runtime_updates_refresh_avoid_relay_preference() {
|
|
let config = test_config("portable-runtime-update");
|
|
let instance = build_instance(config.clone()).unwrap();
|
|
|
|
assert!(
|
|
!instance
|
|
.node_snapshot()
|
|
.await
|
|
.feature_flags
|
|
.avoid_relay_data
|
|
);
|
|
|
|
let mut enabled = runtime_snapshot(&config);
|
|
Arc::make_mut(&mut enabled.peer).avoid_relay_data_preference = true;
|
|
instance.update_runtime_config(enabled).await.unwrap();
|
|
assert!(
|
|
instance
|
|
.node_snapshot()
|
|
.await
|
|
.feature_flags
|
|
.avoid_relay_data
|
|
);
|
|
|
|
let mut disabled = runtime_snapshot(&config);
|
|
Arc::make_mut(&mut disabled.peer).avoid_relay_data_preference = false;
|
|
instance.update_runtime_config(disabled).await.unwrap();
|
|
assert!(
|
|
!instance
|
|
.node_snapshot()
|
|
.await
|
|
.feature_flags
|
|
.avoid_relay_data
|
|
);
|
|
}
|
|
|
|
#[cfg(all(feature = "test-utils", feature = "dhcp-ipv4"))]
|
|
#[cfg_attr(
|
|
not(target_os = "wasi"),
|
|
tokio::test(flavor = "multi_thread", worker_threads = 2)
|
|
)]
|
|
#[cfg_attr(target_os = "wasi", tokio::test)]
|
|
async fn concurrent_runtime_updates_keep_snapshot_and_derived_state_coherent() {
|
|
let config = test_config("concurrent-runtime-update");
|
|
let instance = build_instance(config.clone()).unwrap();
|
|
instance.start().await.unwrap();
|
|
|
|
let original = instance.node_snapshot().await;
|
|
let mut full = runtime_snapshot(&config);
|
|
full.services.dhcp_ipv4 = true;
|
|
full.services.acl.tcp_whitelist = vec!["80".to_owned()];
|
|
{
|
|
let peer = Arc::make_mut(&mut full.peer);
|
|
peer.runtime.core.node.hostname = Some("full".to_owned());
|
|
peer.runtime.core.routes.proxy_networks =
|
|
vec![proxy_network("192.0.2.0/24", Some("198.51.100.0/24"))];
|
|
}
|
|
let mut peer_update = full.clone();
|
|
{
|
|
let peer = Arc::make_mut(&mut peer_update.peer);
|
|
peer.runtime.core.node.hostname = Some("peer".to_owned());
|
|
peer.runtime.core.routes.proxy_networks =
|
|
vec![proxy_network("203.0.113.0/24", Some("10.20.30.0/24"))];
|
|
}
|
|
|
|
let start = Arc::new(tokio::sync::Barrier::new(3));
|
|
let full_update = tokio::spawn({
|
|
let instance = instance.clone();
|
|
let start = start.clone();
|
|
async move {
|
|
start.wait().await;
|
|
instance.update_runtime_config(full).await
|
|
}
|
|
});
|
|
let peer_update = tokio::spawn({
|
|
let instance = instance.clone();
|
|
let start = start.clone();
|
|
async move {
|
|
start.wait().await;
|
|
instance.update_runtime_config(peer_update).await
|
|
}
|
|
});
|
|
start.wait().await;
|
|
full_update.await.unwrap().unwrap();
|
|
peer_update.await.unwrap().unwrap();
|
|
|
|
let final_config = instance.runtime_config.snapshot();
|
|
assert!(final_config.services.dhcp_ipv4);
|
|
assert_eq!(instance.acl_whitelist_snapshot().tcp_ports, ["80"]);
|
|
assert_eq!(instance.acl_reload_count.load(Ordering::Relaxed), 1);
|
|
let node = instance.node_snapshot().await;
|
|
assert_eq!(node.peer_id, original.peer_id);
|
|
assert_eq!(node.instance_id, original.instance_id);
|
|
assert_eq!(
|
|
final_config.peer.runtime.core.node.hostname.as_deref(),
|
|
Some(node.hostname.as_str())
|
|
);
|
|
match node.hostname.as_str() {
|
|
"full" => assert_eq!(
|
|
instance
|
|
.proxy_cidr_table
|
|
.lookup_v4("198.51.100.42".parse().unwrap()),
|
|
Some("192.0.2.42".parse().unwrap())
|
|
),
|
|
"peer" => assert_eq!(
|
|
instance
|
|
.proxy_cidr_table
|
|
.lookup_v4("10.20.30.42".parse().unwrap()),
|
|
Some("203.0.113.42".parse().unwrap())
|
|
),
|
|
hostname => panic!("unexpected final hostname: {hostname:?}"),
|
|
}
|
|
|
|
instance.stop().await;
|
|
}
|
|
|
|
#[cfg(all(feature = "test-utils", feature = "dhcp-ipv4"))]
|
|
#[tokio::test]
|
|
async fn active_runtime_update_skips_unchanged_and_rejects_invalid_acl() {
|
|
let config = test_config("invalid-active-acl-update");
|
|
let instance = build_instance(config.clone()).unwrap();
|
|
instance.start().await.unwrap();
|
|
|
|
let mut unrelated = runtime_snapshot(&config);
|
|
Arc::make_mut(&mut unrelated.peer)
|
|
.runtime
|
|
.core
|
|
.node
|
|
.hostname = Some("accepted".to_owned());
|
|
instance.update_runtime_config(unrelated).await.unwrap();
|
|
assert_eq!(instance.acl_reload_count.load(Ordering::Relaxed), 0);
|
|
let before = instance.node_snapshot().await;
|
|
|
|
let mut rejected = runtime_snapshot(&config);
|
|
rejected.services.dhcp_ipv4 = true;
|
|
rejected.services.acl.tcp_whitelist = vec!["invalid".to_owned()];
|
|
Arc::make_mut(&mut rejected.peer).runtime.core.node.hostname = Some("rejected".to_owned());
|
|
|
|
let error = instance.update_runtime_config(rejected).await.unwrap_err();
|
|
assert!(error.to_string().contains("Invalid port number"));
|
|
assert!(!instance.runtime_config.snapshot().services.dhcp_ipv4);
|
|
assert!(instance.acl_whitelist_snapshot().tcp_ports.is_empty());
|
|
assert_eq!(instance.acl_reload_count.load(Ordering::Relaxed), 0);
|
|
assert_eq!(instance.node_snapshot().await.hostname, before.hostname);
|
|
instance.stop().await;
|
|
}
|
|
|
|
#[cfg(feature = "proxy-packet")]
|
|
#[tokio::test]
|
|
async fn runtime_core_instance_owns_connectivity_lifecycle() {
|
|
let mut config = test_config("connectivity-lifecycle");
|
|
config.peer.snapshot.runtime.core.routes.proxy_networks =
|
|
vec![proxy_network("10.1.2.0/24", None)];
|
|
let initial_peer: Url = "tcp://127.0.0.1:29999".parse().unwrap();
|
|
config.connectivity.initial_peers = vec![initial_peer.clone()];
|
|
let proxy = Arc::new(RecordingProxyService::default());
|
|
let instance = build_with_engines(
|
|
config,
|
|
WrappedTransportEngines {
|
|
kcp: Some(proxy.clone()),
|
|
quic: None,
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(instance.state(), CoreInstanceState::Created);
|
|
assert_eq!(instance.list_connectors().len(), 1);
|
|
assert_eq!(instance.list_connectors()[0].url, initial_peer);
|
|
instance.start().await.unwrap();
|
|
assert_eq!(instance.state(), CoreInstanceState::Running);
|
|
assert!(instance.is_ready());
|
|
assert!(instance.start().await.is_err());
|
|
assert_eq!(proxy.start_calls.load(Ordering::Relaxed), 1);
|
|
|
|
instance.stop().await;
|
|
instance.stop().await;
|
|
assert_eq!(instance.state(), CoreInstanceState::Stopped);
|
|
assert_eq!(proxy.stop_calls.load(Ordering::Relaxed), 1);
|
|
}
|
|
|
|
#[cfg(feature = "proxy-smoltcp-stack")]
|
|
#[tokio::test]
|
|
async fn startup_plan_controls_gateway_for_initial_and_updated_config() {
|
|
fn build(config: CoreInstanceConfig) -> (Arc<CoreInstance<TestHost>>, Arc<TestHost>) {
|
|
let host = Arc::new(TestHost {
|
|
reject_socks5_listener: true,
|
|
..Default::default()
|
|
});
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
let adapters = adapters_with_host(host.clone(), None, Arc::new(packet_sink));
|
|
(CoreInstance::new(config, adapters).unwrap(), host)
|
|
}
|
|
|
|
let mut enabled_config = test_config("gateway-enabled-by-default");
|
|
enabled_config.connectivity.runtime.gateway.socks5_bind =
|
|
Some("127.0.0.1:1080".parse().unwrap());
|
|
let (enabled, _) = build(enabled_config);
|
|
let error = enabled.start().await.unwrap_err();
|
|
assert!(error.to_string().contains("rejected SOCKS5 listener"));
|
|
assert_eq!(enabled.state(), CoreInstanceState::Stopped);
|
|
|
|
let mut disabled_config = test_config("gateway-disabled-by-plan");
|
|
disabled_config.connectivity.startup_plan.gateway = false;
|
|
disabled_config.peer.snapshot.runtime.core.routes.ipv4 =
|
|
Some(IpPrefix::new("10.144.0.1".parse().unwrap(), 24).unwrap());
|
|
let (disabled, _) = build(disabled_config.clone());
|
|
let mut updated = runtime_snapshot(&disabled_config);
|
|
updated.services.gateway.socks5_bind = Some("127.0.0.1:1080".parse().unwrap());
|
|
disabled.update_runtime_config(updated).await.unwrap();
|
|
disabled.start().await.unwrap();
|
|
assert_eq!(disabled.state(), CoreInstanceState::Running);
|
|
let socket = disabled
|
|
.data_plane_udp_bind(0, Duration::from_secs(1))
|
|
.await
|
|
.expect("startup plan must not disable the data-plane runtime");
|
|
drop(socket);
|
|
disabled.stop().await;
|
|
}
|
|
|
|
#[cfg(feature = "proxy-smoltcp-stack")]
|
|
#[tokio::test]
|
|
async fn failed_port_forward_start_releases_started_listeners() {
|
|
let mut config = test_config("port-forward-start-rollback");
|
|
config.connectivity.runtime.gateway.port_forwards = vec![
|
|
PortForwardConfig {
|
|
bind_addr: "127.0.0.1:18080".parse().unwrap(),
|
|
dst_addr: "10.144.0.2:80".parse().unwrap(),
|
|
proto: "tcp".to_owned(),
|
|
},
|
|
PortForwardConfig {
|
|
bind_addr: "127.0.0.1:18081".parse().unwrap(),
|
|
dst_addr: "10.144.0.2:81".parse().unwrap(),
|
|
proto: "unsupported".to_owned(),
|
|
},
|
|
];
|
|
let host = Arc::new(TestHost::default());
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
let adapters = adapters_with_host(host.clone(), None, Arc::new(packet_sink));
|
|
let instance = CoreInstance::new(config, adapters).unwrap();
|
|
|
|
let error = instance.start().await.unwrap_err();
|
|
|
|
assert!(error.to_string().contains("unsupported protocol"));
|
|
assert_eq!(instance.state(), CoreInstanceState::Stopped);
|
|
assert_eq!(host.active_tcp_listeners.load(Ordering::Relaxed), 0);
|
|
}
|
|
|
|
#[cfg(feature = "proxy-packet")]
|
|
#[tokio::test]
|
|
async fn runtime_core_instance_owns_wrapped_transport_source_nat() {
|
|
let mut config = test_config("wrapped-source");
|
|
config.peer.snapshot.flags.enable_kcp_proxy = true;
|
|
config.peer.snapshot.flags.disable_kcp_input = true;
|
|
let engine = Arc::new(RecordingProxyService::default());
|
|
let instance = build_with_engines(
|
|
config,
|
|
WrappedTransportEngines {
|
|
kcp: Some(engine.clone()),
|
|
quic: None,
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
instance.start().await.unwrap();
|
|
assert!(
|
|
instance.wrapped_transport_is_started(
|
|
WrappedTransportKind::Kcp,
|
|
WrappedTransportRole::Source,
|
|
)
|
|
);
|
|
assert!(
|
|
instance
|
|
.wrapped_tcp_proxy_entry_snapshots(
|
|
WrappedTransportKind::Kcp,
|
|
WrappedTransportRole::Source,
|
|
)
|
|
.is_empty()
|
|
);
|
|
|
|
instance.stop().await;
|
|
assert!(
|
|
!instance.wrapped_transport_is_started(
|
|
WrappedTransportKind::Kcp,
|
|
WrappedTransportRole::Source,
|
|
)
|
|
);
|
|
assert_eq!(engine.stop_calls.load(Ordering::Relaxed), 1);
|
|
}
|
|
|
|
#[cfg(feature = "proxy-packet")]
|
|
#[tokio::test]
|
|
async fn runtime_core_instance_owns_wrapped_transport_destination_sessions() {
|
|
let mut config = test_config("wrapped-destination");
|
|
config.peer.snapshot.flags.enable_kcp_proxy = false;
|
|
config.peer.snapshot.flags.disable_kcp_input = false;
|
|
let engine = Arc::new(RecordingProxyService::default());
|
|
let (connections, mut connection_receiver) = tokio::sync::mpsc::unbounded_channel();
|
|
let host = Arc::new(TestHost {
|
|
proxy_nat_connections: Some(connections),
|
|
..Default::default()
|
|
});
|
|
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
|
let mut adapters = adapters_with_host(host, None, Arc::new(packet_sink));
|
|
adapters.wrapped_transports = WrappedTransportEngines {
|
|
kcp: Some(engine.clone()),
|
|
quic: None,
|
|
};
|
|
let instance = CoreInstance::new(config, adapters).unwrap();
|
|
|
|
instance.start().await.unwrap();
|
|
assert!(instance.wrapped_transport_is_started(
|
|
WrappedTransportKind::Kcp,
|
|
WrappedTransportRole::Destination,
|
|
));
|
|
|
|
let destination: SocketAddr = "127.0.0.1:20100".parse().unwrap();
|
|
let ingress = engine
|
|
.destination_ingress()
|
|
.expect("core should inject a destination ingress");
|
|
let (core_stream, peer_stream) = tokio::io::duplex(1024);
|
|
ingress
|
|
.submit(
|
|
crate::gateway::proxy::wrapped_transport::WrappedTransportAcceptedStream {
|
|
src: "10.0.0.2:40000".parse().unwrap(),
|
|
dst: destination,
|
|
initial_acl_packet_size: 16,
|
|
stream: Box::new(core_stream),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let (connected_destination, destination_stream) = tokio::time::timeout(
|
|
std::time::Duration::from_secs(2),
|
|
connection_receiver.recv(),
|
|
)
|
|
.await
|
|
.expect("core should request the destination socket")
|
|
.unwrap();
|
|
assert_eq!(connected_destination, destination);
|
|
tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
|
loop {
|
|
let entries = instance.wrapped_tcp_proxy_entry_snapshots(
|
|
WrappedTransportKind::Kcp,
|
|
WrappedTransportRole::Destination,
|
|
);
|
|
if entries.iter().any(|entry| {
|
|
entry.state
|
|
== crate::gateway::proxy::tcp_proxy_engine::TcpNatEntryState::Connected
|
|
}) {
|
|
break;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("core should own the connected destination entry");
|
|
|
|
drop(peer_stream);
|
|
drop(destination_stream);
|
|
tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
|
while !instance
|
|
.wrapped_tcp_proxy_entry_snapshots(
|
|
WrappedTransportKind::Kcp,
|
|
WrappedTransportRole::Destination,
|
|
)
|
|
.is_empty()
|
|
{
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("completed destination entry should be removed");
|
|
|
|
let (core_stream, _blocked_peer_stream) = tokio::io::duplex(1024);
|
|
ingress
|
|
.submit(
|
|
crate::gateway::proxy::wrapped_transport::WrappedTransportAcceptedStream {
|
|
src: "10.0.0.2:40001".parse().unwrap(),
|
|
dst: destination,
|
|
initial_acl_packet_size: 16,
|
|
stream: Box::new(core_stream),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let (connected_destination, _blocked_destination_stream) = tokio::time::timeout(
|
|
std::time::Duration::from_secs(2),
|
|
connection_receiver.recv(),
|
|
)
|
|
.await
|
|
.expect("second destination session should request a socket")
|
|
.unwrap();
|
|
assert_eq!(connected_destination, destination);
|
|
tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
|
while instance
|
|
.wrapped_tcp_proxy_entry_snapshots(
|
|
WrappedTransportKind::Kcp,
|
|
WrappedTransportRole::Destination,
|
|
)
|
|
.is_empty()
|
|
{
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("blocked destination session should be visible");
|
|
|
|
tokio::time::timeout(std::time::Duration::from_secs(2), instance.stop())
|
|
.await
|
|
.expect("stop should cancel core-owned destination sessions");
|
|
assert!(
|
|
instance
|
|
.wrapped_tcp_proxy_entry_snapshots(
|
|
WrappedTransportKind::Kcp,
|
|
WrappedTransportRole::Destination,
|
|
)
|
|
.is_empty()
|
|
);
|
|
assert!(
|
|
ingress
|
|
.submit(
|
|
crate::gateway::proxy::wrapped_transport::WrappedTransportAcceptedStream {
|
|
src: "10.0.0.2:40002".parse().unwrap(),
|
|
dst: destination,
|
|
initial_acl_packet_size: 16,
|
|
stream: Box::new(tokio::io::duplex(64).0),
|
|
},
|
|
)
|
|
.await
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn runtime_core_instance_owns_the_transport_proxy_cidr_table() {
|
|
let mut config = test_config("transport-proxy-cidr");
|
|
config.connectivity.runtime.proxy.forward_by_system = true;
|
|
config.peer.snapshot.runtime.core.routes.proxy_networks =
|
|
vec![proxy_network("192.0.2.0/24", Some("198.51.100.0/24"))];
|
|
let mut updated = runtime_snapshot(&config);
|
|
let proxy = Arc::new(RecordingProxyService::default());
|
|
let instance = build_with_engines(
|
|
config,
|
|
WrappedTransportEngines {
|
|
kcp: Some(proxy.clone()),
|
|
quic: None,
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(instance.node_snapshot().await.proxy_networks.len(), 1);
|
|
instance.start().await.unwrap();
|
|
assert_eq!(proxy.start_calls.load(Ordering::Relaxed), 1);
|
|
|
|
Arc::make_mut(&mut updated.peer)
|
|
.runtime
|
|
.core
|
|
.routes
|
|
.proxy_networks = vec![proxy_network("203.0.113.0/24", Some("10.20.30.0/24"))];
|
|
instance.update_runtime_config(updated).await.unwrap();
|
|
let proxy_networks = instance.node_snapshot().await.proxy_networks;
|
|
assert_eq!(proxy_networks.len(), 1);
|
|
assert_eq!(
|
|
proxy_networks[0].real.address,
|
|
"203.0.113.0".parse::<IpAddr>().unwrap()
|
|
);
|
|
assert_eq!(
|
|
proxy_networks[0].mapped.as_ref().unwrap().address,
|
|
"10.20.30.0".parse::<IpAddr>().unwrap()
|
|
);
|
|
|
|
instance.stop().await;
|
|
assert!(instance.start().await.is_err());
|
|
assert_eq!(proxy.stop_calls.load(Ordering::Relaxed), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn runtime_core_rejects_invalid_acl_runtime_snapshot() {
|
|
let config = test_config("explicit-acl");
|
|
let instance = build_instance(config.clone()).unwrap();
|
|
assert_eq!(instance.acl_whitelist_snapshot(), Default::default());
|
|
|
|
let mut updated = runtime_snapshot(&config);
|
|
updated.services.acl.tcp_whitelist = vec!["invalid".to_owned()];
|
|
let error = instance.update_runtime_config(updated).await.unwrap_err();
|
|
assert!(error.to_string().contains("Invalid port number"));
|
|
assert!(instance.acl_whitelist_snapshot().tcp_ports.is_empty());
|
|
instance.start().await.unwrap();
|
|
instance.stop().await;
|
|
}
|
|
|
|
#[cfg(feature = "dhcp-ipv4")]
|
|
#[tokio::test]
|
|
async fn runtime_core_accepts_explicit_dhcp_runtime_snapshot() {
|
|
let config = test_config("explicit-dhcp");
|
|
let instance = build_instance(config.clone()).unwrap();
|
|
let mut updated = runtime_snapshot(&config);
|
|
updated.services.dhcp_ipv4 = true;
|
|
instance.update_runtime_config(updated).await.unwrap();
|
|
let error = instance.start().await.unwrap_err();
|
|
assert!(error.to_string().contains("no host adapter was provided"));
|
|
assert_eq!(instance.state(), CoreInstanceState::Stopped);
|
|
}
|
|
|
|
#[cfg(feature = "public-ipv6-provider")]
|
|
#[tokio::test]
|
|
async fn runtime_core_accepts_explicit_public_ipv6_runtime_snapshot() {
|
|
let config = test_config("explicit-public-ipv6");
|
|
let instance = build_instance(config.clone()).unwrap();
|
|
let mut updated = runtime_snapshot(&config);
|
|
updated.services.public_ipv6_provider.provider_enabled = true;
|
|
updated.services.public_ipv6_provider.provider_supported = true;
|
|
updated.services.public_ipv6_provider.configured_prefix =
|
|
Some("fd00::/64".parse().unwrap());
|
|
instance.update_runtime_config(updated).await.unwrap();
|
|
|
|
let error = instance.start().await.unwrap_err();
|
|
assert!(error.to_string().contains("not a valid global unicast"));
|
|
assert_eq!(instance.state(), CoreInstanceState::Stopped);
|
|
}
|
|
|
|
#[cfg(not(feature = "proxy-packet"))]
|
|
#[test]
|
|
fn runtime_core_rejects_packet_proxy_requests_when_unavailable() {
|
|
let mut config = test_config("packet-proxy-unavailable");
|
|
config.connectivity.runtime.proxy.enable_exit_node = true;
|
|
|
|
let error = match build_instance(config) {
|
|
Ok(_) => panic!("packet proxy request unexpectedly succeeded"),
|
|
Err(error) => error,
|
|
};
|
|
|
|
assert!(
|
|
error
|
|
.to_string()
|
|
.contains("does not include packet proxy services")
|
|
);
|
|
}
|
|
|
|
#[cfg(not(feature = "proxy-smoltcp-stack"))]
|
|
#[tokio::test]
|
|
async fn runtime_core_rejects_unavailable_gateway_updates() {
|
|
let config = test_config("smoltcp-gateway-update-unavailable");
|
|
let instance = build_instance(config.clone()).unwrap();
|
|
let mut updated = runtime_snapshot(&config);
|
|
updated.services.gateway.socks5_bind = Some("127.0.0.1:1080".parse().unwrap());
|
|
|
|
let error = instance.update_runtime_config(updated).await.unwrap_err();
|
|
|
|
assert!(
|
|
error
|
|
.to_string()
|
|
.contains("does not include the smoltcp gateway")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stopping_while_transport_proxy_starts_rolls_back_once() {
|
|
let mut config = test_config("blocking-transport-proxy");
|
|
config.connectivity.runtime.proxy.forward_by_system = true;
|
|
config.peer.snapshot.runtime.core.routes.proxy_networks =
|
|
vec![proxy_network("10.1.2.0/24", None)];
|
|
config.connectivity.initial_peers = vec!["tcp://127.0.0.1:29998".parse().unwrap()];
|
|
let (proxy, start_gate) = RecordingProxyService::blocking();
|
|
let instance = build_with_engines(
|
|
config,
|
|
WrappedTransportEngines {
|
|
kcp: Some(proxy.clone()),
|
|
quic: None,
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
let start_task = tokio::spawn({
|
|
let instance = instance.clone();
|
|
async move { instance.start().await }
|
|
});
|
|
start_gate.entered.notified().await;
|
|
let stop_task = tokio::spawn({
|
|
let instance = instance.clone();
|
|
async move { instance.stop().await }
|
|
});
|
|
while !instance.cancel.is_cancelled() {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
|
|
assert!(start_task.await.unwrap().is_err());
|
|
stop_task.await.unwrap();
|
|
assert_eq!(instance.list_connectors().len(), 1);
|
|
assert_eq!(instance.state(), CoreInstanceState::Stopped);
|
|
assert!(!instance.is_ready());
|
|
assert_eq!(proxy.start_calls.load(Ordering::Relaxed), 1);
|
|
assert_eq!(proxy.stop_calls.load(Ordering::Relaxed), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn start_serializes_runtime_updates() {
|
|
let mut config = test_config("serialized-start");
|
|
config.connectivity.runtime.proxy.forward_by_system = true;
|
|
config.peer.snapshot.runtime.core.routes.proxy_networks =
|
|
vec![proxy_network("10.1.4.0/24", None)];
|
|
let mut updated = runtime_snapshot(&config);
|
|
Arc::make_mut(&mut updated.peer).runtime.core.node.hostname =
|
|
Some("updated-after-start".to_owned());
|
|
let (proxy, start_gate) = RecordingProxyService::blocking();
|
|
let instance = build_with_engines(
|
|
config,
|
|
WrappedTransportEngines {
|
|
kcp: Some(proxy),
|
|
quic: None,
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
let start = tokio::spawn({
|
|
let instance = instance.clone();
|
|
async move { instance.start().await }
|
|
});
|
|
start_gate.entered.notified().await;
|
|
|
|
let update = instance.update_runtime_config(updated);
|
|
tokio::pin!(update);
|
|
assert!(matches!(
|
|
futures::poll!(update.as_mut()),
|
|
std::task::Poll::Pending
|
|
));
|
|
|
|
start_gate.release.notify_one();
|
|
start.await.unwrap().unwrap();
|
|
update.await.unwrap();
|
|
assert_eq!(
|
|
instance.node_snapshot().await.hostname,
|
|
"updated-after-start"
|
|
);
|
|
instance.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn aborting_start_stops_partial_runtime() {
|
|
let mut config = test_config("aborted-start");
|
|
config.connectivity.runtime.proxy.forward_by_system = true;
|
|
config.peer.snapshot.runtime.core.routes.proxy_networks =
|
|
vec![proxy_network("10.1.3.0/24", None)];
|
|
let (proxy, start_gate) = RecordingProxyService::blocking();
|
|
let instance = build_with_engines(
|
|
config,
|
|
WrappedTransportEngines {
|
|
kcp: Some(proxy.clone()),
|
|
quic: None,
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
let start = tokio::spawn({
|
|
let instance = instance.clone();
|
|
async move { instance.start().await }
|
|
});
|
|
start_gate.entered.notified().await;
|
|
start.abort();
|
|
assert!(start.await.unwrap_err().is_cancelled());
|
|
|
|
tokio::time::timeout(Duration::from_secs(2), async {
|
|
while instance.state() != CoreInstanceState::Stopped {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("aborted activation should recover the instance");
|
|
assert!(!instance.is_ready());
|
|
assert_eq!(proxy.start_calls.load(Ordering::Relaxed), 1);
|
|
assert_eq!(proxy.stop_calls.load(Ordering::Relaxed), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn invalid_initial_peer_fails_during_construction() {
|
|
let mut config = test_config("invalid-initial-peer");
|
|
config.connectivity.initial_peers =
|
|
vec!["unsupported://peer.example:1234".parse().unwrap()];
|
|
|
|
let error = build_instance(config)
|
|
.err()
|
|
.expect("invalid initial peer should fail construction");
|
|
assert!(
|
|
error
|
|
.to_string()
|
|
.contains("unsupported core manual connector URL"),
|
|
"unexpected construction error: {error:#}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn runtime_core_instances_keep_lifecycle_and_connectors_isolated() {
|
|
let instance_a = build_instance(test_config("instance-a")).unwrap();
|
|
let instance_b = build_instance(test_config("instance-b")).unwrap();
|
|
let connector_a: Url = "tcp://127.0.0.1:21001".parse().unwrap();
|
|
let connector_b: Url = "udp://127.0.0.1:21002".parse().unwrap();
|
|
|
|
instance_a.add_connector(connector_a.clone()).unwrap();
|
|
instance_b.add_connector(connector_b.clone()).unwrap();
|
|
assert_eq!(instance_a.list_connectors()[0].url, connector_a);
|
|
assert_eq!(instance_b.list_connectors()[0].url, connector_b);
|
|
instance_a.clear_connectors();
|
|
instance_b.clear_connectors();
|
|
|
|
let (start_a, start_b) = tokio::join!(instance_a.start(), instance_b.start());
|
|
start_a.unwrap();
|
|
start_b.unwrap();
|
|
assert_eq!(instance_a.state(), CoreInstanceState::Running);
|
|
assert_eq!(instance_b.state(), CoreInstanceState::Running);
|
|
|
|
instance_a.stop().await;
|
|
assert_eq!(instance_a.state(), CoreInstanceState::Stopped);
|
|
assert_eq!(instance_b.state(), CoreInstanceState::Running);
|
|
instance_b.stop().await;
|
|
assert_eq!(instance_b.state(), CoreInstanceState::Stopped);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[tokio::test]
|
|
async fn stop_cancels_pending_listener_start() {
|
|
let state = Arc::new(BlockingListenerState::default());
|
|
let mut config = test_config("pending-listener");
|
|
config.connectivity.listeners = Some(ListenerRuntimeConfig::new(
|
|
vec!["unix:///tmp/easytier-pending-listener".parse().unwrap()],
|
|
false,
|
|
SocketContext::default(),
|
|
));
|
|
let instance = build_with_engines_and_listener(
|
|
config,
|
|
WrappedTransportEngines::default(),
|
|
Some(Arc::new(BlockingExternalListenerFactory {
|
|
state: state.clone(),
|
|
})),
|
|
)
|
|
.unwrap();
|
|
let start_instance = instance.clone();
|
|
let start_task =
|
|
AbortOnDropHandle::new(tokio::spawn(async move { start_instance.start().await }));
|
|
let start_result = tokio::time::timeout(Duration::from_secs(1), async {
|
|
state.start_entered.notified().await;
|
|
instance.stop().await;
|
|
start_task.await.unwrap()
|
|
})
|
|
.await
|
|
.expect("listener cancellation should complete promptly");
|
|
|
|
assert!(start_result.is_err());
|
|
assert_eq!(instance.state(), CoreInstanceState::Stopped);
|
|
assert_eq!(state.drop_calls.load(Ordering::Relaxed), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn external_listener_uses_core_running_listener_registry() {
|
|
let external_url: Url = "unix:///tmp/easytier-external-listener-test"
|
|
.parse()
|
|
.unwrap();
|
|
let mut config = test_config("external-listener-registry");
|
|
config.connectivity.listeners = Some(ListenerRuntimeConfig::new(
|
|
vec![external_url.clone()],
|
|
false,
|
|
SocketContext::default(),
|
|
));
|
|
let instance = build_with_engines_and_listener(
|
|
config,
|
|
WrappedTransportEngines::default(),
|
|
Some(Arc::new(ReadyExternalListenerFactory)),
|
|
)
|
|
.unwrap();
|
|
|
|
instance.start().await.unwrap();
|
|
let running = instance.running_listeners();
|
|
assert_eq!(running.len(), 2);
|
|
assert!(running.iter().any(|url| url.scheme() == "ring"));
|
|
assert!(running.contains(&external_url));
|
|
assert_eq!(instance.node_snapshot().await.listeners, running);
|
|
|
|
instance.stop().await;
|
|
assert!(instance.running_listeners().is_empty());
|
|
}
|
|
}
|