Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

- GUI: Fix a number of small ui issues / inconsistencies
- ASB + GUI + CLI: Allow Tor onion-service connection attempts up to five minutes to complete
high-effort proof-of-work challenges while retaining the 15-second timeout for other addresses.

## [4.13.2] - 2026-07-23

Expand Down
177 changes: 169 additions & 8 deletions swap/src/network/transport.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
use anyhow::Result;
use futures::{AsyncRead, AsyncWrite};
use futures::future::BoxFuture;
use futures::{AsyncRead, AsyncWrite, Future, FutureExt};
use libp2p::core::muxing::StreamMuxerBox;
use libp2p::core::transport::Boxed;
use libp2p::core::transport::{Boxed, ListenerId, TransportError, TransportEvent};
use libp2p::core::upgrade::Version;
use libp2p::multiaddr::Protocol;
use libp2p::noise;
use libp2p::{PeerId, Transport, identity, yamux};
use libp2p::{Multiaddr, PeerId, Transport, identity, yamux};
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use thiserror::Error;

const AUTH_AND_MULTIPLEX_TIMEOUT: Duration = Duration::from_secs(15);
const REGULAR_CONNECTION_SETUP_TIMEOUT: Duration = Duration::from_secs(15);
// Arti's onion-service proof-of-work can take significantly longer than a regular connection
// setup. This timeout is an upper bound around the entire onion dial; Arti still applies its own
// phase-specific timeouts while establishing the connection.
const ONION_CONNECTION_SETUP_TIMEOUT: Duration = Duration::from_secs(5 * 60);
// We have 5 protocols, not more than 2 of which should be active at the same time.
const MAX_NUM_STREAMS: usize = 5;

Expand All @@ -32,10 +41,162 @@ where
let transport = transport
.upgrade(Version::V1)
.authenticate(auth_upgrade)
.multiplex(multiplex_upgrade)
.timeout(AUTH_AND_MULTIPLEX_TIMEOUT)
.map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer)))
.boxed();
.multiplex(multiplex_upgrade);
let transport = ConnectionSetupTimeout::new(
transport,
REGULAR_CONNECTION_SETUP_TIMEOUT,
ONION_CONNECTION_SETUP_TIMEOUT,
)
.map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer)))
.boxed();

Ok(transport)
}

struct ConnectionSetupTimeout<T> {
inner: T,
regular_timeout: Duration,
onion_timeout: Duration,
}

impl<T> ConnectionSetupTimeout<T> {
fn new(inner: T, regular_timeout: Duration, onion_timeout: Duration) -> Self {
Self {
inner,
regular_timeout,
onion_timeout,
}
}

fn outgoing_timeout(&self, addr: &Multiaddr) -> Duration {
if addr
.iter()
.any(|protocol| matches!(protocol, Protocol::Onion3(_)))
{
return self.onion_timeout;
}

self.regular_timeout
}
}

impl<T> Transport for ConnectionSetupTimeout<T>
where
T: Transport + Unpin,
T::Dial: Send + 'static,
T::ListenerUpgrade: Send + 'static,
T::Output: Send + 'static,
T::Error: Send + Sync + 'static,
{
type Output = T::Output;
type Error = ConnectionSetupTimeoutError<T::Error>;
type ListenerUpgrade = BoxFuture<'static, Result<Self::Output, Self::Error>>;
type Dial = BoxFuture<'static, Result<Self::Output, Self::Error>>;

fn listen_on(
&mut self,
id: ListenerId,
addr: Multiaddr,
) -> Result<(), TransportError<Self::Error>> {
self.inner
.listen_on(id, addr)
.map_err(|error| error.map(ConnectionSetupTimeoutError::Transport))
}

fn remove_listener(&mut self, id: ListenerId) -> bool {
self.inner.remove_listener(id)
}

fn dial(&mut self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> {
let timeout = self.outgoing_timeout(&addr);
let dial = self
.inner
.dial(addr)
.map_err(|error| error.map(ConnectionSetupTimeoutError::Transport))?;

Ok(with_timeout(dial, timeout))
}

fn dial_as_listener(
&mut self,
addr: Multiaddr,
) -> Result<Self::Dial, TransportError<Self::Error>> {
let timeout = self.outgoing_timeout(&addr);
let dial = self
.inner
.dial_as_listener(addr)
.map_err(|error| error.map(ConnectionSetupTimeoutError::Transport))?;

Ok(with_timeout(dial, timeout))
}

fn address_translation(&self, server: &Multiaddr, observed: &Multiaddr) -> Option<Multiaddr> {
self.inner.address_translation(server, observed)
}

fn poll(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<TransportEvent<Self::ListenerUpgrade, Self::Error>> {
let timeout = self.regular_timeout;

Pin::new(&mut self.inner).poll(cx).map(|event| {
event
.map_upgrade(|upgrade| with_timeout(upgrade, timeout))
.map_err(ConnectionSetupTimeoutError::Transport)
})
}
}

fn with_timeout<F, O, E>(
future: F,
timeout: Duration,
) -> BoxFuture<'static, Result<O, ConnectionSetupTimeoutError<E>>>
where
F: Future<Output = Result<O, E>> + Send + 'static,
O: Send + 'static,
E: Send + 'static,
{
async move {
tokio::time::timeout(timeout, future)
.await
.map_err(|_| ConnectionSetupTimeoutError::Timeout(timeout))?
.map_err(ConnectionSetupTimeoutError::Transport)
}
.boxed()
}

#[derive(Debug, Error)]
enum ConnectionSetupTimeoutError<E> {
#[error("Connection setup timed out after {0:?}")]
Timeout(Duration),
#[error(transparent)]
Transport(E),
}

#[cfg(test)]
mod tests {
use super::*;
use libp2p::core::transport::dummy::DummyTransport;

const ONION_ADDRESS: &str =
"/onion3/lmnlknoxbmd2alyee5lihegdrjiyn6qzgbvapcz5klz3ptf7szi7dsqd:9939";

#[test]
fn onion_dials_use_the_longer_connection_setup_timeout() {
let transport = ConnectionSetupTimeout::new(
DummyTransport::<()>::new(),
REGULAR_CONNECTION_SETUP_TIMEOUT,
ONION_CONNECTION_SETUP_TIMEOUT,
);

assert_eq!(
transport.outgoing_timeout(&ONION_ADDRESS.parse().unwrap()),
ONION_CONNECTION_SETUP_TIMEOUT
);
assert_eq!(
transport.outgoing_timeout(&"/ip4/127.0.0.1/tcp/9939".parse().unwrap()),
REGULAR_CONNECTION_SETUP_TIMEOUT
);
}
}
Loading