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}