Skip to main content

rings_node/onion/circuit/
shell.rs

1use std::sync::Arc;
2
3use bytes::Bytes;
4use rings_core::dht::Did;
5use rings_core::ecc::PublicKey;
6use rings_core::session::SessionSk;
7use rings_core::utils::get_epoch_ms;
8
9use super::cell::open_cell;
10use super::cell::seal_encoded_message;
11use super::codec::encode_local_message;
12use super::codec::OnionLocalMessage;
13use super::crypto::decrypt_client_payload;
14use super::crypto::decrypt_forward_layer;
15use super::limiter::OnionCryptoGate;
16use super::send_outbox::OnionLinkSender;
17#[cfg(all(test, rings_native))]
18use super::send_outbox::OnionSendTestHook;
19use super::OnionAuthenticatedPayload;
20use super::OnionCellBucket;
21use super::OnionCircuitEffect;
22use super::OnionCircuitId;
23use super::OnionCircuitPayload;
24use super::OnionClientReturn;
25use super::OnionForwardNonce;
26use super::OnionForwardSequence;
27use crate::error::Error;
28use crate::error::Result;
29use crate::extension::ext::EffectScope;
30use crate::extension::ext::Interpret;
31use crate::extension::ext::Scope;
32use crate::extension::transport::platform::spawn_detached;
33
34/// Interpreter for route-aware circuit effects.
35pub struct OnionCircuitShell<H> {
36    session_sk: SessionSk,
37    crypto_gate: OnionCryptoGate,
38    link_sender: OnionLinkSender,
39    handler: Arc<H>,
40}
41
42impl<H> OnionCircuitShell<H> {
43    /// Create a circuit interpreter backed by `handler`.
44    pub fn new(session_sk: SessionSk, handler: H) -> Self {
45        Self {
46            session_sk,
47            crypto_gate: OnionCryptoGate::default(),
48            link_sender: OnionLinkSender::default(),
49            handler: Arc::new(handler),
50        }
51    }
52
53    #[cfg(all(test, rings_native))]
54    pub(super) fn new_with_send_test_hook(
55        session_sk: SessionSk,
56        handler: H,
57        test_hook: Arc<OnionSendTestHook>,
58    ) -> Self {
59        Self {
60            session_sk,
61            crypto_gate: OnionCryptoGate::default(),
62            link_sender: OnionLinkSender::with_test_hook(test_hook),
63            handler: Arc::new(handler),
64        }
65    }
66
67    /// Create an interpreter sharing one node-level link sender with endpoint adapters.
68    pub(crate) fn with_link_sender(
69        session_sk: SessionSk,
70        handler: H,
71        link_sender: OnionLinkSender,
72    ) -> Self {
73        Self {
74            session_sk,
75            crypto_gate: OnionCryptoGate::default(),
76            link_sender,
77            handler: Arc::new(handler),
78        }
79    }
80
81    fn admit_crypto(&self, from: Did, now_ms: u128, visible_cell_bytes: u64) -> Result<()> {
82        self.crypto_gate.admit(from, now_ms, visible_cell_bytes)
83    }
84
85    fn decrypt_cell_reinject(
86        &self,
87        from: Did,
88        bucket: OnionCellBucket,
89        sealed: &rings_core::ecc::elgamal::impls::secp256k1::AeadCiphertext,
90    ) -> Result<Option<Bytes>> {
91        let received_at_ms = get_epoch_ms();
92        let visible_cell_bytes = u64::try_from(bucket.plaintext_len())
93            .map_err(|_| Error::OnionRouteError(crate::onion::OnionRouteError::InvalidCell))?;
94        match self.admit_crypto(from, received_at_ms, visible_cell_bytes) {
95            Ok(()) => {}
96            Err(Error::NoPermission) => {
97                drop_bad_crypto("forward admission denied", Error::NoPermission);
98                return Ok(None);
99            }
100            Err(error) => return Err(error),
101        }
102        let message = match open_cell(&self.session_sk, bucket, sealed) {
103            Ok(message) => message,
104            Err(error) => {
105                drop_bad_crypto("cell decrypt", error);
106                return Ok(None);
107            }
108        };
109        encode_local_message(OnionLocalMessage::CellReady {
110            from,
111            received_at_ms,
112            bucket,
113            message,
114        })
115        .map(Some)
116    }
117
118    fn decrypt_forward_reinject(
119        &self,
120        from: Did,
121        received_at_ms: u128,
122        bucket: OnionCellBucket,
123        circuit_id: OnionCircuitId,
124        payload: &rings_core::ecc::elgamal::impls::secp256k1::AeadCiphertext,
125    ) -> Result<Option<Bytes>> {
126        match self.admit_crypto(from, received_at_ms, 0) {
127            Ok(()) => {}
128            Err(Error::NoPermission) => {
129                drop_bad_crypto("forward admission denied", Error::NoPermission);
130                return Ok(None);
131            }
132            Err(error) => return Err(error),
133        }
134        let layer = match decrypt_forward_layer(&self.session_sk, circuit_id, payload) {
135            Ok(layer) => layer,
136            Err(error) => {
137                drop_bad_crypto("forward decrypt", error);
138                return Ok(None);
139            }
140        };
141        encode_local_message(OnionLocalMessage::ForwardReady {
142            from,
143            received_at_ms,
144            bucket,
145            circuit_id,
146            layer,
147        })
148        .map(Some)
149    }
150
151    fn decrypt_client_payload(
152        &self,
153        from: Did,
154        payload: &rings_core::ecc::elgamal::impls::secp256k1::AeadCiphertext,
155    ) -> Result<Option<OnionAuthenticatedPayload>> {
156        let received_at_ms = get_epoch_ms();
157        match self.admit_crypto(from, received_at_ms, 0) {
158            Ok(()) => {}
159            Err(Error::NoPermission) => {
160                drop_bad_crypto("client admission denied", Error::NoPermission);
161                return Ok(None);
162            }
163            Err(error) => return Err(error),
164        }
165        match decrypt_client_payload(&self.session_sk, payload) {
166            Ok(payload) => Ok(Some(payload)),
167            Err(error) => {
168                drop_bad_crypto("client decrypt", error);
169                Ok(None)
170            }
171        }
172    }
173}
174
175#[cfg_attr(rings_browser, async_trait::async_trait(?Send))]
176#[cfg_attr(rings_native, async_trait::async_trait)]
177impl<H> Interpret for OnionCircuitShell<H>
178where H: OnionCircuitHandler + crate::extension::ext::MaybeSend + 'static
179{
180    type Effect = OnionCircuitEffect;
181
182    async fn run(&self, scope: &EffectScope, effect: OnionCircuitEffect) -> Result<Vec<Bytes>> {
183        match effect {
184            OnionCircuitEffect::DecryptCell {
185                from,
186                bucket,
187                sealed,
188            } => Ok(self
189                .decrypt_cell_reinject(from, bucket, &sealed)?
190                .into_iter()
191                .collect()),
192            OnionCircuitEffect::DecryptForward {
193                from,
194                received_at_ms,
195                bucket,
196                circuit_id,
197                payload,
198            } => Ok(self
199                .decrypt_forward_reinject(from, received_at_ms, bucket, circuit_id, &payload)?
200                .into_iter()
201                .collect()),
202            OnionCircuitEffect::SealAndSend {
203                to,
204                recipient,
205                bucket,
206                encoded_message,
207            } => {
208                let payload = seal_encoded_message(&encoded_message, recipient, Some(bucket))?;
209                self.link_sender.enqueue_sealed(
210                    scope.lifecycle(),
211                    super::OnionLink::new(to, recipient),
212                    payload,
213                )?;
214                Ok(Vec::new())
215            }
216            OnionCircuitEffect::Exit {
217                from,
218                circuit_id,
219                return_peer,
220                return_session_public_key,
221                client,
222                forward_nonce,
223                forward_sequence,
224                payload,
225            } => {
226                let lifecycle = scope.lifecycle();
227                let handler = Arc::clone(&self.handler);
228                spawn_detached(async move {
229                    let result = handler
230                        .handle_exit(&lifecycle, OnionCircuitExitFrame {
231                            from,
232                            circuit_id,
233                            return_peer,
234                            return_session_public_key,
235                            client,
236                            forward_nonce,
237                            forward_sequence,
238                            payload,
239                        })
240                        .await;
241                    if let Err(error) = result {
242                        tracing::warn!(%error, "onion exit effect failed");
243                    }
244                });
245                Ok(Vec::new())
246            }
247            OnionCircuitEffect::DecryptClient {
248                from,
249                circuit_id,
250                payload,
251            } => {
252                if let Some(payload) = self.decrypt_client_payload(from, &payload)? {
253                    let lifecycle = scope.lifecycle();
254                    self.handler
255                        .handle_client(&lifecycle, from, circuit_id, payload)
256                        .await?;
257                }
258                Ok(Vec::new())
259            }
260        }
261    }
262}
263
264/// Fully decrypted forward frame that has reached the exit adapter.
265#[derive(Clone, Debug)]
266pub struct OnionCircuitExitFrame {
267    /// Previous peer that delivered this exit frame.
268    pub from: Did,
269    /// Edge-local circuit id for the exit-to-return-peer edge.
270    pub circuit_id: OnionCircuitId,
271    /// Relay peer that should receive backward frames from the exit.
272    pub return_peer: Did,
273    /// Session key of the immediate return peer used to encrypt the first backward cell.
274    pub return_session_public_key: PublicKey<33>,
275    /// Client return key encrypted into the exit layer.
276    pub client: OnionClientReturn,
277    /// One-shot replay token consumed by `Open`/HTTPS exit operations before side effects.
278    pub forward_nonce: OnionForwardNonce,
279    /// Monotonic client-to-exit sequence within this circuit.
280    pub forward_sequence: OnionForwardSequence,
281    /// Adapter payload carried by the exit layer.
282    pub payload: OnionCircuitPayload,
283}
284
285/// Runtime-specific circuit handling.
286#[cfg_attr(rings_browser, async_trait::async_trait(?Send))]
287#[cfg_attr(rings_native, async_trait::async_trait)]
288pub trait OnionCircuitHandler {
289    /// Handle a frame that reached this node as the exit.
290    async fn handle_exit(&self, scope: &Scope, frame: OnionCircuitExitFrame) -> Result<()>;
291
292    /// Handle a frame that reached this node as the client.
293    async fn handle_client(
294        &self,
295        scope: &Scope,
296        from: Did,
297        circuit_id: OnionCircuitId,
298        payload: OnionAuthenticatedPayload,
299    ) -> Result<()>;
300}
301
302fn drop_bad_crypto(context: &str, error: Error) {
303    tracing::debug!("drop onion circuit message after {context}: {error}");
304}