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