diff --git a/Cargo.lock b/Cargo.lock index 807073c09..142e332de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2371,6 +2371,7 @@ dependencies = [ "serde_json", "sha2", "snow", + "static_assertions", "strum", "test-case", "test-log", diff --git a/node/Native.d.ts b/node/Native.d.ts index e2cdf3cf0..b37b7451c 100644 --- a/node/Native.d.ts +++ b/node/Native.d.ts @@ -162,7 +162,7 @@ export function AuthCredentialWithPniResponse_CheckValidContents(bytes: Buffer): export function AuthCredentialWithPni_CheckValidContents(bytes: Buffer): void; export function AuthenticatedChatConnection_connect(asyncRuntime: Wrapper, connectionManager: Wrapper, username: string, password: string, receiveStories: boolean): CancellablePromise; export function AuthenticatedChatConnection_disconnect(asyncRuntime: Wrapper, chat: Wrapper): CancellablePromise; -export function AuthenticatedChatConnection_info(chat: Wrapper): ConnectionInfo; +export function AuthenticatedChatConnection_info(chat: Wrapper): ChatConnectionInfo; export function AuthenticatedChatConnection_init_listener(chat: Wrapper, listener: ChatListener): void; export function AuthenticatedChatConnection_send(asyncRuntime: Wrapper, chat: Wrapper, httpRequest: Wrapper, timeoutMillis: number): CancellablePromise; export function BackupAuthCredentialPresentation_CheckValidContents(presentationBytes: Buffer): void; @@ -205,6 +205,9 @@ export function Cds2ClientState_New(mrenclave: Buffer, attestationMsg: Buffer, c export function CdsiLookup_complete(asyncRuntime: Wrapper, lookup: Wrapper): CancellablePromise; export function CdsiLookup_new(asyncRuntime: Wrapper, connectionManager: Wrapper, username: string, password: string, request: Wrapper): CancellablePromise; export function CdsiLookup_token(lookup: Wrapper): Buffer; +export function ChatConnectionInfo_description(connectionInfo: Wrapper): string; +export function ChatConnectionInfo_ip_version(connectionInfo: Wrapper): number; +export function ChatConnectionInfo_local_port(connectionInfo: Wrapper): number; export function ChatService_SetListenerAuth(runtime: Wrapper, chat: Wrapper, listener: ChatListener | null): void; export function ChatService_SetListenerUnauth(runtime: Wrapper, chat: Wrapper, listener: ChatListener | null): void; export function ChatService_auth_send(asyncRuntime: Wrapper, chat: Wrapper, httpRequest: Wrapper, timeoutMillis: number): CancellablePromise; @@ -223,8 +226,6 @@ export function CiphertextMessage_Type(msg: Wrapper): number; export function ComparableBackup_GetComparableString(backup: Wrapper): string; export function ComparableBackup_GetUnknownFields(backup: Wrapper): string[]; export function ComparableBackup_ReadUnencrypted(stream: InputStream, len: bigint, purpose: number): Promise; -export function ConnectionInfo_ip_version(connectionInfo: Wrapper): number; -export function ConnectionInfo_local_port(connectionInfo: Wrapper): number; export function ConnectionManager_clear_proxy(connectionManager: Wrapper): void; export function ConnectionManager_new(environment: number, userAgent: string): ConnectionManager; export function ConnectionManager_on_network_change(connectionManager: Wrapper): void; @@ -569,7 +570,7 @@ export function TokioAsyncContext_cancel(context: Wrapper, ra export function TokioAsyncContext_new(): TokioAsyncContext; export function UnauthenticatedChatConnection_connect(asyncRuntime: Wrapper, connectionManager: Wrapper): CancellablePromise; export function UnauthenticatedChatConnection_disconnect(asyncRuntime: Wrapper, chat: Wrapper): CancellablePromise; -export function UnauthenticatedChatConnection_info(chat: Wrapper): ConnectionInfo; +export function UnauthenticatedChatConnection_info(chat: Wrapper): ChatConnectionInfo; export function UnauthenticatedChatConnection_init_listener(chat: Wrapper, listener: ChatListener): void; export function UnauthenticatedChatConnection_send(asyncRuntime: Wrapper, chat: Wrapper, httpRequest: Wrapper, timeoutMillis: number): CancellablePromise; export function UnidentifiedSenderMessageContent_Deserialize(data: Buffer): UnidentifiedSenderMessageContent; @@ -598,10 +599,10 @@ interface Aes256GcmSiv { readonly __type: unique symbol; } interface AuthChat { readonly __type: unique symbol; } interface AuthenticatedChatConnection { readonly __type: unique symbol; } interface CdsiLookup { readonly __type: unique symbol; } +interface ChatConnectionInfo { readonly __type: unique symbol; } interface CiphertextMessage { readonly __type: unique symbol; } interface ComparableBackup { readonly __type: unique symbol; } interface ComparableBackup { readonly __type: unique symbol; } -interface ConnectionInfo { readonly __type: unique symbol; } interface ConnectionManager { readonly __type: unique symbol; } interface DecryptionErrorMessage { readonly __type: unique symbol; } interface ExpiringProfileKeyCredential { readonly __type: unique symbol; } diff --git a/node/ts/net.ts b/node/ts/net.ts index dbaf90b50..a16f5c765 100644 --- a/node/ts/net.ts +++ b/node/ts/net.ts @@ -242,19 +242,20 @@ export type ChatConnection = { export interface ConnectionInfo { localPort: number; ipVersion: 'IPv4' | 'IPv6'; + toString: () => string; } class ConnectionInfoImpl - implements Wrapper, ConnectionInfo + implements Wrapper, ConnectionInfo { - constructor(public _nativeHandle: Native.ConnectionInfo) {} + constructor(public _nativeHandle: Native.ChatConnectionInfo) {} public get localPort(): number { - return Native.ConnectionInfo_local_port(this); + return Native.ChatConnectionInfo_local_port(this); } public get ipVersion(): 'IPv4' | 'IPv6' { - const value = Native.ConnectionInfo_ip_version(this); + const value = Native.ChatConnectionInfo_ip_version(this); switch (value) { case 1: return 'IPv4'; @@ -264,6 +265,10 @@ class ConnectionInfoImpl throw new TypeError(`ip type was unexpectedly ${value}`); } } + + public toString() : string { + return Native.ChatConnectionInfo_description(this) + } } export class UnauthenticatedChatConnection implements ChatConnection { diff --git a/rust/bridge/ffi/cbindgen.toml b/rust/bridge/ffi/cbindgen.toml index 3bd5b95cf..29c9d2cc9 100644 --- a/rust/bridge/ffi/cbindgen.toml +++ b/rust/bridge/ffi/cbindgen.toml @@ -83,6 +83,7 @@ include = [ "libsignal-account-keys", "libsignal-bridge-types", "libsignal-core", + "libsignal-net", "libsignal-protocol", "mediasan-common", "signal-crypto", diff --git a/rust/bridge/shared/src/net.rs b/rust/bridge/shared/src/net.rs index 6962d8629..087bab77a 100644 --- a/rust/bridge/shared/src/net.rs +++ b/rust/bridge/shared/src/net.rs @@ -7,9 +7,9 @@ use std::num::NonZeroU16; use base64::prelude::{Engine, BASE64_STANDARD}; use libsignal_bridge_macros::bridge_fn; -use libsignal_bridge_types::net::ConnectionInfo; pub use libsignal_bridge_types::net::{ConnectionManager, Environment, TokioAsyncContext}; use libsignal_net::auth::Auth; +use libsignal_net::chat::ConnectionInfo; use crate::support::*; use crate::*; diff --git a/rust/bridge/shared/src/net/chat.rs b/rust/bridge/shared/src/net/chat.rs index 625d8fcea..ced0cec11 100644 --- a/rust/bridge/shared/src/net/chat.rs +++ b/rust/bridge/shared/src/net/chat.rs @@ -9,7 +9,7 @@ use http::uri::InvalidUri; use http::{HeaderName, HeaderValue, StatusCode}; use libsignal_bridge_macros::{bridge_fn, bridge_io}; use libsignal_bridge_types::net::chat::*; -use libsignal_bridge_types::net::{ConnectionInfo, ConnectionManager, TokioAsyncContext}; +use libsignal_bridge_types::net::{ConnectionManager, TokioAsyncContext}; use libsignal_bridge_types::support::AsType; use libsignal_net::auth::Auth; use libsignal_net::chat::{ @@ -61,13 +61,18 @@ fn HttpRequest_add_header( } #[bridge_fn(jni = false)] -fn ConnectionInfo_local_port(connection_info: &ConnectionInfo) -> u16 { - connection_info.0.local_port +fn ChatConnectionInfo_local_port(connection_info: &ChatConnectionInfo) -> u16 { + connection_info.transport_info.local_port } #[bridge_fn(jni = false)] -fn ConnectionInfo_ip_version(connection_info: &ConnectionInfo) -> u8 { - connection_info.0.ip_version as u8 +fn ChatConnectionInfo_ip_version(connection_info: &ChatConnectionInfo) -> u8 { + connection_info.transport_info.ip_version as u8 +} + +#[bridge_fn(jni = false)] +fn ChatConnectionInfo_description(connection_info: &ChatConnectionInfo) -> String { + connection_info.to_string() } #[bridge_fn] @@ -127,7 +132,7 @@ async fn UnauthenticatedChatConnection_disconnect(chat: &UnauthenticatedChatConn } #[bridge_fn(jni = false)] -fn UnauthenticatedChatConnection_info(chat: &UnauthenticatedChatConnection) -> ConnectionInfo { +fn UnauthenticatedChatConnection_info(chat: &UnauthenticatedChatConnection) -> ChatConnectionInfo { chat.info() } @@ -177,7 +182,7 @@ async fn AuthenticatedChatConnection_disconnect(chat: &AuthenticatedChatConnecti } #[bridge_fn(jni = false)] -fn AuthenticatedChatConnection_info(chat: &AuthenticatedChatConnection) -> ConnectionInfo { +fn AuthenticatedChatConnection_info(chat: &AuthenticatedChatConnection) -> ChatConnectionInfo { chat.info() } diff --git a/rust/bridge/shared/types/src/net.rs b/rust/bridge/shared/types/src/net.rs index ece1d4d19..7cbccd24e 100644 --- a/rust/bridge/shared/types/src/net.rs +++ b/rust/bridge/shared/types/src/net.rs @@ -57,10 +57,6 @@ impl Environment { } } -// Trivial wrapper type that cbindgen will treat as opaque. -pub struct ConnectionInfo(pub libsignal_net::infra::ConnectionInfo); -bridge_as_handle!(ConnectionInfo); - const CONNECT_PARAMS: ConnectionOutcomeParams = { ConnectionOutcomeParams { age_cutoff: Duration::from_secs(5 * 60), diff --git a/rust/bridge/shared/types/src/net/chat.rs b/rust/bridge/shared/types/src/net/chat.rs index 07b2e9638..6687146f9 100644 --- a/rust/bridge/shared/types/src/net/chat.rs +++ b/rust/bridge/shared/types/src/net/chat.rs @@ -11,25 +11,28 @@ use std::time::Duration; use atomic_take::AtomicTake; use futures_util::stream::BoxStream; -use futures_util::StreamExt as _; +use futures_util::{FutureExt as _, StreamExt as _}; use http::status::InvalidStatusCode; use http::uri::{InvalidUri, PathAndQuery}; use http::{HeaderMap, HeaderName, HeaderValue}; use libsignal_net::auth::Auth; use libsignal_net::chat::{ - self, ChatConnection, ChatServiceError, DebugInfo as ChatServiceDebugInfo, Request, - Response as ChatResponse, + self, ChatConnection, ChatServiceError, ConnectionInfo, DebugInfo as ChatServiceDebugInfo, + Request, Response as ChatResponse, }; use libsignal_net::infra::route::{ConnectionProxyConfig, DirectOrProxyProvider}; use libsignal_net::infra::tcp_ssl::InvalidProxyConfig; -use libsignal_net::infra::Connection; use libsignal_protocol::Timestamp; use tokio::sync::{mpsc, oneshot}; -use crate::net::{ConnectionInfo, ConnectionManager, TokioAsyncContext}; +use crate::net::{ConnectionManager, TokioAsyncContext}; use crate::support::*; use crate::*; +pub type ChatConnectionInfo = ConnectionInfo; + +bridge_as_handle!(ChatConnectionInfo); + enum ChatListenerState { Inactive(BoxStream<'static, chat::server_requests::ServerEvent>), Active { @@ -251,7 +254,7 @@ enum MaybeChatConnection { impl UnauthenticatedChatConnection { pub async fn connect(connection_manager: &ConnectionManager) -> Result { - let inner = establish_chat_connection(connection_manager, None).await?; + let inner = establish_chat_connection("unauthenticated", connection_manager, None).await?; log::info!("connected unauthenticated chat"); Ok(Self { inner: MaybeChatConnection::WaitingForListener( @@ -268,7 +271,8 @@ impl AuthenticatedChatConnection { auth: Auth, receive_stories: bool, ) -> Result { - let pending = establish_chat_connection( + let inner = establish_chat_connection( + "authenticated", connection_manager, Some(chat::AuthenticatedChatHeaders { auth, @@ -276,11 +280,10 @@ impl AuthenticatedChatConnection { }), ) .await?; - log::info!("connected authenticated chat"); Ok(Self { inner: MaybeChatConnection::WaitingForListener( tokio::runtime::Handle::current(), - pending, + inner, ) .into(), }) @@ -340,13 +343,15 @@ impl> + Sync> BridgeChatConnec fn info(&self) -> ConnectionInfo { let guard = self.as_ref().blocking_read(); - ConnectionInfo(match &*guard { + let connection_info = match &*guard { MaybeChatConnection::Running(chat_connection) => chat_connection.connection_info(), MaybeChatConnection::WaitingForListener(_, pending_chat_connection) => { pending_chat_connection.connection_info() } MaybeChatConnection::TemporarilyEvicted => unreachable!("unobservable state"), - }) + }; + + connection_info.clone() } } @@ -371,6 +376,7 @@ fn init_listener(connection: &mut MaybeChatConnection, listener: Box, ) -> Result { @@ -404,6 +410,7 @@ async fn establish_chat_connection( } = ws_config; let chat_connect = &env.chat_domain_config.connect; + log::info!("connecting {auth_type} chat"); ChatConnection::start_connect_with( connect, @@ -423,6 +430,10 @@ async fn establish_chat_connection( }, auth, ) + .inspect(|r| match r { + Ok(_) => log::info!("successfully connected {auth_type} chat"), + Err(e) => log::warn!("failed to connect {auth_type} chat: {e}"), + }) .await } diff --git a/rust/net/Cargo.toml b/rust/net/Cargo.toml index 7076977c7..8482570f3 100644 --- a/rust/net/Cargo.toml +++ b/rust/net/Cargo.toml @@ -48,6 +48,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha2 = { workspace = true } snow = { workspace = true } +static_assertions = { workspace = true } strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["rt", "time", "macros"] } diff --git a/rust/net/infra/src/errors.rs b/rust/net/infra/src/errors.rs index 59c0cb60d..a6df105eb 100644 --- a/rust/net/infra/src/errors.rs +++ b/rust/net/infra/src/errors.rs @@ -11,6 +11,10 @@ use crate::certs; pub trait LogSafeDisplay: Display {} +/// Vacuous implementation since you can't actually [`Display::fmt`] a +/// [`std::convert::Infallible`]. +impl LogSafeDisplay for std::convert::Infallible {} + /// Errors that can occur during transport-level connection establishment. #[derive(displaydoc::Display, Debug, thiserror::Error)] pub enum TransportConnectError { diff --git a/rust/net/infra/src/lib.rs b/rust/net/infra/src/lib.rs index 6e4cff111..9bae9d92a 100644 --- a/rust/net/infra/src/lib.rs +++ b/rust/net/infra/src/lib.rs @@ -20,7 +20,7 @@ use crate::certs::RootCertificates; use crate::connection_manager::{ MultiRouteConnectionManager, SingleRouteThrottlingConnectionManager, }; -use crate::errors::TransportConnectError; +use crate::errors::{LogSafeDisplay, TransportConnectError}; use crate::host::Host; use crate::timeouts::{WS_KEEP_ALIVE_INTERVAL, WS_MAX_IDLE_INTERVAL}; use crate::utils::ObservableEvent; @@ -68,6 +68,13 @@ impl From<&IpAddr> for IpType { } } +impl LogSafeDisplay for IpType {} +impl std::fmt::Display for IpType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } +} + /// Whether or not to enable domain fronting. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] pub struct EnableDomainFronting(pub bool); @@ -179,7 +186,7 @@ pub struct ServiceConnectionInfo { /// remote host. #[derive(Debug, Clone)] #[cfg_attr(test, derive(PartialEq))] -pub struct ConnectionInfo { +pub struct TransportInfo { /// The IP address version over which the connection is established. pub ip_version: IpType, @@ -189,8 +196,8 @@ pub struct ConnectionInfo { /// An established connection. pub trait Connection { - /// Returns information about the connection. - fn connection_info(&self) -> ConnectionInfo; + /// Returns transport-level information about the connection. + fn transport_info(&self) -> TransportInfo; } /// Source for the result of a hostname lookup. @@ -215,7 +222,7 @@ pub enum DnsSource { } /// Type of the route used for the connection. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, strum::Display)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, strum::Display, strum::IntoStaticStr)] #[strum(serialize_all = "lowercase")] pub enum RouteType { /// Direct connection to the service. diff --git a/rust/net/infra/src/route.rs b/rust/net/infra/src/route.rs index b20eecb9c..b69598c4e 100644 --- a/rust/net/infra/src/route.rs +++ b/rust/net/infra/src/route.rs @@ -14,12 +14,16 @@ use futures_util::{FutureExt, StreamExt}; use tokio::time::Instant; use tokio_util::either::Either; +use crate::errors::LogSafeDisplay; use crate::host::Host; use crate::utils::future::SomeOrPending; mod connect; pub use connect::*; +mod describe; +pub use describe::*; + mod http; pub use http::*; @@ -153,10 +157,10 @@ pub struct OutcomeUpdates { /// /// The `Future` returned by this function resolves when all connection attempts /// are exhausted or a one of them produces a fatal error. -pub async fn connect( +pub async fn connect( route_resolver: &RouteResolver, - delay_policy: &impl RouteDelayPolicy, - route_provider: P, + delay_policy: impl RouteDelayPolicy, + ordered_routes: impl Iterator, resolver: &impl Resolver, connector: C, mut on_error: impl FnMut(C::Error) -> ControlFlow, @@ -167,11 +171,9 @@ pub async fn connect( where R: Clone, C: Connector, - P: RouteProvider, - P::Route: ResolveHostnames + Clone + 'static, + UR: ResolveHostnames + Clone + 'static, R: ResolvedRoute, { - let ordered_routes = route_provider.routes(); let resolver_stream = route_resolver.resolve(ordered_routes, resolver); let schedule = Some(Schedule::new( resolver_stream, @@ -283,6 +285,17 @@ where ) } +impl LogSafeDisplay for ConnectError {} +impl std::fmt::Display for ConnectError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConnectError::NoResolvedRoutes => f.write_str("no resolved routes"), + ConnectError::AllAttemptsFailed => f.write_str("all connect attempts failed"), + ConnectError::FatalConnect(e) => write!(f, "fatal connect error: {e}"), + } + } +} + const PER_CONNECTION_WAIT_DURATION: Duration = Duration::from_millis(500); fn pull_next_route_delay(connects_in_progress: &FuturesUnordered) -> Duration { @@ -299,6 +312,12 @@ impl RouteProvider for &R { } } +impl From for Arc { + fn from(value: UnresolvedHost) -> Self { + value.0 + } +} + impl From> for UnresolvedHost { fn from(value: Arc) -> Self { Self(value) @@ -407,6 +426,7 @@ mod test { http_host: "front-host".into(), sni_list: vec!["front-sni1".into(), "front-sni2".into()], path_prefix: "/front-host-path-prefix".into(), + front_name: "front-host", }], http_version: HttpVersion::Http2, }, @@ -434,6 +454,7 @@ mod test { fragment: HttpRouteFragment { host_header: "http-host".into(), path_prefix: "".into(), + front_name: None, }, inner: TlsRoute { fragment: TlsRouteFragment { @@ -458,6 +479,7 @@ mod test { fragment: HttpRouteFragment { host_header: "front-host".into(), path_prefix: "/front-host-path-prefix".into(), + front_name: Some("front-host"), }, inner: TlsRoute { fragment: TlsRouteFragment { @@ -482,6 +504,7 @@ mod test { fragment: HttpRouteFragment { host_header: "front-host".into(), path_prefix: "/front-host-path-prefix".into(), + front_name: Some("front-host"), }, inner: TlsRoute { fragment: TlsRouteFragment { @@ -702,8 +725,7 @@ mod test { &outcomes, HOSTNAMES .iter() - .map(|(h, _addr)| FakeRoute(UnresolvedHost::from(Arc::from(*h)))) - .collect_vec(), + .map(|(h, _addr)| FakeRoute(UnresolvedHost::from(Arc::from(*h)))), &resolver, connector, |_err: FakeConnectError| ControlFlow::::Continue(()), @@ -802,8 +824,7 @@ mod test { &outcomes, HOSTNAMES .iter() - .map(|(h, _addr)| FakeRoute(UnresolvedHost::from(Arc::from(*h)))) - .collect_vec(), + .map(|(h, _addr)| FakeRoute(UnresolvedHost::from(Arc::from(*h)))), &resolver, connector, |_err: FakeConnectError| ControlFlow::::Continue(()), diff --git a/rust/net/infra/src/route/connect/throttle.rs b/rust/net/infra/src/route/connect/throttle.rs index 98aab9354..499b026f8 100644 --- a/rust/net/infra/src/route/connect/throttle.rs +++ b/rust/net/infra/src/route/connect/throttle.rs @@ -12,7 +12,7 @@ use pin_project::pin_project; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use crate::route::connect::Connector; -use crate::{Connection, ConnectionInfo}; +use crate::{Connection, TransportInfo}; /// [`Connector`] wrapper that limits the number of concurrent connection /// attempts. @@ -139,7 +139,7 @@ impl, T> Sink for ThrottledConnection { } impl Connection for ThrottledConnection { - fn connection_info(&self) -> ConnectionInfo { - self.0.connection_info() + fn transport_info(&self) -> TransportInfo { + self.0.transport_info() } } diff --git a/rust/net/infra/src/route/describe.rs b/rust/net/infra/src/route/describe.rs new file mode 100644 index 000000000..eb286f733 --- /dev/null +++ b/rust/net/infra/src/route/describe.rs @@ -0,0 +1,199 @@ +// +// Copyright 2024 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +use std::future::Future; +use std::net::IpAddr; +use std::num::NonZeroU16; +use std::sync::Arc; + +use futures_util::TryFutureExt as _; +use tokio::time::Instant; + +use crate::dns::dns_utils::log_safe_domain; +use crate::errors::LogSafeDisplay; +use crate::host::Host; +use crate::route::{ + ConnectionProxyKind, ConnectionProxyRoute, Connector, DirectOrProxyRoute, HttpsTlsRoute, + ResolveHostnames, ResolvedRoute, RouteDelayPolicy, SocksRoute, SocksTarget, TcpRoute, TlsRoute, + UnresolvedHost, UnresolvedWebsocketServiceRoute, DEFAULT_HTTPS_PORT, +}; + +/// A type that is not itself loggable but can produce a [`LogSafeDisplay`] +/// value. +pub trait DescribeForLog { + /// The loggable description of `Self`. + type Description: LogSafeDisplay; + + /// Produce a `Self::Description` for logging. + fn describe_for_log(&self) -> Self::Description; +} + +/// Wrapper for a [resolvable](ResolveHostnames) [`Route`](super) that resolves +/// to [`WithLoggableDescription`]. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct ResolveWithSavedDescription(pub R); + +/// A [route](super) with a description for logging. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct WithLoggableDescription { + pub route: R, + pub description: D, +} + +/// [`Connector`] implementation for [`WithLoggableDescription`]. +/// +/// Delegates to the wrapped connector, and produces on success its connection +/// along with the loggable description from the input route. +pub struct DescribedRouteConnector(pub C); + +/// [`RouteDelayPolicy`] for [`WithLoggableDescription`] that delegates to +/// an inner policy. +/// +/// Delegates to an inner `RouteDelayPolicy` that doesn't use the description. +pub struct WithoutLoggableDescription

(pub P); + +impl, R, D> RouteDelayPolicy> + for WithoutLoggableDescription

+{ + fn compute_delay( + &self, + route: &WithLoggableDescription, + now: Instant, + ) -> std::time::Duration { + let WithLoggableDescription { + route, + description: _, + } = route; + self.0.compute_delay(route, now) + } +} + +/// Loggable description for a [`UnresolvedWebsocketServiceRoute`]. +#[derive(Clone, Debug, PartialEq)] +pub struct UnresolvedRouteDescription { + front: Option<&'static str>, + proxy: Option, + target: (Host>, NonZeroU16), +} + +impl ResolveHostnames for ResolveWithSavedDescription { + type Resolved = WithLoggableDescription; + + fn hostnames(&self) -> impl Iterator { + self.0.hostnames() + } + + fn resolve(self, lookup: impl FnMut(&str) -> IpAddr) -> Self::Resolved { + let description = self.0.describe_for_log(); + let resolved = self.0.resolve(lookup); + WithLoggableDescription { + route: resolved, + description, + } + } +} + +impl ResolvedRoute for WithLoggableDescription { + fn immediate_target(&self) -> &IpAddr { + self.route.immediate_target() + } +} + +impl, D: Send> + Connector, Inner> for DescribedRouteConnector +{ + type Connection = (C::Connection, D); + + type Error = C::Error; + + fn connect_over( + &self, + over: Inner, + route: WithLoggableDescription, + ) -> impl Future> + Send { + self.0 + .connect_over(over, route.route) + .map_ok(|c| (c, route.description)) + } +} + +impl LogSafeDisplay for UnresolvedRouteDescription {} +impl std::fmt::Display for UnresolvedRouteDescription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + front, + proxy, + target: (domain, port), + } = self; + write!( + f, + "{}:{port}", + domain.as_deref().map_domain(log_safe_domain) + )?; + if let Some(front) = front { + write!(f, " fronted by {}", front) + } else { + f.write_str(" (direct)") + }?; + if let Some(proxy_kind) = proxy { + write!(f, " through {proxy_kind:?} proxy")? + } + Ok(()) + } +} + +impl DescribeForLog for UnresolvedWebsocketServiceRoute { + type Description = UnresolvedRouteDescription; + + fn describe_for_log(&self) -> Self::Description { + let Self { + fragment: _ws_fragment, + inner: + HttpsTlsRoute { + fragment: http_fragment, + inner: + TlsRoute { + fragment: tls_fragment, + inner: direct_or_proxy, + }, + }, + } = self; + + let target = match direct_or_proxy { + DirectOrProxyRoute::Direct(TcpRoute { address, port }) => { + (Host::Domain(address.clone().into()), *port) + } + DirectOrProxyRoute::Proxy(proxy) => match proxy { + ConnectionProxyRoute::Tls { proxy: _ } | ConnectionProxyRoute::Tcp { proxy: _ } => { + // The host is implicit; the proxy will look for the TLS SNI and resolve that. + (tls_fragment.sni.clone(), DEFAULT_HTTPS_PORT) + } + ConnectionProxyRoute::Socks(SocksRoute { + target_addr, + target_port, + .. + }) => ( + match target_addr { + SocksTarget::ResolvedLocally(host) => host.clone().map_domain(Arc::from), + SocksTarget::ResolvedRemotely { name } => Host::Domain(name.clone()), + }, + *target_port, + ), + }, + }; + + let proxy = match &direct_or_proxy { + DirectOrProxyRoute::Direct(_) => None, + DirectOrProxyRoute::Proxy(proxy) => Some(ConnectionProxyKind::from(proxy)), + }; + let front = http_fragment.front_name; + + UnresolvedRouteDescription { + front, + proxy, + target, + } + } +} diff --git a/rust/net/infra/src/route/http.rs b/rust/net/infra/src/route/http.rs index 69c1c1ff1..fadf2c481 100644 --- a/rust/net/infra/src/route/http.rs +++ b/rust/net/infra/src/route/http.rs @@ -22,6 +22,8 @@ pub const DEFAULT_HTTPS_PORT: NonZeroU16 = nonzero!(443u16); pub struct HttpRouteFragment { pub host_header: Arc, pub path_prefix: Arc, + /// Only for logging; the name of the domain front for this proxy. + pub front_name: Option<&'static str>, } pub type HttpsTlsRoute = SimpleRoute; @@ -61,6 +63,8 @@ pub struct DomainFrontConfig { pub root_certs: RootCertificates, /// A string to prepend to the path of outgoing HTTP requests. pub path_prefix: Arc, + /// A loggable name for the front. + pub front_name: &'static str, } impl HttpsProvider { @@ -103,6 +107,7 @@ impl RouteProvider for DomainFrontRouteProvider { sni_list, root_certs, path_prefix, + front_name, }| { sni_list.iter().map(|sni| HttpsTlsRoute { inner: TlsRoute { @@ -119,6 +124,7 @@ impl RouteProvider for DomainFrontRouteProvider { fragment: HttpRouteFragment { host_header: Arc::clone(http_host), path_prefix: Arc::clone(path_prefix), + front_name: Some(*front_name), }, }) }, @@ -150,6 +156,7 @@ where fragment: HttpRouteFragment { host_header: Arc::clone(direct_host_header), path_prefix: "".into(), + front_name: None, }, inner, } @@ -210,12 +217,14 @@ mod test { sni_list: vec!["front-sni-1a".into(), "front-sni-1b".into()], root_certs: RootCertificates::Native, path_prefix: "/prefix-1".into(), + front_name: "front-1", }, DomainFrontConfig { http_host: "front-host-2".into(), sni_list: vec!["front-sni-2".into()], root_certs: RootCertificates::Native, path_prefix: "/prefix-2".into(), + front_name: "front-2", }, ], http_version: HttpVersion::Http1_1, @@ -239,6 +248,7 @@ mod test { fragment: HttpRouteFragment { host_header: "direct-host".into(), path_prefix: "".into(), + front_name: None, }, inner: TlsRoute { fragment: TlsRouteFragment { @@ -256,6 +266,7 @@ mod test { fragment: HttpRouteFragment { host_header: "front-host-1".into(), path_prefix: "/prefix-1".into(), + front_name: Some("front-1") }, inner: TlsRoute { fragment: TlsRouteFragment { @@ -273,6 +284,7 @@ mod test { fragment: HttpRouteFragment { host_header: "front-host-1".into(), path_prefix: "/prefix-1".into(), + front_name: Some("front-1") }, inner: TlsRoute { fragment: TlsRouteFragment { @@ -290,6 +302,7 @@ mod test { fragment: HttpRouteFragment { host_header: "front-host-2".into(), path_prefix: "/prefix-2".into(), + front_name: Some("front-2") }, inner: TlsRoute { fragment: TlsRouteFragment { diff --git a/rust/net/infra/src/route/proxy.rs b/rust/net/infra/src/route/proxy.rs index 83e0014c6..2a988b58e 100644 --- a/rust/net/infra/src/route/proxy.rs +++ b/rust/net/infra/src/route/proxy.rs @@ -23,7 +23,8 @@ pub struct SocksRoute { pub protocol: socks::Protocol, } -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, strum::EnumDiscriminants)] +#[strum_discriminants(name(ConnectionProxyKind))] pub enum ConnectionProxyRoute { Tls { proxy: TlsRoute>, diff --git a/rust/net/infra/src/route/resolve.rs b/rust/net/infra/src/route/resolve.rs index cd4fbc04f..4a839fcf6 100644 --- a/rust/net/infra/src/route/resolve.rs +++ b/rust/net/infra/src/route/resolve.rs @@ -590,6 +590,7 @@ mod test { let http_fragment = HttpRouteFragment { host_header: "target-domain".into(), path_prefix: "".into(), + front_name: None, }; let tls_fragment = TlsRouteFragment { diff --git a/rust/net/infra/src/tcp_ssl.rs b/rust/net/infra/src/tcp_ssl.rs index 546f22080..5f9fcdd48 100644 --- a/rust/net/infra/src/tcp_ssl.rs +++ b/rust/net/infra/src/tcp_ssl.rs @@ -176,8 +176,8 @@ where } impl Connection for SslStream { - fn connection_info(&self) -> crate::ConnectionInfo { - self.get_ref().connection_info() + fn transport_info(&self) -> crate::TransportInfo { + self.get_ref().transport_info() } } diff --git a/rust/net/infra/src/tcp_ssl/proxy.rs b/rust/net/infra/src/tcp_ssl/proxy.rs index 62db3044c..066f02a2a 100644 --- a/rust/net/infra/src/tcp_ssl/proxy.rs +++ b/rust/net/infra/src/tcp_ssl/proxy.rs @@ -67,18 +67,18 @@ impl Connector, ()> for StatelessProxied { } impl Connection for Either { - fn connection_info(&self) -> crate::ConnectionInfo { + fn transport_info(&self) -> crate::TransportInfo { match self { - Self::Left(l) => l.connection_info(), - Self::Right(r) => r.connection_info(), + Self::Left(l) => l.transport_info(), + Self::Right(r) => r.transport_info(), } } } impl Connection for TcpStream { - fn connection_info(&self) -> crate::ConnectionInfo { + fn transport_info(&self) -> crate::TransportInfo { let local_addr = self.local_addr().expect("has local addr"); - crate::ConnectionInfo { + crate::TransportInfo { ip_version: IpType::from(&local_addr.ip()), local_port: local_addr.port(), } diff --git a/rust/net/infra/src/tcp_ssl/proxy/socks.rs b/rust/net/infra/src/tcp_ssl/proxy/socks.rs index 69bd6ea70..c06fee137 100644 --- a/rust/net/infra/src/tcp_ssl/proxy/socks.rs +++ b/rust/net/infra/src/tcp_ssl/proxy/socks.rs @@ -201,14 +201,14 @@ impl Connector, ()> for super::StatelessProxied { } impl Connection for Socks4Stream { - fn connection_info(&self) -> crate::ConnectionInfo { - (**self).connection_info() + fn transport_info(&self) -> crate::TransportInfo { + (**self).transport_info() } } impl Connection for Socks5Stream { - fn connection_info(&self) -> crate::ConnectionInfo { - (**self).connection_info() + fn transport_info(&self) -> crate::TransportInfo { + (**self).transport_info() } } diff --git a/rust/net/infra/src/ws.rs b/rust/net/infra/src/ws.rs index 1a99f9d07..15d72d942 100644 --- a/rust/net/infra/src/ws.rs +++ b/rust/net/infra/src/ws.rs @@ -176,6 +176,7 @@ where HttpRouteFragment { host_header, path_prefix, + front_name: _, }, ) = route; @@ -616,8 +617,8 @@ impl NextOrClose { impl Connection for tokio_tungstenite::WebSocketStream { - fn connection_info(&self) -> crate::ConnectionInfo { - self.get_ref().connection_info() + fn transport_info(&self) -> crate::TransportInfo { + self.get_ref().transport_info() } } @@ -646,6 +647,7 @@ pub mod testutil { HttpRouteFragment { host_header: "localhost".into(), path_prefix: "".into(), + front_name: None, }, ), ); diff --git a/rust/net/src/cdsi.rs b/rust/net/src/cdsi.rs index 3c55c571b..cc597fbd2 100644 --- a/rust/net/src/cdsi.rs +++ b/rust/net/src/cdsi.rs @@ -356,7 +356,7 @@ impl CdsiConnection { params: &EndpointParams<'_, Cdsi>, auth: Auth, ) -> Result { - let connection = ConnectState::connect_attested_ws( + let (connection, _route_info) = ConnectState::connect_attested_ws( connect, route_provider, auth, diff --git a/rust/net/src/chat.rs b/rust/net/src/chat.rs index 52067fcbe..f10f47a24 100644 --- a/rust/net/src/chat.rs +++ b/rust/net/src/chat.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // +use std::fmt::Display; use std::ops::ControlFlow; use std::sync::Arc; use std::time::Duration; @@ -25,13 +26,13 @@ use libsignal_net_infra::timeouts::{MULTI_ROUTE_CONNECTION_TIMEOUT, ONE_ROUTE_CO use libsignal_net_infra::utils::ObservableEvent; use libsignal_net_infra::ws::{WebSocketClientConnector, WebSocketConnectError}; use libsignal_net_infra::{ - make_ws_config, AsHttpHeader, Connection, ConnectionInfo, EndpointConnection, - HttpRequestDecorator, IpType, TransportConnector, + make_ws_config, AsHttpHeader, Connection, EndpointConnection, HttpRequestDecorator, IpType, + TransportConnector, TransportInfo, }; use crate::auth::Auth; use crate::chat::ws::{ChatOverWebSocketServiceConnector, ServerEvent}; -use crate::connect_state::ConnectState; +use crate::connect_state::{ConnectState, RouteInfo}; use crate::env::{add_user_agent_header, ConnectionConfig, UserAgent}; use crate::proto; @@ -516,17 +517,18 @@ pub fn endpoint_connection( ) } +/// Information about an established connection. +#[derive(Clone, Debug)] +pub struct ConnectionInfo { + pub route_info: RouteInfo, + pub transport_info: TransportInfo, +} + pub struct ChatConnection { inner: self::ws2::Chat, connection_info: ConnectionInfo, } -impl Connection for ChatConnection { - fn connection_info(&self) -> ConnectionInfo { - self.connection_info.clone() - } -} - type ChatConnector = ComposedConnector< ThrottlingConnector, StatelessTransportConnector, @@ -540,12 +542,6 @@ pub struct PendingChatConnection { connection_info: ConnectionInfo, } -impl Connection for PendingChatConnection { - fn connection_info(&self) -> ConnectionInfo { - self.connection_info.clone() - } -} - pub struct AuthenticatedChatHeaders { pub auth: Auth, pub receive_stories: ReceiveStories, @@ -594,18 +590,25 @@ impl ChatConnection { ThrottlingConnector::new(crate::infra::ws::Stateless, 1), resolver, confirmation_header_name.as_ref(), - |error| match error.classify() { - ErrorClass::Intermittent => ControlFlow::Continue(()), - ErrorClass::Fatal | ErrorClass::RetryAt(_) => { - ControlFlow::Break(ChatServiceError::from(error)) + |error| { + log::debug!("connection attempt failed with {error}"); + match error.classify() { + ErrorClass::Intermittent => ControlFlow::Continue(()), + ErrorClass::Fatal | ErrorClass::RetryAt(_) => { + ControlFlow::Break(ChatServiceError::from(error)) + } } }, ) .await; match result { - Ok(ws_connection) => Ok({ - let connection_info = ws_connection.connection_info(); + Ok((ws_connection, route_info)) => Ok({ + let transport_info = ws_connection.transport_info(); + let connection_info = ConnectionInfo { + transport_info, + route_info, + }; PendingChatConnection { connection: ws_connection, connection_info, @@ -634,8 +637,8 @@ impl ChatConnection { ) -> Self { let PendingChatConnection { connection, - connection_info, ws_config, + connection_info, } = pending; Self { inner: crate::chat::ws2::Chat::new(tokio_runtime, connection, ws_config, listener), @@ -657,6 +660,30 @@ impl ChatConnection { pub async fn disconect(&self) { self.inner.disconnect().await } + + pub fn connection_info(&self) -> &ConnectionInfo { + &self.connection_info + } +} + +impl PendingChatConnection { + pub fn connection_info(&self) -> &ConnectionInfo { + &self.connection_info + } +} + +impl Display for ConnectionInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + transport_info: + TransportInfo { + local_port, + ip_version, + }, + route_info, + } = self; + write!(f, "from {ip_version}:{local_port} via {route_info}") + } } #[cfg(any(test, feature = "test-util"))] diff --git a/rust/net/src/connect_state.rs b/rust/net/src/connect_state.rs index 53860e173..fa6119bf2 100644 --- a/rust/net/src/connect_state.rs +++ b/rust/net/src/connect_state.rs @@ -7,13 +7,16 @@ use std::default::Default; use std::ops::ControlFlow; use http::HeaderName; +use itertools::Itertools as _; use libsignal_net_infra::connection_manager::{ErrorClass, ErrorClassifier as _}; use libsignal_net_infra::dns::DnsResolver; +use libsignal_net_infra::errors::LogSafeDisplay; use libsignal_net_infra::route::{ ComposedConnector, ConnectError, ConnectionOutcomeParams, ConnectionOutcomes, Connector, - HttpRouteFragment, RouteProvider, RouteProviderExt as _, RouteResolver, - StatelessTransportConnector, TransportRoute, UnresolvedWebsocketServiceRoute, - WebSocketRouteFragment, WebSocketServiceRoute, + DescribedRouteConnector, HttpRouteFragment, ResolveWithSavedDescription, RouteProvider, + RouteProviderExt as _, RouteResolver, StatelessTransportConnector, TransportRoute, + UnresolvedRouteDescription, UnresolvedWebsocketServiceRoute, WebSocketRouteFragment, + WebSocketServiceRoute, WithLoggableDescription, WithoutLoggableDescription, }; use libsignal_net_infra::ws::WebSocketConnectError; use libsignal_net_infra::ws2::attested::AttestedConnection; @@ -44,6 +47,19 @@ impl ConnectState { } } +#[derive(Clone, Debug, PartialEq)] +pub struct RouteInfo { + unresolved: UnresolvedRouteDescription, +} + +impl LogSafeDisplay for RouteInfo {} +impl std::fmt::Display for RouteInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { unresolved } = self; + (unresolved as &dyn LogSafeDisplay).fmt(f) + } +} + impl ConnectState where TC: Connector + Sync, @@ -56,7 +72,7 @@ where resolver: &DnsResolver, confirmation_header_name: Option<&HeaderName>, mut on_error: impl FnMut(WebSocketServiceConnectError) -> ControlFlow, - ) -> Result> + ) -> Result<(WC::Connection, RouteInfo), ConnectError> where WC: Connector< (WebSocketRouteFragment, HttpRouteFragment), @@ -64,15 +80,27 @@ where Error = tungstenite::Error, > + Send + Sync, + E: LogSafeDisplay, { let connect_read = this.read().await; + let routes = routes.routes().collect_vec(); + + log::info!("starting connection attempt with {} routes", routes.len()); + + let route_provider = routes.into_iter().map(ResolveWithSavedDescription); + let connector = DescribedRouteConnector(ComposedConnector::new( + ws_connector, + &connect_read.transport_connector, + )); + let delay_policy = WithoutLoggableDescription(&connect_read.attempts_record); + let start = Instant::now(); let (result, updates) = crate::infra::route::connect( &connect_read.route_resolver, - &connect_read.attempts_record, - routes, + delay_policy, + route_provider, resolver, - ComposedConnector::new(ws_connector, &connect_read.transport_connector), + connector, |error| { let error = WebSocketServiceConnectError::from_websocket_error( error, @@ -89,12 +117,35 @@ where // doesn't matter. drop(connect_read); - this.write() - .await - .attempts_record - .apply_outcome_updates(updates.outcomes, updates.finished_at); + match &result { + Ok((_connection, route)) => log::info!( + "connection through {route} succeeded after {:.3?}", + start.elapsed() + ), + Err(e) => log::info!("connection failed with {e}"), + } + + this.write().await.attempts_record.apply_outcome_updates( + updates.outcomes.into_iter().map( + |( + WithLoggableDescription { + route, + description: _, + }, + outcome, + )| (route, outcome), + ), + updates.finished_at, + ); - result + result.map(|(connection, description)| { + ( + connection, + RouteInfo { + unresolved: description, + }, + ) + }) } pub(crate) async fn connect_attested_ws( @@ -105,7 +156,7 @@ where confirmation_header_name: Option, ws_config: libsignal_net_infra::ws2::Config, new_handshake: impl FnOnce(&[u8]) -> Result, - ) -> Result + ) -> Result<(AttestedConnection, RouteInfo), crate::enclave::Error> where TC::Connection: AsyncDuplexStream + 'static, { @@ -114,7 +165,7 @@ where route }); - let ws = ConnectState::connect_ws( + let (ws, route_info) = ConnectState::connect_ws( connect, ws_routes, crate::infra::ws::Stateless, @@ -135,9 +186,8 @@ where ConnectError::FatalConnect(e) => e, })?; - AttestedConnection::connect(ws, ws_config, new_handshake) - .await - .map_err(Into::into) + let connection = AttestedConnection::connect(ws, ws_config, new_handshake).await?; + Ok((connection, route_info)) } } @@ -159,7 +209,7 @@ mod test { DirectOrProxyRoute, HttpsTlsRoute, TcpRoute, TlsRoute, TlsRouteFragment, UnresolvedHost, WebSocketRoute, }; - use libsignal_net_infra::{Alpn, DnsSource}; + use libsignal_net_infra::{Alpn, DnsSource, RouteType}; use nonzero_ext::nonzero; use super::*; @@ -199,6 +249,7 @@ mod test { fragment: HttpRouteFragment { host_header: "failing-host".into(), path_prefix: "".into(), + front_name: None, }, inner: fake_transport_route.clone(), }, @@ -214,6 +265,7 @@ mod test { fragment: HttpRouteFragment { host_header: "successful-host".into(), path_prefix: "".into(), + front_name: Some(RouteType::ProxyF.into()), }, inner: fake_transport_route, }, @@ -256,15 +308,19 @@ mod test { e, WebSocketConnectError::WebSocketError(tungstenite::Error::ConnectionClosed) ); - ControlFlow::<()>::Continue(()) + ControlFlow::::Continue(()) }, ) // This previously hung forever due to a deadlock bug. .await; + let (connection, info) = result.expect("succeeded"); assert_eq!( - result, - Ok((succeeding_route.fragment, succeeding_route.inner.fragment)) - ) + connection, + (succeeding_route.fragment, succeeding_route.inner.fragment) + ); + let RouteInfo { unresolved } = info; + + assert_eq!(unresolved.to_string(), "REDACTED:1234 fronted by proxyf"); } } diff --git a/rust/net/src/env.rs b/rust/net/src/env.rs index 2af81079f..ba8424cd0 100644 --- a/rust/net/src/env.rs +++ b/rust/net/src/env.rs @@ -438,7 +438,7 @@ impl ConnectionConfig { let fronting_path_prefix = Arc::from(*path_prefix); let make_proxy_config = move |config: &ProxyConfig| { let ProxyConfig { - route_type: _, + route_type, http_host, sni_list, certs, @@ -448,6 +448,7 @@ impl ConnectionConfig { http_host: (*http_host).into(), sni_list: sni_list.iter().map(|sni| (*sni).into()).collect(), path_prefix: Arc::clone(&fronting_path_prefix), + front_name: route_type.into(), } }; configs.iter().map(make_proxy_config) @@ -786,6 +787,7 @@ mod test { fragment: HttpRouteFragment { host_header: "host".into(), path_prefix: "".into(), + front_name: None, }, inner: TlsRoute { fragment: TlsRouteFragment { diff --git a/swift/Sources/LibSignalClient/ChatConnection.swift b/swift/Sources/LibSignalClient/ChatConnection.swift index 7b15aa4b7..d0b6b84c6 100644 --- a/swift/Sources/LibSignalClient/ChatConnection.swift +++ b/swift/Sources/LibSignalClient/ChatConnection.swift @@ -21,13 +21,13 @@ public protocol ChatConnection: AnyObject { func info() -> ConnectionInfo } -public class ConnectionInfo: NativeHandleOwner { +public class ConnectionInfo: NativeHandleOwner, CustomStringConvertible { /// The local port used by the connection. public var localPort: UInt16 { withNativeHandle { connectionInfo in failOnError { try invokeFnReturningInteger { - signal_connection_info_local_port($0, connectionInfo) + signal_chat_connection_info_local_port($0, connectionInfo) } } } @@ -38,12 +38,23 @@ public class ConnectionInfo: NativeHandleOwner { let rawValue = withNativeHandle { connectionInfo in failOnError { try invokeFnReturningInteger { - signal_connection_info_ip_version($0, connectionInfo) + signal_chat_connection_info_ip_version($0, connectionInfo) } } } return IpType(rawValue: rawValue) ?? .unknown } + + /// A developer-facing description of the connection. + public var description: String { + withNativeHandle { connectionInfo in + failOnError { + try invokeFnReturningString { + signal_chat_connection_info_description($0, connectionInfo) + } + } + } + } } extension ChatConnection { diff --git a/swift/Sources/SignalFfi/signal_ffi.h b/swift/Sources/SignalFfi/signal_ffi.h index 3bf9196ef..c60badd23 100644 --- a/swift/Sources/SignalFfi/signal_ffi.h +++ b/swift/Sources/SignalFfi/signal_ffi.h @@ -234,6 +234,9 @@ typedef struct SignalChatUnauthChatService SignalChatUnauthChatService; typedef struct SignalCiphertextMessage SignalCiphertextMessage; +/** + * Information about an established connection. + */ typedef struct SignalConnectionInfo SignalConnectionInfo; typedef struct SignalConnectionManager SignalConnectionManager; @@ -565,6 +568,8 @@ typedef SignalChatAuthChatService SignalAuthChat; typedef SignalChatUnauthChatService SignalUnauthChat; +typedef SignalConnectionInfo SignalChatConnectionInfo; + /** * A C callback used to report the results of Rust futures. * @@ -1548,9 +1553,11 @@ SignalFfiError *signal_http_request_new_without_body(SignalHttpRequest **out, co SignalFfiError *signal_http_request_add_header(const SignalHttpRequest *request, const char *name, const char *value); -SignalFfiError *signal_connection_info_local_port(uint16_t *out, const SignalConnectionInfo *connection_info); +SignalFfiError *signal_chat_connection_info_local_port(uint16_t *out, const SignalChatConnectionInfo *connection_info); + +SignalFfiError *signal_chat_connection_info_ip_version(uint8_t *out, const SignalChatConnectionInfo *connection_info); -SignalFfiError *signal_connection_info_ip_version(uint8_t *out, const SignalConnectionInfo *connection_info); +SignalFfiError *signal_chat_connection_info_description(const char **out, const SignalChatConnectionInfo *connection_info); SignalFfiError *signal_chat_service_new_unauth(SignalUnauthChat **out, const SignalConnectionManager *connection_manager); @@ -1564,7 +1571,7 @@ SignalFfiError *signal_unauthenticated_chat_connection_send(SignalCPromiseFfiCha SignalFfiError *signal_unauthenticated_chat_connection_disconnect(SignalCPromisebool *promise, const SignalTokioAsyncContext *async_runtime, const SignalUnauthenticatedChatConnection *chat); -SignalFfiError *signal_unauthenticated_chat_connection_info(SignalConnectionInfo **out, const SignalUnauthenticatedChatConnection *chat); +SignalFfiError *signal_unauthenticated_chat_connection_info(SignalChatConnectionInfo **out, const SignalUnauthenticatedChatConnection *chat); SignalFfiError *signal_authenticated_chat_connection_connect(SignalCPromiseAuthenticatedChatConnection *promise, const SignalTokioAsyncContext *async_runtime, const SignalConnectionManager *connection_manager, const char *username, const char *password, bool receive_stories); @@ -1574,7 +1581,7 @@ SignalFfiError *signal_authenticated_chat_connection_send(SignalCPromiseFfiChatR SignalFfiError *signal_authenticated_chat_connection_disconnect(SignalCPromisebool *promise, const SignalTokioAsyncContext *async_runtime, const SignalAuthenticatedChatConnection *chat); -SignalFfiError *signal_authenticated_chat_connection_info(SignalConnectionInfo **out, const SignalAuthenticatedChatConnection *chat); +SignalFfiError *signal_authenticated_chat_connection_info(SignalChatConnectionInfo **out, const SignalAuthenticatedChatConnection *chat); SignalFfiError *signal_chat_service_disconnect_unauth(SignalCPromisebool *promise, const SignalTokioAsyncContext *async_runtime, const SignalUnauthChat *chat);