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::crpc::{
33        model::{SnapDataPlaneResolver, SnapTunIdentityRegistry},
34        nest_crpc_api,
35    },
36    model::UnderlayDiscovery,
37    pg_wap::crpc::model::ControlServiceAPIHandler,
38    server::{
39        auth::AuthMiddlewareLayer,
40        metrics::{Metrics, PrometheusMiddlewareLayer},
41    },
42};
43
44pub mod auth;
45pub mod identity_registry;
46pub mod jwks_key_store;
47pub mod metrics;
48pub mod state;
49pub mod token_verifier;
50
51pub use token_verifier::SnapTokenVerifier;
52
53const CONTROL_PLANE_API_TIMEOUT: Duration = Duration::from_secs(30);
54
55/// Builds the SNAP control plane router.
56pub fn build_router<UD, SL, SR, IR>(
57    underlay_discovery: UD,
58    snap_cp_api: Url,
59    segment_lister: SL,
60    snap_resolver: SR,
61    identity_registry: Arc<IR>,
62    wap_control_api: Option<Arc<dyn ControlServiceAPIHandler>>,
63    token_verifier: SnapTokenVerifier,
64    metrics: Metrics,
65) -> std::io::Result<Router>
66where
67    UD: UnderlayDiscovery + 'static + Send + Sync,
68    SL: SegmentsDiscovery + 'static + Send + Sync,
69    SR: SnapDataPlaneResolver + 'static + Send + Sync,
70    IR: SnapTunIdentityRegistry + 'static + Send + Sync,
71{
72    // Create a sub-router for authenticated endpoints
73    let mut auth_router = Router::new();
74    auth_router = nest_endhost_api(
75        auth_router,
76        Arc::new(UnderlayDiscoveryAdapter::new(
77            Arc::new(underlay_discovery),
78            snap_cp_api,
79        )),
80        Arc::new(segment_lister),
81    );
82    auth_router = nest_crpc_api(auth_router, Arc::new(snap_resolver), identity_registry);
83    auth_router =
84        auth_router.layer(ServiceBuilder::new().layer(AuthMiddlewareLayer::new(token_verifier)));
85
86    // Main unauthorized router.
87    let mut router = Router::new();
88    // XXX(bunert): For now the pathguard WAP HTTP API is unauthenticated. This will change in the
89    // future.
90    if let Some(wap_control_api) = wap_control_api {
91        // The WAP control API is called cross-origin from the webscion browser SDK. CORS is not a
92        // security boundary here so we reflect any request Origin. Reflection (instead of a literal
93        // `*`) keeps this valid should the endpoint ever gain credentialed auth, where
94        // `Access-Control-Allow-Credentials` is incompatible with `*`.
95
96        let crpc_api_router =
97            crate::pg_wap::crpc::api::nest_crpc_api(Router::new(), wap_control_api).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
104        router = router.merge(crpc_api_router);
105    }
106
107    // Merge the authenticated router into the main router
108    router = router.merge(auth_router);
109
110    // Apply common middlewares to ALL routes (error handling, tracing, timeout, metrics)
111    router = router.layer(
112        ServiceBuilder::new()
113            .layer(HandleErrorLayer::new(|err: BoxError| {
114                async move {
115                    tracing::error!(error=%err, "Control plane API error");
116                    (
117                        StatusCode::INTERNAL_SERVER_ERROR,
118                        format!("Unhandled error: {err}"),
119                    )
120                }
121            }))
122            .layer(info_trace_layer())
123            .layer(TimeoutLayer::new(CONTROL_PLANE_API_TIMEOUT))
124            .layer(PrometheusMiddlewareLayer::new(metrics)),
125    );
126    Ok(router)
127}
128
129/// Adapter implementing UnderlayDiscovery for any DataPlaneDiscovery.
130struct UnderlayDiscoveryAdapter<T: UnderlayDiscovery> {
131    underlay_discovery: Arc<T>,
132    snap_cp_api: Url,
133}
134
135impl<T: UnderlayDiscovery> UnderlayDiscoveryAdapter<T> {
136    fn new(underlay_discovery: Arc<T>, snap_cp_api: Url) -> Self {
137        Self {
138            underlay_discovery,
139            snap_cp_api,
140        }
141    }
142}
143
144impl<T: UnderlayDiscovery> endhost_api_models::UnderlayDiscovery for UnderlayDiscoveryAdapter<T> {
145    fn list_underlays(&self, isd_as: IsdAsn) -> Underlays {
146        let dps = self.underlay_discovery.list_udp_underlays();
147        let mut udp_underlay = Vec::new();
148        for dp in dps {
149            for router_as in dp.isd_ases {
150                if isd_as != IsdAsn::WILDCARD && router_as.isd_as != isd_as {
151                    continue;
152                };
153
154                udp_underlay.push(ScionRouter {
155                    isd_as: router_as.isd_as,
156                    internal_interface: dp.endpoint,
157                    interfaces: router_as.interfaces.clone(),
158                });
159            }
160        }
161
162        let sus = self.underlay_discovery.list_snap_underlays();
163        if sus.is_empty() {
164            return Underlays {
165                udp_underlay,
166                snap_underlay: Vec::new(),
167            };
168        }
169
170        let mut snap_underlay = Vec::new();
171        let all_ases: Vec<IsdAsn> = sus.iter().flat_map(|su| su.isd_ases.clone()).collect();
172        if isd_as == IsdAsn::WILDCARD || all_ases.contains(&isd_as) {
173            snap_underlay.push(endhost_api_models::underlays::Snap {
174                address: self.snap_cp_api.clone(),
175                isd_ases: all_ases,
176            });
177        }
178
179        Underlays {
180            udp_underlay,
181            snap_underlay,
182        }
183    }
184}