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::{net::SocketAddr, 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 tokio::net::TcpListener;
28use tokio_util::sync::CancellationToken;
29use tower::{ServiceBuilder, timeout::TimeoutLayer};
30use url::Url;
31
32use crate::{
33    crpc_api::api_service::{
34        model::{SnapDataPlaneResolver, SnapTunIdentityRegistry},
35        nest_snap_control_api,
36    },
37    model::UnderlayDiscovery,
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 mock_segment_lister;
49pub mod state;
50pub mod token_verifier;
51
52pub use token_verifier::SnapTokenVerifier;
53
54const CONTROL_PLANE_API_TIMEOUT: Duration = Duration::from_secs(30);
55
56/// Start the SNAP control plane API server.
57pub async fn start<UD, SL, SR, IR>(
58    cancellation_token: CancellationToken,
59    listener: TcpListener,
60    underlay_discovery: UD,
61    segment_lister: SL,
62    snap_resolver: SR,
63    identity_registry: Arc<IR>,
64    token_verifier: SnapTokenVerifier,
65    metrics: Metrics,
66) -> std::io::Result<()>
67where
68    UD: UnderlayDiscovery + 'static + Send + Sync,
69    SL: SegmentsDiscovery + 'static + Send + Sync,
70    SR: SnapDataPlaneResolver + 'static + Send + Sync,
71    IR: SnapTunIdentityRegistry + 'static + Send + Sync,
72{
73    let router = Router::new();
74
75    let dp_discovery = Arc::new(underlay_discovery);
76    let segment_lister = Arc::new(segment_lister);
77    let snap_resolver = Arc::new(snap_resolver);
78
79    let snap_cp_addr = listener
80        .local_addr()
81        .map_err(|e| std::io::Error::other(format!("Failed to get own local address: {e}")))?;
82
83    let snap_cp_api = match snap_cp_addr {
84        SocketAddr::V4(addr) => {
85            Url::parse(&format!("http://{addr}"))
86                .expect("It is safe to format a SocketAddr as a URL")
87        }
88        SocketAddr::V6(addr) => {
89            Url::parse(&format!("http://[{}]:{}", addr.ip(), addr.port()))
90                .expect("It is safe to format a SocketAddr as a URL")
91        }
92    };
93
94    let router = nest_endhost_api(
95        router,
96        Arc::new(UnderlayDiscoveryAdapter::new(
97            dp_discovery.clone(),
98            snap_cp_api,
99        )),
100        segment_lister.clone(),
101    );
102
103    let router = nest_snap_control_api(router, snap_resolver, identity_registry);
104
105    let router = router.layer(
106        ServiceBuilder::new()
107            .layer(HandleErrorLayer::new(|err: BoxError| {
108                async move {
109                    tracing::error!(error=%err, "Control plane API error");
110
111                    (
112                        StatusCode::INTERNAL_SERVER_ERROR,
113                        format!("Unhandled error: {err}"),
114                    )
115                }
116            }))
117            .layer(info_trace_layer())
118            .layer(TimeoutLayer::new(CONTROL_PLANE_API_TIMEOUT))
119            .layer(PrometheusMiddlewareLayer::new(metrics))
120            .layer(AuthMiddlewareLayer::new(token_verifier)),
121    );
122
123    tracing::info!(addr=%snap_cp_addr, "Starting control plane API");
124
125    if let Err(e) = axum::serve(
126        listener,
127        router.into_make_service_with_connect_info::<SocketAddr>(),
128    )
129    .with_graceful_shutdown(cancellation_token.cancelled_owned())
130    .await
131    {
132        tracing::error!(error=%e, "Control plane API server unexpectedly stopped");
133    }
134
135    tracing::info!("Shutting down control plane API server");
136
137    Ok(())
138}
139
140/// Adapter implementing UnderlayDiscovery for any DataPlaneDiscovery.
141struct UnderlayDiscoveryAdapter<T: UnderlayDiscovery> {
142    underlay_discovery: Arc<T>,
143    snap_cp_api: Url,
144}
145
146impl<T: UnderlayDiscovery> UnderlayDiscoveryAdapter<T> {
147    fn new(underlay_discovery: Arc<T>, snap_cp_api: Url) -> Self {
148        Self {
149            underlay_discovery,
150            snap_cp_api,
151        }
152    }
153}
154
155impl<T: UnderlayDiscovery> endhost_api_models::UnderlayDiscovery for UnderlayDiscoveryAdapter<T> {
156    fn list_underlays(&self, isd_as: IsdAsn) -> Underlays {
157        let dps = self.underlay_discovery.list_udp_underlays();
158        let mut udp_underlay = Vec::new();
159        for dp in dps {
160            for router_as in dp.isd_ases {
161                if isd_as != IsdAsn::WILDCARD && router_as.isd_as != isd_as {
162                    continue;
163                };
164
165                udp_underlay.push(ScionRouter {
166                    isd_as: router_as.isd_as,
167                    internal_interface: dp.endpoint,
168                    interfaces: router_as.interfaces.clone(),
169                });
170            }
171        }
172
173        let sus = self.underlay_discovery.list_snap_underlays();
174        if sus.is_empty() {
175            return Underlays {
176                udp_underlay,
177                snap_underlay: Vec::new(),
178            };
179        }
180
181        let mut snap_underlay = Vec::new();
182        let all_ases: Vec<IsdAsn> = sus.iter().flat_map(|su| su.isd_ases.clone()).collect();
183        if isd_as == IsdAsn::WILDCARD || all_ases.contains(&isd_as) {
184            snap_underlay.push(endhost_api_models::underlays::Snap {
185                address: self.snap_cp_api.clone(),
186                isd_ases: all_ases,
187            });
188        }
189
190        Underlays {
191            udp_underlay,
192            snap_underlay,
193        }
194    }
195}