diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml new file mode 100644 index 00000000..139751c3 --- /dev/null +++ b/.github/workflows/js.yml @@ -0,0 +1,62 @@ +name: EasyTier JavaScript Hosts + +on: + push: + branches: ["develop", "main", "releases/**"] + paths: + - "Cargo.toml" + - "Cargo.lock" + - "easytier-core/**" + - "easytier-proto/**" + - "easytier-js/**" + - ".github/workflows/js.yml" + pull_request: + branches: ["develop", "main"] + types: [opened, synchronize, reopened, ready_for_review] + paths: + - "Cargo.toml" + - "Cargo.lock" + - "easytier-core/**" + - "easytier-proto/**" + - "easytier-js/**" + - ".github/workflows/js.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 1.95 + target: wasm32-wasip1 + cache: false + rustflags: '' + + - uses: arduino/setup-protoc@v3 + with: + version: '35.1' + repo-token: ${{ github.token }} + + - uses: actions/setup-node@v5 + with: + node-version: 22 + + - uses: pnpm/action-setup@v5 + with: + version: 10 + run_install: false + + - name: Install dependencies + run: pnpm --dir easytier-js install --frozen-lockfile + + - name: Build and test + run: pnpm --dir easytier-js check diff --git a/CONTEXT.md b/CONTEXT.md index 3c1a477a..ec78a17c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -90,3 +90,16 @@ A compact compatibility Host retains accepted values in the authoritative TOML model for management readback, while the shared host-aware normalization path omits capabilities that the compact runtime cannot execute. Omitted settings are silent no-ops and must not be advertised as live network capabilities. + +## Web compatibility Host + +The Web compatibility Host runs the portable EasyTier guest in JavaScript +runtimes that provide WebAssembly JSPI. Its shared runtime Module owns guest +lifecycle, Host capability operations, data-plane resources, and WebSocket +message handling. Browser and Cloudflare Adapters own only the platform-specific +way that WebSockets are dialed or accepted and the matching guest artifact. + +The Browser Adapter is an outbound-only EasyTier instance with a smoltcp TCP +data plane. The Cloudflare Adapter is an inbound-only relay hosted by one named +Durable Object. Their public configuration exposes only capabilities each Host +can execute; guest ABI details and serialized TOML remain internal. diff --git a/docs/core-architecture.md b/docs/core-architecture.md index a717cdd4..c89c9136 100644 --- a/docs/core-architecture.md +++ b/docs/core-architecture.md @@ -184,12 +184,27 @@ wire codecs. It does not own socket I/O or connection policy. - DNS and DNS record resolution; - connector environment observations; - packet ingress and egress; -- Host socket operation bridges and handle-based TCP/UDP/listener adapters. +- Host socket operation bridges and handle-based TCP/UDP/listener adapters; +- host-owned, message-preserving tunnel endpoints. Core owns scheduling, backpressure, cancellation, UDP session state, and protocol state even when each actual operation crosses a Host Adapter. A Host Adapter owns the real resource and performs the OS operation. +For host-owned tunnels, ownership crosses the guest ABI only after a bounded +Host listener queue accepts the tunnel. The queue is registered through +`CoreHostAdapters` and consumed by the normal `CoreListenerRuntime`, so the +tunnel still reaches `PeerAcceptedTunnelHandler` and cannot bypass peer +handshake, admission, events, or routing policy. Each Host receive produces +one complete `DummyTunnel` payload; no stream framing is added. The Host owns +transport-specific message validation and maps a clean close to tunnel EOF. + +Hosts without outbound sockets select `CoreConnectivityMode::InboundOnly` for +one instance. That startup plan retains listeners and the peer/router while +omitting STUN, outbound connectors, and hole punching. Cargo features only +compile Host Adapters; they do not change `CoreInstance` fields, lifecycle, or +management semantics. + The native `NativeHostRuntime` is process-wide and does not retain an instance `GlobalCtx`, namespace guard, socket mark, or connectivity state. Differences between instances travel in each request's `SocketContext`. A narrow diff --git a/docs/data-plane-runtime-plan.md b/docs/data-plane-runtime-plan.md index 2cfbbfe8..947c09b8 100644 --- a/docs/data-plane-runtime-plan.md +++ b/docs/data-plane-runtime-plan.md @@ -5,9 +5,8 @@ Accepted. This document is the implementation plan for restructuring the EasyTier data -plane and exposing it through native FFI and the standalone -`easytier-go-host` project. It describes a target architecture, not the current -implementation. +plane and exposing it through native FFI and the `easytier-go` module. It +describes a target architecture, not the current implementation. The implementation scope is: @@ -16,7 +15,7 @@ The implementation scope is: backend; - `easytier-contrib/easytier-ffi`; - the WASI guest ABI implemented by `easytier-core`; -- `/data/project/easytier-go-host`; +- `easytier-go`; - TCP, UDP, and smoltcp data-plane paths; - moving KCP route selection and source-connection ownership below the `DataPlaneRuntime` Interface without making KCP portable. @@ -987,7 +986,7 @@ Repository: EasyTier. ### Phase 8: Go `coreabi` and engine -Repository: `easytier-go-host`. +Repository: EasyTier (`easytier-go`). - Add typed data-plane guest calls and wire codecs. - Extend the single driver with submit, cancel, close, and completion drain. @@ -996,7 +995,7 @@ Repository: `easytier-go-host`. ### Phase 9: Go standard network Adapters and artifact -Repository: `easytier-go-host`. +Repository: EasyTier (`easytier-go`). - Add `Dial`, `Listen`, and `ListenPacket`. - Implement TCP, UDP, deadlines, cancellation, close, and error mapping. @@ -1006,7 +1005,8 @@ Repository: `easytier-go-host`. - Run real two-instance TCP and UDP integration tests. Each phase ends in a reviewable commit. Commit messages use a 72-column text -width. The complete task receives one final review across both repositories. +width. The complete task receives one final review across both implementation +areas. The operation-broker commit may receive an additional high-risk incremental review because it contains concurrency logic. diff --git a/easytier-core/Cargo.toml b/easytier-core/Cargo.toml index 29f4e72c..5b851d93 100644 --- a/easytier-core/Cargo.toml +++ b/easytier-core/Cargo.toml @@ -146,6 +146,8 @@ proxy-smoltcp-stack = [ "smoltcp/proto-ipv6", "smoltcp/async", ] +wasm-host-tunnel = [] +wasm-host-tunnel-outbound = ["wasm-host-tunnel"] test-utils = [] tracing-log = ["tracing/log"] zstd = ["dep:zstd"] diff --git a/easytier-core/src/connectivity/composite.rs b/easytier-core/src/connectivity/composite.rs index c4af43a6..6eb3bfe2 100644 --- a/easytier-core/src/connectivity/composite.rs +++ b/easytier-core/src/connectivity/composite.rs @@ -103,6 +103,17 @@ impl InterfaceAddrCache { /// Mechanical connector operations supplied by one process-wide runtime. #[async_trait] pub trait ConnectorRuntime: VirtualTcpSocketFactory + Send + Sync + 'static { + fn supports_external_tunnel(&self, _scheme: &str) -> bool { + false + } + + async fn connect_external_tunnel( + &self, + _url: &Url, + ) -> anyhow::Result>> { + Ok(None) + } + async fn connect_byte_stream( &self, url: &Url, @@ -207,6 +218,17 @@ where S: ConnectorRuntime + VirtualUdpSocketFactory, E: ConnectorEnvironment, { + fn supports_external_tunnel(&self, scheme: &str) -> bool { + self.sockets.supports_external_tunnel(scheme) + } + + async fn connect_external_tunnel( + &self, + url: &Url, + ) -> anyhow::Result>> { + self.sockets.connect_external_tunnel(url).await + } + async fn local_addr_for_remote( &self, remote_addr: SocketAddr, diff --git a/easytier-core/src/connectivity/connector_host.rs b/easytier-core/src/connectivity/connector_host.rs index aaa2cabe..c96e5e7b 100644 --- a/easytier-core/src/connectivity/connector_host.rs +++ b/easytier-core/src/connectivity/connector_host.rs @@ -24,6 +24,7 @@ use url::Url; use crate::{ connectivity::{ composite::{ConnectorEnvironment, ConnectorHostAdapter, ConnectorRuntime}, + manual::ExternalTunnelConnector, transport::ConnectedByteStream, }, host::environment::{HostConnectorEnvironmentIo, local_addr_for_remote}, @@ -113,6 +114,7 @@ where listeners: HostTcpListenerFactory, environment: Arc, environment_io: Arc, + external_tunnel_connector: Option>, } impl HostConnectorRuntime @@ -132,8 +134,17 @@ where listeners: HostTcpListenerFactory::new(runtime, backend), environment: Arc::new(environment), environment_io, + external_tunnel_connector: None, } } + + pub fn with_external_tunnel_connector( + mut self, + connector: Arc, + ) -> Self { + self.external_tunnel_connector = Some(connector); + self + } } #[async_trait] @@ -181,6 +192,25 @@ where B: ConnectorHostSocketBackend, E: HostConnectorEnvironmentIo, { + fn supports_external_tunnel(&self, scheme: &str) -> bool { + self.external_tunnel_connector + .as_ref() + .is_some_and(|connector| connector.supports_scheme(scheme)) + } + + async fn connect_external_tunnel( + &self, + url: &Url, + ) -> anyhow::Result>> { + let Some(connector) = &self.external_tunnel_connector else { + return Ok(None); + }; + if !connector.supports_scheme(url.scheme()) { + return Ok(None); + } + Ok(Some(connector.connect(url).await?)) + } + async fn connect_byte_stream( &self, url: &Url, @@ -257,6 +287,24 @@ where ConnectorHostAdapter::new(runtime.clone(), runtime) } +pub fn new_connector_host_with_external_tunnel( + socket_runtime: HostSocketRuntime, + backend: Arc, + environment: HostConnectorEnvironmentSnapshot, + environment_io: Arc, + connector: Arc, +) -> ConnectorHost +where + B: ConnectorHostSocketBackend, + E: HostConnectorEnvironmentIo, +{ + let runtime = Arc::new( + HostConnectorRuntime::new(socket_runtime, backend, environment, environment_io) + .with_external_tunnel_connector(connector), + ); + ConnectorHostAdapter::new(runtime.clone(), runtime) +} + #[cfg(test)] mod tests { use std::{ @@ -299,6 +347,19 @@ mod tests { struct FixedStunProvider; + struct ExternalFailingConnector; + + #[async_trait] + impl ExternalTunnelConnector for ExternalFailingConnector { + fn supports_scheme(&self, scheme: &str) -> bool { + scheme == "ws" + } + + async fn connect(&self, _url: &Url) -> anyhow::Result> { + anyhow::bail!("external connector called") + } + } + #[async_trait] impl StunInfoProvider for FixedStunProvider { fn get_stun_info(&self) -> StunInfo { @@ -579,6 +640,34 @@ mod tests { ); } + #[tokio::test] + async fn delegates_external_tunnel_connections() { + let host = new_connector_host_with_external_tunnel( + HostSocketRuntime::new(), + Arc::new(UnsupportedBackend::default()), + test_environment_snapshot(), + Arc::new(TestEnvironmentIo::default()), + Arc::new(ExternalFailingConnector), + ); + + let error = ManualConnectorHost::connect_external_tunnel( + &host, + &"ws://relay.example/".parse().unwrap(), + ) + .await + .unwrap_err(); + assert_eq!(error.to_string(), "external connector called"); + assert!( + ManualConnectorHost::connect_external_tunnel( + &host, + &"tcp://relay.example:11010".parse().unwrap(), + ) + .await + .unwrap() + .is_none() + ); + } + #[tokio::test] async fn direct_rpc_projects_host_observations_without_instance_policy() { let host = Arc::new(new_connector_host( diff --git a/easytier-core/src/connectivity/manual/mod.rs b/easytier-core/src/connectivity/manual/mod.rs index 20f84691..bcca4f12 100644 --- a/easytier-core/src/connectivity/manual/mod.rs +++ b/easytier-core/src/connectivity/manual/mod.rs @@ -94,6 +94,14 @@ pub struct ManualInterfaceAddrs { #[async_trait] pub trait ManualConnectorHost: VirtualTcpSocketFactory + VirtualUdpSocketFactory { + fn supports_external_tunnel(&self, _scheme: &str) -> bool { + false + } + + async fn connect_external_tunnel(&self, _url: &Url) -> anyhow::Result>> { + Ok(None) + } + async fn local_addr_for_remote( &self, remote_addr: SocketAddr, @@ -110,6 +118,13 @@ pub trait ManualConnectorHost: VirtualTcpSocketFactory + VirtualUdpSocketFactory } } +#[async_trait] +pub trait ExternalTunnelConnector: Send + Sync + 'static { + fn supports_scheme(&self, scheme: &str) -> bool; + + async fn connect(&self, url: &Url) -> anyhow::Result>; +} + #[async_trait] pub(crate) trait ManualEndpointResolver: Send + Sync + 'static { async fn resolve_endpoint(&self, url: &Url) -> anyhow::Result; @@ -182,6 +197,14 @@ where )); } + if let Some(tunnel) = self.host.connect_external_tunnel(&endpoint.url).await? { + return Ok(apply_resolved_endpoint_info( + tunnel, + requested_url, + endpoint.tunnel_prefixes, + )); + } + if !self.protocol.supports_scheme(endpoint.url.scheme()) { anyhow::bail!( "unsupported client protocol upgrader: {}", @@ -710,17 +733,20 @@ where return Err(error); } }; - let ip_versions = match resolve_reconnect_ip_versions( - &normalized_url, - connect_timeout, - ManualTransport::from_url(&normalized_url) - .ok() - .map(|transport| data.options.socket_context(transport, IpVersion::Both)) - .unwrap_or_default(), - data.dns.as_ref(), - ) - .await - { + let ip_versions = match if data.host.supports_external_tunnel(normalized_url.scheme()) { + Ok(vec![IpVersion::Both]) + } else { + resolve_reconnect_ip_versions( + &normalized_url, + connect_timeout, + ManualTransport::from_url(&normalized_url) + .ok() + .map(|transport| data.options.socket_context(transport, IpVersion::Both)) + .unwrap_or_default(), + data.dns.as_ref(), + ) + .await + } { Ok(ip_versions) => ip_versions, Err(error) => { emit_connect_error(&data, &url, IpVersion::Both, &error); @@ -770,13 +796,17 @@ where ), ) .await?; - if endpoint.url.scheme() != "ring" && !data.protocol.supports_scheme(endpoint.url.scheme()) { + let uses_external_tunnel = data.host.supports_external_tunnel(endpoint.url.scheme()); + if endpoint.url.scheme() != "ring" + && !uses_external_tunnel + && !data.protocol.supports_scheme(endpoint.url.scheme()) + { anyhow::bail!( "unsupported client protocol upgrader: {}", endpoint.url.scheme() ); } - let transport = (endpoint.url.scheme() != "ring") + let transport = (endpoint.url.scheme() != "ring" && !uses_external_tunnel) .then(|| ManualTransport::from_url(&endpoint.url)) .transpose()?; let resolved = match transport { @@ -822,6 +852,15 @@ where if endpoint.url.scheme() == "ring" { return connect_ring_tunnel(&data.ring_registry, &endpoint.url); } + if uses_external_tunnel { + return data + .host + .connect_external_tunnel(&endpoint.url) + .await? + .ok_or_else(|| { + anyhow::anyhow!("host did not provide external tunnel for {}", endpoint.url) + }); + } let transport = transport.expect("non-Ring endpoint should have a transport"); let connected = match resolved { Some((remote_addr, bind_addrs)) => { diff --git a/easytier-core/src/gateway/dataplane/operation.rs b/easytier-core/src/gateway/dataplane/operation.rs index 1ede188e..cdab3b60 100644 --- a/easytier-core/src/gateway/dataplane/operation.rs +++ b/easytier-core/src/gateway/dataplane/operation.rs @@ -53,6 +53,7 @@ pub enum DataPlaneOperationKind { UdpBind = 6, UdpReceive = 7, UdpSend = 8, + TcpShutdownWrite = 9, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -100,6 +101,7 @@ pub enum DataPlaneOperationResult { TcpWritten { len: usize, }, + TcpWriteShutdown, UdpBound { socket: DataPlaneResourceId, local_addr: SocketAddr, @@ -136,6 +138,7 @@ impl DataPlaneOperationResult { Self::UdpBound { socket, .. } => Some(*socket), Self::TcpRead { .. } | Self::TcpWritten { .. } + | Self::TcpWriteShutdown | Self::UdpReceived { .. } | Self::UdpSent { .. } => None, } diff --git a/easytier-core/src/gateway/dataplane/session.rs b/easytier-core/src/gateway/dataplane/session.rs index bc475d37..8140ab19 100644 --- a/easytier-core/src/gateway/dataplane/session.rs +++ b/easytier-core/src/gateway/dataplane/session.rs @@ -168,6 +168,7 @@ enum PendingOperationResult { eof: bool, }, TcpWritten(usize), + TcpWriteShutdown, UdpBound(DataPlaneUdpSocket), UdpReceived { data: Vec, @@ -624,6 +625,36 @@ where Ok(operation_id) } + pub fn submit_tcp_shutdown_write( + self: &Arc, + stream_id: DataPlaneResourceId, + ) -> DataPlaneResult { + Self::ensure_executor()?; + let (stream, operation_id, cancel) = { + let mut state = self.lock_state(); + let stream = Self::require_tcp(&state, stream_id)?; + let (operation_id, cancel) = self.admit_locked( + &mut state, + DataPlaneOperationKind::TcpShutdownWrite, + Some(stream_id), + 0, + false, + )?; + (stream, operation_id, cancel) + }; + self.spawn_operation(operation_id, async move { + stream + .write_deadline + .run(cancel, async { + stream.write.lock().await.shutdown().await?; + Ok::<_, std::io::Error>(()) + }) + .await?; + Ok(PendingOperationResult::TcpWriteShutdown) + }); + Ok(operation_id) + } + pub fn submit_udp_bind( self: &Arc, local_port: u16, @@ -880,6 +911,7 @@ where DataPlaneOperationResult::TcpRead { data, eof } } PendingOperationResult::TcpWritten(len) => DataPlaneOperationResult::TcpWritten { len }, + PendingOperationResult::TcpWriteShutdown => DataPlaneOperationResult::TcpWriteShutdown, PendingOperationResult::UdpBound(socket) => { let local_addr = socket.local_addr(); let socket = Self::insert_udp_resource_locked(resources, socket)?; diff --git a/easytier-core/src/gateway/dataplane/tests.rs b/easytier-core/src/gateway/dataplane/tests.rs index 83b2c39f..8ac22793 100644 --- a/easytier-core/src/gateway/dataplane/tests.rs +++ b/easytier-core/src/gateway/dataplane/tests.rs @@ -327,6 +327,55 @@ async fn data_plane_sessions_complete_tcp_operations_end_to_end() { assert_eq!(written, 4); assert_eq!(received, b"ping"); + let eof_read = session_b.submit_tcp_read(server, 16).unwrap(); + let shutdown = session_a.submit_tcp_shutdown_write(client).unwrap(); + let shutdown_completion = wait_for_session_completion(&session_a).await; + let eof_completion = wait_for_session_completion(&session_b).await; + assert_eq!(shutdown_completion.operation_id, shutdown); + assert_eq!(eof_completion.operation_id, eof_read); + session_a + .take_result_with(shutdown, |outcome| match outcome { + Ok(DataPlaneOperationResult::TcpWriteShutdown) => Some(()), + _ => None, + }) + .unwrap() + .unwrap(); + let eof = session_b + .take_result_with(eof_read, |outcome| match outcome { + Ok(DataPlaneOperationResult::TcpRead { data, eof }) => Some((data.clone(), *eof)), + _ => None, + }) + .unwrap() + .unwrap(); + assert_eq!(eof, (Vec::new(), true)); + + let response_read = session_a.submit_tcp_read(client, 16).unwrap(); + let response_write = session_b + .submit_tcp_write(server, b"pong".to_vec()) + .unwrap(); + let (response_read_completion, response_write_completion) = tokio::join!( + wait_for_session_completion(&session_a), + wait_for_session_completion(&session_b), + ); + assert_eq!(response_read_completion.operation_id, response_read); + assert_eq!(response_write_completion.operation_id, response_write); + let response = session_a + .take_result_with(response_read, |outcome| match outcome { + Ok(DataPlaneOperationResult::TcpRead { data, eof }) if !eof => Some(data.clone()), + _ => None, + }) + .unwrap() + .unwrap(); + let response_len = session_b + .take_result_with(response_write, |outcome| match outcome { + Ok(DataPlaneOperationResult::TcpWritten { len }) => Some(*len), + _ => None, + }) + .unwrap() + .unwrap(); + assert_eq!(response, b"pong"); + assert_eq!(response_len, 4); + let blocked_read = session_b.submit_tcp_read(server, 16).unwrap(); session_b.close_resource(server); let close_completion = wait_for_session_completion(&session_b).await; diff --git a/easytier-core/src/gateway/smoltcp/tokio_smoltcp/socket.rs b/easytier-core/src/gateway/smoltcp/tokio_smoltcp/socket.rs index 0bab7442..714e5c46 100644 --- a/easytier-core/src/gateway/smoltcp/tokio_smoltcp/socket.rs +++ b/easytier-core/src/gateway/smoltcp/tokio_smoltcp/socket.rs @@ -199,11 +199,14 @@ impl AsyncWrite for TcpStream { fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut socket = self.reactor.get_socket::(*self.handle); - if socket.is_open() { + if socket.may_send() { socket.close(); self.reactor.notify(); } - if socket.state() == tcp::State::Closed { + if matches!( + socket.state(), + tcp::State::FinWait2 | tcp::State::TimeWait | tcp::State::Closed + ) { return Poll::Ready(Ok(())); } diff --git a/easytier-core/src/host/mod.rs b/easytier-core/src/host/mod.rs index 4c955685..3925f10b 100644 --- a/easytier-core/src/host/mod.rs +++ b/easytier-core/src/host/mod.rs @@ -15,3 +15,4 @@ pub mod packet; pub mod socket; #[cfg(test)] pub(crate) mod testkit; +pub mod tunnel; diff --git a/easytier-core/src/host/socket/mod.rs b/easytier-core/src/host/socket/mod.rs index 8e295343..a27f49d7 100644 --- a/easytier-core/src/host/socket/mod.rs +++ b/easytier-core/src/host/socket/mod.rs @@ -184,7 +184,7 @@ impl HostSocketRuntime { } } - pub(in crate::host) async fn run_operation( + pub(crate) async fn run_operation( &self, io: Arc, submit: impl FnOnce(&I, HostOperationId) -> io::Result<()>, diff --git a/easytier-core/src/host/tunnel.rs b/easytier-core/src/host/tunnel.rs new file mode 100644 index 00000000..d6db54e7 --- /dev/null +++ b/easytier-core/src/host/tunnel.rs @@ -0,0 +1,36 @@ +//! Host-backed message tunnels. +//! +//! The host owns the concrete transport and exposes complete EasyTier tunnel +//! payloads. Core owns packet semantics and the peer lifecycle above them. + +use std::{io, task::Poll}; + +use super::socket::{HostOperationId, HostSocketHandle, HostSocketIo}; + +/// Maximum message payload accepted from a host tunnel adapter. +pub const MAX_HOST_TUNNEL_PAYLOAD_LEN: usize = 1024 * 1024; + +/// Mechanical host I/O below EasyTier's message-tunnel seam. +/// +/// Submit methods copy their complete input before returning. A receive keeps +/// the host transport's message boundary intact. A clean remote close is +/// reported as [`io::ErrorKind::UnexpectedEof`] by `take_receive`. +pub trait HostTunnelIo: HostSocketIo { + fn submit_receive( + &self, + handle: HostSocketHandle, + operation: HostOperationId, + capacity: usize, + ) -> io::Result<()>; + + fn take_receive(&self, operation: HostOperationId) -> Poll>>; + + fn submit_send( + &self, + handle: HostSocketHandle, + operation: HostOperationId, + source: &[u8], + ) -> io::Result<()>; + + fn take_send(&self, operation: HostOperationId) -> Poll>; +} diff --git a/easytier-core/src/instance/config.rs b/easytier-core/src/instance/config.rs index 3e5fa9fe..7cb91dd4 100644 --- a/easytier-core/src/instance/config.rs +++ b/easytier-core/src/instance/config.rs @@ -28,7 +28,7 @@ use crate::{ use easytier_proto::common::CompressionAlgoPb; -use super::{CoreConnectivityConfig, CoreInstanceConfig}; +use super::{CoreConnectivityConfig, CoreConnectivityMode, CoreInstanceConfig}; const OSPF_UPDATE_MY_FOREIGN_NETWORK_INTERVAL_SEC: u64 = 10; const MAX_DIRECT_CONNS_PER_PEER_IN_FOREIGN_NETWORK: usize = 3; @@ -58,6 +58,7 @@ pub struct CoreInstanceHostConfig { pub upnp_enabled: bool, pub tcp_hole_punching_enabled: bool, pub ignore_unsupported_config: bool, + pub connectivity: CoreConnectivityMode, pub easytier_version: String, pub endpoint_protocols: Vec, } @@ -83,6 +84,7 @@ impl Default for CoreInstanceHostConfig { upnp_enabled: true, tcp_hole_punching_enabled: true, ignore_unsupported_config: false, + connectivity: CoreConnectivityMode::Full, easytier_version: env!("CARGO_PKG_VERSION").to_owned(), endpoint_protocols: ManualEndpointDiscoveryConfig::default().srv_protocols, } @@ -352,6 +354,8 @@ impl CoreInstanceConfig { runtime, startup_plan: super::CoreInstanceStartupPlan { gateway: host.gateway_enabled, + packet_proxy: host.proxy_enabled, + connectivity: host.connectivity, }, stun: StunServerConfig { udp_servers: stun_servers @@ -588,6 +592,7 @@ disable_p2p = true icmp_failure_is_fatal: true, public_ipv6_provider_supported: true, gateway_enabled: false, + connectivity: CoreConnectivityMode::InboundOnly, easytier_version: "host-version".to_owned(), endpoint_protocols: vec!["host-protocol".to_owned()], ..Default::default() @@ -606,6 +611,10 @@ disable_p2p = true .as_deref(), Some("host-fallback") ); + assert_eq!( + normalized.connectivity.startup_plan.connectivity, + CoreConnectivityMode::InboundOnly + ); assert!( normalized .peer @@ -677,6 +686,7 @@ data_compress_algo = "Zstd" let normalized = CoreInstanceConfig::from_toml_with_host(&config, &host).unwrap(); assert_eq!(config.dump(), before); + assert!(!normalized.connectivity.startup_plan.packet_proxy); assert_eq!(normalized.connectivity.initial_peers.len(), 1); assert_eq!( normalized diff --git a/easytier-core/src/instance/lifecycle.rs b/easytier-core/src/instance/lifecycle.rs index 499a8ac9..18139b39 100644 --- a/easytier-core/src/instance/lifecycle.rs +++ b/easytier-core/src/instance/lifecycle.rs @@ -111,10 +111,16 @@ where .ok_or_else(|| anyhow::anyhow!("packet egress is one-shot and already started"))?; self.packet_egress.start(packet_receiver).await?; self.peer_manager.run().await.map_err(anyhow::Error::from)?; - self.direct.run(); + if let Some(direct) = &self.direct { + direct.run(); + } #[cfg(feature = "tcp-hole-punch")] - self.tcp_hole_punch.run(); - self.manual.start(); + if let Some(tcp_hole_punch) = &self.tcp_hole_punch { + tcp_hole_punch.run(); + } + if let Some(manual) = &self.manual { + manual.start(); + } #[cfg(feature = "public-ipv6-provider")] self.public_ipv6_provider.start().await; @@ -128,8 +134,12 @@ where wrapped_transport.start().await?; } #[cfg(feature = "proxy-packet")] - self.start_packet_proxy().await?; - self.udp_hole_punch.start().await?; + if self.startup_plan.packet_proxy { + self.start_packet_proxy().await?; + } + if let Some(udp_hole_punch) = &self.udp_hole_punch { + udp_hole_punch.start().await?; + } self.peer_center.init().await; #[cfg(feature = "proxy-cidr-monitor")] self.proxy_cidr_monitor @@ -170,7 +180,9 @@ where if let Some(listener) = &self.listener { listener.stop().await; } - self.udp_hole_punch.stop().await; + if let Some(udp_hole_punch) = &self.udp_hole_punch { + udp_hole_punch.stop().await; + } #[cfg(feature = "proxy-smoltcp-stack")] self.port_forward_adapter.stop().await; #[cfg(feature = "proxy-smoltcp-stack")] @@ -185,10 +197,16 @@ where } #[cfg(feature = "proxy-packet")] self.packet_proxy.stop().await; - self.manual.stop().await; + if let Some(manual) = &self.manual { + manual.stop().await; + } #[cfg(feature = "tcp-hole-punch")] - self.tcp_hole_punch.stop().await; - self.direct.stop().await; + if let Some(tcp_hole_punch) = &self.tcp_hole_punch { + tcp_hole_punch.stop().await; + } + if let Some(direct) = &self.direct { + direct.stop().await; + } self.peer_center.stop().await; // Host packet tasks can still call the packet plane, so stop them diff --git a/easytier-core/src/instance/management.rs b/easytier-core/src/instance/management.rs index 001dbfd9..b8ec79ec 100644 --- a/easytier-core/src/instance/management.rs +++ b/easytier-core/src/instance/management.rs @@ -30,19 +30,29 @@ where } pub fn add_connector(&self, url: Url) -> anyhow::Result<()> { - self.manual.add_connector(url) + self.manual + .as_ref() + .ok_or_else(|| anyhow::anyhow!("inbound-only connectivity has no outbound connectors"))? + .add_connector(url) } pub fn remove_connector(&self, url: &Url) -> bool { - self.manual.remove_connector(url) + self.manual + .as_ref() + .is_some_and(|manual| manual.remove_connector(url)) } pub fn clear_connectors(&self) { - self.manual.clear_connectors(); + if let Some(manual) = &self.manual { + manual.clear_connectors(); + } } pub fn list_connectors(&self) -> Vec { - self.manual.list_connectors() + self.manual + .as_ref() + .map(|manual| manual.list_connectors()) + .unwrap_or_default() } pub fn running_listeners(&self) -> Vec { @@ -92,10 +102,11 @@ where .peer_manager .node_snapshot(self.running_listeners()) .await; - snapshot.ip_list = self - .direct - .local_address_observations_with_stun(&snapshot.stun_info) - .await; + if let Some(direct) = &self.direct { + snapshot.ip_list = direct + .local_address_observations_with_stun(&snapshot.stun_info) + .await; + } snapshot } diff --git a/easytier-core/src/instance/mod.rs b/easytier-core/src/instance/mod.rs index c208446f..9bad5b3b 100644 --- a/easytier-core/src/instance/mod.rs +++ b/easytier-core/src/instance/mod.rs @@ -64,8 +64,8 @@ use crate::{ packet::{HostPacketReceiver, PacketSink, host_packet_channel}, }, listener::{ - AcceptedSocketHandler, ExternalListenerFactory, ExternalListenerRequest, ListenerFactory, - RunningListenerRegistry, + AcceptedSocketHandler, ExternalListenerFactory, ExternalListenerRequest, + HostListenerRegistration, ListenerFactory, RunningListenerRegistry, plan::{ListenerRuntimeConfig, PreparedListenerPlan, prepare_listener_plan}, transport::{ AcceptedTransport, CoreListenerRuntime, HostAcceptedTcpSocket, @@ -146,9 +146,18 @@ impl CoreInstanceState { } } +fn default_true() -> bool { + true +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct CoreInstanceStartupPlan { + #[serde(default = "default_true")] pub gateway: bool, + #[serde(default = "default_true")] + pub packet_proxy: bool, + #[serde(default)] + pub connectivity: CoreConnectivityMode, } impl CoreInstanceStartupPlan { @@ -159,10 +168,26 @@ impl CoreInstanceStartupPlan { impl Default for CoreInstanceStartupPlan { fn default() -> Self { - Self { gateway: true } + Self { + gateway: true, + packet_proxy: true, + connectivity: CoreConnectivityMode::Full, + } } } +/// Selects which portable connectivity Modules participate in one instance. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum CoreConnectivityMode { + #[default] + Full, + /// Dial configured peers without listeners, discovery, or direct connectivity. + OutboundOnly, + /// Accept Host-registered listeners without constructing outbound socket Modules. + InboundOnly, +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct CoreConnectivityConfig { pub initial_peers: Vec, @@ -279,6 +304,7 @@ where pub protocol: Option::Socket>>>, pub external_listener_factory: Option>>>>, + pub host_listener_registrations: Vec, pub server_protocol: Option>>>, /// Optional OS port-mapping adapter. STUN-only hole punching remains /// available when the host does not provide one. @@ -338,6 +364,7 @@ where wrapped_transports: WrappedTransportEngines::default(), protocol: None, external_listener_factory: None, + host_listener_registrations: Vec::new(), server_protocol: None, udp_hole_punch_platform: None, #[cfg(feature = "proxy-packet")] @@ -380,13 +407,13 @@ where pub(super) cancel: CancellationToken, pub(super) peer_manager: Arc, packet_plane: Arc, - pub(super) manual: ManualConnectorManager, - pub(super) direct: DirectConnectorManager, + pub(super) manual: Option>, + pub(super) direct: Option>, #[cfg(feature = "tcp-hole-punch")] - tcp_hole_punch: TcpHolePunchConnector, + tcp_hole_punch: Option>, pub(super) listener: Option>>, running_listeners: Arc, - pub(super) udp_hole_punch: CoreUdpHolePunchService, + pub(super) udp_hole_punch: Option>, #[cfg(feature = "wrapped-transport")] wrapped_transport: Option>, #[cfg(feature = "proxy-smoltcp-stack")] @@ -411,7 +438,7 @@ where public_ipv6_provider: PublicIpv6ProviderRuntime, #[cfg(feature = "vpn-portal")] vpn_portal: Arc, - #[cfg(feature = "proxy-smoltcp-stack")] + #[cfg(feature = "proxy-packet")] pub(super) startup_plan: CoreInstanceStartupPlan, pub(super) runtime_config: CoreRuntimeConfigStore, #[cfg(feature = "test-utils")] @@ -472,6 +499,7 @@ where mut adapters: CoreHostAdapters, ) -> anyhow::Result> { build_capabilities::validate(&config)?; + let connectivity_mode = config.connectivity.startup_plan.connectivity; let instance_name = config.instance_name; #[cfg(feature = "vpn-portal")] let vpn_portal_config = config.vpn_portal.clone(); @@ -497,17 +525,26 @@ where public_ipv6_host, public_ipv6_events, ); - let stun = Self::prepare_stun(&adapters, &config.connectivity); - let peer_stun: Arc = stun.clone(); - let foreign_rpc_registrar = Arc::new(ForeignDirectConnectorRpcRegistrar::new( - adapters.host.clone(), - stun.clone(), - )); + let stun = (connectivity_mode == CoreConnectivityMode::Full) + .then(|| Self::prepare_stun(&adapters, &config.connectivity)); + let (peer_stun, foreign_rpc_registrar): ( + Arc, + Arc, + ) = match &stun { + Some(stun) => ( + Arc::new(CoreStunPeerInfoSource(stun.clone())), + Arc::new(ForeignDirectConnectorRpcRegistrar::new( + adapters.host.clone(), + stun.clone(), + )), + ), + None => (Arc::new(()), Arc::new(())), + }; let peer_manager = Arc::new(PeerManagerCore::new( config.peer, config.managed_credentials, runtime_config.clone(), - Arc::new(CoreStunPeerInfoSource(peer_stun)), + peer_stun, packet_tx, public_ipv6_runtime.clone(), events.clone(), @@ -515,8 +552,11 @@ where foreign_rpc_registrar, )?); let config = config.connectivity; + let configured_listeners = (connectivity_mode != CoreConnectivityMode::OutboundOnly) + .then_some(config.listeners.as_ref()) + .flatten(); let listener_plan = prepare_listener_plan( - config.listeners.as_ref(), + configured_listeners, peer_manager.instance_id(), adapters.server_protocol.as_deref(), adapters.external_listener_factory.as_deref(), @@ -536,6 +576,7 @@ where wrapped_transports, protocol, external_listener_factory, + host_listener_registrations, server_protocol, udp_hole_punch_platform, #[cfg(feature = "proxy-packet")] @@ -549,6 +590,12 @@ where #[cfg(feature = "vpn-portal")] vpn_portal, } = adapters; + let host_listener_registrations = if connectivity_mode == CoreConnectivityMode::OutboundOnly + { + Vec::new() + } else { + host_listener_registrations + }; let dns_records: Arc = dns.clone(); let dns: Arc = dns; let ring_registry = process_runtime.ring_registry(); @@ -563,19 +610,22 @@ where manual: manual_options, direct: direct_options, } = config; - #[cfg(not(feature = "proxy-smoltcp-stack"))] + #[cfg(not(feature = "proxy-packet"))] let _ = startup_plan; + if connectivity_mode == CoreConnectivityMode::InboundOnly && !initial_peers.is_empty() { + anyhow::bail!("inbound-only connectivity does not support outbound peers"); + } + let accepted_tunnel_handler = PeerAcceptedTunnelHandler::new(&peer_manager, events.clone()); let accepted_transport_handler: Arc< dyn AcceptedSocketHandler>>, > = match server_protocol { - Some(server_protocol) => { - let tunnel_handler = PeerAcceptedTunnelHandler::new(&peer_manager, events.clone()); - Arc::new(ProtocolAcceptedTransportHandler::new( - &tunnel_handler, - server_protocol, - )) - } - None => Arc::new(RawAcceptedTransportHandler::new(&peer_manager)), + Some(server_protocol) => Arc::new(ProtocolAcceptedTransportHandler::new( + &accepted_tunnel_handler, + server_protocol, + )), + None => Arc::new(RawAcceptedTransportHandler::new( + accepted_tunnel_handler.clone(), + )), }; let running_listeners = Arc::new(RunningListenerRegistry::default()); let PreparedListenerPlan { @@ -583,8 +633,11 @@ where external, failures, } = listener_plan; - let mut external_factories = Vec::with_capacity(external.len()); - if !external.is_empty() && external_listener_factory.is_none() { + let mut external_factories = + Vec::with_capacity(external.len() + host_listener_registrations.len()); + if (!external.is_empty() || !host_listener_registrations.is_empty()) + && external_listener_factory.is_none() + { anyhow::bail!("listener plan requires an external listener factory"); } for (listener, socket_context) in external { @@ -598,6 +651,19 @@ where listener.must_succeed, )); } + for request in host_listener_registrations { + let factory = external_listener_factory.clone().unwrap(); + if !factory.supports_scheme(request.url.scheme()) { + anyhow::bail!( + "external listener factory does not support Host listener scheme {}", + request.url.scheme() + ); + } + external_factories.push(ListenerFactory::new( + move || factory.create(request.clone()), + true, + )); + } let has_listener_work = !transports.is_empty() || !external_factories.is_empty() || !failures.is_empty(); let listener = has_listener_work.then(|| { @@ -613,40 +679,51 @@ where running_listeners.clone(), )) }); - let protocol = protocol.unwrap_or_else(|| { - Arc::new(CoreClientProtocolUpgrader::new( - CoreClientProtocolConfig::default(), - )) + let protocol = (connectivity_mode != CoreConnectivityMode::InboundOnly).then(|| { + protocol.unwrap_or_else(|| { + Arc::new(CoreClientProtocolUpgrader::new( + CoreClientProtocolConfig::default(), + )) + }) }); - let endpoint_resolver = Arc::new(CoreManualEndpointResolver::new( - host.clone(), - dns.clone(), - dns_records, - endpoint_discovery, - )); - let manual = ManualConnectorManager::new( - peer_manager.clone(), - host.clone(), - dns.clone(), - endpoint_resolver, - protocol.clone(), - ring_registry, - manual_options, - events.clone(), - ); - for url in initial_peers { - manual.add_connector(url)?; - } - let udp_hole_punch_socket_context = direct_options.udp_bind.context.clone(); - let udp_hole_punch = CoreUdpHolePunchService::new( - peer_manager.clone(), - host.clone(), - stun.clone(), - udp_hole_punch_platform, - events.clone(), - udp_hole_punch_socket_context, - protocol.clone(), - ); + let manual = if let Some(protocol) = &protocol { + let endpoint_resolver = Arc::new(CoreManualEndpointResolver::new( + host.clone(), + dns.clone(), + dns_records, + endpoint_discovery, + )); + let manual = ManualConnectorManager::new( + peer_manager.clone(), + host.clone(), + dns.clone(), + endpoint_resolver, + protocol.clone(), + ring_registry, + manual_options, + events.clone(), + ); + for url in initial_peers { + manual.add_connector(url)?; + } + Some(manual) + } else { + None + }; + let udp_hole_punch = stun + .as_ref() + .zip(protocol.as_ref()) + .map(|(stun, protocol)| { + CoreUdpHolePunchService::new( + peer_manager.clone(), + host.clone(), + stun.clone(), + udp_hole_punch_platform, + events.clone(), + direct_options.udp_bind.context.clone(), + protocol.clone(), + ) + }); let proxy_cidr_table = Arc::new(ProxyCidrTable::from_snapshot(proxy_cidr_snapshot( runtime_config.snapshot().as_ref(), ))); @@ -708,28 +785,38 @@ where events.clone(), ); #[cfg(feature = "tcp-hole-punch")] - let tcp_hole_punch = TcpHolePunchConnector::new( - peer_manager.clone(), - host.clone(), - stun.clone(), - direct_options.tcp_bind.context.clone(), - protocol.clone(), - Arc::new(crate::connectivity::protocol::CoreServerProtocolUpgrader::< - HostAcceptedTcpSocket, - >::new( - crate::connectivity::protocol::CoreServerProtocolConfig::default(), - )), - ); - let direct = DirectConnectorManager::new_with_running_listeners( - peer_manager.clone(), - host.clone(), - protected_tcp_ports, - stun.clone(), - running_listeners.clone(), - dns, - protocol, - direct_options, - ); + let tcp_hole_punch = stun + .as_ref() + .zip(protocol.as_ref()) + .map(|(stun, protocol)| { + TcpHolePunchConnector::new( + peer_manager.clone(), + host.clone(), + stun.clone(), + direct_options.tcp_bind.context.clone(), + protocol.clone(), + Arc::new(crate::connectivity::protocol::CoreServerProtocolUpgrader::< + HostAcceptedTcpSocket, + >::new( + crate::connectivity::protocol::CoreServerProtocolConfig::default(), + )), + ) + }); + let direct = match (stun, protocol) { + (Some(stun), Some(protocol)) => { + Some(DirectConnectorManager::new_with_running_listeners( + peer_manager.clone(), + host.clone(), + protected_tcp_ports, + stun, + running_listeners.clone(), + dns, + protocol, + direct_options, + )) + } + _ => None, + }; let peer_center = Arc::new(PeerCenterInstance::new(peer_manager.clone())); #[cfg(feature = "public-ipv6-provider")] let public_ipv6_provider = PublicIpv6ProviderRuntime::new( @@ -799,7 +886,7 @@ where public_ipv6_provider, #[cfg(feature = "vpn-portal")] vpn_portal, - #[cfg(feature = "proxy-smoltcp-stack")] + #[cfg(feature = "proxy-packet")] startup_plan, runtime_config, #[cfg(feature = "test-utils")] diff --git a/easytier-core/src/instance/tests.rs b/easytier-core/src/instance/tests.rs index 13de336d..c2031a03 100644 --- a/easytier-core/src/instance/tests.rs +++ b/easytier-core/src/instance/tests.rs @@ -185,6 +185,10 @@ fn core_instance_config_round_trips_as_normalized_json() { assert!(!decoded.connectivity.direct.testing); assert!(decoded.connectivity.startup_plan.gateway); + assert_eq!( + decoded.connectivity.startup_plan.connectivity, + CoreConnectivityMode::Full + ); assert_eq!(serde_json::to_value(&decoded).unwrap(), encoded); let mut legacy = encoded; @@ -3016,4 +3020,88 @@ virtual_ip = "10.82.0.2/24" instance.stop().await; assert!(instance.running_listeners().is_empty()); } + + #[tokio::test] + async fn inbound_only_uses_host_registered_listener_lifecycle() { + let external_url: Url = "unix:///tmp/easytier-host-listener-test".parse().unwrap(); + let mut config = test_config("host-listener"); + config.connectivity.startup_plan.connectivity = CoreConnectivityMode::InboundOnly; + let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16); + let mut adapters = adapters( + Some(Arc::new(ReadyExternalListenerFactory)), + Arc::new(packet_sink), + ); + adapters + .host_listener_registrations + .push(ExternalListenerRequest { + url: external_url.clone(), + socket_context: SocketContext::default(), + }); + let instance = CoreInstance::new(config, adapters).unwrap(); + + assert!( + instance + .add_connector("tcp://127.0.0.1:11010".parse().unwrap()) + .is_err() + ); + instance.start().await.unwrap(); + assert!(instance.running_listeners().contains(&external_url)); + + instance.stop().await; + assert!(instance.running_listeners().is_empty()); + } + + #[tokio::test] + async fn inbound_only_rejects_initial_peers() { + let mut config = test_config("inbound-only-peer"); + config.connectivity.startup_plan.connectivity = CoreConnectivityMode::InboundOnly; + config + .connectivity + .initial_peers + .push("tcp://127.0.0.1:11010".parse().unwrap()); + + let Err(error) = build_instance(config) else { + panic!("inbound-only instance accepted an outbound peer"); + }; + assert!( + error + .to_string() + .contains("inbound-only connectivity does not support outbound peers"), + "unexpected construction error: {error:#}" + ); + } + + #[tokio::test] + async fn outbound_only_ignores_configured_and_host_registered_listeners() { + let configured_url: Url = "unix:///tmp/easytier-outbound-configured-listener" + .parse() + .unwrap(); + let host_url: Url = "unix:///tmp/easytier-outbound-host-listener" + .parse() + .unwrap(); + let mut config = test_config("outbound-only-listeners"); + config.connectivity.startup_plan.connectivity = CoreConnectivityMode::OutboundOnly; + config.connectivity.listeners = Some(ListenerRuntimeConfig::new( + vec![configured_url], + false, + SocketContext::default(), + )); + let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16); + let mut adapters = adapters( + Some(Arc::new(ReadyExternalListenerFactory)), + Arc::new(packet_sink), + ); + adapters + .host_listener_registrations + .push(ExternalListenerRequest { + url: host_url, + socket_context: SocketContext::default(), + }); + let instance = CoreInstance::new(config, adapters).unwrap(); + + instance.start().await.unwrap(); + assert!(instance.running_listeners().is_empty()); + + instance.stop().await; + } } diff --git a/easytier-core/src/listener/mod.rs b/easytier-core/src/listener/mod.rs index 4f6955d0..ec118e56 100644 --- a/easytier-core/src/listener/mod.rs +++ b/easytier-core/src/listener/mod.rs @@ -15,6 +15,14 @@ use crate::{ }; pub mod plan; +#[cfg(any( + test, + all( + feature = "wasm-host-tunnel", + not(feature = "wasm-host-tunnel-outbound") + ) +))] +pub(crate) mod queue; pub mod transport; pub trait ExternalListenerFactory: Send + Sync + 'static @@ -35,6 +43,10 @@ pub struct ExternalListenerRequest { pub socket_context: SocketContext, } +/// One listener supplied by the Host rather than the portable TOML model. +/// Host listeners are mandatory: startup fails when one cannot bind. +pub type HostListenerRegistration = ExternalListenerRequest; + #[async_trait] pub trait AcceptedSocketHandler: Send + Sync { async fn handle_accepted_socket(&self, accepted: Accepted) -> anyhow::Result<()>; diff --git a/easytier-core/src/listener/queue.rs b/easytier-core/src/listener/queue.rs new file mode 100644 index 00000000..3734b10c --- /dev/null +++ b/easytier-core/src/listener/queue.rs @@ -0,0 +1,149 @@ +use std::{collections::VecDeque, sync::Mutex}; + +use tokio::sync::Notify; + +struct HostListenerQueueState { + closed: bool, + listeners: usize, + pending: VecDeque, +} + +/// Bounded handoff from a synchronous Host callback to async listeners. +pub(crate) struct HostListenerQueue { + capacity: usize, + state: Mutex>, + changed: Notify, +} + +impl HostListenerQueue { + pub(crate) fn new(capacity: usize) -> Self { + Self { + capacity, + state: Mutex::new(HostListenerQueueState { + closed: false, + listeners: 0, + pending: VecDeque::new(), + }), + changed: Notify::new(), + } + } + + pub(crate) fn register_listener(&self) -> bool { + let mut state = self.state.lock().unwrap(); + if state.closed { + return false; + } + state.listeners += 1; + true + } + + pub(crate) fn unregister_listener(&self) { + let pending = { + let mut state = self.state.lock().unwrap(); + debug_assert!(state.listeners > 0); + state.listeners -= 1; + if state.listeners != 0 { + return; + } + state.closed = true; + std::mem::take(&mut state.pending) + }; + drop(pending); + self.changed.notify_waiters(); + } + + /// Constructs `T` only after the queue accepts Host-to-guest ownership. + pub(crate) fn enqueue_with(&self, create: impl FnOnce() -> T) -> anyhow::Result<()> { + { + let mut state = self.state.lock().unwrap(); + if state.closed || state.listeners == 0 { + anyhow::bail!("Host listener queue is closed"); + } + if state.pending.len() >= self.capacity { + anyhow::bail!("Host listener admission queue is full"); + } + state.pending.push_back(create()); + } + self.changed.notify_one(); + Ok(()) + } + + pub(crate) async fn accept(&self) -> Option { + loop { + let changed = self.changed.notified(); + { + let mut state = self.state.lock().unwrap(); + if let Some(item) = state.pending.pop_front() { + return Some(item); + } + if state.closed { + return None; + } + } + changed.await; + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }; + + use super::HostListenerQueue; + + struct DropCounter(Arc); + + impl Drop for DropCounter { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::Relaxed); + } + } + + #[tokio::test] + async fn constructs_only_after_listener_accepts_ownership() { + let queue = HostListenerQueue::new(1); + let constructed = AtomicUsize::new(0); + + assert!( + queue + .enqueue_with(|| { + constructed.fetch_add(1, Ordering::Relaxed); + }) + .is_err() + ); + assert_eq!(constructed.load(Ordering::Relaxed), 0); + + assert!(queue.register_listener()); + queue + .enqueue_with(|| { + constructed.fetch_add(1, Ordering::Relaxed); + }) + .unwrap(); + assert!( + queue + .enqueue_with(|| { + constructed.fetch_add(1, Ordering::Relaxed); + }) + .is_err() + ); + assert_eq!(constructed.load(Ordering::Relaxed), 1); + queue.accept().await.unwrap(); + queue.unregister_listener(); + } + + #[test] + fn last_listener_closes_and_drains_pending_items() { + let queue = HostListenerQueue::new(1); + let drops = Arc::new(AtomicUsize::new(0)); + assert!(queue.register_listener()); + queue.enqueue_with(|| DropCounter(drops.clone())).unwrap(); + + queue.unregister_listener(); + + assert_eq!(drops.load(Ordering::Relaxed), 1); + assert!(!queue.register_listener()); + } +} diff --git a/easytier-core/src/peers/admission.rs b/easytier-core/src/peers/admission.rs index 66c690d6..f082207d 100644 --- a/easytier-core/src/peers/admission.rs +++ b/easytier-core/src/peers/admission.rs @@ -77,14 +77,12 @@ impl AcceptedTunnelHandler for PeerAcceptedTunnelHandler { } pub(crate) struct RawAcceptedTransportHandler { - peer_manager: Weak, + tunnel_handler: Arc, } impl RawAcceptedTransportHandler { - pub(crate) fn new(peer_manager: &Arc) -> Self { - Self { - peer_manager: Arc::downgrade(peer_manager), - } + pub(crate) fn new(tunnel_handler: Arc) -> Self { + Self { tunnel_handler } } } @@ -97,10 +95,6 @@ where &self, accepted: AcceptedTransport, ) -> anyhow::Result<()> { - let peer_manager = self - .peer_manager - .upgrade() - .ok_or_else(|| anyhow::anyhow!("peer manager is gone"))?; let tunnel = match accepted { AcceptedTransport::Tunnel { tunnel, .. } => tunnel, AcceptedTransport::Tcp { @@ -125,7 +119,48 @@ where remote_url, } => raw::upgrade_accepted_byte_stream(socket, local_url, remote_url)?, }; - peer_manager.add_tunnel_as_server(tunnel, true).await?; - Ok(()) + self.tunnel_handler.handle_tunnel(tunnel).await + } +} + +#[cfg(test)] +mod tests { + use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }; + + use super::*; + use crate::{host::testkit::TestTcpSocket, tunnel::ring::RingTunnelRegistry}; + + struct RecordingTunnelHandler(AtomicUsize); + + #[async_trait] + impl AcceptedTunnelHandler for RecordingTunnelHandler { + async fn handle_tunnel(&self, _tunnel: Box) -> anyhow::Result<()> { + self.0.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + } + + #[tokio::test] + async fn raw_transport_delegates_tunnel_to_shared_admission_handler() { + let registry = Arc::new(RingTunnelRegistry::default()); + let local_id = uuid::Uuid::new_v4(); + let mut listener = registry.bind(local_id).unwrap(); + let _client = registry.connect(local_id).unwrap(); + let tunnel = listener.accept().await.unwrap().into_tunnel(); + let recorder = Arc::new(RecordingTunnelHandler(AtomicUsize::new(0))); + let handler = RawAcceptedTransportHandler::new(recorder.clone()); + + handler + .handle_accepted_socket(AcceptedTransport::::Tunnel { + tunnel, + local_url: format!("ring://{local_id}").parse().unwrap(), + }) + .await + .unwrap(); + + assert_eq!(recorder.0.load(Ordering::Relaxed), 1); } } diff --git a/easytier-core/src/tunnel/host_tunnel.rs b/easytier-core/src/tunnel/host_tunnel.rs new file mode 100644 index 00000000..5f65ccbb --- /dev/null +++ b/easytier-core/src/tunnel/host_tunnel.rs @@ -0,0 +1,221 @@ +//! EasyTier message tunnel over a host-owned transport. + +use std::{io, sync::Arc}; + +use futures::{sink, stream}; +use url::Url; + +use crate::{ + host::{ + socket::{HostSocketHandle, HostSocketRuntime}, + tunnel::{HostTunnelIo, MAX_HOST_TUNNEL_PAYLOAD_LEN}, + }, + packet::{ZCPacket, ZCPacketType}, + proto::common::TunnelInfo, +}; + +use super::{Tunnel, TunnelError, wrapper::TunnelWrapper}; + +struct HostTunnelResource { + io: Arc, + handle: HostSocketHandle, +} + +impl Drop for HostTunnelResource { + fn drop(&mut self) { + let _ = self.io.close(self.handle); + } +} + +/// Builds a message-preserving EasyTier tunnel around a host-owned transport. +pub fn new_host_tunnel( + runtime: HostSocketRuntime, + io: Arc, + handle: HostSocketHandle, + local_url: Url, + remote_url: Url, + resolved_remote_url: Option, +) -> Box { + let resource = Arc::new(HostTunnelResource { io, handle }); + let reader_resource = resource.clone(); + let reader_runtime = runtime.clone(); + let reader = stream::unfold( + (reader_runtime, reader_resource), + |(runtime, resource)| async move { + let result = runtime + .run_operation( + resource.io.clone(), + |io, operation| { + io.submit_receive(resource.handle, operation, MAX_HOST_TUNNEL_PAYLOAD_LEN) + }, + |io, operation| io.take_receive(operation), + |io, operation| io.cancel_operation(operation), + ) + .await; + let item = match result { + Ok(message) => Ok(ZCPacket::new_from_buf( + bytes::BytesMut::from(message.as_slice()), + ZCPacketType::DummyTunnel, + )), + Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => return None, + Err(error) => Err(TunnelError::IOError(error)), + }; + Some((item, (runtime, resource))) + }, + ); + + let writer = sink::unfold( + (runtime, resource), + |(runtime, resource), packet: ZCPacket| async move { + let payload = packet.tunnel_payload_bytes(); + runtime + .run_operation( + resource.io.clone(), + |io, operation| io.submit_send(resource.handle, operation, &payload), + |io, operation| io.take_send(operation), + |io, operation| io.cancel_operation(operation), + ) + .await + .map_err(TunnelError::IOError)?; + Ok((runtime, resource)) + }, + ); + + let remote_addr = remote_url.clone().into(); + let resolved_remote_addr = resolved_remote_url + .unwrap_or_else(|| remote_url.clone()) + .into(); + let info = TunnelInfo { + tunnel_type: local_url.scheme().to_owned(), + local_addr: Some(local_url.into()), + remote_addr: Some(remote_addr), + resolved_remote_addr: Some(resolved_remote_addr), + }; + Box::new(TunnelWrapper::new(reader, writer, Some(info))) +} + +#[cfg(test)] +mod tests { + use std::{ + collections::{HashMap, VecDeque}, + sync::{ + Mutex, + atomic::{AtomicUsize, Ordering}, + }, + task::Poll, + }; + + use futures::{SinkExt as _, StreamExt as _}; + + use super::*; + use crate::host::socket::{HostOperationId, HostSocketIo}; + + #[derive(Default)] + struct MockTunnelIo { + incoming: Mutex>>>, + receives: Mutex>>>, + sends: Mutex>>, + sent: Mutex>>, + closes: AtomicUsize, + } + + impl HostSocketIo for MockTunnelIo { + fn cancel_operation(&self, operation: HostOperationId) -> io::Result<()> { + self.receives.lock().unwrap().remove(&operation); + self.sends.lock().unwrap().remove(&operation); + Ok(()) + } + + fn close(&self, _handle: HostSocketHandle) -> io::Result<()> { + self.closes.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + } + + impl HostTunnelIo for MockTunnelIo { + fn submit_receive( + &self, + _handle: HostSocketHandle, + operation: HostOperationId, + _capacity: usize, + ) -> io::Result<()> { + let result = self.incoming.lock().unwrap().pop_front().unwrap(); + self.receives.lock().unwrap().insert(operation, result); + Ok(()) + } + + fn take_receive(&self, operation: HostOperationId) -> Poll>> { + Poll::Ready(self.receives.lock().unwrap().remove(&operation).unwrap()) + } + + fn submit_send( + &self, + _handle: HostSocketHandle, + operation: HostOperationId, + source: &[u8], + ) -> io::Result<()> { + self.sends + .lock() + .unwrap() + .insert(operation, source.to_vec()); + Ok(()) + } + + fn take_send(&self, operation: HostOperationId) -> Poll> { + let message = self.sends.lock().unwrap().remove(&operation).unwrap(); + self.sent.lock().unwrap().push(message); + Poll::Ready(Ok(())) + } + } + + fn tunnel(io: Arc) -> Box { + new_host_tunnel( + HostSocketRuntime::new(), + io, + HostSocketHandle(7), + Url::parse("test-tunnel://relay.example/").unwrap(), + Url::parse("test-tunnel://client.example/").unwrap(), + None, + ) + } + + #[test] + fn preserves_message_boundaries_and_closes_once() { + let io = Arc::new(MockTunnelIo::default()); + io.incoming.lock().unwrap().push_back(Ok(vec![1, 2, 3])); + io.incoming.lock().unwrap().push_back(Ok(vec![8, 9])); + let tunnel = tunnel(io.clone()); + assert_eq!(tunnel.info().unwrap().tunnel_type, "test-tunnel"); + let (mut reader, mut writer) = tunnel.split(); + + let first = futures::executor::block_on(reader.next()).unwrap().unwrap(); + let second = futures::executor::block_on(reader.next()).unwrap().unwrap(); + assert_eq!(first.tunnel_payload(), &[1, 2, 3]); + assert_eq!(second.tunnel_payload(), &[8, 9]); + + let packet = ZCPacket::new_from_buf( + bytes::BytesMut::from(&[4, 5, 6][..]), + ZCPacketType::DummyTunnel, + ); + futures::executor::block_on(writer.send(packet)).unwrap(); + assert_eq!(*io.sent.lock().unwrap(), vec![vec![4, 5, 6]]); + + drop(tunnel); + drop(reader); + drop(writer); + assert_eq!(io.closes.load(Ordering::SeqCst), 1); + } + + #[test] + fn maps_clean_remote_close_to_stream_eof() { + let io = Arc::new(MockTunnelIo::default()); + io.incoming + .lock() + .unwrap() + .push_back(Err(io::Error::new(io::ErrorKind::UnexpectedEof, "closed"))); + let tunnel = tunnel(io); + let (mut reader, _writer) = tunnel.split(); + + assert!(futures::executor::block_on(reader.next()).is_none()); + } +} diff --git a/easytier-core/src/tunnel/mod.rs b/easytier-core/src/tunnel/mod.rs index 7ba9878d..1a6b046c 100644 --- a/easytier-core/src/tunnel/mod.rs +++ b/easytier-core/src/tunnel/mod.rs @@ -18,6 +18,7 @@ pub use crate::socket::IpVersion; pub(crate) mod encrypt; pub mod filter; pub mod framed; +pub mod host_tunnel; pub mod mpsc; pub mod ring; pub(crate) mod secure_datagram; diff --git a/easytier-core/src/wasi/abi.rs b/easytier-core/src/wasi/abi.rs index 1f6a3557..16d3e109 100644 --- a/easytier-core/src/wasi/abi.rs +++ b/easytier-core/src/wasi/abi.rs @@ -34,7 +34,7 @@ pub const CORE_INSTANCE_CONFIG_VERSION: u32 = 14; pub const WEB_CLIENT_CONFIG_VERSION: u32 = 1; /// Version of the public data-plane guest export contract. -pub const DATA_PLANE_ABI_VERSION: u32 = 3; +pub const DATA_PLANE_ABI_VERSION: u32 = 4; /// Version of the protobuf RPC guest export contract. #[cfg(feature = "management-rpc")] @@ -54,6 +54,8 @@ pub const DATA_PLANE_UDP_CAPABILITY: u64 = 1 << 2; pub const DATA_PLANE_DEADLINE_READ: u32 = 1 << 0; /// Update the write deadline in `easytier_data_plane_resource_deadline_set`. pub const DATA_PLANE_DEADLINE_WRITE: u32 = 1 << 1; +/// Version of the host tunnel metadata and export contract. +pub const HOST_TUNNEL_ABI_VERSION: u32 = 1; /// Guest exports a WASI runtime calls to manage a core instance. /// @@ -62,6 +64,9 @@ pub const DATA_PLANE_DEADLINE_WRITE: u32 = 1 << 1; /// all asynchronous guest work through `easytier_instance_drive` and host /// completion notifications. pub const GUEST_EXPORTS: &[&str] = &[ + // WASI command initialization. This only binds the runtime; core lifecycle + // still starts through the instance exports below. + "_start", // Guest-memory buffers. "easytier_buffer_alloc", "easytier_buffer_free", @@ -112,6 +117,13 @@ pub const RPC_GUEST_EXPORTS: &[&str] = &[ "easytier_rpc_operation_free", ]; +/// Guest exports present with the host tunnel feature. +#[cfg(feature = "wasm-host-tunnel")] +pub const HOST_TUNNEL_GUEST_EXPORTS: &[&str] = &[ + "easytier_host_tunnel_abi_version", + "easytier_instance_accept_tunnel", +]; + /// Guest exports present when the core is built with the smoltcp data plane. #[cfg(feature = "proxy-smoltcp-stack")] pub const DATA_PLANE_GUEST_EXPORTS: &[&str] = &[ @@ -124,6 +136,7 @@ pub const DATA_PLANE_GUEST_EXPORTS: &[&str] = &[ "easytier_data_plane_tcp_accept_submit", "easytier_data_plane_tcp_read_submit", "easytier_data_plane_tcp_write_submit", + "easytier_data_plane_tcp_shutdown_write_submit", "easytier_data_plane_udp_bind_submit", "easytier_data_plane_udp_receive_submit", "easytier_data_plane_udp_send_submit", @@ -136,6 +149,7 @@ pub const DATA_PLANE_GUEST_EXPORTS: &[&str] = &[ "easytier_data_plane_tcp_accept_result_take", "easytier_data_plane_tcp_read_result_take", "easytier_data_plane_tcp_write_result_take", + "easytier_data_plane_tcp_shutdown_write_result_take", "easytier_data_plane_udp_bind_result_take", "easytier_data_plane_udp_receive_result_take", "easytier_data_plane_udp_send_result_take", diff --git a/easytier-core/src/wasi/adapter/mod.rs b/easytier-core/src/wasi/adapter/mod.rs index 1fd692c9..7ef44d17 100644 --- a/easytier-core/src/wasi/adapter/mod.rs +++ b/easytier-core/src/wasi/adapter/mod.rs @@ -7,3 +7,5 @@ pub mod event; pub mod management; pub mod packet; pub mod socket; +#[cfg(feature = "wasm-host-tunnel")] +pub mod tunnel; diff --git a/easytier-core/src/wasi/adapter/tunnel.rs b/easytier-core/src/wasi/adapter/tunnel.rs new file mode 100644 index 00000000..35104069 --- /dev/null +++ b/easytier-core/src/wasi/adapter/tunnel.rs @@ -0,0 +1,397 @@ +//! WASI imports for host-owned message tunnels. + +use std::{ + collections::HashMap, + io, + sync::{Arc, Mutex}, + task::Poll, +}; + +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +use std::{fmt, marker::PhantomData}; + +use crate::{ + host::{ + socket::{HostOperationId, HostSocketHandle, HostSocketIo}, + tunnel::HostTunnelIo, + }, + tunnel::Tunnel, + wasi::{ + imports::{ + HOST_PENDING, HOST_TUNNEL_CLOSED, cancel_operation, close, start_tunnel_receive, + start_tunnel_send, take_tunnel_receive, take_tunnel_send, + }, + wire::common::{host_error, status}, + }, +}; + +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +use crate::{ + listener::{ + ExternalListenerFactory, ExternalListenerRequest, queue::HostListenerQueue, + transport::AcceptedTransport, + }, + socket::{SocketListener, tcp::VirtualTcpSocket}, +}; + +#[cfg(feature = "wasm-host-tunnel-outbound")] +use crate::connectivity::manual::ExternalTunnelConnector; +#[cfg(feature = "wasm-host-tunnel-outbound")] +use crate::wasi::imports::{start_tunnel_connect, take_tunnel_connect}; + +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +const MAX_PENDING_HOST_TUNNELS: usize = 256; + +#[derive(Default)] +pub struct WasiHostTunnelIo { + receive_capacities: Mutex>, +} + +impl WasiHostTunnelIo { + fn forget_operation(&self, operation: HostOperationId) { + self.receive_capacities.lock().unwrap().remove(&operation); + } +} + +impl HostSocketIo for WasiHostTunnelIo { + fn cancel_operation(&self, operation: HostOperationId) -> io::Result<()> { + self.forget_operation(operation); + status("cancel_operation", unsafe { cancel_operation(operation.0) }) + } + + fn close(&self, handle: HostSocketHandle) -> io::Result<()> { + status("close", unsafe { close(handle.0) }) + } +} + +impl HostTunnelIo for WasiHostTunnelIo { + fn submit_receive( + &self, + handle: HostSocketHandle, + operation: HostOperationId, + capacity: usize, + ) -> io::Result<()> { + let capacity = u32::try_from(capacity).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + "host tunnel receive buffer is too large", + ) + })?; + status("start_tunnel_receive", unsafe { + start_tunnel_receive(handle.0, operation.0, capacity) + })?; + self.receive_capacities + .lock() + .unwrap() + .insert(operation, capacity as usize); + Ok(()) + } + + fn take_receive(&self, operation: HostOperationId) -> Poll>> { + let mut capacities = self.receive_capacities.lock().unwrap(); + let Some(&capacity) = capacities.get(&operation) else { + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::NotFound, + "WASI host tunnel receive capacity is missing", + ))); + }; + let result = unsafe { take_tunnel_receive(operation.0, 0, 0) }; + match result { + HOST_PENDING => Poll::Pending, + HOST_TUNNEL_CLOSED => { + capacities.remove(&operation); + Poll::Ready(Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "host tunnel closed", + ))) + } + length if length >= 0 => { + let length = length as usize; + if length > capacity { + capacities.remove(&operation); + let _ = unsafe { cancel_operation(operation.0) }; + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidData, + "host tunnel payload exceeds submitted capacity", + ))); + } + let mut buffer = vec![0; length]; + let copied = unsafe { + take_tunnel_receive( + operation.0, + buffer.as_mut_ptr() as u32, + buffer.len() as u32, + ) + }; + capacities.remove(&operation); + if copied == length as i32 { + Poll::Ready(Ok(buffer)) + } else { + let _ = unsafe { cancel_operation(operation.0) }; + Poll::Ready(Err(host_error("take_tunnel_receive copy", copied))) + } + } + code => { + capacities.remove(&operation); + Poll::Ready(Err(host_error("take_tunnel_receive", code))) + } + } + } + + fn submit_send( + &self, + handle: HostSocketHandle, + operation: HostOperationId, + source: &[u8], + ) -> io::Result<()> { + let length = u32::try_from(source.len()).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + "host tunnel send buffer is too large", + ) + })?; + status("start_tunnel_send", unsafe { + start_tunnel_send(handle.0, operation.0, source.as_ptr() as u32, length) + }) + } + + fn take_send(&self, operation: HostOperationId) -> Poll> { + match unsafe { take_tunnel_send(operation.0) } { + HOST_PENDING => Poll::Pending, + 0 => Poll::Ready(Ok(())), + code => Poll::Ready(Err(host_error("take_tunnel_send", code))), + } + } +} + +#[cfg(feature = "wasm-host-tunnel-outbound")] +pub(crate) struct WasiHostTunnelConnector { + runtime: crate::host::socket::HostSocketRuntime, + io: Arc, + supported_schemes: Arc<[String]>, +} + +#[cfg(feature = "wasm-host-tunnel-outbound")] +impl WasiHostTunnelConnector { + pub(crate) fn new( + runtime: crate::host::socket::HostSocketRuntime, + io: Arc, + supported_schemes: Arc<[String]>, + ) -> Self { + Self { + runtime, + io, + supported_schemes, + } + } +} + +#[cfg(feature = "wasm-host-tunnel-outbound")] +#[async_trait::async_trait] +impl ExternalTunnelConnector for WasiHostTunnelConnector { + fn supports_scheme(&self, scheme: &str) -> bool { + self.supported_schemes + .iter() + .any(|supported| supported == scheme) + } + + async fn connect(&self, url: &url::Url) -> anyhow::Result> { + let encoded = url.as_str().as_bytes(); + let encoded_len = u32::try_from(encoded.len()) + .map_err(|_| anyhow::anyhow!("host tunnel URL exceeds WASI guest memory"))?; + let handle = self + .runtime + .run_operation( + self.io.clone(), + |_, operation| { + status("start_tunnel_connect", unsafe { + start_tunnel_connect(operation.0, encoded.as_ptr() as u32, encoded_len) + }) + }, + |_, operation| match unsafe { take_tunnel_connect(operation.0) } { + value if value == i64::from(HOST_PENDING) => Poll::Pending, + value if value > 0 => Poll::Ready(Ok(HostSocketHandle(value as u64))), + value => Poll::Ready(Err(host_error( + "take_tunnel_connect", + i32::try_from(value).unwrap_or(i32::MIN), + ))), + }, + |io, operation| io.cancel_operation(operation), + ) + .await?; + let local_url = url::Url::parse(&format!("{}://0.0.0.0:0", url.scheme()))?; + Ok(crate::tunnel::host_tunnel::new_host_tunnel( + self.runtime.clone(), + self.io.clone(), + handle, + local_url, + url.clone(), + None, + )) + } +} + +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +type HostTunnelQueue = HostListenerQueue>; + +/// Owns the Host Tunnel I/O Adapter and its listener admission queue. +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +pub(crate) struct WasiHostTunnelIngress { + runtime: crate::host::socket::HostSocketRuntime, + io: Arc, + queue: Arc, + supported_schemes: Arc<[String]>, +} + +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +impl WasiHostTunnelIngress { + pub(crate) fn new( + runtime: crate::host::socket::HostSocketRuntime, + supported_schemes: Arc<[String]>, + ) -> Self { + Self::with_io( + runtime, + Arc::new(WasiHostTunnelIo::default()), + supported_schemes, + ) + } + + pub(crate) fn with_io( + runtime: crate::host::socket::HostSocketRuntime, + io: Arc, + supported_schemes: Arc<[String]>, + ) -> Self { + Self { + runtime, + io, + queue: Arc::new(HostListenerQueue::new(MAX_PENDING_HOST_TUNNELS)), + supported_schemes, + } + } + + pub(crate) fn listener_factory( + &self, + ) -> Arc>> + where + TcpSocket: VirtualTcpSocket, + { + Arc::new(WasiHostTunnelListenerFactory { + queue: self.queue.clone(), + supported_schemes: self.supported_schemes.clone(), + }) + } + + pub(crate) fn accept( + &self, + handle: HostSocketHandle, + metadata: crate::wasi::schema::WasiHostTunnelMetadata, + ) -> anyhow::Result<()> { + let crate::wasi::schema::WasiHostTunnelMetadata { + local_url, + remote_url, + resolved_remote_url, + .. + } = metadata; + self.queue.enqueue_with(|| { + crate::tunnel::host_tunnel::new_host_tunnel( + self.runtime.clone(), + self.io.clone(), + handle, + local_url, + remote_url, + resolved_remote_url, + ) + }) + } +} + +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +struct WasiHostTunnelListenerFactory { + queue: Arc, + supported_schemes: Arc<[String]>, +} + +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +impl ExternalListenerFactory> + for WasiHostTunnelListenerFactory +where + TcpSocket: VirtualTcpSocket, +{ + fn supports_scheme(&self, scheme: &str) -> bool { + self.supported_schemes + .iter() + .any(|supported| supported == scheme) + } + + fn create( + &self, + request: ExternalListenerRequest, + ) -> Box>> { + Box::new(WasiHostTunnelListener { + registered: self.queue.register_listener(), + queue: self.queue.clone(), + local_url: request.url, + tcp_socket: PhantomData, + }) + } +} + +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +struct WasiHostTunnelListener { + registered: bool, + queue: Arc, + local_url: url::Url, + tcp_socket: PhantomData TcpSocket>, +} + +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +impl fmt::Debug for WasiHostTunnelListener { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WasiHostTunnelListener") + .field("local_url", &self.local_url) + .field("registered", &self.registered) + .finish() + } +} + +#[async_trait::async_trait] +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +impl SocketListener for WasiHostTunnelListener +where + TcpSocket: VirtualTcpSocket, +{ + type Accepted = AcceptedTransport; + + async fn listen(&mut self) -> anyhow::Result<()> { + if !self.registered { + anyhow::bail!("Host Tunnel listener queue is closed"); + } + Ok(()) + } + + async fn accept(&mut self) -> anyhow::Result { + let tunnel = self + .queue + .accept() + .await + .ok_or_else(|| anyhow::anyhow!("Host Tunnel listener queue is closed"))?; + Ok(AcceptedTransport::Tunnel { + tunnel, + local_url: self.local_url.clone(), + }) + } + + fn local_url(&self) -> url::Url { + self.local_url.clone() + } +} + +#[cfg(not(feature = "wasm-host-tunnel-outbound"))] +impl Drop for WasiHostTunnelListener { + fn drop(&mut self) { + if self.registered { + self.queue.unregister_listener(); + } + } +} diff --git a/easytier-core/src/wasi/imports.rs b/easytier-core/src/wasi/imports.rs index 11aad863..d19e7275 100644 --- a/easytier-core/src/wasi/imports.rs +++ b/easytier-core/src/wasi/imports.rs @@ -6,6 +6,8 @@ pub(crate) const HOST_PENDING: i32 = -1; pub(crate) const HOST_WOULD_BLOCK: i32 = -5; +#[cfg(feature = "wasm-host-tunnel")] +pub(crate) const HOST_TUNNEL_CLOSED: i32 = -10; #[link(wasm_import_module = "easytier_host")] unsafe extern "C" { @@ -188,6 +190,33 @@ unsafe extern "C" { /// Reports packet-sink write readiness; it never accepts a packet itself. pub(crate) fn take_packet_write_ready(operation: u64) -> i32; + #[cfg(feature = "wasm-host-tunnel")] + /// Starts receiving one complete host tunnel payload. + pub(crate) fn start_tunnel_receive(handle: u64, operation: u64, capacity: u32) -> i32; + + #[cfg(feature = "wasm-host-tunnel")] + /// Probes or copies one payload, or returns the closed sentinel. + /// + /// A null destination with zero capacity returns the payload length without + /// consuming it. A second call copies and consumes the payload. + pub(crate) fn take_tunnel_receive(operation: u64, destination: u32, capacity: u32) -> i32; + + #[cfg(feature = "wasm-host-tunnel")] + /// Starts sending one complete host tunnel payload. + pub(crate) fn start_tunnel_send(handle: u64, operation: u64, source: u32, length: u32) -> i32; + + #[cfg(feature = "wasm-host-tunnel")] + /// Reports completion of one host tunnel send. + pub(crate) fn take_tunnel_send(operation: u64) -> i32; + + #[cfg(feature = "wasm-host-tunnel-outbound")] + /// Starts opening one outbound host tunnel for the requested URL. + pub(crate) fn start_tunnel_connect(operation: u64, url: u32, url_len: u32) -> i32; + + #[cfg(feature = "wasm-host-tunnel-outbound")] + /// Returns the connected tunnel handle, or a negative host status. + pub(crate) fn take_tunnel_connect(operation: u64) -> i64; + /// Cancels a pending or completed-but-unread operation and releases host state. /// /// Cancellation must be idempotent when the operation is already absent. diff --git a/easytier-core/src/wasi/runtime.rs b/easytier-core/src/wasi/runtime.rs index 573fde3b..6adc3cfc 100644 --- a/easytier-core/src/wasi/runtime.rs +++ b/easytier-core/src/wasi/runtime.rs @@ -45,6 +45,11 @@ pub(super) type WasiCore = crate::instance::CoreInstance< pub(super) struct WasiCoreRuntime { socket_runtime: crate::host::socket::HostSocketRuntime, + #[cfg(all( + feature = "wasm-host-tunnel", + not(feature = "wasm-host-tunnel-outbound") + ))] + tunnel_ingress: crate::wasi::adapter::tunnel::WasiHostTunnelIngress, core: std::sync::Arc, } @@ -56,6 +61,27 @@ impl WasiCoreRuntime { pub(super) fn notify_host_completions(&self) { self.socket_runtime.notify_completions(); } + + #[cfg(all( + feature = "wasm-host-tunnel", + not(feature = "wasm-host-tunnel-outbound") + ))] + pub(super) fn accept_tunnel( + &self, + handle: crate::host::socket::HostSocketHandle, + metadata: crate::wasi::schema::WasiHostTunnelMetadata, + ) -> anyhow::Result<()> { + self.tunnel_ingress.accept(handle, metadata) + } + + #[cfg(feature = "wasm-host-tunnel-outbound")] + pub(super) fn accept_tunnel( + &self, + _handle: crate::host::socket::HostSocketHandle, + _metadata: crate::wasi::schema::WasiHostTunnelMetadata, + ) -> anyhow::Result<()> { + anyhow::bail!("Host Tunnel admission is unavailable in outbound-only WASI") + } } pub(super) fn new_wasi_core_runtime( @@ -69,7 +95,6 @@ pub(super) fn new_wasi_core_runtime( use crate::host::{dns::HostDnsResolver, packet::HostPacketSink, socket::HostSocketRuntime}; use crate::{ - connectivity::connector_host::new_connector_host, instance::{CoreHostAdapters, CoreInstance}, wasi::adapter::{ dns::WasiHostDnsIo, environment::WasiHostConnectorEnvironmentIo, @@ -78,12 +103,36 @@ pub(super) fn new_wasi_core_runtime( }, }; + #[cfg(not(feature = "wasm-host-tunnel-outbound"))] + use crate::connectivity::connector_host::new_connector_host; + #[cfg(feature = "wasm-host-tunnel-outbound")] + use crate::connectivity::connector_host::new_connector_host_with_external_tunnel; + let socket_runtime = HostSocketRuntime::new(); + let socket_backend = Arc::new(WasiHostSocketBackend::default()); + let environment_io = Arc::new(WasiHostConnectorEnvironmentIo); + #[cfg(feature = "wasm-host-tunnel")] + let host_tunnel_schemes: Arc<[String]> = Arc::from(["ws".to_owned(), "wss".to_owned()]); + #[cfg(feature = "wasm-host-tunnel-outbound")] + let tunnel_io = Arc::new(crate::wasi::adapter::tunnel::WasiHostTunnelIo::default()); + #[cfg(feature = "wasm-host-tunnel-outbound")] + let host = Arc::new(new_connector_host_with_external_tunnel( + socket_runtime.clone(), + socket_backend, + environment_snapshot, + environment_io, + Arc::new(crate::wasi::adapter::tunnel::WasiHostTunnelConnector::new( + socket_runtime.clone(), + tunnel_io, + host_tunnel_schemes.clone(), + )), + )); + #[cfg(not(feature = "wasm-host-tunnel-outbound"))] let host = Arc::new(new_connector_host( socket_runtime.clone(), - Arc::new(WasiHostSocketBackend::default()), + socket_backend, environment_snapshot, - Arc::new(WasiHostConnectorEnvironmentIo), + environment_io, )); let dns = Arc::new(HostDnsResolver::new( socket_runtime.clone(), @@ -97,10 +146,45 @@ pub(super) fn new_wasi_core_runtime( let mut adapters = CoreHostAdapters::new(host, dns, packet_sink, process_runtime); adapters.instance_runtime = Arc::new(WasiInstanceRuntimeHost); adapters.events = Arc::new(WasiHostEventSink::new(event_sink)); + #[cfg(feature = "wasm-host-tunnel-outbound")] + { + adapters.config.connectivity = crate::instance::CoreConnectivityMode::OutboundOnly; + adapters.config.smoltcp_available = true; + adapters.config.requires_smoltcp = true; + adapters.config.gateway_enabled = false; + adapters.config.proxy_enabled = false; + adapters.config.ignore_unsupported_config = true; + adapters.config.endpoint_protocols = host_tunnel_schemes.to_vec(); + } + #[cfg(feature = "wasm-host-tunnel")] + #[cfg(not(feature = "wasm-host-tunnel-outbound"))] + let tunnel_ingress = crate::wasi::adapter::tunnel::WasiHostTunnelIngress::new( + socket_runtime.clone(), + host_tunnel_schemes, + ); + #[cfg(feature = "wasm-host-tunnel")] + #[cfg(not(feature = "wasm-host-tunnel-outbound"))] + { + adapters.config.connectivity = crate::instance::CoreConnectivityMode::InboundOnly; + adapters.external_listener_factory = Some(tunnel_ingress.listener_factory()); + adapters + .host_listener_registrations + .push(crate::listener::ExternalListenerRequest { + url: "wss://0.0.0.0:443" + .parse() + .expect("Host Tunnel listener URL must be valid"), + socket_context: crate::socket::SocketContext::default(), + }); + } let core = CoreInstance::from_toml(config, adapters)?; Ok(WasiCoreRuntime { socket_runtime, + #[cfg(all( + feature = "wasm-host-tunnel", + not(feature = "wasm-host-tunnel-outbound") + ))] + tunnel_ingress, core, }) } @@ -128,6 +212,8 @@ mod abi { use super::{WasiCoreRuntime, new_wasi_core_runtime}; use crate::wasi::schema::WasiCoreInstanceCreateConfig; + #[cfg(feature = "wasm-host-tunnel")] + use crate::wasi::schema::WasiHostTunnelMetadata; #[cfg(feature = "proxy-smoltcp-stack")] mod data_plane; @@ -137,6 +223,8 @@ mod abi { mod web_client; const MAX_CREATE_CONFIG_LEN: usize = 16 * 1024 * 1024; + #[cfg(feature = "wasm-host-tunnel")] + const MAX_HOST_TUNNEL_METADATA_LEN: usize = 16 * 1024; const MAX_GUEST_BUFFER_LEN: usize = MAX_CREATE_CONFIG_LEN; #[cfg(feature = "management-rpc")] const MAX_RPC_MESSAGE_LEN: usize = 16 * 1024 * 1024; @@ -480,6 +568,18 @@ mod abi { } }); } + + #[cfg(feature = "wasm-host-tunnel")] + fn accept_tunnel( + &self, + tunnel_handle: crate::host::socket::HostSocketHandle, + metadata: WasiHostTunnelMetadata, + ) -> anyhow::Result<()> { + if self.core.core().state() != CoreInstanceState::Running { + anyhow::bail!("core instance is not running"); + } + self.core.accept_tunnel(tunnel_handle, metadata) + } } fn decode_create_config(encoded: &[u8]) -> anyhow::Result { @@ -583,6 +683,10 @@ mod abi { with_abi_state(|state| state.read_buffer(pointer, length)) } + #[unsafe(no_mangle)] + /// WASI command entrypoint used only to initialize the host runtime. + pub extern "C" fn _start() {} + #[unsafe(no_mangle)] /// Allocates a guest-owned ABI buffer and returns its linear-memory offset. /// @@ -795,6 +899,63 @@ mod abi { }) } + #[cfg(feature = "wasm-host-tunnel")] + #[unsafe(no_mangle)] + /// Returns the host tunnel ABI version implemented by this guest. + pub extern "C" fn easytier_host_tunnel_abi_version() -> u32 { + crate::wasi::abi::HOST_TUNNEL_ABI_VERSION + } + + #[cfg(feature = "wasm-host-tunnel")] + #[unsafe(no_mangle)] + /// Transfers one host-owned transport into server tunnel admission. + /// + /// `metadata` is a versioned JSON document. A zero return transfers + /// ownership of `tunnel_handle` to the guest; on failure the host keeps + /// ownership and must close it. Admission is scheduled and completed by + /// later drive calls so the export never waits for the peer handshake. + pub extern "C" fn easytier_instance_accept_tunnel( + handle: u64, + tunnel_handle: u64, + metadata_pointer: u32, + metadata_length: u32, + ) -> i32 { + if tunnel_handle == 0 { + set_instance_error(handle, "host tunnel handle must be non-zero"); + return INVALID_INPUT; + } + let encoded = match read_guest_buffer( + metadata_pointer, + metadata_length, + MAX_HOST_TUNNEL_METADATA_LEN, + ) { + Ok(encoded) => encoded, + Err(error) => { + set_instance_error(handle, error); + return INVALID_INPUT; + } + }; + let metadata: WasiHostTunnelMetadata = match serde_json::from_slice(&encoded) { + Ok(metadata) => metadata, + Err(error) => { + set_instance_error(handle, error); + return INVALID_INPUT; + } + }; + if let Err(error) = metadata.validate() { + set_instance_error(handle, error); + return INVALID_INPUT; + } + + with_instance(handle, |instance| { + instance.accept_tunnel( + crate::host::socket::HostSocketHandle(tunnel_handle), + metadata, + )?; + Ok(0) + }) + } + #[unsafe(no_mangle)] /// Destroys an instance and releases its lifecycle, timer, and runtime state. pub extern "C" fn easytier_instance_drop(handle: u64) -> i32 { diff --git a/easytier-core/src/wasi/runtime/abi/data_plane.rs b/easytier-core/src/wasi/runtime/abi/data_plane.rs index a4617977..90e1318e 100644 --- a/easytier-core/src/wasi/runtime/abi/data_plane.rs +++ b/easytier-core/src/wasi/runtime/abi/data_plane.rs @@ -233,7 +233,7 @@ fn require_ipv4(address: SocketAddr) -> Result { address.is_ipv4().then_some(address).ok_or_else(|| { error( DataPlaneErrorKind::AddressFamilyUnsupported, - "data-plane ABI v2 supports IPv4 only", + "data-plane ABI supports IPv4 only", ) }) } @@ -355,6 +355,24 @@ pub extern "C" fn easytier_data_plane_tcp_write_submit( }) } +#[unsafe(no_mangle)] +pub extern "C" fn easytier_data_plane_tcp_shutdown_write_submit( + handle: u64, + stream: u64, + output_operation: u32, +) -> i32 { + let stream = match resource_id(stream) { + Ok(stream) => stream, + Err(error) => { + set_instance_error(handle, error.message()); + return error_status(error.kind()); + } + }; + submit_operation(handle, output_operation, |instance| { + instance.submit_data_plane(|session| session.submit_tcp_shutdown_write(stream)) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn easytier_data_plane_udp_bind_submit( handle: u64, @@ -660,6 +678,26 @@ pub extern "C" fn easytier_data_plane_tcp_write_result_take(handle: u64, operati }) } +#[unsafe(no_mangle)] +pub extern "C" fn easytier_data_plane_tcp_shutdown_write_result_take( + handle: u64, + operation: u64, +) -> i32 { + data_plane_call(handle, |instance| { + let session = instance.data_plane_session(); + take_result( + &session, + operation_id(operation)?, + DataPlaneOperationKind::TcpShutdownWrite, + |result| match result { + DataPlaneOperationResult::TcpWriteShutdown => Ok(()), + _ => Err(invalid_input("TCP write shutdown result variant mismatch")), + }, + )?; + Ok(0) + }) +} + #[unsafe(no_mangle)] pub extern "C" fn easytier_data_plane_udp_bind_result_take( handle: u64, diff --git a/easytier-core/src/wasi/runtime_driver.rs b/easytier-core/src/wasi/runtime_driver.rs index feb9b22f..38239e80 100644 --- a/easytier-core/src/wasi/runtime_driver.rs +++ b/easytier-core/src/wasi/runtime_driver.rs @@ -12,6 +12,10 @@ use std::{ use tokio::runtime::Runtime; +// A zero-duration timer can win before the executor's park hook observes +// quiescence, especially when JSPI resumes the guest on a slower embedder. +const DRIVE_BUDGET: Duration = Duration::from_millis(10); + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum RuntimeDriveOutcome { Quiescent, @@ -52,7 +56,7 @@ impl RuntimeDriver { let _active = RuntimeDriverGuard::activate(self.state.as_ref()); runtime.block_on(async { - let budget = tokio::time::sleep(Duration::ZERO); + let budget = tokio::time::sleep(DRIVE_BUDGET); tokio::pin!(budget); poll_fn(|context| { if self.state.poll_quiescent(context.waker()) { diff --git a/easytier-core/src/wasi/schema.rs b/easytier-core/src/wasi/schema.rs index 40a7153b..090dec85 100644 --- a/easytier-core/src/wasi/schema.rs +++ b/easytier-core/src/wasi/schema.rs @@ -60,3 +60,40 @@ impl WasiWebClientCreateConfig { Ok(()) } } + +#[cfg(feature = "wasm-host-tunnel")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct WasiHostTunnelMetadata { + pub version: u32, + pub local_url: url::Url, + pub remote_url: url::Url, + #[serde(default)] + pub resolved_remote_url: Option, +} + +#[cfg(feature = "wasm-host-tunnel")] +impl WasiHostTunnelMetadata { + pub(crate) fn validate(&self) -> anyhow::Result<()> { + if self.version != crate::wasi::abi::HOST_TUNNEL_ABI_VERSION { + anyhow::bail!("unsupported host tunnel ABI version: {}", self.version); + } + Ok(()) + } +} + +#[cfg(all(test, feature = "wasm-host-tunnel"))] +mod tests { + use super::*; + + #[test] + fn host_tunnel_metadata_accepts_transport_neutral_urls() { + let metadata = WasiHostTunnelMetadata { + version: crate::wasi::abi::HOST_TUNNEL_ABI_VERSION, + local_url: "test-tunnel://listener.example/".parse().unwrap(), + remote_url: "test-tunnel://peer.example/".parse().unwrap(), + resolved_remote_url: None, + }; + + metadata.validate().unwrap(); + } +} diff --git a/easytier-core/src/wasi/wire/data_plane.rs b/easytier-core/src/wasi/wire/data_plane.rs index fa62c8c1..68179385 100644 --- a/easytier-core/src/wasi/wire/data_plane.rs +++ b/easytier-core/src/wasi/wire/data_plane.rs @@ -22,7 +22,7 @@ pub(crate) fn decode_ipv4_socket_address(wire: &[u8]) -> io::Result if !address.is_ipv4() { return Err(io::Error::new( io::ErrorKind::Unsupported, - "data-plane ABI v2 supports IPv4 only", + "data-plane ABI supports IPv4 only", )); } Ok(address) diff --git a/easytier-go/KNOWN_LIMITATIONS.md b/easytier-go/KNOWN_LIMITATIONS.md new file mode 100644 index 00000000..b4f89b24 --- /dev/null +++ b/easytier-go/KNOWN_LIMITATIONS.md @@ -0,0 +1,27 @@ +# Known limitations + +These edge cases are intentionally kept separate from feature changes so they +can be addressed without expanding unrelated patches. + +## Port-forward bind conflicts + +`InstanceConfigBuilder.Build` rejects exact duplicate port-forward rules, but +two rules with the same protocol and bind address and different destinations +are rejected later by `Instance.Start` when Core binds the second socket. +Callers should keep each `(protocol, bind address)` pair unique. + +## Maximum UDP datagram reassembly + +Core buffers can carry the maximum IPv4 UDP payload of 65,507 bytes. At the +current data-plane MTU this requires about 52 IPv4 fragments, while smoltcp can +track 16 disjoint reassembly segments. Extremely out-of-order delivery that +creates more than 16 holes can therefore drop an otherwise valid maximum-size +datagram. Ordered delivery is covered by the current tests. + +## First inbound `ListenPacket` flow + +An `Instance.ListenPacket` socket currently installs an exact UDP data-plane +flow after sending to a peer. A newly bound socket may not receive the first +datagram from a previously unseen peer until a reciprocal flow has been +established. This does not affect the TUN packet plane or Core port-forward +destinations reached through it. diff --git a/easytier-go/LICENSE b/easytier-go/LICENSE new file mode 100644 index 00000000..0a041280 --- /dev/null +++ b/easytier-go/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/easytier-go/PERFORMANCE.md b/easytier-go/PERFORMANCE.md new file mode 100644 index 00000000..1f3f21fa --- /dev/null +++ b/easytier-go/PERFORMANCE.md @@ -0,0 +1,378 @@ +# Performance log + +This file records reproducible `easytier-go` data-plane optimizations. +Each round must describe the change, the benchmark conditions, the before and +after result, and any profile evidence used to choose the next change. + +## Benchmark setup + +- Host CPU: Intel Core i7-14700KF. +- Go host CPUs: P-core logical CPUs `8,10,12,14`. +- Native peer and iperf CPUs: P-core logical CPUs `0,2,4,6`. +- Underlay: UDP over the local Docker bridge. +- Native peer: `10.15.15.1`, listening on underlay UDP port `12012`. +- Go host: `10.15.15.2`, forwarding local port `5202` through + `Instance.Dial` to `10.15.15.1:5201`. +- Go forwarder: `go run ./cmd/dial-forward-bench`; it intentionally keeps + forwarding outside the Core so measurements include the public data-plane + ABI. +- MTU: 1380; encryption enabled with the default algorithm. +- TCP tests use one stream unless noted otherwise. +- UDP tests use 1200-byte datagrams. +- Reported results exclude the first two seconds of each iperf run. + +The native comparison uses EasyTier's built-in port forward on the same +machine, CPU groups, peer, network, and iperf server. It isolates the overhead +of the Go/WASM data-plane interface from EasyTier's virtual TCP/UDP stack and +transport. + +## Round 0: Dial baseline + +Go host commit: `a73f2bac` + +EasyTier commit: `30764897` + +### Throughput + +| Path | Direction | Offered load | Result | +| --- | --- | ---: | ---: | +| Go `DialTCP` | Go to native | unlimited | 14.0 Mbit/s | +| Go `DialTCP` | native to Go | unlimited | 1.56 Gbit/s | +| Go `DialTCP`, 4 streams | Go to native | unlimited | 56.6 Mbit/s | +| Native TCP port forward | forward | unlimited | 1.35 Gbit/s | +| Native TCP port forward | reverse | unlimited | 3.65 Gbit/s | +| Go `DialUDP` | Go to native | 10 Mbit/s | 10.0 Mbit/s, 0% loss | +| Go `DialUDP` | Go to native | 100 Mbit/s | tail stalled after 4 seconds | +| Go `DialUDP` | native to Go | 100 Mbit/s | 61.6 Mbit/s, 38% loss | +| Go `DialUDP` | native to Go | 1 Gbit/s | 86.5 Mbit/s, 91% loss | +| Native UDP port forward | forward | 1 Gbit/s | 999 Mbit/s | + +At a 100 Mbit/s forward UDP load, the receiver averaged 40.9 Mbit/s over the +full test because traffic stopped after four measured seconds. This is not a +stable throughput result. + +### TCP forward profile + +`perf stat` attached to the Go host during a 10-second single-stream test: + +| Counter | Value | +| --- | ---: | +| elapsed time | 12.014 s | +| task clock | 1.186 s | +| context switches | 89,072 | +| CPU migrations | 12,015 | +| core cycles | 5.98 billion | +| core instructions | 4.09 billion | +| core cache misses | 23.0 million | + +The Go process consumed only 0.099 CPU equivalents while forwarding at +14 Mbit/s. Four TCP streams scaled almost exactly linearly, showing a +per-stream serialization limit rather than CPU or transport saturation. + +The Go `streamConn.Write` waits for every guest TCP-write operation. The guest +operation currently calls `AsyncWriteExt::write`, which may complete after a +partial write. Each partial result therefore incurs another complete +submit/drive/complete/take host-to-WASM cycle. The native port-forward result +shows that EasyTier's underlying data plane is not the limiting component. + +## Round 1: finish each guest TCP write + +Go host commit: `a73f2bac` + +EasyTier commit: `73612470` + +The guest TCP operation now uses `write_all` semantics instead of completing +after the first partial virtual-socket write. + +| Path | Before | After | Change | +| --- | ---: | ---: | ---: | +| TCP forward | 14.0 Mbit/s | 15.9 Mbit/s | +13.6% | + +This removed redundant operation completion cycles, but CPU utilization +remained low. The result showed that partial completion was real overhead but +was not the main serialization point. + +## Round 2: repoll immediately when smoltcp has ready egress + +EasyTier commit: `824e01a3` + +The smoltcp reactor reported a zero poll delay when more egress was ready, but +the portable runtime crossed the host timer boundary before polling again. +The reactor now polls again in the same drive turn for a zero delay. + +| Path | Before | After | Change | +| --- | ---: | ---: | ---: | +| TCP forward | 15.9 Mbit/s | 1.17 Gbit/s | 73.6x | +| Median packet gap | 891 us | 8.41 us | -99.1% | + +The packet-gap distribution was captured in +`/tmp/easytier-dial-forward-immediate-repoll.pcap`. Extending every WASI drive +turn by a fixed 50 us did not improve throughput and was later reverted; the +zero-delay signal was the correct condition for immediate work. + +## Round 3: preserve pending UDP operations + +Go host commit: `cf53f92` + +The example refreshed `SetDeadline` before every datagram. Each refresh +cancelled the currently pending read operation, causing forwarding to stall +under sustained load. Deadline refreshes are now limited to half of the idle +timeout, retaining idle-session cleanup without repeatedly cancelling active +operations. + +| UDP forward offered load | Before | After | +| --- | ---: | ---: | +| 300 Mbit/s | 45.3 Mbit/s average, tail stalled | 298 Mbit/s, 0.8% loss | + +Logging added while diagnosing this behavior showed successful writes until +the receive operation was cancelled; the stall was not a transport write +failure. + +## Round 4: reduce per-datagram host/guest overhead + +The following measurements use a 1 Gbit/s offered UDP load. Each row is +measured against the immediately preceding retained state. + +| Change | Commit | Before | After | Change | +| --- | --- | ---: | ---: | ---: | +| Reuse operation deadline contexts | `3d7b622` | 525 Mbit/s | 558 Mbit/s | +6.3% | +| Reuse separate UDP address and payload inputs | `f9092c9` | 558 Mbit/s | 587 Mbit/s | +5.2% | +| Reuse data-plane output storage | `f469a34` | 587 Mbit/s | 652 Mbit/s | +11.1% | +| Restore a zero-duration WASI drive budget | `a5ea9b6` | 652 Mbit/s | 672 Mbit/s | +3.1% | +| Cache hot guest function handles | `47f0809` | 672 Mbit/s | 677 Mbit/s | +0.7% | +| Reuse the instance deadline timer | `657022b` | 677 Mbit/s | 677 Mbit/s | neutral | +| Coalesce immediately completed outcomes | `9d1ec76` | 677 Mbit/s | 688 Mbit/s | +1.6% | + +The reusable input experiment initially used an interior pointer into one +larger guest allocation. The ABI only accepts allocation base pointers, so +the final implementation keeps the remote address and payload in distinct +reusable allocations. + +Restoring the zero-duration drive budget also raised TCP forward throughput +from 1.17 to approximately 1.30 Gbit/s. Reusing the deadline timer was kept +despite neutral throughput because it removes a timer allocation from every +operation without adding synchronization. + +The final rebuilt artifact and host measured 693 Mbit/s UDP forward, within +the variation of the 688 Mbit/s retained baseline. + +### Profile evidence + +The final-stage profile is stored at +`/tmp/easytier-dial-udp-zero-budget.perf.data`. Approximately 49% of samples +were in the WASM JIT and 31% in the Go executable, with the remainder +primarily in the kernel. Before function caching, `NextDeadline` accounted +for 2.25% and Go map hashing for 1.48%. After removing these lookup costs, no +remaining individual host-side helper accounted for enough time to explain +the gap to TCP; guest drive execution and one-operation-per-datagram boundary +crossings dominate. + +## Round 5: enlarge data-plane UDP receive queues + +EasyTier commit: `1df439d` + +The data-plane TCP buffers were already 128 KiB, while UDP inherited the +smoltcp default receive queue of about 8 KiB, enough for only a few 1200-byte +datagrams. The data-plane-only UDP receive buffer is now 128 KiB with 128 +packet metadata entries. + +| UDP reverse offered load | Before | After | +| --- | ---: | ---: | +| 100 Mbit/s | 61.6 Mbit/s, 38% loss | 100 Mbit/s, 0% loss | +| 300 Mbit/s | not stable | 300 Mbit/s, 0.15% loss | +| 500 Mbit/s | not stable | 495 Mbit/s, 0.95% loss | +| 800 Mbit/s | 558 Mbit/s saturated | about 570 Mbit/s saturated | + +Removing the redundant receive-result-size ABI query in `104223c` raised the +800 Mbit/s saturated result from 558 to 563 Mbit/s. Subsequent retained +changes brought it to about 570 Mbit/s. + +## Rejected experiments + +Rejected changes were reverted and are not part of the final data path. + +| Experiment | Before | After | Reason rejected | +| --- | ---: | ---: | --- | +| Fixed 50 us WASI drive budget | 1.17 Gbit/s TCP | no gain | Added unconditional work instead of following readiness | +| Pool queued UDP payloads | 652 Mbit/s | 651 Mbit/s | `sync.Pool` overhead offset allocation savings | +| Lock guest driver to one OS thread | 652 Mbit/s | 436 Mbit/s | Prevented the Go scheduler from placing other work effectively | +| Return tickets before the first drive | 677 Mbit/s | 564 Mbit/s | Added an extra wakeup to the normal completion path | +| Pool one-shot response channels | 688 Mbit/s | 654 Mbit/s | Pool bookkeeping cost more than direct allocation | + +Pinning the complete process to one P-core produced 680 Mbit/s on the earlier +652 Mbit/s build, showing that CPU migration and core selection affect the +result. Thread pinning inside the library was nevertheless rejected because +it regressed throughput and imposed scheduling policy on applications. + +## Round 6: preserve TCP write progress across cancellation + +EasyTier commits: `6b55489`, `4d270f3` + +`write_all` kept the TCP fast path inside the guest, but it could lose the +already-written prefix when a deadline update cancelled the future. The host +would then replay the complete chunk. The replacement still completes writes +inside the guest, but uses cancellation-safe underlying writes and reports a +completed prefix before observing cancellation or deadline expiry. + +WebClient retry, feature timeout, heartbeat, and UDP port-mapping renewal +timers now use the tracked portable time abstraction. This lets an externally +driven host advertise and advance their deadlines without adding a periodic +timer-driving fallback. + +| CPU | Direction | Before | After | Change | +| --- | --- | ---: | ---: | ---: | +| i7-14700KF | TCP forward | 1.30 Gbit/s | 1.31 Gbit/s | within variation | +| i7-14700KF | TCP reverse | 1.60 Gbit/s | 1.61 Gbit/s | within variation | +| Intel N100 | TCP forward | 442 Mbit/s | 436 Mbit/s | -1.4% | +| Intel N100 | TCP reverse | 524 Mbit/s | 517 Mbit/s | -1.3% | + +The N100 results varied by more than these deltas between one-second +intervals, so the change has no measurable systematic throughput cost. + +## Round 7: store deadlines on guest resources + +EasyTier commits: `05ba813`, `66252ed` + +Go host commits: `200f32c`, `59d6e5a` + +The ABI previously passed a remaining timeout with every TCP and UDP data +operation. Once a Go caller configured a connection deadline, the WASI guest +therefore registered a new timer for every read and write. Deadline changes +also required the Go host to cancel and resubmit an active operation. + +Data-plane ABI v3 stores independent read and write deadlines on each TCP or +UDP resource. Hosts update them through one resource setter, while data +operations reuse the active expiration signal without creating timers. +Connect, bind, and accept retain their per-operation timeouts. + +The dedicated A/B benchmark compared the pre-change `fd1be5c` Go host with +the updated host. The helper connected directly to the native peer through +`udp://172.17.0.3:12012`, set a one-hour deadline once when requested, and +excluded the first two seconds. It used the CPU groups described above. + +| TCP forward | Before | After | Change | +| --- | ---: | ---: | ---: | +| No deadline | 1.30 Gbit/s | 1.30 Gbit/s | neutral | +| One-hour deadline | 1.29 Gbit/s | 1.32 Gbit/s | within variation | + +An earlier run reported approximately 750 Mbit/s, but its helper used a +loopback peer URI across different network namespaces while the machine was +also under compiler load. Those TCP and UDP figures were discarded because +the traffic path was not controlled. The direct A/B result shows that +resource deadlines retain the established TCP throughput and remove the +per-operation timer construction without a measurable deadline penalty. + +## Round 8: batch host TCP underlay writes + +EasyTier commit: `cf26d50` + +Go host commits: `8d010e0`, `4197f0d` + +The TUN example connected directly to the long-running native peer through +`tcp://172.17.0.2:11010`. The overlay addresses were `10.12.12.2` and +`10.12.12.1`. Tests used one iperf TCP stream for 15 seconds and excluded +the first two seconds. The application and native peer remained unpinned to +match the reported command; the iperf client used CPUs `0,2,4,6`. + +The TCP host imports still used wazero's reflection-based `WithFunc` path. +Changing the four stream read and write imports to typed module functions +removed reflection and signature decoding from every host operation. + +EasyTier's `FramedWriter` already queues up to 64 frames and exposes them as +vectored slices. `HostTcpStream` did not advertise vectored-write support, so +`poll_write_buf` submitted only the first roughly 1.4 KB frame. Each VPN +packet therefore required a separate asynchronous host operation and guest +wakeup. `HostTcpStream` now combines the queued slices and submits the batch +through the existing ordered write operation. The ABI and concurrency model +are unchanged. + +| Underlay and implementation | Forward throughput | Go host CPU | +| --- | ---: | ---: | +| TCP, baseline | 302-305 Mbit/s | about 1.44 cores | +| TCP, typed host imports | 394 Mbit/s | about 1.58 cores | +| TCP, typed imports and vectored writes | 1.88-1.93 Gbit/s | about 2.12 cores | +| UDP reference | 914-924 Mbit/s | about 2.01 cores | + +With probes attached, baseline TCP issued about 25,000 host writes and +36,000 guest drive calls per second while forwarding at 234 Mbit/s. The +vectored build issued about 6,800 host writes and 14,000 drive calls per +second while forwarding at 1.85 Gbit/s. Average payload per host write grew +from approximately 1.2 KB to 34 KB. + +TCP reverse throughput reached 969 Mbit/s, compared with 1.05 Gbit/s over +the UDP underlay in the same unpinned setup. The remaining reverse difference +was 7.7%, rather than the original threefold forward gap. + +## Round 9: batch host TCP underlay reads + +EasyTier commit: `398b699` + +The forward write batching left the opposite direction asymmetric. +`FramedReader` normally supplied only 2-4 KiB of spare capacity, so each host +TCP read transferred at most a few frames before completing the Go operation, +copying into guest memory, and waking the guest executor. + +`HostTcpStream` now requests a bounded 64 KiB read even when its caller +provides a smaller buffer. It returns the requested prefix immediately and +retains the remainder in its existing read buffer. The stream still permits +only one pending host read, so the ABI and concurrency model are unchanged +and read-ahead remains bounded per active TCP stream. + +The direct A/B used the Round 8 topology and CPU placement. System variation +put the old artifact at 850 Mbit/s reverse and 1.79 Gbit/s forward during this +comparison. + +| Artifact and direction | Throughput | Go host CPU | +| --- | ---: | ---: | +| Old artifact, reverse | 850 Mbit/s | not sampled | +| 64 KiB read-ahead, reverse, first run | 1.80 Gbit/s | about 2 cores | +| 64 KiB read-ahead, reverse, repeat | 1.84 Gbit/s | about 2 cores | +| 64 KiB read-ahead, reverse, 60 seconds | 1.90 Gbit/s | not sampled | +| Old artifact, forward | 1.79 Gbit/s | not sampled | +| 64 KiB read-ahead, forward | 1.75-1.76 Gbit/s | not sampled | + +The repeat reverse run improved by 116%, and the 60-second run showed no +tail stall. Forward changed by about 2%, within the observed run-to-run +variation. Reverse and forward are therefore symmetric under the same test +conditions without enlarging the public ABI or adding concurrent reads. + +## Intel N100 comparison + +The N100 has four physical E-cores with no SMT. The Go host was pinned to +CPUs `0-2`, while iperf used CPU `3`. The native EasyTier peer and iperf +server remained on the i7-14700KF host. + +Raw TCP between the machines reached 2.30 Gbit/s forward and 2.35 Gbit/s +reverse, excluding the network as the limiting component. + +| Path | Direction/load | Result | +| --- | --- | ---: | +| Go `DialTCP` | Go to native | 436 Mbit/s | +| Go `DialTCP` | native to Go | 517 Mbit/s | +| Go `DialUDP` | forward, 1 Gbit/s offered | 134 Mbit/s, saturated | +| Go `DialUDP` | reverse, 300 Mbit/s offered | 120 Mbit/s, saturated | +| Go `DialUDP` | forward, 100 Mbit/s offered | 99.3 Mbit/s, 0.79% loss | +| Go `DialUDP` | reverse, 100 Mbit/s offered | 99.7 Mbit/s, 0.33% loss | + +At saturation the Go host used approximately 1.24-1.41 CPU equivalents. +UDP loses proportionally more performance than TCP on the weaker cores, +consistent with the one-operation-per-datagram ABI and scheduling cost. + +## Final result + +Final Go host commit under test: `47949b5` + +Embedded EasyTier commit: `4d270f3` + +| Path | Direction | Offered load | Final result | +| --- | --- | ---: | ---: | +| Go `DialTCP` | Go to native | unlimited | 1.31 Gbit/s | +| Go `DialTCP` | native to Go | unlimited | 1.61 Gbit/s | +| Go `DialUDP` | Go to native | 1 Gbit/s | 684 Mbit/s | +| Go `DialUDP` | native to Go | 500 Mbit/s | 494 Mbit/s, 1.2% loss | +| Go `DialUDP` | native to Go | 800 Mbit/s | 578 Mbit/s, 28% loss | + +Final TCP and UDP runs lasted 15 seconds after a three-second warm-up. The +UDP API remains one operation per datagram. A batch API was deliberately not +introduced because it would add a new public operation model and buffering +policy for a path whose current performance is acceptable. diff --git a/easytier-go/README.md b/easytier-go/README.md new file mode 100644 index 00000000..8ff63f26 --- /dev/null +++ b/easytier-go/README.md @@ -0,0 +1,352 @@ +# EasyTier Go + +`easytier-go` runs the `wasm32-wasip1` build of `easytier-core` in a +pure-Go process through wazero. EasyTier remains the source and producer of the +embedded WASM; this repository adapts Go host capabilities to the ABI exported +and imported by that artifact. + +```go +import ( + "net/netip" + + corehost "github.com/EasyTier/EasyTier/easytier-go" +) +``` + +## Public API + +The public package owns wazero, standard WASI, the EasyTier host ABI, guest +driving, completion notification, and resource shutdown. Applications create a +host, build a typed instance configuration, and then use standard Go network +interfaces: + +```go +host, err := corehost.New(ctx, corehost.Options{}) +if err != nil { + return err +} +defer host.Close(ctx) + +config, err := corehost.NewInstanceConfigBuilder("office"). + NetworkSecret("secret"). + IPv4(netip.MustParsePrefix("10.144.0.10/24")). + AddPeers("tcp://198.51.100.10:11010"). + Build() +if err != nil { + return err +} + +instance, err := host.CreateInstance(ctx, config) +if err != nil { + return err +} +defer instance.Close(ctx) + +if err := instance.Start(ctx); err != nil { + return err +} +if err := instance.SendPacket(ctx, packet); err != nil { + return err +} +received, err := instance.ReceivePacket(ctx) + +listener, err := instance.Listen("tcp4", ":8080") +connection, err := instance.Dial(ctx, "tcp4", "10.144.0.2:8080") +packets, err := instance.ListenPacket("udp4", ":5353") +``` + +### Web Client management + +A host can also connect to an EasyTier Web configuration server. The embedded +Rust WebClient retains the config-server protocol, heartbeat, reconnect, and +secure-tunnel behavior; Go owns the resulting process-level instances: + +```go +webClient, err := host.ConnectWebClient(ctx, corehost.WebClientOptions{ + Endpoint: "udp://config.example.com:22020/team-token", + MachineID: "11111111-2222-4333-8444-555555555555", + Hostname: "edge-gateway", + SecureMode: true, +}) +if err != nil { + return err +} +defer webClient.Close(ctx) + +for _, instance := range host.Instances() { + log.Printf("%s: %v", instance.ID(), instance.State()) +} +``` + +`MachineID` must be a stable UUID persisted by the application. `Endpoint` +accepts `tcp://`, `udp://`, or the same shorthand token understood by native +EasyTier. WebSocket transports are not part of this initial host integration. +One WebClient may run per `Host`. + +Web-created instances support the complete `WebClientService` lifecycle and +status surface. Instances created through `Host.CreateInstance` are included +in heartbeats and status listings, but are reported as read-only and cannot be +overwritten, retained away, or deleted by the Web server. `Host.Instances` +returns both ownership classes; Web-created instances use the same `Instance` +data-plane and management APIs as application-created instances. + +`Instance.ListPeer` and `Instance.ListRoute` call the embedded core's existing +instance-scoped management RPCs and return peer and route slices directly. +Their element types reuse the generated EasyTier protobuf models, while the +request and response envelopes stay internal to the host. Callers never +construct wire bytes or a separate RPC client. Cancelling the context frees +the pending guest operation. + +`InstanceConfigBuilder` exposes the instance settings supported by this host: +network identity, hostname, virtual IPv4 address, peers and listeners, IPv4 and +IPv6 STUN servers, Core-owned TCP and UDP port forwards, P2P policy, +hole-punching methods, encryption, and secure mode. Omitted optional settings +retain the embedded core's defaults. Calling `STUNServers()` or +`STUNServersV6()` with no arguments explicitly selects an empty list. + +`AddPortForwards` accepts typed rules containing a `PortForwardTCP` or +`PortForwardUDP` protocol and `netip.AddrPort` bind and destination addresses. +The embedded core owns their listener, overlay-flow, reload, and shutdown +lifecycle. + +Secure mode can generate an X25519 key with `SecureMode()` or use a caller +supplied raw 32-byte private key with `SecureModeWithPrivateKey(key)`. The +public key is always derived by the builder. Secure mode currently requires a +non-empty shared network secret; credential-based networks are a separate +future configuration path. + +`Dial` returns `net.Conn`, `Listen` returns `net.Listener`, and `ListenPacket` +returns `net.PacketConn`. ABI v2 currently supports `tcp`, `tcp4`, `udp`, and +`udp4`; destinations must be IPv4 literals and listeners bind all overlay IPv4 +addresses. These APIs are overlay-only: an absent EasyTier route is returned as +a normal network error and never falls back to the host network. + +See [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md) for current UDP and +port-forward edge cases. + +No public type exposes wazero runtimes, WebAssembly pointers, raw handles, +submit/take operations, or the cooperative `drive` loop. Each host owns one +wazero runtime, guest module, and host completion domain; each instance is +represented by an EasyTier guest handle. Per-instance drivers serialize guest +calls through the host. The engine continues driving EasyTier after `Start` +returns, calls `easytier_instance_notify_completions` before driving a host +completion, and drains bounded data-plane completion batches after each guest +turn. + +The host serializes the typed configuration to TOML internally, wraps it in +EasyTier's version 14 create envelope, and adds the configured environment +snapshot. TOML, schema versions, and JSON envelopes are not +application-facing APIs. + +## Cross-platform TUN example + +The TUN example joins an existing EasyTier network with a fixed virtual IPv4 +address on Linux, macOS, or Windows. It creates and configures the native TUN +interface itself, then forwards raw IPv4 packets through `SendPacket` and +`ReceivePacket`: + +```sh +cd examples/tun +sudo go run . \ + -p tcp://198.51.100.10:11010 \ + --network-name office \ + --network-secret secret \ + --ipv4 10.144.0.10/24 +``` + +Repeat `-p` to configure more peers. The command creates `et-goN` on Linux and +Windows or `utunN` on macOS, assigns the requested address, and sets an MTU of +1380. Run it as root or with `CAP_NET_ADMIN` on Linux, with `sudo` on macOS, or +from an Administrator terminal on Windows. Closing the command removes the TUN +interface. The example does not install a default route or enable GSO. + +On Linux and macOS, send `SIGUSR1` to print the current peer list or `SIGUSR2` +to print the current route list. + +Repeat `-port-forward` to expose local TCP or UDP ports through the embedded +core's port-forward manager: + +```sh +sudo go run . \ + -p tcp://198.51.100.10:11010 \ + --network-name office \ + --network-secret secret \ + --ipv4 10.144.0.10/24 \ + -port-forward tcp://127.0.0.1:5202/10.144.0.20:5201 \ + -port-forward udp://127.0.0.1:5202/10.144.0.20:5201 +``` + +For example, run `iperf3 -c 127.0.0.1 -p 5202` for TCP or add +`-u -b 0 -l 1200` for UDP. iperf3's UDP mode still needs the TCP forward for +its control connection. The TUN example only parses these rules into the +instance configuration; the core owns the host listeners and per-client +overlay flows. + +## Instance.Dial example + +The Dial example is a small overlay client dedicated to the public +`Instance.Dial` API. TCP mode bridges the connected stream to standard input +and output until the remote side closes or the command is interrupted: + +```sh +printf 'GET / HTTP/1.0\r\nHost: 10.144.0.20\r\n\r\n' | + go run ./examples/dial \ + -p tcp://198.51.100.10:11010 \ + --network-name office \ + --network-secret secret \ + --ipv4 10.144.0.10/24 \ + --network tcp4 \ + --address 10.144.0.20:8080 +``` + +With `--network udp4`, standard input is sent as one datagram and one response +datagram is written to standard output. The command does not create a local +listener or implement port-forward management. It waits up to 10 seconds for a +matching overlay or proxy route before dialing; override that limit with +`--connect-timeout`. + +## Web Client example + +The Web Client example registers a Host with an EasyTier Web configuration +server and lets the server create, delete, and inspect its instances: + +```sh +go run ./examples/web-client \ + --web-endpoint tcp://config.example.com:22020/team-token \ + --web-machine-id 11111111-2222-4333-8444-555555555555 \ + --web-hostname edge-gateway \ + --web-secure +``` + +`--web-machine-id` must remain stable across restarts. `--web-hostname` defaults +to the system hostname. This example manages Host instances but does not create +or attach an operating-system TUN interface. + +## Performance compared with native EasyTier + +In this A/B benchmark two nodes on one i7-14700KF host (Linux 6.11) each run +in their own network namespace, joined by a veth pair. Node A always runs a +native `easytier-core` build of EasyTier master (`2.6.4-6a186167`) with +overlay address `10.144.0.1/24`; node B runs either the same native binary or +this Go host (commit `78889d12`, embedded EasyTier `af640d49`) with +`10.144.0.2/24`. A master build is used as the native baseline because the +2.6.4 release predates several native data-plane throughput fixes (EasyTier +#2451, #2452). The underlay tunnel between the nodes is either `tcp://` or +`udp://`. Encryption is enabled and the overlay MTU is 1360 on both ends. +Node A and the iperf3 server are pinned to CPUs `0,2,4,6`, node B to +`8,10,12,14`. Each iperf3 run lasts 15 seconds and excludes the first +3 seconds. Forward means node B sends to node A; reverse uses `iperf3 -R`. +Measured 2026-07-28. + +The forwarding rows deliberately compare native Core port forwarding with the +Go host's benchmark-only `cmd/dial-forward-bench`, which carries traffic +through the public `Instance.Dial` API. + +TCP, one stream: + +| Scenario | Direction | `tcp://` native | `tcp://` Go host | `udp://` native | `udp://` Go host | +| --- | --- | ---: | ---: | ---: | ---: | +| TUN | forward | 6.06 Gbit/s | 2.12 Gbit/s | 3.71 Gbit/s | 1.57 Gbit/s | +| TUN | reverse | 6.03 Gbit/s | 2.57 Gbit/s | 3.69 Gbit/s | 1.48 Gbit/s | +| Native port forward / Go Dial | forward | 1.25 Gbit/s | 1.29 Gbit/s | 1.19 Gbit/s | 1.20 Gbit/s | +| Native port forward / Go Dial | reverse | 6.49 Gbit/s | 1.95 Gbit/s | 4.26 Gbit/s | 1.43 Gbit/s | + +UDP native port forward / Go Dial, 1 Gbit/s offered with 1200-byte datagrams +(received / lost): + +| Direction | `tcp://` native | `tcp://` Go host | `udp://` native | `udp://` Go host | +| --- | ---: | ---: | ---: | ---: | +| forward | 996 Mbit/s / 0.3% | 546 Mbit/s / 45% | 996 Mbit/s / 0.3% | 699 Mbit/s / 30% | +| reverse | 950 Mbit/s / 5.0% | 490 Mbit/s / 51% | 983 Mbit/s / 1.7% | 350 Mbit/s / 65% | + +Reading the numbers: + +- On TUN the Go host reaches roughly 35-45% of native single-stream + throughput. Both sides run the same EasyTier core logic, so the gap is the + WASM/Go data-plane boundary rather than routing or cryptography. +- TCP forwarding is a tie at about 1.2 Gbit/s: both paths are bounded by the + virtual TCP send path inside the shared EasyTier core, not by the host. +- TCP reverse forwarding favors native by about 3x (4.3-6.5 versus + 1.4-2.0 Gbit/s); the Go host benchmark's per-operation receive path is the + limit. +- Native sustains the offered 1 Gbit/s UDP nearly loss-free in both + directions, while the Go host saturates at 350-700 Mbit/s with significant + loss, consistent with the one-operation-per-datagram data-plane ABI + documented in `PERFORMANCE.md`. + +## Platform capabilities + +The default platform implementation uses Go's standard `net` and +`net.Resolver` packages. Applications that need netns, socket marks, device +binding, reuse policy, or custom DNS can inject capabilities through +`platform.Services`: + +```go +host, err := corehost.New(ctx, corehost.Options{ + Platform: platform.Services{ + Sockets: socketFactory, + DNS: dnsResolver, + Environment: connectorEnvironment, + Snapshot: environmentSnapshot, + }, +}) +``` + +`platform.SocketFactory` owns only TCP connect, UDP bind, and TCP listen +creation. Once a standard Go network resource is returned, the host runtime +owns its reads, writes, accepts, cancellation, and close path. EasyTier retains +all routing, peer admission, protocol, retry, and connection policy. + +The implementation is split by responsibility: + +- `platform` defines public capability ports; `platform/netstd` implements + their portable defaults. +- `proto` contains generated Go bindings for the existing EasyTier management + protobuf definitions. +- `internal/reactor` owns typed asynchronous operations, resources, operation + IDs, backpressure, and completion signals without depending on wazero. +- `internal/hostabi` implements the custom `easytier_host` imports, guest + memory copying, wire codecs, and ABI status translation. +- `internal/coreabi` owns guest memory, the big-endian data-plane wire codec, + ABI discovery, and typed `easytier_instance_*`, `easytier_data_plane_*`, and + `easytier_rpc_*` export calls. +- `internal/engine` composes standard WASI, both EasyTier ABI directions, the + single-owner driver, operation cancellation, deadlines, standard Go network + resources, and instance shutdown. +- `internal/artifact` contains only the embedded core and its provenance. + +## Embedded artifact + +The committed WASM lets downstream Go builds and tests run without a Rust +toolchain. Refresh it from a clean EasyTier checkout whenever the guest ABI or +core implementation changes: + +```sh +EASYTIER_SOURCE=/path/to/EasyTier go generate ./... +``` + +Generation runs EasyTier's `script/build-wasi-core.sh`, which builds release +`easytier_core.wasm` with the Go-host features and writes an optimized +`easytier_core_go_host.wasm` with the pinned, SHA-256-verified Binaryen release. +The generator supplies a fixed source path remap and source-date epoch, then +records the EasyTier commit and optimized artifact SHA-256. Tracked EasyTier +changes block generation; unrelated untracked files do not. +`corehost.CoreInfo()` exposes that provenance without exposing the artifact +bytes. + +The same generator rebuilds the Go protobuf bindings from that exact clean +EasyTier commit and records their source commit and schema SHA-256. Host +creation rejects an artifact/binding commit mismatch. Generation requires +`protoc` 35.1 and `protoc-gen-go` 1.36.11 on `PATH`. + +The test-only socket probe is retained from +[EasyTier commit 6a3d15f](https://github.com/EasyTier/EasyTier/tree/6a3d15f8758eed759d55401ff4ed7c47021b0819/tools/wasi-socket-poc/guest); +its full commit and checksum are recorded in +`testdata/wasi_socket_guest.source`. + +Run all reactor, ABI conformance, lifecycle, and two-instance network tests +with: + +```sh +go test -count=1 ./... +``` diff --git a/easytier-go/cmd/dial-forward-bench/main.go b/easytier-go/cmd/dial-forward-bench/main.go new file mode 100644 index 00000000..f973a509 --- /dev/null +++ b/easytier-go/cmd/dial-forward-bench/main.go @@ -0,0 +1,364 @@ +// Command dial-forward-bench measures the public Instance.Dial data path. +// +// User-facing port forwarding belongs to the embedded EasyTier core. This +// command intentionally keeps the forwarding loop in Go so benchmarks include +// the public Go/WASM data-plane boundary. +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "log" + "net" + "net/netip" + "os" + "os/signal" + "strings" + "sync/atomic" + "syscall" + + corehost "github.com/EasyTier/EasyTier/easytier-go" + "github.com/EasyTier/EasyTier/easytier-go/internal/contextutil" +) + +type options struct { + peers peerList + portForwards portForwardList + networkName string + networkSecret string + ipv4 string +} + +func main() { + options, err := parseOptions(os.Args[1:]) + if err == flag.ErrHelp { + return + } + if err != nil { + log.Printf("invalid arguments: %v", err) + os.Exit(2) + } + + ctx, stop := signal.NotifyContext( + context.Background(), + os.Interrupt, + syscall.SIGTERM, + ) + defer stop() + if err := run(ctx, options); err != nil { + log.Printf("EasyTier Dial benchmark failed: %v", err) + os.Exit(1) + } +} + +func run(ctx context.Context, options options) error { + prefix, err := netip.ParsePrefix(options.ipv4) + if err != nil { + return fmt.Errorf("parse instance IPv4 prefix: %w", err) + } + config, err := corehost.NewInstanceConfigBuilder(options.networkName). + NetworkSecret(options.networkSecret). + IPv4(prefix). + AddPeers([]string(options.peers)...). + Build() + if err != nil { + return fmt.Errorf("build EasyTier instance config: %w", err) + } + + host, err := corehost.New(ctx, corehost.Options{}) + if err != nil { + return fmt.Errorf("create EasyTier host: %w", err) + } + defer host.Close(context.Background()) + instance, err := host.CreateInstance(ctx, config) + if err != nil { + return fmt.Errorf("create EasyTier instance: %w", err) + } + defer instance.Close(context.Background()) + if err := instance.Start(ctx); err != nil { + return fmt.Errorf("start EasyTier instance: %w", err) + } + + forwarders, err := startPortForwards( + ctx, + instance.Dial, + options.portForwards, + ) + if err != nil { + return err + } + defer forwarders.Close() + <-ctx.Done() + return nil +} + +func parseOptions(arguments []string) (options, error) { + flags := flag.NewFlagSet("dial-forward-bench", flag.ContinueOnError) + var options options + flags.Var(&options.peers, "p", "EasyTier peer URI; may be repeated") + flags.Var( + &options.portForwards, + "port-forward", + "tcp://bind/overlay-target or udp://bind/overlay-target; may be repeated", + ) + flags.StringVar(&options.networkName, "network-name", "", "EasyTier network name") + flags.StringVar(&options.networkSecret, "network-secret", "", "EasyTier network secret") + flags.StringVar(&options.ipv4, "ipv4", "", "fixed EasyTier IPv4 address and prefix") + if err := flags.Parse(arguments); err != nil { + return options, err + } + if flags.NArg() != 0 { + return options, fmt.Errorf("unexpected argument %q", flags.Arg(0)) + } + if err := options.validate(); err != nil { + return options, err + } + return options, nil +} + +func (options options) validate() error { + if len(options.peers) == 0 { + return fmt.Errorf("at least one -p peer is required") + } + if len(options.portForwards) == 0 { + return fmt.Errorf("at least one --port-forward rule is required") + } + if strings.TrimSpace(options.networkName) == "" { + return fmt.Errorf("--network-name is required") + } + if options.networkSecret == "" { + return fmt.Errorf("--network-secret is required") + } + prefix, err := netip.ParsePrefix(options.ipv4) + if err != nil { + return fmt.Errorf("parse --ipv4: %w", err) + } + if !prefix.Addr().Is4() { + return fmt.Errorf("--ipv4 must be an IPv4 prefix") + } + return nil +} + +type peerList []string + +func (peers *peerList) Set(value string) error { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("peer URI must not be empty") + } + *peers = append(*peers, value) + return nil +} + +func (peers *peerList) String() string { + return strings.Join(*peers, ",") +} + +type portForwardRule struct { + protocol string + bind netip.AddrPort + target netip.AddrPort +} + +func (rule portForwardRule) String() string { + return fmt.Sprintf("%s://%s/%s", rule.protocol, rule.bind, rule.target) +} + +type portForwardList []portForwardRule + +func (rules *portForwardList) Set(value string) error { + rule, err := parsePortForwardRule(value) + if err == nil { + *rules = append(*rules, rule) + } + return err +} + +func (rules *portForwardList) String() string { + values := make([]string, len(*rules)) + for index, rule := range *rules { + values[index] = rule.String() + } + return strings.Join(values, ",") +} + +func parsePortForwardRule(value string) (portForwardRule, error) { + protocol, addresses, ok := strings.Cut(value, "://") + if !ok || (protocol != "tcp" && protocol != "udp") { + return portForwardRule{}, fmt.Errorf( + "port forward %q must use tcp:// or udp://", + value, + ) + } + bindText, targetText, ok := strings.Cut(addresses, "/") + if !ok { + return portForwardRule{}, fmt.Errorf( + "port forward %q must contain bind/overlay-target", + value, + ) + } + bind, err := netip.ParseAddrPort(bindText) + if err != nil { + return portForwardRule{}, fmt.Errorf("parse bind address: %w", err) + } + target, err := netip.ParseAddrPort(targetText) + if err != nil { + return portForwardRule{}, fmt.Errorf("parse target address: %w", err) + } + if !bind.Addr().Is4() || !target.Addr().Is4() { + return portForwardRule{}, fmt.Errorf("port forward requires IPv4 addresses") + } + return portForwardRule{protocol: protocol, bind: bind, target: target}, nil +} + +type overlayDialFunc func(context.Context, string, string) (net.Conn, error) + +type portForwardSet struct { + ctx context.Context + cancel context.CancelFunc + dial overlayDialFunc + addresses []net.Addr + closers []io.Closer +} + +func startPortForwards( + ctx context.Context, + dial overlayDialFunc, + rules []portForwardRule, +) (*portForwardSet, error) { + if dial == nil { + return nil, fmt.Errorf("port forward dial function is nil") + } + forwardContext, cancel := context.WithCancel(ctx) + forwards := &portForwardSet{ + ctx: forwardContext, + cancel: cancel, + dial: dial, + } + for _, rule := range rules { + var err error + switch rule.protocol { + case "tcp": + err = forwards.startTCP(rule) + case "udp": + err = forwards.startUDP(rule) + default: + err = fmt.Errorf("unsupported protocol %q", rule.protocol) + } + if err != nil { + _ = forwards.Close() + return nil, fmt.Errorf("start port forward %s: %w", rule, err) + } + } + return forwards, nil +} + +func (forwards *portForwardSet) Close() error { + forwards.cancel() + var errs []error + for _, closer := range forwards.closers { + errs = append(errs, closer.Close()) + } + return errors.Join(errs...) +} + +func (forwards *portForwardSet) startTCP(rule portForwardRule) error { + listener, err := net.ListenTCP("tcp4", net.TCPAddrFromAddrPort(rule.bind)) + if err != nil { + return err + } + forwards.closers = append(forwards.closers, listener) + forwards.addresses = append(forwards.addresses, listener.Addr()) + log.Printf("forwarding tcp://%s to %s through Instance.Dial", listener.Addr(), rule.target) + go func() { + for { + local, err := listener.Accept() + if err != nil { + return + } + go forwards.forwardTCP(local, rule.target) + } + }() + return nil +} + +func (forwards *portForwardSet) forwardTCP( + local net.Conn, + target netip.AddrPort, +) { + defer local.Close() + overlay, err := forwards.dial( + forwards.ctx, + "tcp4", + target.String(), + ) + if err != nil { + return + } + defer overlay.Close() + stopClose := contextutil.AfterFunc(forwards.ctx, func() { + _ = local.Close() + _ = overlay.Close() + }) + defer stopClose() + + done := make(chan struct{}, 2) + go func() { _, _ = io.Copy(overlay, local); done <- struct{}{} }() + go func() { _, _ = io.Copy(local, overlay); done <- struct{}{} }() + <-done +} + +func (forwards *portForwardSet) startUDP(rule portForwardRule) error { + overlay, err := forwards.dial( + forwards.ctx, + "udp4", + rule.target.String(), + ) + if err != nil { + return err + } + listener, err := net.ListenUDP("udp4", net.UDPAddrFromAddrPort(rule.bind)) + if err != nil { + _ = overlay.Close() + return err + } + forwards.closers = append(forwards.closers, listener, overlay) + forwards.addresses = append(forwards.addresses, listener.LocalAddr()) + log.Printf( + "forwarding udp://%s to %s through Instance.Dial", + listener.LocalAddr(), + rule.target, + ) + + var lastClient atomic.Value + go func() { + packet := make([]byte, 65535) + for { + length, client, err := listener.ReadFromUDPAddrPort(packet) + if err != nil { + return + } + lastClient.Store(client) + if written, err := overlay.Write(packet[:length]); err != nil || + written != length { + return + } + } + }() + go func() { + packet := make([]byte, 65535) + for { + length, err := overlay.Read(packet) + if err != nil { + return + } + client, ok := lastClient.Load().(netip.AddrPort) + if ok { + _, _ = listener.WriteToUDPAddrPort(packet[:length], client) + } + } + }() + return nil +} diff --git a/easytier-go/cmd/dial-forward-bench/main_test.go b/easytier-go/cmd/dial-forward-bench/main_test.go new file mode 100644 index 00000000..c886a7ec --- /dev/null +++ b/easytier-go/cmd/dial-forward-bench/main_test.go @@ -0,0 +1,188 @@ +package main + +import ( + "bytes" + "context" + "io" + "net" + "net/netip" + "strings" + "testing" + "time" +) + +func TestParseOptions(t *testing.T) { + options, err := parseOptions([]string{ + "-p", "tcp://198.51.100.10:11010", + "--network-name", "office", + "--network-secret", "secret", + "--ipv4", "10.144.0.10/24", + "--port-forward", "tcp://127.0.0.1:5202/10.144.0.20:5201", + "--port-forward", "udp://127.0.0.1:5202/10.144.0.20:5201", + }) + if err != nil { + t.Fatalf("parse options: %v", err) + } + if len(options.portForwards) != 2 { + t.Fatalf("port-forward count = %d", len(options.portForwards)) + } +} + +func TestPortForwardList(t *testing.T) { + var rules portForwardList + for _, value := range []string{ + "tcp://127.0.0.1:5202/10.144.0.20:5201", + "udp://0.0.0.0:5203/10.144.0.21:5201", + } { + if err := rules.Set(value); err != nil { + t.Fatalf("set %q: %v", value, err) + } + } + if got := rules.String(); got != + "tcp://127.0.0.1:5202/10.144.0.20:5201,"+ + "udp://0.0.0.0:5203/10.144.0.21:5201" { + t.Fatalf("rules string = %q", got) + } + if err := rules.Set("sctp://127.0.0.1:1/10.0.0.1:2"); err == nil { + t.Fatal("unsupported protocol was accepted") + } +} + +func TestTCPPortForward(t *testing.T) { + echo, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen echo: %v", err) + } + defer echo.Close() + go func() { + connection, acceptErr := echo.Accept() + if acceptErr == nil { + defer connection.Close() + _, _ = io.Copy(connection, connection) + } + }() + + rule, err := parsePortForwardRule( + "tcp://127.0.0.1:0/" + echo.Addr().String(), + ) + if err != nil { + t.Fatalf("parse forward: %v", err) + } + forwards, err := startPortForwards( + context.Background(), + standardDial, + []portForwardRule{rule}, + ) + if err != nil { + t.Fatalf("start forward: %v", err) + } + defer forwards.Close() + + connection, err := net.Dial("tcp4", forwards.addresses[0].String()) + if err != nil { + t.Fatalf("dial forward: %v", err) + } + defer connection.Close() + _ = connection.SetDeadline(time.Now().Add(5 * time.Second)) + sent := bytes.Repeat([]byte("easytier"), 1024) + if _, err := connection.Write(sent); err != nil { + t.Fatalf("write forward: %v", err) + } + received := make([]byte, len(sent)) + if _, err := io.ReadFull(connection, received); err != nil { + t.Fatalf("read forward: %v", err) + } + if !bytes.Equal(received, sent) { + t.Fatal("forwarded TCP data differs") + } +} + +func TestUDPPortForward(t *testing.T) { + echo, err := net.ListenUDP( + "udp4", + net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:0")), + ) + if err != nil { + t.Fatalf("listen echo: %v", err) + } + defer echo.Close() + _ = echo.SetDeadline(time.Now().Add(5 * time.Second)) + firstReceived := make(chan struct{}) + go func() { + packet := make([]byte, 65535) + if _, _, readErr := echo.ReadFromUDPAddrPort(packet); readErr != nil { + close(firstReceived) + return + } + close(firstReceived) + length, client, readErr := echo.ReadFromUDPAddrPort(packet) + if readErr != nil { + return + } + _, _ = echo.WriteToUDPAddrPort(packet[:length], client) + }() + + rule, err := parsePortForwardRule( + "udp://127.0.0.1:0/" + echo.LocalAddr().String(), + ) + if err != nil { + t.Fatalf("parse forward: %v", err) + } + forwards, err := startPortForwards( + context.Background(), + standardDial, + []portForwardRule{rule}, + ) + if err != nil { + t.Fatalf("start forward: %v", err) + } + defer forwards.Close() + + firstClient, err := net.Dial("udp4", forwards.addresses[0].String()) + if err != nil { + t.Fatalf("dial forward: %v", err) + } + defer firstClient.Close() + if _, err := firstClient.Write([]byte("first")); err != nil { + t.Fatalf("write from first client: %v", err) + } + <-firstReceived + + lastClient, err := net.Dial("udp4", forwards.addresses[0].String()) + if err != nil { + t.Fatalf("dial forward from last client: %v", err) + } + defer lastClient.Close() + _ = lastClient.SetDeadline(time.Now().Add(5 * time.Second)) + sent := []byte("last") + if _, err := lastClient.Write(sent); err != nil { + t.Fatalf("write from last client: %v", err) + } + received := make([]byte, len(sent)) + if _, err := io.ReadFull(lastClient, received); err != nil { + t.Fatalf("read forward: %v", err) + } + if !bytes.Equal(received, sent) { + t.Fatal("forwarded UDP data differs") + } +} + +func standardDial( + ctx context.Context, + network string, + address string, +) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, network, address) +} + +func TestParseOptionsRequiresForward(t *testing.T) { + _, err := parseOptions([]string{ + "-p", "tcp://198.51.100.10:11010", + "--network-name", "office", + "--network-secret", "secret", + "--ipv4", "10.144.0.10/24", + }) + if err == nil || !strings.Contains(err.Error(), "--port-forward") { + t.Fatalf("parse error = %v, want missing port-forward", err) + } +} diff --git a/easytier-go/cmd/update-wasm/main.go b/easytier-go/cmd/update-wasm/main.go new file mode 100644 index 00000000..17c00a76 --- /dev/null +++ b/easytier-go/cmd/update-wasm/main.go @@ -0,0 +1,254 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "flag" + "fmt" + "go/format" + "os" + "os/exec" + "path/filepath" + "strings" +) + +const ( + coreArtifact = "wasm32-wasip1/release/easytier_core_go_host.wasm" + coreBuildScript = "script/build-wasi-core.sh" + protoGenerateScript = "script/generate-proto.sh" +) + +var managementProtoFiles = []string{ + "common.proto", + "error.proto", + "acl.proto", + "peer_rpc.proto", + "api_instance.proto", + "api_config.proto", + "api_manage.proto", + "web.proto", +} + +func main() { + var source string + flag.StringVar(&source, "easytier", os.Getenv("EASYTIER_SOURCE"), "path to a clean EasyTier checkout") + flag.Parse() + + if err := update(source); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func update(source string) error { + workingDirectory, err := os.Getwd() + if err != nil { + return fmt.Errorf("resolve generator directory: %w", err) + } + moduleFile, err := commandOutput(workingDirectory, "go", "env", "GOMOD") + if err != nil { + return err + } + if moduleFile == "" || moduleFile == os.DevNull { + return fmt.Errorf("locate Go module from %s", workingDirectory) + } + repositoryRoot := filepath.Dir(moduleFile) + if source == "" { + source = filepath.Join(repositoryRoot, "..") + } + source, err = filepath.Abs(source) + if err != nil { + return fmt.Errorf("resolve EasyTier source: %w", err) + } + if _, err := os.Stat(filepath.Join(source, "Cargo.toml")); err != nil { + return fmt.Errorf("validate EasyTier source %s: %w", source, err) + } + if err := requireCleanCheckout(source); err != nil { + return err + } + commit, err := commandOutput(source, "git", "rev-parse", "HEAD") + if err != nil { + return err + } + sourceDateEpoch, err := commandOutput(source, "git", "show", "-s", "--format=%ct", "HEAD") + if err != nil { + return err + } + + if err := buildCore(source, sourceDateEpoch); err != nil { + return err + } + if err := generateProto(repositoryRoot, source); err != nil { + return err + } + protoDigest, err := protoSchemaDigest(source) + if err != nil { + return err + } + + coreDigest, err := copyArtifact( + coreArtifactPath(source, os.Getenv("CARGO_TARGET_DIR")), + filepath.Join(repositoryRoot, "internal", "artifact", "easytier_core.wasm"), + ) + if err != nil { + return err + } + if err := writeProvenance(repositoryRoot, commit, coreDigest); err != nil { + return err + } + if err := writeProtoProvenance(repositoryRoot, commit, protoDigest); err != nil { + return err + } + fmt.Printf("embedded EasyTier %s (sha256 %s)\n", commit, coreDigest) + return nil +} + +func coreArtifactPath(source, cargoTargetDirectory string) string { + if cargoTargetDirectory == "" { + cargoTargetDirectory = "target" + } + if !filepath.IsAbs(cargoTargetDirectory) { + cargoTargetDirectory = filepath.Join(source, cargoTargetDirectory) + } + return filepath.Join(cargoTargetDirectory, coreArtifact) +} + +func requireCleanCheckout(source string) error { + status, err := commandOutput(source, "git", "status", "--porcelain", "--untracked-files=no") + if err != nil { + return err + } + if status != "" { + return fmt.Errorf("EasyTier checkout is dirty:\n%s", status) + } + return nil +} + +func commandOutput(directory, name string, args ...string) (string, error) { + command := exec.Command(name, args...) + command.Dir = directory + output, err := command.CombinedOutput() + if err != nil { + return "", fmt.Errorf("run %s: %w\n%s", name, err, output) + } + return strings.TrimSpace(string(output)), nil +} + +func buildCore(source, sourceDateEpoch string) error { + command := exec.Command("bash", filepath.Join(source, coreBuildScript)) + command.Dir = source + command.Env = reproducibleBuildEnvironment(source, sourceDateEpoch) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + if err := command.Run(); err != nil { + return fmt.Errorf("run EasyTier WASI build script: %w", err) + } + return nil +} + +func generateProto(repositoryRoot, source string) error { + command := exec.Command( + "bash", + filepath.Join(repositoryRoot, protoGenerateScript), + source, + ) + command.Dir = repositoryRoot + command.Stdout = os.Stdout + command.Stderr = os.Stderr + if err := command.Run(); err != nil { + return fmt.Errorf("generate EasyTier Go protobuf bindings: %w", err) + } + return nil +} + +func protoSchemaDigest(source string) (string, error) { + digest := sha256.New() + protoRoot := filepath.Join(source, "easytier-proto", "proto") + for _, name := range managementProtoFiles { + contents, err := os.ReadFile(filepath.Join(protoRoot, name)) + if err != nil { + return "", fmt.Errorf("read EasyTier protobuf source %s: %w", name, err) + } + digest.Write([]byte(name)) + digest.Write([]byte{0}) + digest.Write(contents) + digest.Write([]byte{0}) + } + return hex.EncodeToString(digest.Sum(nil)), nil +} + +func reproducibleBuildEnvironment(source, sourceDateEpoch string) []string { + environment := make([]string, 0, len(os.Environ())+3) + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, "RUSTFLAGS=") || + strings.HasPrefix(entry, "CARGO_ENCODED_RUSTFLAGS=") || + strings.HasPrefix(entry, "SOURCE_DATE_EPOCH=") { + continue + } + environment = append(environment, entry) + } + remap := "--remap-path-prefix=" + source + "=/workspace/EasyTier" + return append( + environment, + "RUSTFLAGS=", + "CARGO_ENCODED_RUSTFLAGS="+remap, + "SOURCE_DATE_EPOCH="+sourceDateEpoch, + ) +} + +func copyArtifact(source, destination string) (string, error) { + contents, err := os.ReadFile(source) + if err != nil { + return "", fmt.Errorf("read artifact %s: %w", source, err) + } + if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil { + return "", fmt.Errorf("create artifact directory: %w", err) + } + if err := os.WriteFile(destination, contents, 0o644); err != nil { + return "", fmt.Errorf("write artifact %s: %w", destination, err) + } + digest := sha256.Sum256(contents) + return hex.EncodeToString(digest[:]), nil +} + +func writeProvenance(repositoryRoot, commit, digest string) error { + source := fmt.Sprintf(`// Code generated by go generate; DO NOT EDIT. + +package artifact + +const ( + Commit = %q + SHA256 = %q +) +`, commit, digest) + formatted, err := format.Source([]byte(source)) + if err != nil { + return fmt.Errorf("format provenance: %w", err) + } + path := filepath.Join(repositoryRoot, "internal", "artifact", "provenance.go") + if err := os.WriteFile(path, formatted, 0o644); err != nil { + return fmt.Errorf("write provenance: %w", err) + } + return nil +} + +func writeProtoProvenance(repositoryRoot, commit, digest string) error { + source := fmt.Sprintf(`// Code generated by go generate; DO NOT EDIT. + +package proto + +const ( + EasyTierCommit = %q + SchemaSHA256 = %q +) +`, commit, digest) + formatted, err := format.Source([]byte(source)) + if err != nil { + return fmt.Errorf("format protobuf provenance: %w", err) + } + path := filepath.Join(repositoryRoot, "proto", "provenance.go") + if err := os.WriteFile(path, formatted, 0o644); err != nil { + return fmt.Errorf("write protobuf provenance: %w", err) + } + return nil +} diff --git a/easytier-go/cmd/update-wasm/main_test.go b/easytier-go/cmd/update-wasm/main_test.go new file mode 100644 index 00000000..1c21ed49 --- /dev/null +++ b/easytier-go/cmd/update-wasm/main_test.go @@ -0,0 +1,92 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCoreArtifactPathUsesCargoTargetDirectory(t *testing.T) { + source := filepath.Join(string(filepath.Separator), "checkout") + for _, test := range []struct { + name string + cargoTargetDir string + wantTargetDir string + }{ + {name: "default", wantTargetDir: filepath.Join(source, "target")}, + { + name: "relative", + cargoTargetDir: "build", + wantTargetDir: filepath.Join(source, "build"), + }, + { + name: "absolute", + cargoTargetDir: filepath.Join(string(filepath.Separator), "cache"), + wantTargetDir: filepath.Join(string(filepath.Separator), "cache"), + }, + } { + t.Run(test.name, func(t *testing.T) { + want := filepath.Join(test.wantTargetDir, coreArtifact) + if got := coreArtifactPath(source, test.cargoTargetDir); got != want { + t.Fatalf("core artifact path = %q, want %q", got, want) + } + }) + } +} + +func TestReproducibleBuildEnvironmentControlsRustInputs(t *testing.T) { + t.Setenv("RUSTFLAGS", "-C target-cpu=native") + t.Setenv("CARGO_ENCODED_RUSTFLAGS", "-C\u001fopt-level=1") + t.Setenv("SOURCE_DATE_EPOCH", "1") + environment := reproducibleBuildEnvironment("/checkout", "1700000000") + joined := strings.Join(environment, "\n") + if strings.Contains(joined, "target-cpu=native") || + strings.Contains(joined, "opt-level=1") { + t.Fatalf("uncontrolled Rust flags remained in environment:\n%s", joined) + } + for _, want := range []string{ + "CARGO_ENCODED_RUSTFLAGS=--remap-path-prefix=/checkout=/workspace/EasyTier", + "SOURCE_DATE_EPOCH=1700000000", + } { + if !strings.Contains(joined, want) { + t.Fatalf("build environment does not contain %q", want) + } + } +} + +func TestProtoSchemaDigestCoversEveryInput(t *testing.T) { + source := t.TempDir() + protoRoot := filepath.Join(source, "easytier-proto", "proto") + if err := os.MkdirAll(protoRoot, 0o755); err != nil { + t.Fatalf("create proto root: %v", err) + } + for _, name := range managementProtoFiles { + if err := os.WriteFile( + filepath.Join(protoRoot, name), + []byte(name), + 0o644, + ); err != nil { + t.Fatalf("write %s: %v", name, err) + } + } + before, err := protoSchemaDigest(source) + if err != nil { + t.Fatalf("digest proto schema: %v", err) + } + changed := managementProtoFiles[len(managementProtoFiles)-1] + if err := os.WriteFile( + filepath.Join(protoRoot, changed), + []byte("changed"), + 0o644, + ); err != nil { + t.Fatalf("change %s: %v", changed, err) + } + after, err := protoSchemaDigest(source) + if err != nil { + t.Fatalf("digest changed proto schema: %v", err) + } + if before == after { + t.Fatal("schema digest did not change with a protobuf input") + } +} diff --git a/easytier-go/examples/dial/main.go b/easytier-go/examples/dial/main.go new file mode 100644 index 00000000..2b515aec --- /dev/null +++ b/easytier-go/examples/dial/main.go @@ -0,0 +1,305 @@ +package main + +import ( + "context" + "encoding/binary" + "errors" + "flag" + "fmt" + "io" + "log" + "net" + "net/netip" + "os" + "os/signal" + "strings" + "syscall" + "time" + + corehost "github.com/EasyTier/EasyTier/easytier-go" + "github.com/EasyTier/EasyTier/easytier-go/internal/contextutil" +) + +const maxUDPPayload = 65507 + +type peerList []string + +func (peers *peerList) Set(value string) error { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("peer URI must not be empty") + } + *peers = append(*peers, value) + return nil +} + +func (peers *peerList) String() string { + return strings.Join(*peers, ",") +} + +type options struct { + peers peerList + networkName string + networkSecret string + ipv4 string + network string + address string + connectTimeout time.Duration +} + +func main() { + options, err := parseOptions(os.Args[1:]) + if err == flag.ErrHelp { + return + } + if err != nil { + log.Printf("invalid arguments: %v", err) + os.Exit(2) + } + + ctx, stop := signal.NotifyContext( + context.Background(), + os.Interrupt, + syscall.SIGTERM, + ) + defer stop() + if err := run(ctx, options, os.Stdin, os.Stdout); err != nil { + log.Printf("EasyTier dial failed: %v", err) + os.Exit(1) + } +} + +func parseOptions(arguments []string) (options, error) { + flags := flag.NewFlagSet("dial", flag.ContinueOnError) + var options options + flags.Var(&options.peers, "p", "EasyTier peer URI; may be repeated") + flags.StringVar(&options.networkName, "network-name", "", "EasyTier network name") + flags.StringVar(&options.networkSecret, "network-secret", "", "EasyTier network secret") + flags.StringVar(&options.ipv4, "ipv4", "", "fixed EasyTier IPv4 address and prefix") + flags.StringVar(&options.network, "network", "tcp4", "overlay network: tcp4 or udp4") + flags.StringVar(&options.address, "address", "", "overlay IPv4 destination") + flags.DurationVar( + &options.connectTimeout, + "connect-timeout", + 10*time.Second, + "time to wait for an overlay route and connection", + ) + if err := flags.Parse(arguments); err != nil { + return options, err + } + if flags.NArg() != 0 { + return options, fmt.Errorf("unexpected argument %q", flags.Arg(0)) + } + if err := options.validate(); err != nil { + return options, err + } + return options, nil +} + +func (options options) validate() error { + if len(options.peers) == 0 { + return fmt.Errorf("at least one -p peer is required") + } + if strings.TrimSpace(options.networkName) == "" { + return fmt.Errorf("--network-name is required") + } + if options.networkSecret == "" { + return fmt.Errorf("--network-secret is required") + } + prefix, err := netip.ParsePrefix(options.ipv4) + if err != nil { + return fmt.Errorf("parse --ipv4: %w", err) + } + if !prefix.Addr().Is4() { + return fmt.Errorf("--ipv4 must be an IPv4 prefix") + } + if options.network != "tcp4" && options.network != "udp4" { + return fmt.Errorf("--network must be tcp4 or udp4") + } + if options.connectTimeout <= 0 { + return fmt.Errorf("--connect-timeout must be positive") + } + address, err := netip.ParseAddrPort(options.address) + if err != nil || !address.Addr().Is4() || address.Port() == 0 { + return fmt.Errorf("--address must be an IPv4 socket address with a nonzero port") + } + return nil +} + +func run( + ctx context.Context, + options options, + input io.Reader, + output io.Writer, +) error { + prefix, err := netip.ParsePrefix(options.ipv4) + if err != nil { + return fmt.Errorf("parse instance IPv4 prefix: %w", err) + } + config, err := corehost.NewInstanceConfigBuilder(options.networkName). + NetworkSecret(options.networkSecret). + IPv4(prefix). + AddPeers([]string(options.peers)...). + Build() + if err != nil { + return fmt.Errorf("build EasyTier instance config: %w", err) + } + + host, err := corehost.New(ctx, corehost.Options{}) + if err != nil { + return fmt.Errorf("create EasyTier host: %w", err) + } + defer host.Close(context.Background()) + instance, err := host.CreateInstance(ctx, config) + if err != nil { + return fmt.Errorf("create EasyTier instance: %w", err) + } + defer instance.Close(context.Background()) + if err := instance.Start(ctx); err != nil { + return fmt.Errorf("start EasyTier instance: %w", err) + } + + target := netip.MustParseAddrPort(options.address) + connectContext, cancelConnect := context.WithTimeout(ctx, options.connectTimeout) + defer cancelConnect() + if target.Addr() != prefix.Addr() { + if err := waitForRoute(connectContext, instance, target.Addr()); err != nil { + return fmt.Errorf("wait for overlay route to %s: %w", target.Addr(), err) + } + } + connection, err := instance.Dial( + connectContext, + options.network, + options.address, + ) + if err != nil { + return err + } + defer connection.Close() + stopClose := contextutil.AfterFunc(ctx, func() { + _ = connection.Close() + }) + defer stopClose() + + log.Printf("connected %s to %s through EasyTier", options.network, options.address) + switch options.network { + case "tcp4": + err = relayTCP(connection, input, output) + case "udp4": + err = exchangeUDP(ctx, connection, input, output) + } + if ctx.Err() != nil { + return nil + } + return err +} + +func waitForRoute( + ctx context.Context, + instance *corehost.Instance, + target netip.Addr, +) error { + ticker := time.NewTicker(50 * time.Millisecond) + defer ticker.Stop() + for { + routes, err := instance.ListRoute(ctx) + if err != nil { + return err + } + if routesReach(routes, target) { + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } +} + +func routesReach(routes []*corehost.Route, target netip.Addr) bool { + octets := target.As4() + targetValue := binary.BigEndian.Uint32(octets[:]) + for _, route := range routes { + if route.GetIpv4Addr().GetAddress().GetAddr() == targetValue { + return true + } + for _, cidr := range route.GetProxyCidrs() { + prefix, err := netip.ParsePrefix(cidr) + if err == nil && prefix.Contains(target) { + return true + } + } + } + return false +} + +func relayTCP(connection net.Conn, input io.Reader, output io.Writer) error { + writeResult := make(chan error, 1) + go func() { + _, err := io.Copy(connection, input) + writeResult <- err + }() + + _, readErr := io.Copy(output, connection) + select { + case writeErr := <-writeResult: + return errors.Join(writeErr, readErr) + default: + return readErr + } +} + +func exchangeUDP( + ctx context.Context, + connection net.Conn, + input io.Reader, + output io.Writer, +) error { + type readResult struct { + payload []byte + err error + } + result := make(chan readResult, 1) + go func() { + payload, err := io.ReadAll(io.LimitReader(input, maxUDPPayload+1)) + result <- readResult{payload: payload, err: err} + }() + + var payload []byte + select { + case <-ctx.Done(): + return ctx.Err() + case read := <-result: + if read.err != nil { + return fmt.Errorf("read UDP payload: %w", read.err) + } + payload = read.payload + } + if len(payload) == 0 { + return fmt.Errorf("UDP input must contain one non-empty datagram") + } + if len(payload) > maxUDPPayload { + return fmt.Errorf("UDP input exceeds the maximum payload of %d bytes", maxUDPPayload) + } + written, err := connection.Write(payload) + if err != nil { + return fmt.Errorf("write UDP datagram: %w", err) + } + if written != len(payload) { + return io.ErrShortWrite + } + + response := make([]byte, maxUDPPayload) + length, err := connection.Read(response) + if err != nil { + return fmt.Errorf("read UDP datagram: %w", err) + } + written, err = output.Write(response[:length]) + if err != nil { + return fmt.Errorf("write UDP response: %w", err) + } + if written != length { + return io.ErrShortWrite + } + return nil +} diff --git a/easytier-go/examples/dial/main_test.go b/easytier-go/examples/dial/main_test.go new file mode 100644 index 00000000..83001cfb --- /dev/null +++ b/easytier-go/examples/dial/main_test.go @@ -0,0 +1,209 @@ +package main + +import ( + "bytes" + "context" + "encoding/binary" + "io" + "net" + "net/netip" + "strings" + "testing" + "time" + + corehost "github.com/EasyTier/EasyTier/easytier-go" + "github.com/EasyTier/EasyTier/easytier-go/proto/common" +) + +func TestParseOptions(t *testing.T) { + options, err := parseOptions([]string{ + "-p", "tcp://198.51.100.10:11010", + "--network-name", "office", + "--network-secret", "secret", + "--ipv4", "10.144.0.10/24", + "--network", "udp4", + "--address", "10.144.0.20:7000", + "--connect-timeout", "3s", + }) + if err != nil { + t.Fatalf("parse options: %v", err) + } + if options.network != "udp4" || + options.address != "10.144.0.20:7000" || + options.connectTimeout != 3*time.Second || + len(options.peers) != 1 { + t.Fatalf("parsed options = %+v", options) + } +} + +func TestParseOptionsRejectsInvalidArguments(t *testing.T) { + valid := []string{ + "-p", "tcp://198.51.100.10:11010", + "--network-name", "office", + "--network-secret", "secret", + "--ipv4", "10.144.0.10/24", + "--address", "10.144.0.20:7000", + } + tests := []struct { + name string + arguments []string + want string + }{ + { + name: "network", + arguments: append(append([]string(nil), valid...), "--network", "sctp"), + want: "--network", + }, + { + name: "destination", + arguments: []string{ + "-p", "tcp://198.51.100.10:11010", + "--network-name", "office", + "--network-secret", "secret", + "--ipv4", "10.144.0.10/24", + "--address", "[2001:db8::1]:7000", + }, + want: "--address", + }, + { + name: "connect timeout", + arguments: append(append([]string(nil), valid...), "--connect-timeout", "0s"), + want: "--connect-timeout", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := parseOptions(test.arguments) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("parse error = %v, want containing %q", err, test.want) + } + }) + } +} + +func TestRoutesReachOverlayOrProxyDestination(t *testing.T) { + overlay := netip.MustParseAddr("10.144.0.20") + overlayOctets := overlay.As4() + routes := []*corehost.Route{ + { + Ipv4Addr: &common.Ipv4Inet{ + Address: &common.Ipv4Addr{ + Addr: binary.BigEndian.Uint32(overlayOctets[:]), + }, + }, + }, + {ProxyCidrs: []string{"10.200.0.0/24"}}, + } + if !routesReach(routes, overlay) { + t.Fatal("overlay destination was not reachable") + } + if !routesReach(routes, netip.MustParseAddr("10.200.0.8")) { + t.Fatal("proxy destination was not reachable") + } + if routesReach(routes, netip.MustParseAddr("10.201.0.8")) { + t.Fatal("unknown destination was reachable") + } +} + +func TestRelayTCP(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + serverDone := make(chan error, 1) + go func() { + request := make([]byte, len("request")) + if _, err := io.ReadFull(server, request); err != nil { + serverDone <- err + return + } + if string(request) != "request" { + serverDone <- io.ErrUnexpectedEOF + return + } + if _, err := server.Write([]byte("response")); err != nil { + serverDone <- err + return + } + serverDone <- server.Close() + }() + + var output bytes.Buffer + if err := relayTCP(client, strings.NewReader("request"), &output); err != nil { + t.Fatalf("relay TCP: %v", err) + } + if err := <-serverDone; err != nil { + t.Fatalf("serve TCP exchange: %v", err) + } + if output.String() != "response" { + t.Fatalf("TCP output = %q", output.String()) + } +} + +func TestExchangeUDP(t *testing.T) { + echo, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) + if err != nil { + t.Fatalf("listen UDP echo: %v", err) + } + defer echo.Close() + echoDone := make(chan error, 1) + go func() { + packet := make([]byte, maxUDPPayload) + length, client, err := echo.ReadFromUDP(packet) + if err == nil { + _, err = echo.WriteToUDP(packet[:length], client) + } + echoDone <- err + }() + + connection, err := net.DialUDP("udp4", nil, echo.LocalAddr().(*net.UDPAddr)) + if err != nil { + t.Fatalf("dial UDP echo: %v", err) + } + defer connection.Close() + _ = connection.SetDeadline(time.Now().Add(5 * time.Second)) + + var output bytes.Buffer + if err := exchangeUDP( + context.Background(), + connection, + strings.NewReader("datagram"), + &output, + ); err != nil { + t.Fatalf("exchange UDP: %v", err) + } + if err := <-echoDone; err != nil { + t.Fatalf("serve UDP exchange: %v", err) + } + if output.String() != "datagram" { + t.Fatalf("UDP output = %q", output.String()) + } +} + +func TestExchangeUDPRejectsOversizedInput(t *testing.T) { + connection, peer := net.Pipe() + defer connection.Close() + defer peer.Close() + err := exchangeUDP( + context.Background(), + connection, + strings.NewReader(strings.Repeat("x", maxUDPPayload+1)), + io.Discard, + ) + if err == nil || !strings.Contains(err.Error(), "maximum payload") { + t.Fatalf("oversized UDP error = %v", err) + } +} + +func TestExchangeUDPCanCancelInputRead(t *testing.T) { + connection, peer := net.Pipe() + defer connection.Close() + defer peer.Close() + input, inputWriter := io.Pipe() + defer input.Close() + defer inputWriter.Close() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + if err := exchangeUDP(ctx, connection, input, io.Discard); err != context.Canceled { + t.Fatalf("cancelled UDP exchange error = %v", err) + } +} diff --git a/easytier-go/examples/tun/go.mod b/easytier-go/examples/tun/go.mod new file mode 100644 index 00000000..3b236045 --- /dev/null +++ b/easytier-go/examples/tun/go.mod @@ -0,0 +1,34 @@ +module github.com/EasyTier/EasyTier/easytier-go/examples/tun + +go 1.25.0 + +require ( + github.com/EasyTier/EasyTier/easytier-go v0.0.0 + github.com/sagernet/sing-tun v0.8.11 +) + +require ( + github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/mdlayher/netlink v1.9.0 // indirect + github.com/mdlayher/socket v0.5.1 // indirect + github.com/metacubex/wazero v0.0.0-20260628025728-9ae6bdcf2a7d // indirect + github.com/sagernet/fswatch v0.1.1 // indirect + github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 // indirect + github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect + github.com/sagernet/nftables v0.3.0-mod.2 // indirect + github.com/sagernet/sing v0.8.0 // indirect + github.com/vishvananda/netns v0.0.4 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/time v0.7.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) + +replace github.com/EasyTier/EasyTier/easytier-go => ../.. diff --git a/easytier-go/examples/tun/go.sum b/easytier-go/examples/tun/go.sum new file mode 100644 index 00000000..b2d2b250 --- /dev/null +++ b/easytier-go/examples/tun/go.sum @@ -0,0 +1,53 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE= +github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco= +github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/metacubex/wazero v0.0.0-20260628025728-9ae6bdcf2a7d h1:UMFI+kdp0jA8s9tC7oul0pWeTW6s8lnm0dD5PT+RY14= +github.com/metacubex/wazero v0.0.0-20260628025728-9ae6bdcf2a7d/go.mod h1:p48xp436h1oGfoXuEnVONeDhTW+E2UpY94GAVxA62oI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= +github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= +github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/GejpNh+z30d5H1hwaB0nD9eY3o= +github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1/go.mod h1:NJKBtm9nVEK3iyOYWsUlrDQuoGh4zJ4KOPhSYVidvQ4= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje+vW5Q0OQ= +github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= +github.com/sagernet/sing v0.8.0 h1:OwLEwbcYfZHvu4olZVljxxC1XRicBqJ1HfiFr6F2WEE= +github.com/sagernet/sing v0.8.0/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing-tun v0.8.11 h1:BFu4+8LNl2JiTQtto5f+5AbkH90qgdoZEAqUbGiEXCg= +github.com/sagernet/sing-tun v0.8.11/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/easytier-go/examples/tun/main.go b/easytier-go/examples/tun/main.go new file mode 100644 index 00000000..362d33ca --- /dev/null +++ b/easytier-go/examples/tun/main.go @@ -0,0 +1,498 @@ +package main + +import ( + "context" + "encoding/binary" + "errors" + "flag" + "fmt" + "io" + "log" + "net/netip" + "os" + "os/signal" + "runtime" + "strings" + "sync" + "syscall" + "time" + + corehost "github.com/EasyTier/EasyTier/easytier-go" + tun "github.com/sagernet/sing-tun" +) + +const ( + tunMTU = 1380 + tunNamePrefix = "et-go" + tunReadWorkers = 32 +) + +func main() { + options := parseOptions() + if _, err := options.validate(); err != nil { + log.Printf("invalid arguments: %v", err) + flag.Usage() + os.Exit(2) + } + + ctx, stop := signal.NotifyContext( + context.Background(), + os.Interrupt, + syscall.SIGTERM, + ) + defer stop() + if err := run(ctx, options); err != nil { + log.Printf("EasyTier TUN failed: %v", err) + os.Exit(1) + } +} + +// EasyTier lifecycle + +func run(ctx context.Context, options options) error { + prefix, err := options.validate() + if err != nil { + return err + } + + host, err := corehost.New(ctx, corehost.Options{}) + if err != nil { + return fmt.Errorf("create EasyTier host: %w", err) + } + defer host.Close(context.Background()) + + config, err := corehost.NewInstanceConfigBuilder(options.networkName). + NetworkSecret(options.networkSecret). + IPv4(prefix). + AddPeers([]string(options.peers)...). + AddPortForwards([]corehost.PortForwardConfig(options.portForwards)...). + Build() + if err != nil { + return fmt.Errorf("build EasyTier instance config: %w", err) + } + instance, err := host.CreateInstance(ctx, config) + if err != nil { + return fmt.Errorf("create EasyTier instance: %w", err) + } + defer instance.Close(context.Background()) + go logEvents(ctx, instance.Events()) + + device, err := createTun(prefix) + if err != nil { + return err + } + defer device.Close() + deviceName, err := device.Name() + if err != nil { + return fmt.Errorf("query TUN interface name: %w", err) + } + + if err := instance.Start(ctx); err != nil { + return fmt.Errorf("start EasyTier instance: %w", err) + } + startManagementSignals(ctx, instance) + log.Printf( + "connected %s to EasyTier network %q as %s", + deviceName, + options.networkName, + prefix, + ) + return bridgePackets(ctx, device, instance) +} + +func logEvents(ctx context.Context, events <-chan corehost.Event) { + for { + select { + case event, open := <-events: + if !open { + return + } + log.Printf("EasyTier event [%s]: %s", event.Kind, event.Message) + case <-ctx.Done(): + return + } + } +} + +func startManagementSignals( + ctx context.Context, + instance *corehost.Instance, +) { + var peerSignal syscall.Signal + var routeSignal syscall.Signal + // SIGUSR1 and SIGUSR2 have different numbers on Linux and macOS. + switch runtime.GOOS { + case "linux": + peerSignal, routeSignal = 10, 12 + case "darwin": + peerSignal, routeSignal = 30, 31 + default: + return + } + + signals := make(chan os.Signal, 1) + signal.Notify(signals, peerSignal, routeSignal) + go func() { + defer signal.Stop(signals) + for { + select { + case received := <-signals: + queryContext, cancel := context.WithTimeout(ctx, 5*time.Second) + switch received { + case peerSignal: + response, err := instance.ListPeer(queryContext) + if err != nil { + log.Printf("query EasyTier peer list: %v", err) + } else { + log.Printf("EasyTier peer list: %v", response) + } + case routeSignal: + response, err := instance.ListRoute(queryContext) + if err != nil { + log.Printf("query EasyTier route list: %v", err) + } else { + log.Printf("EasyTier route list: %v", response) + } + } + cancel() + case <-ctx.Done(): + return + } + } + }() +} + +// Native TUN adapter + +func createTun(prefix netip.Prefix) (*packetDevice, error) { + name := tun.CalculateInterfaceName(tunNamePrefix) + nativeTun, err := tun.New(tun.Options{ + Name: name, + Inet4Address: []netip.Prefix{prefix}, + MTU: tunMTU, + GSO: false, + AutoRoute: false, + StrictRoute: false, + InterfaceMonitor: noRouteInterfaceMonitor{}, + }) + if err != nil { + return nil, fmt.Errorf( + "create TUN %q (requires elevated privileges): %w", + name, + err, + ) + } + device := newPacketDevice(nativeTun) + if err := nativeTun.Start(); err != nil { + _ = device.Close() + return nil, fmt.Errorf("start TUN %q: %w", name, err) + } + return device, nil +} + +type packetDevice struct { + tun.Tun + packetOffset int + readMutex sync.Mutex + writeMutex sync.Mutex + readBuffer []byte + writeBuffer []byte + closeOnce sync.Once + closeErr error +} + +func newPacketDevice(nativeTun tun.Tun) *packetDevice { + device := &packetDevice{Tun: nativeTun} + if runtime.GOOS == "darwin" { + device.packetOffset = 4 + device.readBuffer = make([]byte, 65535+device.packetOffset) + device.writeBuffer = make([]byte, 65535+device.packetOffset) + } + return device +} + +func (device *packetDevice) Read(packet []byte) (int, error) { + device.readMutex.Lock() + defer device.readMutex.Unlock() + + if device.packetOffset == 0 { + return device.Tun.Read(packet) + } + if len(packet)+device.packetOffset > len(device.readBuffer) { + return 0, io.ErrShortBuffer + } + frame := device.readBuffer[:len(packet)+device.packetOffset] + length, err := device.Tun.Read(frame) + if err != nil { + return 0, err + } + if length < device.packetOffset { + return 0, io.ErrUnexpectedEOF + } + length -= device.packetOffset + copy(packet, frame[device.packetOffset:device.packetOffset+length]) + return length, nil +} + +func (device *packetDevice) Write(packet []byte) (int, error) { + device.writeMutex.Lock() + defer device.writeMutex.Unlock() + + if device.packetOffset == 0 { + return device.Tun.Write(packet) + } + if len(packet)+device.packetOffset > len(device.writeBuffer) { + return 0, io.ErrShortBuffer + } + frame := device.writeBuffer[:len(packet)+device.packetOffset] + if err := encodeNativeTunPacketHeader( + frame[:device.packetOffset], + packet, + ); err != nil { + return 0, err + } + copy(frame[device.packetOffset:], packet) + written, err := device.Tun.Write(frame) + if written <= device.packetOffset { + return 0, err + } + return written - device.packetOffset, err +} + +func (device *packetDevice) Close() error { + device.closeOnce.Do(func() { + device.closeErr = device.Tun.Close() + }) + return device.closeErr +} + +func encodeNativeTunPacketHeader(header, packet []byte) error { + if len(header) == 0 { + return nil + } + if len(packet) == 0 { + return fmt.Errorf("encode utun header: empty packet") + } + family := uint32(syscall.AF_INET) + switch packet[0] >> 4 { + case 4: + case 6: + family = syscall.AF_INET6 + default: + return fmt.Errorf("unsupported IP version %d", packet[0]>>4) + } + binary.BigEndian.PutUint32(header, family) + return nil +} + +// AutoRoute is disabled, so sing-tun v0.8.11 only calls this method. +type noRouteInterfaceMonitor struct { + tun.DefaultInterfaceMonitor +} + +func (noRouteInterfaceMonitor) RegisterMyInterface(string) { +} + +// Packet bridge + +func bridgePackets( + ctx context.Context, + device *packetDevice, + instance *corehost.Instance, +) error { + pumpContext, cancel := context.WithCancel(ctx) + defer cancel() + results := make(chan error, tunReadWorkers+1) + for range tunReadWorkers { + go func() { results <- copyTunToEasyTier(pumpContext, device, instance) }() + } + go func() { results <- copyEasyTierToTun(pumpContext, instance, device) }() + + errs := []error{<-results} + cancel() + errs = append(errs, device.Close()) + for range tunReadWorkers { + errs = append(errs, <-results) + } + return errors.Join(errs...) +} + +func copyTunToEasyTier( + ctx context.Context, + device io.Reader, + instance *corehost.Instance, +) error { + packet := make([]byte, 65535) + for { + length, err := device.Read(packet) + if err != nil { + if ctx.Err() != nil { + return nil + } + return fmt.Errorf("read TUN packet: %w", err) + } + if length == 0 || packet[0]>>4 != 4 { + continue + } + if err := instance.SendPacket(ctx, packet[:length]); err != nil { + if ctx.Err() != nil { + return nil + } + return fmt.Errorf("send TUN packet to EasyTier: %w", err) + } + } +} + +func copyEasyTierToTun( + ctx context.Context, + instance *corehost.Instance, + device io.Writer, +) error { + for { + packet, err := instance.ReceivePacket(ctx) + if err != nil { + if ctx.Err() != nil { + return nil + } + return fmt.Errorf("receive EasyTier packet: %w", err) + } + written, err := device.Write(packet) + if err != nil { + if ctx.Err() != nil { + return nil + } + return fmt.Errorf("write EasyTier packet to TUN: %w", err) + } + // Wintun reports a full send ring as (0, nil), meaning that it has + // dropped this packet. Keep the bridge alive for subsequent packets. + if written == 0 { + continue + } + if written != len(packet) { + return io.ErrShortWrite + } + } +} + +// Optional port forwarding + +type portForwardList []corehost.PortForwardConfig + +func (rules *portForwardList) Set(value string) error { + rule, err := parsePortForwardRule(value) + if err == nil { + *rules = append(*rules, rule) + } + return err +} + +func (rules *portForwardList) String() string { + values := make([]string, len(*rules)) + for index, rule := range *rules { + values[index] = fmt.Sprintf( + "%s://%s/%s", + rule.Protocol, + rule.Bind, + rule.Destination, + ) + } + return strings.Join(values, ",") +} + +func parsePortForwardRule(value string) (corehost.PortForwardConfig, error) { + protocolText, addresses, ok := strings.Cut(value, "://") + if !ok || (protocolText != "tcp" && protocolText != "udp") { + return corehost.PortForwardConfig{}, fmt.Errorf( + "port forward %q must use tcp:// or udp://", + value, + ) + } + bindText, targetText, ok := strings.Cut(addresses, "/") + if !ok { + return corehost.PortForwardConfig{}, fmt.Errorf( + "port forward %q must contain bind/overlay-target", + value, + ) + } + bind, err := netip.ParseAddrPort(bindText) + if err != nil { + return corehost.PortForwardConfig{}, fmt.Errorf("parse bind address: %w", err) + } + target, err := netip.ParseAddrPort(targetText) + if err != nil { + return corehost.PortForwardConfig{}, fmt.Errorf("parse target address: %w", err) + } + if !bind.Addr().Is4() || !target.Addr().Is4() { + return corehost.PortForwardConfig{}, fmt.Errorf( + "port forward requires IPv4 addresses", + ) + } + protocol := corehost.PortForwardTCP + if protocolText == "udp" { + protocol = corehost.PortForwardUDP + } + return corehost.PortForwardConfig{ + Protocol: protocol, + Bind: bind, + Destination: target, + }, nil +} + +// Command-line options + +func parseOptions() options { + var options options + flag.Var(&options.peers, "p", "EasyTier peer URI; may be repeated") + flag.Var( + &options.portForwards, + "port-forward", + "tcp://bind/overlay-target or udp://bind/overlay-target; may be repeated", + ) + flag.StringVar(&options.networkName, "network-name", "", "EasyTier network name") + flag.StringVar(&options.networkSecret, "network-secret", "", "EasyTier network secret") + flag.StringVar(&options.ipv4, "ipv4", "", "fixed EasyTier IPv4 address and prefix") + flag.Parse() + return options +} + +type peerList []string + +func (peers *peerList) Set(value string) error { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("peer URI must not be empty") + } + *peers = append(*peers, value) + return nil +} + +func (peers *peerList) String() string { + return strings.Join(*peers, ",") +} + +type options struct { + peers peerList + portForwards portForwardList + networkName string + networkSecret string + ipv4 string +} + +func (options options) validate() (netip.Prefix, error) { + if len(options.peers) == 0 { + return netip.Prefix{}, fmt.Errorf("at least one -p peer is required") + } + if strings.TrimSpace(options.networkName) == "" { + return netip.Prefix{}, fmt.Errorf("--network-name is required") + } + if options.networkSecret == "" { + return netip.Prefix{}, fmt.Errorf("--network-secret is required") + } + prefix, err := netip.ParsePrefix(options.ipv4) + if err != nil { + return netip.Prefix{}, fmt.Errorf("parse --ipv4: %w", err) + } + if !prefix.Addr().Is4() { + return netip.Prefix{}, fmt.Errorf("--ipv4 must be an IPv4 prefix") + } + return prefix, nil +} diff --git a/easytier-go/examples/tun/main_test.go b/easytier-go/examples/tun/main_test.go new file mode 100644 index 00000000..6250059b --- /dev/null +++ b/easytier-go/examples/tun/main_test.go @@ -0,0 +1,256 @@ +package main + +import ( + "io" + "net/netip" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + tun "github.com/sagernet/sing-tun" +) + +func TestOptionsValidate(t *testing.T) { + valid := options{ + peers: peerList{"tcp://198.51.100.10:11010"}, + networkName: "office", + networkSecret: "secret", + ipv4: "10.144.0.10/24", + } + + tests := []struct { + name string + mutate func(*options) + wantErr string + }{ + { + name: "missing peer", + mutate: func(value *options) { value.peers = nil }, + wantErr: "at least one -p", + }, + { + name: "missing network name", + mutate: func(value *options) { value.networkName = "" }, + wantErr: "--network-name", + }, + { + name: "missing network secret", + mutate: func(value *options) { value.networkSecret = "" }, + wantErr: "--network-secret", + }, + { + name: "invalid IPv4 prefix", + mutate: func(value *options) { value.ipv4 = "10.144.0.10" }, + wantErr: "--ipv4", + }, + { + name: "IPv6 prefix", + mutate: func(value *options) { value.ipv4 = "fd00::10/64" }, + wantErr: "IPv4", + }, + } + + prefix, err := valid.validate() + if err != nil { + t.Fatalf("validate valid options: %v", err) + } + if prefix != netip.MustParsePrefix("10.144.0.10/24") { + t.Fatalf("validated prefix = %s", prefix) + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + value := valid + value.peers = append(peerList(nil), valid.peers...) + test.mutate(&value) + _, err := value.validate() + if err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("validate error = %v, want containing %q", err, test.wantErr) + } + }) + } +} + +func TestPeerListRejectsEmptyPeer(t *testing.T) { + var peers peerList + if err := peers.Set(" "); err == nil { + t.Fatal("empty peer was accepted") + } + if err := peers.Set("tcp://198.51.100.10:11010"); err != nil { + t.Fatalf("set peer: %v", err) + } + if got := peers.String(); got != "tcp://198.51.100.10:11010" { + t.Fatalf("peer list string = %q", got) + } +} + +type blockingTun struct { + readStarted chan struct{} + closed chan struct{} + startOnce sync.Once + closeOnce sync.Once + closeCount atomic.Int32 +} + +func (device *blockingTun) Read([]byte) (int, error) { + device.startOnce.Do(func() { close(device.readStarted) }) + <-device.closed + return 0, io.ErrClosedPipe +} + +func (*blockingTun) Write(packet []byte) (int, error) { + return len(packet), nil +} + +func (*blockingTun) Name() (string, error) { + return "test-tun", nil +} + +func (*blockingTun) Start() error { + return nil +} + +func (device *blockingTun) Close() error { + device.closeOnce.Do(func() { + device.closeCount.Add(1) + close(device.closed) + }) + return nil +} + +func (*blockingTun) UpdateRouteOptions(tun.Options) error { + return nil +} + +func TestPacketDeviceCloseUnblocksRead(t *testing.T) { + nativeTun := &blockingTun{ + readStarted: make(chan struct{}), + closed: make(chan struct{}), + } + device := newPacketDevice(nativeTun) + readDone := make(chan error, 1) + go func() { + _, err := device.Read(make([]byte, 1500)) + readDone <- err + }() + <-nativeTun.readStarted + + if err := device.Close(); err != nil { + t.Fatalf("close device: %v", err) + } + if err := device.Close(); err != nil { + t.Fatalf("close device again: %v", err) + } + select { + case err := <-readDone: + if err != io.ErrClosedPipe { + t.Fatalf("read error = %v, want %v", err, io.ErrClosedPipe) + } + case <-time.After(time.Second): + t.Fatal("read remained blocked after close") + } + if count := nativeTun.closeCount.Load(); count != 1 { + t.Fatalf("native close count = %d, want 1", count) + } +} + +type memoryTun struct { + readPacket []byte + wrotePacket []byte +} + +func (device *memoryTun) Read(packet []byte) (int, error) { + return copy(packet, device.readPacket), nil +} + +func (device *memoryTun) Write(packet []byte) (int, error) { + device.wrotePacket = append(device.wrotePacket[:0], packet...) + return len(packet), nil +} + +func (*memoryTun) Name() (string, error) { + return "memory-tun", nil +} + +func (*memoryTun) Start() error { + return nil +} + +func (*memoryTun) Close() error { + return nil +} + +func (*memoryTun) UpdateRouteOptions(tun.Options) error { + return nil +} + +func TestPacketDeviceNativeFraming(t *testing.T) { + ipPacket := []byte{ + 0x45, 0, 0, 20, + 0, 0, 0, 0, + 64, 1, 0, 0, + 10, 0, 0, 1, + 10, 0, 0, 2, + } + nativeTun := &memoryTun{} + device := newPacketDevice(nativeTun) + nativePacket := make([]byte, device.packetOffset+len(ipPacket)) + if err := encodeNativeTunPacketHeader( + nativePacket[:device.packetOffset], + ipPacket, + ); err != nil { + t.Fatalf("encode native test packet: %v", err) + } + copy(nativePacket[device.packetOffset:], ipPacket) + + nativeTun.readPacket = nativePacket + received := make([]byte, 1500) + length, err := device.Read(received) + if err != nil { + t.Fatalf("read packet: %v", err) + } + if string(received[:length]) != string(ipPacket) { + t.Fatalf("read packet = %x, want %x", received[:length], ipPacket) + } + written, err := device.Write(ipPacket) + if err != nil { + t.Fatalf("write packet: %v", err) + } + if written != len(ipPacket) { + t.Fatalf("write length = %d, want %d", written, len(ipPacket)) + } + if string(nativeTun.wrotePacket) != string(nativePacket) { + t.Fatalf( + "native write packet = %x, want %x", + nativeTun.wrotePacket, + nativePacket, + ) + } +} + +func TestPortForwardList(t *testing.T) { + var rules portForwardList + for _, value := range []string{ + "tcp://127.0.0.1:5202/10.144.0.20:5201", + "udp://0.0.0.0:5203/10.144.0.21:5201", + } { + if err := rules.Set(value); err != nil { + t.Fatalf("set %q: %v", value, err) + } + } + if got := rules.String(); got != + "tcp://127.0.0.1:5202/10.144.0.20:5201,"+ + "udp://0.0.0.0:5203/10.144.0.21:5201" { + t.Fatalf("rules string = %q", got) + } + if err := rules.Set("sctp://127.0.0.1:1/10.0.0.1:2"); err == nil { + t.Fatal("unsupported protocol was accepted") + } + if rules[0].Protocol != "tcp" || + rules[0].Bind != netip.MustParseAddrPort("127.0.0.1:5202") || + rules[0].Destination != netip.MustParseAddrPort("10.144.0.20:5201") { + t.Fatalf("first port-forward rule = %+v", rules[0]) + } +} diff --git a/easytier-go/examples/web-client/main.go b/easytier-go/examples/web-client/main.go new file mode 100644 index 00000000..f44c05a0 --- /dev/null +++ b/easytier-go/examples/web-client/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + "os/signal" + "strings" + "syscall" + + corehost "github.com/EasyTier/EasyTier/easytier-go" +) + +type options struct { + endpoint string + machineID string + hostname string + secure bool +} + +func main() { + options, err := parseOptions(os.Args[1:]) + if err == flag.ErrHelp { + return + } + if err != nil { + log.Printf("invalid arguments: %v", err) + os.Exit(2) + } + + ctx, stop := signal.NotifyContext( + context.Background(), + os.Interrupt, + syscall.SIGTERM, + ) + defer stop() + if err := run(ctx, options); err != nil { + log.Printf("EasyTier Web client failed: %v", err) + os.Exit(1) + } +} + +func parseOptions(arguments []string) (options, error) { + flags := flag.NewFlagSet("web-client", flag.ContinueOnError) + var options options + flags.StringVar(&options.endpoint, "web-endpoint", "", "Web config-server endpoint") + flags.StringVar(&options.machineID, "web-machine-id", "", "stable machine UUID") + flags.StringVar(&options.hostname, "web-hostname", "", "machine hostname") + flags.BoolVar(&options.secure, "web-secure", false, "use a secure Web connection") + if err := flags.Parse(arguments); err != nil { + return options, err + } + if flags.NArg() != 0 { + return options, fmt.Errorf("unexpected argument %q", flags.Arg(0)) + } + options.endpoint = strings.TrimSpace(options.endpoint) + options.machineID = strings.TrimSpace(options.machineID) + options.hostname = strings.TrimSpace(options.hostname) + if options.endpoint == "" { + return options, fmt.Errorf("--web-endpoint is required") + } + if options.machineID == "" { + return options, fmt.Errorf("--web-machine-id is required") + } + return options, nil +} + +func run(ctx context.Context, options options) error { + host, err := corehost.New(ctx, corehost.Options{}) + if err != nil { + return fmt.Errorf("create EasyTier host: %w", err) + } + defer host.Close(context.Background()) + + client, err := host.ConnectWebClient(ctx, corehost.WebClientOptions{ + Endpoint: options.endpoint, + MachineID: options.machineID, + Hostname: options.hostname, + SecureMode: options.secure, + }) + if err != nil { + return fmt.Errorf("connect EasyTier Web client: %w", err) + } + defer client.Close(context.Background()) + + log.Printf("EasyTier Web client started for machine %s", options.machineID) + <-ctx.Done() + return nil +} diff --git a/easytier-go/examples/web-client/main_test.go b/easytier-go/examples/web-client/main_test.go new file mode 100644 index 00000000..99955020 --- /dev/null +++ b/easytier-go/examples/web-client/main_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "strings" + "testing" +) + +func TestParseOptions(t *testing.T) { + options, err := parseOptions([]string{ + "--web-endpoint", " tcp://config.example.com:22020/team ", + "--web-machine-id", " 11111111-2222-4333-8444-555555555555 ", + "--web-hostname", " edge-gateway ", + "--web-secure", + }) + if err != nil { + t.Fatalf("parse options: %v", err) + } + if options.endpoint != "tcp://config.example.com:22020/team" || + options.machineID != "11111111-2222-4333-8444-555555555555" || + options.hostname != "edge-gateway" || + !options.secure { + t.Fatalf("parsed options = %+v", options) + } +} + +func TestParseOptionsRequiresEndpointAndMachineID(t *testing.T) { + tests := []struct { + name string + arguments []string + want string + }{ + { + name: "endpoint", + arguments: []string{ + "--web-machine-id", "11111111-2222-4333-8444-555555555555", + }, + want: "--web-endpoint", + }, + { + name: "machine ID", + arguments: []string{"--web-endpoint", "tcp://config.example.com:22020/team"}, + want: "--web-machine-id", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := parseOptions(test.arguments) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("parse error = %v, want containing %q", err, test.want) + } + }) + } +} diff --git a/easytier-go/go.mod b/easytier-go/go.mod new file mode 100644 index 00000000..acf062a6 --- /dev/null +++ b/easytier-go/go.mod @@ -0,0 +1,9 @@ +module github.com/EasyTier/EasyTier/easytier-go + +go 1.20 + +require ( + github.com/metacubex/wazero v0.0.0-20260628025728-9ae6bdcf2a7d + golang.org/x/sys v0.30.0 + google.golang.org/protobuf v1.34.2 +) diff --git a/easytier-go/go.sum b/easytier-go/go.sum new file mode 100644 index 00000000..3234afbe --- /dev/null +++ b/easytier-go/go.sum @@ -0,0 +1,8 @@ +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/metacubex/wazero v0.0.0-20260628025728-9ae6bdcf2a7d h1:UMFI+kdp0jA8s9tC7oul0pWeTW6s8lnm0dD5PT+RY14= +github.com/metacubex/wazero v0.0.0-20260628025728-9ae6bdcf2a7d/go.mod h1:p48xp436h1oGfoXuEnVONeDhTW+E2UpY94GAVxA62oI= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/easytier-go/host.go b/easytier-go/host.go new file mode 100644 index 00000000..774ccada --- /dev/null +++ b/easytier-go/host.go @@ -0,0 +1,36 @@ +package host + +import ( + "context" + + internalhost "github.com/EasyTier/EasyTier/easytier-go/internal/host" +) + +type State = internalhost.State + +const ( + StateCreated State = internalhost.StateCreated + StateStarting State = internalhost.StateStarting + StateRunning State = internalhost.StateRunning + StateStopping State = internalhost.StateStopping + StateStopped State = internalhost.StateStopped +) + +type Options = internalhost.Options + +type EmbeddedCoreInfo = internalhost.EmbeddedCoreInfo + +// Event is one best-effort notification emitted by an EasyTier instance. +type Event = internalhost.Event + +type Host = internalhost.Host + +type Instance = internalhost.Instance + +func New(ctx context.Context, options Options) (*Host, error) { + return internalhost.New(ctx, options) +} + +func CoreInfo() EmbeddedCoreInfo { + return internalhost.CoreInfo() +} diff --git a/easytier-go/instance_config.go b/easytier-go/instance_config.go new file mode 100644 index 00000000..79e08c19 --- /dev/null +++ b/easytier-go/instance_config.go @@ -0,0 +1,31 @@ +package host + +import internalhost "github.com/EasyTier/EasyTier/easytier-go/internal/host" + +// P2PPolicy controls when an instance attempts peer-to-peer connections. +type P2PPolicy = internalhost.P2PPolicy + +// HolePunchingPolicy selects the hole-punching methods available to an instance. +type HolePunchingPolicy = internalhost.HolePunchingPolicy + +// PortForwardProtocol identifies the transport used by a port-forward rule. +type PortForwardProtocol = internalhost.PortForwardProtocol + +const ( + PortForwardTCP PortForwardProtocol = internalhost.PortForwardTCP + PortForwardUDP PortForwardProtocol = internalhost.PortForwardUDP +) + +// PortForwardConfig exposes a host socket through the EasyTier data plane. +type PortForwardConfig = internalhost.PortForwardConfig + +// InstanceConfig is an immutable EasyTier instance configuration. +type InstanceConfig = internalhost.InstanceConfig + +// InstanceConfigBuilder builds a validated InstanceConfig. +type InstanceConfigBuilder = internalhost.InstanceConfigBuilder + +// NewInstanceConfigBuilder starts a configuration for networkName. +func NewInstanceConfigBuilder(networkName string) *InstanceConfigBuilder { + return internalhost.NewInstanceConfigBuilder(networkName) +} diff --git a/easytier-go/internal/artifact/easytier_core.wasm b/easytier-go/internal/artifact/easytier_core.wasm new file mode 100644 index 00000000..e116661f Binary files /dev/null and b/easytier-go/internal/artifact/easytier_core.wasm differ diff --git a/easytier-go/internal/artifact/embed.go b/easytier-go/internal/artifact/embed.go new file mode 100644 index 00000000..4e913bfe --- /dev/null +++ b/easytier-go/internal/artifact/embed.go @@ -0,0 +1,12 @@ +package artifact + +import _ "embed" + +//go:generate go run ../../cmd/update-wasm + +//go:embed easytier_core.wasm +var core []byte + +func Core() []byte { + return core +} diff --git a/easytier-go/internal/artifact/embed_test.go b/easytier-go/internal/artifact/embed_test.go new file mode 100644 index 00000000..8fe23d1a --- /dev/null +++ b/easytier-go/internal/artifact/embed_test.go @@ -0,0 +1,17 @@ +package artifact + +import ( + "crypto/sha256" + "encoding/hex" + "testing" +) + +func TestEmbeddedCoreMatchesProvenance(t *testing.T) { + if len(Commit) != 40 { + t.Fatalf("EasyTier commit = %q", Commit) + } + digest := sha256.Sum256(Core()) + if got := hex.EncodeToString(digest[:]); got != SHA256 { + t.Fatalf("embedded core SHA-256 = %s, want %s", got, SHA256) + } +} diff --git a/easytier-go/internal/artifact/provenance.go b/easytier-go/internal/artifact/provenance.go new file mode 100644 index 00000000..68b90326 --- /dev/null +++ b/easytier-go/internal/artifact/provenance.go @@ -0,0 +1,8 @@ +// Code generated by go generate; DO NOT EDIT. + +package artifact + +const ( + Commit = "63519db2b5f2a6a1b9b7f20905f036dab54eb829" + SHA256 = "8b82f37d62fba2ffe8e256386096fddf448d4261d51c495d5bd97cd11bbb24a0" +) diff --git a/easytier-go/internal/contextutil/context.go b/easytier-go/internal/contextutil/context.go new file mode 100644 index 00000000..fc64b10a --- /dev/null +++ b/easytier-go/internal/contextutil/context.go @@ -0,0 +1,57 @@ +package contextutil + +import ( + "context" + "sync" + "time" +) + +type withoutCancelContext struct { + context.Context +} + +func (withoutCancelContext) Deadline() (time.Time, bool) { + return time.Time{}, false +} + +func (withoutCancelContext) Done() <-chan struct{} { + return nil +} + +func (withoutCancelContext) Err() error { + return nil +} + +// WithoutCancel returns a copy of parent that is not canceled when parent is. +// +// This is the Go 1.20-compatible equivalent of context.WithoutCancel. +func WithoutCancel(parent context.Context) context.Context { + if parent == nil { + panic("cannot create context from nil parent") + } + return withoutCancelContext{Context: parent} +} + +// AfterFunc arranges to call function in its own goroutine after context is +// canceled. The returned stop function reports whether it prevented the call. +// +// This is the Go 1.20-compatible equivalent of context.AfterFunc. +func AfterFunc(ctx context.Context, function func()) func() bool { + var once sync.Once + stopped := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + once.Do(func() { go function() }) + case <-stopped: + } + }() + return func() bool { + prevented := false + once.Do(func() { + prevented = true + close(stopped) + }) + return prevented + } +} diff --git a/easytier-go/internal/contextutil/context_test.go b/easytier-go/internal/contextutil/context_test.go new file mode 100644 index 00000000..68efb1a2 --- /dev/null +++ b/easytier-go/internal/contextutil/context_test.go @@ -0,0 +1,62 @@ +package contextutil + +import ( + "context" + "testing" + "time" +) + +type contextKey struct{} + +func TestWithoutCancelPreservesValuesOnly(t *testing.T) { + deadline := time.Now().Add(time.Minute) + parent, cancel := context.WithDeadline( + context.WithValue(context.Background(), contextKey{}, "value"), + deadline, + ) + cancel() + + ctx := WithoutCancel(parent) + if got := ctx.Value(contextKey{}); got != "value" { + t.Fatalf("value = %v, want value", got) + } + if _, ok := ctx.Deadline(); ok { + t.Fatal("deadline was preserved") + } + if ctx.Done() != nil { + t.Fatal("Done channel is non-nil") + } + if err := ctx.Err(); err != nil { + t.Fatalf("Err = %v, want nil", err) + } +} + +func TestAfterFuncRunsOnCancellation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + called := make(chan struct{}) + stop := AfterFunc(ctx, func() { close(called) }) + cancel() + select { + case <-called: + case <-time.After(time.Second): + t.Fatal("function was not called") + } + if stop() { + t.Fatal("stop prevented an already started function") + } +} + +func TestAfterFuncCanBeStopped(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + called := make(chan struct{}) + stop := AfterFunc(ctx, func() { close(called) }) + if !stop() { + t.Fatal("stop did not prevent function") + } + cancel() + select { + case <-called: + t.Fatal("stopped function was called") + case <-time.After(10 * time.Millisecond): + } +} diff --git a/easytier-go/internal/coreabi/core.go b/easytier-go/internal/coreabi/core.go new file mode 100644 index 00000000..f882577e --- /dev/null +++ b/easytier-go/internal/coreabi/core.go @@ -0,0 +1,495 @@ +package coreabi + +import ( + "context" + "errors" + "fmt" + + "github.com/EasyTier/EasyTier/easytier-go/internal/contextutil" + "github.com/metacubex/wazero/api" +) + +type State int32 + +const ( + StateCreated State = iota + StateStarting + StateRunning + StateStopping + StateStopped +) + +type Core struct { + module api.Module + functions map[string]guestFunction + driveFunction guestFunction + deadlineFunction guestFunction + completionFunction guestFunction + handle uint64 + packetBuffer uint32 + packetBufferCapacity uint32 + dataPlaneAddress uint32 + dataPlaneInput uint32 + dataPlaneInputSize uint32 + dataPlaneOutput uint32 + dataPlaneOutputSize uint32 + dropped bool +} + +type guestFunction struct { + call api.Function + parameterCount int +} + +func New(module api.Module) (*Core, error) { + if module == nil { + return nil, fmt.Errorf("create core ABI with nil guest module") + } + core := &Core{ + module: module, + functions: make(map[string]guestFunction), + } + if err := core.validateDataPlaneABI(context.Background()); err != nil { + return nil, fmt.Errorf("validate EasyTier data plane ABI: %w", err) + } + if err := core.validateRPCABI(context.Background()); err != nil { + return nil, fmt.Errorf("validate EasyTier RPC ABI: %w", err) + } + return core, nil +} + +func (core *Core) Create( + ctx context.Context, + configEnvelope []byte, + packetSink uint64, + eventSink uint64, +) (err error) { + if core.handle != 0 || core.dropped { + return fmt.Errorf("create EasyTier instance in used guest module") + } + pointer, err := core.allocate(ctx, uint32(len(configEnvelope))) + if err != nil { + return err + } + defer core.cleanupBuffer(ctx, pointer, &err) + if !core.module.Memory().Write(pointer, configEnvelope) { + return fmt.Errorf("write EasyTier config to guest memory") + } + result, err := core.callOne( + ctx, + "easytier_instance_create", + uint64(pointer), + uint64(len(configEnvelope)), + packetSink, + eventSink, + ) + if err != nil { + return err + } + if result == 0 { + message, readErr := core.errorMessage(ctx, 0) + if readErr != nil { + return fmt.Errorf("create EasyTier instance: %w", readErr) + } + return fmt.Errorf("create EasyTier instance: %s", message) + } + core.handle = result + return nil +} + +func (core *Core) Start(ctx context.Context) error { + return core.callStatus(ctx, "easytier_instance_start") +} + +func (core *Core) Stop(ctx context.Context) error { + return core.callStatus(ctx, "easytier_instance_stop") +} + +func (core *Core) Drive(ctx context.Context) (State, error) { + result, err := core.callCached( + ctx, + "easytier_instance_drive", + &core.driveFunction, + core.handle, + ) + if err != nil { + return 0, err + } + state := State(int32(result)) + if state < 0 { + return 0, core.statusError(ctx, "drive EasyTier instance", int32(state)) + } + return state, nil +} + +func (core *Core) NotifyCompletions(ctx context.Context) error { + return core.callStatus(ctx, "easytier_instance_notify_completions") +} + +func (core *Core) State(ctx context.Context) (State, error) { + result, err := core.callOne(ctx, "easytier_instance_state", core.handle) + if err != nil { + return 0, err + } + state := State(int32(result)) + if state < 0 { + return 0, core.statusError(ctx, "query EasyTier state", int32(state)) + } + return state, nil +} + +func (core *Core) NextDeadline(ctx context.Context) (int64, error) { + result, err := core.callCached( + ctx, + "easytier_instance_next_deadline_millis", + &core.deadlineFunction, + core.handle, + ) + if err != nil { + return 0, err + } + deadline := int64(result) + if deadline < 0 { + return 0, core.statusError(ctx, "query EasyTier deadline", int32(deadline)) + } + return deadline, nil +} + +func (core *Core) SendPacket(ctx context.Context, packet []byte) (err error) { + if len(packet) == 0 { + return fmt.Errorf("send empty packet to EasyTier instance") + } + pointer, err := core.ensurePacketBuffer(ctx, uint32(len(packet))) + if err != nil { + return err + } + if !core.module.Memory().Write(pointer, packet) { + return fmt.Errorf("write packet to guest memory") + } + result, err := core.callOne( + ctx, + "easytier_instance_send_packet", + core.handle, + uint64(pointer), + uint64(len(packet)), + ) + if err != nil { + return err + } + if status := int32(result); status != 0 { + return core.statusError(ctx, "send packet to EasyTier instance", status) + } + return nil +} + +func (core *Core) Drop(ctx context.Context) error { + if core.dropped || core.handle == 0 { + return nil + } + if err := core.callStatus(ctx, "easytier_instance_drop"); err != nil { + return err + } + core.dropped = true + core.handle = 0 + var releaseErr error + if core.packetBuffer != 0 { + err := core.free(ctx, core.packetBuffer) + core.packetBuffer = 0 + core.packetBufferCapacity = 0 + if err != nil { + releaseErr = errors.Join( + releaseErr, + fmt.Errorf("release EasyTier packet buffer: %w", err), + ) + } + } + if core.dataPlaneInput != 0 { + err := core.free(ctx, core.dataPlaneInput) + core.dataPlaneInput = 0 + core.dataPlaneInputSize = 0 + if err != nil { + releaseErr = errors.Join( + releaseErr, + fmt.Errorf("release EasyTier data plane input buffer: %w", err), + ) + } + } + if core.dataPlaneAddress != 0 { + err := core.free(ctx, core.dataPlaneAddress) + core.dataPlaneAddress = 0 + if err != nil { + releaseErr = errors.Join( + releaseErr, + fmt.Errorf("release EasyTier data plane address buffer: %w", err), + ) + } + } + if core.dataPlaneOutput != 0 { + err := core.free(ctx, core.dataPlaneOutput) + core.dataPlaneOutput = 0 + core.dataPlaneOutputSize = 0 + if err != nil { + releaseErr = errors.Join( + releaseErr, + fmt.Errorf("release EasyTier data plane output buffer: %w", err), + ) + } + } + return releaseErr +} + +func (core *Core) callStatus(ctx context.Context, name string) error { + if core.dropped || core.handle == 0 { + return fmt.Errorf("%s on unavailable EasyTier instance", name) + } + result, err := core.callOne(ctx, name, core.handle) + if err != nil { + return err + } + if status := int32(result); status != 0 { + return core.statusError(ctx, name, status) + } + return nil +} + +func (core *Core) statusError( + ctx context.Context, + operation string, + status int32, +) error { + message, err := core.errorMessage(ctx, core.handle) + if err != nil { + return fmt.Errorf("%s: status=%d: %w", operation, status, err) + } + return fmt.Errorf("%s: status=%d: %s", operation, status, message) +} + +func (core *Core) errorMessage( + ctx context.Context, + handle uint64, +) (message string, err error) { + length, err := core.callOne(ctx, "easytier_instance_error_len", handle) + if err != nil { + return "", err + } + if length == 0 { + return "", nil + } + pointer, err := core.allocate(ctx, uint32(length)) + if err != nil { + return "", err + } + defer core.cleanupBuffer(ctx, pointer, &err) + result, err := core.callOne( + ctx, + "easytier_instance_error_copy", + handle, + uint64(pointer), + length, + ) + if err != nil { + return "", err + } + if status := int32(result); status < 0 { + return "", fmt.Errorf("copy EasyTier error: status=%d", status) + } + encoded, ok := core.module.Memory().Read(pointer, uint32(length)) + if !ok { + return "", fmt.Errorf("read EasyTier error from guest memory") + } + return string(encoded), nil +} + +func (core *Core) allocate(ctx context.Context, length uint32) (uint32, error) { + result, err := core.callOne(ctx, "easytier_buffer_alloc", uint64(length)) + if err != nil { + return 0, err + } + if result == 0 { + return 0, fmt.Errorf("allocate %d guest bytes", length) + } + return uint32(result), nil +} + +func (core *Core) free(ctx context.Context, pointer uint32) error { + result, err := core.callOne(ctx, "easytier_buffer_free", uint64(pointer)) + if err != nil { + return err + } + if status := int32(result); status != 0 { + return fmt.Errorf("free guest buffer: status=%d", status) + } + return nil +} + +func (core *Core) ensurePacketBuffer( + ctx context.Context, + length uint32, +) (uint32, error) { + if core.packetBufferCapacity >= length { + return core.packetBuffer, nil + } + if core.packetBuffer != 0 { + if err := core.free(ctx, core.packetBuffer); err != nil { + return 0, fmt.Errorf("grow EasyTier packet buffer: %w", err) + } + core.packetBuffer = 0 + core.packetBufferCapacity = 0 + } + pointer, err := core.allocate(ctx, length) + if err != nil { + return 0, err + } + core.packetBuffer = pointer + core.packetBufferCapacity = length + return pointer, nil +} + +func (core *Core) ensureDataPlaneInput( + ctx context.Context, + length uint32, +) (uint32, error) { + if core.dataPlaneInputSize >= length { + return core.dataPlaneInput, nil + } + if core.dataPlaneInput != 0 { + if err := core.free(ctx, core.dataPlaneInput); err != nil { + return 0, fmt.Errorf("grow EasyTier data plane input buffer: %w", err) + } + core.dataPlaneInput = 0 + core.dataPlaneInputSize = 0 + } + pointer, err := core.allocate(ctx, length) + if err != nil { + return 0, err + } + core.dataPlaneInput = pointer + core.dataPlaneInputSize = length + return pointer, nil +} + +func (core *Core) ensureDataPlaneAddress(ctx context.Context) (uint32, error) { + if core.dataPlaneAddress != 0 { + return core.dataPlaneAddress, nil + } + pointer, err := core.allocate(ctx, socketAddressLength) + if err != nil { + return 0, err + } + core.dataPlaneAddress = pointer + return pointer, nil +} + +func (core *Core) ensureDataPlaneOutput( + ctx context.Context, + length uint32, +) (uint32, error) { + if core.dataPlaneOutputSize >= length { + return core.dataPlaneOutput, nil + } + if core.dataPlaneOutput != 0 { + if err := core.free(ctx, core.dataPlaneOutput); err != nil { + return 0, fmt.Errorf("grow EasyTier data plane output buffer: %w", err) + } + core.dataPlaneOutput = 0 + core.dataPlaneOutputSize = 0 + } + pointer, err := core.allocate(ctx, length) + if err != nil { + return 0, err + } + core.dataPlaneOutput = pointer + core.dataPlaneOutputSize = length + return pointer, nil +} + +func (core *Core) cleanupBuffer( + ctx context.Context, + pointer uint32, + operationErr *error, +) { + cleanupErr := core.free(contextutil.WithoutCancel(ctx), pointer) + if *operationErr == nil && cleanupErr != nil { + *operationErr = cleanupErr + } +} + +func (core *Core) callOne( + ctx context.Context, + name string, + params ...uint64, +) (uint64, error) { + function, err := core.resolveFunction(name) + if err != nil { + return 0, err + } + return callGuestFunction(ctx, name, function, params) +} + +func (core *Core) callCached( + ctx context.Context, + name string, + cached *guestFunction, + params ...uint64, +) (uint64, error) { + if cached.call == nil { + function, err := core.resolveFunction(name) + if err != nil { + return 0, err + } + *cached = function + } + return callGuestFunction(ctx, name, *cached, params) +} + +func (core *Core) resolveFunction(name string) (guestFunction, error) { + function, exists := core.functions[name] + if !exists { + call := core.module.ExportedFunction(name) + if call == nil { + return guestFunction{}, fmt.Errorf( + "EasyTier guest does not export %s", + name, + ) + } + definition := call.Definition() + if count := len(definition.ResultTypes()); count != 1 { + return guestFunction{}, fmt.Errorf( + "%s returned %d values", + name, + count, + ) + } + function = guestFunction{ + call: call, + parameterCount: len(definition.ParamTypes()), + } + core.functions[name] = function + } + return function, nil +} + +func callGuestFunction( + ctx context.Context, + name string, + function guestFunction, + params []uint64, +) (uint64, error) { + if len(params) != function.parameterCount { + return 0, fmt.Errorf( + "%s expected %d params, but passed %d", + name, + function.parameterCount, + len(params), + ) + } + + stack := params + if len(stack) == 0 { + stack = make([]uint64, 1) + } + if err := function.call.CallWithStack(ctx, stack); err != nil { + return 0, fmt.Errorf("%s: %w", name, err) + } + return stack[0], nil +} diff --git a/easytier-go/internal/coreabi/core_test.go b/easytier-go/internal/coreabi/core_test.go new file mode 100644 index 00000000..169d8f0a --- /dev/null +++ b/easytier-go/internal/coreabi/core_test.go @@ -0,0 +1,62 @@ +package coreabi + +import ( + "context" + "testing" + + "github.com/metacubex/wazero/api" + "github.com/metacubex/wazero/experimental/wazerotest" +) + +func TestCallOneCachesFunction(t *testing.T) { + callCount := 0 + increment := wazerotest.NewFunction(func( + _ context.Context, + _ api.Module, + value uint64, + ) uint64 { + callCount++ + return value + 1 + }) + increment.ExportNames = []string{"increment"} + module := wazerotest.NewModule(nil, increment) + core := &Core{ + module: module, + functions: make(map[string]guestFunction), + } + + for i := uint64(0); i < 2; i++ { + result, err := core.callOne(context.Background(), "increment", i) + if err != nil { + t.Fatalf("call increment: %v", err) + } + if result != i+1 { + t.Fatalf("increment result = %d, want %d", result, i+1) + } + } + if callCount != 2 { + t.Fatalf("call count = %d, want 2", callCount) + } + if len(core.functions) != 1 { + t.Fatalf("cached functions = %d, want 1", len(core.functions)) + } +} + +func TestCallOneValidatesSignature(t *testing.T) { + noResult := wazerotest.NewFunction(func( + _ context.Context, + _ api.Module, + ) { + }) + noResult.ExportNames = []string{"no_result"} + module := wazerotest.NewModule(nil, noResult) + core := &Core{ + module: module, + functions: make(map[string]guestFunction), + } + + _, err := core.callOne(context.Background(), "no_result") + if err == nil { + t.Fatal("call function without result: expected error") + } +} diff --git a/easytier-go/internal/coreabi/dataplane.go b/easytier-go/internal/coreabi/dataplane.go new file mode 100644 index 00000000..fc4fb3e6 --- /dev/null +++ b/easytier-go/internal/coreabi/dataplane.go @@ -0,0 +1,715 @@ +package coreabi + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "math" + "net/netip" + + "github.com/EasyTier/EasyTier/easytier-go/internal/contextutil" +) + +const ( + DataPlaneABIVersion = 3 + + DataPlaneCapability uint64 = 1 << 0 + DataPlaneTCPCapability uint64 = 1 << 1 + DataPlaneUDPCapability uint64 = 1 << 2 + + DeadlineRead DeadlineDirection = 1 << 0 + DeadlineWrite DeadlineDirection = 1 << 1 + + requiredDataPlaneCapabilities = DataPlaneCapability | + DataPlaneTCPCapability | + DataPlaneUDPCapability +) + +type OperationID uint64 +type ResourceID uint64 +type DeadlineDirection uint32 + +type OperationKind uint16 + +const ( + OperationTCPConnect OperationKind = iota + 1 + OperationTCPBind + OperationTCPAccept + OperationTCPRead + OperationTCPWrite + OperationUDPBind + OperationUDPReceive + OperationUDPSend +) + +func (kind OperationKind) valid() bool { + return kind >= OperationTCPConnect && kind <= OperationUDPSend +} + +type ErrorKind uint16 + +const ( + ErrorNone ErrorKind = iota + ErrorCancelled + ErrorDeadlineExceeded + ErrorInstanceStopped + ErrorHandleClosed + ErrorNoOverlayRoute + ErrorPathNotReady + ErrorAddressFamilyUnsupported + ErrorAddressInUse + ErrorConnectionRefused + ErrorNetworkChanged + ErrorResourceLimit + ErrorIO + ErrorBufferTooSmall +) + +func (kind ErrorKind) validCompletionStatus() bool { + return kind <= ErrorBufferTooSmall +} + +type DataPlaneError struct { + Kind ErrorKind + Message string +} + +func (err *DataPlaneError) Error() string { + if err.Message == "" { + return fmt.Sprintf("data plane error %d", err.Kind) + } + return err.Message +} + +type Completion struct { + Operation OperationID + Kind OperationKind + Status ErrorKind +} + +type OperationResult struct { + Kind OperationKind + Resource ResourceID + Local netip.AddrPort + Peer netip.AddrPort + Data []byte + Length int + EOF bool + Truncated bool +} + +func (core *Core) validateDataPlaneABI(ctx context.Context) error { + version, err := core.callOne(ctx, "easytier_data_plane_abi_version") + if err != nil { + return err + } + if version != DataPlaneABIVersion { + return fmt.Errorf( + "unsupported EasyTier data plane ABI version %d, want %d", + version, + DataPlaneABIVersion, + ) + } + capabilities, err := core.callOne(ctx, "easytier_data_plane_capabilities") + if err != nil { + return err + } + if missing := requiredDataPlaneCapabilities &^ capabilities; missing != 0 { + return fmt.Errorf( + "EasyTier data plane capabilities %#x are missing %#x", + capabilities, + missing, + ) + } + return nil +} + +func (core *Core) SubmitTCPConnect( + ctx context.Context, + peer netip.AddrPort, + timeoutMillis uint64, +) (OperationID, error) { + wire, err := encodeSocketAddress(peer) + if err != nil { + return 0, err + } + return core.submitWithInput( + ctx, + "easytier_data_plane_tcp_connect_submit", + wire[:], + timeoutMillis, + ) +} + +func (core *Core) SubmitTCPBind( + ctx context.Context, + port uint16, + timeoutMillis uint64, +) (OperationID, error) { + return core.submit( + ctx, + "easytier_data_plane_tcp_bind_submit", + uint64(port), + timeoutMillis, + ) +} + +func (core *Core) SubmitTCPAccept( + ctx context.Context, + listener ResourceID, + timeoutMillis uint64, +) (OperationID, error) { + return core.submit( + ctx, + "easytier_data_plane_tcp_accept_submit", + uint64(listener), + timeoutMillis, + ) +} + +func (core *Core) SubmitTCPRead( + ctx context.Context, + stream ResourceID, + maximum uint32, +) (OperationID, error) { + return core.submit( + ctx, + "easytier_data_plane_tcp_read_submit", + uint64(stream), + uint64(maximum), + ) +} + +func (core *Core) SubmitTCPWrite( + ctx context.Context, + stream ResourceID, + data []byte, +) (operation OperationID, err error) { + if len(data) > maxDataPlaneTransferBytes { + return 0, fmt.Errorf( + "TCP write length %d exceeds data plane limit %d", + len(data), + maxDataPlaneTransferBytes, + ) + } + pointer, err := core.writeInput(ctx, data) + if err != nil { + return 0, err + } + if pointer != 0 { + defer core.cleanupBuffer(ctx, pointer, &err) + } + return core.submit( + ctx, + "easytier_data_plane_tcp_write_submit", + uint64(stream), + uint64(pointer), + uint64(len(data)), + ) +} + +func (core *Core) SubmitUDPBind( + ctx context.Context, + port uint16, + timeoutMillis uint64, +) (OperationID, error) { + return core.submit( + ctx, + "easytier_data_plane_udp_bind_submit", + uint64(port), + timeoutMillis, + ) +} + +func (core *Core) SubmitUDPReceive( + ctx context.Context, + socket ResourceID, + maximum uint32, +) (OperationID, error) { + return core.submit( + ctx, + "easytier_data_plane_udp_receive_submit", + uint64(socket), + uint64(maximum), + ) +} + +func (core *Core) SubmitUDPSend( + ctx context.Context, + socket ResourceID, + peer netip.AddrPort, + data []byte, +) (operation OperationID, err error) { + if len(data) > maxDataPlaneTransferBytes { + return 0, fmt.Errorf( + "UDP send length %d exceeds data plane limit %d", + len(data), + maxDataPlaneTransferBytes, + ) + } + address, err := encodeSocketAddress(peer) + if err != nil { + return 0, err + } + addressPointer, err := core.ensureDataPlaneAddress(ctx) + if err != nil { + return 0, err + } + if !core.module.Memory().Write(addressPointer, address[:]) { + return 0, fmt.Errorf("write UDP peer address to guest memory") + } + var dataPointer uint32 + if len(data) != 0 { + dataPointer, err = core.ensureDataPlaneInput(ctx, uint32(len(data))) + if err != nil { + return 0, err + } + if !core.module.Memory().Write(dataPointer, data) { + return 0, fmt.Errorf("write UDP payload to guest memory") + } + } + return core.submit( + ctx, + "easytier_data_plane_udp_send_submit", + uint64(socket), + uint64(addressPointer), + uint64(dataPointer), + uint64(len(data)), + ) +} + +func (core *Core) SetResourceDeadline( + ctx context.Context, + resource ResourceID, + direction DeadlineDirection, + timeoutMillis uint64, +) error { + result, err := core.callOne( + ctx, + "easytier_data_plane_resource_deadline_set", + core.handle, + uint64(resource), + uint64(direction), + timeoutMillis, + ) + if err != nil { + return err + } + if status := int32(result); status != 0 { + return core.dataPlaneStatusError( + ctx, + "set data plane resource deadline", + status, + ) + } + return nil +} + +func (core *Core) submitWithInput( + ctx context.Context, + name string, + input []byte, + params ...uint64, +) (operation OperationID, err error) { + pointer, err := core.writeInput(ctx, input) + if err != nil { + return 0, err + } + defer core.cleanupBuffer(ctx, pointer, &err) + return core.submit(ctx, name, append([]uint64{uint64(pointer)}, params...)...) +} + +func (core *Core) submit( + ctx context.Context, + name string, + params ...uint64, +) (operation OperationID, err error) { + pointer, err := core.ensureDataPlaneOutput(ctx, operationIDLength) + if err != nil { + return 0, err + } + callParams := make([]uint64, 0, len(params)+2) + callParams = append(callParams, core.handle) + callParams = append(callParams, params...) + callParams = append(callParams, uint64(pointer)) + result, err := core.callOne(ctx, name, callParams...) + if err != nil { + return 0, err + } + if status := int32(result); status != 0 { + return 0, core.dataPlaneStatusError(ctx, name, status) + } + wire, ok := core.module.Memory().Read(pointer, operationIDLength) + if !ok { + return 0, fmt.Errorf("read operation ID from guest memory") + } + operation = OperationID(binary.BigEndian.Uint64(wire)) + if operation == 0 { + return 0, fmt.Errorf("%s returned zero operation ID", name) + } + return operation, nil +} + +func (core *Core) DrainCompletions( + ctx context.Context, + maximum uint32, +) (completions []Completion, err error) { + if maximum == 0 { + return nil, nil + } + if uint64(maximum)*completionLength > math.MaxUint32 { + return nil, fmt.Errorf("completion batch %d exceeds guest address space", maximum) + } + length := maximum * completionLength + pointer, err := core.ensureDataPlaneOutput(ctx, length) + if err != nil { + return nil, err + } + result, err := core.callCached( + ctx, + "easytier_data_plane_completion_drain", + &core.completionFunction, + core.handle, + uint64(pointer), + uint64(maximum), + ) + if err != nil { + return nil, err + } + count := int32(result) + if count < 0 { + return nil, core.dataPlaneStatusError( + ctx, + "drain data plane completions", + count, + ) + } + if uint32(count) > maximum { + return nil, fmt.Errorf( + "data plane drained %d completions into capacity %d", + count, + maximum, + ) + } + wire, ok := core.module.Memory().Read( + pointer, + uint32(count)*completionLength, + ) + if !ok { + return nil, fmt.Errorf("read data plane completions from guest memory") + } + return decodeCompletions(wire) +} + +func (core *Core) TakeResult( + ctx context.Context, + completion Completion, +) (OperationResult, error) { + result := OperationResult{Kind: completion.Kind} + var err error + switch completion.Kind { + case OperationTCPConnect, OperationTCPAccept: + result.Resource, result.Local, result.Peer, err = + core.takeStreamResult(ctx, completion) + case OperationTCPBind, OperationUDPBind: + result.Resource, result.Local, err = + core.takeBindResult(ctx, completion) + case OperationTCPRead: + result.Data, result.EOF, err = core.takeTCPReadResult(ctx, completion) + case OperationTCPWrite: + result.Length, err = core.takeLengthResult( + ctx, + completion, + "easytier_data_plane_tcp_write_result_take", + ) + case OperationUDPReceive: + result.Data, result.Peer, result.Truncated, err = + core.takeUDPReceiveResult(ctx, completion) + case OperationUDPSend: + result.Length, err = core.takeLengthResult( + ctx, + completion, + "easytier_data_plane_udp_send_result_take", + ) + default: + return OperationResult{}, fmt.Errorf( + "take unknown data plane operation kind %d", + completion.Kind, + ) + } + return result, err +} + +func (core *Core) takeStreamResult( + ctx context.Context, + completion Completion, +) (resource ResourceID, local, peer netip.AddrPort, err error) { + name := "easytier_data_plane_tcp_connect_result_take" + if completion.Kind == OperationTCPAccept { + name = "easytier_data_plane_tcp_accept_result_take" + } + wire, err := core.takeFixedResult(ctx, completion, name, streamResultLength) + if err != nil { + return 0, netip.AddrPort{}, netip.AddrPort{}, err + } + return decodeStreamResult(wire) +} + +func (core *Core) takeBindResult( + ctx context.Context, + completion Completion, +) (resource ResourceID, local netip.AddrPort, err error) { + name := "easytier_data_plane_tcp_bind_result_take" + if completion.Kind == OperationUDPBind { + name = "easytier_data_plane_udp_bind_result_take" + } + wire, err := core.takeFixedResult(ctx, completion, name, bindResultLength) + if err != nil { + return 0, netip.AddrPort{}, err + } + return decodeBindResult(wire) +} + +func (core *Core) takeFixedResult( + ctx context.Context, + completion Completion, + name string, + length uint32, +) (wire []byte, err error) { + pointer, err := core.ensureDataPlaneOutput(ctx, length) + if err != nil { + return nil, err + } + result, err := core.callOne( + ctx, + name, + core.handle, + uint64(completion.Operation), + uint64(pointer), + ) + if err != nil { + return nil, err + } + if status := int32(result); status != 0 { + return nil, core.dataPlaneStatusError(ctx, name, status) + } + bytes, ok := core.module.Memory().Read(pointer, length) + if !ok { + return nil, fmt.Errorf("read %s from guest memory", name) + } + return append([]byte(nil), bytes...), nil +} + +func (core *Core) takeTCPReadResult( + ctx context.Context, + completion Completion, +) (data []byte, eof bool, err error) { + data, metadata, err := core.takeVariableResult( + ctx, + completion, + "easytier_data_plane_tcp_read_result_take", + tcpReadMetadataLength, + ) + if err != nil { + return nil, false, err + } + if metadata[0] > 1 { + return nil, false, fmt.Errorf( + "data plane returned invalid TCP EOF flag %d", + metadata[0], + ) + } + return data, metadata[0] == 1, nil +} + +func (core *Core) takeUDPReceiveResult( + ctx context.Context, + completion Completion, +) (data []byte, peer netip.AddrPort, truncated bool, err error) { + data, metadata, err := core.takeVariableResult( + ctx, + completion, + "easytier_data_plane_udp_receive_result_take", + udpReceiveMetadataLength, + ) + if err != nil { + return nil, netip.AddrPort{}, false, err + } + peer, err = decodeSocketAddress(metadata[:socketAddressLength]) + if err != nil { + return nil, netip.AddrPort{}, false, err + } + if metadata[socketAddressLength] > 1 { + return nil, netip.AddrPort{}, false, fmt.Errorf( + "data plane returned invalid UDP truncation flag %d", + metadata[socketAddressLength], + ) + } + return data, peer, metadata[socketAddressLength] == 1, nil +} + +func (core *Core) takeVariableResult( + ctx context.Context, + completion Completion, + name string, + metadataLength uint32, +) (data, metadata []byte, err error) { + capacity := uint32(maxDataPlaneTransferBytes) + if completion.Kind == OperationUDPReceive { + capacity = math.MaxUint16 + } + dataPointer, err := core.ensureDataPlaneInput(ctx, capacity) + if err != nil { + return nil, nil, err + } + metadataPointer, err := core.ensureDataPlaneOutput(ctx, metadataLength) + if err != nil { + return nil, nil, err + } + result, err := core.callOne( + ctx, + name, + core.handle, + uint64(completion.Operation), + uint64(dataPointer), + uint64(capacity), + uint64(metadataPointer), + ) + if err != nil { + return nil, nil, err + } + length := int32(result) + if length < 0 { + return nil, nil, core.dataPlaneStatusError(ctx, name, length) + } + if uint32(length) > capacity { + return nil, nil, fmt.Errorf( + "%s returned length %d into capacity %d", + name, + length, + capacity, + ) + } + if length != 0 { + bytes, ok := core.module.Memory().Read(dataPointer, uint32(length)) + if !ok { + return nil, nil, fmt.Errorf("read %s payload from guest memory", name) + } + data = append([]byte(nil), bytes...) + } + bytes, ok := core.module.Memory().Read(metadataPointer, metadataLength) + if !ok { + return nil, nil, fmt.Errorf("read %s metadata from guest memory", name) + } + return data, append([]byte(nil), bytes...), nil +} + +func (core *Core) takeLengthResult( + ctx context.Context, + completion Completion, + name string, +) (int, error) { + result, err := core.callOne( + ctx, + name, + core.handle, + uint64(completion.Operation), + ) + if err != nil { + return 0, err + } + length := int32(result) + if length < 0 { + return 0, core.dataPlaneStatusError(ctx, name, length) + } + return int(length), nil +} + +func (core *Core) CancelOperation( + ctx context.Context, + operation OperationID, +) error { + return core.dataPlaneCallStatus( + ctx, + "easytier_data_plane_operation_cancel", + uint64(operation), + ) +} + +func (core *Core) FreeOperation( + ctx context.Context, + operation OperationID, +) error { + return core.dataPlaneCallStatus( + ctx, + "easytier_data_plane_operation_free", + uint64(operation), + ) +} + +func (core *Core) CloseResource(ctx context.Context, resource ResourceID) error { + return core.dataPlaneCallStatus( + ctx, + "easytier_data_plane_resource_close", + uint64(resource), + ) +} + +func (core *Core) dataPlaneCallStatus( + ctx context.Context, + name string, + params ...uint64, +) error { + callParams := append([]uint64{core.handle}, params...) + result, err := core.callOne(ctx, name, callParams...) + if err != nil { + return err + } + if status := int32(result); status != 0 { + return core.dataPlaneStatusError(ctx, name, status) + } + return nil +} + +func (core *Core) dataPlaneStatusError( + ctx context.Context, + operation string, + status int32, +) error { + if status >= 0 || status < -int32(ErrorBufferTooSmall) { + return core.statusError(ctx, operation, status) + } + message, err := core.errorMessage(ctx, core.handle) + if err != nil { + return errors.Join( + &DataPlaneError{Kind: ErrorKind(-status)}, + fmt.Errorf("%s: %w", operation, err), + ) + } + return &DataPlaneError{ + Kind: ErrorKind(-status), + Message: message, + } +} + +func (core *Core) writeInput( + ctx context.Context, + data []byte, +) (uint32, error) { + if len(data) == 0 { + return 0, nil + } + if uint64(len(data)) > math.MaxUint32 { + return 0, fmt.Errorf("guest input length %d exceeds wasm32", len(data)) + } + pointer, err := core.allocate(ctx, uint32(len(data))) + if err != nil { + return 0, err + } + if !core.module.Memory().Write(pointer, data) { + _ = core.free(contextutil.WithoutCancel(ctx), pointer) + return 0, fmt.Errorf("write data plane input to guest memory") + } + return pointer, nil +} diff --git a/easytier-go/internal/coreabi/dataplane_wire.go b/easytier-go/internal/coreabi/dataplane_wire.go new file mode 100644 index 00000000..147df0d4 --- /dev/null +++ b/easytier-go/internal/coreabi/dataplane_wire.go @@ -0,0 +1,129 @@ +package coreabi + +import ( + "encoding/binary" + "fmt" + "net/netip" +) + +const ( + socketAddressLength = 27 + operationIDLength = 8 + completionLength = 12 + streamResultLength = 8 + socketAddressLength*2 + bindResultLength = 8 + socketAddressLength + tcpReadMetadataLength = 1 + udpReceiveMetadataLength = socketAddressLength + 1 + maxDataPlaneTransferBytes = 1024 * 1024 +) + +func encodeSocketAddress(address netip.AddrPort) ([socketAddressLength]byte, error) { + var wire [socketAddressLength]byte + if !address.IsValid() || !address.Addr().Is4() { + return wire, fmt.Errorf("data plane ABI v2 requires an IPv4 address") + } + wire[0] = 4 + ip := address.Addr().As4() + copy(wire[1:5], ip[:]) + binary.BigEndian.PutUint16(wire[17:19], address.Port()) + return wire, nil +} + +func decodeSocketAddress(wire []byte) (netip.AddrPort, error) { + if len(wire) != socketAddressLength { + return netip.AddrPort{}, fmt.Errorf( + "decode socket address with length %d", len(wire), + ) + } + if wire[0] != 4 { + return netip.AddrPort{}, fmt.Errorf( + "data plane ABI v2 returned address family %d", wire[0], + ) + } + for _, value := range wire[5:17] { + if value != 0 { + return netip.AddrPort{}, fmt.Errorf( + "data plane ABI returned noncanonical IPv4 padding", + ) + } + } + for _, value := range wire[19:27] { + if value != 0 { + return netip.AddrPort{}, fmt.Errorf( + "data plane ABI returned IPv4 flow or scope metadata", + ) + } + } + address := netip.AddrFrom4([4]byte(wire[1:5])) + return netip.AddrPortFrom(address, binary.BigEndian.Uint16(wire[17:19])), nil +} + +func decodeCompletions(wire []byte) ([]Completion, error) { + if len(wire)%completionLength != 0 { + return nil, fmt.Errorf("decode completion bytes with length %d", len(wire)) + } + completions := make([]Completion, 0, len(wire)/completionLength) + for len(wire) != 0 { + operation := OperationID(binary.BigEndian.Uint64(wire[:8])) + kind := OperationKind(binary.BigEndian.Uint16(wire[8:10])) + status := ErrorKind(binary.BigEndian.Uint16(wire[10:12])) + if operation == 0 { + return nil, fmt.Errorf("data plane returned zero operation ID") + } + if !kind.valid() { + return nil, fmt.Errorf("data plane returned operation kind %d", kind) + } + if !status.validCompletionStatus() { + return nil, fmt.Errorf("data plane returned completion status %d", status) + } + completions = append(completions, Completion{ + Operation: operation, + Kind: kind, + Status: status, + }) + wire = wire[completionLength:] + } + return completions, nil +} + +func decodeStreamResult(wire []byte) (ResourceID, netip.AddrPort, netip.AddrPort, error) { + if len(wire) != streamResultLength { + return 0, netip.AddrPort{}, netip.AddrPort{}, fmt.Errorf( + "decode stream result with length %d", len(wire), + ) + } + resource := ResourceID(binary.BigEndian.Uint64(wire[:8])) + if resource == 0 { + return 0, netip.AddrPort{}, netip.AddrPort{}, fmt.Errorf( + "data plane returned zero stream resource ID", + ) + } + local, err := decodeSocketAddress(wire[8 : 8+socketAddressLength]) + if err != nil { + return 0, netip.AddrPort{}, netip.AddrPort{}, err + } + peer, err := decodeSocketAddress(wire[8+socketAddressLength:]) + if err != nil { + return 0, netip.AddrPort{}, netip.AddrPort{}, err + } + return resource, local, peer, nil +} + +func decodeBindResult(wire []byte) (ResourceID, netip.AddrPort, error) { + if len(wire) != bindResultLength { + return 0, netip.AddrPort{}, fmt.Errorf( + "decode bind result with length %d", len(wire), + ) + } + resource := ResourceID(binary.BigEndian.Uint64(wire[:8])) + if resource == 0 { + return 0, netip.AddrPort{}, fmt.Errorf( + "data plane returned zero bound resource ID", + ) + } + address, err := decodeSocketAddress(wire[8:]) + if err != nil { + return 0, netip.AddrPort{}, err + } + return resource, address, nil +} diff --git a/easytier-go/internal/coreabi/dataplane_wire_test.go b/easytier-go/internal/coreabi/dataplane_wire_test.go new file mode 100644 index 00000000..943a857c --- /dev/null +++ b/easytier-go/internal/coreabi/dataplane_wire_test.go @@ -0,0 +1,73 @@ +package coreabi + +import ( + "encoding/binary" + "net/netip" + "reflect" + "testing" +) + +func TestSocketAddressWireMatchesEasyTierABI(t *testing.T) { + address := netip.MustParseAddrPort("192.0.2.1:11013") + wire, err := encodeSocketAddress(address) + if err != nil { + t.Fatalf("encode address: %v", err) + } + want := [socketAddressLength]byte{ + 4, 192, 0, 2, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x2b, 0x05, + 0, 0, 0, 0, + 0, 0, 0, 0, + } + if wire != want { + t.Fatalf("encoded address = %v, want %v", wire, want) + } + decoded, err := decodeSocketAddress(wire[:]) + if err != nil { + t.Fatalf("decode address: %v", err) + } + if decoded != address { + t.Fatalf("decoded address = %v, want %v", decoded, address) + } +} + +func TestSocketAddressWireRejectsIPv6AndNoncanonicalIPv4(t *testing.T) { + if _, err := encodeSocketAddress( + netip.MustParseAddrPort("[2001:db8::1]:80"), + ); err == nil { + t.Fatal("encode IPv6 address succeeded") + } + wire, err := encodeSocketAddress(netip.MustParseAddrPort("192.0.2.1:80")) + if err != nil { + t.Fatalf("encode IPv4 address: %v", err) + } + wire[5] = 1 + if _, err := decodeSocketAddress(wire[:]); err == nil { + t.Fatal("decode noncanonical IPv4 address succeeded") + } +} + +func TestCompletionWireMatchesEasyTierABI(t *testing.T) { + wire := make([]byte, completionLength*2) + binary.BigEndian.PutUint64(wire[:8], 0x0102_0304_0506_0708) + binary.BigEndian.PutUint16(wire[8:10], uint16(OperationUDPReceive)) + binary.BigEndian.PutUint16(wire[10:12], uint16(ErrorBufferTooSmall)) + binary.BigEndian.PutUint64(wire[12:20], 9) + binary.BigEndian.PutUint16(wire[20:22], uint16(OperationTCPWrite)) + completions, err := decodeCompletions(wire) + if err != nil { + t.Fatalf("decode completions: %v", err) + } + want := []Completion{ + { + Operation: 0x0102_0304_0506_0708, + Kind: OperationUDPReceive, + Status: ErrorBufferTooSmall, + }, + {Operation: 9, Kind: OperationTCPWrite, Status: ErrorNone}, + } + if !reflect.DeepEqual(completions, want) { + t.Fatalf("completions = %#v, want %#v", completions, want) + } +} diff --git a/easytier-go/internal/coreabi/rpc.go b/easytier-go/internal/coreabi/rpc.go new file mode 100644 index 00000000..8e3eab5c --- /dev/null +++ b/easytier-go/internal/coreabi/rpc.go @@ -0,0 +1,190 @@ +package coreabi + +import ( + "context" + "encoding/binary" + "fmt" +) + +const ( + RPCABIVersion = 2 + + rpcStatusPending int32 = -6 + maxRPCMessageBytes = 16 * 1024 * 1024 + rpcOperationIDBytes = 8 +) + +type RPCOperationID uint64 + +func (core *Core) validateRPCABI(ctx context.Context) error { + version, err := core.callOne(ctx, "easytier_rpc_abi_version") + if err != nil { + return err + } + if version != RPCABIVersion { + return fmt.Errorf( + "unsupported EasyTier RPC ABI version %d, want %d", + version, + RPCABIVersion, + ) + } + return nil +} + +func (core *Core) SubmitRPC( + ctx context.Context, + encodedRequest []byte, +) (operation RPCOperationID, err error) { + if len(encodedRequest) == 0 { + return 0, fmt.Errorf("submit empty RPC request") + } + if len(encodedRequest) > maxRPCMessageBytes { + return 0, fmt.Errorf( + "RPC request length %d exceeds limit %d", + len(encodedRequest), + maxRPCMessageBytes, + ) + } + requestPointer, err := core.allocate(ctx, uint32(len(encodedRequest))) + if err != nil { + return 0, err + } + defer core.cleanupBuffer(ctx, requestPointer, &err) + if !core.module.Memory().Write(requestPointer, encodedRequest) { + return 0, fmt.Errorf("write RPC request to guest memory") + } + operationPointer, err := core.allocate(ctx, rpcOperationIDBytes) + if err != nil { + return 0, err + } + defer core.cleanupBuffer(ctx, operationPointer, &err) + + result, err := core.callOne( + ctx, + "easytier_rpc_request_submit", + core.handle, + uint64(requestPointer), + uint64(len(encodedRequest)), + uint64(operationPointer), + ) + if err != nil { + return 0, err + } + if status := int32(result); status != 0 { + return 0, core.statusError(ctx, "submit EasyTier RPC request", status) + } + wire, ok := core.module.Memory().Read( + operationPointer, + rpcOperationIDBytes, + ) + if !ok { + return 0, fmt.Errorf("read RPC operation ID from guest memory") + } + operation = RPCOperationID(binary.BigEndian.Uint64(wire)) + if operation == 0 { + return 0, fmt.Errorf("EasyTier RPC submit returned zero operation ID") + } + return operation, nil +} + +func (core *Core) TakeRPCResponse( + ctx context.Context, + operation RPCOperationID, +) (response []byte, ready bool, err error) { + result, err := core.callOne( + ctx, + "easytier_rpc_response_take", + core.handle, + uint64(operation), + 0, + 0, + ) + if err != nil { + return nil, false, err + } + required := int32(result) + if required == rpcStatusPending { + return nil, false, nil + } + if required < 0 { + return nil, false, core.statusError( + ctx, + "probe EasyTier RPC response", + required, + ) + } + if required > maxRPCMessageBytes { + return nil, false, fmt.Errorf( + "RPC response length %d exceeds limit %d", + required, + maxRPCMessageBytes, + ) + } + + capacity := uint32(required) + if capacity == 0 { + // A non-zero output pointer distinguishes consumption from a size probe. + capacity = 1 + } + outputPointer, err := core.allocate(ctx, capacity) + if err != nil { + return nil, false, err + } + defer core.cleanupBuffer(ctx, outputPointer, &err) + result, err = core.callOne( + ctx, + "easytier_rpc_response_take", + core.handle, + uint64(operation), + uint64(outputPointer), + uint64(capacity), + ) + if err != nil { + return nil, false, err + } + length := int32(result) + if length == rpcStatusPending { + return nil, false, nil + } + if length < 0 { + return nil, false, core.statusError( + ctx, + "take EasyTier RPC response", + length, + ) + } + if uint32(length) > capacity { + return nil, false, fmt.Errorf( + "RPC response grew from %d to %d bytes", + required, + length, + ) + } + if length == 0 { + return []byte{}, true, nil + } + wire, ok := core.module.Memory().Read(outputPointer, uint32(length)) + if !ok { + return nil, false, fmt.Errorf("read RPC response from guest memory") + } + return append([]byte(nil), wire...), true, nil +} + +func (core *Core) FreeRPCOperation( + ctx context.Context, + operation RPCOperationID, +) error { + result, err := core.callOne( + ctx, + "easytier_rpc_operation_free", + core.handle, + uint64(operation), + ) + if err != nil { + return err + } + if status := int32(result); status != 0 { + return core.statusError(ctx, "free EasyTier RPC operation", status) + } + return nil +} diff --git a/easytier-go/internal/coreabi/rpc_test.go b/easytier-go/internal/coreabi/rpc_test.go new file mode 100644 index 00000000..11dad163 --- /dev/null +++ b/easytier-go/internal/coreabi/rpc_test.go @@ -0,0 +1,188 @@ +package coreabi + +import ( + "bytes" + "context" + "encoding/binary" + "testing" + + "github.com/metacubex/wazero/api" + "github.com/metacubex/wazero/experimental/wazerotest" +) + +func TestValidateRPCABI(t *testing.T) { + for _, test := range []struct { + name string + version uint32 + wantErr bool + }{ + {name: "supported", version: RPCABIVersion}, + {name: "unsupported", version: RPCABIVersion + 1, wantErr: true}, + } { + t.Run(test.name, func(t *testing.T) { + version := exportTestFunction( + "easytier_rpc_abi_version", + func(context.Context, api.Module) uint32 { + return test.version + }, + ) + core := &Core{ + module: wazerotest.NewModule(nil, version), + functions: make(map[string]guestFunction), + } + err := core.validateRPCABI(context.Background()) + if (err != nil) != test.wantErr { + t.Fatalf( + "validate RPC ABI version %d: err=%v, wantErr=%v", + test.version, + err, + test.wantErr, + ) + } + }) + } +} + +func TestRPCRequestResponseAndFree(t *testing.T) { + memory := wazerotest.NewMemory(64 * 1024) + nextPointer := uint32(1024) + var submitted []byte + var freed RPCOperationID + response := []byte{0x0a, 0x02, 0x08, 0x01} + + allocate := exportTestFunction( + "easytier_buffer_alloc", + func(_ context.Context, _ api.Module, length uint32) uint32 { + pointer := nextPointer + nextPointer += length + 8 + return pointer + }, + ) + free := exportTestFunction( + "easytier_buffer_free", + func(context.Context, api.Module, uint32) int32 { + return 0 + }, + ) + submit := exportTestFunction( + "easytier_rpc_request_submit", + func( + _ context.Context, + module api.Module, + _ uint64, + requestPointer uint32, + requestLength uint32, + operationPointer uint32, + ) int32 { + wire, _ := module.Memory().Read(requestPointer, requestLength) + submitted = append([]byte(nil), wire...) + var operation [rpcOperationIDBytes]byte + binary.BigEndian.PutUint64(operation[:], 41) + module.Memory().Write(operationPointer, operation[:]) + return 0 + }, + ) + take := exportTestFunction( + "easytier_rpc_response_take", + func( + _ context.Context, + module api.Module, + _ uint64, + _ uint64, + output uint32, + capacity uint32, + ) int32 { + if output == 0 && capacity == 0 { + return int32(len(response)) + } + module.Memory().Write(output, response) + return int32(len(response)) + }, + ) + freeOperation := exportTestFunction( + "easytier_rpc_operation_free", + func( + _ context.Context, + _ api.Module, + _ uint64, + operation uint64, + ) int32 { + freed = RPCOperationID(operation) + return 0 + }, + ) + core := &Core{ + module: wazerotest.NewModule( + memory, + allocate, + free, + submit, + take, + freeOperation, + ), + functions: make(map[string]guestFunction), + handle: 7, + } + + request := []byte{0x0a, 0x01, 0x00} + operation, err := core.SubmitRPC(context.Background(), request) + if err != nil { + t.Fatalf("submit RPC: %v", err) + } + if operation != 41 { + t.Fatalf("RPC operation = %d, want 41", operation) + } + if !bytes.Equal(submitted, request) { + t.Fatalf("submitted RPC = %x, want %x", submitted, request) + } + got, ready, err := core.TakeRPCResponse(context.Background(), operation) + if err != nil { + t.Fatalf("take RPC response: %v", err) + } + if !ready { + t.Fatal("RPC response remained pending") + } + if !bytes.Equal(got, response) { + t.Fatalf("RPC response = %x, want %x", got, response) + } + if err := core.FreeRPCOperation(context.Background(), 52); err != nil { + t.Fatalf("free RPC operation: %v", err) + } + if freed != 52 { + t.Fatalf("freed RPC operation = %d, want 52", freed) + } +} + +func TestTakeRPCResponseReportsPending(t *testing.T) { + take := exportTestFunction( + "easytier_rpc_response_take", + func( + context.Context, + api.Module, + uint64, + uint64, + uint32, + uint32, + ) int32 { + return rpcStatusPending + }, + ) + core := &Core{ + module: wazerotest.NewModule(nil, take), + functions: make(map[string]guestFunction), + handle: 7, + } + response, ready, err := core.TakeRPCResponse(context.Background(), 1) + if err != nil { + t.Fatalf("take pending RPC response: %v", err) + } + if ready || response != nil { + t.Fatalf("pending RPC response = (%x, %v), want (nil, false)", response, ready) + } +} + +func exportTestFunction(name string, implementation any) *wazerotest.Function { + function := wazerotest.NewFunction(implementation) + function.ExportNames = []string{name} + return function +} diff --git a/easytier-go/internal/coreabi/web_client.go b/easytier-go/internal/coreabi/web_client.go new file mode 100644 index 00000000..182f9d5a --- /dev/null +++ b/easytier-go/internal/coreabi/web_client.go @@ -0,0 +1,146 @@ +package coreabi + +import ( + "context" + "fmt" + + "github.com/metacubex/wazero/api" +) + +type WebClient struct { + core *Core + driveFunction guestFunction + deadlineFunction guestFunction + completionFunction guestFunction + created bool + dropped bool +} + +func NewWebClient(module api.Module) (*WebClient, error) { + core, err := New(module) + if err != nil { + return nil, err + } + return &WebClient{core: core}, nil +} + +func (client *WebClient) Create(ctx context.Context, envelope []byte) (err error) { + if client.created || client.dropped { + return fmt.Errorf("create used EasyTier WebClient") + } + pointer, err := client.core.allocate(ctx, uint32(len(envelope))) + if err != nil { + return err + } + defer client.core.cleanupBuffer(ctx, pointer, &err) + if !client.core.module.Memory().Write(pointer, envelope) { + return fmt.Errorf("write EasyTier WebClient config to guest memory") + } + result, err := client.core.callOne( + ctx, + "easytier_web_client_create", + uint64(pointer), + uint64(len(envelope)), + ) + if err != nil { + return err + } + if status := int32(result); status != 0 { + return client.statusError(ctx, "create EasyTier WebClient", status) + } + client.created = true + return nil +} + +func (client *WebClient) Drive(ctx context.Context) error { + result, err := client.core.callCached( + ctx, + "easytier_web_client_drive", + &client.driveFunction, + ) + if err != nil { + return err + } + if status := int32(result); status != 0 { + return client.statusError(ctx, "drive EasyTier WebClient", status) + } + return nil +} + +func (client *WebClient) NotifyCompletions(ctx context.Context) error { + result, err := client.core.callCached( + ctx, + "easytier_web_client_notify_completions", + &client.completionFunction, + ) + if err != nil { + return err + } + if status := int32(result); status != 0 { + return client.statusError(ctx, "notify EasyTier WebClient", status) + } + return nil +} + +func (client *WebClient) NextDeadline(ctx context.Context) (int64, error) { + result, err := client.core.callCached( + ctx, + "easytier_web_client_next_deadline_millis", + &client.deadlineFunction, + ) + if err != nil { + return 0, err + } + deadline := int64(result) + if deadline < 0 { + return 0, client.statusError( + ctx, + "query EasyTier WebClient deadline", + int32(deadline), + ) + } + return deadline, nil +} + +func (client *WebClient) IsConnected(ctx context.Context) (bool, error) { + result, err := client.core.callOne(ctx, "easytier_web_client_is_connected") + if err != nil { + return false, err + } + status := int32(result) + if status < 0 { + return false, client.statusError( + ctx, + "query EasyTier WebClient connection", + status, + ) + } + return status != 0, nil +} + +func (client *WebClient) Drop(ctx context.Context) error { + if !client.created || client.dropped { + return nil + } + result, err := client.core.callOne(ctx, "easytier_web_client_drop") + if err != nil { + return err + } + if status := int32(result); status != 0 { + return client.statusError(ctx, "drop EasyTier WebClient", status) + } + client.dropped = true + return nil +} + +func (client *WebClient) statusError( + ctx context.Context, + operation string, + status int32, +) error { + message, err := client.core.errorMessage(ctx, 0) + if err != nil { + return fmt.Errorf("%s: status=%d: %w", operation, status, err) + } + return fmt.Errorf("%s: status=%d: %s", operation, status, message) +} diff --git a/easytier-go/internal/engine/config.go b/easytier-go/internal/engine/config.go new file mode 100644 index 00000000..61e5a183 --- /dev/null +++ b/easytier-go/internal/engine/config.go @@ -0,0 +1,194 @@ +package engine + +import ( + "encoding/json" + "fmt" + "net/netip" + "net/url" + + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +const createConfigVersion = 14 +const webClientConfigVersion = 1 + +type createEnvelope struct { + Version uint32 `json:"version"` + Config string `json:"config"` + Environment environmentSnapshot `json:"environment"` +} + +type webClientEnvelope struct { + Version uint32 `json:"version"` + Endpoint string `json:"endpoint"` + MachineID string `json:"machine_id"` + Hostname string `json:"hostname"` + SecureMode bool `json:"secure_mode"` + OSType string `json:"os_type"` + Environment environmentSnapshot `json:"environment"` +} + +type environmentSnapshot struct { + PublicIPv4 *string `json:"public_ipv4"` + InterfaceIPv4s []string `json:"interface_ipv4s"` + PublicIPv6 *string `json:"public_ipv6"` + InterfaceIPv6s []string `json:"interface_ipv6s"` + MappedListeners []string `json:"mapped_listeners"` + LocalIPs []string `json:"local_ips"` + ProtectedTCPPorts []uint16 `json:"protected_tcp_ports"` + PreferredIPv6Sources []preferredIPv6Source `json:"preferred_ipv6_sources"` +} + +type preferredIPv6Source struct { + IP string `json:"ip"` + IfIndex uint32 `json:"ifindex"` +} + +func encodeCreateEnvelope( + configTOML string, + snapshot platform.EnvironmentSnapshot, +) ([]byte, error) { + if configTOML == "" { + return nil, fmt.Errorf("EasyTier TOML configuration is empty") + } + environment, err := encodeEnvironmentSnapshot(snapshot) + if err != nil { + return nil, err + } + encoded, err := json.Marshal(createEnvelope{ + Version: createConfigVersion, + Config: configTOML, + Environment: environment, + }) + if err != nil { + return nil, fmt.Errorf("encode EasyTier create envelope: %w", err) + } + return encoded, nil +} + +func encodeWebClientEnvelope( + options WebClientOptions, + snapshot platform.EnvironmentSnapshot, +) ([]byte, error) { + if options.Endpoint == "" { + return nil, fmt.Errorf("EasyTier WebClient endpoint is empty") + } + if options.MachineID == "" { + return nil, fmt.Errorf("EasyTier WebClient machine ID is empty") + } + if options.Hostname == "" { + return nil, fmt.Errorf("EasyTier WebClient hostname is empty") + } + environment, err := encodeEnvironmentSnapshot(snapshot) + if err != nil { + return nil, err + } + encoded, err := json.Marshal(webClientEnvelope{ + Version: webClientConfigVersion, + Endpoint: options.Endpoint, + MachineID: options.MachineID, + Hostname: options.Hostname, + SecureMode: options.SecureMode, + OSType: options.OSType, + Environment: environment, + }) + if err != nil { + return nil, fmt.Errorf("encode EasyTier WebClient envelope: %w", err) + } + return encoded, nil +} + +func encodeEnvironmentSnapshot( + snapshot platform.EnvironmentSnapshot, +) (environmentSnapshot, error) { + encoded := environmentSnapshot{ + InterfaceIPv4s: make([]string, len(snapshot.InterfaceIPv4s)), + InterfaceIPv6s: make([]string, len(snapshot.InterfaceIPv6s)), + MappedListeners: append([]string{}, snapshot.MappedListeners...), + LocalIPs: make([]string, len(snapshot.LocalIPs)), + ProtectedTCPPorts: append([]uint16{}, snapshot.ProtectedTCPPorts...), + PreferredIPv6Sources: make([]preferredIPv6Source, len(snapshot.PreferredIPv6Sources)), + } + var err error + encoded.PublicIPv4, err = encodeOptionalIP("public IPv4", snapshot.PublicIPv4, true) + if err != nil { + return environmentSnapshot{}, err + } + encoded.PublicIPv6, err = encodeOptionalIP("public IPv6", snapshot.PublicIPv6, false) + if err != nil { + return environmentSnapshot{}, err + } + for index, address := range snapshot.InterfaceIPv4s { + if !address.Is4() { + return environmentSnapshot{}, fmt.Errorf( + "interface IPv4 %d is not IPv4: %s", + index, + address, + ) + } + encoded.InterfaceIPv4s[index] = address.String() + } + for index, address := range snapshot.InterfaceIPv6s { + if !isIPv6(address) { + return environmentSnapshot{}, fmt.Errorf( + "interface IPv6 %d is not IPv6: %s", + index, + address, + ) + } + encoded.InterfaceIPv6s[index] = address.String() + } + for index, listener := range encoded.MappedListeners { + parsed, parseErr := url.ParseRequestURI(listener) + if parseErr != nil || parsed.Scheme == "" { + return environmentSnapshot{}, fmt.Errorf( + "mapped listener %d is not an absolute URL: %q", + index, + listener, + ) + } + } + for index, address := range snapshot.LocalIPs { + if !address.IsValid() { + return environmentSnapshot{}, fmt.Errorf("local IP %d is invalid", index) + } + encoded.LocalIPs[index] = address.String() + } + for index, source := range snapshot.PreferredIPv6Sources { + if !isIPv6(source.IP) { + return environmentSnapshot{}, fmt.Errorf( + "preferred IPv6 source %d is not IPv6: %s", + index, + source.IP, + ) + } + encoded.PreferredIPv6Sources[index] = preferredIPv6Source{ + IP: source.IP.String(), + IfIndex: source.IfIndex, + } + } + return encoded, nil +} + +func encodeOptionalIP( + name string, + address *netip.Addr, + ipv4 bool, +) (*string, error) { + if address == nil { + return nil, nil + } + valid := address.Is4() + if !ipv4 { + valid = isIPv6(*address) + } + if !valid { + return nil, fmt.Errorf("%s has wrong address family: %s", name, address) + } + encoded := address.String() + return &encoded, nil +} + +func isIPv6(address netip.Addr) bool { + return address.Is6() && !address.Is4In6() +} diff --git a/easytier-go/internal/engine/config_test.go b/easytier-go/internal/engine/config_test.go new file mode 100644 index 00000000..434f1f65 --- /dev/null +++ b/easytier-go/internal/engine/config_test.go @@ -0,0 +1,56 @@ +package engine + +import ( + "encoding/json" + "net/netip" + "testing" + + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +func TestEncodeCreateEnvelopeOwnsVersionAndEnvironmentSchema(t *testing.T) { + ipv4 := netip.MustParseAddr("198.51.100.4") + ipv6 := netip.MustParseAddr("2001:db8::4") + encoded, err := encodeCreateEnvelope("instance_name = \"test\"\n", platform.EnvironmentSnapshot{ + PublicIPv4: &ipv4, + InterfaceIPv6s: []netip.Addr{ipv6}, + MappedListeners: []string{"tcp://127.0.0.1:11010"}, + PreferredIPv6Sources: []platform.PreferredIPv6Source{{ + IP: ipv6, + IfIndex: 7, + }}, + }) + if err != nil { + t.Fatalf("encode create envelope: %v", err) + } + var document map[string]any + if err := json.Unmarshal(encoded, &document); err != nil { + t.Fatalf("decode create envelope: %v", err) + } + if document["version"] != float64(createConfigVersion) { + t.Fatalf("version = %v, want %d", document["version"], createConfigVersion) + } + environment := document["environment"].(map[string]any) + if environment["public_ipv4"] != ipv4.String() { + t.Fatalf("public_ipv4 = %v", environment["public_ipv4"]) + } + if environment["public_ipv6"] != nil { + t.Fatalf("public_ipv6 = %v, want nil", environment["public_ipv6"]) + } + if got := environment["preferred_ipv6_sources"].([]any)[0].(map[string]any)["ifindex"]; got != float64(7) { + t.Fatalf("preferred IPv6 ifindex = %v", got) + } + if environment["interface_ipv4s"] == nil || environment["local_ips"] == nil { + t.Fatal("empty environment collections encoded as null") + } +} + +func TestEncodeCreateEnvelopeRejectsWrongAddressFamily(t *testing.T) { + ipv6 := netip.MustParseAddr("2001:db8::1") + _, err := encodeCreateEnvelope("instance_name = \"test\"\n", platform.EnvironmentSnapshot{ + PublicIPv4: &ipv6, + }) + if err == nil { + t.Fatal("accepted IPv6 public address as public IPv4") + } +} diff --git a/easytier-go/internal/engine/conn.go b/easytier-go/internal/engine/conn.go new file mode 100644 index 00000000..ad1e56db --- /dev/null +++ b/easytier-go/internal/engine/conn.go @@ -0,0 +1,205 @@ +package engine + +import ( + "context" + "errors" + "io" + "net" + "net/netip" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" +) + +const maxDataPlaneTransfer = 1024 * 1024 + +type streamConn struct { + instance *Instance + resource coreabi.ResourceID + local netip.AddrPort + peer netip.AddrPort + + readMu sync.Mutex + writeMu sync.Mutex + + closed atomic.Bool + closeOnce sync.Once + closeDone chan struct{} + closeErr error +} + +func newStreamConn( + instance *Instance, + result coreabi.OperationResult, +) *streamConn { + return &streamConn{ + instance: instance, + resource: result.Resource, + local: result.Local, + peer: result.Peer, + closeDone: make(chan struct{}), + } +} + +func (conn *streamConn) Read(buffer []byte) (int, error) { + if len(buffer) == 0 { + return 0, nil + } + conn.readMu.Lock() + defer conn.readMu.Unlock() + if conn.closed.Load() { + return 0, net.ErrClosed + } + maximum := len(buffer) + if maximum > maxDataPlaneTransfer { + maximum = maxDataPlaneTransfer + } + result, err := conn.instance.performOperation( + context.Background(), + coreabi.OperationTCPRead, + func( + callCtx context.Context, + core dataPlaneCore, + ) (coreabi.OperationID, error) { + return core.SubmitTCPRead( + callCtx, + conn.resource, + uint32(maximum), + ) + }, + ) + if err != nil { + return 0, normalizeDeadlineError(err) + } + n := copy(buffer, result.Data) + if n == 0 && result.EOF { + return 0, io.EOF + } + return n, nil +} + +func (conn *streamConn) Write(buffer []byte) (int, error) { + if len(buffer) == 0 { + return 0, nil + } + conn.writeMu.Lock() + defer conn.writeMu.Unlock() + if conn.closed.Load() { + return 0, net.ErrClosed + } + written := 0 + for written < len(buffer) { + end := written + maxDataPlaneTransfer + if end > len(buffer) { + end = len(buffer) + } + chunk := buffer[written:end] + result, err := conn.instance.performOperation( + context.Background(), + coreabi.OperationTCPWrite, + func( + callCtx context.Context, + core dataPlaneCore, + ) (coreabi.OperationID, error) { + return core.SubmitTCPWrite( + callCtx, + conn.resource, + chunk, + ) + }, + ) + if err != nil { + return written, normalizeDeadlineError(err) + } + if result.Length <= 0 || result.Length > len(chunk) { + return written, io.ErrShortWrite + } + written += result.Length + } + return written, nil +} + +func (conn *streamConn) Close() error { + conn.closeOnce.Do(func() { + conn.closed.Store(true) + conn.closeErr = conn.instance.closeDataPlaneResource(conn.resource) + close(conn.closeDone) + }) + <-conn.closeDone + return conn.closeErr +} + +func (conn *streamConn) LocalAddr() net.Addr { + return net.TCPAddrFromAddrPort(conn.local) +} + +func (conn *streamConn) RemoteAddr() net.Addr { + return net.TCPAddrFromAddrPort(conn.peer) +} + +func (conn *streamConn) SetDeadline(deadline time.Time) error { + if conn.closed.Load() { + return net.ErrClosed + } + return conn.instance.setDataPlaneResourceDeadline( + conn.resource, + coreabi.DeadlineRead|coreabi.DeadlineWrite, + deadline, + ) +} + +func (conn *streamConn) SetReadDeadline(deadline time.Time) error { + if conn.closed.Load() { + return net.ErrClosed + } + return conn.instance.setDataPlaneResourceDeadline( + conn.resource, + coreabi.DeadlineRead, + deadline, + ) +} + +func (conn *streamConn) SetWriteDeadline(deadline time.Time) error { + if conn.closed.Load() { + return net.ErrClosed + } + return conn.instance.setDataPlaneResourceDeadline( + conn.resource, + coreabi.DeadlineWrite, + deadline, + ) +} + +func normalizeDeadlineError(err error) error { + if errors.Is(err, context.DeadlineExceeded) { + return os.ErrDeadlineExceeded + } + return err +} + +func (instance *Instance) Dial( + ctx context.Context, + peer netip.AddrPort, +) (net.Conn, error) { + timeout, err := contextTimeoutMillis(ctx) + if err != nil { + return nil, normalizeDeadlineError(err) + } + result, err := instance.performOperation( + ctx, + coreabi.OperationTCPConnect, + func( + callCtx context.Context, + core dataPlaneCore, + ) (coreabi.OperationID, error) { + return core.SubmitTCPConnect(callCtx, peer, timeout) + }, + ) + if err != nil { + return nil, normalizeDeadlineError(err) + } + return newStreamConn(instance, result), nil +} diff --git a/easytier-go/internal/engine/dataplane.go b/easytier-go/internal/engine/dataplane.go new file mode 100644 index 00000000..13e49697 --- /dev/null +++ b/easytier-go/internal/engine/dataplane.go @@ -0,0 +1,510 @@ +package engine + +import ( + "context" + "errors" + "fmt" + "math" + "net" + "net/netip" + "os" + "syscall" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" +) + +const dataPlaneCompletionBatch = 64 + +type dataPlaneCore interface { + SubmitTCPConnect( + context.Context, + netip.AddrPort, + uint64, + ) (coreabi.OperationID, error) + SubmitTCPBind( + context.Context, + uint16, + uint64, + ) (coreabi.OperationID, error) + SubmitTCPAccept( + context.Context, + coreabi.ResourceID, + uint64, + ) (coreabi.OperationID, error) + SubmitTCPRead( + context.Context, + coreabi.ResourceID, + uint32, + ) (coreabi.OperationID, error) + SubmitTCPWrite( + context.Context, + coreabi.ResourceID, + []byte, + ) (coreabi.OperationID, error) + SubmitUDPBind( + context.Context, + uint16, + uint64, + ) (coreabi.OperationID, error) + SubmitUDPReceive( + context.Context, + coreabi.ResourceID, + uint32, + ) (coreabi.OperationID, error) + SubmitUDPSend( + context.Context, + coreabi.ResourceID, + netip.AddrPort, + []byte, + ) (coreabi.OperationID, error) + SetResourceDeadline( + context.Context, + coreabi.ResourceID, + coreabi.DeadlineDirection, + uint64, + ) error + DrainCompletions(context.Context, uint32) ([]coreabi.Completion, error) + TakeResult( + context.Context, + coreabi.Completion, + ) (coreabi.OperationResult, error) + CancelOperation(context.Context, coreabi.OperationID) error + CloseResource(context.Context, coreabi.ResourceID) error +} + +type dataPlaneCommandKind uint8 + +const ( + dataPlaneSubmit dataPlaneCommandKind = iota + 1 + dataPlaneCancel + dataPlaneCloseResource + dataPlaneSetDeadline +) + +type dataPlaneCommand struct { + kind dataPlaneCommandKind + operation coreabi.OperationID + resource coreabi.ResourceID + direction coreabi.DeadlineDirection + deadline time.Time + submit func(context.Context, dataPlaneCore) (coreabi.OperationID, error) + opKind coreabi.OperationKind + response chan dataPlaneCommandResponse +} + +type dataPlaneCommandResponse struct { + ticket operationTicket + outcome operationOutcome + completed bool + err error +} + +type operationTicket struct { + id coreabi.OperationID + result <-chan operationOutcome +} + +type operationOutcome struct { + result coreabi.OperationResult + err error +} + +type pendingOperation struct { + kind coreabi.OperationKind + result chan operationOutcome +} + +func (instance *Instance) performOperation( + ctx context.Context, + kind coreabi.OperationKind, + submit func(context.Context, dataPlaneCore) (coreabi.OperationID, error), +) (coreabi.OperationResult, error) { + if ctx == nil { + return coreabi.OperationResult{}, fmt.Errorf( + "submit EasyTier data plane operation with nil context", + ) + } + response := make(chan dataPlaneCommandResponse, 1) + request := dataPlaneCommand{ + kind: dataPlaneSubmit, + submit: submit, + opKind: kind, + response: response, + } + select { + case instance.dataPlaneCommands <- request: + case <-instance.done: + return coreabi.OperationResult{}, net.ErrClosed + case <-instance.closeRequested: + return coreabi.OperationResult{}, net.ErrClosed + case <-ctx.Done(): + return coreabi.OperationResult{}, ctx.Err() + } + + var ticket operationTicket + select { + case submitted := <-response: + if submitted.err != nil { + return coreabi.OperationResult{}, mapDataPlaneError(submitted.err) + } + if submitted.completed { + return submitted.outcome.result, mapDataPlaneError(submitted.outcome.err) + } + ticket = submitted.ticket + case <-instance.done: + return coreabi.OperationResult{}, net.ErrClosed + case <-instance.closeRequested: + return coreabi.OperationResult{}, net.ErrClosed + case <-ctx.Done(): + // The driver may already have submitted the operation. Wait for its + // response so that cancellation always targets the actual operation. + select { + case submitted := <-response: + if submitted.err != nil { + return coreabi.OperationResult{}, mapDataPlaneError(submitted.err) + } + if submitted.completed { + return submitted.outcome.result, mapDataPlaneError( + submitted.outcome.err, + ) + } + ticket = submitted.ticket + case <-instance.done: + return coreabi.OperationResult{}, net.ErrClosed + case <-instance.closeRequested: + return coreabi.OperationResult{}, net.ErrClosed + } + return instance.cancelAndWait(ctx, ticket) + } + + select { + case outcome := <-ticket.result: + return outcome.result, mapDataPlaneError(outcome.err) + case <-ctx.Done(): + return instance.cancelAndWait(ctx, ticket) + case <-instance.done: + return coreabi.OperationResult{}, net.ErrClosed + case <-instance.closeRequested: + return coreabi.OperationResult{}, net.ErrClosed + } +} + +func (instance *Instance) cancelAndWait( + cancelled context.Context, + ticket operationTicket, +) (coreabi.OperationResult, error) { + response := make(chan dataPlaneCommandResponse, 1) + request := dataPlaneCommand{ + kind: dataPlaneCancel, + operation: ticket.id, + response: response, + } + select { + case instance.dataPlaneCommands <- request: + case outcome := <-ticket.result: + return outcome.result, mapDataPlaneError(outcome.err) + case <-instance.done: + return coreabi.OperationResult{}, net.ErrClosed + case <-instance.closeRequested: + return coreabi.OperationResult{}, net.ErrClosed + } + select { + case result := <-response: + if result.err != nil { + return coreabi.OperationResult{}, mapDataPlaneError(result.err) + } + case outcome := <-ticket.result: + return cancelledOutcome(cancelled, outcome) + case <-instance.done: + return coreabi.OperationResult{}, net.ErrClosed + case <-instance.closeRequested: + return coreabi.OperationResult{}, net.ErrClosed + } + + select { + case outcome := <-ticket.result: + return cancelledOutcome(cancelled, outcome) + case <-instance.done: + return coreabi.OperationResult{}, net.ErrClosed + case <-instance.closeRequested: + return coreabi.OperationResult{}, net.ErrClosed + } +} + +func cancelledOutcome( + cancelled context.Context, + outcome operationOutcome, +) (coreabi.OperationResult, error) { + var dataPlaneErr *coreabi.DataPlaneError + if errors.As(outcome.err, &dataPlaneErr) && + dataPlaneErr.Kind == coreabi.ErrorCancelled { + return coreabi.OperationResult{}, cancelled.Err() + } + return outcome.result, mapDataPlaneError(outcome.err) +} + +func (instance *Instance) closeDataPlaneResource( + resource coreabi.ResourceID, +) error { + response := make(chan dataPlaneCommandResponse, 1) + request := dataPlaneCommand{ + kind: dataPlaneCloseResource, + resource: resource, + response: response, + } + select { + case instance.dataPlaneCommands <- request: + case <-instance.done: + return net.ErrClosed + case <-instance.closeRequested: + return net.ErrClosed + } + select { + case result := <-response: + return mapDataPlaneError(result.err) + case <-instance.done: + return net.ErrClosed + case <-instance.closeRequested: + return net.ErrClosed + } +} + +func (instance *Instance) setDataPlaneResourceDeadline( + resource coreabi.ResourceID, + direction coreabi.DeadlineDirection, + deadline time.Time, +) error { + response := make(chan dataPlaneCommandResponse, 1) + request := dataPlaneCommand{ + kind: dataPlaneSetDeadline, + resource: resource, + direction: direction, + deadline: deadline, + response: response, + } + select { + case instance.dataPlaneCommands <- request: + case <-instance.done: + return net.ErrClosed + case <-instance.closeRequested: + return net.ErrClosed + } + select { + case result := <-response: + return mapDataPlaneError(result.err) + case <-instance.done: + return net.ErrClosed + case <-instance.closeRequested: + return net.ErrClosed + } +} + +func (instance *Instance) handleDataPlaneCommand( + request dataPlaneCommand, +) dataPlaneCommandResponse { + instance.host.guestMu.Lock() + defer instance.host.guestMu.Unlock() + switch request.kind { + case dataPlaneSubmit: + operation, err := request.submit(instance.ctx, instance.dataPlane) + if err != nil { + return dataPlaneCommandResponse{err: err} + } + result := make(chan operationOutcome, 1) + instance.pendingOperations[operation] = &pendingOperation{ + kind: request.opKind, + result: result, + } + return dataPlaneCommandResponse{ticket: operationTicket{ + id: operation, + result: result, + }} + case dataPlaneCancel: + if _, exists := instance.pendingOperations[request.operation]; !exists { + return dataPlaneCommandResponse{} + } + return dataPlaneCommandResponse{ + err: instance.dataPlane.CancelOperation( + instance.ctx, + request.operation, + ), + } + case dataPlaneCloseResource: + return dataPlaneCommandResponse{ + err: instance.dataPlane.CloseResource( + instance.ctx, + request.resource, + ), + } + case dataPlaneSetDeadline: + return dataPlaneCommandResponse{ + err: instance.dataPlane.SetResourceDeadline( + instance.ctx, + request.resource, + request.direction, + resourceDeadlineTimeoutMillis(request.deadline), + ), + } + default: + return dataPlaneCommandResponse{ + err: fmt.Errorf("unknown data plane command %d", request.kind), + } + } +} + +func (instance *Instance) drainDataPlaneCompletions() (bool, error) { + if instance.dataPlane == nil || len(instance.pendingOperations) == 0 { + return false, nil + } + completions, err := instance.dataPlane.DrainCompletions( + instance.ctx, + dataPlaneCompletionBatch, + ) + if err != nil { + return false, err + } + for _, completion := range completions { + pending := instance.pendingOperations[completion.Operation] + if pending == nil { + return false, fmt.Errorf( + "EasyTier completed unknown data plane operation %d", + completion.Operation, + ) + } + if pending.kind != completion.Kind { + return false, fmt.Errorf( + "EasyTier completed operation %d as kind %d, want %d", + completion.Operation, + completion.Kind, + pending.kind, + ) + } + result, resultErr := instance.dataPlane.TakeResult( + instance.ctx, + completion, + ) + if err := validateCompletionResult(completion, resultErr); err != nil { + return false, err + } + delete(instance.pendingOperations, completion.Operation) + pending.result <- operationOutcome{result: result, err: resultErr} + } + return len(completions) == dataPlaneCompletionBatch, nil +} + +func validateCompletionResult( + completion coreabi.Completion, + resultErr error, +) error { + var dataPlaneErr *coreabi.DataPlaneError + if completion.Status == coreabi.ErrorNone { + if resultErr != nil { + return fmt.Errorf( + "take successful data plane operation %d: %w", + completion.Operation, + resultErr, + ) + } + return nil + } + if !errors.As(resultErr, &dataPlaneErr) { + return fmt.Errorf( + "take failed data plane operation %d with status %d: %w", + completion.Operation, + completion.Status, + resultErr, + ) + } + if dataPlaneErr.Kind != completion.Status { + return fmt.Errorf( + "data plane operation %d status %d disagrees with result %d", + completion.Operation, + completion.Status, + dataPlaneErr.Kind, + ) + } + return nil +} + +func (instance *Instance) failPendingOperations(err error) { + for operation, pending := range instance.pendingOperations { + pending.result <- operationOutcome{err: err} + delete(instance.pendingOperations, operation) + } +} + +func timeoutFromDeadline(deadline time.Time) (uint64, error) { + if deadline.IsZero() { + return math.MaxUint64, nil + } + remaining := time.Until(deadline) + if remaining <= 0 { + return 0, os.ErrDeadlineExceeded + } + millis := remaining / time.Millisecond + if remaining%time.Millisecond != 0 { + millis++ + } + return uint64(millis), nil +} + +func resourceDeadlineTimeoutMillis(deadline time.Time) uint64 { + if deadline.IsZero() { + return math.MaxUint64 + } + remaining := time.Until(deadline) + if remaining <= 0 { + return 0 + } + millis := remaining / time.Millisecond + if remaining%time.Millisecond != 0 { + millis++ + } + return uint64(millis) +} + +func contextTimeoutMillis(ctx context.Context) (uint64, error) { + if err := ctx.Err(); err != nil { + return 0, err + } + deadline, exists := ctx.Deadline() + if !exists { + return math.MaxUint64, nil + } + return timeoutFromDeadline(deadline) +} + +func mapDataPlaneError(err error) error { + if err == nil { + return nil + } + var dataPlaneErr *coreabi.DataPlaneError + if !errors.As(err, &dataPlaneErr) { + return err + } + switch dataPlaneErr.Kind { + case coreabi.ErrorCancelled: + return context.Canceled + case coreabi.ErrorDeadlineExceeded: + return os.ErrDeadlineExceeded + case coreabi.ErrorInstanceStopped, coreabi.ErrorHandleClosed: + return net.ErrClosed + case coreabi.ErrorNoOverlayRoute, coreabi.ErrorPathNotReady: + return syscall.ENETUNREACH + case coreabi.ErrorAddressFamilyUnsupported: + return syscall.EAFNOSUPPORT + case coreabi.ErrorAddressInUse: + return syscall.EADDRINUSE + case coreabi.ErrorConnectionRefused: + return syscall.ECONNREFUSED + case coreabi.ErrorNetworkChanged: + return syscall.ENETRESET + case coreabi.ErrorResourceLimit: + return syscall.ENOBUFS + case coreabi.ErrorBufferTooSmall: + return syscall.EMSGSIZE + default: + return err + } +} diff --git a/easytier-go/internal/engine/driver_test.go b/easytier-go/internal/engine/driver_test.go new file mode 100644 index 00000000..cffcd883 --- /dev/null +++ b/easytier-go/internal/engine/driver_test.go @@ -0,0 +1,164 @@ +package engine + +import ( + "context" + "errors" + "math" + "reflect" + "testing" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" +) + +type recordingCore struct { + calls []string +} + +func (*recordingCore) Start(context.Context) error { return nil } +func (*recordingCore) Stop(context.Context) error { return nil } +func (core *recordingCore) Drive(context.Context) (coreabi.State, error) { + core.calls = append(core.calls, "drive") + return coreabi.StateRunning, nil +} +func (core *recordingCore) NotifyCompletions(context.Context) error { + core.calls = append(core.calls, "notify") + return nil +} +func (core *recordingCore) NextDeadline(context.Context) (int64, error) { + core.calls = append(core.calls, "deadline") + return math.MaxInt64, nil +} +func (core *recordingCore) SendPacket(context.Context, []byte) error { + core.calls = append(core.calls, "send") + return nil +} +func (*recordingCore) Drop(context.Context) error { return nil } + +func TestCompletionNotifiesGuestBeforeDriving(t *testing.T) { + core := &recordingCore{} + instance := &Instance{ + host: &Host{}, + ctx: context.Background(), + core: core, + running: make(chan struct{}), + stopped: make(chan struct{}), + } + if _, err := instance.drive(true); err != nil { + t.Fatalf("drive completion: %v", err) + } + want := []string{"notify", "drive", "deadline"} + if !reflect.DeepEqual(core.calls, want) { + t.Fatalf("completion call order = %v, want %v", core.calls, want) + } +} + +func TestPacketIngressBatchDrivesOnce(t *testing.T) { + core := &recordingCore{} + instance := &Instance{ + host: &Host{}, + ctx: context.Background(), + core: core, + commands: make(chan command, maximumPacketIngressBatch), + running: make(chan struct{}), + stopped: make(chan struct{}), + } + requests := make([]command, 3) + for index := range requests { + requests[index] = command{ + kind: commandSendPacket, + packet: []byte{byte(index)}, + response: make(chan error, 1), + } + if index != 0 { + instance.commands <- requests[index] + } + } + + if _, err := instance.handlePacketBatch(requests[0], math.MaxInt64); err != nil { + t.Fatalf("handle packet batch: %v", err) + } + for index := range requests { + if err := <-requests[index].response; err != nil { + t.Fatalf("packet %d response: %v", index, err) + } + } + want := []string{"send", "send", "send", "drive", "deadline"} + if !reflect.DeepEqual(core.calls, want) { + t.Fatalf("packet batch call order = %v, want %v", core.calls, want) + } +} + +func TestSendPacketBorrowsBufferUntilGuestConsumesIt(t *testing.T) { + instance := &Instance{ + commands: make(chan command, 1), + closeRequested: make(chan struct{}), + done: make(chan struct{}), + } + ctx, cancel := context.WithCancel(context.Background()) + packet := []byte{1, 2, 3} + result := make(chan error, 1) + go func() { + result <- instance.SendPacket(ctx, packet) + }() + + var request command + select { + case request = <-instance.commands: + case <-time.After(time.Second): + t.Fatal("SendPacket did not enqueue") + } + if &request.packet[0] != &packet[0] { + t.Fatal("SendPacket copied the packet before enqueue") + } + + cancel() + select { + case err := <-result: + t.Fatalf("SendPacket returned before guest consumption: %v", err) + case <-time.After(20 * time.Millisecond): + } + + request.response <- nil + select { + case err := <-result: + if err != nil { + t.Fatalf("SendPacket after guest consumption: %v", err) + } + case <-time.After(time.Second): + t.Fatal("SendPacket did not return after guest consumption") + } +} + +func TestSendPacketBackpressureRemainsCancelableBeforeEnqueue(t *testing.T) { + instance := &Instance{ + commands: make(chan command, 1), + closeRequested: make(chan struct{}), + done: make(chan struct{}), + } + instance.commands <- command{kind: commandStart} + + ctx, cancel := context.WithCancel(context.Background()) + result := make(chan error, 1) + go func() { + result <- instance.SendPacket(ctx, []byte{1}) + }() + + select { + case err := <-result: + t.Fatalf("SendPacket bypassed full command queue: %v", err) + case <-time.After(20 * time.Millisecond): + } + cancel() + select { + case err := <-result: + if !errors.Is(err, context.Canceled) { + t.Fatalf("SendPacket on full queue returned %v, want context cancellation", err) + } + case <-time.After(time.Second): + t.Fatal("SendPacket on full queue ignored context cancellation") + } + if got := len(instance.commands); got != 1 { + t.Fatalf("queued commands = %d, want the original command only", got) + } +} diff --git a/easytier-go/internal/engine/host.go b/easytier-go/internal/engine/host.go new file mode 100644 index 00000000..a67dc79e --- /dev/null +++ b/easytier-go/internal/engine/host.go @@ -0,0 +1,311 @@ +package engine + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "sync" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/internal/artifact" + "github.com/EasyTier/EasyTier/easytier-go/internal/contextutil" + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" + "github.com/EasyTier/EasyTier/easytier-go/internal/hostabi" + "github.com/EasyTier/EasyTier/easytier-go/internal/reactor" + "github.com/EasyTier/EasyTier/easytier-go/platform" + "github.com/metacubex/wazero" + "github.com/metacubex/wazero/api" + "github.com/metacubex/wazero/imports/wasi_snapshot_preview1" +) + +const defaultPacketQueueCapacity = 64 +const maximumPacketIngressBatch = 32 + +type Options struct { + Services platform.Services + PacketQueueCapacity int + Management reactor.ManagementHandler +} + +type Host struct { + ctx context.Context + cancel context.CancelFunc + + runtime wazero.Runtime + module api.Module + reactor *reactor.Reactor + options Options + + guestMu sync.Mutex + + mu sync.Mutex + closed bool + instances map[*Instance]struct{} + webClient *WebClient + closeOnce sync.Once + closeDone chan struct{} + closeErr error +} + +func NewHost(ctx context.Context, options Options) (_ *Host, err error) { + if ctx == nil { + return nil, fmt.Errorf("create EasyTier engine host with nil context") + } + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + if options.PacketQueueCapacity == 0 { + options.PacketQueueCapacity = defaultPacketQueueCapacity + } + if options.PacketQueueCapacity < 0 { + return nil, fmt.Errorf("packet queue capacity must be positive") + } + + lifetime, cancel := context.WithCancel(contextutil.WithoutCancel(ctx)) + runtime := wazero.NewRuntime(lifetime) + var module api.Module + var hostReactor *reactor.Reactor + defer func() { + if err == nil { + return + } + cancel() + if module != nil { + _ = module.Close(contextutil.WithoutCancel(ctx)) + } + if hostReactor != nil { + hostReactor.Close() + } + _ = runtime.Close(contextutil.WithoutCancel(ctx)) + }() + + if _, err = wasi_snapshot_preview1.Instantiate(ctx, runtime); err != nil { + return nil, fmt.Errorf("instantiate WASI preview1: %w", err) + } + hostReactor = reactor.New(lifetime, reactor.Options{ + Services: options.Services, + Management: options.Management, + }) + adapter, err := hostabi.New(hostReactor) + if err != nil { + return nil, err + } + if err = adapter.Instantiate(ctx, runtime); err != nil { + return nil, fmt.Errorf("instantiate EasyTier host ABI: %w", err) + } + compiled, err := runtime.CompileModule(ctx, artifact.Core()) + if err != nil { + return nil, fmt.Errorf("compile embedded EasyTier core: %w", err) + } + defer compiled.Close(contextutil.WithoutCancel(ctx)) + module, err = runtime.InstantiateModule(ctx, compiled, newModuleConfig()) + if err != nil { + return nil, fmt.Errorf("instantiate embedded EasyTier core: %w", err) + } + + host := &Host{ + ctx: lifetime, + cancel: cancel, + runtime: runtime, + module: module, + reactor: hostReactor, + options: options, + instances: make(map[*Instance]struct{}), + closeDone: make(chan struct{}), + } + go host.broadcastCompletions() + return host, nil +} + +func newModuleConfig() wazero.ModuleConfig { + return wazero.NewModuleConfig(). + WithRandSource(rand.Reader). + WithSysWalltime(). + WithSysNanotime(). + WithSysNanosleep() +} + +func (host *Host) CreateInstance( + ctx context.Context, + configTOML string, +) (*Instance, error) { + if host == nil { + return nil, fmt.Errorf("create instance with nil EasyTier engine host") + } + if ctx == nil { + return nil, fmt.Errorf("create EasyTier instance with nil context") + } + envelope, err := encodeCreateEnvelope(configTOML, host.options.Services.Snapshot) + if err != nil { + return nil, err + } + + host.mu.Lock() + defer host.mu.Unlock() + if host.closed { + return nil, fmt.Errorf("create instance with closed EasyTier engine host") + } + packetSink, err := host.reactor.RegisterPacketSink(host.options.PacketQueueCapacity) + if err != nil { + return nil, fmt.Errorf("register EasyTier packet sink: %w", err) + } + events := make(chan Event, instanceEventQueueCapacity) + journal := newEventJournal() + eventSink, err := host.reactor.RegisterEventSink(func(kind, message string) bool { + journal.add(kind, message) + select { + case events <- Event{Kind: kind, Message: message}: + return true + default: + return false + } + }) + if err != nil { + host.reactor.UnregisterPacketSink(packetSink) + close(events) + return nil, fmt.Errorf("register EasyTier event sink: %w", err) + } + host.guestMu.Lock() + core, err := coreabi.New(host.module) + if err == nil { + err = core.Create(ctx, envelope, packetSink, eventSink) + } + host.guestMu.Unlock() + if err != nil { + host.reactor.UnregisterPacketSink(packetSink) + host.reactor.UnregisterEventSink(eventSink) + close(events) + return nil, err + } + + lifetime, cancel := context.WithCancel(host.ctx) + instance := &Instance{ + host: host, + ctx: lifetime, + cancel: cancel, + core: core, + dataPlane: core, + rpc: core, + reactor: host.reactor, + packetSink: packetSink, + eventSink: eventSink, + events: events, + journal: journal, + commands: make(chan command, maximumPacketIngressBatch), + dataPlaneCommands: make(chan dataPlaneCommand), + rpcCommands: make(chan rpcCommand), + pendingOperations: make( + map[coreabi.OperationID]*pendingOperation, + ), + pendingRPCs: make( + map[coreabi.RPCOperationID]*pendingRPC, + ), + completions: make(chan struct{}, 1), + closeRequested: make(chan struct{}), + done: make(chan struct{}), + running: make(chan struct{}), + stopped: make(chan struct{}), + } + instance.state.Store(int32(coreabi.StateCreated)) + host.instances[instance] = struct{}{} + go instance.run() + return instance, nil +} + +func (host *Host) Close(ctx context.Context) error { + if host == nil { + return nil + } + if ctx == nil { + return fmt.Errorf("close EasyTier engine host with nil context") + } + host.closeOnce.Do(func() { + host.mu.Lock() + host.closed = true + webClient := host.webClient + instances := make([]*Instance, 0, len(host.instances)) + for instance := range host.instances { + instances = append(instances, instance) + } + host.mu.Unlock() + go host.shutdown(webClient, instances) + }) + select { + case <-host.closeDone: + host.mu.Lock() + err := host.closeErr + host.mu.Unlock() + return err + case <-ctx.Done(): + return ctx.Err() + } +} + +func (host *Host) shutdown(webClient *WebClient, instances []*Instance) { + var closeErrors []error + if webClient != nil { + if err := webClient.Close(host.ctx); err != nil { + closeErrors = append(closeErrors, err) + } + } + for _, instance := range instances { + if err := instance.Close(host.ctx); err != nil { + closeErrors = append(closeErrors, err) + } + } + + host.cancel() + host.reactor.Close() + cleanupContext, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + moduleErr := host.module.Close(cleanupContext) + runtimeErr := host.runtime.Close(cleanupContext) + + host.mu.Lock() + host.closeErr = errors.Join( + append(closeErrors, moduleErr, runtimeErr)..., + ) + host.mu.Unlock() + close(host.closeDone) +} + +func (host *Host) broadcastCompletions() { + for { + select { + case <-host.reactor.Completions(): + host.mu.Lock() + if host.webClient != nil { + select { + case host.webClient.completions <- struct{}{}: + default: + } + } + for instance := range host.instances { + select { + case instance.completions <- struct{}{}: + default: + } + } + host.mu.Unlock() + case <-host.ctx.Done(): + return + } + } +} + +func (host *Host) removeWebClient(client *WebClient) { + host.mu.Lock() + if host.webClient == client { + host.webClient = nil + } + host.mu.Unlock() +} + +func (host *Host) removeInstance(instance *Instance) { + host.mu.Lock() + delete(host.instances, instance) + host.mu.Unlock() +} diff --git a/easytier-go/internal/engine/instance.go b/easytier-go/internal/engine/instance.go new file mode 100644 index 00000000..80e561df --- /dev/null +++ b/easytier-go/internal/engine/instance.go @@ -0,0 +1,526 @@ +package engine + +import ( + "context" + "errors" + "fmt" + "math" + "net" + "sync" + "sync/atomic" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/internal/contextutil" + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" + "github.com/EasyTier/EasyTier/easytier-go/internal/reactor" +) + +type commandKind uint8 + +const ( + commandStart commandKind = iota + 1 + commandStop + commandSendPacket +) + +type command struct { + kind commandKind + packet []byte + response chan error +} + +type guestCore interface { + Start(context.Context) error + Stop(context.Context) error + Drive(context.Context) (coreabi.State, error) + NotifyCompletions(context.Context) error + NextDeadline(context.Context) (int64, error) + SendPacket(context.Context, []byte) error + Drop(context.Context) error +} + +type Event struct { + // Kind is a stable snake-case CoreEvent variant name. + Kind string + // Message is a human-readable description intended for logging. + Message string +} + +const instanceEventQueueCapacity = 256 + +type eventJournal struct { + mu sync.Mutex + events []string +} + +func newEventJournal() *eventJournal { + return &eventJournal{events: make([]string, 0, instanceEventQueueCapacity)} +} + +func (journal *eventJournal) add(kind, message string) { + if message == "" { + message = kind + } + journal.mu.Lock() + if len(journal.events) == cap(journal.events) { + copy(journal.events, journal.events[1:]) + journal.events = journal.events[:len(journal.events)-1] + } + journal.events = append(journal.events, message) + journal.mu.Unlock() +} + +func (journal *eventJournal) snapshot() []string { + journal.mu.Lock() + defer journal.mu.Unlock() + return append([]string(nil), journal.events...) +} + +type Instance struct { + host *Host + ctx context.Context + cancel context.CancelFunc + + core guestCore + dataPlane dataPlaneCore + rpc rpcCore + reactor *reactor.Reactor + packetSink uint64 + eventSink uint64 + events chan Event + journal *eventJournal + + commands chan command + dataPlaneCommands chan dataPlaneCommand + rpcCommands chan rpcCommand + pendingOperations map[coreabi.OperationID]*pendingOperation + pendingRPCs map[coreabi.RPCOperationID]*pendingRPC + completions chan struct{} + closeRequested chan struct{} + closeOnce sync.Once + closing atomic.Bool + done chan struct{} + running chan struct{} + runningOnce sync.Once + stopped chan struct{} + stoppedOnce sync.Once + state atomic.Int32 + + errMu sync.Mutex + terminalErr error +} + +func (instance *Instance) Start(ctx context.Context) error { + if ctx == nil { + return fmt.Errorf("start EasyTier instance with nil context") + } + if err := instance.execute(ctx, command{kind: commandStart}); err != nil { + return err + } + select { + case <-instance.running: + return nil + case <-instance.done: + return instance.finishedError("start EasyTier instance") + case <-ctx.Done(): + return ctx.Err() + } +} + +func (instance *Instance) Stop(ctx context.Context) error { + if ctx == nil { + return fmt.Errorf("stop EasyTier instance with nil context") + } + if err := instance.execute(ctx, command{kind: commandStop}); err != nil { + return err + } + select { + case <-instance.stopped: + return nil + case <-instance.done: + return instance.finishedError("stop EasyTier instance") + case <-ctx.Done(): + return ctx.Err() + } +} + +func (instance *Instance) SendPacket(ctx context.Context, packet []byte) error { + if ctx == nil { + return fmt.Errorf("send EasyTier packet with nil context") + } + if len(packet) == 0 { + return fmt.Errorf("send empty packet") + } + return instance.execute(ctx, command{ + kind: commandSendPacket, + packet: packet, + }) +} + +func (instance *Instance) ReceivePacket(ctx context.Context) ([]byte, error) { + if ctx == nil { + return nil, fmt.Errorf("receive EasyTier packet with nil context") + } + select { + case <-instance.done: + return nil, instance.finishedError("receive EasyTier packet") + default: + } + return instance.reactor.ReceivePacket(ctx, instance.packetSink) +} + +func (instance *Instance) Events() <-chan Event { + return instance.events +} + +func (instance *Instance) Wait(ctx context.Context) error { + if ctx == nil { + return fmt.Errorf("wait for EasyTier instance with nil context") + } + select { + case <-instance.stopped: + return nil + case <-instance.done: + return instance.finishedError("wait for EasyTier instance") + case <-ctx.Done(): + return ctx.Err() + } +} + +func (instance *Instance) State() coreabi.State { + return coreabi.State(instance.state.Load()) +} + +func (instance *Instance) ManagementEvents() []string { + return instance.journal.snapshot() +} + +func (instance *Instance) TerminalError() error { + return instance.terminalError() +} + +func (instance *Instance) Close(ctx context.Context) error { + if ctx == nil { + return fmt.Errorf("close EasyTier instance with nil context") + } + instance.closeOnce.Do(func() { + instance.closing.Store(true) + close(instance.closeRequested) + }) + select { + case <-instance.done: + return instance.terminalError() + case <-ctx.Done(): + return ctx.Err() + } +} + +func (instance *Instance) execute(ctx context.Context, request command) error { + if ctx == nil { + return fmt.Errorf("execute EasyTier command with nil context") + } + request.response = make(chan error, 1) + if instance.closing.Load() { + return fmt.Errorf("execute EasyTier command on closing instance") + } + select { + case instance.commands <- request: + case <-instance.done: + return instance.finishedError("execute EasyTier command") + case <-instance.closeRequested: + return fmt.Errorf("execute EasyTier command on closing instance") + case <-ctx.Done(): + return ctx.Err() + } + if request.kind == commandSendPacket { + // The guest may retain packet until it responds. Do not let caller + // cancellation end the borrow after the request enters the queue. + select { + case err := <-request.response: + return err + case <-instance.done: + return instance.finishedError("execute EasyTier command") + } + } + select { + case err := <-request.response: + return err + case <-instance.done: + return instance.finishedError("execute EasyTier command") + case <-instance.closeRequested: + return fmt.Errorf("execute EasyTier command on closing instance") + case <-ctx.Done(): + return ctx.Err() + } +} + +func (instance *Instance) run() { + runErr := instance.driveLoop() + instance.state.Store(int32(coreabi.StateStopped)) + instance.failPendingOperations(net.ErrClosed) + instance.failPendingRPCs(net.ErrClosed) + cleanupErr := instance.shutdown() + instance.errMu.Lock() + instance.terminalErr = errors.Join(runErr, cleanupErr) + instance.errMu.Unlock() + close(instance.done) +} + +func (instance *Instance) driveLoop() error { + deadline := int64(math.MaxInt64) + timer := time.NewTimer(time.Hour) + stopTimer(timer) + defer stopTimer(timer) + for { + timerChannel := deadlineTimer(timer, deadline) + select { + case request := <-instance.commands: + stopTimer(timer) + if request.kind == commandSendPacket { + next, err := instance.handlePacketBatch(request, deadline) + if err != nil { + return err + } + deadline = next + continue + } + commandErr := instance.handleCommand(request) + if commandErr != nil { + request.response <- commandErr + continue + } + next, driveErr := instance.drive(false) + request.response <- driveErr + if driveErr != nil { + return driveErr + } + deadline = next + case request := <-instance.dataPlaneCommands: + stopTimer(timer) + response := instance.handleDataPlaneCommand(request) + if response.err != nil { + request.response <- response + continue + } + next, driveErr := instance.drive(false) + if driveErr != nil { + response.err = driveErr + } else { + select { + case response.outcome = <-response.ticket.result: + response.completed = true + default: + } + } + request.response <- response + if driveErr != nil { + return driveErr + } + deadline = next + case request := <-instance.rpcCommands: + stopTimer(timer) + response := instance.handleRPCCommand(request) + if response.err != nil { + request.response <- response + continue + } + next, driveErr := instance.drive(false) + if driveErr != nil { + response.err = driveErr + } else if response.ticket.result != nil { + select { + case response.outcome = <-response.ticket.result: + response.completed = true + default: + } + } + request.response <- response + if driveErr != nil { + return driveErr + } + deadline = next + case <-instance.completions: + stopTimer(timer) + next, err := instance.drive(true) + if err != nil { + return err + } + deadline = next + case <-timerChannel: + next, err := instance.drive(false) + if err != nil { + return err + } + deadline = next + case <-instance.closeRequested: + stopTimer(timer) + return nil + case <-instance.ctx.Done(): + stopTimer(timer) + return instance.ctx.Err() + } + } +} + +func (instance *Instance) handlePacketBatch( + first command, + deadline int64, +) (int64, error) { + var batch [maximumPacketIngressBatch]command + batch[0] = first + count := 1 + var following *command + +drain: + for count < len(batch) { + select { + case request := <-instance.commands: + if request.kind != commandSendPacket { + following = &request + break drain + } + batch[count] = request + count++ + default: + break drain + } + } + + successful := false + var sendErrors [maximumPacketIngressBatch]error + for index := 0; index < count; index++ { + err := instance.handleCommand(batch[index]) + sendErrors[index] = err + successful = successful || err == nil + } + var driveErr error + if successful { + deadline, driveErr = instance.drive(false) + } + for index := 0; index < count; index++ { + if sendErrors[index] != nil { + batch[index].response <- sendErrors[index] + } else { + batch[index].response <- driveErr + } + } + if driveErr != nil { + return deadline, driveErr + } + + if following == nil { + return deadline, nil + } + commandErr := instance.handleCommand(*following) + if commandErr != nil { + following.response <- commandErr + return deadline, nil + } + deadline, driveErr = instance.drive(false) + following.response <- driveErr + return deadline, driveErr +} + +func (instance *Instance) handleCommand(request command) error { + instance.host.guestMu.Lock() + defer instance.host.guestMu.Unlock() + switch request.kind { + case commandStart: + return instance.core.Start(instance.ctx) + case commandStop: + return instance.core.Stop(instance.ctx) + case commandSendPacket: + return instance.core.SendPacket(instance.ctx, request.packet) + default: + return fmt.Errorf("unknown EasyTier command %d", request.kind) + } +} + +func (instance *Instance) drive(notify bool) (int64, error) { + instance.host.guestMu.Lock() + defer instance.host.guestMu.Unlock() + if notify { + if err := instance.core.NotifyCompletions(instance.ctx); err != nil { + return 0, err + } + } + state, err := instance.core.Drive(instance.ctx) + if err != nil { + return 0, err + } + instance.state.Store(int32(state)) + if state == coreabi.StateRunning { + instance.runningOnce.Do(func() { close(instance.running) }) + } + if state == coreabi.StateStopped { + instance.stoppedOnce.Do(func() { close(instance.stopped) }) + } + moreDataPlaneCompletions, err := instance.drainDataPlaneCompletions() + if err != nil { + return 0, err + } + if err := instance.takeRPCResponses(); err != nil { + return 0, err + } + deadline, err := instance.core.NextDeadline(instance.ctx) + if err != nil { + return 0, err + } + if moreDataPlaneCompletions { + return 0, nil + } + return deadline, nil +} + +func (instance *Instance) shutdown() error { + cleanupContext, cancel := context.WithTimeout( + contextutil.WithoutCancel(instance.ctx), + 5*time.Second, + ) + defer cancel() + instance.host.guestMu.Lock() + dropErr := instance.core.Drop(cleanupContext) + instance.host.guestMu.Unlock() + instance.reactor.UnregisterPacketSink(instance.packetSink) + instance.reactor.UnregisterEventSink(instance.eventSink) + close(instance.events) + instance.cancel() + instance.host.removeInstance(instance) + return dropErr +} + +func (instance *Instance) finishedError(operation string) error { + if err := instance.terminalError(); err != nil { + return fmt.Errorf("%s: %w", operation, err) + } + return fmt.Errorf("%s: instance is closed", operation) +} + +func (instance *Instance) terminalError() error { + instance.errMu.Lock() + defer instance.errMu.Unlock() + return instance.terminalErr +} + +func deadlineTimer(timer *time.Timer, deadline int64) <-chan time.Time { + stopTimer(timer) + if deadline == math.MaxInt64 { + return nil + } + duration := time.Duration(deadline) * time.Millisecond + if deadline > int64(math.MaxInt64/time.Millisecond) { + duration = time.Duration(math.MaxInt64) + } + timer.Reset(duration) + return timer.C +} + +func stopTimer(timer *time.Timer) { + if timer == nil || timer.Stop() { + return + } + select { + case <-timer.C: + default: + } +} diff --git a/easytier-go/internal/engine/instance_test.go b/easytier-go/internal/engine/instance_test.go new file mode 100644 index 00000000..e6d70436 --- /dev/null +++ b/easytier-go/internal/engine/instance_test.go @@ -0,0 +1,96 @@ +package engine + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" + "github.com/EasyTier/EasyTier/easytier-go/platform/netstd" +) + +const minimalConfig = `instance_id = "018f4fb1-7a2c-7d1f-9d89-935b0ad7e135" +instance_name = "go-host-engine" + +[network_identity] +network_name = "default" +network_secret = "test" + +[flags] +disable_p2p = true +enable_encryption = false +bind_device = false +` + +func TestHostCreatesAndDrivesInstanceLifecycle(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + host, err := NewHost(ctx, Options{Services: netstd.Services()}) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + instance, err := host.CreateInstance(ctx, minimalConfig) + if err != nil { + t.Fatalf("create instance: %v", err) + } + defer instance.Close(ctx) + + if err := instance.Start(ctx); err != nil { + t.Fatalf("start instance: %v", err) + } + if state := instance.State(); state != 2 { + t.Fatalf("running state = %d, want 2", state) + } + if err := instance.Stop(ctx); err != nil { + t.Fatalf("stop instance: %v", err) + } + if err := instance.Wait(ctx); err != nil { + t.Fatalf("wait for stopped instance: %v", err) + } +} + +func TestClosingOneHandleLeavesOtherInstanceRunning(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + host, err := NewHost(ctx, Options{Services: netstd.Services()}) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + + first, err := host.CreateInstance(ctx, minimalConfig) + if err != nil { + t.Fatalf("create first instance: %v", err) + } + secondConfig := strings.NewReplacer( + "018f4fb1-7a2c-7d1f-9d89-935b0ad7e135", + "018f4fb1-7a2c-7d1f-9d89-935b0ad7e136", + "go-host-engine", + "go-host-engine-second", + ).Replace(minimalConfig) + second, err := host.CreateInstance(ctx, secondConfig) + if err != nil { + t.Fatalf("create second instance: %v", err) + } + defer second.Close(ctx) + if err := first.Start(ctx); err != nil { + t.Fatalf("start first instance: %v", err) + } + if err := second.Start(ctx); err != nil { + t.Fatalf("start second instance: %v", err) + } + if err := first.Close(ctx); err != nil { + t.Fatalf("close first instance: %v", err) + } + if state := first.State(); state != coreabi.StateStopped { + t.Fatalf("closed first state = %d, want stopped", state) + } + if state := second.State(); state != 2 { + t.Fatalf("second state after first close = %d, want running", state) + } + if err := second.Stop(ctx); err != nil { + t.Fatalf("stop second instance: %v", err) + } +} diff --git a/easytier-go/internal/engine/listener.go b/easytier-go/internal/engine/listener.go new file mode 100644 index 00000000..4f01b889 --- /dev/null +++ b/easytier-go/internal/engine/listener.go @@ -0,0 +1,84 @@ +package engine + +import ( + "context" + "net" + "net/netip" + "sync" + "sync/atomic" + + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" +) + +type streamListener struct { + instance *Instance + resource coreabi.ResourceID + local netip.AddrPort + + closed atomic.Bool + closeOnce sync.Once + closeDone chan struct{} + closeErr error +} + +func (listener *streamListener) Accept() (net.Conn, error) { + if listener.closed.Load() { + return nil, net.ErrClosed + } + result, err := listener.instance.performOperation( + context.Background(), + coreabi.OperationTCPAccept, + func( + ctx context.Context, + core dataPlaneCore, + ) (coreabi.OperationID, error) { + return core.SubmitTCPAccept( + ctx, + listener.resource, + ^uint64(0), + ) + }, + ) + if err != nil { + return nil, err + } + return newStreamConn(listener.instance, result), nil +} + +func (listener *streamListener) Close() error { + listener.closeOnce.Do(func() { + listener.closed.Store(true) + listener.closeErr = listener.instance.closeDataPlaneResource( + listener.resource, + ) + close(listener.closeDone) + }) + <-listener.closeDone + return listener.closeErr +} + +func (listener *streamListener) Addr() net.Addr { + return net.TCPAddrFromAddrPort(listener.local) +} + +func (instance *Instance) Listen(port uint16) (net.Listener, error) { + result, err := instance.performOperation( + context.Background(), + coreabi.OperationTCPBind, + func( + ctx context.Context, + core dataPlaneCore, + ) (coreabi.OperationID, error) { + return core.SubmitTCPBind(ctx, port, ^uint64(0)) + }, + ) + if err != nil { + return nil, err + } + return &streamListener{ + instance: instance, + resource: result.Resource, + local: result.Local, + closeDone: make(chan struct{}), + }, nil +} diff --git a/easytier-go/internal/engine/module_config_test.go b/easytier-go/internal/engine/module_config_test.go new file mode 100644 index 00000000..4859b45a --- /dev/null +++ b/easytier-go/internal/engine/module_config_test.go @@ -0,0 +1,61 @@ +package engine + +import ( + "context" + "testing" + "time" + + "github.com/metacubex/wazero" + "github.com/metacubex/wazero/imports/wasi_snapshot_preview1" +) + +// wasiWalltimeProbe imports clock_time_get and exports now(), which returns +// the realtime clock value written by WASI at memory offset zero. +var wasiWalltimeProbe = []byte{ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x0c, 0x02, 0x60, 0x03, 0x7f, 0x7e, 0x7f, + 0x01, 0x7f, 0x60, 0x00, 0x01, 0x7e, + 0x02, 0x29, 0x01, 0x16, + 'w', 'a', 's', 'i', '_', 's', 'n', 'a', 'p', 's', 'h', + 'o', 't', '_', 'p', 'r', 'e', 'v', 'i', 'e', 'w', '1', + 0x0e, + 'c', 'l', 'o', 'c', 'k', '_', 't', 'i', 'm', 'e', '_', 'g', 'e', 't', + 0x00, 0x00, + 0x03, 0x02, 0x01, 0x01, + 0x05, 0x03, 0x01, 0x00, 0x01, + 0x07, 0x10, 0x02, + 0x06, 'm', 'e', 'm', 'o', 'r', 'y', 0x02, 0x00, + 0x03, 'n', 'o', 'w', 0x00, 0x01, + 0x0a, 0x12, 0x01, 0x10, 0x00, + 0x41, 0x00, 0x42, 0x00, 0x41, 0x00, 0x10, 0x00, 0x1a, + 0x41, 0x00, 0x29, 0x03, 0x00, 0x0b, +} + +func TestModuleConfigUsesSystemWalltime(t *testing.T) { + ctx := context.Background() + runtime := wazero.NewRuntime(ctx) + defer runtime.Close(ctx) + if _, err := wasi_snapshot_preview1.Instantiate(ctx, runtime); err != nil { + t.Fatalf("instantiate WASI: %v", err) + } + compiled, err := runtime.CompileModule(ctx, wasiWalltimeProbe) + if err != nil { + t.Fatalf("compile walltime probe: %v", err) + } + defer compiled.Close(ctx) + + before := time.Now() + module, err := runtime.InstantiateModule(ctx, compiled, newModuleConfig()) + if err != nil { + t.Fatalf("instantiate walltime probe: %v", err) + } + results, err := module.ExportedFunction("now").Call(ctx) + if err != nil { + t.Fatalf("read WASI walltime: %v", err) + } + after := time.Now() + got := time.Unix(0, int64(results[0])) + if got.Before(before.Add(-time.Second)) || got.After(after.Add(time.Second)) { + t.Fatalf("WASI walltime = %s, want system time between %s and %s", got, before, after) + } +} diff --git a/easytier-go/internal/engine/packet_conn.go b/easytier-go/internal/engine/packet_conn.go new file mode 100644 index 00000000..6e1fe5b7 --- /dev/null +++ b/easytier-go/internal/engine/packet_conn.go @@ -0,0 +1,284 @@ +package engine + +import ( + "context" + "io" + "net" + "net/netip" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" +) + +type packetConn struct { + instance *Instance + resource coreabi.ResourceID + local netip.AddrPort + + readMu sync.Mutex + writeMu sync.Mutex + + closed atomic.Bool + closeOnce sync.Once + closeDone chan struct{} + closeErr error +} + +type datagramConn struct { + packet *packetConn + peer netip.AddrPort +} + +var _ net.Conn = (*datagramConn)(nil) + +func newPacketConn( + instance *Instance, + result coreabi.OperationResult, +) *packetConn { + return &packetConn{ + instance: instance, + resource: result.Resource, + local: result.Local, + closeDone: make(chan struct{}), + } +} + +func (conn *packetConn) ReadFrom(buffer []byte) (int, net.Addr, error) { + conn.readMu.Lock() + defer conn.readMu.Unlock() + if conn.closed.Load() { + return 0, nil, net.ErrClosed + } + maximum := len(buffer) + if maximum > maxDataPlaneTransfer { + maximum = maxDataPlaneTransfer + } + result, err := conn.instance.performOperation( + context.Background(), + coreabi.OperationUDPReceive, + func( + callCtx context.Context, + core dataPlaneCore, + ) (coreabi.OperationID, error) { + return core.SubmitUDPReceive( + callCtx, + conn.resource, + uint32(maximum), + ) + }, + ) + if err != nil { + return 0, nil, normalizeDeadlineError(err) + } + n := copy(buffer, result.Data) + peer := net.UDPAddrFromAddrPort(result.Peer) + if result.Truncated { + return n, peer, io.ErrShortBuffer + } + return n, peer, nil +} + +func (conn *packetConn) WriteTo(buffer []byte, address net.Addr) (int, error) { + conn.writeMu.Lock() + defer conn.writeMu.Unlock() + if conn.closed.Load() { + return 0, net.ErrClosed + } + if len(buffer) > maxDataPlaneTransfer { + return 0, syscall.EMSGSIZE + } + peer, err := udpAddrPort(address) + if err != nil { + return 0, err + } + result, err := conn.instance.performOperation( + context.Background(), + coreabi.OperationUDPSend, + func( + callCtx context.Context, + core dataPlaneCore, + ) (coreabi.OperationID, error) { + return core.SubmitUDPSend( + callCtx, + conn.resource, + peer, + buffer, + ) + }, + ) + if err != nil { + return 0, normalizeDeadlineError(err) + } + if result.Length != len(buffer) { + return result.Length, io.ErrShortWrite + } + return result.Length, nil +} + +func (conn *packetConn) Close() error { + conn.closeOnce.Do(func() { + conn.closed.Store(true) + conn.closeErr = conn.instance.closeDataPlaneResource(conn.resource) + close(conn.closeDone) + }) + <-conn.closeDone + return conn.closeErr +} + +func (conn *packetConn) LocalAddr() net.Addr { + return net.UDPAddrFromAddrPort(conn.local) +} + +func (conn *packetConn) SetDeadline(deadline time.Time) error { + if conn.closed.Load() { + return net.ErrClosed + } + return conn.instance.setDataPlaneResourceDeadline( + conn.resource, + coreabi.DeadlineRead|coreabi.DeadlineWrite, + deadline, + ) +} + +func (conn *packetConn) SetReadDeadline(deadline time.Time) error { + if conn.closed.Load() { + return net.ErrClosed + } + return conn.instance.setDataPlaneResourceDeadline( + conn.resource, + coreabi.DeadlineRead, + deadline, + ) +} + +func (conn *packetConn) SetWriteDeadline(deadline time.Time) error { + if conn.closed.Load() { + return net.ErrClosed + } + return conn.instance.setDataPlaneResourceDeadline( + conn.resource, + coreabi.DeadlineWrite, + deadline, + ) +} + +func udpAddrPort(address net.Addr) (netip.AddrPort, error) { + if address == nil { + return netip.AddrPort{}, &net.AddrError{ + Err: "address is nil", + } + } + udp, ok := address.(*net.UDPAddr) + if !ok { + return netip.AddrPort{}, &net.AddrError{ + Err: "address is not UDP", + Addr: address.String(), + } + } + peer := udp.AddrPort() + if peer.IsValid() { + peer = netip.AddrPortFrom(peer.Addr().Unmap(), peer.Port()) + } + if !peer.IsValid() || !peer.Addr().Is4() { + return netip.AddrPort{}, &net.AddrError{ + Err: "EasyTier data plane requires an IPv4 address", + Addr: address.String(), + } + } + return peer, nil +} + +func (instance *Instance) ListenPacket(port uint16) (net.PacketConn, error) { + result, err := instance.performOperation( + context.Background(), + coreabi.OperationUDPBind, + func( + ctx context.Context, + core dataPlaneCore, + ) (coreabi.OperationID, error) { + return core.SubmitUDPBind(ctx, port, ^uint64(0)) + }, + ) + if err != nil { + return nil, err + } + return newPacketConn(instance, result), nil +} + +func (instance *Instance) DialUDP( + ctx context.Context, + peer netip.AddrPort, +) (net.Conn, error) { + timeout, err := contextTimeoutMillis(ctx) + if err != nil { + return nil, normalizeDeadlineError(err) + } + result, err := instance.performOperation( + ctx, + coreabi.OperationUDPBind, + func( + callCtx context.Context, + core dataPlaneCore, + ) (coreabi.OperationID, error) { + return core.SubmitUDPBind(callCtx, 0, timeout) + }, + ) + if err != nil { + return nil, normalizeDeadlineError(err) + } + return &datagramConn{ + packet: newPacketConn(instance, result), + peer: peer, + }, nil +} + +func (conn *datagramConn) Read(buffer []byte) (int, error) { + if len(buffer) == 0 { + return 0, nil + } + for { + n, source, err := conn.packet.ReadFrom(buffer) + if source == nil { + return n, err + } + sourcePeer, addressErr := udpAddrPort(source) + if addressErr != nil { + return 0, addressErr + } + if sourcePeer != conn.peer { + continue + } + return n, err + } +} + +func (conn *datagramConn) Write(buffer []byte) (int, error) { + return conn.packet.WriteTo(buffer, net.UDPAddrFromAddrPort(conn.peer)) +} + +func (conn *datagramConn) Close() error { + return conn.packet.Close() +} + +func (conn *datagramConn) LocalAddr() net.Addr { + return conn.packet.LocalAddr() +} + +func (conn *datagramConn) RemoteAddr() net.Addr { + return net.UDPAddrFromAddrPort(conn.peer) +} + +func (conn *datagramConn) SetDeadline(deadline time.Time) error { + return conn.packet.SetDeadline(deadline) +} + +func (conn *datagramConn) SetReadDeadline(deadline time.Time) error { + return conn.packet.SetReadDeadline(deadline) +} + +func (conn *datagramConn) SetWriteDeadline(deadline time.Time) error { + return conn.packet.SetWriteDeadline(deadline) +} diff --git a/easytier-go/internal/engine/rpc.go b/easytier-go/internal/engine/rpc.go new file mode 100644 index 00000000..39d89b7b --- /dev/null +++ b/easytier-go/internal/engine/rpc.go @@ -0,0 +1,228 @@ +package engine + +import ( + "context" + "fmt" + "net" + + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" +) + +type rpcCore interface { + SubmitRPC( + context.Context, + []byte, + ) (coreabi.RPCOperationID, error) + TakeRPCResponse( + context.Context, + coreabi.RPCOperationID, + ) ([]byte, bool, error) + FreeRPCOperation(context.Context, coreabi.RPCOperationID) error +} + +type rpcCommandKind uint8 + +const ( + rpcSubmit rpcCommandKind = iota + 1 + rpcFree +) + +type rpcCommand struct { + kind rpcCommandKind + request []byte + operation coreabi.RPCOperationID + response chan rpcCommandResponse +} + +type rpcCommandResponse struct { + ticket rpcTicket + outcome rpcOutcome + completed bool + err error +} + +type rpcTicket struct { + id coreabi.RPCOperationID + result <-chan rpcOutcome +} + +type rpcOutcome struct { + response []byte + err error +} + +type pendingRPC struct { + result chan rpcOutcome +} + +func (instance *Instance) RPC( + ctx context.Context, + encodedRequest []byte, +) ([]byte, error) { + if ctx == nil { + return nil, fmt.Errorf("submit EasyTier RPC with nil context") + } + response := make(chan rpcCommandResponse, 1) + request := rpcCommand{ + kind: rpcSubmit, + request: encodedRequest, + response: response, + } + select { + case instance.rpcCommands <- request: + case <-instance.done: + return nil, net.ErrClosed + case <-instance.closeRequested: + return nil, net.ErrClosed + case <-ctx.Done(): + return nil, ctx.Err() + } + + var ticket rpcTicket + select { + case submitted := <-response: + if submitted.err != nil { + return nil, submitted.err + } + if submitted.completed { + return submitted.outcome.response, submitted.outcome.err + } + ticket = submitted.ticket + case <-instance.done: + return nil, net.ErrClosed + case <-ctx.Done(): + // The driver has accepted the request and may already have submitted + // it. Wait for its operation ID so cancellation cannot leak it. + select { + case submitted := <-response: + if submitted.err != nil { + return nil, submitted.err + } + if submitted.completed { + return submitted.outcome.response, submitted.outcome.err + } + ticket = submitted.ticket + case <-instance.done: + return nil, net.ErrClosed + } + return instance.freeRPCAndWait(ctx, ticket) + } + + select { + case outcome := <-ticket.result: + return outcome.response, outcome.err + case <-ctx.Done(): + return instance.freeRPCAndWait(ctx, ticket) + case <-instance.done: + return nil, net.ErrClosed + case <-instance.closeRequested: + return nil, net.ErrClosed + } +} + +func (instance *Instance) freeRPCAndWait( + cancelled context.Context, + ticket rpcTicket, +) ([]byte, error) { + response := make(chan rpcCommandResponse, 1) + request := rpcCommand{ + kind: rpcFree, + operation: ticket.id, + response: response, + } + select { + case instance.rpcCommands <- request: + case outcome := <-ticket.result: + return outcome.response, outcome.err + case <-instance.done: + return nil, net.ErrClosed + case <-instance.closeRequested: + return nil, net.ErrClosed + } + select { + case result := <-response: + if result.err != nil { + return nil, result.err + } + case outcome := <-ticket.result: + return outcome.response, outcome.err + case <-instance.done: + return nil, net.ErrClosed + case <-instance.closeRequested: + return nil, net.ErrClosed + } + select { + case outcome := <-ticket.result: + return outcome.response, outcome.err + default: + return nil, cancelled.Err() + } +} + +func (instance *Instance) handleRPCCommand( + request rpcCommand, +) rpcCommandResponse { + instance.host.guestMu.Lock() + defer instance.host.guestMu.Unlock() + switch request.kind { + case rpcSubmit: + operation, err := instance.rpc.SubmitRPC( + instance.ctx, + request.request, + ) + if err != nil { + return rpcCommandResponse{err: err} + } + result := make(chan rpcOutcome, 1) + instance.pendingRPCs[operation] = &pendingRPC{result: result} + return rpcCommandResponse{ticket: rpcTicket{ + id: operation, + result: result, + }} + case rpcFree: + if _, exists := instance.pendingRPCs[request.operation]; !exists { + return rpcCommandResponse{} + } + if err := instance.rpc.FreeRPCOperation( + instance.ctx, + request.operation, + ); err != nil { + return rpcCommandResponse{err: err} + } + delete(instance.pendingRPCs, request.operation) + return rpcCommandResponse{} + default: + return rpcCommandResponse{ + err: fmt.Errorf("unknown RPC command %d", request.kind), + } + } +} + +// takeRPCResponses runs with host.guestMu held. +func (instance *Instance) takeRPCResponses() error { + if instance.rpc == nil || len(instance.pendingRPCs) == 0 { + return nil + } + for operation, pending := range instance.pendingRPCs { + response, ready, err := instance.rpc.TakeRPCResponse( + instance.ctx, + operation, + ) + if err != nil { + return err + } + if !ready { + continue + } + delete(instance.pendingRPCs, operation) + pending.result <- rpcOutcome{response: response} + } + return nil +} + +func (instance *Instance) failPendingRPCs(err error) { + for operation, pending := range instance.pendingRPCs { + pending.result <- rpcOutcome{err: err} + delete(instance.pendingRPCs, operation) + } +} diff --git a/easytier-go/internal/engine/rpc_test.go b/easytier-go/internal/engine/rpc_test.go new file mode 100644 index 00000000..41ed22ed --- /dev/null +++ b/easytier-go/internal/engine/rpc_test.go @@ -0,0 +1,128 @@ +package engine + +import ( + "bytes" + "context" + "errors" + "math" + "testing" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" +) + +type readyRPC struct { + response []byte + taken []coreabi.RPCOperationID +} + +func (*readyRPC) SubmitRPC( + context.Context, + []byte, +) (coreabi.RPCOperationID, error) { + return 0, errors.New("unexpected RPC submission") +} + +func (rpc *readyRPC) TakeRPCResponse( + _ context.Context, + operation coreabi.RPCOperationID, +) ([]byte, bool, error) { + rpc.taken = append(rpc.taken, operation) + return append([]byte(nil), rpc.response...), true, nil +} + +func (*readyRPC) FreeRPCOperation( + context.Context, + coreabi.RPCOperationID, +) error { + return errors.New("unexpected RPC free") +} + +func TestDriveTakesReadyRPCResponse(t *testing.T) { + core := &recordingCore{} + rpc := &readyRPC{response: []byte{1, 2, 3}} + result := make(chan rpcOutcome, 1) + instance := &Instance{ + host: &Host{}, + ctx: context.Background(), + core: core, + rpc: rpc, + pendingRPCs: map[coreabi.RPCOperationID]*pendingRPC{ + 9: {result: result}, + }, + running: make(chan struct{}), + stopped: make(chan struct{}), + } + + deadline, err := instance.drive(false) + if err != nil { + t.Fatalf("drive RPC response: %v", err) + } + if deadline != math.MaxInt64 { + t.Fatalf("next deadline = %d, want no deadline", deadline) + } + outcome := <-result + if outcome.err != nil { + t.Fatalf("RPC outcome: %v", outcome.err) + } + if !bytes.Equal(outcome.response, rpc.response) { + t.Fatalf("RPC response = %x, want %x", outcome.response, rpc.response) + } + if len(instance.pendingRPCs) != 0 { + t.Fatalf("pending RPCs = %d, want 0", len(instance.pendingRPCs)) + } + if len(rpc.taken) != 1 || rpc.taken[0] != 9 { + t.Fatalf("taken RPCs = %v, want [9]", rpc.taken) + } +} + +func TestRPCCancellationFreesSubmittedOperation(t *testing.T) { + instance := &Instance{ + rpcCommands: make(chan rpcCommand), + closeRequested: make(chan struct{}), + done: make(chan struct{}), + } + ctx, cancel := context.WithCancel(context.Background()) + callResult := make(chan error, 1) + go func() { + _, err := instance.RPC(ctx, []byte{1, 2, 3}) + callResult <- err + }() + + var submit rpcCommand + select { + case submit = <-instance.rpcCommands: + case <-time.After(time.Second): + t.Fatal("RPC did not enqueue submission") + } + result := make(chan rpcOutcome, 1) + cancel() + submit.response <- rpcCommandResponse{ticket: rpcTicket{ + id: 17, + result: result, + }} + + var free rpcCommand + select { + case free = <-instance.rpcCommands: + case <-time.After(time.Second): + t.Fatal("RPC cancellation did not enqueue free") + } + if free.kind != rpcFree || free.operation != 17 { + t.Fatalf( + "RPC cancellation command = (kind=%d, operation=%d)", + free.kind, + free.operation, + ) + } + free.response <- rpcCommandResponse{} + + select { + case err := <-callResult: + if !errors.Is(err, context.Canceled) { + t.Fatalf("cancelled RPC error = %v, want context cancellation", err) + } + case <-time.After(time.Second): + t.Fatal("cancelled RPC did not return") + } +} diff --git a/easytier-go/internal/engine/web_client.go b/easytier-go/internal/engine/web_client.go new file mode 100644 index 00000000..b17a18ea --- /dev/null +++ b/easytier-go/internal/engine/web_client.go @@ -0,0 +1,182 @@ +package engine + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/internal/contextutil" + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" +) + +type WebClientOptions struct { + Endpoint string + MachineID string + Hostname string + SecureMode bool + OSType string +} + +type WebClient struct { + host *Host + ctx context.Context + cancel context.CancelFunc + core *coreabi.WebClient + + completions chan struct{} + closeRequested chan struct{} + closeOnce sync.Once + done chan struct{} + connected atomic.Bool + + errMu sync.Mutex + terminalErr error +} + +func (host *Host) CreateWebClient( + ctx context.Context, + options WebClientOptions, +) (*WebClient, error) { + if host == nil { + return nil, fmt.Errorf("create WebClient with nil EasyTier engine host") + } + if ctx == nil { + return nil, fmt.Errorf("create EasyTier WebClient with nil context") + } + envelope, err := encodeWebClientEnvelope(options, host.options.Services.Snapshot) + if err != nil { + return nil, err + } + + host.mu.Lock() + defer host.mu.Unlock() + if host.closed { + return nil, fmt.Errorf("create WebClient with closed EasyTier engine host") + } + if host.webClient != nil { + return nil, fmt.Errorf("EasyTier WebClient is already running") + } + host.guestMu.Lock() + core, err := coreabi.NewWebClient(host.module) + if err == nil { + err = core.Create(ctx, envelope) + } + host.guestMu.Unlock() + if err != nil { + return nil, err + } + + lifetime, cancel := context.WithCancel(host.ctx) + client := &WebClient{ + host: host, + ctx: lifetime, + cancel: cancel, + core: core, + completions: make(chan struct{}, 1), + closeRequested: make(chan struct{}), + done: make(chan struct{}), + } + host.webClient = client + go client.run() + return client, nil +} + +func (client *WebClient) Connected() bool { + return client != nil && client.connected.Load() +} + +func (client *WebClient) Close(ctx context.Context) error { + if client == nil { + return nil + } + if ctx == nil { + return fmt.Errorf("close EasyTier WebClient with nil context") + } + client.closeOnce.Do(func() { close(client.closeRequested) }) + select { + case <-client.done: + return client.terminalError() + case <-ctx.Done(): + return ctx.Err() + } +} + +func (client *WebClient) run() { + runErr := client.driveLoop() + cleanupErr := client.shutdown() + client.errMu.Lock() + client.terminalErr = errors.Join(runErr, cleanupErr) + client.errMu.Unlock() + close(client.done) +} + +func (client *WebClient) driveLoop() error { + deadline := int64(0) + timer := time.NewTimer(0) + stopTimer(timer) + defer stopTimer(timer) + for { + select { + case <-client.completions: + stopTimer(timer) + next, err := client.drive(true) + if err != nil { + return err + } + deadline = next + case <-deadlineTimer(timer, deadline): + next, err := client.drive(false) + if err != nil { + return err + } + deadline = next + case <-client.closeRequested: + return nil + case <-client.ctx.Done(): + return client.ctx.Err() + } + } +} + +func (client *WebClient) drive(notify bool) (int64, error) { + client.host.guestMu.Lock() + defer client.host.guestMu.Unlock() + if notify { + if err := client.core.NotifyCompletions(client.ctx); err != nil { + return 0, err + } + } + if err := client.core.Drive(client.ctx); err != nil { + return 0, err + } + connected, err := client.core.IsConnected(client.ctx) + if err != nil { + return 0, err + } + client.connected.Store(connected) + return client.core.NextDeadline(client.ctx) +} + +func (client *WebClient) shutdown() error { + cleanupContext, cancel := context.WithTimeout( + contextutil.WithoutCancel(client.ctx), + 5*time.Second, + ) + defer cancel() + client.host.guestMu.Lock() + err := client.core.Drop(cleanupContext) + client.host.guestMu.Unlock() + client.connected.Store(false) + client.cancel() + client.host.removeWebClient(client) + return err +} + +func (client *WebClient) terminalError() error { + client.errMu.Lock() + defer client.errMu.Unlock() + return client.terminalErr +} diff --git a/easytier-go/internal/host/host.go b/easytier-go/internal/host/host.go new file mode 100644 index 00000000..a86fc861 --- /dev/null +++ b/easytier-go/internal/host/host.go @@ -0,0 +1,417 @@ +package host + +import ( + "context" + "fmt" + "net" + "net/netip" + "strconv" + + "github.com/EasyTier/EasyTier/easytier-go/internal/artifact" + "github.com/EasyTier/EasyTier/easytier-go/internal/contextutil" + "github.com/EasyTier/EasyTier/easytier-go/internal/coreabi" + "github.com/EasyTier/EasyTier/easytier-go/internal/engine" + "github.com/EasyTier/EasyTier/easytier-go/platform" + "github.com/EasyTier/EasyTier/easytier-go/platform/netstd" + hostproto "github.com/EasyTier/EasyTier/easytier-go/proto" + "github.com/EasyTier/EasyTier/easytier-go/proto/api/manage" +) + +type State int32 + +const ( + StateCreated State = iota + StateStarting + StateRunning + StateStopping + StateStopped +) + +type Options struct { + Platform platform.Services + PacketQueueCapacity int +} + +type EmbeddedCoreInfo struct { + EasyTierCommit string + SHA256 string +} + +// Event is one best-effort notification emitted by an EasyTier instance. +type Event = engine.Event + +type Host struct { + engine *engine.Host + manager *instanceManager +} + +type Instance struct { + engine *engine.Instance + id string + manager *instanceManager +} + +func New(ctx context.Context, options Options) (*Host, error) { + if ctx == nil { + return nil, fmt.Errorf("create EasyTier host with nil context") + } + if artifact.Commit != hostproto.EasyTierCommit { + return nil, fmt.Errorf( + "embedded EasyTier commit %s does not match protobuf commit %s", + artifact.Commit, + hostproto.EasyTierCommit, + ) + } + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + manager := newInstanceManager() + runtime, err := engine.NewHost( + ctx, + engine.Options{ + Services: mergeServices(options.Platform), + PacketQueueCapacity: options.PacketQueueCapacity, + Management: manager.handle, + }, + ) + if err != nil { + return nil, err + } + host := &Host{engine: runtime, manager: manager} + manager.host = host + return host, nil +} + +func CoreInfo() EmbeddedCoreInfo { + return EmbeddedCoreInfo{ + EasyTierCommit: artifact.Commit, + SHA256: artifact.SHA256, + } +} + +// CreateInstance creates an EasyTier instance from a built configuration. +func (host *Host) CreateInstance( + ctx context.Context, + config InstanceConfig, +) (*Instance, error) { + if host == nil { + return nil, fmt.Errorf("create instance with nil EasyTier host") + } + configTOML, err := encodeInstanceConfig(config) + if err != nil { + return nil, err + } + id, idString, err := newInstanceUUID() + if err != nil { + return nil, err + } + runtime, err := host.engine.CreateInstance( + ctx, + bindInstanceIdentity(configTOML, idString, config.document.networkName), + ) + if err != nil { + return nil, err + } + instance := &Instance{ + engine: runtime, + id: idString, + manager: host.manager, + } + if err := host.manager.register(&managedInstance{ + id: id, + instance: instance, + owner: instanceOwnerApplication, + source: manage.ConfigSource_ConfigSourceUser, + name: config.document.networkName, + }); err != nil { + _ = runtime.Close(contextutil.WithoutCancel(ctx)) + return nil, err + } + return instance, nil +} + +// Instances returns a stable snapshot of all application- and Web-owned instances. +func (host *Host) Instances() []*Instance { + if host == nil || host.manager == nil { + return nil + } + entries := host.manager.snapshot() + instances := make([]*Instance, len(entries)) + for index, entry := range entries { + instances[index] = entry.instance + } + return instances +} + +func (host *Host) Close(ctx context.Context) error { + if host == nil { + return nil + } + if ctx == nil { + return fmt.Errorf("close EasyTier host with nil context") + } + err := host.engine.Close(ctx) + if ctx.Err() == nil { + host.manager.clear() + } + return err +} + +func (instance *Instance) Start(ctx context.Context) error { + if instance == nil || instance.engine == nil { + return fmt.Errorf("start nil EasyTier instance") + } + return instance.engine.Start(ctx) +} + +func (instance *Instance) Stop(ctx context.Context) error { + if instance == nil || instance.engine == nil { + return fmt.Errorf("stop nil EasyTier instance") + } + return instance.engine.Stop(ctx) +} + +func (instance *Instance) SendPacket(ctx context.Context, packet []byte) error { + if instance == nil || instance.engine == nil { + return fmt.Errorf("send packet through nil EasyTier instance") + } + return instance.engine.SendPacket(ctx, packet) +} + +func (instance *Instance) ReceivePacket(ctx context.Context) ([]byte, error) { + if instance == nil || instance.engine == nil { + return nil, fmt.Errorf("receive packet from nil EasyTier instance") + } + return instance.engine.ReceivePacket(ctx) +} + +// Events returns the instance's bounded event stream. +// +// Slow consumers may miss events. The channel closes with the instance. +func (instance *Instance) Events() <-chan Event { + if instance == nil || instance.engine == nil { + return nil + } + return instance.engine.Events() +} + +func (instance *Instance) Dial( + ctx context.Context, + network string, + address string, +) (net.Conn, error) { + if instance == nil || instance.engine == nil { + return nil, fmt.Errorf("dial through nil EasyTier instance") + } + if ctx == nil { + return nil, fmt.Errorf("dial through EasyTier with nil context") + } + switch network { + case "tcp", "tcp4", "udp", "udp4": + default: + return nil, net.UnknownNetworkError(network) + } + peer, err := parseDialAddress(address) + if err != nil { + return nil, err + } + var connection net.Conn + switch network { + case "tcp", "tcp4": + connection, err = instance.engine.Dial(ctx, peer) + case "udp", "udp4": + connection, err = instance.engine.DialUDP(ctx, peer) + } + if err != nil { + var target net.Addr = net.TCPAddrFromAddrPort(peer) + if network == "udp" || network == "udp4" { + target = net.UDPAddrFromAddrPort(peer) + } + return nil, &net.OpError{ + Op: "dial", + Net: network, + Addr: target, + Err: err, + } + } + return connection, nil +} + +func (instance *Instance) Listen( + network string, + address string, +) (net.Listener, error) { + if instance == nil || instance.engine == nil { + return nil, fmt.Errorf("listen through nil EasyTier instance") + } + if err := requireNetwork(network, "tcp"); err != nil { + return nil, err + } + port, err := parseBindPort(address) + if err != nil { + return nil, err + } + listener, err := instance.engine.Listen(port) + if err != nil { + return nil, &net.OpError{ + Op: "listen", + Net: network, + Addr: &net.TCPAddr{ + IP: net.IPv4zero, + Port: int(port), + }, + Err: err, + } + } + return listener, nil +} + +func (instance *Instance) ListenPacket( + network string, + address string, +) (net.PacketConn, error) { + if instance == nil || instance.engine == nil { + return nil, fmt.Errorf("listen for packets through nil EasyTier instance") + } + if err := requireNetwork(network, "udp"); err != nil { + return nil, err + } + port, err := parseBindPort(address) + if err != nil { + return nil, err + } + connection, err := instance.engine.ListenPacket(port) + if err != nil { + return nil, &net.OpError{ + Op: "listen", + Net: network, + Addr: &net.UDPAddr{ + IP: net.IPv4zero, + Port: int(port), + }, + Err: err, + } + } + return connection, nil +} + +func (instance *Instance) Wait(ctx context.Context) error { + if instance == nil || instance.engine == nil { + return fmt.Errorf("wait for nil EasyTier instance") + } + return instance.engine.Wait(ctx) +} + +func (instance *Instance) State() State { + if instance == nil || instance.engine == nil { + return StateStopped + } + return mapState(instance.engine.State()) +} + +// ID returns the instance's stable UUID. +func (instance *Instance) ID() string { + if instance == nil { + return "" + } + return instance.id +} + +func (instance *Instance) Close(ctx context.Context) error { + if instance == nil || instance.engine == nil { + return nil + } + err := instance.engine.Close(ctx) + if ctx != nil && ctx.Err() == nil && instance.manager != nil { + instance.manager.remove(instance) + } + return err +} + +func mergeServices(configured platform.Services) platform.Services { + services := netstd.Services() + if configured.Sockets != nil { + services.Sockets = configured.Sockets + } + if configured.DNS != nil { + services.DNS = configured.DNS + } + if configured.Environment != nil { + services.Environment = configured.Environment + } + services.Snapshot = configured.Snapshot + return services +} + +func mapState(state coreabi.State) State { + switch state { + case coreabi.StateCreated: + return StateCreated + case coreabi.StateStarting: + return StateStarting + case coreabi.StateRunning: + return StateRunning + case coreabi.StateStopping: + return StateStopping + case coreabi.StateStopped: + return StateStopped + default: + return StateStopped + } +} + +func requireNetwork(network string, protocol string) error { + if network == protocol || network == protocol+"4" { + return nil + } + return net.UnknownNetworkError(network) +} + +func parseDialAddress(address string) (netip.AddrPort, error) { + host, port, err := splitAddress(address) + if err != nil { + return netip.AddrPort{}, err + } + ip, err := netip.ParseAddr(host) + if err != nil || !ip.Is4() { + return netip.AddrPort{}, &net.AddrError{ + Err: "EasyTier data plane requires an IPv4 literal", + Addr: address, + } + } + return netip.AddrPortFrom(ip, port), nil +} + +func parseBindPort(address string) (uint16, error) { + host, port, err := splitAddress(address) + if err != nil { + return 0, err + } + if host != "" { + ip, parseErr := netip.ParseAddr(host) + if parseErr != nil || !ip.Is4() || !ip.IsUnspecified() { + return 0, &net.AddrError{ + Err: "EasyTier listeners bind all overlay IPv4 addresses", + Addr: address, + } + } + } + return port, nil +} + +func splitAddress(address string) (string, uint16, error) { + host, service, err := net.SplitHostPort(address) + if err != nil { + return "", 0, err + } + port, err := strconv.ParseUint(service, 10, 16) + if err != nil { + return "", 0, &net.AddrError{ + Err: "port must be a decimal number from 0 to 65535", + Addr: address, + } + } + return host, uint16(port), nil +} diff --git a/easytier-go/internal/host/instance_config.go b/easytier-go/internal/host/instance_config.go new file mode 100644 index 00000000..1cfcd484 --- /dev/null +++ b/easytier-go/internal/host/instance_config.go @@ -0,0 +1,607 @@ +package host + +import ( + "crypto/ecdh" + "crypto/rand" + "fmt" + "net/netip" + "net/url" + "strconv" + "strings" + "unicode" + "unicode/utf8" +) + +// P2PPolicy controls when an instance attempts peer-to-peer connections. +// +// Disable normally suppresses P2P, but a peer advertising Need may still +// request it. Need advertises that this instance requires P2P and may override +// a peer's Disable setting. Lazy defers background P2P until traffic needs it, +// except for peers advertising Need. +type P2PPolicy struct { + Disable bool + Need bool + Lazy bool +} + +// HolePunchingPolicy selects the hole-punching methods available to an +// instance. SymmetricUDP requires UDP. +type HolePunchingPolicy struct { + TCP bool + UDP bool + SymmetricUDP bool +} + +// PortForwardProtocol identifies the transport used by a port-forward rule. +type PortForwardProtocol string + +const ( + PortForwardTCP PortForwardProtocol = "tcp" + PortForwardUDP PortForwardProtocol = "udp" +) + +// PortForwardConfig exposes a host socket through the EasyTier data plane. +type PortForwardConfig struct { + Protocol PortForwardProtocol + Bind netip.AddrPort + Destination netip.AddrPort +} + +// InstanceConfig is an immutable EasyTier instance configuration. +// +// Its zero value is invalid. Construct one with InstanceConfigBuilder. +type InstanceConfig struct { + document *instanceConfigDocument +} + +// InstanceConfigBuilder builds a validated InstanceConfig. +// +// A builder is not safe for concurrent use. +type InstanceConfigBuilder struct { + document instanceConfigDocument + networkSecretSet bool +} + +type instanceConfigDocument struct { + networkName string + networkSecret string + hostname *string + ipv4 *netip.Prefix + peers []string + listeners []string + portForwards []PortForwardConfig + stunServers []string + stunServersSet bool + stunServersV6 []string + stunServersV6Set bool + p2p *P2PPolicy + holePunching *HolePunchingPolicy + encryption *bool + secureMode secureMode + securePrivateKey []byte +} + +type secureMode uint8 + +const ( + secureModeDisabled secureMode = iota + secureModeAutomatic + secureModeManual +) + +// NewInstanceConfigBuilder starts a configuration for networkName. +func NewInstanceConfigBuilder(networkName string) *InstanceConfigBuilder { + return &InstanceConfigBuilder{ + document: instanceConfigDocument{networkName: networkName}, + } +} + +// NetworkSecret selects shared-secret network authentication. +// +// An empty secret is valid unless secure mode is enabled. +func (builder *InstanceConfigBuilder) NetworkSecret( + secret string, +) *InstanceConfigBuilder { + builder.document.networkSecret = secret + builder.networkSecretSet = true + return builder +} + +// Hostname sets the hostname advertised by this instance. +func (builder *InstanceConfigBuilder) Hostname( + hostname string, +) *InstanceConfigBuilder { + builder.document.hostname = &hostname + return builder +} + +// IPv4 sets the instance's virtual IPv4 address and network prefix. +func (builder *InstanceConfigBuilder) IPv4( + prefix netip.Prefix, +) *InstanceConfigBuilder { + builder.document.ipv4 = &prefix + return builder +} + +// AddPeers appends TCP or UDP peer endpoints. +func (builder *InstanceConfigBuilder) AddPeers( + uris ...string, +) *InstanceConfigBuilder { + builder.document.peers = append(builder.document.peers, uris...) + return builder +} + +// AddListeners appends TCP or UDP listener endpoints. +func (builder *InstanceConfigBuilder) AddListeners( + uris ...string, +) *InstanceConfigBuilder { + builder.document.listeners = append(builder.document.listeners, uris...) + return builder +} + +// AddPortForwards appends TCP or UDP host-to-overlay forwarding rules. +func (builder *InstanceConfigBuilder) AddPortForwards( + forwards ...PortForwardConfig, +) *InstanceConfigBuilder { + builder.document.portForwards = append(builder.document.portForwards, forwards...) + return builder +} + +// STUNServers replaces the IPv4 UDP STUN server list. +// +// Calling STUNServers with no arguments explicitly disables the list. Omitting +// the call uses the embedded core's defaults. +func (builder *InstanceConfigBuilder) STUNServers( + servers ...string, +) *InstanceConfigBuilder { + builder.document.stunServers = append([]string(nil), servers...) + builder.document.stunServersSet = true + return builder +} + +// STUNServersV6 replaces the IPv6 UDP STUN server list. +// +// Calling STUNServersV6 with no arguments explicitly disables the list. +// Omitting the call uses the embedded core's defaults. +func (builder *InstanceConfigBuilder) STUNServersV6( + servers ...string, +) *InstanceConfigBuilder { + builder.document.stunServersV6 = append([]string(nil), servers...) + builder.document.stunServersV6Set = true + return builder +} + +// P2P sets the instance's P2P connection policy. +func (builder *InstanceConfigBuilder) P2P( + policy P2PPolicy, +) *InstanceConfigBuilder { + builder.document.p2p = &policy + return builder +} + +// HolePunching sets the instance's TCP and UDP hole-punching policy. +func (builder *InstanceConfigBuilder) HolePunching( + policy HolePunchingPolicy, +) *InstanceConfigBuilder { + builder.document.holePunching = &policy + return builder +} + +// Encryption explicitly enables or disables EasyTier data encryption. +func (builder *InstanceConfigBuilder) Encryption( + enabled bool, +) *InstanceConfigBuilder { + builder.document.encryption = &enabled + return builder +} + +// SecureMode enables secure mode with an automatically generated X25519 key. +func (builder *InstanceConfigBuilder) SecureMode() *InstanceConfigBuilder { + builder.document.secureMode = secureModeAutomatic + builder.document.securePrivateKey = nil + return builder +} + +// SecureModeWithPrivateKey enables secure mode with a caller-provided raw +// 32-byte X25519 private key. The key is copied immediately. +func (builder *InstanceConfigBuilder) SecureModeWithPrivateKey( + privateKey []byte, +) *InstanceConfigBuilder { + builder.document.secureMode = secureModeManual + builder.document.securePrivateKey = append([]byte(nil), privateKey...) + return builder +} + +// Build validates the builder and returns an immutable configuration. +func (builder *InstanceConfigBuilder) Build() (InstanceConfig, error) { + if builder == nil { + return InstanceConfig{}, invalidInstanceConfig("", "builder is nil") + } + if err := builder.validateIdentity(); err != nil { + return InstanceConfig{}, err + } + + document := builder.document.clone() + if err := validateHostname(document.hostname); err != nil { + return InstanceConfig{}, err + } + if err := validateIPv4(document.ipv4); err != nil { + return InstanceConfig{}, err + } + if err := normalizeEndpoints(document.peers, false, "peers"); err != nil { + return InstanceConfig{}, err + } + if err := normalizeEndpoints(document.listeners, true, "listeners"); err != nil { + return InstanceConfig{}, err + } + if err := validatePortForwards(document.portForwards); err != nil { + return InstanceConfig{}, err + } + if document.stunServersSet { + if err := validateSTUNServers(document.stunServers, false, "stun_servers"); err != nil { + return InstanceConfig{}, err + } + } + if document.stunServersV6Set { + if err := validateSTUNServers( + document.stunServersV6, + true, + "stun_servers_v6", + ); err != nil { + return InstanceConfig{}, err + } + } + if document.holePunching != nil && + document.holePunching.SymmetricUDP && + !document.holePunching.UDP { + return InstanceConfig{}, invalidInstanceConfig( + "hole_punching.symmetric_udp", + "requires UDP hole punching", + ) + } + if document.secureMode != secureModeDisabled { + if document.networkSecret == "" { + return InstanceConfig{}, invalidInstanceConfig( + "secure_mode", + "requires a non-empty network secret", + ) + } + privateKey, err := builder.secureModePrivateKey() + if err != nil { + return InstanceConfig{}, err + } + document.securePrivateKey = privateKey + } + + return InstanceConfig{document: &document}, nil +} + +func (builder *InstanceConfigBuilder) validateIdentity() error { + if !utf8.ValidString(builder.document.networkName) { + return invalidInstanceConfig("network.name", "must be valid UTF-8") + } + if builder.document.networkName == "" { + return invalidInstanceConfig("network.name", "must not be empty") + } + if !builder.networkSecretSet { + return invalidInstanceConfig("network.secret", "must be explicitly set") + } + if !utf8.ValidString(builder.document.networkSecret) { + return invalidInstanceConfig("network.secret", "must be valid UTF-8") + } + return nil +} + +func (builder *InstanceConfigBuilder) secureModePrivateKey() ([]byte, error) { + switch builder.document.secureMode { + case secureModeAutomatic: + if len(builder.document.securePrivateKey) == 0 { + privateKey, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate secure mode X25519 key: %w", err) + } + builder.document.securePrivateKey = privateKey.Bytes() + } + case secureModeManual: + if len(builder.document.securePrivateKey) != 32 { + return nil, invalidInstanceConfig( + "secure_mode.private_key", + "must contain exactly 32 bytes", + ) + } + if _, err := ecdh.X25519().NewPrivateKey( + builder.document.securePrivateKey, + ); err != nil { + return nil, invalidInstanceConfig( + "secure_mode.private_key", + "is not a valid X25519 private key", + ) + } + default: + return nil, invalidInstanceConfig("secure_mode", "has an invalid mode") + } + return append([]byte(nil), builder.document.securePrivateKey...), nil +} + +func validateHostname(hostname *string) error { + if hostname == nil { + return nil + } + if !utf8.ValidString(*hostname) { + return invalidInstanceConfig("hostname", "must be valid UTF-8") + } + if *hostname == "" { + return invalidInstanceConfig("hostname", "must not be empty") + } + if utf8.RuneCountInString(*hostname) > 32 { + return invalidInstanceConfig("hostname", "must not exceed 32 characters") + } + for _, character := range *hostname { + if unicode.IsControl(character) { + return invalidInstanceConfig("hostname", "must not contain control characters") + } + } + return nil +} + +func validateIPv4(prefix *netip.Prefix) error { + if prefix == nil { + return nil + } + if !prefix.IsValid() || !prefix.Addr().Is4() { + return invalidInstanceConfig("ipv4", "must be a valid IPv4 prefix") + } + if prefix.Bits() == 32 { + return invalidInstanceConfig( + "ipv4", + "must include a network prefix shorter than /32", + ) + } + return nil +} + +func validatePortForwards(forwards []PortForwardConfig) error { + seen := make(map[PortForwardConfig]struct{}, len(forwards)) + for index, forward := range forwards { + field := fmt.Sprintf("port_forwards[%d]", index) + if forward.Protocol != PortForwardTCP && + forward.Protocol != PortForwardUDP { + return invalidInstanceConfig( + field+".protocol", + "must be TCP or UDP", + ) + } + if !forward.Bind.IsValid() || !forward.Bind.Addr().Is4() { + return invalidInstanceConfig( + field+".bind", + "must be a valid IPv4 socket address", + ) + } + if !forward.Destination.IsValid() || + !forward.Destination.Addr().Is4() { + return invalidInstanceConfig( + field+".destination", + "must be a valid IPv4 socket address", + ) + } + if forward.Destination.Port() == 0 { + return invalidInstanceConfig( + field+".destination", + "port must not be zero", + ) + } + if _, exists := seen[forward]; exists { + return invalidInstanceConfig(field, "duplicates another port forward") + } + seen[forward] = struct{}{} + } + return nil +} + +func normalizeEndpoints(values []string, listener bool, field string) error { + seen := make(map[string]struct{}, len(values)) + for index, value := range values { + normalized, err := normalizeEndpoint(value, listener) + if err != nil { + return invalidInstanceConfig( + fmt.Sprintf("%s[%d]", field, index), + err.Error(), + ) + } + if _, exists := seen[normalized]; exists { + return invalidInstanceConfig( + fmt.Sprintf("%s[%d]", field, index), + "duplicates another endpoint", + ) + } + seen[normalized] = struct{}{} + values[index] = normalized + } + return nil +} + +func normalizeEndpoint(value string, listener bool) (string, error) { + if !utf8.ValidString(value) { + return "", fmt.Errorf("must be valid UTF-8") + } + if value == "" || strings.TrimSpace(value) != value { + return "", fmt.Errorf("must be a non-empty URI without surrounding whitespace") + } + parsed, err := url.ParseRequestURI(value) + if err != nil || parsed.Scheme == "" || parsed.Host == "" { + return "", fmt.Errorf("must be an absolute TCP or UDP URI") + } + parsed.Scheme = strings.ToLower(parsed.Scheme) + if parsed.Scheme != "tcp" && parsed.Scheme != "udp" { + return "", fmt.Errorf("must use the TCP or UDP scheme") + } + if parsed.User != nil || + parsed.Opaque != "" || + parsed.Path != "" || + parsed.RawPath != "" || + parsed.RawQuery != "" || + parsed.ForceQuery || + parsed.Fragment != "" { + return "", fmt.Errorf("must not contain credentials, a path, query, or fragment") + } + host := parsed.Hostname() + if host == "" { + return "", fmt.Errorf("must include a host") + } + if port := parsed.Port(); port != "" { + value, parseErr := strconv.ParseUint(port, 10, 16) + if parseErr != nil || (!listener && value == 0) { + return "", fmt.Errorf("has an invalid port") + } + } + if listener { + if _, err := netip.ParseAddr(host); err != nil { + return "", fmt.Errorf("listener host must be an IP address") + } + } + return parsed.String(), nil +} + +func validateSTUNServers(values []string, ipv6 bool, field string) error { + seen := make(map[string]struct{}, len(values)) + for index, value := range values { + if err := validateSTUNServer(value, ipv6); err != nil { + return invalidInstanceConfig( + fmt.Sprintf("%s[%d]", field, index), + err.Error(), + ) + } + if _, exists := seen[value]; exists { + return invalidInstanceConfig( + fmt.Sprintf("%s[%d]", field, index), + "duplicates another STUN server", + ) + } + seen[value] = struct{}{} + } + return nil +} + +func validateSTUNServer(value string, ipv6 bool) error { + if !utf8.ValidString(value) { + return fmt.Errorf("must be valid UTF-8") + } + if value == "" || strings.TrimSpace(value) != value { + return fmt.Errorf("must be non-empty without surrounding whitespace") + } + if domain, found := strings.CutPrefix(value, "txt:"); found { + if !validSTUNHostname(domain) { + return fmt.Errorf("has an invalid TXT lookup name") + } + return nil + } + if address, err := netip.ParseAddr(value); err == nil { + return validateSTUNAddressFamily(address, ipv6) + } + if address, err := netip.ParseAddrPort(value); err == nil { + if address.Port() == 0 { + return fmt.Errorf("port must not be zero") + } + return validateSTUNAddressFamily(address.Addr(), ipv6) + } + + parsed, err := url.Parse("stun://" + value) + if err != nil || + parsed.Host == "" || + parsed.User != nil || + parsed.Path != "" || + parsed.RawQuery != "" || + parsed.Fragment != "" { + return fmt.Errorf("must be a host, IP address, socket address, or txt: name") + } + host := parsed.Hostname() + if !validSTUNHostname(host) { + return fmt.Errorf("has an invalid host") + } + if strings.HasSuffix(parsed.Host, ":") { + return fmt.Errorf("has an invalid port") + } + if port := parsed.Port(); port != "" { + value, parseErr := strconv.ParseUint(port, 10, 16) + if parseErr != nil || value == 0 { + return fmt.Errorf("has an invalid port") + } + } + if address, err := netip.ParseAddr(host); err == nil { + return validateSTUNAddressFamily(address, ipv6) + } + return nil +} + +func validSTUNHostname(value string) bool { + if value == "" || strings.ContainsAny(value, " \t\r\n/?#@[]:") { + return false + } + for _, character := range value { + if unicode.IsControl(character) { + return false + } + } + return true +} + +func validateSTUNAddressFamily(address netip.Addr, ipv6 bool) error { + if ipv6 { + if address.Is6() && !address.Is4In6() { + return nil + } + return fmt.Errorf("literal address must be IPv6") + } + if address.Is4() { + return nil + } + return fmt.Errorf("literal address must be IPv4") +} + +func invalidInstanceConfig(field string, message string) error { + if field == "" { + return fmt.Errorf("invalid EasyTier instance config: %s", message) + } + return fmt.Errorf("invalid EasyTier instance config %s: %s", field, message) +} + +func (document instanceConfigDocument) clone() instanceConfigDocument { + cloned := document + cloned.peers = append([]string(nil), document.peers...) + cloned.listeners = append([]string(nil), document.listeners...) + cloned.portForwards = append([]PortForwardConfig(nil), document.portForwards...) + cloned.stunServers = append([]string(nil), document.stunServers...) + cloned.stunServersV6 = append([]string(nil), document.stunServersV6...) + cloned.securePrivateKey = append([]byte(nil), document.securePrivateKey...) + if document.hostname != nil { + value := *document.hostname + cloned.hostname = &value + } + if document.ipv4 != nil { + value := *document.ipv4 + cloned.ipv4 = &value + } + if document.p2p != nil { + value := *document.p2p + cloned.p2p = &value + } + if document.holePunching != nil { + value := *document.holePunching + cloned.holePunching = &value + } + if document.encryption != nil { + value := *document.encryption + cloned.encryption = &value + } + return cloned +} + +// Format prevents configuration secrets and private keys from being printed. +func (config InstanceConfig) Format(state fmt.State, _ rune) { + _, _ = state.Write([]byte("InstanceConfig{}")) +} + +// Format prevents configuration secrets and private keys from being printed. +func (builder InstanceConfigBuilder) Format(state fmt.State, _ rune) { + _, _ = state.Write([]byte("InstanceConfigBuilder{}")) +} diff --git a/easytier-go/internal/host/instance_config_test.go b/easytier-go/internal/host/instance_config_test.go new file mode 100644 index 00000000..f525248c --- /dev/null +++ b/easytier-go/internal/host/instance_config_test.go @@ -0,0 +1,534 @@ +package host + +import ( + "bytes" + "crypto/ecdh" + "encoding/base64" + "fmt" + "net/netip" + "strings" + "testing" +) + +func TestInstanceConfigEncodesSupportedSettings(t *testing.T) { + privateKeyBytes := make([]byte, 32) + for index := range privateKeyBytes { + privateKeyBytes[index] = byte(index + 1) + } + privateKey, err := ecdh.X25519().NewPrivateKey(privateKeyBytes) + if err != nil { + t.Fatalf("create expected private key: %v", err) + } + + config, err := NewInstanceConfigBuilder(`office "blue"`). + NetworkSecret("line1\nline2"). + Hostname(`node "blue"`). + IPv4(netip.MustParsePrefix("10.144.0.10/24")). + AddPeers( + "TCP://peer.example.com:11010", + "udp://203.0.113.20", + ). + AddListeners( + "tcp://0.0.0.0:11010", + "udp://[::]:0", + ). + AddPortForwards( + PortForwardConfig{ + Protocol: PortForwardTCP, + Bind: netip.MustParseAddrPort("127.0.0.1:5202"), + Destination: netip.MustParseAddrPort("10.144.0.20:5201"), + }, + PortForwardConfig{ + Protocol: PortForwardUDP, + Bind: netip.MustParseAddrPort("0.0.0.0:5203"), + Destination: netip.MustParseAddrPort("10.144.0.21:5201"), + }, + ). + STUNServers( + "stun.example.com", + "192.0.2.1:3478", + "txt:_stun.example.com", + ). + STUNServersV6( + "2001:db8::1", + "[2001:db8::2]:3478", + ). + P2P(P2PPolicy{Disable: true, Need: true, Lazy: true}). + HolePunching(HolePunchingPolicy{ + TCP: true, + UDP: true, + SymmetricUDP: false, + }). + Encryption(false). + SecureModeWithPrivateKey(privateKeyBytes). + Build() + if err != nil { + t.Fatalf("build config: %v", err) + } + + encoded, err := encodeInstanceConfig(config) + if err != nil { + t.Fatalf("encode config: %v", err) + } + want := fmt.Sprintf(`hostname = "node \"blue\"" +ipv4 = "10.144.0.10/24" +listeners = ["tcp://0.0.0.0:11010", "udp://[::]:0"] +stun_servers = ["stun.example.com", "192.0.2.1:3478", "txt:_stun.example.com"] +stun_servers_v6 = ["2001:db8::1", "[2001:db8::2]:3478"] + +[network_identity] +network_name = "office \"blue\"" +network_secret = "line1\nline2" + +[[peer]] +uri = "tcp://peer.example.com:11010" + +[[peer]] +uri = "udp://203.0.113.20" + +[[port_forward]] +bind_addr = "127.0.0.1:5202" +dst_addr = "10.144.0.20:5201" +proto = "tcp" + +[[port_forward]] +bind_addr = "0.0.0.0:5203" +dst_addr = "10.144.0.21:5201" +proto = "udp" + +[flags] +enable_encryption = false +disable_p2p = true +need_p2p = true +lazy_p2p = true +disable_tcp_hole_punching = false +disable_udp_hole_punching = false +disable_sym_hole_punching = true + +[secure_mode] +enabled = true +local_private_key = %q +local_public_key = %q +`, + base64.StdEncoding.EncodeToString(privateKey.Bytes()), + base64.StdEncoding.EncodeToString(privateKey.PublicKey().Bytes()), + ) + if encoded != want { + t.Fatalf("encoded config:\n%s\nwant:\n%s", encoded, want) + } +} + +func TestInstanceConfigPreservesCoreDefaults(t *testing.T) { + config, err := NewInstanceConfigBuilder("default"). + NetworkSecret(""). + Build() + if err != nil { + t.Fatalf("build config: %v", err) + } + + encoded, err := encodeInstanceConfig(config) + if err != nil { + t.Fatalf("encode config: %v", err) + } + want := `[network_identity] +network_name = "default" +network_secret = "" +` + if encoded != want { + t.Fatalf("encoded config:\n%s\nwant:\n%s", encoded, want) + } +} + +func TestInstanceConfigCanExplicitlyDisableSTUNServers(t *testing.T) { + config, err := NewInstanceConfigBuilder("default"). + NetworkSecret(""). + STUNServers(). + STUNServersV6(). + Build() + if err != nil { + t.Fatalf("build config: %v", err) + } + + encoded, err := encodeInstanceConfig(config) + if err != nil { + t.Fatalf("encode config: %v", err) + } + want := `stun_servers = [] +stun_servers_v6 = [] + +[network_identity] +network_name = "default" +network_secret = "" +` + if encoded != want { + t.Fatalf("encoded config:\n%s\nwant:\n%s", encoded, want) + } +} + +func TestInstanceConfigValidation(t *testing.T) { + invalidUTF8 := string([]byte{0xff}) + tests := []struct { + name string + build func() error + wantField string + }{ + { + name: "nil builder", + build: func() error { + var builder *InstanceConfigBuilder + _, err := builder.Build() + return err + }, + wantField: "builder is nil", + }, + { + name: "missing network name", + build: func() error { + _, err := NewInstanceConfigBuilder(""). + NetworkSecret(""). + Build() + return err + }, + wantField: "network.name", + }, + { + name: "invalid network name encoding", + build: func() error { + _, err := NewInstanceConfigBuilder(invalidUTF8). + NetworkSecret(""). + Build() + return err + }, + wantField: "network.name", + }, + { + name: "network secret not specified", + build: func() error { + _, err := NewInstanceConfigBuilder("default").Build() + return err + }, + wantField: "network.secret", + }, + { + name: "invalid network secret encoding", + build: func() error { + _, err := NewInstanceConfigBuilder("default"). + NetworkSecret(invalidUTF8). + Build() + return err + }, + wantField: "network.secret", + }, + { + name: "empty hostname", + build: func() error { + _, err := validInstanceConfigBuilder().Hostname("").Build() + return err + }, + wantField: "hostname", + }, + { + name: "hostname exceeds core limit", + build: func() error { + _, err := validInstanceConfigBuilder(). + Hostname(strings.Repeat("a", 33)). + Build() + return err + }, + wantField: "hostname", + }, + { + name: "invalid IPv4 prefix", + build: func() error { + _, err := validInstanceConfigBuilder(). + IPv4(netip.Prefix{}). + Build() + return err + }, + wantField: "ipv4", + }, + { + name: "IPv6 prefix", + build: func() error { + _, err := validInstanceConfigBuilder(). + IPv4(netip.MustParsePrefix("fd00::1/64")). + Build() + return err + }, + wantField: "ipv4", + }, + { + name: "IPv4 host prefix", + build: func() error { + _, err := validInstanceConfigBuilder(). + IPv4(netip.MustParsePrefix("10.0.0.1/32")). + Build() + return err + }, + wantField: "ipv4", + }, + { + name: "unsupported peer scheme", + build: func() error { + _, err := validInstanceConfigBuilder(). + AddPeers("ws://peer.example.com:11010"). + Build() + return err + }, + wantField: "peers[0]", + }, + { + name: "peer port zero", + build: func() error { + _, err := validInstanceConfigBuilder(). + AddPeers("tcp://peer.example.com:0"). + Build() + return err + }, + wantField: "peers[0]", + }, + { + name: "duplicate peer", + build: func() error { + _, err := validInstanceConfigBuilder(). + AddPeers( + "TCP://peer.example.com:11010", + "tcp://peer.example.com:11010", + ). + Build() + return err + }, + wantField: "peers[1]", + }, + { + name: "listener hostname", + build: func() error { + _, err := validInstanceConfigBuilder(). + AddListeners("tcp://listener.example.com:11010"). + Build() + return err + }, + wantField: "listeners[0]", + }, + { + name: "unsupported port forward protocol", + build: func() error { + _, err := validInstanceConfigBuilder(). + AddPortForwards(PortForwardConfig{ + Protocol: "sctp", + Bind: netip.MustParseAddrPort("127.0.0.1:5202"), + Destination: netip.MustParseAddrPort("10.144.0.20:5201"), + }). + Build() + return err + }, + wantField: "port_forwards[0].protocol", + }, + { + name: "IPv6 port forward bind", + build: func() error { + _, err := validInstanceConfigBuilder(). + AddPortForwards(PortForwardConfig{ + Protocol: PortForwardTCP, + Bind: netip.MustParseAddrPort("[::1]:5202"), + Destination: netip.MustParseAddrPort("10.144.0.20:5201"), + }). + Build() + return err + }, + wantField: "port_forwards[0].bind", + }, + { + name: "zero port forward destination port", + build: func() error { + _, err := validInstanceConfigBuilder(). + AddPortForwards(PortForwardConfig{ + Protocol: PortForwardUDP, + Bind: netip.MustParseAddrPort("127.0.0.1:5202"), + Destination: netip.MustParseAddrPort("10.144.0.20:0"), + }). + Build() + return err + }, + wantField: "port_forwards[0].destination", + }, + { + name: "duplicate port forward", + build: func() error { + forward := PortForwardConfig{ + Protocol: PortForwardTCP, + Bind: netip.MustParseAddrPort("127.0.0.1:5202"), + Destination: netip.MustParseAddrPort("10.144.0.20:5201"), + } + _, err := validInstanceConfigBuilder(). + AddPortForwards(forward, forward). + Build() + return err + }, + wantField: "port_forwards[1]", + }, + { + name: "IPv6 literal in IPv4 STUN list", + build: func() error { + _, err := validInstanceConfigBuilder(). + STUNServers("2001:db8::1"). + Build() + return err + }, + wantField: "stun_servers[0]", + }, + { + name: "IPv4 literal in IPv6 STUN list", + build: func() error { + _, err := validInstanceConfigBuilder(). + STUNServersV6("192.0.2.1"). + Build() + return err + }, + wantField: "stun_servers_v6[0]", + }, + { + name: "duplicate STUN server", + build: func() error { + _, err := validInstanceConfigBuilder(). + STUNServers("stun.example.com", "stun.example.com"). + Build() + return err + }, + wantField: "stun_servers[1]", + }, + { + name: "symmetric UDP without UDP", + build: func() error { + _, err := validInstanceConfigBuilder(). + HolePunching(HolePunchingPolicy{SymmetricUDP: true}). + Build() + return err + }, + wantField: "hole_punching.symmetric_udp", + }, + { + name: "secure mode without shared secret", + build: func() error { + _, err := NewInstanceConfigBuilder("default"). + NetworkSecret(""). + SecureMode(). + Build() + return err + }, + wantField: "secure_mode", + }, + { + name: "invalid manual private key", + build: func() error { + _, err := validInstanceConfigBuilder(). + SecureModeWithPrivateKey(make([]byte, 31)). + Build() + return err + }, + wantField: "secure_mode.private_key", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.build() + if err == nil || !strings.Contains(err.Error(), test.wantField) { + t.Fatalf("Build() error = %v, want containing %q", err, test.wantField) + } + }) + } +} + +func TestInstanceConfigCopiesInputsAndRedactsSecrets(t *testing.T) { + privateKey := bytes.Repeat([]byte{7}, 32) + peers := []string{"tcp://peer.example.com:11010"} + builder := NewInstanceConfigBuilder("private-network"). + NetworkSecret("very-secret-value"). + AddPeers(peers...). + SecureModeWithPrivateKey(privateKey) + + config, err := builder.Build() + if err != nil { + t.Fatalf("build config: %v", err) + } + before, err := encodeInstanceConfig(config) + if err != nil { + t.Fatalf("encode config before input mutation: %v", err) + } + + privateKey[0] = 9 + peers[0] = "udp://other.example.com:11010" + builder.NetworkSecret("changed-secret"). + AddPeers("tcp://another.example.com:11010") + after, err := encodeInstanceConfig(config) + if err != nil { + t.Fatalf("encode config after input mutation: %v", err) + } + if after != before { + t.Fatalf("built config changed after mutating inputs:\nbefore:\n%s\nafter:\n%s", before, after) + } + + formatted := []string{ + fmt.Sprintf("%v", config), + fmt.Sprintf("%+v", config), + fmt.Sprintf("%#v", config), + fmt.Sprintf("%v", builder), + fmt.Sprintf("%+v", *builder), + fmt.Sprintf("%#v", *builder), + } + for _, value := range formatted { + if strings.Contains(value, "very-secret-value") || + strings.Contains(value, "changed-secret") { + t.Fatalf("formatted configuration exposed a secret: %s", value) + } + if !strings.Contains(value, "") { + t.Fatalf("formatted configuration was not redacted: %s", value) + } + } +} + +func TestAutomaticSecureModeProducesStableKeyPair(t *testing.T) { + builder := validInstanceConfigBuilder().SecureMode() + first, err := builder.Build() + if err != nil { + t.Fatalf("build first config: %v", err) + } + second, err := builder.Build() + if err != nil { + t.Fatalf("build second config: %v", err) + } + if !bytes.Equal( + first.document.securePrivateKey, + second.document.securePrivateKey, + ) { + t.Fatal("automatic secure-mode key changed between builds") + } + + privateKey, err := ecdh.X25519().NewPrivateKey(first.document.securePrivateKey) + if err != nil { + t.Fatalf("parse generated private key: %v", err) + } + encoded, err := encodeInstanceConfig(first) + if err != nil { + t.Fatalf("encode config: %v", err) + } + privateKeyText := base64.StdEncoding.EncodeToString(privateKey.Bytes()) + publicKeyText := base64.StdEncoding.EncodeToString(privateKey.PublicKey().Bytes()) + if !strings.Contains(encoded, `local_private_key = "`+privateKeyText+`"`) { + t.Fatal("encoded config does not contain generated private key") + } + if !strings.Contains(encoded, `local_public_key = "`+publicKeyText+`"`) { + t.Fatal("encoded config does not contain derived public key") + } +} + +func TestZeroInstanceConfigIsInvalid(t *testing.T) { + if _, err := encodeInstanceConfig(InstanceConfig{}); err == nil { + t.Fatal("zero InstanceConfig was accepted") + } +} + +func validInstanceConfigBuilder() *InstanceConfigBuilder { + return NewInstanceConfigBuilder("default").NetworkSecret("secret") +} diff --git a/easytier-go/internal/host/instance_config_toml.go b/easytier-go/internal/host/instance_config_toml.go new file mode 100644 index 00000000..814b1df1 --- /dev/null +++ b/easytier-go/internal/host/instance_config_toml.go @@ -0,0 +1,142 @@ +package host + +import ( + "crypto/ecdh" + "encoding/base64" + "encoding/json" + "fmt" + "strings" +) + +func encodeInstanceConfig(config InstanceConfig) (string, error) { + if config.document == nil { + return "", invalidInstanceConfig("", "configuration was not built") + } + document := config.document + var encoded strings.Builder + + if document.hostname != nil { + writeTOMLStringField(&encoded, "hostname", *document.hostname) + } + if document.ipv4 != nil { + writeTOMLStringField(&encoded, "ipv4", document.ipv4.String()) + } + if len(document.listeners) != 0 { + writeTOMLStringArrayField(&encoded, "listeners", document.listeners) + } + if document.stunServersSet { + writeTOMLStringArrayField(&encoded, "stun_servers", document.stunServers) + } + if document.stunServersV6Set { + writeTOMLStringArrayField(&encoded, "stun_servers_v6", document.stunServersV6) + } + if encoded.Len() != 0 { + encoded.WriteByte('\n') + } + + encoded.WriteString("[network_identity]\n") + writeTOMLStringField(&encoded, "network_name", document.networkName) + writeTOMLStringField(&encoded, "network_secret", document.networkSecret) + + for _, peer := range document.peers { + encoded.WriteString("\n[[peer]]\n") + writeTOMLStringField(&encoded, "uri", peer) + } + + for _, forward := range document.portForwards { + encoded.WriteString("\n[[port_forward]]\n") + writeTOMLStringField(&encoded, "bind_addr", forward.Bind.String()) + writeTOMLStringField( + &encoded, + "dst_addr", + forward.Destination.String(), + ) + writeTOMLStringField(&encoded, "proto", string(forward.Protocol)) + } + + if document.encryption != nil || + document.p2p != nil || + document.holePunching != nil { + encoded.WriteString("\n[flags]\n") + if document.encryption != nil { + writeTOMLBoolField(&encoded, "enable_encryption", *document.encryption) + } + if document.p2p != nil { + writeTOMLBoolField(&encoded, "disable_p2p", document.p2p.Disable) + writeTOMLBoolField(&encoded, "need_p2p", document.p2p.Need) + writeTOMLBoolField(&encoded, "lazy_p2p", document.p2p.Lazy) + } + if document.holePunching != nil { + writeTOMLBoolField( + &encoded, + "disable_tcp_hole_punching", + !document.holePunching.TCP, + ) + writeTOMLBoolField( + &encoded, + "disable_udp_hole_punching", + !document.holePunching.UDP, + ) + writeTOMLBoolField( + &encoded, + "disable_sym_hole_punching", + !document.holePunching.SymmetricUDP, + ) + } + } + + if document.secureMode != secureModeDisabled { + privateKey, err := ecdh.X25519().NewPrivateKey(document.securePrivateKey) + if err != nil { + return "", invalidInstanceConfig( + "secure_mode.private_key", + "is not a valid X25519 private key", + ) + } + encoded.WriteString("\n[secure_mode]\n") + writeTOMLBoolField(&encoded, "enabled", true) + writeTOMLStringField( + &encoded, + "local_private_key", + base64.StdEncoding.EncodeToString(privateKey.Bytes()), + ) + writeTOMLStringField( + &encoded, + "local_public_key", + base64.StdEncoding.EncodeToString(privateKey.PublicKey().Bytes()), + ) + } + + return encoded.String(), nil +} + +func writeTOMLStringField(encoded *strings.Builder, name string, value string) { + fmt.Fprintf(encoded, "%s = %s\n", name, quoteTOMLString(value)) +} + +func writeTOMLBoolField(encoded *strings.Builder, name string, value bool) { + fmt.Fprintf(encoded, "%s = %t\n", name, value) +} + +func writeTOMLStringArrayField( + encoded *strings.Builder, + name string, + values []string, +) { + fmt.Fprintf(encoded, "%s = [", name) + for index, value := range values { + if index != 0 { + encoded.WriteString(", ") + } + encoded.WriteString(quoteTOMLString(value)) + } + encoded.WriteString("]\n") +} + +func quoteTOMLString(value string) string { + encoded, err := json.Marshal(value) + if err != nil { + panic("encoding a validated Go string as JSON cannot fail") + } + return string(encoded) +} diff --git a/easytier-go/internal/host/management.go b/easytier-go/internal/host/management.go new file mode 100644 index 00000000..9a492d66 --- /dev/null +++ b/easytier-go/internal/host/management.go @@ -0,0 +1,774 @@ +package host + +import ( + "context" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "sort" + "strings" + "sync" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/internal/contextutil" + apiconfig "github.com/EasyTier/EasyTier/easytier-go/proto/api/config" + apiinstance "github.com/EasyTier/EasyTier/easytier-go/proto/api/instance" + "github.com/EasyTier/EasyTier/easytier-go/proto/api/manage" + "github.com/EasyTier/EasyTier/easytier-go/proto/common" + errorpb "github.com/EasyTier/EasyTier/easytier-go/proto/error" + "google.golang.org/protobuf/proto" +) + +const ( + runNetworkInstanceMethod = "api.manage.WebClientService.RunNetworkInstance" + retainNetworkInstanceMethod = "api.manage.WebClientService.RetainNetworkInstance" + collectNetworkInfoMethod = "api.manage.WebClientService.CollectNetworkInfo" + listNetworkInstanceMethod = "api.manage.WebClientService.ListNetworkInstance" + deleteNetworkInstanceMethod = "api.manage.WebClientService.DeleteNetworkInstance" + getNetworkInstanceConfigMethod = "api.manage.WebClientService.GetNetworkInstanceConfig" + listNetworkInstanceMetaMethod = "api.manage.WebClientService.ListNetworkInstanceMeta" + patchConfigMethod = "api.config.ConfigRpc.PatchConfig" + getConfigMethod = "api.config.ConfigRpc.GetConfig" +) + +type instanceOwner uint8 + +const ( + instanceOwnerApplication instanceOwner = iota + instanceOwnerWeb +) + +type managedInstance struct { + id *common.UUID + instance *Instance + owner instanceOwner + config *manage.NetworkConfig + configTOML string + source manage.ConfigSource + name string +} + +type instanceManager struct { + mu sync.RWMutex + mutations sync.Mutex + host *Host + instances map[string]*managedInstance +} + +func newInstanceManager() *instanceManager { + return &instanceManager{instances: make(map[string]*managedInstance)} +} + +func (manager *instanceManager) register(entry *managedInstance) error { + id := uuidString(entry.id) + manager.mu.Lock() + defer manager.mu.Unlock() + if _, exists := manager.instances[id]; exists { + return fmt.Errorf("EasyTier instance %s already exists", id) + } + manager.instances[id] = entry + return nil +} + +func (manager *instanceManager) remove(instance *Instance) { + manager.mu.Lock() + if entry := manager.instances[instance.id]; entry != nil && + entry.instance == instance { + delete(manager.instances, instance.id) + } + manager.mu.Unlock() +} + +func (manager *instanceManager) clear() { + manager.mu.Lock() + manager.instances = make(map[string]*managedInstance) + manager.mu.Unlock() +} + +func (manager *instanceManager) snapshot() []*managedInstance { + manager.mu.RLock() + entries := make([]*managedInstance, 0, len(manager.instances)) + for _, entry := range manager.instances { + snapshot := *entry + entries = append(entries, &snapshot) + } + manager.mu.RUnlock() + sort.Slice(entries, func(i, j int) bool { + return uuidString(entries[i].id) < uuidString(entries[j].id) + }) + return entries +} + +func (manager *instanceManager) handle( + ctx context.Context, + encoded []byte, +) []byte { + started := time.Now() + var envelope common.HostManagementRequest + if err := proto.Unmarshal(encoded, &envelope); err != nil { + return encodeManagementResponse(nil, err, started) + } + if envelope.Rpc == nil { + return encodeManagementResponse( + nil, + fmt.Errorf("host management RPC is required"), + started, + ) + } + response, err := manager.dispatch( + ctx, + envelope.Rpc.FullMethodName, + envelope.Rpc.Request, + envelope.GetPreparedConfig(), + envelope.GetPreparedInstanceId(), + ) + return encodeManagementResponse(response, err, started) +} + +func (manager *instanceManager) dispatch( + ctx context.Context, + method string, + encoded []byte, + preparedConfig string, + preparedInstanceID *common.UUID, +) (proto.Message, error) { + switch method { + case runNetworkInstanceMethod: + request := new(manage.RunNetworkInstanceRequest) + if err := proto.Unmarshal(encoded, request); err != nil { + return nil, err + } + return manager.runNetworkInstance( + ctx, + request, + preparedConfig, + preparedInstanceID, + ) + case retainNetworkInstanceMethod: + request := new(manage.RetainNetworkInstanceRequest) + if err := proto.Unmarshal(encoded, request); err != nil { + return nil, err + } + return manager.retainNetworkInstances(ctx, request) + case collectNetworkInfoMethod: + request := new(manage.CollectNetworkInfoRequest) + if err := proto.Unmarshal(encoded, request); err != nil { + return nil, err + } + return manager.collectNetworkInfo(ctx, request) + case listNetworkInstanceMethod: + request := new(manage.ListNetworkInstanceRequest) + if err := proto.Unmarshal(encoded, request); err != nil { + return nil, err + } + return manager.listNetworkInstances(), nil + case deleteNetworkInstanceMethod: + request := new(manage.DeleteNetworkInstanceRequest) + if err := proto.Unmarshal(encoded, request); err != nil { + return nil, err + } + return manager.deleteNetworkInstances(ctx, request) + case getNetworkInstanceConfigMethod: + request := new(manage.GetNetworkInstanceConfigRequest) + if err := proto.Unmarshal(encoded, request); err != nil { + return nil, err + } + return manager.getNetworkInstanceConfig(request) + case listNetworkInstanceMetaMethod: + request := new(manage.ListNetworkInstanceMetaRequest) + if err := proto.Unmarshal(encoded, request); err != nil { + return nil, err + } + return manager.listNetworkInstanceMeta(request), nil + case patchConfigMethod: + request := new(apiconfig.PatchConfigRequest) + if err := proto.Unmarshal(encoded, request); err != nil { + return nil, err + } + return manager.patchConfig(ctx, request) + case getConfigMethod: + request := new(apiconfig.GetConfigRequest) + if err := proto.Unmarshal(encoded, request); err != nil { + return nil, err + } + return manager.getConfig(request) + default: + return nil, fmt.Errorf("unsupported host management method %q", method) + } +} + +func (manager *instanceManager) runNetworkInstance( + ctx context.Context, + request *manage.RunNetworkInstanceRequest, + preparedConfig string, + preparedInstanceID *common.UUID, +) (*manage.RunNetworkInstanceResponse, error) { + if request.Config == nil { + return nil, fmt.Errorf("config is required") + } + if preparedConfig == "" { + return nil, fmt.Errorf("prepared EasyTier config is required") + } + if preparedInstanceID == nil { + return nil, fmt.Errorf("prepared EasyTier instance ID is required") + } + request.InstId = cloneUUID(preparedInstanceID) + id := uuidString(preparedInstanceID) + request.Config.InstanceId = &id + manager.mutations.Lock() + defer manager.mutations.Unlock() + + manager.mu.RLock() + previous := manager.instances[id] + manager.mu.RUnlock() + if previous != nil { + if previous.owner != instanceOwnerWeb { + return nil, fmt.Errorf("configuration for instance %s is read-only", id) + } + if !request.Overwrite && previous.instance.engine.TerminalError() == nil { + return &manage.RunNetworkInstanceResponse{ + InstId: cloneUUID(request.InstId), + }, nil + } + if err := closeManagedInstance(ctx, previous.instance); err != nil { + return nil, fmt.Errorf("replace EasyTier instance %s: %w", id, err) + } + } + source := request.Source + if source == manage.ConfigSource_ConfigSourceUnspecified && previous != nil { + source = previous.source + } + + entry, err := manager.createWebInstance( + ctx, + request.InstId, + request.Config, + source, + preparedConfig, + ) + if err != nil && previous != nil { + restoreContext, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _, restoreErr := manager.createWebInstance( + restoreContext, + previous.id, + previous.config, + previous.source, + previous.configTOML, + ) + err = errors.Join(err, restoreErr) + } + if err != nil { + return nil, err + } + return &manage.RunNetworkInstanceResponse{InstId: cloneUUID(entry.id)}, nil +} + +func (manager *instanceManager) createWebInstance( + ctx context.Context, + id *common.UUID, + config *manage.NetworkConfig, + source manage.ConfigSource, + configTOML string, +) (*managedInstance, error) { + runtime, err := manager.host.engine.CreateInstance(ctx, configTOML) + if err != nil { + return nil, err + } + instance := &Instance{ + engine: runtime, + id: uuidString(id), + manager: manager, + } + if err := runtime.Start(ctx); err != nil { + _ = runtime.Close(contextutil.WithoutCancel(ctx)) + return nil, err + } + if source == manage.ConfigSource_ConfigSourceUnspecified { + source = manage.ConfigSource_ConfigSourceUser + } + entry := &managedInstance{ + id: cloneUUID(id), + instance: instance, + owner: instanceOwnerWeb, + config: proto.Clone(config).(*manage.NetworkConfig), + configTOML: configTOML, + source: source, + name: config.GetNetworkName(), + } + if err := manager.register(entry); err != nil { + _ = runtime.Close(contextutil.WithoutCancel(ctx)) + return nil, err + } + return entry, nil +} + +func (manager *instanceManager) patchConfig( + ctx context.Context, + request *apiconfig.PatchConfigRequest, +) (*apiconfig.PatchConfigResponse, error) { + id, err := selectedInstanceID(request.Instance) + if err != nil { + return nil, err + } + key := uuidString(id) + manager.mutations.Lock() + defer manager.mutations.Unlock() + + manager.mu.RLock() + entry := manager.instances[key] + manager.mu.RUnlock() + if entry == nil { + return nil, fmt.Errorf("EasyTier instance %s not found", key) + } + if entry.owner != instanceOwnerWeb { + return nil, fmt.Errorf("configuration for instance %s is read-only", key) + } + if request.Patch == nil { + return new(apiconfig.PatchConfigResponse), nil + } + + response := new(apiconfig.PatchConfigResponse) + patchErr := entry.instance.callRPCRequest( + ctx, + patchConfigMethod, + &apiconfig.PatchConfigRequest{ + Patch: hostedConfigPatch(request.Patch), + Instance: request.Instance, + }, + response, + ) + snapshotContext, cancel := context.WithTimeout( + contextutil.WithoutCancel(ctx), + 10*time.Second, + ) + defer cancel() + effective := new(apiconfig.GetConfigResponse) + if err := entry.instance.callRPCRequest( + snapshotContext, + getConfigMethod, + &apiconfig.GetConfigRequest{Instance: request.Instance}, + effective, + ); err != nil { + return nil, errors.Join( + patchErr, + fmt.Errorf("read effective EasyTier configuration: %w", err), + ) + } + if effective.Config == nil || effective.TomlConfig == "" { + return nil, errors.Join( + patchErr, + fmt.Errorf("EasyTier instance %s returned an incomplete config snapshot", key), + ) + } + + manager.mu.Lock() + if manager.instances[key] != entry { + manager.mu.Unlock() + return nil, errors.Join( + patchErr, + fmt.Errorf("EasyTier instance %s is no longer running", key), + ) + } + entry.config = mergeHostedConfigPatch(entry.config, effective.Config, request.Patch) + entry.configTOML = effective.TomlConfig + manager.mu.Unlock() + if patchErr != nil { + return nil, patchErr + } + return response, nil +} + +func (manager *instanceManager) getConfig( + request *apiconfig.GetConfigRequest, +) (*apiconfig.GetConfigResponse, error) { + id, err := selectedInstanceID(request.Instance) + if err != nil { + return nil, err + } + response, err := manager.getNetworkInstanceConfig( + &manage.GetNetworkInstanceConfigRequest{InstId: id}, + ) + if err != nil { + return nil, err + } + return &apiconfig.GetConfigResponse{Config: response.Config}, nil +} + +func selectedInstanceID( + identifier *apiinstance.InstanceIdentifier, +) (*common.UUID, error) { + id := identifier.GetId() + if id == nil { + return nil, fmt.Errorf("instance ID is required") + } + return id, nil +} + +func hostedConfigPatch( + patch *apiconfig.InstanceConfigPatch, +) *apiconfig.InstanceConfigPatch { + return &apiconfig.InstanceConfigPatch{ + PortForwards: patch.PortForwards, + Acl: patch.Acl, + ProxyNetworks: patch.ProxyNetworks, + DisableRelayData: patch.DisableRelayData, + } +} + +func mergeHostedConfigPatch( + desired *manage.NetworkConfig, + effective *manage.NetworkConfig, + patch *apiconfig.InstanceConfigPatch, +) *manage.NetworkConfig { + merged := proto.Clone(desired).(*manage.NetworkConfig) + runtime := proto.Clone(effective).(*manage.NetworkConfig) + if len(patch.PortForwards) != 0 { + merged.PortForwards = runtime.PortForwards + } + if patch.Acl != nil { + merged.Acl = runtime.Acl + } + if len(patch.ProxyNetworks) != 0 { + merged.ProxyCidrs = runtime.ProxyCidrs + } + if patch.DisableRelayData != nil { + merged.DisableRelayData = runtime.DisableRelayData + } + return merged +} + +func (manager *instanceManager) retainNetworkInstances( + ctx context.Context, + request *manage.RetainNetworkInstanceRequest, +) (*manage.RetainNetworkInstanceResponse, error) { + retained := uuidSet(request.InstIds) + manager.mutations.Lock() + defer manager.mutations.Unlock() + if err := manager.closeWebInstances(ctx, func(id string) bool { + _, keep := retained[id] + return !keep + }); err != nil { + return nil, err + } + return &manage.RetainNetworkInstanceResponse{ + RemainInstIds: manager.instanceIDs(), + }, nil +} + +func (manager *instanceManager) deleteNetworkInstances( + ctx context.Context, + request *manage.DeleteNetworkInstanceRequest, +) (*manage.DeleteNetworkInstanceResponse, error) { + deleted := uuidSet(request.InstIds) + manager.mutations.Lock() + defer manager.mutations.Unlock() + if err := manager.closeWebInstances(ctx, func(id string) bool { + _, remove := deleted[id] + return remove + }); err != nil { + return nil, err + } + return &manage.DeleteNetworkInstanceResponse{ + RemainInstIds: manager.instanceIDs(), + }, nil +} + +func (manager *instanceManager) closeWebInstances( + ctx context.Context, + shouldClose func(string) bool, +) error { + for _, entry := range manager.snapshot() { + if entry.owner != instanceOwnerWeb || + !shouldClose(uuidString(entry.id)) { + continue + } + if err := closeManagedInstance(ctx, entry.instance); err != nil { + return err + } + } + return nil +} + +func closeManagedInstance(ctx context.Context, instance *Instance) error { + alreadyFailed := instance.engine.TerminalError() != nil + err := instance.Close(ctx) + if alreadyFailed && ctx.Err() == nil { + return nil + } + return err +} + +func (manager *instanceManager) listNetworkInstances() *manage.ListNetworkInstanceResponse { + return &manage.ListNetworkInstanceResponse{InstIds: manager.instanceIDs()} +} + +func (manager *instanceManager) instanceIDs() []*common.UUID { + entries := manager.snapshot() + ids := make([]*common.UUID, len(entries)) + for index, entry := range entries { + ids[index] = cloneUUID(entry.id) + } + return ids +} + +func (manager *instanceManager) collectNetworkInfo( + ctx context.Context, + request *manage.CollectNetworkInfoRequest, +) (*manage.CollectNetworkInfoResponse, error) { + included := uuidSet(request.InstIds) + info := make(map[string]*manage.NetworkInstanceRunningInfo) + for _, entry := range manager.snapshot() { + id := uuidString(entry.id) + if len(included) != 0 { + if _, exists := included[id]; !exists { + continue + } + } + snapshot, err := entry.runningInfo(ctx) + if err != nil { + return nil, err + } + info[id] = snapshot + } + return &manage.CollectNetworkInfoResponse{ + Info: &manage.NetworkInstanceRunningInfoMap{Map: info}, + }, nil +} + +func (entry *managedInstance) runningInfo( + ctx context.Context, +) (*manage.NetworkInstanceRunningInfo, error) { + state := entry.instance.State() + running := state != StateCreated && state != StateStopped + info := &manage.NetworkInstanceRunningInfo{ + Running: running, + Events: entry.instance.engine.ManagementEvents(), + } + if err := entry.instance.engine.TerminalError(); err != nil { + message := err.Error() + info.ErrorMsg = &message + } + if state != StateRunning { + return info, nil + } + peers, err := entry.instance.listPeerResponse(ctx) + if err != nil { + return nil, err + } + routes, err := entry.instance.listRouteResponse(ctx) + if err != nil { + return nil, err + } + nodeResponse, err := entry.instance.showNodeInfo(ctx) + if err != nil { + return nil, err + } + foreign, err := entry.instance.foreignNetworkSummary(ctx) + if err != nil { + return nil, err + } + node := nodeResponse.NodeInfo + listeners := make([]*common.Url, len(node.GetListeners())) + for index, listener := range node.GetListeners() { + listeners[index] = &common.Url{Url: listener} + } + var virtualIPv4 *common.Ipv4Inet + for _, route := range routes.Routes { + if route.PeerId == node.GetPeerId() { + virtualIPv4 = route.Ipv4Addr + break + } + } + info.DevName = entry.config.GetDevName() + info.MyNodeInfo = &manage.MyNodeInfo{ + VirtualIpv4: virtualIPv4, + Hostname: node.GetHostname(), + Version: node.GetVersion(), + Ips: node.GetIpList(), + StunInfo: node.GetStunInfo(), + Listeners: listeners, + PeerId: node.GetPeerId(), + } + info.Routes = routes.Routes + info.Peers = peers.PeerInfos + info.PeerRoutePairs = peerRoutePairs(peers.PeerInfos, routes.Routes) + info.ForeignNetworkSummary = foreign.Summary + return info, nil +} + +func (manager *instanceManager) getNetworkInstanceConfig( + request *manage.GetNetworkInstanceConfigRequest, +) (*manage.GetNetworkInstanceConfigResponse, error) { + if request.InstId == nil { + return nil, fmt.Errorf("instance ID is required") + } + id := uuidString(request.InstId) + manager.mu.RLock() + defer manager.mu.RUnlock() + entry := manager.instances[id] + if entry == nil { + return nil, fmt.Errorf("EasyTier instance %s not found", id) + } + if entry.owner != instanceOwnerWeb { + return nil, fmt.Errorf("configuration for instance %s is read-only", id) + } + config := proto.Clone(entry.config).(*manage.NetworkConfig) + return &manage.GetNetworkInstanceConfigResponse{ + Config: config, + Source: entry.source, + }, nil +} + +func (manager *instanceManager) listNetworkInstanceMeta( + request *manage.ListNetworkInstanceMetaRequest, +) *manage.ListNetworkInstanceMetaResponse { + included := uuidSet(request.InstIds) + response := &manage.ListNetworkInstanceMetaResponse{} + for _, entry := range manager.snapshot() { + if _, exists := included[uuidString(entry.id)]; !exists { + continue + } + permission := uint32(0) + if entry.owner == instanceOwnerApplication { + permission = 3 + } + networkName := entry.name + if entry.config != nil { + networkName = entry.config.GetNetworkName() + } + response.Metas = append(response.Metas, &manage.NetworkMeta{ + InstId: cloneUUID(entry.id), + NetworkName: networkName, + ConfigPermission: permission, + InstanceName: entry.name, + Source: entry.source, + }) + } + return response +} + +func encodeManagementResponse( + response proto.Message, + responseErr error, + started time.Time, +) []byte { + runtimeUs := uint64(time.Since(started).Microseconds()) + if runtimeUs == 0 { + runtimeUs = 1 + } + envelope := &common.RpcResponse{ + RuntimeUs: runtimeUs, + } + if responseErr == nil { + envelope.Response, responseErr = proto.Marshal(response) + } + if responseErr != nil { + envelope.Error = &errorpb.Error{ + ErrorKind: &errorpb.Error_ExecuteError{ + ExecuteError: &errorpb.ExecuteError{ + ErrorMessage: responseErr.Error(), + }, + }, + } + } + encoded, err := proto.Marshal(envelope) + if err != nil { + panic("marshal EasyTier RPC response: " + err.Error()) + } + return encoded +} + +func peerRoutePairs( + peers []*apiinstance.PeerInfo, + routes []*apiinstance.Route, +) []*apiinstance.PeerRoutePair { + byID := make(map[uint32]*apiinstance.PeerInfo, len(peers)) + for _, peer := range peers { + byID[peer.PeerId] = peer + } + pairs := make([]*apiinstance.PeerRoutePair, 0, len(routes)) + for _, route := range routes { + if peer := byID[route.PeerId]; peer != nil { + pairs = append(pairs, &apiinstance.PeerRoutePair{ + Route: route, + Peer: peer, + }) + } + } + sort.Slice(pairs, func(i, j int) bool { + left, right := pairs[i].Route, pairs[j].Route + leftPublic := left.GetFeatureFlag().GetIsPublicServer() + rightPublic := right.GetFeatureFlag().GetIsPublicServer() + if leftPublic != rightPublic { + return leftPublic + } + return left.GetIpv4Addr().GetAddress().GetAddr() < + right.GetIpv4Addr().GetAddress().GetAddr() + }) + return pairs +} + +func newInstanceUUID() (*common.UUID, string, error) { + var value [16]byte + if _, err := rand.Read(value[:]); err != nil { + return nil, "", fmt.Errorf("generate EasyTier instance ID: %w", err) + } + value[6] = value[6]&0x0f | 0x40 + value[8] = value[8]&0x3f | 0x80 + id := &common.UUID{ + Part1: binary.BigEndian.Uint32(value[0:4]), + Part2: binary.BigEndian.Uint32(value[4:8]), + Part3: binary.BigEndian.Uint32(value[8:12]), + Part4: binary.BigEndian.Uint32(value[12:16]), + } + return id, uuidString(id), nil +} + +func uuidString(id *common.UUID) string { + var value [16]byte + binary.BigEndian.PutUint32(value[0:4], id.GetPart1()) + binary.BigEndian.PutUint32(value[4:8], id.GetPart2()) + binary.BigEndian.PutUint32(value[8:12], id.GetPart3()) + binary.BigEndian.PutUint32(value[12:16], id.GetPart4()) + return fmt.Sprintf( + "%x-%x-%x-%x-%x", + value[0:4], + value[4:6], + value[6:8], + value[8:10], + value[10:16], + ) +} + +func cloneUUID(id *common.UUID) *common.UUID { + if id == nil { + return nil + } + return &common.UUID{ + Part1: id.Part1, + Part2: id.Part2, + Part3: id.Part3, + Part4: id.Part4, + } +} + +func uuidSet(ids []*common.UUID) map[string]struct{} { + set := make(map[string]struct{}, len(ids)) + for _, id := range ids { + if id != nil { + set[uuidString(id)] = struct{}{} + } + } + return set +} + +func bindInstanceIdentity(config, id, name string) string { + var encoded strings.Builder + writeTOMLStringField(&encoded, "instance_id", id) + writeTOMLStringField(&encoded, "instance_name", name) + encoded.WriteByte('\n') + encoded.WriteString(config) + return encoded.String() +} diff --git a/easytier-go/internal/host/management_test.go b/easytier-go/internal/host/management_test.go new file mode 100644 index 00000000..02bf6fd5 --- /dev/null +++ b/easytier-go/internal/host/management_test.go @@ -0,0 +1,348 @@ +package host + +import ( + "bytes" + "context" + "fmt" + "net/netip" + "testing" + "time" + + apiconfig "github.com/EasyTier/EasyTier/easytier-go/proto/api/config" + apiinstance "github.com/EasyTier/EasyTier/easytier-go/proto/api/instance" + "github.com/EasyTier/EasyTier/easytier-go/proto/api/manage" + "github.com/EasyTier/EasyTier/easytier-go/proto/common" + "google.golang.org/protobuf/encoding/protowire" + "google.golang.org/protobuf/proto" +) + +func TestApplicationInstanceIsReadOnlyToWebManagement(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + host, err := New(ctx, Options{}) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + + instance, err := host.CreateInstance(ctx, managementTestConfig(t, "application")) + if err != nil { + t.Fatalf("create application instance: %v", err) + } + if instance.ID() == "" { + t.Fatal("application instance ID is empty") + } + if got := host.Instances(); len(got) != 1 || got[0] != instance { + t.Fatalf("host instances = %v, want application instance", got) + } + entry := host.manager.snapshot()[0] + meta := host.manager.listNetworkInstanceMeta( + &manage.ListNetworkInstanceMetaRequest{ + InstIds: []*common.UUID{entry.id}, + }, + ) + if len(meta.Metas) != 1 || meta.Metas[0].ConfigPermission != 3 { + t.Fatalf("application metadata = %v, want read-only/no-delete", meta.Metas) + } + if _, err := host.manager.getNetworkInstanceConfig( + &manage.GetNetworkInstanceConfigRequest{InstId: entry.id}, + ); err == nil { + t.Fatal("application config was writable through Web management") + } + response, err := host.manager.deleteNetworkInstances( + ctx, + &manage.DeleteNetworkInstanceRequest{ + InstIds: []*common.UUID{entry.id}, + }, + ) + if err != nil { + t.Fatalf("delete application instance through Web management: %v", err) + } + if len(response.RemainInstIds) != 1 { + t.Fatalf("remaining IDs = %v, want application instance", response.RemainInstIds) + } + if err := instance.Close(ctx); err != nil { + t.Fatalf("close application instance: %v", err) + } + if len(host.Instances()) != 0 { + t.Fatal("closed application instance remained registered") + } +} + +func TestWebManagementRunsCollectsAndDeletesInstance(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + host, err := New(ctx, Options{}) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + + id := &common.UUID{Part1: 1, Part2: 2, Part3: 3, Part4: 4} + idString := uuidString(id) + config := managementTestConfig(t, "managed") + configTOML, err := encodeInstanceConfig(config) + if err != nil { + t.Fatalf("encode managed config: %v", err) + } + networkName := "managed" + networkConfig := &manage.NetworkConfig{ + InstanceId: &idString, + NetworkName: &networkName, + } + unknownConfig := protowire.AppendTag(nil, 1000, protowire.BytesType) + unknownConfig = protowire.AppendString(unknownConfig, "future-config") + networkConfig.ProtoReflect().SetUnknown(unknownConfig) + run := callManagement( + t, + host, + ctx, + runNetworkInstanceMethod, + &manage.RunNetworkInstanceRequest{ + InstId: id, + Config: networkConfig, + Source: manage.ConfigSource_ConfigSourceWeb, + }, + bindInstanceIdentity(configTOML, idString, networkName), + new(manage.RunNetworkInstanceResponse), + ) + if run.(*manage.RunNetworkInstanceResponse).GetInstId() == nil { + t.Fatal("run response omitted instance ID") + } + if instances := host.Instances(); len(instances) != 1 || + instances[0].ID() != idString { + t.Fatalf("host instances = %v, want managed instance %s", instances, idString) + } + listed := callManagement( + t, + host, + ctx, + listNetworkInstanceMethod, + new(manage.ListNetworkInstanceRequest), + "", + new(manage.ListNetworkInstanceResponse), + ).(*manage.ListNetworkInstanceResponse) + if len(listed.InstIds) != 1 { + t.Fatalf("listed IDs = %v, want managed instance", listed.InstIds) + } + metas := callManagement( + t, + host, + ctx, + listNetworkInstanceMetaMethod, + &manage.ListNetworkInstanceMetaRequest{ + InstIds: []*common.UUID{id}, + }, + "", + new(manage.ListNetworkInstanceMetaResponse), + ).(*manage.ListNetworkInstanceMetaResponse) + if len(metas.Metas) != 1 || metas.Metas[0].ConfigPermission != 0 { + t.Fatalf("managed metadata = %v, want writable instance", metas.Metas) + } + retained := callManagement( + t, + host, + ctx, + retainNetworkInstanceMethod, + &manage.RetainNetworkInstanceRequest{ + InstIds: []*common.UUID{id}, + }, + "", + new(manage.RetainNetworkInstanceResponse), + ).(*manage.RetainNetworkInstanceResponse) + if len(retained.RemainInstIds) != 1 { + t.Fatalf("retained IDs = %v, want managed instance", retained.RemainInstIds) + } + + collected := callManagement( + t, + host, + ctx, + collectNetworkInfoMethod, + &manage.CollectNetworkInfoRequest{InstIds: []*common.UUID{id}}, + "", + new(manage.CollectNetworkInfoResponse), + ).(*manage.CollectNetworkInfoResponse) + info := collected.GetInfo().GetMap()[idString] + if info == nil || !info.Running || info.MyNodeInfo == nil { + t.Fatalf("managed instance status = %v, want running node info", info) + } + if info.MyNodeInfo.Hostname != "managed-host" || + info.MyNodeInfo.PeerId == 0 { + t.Fatalf("managed node info = %v, want live node identity", info.MyNodeInfo) + } + + managedConfig := callManagement( + t, + host, + ctx, + getNetworkInstanceConfigMethod, + &manage.GetNetworkInstanceConfigRequest{InstId: id}, + "", + new(manage.GetNetworkInstanceConfigResponse), + ).(*manage.GetNetworkInstanceConfigResponse) + if managedConfig.GetConfig().GetNetworkName() != networkName || + managedConfig.Source != manage.ConfigSource_ConfigSourceWeb { + t.Fatalf("managed config response = %v", managedConfig) + } + if unknown := managedConfig.Config.ProtoReflect().GetUnknown(); !bytes.Equal( + unknown, + unknownConfig, + ) { + t.Fatalf("managed config unknown fields = %x, want %x", unknown, unknownConfig) + } + + disableRelayData := true + unsupportedHostname := "ignored-hostname" + identifier := &apiinstance.InstanceIdentifier{ + Selector: &apiinstance.InstanceIdentifier_Id{Id: id}, + } + callManagement( + t, + host, + ctx, + patchConfigMethod, + &apiconfig.PatchConfigRequest{ + Instance: identifier, + Patch: &apiconfig.InstanceConfigPatch{ + Hostname: &unsupportedHostname, + DisableRelayData: &disableRelayData, + }, + }, + "", + new(apiconfig.PatchConfigResponse), + ) + patchedConfig := callManagement( + t, + host, + ctx, + getConfigMethod, + &apiconfig.GetConfigRequest{Instance: identifier}, + "", + new(apiconfig.GetConfigResponse), + ).(*apiconfig.GetConfigResponse).Config + if patchedConfig == nil || !patchedConfig.GetDisableRelayData() { + t.Fatalf("patched config = %v, want relay data disabled", patchedConfig) + } + if patchedConfig.Hostname != nil { + t.Fatalf("unsupported hostname patch was applied: %q", patchedConfig.GetHostname()) + } + if unknown := patchedConfig.ProtoReflect().GetUnknown(); !bytes.Equal( + unknown, + unknownConfig, + ) { + t.Fatalf("patched config unknown fields = %x, want %x", unknown, unknownConfig) + } + + originalInstance := host.manager.snapshot()[0].instance + replacementConfig := proto.Clone(networkConfig).(*manage.NetworkConfig) + _, err = host.manager.runNetworkInstance( + ctx, + &manage.RunNetworkInstanceRequest{ + InstId: id, + Config: replacementConfig, + Overwrite: true, + Source: manage.ConfigSource_ConfigSourceWeb, + }, + "[", + id, + ) + if err == nil { + t.Fatal("invalid replacement config unexpectedly succeeded") + } + restoredEntries := host.manager.snapshot() + if len(restoredEntries) != 1 || restoredEntries[0].instance == originalInstance { + t.Fatalf("restored entries = %v, want one replacement instance", restoredEntries) + } + effectiveConfig := new(apiconfig.GetConfigResponse) + if err := restoredEntries[0].instance.callRPCRequest( + ctx, + getConfigMethod, + &apiconfig.GetConfigRequest{Instance: identifier}, + effectiveConfig, + ); err != nil { + t.Fatalf("get restored runtime config: %v", err) + } + if effectiveConfig.Config == nil || + !effectiveConfig.Config.GetDisableRelayData() { + t.Fatalf("restored runtime config = %v, want relay data disabled", effectiveConfig.Config) + } + if effectiveConfig.TomlConfig == "" || + effectiveConfig.TomlConfig != restoredEntries[0].configTOML { + t.Fatal("restored TOML config does not match the running Core") + } + + deleted := callManagement( + t, + host, + ctx, + deleteNetworkInstanceMethod, + &manage.DeleteNetworkInstanceRequest{InstIds: []*common.UUID{id}}, + "", + new(manage.DeleteNetworkInstanceResponse), + ).(*manage.DeleteNetworkInstanceResponse) + if len(deleted.RemainInstIds) != 0 || len(host.Instances()) != 0 { + t.Fatalf("managed instance remained after delete: %v", deleted.RemainInstIds) + } +} + +func callManagement( + t *testing.T, + host *Host, + ctx context.Context, + method string, + request proto.Message, + preparedConfig string, + response proto.Message, +) proto.Message { + t.Helper() + encodedRequest, err := proto.Marshal(request) + if err != nil { + t.Fatalf("encode %s request: %v", method, err) + } + var prepared *string + var preparedInstanceID *common.UUID + if preparedConfig != "" { + prepared = &preparedConfig + } + if request, ok := request.(*manage.RunNetworkInstanceRequest); ok { + preparedInstanceID = request.InstId + } + encodedEnvelope, err := proto.Marshal(&common.HostManagementRequest{ + Rpc: &common.DirectRpcRequest{ + FullMethodName: method, + Request: encodedRequest, + }, + PreparedConfig: prepared, + PreparedInstanceId: preparedInstanceID, + }) + if err != nil { + t.Fatalf("encode %s envelope: %v", method, err) + } + var result common.RpcResponse + if err := proto.Unmarshal(host.manager.handle(ctx, encodedEnvelope), &result); err != nil { + t.Fatalf("decode %s envelope: %v", method, err) + } + if result.Error != nil { + t.Fatalf("%s failed: %v", method, result.Error) + } + if err := proto.Unmarshal(result.Response, response); err != nil { + t.Fatalf("decode %s response: %v", method, err) + } + return response +} + +func managementTestConfig(t *testing.T, name string) InstanceConfig { + t.Helper() + config, err := NewInstanceConfigBuilder(name). + NetworkSecret("test"). + Hostname(fmt.Sprintf("%s-host", name)). + IPv4(netip.MustParsePrefix("10.144.0.1/24")). + P2P(P2PPolicy{Disable: true}). + Encryption(false). + Build() + if err != nil { + t.Fatalf("build management test config: %v", err) + } + return config +} diff --git a/easytier-go/internal/host/rpc.go b/easytier-go/internal/host/rpc.go new file mode 100644 index 00000000..61edd1dc --- /dev/null +++ b/easytier-go/internal/host/rpc.go @@ -0,0 +1,169 @@ +package host + +import ( + "context" + "fmt" + "time" + + apiinstance "github.com/EasyTier/EasyTier/easytier-go/proto/api/instance" + "github.com/EasyTier/EasyTier/easytier-go/proto/common" + "google.golang.org/protobuf/proto" +) + +const ( + listPeerRPCMethod = "api.instance.PeerManageRpc.ListPeer" + listRouteRPCMethod = "api.instance.PeerManageRpc.ListRoute" + showNodeInfoRPCMethod = "api.instance.PeerManageRpc.ShowNodeInfo" + foreignNetworkSummaryRPCMethod = "api.instance.PeerManageRpc.GetForeignNetworkSummary" +) + +// PeerInfo describes one peer visible to an EasyTier instance. +type PeerInfo = apiinstance.PeerInfo + +// Route describes one route visible to an EasyTier instance. +type Route = apiinstance.Route + +// ListPeer returns the peers visible to this EasyTier instance. +func (instance *Instance) ListPeer( + ctx context.Context, +) ([]*PeerInfo, error) { + response, err := instance.listPeerResponse(ctx) + if err != nil { + return nil, err + } + return response.PeerInfos, nil +} + +// ListRoute returns the routing table visible to this EasyTier instance. +func (instance *Instance) ListRoute( + ctx context.Context, +) ([]*Route, error) { + response, err := instance.listRouteResponse(ctx) + if err != nil { + return nil, err + } + return response.Routes, nil +} + +func (instance *Instance) listPeerResponse( + ctx context.Context, +) (*apiinstance.ListPeerResponse, error) { + response := new(apiinstance.ListPeerResponse) + if err := instance.callRPC(ctx, listPeerRPCMethod, response); err != nil { + return nil, err + } + return response, nil +} + +func (instance *Instance) listRouteResponse( + ctx context.Context, +) (*apiinstance.ListRouteResponse, error) { + response := new(apiinstance.ListRouteResponse) + if err := instance.callRPC(ctx, listRouteRPCMethod, response); err != nil { + return nil, err + } + return response, nil +} + +func (instance *Instance) showNodeInfo( + ctx context.Context, +) (*apiinstance.ShowNodeInfoResponse, error) { + response := new(apiinstance.ShowNodeInfoResponse) + if err := instance.callRPC(ctx, showNodeInfoRPCMethod, response); err != nil { + return nil, err + } + return response, nil +} + +func (instance *Instance) foreignNetworkSummary( + ctx context.Context, +) (*apiinstance.GetForeignNetworkSummaryResponse, error) { + response := new(apiinstance.GetForeignNetworkSummaryResponse) + if err := instance.callRPC( + ctx, + foreignNetworkSummaryRPCMethod, + response, + ); err != nil { + return nil, err + } + return response, nil +} + +func (instance *Instance) callRPC( + ctx context.Context, + fullMethodName string, + response proto.Message, +) error { + return instance.callRPCRequest(ctx, fullMethodName, nil, response) +} + +func (instance *Instance) callRPCRequest( + ctx context.Context, + fullMethodName string, + request proto.Message, + response proto.Message, +) error { + if instance == nil || instance.engine == nil { + return fmt.Errorf("call RPC through nil EasyTier instance") + } + if ctx == nil { + return fmt.Errorf("call EasyTier RPC with nil context") + } + if err := ctx.Err(); err != nil { + return err + } + var timeoutMillis *uint64 + if deadline, exists := ctx.Deadline(); exists { + remaining := time.Until(deadline) + if remaining <= 0 { + return context.DeadlineExceeded + } + millis := uint64(remaining / time.Millisecond) + if remaining%time.Millisecond != 0 { + millis++ + } + timeoutMillis = &millis + } + var payload []byte + var err error + if request != nil { + payload, err = proto.Marshal(request) + if err != nil { + return fmt.Errorf("encode EasyTier RPC %s request: %w", fullMethodName, err) + } + } + encodedRequest, err := proto.Marshal(&common.DirectRpcRequest{ + FullMethodName: fullMethodName, + Request: payload, + TimeoutMs: timeoutMillis, + }) + if err != nil { + return fmt.Errorf("encode EasyTier RPC envelope: %w", err) + } + encodedResponse, err := instance.engine.RPC(ctx, encodedRequest) + if contextErr := ctx.Err(); contextErr != nil { + return contextErr + } + if err != nil { + return err + } + var envelope common.RpcResponse + if err := proto.Unmarshal(encodedResponse, &envelope); err != nil { + return fmt.Errorf("decode EasyTier RPC %s response: %w", fullMethodName, err) + } + if envelope.Error != nil { + return fmt.Errorf( + "EasyTier RPC %s failed: %s", + fullMethodName, + envelope.Error, + ) + } + if err := proto.Unmarshal(envelope.Response, response); err != nil { + return fmt.Errorf( + "decode EasyTier RPC %s payload: %w", + fullMethodName, + err, + ) + } + return nil +} diff --git a/easytier-go/internal/host/web_client.go b/easytier-go/internal/host/web_client.go new file mode 100644 index 00000000..211ddf4c --- /dev/null +++ b/easytier-go/internal/host/web_client.go @@ -0,0 +1,65 @@ +package host + +import ( + "context" + "fmt" + "os" + "runtime" + + "github.com/EasyTier/EasyTier/easytier-go/internal/engine" +) + +type WebClientOptions struct { + // Endpoint is a tcp:// or udp:// config-server URL, or a shorthand token. + Endpoint string + // MachineID is the stable UUID used to identify this host. + MachineID string + Hostname string + SecureMode bool +} + +type WebClient struct { + engine *engine.WebClient +} + +// ConnectWebClient connects this Host to an EasyTier configuration server. +func (host *Host) ConnectWebClient( + ctx context.Context, + options WebClientOptions, +) (*WebClient, error) { + if host == nil || host.engine == nil { + return nil, fmt.Errorf("connect WebClient with nil EasyTier host") + } + if ctx == nil { + return nil, fmt.Errorf("connect EasyTier WebClient with nil context") + } + if options.Hostname == "" { + hostname, err := os.Hostname() + if err != nil { + return nil, fmt.Errorf("resolve WebClient hostname: %w", err) + } + options.Hostname = hostname + } + client, err := host.engine.CreateWebClient(ctx, engine.WebClientOptions{ + Endpoint: options.Endpoint, + MachineID: options.MachineID, + Hostname: options.Hostname, + SecureMode: options.SecureMode, + OSType: runtime.GOOS, + }) + if err != nil { + return nil, err + } + return &WebClient{engine: client}, nil +} + +func (client *WebClient) Connected() bool { + return client != nil && client.engine != nil && client.engine.Connected() +} + +func (client *WebClient) Close(ctx context.Context) error { + if client == nil || client.engine == nil { + return nil + } + return client.engine.Close(ctx) +} diff --git a/easytier-go/internal/hostabi/adapter.go b/easytier-go/internal/hostabi/adapter.go new file mode 100644 index 00000000..abee2952 --- /dev/null +++ b/easytier-go/internal/hostabi/adapter.go @@ -0,0 +1,157 @@ +package hostabi + +import ( + "context" + "fmt" + + "github.com/EasyTier/EasyTier/easytier-go/internal/reactor" + "github.com/metacubex/wazero" +) + +const importModule = "easytier_host" + +type Adapter struct { + reactor *reactor.Reactor + aeads aeadCache +} + +func New(runtime *reactor.Reactor) (*Adapter, error) { + if runtime == nil { + return nil, fmt.Errorf("create host ABI adapter with nil reactor") + } + return &Adapter{reactor: runtime}, nil +} + +func (adapter *Adapter) Instantiate(ctx context.Context, runtime wazero.Runtime) error { + if runtime == nil { + return fmt.Errorf("instantiate host ABI with nil wazero runtime") + } + _, err := runtime.NewHostModuleBuilder(importModule). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.emitEventFunction(), + emitEventParameterTypes, + hostI32ResultTypes, + ). + Export("emit_event"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.cryptoAEADFunction(false), + cryptoAEADParameterTypes, + hostI32ResultTypes, + ). + Export("crypto_aead_seal"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.cryptoAEADFunction(true), + cryptoAEADParameterTypes, + hostI32ResultTypes, + ). + Export("crypto_aead_open"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.startReadFunction(), + startUDPReceiveParameterTypes, + hostI32ResultTypes, + ). + Export("start_read"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.takeReadFunction(), + takeReadParameterTypes, + hostI32ResultTypes, + ). + Export("take_read"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.startWriteFunction(), + startWriteParameterTypes, + hostI32ResultTypes, + ). + Export("start_write"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.takeWriteFunction(), + operationParameterTypes, + hostI32ResultTypes, + ). + Export("take_write"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.startUDPReceiveFunction(), + startUDPReceiveParameterTypes, + hostI32ResultTypes, + ). + Export("start_udp_recv"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.takeUDPReceiveFunction(), + takeUDPReceiveParameterTypes, + hostI32ResultTypes, + ). + Export("take_udp_recv"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.tryUDPSendFunction(), + tryUDPSendParameterTypes, + hostI32ResultTypes, + ). + Export("try_udp_send"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.startUDPSendReadyFunction(), + handleOperationParameterTypes, + hostI32ResultTypes, + ). + Export("start_udp_send_ready"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.takeUDPSendReadyFunction(), + operationParameterTypes, + hostI32ResultTypes, + ). + Export("take_udp_send_ready"). + NewFunctionBuilder().WithFunc(adapter.startTCPConnect).Export("start_tcp_connect"). + NewFunctionBuilder().WithFunc(adapter.takeTCPConnect).Export("take_tcp_connect"). + NewFunctionBuilder().WithFunc(adapter.startUDPBind).Export("start_udp_bind"). + NewFunctionBuilder().WithFunc(adapter.takeUDPBind).Export("take_udp_bind"). + NewFunctionBuilder().WithFunc(adapter.startTCPListen).Export("start_tcp_bind"). + NewFunctionBuilder().WithFunc(adapter.takeTCPListen).Export("take_tcp_bind"). + NewFunctionBuilder().WithFunc(adapter.startTCPAccept).Export("start_tcp_accept"). + NewFunctionBuilder().WithFunc(adapter.takeTCPAccept).Export("take_tcp_accept"). + NewFunctionBuilder().WithFunc(adapter.startDNSAddress).Export("start_dns_resolve"). + NewFunctionBuilder().WithFunc(adapter.takeDNSAddress).Export("take_dns_resolve"). + NewFunctionBuilder().WithFunc(adapter.startDNSTXT).Export("start_dns_txt"). + NewFunctionBuilder().WithFunc(adapter.takeDNSTXT).Export("take_dns_txt"). + NewFunctionBuilder().WithFunc(adapter.startDNSSRV).Export("start_dns_srv"). + NewFunctionBuilder().WithFunc(adapter.takeDNSSRV).Export("take_dns_srv"). + NewFunctionBuilder().WithFunc(adapter.startLocalAddrForRemote).Export("start_local_addr_for_remote"). + NewFunctionBuilder().WithFunc(adapter.takeLocalAddrForRemote).Export("take_local_addr_for_remote"). + NewFunctionBuilder().WithFunc(adapter.startManagement).Export("start_management_call"). + NewFunctionBuilder().WithFunc(adapter.takeManagement).Export("take_management_call"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.tryPacketWriteFunction(), + tryPacketWriteParameterTypes, + hostI32ResultTypes, + ). + Export("try_packet_write"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.startPacketWriteReadyFunction(), + handleOperationParameterTypes, + hostI32ResultTypes, + ). + Export("start_packet_write_ready"). + NewFunctionBuilder(). + WithGoModuleFunction( + adapter.takePacketWriteReadyFunction(), + operationParameterTypes, + hostI32ResultTypes, + ). + Export("take_packet_write_ready"). + NewFunctionBuilder().WithFunc(adapter.cancelOperation).Export("cancel_operation"). + NewFunctionBuilder().WithFunc(adapter.closeHandle).Export("close"). + Instantiate(ctx) + return err +} diff --git a/easytier-go/internal/hostabi/conformance_test.go b/easytier-go/internal/hostabi/conformance_test.go new file mode 100644 index 00000000..42f6e996 --- /dev/null +++ b/easytier-go/internal/hostabi/conformance_test.go @@ -0,0 +1,546 @@ +package hostabi + +import ( + "context" + "fmt" + "io" + "net" + "net/netip" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/internal/contextutil" + "github.com/EasyTier/EasyTier/easytier-go/internal/reactor" + "github.com/EasyTier/EasyTier/easytier-go/platform" + "github.com/metacubex/wazero" + "github.com/metacubex/wazero/api" + "github.com/metacubex/wazero/imports/wasi_snapshot_preview1" +) + +const ( + probeTimerProgress = 1 << 0 + probeSecondSocketProgress = 1 << 1 + probePendingReadCompleted = 1 << 2 + probePendingReadIsolated = 1 << 3 + probeDone = 1 << 4 + probeError = 1 << 31 +) + +func TestStreamABIConformance(t *testing.T) { + const ( + pendingHandle uint64 = 1<<40 | 1 + activeHandle uint64 = 1<<40 | 2 + ) + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + pendingHost, pendingPeer := net.Pipe() + activeHost, activePeer := net.Pipe() + defer pendingPeer.Close() + defer activePeer.Close() + hostReactor := reactor.New(ctx, reactor.Options{ + InitialStreams: map[uint64]net.Conn{ + pendingHandle: pendingHost, + activeHandle: activeHost, + }, + }) + defer hostReactor.Close() + module := instantiateProbe(t, ctx, hostReactor) + + results, err := module.ExportedFunction("init_opaque_probe").Call( + ctx, + pendingHandle, + activeHandle, + ) + requireStatus(t, "initialize stream probe", results, err, 0) + status := driveProbe(t, ctx, module, "drive_opaque_probe") + + echo := make(chan error, 1) + go func() { + if err := activePeer.SetDeadline(time.Now().Add(10 * time.Second)); err != nil { + echo <- err + return + } + if _, err := activePeer.Write([]byte{0x5a}); err != nil { + echo <- err + return + } + got := make([]byte, 1) + if _, err := io.ReadFull(activePeer, got); err != nil { + echo <- err + return + } + if got[0] != 0x5a { + echo <- fmt.Errorf("echo = %x, want 5a", got) + return + } + echo <- nil + }() + + for status&probeSecondSocketProgress == 0 { + waitCompletion(t, ctx, hostReactor) + status = driveProbe(t, ctx, module, "drive_opaque_probe") + } + for status&probeDone == 0 { + timer := time.NewTimer(5 * time.Millisecond) + select { + case <-hostReactor.Completions(): + stopTimer(timer) + case <-timer.C: + case <-ctx.Done(): + stopTimer(timer) + t.Fatalf("wait for stream probe: %v", ctx.Err()) + } + status = driveProbe(t, ctx, module, "drive_opaque_probe") + } + want := uint32( + probeTimerProgress | + probeSecondSocketProgress | + probePendingReadIsolated | + probeDone, + ) + if status != want || status&probePendingReadCompleted != 0 { + t.Fatalf("stream probe status = 0x%x, want 0x%x", status, want) + } + select { + case err := <-echo: + if err != nil { + t.Fatal(err) + } + case <-ctx.Done(): + t.Fatalf("wait for stream echo: %v", ctx.Err()) + } +} + +func TestUDPABIConformance(t *testing.T) { + const handle uint64 = 1<<40 | 3 + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + hostPacket, err := net.ListenPacket("udp4", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen host UDP: %v", err) + } + peerPacket, err := net.ListenPacket("udp4", "127.0.0.1:0") + if err != nil { + hostPacket.Close() + t.Fatalf("listen peer UDP: %v", err) + } + defer peerPacket.Close() + writes := make(chan packetWrite, 1) + hostReactor := reactor.New(ctx, reactor.Options{ + InitialDatagrams: map[uint64]net.PacketConn{handle: &recordingPacketConn{ + PacketConn: hostPacket, + writes: writes, + }}, + }) + defer hostReactor.Close() + module := instantiateProbe(t, ctx, hostReactor) + + results, err := module.ExportedFunction("init_udp_probe").Call(ctx, handle) + requireStatus(t, "initialize UDP probe", results, err, 0) + if status := driveProbe(t, ctx, module, "drive_udp_probe"); status != 0 { + t.Fatalf("initial UDP status = 0x%x", status) + } + if err := peerPacket.SetDeadline(time.Now().Add(10 * time.Second)); err != nil { + t.Fatal(err) + } + if _, err := peerPacket.WriteTo([]byte("udp"), hostPacket.LocalAddr()); err != nil { + t.Fatalf("send UDP payload: %v", err) + } + waitCompletion(t, ctx, hostReactor) + if status := driveProbe(t, ctx, module, "drive_udp_probe"); status != probeDone { + t.Fatalf("UDP probe status = 0x%x, want 0x%x", status, probeDone) + } + select { + case write := <-writes: + if write.err != nil { + t.Fatalf("host UDP write to %v: %v", write.peer, write.err) + } + if write.peer.String() != peerPacket.LocalAddr().String() { + t.Fatalf("host UDP peer = %v, want %v", write.peer, peerPacket.LocalAddr()) + } + case <-ctx.Done(): + t.Fatalf("wait for host UDP write: %v", ctx.Err()) + } + buffer := make([]byte, 16) + n, source, err := peerPacket.ReadFrom(buffer) + if err != nil { + t.Fatalf("read UDP echo: %v", err) + } + if string(buffer[:n]) != "udp" || source.String() != hostPacket.LocalAddr().String() { + t.Fatalf("UDP echo = %q from %v", buffer[:n], source) + } +} + +type packetWrite struct { + peer net.Addr + err error +} + +type recordingPacketConn struct { + net.PacketConn + writes chan<- packetWrite +} + +func (connection *recordingPacketConn) WriteTo( + packet []byte, + peer net.Addr, +) (int, error) { + written, err := connection.PacketConn.WriteTo(packet, peer) + connection.writes <- packetWrite{peer: peer, err: err} + return written, err +} + +type probeDNSResolver struct { + mu sync.Mutex + queries []platform.DNSQuery + addressStarted chan struct{} + releaseAddress chan struct{} + startOnce sync.Once +} + +func (resolver *probeDNSResolver) record(query platform.DNSQuery) { + resolver.mu.Lock() + resolver.queries = append(resolver.queries, query) + resolver.mu.Unlock() +} + +func (resolver *probeDNSResolver) LookupIP( + ctx context.Context, + query platform.DNSQuery, +) ([]netip.Addr, error) { + resolver.record(query) + resolver.startOnce.Do(func() { close(resolver.addressStarted) }) + select { + case <-resolver.releaseAddress: + case <-ctx.Done(): + return nil, ctx.Err() + } + return []netip.Addr{ + netip.MustParseAddr("192.0.2.1"), + netip.MustParseAddr("2001:db8::1"), + }, nil +} + +func (resolver *probeDNSResolver) LookupTXT( + _ context.Context, + query platform.DNSQuery, +) (string, error) { + resolver.record(query) + return "tcp://peer.example:11010", nil +} + +func (resolver *probeDNSResolver) LookupSRV( + _ context.Context, + query platform.DNSQuery, +) ([]*net.SRV, error) { + resolver.record(query) + return []*net.SRV{{ + Target: "peer.example.", + Port: 11010, + Priority: 10, + Weight: 20, + }}, nil +} + +func TestDNSABIConformance(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + resolver := &probeDNSResolver{ + addressStarted: make(chan struct{}), + releaseAddress: make(chan struct{}), + } + hostReactor := reactor.New(ctx, reactor.Options{ + Services: platform.Services{DNS: resolver}, + }) + defer hostReactor.Close() + module := instantiateProbe(t, ctx, hostReactor) + + results, err := module.ExportedFunction("init_dns_probe").Call(ctx) + requireStatus(t, "initialize DNS probe", results, err, 0) + if status := driveProbe(t, ctx, module, "drive_dns_probe"); status != 0 { + t.Fatalf("initial DNS status = 0x%x", status) + } + select { + case <-resolver.addressStarted: + case <-ctx.Done(): + t.Fatalf("wait for DNS lookup: %v", ctx.Err()) + } + select { + case <-hostReactor.Completions(): + t.Fatal("DNS completed before resolver release") + default: + } + close(resolver.releaseAddress) + + status := uint32(0) + for status&probeDone == 0 { + waitCompletion(t, ctx, hostReactor) + status = driveProbe(t, ctx, module, "drive_dns_probe") + } + resolver.mu.Lock() + queries := append([]platform.DNSQuery(nil), resolver.queries...) + resolver.mu.Unlock() + if len(queries) != 3 { + t.Fatalf("DNS query count = %d, want 3", len(queries)) + } + assertDNSQuery(t, queries[0], "peer.example", 0, uint32Pointer(7), stringPointer("mihomo")) + assertDNSQuery(t, queries[1], "_easytier.example", 4, nil, nil) + assertDNSQuery(t, queries[2], "_easytier._udp.example", 6, uint32Pointer(9), stringPointer("")) +} + +type probeEnvironment struct { + mu sync.Mutex + remote *net.UDPAddr + context platform.SocketContext + started chan struct{} + released chan struct{} +} + +func (environment *probeEnvironment) LocalAddrForRemote( + ctx context.Context, + remote *net.UDPAddr, + socketContext platform.SocketContext, +) (net.Addr, error) { + environment.mu.Lock() + environment.remote = remote + environment.context = socketContext + environment.mu.Unlock() + close(environment.started) + select { + case <-environment.released: + case <-ctx.Done(): + return nil, ctx.Err() + } + return &net.UDPAddr{IP: net.ParseIP("192.0.2.10"), Port: 40000}, nil +} + +func TestEnvironmentABIConformance(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + environment := &probeEnvironment{ + started: make(chan struct{}), + released: make(chan struct{}), + } + hostReactor := reactor.New(ctx, reactor.Options{ + Services: platform.Services{Environment: environment}, + }) + defer hostReactor.Close() + module := instantiateProbe(t, ctx, hostReactor) + + results, err := module.ExportedFunction("init_environment_probe").Call(ctx) + requireStatus(t, "initialize environment probe", results, err, 0) + if status := driveProbe(t, ctx, module, "drive_environment_probe"); status != 0 { + t.Fatalf("initial environment status = 0x%x", status) + } + select { + case <-environment.started: + case <-ctx.Done(): + t.Fatalf("wait for environment operation: %v", ctx.Err()) + } + close(environment.released) + waitCompletion(t, ctx, hostReactor) + if status := driveProbe(t, ctx, module, "drive_environment_probe"); status != probeDone { + t.Fatalf("environment status = 0x%x, want 0x%x", status, probeDone) + } + environment.mu.Lock() + defer environment.mu.Unlock() + if environment.remote.String() != "203.0.113.2:443" || + environment.context.IPVersion != platform.IPVersionBoth || + environment.context.SocketMark != nil || + environment.context.NetNS != nil { + t.Fatalf( + "environment request = remote %v, context %#v", + environment.remote, + environment.context, + ) + } +} + +func TestPacketABIBackpressure(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + hostReactor := reactor.New(ctx, reactor.Options{}) + defer hostReactor.Close() + handle, err := hostReactor.RegisterPacketSink(1) + if err != nil { + t.Fatalf("register packet sink: %v", err) + } + module := instantiateProbe(t, ctx, hostReactor) + results, err := module.ExportedFunction("init_packet_probe").Call(ctx, handle) + requireStatus(t, "initialize packet probe", results, err, 0) + if status := driveProbe(t, ctx, module, "drive_packet_probe"); status != 0 { + t.Fatalf("initial packet status = 0x%x", status) + } + select { + case <-hostReactor.Completions(): + t.Fatal("packet writer became ready while sink was full") + default: + } + first, err := hostReactor.ReceivePacket(ctx, handle) + if err != nil || string(first) != "first-packet" { + t.Fatalf("first packet = %q, error %v", first, err) + } + waitCompletion(t, ctx, hostReactor) + if status := driveProbe(t, ctx, module, "drive_packet_probe"); status != probeDone { + t.Fatalf("packet status = 0x%x, want 0x%x", status, probeDone) + } + second, err := hostReactor.ReceivePacket(ctx, handle) + if err != nil || string(second) != "second-packet" { + t.Fatalf("second packet = %q, error %v", second, err) + } +} + +func TestUDPMetadataWireFormat(t *testing.T) { + expected := [udpMetadataLen]byte{ + 0x04, 0xc0, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x05, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xc6, 0x33, 0x64, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + encoded, err := encodeUDPMetadata( + &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 11013}, + net.IPv4(198, 51, 100, 2), + 0, + ) + if err != nil { + t.Fatal(err) + } + if encoded != expected { + t.Fatalf("UDP metadata = %x, want %x", encoded, expected) + } +} + +func instantiateProbe( + t *testing.T, + ctx context.Context, + hostReactor *reactor.Reactor, +) api.Module { + t.Helper() + runtime := wazero.NewRuntimeWithConfig( + ctx, + wazero.NewRuntimeConfig().WithCloseOnContextDone(true), + ) + t.Cleanup(func() { + if err := runtime.Close(contextutil.WithoutCancel(ctx)); err != nil { + t.Errorf("close probe runtime: %v", err) + } + }) + adapter, err := New(hostReactor) + if err != nil { + t.Fatalf("create host ABI adapter: %v", err) + } + if err := adapter.Instantiate(ctx, runtime); err != nil { + t.Fatalf("instantiate host ABI: %v", err) + } + wasi_snapshot_preview1.MustInstantiate(ctx, runtime) + wasm, err := os.ReadFile(filepath.Join("..", "..", "testdata", "wasi_socket_guest.wasm")) + if err != nil { + t.Fatalf("read probe WASM: %v", err) + } + compiled, err := runtime.CompileModule(ctx, wasm) + if err != nil { + t.Fatalf("compile probe WASM: %v", err) + } + t.Cleanup(func() { _ = compiled.Close(contextutil.WithoutCancel(ctx)) }) + module, err := runtime.InstantiateModule( + ctx, + compiled, + wazero.NewModuleConfig(). + WithStartFunctions("_initialize"). + WithSysWalltime(). + WithSysNanotime(). + WithSysNanosleep(), + ) + if err != nil { + t.Fatalf("instantiate probe WASM: %v", err) + } + return module +} + +func driveProbe( + t *testing.T, + ctx context.Context, + module api.Module, + export string, +) uint32 { + t.Helper() + results, err := module.ExportedFunction(export).Call(ctx) + if err != nil || len(results) != 1 { + t.Fatalf("%s: results=%v error=%v", export, results, err) + } + status := uint32(results[0]) + if status&probeError != 0 { + t.Fatalf("%s failed with status 0x%x", export, status) + } + return status +} + +func requireStatus( + t *testing.T, + operation string, + results []uint64, + err error, + want int32, +) { + t.Helper() + if err != nil || len(results) != 1 || int32(results[0]) != want { + t.Fatalf("%s: results=%v error=%v want=%d", operation, results, err, want) + } +} + +func waitCompletion( + t *testing.T, + ctx context.Context, + hostReactor *reactor.Reactor, +) { + t.Helper() + select { + case <-hostReactor.Completions(): + case <-ctx.Done(): + t.Fatalf("wait for host completion: %v", ctx.Err()) + } +} + +func assertDNSQuery( + t *testing.T, + query platform.DNSQuery, + host string, + ipVersion uint8, + mark *uint32, + netns *string, +) { + t.Helper() + if query.Host != host || + query.IPVersion != ipVersion || + !equalPointer(query.SocketMark, mark) || + !equalPointer(query.NetNS, netns) { + t.Fatalf("DNS query = %#v", query) + } +} + +func equalPointer[T comparable](left, right *T) bool { + return left == nil && right == nil || + left != nil && right != nil && *left == *right +} + +func uint32Pointer(value uint32) *uint32 { + return &value +} + +func stringPointer(value string) *string { + return &value +} + +func stopTimer(timer *time.Timer) { + if timer == nil || timer.Stop() { + return + } + select { + case <-timer.C: + default: + } +} diff --git a/easytier-go/internal/hostabi/connect_error_other.go b/easytier-go/internal/hostabi/connect_error_other.go new file mode 100644 index 00000000..b20648a0 --- /dev/null +++ b/easytier-go/internal/hostabi/connect_error_other.go @@ -0,0 +1,11 @@ +//go:build js || plan9 || wasip1 + +package hostabi + +func isConnectionRefused(_ error) bool { return false } + +func isConnectionAborted(_ error) bool { return false } + +func isConnectionReset(_ error) bool { return false } + +func isNotConnected(_ error) bool { return false } diff --git a/easytier-go/internal/hostabi/connect_error_unix.go b/easytier-go/internal/hostabi/connect_error_unix.go new file mode 100644 index 00000000..d7e72c1d --- /dev/null +++ b/easytier-go/internal/hostabi/connect_error_unix.go @@ -0,0 +1,24 @@ +//go:build aix || android || darwin || dragonfly || freebsd || illumos || ios || linux || netbsd || openbsd || solaris + +package hostabi + +import ( + "errors" + "syscall" +) + +func isConnectionRefused(err error) bool { + return errors.Is(err, syscall.ECONNREFUSED) +} + +func isConnectionAborted(err error) bool { + return errors.Is(err, syscall.ECONNABORTED) +} + +func isConnectionReset(err error) bool { + return errors.Is(err, syscall.ECONNRESET) +} + +func isNotConnected(err error) bool { + return errors.Is(err, syscall.ENOTCONN) +} diff --git a/easytier-go/internal/hostabi/connect_error_windows.go b/easytier-go/internal/hostabi/connect_error_windows.go new file mode 100644 index 00000000..5c3bc2f8 --- /dev/null +++ b/easytier-go/internal/hostabi/connect_error_windows.go @@ -0,0 +1,31 @@ +//go:build windows + +package hostabi + +import ( + "errors" + "syscall" +) + +const ( + wsaNotConnected = syscall.Errno(10057) + wsaConnectionAborted = syscall.Errno(10053) + wsaConnectionReset = syscall.Errno(10054) + wsaConnectionRefused = syscall.Errno(10061) +) + +func isConnectionRefused(err error) bool { + return errors.Is(err, wsaConnectionRefused) +} + +func isConnectionAborted(err error) bool { + return errors.Is(err, wsaConnectionAborted) +} + +func isConnectionReset(err error) bool { + return errors.Is(err, wsaConnectionReset) +} + +func isNotConnected(err error) bool { + return errors.Is(err, wsaNotConnected) +} diff --git a/easytier-go/internal/hostabi/crypto.go b/easytier-go/internal/hostabi/crypto.go new file mode 100644 index 00000000..5d51356b --- /dev/null +++ b/easytier-go/internal/hostabi/crypto.go @@ -0,0 +1,236 @@ +package hostabi + +import ( + "context" + "crypto/aes" + "crypto/cipher" + "sync" + + "github.com/metacubex/wazero/api" +) + +const ( + aeadAES128GCM uint32 = 1 + aeadAES256GCM uint32 = 2 + aeadCacheCapacity = 64 + statusCryptoAuthFailed int32 = -10 + statusCryptoFallback int32 = -11 +) + +var ( + cryptoAEADParameterTypes = []api.ValueType{ + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + } + hostI32ResultTypes = []api.ValueType{api.ValueTypeI32} +) + +type aeadCacheKey struct { + algorithm uint32 + key [32]byte +} + +type aeadCache struct { + mu sync.Mutex + entries map[aeadCacheKey]cipher.AEAD + order [aeadCacheCapacity]aeadCacheKey + size int + next int +} + +func (adapter *Adapter) cryptoAEADFunction(open bool) api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + function := adapter.cryptoAEADSeal + if open { + function = adapter.cryptoAEADOpen + } + stack[0] = api.EncodeI32(function( + ctx, + module, + api.DecodeU32(stack[0]), + api.DecodeU32(stack[1]), + api.DecodeU32(stack[2]), + api.DecodeU32(stack[3]), + api.DecodeU32(stack[4]), + api.DecodeU32(stack[5]), + api.DecodeU32(stack[6]), + api.DecodeU32(stack[7]), + api.DecodeU32(stack[8]), + )) + }) +} + +func (cache *aeadCache) get( + algorithm uint32, + key []byte, +) (cipher.AEAD, bool) { + if (algorithm != aeadAES128GCM || len(key) != 16) && + (algorithm != aeadAES256GCM || len(key) != 32) { + return nil, false + } + + var cacheKey aeadCacheKey + cacheKey.algorithm = algorithm + copy(cacheKey.key[:], key) + + cache.mu.Lock() + defer cache.mu.Unlock() + if cached := cache.entries[cacheKey]; cached != nil { + return cached, true + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, false + } + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, false + } + if cache.entries == nil { + cache.entries = make(map[aeadCacheKey]cipher.AEAD, aeadCacheCapacity) + } + if cache.size < aeadCacheCapacity { + cache.order[cache.size] = cacheKey + cache.size++ + } else { + delete(cache.entries, cache.order[cache.next]) + cache.order[cache.next] = cacheKey + cache.next = (cache.next + 1) % aeadCacheCapacity + } + cache.entries[cacheKey] = aead + return aead, true +} + +func (adapter *Adapter) cryptoAEADSeal( + _ context.Context, + module api.Module, + algorithm uint32, + keyPointer uint32, + keyLength uint32, + noncePointer uint32, + nonceLength uint32, + aadPointer uint32, + aadLength uint32, + bufferPointer uint32, + textLength uint32, +) int32 { + nonce, aad, aead, ok := adapter.cryptoAEADInputs( + module, + algorithm, + keyPointer, + keyLength, + noncePointer, + nonceLength, + aadPointer, + aadLength, + ) + if !ok { + return statusCryptoFallback + } + if textLength > ^uint32(0)-uint32(aead.Overhead()) { + return statusCryptoFallback + } + buffer, ok := module.Memory().Read( + bufferPointer, + textLength+uint32(aead.Overhead()), + ) + if !ok { + return statusCryptoFallback + } + aead.Seal(buffer[:0], nonce, buffer[:textLength], aad) + return 0 +} + +func (adapter *Adapter) cryptoAEADOpen( + _ context.Context, + module api.Module, + algorithm uint32, + keyPointer uint32, + keyLength uint32, + noncePointer uint32, + nonceLength uint32, + aadPointer uint32, + aadLength uint32, + bufferPointer uint32, + textLength uint32, +) int32 { + nonce, aad, aead, ok := adapter.cryptoAEADInputs( + module, + algorithm, + keyPointer, + keyLength, + noncePointer, + nonceLength, + aadPointer, + aadLength, + ) + if !ok { + return statusCryptoFallback + } + if textLength > ^uint32(0)-uint32(aead.Overhead()) { + return statusCryptoFallback + } + buffer, ok := module.Memory().Read( + bufferPointer, + textLength+uint32(aead.Overhead()), + ) + if !ok { + return statusCryptoFallback + } + opened, err := aead.Open(buffer[:0], nonce, buffer, aad) + if err != nil { + return statusCryptoAuthFailed + } + if len(opened) != int(textLength) { + return statusCryptoAuthFailed + } + return 0 +} + +func (adapter *Adapter) cryptoAEADInputs( + module api.Module, + algorithm uint32, + keyPointer uint32, + keyLength uint32, + noncePointer uint32, + nonceLength uint32, + aadPointer uint32, + aadLength uint32, +) ([]byte, []byte, cipher.AEAD, bool) { + key, ok := readCryptoBytes(module, keyPointer, keyLength) + if !ok { + return nil, nil, nil, false + } + aead, ok := adapter.aeads.get(algorithm, key) + if !ok { + return nil, nil, nil, false + } + nonce, ok := readCryptoBytes(module, noncePointer, nonceLength) + if !ok || len(nonce) != aead.NonceSize() { + return nil, nil, nil, false + } + aad, ok := readCryptoBytes(module, aadPointer, aadLength) + if !ok { + return nil, nil, nil, false + } + return nonce, aad, aead, true +} + +func readCryptoBytes( + module api.Module, + pointer uint32, + length uint32, +) ([]byte, bool) { + if length == 0 { + return nil, true + } + return module.Memory().Read(pointer, length) +} diff --git a/easytier-go/internal/hostabi/crypto_test.go b/easytier-go/internal/hostabi/crypto_test.go new file mode 100644 index 00000000..91d0008d --- /dev/null +++ b/easytier-go/internal/hostabi/crypto_test.go @@ -0,0 +1,146 @@ +package hostabi + +import ( + "bytes" + "context" + "testing" + + "github.com/metacubex/wazero/api" + "github.com/metacubex/wazero/experimental/wazerotest" +) + +func TestCryptoAEADAES128GCMVector(t *testing.T) { + adapter, module := newCryptoTestModule(t) + key := make([]byte, 16) + nonce := make([]byte, 12) + plaintext := make([]byte, 16) + writeCryptoTestBytes(t, module, 0, key) + writeCryptoTestBytes(t, module, 32, nonce) + writeCryptoTestBytes(t, module, 64, append(plaintext, make([]byte, 16)...)) + + status := adapter.cryptoAEADSeal( + context.Background(), + module, + aeadAES128GCM, + 0, + uint32(len(key)), + 32, + uint32(len(nonce)), + 0, + 0, + 64, + uint32(len(plaintext)), + ) + if status != 0 { + t.Fatalf("seal status = %d", status) + } + got, _ := module.Memory().Read(64, 32) + want := []byte{ + 0x03, 0x88, 0xda, 0xce, 0x60, 0xb6, 0xa3, 0x92, + 0xf3, 0x28, 0xc2, 0xb9, 0x71, 0xb2, 0xfe, 0x78, + 0xab, 0x6e, 0x47, 0xd4, 0x2c, 0xec, 0x13, 0xbd, + 0xf5, 0x3a, 0x67, 0xb2, 0x12, 0x57, 0xbd, 0xdf, + } + if !bytes.Equal(got, want) { + t.Fatalf("sealed bytes = %x, want %x", got, want) + } + + status = adapter.cryptoAEADOpen( + context.Background(), + module, + aeadAES128GCM, + 0, + uint32(len(key)), + 32, + uint32(len(nonce)), + 0, + 0, + 64, + uint32(len(plaintext)), + ) + if status != 0 { + t.Fatalf("open status = %d", status) + } + got, _ = module.Memory().Read(64, uint32(len(plaintext))) + if !bytes.Equal(got, plaintext) { + t.Fatalf("opened bytes = %x, want %x", got, plaintext) + } +} + +func TestCryptoAEADFallbackDoesNotModifyBuffer(t *testing.T) { + adapter, module := newCryptoTestModule(t) + key := make([]byte, 32) + nonce := make([]byte, 12) + buffer := bytes.Repeat([]byte{7}, 32) + writeCryptoTestBytes(t, module, 0, key) + writeCryptoTestBytes(t, module, 32, nonce) + writeCryptoTestBytes(t, module, 64, buffer) + + status := adapter.cryptoAEADSeal( + context.Background(), + module, + 3, + 0, + uint32(len(key)), + 32, + uint32(len(nonce)), + 0, + 0, + 64, + 16, + ) + if status != statusCryptoFallback { + t.Fatalf("seal status = %d, want fallback", status) + } + got, _ := module.Memory().Read(64, uint32(len(buffer))) + if !bytes.Equal(got, buffer) { + t.Fatalf("fallback changed buffer: %x", got) + } +} + +func TestCryptoAEADOpenReportsAuthenticationFailure(t *testing.T) { + adapter, module := newCryptoTestModule(t) + key := make([]byte, 16) + nonce := make([]byte, 12) + buffer := make([]byte, 32) + buffer[len(buffer)-1] = 1 + writeCryptoTestBytes(t, module, 0, key) + writeCryptoTestBytes(t, module, 32, nonce) + writeCryptoTestBytes(t, module, 64, buffer) + + status := adapter.cryptoAEADOpen( + context.Background(), + module, + aeadAES128GCM, + 0, + uint32(len(key)), + 32, + uint32(len(nonce)), + 0, + 0, + 64, + 16, + ) + if status != statusCryptoAuthFailed { + t.Fatalf("open status = %d, want authentication failure", status) + } +} + +func newCryptoTestModule(t *testing.T) (*Adapter, api.Module) { + t.Helper() + return &Adapter{}, wazerotest.NewModule( + wazerotest.NewMemory(wazerotest.PageSize), + ) +} + +func writeCryptoTestBytes( + t *testing.T, + module api.Module, + pointer uint32, + value []byte, +) { + t.Helper() + if !module.Memory().Write(pointer, value) { + t.Fatalf("write %d bytes at %d", len(value), pointer) + } +} diff --git a/easytier-go/internal/hostabi/dns.go b/easytier-go/internal/hostabi/dns.go new file mode 100644 index 00000000..ae53c834 --- /dev/null +++ b/easytier-go/internal/hostabi/dns.go @@ -0,0 +1,132 @@ +package hostabi + +import ( + "context" + + "github.com/EasyTier/EasyTier/easytier-go/internal/reactor" + "github.com/metacubex/wazero/api" +) + +const maxDNSQueryLen = 4096 + +func (adapter *Adapter) startDNSAddress( + _ context.Context, + module api.Module, + operation uint64, + queryPointer uint32, + queryLength uint32, +) int32 { + return adapter.startDNS(module, operation, queryPointer, queryLength, reactor.DNSAddress) +} + +func (adapter *Adapter) startDNSTXT( + _ context.Context, + module api.Module, + operation uint64, + queryPointer uint32, + queryLength uint32, +) int32 { + return adapter.startDNS(module, operation, queryPointer, queryLength, reactor.DNSTXT) +} + +func (adapter *Adapter) startDNSSRV( + _ context.Context, + module api.Module, + operation uint64, + queryPointer uint32, + queryLength uint32, +) int32 { + return adapter.startDNS(module, operation, queryPointer, queryLength, reactor.DNSSRV) +} + +func (adapter *Adapter) startDNS( + module api.Module, + operation uint64, + queryPointer uint32, + queryLength uint32, + kind reactor.DNSKind, +) int32 { + if queryLength == 0 || queryLength > maxDNSQueryLen { + return statusInvalid + } + encoded, ok := module.Memory().Read(queryPointer, queryLength) + if !ok { + return statusMemory + } + query, err := decodeDNSQuery(append([]byte(nil), encoded...)) + if err != nil { + return statusInvalid + } + return operationStatus(adapter.reactor.StartDNS(operation, kind, query)) +} + +func (adapter *Adapter) takeDNSAddress( + _ context.Context, + module api.Module, + operation uint64, + resultPointer uint32, + resultCapacity uint32, +) int32 { + return adapter.takeDNS(module, operation, resultPointer, resultCapacity, reactor.DNSAddress) +} + +func (adapter *Adapter) takeDNSTXT( + _ context.Context, + module api.Module, + operation uint64, + resultPointer uint32, + resultCapacity uint32, +) int32 { + return adapter.takeDNS(module, operation, resultPointer, resultCapacity, reactor.DNSTXT) +} + +func (adapter *Adapter) takeDNSSRV( + _ context.Context, + module api.Module, + operation uint64, + resultPointer uint32, + resultCapacity uint32, +) int32 { + return adapter.takeDNS(module, operation, resultPointer, resultCapacity, reactor.DNSSRV) +} + +func (adapter *Adapter) takeDNS( + module api.Module, + operation uint64, + resultPointer uint32, + resultCapacity uint32, + kind reactor.DNSKind, +) int32 { + result, err := adapter.reactor.DNSResult(operation) + if err != nil { + return operationStatus(err) + } + if result.Kind != kind { + return statusInvalid + } + var encoded []byte + switch kind { + case reactor.DNSAddress: + encoded, err = encodeDNSAddresses(result.Addresses) + case reactor.DNSTXT: + encoded, err = encodeDNSTXT(result.Text) + case reactor.DNSSRV: + encoded, err = encodeDNSSRV(result.Records) + default: + return statusInvalid + } + if err != nil { + _ = adapter.reactor.FinishDNS(operation) + return statusInvalid + } + if resultCapacity < uint32(len(encoded)) { + return int32(len(encoded)) + } + if err := adapter.reactor.FinishDNS(operation); err != nil { + return operationStatus(err) + } + if !module.Memory().Write(resultPointer, encoded) { + return statusMemory + } + return int32(len(encoded)) +} diff --git a/easytier-go/internal/hostabi/dns_wire.go b/easytier-go/internal/hostabi/dns_wire.go new file mode 100644 index 00000000..109fbe04 --- /dev/null +++ b/easytier-go/internal/hostabi/dns_wire.go @@ -0,0 +1,124 @@ +package hostabi + +import ( + "encoding/binary" + "fmt" + "net" + "net/netip" + "unicode/utf8" + + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +const ( + dnsWireVersion = 1 + maxDNSResultLen = 1024 * 1024 +) + +func decodeDNSQuery(encoded []byte) (platform.DNSQuery, error) { + if len(encoded) < 16 || encoded[0] != dnsWireVersion { + return platform.DNSQuery{}, fmt.Errorf("invalid DNS query header") + } + query := platform.DNSQuery{IPVersion: encoded[1]} + if query.IPVersion != 0 && query.IPVersion != 4 && query.IPVersion != 6 { + return platform.DNSQuery{}, fmt.Errorf("invalid DNS IP version") + } + if encoded[2] > 1 { + return platform.DNSQuery{}, fmt.Errorf("invalid DNS socket mark presence") + } + if encoded[2] == 1 { + mark := binary.BigEndian.Uint32(encoded[3:7]) + query.SocketMark = &mark + } else if binary.BigEndian.Uint32(encoded[3:7]) != 0 { + return platform.DNSQuery{}, fmt.Errorf("DNS socket mark value without presence") + } + if encoded[7] > 1 { + return platform.DNSQuery{}, fmt.Errorf("invalid DNS netns presence") + } + offset := 12 + netnsLengthWire := binary.BigEndian.Uint32(encoded[8:12]) + if uint64(netnsLengthWire) > uint64(len(encoded)-offset) { + return platform.DNSQuery{}, fmt.Errorf("truncated DNS netns token") + } + netnsLength := int(netnsLengthWire) + if encoded[7] == 0 && netnsLength != 0 { + return platform.DNSQuery{}, fmt.Errorf("DNS netns length without presence") + } + if encoded[7] == 1 { + netnsBytes := encoded[offset : offset+netnsLength] + if !utf8.Valid(netnsBytes) { + return platform.DNSQuery{}, fmt.Errorf("DNS netns token is not UTF-8") + } + netns := string(netnsBytes) + query.NetNS = &netns + } + offset += netnsLength + if len(encoded)-offset < 4 { + return platform.DNSQuery{}, fmt.Errorf("missing DNS host length") + } + hostLengthWire := binary.BigEndian.Uint32(encoded[offset : offset+4]) + offset += 4 + if hostLengthWire == 0 || uint64(hostLengthWire) != uint64(len(encoded)-offset) { + return platform.DNSQuery{}, fmt.Errorf("invalid DNS host length") + } + host := encoded[offset:] + if !utf8.Valid(host) { + return platform.DNSQuery{}, fmt.Errorf("DNS host is not UTF-8") + } + query.Host = string(host) + return query, nil +} + +func encodeDNSAddresses(addresses []netip.Addr) ([]byte, error) { + result := make([]byte, 4) + binary.BigEndian.PutUint32(result, uint32(len(addresses))) + for _, address := range addresses { + if address.Is4() { + ipv4 := address.As4() + result = append(result, 4) + result = append(result, ipv4[:]...) + continue + } + if !address.Is6() { + return nil, fmt.Errorf("invalid DNS address") + } + ipv6 := address.As16() + result = append(result, 6) + result = append(result, ipv6[:]...) + } + return boundedDNSResult(result) +} + +func encodeDNSTXT(text string) ([]byte, error) { + if !utf8.ValidString(text) { + return nil, fmt.Errorf("normalized DNS TXT value is not UTF-8") + } + result := make([]byte, 4, 4+len(text)) + binary.BigEndian.PutUint32(result, uint32(len(text))) + result = append(result, text...) + return boundedDNSResult(result) +} + +func encodeDNSSRV(records []*net.SRV) ([]byte, error) { + result := make([]byte, 4) + binary.BigEndian.PutUint32(result, uint32(len(records))) + for _, record := range records { + if record == nil { + return nil, fmt.Errorf("nil DNS SRV record") + } + target := []byte(record.Target) + result = binary.BigEndian.AppendUint16(result, record.Priority) + result = binary.BigEndian.AppendUint16(result, record.Weight) + result = binary.BigEndian.AppendUint16(result, record.Port) + result = binary.BigEndian.AppendUint32(result, uint32(len(target))) + result = append(result, target...) + } + return boundedDNSResult(result) +} + +func boundedDNSResult(result []byte) ([]byte, error) { + if len(result) == 0 || len(result) > maxDNSResultLen { + return nil, fmt.Errorf("DNS result exceeds bridge limit") + } + return result, nil +} diff --git a/easytier-go/internal/hostabi/environment.go b/easytier-go/internal/hostabi/environment.go new file mode 100644 index 00000000..ded43183 --- /dev/null +++ b/easytier-go/internal/hostabi/environment.go @@ -0,0 +1,65 @@ +package hostabi + +import ( + "context" + + "github.com/metacubex/wazero/api" +) + +func (adapter *Adapter) startLocalAddrForRemote( + _ context.Context, + module api.Module, + operation uint64, + remotePointer uint32, + remoteLength uint32, + contextPointer uint32, + contextLength uint32, +) int32 { + if remoteLength != socketAddressLen { + return statusInvalid + } + encoded, ok := module.Memory().Read(remotePointer, remoteLength) + if !ok { + return statusMemory + } + remote, err := decodeSocketAddress(append([]byte(nil), encoded...), false) + if err != nil { + return statusInvalid + } + encodedContext, ok := readOwnedOptions(module, contextPointer, contextLength) + if !ok { + return statusMemory + } + socketContext, remainder, err := decodeSocketContext(encodedContext) + if err != nil || len(remainder) != 0 { + return statusInvalid + } + return operationStatus( + adapter.reactor.StartLocalAddrForRemote(operation, remote, socketContext), + ) +} + +func (adapter *Adapter) takeLocalAddrForRemote( + _ context.Context, + module api.Module, + operation uint64, + resultPointer uint32, + resultLength uint32, +) int32 { + if !validResultMemory(module, resultPointer, resultLength, socketAddressLen) { + _ = adapter.reactor.CancelOperation(operation) + return statusMemory + } + address, err := adapter.reactor.TakeLocalAddrForRemote(operation) + if err != nil { + return operationStatus(err) + } + encoded, err := encodeNetAddr(address) + if err != nil { + return statusInvalid + } + if !module.Memory().Write(resultPointer, encoded[:]) { + return statusMemory + } + return 0 +} diff --git a/easytier-go/internal/hostabi/event.go b/easytier-go/internal/hostabi/event.go new file mode 100644 index 00000000..980c996a --- /dev/null +++ b/easytier-go/internal/hostabi/event.go @@ -0,0 +1,64 @@ +package hostabi + +import ( + "context" + + "github.com/metacubex/wazero/api" +) + +const ( + maxHostEventKindLen = 64 + maxHostEventMessageLen = 64 * 1024 +) + +var emitEventParameterTypes = []api.ValueType{ + api.ValueTypeI64, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, +} + +func (adapter *Adapter) emitEventFunction() api.GoModuleFunction { + return api.GoModuleFunc(func( + _ context.Context, + module api.Module, + stack []uint64, + ) { + stack[0] = api.EncodeI32(adapter.emitEvent( + module, + stack[0], + api.DecodeU32(stack[1]), + api.DecodeU32(stack[2]), + api.DecodeU32(stack[3]), + api.DecodeU32(stack[4]), + )) + }) +} + +func (adapter *Adapter) emitEvent( + module api.Module, + handle uint64, + kindPointer uint32, + kindLength uint32, + messagePointer uint32, + messageLength uint32, +) int32 { + if kindLength == 0 || kindLength > maxHostEventKindLen || + messageLength == 0 || messageLength > maxHostEventMessageLen { + return statusInvalid + } + kind, ok := module.Memory().Read(kindPointer, kindLength) + if !ok { + return statusMemory + } + message, ok := module.Memory().Read(messagePointer, messageLength) + if !ok { + return statusMemory + } + return operationStatus(adapter.reactor.TryEventWrite( + handle, + string(kind), + string(message), + )) +} diff --git a/easytier-go/internal/hostabi/factory.go b/easytier-go/internal/hostabi/factory.go new file mode 100644 index 00000000..893c2718 --- /dev/null +++ b/easytier-go/internal/hostabi/factory.go @@ -0,0 +1,143 @@ +package hostabi + +import ( + "context" + + "github.com/metacubex/wazero/api" +) + +func (adapter *Adapter) startTCPConnect( + _ context.Context, + module api.Module, + operation uint64, + optionsPointer uint32, + optionsLength uint32, +) int32 { + encoded, ok := readOwnedOptions(module, optionsPointer, optionsLength) + if !ok { + return statusMemory + } + options, err := decodeTCPConnectOptions(encoded) + if err != nil { + return statusInvalid + } + return operationStatus(adapter.reactor.StartTCPConnect(operation, options)) +} + +func (adapter *Adapter) takeTCPConnect( + _ context.Context, + module api.Module, + operation uint64, + resultPointer uint32, + resultLength uint32, +) int32 { + if !validResultMemory(module, resultPointer, resultLength, tcpSocketResultLen) { + _ = adapter.reactor.CancelOperation(operation) + return statusMemory + } + result, err := adapter.reactor.TakeTCPConnect(operation) + if err != nil { + return connectStatus(err) + } + encoded, err := encodeTCPSocketResult(result.Handle, result.Local, result.Peer) + if err != nil || !module.Memory().Write(resultPointer, encoded[:]) { + _ = adapter.reactor.CloseHandle(result.Handle) + return statusMemory + } + return 0 +} + +func (adapter *Adapter) startUDPBind( + _ context.Context, + module api.Module, + operation uint64, + optionsPointer uint32, + optionsLength uint32, +) int32 { + encoded, ok := readOwnedOptions(module, optionsPointer, optionsLength) + if !ok { + return statusMemory + } + options, err := decodeUDPBindOptions(encoded) + if err != nil { + return statusInvalid + } + return operationStatus(adapter.reactor.StartUDPBind(operation, options)) +} + +func (adapter *Adapter) takeUDPBind( + _ context.Context, + module api.Module, + operation uint64, + resultPointer uint32, + resultLength uint32, +) int32 { + if !validResultMemory(module, resultPointer, resultLength, boundSocketResultLen) { + _ = adapter.reactor.CancelOperation(operation) + return statusMemory + } + result, err := adapter.reactor.TakeUDPBind(operation) + if err != nil { + return operationStatus(err) + } + encoded, err := encodeBoundSocketResult(result.Handle, result.Local) + if err != nil || !module.Memory().Write(resultPointer, encoded[:]) { + _ = adapter.reactor.CloseHandle(result.Handle) + return statusMemory + } + return 0 +} + +func (adapter *Adapter) startTCPListen( + _ context.Context, + module api.Module, + operation uint64, + optionsPointer uint32, + optionsLength uint32, +) int32 { + encoded, ok := readOwnedOptions(module, optionsPointer, optionsLength) + if !ok { + return statusMemory + } + options, err := decodeTCPListenOptions(encoded) + if err != nil { + return statusInvalid + } + return operationStatus(adapter.reactor.StartTCPListen(operation, options)) +} + +func (adapter *Adapter) takeTCPListen( + _ context.Context, + module api.Module, + operation uint64, + resultPointer uint32, + resultLength uint32, +) int32 { + if !validResultMemory(module, resultPointer, resultLength, boundSocketResultLen) { + _ = adapter.reactor.CancelOperation(operation) + return statusMemory + } + result, err := adapter.reactor.TakeTCPListen(operation) + if err != nil { + return operationStatus(err) + } + encoded, err := encodeBoundSocketResult(result.Handle, result.Local) + if err != nil || !module.Memory().Write(resultPointer, encoded[:]) { + _ = adapter.reactor.CloseHandle(result.Handle) + return statusMemory + } + return 0 +} + +func validResultMemory( + module api.Module, + pointer uint32, + length uint32, + expected uint32, +) bool { + if length != expected { + return false + } + _, ok := module.Memory().Read(pointer, length) + return ok +} diff --git a/easytier-go/internal/hostabi/listener.go b/easytier-go/internal/hostabi/listener.go new file mode 100644 index 00000000..b8925c64 --- /dev/null +++ b/easytier-go/internal/hostabi/listener.go @@ -0,0 +1,39 @@ +package hostabi + +import ( + "context" + + "github.com/metacubex/wazero/api" +) + +func (adapter *Adapter) startTCPAccept( + _ context.Context, + _ api.Module, + handle uint64, + operation uint64, +) int32 { + return operationStatus(adapter.reactor.StartTCPAccept(handle, operation)) +} + +func (adapter *Adapter) takeTCPAccept( + _ context.Context, + module api.Module, + operation uint64, + resultPointer uint32, + resultLength uint32, +) int32 { + if !validResultMemory(module, resultPointer, resultLength, tcpSocketResultLen) { + _ = adapter.reactor.CancelOperation(operation) + return statusMemory + } + result, err := adapter.reactor.TakeTCPAccept(operation) + if err != nil { + return operationStatus(err) + } + encoded, err := encodeTCPSocketResult(result.Handle, result.Local, result.Peer) + if err != nil || !module.Memory().Write(resultPointer, encoded[:]) { + _ = adapter.reactor.CloseHandle(result.Handle) + return statusMemory + } + return 0 +} diff --git a/easytier-go/internal/hostabi/management.go b/easytier-go/internal/hostabi/management.go new file mode 100644 index 00000000..d232f3f2 --- /dev/null +++ b/easytier-go/internal/hostabi/management.go @@ -0,0 +1,55 @@ +package hostabi + +import ( + "context" + + "github.com/metacubex/wazero/api" +) + +const maxManagementMessageLen = 16 * 1024 * 1024 + +func (adapter *Adapter) startManagement( + _ context.Context, + module api.Module, + operation uint64, + requestPointer uint32, + requestLength uint32, +) int32 { + if requestLength == 0 || requestLength > maxManagementMessageLen { + return statusInvalid + } + request, ok := module.Memory().Read(requestPointer, requestLength) + if !ok { + return statusMemory + } + return operationStatus( + adapter.reactor.StartManagement(operation, append([]byte(nil), request...)), + ) +} + +func (adapter *Adapter) takeManagement( + _ context.Context, + module api.Module, + operation uint64, + resultPointer uint32, + resultCapacity uint32, +) int32 { + result, err := adapter.reactor.ManagementResult(operation) + if err != nil { + return operationStatus(err) + } + if len(result) > maxManagementMessageLen { + _ = adapter.reactor.CancelOperation(operation) + return statusInvalid + } + if resultCapacity < uint32(len(result)) { + return int32(len(result)) + } + if err := adapter.reactor.FinishManagement(operation); err != nil { + return operationStatus(err) + } + if !module.Memory().Write(resultPointer, result) { + return statusMemory + } + return int32(len(result)) +} diff --git a/easytier-go/internal/hostabi/packet.go b/easytier-go/internal/hostabi/packet.go new file mode 100644 index 00000000..1dfc2acf --- /dev/null +++ b/easytier-go/internal/hostabi/packet.go @@ -0,0 +1,82 @@ +package hostabi + +import ( + "context" + + "github.com/metacubex/wazero/api" +) + +const maxHostPacketLen = 1024 * 1024 + +var tryPacketWriteParameterTypes = []api.ValueType{ + api.ValueTypeI64, + api.ValueTypeI32, + api.ValueTypeI32, +} + +func (adapter *Adapter) tryPacketWriteFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.tryPacketWrite( + ctx, + module, + stack[0], + api.DecodeU32(stack[1]), + api.DecodeU32(stack[2]), + )) + }) +} + +func (adapter *Adapter) startPacketWriteReadyFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.startPacketWriteReady( + ctx, + module, + stack[0], + stack[1], + )) + }) +} + +func (adapter *Adapter) takePacketWriteReadyFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.takePacketWriteReady( + ctx, + module, + stack[0], + )) + }) +} + +func (adapter *Adapter) tryPacketWrite( + _ context.Context, + module api.Module, + handle uint64, + packetPointer uint32, + packetLength uint32, +) int32 { + if packetLength == 0 || packetLength > maxHostPacketLen { + return statusInvalid + } + packet, ok := module.Memory().Read(packetPointer, packetLength) + if !ok { + return statusMemory + } + return operationStatus(adapter.reactor.TryPacketWrite(handle, packet)) +} + +func (adapter *Adapter) startPacketWriteReady( + _ context.Context, + _ api.Module, + handle uint64, + operation uint64, +) int32 { + return operationStatus(adapter.reactor.StartPacketWriteReady(handle, operation)) +} + +func (adapter *Adapter) takePacketWriteReady( + _ context.Context, + _ api.Module, + operation uint64, +) int32 { + return operationStatus(adapter.reactor.TakePacketWriteReady(operation)) +} diff --git a/easytier-go/internal/hostabi/socket_wire.go b/easytier-go/internal/hostabi/socket_wire.go new file mode 100644 index 00000000..1f1e5304 --- /dev/null +++ b/easytier-go/internal/hostabi/socket_wire.go @@ -0,0 +1,427 @@ +package hostabi + +import ( + "encoding/binary" + "fmt" + "net" + "unicode/utf8" + + "github.com/EasyTier/EasyTier/easytier-go/platform" + "github.com/metacubex/wazero/api" +) + +const ( + socketAddressLen = 27 + tcpSocketResultLen = 62 + boundSocketResultLen = 35 + maxFactoryOptionsSize = 4096 +) + +func readOwnedOptions(module api.Module, pointer, length uint32) ([]byte, bool) { + if length > maxFactoryOptionsSize { + return nil, false + } + options, ok := module.Memory().Read(pointer, length) + if !ok { + return nil, false + } + return append([]byte(nil), options...), true +} + +func decodeTCPConnectOptions(encoded []byte) (platform.TCPConnectOptions, error) { + if len(encoded) < 75 || encoded[0] != 2 { + return platform.TCPConnectOptions{}, fmt.Errorf("invalid TCP connect options") + } + remote, err := decodeSocketAddress(encoded[1:28], false) + if err != nil || remote == nil { + return platform.TCPConnectOptions{}, fmt.Errorf("invalid TCP remote address") + } + local, err := decodeSocketAddress(encoded[28:55], true) + if err != nil { + return platform.TCPConnectOptions{}, err + } + socketContext, remainder, err := decodeSocketContext(encoded[55:]) + if err != nil { + return platform.TCPConnectOptions{}, fmt.Errorf("invalid TCP socket context: %w", err) + } + if len(remainder) < 9 { + return platform.TCPConnectOptions{}, fmt.Errorf("truncated TCP bind policy") + } + bind, err := decodeTCPBindPolicy( + udpToTCPAddr(local), + socketContext, + remainder[0], + remainder[1], + remainder[2], + remainder[4:], + ) + if err != nil { + return platform.TCPConnectOptions{}, err + } + purpose, err := decodeTCPConnectPurpose(remainder[3]) + if err != nil { + return platform.TCPConnectOptions{}, err + } + return platform.TCPConnectOptions{ + RemoteAddr: &net.TCPAddr{IP: remote.IP, Port: remote.Port, Zone: remote.Zone}, + Bind: bind, + Purpose: purpose, + }, nil +} + +func decodeUDPBindOptions(encoded []byte) (platform.UDPBindOptions, error) { + if len(encoded) < 48 || encoded[0] != 2 { + return platform.UDPBindOptions{}, fmt.Errorf("invalid UDP bind options") + } + local, err := decodeSocketAddress(encoded[1:28], true) + if err != nil { + return platform.UDPBindOptions{}, err + } + socketContext, remainder, err := decodeSocketContext(encoded[28:]) + if err != nil { + return platform.UDPBindOptions{}, fmt.Errorf("invalid UDP socket context: %w", err) + } + if len(remainder) < 9 { + return platform.UDPBindOptions{}, fmt.Errorf("truncated UDP bind policy") + } + reuseAddr, err := decodeWireBool("UDP reuse_addr", remainder[0]) + if err != nil { + return platform.UDPBindOptions{}, err + } + reusePort, err := decodeWireBool("UDP reuse_port", remainder[1]) + if err != nil { + return platform.UDPBindOptions{}, err + } + onlyV6, err := decodeWireBool("UDP only_v6", remainder[2]) + if err != nil { + return platform.UDPBindOptions{}, err + } + purpose, err := decodeUDPBindPurpose(remainder[3]) + if err != nil { + return platform.UDPBindOptions{}, err + } + device, err := decodeBindDevice(remainder[4:]) + if err != nil { + return platform.UDPBindOptions{}, err + } + return platform.UDPBindOptions{ + Context: socketContext, + LocalAddr: local, + BindDevice: device, + ReuseAddr: reuseAddr, + ReusePort: reusePort, + OnlyV6: onlyV6, + Purpose: purpose, + }, nil +} + +func decodeTCPListenOptions(encoded []byte) (platform.TCPListenOptions, error) { + if len(encoded) < 48 || encoded[0] != 2 { + return platform.TCPListenOptions{}, fmt.Errorf("invalid TCP listen options") + } + local, err := decodeSocketAddress(encoded[1:28], false) + if err != nil || local == nil { + return platform.TCPListenOptions{}, fmt.Errorf("invalid TCP listen address") + } + socketContext, remainder, err := decodeSocketContext(encoded[28:]) + if err != nil { + return platform.TCPListenOptions{}, fmt.Errorf("invalid TCP listen context: %w", err) + } + if len(remainder) < 9 { + return platform.TCPListenOptions{}, fmt.Errorf("truncated TCP listen bind policy") + } + bind, err := decodeTCPBindPolicy( + &net.TCPAddr{IP: local.IP, Port: local.Port, Zone: local.Zone}, + socketContext, + remainder[0], + remainder[1], + remainder[2], + remainder[4:], + ) + if err != nil { + return platform.TCPListenOptions{}, err + } + purpose, err := decodeTCPListenPurpose(remainder[3]) + if err != nil { + return platform.TCPListenOptions{}, err + } + return platform.TCPListenOptions{Bind: bind, Purpose: purpose}, nil +} + +func decodeTCPBindPolicy( + localAddr *net.TCPAddr, + socketContext platform.SocketContext, + reuseMode byte, + reusePortByte byte, + onlyV6Byte byte, + deviceBytes []byte, +) (platform.TCPBindOptions, error) { + var reuseAddr *bool + switch reuseMode { + case 0: + case 1: + value := false + reuseAddr = &value + case 2: + value := true + reuseAddr = &value + default: + return platform.TCPBindOptions{}, fmt.Errorf("invalid TCP reuse_addr") + } + reusePort, err := decodeWireBool("TCP reuse_port", reusePortByte) + if err != nil { + return platform.TCPBindOptions{}, err + } + onlyV6, err := decodeWireBool("TCP only_v6", onlyV6Byte) + if err != nil { + return platform.TCPBindOptions{}, err + } + device, err := decodeBindDevice(deviceBytes) + if err != nil { + return platform.TCPBindOptions{}, err + } + return platform.TCPBindOptions{ + Context: socketContext, + LocalAddr: localAddr, + BindDevice: device, + ReuseAddr: reuseAddr, + ReusePort: reusePort, + OnlyV6: onlyV6, + }, nil +} + +func decodeSocketContext( + encoded []byte, +) (platform.SocketContext, []byte, error) { + if len(encoded) < 11 { + return platform.SocketContext{}, nil, fmt.Errorf("truncated socket context") + } + ipVersion, err := decodeIPVersion(encoded[0]) + if err != nil { + return platform.SocketContext{}, nil, err + } + mark, err := decodeSocketMark(encoded[1], encoded[2:6]) + if err != nil { + return platform.SocketContext{}, nil, err + } + if encoded[6] > 1 { + return platform.SocketContext{}, nil, fmt.Errorf("invalid netns presence") + } + length := int(binary.BigEndian.Uint32(encoded[7:11])) + if length > len(encoded)-11 || (encoded[6] == 0 && length != 0) { + return platform.SocketContext{}, nil, fmt.Errorf("invalid netns length") + } + var netns *string + if encoded[6] == 1 { + token := encoded[11 : 11+length] + if !utf8.Valid(token) { + return platform.SocketContext{}, nil, fmt.Errorf("netns token is not UTF-8") + } + value := string(token) + netns = &value + } + return platform.SocketContext{ + IPVersion: ipVersion, + SocketMark: mark, + NetNS: netns, + }, encoded[11+length:], nil +} + +func decodeIPVersion(encoded byte) (platform.IPVersion, error) { + switch encoded { + case 0: + return platform.IPVersionV4, nil + case 1: + return platform.IPVersionV6, nil + case 2: + return platform.IPVersionBoth, nil + default: + return 0, fmt.Errorf("invalid IP version %d", encoded) + } +} + +func decodeSocketMark(present byte, encoded []byte) (*uint32, error) { + if present > 1 || len(encoded) != 4 || + (present == 0 && binary.BigEndian.Uint32(encoded) != 0) { + return nil, fmt.Errorf("invalid socket mark encoding") + } + if present == 0 { + return nil, nil + } + mark := binary.BigEndian.Uint32(encoded) + return &mark, nil +} + +func decodeWireBool(name string, encoded byte) (bool, error) { + if encoded > 1 { + return false, fmt.Errorf("invalid %s", name) + } + return encoded == 1, nil +} + +func decodeBindDevice(encoded []byte) (*string, error) { + if len(encoded) < 5 || encoded[0] > 1 { + return nil, fmt.Errorf("invalid bind device encoding") + } + length := int(binary.BigEndian.Uint32(encoded[1:5])) + if len(encoded) != 5+length || (encoded[0] == 0 && length != 0) { + return nil, fmt.Errorf("invalid bind device length") + } + if encoded[0] == 0 { + return nil, nil + } + device := string(encoded[5:]) + return &device, nil +} + +func decodeSocketAddress(encoded []byte, optional bool) (*net.UDPAddr, error) { + if len(encoded) != socketAddressLen { + return nil, fmt.Errorf("invalid socket address length") + } + if optional && encoded[0] == 0 { + for _, value := range encoded[1:] { + if value != 0 { + return nil, fmt.Errorf("noncanonical absent socket address") + } + } + return nil, nil + } + metadata := make([]byte, udpMetadataLen) + copy(metadata, encoded) + address, _, flowinfo, _, err := decodeUDPMetadata(metadata) + if err == nil && flowinfo != 0 { + return nil, fmt.Errorf("IPv6 flowinfo is not supported") + } + return address, err +} + +func udpToTCPAddr(address *net.UDPAddr) *net.TCPAddr { + if address == nil { + return nil + } + return &net.TCPAddr{IP: address.IP, Port: address.Port, Zone: address.Zone} +} + +func encodeTCPSocketResult( + handle uint64, + localAddr net.Addr, + peerAddr net.Addr, +) ([tcpSocketResultLen]byte, error) { + var encoded [tcpSocketResultLen]byte + binary.BigEndian.PutUint64(encoded[:8], handle) + local, err := encodeNetAddr(localAddr) + if err != nil { + return encoded, err + } + peer, err := encodeNetAddr(peerAddr) + if err != nil { + return encoded, err + } + copy(encoded[8:35], local[:]) + copy(encoded[35:], peer[:]) + return encoded, nil +} + +func encodeBoundSocketResult( + handle uint64, + localAddr net.Addr, +) ([boundSocketResultLen]byte, error) { + var encoded [boundSocketResultLen]byte + binary.BigEndian.PutUint64(encoded[:8], handle) + local, err := encodeNetAddr(localAddr) + if err != nil { + return encoded, err + } + copy(encoded[8:], local[:]) + return encoded, nil +} + +func encodeNetAddr(address net.Addr) ([socketAddressLen]byte, error) { + var encoded [socketAddressLen]byte + var udpAddr *net.UDPAddr + switch address := address.(type) { + case *net.TCPAddr: + udpAddr = &net.UDPAddr{IP: address.IP, Port: address.Port, Zone: address.Zone} + case *net.UDPAddr: + udpAddr = address + default: + return encoded, fmt.Errorf("unsupported socket address %T", address) + } + metadata, err := encodeUDPMetadata(udpAddr, nil, 0) + if err != nil { + return encoded, err + } + copy(encoded[:], metadata[:socketAddressLen]) + return encoded, nil +} + +func decodeTCPConnectPurpose(encoded byte) (platform.TCPConnectPurpose, error) { + switch encoded { + case 0: + return platform.TCPConnectDirect, nil + case 1: + return platform.TCPConnectFake, nil + case 2: + return platform.TCPConnectHolePunch, nil + case 3: + return platform.TCPConnectManual, nil + case 4: + return platform.TCPConnectProxyNAT, nil + case 5: + return platform.TCPConnectSTUNProbe, nil + case 6: + return platform.TCPConnectSocks5, nil + case 7: + return platform.TCPConnectPortForward, nil + case 8: + return platform.TCPConnectDataPlane, nil + default: + return 0, fmt.Errorf("invalid TCP connect purpose %d", encoded) + } +} + +func decodeUDPBindPurpose(encoded byte) (platform.UDPBindPurpose, error) { + switch encoded { + case 0: + return platform.UDPBindHolePunchControl, nil + case 1: + return platform.UDPBindHolePunchCandidate, nil + case 2: + return platform.UDPBindDirect, nil + case 3: + return platform.UDPBindPortBoundListener, nil + case 4: + return platform.UDPBindProxyNAT, nil + case 5: + return platform.UDPBindSTUNProbe, nil + case 6: + return platform.UDPBindSocks5, nil + case 7: + return platform.UDPBindPortForward, nil + case 8: + return platform.UDPBindPortLease, nil + default: + return 0, fmt.Errorf("invalid UDP bind purpose %d", encoded) + } +} + +func decodeTCPListenPurpose(encoded byte) (platform.TCPListenPurpose, error) { + switch encoded { + case 0: + return platform.TCPListenDirect, nil + case 1: + return platform.TCPListenHolePunch, nil + case 2: + return platform.TCPListenManual, nil + case 3: + return platform.TCPListenProxyNAT, nil + case 4: + return platform.TCPListenSocks5, nil + case 5: + return platform.TCPListenPortForward, nil + case 6: + return platform.TCPListenPortLease, nil + default: + return 0, fmt.Errorf("invalid TCP listen purpose %d", encoded) + } +} diff --git a/easytier-go/internal/hostabi/status.go b/easytier-go/internal/hostabi/status.go new file mode 100644 index 00000000..85e209dd --- /dev/null +++ b/easytier-go/internal/hostabi/status.go @@ -0,0 +1,55 @@ +package hostabi + +import ( + "errors" + + "github.com/EasyTier/EasyTier/easytier-go/internal/reactor" +) + +const ( + statusPending int32 = -1 + statusInvalid int32 = -2 + statusIOError int32 = -3 + statusMemory int32 = -4 + statusWouldBlock int32 = -5 + statusConnectionRefused int32 = -6 + statusConnectionAborted int32 = -7 + statusConnectionReset int32 = -8 + statusNotConnected int32 = -9 +) + +func operationStatus(err error) int32 { + switch { + case err == nil: + return 0 + case errors.Is(err, reactor.ErrPending): + return statusPending + case errors.Is(err, reactor.ErrInvalid): + return statusInvalid + case errors.Is(err, reactor.ErrWouldBlock): + return statusWouldBlock + default: + return statusIOError + } +} + +func connectStatus(err error) int32 { + switch { + case err == nil: + return 0 + case errors.Is(err, reactor.ErrPending): + return statusPending + case errors.Is(err, reactor.ErrInvalid): + return statusInvalid + case isConnectionRefused(err): + return statusConnectionRefused + case isConnectionAborted(err): + return statusConnectionAborted + case isConnectionReset(err): + return statusConnectionReset + case isNotConnected(err): + return statusNotConnected + default: + return statusIOError + } +} diff --git a/easytier-go/internal/hostabi/stream.go b/easytier-go/internal/hostabi/stream.go new file mode 100644 index 00000000..a800cdab --- /dev/null +++ b/easytier-go/internal/hostabi/stream.go @@ -0,0 +1,137 @@ +package hostabi + +import ( + "context" + + "github.com/metacubex/wazero/api" +) + +var ( + takeReadParameterTypes = []api.ValueType{ + api.ValueTypeI64, + api.ValueTypeI32, + api.ValueTypeI32, + } + startWriteParameterTypes = []api.ValueType{ + api.ValueTypeI64, + api.ValueTypeI64, + api.ValueTypeI32, + api.ValueTypeI32, + } +) + +func (adapter *Adapter) startReadFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.startRead( + ctx, + module, + stack[0], + stack[1], + api.DecodeU32(stack[2]), + )) + }) +} + +func (adapter *Adapter) takeReadFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.takeRead( + ctx, + module, + stack[0], + api.DecodeU32(stack[1]), + api.DecodeU32(stack[2]), + )) + }) +} + +func (adapter *Adapter) startWriteFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.startWrite( + ctx, + module, + stack[0], + stack[1], + api.DecodeU32(stack[2]), + api.DecodeU32(stack[3]), + )) + }) +} + +func (adapter *Adapter) takeWriteFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.takeWrite( + ctx, + module, + stack[0], + )) + }) +} + +func (adapter *Adapter) startRead( + _ context.Context, + _ api.Module, + handle uint64, + operation uint64, + capacity uint32, +) int32 { + return operationStatus(adapter.reactor.StartRead(handle, operation, capacity)) +} + +func (adapter *Adapter) takeRead( + _ context.Context, + module api.Module, + operation uint64, + destination uint32, + capacity uint32, +) int32 { + data, err := adapter.reactor.TakeRead(operation) + if err != nil { + return operationStatus(err) + } + if uint32(len(data)) > capacity { + return statusMemory + } + if len(data) > 0 && !module.Memory().Write(destination, data) { + return statusMemory + } + return int32(len(data)) +} + +func (adapter *Adapter) startWrite( + _ context.Context, + module api.Module, + handle uint64, + operation uint64, + source uint32, + length uint32, +) int32 { + data, ok := module.Memory().Read(source, length) + if !ok { + return statusMemory + } + return operationStatus(adapter.reactor.StartWrite(handle, operation, data)) +} + +func (adapter *Adapter) takeWrite( + _ context.Context, + _ api.Module, + operation uint64, +) int32 { + return operationStatus(adapter.reactor.TakeWrite(operation)) +} + +func (adapter *Adapter) cancelOperation( + _ context.Context, + _ api.Module, + operation uint64, +) int32 { + return operationStatus(adapter.reactor.CancelOperation(operation)) +} + +func (adapter *Adapter) closeHandle( + _ context.Context, + _ api.Module, + handle uint64, +) int32 { + return operationStatus(adapter.reactor.CloseHandle(handle)) +} diff --git a/easytier-go/internal/hostabi/udp.go b/easytier-go/internal/hostabi/udp.go new file mode 100644 index 00000000..acace16a --- /dev/null +++ b/easytier-go/internal/hostabi/udp.go @@ -0,0 +1,184 @@ +package hostabi + +import ( + "context" + + "github.com/metacubex/wazero/api" +) + +var ( + startUDPReceiveParameterTypes = []api.ValueType{ + api.ValueTypeI64, + api.ValueTypeI64, + api.ValueTypeI32, + } + takeUDPReceiveParameterTypes = []api.ValueType{ + api.ValueTypeI64, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + } + tryUDPSendParameterTypes = []api.ValueType{ + api.ValueTypeI64, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + api.ValueTypeI32, + } + handleOperationParameterTypes = []api.ValueType{ + api.ValueTypeI64, + api.ValueTypeI64, + } + operationParameterTypes = []api.ValueType{api.ValueTypeI64} +) + +func (adapter *Adapter) startUDPReceiveFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.startUDPReceive( + ctx, + module, + stack[0], + stack[1], + api.DecodeU32(stack[2]), + )) + }) +} + +func (adapter *Adapter) takeUDPReceiveFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.takeUDPReceive( + ctx, + module, + stack[0], + api.DecodeU32(stack[1]), + api.DecodeU32(stack[2]), + api.DecodeU32(stack[3]), + api.DecodeU32(stack[4]), + )) + }) +} + +func (adapter *Adapter) tryUDPSendFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.tryUDPSend( + ctx, + module, + stack[0], + api.DecodeU32(stack[1]), + api.DecodeU32(stack[2]), + api.DecodeU32(stack[3]), + api.DecodeU32(stack[4]), + )) + }) +} + +func (adapter *Adapter) startUDPSendReadyFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.startUDPSendReady( + ctx, + module, + stack[0], + stack[1], + )) + }) +} + +func (adapter *Adapter) takeUDPSendReadyFunction() api.GoModuleFunction { + return api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) { + stack[0] = api.EncodeI32(adapter.takeUDPSendReady( + ctx, + module, + stack[0], + )) + }) +} + +func (adapter *Adapter) startUDPReceive( + _ context.Context, + _ api.Module, + handle uint64, + operation uint64, + _ uint32, +) int32 { + return operationStatus(adapter.reactor.StartUDPReceive(handle, operation)) +} + +func (adapter *Adapter) takeUDPReceive( + _ context.Context, + module api.Module, + operation uint64, + destination uint32, + capacity uint32, + metadataDestination uint32, + metadataLength uint32, +) int32 { + if metadataLength != udpMetadataLen { + return statusMemory + } + if _, ok := module.Memory().Read(destination, capacity); !ok { + return statusMemory + } + if _, ok := module.Memory().Read(metadataDestination, metadataLength); !ok { + return statusMemory + } + datagram, err := adapter.reactor.TakeUDPReceive(operation, capacity) + if err != nil { + return operationStatus(err) + } + if len(datagram.Data) > 0 && !module.Memory().Write(destination, datagram.Data) { + return statusMemory + } + metadata, err := encodeUDPMetadata(datagram.Peer, nil, 0) + if err != nil { + return statusInvalid + } + if !module.Memory().Write(metadataDestination, metadata[:]) { + return statusMemory + } + return int32(len(datagram.Data)) +} + +func (adapter *Adapter) tryUDPSend( + _ context.Context, + module api.Module, + handle uint64, + source uint32, + length uint32, + metadataSource uint32, + metadataLength uint32, +) int32 { + if metadataLength != udpMetadataLen || length > 65535 { + return statusMemory + } + metadata, ok := module.Memory().Read(metadataSource, metadataLength) + if !ok { + return statusMemory + } + peer, sourceIP, flowinfo, sourceIfindex, err := decodeUDPMetadata(metadata) + if err != nil || sourceIP != nil || flowinfo != 0 || sourceIfindex != 0 { + return statusInvalid + } + data, ok := module.Memory().Read(source, length) + if !ok { + return statusMemory + } + return operationStatus(adapter.reactor.TryUDPSend(handle, data, peer)) +} + +func (adapter *Adapter) startUDPSendReady( + _ context.Context, + _ api.Module, + handle uint64, + operation uint64, +) int32 { + return operationStatus(adapter.reactor.StartUDPSendReady(handle, operation)) +} + +func (adapter *Adapter) takeUDPSendReady( + _ context.Context, + _ api.Module, + operation uint64, +) int32 { + return operationStatus(adapter.reactor.TakeUDPSendReady(operation)) +} diff --git a/easytier-go/internal/hostabi/udp_wire.go b/easytier-go/internal/hostabi/udp_wire.go new file mode 100644 index 00000000..cc3b7cb5 --- /dev/null +++ b/easytier-go/internal/hostabi/udp_wire.go @@ -0,0 +1,122 @@ +package hostabi + +import ( + "encoding/binary" + "errors" + "net" +) + +const udpMetadataLen = 48 + +func encodeUDPMetadata( + peer *net.UDPAddr, + optionalIP net.IP, + optionalIfindex uint32, +) ([udpMetadataLen]byte, error) { + var metadata [udpMetadataLen]byte + if peer == nil { + return metadata, errors.New("nil UDP peer") + } + if ipv4 := peer.IP.To4(); ipv4 != nil { + metadata[0] = 4 + copy(metadata[1:5], ipv4) + } else if ipv6 := peer.IP.To16(); ipv6 != nil { + metadata[0] = 6 + copy(metadata[1:17], ipv6) + if peer.Zone != "" { + iface, err := net.InterfaceByName(peer.Zone) + if err != nil { + return metadata, err + } + binary.BigEndian.PutUint32(metadata[23:27], uint32(iface.Index)) + } + } else { + return metadata, errors.New("invalid UDP peer IP") + } + if peer.Port < 0 || peer.Port > 65535 { + return metadata, errors.New("invalid UDP peer port") + } + binary.BigEndian.PutUint16(metadata[17:19], uint16(peer.Port)) + + if optionalIP == nil { + if optionalIfindex != 0 { + return metadata, errors.New("optional UDP interface index requires an IP") + } + return metadata, nil + } + if ipv4 := optionalIP.To4(); ipv4 != nil { + if optionalIfindex != 0 { + return metadata, errors.New("optional IPv4 cannot carry an interface index") + } + metadata[27] = 4 + copy(metadata[28:32], ipv4) + return metadata, nil + } + if ipv6 := optionalIP.To16(); ipv6 != nil { + metadata[27] = 6 + copy(metadata[28:44], ipv6) + binary.BigEndian.PutUint32(metadata[44:48], optionalIfindex) + return metadata, nil + } + return metadata, errors.New("invalid optional UDP IP") +} + +func decodeUDPMetadata(metadata []byte) (*net.UDPAddr, net.IP, uint32, uint32, error) { + if len(metadata) != udpMetadataLen { + return nil, nil, 0, 0, errors.New("invalid UDP metadata length") + } + var peerIP net.IP + switch metadata[0] { + case 4: + if !allZero(metadata[5:17]) || !allZero(metadata[19:27]) { + return nil, nil, 0, 0, errors.New("noncanonical IPv4 peer metadata") + } + peerIP = net.IPv4(metadata[1], metadata[2], metadata[3], metadata[4]) + case 6: + peerIP = append(net.IP(nil), metadata[1:17]...) + default: + return nil, nil, 0, 0, errors.New("invalid UDP peer family") + } + flowinfo := binary.BigEndian.Uint32(metadata[19:23]) + scopeID := binary.BigEndian.Uint32(metadata[23:27]) + zone := "" + if scopeID != 0 { + iface, err := net.InterfaceByIndex(int(scopeID)) + if err != nil { + return nil, nil, 0, 0, err + } + zone = iface.Name + } + peer := &net.UDPAddr{ + IP: peerIP, + Port: int(binary.BigEndian.Uint16(metadata[17:19])), + Zone: zone, + } + + var optionalIP net.IP + switch metadata[27] { + case 0: + if !allZero(metadata[28:48]) { + return nil, nil, 0, 0, errors.New("noncanonical absent optional IP") + } + case 4: + if !allZero(metadata[32:48]) { + return nil, nil, 0, 0, errors.New("noncanonical optional IPv4") + } + optionalIP = net.IPv4(metadata[28], metadata[29], metadata[30], metadata[31]) + case 6: + optionalIP = append(net.IP(nil), metadata[28:44]...) + default: + return nil, nil, 0, 0, errors.New("invalid optional UDP IP family") + } + return peer, optionalIP, flowinfo, binary.BigEndian.Uint32(metadata[44:48]), nil +} + +func allZero(values []byte) bool { + for _, value := range values { + if value != 0 { + return false + } + } + return true +} diff --git a/easytier-go/internal/reactor/dns.go b/easytier-go/internal/reactor/dns.go new file mode 100644 index 00000000..725067ac --- /dev/null +++ b/easytier-go/internal/reactor/dns.go @@ -0,0 +1,125 @@ +package reactor + +import ( + "context" + "fmt" + "net" + "net/netip" + + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +type DNSKind uint8 + +const ( + DNSAddress DNSKind = iota + DNSTXT + DNSSRV +) + +type DNSResult struct { + Kind DNSKind + Addresses []netip.Addr + Text string + Records []*net.SRV +} + +type dnsOperation struct { + kind DNSKind + cancel context.CancelFunc + done bool + result DNSResult + err error +} + +func (reactor *Reactor) StartDNS( + operation uint64, + kind DNSKind, + query platform.DNSQuery, +) error { + if reactor.services.DNS == nil { + return fmt.Errorf("DNS operation: no resolver configured") + } + ctx, cancel := context.WithCancel(reactor.ctx) + result := &dnsOperation{kind: kind, cancel: cancel} + reactor.mu.Lock() + if err := reactor.claimOperationLocked(operation, operationDNS); err != nil { + reactor.mu.Unlock() + cancel() + return err + } + reactor.dns[operation] = result + reactor.workers.Add(1) + reactor.mu.Unlock() + + go func() { + defer reactor.workers.Done() + var value DNSResult + var resolveErr error + switch kind { + case DNSAddress: + value.Addresses, resolveErr = reactor.services.DNS.LookupIP(ctx, query) + case DNSTXT: + value.Text, resolveErr = reactor.services.DNS.LookupTXT(ctx, query) + case DNSSRV: + value.Records, resolveErr = reactor.services.DNS.LookupSRV(ctx, query) + default: + resolveErr = fmt.Errorf("unsupported DNS query kind %d", kind) + } + + reactor.mu.Lock() + if reactor.dns[operation] != result { + reactor.mu.Unlock() + return + } + result.done = true + result.result = value + result.err = resolveErr + reactor.mu.Unlock() + reactor.signalCompletion() + }() + return nil +} + +func (reactor *Reactor) DNSResult(operation uint64) (DNSResult, error) { + reactor.mu.Lock() + result, exists := reactor.dns[operation] + if !exists || reactor.operations[operation] != operationDNS { + reactor.mu.Unlock() + return DNSResult{}, ErrInvalid + } + if !result.done { + reactor.mu.Unlock() + return DNSResult{}, ErrPending + } + if result.err != nil { + delete(reactor.dns, operation) + reactor.releaseOperationLocked(operation, operationDNS) + result.cancel() + err := result.err + reactor.mu.Unlock() + return DNSResult{}, err + } + value := DNSResult{ + Kind: result.kind, + Addresses: append([]netip.Addr(nil), result.result.Addresses...), + Text: result.result.Text, + Records: append([]*net.SRV(nil), result.result.Records...), + } + reactor.mu.Unlock() + return value, nil +} + +func (reactor *Reactor) FinishDNS(operation uint64) error { + reactor.mu.Lock() + result, exists := reactor.dns[operation] + if !exists || !result.done || reactor.operations[operation] != operationDNS { + reactor.mu.Unlock() + return ErrInvalid + } + delete(reactor.dns, operation) + reactor.releaseOperationLocked(operation, operationDNS) + result.cancel() + reactor.mu.Unlock() + return nil +} diff --git a/easytier-go/internal/reactor/environment.go b/easytier-go/internal/reactor/environment.go new file mode 100644 index 00000000..a8f9a2b3 --- /dev/null +++ b/easytier-go/internal/reactor/environment.go @@ -0,0 +1,79 @@ +package reactor + +import ( + "context" + "fmt" + "net" + + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +type environmentOperation struct { + cancel context.CancelFunc + done bool + result net.Addr + err error +} + +func (reactor *Reactor) StartLocalAddrForRemote( + operation uint64, + remote *net.UDPAddr, + socketContext platform.SocketContext, +) error { + if reactor.services.Environment == nil { + return fmt.Errorf("environment operation: no connector environment configured") + } + ctx, cancel := context.WithCancel(reactor.ctx) + result := &environmentOperation{cancel: cancel} + reactor.mu.Lock() + if err := reactor.claimOperationLocked(operation, operationEnvironment); err != nil { + reactor.mu.Unlock() + cancel() + return err + } + reactor.environments[operation] = result + reactor.workers.Add(1) + reactor.mu.Unlock() + + go func() { + defer reactor.workers.Done() + address, resolveErr := reactor.services.Environment.LocalAddrForRemote( + ctx, + remote, + socketContext, + ) + reactor.mu.Lock() + if reactor.environments[operation] != result { + reactor.mu.Unlock() + return + } + result.result = address + result.err = resolveErr + result.done = true + reactor.mu.Unlock() + reactor.signalCompletion() + }() + return nil +} + +func (reactor *Reactor) TakeLocalAddrForRemote(operation uint64) (net.Addr, error) { + reactor.mu.Lock() + result, exists := reactor.environments[operation] + if !exists || reactor.operations[operation] != operationEnvironment { + reactor.mu.Unlock() + return nil, ErrInvalid + } + if !result.done { + reactor.mu.Unlock() + return nil, ErrPending + } + delete(reactor.environments, operation) + reactor.releaseOperationLocked(operation, operationEnvironment) + result.cancel() + address, err := result.result, result.err + reactor.mu.Unlock() + if err == nil && address == nil { + err = fmt.Errorf("connector environment returned no address") + } + return address, err +} diff --git a/easytier-go/internal/reactor/event.go b/easytier-go/internal/reactor/event.go new file mode 100644 index 00000000..37291e75 --- /dev/null +++ b/easytier-go/internal/reactor/event.go @@ -0,0 +1,38 @@ +package reactor + +import "fmt" + +type eventSink func(kind, message string) bool + +func (reactor *Reactor) RegisterEventSink(deliver eventSink) (uint64, error) { + if deliver == nil { + return 0, fmt.Errorf("event sink delivery function must not be nil") + } + reactor.mu.Lock() + defer reactor.mu.Unlock() + if reactor.closed { + return 0, ErrInvalid + } + handle := reactor.allocateHandleLocked() + reactor.eventSinks[handle] = deliver + return handle, nil +} + +func (reactor *Reactor) UnregisterEventSink(handle uint64) { + reactor.mu.Lock() + delete(reactor.eventSinks, handle) + reactor.mu.Unlock() +} + +func (reactor *Reactor) TryEventWrite(handle uint64, kind, message string) error { + reactor.mu.Lock() + defer reactor.mu.Unlock() + deliver, exists := reactor.eventSinks[handle] + if !exists { + return ErrInvalid + } + if !deliver(kind, message) { + return ErrWouldBlock + } + return nil +} diff --git a/easytier-go/internal/reactor/factory.go b/easytier-go/internal/reactor/factory.go new file mode 100644 index 00000000..b8a676cc --- /dev/null +++ b/easytier-go/internal/reactor/factory.go @@ -0,0 +1,288 @@ +package reactor + +import ( + "context" + "fmt" + "net" + + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +type createOperation struct { + kind createKind + done bool + cancel context.CancelFunc + connection net.Conn + datagram net.PacketConn + listener net.Listener + localAddr net.Addr + peerAddr net.Addr + err error +} + +type createKind uint8 + +const ( + createTCPConnect createKind = iota + 1 + createUDPBind + createTCPListen +) + +func (operation *createOperation) resource() ioResource { + switch { + case operation.connection != nil: + return operation.connection + case operation.datagram != nil: + return operation.datagram + case operation.listener != nil: + return operation.listener + default: + return nil + } +} + +type StreamResult struct { + Handle uint64 + Local net.Addr + Peer net.Addr +} + +type BoundResult struct { + Handle uint64 + Local net.Addr +} + +func (reactor *Reactor) beginCreate( + operation uint64, + kind createKind, +) (*createOperation, context.Context, error) { + ctx, cancel := context.WithCancel(reactor.ctx) + create := &createOperation{kind: kind, cancel: cancel} + reactor.mu.Lock() + if err := reactor.claimOperationLocked(operation, operationCreate); err != nil { + reactor.mu.Unlock() + cancel() + return nil, nil, err + } + reactor.creates[operation] = create + reactor.workers.Add(1) + reactor.mu.Unlock() + return create, ctx, nil +} + +func (reactor *Reactor) StartTCPConnect( + operation uint64, + options platform.TCPConnectOptions, +) error { + if reactor.services.Sockets == nil { + return fmt.Errorf("TCP connect: no socket factory configured") + } + create, ctx, err := reactor.beginCreate(operation, createTCPConnect) + if err != nil { + return err + } + go func() { + defer reactor.workers.Done() + connection, connectErr := reactor.services.Sockets.ConnectTCP(ctx, options) + + reactor.mu.Lock() + if reactor.creates[operation] != create { + reactor.mu.Unlock() + if connection != nil { + _ = connection.Close() + } + return + } + create.connection = connection + create.err = connectErr + create.done = true + if connection != nil { + create.localAddr = connection.LocalAddr() + create.peerAddr = connection.RemoteAddr() + } + reactor.mu.Unlock() + reactor.signalCompletion() + }() + return nil +} + +func (reactor *Reactor) TakeTCPConnect(operation uint64) (StreamResult, error) { + reactor.mu.Lock() + create, exists := reactor.creates[operation] + if !exists || + create.kind != createTCPConnect || + reactor.operations[operation] != operationCreate { + reactor.mu.Unlock() + return StreamResult{}, ErrInvalid + } + if !create.done { + reactor.mu.Unlock() + return StreamResult{}, ErrPending + } + delete(reactor.creates, operation) + reactor.releaseOperationLocked(operation, operationCreate) + create.cancel() + if create.err != nil || create.connection == nil { + connection := create.connection + err := create.err + reactor.mu.Unlock() + if connection != nil { + _ = connection.Close() + } + if err == nil { + err = fmt.Errorf("socket factory returned no TCP connection") + } + return StreamResult{}, err + } + handle := reactor.allocateHandleLocked() + reactor.streams[handle] = newStreamState(create.connection) + result := StreamResult{Handle: handle, Local: create.localAddr, Peer: create.peerAddr} + create.connection = nil + reactor.mu.Unlock() + return result, nil +} + +func (reactor *Reactor) StartUDPBind( + operation uint64, + options platform.UDPBindOptions, +) error { + if reactor.services.Sockets == nil { + return fmt.Errorf("UDP bind: no socket factory configured") + } + create, ctx, err := reactor.beginCreate(operation, createUDPBind) + if err != nil { + return err + } + go func() { + defer reactor.workers.Done() + connection, bindErr := reactor.services.Sockets.BindUDP(ctx, options) + + reactor.mu.Lock() + if reactor.creates[operation] != create { + reactor.mu.Unlock() + if connection != nil { + _ = connection.Close() + } + return + } + create.datagram = connection + create.err = bindErr + create.done = true + if connection != nil { + create.localAddr = connection.LocalAddr() + } + reactor.mu.Unlock() + reactor.signalCompletion() + }() + return nil +} + +func (reactor *Reactor) TakeUDPBind(operation uint64) (BoundResult, error) { + reactor.mu.Lock() + create, exists := reactor.creates[operation] + if !exists || + create.kind != createUDPBind || + reactor.operations[operation] != operationCreate { + reactor.mu.Unlock() + return BoundResult{}, ErrInvalid + } + if !create.done { + reactor.mu.Unlock() + return BoundResult{}, ErrPending + } + delete(reactor.creates, operation) + reactor.releaseOperationLocked(operation, operationCreate) + create.cancel() + if create.err != nil || create.datagram == nil { + connection := create.datagram + err := create.err + reactor.mu.Unlock() + if connection != nil { + _ = connection.Close() + } + if err == nil { + err = fmt.Errorf("socket factory returned no UDP socket") + } + return BoundResult{}, err + } + handle := reactor.allocateHandleLocked() + state := newDatagramState(create.datagram) + reactor.datagrams[handle] = state + result := BoundResult{Handle: handle, Local: create.localAddr} + create.datagram = nil + reactor.workers.Add(1) + reactor.mu.Unlock() + go reactor.runUDPSends(handle, state) + return result, nil +} + +func (reactor *Reactor) StartTCPListen( + operation uint64, + options platform.TCPListenOptions, +) error { + if reactor.services.Sockets == nil { + return fmt.Errorf("TCP listen: no socket factory configured") + } + create, ctx, err := reactor.beginCreate(operation, createTCPListen) + if err != nil { + return err + } + go func() { + defer reactor.workers.Done() + listener, listenErr := reactor.services.Sockets.ListenTCP(ctx, options) + + reactor.mu.Lock() + if reactor.creates[operation] != create { + reactor.mu.Unlock() + if listener != nil { + _ = listener.Close() + } + return + } + create.listener = listener + create.err = listenErr + create.done = true + if listener != nil { + create.localAddr = listener.Addr() + } + reactor.mu.Unlock() + reactor.signalCompletion() + }() + return nil +} + +func (reactor *Reactor) TakeTCPListen(operation uint64) (BoundResult, error) { + reactor.mu.Lock() + create, exists := reactor.creates[operation] + if !exists || + create.kind != createTCPListen || + reactor.operations[operation] != operationCreate { + reactor.mu.Unlock() + return BoundResult{}, ErrInvalid + } + if !create.done { + reactor.mu.Unlock() + return BoundResult{}, ErrPending + } + delete(reactor.creates, operation) + reactor.releaseOperationLocked(operation, operationCreate) + create.cancel() + if create.err != nil || create.listener == nil { + listener := create.listener + err := create.err + reactor.mu.Unlock() + if listener != nil { + _ = listener.Close() + } + if err == nil { + err = fmt.Errorf("socket factory returned no TCP listener") + } + return BoundResult{}, err + } + handle := reactor.allocateHandleLocked() + reactor.listeners[handle] = &listenerState{listener: create.listener} + result := BoundResult{Handle: handle, Local: create.localAddr} + create.listener = nil + reactor.mu.Unlock() + return result, nil +} diff --git a/easytier-go/internal/reactor/listener.go b/easytier-go/internal/reactor/listener.go new file mode 100644 index 00000000..b362198b --- /dev/null +++ b/easytier-go/internal/reactor/listener.go @@ -0,0 +1,144 @@ +package reactor + +import "net" + +type listenerState struct { + listener net.Listener + accepted []net.Conn + acceptRunning bool + acceptErr error +} + +type acceptWaiter struct { + handle uint64 + ready bool +} + +func (reactor *Reactor) StartTCPAccept(handle, operation uint64) error { + reactor.mu.Lock() + state, exists := reactor.listeners[handle] + if !exists { + reactor.mu.Unlock() + return ErrInvalid + } + if err := reactor.claimOperationLocked(operation, operationAccept); err != nil { + reactor.mu.Unlock() + return err + } + waiter := &acceptWaiter{ + handle: handle, + ready: len(state.accepted) > 0 || state.acceptErr != nil, + } + reactor.accepts[operation] = waiter + startWorker := !waiter.ready && !state.acceptRunning + if startWorker { + state.acceptRunning = true + reactor.workers.Add(1) + } + reactor.mu.Unlock() + + if startWorker { + go reactor.runTCPAccept(handle, state) + } + if waiter.ready { + reactor.signalCompletion() + } + return nil +} + +func (reactor *Reactor) runTCPAccept(handle uint64, state *listenerState) { + defer reactor.workers.Done() + connection, err := state.listener.Accept() + + reactor.mu.Lock() + if reactor.listeners[handle] != state { + reactor.mu.Unlock() + if connection != nil { + _ = connection.Close() + } + return + } + state.acceptRunning = false + if err != nil { + state.acceptErr = err + } else { + state.accepted = append(state.accepted, connection) + } + for _, waiter := range reactor.accepts { + if waiter.handle == handle { + waiter.ready = true + } + } + reactor.mu.Unlock() + reactor.signalCompletion() +} + +func (reactor *Reactor) TakeTCPAccept(operation uint64) (StreamResult, error) { + reactor.mu.Lock() + waiter, exists := reactor.accepts[operation] + if !exists || reactor.operations[operation] != operationAccept { + reactor.mu.Unlock() + return StreamResult{}, ErrInvalid + } + state, exists := reactor.listeners[waiter.handle] + if !exists { + delete(reactor.accepts, operation) + reactor.releaseOperationLocked(operation, operationAccept) + reactor.mu.Unlock() + return StreamResult{}, ErrInvalid + } + if len(state.accepted) == 0 { + if state.acceptErr == nil { + reactor.mu.Unlock() + return StreamResult{}, ErrPending + } + err := state.acceptErr + state.acceptErr = nil + delete(reactor.accepts, operation) + reactor.releaseOperationLocked(operation, operationAccept) + startWorker := !state.acceptRunning && reactor.hasAcceptWaiterLocked(waiter.handle) + if startWorker { + state.acceptRunning = true + reactor.workers.Add(1) + } + reactor.mu.Unlock() + if startWorker { + go reactor.runTCPAccept(waiter.handle, state) + } + return StreamResult{}, err + } + + connection := state.accepted[0] + state.accepted[0] = nil + state.accepted = state.accepted[1:] + delete(reactor.accepts, operation) + reactor.releaseOperationLocked(operation, operationAccept) + handle := reactor.allocateHandleLocked() + reactor.streams[handle] = newStreamState(connection) + startWorker := len(state.accepted) == 0 && + !state.acceptRunning && + reactor.hasAcceptWaiterLocked(waiter.handle) + if startWorker { + state.acceptRunning = true + reactor.workers.Add(1) + } + result := StreamResult{ + Handle: handle, + Local: connection.LocalAddr(), + Peer: connection.RemoteAddr(), + } + reactor.mu.Unlock() + if startWorker { + go reactor.runTCPAccept(waiter.handle, state) + } + return result, nil +} + +func (reactor *Reactor) hasAcceptWaiterLocked(handle uint64) bool { + for _, waiter := range reactor.accepts { + if waiter.handle == handle { + return true + } + } + return false +} diff --git a/easytier-go/internal/reactor/management.go b/easytier-go/internal/reactor/management.go new file mode 100644 index 00000000..d2afa783 --- /dev/null +++ b/easytier-go/internal/reactor/management.go @@ -0,0 +1,72 @@ +package reactor + +import ( + "context" + "fmt" +) + +type ManagementHandler func(context.Context, []byte) []byte + +type managementOperation struct { + cancel context.CancelFunc + done bool + result []byte +} + +func (reactor *Reactor) StartManagement(operation uint64, request []byte) error { + if reactor.managementHandler == nil { + return fmt.Errorf("management operation: no handler configured") + } + ctx, cancel := context.WithCancel(reactor.ctx) + state := &managementOperation{cancel: cancel} + reactor.mu.Lock() + if err := reactor.claimOperationLocked(operation, operationManagement); err != nil { + reactor.mu.Unlock() + cancel() + return err + } + reactor.management[operation] = state + reactor.workers.Add(1) + reactor.mu.Unlock() + + go func() { + defer reactor.workers.Done() + result := reactor.managementHandler(ctx, request) + reactor.mu.Lock() + if reactor.management[operation] != state { + reactor.mu.Unlock() + return + } + state.done = true + state.result = result + reactor.mu.Unlock() + reactor.signalCompletion() + }() + return nil +} + +func (reactor *Reactor) ManagementResult(operation uint64) ([]byte, error) { + reactor.mu.Lock() + defer reactor.mu.Unlock() + state, exists := reactor.management[operation] + if !exists || reactor.operations[operation] != operationManagement { + return nil, ErrInvalid + } + if !state.done { + return nil, ErrPending + } + return state.result, nil +} + +func (reactor *Reactor) FinishManagement(operation uint64) error { + reactor.mu.Lock() + defer reactor.mu.Unlock() + state, exists := reactor.management[operation] + if !exists || !state.done || + !reactor.releaseOperationLocked(operation, operationManagement) { + return ErrInvalid + } + delete(reactor.management, operation) + state.cancel() + return nil +} diff --git a/easytier-go/internal/reactor/management_test.go b/easytier-go/internal/reactor/management_test.go new file mode 100644 index 00000000..4de354f6 --- /dev/null +++ b/easytier-go/internal/reactor/management_test.go @@ -0,0 +1,45 @@ +package reactor + +import ( + "context" + "errors" + "testing" + "time" +) + +func TestManagementOperationCopiesAndConsumesResult(t *testing.T) { + release := make(chan struct{}) + runtime := New(context.Background(), Options{ + Management: func(_ context.Context, request []byte) []byte { + <-release + return append([]byte("response:"), request...) + }, + }) + defer runtime.Close() + + if err := runtime.StartManagement(7, []byte("request")); err != nil { + t.Fatalf("start management operation: %v", err) + } + if _, err := runtime.ManagementResult(7); !errors.Is(err, ErrPending) { + t.Fatalf("initial result error = %v, want pending", err) + } + close(release) + select { + case <-runtime.Completions(): + case <-time.After(time.Second): + t.Fatal("management operation did not complete") + } + result, err := runtime.ManagementResult(7) + if err != nil { + t.Fatalf("take management result: %v", err) + } + if string(result) != "response:request" { + t.Fatalf("management result = %q", result) + } + if err := runtime.FinishManagement(7); err != nil { + t.Fatalf("finish management operation: %v", err) + } + if _, err := runtime.ManagementResult(7); !errors.Is(err, ErrInvalid) { + t.Fatalf("consumed result error = %v, want invalid", err) + } +} diff --git a/easytier-go/internal/reactor/packet.go b/easytier-go/internal/reactor/packet.go new file mode 100644 index 00000000..bcb64f0b --- /dev/null +++ b/easytier-go/internal/reactor/packet.go @@ -0,0 +1,159 @@ +package reactor + +import ( + "context" + "fmt" + "net" +) + +type packetSink struct { + capacity int + packets [][]byte + available chan struct{} + closed chan struct{} +} + +type packetWriteWaiter struct { + handle uint64 + ready bool +} + +func (reactor *Reactor) RegisterPacketSink(capacity int) (uint64, error) { + if capacity <= 0 { + return 0, fmt.Errorf("packet sink capacity must be positive") + } + reactor.mu.Lock() + defer reactor.mu.Unlock() + if reactor.closed { + return 0, ErrInvalid + } + handle := reactor.allocateHandleLocked() + reactor.packetSinks[handle] = &packetSink{ + capacity: capacity, + available: make(chan struct{}, 1), + closed: make(chan struct{}), + } + return handle, nil +} + +func (reactor *Reactor) UnregisterPacketSink(handle uint64) { + reactor.mu.Lock() + sink := reactor.packetSinks[handle] + if sink == nil { + reactor.mu.Unlock() + return + } + delete(reactor.packetSinks, handle) + for operation, waiter := range reactor.packetWrites { + if waiter.handle != handle { + continue + } + delete(reactor.packetWrites, operation) + reactor.releaseOperationLocked(operation, operationPacketWrite) + } + close(sink.closed) + reactor.mu.Unlock() +} + +func (reactor *Reactor) TryPacketWrite(handle uint64, packet []byte) error { + reactor.mu.Lock() + sink, exists := reactor.packetSinks[handle] + if !exists { + reactor.mu.Unlock() + return ErrInvalid + } + if len(sink.packets) >= sink.capacity { + reactor.mu.Unlock() + return ErrWouldBlock + } + sink.packets = append(sink.packets, append([]byte(nil), packet...)) + available := sink.available + reactor.mu.Unlock() + select { + case available <- struct{}{}: + default: + } + return nil +} + +func (reactor *Reactor) StartPacketWriteReady(handle, operation uint64) error { + reactor.mu.Lock() + sink, exists := reactor.packetSinks[handle] + if !exists { + reactor.mu.Unlock() + return ErrInvalid + } + if err := reactor.claimOperationLocked(operation, operationPacketWrite); err != nil { + reactor.mu.Unlock() + return err + } + waiter := &packetWriteWaiter{ + handle: handle, + ready: len(sink.packets) < sink.capacity, + } + reactor.packetWrites[operation] = waiter + ready := waiter.ready + reactor.mu.Unlock() + if ready { + reactor.signalCompletion() + } + return nil +} + +func (reactor *Reactor) TakePacketWriteReady(operation uint64) error { + reactor.mu.Lock() + waiter, exists := reactor.packetWrites[operation] + if !exists || reactor.operations[operation] != operationPacketWrite { + reactor.mu.Unlock() + return ErrInvalid + } + if !waiter.ready { + reactor.mu.Unlock() + return ErrPending + } + delete(reactor.packetWrites, operation) + reactor.releaseOperationLocked(operation, operationPacketWrite) + reactor.mu.Unlock() + return nil +} + +func (reactor *Reactor) ReceivePacket(ctx context.Context, handle uint64) ([]byte, error) { + for { + reactor.mu.Lock() + sink, exists := reactor.packetSinks[handle] + if !exists { + reactor.mu.Unlock() + return nil, fmt.Errorf("receive packet: %w", net.ErrClosed) + } + if len(sink.packets) > 0 { + packet := sink.packets[0] + sink.packets[0] = nil + sink.packets = sink.packets[1:] + ready := false + for _, waiter := range reactor.packetWrites { + if waiter.handle == handle { + waiter.ready = true + ready = true + } + } + reactor.mu.Unlock() + if ready { + reactor.signalCompletion() + } + return packet, nil + } + available := sink.available + lifetimeDone := reactor.ctx.Done() + reactor.mu.Unlock() + + select { + case <-available: + case <-sink.closed: + return nil, fmt.Errorf("receive packet: %w", net.ErrClosed) + case <-lifetimeDone: + return nil, fmt.Errorf("receive packet: %w", reactor.ctx.Err()) + case <-ctx.Done(): + return nil, ctx.Err() + } + } +} diff --git a/easytier-go/internal/reactor/reactor.go b/easytier-go/internal/reactor/reactor.go new file mode 100644 index 00000000..504ee8e3 --- /dev/null +++ b/easytier-go/internal/reactor/reactor.go @@ -0,0 +1,343 @@ +package reactor + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + + "github.com/EasyTier/EasyTier/easytier-go/internal/contextutil" + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +var ( + ErrInvalid = errors.New("invalid host operation") + ErrPending = errors.New("host operation is pending") + ErrWouldBlock = errors.New("host operation would block") +) + +type operationKind uint8 + +const ( + operationRead operationKind = iota + 1 + operationWrite + operationUDPRead + operationUDPWrite + operationCreate + operationAccept + operationDNS + operationEnvironment + operationPacketWrite + operationManagement +) + +type Options struct { + Services platform.Services + InitialStreams map[uint64]net.Conn + InitialDatagrams map[uint64]net.PacketConn + Management ManagementHandler +} + +type Reactor struct { + ctx context.Context + cancel context.CancelFunc + + mu sync.Mutex + closed bool + closeDone chan struct{} + completion chan struct{} + workers sync.WaitGroup + nextHandle uint64 + + services platform.Services + managementHandler ManagementHandler + + operations map[uint64]operationKind + streams map[uint64]*streamState + datagrams map[uint64]*datagramState + drainingDatagrams map[*datagramState]struct{} + listeners map[uint64]*listenerState + reads map[uint64]*readOperation + writes map[uint64]*writeOperation + udpReads map[uint64]*udpReadWaiter + udpWrites map[uint64]*udpWriteWaiter + accepts map[uint64]*acceptWaiter + creates map[uint64]*createOperation + dns map[uint64]*dnsOperation + environments map[uint64]*environmentOperation + eventSinks map[uint64]eventSink + packetSinks map[uint64]*packetSink + packetWrites map[uint64]*packetWriteWaiter + management map[uint64]*managementOperation +} + +func New(parent context.Context, options Options) *Reactor { + if parent == nil { + parent = context.Background() + } + ctx, cancel := context.WithCancel(contextutil.WithoutCancel(parent)) + reactor := &Reactor{ + ctx: ctx, + cancel: cancel, + closeDone: make(chan struct{}), + completion: make(chan struct{}, 1), + nextHandle: 1 << 48, + services: options.Services, + managementHandler: options.Management, + operations: make(map[uint64]operationKind), + streams: make(map[uint64]*streamState, len(options.InitialStreams)), + datagrams: make(map[uint64]*datagramState, len(options.InitialDatagrams)), + drainingDatagrams: make(map[*datagramState]struct{}), + listeners: make(map[uint64]*listenerState), + reads: make(map[uint64]*readOperation), + writes: make(map[uint64]*writeOperation), + udpReads: make(map[uint64]*udpReadWaiter), + udpWrites: make(map[uint64]*udpWriteWaiter), + accepts: make(map[uint64]*acceptWaiter), + creates: make(map[uint64]*createOperation), + dns: make(map[uint64]*dnsOperation), + environments: make(map[uint64]*environmentOperation), + eventSinks: make(map[uint64]eventSink), + packetSinks: make(map[uint64]*packetSink), + packetWrites: make(map[uint64]*packetWriteWaiter), + management: make(map[uint64]*managementOperation), + } + for handle, connection := range options.InitialStreams { + reactor.streams[handle] = newStreamState(connection) + } + for handle, connection := range options.InitialDatagrams { + state := newDatagramState(connection) + reactor.datagrams[handle] = state + reactor.workers.Add(1) + go reactor.runUDPSends(handle, state) + } + return reactor +} + +func (reactor *Reactor) Completions() <-chan struct{} { + return reactor.completion +} + +func (reactor *Reactor) signalCompletion() { + select { + case reactor.completion <- struct{}{}: + default: + } +} + +func (reactor *Reactor) claimOperationLocked(id uint64, kind operationKind) error { + if reactor.closed { + return ErrInvalid + } + if _, exists := reactor.operations[id]; exists { + return ErrInvalid + } + reactor.operations[id] = kind + return nil +} + +func (reactor *Reactor) releaseOperationLocked(id uint64, kind operationKind) bool { + if reactor.operations[id] != kind { + return false + } + delete(reactor.operations, id) + return true +} + +func (reactor *Reactor) allocateHandleLocked() uint64 { + reactor.nextHandle++ + return reactor.nextHandle +} + +func (reactor *Reactor) CancelOperation(id uint64) error { + reactor.mu.Lock() + kind, exists := reactor.operations[id] + if !exists { + reactor.mu.Unlock() + return nil + } + delete(reactor.operations, id) + + var cancel context.CancelFunc + var resource ioResource + var readTask streamReadTask + var signalCompletion bool + switch kind { + case operationRead: + readTask, signalCompletion = reactor.cancelStreamReadLocked(id) + case operationWrite: + reactor.cancelStreamWriteLocked(id) + case operationUDPRead: + delete(reactor.udpReads, id) + case operationUDPWrite: + delete(reactor.udpWrites, id) + case operationAccept: + delete(reactor.accepts, id) + case operationCreate: + create := reactor.creates[id] + delete(reactor.creates, id) + if create != nil { + cancel = create.cancel + resource = create.resource() + } + case operationDNS: + operation := reactor.dns[id] + delete(reactor.dns, id) + if operation != nil { + cancel = operation.cancel + } + case operationEnvironment: + operation := reactor.environments[id] + delete(reactor.environments, id) + if operation != nil { + cancel = operation.cancel + } + case operationPacketWrite: + delete(reactor.packetWrites, id) + case operationManagement: + operation := reactor.management[id] + delete(reactor.management, id) + if operation != nil { + cancel = operation.cancel + } + default: + reactor.mu.Unlock() + return fmt.Errorf("%w: unknown operation kind %d", ErrInvalid, kind) + } + reactor.mu.Unlock() + readTask.launch(reactor) + if signalCompletion { + reactor.signalCompletion() + } + if cancel != nil { + cancel() + } + if resource != nil { + _ = resource.Close() + } + return nil +} + +type ioResource interface { + Close() error +} + +func (reactor *Reactor) CloseHandle(handle uint64) error { + reactor.mu.Lock() + stream := reactor.streams[handle] + delete(reactor.streams, handle) + streamCompletion := false + if stream != nil { + streamCompletion = reactor.closeStreamLocked(stream) + } + datagram := reactor.datagrams[handle] + delete(reactor.datagrams, handle) + if datagram != nil { + reactor.drainingDatagrams[datagram] = struct{}{} + } + listener := reactor.listeners[handle] + delete(reactor.listeners, handle) + reactor.mu.Unlock() + if streamCompletion { + reactor.signalCompletion() + } + + if stream == nil && datagram == nil && listener == nil { + return nil + } + var closeErrors []error + if stream != nil { + if err := stream.connection.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + closeErrors = append(closeErrors, err) + } + } + if datagram != nil { + datagram.closeAfterQueuedSends() + } + if listener != nil { + if err := listener.listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + closeErrors = append(closeErrors, err) + } + for _, connection := range listener.accepted { + _ = connection.Close() + } + } + return errors.Join(closeErrors...) +} + +func (reactor *Reactor) Close() { + reactor.mu.Lock() + if reactor.closed { + done := reactor.closeDone + reactor.mu.Unlock() + <-done + return + } + reactor.closed = true + reactor.cancel() + + streams := reactor.streams + datagrams := reactor.datagrams + drainingDatagrams := reactor.drainingDatagrams + listeners := reactor.listeners + creates := reactor.creates + dnsOperations := reactor.dns + environmentOperations := reactor.environments + managementOperations := reactor.management + for _, stream := range streams { + stream.shutdownLocked() + } + + reactor.operations = make(map[uint64]operationKind) + reactor.streams = make(map[uint64]*streamState) + reactor.datagrams = make(map[uint64]*datagramState) + reactor.drainingDatagrams = make(map[*datagramState]struct{}) + reactor.listeners = make(map[uint64]*listenerState) + reactor.reads = make(map[uint64]*readOperation) + reactor.writes = make(map[uint64]*writeOperation) + reactor.udpReads = make(map[uint64]*udpReadWaiter) + reactor.udpWrites = make(map[uint64]*udpWriteWaiter) + reactor.accepts = make(map[uint64]*acceptWaiter) + reactor.creates = make(map[uint64]*createOperation) + reactor.dns = make(map[uint64]*dnsOperation) + reactor.environments = make(map[uint64]*environmentOperation) + reactor.eventSinks = make(map[uint64]eventSink) + reactor.packetSinks = make(map[uint64]*packetSink) + reactor.packetWrites = make(map[uint64]*packetWriteWaiter) + reactor.management = make(map[uint64]*managementOperation) + reactor.mu.Unlock() + + for _, stream := range streams { + _ = stream.connection.Close() + } + for _, datagram := range datagrams { + datagram.closeNow() + } + for datagram := range drainingDatagrams { + datagram.closeNow() + } + for _, listener := range listeners { + _ = listener.listener.Close() + for _, connection := range listener.accepted { + _ = connection.Close() + } + } + for _, create := range creates { + create.cancel() + if resource := create.resource(); resource != nil { + _ = resource.Close() + } + } + for _, operation := range dnsOperations { + operation.cancel() + } + for _, operation := range environmentOperations { + operation.cancel() + } + for _, operation := range managementOperations { + operation.cancel() + } + reactor.workers.Wait() + close(reactor.closeDone) +} diff --git a/easytier-go/internal/reactor/reactor_test.go b/easytier-go/internal/reactor/reactor_test.go new file mode 100644 index 00000000..26e599ed --- /dev/null +++ b/easytier-go/internal/reactor/reactor_test.go @@ -0,0 +1,531 @@ +package reactor + +import ( + "context" + "errors" + "net" + "sync" + "testing" + "time" + + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +func TestOperationIDsAreUniqueAcrossOperationKinds(t *testing.T) { + host, peer := net.Pipe() + defer peer.Close() + runtime := New(context.Background(), Options{ + InitialStreams: map[uint64]net.Conn{1: host}, + }) + defer runtime.Close() + if err := runtime.StartRead(1, 99, 1); err != nil { + t.Fatalf("start read: %v", err) + } + if err := runtime.StartWrite(1, 99, []byte("x")); !errors.Is(err, ErrInvalid) { + t.Fatalf("duplicate cross-kind operation error = %v, want invalid", err) + } + if err := runtime.CancelOperation(99); err != nil { + t.Fatalf("cancel read: %v", err) + } + if err := runtime.StartWrite(1, 99, []byte("x")); err != nil { + t.Fatalf("reuse released operation ID: %v", err) + } +} + +type blockingSocketFactory struct { + started chan struct{} + once sync.Once +} + +func (factory *blockingSocketFactory) ConnectTCP( + ctx context.Context, + _ platform.TCPConnectOptions, +) (net.Conn, error) { + factory.once.Do(func() { close(factory.started) }) + <-ctx.Done() + return nil, ctx.Err() +} + +func (*blockingSocketFactory) BindUDP( + context.Context, + platform.UDPBindOptions, +) (net.PacketConn, error) { + return nil, errors.New("not used") +} + +func (*blockingSocketFactory) ListenTCP( + context.Context, + platform.TCPListenOptions, +) (net.Listener, error) { + return nil, errors.New("not used") +} + +func TestCloseCancelsInstanceScopedOperations(t *testing.T) { + factory := &blockingSocketFactory{started: make(chan struct{})} + runtime := New(context.Background(), Options{ + Services: platform.Services{Sockets: factory}, + }) + if err := runtime.StartTCPConnect(1, platform.TCPConnectOptions{}); err != nil { + t.Fatalf("start TCP connect: %v", err) + } + select { + case <-factory.started: + case <-time.After(time.Second): + t.Fatal("socket factory did not start") + } + closed := make(chan struct{}) + go func() { + runtime.Close() + close(closed) + }() + select { + case <-closed: + case <-time.After(time.Second): + t.Fatal("reactor close did not cancel socket factory") + } +} + +func TestCreateTakeMustMatchStartKind(t *testing.T) { + factory := &blockingSocketFactory{started: make(chan struct{})} + runtime := New(context.Background(), Options{ + Services: platform.Services{Sockets: factory}, + }) + defer runtime.Close() + if err := runtime.StartTCPConnect(1, platform.TCPConnectOptions{}); err != nil { + t.Fatalf("start TCP connect: %v", err) + } + if _, err := runtime.TakeUDPBind(1); !errors.Is(err, ErrInvalid) { + t.Fatalf("take TCP connect as UDP bind error = %v, want invalid", err) + } + if err := runtime.CancelOperation(1); err != nil { + t.Fatalf("cancel TCP connect: %v", err) + } +} + +func TestReceivePacketUnblocksWhenReactorCloses(t *testing.T) { + runtime := New(context.Background(), Options{}) + handle, err := runtime.RegisterPacketSink(1) + if err != nil { + t.Fatalf("register packet sink: %v", err) + } + result := make(chan error, 1) + go func() { + _, err := runtime.ReceivePacket(context.Background(), handle) + result <- err + }() + runtime.Close() + select { + case err := <-result: + if err == nil { + t.Fatal("packet receive succeeded after close") + } + case <-time.After(time.Second): + t.Fatal("packet receive remained blocked after close") + } +} + +func TestEventSinkRejectsFullAndUnregisteredWrites(t *testing.T) { + runtime := New(context.Background(), Options{}) + defer runtime.Close() + events := make(chan string, 1) + handle, err := runtime.RegisterEventSink(func(kind, message string) bool { + select { + case events <- kind + ":" + message: + return true + default: + return false + } + }) + if err != nil { + t.Fatalf("register event sink: %v", err) + } + if err := runtime.TryEventWrite(handle, "peer_added", "PeerAdded(7)"); err != nil { + t.Fatalf("write event: %v", err) + } + if err := runtime.TryEventWrite(handle, "peer_removed", "PeerRemoved(7)"); !errors.Is( + err, + ErrWouldBlock, + ) { + t.Fatalf("full event sink error = %v, want would block", err) + } + if event := <-events; event != "peer_added:PeerAdded(7)" { + t.Fatalf("event = %q", event) + } + runtime.UnregisterEventSink(handle) + if err := runtime.TryEventWrite(handle, "peer_removed", "PeerRemoved(7)"); !errors.Is( + err, + ErrInvalid, + ) { + t.Fatalf("unregistered event sink error = %v, want invalid", err) + } +} + +func TestReceivePacketUnblocksWhenSinkIsUnregistered(t *testing.T) { + runtime := New(context.Background(), Options{}) + defer runtime.Close() + handle, err := runtime.RegisterPacketSink(1) + if err != nil { + t.Fatalf("register packet sink: %v", err) + } + result := make(chan error, 1) + go func() { + _, err := runtime.ReceivePacket(context.Background(), handle) + result <- err + }() + runtime.UnregisterPacketSink(handle) + select { + case err := <-result: + if !errors.Is(err, net.ErrClosed) { + t.Fatalf("packet receive error = %v, want net.ErrClosed", err) + } + case <-time.After(time.Second): + t.Fatal("packet receive remained blocked after sink unregister") + } +} + +func TestConcurrentCloseWaitsForOneCleanup(t *testing.T) { + runtime := New(context.Background(), Options{}) + var workers sync.WaitGroup + workers.Add(2) + for worker := 0; worker < 2; worker++ { + go func() { + defer workers.Done() + runtime.Close() + }() + } + done := make(chan struct{}) + go func() { + workers.Wait() + close(done) + }() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("concurrent close did not converge") + } +} + +func TestUDPSendWorkerWritesQueuedDatagram(t *testing.T) { + hostPacket, err := net.ListenPacket("udp4", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen host UDP: %v", err) + } + peerPacket, err := net.ListenPacket("udp4", "127.0.0.1:0") + if err != nil { + hostPacket.Close() + t.Fatalf("listen peer UDP: %v", err) + } + defer peerPacket.Close() + const handle uint64 = 17 + runtime := New(context.Background(), Options{ + InitialDatagrams: map[uint64]net.PacketConn{handle: hostPacket}, + }) + defer runtime.Close() + peer := peerPacket.LocalAddr().(*net.UDPAddr) + if err := runtime.TryUDPSend(handle, []byte("udp"), peer); err != nil { + t.Fatalf("queue UDP send: %v", err) + } + if err := peerPacket.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatal(err) + } + buffer := make([]byte, 16) + n, _, err := peerPacket.ReadFrom(buffer) + if err != nil { + t.Fatalf("read queued UDP send: %v", err) + } + if string(buffer[:n]) != "udp" { + t.Fatalf("queued UDP payload = %q", buffer[:n]) + } +} + +type blockingWritePacketConn struct { + writeStarted chan struct{} + closed chan struct{} + startOnce sync.Once + closeOnce sync.Once +} + +func newBlockingWritePacketConn() *blockingWritePacketConn { + return &blockingWritePacketConn{ + writeStarted: make(chan struct{}), + closed: make(chan struct{}), + } +} + +func (connection *blockingWritePacketConn) ReadFrom([]byte) (int, net.Addr, error) { + <-connection.closed + return 0, nil, net.ErrClosed +} + +func (connection *blockingWritePacketConn) WriteTo( + []byte, + net.Addr, +) (int, error) { + connection.startOnce.Do(func() { close(connection.writeStarted) }) + <-connection.closed + return 0, net.ErrClosed +} + +func (connection *blockingWritePacketConn) Close() error { + connection.closeOnce.Do(func() { close(connection.closed) }) + return nil +} + +func (*blockingWritePacketConn) LocalAddr() net.Addr { + return &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)} +} + +func (*blockingWritePacketConn) SetDeadline(time.Time) error { return nil } +func (*blockingWritePacketConn) SetReadDeadline(time.Time) error { return nil } +func (*blockingWritePacketConn) SetWriteDeadline(time.Time) error { return nil } + +func TestCloseForcesClosedHandleWithBlockedUDPSend(t *testing.T) { + const handle uint64 = 23 + connection := newBlockingWritePacketConn() + runtime := New(context.Background(), Options{ + InitialDatagrams: map[uint64]net.PacketConn{handle: connection}, + }) + if err := runtime.TryUDPSend( + handle, + []byte("blocked"), + &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1}, + ); err != nil { + t.Fatalf("queue UDP send: %v", err) + } + select { + case <-connection.writeStarted: + case <-time.After(time.Second): + connection.Close() + runtime.Close() + t.Fatal("UDP write did not start") + } + if err := runtime.CloseHandle(handle); err != nil { + connection.Close() + runtime.Close() + t.Fatalf("close UDP handle: %v", err) + } + + closed := make(chan struct{}) + go func() { + runtime.Close() + close(closed) + }() + select { + case <-closed: + case <-time.After(time.Second): + connection.Close() + <-closed + t.Fatal("reactor close did not force-close draining UDP socket") + } +} + +var errTransientUDPRead = errors.New("transient UDP read error") + +type retryReadPacketConn struct { + firstReadStarted chan struct{} + releaseFirstRead chan struct{} + secondReadStarted chan struct{} + secondPayload chan []byte + closed chan struct{} + closeOnce sync.Once + mu sync.Mutex + readCount int +} + +func newRetryReadPacketConn() *retryReadPacketConn { + return &retryReadPacketConn{ + firstReadStarted: make(chan struct{}), + releaseFirstRead: make(chan struct{}), + secondReadStarted: make(chan struct{}), + secondPayload: make(chan []byte), + closed: make(chan struct{}), + } +} + +func (connection *retryReadPacketConn) ReadFrom(buffer []byte) (int, net.Addr, error) { + connection.mu.Lock() + connection.readCount++ + readCount := connection.readCount + connection.mu.Unlock() + + switch readCount { + case 1: + close(connection.firstReadStarted) + select { + case <-connection.releaseFirstRead: + return 0, nil, errTransientUDPRead + case <-connection.closed: + return 0, nil, net.ErrClosed + } + case 2: + close(connection.secondReadStarted) + select { + case payload := <-connection.secondPayload: + return copy(buffer, payload), &net.UDPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: 2, + }, nil + case <-connection.closed: + return 0, nil, net.ErrClosed + } + default: + <-connection.closed + return 0, nil, net.ErrClosed + } +} + +func (*retryReadPacketConn) WriteTo(payload []byte, _ net.Addr) (int, error) { + return len(payload), nil +} + +func (connection *retryReadPacketConn) Close() error { + connection.closeOnce.Do(func() { close(connection.closed) }) + return nil +} + +func (*retryReadPacketConn) LocalAddr() net.Addr { + return &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)} +} + +func (*retryReadPacketConn) SetDeadline(time.Time) error { return nil } +func (*retryReadPacketConn) SetReadDeadline(time.Time) error { return nil } +func (*retryReadPacketConn) SetWriteDeadline(time.Time) error { return nil } + +func TestUDPReceiveErrorRestartsWorkerForRemainingWaiter(t *testing.T) { + const handle uint64 = 29 + connection := newRetryReadPacketConn() + runtime := New(context.Background(), Options{ + InitialDatagrams: map[uint64]net.PacketConn{handle: connection}, + }) + defer runtime.Close() + + if err := runtime.StartUDPReceive(handle, 1); err != nil { + t.Fatalf("start first UDP receive: %v", err) + } + select { + case <-connection.firstReadStarted: + case <-time.After(time.Second): + t.Fatal("first UDP read did not start") + } + if err := runtime.StartUDPReceive(handle, 2); err != nil { + t.Fatalf("start second UDP receive: %v", err) + } + close(connection.releaseFirstRead) + select { + case <-runtime.Completions(): + case <-time.After(time.Second): + t.Fatal("transient UDP read error did not signal completion") + } + if _, err := runtime.TakeUDPReceive(1, 64); !errors.Is(err, errTransientUDPRead) { + t.Fatalf("first UDP receive error = %v, want transient error", err) + } + + select { + case <-connection.secondReadStarted: + case <-time.After(time.Second): + t.Fatal("remaining UDP waiter did not restart receive worker") + } + connection.secondPayload <- []byte("retry") + select { + case <-runtime.Completions(): + case <-time.After(time.Second): + t.Fatal("retried UDP read did not signal completion") + } + datagram, err := runtime.TakeUDPReceive(2, 64) + if err != nil { + t.Fatalf("take retried UDP receive: %v", err) + } + if string(datagram.Data) != "retry" { + t.Fatalf("retried UDP payload = %q, want retry", datagram.Data) + } +} + +type prefetchPacketConn struct { + readStarted chan struct{} + payloads chan []byte + closed chan struct{} + closeOnce sync.Once +} + +func newPrefetchPacketConn() *prefetchPacketConn { + return &prefetchPacketConn{ + readStarted: make(chan struct{}, 3), + payloads: make(chan []byte, 2), + closed: make(chan struct{}), + } +} + +func (connection *prefetchPacketConn) ReadFrom(buffer []byte) (int, net.Addr, error) { + connection.readStarted <- struct{}{} + select { + case payload := <-connection.payloads: + return copy(buffer, payload), &net.UDPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: 3, + }, nil + case <-connection.closed: + return 0, nil, net.ErrClosed + } +} + +func (*prefetchPacketConn) WriteTo(payload []byte, _ net.Addr) (int, error) { + return len(payload), nil +} + +func (connection *prefetchPacketConn) Close() error { + connection.closeOnce.Do(func() { close(connection.closed) }) + return nil +} + +func (*prefetchPacketConn) LocalAddr() net.Addr { + return &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)} +} + +func (*prefetchPacketConn) SetDeadline(time.Time) error { return nil } +func (*prefetchPacketConn) SetReadDeadline(time.Time) error { return nil } +func (*prefetchPacketConn) SetWriteDeadline(time.Time) error { return nil } + +func TestUDPReceivePrefetchesWhileWaiterIsPending(t *testing.T) { + const handle uint64 = 31 + connection := newPrefetchPacketConn() + runtime := New(context.Background(), Options{ + InitialDatagrams: map[uint64]net.PacketConn{handle: connection}, + }) + defer runtime.Close() + + if err := runtime.StartUDPReceive(handle, 1); err != nil { + t.Fatalf("start first UDP receive: %v", err) + } + waitForUDPReadStart(t, connection) + connection.payloads <- []byte("first") + waitForUDPReadStart(t, connection) + connection.payloads <- []byte("second") + waitForUDPReadStart(t, connection) + + first, err := runtime.TakeUDPReceive(1, 64) + if err != nil { + t.Fatalf("take first prefetched UDP receive: %v", err) + } + if string(first.Data) != "first" { + t.Fatalf("first prefetched UDP payload = %q, want first", first.Data) + } + if err := runtime.StartUDPReceive(handle, 2); err != nil { + t.Fatalf("start second UDP receive: %v", err) + } + second, err := runtime.TakeUDPReceive(2, 64) + if err != nil { + t.Fatalf("take second prefetched UDP receive: %v", err) + } + if string(second.Data) != "second" { + t.Fatalf("second prefetched UDP payload = %q, want second", second.Data) + } +} + +func waitForUDPReadStart(t *testing.T, connection *prefetchPacketConn) { + t.Helper() + select { + case <-connection.readStarted: + case <-time.After(time.Second): + t.Fatal("UDP read did not start") + } +} diff --git a/easytier-go/internal/reactor/stream.go b/easytier-go/internal/reactor/stream.go new file mode 100644 index 00000000..037593b3 --- /dev/null +++ b/easytier-go/internal/reactor/stream.go @@ -0,0 +1,363 @@ +package reactor + +import ( + "errors" + "io" + "net" +) + +type streamState struct { + connection net.Conn + closed bool + + readQueue []*readOperation + readRunning bool + readReady bool + readData []byte + readErr error + + writeQueue []*writeOperation + writeRunning bool + writeActive *writeOperation +} + +type readOperation struct { + capacity uint32 + stream *streamState +} + +type writeOperation struct { + operation uint64 + stream *streamState + data []byte + done bool + err error +} + +type streamReadTask struct { + capacity uint32 + stream *streamState +} + +func newStreamState(connection net.Conn) *streamState { + return &streamState{connection: connection} +} + +func (task streamReadTask) launch(reactor *Reactor) { + if task.stream != nil { + go reactor.runStreamRead(task.capacity, task.stream) + } +} + +func (reactor *Reactor) StartRead(handle, operation uint64, capacity uint32) error { + reactor.mu.Lock() + stream, exists := reactor.streams[handle] + if !exists || capacity == 0 { + reactor.mu.Unlock() + return ErrInvalid + } + if err := reactor.claimOperationLocked(operation, operationRead); err != nil { + reactor.mu.Unlock() + return err + } + waiter := &readOperation{ + capacity: capacity, + stream: stream, + } + reactor.reads[operation] = waiter + stream.readQueue = append(stream.readQueue, waiter) + ready := stream.readAvailableLocked() + task := reactor.prepareStreamReadLocked(stream) + reactor.mu.Unlock() + + task.launch(reactor) + if ready { + reactor.signalCompletion() + } + return nil +} + +func (reactor *Reactor) prepareStreamReadLocked(stream *streamState) streamReadTask { + if reactor.closed || + stream.closed || + stream.readRunning || + stream.readReady || + len(stream.readQueue) == 0 { + return streamReadTask{} + } + stream.readRunning = true + reactor.workers.Add(1) + return streamReadTask{ + capacity: stream.readQueue[0].capacity, + stream: stream, + } +} + +func (reactor *Reactor) runStreamRead( + capacity uint32, + stream *streamState, +) { + defer reactor.workers.Done() + buffer := make([]byte, capacity) + n, err := stream.connection.Read(buffer) + + reactor.mu.Lock() + stream.readRunning = false + stream.readData = append(stream.readData[:0], buffer[:n]...) + stream.readErr = err + stream.readReady = true + ready := len(stream.readQueue) > 0 + reactor.mu.Unlock() + if ready { + reactor.signalCompletion() + } +} + +func (reactor *Reactor) TakeRead(operation uint64) ([]byte, error) { + reactor.mu.Lock() + waiter, exists := reactor.reads[operation] + if !exists || reactor.operations[operation] != operationRead { + reactor.mu.Unlock() + return nil, ErrInvalid + } + stream := waiter.stream + if len(stream.readQueue) == 0 || stream.readQueue[0] != waiter { + reactor.mu.Unlock() + return nil, ErrPending + } + if !stream.readAvailableLocked() { + reactor.mu.Unlock() + return nil, ErrPending + } + + var data []byte + var err error + if stream.readReady { + take := len(stream.readData) + if take > int(waiter.capacity) { + take = int(waiter.capacity) + } + data = append([]byte(nil), stream.readData[:take]...) + if take == len(stream.readData) { + stream.readData = nil + err = stream.readErr + stream.readErr = nil + stream.readReady = false + } else { + stream.readData = stream.readData[take:] + } + } else { + err = net.ErrClosed + } + + stream.readQueue[0] = nil + stream.readQueue = stream.readQueue[1:] + if len(stream.readQueue) == 0 { + stream.readQueue = nil + } + delete(reactor.reads, operation) + reactor.releaseOperationLocked(operation, operationRead) + ready := len(stream.readQueue) > 0 && stream.readAvailableLocked() + task := reactor.prepareStreamReadLocked(stream) + reactor.mu.Unlock() + + task.launch(reactor) + if ready { + reactor.signalCompletion() + } + if errors.Is(err, io.EOF) { + err = nil + } + return data, err +} + +func (reactor *Reactor) cancelStreamReadLocked( + operation uint64, +) (streamReadTask, bool) { + waiter := reactor.reads[operation] + delete(reactor.reads, operation) + if waiter == nil { + return streamReadTask{}, false + } + stream := waiter.stream + wasFirst := len(stream.readQueue) > 0 && stream.readQueue[0] == waiter + stream.readQueue = removeReadOperation(stream.readQueue, waiter) + ready := wasFirst && len(stream.readQueue) > 0 && stream.readAvailableLocked() + task := reactor.prepareStreamReadLocked(stream) + return task, ready +} + +func removeReadOperation( + queue []*readOperation, + operation *readOperation, +) []*readOperation { + for index, queued := range queue { + if queued != operation { + continue + } + copy(queue[index:], queue[index+1:]) + queue[len(queue)-1] = nil + queue = queue[:len(queue)-1] + if len(queue) == 0 { + return nil + } + return queue + } + return queue +} + +func (stream *streamState) readAvailableLocked() bool { + return stream.readReady || stream.closed && !stream.readRunning +} + +func (reactor *Reactor) StartWrite(handle, operation uint64, data []byte) error { + data = append([]byte(nil), data...) + reactor.mu.Lock() + stream, exists := reactor.streams[handle] + if !exists { + reactor.mu.Unlock() + return ErrInvalid + } + if err := reactor.claimOperationLocked(operation, operationWrite); err != nil { + reactor.mu.Unlock() + return err + } + request := &writeOperation{ + operation: operation, + stream: stream, + data: data, + } + reactor.writes[operation] = request + stream.writeQueue = append(stream.writeQueue, request) + startWorker := !stream.writeRunning + if startWorker { + stream.writeRunning = true + reactor.workers.Add(1) + } + reactor.mu.Unlock() + + if startWorker { + go reactor.runStreamWrites(stream) + } + return nil +} + +func (reactor *Reactor) runStreamWrites(stream *streamState) { + defer reactor.workers.Done() + for { + reactor.mu.Lock() + if reactor.closed || len(stream.writeQueue) == 0 { + stream.writeQueue = nil + stream.writeRunning = false + reactor.mu.Unlock() + return + } + request := stream.writeQueue[0] + stream.writeQueue[0] = nil + stream.writeQueue = stream.writeQueue[1:] + if len(stream.writeQueue) == 0 { + stream.writeQueue = nil + } + stream.writeActive = request + data := request.data + reactor.mu.Unlock() + + written := 0 + var writeErr error + for written < len(data) { + n, err := stream.connection.Write(data[written:]) + written += n + if err != nil { + writeErr = err + break + } + if n == 0 { + writeErr = io.ErrShortWrite + break + } + } + + reactor.mu.Lock() + stream.writeActive = nil + request.data = nil + ready := reactor.writes[request.operation] == request && + reactor.operations[request.operation] == operationWrite + if ready { + request.err = writeErr + request.done = true + } + reactor.mu.Unlock() + if ready { + reactor.signalCompletion() + } + } +} + +func (reactor *Reactor) TakeWrite(operation uint64) error { + reactor.mu.Lock() + result, exists := reactor.writes[operation] + if !exists || reactor.operations[operation] != operationWrite { + reactor.mu.Unlock() + return ErrInvalid + } + if !result.done { + reactor.mu.Unlock() + return ErrPending + } + delete(reactor.writes, operation) + reactor.releaseOperationLocked(operation, operationWrite) + err := result.err + reactor.mu.Unlock() + return err +} + +func (reactor *Reactor) cancelStreamWriteLocked(operation uint64) { + request := reactor.writes[operation] + delete(reactor.writes, operation) + if request == nil || request.stream.writeActive == request { + return + } + request.stream.writeQueue = removeWriteOperation(request.stream.writeQueue, request) + request.data = nil +} + +func removeWriteOperation( + queue []*writeOperation, + operation *writeOperation, +) []*writeOperation { + for index, queued := range queue { + if queued != operation { + continue + } + copy(queue[index:], queue[index+1:]) + queue[len(queue)-1] = nil + queue = queue[:len(queue)-1] + if len(queue) == 0 { + return nil + } + return queue + } + return queue +} + +func (reactor *Reactor) closeStreamLocked(stream *streamState) bool { + stream.closed = true + ready := len(stream.readQueue) > 0 && stream.readAvailableLocked() + for _, request := range stream.writeQueue { + request.data = nil + if reactor.writes[request.operation] != request { + continue + } + request.err = net.ErrClosed + request.done = true + ready = true + } + stream.writeQueue = nil + return ready +} + +func (stream *streamState) shutdownLocked() { + stream.closed = true + stream.readQueue = nil + stream.writeQueue = nil +} diff --git a/easytier-go/internal/reactor/stream_test.go b/easytier-go/internal/reactor/stream_test.go new file mode 100644 index 00000000..93aefd08 --- /dev/null +++ b/easytier-go/internal/reactor/stream_test.go @@ -0,0 +1,290 @@ +package reactor + +import ( + "context" + "errors" + "net" + "sync" + "testing" + "time" +) + +type controlledReadResult struct { + data []byte + err error +} + +type controlledReadRequest struct { + result chan controlledReadResult +} + +type controlledReadConn struct { + reads chan controlledReadRequest + closed chan struct{} + closeOnce sync.Once +} + +func newControlledReadConn() *controlledReadConn { + return &controlledReadConn{ + reads: make(chan controlledReadRequest, 2), + closed: make(chan struct{}), + } +} + +func (connection *controlledReadConn) Read(buffer []byte) (int, error) { + request := controlledReadRequest{result: make(chan controlledReadResult, 1)} + select { + case connection.reads <- request: + case <-connection.closed: + return 0, net.ErrClosed + } + select { + case result := <-request.result: + return copy(buffer, result.data), result.err + case <-connection.closed: + return 0, net.ErrClosed + } +} + +func (connection *controlledReadConn) Write(payload []byte) (int, error) { + select { + case <-connection.closed: + return 0, net.ErrClosed + default: + return len(payload), nil + } +} + +func (connection *controlledReadConn) Close() error { + connection.closeOnce.Do(func() { close(connection.closed) }) + return nil +} + +func (*controlledReadConn) LocalAddr() net.Addr { return streamTestAddr("local") } +func (*controlledReadConn) RemoteAddr() net.Addr { return streamTestAddr("remote") } + +func (*controlledReadConn) SetDeadline(time.Time) error { return nil } +func (*controlledReadConn) SetReadDeadline(time.Time) error { return nil } +func (*controlledReadConn) SetWriteDeadline(time.Time) error { return nil } + +type streamTestAddr string + +func (streamTestAddr) Network() string { return "test" } +func (address streamTestAddr) String() string { return string(address) } + +func waitForReadRequest(t *testing.T, connection *controlledReadConn) controlledReadRequest { + t.Helper() + select { + case request := <-connection.reads: + return request + case <-time.After(time.Second): + t.Fatal("stream read did not start") + return controlledReadRequest{} + } +} + +func waitForCompletion(t *testing.T, runtime *Reactor) { + t.Helper() + select { + case <-runtime.Completions(): + case <-time.After(time.Second): + t.Fatal("stream operation did not signal completion") + } +} + +func TestCanceledPendingReadPreservesDataForNextOperation(t *testing.T) { + const handle uint64 = 1 + connection := newControlledReadConn() + runtime := New(context.Background(), Options{ + InitialStreams: map[uint64]net.Conn{handle: connection}, + }) + defer runtime.Close() + + if err := runtime.StartRead(handle, 1, 4); err != nil { + t.Fatalf("start first read: %v", err) + } + request := waitForReadRequest(t, connection) + if err := runtime.CancelOperation(1); err != nil { + t.Fatalf("cancel first read: %v", err) + } + if err := runtime.StartRead(handle, 2, 2); err != nil { + t.Fatalf("start second read: %v", err) + } + request.result <- controlledReadResult{data: []byte("data")} + + waitForCompletion(t, runtime) + data, err := runtime.TakeRead(2) + if err != nil { + t.Fatalf("take second read: %v", err) + } + if string(data) != "da" { + t.Fatalf("second read data = %q, want da", data) + } + if err := runtime.StartRead(handle, 3, 2); err != nil { + t.Fatalf("start third read: %v", err) + } + waitForCompletion(t, runtime) + data, err = runtime.TakeRead(3) + if err != nil { + t.Fatalf("take third read: %v", err) + } + if string(data) != "ta" { + t.Fatalf("third read data = %q, want ta", data) + } +} + +func TestCanceledCompletedReadPreservesDataForNextOperation(t *testing.T) { + const handle uint64 = 1 + connection := newControlledReadConn() + runtime := New(context.Background(), Options{ + InitialStreams: map[uint64]net.Conn{handle: connection}, + }) + defer runtime.Close() + + if err := runtime.StartRead(handle, 1, 4); err != nil { + t.Fatalf("start first read: %v", err) + } + request := waitForReadRequest(t, connection) + request.result <- controlledReadResult{data: []byte("data")} + waitForCompletion(t, runtime) + if err := runtime.CancelOperation(1); err != nil { + t.Fatalf("cancel completed read: %v", err) + } + if err := runtime.StartRead(handle, 2, 4); err != nil { + t.Fatalf("start second read: %v", err) + } + + waitForCompletion(t, runtime) + data, err := runtime.TakeRead(2) + if err != nil { + t.Fatalf("take second read: %v", err) + } + if string(data) != "data" { + t.Fatalf("second read data = %q, want data", data) + } +} + +type blockingFirstWriteConn struct { + firstStarted chan struct{} + secondStarted chan struct{} + releaseFirst chan struct{} + closed chan struct{} + closeOnce sync.Once + mu sync.Mutex + writes int +} + +func newBlockingFirstWriteConn() *blockingFirstWriteConn { + return &blockingFirstWriteConn{ + firstStarted: make(chan struct{}), + secondStarted: make(chan struct{}), + releaseFirst: make(chan struct{}), + closed: make(chan struct{}), + } +} + +func (connection *blockingFirstWriteConn) Read([]byte) (int, error) { + <-connection.closed + return 0, net.ErrClosed +} + +func (connection *blockingFirstWriteConn) Write(payload []byte) (int, error) { + connection.mu.Lock() + connection.writes++ + write := connection.writes + connection.mu.Unlock() + + switch write { + case 1: + close(connection.firstStarted) + select { + case <-connection.releaseFirst: + case <-connection.closed: + return 0, net.ErrClosed + } + case 2: + close(connection.secondStarted) + } + return len(payload), nil +} + +func (connection *blockingFirstWriteConn) Close() error { + connection.closeOnce.Do(func() { close(connection.closed) }) + return nil +} + +func (*blockingFirstWriteConn) LocalAddr() net.Addr { return streamTestAddr("local") } +func (*blockingFirstWriteConn) RemoteAddr() net.Addr { return streamTestAddr("remote") } + +func (*blockingFirstWriteConn) SetDeadline(time.Time) error { return nil } +func (*blockingFirstWriteConn) SetReadDeadline(time.Time) error { return nil } +func (*blockingFirstWriteConn) SetWriteDeadline(time.Time) error { return nil } + +func TestCanceledWriteKeepsFollowingWriteSerialized(t *testing.T) { + const handle uint64 = 1 + connection := newBlockingFirstWriteConn() + runtime := New(context.Background(), Options{ + InitialStreams: map[uint64]net.Conn{handle: connection}, + }) + defer runtime.Close() + + if err := runtime.StartWrite(handle, 1, []byte("first")); err != nil { + t.Fatalf("start first write: %v", err) + } + select { + case <-connection.firstStarted: + case <-time.After(time.Second): + t.Fatal("first stream write did not start") + } + if err := runtime.CancelOperation(1); err != nil { + t.Fatalf("cancel first write: %v", err) + } + if err := runtime.StartWrite(handle, 2, []byte("second")); err != nil { + t.Fatalf("start second write: %v", err) + } + + select { + case <-connection.secondStarted: + t.Fatal("second stream write started before canceled write completed") + case <-time.After(100 * time.Millisecond): + } + close(connection.releaseFirst) + select { + case <-connection.secondStarted: + case <-time.After(time.Second): + t.Fatal("second stream write did not start after first completed") + } + waitForCompletion(t, runtime) + if err := runtime.TakeWrite(2); err != nil { + t.Fatalf("take second write: %v", err) + } +} + +func TestClosedHandleCompletesQueuedStreamWrite(t *testing.T) { + const handle uint64 = 1 + connection := newBlockingFirstWriteConn() + runtime := New(context.Background(), Options{ + InitialStreams: map[uint64]net.Conn{handle: connection}, + }) + defer runtime.Close() + + if err := runtime.StartWrite(handle, 1, []byte("first")); err != nil { + t.Fatalf("start first write: %v", err) + } + select { + case <-connection.firstStarted: + case <-time.After(time.Second): + t.Fatal("first stream write did not start") + } + if err := runtime.StartWrite(handle, 2, []byte("second")); err != nil { + t.Fatalf("start second write: %v", err) + } + if err := runtime.CloseHandle(handle); err != nil { + t.Fatalf("close stream handle: %v", err) + } + + waitForCompletion(t, runtime) + if err := runtime.TakeWrite(2); !errors.Is(err, net.ErrClosed) { + t.Fatalf("queued write error = %v, want net.ErrClosed", err) + } +} diff --git a/easytier-go/internal/reactor/udp.go b/easytier-go/internal/reactor/udp.go new file mode 100644 index 00000000..f04e6437 --- /dev/null +++ b/easytier-go/internal/reactor/udp.go @@ -0,0 +1,350 @@ +package reactor + +import ( + "fmt" + "net" + "sync" +) + +const ( + udpReceiveQueueCapacity = 64 + udpSendQueueCapacity = 64 +) + +type Datagram struct { + Data []byte + Peer *net.UDPAddr +} + +type udpSend struct { + data []byte + peer *net.UDPAddr +} + +type datagramState struct { + connection net.PacketConn + receiveBuffer []byte + received []Datagram + receiveRunning bool + receiveErr error + sendQueue chan udpSend + sendErr error + closeOnce sync.Once +} + +type udpReadWaiter struct { + handle uint64 + ready bool +} + +type udpWriteWaiter struct { + handle uint64 + ready bool +} + +func newDatagramState(connection net.PacketConn) *datagramState { + return &datagramState{ + connection: connection, + sendQueue: make(chan udpSend, udpSendQueueCapacity), + } +} + +func (state *datagramState) closeAfterQueuedSends() { + state.closeOnce.Do(func() { + close(state.sendQueue) + }) +} + +func (state *datagramState) closeNow() { + state.closeOnce.Do(func() { + close(state.sendQueue) + }) + _ = state.connection.Close() +} + +func (reactor *Reactor) runUDPSends(handle uint64, state *datagramState) { + defer reactor.workers.Done() + defer reactor.finishUDPSends(state) + defer state.connection.Close() + + for request := range state.sendQueue { + reactor.mu.Lock() + ready := false + for _, waiter := range reactor.udpWrites { + if waiter.handle == handle { + waiter.ready = true + ready = true + } + } + reactor.mu.Unlock() + if ready { + reactor.signalCompletion() + } + + n, err := state.connection.WriteTo(request.data, request.peer) + if err == nil && n != len(request.data) { + err = fmt.Errorf("short UDP write: %d of %d", n, len(request.data)) + } + if err != nil { + reactor.mu.Lock() + ready := false + if reactor.datagrams[handle] == state { + state.sendErr = err + for _, waiter := range reactor.udpWrites { + if waiter.handle == handle { + waiter.ready = true + ready = true + } + } + } + reactor.mu.Unlock() + if ready { + reactor.signalCompletion() + } + } + } +} + +func (reactor *Reactor) finishUDPSends(state *datagramState) { + reactor.mu.Lock() + delete(reactor.drainingDatagrams, state) + reactor.mu.Unlock() +} + +func (reactor *Reactor) StartUDPReceive(handle, operation uint64) error { + reactor.mu.Lock() + state, exists := reactor.datagrams[handle] + if !exists { + reactor.mu.Unlock() + return ErrInvalid + } + if err := reactor.claimOperationLocked(operation, operationUDPRead); err != nil { + reactor.mu.Unlock() + return err + } + waiter := &udpReadWaiter{ + handle: handle, + ready: len(state.received) > 0 || state.receiveErr != nil, + } + reactor.udpReads[operation] = waiter + startWorker := state.receiveErr == nil && + len(state.received) < udpReceiveQueueCapacity && + !state.receiveRunning + if startWorker { + state.receiveRunning = true + reactor.workers.Add(1) + } + reactor.mu.Unlock() + + if startWorker { + go reactor.runUDPReceive(handle, state) + } + if waiter.ready { + reactor.signalCompletion() + } + return nil +} + +func (reactor *Reactor) runUDPReceive(handle uint64, state *datagramState) { + defer reactor.workers.Done() + if state.receiveBuffer == nil { + state.receiveBuffer = make([]byte, 65535) + } + buffer := state.receiveBuffer + for { + n, peer, err := state.connection.ReadFrom(buffer) + + var udpPeer *net.UDPAddr + if err == nil { + var ok bool + udpPeer, ok = peer.(*net.UDPAddr) + if !ok { + err = fmt.Errorf("unsupported UDP peer address %T", peer) + } + } + + reactor.mu.Lock() + if reactor.datagrams[handle] != state { + reactor.mu.Unlock() + return + } + if err != nil { + state.receiveErr = err + } else { + state.received = append(state.received, Datagram{ + Data: append([]byte(nil), buffer[:n]...), + Peer: cloneUDPAddr(udpPeer), + }) + } + ready := false + for _, waiter := range reactor.udpReads { + if waiter.handle == handle { + waiter.ready = true + ready = true + } + } + continueReceiving := err == nil && + ready && + len(state.received) < udpReceiveQueueCapacity + if !continueReceiving { + state.receiveRunning = false + } + reactor.mu.Unlock() + if ready { + reactor.signalCompletion() + } + if !continueReceiving { + return + } + } +} + +func (reactor *Reactor) TakeUDPReceive(operation uint64, capacity uint32) (Datagram, error) { + reactor.mu.Lock() + waiter, exists := reactor.udpReads[operation] + if !exists || reactor.operations[operation] != operationUDPRead { + reactor.mu.Unlock() + return Datagram{}, ErrInvalid + } + state, exists := reactor.datagrams[waiter.handle] + if !exists { + delete(reactor.udpReads, operation) + reactor.releaseOperationLocked(operation, operationUDPRead) + reactor.mu.Unlock() + return Datagram{}, ErrInvalid + } + if len(state.received) == 0 { + if state.receiveErr == nil { + reactor.mu.Unlock() + return Datagram{}, ErrPending + } + err := state.receiveErr + state.receiveErr = nil + delete(reactor.udpReads, operation) + reactor.releaseOperationLocked(operation, operationUDPRead) + startWorker := len(state.received) < udpReceiveQueueCapacity && + !state.receiveRunning && + reactor.hasUDPReadWaiterLocked(waiter.handle) + if startWorker { + state.receiveRunning = true + reactor.workers.Add(1) + } + reactor.mu.Unlock() + if startWorker { + go reactor.runUDPReceive(waiter.handle, state) + } + return Datagram{}, err + } + + datagram := state.received[0] + state.received[0] = Datagram{} + state.received = state.received[1:] + delete(reactor.udpReads, operation) + reactor.releaseOperationLocked(operation, operationUDPRead) + startWorker := len(state.received) < udpReceiveQueueCapacity && + !state.receiveRunning && + reactor.hasUDPReadWaiterLocked(waiter.handle) + if startWorker { + state.receiveRunning = true + reactor.workers.Add(1) + } + reactor.mu.Unlock() + + if startWorker { + go reactor.runUDPReceive(waiter.handle, state) + } + if uint32(len(datagram.Data)) > capacity { + datagram.Data = datagram.Data[:capacity] + } + return datagram, nil +} + +func (reactor *Reactor) TryUDPSend(handle uint64, data []byte, peer *net.UDPAddr) error { + if peer == nil || len(data) > 65535 { + return ErrInvalid + } + reactor.mu.Lock() + defer reactor.mu.Unlock() + state, exists := reactor.datagrams[handle] + if !exists { + return ErrInvalid + } + if state.sendErr != nil { + return state.sendErr + } + if len(state.sendQueue) >= cap(state.sendQueue) { + return ErrWouldBlock + } + state.sendQueue <- udpSend{ + data: append([]byte(nil), data...), + peer: cloneUDPAddr(peer), + } + return nil +} + +func (reactor *Reactor) StartUDPSendReady(handle, operation uint64) error { + reactor.mu.Lock() + state, exists := reactor.datagrams[handle] + if !exists { + reactor.mu.Unlock() + return ErrInvalid + } + if err := reactor.claimOperationLocked(operation, operationUDPWrite); err != nil { + reactor.mu.Unlock() + return err + } + waiter := &udpWriteWaiter{ + handle: handle, + ready: len(state.sendQueue) < cap(state.sendQueue) || state.sendErr != nil, + } + reactor.udpWrites[operation] = waiter + ready := waiter.ready + reactor.mu.Unlock() + if ready { + reactor.signalCompletion() + } + return nil +} + +func (reactor *Reactor) TakeUDPSendReady(operation uint64) error { + reactor.mu.Lock() + waiter, exists := reactor.udpWrites[operation] + if !exists || reactor.operations[operation] != operationUDPWrite { + reactor.mu.Unlock() + return ErrInvalid + } + if !waiter.ready { + reactor.mu.Unlock() + return ErrPending + } + delete(reactor.udpWrites, operation) + reactor.releaseOperationLocked(operation, operationUDPWrite) + state, exists := reactor.datagrams[waiter.handle] + if !exists { + reactor.mu.Unlock() + return ErrInvalid + } + err := state.sendErr + reactor.mu.Unlock() + return err +} + +func (reactor *Reactor) hasUDPReadWaiterLocked(handle uint64) bool { + for _, waiter := range reactor.udpReads { + if waiter.handle == handle { + return true + } + } + return false +} + +func cloneUDPAddr(address *net.UDPAddr) *net.UDPAddr { + if address == nil { + return nil + } + return &net.UDPAddr{ + IP: append(net.IP(nil), address.IP...), + Port: address.Port, + Zone: address.Zone, + } +} diff --git a/easytier-go/platform/dns.go b/easytier-go/platform/dns.go new file mode 100644 index 00000000..50f9ff9c --- /dev/null +++ b/easytier-go/platform/dns.go @@ -0,0 +1,20 @@ +package platform + +import ( + "context" + "net" + "net/netip" +) + +type DNSQuery struct { + Host string + IPVersion uint8 + SocketMark *uint32 + NetNS *string +} + +type DNSResolver interface { + LookupIP(context.Context, DNSQuery) ([]netip.Addr, error) + LookupTXT(context.Context, DNSQuery) (string, error) + LookupSRV(context.Context, DNSQuery) ([]*net.SRV, error) +} diff --git a/easytier-go/platform/environment.go b/easytier-go/platform/environment.go new file mode 100644 index 00000000..a3da4caf --- /dev/null +++ b/easytier-go/platform/environment.go @@ -0,0 +1,42 @@ +package platform + +import ( + "context" + "net" + "net/netip" +) + +type ConnectorEnvironment interface { + LocalAddrForRemote(context.Context, *net.UDPAddr, SocketContext) (net.Addr, error) +} + +// EnvironmentSnapshot is the host-observed network state captured when an +// EasyTier instance is created. Core remains responsible for all policy +// decisions made from these facts. +// +// TODO: Split static declarations from dynamic network facts and add a +// versioned, event-driven host ABI update path. This snapshot is currently +// frozen at CreateInstance, so long-lived instances do not observe interface, +// address, or preferred-source changes. +type EnvironmentSnapshot struct { + PublicIPv4 *netip.Addr + InterfaceIPv4s []netip.Addr + PublicIPv6 *netip.Addr + InterfaceIPv6s []netip.Addr + MappedListeners []string + LocalIPs []netip.Addr + ProtectedTCPPorts []uint16 + PreferredIPv6Sources []PreferredIPv6Source +} + +type PreferredIPv6Source struct { + IP netip.Addr + IfIndex uint32 +} + +type Services struct { + Sockets SocketFactory + DNS DNSResolver + Environment ConnectorEnvironment + Snapshot EnvironmentSnapshot +} diff --git a/easytier-go/platform/netstd/netstd.go b/easytier-go/platform/netstd/netstd.go new file mode 100644 index 00000000..2df1c6e9 --- /dev/null +++ b/easytier-go/platform/netstd/netstd.go @@ -0,0 +1,198 @@ +package netstd + +import ( + "context" + "fmt" + "net" + "net/netip" + "syscall" + + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +// SocketFactory implements platform.SocketFactory with the Go standard +// library and the socket controls required by EasyTier hole punching. +type SocketFactory struct{} + +func (SocketFactory) ConnectTCP( + ctx context.Context, + options platform.TCPConnectOptions, +) (net.Conn, error) { + if options.Purpose == platform.TCPConnectFake { + return nil, fmt.Errorf("FakeTCP is not supported by netstd") + } + if err := validateTCPBindOptions(options.Bind); err != nil { + return nil, err + } + dialer := net.Dialer{ + LocalAddr: options.Bind.LocalAddr, + Control: tcpControl(options.Bind), + } + connection, err := dialer.DialContext(ctx, "tcp", options.RemoteAddr.String()) + if err != nil { + return nil, err + } + if options.Purpose == platform.TCPConnectSTUNProbe { + if err := connection.(*net.TCPConn).SetLinger(0); err != nil { + _ = connection.Close() + return nil, fmt.Errorf("set TCP STUN probe linger: %w", err) + } + } + return connection, nil +} + +func (SocketFactory) BindUDP( + _ context.Context, + options platform.UDPBindOptions, +) (net.PacketConn, error) { + if options.Context.SocketMark != nil || options.Context.NetNS != nil || + options.BindDevice != nil || options.ReuseAddr || options.ReusePort { + return nil, fmt.Errorf("non-default UDP bind policy is not supported by netstd") + } + return net.ListenUDP(udpNetwork(options), options.LocalAddr) +} + +func (SocketFactory) ListenTCP( + ctx context.Context, + options platform.TCPListenOptions, +) (net.Listener, error) { + if err := validateTCPBindOptions(options.Bind); err != nil { + return nil, err + } + config := net.ListenConfig{Control: tcpControl(options.Bind)} + return config.Listen(ctx, "tcp", options.Bind.LocalAddr.String()) +} + +func udpNetwork(options platform.UDPBindOptions) string { + if options.LocalAddr != nil && options.LocalAddr.IP.To4() != nil { + return "udp4" + } + if options.OnlyV6 { + return "udp6" + } + if options.LocalAddr != nil && len(options.LocalAddr.IP) != 0 { + return "udp" + } + if options.Context.IPVersion == platform.IPVersionV4 { + return "udp4" + } + return "udp" +} + +func validateTCPBindOptions(options platform.TCPBindOptions) error { + if options.Context.SocketMark != nil || options.Context.NetNS != nil || + options.BindDevice != nil { + return fmt.Errorf("non-default TCP bind policy is not supported by netstd") + } + return nil +} + +func tcpControl( + options platform.TCPBindOptions, +) func(string, string, syscall.RawConn) error { + return func(network, _ string, connection syscall.RawConn) error { + var socketErr error + if err := connection.Control(func(descriptor uintptr) { + socketErr = applyTCPSocketOptions(descriptor, network, options) + }); err != nil { + return err + } + return socketErr + } +} + +func boolInt(value bool) int { + if value { + return 1 + } + return 0 +} + +type DNSResolver struct { + Resolver *net.Resolver +} + +func (resolver DNSResolver) configured() *net.Resolver { + if resolver.Resolver != nil { + return resolver.Resolver + } + return net.DefaultResolver +} + +func (resolver DNSResolver) LookupIP( + ctx context.Context, + query platform.DNSQuery, +) ([]netip.Addr, error) { + if address, err := netip.ParseAddr(query.Host); err == nil { + return []netip.Addr{address.Unmap()}, nil + } + network := "ip" + switch query.IPVersion { + case 4: + network = "ip4" + case 6: + network = "ip6" + } + addresses, err := resolver.configured().LookupNetIP(ctx, network, query.Host) + if err != nil { + return nil, err + } + for index := range addresses { + addresses[index] = addresses[index].Unmap() + } + return addresses, nil +} + +func (resolver DNSResolver) LookupTXT( + ctx context.Context, + query platform.DNSQuery, +) (string, error) { + records, err := resolver.configured().LookupTXT(ctx, query.Host) + if err != nil { + return "", err + } + if len(records) == 0 { + return "", fmt.Errorf("DNS TXT query for %q returned no records", query.Host) + } + return records[0], nil +} + +func (resolver DNSResolver) LookupSRV( + ctx context.Context, + query platform.DNSQuery, +) ([]*net.SRV, error) { + _, records, err := resolver.configured().LookupSRV(ctx, "", "", query.Host) + return records, err +} + +type ConnectorEnvironment struct{} + +func (ConnectorEnvironment) LocalAddrForRemote( + ctx context.Context, + remote *net.UDPAddr, + socketContext platform.SocketContext, +) (net.Addr, error) { + if socketContext.SocketMark != nil || socketContext.NetNS != nil { + return nil, fmt.Errorf("non-default connector context is not supported by netstd") + } + network := "udp" + if remote.IP.To4() != nil { + network = "udp4" + } else if remote.IP.To16() != nil { + network = "udp6" + } + connection, err := (&net.Dialer{}).DialContext(ctx, network, remote.String()) + if err != nil { + return nil, err + } + defer connection.Close() + return connection.LocalAddr(), nil +} + +func Services() platform.Services { + return platform.Services{ + Sockets: SocketFactory{}, + DNS: DNSResolver{}, + Environment: ConnectorEnvironment{}, + } +} diff --git a/easytier-go/platform/netstd/netstd_test.go b/easytier-go/platform/netstd/netstd_test.go new file mode 100644 index 00000000..8686d0c7 --- /dev/null +++ b/easytier-go/platform/netstd/netstd_test.go @@ -0,0 +1,109 @@ +package netstd + +import ( + "context" + "net" + "testing" + + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +func TestDNSResolverNormalizesIPv4Literal(t *testing.T) { + addresses, err := (DNSResolver{}).LookupIP( + context.Background(), + platform.DNSQuery{Host: "127.0.0.1"}, + ) + if err != nil { + t.Fatalf("resolve IPv4 literal: %v", err) + } + if len(addresses) != 1 || !addresses[0].Is4() { + t.Fatalf("resolved addresses = %v, want one canonical IPv4", addresses) + } +} + +func TestSocketFactoryReusesTCPSourcePort(t *testing.T) { + firstServer := listenTCP4(t) + defer firstServer.Close() + secondServer := listenTCP4(t) + defer secondServer.Close() + sourcePort := unusedTCP4Port(t) + + reuse := true + bind := platform.TCPBindOptions{ + Context: platform.SocketContext{IPVersion: platform.IPVersionV4}, + LocalAddr: &net.TCPAddr{IP: net.IPv4zero, Port: sourcePort}, + ReuseAddr: &reuse, + ReusePort: true, + OnlyV6: true, + } + first := connectTCP(t, firstServer, bind, platform.TCPConnectSTUNProbe) + defer first.Close() + second := connectTCP(t, secondServer, bind, platform.TCPConnectSTUNProbe) + defer second.Close() + + if first.LocalAddr().(*net.TCPAddr).Port != sourcePort || + second.LocalAddr().(*net.TCPAddr).Port != sourcePort { + t.Fatalf("source ports = %v, %v, want %d", + first.LocalAddr(), second.LocalAddr(), sourcePort) + } +} + +func TestSocketFactoryAcceptsOnlyV6ForIPv4HolePunchListener(t *testing.T) { + listener, err := (SocketFactory{}).ListenTCP( + context.Background(), + platform.TCPListenOptions{ + Bind: platform.TCPBindOptions{ + Context: platform.SocketContext{IPVersion: platform.IPVersionV4}, + LocalAddr: &net.TCPAddr{IP: net.IPv4zero}, + OnlyV6: true, + }, + Purpose: platform.TCPListenHolePunch, + }, + ) + if err != nil { + t.Fatalf("listen for IPv4 TCP hole punch: %v", err) + } + if err := listener.Close(); err != nil { + t.Fatalf("close TCP hole punch listener: %v", err) + } +} + +func listenTCP4(t *testing.T) net.Listener { + t.Helper() + listener, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen on loopback: %v", err) + } + return listener +} + +func unusedTCP4Port(t *testing.T) int { + t.Helper() + listener := listenTCP4(t) + port := listener.Addr().(*net.TCPAddr).Port + if err := listener.Close(); err != nil { + t.Fatalf("release source port: %v", err) + } + return port +} + +func connectTCP( + t *testing.T, + server net.Listener, + bind platform.TCPBindOptions, + purpose platform.TCPConnectPurpose, +) net.Conn { + t.Helper() + connection, err := (SocketFactory{}).ConnectTCP( + context.Background(), + platform.TCPConnectOptions{ + RemoteAddr: server.Addr().(*net.TCPAddr), + Bind: bind, + Purpose: purpose, + }, + ) + if err != nil { + t.Fatalf("connect TCP socket: %v", err) + } + return connection +} diff --git a/easytier-go/platform/netstd/socket_options_other.go b/easytier-go/platform/netstd/socket_options_other.go new file mode 100644 index 00000000..fe97683c --- /dev/null +++ b/easytier-go/platform/netstd/socket_options_other.go @@ -0,0 +1,21 @@ +//go:build !aix && !android && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !windows + +package netstd + +import ( + "fmt" + "runtime" + + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +func applyTCPSocketOptions( + _ uintptr, + _ string, + options platform.TCPBindOptions, +) error { + if options.ReuseAddr == nil && !options.ReusePort && !options.OnlyV6 { + return nil + } + return fmt.Errorf("TCP socket options are not supported on %s", runtime.GOOS) +} diff --git a/easytier-go/platform/netstd/socket_options_unix.go b/easytier-go/platform/netstd/socket_options_unix.go new file mode 100644 index 00000000..edde12f2 --- /dev/null +++ b/easytier-go/platform/netstd/socket_options_unix.go @@ -0,0 +1,36 @@ +//go:build aix || android || darwin || dragonfly || freebsd || linux || netbsd || openbsd + +package netstd + +import ( + "fmt" + + "github.com/EasyTier/EasyTier/easytier-go/platform" + "golang.org/x/sys/unix" +) + +func applyTCPSocketOptions( + descriptor uintptr, + network string, + options platform.TCPBindOptions, +) error { + fd := int(descriptor) + reuseAddr := true + if options.ReuseAddr != nil { + reuseAddr = *options.ReuseAddr + } + if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, boolInt(reuseAddr)); err != nil { + return fmt.Errorf("set TCP SO_REUSEADDR: %w", err) + } + if options.ReusePort { + if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { + return fmt.Errorf("set TCP SO_REUSEPORT: %w", err) + } + } + if options.OnlyV6 && network == "tcp6" { + if err := unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_V6ONLY, 1); err != nil { + return fmt.Errorf("set TCP IPV6_V6ONLY: %w", err) + } + } + return nil +} diff --git a/easytier-go/platform/netstd/socket_options_unix_test.go b/easytier-go/platform/netstd/socket_options_unix_test.go new file mode 100644 index 00000000..fa04a800 --- /dev/null +++ b/easytier-go/platform/netstd/socket_options_unix_test.go @@ -0,0 +1,33 @@ +//go:build aix || android || darwin || dragonfly || freebsd || linux || netbsd || openbsd + +package netstd + +import ( + "net" + "testing" + + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +func TestSocketFactoryUsesNativeTCPReuseAddrDefault(t *testing.T) { + firstServer := listenTCP4(t) + defer firstServer.Close() + secondServer := listenTCP4(t) + defer secondServer.Close() + sourcePort := unusedTCP4Port(t) + + reuse := true + first := connectTCP(t, firstServer, platform.TCPBindOptions{ + Context: platform.SocketContext{IPVersion: platform.IPVersionV4}, + LocalAddr: &net.TCPAddr{IP: net.IPv4zero, Port: sourcePort}, + ReuseAddr: &reuse, + }, platform.TCPConnectSTUNProbe) + defer first.Close() + + second := connectTCP(t, secondServer, platform.TCPBindOptions{ + Context: platform.SocketContext{IPVersion: platform.IPVersionV4}, + LocalAddr: &net.TCPAddr{IP: net.IPv4zero, Port: sourcePort}, + OnlyV6: true, + }, platform.TCPConnectHolePunch) + defer second.Close() +} diff --git a/easytier-go/platform/netstd/socket_options_windows.go b/easytier-go/platform/netstd/socket_options_windows.go new file mode 100644 index 00000000..68a886a6 --- /dev/null +++ b/easytier-go/platform/netstd/socket_options_windows.go @@ -0,0 +1,38 @@ +//go:build windows + +package netstd + +import ( + "fmt" + + "github.com/EasyTier/EasyTier/easytier-go/platform" + "golang.org/x/sys/windows" +) + +func applyTCPSocketOptions( + descriptor uintptr, + network string, + options platform.TCPBindOptions, +) error { + reuseAddr := options.ReuseAddr + if options.ReusePort { + if reuseAddr != nil && !*reuseAddr { + return fmt.Errorf("TCP reuse_port requires reuse_addr on Windows") + } + enabled := true + reuseAddr = &enabled + } + if reuseAddr != nil { + if err := windows.SetsockoptInt(windows.Handle(descriptor), + windows.SOL_SOCKET, windows.SO_REUSEADDR, boolInt(*reuseAddr)); err != nil { + return fmt.Errorf("set TCP SO_REUSEADDR: %w", err) + } + } + if options.OnlyV6 && network == "tcp6" { + if err := windows.SetsockoptInt(windows.Handle(descriptor), + windows.IPPROTO_IPV6, windows.IPV6_V6ONLY, 1); err != nil { + return fmt.Errorf("set TCP IPV6_V6ONLY: %w", err) + } + } + return nil +} diff --git a/easytier-go/platform/socket.go b/easytier-go/platform/socket.go new file mode 100644 index 00000000..c22127a2 --- /dev/null +++ b/easytier-go/platform/socket.go @@ -0,0 +1,98 @@ +package platform + +import ( + "context" + "net" +) + +type TCPConnectPurpose uint8 + +const ( + TCPConnectDirect TCPConnectPurpose = iota + TCPConnectFake + TCPConnectHolePunch + TCPConnectManual + TCPConnectProxyNAT + TCPConnectSTUNProbe + TCPConnectSocks5 + TCPConnectPortForward + TCPConnectDataPlane +) + +type UDPBindPurpose uint8 + +const ( + UDPBindHolePunchControl UDPBindPurpose = iota + UDPBindHolePunchCandidate + UDPBindDirect + UDPBindPortBoundListener + UDPBindProxyNAT + UDPBindSTUNProbe + UDPBindSocks5 + UDPBindPortForward + UDPBindPortLease +) + +type TCPListenPurpose uint8 + +const ( + TCPListenDirect TCPListenPurpose = iota + TCPListenHolePunch + TCPListenManual + TCPListenProxyNAT + TCPListenSocks5 + TCPListenPortForward + TCPListenPortLease +) + +type IPVersion uint8 + +const ( + IPVersionV4 IPVersion = iota + IPVersionV6 + IPVersionBoth +) + +type SocketContext struct { + IPVersion IPVersion + SocketMark *uint32 + NetNS *string +} + +type TCPBindOptions struct { + Context SocketContext + LocalAddr *net.TCPAddr + BindDevice *string + ReuseAddr *bool + ReusePort bool + OnlyV6 bool +} + +type TCPConnectOptions struct { + RemoteAddr *net.TCPAddr + Bind TCPBindOptions + Purpose TCPConnectPurpose +} + +type UDPBindOptions struct { + Context SocketContext + LocalAddr *net.UDPAddr + BindDevice *string + ReuseAddr bool + ReusePort bool + OnlyV6 bool + Purpose UDPBindPurpose +} + +type TCPListenOptions struct { + Bind TCPBindOptions + Purpose TCPListenPurpose +} + +// SocketFactory creates sockets authorized for one EasyTier instance. +// The host runtime owns every resource returned after a successful call. +type SocketFactory interface { + ConnectTCP(context.Context, TCPConnectOptions) (net.Conn, error) + BindUDP(context.Context, UDPBindOptions) (net.PacketConn, error) + ListenTCP(context.Context, TCPListenOptions) (net.Listener, error) +} diff --git a/easytier-go/proto/acl/acl.pb.go b/easytier-go/proto/acl/acl.pb.go new file mode 100644 index 00000000..4a3ef976 --- /dev/null +++ b/easytier-go/proto/acl/acl.pb.go @@ -0,0 +1,1216 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.35.1 +// source: acl.proto + +package acl + +import ( + common "github.com/EasyTier/EasyTier/easytier-go/proto/common" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Enhanced protocol enum with more granular options +type Protocol int32 + +const ( + Protocol_Unspecified Protocol = 0 + Protocol_TCP Protocol = 1 + Protocol_UDP Protocol = 2 + Protocol_ICMP Protocol = 3 + Protocol_ICMPv6 Protocol = 4 + Protocol_Any Protocol = 5 +) + +// Enum value maps for Protocol. +var ( + Protocol_name = map[int32]string{ + 0: "Unspecified", + 1: "TCP", + 2: "UDP", + 3: "ICMP", + 4: "ICMPv6", + 5: "Any", + } + Protocol_value = map[string]int32{ + "Unspecified": 0, + "TCP": 1, + "UDP": 2, + "ICMP": 3, + "ICMPv6": 4, + "Any": 5, + } +) + +func (x Protocol) Enum() *Protocol { + p := new(Protocol) + *p = x + return p +} + +func (x Protocol) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Protocol) Descriptor() protoreflect.EnumDescriptor { + return file_acl_proto_enumTypes[0].Descriptor() +} + +func (Protocol) Type() protoreflect.EnumType { + return &file_acl_proto_enumTypes[0] +} + +func (x Protocol) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Protocol.Descriptor instead. +func (Protocol) EnumDescriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{0} +} + +type Action int32 + +const ( + Action_Noop Action = 0 + Action_Allow Action = 1 + Action_Drop Action = 2 // Silent drop (no response) +) + +// Enum value maps for Action. +var ( + Action_name = map[int32]string{ + 0: "Noop", + 1: "Allow", + 2: "Drop", + } + Action_value = map[string]int32{ + "Noop": 0, + "Allow": 1, + "Drop": 2, + } +) + +func (x Action) Enum() *Action { + p := new(Action) + *p = x + return p +} + +func (x Action) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Action) Descriptor() protoreflect.EnumDescriptor { + return file_acl_proto_enumTypes[1].Descriptor() +} + +func (Action) Type() protoreflect.EnumType { + return &file_acl_proto_enumTypes[1] +} + +func (x Action) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Action.Descriptor instead. +func (Action) EnumDescriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{1} +} + +type ChainType int32 + +const ( + ChainType_UnspecifiedChain ChainType = 0 + // send to this node + ChainType_Inbound ChainType = 1 + // send from this node + ChainType_Outbound ChainType = 2 + // subnet proxy + ChainType_Forward ChainType = 3 +) + +// Enum value maps for ChainType. +var ( + ChainType_name = map[int32]string{ + 0: "UnspecifiedChain", + 1: "Inbound", + 2: "Outbound", + 3: "Forward", + } + ChainType_value = map[string]int32{ + "UnspecifiedChain": 0, + "Inbound": 1, + "Outbound": 2, + "Forward": 3, + } +) + +func (x ChainType) Enum() *ChainType { + p := new(ChainType) + *p = x + return p +} + +func (x ChainType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ChainType) Descriptor() protoreflect.EnumDescriptor { + return file_acl_proto_enumTypes[2].Descriptor() +} + +func (ChainType) Type() protoreflect.EnumType { + return &file_acl_proto_enumTypes[2] +} + +func (x ChainType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ChainType.Descriptor instead. +func (ChainType) EnumDescriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{2} +} + +type ConnState int32 + +const ( + ConnState_New ConnState = 0 + ConnState_Established ConnState = 1 + ConnState_Related ConnState = 2 + ConnState_Invalid ConnState = 3 +) + +// Enum value maps for ConnState. +var ( + ConnState_name = map[int32]string{ + 0: "New", + 1: "Established", + 2: "Related", + 3: "Invalid", + } + ConnState_value = map[string]int32{ + "New": 0, + "Established": 1, + "Related": 2, + "Invalid": 3, + } +) + +func (x ConnState) Enum() *ConnState { + p := new(ConnState) + *p = x + return p +} + +func (x ConnState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConnState) Descriptor() protoreflect.EnumDescriptor { + return file_acl_proto_enumTypes[3].Descriptor() +} + +func (ConnState) Type() protoreflect.EnumType { + return &file_acl_proto_enumTypes[3] +} + +func (x ConnState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConnState.Descriptor instead. +func (ConnState) EnumDescriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{3} +} + +// Time-based access control +type TimeWindow struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Days of week: 0=Sunday, 1=Monday, ..., 6=Saturday + DaysOfWeek []uint32 `protobuf:"varint,1,rep,packed,name=days_of_week,json=daysOfWeek,proto3" json:"days_of_week,omitempty"` + // Time in minutes from midnight (0-1439) + StartTime uint32 `protobuf:"varint,2,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + EndTime uint32 `protobuf:"varint,3,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` + // Timezone offset in minutes from UTC + TimezoneOffset int32 `protobuf:"varint,4,opt,name=timezone_offset,json=timezoneOffset,proto3" json:"timezone_offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TimeWindow) Reset() { + *x = TimeWindow{} + mi := &file_acl_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TimeWindow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TimeWindow) ProtoMessage() {} + +func (x *TimeWindow) ProtoReflect() protoreflect.Message { + mi := &file_acl_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TimeWindow.ProtoReflect.Descriptor instead. +func (*TimeWindow) Descriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{0} +} + +func (x *TimeWindow) GetDaysOfWeek() []uint32 { + if x != nil { + return x.DaysOfWeek + } + return nil +} + +func (x *TimeWindow) GetStartTime() uint32 { + if x != nil { + return x.StartTime + } + return 0 +} + +func (x *TimeWindow) GetEndTime() uint32 { + if x != nil { + return x.EndTime + } + return 0 +} + +func (x *TimeWindow) GetTimezoneOffset() int32 { + if x != nil { + return x.TimezoneOffset + } + return 0 +} + +// Enhanced rule with priority and metadata +type Rule struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Rule identification and metadata + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Human-readable rule name + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` // Rule description + Priority uint32 `protobuf:"varint,3,opt,name=priority,proto3" json:"priority,omitempty"` // Higher number = higher priority (0-65535) + Enabled bool `protobuf:"varint,4,opt,name=enabled,proto3" json:"enabled,omitempty"` // Rule enabled/disabled state + // Core matching criteria + Protocol Protocol `protobuf:"varint,5,opt,name=protocol,proto3,enum=acl.Protocol" json:"protocol,omitempty"` + Ports []string `protobuf:"bytes,6,rep,name=ports,proto3" json:"ports,omitempty"` + SourceIps []string `protobuf:"bytes,7,rep,name=source_ips,json=sourceIps,proto3" json:"source_ips,omitempty"` // Source IP ranges + DestinationIps []string `protobuf:"bytes,8,rep,name=destination_ips,json=destinationIps,proto3" json:"destination_ips,omitempty"` // Destination IP ranges + // Enhanced matching criteria + SourcePorts []string `protobuf:"bytes,9,rep,name=source_ports,json=sourcePorts,proto3" json:"source_ports,omitempty"` // Source port range + // Action and logging + Action Action `protobuf:"varint,10,opt,name=action,proto3,enum=acl.Action" json:"action,omitempty"` + // Rate limiting (packets per second) + RateLimit uint32 `protobuf:"varint,11,opt,name=rate_limit,json=rateLimit,proto3" json:"rate_limit,omitempty"` // 0 = no limit + BurstLimit uint32 `protobuf:"varint,12,opt,name=burst_limit,json=burstLimit,proto3" json:"burst_limit,omitempty"` // Burst allowance + // Connection tracking + Stateful bool `protobuf:"varint,13,opt,name=stateful,proto3" json:"stateful,omitempty"` // Enable connection tracking + // Group matching criteria + SourceGroups []string `protobuf:"bytes,14,rep,name=source_groups,json=sourceGroups,proto3" json:"source_groups,omitempty"` + DestinationGroups []string `protobuf:"bytes,15,rep,name=destination_groups,json=destinationGroups,proto3" json:"destination_groups,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Rule) Reset() { + *x = Rule{} + mi := &file_acl_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Rule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Rule) ProtoMessage() {} + +func (x *Rule) ProtoReflect() protoreflect.Message { + mi := &file_acl_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Rule.ProtoReflect.Descriptor instead. +func (*Rule) Descriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{1} +} + +func (x *Rule) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Rule) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Rule) GetPriority() uint32 { + if x != nil { + return x.Priority + } + return 0 +} + +func (x *Rule) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *Rule) GetProtocol() Protocol { + if x != nil { + return x.Protocol + } + return Protocol_Unspecified +} + +func (x *Rule) GetPorts() []string { + if x != nil { + return x.Ports + } + return nil +} + +func (x *Rule) GetSourceIps() []string { + if x != nil { + return x.SourceIps + } + return nil +} + +func (x *Rule) GetDestinationIps() []string { + if x != nil { + return x.DestinationIps + } + return nil +} + +func (x *Rule) GetSourcePorts() []string { + if x != nil { + return x.SourcePorts + } + return nil +} + +func (x *Rule) GetAction() Action { + if x != nil { + return x.Action + } + return Action_Noop +} + +func (x *Rule) GetRateLimit() uint32 { + if x != nil { + return x.RateLimit + } + return 0 +} + +func (x *Rule) GetBurstLimit() uint32 { + if x != nil { + return x.BurstLimit + } + return 0 +} + +func (x *Rule) GetStateful() bool { + if x != nil { + return x.Stateful + } + return false +} + +func (x *Rule) GetSourceGroups() []string { + if x != nil { + return x.SourceGroups + } + return nil +} + +func (x *Rule) GetDestinationGroups() []string { + if x != nil { + return x.DestinationGroups + } + return nil +} + +// Rule chain with metadata and optimization hints +type Chain struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Chain identification + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Human-readable chain name + ChainType ChainType `protobuf:"varint,2,opt,name=chain_type,json=chainType,proto3,enum=acl.ChainType" json:"chain_type,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` // Chain description + Enabled bool `protobuf:"varint,4,opt,name=enabled,proto3" json:"enabled,omitempty"` // Chain enabled/disabled state + // Rules in priority order (highest priority first) + Rules []*Rule `protobuf:"bytes,5,rep,name=rules,proto3" json:"rules,omitempty"` + // Default action when no rules match + DefaultAction Action `protobuf:"varint,6,opt,name=default_action,json=defaultAction,proto3,enum=acl.Action" json:"default_action,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Chain) Reset() { + *x = Chain{} + mi := &file_acl_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Chain) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Chain) ProtoMessage() {} + +func (x *Chain) ProtoReflect() protoreflect.Message { + mi := &file_acl_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Chain.ProtoReflect.Descriptor instead. +func (*Chain) Descriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{2} +} + +func (x *Chain) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Chain) GetChainType() ChainType { + if x != nil { + return x.ChainType + } + return ChainType_UnspecifiedChain +} + +func (x *Chain) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Chain) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *Chain) GetRules() []*Rule { + if x != nil { + return x.Rules + } + return nil +} + +func (x *Chain) GetDefaultAction() Action { + if x != nil { + return x.DefaultAction + } + return Action_Noop +} + +type GroupInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Declares []*GroupIdentity `protobuf:"bytes,1,rep,name=declares,proto3" json:"declares,omitempty"` + Members []string `protobuf:"bytes,2,rep,name=members,proto3" json:"members,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GroupInfo) Reset() { + *x = GroupInfo{} + mi := &file_acl_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GroupInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GroupInfo) ProtoMessage() {} + +func (x *GroupInfo) ProtoReflect() protoreflect.Message { + mi := &file_acl_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GroupInfo.ProtoReflect.Descriptor instead. +func (*GroupInfo) Descriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{3} +} + +func (x *GroupInfo) GetDeclares() []*GroupIdentity { + if x != nil { + return x.Declares + } + return nil +} + +func (x *GroupInfo) GetMembers() []string { + if x != nil { + return x.Members + } + return nil +} + +type GroupIdentity struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupName string `protobuf:"bytes,1,opt,name=group_name,json=groupName,proto3" json:"group_name,omitempty"` + GroupSecret string `protobuf:"bytes,2,opt,name=group_secret,json=groupSecret,proto3" json:"group_secret,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GroupIdentity) Reset() { + *x = GroupIdentity{} + mi := &file_acl_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GroupIdentity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GroupIdentity) ProtoMessage() {} + +func (x *GroupIdentity) ProtoReflect() protoreflect.Message { + mi := &file_acl_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GroupIdentity.ProtoReflect.Descriptor instead. +func (*GroupIdentity) Descriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{4} +} + +func (x *GroupIdentity) GetGroupName() string { + if x != nil { + return x.GroupName + } + return "" +} + +func (x *GroupIdentity) GetGroupSecret() string { + if x != nil { + return x.GroupSecret + } + return "" +} + +type AclV1 struct { + state protoimpl.MessageState `protogen:"open.v1"` + Chains []*Chain `protobuf:"bytes,1,rep,name=chains,proto3" json:"chains,omitempty"` + Group *GroupInfo `protobuf:"bytes,2,opt,name=group,proto3" json:"group,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AclV1) Reset() { + *x = AclV1{} + mi := &file_acl_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AclV1) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AclV1) ProtoMessage() {} + +func (x *AclV1) ProtoReflect() protoreflect.Message { + mi := &file_acl_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AclV1.ProtoReflect.Descriptor instead. +func (*AclV1) Descriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{5} +} + +func (x *AclV1) GetChains() []*Chain { + if x != nil { + return x.Chains + } + return nil +} + +func (x *AclV1) GetGroup() *GroupInfo { + if x != nil { + return x.Group + } + return nil +} + +// Connection tracking entry for stateful ACLs +type ConnTrackEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + SrcAddr *common.SocketAddr `protobuf:"bytes,1,opt,name=src_addr,json=srcAddr,proto3" json:"src_addr,omitempty"` + DstAddr *common.SocketAddr `protobuf:"bytes,2,opt,name=dst_addr,json=dstAddr,proto3" json:"dst_addr,omitempty"` + Protocol Protocol `protobuf:"varint,3,opt,name=protocol,proto3,enum=acl.Protocol" json:"protocol,omitempty"` // IP protocol number (e.g., 6 = TCP, 17 = UDP) + State ConnState `protobuf:"varint,4,opt,name=state,proto3,enum=acl.ConnState" json:"state,omitempty"` + CreatedAt uint64 `protobuf:"varint,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // Unix timestamp (seconds) + LastSeen uint64 `protobuf:"varint,6,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"` // Unix timestamp (seconds) + PacketCount uint64 `protobuf:"varint,7,opt,name=packet_count,json=packetCount,proto3" json:"packet_count,omitempty"` + ByteCount uint64 `protobuf:"varint,8,opt,name=byte_count,json=byteCount,proto3" json:"byte_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConnTrackEntry) Reset() { + *x = ConnTrackEntry{} + mi := &file_acl_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConnTrackEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnTrackEntry) ProtoMessage() {} + +func (x *ConnTrackEntry) ProtoReflect() protoreflect.Message { + mi := &file_acl_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnTrackEntry.ProtoReflect.Descriptor instead. +func (*ConnTrackEntry) Descriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{6} +} + +func (x *ConnTrackEntry) GetSrcAddr() *common.SocketAddr { + if x != nil { + return x.SrcAddr + } + return nil +} + +func (x *ConnTrackEntry) GetDstAddr() *common.SocketAddr { + if x != nil { + return x.DstAddr + } + return nil +} + +func (x *ConnTrackEntry) GetProtocol() Protocol { + if x != nil { + return x.Protocol + } + return Protocol_Unspecified +} + +func (x *ConnTrackEntry) GetState() ConnState { + if x != nil { + return x.State + } + return ConnState_New +} + +func (x *ConnTrackEntry) GetCreatedAt() uint64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *ConnTrackEntry) GetLastSeen() uint64 { + if x != nil { + return x.LastSeen + } + return 0 +} + +func (x *ConnTrackEntry) GetPacketCount() uint64 { + if x != nil { + return x.PacketCount + } + return 0 +} + +func (x *ConnTrackEntry) GetByteCount() uint64 { + if x != nil { + return x.ByteCount + } + return 0 +} + +// Top-level ACL configuration +type Acl struct { + state protoimpl.MessageState `protogen:"open.v1"` + AclV1 *AclV1 `protobuf:"bytes,2,opt,name=acl_v1,json=aclV1,proto3" json:"acl_v1,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Acl) Reset() { + *x = Acl{} + mi := &file_acl_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Acl) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Acl) ProtoMessage() {} + +func (x *Acl) ProtoReflect() protoreflect.Message { + mi := &file_acl_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Acl.ProtoReflect.Descriptor instead. +func (*Acl) Descriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{7} +} + +func (x *Acl) GetAclV1() *AclV1 { + if x != nil { + return x.AclV1 + } + return nil +} + +type StatItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + PacketCount uint64 `protobuf:"varint,1,opt,name=packet_count,json=packetCount,proto3" json:"packet_count,omitempty"` + ByteCount uint64 `protobuf:"varint,2,opt,name=byte_count,json=byteCount,proto3" json:"byte_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StatItem) Reset() { + *x = StatItem{} + mi := &file_acl_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StatItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatItem) ProtoMessage() {} + +func (x *StatItem) ProtoReflect() protoreflect.Message { + mi := &file_acl_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatItem.ProtoReflect.Descriptor instead. +func (*StatItem) Descriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{8} +} + +func (x *StatItem) GetPacketCount() uint64 { + if x != nil { + return x.PacketCount + } + return 0 +} + +func (x *StatItem) GetByteCount() uint64 { + if x != nil { + return x.ByteCount + } + return 0 +} + +type RuleStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rule *Rule `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` + Stat *StatItem `protobuf:"bytes,2,opt,name=stat,proto3" json:"stat,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RuleStats) Reset() { + *x = RuleStats{} + mi := &file_acl_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RuleStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RuleStats) ProtoMessage() {} + +func (x *RuleStats) ProtoReflect() protoreflect.Message { + mi := &file_acl_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RuleStats.ProtoReflect.Descriptor instead. +func (*RuleStats) Descriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{9} +} + +func (x *RuleStats) GetRule() *Rule { + if x != nil { + return x.Rule + } + return nil +} + +func (x *RuleStats) GetStat() *StatItem { + if x != nil { + return x.Stat + } + return nil +} + +type AclStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rules []*RuleStats `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"` + ConnTrack []*ConnTrackEntry `protobuf:"bytes,2,rep,name=conn_track,json=connTrack,proto3" json:"conn_track,omitempty"` + Global map[string]uint64 `protobuf:"bytes,3,rep,name=global,proto3" json:"global,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AclStats) Reset() { + *x = AclStats{} + mi := &file_acl_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AclStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AclStats) ProtoMessage() {} + +func (x *AclStats) ProtoReflect() protoreflect.Message { + mi := &file_acl_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AclStats.ProtoReflect.Descriptor instead. +func (*AclStats) Descriptor() ([]byte, []int) { + return file_acl_proto_rawDescGZIP(), []int{10} +} + +func (x *AclStats) GetRules() []*RuleStats { + if x != nil { + return x.Rules + } + return nil +} + +func (x *AclStats) GetConnTrack() []*ConnTrackEntry { + if x != nil { + return x.ConnTrack + } + return nil +} + +func (x *AclStats) GetGlobal() map[string]uint64 { + if x != nil { + return x.Global + } + return nil +} + +var File_acl_proto protoreflect.FileDescriptor + +const file_acl_proto_rawDesc = "" + + "\n" + + "\tacl.proto\x12\x03acl\x1a\fcommon.proto\"\x91\x01\n" + + "\n" + + "TimeWindow\x12 \n" + + "\fdays_of_week\x18\x01 \x03(\rR\n" + + "daysOfWeek\x12\x1d\n" + + "\n" + + "start_time\x18\x02 \x01(\rR\tstartTime\x12\x19\n" + + "\bend_time\x18\x03 \x01(\rR\aendTime\x12'\n" + + "\x0ftimezone_offset\x18\x04 \x01(\x05R\x0etimezoneOffset\"\xf3\x03\n" + + "\x04Rule\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x1a\n" + + "\bpriority\x18\x03 \x01(\rR\bpriority\x12\x18\n" + + "\aenabled\x18\x04 \x01(\bR\aenabled\x12)\n" + + "\bprotocol\x18\x05 \x01(\x0e2\r.acl.ProtocolR\bprotocol\x12\x14\n" + + "\x05ports\x18\x06 \x03(\tR\x05ports\x12\x1d\n" + + "\n" + + "source_ips\x18\a \x03(\tR\tsourceIps\x12'\n" + + "\x0fdestination_ips\x18\b \x03(\tR\x0edestinationIps\x12!\n" + + "\fsource_ports\x18\t \x03(\tR\vsourcePorts\x12#\n" + + "\x06action\x18\n" + + " \x01(\x0e2\v.acl.ActionR\x06action\x12\x1d\n" + + "\n" + + "rate_limit\x18\v \x01(\rR\trateLimit\x12\x1f\n" + + "\vburst_limit\x18\f \x01(\rR\n" + + "burstLimit\x12\x1a\n" + + "\bstateful\x18\r \x01(\bR\bstateful\x12#\n" + + "\rsource_groups\x18\x0e \x03(\tR\fsourceGroups\x12-\n" + + "\x12destination_groups\x18\x0f \x03(\tR\x11destinationGroups\"\xdb\x01\n" + + "\x05Chain\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12-\n" + + "\n" + + "chain_type\x18\x02 \x01(\x0e2\x0e.acl.ChainTypeR\tchainType\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x18\n" + + "\aenabled\x18\x04 \x01(\bR\aenabled\x12\x1f\n" + + "\x05rules\x18\x05 \x03(\v2\t.acl.RuleR\x05rules\x122\n" + + "\x0edefault_action\x18\x06 \x01(\x0e2\v.acl.ActionR\rdefaultAction\"U\n" + + "\tGroupInfo\x12.\n" + + "\bdeclares\x18\x01 \x03(\v2\x12.acl.GroupIdentityR\bdeclares\x12\x18\n" + + "\amembers\x18\x02 \x03(\tR\amembers\"Q\n" + + "\rGroupIdentity\x12\x1d\n" + + "\n" + + "group_name\x18\x01 \x01(\tR\tgroupName\x12!\n" + + "\fgroup_secret\x18\x02 \x01(\tR\vgroupSecret\"Q\n" + + "\x05AclV1\x12\"\n" + + "\x06chains\x18\x01 \x03(\v2\n" + + ".acl.ChainR\x06chains\x12$\n" + + "\x05group\x18\x02 \x01(\v2\x0e.acl.GroupInfoR\x05group\"\xbd\x02\n" + + "\x0eConnTrackEntry\x12-\n" + + "\bsrc_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\asrcAddr\x12-\n" + + "\bdst_addr\x18\x02 \x01(\v2\x12.common.SocketAddrR\adstAddr\x12)\n" + + "\bprotocol\x18\x03 \x01(\x0e2\r.acl.ProtocolR\bprotocol\x12$\n" + + "\x05state\x18\x04 \x01(\x0e2\x0e.acl.ConnStateR\x05state\x12\x1d\n" + + "\n" + + "created_at\x18\x05 \x01(\x04R\tcreatedAt\x12\x1b\n" + + "\tlast_seen\x18\x06 \x01(\x04R\blastSeen\x12!\n" + + "\fpacket_count\x18\a \x01(\x04R\vpacketCount\x12\x1d\n" + + "\n" + + "byte_count\x18\b \x01(\x04R\tbyteCount\"(\n" + + "\x03Acl\x12!\n" + + "\x06acl_v1\x18\x02 \x01(\v2\n" + + ".acl.AclV1R\x05aclV1\"L\n" + + "\bStatItem\x12!\n" + + "\fpacket_count\x18\x01 \x01(\x04R\vpacketCount\x12\x1d\n" + + "\n" + + "byte_count\x18\x02 \x01(\x04R\tbyteCount\"M\n" + + "\tRuleStats\x12\x1d\n" + + "\x04rule\x18\x01 \x01(\v2\t.acl.RuleR\x04rule\x12!\n" + + "\x04stat\x18\x02 \x01(\v2\r.acl.StatItemR\x04stat\"\xd2\x01\n" + + "\bAclStats\x12$\n" + + "\x05rules\x18\x01 \x03(\v2\x0e.acl.RuleStatsR\x05rules\x122\n" + + "\n" + + "conn_track\x18\x02 \x03(\v2\x13.acl.ConnTrackEntryR\tconnTrack\x121\n" + + "\x06global\x18\x03 \x03(\v2\x19.acl.AclStats.GlobalEntryR\x06global\x1a9\n" + + "\vGlobalEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\x04R\x05value:\x028\x01*L\n" + + "\bProtocol\x12\x0f\n" + + "\vUnspecified\x10\x00\x12\a\n" + + "\x03TCP\x10\x01\x12\a\n" + + "\x03UDP\x10\x02\x12\b\n" + + "\x04ICMP\x10\x03\x12\n" + + "\n" + + "\x06ICMPv6\x10\x04\x12\a\n" + + "\x03Any\x10\x05*'\n" + + "\x06Action\x12\b\n" + + "\x04Noop\x10\x00\x12\t\n" + + "\x05Allow\x10\x01\x12\b\n" + + "\x04Drop\x10\x02*I\n" + + "\tChainType\x12\x14\n" + + "\x10UnspecifiedChain\x10\x00\x12\v\n" + + "\aInbound\x10\x01\x12\f\n" + + "\bOutbound\x10\x02\x12\v\n" + + "\aForward\x10\x03*?\n" + + "\tConnState\x12\a\n" + + "\x03New\x10\x00\x12\x0f\n" + + "\vEstablished\x10\x01\x12\v\n" + + "\aRelated\x10\x02\x12\v\n" + + "\aInvalid\x10\x03b\x06proto3" + +var ( + file_acl_proto_rawDescOnce sync.Once + file_acl_proto_rawDescData []byte +) + +func file_acl_proto_rawDescGZIP() []byte { + file_acl_proto_rawDescOnce.Do(func() { + file_acl_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_acl_proto_rawDesc), len(file_acl_proto_rawDesc))) + }) + return file_acl_proto_rawDescData +} + +var file_acl_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_acl_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_acl_proto_goTypes = []any{ + (Protocol)(0), // 0: acl.Protocol + (Action)(0), // 1: acl.Action + (ChainType)(0), // 2: acl.ChainType + (ConnState)(0), // 3: acl.ConnState + (*TimeWindow)(nil), // 4: acl.TimeWindow + (*Rule)(nil), // 5: acl.Rule + (*Chain)(nil), // 6: acl.Chain + (*GroupInfo)(nil), // 7: acl.GroupInfo + (*GroupIdentity)(nil), // 8: acl.GroupIdentity + (*AclV1)(nil), // 9: acl.AclV1 + (*ConnTrackEntry)(nil), // 10: acl.ConnTrackEntry + (*Acl)(nil), // 11: acl.Acl + (*StatItem)(nil), // 12: acl.StatItem + (*RuleStats)(nil), // 13: acl.RuleStats + (*AclStats)(nil), // 14: acl.AclStats + nil, // 15: acl.AclStats.GlobalEntry + (*common.SocketAddr)(nil), // 16: common.SocketAddr +} +var file_acl_proto_depIdxs = []int32{ + 0, // 0: acl.Rule.protocol:type_name -> acl.Protocol + 1, // 1: acl.Rule.action:type_name -> acl.Action + 2, // 2: acl.Chain.chain_type:type_name -> acl.ChainType + 5, // 3: acl.Chain.rules:type_name -> acl.Rule + 1, // 4: acl.Chain.default_action:type_name -> acl.Action + 8, // 5: acl.GroupInfo.declares:type_name -> acl.GroupIdentity + 6, // 6: acl.AclV1.chains:type_name -> acl.Chain + 7, // 7: acl.AclV1.group:type_name -> acl.GroupInfo + 16, // 8: acl.ConnTrackEntry.src_addr:type_name -> common.SocketAddr + 16, // 9: acl.ConnTrackEntry.dst_addr:type_name -> common.SocketAddr + 0, // 10: acl.ConnTrackEntry.protocol:type_name -> acl.Protocol + 3, // 11: acl.ConnTrackEntry.state:type_name -> acl.ConnState + 9, // 12: acl.Acl.acl_v1:type_name -> acl.AclV1 + 5, // 13: acl.RuleStats.rule:type_name -> acl.Rule + 12, // 14: acl.RuleStats.stat:type_name -> acl.StatItem + 13, // 15: acl.AclStats.rules:type_name -> acl.RuleStats + 10, // 16: acl.AclStats.conn_track:type_name -> acl.ConnTrackEntry + 15, // 17: acl.AclStats.global:type_name -> acl.AclStats.GlobalEntry + 18, // [18:18] is the sub-list for method output_type + 18, // [18:18] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name +} + +func init() { file_acl_proto_init() } +func file_acl_proto_init() { + if File_acl_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_acl_proto_rawDesc), len(file_acl_proto_rawDesc)), + NumEnums: 4, + NumMessages: 12, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_acl_proto_goTypes, + DependencyIndexes: file_acl_proto_depIdxs, + EnumInfos: file_acl_proto_enumTypes, + MessageInfos: file_acl_proto_msgTypes, + }.Build() + File_acl_proto = out.File + file_acl_proto_goTypes = nil + file_acl_proto_depIdxs = nil +} diff --git a/easytier-go/proto/api/config/api_config.pb.go b/easytier-go/proto/api/config/api_config.pb.go new file mode 100644 index 00000000..8311c2f7 --- /dev/null +++ b/easytier-go/proto/api/config/api_config.pb.go @@ -0,0 +1,971 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.35.1 +// source: api_config.proto + +package config + +import ( + acl "github.com/EasyTier/EasyTier/easytier-go/proto/acl" + instance "github.com/EasyTier/EasyTier/easytier-go/proto/api/instance" + manage "github.com/EasyTier/EasyTier/easytier-go/proto/api/manage" + common "github.com/EasyTier/EasyTier/easytier-go/proto/common" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ConfigPatchAction int32 + +const ( + ConfigPatchAction_ADD ConfigPatchAction = 0 + ConfigPatchAction_REMOVE ConfigPatchAction = 1 + ConfigPatchAction_CLEAR ConfigPatchAction = 2 +) + +// Enum value maps for ConfigPatchAction. +var ( + ConfigPatchAction_name = map[int32]string{ + 0: "ADD", + 1: "REMOVE", + 2: "CLEAR", + } + ConfigPatchAction_value = map[string]int32{ + "ADD": 0, + "REMOVE": 1, + "CLEAR": 2, + } +) + +func (x ConfigPatchAction) Enum() *ConfigPatchAction { + p := new(ConfigPatchAction) + *p = x + return p +} + +func (x ConfigPatchAction) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfigPatchAction) Descriptor() protoreflect.EnumDescriptor { + return file_api_config_proto_enumTypes[0].Descriptor() +} + +func (ConfigPatchAction) Type() protoreflect.EnumType { + return &file_api_config_proto_enumTypes[0] +} + +func (x ConfigPatchAction) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfigPatchAction.Descriptor instead. +func (ConfigPatchAction) EnumDescriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{0} +} + +type InstanceConfigPatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hostname *string `protobuf:"bytes,1,opt,name=hostname,proto3,oneof" json:"hostname,omitempty"` + Ipv4 *common.Ipv4Inet `protobuf:"bytes,2,opt,name=ipv4,proto3,oneof" json:"ipv4,omitempty"` + Ipv6 *common.Ipv6Inet `protobuf:"bytes,3,opt,name=ipv6,proto3,oneof" json:"ipv6,omitempty"` + PortForwards []*PortForwardPatch `protobuf:"bytes,4,rep,name=port_forwards,json=portForwards,proto3" json:"port_forwards,omitempty"` + Acl *AclPatch `protobuf:"bytes,5,opt,name=acl,proto3,oneof" json:"acl,omitempty"` + ProxyNetworks []*ProxyNetworkPatch `protobuf:"bytes,6,rep,name=proxy_networks,json=proxyNetworks,proto3" json:"proxy_networks,omitempty"` + Routes []*RoutePatch `protobuf:"bytes,7,rep,name=routes,proto3" json:"routes,omitempty"` + ExitNodes []*ExitNodePatch `protobuf:"bytes,8,rep,name=exit_nodes,json=exitNodes,proto3" json:"exit_nodes,omitempty"` + MappedListeners []*UrlPatch `protobuf:"bytes,9,rep,name=mapped_listeners,json=mappedListeners,proto3" json:"mapped_listeners,omitempty"` + Connectors []*UrlPatch `protobuf:"bytes,10,rep,name=connectors,proto3" json:"connectors,omitempty"` + Ipv6PublicAddrProvider *bool `protobuf:"varint,11,opt,name=ipv6_public_addr_provider,json=ipv6PublicAddrProvider,proto3,oneof" json:"ipv6_public_addr_provider,omitempty"` + Ipv6PublicAddrAuto *bool `protobuf:"varint,12,opt,name=ipv6_public_addr_auto,json=ipv6PublicAddrAuto,proto3,oneof" json:"ipv6_public_addr_auto,omitempty"` + Ipv6PublicAddrPrefix *string `protobuf:"bytes,13,opt,name=ipv6_public_addr_prefix,json=ipv6PublicAddrPrefix,proto3,oneof" json:"ipv6_public_addr_prefix,omitempty"` + DisableRelayData *bool `protobuf:"varint,14,opt,name=disable_relay_data,json=disableRelayData,proto3,oneof" json:"disable_relay_data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstanceConfigPatch) Reset() { + *x = InstanceConfigPatch{} + mi := &file_api_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstanceConfigPatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstanceConfigPatch) ProtoMessage() {} + +func (x *InstanceConfigPatch) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InstanceConfigPatch.ProtoReflect.Descriptor instead. +func (*InstanceConfigPatch) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{0} +} + +func (x *InstanceConfigPatch) GetHostname() string { + if x != nil && x.Hostname != nil { + return *x.Hostname + } + return "" +} + +func (x *InstanceConfigPatch) GetIpv4() *common.Ipv4Inet { + if x != nil { + return x.Ipv4 + } + return nil +} + +func (x *InstanceConfigPatch) GetIpv6() *common.Ipv6Inet { + if x != nil { + return x.Ipv6 + } + return nil +} + +func (x *InstanceConfigPatch) GetPortForwards() []*PortForwardPatch { + if x != nil { + return x.PortForwards + } + return nil +} + +func (x *InstanceConfigPatch) GetAcl() *AclPatch { + if x != nil { + return x.Acl + } + return nil +} + +func (x *InstanceConfigPatch) GetProxyNetworks() []*ProxyNetworkPatch { + if x != nil { + return x.ProxyNetworks + } + return nil +} + +func (x *InstanceConfigPatch) GetRoutes() []*RoutePatch { + if x != nil { + return x.Routes + } + return nil +} + +func (x *InstanceConfigPatch) GetExitNodes() []*ExitNodePatch { + if x != nil { + return x.ExitNodes + } + return nil +} + +func (x *InstanceConfigPatch) GetMappedListeners() []*UrlPatch { + if x != nil { + return x.MappedListeners + } + return nil +} + +func (x *InstanceConfigPatch) GetConnectors() []*UrlPatch { + if x != nil { + return x.Connectors + } + return nil +} + +func (x *InstanceConfigPatch) GetIpv6PublicAddrProvider() bool { + if x != nil && x.Ipv6PublicAddrProvider != nil { + return *x.Ipv6PublicAddrProvider + } + return false +} + +func (x *InstanceConfigPatch) GetIpv6PublicAddrAuto() bool { + if x != nil && x.Ipv6PublicAddrAuto != nil { + return *x.Ipv6PublicAddrAuto + } + return false +} + +func (x *InstanceConfigPatch) GetIpv6PublicAddrPrefix() string { + if x != nil && x.Ipv6PublicAddrPrefix != nil { + return *x.Ipv6PublicAddrPrefix + } + return "" +} + +func (x *InstanceConfigPatch) GetDisableRelayData() bool { + if x != nil && x.DisableRelayData != nil { + return *x.DisableRelayData + } + return false +} + +type PortForwardPatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action ConfigPatchAction `protobuf:"varint,1,opt,name=action,proto3,enum=api.config.ConfigPatchAction" json:"action,omitempty"` + Cfg *common.PortForwardConfigPb `protobuf:"bytes,2,opt,name=cfg,proto3" json:"cfg,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PortForwardPatch) Reset() { + *x = PortForwardPatch{} + mi := &file_api_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortForwardPatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortForwardPatch) ProtoMessage() {} + +func (x *PortForwardPatch) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PortForwardPatch.ProtoReflect.Descriptor instead. +func (*PortForwardPatch) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{1} +} + +func (x *PortForwardPatch) GetAction() ConfigPatchAction { + if x != nil { + return x.Action + } + return ConfigPatchAction_ADD +} + +func (x *PortForwardPatch) GetCfg() *common.PortForwardConfigPb { + if x != nil { + return x.Cfg + } + return nil +} + +type StringPatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action ConfigPatchAction `protobuf:"varint,1,opt,name=action,proto3,enum=api.config.ConfigPatchAction" json:"action,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StringPatch) Reset() { + *x = StringPatch{} + mi := &file_api_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StringPatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StringPatch) ProtoMessage() {} + +func (x *StringPatch) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StringPatch.ProtoReflect.Descriptor instead. +func (*StringPatch) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{2} +} + +func (x *StringPatch) GetAction() ConfigPatchAction { + if x != nil { + return x.Action + } + return ConfigPatchAction_ADD +} + +func (x *StringPatch) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type UrlPatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action ConfigPatchAction `protobuf:"varint,1,opt,name=action,proto3,enum=api.config.ConfigPatchAction" json:"action,omitempty"` + Url *common.Url `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UrlPatch) Reset() { + *x = UrlPatch{} + mi := &file_api_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UrlPatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UrlPatch) ProtoMessage() {} + +func (x *UrlPatch) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UrlPatch.ProtoReflect.Descriptor instead. +func (*UrlPatch) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{3} +} + +func (x *UrlPatch) GetAction() ConfigPatchAction { + if x != nil { + return x.Action + } + return ConfigPatchAction_ADD +} + +func (x *UrlPatch) GetUrl() *common.Url { + if x != nil { + return x.Url + } + return nil +} + +type AclPatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + Acl *acl.Acl `protobuf:"bytes,1,opt,name=acl,proto3,oneof" json:"acl,omitempty"` + TcpWhitelist []*StringPatch `protobuf:"bytes,2,rep,name=tcp_whitelist,json=tcpWhitelist,proto3" json:"tcp_whitelist,omitempty"` + UdpWhitelist []*StringPatch `protobuf:"bytes,3,rep,name=udp_whitelist,json=udpWhitelist,proto3" json:"udp_whitelist,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AclPatch) Reset() { + *x = AclPatch{} + mi := &file_api_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AclPatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AclPatch) ProtoMessage() {} + +func (x *AclPatch) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AclPatch.ProtoReflect.Descriptor instead. +func (*AclPatch) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{4} +} + +func (x *AclPatch) GetAcl() *acl.Acl { + if x != nil { + return x.Acl + } + return nil +} + +func (x *AclPatch) GetTcpWhitelist() []*StringPatch { + if x != nil { + return x.TcpWhitelist + } + return nil +} + +func (x *AclPatch) GetUdpWhitelist() []*StringPatch { + if x != nil { + return x.UdpWhitelist + } + return nil +} + +type ProxyNetworkPatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action ConfigPatchAction `protobuf:"varint,1,opt,name=action,proto3,enum=api.config.ConfigPatchAction" json:"action,omitempty"` + Cidr *common.Ipv4Inet `protobuf:"bytes,2,opt,name=cidr,proto3" json:"cidr,omitempty"` + MappedCidr *common.Ipv4Inet `protobuf:"bytes,3,opt,name=mapped_cidr,json=mappedCidr,proto3,oneof" json:"mapped_cidr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProxyNetworkPatch) Reset() { + *x = ProxyNetworkPatch{} + mi := &file_api_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProxyNetworkPatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyNetworkPatch) ProtoMessage() {} + +func (x *ProxyNetworkPatch) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProxyNetworkPatch.ProtoReflect.Descriptor instead. +func (*ProxyNetworkPatch) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{5} +} + +func (x *ProxyNetworkPatch) GetAction() ConfigPatchAction { + if x != nil { + return x.Action + } + return ConfigPatchAction_ADD +} + +func (x *ProxyNetworkPatch) GetCidr() *common.Ipv4Inet { + if x != nil { + return x.Cidr + } + return nil +} + +func (x *ProxyNetworkPatch) GetMappedCidr() *common.Ipv4Inet { + if x != nil { + return x.MappedCidr + } + return nil +} + +type RoutePatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action ConfigPatchAction `protobuf:"varint,1,opt,name=action,proto3,enum=api.config.ConfigPatchAction" json:"action,omitempty"` + Cidr *common.Ipv4Inet `protobuf:"bytes,2,opt,name=cidr,proto3" json:"cidr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoutePatch) Reset() { + *x = RoutePatch{} + mi := &file_api_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoutePatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoutePatch) ProtoMessage() {} + +func (x *RoutePatch) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoutePatch.ProtoReflect.Descriptor instead. +func (*RoutePatch) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{6} +} + +func (x *RoutePatch) GetAction() ConfigPatchAction { + if x != nil { + return x.Action + } + return ConfigPatchAction_ADD +} + +func (x *RoutePatch) GetCidr() *common.Ipv4Inet { + if x != nil { + return x.Cidr + } + return nil +} + +type ExitNodePatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action ConfigPatchAction `protobuf:"varint,1,opt,name=action,proto3,enum=api.config.ConfigPatchAction" json:"action,omitempty"` + Node *common.IpAddr `protobuf:"bytes,2,opt,name=node,proto3" json:"node,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExitNodePatch) Reset() { + *x = ExitNodePatch{} + mi := &file_api_config_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExitNodePatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExitNodePatch) ProtoMessage() {} + +func (x *ExitNodePatch) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExitNodePatch.ProtoReflect.Descriptor instead. +func (*ExitNodePatch) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{7} +} + +func (x *ExitNodePatch) GetAction() ConfigPatchAction { + if x != nil { + return x.Action + } + return ConfigPatchAction_ADD +} + +func (x *ExitNodePatch) GetNode() *common.IpAddr { + if x != nil { + return x.Node + } + return nil +} + +type PatchConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Patch *InstanceConfigPatch `protobuf:"bytes,1,opt,name=patch,proto3" json:"patch,omitempty"` + Instance *instance.InstanceIdentifier `protobuf:"bytes,2,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PatchConfigRequest) Reset() { + *x = PatchConfigRequest{} + mi := &file_api_config_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PatchConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PatchConfigRequest) ProtoMessage() {} + +func (x *PatchConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PatchConfigRequest.ProtoReflect.Descriptor instead. +func (*PatchConfigRequest) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{8} +} + +func (x *PatchConfigRequest) GetPatch() *InstanceConfigPatch { + if x != nil { + return x.Patch + } + return nil +} + +func (x *PatchConfigRequest) GetInstance() *instance.InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type PatchConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PatchConfigResponse) Reset() { + *x = PatchConfigResponse{} + mi := &file_api_config_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PatchConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PatchConfigResponse) ProtoMessage() {} + +func (x *PatchConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PatchConfigResponse.ProtoReflect.Descriptor instead. +func (*PatchConfigResponse) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{9} +} + +type GetConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *instance.InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetConfigRequest) Reset() { + *x = GetConfigRequest{} + mi := &file_api_config_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConfigRequest) ProtoMessage() {} + +func (x *GetConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetConfigRequest.ProtoReflect.Descriptor instead. +func (*GetConfigRequest) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{10} +} + +func (x *GetConfigRequest) GetInstance() *instance.InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type GetConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *manage.NetworkConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + TomlConfig string `protobuf:"bytes,2,opt,name=toml_config,json=tomlConfig,proto3" json:"toml_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetConfigResponse) Reset() { + *x = GetConfigResponse{} + mi := &file_api_config_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConfigResponse) ProtoMessage() {} + +func (x *GetConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_config_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetConfigResponse.ProtoReflect.Descriptor instead. +func (*GetConfigResponse) Descriptor() ([]byte, []int) { + return file_api_config_proto_rawDescGZIP(), []int{11} +} + +func (x *GetConfigResponse) GetConfig() *manage.NetworkConfig { + if x != nil { + return x.Config + } + return nil +} + +func (x *GetConfigResponse) GetTomlConfig() string { + if x != nil { + return x.TomlConfig + } + return "" +} + +var File_api_config_proto protoreflect.FileDescriptor + +const file_api_config_proto_rawDesc = "" + + "\n" + + "\x10api_config.proto\x12\n" + + "api.config\x1a\fcommon.proto\x1a\tacl.proto\x1a\x12api_instance.proto\x1a\x10api_manage.proto\"\x9c\a\n" + + "\x13InstanceConfigPatch\x12\x1f\n" + + "\bhostname\x18\x01 \x01(\tH\x00R\bhostname\x88\x01\x01\x12)\n" + + "\x04ipv4\x18\x02 \x01(\v2\x10.common.Ipv4InetH\x01R\x04ipv4\x88\x01\x01\x12)\n" + + "\x04ipv6\x18\x03 \x01(\v2\x10.common.Ipv6InetH\x02R\x04ipv6\x88\x01\x01\x12A\n" + + "\rport_forwards\x18\x04 \x03(\v2\x1c.api.config.PortForwardPatchR\fportForwards\x12+\n" + + "\x03acl\x18\x05 \x01(\v2\x14.api.config.AclPatchH\x03R\x03acl\x88\x01\x01\x12D\n" + + "\x0eproxy_networks\x18\x06 \x03(\v2\x1d.api.config.ProxyNetworkPatchR\rproxyNetworks\x12.\n" + + "\x06routes\x18\a \x03(\v2\x16.api.config.RoutePatchR\x06routes\x128\n" + + "\n" + + "exit_nodes\x18\b \x03(\v2\x19.api.config.ExitNodePatchR\texitNodes\x12?\n" + + "\x10mapped_listeners\x18\t \x03(\v2\x14.api.config.UrlPatchR\x0fmappedListeners\x124\n" + + "\n" + + "connectors\x18\n" + + " \x03(\v2\x14.api.config.UrlPatchR\n" + + "connectors\x12>\n" + + "\x19ipv6_public_addr_provider\x18\v \x01(\bH\x04R\x16ipv6PublicAddrProvider\x88\x01\x01\x126\n" + + "\x15ipv6_public_addr_auto\x18\f \x01(\bH\x05R\x12ipv6PublicAddrAuto\x88\x01\x01\x12:\n" + + "\x17ipv6_public_addr_prefix\x18\r \x01(\tH\x06R\x14ipv6PublicAddrPrefix\x88\x01\x01\x121\n" + + "\x12disable_relay_data\x18\x0e \x01(\bH\aR\x10disableRelayData\x88\x01\x01B\v\n" + + "\t_hostnameB\a\n" + + "\x05_ipv4B\a\n" + + "\x05_ipv6B\x06\n" + + "\x04_aclB\x1c\n" + + "\x1a_ipv6_public_addr_providerB\x18\n" + + "\x16_ipv6_public_addr_autoB\x1a\n" + + "\x18_ipv6_public_addr_prefixB\x15\n" + + "\x13_disable_relay_data\"x\n" + + "\x10PortForwardPatch\x125\n" + + "\x06action\x18\x01 \x01(\x0e2\x1d.api.config.ConfigPatchActionR\x06action\x12-\n" + + "\x03cfg\x18\x02 \x01(\v2\x1b.common.PortForwardConfigPbR\x03cfg\"Z\n" + + "\vStringPatch\x125\n" + + "\x06action\x18\x01 \x01(\x0e2\x1d.api.config.ConfigPatchActionR\x06action\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value\"`\n" + + "\bUrlPatch\x125\n" + + "\x06action\x18\x01 \x01(\x0e2\x1d.api.config.ConfigPatchActionR\x06action\x12\x1d\n" + + "\x03url\x18\x02 \x01(\v2\v.common.UrlR\x03url\"\xaf\x01\n" + + "\bAclPatch\x12\x1f\n" + + "\x03acl\x18\x01 \x01(\v2\b.acl.AclH\x00R\x03acl\x88\x01\x01\x12<\n" + + "\rtcp_whitelist\x18\x02 \x03(\v2\x17.api.config.StringPatchR\ftcpWhitelist\x12<\n" + + "\rudp_whitelist\x18\x03 \x03(\v2\x17.api.config.StringPatchR\fudpWhitelistB\x06\n" + + "\x04_acl\"\xb8\x01\n" + + "\x11ProxyNetworkPatch\x125\n" + + "\x06action\x18\x01 \x01(\x0e2\x1d.api.config.ConfigPatchActionR\x06action\x12$\n" + + "\x04cidr\x18\x02 \x01(\v2\x10.common.Ipv4InetR\x04cidr\x126\n" + + "\vmapped_cidr\x18\x03 \x01(\v2\x10.common.Ipv4InetH\x00R\n" + + "mappedCidr\x88\x01\x01B\x0e\n" + + "\f_mapped_cidr\"i\n" + + "\n" + + "RoutePatch\x125\n" + + "\x06action\x18\x01 \x01(\x0e2\x1d.api.config.ConfigPatchActionR\x06action\x12$\n" + + "\x04cidr\x18\x02 \x01(\v2\x10.common.Ipv4InetR\x04cidr\"j\n" + + "\rExitNodePatch\x125\n" + + "\x06action\x18\x01 \x01(\x0e2\x1d.api.config.ConfigPatchActionR\x06action\x12\"\n" + + "\x04node\x18\x02 \x01(\v2\x0e.common.IpAddrR\x04node\"\x89\x01\n" + + "\x12PatchConfigRequest\x125\n" + + "\x05patch\x18\x01 \x01(\v2\x1f.api.config.InstanceConfigPatchR\x05patch\x12<\n" + + "\binstance\x18\x02 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"\x15\n" + + "\x13PatchConfigResponse\"P\n" + + "\x10GetConfigRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"g\n" + + "\x11GetConfigResponse\x121\n" + + "\x06config\x18\x01 \x01(\v2\x19.api.manage.NetworkConfigR\x06config\x12\x1f\n" + + "\vtoml_config\x18\x02 \x01(\tR\n" + + "tomlConfig*3\n" + + "\x11ConfigPatchAction\x12\a\n" + + "\x03ADD\x10\x00\x12\n" + + "\n" + + "\x06REMOVE\x10\x01\x12\t\n" + + "\x05CLEAR\x10\x022\xa5\x01\n" + + "\tConfigRpc\x12N\n" + + "\vPatchConfig\x12\x1e.api.config.PatchConfigRequest\x1a\x1f.api.config.PatchConfigResponse\x12H\n" + + "\tGetConfig\x12\x1c.api.config.GetConfigRequest\x1a\x1d.api.config.GetConfigResponseb\x06proto3" + +var ( + file_api_config_proto_rawDescOnce sync.Once + file_api_config_proto_rawDescData []byte +) + +func file_api_config_proto_rawDescGZIP() []byte { + file_api_config_proto_rawDescOnce.Do(func() { + file_api_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_config_proto_rawDesc), len(file_api_config_proto_rawDesc))) + }) + return file_api_config_proto_rawDescData +} + +var file_api_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_api_config_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_api_config_proto_goTypes = []any{ + (ConfigPatchAction)(0), // 0: api.config.ConfigPatchAction + (*InstanceConfigPatch)(nil), // 1: api.config.InstanceConfigPatch + (*PortForwardPatch)(nil), // 2: api.config.PortForwardPatch + (*StringPatch)(nil), // 3: api.config.StringPatch + (*UrlPatch)(nil), // 4: api.config.UrlPatch + (*AclPatch)(nil), // 5: api.config.AclPatch + (*ProxyNetworkPatch)(nil), // 6: api.config.ProxyNetworkPatch + (*RoutePatch)(nil), // 7: api.config.RoutePatch + (*ExitNodePatch)(nil), // 8: api.config.ExitNodePatch + (*PatchConfigRequest)(nil), // 9: api.config.PatchConfigRequest + (*PatchConfigResponse)(nil), // 10: api.config.PatchConfigResponse + (*GetConfigRequest)(nil), // 11: api.config.GetConfigRequest + (*GetConfigResponse)(nil), // 12: api.config.GetConfigResponse + (*common.Ipv4Inet)(nil), // 13: common.Ipv4Inet + (*common.Ipv6Inet)(nil), // 14: common.Ipv6Inet + (*common.PortForwardConfigPb)(nil), // 15: common.PortForwardConfigPb + (*common.Url)(nil), // 16: common.Url + (*acl.Acl)(nil), // 17: acl.Acl + (*common.IpAddr)(nil), // 18: common.IpAddr + (*instance.InstanceIdentifier)(nil), // 19: api.instance.InstanceIdentifier + (*manage.NetworkConfig)(nil), // 20: api.manage.NetworkConfig +} +var file_api_config_proto_depIdxs = []int32{ + 13, // 0: api.config.InstanceConfigPatch.ipv4:type_name -> common.Ipv4Inet + 14, // 1: api.config.InstanceConfigPatch.ipv6:type_name -> common.Ipv6Inet + 2, // 2: api.config.InstanceConfigPatch.port_forwards:type_name -> api.config.PortForwardPatch + 5, // 3: api.config.InstanceConfigPatch.acl:type_name -> api.config.AclPatch + 6, // 4: api.config.InstanceConfigPatch.proxy_networks:type_name -> api.config.ProxyNetworkPatch + 7, // 5: api.config.InstanceConfigPatch.routes:type_name -> api.config.RoutePatch + 8, // 6: api.config.InstanceConfigPatch.exit_nodes:type_name -> api.config.ExitNodePatch + 4, // 7: api.config.InstanceConfigPatch.mapped_listeners:type_name -> api.config.UrlPatch + 4, // 8: api.config.InstanceConfigPatch.connectors:type_name -> api.config.UrlPatch + 0, // 9: api.config.PortForwardPatch.action:type_name -> api.config.ConfigPatchAction + 15, // 10: api.config.PortForwardPatch.cfg:type_name -> common.PortForwardConfigPb + 0, // 11: api.config.StringPatch.action:type_name -> api.config.ConfigPatchAction + 0, // 12: api.config.UrlPatch.action:type_name -> api.config.ConfigPatchAction + 16, // 13: api.config.UrlPatch.url:type_name -> common.Url + 17, // 14: api.config.AclPatch.acl:type_name -> acl.Acl + 3, // 15: api.config.AclPatch.tcp_whitelist:type_name -> api.config.StringPatch + 3, // 16: api.config.AclPatch.udp_whitelist:type_name -> api.config.StringPatch + 0, // 17: api.config.ProxyNetworkPatch.action:type_name -> api.config.ConfigPatchAction + 13, // 18: api.config.ProxyNetworkPatch.cidr:type_name -> common.Ipv4Inet + 13, // 19: api.config.ProxyNetworkPatch.mapped_cidr:type_name -> common.Ipv4Inet + 0, // 20: api.config.RoutePatch.action:type_name -> api.config.ConfigPatchAction + 13, // 21: api.config.RoutePatch.cidr:type_name -> common.Ipv4Inet + 0, // 22: api.config.ExitNodePatch.action:type_name -> api.config.ConfigPatchAction + 18, // 23: api.config.ExitNodePatch.node:type_name -> common.IpAddr + 1, // 24: api.config.PatchConfigRequest.patch:type_name -> api.config.InstanceConfigPatch + 19, // 25: api.config.PatchConfigRequest.instance:type_name -> api.instance.InstanceIdentifier + 19, // 26: api.config.GetConfigRequest.instance:type_name -> api.instance.InstanceIdentifier + 20, // 27: api.config.GetConfigResponse.config:type_name -> api.manage.NetworkConfig + 9, // 28: api.config.ConfigRpc.PatchConfig:input_type -> api.config.PatchConfigRequest + 11, // 29: api.config.ConfigRpc.GetConfig:input_type -> api.config.GetConfigRequest + 10, // 30: api.config.ConfigRpc.PatchConfig:output_type -> api.config.PatchConfigResponse + 12, // 31: api.config.ConfigRpc.GetConfig:output_type -> api.config.GetConfigResponse + 30, // [30:32] is the sub-list for method output_type + 28, // [28:30] is the sub-list for method input_type + 28, // [28:28] is the sub-list for extension type_name + 28, // [28:28] is the sub-list for extension extendee + 0, // [0:28] is the sub-list for field type_name +} + +func init() { file_api_config_proto_init() } +func file_api_config_proto_init() { + if File_api_config_proto != nil { + return + } + file_api_config_proto_msgTypes[0].OneofWrappers = []any{} + file_api_config_proto_msgTypes[4].OneofWrappers = []any{} + file_api_config_proto_msgTypes[5].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_config_proto_rawDesc), len(file_api_config_proto_rawDesc)), + NumEnums: 1, + NumMessages: 12, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_config_proto_goTypes, + DependencyIndexes: file_api_config_proto_depIdxs, + EnumInfos: file_api_config_proto_enumTypes, + MessageInfos: file_api_config_proto_msgTypes, + }.Build() + File_api_config_proto = out.File + file_api_config_proto_goTypes = nil + file_api_config_proto_depIdxs = nil +} diff --git a/easytier-go/proto/api/instance/api_instance.pb.go b/easytier-go/proto/api/instance/api_instance.pb.go new file mode 100644 index 00000000..8431e3e7 --- /dev/null +++ b/easytier-go/proto/api/instance/api_instance.pb.go @@ -0,0 +1,4182 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.35.1 +// source: api_instance.proto + +package instance + +import ( + acl "github.com/EasyTier/EasyTier/easytier-go/proto/acl" + common "github.com/EasyTier/EasyTier/easytier-go/proto/common" + peer_rpc "github.com/EasyTier/EasyTier/easytier-go/proto/peer_rpc" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TrustedKeySourcePb int32 + +const ( + TrustedKeySourcePb_TRUSTED_KEY_SOURCE_PB_UNSPECIFIED TrustedKeySourcePb = 0 + TrustedKeySourcePb_TRUSTED_KEY_SOURCE_PB_OSPF_NODE TrustedKeySourcePb = 1 + TrustedKeySourcePb_TRUSTED_KEY_SOURCE_PB_OSPF_CREDENTIAL TrustedKeySourcePb = 2 +) + +// Enum value maps for TrustedKeySourcePb. +var ( + TrustedKeySourcePb_name = map[int32]string{ + 0: "TRUSTED_KEY_SOURCE_PB_UNSPECIFIED", + 1: "TRUSTED_KEY_SOURCE_PB_OSPF_NODE", + 2: "TRUSTED_KEY_SOURCE_PB_OSPF_CREDENTIAL", + } + TrustedKeySourcePb_value = map[string]int32{ + "TRUSTED_KEY_SOURCE_PB_UNSPECIFIED": 0, + "TRUSTED_KEY_SOURCE_PB_OSPF_NODE": 1, + "TRUSTED_KEY_SOURCE_PB_OSPF_CREDENTIAL": 2, + } +) + +func (x TrustedKeySourcePb) Enum() *TrustedKeySourcePb { + p := new(TrustedKeySourcePb) + *p = x + return p +} + +func (x TrustedKeySourcePb) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TrustedKeySourcePb) Descriptor() protoreflect.EnumDescriptor { + return file_api_instance_proto_enumTypes[0].Descriptor() +} + +func (TrustedKeySourcePb) Type() protoreflect.EnumType { + return &file_api_instance_proto_enumTypes[0] +} + +func (x TrustedKeySourcePb) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TrustedKeySourcePb.Descriptor instead. +func (TrustedKeySourcePb) EnumDescriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{0} +} + +type ConnectorStatus int32 + +const ( + ConnectorStatus_CONNECTED ConnectorStatus = 0 + ConnectorStatus_DISCONNECTED ConnectorStatus = 1 + ConnectorStatus_CONNECTING ConnectorStatus = 2 +) + +// Enum value maps for ConnectorStatus. +var ( + ConnectorStatus_name = map[int32]string{ + 0: "CONNECTED", + 1: "DISCONNECTED", + 2: "CONNECTING", + } + ConnectorStatus_value = map[string]int32{ + "CONNECTED": 0, + "DISCONNECTED": 1, + "CONNECTING": 2, + } +) + +func (x ConnectorStatus) Enum() *ConnectorStatus { + p := new(ConnectorStatus) + *p = x + return p +} + +func (x ConnectorStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConnectorStatus) Descriptor() protoreflect.EnumDescriptor { + return file_api_instance_proto_enumTypes[1].Descriptor() +} + +func (ConnectorStatus) Type() protoreflect.EnumType { + return &file_api_instance_proto_enumTypes[1] +} + +func (x ConnectorStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConnectorStatus.Descriptor instead. +func (ConnectorStatus) EnumDescriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{1} +} + +type TcpProxyEntryTransportType int32 + +const ( + TcpProxyEntryTransportType_TCP TcpProxyEntryTransportType = 0 + TcpProxyEntryTransportType_KCP TcpProxyEntryTransportType = 1 + TcpProxyEntryTransportType_QUIC TcpProxyEntryTransportType = 2 +) + +// Enum value maps for TcpProxyEntryTransportType. +var ( + TcpProxyEntryTransportType_name = map[int32]string{ + 0: "TCP", + 1: "KCP", + 2: "QUIC", + } + TcpProxyEntryTransportType_value = map[string]int32{ + "TCP": 0, + "KCP": 1, + "QUIC": 2, + } +) + +func (x TcpProxyEntryTransportType) Enum() *TcpProxyEntryTransportType { + p := new(TcpProxyEntryTransportType) + *p = x + return p +} + +func (x TcpProxyEntryTransportType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TcpProxyEntryTransportType) Descriptor() protoreflect.EnumDescriptor { + return file_api_instance_proto_enumTypes[2].Descriptor() +} + +func (TcpProxyEntryTransportType) Type() protoreflect.EnumType { + return &file_api_instance_proto_enumTypes[2] +} + +func (x TcpProxyEntryTransportType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TcpProxyEntryTransportType.Descriptor instead. +func (TcpProxyEntryTransportType) EnumDescriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{2} +} + +type TcpProxyEntryState int32 + +const ( + TcpProxyEntryState_Unknown TcpProxyEntryState = 0 + // receive syn packet but not start connecting to dst + TcpProxyEntryState_SynReceived TcpProxyEntryState = 1 + // connecting to dst + TcpProxyEntryState_ConnectingDst TcpProxyEntryState = 2 + // connected to dst + TcpProxyEntryState_Connected TcpProxyEntryState = 3 + // connection closed + TcpProxyEntryState_Closed TcpProxyEntryState = 4 + // closing src + TcpProxyEntryState_ClosingSrc TcpProxyEntryState = 5 + // closing dst + TcpProxyEntryState_ClosingDst TcpProxyEntryState = 6 +) + +// Enum value maps for TcpProxyEntryState. +var ( + TcpProxyEntryState_name = map[int32]string{ + 0: "Unknown", + 1: "SynReceived", + 2: "ConnectingDst", + 3: "Connected", + 4: "Closed", + 5: "ClosingSrc", + 6: "ClosingDst", + } + TcpProxyEntryState_value = map[string]int32{ + "Unknown": 0, + "SynReceived": 1, + "ConnectingDst": 2, + "Connected": 3, + "Closed": 4, + "ClosingSrc": 5, + "ClosingDst": 6, + } +) + +func (x TcpProxyEntryState) Enum() *TcpProxyEntryState { + p := new(TcpProxyEntryState) + *p = x + return p +} + +func (x TcpProxyEntryState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TcpProxyEntryState) Descriptor() protoreflect.EnumDescriptor { + return file_api_instance_proto_enumTypes[3].Descriptor() +} + +func (TcpProxyEntryState) Type() protoreflect.EnumType { + return &file_api_instance_proto_enumTypes[3] +} + +func (x TcpProxyEntryState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TcpProxyEntryState.Descriptor instead. +func (TcpProxyEntryState) EnumDescriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{3} +} + +type InstanceIdentifier struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Selector: + // + // *InstanceIdentifier_Id + // *InstanceIdentifier_InstanceSelector_ + Selector isInstanceIdentifier_Selector `protobuf_oneof:"selector"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstanceIdentifier) Reset() { + *x = InstanceIdentifier{} + mi := &file_api_instance_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstanceIdentifier) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstanceIdentifier) ProtoMessage() {} + +func (x *InstanceIdentifier) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InstanceIdentifier.ProtoReflect.Descriptor instead. +func (*InstanceIdentifier) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{0} +} + +func (x *InstanceIdentifier) GetSelector() isInstanceIdentifier_Selector { + if x != nil { + return x.Selector + } + return nil +} + +func (x *InstanceIdentifier) GetId() *common.UUID { + if x != nil { + if x, ok := x.Selector.(*InstanceIdentifier_Id); ok { + return x.Id + } + } + return nil +} + +func (x *InstanceIdentifier) GetInstanceSelector() *InstanceIdentifier_InstanceSelector { + if x != nil { + if x, ok := x.Selector.(*InstanceIdentifier_InstanceSelector_); ok { + return x.InstanceSelector + } + } + return nil +} + +type isInstanceIdentifier_Selector interface { + isInstanceIdentifier_Selector() +} + +type InstanceIdentifier_Id struct { + Id *common.UUID `protobuf:"bytes,1,opt,name=id,proto3,oneof"` +} + +type InstanceIdentifier_InstanceSelector_ struct { + InstanceSelector *InstanceIdentifier_InstanceSelector `protobuf:"bytes,2,opt,name=instance_selector,json=instanceSelector,proto3,oneof"` +} + +func (*InstanceIdentifier_Id) isInstanceIdentifier_Selector() {} + +func (*InstanceIdentifier_InstanceSelector_) isInstanceIdentifier_Selector() {} + +type Status struct { + state protoimpl.MessageState `protogen:"open.v1"` + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Status) Reset() { + *x = Status{} + mi := &file_api_instance_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Status) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Status) ProtoMessage() {} + +func (x *Status) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Status.ProtoReflect.Descriptor instead. +func (*Status) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{1} +} + +func (x *Status) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *Status) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +type PeerConnStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + RxBytes uint64 `protobuf:"varint,1,opt,name=rx_bytes,json=rxBytes,proto3" json:"rx_bytes,omitempty"` + TxBytes uint64 `protobuf:"varint,2,opt,name=tx_bytes,json=txBytes,proto3" json:"tx_bytes,omitempty"` + RxPackets uint64 `protobuf:"varint,3,opt,name=rx_packets,json=rxPackets,proto3" json:"rx_packets,omitempty"` + TxPackets uint64 `protobuf:"varint,4,opt,name=tx_packets,json=txPackets,proto3" json:"tx_packets,omitempty"` + LatencyUs uint64 `protobuf:"varint,5,opt,name=latency_us,json=latencyUs,proto3" json:"latency_us,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerConnStats) Reset() { + *x = PeerConnStats{} + mi := &file_api_instance_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerConnStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerConnStats) ProtoMessage() {} + +func (x *PeerConnStats) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerConnStats.ProtoReflect.Descriptor instead. +func (*PeerConnStats) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{2} +} + +func (x *PeerConnStats) GetRxBytes() uint64 { + if x != nil { + return x.RxBytes + } + return 0 +} + +func (x *PeerConnStats) GetTxBytes() uint64 { + if x != nil { + return x.TxBytes + } + return 0 +} + +func (x *PeerConnStats) GetRxPackets() uint64 { + if x != nil { + return x.RxPackets + } + return 0 +} + +func (x *PeerConnStats) GetTxPackets() uint64 { + if x != nil { + return x.TxPackets + } + return 0 +} + +func (x *PeerConnStats) GetLatencyUs() uint64 { + if x != nil { + return x.LatencyUs + } + return 0 +} + +type PeerConnInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConnId string `protobuf:"bytes,1,opt,name=conn_id,json=connId,proto3" json:"conn_id,omitempty"` + MyPeerId uint32 `protobuf:"varint,2,opt,name=my_peer_id,json=myPeerId,proto3" json:"my_peer_id,omitempty"` + PeerId uint32 `protobuf:"varint,3,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + Features []string `protobuf:"bytes,4,rep,name=features,proto3" json:"features,omitempty"` + Tunnel *common.TunnelInfo `protobuf:"bytes,5,opt,name=tunnel,proto3" json:"tunnel,omitempty"` + Stats *PeerConnStats `protobuf:"bytes,6,opt,name=stats,proto3" json:"stats,omitempty"` + LossRate float32 `protobuf:"fixed32,7,opt,name=loss_rate,json=lossRate,proto3" json:"loss_rate,omitempty"` + IsClient bool `protobuf:"varint,8,opt,name=is_client,json=isClient,proto3" json:"is_client,omitempty"` + NetworkName string `protobuf:"bytes,9,opt,name=network_name,json=networkName,proto3" json:"network_name,omitempty"` + IsClosed bool `protobuf:"varint,10,opt,name=is_closed,json=isClosed,proto3" json:"is_closed,omitempty"` + NoiseLocalStaticPubkey []byte `protobuf:"bytes,11,opt,name=noise_local_static_pubkey,json=noiseLocalStaticPubkey,proto3" json:"noise_local_static_pubkey,omitempty"` + NoiseRemoteStaticPubkey []byte `protobuf:"bytes,12,opt,name=noise_remote_static_pubkey,json=noiseRemoteStaticPubkey,proto3" json:"noise_remote_static_pubkey,omitempty"` + SecureAuthLevel peer_rpc.SecureAuthLevel `protobuf:"varint,13,opt,name=secure_auth_level,json=secureAuthLevel,proto3,enum=peer_rpc.SecureAuthLevel" json:"secure_auth_level,omitempty"` + PeerIdentityType peer_rpc.PeerIdentityType `protobuf:"varint,14,opt,name=peer_identity_type,json=peerIdentityType,proto3,enum=peer_rpc.PeerIdentityType" json:"peer_identity_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerConnInfo) Reset() { + *x = PeerConnInfo{} + mi := &file_api_instance_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerConnInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerConnInfo) ProtoMessage() {} + +func (x *PeerConnInfo) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerConnInfo.ProtoReflect.Descriptor instead. +func (*PeerConnInfo) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{3} +} + +func (x *PeerConnInfo) GetConnId() string { + if x != nil { + return x.ConnId + } + return "" +} + +func (x *PeerConnInfo) GetMyPeerId() uint32 { + if x != nil { + return x.MyPeerId + } + return 0 +} + +func (x *PeerConnInfo) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *PeerConnInfo) GetFeatures() []string { + if x != nil { + return x.Features + } + return nil +} + +func (x *PeerConnInfo) GetTunnel() *common.TunnelInfo { + if x != nil { + return x.Tunnel + } + return nil +} + +func (x *PeerConnInfo) GetStats() *PeerConnStats { + if x != nil { + return x.Stats + } + return nil +} + +func (x *PeerConnInfo) GetLossRate() float32 { + if x != nil { + return x.LossRate + } + return 0 +} + +func (x *PeerConnInfo) GetIsClient() bool { + if x != nil { + return x.IsClient + } + return false +} + +func (x *PeerConnInfo) GetNetworkName() string { + if x != nil { + return x.NetworkName + } + return "" +} + +func (x *PeerConnInfo) GetIsClosed() bool { + if x != nil { + return x.IsClosed + } + return false +} + +func (x *PeerConnInfo) GetNoiseLocalStaticPubkey() []byte { + if x != nil { + return x.NoiseLocalStaticPubkey + } + return nil +} + +func (x *PeerConnInfo) GetNoiseRemoteStaticPubkey() []byte { + if x != nil { + return x.NoiseRemoteStaticPubkey + } + return nil +} + +func (x *PeerConnInfo) GetSecureAuthLevel() peer_rpc.SecureAuthLevel { + if x != nil { + return x.SecureAuthLevel + } + return peer_rpc.SecureAuthLevel(0) +} + +func (x *PeerConnInfo) GetPeerIdentityType() peer_rpc.PeerIdentityType { + if x != nil { + return x.PeerIdentityType + } + return peer_rpc.PeerIdentityType(0) +} + +type PeerInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + Conns []*PeerConnInfo `protobuf:"bytes,2,rep,name=conns,proto3" json:"conns,omitempty"` + DefaultConnId *common.UUID `protobuf:"bytes,3,opt,name=default_conn_id,json=defaultConnId,proto3" json:"default_conn_id,omitempty"` + DirectlyConnectedConns []*common.UUID `protobuf:"bytes,4,rep,name=directly_connected_conns,json=directlyConnectedConns,proto3" json:"directly_connected_conns,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerInfo) Reset() { + *x = PeerInfo{} + mi := &file_api_instance_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerInfo) ProtoMessage() {} + +func (x *PeerInfo) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerInfo.ProtoReflect.Descriptor instead. +func (*PeerInfo) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{4} +} + +func (x *PeerInfo) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *PeerInfo) GetConns() []*PeerConnInfo { + if x != nil { + return x.Conns + } + return nil +} + +func (x *PeerInfo) GetDefaultConnId() *common.UUID { + if x != nil { + return x.DefaultConnId + } + return nil +} + +func (x *PeerInfo) GetDirectlyConnectedConns() []*common.UUID { + if x != nil { + return x.DirectlyConnectedConns + } + return nil +} + +type ListPeerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPeerRequest) Reset() { + *x = ListPeerRequest{} + mi := &file_api_instance_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPeerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPeerRequest) ProtoMessage() {} + +func (x *ListPeerRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPeerRequest.ProtoReflect.Descriptor instead. +func (*ListPeerRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{5} +} + +func (x *ListPeerRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type ListPeerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerInfos []*PeerInfo `protobuf:"bytes,1,rep,name=peer_infos,json=peerInfos,proto3" json:"peer_infos,omitempty"` + MyInfo *NodeInfo `protobuf:"bytes,2,opt,name=my_info,json=myInfo,proto3" json:"my_info,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPeerResponse) Reset() { + *x = ListPeerResponse{} + mi := &file_api_instance_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPeerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPeerResponse) ProtoMessage() {} + +func (x *ListPeerResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPeerResponse.ProtoReflect.Descriptor instead. +func (*ListPeerResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{6} +} + +func (x *ListPeerResponse) GetPeerInfos() []*PeerInfo { + if x != nil { + return x.PeerInfos + } + return nil +} + +func (x *ListPeerResponse) GetMyInfo() *NodeInfo { + if x != nil { + return x.MyInfo + } + return nil +} + +type Route struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + Ipv4Addr *common.Ipv4Inet `protobuf:"bytes,2,opt,name=ipv4_addr,json=ipv4Addr,proto3" json:"ipv4_addr,omitempty"` + NextHopPeerId uint32 `protobuf:"varint,3,opt,name=next_hop_peer_id,json=nextHopPeerId,proto3" json:"next_hop_peer_id,omitempty"` + Cost int32 `protobuf:"varint,4,opt,name=cost,proto3" json:"cost,omitempty"` + PathLatency int32 `protobuf:"varint,11,opt,name=path_latency,json=pathLatency,proto3" json:"path_latency,omitempty"` + ProxyCidrs []string `protobuf:"bytes,5,rep,name=proxy_cidrs,json=proxyCidrs,proto3" json:"proxy_cidrs,omitempty"` + Hostname string `protobuf:"bytes,6,opt,name=hostname,proto3" json:"hostname,omitempty"` + StunInfo *common.StunInfo `protobuf:"bytes,7,opt,name=stun_info,json=stunInfo,proto3" json:"stun_info,omitempty"` + InstId string `protobuf:"bytes,8,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + Version string `protobuf:"bytes,9,opt,name=version,proto3" json:"version,omitempty"` + FeatureFlag *common.PeerFeatureFlag `protobuf:"bytes,10,opt,name=feature_flag,json=featureFlag,proto3" json:"feature_flag,omitempty"` + NextHopPeerIdLatencyFirst *uint32 `protobuf:"varint,12,opt,name=next_hop_peer_id_latency_first,json=nextHopPeerIdLatencyFirst,proto3,oneof" json:"next_hop_peer_id_latency_first,omitempty"` + CostLatencyFirst *int32 `protobuf:"varint,13,opt,name=cost_latency_first,json=costLatencyFirst,proto3,oneof" json:"cost_latency_first,omitempty"` + PathLatencyLatencyFirst *int32 `protobuf:"varint,14,opt,name=path_latency_latency_first,json=pathLatencyLatencyFirst,proto3,oneof" json:"path_latency_latency_first,omitempty"` + Ipv6Addr *common.Ipv6Inet `protobuf:"bytes,15,opt,name=ipv6_addr,json=ipv6Addr,proto3" json:"ipv6_addr,omitempty"` + PublicIpv6Addr *common.Ipv6Inet `protobuf:"bytes,16,opt,name=public_ipv6_addr,json=publicIpv6Addr,proto3" json:"public_ipv6_addr,omitempty"` + Ipv6PublicAddrPrefix *common.Ipv6Inet `protobuf:"bytes,17,opt,name=ipv6_public_addr_prefix,json=ipv6PublicAddrPrefix,proto3" json:"ipv6_public_addr_prefix,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Route) Reset() { + *x = Route{} + mi := &file_api_instance_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Route) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Route) ProtoMessage() {} + +func (x *Route) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Route.ProtoReflect.Descriptor instead. +func (*Route) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{7} +} + +func (x *Route) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *Route) GetIpv4Addr() *common.Ipv4Inet { + if x != nil { + return x.Ipv4Addr + } + return nil +} + +func (x *Route) GetNextHopPeerId() uint32 { + if x != nil { + return x.NextHopPeerId + } + return 0 +} + +func (x *Route) GetCost() int32 { + if x != nil { + return x.Cost + } + return 0 +} + +func (x *Route) GetPathLatency() int32 { + if x != nil { + return x.PathLatency + } + return 0 +} + +func (x *Route) GetProxyCidrs() []string { + if x != nil { + return x.ProxyCidrs + } + return nil +} + +func (x *Route) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *Route) GetStunInfo() *common.StunInfo { + if x != nil { + return x.StunInfo + } + return nil +} + +func (x *Route) GetInstId() string { + if x != nil { + return x.InstId + } + return "" +} + +func (x *Route) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *Route) GetFeatureFlag() *common.PeerFeatureFlag { + if x != nil { + return x.FeatureFlag + } + return nil +} + +func (x *Route) GetNextHopPeerIdLatencyFirst() uint32 { + if x != nil && x.NextHopPeerIdLatencyFirst != nil { + return *x.NextHopPeerIdLatencyFirst + } + return 0 +} + +func (x *Route) GetCostLatencyFirst() int32 { + if x != nil && x.CostLatencyFirst != nil { + return *x.CostLatencyFirst + } + return 0 +} + +func (x *Route) GetPathLatencyLatencyFirst() int32 { + if x != nil && x.PathLatencyLatencyFirst != nil { + return *x.PathLatencyLatencyFirst + } + return 0 +} + +func (x *Route) GetIpv6Addr() *common.Ipv6Inet { + if x != nil { + return x.Ipv6Addr + } + return nil +} + +func (x *Route) GetPublicIpv6Addr() *common.Ipv6Inet { + if x != nil { + return x.PublicIpv6Addr + } + return nil +} + +func (x *Route) GetIpv6PublicAddrPrefix() *common.Ipv6Inet { + if x != nil { + return x.Ipv6PublicAddrPrefix + } + return nil +} + +type PeerRoutePair struct { + state protoimpl.MessageState `protogen:"open.v1"` + Route *Route `protobuf:"bytes,1,opt,name=route,proto3" json:"route,omitempty"` + Peer *PeerInfo `protobuf:"bytes,2,opt,name=peer,proto3" json:"peer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerRoutePair) Reset() { + *x = PeerRoutePair{} + mi := &file_api_instance_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerRoutePair) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerRoutePair) ProtoMessage() {} + +func (x *PeerRoutePair) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerRoutePair.ProtoReflect.Descriptor instead. +func (*PeerRoutePair) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{8} +} + +func (x *PeerRoutePair) GetRoute() *Route { + if x != nil { + return x.Route + } + return nil +} + +func (x *PeerRoutePair) GetPeer() *PeerInfo { + if x != nil { + return x.Peer + } + return nil +} + +type NodeInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + Ipv4Addr string `protobuf:"bytes,2,opt,name=ipv4_addr,json=ipv4Addr,proto3" json:"ipv4_addr,omitempty"` + ProxyCidrs []string `protobuf:"bytes,3,rep,name=proxy_cidrs,json=proxyCidrs,proto3" json:"proxy_cidrs,omitempty"` + Hostname string `protobuf:"bytes,4,opt,name=hostname,proto3" json:"hostname,omitempty"` + StunInfo *common.StunInfo `protobuf:"bytes,5,opt,name=stun_info,json=stunInfo,proto3" json:"stun_info,omitempty"` + InstId string `protobuf:"bytes,6,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + Listeners []string `protobuf:"bytes,7,rep,name=listeners,proto3" json:"listeners,omitempty"` + Config string `protobuf:"bytes,8,opt,name=config,proto3" json:"config,omitempty"` + Version string `protobuf:"bytes,9,opt,name=version,proto3" json:"version,omitempty"` + FeatureFlag *common.PeerFeatureFlag `protobuf:"bytes,10,opt,name=feature_flag,json=featureFlag,proto3" json:"feature_flag,omitempty"` + IpList *peer_rpc.GetIpListResponse `protobuf:"bytes,11,opt,name=ip_list,json=ipList,proto3" json:"ip_list,omitempty"` + PublicIpv6Addr *common.Ipv6Inet `protobuf:"bytes,12,opt,name=public_ipv6_addr,json=publicIpv6Addr,proto3" json:"public_ipv6_addr,omitempty"` + Ipv6PublicAddrPrefix *common.Ipv6Inet `protobuf:"bytes,13,opt,name=ipv6_public_addr_prefix,json=ipv6PublicAddrPrefix,proto3" json:"ipv6_public_addr_prefix,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NodeInfo) Reset() { + *x = NodeInfo{} + mi := &file_api_instance_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NodeInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeInfo) ProtoMessage() {} + +func (x *NodeInfo) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NodeInfo.ProtoReflect.Descriptor instead. +func (*NodeInfo) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{9} +} + +func (x *NodeInfo) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *NodeInfo) GetIpv4Addr() string { + if x != nil { + return x.Ipv4Addr + } + return "" +} + +func (x *NodeInfo) GetProxyCidrs() []string { + if x != nil { + return x.ProxyCidrs + } + return nil +} + +func (x *NodeInfo) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *NodeInfo) GetStunInfo() *common.StunInfo { + if x != nil { + return x.StunInfo + } + return nil +} + +func (x *NodeInfo) GetInstId() string { + if x != nil { + return x.InstId + } + return "" +} + +func (x *NodeInfo) GetListeners() []string { + if x != nil { + return x.Listeners + } + return nil +} + +func (x *NodeInfo) GetConfig() string { + if x != nil { + return x.Config + } + return "" +} + +func (x *NodeInfo) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *NodeInfo) GetFeatureFlag() *common.PeerFeatureFlag { + if x != nil { + return x.FeatureFlag + } + return nil +} + +func (x *NodeInfo) GetIpList() *peer_rpc.GetIpListResponse { + if x != nil { + return x.IpList + } + return nil +} + +func (x *NodeInfo) GetPublicIpv6Addr() *common.Ipv6Inet { + if x != nil { + return x.PublicIpv6Addr + } + return nil +} + +func (x *NodeInfo) GetIpv6PublicAddrPrefix() *common.Ipv6Inet { + if x != nil { + return x.Ipv6PublicAddrPrefix + } + return nil +} + +type ShowNodeInfoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ShowNodeInfoRequest) Reset() { + *x = ShowNodeInfoRequest{} + mi := &file_api_instance_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ShowNodeInfoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ShowNodeInfoRequest) ProtoMessage() {} + +func (x *ShowNodeInfoRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ShowNodeInfoRequest.ProtoReflect.Descriptor instead. +func (*ShowNodeInfoRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{10} +} + +func (x *ShowNodeInfoRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type ShowNodeInfoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeInfo *NodeInfo `protobuf:"bytes,1,opt,name=node_info,json=nodeInfo,proto3" json:"node_info,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ShowNodeInfoResponse) Reset() { + *x = ShowNodeInfoResponse{} + mi := &file_api_instance_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ShowNodeInfoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ShowNodeInfoResponse) ProtoMessage() {} + +func (x *ShowNodeInfoResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ShowNodeInfoResponse.ProtoReflect.Descriptor instead. +func (*ShowNodeInfoResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{11} +} + +func (x *ShowNodeInfoResponse) GetNodeInfo() *NodeInfo { + if x != nil { + return x.NodeInfo + } + return nil +} + +type PublicIpv6LeaseInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + InstId string `protobuf:"bytes,2,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + LeasedAddr *common.Ipv6Inet `protobuf:"bytes,3,opt,name=leased_addr,json=leasedAddr,proto3" json:"leased_addr,omitempty"` + ValidUntilUnixSeconds int64 `protobuf:"varint,4,opt,name=valid_until_unix_seconds,json=validUntilUnixSeconds,proto3" json:"valid_until_unix_seconds,omitempty"` + Reused bool `protobuf:"varint,5,opt,name=reused,proto3" json:"reused,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PublicIpv6LeaseInfo) Reset() { + *x = PublicIpv6LeaseInfo{} + mi := &file_api_instance_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PublicIpv6LeaseInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PublicIpv6LeaseInfo) ProtoMessage() {} + +func (x *PublicIpv6LeaseInfo) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PublicIpv6LeaseInfo.ProtoReflect.Descriptor instead. +func (*PublicIpv6LeaseInfo) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{12} +} + +func (x *PublicIpv6LeaseInfo) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *PublicIpv6LeaseInfo) GetInstId() string { + if x != nil { + return x.InstId + } + return "" +} + +func (x *PublicIpv6LeaseInfo) GetLeasedAddr() *common.Ipv6Inet { + if x != nil { + return x.LeasedAddr + } + return nil +} + +func (x *PublicIpv6LeaseInfo) GetValidUntilUnixSeconds() int64 { + if x != nil { + return x.ValidUntilUnixSeconds + } + return 0 +} + +func (x *PublicIpv6LeaseInfo) GetReused() bool { + if x != nil { + return x.Reused + } + return false +} + +type ListPublicIpv6InfoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPublicIpv6InfoRequest) Reset() { + *x = ListPublicIpv6InfoRequest{} + mi := &file_api_instance_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPublicIpv6InfoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPublicIpv6InfoRequest) ProtoMessage() {} + +func (x *ListPublicIpv6InfoRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPublicIpv6InfoRequest.ProtoReflect.Descriptor instead. +func (*ListPublicIpv6InfoRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{13} +} + +func (x *ListPublicIpv6InfoRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type ListPublicIpv6InfoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProviderPrefix *common.Ipv6Inet `protobuf:"bytes,1,opt,name=provider_prefix,json=providerPrefix,proto3" json:"provider_prefix,omitempty"` + ProviderLeases []*PublicIpv6LeaseInfo `protobuf:"bytes,2,rep,name=provider_leases,json=providerLeases,proto3" json:"provider_leases,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPublicIpv6InfoResponse) Reset() { + *x = ListPublicIpv6InfoResponse{} + mi := &file_api_instance_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPublicIpv6InfoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPublicIpv6InfoResponse) ProtoMessage() {} + +func (x *ListPublicIpv6InfoResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPublicIpv6InfoResponse.ProtoReflect.Descriptor instead. +func (*ListPublicIpv6InfoResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{14} +} + +func (x *ListPublicIpv6InfoResponse) GetProviderPrefix() *common.Ipv6Inet { + if x != nil { + return x.ProviderPrefix + } + return nil +} + +func (x *ListPublicIpv6InfoResponse) GetProviderLeases() []*PublicIpv6LeaseInfo { + if x != nil { + return x.ProviderLeases + } + return nil +} + +type ListRouteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRouteRequest) Reset() { + *x = ListRouteRequest{} + mi := &file_api_instance_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRouteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRouteRequest) ProtoMessage() {} + +func (x *ListRouteRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRouteRequest.ProtoReflect.Descriptor instead. +func (*ListRouteRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{15} +} + +func (x *ListRouteRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type ListRouteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Routes []*Route `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRouteResponse) Reset() { + *x = ListRouteResponse{} + mi := &file_api_instance_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRouteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRouteResponse) ProtoMessage() {} + +func (x *ListRouteResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRouteResponse.ProtoReflect.Descriptor instead. +func (*ListRouteResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{16} +} + +func (x *ListRouteResponse) GetRoutes() []*Route { + if x != nil { + return x.Routes + } + return nil +} + +type DumpRouteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DumpRouteRequest) Reset() { + *x = DumpRouteRequest{} + mi := &file_api_instance_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DumpRouteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DumpRouteRequest) ProtoMessage() {} + +func (x *DumpRouteRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DumpRouteRequest.ProtoReflect.Descriptor instead. +func (*DumpRouteRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{17} +} + +func (x *DumpRouteRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type DumpRouteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Result string `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DumpRouteResponse) Reset() { + *x = DumpRouteResponse{} + mi := &file_api_instance_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DumpRouteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DumpRouteResponse) ProtoMessage() {} + +func (x *DumpRouteResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DumpRouteResponse.ProtoReflect.Descriptor instead. +func (*DumpRouteResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{18} +} + +func (x *DumpRouteResponse) GetResult() string { + if x != nil { + return x.Result + } + return "" +} + +type ListForeignNetworkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + IncludeTrustedKeys bool `protobuf:"varint,2,opt,name=include_trusted_keys,json=includeTrustedKeys,proto3" json:"include_trusted_keys,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListForeignNetworkRequest) Reset() { + *x = ListForeignNetworkRequest{} + mi := &file_api_instance_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListForeignNetworkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListForeignNetworkRequest) ProtoMessage() {} + +func (x *ListForeignNetworkRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListForeignNetworkRequest.ProtoReflect.Descriptor instead. +func (*ListForeignNetworkRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{19} +} + +func (x *ListForeignNetworkRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +func (x *ListForeignNetworkRequest) GetIncludeTrustedKeys() bool { + if x != nil { + return x.IncludeTrustedKeys + } + return false +} + +type TrustedKeyInfoPb struct { + state protoimpl.MessageState `protogen:"open.v1"` + Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"` + Source TrustedKeySourcePb `protobuf:"varint,2,opt,name=source,proto3,enum=api.instance.TrustedKeySourcePb" json:"source,omitempty"` + ExpiryUnix *int64 `protobuf:"varint,3,opt,name=expiry_unix,json=expiryUnix,proto3,oneof" json:"expiry_unix,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TrustedKeyInfoPb) Reset() { + *x = TrustedKeyInfoPb{} + mi := &file_api_instance_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TrustedKeyInfoPb) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrustedKeyInfoPb) ProtoMessage() {} + +func (x *TrustedKeyInfoPb) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrustedKeyInfoPb.ProtoReflect.Descriptor instead. +func (*TrustedKeyInfoPb) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{20} +} + +func (x *TrustedKeyInfoPb) GetPubkey() []byte { + if x != nil { + return x.Pubkey + } + return nil +} + +func (x *TrustedKeyInfoPb) GetSource() TrustedKeySourcePb { + if x != nil { + return x.Source + } + return TrustedKeySourcePb_TRUSTED_KEY_SOURCE_PB_UNSPECIFIED +} + +func (x *TrustedKeyInfoPb) GetExpiryUnix() int64 { + if x != nil && x.ExpiryUnix != nil { + return *x.ExpiryUnix + } + return 0 +} + +type ForeignNetworkEntryPb struct { + state protoimpl.MessageState `protogen:"open.v1"` + Peers []*PeerInfo `protobuf:"bytes,1,rep,name=peers,proto3" json:"peers,omitempty"` + NetworkSecretDigest []byte `protobuf:"bytes,2,opt,name=network_secret_digest,json=networkSecretDigest,proto3" json:"network_secret_digest,omitempty"` + MyPeerIdForThisNetwork uint32 `protobuf:"varint,3,opt,name=my_peer_id_for_this_network,json=myPeerIdForThisNetwork,proto3" json:"my_peer_id_for_this_network,omitempty"` + TrustedKeys []*TrustedKeyInfoPb `protobuf:"bytes,4,rep,name=trusted_keys,json=trustedKeys,proto3" json:"trusted_keys,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ForeignNetworkEntryPb) Reset() { + *x = ForeignNetworkEntryPb{} + mi := &file_api_instance_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ForeignNetworkEntryPb) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForeignNetworkEntryPb) ProtoMessage() {} + +func (x *ForeignNetworkEntryPb) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ForeignNetworkEntryPb.ProtoReflect.Descriptor instead. +func (*ForeignNetworkEntryPb) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{21} +} + +func (x *ForeignNetworkEntryPb) GetPeers() []*PeerInfo { + if x != nil { + return x.Peers + } + return nil +} + +func (x *ForeignNetworkEntryPb) GetNetworkSecretDigest() []byte { + if x != nil { + return x.NetworkSecretDigest + } + return nil +} + +func (x *ForeignNetworkEntryPb) GetMyPeerIdForThisNetwork() uint32 { + if x != nil { + return x.MyPeerIdForThisNetwork + } + return 0 +} + +func (x *ForeignNetworkEntryPb) GetTrustedKeys() []*TrustedKeyInfoPb { + if x != nil { + return x.TrustedKeys + } + return nil +} + +type ListForeignNetworkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // foreign network in local + ForeignNetworks map[string]*ForeignNetworkEntryPb `protobuf:"bytes,1,rep,name=foreign_networks,json=foreignNetworks,proto3" json:"foreign_networks,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListForeignNetworkResponse) Reset() { + *x = ListForeignNetworkResponse{} + mi := &file_api_instance_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListForeignNetworkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListForeignNetworkResponse) ProtoMessage() {} + +func (x *ListForeignNetworkResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListForeignNetworkResponse.ProtoReflect.Descriptor instead. +func (*ListForeignNetworkResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{22} +} + +func (x *ListForeignNetworkResponse) GetForeignNetworks() map[string]*ForeignNetworkEntryPb { + if x != nil { + return x.ForeignNetworks + } + return nil +} + +type ListGlobalForeignNetworkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListGlobalForeignNetworkRequest) Reset() { + *x = ListGlobalForeignNetworkRequest{} + mi := &file_api_instance_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListGlobalForeignNetworkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListGlobalForeignNetworkRequest) ProtoMessage() {} + +func (x *ListGlobalForeignNetworkRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListGlobalForeignNetworkRequest.ProtoReflect.Descriptor instead. +func (*ListGlobalForeignNetworkRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{23} +} + +func (x *ListGlobalForeignNetworkRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type ListGlobalForeignNetworkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ForeignNetworks map[uint32]*ListGlobalForeignNetworkResponse_ForeignNetworks `protobuf:"bytes,1,rep,name=foreign_networks,json=foreignNetworks,proto3" json:"foreign_networks,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListGlobalForeignNetworkResponse) Reset() { + *x = ListGlobalForeignNetworkResponse{} + mi := &file_api_instance_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListGlobalForeignNetworkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListGlobalForeignNetworkResponse) ProtoMessage() {} + +func (x *ListGlobalForeignNetworkResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListGlobalForeignNetworkResponse.ProtoReflect.Descriptor instead. +func (*ListGlobalForeignNetworkResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{24} +} + +func (x *ListGlobalForeignNetworkResponse) GetForeignNetworks() map[uint32]*ListGlobalForeignNetworkResponse_ForeignNetworks { + if x != nil { + return x.ForeignNetworks + } + return nil +} + +type GetForeignNetworkSummaryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetForeignNetworkSummaryRequest) Reset() { + *x = GetForeignNetworkSummaryRequest{} + mi := &file_api_instance_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetForeignNetworkSummaryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetForeignNetworkSummaryRequest) ProtoMessage() {} + +func (x *GetForeignNetworkSummaryRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetForeignNetworkSummaryRequest.ProtoReflect.Descriptor instead. +func (*GetForeignNetworkSummaryRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{25} +} + +func (x *GetForeignNetworkSummaryRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type GetForeignNetworkSummaryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Summary *peer_rpc.RouteForeignNetworkSummary `protobuf:"bytes,1,opt,name=summary,proto3" json:"summary,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetForeignNetworkSummaryResponse) Reset() { + *x = GetForeignNetworkSummaryResponse{} + mi := &file_api_instance_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetForeignNetworkSummaryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetForeignNetworkSummaryResponse) ProtoMessage() {} + +func (x *GetForeignNetworkSummaryResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetForeignNetworkSummaryResponse.ProtoReflect.Descriptor instead. +func (*GetForeignNetworkSummaryResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{26} +} + +func (x *GetForeignNetworkSummaryResponse) GetSummary() *peer_rpc.RouteForeignNetworkSummary { + if x != nil { + return x.Summary + } + return nil +} + +type Connector struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url *common.Url `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Status ConnectorStatus `protobuf:"varint,2,opt,name=status,proto3,enum=api.instance.ConnectorStatus" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Connector) Reset() { + *x = Connector{} + mi := &file_api_instance_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Connector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Connector) ProtoMessage() {} + +func (x *Connector) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Connector.ProtoReflect.Descriptor instead. +func (*Connector) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{27} +} + +func (x *Connector) GetUrl() *common.Url { + if x != nil { + return x.Url + } + return nil +} + +func (x *Connector) GetStatus() ConnectorStatus { + if x != nil { + return x.Status + } + return ConnectorStatus_CONNECTED +} + +type ListConnectorRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListConnectorRequest) Reset() { + *x = ListConnectorRequest{} + mi := &file_api_instance_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListConnectorRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListConnectorRequest) ProtoMessage() {} + +func (x *ListConnectorRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListConnectorRequest.ProtoReflect.Descriptor instead. +func (*ListConnectorRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{28} +} + +func (x *ListConnectorRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type ListConnectorResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Connectors []*Connector `protobuf:"bytes,1,rep,name=connectors,proto3" json:"connectors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListConnectorResponse) Reset() { + *x = ListConnectorResponse{} + mi := &file_api_instance_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListConnectorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListConnectorResponse) ProtoMessage() {} + +func (x *ListConnectorResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListConnectorResponse.ProtoReflect.Descriptor instead. +func (*ListConnectorResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{29} +} + +func (x *ListConnectorResponse) GetConnectors() []*Connector { + if x != nil { + return x.Connectors + } + return nil +} + +type MappedListener struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url *common.Url `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MappedListener) Reset() { + *x = MappedListener{} + mi := &file_api_instance_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MappedListener) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MappedListener) ProtoMessage() {} + +func (x *MappedListener) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MappedListener.ProtoReflect.Descriptor instead. +func (*MappedListener) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{30} +} + +func (x *MappedListener) GetUrl() *common.Url { + if x != nil { + return x.Url + } + return nil +} + +type ListMappedListenerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMappedListenerRequest) Reset() { + *x = ListMappedListenerRequest{} + mi := &file_api_instance_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMappedListenerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMappedListenerRequest) ProtoMessage() {} + +func (x *ListMappedListenerRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMappedListenerRequest.ProtoReflect.Descriptor instead. +func (*ListMappedListenerRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{31} +} + +func (x *ListMappedListenerRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type ListMappedListenerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mappedlisteners []*MappedListener `protobuf:"bytes,1,rep,name=mappedlisteners,proto3" json:"mappedlisteners,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMappedListenerResponse) Reset() { + *x = ListMappedListenerResponse{} + mi := &file_api_instance_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMappedListenerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMappedListenerResponse) ProtoMessage() {} + +func (x *ListMappedListenerResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMappedListenerResponse.ProtoReflect.Descriptor instead. +func (*ListMappedListenerResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{32} +} + +func (x *ListMappedListenerResponse) GetMappedlisteners() []*MappedListener { + if x != nil { + return x.Mappedlisteners + } + return nil +} + +type VpnPortalInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + VpnType string `protobuf:"bytes,1,opt,name=vpn_type,json=vpnType,proto3" json:"vpn_type,omitempty"` + ClientConfig string `protobuf:"bytes,2,opt,name=client_config,json=clientConfig,proto3" json:"client_config,omitempty"` + ConnectedClients []string `protobuf:"bytes,3,rep,name=connected_clients,json=connectedClients,proto3" json:"connected_clients,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VpnPortalInfo) Reset() { + *x = VpnPortalInfo{} + mi := &file_api_instance_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VpnPortalInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VpnPortalInfo) ProtoMessage() {} + +func (x *VpnPortalInfo) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VpnPortalInfo.ProtoReflect.Descriptor instead. +func (*VpnPortalInfo) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{33} +} + +func (x *VpnPortalInfo) GetVpnType() string { + if x != nil { + return x.VpnType + } + return "" +} + +func (x *VpnPortalInfo) GetClientConfig() string { + if x != nil { + return x.ClientConfig + } + return "" +} + +func (x *VpnPortalInfo) GetConnectedClients() []string { + if x != nil { + return x.ConnectedClients + } + return nil +} + +type GetVpnPortalInfoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetVpnPortalInfoRequest) Reset() { + *x = GetVpnPortalInfoRequest{} + mi := &file_api_instance_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetVpnPortalInfoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetVpnPortalInfoRequest) ProtoMessage() {} + +func (x *GetVpnPortalInfoRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetVpnPortalInfoRequest.ProtoReflect.Descriptor instead. +func (*GetVpnPortalInfoRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{34} +} + +func (x *GetVpnPortalInfoRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type GetVpnPortalInfoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + VpnPortalInfo *VpnPortalInfo `protobuf:"bytes,1,opt,name=vpn_portal_info,json=vpnPortalInfo,proto3" json:"vpn_portal_info,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetVpnPortalInfoResponse) Reset() { + *x = GetVpnPortalInfoResponse{} + mi := &file_api_instance_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetVpnPortalInfoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetVpnPortalInfoResponse) ProtoMessage() {} + +func (x *GetVpnPortalInfoResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetVpnPortalInfoResponse.ProtoReflect.Descriptor instead. +func (*GetVpnPortalInfoResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{35} +} + +func (x *GetVpnPortalInfoResponse) GetVpnPortalInfo() *VpnPortalInfo { + if x != nil { + return x.VpnPortalInfo + } + return nil +} + +type TcpProxyEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Src *common.SocketAddr `protobuf:"bytes,1,opt,name=src,proto3" json:"src,omitempty"` + Dst *common.SocketAddr `protobuf:"bytes,2,opt,name=dst,proto3" json:"dst,omitempty"` + StartTime uint64 `protobuf:"varint,3,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + State TcpProxyEntryState `protobuf:"varint,4,opt,name=state,proto3,enum=api.instance.TcpProxyEntryState" json:"state,omitempty"` + TransportType TcpProxyEntryTransportType `protobuf:"varint,5,opt,name=transport_type,json=transportType,proto3,enum=api.instance.TcpProxyEntryTransportType" json:"transport_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpProxyEntry) Reset() { + *x = TcpProxyEntry{} + mi := &file_api_instance_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpProxyEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpProxyEntry) ProtoMessage() {} + +func (x *TcpProxyEntry) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TcpProxyEntry.ProtoReflect.Descriptor instead. +func (*TcpProxyEntry) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{36} +} + +func (x *TcpProxyEntry) GetSrc() *common.SocketAddr { + if x != nil { + return x.Src + } + return nil +} + +func (x *TcpProxyEntry) GetDst() *common.SocketAddr { + if x != nil { + return x.Dst + } + return nil +} + +func (x *TcpProxyEntry) GetStartTime() uint64 { + if x != nil { + return x.StartTime + } + return 0 +} + +func (x *TcpProxyEntry) GetState() TcpProxyEntryState { + if x != nil { + return x.State + } + return TcpProxyEntryState_Unknown +} + +func (x *TcpProxyEntry) GetTransportType() TcpProxyEntryTransportType { + if x != nil { + return x.TransportType + } + return TcpProxyEntryTransportType_TCP +} + +type ListTcpProxyEntryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTcpProxyEntryRequest) Reset() { + *x = ListTcpProxyEntryRequest{} + mi := &file_api_instance_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTcpProxyEntryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTcpProxyEntryRequest) ProtoMessage() {} + +func (x *ListTcpProxyEntryRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTcpProxyEntryRequest.ProtoReflect.Descriptor instead. +func (*ListTcpProxyEntryRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{37} +} + +func (x *ListTcpProxyEntryRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type ListTcpProxyEntryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entries []*TcpProxyEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTcpProxyEntryResponse) Reset() { + *x = ListTcpProxyEntryResponse{} + mi := &file_api_instance_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTcpProxyEntryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTcpProxyEntryResponse) ProtoMessage() {} + +func (x *ListTcpProxyEntryResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTcpProxyEntryResponse.ProtoReflect.Descriptor instead. +func (*ListTcpProxyEntryResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{38} +} + +func (x *ListTcpProxyEntryResponse) GetEntries() []*TcpProxyEntry { + if x != nil { + return x.Entries + } + return nil +} + +type GetAclStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAclStatsRequest) Reset() { + *x = GetAclStatsRequest{} + mi := &file_api_instance_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAclStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAclStatsRequest) ProtoMessage() {} + +func (x *GetAclStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAclStatsRequest.ProtoReflect.Descriptor instead. +func (*GetAclStatsRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{39} +} + +func (x *GetAclStatsRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type GetAclStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AclStats *acl.AclStats `protobuf:"bytes,1,opt,name=acl_stats,json=aclStats,proto3" json:"acl_stats,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAclStatsResponse) Reset() { + *x = GetAclStatsResponse{} + mi := &file_api_instance_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAclStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAclStatsResponse) ProtoMessage() {} + +func (x *GetAclStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAclStatsResponse.ProtoReflect.Descriptor instead. +func (*GetAclStatsResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{40} +} + +func (x *GetAclStatsResponse) GetAclStats() *acl.AclStats { + if x != nil { + return x.AclStats + } + return nil +} + +type GetWhitelistRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWhitelistRequest) Reset() { + *x = GetWhitelistRequest{} + mi := &file_api_instance_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWhitelistRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWhitelistRequest) ProtoMessage() {} + +func (x *GetWhitelistRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetWhitelistRequest.ProtoReflect.Descriptor instead. +func (*GetWhitelistRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{41} +} + +func (x *GetWhitelistRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type GetWhitelistResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + TcpPorts []string `protobuf:"bytes,1,rep,name=tcp_ports,json=tcpPorts,proto3" json:"tcp_ports,omitempty"` + UdpPorts []string `protobuf:"bytes,2,rep,name=udp_ports,json=udpPorts,proto3" json:"udp_ports,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWhitelistResponse) Reset() { + *x = GetWhitelistResponse{} + mi := &file_api_instance_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWhitelistResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWhitelistResponse) ProtoMessage() {} + +func (x *GetWhitelistResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetWhitelistResponse.ProtoReflect.Descriptor instead. +func (*GetWhitelistResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{42} +} + +func (x *GetWhitelistResponse) GetTcpPorts() []string { + if x != nil { + return x.TcpPorts + } + return nil +} + +func (x *GetWhitelistResponse) GetUdpPorts() []string { + if x != nil { + return x.UdpPorts + } + return nil +} + +type ListPortForwardRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPortForwardRequest) Reset() { + *x = ListPortForwardRequest{} + mi := &file_api_instance_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPortForwardRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPortForwardRequest) ProtoMessage() {} + +func (x *ListPortForwardRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPortForwardRequest.ProtoReflect.Descriptor instead. +func (*ListPortForwardRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{43} +} + +func (x *ListPortForwardRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type ListPortForwardResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Cfgs []*common.PortForwardConfigPb `protobuf:"bytes,1,rep,name=cfgs,proto3" json:"cfgs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPortForwardResponse) Reset() { + *x = ListPortForwardResponse{} + mi := &file_api_instance_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPortForwardResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPortForwardResponse) ProtoMessage() {} + +func (x *ListPortForwardResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPortForwardResponse.ProtoReflect.Descriptor instead. +func (*ListPortForwardResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{44} +} + +func (x *ListPortForwardResponse) GetCfgs() []*common.PortForwardConfigPb { + if x != nil { + return x.Cfgs + } + return nil +} + +type MetricSnapshot struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value uint64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` + Labels map[string]string `protobuf:"bytes,3,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MetricSnapshot) Reset() { + *x = MetricSnapshot{} + mi := &file_api_instance_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MetricSnapshot) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MetricSnapshot) ProtoMessage() {} + +func (x *MetricSnapshot) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MetricSnapshot.ProtoReflect.Descriptor instead. +func (*MetricSnapshot) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{45} +} + +func (x *MetricSnapshot) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *MetricSnapshot) GetValue() uint64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *MetricSnapshot) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +type GetStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetStatsRequest) Reset() { + *x = GetStatsRequest{} + mi := &file_api_instance_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetStatsRequest) ProtoMessage() {} + +func (x *GetStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetStatsRequest.ProtoReflect.Descriptor instead. +func (*GetStatsRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{46} +} + +func (x *GetStatsRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type GetStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metrics []*MetricSnapshot `protobuf:"bytes,1,rep,name=metrics,proto3" json:"metrics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetStatsResponse) Reset() { + *x = GetStatsResponse{} + mi := &file_api_instance_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetStatsResponse) ProtoMessage() {} + +func (x *GetStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetStatsResponse.ProtoReflect.Descriptor instead. +func (*GetStatsResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{47} +} + +func (x *GetStatsResponse) GetMetrics() []*MetricSnapshot { + if x != nil { + return x.Metrics + } + return nil +} + +type GetPrometheusStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPrometheusStatsRequest) Reset() { + *x = GetPrometheusStatsRequest{} + mi := &file_api_instance_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPrometheusStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrometheusStatsRequest) ProtoMessage() {} + +func (x *GetPrometheusStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrometheusStatsRequest.ProtoReflect.Descriptor instead. +func (*GetPrometheusStatsRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{48} +} + +func (x *GetPrometheusStatsRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type GetPrometheusStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + PrometheusText string `protobuf:"bytes,1,opt,name=prometheus_text,json=prometheusText,proto3" json:"prometheus_text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPrometheusStatsResponse) Reset() { + *x = GetPrometheusStatsResponse{} + mi := &file_api_instance_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPrometheusStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrometheusStatsResponse) ProtoMessage() {} + +func (x *GetPrometheusStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrometheusStatsResponse.ProtoReflect.Descriptor instead. +func (*GetPrometheusStatsResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{49} +} + +func (x *GetPrometheusStatsResponse) GetPrometheusText() string { + if x != nil { + return x.PrometheusText + } + return "" +} + +type GenerateCredentialRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Groups []string `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty"` // optional: ACL groups for this credential + AllowRelay bool `protobuf:"varint,2,opt,name=allow_relay,json=allowRelay,proto3" json:"allow_relay,omitempty"` // optional: allow relay through credential node + AllowedProxyCidrs []string `protobuf:"bytes,3,rep,name=allowed_proxy_cidrs,json=allowedProxyCidrs,proto3" json:"allowed_proxy_cidrs,omitempty"` // optional: restrict proxy_cidrs + TtlSeconds int64 `protobuf:"varint,4,opt,name=ttl_seconds,json=ttlSeconds,proto3" json:"ttl_seconds,omitempty"` // must be > 0: credential TTL in seconds (0 / omitted is invalid) + CredentialId *string `protobuf:"bytes,5,opt,name=credential_id,json=credentialId,proto3,oneof" json:"credential_id,omitempty"` // optional: user-specified credential id, reused if already exists + Instance *InstanceIdentifier `protobuf:"bytes,6,opt,name=instance,proto3" json:"instance,omitempty"` // target network instance + Reusable *bool `protobuf:"varint,7,opt,name=reusable,proto3,oneof" json:"reusable,omitempty"` // default true: allow multiple peers to reuse this credential + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GenerateCredentialRequest) Reset() { + *x = GenerateCredentialRequest{} + mi := &file_api_instance_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GenerateCredentialRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerateCredentialRequest) ProtoMessage() {} + +func (x *GenerateCredentialRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GenerateCredentialRequest.ProtoReflect.Descriptor instead. +func (*GenerateCredentialRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{50} +} + +func (x *GenerateCredentialRequest) GetGroups() []string { + if x != nil { + return x.Groups + } + return nil +} + +func (x *GenerateCredentialRequest) GetAllowRelay() bool { + if x != nil { + return x.AllowRelay + } + return false +} + +func (x *GenerateCredentialRequest) GetAllowedProxyCidrs() []string { + if x != nil { + return x.AllowedProxyCidrs + } + return nil +} + +func (x *GenerateCredentialRequest) GetTtlSeconds() int64 { + if x != nil { + return x.TtlSeconds + } + return 0 +} + +func (x *GenerateCredentialRequest) GetCredentialId() string { + if x != nil && x.CredentialId != nil { + return *x.CredentialId + } + return "" +} + +func (x *GenerateCredentialRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +func (x *GenerateCredentialRequest) GetReusable() bool { + if x != nil && x.Reusable != nil { + return *x.Reusable + } + return false +} + +type GenerateCredentialResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + CredentialId string `protobuf:"bytes,1,opt,name=credential_id,json=credentialId,proto3" json:"credential_id,omitempty"` // UUID + CredentialSecret string `protobuf:"bytes,2,opt,name=credential_secret,json=credentialSecret,proto3" json:"credential_secret,omitempty"` // private key base64 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GenerateCredentialResponse) Reset() { + *x = GenerateCredentialResponse{} + mi := &file_api_instance_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GenerateCredentialResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerateCredentialResponse) ProtoMessage() {} + +func (x *GenerateCredentialResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GenerateCredentialResponse.ProtoReflect.Descriptor instead. +func (*GenerateCredentialResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{51} +} + +func (x *GenerateCredentialResponse) GetCredentialId() string { + if x != nil { + return x.CredentialId + } + return "" +} + +func (x *GenerateCredentialResponse) GetCredentialSecret() string { + if x != nil { + return x.CredentialSecret + } + return "" +} + +type RevokeCredentialRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + CredentialId string `protobuf:"bytes,1,opt,name=credential_id,json=credentialId,proto3" json:"credential_id,omitempty"` + Instance *InstanceIdentifier `protobuf:"bytes,2,opt,name=instance,proto3" json:"instance,omitempty"` // target network instance + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeCredentialRequest) Reset() { + *x = RevokeCredentialRequest{} + mi := &file_api_instance_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeCredentialRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeCredentialRequest) ProtoMessage() {} + +func (x *RevokeCredentialRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeCredentialRequest.ProtoReflect.Descriptor instead. +func (*RevokeCredentialRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{52} +} + +func (x *RevokeCredentialRequest) GetCredentialId() string { + if x != nil { + return x.CredentialId + } + return "" +} + +func (x *RevokeCredentialRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type RevokeCredentialResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeCredentialResponse) Reset() { + *x = RevokeCredentialResponse{} + mi := &file_api_instance_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeCredentialResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeCredentialResponse) ProtoMessage() {} + +func (x *RevokeCredentialResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeCredentialResponse.ProtoReflect.Descriptor instead. +func (*RevokeCredentialResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{53} +} + +func (x *RevokeCredentialResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type ListCredentialsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instance *InstanceIdentifier `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` // target network instance + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListCredentialsRequest) Reset() { + *x = ListCredentialsRequest{} + mi := &file_api_instance_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListCredentialsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListCredentialsRequest) ProtoMessage() {} + +func (x *ListCredentialsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[54] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListCredentialsRequest.ProtoReflect.Descriptor instead. +func (*ListCredentialsRequest) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{54} +} + +func (x *ListCredentialsRequest) GetInstance() *InstanceIdentifier { + if x != nil { + return x.Instance + } + return nil +} + +type CredentialInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + CredentialId string `protobuf:"bytes,1,opt,name=credential_id,json=credentialId,proto3" json:"credential_id,omitempty"` // UUID + Groups []string `protobuf:"bytes,2,rep,name=groups,proto3" json:"groups,omitempty"` + AllowRelay bool `protobuf:"varint,3,opt,name=allow_relay,json=allowRelay,proto3" json:"allow_relay,omitempty"` + ExpiryUnix int64 `protobuf:"varint,4,opt,name=expiry_unix,json=expiryUnix,proto3" json:"expiry_unix,omitempty"` + AllowedProxyCidrs []string `protobuf:"bytes,5,rep,name=allowed_proxy_cidrs,json=allowedProxyCidrs,proto3" json:"allowed_proxy_cidrs,omitempty"` + Reusable *bool `protobuf:"varint,6,opt,name=reusable,proto3,oneof" json:"reusable,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CredentialInfo) Reset() { + *x = CredentialInfo{} + mi := &file_api_instance_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CredentialInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CredentialInfo) ProtoMessage() {} + +func (x *CredentialInfo) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CredentialInfo.ProtoReflect.Descriptor instead. +func (*CredentialInfo) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{55} +} + +func (x *CredentialInfo) GetCredentialId() string { + if x != nil { + return x.CredentialId + } + return "" +} + +func (x *CredentialInfo) GetGroups() []string { + if x != nil { + return x.Groups + } + return nil +} + +func (x *CredentialInfo) GetAllowRelay() bool { + if x != nil { + return x.AllowRelay + } + return false +} + +func (x *CredentialInfo) GetExpiryUnix() int64 { + if x != nil { + return x.ExpiryUnix + } + return 0 +} + +func (x *CredentialInfo) GetAllowedProxyCidrs() []string { + if x != nil { + return x.AllowedProxyCidrs + } + return nil +} + +func (x *CredentialInfo) GetReusable() bool { + if x != nil && x.Reusable != nil { + return *x.Reusable + } + return false +} + +type ListCredentialsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Credentials []*CredentialInfo `protobuf:"bytes,1,rep,name=credentials,proto3" json:"credentials,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListCredentialsResponse) Reset() { + *x = ListCredentialsResponse{} + mi := &file_api_instance_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListCredentialsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListCredentialsResponse) ProtoMessage() {} + +func (x *ListCredentialsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[56] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListCredentialsResponse.ProtoReflect.Descriptor instead. +func (*ListCredentialsResponse) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{56} +} + +func (x *ListCredentialsResponse) GetCredentials() []*CredentialInfo { + if x != nil { + return x.Credentials + } + return nil +} + +type InstanceIdentifier_InstanceSelector struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name *string `protobuf:"bytes,1,opt,name=name,proto3,oneof" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstanceIdentifier_InstanceSelector) Reset() { + *x = InstanceIdentifier_InstanceSelector{} + mi := &file_api_instance_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstanceIdentifier_InstanceSelector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstanceIdentifier_InstanceSelector) ProtoMessage() {} + +func (x *InstanceIdentifier_InstanceSelector) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[57] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InstanceIdentifier_InstanceSelector.ProtoReflect.Descriptor instead. +func (*InstanceIdentifier_InstanceSelector) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *InstanceIdentifier_InstanceSelector) GetName() string { + if x != nil && x.Name != nil { + return *x.Name + } + return "" +} + +// foreign network in the entire network +type ListGlobalForeignNetworkResponse_OneForeignNetwork struct { + state protoimpl.MessageState `protogen:"open.v1"` + NetworkName string `protobuf:"bytes,1,opt,name=network_name,json=networkName,proto3" json:"network_name,omitempty"` + PeerIds []uint32 `protobuf:"varint,2,rep,packed,name=peer_ids,json=peerIds,proto3" json:"peer_ids,omitempty"` + LastUpdated string `protobuf:"bytes,3,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"` + Version uint32 `protobuf:"varint,4,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListGlobalForeignNetworkResponse_OneForeignNetwork) Reset() { + *x = ListGlobalForeignNetworkResponse_OneForeignNetwork{} + mi := &file_api_instance_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListGlobalForeignNetworkResponse_OneForeignNetwork) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListGlobalForeignNetworkResponse_OneForeignNetwork) ProtoMessage() {} + +func (x *ListGlobalForeignNetworkResponse_OneForeignNetwork) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[59] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListGlobalForeignNetworkResponse_OneForeignNetwork.ProtoReflect.Descriptor instead. +func (*ListGlobalForeignNetworkResponse_OneForeignNetwork) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{24, 0} +} + +func (x *ListGlobalForeignNetworkResponse_OneForeignNetwork) GetNetworkName() string { + if x != nil { + return x.NetworkName + } + return "" +} + +func (x *ListGlobalForeignNetworkResponse_OneForeignNetwork) GetPeerIds() []uint32 { + if x != nil { + return x.PeerIds + } + return nil +} + +func (x *ListGlobalForeignNetworkResponse_OneForeignNetwork) GetLastUpdated() string { + if x != nil { + return x.LastUpdated + } + return "" +} + +func (x *ListGlobalForeignNetworkResponse_OneForeignNetwork) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +type ListGlobalForeignNetworkResponse_ForeignNetworks struct { + state protoimpl.MessageState `protogen:"open.v1"` + ForeignNetworks []*ListGlobalForeignNetworkResponse_OneForeignNetwork `protobuf:"bytes,1,rep,name=foreign_networks,json=foreignNetworks,proto3" json:"foreign_networks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListGlobalForeignNetworkResponse_ForeignNetworks) Reset() { + *x = ListGlobalForeignNetworkResponse_ForeignNetworks{} + mi := &file_api_instance_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListGlobalForeignNetworkResponse_ForeignNetworks) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListGlobalForeignNetworkResponse_ForeignNetworks) ProtoMessage() {} + +func (x *ListGlobalForeignNetworkResponse_ForeignNetworks) ProtoReflect() protoreflect.Message { + mi := &file_api_instance_proto_msgTypes[60] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListGlobalForeignNetworkResponse_ForeignNetworks.ProtoReflect.Descriptor instead. +func (*ListGlobalForeignNetworkResponse_ForeignNetworks) Descriptor() ([]byte, []int) { + return file_api_instance_proto_rawDescGZIP(), []int{24, 1} +} + +func (x *ListGlobalForeignNetworkResponse_ForeignNetworks) GetForeignNetworks() []*ListGlobalForeignNetworkResponse_OneForeignNetwork { + if x != nil { + return x.ForeignNetworks + } + return nil +} + +var File_api_instance_proto protoreflect.FileDescriptor + +const file_api_instance_proto_rawDesc = "" + + "\n" + + "\x12api_instance.proto\x12\fapi.instance\x1a\fcommon.proto\x1a\x0epeer_rpc.proto\x1a\tacl.proto\"\xd8\x01\n" + + "\x12InstanceIdentifier\x12\x1e\n" + + "\x02id\x18\x01 \x01(\v2\f.common.UUIDH\x00R\x02id\x12`\n" + + "\x11instance_selector\x18\x02 \x01(\v21.api.instance.InstanceIdentifier.InstanceSelectorH\x00R\x10instanceSelector\x1a4\n" + + "\x10InstanceSelector\x12\x17\n" + + "\x04name\x18\x01 \x01(\tH\x00R\x04name\x88\x01\x01B\a\n" + + "\x05_nameB\n" + + "\n" + + "\bselector\"6\n" + + "\x06Status\x12\x12\n" + + "\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"\xa2\x01\n" + + "\rPeerConnStats\x12\x19\n" + + "\brx_bytes\x18\x01 \x01(\x04R\arxBytes\x12\x19\n" + + "\btx_bytes\x18\x02 \x01(\x04R\atxBytes\x12\x1d\n" + + "\n" + + "rx_packets\x18\x03 \x01(\x04R\trxPackets\x12\x1d\n" + + "\n" + + "tx_packets\x18\x04 \x01(\x04R\ttxPackets\x12\x1d\n" + + "\n" + + "latency_us\x18\x05 \x01(\x04R\tlatencyUs\"\xdc\x04\n" + + "\fPeerConnInfo\x12\x17\n" + + "\aconn_id\x18\x01 \x01(\tR\x06connId\x12\x1c\n" + + "\n" + + "my_peer_id\x18\x02 \x01(\rR\bmyPeerId\x12\x17\n" + + "\apeer_id\x18\x03 \x01(\rR\x06peerId\x12\x1a\n" + + "\bfeatures\x18\x04 \x03(\tR\bfeatures\x12*\n" + + "\x06tunnel\x18\x05 \x01(\v2\x12.common.TunnelInfoR\x06tunnel\x121\n" + + "\x05stats\x18\x06 \x01(\v2\x1b.api.instance.PeerConnStatsR\x05stats\x12\x1b\n" + + "\tloss_rate\x18\a \x01(\x02R\blossRate\x12\x1b\n" + + "\tis_client\x18\b \x01(\bR\bisClient\x12!\n" + + "\fnetwork_name\x18\t \x01(\tR\vnetworkName\x12\x1b\n" + + "\tis_closed\x18\n" + + " \x01(\bR\bisClosed\x129\n" + + "\x19noise_local_static_pubkey\x18\v \x01(\fR\x16noiseLocalStaticPubkey\x12;\n" + + "\x1anoise_remote_static_pubkey\x18\f \x01(\fR\x17noiseRemoteStaticPubkey\x12E\n" + + "\x11secure_auth_level\x18\r \x01(\x0e2\x19.peer_rpc.SecureAuthLevelR\x0fsecureAuthLevel\x12H\n" + + "\x12peer_identity_type\x18\x0e \x01(\x0e2\x1a.peer_rpc.PeerIdentityTypeR\x10peerIdentityType\"\xd3\x01\n" + + "\bPeerInfo\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x120\n" + + "\x05conns\x18\x02 \x03(\v2\x1a.api.instance.PeerConnInfoR\x05conns\x124\n" + + "\x0fdefault_conn_id\x18\x03 \x01(\v2\f.common.UUIDR\rdefaultConnId\x12F\n" + + "\x18directly_connected_conns\x18\x04 \x03(\v2\f.common.UUIDR\x16directlyConnectedConns\"O\n" + + "\x0fListPeerRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"z\n" + + "\x10ListPeerResponse\x125\n" + + "\n" + + "peer_infos\x18\x01 \x03(\v2\x16.api.instance.PeerInfoR\tpeerInfos\x12/\n" + + "\amy_info\x18\x02 \x01(\v2\x16.api.instance.NodeInfoR\x06myInfo\"\xd4\x06\n" + + "\x05Route\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12-\n" + + "\tipv4_addr\x18\x02 \x01(\v2\x10.common.Ipv4InetR\bipv4Addr\x12'\n" + + "\x10next_hop_peer_id\x18\x03 \x01(\rR\rnextHopPeerId\x12\x12\n" + + "\x04cost\x18\x04 \x01(\x05R\x04cost\x12!\n" + + "\fpath_latency\x18\v \x01(\x05R\vpathLatency\x12\x1f\n" + + "\vproxy_cidrs\x18\x05 \x03(\tR\n" + + "proxyCidrs\x12\x1a\n" + + "\bhostname\x18\x06 \x01(\tR\bhostname\x12-\n" + + "\tstun_info\x18\a \x01(\v2\x10.common.StunInfoR\bstunInfo\x12\x17\n" + + "\ainst_id\x18\b \x01(\tR\x06instId\x12\x18\n" + + "\aversion\x18\t \x01(\tR\aversion\x12:\n" + + "\ffeature_flag\x18\n" + + " \x01(\v2\x17.common.PeerFeatureFlagR\vfeatureFlag\x12F\n" + + "\x1enext_hop_peer_id_latency_first\x18\f \x01(\rH\x00R\x19nextHopPeerIdLatencyFirst\x88\x01\x01\x121\n" + + "\x12cost_latency_first\x18\r \x01(\x05H\x01R\x10costLatencyFirst\x88\x01\x01\x12@\n" + + "\x1apath_latency_latency_first\x18\x0e \x01(\x05H\x02R\x17pathLatencyLatencyFirst\x88\x01\x01\x12-\n" + + "\tipv6_addr\x18\x0f \x01(\v2\x10.common.Ipv6InetR\bipv6Addr\x12:\n" + + "\x10public_ipv6_addr\x18\x10 \x01(\v2\x10.common.Ipv6InetR\x0epublicIpv6Addr\x12G\n" + + "\x17ipv6_public_addr_prefix\x18\x11 \x01(\v2\x10.common.Ipv6InetR\x14ipv6PublicAddrPrefixB!\n" + + "\x1f_next_hop_peer_id_latency_firstB\x15\n" + + "\x13_cost_latency_firstB\x1d\n" + + "\x1b_path_latency_latency_first\"f\n" + + "\rPeerRoutePair\x12)\n" + + "\x05route\x18\x01 \x01(\v2\x13.api.instance.RouteR\x05route\x12*\n" + + "\x04peer\x18\x02 \x01(\v2\x16.api.instance.PeerInfoR\x04peer\"\x8c\x04\n" + + "\bNodeInfo\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12\x1b\n" + + "\tipv4_addr\x18\x02 \x01(\tR\bipv4Addr\x12\x1f\n" + + "\vproxy_cidrs\x18\x03 \x03(\tR\n" + + "proxyCidrs\x12\x1a\n" + + "\bhostname\x18\x04 \x01(\tR\bhostname\x12-\n" + + "\tstun_info\x18\x05 \x01(\v2\x10.common.StunInfoR\bstunInfo\x12\x17\n" + + "\ainst_id\x18\x06 \x01(\tR\x06instId\x12\x1c\n" + + "\tlisteners\x18\a \x03(\tR\tlisteners\x12\x16\n" + + "\x06config\x18\b \x01(\tR\x06config\x12\x18\n" + + "\aversion\x18\t \x01(\tR\aversion\x12:\n" + + "\ffeature_flag\x18\n" + + " \x01(\v2\x17.common.PeerFeatureFlagR\vfeatureFlag\x124\n" + + "\aip_list\x18\v \x01(\v2\x1b.peer_rpc.GetIpListResponseR\x06ipList\x12:\n" + + "\x10public_ipv6_addr\x18\f \x01(\v2\x10.common.Ipv6InetR\x0epublicIpv6Addr\x12G\n" + + "\x17ipv6_public_addr_prefix\x18\r \x01(\v2\x10.common.Ipv6InetR\x14ipv6PublicAddrPrefix\"S\n" + + "\x13ShowNodeInfoRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"K\n" + + "\x14ShowNodeInfoResponse\x123\n" + + "\tnode_info\x18\x01 \x01(\v2\x16.api.instance.NodeInfoR\bnodeInfo\"\xcb\x01\n" + + "\x13PublicIpv6LeaseInfo\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12\x17\n" + + "\ainst_id\x18\x02 \x01(\tR\x06instId\x121\n" + + "\vleased_addr\x18\x03 \x01(\v2\x10.common.Ipv6InetR\n" + + "leasedAddr\x127\n" + + "\x18valid_until_unix_seconds\x18\x04 \x01(\x03R\x15validUntilUnixSeconds\x12\x16\n" + + "\x06reused\x18\x05 \x01(\bR\x06reused\"Y\n" + + "\x19ListPublicIpv6InfoRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"\xa3\x01\n" + + "\x1aListPublicIpv6InfoResponse\x129\n" + + "\x0fprovider_prefix\x18\x01 \x01(\v2\x10.common.Ipv6InetR\x0eproviderPrefix\x12J\n" + + "\x0fprovider_leases\x18\x02 \x03(\v2!.api.instance.PublicIpv6LeaseInfoR\x0eproviderLeases\"P\n" + + "\x10ListRouteRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"@\n" + + "\x11ListRouteResponse\x12+\n" + + "\x06routes\x18\x01 \x03(\v2\x13.api.instance.RouteR\x06routes\"P\n" + + "\x10DumpRouteRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"+\n" + + "\x11DumpRouteResponse\x12\x16\n" + + "\x06result\x18\x01 \x01(\tR\x06result\"\x8b\x01\n" + + "\x19ListForeignNetworkRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\x120\n" + + "\x14include_trusted_keys\x18\x02 \x01(\bR\x12includeTrustedKeys\"\x9a\x01\n" + + "\x10TrustedKeyInfoPb\x12\x16\n" + + "\x06pubkey\x18\x01 \x01(\fR\x06pubkey\x128\n" + + "\x06source\x18\x02 \x01(\x0e2 .api.instance.TrustedKeySourcePbR\x06source\x12$\n" + + "\vexpiry_unix\x18\x03 \x01(\x03H\x00R\n" + + "expiryUnix\x88\x01\x01B\x0e\n" + + "\f_expiry_unix\"\xf9\x01\n" + + "\x15ForeignNetworkEntryPb\x12,\n" + + "\x05peers\x18\x01 \x03(\v2\x16.api.instance.PeerInfoR\x05peers\x122\n" + + "\x15network_secret_digest\x18\x02 \x01(\fR\x13networkSecretDigest\x12;\n" + + "\x1bmy_peer_id_for_this_network\x18\x03 \x01(\rR\x16myPeerIdForThisNetwork\x12A\n" + + "\ftrusted_keys\x18\x04 \x03(\v2\x1e.api.instance.TrustedKeyInfoPbR\vtrustedKeys\"\xef\x01\n" + + "\x1aListForeignNetworkResponse\x12h\n" + + "\x10foreign_networks\x18\x01 \x03(\v2=.api.instance.ListForeignNetworkResponse.ForeignNetworksEntryR\x0fforeignNetworks\x1ag\n" + + "\x14ForeignNetworksEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x129\n" + + "\x05value\x18\x02 \x01(\v2#.api.instance.ForeignNetworkEntryPbR\x05value:\x028\x01\"_\n" + + "\x1fListGlobalForeignNetworkRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"\xa8\x04\n" + + " ListGlobalForeignNetworkResponse\x12n\n" + + "\x10foreign_networks\x18\x01 \x03(\v2C.api.instance.ListGlobalForeignNetworkResponse.ForeignNetworksEntryR\x0fforeignNetworks\x1a\x8e\x01\n" + + "\x11OneForeignNetwork\x12!\n" + + "\fnetwork_name\x18\x01 \x01(\tR\vnetworkName\x12\x19\n" + + "\bpeer_ids\x18\x02 \x03(\rR\apeerIds\x12!\n" + + "\flast_updated\x18\x03 \x01(\tR\vlastUpdated\x12\x18\n" + + "\aversion\x18\x04 \x01(\rR\aversion\x1a~\n" + + "\x0fForeignNetworks\x12k\n" + + "\x10foreign_networks\x18\x01 \x03(\v2@.api.instance.ListGlobalForeignNetworkResponse.OneForeignNetworkR\x0fforeignNetworks\x1a\x82\x01\n" + + "\x14ForeignNetworksEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\rR\x03key\x12T\n" + + "\x05value\x18\x02 \x01(\v2>.api.instance.ListGlobalForeignNetworkResponse.ForeignNetworksR\x05value:\x028\x01\"_\n" + + "\x1fGetForeignNetworkSummaryRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"b\n" + + " GetForeignNetworkSummaryResponse\x12>\n" + + "\asummary\x18\x01 \x01(\v2$.peer_rpc.RouteForeignNetworkSummaryR\asummary\"a\n" + + "\tConnector\x12\x1d\n" + + "\x03url\x18\x01 \x01(\v2\v.common.UrlR\x03url\x125\n" + + "\x06status\x18\x02 \x01(\x0e2\x1d.api.instance.ConnectorStatusR\x06status\"T\n" + + "\x14ListConnectorRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"P\n" + + "\x15ListConnectorResponse\x127\n" + + "\n" + + "connectors\x18\x01 \x03(\v2\x17.api.instance.ConnectorR\n" + + "connectors\"/\n" + + "\x0eMappedListener\x12\x1d\n" + + "\x03url\x18\x01 \x01(\v2\v.common.UrlR\x03url\"Y\n" + + "\x19ListMappedListenerRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"d\n" + + "\x1aListMappedListenerResponse\x12F\n" + + "\x0fmappedlisteners\x18\x01 \x03(\v2\x1c.api.instance.MappedListenerR\x0fmappedlisteners\"|\n" + + "\rVpnPortalInfo\x12\x19\n" + + "\bvpn_type\x18\x01 \x01(\tR\avpnType\x12#\n" + + "\rclient_config\x18\x02 \x01(\tR\fclientConfig\x12+\n" + + "\x11connected_clients\x18\x03 \x03(\tR\x10connectedClients\"W\n" + + "\x17GetVpnPortalInfoRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"_\n" + + "\x18GetVpnPortalInfoResponse\x12C\n" + + "\x0fvpn_portal_info\x18\x01 \x01(\v2\x1b.api.instance.VpnPortalInfoR\rvpnPortalInfo\"\x83\x02\n" + + "\rTcpProxyEntry\x12$\n" + + "\x03src\x18\x01 \x01(\v2\x12.common.SocketAddrR\x03src\x12$\n" + + "\x03dst\x18\x02 \x01(\v2\x12.common.SocketAddrR\x03dst\x12\x1d\n" + + "\n" + + "start_time\x18\x03 \x01(\x04R\tstartTime\x126\n" + + "\x05state\x18\x04 \x01(\x0e2 .api.instance.TcpProxyEntryStateR\x05state\x12O\n" + + "\x0etransport_type\x18\x05 \x01(\x0e2(.api.instance.TcpProxyEntryTransportTypeR\rtransportType\"X\n" + + "\x18ListTcpProxyEntryRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"R\n" + + "\x19ListTcpProxyEntryResponse\x125\n" + + "\aentries\x18\x01 \x03(\v2\x1b.api.instance.TcpProxyEntryR\aentries\"R\n" + + "\x12GetAclStatsRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"A\n" + + "\x13GetAclStatsResponse\x12*\n" + + "\tacl_stats\x18\x01 \x01(\v2\r.acl.AclStatsR\baclStats\"S\n" + + "\x13GetWhitelistRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"P\n" + + "\x14GetWhitelistResponse\x12\x1b\n" + + "\ttcp_ports\x18\x01 \x03(\tR\btcpPorts\x12\x1b\n" + + "\tudp_ports\x18\x02 \x03(\tR\budpPorts\"V\n" + + "\x16ListPortForwardRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"J\n" + + "\x17ListPortForwardResponse\x12/\n" + + "\x04cfgs\x18\x01 \x03(\v2\x1b.common.PortForwardConfigPbR\x04cfgs\"\xb7\x01\n" + + "\x0eMetricSnapshot\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05value\x18\x02 \x01(\x04R\x05value\x12@\n" + + "\x06labels\x18\x03 \x03(\v2(.api.instance.MetricSnapshot.LabelsEntryR\x06labels\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"O\n" + + "\x0fGetStatsRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"J\n" + + "\x10GetStatsResponse\x126\n" + + "\ametrics\x18\x01 \x03(\v2\x1c.api.instance.MetricSnapshotR\ametrics\"Y\n" + + "\x19GetPrometheusStatsRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"E\n" + + "\x1aGetPrometheusStatsResponse\x12'\n" + + "\x0fprometheus_text\x18\x01 \x01(\tR\x0eprometheusText\"\xcd\x02\n" + + "\x19GenerateCredentialRequest\x12\x16\n" + + "\x06groups\x18\x01 \x03(\tR\x06groups\x12\x1f\n" + + "\vallow_relay\x18\x02 \x01(\bR\n" + + "allowRelay\x12.\n" + + "\x13allowed_proxy_cidrs\x18\x03 \x03(\tR\x11allowedProxyCidrs\x12\x1f\n" + + "\vttl_seconds\x18\x04 \x01(\x03R\n" + + "ttlSeconds\x12(\n" + + "\rcredential_id\x18\x05 \x01(\tH\x00R\fcredentialId\x88\x01\x01\x12<\n" + + "\binstance\x18\x06 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\x12\x1f\n" + + "\breusable\x18\a \x01(\bH\x01R\breusable\x88\x01\x01B\x10\n" + + "\x0e_credential_idB\v\n" + + "\t_reusable\"n\n" + + "\x1aGenerateCredentialResponse\x12#\n" + + "\rcredential_id\x18\x01 \x01(\tR\fcredentialId\x12+\n" + + "\x11credential_secret\x18\x02 \x01(\tR\x10credentialSecret\"|\n" + + "\x17RevokeCredentialRequest\x12#\n" + + "\rcredential_id\x18\x01 \x01(\tR\fcredentialId\x12<\n" + + "\binstance\x18\x02 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"4\n" + + "\x18RevokeCredentialResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\"V\n" + + "\x16ListCredentialsRequest\x12<\n" + + "\binstance\x18\x01 \x01(\v2 .api.instance.InstanceIdentifierR\binstance\"\xed\x01\n" + + "\x0eCredentialInfo\x12#\n" + + "\rcredential_id\x18\x01 \x01(\tR\fcredentialId\x12\x16\n" + + "\x06groups\x18\x02 \x03(\tR\x06groups\x12\x1f\n" + + "\vallow_relay\x18\x03 \x01(\bR\n" + + "allowRelay\x12\x1f\n" + + "\vexpiry_unix\x18\x04 \x01(\x03R\n" + + "expiryUnix\x12.\n" + + "\x13allowed_proxy_cidrs\x18\x05 \x03(\tR\x11allowedProxyCidrs\x12\x1f\n" + + "\breusable\x18\x06 \x01(\bH\x00R\breusable\x88\x01\x01B\v\n" + + "\t_reusable\"Y\n" + + "\x17ListCredentialsResponse\x12>\n" + + "\vcredentials\x18\x01 \x03(\v2\x1c.api.instance.CredentialInfoR\vcredentials*\x8b\x01\n" + + "\x12TrustedKeySourcePb\x12%\n" + + "!TRUSTED_KEY_SOURCE_PB_UNSPECIFIED\x10\x00\x12#\n" + + "\x1fTRUSTED_KEY_SOURCE_PB_OSPF_NODE\x10\x01\x12)\n" + + "%TRUSTED_KEY_SOURCE_PB_OSPF_CREDENTIAL\x10\x02*B\n" + + "\x0fConnectorStatus\x12\r\n" + + "\tCONNECTED\x10\x00\x12\x10\n" + + "\fDISCONNECTED\x10\x01\x12\x0e\n" + + "\n" + + "CONNECTING\x10\x02*8\n" + + "\x1aTcpProxyEntryTransportType\x12\a\n" + + "\x03TCP\x10\x00\x12\a\n" + + "\x03KCP\x10\x01\x12\b\n" + + "\x04QUIC\x10\x02*\x80\x01\n" + + "\x12TcpProxyEntryState\x12\v\n" + + "\aUnknown\x10\x00\x12\x0f\n" + + "\vSynReceived\x10\x01\x12\x11\n" + + "\rConnectingDst\x10\x02\x12\r\n" + + "\tConnected\x10\x03\x12\n" + + "\n" + + "\x06Closed\x10\x04\x12\x0e\n" + + "\n" + + "ClosingSrc\x10\x05\x12\x0e\n" + + "\n" + + "ClosingDst\x10\x062\x95\x06\n" + + "\rPeerManageRpc\x12I\n" + + "\bListPeer\x12\x1d.api.instance.ListPeerRequest\x1a\x1e.api.instance.ListPeerResponse\x12g\n" + + "\x12ListPublicIpv6Info\x12'.api.instance.ListPublicIpv6InfoRequest\x1a(.api.instance.ListPublicIpv6InfoResponse\x12L\n" + + "\tListRoute\x12\x1e.api.instance.ListRouteRequest\x1a\x1f.api.instance.ListRouteResponse\x12L\n" + + "\tDumpRoute\x12\x1e.api.instance.DumpRouteRequest\x1a\x1f.api.instance.DumpRouteResponse\x12g\n" + + "\x12ListForeignNetwork\x12'.api.instance.ListForeignNetworkRequest\x1a(.api.instance.ListForeignNetworkResponse\x12y\n" + + "\x18ListGlobalForeignNetwork\x12-.api.instance.ListGlobalForeignNetworkRequest\x1a..api.instance.ListGlobalForeignNetworkResponse\x12U\n" + + "\fShowNodeInfo\x12!.api.instance.ShowNodeInfoRequest\x1a\".api.instance.ShowNodeInfoResponse\x12y\n" + + "\x18GetForeignNetworkSummary\x12-.api.instance.GetForeignNetworkSummaryRequest\x1a..api.instance.GetForeignNetworkSummaryResponse2n\n" + + "\x12ConnectorManageRpc\x12X\n" + + "\rListConnector\x12\".api.instance.ListConnectorRequest\x1a#.api.instance.ListConnectorResponse2\x82\x01\n" + + "\x17MappedListenerManageRpc\x12g\n" + + "\x12ListMappedListener\x12'.api.instance.ListMappedListenerRequest\x1a(.api.instance.ListMappedListenerResponse2q\n" + + "\fVpnPortalRpc\x12a\n" + + "\x10GetVpnPortalInfo\x12%.api.instance.GetVpnPortalInfoRequest\x1a&.api.instance.GetVpnPortalInfoResponse2s\n" + + "\vTcpProxyRpc\x12d\n" + + "\x11ListTcpProxyEntry\x12&.api.instance.ListTcpProxyEntryRequest\x1a'.api.instance.ListTcpProxyEntryResponse2\xb9\x01\n" + + "\fAclManageRpc\x12R\n" + + "\vGetAclStats\x12 .api.instance.GetAclStatsRequest\x1a!.api.instance.GetAclStatsResponse\x12U\n" + + "\fGetWhitelist\x12!.api.instance.GetWhitelistRequest\x1a\".api.instance.GetWhitelistResponse2v\n" + + "\x14PortForwardManageRpc\x12^\n" + + "\x0fListPortForward\x12$.api.instance.ListPortForwardRequest\x1a%.api.instance.ListPortForwardResponse2\xbe\x01\n" + + "\bStatsRpc\x12I\n" + + "\bGetStats\x12\x1d.api.instance.GetStatsRequest\x1a\x1e.api.instance.GetStatsResponse\x12g\n" + + "\x12GetPrometheusStats\x12'.api.instance.GetPrometheusStatsRequest\x1a(.api.instance.GetPrometheusStatsResponse2\xc1\x02\n" + + "\x13CredentialManageRpc\x12g\n" + + "\x12GenerateCredential\x12'.api.instance.GenerateCredentialRequest\x1a(.api.instance.GenerateCredentialResponse\x12a\n" + + "\x10RevokeCredential\x12%.api.instance.RevokeCredentialRequest\x1a&.api.instance.RevokeCredentialResponse\x12^\n" + + "\x0fListCredentials\x12$.api.instance.ListCredentialsRequest\x1a%.api.instance.ListCredentialsResponseb\x06proto3" + +var ( + file_api_instance_proto_rawDescOnce sync.Once + file_api_instance_proto_rawDescData []byte +) + +func file_api_instance_proto_rawDescGZIP() []byte { + file_api_instance_proto_rawDescOnce.Do(func() { + file_api_instance_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_instance_proto_rawDesc), len(file_api_instance_proto_rawDesc))) + }) + return file_api_instance_proto_rawDescData +} + +var file_api_instance_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_api_instance_proto_msgTypes = make([]protoimpl.MessageInfo, 63) +var file_api_instance_proto_goTypes = []any{ + (TrustedKeySourcePb)(0), // 0: api.instance.TrustedKeySourcePb + (ConnectorStatus)(0), // 1: api.instance.ConnectorStatus + (TcpProxyEntryTransportType)(0), // 2: api.instance.TcpProxyEntryTransportType + (TcpProxyEntryState)(0), // 3: api.instance.TcpProxyEntryState + (*InstanceIdentifier)(nil), // 4: api.instance.InstanceIdentifier + (*Status)(nil), // 5: api.instance.Status + (*PeerConnStats)(nil), // 6: api.instance.PeerConnStats + (*PeerConnInfo)(nil), // 7: api.instance.PeerConnInfo + (*PeerInfo)(nil), // 8: api.instance.PeerInfo + (*ListPeerRequest)(nil), // 9: api.instance.ListPeerRequest + (*ListPeerResponse)(nil), // 10: api.instance.ListPeerResponse + (*Route)(nil), // 11: api.instance.Route + (*PeerRoutePair)(nil), // 12: api.instance.PeerRoutePair + (*NodeInfo)(nil), // 13: api.instance.NodeInfo + (*ShowNodeInfoRequest)(nil), // 14: api.instance.ShowNodeInfoRequest + (*ShowNodeInfoResponse)(nil), // 15: api.instance.ShowNodeInfoResponse + (*PublicIpv6LeaseInfo)(nil), // 16: api.instance.PublicIpv6LeaseInfo + (*ListPublicIpv6InfoRequest)(nil), // 17: api.instance.ListPublicIpv6InfoRequest + (*ListPublicIpv6InfoResponse)(nil), // 18: api.instance.ListPublicIpv6InfoResponse + (*ListRouteRequest)(nil), // 19: api.instance.ListRouteRequest + (*ListRouteResponse)(nil), // 20: api.instance.ListRouteResponse + (*DumpRouteRequest)(nil), // 21: api.instance.DumpRouteRequest + (*DumpRouteResponse)(nil), // 22: api.instance.DumpRouteResponse + (*ListForeignNetworkRequest)(nil), // 23: api.instance.ListForeignNetworkRequest + (*TrustedKeyInfoPb)(nil), // 24: api.instance.TrustedKeyInfoPb + (*ForeignNetworkEntryPb)(nil), // 25: api.instance.ForeignNetworkEntryPb + (*ListForeignNetworkResponse)(nil), // 26: api.instance.ListForeignNetworkResponse + (*ListGlobalForeignNetworkRequest)(nil), // 27: api.instance.ListGlobalForeignNetworkRequest + (*ListGlobalForeignNetworkResponse)(nil), // 28: api.instance.ListGlobalForeignNetworkResponse + (*GetForeignNetworkSummaryRequest)(nil), // 29: api.instance.GetForeignNetworkSummaryRequest + (*GetForeignNetworkSummaryResponse)(nil), // 30: api.instance.GetForeignNetworkSummaryResponse + (*Connector)(nil), // 31: api.instance.Connector + (*ListConnectorRequest)(nil), // 32: api.instance.ListConnectorRequest + (*ListConnectorResponse)(nil), // 33: api.instance.ListConnectorResponse + (*MappedListener)(nil), // 34: api.instance.MappedListener + (*ListMappedListenerRequest)(nil), // 35: api.instance.ListMappedListenerRequest + (*ListMappedListenerResponse)(nil), // 36: api.instance.ListMappedListenerResponse + (*VpnPortalInfo)(nil), // 37: api.instance.VpnPortalInfo + (*GetVpnPortalInfoRequest)(nil), // 38: api.instance.GetVpnPortalInfoRequest + (*GetVpnPortalInfoResponse)(nil), // 39: api.instance.GetVpnPortalInfoResponse + (*TcpProxyEntry)(nil), // 40: api.instance.TcpProxyEntry + (*ListTcpProxyEntryRequest)(nil), // 41: api.instance.ListTcpProxyEntryRequest + (*ListTcpProxyEntryResponse)(nil), // 42: api.instance.ListTcpProxyEntryResponse + (*GetAclStatsRequest)(nil), // 43: api.instance.GetAclStatsRequest + (*GetAclStatsResponse)(nil), // 44: api.instance.GetAclStatsResponse + (*GetWhitelistRequest)(nil), // 45: api.instance.GetWhitelistRequest + (*GetWhitelistResponse)(nil), // 46: api.instance.GetWhitelistResponse + (*ListPortForwardRequest)(nil), // 47: api.instance.ListPortForwardRequest + (*ListPortForwardResponse)(nil), // 48: api.instance.ListPortForwardResponse + (*MetricSnapshot)(nil), // 49: api.instance.MetricSnapshot + (*GetStatsRequest)(nil), // 50: api.instance.GetStatsRequest + (*GetStatsResponse)(nil), // 51: api.instance.GetStatsResponse + (*GetPrometheusStatsRequest)(nil), // 52: api.instance.GetPrometheusStatsRequest + (*GetPrometheusStatsResponse)(nil), // 53: api.instance.GetPrometheusStatsResponse + (*GenerateCredentialRequest)(nil), // 54: api.instance.GenerateCredentialRequest + (*GenerateCredentialResponse)(nil), // 55: api.instance.GenerateCredentialResponse + (*RevokeCredentialRequest)(nil), // 56: api.instance.RevokeCredentialRequest + (*RevokeCredentialResponse)(nil), // 57: api.instance.RevokeCredentialResponse + (*ListCredentialsRequest)(nil), // 58: api.instance.ListCredentialsRequest + (*CredentialInfo)(nil), // 59: api.instance.CredentialInfo + (*ListCredentialsResponse)(nil), // 60: api.instance.ListCredentialsResponse + (*InstanceIdentifier_InstanceSelector)(nil), // 61: api.instance.InstanceIdentifier.InstanceSelector + nil, // 62: api.instance.ListForeignNetworkResponse.ForeignNetworksEntry + (*ListGlobalForeignNetworkResponse_OneForeignNetwork)(nil), // 63: api.instance.ListGlobalForeignNetworkResponse.OneForeignNetwork + (*ListGlobalForeignNetworkResponse_ForeignNetworks)(nil), // 64: api.instance.ListGlobalForeignNetworkResponse.ForeignNetworks + nil, // 65: api.instance.ListGlobalForeignNetworkResponse.ForeignNetworksEntry + nil, // 66: api.instance.MetricSnapshot.LabelsEntry + (*common.UUID)(nil), // 67: common.UUID + (*common.TunnelInfo)(nil), // 68: common.TunnelInfo + (peer_rpc.SecureAuthLevel)(0), // 69: peer_rpc.SecureAuthLevel + (peer_rpc.PeerIdentityType)(0), // 70: peer_rpc.PeerIdentityType + (*common.Ipv4Inet)(nil), // 71: common.Ipv4Inet + (*common.StunInfo)(nil), // 72: common.StunInfo + (*common.PeerFeatureFlag)(nil), // 73: common.PeerFeatureFlag + (*common.Ipv6Inet)(nil), // 74: common.Ipv6Inet + (*peer_rpc.GetIpListResponse)(nil), // 75: peer_rpc.GetIpListResponse + (*peer_rpc.RouteForeignNetworkSummary)(nil), // 76: peer_rpc.RouteForeignNetworkSummary + (*common.Url)(nil), // 77: common.Url + (*common.SocketAddr)(nil), // 78: common.SocketAddr + (*acl.AclStats)(nil), // 79: acl.AclStats + (*common.PortForwardConfigPb)(nil), // 80: common.PortForwardConfigPb +} +var file_api_instance_proto_depIdxs = []int32{ + 67, // 0: api.instance.InstanceIdentifier.id:type_name -> common.UUID + 61, // 1: api.instance.InstanceIdentifier.instance_selector:type_name -> api.instance.InstanceIdentifier.InstanceSelector + 68, // 2: api.instance.PeerConnInfo.tunnel:type_name -> common.TunnelInfo + 6, // 3: api.instance.PeerConnInfo.stats:type_name -> api.instance.PeerConnStats + 69, // 4: api.instance.PeerConnInfo.secure_auth_level:type_name -> peer_rpc.SecureAuthLevel + 70, // 5: api.instance.PeerConnInfo.peer_identity_type:type_name -> peer_rpc.PeerIdentityType + 7, // 6: api.instance.PeerInfo.conns:type_name -> api.instance.PeerConnInfo + 67, // 7: api.instance.PeerInfo.default_conn_id:type_name -> common.UUID + 67, // 8: api.instance.PeerInfo.directly_connected_conns:type_name -> common.UUID + 4, // 9: api.instance.ListPeerRequest.instance:type_name -> api.instance.InstanceIdentifier + 8, // 10: api.instance.ListPeerResponse.peer_infos:type_name -> api.instance.PeerInfo + 13, // 11: api.instance.ListPeerResponse.my_info:type_name -> api.instance.NodeInfo + 71, // 12: api.instance.Route.ipv4_addr:type_name -> common.Ipv4Inet + 72, // 13: api.instance.Route.stun_info:type_name -> common.StunInfo + 73, // 14: api.instance.Route.feature_flag:type_name -> common.PeerFeatureFlag + 74, // 15: api.instance.Route.ipv6_addr:type_name -> common.Ipv6Inet + 74, // 16: api.instance.Route.public_ipv6_addr:type_name -> common.Ipv6Inet + 74, // 17: api.instance.Route.ipv6_public_addr_prefix:type_name -> common.Ipv6Inet + 11, // 18: api.instance.PeerRoutePair.route:type_name -> api.instance.Route + 8, // 19: api.instance.PeerRoutePair.peer:type_name -> api.instance.PeerInfo + 72, // 20: api.instance.NodeInfo.stun_info:type_name -> common.StunInfo + 73, // 21: api.instance.NodeInfo.feature_flag:type_name -> common.PeerFeatureFlag + 75, // 22: api.instance.NodeInfo.ip_list:type_name -> peer_rpc.GetIpListResponse + 74, // 23: api.instance.NodeInfo.public_ipv6_addr:type_name -> common.Ipv6Inet + 74, // 24: api.instance.NodeInfo.ipv6_public_addr_prefix:type_name -> common.Ipv6Inet + 4, // 25: api.instance.ShowNodeInfoRequest.instance:type_name -> api.instance.InstanceIdentifier + 13, // 26: api.instance.ShowNodeInfoResponse.node_info:type_name -> api.instance.NodeInfo + 74, // 27: api.instance.PublicIpv6LeaseInfo.leased_addr:type_name -> common.Ipv6Inet + 4, // 28: api.instance.ListPublicIpv6InfoRequest.instance:type_name -> api.instance.InstanceIdentifier + 74, // 29: api.instance.ListPublicIpv6InfoResponse.provider_prefix:type_name -> common.Ipv6Inet + 16, // 30: api.instance.ListPublicIpv6InfoResponse.provider_leases:type_name -> api.instance.PublicIpv6LeaseInfo + 4, // 31: api.instance.ListRouteRequest.instance:type_name -> api.instance.InstanceIdentifier + 11, // 32: api.instance.ListRouteResponse.routes:type_name -> api.instance.Route + 4, // 33: api.instance.DumpRouteRequest.instance:type_name -> api.instance.InstanceIdentifier + 4, // 34: api.instance.ListForeignNetworkRequest.instance:type_name -> api.instance.InstanceIdentifier + 0, // 35: api.instance.TrustedKeyInfoPb.source:type_name -> api.instance.TrustedKeySourcePb + 8, // 36: api.instance.ForeignNetworkEntryPb.peers:type_name -> api.instance.PeerInfo + 24, // 37: api.instance.ForeignNetworkEntryPb.trusted_keys:type_name -> api.instance.TrustedKeyInfoPb + 62, // 38: api.instance.ListForeignNetworkResponse.foreign_networks:type_name -> api.instance.ListForeignNetworkResponse.ForeignNetworksEntry + 4, // 39: api.instance.ListGlobalForeignNetworkRequest.instance:type_name -> api.instance.InstanceIdentifier + 65, // 40: api.instance.ListGlobalForeignNetworkResponse.foreign_networks:type_name -> api.instance.ListGlobalForeignNetworkResponse.ForeignNetworksEntry + 4, // 41: api.instance.GetForeignNetworkSummaryRequest.instance:type_name -> api.instance.InstanceIdentifier + 76, // 42: api.instance.GetForeignNetworkSummaryResponse.summary:type_name -> peer_rpc.RouteForeignNetworkSummary + 77, // 43: api.instance.Connector.url:type_name -> common.Url + 1, // 44: api.instance.Connector.status:type_name -> api.instance.ConnectorStatus + 4, // 45: api.instance.ListConnectorRequest.instance:type_name -> api.instance.InstanceIdentifier + 31, // 46: api.instance.ListConnectorResponse.connectors:type_name -> api.instance.Connector + 77, // 47: api.instance.MappedListener.url:type_name -> common.Url + 4, // 48: api.instance.ListMappedListenerRequest.instance:type_name -> api.instance.InstanceIdentifier + 34, // 49: api.instance.ListMappedListenerResponse.mappedlisteners:type_name -> api.instance.MappedListener + 4, // 50: api.instance.GetVpnPortalInfoRequest.instance:type_name -> api.instance.InstanceIdentifier + 37, // 51: api.instance.GetVpnPortalInfoResponse.vpn_portal_info:type_name -> api.instance.VpnPortalInfo + 78, // 52: api.instance.TcpProxyEntry.src:type_name -> common.SocketAddr + 78, // 53: api.instance.TcpProxyEntry.dst:type_name -> common.SocketAddr + 3, // 54: api.instance.TcpProxyEntry.state:type_name -> api.instance.TcpProxyEntryState + 2, // 55: api.instance.TcpProxyEntry.transport_type:type_name -> api.instance.TcpProxyEntryTransportType + 4, // 56: api.instance.ListTcpProxyEntryRequest.instance:type_name -> api.instance.InstanceIdentifier + 40, // 57: api.instance.ListTcpProxyEntryResponse.entries:type_name -> api.instance.TcpProxyEntry + 4, // 58: api.instance.GetAclStatsRequest.instance:type_name -> api.instance.InstanceIdentifier + 79, // 59: api.instance.GetAclStatsResponse.acl_stats:type_name -> acl.AclStats + 4, // 60: api.instance.GetWhitelistRequest.instance:type_name -> api.instance.InstanceIdentifier + 4, // 61: api.instance.ListPortForwardRequest.instance:type_name -> api.instance.InstanceIdentifier + 80, // 62: api.instance.ListPortForwardResponse.cfgs:type_name -> common.PortForwardConfigPb + 66, // 63: api.instance.MetricSnapshot.labels:type_name -> api.instance.MetricSnapshot.LabelsEntry + 4, // 64: api.instance.GetStatsRequest.instance:type_name -> api.instance.InstanceIdentifier + 49, // 65: api.instance.GetStatsResponse.metrics:type_name -> api.instance.MetricSnapshot + 4, // 66: api.instance.GetPrometheusStatsRequest.instance:type_name -> api.instance.InstanceIdentifier + 4, // 67: api.instance.GenerateCredentialRequest.instance:type_name -> api.instance.InstanceIdentifier + 4, // 68: api.instance.RevokeCredentialRequest.instance:type_name -> api.instance.InstanceIdentifier + 4, // 69: api.instance.ListCredentialsRequest.instance:type_name -> api.instance.InstanceIdentifier + 59, // 70: api.instance.ListCredentialsResponse.credentials:type_name -> api.instance.CredentialInfo + 25, // 71: api.instance.ListForeignNetworkResponse.ForeignNetworksEntry.value:type_name -> api.instance.ForeignNetworkEntryPb + 63, // 72: api.instance.ListGlobalForeignNetworkResponse.ForeignNetworks.foreign_networks:type_name -> api.instance.ListGlobalForeignNetworkResponse.OneForeignNetwork + 64, // 73: api.instance.ListGlobalForeignNetworkResponse.ForeignNetworksEntry.value:type_name -> api.instance.ListGlobalForeignNetworkResponse.ForeignNetworks + 9, // 74: api.instance.PeerManageRpc.ListPeer:input_type -> api.instance.ListPeerRequest + 17, // 75: api.instance.PeerManageRpc.ListPublicIpv6Info:input_type -> api.instance.ListPublicIpv6InfoRequest + 19, // 76: api.instance.PeerManageRpc.ListRoute:input_type -> api.instance.ListRouteRequest + 21, // 77: api.instance.PeerManageRpc.DumpRoute:input_type -> api.instance.DumpRouteRequest + 23, // 78: api.instance.PeerManageRpc.ListForeignNetwork:input_type -> api.instance.ListForeignNetworkRequest + 27, // 79: api.instance.PeerManageRpc.ListGlobalForeignNetwork:input_type -> api.instance.ListGlobalForeignNetworkRequest + 14, // 80: api.instance.PeerManageRpc.ShowNodeInfo:input_type -> api.instance.ShowNodeInfoRequest + 29, // 81: api.instance.PeerManageRpc.GetForeignNetworkSummary:input_type -> api.instance.GetForeignNetworkSummaryRequest + 32, // 82: api.instance.ConnectorManageRpc.ListConnector:input_type -> api.instance.ListConnectorRequest + 35, // 83: api.instance.MappedListenerManageRpc.ListMappedListener:input_type -> api.instance.ListMappedListenerRequest + 38, // 84: api.instance.VpnPortalRpc.GetVpnPortalInfo:input_type -> api.instance.GetVpnPortalInfoRequest + 41, // 85: api.instance.TcpProxyRpc.ListTcpProxyEntry:input_type -> api.instance.ListTcpProxyEntryRequest + 43, // 86: api.instance.AclManageRpc.GetAclStats:input_type -> api.instance.GetAclStatsRequest + 45, // 87: api.instance.AclManageRpc.GetWhitelist:input_type -> api.instance.GetWhitelistRequest + 47, // 88: api.instance.PortForwardManageRpc.ListPortForward:input_type -> api.instance.ListPortForwardRequest + 50, // 89: api.instance.StatsRpc.GetStats:input_type -> api.instance.GetStatsRequest + 52, // 90: api.instance.StatsRpc.GetPrometheusStats:input_type -> api.instance.GetPrometheusStatsRequest + 54, // 91: api.instance.CredentialManageRpc.GenerateCredential:input_type -> api.instance.GenerateCredentialRequest + 56, // 92: api.instance.CredentialManageRpc.RevokeCredential:input_type -> api.instance.RevokeCredentialRequest + 58, // 93: api.instance.CredentialManageRpc.ListCredentials:input_type -> api.instance.ListCredentialsRequest + 10, // 94: api.instance.PeerManageRpc.ListPeer:output_type -> api.instance.ListPeerResponse + 18, // 95: api.instance.PeerManageRpc.ListPublicIpv6Info:output_type -> api.instance.ListPublicIpv6InfoResponse + 20, // 96: api.instance.PeerManageRpc.ListRoute:output_type -> api.instance.ListRouteResponse + 22, // 97: api.instance.PeerManageRpc.DumpRoute:output_type -> api.instance.DumpRouteResponse + 26, // 98: api.instance.PeerManageRpc.ListForeignNetwork:output_type -> api.instance.ListForeignNetworkResponse + 28, // 99: api.instance.PeerManageRpc.ListGlobalForeignNetwork:output_type -> api.instance.ListGlobalForeignNetworkResponse + 15, // 100: api.instance.PeerManageRpc.ShowNodeInfo:output_type -> api.instance.ShowNodeInfoResponse + 30, // 101: api.instance.PeerManageRpc.GetForeignNetworkSummary:output_type -> api.instance.GetForeignNetworkSummaryResponse + 33, // 102: api.instance.ConnectorManageRpc.ListConnector:output_type -> api.instance.ListConnectorResponse + 36, // 103: api.instance.MappedListenerManageRpc.ListMappedListener:output_type -> api.instance.ListMappedListenerResponse + 39, // 104: api.instance.VpnPortalRpc.GetVpnPortalInfo:output_type -> api.instance.GetVpnPortalInfoResponse + 42, // 105: api.instance.TcpProxyRpc.ListTcpProxyEntry:output_type -> api.instance.ListTcpProxyEntryResponse + 44, // 106: api.instance.AclManageRpc.GetAclStats:output_type -> api.instance.GetAclStatsResponse + 46, // 107: api.instance.AclManageRpc.GetWhitelist:output_type -> api.instance.GetWhitelistResponse + 48, // 108: api.instance.PortForwardManageRpc.ListPortForward:output_type -> api.instance.ListPortForwardResponse + 51, // 109: api.instance.StatsRpc.GetStats:output_type -> api.instance.GetStatsResponse + 53, // 110: api.instance.StatsRpc.GetPrometheusStats:output_type -> api.instance.GetPrometheusStatsResponse + 55, // 111: api.instance.CredentialManageRpc.GenerateCredential:output_type -> api.instance.GenerateCredentialResponse + 57, // 112: api.instance.CredentialManageRpc.RevokeCredential:output_type -> api.instance.RevokeCredentialResponse + 60, // 113: api.instance.CredentialManageRpc.ListCredentials:output_type -> api.instance.ListCredentialsResponse + 94, // [94:114] is the sub-list for method output_type + 74, // [74:94] is the sub-list for method input_type + 74, // [74:74] is the sub-list for extension type_name + 74, // [74:74] is the sub-list for extension extendee + 0, // [0:74] is the sub-list for field type_name +} + +func init() { file_api_instance_proto_init() } +func file_api_instance_proto_init() { + if File_api_instance_proto != nil { + return + } + file_api_instance_proto_msgTypes[0].OneofWrappers = []any{ + (*InstanceIdentifier_Id)(nil), + (*InstanceIdentifier_InstanceSelector_)(nil), + } + file_api_instance_proto_msgTypes[7].OneofWrappers = []any{} + file_api_instance_proto_msgTypes[20].OneofWrappers = []any{} + file_api_instance_proto_msgTypes[50].OneofWrappers = []any{} + file_api_instance_proto_msgTypes[55].OneofWrappers = []any{} + file_api_instance_proto_msgTypes[57].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_instance_proto_rawDesc), len(file_api_instance_proto_rawDesc)), + NumEnums: 4, + NumMessages: 63, + NumExtensions: 0, + NumServices: 9, + }, + GoTypes: file_api_instance_proto_goTypes, + DependencyIndexes: file_api_instance_proto_depIdxs, + EnumInfos: file_api_instance_proto_enumTypes, + MessageInfos: file_api_instance_proto_msgTypes, + }.Build() + File_api_instance_proto = out.File + file_api_instance_proto_goTypes = nil + file_api_instance_proto_depIdxs = nil +} diff --git a/easytier-go/proto/api/manage/api_manage.pb.go b/easytier-go/proto/api/manage/api_manage.pb.go new file mode 100644 index 00000000..44db9480 --- /dev/null +++ b/easytier-go/proto/api/manage/api_manage.pb.go @@ -0,0 +1,2259 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.35.1 +// source: api_manage.proto + +package manage + +import ( + acl "github.com/EasyTier/EasyTier/easytier-go/proto/acl" + instance "github.com/EasyTier/EasyTier/easytier-go/proto/api/instance" + common "github.com/EasyTier/EasyTier/easytier-go/proto/common" + peer_rpc "github.com/EasyTier/EasyTier/easytier-go/proto/peer_rpc" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NetworkingMethod int32 + +const ( + NetworkingMethod_PublicServer NetworkingMethod = 0 + NetworkingMethod_Manual NetworkingMethod = 1 + NetworkingMethod_Standalone NetworkingMethod = 2 +) + +// Enum value maps for NetworkingMethod. +var ( + NetworkingMethod_name = map[int32]string{ + 0: "PublicServer", + 1: "Manual", + 2: "Standalone", + } + NetworkingMethod_value = map[string]int32{ + "PublicServer": 0, + "Manual": 1, + "Standalone": 2, + } +) + +func (x NetworkingMethod) Enum() *NetworkingMethod { + p := new(NetworkingMethod) + *p = x + return p +} + +func (x NetworkingMethod) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NetworkingMethod) Descriptor() protoreflect.EnumDescriptor { + return file_api_manage_proto_enumTypes[0].Descriptor() +} + +func (NetworkingMethod) Type() protoreflect.EnumType { + return &file_api_manage_proto_enumTypes[0] +} + +func (x NetworkingMethod) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NetworkingMethod.Descriptor instead. +func (NetworkingMethod) EnumDescriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{0} +} + +type ConfigSource int32 + +const ( + ConfigSource_ConfigSourceUnspecified ConfigSource = 0 + ConfigSource_ConfigSourceUser ConfigSource = 1 + ConfigSource_ConfigSourceWeb ConfigSource = 2 +) + +// Enum value maps for ConfigSource. +var ( + ConfigSource_name = map[int32]string{ + 0: "ConfigSourceUnspecified", + 1: "ConfigSourceUser", + 2: "ConfigSourceWeb", + } + ConfigSource_value = map[string]int32{ + "ConfigSourceUnspecified": 0, + "ConfigSourceUser": 1, + "ConfigSourceWeb": 2, + } +) + +func (x ConfigSource) Enum() *ConfigSource { + p := new(ConfigSource) + *p = x + return p +} + +func (x ConfigSource) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfigSource) Descriptor() protoreflect.EnumDescriptor { + return file_api_manage_proto_enumTypes[1].Descriptor() +} + +func (ConfigSource) Type() protoreflect.EnumType { + return &file_api_manage_proto_enumTypes[1] +} + +func (x ConfigSource) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfigSource.Descriptor instead. +func (ConfigSource) EnumDescriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{1} +} + +type NetworkConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstanceId *string `protobuf:"bytes,1,opt,name=instance_id,json=instanceId,proto3,oneof" json:"instance_id,omitempty"` + Dhcp *bool `protobuf:"varint,2,opt,name=dhcp,proto3,oneof" json:"dhcp,omitempty"` + VirtualIpv4 *string `protobuf:"bytes,3,opt,name=virtual_ipv4,json=virtualIpv4,proto3,oneof" json:"virtual_ipv4,omitempty"` + NetworkLength *int32 `protobuf:"varint,4,opt,name=network_length,json=networkLength,proto3,oneof" json:"network_length,omitempty"` + Hostname *string `protobuf:"bytes,5,opt,name=hostname,proto3,oneof" json:"hostname,omitempty"` + NetworkName *string `protobuf:"bytes,6,opt,name=network_name,json=networkName,proto3,oneof" json:"network_name,omitempty"` + NetworkSecret *string `protobuf:"bytes,7,opt,name=network_secret,json=networkSecret,proto3,oneof" json:"network_secret,omitempty"` + NetworkingMethod *NetworkingMethod `protobuf:"varint,8,opt,name=networking_method,json=networkingMethod,proto3,enum=api.manage.NetworkingMethod,oneof" json:"networking_method,omitempty"` + PublicServerUrl *string `protobuf:"bytes,9,opt,name=public_server_url,json=publicServerUrl,proto3,oneof" json:"public_server_url,omitempty"` + PeerUrls []string `protobuf:"bytes,10,rep,name=peer_urls,json=peerUrls,proto3" json:"peer_urls,omitempty"` + ProxyCidrs []string `protobuf:"bytes,11,rep,name=proxy_cidrs,json=proxyCidrs,proto3" json:"proxy_cidrs,omitempty"` + EnableVpnPortal *bool `protobuf:"varint,12,opt,name=enable_vpn_portal,json=enableVpnPortal,proto3,oneof" json:"enable_vpn_portal,omitempty"` + VpnPortalListenPort *int32 `protobuf:"varint,13,opt,name=vpn_portal_listen_port,json=vpnPortalListenPort,proto3,oneof" json:"vpn_portal_listen_port,omitempty"` + VpnPortalClientNetworkAddr *string `protobuf:"bytes,14,opt,name=vpn_portal_client_network_addr,json=vpnPortalClientNetworkAddr,proto3,oneof" json:"vpn_portal_client_network_addr,omitempty"` + VpnPortalClientNetworkLen *int32 `protobuf:"varint,15,opt,name=vpn_portal_client_network_len,json=vpnPortalClientNetworkLen,proto3,oneof" json:"vpn_portal_client_network_len,omitempty"` + AdvancedSettings *bool `protobuf:"varint,16,opt,name=advanced_settings,json=advancedSettings,proto3,oneof" json:"advanced_settings,omitempty"` + ListenerUrls []string `protobuf:"bytes,17,rep,name=listener_urls,json=listenerUrls,proto3" json:"listener_urls,omitempty"` + // optional int32 rpc_port = 18; + LatencyFirst *bool `protobuf:"varint,19,opt,name=latency_first,json=latencyFirst,proto3,oneof" json:"latency_first,omitempty"` + DevName *string `protobuf:"bytes,20,opt,name=dev_name,json=devName,proto3,oneof" json:"dev_name,omitempty"` + UseSmoltcp *bool `protobuf:"varint,21,opt,name=use_smoltcp,json=useSmoltcp,proto3,oneof" json:"use_smoltcp,omitempty"` + DisableIpv6 *bool `protobuf:"varint,47,opt,name=disable_ipv6,json=disableIpv6,proto3,oneof" json:"disable_ipv6,omitempty"` + EnableKcpProxy *bool `protobuf:"varint,22,opt,name=enable_kcp_proxy,json=enableKcpProxy,proto3,oneof" json:"enable_kcp_proxy,omitempty"` + DisableKcpInput *bool `protobuf:"varint,23,opt,name=disable_kcp_input,json=disableKcpInput,proto3,oneof" json:"disable_kcp_input,omitempty"` + DisableP2P *bool `protobuf:"varint,24,opt,name=disable_p2p,json=disableP2p,proto3,oneof" json:"disable_p2p,omitempty"` + BindDevice *bool `protobuf:"varint,25,opt,name=bind_device,json=bindDevice,proto3,oneof" json:"bind_device,omitempty"` + NoTun *bool `protobuf:"varint,26,opt,name=no_tun,json=noTun,proto3,oneof" json:"no_tun,omitempty"` + EnableExitNode *bool `protobuf:"varint,27,opt,name=enable_exit_node,json=enableExitNode,proto3,oneof" json:"enable_exit_node,omitempty"` + RelayAllPeerRpc *bool `protobuf:"varint,28,opt,name=relay_all_peer_rpc,json=relayAllPeerRpc,proto3,oneof" json:"relay_all_peer_rpc,omitempty"` + MultiThread *bool `protobuf:"varint,29,opt,name=multi_thread,json=multiThread,proto3,oneof" json:"multi_thread,omitempty"` + EnableRelayNetworkWhitelist *bool `protobuf:"varint,30,opt,name=enable_relay_network_whitelist,json=enableRelayNetworkWhitelist,proto3,oneof" json:"enable_relay_network_whitelist,omitempty"` + RelayNetworkWhitelist []string `protobuf:"bytes,31,rep,name=relay_network_whitelist,json=relayNetworkWhitelist,proto3" json:"relay_network_whitelist,omitempty"` + EnableManualRoutes *bool `protobuf:"varint,32,opt,name=enable_manual_routes,json=enableManualRoutes,proto3,oneof" json:"enable_manual_routes,omitempty"` + Routes []string `protobuf:"bytes,33,rep,name=routes,proto3" json:"routes,omitempty"` + ExitNodes []string `protobuf:"bytes,34,rep,name=exit_nodes,json=exitNodes,proto3" json:"exit_nodes,omitempty"` + ProxyForwardBySystem *bool `protobuf:"varint,35,opt,name=proxy_forward_by_system,json=proxyForwardBySystem,proto3,oneof" json:"proxy_forward_by_system,omitempty"` + DisableEncryption *bool `protobuf:"varint,36,opt,name=disable_encryption,json=disableEncryption,proto3,oneof" json:"disable_encryption,omitempty"` + EnableSocks5 *bool `protobuf:"varint,37,opt,name=enable_socks5,json=enableSocks5,proto3,oneof" json:"enable_socks5,omitempty"` + Socks5Port *int32 `protobuf:"varint,38,opt,name=socks5_port,json=socks5Port,proto3,oneof" json:"socks5_port,omitempty"` + DisableUdpHolePunching *bool `protobuf:"varint,39,opt,name=disable_udp_hole_punching,json=disableUdpHolePunching,proto3,oneof" json:"disable_udp_hole_punching,omitempty"` + Mtu *int32 `protobuf:"varint,40,opt,name=mtu,proto3,oneof" json:"mtu,omitempty"` + MappedListeners []string `protobuf:"bytes,41,rep,name=mapped_listeners,json=mappedListeners,proto3" json:"mapped_listeners,omitempty"` + EnableMagicDns *bool `protobuf:"varint,42,opt,name=enable_magic_dns,json=enableMagicDns,proto3,oneof" json:"enable_magic_dns,omitempty"` + EnablePrivateMode *bool `protobuf:"varint,43,opt,name=enable_private_mode,json=enablePrivateMode,proto3,oneof" json:"enable_private_mode,omitempty"` + EnableQuicProxy *bool `protobuf:"varint,45,opt,name=enable_quic_proxy,json=enableQuicProxy,proto3,oneof" json:"enable_quic_proxy,omitempty"` + DisableQuicInput *bool `protobuf:"varint,46,opt,name=disable_quic_input,json=disableQuicInput,proto3,oneof" json:"disable_quic_input,omitempty"` + // Deprecated: Marked as deprecated in api_manage.proto. + QuicListenPort *int32 `protobuf:"varint,50,opt,name=quic_listen_port,json=quicListenPort,proto3,oneof" json:"quic_listen_port,omitempty"` + PortForwards []*PortForwardConfig `protobuf:"bytes,48,rep,name=port_forwards,json=portForwards,proto3" json:"port_forwards,omitempty"` + DisableSymHolePunching *bool `protobuf:"varint,49,opt,name=disable_sym_hole_punching,json=disableSymHolePunching,proto3,oneof" json:"disable_sym_hole_punching,omitempty"` + P2POnly *bool `protobuf:"varint,51,opt,name=p2p_only,json=p2pOnly,proto3,oneof" json:"p2p_only,omitempty"` + DataCompressAlgo *common.CompressionAlgoPb `protobuf:"varint,52,opt,name=data_compress_algo,json=dataCompressAlgo,proto3,enum=common.CompressionAlgoPb,oneof" json:"data_compress_algo,omitempty"` + EncryptionAlgorithm *string `protobuf:"bytes,53,opt,name=encryption_algorithm,json=encryptionAlgorithm,proto3,oneof" json:"encryption_algorithm,omitempty"` + DisableTcpHolePunching *bool `protobuf:"varint,54,opt,name=disable_tcp_hole_punching,json=disableTcpHolePunching,proto3,oneof" json:"disable_tcp_hole_punching,omitempty"` + SecureMode *common.SecureModeConfig `protobuf:"bytes,55,opt,name=secure_mode,json=secureMode,proto3" json:"secure_mode,omitempty"` + Acl *acl.Acl `protobuf:"bytes,56,opt,name=acl,proto3,oneof" json:"acl,omitempty"` + CredentialFile *string `protobuf:"bytes,57,opt,name=credential_file,json=credentialFile,proto3,oneof" json:"credential_file,omitempty"` + LazyP2P *bool `protobuf:"varint,58,opt,name=lazy_p2p,json=lazyP2p,proto3,oneof" json:"lazy_p2p,omitempty"` + NeedP2P *bool `protobuf:"varint,59,opt,name=need_p2p,json=needP2p,proto3,oneof" json:"need_p2p,omitempty"` + InstanceRecvBpsLimit *uint64 `protobuf:"varint,60,opt,name=instance_recv_bps_limit,json=instanceRecvBpsLimit,proto3,oneof" json:"instance_recv_bps_limit,omitempty"` + DisableUpnp *bool `protobuf:"varint,61,opt,name=disable_upnp,json=disableUpnp,proto3,oneof" json:"disable_upnp,omitempty"` + Ipv6PublicAddrProvider *bool `protobuf:"varint,62,opt,name=ipv6_public_addr_provider,json=ipv6PublicAddrProvider,proto3,oneof" json:"ipv6_public_addr_provider,omitempty"` + Ipv6PublicAddrAuto *bool `protobuf:"varint,63,opt,name=ipv6_public_addr_auto,json=ipv6PublicAddrAuto,proto3,oneof" json:"ipv6_public_addr_auto,omitempty"` + Ipv6PublicAddrPrefix *string `protobuf:"bytes,64,opt,name=ipv6_public_addr_prefix,json=ipv6PublicAddrPrefix,proto3,oneof" json:"ipv6_public_addr_prefix,omitempty"` + DisableRelayData *bool `protobuf:"varint,65,opt,name=disable_relay_data,json=disableRelayData,proto3,oneof" json:"disable_relay_data,omitempty"` + EnableUdpBroadcastRelay *bool `protobuf:"varint,66,opt,name=enable_udp_broadcast_relay,json=enableUdpBroadcastRelay,proto3,oneof" json:"enable_udp_broadcast_relay,omitempty"` + SocketMark *uint32 `protobuf:"varint,67,opt,name=socket_mark,json=socketMark,proto3,oneof" json:"socket_mark,omitempty"` + Peers []*NetworkPeerConfig `protobuf:"bytes,68,rep,name=peers,proto3" json:"peers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkConfig) Reset() { + *x = NetworkConfig{} + mi := &file_api_manage_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkConfig) ProtoMessage() {} + +func (x *NetworkConfig) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead. +func (*NetworkConfig) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{0} +} + +func (x *NetworkConfig) GetInstanceId() string { + if x != nil && x.InstanceId != nil { + return *x.InstanceId + } + return "" +} + +func (x *NetworkConfig) GetDhcp() bool { + if x != nil && x.Dhcp != nil { + return *x.Dhcp + } + return false +} + +func (x *NetworkConfig) GetVirtualIpv4() string { + if x != nil && x.VirtualIpv4 != nil { + return *x.VirtualIpv4 + } + return "" +} + +func (x *NetworkConfig) GetNetworkLength() int32 { + if x != nil && x.NetworkLength != nil { + return *x.NetworkLength + } + return 0 +} + +func (x *NetworkConfig) GetHostname() string { + if x != nil && x.Hostname != nil { + return *x.Hostname + } + return "" +} + +func (x *NetworkConfig) GetNetworkName() string { + if x != nil && x.NetworkName != nil { + return *x.NetworkName + } + return "" +} + +func (x *NetworkConfig) GetNetworkSecret() string { + if x != nil && x.NetworkSecret != nil { + return *x.NetworkSecret + } + return "" +} + +func (x *NetworkConfig) GetNetworkingMethod() NetworkingMethod { + if x != nil && x.NetworkingMethod != nil { + return *x.NetworkingMethod + } + return NetworkingMethod_PublicServer +} + +func (x *NetworkConfig) GetPublicServerUrl() string { + if x != nil && x.PublicServerUrl != nil { + return *x.PublicServerUrl + } + return "" +} + +func (x *NetworkConfig) GetPeerUrls() []string { + if x != nil { + return x.PeerUrls + } + return nil +} + +func (x *NetworkConfig) GetProxyCidrs() []string { + if x != nil { + return x.ProxyCidrs + } + return nil +} + +func (x *NetworkConfig) GetEnableVpnPortal() bool { + if x != nil && x.EnableVpnPortal != nil { + return *x.EnableVpnPortal + } + return false +} + +func (x *NetworkConfig) GetVpnPortalListenPort() int32 { + if x != nil && x.VpnPortalListenPort != nil { + return *x.VpnPortalListenPort + } + return 0 +} + +func (x *NetworkConfig) GetVpnPortalClientNetworkAddr() string { + if x != nil && x.VpnPortalClientNetworkAddr != nil { + return *x.VpnPortalClientNetworkAddr + } + return "" +} + +func (x *NetworkConfig) GetVpnPortalClientNetworkLen() int32 { + if x != nil && x.VpnPortalClientNetworkLen != nil { + return *x.VpnPortalClientNetworkLen + } + return 0 +} + +func (x *NetworkConfig) GetAdvancedSettings() bool { + if x != nil && x.AdvancedSettings != nil { + return *x.AdvancedSettings + } + return false +} + +func (x *NetworkConfig) GetListenerUrls() []string { + if x != nil { + return x.ListenerUrls + } + return nil +} + +func (x *NetworkConfig) GetLatencyFirst() bool { + if x != nil && x.LatencyFirst != nil { + return *x.LatencyFirst + } + return false +} + +func (x *NetworkConfig) GetDevName() string { + if x != nil && x.DevName != nil { + return *x.DevName + } + return "" +} + +func (x *NetworkConfig) GetUseSmoltcp() bool { + if x != nil && x.UseSmoltcp != nil { + return *x.UseSmoltcp + } + return false +} + +func (x *NetworkConfig) GetDisableIpv6() bool { + if x != nil && x.DisableIpv6 != nil { + return *x.DisableIpv6 + } + return false +} + +func (x *NetworkConfig) GetEnableKcpProxy() bool { + if x != nil && x.EnableKcpProxy != nil { + return *x.EnableKcpProxy + } + return false +} + +func (x *NetworkConfig) GetDisableKcpInput() bool { + if x != nil && x.DisableKcpInput != nil { + return *x.DisableKcpInput + } + return false +} + +func (x *NetworkConfig) GetDisableP2P() bool { + if x != nil && x.DisableP2P != nil { + return *x.DisableP2P + } + return false +} + +func (x *NetworkConfig) GetBindDevice() bool { + if x != nil && x.BindDevice != nil { + return *x.BindDevice + } + return false +} + +func (x *NetworkConfig) GetNoTun() bool { + if x != nil && x.NoTun != nil { + return *x.NoTun + } + return false +} + +func (x *NetworkConfig) GetEnableExitNode() bool { + if x != nil && x.EnableExitNode != nil { + return *x.EnableExitNode + } + return false +} + +func (x *NetworkConfig) GetRelayAllPeerRpc() bool { + if x != nil && x.RelayAllPeerRpc != nil { + return *x.RelayAllPeerRpc + } + return false +} + +func (x *NetworkConfig) GetMultiThread() bool { + if x != nil && x.MultiThread != nil { + return *x.MultiThread + } + return false +} + +func (x *NetworkConfig) GetEnableRelayNetworkWhitelist() bool { + if x != nil && x.EnableRelayNetworkWhitelist != nil { + return *x.EnableRelayNetworkWhitelist + } + return false +} + +func (x *NetworkConfig) GetRelayNetworkWhitelist() []string { + if x != nil { + return x.RelayNetworkWhitelist + } + return nil +} + +func (x *NetworkConfig) GetEnableManualRoutes() bool { + if x != nil && x.EnableManualRoutes != nil { + return *x.EnableManualRoutes + } + return false +} + +func (x *NetworkConfig) GetRoutes() []string { + if x != nil { + return x.Routes + } + return nil +} + +func (x *NetworkConfig) GetExitNodes() []string { + if x != nil { + return x.ExitNodes + } + return nil +} + +func (x *NetworkConfig) GetProxyForwardBySystem() bool { + if x != nil && x.ProxyForwardBySystem != nil { + return *x.ProxyForwardBySystem + } + return false +} + +func (x *NetworkConfig) GetDisableEncryption() bool { + if x != nil && x.DisableEncryption != nil { + return *x.DisableEncryption + } + return false +} + +func (x *NetworkConfig) GetEnableSocks5() bool { + if x != nil && x.EnableSocks5 != nil { + return *x.EnableSocks5 + } + return false +} + +func (x *NetworkConfig) GetSocks5Port() int32 { + if x != nil && x.Socks5Port != nil { + return *x.Socks5Port + } + return 0 +} + +func (x *NetworkConfig) GetDisableUdpHolePunching() bool { + if x != nil && x.DisableUdpHolePunching != nil { + return *x.DisableUdpHolePunching + } + return false +} + +func (x *NetworkConfig) GetMtu() int32 { + if x != nil && x.Mtu != nil { + return *x.Mtu + } + return 0 +} + +func (x *NetworkConfig) GetMappedListeners() []string { + if x != nil { + return x.MappedListeners + } + return nil +} + +func (x *NetworkConfig) GetEnableMagicDns() bool { + if x != nil && x.EnableMagicDns != nil { + return *x.EnableMagicDns + } + return false +} + +func (x *NetworkConfig) GetEnablePrivateMode() bool { + if x != nil && x.EnablePrivateMode != nil { + return *x.EnablePrivateMode + } + return false +} + +func (x *NetworkConfig) GetEnableQuicProxy() bool { + if x != nil && x.EnableQuicProxy != nil { + return *x.EnableQuicProxy + } + return false +} + +func (x *NetworkConfig) GetDisableQuicInput() bool { + if x != nil && x.DisableQuicInput != nil { + return *x.DisableQuicInput + } + return false +} + +// Deprecated: Marked as deprecated in api_manage.proto. +func (x *NetworkConfig) GetQuicListenPort() int32 { + if x != nil && x.QuicListenPort != nil { + return *x.QuicListenPort + } + return 0 +} + +func (x *NetworkConfig) GetPortForwards() []*PortForwardConfig { + if x != nil { + return x.PortForwards + } + return nil +} + +func (x *NetworkConfig) GetDisableSymHolePunching() bool { + if x != nil && x.DisableSymHolePunching != nil { + return *x.DisableSymHolePunching + } + return false +} + +func (x *NetworkConfig) GetP2POnly() bool { + if x != nil && x.P2POnly != nil { + return *x.P2POnly + } + return false +} + +func (x *NetworkConfig) GetDataCompressAlgo() common.CompressionAlgoPb { + if x != nil && x.DataCompressAlgo != nil { + return *x.DataCompressAlgo + } + return common.CompressionAlgoPb(0) +} + +func (x *NetworkConfig) GetEncryptionAlgorithm() string { + if x != nil && x.EncryptionAlgorithm != nil { + return *x.EncryptionAlgorithm + } + return "" +} + +func (x *NetworkConfig) GetDisableTcpHolePunching() bool { + if x != nil && x.DisableTcpHolePunching != nil { + return *x.DisableTcpHolePunching + } + return false +} + +func (x *NetworkConfig) GetSecureMode() *common.SecureModeConfig { + if x != nil { + return x.SecureMode + } + return nil +} + +func (x *NetworkConfig) GetAcl() *acl.Acl { + if x != nil { + return x.Acl + } + return nil +} + +func (x *NetworkConfig) GetCredentialFile() string { + if x != nil && x.CredentialFile != nil { + return *x.CredentialFile + } + return "" +} + +func (x *NetworkConfig) GetLazyP2P() bool { + if x != nil && x.LazyP2P != nil { + return *x.LazyP2P + } + return false +} + +func (x *NetworkConfig) GetNeedP2P() bool { + if x != nil && x.NeedP2P != nil { + return *x.NeedP2P + } + return false +} + +func (x *NetworkConfig) GetInstanceRecvBpsLimit() uint64 { + if x != nil && x.InstanceRecvBpsLimit != nil { + return *x.InstanceRecvBpsLimit + } + return 0 +} + +func (x *NetworkConfig) GetDisableUpnp() bool { + if x != nil && x.DisableUpnp != nil { + return *x.DisableUpnp + } + return false +} + +func (x *NetworkConfig) GetIpv6PublicAddrProvider() bool { + if x != nil && x.Ipv6PublicAddrProvider != nil { + return *x.Ipv6PublicAddrProvider + } + return false +} + +func (x *NetworkConfig) GetIpv6PublicAddrAuto() bool { + if x != nil && x.Ipv6PublicAddrAuto != nil { + return *x.Ipv6PublicAddrAuto + } + return false +} + +func (x *NetworkConfig) GetIpv6PublicAddrPrefix() string { + if x != nil && x.Ipv6PublicAddrPrefix != nil { + return *x.Ipv6PublicAddrPrefix + } + return "" +} + +func (x *NetworkConfig) GetDisableRelayData() bool { + if x != nil && x.DisableRelayData != nil { + return *x.DisableRelayData + } + return false +} + +func (x *NetworkConfig) GetEnableUdpBroadcastRelay() bool { + if x != nil && x.EnableUdpBroadcastRelay != nil { + return *x.EnableUdpBroadcastRelay + } + return false +} + +func (x *NetworkConfig) GetSocketMark() uint32 { + if x != nil && x.SocketMark != nil { + return *x.SocketMark + } + return 0 +} + +func (x *NetworkConfig) GetPeers() []*NetworkPeerConfig { + if x != nil { + return x.Peers + } + return nil +} + +type NetworkPeerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"` + PeerPublicKey *string `protobuf:"bytes,2,opt,name=peer_public_key,json=peerPublicKey,proto3,oneof" json:"peer_public_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkPeerConfig) Reset() { + *x = NetworkPeerConfig{} + mi := &file_api_manage_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkPeerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkPeerConfig) ProtoMessage() {} + +func (x *NetworkPeerConfig) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkPeerConfig.ProtoReflect.Descriptor instead. +func (*NetworkPeerConfig) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{1} +} + +func (x *NetworkPeerConfig) GetUri() string { + if x != nil { + return x.Uri + } + return "" +} + +func (x *NetworkPeerConfig) GetPeerPublicKey() string { + if x != nil && x.PeerPublicKey != nil { + return *x.PeerPublicKey + } + return "" +} + +type PortForwardConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + BindIp string `protobuf:"bytes,1,opt,name=bind_ip,json=bindIp,proto3" json:"bind_ip,omitempty"` + BindPort uint32 `protobuf:"varint,2,opt,name=bind_port,json=bindPort,proto3" json:"bind_port,omitempty"` + DstIp string `protobuf:"bytes,3,opt,name=dst_ip,json=dstIp,proto3" json:"dst_ip,omitempty"` + DstPort uint32 `protobuf:"varint,4,opt,name=dst_port,json=dstPort,proto3" json:"dst_port,omitempty"` + Proto string `protobuf:"bytes,5,opt,name=proto,proto3" json:"proto,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PortForwardConfig) Reset() { + *x = PortForwardConfig{} + mi := &file_api_manage_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortForwardConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortForwardConfig) ProtoMessage() {} + +func (x *PortForwardConfig) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PortForwardConfig.ProtoReflect.Descriptor instead. +func (*PortForwardConfig) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{2} +} + +func (x *PortForwardConfig) GetBindIp() string { + if x != nil { + return x.BindIp + } + return "" +} + +func (x *PortForwardConfig) GetBindPort() uint32 { + if x != nil { + return x.BindPort + } + return 0 +} + +func (x *PortForwardConfig) GetDstIp() string { + if x != nil { + return x.DstIp + } + return "" +} + +func (x *PortForwardConfig) GetDstPort() uint32 { + if x != nil { + return x.DstPort + } + return 0 +} + +func (x *PortForwardConfig) GetProto() string { + if x != nil { + return x.Proto + } + return "" +} + +type MyNodeInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + VirtualIpv4 *common.Ipv4Inet `protobuf:"bytes,1,opt,name=virtual_ipv4,json=virtualIpv4,proto3" json:"virtual_ipv4,omitempty"` + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Ips *peer_rpc.GetIpListResponse `protobuf:"bytes,4,opt,name=ips,proto3" json:"ips,omitempty"` + StunInfo *common.StunInfo `protobuf:"bytes,5,opt,name=stun_info,json=stunInfo,proto3" json:"stun_info,omitempty"` + Listeners []*common.Url `protobuf:"bytes,6,rep,name=listeners,proto3" json:"listeners,omitempty"` + VpnPortalCfg *string `protobuf:"bytes,7,opt,name=vpn_portal_cfg,json=vpnPortalCfg,proto3,oneof" json:"vpn_portal_cfg,omitempty"` + PeerId uint32 `protobuf:"varint,8,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MyNodeInfo) Reset() { + *x = MyNodeInfo{} + mi := &file_api_manage_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MyNodeInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MyNodeInfo) ProtoMessage() {} + +func (x *MyNodeInfo) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MyNodeInfo.ProtoReflect.Descriptor instead. +func (*MyNodeInfo) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{3} +} + +func (x *MyNodeInfo) GetVirtualIpv4() *common.Ipv4Inet { + if x != nil { + return x.VirtualIpv4 + } + return nil +} + +func (x *MyNodeInfo) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *MyNodeInfo) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *MyNodeInfo) GetIps() *peer_rpc.GetIpListResponse { + if x != nil { + return x.Ips + } + return nil +} + +func (x *MyNodeInfo) GetStunInfo() *common.StunInfo { + if x != nil { + return x.StunInfo + } + return nil +} + +func (x *MyNodeInfo) GetListeners() []*common.Url { + if x != nil { + return x.Listeners + } + return nil +} + +func (x *MyNodeInfo) GetVpnPortalCfg() string { + if x != nil && x.VpnPortalCfg != nil { + return *x.VpnPortalCfg + } + return "" +} + +func (x *MyNodeInfo) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +type NetworkInstanceRunningInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + DevName string `protobuf:"bytes,1,opt,name=dev_name,json=devName,proto3" json:"dev_name,omitempty"` + MyNodeInfo *MyNodeInfo `protobuf:"bytes,2,opt,name=my_node_info,json=myNodeInfo,proto3" json:"my_node_info,omitempty"` + Events []string `protobuf:"bytes,3,rep,name=events,proto3" json:"events,omitempty"` + Routes []*instance.Route `protobuf:"bytes,4,rep,name=routes,proto3" json:"routes,omitempty"` + Peers []*instance.PeerInfo `protobuf:"bytes,5,rep,name=peers,proto3" json:"peers,omitempty"` + PeerRoutePairs []*instance.PeerRoutePair `protobuf:"bytes,6,rep,name=peer_route_pairs,json=peerRoutePairs,proto3" json:"peer_route_pairs,omitempty"` + Running bool `protobuf:"varint,7,opt,name=running,proto3" json:"running,omitempty"` + ErrorMsg *string `protobuf:"bytes,8,opt,name=error_msg,json=errorMsg,proto3,oneof" json:"error_msg,omitempty"` + ForeignNetworkSummary *peer_rpc.RouteForeignNetworkSummary `protobuf:"bytes,9,opt,name=foreign_network_summary,json=foreignNetworkSummary,proto3" json:"foreign_network_summary,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkInstanceRunningInfo) Reset() { + *x = NetworkInstanceRunningInfo{} + mi := &file_api_manage_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkInstanceRunningInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkInstanceRunningInfo) ProtoMessage() {} + +func (x *NetworkInstanceRunningInfo) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkInstanceRunningInfo.ProtoReflect.Descriptor instead. +func (*NetworkInstanceRunningInfo) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{4} +} + +func (x *NetworkInstanceRunningInfo) GetDevName() string { + if x != nil { + return x.DevName + } + return "" +} + +func (x *NetworkInstanceRunningInfo) GetMyNodeInfo() *MyNodeInfo { + if x != nil { + return x.MyNodeInfo + } + return nil +} + +func (x *NetworkInstanceRunningInfo) GetEvents() []string { + if x != nil { + return x.Events + } + return nil +} + +func (x *NetworkInstanceRunningInfo) GetRoutes() []*instance.Route { + if x != nil { + return x.Routes + } + return nil +} + +func (x *NetworkInstanceRunningInfo) GetPeers() []*instance.PeerInfo { + if x != nil { + return x.Peers + } + return nil +} + +func (x *NetworkInstanceRunningInfo) GetPeerRoutePairs() []*instance.PeerRoutePair { + if x != nil { + return x.PeerRoutePairs + } + return nil +} + +func (x *NetworkInstanceRunningInfo) GetRunning() bool { + if x != nil { + return x.Running + } + return false +} + +func (x *NetworkInstanceRunningInfo) GetErrorMsg() string { + if x != nil && x.ErrorMsg != nil { + return *x.ErrorMsg + } + return "" +} + +func (x *NetworkInstanceRunningInfo) GetForeignNetworkSummary() *peer_rpc.RouteForeignNetworkSummary { + if x != nil { + return x.ForeignNetworkSummary + } + return nil +} + +type NetworkInstanceRunningInfoMap struct { + state protoimpl.MessageState `protogen:"open.v1"` + Map map[string]*NetworkInstanceRunningInfo `protobuf:"bytes,1,rep,name=map,proto3" json:"map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkInstanceRunningInfoMap) Reset() { + *x = NetworkInstanceRunningInfoMap{} + mi := &file_api_manage_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkInstanceRunningInfoMap) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkInstanceRunningInfoMap) ProtoMessage() {} + +func (x *NetworkInstanceRunningInfoMap) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkInstanceRunningInfoMap.ProtoReflect.Descriptor instead. +func (*NetworkInstanceRunningInfoMap) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{5} +} + +func (x *NetworkInstanceRunningInfoMap) GetMap() map[string]*NetworkInstanceRunningInfo { + if x != nil { + return x.Map + } + return nil +} + +type NetworkMeta struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstId *common.UUID `protobuf:"bytes,1,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + NetworkName string `protobuf:"bytes,2,opt,name=network_name,json=networkName,proto3" json:"network_name,omitempty"` + ConfigPermission uint32 `protobuf:"varint,3,opt,name=config_permission,json=configPermission,proto3" json:"config_permission,omitempty"` + InstanceName string `protobuf:"bytes,4,opt,name=instance_name,json=instanceName,proto3" json:"instance_name,omitempty"` + Source ConfigSource `protobuf:"varint,5,opt,name=source,proto3,enum=api.manage.ConfigSource" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkMeta) Reset() { + *x = NetworkMeta{} + mi := &file_api_manage_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkMeta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkMeta) ProtoMessage() {} + +func (x *NetworkMeta) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkMeta.ProtoReflect.Descriptor instead. +func (*NetworkMeta) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{6} +} + +func (x *NetworkMeta) GetInstId() *common.UUID { + if x != nil { + return x.InstId + } + return nil +} + +func (x *NetworkMeta) GetNetworkName() string { + if x != nil { + return x.NetworkName + } + return "" +} + +func (x *NetworkMeta) GetConfigPermission() uint32 { + if x != nil { + return x.ConfigPermission + } + return 0 +} + +func (x *NetworkMeta) GetInstanceName() string { + if x != nil { + return x.InstanceName + } + return "" +} + +func (x *NetworkMeta) GetSource() ConfigSource { + if x != nil { + return x.Source + } + return ConfigSource_ConfigSourceUnspecified +} + +type ValidateConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *NetworkConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateConfigRequest) Reset() { + *x = ValidateConfigRequest{} + mi := &file_api_manage_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateConfigRequest) ProtoMessage() {} + +func (x *ValidateConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateConfigRequest.ProtoReflect.Descriptor instead. +func (*ValidateConfigRequest) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{7} +} + +func (x *ValidateConfigRequest) GetConfig() *NetworkConfig { + if x != nil { + return x.Config + } + return nil +} + +type ValidateConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + TomlConfig string `protobuf:"bytes,1,opt,name=toml_config,json=tomlConfig,proto3" json:"toml_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateConfigResponse) Reset() { + *x = ValidateConfigResponse{} + mi := &file_api_manage_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateConfigResponse) ProtoMessage() {} + +func (x *ValidateConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateConfigResponse.ProtoReflect.Descriptor instead. +func (*ValidateConfigResponse) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{8} +} + +func (x *ValidateConfigResponse) GetTomlConfig() string { + if x != nil { + return x.TomlConfig + } + return "" +} + +type RunNetworkInstanceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstId *common.UUID `protobuf:"bytes,1,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + Config *NetworkConfig `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + Overwrite bool `protobuf:"varint,3,opt,name=overwrite,proto3" json:"overwrite,omitempty"` + Source ConfigSource `protobuf:"varint,4,opt,name=source,proto3,enum=api.manage.ConfigSource" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RunNetworkInstanceRequest) Reset() { + *x = RunNetworkInstanceRequest{} + mi := &file_api_manage_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RunNetworkInstanceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunNetworkInstanceRequest) ProtoMessage() {} + +func (x *RunNetworkInstanceRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunNetworkInstanceRequest.ProtoReflect.Descriptor instead. +func (*RunNetworkInstanceRequest) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{9} +} + +func (x *RunNetworkInstanceRequest) GetInstId() *common.UUID { + if x != nil { + return x.InstId + } + return nil +} + +func (x *RunNetworkInstanceRequest) GetConfig() *NetworkConfig { + if x != nil { + return x.Config + } + return nil +} + +func (x *RunNetworkInstanceRequest) GetOverwrite() bool { + if x != nil { + return x.Overwrite + } + return false +} + +func (x *RunNetworkInstanceRequest) GetSource() ConfigSource { + if x != nil { + return x.Source + } + return ConfigSource_ConfigSourceUnspecified +} + +type RunNetworkInstanceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstId *common.UUID `protobuf:"bytes,1,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RunNetworkInstanceResponse) Reset() { + *x = RunNetworkInstanceResponse{} + mi := &file_api_manage_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RunNetworkInstanceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunNetworkInstanceResponse) ProtoMessage() {} + +func (x *RunNetworkInstanceResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunNetworkInstanceResponse.ProtoReflect.Descriptor instead. +func (*RunNetworkInstanceResponse) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{10} +} + +func (x *RunNetworkInstanceResponse) GetInstId() *common.UUID { + if x != nil { + return x.InstId + } + return nil +} + +type RetainNetworkInstanceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstIds []*common.UUID `protobuf:"bytes,1,rep,name=inst_ids,json=instIds,proto3" json:"inst_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetainNetworkInstanceRequest) Reset() { + *x = RetainNetworkInstanceRequest{} + mi := &file_api_manage_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetainNetworkInstanceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetainNetworkInstanceRequest) ProtoMessage() {} + +func (x *RetainNetworkInstanceRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetainNetworkInstanceRequest.ProtoReflect.Descriptor instead. +func (*RetainNetworkInstanceRequest) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{11} +} + +func (x *RetainNetworkInstanceRequest) GetInstIds() []*common.UUID { + if x != nil { + return x.InstIds + } + return nil +} + +type RetainNetworkInstanceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + RemainInstIds []*common.UUID `protobuf:"bytes,1,rep,name=remain_inst_ids,json=remainInstIds,proto3" json:"remain_inst_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetainNetworkInstanceResponse) Reset() { + *x = RetainNetworkInstanceResponse{} + mi := &file_api_manage_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetainNetworkInstanceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetainNetworkInstanceResponse) ProtoMessage() {} + +func (x *RetainNetworkInstanceResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetainNetworkInstanceResponse.ProtoReflect.Descriptor instead. +func (*RetainNetworkInstanceResponse) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{12} +} + +func (x *RetainNetworkInstanceResponse) GetRemainInstIds() []*common.UUID { + if x != nil { + return x.RemainInstIds + } + return nil +} + +type CollectNetworkInfoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstIds []*common.UUID `protobuf:"bytes,1,rep,name=inst_ids,json=instIds,proto3" json:"inst_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CollectNetworkInfoRequest) Reset() { + *x = CollectNetworkInfoRequest{} + mi := &file_api_manage_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CollectNetworkInfoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CollectNetworkInfoRequest) ProtoMessage() {} + +func (x *CollectNetworkInfoRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CollectNetworkInfoRequest.ProtoReflect.Descriptor instead. +func (*CollectNetworkInfoRequest) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{13} +} + +func (x *CollectNetworkInfoRequest) GetInstIds() []*common.UUID { + if x != nil { + return x.InstIds + } + return nil +} + +type CollectNetworkInfoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Info *NetworkInstanceRunningInfoMap `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CollectNetworkInfoResponse) Reset() { + *x = CollectNetworkInfoResponse{} + mi := &file_api_manage_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CollectNetworkInfoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CollectNetworkInfoResponse) ProtoMessage() {} + +func (x *CollectNetworkInfoResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CollectNetworkInfoResponse.ProtoReflect.Descriptor instead. +func (*CollectNetworkInfoResponse) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{14} +} + +func (x *CollectNetworkInfoResponse) GetInfo() *NetworkInstanceRunningInfoMap { + if x != nil { + return x.Info + } + return nil +} + +type ListNetworkInstanceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListNetworkInstanceRequest) Reset() { + *x = ListNetworkInstanceRequest{} + mi := &file_api_manage_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNetworkInstanceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNetworkInstanceRequest) ProtoMessage() {} + +func (x *ListNetworkInstanceRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNetworkInstanceRequest.ProtoReflect.Descriptor instead. +func (*ListNetworkInstanceRequest) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{15} +} + +type ListNetworkInstanceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstIds []*common.UUID `protobuf:"bytes,1,rep,name=inst_ids,json=instIds,proto3" json:"inst_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListNetworkInstanceResponse) Reset() { + *x = ListNetworkInstanceResponse{} + mi := &file_api_manage_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNetworkInstanceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNetworkInstanceResponse) ProtoMessage() {} + +func (x *ListNetworkInstanceResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNetworkInstanceResponse.ProtoReflect.Descriptor instead. +func (*ListNetworkInstanceResponse) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{16} +} + +func (x *ListNetworkInstanceResponse) GetInstIds() []*common.UUID { + if x != nil { + return x.InstIds + } + return nil +} + +type DeleteNetworkInstanceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstIds []*common.UUID `protobuf:"bytes,1,rep,name=inst_ids,json=instIds,proto3" json:"inst_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteNetworkInstanceRequest) Reset() { + *x = DeleteNetworkInstanceRequest{} + mi := &file_api_manage_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteNetworkInstanceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteNetworkInstanceRequest) ProtoMessage() {} + +func (x *DeleteNetworkInstanceRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteNetworkInstanceRequest.ProtoReflect.Descriptor instead. +func (*DeleteNetworkInstanceRequest) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{17} +} + +func (x *DeleteNetworkInstanceRequest) GetInstIds() []*common.UUID { + if x != nil { + return x.InstIds + } + return nil +} + +type DeleteNetworkInstanceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + RemainInstIds []*common.UUID `protobuf:"bytes,1,rep,name=remain_inst_ids,json=remainInstIds,proto3" json:"remain_inst_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteNetworkInstanceResponse) Reset() { + *x = DeleteNetworkInstanceResponse{} + mi := &file_api_manage_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteNetworkInstanceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteNetworkInstanceResponse) ProtoMessage() {} + +func (x *DeleteNetworkInstanceResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteNetworkInstanceResponse.ProtoReflect.Descriptor instead. +func (*DeleteNetworkInstanceResponse) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{18} +} + +func (x *DeleteNetworkInstanceResponse) GetRemainInstIds() []*common.UUID { + if x != nil { + return x.RemainInstIds + } + return nil +} + +type GetNetworkInstanceConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstId *common.UUID `protobuf:"bytes,1,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetNetworkInstanceConfigRequest) Reset() { + *x = GetNetworkInstanceConfigRequest{} + mi := &file_api_manage_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetNetworkInstanceConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetNetworkInstanceConfigRequest) ProtoMessage() {} + +func (x *GetNetworkInstanceConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetNetworkInstanceConfigRequest.ProtoReflect.Descriptor instead. +func (*GetNetworkInstanceConfigRequest) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{19} +} + +func (x *GetNetworkInstanceConfigRequest) GetInstId() *common.UUID { + if x != nil { + return x.InstId + } + return nil +} + +type GetNetworkInstanceConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *NetworkConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + Source ConfigSource `protobuf:"varint,2,opt,name=source,proto3,enum=api.manage.ConfigSource" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetNetworkInstanceConfigResponse) Reset() { + *x = GetNetworkInstanceConfigResponse{} + mi := &file_api_manage_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetNetworkInstanceConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetNetworkInstanceConfigResponse) ProtoMessage() {} + +func (x *GetNetworkInstanceConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetNetworkInstanceConfigResponse.ProtoReflect.Descriptor instead. +func (*GetNetworkInstanceConfigResponse) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{20} +} + +func (x *GetNetworkInstanceConfigResponse) GetConfig() *NetworkConfig { + if x != nil { + return x.Config + } + return nil +} + +func (x *GetNetworkInstanceConfigResponse) GetSource() ConfigSource { + if x != nil { + return x.Source + } + return ConfigSource_ConfigSourceUnspecified +} + +type ListNetworkInstanceMetaRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstIds []*common.UUID `protobuf:"bytes,1,rep,name=inst_ids,json=instIds,proto3" json:"inst_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListNetworkInstanceMetaRequest) Reset() { + *x = ListNetworkInstanceMetaRequest{} + mi := &file_api_manage_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNetworkInstanceMetaRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNetworkInstanceMetaRequest) ProtoMessage() {} + +func (x *ListNetworkInstanceMetaRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNetworkInstanceMetaRequest.ProtoReflect.Descriptor instead. +func (*ListNetworkInstanceMetaRequest) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{21} +} + +func (x *ListNetworkInstanceMetaRequest) GetInstIds() []*common.UUID { + if x != nil { + return x.InstIds + } + return nil +} + +type ListNetworkInstanceMetaResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metas []*NetworkMeta `protobuf:"bytes,1,rep,name=metas,proto3" json:"metas,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListNetworkInstanceMetaResponse) Reset() { + *x = ListNetworkInstanceMetaResponse{} + mi := &file_api_manage_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNetworkInstanceMetaResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNetworkInstanceMetaResponse) ProtoMessage() {} + +func (x *ListNetworkInstanceMetaResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_manage_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNetworkInstanceMetaResponse.ProtoReflect.Descriptor instead. +func (*ListNetworkInstanceMetaResponse) Descriptor() ([]byte, []int) { + return file_api_manage_proto_rawDescGZIP(), []int{22} +} + +func (x *ListNetworkInstanceMetaResponse) GetMetas() []*NetworkMeta { + if x != nil { + return x.Metas + } + return nil +} + +var File_api_manage_proto protoreflect.FileDescriptor + +const file_api_manage_proto_rawDesc = "" + + "\n" + + "\x10api_manage.proto\x12\n" + + "api.manage\x1a\fcommon.proto\x1a\x0epeer_rpc.proto\x1a\x12api_instance.proto\x1a\tacl.proto\"\xef!\n" + + "\rNetworkConfig\x12$\n" + + "\vinstance_id\x18\x01 \x01(\tH\x00R\n" + + "instanceId\x88\x01\x01\x12\x17\n" + + "\x04dhcp\x18\x02 \x01(\bH\x01R\x04dhcp\x88\x01\x01\x12&\n" + + "\fvirtual_ipv4\x18\x03 \x01(\tH\x02R\vvirtualIpv4\x88\x01\x01\x12*\n" + + "\x0enetwork_length\x18\x04 \x01(\x05H\x03R\rnetworkLength\x88\x01\x01\x12\x1f\n" + + "\bhostname\x18\x05 \x01(\tH\x04R\bhostname\x88\x01\x01\x12&\n" + + "\fnetwork_name\x18\x06 \x01(\tH\x05R\vnetworkName\x88\x01\x01\x12*\n" + + "\x0enetwork_secret\x18\a \x01(\tH\x06R\rnetworkSecret\x88\x01\x01\x12N\n" + + "\x11networking_method\x18\b \x01(\x0e2\x1c.api.manage.NetworkingMethodH\aR\x10networkingMethod\x88\x01\x01\x12/\n" + + "\x11public_server_url\x18\t \x01(\tH\bR\x0fpublicServerUrl\x88\x01\x01\x12\x1b\n" + + "\tpeer_urls\x18\n" + + " \x03(\tR\bpeerUrls\x12\x1f\n" + + "\vproxy_cidrs\x18\v \x03(\tR\n" + + "proxyCidrs\x12/\n" + + "\x11enable_vpn_portal\x18\f \x01(\bH\tR\x0fenableVpnPortal\x88\x01\x01\x128\n" + + "\x16vpn_portal_listen_port\x18\r \x01(\x05H\n" + + "R\x13vpnPortalListenPort\x88\x01\x01\x12G\n" + + "\x1evpn_portal_client_network_addr\x18\x0e \x01(\tH\vR\x1avpnPortalClientNetworkAddr\x88\x01\x01\x12E\n" + + "\x1dvpn_portal_client_network_len\x18\x0f \x01(\x05H\fR\x19vpnPortalClientNetworkLen\x88\x01\x01\x120\n" + + "\x11advanced_settings\x18\x10 \x01(\bH\rR\x10advancedSettings\x88\x01\x01\x12#\n" + + "\rlistener_urls\x18\x11 \x03(\tR\flistenerUrls\x12(\n" + + "\rlatency_first\x18\x13 \x01(\bH\x0eR\flatencyFirst\x88\x01\x01\x12\x1e\n" + + "\bdev_name\x18\x14 \x01(\tH\x0fR\adevName\x88\x01\x01\x12$\n" + + "\vuse_smoltcp\x18\x15 \x01(\bH\x10R\n" + + "useSmoltcp\x88\x01\x01\x12&\n" + + "\fdisable_ipv6\x18/ \x01(\bH\x11R\vdisableIpv6\x88\x01\x01\x12-\n" + + "\x10enable_kcp_proxy\x18\x16 \x01(\bH\x12R\x0eenableKcpProxy\x88\x01\x01\x12/\n" + + "\x11disable_kcp_input\x18\x17 \x01(\bH\x13R\x0fdisableKcpInput\x88\x01\x01\x12$\n" + + "\vdisable_p2p\x18\x18 \x01(\bH\x14R\n" + + "disableP2p\x88\x01\x01\x12$\n" + + "\vbind_device\x18\x19 \x01(\bH\x15R\n" + + "bindDevice\x88\x01\x01\x12\x1a\n" + + "\x06no_tun\x18\x1a \x01(\bH\x16R\x05noTun\x88\x01\x01\x12-\n" + + "\x10enable_exit_node\x18\x1b \x01(\bH\x17R\x0eenableExitNode\x88\x01\x01\x120\n" + + "\x12relay_all_peer_rpc\x18\x1c \x01(\bH\x18R\x0frelayAllPeerRpc\x88\x01\x01\x12&\n" + + "\fmulti_thread\x18\x1d \x01(\bH\x19R\vmultiThread\x88\x01\x01\x12H\n" + + "\x1eenable_relay_network_whitelist\x18\x1e \x01(\bH\x1aR\x1benableRelayNetworkWhitelist\x88\x01\x01\x126\n" + + "\x17relay_network_whitelist\x18\x1f \x03(\tR\x15relayNetworkWhitelist\x125\n" + + "\x14enable_manual_routes\x18 \x01(\bH\x1bR\x12enableManualRoutes\x88\x01\x01\x12\x16\n" + + "\x06routes\x18! \x03(\tR\x06routes\x12\x1d\n" + + "\n" + + "exit_nodes\x18\" \x03(\tR\texitNodes\x12:\n" + + "\x17proxy_forward_by_system\x18# \x01(\bH\x1cR\x14proxyForwardBySystem\x88\x01\x01\x122\n" + + "\x12disable_encryption\x18$ \x01(\bH\x1dR\x11disableEncryption\x88\x01\x01\x12(\n" + + "\renable_socks5\x18% \x01(\bH\x1eR\fenableSocks5\x88\x01\x01\x12$\n" + + "\vsocks5_port\x18& \x01(\x05H\x1fR\n" + + "socks5Port\x88\x01\x01\x12>\n" + + "\x19disable_udp_hole_punching\x18' \x01(\bH R\x16disableUdpHolePunching\x88\x01\x01\x12\x15\n" + + "\x03mtu\x18( \x01(\x05H!R\x03mtu\x88\x01\x01\x12)\n" + + "\x10mapped_listeners\x18) \x03(\tR\x0fmappedListeners\x12-\n" + + "\x10enable_magic_dns\x18* \x01(\bH\"R\x0eenableMagicDns\x88\x01\x01\x123\n" + + "\x13enable_private_mode\x18+ \x01(\bH#R\x11enablePrivateMode\x88\x01\x01\x12/\n" + + "\x11enable_quic_proxy\x18- \x01(\bH$R\x0fenableQuicProxy\x88\x01\x01\x121\n" + + "\x12disable_quic_input\x18. \x01(\bH%R\x10disableQuicInput\x88\x01\x01\x121\n" + + "\x10quic_listen_port\x182 \x01(\x05B\x02\x18\x01H&R\x0equicListenPort\x88\x01\x01\x12B\n" + + "\rport_forwards\x180 \x03(\v2\x1d.api.manage.PortForwardConfigR\fportForwards\x12>\n" + + "\x19disable_sym_hole_punching\x181 \x01(\bH'R\x16disableSymHolePunching\x88\x01\x01\x12\x1e\n" + + "\bp2p_only\x183 \x01(\bH(R\ap2pOnly\x88\x01\x01\x12L\n" + + "\x12data_compress_algo\x184 \x01(\x0e2\x19.common.CompressionAlgoPbH)R\x10dataCompressAlgo\x88\x01\x01\x126\n" + + "\x14encryption_algorithm\x185 \x01(\tH*R\x13encryptionAlgorithm\x88\x01\x01\x12>\n" + + "\x19disable_tcp_hole_punching\x186 \x01(\bH+R\x16disableTcpHolePunching\x88\x01\x01\x129\n" + + "\vsecure_mode\x187 \x01(\v2\x18.common.SecureModeConfigR\n" + + "secureMode\x12\x1f\n" + + "\x03acl\x188 \x01(\v2\b.acl.AclH,R\x03acl\x88\x01\x01\x12,\n" + + "\x0fcredential_file\x189 \x01(\tH-R\x0ecredentialFile\x88\x01\x01\x12\x1e\n" + + "\blazy_p2p\x18: \x01(\bH.R\alazyP2p\x88\x01\x01\x12\x1e\n" + + "\bneed_p2p\x18; \x01(\bH/R\aneedP2p\x88\x01\x01\x12:\n" + + "\x17instance_recv_bps_limit\x18< \x01(\x04H0R\x14instanceRecvBpsLimit\x88\x01\x01\x12&\n" + + "\fdisable_upnp\x18= \x01(\bH1R\vdisableUpnp\x88\x01\x01\x12>\n" + + "\x19ipv6_public_addr_provider\x18> \x01(\bH2R\x16ipv6PublicAddrProvider\x88\x01\x01\x126\n" + + "\x15ipv6_public_addr_auto\x18? \x01(\bH3R\x12ipv6PublicAddrAuto\x88\x01\x01\x12:\n" + + "\x17ipv6_public_addr_prefix\x18@ \x01(\tH4R\x14ipv6PublicAddrPrefix\x88\x01\x01\x121\n" + + "\x12disable_relay_data\x18A \x01(\bH5R\x10disableRelayData\x88\x01\x01\x12@\n" + + "\x1aenable_udp_broadcast_relay\x18B \x01(\bH6R\x17enableUdpBroadcastRelay\x88\x01\x01\x12$\n" + + "\vsocket_mark\x18C \x01(\rH7R\n" + + "socketMark\x88\x01\x01\x123\n" + + "\x05peers\x18D \x03(\v2\x1d.api.manage.NetworkPeerConfigR\x05peersB\x0e\n" + + "\f_instance_idB\a\n" + + "\x05_dhcpB\x0f\n" + + "\r_virtual_ipv4B\x11\n" + + "\x0f_network_lengthB\v\n" + + "\t_hostnameB\x0f\n" + + "\r_network_nameB\x11\n" + + "\x0f_network_secretB\x14\n" + + "\x12_networking_methodB\x14\n" + + "\x12_public_server_urlB\x14\n" + + "\x12_enable_vpn_portalB\x19\n" + + "\x17_vpn_portal_listen_portB!\n" + + "\x1f_vpn_portal_client_network_addrB \n" + + "\x1e_vpn_portal_client_network_lenB\x14\n" + + "\x12_advanced_settingsB\x10\n" + + "\x0e_latency_firstB\v\n" + + "\t_dev_nameB\x0e\n" + + "\f_use_smoltcpB\x0f\n" + + "\r_disable_ipv6B\x13\n" + + "\x11_enable_kcp_proxyB\x14\n" + + "\x12_disable_kcp_inputB\x0e\n" + + "\f_disable_p2pB\x0e\n" + + "\f_bind_deviceB\t\n" + + "\a_no_tunB\x13\n" + + "\x11_enable_exit_nodeB\x15\n" + + "\x13_relay_all_peer_rpcB\x0f\n" + + "\r_multi_threadB!\n" + + "\x1f_enable_relay_network_whitelistB\x17\n" + + "\x15_enable_manual_routesB\x1a\n" + + "\x18_proxy_forward_by_systemB\x15\n" + + "\x13_disable_encryptionB\x10\n" + + "\x0e_enable_socks5B\x0e\n" + + "\f_socks5_portB\x1c\n" + + "\x1a_disable_udp_hole_punchingB\x06\n" + + "\x04_mtuB\x13\n" + + "\x11_enable_magic_dnsB\x16\n" + + "\x14_enable_private_modeB\x14\n" + + "\x12_enable_quic_proxyB\x15\n" + + "\x13_disable_quic_inputB\x13\n" + + "\x11_quic_listen_portB\x1c\n" + + "\x1a_disable_sym_hole_punchingB\v\n" + + "\t_p2p_onlyB\x15\n" + + "\x13_data_compress_algoB\x17\n" + + "\x15_encryption_algorithmB\x1c\n" + + "\x1a_disable_tcp_hole_punchingB\x06\n" + + "\x04_aclB\x12\n" + + "\x10_credential_fileB\v\n" + + "\t_lazy_p2pB\v\n" + + "\t_need_p2pB\x1a\n" + + "\x18_instance_recv_bps_limitB\x0f\n" + + "\r_disable_upnpB\x1c\n" + + "\x1a_ipv6_public_addr_providerB\x18\n" + + "\x16_ipv6_public_addr_autoB\x1a\n" + + "\x18_ipv6_public_addr_prefixB\x15\n" + + "\x13_disable_relay_dataB\x1d\n" + + "\x1b_enable_udp_broadcast_relayB\x0e\n" + + "\f_socket_mark\"f\n" + + "\x11NetworkPeerConfig\x12\x10\n" + + "\x03uri\x18\x01 \x01(\tR\x03uri\x12+\n" + + "\x0fpeer_public_key\x18\x02 \x01(\tH\x00R\rpeerPublicKey\x88\x01\x01B\x12\n" + + "\x10_peer_public_key\"\x91\x01\n" + + "\x11PortForwardConfig\x12\x17\n" + + "\abind_ip\x18\x01 \x01(\tR\x06bindIp\x12\x1b\n" + + "\tbind_port\x18\x02 \x01(\rR\bbindPort\x12\x15\n" + + "\x06dst_ip\x18\x03 \x01(\tR\x05dstIp\x12\x19\n" + + "\bdst_port\x18\x04 \x01(\rR\adstPort\x12\x14\n" + + "\x05proto\x18\x05 \x01(\tR\x05proto\"\xd7\x02\n" + + "\n" + + "MyNodeInfo\x123\n" + + "\fvirtual_ipv4\x18\x01 \x01(\v2\x10.common.Ipv4InetR\vvirtualIpv4\x12\x1a\n" + + "\bhostname\x18\x02 \x01(\tR\bhostname\x12\x18\n" + + "\aversion\x18\x03 \x01(\tR\aversion\x12-\n" + + "\x03ips\x18\x04 \x01(\v2\x1b.peer_rpc.GetIpListResponseR\x03ips\x12-\n" + + "\tstun_info\x18\x05 \x01(\v2\x10.common.StunInfoR\bstunInfo\x12)\n" + + "\tlisteners\x18\x06 \x03(\v2\v.common.UrlR\tlisteners\x12)\n" + + "\x0evpn_portal_cfg\x18\a \x01(\tH\x00R\fvpnPortalCfg\x88\x01\x01\x12\x17\n" + + "\apeer_id\x18\b \x01(\rR\x06peerIdB\x11\n" + + "\x0f_vpn_portal_cfg\"\xd3\x03\n" + + "\x1aNetworkInstanceRunningInfo\x12\x19\n" + + "\bdev_name\x18\x01 \x01(\tR\adevName\x128\n" + + "\fmy_node_info\x18\x02 \x01(\v2\x16.api.manage.MyNodeInfoR\n" + + "myNodeInfo\x12\x16\n" + + "\x06events\x18\x03 \x03(\tR\x06events\x12+\n" + + "\x06routes\x18\x04 \x03(\v2\x13.api.instance.RouteR\x06routes\x12,\n" + + "\x05peers\x18\x05 \x03(\v2\x16.api.instance.PeerInfoR\x05peers\x12E\n" + + "\x10peer_route_pairs\x18\x06 \x03(\v2\x1b.api.instance.PeerRoutePairR\x0epeerRoutePairs\x12\x18\n" + + "\arunning\x18\a \x01(\bR\arunning\x12 \n" + + "\terror_msg\x18\b \x01(\tH\x00R\berrorMsg\x88\x01\x01\x12\\\n" + + "\x17foreign_network_summary\x18\t \x01(\v2$.peer_rpc.RouteForeignNetworkSummaryR\x15foreignNetworkSummaryB\f\n" + + "\n" + + "_error_msg\"\xc5\x01\n" + + "\x1dNetworkInstanceRunningInfoMap\x12D\n" + + "\x03map\x18\x01 \x03(\v22.api.manage.NetworkInstanceRunningInfoMap.MapEntryR\x03map\x1a^\n" + + "\bMapEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12<\n" + + "\x05value\x18\x02 \x01(\v2&.api.manage.NetworkInstanceRunningInfoR\x05value:\x028\x01\"\xdb\x01\n" + + "\vNetworkMeta\x12%\n" + + "\ainst_id\x18\x01 \x01(\v2\f.common.UUIDR\x06instId\x12!\n" + + "\fnetwork_name\x18\x02 \x01(\tR\vnetworkName\x12+\n" + + "\x11config_permission\x18\x03 \x01(\rR\x10configPermission\x12#\n" + + "\rinstance_name\x18\x04 \x01(\tR\finstanceName\x120\n" + + "\x06source\x18\x05 \x01(\x0e2\x18.api.manage.ConfigSourceR\x06source\"J\n" + + "\x15ValidateConfigRequest\x121\n" + + "\x06config\x18\x01 \x01(\v2\x19.api.manage.NetworkConfigR\x06config\"9\n" + + "\x16ValidateConfigResponse\x12\x1f\n" + + "\vtoml_config\x18\x01 \x01(\tR\n" + + "tomlConfig\"\xc5\x01\n" + + "\x19RunNetworkInstanceRequest\x12%\n" + + "\ainst_id\x18\x01 \x01(\v2\f.common.UUIDR\x06instId\x121\n" + + "\x06config\x18\x02 \x01(\v2\x19.api.manage.NetworkConfigR\x06config\x12\x1c\n" + + "\toverwrite\x18\x03 \x01(\bR\toverwrite\x120\n" + + "\x06source\x18\x04 \x01(\x0e2\x18.api.manage.ConfigSourceR\x06source\"C\n" + + "\x1aRunNetworkInstanceResponse\x12%\n" + + "\ainst_id\x18\x01 \x01(\v2\f.common.UUIDR\x06instId\"G\n" + + "\x1cRetainNetworkInstanceRequest\x12'\n" + + "\binst_ids\x18\x01 \x03(\v2\f.common.UUIDR\ainstIds\"U\n" + + "\x1dRetainNetworkInstanceResponse\x124\n" + + "\x0fremain_inst_ids\x18\x01 \x03(\v2\f.common.UUIDR\rremainInstIds\"D\n" + + "\x19CollectNetworkInfoRequest\x12'\n" + + "\binst_ids\x18\x01 \x03(\v2\f.common.UUIDR\ainstIds\"[\n" + + "\x1aCollectNetworkInfoResponse\x12=\n" + + "\x04info\x18\x01 \x01(\v2).api.manage.NetworkInstanceRunningInfoMapR\x04info\"\x1c\n" + + "\x1aListNetworkInstanceRequest\"F\n" + + "\x1bListNetworkInstanceResponse\x12'\n" + + "\binst_ids\x18\x01 \x03(\v2\f.common.UUIDR\ainstIds\"G\n" + + "\x1cDeleteNetworkInstanceRequest\x12'\n" + + "\binst_ids\x18\x01 \x03(\v2\f.common.UUIDR\ainstIds\"U\n" + + "\x1dDeleteNetworkInstanceResponse\x124\n" + + "\x0fremain_inst_ids\x18\x01 \x03(\v2\f.common.UUIDR\rremainInstIds\"H\n" + + "\x1fGetNetworkInstanceConfigRequest\x12%\n" + + "\ainst_id\x18\x01 \x01(\v2\f.common.UUIDR\x06instId\"\x87\x01\n" + + " GetNetworkInstanceConfigResponse\x121\n" + + "\x06config\x18\x01 \x01(\v2\x19.api.manage.NetworkConfigR\x06config\x120\n" + + "\x06source\x18\x02 \x01(\x0e2\x18.api.manage.ConfigSourceR\x06source\"I\n" + + "\x1eListNetworkInstanceMetaRequest\x12'\n" + + "\binst_ids\x18\x01 \x03(\v2\f.common.UUIDR\ainstIds\"P\n" + + "\x1fListNetworkInstanceMetaResponse\x12-\n" + + "\x05metas\x18\x01 \x03(\v2\x17.api.manage.NetworkMetaR\x05metas*@\n" + + "\x10NetworkingMethod\x12\x10\n" + + "\fPublicServer\x10\x00\x12\n" + + "\n" + + "\x06Manual\x10\x01\x12\x0e\n" + + "\n" + + "Standalone\x10\x02*V\n" + + "\fConfigSource\x12\x1b\n" + + "\x17ConfigSourceUnspecified\x10\x00\x12\x14\n" + + "\x10ConfigSourceUser\x10\x01\x12\x13\n" + + "\x0fConfigSourceWeb\x10\x022\xf4\x06\n" + + "\x10WebClientService\x12Y\n" + + "\x0eValidateConfig\x12!.api.manage.ValidateConfigRequest\x1a\".api.manage.ValidateConfigResponse\"\x00\x12e\n" + + "\x12RunNetworkInstance\x12%.api.manage.RunNetworkInstanceRequest\x1a&.api.manage.RunNetworkInstanceResponse\"\x00\x12n\n" + + "\x15RetainNetworkInstance\x12(.api.manage.RetainNetworkInstanceRequest\x1a).api.manage.RetainNetworkInstanceResponse\"\x00\x12e\n" + + "\x12CollectNetworkInfo\x12%.api.manage.CollectNetworkInfoRequest\x1a&.api.manage.CollectNetworkInfoResponse\"\x00\x12h\n" + + "\x13ListNetworkInstance\x12&.api.manage.ListNetworkInstanceRequest\x1a'.api.manage.ListNetworkInstanceResponse\"\x00\x12n\n" + + "\x15DeleteNetworkInstance\x12(.api.manage.DeleteNetworkInstanceRequest\x1a).api.manage.DeleteNetworkInstanceResponse\"\x00\x12w\n" + + "\x18GetNetworkInstanceConfig\x12+.api.manage.GetNetworkInstanceConfigRequest\x1a,.api.manage.GetNetworkInstanceConfigResponse\"\x00\x12t\n" + + "\x17ListNetworkInstanceMeta\x12*.api.manage.ListNetworkInstanceMetaRequest\x1a+.api.manage.ListNetworkInstanceMetaResponse\"\x00b\x06proto3" + +var ( + file_api_manage_proto_rawDescOnce sync.Once + file_api_manage_proto_rawDescData []byte +) + +func file_api_manage_proto_rawDescGZIP() []byte { + file_api_manage_proto_rawDescOnce.Do(func() { + file_api_manage_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_manage_proto_rawDesc), len(file_api_manage_proto_rawDesc))) + }) + return file_api_manage_proto_rawDescData +} + +var file_api_manage_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_api_manage_proto_msgTypes = make([]protoimpl.MessageInfo, 24) +var file_api_manage_proto_goTypes = []any{ + (NetworkingMethod)(0), // 0: api.manage.NetworkingMethod + (ConfigSource)(0), // 1: api.manage.ConfigSource + (*NetworkConfig)(nil), // 2: api.manage.NetworkConfig + (*NetworkPeerConfig)(nil), // 3: api.manage.NetworkPeerConfig + (*PortForwardConfig)(nil), // 4: api.manage.PortForwardConfig + (*MyNodeInfo)(nil), // 5: api.manage.MyNodeInfo + (*NetworkInstanceRunningInfo)(nil), // 6: api.manage.NetworkInstanceRunningInfo + (*NetworkInstanceRunningInfoMap)(nil), // 7: api.manage.NetworkInstanceRunningInfoMap + (*NetworkMeta)(nil), // 8: api.manage.NetworkMeta + (*ValidateConfigRequest)(nil), // 9: api.manage.ValidateConfigRequest + (*ValidateConfigResponse)(nil), // 10: api.manage.ValidateConfigResponse + (*RunNetworkInstanceRequest)(nil), // 11: api.manage.RunNetworkInstanceRequest + (*RunNetworkInstanceResponse)(nil), // 12: api.manage.RunNetworkInstanceResponse + (*RetainNetworkInstanceRequest)(nil), // 13: api.manage.RetainNetworkInstanceRequest + (*RetainNetworkInstanceResponse)(nil), // 14: api.manage.RetainNetworkInstanceResponse + (*CollectNetworkInfoRequest)(nil), // 15: api.manage.CollectNetworkInfoRequest + (*CollectNetworkInfoResponse)(nil), // 16: api.manage.CollectNetworkInfoResponse + (*ListNetworkInstanceRequest)(nil), // 17: api.manage.ListNetworkInstanceRequest + (*ListNetworkInstanceResponse)(nil), // 18: api.manage.ListNetworkInstanceResponse + (*DeleteNetworkInstanceRequest)(nil), // 19: api.manage.DeleteNetworkInstanceRequest + (*DeleteNetworkInstanceResponse)(nil), // 20: api.manage.DeleteNetworkInstanceResponse + (*GetNetworkInstanceConfigRequest)(nil), // 21: api.manage.GetNetworkInstanceConfigRequest + (*GetNetworkInstanceConfigResponse)(nil), // 22: api.manage.GetNetworkInstanceConfigResponse + (*ListNetworkInstanceMetaRequest)(nil), // 23: api.manage.ListNetworkInstanceMetaRequest + (*ListNetworkInstanceMetaResponse)(nil), // 24: api.manage.ListNetworkInstanceMetaResponse + nil, // 25: api.manage.NetworkInstanceRunningInfoMap.MapEntry + (common.CompressionAlgoPb)(0), // 26: common.CompressionAlgoPb + (*common.SecureModeConfig)(nil), // 27: common.SecureModeConfig + (*acl.Acl)(nil), // 28: acl.Acl + (*common.Ipv4Inet)(nil), // 29: common.Ipv4Inet + (*peer_rpc.GetIpListResponse)(nil), // 30: peer_rpc.GetIpListResponse + (*common.StunInfo)(nil), // 31: common.StunInfo + (*common.Url)(nil), // 32: common.Url + (*instance.Route)(nil), // 33: api.instance.Route + (*instance.PeerInfo)(nil), // 34: api.instance.PeerInfo + (*instance.PeerRoutePair)(nil), // 35: api.instance.PeerRoutePair + (*peer_rpc.RouteForeignNetworkSummary)(nil), // 36: peer_rpc.RouteForeignNetworkSummary + (*common.UUID)(nil), // 37: common.UUID +} +var file_api_manage_proto_depIdxs = []int32{ + 0, // 0: api.manage.NetworkConfig.networking_method:type_name -> api.manage.NetworkingMethod + 4, // 1: api.manage.NetworkConfig.port_forwards:type_name -> api.manage.PortForwardConfig + 26, // 2: api.manage.NetworkConfig.data_compress_algo:type_name -> common.CompressionAlgoPb + 27, // 3: api.manage.NetworkConfig.secure_mode:type_name -> common.SecureModeConfig + 28, // 4: api.manage.NetworkConfig.acl:type_name -> acl.Acl + 3, // 5: api.manage.NetworkConfig.peers:type_name -> api.manage.NetworkPeerConfig + 29, // 6: api.manage.MyNodeInfo.virtual_ipv4:type_name -> common.Ipv4Inet + 30, // 7: api.manage.MyNodeInfo.ips:type_name -> peer_rpc.GetIpListResponse + 31, // 8: api.manage.MyNodeInfo.stun_info:type_name -> common.StunInfo + 32, // 9: api.manage.MyNodeInfo.listeners:type_name -> common.Url + 5, // 10: api.manage.NetworkInstanceRunningInfo.my_node_info:type_name -> api.manage.MyNodeInfo + 33, // 11: api.manage.NetworkInstanceRunningInfo.routes:type_name -> api.instance.Route + 34, // 12: api.manage.NetworkInstanceRunningInfo.peers:type_name -> api.instance.PeerInfo + 35, // 13: api.manage.NetworkInstanceRunningInfo.peer_route_pairs:type_name -> api.instance.PeerRoutePair + 36, // 14: api.manage.NetworkInstanceRunningInfo.foreign_network_summary:type_name -> peer_rpc.RouteForeignNetworkSummary + 25, // 15: api.manage.NetworkInstanceRunningInfoMap.map:type_name -> api.manage.NetworkInstanceRunningInfoMap.MapEntry + 37, // 16: api.manage.NetworkMeta.inst_id:type_name -> common.UUID + 1, // 17: api.manage.NetworkMeta.source:type_name -> api.manage.ConfigSource + 2, // 18: api.manage.ValidateConfigRequest.config:type_name -> api.manage.NetworkConfig + 37, // 19: api.manage.RunNetworkInstanceRequest.inst_id:type_name -> common.UUID + 2, // 20: api.manage.RunNetworkInstanceRequest.config:type_name -> api.manage.NetworkConfig + 1, // 21: api.manage.RunNetworkInstanceRequest.source:type_name -> api.manage.ConfigSource + 37, // 22: api.manage.RunNetworkInstanceResponse.inst_id:type_name -> common.UUID + 37, // 23: api.manage.RetainNetworkInstanceRequest.inst_ids:type_name -> common.UUID + 37, // 24: api.manage.RetainNetworkInstanceResponse.remain_inst_ids:type_name -> common.UUID + 37, // 25: api.manage.CollectNetworkInfoRequest.inst_ids:type_name -> common.UUID + 7, // 26: api.manage.CollectNetworkInfoResponse.info:type_name -> api.manage.NetworkInstanceRunningInfoMap + 37, // 27: api.manage.ListNetworkInstanceResponse.inst_ids:type_name -> common.UUID + 37, // 28: api.manage.DeleteNetworkInstanceRequest.inst_ids:type_name -> common.UUID + 37, // 29: api.manage.DeleteNetworkInstanceResponse.remain_inst_ids:type_name -> common.UUID + 37, // 30: api.manage.GetNetworkInstanceConfigRequest.inst_id:type_name -> common.UUID + 2, // 31: api.manage.GetNetworkInstanceConfigResponse.config:type_name -> api.manage.NetworkConfig + 1, // 32: api.manage.GetNetworkInstanceConfigResponse.source:type_name -> api.manage.ConfigSource + 37, // 33: api.manage.ListNetworkInstanceMetaRequest.inst_ids:type_name -> common.UUID + 8, // 34: api.manage.ListNetworkInstanceMetaResponse.metas:type_name -> api.manage.NetworkMeta + 6, // 35: api.manage.NetworkInstanceRunningInfoMap.MapEntry.value:type_name -> api.manage.NetworkInstanceRunningInfo + 9, // 36: api.manage.WebClientService.ValidateConfig:input_type -> api.manage.ValidateConfigRequest + 11, // 37: api.manage.WebClientService.RunNetworkInstance:input_type -> api.manage.RunNetworkInstanceRequest + 13, // 38: api.manage.WebClientService.RetainNetworkInstance:input_type -> api.manage.RetainNetworkInstanceRequest + 15, // 39: api.manage.WebClientService.CollectNetworkInfo:input_type -> api.manage.CollectNetworkInfoRequest + 17, // 40: api.manage.WebClientService.ListNetworkInstance:input_type -> api.manage.ListNetworkInstanceRequest + 19, // 41: api.manage.WebClientService.DeleteNetworkInstance:input_type -> api.manage.DeleteNetworkInstanceRequest + 21, // 42: api.manage.WebClientService.GetNetworkInstanceConfig:input_type -> api.manage.GetNetworkInstanceConfigRequest + 23, // 43: api.manage.WebClientService.ListNetworkInstanceMeta:input_type -> api.manage.ListNetworkInstanceMetaRequest + 10, // 44: api.manage.WebClientService.ValidateConfig:output_type -> api.manage.ValidateConfigResponse + 12, // 45: api.manage.WebClientService.RunNetworkInstance:output_type -> api.manage.RunNetworkInstanceResponse + 14, // 46: api.manage.WebClientService.RetainNetworkInstance:output_type -> api.manage.RetainNetworkInstanceResponse + 16, // 47: api.manage.WebClientService.CollectNetworkInfo:output_type -> api.manage.CollectNetworkInfoResponse + 18, // 48: api.manage.WebClientService.ListNetworkInstance:output_type -> api.manage.ListNetworkInstanceResponse + 20, // 49: api.manage.WebClientService.DeleteNetworkInstance:output_type -> api.manage.DeleteNetworkInstanceResponse + 22, // 50: api.manage.WebClientService.GetNetworkInstanceConfig:output_type -> api.manage.GetNetworkInstanceConfigResponse + 24, // 51: api.manage.WebClientService.ListNetworkInstanceMeta:output_type -> api.manage.ListNetworkInstanceMetaResponse + 44, // [44:52] is the sub-list for method output_type + 36, // [36:44] is the sub-list for method input_type + 36, // [36:36] is the sub-list for extension type_name + 36, // [36:36] is the sub-list for extension extendee + 0, // [0:36] is the sub-list for field type_name +} + +func init() { file_api_manage_proto_init() } +func file_api_manage_proto_init() { + if File_api_manage_proto != nil { + return + } + file_api_manage_proto_msgTypes[0].OneofWrappers = []any{} + file_api_manage_proto_msgTypes[1].OneofWrappers = []any{} + file_api_manage_proto_msgTypes[3].OneofWrappers = []any{} + file_api_manage_proto_msgTypes[4].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_manage_proto_rawDesc), len(file_api_manage_proto_rawDesc)), + NumEnums: 2, + NumMessages: 24, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_manage_proto_goTypes, + DependencyIndexes: file_api_manage_proto_depIdxs, + EnumInfos: file_api_manage_proto_enumTypes, + MessageInfos: file_api_manage_proto_msgTypes, + }.Build() + File_api_manage_proto = out.File + file_api_manage_proto_goTypes = nil + file_api_manage_proto_depIdxs = nil +} diff --git a/easytier-go/proto/common/common.pb.go b/easytier-go/proto/common/common.pb.go new file mode 100644 index 00000000..dd7622e3 --- /dev/null +++ b/easytier-go/proto/common/common.pb.go @@ -0,0 +1,2532 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.35.1 +// source: common.proto + +package common + +import ( + error1 "github.com/EasyTier/EasyTier/easytier-go/proto/error" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CompressionAlgoPb int32 + +const ( + CompressionAlgoPb_Invalid CompressionAlgoPb = 0 + CompressionAlgoPb_None CompressionAlgoPb = 1 + CompressionAlgoPb_Zstd CompressionAlgoPb = 2 +) + +// Enum value maps for CompressionAlgoPb. +var ( + CompressionAlgoPb_name = map[int32]string{ + 0: "Invalid", + 1: "None", + 2: "Zstd", + } + CompressionAlgoPb_value = map[string]int32{ + "Invalid": 0, + "None": 1, + "Zstd": 2, + } +) + +func (x CompressionAlgoPb) Enum() *CompressionAlgoPb { + p := new(CompressionAlgoPb) + *p = x + return p +} + +func (x CompressionAlgoPb) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CompressionAlgoPb) Descriptor() protoreflect.EnumDescriptor { + return file_common_proto_enumTypes[0].Descriptor() +} + +func (CompressionAlgoPb) Type() protoreflect.EnumType { + return &file_common_proto_enumTypes[0] +} + +func (x CompressionAlgoPb) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CompressionAlgoPb.Descriptor instead. +func (CompressionAlgoPb) EnumDescriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{0} +} + +type NatType int32 + +const ( + // has NAT; but own a single public IP, port is not changed + NatType_Unknown NatType = 0 + NatType_OpenInternet NatType = 1 + NatType_NoPAT NatType = 2 + NatType_FullCone NatType = 3 + NatType_Restricted NatType = 4 + NatType_PortRestricted NatType = 5 + NatType_Symmetric NatType = 6 + NatType_SymUdpFirewall NatType = 7 + NatType_SymmetricEasyInc NatType = 8 + NatType_SymmetricEasyDec NatType = 9 +) + +// Enum value maps for NatType. +var ( + NatType_name = map[int32]string{ + 0: "Unknown", + 1: "OpenInternet", + 2: "NoPAT", + 3: "FullCone", + 4: "Restricted", + 5: "PortRestricted", + 6: "Symmetric", + 7: "SymUdpFirewall", + 8: "SymmetricEasyInc", + 9: "SymmetricEasyDec", + } + NatType_value = map[string]int32{ + "Unknown": 0, + "OpenInternet": 1, + "NoPAT": 2, + "FullCone": 3, + "Restricted": 4, + "PortRestricted": 5, + "Symmetric": 6, + "SymUdpFirewall": 7, + "SymmetricEasyInc": 8, + "SymmetricEasyDec": 9, + } +) + +func (x NatType) Enum() *NatType { + p := new(NatType) + *p = x + return p +} + +func (x NatType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NatType) Descriptor() protoreflect.EnumDescriptor { + return file_common_proto_enumTypes[1].Descriptor() +} + +func (NatType) Type() protoreflect.EnumType { + return &file_common_proto_enumTypes[1] +} + +func (x NatType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NatType.Descriptor instead. +func (NatType) EnumDescriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{1} +} + +type SocketType int32 + +const ( + SocketType_TCP SocketType = 0 + SocketType_UDP SocketType = 1 +) + +// Enum value maps for SocketType. +var ( + SocketType_name = map[int32]string{ + 0: "TCP", + 1: "UDP", + } + SocketType_value = map[string]int32{ + "TCP": 0, + "UDP": 1, + } +) + +func (x SocketType) Enum() *SocketType { + p := new(SocketType) + *p = x + return p +} + +func (x SocketType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SocketType) Descriptor() protoreflect.EnumDescriptor { + return file_common_proto_enumTypes[2].Descriptor() +} + +func (SocketType) Type() protoreflect.EnumType { + return &file_common_proto_enumTypes[2] +} + +func (x SocketType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SocketType.Descriptor instead. +func (SocketType) EnumDescriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{2} +} + +type FlagsInConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + DefaultProtocol string `protobuf:"bytes,1,opt,name=default_protocol,json=defaultProtocol,proto3" json:"default_protocol,omitempty"` + DevName string `protobuf:"bytes,2,opt,name=dev_name,json=devName,proto3" json:"dev_name,omitempty"` + EnableEncryption bool `protobuf:"varint,3,opt,name=enable_encryption,json=enableEncryption,proto3" json:"enable_encryption,omitempty"` + EnableIpv6 bool `protobuf:"varint,4,opt,name=enable_ipv6,json=enableIpv6,proto3" json:"enable_ipv6,omitempty"` + Mtu uint32 `protobuf:"varint,5,opt,name=mtu,proto3" json:"mtu,omitempty"` + LatencyFirst bool `protobuf:"varint,6,opt,name=latency_first,json=latencyFirst,proto3" json:"latency_first,omitempty"` + EnableExitNode bool `protobuf:"varint,7,opt,name=enable_exit_node,json=enableExitNode,proto3" json:"enable_exit_node,omitempty"` + NoTun bool `protobuf:"varint,8,opt,name=no_tun,json=noTun,proto3" json:"no_tun,omitempty"` + UseSmoltcp bool `protobuf:"varint,9,opt,name=use_smoltcp,json=useSmoltcp,proto3" json:"use_smoltcp,omitempty"` + RelayNetworkWhitelist string `protobuf:"bytes,10,opt,name=relay_network_whitelist,json=relayNetworkWhitelist,proto3" json:"relay_network_whitelist,omitempty"` + DisableP2P bool `protobuf:"varint,11,opt,name=disable_p2p,json=disableP2p,proto3" json:"disable_p2p,omitempty"` + RelayAllPeerRpc bool `protobuf:"varint,12,opt,name=relay_all_peer_rpc,json=relayAllPeerRpc,proto3" json:"relay_all_peer_rpc,omitempty"` + DisableUdpHolePunching bool `protobuf:"varint,13,opt,name=disable_udp_hole_punching,json=disableUdpHolePunching,proto3" json:"disable_udp_hole_punching,omitempty"` + // string ipv6_listener = 14; [deprecated = true]; use -l udp://[::]:12345 + // instead + MultiThread bool `protobuf:"varint,15,opt,name=multi_thread,json=multiThread,proto3" json:"multi_thread,omitempty"` + DataCompressAlgo CompressionAlgoPb `protobuf:"varint,16,opt,name=data_compress_algo,json=dataCompressAlgo,proto3,enum=common.CompressionAlgoPb" json:"data_compress_algo,omitempty"` + BindDevice bool `protobuf:"varint,17,opt,name=bind_device,json=bindDevice,proto3" json:"bind_device,omitempty"` + // should we convert all tcp streams into kcp streams + EnableKcpProxy bool `protobuf:"varint,18,opt,name=enable_kcp_proxy,json=enableKcpProxy,proto3" json:"enable_kcp_proxy,omitempty"` + // does this peer allow kcp input + DisableKcpInput bool `protobuf:"varint,19,opt,name=disable_kcp_input,json=disableKcpInput,proto3" json:"disable_kcp_input,omitempty"` + // disable relay local network kcp packets + DisableRelayKcp bool `protobuf:"varint,20,opt,name=disable_relay_kcp,json=disableRelayKcp,proto3" json:"disable_relay_kcp,omitempty"` + ProxyForwardBySystem bool `protobuf:"varint,21,opt,name=proxy_forward_by_system,json=proxyForwardBySystem,proto3" json:"proxy_forward_by_system,omitempty"` + // enable magic dns or not + AcceptDns bool `protobuf:"varint,22,opt,name=accept_dns,json=acceptDns,proto3" json:"accept_dns,omitempty"` + // enable private mode + PrivateMode bool `protobuf:"varint,23,opt,name=private_mode,json=privateMode,proto3" json:"private_mode,omitempty"` + // should we convert all tcp streams into quic streams + EnableQuicProxy bool `protobuf:"varint,24,opt,name=enable_quic_proxy,json=enableQuicProxy,proto3" json:"enable_quic_proxy,omitempty"` + // does this peer allow quic input + DisableQuicInput bool `protobuf:"varint,25,opt,name=disable_quic_input,json=disableQuicInput,proto3" json:"disable_quic_input,omitempty"` + // disable relay local network quic packets + DisableRelayQuic bool `protobuf:"varint,35,opt,name=disable_relay_quic,json=disableRelayQuic,proto3" json:"disable_relay_quic,omitempty"` + // quic listen port + // + // Deprecated: Marked as deprecated in common.proto. + QuicListenPort uint32 `protobuf:"varint,33,opt,name=quic_listen_port,json=quicListenPort,proto3" json:"quic_listen_port,omitempty"` + // a global relay limit, only work for foreign network + ForeignRelayBpsLimit uint64 `protobuf:"varint,26,opt,name=foreign_relay_bps_limit,json=foreignRelayBpsLimit,proto3" json:"foreign_relay_bps_limit,omitempty"` + MultiThreadCount uint32 `protobuf:"varint,27,opt,name=multi_thread_count,json=multiThreadCount,proto3" json:"multi_thread_count,omitempty"` + // enable relay foreign network kcp packets + EnableRelayForeignNetworkKcp bool `protobuf:"varint,28,opt,name=enable_relay_foreign_network_kcp,json=enableRelayForeignNetworkKcp,proto3" json:"enable_relay_foreign_network_kcp,omitempty"` + // enable relay foreign network quic packets + EnableRelayForeignNetworkQuic bool `protobuf:"varint,36,opt,name=enable_relay_foreign_network_quic,json=enableRelayForeignNetworkQuic,proto3" json:"enable_relay_foreign_network_quic,omitempty"` + // encryption algorithm to use, empty string means default (aes-gcm) + EncryptionAlgorithm string `protobuf:"bytes,29,opt,name=encryption_algorithm,json=encryptionAlgorithm,proto3" json:"encryption_algorithm,omitempty"` + // disable symmetric nat hole punching, treat symmetric as cone when enabled + DisableSymHolePunching bool `protobuf:"varint,30,opt,name=disable_sym_hole_punching,json=disableSymHolePunching,proto3" json:"disable_sym_hole_punching,omitempty"` + // tld dns zone for magic dns + TldDnsZone string `protobuf:"bytes,31,opt,name=tld_dns_zone,json=tldDnsZone,proto3" json:"tld_dns_zone,omitempty"` + P2POnly bool `protobuf:"varint,32,opt,name=p2p_only,json=p2pOnly,proto3" json:"p2p_only,omitempty"` + DisableTcpHolePunching bool `protobuf:"varint,34,opt,name=disable_tcp_hole_punching,json=disableTcpHolePunching,proto3" json:"disable_tcp_hole_punching,omitempty"` + LazyP2P bool `protobuf:"varint,37,opt,name=lazy_p2p,json=lazyP2p,proto3" json:"lazy_p2p,omitempty"` + NeedP2P bool `protobuf:"varint,38,opt,name=need_p2p,json=needP2p,proto3" json:"need_p2p,omitempty"` + InstanceRecvBpsLimit uint64 `protobuf:"varint,39,opt,name=instance_recv_bps_limit,json=instanceRecvBpsLimit,proto3" json:"instance_recv_bps_limit,omitempty"` + DisableUpnp bool `protobuf:"varint,40,opt,name=disable_upnp,json=disableUpnp,proto3" json:"disable_upnp,omitempty"` + DisableRelayData bool `protobuf:"varint,41,opt,name=disable_relay_data,json=disableRelayData,proto3" json:"disable_relay_data,omitempty"` + EnableUdpBroadcastRelay bool `protobuf:"varint,42,opt,name=enable_udp_broadcast_relay,json=enableUdpBroadcastRelay,proto3" json:"enable_udp_broadcast_relay,omitempty"` + // Linux-only: SO_MARK (fwmark) value applied to every outbound underlay + // socket (TCP/UDP/QUIC/WS/WG connectors and listeners). Unset = leave + // SO_MARK untouched (kernel default 0). Any set value (including 0) is + // applied via setsockopt. Requires CAP_NET_ADMIN; silently ignored on + // non-Linux platforms. + SocketMark *uint32 `protobuf:"varint,43,opt,name=socket_mark,json=socketMark,proto3,oneof" json:"socket_mark,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FlagsInConfig) Reset() { + *x = FlagsInConfig{} + mi := &file_common_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FlagsInConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FlagsInConfig) ProtoMessage() {} + +func (x *FlagsInConfig) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FlagsInConfig.ProtoReflect.Descriptor instead. +func (*FlagsInConfig) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{0} +} + +func (x *FlagsInConfig) GetDefaultProtocol() string { + if x != nil { + return x.DefaultProtocol + } + return "" +} + +func (x *FlagsInConfig) GetDevName() string { + if x != nil { + return x.DevName + } + return "" +} + +func (x *FlagsInConfig) GetEnableEncryption() bool { + if x != nil { + return x.EnableEncryption + } + return false +} + +func (x *FlagsInConfig) GetEnableIpv6() bool { + if x != nil { + return x.EnableIpv6 + } + return false +} + +func (x *FlagsInConfig) GetMtu() uint32 { + if x != nil { + return x.Mtu + } + return 0 +} + +func (x *FlagsInConfig) GetLatencyFirst() bool { + if x != nil { + return x.LatencyFirst + } + return false +} + +func (x *FlagsInConfig) GetEnableExitNode() bool { + if x != nil { + return x.EnableExitNode + } + return false +} + +func (x *FlagsInConfig) GetNoTun() bool { + if x != nil { + return x.NoTun + } + return false +} + +func (x *FlagsInConfig) GetUseSmoltcp() bool { + if x != nil { + return x.UseSmoltcp + } + return false +} + +func (x *FlagsInConfig) GetRelayNetworkWhitelist() string { + if x != nil { + return x.RelayNetworkWhitelist + } + return "" +} + +func (x *FlagsInConfig) GetDisableP2P() bool { + if x != nil { + return x.DisableP2P + } + return false +} + +func (x *FlagsInConfig) GetRelayAllPeerRpc() bool { + if x != nil { + return x.RelayAllPeerRpc + } + return false +} + +func (x *FlagsInConfig) GetDisableUdpHolePunching() bool { + if x != nil { + return x.DisableUdpHolePunching + } + return false +} + +func (x *FlagsInConfig) GetMultiThread() bool { + if x != nil { + return x.MultiThread + } + return false +} + +func (x *FlagsInConfig) GetDataCompressAlgo() CompressionAlgoPb { + if x != nil { + return x.DataCompressAlgo + } + return CompressionAlgoPb_Invalid +} + +func (x *FlagsInConfig) GetBindDevice() bool { + if x != nil { + return x.BindDevice + } + return false +} + +func (x *FlagsInConfig) GetEnableKcpProxy() bool { + if x != nil { + return x.EnableKcpProxy + } + return false +} + +func (x *FlagsInConfig) GetDisableKcpInput() bool { + if x != nil { + return x.DisableKcpInput + } + return false +} + +func (x *FlagsInConfig) GetDisableRelayKcp() bool { + if x != nil { + return x.DisableRelayKcp + } + return false +} + +func (x *FlagsInConfig) GetProxyForwardBySystem() bool { + if x != nil { + return x.ProxyForwardBySystem + } + return false +} + +func (x *FlagsInConfig) GetAcceptDns() bool { + if x != nil { + return x.AcceptDns + } + return false +} + +func (x *FlagsInConfig) GetPrivateMode() bool { + if x != nil { + return x.PrivateMode + } + return false +} + +func (x *FlagsInConfig) GetEnableQuicProxy() bool { + if x != nil { + return x.EnableQuicProxy + } + return false +} + +func (x *FlagsInConfig) GetDisableQuicInput() bool { + if x != nil { + return x.DisableQuicInput + } + return false +} + +func (x *FlagsInConfig) GetDisableRelayQuic() bool { + if x != nil { + return x.DisableRelayQuic + } + return false +} + +// Deprecated: Marked as deprecated in common.proto. +func (x *FlagsInConfig) GetQuicListenPort() uint32 { + if x != nil { + return x.QuicListenPort + } + return 0 +} + +func (x *FlagsInConfig) GetForeignRelayBpsLimit() uint64 { + if x != nil { + return x.ForeignRelayBpsLimit + } + return 0 +} + +func (x *FlagsInConfig) GetMultiThreadCount() uint32 { + if x != nil { + return x.MultiThreadCount + } + return 0 +} + +func (x *FlagsInConfig) GetEnableRelayForeignNetworkKcp() bool { + if x != nil { + return x.EnableRelayForeignNetworkKcp + } + return false +} + +func (x *FlagsInConfig) GetEnableRelayForeignNetworkQuic() bool { + if x != nil { + return x.EnableRelayForeignNetworkQuic + } + return false +} + +func (x *FlagsInConfig) GetEncryptionAlgorithm() string { + if x != nil { + return x.EncryptionAlgorithm + } + return "" +} + +func (x *FlagsInConfig) GetDisableSymHolePunching() bool { + if x != nil { + return x.DisableSymHolePunching + } + return false +} + +func (x *FlagsInConfig) GetTldDnsZone() string { + if x != nil { + return x.TldDnsZone + } + return "" +} + +func (x *FlagsInConfig) GetP2POnly() bool { + if x != nil { + return x.P2POnly + } + return false +} + +func (x *FlagsInConfig) GetDisableTcpHolePunching() bool { + if x != nil { + return x.DisableTcpHolePunching + } + return false +} + +func (x *FlagsInConfig) GetLazyP2P() bool { + if x != nil { + return x.LazyP2P + } + return false +} + +func (x *FlagsInConfig) GetNeedP2P() bool { + if x != nil { + return x.NeedP2P + } + return false +} + +func (x *FlagsInConfig) GetInstanceRecvBpsLimit() uint64 { + if x != nil { + return x.InstanceRecvBpsLimit + } + return 0 +} + +func (x *FlagsInConfig) GetDisableUpnp() bool { + if x != nil { + return x.DisableUpnp + } + return false +} + +func (x *FlagsInConfig) GetDisableRelayData() bool { + if x != nil { + return x.DisableRelayData + } + return false +} + +func (x *FlagsInConfig) GetEnableUdpBroadcastRelay() bool { + if x != nil { + return x.EnableUdpBroadcastRelay + } + return false +} + +func (x *FlagsInConfig) GetSocketMark() uint32 { + if x != nil && x.SocketMark != nil { + return *x.SocketMark + } + return 0 +} + +type RpcDescriptor struct { + state protoimpl.MessageState `protogen:"open.v1"` + // allow same service registered multiple times in different domain + DomainName string `protobuf:"bytes,1,opt,name=domain_name,json=domainName,proto3" json:"domain_name,omitempty"` + ProtoName string `protobuf:"bytes,2,opt,name=proto_name,json=protoName,proto3" json:"proto_name,omitempty"` + ServiceName string `protobuf:"bytes,3,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + MethodIndex uint32 `protobuf:"varint,4,opt,name=method_index,json=methodIndex,proto3" json:"method_index,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RpcDescriptor) Reset() { + *x = RpcDescriptor{} + mi := &file_common_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RpcDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RpcDescriptor) ProtoMessage() {} + +func (x *RpcDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RpcDescriptor.ProtoReflect.Descriptor instead. +func (*RpcDescriptor) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{1} +} + +func (x *RpcDescriptor) GetDomainName() string { + if x != nil { + return x.DomainName + } + return "" +} + +func (x *RpcDescriptor) GetProtoName() string { + if x != nil { + return x.ProtoName + } + return "" +} + +func (x *RpcDescriptor) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *RpcDescriptor) GetMethodIndex() uint32 { + if x != nil { + return x.MethodIndex + } + return 0 +} + +type RpcRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Deprecated: Marked as deprecated in common.proto. + Descriptor_ *RpcDescriptor `protobuf:"bytes,1,opt,name=descriptor,proto3" json:"descriptor,omitempty"` + Request []byte `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` + TimeoutMs int32 `protobuf:"varint,3,opt,name=timeout_ms,json=timeoutMs,proto3" json:"timeout_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RpcRequest) Reset() { + *x = RpcRequest{} + mi := &file_common_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RpcRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RpcRequest) ProtoMessage() {} + +func (x *RpcRequest) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RpcRequest.ProtoReflect.Descriptor instead. +func (*RpcRequest) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{2} +} + +// Deprecated: Marked as deprecated in common.proto. +func (x *RpcRequest) GetDescriptor_() *RpcDescriptor { + if x != nil { + return x.Descriptor_ + } + return nil +} + +func (x *RpcRequest) GetRequest() []byte { + if x != nil { + return x.Request + } + return nil +} + +func (x *RpcRequest) GetTimeoutMs() int32 { + if x != nil { + return x.TimeoutMs + } + return 0 +} + +// One transport-neutral RPC invocation submitted through a direct ABI. +// +// full_method_name uses the protobuf reflection form: +// ".." or "." for an empty package. +type DirectRpcRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + FullMethodName string `protobuf:"bytes,1,opt,name=full_method_name,json=fullMethodName,proto3" json:"full_method_name,omitempty"` + Request []byte `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` + TimeoutMs *uint64 `protobuf:"varint,3,opt,name=timeout_ms,json=timeoutMs,proto3,oneof" json:"timeout_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DirectRpcRequest) Reset() { + *x = DirectRpcRequest{} + mi := &file_common_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DirectRpcRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DirectRpcRequest) ProtoMessage() {} + +func (x *DirectRpcRequest) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DirectRpcRequest.ProtoReflect.Descriptor instead. +func (*DirectRpcRequest) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{3} +} + +func (x *DirectRpcRequest) GetFullMethodName() string { + if x != nil { + return x.FullMethodName + } + return "" +} + +func (x *DirectRpcRequest) GetRequest() []byte { + if x != nil { + return x.Request + } + return nil +} + +func (x *DirectRpcRequest) GetTimeoutMs() uint64 { + if x != nil && x.TimeoutMs != nil { + return *x.TimeoutMs + } + return 0 +} + +// One process-level management call delegated by the WASI WebClient to its host. +type HostManagementRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rpc *DirectRpcRequest `protobuf:"bytes,1,opt,name=rpc,proto3" json:"rpc,omitempty"` + PreparedConfig *string `protobuf:"bytes,2,opt,name=prepared_config,json=preparedConfig,proto3,oneof" json:"prepared_config,omitempty"` + // Runtime identity paired with prepared_config without rewriting rpc.request. + PreparedInstanceId *UUID `protobuf:"bytes,3,opt,name=prepared_instance_id,json=preparedInstanceId,proto3" json:"prepared_instance_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HostManagementRequest) Reset() { + *x = HostManagementRequest{} + mi := &file_common_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HostManagementRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostManagementRequest) ProtoMessage() {} + +func (x *HostManagementRequest) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostManagementRequest.ProtoReflect.Descriptor instead. +func (*HostManagementRequest) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{4} +} + +func (x *HostManagementRequest) GetRpc() *DirectRpcRequest { + if x != nil { + return x.Rpc + } + return nil +} + +func (x *HostManagementRequest) GetPreparedConfig() string { + if x != nil && x.PreparedConfig != nil { + return *x.PreparedConfig + } + return "" +} + +func (x *HostManagementRequest) GetPreparedInstanceId() *UUID { + if x != nil { + return x.PreparedInstanceId + } + return nil +} + +type RpcResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Response []byte `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` + Error *error1.Error `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + RuntimeUs uint64 `protobuf:"varint,3,opt,name=runtime_us,json=runtimeUs,proto3" json:"runtime_us,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RpcResponse) Reset() { + *x = RpcResponse{} + mi := &file_common_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RpcResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RpcResponse) ProtoMessage() {} + +func (x *RpcResponse) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RpcResponse.ProtoReflect.Descriptor instead. +func (*RpcResponse) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{5} +} + +func (x *RpcResponse) GetResponse() []byte { + if x != nil { + return x.Response + } + return nil +} + +func (x *RpcResponse) GetError() *error1.Error { + if x != nil { + return x.Error + } + return nil +} + +func (x *RpcResponse) GetRuntimeUs() uint64 { + if x != nil { + return x.RuntimeUs + } + return 0 +} + +type RpcCompressionInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // use this to compress the content + Algo CompressionAlgoPb `protobuf:"varint,1,opt,name=algo,proto3,enum=common.CompressionAlgoPb" json:"algo,omitempty"` + // tell the peer which compression algo is used to compress the next + // response/request + AcceptedAlgo CompressionAlgoPb `protobuf:"varint,2,opt,name=accepted_algo,json=acceptedAlgo,proto3,enum=common.CompressionAlgoPb" json:"accepted_algo,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RpcCompressionInfo) Reset() { + *x = RpcCompressionInfo{} + mi := &file_common_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RpcCompressionInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RpcCompressionInfo) ProtoMessage() {} + +func (x *RpcCompressionInfo) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RpcCompressionInfo.ProtoReflect.Descriptor instead. +func (*RpcCompressionInfo) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{6} +} + +func (x *RpcCompressionInfo) GetAlgo() CompressionAlgoPb { + if x != nil { + return x.Algo + } + return CompressionAlgoPb_Invalid +} + +func (x *RpcCompressionInfo) GetAcceptedAlgo() CompressionAlgoPb { + if x != nil { + return x.AcceptedAlgo + } + return CompressionAlgoPb_Invalid +} + +type RpcPacket struct { + state protoimpl.MessageState `protogen:"open.v1"` + FromPeer uint32 `protobuf:"varint,1,opt,name=from_peer,json=fromPeer,proto3" json:"from_peer,omitempty"` + ToPeer uint32 `protobuf:"varint,2,opt,name=to_peer,json=toPeer,proto3" json:"to_peer,omitempty"` + TransactionId int64 `protobuf:"varint,3,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` + Descriptor_ *RpcDescriptor `protobuf:"bytes,4,opt,name=descriptor,proto3" json:"descriptor,omitempty"` + Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"` + IsRequest bool `protobuf:"varint,6,opt,name=is_request,json=isRequest,proto3" json:"is_request,omitempty"` + TotalPieces uint32 `protobuf:"varint,7,opt,name=total_pieces,json=totalPieces,proto3" json:"total_pieces,omitempty"` + PieceIdx uint32 `protobuf:"varint,8,opt,name=piece_idx,json=pieceIdx,proto3" json:"piece_idx,omitempty"` + TraceId int32 `protobuf:"varint,9,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` + CompressionInfo *RpcCompressionInfo `protobuf:"bytes,10,opt,name=compression_info,json=compressionInfo,proto3" json:"compression_info,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RpcPacket) Reset() { + *x = RpcPacket{} + mi := &file_common_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RpcPacket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RpcPacket) ProtoMessage() {} + +func (x *RpcPacket) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RpcPacket.ProtoReflect.Descriptor instead. +func (*RpcPacket) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{7} +} + +func (x *RpcPacket) GetFromPeer() uint32 { + if x != nil { + return x.FromPeer + } + return 0 +} + +func (x *RpcPacket) GetToPeer() uint32 { + if x != nil { + return x.ToPeer + } + return 0 +} + +func (x *RpcPacket) GetTransactionId() int64 { + if x != nil { + return x.TransactionId + } + return 0 +} + +func (x *RpcPacket) GetDescriptor_() *RpcDescriptor { + if x != nil { + return x.Descriptor_ + } + return nil +} + +func (x *RpcPacket) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +func (x *RpcPacket) GetIsRequest() bool { + if x != nil { + return x.IsRequest + } + return false +} + +func (x *RpcPacket) GetTotalPieces() uint32 { + if x != nil { + return x.TotalPieces + } + return 0 +} + +func (x *RpcPacket) GetPieceIdx() uint32 { + if x != nil { + return x.PieceIdx + } + return 0 +} + +func (x *RpcPacket) GetTraceId() int32 { + if x != nil { + return x.TraceId + } + return 0 +} + +func (x *RpcPacket) GetCompressionInfo() *RpcCompressionInfo { + if x != nil { + return x.CompressionInfo + } + return nil +} + +type Void struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Void) Reset() { + *x = Void{} + mi := &file_common_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Void) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Void) ProtoMessage() {} + +func (x *Void) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Void.ProtoReflect.Descriptor instead. +func (*Void) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{8} +} + +type UUID struct { + state protoimpl.MessageState `protogen:"open.v1"` + Part1 uint32 `protobuf:"varint,1,opt,name=part1,proto3" json:"part1,omitempty"` + Part2 uint32 `protobuf:"varint,2,opt,name=part2,proto3" json:"part2,omitempty"` + Part3 uint32 `protobuf:"varint,3,opt,name=part3,proto3" json:"part3,omitempty"` + Part4 uint32 `protobuf:"varint,4,opt,name=part4,proto3" json:"part4,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UUID) Reset() { + *x = UUID{} + mi := &file_common_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UUID) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UUID) ProtoMessage() {} + +func (x *UUID) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UUID.ProtoReflect.Descriptor instead. +func (*UUID) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{9} +} + +func (x *UUID) GetPart1() uint32 { + if x != nil { + return x.Part1 + } + return 0 +} + +func (x *UUID) GetPart2() uint32 { + if x != nil { + return x.Part2 + } + return 0 +} + +func (x *UUID) GetPart3() uint32 { + if x != nil { + return x.Part3 + } + return 0 +} + +func (x *UUID) GetPart4() uint32 { + if x != nil { + return x.Part4 + } + return 0 +} + +type Ipv4Addr struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addr uint32 `protobuf:"varint,1,opt,name=addr,proto3" json:"addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Ipv4Addr) Reset() { + *x = Ipv4Addr{} + mi := &file_common_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Ipv4Addr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Ipv4Addr) ProtoMessage() {} + +func (x *Ipv4Addr) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Ipv4Addr.ProtoReflect.Descriptor instead. +func (*Ipv4Addr) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{10} +} + +func (x *Ipv4Addr) GetAddr() uint32 { + if x != nil { + return x.Addr + } + return 0 +} + +type Ipv6Addr struct { + state protoimpl.MessageState `protogen:"open.v1"` + Part1 uint32 `protobuf:"varint,1,opt,name=part1,proto3" json:"part1,omitempty"` + Part2 uint32 `protobuf:"varint,2,opt,name=part2,proto3" json:"part2,omitempty"` + Part3 uint32 `protobuf:"varint,3,opt,name=part3,proto3" json:"part3,omitempty"` + Part4 uint32 `protobuf:"varint,4,opt,name=part4,proto3" json:"part4,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Ipv6Addr) Reset() { + *x = Ipv6Addr{} + mi := &file_common_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Ipv6Addr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Ipv6Addr) ProtoMessage() {} + +func (x *Ipv6Addr) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Ipv6Addr.ProtoReflect.Descriptor instead. +func (*Ipv6Addr) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{11} +} + +func (x *Ipv6Addr) GetPart1() uint32 { + if x != nil { + return x.Part1 + } + return 0 +} + +func (x *Ipv6Addr) GetPart2() uint32 { + if x != nil { + return x.Part2 + } + return 0 +} + +func (x *Ipv6Addr) GetPart3() uint32 { + if x != nil { + return x.Part3 + } + return 0 +} + +func (x *Ipv6Addr) GetPart4() uint32 { + if x != nil { + return x.Part4 + } + return 0 +} + +type IpAddr struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Ip: + // + // *IpAddr_Ipv4 + // *IpAddr_Ipv6 + Ip isIpAddr_Ip `protobuf_oneof:"ip"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IpAddr) Reset() { + *x = IpAddr{} + mi := &file_common_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IpAddr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IpAddr) ProtoMessage() {} + +func (x *IpAddr) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IpAddr.ProtoReflect.Descriptor instead. +func (*IpAddr) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{12} +} + +func (x *IpAddr) GetIp() isIpAddr_Ip { + if x != nil { + return x.Ip + } + return nil +} + +func (x *IpAddr) GetIpv4() *Ipv4Addr { + if x != nil { + if x, ok := x.Ip.(*IpAddr_Ipv4); ok { + return x.Ipv4 + } + } + return nil +} + +func (x *IpAddr) GetIpv6() *Ipv6Addr { + if x != nil { + if x, ok := x.Ip.(*IpAddr_Ipv6); ok { + return x.Ipv6 + } + } + return nil +} + +type isIpAddr_Ip interface { + isIpAddr_Ip() +} + +type IpAddr_Ipv4 struct { + Ipv4 *Ipv4Addr `protobuf:"bytes,1,opt,name=ipv4,proto3,oneof"` +} + +type IpAddr_Ipv6 struct { + Ipv6 *Ipv6Addr `protobuf:"bytes,2,opt,name=ipv6,proto3,oneof"` +} + +func (*IpAddr_Ipv4) isIpAddr_Ip() {} + +func (*IpAddr_Ipv6) isIpAddr_Ip() {} + +type Ipv4Inet struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address *Ipv4Addr `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + NetworkLength uint32 `protobuf:"varint,2,opt,name=network_length,json=networkLength,proto3" json:"network_length,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Ipv4Inet) Reset() { + *x = Ipv4Inet{} + mi := &file_common_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Ipv4Inet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Ipv4Inet) ProtoMessage() {} + +func (x *Ipv4Inet) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Ipv4Inet.ProtoReflect.Descriptor instead. +func (*Ipv4Inet) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{13} +} + +func (x *Ipv4Inet) GetAddress() *Ipv4Addr { + if x != nil { + return x.Address + } + return nil +} + +func (x *Ipv4Inet) GetNetworkLength() uint32 { + if x != nil { + return x.NetworkLength + } + return 0 +} + +type Ipv6Inet struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address *Ipv6Addr `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + NetworkLength uint32 `protobuf:"varint,2,opt,name=network_length,json=networkLength,proto3" json:"network_length,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Ipv6Inet) Reset() { + *x = Ipv6Inet{} + mi := &file_common_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Ipv6Inet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Ipv6Inet) ProtoMessage() {} + +func (x *Ipv6Inet) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Ipv6Inet.ProtoReflect.Descriptor instead. +func (*Ipv6Inet) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{14} +} + +func (x *Ipv6Inet) GetAddress() *Ipv6Addr { + if x != nil { + return x.Address + } + return nil +} + +func (x *Ipv6Inet) GetNetworkLength() uint32 { + if x != nil { + return x.NetworkLength + } + return 0 +} + +type IpInet struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Ip: + // + // *IpInet_Ipv4 + // *IpInet_Ipv6 + Ip isIpInet_Ip `protobuf_oneof:"ip"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IpInet) Reset() { + *x = IpInet{} + mi := &file_common_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IpInet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IpInet) ProtoMessage() {} + +func (x *IpInet) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IpInet.ProtoReflect.Descriptor instead. +func (*IpInet) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{15} +} + +func (x *IpInet) GetIp() isIpInet_Ip { + if x != nil { + return x.Ip + } + return nil +} + +func (x *IpInet) GetIpv4() *Ipv4Inet { + if x != nil { + if x, ok := x.Ip.(*IpInet_Ipv4); ok { + return x.Ipv4 + } + } + return nil +} + +func (x *IpInet) GetIpv6() *Ipv6Inet { + if x != nil { + if x, ok := x.Ip.(*IpInet_Ipv6); ok { + return x.Ipv6 + } + } + return nil +} + +type isIpInet_Ip interface { + isIpInet_Ip() +} + +type IpInet_Ipv4 struct { + Ipv4 *Ipv4Inet `protobuf:"bytes,1,opt,name=ipv4,proto3,oneof"` +} + +type IpInet_Ipv6 struct { + Ipv6 *Ipv6Inet `protobuf:"bytes,2,opt,name=ipv6,proto3,oneof"` +} + +func (*IpInet_Ipv4) isIpInet_Ip() {} + +func (*IpInet_Ipv6) isIpInet_Ip() {} + +type Url struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Url) Reset() { + *x = Url{} + mi := &file_common_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Url) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Url) ProtoMessage() {} + +func (x *Url) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Url.ProtoReflect.Descriptor instead. +func (*Url) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{16} +} + +func (x *Url) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +type SocketAddr struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Ip: + // + // *SocketAddr_Ipv4 + // *SocketAddr_Ipv6 + Ip isSocketAddr_Ip `protobuf_oneof:"ip"` + Port uint32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SocketAddr) Reset() { + *x = SocketAddr{} + mi := &file_common_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SocketAddr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SocketAddr) ProtoMessage() {} + +func (x *SocketAddr) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SocketAddr.ProtoReflect.Descriptor instead. +func (*SocketAddr) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{17} +} + +func (x *SocketAddr) GetIp() isSocketAddr_Ip { + if x != nil { + return x.Ip + } + return nil +} + +func (x *SocketAddr) GetIpv4() *Ipv4Addr { + if x != nil { + if x, ok := x.Ip.(*SocketAddr_Ipv4); ok { + return x.Ipv4 + } + } + return nil +} + +func (x *SocketAddr) GetIpv6() *Ipv6Addr { + if x != nil { + if x, ok := x.Ip.(*SocketAddr_Ipv6); ok { + return x.Ipv6 + } + } + return nil +} + +func (x *SocketAddr) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +type isSocketAddr_Ip interface { + isSocketAddr_Ip() +} + +type SocketAddr_Ipv4 struct { + Ipv4 *Ipv4Addr `protobuf:"bytes,1,opt,name=ipv4,proto3,oneof"` +} + +type SocketAddr_Ipv6 struct { + Ipv6 *Ipv6Addr `protobuf:"bytes,2,opt,name=ipv6,proto3,oneof"` +} + +func (*SocketAddr_Ipv4) isSocketAddr_Ip() {} + +func (*SocketAddr_Ipv6) isSocketAddr_Ip() {} + +type TunnelInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + TunnelType string `protobuf:"bytes,1,opt,name=tunnel_type,json=tunnelType,proto3" json:"tunnel_type,omitempty"` + LocalAddr *Url `protobuf:"bytes,2,opt,name=local_addr,json=localAddr,proto3" json:"local_addr,omitempty"` + RemoteAddr *Url `protobuf:"bytes,3,opt,name=remote_addr,json=remoteAddr,proto3" json:"remote_addr,omitempty"` + ResolvedRemoteAddr *Url `protobuf:"bytes,4,opt,name=resolved_remote_addr,json=resolvedRemoteAddr,proto3" json:"resolved_remote_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TunnelInfo) Reset() { + *x = TunnelInfo{} + mi := &file_common_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TunnelInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TunnelInfo) ProtoMessage() {} + +func (x *TunnelInfo) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TunnelInfo.ProtoReflect.Descriptor instead. +func (*TunnelInfo) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{18} +} + +func (x *TunnelInfo) GetTunnelType() string { + if x != nil { + return x.TunnelType + } + return "" +} + +func (x *TunnelInfo) GetLocalAddr() *Url { + if x != nil { + return x.LocalAddr + } + return nil +} + +func (x *TunnelInfo) GetRemoteAddr() *Url { + if x != nil { + return x.RemoteAddr + } + return nil +} + +func (x *TunnelInfo) GetResolvedRemoteAddr() *Url { + if x != nil { + return x.ResolvedRemoteAddr + } + return nil +} + +type StunInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + UdpNatType NatType `protobuf:"varint,1,opt,name=udp_nat_type,json=udpNatType,proto3,enum=common.NatType" json:"udp_nat_type,omitempty"` + TcpNatType NatType `protobuf:"varint,2,opt,name=tcp_nat_type,json=tcpNatType,proto3,enum=common.NatType" json:"tcp_nat_type,omitempty"` + LastUpdateTime int64 `protobuf:"varint,3,opt,name=last_update_time,json=lastUpdateTime,proto3" json:"last_update_time,omitempty"` + PublicIp []string `protobuf:"bytes,4,rep,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"` + MinPort uint32 `protobuf:"varint,5,opt,name=min_port,json=minPort,proto3" json:"min_port,omitempty"` + MaxPort uint32 `protobuf:"varint,6,opt,name=max_port,json=maxPort,proto3" json:"max_port,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StunInfo) Reset() { + *x = StunInfo{} + mi := &file_common_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StunInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StunInfo) ProtoMessage() {} + +func (x *StunInfo) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StunInfo.ProtoReflect.Descriptor instead. +func (*StunInfo) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{19} +} + +func (x *StunInfo) GetUdpNatType() NatType { + if x != nil { + return x.UdpNatType + } + return NatType_Unknown +} + +func (x *StunInfo) GetTcpNatType() NatType { + if x != nil { + return x.TcpNatType + } + return NatType_Unknown +} + +func (x *StunInfo) GetLastUpdateTime() int64 { + if x != nil { + return x.LastUpdateTime + } + return 0 +} + +func (x *StunInfo) GetPublicIp() []string { + if x != nil { + return x.PublicIp + } + return nil +} + +func (x *StunInfo) GetMinPort() uint32 { + if x != nil { + return x.MinPort + } + return 0 +} + +func (x *StunInfo) GetMaxPort() uint32 { + if x != nil { + return x.MaxPort + } + return 0 +} + +type PeerFeatureFlag struct { + state protoimpl.MessageState `protogen:"open.v1"` + IsPublicServer bool `protobuf:"varint,1,opt,name=is_public_server,json=isPublicServer,proto3" json:"is_public_server,omitempty"` + AvoidRelayData bool `protobuf:"varint,2,opt,name=avoid_relay_data,json=avoidRelayData,proto3" json:"avoid_relay_data,omitempty"` + KcpInput bool `protobuf:"varint,3,opt,name=kcp_input,json=kcpInput,proto3" json:"kcp_input,omitempty"` + NoRelayKcp bool `protobuf:"varint,4,opt,name=no_relay_kcp,json=noRelayKcp,proto3" json:"no_relay_kcp,omitempty"` + SupportConnListSync bool `protobuf:"varint,5,opt,name=support_conn_list_sync,json=supportConnListSync,proto3" json:"support_conn_list_sync,omitempty"` + QuicInput bool `protobuf:"varint,6,opt,name=quic_input,json=quicInput,proto3" json:"quic_input,omitempty"` + NoRelayQuic bool `protobuf:"varint,7,opt,name=no_relay_quic,json=noRelayQuic,proto3" json:"no_relay_quic,omitempty"` + IsCredentialPeer bool `protobuf:"varint,8,opt,name=is_credential_peer,json=isCredentialPeer,proto3" json:"is_credential_peer,omitempty"` + NeedP2P bool `protobuf:"varint,9,opt,name=need_p2p,json=needP2p,proto3" json:"need_p2p,omitempty"` + DisableP2P bool `protobuf:"varint,10,opt,name=disable_p2p,json=disableP2p,proto3" json:"disable_p2p,omitempty"` + Ipv6PublicAddrProvider bool `protobuf:"varint,11,opt,name=ipv6_public_addr_provider,json=ipv6PublicAddrProvider,proto3" json:"ipv6_public_addr_provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerFeatureFlag) Reset() { + *x = PeerFeatureFlag{} + mi := &file_common_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerFeatureFlag) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerFeatureFlag) ProtoMessage() {} + +func (x *PeerFeatureFlag) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerFeatureFlag.ProtoReflect.Descriptor instead. +func (*PeerFeatureFlag) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{20} +} + +func (x *PeerFeatureFlag) GetIsPublicServer() bool { + if x != nil { + return x.IsPublicServer + } + return false +} + +func (x *PeerFeatureFlag) GetAvoidRelayData() bool { + if x != nil { + return x.AvoidRelayData + } + return false +} + +func (x *PeerFeatureFlag) GetKcpInput() bool { + if x != nil { + return x.KcpInput + } + return false +} + +func (x *PeerFeatureFlag) GetNoRelayKcp() bool { + if x != nil { + return x.NoRelayKcp + } + return false +} + +func (x *PeerFeatureFlag) GetSupportConnListSync() bool { + if x != nil { + return x.SupportConnListSync + } + return false +} + +func (x *PeerFeatureFlag) GetQuicInput() bool { + if x != nil { + return x.QuicInput + } + return false +} + +func (x *PeerFeatureFlag) GetNoRelayQuic() bool { + if x != nil { + return x.NoRelayQuic + } + return false +} + +func (x *PeerFeatureFlag) GetIsCredentialPeer() bool { + if x != nil { + return x.IsCredentialPeer + } + return false +} + +func (x *PeerFeatureFlag) GetNeedP2P() bool { + if x != nil { + return x.NeedP2P + } + return false +} + +func (x *PeerFeatureFlag) GetDisableP2P() bool { + if x != nil { + return x.DisableP2P + } + return false +} + +func (x *PeerFeatureFlag) GetIpv6PublicAddrProvider() bool { + if x != nil { + return x.Ipv6PublicAddrProvider + } + return false +} + +type PortForwardConfigPb struct { + state protoimpl.MessageState `protogen:"open.v1"` + BindAddr *SocketAddr `protobuf:"bytes,1,opt,name=bind_addr,json=bindAddr,proto3" json:"bind_addr,omitempty"` + DstAddr *SocketAddr `protobuf:"bytes,2,opt,name=dst_addr,json=dstAddr,proto3" json:"dst_addr,omitempty"` + SocketType SocketType `protobuf:"varint,3,opt,name=socket_type,json=socketType,proto3,enum=common.SocketType" json:"socket_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PortForwardConfigPb) Reset() { + *x = PortForwardConfigPb{} + mi := &file_common_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortForwardConfigPb) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortForwardConfigPb) ProtoMessage() {} + +func (x *PortForwardConfigPb) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PortForwardConfigPb.ProtoReflect.Descriptor instead. +func (*PortForwardConfigPb) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{21} +} + +func (x *PortForwardConfigPb) GetBindAddr() *SocketAddr { + if x != nil { + return x.BindAddr + } + return nil +} + +func (x *PortForwardConfigPb) GetDstAddr() *SocketAddr { + if x != nil { + return x.DstAddr + } + return nil +} + +func (x *PortForwardConfigPb) GetSocketType() SocketType { + if x != nil { + return x.SocketType + } + return SocketType_TCP +} + +type ProxyDstInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + DstAddr *SocketAddr `protobuf:"bytes,1,opt,name=dst_addr,json=dstAddr,proto3" json:"dst_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProxyDstInfo) Reset() { + *x = ProxyDstInfo{} + mi := &file_common_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProxyDstInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyDstInfo) ProtoMessage() {} + +func (x *ProxyDstInfo) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProxyDstInfo.ProtoReflect.Descriptor instead. +func (*ProxyDstInfo) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{22} +} + +func (x *ProxyDstInfo) GetDstAddr() *SocketAddr { + if x != nil { + return x.DstAddr + } + return nil +} + +type LimiterConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + BurstRate *uint64 `protobuf:"varint,1,opt,name=burst_rate,json=burstRate,proto3,oneof" json:"burst_rate,omitempty"` // default 1 means no burst (capacity is same with bps) + Bps *uint64 `protobuf:"varint,2,opt,name=bps,proto3,oneof" json:"bps,omitempty"` // default 0 means no limit (unit is B/s) + FillDurationMs *uint64 `protobuf:"varint,3,opt,name=fill_duration_ms,json=fillDurationMs,proto3,oneof" json:"fill_duration_ms,omitempty"` // default 10ms, the period to fill the bucket + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LimiterConfig) Reset() { + *x = LimiterConfig{} + mi := &file_common_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LimiterConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LimiterConfig) ProtoMessage() {} + +func (x *LimiterConfig) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LimiterConfig.ProtoReflect.Descriptor instead. +func (*LimiterConfig) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{23} +} + +func (x *LimiterConfig) GetBurstRate() uint64 { + if x != nil && x.BurstRate != nil { + return *x.BurstRate + } + return 0 +} + +func (x *LimiterConfig) GetBps() uint64 { + if x != nil && x.Bps != nil { + return *x.Bps + } + return 0 +} + +func (x *LimiterConfig) GetFillDurationMs() uint64 { + if x != nil && x.FillDurationMs != nil { + return *x.FillDurationMs + } + return 0 +} + +type SecureModeConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + // base64(X25519 private key), used by shared node to present a stable identity + LocalPrivateKey *string `protobuf:"bytes,2,opt,name=local_private_key,json=localPrivateKey,proto3,oneof" json:"local_private_key,omitempty"` + // base64(X25519 public key), required if local_private_key is set + LocalPublicKey *string `protobuf:"bytes,3,opt,name=local_public_key,json=localPublicKey,proto3,oneof" json:"local_public_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecureModeConfig) Reset() { + *x = SecureModeConfig{} + mi := &file_common_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecureModeConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecureModeConfig) ProtoMessage() {} + +func (x *SecureModeConfig) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecureModeConfig.ProtoReflect.Descriptor instead. +func (*SecureModeConfig) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{24} +} + +func (x *SecureModeConfig) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *SecureModeConfig) GetLocalPrivateKey() string { + if x != nil && x.LocalPrivateKey != nil { + return *x.LocalPrivateKey + } + return "" +} + +func (x *SecureModeConfig) GetLocalPublicKey() string { + if x != nil && x.LocalPublicKey != nil { + return *x.LocalPublicKey + } + return "" +} + +var File_common_proto protoreflect.FileDescriptor + +const file_common_proto_rawDesc = "" + + "\n" + + "\fcommon.proto\x12\x06common\x1a\verror.proto\"\xc9\x0e\n" + + "\rFlagsInConfig\x12)\n" + + "\x10default_protocol\x18\x01 \x01(\tR\x0fdefaultProtocol\x12\x19\n" + + "\bdev_name\x18\x02 \x01(\tR\adevName\x12+\n" + + "\x11enable_encryption\x18\x03 \x01(\bR\x10enableEncryption\x12\x1f\n" + + "\venable_ipv6\x18\x04 \x01(\bR\n" + + "enableIpv6\x12\x10\n" + + "\x03mtu\x18\x05 \x01(\rR\x03mtu\x12#\n" + + "\rlatency_first\x18\x06 \x01(\bR\flatencyFirst\x12(\n" + + "\x10enable_exit_node\x18\a \x01(\bR\x0eenableExitNode\x12\x15\n" + + "\x06no_tun\x18\b \x01(\bR\x05noTun\x12\x1f\n" + + "\vuse_smoltcp\x18\t \x01(\bR\n" + + "useSmoltcp\x126\n" + + "\x17relay_network_whitelist\x18\n" + + " \x01(\tR\x15relayNetworkWhitelist\x12\x1f\n" + + "\vdisable_p2p\x18\v \x01(\bR\n" + + "disableP2p\x12+\n" + + "\x12relay_all_peer_rpc\x18\f \x01(\bR\x0frelayAllPeerRpc\x129\n" + + "\x19disable_udp_hole_punching\x18\r \x01(\bR\x16disableUdpHolePunching\x12!\n" + + "\fmulti_thread\x18\x0f \x01(\bR\vmultiThread\x12G\n" + + "\x12data_compress_algo\x18\x10 \x01(\x0e2\x19.common.CompressionAlgoPbR\x10dataCompressAlgo\x12\x1f\n" + + "\vbind_device\x18\x11 \x01(\bR\n" + + "bindDevice\x12(\n" + + "\x10enable_kcp_proxy\x18\x12 \x01(\bR\x0eenableKcpProxy\x12*\n" + + "\x11disable_kcp_input\x18\x13 \x01(\bR\x0fdisableKcpInput\x12*\n" + + "\x11disable_relay_kcp\x18\x14 \x01(\bR\x0fdisableRelayKcp\x125\n" + + "\x17proxy_forward_by_system\x18\x15 \x01(\bR\x14proxyForwardBySystem\x12\x1d\n" + + "\n" + + "accept_dns\x18\x16 \x01(\bR\tacceptDns\x12!\n" + + "\fprivate_mode\x18\x17 \x01(\bR\vprivateMode\x12*\n" + + "\x11enable_quic_proxy\x18\x18 \x01(\bR\x0fenableQuicProxy\x12,\n" + + "\x12disable_quic_input\x18\x19 \x01(\bR\x10disableQuicInput\x12,\n" + + "\x12disable_relay_quic\x18# \x01(\bR\x10disableRelayQuic\x12,\n" + + "\x10quic_listen_port\x18! \x01(\rB\x02\x18\x01R\x0equicListenPort\x125\n" + + "\x17foreign_relay_bps_limit\x18\x1a \x01(\x04R\x14foreignRelayBpsLimit\x12,\n" + + "\x12multi_thread_count\x18\x1b \x01(\rR\x10multiThreadCount\x12F\n" + + " enable_relay_foreign_network_kcp\x18\x1c \x01(\bR\x1cenableRelayForeignNetworkKcp\x12H\n" + + "!enable_relay_foreign_network_quic\x18$ \x01(\bR\x1denableRelayForeignNetworkQuic\x121\n" + + "\x14encryption_algorithm\x18\x1d \x01(\tR\x13encryptionAlgorithm\x129\n" + + "\x19disable_sym_hole_punching\x18\x1e \x01(\bR\x16disableSymHolePunching\x12 \n" + + "\ftld_dns_zone\x18\x1f \x01(\tR\n" + + "tldDnsZone\x12\x19\n" + + "\bp2p_only\x18 \x01(\bR\ap2pOnly\x129\n" + + "\x19disable_tcp_hole_punching\x18\" \x01(\bR\x16disableTcpHolePunching\x12\x19\n" + + "\blazy_p2p\x18% \x01(\bR\alazyP2p\x12\x19\n" + + "\bneed_p2p\x18& \x01(\bR\aneedP2p\x125\n" + + "\x17instance_recv_bps_limit\x18' \x01(\x04R\x14instanceRecvBpsLimit\x12!\n" + + "\fdisable_upnp\x18( \x01(\bR\vdisableUpnp\x12,\n" + + "\x12disable_relay_data\x18) \x01(\bR\x10disableRelayData\x12;\n" + + "\x1aenable_udp_broadcast_relay\x18* \x01(\bR\x17enableUdpBroadcastRelay\x12$\n" + + "\vsocket_mark\x18+ \x01(\rH\x00R\n" + + "socketMark\x88\x01\x01B\x0e\n" + + "\f_socket_mark\"\x95\x01\n" + + "\rRpcDescriptor\x12\x1f\n" + + "\vdomain_name\x18\x01 \x01(\tR\n" + + "domainName\x12\x1d\n" + + "\n" + + "proto_name\x18\x02 \x01(\tR\tprotoName\x12!\n" + + "\fservice_name\x18\x03 \x01(\tR\vserviceName\x12!\n" + + "\fmethod_index\x18\x04 \x01(\rR\vmethodIndex\"\x80\x01\n" + + "\n" + + "RpcRequest\x129\n" + + "\n" + + "descriptor\x18\x01 \x01(\v2\x15.common.RpcDescriptorB\x02\x18\x01R\n" + + "descriptor\x12\x18\n" + + "\arequest\x18\x02 \x01(\fR\arequest\x12\x1d\n" + + "\n" + + "timeout_ms\x18\x03 \x01(\x05R\ttimeoutMs\"\x89\x01\n" + + "\x10DirectRpcRequest\x12(\n" + + "\x10full_method_name\x18\x01 \x01(\tR\x0efullMethodName\x12\x18\n" + + "\arequest\x18\x02 \x01(\fR\arequest\x12\"\n" + + "\n" + + "timeout_ms\x18\x03 \x01(\x04H\x00R\ttimeoutMs\x88\x01\x01B\r\n" + + "\v_timeout_ms\"\xc5\x01\n" + + "\x15HostManagementRequest\x12*\n" + + "\x03rpc\x18\x01 \x01(\v2\x18.common.DirectRpcRequestR\x03rpc\x12,\n" + + "\x0fprepared_config\x18\x02 \x01(\tH\x00R\x0epreparedConfig\x88\x01\x01\x12>\n" + + "\x14prepared_instance_id\x18\x03 \x01(\v2\f.common.UUIDR\x12preparedInstanceIdB\x12\n" + + "\x10_prepared_config\"l\n" + + "\vRpcResponse\x12\x1a\n" + + "\bresponse\x18\x01 \x01(\fR\bresponse\x12\"\n" + + "\x05error\x18\x02 \x01(\v2\f.error.ErrorR\x05error\x12\x1d\n" + + "\n" + + "runtime_us\x18\x03 \x01(\x04R\truntimeUs\"\x83\x01\n" + + "\x12RpcCompressionInfo\x12-\n" + + "\x04algo\x18\x01 \x01(\x0e2\x19.common.CompressionAlgoPbR\x04algo\x12>\n" + + "\raccepted_algo\x18\x02 \x01(\x0e2\x19.common.CompressionAlgoPbR\facceptedAlgo\"\xf4\x02\n" + + "\tRpcPacket\x12\x1b\n" + + "\tfrom_peer\x18\x01 \x01(\rR\bfromPeer\x12\x17\n" + + "\ato_peer\x18\x02 \x01(\rR\x06toPeer\x12%\n" + + "\x0etransaction_id\x18\x03 \x01(\x03R\rtransactionId\x125\n" + + "\n" + + "descriptor\x18\x04 \x01(\v2\x15.common.RpcDescriptorR\n" + + "descriptor\x12\x12\n" + + "\x04body\x18\x05 \x01(\fR\x04body\x12\x1d\n" + + "\n" + + "is_request\x18\x06 \x01(\bR\tisRequest\x12!\n" + + "\ftotal_pieces\x18\a \x01(\rR\vtotalPieces\x12\x1b\n" + + "\tpiece_idx\x18\b \x01(\rR\bpieceIdx\x12\x19\n" + + "\btrace_id\x18\t \x01(\x05R\atraceId\x12E\n" + + "\x10compression_info\x18\n" + + " \x01(\v2\x1a.common.RpcCompressionInfoR\x0fcompressionInfo\"\x06\n" + + "\x04Void\"^\n" + + "\x04UUID\x12\x14\n" + + "\x05part1\x18\x01 \x01(\rR\x05part1\x12\x14\n" + + "\x05part2\x18\x02 \x01(\rR\x05part2\x12\x14\n" + + "\x05part3\x18\x03 \x01(\rR\x05part3\x12\x14\n" + + "\x05part4\x18\x04 \x01(\rR\x05part4\"\x1e\n" + + "\bIpv4Addr\x12\x12\n" + + "\x04addr\x18\x01 \x01(\rR\x04addr\"b\n" + + "\bIpv6Addr\x12\x14\n" + + "\x05part1\x18\x01 \x01(\rR\x05part1\x12\x14\n" + + "\x05part2\x18\x02 \x01(\rR\x05part2\x12\x14\n" + + "\x05part3\x18\x03 \x01(\rR\x05part3\x12\x14\n" + + "\x05part4\x18\x04 \x01(\rR\x05part4\"^\n" + + "\x06IpAddr\x12&\n" + + "\x04ipv4\x18\x01 \x01(\v2\x10.common.Ipv4AddrH\x00R\x04ipv4\x12&\n" + + "\x04ipv6\x18\x02 \x01(\v2\x10.common.Ipv6AddrH\x00R\x04ipv6B\x04\n" + + "\x02ip\"]\n" + + "\bIpv4Inet\x12*\n" + + "\aaddress\x18\x01 \x01(\v2\x10.common.Ipv4AddrR\aaddress\x12%\n" + + "\x0enetwork_length\x18\x02 \x01(\rR\rnetworkLength\"]\n" + + "\bIpv6Inet\x12*\n" + + "\aaddress\x18\x01 \x01(\v2\x10.common.Ipv6AddrR\aaddress\x12%\n" + + "\x0enetwork_length\x18\x02 \x01(\rR\rnetworkLength\"^\n" + + "\x06IpInet\x12&\n" + + "\x04ipv4\x18\x01 \x01(\v2\x10.common.Ipv4InetH\x00R\x04ipv4\x12&\n" + + "\x04ipv6\x18\x02 \x01(\v2\x10.common.Ipv6InetH\x00R\x04ipv6B\x04\n" + + "\x02ip\"\x17\n" + + "\x03Url\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\"v\n" + + "\n" + + "SocketAddr\x12&\n" + + "\x04ipv4\x18\x01 \x01(\v2\x10.common.Ipv4AddrH\x00R\x04ipv4\x12&\n" + + "\x04ipv6\x18\x02 \x01(\v2\x10.common.Ipv6AddrH\x00R\x04ipv6\x12\x12\n" + + "\x04port\x18\x03 \x01(\rR\x04portB\x04\n" + + "\x02ip\"\xc6\x01\n" + + "\n" + + "TunnelInfo\x12\x1f\n" + + "\vtunnel_type\x18\x01 \x01(\tR\n" + + "tunnelType\x12*\n" + + "\n" + + "local_addr\x18\x02 \x01(\v2\v.common.UrlR\tlocalAddr\x12,\n" + + "\vremote_addr\x18\x03 \x01(\v2\v.common.UrlR\n" + + "remoteAddr\x12=\n" + + "\x14resolved_remote_addr\x18\x04 \x01(\v2\v.common.UrlR\x12resolvedRemoteAddr\"\xed\x01\n" + + "\bStunInfo\x121\n" + + "\fudp_nat_type\x18\x01 \x01(\x0e2\x0f.common.NatTypeR\n" + + "udpNatType\x121\n" + + "\ftcp_nat_type\x18\x02 \x01(\x0e2\x0f.common.NatTypeR\n" + + "tcpNatType\x12(\n" + + "\x10last_update_time\x18\x03 \x01(\x03R\x0elastUpdateTime\x12\x1b\n" + + "\tpublic_ip\x18\x04 \x03(\tR\bpublicIp\x12\x19\n" + + "\bmin_port\x18\x05 \x01(\rR\aminPort\x12\x19\n" + + "\bmax_port\x18\x06 \x01(\rR\amaxPort\"\xc1\x03\n" + + "\x0fPeerFeatureFlag\x12(\n" + + "\x10is_public_server\x18\x01 \x01(\bR\x0eisPublicServer\x12(\n" + + "\x10avoid_relay_data\x18\x02 \x01(\bR\x0eavoidRelayData\x12\x1b\n" + + "\tkcp_input\x18\x03 \x01(\bR\bkcpInput\x12 \n" + + "\fno_relay_kcp\x18\x04 \x01(\bR\n" + + "noRelayKcp\x123\n" + + "\x16support_conn_list_sync\x18\x05 \x01(\bR\x13supportConnListSync\x12\x1d\n" + + "\n" + + "quic_input\x18\x06 \x01(\bR\tquicInput\x12\"\n" + + "\rno_relay_quic\x18\a \x01(\bR\vnoRelayQuic\x12,\n" + + "\x12is_credential_peer\x18\b \x01(\bR\x10isCredentialPeer\x12\x19\n" + + "\bneed_p2p\x18\t \x01(\bR\aneedP2p\x12\x1f\n" + + "\vdisable_p2p\x18\n" + + " \x01(\bR\n" + + "disableP2p\x129\n" + + "\x19ipv6_public_addr_provider\x18\v \x01(\bR\x16ipv6PublicAddrProvider\"\xaa\x01\n" + + "\x13PortForwardConfigPb\x12/\n" + + "\tbind_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\bbindAddr\x12-\n" + + "\bdst_addr\x18\x02 \x01(\v2\x12.common.SocketAddrR\adstAddr\x123\n" + + "\vsocket_type\x18\x03 \x01(\x0e2\x12.common.SocketTypeR\n" + + "socketType\"=\n" + + "\fProxyDstInfo\x12-\n" + + "\bdst_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\adstAddr\"\xa5\x01\n" + + "\rLimiterConfig\x12\"\n" + + "\n" + + "burst_rate\x18\x01 \x01(\x04H\x00R\tburstRate\x88\x01\x01\x12\x15\n" + + "\x03bps\x18\x02 \x01(\x04H\x01R\x03bps\x88\x01\x01\x12-\n" + + "\x10fill_duration_ms\x18\x03 \x01(\x04H\x02R\x0efillDurationMs\x88\x01\x01B\r\n" + + "\v_burst_rateB\x06\n" + + "\x04_bpsB\x13\n" + + "\x11_fill_duration_ms\"\xb7\x01\n" + + "\x10SecureModeConfig\x12\x18\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\x12/\n" + + "\x11local_private_key\x18\x02 \x01(\tH\x00R\x0flocalPrivateKey\x88\x01\x01\x12-\n" + + "\x10local_public_key\x18\x03 \x01(\tH\x01R\x0elocalPublicKey\x88\x01\x01B\x14\n" + + "\x12_local_private_keyB\x13\n" + + "\x11_local_public_key*4\n" + + "\x11CompressionAlgoPb\x12\v\n" + + "\aInvalid\x10\x00\x12\b\n" + + "\x04None\x10\x01\x12\b\n" + + "\x04Zstd\x10\x02*\xb4\x01\n" + + "\aNatType\x12\v\n" + + "\aUnknown\x10\x00\x12\x10\n" + + "\fOpenInternet\x10\x01\x12\t\n" + + "\x05NoPAT\x10\x02\x12\f\n" + + "\bFullCone\x10\x03\x12\x0e\n" + + "\n" + + "Restricted\x10\x04\x12\x12\n" + + "\x0ePortRestricted\x10\x05\x12\r\n" + + "\tSymmetric\x10\x06\x12\x12\n" + + "\x0eSymUdpFirewall\x10\a\x12\x14\n" + + "\x10SymmetricEasyInc\x10\b\x12\x14\n" + + "\x10SymmetricEasyDec\x10\t*\x1e\n" + + "\n" + + "SocketType\x12\a\n" + + "\x03TCP\x10\x00\x12\a\n" + + "\x03UDP\x10\x01b\x06proto3" + +var ( + file_common_proto_rawDescOnce sync.Once + file_common_proto_rawDescData []byte +) + +func file_common_proto_rawDescGZIP() []byte { + file_common_proto_rawDescOnce.Do(func() { + file_common_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_proto_rawDesc), len(file_common_proto_rawDesc))) + }) + return file_common_proto_rawDescData +} + +var file_common_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_common_proto_goTypes = []any{ + (CompressionAlgoPb)(0), // 0: common.CompressionAlgoPb + (NatType)(0), // 1: common.NatType + (SocketType)(0), // 2: common.SocketType + (*FlagsInConfig)(nil), // 3: common.FlagsInConfig + (*RpcDescriptor)(nil), // 4: common.RpcDescriptor + (*RpcRequest)(nil), // 5: common.RpcRequest + (*DirectRpcRequest)(nil), // 6: common.DirectRpcRequest + (*HostManagementRequest)(nil), // 7: common.HostManagementRequest + (*RpcResponse)(nil), // 8: common.RpcResponse + (*RpcCompressionInfo)(nil), // 9: common.RpcCompressionInfo + (*RpcPacket)(nil), // 10: common.RpcPacket + (*Void)(nil), // 11: common.Void + (*UUID)(nil), // 12: common.UUID + (*Ipv4Addr)(nil), // 13: common.Ipv4Addr + (*Ipv6Addr)(nil), // 14: common.Ipv6Addr + (*IpAddr)(nil), // 15: common.IpAddr + (*Ipv4Inet)(nil), // 16: common.Ipv4Inet + (*Ipv6Inet)(nil), // 17: common.Ipv6Inet + (*IpInet)(nil), // 18: common.IpInet + (*Url)(nil), // 19: common.Url + (*SocketAddr)(nil), // 20: common.SocketAddr + (*TunnelInfo)(nil), // 21: common.TunnelInfo + (*StunInfo)(nil), // 22: common.StunInfo + (*PeerFeatureFlag)(nil), // 23: common.PeerFeatureFlag + (*PortForwardConfigPb)(nil), // 24: common.PortForwardConfigPb + (*ProxyDstInfo)(nil), // 25: common.ProxyDstInfo + (*LimiterConfig)(nil), // 26: common.LimiterConfig + (*SecureModeConfig)(nil), // 27: common.SecureModeConfig + (*error1.Error)(nil), // 28: error.Error +} +var file_common_proto_depIdxs = []int32{ + 0, // 0: common.FlagsInConfig.data_compress_algo:type_name -> common.CompressionAlgoPb + 4, // 1: common.RpcRequest.descriptor:type_name -> common.RpcDescriptor + 6, // 2: common.HostManagementRequest.rpc:type_name -> common.DirectRpcRequest + 12, // 3: common.HostManagementRequest.prepared_instance_id:type_name -> common.UUID + 28, // 4: common.RpcResponse.error:type_name -> error.Error + 0, // 5: common.RpcCompressionInfo.algo:type_name -> common.CompressionAlgoPb + 0, // 6: common.RpcCompressionInfo.accepted_algo:type_name -> common.CompressionAlgoPb + 4, // 7: common.RpcPacket.descriptor:type_name -> common.RpcDescriptor + 9, // 8: common.RpcPacket.compression_info:type_name -> common.RpcCompressionInfo + 13, // 9: common.IpAddr.ipv4:type_name -> common.Ipv4Addr + 14, // 10: common.IpAddr.ipv6:type_name -> common.Ipv6Addr + 13, // 11: common.Ipv4Inet.address:type_name -> common.Ipv4Addr + 14, // 12: common.Ipv6Inet.address:type_name -> common.Ipv6Addr + 16, // 13: common.IpInet.ipv4:type_name -> common.Ipv4Inet + 17, // 14: common.IpInet.ipv6:type_name -> common.Ipv6Inet + 13, // 15: common.SocketAddr.ipv4:type_name -> common.Ipv4Addr + 14, // 16: common.SocketAddr.ipv6:type_name -> common.Ipv6Addr + 19, // 17: common.TunnelInfo.local_addr:type_name -> common.Url + 19, // 18: common.TunnelInfo.remote_addr:type_name -> common.Url + 19, // 19: common.TunnelInfo.resolved_remote_addr:type_name -> common.Url + 1, // 20: common.StunInfo.udp_nat_type:type_name -> common.NatType + 1, // 21: common.StunInfo.tcp_nat_type:type_name -> common.NatType + 20, // 22: common.PortForwardConfigPb.bind_addr:type_name -> common.SocketAddr + 20, // 23: common.PortForwardConfigPb.dst_addr:type_name -> common.SocketAddr + 2, // 24: common.PortForwardConfigPb.socket_type:type_name -> common.SocketType + 20, // 25: common.ProxyDstInfo.dst_addr:type_name -> common.SocketAddr + 26, // [26:26] is the sub-list for method output_type + 26, // [26:26] is the sub-list for method input_type + 26, // [26:26] is the sub-list for extension type_name + 26, // [26:26] is the sub-list for extension extendee + 0, // [0:26] is the sub-list for field type_name +} + +func init() { file_common_proto_init() } +func file_common_proto_init() { + if File_common_proto != nil { + return + } + file_common_proto_msgTypes[0].OneofWrappers = []any{} + file_common_proto_msgTypes[3].OneofWrappers = []any{} + file_common_proto_msgTypes[4].OneofWrappers = []any{} + file_common_proto_msgTypes[12].OneofWrappers = []any{ + (*IpAddr_Ipv4)(nil), + (*IpAddr_Ipv6)(nil), + } + file_common_proto_msgTypes[15].OneofWrappers = []any{ + (*IpInet_Ipv4)(nil), + (*IpInet_Ipv6)(nil), + } + file_common_proto_msgTypes[17].OneofWrappers = []any{ + (*SocketAddr_Ipv4)(nil), + (*SocketAddr_Ipv6)(nil), + } + file_common_proto_msgTypes[23].OneofWrappers = []any{} + file_common_proto_msgTypes[24].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_proto_rawDesc), len(file_common_proto_rawDesc)), + NumEnums: 3, + NumMessages: 25, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_proto_goTypes, + DependencyIndexes: file_common_proto_depIdxs, + EnumInfos: file_common_proto_enumTypes, + MessageInfos: file_common_proto_msgTypes, + }.Build() + File_common_proto = out.File + file_common_proto_goTypes = nil + file_common_proto_depIdxs = nil +} diff --git a/easytier-go/proto/error/error.pb.go b/easytier-go/proto/error/error.pb.go new file mode 100644 index 00000000..fbaff4b7 --- /dev/null +++ b/easytier-go/proto/error/error.pb.go @@ -0,0 +1,652 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.35.1 +// source: error.proto + +package error + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type OtherError struct { + state protoimpl.MessageState `protogen:"open.v1"` + ErrorMessage string `protobuf:"bytes,1,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OtherError) Reset() { + *x = OtherError{} + mi := &file_error_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OtherError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OtherError) ProtoMessage() {} + +func (x *OtherError) ProtoReflect() protoreflect.Message { + mi := &file_error_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OtherError.ProtoReflect.Descriptor instead. +func (*OtherError) Descriptor() ([]byte, []int) { + return file_error_proto_rawDescGZIP(), []int{0} +} + +func (x *OtherError) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type InvalidMethodIndex struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + MethodIndex uint32 `protobuf:"varint,2,opt,name=method_index,json=methodIndex,proto3" json:"method_index,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InvalidMethodIndex) Reset() { + *x = InvalidMethodIndex{} + mi := &file_error_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InvalidMethodIndex) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvalidMethodIndex) ProtoMessage() {} + +func (x *InvalidMethodIndex) ProtoReflect() protoreflect.Message { + mi := &file_error_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InvalidMethodIndex.ProtoReflect.Descriptor instead. +func (*InvalidMethodIndex) Descriptor() ([]byte, []int) { + return file_error_proto_rawDescGZIP(), []int{1} +} + +func (x *InvalidMethodIndex) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *InvalidMethodIndex) GetMethodIndex() uint32 { + if x != nil { + return x.MethodIndex + } + return 0 +} + +type InvalidService struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InvalidService) Reset() { + *x = InvalidService{} + mi := &file_error_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InvalidService) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvalidService) ProtoMessage() {} + +func (x *InvalidService) ProtoReflect() protoreflect.Message { + mi := &file_error_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InvalidService.ProtoReflect.Descriptor instead. +func (*InvalidService) Descriptor() ([]byte, []int) { + return file_error_proto_rawDescGZIP(), []int{2} +} + +func (x *InvalidService) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +type ProstDecodeError struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProstDecodeError) Reset() { + *x = ProstDecodeError{} + mi := &file_error_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProstDecodeError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProstDecodeError) ProtoMessage() {} + +func (x *ProstDecodeError) ProtoReflect() protoreflect.Message { + mi := &file_error_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProstDecodeError.ProtoReflect.Descriptor instead. +func (*ProstDecodeError) Descriptor() ([]byte, []int) { + return file_error_proto_rawDescGZIP(), []int{3} +} + +type ProstEncodeError struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProstEncodeError) Reset() { + *x = ProstEncodeError{} + mi := &file_error_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProstEncodeError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProstEncodeError) ProtoMessage() {} + +func (x *ProstEncodeError) ProtoReflect() protoreflect.Message { + mi := &file_error_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProstEncodeError.ProtoReflect.Descriptor instead. +func (*ProstEncodeError) Descriptor() ([]byte, []int) { + return file_error_proto_rawDescGZIP(), []int{4} +} + +type ExecuteError struct { + state protoimpl.MessageState `protogen:"open.v1"` + ErrorMessage string `protobuf:"bytes,1,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecuteError) Reset() { + *x = ExecuteError{} + mi := &file_error_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecuteError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteError) ProtoMessage() {} + +func (x *ExecuteError) ProtoReflect() protoreflect.Message { + mi := &file_error_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteError.ProtoReflect.Descriptor instead. +func (*ExecuteError) Descriptor() ([]byte, []int) { + return file_error_proto_rawDescGZIP(), []int{5} +} + +func (x *ExecuteError) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type MalformatRpcPacket struct { + state protoimpl.MessageState `protogen:"open.v1"` + ErrorMessage string `protobuf:"bytes,1,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MalformatRpcPacket) Reset() { + *x = MalformatRpcPacket{} + mi := &file_error_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MalformatRpcPacket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MalformatRpcPacket) ProtoMessage() {} + +func (x *MalformatRpcPacket) ProtoReflect() protoreflect.Message { + mi := &file_error_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MalformatRpcPacket.ProtoReflect.Descriptor instead. +func (*MalformatRpcPacket) Descriptor() ([]byte, []int) { + return file_error_proto_rawDescGZIP(), []int{6} +} + +func (x *MalformatRpcPacket) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type Timeout struct { + state protoimpl.MessageState `protogen:"open.v1"` + ErrorMessage string `protobuf:"bytes,1,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Timeout) Reset() { + *x = Timeout{} + mi := &file_error_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Timeout) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Timeout) ProtoMessage() {} + +func (x *Timeout) ProtoReflect() protoreflect.Message { + mi := &file_error_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Timeout.ProtoReflect.Descriptor instead. +func (*Timeout) Descriptor() ([]byte, []int) { + return file_error_proto_rawDescGZIP(), []int{7} +} + +func (x *Timeout) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type Error struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to ErrorKind: + // + // *Error_OtherError + // *Error_InvalidMethodIndex + // *Error_InvalidService + // *Error_ProstDecodeError + // *Error_ProstEncodeError + // *Error_ExecuteError + // *Error_MalformatRpcPacket + // *Error_Timeout + ErrorKind isError_ErrorKind `protobuf_oneof:"error_kind"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Error) Reset() { + *x = Error{} + mi := &file_error_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Error) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Error) ProtoMessage() {} + +func (x *Error) ProtoReflect() protoreflect.Message { + mi := &file_error_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Error.ProtoReflect.Descriptor instead. +func (*Error) Descriptor() ([]byte, []int) { + return file_error_proto_rawDescGZIP(), []int{8} +} + +func (x *Error) GetErrorKind() isError_ErrorKind { + if x != nil { + return x.ErrorKind + } + return nil +} + +func (x *Error) GetOtherError() *OtherError { + if x != nil { + if x, ok := x.ErrorKind.(*Error_OtherError); ok { + return x.OtherError + } + } + return nil +} + +func (x *Error) GetInvalidMethodIndex() *InvalidMethodIndex { + if x != nil { + if x, ok := x.ErrorKind.(*Error_InvalidMethodIndex); ok { + return x.InvalidMethodIndex + } + } + return nil +} + +func (x *Error) GetInvalidService() *InvalidService { + if x != nil { + if x, ok := x.ErrorKind.(*Error_InvalidService); ok { + return x.InvalidService + } + } + return nil +} + +func (x *Error) GetProstDecodeError() *ProstDecodeError { + if x != nil { + if x, ok := x.ErrorKind.(*Error_ProstDecodeError); ok { + return x.ProstDecodeError + } + } + return nil +} + +func (x *Error) GetProstEncodeError() *ProstEncodeError { + if x != nil { + if x, ok := x.ErrorKind.(*Error_ProstEncodeError); ok { + return x.ProstEncodeError + } + } + return nil +} + +func (x *Error) GetExecuteError() *ExecuteError { + if x != nil { + if x, ok := x.ErrorKind.(*Error_ExecuteError); ok { + return x.ExecuteError + } + } + return nil +} + +func (x *Error) GetMalformatRpcPacket() *MalformatRpcPacket { + if x != nil { + if x, ok := x.ErrorKind.(*Error_MalformatRpcPacket); ok { + return x.MalformatRpcPacket + } + } + return nil +} + +func (x *Error) GetTimeout() *Timeout { + if x != nil { + if x, ok := x.ErrorKind.(*Error_Timeout); ok { + return x.Timeout + } + } + return nil +} + +type isError_ErrorKind interface { + isError_ErrorKind() +} + +type Error_OtherError struct { + OtherError *OtherError `protobuf:"bytes,1,opt,name=other_error,json=otherError,proto3,oneof"` +} + +type Error_InvalidMethodIndex struct { + InvalidMethodIndex *InvalidMethodIndex `protobuf:"bytes,2,opt,name=invalid_method_index,json=invalidMethodIndex,proto3,oneof"` +} + +type Error_InvalidService struct { + InvalidService *InvalidService `protobuf:"bytes,3,opt,name=invalid_service,json=invalidService,proto3,oneof"` +} + +type Error_ProstDecodeError struct { + ProstDecodeError *ProstDecodeError `protobuf:"bytes,4,opt,name=prost_decode_error,json=prostDecodeError,proto3,oneof"` +} + +type Error_ProstEncodeError struct { + ProstEncodeError *ProstEncodeError `protobuf:"bytes,5,opt,name=prost_encode_error,json=prostEncodeError,proto3,oneof"` +} + +type Error_ExecuteError struct { + ExecuteError *ExecuteError `protobuf:"bytes,6,opt,name=execute_error,json=executeError,proto3,oneof"` +} + +type Error_MalformatRpcPacket struct { + MalformatRpcPacket *MalformatRpcPacket `protobuf:"bytes,7,opt,name=malformat_rpc_packet,json=malformatRpcPacket,proto3,oneof"` +} + +type Error_Timeout struct { + Timeout *Timeout `protobuf:"bytes,8,opt,name=timeout,proto3,oneof"` +} + +func (*Error_OtherError) isError_ErrorKind() {} + +func (*Error_InvalidMethodIndex) isError_ErrorKind() {} + +func (*Error_InvalidService) isError_ErrorKind() {} + +func (*Error_ProstDecodeError) isError_ErrorKind() {} + +func (*Error_ProstEncodeError) isError_ErrorKind() {} + +func (*Error_ExecuteError) isError_ErrorKind() {} + +func (*Error_MalformatRpcPacket) isError_ErrorKind() {} + +func (*Error_Timeout) isError_ErrorKind() {} + +var File_error_proto protoreflect.FileDescriptor + +const file_error_proto_rawDesc = "" + + "\n" + + "\verror.proto\x12\x05error\"1\n" + + "\n" + + "OtherError\x12#\n" + + "\rerror_message\x18\x01 \x01(\tR\ferrorMessage\"Z\n" + + "\x12InvalidMethodIndex\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12!\n" + + "\fmethod_index\x18\x02 \x01(\rR\vmethodIndex\"3\n" + + "\x0eInvalidService\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\"\x12\n" + + "\x10ProstDecodeError\"\x12\n" + + "\x10ProstEncodeError\"3\n" + + "\fExecuteError\x12#\n" + + "\rerror_message\x18\x01 \x01(\tR\ferrorMessage\"9\n" + + "\x12MalformatRpcPacket\x12#\n" + + "\rerror_message\x18\x01 \x01(\tR\ferrorMessage\".\n" + + "\aTimeout\x12#\n" + + "\rerror_message\x18\x01 \x01(\tR\ferrorMessage\"\xa5\x04\n" + + "\x05Error\x124\n" + + "\vother_error\x18\x01 \x01(\v2\x11.error.OtherErrorH\x00R\n" + + "otherError\x12M\n" + + "\x14invalid_method_index\x18\x02 \x01(\v2\x19.error.InvalidMethodIndexH\x00R\x12invalidMethodIndex\x12@\n" + + "\x0finvalid_service\x18\x03 \x01(\v2\x15.error.InvalidServiceH\x00R\x0einvalidService\x12G\n" + + "\x12prost_decode_error\x18\x04 \x01(\v2\x17.error.ProstDecodeErrorH\x00R\x10prostDecodeError\x12G\n" + + "\x12prost_encode_error\x18\x05 \x01(\v2\x17.error.ProstEncodeErrorH\x00R\x10prostEncodeError\x12:\n" + + "\rexecute_error\x18\x06 \x01(\v2\x13.error.ExecuteErrorH\x00R\fexecuteError\x12M\n" + + "\x14malformat_rpc_packet\x18\a \x01(\v2\x19.error.MalformatRpcPacketH\x00R\x12malformatRpcPacket\x12*\n" + + "\atimeout\x18\b \x01(\v2\x0e.error.TimeoutH\x00R\atimeoutB\f\n" + + "\n" + + "error_kindb\x06proto3" + +var ( + file_error_proto_rawDescOnce sync.Once + file_error_proto_rawDescData []byte +) + +func file_error_proto_rawDescGZIP() []byte { + file_error_proto_rawDescOnce.Do(func() { + file_error_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_error_proto_rawDesc), len(file_error_proto_rawDesc))) + }) + return file_error_proto_rawDescData +} + +var file_error_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_error_proto_goTypes = []any{ + (*OtherError)(nil), // 0: error.OtherError + (*InvalidMethodIndex)(nil), // 1: error.InvalidMethodIndex + (*InvalidService)(nil), // 2: error.InvalidService + (*ProstDecodeError)(nil), // 3: error.ProstDecodeError + (*ProstEncodeError)(nil), // 4: error.ProstEncodeError + (*ExecuteError)(nil), // 5: error.ExecuteError + (*MalformatRpcPacket)(nil), // 6: error.MalformatRpcPacket + (*Timeout)(nil), // 7: error.Timeout + (*Error)(nil), // 8: error.Error +} +var file_error_proto_depIdxs = []int32{ + 0, // 0: error.Error.other_error:type_name -> error.OtherError + 1, // 1: error.Error.invalid_method_index:type_name -> error.InvalidMethodIndex + 2, // 2: error.Error.invalid_service:type_name -> error.InvalidService + 3, // 3: error.Error.prost_decode_error:type_name -> error.ProstDecodeError + 4, // 4: error.Error.prost_encode_error:type_name -> error.ProstEncodeError + 5, // 5: error.Error.execute_error:type_name -> error.ExecuteError + 6, // 6: error.Error.malformat_rpc_packet:type_name -> error.MalformatRpcPacket + 7, // 7: error.Error.timeout:type_name -> error.Timeout + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_error_proto_init() } +func file_error_proto_init() { + if File_error_proto != nil { + return + } + file_error_proto_msgTypes[8].OneofWrappers = []any{ + (*Error_OtherError)(nil), + (*Error_InvalidMethodIndex)(nil), + (*Error_InvalidService)(nil), + (*Error_ProstDecodeError)(nil), + (*Error_ProstEncodeError)(nil), + (*Error_ExecuteError)(nil), + (*Error_MalformatRpcPacket)(nil), + (*Error_Timeout)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_error_proto_rawDesc), len(file_error_proto_rawDesc)), + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_error_proto_goTypes, + DependencyIndexes: file_error_proto_depIdxs, + MessageInfos: file_error_proto_msgTypes, + }.Build() + File_error_proto = out.File + file_error_proto_goTypes = nil + file_error_proto_depIdxs = nil +} diff --git a/easytier-go/proto/generate.go b/easytier-go/proto/generate.go new file mode 100644 index 00000000..0c7ccb87 --- /dev/null +++ b/easytier-go/proto/generate.go @@ -0,0 +1,3 @@ +// Package proto owns generated Go bindings for the protobuf definitions used +// by the embedded EasyTier core. +package proto diff --git a/easytier-go/proto/peer_rpc/peer_rpc.pb.go b/easytier-go/proto/peer_rpc/peer_rpc.pb.go new file mode 100644 index 00000000..5ec454ef --- /dev/null +++ b/easytier-go/proto/peer_rpc/peer_rpc.pb.go @@ -0,0 +1,3895 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.35.1 +// source: peer_rpc.proto + +package peer_rpc + +import ( + common "github.com/EasyTier/EasyTier/easytier-go/proto/common" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SyncRouteInfoError int32 + +const ( + SyncRouteInfoError_DuplicatePeerId SyncRouteInfoError = 0 + SyncRouteInfoError_Stopped SyncRouteInfoError = 1 +) + +// Enum value maps for SyncRouteInfoError. +var ( + SyncRouteInfoError_name = map[int32]string{ + 0: "DuplicatePeerId", + 1: "Stopped", + } + SyncRouteInfoError_value = map[string]int32{ + "DuplicatePeerId": 0, + "Stopped": 1, + } +) + +func (x SyncRouteInfoError) Enum() *SyncRouteInfoError { + p := new(SyncRouteInfoError) + *p = x + return p +} + +func (x SyncRouteInfoError) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SyncRouteInfoError) Descriptor() protoreflect.EnumDescriptor { + return file_peer_rpc_proto_enumTypes[0].Descriptor() +} + +func (SyncRouteInfoError) Type() protoreflect.EnumType { + return &file_peer_rpc_proto_enumTypes[0] +} + +func (x SyncRouteInfoError) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SyncRouteInfoError.Descriptor instead. +func (SyncRouteInfoError) EnumDescriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{0} +} + +type SecureAuthLevel int32 + +const ( + SecureAuthLevel_None SecureAuthLevel = 0 + SecureAuthLevel_EncryptedUnauthenticated SecureAuthLevel = 1 + SecureAuthLevel_PeerVerified SecureAuthLevel = 2 + SecureAuthLevel_NetworkSecretConfirmed SecureAuthLevel = 3 +) + +// Enum value maps for SecureAuthLevel. +var ( + SecureAuthLevel_name = map[int32]string{ + 0: "None", + 1: "EncryptedUnauthenticated", + 2: "PeerVerified", + 3: "NetworkSecretConfirmed", + } + SecureAuthLevel_value = map[string]int32{ + "None": 0, + "EncryptedUnauthenticated": 1, + "PeerVerified": 2, + "NetworkSecretConfirmed": 3, + } +) + +func (x SecureAuthLevel) Enum() *SecureAuthLevel { + p := new(SecureAuthLevel) + *p = x + return p +} + +func (x SecureAuthLevel) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SecureAuthLevel) Descriptor() protoreflect.EnumDescriptor { + return file_peer_rpc_proto_enumTypes[1].Descriptor() +} + +func (SecureAuthLevel) Type() protoreflect.EnumType { + return &file_peer_rpc_proto_enumTypes[1] +} + +func (x SecureAuthLevel) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SecureAuthLevel.Descriptor instead. +func (SecureAuthLevel) EnumDescriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{1} +} + +type PeerIdentityType int32 + +const ( + PeerIdentityType_Admin PeerIdentityType = 0 + PeerIdentityType_Credential PeerIdentityType = 1 + PeerIdentityType_SharedNode PeerIdentityType = 2 +) + +// Enum value maps for PeerIdentityType. +var ( + PeerIdentityType_name = map[int32]string{ + 0: "Admin", + 1: "Credential", + 2: "SharedNode", + } + PeerIdentityType_value = map[string]int32{ + "Admin": 0, + "Credential": 1, + "SharedNode": 2, + } +) + +func (x PeerIdentityType) Enum() *PeerIdentityType { + p := new(PeerIdentityType) + *p = x + return p +} + +func (x PeerIdentityType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PeerIdentityType) Descriptor() protoreflect.EnumDescriptor { + return file_peer_rpc_proto_enumTypes[2].Descriptor() +} + +func (PeerIdentityType) Type() protoreflect.EnumType { + return &file_peer_rpc_proto_enumTypes[2] +} + +func (x PeerIdentityType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PeerIdentityType.Descriptor instead. +func (PeerIdentityType) EnumDescriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{2} +} + +type PeerConnSessionActionPb int32 + +const ( + PeerConnSessionActionPb_Join PeerConnSessionActionPb = 0 + PeerConnSessionActionPb_Sync PeerConnSessionActionPb = 1 + PeerConnSessionActionPb_Create PeerConnSessionActionPb = 2 +) + +// Enum value maps for PeerConnSessionActionPb. +var ( + PeerConnSessionActionPb_name = map[int32]string{ + 0: "Join", + 1: "Sync", + 2: "Create", + } + PeerConnSessionActionPb_value = map[string]int32{ + "Join": 0, + "Sync": 1, + "Create": 2, + } +) + +func (x PeerConnSessionActionPb) Enum() *PeerConnSessionActionPb { + p := new(PeerConnSessionActionPb) + *p = x + return p +} + +func (x PeerConnSessionActionPb) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PeerConnSessionActionPb) Descriptor() protoreflect.EnumDescriptor { + return file_peer_rpc_proto_enumTypes[3].Descriptor() +} + +func (PeerConnSessionActionPb) Type() protoreflect.EnumType { + return &file_peer_rpc_proto_enumTypes[3] +} + +func (x PeerConnSessionActionPb) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PeerConnSessionActionPb.Descriptor instead. +func (PeerConnSessionActionPb) EnumDescriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{3} +} + +type TrustedCredentialPubkey struct { + state protoimpl.MessageState `protogen:"open.v1"` + Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"` // X25519 public key (32 bytes) + Groups []string `protobuf:"bytes,2,rep,name=groups,proto3" json:"groups,omitempty"` // ACL groups this credential belongs to + AllowRelay bool `protobuf:"varint,3,opt,name=allow_relay,json=allowRelay,proto3" json:"allow_relay,omitempty"` // whether this credential node can relay data + ExpiryUnix int64 `protobuf:"varint,4,opt,name=expiry_unix,json=expiryUnix,proto3" json:"expiry_unix,omitempty"` // expiry time (Unix timestamp) + AllowedProxyCidrs []string `protobuf:"bytes,5,rep,name=allowed_proxy_cidrs,json=allowedProxyCidrs,proto3" json:"allowed_proxy_cidrs,omitempty"` // allowed proxy_cidrs ranges + Reusable *bool `protobuf:"varint,6,opt,name=reusable,proto3,oneof" json:"reusable,omitempty"` // whether multiple peers may use the same credential concurrently + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TrustedCredentialPubkey) Reset() { + *x = TrustedCredentialPubkey{} + mi := &file_peer_rpc_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TrustedCredentialPubkey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrustedCredentialPubkey) ProtoMessage() {} + +func (x *TrustedCredentialPubkey) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrustedCredentialPubkey.ProtoReflect.Descriptor instead. +func (*TrustedCredentialPubkey) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{0} +} + +func (x *TrustedCredentialPubkey) GetPubkey() []byte { + if x != nil { + return x.Pubkey + } + return nil +} + +func (x *TrustedCredentialPubkey) GetGroups() []string { + if x != nil { + return x.Groups + } + return nil +} + +func (x *TrustedCredentialPubkey) GetAllowRelay() bool { + if x != nil { + return x.AllowRelay + } + return false +} + +func (x *TrustedCredentialPubkey) GetExpiryUnix() int64 { + if x != nil { + return x.ExpiryUnix + } + return 0 +} + +func (x *TrustedCredentialPubkey) GetAllowedProxyCidrs() []string { + if x != nil { + return x.AllowedProxyCidrs + } + return nil +} + +func (x *TrustedCredentialPubkey) GetReusable() bool { + if x != nil && x.Reusable != nil { + return *x.Reusable + } + return false +} + +type TrustedCredentialPubkeyProof struct { + state protoimpl.MessageState `protogen:"open.v1"` + Credential *TrustedCredentialPubkey `protobuf:"bytes,1,opt,name=credential,proto3" json:"credential,omitempty"` + CredentialHmac []byte `protobuf:"bytes,2,opt,name=credential_hmac,json=credentialHmac,proto3" json:"credential_hmac,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TrustedCredentialPubkeyProof) Reset() { + *x = TrustedCredentialPubkeyProof{} + mi := &file_peer_rpc_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TrustedCredentialPubkeyProof) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TrustedCredentialPubkeyProof) ProtoMessage() {} + +func (x *TrustedCredentialPubkeyProof) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TrustedCredentialPubkeyProof.ProtoReflect.Descriptor instead. +func (*TrustedCredentialPubkeyProof) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{1} +} + +func (x *TrustedCredentialPubkeyProof) GetCredential() *TrustedCredentialPubkey { + if x != nil { + return x.Credential + } + return nil +} + +func (x *TrustedCredentialPubkeyProof) GetCredentialHmac() []byte { + if x != nil { + return x.CredentialHmac + } + return nil +} + +type RoutePeerInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // means next hop in route table. + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + InstId *common.UUID `protobuf:"bytes,2,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + Cost uint32 `protobuf:"varint,3,opt,name=cost,proto3" json:"cost,omitempty"` + Ipv4Addr *common.Ipv4Addr `protobuf:"bytes,4,opt,name=ipv4_addr,json=ipv4Addr,proto3,oneof" json:"ipv4_addr,omitempty"` + ProxyCidrs []string `protobuf:"bytes,5,rep,name=proxy_cidrs,json=proxyCidrs,proto3" json:"proxy_cidrs,omitempty"` + Hostname *string `protobuf:"bytes,6,opt,name=hostname,proto3,oneof" json:"hostname,omitempty"` + UdpNatType common.NatType `protobuf:"varint,7,opt,name=udp_nat_type,json=udpNatType,proto3,enum=common.NatType" json:"udp_nat_type,omitempty"` + LastUpdate *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=last_update,json=lastUpdate,proto3" json:"last_update,omitempty"` + Version uint32 `protobuf:"varint,9,opt,name=version,proto3" json:"version,omitempty"` + EasytierVersion string `protobuf:"bytes,10,opt,name=easytier_version,json=easytierVersion,proto3" json:"easytier_version,omitempty"` + FeatureFlag *common.PeerFeatureFlag `protobuf:"bytes,11,opt,name=feature_flag,json=featureFlag,proto3" json:"feature_flag,omitempty"` + PeerRouteId uint64 `protobuf:"varint,12,opt,name=peer_route_id,json=peerRouteId,proto3" json:"peer_route_id,omitempty"` + NetworkLength uint32 `protobuf:"varint,13,opt,name=network_length,json=networkLength,proto3" json:"network_length,omitempty"` + // Deprecated: Marked as deprecated in peer_rpc.proto. + QuicPort *uint32 `protobuf:"varint,14,opt,name=quic_port,json=quicPort,proto3,oneof" json:"quic_port,omitempty"` + Ipv6Addr *common.Ipv6Inet `protobuf:"bytes,15,opt,name=ipv6_addr,json=ipv6Addr,proto3,oneof" json:"ipv6_addr,omitempty"` + Groups []*PeerGroupInfo `protobuf:"bytes,16,rep,name=groups,proto3" json:"groups,omitempty"` + TcpNatType common.NatType `protobuf:"varint,17,opt,name=tcp_nat_type,json=tcpNatType,proto3,enum=common.NatType" json:"tcp_nat_type,omitempty"` + NoiseStaticPubkey []byte `protobuf:"bytes,18,opt,name=noise_static_pubkey,json=noiseStaticPubkey,proto3" json:"noise_static_pubkey,omitempty"` + // Trusted credential public keys published by admin nodes (holding network_secret) + TrustedCredentialPubkeys []*TrustedCredentialPubkeyProof `protobuf:"bytes,19,rep,name=trusted_credential_pubkeys,json=trustedCredentialPubkeys,proto3" json:"trusted_credential_pubkeys,omitempty"` + Ipv6PublicAddrPrefix *common.Ipv6Inet `protobuf:"bytes,22,opt,name=ipv6_public_addr_prefix,json=ipv6PublicAddrPrefix,proto3,oneof" json:"ipv6_public_addr_prefix,omitempty"` + Ipv6PublicAddrLease *common.Ipv6Inet `protobuf:"bytes,24,opt,name=ipv6_public_addr_lease,json=ipv6PublicAddrLease,proto3,oneof" json:"ipv6_public_addr_lease,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoutePeerInfo) Reset() { + *x = RoutePeerInfo{} + mi := &file_peer_rpc_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoutePeerInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoutePeerInfo) ProtoMessage() {} + +func (x *RoutePeerInfo) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoutePeerInfo.ProtoReflect.Descriptor instead. +func (*RoutePeerInfo) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{2} +} + +func (x *RoutePeerInfo) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *RoutePeerInfo) GetInstId() *common.UUID { + if x != nil { + return x.InstId + } + return nil +} + +func (x *RoutePeerInfo) GetCost() uint32 { + if x != nil { + return x.Cost + } + return 0 +} + +func (x *RoutePeerInfo) GetIpv4Addr() *common.Ipv4Addr { + if x != nil { + return x.Ipv4Addr + } + return nil +} + +func (x *RoutePeerInfo) GetProxyCidrs() []string { + if x != nil { + return x.ProxyCidrs + } + return nil +} + +func (x *RoutePeerInfo) GetHostname() string { + if x != nil && x.Hostname != nil { + return *x.Hostname + } + return "" +} + +func (x *RoutePeerInfo) GetUdpNatType() common.NatType { + if x != nil { + return x.UdpNatType + } + return common.NatType(0) +} + +func (x *RoutePeerInfo) GetLastUpdate() *timestamppb.Timestamp { + if x != nil { + return x.LastUpdate + } + return nil +} + +func (x *RoutePeerInfo) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *RoutePeerInfo) GetEasytierVersion() string { + if x != nil { + return x.EasytierVersion + } + return "" +} + +func (x *RoutePeerInfo) GetFeatureFlag() *common.PeerFeatureFlag { + if x != nil { + return x.FeatureFlag + } + return nil +} + +func (x *RoutePeerInfo) GetPeerRouteId() uint64 { + if x != nil { + return x.PeerRouteId + } + return 0 +} + +func (x *RoutePeerInfo) GetNetworkLength() uint32 { + if x != nil { + return x.NetworkLength + } + return 0 +} + +// Deprecated: Marked as deprecated in peer_rpc.proto. +func (x *RoutePeerInfo) GetQuicPort() uint32 { + if x != nil && x.QuicPort != nil { + return *x.QuicPort + } + return 0 +} + +func (x *RoutePeerInfo) GetIpv6Addr() *common.Ipv6Inet { + if x != nil { + return x.Ipv6Addr + } + return nil +} + +func (x *RoutePeerInfo) GetGroups() []*PeerGroupInfo { + if x != nil { + return x.Groups + } + return nil +} + +func (x *RoutePeerInfo) GetTcpNatType() common.NatType { + if x != nil { + return x.TcpNatType + } + return common.NatType(0) +} + +func (x *RoutePeerInfo) GetNoiseStaticPubkey() []byte { + if x != nil { + return x.NoiseStaticPubkey + } + return nil +} + +func (x *RoutePeerInfo) GetTrustedCredentialPubkeys() []*TrustedCredentialPubkeyProof { + if x != nil { + return x.TrustedCredentialPubkeys + } + return nil +} + +func (x *RoutePeerInfo) GetIpv6PublicAddrPrefix() *common.Ipv6Inet { + if x != nil { + return x.Ipv6PublicAddrPrefix + } + return nil +} + +func (x *RoutePeerInfo) GetIpv6PublicAddrLease() *common.Ipv6Inet { + if x != nil { + return x.Ipv6PublicAddrLease + } + return nil +} + +type PeerIdVersion struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerIdVersion) Reset() { + *x = PeerIdVersion{} + mi := &file_peer_rpc_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerIdVersion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerIdVersion) ProtoMessage() {} + +func (x *PeerIdVersion) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerIdVersion.ProtoReflect.Descriptor instead. +func (*PeerIdVersion) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{3} +} + +func (x *PeerIdVersion) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *PeerIdVersion) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +type RouteConnBitmap struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerIds []*PeerIdVersion `protobuf:"bytes,1,rep,name=peer_ids,json=peerIds,proto3" json:"peer_ids,omitempty"` + Bitmap []byte `protobuf:"bytes,2,opt,name=bitmap,proto3" json:"bitmap,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RouteConnBitmap) Reset() { + *x = RouteConnBitmap{} + mi := &file_peer_rpc_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RouteConnBitmap) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RouteConnBitmap) ProtoMessage() {} + +func (x *RouteConnBitmap) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RouteConnBitmap.ProtoReflect.Descriptor instead. +func (*RouteConnBitmap) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{4} +} + +func (x *RouteConnBitmap) GetPeerIds() []*PeerIdVersion { + if x != nil { + return x.PeerIds + } + return nil +} + +func (x *RouteConnBitmap) GetBitmap() []byte { + if x != nil { + return x.Bitmap + } + return nil +} + +type RouteConnPeerList struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerConnInfos []*RouteConnPeerList_PeerConnInfo `protobuf:"bytes,1,rep,name=peer_conn_infos,json=peerConnInfos,proto3" json:"peer_conn_infos,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RouteConnPeerList) Reset() { + *x = RouteConnPeerList{} + mi := &file_peer_rpc_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RouteConnPeerList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RouteConnPeerList) ProtoMessage() {} + +func (x *RouteConnPeerList) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RouteConnPeerList.ProtoReflect.Descriptor instead. +func (*RouteConnPeerList) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{5} +} + +func (x *RouteConnPeerList) GetPeerConnInfos() []*RouteConnPeerList_PeerConnInfo { + if x != nil { + return x.PeerConnInfos + } + return nil +} + +type RoutePeerInfos struct { + state protoimpl.MessageState `protogen:"open.v1"` + Items []*RoutePeerInfo `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoutePeerInfos) Reset() { + *x = RoutePeerInfos{} + mi := &file_peer_rpc_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoutePeerInfos) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoutePeerInfos) ProtoMessage() {} + +func (x *RoutePeerInfos) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoutePeerInfos.ProtoReflect.Descriptor instead. +func (*RoutePeerInfos) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{6} +} + +func (x *RoutePeerInfos) GetItems() []*RoutePeerInfo { + if x != nil { + return x.Items + } + return nil +} + +type ForeignNetworkRouteInfoKey struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + NetworkName string `protobuf:"bytes,2,opt,name=network_name,json=networkName,proto3" json:"network_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ForeignNetworkRouteInfoKey) Reset() { + *x = ForeignNetworkRouteInfoKey{} + mi := &file_peer_rpc_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ForeignNetworkRouteInfoKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForeignNetworkRouteInfoKey) ProtoMessage() {} + +func (x *ForeignNetworkRouteInfoKey) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ForeignNetworkRouteInfoKey.ProtoReflect.Descriptor instead. +func (*ForeignNetworkRouteInfoKey) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{7} +} + +func (x *ForeignNetworkRouteInfoKey) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *ForeignNetworkRouteInfoKey) GetNetworkName() string { + if x != nil { + return x.NetworkName + } + return "" +} + +type ForeignNetworkRouteInfoEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + ForeignPeerIds []uint32 `protobuf:"varint,1,rep,packed,name=foreign_peer_ids,json=foreignPeerIds,proto3" json:"foreign_peer_ids,omitempty"` + LastUpdate *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=last_update,json=lastUpdate,proto3" json:"last_update,omitempty"` + Version uint32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` + NetworkSecretDigest []byte `protobuf:"bytes,4,opt,name=network_secret_digest,json=networkSecretDigest,proto3" json:"network_secret_digest,omitempty"` + MyPeerIdForThisNetwork uint32 `protobuf:"varint,5,opt,name=my_peer_id_for_this_network,json=myPeerIdForThisNetwork,proto3" json:"my_peer_id_for_this_network,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ForeignNetworkRouteInfoEntry) Reset() { + *x = ForeignNetworkRouteInfoEntry{} + mi := &file_peer_rpc_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ForeignNetworkRouteInfoEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForeignNetworkRouteInfoEntry) ProtoMessage() {} + +func (x *ForeignNetworkRouteInfoEntry) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ForeignNetworkRouteInfoEntry.ProtoReflect.Descriptor instead. +func (*ForeignNetworkRouteInfoEntry) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{8} +} + +func (x *ForeignNetworkRouteInfoEntry) GetForeignPeerIds() []uint32 { + if x != nil { + return x.ForeignPeerIds + } + return nil +} + +func (x *ForeignNetworkRouteInfoEntry) GetLastUpdate() *timestamppb.Timestamp { + if x != nil { + return x.LastUpdate + } + return nil +} + +func (x *ForeignNetworkRouteInfoEntry) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *ForeignNetworkRouteInfoEntry) GetNetworkSecretDigest() []byte { + if x != nil { + return x.NetworkSecretDigest + } + return nil +} + +func (x *ForeignNetworkRouteInfoEntry) GetMyPeerIdForThisNetwork() uint32 { + if x != nil { + return x.MyPeerIdForThisNetwork + } + return 0 +} + +type RouteForeignNetworkInfos struct { + state protoimpl.MessageState `protogen:"open.v1"` + Infos []*RouteForeignNetworkInfos_Info `protobuf:"bytes,1,rep,name=infos,proto3" json:"infos,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RouteForeignNetworkInfos) Reset() { + *x = RouteForeignNetworkInfos{} + mi := &file_peer_rpc_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RouteForeignNetworkInfos) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RouteForeignNetworkInfos) ProtoMessage() {} + +func (x *RouteForeignNetworkInfos) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RouteForeignNetworkInfos.ProtoReflect.Descriptor instead. +func (*RouteForeignNetworkInfos) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{9} +} + +func (x *RouteForeignNetworkInfos) GetInfos() []*RouteForeignNetworkInfos_Info { + if x != nil { + return x.Infos + } + return nil +} + +type RouteForeignNetworkSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + InfoMap map[uint32]*RouteForeignNetworkSummary_Info `protobuf:"bytes,1,rep,name=info_map,json=infoMap,proto3" json:"info_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RouteForeignNetworkSummary) Reset() { + *x = RouteForeignNetworkSummary{} + mi := &file_peer_rpc_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RouteForeignNetworkSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RouteForeignNetworkSummary) ProtoMessage() {} + +func (x *RouteForeignNetworkSummary) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RouteForeignNetworkSummary.ProtoReflect.Descriptor instead. +func (*RouteForeignNetworkSummary) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{10} +} + +func (x *RouteForeignNetworkSummary) GetInfoMap() map[uint32]*RouteForeignNetworkSummary_Info { + if x != nil { + return x.InfoMap + } + return nil +} + +type PeerGroupInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupName string `protobuf:"bytes,1,opt,name=group_name,json=groupName,proto3" json:"group_name,omitempty"` + GroupProof []byte `protobuf:"bytes,2,opt,name=group_proof,json=groupProof,proto3" json:"group_proof,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerGroupInfo) Reset() { + *x = PeerGroupInfo{} + mi := &file_peer_rpc_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerGroupInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerGroupInfo) ProtoMessage() {} + +func (x *PeerGroupInfo) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerGroupInfo.ProtoReflect.Descriptor instead. +func (*PeerGroupInfo) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{11} +} + +func (x *PeerGroupInfo) GetGroupName() string { + if x != nil { + return x.GroupName + } + return "" +} + +func (x *PeerGroupInfo) GetGroupProof() []byte { + if x != nil { + return x.GroupProof + } + return nil +} + +type SyncRouteInfoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + MyPeerId uint32 `protobuf:"varint,1,opt,name=my_peer_id,json=myPeerId,proto3" json:"my_peer_id,omitempty"` + MySessionId uint64 `protobuf:"varint,2,opt,name=my_session_id,json=mySessionId,proto3" json:"my_session_id,omitempty"` + IsInitiator bool `protobuf:"varint,3,opt,name=is_initiator,json=isInitiator,proto3" json:"is_initiator,omitempty"` + PeerInfos *RoutePeerInfos `protobuf:"bytes,4,opt,name=peer_infos,json=peerInfos,proto3" json:"peer_infos,omitempty"` + // Types that are valid to be assigned to ConnInfo: + // + // *SyncRouteInfoRequest_ConnBitmap + // *SyncRouteInfoRequest_ConnPeerList + ConnInfo isSyncRouteInfoRequest_ConnInfo `protobuf_oneof:"conn_info"` + ForeignNetworkInfos *RouteForeignNetworkInfos `protobuf:"bytes,6,opt,name=foreign_network_infos,json=foreignNetworkInfos,proto3" json:"foreign_network_infos,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncRouteInfoRequest) Reset() { + *x = SyncRouteInfoRequest{} + mi := &file_peer_rpc_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncRouteInfoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncRouteInfoRequest) ProtoMessage() {} + +func (x *SyncRouteInfoRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncRouteInfoRequest.ProtoReflect.Descriptor instead. +func (*SyncRouteInfoRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{12} +} + +func (x *SyncRouteInfoRequest) GetMyPeerId() uint32 { + if x != nil { + return x.MyPeerId + } + return 0 +} + +func (x *SyncRouteInfoRequest) GetMySessionId() uint64 { + if x != nil { + return x.MySessionId + } + return 0 +} + +func (x *SyncRouteInfoRequest) GetIsInitiator() bool { + if x != nil { + return x.IsInitiator + } + return false +} + +func (x *SyncRouteInfoRequest) GetPeerInfos() *RoutePeerInfos { + if x != nil { + return x.PeerInfos + } + return nil +} + +func (x *SyncRouteInfoRequest) GetConnInfo() isSyncRouteInfoRequest_ConnInfo { + if x != nil { + return x.ConnInfo + } + return nil +} + +func (x *SyncRouteInfoRequest) GetConnBitmap() *RouteConnBitmap { + if x != nil { + if x, ok := x.ConnInfo.(*SyncRouteInfoRequest_ConnBitmap); ok { + return x.ConnBitmap + } + } + return nil +} + +func (x *SyncRouteInfoRequest) GetConnPeerList() *RouteConnPeerList { + if x != nil { + if x, ok := x.ConnInfo.(*SyncRouteInfoRequest_ConnPeerList); ok { + return x.ConnPeerList + } + } + return nil +} + +func (x *SyncRouteInfoRequest) GetForeignNetworkInfos() *RouteForeignNetworkInfos { + if x != nil { + return x.ForeignNetworkInfos + } + return nil +} + +type isSyncRouteInfoRequest_ConnInfo interface { + isSyncRouteInfoRequest_ConnInfo() +} + +type SyncRouteInfoRequest_ConnBitmap struct { + ConnBitmap *RouteConnBitmap `protobuf:"bytes,5,opt,name=conn_bitmap,json=connBitmap,proto3,oneof"` +} + +type SyncRouteInfoRequest_ConnPeerList struct { + ConnPeerList *RouteConnPeerList `protobuf:"bytes,7,opt,name=conn_peer_list,json=connPeerList,proto3,oneof"` +} + +func (*SyncRouteInfoRequest_ConnBitmap) isSyncRouteInfoRequest_ConnInfo() {} + +func (*SyncRouteInfoRequest_ConnPeerList) isSyncRouteInfoRequest_ConnInfo() {} + +type SyncRouteInfoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + IsInitiator bool `protobuf:"varint,1,opt,name=is_initiator,json=isInitiator,proto3" json:"is_initiator,omitempty"` + SessionId uint64 `protobuf:"varint,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Error *SyncRouteInfoError `protobuf:"varint,3,opt,name=error,proto3,enum=peer_rpc.SyncRouteInfoError,oneof" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncRouteInfoResponse) Reset() { + *x = SyncRouteInfoResponse{} + mi := &file_peer_rpc_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncRouteInfoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncRouteInfoResponse) ProtoMessage() {} + +func (x *SyncRouteInfoResponse) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncRouteInfoResponse.ProtoReflect.Descriptor instead. +func (*SyncRouteInfoResponse) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{13} +} + +func (x *SyncRouteInfoResponse) GetIsInitiator() bool { + if x != nil { + return x.IsInitiator + } + return false +} + +func (x *SyncRouteInfoResponse) GetSessionId() uint64 { + if x != nil { + return x.SessionId + } + return 0 +} + +func (x *SyncRouteInfoResponse) GetError() SyncRouteInfoError { + if x != nil && x.Error != nil { + return *x.Error + } + return SyncRouteInfoError_DuplicatePeerId +} + +type AcquireIpv6PublicAddrLeaseRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + InstId *common.UUID `protobuf:"bytes,2,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AcquireIpv6PublicAddrLeaseRequest) Reset() { + *x = AcquireIpv6PublicAddrLeaseRequest{} + mi := &file_peer_rpc_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AcquireIpv6PublicAddrLeaseRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AcquireIpv6PublicAddrLeaseRequest) ProtoMessage() {} + +func (x *AcquireIpv6PublicAddrLeaseRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AcquireIpv6PublicAddrLeaseRequest.ProtoReflect.Descriptor instead. +func (*AcquireIpv6PublicAddrLeaseRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{14} +} + +func (x *AcquireIpv6PublicAddrLeaseRequest) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *AcquireIpv6PublicAddrLeaseRequest) GetInstId() *common.UUID { + if x != nil { + return x.InstId + } + return nil +} + +type RenewIpv6PublicAddrLeaseRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + InstId *common.UUID `protobuf:"bytes,2,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + LeasedAddr *common.Ipv6Inet `protobuf:"bytes,3,opt,name=leased_addr,json=leasedAddr,proto3" json:"leased_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenewIpv6PublicAddrLeaseRequest) Reset() { + *x = RenewIpv6PublicAddrLeaseRequest{} + mi := &file_peer_rpc_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenewIpv6PublicAddrLeaseRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenewIpv6PublicAddrLeaseRequest) ProtoMessage() {} + +func (x *RenewIpv6PublicAddrLeaseRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenewIpv6PublicAddrLeaseRequest.ProtoReflect.Descriptor instead. +func (*RenewIpv6PublicAddrLeaseRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{15} +} + +func (x *RenewIpv6PublicAddrLeaseRequest) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *RenewIpv6PublicAddrLeaseRequest) GetInstId() *common.UUID { + if x != nil { + return x.InstId + } + return nil +} + +func (x *RenewIpv6PublicAddrLeaseRequest) GetLeasedAddr() *common.Ipv6Inet { + if x != nil { + return x.LeasedAddr + } + return nil +} + +type ReleaseIpv6PublicAddrLeaseRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + InstId *common.UUID `protobuf:"bytes,2,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReleaseIpv6PublicAddrLeaseRequest) Reset() { + *x = ReleaseIpv6PublicAddrLeaseRequest{} + mi := &file_peer_rpc_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReleaseIpv6PublicAddrLeaseRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReleaseIpv6PublicAddrLeaseRequest) ProtoMessage() {} + +func (x *ReleaseIpv6PublicAddrLeaseRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReleaseIpv6PublicAddrLeaseRequest.ProtoReflect.Descriptor instead. +func (*ReleaseIpv6PublicAddrLeaseRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{16} +} + +func (x *ReleaseIpv6PublicAddrLeaseRequest) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *ReleaseIpv6PublicAddrLeaseRequest) GetInstId() *common.UUID { + if x != nil { + return x.InstId + } + return nil +} + +type GetIpv6PublicAddrLeaseRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + InstId *common.UUID `protobuf:"bytes,2,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetIpv6PublicAddrLeaseRequest) Reset() { + *x = GetIpv6PublicAddrLeaseRequest{} + mi := &file_peer_rpc_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetIpv6PublicAddrLeaseRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetIpv6PublicAddrLeaseRequest) ProtoMessage() {} + +func (x *GetIpv6PublicAddrLeaseRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetIpv6PublicAddrLeaseRequest.ProtoReflect.Descriptor instead. +func (*GetIpv6PublicAddrLeaseRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{17} +} + +func (x *GetIpv6PublicAddrLeaseRequest) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *GetIpv6PublicAddrLeaseRequest) GetInstId() *common.UUID { + if x != nil { + return x.InstId + } + return nil +} + +type Ipv6PublicAddrLeaseReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProviderPeerId uint32 `protobuf:"varint,1,opt,name=provider_peer_id,json=providerPeerId,proto3" json:"provider_peer_id,omitempty"` + ProviderInstId *common.UUID `protobuf:"bytes,2,opt,name=provider_inst_id,json=providerInstId,proto3" json:"provider_inst_id,omitempty"` + ProviderPrefix *common.Ipv6Inet `protobuf:"bytes,3,opt,name=provider_prefix,json=providerPrefix,proto3" json:"provider_prefix,omitempty"` + LeasedAddr *common.Ipv6Inet `protobuf:"bytes,4,opt,name=leased_addr,json=leasedAddr,proto3" json:"leased_addr,omitempty"` + ValidUntil *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=valid_until,json=validUntil,proto3" json:"valid_until,omitempty"` + Reused bool `protobuf:"varint,6,opt,name=reused,proto3" json:"reused,omitempty"` + ErrorMsg *string `protobuf:"bytes,7,opt,name=error_msg,json=errorMsg,proto3,oneof" json:"error_msg,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Ipv6PublicAddrLeaseReply) Reset() { + *x = Ipv6PublicAddrLeaseReply{} + mi := &file_peer_rpc_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Ipv6PublicAddrLeaseReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Ipv6PublicAddrLeaseReply) ProtoMessage() {} + +func (x *Ipv6PublicAddrLeaseReply) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Ipv6PublicAddrLeaseReply.ProtoReflect.Descriptor instead. +func (*Ipv6PublicAddrLeaseReply) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{18} +} + +func (x *Ipv6PublicAddrLeaseReply) GetProviderPeerId() uint32 { + if x != nil { + return x.ProviderPeerId + } + return 0 +} + +func (x *Ipv6PublicAddrLeaseReply) GetProviderInstId() *common.UUID { + if x != nil { + return x.ProviderInstId + } + return nil +} + +func (x *Ipv6PublicAddrLeaseReply) GetProviderPrefix() *common.Ipv6Inet { + if x != nil { + return x.ProviderPrefix + } + return nil +} + +func (x *Ipv6PublicAddrLeaseReply) GetLeasedAddr() *common.Ipv6Inet { + if x != nil { + return x.LeasedAddr + } + return nil +} + +func (x *Ipv6PublicAddrLeaseReply) GetValidUntil() *timestamppb.Timestamp { + if x != nil { + return x.ValidUntil + } + return nil +} + +func (x *Ipv6PublicAddrLeaseReply) GetReused() bool { + if x != nil { + return x.Reused + } + return false +} + +func (x *Ipv6PublicAddrLeaseReply) GetErrorMsg() string { + if x != nil && x.ErrorMsg != nil { + return *x.ErrorMsg + } + return "" +} + +type GetIpListRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetIpListRequest) Reset() { + *x = GetIpListRequest{} + mi := &file_peer_rpc_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetIpListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetIpListRequest) ProtoMessage() {} + +func (x *GetIpListRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetIpListRequest.ProtoReflect.Descriptor instead. +func (*GetIpListRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{19} +} + +type GetIpListResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + PublicIpv4 *common.Ipv4Addr `protobuf:"bytes,1,opt,name=public_ipv4,json=publicIpv4,proto3" json:"public_ipv4,omitempty"` + InterfaceIpv4S []*common.Ipv4Addr `protobuf:"bytes,2,rep,name=interface_ipv4s,json=interfaceIpv4s,proto3" json:"interface_ipv4s,omitempty"` + PublicIpv6 *common.Ipv6Addr `protobuf:"bytes,3,opt,name=public_ipv6,json=publicIpv6,proto3" json:"public_ipv6,omitempty"` + InterfaceIpv6S []*common.Ipv6Addr `protobuf:"bytes,4,rep,name=interface_ipv6s,json=interfaceIpv6s,proto3" json:"interface_ipv6s,omitempty"` + Listeners []*common.Url `protobuf:"bytes,5,rep,name=listeners,proto3" json:"listeners,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetIpListResponse) Reset() { + *x = GetIpListResponse{} + mi := &file_peer_rpc_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetIpListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetIpListResponse) ProtoMessage() {} + +func (x *GetIpListResponse) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetIpListResponse.ProtoReflect.Descriptor instead. +func (*GetIpListResponse) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{20} +} + +func (x *GetIpListResponse) GetPublicIpv4() *common.Ipv4Addr { + if x != nil { + return x.PublicIpv4 + } + return nil +} + +func (x *GetIpListResponse) GetInterfaceIpv4S() []*common.Ipv4Addr { + if x != nil { + return x.InterfaceIpv4S + } + return nil +} + +func (x *GetIpListResponse) GetPublicIpv6() *common.Ipv6Addr { + if x != nil { + return x.PublicIpv6 + } + return nil +} + +func (x *GetIpListResponse) GetInterfaceIpv6S() []*common.Ipv6Addr { + if x != nil { + return x.InterfaceIpv6S + } + return nil +} + +func (x *GetIpListResponse) GetListeners() []*common.Url { + if x != nil { + return x.Listeners + } + return nil +} + +type SendUdpHolePunchPacketRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConnectorAddr *common.SocketAddr `protobuf:"bytes,1,opt,name=connector_addr,json=connectorAddr,proto3" json:"connector_addr,omitempty"` + ListenerPort uint32 `protobuf:"varint,2,opt,name=listener_port,json=listenerPort,proto3" json:"listener_port,omitempty"` + PreferredSrcIpv6 *common.Ipv6Addr `protobuf:"bytes,3,opt,name=preferred_src_ipv6,json=preferredSrcIpv6,proto3" json:"preferred_src_ipv6,omitempty"` + ConnectorAddrs []*common.SocketAddr `protobuf:"bytes,4,rep,name=connector_addrs,json=connectorAddrs,proto3" json:"connector_addrs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendUdpHolePunchPacketRequest) Reset() { + *x = SendUdpHolePunchPacketRequest{} + mi := &file_peer_rpc_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendUdpHolePunchPacketRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendUdpHolePunchPacketRequest) ProtoMessage() {} + +func (x *SendUdpHolePunchPacketRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendUdpHolePunchPacketRequest.ProtoReflect.Descriptor instead. +func (*SendUdpHolePunchPacketRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{21} +} + +func (x *SendUdpHolePunchPacketRequest) GetConnectorAddr() *common.SocketAddr { + if x != nil { + return x.ConnectorAddr + } + return nil +} + +func (x *SendUdpHolePunchPacketRequest) GetListenerPort() uint32 { + if x != nil { + return x.ListenerPort + } + return 0 +} + +func (x *SendUdpHolePunchPacketRequest) GetPreferredSrcIpv6() *common.Ipv6Addr { + if x != nil { + return x.PreferredSrcIpv6 + } + return nil +} + +func (x *SendUdpHolePunchPacketRequest) GetConnectorAddrs() []*common.SocketAddr { + if x != nil { + return x.ConnectorAddrs + } + return nil +} + +type SelectPunchListenerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ForceNew bool `protobuf:"varint,1,opt,name=force_new,json=forceNew,proto3" json:"force_new,omitempty"` + PreferPortMapping bool `protobuf:"varint,2,opt,name=prefer_port_mapping,json=preferPortMapping,proto3" json:"prefer_port_mapping,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SelectPunchListenerRequest) Reset() { + *x = SelectPunchListenerRequest{} + mi := &file_peer_rpc_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SelectPunchListenerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SelectPunchListenerRequest) ProtoMessage() {} + +func (x *SelectPunchListenerRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SelectPunchListenerRequest.ProtoReflect.Descriptor instead. +func (*SelectPunchListenerRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{22} +} + +func (x *SelectPunchListenerRequest) GetForceNew() bool { + if x != nil { + return x.ForceNew + } + return false +} + +func (x *SelectPunchListenerRequest) GetPreferPortMapping() bool { + if x != nil { + return x.PreferPortMapping + } + return false +} + +type SelectPunchListenerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ListenerMappedAddr *common.SocketAddr `protobuf:"bytes,1,opt,name=listener_mapped_addr,json=listenerMappedAddr,proto3" json:"listener_mapped_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SelectPunchListenerResponse) Reset() { + *x = SelectPunchListenerResponse{} + mi := &file_peer_rpc_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SelectPunchListenerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SelectPunchListenerResponse) ProtoMessage() {} + +func (x *SelectPunchListenerResponse) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SelectPunchListenerResponse.ProtoReflect.Descriptor instead. +func (*SelectPunchListenerResponse) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{23} +} + +func (x *SelectPunchListenerResponse) GetListenerMappedAddr() *common.SocketAddr { + if x != nil { + return x.ListenerMappedAddr + } + return nil +} + +type SendPunchPacketConeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ListenerMappedAddr *common.SocketAddr `protobuf:"bytes,1,opt,name=listener_mapped_addr,json=listenerMappedAddr,proto3" json:"listener_mapped_addr,omitempty"` + DestAddr *common.SocketAddr `protobuf:"bytes,2,opt,name=dest_addr,json=destAddr,proto3" json:"dest_addr,omitempty"` + TransactionId uint32 `protobuf:"varint,3,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` + // send this many packets in a batch + PacketCountPerBatch uint32 `protobuf:"varint,4,opt,name=packet_count_per_batch,json=packetCountPerBatch,proto3" json:"packet_count_per_batch,omitempty"` + // send total this batch count, total packet count = packet_batch_size * packet_batch_count + PacketBatchCount uint32 `protobuf:"varint,5,opt,name=packet_batch_count,json=packetBatchCount,proto3" json:"packet_batch_count,omitempty"` + // interval between each batch + PacketIntervalMs uint32 `protobuf:"varint,6,opt,name=packet_interval_ms,json=packetIntervalMs,proto3" json:"packet_interval_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendPunchPacketConeRequest) Reset() { + *x = SendPunchPacketConeRequest{} + mi := &file_peer_rpc_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendPunchPacketConeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendPunchPacketConeRequest) ProtoMessage() {} + +func (x *SendPunchPacketConeRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendPunchPacketConeRequest.ProtoReflect.Descriptor instead. +func (*SendPunchPacketConeRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{24} +} + +func (x *SendPunchPacketConeRequest) GetListenerMappedAddr() *common.SocketAddr { + if x != nil { + return x.ListenerMappedAddr + } + return nil +} + +func (x *SendPunchPacketConeRequest) GetDestAddr() *common.SocketAddr { + if x != nil { + return x.DestAddr + } + return nil +} + +func (x *SendPunchPacketConeRequest) GetTransactionId() uint32 { + if x != nil { + return x.TransactionId + } + return 0 +} + +func (x *SendPunchPacketConeRequest) GetPacketCountPerBatch() uint32 { + if x != nil { + return x.PacketCountPerBatch + } + return 0 +} + +func (x *SendPunchPacketConeRequest) GetPacketBatchCount() uint32 { + if x != nil { + return x.PacketBatchCount + } + return 0 +} + +func (x *SendPunchPacketConeRequest) GetPacketIntervalMs() uint32 { + if x != nil { + return x.PacketIntervalMs + } + return 0 +} + +type SendPunchPacketHardSymRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ListenerMappedAddr *common.SocketAddr `protobuf:"bytes,1,opt,name=listener_mapped_addr,json=listenerMappedAddr,proto3" json:"listener_mapped_addr,omitempty"` + PublicIps []*common.Ipv4Addr `protobuf:"bytes,2,rep,name=public_ips,json=publicIps,proto3" json:"public_ips,omitempty"` + TransactionId uint32 `protobuf:"varint,3,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` + PortIndex uint32 `protobuf:"varint,4,opt,name=port_index,json=portIndex,proto3" json:"port_index,omitempty"` + Round uint32 `protobuf:"varint,5,opt,name=round,proto3" json:"round,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendPunchPacketHardSymRequest) Reset() { + *x = SendPunchPacketHardSymRequest{} + mi := &file_peer_rpc_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendPunchPacketHardSymRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendPunchPacketHardSymRequest) ProtoMessage() {} + +func (x *SendPunchPacketHardSymRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendPunchPacketHardSymRequest.ProtoReflect.Descriptor instead. +func (*SendPunchPacketHardSymRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{25} +} + +func (x *SendPunchPacketHardSymRequest) GetListenerMappedAddr() *common.SocketAddr { + if x != nil { + return x.ListenerMappedAddr + } + return nil +} + +func (x *SendPunchPacketHardSymRequest) GetPublicIps() []*common.Ipv4Addr { + if x != nil { + return x.PublicIps + } + return nil +} + +func (x *SendPunchPacketHardSymRequest) GetTransactionId() uint32 { + if x != nil { + return x.TransactionId + } + return 0 +} + +func (x *SendPunchPacketHardSymRequest) GetPortIndex() uint32 { + if x != nil { + return x.PortIndex + } + return 0 +} + +func (x *SendPunchPacketHardSymRequest) GetRound() uint32 { + if x != nil { + return x.Round + } + return 0 +} + +type SendPunchPacketHardSymResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + NextPortIndex uint32 `protobuf:"varint,1,opt,name=next_port_index,json=nextPortIndex,proto3" json:"next_port_index,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendPunchPacketHardSymResponse) Reset() { + *x = SendPunchPacketHardSymResponse{} + mi := &file_peer_rpc_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendPunchPacketHardSymResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendPunchPacketHardSymResponse) ProtoMessage() {} + +func (x *SendPunchPacketHardSymResponse) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendPunchPacketHardSymResponse.ProtoReflect.Descriptor instead. +func (*SendPunchPacketHardSymResponse) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{26} +} + +func (x *SendPunchPacketHardSymResponse) GetNextPortIndex() uint32 { + if x != nil { + return x.NextPortIndex + } + return 0 +} + +type SendPunchPacketEasySymRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ListenerMappedAddr *common.SocketAddr `protobuf:"bytes,1,opt,name=listener_mapped_addr,json=listenerMappedAddr,proto3" json:"listener_mapped_addr,omitempty"` + PublicIps []*common.Ipv4Addr `protobuf:"bytes,2,rep,name=public_ips,json=publicIps,proto3" json:"public_ips,omitempty"` + TransactionId uint32 `protobuf:"varint,3,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` + BasePortNum uint32 `protobuf:"varint,4,opt,name=base_port_num,json=basePortNum,proto3" json:"base_port_num,omitempty"` + MaxPortNum uint32 `protobuf:"varint,5,opt,name=max_port_num,json=maxPortNum,proto3" json:"max_port_num,omitempty"` + IsIncremental bool `protobuf:"varint,6,opt,name=is_incremental,json=isIncremental,proto3" json:"is_incremental,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendPunchPacketEasySymRequest) Reset() { + *x = SendPunchPacketEasySymRequest{} + mi := &file_peer_rpc_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendPunchPacketEasySymRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendPunchPacketEasySymRequest) ProtoMessage() {} + +func (x *SendPunchPacketEasySymRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendPunchPacketEasySymRequest.ProtoReflect.Descriptor instead. +func (*SendPunchPacketEasySymRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{27} +} + +func (x *SendPunchPacketEasySymRequest) GetListenerMappedAddr() *common.SocketAddr { + if x != nil { + return x.ListenerMappedAddr + } + return nil +} + +func (x *SendPunchPacketEasySymRequest) GetPublicIps() []*common.Ipv4Addr { + if x != nil { + return x.PublicIps + } + return nil +} + +func (x *SendPunchPacketEasySymRequest) GetTransactionId() uint32 { + if x != nil { + return x.TransactionId + } + return 0 +} + +func (x *SendPunchPacketEasySymRequest) GetBasePortNum() uint32 { + if x != nil { + return x.BasePortNum + } + return 0 +} + +func (x *SendPunchPacketEasySymRequest) GetMaxPortNum() uint32 { + if x != nil { + return x.MaxPortNum + } + return 0 +} + +func (x *SendPunchPacketEasySymRequest) GetIsIncremental() bool { + if x != nil { + return x.IsIncremental + } + return false +} + +type SendPunchPacketBothEasySymRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UdpSocketCount uint32 `protobuf:"varint,1,opt,name=udp_socket_count,json=udpSocketCount,proto3" json:"udp_socket_count,omitempty"` + PublicIp *common.Ipv4Addr `protobuf:"bytes,2,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"` + TransactionId uint32 `protobuf:"varint,3,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` + DstPortNum uint32 `protobuf:"varint,4,opt,name=dst_port_num,json=dstPortNum,proto3" json:"dst_port_num,omitempty"` + WaitTimeMs uint32 `protobuf:"varint,5,opt,name=wait_time_ms,json=waitTimeMs,proto3" json:"wait_time_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendPunchPacketBothEasySymRequest) Reset() { + *x = SendPunchPacketBothEasySymRequest{} + mi := &file_peer_rpc_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendPunchPacketBothEasySymRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendPunchPacketBothEasySymRequest) ProtoMessage() {} + +func (x *SendPunchPacketBothEasySymRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendPunchPacketBothEasySymRequest.ProtoReflect.Descriptor instead. +func (*SendPunchPacketBothEasySymRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{28} +} + +func (x *SendPunchPacketBothEasySymRequest) GetUdpSocketCount() uint32 { + if x != nil { + return x.UdpSocketCount + } + return 0 +} + +func (x *SendPunchPacketBothEasySymRequest) GetPublicIp() *common.Ipv4Addr { + if x != nil { + return x.PublicIp + } + return nil +} + +func (x *SendPunchPacketBothEasySymRequest) GetTransactionId() uint32 { + if x != nil { + return x.TransactionId + } + return 0 +} + +func (x *SendPunchPacketBothEasySymRequest) GetDstPortNum() uint32 { + if x != nil { + return x.DstPortNum + } + return 0 +} + +func (x *SendPunchPacketBothEasySymRequest) GetWaitTimeMs() uint32 { + if x != nil { + return x.WaitTimeMs + } + return 0 +} + +type SendPunchPacketBothEasySymResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // is doing punch with other peer + IsBusy bool `protobuf:"varint,1,opt,name=is_busy,json=isBusy,proto3" json:"is_busy,omitempty"` + BaseMappedAddr *common.SocketAddr `protobuf:"bytes,2,opt,name=base_mapped_addr,json=baseMappedAddr,proto3" json:"base_mapped_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendPunchPacketBothEasySymResponse) Reset() { + *x = SendPunchPacketBothEasySymResponse{} + mi := &file_peer_rpc_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendPunchPacketBothEasySymResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendPunchPacketBothEasySymResponse) ProtoMessage() {} + +func (x *SendPunchPacketBothEasySymResponse) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendPunchPacketBothEasySymResponse.ProtoReflect.Descriptor instead. +func (*SendPunchPacketBothEasySymResponse) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{29} +} + +func (x *SendPunchPacketBothEasySymResponse) GetIsBusy() bool { + if x != nil { + return x.IsBusy + } + return false +} + +func (x *SendPunchPacketBothEasySymResponse) GetBaseMappedAddr() *common.SocketAddr { + if x != nil { + return x.BaseMappedAddr + } + return nil +} + +type TcpHolePunchRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConnectorMappedAddr *common.SocketAddr `protobuf:"bytes,1,opt,name=connector_mapped_addr,json=connectorMappedAddr,proto3" json:"connector_mapped_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpHolePunchRequest) Reset() { + *x = TcpHolePunchRequest{} + mi := &file_peer_rpc_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpHolePunchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpHolePunchRequest) ProtoMessage() {} + +func (x *TcpHolePunchRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TcpHolePunchRequest.ProtoReflect.Descriptor instead. +func (*TcpHolePunchRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{30} +} + +func (x *TcpHolePunchRequest) GetConnectorMappedAddr() *common.SocketAddr { + if x != nil { + return x.ConnectorMappedAddr + } + return nil +} + +type TcpHolePunchResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ListenerMappedAddr *common.SocketAddr `protobuf:"bytes,1,opt,name=listener_mapped_addr,json=listenerMappedAddr,proto3" json:"listener_mapped_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpHolePunchResponse) Reset() { + *x = TcpHolePunchResponse{} + mi := &file_peer_rpc_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpHolePunchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpHolePunchResponse) ProtoMessage() {} + +func (x *TcpHolePunchResponse) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TcpHolePunchResponse.ProtoReflect.Descriptor instead. +func (*TcpHolePunchResponse) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{31} +} + +func (x *TcpHolePunchResponse) GetListenerMappedAddr() *common.SocketAddr { + if x != nil { + return x.ListenerMappedAddr + } + return nil +} + +type DirectConnectedPeerInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + LatencyMs int32 `protobuf:"varint,1,opt,name=latency_ms,json=latencyMs,proto3" json:"latency_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DirectConnectedPeerInfo) Reset() { + *x = DirectConnectedPeerInfo{} + mi := &file_peer_rpc_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DirectConnectedPeerInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DirectConnectedPeerInfo) ProtoMessage() {} + +func (x *DirectConnectedPeerInfo) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DirectConnectedPeerInfo.ProtoReflect.Descriptor instead. +func (*DirectConnectedPeerInfo) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{32} +} + +func (x *DirectConnectedPeerInfo) GetLatencyMs() int32 { + if x != nil { + return x.LatencyMs + } + return 0 +} + +type PeerInfoForGlobalMap struct { + state protoimpl.MessageState `protogen:"open.v1"` + DirectPeers map[uint32]*DirectConnectedPeerInfo `protobuf:"bytes,1,rep,name=direct_peers,json=directPeers,proto3" json:"direct_peers,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerInfoForGlobalMap) Reset() { + *x = PeerInfoForGlobalMap{} + mi := &file_peer_rpc_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerInfoForGlobalMap) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerInfoForGlobalMap) ProtoMessage() {} + +func (x *PeerInfoForGlobalMap) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerInfoForGlobalMap.ProtoReflect.Descriptor instead. +func (*PeerInfoForGlobalMap) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{33} +} + +func (x *PeerInfoForGlobalMap) GetDirectPeers() map[uint32]*DirectConnectedPeerInfo { + if x != nil { + return x.DirectPeers + } + return nil +} + +type ReportPeersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + MyPeerId uint32 `protobuf:"varint,1,opt,name=my_peer_id,json=myPeerId,proto3" json:"my_peer_id,omitempty"` + PeerInfos *PeerInfoForGlobalMap `protobuf:"bytes,2,opt,name=peer_infos,json=peerInfos,proto3" json:"peer_infos,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportPeersRequest) Reset() { + *x = ReportPeersRequest{} + mi := &file_peer_rpc_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportPeersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportPeersRequest) ProtoMessage() {} + +func (x *ReportPeersRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReportPeersRequest.ProtoReflect.Descriptor instead. +func (*ReportPeersRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{34} +} + +func (x *ReportPeersRequest) GetMyPeerId() uint32 { + if x != nil { + return x.MyPeerId + } + return 0 +} + +func (x *ReportPeersRequest) GetPeerInfos() *PeerInfoForGlobalMap { + if x != nil { + return x.PeerInfos + } + return nil +} + +type ReportPeersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportPeersResponse) Reset() { + *x = ReportPeersResponse{} + mi := &file_peer_rpc_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportPeersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportPeersResponse) ProtoMessage() {} + +func (x *ReportPeersResponse) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReportPeersResponse.ProtoReflect.Descriptor instead. +func (*ReportPeersResponse) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{35} +} + +type GlobalPeerMap struct { + state protoimpl.MessageState `protogen:"open.v1"` + Map map[uint32]*PeerInfoForGlobalMap `protobuf:"bytes,1,rep,name=map,proto3" json:"map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GlobalPeerMap) Reset() { + *x = GlobalPeerMap{} + mi := &file_peer_rpc_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GlobalPeerMap) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GlobalPeerMap) ProtoMessage() {} + +func (x *GlobalPeerMap) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GlobalPeerMap.ProtoReflect.Descriptor instead. +func (*GlobalPeerMap) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{36} +} + +func (x *GlobalPeerMap) GetMap() map[uint32]*PeerInfoForGlobalMap { + if x != nil { + return x.Map + } + return nil +} + +type GetGlobalPeerMapRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Digest uint64 `protobuf:"varint,1,opt,name=digest,proto3" json:"digest,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGlobalPeerMapRequest) Reset() { + *x = GetGlobalPeerMapRequest{} + mi := &file_peer_rpc_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGlobalPeerMapRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGlobalPeerMapRequest) ProtoMessage() {} + +func (x *GetGlobalPeerMapRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetGlobalPeerMapRequest.ProtoReflect.Descriptor instead. +func (*GetGlobalPeerMapRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{37} +} + +func (x *GetGlobalPeerMapRequest) GetDigest() uint64 { + if x != nil { + return x.Digest + } + return 0 +} + +type GetGlobalPeerMapResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + GlobalPeerMap map[uint32]*PeerInfoForGlobalMap `protobuf:"bytes,1,rep,name=global_peer_map,json=globalPeerMap,proto3" json:"global_peer_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Digest *uint64 `protobuf:"varint,2,opt,name=digest,proto3,oneof" json:"digest,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGlobalPeerMapResponse) Reset() { + *x = GetGlobalPeerMapResponse{} + mi := &file_peer_rpc_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGlobalPeerMapResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGlobalPeerMapResponse) ProtoMessage() {} + +func (x *GetGlobalPeerMapResponse) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetGlobalPeerMapResponse.ProtoReflect.Descriptor instead. +func (*GetGlobalPeerMapResponse) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{38} +} + +func (x *GetGlobalPeerMapResponse) GetGlobalPeerMap() map[uint32]*PeerInfoForGlobalMap { + if x != nil { + return x.GlobalPeerMap + } + return nil +} + +func (x *GetGlobalPeerMapResponse) GetDigest() uint64 { + if x != nil && x.Digest != nil { + return *x.Digest + } + return 0 +} + +type HandshakeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Magic uint32 `protobuf:"varint,1,opt,name=magic,proto3" json:"magic,omitempty"` + MyPeerId uint32 `protobuf:"varint,2,opt,name=my_peer_id,json=myPeerId,proto3" json:"my_peer_id,omitempty"` + Version uint32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` + Features []string `protobuf:"bytes,4,rep,name=features,proto3" json:"features,omitempty"` + NetworkName string `protobuf:"bytes,5,opt,name=network_name,json=networkName,proto3" json:"network_name,omitempty"` + NetworkSecretDigest []byte `protobuf:"bytes,6,opt,name=network_secret_digest,json=networkSecretDigest,proto3" json:"network_secret_digest,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HandshakeRequest) Reset() { + *x = HandshakeRequest{} + mi := &file_peer_rpc_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HandshakeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HandshakeRequest) ProtoMessage() {} + +func (x *HandshakeRequest) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HandshakeRequest.ProtoReflect.Descriptor instead. +func (*HandshakeRequest) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{39} +} + +func (x *HandshakeRequest) GetMagic() uint32 { + if x != nil { + return x.Magic + } + return 0 +} + +func (x *HandshakeRequest) GetMyPeerId() uint32 { + if x != nil { + return x.MyPeerId + } + return 0 +} + +func (x *HandshakeRequest) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *HandshakeRequest) GetFeatures() []string { + if x != nil { + return x.Features + } + return nil +} + +func (x *HandshakeRequest) GetNetworkName() string { + if x != nil { + return x.NetworkName + } + return "" +} + +func (x *HandshakeRequest) GetNetworkSecretDigest() []byte { + if x != nil { + return x.NetworkSecretDigest + } + return nil +} + +type KcpConnData struct { + state protoimpl.MessageState `protogen:"open.v1"` + Src *common.SocketAddr `protobuf:"bytes,1,opt,name=src,proto3" json:"src,omitempty"` + Dst *common.SocketAddr `protobuf:"bytes,4,opt,name=dst,proto3" json:"dst,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *KcpConnData) Reset() { + *x = KcpConnData{} + mi := &file_peer_rpc_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *KcpConnData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KcpConnData) ProtoMessage() {} + +func (x *KcpConnData) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KcpConnData.ProtoReflect.Descriptor instead. +func (*KcpConnData) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{40} +} + +func (x *KcpConnData) GetSrc() *common.SocketAddr { + if x != nil { + return x.Src + } + return nil +} + +func (x *KcpConnData) GetDst() *common.SocketAddr { + if x != nil { + return x.Dst + } + return nil +} + +type PeerConnNoiseMsg1Pb struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + ANetworkName string `protobuf:"bytes,2,opt,name=a_network_name,json=aNetworkName,proto3" json:"a_network_name,omitempty"` + ASessionGeneration *uint32 `protobuf:"varint,3,opt,name=a_session_generation,json=aSessionGeneration,proto3,oneof" json:"a_session_generation,omitempty"` + AConnId *common.UUID `protobuf:"bytes,4,opt,name=a_conn_id,json=aConnId,proto3" json:"a_conn_id,omitempty"` + ClientEncryptionAlgorithm string `protobuf:"bytes,5,opt,name=client_encryption_algorithm,json=clientEncryptionAlgorithm,proto3" json:"client_encryption_algorithm,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerConnNoiseMsg1Pb) Reset() { + *x = PeerConnNoiseMsg1Pb{} + mi := &file_peer_rpc_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerConnNoiseMsg1Pb) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerConnNoiseMsg1Pb) ProtoMessage() {} + +func (x *PeerConnNoiseMsg1Pb) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerConnNoiseMsg1Pb.ProtoReflect.Descriptor instead. +func (*PeerConnNoiseMsg1Pb) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{41} +} + +func (x *PeerConnNoiseMsg1Pb) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *PeerConnNoiseMsg1Pb) GetANetworkName() string { + if x != nil { + return x.ANetworkName + } + return "" +} + +func (x *PeerConnNoiseMsg1Pb) GetASessionGeneration() uint32 { + if x != nil && x.ASessionGeneration != nil { + return *x.ASessionGeneration + } + return 0 +} + +func (x *PeerConnNoiseMsg1Pb) GetAConnId() *common.UUID { + if x != nil { + return x.AConnId + } + return nil +} + +func (x *PeerConnNoiseMsg1Pb) GetClientEncryptionAlgorithm() string { + if x != nil { + return x.ClientEncryptionAlgorithm + } + return "" +} + +type PeerConnNoiseMsg2Pb struct { + state protoimpl.MessageState `protogen:"open.v1"` + BNetworkName string `protobuf:"bytes,1,opt,name=b_network_name,json=bNetworkName,proto3" json:"b_network_name,omitempty"` + RoleHint uint32 `protobuf:"varint,2,opt,name=role_hint,json=roleHint,proto3" json:"role_hint,omitempty"` + Action PeerConnSessionActionPb `protobuf:"varint,3,opt,name=action,proto3,enum=peer_rpc.PeerConnSessionActionPb" json:"action,omitempty"` + BSessionGeneration uint32 `protobuf:"varint,4,opt,name=b_session_generation,json=bSessionGeneration,proto3" json:"b_session_generation,omitempty"` + RootKey_32 []byte `protobuf:"bytes,5,opt,name=root_key_32,json=rootKey32,proto3,oneof" json:"root_key_32,omitempty"` + InitialEpoch uint32 `protobuf:"varint,6,opt,name=initial_epoch,json=initialEpoch,proto3" json:"initial_epoch,omitempty"` + BConnId *common.UUID `protobuf:"bytes,7,opt,name=b_conn_id,json=bConnId,proto3" json:"b_conn_id,omitempty"` + AConnIdEcho *common.UUID `protobuf:"bytes,8,opt,name=a_conn_id_echo,json=aConnIdEcho,proto3" json:"a_conn_id_echo,omitempty"` + SecretProof_32 []byte `protobuf:"bytes,9,opt,name=secret_proof_32,json=secretProof32,proto3,oneof" json:"secret_proof_32,omitempty"` + ServerEncryptionAlgorithm string `protobuf:"bytes,10,opt,name=server_encryption_algorithm,json=serverEncryptionAlgorithm,proto3" json:"server_encryption_algorithm,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerConnNoiseMsg2Pb) Reset() { + *x = PeerConnNoiseMsg2Pb{} + mi := &file_peer_rpc_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerConnNoiseMsg2Pb) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerConnNoiseMsg2Pb) ProtoMessage() {} + +func (x *PeerConnNoiseMsg2Pb) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerConnNoiseMsg2Pb.ProtoReflect.Descriptor instead. +func (*PeerConnNoiseMsg2Pb) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{42} +} + +func (x *PeerConnNoiseMsg2Pb) GetBNetworkName() string { + if x != nil { + return x.BNetworkName + } + return "" +} + +func (x *PeerConnNoiseMsg2Pb) GetRoleHint() uint32 { + if x != nil { + return x.RoleHint + } + return 0 +} + +func (x *PeerConnNoiseMsg2Pb) GetAction() PeerConnSessionActionPb { + if x != nil { + return x.Action + } + return PeerConnSessionActionPb_Join +} + +func (x *PeerConnNoiseMsg2Pb) GetBSessionGeneration() uint32 { + if x != nil { + return x.BSessionGeneration + } + return 0 +} + +func (x *PeerConnNoiseMsg2Pb) GetRootKey_32() []byte { + if x != nil { + return x.RootKey_32 + } + return nil +} + +func (x *PeerConnNoiseMsg2Pb) GetInitialEpoch() uint32 { + if x != nil { + return x.InitialEpoch + } + return 0 +} + +func (x *PeerConnNoiseMsg2Pb) GetBConnId() *common.UUID { + if x != nil { + return x.BConnId + } + return nil +} + +func (x *PeerConnNoiseMsg2Pb) GetAConnIdEcho() *common.UUID { + if x != nil { + return x.AConnIdEcho + } + return nil +} + +func (x *PeerConnNoiseMsg2Pb) GetSecretProof_32() []byte { + if x != nil { + return x.SecretProof_32 + } + return nil +} + +func (x *PeerConnNoiseMsg2Pb) GetServerEncryptionAlgorithm() string { + if x != nil { + return x.ServerEncryptionAlgorithm + } + return "" +} + +type RelayNoiseMsg1Pb struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + ASessionGeneration *uint32 `protobuf:"varint,3,opt,name=a_session_generation,json=aSessionGeneration,proto3,oneof" json:"a_session_generation,omitempty"` + AConnId *common.UUID `protobuf:"bytes,4,opt,name=a_conn_id,json=aConnId,proto3" json:"a_conn_id,omitempty"` + ClientEncryptionAlgorithm string `protobuf:"bytes,5,opt,name=client_encryption_algorithm,json=clientEncryptionAlgorithm,proto3" json:"client_encryption_algorithm,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayNoiseMsg1Pb) Reset() { + *x = RelayNoiseMsg1Pb{} + mi := &file_peer_rpc_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayNoiseMsg1Pb) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayNoiseMsg1Pb) ProtoMessage() {} + +func (x *RelayNoiseMsg1Pb) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelayNoiseMsg1Pb.ProtoReflect.Descriptor instead. +func (*RelayNoiseMsg1Pb) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{43} +} + +func (x *RelayNoiseMsg1Pb) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *RelayNoiseMsg1Pb) GetASessionGeneration() uint32 { + if x != nil && x.ASessionGeneration != nil { + return *x.ASessionGeneration + } + return 0 +} + +func (x *RelayNoiseMsg1Pb) GetAConnId() *common.UUID { + if x != nil { + return x.AConnId + } + return nil +} + +func (x *RelayNoiseMsg1Pb) GetClientEncryptionAlgorithm() string { + if x != nil { + return x.ClientEncryptionAlgorithm + } + return "" +} + +type RelayNoiseMsg2Pb struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action PeerConnSessionActionPb `protobuf:"varint,3,opt,name=action,proto3,enum=peer_rpc.PeerConnSessionActionPb" json:"action,omitempty"` + BSessionGeneration uint32 `protobuf:"varint,4,opt,name=b_session_generation,json=bSessionGeneration,proto3" json:"b_session_generation,omitempty"` + RootKey_32 []byte `protobuf:"bytes,5,opt,name=root_key_32,json=rootKey32,proto3,oneof" json:"root_key_32,omitempty"` + InitialEpoch uint32 `protobuf:"varint,6,opt,name=initial_epoch,json=initialEpoch,proto3" json:"initial_epoch,omitempty"` + BConnId *common.UUID `protobuf:"bytes,7,opt,name=b_conn_id,json=bConnId,proto3" json:"b_conn_id,omitempty"` + AConnIdEcho *common.UUID `protobuf:"bytes,8,opt,name=a_conn_id_echo,json=aConnIdEcho,proto3" json:"a_conn_id_echo,omitempty"` + ServerEncryptionAlgorithm string `protobuf:"bytes,10,opt,name=server_encryption_algorithm,json=serverEncryptionAlgorithm,proto3" json:"server_encryption_algorithm,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayNoiseMsg2Pb) Reset() { + *x = RelayNoiseMsg2Pb{} + mi := &file_peer_rpc_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayNoiseMsg2Pb) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayNoiseMsg2Pb) ProtoMessage() {} + +func (x *RelayNoiseMsg2Pb) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelayNoiseMsg2Pb.ProtoReflect.Descriptor instead. +func (*RelayNoiseMsg2Pb) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{44} +} + +func (x *RelayNoiseMsg2Pb) GetAction() PeerConnSessionActionPb { + if x != nil { + return x.Action + } + return PeerConnSessionActionPb_Join +} + +func (x *RelayNoiseMsg2Pb) GetBSessionGeneration() uint32 { + if x != nil { + return x.BSessionGeneration + } + return 0 +} + +func (x *RelayNoiseMsg2Pb) GetRootKey_32() []byte { + if x != nil { + return x.RootKey_32 + } + return nil +} + +func (x *RelayNoiseMsg2Pb) GetInitialEpoch() uint32 { + if x != nil { + return x.InitialEpoch + } + return 0 +} + +func (x *RelayNoiseMsg2Pb) GetBConnId() *common.UUID { + if x != nil { + return x.BConnId + } + return nil +} + +func (x *RelayNoiseMsg2Pb) GetAConnIdEcho() *common.UUID { + if x != nil { + return x.AConnIdEcho + } + return nil +} + +func (x *RelayNoiseMsg2Pb) GetServerEncryptionAlgorithm() string { + if x != nil { + return x.ServerEncryptionAlgorithm + } + return "" +} + +type PeerConnNoiseMsg3Pb struct { + state protoimpl.MessageState `protogen:"open.v1"` + AConnIdEcho *common.UUID `protobuf:"bytes,1,opt,name=a_conn_id_echo,json=aConnIdEcho,proto3" json:"a_conn_id_echo,omitempty"` + BConnIdEcho *common.UUID `protobuf:"bytes,2,opt,name=b_conn_id_echo,json=bConnIdEcho,proto3" json:"b_conn_id_echo,omitempty"` + SecretProof_32 []byte `protobuf:"bytes,3,opt,name=secret_proof_32,json=secretProof32,proto3,oneof" json:"secret_proof_32,omitempty"` + SecretDigest []byte `protobuf:"bytes,4,opt,name=secret_digest,json=secretDigest,proto3" json:"secret_digest,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerConnNoiseMsg3Pb) Reset() { + *x = PeerConnNoiseMsg3Pb{} + mi := &file_peer_rpc_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerConnNoiseMsg3Pb) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerConnNoiseMsg3Pb) ProtoMessage() {} + +func (x *PeerConnNoiseMsg3Pb) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerConnNoiseMsg3Pb.ProtoReflect.Descriptor instead. +func (*PeerConnNoiseMsg3Pb) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{45} +} + +func (x *PeerConnNoiseMsg3Pb) GetAConnIdEcho() *common.UUID { + if x != nil { + return x.AConnIdEcho + } + return nil +} + +func (x *PeerConnNoiseMsg3Pb) GetBConnIdEcho() *common.UUID { + if x != nil { + return x.BConnIdEcho + } + return nil +} + +func (x *PeerConnNoiseMsg3Pb) GetSecretProof_32() []byte { + if x != nil { + return x.SecretProof_32 + } + return nil +} + +func (x *PeerConnNoiseMsg3Pb) GetSecretDigest() []byte { + if x != nil { + return x.SecretDigest + } + return nil +} + +type RouteConnPeerList_PeerConnInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId *PeerIdVersion `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + ConnectedPeerIds []uint32 `protobuf:"varint,2,rep,packed,name=connected_peer_ids,json=connectedPeerIds,proto3" json:"connected_peer_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RouteConnPeerList_PeerConnInfo) Reset() { + *x = RouteConnPeerList_PeerConnInfo{} + mi := &file_peer_rpc_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RouteConnPeerList_PeerConnInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RouteConnPeerList_PeerConnInfo) ProtoMessage() {} + +func (x *RouteConnPeerList_PeerConnInfo) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RouteConnPeerList_PeerConnInfo.ProtoReflect.Descriptor instead. +func (*RouteConnPeerList_PeerConnInfo) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{5, 0} +} + +func (x *RouteConnPeerList_PeerConnInfo) GetPeerId() *PeerIdVersion { + if x != nil { + return x.PeerId + } + return nil +} + +func (x *RouteConnPeerList_PeerConnInfo) GetConnectedPeerIds() []uint32 { + if x != nil { + return x.ConnectedPeerIds + } + return nil +} + +type RouteForeignNetworkInfos_Info struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key *ForeignNetworkRouteInfoKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value *ForeignNetworkRouteInfoEntry `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RouteForeignNetworkInfos_Info) Reset() { + *x = RouteForeignNetworkInfos_Info{} + mi := &file_peer_rpc_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RouteForeignNetworkInfos_Info) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RouteForeignNetworkInfos_Info) ProtoMessage() {} + +func (x *RouteForeignNetworkInfos_Info) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RouteForeignNetworkInfos_Info.ProtoReflect.Descriptor instead. +func (*RouteForeignNetworkInfos_Info) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{9, 0} +} + +func (x *RouteForeignNetworkInfos_Info) GetKey() *ForeignNetworkRouteInfoKey { + if x != nil { + return x.Key + } + return nil +} + +func (x *RouteForeignNetworkInfos_Info) GetValue() *ForeignNetworkRouteInfoEntry { + if x != nil { + return x.Value + } + return nil +} + +type RouteForeignNetworkSummary_Info struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + NetworkCount uint32 `protobuf:"varint,2,opt,name=network_count,json=networkCount,proto3" json:"network_count,omitempty"` + PeerCount uint32 `protobuf:"varint,3,opt,name=peer_count,json=peerCount,proto3" json:"peer_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RouteForeignNetworkSummary_Info) Reset() { + *x = RouteForeignNetworkSummary_Info{} + mi := &file_peer_rpc_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RouteForeignNetworkSummary_Info) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RouteForeignNetworkSummary_Info) ProtoMessage() {} + +func (x *RouteForeignNetworkSummary_Info) ProtoReflect() protoreflect.Message { + mi := &file_peer_rpc_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RouteForeignNetworkSummary_Info.ProtoReflect.Descriptor instead. +func (*RouteForeignNetworkSummary_Info) Descriptor() ([]byte, []int) { + return file_peer_rpc_proto_rawDescGZIP(), []int{10, 0} +} + +func (x *RouteForeignNetworkSummary_Info) GetPeerId() uint32 { + if x != nil { + return x.PeerId + } + return 0 +} + +func (x *RouteForeignNetworkSummary_Info) GetNetworkCount() uint32 { + if x != nil { + return x.NetworkCount + } + return 0 +} + +func (x *RouteForeignNetworkSummary_Info) GetPeerCount() uint32 { + if x != nil { + return x.PeerCount + } + return 0 +} + +var File_peer_rpc_proto protoreflect.FileDescriptor + +const file_peer_rpc_proto_rawDesc = "" + + "\n" + + "\x0epeer_rpc.proto\x12\bpeer_rpc\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\fcommon.proto\"\xe9\x01\n" + + "\x17TrustedCredentialPubkey\x12\x16\n" + + "\x06pubkey\x18\x01 \x01(\fR\x06pubkey\x12\x16\n" + + "\x06groups\x18\x02 \x03(\tR\x06groups\x12\x1f\n" + + "\vallow_relay\x18\x03 \x01(\bR\n" + + "allowRelay\x12\x1f\n" + + "\vexpiry_unix\x18\x04 \x01(\x03R\n" + + "expiryUnix\x12.\n" + + "\x13allowed_proxy_cidrs\x18\x05 \x03(\tR\x11allowedProxyCidrs\x12\x1f\n" + + "\breusable\x18\x06 \x01(\bH\x00R\breusable\x88\x01\x01B\v\n" + + "\t_reusable\"\x8a\x01\n" + + "\x1cTrustedCredentialPubkeyProof\x12A\n" + + "\n" + + "credential\x18\x01 \x01(\v2!.peer_rpc.TrustedCredentialPubkeyR\n" + + "credential\x12'\n" + + "\x0fcredential_hmac\x18\x02 \x01(\fR\x0ecredentialHmac\"\xf1\b\n" + + "\rRoutePeerInfo\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12%\n" + + "\ainst_id\x18\x02 \x01(\v2\f.common.UUIDR\x06instId\x12\x12\n" + + "\x04cost\x18\x03 \x01(\rR\x04cost\x122\n" + + "\tipv4_addr\x18\x04 \x01(\v2\x10.common.Ipv4AddrH\x00R\bipv4Addr\x88\x01\x01\x12\x1f\n" + + "\vproxy_cidrs\x18\x05 \x03(\tR\n" + + "proxyCidrs\x12\x1f\n" + + "\bhostname\x18\x06 \x01(\tH\x01R\bhostname\x88\x01\x01\x121\n" + + "\fudp_nat_type\x18\a \x01(\x0e2\x0f.common.NatTypeR\n" + + "udpNatType\x12;\n" + + "\vlast_update\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "lastUpdate\x12\x18\n" + + "\aversion\x18\t \x01(\rR\aversion\x12)\n" + + "\x10easytier_version\x18\n" + + " \x01(\tR\x0feasytierVersion\x12:\n" + + "\ffeature_flag\x18\v \x01(\v2\x17.common.PeerFeatureFlagR\vfeatureFlag\x12\"\n" + + "\rpeer_route_id\x18\f \x01(\x04R\vpeerRouteId\x12%\n" + + "\x0enetwork_length\x18\r \x01(\rR\rnetworkLength\x12$\n" + + "\tquic_port\x18\x0e \x01(\rB\x02\x18\x01H\x02R\bquicPort\x88\x01\x01\x122\n" + + "\tipv6_addr\x18\x0f \x01(\v2\x10.common.Ipv6InetH\x03R\bipv6Addr\x88\x01\x01\x12/\n" + + "\x06groups\x18\x10 \x03(\v2\x17.peer_rpc.PeerGroupInfoR\x06groups\x121\n" + + "\ftcp_nat_type\x18\x11 \x01(\x0e2\x0f.common.NatTypeR\n" + + "tcpNatType\x12.\n" + + "\x13noise_static_pubkey\x18\x12 \x01(\fR\x11noiseStaticPubkey\x12d\n" + + "\x1atrusted_credential_pubkeys\x18\x13 \x03(\v2&.peer_rpc.TrustedCredentialPubkeyProofR\x18trustedCredentialPubkeys\x12L\n" + + "\x17ipv6_public_addr_prefix\x18\x16 \x01(\v2\x10.common.Ipv6InetH\x04R\x14ipv6PublicAddrPrefix\x88\x01\x01\x12J\n" + + "\x16ipv6_public_addr_lease\x18\x18 \x01(\v2\x10.common.Ipv6InetH\x05R\x13ipv6PublicAddrLease\x88\x01\x01B\f\n" + + "\n" + + "_ipv4_addrB\v\n" + + "\t_hostnameB\f\n" + + "\n" + + "_quic_portB\f\n" + + "\n" + + "_ipv6_addrB\x1a\n" + + "\x18_ipv6_public_addr_prefixB\x19\n" + + "\x17_ipv6_public_addr_lease\"B\n" + + "\rPeerIdVersion\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12\x18\n" + + "\aversion\x18\x02 \x01(\rR\aversion\"]\n" + + "\x0fRouteConnBitmap\x122\n" + + "\bpeer_ids\x18\x01 \x03(\v2\x17.peer_rpc.PeerIdVersionR\apeerIds\x12\x16\n" + + "\x06bitmap\x18\x02 \x01(\fR\x06bitmap\"\xd5\x01\n" + + "\x11RouteConnPeerList\x12P\n" + + "\x0fpeer_conn_infos\x18\x01 \x03(\v2(.peer_rpc.RouteConnPeerList.PeerConnInfoR\rpeerConnInfos\x1an\n" + + "\fPeerConnInfo\x120\n" + + "\apeer_id\x18\x01 \x01(\v2\x17.peer_rpc.PeerIdVersionR\x06peerId\x12,\n" + + "\x12connected_peer_ids\x18\x02 \x03(\rR\x10connectedPeerIds\"?\n" + + "\x0eRoutePeerInfos\x12-\n" + + "\x05items\x18\x01 \x03(\v2\x17.peer_rpc.RoutePeerInfoR\x05items\"X\n" + + "\x1aForeignNetworkRouteInfoKey\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12!\n" + + "\fnetwork_name\x18\x02 \x01(\tR\vnetworkName\"\x90\x02\n" + + "\x1cForeignNetworkRouteInfoEntry\x12(\n" + + "\x10foreign_peer_ids\x18\x01 \x03(\rR\x0eforeignPeerIds\x12;\n" + + "\vlast_update\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "lastUpdate\x12\x18\n" + + "\aversion\x18\x03 \x01(\rR\aversion\x122\n" + + "\x15network_secret_digest\x18\x04 \x01(\fR\x13networkSecretDigest\x12;\n" + + "\x1bmy_peer_id_for_this_network\x18\x05 \x01(\rR\x16myPeerIdForThisNetwork\"\xd7\x01\n" + + "\x18RouteForeignNetworkInfos\x12=\n" + + "\x05infos\x18\x01 \x03(\v2'.peer_rpc.RouteForeignNetworkInfos.InfoR\x05infos\x1a|\n" + + "\x04Info\x126\n" + + "\x03key\x18\x01 \x01(\v2$.peer_rpc.ForeignNetworkRouteInfoKeyR\x03key\x12<\n" + + "\x05value\x18\x02 \x01(\v2&.peer_rpc.ForeignNetworkRouteInfoEntryR\x05value\"\xb6\x02\n" + + "\x1aRouteForeignNetworkSummary\x12L\n" + + "\binfo_map\x18\x01 \x03(\v21.peer_rpc.RouteForeignNetworkSummary.InfoMapEntryR\ainfoMap\x1ac\n" + + "\x04Info\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12#\n" + + "\rnetwork_count\x18\x02 \x01(\rR\fnetworkCount\x12\x1d\n" + + "\n" + + "peer_count\x18\x03 \x01(\rR\tpeerCount\x1ae\n" + + "\fInfoMapEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\rR\x03key\x12?\n" + + "\x05value\x18\x02 \x01(\v2).peer_rpc.RouteForeignNetworkSummary.InfoR\x05value:\x028\x01\"O\n" + + "\rPeerGroupInfo\x12\x1d\n" + + "\n" + + "group_name\x18\x01 \x01(\tR\tgroupName\x12\x1f\n" + + "\vgroup_proof\x18\x02 \x01(\fR\n" + + "groupProof\"\x9c\x03\n" + + "\x14SyncRouteInfoRequest\x12\x1c\n" + + "\n" + + "my_peer_id\x18\x01 \x01(\rR\bmyPeerId\x12\"\n" + + "\rmy_session_id\x18\x02 \x01(\x04R\vmySessionId\x12!\n" + + "\fis_initiator\x18\x03 \x01(\bR\visInitiator\x127\n" + + "\n" + + "peer_infos\x18\x04 \x01(\v2\x18.peer_rpc.RoutePeerInfosR\tpeerInfos\x12<\n" + + "\vconn_bitmap\x18\x05 \x01(\v2\x19.peer_rpc.RouteConnBitmapH\x00R\n" + + "connBitmap\x12C\n" + + "\x0econn_peer_list\x18\a \x01(\v2\x1b.peer_rpc.RouteConnPeerListH\x00R\fconnPeerList\x12V\n" + + "\x15foreign_network_infos\x18\x06 \x01(\v2\".peer_rpc.RouteForeignNetworkInfosR\x13foreignNetworkInfosB\v\n" + + "\tconn_info\"\x9c\x01\n" + + "\x15SyncRouteInfoResponse\x12!\n" + + "\fis_initiator\x18\x01 \x01(\bR\visInitiator\x12\x1d\n" + + "\n" + + "session_id\x18\x02 \x01(\x04R\tsessionId\x127\n" + + "\x05error\x18\x03 \x01(\x0e2\x1c.peer_rpc.SyncRouteInfoErrorH\x00R\x05error\x88\x01\x01B\b\n" + + "\x06_error\"c\n" + + "!AcquireIpv6PublicAddrLeaseRequest\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12%\n" + + "\ainst_id\x18\x02 \x01(\v2\f.common.UUIDR\x06instId\"\x94\x01\n" + + "\x1fRenewIpv6PublicAddrLeaseRequest\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12%\n" + + "\ainst_id\x18\x02 \x01(\v2\f.common.UUIDR\x06instId\x121\n" + + "\vleased_addr\x18\x03 \x01(\v2\x10.common.Ipv6InetR\n" + + "leasedAddr\"c\n" + + "!ReleaseIpv6PublicAddrLeaseRequest\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12%\n" + + "\ainst_id\x18\x02 \x01(\v2\f.common.UUIDR\x06instId\"_\n" + + "\x1dGetIpv6PublicAddrLeaseRequest\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12%\n" + + "\ainst_id\x18\x02 \x01(\v2\f.common.UUIDR\x06instId\"\xef\x02\n" + + "\x18Ipv6PublicAddrLeaseReply\x12(\n" + + "\x10provider_peer_id\x18\x01 \x01(\rR\x0eproviderPeerId\x126\n" + + "\x10provider_inst_id\x18\x02 \x01(\v2\f.common.UUIDR\x0eproviderInstId\x129\n" + + "\x0fprovider_prefix\x18\x03 \x01(\v2\x10.common.Ipv6InetR\x0eproviderPrefix\x121\n" + + "\vleased_addr\x18\x04 \x01(\v2\x10.common.Ipv6InetR\n" + + "leasedAddr\x12;\n" + + "\vvalid_until\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "validUntil\x12\x16\n" + + "\x06reused\x18\x06 \x01(\bR\x06reused\x12 \n" + + "\terror_msg\x18\a \x01(\tH\x00R\berrorMsg\x88\x01\x01B\f\n" + + "\n" + + "_error_msg\"\x12\n" + + "\x10GetIpListRequest\"\x9a\x02\n" + + "\x11GetIpListResponse\x121\n" + + "\vpublic_ipv4\x18\x01 \x01(\v2\x10.common.Ipv4AddrR\n" + + "publicIpv4\x129\n" + + "\x0finterface_ipv4s\x18\x02 \x03(\v2\x10.common.Ipv4AddrR\x0einterfaceIpv4s\x121\n" + + "\vpublic_ipv6\x18\x03 \x01(\v2\x10.common.Ipv6AddrR\n" + + "publicIpv6\x129\n" + + "\x0finterface_ipv6s\x18\x04 \x03(\v2\x10.common.Ipv6AddrR\x0einterfaceIpv6s\x12)\n" + + "\tlisteners\x18\x05 \x03(\v2\v.common.UrlR\tlisteners\"\xfc\x01\n" + + "\x1dSendUdpHolePunchPacketRequest\x129\n" + + "\x0econnector_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\rconnectorAddr\x12#\n" + + "\rlistener_port\x18\x02 \x01(\rR\flistenerPort\x12>\n" + + "\x12preferred_src_ipv6\x18\x03 \x01(\v2\x10.common.Ipv6AddrR\x10preferredSrcIpv6\x12;\n" + + "\x0fconnector_addrs\x18\x04 \x03(\v2\x12.common.SocketAddrR\x0econnectorAddrs\"i\n" + + "\x1aSelectPunchListenerRequest\x12\x1b\n" + + "\tforce_new\x18\x01 \x01(\bR\bforceNew\x12.\n" + + "\x13prefer_port_mapping\x18\x02 \x01(\bR\x11preferPortMapping\"c\n" + + "\x1bSelectPunchListenerResponse\x12D\n" + + "\x14listener_mapped_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\x12listenerMappedAddr\"\xcb\x02\n" + + "\x1aSendPunchPacketConeRequest\x12D\n" + + "\x14listener_mapped_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\x12listenerMappedAddr\x12/\n" + + "\tdest_addr\x18\x02 \x01(\v2\x12.common.SocketAddrR\bdestAddr\x12%\n" + + "\x0etransaction_id\x18\x03 \x01(\rR\rtransactionId\x123\n" + + "\x16packet_count_per_batch\x18\x04 \x01(\rR\x13packetCountPerBatch\x12,\n" + + "\x12packet_batch_count\x18\x05 \x01(\rR\x10packetBatchCount\x12,\n" + + "\x12packet_interval_ms\x18\x06 \x01(\rR\x10packetIntervalMs\"\xf2\x01\n" + + "\x1dSendPunchPacketHardSymRequest\x12D\n" + + "\x14listener_mapped_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\x12listenerMappedAddr\x12/\n" + + "\n" + + "public_ips\x18\x02 \x03(\v2\x10.common.Ipv4AddrR\tpublicIps\x12%\n" + + "\x0etransaction_id\x18\x03 \x01(\rR\rtransactionId\x12\x1d\n" + + "\n" + + "port_index\x18\x04 \x01(\rR\tportIndex\x12\x14\n" + + "\x05round\x18\x05 \x01(\rR\x05round\"H\n" + + "\x1eSendPunchPacketHardSymResponse\x12&\n" + + "\x0fnext_port_index\x18\x01 \x01(\rR\rnextPortIndex\"\xaa\x02\n" + + "\x1dSendPunchPacketEasySymRequest\x12D\n" + + "\x14listener_mapped_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\x12listenerMappedAddr\x12/\n" + + "\n" + + "public_ips\x18\x02 \x03(\v2\x10.common.Ipv4AddrR\tpublicIps\x12%\n" + + "\x0etransaction_id\x18\x03 \x01(\rR\rtransactionId\x12\"\n" + + "\rbase_port_num\x18\x04 \x01(\rR\vbasePortNum\x12 \n" + + "\fmax_port_num\x18\x05 \x01(\rR\n" + + "maxPortNum\x12%\n" + + "\x0eis_incremental\x18\x06 \x01(\bR\risIncremental\"\xe7\x01\n" + + "!SendPunchPacketBothEasySymRequest\x12(\n" + + "\x10udp_socket_count\x18\x01 \x01(\rR\x0eudpSocketCount\x12-\n" + + "\tpublic_ip\x18\x02 \x01(\v2\x10.common.Ipv4AddrR\bpublicIp\x12%\n" + + "\x0etransaction_id\x18\x03 \x01(\rR\rtransactionId\x12 \n" + + "\fdst_port_num\x18\x04 \x01(\rR\n" + + "dstPortNum\x12 \n" + + "\fwait_time_ms\x18\x05 \x01(\rR\n" + + "waitTimeMs\"{\n" + + "\"SendPunchPacketBothEasySymResponse\x12\x17\n" + + "\ais_busy\x18\x01 \x01(\bR\x06isBusy\x12<\n" + + "\x10base_mapped_addr\x18\x02 \x01(\v2\x12.common.SocketAddrR\x0ebaseMappedAddr\"]\n" + + "\x13TcpHolePunchRequest\x12F\n" + + "\x15connector_mapped_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\x13connectorMappedAddr\"\\\n" + + "\x14TcpHolePunchResponse\x12D\n" + + "\x14listener_mapped_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\x12listenerMappedAddr\"8\n" + + "\x17DirectConnectedPeerInfo\x12\x1d\n" + + "\n" + + "latency_ms\x18\x01 \x01(\x05R\tlatencyMs\"\xcd\x01\n" + + "\x14PeerInfoForGlobalMap\x12R\n" + + "\fdirect_peers\x18\x01 \x03(\v2/.peer_rpc.PeerInfoForGlobalMap.DirectPeersEntryR\vdirectPeers\x1aa\n" + + "\x10DirectPeersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\rR\x03key\x127\n" + + "\x05value\x18\x02 \x01(\v2!.peer_rpc.DirectConnectedPeerInfoR\x05value:\x028\x01\"q\n" + + "\x12ReportPeersRequest\x12\x1c\n" + + "\n" + + "my_peer_id\x18\x01 \x01(\rR\bmyPeerId\x12=\n" + + "\n" + + "peer_infos\x18\x02 \x01(\v2\x1e.peer_rpc.PeerInfoForGlobalMapR\tpeerInfos\"\x15\n" + + "\x13ReportPeersResponse\"\x9b\x01\n" + + "\rGlobalPeerMap\x122\n" + + "\x03map\x18\x01 \x03(\v2 .peer_rpc.GlobalPeerMap.MapEntryR\x03map\x1aV\n" + + "\bMapEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\rR\x03key\x124\n" + + "\x05value\x18\x02 \x01(\v2\x1e.peer_rpc.PeerInfoForGlobalMapR\x05value:\x028\x01\"1\n" + + "\x17GetGlobalPeerMapRequest\x12\x16\n" + + "\x06digest\x18\x01 \x01(\x04R\x06digest\"\x83\x02\n" + + "\x18GetGlobalPeerMapResponse\x12]\n" + + "\x0fglobal_peer_map\x18\x01 \x03(\v25.peer_rpc.GetGlobalPeerMapResponse.GlobalPeerMapEntryR\rglobalPeerMap\x12\x1b\n" + + "\x06digest\x18\x02 \x01(\x04H\x00R\x06digest\x88\x01\x01\x1a`\n" + + "\x12GlobalPeerMapEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\rR\x03key\x124\n" + + "\x05value\x18\x02 \x01(\v2\x1e.peer_rpc.PeerInfoForGlobalMapR\x05value:\x028\x01B\t\n" + + "\a_digest\"\xd3\x01\n" + + "\x10HandshakeRequest\x12\x14\n" + + "\x05magic\x18\x01 \x01(\rR\x05magic\x12\x1c\n" + + "\n" + + "my_peer_id\x18\x02 \x01(\rR\bmyPeerId\x12\x18\n" + + "\aversion\x18\x03 \x01(\rR\aversion\x12\x1a\n" + + "\bfeatures\x18\x04 \x03(\tR\bfeatures\x12!\n" + + "\fnetwork_name\x18\x05 \x01(\tR\vnetworkName\x122\n" + + "\x15network_secret_digest\x18\x06 \x01(\fR\x13networkSecretDigest\"Y\n" + + "\vKcpConnData\x12$\n" + + "\x03src\x18\x01 \x01(\v2\x12.common.SocketAddrR\x03src\x12$\n" + + "\x03dst\x18\x04 \x01(\v2\x12.common.SocketAddrR\x03dst\"\x8f\x02\n" + + "\x13PeerConnNoiseMsg1Pb\x12\x18\n" + + "\aversion\x18\x01 \x01(\rR\aversion\x12$\n" + + "\x0ea_network_name\x18\x02 \x01(\tR\faNetworkName\x125\n" + + "\x14a_session_generation\x18\x03 \x01(\rH\x00R\x12aSessionGeneration\x88\x01\x01\x12(\n" + + "\ta_conn_id\x18\x04 \x01(\v2\f.common.UUIDR\aaConnId\x12>\n" + + "\x1bclient_encryption_algorithm\x18\x05 \x01(\tR\x19clientEncryptionAlgorithmB\x17\n" + + "\x15_a_session_generation\"\xfd\x03\n" + + "\x13PeerConnNoiseMsg2Pb\x12$\n" + + "\x0eb_network_name\x18\x01 \x01(\tR\fbNetworkName\x12\x1b\n" + + "\trole_hint\x18\x02 \x01(\rR\broleHint\x129\n" + + "\x06action\x18\x03 \x01(\x0e2!.peer_rpc.PeerConnSessionActionPbR\x06action\x120\n" + + "\x14b_session_generation\x18\x04 \x01(\rR\x12bSessionGeneration\x12#\n" + + "\vroot_key_32\x18\x05 \x01(\fH\x00R\trootKey32\x88\x01\x01\x12#\n" + + "\rinitial_epoch\x18\x06 \x01(\rR\finitialEpoch\x12(\n" + + "\tb_conn_id\x18\a \x01(\v2\f.common.UUIDR\abConnId\x121\n" + + "\x0ea_conn_id_echo\x18\b \x01(\v2\f.common.UUIDR\vaConnIdEcho\x12+\n" + + "\x0fsecret_proof_32\x18\t \x01(\fH\x01R\rsecretProof32\x88\x01\x01\x12>\n" + + "\x1bserver_encryption_algorithm\x18\n" + + " \x01(\tR\x19serverEncryptionAlgorithmB\x0e\n" + + "\f_root_key_32B\x12\n" + + "\x10_secret_proof_32\"\xe6\x01\n" + + "\x10RelayNoiseMsg1Pb\x12\x18\n" + + "\aversion\x18\x01 \x01(\rR\aversion\x125\n" + + "\x14a_session_generation\x18\x03 \x01(\rH\x00R\x12aSessionGeneration\x88\x01\x01\x12(\n" + + "\ta_conn_id\x18\x04 \x01(\v2\f.common.UUIDR\aaConnId\x12>\n" + + "\x1bclient_encryption_algorithm\x18\x05 \x01(\tR\x19clientEncryptionAlgorithmB\x17\n" + + "\x15_a_session_generation\"\xf6\x02\n" + + "\x10RelayNoiseMsg2Pb\x129\n" + + "\x06action\x18\x03 \x01(\x0e2!.peer_rpc.PeerConnSessionActionPbR\x06action\x120\n" + + "\x14b_session_generation\x18\x04 \x01(\rR\x12bSessionGeneration\x12#\n" + + "\vroot_key_32\x18\x05 \x01(\fH\x00R\trootKey32\x88\x01\x01\x12#\n" + + "\rinitial_epoch\x18\x06 \x01(\rR\finitialEpoch\x12(\n" + + "\tb_conn_id\x18\a \x01(\v2\f.common.UUIDR\abConnId\x121\n" + + "\x0ea_conn_id_echo\x18\b \x01(\v2\f.common.UUIDR\vaConnIdEcho\x12>\n" + + "\x1bserver_encryption_algorithm\x18\n" + + " \x01(\tR\x19serverEncryptionAlgorithmB\x0e\n" + + "\f_root_key_32\"\xe1\x01\n" + + "\x13PeerConnNoiseMsg3Pb\x121\n" + + "\x0ea_conn_id_echo\x18\x01 \x01(\v2\f.common.UUIDR\vaConnIdEcho\x121\n" + + "\x0eb_conn_id_echo\x18\x02 \x01(\v2\f.common.UUIDR\vbConnIdEcho\x12+\n" + + "\x0fsecret_proof_32\x18\x03 \x01(\fH\x00R\rsecretProof32\x88\x01\x01\x12#\n" + + "\rsecret_digest\x18\x04 \x01(\fR\fsecretDigestB\x12\n" + + "\x10_secret_proof_32*6\n" + + "\x12SyncRouteInfoError\x12\x13\n" + + "\x0fDuplicatePeerId\x10\x00\x12\v\n" + + "\aStopped\x10\x01*g\n" + + "\x0fSecureAuthLevel\x12\b\n" + + "\x04None\x10\x00\x12\x1c\n" + + "\x18EncryptedUnauthenticated\x10\x01\x12\x10\n" + + "\fPeerVerified\x10\x02\x12\x1a\n" + + "\x16NetworkSecretConfirmed\x10\x03*=\n" + + "\x10PeerIdentityType\x12\t\n" + + "\x05Admin\x10\x00\x12\x0e\n" + + "\n" + + "Credential\x10\x01\x12\x0e\n" + + "\n" + + "SharedNode\x10\x02*9\n" + + "\x17PeerConnSessionActionPb\x12\b\n" + + "\x04Join\x10\x00\x12\b\n" + + "\x04Sync\x10\x01\x12\n" + + "\n" + + "\x06Create\x10\x022`\n" + + "\fOspfRouteRpc\x12P\n" + + "\rSyncRouteInfo\x12\x1e.peer_rpc.SyncRouteInfoRequest\x1a\x1f.peer_rpc.SyncRouteInfoResponse2\xf5\x02\n" + + "\x11PublicIpv6AddrRpc\x12_\n" + + "\fAcquireLease\x12+.peer_rpc.AcquireIpv6PublicAddrLeaseRequest\x1a\".peer_rpc.Ipv6PublicAddrLeaseReply\x12[\n" + + "\n" + + "RenewLease\x12).peer_rpc.RenewIpv6PublicAddrLeaseRequest\x1a\".peer_rpc.Ipv6PublicAddrLeaseReply\x12I\n" + + "\fReleaseLease\x12+.peer_rpc.ReleaseIpv6PublicAddrLeaseRequest\x1a\f.common.Void\x12W\n" + + "\bGetLease\x12'.peer_rpc.GetIpv6PublicAddrLeaseRequest\x1a\".peer_rpc.Ipv6PublicAddrLeaseReply2\xab\x01\n" + + "\x12DirectConnectorRpc\x12D\n" + + "\tGetIpList\x12\x1a.peer_rpc.GetIpListRequest\x1a\x1b.peer_rpc.GetIpListResponse\x12O\n" + + "\x16SendUdpHolePunchPacket\x12'.peer_rpc.SendUdpHolePunchPacketRequest\x1a\f.common.Void2\xf7\x03\n" + + "\x0fUdpHolePunchRpc\x12b\n" + + "\x13SelectPunchListener\x12$.peer_rpc.SelectPunchListenerRequest\x1a%.peer_rpc.SelectPunchListenerResponse\x12I\n" + + "\x13SendPunchPacketCone\x12$.peer_rpc.SendPunchPacketConeRequest\x1a\f.common.Void\x12k\n" + + "\x16SendPunchPacketHardSym\x12'.peer_rpc.SendPunchPacketHardSymRequest\x1a(.peer_rpc.SendPunchPacketHardSymResponse\x12O\n" + + "\x16SendPunchPacketEasySym\x12'.peer_rpc.SendPunchPacketEasySymRequest\x1a\f.common.Void\x12w\n" + + "\x1aSendPunchPacketBothEasySym\x12+.peer_rpc.SendPunchPacketBothEasySymRequest\x1a,.peer_rpc.SendPunchPacketBothEasySymResponse2f\n" + + "\x0fTcpHolePunchRpc\x12S\n" + + "\x12ExchangeMappedAddr\x12\x1d.peer_rpc.TcpHolePunchRequest\x1a\x1e.peer_rpc.TcpHolePunchResponse2\xb6\x01\n" + + "\rPeerCenterRpc\x12J\n" + + "\vReportPeers\x12\x1c.peer_rpc.ReportPeersRequest\x1a\x1d.peer_rpc.ReportPeersResponse\x12Y\n" + + "\x10GetGlobalPeerMap\x12!.peer_rpc.GetGlobalPeerMapRequest\x1a\".peer_rpc.GetGlobalPeerMapResponseb\x06proto3" + +var ( + file_peer_rpc_proto_rawDescOnce sync.Once + file_peer_rpc_proto_rawDescData []byte +) + +func file_peer_rpc_proto_rawDescGZIP() []byte { + file_peer_rpc_proto_rawDescOnce.Do(func() { + file_peer_rpc_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_peer_rpc_proto_rawDesc), len(file_peer_rpc_proto_rawDesc))) + }) + return file_peer_rpc_proto_rawDescData +} + +var file_peer_rpc_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_peer_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 53) +var file_peer_rpc_proto_goTypes = []any{ + (SyncRouteInfoError)(0), // 0: peer_rpc.SyncRouteInfoError + (SecureAuthLevel)(0), // 1: peer_rpc.SecureAuthLevel + (PeerIdentityType)(0), // 2: peer_rpc.PeerIdentityType + (PeerConnSessionActionPb)(0), // 3: peer_rpc.PeerConnSessionActionPb + (*TrustedCredentialPubkey)(nil), // 4: peer_rpc.TrustedCredentialPubkey + (*TrustedCredentialPubkeyProof)(nil), // 5: peer_rpc.TrustedCredentialPubkeyProof + (*RoutePeerInfo)(nil), // 6: peer_rpc.RoutePeerInfo + (*PeerIdVersion)(nil), // 7: peer_rpc.PeerIdVersion + (*RouteConnBitmap)(nil), // 8: peer_rpc.RouteConnBitmap + (*RouteConnPeerList)(nil), // 9: peer_rpc.RouteConnPeerList + (*RoutePeerInfos)(nil), // 10: peer_rpc.RoutePeerInfos + (*ForeignNetworkRouteInfoKey)(nil), // 11: peer_rpc.ForeignNetworkRouteInfoKey + (*ForeignNetworkRouteInfoEntry)(nil), // 12: peer_rpc.ForeignNetworkRouteInfoEntry + (*RouteForeignNetworkInfos)(nil), // 13: peer_rpc.RouteForeignNetworkInfos + (*RouteForeignNetworkSummary)(nil), // 14: peer_rpc.RouteForeignNetworkSummary + (*PeerGroupInfo)(nil), // 15: peer_rpc.PeerGroupInfo + (*SyncRouteInfoRequest)(nil), // 16: peer_rpc.SyncRouteInfoRequest + (*SyncRouteInfoResponse)(nil), // 17: peer_rpc.SyncRouteInfoResponse + (*AcquireIpv6PublicAddrLeaseRequest)(nil), // 18: peer_rpc.AcquireIpv6PublicAddrLeaseRequest + (*RenewIpv6PublicAddrLeaseRequest)(nil), // 19: peer_rpc.RenewIpv6PublicAddrLeaseRequest + (*ReleaseIpv6PublicAddrLeaseRequest)(nil), // 20: peer_rpc.ReleaseIpv6PublicAddrLeaseRequest + (*GetIpv6PublicAddrLeaseRequest)(nil), // 21: peer_rpc.GetIpv6PublicAddrLeaseRequest + (*Ipv6PublicAddrLeaseReply)(nil), // 22: peer_rpc.Ipv6PublicAddrLeaseReply + (*GetIpListRequest)(nil), // 23: peer_rpc.GetIpListRequest + (*GetIpListResponse)(nil), // 24: peer_rpc.GetIpListResponse + (*SendUdpHolePunchPacketRequest)(nil), // 25: peer_rpc.SendUdpHolePunchPacketRequest + (*SelectPunchListenerRequest)(nil), // 26: peer_rpc.SelectPunchListenerRequest + (*SelectPunchListenerResponse)(nil), // 27: peer_rpc.SelectPunchListenerResponse + (*SendPunchPacketConeRequest)(nil), // 28: peer_rpc.SendPunchPacketConeRequest + (*SendPunchPacketHardSymRequest)(nil), // 29: peer_rpc.SendPunchPacketHardSymRequest + (*SendPunchPacketHardSymResponse)(nil), // 30: peer_rpc.SendPunchPacketHardSymResponse + (*SendPunchPacketEasySymRequest)(nil), // 31: peer_rpc.SendPunchPacketEasySymRequest + (*SendPunchPacketBothEasySymRequest)(nil), // 32: peer_rpc.SendPunchPacketBothEasySymRequest + (*SendPunchPacketBothEasySymResponse)(nil), // 33: peer_rpc.SendPunchPacketBothEasySymResponse + (*TcpHolePunchRequest)(nil), // 34: peer_rpc.TcpHolePunchRequest + (*TcpHolePunchResponse)(nil), // 35: peer_rpc.TcpHolePunchResponse + (*DirectConnectedPeerInfo)(nil), // 36: peer_rpc.DirectConnectedPeerInfo + (*PeerInfoForGlobalMap)(nil), // 37: peer_rpc.PeerInfoForGlobalMap + (*ReportPeersRequest)(nil), // 38: peer_rpc.ReportPeersRequest + (*ReportPeersResponse)(nil), // 39: peer_rpc.ReportPeersResponse + (*GlobalPeerMap)(nil), // 40: peer_rpc.GlobalPeerMap + (*GetGlobalPeerMapRequest)(nil), // 41: peer_rpc.GetGlobalPeerMapRequest + (*GetGlobalPeerMapResponse)(nil), // 42: peer_rpc.GetGlobalPeerMapResponse + (*HandshakeRequest)(nil), // 43: peer_rpc.HandshakeRequest + (*KcpConnData)(nil), // 44: peer_rpc.KcpConnData + (*PeerConnNoiseMsg1Pb)(nil), // 45: peer_rpc.PeerConnNoiseMsg1Pb + (*PeerConnNoiseMsg2Pb)(nil), // 46: peer_rpc.PeerConnNoiseMsg2Pb + (*RelayNoiseMsg1Pb)(nil), // 47: peer_rpc.RelayNoiseMsg1Pb + (*RelayNoiseMsg2Pb)(nil), // 48: peer_rpc.RelayNoiseMsg2Pb + (*PeerConnNoiseMsg3Pb)(nil), // 49: peer_rpc.PeerConnNoiseMsg3Pb + (*RouteConnPeerList_PeerConnInfo)(nil), // 50: peer_rpc.RouteConnPeerList.PeerConnInfo + (*RouteForeignNetworkInfos_Info)(nil), // 51: peer_rpc.RouteForeignNetworkInfos.Info + (*RouteForeignNetworkSummary_Info)(nil), // 52: peer_rpc.RouteForeignNetworkSummary.Info + nil, // 53: peer_rpc.RouteForeignNetworkSummary.InfoMapEntry + nil, // 54: peer_rpc.PeerInfoForGlobalMap.DirectPeersEntry + nil, // 55: peer_rpc.GlobalPeerMap.MapEntry + nil, // 56: peer_rpc.GetGlobalPeerMapResponse.GlobalPeerMapEntry + (*common.UUID)(nil), // 57: common.UUID + (*common.Ipv4Addr)(nil), // 58: common.Ipv4Addr + (common.NatType)(0), // 59: common.NatType + (*timestamppb.Timestamp)(nil), // 60: google.protobuf.Timestamp + (*common.PeerFeatureFlag)(nil), // 61: common.PeerFeatureFlag + (*common.Ipv6Inet)(nil), // 62: common.Ipv6Inet + (*common.Ipv6Addr)(nil), // 63: common.Ipv6Addr + (*common.Url)(nil), // 64: common.Url + (*common.SocketAddr)(nil), // 65: common.SocketAddr + (*common.Void)(nil), // 66: common.Void +} +var file_peer_rpc_proto_depIdxs = []int32{ + 4, // 0: peer_rpc.TrustedCredentialPubkeyProof.credential:type_name -> peer_rpc.TrustedCredentialPubkey + 57, // 1: peer_rpc.RoutePeerInfo.inst_id:type_name -> common.UUID + 58, // 2: peer_rpc.RoutePeerInfo.ipv4_addr:type_name -> common.Ipv4Addr + 59, // 3: peer_rpc.RoutePeerInfo.udp_nat_type:type_name -> common.NatType + 60, // 4: peer_rpc.RoutePeerInfo.last_update:type_name -> google.protobuf.Timestamp + 61, // 5: peer_rpc.RoutePeerInfo.feature_flag:type_name -> common.PeerFeatureFlag + 62, // 6: peer_rpc.RoutePeerInfo.ipv6_addr:type_name -> common.Ipv6Inet + 15, // 7: peer_rpc.RoutePeerInfo.groups:type_name -> peer_rpc.PeerGroupInfo + 59, // 8: peer_rpc.RoutePeerInfo.tcp_nat_type:type_name -> common.NatType + 5, // 9: peer_rpc.RoutePeerInfo.trusted_credential_pubkeys:type_name -> peer_rpc.TrustedCredentialPubkeyProof + 62, // 10: peer_rpc.RoutePeerInfo.ipv6_public_addr_prefix:type_name -> common.Ipv6Inet + 62, // 11: peer_rpc.RoutePeerInfo.ipv6_public_addr_lease:type_name -> common.Ipv6Inet + 7, // 12: peer_rpc.RouteConnBitmap.peer_ids:type_name -> peer_rpc.PeerIdVersion + 50, // 13: peer_rpc.RouteConnPeerList.peer_conn_infos:type_name -> peer_rpc.RouteConnPeerList.PeerConnInfo + 6, // 14: peer_rpc.RoutePeerInfos.items:type_name -> peer_rpc.RoutePeerInfo + 60, // 15: peer_rpc.ForeignNetworkRouteInfoEntry.last_update:type_name -> google.protobuf.Timestamp + 51, // 16: peer_rpc.RouteForeignNetworkInfos.infos:type_name -> peer_rpc.RouteForeignNetworkInfos.Info + 53, // 17: peer_rpc.RouteForeignNetworkSummary.info_map:type_name -> peer_rpc.RouteForeignNetworkSummary.InfoMapEntry + 10, // 18: peer_rpc.SyncRouteInfoRequest.peer_infos:type_name -> peer_rpc.RoutePeerInfos + 8, // 19: peer_rpc.SyncRouteInfoRequest.conn_bitmap:type_name -> peer_rpc.RouteConnBitmap + 9, // 20: peer_rpc.SyncRouteInfoRequest.conn_peer_list:type_name -> peer_rpc.RouteConnPeerList + 13, // 21: peer_rpc.SyncRouteInfoRequest.foreign_network_infos:type_name -> peer_rpc.RouteForeignNetworkInfos + 0, // 22: peer_rpc.SyncRouteInfoResponse.error:type_name -> peer_rpc.SyncRouteInfoError + 57, // 23: peer_rpc.AcquireIpv6PublicAddrLeaseRequest.inst_id:type_name -> common.UUID + 57, // 24: peer_rpc.RenewIpv6PublicAddrLeaseRequest.inst_id:type_name -> common.UUID + 62, // 25: peer_rpc.RenewIpv6PublicAddrLeaseRequest.leased_addr:type_name -> common.Ipv6Inet + 57, // 26: peer_rpc.ReleaseIpv6PublicAddrLeaseRequest.inst_id:type_name -> common.UUID + 57, // 27: peer_rpc.GetIpv6PublicAddrLeaseRequest.inst_id:type_name -> common.UUID + 57, // 28: peer_rpc.Ipv6PublicAddrLeaseReply.provider_inst_id:type_name -> common.UUID + 62, // 29: peer_rpc.Ipv6PublicAddrLeaseReply.provider_prefix:type_name -> common.Ipv6Inet + 62, // 30: peer_rpc.Ipv6PublicAddrLeaseReply.leased_addr:type_name -> common.Ipv6Inet + 60, // 31: peer_rpc.Ipv6PublicAddrLeaseReply.valid_until:type_name -> google.protobuf.Timestamp + 58, // 32: peer_rpc.GetIpListResponse.public_ipv4:type_name -> common.Ipv4Addr + 58, // 33: peer_rpc.GetIpListResponse.interface_ipv4s:type_name -> common.Ipv4Addr + 63, // 34: peer_rpc.GetIpListResponse.public_ipv6:type_name -> common.Ipv6Addr + 63, // 35: peer_rpc.GetIpListResponse.interface_ipv6s:type_name -> common.Ipv6Addr + 64, // 36: peer_rpc.GetIpListResponse.listeners:type_name -> common.Url + 65, // 37: peer_rpc.SendUdpHolePunchPacketRequest.connector_addr:type_name -> common.SocketAddr + 63, // 38: peer_rpc.SendUdpHolePunchPacketRequest.preferred_src_ipv6:type_name -> common.Ipv6Addr + 65, // 39: peer_rpc.SendUdpHolePunchPacketRequest.connector_addrs:type_name -> common.SocketAddr + 65, // 40: peer_rpc.SelectPunchListenerResponse.listener_mapped_addr:type_name -> common.SocketAddr + 65, // 41: peer_rpc.SendPunchPacketConeRequest.listener_mapped_addr:type_name -> common.SocketAddr + 65, // 42: peer_rpc.SendPunchPacketConeRequest.dest_addr:type_name -> common.SocketAddr + 65, // 43: peer_rpc.SendPunchPacketHardSymRequest.listener_mapped_addr:type_name -> common.SocketAddr + 58, // 44: peer_rpc.SendPunchPacketHardSymRequest.public_ips:type_name -> common.Ipv4Addr + 65, // 45: peer_rpc.SendPunchPacketEasySymRequest.listener_mapped_addr:type_name -> common.SocketAddr + 58, // 46: peer_rpc.SendPunchPacketEasySymRequest.public_ips:type_name -> common.Ipv4Addr + 58, // 47: peer_rpc.SendPunchPacketBothEasySymRequest.public_ip:type_name -> common.Ipv4Addr + 65, // 48: peer_rpc.SendPunchPacketBothEasySymResponse.base_mapped_addr:type_name -> common.SocketAddr + 65, // 49: peer_rpc.TcpHolePunchRequest.connector_mapped_addr:type_name -> common.SocketAddr + 65, // 50: peer_rpc.TcpHolePunchResponse.listener_mapped_addr:type_name -> common.SocketAddr + 54, // 51: peer_rpc.PeerInfoForGlobalMap.direct_peers:type_name -> peer_rpc.PeerInfoForGlobalMap.DirectPeersEntry + 37, // 52: peer_rpc.ReportPeersRequest.peer_infos:type_name -> peer_rpc.PeerInfoForGlobalMap + 55, // 53: peer_rpc.GlobalPeerMap.map:type_name -> peer_rpc.GlobalPeerMap.MapEntry + 56, // 54: peer_rpc.GetGlobalPeerMapResponse.global_peer_map:type_name -> peer_rpc.GetGlobalPeerMapResponse.GlobalPeerMapEntry + 65, // 55: peer_rpc.KcpConnData.src:type_name -> common.SocketAddr + 65, // 56: peer_rpc.KcpConnData.dst:type_name -> common.SocketAddr + 57, // 57: peer_rpc.PeerConnNoiseMsg1Pb.a_conn_id:type_name -> common.UUID + 3, // 58: peer_rpc.PeerConnNoiseMsg2Pb.action:type_name -> peer_rpc.PeerConnSessionActionPb + 57, // 59: peer_rpc.PeerConnNoiseMsg2Pb.b_conn_id:type_name -> common.UUID + 57, // 60: peer_rpc.PeerConnNoiseMsg2Pb.a_conn_id_echo:type_name -> common.UUID + 57, // 61: peer_rpc.RelayNoiseMsg1Pb.a_conn_id:type_name -> common.UUID + 3, // 62: peer_rpc.RelayNoiseMsg2Pb.action:type_name -> peer_rpc.PeerConnSessionActionPb + 57, // 63: peer_rpc.RelayNoiseMsg2Pb.b_conn_id:type_name -> common.UUID + 57, // 64: peer_rpc.RelayNoiseMsg2Pb.a_conn_id_echo:type_name -> common.UUID + 57, // 65: peer_rpc.PeerConnNoiseMsg3Pb.a_conn_id_echo:type_name -> common.UUID + 57, // 66: peer_rpc.PeerConnNoiseMsg3Pb.b_conn_id_echo:type_name -> common.UUID + 7, // 67: peer_rpc.RouteConnPeerList.PeerConnInfo.peer_id:type_name -> peer_rpc.PeerIdVersion + 11, // 68: peer_rpc.RouteForeignNetworkInfos.Info.key:type_name -> peer_rpc.ForeignNetworkRouteInfoKey + 12, // 69: peer_rpc.RouteForeignNetworkInfos.Info.value:type_name -> peer_rpc.ForeignNetworkRouteInfoEntry + 52, // 70: peer_rpc.RouteForeignNetworkSummary.InfoMapEntry.value:type_name -> peer_rpc.RouteForeignNetworkSummary.Info + 36, // 71: peer_rpc.PeerInfoForGlobalMap.DirectPeersEntry.value:type_name -> peer_rpc.DirectConnectedPeerInfo + 37, // 72: peer_rpc.GlobalPeerMap.MapEntry.value:type_name -> peer_rpc.PeerInfoForGlobalMap + 37, // 73: peer_rpc.GetGlobalPeerMapResponse.GlobalPeerMapEntry.value:type_name -> peer_rpc.PeerInfoForGlobalMap + 16, // 74: peer_rpc.OspfRouteRpc.SyncRouteInfo:input_type -> peer_rpc.SyncRouteInfoRequest + 18, // 75: peer_rpc.PublicIpv6AddrRpc.AcquireLease:input_type -> peer_rpc.AcquireIpv6PublicAddrLeaseRequest + 19, // 76: peer_rpc.PublicIpv6AddrRpc.RenewLease:input_type -> peer_rpc.RenewIpv6PublicAddrLeaseRequest + 20, // 77: peer_rpc.PublicIpv6AddrRpc.ReleaseLease:input_type -> peer_rpc.ReleaseIpv6PublicAddrLeaseRequest + 21, // 78: peer_rpc.PublicIpv6AddrRpc.GetLease:input_type -> peer_rpc.GetIpv6PublicAddrLeaseRequest + 23, // 79: peer_rpc.DirectConnectorRpc.GetIpList:input_type -> peer_rpc.GetIpListRequest + 25, // 80: peer_rpc.DirectConnectorRpc.SendUdpHolePunchPacket:input_type -> peer_rpc.SendUdpHolePunchPacketRequest + 26, // 81: peer_rpc.UdpHolePunchRpc.SelectPunchListener:input_type -> peer_rpc.SelectPunchListenerRequest + 28, // 82: peer_rpc.UdpHolePunchRpc.SendPunchPacketCone:input_type -> peer_rpc.SendPunchPacketConeRequest + 29, // 83: peer_rpc.UdpHolePunchRpc.SendPunchPacketHardSym:input_type -> peer_rpc.SendPunchPacketHardSymRequest + 31, // 84: peer_rpc.UdpHolePunchRpc.SendPunchPacketEasySym:input_type -> peer_rpc.SendPunchPacketEasySymRequest + 32, // 85: peer_rpc.UdpHolePunchRpc.SendPunchPacketBothEasySym:input_type -> peer_rpc.SendPunchPacketBothEasySymRequest + 34, // 86: peer_rpc.TcpHolePunchRpc.ExchangeMappedAddr:input_type -> peer_rpc.TcpHolePunchRequest + 38, // 87: peer_rpc.PeerCenterRpc.ReportPeers:input_type -> peer_rpc.ReportPeersRequest + 41, // 88: peer_rpc.PeerCenterRpc.GetGlobalPeerMap:input_type -> peer_rpc.GetGlobalPeerMapRequest + 17, // 89: peer_rpc.OspfRouteRpc.SyncRouteInfo:output_type -> peer_rpc.SyncRouteInfoResponse + 22, // 90: peer_rpc.PublicIpv6AddrRpc.AcquireLease:output_type -> peer_rpc.Ipv6PublicAddrLeaseReply + 22, // 91: peer_rpc.PublicIpv6AddrRpc.RenewLease:output_type -> peer_rpc.Ipv6PublicAddrLeaseReply + 66, // 92: peer_rpc.PublicIpv6AddrRpc.ReleaseLease:output_type -> common.Void + 22, // 93: peer_rpc.PublicIpv6AddrRpc.GetLease:output_type -> peer_rpc.Ipv6PublicAddrLeaseReply + 24, // 94: peer_rpc.DirectConnectorRpc.GetIpList:output_type -> peer_rpc.GetIpListResponse + 66, // 95: peer_rpc.DirectConnectorRpc.SendUdpHolePunchPacket:output_type -> common.Void + 27, // 96: peer_rpc.UdpHolePunchRpc.SelectPunchListener:output_type -> peer_rpc.SelectPunchListenerResponse + 66, // 97: peer_rpc.UdpHolePunchRpc.SendPunchPacketCone:output_type -> common.Void + 30, // 98: peer_rpc.UdpHolePunchRpc.SendPunchPacketHardSym:output_type -> peer_rpc.SendPunchPacketHardSymResponse + 66, // 99: peer_rpc.UdpHolePunchRpc.SendPunchPacketEasySym:output_type -> common.Void + 33, // 100: peer_rpc.UdpHolePunchRpc.SendPunchPacketBothEasySym:output_type -> peer_rpc.SendPunchPacketBothEasySymResponse + 35, // 101: peer_rpc.TcpHolePunchRpc.ExchangeMappedAddr:output_type -> peer_rpc.TcpHolePunchResponse + 39, // 102: peer_rpc.PeerCenterRpc.ReportPeers:output_type -> peer_rpc.ReportPeersResponse + 42, // 103: peer_rpc.PeerCenterRpc.GetGlobalPeerMap:output_type -> peer_rpc.GetGlobalPeerMapResponse + 89, // [89:104] is the sub-list for method output_type + 74, // [74:89] is the sub-list for method input_type + 74, // [74:74] is the sub-list for extension type_name + 74, // [74:74] is the sub-list for extension extendee + 0, // [0:74] is the sub-list for field type_name +} + +func init() { file_peer_rpc_proto_init() } +func file_peer_rpc_proto_init() { + if File_peer_rpc_proto != nil { + return + } + file_peer_rpc_proto_msgTypes[0].OneofWrappers = []any{} + file_peer_rpc_proto_msgTypes[2].OneofWrappers = []any{} + file_peer_rpc_proto_msgTypes[12].OneofWrappers = []any{ + (*SyncRouteInfoRequest_ConnBitmap)(nil), + (*SyncRouteInfoRequest_ConnPeerList)(nil), + } + file_peer_rpc_proto_msgTypes[13].OneofWrappers = []any{} + file_peer_rpc_proto_msgTypes[18].OneofWrappers = []any{} + file_peer_rpc_proto_msgTypes[38].OneofWrappers = []any{} + file_peer_rpc_proto_msgTypes[41].OneofWrappers = []any{} + file_peer_rpc_proto_msgTypes[42].OneofWrappers = []any{} + file_peer_rpc_proto_msgTypes[43].OneofWrappers = []any{} + file_peer_rpc_proto_msgTypes[44].OneofWrappers = []any{} + file_peer_rpc_proto_msgTypes[45].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_peer_rpc_proto_rawDesc), len(file_peer_rpc_proto_rawDesc)), + NumEnums: 4, + NumMessages: 53, + NumExtensions: 0, + NumServices: 6, + }, + GoTypes: file_peer_rpc_proto_goTypes, + DependencyIndexes: file_peer_rpc_proto_depIdxs, + EnumInfos: file_peer_rpc_proto_enumTypes, + MessageInfos: file_peer_rpc_proto_msgTypes, + }.Build() + File_peer_rpc_proto = out.File + file_peer_rpc_proto_goTypes = nil + file_peer_rpc_proto_depIdxs = nil +} diff --git a/easytier-go/proto/provenance.go b/easytier-go/proto/provenance.go new file mode 100644 index 00000000..8bdf7878 --- /dev/null +++ b/easytier-go/proto/provenance.go @@ -0,0 +1,8 @@ +// Code generated by go generate; DO NOT EDIT. + +package proto + +const ( + EasyTierCommit = "63519db2b5f2a6a1b9b7f20905f036dab54eb829" + SchemaSHA256 = "4fc7f40eac5d3803ed8afe707f2be1db121a022547f91568955c369212875064" +) diff --git a/easytier-go/proto/web/web.pb.go b/easytier-go/proto/web/web.pb.go new file mode 100644 index 00000000..8977ef01 --- /dev/null +++ b/easytier-go/proto/web/web.pb.go @@ -0,0 +1,399 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.35.1 +// source: web.proto + +package web + +import ( + common "github.com/EasyTier/EasyTier/easytier-go/proto/common" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DeviceOsInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + OsType string `protobuf:"bytes,1,opt,name=os_type,json=osType,proto3" json:"os_type,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + Distribution string `protobuf:"bytes,3,opt,name=distribution,proto3" json:"distribution,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeviceOsInfo) Reset() { + *x = DeviceOsInfo{} + mi := &file_web_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeviceOsInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeviceOsInfo) ProtoMessage() {} + +func (x *DeviceOsInfo) ProtoReflect() protoreflect.Message { + mi := &file_web_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeviceOsInfo.ProtoReflect.Descriptor instead. +func (*DeviceOsInfo) Descriptor() ([]byte, []int) { + return file_web_proto_rawDescGZIP(), []int{0} +} + +func (x *DeviceOsInfo) GetOsType() string { + if x != nil { + return x.OsType + } + return "" +} + +func (x *DeviceOsInfo) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *DeviceOsInfo) GetDistribution() string { + if x != nil { + return x.Distribution + } + return "" +} + +type HeartbeatRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + MachineId *common.UUID `protobuf:"bytes,1,opt,name=machine_id,json=machineId,proto3" json:"machine_id,omitempty"` + InstId *common.UUID `protobuf:"bytes,2,opt,name=inst_id,json=instId,proto3" json:"inst_id,omitempty"` + UserToken string `protobuf:"bytes,3,opt,name=user_token,json=userToken,proto3" json:"user_token,omitempty"` + EasytierVersion string `protobuf:"bytes,4,opt,name=easytier_version,json=easytierVersion,proto3" json:"easytier_version,omitempty"` + ReportTime string `protobuf:"bytes,5,opt,name=report_time,json=reportTime,proto3" json:"report_time,omitempty"` + Hostname string `protobuf:"bytes,6,opt,name=hostname,proto3" json:"hostname,omitempty"` + RunningNetworkInstances []*common.UUID `protobuf:"bytes,7,rep,name=running_network_instances,json=runningNetworkInstances,proto3" json:"running_network_instances,omitempty"` + DeviceOs *DeviceOsInfo `protobuf:"bytes,8,opt,name=device_os,json=deviceOs,proto3" json:"device_os,omitempty"` + SupportConfigSource bool `protobuf:"varint,9,opt,name=support_config_source,json=supportConfigSource,proto3" json:"support_config_source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeartbeatRequest) Reset() { + *x = HeartbeatRequest{} + mi := &file_web_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeartbeatRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatRequest) ProtoMessage() {} + +func (x *HeartbeatRequest) ProtoReflect() protoreflect.Message { + mi := &file_web_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatRequest.ProtoReflect.Descriptor instead. +func (*HeartbeatRequest) Descriptor() ([]byte, []int) { + return file_web_proto_rawDescGZIP(), []int{1} +} + +func (x *HeartbeatRequest) GetMachineId() *common.UUID { + if x != nil { + return x.MachineId + } + return nil +} + +func (x *HeartbeatRequest) GetInstId() *common.UUID { + if x != nil { + return x.InstId + } + return nil +} + +func (x *HeartbeatRequest) GetUserToken() string { + if x != nil { + return x.UserToken + } + return "" +} + +func (x *HeartbeatRequest) GetEasytierVersion() string { + if x != nil { + return x.EasytierVersion + } + return "" +} + +func (x *HeartbeatRequest) GetReportTime() string { + if x != nil { + return x.ReportTime + } + return "" +} + +func (x *HeartbeatRequest) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *HeartbeatRequest) GetRunningNetworkInstances() []*common.UUID { + if x != nil { + return x.RunningNetworkInstances + } + return nil +} + +func (x *HeartbeatRequest) GetDeviceOs() *DeviceOsInfo { + if x != nil { + return x.DeviceOs + } + return nil +} + +func (x *HeartbeatRequest) GetSupportConfigSource() bool { + if x != nil { + return x.SupportConfigSource + } + return false +} + +type HeartbeatResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeartbeatResponse) Reset() { + *x = HeartbeatResponse{} + mi := &file_web_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeartbeatResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatResponse) ProtoMessage() {} + +func (x *HeartbeatResponse) ProtoReflect() protoreflect.Message { + mi := &file_web_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatResponse.ProtoReflect.Descriptor instead. +func (*HeartbeatResponse) Descriptor() ([]byte, []int) { + return file_web_proto_rawDescGZIP(), []int{2} +} + +type GetFeatureRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFeatureRequest) Reset() { + *x = GetFeatureRequest{} + mi := &file_web_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFeatureRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFeatureRequest) ProtoMessage() {} + +func (x *GetFeatureRequest) ProtoReflect() protoreflect.Message { + mi := &file_web_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFeatureRequest.ProtoReflect.Descriptor instead. +func (*GetFeatureRequest) Descriptor() ([]byte, []int) { + return file_web_proto_rawDescGZIP(), []int{3} +} + +type GetFeatureResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + SupportEncryption bool `protobuf:"varint,1,opt,name=support_encryption,json=supportEncryption,proto3" json:"support_encryption,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFeatureResponse) Reset() { + *x = GetFeatureResponse{} + mi := &file_web_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFeatureResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFeatureResponse) ProtoMessage() {} + +func (x *GetFeatureResponse) ProtoReflect() protoreflect.Message { + mi := &file_web_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFeatureResponse.ProtoReflect.Descriptor instead. +func (*GetFeatureResponse) Descriptor() ([]byte, []int) { + return file_web_proto_rawDescGZIP(), []int{4} +} + +func (x *GetFeatureResponse) GetSupportEncryption() bool { + if x != nil { + return x.SupportEncryption + } + return false +} + +var File_web_proto protoreflect.FileDescriptor + +const file_web_proto_rawDesc = "" + + "\n" + + "\tweb.proto\x12\x03web\x1a\fcommon.proto\"e\n" + + "\fDeviceOsInfo\x12\x17\n" + + "\aos_type\x18\x01 \x01(\tR\x06osType\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\x12\"\n" + + "\fdistribution\x18\x03 \x01(\tR\fdistribution\"\x9b\x03\n" + + "\x10HeartbeatRequest\x12+\n" + + "\n" + + "machine_id\x18\x01 \x01(\v2\f.common.UUIDR\tmachineId\x12%\n" + + "\ainst_id\x18\x02 \x01(\v2\f.common.UUIDR\x06instId\x12\x1d\n" + + "\n" + + "user_token\x18\x03 \x01(\tR\tuserToken\x12)\n" + + "\x10easytier_version\x18\x04 \x01(\tR\x0feasytierVersion\x12\x1f\n" + + "\vreport_time\x18\x05 \x01(\tR\n" + + "reportTime\x12\x1a\n" + + "\bhostname\x18\x06 \x01(\tR\bhostname\x12H\n" + + "\x19running_network_instances\x18\a \x03(\v2\f.common.UUIDR\x17runningNetworkInstances\x12.\n" + + "\tdevice_os\x18\b \x01(\v2\x11.web.DeviceOsInfoR\bdeviceOs\x122\n" + + "\x15support_config_source\x18\t \x01(\bR\x13supportConfigSource\"\x13\n" + + "\x11HeartbeatResponse\"\x13\n" + + "\x11GetFeatureRequest\"C\n" + + "\x12GetFeatureResponse\x12-\n" + + "\x12support_encryption\x18\x01 \x01(\bR\x11supportEncryption2\x8d\x01\n" + + "\x10WebServerService\x12:\n" + + "\tHeartbeat\x12\x15.web.HeartbeatRequest\x1a\x16.web.HeartbeatResponse\x12=\n" + + "\n" + + "GetFeature\x12\x16.web.GetFeatureRequest\x1a\x17.web.GetFeatureResponseb\x06proto3" + +var ( + file_web_proto_rawDescOnce sync.Once + file_web_proto_rawDescData []byte +) + +func file_web_proto_rawDescGZIP() []byte { + file_web_proto_rawDescOnce.Do(func() { + file_web_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_web_proto_rawDesc), len(file_web_proto_rawDesc))) + }) + return file_web_proto_rawDescData +} + +var file_web_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_web_proto_goTypes = []any{ + (*DeviceOsInfo)(nil), // 0: web.DeviceOsInfo + (*HeartbeatRequest)(nil), // 1: web.HeartbeatRequest + (*HeartbeatResponse)(nil), // 2: web.HeartbeatResponse + (*GetFeatureRequest)(nil), // 3: web.GetFeatureRequest + (*GetFeatureResponse)(nil), // 4: web.GetFeatureResponse + (*common.UUID)(nil), // 5: common.UUID +} +var file_web_proto_depIdxs = []int32{ + 5, // 0: web.HeartbeatRequest.machine_id:type_name -> common.UUID + 5, // 1: web.HeartbeatRequest.inst_id:type_name -> common.UUID + 5, // 2: web.HeartbeatRequest.running_network_instances:type_name -> common.UUID + 0, // 3: web.HeartbeatRequest.device_os:type_name -> web.DeviceOsInfo + 1, // 4: web.WebServerService.Heartbeat:input_type -> web.HeartbeatRequest + 3, // 5: web.WebServerService.GetFeature:input_type -> web.GetFeatureRequest + 2, // 6: web.WebServerService.Heartbeat:output_type -> web.HeartbeatResponse + 4, // 7: web.WebServerService.GetFeature:output_type -> web.GetFeatureResponse + 6, // [6:8] is the sub-list for method output_type + 4, // [4:6] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_web_proto_init() } +func file_web_proto_init() { + if File_web_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_web_proto_rawDesc), len(file_web_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_web_proto_goTypes, + DependencyIndexes: file_web_proto_depIdxs, + MessageInfos: file_web_proto_msgTypes, + }.Build() + File_web_proto = out.File + file_web_proto_goTypes = nil + file_web_proto_depIdxs = nil +} diff --git a/easytier-go/rpc.go b/easytier-go/rpc.go new file mode 100644 index 00000000..673cad1b --- /dev/null +++ b/easytier-go/rpc.go @@ -0,0 +1,9 @@ +package host + +import internalhost "github.com/EasyTier/EasyTier/easytier-go/internal/host" + +// PeerInfo describes one peer visible to an EasyTier instance. +type PeerInfo = internalhost.PeerInfo + +// Route describes one route visible to an EasyTier instance. +type Route = internalhost.Route diff --git a/easytier-go/script/generate-proto.sh b/easytier-go/script/generate-proto.sh new file mode 100755 index 00000000..3019cd8e --- /dev/null +++ b/easytier-go/script/generate-proto.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +easytier_source="${1:-${EASYTIER_SOURCE:-"${repository_root}/.."}}" +proto_root="${easytier_source}/easytier-proto/proto" + +if [[ ! -f "${proto_root}/api_instance.proto" ]]; then + echo "EasyTier proto source not found at ${proto_root}" >&2 + exit 1 +fi + +if [[ "$(protoc --version)" != "libprotoc 35.1" ]]; then + echo "protoc 35.1 is required" >&2 + exit 1 +fi +if [[ "$(protoc-gen-go --version)" != "protoc-gen-go v1.36.11" ]]; then + echo "protoc-gen-go v1.36.11 is required" >&2 + exit 1 +fi + +protoc \ + -I "${proto_root}" \ + --go_out="${repository_root}" \ + --go_opt=module=github.com/EasyTier/EasyTier/easytier-go \ + --go_opt=Mcommon.proto=github.com/EasyTier/EasyTier/easytier-go/proto/common \ + --go_opt=Merror.proto=github.com/EasyTier/EasyTier/easytier-go/proto/error \ + --go_opt=Macl.proto=github.com/EasyTier/EasyTier/easytier-go/proto/acl \ + --go_opt=Mpeer_rpc.proto=github.com/EasyTier/EasyTier/easytier-go/proto/peer_rpc \ + --go_opt=Mapi_instance.proto=github.com/EasyTier/EasyTier/easytier-go/proto/api/instance \ + --go_opt=Mapi_config.proto=github.com/EasyTier/EasyTier/easytier-go/proto/api/config \ + --go_opt=Mapi_manage.proto=github.com/EasyTier/EasyTier/easytier-go/proto/api/manage \ + --go_opt=Mweb.proto=github.com/EasyTier/EasyTier/easytier-go/proto/web \ + "${proto_root}/common.proto" \ + "${proto_root}/error.proto" \ + "${proto_root}/acl.proto" \ + "${proto_root}/peer_rpc.proto" \ + "${proto_root}/api_instance.proto" \ + "${proto_root}/api_config.proto" \ + "${proto_root}/api_manage.proto" \ + "${proto_root}/web.proto" diff --git a/easytier-go/testdata/wasi_socket_guest.source b/easytier-go/testdata/wasi_socket_guest.source new file mode 100644 index 00000000..1e855924 --- /dev/null +++ b/easytier-go/testdata/wasi_socket_guest.source @@ -0,0 +1,4 @@ +repository=https://github.com/EasyTier/EasyTier +commit=6a3d15f8758eed759d55401ff4ed7c47021b0819 +path=tools/wasi-socket-poc/guest +sha256=b672ae94eb31219b49b9c7cbe974ace81bb28effefae1cabde1c8abd68c01959 diff --git a/easytier-go/testdata/wasi_socket_guest.wasm b/easytier-go/testdata/wasi_socket_guest.wasm new file mode 100644 index 00000000..436dc8c1 Binary files /dev/null and b/easytier-go/testdata/wasi_socket_guest.wasm differ diff --git a/easytier-go/tests/host_dataplane_test.go b/easytier-go/tests/host_dataplane_test.go new file mode 100644 index 00000000..a687cc0d --- /dev/null +++ b/easytier-go/tests/host_dataplane_test.go @@ -0,0 +1,455 @@ +package host_test + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "os" + "syscall" + "testing" + "time" + + corehost "github.com/EasyTier/EasyTier/easytier-go" + "github.com/EasyTier/EasyTier/easytier-go/platform" +) + +func TestPublicDataPlaneTCPAndUDP(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) + defer cancel() + host, server, client := startDataPlanePair(t, ctx) + defer host.Close(ctx) + defer server.Close(ctx) + defer client.Close(ctx) + + testTCPDataPlane(t, ctx, server, client) + testUDPDataPlane(t, ctx, server, client) +} + +func startDataPlanePair( + t *testing.T, + ctx context.Context, +) (*corehost.Host, *corehost.Instance, *corehost.Instance) { + t.Helper() + sockets := &recordingSocketFactory{} + host, err := corehost.New(ctx, corehost.Options{ + Platform: platform.Services{Sockets: sockets}, + }) + if err != nil { + t.Fatalf("create host: %v", err) + } + server, err := host.CreateInstance( + ctx, + instanceConfig(t, 101, "10.144.0.101", 0, false, true), + ) + if err != nil { + host.Close(ctx) + t.Fatalf("create server: %v", err) + } + if err := server.Start(ctx); err != nil { + server.Close(ctx) + host.Close(ctx) + t.Fatalf("start server: %v", err) + } + client, err := host.CreateInstance( + ctx, + instanceConfig( + t, + 102, + "10.144.0.102", + sockets.listenerPort(t), + true, + false, + ), + ) + if err != nil { + server.Close(ctx) + host.Close(ctx) + t.Fatalf("create client: %v", err) + } + if err := client.Start(ctx); err != nil { + client.Close(ctx) + server.Close(ctx) + host.Close(ctx) + t.Fatalf("start client: %v", err) + } + return host, server, client +} + +func testTCPDataPlane( + t *testing.T, + ctx context.Context, + server *corehost.Instance, + client *corehost.Instance, +) { + t.Helper() + listener := listenTCPEventually(t, ctx, server) + defer listener.Close() + serverPort := listener.Addr().(*net.TCPAddr).Port + accepted := make(chan connectionResult, 1) + go func() { + connection, err := listener.Accept() + accepted <- connectionResult{connection: connection, err: err} + }() + + clientConnection := dialEventually( + t, + ctx, + client, + fmt.Sprintf("10.144.0.101:%d", serverPort), + ) + defer clientConnection.Close() + var serverConnection net.Conn + select { + case result := <-accepted: + if result.err != nil { + t.Fatalf("accept EasyTier TCP: %v", result.err) + } + serverConnection = result.connection + case <-ctx.Done(): + t.Fatalf("accept EasyTier TCP: %v", ctx.Err()) + } + defer serverConnection.Close() + + if err := clientConnection.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + t.Fatalf("set client TCP deadline: %v", err) + } + if err := serverConnection.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + t.Fatalf("set server TCP deadline: %v", err) + } + assertStreamExchange(t, clientConnection, serverConnection, []byte("client-to-server")) + assertStreamExchange(t, serverConnection, clientConnection, []byte("server-to-client")) + + if err := clientConnection.SetReadDeadline( + time.Now().Add(100 * time.Millisecond), + ); err != nil { + t.Fatalf("set expiring TCP read deadline: %v", err) + } + if _, err := clientConnection.Read(make([]byte, 1)); !errors.Is( + err, + os.ErrDeadlineExceeded, + ) { + t.Fatalf("TCP read deadline error = %v, want deadline exceeded", err) + } + + if err := clientConnection.SetReadDeadline( + time.Now().Add(100 * time.Millisecond), + ); err != nil { + t.Fatalf("set initial TCP read deadline: %v", err) + } + read := make(chan readResult, 1) + go func() { + buffer := make([]byte, 16) + n, err := clientConnection.Read(buffer) + read <- readResult{data: buffer[:n], err: err} + }() + time.Sleep(40 * time.Millisecond) + if err := clientConnection.SetReadDeadline( + time.Now().Add(2 * time.Second), + ); err != nil { + t.Fatalf("extend active TCP read deadline: %v", err) + } + time.Sleep(100 * time.Millisecond) + if _, err := serverConnection.Write([]byte("extended")); err != nil { + t.Fatalf("write after extending TCP deadline: %v", err) + } + select { + case result := <-read: + if result.err != nil { + t.Fatalf("read after extending TCP deadline: %v", result.err) + } + if string(result.data) != "extended" { + t.Fatalf("extended deadline read = %q", result.data) + } + case <-ctx.Done(): + t.Fatalf("read after extending TCP deadline: %v", ctx.Err()) + } + + if err := clientConnection.SetReadDeadline(time.Time{}); err != nil { + t.Fatalf("clear TCP read deadline: %v", err) + } + blockedRead := make(chan error, 1) + go func() { + _, err := clientConnection.Read(make([]byte, 1)) + blockedRead <- err + }() + time.Sleep(40 * time.Millisecond) + if err := clientConnection.Close(); err != nil { + t.Fatalf("close TCP connection with blocked read: %v", err) + } + select { + case err := <-blockedRead: + if !errors.Is(err, net.ErrClosed) { + t.Fatalf("blocked TCP read error = %v, want net.ErrClosed", err) + } + case <-ctx.Done(): + t.Fatalf("blocked TCP read did not wake: %v", ctx.Err()) + } +} + +func testUDPDataPlane( + t *testing.T, + ctx context.Context, + server *corehost.Instance, + client *corehost.Instance, +) { + t.Helper() + serverPacket := listenUDPEventually(t, ctx, server) + defer serverPacket.Close() + clientPacket := listenUDPEventually(t, ctx, client) + defer clientPacket.Close() + if err := serverPacket.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + t.Fatalf("set server UDP deadline: %v", err) + } + if err := clientPacket.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + t.Fatalf("set client UDP deadline: %v", err) + } + + serverPort := serverPacket.LocalAddr().(*net.UDPAddr).Port + serverOverlay := &net.UDPAddr{ + IP: net.IPv4(10, 144, 0, 101), + Port: serverPort, + } + clientOverlay := &net.UDPAddr{ + IP: net.IPv4(10, 144, 0, 102), + Port: clientPacket.LocalAddr().(*net.UDPAddr).Port, + } + writePacketEventually(t, ctx, serverPacket, []byte("warmup"), clientOverlay) + writePacketEventually(t, ctx, clientPacket, []byte("datagram"), serverOverlay) + buffer := make([]byte, 64) + n, peer, err := serverPacket.ReadFrom(buffer) + if err != nil { + t.Fatalf("read EasyTier UDP: %v", err) + } + if string(buffer[:n]) != "datagram" { + t.Fatalf("UDP payload = %q", buffer[:n]) + } + if peer.(*net.UDPAddr).IP.String() != "10.144.0.102" { + t.Fatalf("UDP source = %v", peer) + } + + if _, err := serverPacket.WriteTo([]byte("reply"), clientOverlay); err != nil { + t.Fatalf("reply over EasyTier UDP: %v", err) + } + for { + n, _, err = clientPacket.ReadFrom(buffer) + if err != nil { + t.Fatalf("read EasyTier UDP reply: %v", err) + } + if string(buffer[:n]) == "reply" { + break + } + } + + if _, err := clientPacket.WriteTo([]byte("truncate"), serverOverlay); err != nil { + t.Fatalf("write truncation UDP packet: %v", err) + } + n, _, err = serverPacket.ReadFrom(make([]byte, 2)) + if n != 2 || !errors.Is(err, io.ErrShortBuffer) { + t.Fatalf("truncated UDP read = (%d, %v), want (2, io.ErrShortBuffer)", n, err) + } + + testConnectedUDPDataPlane(t, ctx, server, client) +} + +func testConnectedUDPDataPlane( + t *testing.T, + ctx context.Context, + server *corehost.Instance, + client *corehost.Instance, +) { + t.Helper() + serverPacket := listenUDPEventually(t, ctx, server) + defer serverPacket.Close() + wrongPeer := listenUDPEventually(t, ctx, server) + defer wrongPeer.Close() + serverOverlay := &net.UDPAddr{ + IP: net.IPv4(10, 144, 0, 101), + Port: serverPacket.LocalAddr().(*net.UDPAddr).Port, + } + clientConnection, err := client.Dial(ctx, "udp4", serverOverlay.String()) + if err != nil { + t.Fatalf("dial through EasyTier UDP: %v", err) + } + defer clientConnection.Close() + if err := clientConnection.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + t.Fatalf("set connected UDP deadline: %v", err) + } + if err := serverPacket.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + t.Fatalf("set connected UDP server deadline: %v", err) + } + + clientOverlay := &net.UDPAddr{ + IP: net.IPv4(10, 144, 0, 102), + Port: clientConnection.LocalAddr().(*net.UDPAddr).Port, + } + if _, err := serverPacket.WriteTo([]byte("warmup"), clientOverlay); err != nil { + t.Fatalf("warm connected EasyTier UDP path: %v", err) + } + if _, err := clientConnection.Write([]byte("connected")); err != nil { + t.Fatalf("write connected EasyTier UDP: %v", err) + } + buffer := make([]byte, 64) + n, peer, err := serverPacket.ReadFrom(buffer) + if err != nil { + t.Fatalf("read connected EasyTier UDP: %v", err) + } + if string(buffer[:n]) != "connected" { + t.Fatalf("connected UDP payload = %q", buffer[:n]) + } + if peer.(*net.UDPAddr).IP.String() != "10.144.0.102" { + t.Fatalf("connected UDP source = %v", peer) + } + + if _, err := wrongPeer.WriteTo([]byte("wrong-peer"), clientOverlay); err != nil { + t.Fatalf("write wrong-peer EasyTier UDP: %v", err) + } + if _, err := serverPacket.WriteTo([]byte("right-peer"), clientOverlay); err != nil { + t.Fatalf("write fixed-peer EasyTier UDP: %v", err) + } + for { + n, err = clientConnection.Read(buffer) + if err != nil { + t.Fatalf("read connected EasyTier UDP reply: %v", err) + } + if string(buffer[:n]) == "right-peer" { + break + } + if string(buffer[:n]) != "warmup" { + t.Fatalf( + "connected UDP reply = %q, want fixed-peer datagram", + buffer[:n], + ) + } + } + if clientConnection.RemoteAddr().String() != serverOverlay.String() { + t.Fatalf( + "connected UDP remote = %v, want %v", + clientConnection.RemoteAddr(), + serverOverlay, + ) + } +} + +func listenTCPEventually( + t *testing.T, + ctx context.Context, + instance *corehost.Instance, +) net.Listener { + t.Helper() + for { + listener, err := instance.Listen("tcp4", ":0") + if err == nil { + return listener + } + if !errors.Is(err, syscall.ENETUNREACH) { + t.Fatalf("listen through EasyTier TCP: %v", err) + } + select { + case <-time.After(20 * time.Millisecond): + case <-ctx.Done(): + t.Fatalf("wait for EasyTier TCP data plane: %v", ctx.Err()) + } + } +} + +func listenUDPEventually( + t *testing.T, + ctx context.Context, + instance *corehost.Instance, +) net.PacketConn { + t.Helper() + for { + connection, err := instance.ListenPacket("udp4", ":0") + if err == nil { + return connection + } + if !errors.Is(err, syscall.ENETUNREACH) { + t.Fatalf("listen through EasyTier UDP: %v", err) + } + select { + case <-time.After(20 * time.Millisecond): + case <-ctx.Done(): + t.Fatalf("wait for EasyTier UDP data plane: %v", ctx.Err()) + } + } +} + +func dialEventually( + t *testing.T, + ctx context.Context, + instance *corehost.Instance, + address string, +) net.Conn { + t.Helper() + for { + attempt, cancel := context.WithTimeout(ctx, 500*time.Millisecond) + connection, err := instance.Dial(attempt, "tcp4", address) + cancel() + if err == nil { + return connection + } + if !errors.Is(err, syscall.ENETUNREACH) && + !errors.Is(err, syscall.ECONNREFUSED) && + !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("dial through EasyTier TCP: %v", err) + } + select { + case <-time.After(20 * time.Millisecond): + case <-ctx.Done(): + t.Fatalf("wait for EasyTier TCP route: %v", ctx.Err()) + } + } +} + +func writePacketEventually( + t *testing.T, + ctx context.Context, + connection net.PacketConn, + data []byte, + peer net.Addr, +) { + t.Helper() + for { + if _, err := connection.WriteTo(data, peer); err == nil { + return + } else if !errors.Is(err, syscall.ENETUNREACH) { + t.Fatalf("write through EasyTier UDP: %v", err) + } + select { + case <-time.After(20 * time.Millisecond): + case <-ctx.Done(): + t.Fatalf("wait for EasyTier UDP route: %v", ctx.Err()) + } + } +} + +func assertStreamExchange( + t *testing.T, + writer net.Conn, + reader net.Conn, + payload []byte, +) { + t.Helper() + if _, err := writer.Write(payload); err != nil { + t.Fatalf("write EasyTier TCP: %v", err) + } + received := make([]byte, len(payload)) + if _, err := io.ReadFull(reader, received); err != nil { + t.Fatalf("read EasyTier TCP: %v", err) + } + if string(received) != string(payload) { + t.Fatalf("TCP payload = %q, want %q", received, payload) + } +} + +type connectionResult struct { + connection net.Conn + err error +} + +type readResult struct { + data []byte + err error +} diff --git a/easytier-go/tests/host_public_test.go b/easytier-go/tests/host_public_test.go new file mode 100644 index 00000000..e21ea88b --- /dev/null +++ b/easytier-go/tests/host_public_test.go @@ -0,0 +1,692 @@ +package host_test + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "net/netip" + "strings" + "sync" + "testing" + "time" + + corehost "github.com/EasyTier/EasyTier/easytier-go" + "github.com/EasyTier/EasyTier/easytier-go/platform" + "github.com/EasyTier/EasyTier/easytier-go/platform/netstd" + hostproto "github.com/EasyTier/EasyTier/easytier-go/proto" +) + +func TestPublicLifecycleDoesNotExposeWazero(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + host, err := corehost.New(ctx, corehost.Options{}) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + instance, err := host.CreateInstance( + ctx, + instanceConfig(t, 1, "10.144.0.1", 0, false, false), + ) + if err != nil { + t.Fatalf("create instance: %v", err) + } + defer instance.Close(ctx) + if err := instance.Start(ctx); err != nil { + t.Fatalf("start instance: %v", err) + } + if state := instance.State(); state != corehost.StateRunning { + t.Fatalf("state = %d, want running", state) + } + if err := instance.Start(ctx); err == nil { + t.Fatal("started running instance twice") + } + if state := instance.State(); state != corehost.StateRunning { + t.Fatalf("duplicate start terminated instance: state=%d", state) + } + if err := instance.Stop(ctx); err != nil { + t.Fatalf("stop instance: %v", err) + } + if err := instance.Wait(ctx); err != nil { + t.Fatalf("wait for instance: %v", err) + } +} + +func TestPublicFacadeConnectsTwoCoresAndExchangesPacket(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + sockets := &recordingSocketFactory{} + host, err := corehost.New(ctx, corehost.Options{ + Platform: platform.Services{Sockets: sockets}, + }) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + server, err := host.CreateInstance( + ctx, + instanceConfig(t, 1, "10.144.0.1", 0, false, true), + ) + if err != nil { + t.Fatalf("create server: %v", err) + } + defer server.Close(ctx) + if err := server.Start(ctx); err != nil { + t.Fatalf("start server: %v", err) + } + port := sockets.listenerPort(t) + client, err := host.CreateInstance( + ctx, + instanceConfig(t, 2, "10.144.0.2", port, true, false), + ) + if err != nil { + t.Fatalf("create client: %v", err) + } + defer client.Close(ctx) + if err := client.Start(ctx); err != nil { + t.Fatalf("start client: %v", err) + } + + packet := ipv4Packet( + net.IPv4(10, 144, 0, 2), + net.IPv4(10, 144, 0, 1), + []byte("public-go-host"), + ) + var received []byte + exchangeDeadline := time.Now().Add(10 * time.Second) + for time.Now().Before(exchangeDeadline) { + if err := client.SendPacket(ctx, packet); err != nil { + t.Fatalf("send packet: %v", err) + } + receiveContext, stopReceive := context.WithTimeout(ctx, 100*time.Millisecond) + received, err = server.ReceivePacket(receiveContext) + timedOut := errors.Is(err, context.DeadlineExceeded) + stopReceive() + if err == nil { + break + } + if !timedOut { + t.Fatalf("receive packet: %v", err) + } + } + if string(received) != string(packet) { + t.Fatalf("received packet = %x, want %x", received, packet) + } + event := waitForEvent(t, ctx, client.Events(), "peer_added") + if !strings.Contains(event.Message, "PeerAdded") { + t.Fatalf("peer event message = %q", event.Message) + } + peers, err := client.ListPeer(ctx) + if err != nil { + t.Fatalf("list connected peers: %v", err) + } + if len(peers) == 0 { + t.Fatal("connected peer list is empty") + } + + if err := client.Stop(ctx); err != nil { + t.Fatalf("stop client: %v", err) + } + if err := server.Stop(ctx); err != nil { + t.Fatalf("stop server: %v", err) + } +} + +func TestPublicConfigUsesCoreTCPPortForward(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + sockets := &recordingSocketFactory{} + host, err := corehost.New(ctx, corehost.Options{ + Platform: platform.Services{Sockets: sockets}, + }) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + + server, err := host.CreateInstance( + ctx, + instanceConfig(t, 201, "10.144.0.201", 0, false, true), + ) + if err != nil { + t.Fatalf("create server: %v", err) + } + defer server.Close(ctx) + if err := server.Start(ctx); err != nil { + t.Fatalf("start server: %v", err) + } + underlayPort := sockets.listenerPort(t) + overlayListener := listenTCPEventually(t, ctx, server) + defer overlayListener.Close() + go func() { + for { + connection, acceptErr := overlayListener.Accept() + if acceptErr != nil { + return + } + go func() { + defer connection.Close() + _, _ = io.Copy(connection, connection) + }() + } + }() + + probe, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + t.Fatalf("reserve port-forward address: %v", err) + } + forwardAddress := netip.MustParseAddrPort(probe.Addr().String()) + if err := probe.Close(); err != nil { + t.Fatalf("release port-forward address: %v", err) + } + destination := netip.AddrPortFrom( + netip.MustParseAddr("10.144.0.201"), + uint16(overlayListener.Addr().(*net.TCPAddr).Port), + ) + clientConfig, err := corehost.NewInstanceConfigBuilder("default"). + NetworkSecret("test"). + Hostname("go-host-202"). + IPv4(netip.MustParsePrefix("10.144.0.202/24")). + AddPeers(fmt.Sprintf("tcp://127.0.0.1:%d", underlayPort)). + AddPortForwards(corehost.PortForwardConfig{ + Protocol: corehost.PortForwardTCP, + Bind: forwardAddress, + Destination: destination, + }). + P2P(corehost.P2PPolicy{Disable: true}). + Encryption(false). + Build() + if err != nil { + t.Fatalf("build client config: %v", err) + } + client, err := host.CreateInstance(ctx, clientConfig) + if err != nil { + t.Fatalf("create client: %v", err) + } + defer client.Close(ctx) + if err := client.Start(ctx); err != nil { + t.Fatalf("start client: %v", err) + } + + payload := []byte("core-owned-port-forward") + var lastErr error + forwardDeadline := time.Now().Add(10 * time.Second) + for time.Now().Before(forwardDeadline) { + connection, dialErr := net.DialTimeout( + "tcp4", + forwardAddress.String(), + 200*time.Millisecond, + ) + if dialErr == nil { + _ = connection.SetDeadline(time.Now().Add(time.Second)) + _, writeErr := connection.Write(payload) + response := make([]byte, len(payload)) + _, readErr := io.ReadFull(connection, response) + _ = connection.Close() + if writeErr == nil && readErr == nil && + string(response) == string(payload) { + return + } + if writeErr == nil && readErr == nil { + lastErr = fmt.Errorf("forwarded response = %q", response) + } else { + lastErr = errors.Join(writeErr, readErr) + } + } else { + lastErr = dialErr + } + select { + case <-time.After(20 * time.Millisecond): + case <-ctx.Done(): + t.Fatalf("wait for core port forward: %v", ctx.Err()) + } + } + t.Fatalf("core TCP port forward did not carry traffic: %v", lastErr) +} + +func TestPublicConfigUsesCoreUDPPortForwardForMaximumPayload(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + sockets := &recordingSocketFactory{} + host, err := corehost.New(ctx, corehost.Options{ + Platform: platform.Services{Sockets: sockets}, + }) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + + server, err := host.CreateInstance( + ctx, + instanceConfig(t, 203, "10.144.0.203", 0, false, true), + ) + if err != nil { + t.Fatalf("create server: %v", err) + } + defer server.Close(ctx) + if err := server.Start(ctx); err != nil { + t.Fatalf("start server: %v", err) + } + underlayPort := sockets.listenerPort(t) + reflectorDone := make(chan error, 1) + go func() { + for { + packet, receiveErr := server.ReceivePacket(ctx) + if receiveErr != nil { + reflectorDone <- receiveErr + return + } + response, ok := reflectIPv4UDPFragment(packet) + if !ok { + continue + } + if sendErr := server.SendPacket(ctx, response); sendErr != nil { + reflectorDone <- sendErr + return + } + } + }() + + probe, err := net.ListenUDP( + "udp4", + &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}, + ) + if err != nil { + t.Fatalf("reserve UDP port-forward address: %v", err) + } + forwardAddress := netip.MustParseAddrPort(probe.LocalAddr().String()) + if err := probe.Close(); err != nil { + t.Fatalf("release UDP port-forward address: %v", err) + } + destination := netip.AddrPortFrom( + netip.MustParseAddr("10.144.0.203"), + 41000, + ) + clientConfig, err := corehost.NewInstanceConfigBuilder("default"). + NetworkSecret("test"). + Hostname("go-host-204"). + IPv4(netip.MustParsePrefix("10.144.0.204/24")). + AddPeers(fmt.Sprintf("tcp://127.0.0.1:%d", underlayPort)). + AddPortForwards(corehost.PortForwardConfig{ + Protocol: corehost.PortForwardUDP, + Bind: forwardAddress, + Destination: destination, + }). + P2P(corehost.P2PPolicy{Disable: true}). + Encryption(false). + Build() + if err != nil { + t.Fatalf("build client config: %v", err) + } + client, err := host.CreateInstance(ctx, clientConfig) + if err != nil { + t.Fatalf("create client: %v", err) + } + defer client.Close(ctx) + if err := client.Start(ctx); err != nil { + t.Fatalf("start client: %v", err) + } + eventContext, stopEvent := context.WithTimeout(ctx, 5*time.Second) + _ = waitForEvent( + t, + eventContext, + client.Events(), + "gateway_port_forward_added", + ) + stopEvent() + waitForOverlayRoute( + t, + ctx, + client, + netip.MustParseAddr("10.144.0.203"), + ) + waitForOverlayRoute( + t, + ctx, + server, + netip.MustParseAddr("10.144.0.204"), + ) + + connection, err := net.DialUDP( + "udp4", + nil, + net.UDPAddrFromAddrPort(forwardAddress), + ) + if err != nil { + t.Fatalf("dial UDP port forward: %v", err) + } + defer connection.Close() + var lastErr error + forwardDeadline := time.Now().Add(10 * time.Second) + for time.Now().Before(forwardDeadline) { + if err := connection.SetDeadline(time.Now().Add(500 * time.Millisecond)); err != nil { + t.Fatalf("set UDP port-forward deadline: %v", err) + } + if _, err := connection.Write([]byte("warmup")); err != nil { + lastErr = err + continue + } + warmup := make([]byte, len("warmup")) + length, err := connection.Read(warmup) + if err != nil { + lastErr = err + select { + case reflectorErr := <-reflectorDone: + t.Fatalf("reflect UDP port-forward packet: %v", reflectorErr) + default: + } + continue + } + if string(warmup[:length]) == "warmup" { + lastErr = nil + break + } + lastErr = fmt.Errorf("UDP port-forward warmup response = %q", warmup[:length]) + } + if lastErr != nil { + t.Fatalf("core UDP port forward did not become ready: %v", lastErr) + } + + payload := make([]byte, 65_507) + for index := range payload { + payload[index] = byte(index) + } + response := make([]byte, len(payload)) + forwardDeadline = time.Now().Add(10 * time.Second) + for time.Now().Before(forwardDeadline) { + if err := connection.SetDeadline(time.Now().Add(500 * time.Millisecond)); err != nil { + t.Fatalf("set UDP port-forward deadline: %v", err) + } + if _, err := connection.Write(payload); err != nil { + lastErr = err + continue + } + length, err := connection.Read(response) + if err != nil { + lastErr = err + select { + case reflectorErr := <-reflectorDone: + t.Fatalf("reflect maximum UDP port-forward payload: %v", reflectorErr) + default: + } + continue + } + if string(response[:length]) == "warmup" { + continue + } + if length != len(payload) { + t.Fatalf( + "forwarded UDP payload length = %d, want %d", + length, + len(payload), + ) + } + if !bytes.Equal(response[:length], payload) { + t.Fatal("forwarded UDP payload contents changed") + } + return + } + t.Fatalf("core UDP port forward did not carry maximum payload: %v", lastErr) +} + +func reflectIPv4UDPFragment(packet []byte) ([]byte, bool) { + if len(packet) < 20 || packet[0]>>4 != 4 || packet[9] != 17 { + return nil, false + } + headerLength := int(packet[0]&0x0f) * 4 + totalLength := int(binary.BigEndian.Uint16(packet[2:4])) + if headerLength < 20 || totalLength < headerLength || + totalLength > len(packet) { + return nil, false + } + response := append([]byte(nil), packet[:totalLength]...) + source := append([]byte(nil), response[12:16]...) + copy(response[12:16], response[16:20]) + copy(response[16:20], source) + fragmentOffset := binary.BigEndian.Uint16(response[6:8]) & 0x1fff + if fragmentOffset == 0 { + if totalLength < headerLength+8 { + return nil, false + } + // Swapping both endpoints preserves the UDP checksum sum, including + // its pseudo-header, so only the IPv4 header checksum must change. + sourcePort := append([]byte(nil), response[headerLength:headerLength+2]...) + copy( + response[headerLength:headerLength+2], + response[headerLength+2:headerLength+4], + ) + copy(response[headerLength+2:headerLength+4], sourcePort) + } + response[10], response[11] = 0, 0 + binary.BigEndian.PutUint16( + response[10:12], + ipv4Checksum(response[:headerLength]), + ) + return response, true +} + +func waitForOverlayRoute( + t *testing.T, + ctx context.Context, + instance *corehost.Instance, + target netip.Addr, +) { + t.Helper() + octets := target.As4() + targetValue := binary.BigEndian.Uint32(octets[:]) + for { + routes, err := instance.ListRoute(ctx) + if err != nil { + t.Fatalf("list routes: %v", err) + } + for _, route := range routes { + if route.GetIpv4Addr().GetAddress().GetAddr() == targetValue { + return + } + } + select { + case <-time.After(20 * time.Millisecond): + case <-ctx.Done(): + t.Fatalf("wait for overlay route to %s: %v", target, ctx.Err()) + } + } +} + +func TestPublicEventStreamClosesWithInstance(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + host, err := corehost.New(ctx, corehost.Options{}) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + instance, err := host.CreateInstance( + ctx, + instanceConfig(t, 3, "10.144.0.3", 0, false, false), + ) + if err != nil { + t.Fatalf("create instance: %v", err) + } + events := instance.Events() + if err := instance.Close(ctx); err != nil { + t.Fatalf("close instance: %v", err) + } + select { + case _, open := <-events: + if open { + t.Fatal("event stream remained open after instance close") + } + case <-ctx.Done(): + t.Fatal("event stream did not close with instance") + } +} + +func TestEmbeddedCoreInfoIsPublicWithoutArtifactBytes(t *testing.T) { + info := corehost.CoreInfo() + if len(info.EasyTierCommit) != 40 { + t.Fatalf("EasyTier commit = %q", info.EasyTierCommit) + } + if len(info.SHA256) != 64 { + t.Fatalf("EasyTier SHA-256 = %q", info.SHA256) + } + if info.EasyTierCommit != hostproto.EasyTierCommit { + t.Fatalf( + "EasyTier artifact commit %q != protobuf commit %q", + info.EasyTierCommit, + hostproto.EasyTierCommit, + ) + } + if len(hostproto.SchemaSHA256) != 64 { + t.Fatalf("protobuf schema SHA-256 = %q", hostproto.SchemaSHA256) + } +} + +func TestPublicCreateInstanceAcceptsSecureModeConfig(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + host, err := corehost.New(ctx, corehost.Options{}) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + + privateKey := make([]byte, 32) + for index := range privateKey { + privateKey[index] = byte(index + 1) + } + config, err := corehost.NewInstanceConfigBuilder("secure-test"). + NetworkSecret("test"). + P2P(corehost.P2PPolicy{Disable: true}). + Encryption(false). + SecureModeWithPrivateKey(privateKey). + Build() + if err != nil { + t.Fatalf("build secure-mode config: %v", err) + } + instance, err := host.CreateInstance(ctx, config) + if err != nil { + t.Fatalf("create secure-mode instance: %v", err) + } + defer instance.Close(ctx) +} + +func instanceConfig( + t *testing.T, + id int, + ipv4 string, + port int, + connect bool, + listen bool, +) corehost.InstanceConfig { + t.Helper() + builder := corehost.NewInstanceConfigBuilder("default"). + NetworkSecret("test"). + Hostname(fmt.Sprintf("go-host-%d", id)). + IPv4(netip.MustParsePrefix(ipv4 + "/24")). + P2P(corehost.P2PPolicy{Disable: true}). + Encryption(false) + if connect { + builder.AddPeers(fmt.Sprintf("tcp://127.0.0.1:%d", port)) + } else if listen { + builder.AddListeners(fmt.Sprintf("tcp://127.0.0.1:%d", port)) + } + config, err := builder.Build() + if err != nil { + t.Fatalf("build instance config: %v", err) + } + return config +} + +func waitForEvent( + t *testing.T, + ctx context.Context, + events <-chan corehost.Event, + kind string, +) corehost.Event { + t.Helper() + for { + select { + case event, open := <-events: + if !open { + t.Fatalf("event stream closed before %q", kind) + } + if event.Kind == kind { + return event + } + case <-ctx.Done(): + t.Fatalf("wait for event %q: %v", kind, ctx.Err()) + } + } +} + +type recordingSocketFactory struct { + inner netstd.SocketFactory + mu sync.Mutex + port int +} + +func (factory *recordingSocketFactory) ConnectTCP( + ctx context.Context, + options platform.TCPConnectOptions, +) (net.Conn, error) { + return factory.inner.ConnectTCP(ctx, options) +} + +func (factory *recordingSocketFactory) BindUDP( + ctx context.Context, + options platform.UDPBindOptions, +) (net.PacketConn, error) { + return factory.inner.BindUDP(ctx, options) +} + +func (factory *recordingSocketFactory) ListenTCP( + ctx context.Context, + options platform.TCPListenOptions, +) (net.Listener, error) { + listener, err := factory.inner.ListenTCP(ctx, options) + if err != nil { + return nil, err + } + factory.mu.Lock() + factory.port = listener.Addr().(*net.TCPAddr).Port + factory.mu.Unlock() + return listener, nil +} + +func (factory *recordingSocketFactory) listenerPort(t *testing.T) int { + t.Helper() + factory.mu.Lock() + defer factory.mu.Unlock() + if factory.port == 0 { + t.Fatal("EasyTier did not create a TCP listener") + } + return factory.port +} + +func ipv4Packet(source, destination net.IP, payload []byte) []byte { + packet := make([]byte, 20+len(payload)) + packet[0] = 0x45 + binary.BigEndian.PutUint16(packet[2:4], uint16(len(packet))) + packet[8] = 64 + packet[9] = 1 + copy(packet[12:16], source.To4()) + copy(packet[16:20], destination.To4()) + copy(packet[20:], payload) + binary.BigEndian.PutUint16(packet[10:12], ipv4Checksum(packet[:20])) + return packet +} + +func ipv4Checksum(header []byte) uint16 { + var sum uint32 + for index := 0; index < len(header); index += 2 { + sum += uint32(binary.BigEndian.Uint16(header[index : index+2])) + } + for sum > 0xffff { + sum = (sum & 0xffff) + (sum >> 16) + } + return ^uint16(sum) +} diff --git a/easytier-go/tests/host_rpc_test.go b/easytier-go/tests/host_rpc_test.go new file mode 100644 index 00000000..431477a9 --- /dev/null +++ b/easytier-go/tests/host_rpc_test.go @@ -0,0 +1,50 @@ +package host_test + +import ( + "context" + "errors" + "testing" + "time" + + corehost "github.com/EasyTier/EasyTier/easytier-go" +) + +func TestPublicManagementRPCMethods(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + host, err := corehost.New(ctx, corehost.Options{}) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + instance, err := host.CreateInstance( + ctx, + instanceConfig(t, 201, "10.144.0.201", 0, false, false), + ) + if err != nil { + t.Fatalf("create instance: %v", err) + } + defer instance.Close(ctx) + if err := instance.Start(ctx); err != nil { + t.Fatalf("start instance: %v", err) + } + if _, err := instance.ListPeer(nil); err == nil { + t.Fatal("ListPeer accepted a nil context") + } + if _, err := instance.ListRoute(nil); err == nil { + t.Fatal("ListRoute accepted a nil context") + } + + cancelled, cancelQuery := context.WithCancel(ctx) + cancelQuery() + if _, err := instance.ListPeer(cancelled); !errors.Is(err, context.Canceled) { + t.Fatalf("ListPeer with cancelled context error = %v", err) + } + + if _, err := instance.ListPeer(context.Background()); err != nil { + t.Fatalf("list peers: %v", err) + } + if _, err := instance.ListRoute(ctx); err != nil { + t.Fatalf("list routes: %v", err) + } +} diff --git a/easytier-go/tests/web_client_e2e_test.go b/easytier-go/tests/web_client_e2e_test.go new file mode 100644 index 00000000..053672f3 --- /dev/null +++ b/easytier-go/tests/web_client_e2e_test.go @@ -0,0 +1,254 @@ +package host_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "os" + "strconv" + "strings" + "testing" + "time" + + corehost "github.com/EasyTier/EasyTier/easytier-go" +) + +func TestWebClientEndToEnd(t *testing.T) { + endpoint := os.Getenv("EASYTIER_WEB_E2E_ENDPOINT") + apiURL := os.Getenv("EASYTIER_WEB_E2E_API") + authToken := os.Getenv("EASYTIER_WEB_E2E_AUTH") + if endpoint == "" || apiURL == "" || authToken == "" { + t.Skip("EasyTier WebClient E2E environment is not configured") + } + userID, err := strconv.Atoi(os.Getenv("EASYTIER_WEB_E2E_USER_ID")) + if err != nil || userID == 0 { + userID = 1 + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + host, err := corehost.New(ctx, corehost.Options{}) + if err != nil { + t.Fatalf("create host: %v", err) + } + defer host.Close(ctx) + application, err := host.CreateInstance( + ctx, + instanceConfig(t, 99, "10.154.0.1", 0, false, false), + ) + if err != nil { + t.Fatalf("create application instance: %v", err) + } + defer application.Close(ctx) + + const machineID = "11111111-2222-4333-8444-555555555555" + client, err := host.ConnectWebClient(ctx, corehost.WebClientOptions{ + Endpoint: endpoint, + MachineID: machineID, + Hostname: "go-host-e2e", + }) + if err != nil { + t.Fatalf("connect WebClient: %v", err) + } + defer client.Close(ctx) + waitFor(t, ctx, client.Connected, "WebClient connection") + + base := fmt.Sprintf( + "%s/api/internal/users/%d/machines/%s/networks", + strings.TrimRight(apiURL, "/"), + userID, + machineID, + ) + waitFor(t, ctx, func() bool { + body, status := webRequest(t, ctx, authToken, http.MethodGet, base, nil) + var response struct { + Running []json.RawMessage `json:"running_inst_ids"` + } + return status == http.StatusOK && + json.Unmarshal(body, &response) == nil && + len(response.Running) == 1 + }, "application instance heartbeat") + + const managedID = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee" + networkName := "go-host-managed" + networkSecret := "test" + managedConfig := map[string]any{ + "instance_id": managedID, + "dhcp": true, + "network_name": networkName, + "network_secret": networkSecret, + "networking_method": 2, + "listener_urls": []string{"tcp://0.0.0.0:11010", "udp://0.0.0.0:11010", "wg://0.0.0.0:11011"}, + "proxy_cidrs": []string{"10.200.0.0/24"}, + "disable_p2p": true, + "disable_ipv6": true, + "enable_vpn_portal": true, + "vpn_portal_listen_port": 11012, + "vpn_portal_client_network_addr": "10.210.0.0", + "vpn_portal_client_network_len": 24, + "data_compress_algo": 2, + "credential_file": "/unsupported", + "enable_quic_proxy": true, + "mapped_listeners": []string{"wg://0.0.0.0:11012"}, + "advanced_settings": true, + } + payload, err := json.Marshal(map[string]any{ + "config": managedConfig, + "save": false, + }) + if err != nil { + t.Fatalf("encode managed network request: %v", err) + } + body, status := webRequest(t, ctx, authToken, http.MethodPost, base, payload) + if status != http.StatusOK { + t.Fatalf("run managed instance: status=%d body=%s", status, body) + } + var managedInstance *corehost.Instance + waitFor(t, ctx, func() bool { + for _, instance := range host.Instances() { + if instance.ID() == managedID { + managedInstance = instance + return true + } + } + return false + }, "managed instance creation") + + infoURL := base + "/info" + body, status = webRequest( + t, + ctx, + authToken, + http.MethodGet, + infoURL, + []byte(fmt.Sprintf(`{"inst_ids":["%s"]}`, managedID)), + ) + if status != http.StatusOK || + !bytes.Contains(body, []byte(managedID)) || + !bytes.Contains(body, []byte(`"running":true`)) || + !bytes.Contains(body, []byte("tcp://")) || + !bytes.Contains(body, []byte("udp://")) || + bytes.Contains(body, []byte("wg://")) { + t.Fatalf("collect managed status: status=%d body=%s", status, body) + } + + portProbe, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("reserve port-forward address: %v", err) + } + portForwardAddress := portProbe.Addr().String() + portForwardPort := portProbe.Addr().(*net.TCPAddr).Port + if err := portProbe.Close(); err != nil { + t.Fatalf("release port-forward address: %v", err) + } + managedConfig["proxy_cidrs"] = []string{"10.201.0.0/24"} + managedConfig["disable_relay_data"] = true + managedConfig["port_forwards"] = []map[string]any{{ + "proto": "tcp", + "bind_ip": "127.0.0.1", + "bind_port": portForwardPort, + "dst_ip": "10.201.0.1", + "dst_port": 80, + }} + payload, err = json.Marshal(map[string]any{ + "managed_network_configs": []map[string]any{{ + "instance_id": managedID, + "network_config": managedConfig, + }}, + "config_revision": "revision-1", + }) + if err != nil { + t.Fatalf("encode managed network update: %v", err) + } + body, status = webRequest(t, ctx, authToken, http.MethodPut, base, payload) + if status != http.StatusOK { + t.Fatalf("update managed instance: status=%d body=%s", status, body) + } + waitFor(t, ctx, func() bool { + connection, err := net.DialTimeout( + "tcp", + portForwardAddress, + 100*time.Millisecond, + ) + if err != nil { + return false + } + connection.Close() + return true + }, "managed instance hot patch") + for _, instance := range host.Instances() { + if instance.ID() == managedID && instance != managedInstance { + t.Fatal("managed hot patch replaced the running instance") + } + } + + body, status = webRequest( + t, + ctx, + authToken, + http.MethodDelete, + base+"/"+managedID, + nil, + ) + if status != http.StatusOK { + t.Fatalf("delete managed instance: status=%d body=%s", status, body) + } + waitFor(t, ctx, func() bool { + instances := host.Instances() + return len(instances) == 1 && instances[0].ID() == application.ID() + }, "managed instance deletion") +} + +func webRequest( + t *testing.T, + ctx context.Context, + authToken string, + method string, + url string, + body []byte, +) ([]byte, int) { + t.Helper() + request, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body)) + if err != nil { + t.Fatalf("create %s request: %v", method, err) + } + request.Header.Set("X-Internal-Auth", authToken) + if body != nil { + request.Header.Set("Content-Type", "application/json") + } + response, err := http.DefaultClient.Do(request) + if err != nil { + return nil, 0 + } + defer response.Body.Close() + encoded, err := io.ReadAll(response.Body) + if err != nil { + t.Fatalf("read %s response: %v", method, err) + } + return encoded, response.StatusCode +} + +func waitFor( + t *testing.T, + ctx context.Context, + condition func() bool, + description string, +) { + t.Helper() + ticker := time.NewTicker(50 * time.Millisecond) + defer ticker.Stop() + for { + if condition() { + return + } + select { + case <-ticker.C: + case <-ctx.Done(): + t.Fatalf("wait for %s: %v", description, ctx.Err()) + } + } +} diff --git a/easytier-go/web_client.go b/easytier-go/web_client.go new file mode 100644 index 00000000..e7e13ff5 --- /dev/null +++ b/easytier-go/web_client.go @@ -0,0 +1,7 @@ +package host + +import internalhost "github.com/EasyTier/EasyTier/easytier-go/internal/host" + +type WebClientOptions = internalhost.WebClientOptions + +type WebClient = internalhost.WebClient diff --git a/easytier-js/README.md b/easytier-js/README.md new file mode 100644 index 00000000..1baa03f6 --- /dev/null +++ b/easytier-js/README.md @@ -0,0 +1,36 @@ +# EasyTier JavaScript + +The EasyTier JavaScript packages run the same EasyTier WASM core across web +runtimes while keeping application-facing APIs small: + +- [`@easytier/browser`](./browser) provides a browser TCP client. +- [`@easytier/cloudflare`](./cloudflare) provides a Cloudflare Durable Object + relay. +- [`@easytier/runtime`](./runtime) contains their shared runtime and adapter + implementation. Most applications should use one of the two public host + packages instead of depending on it directly. + +A complete browser and Cloudflare Worker walkthrough is available in +[`examples/web`](./examples/web). + +Source builds require Node.js 22, pnpm 10 or newer, Rust 1.95 with the +`wasm32-wasip1` target, and Protocol Buffers 35.1. Install the Rust target with: + +```sh +rustup target add wasm32-wasip1 +``` + +The JavaScript hosts use their own workspace so their Cloudflare development +toolchain does not affect EasyTier's existing GUI and web workspace. From a +clean repository checkout, run: + +```sh +cd easytier-js +pnpm install +pnpm check +``` + +The check command builds both Wasm profiles from source, builds and tests the +three packages, and validates the complete example. No pre-generated Wasm file +is required. Published packages already contain their compiled JavaScript, +type declarations, and Wasm artifacts; package consumers do not need Rust. diff --git a/easytier-js/browser/.gitignore b/easytier-js/browser/.gitignore new file mode 100644 index 00000000..5ff82e1d --- /dev/null +++ b/easytier-js/browser/.gitignore @@ -0,0 +1,5 @@ +dist/ +src/generated/ +demo/generated/ +demo/smoke.js +node_modules/ diff --git a/easytier-js/browser/README.md b/easytier-js/browser/README.md new file mode 100644 index 00000000..f1264e65 --- /dev/null +++ b/easytier-js/browser/README.md @@ -0,0 +1,65 @@ +# `@easytier/browser` + +Run an outbound EasyTier instance and its TCP data plane directly in a browser. +The package embeds the matching EasyTier WebAssembly artifact in its JavaScript +entry point; applications do not configure a bundler loader, compile Rust, load +WASI, or work with Guest handles. + +## Install + +```sh +pnpm add @easytier/browser +``` + +## Connect to an EasyTier network + +```ts +import { createEasyTier } from "@easytier/browser"; + +const easytier = await createEasyTier({ + networkName: "office", + networkSecret: "secret", + ipv4: "10.144.0.10/24", + peers: "wss://relay.example.com/", + encryption: true, +}, { + onEvent(event) { + console.log(event.kind, event.message); + }, +}); +``` + +`createEasyTier()` resolves after the EasyTier Instance is running. When the +peer is an `@easytier/cloudflare` relay, `networkName` and `networkSecret` must +match the relay configuration. Use `ws://` for local development and `wss://` +for a deployed Worker. + +## Use the TCP data plane + +```ts +const status = await easytier.status(); +console.log(status.state, status.connections); + +const stream = await easytier.connectTcp("10.144.0.20:8080", { + timeout: 10_000, +}); +await stream.write(new TextEncoder().encode("hello")); +const response = await stream.read(); +console.log(new TextDecoder().decode(response.data)); +await stream.close(); +await easytier.close(); +``` + +The Browser Adapter supports `ws://` and `wss://` peers and an overlay IPv4 TCP +data plane. It does not expose native listeners, TUN, STUN, or hole punching. +The browser must support WebAssembly JSPI. + +## Run the complete example + +The repository contains a standalone Browser-to-Cloudflare example that only +uses the public package entries: + +[`easytier-js/examples/web`](https://github.com/EasyTier/EasyTier/tree/main/easytier-js/examples/web) + +It includes the Worker configuration, local secret setup, Browser UI, health +check, deployment commands, and the expected `peer_added` result. diff --git a/easytier-js/browser/demo/index.html b/easytier-js/browser/demo/index.html new file mode 100644 index 00000000..f06b9c56 --- /dev/null +++ b/easytier-js/browser/demo/index.html @@ -0,0 +1,15 @@ + + + + + + EasyTier browser smoke + + +
+

EasyTier browser smoke

+

loading module

+
+ + + diff --git a/easytier-js/browser/demo/node-smoke.ts b/easytier-js/browser/demo/node-smoke.ts new file mode 100644 index 00000000..7e894491 --- /dev/null +++ b/easytier-js/browser/demo/node-smoke.ts @@ -0,0 +1,72 @@ +import { readFile } from "node:fs/promises"; + +import { + createEasyTierRuntime, + type RuntimeWebSocket, +} from "@easytier/runtime/adapter"; +import { + connectWithRetry, + deadline, + probeHttpMarker, + SMOKE_RETRY_MILLISECONDS, +} from "./smoke-shared"; + +const relayUrl = process.env.EASYTIER_BROWSER_RELAY ?? "ws://127.0.0.1:11011/"; +const networkName = process.env.EASYTIER_BROWSER_NETWORK ?? "browser-smoke"; +const networkSecret = process.env.EASYTIER_BROWSER_SECRET ?? "browser-smoke-test"; +const ipv4 = process.env.EASYTIER_BROWSER_IPV4 ?? "10.144.144.2"; +const moduleBytes = await readFile( + process.env.EASYTIER_BROWSER_WASM ?? + new URL("./easytier_core.wasm", import.meta.url), +); +const copiedModuleBytes = new Uint8Array(moduleBytes.byteLength); +copiedModuleBytes.set(moduleBytes); +const module = new WebAssembly.Module(copiedModuleBytes); + +let peerAdded = false; +const runtime = await createEasyTierRuntime({ + module, + config: { + profile: "browser", + instanceId: + process.env.EASYTIER_BROWSER_INSTANCE_ID ?? crypto.randomUUID(), + instanceName: "browser-node-smoke", + networkName, + networkSecret, + encryption: true, + ipv4: `${ipv4}/24`, + peers: [relayUrl], + }, + connectWebSocket: (url) => + new WebSocket(url) as unknown as RuntimeWebSocket, + onEvent: (event) => { + console.log(JSON.stringify({ event: "easytier_core_event", ...event })); + if (event.kind === "peer_added") { + peerAdded = true; + } + }, +}); + +const timeoutAt = deadline(); +while (Date.now() < timeoutAt) { + const health = await runtime.status(); + if (peerAdded) { + const target = process.env.EASYTIER_BROWSER_TCP_TARGET; + if (target !== undefined) { + const stream = await connectWithRetry( + () => runtime.connectTcp(target, { timeout: 1_000 }), + timeoutAt, + ); + const host = target.slice(0, target.lastIndexOf(":")); + await probeHttpMarker(stream, host, "browser-data-plane-ok"); + console.log( + JSON.stringify({ event: "browser_smoke_data_plane", target }), + ); + } + console.log(JSON.stringify({ event: "browser_smoke_connected", health })); + process.exit(0); + } + await new Promise((resolve) => setTimeout(resolve, SMOKE_RETRY_MILLISECONDS)); +} + +throw new Error(`EasyTier did not connect to ${relayUrl}`); diff --git a/easytier-js/browser/demo/smoke-shared.ts b/easytier-js/browser/demo/smoke-shared.ts new file mode 100644 index 00000000..497d4930 --- /dev/null +++ b/easytier-js/browser/demo/smoke-shared.ts @@ -0,0 +1,71 @@ +export const SMOKE_TIMEOUT_MILLISECONDS = 30_000; +export const SMOKE_RETRY_MILLISECONDS = 100; + +export interface SmokeStream { + read(): Promise<{ data: Uint8Array; eof: boolean }>; + write(data: Uint8Array): Promise; + close(): Promise; +} + +export function deadline(): number { + return Date.now() + SMOKE_TIMEOUT_MILLISECONDS; +} + +export async function waitForPeer( + peerAdded: () => boolean, + timeoutAt: number, + error: string, +): Promise { + while (!peerAdded() && Date.now() < timeoutAt) { + await new Promise((resolve) => setTimeout(resolve, SMOKE_RETRY_MILLISECONDS)); + } + if (!peerAdded()) { + throw new Error(error); + } +} + +export async function connectWithRetry( + connect: () => Promise, + timeoutAt: number, +): Promise { + for (;;) { + try { + return await connect(); + } catch (error) { + if (Date.now() >= timeoutAt) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, SMOKE_RETRY_MILLISECONDS)); + } + } +} + +export async function probeHttpMarker( + stream: SmokeStream, + host: string, + marker: string, +): Promise { + const request = new TextEncoder().encode( + `GET / HTTP/1.1\r\nHost: ${host}\r\nConnection: close\r\n\r\n`, + ); + let requestOffset = 0; + while (requestOffset < request.byteLength) { + const written = await stream.write(request.subarray(requestOffset)); + if (written <= 0) { + throw new Error(`invalid HTTP request write length ${written}`); + } + requestOffset += written; + } + let response = ""; + while (!response.includes(marker)) { + const result = await stream.read(); + response += new TextDecoder().decode(result.data); + if (result.eof) { + break; + } + } + await stream.close(); + if (!response.includes(marker)) { + throw new Error(`unexpected TCP response: ${response}`); + } +} diff --git a/easytier-js/browser/demo/smoke.ts b/easytier-js/browser/demo/smoke.ts new file mode 100644 index 00000000..22f5bf57 --- /dev/null +++ b/easytier-js/browser/demo/smoke.ts @@ -0,0 +1,85 @@ +import { createEasyTier } from "../src/index"; +import { + connectWithRetry, + deadline, + probeHttpMarker, + waitForPeer, +} from "./smoke-shared"; + +interface BrowserLocation { + search: string; +} + +interface StatusElement { + dataset: Record; + textContent: string | null; +} + +interface BrowserDocument { + querySelector(selector: string): StatusElement | null; +} + +const browser = globalThis as unknown as { + document: BrowserDocument; + location: BrowserLocation; +}; +const status = browser.document.querySelector("#status"); +if (status === null) { + throw new Error("browser smoke status element is missing"); +} + +function setStatus(state: string, message: string): void { + status!.dataset.state = state; + status!.textContent = message; + console.log(JSON.stringify({ event: "browser_smoke_status", state, message })); +} + +async function run(): Promise { + const query = new URLSearchParams(browser.location.search); + const relayUrl = query.get("relay") ?? "ws://127.0.0.1:11011/"; + const target = query.get("target") ?? "100.64.0.1:18080"; + const separator = target.lastIndexOf(":"); + if (separator < 1) { + throw new Error(`invalid target: ${target}`); + } + const host = target.slice(0, separator); + const port = Number(target.slice(separator + 1)); + let peerAdded = false; + const runtime = await createEasyTier( + { + networkName: "browser-smoke", + networkSecret: "browser-smoke-test", + instanceName: "browser-chromium-smoke", + ipv4: "10.144.144.2/24", + peers: relayUrl, + encryption: true, + }, + { + onEvent: (event) => { + console.log(JSON.stringify({ event: "easytier_core_event", ...event })); + if (event.kind === "peer_added") { + peerAdded = true; + } + }, + }, + ); + setStatus("joining", `joining ${relayUrl}`); + + const timeoutAt = deadline(); + await waitForPeer( + () => peerAdded, + timeoutAt, + `EasyTier did not connect to ${relayUrl}`, + ); + const stream = await connectWithRetry( + () => runtime.connectTcp(`${host}:${port}`, { timeout: 1_000 }), + timeoutAt, + ); + await probeHttpMarker(stream, host, "browser-data-plane-ok"); + setStatus("connected", `connected to ${target} through EasyTier`); +} + +setStatus("starting", "starting browser EasyTier runtime"); +void run().catch((error: unknown) => { + setStatus("failed", String(error)); +}); diff --git a/easytier-js/browser/package.json b/easytier-js/browser/package.json new file mode 100644 index 00000000..de48fc5d --- /dev/null +++ b/easytier-js/browser/package.json @@ -0,0 +1,48 @@ +{ + "name": "@easytier/browser", + "version": "0.1.0", + "description": "Run an EasyTier TCP client in the browser", + "type": "module", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/EasyTier/EasyTier.git", + "directory": "easytier-js/browser" + }, + "homepage": "https://github.com/EasyTier/EasyTier", + "bugs": "https://github.com/EasyTier/EasyTier/issues", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist/*.js", + "dist/*.d.ts", + "README.md" + ], + "scripts": { + "build:dependencies": "pnpm --filter @easytier/runtime build", + "build:wasm": "node ../runtime/scripts/build-wasm.mjs browser src/generated/easytier_core.wasm && node ../runtime/scripts/validate-wasm.mjs browser src/generated/easytier_core.wasm", + "build": "pnpm build:wasm && pnpm build:dependencies && tsc -p tsconfig.build.json && esbuild src/index.ts --bundle --format=esm --platform=browser --loader:.wasm=binary --external:@easytier/runtime --external:@easytier/runtime/* --outfile=dist/index.js", + "build:demo": "pnpm build:dependencies && esbuild demo/smoke.ts --bundle --format=esm --loader:.wasm=binary --outfile=demo/smoke.js && node ../runtime/scripts/copy-artifact.mjs src/generated/easytier_core.wasm demo/generated/easytier_core.wasm", + "smoke:node": "pnpm build:demo && esbuild demo/node-smoke.ts --bundle --platform=node --format=esm --outfile=demo/generated/node-smoke.js && node demo/generated/node-smoke.js", + "check": "pnpm build && tsc --noEmit", + "test": "pnpm build && vitest run", + "prepack": "pnpm check" + }, + "dependencies": { + "@easytier/runtime": "workspace:0.1.0" + }, + "devDependencies": { + "@types/node": "22.18.1", + "esbuild": "0.25.9", + "typescript": "5.9.3", + "vitest": "2.1.9" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/easytier-js/browser/src/create.ts b/easytier-js/browser/src/create.ts new file mode 100644 index 00000000..5ac62645 --- /dev/null +++ b/easytier-js/browser/src/create.ts @@ -0,0 +1,42 @@ +import { + createEasyTierRuntime, + type RuntimeWebSocket, +} from "@easytier/runtime/adapter"; +import type { + EasyTierEvent, + EasyTierInstance, + EasyTierNetworkConfig, +} from "@easytier/runtime"; + +export interface BrowserEasyTierConfig extends EasyTierNetworkConfig { + ipv4: string; + peers: string | readonly string[]; +} + +export interface BrowserEasyTierOptions { + onEvent?: (event: EasyTierEvent) => void; +} + +export async function createEasyTierWithArtifact( + artifact: BufferSource, + config: BrowserEasyTierConfig, + options: BrowserEasyTierOptions = {}, +): Promise { + return createEasyTierRuntime({ + module: () => WebAssembly.compile(artifact), + config: { + profile: "browser", + instanceId: crypto.randomUUID(), + instanceName: config.instanceName ?? "easytier-browser", + networkName: config.networkName, + networkSecret: config.networkSecret, + encryption: config.encryption ?? true, + ipv4: config.ipv4, + peers: + typeof config.peers === "string" ? [config.peers] : [...config.peers], + }, + connectWebSocket: (url) => + new WebSocket(url) as unknown as RuntimeWebSocket, + onEvent: options.onEvent, + }); +} diff --git a/easytier-js/browser/src/index.ts b/easytier-js/browser/src/index.ts new file mode 100644 index 00000000..721d1dea --- /dev/null +++ b/easytier-js/browser/src/index.ts @@ -0,0 +1,39 @@ +import coreBytes from "./generated/easytier_core.wasm"; +import { createEasyTierWithArtifact } from "./create.js"; +import type { + EasyTierEvent, + EasyTierInstance, + EasyTierIpv4SocketAddress, + EasyTierNetworkConfig, + EasyTierOperationOptions, + EasyTierState, + EasyTierStatus, + EasyTierTcpListener, + EasyTierTcpReadResult, + EasyTierTcpStream, +} from "@easytier/runtime"; +import type { + BrowserEasyTierConfig, + BrowserEasyTierOptions, +} from "./create.js"; + +export type { + EasyTierEvent, + EasyTierInstance, + EasyTierIpv4SocketAddress, + EasyTierNetworkConfig, + EasyTierOperationOptions, + EasyTierState, + EasyTierStatus, + EasyTierTcpListener, + EasyTierTcpReadResult, + EasyTierTcpStream, +}; +export type { BrowserEasyTierConfig, BrowserEasyTierOptions }; + +export async function createEasyTier( + config: BrowserEasyTierConfig, + options: BrowserEasyTierOptions = {}, +): Promise { + return createEasyTierWithArtifact(coreBytes, config, options); +} diff --git a/easytier-js/browser/src/wasm.d.ts b/easytier-js/browser/src/wasm.d.ts new file mode 100644 index 00000000..6808d468 --- /dev/null +++ b/easytier-js/browser/src/wasm.d.ts @@ -0,0 +1,4 @@ +declare module "*.wasm" { + const bytes: Uint8Array; + export default bytes; +} diff --git a/easytier-js/browser/test/index.test.ts b/easytier-js/browser/test/index.test.ts new file mode 100644 index 00000000..fd3e6bad --- /dev/null +++ b/easytier-js/browser/test/index.test.ts @@ -0,0 +1,82 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const { createRuntime } = vi.hoisted(() => ({ + createRuntime: vi.fn(), +})); + +vi.mock("@easytier/runtime/adapter", () => ({ + createEasyTierRuntime: createRuntime, +})); + +import { createEasyTierWithArtifact } from "../src/create"; + +const EMPTY_WASM_MODULE = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]); + +describe("createEasyTier", () => { + afterEach(() => { + vi.unstubAllGlobals(); + createRuntime.mockReset(); + }); + + it("starts the browser profile from typed configuration", async () => { + const instance = { close: vi.fn() }; + let loadedModule: WebAssembly.Module | undefined; + createRuntime.mockImplementation(async (options) => { + loadedModule = await options.module(); + return instance; + }); + class MockWebSocket { + constructor(readonly url: string) {} + } + vi.stubGlobal("WebSocket", MockWebSocket); + + await expect( + createEasyTierWithArtifact( + EMPTY_WASM_MODULE, + { + networkName: "office", + networkSecret: "secret", + instanceName: "dashboard", + ipv4: "10.144.0.10/24", + peers: "wss://relay.example.com/", + }, + { onEvent: vi.fn() }, + ), + ).resolves.toBe(instance); + + expect(createRuntime).toHaveBeenCalledOnce(); + const options = createRuntime.mock.calls[0]?.[0]; + expect(options.config).toMatchObject({ + profile: "browser", + instanceName: "dashboard", + networkName: "office", + networkSecret: "secret", + encryption: true, + ipv4: "10.144.0.10/24", + peers: ["wss://relay.example.com/"], + }); + expect(loadedModule).toBeInstanceOf(WebAssembly.Module); + expect(options.connectWebSocket("wss://peer.example/").url).toBe( + "wss://peer.example/", + ); + }); + + it("fails before starting when the embedded artifact is invalid", async () => { + createRuntime.mockImplementation(async (options) => { + await options.module(); + }); + + await expect( + createEasyTierWithArtifact( + new Uint8Array([1, 2, 3]), + { + networkName: "office", + networkSecret: "secret", + ipv4: "10.144.0.10/24", + peers: ["wss://relay.example.com/"], + }, + ), + ).rejects.toThrow(); + expect(createRuntime).toHaveBeenCalledOnce(); + }); +}); diff --git a/easytier-js/browser/tsconfig.build.json b/easytier-js/browser/tsconfig.build.json new file mode 100644 index 00000000..ee1e72e2 --- /dev/null +++ b/easytier-js/browser/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "noEmit": false, + "outDir": "dist", + "rootDir": "src", + "types": [] + }, + "include": ["src/**/*.ts", "src/**/*.wasm"], + "exclude": ["test/**/*.ts", "demo/**/*.ts"] +} diff --git a/easytier-js/browser/tsconfig.json b/easytier-js/browser/tsconfig.json new file mode 100644 index 00000000..920c32c2 --- /dev/null +++ b/easytier-js/browser/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "types": ["@types/node"], + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "noUncheckedIndexedAccess": true, + "useDefineForClassFields": true + }, + "include": ["src/**/*.ts", "test/**/*.ts", "demo/**/*.ts"] +} diff --git a/easytier-js/cloudflare/.gitignore b/easytier-js/cloudflare/.gitignore new file mode 100644 index 00000000..a4b2eda3 --- /dev/null +++ b/easytier-js/cloudflare/.gitignore @@ -0,0 +1,4 @@ +src/generated/ +dist/ +.wrangler/ +node_modules/ diff --git a/easytier-js/cloudflare/README.md b/easytier-js/cloudflare/README.md new file mode 100644 index 00000000..09d32565 --- /dev/null +++ b/easytier-js/cloudflare/README.md @@ -0,0 +1,122 @@ +# `@easytier/cloudflare` + +Run an inbound EasyTier relay in a Cloudflare Durable Object. The package +includes the matching EasyTier WebAssembly artifact and owns WebSocket +admission, Guest lifecycle, and request routing. + +## Install + +```sh +pnpm add @easytier/cloudflare +pnpm add --save-dev wrangler +``` + +## Create the Worker + +```ts +import { createEasyTierCloudflare } from "@easytier/cloudflare"; + +const easytier = createEasyTierCloudflare({ + namespace: (env) => env.EASYTIER_CORE, + config: (env) => ({ + networkName: "office", + networkSecret: env.EASYTIER_NETWORK_SECRET, + instanceName: "edge-relay", + encryption: true, + }), +}); + +export class EasyTierCoreObject extends easytier.DurableObject {} +export default easytier; +``` + +`Env` is generated from `wrangler.jsonc` by `wrangler types`; applications do +not need to maintain a parallel binding interface by hand. + +## Configure Wrangler + +```jsonc +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "easytier-relay", + "main": "src/worker.ts", + "compatibility_date": "2026-09-05", + "compatibility_flags": ["nodejs_compat"], + "secrets": { + "required": ["EASYTIER_NETWORK_SECRET"] + }, + "durable_objects": { + "bindings": [ + { + "name": "EASYTIER_CORE", + "class_name": "EasyTierCoreObject" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["EasyTierCoreObject"] + } + ] +} +``` + +Generate the environment type after changing the configuration: + +```sh +pnpm wrangler types +``` + +For local development, put the secret in an untracked `.dev.vars` file: + +```dotenv +EASYTIER_NETWORK_SECRET=replace-with-a-local-secret +``` + +Then start the Worker and check the EasyTier Instance: + +```sh +pnpm wrangler dev --local +curl http://127.0.0.1:8787/health +``` + +The health response contains only the public Instance state and connection +count: + +```json +{"ok":true,"state":"running","connections":0} +``` + +Set the production secret interactively before the first deployment: + +```sh +pnpm wrangler secret put EASYTIER_NETWORK_SECRET +pnpm wrangler deploy +``` + +`createEasyTierCloudflare()` returns both the Durable Object base class and a +fetch handler. The one-line named subclass gives Wrangler a concrete class and +type to bind. Pass a custom `objectName` string or callback to route independent +EasyTier networks to different named objects; the default is `primary`. + +The Cloudflare Adapter is an inbound-only relay. Its public Interface does not +expose TOML, WebAssembly, Host Tunnel handles, JSPI scheduling, or the socket +admission sequence. `GET /health` returns only the Instance state and active +connection count. Other non-WebSocket paths return `404`. + +The Durable Object intentionally uses the standard WebSocket API rather than +hibernation. EasyTier's Wasm memory, Tokio executor, and peer graph are +in-memory state and cannot be reconstructed from socket attachments alone. + +For local development in the EasyTier repository: + +```sh +cd easytier-js +pnpm install +pnpm --filter @easytier/web-example build:packages +pnpm --filter @easytier/web-example dev:cloudflare +``` + +See the complete Browser-to-Cloudflare walkthrough in +[`easytier-js/examples/web`](https://github.com/EasyTier/EasyTier/tree/main/easytier-js/examples/web). diff --git a/easytier-js/cloudflare/example/env.d.ts b/easytier-js/cloudflare/example/env.d.ts new file mode 100644 index 00000000..2b53583d --- /dev/null +++ b/easytier-js/cloudflare/example/env.d.ts @@ -0,0 +1,9 @@ +interface Env { + EASYTIER_CORE: DurableObjectNamespace< + import("./worker").EasyTierCoreObject + >; + EASYTIER_NETWORK_NAME: string; + EASYTIER_NETWORK_SECRET: string; + EASYTIER_ENABLE_ENCRYPTION?: string; + EASYTIER_OBJECT_NAME?: string; +} diff --git a/easytier-js/cloudflare/example/worker.ts b/easytier-js/cloudflare/example/worker.ts new file mode 100644 index 00000000..a3eec313 --- /dev/null +++ b/easytier-js/cloudflare/example/worker.ts @@ -0,0 +1,16 @@ +import { createEasyTierCloudflare } from "../src/index"; + +const easytier = createEasyTierCloudflare({ + namespace: (env) => env.EASYTIER_CORE, + objectName: (request, env) => + env.EASYTIER_OBJECT_NAME ?? new URL(request.url).hostname, + config: (env) => ({ + networkName: env.EASYTIER_NETWORK_NAME, + networkSecret: env.EASYTIER_NETWORK_SECRET, + instanceName: "cloudflare-worker", + encryption: env.EASYTIER_ENABLE_ENCRYPTION !== "false", + }), +}); + +export class EasyTierCoreObject extends easytier.DurableObject {} +export default easytier; diff --git a/easytier-js/cloudflare/package.json b/easytier-js/cloudflare/package.json new file mode 100644 index 00000000..f15fde97 --- /dev/null +++ b/easytier-js/cloudflare/package.json @@ -0,0 +1,47 @@ +{ + "name": "@easytier/cloudflare", + "version": "0.1.0", + "description": "Run an EasyTier relay in a Cloudflare Durable Object", + "type": "module", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/EasyTier/EasyTier.git", + "directory": "easytier-js/cloudflare" + }, + "homepage": "https://github.com/EasyTier/EasyTier", + "bugs": "https://github.com/EasyTier/EasyTier/issues", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build:dependencies": "pnpm --filter @easytier/runtime build", + "build:wasm": "node ../runtime/scripts/build-wasm.mjs cloudflare src/generated/easytier_core.wasm && node ../runtime/scripts/validate-wasm.mjs cloudflare src/generated/easytier_core.wasm", + "build": "pnpm build:wasm && pnpm build:dependencies && tsc -p tsconfig.build.json && node ../runtime/scripts/copy-artifact.mjs src/generated/easytier_core.wasm dist/generated/easytier_core.wasm", + "check": "pnpm build && tsc --noEmit && wrangler deploy --dry-run", + "deploy": "pnpm build && wrangler deploy", + "test": "pnpm build && vitest run", + "prepack": "pnpm check" + }, + "dependencies": { + "@cloudflare/workers-types": "5.20260724.1", + "@easytier/runtime": "workspace:0.1.0" + }, + "devDependencies": { + "@types/node": "22.18.1", + "typescript": "5.9.3", + "vitest": "2.1.9", + "wrangler": "4.114.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/easytier-js/cloudflare/src/app.ts b/easytier-js/cloudflare/src/app.ts new file mode 100644 index 00000000..fb21401d --- /dev/null +++ b/easytier-js/cloudflare/src/app.ts @@ -0,0 +1,142 @@ +import { DurableObject } from "cloudflare:workers"; + +import { + createEasyTierRuntime, + type HostTunnelMetadata, + type RuntimeAdapter, + type RuntimeWebSocket, +} from "@easytier/runtime/adapter"; +import type { + CloudflareEasyTierOptions, + EasyTierCloudflareApplication, +} from "./types.js"; + +const INSTANCE_ID_STORAGE_KEY = "easytier.instance-id"; + +export function createCloudflareApplication( + module: WebAssembly.Module, + options: CloudflareEasyTierOptions, +): EasyTierCloudflareApplication { + const EasyTierDurableObject = class extends DurableObject { + private runtimePromise: Promise | undefined; + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const upgrade = request.headers.get("Upgrade"); + if (upgrade?.toLowerCase() !== "websocket") { + if (url.pathname !== "/health") { + return new Response("Not found", { status: 404 }); + } + try { + const status = await (await this.runtime()).status(); + return Response.json({ ok: true, ...status }); + } catch (error) { + console.error( + JSON.stringify({ + event: "easytier_cloudflare_health_failed", + error: String(error), + }), + ); + return Response.json( + { ok: false, state: "stopped", connections: 0 }, + { status: 503 }, + ); + } + } + + let runtime: RuntimeAdapter; + try { + runtime = await this.runtime(); + } catch (error) { + console.error( + JSON.stringify({ + event: "easytier_cloudflare_start_failed", + error: String(error), + }), + ); + return new Response("EasyTier relay unavailable", { status: 503 }); + } + if (!runtime.canAcceptWebSocket()) { + return new Response("WebSocket connection limit reached", { + status: 503, + }); + } + + const pair = new WebSocketPair(); + const client = pair[0]; + const server = pair[1]; + try { + await runtime.acceptWebSocket( + server as unknown as RuntimeWebSocket, + tunnelMetadata(request), + ); + } catch (error) { + console.error( + JSON.stringify({ + event: "easytier_cloudflare_admission_failed", + error: String(error), + }), + ); + return new Response("EasyTier relay unavailable", { status: 503 }); + } + + return new Response(null, { status: 101, webSocket: client }); + } + + private runtime(): Promise { + this.runtimePromise ??= this.initializeRuntime(); + return this.runtimePromise; + } + + private async initializeRuntime(): Promise { + const config = await options.config(this.env); + let instanceId = await this.ctx.storage.get( + INSTANCE_ID_STORAGE_KEY, + ); + if (instanceId === undefined) { + instanceId = crypto.randomUUID(); + await this.ctx.storage.put(INSTANCE_ID_STORAGE_KEY, instanceId); + } + return createEasyTierRuntime({ + module, + config: { + profile: "cloudflare", + instanceId, + instanceName: config.instanceName ?? "easytier-cloudflare", + networkName: config.networkName, + networkSecret: config.networkSecret, + encryption: config.encryption ?? true, + }, + }); + } + }; + + return { + DurableObject: EasyTierDurableObject, + async fetch(request, env, _context): Promise { + const objectName = + typeof options.objectName === "function" + ? options.objectName(request, env) + : (options.objectName ?? "primary"); + if (objectName.trim() === "") { + return new Response("EasyTier object name is empty", { status: 500 }); + } + return options.namespace(env).getByName(objectName).fetch(request); + }, + }; +} + +function tunnelMetadata(request: Request): HostTunnelMetadata { + const local = new URL(request.url); + local.protocol = local.protocol === "https:" ? "wss:" : "ws:"; + const remote = new URL(`wss://client.invalid/${crypto.randomUUID()}`); + const connectingIp = request.headers.get("CF-Connecting-IP"); + if (connectingIp !== null) { + remote.searchParams.set("ip", connectingIp); + } + return { + version: 1, + local_url: local.toString(), + remote_url: remote.toString(), + }; +} diff --git a/easytier-js/cloudflare/src/index.ts b/easytier-js/cloudflare/src/index.ts new file mode 100644 index 00000000..2f006323 --- /dev/null +++ b/easytier-js/cloudflare/src/index.ts @@ -0,0 +1,19 @@ +import coreModule from "./generated/easytier_core.wasm"; + +import { createCloudflareApplication } from "./app.js"; +import type { + CloudflareEasyTierOptions, + EasyTierCloudflareApplication, +} from "./types.js"; + +export type { + CloudflareEasyTierConfig, + CloudflareEasyTierOptions, + EasyTierCloudflareApplication, +} from "./types.js"; + +export function createEasyTierCloudflare( + options: CloudflareEasyTierOptions, +): EasyTierCloudflareApplication { + return createCloudflareApplication(coreModule, options); +} diff --git a/easytier-js/cloudflare/src/types.ts b/easytier-js/cloudflare/src/types.ts new file mode 100644 index 00000000..b8200722 --- /dev/null +++ b/easytier-js/cloudflare/src/types.ts @@ -0,0 +1,29 @@ +import type { DurableObject } from "cloudflare:workers"; +import type { EasyTierNetworkConfig } from "@easytier/runtime"; + +export type CloudflareEasyTierConfig = EasyTierNetworkConfig; + +export interface EasyTierDurableObjectNamespace { + getByName(name: string): { + fetch(request: Request): Promise; + }; +} + +export interface CloudflareEasyTierOptions { + namespace(env: Env): EasyTierDurableObjectNamespace; + config(env: Env): + | CloudflareEasyTierConfig + | Promise; + objectName?: string | ((request: Request, env: Env) => string); +} + +export type EasyTierDurableObjectClass = typeof DurableObject; + +export interface EasyTierCloudflareApplication { + readonly DurableObject: EasyTierDurableObjectClass; + fetch( + request: Request, + env: Env, + context: ExecutionContext, + ): Promise; +} diff --git a/easytier-js/cloudflare/src/wasm.d.ts b/easytier-js/cloudflare/src/wasm.d.ts new file mode 100644 index 00000000..aff905f9 --- /dev/null +++ b/easytier-js/cloudflare/src/wasm.d.ts @@ -0,0 +1,4 @@ +declare module "*.wasm" { + const module: WebAssembly.Module; + export default module; +} diff --git a/easytier-js/cloudflare/test/app.test.ts b/easytier-js/cloudflare/test/app.test.ts new file mode 100644 index 00000000..140c6077 --- /dev/null +++ b/easytier-js/cloudflare/test/app.test.ts @@ -0,0 +1,120 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { createRuntime } = vi.hoisted(() => ({ + createRuntime: vi.fn(), +})); + +vi.mock("cloudflare:workers", () => ({ + DurableObject: class { + protected readonly ctx: DurableObjectState; + protected readonly env: Env; + + constructor(ctx: DurableObjectState, env: Env) { + this.ctx = ctx; + this.env = env; + } + }, +})); + +vi.mock("@easytier/runtime/adapter", () => ({ + createEasyTierRuntime: createRuntime, +})); + +import { createCloudflareApplication } from "../src/app"; + +interface TestEnv { + namespace: DurableObjectNamespace; + secret: string; +} + +describe("createEasyTierCloudflare", () => { + beforeEach(() => { + createRuntime.mockReset(); + createRuntime.mockResolvedValue({ + status: vi.fn().mockResolvedValue({ + state: "running", + connections: 2, + }), + }); + }); + + it("routes requests through the configured named Durable Object", async () => { + const fetch = vi.fn().mockResolvedValue(new Response("ok")); + const getByName = vi.fn().mockReturnValue({ fetch }); + const namespace = { getByName } as unknown as DurableObjectNamespace; + const application = createCloudflareApplication( + {} as WebAssembly.Module, + { + namespace: (env: TestEnv) => env.namespace, + objectName: (request) => new URL(request.url).hostname, + config: (env) => ({ + networkName: "office", + networkSecret: env.secret, + }), + }, + ); + const request = new Request("https://relay.example.com/health"); + + await expect( + application.fetch( + request, + { namespace, secret: "secret" }, + {} as ExecutionContext, + ), + ).resolves.toHaveProperty("status", 200); + expect(getByName).toHaveBeenCalledWith("relay.example.com"); + expect(fetch).toHaveBeenCalledWith(request); + }); + + it("persists the hidden EasyTier instance identity across restarts", async () => { + const values = new Map(); + const storage = { + get: vi.fn(async (key: string) => values.get(key)), + put: vi.fn(async (key: string, value: unknown) => { + values.set(key, value); + }), + }; + const application = createCloudflareApplication( + {} as WebAssembly.Module, + { + namespace: (env: TestEnv) => env.namespace, + config: (env) => ({ + networkName: "office", + networkSecret: env.secret, + encryption: false, + }), + }, + ); + const state = { storage } as unknown as DurableObjectState; + const env = { + namespace: {} as DurableObjectNamespace, + secret: "do-not-log-this", + }; + class TestEasyTierCoreObject extends application.DurableObject {} + + const first = new TestEasyTierCoreObject(state, env); + const firstResponse = await first.fetch!( + new Request("https://relay.example.com/health"), + ); + expect(await firstResponse.json()).toEqual({ + ok: true, + state: "running", + connections: 2, + }); + + const second = new TestEasyTierCoreObject(state, env); + await second.fetch!(new Request("https://relay.example.com/health")); + + expect(createRuntime).toHaveBeenCalledTimes(2); + const firstConfig = createRuntime.mock.calls[0]?.[0].config; + const secondConfig = createRuntime.mock.calls[1]?.[0].config; + expect(firstConfig).toMatchObject({ + profile: "cloudflare", + networkName: "office", + networkSecret: "do-not-log-this", + encryption: false, + }); + expect(secondConfig.instanceId).toBe(firstConfig.instanceId); + expect(storage.put).toHaveBeenCalledTimes(1); + }); +}); diff --git a/easytier-js/cloudflare/tsconfig.build.json b/easytier-js/cloudflare/tsconfig.build.json new file mode 100644 index 00000000..5f2648fe --- /dev/null +++ b/easytier-js/cloudflare/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "noEmit": false, + "outDir": "dist", + "rootDir": "src", + "types": ["@cloudflare/workers-types"] + }, + "include": ["src/**/*.ts"], + "exclude": ["test/**/*.ts", "example/**/*.ts"] +} diff --git a/easytier-js/cloudflare/tsconfig.json b/easytier-js/cloudflare/tsconfig.json new file mode 100644 index 00000000..754e7973 --- /dev/null +++ b/easytier-js/cloudflare/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["@cloudflare/workers-types", "@types/node"], + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "noUncheckedIndexedAccess": true, + "useDefineForClassFields": true + }, + "include": ["src/**/*.ts", "test/**/*.ts", "example/**/*.ts"] +} diff --git a/easytier-js/cloudflare/wrangler.jsonc b/easytier-js/cloudflare/wrangler.jsonc new file mode 100644 index 00000000..f80bae16 --- /dev/null +++ b/easytier-js/cloudflare/wrangler.jsonc @@ -0,0 +1,38 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "wasi-relay", + "main": "example/worker.ts", + "compatibility_date": "2026-07-22", + "compatibility_flags": ["nodejs_compat"], + "rules": [ + { + "type": "CompiledWasm", + "globs": ["**/*.wasm"], + "fallthrough": true + } + ], + "vars": { + "EASYTIER_OBJECT_NAME": "primary", + "EASYTIER_NETWORK_NAME": "cf-wasi", + "EASYTIER_NETWORK_SECRET": "cf-wasi-test", + "EASYTIER_ENABLE_ENCRYPTION": "false" + }, + "durable_objects": { + "bindings": [ + { + "name": "EASYTIER_CORE", + "class_name": "EasyTierCoreObject" + } + ] + }, + "exports": { + "EasyTierCoreObject": { + "type": "durable-object", + "storage": "sqlite" + } + }, + "observability": { + "enabled": true, + "head_sampling_rate": 1 + } +} diff --git a/easytier-js/examples/web/.gitignore b/easytier-js/examples/web/.gitignore new file mode 100644 index 00000000..47eb8980 --- /dev/null +++ b/easytier-js/examples/web/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +browser/dist/ +cloudflare/.dev.vars* +!cloudflare/.dev.vars.example +cloudflare/.wrangler/ +cloudflare/worker-configuration.d.ts diff --git a/easytier-js/examples/web/README.md b/easytier-js/examples/web/README.md new file mode 100644 index 00000000..ec7299a6 --- /dev/null +++ b/easytier-js/examples/web/README.md @@ -0,0 +1,97 @@ +# EasyTier Browser and Cloudflare example + +This example connects `@easytier/browser` to an inbound +`@easytier/cloudflare` relay. Its application code imports only the two public +package entries; it does not use `@easytier/runtime`, Wasm loaders, TOML, ABI +handles, or Host Tunnel internals. + +## Prepare the repository build + +From the EasyTier repository root: + +```sh +cd easytier-js +pnpm install +pnpm --filter @easytier/web-example build:packages +``` + +The build starts from source and generates both required Wasm profiles. No +artifact from an earlier build is required. Published package consumers receive +the matching Wasm artifacts in the npm packages. + +## Set the local secret + +Copy the example file and replace its placeholder with a local secret: + +```sh +cp examples/web/cloudflare/.dev.vars.example \ + examples/web/cloudflare/.dev.vars +``` + +The `.dev.vars` file is ignored by Git. Enter the same value in the Browser UI. + +## Start the Cloudflare relay + +In one terminal, from the `easytier-js` workspace: + +```sh +pnpm --filter @easytier/web-example dev:cloudflare +``` + +Check that the EasyTier Instance is running: + +```sh +curl http://127.0.0.1:8787/health +``` + +Expected response: + +```json +{"ok":true,"state":"running","connections":0} +``` + +## Start the Browser application + +In another terminal: + +```sh +pnpm --filter @easytier/web-example dev:browser +``` + +Open the URL printed by Vite. Keep the default relay URL +`ws://127.0.0.1:8787/`, enter the secret from `.dev.vars`, and select +**Connect**. A successful session shows `peer_added`; the relay health response +then reports one active connection. + +The Browser and Worker must use the same network name and secret. Local +development uses `ws://`; a deployed Worker uses `wss://`. + +## Validate the example + +```sh +pnpm --filter @easytier/web-example check +``` + +This type-checks and bundles the Browser application, generates Cloudflare +binding types from `wrangler.jsonc`, type-checks the Worker, and runs a Wrangler +deployment dry-run. + +## Deploy the relay + +Authenticate Wrangler, set the secret interactively, and deploy: + +```sh +pnpm --filter @easytier/web-example exec wrangler login +pnpm --filter @easytier/web-example exec wrangler secret put \ + EASYTIER_NETWORK_SECRET --config cloudflare/wrangler.jsonc +pnpm --filter @easytier/web-example deploy:cloudflare +``` + +After deployment, enter the Worker URL in the Browser UI with the `wss://` +scheme. + +When the packages are published, an external application installs them with: + +```sh +pnpm add @easytier/browser @easytier/cloudflare +``` diff --git a/easytier-js/examples/web/browser/index.html b/easytier-js/examples/web/browser/index.html new file mode 100644 index 00000000..1337a72b --- /dev/null +++ b/easytier-js/examples/web/browser/index.html @@ -0,0 +1,89 @@ + + + + + + EasyTier Web example + + + +
+

EasyTier Browser → Cloudflare

+

Connect a browser EasyTier Instance to the local Worker relay.

+
+ + + + +
+ + +
+
+ Not connected +

+    
+ + + diff --git a/easytier-js/examples/web/browser/main.ts b/easytier-js/examples/web/browser/main.ts new file mode 100644 index 00000000..5a946fe0 --- /dev/null +++ b/easytier-js/examples/web/browser/main.ts @@ -0,0 +1,105 @@ +import { + createEasyTier, + type EasyTierInstance, +} from "@easytier/browser"; + +const form = requireElement("connection-form"); +const connectButton = requireElement("connect"); +const disconnectButton = requireElement("disconnect"); +const status = requireElement("status"); +const events = requireElement("events"); + +let instance: EasyTierInstance | undefined; + +form.addEventListener("submit", (event) => { + event.preventDefault(); + void connect(); +}); + +disconnectButton.addEventListener("click", () => { + void disconnect(); +}); + +async function connect(): Promise { + if (instance !== undefined) { + return; + } + + const fields = new FormData(form); + connectButton.disabled = true; + events.textContent = ""; + setStatus("Starting EasyTier…"); + + let peerConnected = false; + try { + instance = await createEasyTier( + { + networkName: requiredField(fields, "network"), + networkSecret: requiredField(fields, "secret"), + instanceName: "easytier-web-example", + ipv4: requiredField(fields, "ipv4"), + peers: requiredField(fields, "relay"), + }, + { + onEvent(event) { + appendEvent(event.kind, event.message); + if (event.kind === "peer_added") { + peerConnected = true; + setStatus("Connected to the EasyTier relay"); + } + }, + }, + ); + disconnectButton.disabled = false; + if (!peerConnected) { + setStatus("EasyTier is running; waiting for the relay…"); + } + } catch (error) { + instance = undefined; + connectButton.disabled = false; + setStatus(`Connection failed: ${String(error)}`); + } +} + +async function disconnect(): Promise { + const current = instance; + if (current === undefined) { + return; + } + + instance = undefined; + disconnectButton.disabled = true; + try { + await current.close(); + setStatus("Disconnected"); + } catch (error) { + setStatus(`Disconnect failed: ${String(error)}`); + } finally { + connectButton.disabled = false; + } +} + +function appendEvent(kind: string, message: string): void { + events.textContent += `${kind}: ${message}\n`; + events.scrollTop = events.scrollHeight; +} + +function requiredField(fields: FormData, name: string): string { + const value = fields.get(name); + if (typeof value !== "string" || value.trim() === "") { + throw new Error(`${name} is required`); + } + return value; +} + +function requireElement(id: string): T { + const element = document.getElementById(id); + if (element === null) { + throw new Error(`missing element: ${id}`); + } + return element as T; +} + +function setStatus(message: string): void { + status.value = message; +} diff --git a/easytier-js/examples/web/browser/tsconfig.json b/easytier-js/examples/web/browser/tsconfig.json new file mode 100644 index 00000000..95410a95 --- /dev/null +++ b/easytier-js/examples/web/browser/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "noEmit": true, + "noUncheckedIndexedAccess": true + }, + "include": ["main.ts"] +} diff --git a/easytier-js/examples/web/cloudflare/.dev.vars.example b/easytier-js/examples/web/cloudflare/.dev.vars.example new file mode 100644 index 00000000..b9a36a25 --- /dev/null +++ b/easytier-js/examples/web/cloudflare/.dev.vars.example @@ -0,0 +1 @@ +EASYTIER_NETWORK_SECRET=replace-with-a-local-secret diff --git a/easytier-js/examples/web/cloudflare/tsconfig.json b/easytier-js/examples/web/cloudflare/tsconfig.json new file mode 100644 index 00000000..215c9957 --- /dev/null +++ b/easytier-js/examples/web/cloudflare/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["./worker-configuration.d.ts"], + "strict": true, + "noEmit": true, + "noUncheckedIndexedAccess": true, + "useDefineForClassFields": true + }, + "include": ["worker.ts", "worker-configuration.d.ts"] +} diff --git a/easytier-js/examples/web/cloudflare/worker.ts b/easytier-js/examples/web/cloudflare/worker.ts new file mode 100644 index 00000000..e1649377 --- /dev/null +++ b/easytier-js/examples/web/cloudflare/worker.ts @@ -0,0 +1,14 @@ +import { createEasyTierCloudflare } from "@easytier/cloudflare"; + +const easytier = createEasyTierCloudflare({ + namespace: (env) => env.EASYTIER_CORE, + config: (env) => ({ + networkName: env.EASYTIER_NETWORK_NAME, + networkSecret: env.EASYTIER_NETWORK_SECRET, + instanceName: "easytier-web-example-relay", + encryption: true, + }), +}); + +export class EasyTierCoreObject extends easytier.DurableObject {} +export default easytier; diff --git a/easytier-js/examples/web/cloudflare/wrangler.jsonc b/easytier-js/examples/web/cloudflare/wrangler.jsonc new file mode 100644 index 00000000..dc299a30 --- /dev/null +++ b/easytier-js/examples/web/cloudflare/wrangler.jsonc @@ -0,0 +1,38 @@ +{ + "$schema": "../node_modules/wrangler/config-schema.json", + "name": "easytier-web-example", + "main": "worker.ts", + "compatibility_date": "2026-07-29", + "compatibility_flags": ["nodejs_compat"], + "rules": [ + { + "type": "CompiledWasm", + "globs": ["**/*.wasm"], + "fallthrough": true + } + ], + "vars": { + "EASYTIER_NETWORK_NAME": "easytier-web-example" + }, + "secrets": { + "required": ["EASYTIER_NETWORK_SECRET"] + }, + "durable_objects": { + "bindings": [ + { + "name": "EASYTIER_CORE", + "class_name": "EasyTierCoreObject" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["EasyTierCoreObject"] + } + ], + "observability": { + "enabled": true, + "head_sampling_rate": 1 + } +} diff --git a/easytier-js/examples/web/package.json b/easytier-js/examples/web/package.json new file mode 100644 index 00000000..8dc86ff3 --- /dev/null +++ b/easytier-js/examples/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "@easytier/web-example", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build:packages": "pnpm --filter @easytier/browser build && pnpm --filter @easytier/cloudflare build", + "build:browser": "tsc --noEmit -p browser/tsconfig.json && vite build browser", + "types:cloudflare": "wrangler types cloudflare/worker-configuration.d.ts --config cloudflare/wrangler.jsonc", + "check:cloudflare": "pnpm types:cloudflare && tsc --noEmit -p cloudflare/tsconfig.json && wrangler deploy --dry-run --config cloudflare/wrangler.jsonc", + "check": "pnpm build:browser && pnpm check:cloudflare", + "dev:browser": "vite browser --host 127.0.0.1", + "dev:cloudflare": "pnpm types:cloudflare && wrangler dev --local --config cloudflare/wrangler.jsonc", + "deploy:cloudflare": "pnpm types:cloudflare && wrangler deploy --config cloudflare/wrangler.jsonc" + }, + "dependencies": { + "@easytier/browser": "workspace:0.1.0", + "@easytier/cloudflare": "workspace:0.1.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "5.20260724.1", + "@types/node": "22.18.1", + "typescript": "5.9.3", + "vite": "5.4.21", + "wrangler": "4.114.0" + } +} diff --git a/easytier-js/package.json b/easytier-js/package.json new file mode 100644 index 00000000..c302a468 --- /dev/null +++ b/easytier-js/package.json @@ -0,0 +1,11 @@ +{ + "name": "@easytier/js-workspace", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "pnpm --filter @easytier/browser build && pnpm --filter @easytier/cloudflare build", + "test": "pnpm build && pnpm test:unit", + "test:unit": "pnpm --filter @easytier/runtime test && pnpm --filter @easytier/browser exec vitest run && pnpm --filter @easytier/cloudflare exec vitest run", + "check": "pnpm build && pnpm test:unit && pnpm --filter @easytier/web-example check" + } +} diff --git a/easytier-js/pnpm-lock.yaml b/easytier-js/pnpm-lock.yaml new file mode 100644 index 00000000..ce1ff271 --- /dev/null +++ b/easytier-js/pnpm-lock.yaml @@ -0,0 +1,2139 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + minimatch: 10.2.4 + +importers: + + .: {} + + browser: + dependencies: + '@easytier/runtime': + specifier: workspace:0.1.0 + version: link:../runtime + devDependencies: + '@types/node': + specifier: 22.18.1 + version: 22.18.1 + esbuild: + specifier: 0.25.9 + version: 0.25.9 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 2.1.9 + version: 2.1.9(@types/node@22.18.1) + + cloudflare: + dependencies: + '@cloudflare/workers-types': + specifier: 5.20260724.1 + version: 5.20260724.1 + '@easytier/runtime': + specifier: workspace:0.1.0 + version: link:../runtime + devDependencies: + '@types/node': + specifier: 22.18.1 + version: 22.18.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 2.1.9 + version: 2.1.9(@types/node@22.18.1) + wrangler: + specifier: 4.114.0 + version: 4.114.0(@cloudflare/workers-types@5.20260724.1) + + examples/web: + dependencies: + '@easytier/browser': + specifier: workspace:0.1.0 + version: link:../../browser + '@easytier/cloudflare': + specifier: workspace:0.1.0 + version: link:../../cloudflare + devDependencies: + '@cloudflare/workers-types': + specifier: 5.20260724.1 + version: 5.20260724.1 + '@types/node': + specifier: 22.18.1 + version: 22.18.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@22.18.1) + wrangler: + specifier: 4.114.0 + version: 4.114.0(@cloudflare/workers-types@5.20260724.1) + + runtime: + devDependencies: + '@types/node': + specifier: 22.18.1 + version: 22.18.1 + binaryen: + specifier: 131.0.0 + version: 131.0.0 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 2.1.9 + version: 2.1.9(@types/node@22.18.1) + +packages: + + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260722.1': + resolution: {integrity: sha512-vZOP8vIS3NwnuaO+gz0FZ7kIGeiO3bZmxV35Ph9zOXKSREhDFlH7wQ7mkCdhW3O4jnXsew+XT7b+DNEI2CcJGQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260722.1': + resolution: {integrity: sha512-EmIQymihDq6WNdER4+LF8Qn80yqayBUpJ+tkOO7wmY8pmgfyXjIUFNXotl21AHovTeu2seR7HdVUgeN/BilCWw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260722.1': + resolution: {integrity: sha512-jvZ3k9fxcnEn04s80CgIYxQfpOyAiz/8qC42DP8EBa9tR27qWyg9wmm31zIobVlrgBZn/+8NfdP73avRGcQOjQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260722.1': + resolution: {integrity: sha512-BOSB55SMNdy+DA5uj2WirgiNanpHGis5PVvXH1wSfvjRKr4JGgWK+EZzxz0RFUo6QjjQQC/NimEzNZ7va7jmKg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260722.1': + resolution: {integrity: sha512-sYM8YgUpKnRz2xjvdJLX1Ojzoi4MlA4gk8WTTExhGydjYB2UTs5NIbv0ZmpKgMoK9io3ixgmiW56ZnTbcWOdiA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@5.20260724.1': + resolution: {integrity: sha512-gl0brZ60JhkZU3INgr1jsxaFEiWf3AB9RsCjLTMwT5RKspWRUpsFd0wxwbys7gb8Ywkfq9tORhw0y9G+7bDUYw==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@esbuild/aix-ppc64@0.21.3': + resolution: {integrity: sha512-yTgnwQpFVYfvvo4SvRFB0SwrW8YjOxEoT7wfMT7Ol5v7v5LDNvSGo67aExmxOb87nQNeWPVvaGBNfQ7BXcrZ9w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.3': + resolution: {integrity: sha512-c+ty9necz3zB1Y+d/N+mC6KVVkGUUOcm4ZmT5i/Fk5arOaY3i6CA3P5wo/7+XzV8cb4GrI/Zjp8NuOQ9Lfsosw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.3': + resolution: {integrity: sha512-bviJOLMgurLJtF1/mAoJLxDZDL6oU5/ztMHnJQRejbJrSc9FFu0QoUoFhvi6qSKJEw9y5oGyvr9fuDtzJ30rNQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.3': + resolution: {integrity: sha512-JReHfYCRK3FVX4Ra+y5EBH1b9e16TV2OxrPAvzMsGeES0X2Ndm9ImQRI4Ket757vhc5XBOuGperw63upesclRw==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.3': + resolution: {integrity: sha512-U3fuQ0xNiAkXOmQ6w5dKpEvXQRSpHOnbw7gEfHCRXPeTKW9sBzVck6C5Yneb8LfJm0l6le4NQfkNPnWMSlTFUQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.3': + resolution: {integrity: sha512-3m1CEB7F07s19wmaMNI2KANLcnaqryJxO1fXHUV5j1rWn+wMxdUYoPyO2TnAbfRZdi7ADRwJClmOwgT13qlP3Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.3': + resolution: {integrity: sha512-fsNAAl5pU6wmKHq91cHWQT0Fz0vtyE1JauMzKotrwqIKAswwP5cpHUCxZNSTuA/JlqtScq20/5KZ+TxQdovU/g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.3': + resolution: {integrity: sha512-tci+UJ4zP5EGF4rp8XlZIdq1q1a/1h9XuronfxTMCNBslpCtmk97Q/5qqy1Mu4zIc0yswN/yP/BLX+NTUC1bXA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.3': + resolution: {integrity: sha512-vvG6R5g5ieB4eCJBQevyDMb31LMHthLpXTc2IGkFnPWS/GzIFDnaYFp558O+XybTmYrVjxnryru7QRleJvmZ6Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.3': + resolution: {integrity: sha512-f6kz2QpSuyHHg01cDawj0vkyMwuIvN62UAguQfnNVzbge2uWLhA7TCXOn83DT0ZvyJmBI943MItgTovUob36SQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.3': + resolution: {integrity: sha512-HjCWhH7K96Na+66TacDLJmOI9R8iDWDDiqe17C7znGvvE4sW1ECt9ly0AJ3dJH62jHyVqW9xpxZEU1jKdt+29A==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.3': + resolution: {integrity: sha512-BGpimEccmHBZRcAhdlRIxMp7x9PyJxUtj7apL2IuoG9VxvU/l/v1z015nFs7Si7tXUwEsvjc1rOJdZCn4QTU+Q==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.3': + resolution: {integrity: sha512-5rMOWkp7FQGtAH3QJddP4w3s47iT20hwftqdm7b+loe95o8JU8ro3qZbhgMRy0VuFU0DizymF1pBKkn3YHWtsw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.3': + resolution: {integrity: sha512-h0zj1ldel89V5sjPLo5H1SyMzp4VrgN1tPkN29TmjvO1/r0MuMRwJxL8QY05SmfsZRs6TF0c/IDH3u7XYYmbAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.3': + resolution: {integrity: sha512-dkAKcTsTJ+CRX6bnO17qDJbLoW37npd5gSNtSzjYQr0svghLJYGYB0NF1SNcU1vDcjXLYS5pO4qOW4YbFama4A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.3': + resolution: {integrity: sha512-vnD1YUkovEdnZWEuMmy2X2JmzsHQqPpZElXx6dxENcIwTu+Cu5ERax6+Ke1QsE814Zf3c6rxCfwQdCTQ7tPuXA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.3': + resolution: {integrity: sha512-IOXOIm9WaK7plL2gMhsWJd+l2bfrhfilv0uPTptoRoSb2p09RghhQQp9YY6ZJhk/kqmeRt6siRdMSLLwzuT0KQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.3': + resolution: {integrity: sha512-uTgCwsvQ5+vCQnqM//EfDSuomo2LhdWhFPS8VL8xKf+PKTCrcT/2kPPoWMTs22aB63MLdGMJiE3f1PHvCDmUOw==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.3': + resolution: {integrity: sha512-vNAkR17Ub2MgEud2Wag/OE4HTSI6zlb291UYzHez/psiKarp0J8PKGDnAhMBcHFoOHMXHfExzmjMojJNbAStrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.3': + resolution: {integrity: sha512-W8H9jlGiSBomkgmouaRoTXo49j4w4Kfbl6I1bIdO/vT0+0u4f20ko3ELzV3hPI6XV6JNBVX+8BC+ajHkvffIJA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.3': + resolution: {integrity: sha512-EjEomwyLSCg8Ag3LDILIqYCZAq/y3diJ04PnqGRgq8/4O3VNlXyMd54j/saShaN4h5o5mivOjAzmU6C3X4v0xw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.3': + resolution: {integrity: sha512-WGiE/GgbsEwR33++5rzjiYsKyHywE8QSZPF7Rfx9EBfK3Qn3xyR6IjyCr5Uk38Kg8fG4/2phN7sXp4NPWd3fcw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.3': + resolution: {integrity: sha512-xRxC0jaJWDLYvcUvjQmHCJSfMrgmUuvsoXgDeU/wTorQ1ngDdUBuFtgY3W1Pc5sprGAvZBtWdJX7RPg/iZZUqA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.2': + resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.2': + resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.2': + resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.1': + resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.1': + resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.1': + resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.1': + resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.1': + resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.1': + resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.1': + resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.1': + resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.2': + resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.2': + resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.2': + resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.2': + resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.2': + resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.2': + resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.2': + resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.2': + resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.2': + resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.2': + resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.2': + resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.2': + resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.2': + resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@rollup/rollup-android-arm-eabi@4.63.1': + resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.1': + resolution: {integrity: sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.1': + resolution: {integrity: sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.1': + resolution: {integrity: sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.1': + resolution: {integrity: sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.1': + resolution: {integrity: sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': + resolution: {integrity: sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.1': + resolution: {integrity: sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.1': + resolution: {integrity: sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.1': + resolution: {integrity: sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.1': + resolution: {integrity: sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.1': + resolution: {integrity: sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.1': + resolution: {integrity: sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.1': + resolution: {integrity: sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.1': + resolution: {integrity: sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.1': + resolution: {integrity: sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.1': + resolution: {integrity: sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.1': + resolution: {integrity: sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.1': + resolution: {integrity: sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.1': + resolution: {integrity: sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.1': + resolution: {integrity: sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.1': + resolution: {integrity: sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.1': + resolution: {integrity: sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.1': + resolution: {integrity: sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.1': + resolution: {integrity: sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==} + cpu: [x64] + os: [win32] + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.24': + resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.18.1': + resolution: {integrity: sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==} + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + binaryen@131.0.0: + resolution: {integrity: sha512-anblNezmBjfWGfOMvZq1j5MwhVnMs+gezqbDz4FBFwx6twDwOsQSed+E8i4UKmLYXb3r+f3HWmz7Lx5sx5qOyQ==} + hasBin: true + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.1.2: + resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} + engines: {node: '>=12'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + + esbuild@0.21.3: + resolution: {integrity: sha512-Kgq0/ZsAPzKrbOjCQcjoSmPoWhlcVnGAUo7jvaLHoxW1Drto0KGkR1xBNg2Cp43b9ImvxmPEJZ9xkfcnqPsfBw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.12: + resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + + miniflare@4.20260722.0: + resolution: {integrity: sha512-LW6ABMhCx/yIEFBLC/DO4yAhdm2T/G7jp7pr5T2kj895+CCIaHZqpMXdW9O6YE48LcYcCJChwWc8aEs1vpbTXw==} + engines: {node: '>=22.0.0'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.63.1: + resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.35.2: + resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} + engines: {node: '>=20.9.0'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + + supports-color@10.0.0: + resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + tinypool@1.0.1: + resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + workerd@1.20260722.1: + resolution: {integrity: sha512-NycKuc1x2onvsRfGGpM093vRlLFU2zHDAM0+APpccfg4+gZxDGCH27RmdDvkeBuoZyYqgLo3oAfF6re4mvC3vQ==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.114.0: + resolution: {integrity: sha512-M65P25t5UHA1TIJfgZXDcj+YzVobgKdRguM2QPz0xnxLFuOcuE3ErgllDht0iaho7MS4o0g/Bb4YK2+GT+bibg==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^5.20260722.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + +snapshots: + + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260722.1 + + '@cloudflare/workerd-darwin-64@1.20260722.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260722.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260722.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260722.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260722.1': + optional: true + + '@cloudflare/workers-types@5.20260724.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.21.3': + optional: true + + '@esbuild/aix-ppc64@0.25.9': + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.21.3': + optional: true + + '@esbuild/android-arm64@0.25.9': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.21.3': + optional: true + + '@esbuild/android-arm@0.25.9': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.21.3': + optional: true + + '@esbuild/android-x64@0.25.9': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.21.3': + optional: true + + '@esbuild/darwin-arm64@0.25.9': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.21.3': + optional: true + + '@esbuild/darwin-x64@0.25.9': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.21.3': + optional: true + + '@esbuild/freebsd-arm64@0.25.9': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.21.3': + optional: true + + '@esbuild/freebsd-x64@0.25.9': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.21.3': + optional: true + + '@esbuild/linux-arm64@0.25.9': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.21.3': + optional: true + + '@esbuild/linux-arm@0.25.9': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.21.3': + optional: true + + '@esbuild/linux-ia32@0.25.9': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.21.3': + optional: true + + '@esbuild/linux-loong64@0.25.9': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.21.3': + optional: true + + '@esbuild/linux-mips64el@0.25.9': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.21.3': + optional: true + + '@esbuild/linux-ppc64@0.25.9': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.21.3': + optional: true + + '@esbuild/linux-riscv64@0.25.9': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.21.3': + optional: true + + '@esbuild/linux-s390x@0.25.9': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.21.3': + optional: true + + '@esbuild/linux-x64@0.25.9': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.25.9': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.21.3': + optional: true + + '@esbuild/netbsd-x64@0.25.9': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.25.9': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.21.3': + optional: true + + '@esbuild/openbsd-x64@0.25.9': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.25.9': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.21.3': + optional: true + + '@esbuild/sunos-x64@0.25.9': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.21.3': + optional: true + + '@esbuild/win32-arm64@0.25.9': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.21.3': + optional: true + + '@esbuild/win32-ia32@0.25.9': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.21.3': + optional: true + + '@esbuild/win32-x64@0.25.9': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.1 + optional: true + + '@img/sharp-darwin-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.1 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.1': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.1': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.1': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + optional: true + + '@img/sharp-linux-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.1 + optional: true + + '@img/sharp-linux-arm@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.1 + optional: true + + '@img/sharp-linux-ppc64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.1 + optional: true + + '@img/sharp-linux-riscv64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.1 + optional: true + + '@img/sharp-linux-s390x@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.1 + optional: true + + '@img/sharp-linux-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.1 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + optional: true + + '@img/sharp-wasm32@0.35.2': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 + optional: true + + '@img/sharp-win32-arm64@0.35.2': + optional: true + + '@img/sharp-win32-ia32@0.35.2': + optional: true + + '@img/sharp-win32-x64@0.35.2': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.0.0 + + '@poppinss/exception@1.2.3': {} + + '@rollup/rollup-android-arm-eabi@4.63.1': + optional: true + + '@rollup/rollup-android-arm64@4.63.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.1': + optional: true + + '@rollup/rollup-darwin-x64@4.63.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.1': + optional: true + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.24': {} + + '@types/estree@1.0.9': {} + + '@types/node@22.18.1': + dependencies: + undici-types: 6.21.0 + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.1.2 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.18.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.12 + optionalDependencies: + vite: 5.4.21(@types/node@22.18.1) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.12 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + assertion-error@2.0.1: {} + + binaryen@131.0.0: {} + + blake3-wasm@2.1.5: {} + + cac@6.7.14: {} + + chai@5.1.2: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + cookie@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + detect-libc@2.1.2: {} + + error-stack-parser-es@1.0.5: {} + + es-module-lexer@1.5.4: {} + + esbuild@0.21.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.3 + '@esbuild/android-arm': 0.21.3 + '@esbuild/android-arm64': 0.21.3 + '@esbuild/android-x64': 0.21.3 + '@esbuild/darwin-arm64': 0.21.3 + '@esbuild/darwin-x64': 0.21.3 + '@esbuild/freebsd-arm64': 0.21.3 + '@esbuild/freebsd-x64': 0.21.3 + '@esbuild/linux-arm': 0.21.3 + '@esbuild/linux-arm64': 0.21.3 + '@esbuild/linux-ia32': 0.21.3 + '@esbuild/linux-loong64': 0.21.3 + '@esbuild/linux-mips64el': 0.21.3 + '@esbuild/linux-ppc64': 0.21.3 + '@esbuild/linux-riscv64': 0.21.3 + '@esbuild/linux-s390x': 0.21.3 + '@esbuild/linux-x64': 0.21.3 + '@esbuild/netbsd-x64': 0.21.3 + '@esbuild/openbsd-x64': 0.21.3 + '@esbuild/sunos-x64': 0.21.3 + '@esbuild/win32-arm64': 0.21.3 + '@esbuild/win32-ia32': 0.21.3 + '@esbuild/win32-x64': 0.21.3 + + esbuild@0.25.9: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fsevents@2.3.3: + optional: true + + kleur@4.1.5: {} + + loupe@3.2.1: {} + + magic-string@0.30.12: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + miniflare@4.20260722.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.2 + undici: 7.28.0 + workerd: 1.20260722.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + path-to-regexp@6.3.0: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.63.1: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.1 + '@rollup/rollup-android-arm64': 4.63.1 + '@rollup/rollup-darwin-arm64': 4.63.1 + '@rollup/rollup-darwin-x64': 4.63.1 + '@rollup/rollup-freebsd-arm64': 4.63.1 + '@rollup/rollup-freebsd-x64': 4.63.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.1 + '@rollup/rollup-linux-arm-musleabihf': 4.63.1 + '@rollup/rollup-linux-arm64-gnu': 4.63.1 + '@rollup/rollup-linux-arm64-musl': 4.63.1 + '@rollup/rollup-linux-loong64-gnu': 4.63.1 + '@rollup/rollup-linux-loong64-musl': 4.63.1 + '@rollup/rollup-linux-ppc64-gnu': 4.63.1 + '@rollup/rollup-linux-ppc64-musl': 4.63.1 + '@rollup/rollup-linux-riscv64-gnu': 4.63.1 + '@rollup/rollup-linux-riscv64-musl': 4.63.1 + '@rollup/rollup-linux-s390x-gnu': 4.63.1 + '@rollup/rollup-linux-x64-gnu': 4.63.1 + '@rollup/rollup-linux-x64-musl': 4.63.1 + '@rollup/rollup-openbsd-x64': 4.63.1 + '@rollup/rollup-openharmony-arm64': 4.63.1 + '@rollup/rollup-win32-arm64-msvc': 4.63.1 + '@rollup/rollup-win32-ia32-msvc': 4.63.1 + '@rollup/rollup-win32-x64-gnu': 4.63.1 + '@rollup/rollup-win32-x64-msvc': 4.63.1 + fsevents: 2.3.3 + + semver@7.8.5: {} + + sharp@0.35.2: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.2 + '@img/sharp-darwin-x64': 0.35.2 + '@img/sharp-freebsd-wasm32': 0.35.2 + '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-x64': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-linux-arm': 0.35.2 + '@img/sharp-linux-arm64': 0.35.2 + '@img/sharp-linux-ppc64': 0.35.2 + '@img/sharp-linux-riscv64': 0.35.2 + '@img/sharp-linux-s390x': 0.35.2 + '@img/sharp-linux-x64': 0.35.2 + '@img/sharp-linuxmusl-arm64': 0.35.2 + '@img/sharp-linuxmusl-x64': 0.35.2 + '@img/sharp-webcontainers-wasm32': 0.35.2 + '@img/sharp-win32-arm64': 0.35.2 + '@img/sharp-win32-ia32': 0.35.2 + '@img/sharp-win32-x64': 0.35.2 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.8.0: {} + + supports-color@10.0.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.1: {} + + tinypool@1.0.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + undici@7.28.0: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + vite-node@2.1.9(@types/node@22.18.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.5.4 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.18.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@22.18.1): + dependencies: + esbuild: 0.21.3 + postcss: 8.5.28 + rollup: 4.63.1 + optionalDependencies: + '@types/node': 22.18.1 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@22.18.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.18.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.1.2 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.12 + pathe: 1.1.2 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.1 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.18.1) + vite-node: 2.1.9(@types/node@22.18.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.18.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + workerd@1.20260722.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260722.1 + '@cloudflare/workerd-darwin-arm64': 1.20260722.1 + '@cloudflare/workerd-linux-64': 1.20260722.1 + '@cloudflare/workerd-linux-arm64': 1.20260722.1 + '@cloudflare/workerd-windows-64': 1.20260722.1 + + wrangler@4.114.0(@cloudflare/workers-types@5.20260724.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 4.20260722.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260722.1 + optionalDependencies: + '@cloudflare/workers-types': 5.20260724.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.21.0: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.24 + cookie: 1.0.2 + youch-core: 0.3.3 diff --git a/easytier-js/pnpm-workspace.yaml b/easytier-js/pnpm-workspace.yaml new file mode 100644 index 00000000..d9741d3a --- /dev/null +++ b/easytier-js/pnpm-workspace.yaml @@ -0,0 +1,15 @@ +packages: + - 'runtime' + - 'browser' + - 'cloudflare' + - 'examples/web' + +overrides: + minimatch: 10.2.4 + +allowBuilds: + esbuild: true + workerd: true + +minimumReleaseAgeExclude: + - '@cloudflare/workers-types@5.20260724.1' diff --git a/easytier-js/runtime/.gitignore b/easytier-js/runtime/.gitignore new file mode 100644 index 00000000..1eae0cf6 --- /dev/null +++ b/easytier-js/runtime/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/easytier-js/runtime/README.md b/easytier-js/runtime/README.md new file mode 100644 index 00000000..ec70096f --- /dev/null +++ b/easytier-js/runtime/README.md @@ -0,0 +1,16 @@ +# `@easytier/runtime` + +Shared WebAssembly runtime used by the official EasyTier Browser and +Cloudflare Adapters. Application code should normally install +`@easytier/browser` or `@easytier/cloudflare` instead. + +The package root contains only cross-platform EasyTier types. The +`@easytier/runtime/adapter` entry is intended for platform Adapter authors and +hides Guest memory, ABI handles, Host capability operations, and the operation +broker from application-facing Interfaces. + +The runtime, Browser Adapter, and Cloudflare Adapter use the same version. +Release automation must publish `@easytier/runtime` first, followed by +`@easytier/browser` and `@easytier/cloudflare`. Platform package manifests use +an exact workspace version, which `pnpm pack` rewrites to an exact registry +dependency. Consumers never run Cargo or an npm install hook. diff --git a/easytier-js/runtime/package.json b/easytier-js/runtime/package.json new file mode 100644 index 00000000..1feb5ea3 --- /dev/null +++ b/easytier-js/runtime/package.json @@ -0,0 +1,44 @@ +{ + "name": "@easytier/runtime", + "version": "0.1.0", + "description": "Shared WebAssembly runtime for EasyTier web hosts", + "type": "module", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/EasyTier/EasyTier.git", + "directory": "easytier-js/runtime" + }, + "homepage": "https://github.com/EasyTier/EasyTier", + "bugs": "https://github.com/EasyTier/EasyTier/issues", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./adapter": { + "types": "./dist/adapter.d.ts", + "import": "./dist/adapter.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "check": "tsc --noEmit && pnpm build", + "test": "vitest run", + "prepack": "pnpm check" + }, + "devDependencies": { + "@types/node": "22.18.1", + "binaryen": "131.0.0", + "typescript": "5.9.3", + "vitest": "2.1.9" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/easytier-js/runtime/scripts/build-wasm.mjs b/easytier-js/runtime/scripts/build-wasm.mjs new file mode 100644 index 00000000..db5466d3 --- /dev/null +++ b/easytier-js/runtime/scripts/build-wasm.mjs @@ -0,0 +1,107 @@ +import { mkdir } from "node:fs/promises"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const packageDirectory = path.dirname( + path.dirname(fileURLToPath(import.meta.url)), +); +const repositoryRoot = path.resolve(packageDirectory, "../.."); +const [profile, outputArgument] = process.argv.slice(2); +if (profile !== "browser" && profile !== "cloudflare") { + throw new Error( + "usage: build-wasm.mjs ", + ); +} +if (outputArgument === undefined) { + throw new Error("Wasm output file is required"); +} +const browserBuild = profile === "browser"; +const executableSuffix = process.platform === "win32" ? ".cmd" : ""; +const wasmOptExecutable = path.join( + packageDirectory, + "node_modules", + ".bin", + `wasm-opt${executableSuffix}`, +); +const artifact = path.join( + repositoryRoot, + "target/wasm32-wasip1/release/easytier_core.wasm", +); +const output = path.resolve(process.cwd(), outputArgument); +const outputDirectory = path.dirname(output); + +await new Promise((resolve, reject) => { + const cargo = spawn( + "cargo", + [ + "build", + "-p", + "easytier-core", + "--release", + "--target", + "wasm32-wasip1", + "--no-default-features", + "--features", + browserBuild + ? "wasm-host-tunnel-outbound,aes-gcm,proxy-smoltcp-stack" + : "wasm-host-tunnel,aes-gcm", + ], + { + cwd: repositoryRoot, + env: { + ...process.env, + CARGO_PROFILE_RELEASE_OPT_LEVEL: "z", + }, + stdio: "inherit", + }, + ); + cargo.once("error", reject); + cargo.once("exit", (code, signal) => { + if (code === 0) { + resolve(); + return; + } + reject( + new Error( + `cargo build failed (${signal === null ? `exit ${code}` : signal})`, + ), + ); + }); +}); + +await mkdir(outputDirectory, { recursive: true }); +await new Promise((resolve, reject) => { + const wasmOpt = spawn( + wasmOptExecutable, + [ + artifact, + "-Oz", + "--enable-bulk-memory", + "--enable-nontrapping-float-to-int", + "--strip-debug", + "--strip-producers", + "-o", + output, + ], + { + cwd: packageDirectory, + stdio: "inherit", + }, + ); + wasmOpt.once("error", reject); + wasmOpt.once("exit", (code, signal) => { + if (code === 0) { + resolve(); + return; + } + reject( + new Error( + `wasm-opt failed (${signal === null ? `exit ${code}` : signal})`, + ), + ); + }); +}); +console.log( + `optimized ${path.relative(process.cwd(), output)} (${profile})`, +); diff --git a/easytier-js/runtime/scripts/copy-artifact.mjs b/easytier-js/runtime/scripts/copy-artifact.mjs new file mode 100644 index 00000000..9f42f203 --- /dev/null +++ b/easytier-js/runtime/scripts/copy-artifact.mjs @@ -0,0 +1,12 @@ +import { copyFile, mkdir } from "node:fs/promises"; +import path from "node:path"; + +const [sourceArgument, destinationArgument] = process.argv.slice(2); +if (sourceArgument === undefined || destinationArgument === undefined) { + throw new Error("usage: copy-artifact.mjs "); +} + +const source = path.resolve(process.cwd(), sourceArgument); +const destination = path.resolve(process.cwd(), destinationArgument); +await mkdir(path.dirname(destination), { recursive: true }); +await copyFile(source, destination); diff --git a/easytier-js/runtime/scripts/validate-wasm.mjs b/easytier-js/runtime/scripts/validate-wasm.mjs new file mode 100644 index 00000000..5b61e4fd --- /dev/null +++ b/easytier-js/runtime/scripts/validate-wasm.mjs @@ -0,0 +1,45 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +const [profile, artifactArgument] = process.argv.slice(2); +if (profile !== "browser" && profile !== "cloudflare") { + throw new Error( + "usage: validate-wasm.mjs ", + ); +} +if (artifactArgument === undefined) { + throw new Error("Wasm artifact file is required"); +} + +const artifact = path.resolve(process.cwd(), artifactArgument); +const module = new WebAssembly.Module(await readFile(artifact)); +const imports = new Set( + WebAssembly.Module.imports(module) + .filter((entry) => entry.module === "easytier_host") + .map((entry) => entry.name), +); +const exports = new Set( + WebAssembly.Module.exports(module).map((entry) => entry.name), +); + +requireSymbol(exports, "easytier_host_tunnel_abi_version", "export"); +requireSymbol(exports, "easytier_instance_accept_tunnel", "export"); +if (profile === "browser") { + requireSymbol(imports, "start_tunnel_connect", "import"); + requireSymbol(exports, "easytier_data_plane_tcp_connect_submit", "export"); +} else { + rejectSymbol(imports, "start_tunnel_connect", "import"); + rejectSymbol(exports, "easytier_data_plane_tcp_connect_submit", "export"); +} + +function requireSymbol(symbols, name, kind) { + if (!symbols.has(name)) { + throw new Error(`${profile} artifact is missing ${kind} ${name}`); + } +} + +function rejectSymbol(symbols, name, kind) { + if (symbols.has(name)) { + throw new Error(`${profile} artifact unexpectedly contains ${kind} ${name}`); + } +} diff --git a/easytier-js/runtime/src/adapter.ts b/easytier-js/runtime/src/adapter.ts new file mode 100644 index 00000000..b6040f5c --- /dev/null +++ b/easytier-js/runtime/src/adapter.ts @@ -0,0 +1,119 @@ +import type { + EasyTierEvent, + EasyTierInstance, + EasyTierOperationOptions, + EasyTierState, + EasyTierStatus, + EasyTierTcpListener, + EasyTierTcpStream, +} from "./index.js"; +import { encodeRuntimeConfig, type RuntimeInstanceConfig } from "./config.js"; +import { + EasyTierRuntime, + type HostTunnelMetadata, +} from "./runtime.js"; +import type { RuntimeWebSocket } from "./websocket-host.js"; + +export type { + HostTunnelMetadata, + RuntimeInstanceConfig, + RuntimeWebSocket, +}; + +export interface RuntimeAdapter extends EasyTierInstance { + canAcceptWebSocket(): boolean; + acceptWebSocket( + socket: RuntimeWebSocket, + metadata: HostTunnelMetadata, + ): Promise; +} + +export interface CreateRuntimeOptions { + module: WebAssembly.Module | (() => Promise); + config: RuntimeInstanceConfig; + connectWebSocket?: (url: string) => RuntimeWebSocket; + onEvent?: (event: EasyTierEvent) => void; +} + +export async function createEasyTierRuntime( + options: CreateRuntimeOptions, +): Promise { + const config = encodeRuntimeConfig(options.config); + const module = + typeof options.module === "function" + ? await options.module() + : options.module; + const runtime = new EasyTierRuntime( + module, + config, + options.connectWebSocket, + options.onEvent, + ); + try { + await runtime.ready; + } catch (error) { + await runtime.stop().catch(() => {}); + throw new Error("failed to start EasyTier", { cause: error }); + } + + return { + connectTcp: ( + address: string, + operation?: EasyTierOperationOptions, + ): Promise => { + const target = parseTcpAddress(address); + return runtime.connectTcp(target.ipv4, target.port, operation?.timeout); + }, + listenTcp: ( + port: number, + operation?: EasyTierOperationOptions, + ): Promise => + runtime.bindTcp(port, operation?.timeout).then((listener) => ({ + localAddress: listener.localAddress, + accept: (acceptOptions) => + listener.accept(acceptOptions?.timeout), + close: () => listener.close(), + })), + status: async (): Promise => { + const health = await runtime.health(); + return { + state: stateName(health.state), + connections: health.connections, + }; + }, + close: () => runtime.stop(), + canAcceptWebSocket: () => runtime.canAcceptWebSocket(), + acceptWebSocket: (socket, metadata) => + runtime.acceptWebSocket(socket, metadata), + }; +} + +function parseTcpAddress(address: string): { ipv4: string; port: number } { + const separator = address.lastIndexOf(":"); + if (separator <= 0) { + throw new Error(`invalid IPv4 TCP address: ${address}`); + } + const ipv4 = address.slice(0, separator); + const port = Number(address.slice(separator + 1)); + if (!Number.isInteger(port) || port < 1 || port > 65_535) { + throw new Error(`invalid TCP port in address: ${address}`); + } + return { ipv4, port }; +} + +function stateName(state: number): EasyTierState { + switch (state) { + case 0: + return "created"; + case 1: + return "starting"; + case 2: + return "running"; + case 3: + return "stopping"; + case 4: + return "stopped"; + default: + throw new Error(`guest returned unknown instance state ${state}`); + } +} diff --git a/easytier-js/runtime/src/config.ts b/easytier-js/runtime/src/config.ts new file mode 100644 index 00000000..c839c7e9 --- /dev/null +++ b/easytier-js/runtime/src/config.ts @@ -0,0 +1,118 @@ +export type RuntimeProfile = "browser" | "cloudflare"; + +export interface RuntimeInstanceConfig { + profile: RuntimeProfile; + instanceId: string; + instanceName: string; + networkName: string; + networkSecret: string; + encryption: boolean; + ipv4?: string; + peers?: readonly string[]; +} + +export function encodeRuntimeConfig(config: RuntimeInstanceConfig): string { + requireText(config.instanceId, "instanceId"); + requireText(config.instanceName, "instanceName"); + requireText(config.networkName, "networkName"); + requireString(config.networkSecret, "networkSecret"); + + const lines = [ + `instance_id = ${quote(config.instanceId)}`, + `instance_name = ${quote(config.instanceName)}`, + ]; + + if (config.profile === "browser") { + const ipv4 = config.ipv4; + if (ipv4 === undefined || !isIpv4Prefix(ipv4)) { + throw new Error("ipv4 must be an IPv4 address with a network prefix"); + } + const peers = config.peers ?? []; + if (peers.length === 0) { + throw new Error("peers must contain at least one WebSocket URL"); + } + lines.push(`ipv4 = ${quote(ipv4)}`); + lines.push("listeners = []"); + lines.push(""); + lines.push("[network_identity]"); + lines.push(`network_name = ${quote(config.networkName)}`); + lines.push(`network_secret = ${quote(config.networkSecret)}`); + for (const peer of peers) { + lines.push(""); + lines.push("[[peer]]"); + lines.push(`uri = ${quoteWebSocketUrl(peer)}`); + } + lines.push(""); + lines.push("[flags]"); + lines.push("no_tun = true"); + lines.push("use_smoltcp = true"); + } else { + if (config.ipv4 !== undefined || (config.peers?.length ?? 0) !== 0) { + throw new Error("Cloudflare relay configuration cannot dial peers"); + } + lines.push("listeners = []"); + lines.push(""); + lines.push("[network_identity]"); + lines.push(`network_name = ${quote(config.networkName)}`); + lines.push(`network_secret = ${quote(config.networkSecret)}`); + lines.push(""); + lines.push("[flags]"); + lines.push("no_tun = false"); + lines.push("proxy_forward_by_system = true"); + } + + lines.push("disable_p2p = true"); + lines.push(`enable_encryption = ${config.encryption}`); + lines.push("bind_device = false"); + lines.push(""); + return lines.join("\n"); +} + +function requireText(value: string, field: string): void { + requireString(value, field); + if (value.trim() === "") { + throw new Error(`${field} must not be empty`); + } +} + +function requireString(value: string, field: string): void { + if (typeof value !== "string") { + throw new Error(`${field} must be a string`); + } +} + +function quote(value: string): string { + return JSON.stringify(value); +} + +function quoteWebSocketUrl(value: string): string { + requireText(value, "peer"); + let url: URL; + try { + url = new URL(value); + } catch { + throw new Error(`invalid WebSocket peer URL: ${value}`); + } + if (url.protocol !== "ws:" && url.protocol !== "wss:") { + throw new Error(`WebSocket peer must use ws:// or wss://: ${value}`); + } + return quote(url.toString()); +} + +function isIpv4Prefix(value: string): boolean { + const separator = value.lastIndexOf("/"); + if (separator <= 0) { + return false; + } + const prefix = Number(value.slice(separator + 1)); + if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) { + return false; + } + const octets = value.slice(0, separator).split(".").map(Number); + return ( + octets.length === 4 && + octets.every( + (octet) => Number.isInteger(octet) && octet >= 0 && octet <= 255, + ) + ); +} diff --git a/easytier-js/runtime/src/data-plane.ts b/easytier-js/runtime/src/data-plane.ts new file mode 100644 index 00000000..d61d34ed --- /dev/null +++ b/easytier-js/runtime/src/data-plane.ts @@ -0,0 +1,632 @@ +const DATA_PLANE_ABI_VERSION = 4; +const DATA_PLANE_TCP_CAPABILITY = 1n << 1n; +const COMPLETION_RECORD_LENGTH = 12; +const COMPLETION_BATCH_SIZE = 64; +const SOCKET_ADDRESS_LENGTH = 27; +const STREAM_RESULT_LENGTH = 8 + SOCKET_ADDRESS_LENGTH * 2; +const LISTENER_RESULT_LENGTH = 8 + SOCKET_ADDRESS_LENGTH; +const TCP_READ_METADATA_LENGTH = 1; +const INFINITE_TIMEOUT = 0xffff_ffff_ffff_ffffn; + +export type DataPlaneValue = number | bigint; + +export type DataPlaneExportName = + | "dataPlaneAbiVersion" + | "dataPlaneCapabilities" + | "dataPlaneTcpConnectSubmit" + | "dataPlaneTcpBindSubmit" + | "dataPlaneTcpAcceptSubmit" + | "dataPlaneTcpReadSubmit" + | "dataPlaneTcpWriteSubmit" + | "dataPlaneTcpShutdownWriteSubmit" + | "dataPlaneCompletionDrain" + | "dataPlaneResultSize" + | "dataPlaneTcpConnectResultTake" + | "dataPlaneTcpBindResultTake" + | "dataPlaneTcpAcceptResultTake" + | "dataPlaneTcpReadResultTake" + | "dataPlaneTcpWriteResultTake" + | "dataPlaneTcpShutdownWriteResultTake" + | "dataPlaneOperationFree" + | "dataPlaneResourceClose"; + +export interface DataPlaneBindings { + readonly instanceHandle: bigint; + call( + name: DataPlaneExportName, + parameters: DataPlaneValue[], + ): Promise; + allocate(length: number): Promise; + free(pointer: number): Promise; + copyIntoGuest(bytes: Uint8Array): Promise; + readGuest(pointer: number, length: number): Uint8Array; + instanceError(context: string): Promise; + runExclusive(operation: () => Promise): Promise; + drive(): Promise; +} + +interface PendingOperation { + kind: number; + resolve(value: T): void; + reject(reason: unknown): void; + take(operation: bigint): Promise; +} + +export interface TcpReadResult { + data: Uint8Array; + eof: boolean; +} + +export interface EasyTierIpv4SocketAddress { + ipv4: string; + port: number; +} + +export class EasyTierTcpStream { + private closed = false; + private writeShutdown: Promise | undefined; + + constructor( + private readonly dataPlane: EasyTierDataPlane, + private readonly resource: bigint, + readonly localAddress: EasyTierIpv4SocketAddress, + readonly peerAddress: EasyTierIpv4SocketAddress, + ) {} + + read(maxLength = 64 * 1024): Promise { + if (!Number.isInteger(maxLength) || maxLength <= 0) { + return Promise.reject(new Error("TCP read length must be a positive integer")); + } + this.requireOpen(); + return this.dataPlane.readTcp(this.resource, maxLength); + } + + write(data: Uint8Array): Promise { + this.requireOpen(); + if (this.writeShutdown !== undefined) { + return Promise.reject(new Error("TCP stream write side is shut down")); + } + return this.dataPlane.writeTcp(this.resource, data); + } + + shutdownWrite(): Promise { + this.requireOpen(); + if (this.writeShutdown !== undefined) { + return this.writeShutdown; + } + const shutdown = this.dataPlane + .shutdownTcpWrite(this.resource) + .catch((error: unknown) => { + this.writeShutdown = undefined; + throw error; + }); + this.writeShutdown = shutdown; + return shutdown; + } + + async close(): Promise { + if (this.closed) { + return; + } + this.closed = true; + await this.dataPlane.closeResource(this.resource); + } + + private requireOpen(): void { + if (this.closed) { + throw new Error("TCP stream is closed"); + } + } +} + +export class EasyTierTcpListener { + private closed = false; + + constructor( + private readonly dataPlane: EasyTierDataPlane, + private readonly resource: bigint, + readonly localAddress: EasyTierIpv4SocketAddress, + ) {} + + accept(timeoutMilliseconds?: number): Promise { + this.requireOpen(); + return this.dataPlane.acceptTcp(this.resource, timeoutMilliseconds); + } + + async close(): Promise { + if (this.closed) { + return; + } + this.closed = true; + await this.dataPlane.closeResource(this.resource); + } + + private requireOpen(): void { + if (this.closed) { + throw new Error("TCP listener is closed"); + } + } +} + +export class EasyTierDataPlane { + private readonly pending = new Map>(); + private stoppedError: Error | undefined; + + constructor(private readonly bindings: DataPlaneBindings) {} + + async initialize(): Promise { + const version = Number(await this.bindings.call("dataPlaneAbiVersion", [])); + if (version !== DATA_PLANE_ABI_VERSION) { + throw new Error( + `data-plane ABI ${version} is unsupported; expected ${DATA_PLANE_ABI_VERSION}`, + ); + } + const capabilities = BigInt( + await this.bindings.call("dataPlaneCapabilities", []), + ); + if ((capabilities & DATA_PLANE_TCP_CAPABILITY) === 0n) { + throw new Error("EasyTier guest does not provide the TCP data plane"); + } + } + + connectTcp( + ipv4: string, + port: number, + timeoutMilliseconds?: number, + ): Promise { + const address = encodeIpv4SocketAddress(ipv4, port); + return this.submit( + 1, + async (operationPointer) => { + const addressPointer = await this.bindings.copyIntoGuest(address); + try { + return Number( + await this.bindings.call("dataPlaneTcpConnectSubmit", [ + this.bindings.instanceHandle, + addressPointer, + timeoutValue(timeoutMilliseconds), + operationPointer, + ]), + ); + } finally { + await this.bindings.free(addressPointer); + } + }, + async (operation) => { + const pointer = await this.bindings.allocate(STREAM_RESULT_LENGTH); + try { + await this.requireSuccess( + Number( + await this.bindings.call("dataPlaneTcpConnectResultTake", [ + this.bindings.instanceHandle, + operation, + pointer, + ]), + ), + "TCP connect result", + ); + return this.streamFromResult( + this.bindings.readGuest(pointer, STREAM_RESULT_LENGTH), + ); + } finally { + await this.bindings.free(pointer); + } + }, + ); + } + + bindTcp( + localPort: number, + timeoutMilliseconds?: number, + ): Promise { + if (!Number.isInteger(localPort) || localPort < 0 || localPort > 65_535) { + throw new Error("TCP local port must be between 0 and 65535"); + } + return this.submit( + 2, + async (operationPointer) => + Number( + await this.bindings.call("dataPlaneTcpBindSubmit", [ + this.bindings.instanceHandle, + localPort, + timeoutValue(timeoutMilliseconds), + operationPointer, + ]), + ), + async (operation) => { + const pointer = await this.bindings.allocate(LISTENER_RESULT_LENGTH); + try { + await this.requireSuccess( + Number( + await this.bindings.call("dataPlaneTcpBindResultTake", [ + this.bindings.instanceHandle, + operation, + pointer, + ]), + ), + "TCP bind result", + ); + const result = this.bindings.readGuest( + pointer, + LISTENER_RESULT_LENGTH, + ); + return new EasyTierTcpListener( + this, + readU64(result, 0), + decodeIpv4SocketAddress(result, 8), + ); + } finally { + await this.bindings.free(pointer); + } + }, + ); + } + + acceptTcp( + listener: bigint, + timeoutMilliseconds?: number, + ): Promise { + return this.submit( + 3, + async (operationPointer) => + Number( + await this.bindings.call("dataPlaneTcpAcceptSubmit", [ + this.bindings.instanceHandle, + listener, + timeoutValue(timeoutMilliseconds), + operationPointer, + ]), + ), + async (operation) => { + const pointer = await this.bindings.allocate(STREAM_RESULT_LENGTH); + try { + await this.requireSuccess( + Number( + await this.bindings.call("dataPlaneTcpAcceptResultTake", [ + this.bindings.instanceHandle, + operation, + pointer, + ]), + ), + "TCP accept result", + ); + return this.streamFromResult( + this.bindings.readGuest(pointer, STREAM_RESULT_LENGTH), + ); + } finally { + await this.bindings.free(pointer); + } + }, + ); + } + + readTcp(resource: bigint, maxLength: number): Promise { + return this.submit( + 4, + async (operationPointer) => + Number( + await this.bindings.call("dataPlaneTcpReadSubmit", [ + this.bindings.instanceHandle, + resource, + maxLength, + operationPointer, + ]), + ), + async (operation) => { + const resultLength = Number( + await this.bindings.call("dataPlaneResultSize", [ + this.bindings.instanceHandle, + operation, + ]), + ); + await this.requireSuccess(resultLength, "TCP read result size"); + const dataPointer = + resultLength === 0 ? 0 : await this.bindings.allocate(resultLength); + const metadataPointer = await this.bindings.allocate( + TCP_READ_METADATA_LENGTH, + ); + try { + const readLength = Number( + await this.bindings.call("dataPlaneTcpReadResultTake", [ + this.bindings.instanceHandle, + operation, + dataPointer, + resultLength, + metadataPointer, + ]), + ); + await this.requireSuccess(readLength, "TCP read result"); + return { + data: + readLength === 0 + ? new Uint8Array() + : this.bindings.readGuest(dataPointer, readLength), + eof: this.bindings.readGuest(metadataPointer, 1)[0] === 1, + }; + } finally { + if (dataPointer !== 0) { + await this.bindings.free(dataPointer); + } + await this.bindings.free(metadataPointer); + } + }, + ); + } + + writeTcp(resource: bigint, data: Uint8Array): Promise { + return this.submit( + 5, + async (operationPointer) => { + const dataPointer = + data.byteLength === 0 ? 0 : await this.bindings.copyIntoGuest(data); + try { + return Number( + await this.bindings.call("dataPlaneTcpWriteSubmit", [ + this.bindings.instanceHandle, + resource, + dataPointer, + data.byteLength, + operationPointer, + ]), + ); + } finally { + if (dataPointer !== 0) { + await this.bindings.free(dataPointer); + } + } + }, + async (operation) => { + const written = Number( + await this.bindings.call("dataPlaneTcpWriteResultTake", [ + this.bindings.instanceHandle, + operation, + ]), + ); + await this.requireSuccess(written, "TCP write result"); + return written; + }, + ); + } + + shutdownTcpWrite(resource: bigint): Promise { + return this.submit( + 9, + async (operationPointer) => + Number( + await this.bindings.call("dataPlaneTcpShutdownWriteSubmit", [ + this.bindings.instanceHandle, + resource, + operationPointer, + ]), + ), + async (operation) => { + await this.requireSuccess( + Number( + await this.bindings.call("dataPlaneTcpShutdownWriteResultTake", [ + this.bindings.instanceHandle, + operation, + ]), + ), + "TCP write shutdown result", + ); + }, + ); + } + + shutdown(error = new Error("EasyTier runtime is stopped")): void { + if (this.stoppedError !== undefined) { + return; + } + this.stoppedError = error; + for (const operation of this.pending.values()) { + operation.reject(error); + } + this.pending.clear(); + } + + closeResource(resource: bigint): Promise { + if (this.stoppedError !== undefined) { + return Promise.reject(this.stoppedError); + } + return this.bindings.runExclusive(async () => { + const status = Number( + await this.bindings.call("dataPlaneResourceClose", [ + this.bindings.instanceHandle, + resource, + ]), + ); + await this.requireSuccess(status, "TCP close"); + }); + } + + async drainCompletions(): Promise { + if (this.stoppedError !== undefined) { + return; + } + if (this.pending.size === 0) { + return; + } + const length = COMPLETION_BATCH_SIZE * COMPLETION_RECORD_LENGTH; + const pointer = await this.bindings.allocate(length); + try { + for (;;) { + const count = Number( + await this.bindings.call("dataPlaneCompletionDrain", [ + this.bindings.instanceHandle, + pointer, + COMPLETION_BATCH_SIZE, + ]), + ); + await this.requireSuccess(count, "data-plane completion drain"); + const records = this.bindings.readGuest( + pointer, + count * COMPLETION_RECORD_LENGTH, + ); + for (let index = 0; index < count; index += 1) { + const offset = index * COMPLETION_RECORD_LENGTH; + const operation = readU64(records, offset); + const kind = readU16(records, offset + 8); + const status = readU16(records, offset + 10); + await this.complete(operation, kind, status); + } + if (count < COMPLETION_BATCH_SIZE) { + return; + } + } + } finally { + await this.bindings.free(pointer); + } + } + + private async submit( + kind: number, + submit: (operationPointer: number) => Promise, + take: (operation: bigint) => Promise, + ): Promise { + if (this.stoppedError !== undefined) { + throw this.stoppedError; + } + let resolve!: (value: T) => void; + let reject!: (reason: unknown) => void; + const completion = new Promise((resolveValue, rejectValue) => { + resolve = resolveValue; + reject = rejectValue; + }); + // A synchronously completed operation may reject while submission still + // owns the runtime lock. Mark it handled until this method returns it. + void completion.catch(() => {}); + await this.bindings.runExclusive(async () => { + const operationPointer = await this.bindings.allocate(8); + try { + const status = await submit(operationPointer); + await this.requireSuccess(status, "data-plane operation submission"); + const operation = readU64( + this.bindings.readGuest(operationPointer, 8), + 0, + ); + this.pending.set(operation, { + kind, + resolve: resolve as (value: unknown) => void, + reject, + take, + }); + } finally { + await this.bindings.free(operationPointer); + } + await this.bindings.drive(); + }); + return completion; + } + + private async complete( + operation: bigint, + kind: number, + completionStatus: number, + ): Promise { + const pending = this.pending.get(operation); + if (pending === undefined) { + await this.bindings.call("dataPlaneOperationFree", [ + this.bindings.instanceHandle, + operation, + ]); + return; + } + this.pending.delete(operation); + try { + if (kind !== pending.kind) { + throw new Error( + `data-plane operation ${operation} completed as kind ${kind}; expected ${pending.kind}`, + ); + } + const result = await pending.take(operation); + if (completionStatus !== 0) { + throw new Error( + `data-plane operation ${operation} failed with status ${completionStatus}`, + ); + } + pending.resolve(result); + } catch (error) { + pending.reject(error); + } + } + + private async requireSuccess(status: number, context: string): Promise { + if (status < 0) { + throw new Error(await this.bindings.instanceError(`${context} (${status})`)); + } + } + + private streamFromResult(result: Uint8Array): EasyTierTcpStream { + return new EasyTierTcpStream( + this, + readU64(result, 0), + decodeIpv4SocketAddress(result, 8), + decodeIpv4SocketAddress(result, 8 + SOCKET_ADDRESS_LENGTH), + ); + } +} + +function timeoutValue(milliseconds: number | undefined): bigint { + if (milliseconds === undefined) { + return INFINITE_TIMEOUT; + } + if (!Number.isSafeInteger(milliseconds) || milliseconds < 0) { + throw new Error("TCP timeout must be a non-negative safe integer"); + } + return BigInt(milliseconds); +} + +function encodeIpv4SocketAddress(ipv4: string, port: number): Uint8Array { + if (!Number.isInteger(port) || port < 1 || port > 65_535) { + throw new Error("TCP port must be between 1 and 65535"); + } + const octets = ipv4.split(".").map(Number); + if ( + octets.length !== 4 || + octets.some( + (octet) => + !Number.isInteger(octet) || octet < 0 || octet > 255, + ) + ) { + throw new Error(`invalid IPv4 address: ${ipv4}`); + } + const encoded = new Uint8Array(SOCKET_ADDRESS_LENGTH); + encoded[0] = 4; + encoded.set(octets, 1); + new DataView(encoded.buffer).setUint16(17, port, false); + return encoded; +} + +function decodeIpv4SocketAddress( + bytes: Uint8Array, + offset: number, +): EasyTierIpv4SocketAddress { + if (bytes[offset] !== 4) { + throw new Error(`guest returned a non-IPv4 socket address`); + } + const octets = bytes.subarray(offset + 1, offset + 5); + return { + ipv4: Array.from(octets).join("."), + port: new DataView( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength, + ).getUint16(offset + 17, false), + }; +} + +function readU16(bytes: Uint8Array, offset: number): number { + return new DataView( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength, + ).getUint16(offset, false); +} + +function readU64(bytes: Uint8Array, offset: number): bigint { + return new DataView( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength, + ).getBigUint64(offset, false); +} diff --git a/easytier-js/runtime/src/index.ts b/easytier-js/runtime/src/index.ts new file mode 100644 index 00000000..0eb76087 --- /dev/null +++ b/easytier-js/runtime/src/index.ts @@ -0,0 +1,66 @@ +export type EasyTierState = + | "created" + | "starting" + | "running" + | "stopping" + | "stopped"; + +export interface EasyTierNetworkConfig { + networkName: string; + networkSecret: string; + instanceName?: string; + encryption?: boolean; +} + +export interface EasyTierEvent { + kind: string; + message: string; +} + +export interface EasyTierStatus { + state: EasyTierState; + connections: number; +} + +export interface EasyTierOperationOptions { + /** Timeout in milliseconds. Omit to wait indefinitely. */ + timeout?: number; +} + +export interface EasyTierIpv4SocketAddress { + ipv4: string; + port: number; +} + +export interface EasyTierTcpReadResult { + data: Uint8Array; + eof: boolean; +} + +export interface EasyTierTcpStream { + readonly localAddress: EasyTierIpv4SocketAddress; + readonly peerAddress: EasyTierIpv4SocketAddress; + read(maxLength?: number): Promise; + write(data: Uint8Array): Promise; + shutdownWrite(): Promise; + close(): Promise; +} + +export interface EasyTierTcpListener { + readonly localAddress: EasyTierIpv4SocketAddress; + accept(options?: EasyTierOperationOptions): Promise; + close(): Promise; +} + +export interface EasyTierInstance { + connectTcp( + address: string, + options?: EasyTierOperationOptions, + ): Promise; + listenTcp( + port: number, + options?: EasyTierOperationOptions, + ): Promise; + status(): Promise; + close(): Promise; +} diff --git a/easytier-js/runtime/src/jspi.d.ts b/easytier-js/runtime/src/jspi.d.ts new file mode 100644 index 00000000..c710ba00 --- /dev/null +++ b/easytier-js/runtime/src/jspi.d.ts @@ -0,0 +1,13 @@ +declare namespace WebAssembly { + type JspiCallable = (...parameters: never[]) => unknown; + + class Suspending extends Function { + constructor( + callable: (...parameters: never[]) => Promise, + ); + } + + function promising( + callable: T, + ): (...parameters: Parameters) => Promise>>; +} diff --git a/easytier-js/runtime/src/runtime.ts b/easytier-js/runtime/src/runtime.ts new file mode 100644 index 00000000..e2b7f260 --- /dev/null +++ b/easytier-js/runtime/src/runtime.ts @@ -0,0 +1,806 @@ +import { WasiClock } from "./wasi-clock.js"; +import { WasiPreview1 } from "./wasi-preview1.js"; +import { + EasyTierDataPlane, + type DataPlaneExportName, + type EasyTierTcpListener, + type EasyTierTcpStream, +} from "./data-plane.js"; +import { + WebSocketHost, + type EasyTierCoreEvent, + type RuntimeWebSocket, + type WebSocketHostHealth, +} from "./websocket-host.js"; + +const CORE_CONFIG_VERSION = 14; +const HOST_TUNNEL_ABI_VERSION = 1; +const PACKET_SINK_HANDLE = 1n; +const EVENT_SINK_HANDLE = 2n; +const INSTANCE_RUNNING = 2; +const INSTANCE_STOPPED = 4; +const NO_DEADLINE = 0x7fff_ffff_ffff_ffffn; +const MAX_ZERO_DEADLINE_DRIVES = 64; +const MAX_START_DRIVES = 512; +const MAX_STOP_DRIVES = 512; + +export interface HostTunnelMetadata { + version: 1; + local_url: string; + remote_url: string; + resolved_remote_url?: string; +} + +type WasmValue = number | bigint; +type WasmCallable = (...parameters: WasmValue[]) => WasmValue; +type PromisingExport = (...parameters: WasmValue[]) => Promise; + +interface CoreExports { + memory: WebAssembly.Memory; + _start: WasmCallable; + easytier_buffer_alloc: WasmCallable; + easytier_buffer_free: WasmCallable; + easytier_instance_create: WasmCallable; + easytier_instance_start: WasmCallable; + easytier_instance_stop: WasmCallable; + easytier_instance_drive: WasmCallable; + easytier_instance_notify_completions: WasmCallable; + easytier_instance_state: WasmCallable; + easytier_instance_next_deadline_millis: WasmCallable; + easytier_instance_error_len: WasmCallable; + easytier_instance_error_copy: WasmCallable; + easytier_instance_drop: WasmCallable; + easytier_host_tunnel_abi_version: WasmCallable; + easytier_instance_accept_tunnel: WasmCallable; + easytier_data_plane_abi_version?: WasmCallable; + easytier_data_plane_capabilities?: WasmCallable; + easytier_data_plane_tcp_connect_submit?: WasmCallable; + easytier_data_plane_tcp_bind_submit?: WasmCallable; + easytier_data_plane_tcp_accept_submit?: WasmCallable; + easytier_data_plane_tcp_read_submit?: WasmCallable; + easytier_data_plane_tcp_write_submit?: WasmCallable; + easytier_data_plane_tcp_shutdown_write_submit?: WasmCallable; + easytier_data_plane_completion_drain?: WasmCallable; + easytier_data_plane_result_size?: WasmCallable; + easytier_data_plane_tcp_connect_result_take?: WasmCallable; + easytier_data_plane_tcp_bind_result_take?: WasmCallable; + easytier_data_plane_tcp_accept_result_take?: WasmCallable; + easytier_data_plane_tcp_read_result_take?: WasmCallable; + easytier_data_plane_tcp_write_result_take?: WasmCallable; + easytier_data_plane_tcp_shutdown_write_result_take?: WasmCallable; + easytier_data_plane_operation_free?: WasmCallable; + easytier_data_plane_resource_close?: WasmCallable; +} + +interface PromisingCoreExports { + start: PromisingExport; + bufferAlloc: PromisingExport; + bufferFree: PromisingExport; + instanceCreate: PromisingExport; + instanceStart: PromisingExport; + instanceStop: PromisingExport; + instanceDrive: PromisingExport; + notifyCompletions: PromisingExport; + instanceState: PromisingExport; + nextDeadlineMillis: PromisingExport; + errorLength: PromisingExport; + errorCopy: PromisingExport; + instanceDrop: PromisingExport; + tunnelAbiVersion: PromisingExport; + acceptTunnel: PromisingExport; + dataPlaneAbiVersion?: PromisingExport; + dataPlaneCapabilities?: PromisingExport; + dataPlaneTcpConnectSubmit?: PromisingExport; + dataPlaneTcpBindSubmit?: PromisingExport; + dataPlaneTcpAcceptSubmit?: PromisingExport; + dataPlaneTcpReadSubmit?: PromisingExport; + dataPlaneTcpWriteSubmit?: PromisingExport; + dataPlaneTcpShutdownWriteSubmit?: PromisingExport; + dataPlaneCompletionDrain?: PromisingExport; + dataPlaneResultSize?: PromisingExport; + dataPlaneTcpConnectResultTake?: PromisingExport; + dataPlaneTcpBindResultTake?: PromisingExport; + dataPlaneTcpAcceptResultTake?: PromisingExport; + dataPlaneTcpReadResultTake?: PromisingExport; + dataPlaneTcpWriteResultTake?: PromisingExport; + dataPlaneTcpShutdownWriteResultTake?: PromisingExport; + dataPlaneOperationFree?: PromisingExport; + dataPlaneResourceClose?: PromisingExport; +} + +export interface CoreHealth extends WebSocketHostHealth { + state: number; + tunnelAbiVersion: number; +} + +export class EasyTierRuntime { + private readonly host: WebSocketHost; + readonly ready: Promise; + + private instanceHandle = 0n; + private exports: PromisingCoreExports | undefined; + private memory: WebAssembly.Memory | undefined; + private clock: WasiClock | undefined; + private serial = Promise.resolve(); + private timer: ReturnType | undefined; + private timerGeneration = 0; + private timerDueAt: number | undefined; + private armRequest = 0; + private pumpQueued = false; + private completionRequested = false; + private lastError: unknown; + private dataPlane: EasyTierDataPlane | undefined; + private stopping = false; + private stopPromise: Promise | undefined; + + constructor( + private readonly module: WebAssembly.Module, + private readonly config: string, + outboundWebSocketFactory?: (url: string) => RuntimeWebSocket, + onEvent?: (event: EasyTierCoreEvent) => void, + ) { + this.host = new WebSocketHost(outboundWebSocketFactory, onEvent); + this.ready = this.enqueue(() => this.initialize()); + this.host.setWakeGuest(() => this.requestHostCompletion()); + } + + canAcceptWebSocket(): boolean { + return !this.stopping && this.host.canAccept(); + } + + async acceptWebSocket( + socket: RuntimeWebSocket, + metadata: HostTunnelMetadata, + ): Promise { + await this.ready; + this.requireRunning(); + if (!this.host.canAccept()) { + throw new Error("WebSocket connection limit reached"); + } + + socket.binaryType = "arraybuffer"; + const handle = this.host.register(socket); + socket.addEventListener("message", (event) => { + if (event.data === undefined) { + this.host.remoteError(handle); + return; + } + this.host.receive(handle, event.data); + }); + socket.addEventListener("close", () => { + this.host.remoteClose(handle); + }); + socket.addEventListener("error", () => { + this.host.remoteError(handle); + }); + + try { + socket.accept?.(); + await this.attachTunnel(handle, metadata); + } catch (error) { + this.host.reject(handle); + throw error; + } + } + + async attachTunnel( + tunnelHandle: bigint, + metadata: HostTunnelMetadata, + ): Promise { + await this.ready; + this.requireRunning(); + await this.enqueue(async () => { + this.requireRunning(); + const encoded = new TextEncoder().encode(JSON.stringify(metadata)); + const pointer = await this.copyIntoGuest(encoded); + let transferred = false; + try { + try { + const status = Number( + await this.call("acceptTunnel", [ + this.instanceHandle, + tunnelHandle, + pointer, + encoded.byteLength, + ]), + ); + if (status !== 0) { + throw new Error( + await this.instanceError(`Tunnel attach (${status})`), + ); + } + this.host.transferToGuest(tunnelHandle); + transferred = true; + } finally { + await this.call("bufferFree", [pointer]); + } + await this.driveUntilIdle(); + this.armNextDrive(); + } catch (error) { + if (transferred) { + this.host.abort(tunnelHandle, "EasyTier admission failed"); + } + throw error; + } + }); + } + + async health(): Promise { + await this.ready; + return this.enqueue(async () => ({ + state: Number( + await this.call("instanceState", [this.instanceHandle]), + ), + tunnelAbiVersion: Number(await this.call("tunnelAbiVersion", [])), + ...this.host.health(), + })); + } + + async connectTcp( + ipv4: string, + port: number, + timeoutMilliseconds?: number, + ): Promise { + await this.ready; + this.requireRunning(); + if (this.dataPlane === undefined) { + throw new Error("this EasyTier guest does not include the data plane"); + } + return this.dataPlane.connectTcp(ipv4, port, timeoutMilliseconds); + } + + async bindTcp( + localPort: number, + timeoutMilliseconds?: number, + ): Promise { + await this.ready; + this.requireRunning(); + if (this.dataPlane === undefined) { + throw new Error("this EasyTier guest does not include the data plane"); + } + return this.dataPlane.bindTcp(localPort, timeoutMilliseconds); + } + + stop(): Promise { + if (this.stopPromise !== undefined) { + return this.stopPromise; + } + this.stopping = true; + this.armRequest += 1; + this.cancelTimer(); + this.clock?.interrupt(); + const stop = this.ready.then( + () => this.enqueue(() => this.stopInstance()), + (error: unknown) => { + this.releaseRuntimeResources(); + throw error; + }, + ); + this.stopPromise = stop; + return stop; + } + + private async initialize(): Promise { + if ( + typeof WebAssembly.Suspending !== "function" || + typeof WebAssembly.promising !== "function" + ) { + throw new Error("WebAssembly JSPI support is required"); + } + + let instance: WebAssembly.Instance | undefined; + const clock = new WasiClock(() => { + if (instance === undefined) { + throw new Error("Wasm instance is not initialized"); + } + return (instance.exports as unknown as CoreExports).memory; + }); + const wasi = new WasiPreview1(clock); + + instance = new WebAssembly.Instance(this.module, { + wasi_snapshot_preview1: wasi.imports, + easytier_host: this.host.imports, + }); + const raw = instance.exports as unknown as CoreExports; + this.memory = raw.memory; + this.host.bindMemory(raw.memory); + wasi.bindMemory(raw.memory); + this.clock = clock; + this.exports = this.wrapExports(raw); + await this.call("start", []); + + const abiVersion = Number(await this.call("tunnelAbiVersion", [])); + if (abiVersion !== HOST_TUNNEL_ABI_VERSION) { + throw new Error( + `host tunnel ABI ${abiVersion} is unsupported; expected ${HOST_TUNNEL_ABI_VERSION}`, + ); + } + const createConfig = new TextEncoder().encode( + JSON.stringify({ + version: CORE_CONFIG_VERSION, + config: this.config, + environment: { + public_ipv4: null, + interface_ipv4s: [], + public_ipv6: null, + interface_ipv6s: [], + mapped_listeners: [], + local_ips: [], + protected_tcp_ports: [], + preferred_ipv6_sources: [], + }, + }), + ); + const configPointer = await this.copyIntoGuest(createConfig); + try { + this.instanceHandle = BigInt( + await this.call("instanceCreate", [ + configPointer, + createConfig.byteLength, + PACKET_SINK_HANDLE, + EVENT_SINK_HANDLE, + ]), + ); + } finally { + await this.call("bufferFree", [configPointer]); + } + if (this.instanceHandle === 0n) { + throw new Error(await this.instanceError("core instance creation")); + } + if (this.exports.dataPlaneAbiVersion !== undefined) { + this.dataPlane = new EasyTierDataPlane({ + instanceHandle: this.instanceHandle, + call: (name, parameters) => this.call(name, parameters), + allocate: async (length) => { + const pointer = Number(await this.call("bufferAlloc", [length])); + if (pointer === 0) { + throw new Error("guest buffer allocation failed"); + } + return pointer; + }, + free: async (pointer) => { + await this.call("bufferFree", [pointer]); + }, + copyIntoGuest: (bytes) => this.copyIntoGuest(bytes), + readGuest: (pointer, length) => + new Uint8Array(this.requireMemory().buffer, pointer, length).slice(), + instanceError: (context) => this.instanceError(context), + runExclusive: (operation) => this.enqueue(operation), + drive: async () => { + await this.driveUntilIdle(); + this.armNextDrive(); + }, + }); + await this.dataPlane.initialize(); + } + const startStatus = Number( + await this.call("instanceStart", [this.instanceHandle]), + ); + if (startStatus !== 0) { + throw new Error(await this.instanceError(`core start (${startStatus})`)); + } + let lastState = 0; + let lastDeadline = NO_DEADLINE; + for (let attempt = 0; attempt < MAX_START_DRIVES; attempt += 1) { + lastState = Number( + await this.call("instanceDrive", [this.instanceHandle]), + ); + if (lastState === INSTANCE_RUNNING) { + this.armNextDrive(); + console.log( + JSON.stringify({ + event: "easytier_core_started", + tunnelAbiVersion: abiVersion, + startupDrives: attempt + 1, + }), + ); + return; + } + if (lastState < 0) { + throw new Error( + await this.instanceError(`core drive (${lastState})`), + ); + } + lastDeadline = BigInt( + await this.call("nextDeadlineMillis", [this.instanceHandle]), + ); + if (lastDeadline > 0n && lastDeadline !== NO_DEADLINE) { + const wait = Number( + lastDeadline > 1000n ? 1000n : lastDeadline, + ); + await new Promise((resolve) => setTimeout(resolve, wait)); + this.clock?.advanceMillis(wait); + } + } + throw new Error( + `core did not reach running state: state=${lastState}, deadline=${lastDeadline}`, + ); + } + + private async stopInstance(): Promise { + let failure: unknown; + try { + const stopStatus = Number( + await this.call("instanceStop", [this.instanceHandle]), + ); + if (stopStatus !== 0) { + throw new Error( + await this.instanceError(`core stop (${stopStatus})`), + ); + } + + let lastState = 0; + let lastDeadline = NO_DEADLINE; + for (let attempt = 0; attempt < MAX_STOP_DRIVES; attempt += 1) { + if (this.completionRequested) { + this.completionRequested = false; + const notifyStatus = Number( + await this.call("notifyCompletions", [this.instanceHandle]), + ); + if (notifyStatus !== 0) { + throw new Error( + await this.instanceError( + `completion notification (${notifyStatus})`, + ), + ); + } + } + lastState = Number( + await this.call("instanceDrive", [this.instanceHandle]), + ); + if (lastState < 0) { + throw new Error( + await this.instanceError(`core drive (${lastState})`), + ); + } + await this.dataPlane?.drainCompletions(); + if (lastState === INSTANCE_STOPPED) { + break; + } + lastDeadline = BigInt( + await this.call("nextDeadlineMillis", [this.instanceHandle]), + ); + if (lastDeadline > 0n && lastDeadline !== NO_DEADLINE) { + const wait = Number( + lastDeadline > 1000n ? 1000n : lastDeadline, + ); + await new Promise((resolve) => setTimeout(resolve, wait)); + this.clock?.advanceMillis(wait); + } else if (lastDeadline === NO_DEADLINE) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + } + if (lastState !== INSTANCE_STOPPED) { + throw new Error( + `core did not stop: state=${lastState}, deadline=${lastDeadline}`, + ); + } + } catch (error) { + failure = error; + } + + try { + if (this.instanceHandle !== 0n) { + const dropStatus = Number( + await this.call("instanceDrop", [this.instanceHandle]), + ); + if (dropStatus !== 0) { + throw new Error( + await this.instanceError(`core drop (${dropStatus})`), + ); + } + } + } catch (error) { + failure ??= error; + } finally { + this.releaseRuntimeResources(); + } + + if (failure !== undefined) { + throw failure; + } + } + + private releaseRuntimeResources(): void { + this.cancelTimer(); + this.clock?.interrupt(); + this.dataPlane?.shutdown(); + this.dataPlane = undefined; + this.host.shutdown(); + this.instanceHandle = 0n; + this.exports = undefined; + this.memory = undefined; + this.clock = undefined; + this.completionRequested = false; + this.pumpQueued = false; + } + + private requestHostCompletion(): void { + this.completionRequested = true; + this.clock?.interrupt(); + if (this.stopping) { + return; + } + this.queuePump(); + } + + private queuePump(): void { + if (this.pumpQueued || this.stopping) { + return; + } + this.pumpQueued = true; + void this.ready + .then(() => + this.enqueue(async () => { + this.pumpQueued = false; + if (this.stopping) { + return; + } + if (this.completionRequested) { + this.completionRequested = false; + const status = Number( + await this.call("notifyCompletions", [this.instanceHandle]), + ); + if (status !== 0) { + throw new Error( + await this.instanceError(`completion notification (${status})`), + ); + } + } + await this.driveUntilIdle(); + this.armNextDrive(); + }), + ) + .catch((error: unknown) => { + this.lastError = error; + this.pumpQueued = false; + console.error( + JSON.stringify({ + event: "easytier_core_pump_failed", + error: String(error), + }), + ); + }); + } + + private async driveUntilIdle(): Promise { + for (let turn = 0; turn < MAX_ZERO_DEADLINE_DRIVES; turn += 1) { + const state = Number( + await this.call("instanceDrive", [this.instanceHandle]), + ); + if (state < 0) { + throw new Error(await this.instanceError(`core drive (${state})`)); + } + await this.dataPlane?.drainCompletions(); + const deadline = BigInt( + await this.call("nextDeadlineMillis", [this.instanceHandle]), + ); + if (deadline !== 0n) { + return; + } + } + } + + private armNextDrive(): void { + if (this.stopping) { + this.cancelTimer(); + return; + } + const request = ++this.armRequest; + void this.enqueue(async () => { + if (request !== this.armRequest) { + return; + } + const deadline = BigInt( + await this.call("nextDeadlineMillis", [this.instanceHandle]), + ); + if (request !== this.armRequest) { + return; + } + if (deadline === NO_DEADLINE) { + this.cancelTimer(); + return; + } + const milliseconds = Number( + deadline > 2_147_483_647n ? 2_147_483_647n : deadline, + ); + const dueAt = Date.now() + milliseconds; + if ( + this.timer !== undefined && + this.timerDueAt !== undefined && + this.timerDueAt <= dueAt + ) { + return; + } + if (this.timer !== undefined) { + clearTimeout(this.timer); + } + const generation = ++this.timerGeneration; + const timer = setTimeout(() => { + if (generation !== this.timerGeneration) { + return; + } + if (this.timer === timer) { + this.timer = undefined; + this.timerDueAt = undefined; + } + this.clock?.syncWallTime(); + this.queuePump(); + }, milliseconds); + this.timer = timer; + this.timerDueAt = dueAt; + }).catch((error: unknown) => { + this.lastError = error; + console.error( + JSON.stringify({ + event: "easytier_core_timer_failed", + error: String(error), + }), + ); + }); + } + + private wrapExports(raw: CoreExports): PromisingCoreExports { + const wrap = (callable: WasmCallable): PromisingExport => + WebAssembly.promising(callable) as PromisingExport; + const wrapOptional = ( + callable: WasmCallable | undefined, + ): PromisingExport | undefined => + callable === undefined ? undefined : wrap(callable); + return { + start: wrap(raw._start), + bufferAlloc: wrap(raw.easytier_buffer_alloc), + bufferFree: wrap(raw.easytier_buffer_free), + instanceCreate: wrap(raw.easytier_instance_create), + instanceStart: wrap(raw.easytier_instance_start), + instanceStop: wrap(raw.easytier_instance_stop), + instanceDrive: wrap(raw.easytier_instance_drive), + notifyCompletions: wrap(raw.easytier_instance_notify_completions), + instanceState: wrap(raw.easytier_instance_state), + nextDeadlineMillis: wrap(raw.easytier_instance_next_deadline_millis), + errorLength: wrap(raw.easytier_instance_error_len), + errorCopy: wrap(raw.easytier_instance_error_copy), + instanceDrop: wrap(raw.easytier_instance_drop), + tunnelAbiVersion: wrap(raw.easytier_host_tunnel_abi_version), + acceptTunnel: wrap(raw.easytier_instance_accept_tunnel), + dataPlaneAbiVersion: wrapOptional(raw.easytier_data_plane_abi_version), + dataPlaneCapabilities: wrapOptional( + raw.easytier_data_plane_capabilities, + ), + dataPlaneTcpConnectSubmit: wrapOptional( + raw.easytier_data_plane_tcp_connect_submit, + ), + dataPlaneTcpBindSubmit: wrapOptional( + raw.easytier_data_plane_tcp_bind_submit, + ), + dataPlaneTcpAcceptSubmit: wrapOptional( + raw.easytier_data_plane_tcp_accept_submit, + ), + dataPlaneTcpReadSubmit: wrapOptional( + raw.easytier_data_plane_tcp_read_submit, + ), + dataPlaneTcpWriteSubmit: wrapOptional( + raw.easytier_data_plane_tcp_write_submit, + ), + dataPlaneTcpShutdownWriteSubmit: wrapOptional( + raw.easytier_data_plane_tcp_shutdown_write_submit, + ), + dataPlaneCompletionDrain: wrapOptional( + raw.easytier_data_plane_completion_drain, + ), + dataPlaneResultSize: wrapOptional(raw.easytier_data_plane_result_size), + dataPlaneTcpConnectResultTake: wrapOptional( + raw.easytier_data_plane_tcp_connect_result_take, + ), + dataPlaneTcpBindResultTake: wrapOptional( + raw.easytier_data_plane_tcp_bind_result_take, + ), + dataPlaneTcpAcceptResultTake: wrapOptional( + raw.easytier_data_plane_tcp_accept_result_take, + ), + dataPlaneTcpReadResultTake: wrapOptional( + raw.easytier_data_plane_tcp_read_result_take, + ), + dataPlaneTcpWriteResultTake: wrapOptional( + raw.easytier_data_plane_tcp_write_result_take, + ), + dataPlaneTcpShutdownWriteResultTake: wrapOptional( + raw.easytier_data_plane_tcp_shutdown_write_result_take, + ), + dataPlaneOperationFree: wrapOptional( + raw.easytier_data_plane_operation_free, + ), + dataPlaneResourceClose: wrapOptional( + raw.easytier_data_plane_resource_close, + ), + }; + } + + private cancelTimer(): void { + if (this.timer !== undefined) { + clearTimeout(this.timer); + this.timer = undefined; + this.timerDueAt = undefined; + } + this.timerGeneration += 1; + } + + private async copyIntoGuest(bytes: Uint8Array): Promise { + const pointer = Number(await this.call("bufferAlloc", [bytes.byteLength])); + if (pointer === 0) { + throw new Error("guest buffer allocation failed"); + } + const memory = this.requireMemory(); + new Uint8Array(memory.buffer, pointer, bytes.byteLength).set(bytes); + return pointer; + } + + private async instanceError(context: string): Promise { + const errorLength = Number( + await this.call("errorLength", [this.instanceHandle]), + ); + if (errorLength <= 0) { + return `${context} failed`; + } + const pointer = Number(await this.call("bufferAlloc", [errorLength])); + if (pointer === 0) { + return `${context} failed and error allocation failed`; + } + try { + const copied = Number( + await this.call("errorCopy", [ + this.instanceHandle, + pointer, + errorLength, + ]), + ); + if (copied < 0) { + return `${context} failed and error copy returned ${copied}`; + } + const encoded = new Uint8Array( + this.requireMemory().buffer, + pointer, + copied, + ).slice(); + return `${context} failed: ${new TextDecoder().decode(encoded)}`; + } finally { + await this.call("bufferFree", [pointer]); + } + } + + private call( + name: keyof PromisingCoreExports | DataPlaneExportName, + parameters: Array, + ): Promise { + if (this.lastError !== undefined) { + return Promise.reject(this.lastError); + } + const callable = this.exports?.[name]; + if (callable === undefined) { + return Promise.reject(new Error(`guest export ${name} is unavailable`)); + } + return callable(...parameters); + } + + private requireMemory(): WebAssembly.Memory { + if (this.memory === undefined) { + throw new Error("guest memory is unavailable"); + } + return this.memory; + } + + private requireRunning(): void { + if (this.stopping) { + throw new Error("EasyTier runtime is stopped"); + } + } + + private enqueue(operation: () => Promise): Promise { + const next = this.serial.then(operation, operation); + this.serial = next.then( + () => undefined, + () => undefined, + ); + return next; + } +} diff --git a/easytier-js/runtime/src/wasi-clock.ts b/easytier-js/runtime/src/wasi-clock.ts new file mode 100644 index 00000000..adb01f31 --- /dev/null +++ b/easytier-js/runtime/src/wasi-clock.ts @@ -0,0 +1,144 @@ +const WASI_SUCCESS = 0; +const WASI_EINVAL = 28; +const WASI_ENOSYS = 52; + +const CLOCK_EVENT = 0; +const SUBSCRIPTION_SIZE = 48; +const EVENT_SIZE = 32; +const SUBSCRIPTION_CLOCK = 0; +const ABSOLUTE_CLOCK = 1; +const MAX_TIMER_MILLIS = 2_147_483_647; +const NANOS_PER_MILLI = 1_000_000n; + +interface ClockSubscription { + userdata: bigint; + clockId: number; + deadlineNanos: bigint; + waitNanos: bigint; +} + +type PollResolution = "timer" | "interrupt"; + +export class WasiClock { + private nowNanos = BigInt(Date.now()) * NANOS_PER_MILLI; + private interruptPoll: (() => void) | undefined; + + constructor(private readonly memory: () => WebAssembly.Memory) {} + + readonly clockTimeGet = ( + clockId: number, + _precision: bigint, + resultPointer: number, + ): number => { + if (clockId < 0 || clockId > 3) { + return WASI_EINVAL; + } + this.syncWallClock(); + this.view().setBigUint64(resultPointer, this.nowNanos, true); + return WASI_SUCCESS; + }; + + readonly pollOneoff = async ( + subscriptionsPointer: number, + eventsPointer: number, + subscriptionCount: number, + resultCountPointer: number, + ): Promise => { + if (subscriptionCount <= 0) { + return WASI_EINVAL; + } + const subscriptions = this.readClockSubscriptions( + subscriptionsPointer, + subscriptionCount, + ); + if (subscriptions.length === 0) { + return WASI_ENOSYS; + } + const selected = subscriptions.reduce((left, right) => + left.waitNanos <= right.waitNanos ? left : right, + ); + const waitMillis = Number( + (selected.waitNanos + NANOS_PER_MILLI - 1n) / NANOS_PER_MILLI, + ); + const boundedWait = Math.min(waitMillis, MAX_TIMER_MILLIS); + const resolution = await new Promise((resolve) => { + const timer = setTimeout(() => resolve("timer"), boundedWait); + this.interruptPoll = () => { + clearTimeout(timer); + resolve("interrupt"); + }; + }); + this.interruptPoll = undefined; + if (resolution === "timer") { + this.nowNanos = selected.deadlineNanos; + this.syncWallClock(); + } + this.writeClockEvent(eventsPointer, selected.userdata); + this.view().setUint32(resultCountPointer, 1, true); + return WASI_SUCCESS; + }; + + interrupt(): void { + this.interruptPoll?.(); + } + + advanceMillis(milliseconds: number): void { + if (Number.isFinite(milliseconds) && milliseconds > 0) { + this.nowNanos += BigInt(Math.ceil(milliseconds)) * NANOS_PER_MILLI; + } + this.syncWallClock(); + } + + syncWallTime(): void { + this.syncWallClock(); + } + + private readClockSubscriptions( + pointer: number, + count: number, + ): ClockSubscription[] { + const view = this.view(); + const subscriptions: ClockSubscription[] = []; + for (let index = 0; index < count; index += 1) { + const offset = pointer + index * SUBSCRIPTION_SIZE; + const type = view.getUint8(offset + 8); + if (type !== SUBSCRIPTION_CLOCK) { + continue; + } + const userdata = view.getBigUint64(offset, true); + const clockId = view.getUint32(offset + 16, true); + const timeout = view.getBigUint64(offset + 24, true); + const flags = view.getUint16(offset + 40, true); + const deadlineNanos = + (flags & ABSOLUTE_CLOCK) === 0 ? this.nowNanos + timeout : timeout; + subscriptions.push({ + userdata, + clockId, + deadlineNanos, + waitNanos: + deadlineNanos > this.nowNanos ? deadlineNanos - this.nowNanos : 0n, + }); + } + return subscriptions; + } + + private writeClockEvent(pointer: number, userdata: bigint): void { + const bytes = new Uint8Array(this.memory().buffer, pointer, EVENT_SIZE); + bytes.fill(0); + const view = this.view(); + view.setBigUint64(pointer, userdata, true); + view.setUint16(pointer + 8, WASI_SUCCESS, true); + view.setUint8(pointer + 10, CLOCK_EVENT); + } + + private syncWallClock(): void { + const wallClock = BigInt(Date.now()) * NANOS_PER_MILLI; + if (wallClock > this.nowNanos) { + this.nowNanos = wallClock; + } + } + + private view(): DataView { + return new DataView(this.memory().buffer); + } +} diff --git a/easytier-js/runtime/src/wasi-preview1.ts b/easytier-js/runtime/src/wasi-preview1.ts new file mode 100644 index 00000000..00aae81a --- /dev/null +++ b/easytier-js/runtime/src/wasi-preview1.ts @@ -0,0 +1,116 @@ +import { WasiClock } from "./wasi-clock.js"; + +const WASI_SUCCESS = 0; +const IOVEC_SIZE = 8; +const RANDOM_CHUNK_SIZE = 65_536; + +export class WasiPreview1 { + readonly imports: WebAssembly.ModuleImports; + + private memory: WebAssembly.Memory | undefined; + + constructor(clock: WasiClock) { + this.imports = { + random_get: (pointer: number, length: number) => + this.randomGet(pointer, length), + clock_time_get: clock.clockTimeGet, + environ_get: (pointers: number, buffer: number) => + this.environGet(pointers, buffer), + environ_sizes_get: (count: number, size: number) => + this.environSizesGet(count, size), + fd_write: ( + descriptor: number, + iovecs: number, + iovecCount: number, + written: number, + ) => this.fdWrite(descriptor, iovecs, iovecCount, written), + poll_oneoff: new WebAssembly.Suspending( + clock.pollOneoff as (...parameters: never[]) => Promise, + ), + proc_exit: (exitCode: number) => { + throw new Error(`WASI process exited with status ${exitCode}`); + }, + sched_yield: () => WASI_SUCCESS, + }; + } + + bindMemory(memory: WebAssembly.Memory): void { + this.memory = memory; + } + + private randomGet(pointer: number, length: number): number { + const destination = this.bytes(pointer, length); + for (let offset = 0; offset < destination.byteLength; ) { + const end = Math.min(offset + RANDOM_CHUNK_SIZE, destination.byteLength); + crypto.getRandomValues(destination.subarray(offset, end)); + offset = end; + } + return WASI_SUCCESS; + } + + private environSizesGet( + countPointer: number, + sizePointer: number, + ): number { + const view = this.view(); + view.setUint32(countPointer, 0, true); + view.setUint32(sizePointer, 0, true); + return WASI_SUCCESS; + } + + private environGet(_pointers: number, _buffer: number): number { + return WASI_SUCCESS; + } + + private fdWrite( + descriptor: number, + iovecsPointer: number, + iovecCount: number, + writtenPointer: number, + ): number { + const view = this.view(); + const chunks: Uint8Array[] = []; + let byteLength = 0; + for (let index = 0; index < iovecCount; index += 1) { + const offset = iovecsPointer + index * IOVEC_SIZE; + const pointer = view.getUint32(offset, true); + const length = view.getUint32(offset + 4, true); + chunks.push(this.bytes(pointer, length)); + byteLength += length; + } + view.setUint32(writtenPointer, byteLength, true); + + if ((descriptor === 1 || descriptor === 2) && byteLength > 0) { + const output = new Uint8Array(byteLength); + let offset = 0; + for (const chunk of chunks) { + output.set(chunk, offset); + offset += chunk.byteLength; + } + const message = new TextDecoder().decode(output).trimEnd(); + if (message.length > 0) { + if (descriptor === 2) { + console.error(message); + } else { + console.log(message); + } + } + } + return WASI_SUCCESS; + } + + private bytes(pointer: number, length: number): Uint8Array { + return new Uint8Array(this.requireMemory().buffer, pointer, length); + } + + private view(): DataView { + return new DataView(this.requireMemory().buffer); + } + + private requireMemory(): WebAssembly.Memory { + if (this.memory === undefined) { + throw new Error("WASI memory is not bound"); + } + return this.memory; + } +} diff --git a/easytier-js/runtime/src/websocket-host.ts b/easytier-js/runtime/src/websocket-host.ts new file mode 100644 index 00000000..b6474d40 --- /dev/null +++ b/easytier-js/runtime/src/websocket-host.ts @@ -0,0 +1,892 @@ +const HOST_PENDING = -1; +const HOST_INVALID = -3; +const HOST_UNSUPPORTED = -4; +const HOST_TUNNEL_CLOSED = -10; + +const PACKET_SINK_HANDLE = 1n; +const EVENT_SINK_HANDLE = 2n; +const FIRST_WEBSOCKET_HANDLE = 3n; +const MAX_CONNECTIONS = 256; +const MAX_MESSAGE_BYTES = 1024 * 1024; +const MAX_QUEUED_MESSAGES = 64; +const MAX_QUEUED_BYTES_PER_CONNECTION = 2 * 1024 * 1024; +const MAX_QUEUED_BYTES_PER_OBJECT = 16 * 1024 * 1024; +const MAX_BUFFERED_SEND_BYTES = 4 * 1024 * 1024; +const MAX_URL_BYTES = 16 * 1024; + +type IncomingMessage = + | { kind: "binary"; bytes: Uint8Array } + | { kind: "error"; status: number }; + +type ReceiveOperation = + | { + kind: "receive"; + handle: bigint; + capacity: number; + state: "pending"; + } + | { + kind: "receive"; + handle: bigint; + capacity: number; + state: "ready"; + message: IncomingMessage; + }; + +interface SendOperation { + kind: "send"; + handle: bigint; + state: "ready"; + status: number; +} + +type ConnectOperation = { + kind: "connect"; + handle: bigint; + state: "pending" | "ready"; + result: bigint; +}; + +type TcpPortLeaseOperation = { + kind: "tcp-port-lease"; + handle: bigint; + port: number; + state: "ready"; +}; + +type HostOperation = + | ReceiveOperation + | SendOperation + | ConnectOperation + | TcpPortLeaseOperation; + +interface WebSocketState { + socket: RuntimeWebSocket; + incoming: IncomingMessage[]; + queuedBytes: number; + pendingReceive: bigint | undefined; + remoteClosed: boolean; + guestOwned: boolean; +} + +export interface RuntimeWebSocketEvent { + data?: string | ArrayBuffer; +} + +export interface RuntimeWebSocket { + binaryType: string; + readonly bufferedAmount?: number; + send(message: Uint8Array): void; + close(code?: number, reason?: string): void; + addEventListener( + type: "open" | "message" | "close" | "error", + listener: (event: RuntimeWebSocketEvent) => void, + ): void; + accept?(): void; +} + +export interface WebSocketHostHealth { + connections: number; + queuedBytes: number; + pendingOperations: number; +} + +export interface EasyTierCoreEvent { + kind: string; + message: string; +} + +export class WebSocketHost { + private readonly sockets = new Map(); + private readonly tcpPortLeases = new Map(); + private readonly operations = new Map(); + private nextHandle = FIRST_WEBSOCKET_HANDLE; + private nextTcpPort = 49_152; + private totalQueuedBytes = 0; + private wakeGuest: () => void = () => {}; + private wasmMemory: WebAssembly.Memory | undefined; + + constructor( + private readonly outboundFactory?: (url: string) => RuntimeWebSocket, + private readonly onEvent: (event: EasyTierCoreEvent) => void = (event) => { + console.log(JSON.stringify({ event: "easytier_core_event", ...event })); + }, + ) {} + + readonly imports: WebAssembly.Imports["easytier_host"] = { + emit_event: ( + handle: bigint, + kind: number, + kindLength: number, + message: number, + messageLength: number, + ) => this.emitEvent(handle, kind, kindLength, message, messageLength), + start_tunnel_receive: ( + handle: bigint, + operation: bigint, + capacity: number, + ) => this.startReceive(handle, operation, capacity), + take_tunnel_receive: ( + operation: bigint, + destination: number, + capacity: number, + ) => this.takeReceive(operation, destination, capacity), + start_tunnel_send: ( + handle: bigint, + operation: bigint, + source: number, + length: number, + ) => this.startSend(handle, operation, source, length), + take_tunnel_send: (operation: bigint) => this.takeSend(operation), + start_tunnel_connect: ( + operation: bigint, + url: number, + urlLength: number, + ) => this.startConnect(operation, url, urlLength), + take_tunnel_connect: (operation: bigint) => + this.takeConnect(operation), + cancel_operation: (operation: bigint) => this.cancelOperation(operation), + close: (handle: bigint) => this.closeHandle(handle), + try_packet_write: ( + handle: bigint, + _packet: number, + _packetLength: number, + ) => (handle === PACKET_SINK_HANDLE ? 0 : HOST_INVALID), + start_packet_write_ready: () => HOST_UNSUPPORTED, + take_packet_write_ready: () => HOST_UNSUPPORTED, + start_read: () => HOST_UNSUPPORTED, + take_read: () => HOST_UNSUPPORTED, + start_write: () => HOST_UNSUPPORTED, + take_write: () => HOST_UNSUPPORTED, + start_udp_recv: () => HOST_UNSUPPORTED, + take_udp_recv: () => HOST_UNSUPPORTED, + try_udp_send: () => HOST_UNSUPPORTED, + start_udp_send_ready: () => HOST_UNSUPPORTED, + take_udp_send_ready: () => HOST_UNSUPPORTED, + start_tcp_connect: () => HOST_UNSUPPORTED, + take_tcp_connect: () => HOST_UNSUPPORTED, + start_udp_bind: () => HOST_UNSUPPORTED, + take_udp_bind: () => HOST_UNSUPPORTED, + start_tcp_bind: ( + operation: bigint, + options: number, + optionsLength: number, + ) => this.startTcpBind(operation, options, optionsLength), + take_tcp_bind: ( + operation: bigint, + destination: number, + capacity: number, + ) => this.takeTcpBind(operation, destination, capacity), + start_tcp_accept: () => HOST_UNSUPPORTED, + take_tcp_accept: () => HOST_UNSUPPORTED, + start_dns_resolve: () => HOST_UNSUPPORTED, + take_dns_resolve: () => HOST_UNSUPPORTED, + start_dns_txt: () => HOST_UNSUPPORTED, + take_dns_txt: () => HOST_UNSUPPORTED, + start_dns_srv: () => HOST_UNSUPPORTED, + take_dns_srv: () => HOST_UNSUPPORTED, + start_local_addr_for_remote: () => HOST_UNSUPPORTED, + take_local_addr_for_remote: () => HOST_UNSUPPORTED, + }; + + bindMemory(memory: WebAssembly.Memory): void { + this.wasmMemory = memory; + } + + setWakeGuest(wakeGuest: () => void): void { + this.wakeGuest = wakeGuest; + } + + canAccept(): boolean { + return this.sockets.size < MAX_CONNECTIONS; + } + + register(socket: RuntimeWebSocket): bigint { + if (!this.canAccept()) { + throw new Error("WebSocket connection limit reached"); + } + const handle = this.allocateHandle(); + this.sockets.set(handle, { + socket, + incoming: [], + queuedBytes: 0, + pendingReceive: undefined, + remoteClosed: false, + guestOwned: false, + }); + return handle; + } + + transferToGuest(handle: bigint): void { + const state = this.requireSocket(handle); + state.guestOwned = true; + } + + reject(handle: bigint): void { + const state = this.sockets.get(handle); + if (state === undefined || state.guestOwned) { + return; + } + this.releaseSocket(handle, state, 1011, "EasyTier attach failed"); + } + + abort(handle: bigint, reason: string): void { + const state = this.sockets.get(handle); + if (state === undefined) { + return; + } + this.releaseSocket(handle, state, 1011, reason); + } + + receive(handle: bigint, data: string | ArrayBuffer): void { + const state = this.sockets.get(handle); + if (state === undefined || state.remoteClosed) { + return; + } + if (typeof data === "string") { + this.terminateWithError( + handle, + state, + 1003, + "binary tunnel payload required", + ); + return; + } + if (data.byteLength > MAX_MESSAGE_BYTES) { + this.terminateWithError(handle, state, 1009, "message too large"); + return; + } + const bytes = new Uint8Array(data).slice(); + this.enqueue(handle, state, { kind: "binary", bytes }, bytes.byteLength); + } + + remoteClose(handle: bigint): void { + const state = this.sockets.get(handle); + if (state === undefined || state.remoteClosed) { + return; + } + state.remoteClosed = true; + if (state.pendingReceive !== undefined && state.incoming.length === 0) { + this.completeReceive(state.pendingReceive, { + kind: "error", + status: HOST_TUNNEL_CLOSED, + }); + } + } + + remoteError(handle: bigint): void { + const state = this.sockets.get(handle); + if (state === undefined || state.remoteClosed) { + return; + } + this.terminateWithError(handle, state, 1011, "WebSocket error"); + } + + health(): WebSocketHostHealth { + return { + connections: this.sockets.size, + queuedBytes: this.totalQueuedBytes, + pendingOperations: this.operations.size, + }; + } + + shutdown(reason = "EasyTier runtime stopped"): void { + for (const [handle, state] of [...this.sockets]) { + this.releaseSocket(handle, state, 1000, reason); + } + this.tcpPortLeases.clear(); + this.operations.clear(); + this.totalQueuedBytes = 0; + this.wasmMemory = undefined; + this.wakeGuest = () => {}; + } + + private emitEvent( + handle: bigint, + kindPointer: number, + kindLength: number, + messagePointer: number, + messageLength: number, + ): number { + if (handle !== EVENT_SINK_HANDLE) { + return HOST_INVALID; + } + const decoder = new TextDecoder(); + try { + this.onEvent({ + kind: decoder.decode(this.memoryBytes(kindPointer, kindLength)), + message: decoder.decode(this.memoryBytes(messagePointer, messageLength)), + }); + } catch (error) { + console.error( + JSON.stringify({ + event: "easytier_event_handler_failed", + error: String(error), + }), + ); + } + return 0; + } + + private startConnect( + operation: bigint, + urlPointer: number, + urlLength: number, + ): number { + if (this.outboundFactory === undefined) { + return HOST_UNSUPPORTED; + } + if ( + urlLength <= 0 || + urlLength > MAX_URL_BYTES || + this.operations.has(operation) || + !this.canAccept() + ) { + return HOST_INVALID; + } + + let requestedUrl: string; + let socket: RuntimeWebSocket; + try { + requestedUrl = new TextDecoder("utf-8", { + fatal: true, + ignoreBOM: false, + }).decode( + this.memoryBytes(urlPointer, urlLength), + ); + const parsed = new URL(requestedUrl); + if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") { + return HOST_INVALID; + } + socket = this.outboundFactory(requestedUrl); + } catch { + return HOST_INVALID; + } + + socket.binaryType = "arraybuffer"; + const handle = this.register(socket); + this.operations.set(operation, { + kind: "connect", + handle, + state: "pending", + result: BigInt(HOST_PENDING), + }); + socket.addEventListener("open", () => { + this.completeConnect(operation, handle); + }); + socket.addEventListener("message", (event) => { + if (typeof event.data === "string" || event.data instanceof ArrayBuffer) { + this.receive(handle, event.data); + } else { + this.remoteError(handle); + } + }); + socket.addEventListener("close", () => { + if (!this.failConnect(operation, handle, HOST_TUNNEL_CLOSED)) { + this.remoteClose(handle); + } + }); + socket.addEventListener("error", () => { + if (!this.failConnect(operation, handle, HOST_TUNNEL_CLOSED)) { + this.remoteError(handle); + } + }); + return 0; + } + + private takeConnect(operation: bigint): bigint { + const pending = this.operations.get(operation); + if (pending === undefined || pending.kind !== "connect") { + return BigInt(HOST_INVALID); + } + if (pending.state === "pending") { + return BigInt(HOST_PENDING); + } + this.operations.delete(operation); + if (pending.result > 0n) { + this.transferToGuest(pending.handle); + } + return pending.result; + } + + private completeConnect(operation: bigint, handle: bigint): void { + const pending = this.operations.get(operation); + if ( + pending === undefined || + pending.kind !== "connect" || + pending.handle !== handle || + pending.state !== "pending" + ) { + return; + } + this.operations.set(operation, { + ...pending, + state: "ready", + result: handle, + }); + this.wakeGuest(); + } + + private failConnect( + operation: bigint, + handle: bigint, + status: number, + ): boolean { + const pending = this.operations.get(operation); + if ( + pending === undefined || + pending.kind !== "connect" || + pending.handle !== handle + ) { + return false; + } + const state = this.sockets.get(handle); + if (state !== undefined) { + this.sockets.delete(handle); + this.totalQueuedBytes -= state.queuedBytes; + try { + state.socket.close(1011, "WebSocket connect failed"); + } catch { + // The browser has already made the endpoint terminal. + } + } + this.operations.set(operation, { + ...pending, + state: "ready", + result: BigInt(status), + }); + this.wakeGuest(); + return true; + } + + private startReceive( + handle: bigint, + operation: bigint, + capacity: number, + ): number { + if ( + capacity < 0 || + capacity > MAX_MESSAGE_BYTES || + this.operations.has(operation) + ) { + return HOST_INVALID; + } + const state = this.sockets.get(handle); + if (state === undefined || state.pendingReceive !== undefined) { + return HOST_INVALID; + } + const queued = state.incoming.shift(); + if (queued !== undefined) { + this.operations.set(operation, { + kind: "receive", + handle, + capacity, + state: "ready", + message: queued, + }); + return 0; + } + if (state.remoteClosed) { + this.operations.set(operation, { + kind: "receive", + handle, + capacity, + state: "ready", + message: { kind: "error", status: HOST_TUNNEL_CLOSED }, + }); + return 0; + } + state.pendingReceive = operation; + this.operations.set(operation, { + kind: "receive", + handle, + capacity, + state: "pending", + }); + return 0; + } + + private takeReceive( + operation: bigint, + destination: number, + capacity: number, + ): number { + const pending = this.operations.get(operation); + if (pending === undefined || pending.kind !== "receive") { + return HOST_INVALID; + } + if (pending.state === "pending") { + return HOST_PENDING; + } + const { message } = pending; + if (message.kind === "error") { + this.operations.delete(operation); + return message.status; + } + if (message.bytes.byteLength > pending.capacity) { + this.operations.delete(operation); + this.removeQueuedBytesForHandle(pending.handle, message); + return HOST_INVALID; + } + if (destination === 0 && capacity === 0) { + return message.bytes.byteLength; + } + this.operations.delete(operation); + this.removeQueuedBytesForHandle(pending.handle, message); + if (message.bytes.byteLength > capacity) { + return HOST_INVALID; + } + const destinationBytes = this.memoryBytes(destination, capacity); + destinationBytes.set(message.bytes); + return message.bytes.byteLength; + } + + private startSend( + handle: bigint, + operation: bigint, + source: number, + length: number, + ): number { + if ( + length < 0 || + length > MAX_MESSAGE_BYTES || + this.operations.has(operation) + ) { + return HOST_INVALID; + } + const state = this.sockets.get(handle); + if (state === undefined || state.remoteClosed) { + return HOST_TUNNEL_CLOSED; + } + const bufferedAmount = Reflect.get(state.socket, "bufferedAmount"); + if ( + typeof bufferedAmount === "number" && + bufferedAmount + length > MAX_BUFFERED_SEND_BYTES + ) { + this.terminateWithError(handle, state, 1009, "send buffer limit"); + return HOST_INVALID; + } + const message = this.memoryBytes(source, length).slice(); + let status = 0; + try { + state.socket.send(message); + } catch { + status = HOST_TUNNEL_CLOSED; + } + this.operations.set(operation, { + kind: "send", + handle, + state: "ready", + status, + }); + return 0; + } + + private takeSend(operation: bigint): number { + const pending = this.operations.get(operation); + if (pending === undefined || pending.kind !== "send") { + return HOST_INVALID; + } + this.operations.delete(operation); + return pending.status; + } + + private cancelOperation(operation: bigint): number { + const pending = this.operations.get(operation); + if (pending?.kind === "connect") { + const state = this.sockets.get(pending.handle); + if (state !== undefined) { + this.releaseSocket( + pending.handle, + state, + 1000, + "WebSocket connect cancelled", + ); + } + } else if (pending?.kind === "receive" && pending.state === "pending") { + const state = this.sockets.get(pending.handle); + if (state?.pendingReceive === operation) { + state.pendingReceive = undefined; + } + } else if (pending?.kind === "receive") { + this.removeQueuedBytesForHandle(pending.handle, pending.message); + } else if (pending?.kind === "tcp-port-lease") { + this.tcpPortLeases.delete(pending.handle); + } + this.operations.delete(operation); + return 0; + } + + private closeHandle(handle: bigint): number { + if (handle === PACKET_SINK_HANDLE) { + return 0; + } + if (this.tcpPortLeases.delete(handle)) { + return 0; + } + const state = this.sockets.get(handle); + if (state === undefined) { + return 0; + } + this.releaseSocket(handle, state, 1000, "EasyTier tunnel closed"); + return 0; + } + + private enqueue( + handle: bigint, + state: WebSocketState, + message: IncomingMessage, + byteLength: number, + ): void { + if (state.pendingReceive !== undefined) { + if (!this.canQueueBytes(state, byteLength)) { + this.terminateWithError(handle, state, 1009, "receive queue limit"); + return; + } + this.addQueuedBytes(state, byteLength); + this.completeReceive(state.pendingReceive, message); + return; + } + if ( + state.incoming.length >= MAX_QUEUED_MESSAGES || + !this.canQueueBytes(state, byteLength) + ) { + this.terminateWithError(handle, state, 1009, "receive queue limit"); + return; + } + state.incoming.push(message); + this.addQueuedBytes(state, byteLength); + } + + private completeReceive( + operation: bigint, + message: IncomingMessage, + ): void { + const pending = this.operations.get(operation); + if ( + pending === undefined || + pending.kind !== "receive" || + pending.state !== "pending" + ) { + return; + } + const state = this.sockets.get(pending.handle); + if (state?.pendingReceive === operation) { + state.pendingReceive = undefined; + } + this.operations.set(operation, { + ...pending, + state: "ready", + message, + }); + this.wakeGuest(); + } + + private terminateWithError( + handle: bigint, + state: WebSocketState, + closeCode: number, + reason: string, + ): void { + state.remoteClosed = true; + if (state.pendingReceive !== undefined) { + this.completeReceive(state.pendingReceive, { + kind: "error", + status: HOST_INVALID, + }); + } else { + state.incoming.push({ kind: "error", status: HOST_INVALID }); + } + try { + state.socket.close(closeCode, reason); + } catch { + // The endpoint is already terminal; the tombstone remains for guest EOF. + } + if (!state.guestOwned) { + this.releaseSocket(handle, state, closeCode, reason); + } + } + + private releaseSocket( + handle: bigint, + state: WebSocketState, + closeCode: number, + reason: string, + ): void { + this.sockets.delete(handle); + this.totalQueuedBytes -= state.queuedBytes; + for (const [operation, pending] of this.operations) { + if (pending.handle === handle) { + this.operations.delete(operation); + } + } + try { + state.socket.close(closeCode, reason); + } catch { + // Close is intentionally idempotent. + } + } + + private removeQueuedBytes( + state: WebSocketState, + message: IncomingMessage, + ): void { + if (message.kind !== "binary") { + return; + } + state.queuedBytes -= message.bytes.byteLength; + this.totalQueuedBytes -= message.bytes.byteLength; + } + + private removeQueuedBytesForHandle( + handle: bigint, + message: IncomingMessage, + ): void { + const state = this.sockets.get(handle); + if (state !== undefined) { + this.removeQueuedBytes(state, message); + } + } + + private canQueueBytes( + state: WebSocketState, + byteLength: number, + ): boolean { + return ( + state.queuedBytes + byteLength <= + MAX_QUEUED_BYTES_PER_CONNECTION && + this.totalQueuedBytes + byteLength <= + MAX_QUEUED_BYTES_PER_OBJECT + ); + } + + private addQueuedBytes( + state: WebSocketState, + byteLength: number, + ): void { + state.queuedBytes += byteLength; + this.totalQueuedBytes += byteLength; + } + + private allocateHandle(): bigint { + while ( + this.sockets.has(this.nextHandle) || + this.tcpPortLeases.has(this.nextHandle) + ) { + this.nextHandle += 1n; + if (this.nextHandle === 0n) { + this.nextHandle = FIRST_WEBSOCKET_HANDLE; + } + } + const handle = this.nextHandle; + this.nextHandle += 1n; + return handle; + } + + private startTcpBind( + operation: bigint, + optionsPointer: number, + optionsLength: number, + ): number { + if (this.outboundFactory === undefined) { + return HOST_UNSUPPORTED; + } + if (this.operations.has(operation) || optionsLength < 48) { + return HOST_INVALID; + } + let options: Uint8Array; + try { + options = this.memoryBytes(optionsPointer, optionsLength); + } catch { + return HOST_INVALID; + } + if (options.byteLength < 48 || options[0] !== 2) { + return HOST_INVALID; + } + const netnsLength = new DataView( + options.buffer, + options.byteOffset, + options.byteLength, + ).getUint32(35, false); + const purposeOffset = 42 + netnsLength; + if ( + purposeOffset >= options.byteLength || + options[purposeOffset] !== 6 || + options[1] !== 4 || + options.subarray(2, 18).some((byte) => byte !== 0) + ) { + return HOST_UNSUPPORTED; + } + const requestedPort = new DataView( + options.buffer, + options.byteOffset, + options.byteLength, + ).getUint16(18, false); + const port = this.allocateTcpPort(requestedPort); + if (port === undefined) { + return HOST_INVALID; + } + const handle = this.allocateHandle(); + this.tcpPortLeases.set(handle, port); + this.operations.set(operation, { + kind: "tcp-port-lease", + handle, + port, + state: "ready", + }); + return 0; + } + + private takeTcpBind( + operation: bigint, + destination: number, + capacity: number, + ): number { + const pending = this.operations.get(operation); + if ( + pending === undefined || + pending.kind !== "tcp-port-lease" || + capacity < 35 + ) { + return HOST_INVALID; + } + const encoded = this.memoryBytes(destination, capacity); + encoded.subarray(0, 35).fill(0); + const view = new DataView(encoded.buffer, encoded.byteOffset, 35); + view.setBigUint64(0, pending.handle, false); + encoded[8] = 4; + view.setUint16(25, pending.port, false); + this.operations.delete(operation); + return 0; + } + + private allocateTcpPort(requestedPort: number): number | undefined { + const leasedPorts = new Set(this.tcpPortLeases.values()); + if (requestedPort !== 0) { + return leasedPorts.has(requestedPort) ? undefined : requestedPort; + } + for (let attempts = 0; attempts < 16_384; attempts += 1) { + const port = this.nextTcpPort; + this.nextTcpPort = port === 65_535 ? 49_152 : port + 1; + if (!leasedPorts.has(port)) { + return port; + } + } + return undefined; + } + + private requireSocket(handle: bigint): WebSocketState { + const state = this.sockets.get(handle); + if (state === undefined) { + throw new Error(`unknown WebSocket handle ${handle}`); + } + return state; + } + + private memoryBytes(pointer: number, length: number): Uint8Array { + if (this.wasmMemory === undefined) { + throw new Error("Wasm memory is not bound"); + } + return new Uint8Array(this.wasmMemory.buffer, pointer, length); + } +} diff --git a/easytier-js/runtime/test/build-profile.test.ts b/easytier-js/runtime/test/build-profile.test.ts new file mode 100644 index 00000000..2bc8e7e8 --- /dev/null +++ b/easytier-js/runtime/test/build-profile.test.ts @@ -0,0 +1,17 @@ +import { readFile } from "node:fs/promises"; + +import { describe, expect, it } from "vitest"; + +describe("Cloudflare Worker WASM build profile", () => { + it("keeps separate worker and browser capability sets", async () => { + const buildScript = await readFile( + new URL("../scripts/build-wasm.mjs", import.meta.url), + "utf8", + ); + + expect(buildScript).toContain('"wasm-host-tunnel,aes-gcm"'); + expect(buildScript).toContain( + '"wasm-host-tunnel-outbound,aes-gcm,proxy-smoltcp-stack"', + ); + }); +}); diff --git a/easytier-js/runtime/test/config.test.ts b/easytier-js/runtime/test/config.test.ts new file mode 100644 index 00000000..82307bb2 --- /dev/null +++ b/easytier-js/runtime/test/config.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; + +import { encodeRuntimeConfig } from "../src/config"; + +describe("encodeRuntimeConfig", () => { + it("encodes only capabilities supported by the browser profile", () => { + const encoded = encodeRuntimeConfig({ + profile: "browser", + instanceId: "019f94c2-8a97-7be3-b7a5-d6cea838c5d4", + instanceName: "browser", + networkName: "office", + networkSecret: "secret", + encryption: true, + ipv4: "10.144.0.10/24", + peers: ["wss://relay.example.com/"], + }); + + expect(encoded).toContain('ipv4 = "10.144.0.10/24"'); + expect(encoded).toContain('uri = "wss://relay.example.com/"'); + expect(encoded).toContain("use_smoltcp = true"); + expect(encoded).not.toContain("proxy_forward_by_system"); + }); + + it("encodes an inbound-only Cloudflare relay", () => { + const encoded = encodeRuntimeConfig({ + profile: "cloudflare", + instanceId: "019f94c2-8a97-7be3-b7a5-d6cea838c5d4", + instanceName: "relay", + networkName: "office", + networkSecret: "secret", + encryption: false, + }); + + expect(encoded).toContain("proxy_forward_by_system = true"); + expect(encoded).toContain("enable_encryption = false"); + expect(encoded).not.toContain("[[peer]]"); + expect(encoded).not.toContain("use_smoltcp"); + }); + + it("rejects unsupported Browser and Cloudflare capabilities", () => { + expect(() => + encodeRuntimeConfig({ + profile: "browser", + instanceId: "id", + instanceName: "browser", + networkName: "office", + networkSecret: "secret", + encryption: true, + ipv4: "not-a-prefix", + peers: [], + }), + ).toThrow("ipv4 must be an IPv4 address with a network prefix"); + + expect(() => + encodeRuntimeConfig({ + profile: "cloudflare", + instanceId: "id", + instanceName: "relay", + networkName: "office", + networkSecret: "secret", + encryption: true, + peers: ["wss://relay.example.com/"], + }), + ).toThrow("Cloudflare relay configuration cannot dial peers"); + }); +}); diff --git a/easytier-js/runtime/test/data-plane.test.ts b/easytier-js/runtime/test/data-plane.test.ts new file mode 100644 index 00000000..44b75255 --- /dev/null +++ b/easytier-js/runtime/test/data-plane.test.ts @@ -0,0 +1,334 @@ +import { describe, expect, it } from "vitest"; + +import { + EasyTierDataPlane, + type DataPlaneBindings, + type DataPlaneExportName, + type DataPlaneValue, +} from "../src/data-plane"; + +const CONNECT_OPERATION = 101n; +const READ_OPERATION = 102n; +const WRITE_OPERATION = 103n; +const BIND_OPERATION = 104n; +const ACCEPT_OPERATION = 105n; +const SHUTDOWN_OPERATION = 106n; +const STREAM_RESOURCE = 501n; +const ACCEPTED_STREAM_RESOURCE = 502n; +const LISTENER_RESOURCE = 601n; + +class DataPlaneFixture implements DataPlaneBindings { + readonly instanceHandle = 17n; + readonly memory = new WebAssembly.Memory({ initial: 1 }); + readonly closedResources: bigint[] = []; + readonly boundPorts: number[] = []; + readonly acceptedListeners: bigint[] = []; + readonly shutdownResources: bigint[] = []; + readonly submittedAddresses: Uint8Array[] = []; + readonly submittedTimeouts: bigint[] = []; + readonly writes: Uint8Array[] = []; + onDrive: () => Promise = async () => {}; + + private nextPointer = 1024; + private readonly completions: Array<{ + operation: bigint; + kind: number; + status: number; + }> = []; + + async call( + name: DataPlaneExportName, + parameters: DataPlaneValue[], + ): Promise { + switch (name) { + case "dataPlaneAbiVersion": + return 4; + case "dataPlaneCapabilities": + return 2n; + case "dataPlaneTcpConnectSubmit": { + const addressPointer = Number(parameters[1]); + this.submittedAddresses.push(this.readGuest(addressPointer, 27)); + this.submittedTimeouts.push(BigInt(parameters[2]!)); + this.writeU64(Number(parameters[3]), CONNECT_OPERATION); + this.completions.push({ + operation: CONNECT_OPERATION, + kind: 1, + status: 0, + }); + return 0; + } + case "dataPlaneTcpBindSubmit": + this.boundPorts.push(Number(parameters[1])); + this.submittedTimeouts.push(BigInt(parameters[2]!)); + this.writeU64(Number(parameters[3]), BIND_OPERATION); + this.completions.push({ + operation: BIND_OPERATION, + kind: 2, + status: 0, + }); + return 0; + case "dataPlaneTcpAcceptSubmit": + this.acceptedListeners.push(BigInt(parameters[1]!)); + this.submittedTimeouts.push(BigInt(parameters[2]!)); + this.writeU64(Number(parameters[3]), ACCEPT_OPERATION); + this.completions.push({ + operation: ACCEPT_OPERATION, + kind: 3, + status: 0, + }); + return 0; + case "dataPlaneTcpReadSubmit": + expect(parameters[1]).toBe(STREAM_RESOURCE); + this.writeU64(Number(parameters[3]), READ_OPERATION); + this.completions.push({ + operation: READ_OPERATION, + kind: 4, + status: 0, + }); + return 0; + case "dataPlaneTcpWriteSubmit": { + expect(parameters[1]).toBe(STREAM_RESOURCE); + const length = Number(parameters[3]); + this.writes.push(this.readGuest(Number(parameters[2]), length)); + this.writeU64(Number(parameters[4]), WRITE_OPERATION); + this.completions.push({ + operation: WRITE_OPERATION, + kind: 5, + status: 0, + }); + return 0; + } + case "dataPlaneTcpShutdownWriteSubmit": + this.shutdownResources.push(BigInt(parameters[1]!)); + this.writeU64(Number(parameters[2]), SHUTDOWN_OPERATION); + this.completions.push({ + operation: SHUTDOWN_OPERATION, + kind: 9, + status: 0, + }); + return 0; + case "dataPlaneCompletionDrain": { + const output = Number(parameters[1]); + const capacity = Number(parameters[2]); + const count = Math.min(capacity, this.completions.length); + const view = new DataView(this.memory.buffer); + for (let index = 0; index < count; index += 1) { + const completion = this.completions[index]; + if (completion === undefined) { + throw new Error("completion queue changed while draining"); + } + const offset = output + index * 12; + view.setBigUint64(offset, completion.operation, false); + view.setUint16(offset + 8, completion.kind, false); + view.setUint16(offset + 10, completion.status, false); + } + this.completions.splice(0, count); + return count; + } + case "dataPlaneTcpConnectResultTake": + expect(parameters[1]).toBe(CONNECT_OPERATION); + this.writeU64(Number(parameters[2]), STREAM_RESOURCE); + this.writeSocketAddress(Number(parameters[2]) + 8, "10.1.2.4", 40000); + this.writeSocketAddress(Number(parameters[2]) + 35, "10.1.2.3", 8080); + return 0; + case "dataPlaneTcpBindResultTake": + expect(parameters[1]).toBe(BIND_OPERATION); + this.writeU64(Number(parameters[2]), LISTENER_RESOURCE); + this.writeSocketAddress(Number(parameters[2]) + 8, "10.1.2.4", 32123); + return 0; + case "dataPlaneTcpAcceptResultTake": + expect(parameters[1]).toBe(ACCEPT_OPERATION); + this.writeU64(Number(parameters[2]), ACCEPTED_STREAM_RESOURCE); + this.writeSocketAddress(Number(parameters[2]) + 8, "10.1.2.4", 32123); + this.writeSocketAddress(Number(parameters[2]) + 35, "10.1.2.5", 44000); + return 0; + case "dataPlaneResultSize": + expect(parameters[1]).toBe(READ_OPERATION); + return 5; + case "dataPlaneTcpReadResultTake": { + expect(parameters[1]).toBe(READ_OPERATION); + const data = new TextEncoder().encode("hello"); + new Uint8Array( + this.memory.buffer, + Number(parameters[2]), + data.byteLength, + ).set(data); + new Uint8Array(this.memory.buffer, Number(parameters[4]), 1)[0] = 1; + return data.byteLength; + } + case "dataPlaneTcpWriteResultTake": + expect(parameters[1]).toBe(WRITE_OPERATION); + return this.writes.at(-1)?.byteLength ?? 0; + case "dataPlaneTcpShutdownWriteResultTake": + expect(parameters[1]).toBe(SHUTDOWN_OPERATION); + return 0; + case "dataPlaneResourceClose": + this.closedResources.push(BigInt(parameters[1]!)); + return 0; + case "dataPlaneOperationFree": + return 0; + } + } + + async allocate(length: number): Promise { + const pointer = this.nextPointer; + this.nextPointer += Math.max(length, 1) + 8; + return pointer; + } + + async free(_pointer: number): Promise {} + + async copyIntoGuest(bytes: Uint8Array): Promise { + const pointer = await this.allocate(bytes.byteLength); + new Uint8Array(this.memory.buffer, pointer, bytes.byteLength).set(bytes); + return pointer; + } + + readGuest(pointer: number, length: number): Uint8Array { + return new Uint8Array(this.memory.buffer, pointer, length).slice(); + } + + async instanceError(context: string): Promise { + return `${context} failed`; + } + + runExclusive(operation: () => Promise): Promise { + return operation(); + } + + drive(): Promise { + return this.onDrive(); + } + + private writeU64(pointer: number, value: bigint): void { + new DataView(this.memory.buffer).setBigUint64(pointer, value, false); + } + + private writeSocketAddress( + pointer: number, + ipv4: string, + port: number, + ): void { + const bytes = new Uint8Array(this.memory.buffer, pointer, 27); + bytes.fill(0); + bytes[0] = 4; + bytes.set(ipv4.split(".").map(Number), 1); + new DataView(this.memory.buffer).setUint16(pointer + 17, port, false); + } +} + +describe("EasyTierDataPlane", () => { + it("connects, exchanges bytes, and closes an IPv4 TCP stream", async () => { + const bindings = new DataPlaneFixture(); + const dataPlane = new EasyTierDataPlane(bindings); + bindings.onDrive = () => dataPlane.drainCompletions(); + await dataPlane.initialize(); + + const stream = await dataPlane.connectTcp("10.1.2.3", 8080, 250); + expect(bindings.submittedTimeouts).toEqual([250n]); + const address = bindings.submittedAddresses[0]; + expect(address?.subarray(0, 5)).toEqual( + new Uint8Array([4, 10, 1, 2, 3]), + ); + expect( + new DataView( + address!.buffer, + address!.byteOffset, + address!.byteLength, + ).getUint16(17, false), + ).toBe(8080); + expect(stream.localAddress).toEqual({ ipv4: "10.1.2.4", port: 40000 }); + expect(stream.peerAddress).toEqual({ ipv4: "10.1.2.3", port: 8080 }); + + expect(await stream.write(new TextEncoder().encode("request"))).toBe(7); + expect(bindings.writes).toEqual([new TextEncoder().encode("request")]); + await expect(stream.read()).resolves.toEqual({ + data: new TextEncoder().encode("hello"), + eof: true, + }); + + await stream.shutdownWrite(); + await stream.shutdownWrite(); + expect(bindings.shutdownResources).toEqual([STREAM_RESOURCE]); + await expect(stream.write(new Uint8Array([1]))).rejects.toThrow( + "TCP stream write side is shut down", + ); + await expect(stream.read()).resolves.toEqual({ + data: new TextEncoder().encode("hello"), + eof: true, + }); + + await stream.close(); + await stream.close(); + expect(bindings.closedResources).toEqual([STREAM_RESOURCE]); + expect(() => stream.read()).toThrow("TCP stream is closed"); + }); + + it("binds a TCP listener and accepts streams with endpoint metadata", async () => { + const bindings = new DataPlaneFixture(); + const dataPlane = new EasyTierDataPlane(bindings); + bindings.onDrive = () => dataPlane.drainCompletions(); + await dataPlane.initialize(); + + const listener = await dataPlane.bindTcp(0, 500); + expect(listener.localAddress).toEqual({ + ipv4: "10.1.2.4", + port: 32123, + }); + expect(bindings.boundPorts).toEqual([0]); + + const stream = await listener.accept(750); + expect(bindings.acceptedListeners).toEqual([LISTENER_RESOURCE]); + expect(bindings.submittedTimeouts).toEqual([500n, 750n]); + expect(stream.localAddress).toEqual({ + ipv4: "10.1.2.4", + port: 32123, + }); + expect(stream.peerAddress).toEqual({ + ipv4: "10.1.2.5", + port: 44000, + }); + + await stream.close(); + await listener.close(); + await listener.close(); + expect(bindings.closedResources).toEqual([ + ACCEPTED_STREAM_RESOURCE, + LISTENER_RESOURCE, + ]); + expect(() => listener.accept()).toThrow("TCP listener is closed"); + }); + + it("rejects invalid browser TCP endpoints before guest submission", async () => { + const bindings = new DataPlaneFixture(); + const dataPlane = new EasyTierDataPlane(bindings); + await dataPlane.initialize(); + + expect(() => dataPlane.connectTcp("example.com", 443)).toThrow( + "invalid IPv4 address", + ); + expect(() => dataPlane.connectTcp("10.0.0.1", 0)).toThrow( + "TCP port must be between 1 and 65535", + ); + expect(() => dataPlane.bindTcp(-1)).toThrow( + "TCP local port must be between 0 and 65535", + ); + }); + + it("invalidates existing resources when the runtime stops", async () => { + const bindings = new DataPlaneFixture(); + const dataPlane = new EasyTierDataPlane(bindings); + bindings.onDrive = () => dataPlane.drainCompletions(); + await dataPlane.initialize(); + const listener = await dataPlane.bindTcp(22); + + dataPlane.shutdown(); + + await expect(listener.accept()).rejects.toThrow( + "EasyTier runtime is stopped", + ); + await expect(listener.close()).rejects.toThrow( + "EasyTier runtime is stopped", + ); + }); +}); diff --git a/easytier-js/runtime/test/runtime.test.ts b/easytier-js/runtime/test/runtime.test.ts new file mode 100644 index 00000000..2b1b6cec --- /dev/null +++ b/easytier-js/runtime/test/runtime.test.ts @@ -0,0 +1,244 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { EasyTierRuntime } from "../src/runtime"; + +interface RuntimeHarness { + armNextDrive(): void; + serial: Promise; +} + +interface AttachHarness { + attachTunnel( + tunnelHandle: bigint, + metadata: { + version: 1; + local_url: string; + remote_url: string; + }, + ): Promise; + serial: Promise; +} + +describe("EasyTierRuntime timer ownership", () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it("installs only the latest queued deadline timer", async () => { + vi.useFakeTimers(); + const runtime = Object.create( + EasyTierRuntime.prototype, + ) as RuntimeHarness; + Object.assign(runtime, { + call: async (name: string) => + name === "nextDeadlineMillis" ? 1000n : 0, + instanceHandle: 1n, + lastError: undefined, + serial: Promise.resolve(), + timer: undefined, + timerGeneration: 0, + timerDueAt: undefined, + armRequest: 0, + }); + + runtime.armNextDrive(); + runtime.armNextDrive(); + await runtime.serial; + + expect(vi.getTimerCount()).toBe(1); + }); + + it("does not postpone an earlier deadline when re-armed", async () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + let pumpCount = 0; + const clock = { + advanceMillis: vi.fn(), + syncWallTime: vi.fn(), + }; + const runtime = Object.create( + EasyTierRuntime.prototype, + ) as RuntimeHarness; + Object.assign(runtime, { + call: async (name: string) => + name === "nextDeadlineMillis" ? 1000n : 0, + instanceHandle: 1n, + lastError: undefined, + serial: Promise.resolve(), + timer: undefined, + timerGeneration: 0, + timerDueAt: undefined, + armRequest: 0, + clock, + queuePump: () => { + pumpCount += 1; + }, + }); + + runtime.armNextDrive(); + await runtime.serial; + await vi.advanceTimersByTimeAsync(100); + runtime.armNextDrive(); + await runtime.serial; + + await vi.advanceTimersByTimeAsync(899); + expect(pumpCount).toBe(0); + await vi.advanceTimersByTimeAsync(1); + expect(pumpCount).toBe(1); + expect(clock.syncWallTime).toHaveBeenCalledOnce(); + expect(clock.advanceMillis).not.toHaveBeenCalled(); + }); +}); + +describe("EasyTierRuntime host tunnel ownership", () => { + it("commits guest ownership before driving admission", async () => { + const events: string[] = []; + const runtime = Object.create( + EasyTierRuntime.prototype, + ) as AttachHarness; + Object.assign(runtime, { + ready: Promise.resolve(), + serial: Promise.resolve(), + instanceHandle: 1n, + copyIntoGuest: async () => 64, + call: async (name: string) => { + events.push(name); + return 0; + }, + host: { + transferToGuest: () => events.push("transferToGuest"), + abort: () => events.push("abort"), + }, + driveUntilIdle: async () => { + events.push("driveUntilIdle"); + }, + armNextDrive: () => events.push("armNextDrive"), + }); + + await runtime.attachTunnel(2n, { + version: 1, + local_url: "wss://relay.example/", + remote_url: "wss://client.example/", + }); + + expect(events).toEqual([ + "acceptTunnel", + "transferToGuest", + "bufferFree", + "driveUntilIdle", + "armNextDrive", + ]); + }); + + it("aborts guest-owned socket when admission drive fails", async () => { + const events: string[] = []; + const runtime = Object.create( + EasyTierRuntime.prototype, + ) as AttachHarness; + Object.assign(runtime, { + ready: Promise.resolve(), + serial: Promise.resolve(), + instanceHandle: 1n, + copyIntoGuest: async () => 64, + call: async () => 0, + host: { + transferToGuest: () => events.push("transferToGuest"), + abort: () => events.push("abort"), + }, + driveUntilIdle: async () => { + throw new Error("drive failed"); + }, + armNextDrive: () => {}, + }); + + await expect( + runtime.attachTunnel(2n, { + version: 1, + local_url: "wss://relay.example/", + remote_url: "wss://client.example/", + }), + ).rejects.toThrow("drive failed"); + expect(events).toEqual(["transferToGuest", "abort"]); + }); +}); + +describe("EasyTierRuntime data plane", () => { + it("exposes TCP listener creation after runtime initialization", async () => { + const listener = {} as Awaited>; + const bindTcp = vi.fn().mockResolvedValue(listener); + const runtime = Object.create( + EasyTierRuntime.prototype, + ) as EasyTierRuntime; + Object.assign(runtime, { + ready: Promise.resolve(), + dataPlane: { bindTcp }, + }); + + await expect(runtime.bindTcp(22, 500)).resolves.toBe(listener); + expect(bindTcp).toHaveBeenCalledWith(22, 500); + }); + + it("stops and drops the guest exactly once before releasing host resources", async () => { + const calls: string[] = []; + let driveCount = 0; + const dataPlane = { + drainCompletions: vi.fn().mockResolvedValue(undefined), + shutdown: vi.fn(), + }; + const host = { shutdown: vi.fn() }; + const clock = { + interrupt: vi.fn(), + advanceMillis: vi.fn(), + }; + const runtime = Object.create( + EasyTierRuntime.prototype, + ) as EasyTierRuntime; + Object.assign(runtime, { + ready: Promise.resolve(), + serial: Promise.resolve(), + instanceHandle: 7n, + dataPlane, + host, + clock, + stopping: false, + stopPromise: undefined, + armRequest: 0, + timer: undefined, + timerGeneration: 0, + timerDueAt: undefined, + completionRequested: false, + pumpQueued: false, + call: async (name: string) => { + calls.push(name); + switch (name) { + case "instanceStop": + case "instanceDrop": + return 0; + case "instanceDrive": + driveCount += 1; + return driveCount === 1 ? 3 : 4; + case "nextDeadlineMillis": + return 0n; + default: + throw new Error(`unexpected call: ${name}`); + } + }, + }); + + const stopping = runtime.stop(); + expect(runtime.stop()).toBe(stopping); + await stopping; + + expect(calls).toEqual([ + "instanceStop", + "instanceDrive", + "nextDeadlineMillis", + "instanceDrive", + "instanceDrop", + ]); + expect(dataPlane.drainCompletions).toHaveBeenCalledTimes(2); + expect(dataPlane.shutdown).toHaveBeenCalledOnce(); + expect(host.shutdown).toHaveBeenCalledOnce(); + expect(clock.interrupt).toHaveBeenCalledTimes(2); + }); +}); diff --git a/easytier-js/runtime/test/websocket-host.test.ts b/easytier-js/runtime/test/websocket-host.test.ts new file mode 100644 index 00000000..799485b0 --- /dev/null +++ b/easytier-js/runtime/test/websocket-host.test.ts @@ -0,0 +1,419 @@ +import { describe, expect, it, vi } from "vitest"; + +import { WebSocketHost } from "../src/websocket-host"; + +type HostCall = (...parameters: Array) => number | bigint; + +class MockWebSocket { + readonly sent: Uint8Array[] = []; + readonly closes: Array<{ code?: number; reason?: string }> = []; + bufferedAmount = 0; + + send(message: Uint8Array): void { + this.sent.push(message.slice()); + } + + close(code?: number, reason?: string): void { + this.closes.push({ code, reason }); + } +} + +function call( + host: WebSocketHost, + name: string, + ...parameters: Array +): number | bigint { + const imported = host.imports[name]; + if (typeof imported !== "function") { + throw new Error(`missing host import ${name}`); + } + return (imported as HostCall)(...parameters); +} + +class MockOutboundWebSocket extends MockWebSocket { + binaryType: "blob" | "arraybuffer" = "blob"; + private readonly listeners = new Map< + string, + Array<(event: { data?: string | ArrayBuffer }) => void> + >(); + + addEventListener( + type: string, + listener: (event: { data?: string | ArrayBuffer }) => void, + ): void { + const listeners = this.listeners.get(type) ?? []; + listeners.push(listener); + this.listeners.set(type, listeners); + } + + emit(type: string, data?: string | ArrayBuffer): void { + for (const listener of this.listeners.get(type) ?? []) { + listener({ data }); + } + } +} + +function fixture(): { + host: WebSocketHost; + memory: WebAssembly.Memory; + socket: MockWebSocket; + handle: bigint; +} { + const host = new WebSocketHost(); + const memory = new WebAssembly.Memory({ initial: 1 }); + host.bindMemory(memory); + const socket = new MockWebSocket(); + const handle = host.register(socket as unknown as WebSocket); + host.transferToGuest(handle); + return { host, memory, socket, handle }; +} + +function encodeTcpPortLease( + memory: WebAssembly.Memory, + pointer: number, + port: number, + purpose = 6, +): number { + const options = new Uint8Array(memory.buffer, pointer, 48); + options.fill(0); + options[0] = 2; + options[1] = 4; + const view = new DataView(memory.buffer, pointer, options.byteLength); + view.setUint16(18, port, false); + options[42] = purpose; + return options.byteLength; +} + +describe("WebSocketHost", () => { + it("isolates application event handler failures from the guest", () => { + const log = vi.spyOn(console, "error").mockImplementation(() => {}); + const host = new WebSocketHost(undefined, () => { + throw new Error("application callback failed"); + }); + const memory = new WebAssembly.Memory({ initial: 1 }); + host.bindMemory(memory); + const kind = new TextEncoder().encode("peer_added"); + const message = new TextEncoder().encode("connected"); + new Uint8Array(memory.buffer, 64, kind.byteLength).set(kind); + new Uint8Array(memory.buffer, 128, message.byteLength).set(message); + + expect( + call( + host, + "emit_event", + 2n, + 64, + kind.byteLength, + 128, + message.byteLength, + ), + ).toBe(0); + expect(log).toHaveBeenCalledWith( + expect.stringContaining("easytier_event_handler_failed"), + ); + log.mockRestore(); + }); + + it("releases every socket and pending operation on shutdown", () => { + const { host, socket, handle } = fixture(); + expect( + call(host, "start_tunnel_receive", handle, 99n, 1024), + ).toBe(0); + expect(host.health()).toEqual({ + connections: 1, + queuedBytes: 0, + pendingOperations: 1, + }); + + host.shutdown(); + host.shutdown(); + + expect(host.health()).toEqual({ + connections: 0, + queuedBytes: 0, + pendingOperations: 0, + }); + expect(socket.closes).toEqual([ + { code: 1000, reason: "EasyTier runtime stopped" }, + ]); + }); + + it("opens outbound WebSockets as guest-owned tunnel handles", () => { + const sockets: MockOutboundWebSocket[] = []; + const urls: string[] = []; + const host = new WebSocketHost((url) => { + urls.push(url); + const socket = new MockOutboundWebSocket(); + sockets.push(socket); + return socket as unknown as WebSocket; + }); + const memory = new WebAssembly.Memory({ initial: 1 }); + host.bindMemory(memory); + let wakes = 0; + host.setWakeGuest(() => { + wakes += 1; + }); + const encoded = new TextEncoder().encode("wss://relay.example/"); + new Uint8Array(memory.buffer, 64, encoded.byteLength).set(encoded); + + expect( + call( + host, + "start_tunnel_connect", + 1n, + 64, + encoded.byteLength, + ), + ).toBe(0); + expect(call(host, "take_tunnel_connect", 1n)).toBe(-1n); + expect(urls).toEqual(["wss://relay.example/"]); + const socket = sockets[0]; + expect(socket).toBeDefined(); + if (socket === undefined) { + throw new Error("outbound WebSocket was not created"); + } + expect(socket.binaryType).toBe("arraybuffer"); + + socket.emit("open"); + expect(wakes).toBe(1); + const handle = call(host, "take_tunnel_connect", 1n); + expect(typeof handle).toBe("bigint"); + expect(handle).toBeGreaterThan(0n); + expect(host.health()).toEqual({ + connections: 1, + queuedBytes: 0, + pendingOperations: 0, + }); + }); + + it("reports outbound WebSocket failure before ownership transfer", () => { + const socket = new MockOutboundWebSocket(); + const host = new WebSocketHost( + () => socket as unknown as WebSocket, + ); + const memory = new WebAssembly.Memory({ initial: 1 }); + host.bindMemory(memory); + const encoded = new TextEncoder().encode("ws://relay.example/"); + new Uint8Array(memory.buffer, 32, encoded.byteLength).set(encoded); + + expect( + call( + host, + "start_tunnel_connect", + 2n, + 32, + encoded.byteLength, + ), + ).toBe(0); + socket.emit("error"); + expect(call(host, "take_tunnel_connect", 2n)).toBe(-10n); + expect(host.health().connections).toBe(0); + }); + + it("rejects outbound WebSocket requests when no factory is installed", () => { + const host = new WebSocketHost(); + const memory = new WebAssembly.Memory({ initial: 1 }); + host.bindMemory(memory); + const encoded = new TextEncoder().encode("ws://relay.example/"); + new Uint8Array(memory.buffer, 16, encoded.byteLength).set(encoded); + + expect( + call( + host, + "start_tunnel_connect", + 3n, + 16, + encoded.byteLength, + ), + ).toBe(-4); + }); + + it("leases browser-local TCP ports for the smoltcp data plane", () => { + const host = new WebSocketHost(() => { + throw new Error("unexpected WebSocket connection"); + }); + const memory = new WebAssembly.Memory({ initial: 1 }); + host.bindMemory(memory); + const optionsLength = encodeTcpPortLease(memory, 128, 55_000); + + expect(call(host, "start_tcp_bind", 4n, 128, optionsLength)).toBe(0); + expect(call(host, "start_tcp_bind", 4n, 128, optionsLength)).toBe(-3); + expect(call(host, "take_tcp_bind", 4n, 256, 35)).toBe(0); + + const result = new Uint8Array(memory.buffer, 256, 35); + const resultView = new DataView( + result.buffer, + result.byteOffset, + result.byteLength, + ); + const handle = resultView.getBigUint64(0, false); + expect(handle).toBeGreaterThan(0n); + expect(result[8]).toBe(4); + expect(resultView.getUint16(25, false)).toBe(55_000); + + expect(call(host, "start_tcp_bind", 5n, 128, optionsLength)).toBe(-3); + expect(call(host, "close", handle)).toBe(0); + expect(call(host, "start_tcp_bind", 6n, 128, optionsLength)).toBe(0); + expect(call(host, "cancel_operation", 6n)).toBe(0); + }); + + it("rejects host TCP listeners other than browser port leases", () => { + const host = new WebSocketHost(() => { + throw new Error("unexpected WebSocket connection"); + }); + const memory = new WebAssembly.Memory({ initial: 1 }); + host.bindMemory(memory); + + const optionsLength = encodeTcpPortLease(memory, 128, 0, 4); + expect(call(host, "start_tcp_bind", 7n, 128, optionsLength)).toBe(-4); + + const inboundOnlyHost = new WebSocketHost(); + inboundOnlyHost.bindMemory(memory); + encodeTcpPortLease(memory, 128, 0); + expect( + call(inboundOnlyHost, "start_tcp_bind", 8n, 128, optionsLength), + ).toBe(-4); + }); + + it("preserves binary message boundaries", () => { + const { host, memory, handle } = fixture(); + host.receive(handle, new Uint8Array([1, 2, 3]).buffer); + host.receive(handle, new Uint8Array([8, 9]).buffer); + + expect(call(host, "start_tunnel_receive", handle, 10n, 64)).toBe( + 0, + ); + expect(call(host, "take_tunnel_receive", 10n, 100, 64)).toBe(3); + expect(new Uint8Array(memory.buffer, 100, 3)).toEqual( + new Uint8Array([1, 2, 3]), + ); + + expect(call(host, "start_tunnel_receive", handle, 11n, 64)).toBe( + 0, + ); + expect(call(host, "take_tunnel_receive", 11n, 200, 64)).toBe(2); + expect(new Uint8Array(memory.buffer, 200, 2)).toEqual( + new Uint8Array([8, 9]), + ); + }); + + it("accounts for a ready receive until its probed message is taken", () => { + const { host, memory, handle } = fixture(); + expect(call(host, "start_tunnel_receive", handle, 12n, 64)).toBe( + 0, + ); + + host.receive(handle, new Uint8Array([1, 2, 3]).buffer); + expect(host.health()).toEqual({ + connections: 1, + queuedBytes: 3, + pendingOperations: 1, + }); + expect(call(host, "take_tunnel_receive", 12n, 0, 0)).toBe(3); + expect(host.health().queuedBytes).toBe(3); + + expect(call(host, "take_tunnel_receive", 12n, 500, 3)).toBe(3); + expect(new Uint8Array(memory.buffer, 500, 3)).toEqual( + new Uint8Array([1, 2, 3]), + ); + expect(host.health()).toEqual({ + connections: 1, + queuedBytes: 0, + pendingOperations: 0, + }); + + expect(call(host, "start_tunnel_receive", handle, 13n, 64)).toBe( + 0, + ); + host.receive(handle, new Uint8Array([4, 5]).buffer); + expect(call(host, "cancel_operation", 13n)).toBe(0); + expect(host.health().queuedBytes).toBe(0); + + expect(call(host, "start_tunnel_receive", handle, 14n, 64)).toBe( + 0, + ); + host.receive(handle, new ArrayBuffer(0)); + expect(call(host, "take_tunnel_receive", 14n, 0, 0)).toBe(0); + expect(call(host, "take_tunnel_receive", 14n, 1, 0)).toBe(0); + expect(host.health().pendingOperations).toBe(0); + }); + + it("copies outbound data before returning from submit", () => { + const { host, memory, socket, handle } = fixture(); + const source = new Uint8Array(memory.buffer, 300, 3); + source.set([4, 5, 6]); + + expect(call(host, "start_tunnel_send", handle, 20n, 300, 3)).toBe( + 0, + ); + source.fill(9); + expect(call(host, "take_tunnel_send", 20n)).toBe(0); + expect(socket.sent).toEqual([new Uint8Array([4, 5, 6])]); + }); + + it("counts a ready receive against the per-connection byte limit", () => { + const { host, socket, handle } = fixture(); + const megabyte = 1024 * 1024; + expect( + call( + host, + "start_tunnel_receive", + handle, + 21n, + megabyte, + ), + ).toBe(0); + + host.receive(handle, new ArrayBuffer(megabyte)); + host.receive(handle, new ArrayBuffer(megabyte)); + expect(host.health().queuedBytes).toBe(2 * megabyte); + + host.receive(handle, new ArrayBuffer(1)); + expect(socket.closes).toContainEqual({ + code: 1009, + reason: "receive queue limit", + }); + expect(host.health().queuedBytes).toBe(2 * megabyte); + + expect(call(host, "close", handle)).toBe(0); + expect(host.health().queuedBytes).toBe(0); + }); + + it("rejects text before exposing the tunnel payload ABI", () => { + const { host, socket, handle } = fixture(); + host.receive(handle, "not a packet"); + expect(socket.closes).toContainEqual({ + code: 1003, + reason: "binary tunnel payload required", + }); + expect(call(host, "start_tunnel_receive", handle, 30n, 64)).toBe(0); + expect(call(host, "take_tunnel_receive", 30n, 400, 64)).toBe(-3); + }); + + it("reports remote close as tunnel EOF", () => { + const { host, handle } = fixture(); + expect(call(host, "start_tunnel_receive", handle, 31n, 64)).toBe(0); + host.remoteClose(handle); + expect(call(host, "take_tunnel_receive", 31n, 400, 64)).toBe( + -10, + ); + }); + + it("closes resources and cancellation idempotently", () => { + const { host, socket, handle } = fixture(); + expect(call(host, "start_tunnel_receive", handle, 40n, 64)).toBe( + 0, + ); + expect(call(host, "cancel_operation", 40n)).toBe(0); + expect(call(host, "cancel_operation", 40n)).toBe(0); + expect(call(host, "close", handle)).toBe(0); + expect(call(host, "close", handle)).toBe(0); + + expect(socket.closes).toHaveLength(1); + expect(host.health()).toEqual({ + connections: 0, + queuedBytes: 0, + pendingOperations: 0, + }); + }); +}); diff --git a/easytier-js/runtime/tsconfig.build.json b/easytier-js/runtime/tsconfig.build.json new file mode 100644 index 00000000..c281df14 --- /dev/null +++ b/easytier-js/runtime/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "noEmit": false, + "outDir": "dist", + "rootDir": "src", + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["test/**/*.ts"] +} diff --git a/easytier-js/runtime/tsconfig.json b/easytier-js/runtime/tsconfig.json new file mode 100644 index 00000000..2dd9ddee --- /dev/null +++ b/easytier-js/runtime/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "types": ["@types/node"], + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "noUncheckedIndexedAccess": true, + "useDefineForClassFields": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/easytier/src/instance/config.rs b/easytier/src/instance/config.rs index fa4dec9d..d4451275 100644 --- a/easytier/src/instance/config.rs +++ b/easytier/src/instance/config.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use easytier_core::{ - config::peers::HostRoutingPolicy, instance::CoreInstanceHostConfig, + config::peers::HostRoutingPolicy, + instance::{CoreConnectivityMode, CoreInstanceHostConfig}, peers::credential_manager::CredentialStorage, }; use strum::VariantArray as _; @@ -58,6 +59,7 @@ pub(crate) fn runtime_core_host_config() -> CoreInstanceHostConfig { upnp_enabled: cfg!(feature = "upnp"), tcp_hole_punching_enabled: cfg!(feature = "tcp-hole-punch"), ignore_unsupported_config: false, + connectivity: CoreConnectivityMode::Full, easytier_version: EASYTIER_VERSION.to_owned(), endpoint_protocols: IpScheme::VARIANTS.iter().map(ToString::to_string).collect(), } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 66fefd5f..6b8720a4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,6 +6,7 @@ packages: - 'tauri-plugin-vpnservice' overrides: + happy-dom: 16.8.1 minimatch: 10.2.4 allowBuilds: