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 = auth_router
84        .layer(ServiceBuilder::new().layer(AuthMiddlewareLayer::new(token_verifier.clone())));
85
86    let mut router = Router::new();
87    if let Some(wap_control_api) = wap_control_api {
88        // Since the WAP CP must be callable from any website, allow unrestricted CORS.
89        // Sub-router so CORS only applies to WAP CP endpoints, not the rest of the SNAP CP.
90        let crpc_api_router =
91            crate::pg_wap::crpc::api::nest_crpc_api(Router::new(), wap_control_api)
92                .layer(AuthMiddlewareLayer::new(token_verifier))
93                .layer(
94                    CorsLayer::new()
95                        .allow_methods(tower_http::cors::Any)
96                        .allow_headers(tower_http::cors::Any)
97                        .allow_origin(tower_http::cors::AllowOrigin::mirror_request()),
98                );
99
100        router = router.merge(crpc_api_router);
101    }
102
103    // Merge the authenticated router into the main router
104    router = router.merge(auth_router);
105
106    // Apply common middlewares to ALL routes (error handling, tracing, timeout, metrics)
107    router = router.layer(
108        ServiceBuilder::new()
109            .layer(HandleErrorLayer::new(|err: BoxError| {
110                async move {
111                    tracing::error!(error=%err, "Control plane API error");
112                    (
113                        StatusCode::INTERNAL_SERVER_ERROR,
114                        format!("Unhandled error: {err}"),
115                    )
116                }
117            }))
118            .layer(info_trace_layer())
119            .layer(TimeoutLayer::new(CONTROL_PLANE_API_TIMEOUT))
120            .layer(PrometheusMiddlewareLayer::new(metrics)),
121    );
122    Ok(router)
123}
124
125/// Adapter implementing UnderlayDiscovery for any DataPlaneDiscovery.
126struct UnderlayDiscoveryAdapter<T: UnderlayDiscovery> {
127    underlay_discovery: Arc<T>,
128    snap_cp_api: Url,
129}
130
131impl<T: UnderlayDiscovery> UnderlayDiscoveryAdapter<T> {
132    fn new(underlay_discovery: Arc<T>, snap_cp_api: Url) -> Self {
133        Self {
134            underlay_discovery,
135            snap_cp_api,
136        }
137    }
138}
139
140impl<T: UnderlayDiscovery> endhost_api_models::UnderlayDiscovery for UnderlayDiscoveryAdapter<T> {
141    fn list_underlays(&self, isd_as: IsdAsn) -> Underlays {
142        let dps = self.underlay_discovery.list_udp_underlays();
143        let mut udp_underlay = Vec::new();
144        for dp in dps {
145            for router_as in dp.isd_ases {
146                if isd_as != IsdAsn::WILDCARD && router_as.isd_as != isd_as {
147                    continue;
148                };
149
150                udp_underlay.push(ScionRouter {
151                    isd_as: router_as.isd_as,
152                    internal_interface: dp.endpoint,
153                    interfaces: router_as.interfaces.clone(),
154                });
155            }
156        }
157
158        let sus = self.underlay_discovery.list_snap_underlays();
159        if sus.is_empty() {
160            return Underlays {
161                udp_underlay,
162                snap_underlay: Vec::new(),
163            };
164        }
165
166        let mut snap_underlay = Vec::new();
167        let all_ases: Vec<IsdAsn> = sus.iter().flat_map(|su| su.isd_ases.clone()).collect();
168        if isd_as == IsdAsn::WILDCARD || all_ases.contains(&isd_as) {
169            snap_underlay.push(endhost_api_models::underlays::Snap {
170                address: self.snap_cp_api.clone(),
171                isd_ases: all_ases,
172            });
173        }
174
175        Underlays {
176            udp_underlay,
177            snap_underlay,
178        }
179    }
180}