1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::future::Future;
4use std::panic::AssertUnwindSafe;
5use std::sync::Arc;
6use std::time::Duration;
7
8use fedimint_core::core::ModuleInstanceId;
9use fedimint_core::module::{ApiEndpoint, ApiError, ApiMethod, IrohApiRequest};
10use fedimint_core::task::TaskGroup;
11use fedimint_core::util::FmtCompactAnyhow as _;
12use fedimint_logging::LOG_NET_API;
13use fedimint_metrics::prometheus::HistogramTimer;
14use fedimint_server_core::DynServerModule;
15use futures::FutureExt as _;
16use iroh::Endpoint;
17use iroh::endpoint::{Incoming, RecvStream, SendStream, VarInt};
18use serde_json::Value;
19use tokio::sync::Semaphore;
20use tracing::{error, warn};
21
22use super::api::{ConsensusApi, server_endpoints};
23use crate::connection_limits::ConnectionLimits;
24use crate::metrics::{
25 IROH_API_CONNECTION_DURATION_SECONDS, IROH_API_CONNECTION_IDLE_TIMEOUT_TOTAL,
26 IROH_API_CONNECTIONS_ACTIVE, IROH_API_REQUEST_DURATION_SECONDS, IROH_API_REQUEST_RESPONSE_CODE,
27};
28use crate::net::api::HasApiContext;
29
30const IROH_API_CONNECTION_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
32
33const IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_CODE: u32 = 0;
36
37const IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_REASON: &[u8] = b"idle timeout";
39
40const UNKNOWN_METHOD: &str = "unknown";
43
44pub(super) async fn run_iroh_api(
45 api: Arc<IrohApiState>,
46 endpoint: Endpoint,
47 task_group: TaskGroup,
48) {
49 loop {
50 match endpoint.accept().await {
51 Some(incoming) => {
52 let permit = acquire_iroh_api_permit(
53 &api.parallel_connections_limit,
54 api.limits.max_connections,
55 "0.35",
56 "connection",
57 )
58 .await;
59 task_group.spawn_cancellable_silent(
60 "handle-iroh-connection",
61 handle_incoming(
62 api.clone(),
63 task_group.clone(),
64 incoming,
65 permit,
66 api.limits.max_requests_per_connection,
67 )
68 .then(|result| async {
69 if let Err(err) = result {
70 warn!(target: LOG_NET_API, err = %err.fmt_compact_anyhow(), "Failed to handle iroh connection");
71 }
72 }),
73 );
74 }
75 None => return,
76 }
77 }
78}
79
80type CoreApi = BTreeMap<String, ApiEndpoint<ConsensusApi>>;
81type ModuleApi = BTreeMap<ModuleInstanceId, BTreeMap<String, ApiEndpoint<DynServerModule>>>;
82
83pub(super) struct IrohApiState {
84 consensus: ConsensusApi,
85 core: CoreApi,
86 modules: ModuleApi,
87 limits: ConnectionLimits,
88 parallel_connections_limit: Arc<Semaphore>,
89}
90
91impl IrohApiState {
92 pub(super) fn new(consensus: ConsensusApi, limits: ConnectionLimits) -> Arc<Self> {
93 let core_api = server_endpoints()
94 .into_iter()
95 .map(|endpoint| (endpoint.path.to_string(), endpoint))
96 .collect();
97
98 let module_api = consensus
99 .modules
100 .iter_modules()
101 .map(|(id, _, module)| {
102 let api_endpoints = module
103 .api_endpoints()
104 .into_iter()
105 .map(|endpoint| (endpoint.path.to_string(), endpoint))
106 .collect::<BTreeMap<String, ApiEndpoint<DynServerModule>>>();
107
108 (id, api_endpoints)
109 })
110 .collect();
111
112 Arc::new(Self {
113 consensus,
114 core: core_api,
115 modules: module_api,
116 parallel_connections_limit: Arc::new(Semaphore::new(limits.max_connections)),
117 limits,
118 })
119 }
120}
121
122async fn acquire_iroh_api_permit(
123 limit: &Arc<Semaphore>,
124 max: usize,
125 version: &'static str,
126 resource: &'static str,
127) -> tokio::sync::OwnedSemaphorePermit {
128 if limit.available_permits() == 0 {
129 warn!(
130 target: LOG_NET_API,
131 limit = max,
132 version,
133 resource,
134 "Iroh API limit reached, blocking"
135 );
136 }
137 limit
138 .clone()
139 .acquire_owned()
140 .await
141 .expect("semaphore should not be closed")
142}
143
144struct ActiveIrohApiConnection {
145 _duration: HistogramTimer,
146}
147
148impl ActiveIrohApiConnection {
149 fn new() -> Self {
150 IROH_API_CONNECTIONS_ACTIVE.inc();
151 Self {
152 _duration: IROH_API_CONNECTION_DURATION_SECONDS.start_timer(),
153 }
154 }
155}
156
157impl Drop for ActiveIrohApiConnection {
158 fn drop(&mut self) {
159 IROH_API_CONNECTIONS_ACTIVE.dec();
160 }
161}
162
163async fn handle_incoming(
164 api: Arc<IrohApiState>,
165 task_group: TaskGroup,
166 incoming: Incoming,
167 connection_permit: tokio::sync::OwnedSemaphorePermit,
168 iroh_api_max_requests_per_connection: usize,
169) -> anyhow::Result<()> {
170 let connection = incoming.accept()?.await?;
171 handle_iroh_api_connection(
172 api,
173 task_group,
174 VersionedIrohConnection::Legacy(connection),
175 connection_permit,
176 iroh_api_max_requests_per_connection,
177 IrohApiVersion::Legacy,
178 )
179 .await
180}
181
182#[derive(Clone, Copy)]
183enum IrohApiVersion {
184 Legacy,
185 Next,
186}
187
188impl IrohApiVersion {
189 fn log_label(self) -> &'static str {
190 match self {
191 Self::Legacy => "0.35",
192 Self::Next => "1.0",
193 }
194 }
195
196 fn metric_label(self) -> &'static str {
197 match self {
198 Self::Legacy => "default",
199 Self::Next => "next",
200 }
201 }
202
203 fn request_task_name(self) -> &'static str {
204 match self {
205 Self::Legacy => "handle-iroh-request",
206 Self::Next => "handle-iroh-next-request",
207 }
208 }
209}
210
211enum VersionedIrohConnection {
212 Legacy(iroh::endpoint::Connection),
213 Next(iroh_next::endpoint::Connection),
214}
215
216impl VersionedIrohConnection {
217 async fn accept_bi(&self) -> anyhow::Result<(VersionedSendStream, VersionedRecvStream)> {
218 Ok(match self {
219 Self::Legacy(connection) => {
220 let (send, recv) = connection.accept_bi().await?;
221 (
222 VersionedSendStream::Legacy(send),
223 VersionedRecvStream::Legacy(recv),
224 )
225 }
226 Self::Next(connection) => {
227 let (send, recv) = connection.accept_bi().await?;
228 (
229 VersionedSendStream::Next(send),
230 VersionedRecvStream::Next(recv),
231 )
232 }
233 })
234 }
235
236 fn close_for_idle_timeout(&self) {
237 match self {
238 Self::Legacy(connection) => connection.close(
239 VarInt::from_u32(IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_CODE),
240 IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_REASON,
241 ),
242 Self::Next(connection) => connection.close(
243 iroh_next::endpoint::VarInt::from_u32(IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_CODE),
244 IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_REASON,
245 ),
246 }
247 }
248}
249
250enum VersionedSendStream {
251 Legacy(SendStream),
252 Next(iroh_next::endpoint::SendStream),
253}
254
255impl VersionedSendStream {
256 async fn write_response(mut self, response: &[u8]) -> anyhow::Result<()> {
257 match &mut self {
258 Self::Legacy(send) => {
259 send.write_all(response).await?;
260 send.finish()?;
261 }
262 Self::Next(send) => {
263 send.write_all(response).await?;
264 send.finish()?;
265 }
266 }
267 Ok(())
268 }
269}
270
271enum VersionedRecvStream {
272 Legacy(RecvStream),
273 Next(iroh_next::endpoint::RecvStream),
274}
275
276impl VersionedRecvStream {
277 async fn read_request(&mut self) -> anyhow::Result<Vec<u8>> {
278 Ok(match self {
279 Self::Legacy(recv) => recv.read_to_end(100_000).await?,
280 Self::Next(recv) => recv.read_to_end(100_000).await?,
281 })
282 }
283}
284
285async fn handle_iroh_api_connection(
286 api: Arc<IrohApiState>,
287 task_group: TaskGroup,
288 connection: VersionedIrohConnection,
289 _connection_permit: tokio::sync::OwnedSemaphorePermit,
290 max_requests: usize,
291 version: IrohApiVersion,
292) -> anyhow::Result<()> {
293 let parallel_requests_limit = Arc::new(Semaphore::new(max_requests));
294 let _metrics = ActiveIrohApiConnection::new();
295
296 loop {
297 let accept_result = fedimint_core::runtime::timeout(
298 IROH_API_CONNECTION_IDLE_TIMEOUT,
299 connection.accept_bi(),
300 )
301 .await;
302
303 let (send_stream, recv_stream) = match accept_result {
304 Ok(streams) => streams?,
305 Err(_) if parallel_requests_limit.available_permits() < max_requests => continue,
306 Err(_) => {
307 IROH_API_CONNECTION_IDLE_TIMEOUT_TOTAL.inc();
308 tracing::debug!(
309 target: LOG_NET_API,
310 version = version.log_label(),
311 idle_timeout_secs = IROH_API_CONNECTION_IDLE_TIMEOUT.as_secs(),
312 "Closing idle Iroh API connection"
313 );
314 connection.close_for_idle_timeout();
315 return Ok(());
316 }
317 };
318
319 let permit = acquire_iroh_api_permit(
320 ¶llel_requests_limit,
321 max_requests,
322 version.log_label(),
323 "request",
324 )
325 .await;
326 task_group.spawn_cancellable_silent(
327 version.request_task_name(),
328 handle_iroh_api_stream(
329 api.clone(),
330 send_stream,
331 recv_stream,
332 permit,
333 version.metric_label(),
334 )
335 .then(|result| async {
336 if let Err(err) = result {
337 warn!(target: LOG_NET_API, err = %err.fmt_compact_anyhow(), "Failed to handle Iroh API request");
338 }
339 }),
340 );
341 }
342}
343
344async fn handle_iroh_api_stream(
345 api: Arc<IrohApiState>,
346 send_stream: VersionedSendStream,
347 mut recv_stream: VersionedRecvStream,
348 _request_permit: tokio::sync::OwnedSemaphorePermit,
349 metric_label: &'static str,
350) -> anyhow::Result<()> {
351 let request = recv_stream.read_request().await?;
352 let response = handle_iroh_api_request(&api, &request, metric_label).await?;
353 send_stream.write_response(&response).await
354}
355
356async fn handle_iroh_api_request(
357 api: &IrohApiState,
358 request: &[u8],
359 version_label: &'static str,
360) -> anyhow::Result<Vec<u8>> {
361 let request = serde_json::from_slice::<IrohApiRequest>(request)?;
362 let request_method = request.method.clone();
363 let response = record_request_metrics(
364 &api.core,
365 &api.modules,
366 &request_method,
367 version_label,
368 await_response(api, request),
369 )
370 .await;
371
372 Ok(serde_json::to_vec(&response)?)
373}
374
375fn metric_method<'a, C, M>(
376 core: &'a BTreeMap<String, C>,
377 modules: &'a BTreeMap<ModuleInstanceId, BTreeMap<String, M>>,
378 method: &ApiMethod,
379) -> Cow<'a, str> {
380 match method {
381 ApiMethod::Core(method) => core
382 .get_key_value(method)
383 .map_or(Cow::Borrowed(UNKNOWN_METHOD), |(method, _)| {
384 Cow::Borrowed(method)
385 }),
386 ApiMethod::Module(module_id, method) => modules
387 .get_key_value(module_id)
388 .and_then(|(module_id, endpoints)| {
389 endpoints
390 .get_key_value(method)
391 .map(|(method, _)| Cow::Owned(format!("{module_id}-{method}")))
392 })
393 .unwrap_or(Cow::Borrowed(UNKNOWN_METHOD)),
394 }
395}
396
397async fn record_request_metrics<T, C, M>(
398 core: &BTreeMap<String, C>,
399 modules: &BTreeMap<ModuleInstanceId, BTreeMap<String, M>>,
400 request_method: &ApiMethod,
401 version_label: &'static str,
402 response: impl Future<Output = Result<T, ApiError>>,
403) -> Result<T, ApiError> {
404 let method = metric_method(core, modules, request_method);
405 let timer = IROH_API_REQUEST_DURATION_SECONDS
406 .with_label_values(&[method.as_ref()])
407 .start_timer();
408 let response = response.await;
409 timer.observe_duration();
410
411 let response_code = response
412 .as_ref()
413 .map_or_else(|err| err.code.to_string(), |_| "0".to_string());
414 IROH_API_REQUEST_RESPONSE_CODE
415 .with_label_values(&[method.as_ref(), response_code.as_str(), version_label])
416 .inc();
417
418 response
419}
420
421async fn await_response(api: &IrohApiState, request: IrohApiRequest) -> Result<Value, ApiError> {
422 match request.method {
423 ApiMethod::Core(method) => {
424 let endpoint = api
425 .core
426 .get(&method)
427 .ok_or_else(|| ApiError::not_found(method.clone()))?;
428
429 let (state, context) = api.consensus.context(&request.request, None).await;
430
431 run_handler(
432 None,
433 &method,
434 (endpoint.handler)(state, context, request.request),
435 )
436 .await
437 }
438 ApiMethod::Module(module_id, method) => {
439 let endpoint = api
440 .modules
441 .get(&module_id)
442 .ok_or_else(|| ApiError::not_found(module_id.to_string()))?
443 .get(&method)
444 .ok_or_else(|| ApiError::not_found(method.clone()))?;
445
446 let (state, context) = api
447 .consensus
448 .context(&request.request, Some(module_id))
449 .await;
450
451 run_handler(
452 Some(module_id),
453 &method,
454 (endpoint.handler)(state, context, request.request),
455 )
456 .await
457 }
458 }
459}
460
461async fn run_handler(
468 module_id: Option<ModuleInstanceId>,
469 method: &str,
470 handler: impl Future<Output = Result<Value, ApiError>>,
471) -> Result<Value, ApiError> {
472 AssertUnwindSafe(handler)
477 .catch_unwind()
478 .await
479 .unwrap_or_else(|_| {
480 error!(
481 target: LOG_NET_API,
482 module_id = ?module_id,
483 method,
484 "API handler panicked, DO NOT IGNORE, FIX IT!!!"
485 );
486
487 Err(ApiError::server_error("API handler panicked".to_string()))
488 })
489}
490
491pub(super) async fn run_iroh_api_next(
494 api: Arc<IrohApiState>,
495 endpoint: iroh_next::Endpoint,
496 task_group: TaskGroup,
497) {
498 loop {
499 match endpoint.accept().await {
500 Some(incoming) => {
501 let permit = acquire_iroh_api_permit(
502 &api.parallel_connections_limit,
503 api.limits.max_connections,
504 "1.0",
505 "connection",
506 )
507 .await;
508 task_group.spawn_cancellable_silent(
509 "handle-iroh-next-connection",
510 handle_incoming_next(
511 api.clone(),
512 task_group.clone(),
513 incoming,
514 permit,
515 api.limits.max_requests_per_connection,
516 )
517 .then(|result| async {
518 if let Err(err) = result {
519 warn!(target: LOG_NET_API, err = %err.fmt_compact_anyhow(), "Failed to handle iroh-next connection");
520 }
521 }),
522 );
523 }
524 None => return,
525 }
526 }
527}
528
529async fn handle_incoming_next(
530 api: Arc<IrohApiState>,
531 task_group: TaskGroup,
532 incoming: iroh_next::endpoint::Incoming,
533 connection_permit: tokio::sync::OwnedSemaphorePermit,
534 iroh_api_max_requests_per_connection: usize,
535) -> anyhow::Result<()> {
536 let connection = incoming.accept()?.await?;
537 handle_iroh_api_connection(
538 api,
539 task_group,
540 VersionedIrohConnection::Next(connection),
541 connection_permit,
542 iroh_api_max_requests_per_connection,
543 IrohApiVersion::Next,
544 )
545 .await
546}
547
548#[cfg(test)]
549mod tests {
550 use std::collections::BTreeSet;
551 use std::net::SocketAddr;
552
553 use anyhow::Context as _;
554 use fedimint_metrics::prometheus::core::Collector;
555 use fedimint_metrics::prometheus::proto::Metric;
556 use futures::future::pending;
557 use futures::{pin_mut, poll};
558 use iroh_next::endpoint::presets::Minimal;
559 use iroh_next::{EndpointAddr, RelayMode, SecretKey, TransportAddr};
560
561 use super::*;
562
563 const TEST_ALPN: &[u8] = b"fedimint-iroh-api-adapter-test";
564
565 fn has_method_label(metric: &Metric, method: &str) -> bool {
566 metric
567 .get_label()
568 .iter()
569 .any(|label| label.name() == "method" && label.value() == method)
570 }
571
572 fn duration_count(method: &str) -> u64 {
573 IROH_API_REQUEST_DURATION_SECONDS
574 .collect()
575 .into_iter()
576 .flat_map(|family| family.metric)
577 .filter(|metric| has_method_label(metric, method))
578 .map(|metric| metric.histogram.sample_count())
579 .sum()
580 }
581
582 fn response_count(method: &str) -> u64 {
583 IROH_API_REQUEST_RESPONSE_CODE
584 .collect()
585 .into_iter()
586 .flat_map(|family| family.metric)
587 .filter(|metric| has_method_label(metric, method))
588 .map(|metric| metric.counter.value() as u64)
589 .sum()
590 }
591
592 fn method_series(metrics: Vec<Metric>, method: &str) -> BTreeSet<Vec<(String, String)>> {
593 metrics
594 .into_iter()
595 .filter(|metric| has_method_label(metric, method))
596 .map(|metric| {
597 metric
598 .label
599 .into_iter()
600 .map(|label| (label.name().to_owned(), label.value().to_owned()))
601 .collect()
602 })
603 .collect()
604 }
605
606 fn duration_series(method: &str) -> BTreeSet<Vec<(String, String)>> {
607 method_series(
608 IROH_API_REQUEST_DURATION_SECONDS
609 .collect()
610 .into_iter()
611 .flat_map(|family| family.metric)
612 .collect(),
613 method,
614 )
615 }
616
617 fn response_series(method: &str) -> BTreeSet<Vec<(String, String)>> {
618 method_series(
619 IROH_API_REQUEST_RESPONSE_CODE
620 .collect()
621 .into_iter()
622 .flat_map(|family| family.metric)
623 .collect(),
624 method,
625 )
626 }
627
628 #[tokio::test]
629 async fn bounds_method_labels_for_all_iroh_request_metrics() {
630 const CORE_METHOD: &str = "metrics_test_core";
631 const MODULE_ID: ModuleInstanceId = 42;
632 const MODULE_METHOD: &str = "metrics_test_module";
633 const MODULE_LABEL: &str = "42-metrics_test_module";
634
635 let core = BTreeMap::from([(CORE_METHOD.to_owned(), true)]);
636 let modules = BTreeMap::from([(
637 MODULE_ID,
638 BTreeMap::from([(MODULE_METHOD.to_owned(), true)]),
639 )]);
640 let hostile_methods = [
641 ApiMethod::Core("metrics_test_unknown_core".to_owned()),
642 ApiMethod::Core("metrics_test_unknown_core_!@#$%^&*()".to_owned()),
643 ApiMethod::Module(MODULE_ID, "metrics_test_unknown_module_method".to_owned()),
644 ApiMethod::Module(
645 MODULE_ID,
646 "metrics_test_unknown_module_method_with_a_long_suffix".to_owned(),
647 ),
648 ApiMethod::Module(43, "metrics_test_unknown_module".to_owned()),
649 ApiMethod::Module(44, "metrics_test_unknown_module_!@#$%^&*()".to_owned()),
650 ];
651
652 assert_eq!(
653 metric_method(&core, &modules, &ApiMethod::Core(CORE_METHOD.to_owned())),
654 CORE_METHOD
655 );
656 assert_eq!(
657 metric_method(
658 &core,
659 &modules,
660 &ApiMethod::Module(MODULE_ID, MODULE_METHOD.to_owned())
661 ),
662 MODULE_LABEL
663 );
664 for method in &hostile_methods {
665 assert_eq!(metric_method(&core, &modules, method), UNKNOWN_METHOD);
666 }
667
668 let duration_before = [
669 duration_count(CORE_METHOD),
670 duration_count(MODULE_LABEL),
671 duration_count(UNKNOWN_METHOD),
672 ];
673 let response_before = [
674 response_count(CORE_METHOD),
675 response_count(MODULE_LABEL),
676 response_count(UNKNOWN_METHOD),
677 ];
678
679 record_request_metrics(
680 &core,
681 &modules,
682 &ApiMethod::Core(CORE_METHOD.to_owned()),
683 "default",
684 async { Ok::<_, ApiError>(()) },
685 )
686 .await
687 .expect("registered core request succeeds");
688 record_request_metrics(
689 &core,
690 &modules,
691 &ApiMethod::Module(MODULE_ID, MODULE_METHOD.to_owned()),
692 "next",
693 async { Ok::<_, ApiError>(()) },
694 )
695 .await
696 .expect("registered module request succeeds");
697 for method in &hostile_methods {
698 record_request_metrics(&core, &modules, method, "default", async {
699 Err::<(), _>(ApiError::not_found("test rejection".to_owned()))
700 })
701 .await
702 .expect_err("unregistered request is rejected");
703 }
704
705 {
706 let cancelled_method =
707 ApiMethod::Core("metrics_test_cancelled_attacker_input".to_owned());
708 let cancelled = record_request_metrics(
709 &core,
710 &modules,
711 &cancelled_method,
712 "default",
713 pending::<Result<(), ApiError>>(),
714 );
715 pin_mut!(cancelled);
716 assert!(poll!(cancelled.as_mut()).is_pending());
717 }
718
719 assert_eq!(duration_count(CORE_METHOD) - duration_before[0], 1);
720 assert_eq!(duration_count(MODULE_LABEL) - duration_before[1], 1);
721 assert_eq!(
722 duration_count(UNKNOWN_METHOD) - duration_before[2],
723 hostile_methods.len() as u64 + 1
724 );
725 assert_eq!(response_count(CORE_METHOD) - response_before[0], 1);
726 assert_eq!(response_count(MODULE_LABEL) - response_before[1], 1);
727 assert_eq!(
728 response_count(UNKNOWN_METHOD) - response_before[2],
729 hostile_methods.len() as u64
730 );
731 assert_eq!(duration_series(UNKNOWN_METHOD).len(), 1);
732 let unknown_response_series = response_series(UNKNOWN_METHOD);
733 assert_eq!(unknown_response_series.len(), 1);
734 assert!(unknown_response_series.iter().any(|labels| {
735 labels
736 .iter()
737 .any(|(name, value)| name == "code" && value == "404")
738 && labels
739 .iter()
740 .any(|(name, value)| name == "type" && value == "default")
741 }));
742 assert!(response_series(CORE_METHOD).iter().any(|labels| {
743 labels
744 .iter()
745 .any(|(name, value)| name == "code" && value == "0")
746 && labels
747 .iter()
748 .any(|(name, value)| name == "type" && value == "default")
749 }));
750 assert!(response_series(MODULE_LABEL).iter().any(|labels| {
751 labels
752 .iter()
753 .any(|(name, value)| name == "code" && value == "0")
754 && labels
755 .iter()
756 .any(|(name, value)| name == "type" && value == "next")
757 }));
758
759 for method in hostile_methods
760 .iter()
761 .map(ToString::to_string)
762 .chain(["metrics_test_cancelled_attacker_input".to_owned()])
763 {
764 assert!(duration_series(&method).is_empty());
765 assert!(response_series(&method).is_empty());
766 }
767 }
768
769 #[tokio::test]
770 async fn panicking_handler_returns_an_error_instead_of_unwinding() {
771 let error = run_handler(None, "test_endpoint", async { panic!("handler panic") })
772 .await
773 .expect_err("a panicking handler is reported as a server error");
774
775 assert_eq!(error.code, 500);
776
777 let error = run_handler(Some(3), "test_endpoint", async {
778 panic!("module handler panic")
779 })
780 .await
781 .expect_err("a panicking module handler is reported as a server error");
782
783 assert_eq!(error.code, 500);
784 }
785
786 #[tokio::test]
787 async fn shared_connection_limit_applies_across_versions() {
788 let limit = Arc::new(Semaphore::new(1));
789 let legacy_permit = acquire_iroh_api_permit(&limit, 1, "0.35", "connection").await;
790
791 assert!(
792 tokio::time::timeout(
793 Duration::from_millis(20),
794 acquire_iroh_api_permit(&limit, 1, "1.0", "connection"),
795 )
796 .await
797 .is_err()
798 );
799
800 drop(legacy_permit);
801 let _permit = tokio::time::timeout(
802 Duration::from_secs(1),
803 acquire_iroh_api_permit(&limit, 1, "1.0", "connection"),
804 )
805 .await
806 .expect("v1 acquires the shared permit after the legacy connection releases it");
807 }
808
809 #[tokio::test]
810 async fn iroh_v1_request_uses_shared_stream_adapter() -> anyhow::Result<()> {
811 let server = iroh_next::Endpoint::builder(Minimal)
812 .relay_mode(RelayMode::Disabled)
813 .secret_key(SecretKey::from_bytes(&[11; 32]))
814 .alpns(vec![TEST_ALPN.to_vec()])
815 .bind_addr(SocketAddr::from(([127, 0, 0, 1], 0)))?
816 .bind()
817 .await?;
818 let client = iroh_next::Endpoint::builder(Minimal)
819 .relay_mode(RelayMode::Disabled)
820 .bind()
821 .await?;
822 let server_addr = EndpointAddr::from_parts(
823 server.id(),
824 server.bound_sockets().into_iter().map(TransportAddr::Ip),
825 );
826 let (client_done_tx, client_done_rx) = tokio::sync::oneshot::channel();
827
828 let server_request = async {
829 let incoming = server.accept().await.context("server endpoint closed")?;
830 let connection = incoming.accept()?.await?;
831 let (send, mut recv) = VersionedIrohConnection::Next(connection)
832 .accept_bi()
833 .await?;
834 assert_eq!(recv.read_request().await?, b"request");
835 send.write_response(b"response").await?;
836 client_done_rx.await?;
837 anyhow::Ok(())
838 };
839 let client_request = async {
840 let connection = client.connect(server_addr, TEST_ALPN).await?;
841 let (mut send, mut recv) = connection.open_bi().await?;
842 send.write_all(b"request").await?;
843 send.finish()?;
844 let response = recv.read_to_end(100_000).await?;
845 anyhow::ensure!(response == b"response");
846 client_done_tx.send(()).expect("server is still running");
847 anyhow::Ok(())
848 };
849
850 tokio::time::timeout(Duration::from_secs(10), async {
851 tokio::try_join!(server_request, client_request)
852 })
853 .await
854 .context("Iroh v1 adapter test timed out")??;
855 client.close().await;
856 server.close().await;
857 Ok(())
858 }
859}