Skip to main content

tachyon_i2p/
destination.rs

1//! [`Destination`]: a local I2P destination (an eepsite's identity), reachable at a
2//! `.b32.i2p` address.
3
4use crate::error::I2pError;
5use crate::router::I2pRouter;
6use crate::stream::I2pStream;
7use std::collections::HashMap;
8use std::ffi::c_void;
9use std::os::raw::c_int;
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::{Arc, LazyLock, Mutex};
12
13/// How many not-yet-[`accept`](Destination::accept)ed inbound streams may queue before new ones
14/// are refused (closed on libi2pd's accept thread, never reaching this side). Bounded so a
15/// destination nobody accepts on can't grow memory without limit; the accept callback must never
16/// block (see `shim.h`), so refusing is the only option when this is full.
17const ACCEPT_QUEUE_CAPACITY: usize = 128;
18
19/// Length of the `[u8; 32]` in [`Destination::ident_hash`] and [`Destination::connect`].
20///
21/// Not verifiable from Rust: i2pd-sys's `I2PD_IDENT_HASH_LEN` is a C macro and its bindgen run
22/// allowlists functions only. The shim `static_assert`s it against libi2pd's own `IdentHash`
23/// instead, which is the side that would change.
24const IDENT_HASH_LEN: usize = 32;
25
26/// An owned stream pointer in transit: from libi2pd's accept thread into the queue
27/// [`accept`](Destination::accept) drains, or out of the `spawn_blocking` worker in
28/// [`connect`](Destination::connect).
29///
30/// Dropping it without [`into_raw`](RawStream::into_raw) releases the stream back to libi2pd.
31/// That covers the two paths that would otherwise leak the handle and leave the peer's stream
32/// hanging open until it timed out: a `Destination` dropped with inbound streams still queued,
33/// and a `connect` cancelled after the blocking worker already produced a stream.
34struct RawStream(*mut i2pd_sys::I2pdStream);
35
36// SAFETY: the pointer is treated as an opaque, movable handle here; see `shim.h`'s thread-safety
37// note -- libi2pd's own Stream API is designed to be driven from an arbitrary thread.
38unsafe impl Send for RawStream {}
39
40impl RawStream {
41    /// Relinquishes ownership of the pointer to the caller, suppressing the releasing [`Drop`].
42    fn into_raw(self) -> *mut i2pd_sys::I2pdStream {
43        let ptr = self.0;
44        std::mem::forget(self);
45        ptr
46    }
47}
48
49impl Drop for RawStream {
50    fn drop(&mut self) {
51        // SAFETY: this type owns `self.0` -- moved, never copied, from the moment libi2pd hands
52        // it over to the moment `into_raw` gives it up -- so this is its last use.
53        // `i2pd_destroy_stream` closes the stream itself (releasing the handle alone would leave
54        // it half-open on its destination), and is null-tolerant and non-blocking per `shim.h`,
55        // so this is safe to run from the accept callback too.
56        unsafe {
57            i2pd_sys::i2pd_destroy_stream(self.0);
58        }
59    }
60}
61
62/// Registry mapping an opaque token to the queue its destination's inbound streams go into.
63///
64/// The accept callback gets a token as its `ctx` rather than a pointer to a `Box<Sender>`,
65/// because a pointer here could not be freed safely: libi2pd's acceptor slot
66/// (`StreamingDestination::m_Acceptor`, a bare `std::function` with no lock around it) is cleared
67/// by `ResetAcceptor` on *our* thread inside `i2pd_destroy_destination` while the io_service
68/// thread may be invoking that same `std::function`, so freeing a context the callback
69/// dereferences is a use-after-free in that window.
70///
71/// A token is an integer, so the callback dereferences nothing -- it looks the token up under
72/// this mutex, and a deregistered destination simply isn't found, releasing the stream instead of
73/// delivering it. The lock covers only a `HashMap` lookup and a non-blocking `try_send`, which
74/// satisfies the callback's must-never-block contract.
75static ACCEPT_REGISTRY: LazyLock<Mutex<HashMap<usize, tokio::sync::mpsc::Sender<RawStream>>>> =
76    LazyLock::new(|| Mutex::new(HashMap::new()));
77
78/// Source of the tokens above. Monotonic, never reused, so a token freed by one destination can
79/// never be resolved to a later one.
80static NEXT_ACCEPT_TOKEN: AtomicUsize = AtomicUsize::new(1);
81
82/// Nothing under this lock can panic (a `HashMap` lookup and a `try_send`), so poisoning is
83/// unreachable; recovering rather than unwrapping keeps a panic elsewhere from escalating into
84/// every subsequent inbound stream on every destination being dropped.
85fn accept_registry()
86-> std::sync::MutexGuard<'static, HashMap<usize, tokio::sync::mpsc::Sender<RawStream>>> {
87    ACCEPT_REGISTRY.lock().unwrap_or_else(|e| e.into_inner())
88}
89
90/// Invoked on libi2pd's own thread for every inbound stream, and must never block per `shim.h`.
91/// The lookup and `try_send` are both non-blocking; if either fails -- unknown token or full
92/// queue -- `stream` is dropped here, which closes and destroys it via [`RawStream`]'s `Drop`.
93extern "C" fn on_accept(ctx: *mut c_void, stream: *mut i2pd_sys::I2pdStream) {
94    if stream.is_null() {
95        return;
96    }
97    // Own it immediately, so every path out of here releases the stream unless it was handed off.
98    let stream = RawStream(stream);
99    // `ctx` is a token, never a real address -- read its integer value, don't dereference it.
100    let token = ctx.addr();
101
102    let rejected = {
103        let registry = accept_registry();
104        match registry.get(&token) {
105            // On success the queue owns the stream; on failure (queue full) it comes back.
106            Some(tx) => tx.try_send(stream).err().map(|e| e.into_inner()),
107            // Unknown token: the destination is already gone.
108            None => Some(stream),
109        }
110    };
111    // Outside the lock: dropping a `RawStream` calls into libi2pd to close and destroy the
112    // stream, which has no business running inside this crate's critical section.
113    drop(rejected);
114}
115
116pub(crate) struct DestinationHandle {
117    pub(crate) ptr: *mut i2pd_sys::I2pdDestination,
118    accept_token: usize,
119}
120
121// SAFETY: every libi2pd entry point this handle's pointer is passed to is documented (`shim.h`)
122// as safe to call from an arbitrary thread.
123unsafe impl Send for DestinationHandle {}
124unsafe impl Sync for DestinationHandle {}
125
126impl Drop for DestinationHandle {
127    fn drop(&mut self) {
128        // SAFETY: `self.ptr` is a valid destination handle owned by this struct, and this is its
129        // last use.
130        unsafe {
131            i2pd_sys::i2pd_destroy_destination(self.ptr);
132        }
133        // Deregister *after* destroying, never before: `i2pd_destroy_destination` can invoke the
134        // accept callback synchronously on this thread (`StopAcceptingStreams`/`Stop` ->
135        // `StreamingDestination::ResetAcceptor` calls the acceptor one last time), and it takes
136        // this same lock. A callback still racing in from the io_service thread afterwards finds
137        // no entry and releases its stream instead.
138        let mut registry = accept_registry();
139        drop(registry.remove(&self.accept_token));
140    }
141}
142
143/// A local I2P destination -- an eepsite's identity, reachable at a stable `.b32.i2p` address for
144/// as long as this value is alive.
145///
146/// Create one via [`I2pRouter::create_transient_destination`],
147/// [`I2pRouter::create_persistent_destination`], or
148/// [`I2pRouter::destination_from_keys_file`].
149#[derive(Debug)]
150pub struct Destination {
151    handle: Arc<DestinationHandle>,
152    b32_address: Box<str>,
153    ident_hash: [u8; IDENT_HASH_LEN],
154    accept_rx: tokio::sync::mpsc::Receiver<RawStream>,
155    // Keeps libi2pd's global context alive at least as long as this destination. Declared *last*
156    // because fields drop in declaration order: `handle`'s `Drop` touches the global tunnel
157    // pool/netDb, so it must run before the router's `Drop` tears those globals down.
158    router: I2pRouter,
159}
160
161impl std::fmt::Debug for DestinationHandle {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        f.debug_struct("DestinationHandle").finish_non_exhaustive()
164    }
165}
166
167impl std::fmt::Debug for RawStream {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        f.debug_struct("RawStream").finish_non_exhaustive()
170    }
171}
172
173impl Destination {
174    /// Takes ownership of a possibly-null destination pointer fresh out of libi2pd, fetches its
175    /// address, and registers the acceptor. Called only from a `spawn_blocking` worker.
176    pub(crate) fn from_raw(
177        router: I2pRouter,
178        ptr: *mut i2pd_sys::I2pdDestination,
179    ) -> Result<Self, I2pError> {
180        if ptr.is_null() {
181            return Err(I2pError::DestinationCreationFailed);
182        }
183
184        // SAFETY: `ptr` was just null-checked and came straight from libi2pd; the returned
185        // string pointer (if non-null) is freed immediately after copying it out.
186        let b32_address = unsafe {
187            let raw = i2pd_sys::i2pd_destination_b32_address(ptr);
188            if raw.is_null() {
189                i2pd_sys::i2pd_destroy_destination(ptr);
190                return Err(I2pError::DestinationCreationFailed);
191            }
192            let s = std::ffi::CStr::from_ptr(raw.cast())
193                .to_string_lossy()
194                .into_owned();
195            i2pd_sys::i2pd_free_string(raw);
196            s
197        };
198
199        // SAFETY: `ptr` was just null-checked and came straight from libi2pd; `out` is a valid
200        // local buffer of exactly the length the shim writes.
201        let mut ident_hash = [0u8; IDENT_HASH_LEN];
202        let ok = unsafe { i2pd_sys::i2pd_destination_ident_hash(ptr, ident_hash.as_mut_ptr()) };
203        if ok == 0 {
204            // SAFETY: `ptr` is still valid and not yet handed to any `DestinationHandle`.
205            unsafe { i2pd_sys::i2pd_destroy_destination(ptr) };
206            return Err(I2pError::DestinationCreationFailed);
207        }
208
209        let (tx, accept_rx) = tokio::sync::mpsc::channel(ACCEPT_QUEUE_CAPACITY);
210        let accept_token = NEXT_ACCEPT_TOKEN.fetch_add(1, Ordering::Relaxed);
211        // Register before arming the callback, so an inbound stream arriving on the very first
212        // instant the acceptor is live already resolves to this queue.
213        drop(accept_registry().insert(accept_token, tx));
214
215        // SAFETY: `ptr` is a live destination. The `ctx` argument is an integer token, not a
216        // pointer -- `on_accept` never dereferences it -- so there is nothing here for a
217        // late-firing callback to dangle on (see `ACCEPT_REGISTRY`).
218        unsafe {
219            i2pd_sys::i2pd_accept_stream(
220                ptr,
221                Some(on_accept),
222                std::ptr::without_provenance_mut::<c_void>(accept_token),
223            );
224        }
225
226        Ok(Self {
227            handle: Arc::new(DestinationHandle { ptr, accept_token }),
228            b32_address: b32_address.into_boxed_str(),
229            ident_hash,
230            accept_rx,
231            router,
232        })
233    }
234
235    /// This destination's `<52 chars>.b32.i2p` address.
236    #[must_use]
237    pub fn b32_address(&self) -> &str {
238        &self.b32_address
239    }
240
241    /// This destination's raw 32-byte `IdentHash`, the form [`connect`](Self::connect) expects
242    /// for a remote destination. `b32_address` is derived from the same hash.
243    #[must_use]
244    pub const fn ident_hash(&self) -> [u8; 32] {
245        self.ident_hash
246    }
247
248    /// Waits for the next inbound stream. Unlike the underlying libi2pd accept callback this is
249    /// not a one-shot -- call it in the usual `loop { let stream = dest.accept().await?; }` shape
250    /// for as long as the destination lives.
251    ///
252    /// Takes `&mut self`, so accepts are drawn one at a time from a single owner. Streams
253    /// arriving while no call is pending are queued (up to 128) rather than dropped, so a caller
254    /// that hands each one to a spawned task doesn't need concurrent accepts to keep up.
255    ///
256    /// # Errors
257    /// Returns [`I2pError::DestinationClosed`] if this destination is dropped while a call is
258    /// pending.
259    pub async fn accept(&mut self) -> Result<I2pStream, I2pError> {
260        let raw = self
261            .accept_rx
262            .recv()
263            .await
264            .ok_or(I2pError::DestinationClosed)?;
265        // SAFETY: `raw` is a freshly-accepted stream handle owned solely by this `RawStream`;
266        // `into_raw` transfers that ownership on to `I2pStream::from_raw`.
267        Ok(unsafe { I2pStream::from_raw(self.router.clone(), raw.into_raw()) })
268    }
269
270    /// Opens an outbound stream to `remote_ident_hash` (the raw 32-byte `IdentHash`, *not* the
271    /// `.b32.i2p` string). Waits up to `timeout`, off the runtime thread, for the remote's lease
272    /// set to become known and a tunnel to be built -- seconds to minutes on a cold start.
273    ///
274    /// `timeout` truncates to whole seconds, libi2pd's retry-loop granularity. Anything under a
275    /// second, [`Duration::ZERO`](std::time::Duration::ZERO) included, becomes zero, which
276    /// libi2pd treats as try-once rather than as almost a second of waiting.
277    ///
278    /// # Errors
279    /// Returns [`I2pError::ConnectFailed`] on timeout or failure.
280    pub async fn connect(
281        &self,
282        remote_ident_hash: [u8; 32],
283        timeout: std::time::Duration,
284    ) -> Result<I2pStream, I2pError> {
285        let handle = self.handle.clone();
286        let router = self.router.clone();
287        let timeout_secs = c_int::try_from(timeout.as_secs()).unwrap_or(c_int::MAX);
288        let raw = tokio::task::spawn_blocking(move || {
289            // SAFETY: `handle.ptr` is a live destination for the duration of this blocking call
290            // (the `Arc<DestinationHandle>` clone above keeps it alive even if `self` is
291            // dropped concurrently); `remote_ident_hash` is a valid 32-byte buffer. Wrapping the
292            // result in `RawStream` here means a cancelled `connect` (this future dropped while
293            // the blocking worker was still retrying) releases the stream the worker went on to
294            // produce, instead of leaking it.
295            RawStream(unsafe {
296                i2pd_sys::i2pd_create_stream(handle.ptr, remote_ident_hash.as_ptr(), timeout_secs)
297            })
298        })
299        .await
300        .map_err(|_| I2pError::WorkerPanicked)?;
301
302        if raw.0.is_null() {
303            return Err(I2pError::ConnectFailed);
304        }
305        // SAFETY: `raw` is a freshly-created stream handle owned solely by this `RawStream`;
306        // `into_raw` transfers that ownership on to `I2pStream::from_raw`.
307        Ok(unsafe { I2pStream::from_raw(router, raw.into_raw()) })
308    }
309}