1use std::{sync::Arc, time::Duration};
17
18use axum::{BoxError, Router, error_handling::HandleErrorLayer};
19use endhost_api::routes::nest_endhost_api;
20use endhost_api_models::{
21 SegmentsDiscovery,
22 underlays::{ScionRouter, Underlays},
23};
24use http::StatusCode;
25use scion_sdk_observability::info_trace_layer;
26use sciparse::identifier::isd_asn::IsdAsn;
27use tower::{ServiceBuilder, timeout::TimeoutLayer};
28use tower_http::cors::CorsLayer;
29use url::Url;
30
31use crate::{
32 api::{
33 crpc::{
34 model::{SnapDataPlaneResolver, SnapTunIdentityRegistry},
35 nest_crpc_api,
36 },
37 http::{model::PgWapSessionManager, nest_http_api},
38 },
39 model::UnderlayDiscovery,
40 server::{
41 auth::AuthMiddlewareLayer,
42 metrics::{Metrics, PrometheusMiddlewareLayer},
43 },
44};
45
46pub mod auth;
47pub mod identity_registry;
48pub mod jwks_key_store;
49pub mod metrics;
50pub mod state;
51pub mod token_verifier;
52
53pub use token_verifier::SnapTokenVerifier;
54
55const CONTROL_PLANE_API_TIMEOUT: Duration = Duration::from_secs(30);
56
57pub fn build_router<UD, SL, SR, IR>(
59 underlay_discovery: UD,
60 snap_cp_api: Url,
61 segment_lister: SL,
62 snap_resolver: SR,
63 identity_registry: Arc<IR>,
64 pg_wap_session_manager: Option<Arc<dyn PgWapSessionManager>>,
65 token_verifier: SnapTokenVerifier,
66 metrics: Metrics,
67) -> std::io::Result<Router>
68where
69 UD: UnderlayDiscovery + 'static + Send + Sync,
70 SL: SegmentsDiscovery + 'static + Send + Sync,
71 SR: SnapDataPlaneResolver + 'static + Send + Sync,
72 IR: SnapTunIdentityRegistry + 'static + Send + Sync,
73{
74 let mut auth_router = Router::new();
76 auth_router = nest_endhost_api(
77 auth_router,
78 Arc::new(UnderlayDiscoveryAdapter::new(
79 Arc::new(underlay_discovery),
80 snap_cp_api,
81 )),
82 Arc::new(segment_lister),
83 );
84 auth_router = nest_crpc_api(auth_router, Arc::new(snap_resolver), identity_registry);
85 auth_router =
86 auth_router.layer(ServiceBuilder::new().layer(AuthMiddlewareLayer::new(token_verifier)));
87
88 let mut router = Router::new();
90 if let Some(pg_wap_session_manager) = pg_wap_session_manager {
93 let http_api_router = nest_http_api(Router::new(), pg_wap_session_manager).layer(
98 CorsLayer::new()
99 .allow_methods(tower_http::cors::Any)
100 .allow_headers(tower_http::cors::Any)
101 .allow_origin(tower_http::cors::AllowOrigin::mirror_request()),
102 );
103 router = router.merge(http_api_router);
104 }
105
106 router = router.merge(auth_router);
108
109 router = router.layer(
111 ServiceBuilder::new()
112 .layer(HandleErrorLayer::new(|err: BoxError| {
113 async move {
114 tracing::error!(error=%err, "Control plane API error");
115 (
116 StatusCode::INTERNAL_SERVER_ERROR,
117 format!("Unhandled error: {err}"),
118 )
119 }
120 }))
121 .layer(info_trace_layer())
122 .layer(TimeoutLayer::new(CONTROL_PLANE_API_TIMEOUT))
123 .layer(PrometheusMiddlewareLayer::new(metrics)),
124 );
125 Ok(router)
126}
127
128struct UnderlayDiscoveryAdapter<T: UnderlayDiscovery> {
130 underlay_discovery: Arc<T>,
131 snap_cp_api: Url,
132}
133
134impl<T: UnderlayDiscovery> UnderlayDiscoveryAdapter<T> {
135 fn new(underlay_discovery: Arc<T>, snap_cp_api: Url) -> Self {
136 Self {
137 underlay_discovery,
138 snap_cp_api,
139 }
140 }
141}
142
143impl<T: UnderlayDiscovery> endhost_api_models::UnderlayDiscovery for UnderlayDiscoveryAdapter<T> {
144 fn list_underlays(&self, isd_as: IsdAsn) -> Underlays {
145 let dps = self.underlay_discovery.list_udp_underlays();
146 let mut udp_underlay = Vec::new();
147 for dp in dps {
148 for router_as in dp.isd_ases {
149 if isd_as != IsdAsn::WILDCARD && router_as.isd_as != isd_as {
150 continue;
151 };
152
153 udp_underlay.push(ScionRouter {
154 isd_as: router_as.isd_as,
155 internal_interface: dp.endpoint,
156 interfaces: router_as.interfaces.clone(),
157 });
158 }
159 }
160
161 let sus = self.underlay_discovery.list_snap_underlays();
162 if sus.is_empty() {
163 return Underlays {
164 udp_underlay,
165 snap_underlay: Vec::new(),
166 };
167 }
168
169 let mut snap_underlay = Vec::new();
170 let all_ases: Vec<IsdAsn> = sus.iter().flat_map(|su| su.isd_ases.clone()).collect();
171 if isd_as == IsdAsn::WILDCARD || all_ases.contains(&isd_as) {
172 snap_underlay.push(endhost_api_models::underlays::Snap {
173 address: self.snap_cp_api.clone(),
174 isd_ases: all_ases,
175 });
176 }
177
178 Underlays {
179 udp_underlay,
180 snap_underlay,
181 }
182 }
183}