macula_rust/station_link/
call.rs1use std::sync::{Arc, Weak};
8use std::time::Duration;
9
10use tokio::sync::oneshot;
11
12use crate::cbor::Value;
13use crate::frame::{self, RequestSpec, VerifiedRequest};
14use crate::handshake::VERSION_5;
15
16use super::confidential::{reply_outcome, sealed_request, stated, CallSeal, Seal};
17use super::{now_ms, Inner, Link, LinkError};
18use crate::seal;
19
20pub const DEFAULT_CALL_TIMEOUT: Duration = Duration::from_secs(5);
22pub const MAX_CALL_TIMEOUT: Duration = Duration::from_secs(10 * 60);
24
25const LIVENESS_EVERY: Duration = Duration::from_secs(30);
26const LIVENESS_TIMEOUT: Duration = Duration::from_secs(30);
27const LIVENESS_PROCEDURE: &str = "_macula.ping";
28
29#[derive(Debug, Clone, PartialEq)]
38pub struct Call {
39 pub realm: [u8; 32],
40 pub procedure: String,
41 pub target: [u8; 32],
42 pub payload: Value,
43 pub timeout: Duration,
44 pub token: Option<Vec<u8>>,
45 pub proofs: Vec<Vec<u8>>,
46 pub seal: Option<Seal>,
47}
48
49impl Default for Call {
50 fn default() -> Self {
51 Call {
52 realm: [0; 32],
53 procedure: String::new(),
54 target: [0; 32],
55 payload: Value::Map(Vec::new()),
56 timeout: Duration::ZERO,
57 token: None,
58 proofs: Vec::new(),
59 seal: None,
60 }
61 }
62}
63
64pub(super) struct Pending {
66 pub(super) request: VerifiedRequest,
67 pub(super) seal: Option<CallSeal>,
68 pub(super) outcome: oneshot::Sender<Result<Value, LinkError>>,
69}
70
71impl Link {
72 pub async fn call(&self, c: Call) -> Result<Value, LinkError> {
75 call(&self.inner, c).await
76 }
77}
78
79pub(super) async fn call(inner: &Arc<Inner>, c: Call) -> Result<Value, LinkError> {
80 let timeout = if c.timeout.is_zero() {
81 DEFAULT_CALL_TIMEOUT
82 } else {
83 c.timeout.min(MAX_CALL_TIMEOUT)
84 };
85 stated(&c.target, &inner.station.node_id, &c.seal)?;
86 let target = if c.target == [0; 32] {
87 inner.station.node_id
88 } else {
89 c.target
90 };
91 let mut request_id = [0u8; 16];
92 aws_lc_rs::rand::fill(&mut request_id).map_err(|_| LinkError::Io("no randomness".into()))?;
93 let deadline = (now_ms() + timeout.as_millis() as i64) as u64;
94 let (sealed, call_seal) = match &c.seal {
95 Some(Seal::To(key)) => {
96 let (sealed, s) = sealed_request(
97 inner.profile,
98 key,
99 seal::FRAME_CALL,
100 c.realm,
101 &c.procedure,
102 inner.self_id,
103 target,
104 request_id,
105 deadline,
106 &c.payload,
107 )?;
108 (Some(sealed), Some(s))
109 }
110 _ => (None, None),
111 };
112 let signed = frame::sign_call(
113 &RequestSpec {
114 request_id,
115 realm: c.realm,
116 procedure: c.procedure,
117 target,
118 deadline,
119 payload: c.payload,
120 sealed,
121 mode: None,
122 token: c.token,
123 proofs: c.proofs,
124 source_route: None,
125 retry_budget: None,
126 },
127 &inner.key,
128 )?;
129 let request = frame::verify_request(&signed, inner.profile)?;
130 let (outcome_tx, outcome) = oneshot::channel();
131 {
132 let mut state = inner.lock();
133 if let Some(e) = &state.ended {
134 return Err(e.clone());
135 }
136 state.pending.insert(
137 request_id,
138 Pending {
139 request,
140 seal: call_seal,
141 outcome: outcome_tx,
142 },
143 );
144 }
145 let forget = || {
146 inner.lock().pending.remove(&request_id);
147 };
148 if let Err(e) = inner.write_control(&signed).await {
149 forget();
150 return Err(e);
151 }
152 let answered = tokio::time::timeout(timeout, outcome).await;
153 forget();
154 match answered {
155 Ok(Ok(outcome)) => outcome,
156 Ok(Err(_)) => Err(inner.lock().ended.clone().unwrap_or(LinkError::Closed)),
157 Err(_) => Err(LinkError::CallTimeout),
158 }
159}
160
161pub(super) fn replied(inner: &Arc<Inner>, v: &Value) {
164 let Ok((request_id, _)) = frame::claimed_reply_ids(v) else {
165 inner.count("malformed_reply");
166 return;
167 };
168 let (request, seal) = match inner.lock().pending.get(&request_id) {
169 Some(p) => (p.request.clone(), p.seal.clone()),
170 None => {
171 inner.count("unmatched_reply");
172 return;
173 }
174 };
175 let Some(outcome) = verified_outcome(inner, v, &request, seal.as_ref()) else {
176 inner.count("unverified_reply");
177 return;
178 };
179 if let Some(p) = inner.lock().pending.remove(&request_id) {
180 let _ = p.outcome.send(outcome);
181 }
182}
183
184fn verified_outcome(
185 inner: &Inner,
186 v: &Value,
187 request: &VerifiedRequest,
188 seal: Option<&CallSeal>,
189) -> Option<Result<Value, LinkError>> {
190 if v.get("reply").is_some() {
191 let reply = frame::verify_reply(v, request, inner.profile).ok()?;
192 return Some(reply_outcome(reply, request, seal));
193 }
194 let relayed =
195 frame::verify_relay_error(v, request, inner.profile, &inner.station.node_id).ok()?;
196 Some(Err(LinkError::Relay {
197 reported_by: relayed.reported_by,
198 code: relayed.code,
199 }))
200}
201
202pub(super) async fn probe(link: Weak<Inner>) {
208 let mut misses = 0;
209 loop {
210 let Some(mut done) = link.upgrade().map(|l| l.done_rx.clone()) else {
211 return;
212 };
213 tokio::select! {
214 _ = done.wait_for(|ended| *ended) => return,
215 _ = tokio::time::sleep(LIVENESS_EVERY) => {}
216 }
217 let Some(inner) = link.upgrade() else { return };
218 let outcome = match inner.version {
219 VERSION_5 => probe_v5(&inner).await,
220 _ => call(
221 &inner,
222 Call {
223 procedure: LIVENESS_PROCEDURE.to_string(),
224 timeout: LIVENESS_TIMEOUT,
225 ..Call::default()
226 },
227 )
228 .await
229 .map(|_| ()),
230 };
231 misses = if matches!(outcome, Err(LinkError::CallTimeout)) {
232 misses + 1
233 } else {
234 0
235 };
236 if misses >= 2 {
237 inner.end(LinkError::LivenessLost);
238 return;
239 }
240 }
241}
242
243async fn probe_v5(inner: &Inner) -> Result<(), LinkError> {
246 let mut nonce = [0u8; frame::LIVENESS_NONCE_SIZE];
247 aws_lc_rs::rand::fill(&mut nonce).map_err(|_| LinkError::Io("no randomness".into()))?;
248 let mut pongs = inner.pongs_rx.lock().await;
249 while pongs.try_recv().is_ok() {}
252 inner
253 .send_control(&frame::liveness_ping_frame(&nonce))
254 .await?;
255 let mut done = inner.done_rx.clone();
256 let answered = async {
257 while let Some(pong) = pongs.recv().await {
258 if pong == nonce {
259 return Ok(());
260 }
261 }
262 Err(LinkError::Closed)
263 };
264 tokio::select! {
265 _ = done.wait_for(|ended| *ended) => Err(LinkError::Closed),
266 outcome = tokio::time::timeout(LIVENESS_TIMEOUT, answered) => {
267 outcome.unwrap_or(Err(LinkError::CallTimeout))
268 }
269 }
270}