Skip to main content

snap_control/
server.rs

1// Copyright 2025 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! SNAP control plane API server.
15
16use 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
57/// Builds the SNAP control plane router.
58pub 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    // Create a sub-router for authenticated endpoints
75    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    // Main unauthorized router.
89    let mut router = Router::new();
90    // XXX(bunert): For now the pathguard WAP HTTP API is unauthenticated. This will change in the
91    // future.
92    if let Some(pg_wap_session_manager) = pg_wap_session_manager {
93        // The WAP control API is called cross-origin from the webscion browser SDK. CORS is not a
94        // security boundary here so we reflect any request Origin. Reflection (instead of a literal
95        // `*`) keeps this valid should the endpoint ever gain credentialed auth, where
96        // `Access-Control-Allow-Credentials` is incompatible with `*`.
97        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    // Merge the authenticated router into the main router
107    router = router.merge(auth_router);
108
109    // Apply common middlewares to ALL routes (error handling, tracing, timeout, metrics)
110    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
128/// Adapter implementing UnderlayDiscovery for any DataPlaneDiscovery.
129struct 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}