Skip to main content

slim_controller/
service.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::{HashMap, VecDeque};
5use std::pin::Pin;
6use std::sync::Arc;
7
8use std::time::Duration;
9
10use display_error_chain::ErrorChainExt;
11use slim_config::server::ServerConfig;
12use slim_session::subscription_manager::SubscriptionManager;
13use tokio::sync::mpsc;
14use tokio::task::JoinHandle;
15use tokio_stream::{Stream, StreamExt, wrappers::ReceiverStream};
16use tokio_util::sync::CancellationToken;
17use tonic::{Request, Response, Status};
18use tracing::{debug, error, info};
19
20use crate::api::proto::api::v1::control_message::Payload;
21use crate::api::proto::api::v1::controller_service_server::ControllerServiceServer;
22use crate::api::proto::api::v1::{
23    self, AuthMethod, ConnectionDetails, ConnectionDirection, ConnectionListResponse,
24    ConnectionType, RouteListResponse,
25};
26use crate::api::proto::api::v1::{
27    ConnectionEntry, ControlMessage, RouteEntry,
28    controller_service_client::ControllerServiceClient,
29    controller_service_server::ControllerService as GrpcControllerService,
30};
31use crate::errors::ControllerError;
32use prost_types::Struct;
33use slim_config::client::{
34    ClientConfig, RequiredAuthMethod, ServerConnectionConfig, TransportChannel,
35};
36use slim_config::grpc::client::AuthenticationConfig as ClientAuthenticationConfig;
37use slim_config::server::AuthenticationConfig;
38use slim_datapath::api::{
39    MessageType::Link as LinkMessageType, MessageType::Subscribe,
40    MessageType::SubscriptionAck as SubscriptionAckType, MessageType::Unsubscribe,
41    ProtoMessage as DataPlaneMessage,
42};
43use slim_datapath::api::{NameId, ProtoName};
44use slim_datapath::message_processing::MessageProcessor;
45use slim_datapath::messages::utils::SlimHeaderFlags;
46use slim_datapath::tables::{ConnType, SubscriptionTable};
47
48type TxChannel = mpsc::Sender<Result<ControlMessage, Status>>;
49type TxChannels = HashMap<String, TxChannel>;
50
51/// Maximum number of queued subscription notifications
52const MAX_QUEUED_NOTIFICATIONS: usize = 1000;
53
54/// Timeout for waiting on a subscription ack from the datapath.
55const SUBSCRIPTION_ACK_TIMEOUT: Duration = Duration::from_secs(30);
56
57use slim_auth::auth_provider::AuthProvider;
58use slim_auth::traits::TokenProvider;
59
60/// Settings struct for creating a ControlPlane instance
61#[derive(Clone)]
62pub struct ControlPlaneSettings {
63    /// Node ID of this SLIM instance
64    pub id: String,
65    /// Optional group name
66    pub domain_name: Option<String>,
67    /// Server configurations
68    pub servers: Vec<ServerConfig>,
69    /// Client configurations
70    pub clients: Vec<ClientConfig>,
71    /// Client configurations for server nodes
72    pub outbound_clients: Vec<ClientConfig>,
73    /// Data-plane client configurations (dataplane.clients in the service config).
74    /// Used as a credential fallback for CP-managed outbound links when no
75    /// outbound_clients entry matches, so users don't have to duplicate credentials.
76    pub dataplane_clients: Vec<ClientConfig>,
77    /// Message processor instance
78    pub message_processor: MessageProcessor,
79    /// array of connection details used by the control
80    /// plane to store the connection settings (e.g., TLS settings).
81    pub connection_details: Vec<ConnectionDetails>,
82    /// Optional auth provider for generating registration credentials.
83    pub auth_provider: Option<AuthProvider>,
84}
85
86/// Inner structure for the controller service
87/// This structure holds the internal state of the controller service,
88/// including the ID, message processor, connections, and channels.
89/// It is normally wrapped in an Arc to allow shared ownership across multiple threads.
90struct ControllerServiceInternal {
91    /// Node ID of this SLIM instance
92    id: String,
93
94    /// optional group name
95    domain_name: Option<String>,
96
97    /// underlying message processor
98    message_processor: MessageProcessor,
99
100    /// channel to send messages into the datapath
101    tx_slim: mpsc::Sender<Result<DataPlaneMessage, Status>>,
102
103    /// channels to send control messages
104    tx_channels: parking_lot::RwLock<TxChannels>,
105
106    /// cancellation token for graceful shutdown
107    cancellation_tokens: parking_lot::RwLock<HashMap<String, CancellationToken>>,
108
109    /// drain watch channel
110    drain_watch: parking_lot::RwLock<Option<drain::Watch>>,
111
112    /// queue for pending subscription notifications when connections are down
113    pending_notifications: Arc<parking_lot::Mutex<VecDeque<ControlMessage>>>,
114
115    /// Manages pending subscription ack tracking (id generation, registration, resolution).
116    subscription_manager: SubscriptionManager,
117
118    /// connection details used by control plane to store connection settings
119    connection_details: Vec<ConnectionDetails>,
120
121    /// Maps (subscription_name, link_id) → subscription_id for route tracking
122    route_subscription_ids: parking_lot::Mutex<HashMap<(ProtoName, u64), u64>>,
123
124    /// Reverse index: link_id → conn_id for O(1) lookup.
125    /// Populated lazily on first resolve and eagerly on connection create/delete.
126    link_id_to_conn_id: parking_lot::RwLock<HashMap<String, u64>>,
127
128    /// JoinHandles for control-plane stream processing tasks, keyed by endpoint.
129    stream_handles: parking_lot::Mutex<HashMap<String, tokio::task::JoinHandle<()>>>,
130
131    /// connection details for CP reconciled outbound data-plane connections
132    outbound_clients: Vec<ClientConfig>,
133
134    /// Data-plane client configurations (dataplane.clients in the service config).
135    /// Used as a credential fallback for CP-managed outbound links when no
136    /// outbound_clients entry matches, so users don't need to duplicate credentials.
137    dataplane_clients: Vec<ClientConfig>,
138
139    /// Optional auth provider for generating registration credentials.
140    auth_provider: Option<AuthProvider>,
141}
142
143#[derive(Clone)]
144struct ControllerService {
145    /// internal service state
146    inner: Arc<ControllerServiceInternal>,
147}
148
149/// The ControlPlane service is the main entry point for the controller service.
150pub struct ControlPlane {
151    /// servers
152    servers: Vec<ServerConfig>,
153
154    /// clients
155    clients: Vec<ClientConfig>,
156
157    /// drain signal channel
158    drain_signal: parking_lot::RwLock<Option<drain::Signal>>,
159
160    /// controller
161    controller: ControllerService,
162
163    /// channel to receive message from the datapath
164    /// to be used in listen_from_data_plane
165    rx_slim_option: Option<mpsc::Receiver<Result<DataPlaneMessage, Status>>>,
166}
167
168/// ControllerServiceInternal implements Drop trait to cancel all running listeners and
169/// clean up resources.
170impl Drop for ControlPlane {
171    fn drop(&mut self) {
172        // cancel all running listeners
173        for (_endpoint, token) in self.controller.inner.cancellation_tokens.write().drain() {
174            token.cancel();
175        }
176    }
177}
178
179/// Canonicalize a gRPC endpoint for comparison by ensuring the scheme's default
180/// port is always present. This lets "https://host" and "https://host:443"
181/// match even when one form omits the explicit port.
182fn canonical_endpoint(ep: &str) -> String {
183    let (prefix_len, default_port) = if ep.starts_with("https://") {
184        (8usize, "443")
185    } else if ep.starts_with("http://") {
186        (7usize, "80")
187    } else {
188        return ep.to_string();
189    };
190    let host_part = &ep[prefix_len..];
191    if !host_part.contains(':') {
192        format!("{}:{}", ep, default_port)
193    } else {
194        ep.to_string()
195    }
196}
197
198pub(crate) fn from_server_config(server_config: &ServerConfig) -> ConnectionDetails {
199    let mut endpoint = server_config.endpoint.clone();
200    let mut external_endpoint = None;
201    let mut spire_socket_path = None;
202    let mut trust_domain = None;
203    let mut client_config_tls_insecure: Option<bool> = None;
204    let mut remaining_fields = std::collections::BTreeMap::new();
205
206    if let Some(m) = &server_config.metadata {
207        for (k, v) in &m.inner {
208            match k.as_str() {
209                "local_endpoint" => {
210                    if let Some(s) = v.as_str()
211                        && !s.is_empty()
212                    {
213                        endpoint = s.to_string();
214                    }
215                }
216                "external_endpoint" => {
217                    if let Some(s) = v.as_str()
218                        && !s.is_empty()
219                    {
220                        external_endpoint = Some(s.to_string());
221                    }
222                }
223                "spire_socket_path" => {
224                    if let Some(s) = v.as_str()
225                        && !s.is_empty()
226                    {
227                        spire_socket_path = Some(s.to_string());
228                    }
229                }
230                "trust_domain" => {
231                    if let Some(s) = v.as_str()
232                        && !s.is_empty()
233                    {
234                        trust_domain = Some(s.to_string());
235                    }
236                }
237                "client_config" => {
238                    // When client_config is present it owns the remote TLS decision;
239                    // the server-level tls.insecure only governs local (intra-cluster)
240                    // connections and must not be used as a fallback.
241                    // Default to insecure=true so that absence of a tls section means
242                    // no TLS rather than inheriting the server-level flag.
243                    if let Some(cc) = v.as_map() {
244                        let insecure = cc
245                            .inner
246                            .get("tls")
247                            .and_then(|v| v.as_map())
248                            .and_then(|tls| tls.inner.get("insecure"))
249                            .and_then(|v| v.as_bool())
250                            .unwrap_or(true);
251                        client_config_tls_insecure = Some(insecure);
252                    }
253                    remaining_fields.insert(k.clone(), prost_types::Value::from(v));
254                }
255                _ => {
256                    remaining_fields.insert(k.clone(), prost_types::Value::from(v));
257                }
258            }
259        }
260    }
261
262    // tls_required is true when:
263    // - client_config is present and client_config.tls.insecure is false (remote TLS config
264    //   wins; absence of tls section defaults to insecure=true, i.e. no TLS)
265    // - or client_config is absent and the server-level TLS is not insecure (fallback)
266    // - or a SPIRE socket path is configured
267    let tls_required = client_config_tls_insecure
268        .map(|insecure| !insecure)
269        .unwrap_or(!server_config.tls_setting.insecure)
270        || spire_socket_path.is_some();
271    // AuthenticationConfig::Spire is gated to non-windows in slim-config.
272    #[cfg(not(target_family = "windows"))]
273    let auth_is_spire = matches!(server_config.auth, AuthenticationConfig::Spire(_));
274    #[cfg(target_family = "windows")]
275    let auth_is_spire = false;
276    let auth_method = if spire_socket_path.is_some() || trust_domain.is_some() || auth_is_spire {
277        AuthMethod::Spire
278    } else {
279        match &server_config.auth {
280            AuthenticationConfig::Basic(_) => AuthMethod::Basic,
281            AuthenticationConfig::Jwt(_) => AuthMethod::Jwt,
282            AuthenticationConfig::Oidc(_) => AuthMethod::Oidc,
283            _ => AuthMethod::None,
284        }
285    } as i32;
286    let metadata = if remaining_fields.is_empty() {
287        None
288    } else {
289        Some(Struct {
290            fields: remaining_fields,
291        })
292    };
293
294    ConnectionDetails {
295        endpoint,
296        external_endpoint,
297        tls_required,
298        auth_method,
299        spire_trust_domain: trust_domain,
300        metadata,
301    }
302}
303
304/// ControlPlane implements the service trait for the controller service.
305impl ControlPlane {
306    /// Create a new ControlPlane service instance
307    /// This function initializes the ControlPlane with the given ID, servers, clients, and message processor.
308    /// It also sets up the internal state, including the connections and channels.
309    /// # Arguments
310    /// * `id` - The ID of the SLIM instance.
311    /// * `servers` - A vector of server configurations.
312    /// * `clients` - A vector of client configurations.
313    /// * `drain_rx` - A drain watch channel for graceful shutdown.
314    /// * `message_processor` - An Arc to the message processor instance.
315    /// * `pubsub_servers` - A slice of server configurations for pub/sub connections.
316    /// # Returns
317    /// A new instance of ControlPlane.
318    pub fn new(config: ControlPlaneSettings) -> Self {
319        // create local connection with the message processor
320        let (_, tx_slim, rx_slim) = config
321            .message_processor
322            .register_local_connection(true)
323            .unwrap();
324
325        let (signal, watch) = drain::channel();
326
327        ControlPlane {
328            servers: config.servers,
329            clients: config.clients,
330            controller: ControllerService {
331                inner: Arc::new(ControllerServiceInternal {
332                    id: config.id,
333                    domain_name: config.domain_name,
334                    message_processor: config.message_processor,
335                    subscription_manager: SubscriptionManager::new(tx_slim.clone()),
336                    tx_slim,
337                    tx_channels: parking_lot::RwLock::new(HashMap::new()),
338                    cancellation_tokens: parking_lot::RwLock::new(HashMap::new()),
339                    drain_watch: parking_lot::RwLock::new(Some(watch)),
340                    pending_notifications: Arc::new(parking_lot::Mutex::new(VecDeque::new())),
341                    connection_details: config.connection_details,
342                    route_subscription_ids: parking_lot::Mutex::new(HashMap::new()),
343                    link_id_to_conn_id: parking_lot::RwLock::new(HashMap::new()),
344                    stream_handles: parking_lot::Mutex::new(HashMap::new()),
345                    outbound_clients: config.outbound_clients,
346                    dataplane_clients: config.dataplane_clients,
347                    auth_provider: config.auth_provider,
348                }),
349            },
350            drain_signal: parking_lot::RwLock::new(Some(signal)),
351            rx_slim_option: Some(rx_slim),
352        }
353    }
354
355    /// Take an existing ControlPlane instance and return a new one with the provided clients.
356    pub fn with_clients(mut self, clients: Vec<ClientConfig>) -> Self {
357        self.clients = clients;
358        self
359    }
360
361    /// Take an existing ControlPlane instance and return a new one with the provided servers.
362    pub fn with_servers(mut self, servers: Vec<ServerConfig>) -> Self {
363        self.servers = servers;
364        self
365    }
366
367    /// Run the clients and servers of the ControlPlane service.
368    /// This function starts all the servers and clients defined in the ControlPlane.
369    /// # Returns
370    /// A Result indicating success or failure of the operation.
371    /// # Errors
372    /// If there is an error starting any of the servers or clients, it will return a ControllerError.
373    pub async fn run(&mut self) -> Result<(), ControllerError> {
374        let rx = self
375            .rx_slim_option
376            .take()
377            .ok_or(ControllerError::AlreadyStarted)?;
378
379        // Collect servers to avoid borrowing self both mutably and immutably
380        let servers = self.servers.clone();
381        let clients = self.clients.clone();
382
383        // run all servers
384        for server in servers {
385            self.run_server(server).await?;
386        }
387
388        // run all clients
389        for client in clients {
390            self.run_client(client).await?;
391        }
392
393        self.listen_from_data_plane(rx).await?;
394
395        Ok(())
396    }
397
398    pub async fn deregister(&self) -> Result<(), ControllerError> {
399        let node_id = self.controller.inner.id.clone();
400        let deregister_msg = ControlMessage {
401            message_id: uuid::Uuid::new_v4().to_string(),
402            payload: Some(Payload::DeregisterNodeRequest(v1::DeregisterNodeRequest {
403                node: Some(v1::Node { id: node_id }),
404            })),
405        };
406        let channels: Vec<(String, TxChannel)> = self
407            .controller
408            .inner
409            .tx_channels
410            .read()
411            .iter()
412            .map(|(ep, tx)| (ep.clone(), tx.clone()))
413            .collect();
414        for (endpoint, tx) in channels {
415            if let Err(e) = tx.send(Ok(deregister_msg.clone())).await {
416                error!(%endpoint, error = %e, "failed to send deregister request");
417            }
418        }
419        Ok(())
420    }
421
422    pub async fn shutdown(&self) -> Result<(), ControllerError> {
423        // Get signal drain
424        let signal = self
425            .drain_signal
426            .write()
427            .take()
428            .ok_or(ControllerError::AlreadyStopped)?;
429
430        // Stop everything using the cancellation tokens
431        self.controller
432            .inner
433            .cancellation_tokens
434            .write()
435            .drain()
436            .for_each(|(endpoint, token)| {
437                info!(%endpoint, "stopping");
438                token.cancel();
439            });
440
441        // Drop watch channel
442        self.controller.inner.drain_watch.write().take();
443
444        // Wait for drain to complete
445        signal.drain().await;
446
447        Ok(())
448    }
449
450    async fn listen_from_data_plane(
451        &mut self,
452        mut rx: mpsc::Receiver<Result<DataPlaneMessage, Status>>,
453    ) -> Result<(), ControllerError> {
454        let cancellation_token = CancellationToken::new();
455        let cancellation_token_clone = cancellation_token.clone();
456
457        self.controller
458            .inner
459            .cancellation_tokens
460            .write()
461            .insert("DATA_PLANE".to_string(), cancellation_token_clone);
462
463        let clients = self.clients.clone();
464        let controller = self.controller.clone();
465
466        // Get a drain watch clone
467        let watch = self.controller.drain_watch()?;
468
469        debug!("Starting data plane listener");
470        tokio::spawn(async move {
471            let mut drain_fut = std::pin::pin!(watch.signaled());
472            loop {
473                tokio::select! {
474                    next = rx.recv() => {
475                        match next {
476                            Some(res) => {
477                                match res {
478                                    Ok(msg) => {
479                                        debug!("Received message {:?} from data plane, forwarding to control plane", msg);
480                                        match msg.get_type() {
481                                            Subscribe(_) => {
482                                                controller.handle_subscribe_message(msg.get_dst(), &clients).await;
483                                            }
484                                            Unsubscribe(_) => {
485                                                controller.handle_unsubscribe_message(msg.get_dst(), &clients).await;
486                                            }
487                                            SubscriptionAckType(_) => {
488                                                controller.inner.subscription_manager.resolve_ack(msg.get_subscription_ack());
489                                            }
490                                            LinkMessageType(link) => {
491                                                controller.handle_link_received(link, &clients).await;
492                                            }
493                                            _ => {
494                                                debug!("Ignoring unexpected message type from dataplane: {:?}", msg.get_type());
495                                            }
496                                        }
497                                    }
498                                    Err(e) => {
499                                        error!(error = %e.chain(), "received error from the data plane");
500                                        continue;
501                                    }
502                                }
503                            }
504                            None => {
505                                debug!("Data plane receiver channel closed.");
506                                break;
507                            }
508                        }
509                    }
510                    _ = cancellation_token.cancelled() => {
511                        debug!("shutting down stream on cancellation token");
512                        break;
513                    }
514                    _ = &mut drain_fut => {
515                        debug!("shutting down stream on drain");
516                        break;
517                    }
518                }
519            }
520        });
521        Ok(())
522    }
523
524    /// Stop the ControlPlane service.
525    /// This function stops all running listeners and cancels any ongoing operations.
526    /// It cleans up the internal state and ensures that all resources are released properly.
527    pub fn stop(&mut self) {
528        info!("stopping controller service");
529
530        // cancel all running listeners
531        for (endpoint, token) in self.controller.inner.cancellation_tokens.write().drain() {
532            info!(%endpoint, "stopping");
533            token.cancel();
534        }
535    }
536
537    /// Run a client configuration.
538    /// This function connects to the control plane using the provided client configuration.
539    /// It checks if the client is already running and if not, it starts a new connection.
540    async fn run_client(&mut self, client: ClientConfig) -> Result<(), ControllerError> {
541        if self
542            .controller
543            .inner
544            .cancellation_tokens
545            .read()
546            .contains_key(&client.endpoint)
547        {
548            return Err(ControllerError::ClientAlreadyRunning(client.endpoint));
549        }
550
551        let cancellation_token = CancellationToken::new();
552
553        let tx = self
554            .controller
555            .connect(client.clone(), cancellation_token.clone())
556            .await?;
557
558        // Store the cancellation token in the controller service
559        self.controller
560            .inner
561            .cancellation_tokens
562            .write()
563            .insert(client.endpoint.clone(), cancellation_token);
564
565        // Store the sender in the tx_channels map
566        self.controller
567            .inner
568            .tx_channels
569            .write()
570            .insert(client.endpoint.clone(), tx);
571
572        // return the sender for control messages
573        Ok(())
574    }
575
576    /// Run a server configuration.
577    /// This function starts a server using the provided server configuration.
578    /// It checks if the server is already running and if not, it starts a new server.
579    pub async fn run_server(&mut self, config: ServerConfig) -> Result<(), ControllerError> {
580        // Check if the server is already running
581        if self
582            .controller
583            .inner
584            .cancellation_tokens
585            .read()
586            .contains_key(&config.endpoint)
587        {
588            error!(endpoint = config.endpoint, "server is already running",);
589            return Err(ControllerError::ServerAlreadyRunning(config.endpoint));
590        }
591
592        let token = config
593            .run_grpc_server(
594                &[ControllerServiceServer::new(self.controller.clone())],
595                self.controller.drain_watch()?,
596            )
597            .await?;
598
599        // Store the cancellation token in the controller service
600        self.controller
601            .inner
602            .cancellation_tokens
603            .write()
604            .insert(config.endpoint.clone(), token.clone());
605
606        info!(%config.endpoint, "started controlplane server");
607
608        Ok(())
609    }
610}
611
612impl ControllerService {
613    fn resolve_connection_by_link_id(&self, link_id: &str) -> Result<Option<u64>, String> {
614        let cached = self.inner.link_id_to_conn_id.read().get(link_id).copied();
615        if let Some(conn_id) = cached {
616            if self
617                .inner
618                .message_processor
619                .connection_table()
620                .get(conn_id)
621                .is_some_and(|conn| conn.link_id().as_deref() == Some(link_id))
622            {
623                return Ok(Some(conn_id));
624            }
625            self.inner.link_id_to_conn_id.write().remove(link_id);
626        }
627
628        let mut resolved: Option<u64> = None;
629        self.inner
630            .message_processor
631            .connection_table()
632            .for_each(|id, conn| {
633                if conn.link_id().as_deref() == Some(link_id) && resolved.is_none() {
634                    resolved = Some(id);
635                }
636            });
637
638        if let Some(conn_id) = resolved {
639            self.inner
640                .link_id_to_conn_id
641                .write()
642                .insert(link_id.to_string(), conn_id);
643        }
644
645        Ok(resolved)
646    }
647
648    fn disconnect_connection_by_link_id(&self, link_id: &str) -> Result<(), String> {
649        if link_id.trim().is_empty() {
650            return Err("link_id cannot be empty".to_string());
651        }
652
653        let conn_id = match self.resolve_connection_by_link_id(link_id)? {
654            Some(id) => id,
655            None => {
656                return Err(format!("Connection with link_id {} not found", link_id));
657            }
658        };
659
660        if let Err(e) = self.inner.message_processor.disconnect(conn_id) {
661            // Best-effort delete: local/control-plane connections can lack config_data.
662            info!(
663                link_id = %link_id,
664                conn_id,
665                error = %e,
666                "Disconnect returned an error; continuing delete flow"
667            );
668        }
669
670        self.inner.link_id_to_conn_id.write().remove(link_id);
671        self.inner
672            .route_subscription_ids
673            .lock()
674            .retain(|(_name, cid), _| *cid != conn_id);
675
676        info!(link_id = %link_id, conn_id, "Successfully deleted connection by link_id");
677        Ok(())
678    }
679
680    fn resolve_route_connection(&self, route: &v1::Route) -> Result<Option<u64>, String> {
681        if let Some(link_id) = &route.link_id {
682            let trimmed = link_id.trim();
683            if !trimmed.is_empty() {
684                return self.resolve_connection_by_link_id(trimmed);
685            }
686        }
687
688        Ok(None)
689    }
690
691    async fn diff_connections(
692        &self,
693        desired_connections: &[v1::Connection],
694    ) -> Vec<v1::ConnectionAck> {
695        let mut connections_status = Vec::new();
696
697        let desired_link_ids: std::collections::HashSet<String> = desired_connections
698            .iter()
699            .map(|c| c.link_id.clone())
700            .collect();
701
702        let mut live_outgoing_link_ids: Vec<String> = Vec::new();
703        self.inner
704            .message_processor
705            .connection_table()
706            .for_each(|_id, conn| {
707                // Only manage Remote connections (CP-managed inter-group links).
708                // Peer connections are managed by the peer sync system and must not
709                // be deleted by the reconciler.
710                if conn.is_outgoing()
711                    && conn.connection_type() == slim_datapath::tables::ConnType::Remote
712                    && let Some(lid) = conn.link_id()
713                    && !lid.is_empty()
714                {
715                    live_outgoing_link_ids.push(lid);
716                }
717            });
718
719        for link_id in &live_outgoing_link_ids {
720            if desired_link_ids.contains(link_id) {
721                continue;
722            }
723            info!(link_id = %link_id, "desired state: removing connection");
724            let mut success = true;
725            let mut error_msg = String::new();
726            if let Err(err) = self.disconnect_connection_by_link_id(link_id) {
727                success = false;
728                error_msg = err;
729            }
730            connections_status.push(v1::ConnectionAck {
731                link_id: link_id.clone(),
732                success,
733                error_msg,
734            });
735        }
736
737        for conn in desired_connections {
738            let link_id = &conn.link_id;
739            if link_id.is_empty() {
740                continue;
741            }
742
743            let already_exists = match self.resolve_connection_by_link_id(link_id) {
744                Ok(Some(_)) => true,
745                Ok(None) => false,
746                Err(err) => {
747                    connections_status.push(v1::ConnectionAck {
748                        link_id: link_id.clone(),
749                        success: false,
750                        error_msg: err,
751                    });
752                    continue;
753                }
754            };
755
756            if already_exists {
757                connections_status.push(v1::ConnectionAck {
758                    link_id: link_id.clone(),
759                    success: true,
760                    error_msg: String::new(),
761                });
762                continue;
763            }
764
765            info!(?conn, "desired state: creating connection");
766            let mut success = true;
767            let mut error_msg = String::new();
768
769            match serde_json::from_str::<ServerConnectionConfig>(&conn.config_data) {
770                Err(e) => {
771                    success = false;
772                    error_msg = format!("Failed to parse config: {}", e);
773                }
774                Ok(server_config) => {
775                    let target_ep = canonical_endpoint(&server_config.endpoint);
776                    let mut client_config = self
777                        .inner
778                        .outbound_clients
779                        .iter()
780                        .find(|c| canonical_endpoint(&c.endpoint) == target_ep)
781                        .or_else(|| {
782                            // Fall back to the default outbound entry (empty endpoint),
783                            // which acts as a credential template for any CP-assigned link
784                            // that has no specific per-endpoint entry.
785                            self.inner
786                                .outbound_clients
787                                .iter()
788                                .find(|c| c.endpoint.is_empty())
789                        })
790                        .or_else(|| {
791                            // Finally fall back to dataplane.clients: reuse credentials
792                            // the node already has for this endpoint so users don't have
793                            // to duplicate them under controller.outbound_clients.
794                            self.inner
795                                .dataplane_clients
796                                .iter()
797                                .find(|c| canonical_endpoint(&c.endpoint) == target_ep)
798                        })
799                        .cloned()
800                        .unwrap_or_default();
801                    match client_config.merge_server_requirements(&server_config) {
802                        Err(err) => {
803                            success = false;
804                            error_msg = format!(
805                                "Failed to merge connection config to client config: {}",
806                                err
807                            );
808                        }
809                        Ok(()) => {
810                            if matches!(
811                                server_config.auth_method,
812                                // Spire config is not mandatory. If not set falls back to default
813                                // socket path.
814                                RequiredAuthMethod::Basic
815                                    | RequiredAuthMethod::Jwt
816                                    | RequiredAuthMethod::Oidc
817                            ) && matches!(client_config.auth, ClientAuthenticationConfig::None)
818                            {
819                                success = false;
820                                error_msg = format!(
821                                    "no local credentials configured for {}",
822                                    server_config.endpoint
823                                );
824                            }
825                            if success {
826                                client_config.link_id = link_id.clone();
827                                client_config.connection_type = ConnType::Remote;
828                                match self
829                                    .inner
830                                    .message_processor
831                                    .connect(client_config, None, None)
832                                    .await
833                                {
834                                    Err(e) => {
835                                        success = false;
836                                        error_msg = format!("Connection failed: {}", e);
837                                    }
838                                    Ok(conn_id) => {
839                                        self.inner
840                                            .link_id_to_conn_id
841                                            .write()
842                                            .insert(link_id.clone(), conn_id.1);
843                                        info!(
844                                            link_id = %link_id,
845                                            "Successfully created connection"
846                                        );
847                                    }
848                                }
849                            }
850                        }
851                    }
852                }
853            }
854
855            connections_status.push(v1::ConnectionAck {
856                link_id: link_id.clone(),
857                success,
858                error_msg,
859            });
860        }
861
862        connections_status
863    }
864
865    fn resolve_desired_routes<'a>(
866        &self,
867        desired_routes: &'a [v1::Route],
868    ) -> (HashMap<(ProtoName, u64), &'a v1::Route>, Vec<v1::RouteAck>) {
869        type SubKey = (ProtoName, u64);
870        let mut desired_subs: HashMap<SubKey, &v1::Route> = HashMap::new();
871        let mut failures: Vec<v1::RouteAck> = Vec::new();
872
873        for sub in desired_routes {
874            match self.resolve_route_connection(sub) {
875                Ok(Some(conn_id)) => {
876                    let name = sub.name.clone().unwrap();
877                    desired_subs.insert((name, conn_id), sub);
878                }
879                Ok(None) => {
880                    failures.push(v1::RouteAck {
881                        route: Some(sub.clone()),
882                        success: false,
883                        error_msg: "connection not found".to_string(),
884                    });
885                }
886                Err(err) => {
887                    failures.push(v1::RouteAck {
888                        route: Some(sub.clone()),
889                        success: false,
890                        error_msg: err,
891                    });
892                }
893            }
894        }
895
896        (desired_subs, failures)
897    }
898
899    async fn delete_stale_subscriptions(
900        &self,
901        desired_subs: &HashMap<(ProtoName, u64), &v1::Route>,
902    ) -> Vec<v1::RouteAck> {
903        let active_subs: Vec<((ProtoName, u64), u64)> = self
904            .inner
905            .route_subscription_ids
906            .lock()
907            .iter()
908            .map(|((name, conn_id), sub_id)| ((name.clone(), *conn_id), *sub_id))
909            .collect();
910
911        let stale: Vec<_> = active_subs
912            .into_iter()
913            .filter(|((name, conn_id), _)| !desired_subs.contains_key(&(name.clone(), *conn_id)))
914            .collect();
915
916        let futs = stale.iter().map(|((name, conn_id), sub_id)| {
917            let name = name.clone();
918            let conn_id = *conn_id;
919            let sub_id = *sub_id;
920            async move {
921                let conn_alive = self
922                    .inner
923                    .message_processor
924                    .connection_table()
925                    .get(conn_id)
926                    .is_some();
927
928                let (success, error_msg) = if conn_alive {
929                    let source = name.clone().with_id(0);
930                    let unsub_msg = DataPlaneMessage::builder()
931                        .source(source)
932                        .destination(name.clone())
933                        .identity("")
934                        .flags(SlimHeaderFlags::default().with_recv_from(conn_id))
935                        .build_unsubscribe()
936                        .unwrap();
937
938                    match self
939                        .send_unsubscribe_message_with_ack(unsub_msg, sub_id)
940                        .await
941                    {
942                        Ok(()) => (true, String::new()),
943                        Err(err) => (false, format!("Failed to unsubscribe: {}", err)),
944                    }
945                } else {
946                    (true, String::new())
947                };
948
949                (name, conn_id, success, error_msg)
950            }
951        });
952
953        let results = futures::future::join_all(futs).await;
954
955        let mut routes_status = Vec::with_capacity(results.len());
956        for (name, conn_id, success, error_msg) in results {
957            if success {
958                self.inner
959                    .route_subscription_ids
960                    .lock()
961                    .remove(&(name.clone(), conn_id));
962            }
963
964            routes_status.push(v1::RouteAck {
965                route: Some(v1::Route {
966                    name: Some(name.clone()),
967                    link_id: None,
968                    direction: None,
969                }),
970                success,
971                error_msg,
972            });
973        }
974
975        routes_status
976    }
977
978    async fn create_new_subscriptions(
979        &self,
980        desired_subs: &HashMap<(ProtoName, u64), &v1::Route>,
981    ) -> Vec<v1::RouteAck> {
982        let mut routes_status = Vec::new();
983
984        let to_create: Vec<((ProtoName, u64), v1::Route)> = desired_subs
985            .iter()
986            .filter(|((name, conn_id), _)| {
987                !self
988                    .inner
989                    .route_subscription_ids
990                    .lock()
991                    .contains_key(&(name.clone(), *conn_id))
992            })
993            .map(|((name, conn_id), sub)| ((name.clone(), *conn_id), (*sub).clone()))
994            .collect();
995
996        // Report already-active subscriptions as success.
997        for ((name, conn_id), sub) in desired_subs {
998            let dominated = to_create
999                .iter()
1000                .any(|((n, c), _)| n == name && *c == *conn_id);
1001            if !dominated {
1002                routes_status.push(v1::RouteAck {
1003                    route: Some((*sub).clone()),
1004                    success: true,
1005                    error_msg: String::new(),
1006                });
1007            }
1008        }
1009
1010        let futs = to_create.iter().map(|((name, conn_id), sub)| {
1011            let name = name.clone();
1012            let conn_id = *conn_id;
1013            let sub = sub.clone();
1014            async move {
1015                let source = name.clone().with_id(0);
1016                let sub_msg = DataPlaneMessage::builder()
1017                    .source(source)
1018                    .destination(name.clone())
1019                    .identity("")
1020                    .flags(SlimHeaderFlags::default().with_recv_from(conn_id))
1021                    .build_subscribe()
1022                    .unwrap();
1023
1024                let result = self.send_subscribe_message_with_ack(sub_msg).await;
1025                (name, conn_id, sub, result)
1026            }
1027        });
1028
1029        let results = futures::future::join_all(futs).await;
1030
1031        for (name, conn_id, sub, result) in results {
1032            let (success, error_msg) = match result {
1033                Ok(subscription_id) => {
1034                    self.inner
1035                        .route_subscription_ids
1036                        .lock()
1037                        .insert((name, conn_id), subscription_id);
1038                    info!(?sub, "desired state: created route");
1039                    (true, String::new())
1040                }
1041                Err(err) => (false, format!("Failed to subscribe: {}", err)),
1042            };
1043
1044            routes_status.push(v1::RouteAck {
1045                route: Some(sub),
1046                success,
1047                error_msg,
1048            });
1049        }
1050
1051        routes_status
1052    }
1053
1054    /// Handle new control messages.
1055    async fn handle_new_control_message(
1056        &self,
1057        msg: ControlMessage,
1058        tx: &mpsc::Sender<Result<ControlMessage, Status>>,
1059    ) -> Result<(), ControllerError> {
1060        match msg.payload {
1061            Some(ref payload) => {
1062                match payload {
1063                    Payload::ConfigCommand(config) => {
1064                        let (connections_status, routes_status) = if config.reconcile {
1065                            let connections_status =
1066                                self.diff_connections(&config.connections_to_create).await;
1067                            let (desired_subs, mut routes_status) =
1068                                self.resolve_desired_routes(&config.routes_to_set);
1069                            routes_status
1070                                .extend(self.delete_stale_subscriptions(&desired_subs).await);
1071                            routes_status
1072                                .extend(self.create_new_subscriptions(&desired_subs).await);
1073                            (connections_status, routes_status)
1074                        } else {
1075                            let mut connections_status = Vec::new();
1076                            let mut routes_status = Vec::new();
1077
1078                            // Process connections to delete by link_id.
1079                            for link_id in &config.connections_to_delete {
1080                                info!(link_id = %link_id, "received a connection to delete");
1081                                let mut connection_success = true;
1082                                let mut connection_error_msg = String::new();
1083
1084                                if let Err(err) = self.disconnect_connection_by_link_id(link_id) {
1085                                    connection_success = false;
1086                                    connection_error_msg = err;
1087                                }
1088
1089                                connections_status.push(v1::ConnectionAck {
1090                                    link_id: link_id.clone(),
1091                                    success: connection_success,
1092                                    error_msg: connection_error_msg,
1093                                });
1094                            }
1095
1096                            // Process connections to create
1097                            for conn in &config.connections_to_create {
1098                                info!(?conn, "received a connection to create");
1099                                let mut connection_success = true;
1100                                let mut connection_error_msg = String::new();
1101
1102                                match serde_json::from_str::<ClientConfig>(&conn.config_data) {
1103                                    Err(e) => {
1104                                        connection_success = false;
1105                                        connection_error_msg =
1106                                            format!("Failed to parse config: {}", e);
1107                                    }
1108                                    Ok(client_config) => {
1109                                        let client_endpoint = &client_config.endpoint;
1110                                        let requested_link_id =
1111                                            if client_config.link_id.trim().is_empty() {
1112                                                String::new()
1113                                            } else {
1114                                                client_config.link_id.clone()
1115                                            };
1116                                        let mut existing_conn_for_link_id = false;
1117
1118                                        if !requested_link_id.is_empty() {
1119                                            match self
1120                                                .resolve_connection_by_link_id(&requested_link_id)
1121                                            {
1122                                                Err(err) => {
1123                                                    connection_success = false;
1124                                                    connection_error_msg = err;
1125                                                }
1126                                                Ok(Some(conn_id)) => {
1127                                                    existing_conn_for_link_id = true;
1128                                                    self.inner
1129                                                        .link_id_to_conn_id
1130                                                        .write()
1131                                                        .insert(requested_link_id.clone(), conn_id);
1132                                                    info!(
1133                                                        link_id = %requested_link_id,
1134                                                        conn_id,
1135                                                        "Connection already exists for link_id"
1136                                                    );
1137                                                }
1138                                                Ok(None) => {}
1139                                            }
1140                                        }
1141
1142                                        if connection_success && !existing_conn_for_link_id {
1143                                            match self
1144                                                .inner
1145                                                .message_processor
1146                                                .connect(client_config.clone(), None, None)
1147                                                .await
1148                                            {
1149                                                Err(e) => {
1150                                                    connection_success = false;
1151                                                    connection_error_msg =
1152                                                        format!("Connection failed: {}", e);
1153                                                }
1154                                                Ok(conn_id) => {
1155                                                    if !requested_link_id.is_empty() {
1156                                                        self.inner
1157                                                            .link_id_to_conn_id
1158                                                            .write()
1159                                                            .insert(
1160                                                                requested_link_id.clone(),
1161                                                                conn_id.1,
1162                                                            );
1163                                                    }
1164                                                    info!(
1165                                                        endpoint = %client_endpoint, "Successfully created connection",
1166                                                    );
1167                                                }
1168                                            }
1169                                        }
1170                                    }
1171                                }
1172
1173                                // Add connection status
1174                                connections_status.push(v1::ConnectionAck {
1175                                    link_id: conn.link_id.clone(),
1176                                    success: connection_success,
1177                                    error_msg: connection_error_msg,
1178                                });
1179                            }
1180
1181                            // Process routes to set
1182                            for route in &config.routes_to_set {
1183                                let mut route_success = true;
1184                                let mut route_error_msg = String::new();
1185
1186                                let conn = self.resolve_route_connection(route);
1187
1188                                if let Ok(Some(conn)) = conn {
1189                                    let name = route.name.clone().unwrap();
1190                                    let source = name.clone().with_id(NameId::NULL_COMPONENT);
1191
1192                                    let msg = DataPlaneMessage::builder()
1193                                        .source(source.clone())
1194                                        .destination(name.clone())
1195                                        .identity("")
1196                                        .flags(SlimHeaderFlags::default().with_recv_from(conn))
1197                                        .build_subscribe()
1198                                        .unwrap();
1199
1200                                    match self.send_subscribe_message_with_ack(msg).await {
1201                                        Ok(subscription_id) => {
1202                                            // Store the subscription_id for later unsubscription
1203                                            self.inner
1204                                                .route_subscription_ids
1205                                                .lock()
1206                                                .insert((name.clone(), conn), subscription_id);
1207                                            info!(?route, "Successfully created route");
1208                                        }
1209                                        Err(err) => {
1210                                            route_success = false;
1211                                            route_error_msg =
1212                                                format!("Failed to subscribe: {}", err);
1213                                        }
1214                                    }
1215                                } else {
1216                                    route_success = false;
1217                                    route_error_msg = match conn {
1218                                        Ok(None) => {
1219                                            format!(
1220                                                "Connection with link_id {} not found",
1221                                                route.link_id.as_deref().unwrap_or("<none>")
1222                                            )
1223                                        }
1224                                        Err(err) => err,
1225                                        _ => "unknown connection lookup error".to_string(),
1226                                    };
1227                                }
1228
1229                                // Add route status
1230                                routes_status.push(v1::RouteAck {
1231                                    route: Some(route.clone()),
1232                                    success: route_success,
1233                                    error_msg: route_error_msg,
1234                                });
1235                            }
1236
1237                            // Process routes to delete
1238                            for route in &config.routes_to_delete {
1239                                let mut route_success = true;
1240                                let mut route_error_msg = String::new();
1241
1242                                let conn = self.resolve_route_connection(route);
1243
1244                                if let Ok(Some(conn)) = conn {
1245                                    let name = route.name.clone().unwrap();
1246                                    let source = name.clone().with_id(NameId::NULL_COMPONENT);
1247
1248                                    let msg = DataPlaneMessage::builder()
1249                                        .source(source.clone())
1250                                        .destination(name.clone())
1251                                        .identity("")
1252                                        .flags(SlimHeaderFlags::default().with_recv_from(conn))
1253                                        .build_unsubscribe()
1254                                        .unwrap();
1255
1256                                    let sub_id = self
1257                                        .inner
1258                                        .route_subscription_ids
1259                                        .lock()
1260                                        .remove(&(name.clone(), conn));
1261                                    let unsubscribe_result = match sub_id {
1262                                        Some(subscription_id) => {
1263                                            self.send_unsubscribe_message_with_ack(
1264                                                msg,
1265                                                subscription_id,
1266                                            )
1267                                            .await
1268                                        }
1269                                        None => {
1270                                            // No tracking ID in the map (e.g. after a CP restart
1271                                            // or for orphan cleanup from the reconciler).  Generate
1272                                            // a fresh ID — the datapath locates the subscription
1273                                            // by Name+connection, not by ID; the ID is only used
1274                                            // for ack correlation.
1275                                            let (ack_id, ack_rx) =
1276                                                self.inner.subscription_manager.register_ack();
1277                                            let mut fresh_msg = msg;
1278                                            fresh_msg.set_subscription_id(ack_id);
1279                                            if let Err(e) =
1280                                                self.send_control_message(fresh_msg).await
1281                                            {
1282                                                self.inner.subscription_manager.cancel_ack(ack_id);
1283                                                Err(format!("datapath send error: {}", e.chain()))
1284                                            } else {
1285                                                match tokio::time::timeout(
1286                                                    SUBSCRIPTION_ACK_TIMEOUT,
1287                                                    ack_rx,
1288                                                )
1289                                                .await
1290                                                {
1291                                                    Ok(Ok(Ok(()))) => Ok(()),
1292                                                    Ok(Ok(Err(err))) => Err(err.to_string()),
1293                                                    Ok(Err(_)) => {
1294                                                        Err("subscription ack channel closed"
1295                                                            .to_string())
1296                                                    }
1297                                                    Err(_) => {
1298                                                        self.inner
1299                                                            .subscription_manager
1300                                                            .cancel_ack(ack_id);
1301                                                        Err("subscription ack timed out"
1302                                                            .to_string())
1303                                                    }
1304                                                }
1305                                            }
1306                                        }
1307                                    };
1308                                    if let Err(err) = unsubscribe_result {
1309                                        route_success = false;
1310                                        route_error_msg = format!("Failed to unsubscribe: {}", err);
1311                                    } else {
1312                                        info!(?route, "Successfully deleted route");
1313                                    }
1314                                } else {
1315                                    route_success = false;
1316                                    route_error_msg = match conn {
1317                                        Ok(None) => {
1318                                            format!(
1319                                                "Connection with link_id {} not found",
1320                                                route.link_id.as_deref().unwrap_or("<none>")
1321                                            )
1322                                        }
1323                                        Err(err) => err,
1324                                        _ => "unknown connection lookup error".to_string(),
1325                                    };
1326                                }
1327
1328                                // Add route status (for deletion)
1329                                routes_status.push(v1::RouteAck {
1330                                    route: Some(route.clone()),
1331                                    success: route_success,
1332                                    error_msg: route_error_msg,
1333                                });
1334                            }
1335
1336                            (connections_status, routes_status)
1337                        };
1338
1339                        let config_ack = v1::ConfigurationCommandAck {
1340                            original_message_id: msg.message_id.clone(),
1341                            connections_status,
1342                            routes_status,
1343                        };
1344
1345                        let reply = ControlMessage {
1346                            message_id: uuid::Uuid::new_v4().to_string(),
1347                            payload: Some(Payload::ConfigCommandAck(config_ack)),
1348                        };
1349
1350                        if let Err(e) = tx.send(Ok(reply)).await {
1351                            error!(error = %e.chain(), "failed to send ConfigurationCommandAck");
1352                        }
1353
1354                        info!(
1355                            connections = %config.connections_to_create.len(),
1356                            connections_to_delete = %config.connections_to_delete.len(),
1357                            routes_to_set = %config.routes_to_set.len(),
1358                            routes_to_del = %config.routes_to_delete.len(),
1359                            "Processed ConfigurationCommand"
1360                        );
1361                    }
1362                    Payload::RouteListRequest(_) => {
1363                        const CHUNK_SIZE: usize = 100;
1364
1365                        let conn_table = self.inner.message_processor.connection_table();
1366                        let mut entries = Vec::new();
1367
1368                        self.inner.message_processor.subscription_table().for_each(
1369                            |name, id, local, remote, peer, edge| {
1370                                let mut entry = RouteEntry {
1371                                    name: Some(name.clone().with_id(id)),
1372                                    ..Default::default()
1373                                };
1374
1375                                for &cid in local {
1376                                    entry.connections.push(ConnectionEntry {
1377                                        id: cid,
1378                                        connection_type: ConnectionType::Local as i32,
1379                                        config_data: "{}".to_string(),
1380                                        link_id: None,
1381                                        direction: ConnectionDirection::Outgoing as i32,
1382                                        peer_node_id: None,
1383                                    });
1384                                }
1385
1386                                let conn_slices = [
1387                                    (remote, ConnectionType::Remote),
1388                                    (peer, ConnectionType::Peer),
1389                                    (edge, ConnectionType::Edge),
1390                                ];
1391                                for (conns, ct) in conn_slices {
1392                                    for &cid in conns {
1393                                        if let Some(conn) = conn_table.get(cid) {
1394                                            entry.connections.push(ConnectionEntry {
1395                                                id: cid,
1396                                                connection_type: ct as i32,
1397                                                config_data: conn
1398                                                    .config_data()
1399                                                    .and_then(|d| serde_json::to_string(d).ok())
1400                                                    .unwrap_or_else(|| "{}".to_string()),
1401                                                link_id: conn.link_id(),
1402                                                direction: if conn.is_outgoing() {
1403                                                    ConnectionDirection::Outgoing as i32
1404                                                } else {
1405                                                    ConnectionDirection::Incoming as i32
1406                                                },
1407                                                peer_node_id: conn
1408                                                    .peer_node_id()
1409                                                    .map(str::to_string),
1410                                            });
1411                                        } else {
1412                                            error!(%cid, ?ct, "no connection entry for id");
1413                                        }
1414                                    }
1415                                }
1416                                entries.push(entry);
1417                            },
1418                        );
1419
1420                        let chunks: Vec<_> = entries.chunks(CHUNK_SIZE).collect();
1421                        if chunks.is_empty() {
1422                            let resp = ControlMessage {
1423                                message_id: uuid::Uuid::new_v4().to_string(),
1424                                payload: Some(Payload::RouteListResponse(RouteListResponse {
1425                                    original_message_id: msg.message_id.clone(),
1426                                    entries: vec![],
1427                                    done: true,
1428                                })),
1429                            };
1430                            if let Err(e) = tx.send(Ok(resp)).await {
1431                                error!(error = %e.chain(), "failed to send route list response");
1432                            }
1433                        } else {
1434                            let n = chunks.len();
1435                            for (i, chunk) in chunks.into_iter().enumerate() {
1436                                let resp = ControlMessage {
1437                                    message_id: uuid::Uuid::new_v4().to_string(),
1438                                    payload: Some(Payload::RouteListResponse(RouteListResponse {
1439                                        original_message_id: msg.message_id.clone(),
1440                                        entries: chunk.to_vec(),
1441                                        done: i + 1 == n,
1442                                    })),
1443                                };
1444                                if let Err(e) = tx.send(Ok(resp)).await {
1445                                    error!(error = %e.chain(), "failed to send route batch");
1446                                    break;
1447                                }
1448                            }
1449                        }
1450                    }
1451                    Payload::ConnectionListRequest(_) => {
1452                        let mut all_entries = Vec::new();
1453                        self.inner
1454                            .message_processor
1455                            .connection_table()
1456                            .for_each(|id, conn| {
1457                                let ct = match conn.connection_type() {
1458                                    slim_datapath::tables::ConnType::Local => ConnectionType::Local,
1459                                    slim_datapath::tables::ConnType::Remote => {
1460                                        ConnectionType::Remote
1461                                    }
1462                                    slim_datapath::tables::ConnType::Peer => ConnectionType::Peer,
1463                                    slim_datapath::tables::ConnType::Edge => ConnectionType::Edge,
1464                                };
1465                                all_entries.push(ConnectionEntry {
1466                                    id,
1467                                    connection_type: ct as i32,
1468                                    config_data: conn
1469                                        .config_data()
1470                                        .and_then(|d| serde_json::to_string(d).ok())
1471                                        .unwrap_or_else(|| "{}".to_string()),
1472                                    link_id: conn.link_id(),
1473                                    direction: if conn.is_outgoing() {
1474                                        ConnectionDirection::Outgoing as i32
1475                                    } else {
1476                                        ConnectionDirection::Incoming as i32
1477                                    },
1478                                    peer_node_id: conn.peer_node_id().map(str::to_string),
1479                                });
1480                            });
1481
1482                        const CHUNK_SIZE: usize = 100;
1483                        let chunks: Vec<_> = all_entries.chunks(CHUNK_SIZE).collect();
1484                        if chunks.is_empty() {
1485                            let resp = ControlMessage {
1486                                message_id: uuid::Uuid::new_v4().to_string(),
1487                                payload: Some(Payload::ConnectionListResponse(
1488                                    ConnectionListResponse {
1489                                        original_message_id: msg.message_id.clone(),
1490                                        entries: vec![],
1491                                        done: true,
1492                                    },
1493                                )),
1494                            };
1495                            if let Err(e) = tx.send(Ok(resp)).await {
1496                                error!(error = %e.chain(), "failed to send connection list response");
1497                            }
1498                        } else {
1499                            let n = chunks.len();
1500                            for (i, chunk) in chunks.into_iter().enumerate() {
1501                                let resp = ControlMessage {
1502                                    message_id: uuid::Uuid::new_v4().to_string(),
1503                                    payload: Some(Payload::ConnectionListResponse(
1504                                        ConnectionListResponse {
1505                                            original_message_id: msg.message_id.clone(),
1506                                            entries: chunk.to_vec(),
1507                                            done: i + 1 == n,
1508                                        },
1509                                    )),
1510                                };
1511                                if let Err(e) = tx.send(Ok(resp)).await {
1512                                    error!(error = %e.chain(), "failed to send connection list batch");
1513                                    break;
1514                                }
1515                            }
1516                        }
1517                    }
1518                    Payload::RegisterNodeRequest(_) => {
1519                        error!("received a register node request");
1520                    }
1521                    Payload::DeregisterNodeRequest(_) => {
1522                        error!("received a deregister node request");
1523                    }
1524                    _ => {
1525                        // received unsupported message type, do nothing
1526                        debug!("received unsupported message type from control - ignoring");
1527                    }
1528                }
1529            }
1530            None => {
1531                error!(
1532                    message_id = %msg.message_id,
1533                    "received control message with no payload",
1534                );
1535            }
1536        }
1537
1538        Ok(())
1539    }
1540
1541    async fn handle_subscribe_message(&self, dst: ProtoName, clients: &[ClientConfig]) {
1542        let mut sub_vec = vec![];
1543
1544        let cmd = v1::Route {
1545            name: Some(dst),
1546            link_id: None,
1547            direction: None,
1548        };
1549
1550        debug!(
1551            "handle_subscribe_message: sending route_to_set to control plane: {:?}",
1552            cmd
1553        );
1554        sub_vec.push(cmd);
1555
1556        let ctrl = ControlMessage {
1557            message_id: uuid::Uuid::new_v4().to_string(),
1558            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
1559                connections_to_create: vec![],
1560                connections_to_delete: vec![],
1561                routes_to_set: sub_vec,
1562                routes_to_delete: vec![],
1563                reconcile: false,
1564                connections_received: vec![],
1565            })),
1566        };
1567
1568        return self.send_or_queue_notification(ctrl, clients).await;
1569    }
1570
1571    /// Called when the data plane completes a link negotiation for a remote
1572    /// incoming connection. Notifies the control-plane so it can claim the link.
1573    async fn handle_link_received(
1574        &self,
1575        link: &slim_datapath::api::ProtoLink,
1576        clients: &[ClientConfig],
1577    ) {
1578        use slim_datapath::api::ProtoLinkType;
1579
1580        let link_id = match &link.link_type {
1581            Some(ProtoLinkType::LinkNegotiation(payload)) => &payload.link_id,
1582            _ => {
1583                debug!("handle_link_received: ignoring link message without negotiation payload");
1584                return;
1585            }
1586        };
1587
1588        if link_id.is_empty() {
1589            debug!("handle_link_received: ignoring link message with empty link_id");
1590            return;
1591        }
1592
1593        debug!(
1594            "handle_link_received: notifying control-plane of new link_id: {}",
1595            link_id
1596        );
1597
1598        let ctrl = ControlMessage {
1599            message_id: uuid::Uuid::new_v4().to_string(),
1600            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
1601                connections_to_create: vec![],
1602                connections_to_delete: vec![],
1603                routes_to_set: vec![],
1604                routes_to_delete: vec![],
1605                reconcile: false,
1606                connections_received: vec![v1::ConnectionEntry {
1607                    id: 0,
1608                    connection_type: ConnectionType::Remote as i32,
1609                    config_data: String::new(),
1610                    link_id: Some(link_id.clone()),
1611                    direction: ConnectionDirection::Incoming as i32,
1612                    peer_node_id: None,
1613                }],
1614            })),
1615        };
1616
1617        self.send_or_queue_notification(ctrl, clients).await;
1618    }
1619
1620    async fn handle_unsubscribe_message(&self, dst: ProtoName, clients: &[ClientConfig]) {
1621        // Remove the subscription from our tracking map so the reconciler
1622        // knows it needs to re-install it if the route is still desired.
1623        // Without this, a reused conn_id would cause create_new_subscriptions
1624        // to skip re-creation because it thinks the subscription is still active.
1625        self.inner
1626            .route_subscription_ids
1627            .lock()
1628            .retain(|(name, _), _| *name != dst);
1629
1630        let mut unsub_vec = vec![];
1631
1632        let cmd = v1::Route {
1633            name: Some(dst),
1634            link_id: None,
1635            direction: None,
1636        };
1637
1638        debug!(
1639            "handle_unsubscribe_message: sending route_to_delete to control plane: {:?}",
1640            cmd
1641        );
1642        unsub_vec.push(cmd);
1643
1644        let ctrl = ControlMessage {
1645            message_id: uuid::Uuid::new_v4().to_string(),
1646            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
1647                connections_to_create: vec![],
1648                connections_to_delete: vec![],
1649                routes_to_set: vec![],
1650                routes_to_delete: unsub_vec,
1651                reconcile: false,
1652                connections_received: vec![],
1653            })),
1654        };
1655
1656        return self.send_or_queue_notification(ctrl, clients).await;
1657    }
1658
1659    /// Send a subscribe message and await the ack. Returns the subscription_id.
1660    async fn send_subscribe_message_with_ack(
1661        &self,
1662        mut msg: DataPlaneMessage,
1663    ) -> Result<u64, String> {
1664        let (ack_id, ack_rx) = self.inner.subscription_manager.register_ack();
1665        msg.set_subscription_id(ack_id);
1666
1667        if let Err(e) = self.send_control_message(msg).await {
1668            self.inner.subscription_manager.cancel_ack(ack_id);
1669            return Err(format!("datapath send error: {}", e.chain()));
1670        }
1671
1672        match tokio::time::timeout(SUBSCRIPTION_ACK_TIMEOUT, ack_rx).await {
1673            Ok(Ok(Ok(()))) => Ok(ack_id),
1674            Ok(Ok(Err(err))) => Err(err.to_string()),
1675            Ok(Err(_)) => Err("subscription ack channel closed".to_string()),
1676            Err(_) => {
1677                self.inner.subscription_manager.cancel_ack(ack_id);
1678                Err("subscription ack timed out".to_string())
1679            }
1680        }
1681    }
1682
1683    /// Send an unsubscribe message with a given subscription_id and await the ack.
1684    async fn send_unsubscribe_message_with_ack(
1685        &self,
1686        mut msg: DataPlaneMessage,
1687        subscription_id: u64,
1688    ) -> Result<(), String> {
1689        let ack_rx = self
1690            .inner
1691            .subscription_manager
1692            .register_ack_with_id(subscription_id);
1693        msg.set_subscription_id(subscription_id);
1694
1695        if let Err(e) = self.send_control_message(msg).await {
1696            self.inner.subscription_manager.cancel_ack(subscription_id);
1697            return Err(format!("datapath send error: {}", e.chain()));
1698        }
1699
1700        match tokio::time::timeout(SUBSCRIPTION_ACK_TIMEOUT, ack_rx).await {
1701            Ok(Ok(Ok(()))) => Ok(()),
1702            Ok(Ok(Err(err))) => Err(err.to_string()),
1703            Ok(Err(_)) => Err("subscription ack channel closed".to_string()),
1704            Err(_) => {
1705                self.inner.subscription_manager.cancel_ack(subscription_id);
1706                Err("subscription ack timed out".to_string())
1707            }
1708        }
1709    }
1710
1711    /// Send a control message to SLIM.
1712    async fn send_control_message(&self, msg: DataPlaneMessage) -> Result<(), ControllerError> {
1713        self.inner.tx_slim.send(Ok(msg)).await.map_err(|e| {
1714            error!(error = %e.chain(), "error sending message into datapath");
1715            ControllerError::Datapath(slim_datapath::errors::DataPathError::ConnectionError)
1716        })
1717    }
1718
1719    /// Send notification to control plane or queue it if no connection is available.
1720    ///
1721    /// Uses `try_send` to avoid blocking the caller when the bounded channel is
1722    /// full (e.g. due to HTTP/2 back-pressure).  Blocking here would stall the
1723    /// data-plane listener task, preventing it from resolving subscription acks
1724    /// that the control-message handler is waiting on — a deadlock.
1725    async fn send_or_queue_notification(&self, ctrl_msg: ControlMessage, clients: &[ClientConfig]) {
1726        let mut sent = false;
1727
1728        for c in clients {
1729            let tx = match self.inner.tx_channels.read().get(&c.endpoint) {
1730                Some(tx) => tx.clone(),
1731                None => continue,
1732            };
1733
1734            match tx.try_send(Ok(ctrl_msg.clone())) {
1735                Ok(()) => {
1736                    sent = true;
1737                }
1738                Err(mpsc::error::TrySendError::Full(_)) => {
1739                    debug!(
1740                        endpoint = %c.endpoint,
1741                        "channel full, queuing notification instead of blocking"
1742                    );
1743                }
1744                Err(mpsc::error::TrySendError::Closed(_)) => {
1745                    debug!(
1746                        endpoint = %c.endpoint,
1747                        "channel closed, queuing notification"
1748                    );
1749                }
1750            }
1751        }
1752
1753        if !sent {
1754            let mut queue = self.inner.pending_notifications.lock();
1755            if queue.len() >= MAX_QUEUED_NOTIFICATIONS {
1756                queue.pop_front();
1757                debug!("queue full, removed oldest notification");
1758            }
1759            queue.push_back(ctrl_msg);
1760        }
1761    }
1762
1763    /// Get a drain watch clone to pass to a task
1764    fn drain_watch(&self) -> Result<drain::Watch, ControllerError> {
1765        self.inner
1766            .drain_watch
1767            .read()
1768            .clone()
1769            .ok_or(ControllerError::AlreadyStopped)
1770    }
1771
1772    /// Send all queued subscription notifications when connection is restored.
1773    async fn send_queued_notifications(
1774        &self,
1775        tx: &mpsc::Sender<Result<ControlMessage, Status>>,
1776        endpoint: &str,
1777    ) {
1778        let notifications = {
1779            let mut queue = self.inner.pending_notifications.lock();
1780            if queue.is_empty() {
1781                return;
1782            }
1783            queue.drain(..).collect::<Vec<_>>()
1784        };
1785
1786        if notifications.is_empty() {
1787            return;
1788        }
1789
1790        debug!(
1791            "sending {} queued subscription notifications to {}",
1792            notifications.len(),
1793            endpoint
1794        );
1795
1796        let mut failed_notifications = Vec::new();
1797        for notification in notifications {
1798            if let Err(e) = tx.send(Ok(notification)).await {
1799                error!(
1800                    error = %e.chain(),
1801                    %endpoint,
1802                    "failed to send queued notification to control plane",
1803                );
1804
1805                // we can unwrap here because we know we sent a Ok(ControlMessage)
1806                failed_notifications.push(e.0.unwrap());
1807            }
1808        }
1809
1810        // Re-queue any failed notifications
1811        if !failed_notifications.is_empty() {
1812            self.inner
1813                .pending_notifications
1814                .lock()
1815                .extend(failed_notifications);
1816        }
1817    }
1818
1819    /// Replay all locally-registered agent subscriptions to the control plane.
1820    /// Called after (re)connecting so the CP can recreate wildcard route templates.
1821    async fn replay_local_subscriptions(&self, clients: &[ClientConfig]) {
1822        let mut routes: Vec<v1::Route> = Vec::new();
1823        self.inner.message_processor.subscription_table().for_each(
1824            |name, id, local, _remote, _peer, edge| {
1825                if local.is_empty() && edge.is_empty() {
1826                    return;
1827                }
1828                routes.push(v1::Route {
1829                    name: Some(name.clone().with_id(id)),
1830                    link_id: None,
1831                    direction: None,
1832                });
1833            },
1834        );
1835
1836        if routes.is_empty() {
1837            return;
1838        }
1839
1840        info!(
1841            count = routes.len(),
1842            "replaying local subscriptions to control plane"
1843        );
1844
1845        let ctrl = ControlMessage {
1846            message_id: uuid::Uuid::new_v4().to_string(),
1847            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
1848                connections_to_create: vec![],
1849                connections_to_delete: vec![],
1850                routes_to_set: routes,
1851                routes_to_delete: vec![],
1852                reconcile: false,
1853                connections_received: vec![],
1854            })),
1855        };
1856
1857        self.send_or_queue_notification(ctrl, clients).await;
1858    }
1859
1860    /// Process the control message stream.
1861    fn process_control_message_stream(
1862        &self,
1863        config: Option<ClientConfig>,
1864        mut stream: impl Stream<Item = Result<ControlMessage, Status>> + Unpin + Send + 'static,
1865        tx: mpsc::Sender<Result<ControlMessage, Status>>,
1866        cancellation_token: CancellationToken,
1867    ) -> Result<JoinHandle<()>, ControllerError> {
1868        let this = self.clone();
1869        let watch = self.drain_watch()?;
1870
1871        let handle = tokio::spawn(async move {
1872            // Send a register message to the control plane
1873            let endpoint = config
1874                .as_ref()
1875                .map(|c| c.endpoint.clone())
1876                .unwrap_or_else(|| "unknown".to_string());
1877            info!(%endpoint, "connected to control plane");
1878
1879            let mut retry_connect = false;
1880
1881            let mut active_connections = Vec::new();
1882            this.inner
1883                .message_processor
1884                .connection_table()
1885                .for_each(|id, conn| {
1886                    active_connections.push(v1::ConnectionEntry {
1887                        id,
1888                        connection_type: v1::ConnectionType::Remote as i32,
1889                        config_data: match conn.config_data() {
1890                            Some(data) => {
1891                                serde_json::to_string(data).unwrap_or_else(|_| "{}".to_string())
1892                            }
1893                            None => "{}".to_string(),
1894                        },
1895                        link_id: conn.link_id(),
1896                        direction: if conn.is_outgoing() {
1897                            v1::ConnectionDirection::Outgoing as i32
1898                        } else {
1899                            v1::ConnectionDirection::Incoming as i32
1900                        },
1901                        peer_node_id: conn.peer_node_id().map(str::to_string),
1902                    });
1903                });
1904
1905            let active_routes = {
1906                let conn_id_to_link_id: HashMap<u64, String> = this
1907                    .inner
1908                    .link_id_to_conn_id
1909                    .read()
1910                    .iter()
1911                    .map(|(lid, cid)| (*cid, lid.clone()))
1912                    .collect();
1913                this.inner
1914                    .route_subscription_ids
1915                    .lock()
1916                    .iter()
1917                    .map(|((name, conn_id), _sub_id)| v1::Route {
1918                        name: Some(name.clone()),
1919                        link_id: conn_id_to_link_id.get(conn_id).cloned(),
1920                        direction: None,
1921                    })
1922                    .collect::<Vec<_>>()
1923            };
1924
1925            let max_attempts = 10;
1926            let mut credentials = None;
1927            let mut i = 0;
1928            while i < max_attempts && credentials.is_none() {
1929                credentials = match &this.inner.auth_provider {
1930                    Some(provider) => match provider.get_token() {
1931                        Ok(token) => Some(token),
1932                        Err(e) => {
1933                            info!(error = %e, attempt = i + 1, max_attempts, "failed to get auth credentials, will retry");
1934                            tokio::time::sleep(Duration::from_secs(2)).await;
1935                            None
1936                        }
1937                    },
1938                    None => Some(String::new()),
1939                };
1940                i += 1;
1941            }
1942
1943            if credentials.is_none() {
1944                error!(
1945                    attempts = max_attempts,
1946                    "failed to obtain auth credentials, aborting registration"
1947                );
1948                return;
1949            }
1950
1951            let register_request = ControlMessage {
1952                message_id: uuid::Uuid::new_v4().to_string(),
1953                payload: Some(Payload::RegisterNodeRequest(v1::RegisterNodeRequest {
1954                    node_id: this.inner.id.clone(),
1955                    domain_name: this.inner.domain_name.clone(),
1956                    connection_details: this.inner.connection_details.clone(),
1957                    connections: active_connections,
1958                    routes: active_routes,
1959                    credentials: credentials.unwrap(),
1960                })),
1961            };
1962
1963            // send register request if client
1964            if config.is_some()
1965                && let Err(e) = tx.send(Ok(register_request)).await
1966            {
1967                error!(error = %e.chain(), "failed to send register request");
1968                return;
1969            }
1970
1971            // Send queued notifications after the register request so that
1972            // RegisterNodeRequest is always the first message on the stream.
1973            this.send_queued_notifications(&tx, &endpoint).await;
1974
1975            // Wait for RegisterNodeResponse from control plane before processing commands.
1976            if config.is_some() {
1977                let registration_result = tokio::time::timeout(
1978                    Duration::from_secs(10),
1979                    async {
1980                        loop {
1981                            tokio::select! {
1982                                next = stream.next() => {
1983                                    match next {
1984                                        Some(Ok(msg)) => {
1985                                            if let Some(Payload::RegisterNodeResponse(resp)) = msg.payload {
1986                                                if resp.success {
1987                                                    info!(
1988                                                        connections = resp.connections.len(),
1989                                                        routes = resp.routes.len(),
1990                                                        "registration acknowledged by control plane"
1991                                                    );
1992                                                    return Ok(resp);
1993                                                } else {
1994                                                    return Err("registration rejected by control plane".to_string());
1995                                                }
1996                                            }
1997                                        }
1998                                        Some(Err(_)) => return Err("stream error waiting for registration response".to_string()),
1999                                        None => return Err("stream closed before registration response".to_string()),
2000                                    }
2001                                }
2002                                _ = cancellation_token.cancelled() => {
2003                                    return Err("cancelled while waiting for registration response".to_string());
2004                                }
2005                            }
2006                        }
2007                    }
2008                ).await;
2009
2010                match registration_result {
2011                    Ok(Ok(resp)) => {
2012                        // Apply initial desired state from the control plane.
2013                        if !resp.connections.is_empty() || !resp.routes.is_empty() {
2014                            let init_cmd = ControlMessage {
2015                                message_id: uuid::Uuid::new_v4().to_string(),
2016                                payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2017                                    connections_to_create: resp.connections,
2018                                    routes_to_set: resp.routes,
2019                                    routes_to_delete: vec![],
2020                                    connections_to_delete: vec![],
2021                                    reconcile: true,
2022                                    connections_received: vec![],
2023                                })),
2024                            };
2025                            if let Err(e) = this.handle_new_control_message(init_cmd, &tx).await {
2026                                error!(error = %e.chain(), "failed to apply initial state from registration");
2027                            }
2028                        }
2029                    }
2030                    Ok(Err(e)) => {
2031                        error!(%e, "registration handshake failed");
2032                        return;
2033                    }
2034                    Err(_) => {
2035                        error!("registration handshake timed out");
2036                        return;
2037                    }
2038                }
2039            }
2040
2041            // Replay local agent subscriptions so the CP recreates route templates.
2042            if let Some(ref cfg) = config {
2043                this.replay_local_subscriptions(std::slice::from_ref(cfg))
2044                    .await;
2045            }
2046
2047            let mut drain_fut = std::pin::pin!(watch.clone().signaled());
2048
2049            loop {
2050                tokio::select! {
2051                    next = stream.next() => {
2052                        match next {
2053                            Some(Ok(msg)) => {
2054                                if let Err(e) = this.handle_new_control_message(msg, &tx).await {
2055                                    error!(error = %e.chain(), "error processing incoming control message");
2056                                }
2057                            }
2058                            Some(Err(e)) => {
2059                                if let Some(io_err) = Self::match_for_io_error(&e) {
2060                                    if io_err.kind() == std::io::ErrorKind::BrokenPipe {
2061                                        info!("connection closed by peer");
2062                                    } else {
2063                                        // Handle other IO errors (ConnectionAborted, etc.)
2064                                        error!(
2065                                            error = %e.chain(),
2066                                            io_error_kind = ?io_err.kind(),
2067                                            "IO error receiving control messages"
2068                                        );
2069                                    }
2070                                } else {
2071                                    // Handle non-IO errors (e.g., gRPC Canceled, Unavailable, etc.)
2072                                    error!(error = %e.chain(), "error receiving control messages");
2073                                }
2074
2075                                retry_connect = true;
2076                                break;
2077                            }
2078                            None => {
2079                                debug!("end of stream");
2080                                retry_connect = true;
2081                                break;
2082                            }
2083                        }
2084                    }
2085                    _ = cancellation_token.cancelled() => {
2086                        debug!("shutting down stream on cancellation token");
2087                        break;
2088                    }
2089                    _ = &mut drain_fut => {
2090                        debug!("shutting down stream on drain");
2091                        break;
2092                    }
2093                }
2094            }
2095
2096            info!(%endpoint, "control plane stream closed");
2097
2098            if retry_connect && let Some(config) = config {
2099                info!(%config.endpoint, "retrying connection to control plane");
2100                this.connect(config.clone(), cancellation_token)
2101                    .await
2102                    .map_or_else(
2103                        |e| {
2104                            error!(error = %e.chain(), "failed to reconnect to control plane");
2105                        },
2106                        |tx| {
2107                            info!(%config.endpoint, "reconnected to control plane");
2108
2109                            this.inner
2110                                .tx_channels
2111                                .write()
2112                                .insert(config.endpoint.clone(), tx);
2113                        },
2114                    )
2115            }
2116        });
2117
2118        Ok(handle)
2119    }
2120
2121    /// Connect to the control plane using the provided client configuration.
2122    /// This function attempts to establish a connection to the control plane and returns a sender for control messages.
2123    /// It retries the connection a specified number of times if it fails.
2124    async fn connect(
2125        &self,
2126        config: ClientConfig,
2127        cancellation_token: CancellationToken,
2128    ) -> Result<mpsc::Sender<Result<ControlMessage, Status>>, ControllerError> {
2129        info!(%config.endpoint, "connecting to control plane");
2130
2131        // Disconnect all outgoing remote connections — the CP will create
2132        // fresh links after re-registration and nodes will reconnect.
2133        // This prevents stale connections from causing duplicate links.
2134        let mut remote_conn_ids: Vec<u64> = Vec::new();
2135        self.inner
2136            .message_processor
2137            .connection_table()
2138            .for_each(|id, conn| {
2139                if conn.is_outgoing() && matches!(conn.connection_type(), ConnType::Remote) {
2140                    remote_conn_ids.push(id);
2141                }
2142            });
2143        for conn_id in &remote_conn_ids {
2144            if let Err(e) = self.inner.message_processor.disconnect(*conn_id) {
2145                debug!(conn_id, error = %e.chain(), "failed to disconnect remote connection");
2146            }
2147        }
2148        if !remote_conn_ids.is_empty() {
2149            info!(
2150                count = remote_conn_ids.len(),
2151                "disconnected remote connections for clean restart"
2152            );
2153        }
2154
2155        // Reset connection state before establishing a new session. The CP
2156        // will send fresh ConfigCommands after re-registration to rebuild
2157        // these maps.
2158        self.inner.route_subscription_ids.lock().clear();
2159        self.inner.link_id_to_conn_id.write().clear();
2160
2161        // Obtain drain watch to handle graceful shutdown during connection
2162        let watch = self.drain_watch()?;
2163
2164        let connect_fut = async {
2165            let channel = match config.to_channel().await? {
2166                TransportChannel::Grpc(c) => c,
2167                TransportChannel::Websocket(_) => {
2168                    return Err(ControllerError::ConfigError(
2169                        slim_config::errors::ConfigError::GrpcChannelUnsupportedTransport,
2170                    ));
2171                }
2172            };
2173
2174            let mut client = ControllerServiceClient::new(channel.clone());
2175            let (tx, rx) = mpsc::channel::<Result<ControlMessage, Status>>(128);
2176            let out_stream = ReceiverStream::new(rx).filter_map(|res| match res {
2177                Ok(msg) => Some(msg),
2178                Err(e) => {
2179                    error!(error = %e, "dropping outbound control message due to error");
2180                    None
2181                }
2182            });
2183            let stream = client
2184                .open_control_channel(Request::new(out_stream))
2185                .await?;
2186            Ok((tx, stream))
2187        };
2188
2189        let (tx, stream) = tokio::select! {
2190            result = connect_fut => { result? }
2191            _ = cancellation_token.cancelled() => {
2192                debug!("connection cancelled during setup");
2193                return Err(ControllerError::Canceled);
2194            }
2195            _ = watch.signaled() => {
2196                debug!("drain signal received during connection setup");
2197                return Err(ControllerError::Canceled);
2198            }
2199        };
2200
2201        // start processing the incoming stream
2202        let endpoint_key = config.endpoint.clone();
2203        let handle = self.process_control_message_stream(
2204            Some(config),
2205            stream.into_inner(),
2206            tx.clone(),
2207            cancellation_token.clone(),
2208        )?;
2209        self.inner
2210            .stream_handles
2211            .lock()
2212            .insert(endpoint_key, handle);
2213
2214        // return the sender
2215        Ok(tx)
2216    }
2217
2218    fn match_for_io_error(err_status: &Status) -> Option<&std::io::Error> {
2219        let mut err: &(dyn std::error::Error + 'static) = err_status;
2220
2221        loop {
2222            if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
2223                return Some(io_err);
2224            }
2225
2226            // h2::Error do not expose std::io::Error with `source()`
2227            // https://github.com/hyperium/h2/pull/462
2228            if let Some(h2_err) = err.downcast_ref::<h2::Error>()
2229                && let Some(io_err) = h2_err.get_io()
2230            {
2231                return Some(io_err);
2232            }
2233
2234            err = err.source()?;
2235        }
2236    }
2237}
2238
2239#[tonic::async_trait]
2240impl GrpcControllerService for ControllerService {
2241    type OpenControlChannelStream =
2242        Pin<Box<dyn Stream<Item = Result<ControlMessage, Status>> + Send + 'static>>;
2243
2244    async fn open_control_channel(
2245        &self,
2246        request: Request<tonic::Streaming<ControlMessage>>,
2247    ) -> Result<Response<Self::OpenControlChannelStream>, Status> {
2248        // Get the remote endpoint from the request metadata
2249        let remote_endpoint = request
2250            .remote_addr()
2251            .map(|addr| addr.to_string())
2252            .unwrap_or_else(|| "unknown".to_string());
2253
2254        let stream = request.into_inner();
2255        let (tx, rx) = mpsc::channel::<Result<ControlMessage, Status>>(128);
2256
2257        let cancellation_token = CancellationToken::new();
2258
2259        // Server-side connections don't initiate operations requiring acks, so no timer channel needed
2260        let handle = self
2261            .process_control_message_stream(None, stream, tx.clone(), cancellation_token.clone())
2262            .map_err(|e| {
2263                error!(error = %e.chain(), "error processing control message stream");
2264                Status::unavailable("failed to process control message stream")
2265            })?;
2266        self.inner
2267            .stream_handles
2268            .lock()
2269            .insert(remote_endpoint.clone(), handle);
2270
2271        self.inner
2272            .tx_channels
2273            .write()
2274            .insert(remote_endpoint.clone(), tx);
2275
2276        if let Some(old_token) = self
2277            .inner
2278            .cancellation_tokens
2279            .write()
2280            .insert(remote_endpoint.clone(), cancellation_token)
2281        {
2282            old_token.cancel();
2283        }
2284
2285        let out_stream = ReceiverStream::new(rx);
2286        Ok(Response::new(
2287            Box::pin(out_stream) as Self::OpenControlChannelStream
2288        ))
2289    }
2290}
2291
2292#[cfg(test)]
2293mod tests {
2294    use super::*;
2295    use tracing_test::traced_test;
2296
2297    async fn setup_control_planes(
2298        server_endpoint: &str,
2299        server_name: &str,
2300        client_name: &str,
2301    ) -> (ControlPlane, ControlPlane, ClientConfig) {
2302        let server_config = ServerConfig::with_endpoint(server_endpoint)
2303            .with_tls_settings(slim_config::tls::server::TlsServerConfig::insecure());
2304        let client_config = ClientConfig::with_endpoint(&format!("http://{}", server_endpoint))
2305            .with_tls_setting(slim_config::tls::client::TlsClientConfig::insecure());
2306
2307        let message_processor_server = MessageProcessor::new();
2308        let message_processor_client = MessageProcessor::new();
2309
2310        let control_plane_server = ControlPlane::new(ControlPlaneSettings {
2311            id: server_name.to_string(),
2312            domain_name: None,
2313            servers: vec![server_config.clone()],
2314            clients: vec![],
2315            outbound_clients: vec![],
2316            dataplane_clients: vec![],
2317            message_processor: message_processor_server,
2318            connection_details: vec![from_server_config(&server_config)],
2319            auth_provider: None,
2320        });
2321
2322        let control_plane_client = ControlPlane::new(ControlPlaneSettings {
2323            id: client_name.to_string(),
2324            domain_name: None,
2325            servers: vec![],
2326            clients: vec![client_config.clone()],
2327            outbound_clients: vec![],
2328            dataplane_clients: vec![],
2329            message_processor: message_processor_client,
2330            connection_details: vec![],
2331            auth_provider: None,
2332        });
2333
2334        (control_plane_server, control_plane_client, client_config)
2335    }
2336
2337    #[tokio::test]
2338    #[traced_test]
2339    async fn test_end_to_end() {
2340        let (mut control_plane_server, mut control_plane_client, _client_cfg) =
2341            setup_control_planes(
2342                "127.0.0.1:50051",
2343                "test-server-instance",
2344                "test-client-instance",
2345            )
2346            .await;
2347
2348        control_plane_server.run().await.unwrap();
2349        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2350        control_plane_client.run().await.unwrap();
2351        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2352
2353        assert!(logs_contain("received a register node request"));
2354    }
2355
2356    #[tokio::test]
2357    #[traced_test]
2358    async fn test_subscription_notification_queue_drain() {
2359        // Reuse common setup with a different port to avoid clash with other tests.
2360        let (mut control_plane_server, mut control_plane_client, client_config) =
2361            setup_control_planes(
2362                "127.0.0.1:50061",
2363                "queue-drain-server",
2364                "queue-drain-client",
2365            )
2366            .await;
2367
2368        let controller = control_plane_client.controller.clone();
2369        assert_eq!(controller.inner.pending_notifications.lock().len(), 0);
2370
2371        const N: usize = 5;
2372        for i in 0..N {
2373            let ctrl_msg = ControlMessage {
2374                message_id: uuid::Uuid::new_v4().to_string(),
2375                payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2376                    connections_to_create: vec![],
2377                    connections_to_delete: vec![],
2378                    routes_to_set: vec![v1::Route {
2379                        name: Some(
2380                            ProtoName::from_strings(["queued", "sub", &format!("name-{i}")])
2381                                .with_id(i as u128),
2382                        ),
2383                        link_id: None,
2384                        direction: None,
2385                    }],
2386                    routes_to_delete: vec![],
2387                    reconcile: false,
2388                    connections_received: vec![],
2389                })),
2390            };
2391            controller
2392                .send_or_queue_notification(ctrl_msg, std::slice::from_ref(&client_config))
2393                .await;
2394        }
2395        assert_eq!(controller.inner.pending_notifications.lock().len(), N);
2396
2397        control_plane_server.run().await.expect("server run failed");
2398        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2399        control_plane_client.run().await.expect("client run failed");
2400        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
2401
2402        assert_eq!(controller.inner.pending_notifications.lock().len(), 0);
2403        assert!(
2404            logs_contain(&format!("sending {} queued subscription notifications", N)),
2405            "Expected log about sending queued subscription notifications"
2406        );
2407
2408        drop(controller);
2409        drop(control_plane_server);
2410        drop(control_plane_client);
2411    }
2412
2413    #[tokio::test]
2414    #[traced_test]
2415    async fn test_delete_connection_by_link_id_success_ack() {
2416        let (mut control_plane_server, mut control_plane_client, _client_cfg) =
2417            setup_control_planes(
2418                "127.0.0.1:50081",
2419                "delete-linkid-server",
2420                "delete-linkid-client",
2421            )
2422            .await;
2423
2424        control_plane_server.run().await.unwrap();
2425        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2426        control_plane_client.run().await.unwrap();
2427        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2428
2429        let controller = control_plane_client.controller.clone();
2430        let link_id = "test-delete-link-id".to_string();
2431
2432        // Insert a pre-negotiated remote connection with a known link_id for testing.
2433        let (tx, _rx) = tokio::sync::mpsc::channel(16);
2434        let conn = slim_datapath::connection::Connection::new(
2435            slim_datapath::tables::ConnType::Remote,
2436            slim_datapath::connection::Channel::Server(tx),
2437        )
2438        .with_negotiation(&link_id, "1.0.0");
2439        controller
2440            .inner
2441            .message_processor
2442            .forwarder()
2443            .on_connection_established(conn, None)
2444            .unwrap();
2445
2446        let ctrl_msg = ControlMessage {
2447            message_id: uuid::Uuid::new_v4().to_string(),
2448            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2449                connections_to_create: vec![],
2450                connections_to_delete: vec![link_id.clone()],
2451                routes_to_set: vec![],
2452                routes_to_delete: vec![],
2453                reconcile: false,
2454                connections_received: vec![],
2455            })),
2456        };
2457        let (tx, mut rx) = mpsc::channel(1);
2458        controller
2459            .handle_new_control_message(ctrl_msg, &tx)
2460            .await
2461            .expect("config command must be handled");
2462
2463        let ack_msg = rx
2464            .recv()
2465            .await
2466            .expect("expected ack message")
2467            .expect("ack should be ok");
2468        let ack = match ack_msg.payload {
2469            Some(Payload::ConfigCommandAck(ack)) => ack,
2470            _ => panic!("expected ConfigCommandAck payload"),
2471        };
2472        assert_eq!(ack.connections_status.len(), 1);
2473        assert_eq!(ack.connections_status[0].link_id, link_id);
2474        assert!(ack.connections_status[0].success);
2475    }
2476
2477    #[tokio::test]
2478    #[traced_test]
2479    async fn test_delete_connection_by_link_id_unknown_fails_ack() {
2480        let (control_plane_server, control_plane_client, _client_cfg) = setup_control_planes(
2481            "127.0.0.1:50082",
2482            "delete-linkid-server-unknown",
2483            "delete-linkid-client-unknown",
2484        )
2485        .await;
2486
2487        let controller = control_plane_client.controller.clone();
2488        let ctrl_msg = ControlMessage {
2489            message_id: uuid::Uuid::new_v4().to_string(),
2490            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2491                connections_to_create: vec![],
2492                connections_to_delete: vec!["unknown-link-id".to_string()],
2493                routes_to_set: vec![],
2494                routes_to_delete: vec![],
2495                reconcile: false,
2496                connections_received: vec![],
2497            })),
2498        };
2499        let (tx, mut rx) = mpsc::channel(1);
2500        controller
2501            .handle_new_control_message(ctrl_msg, &tx)
2502            .await
2503            .expect("config command must be handled");
2504
2505        let ack_msg = rx
2506            .recv()
2507            .await
2508            .expect("expected ack message")
2509            .expect("ack should be ok");
2510        let ack = match ack_msg.payload {
2511            Some(Payload::ConfigCommandAck(ack)) => ack,
2512            _ => panic!("expected ConfigCommandAck payload"),
2513        };
2514        assert_eq!(ack.connections_status.len(), 1);
2515        assert_eq!(ack.connections_status[0].link_id, "unknown-link-id");
2516        assert!(!ack.connections_status[0].success);
2517        assert!(ack.connections_status[0].error_msg.contains("not found"));
2518
2519        drop(control_plane_server);
2520    }
2521
2522    #[tokio::test]
2523    #[traced_test]
2524    async fn test_create_connection_with_existing_link_id_reuses_connection_ack() {
2525        let (mut control_plane_server, mut control_plane_client, _client_cfg) =
2526            setup_control_planes(
2527                "127.0.0.1:50083",
2528                "create-linkid-server",
2529                "create-linkid-client",
2530            )
2531            .await;
2532
2533        control_plane_server.run().await.unwrap();
2534        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2535        control_plane_client.run().await.unwrap();
2536        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2537
2538        let controller = control_plane_client.controller.clone();
2539        let link_id = "test-create-link-id".to_string();
2540
2541        // Insert a pre-negotiated remote connection with a known link_id for testing.
2542        let (tx, _rx) = tokio::sync::mpsc::channel(16);
2543        let conn = slim_datapath::connection::Connection::new(
2544            slim_datapath::tables::ConnType::Remote,
2545            slim_datapath::connection::Channel::Server(tx),
2546        )
2547        .with_negotiation(&link_id, "1.0.0");
2548        controller
2549            .inner
2550            .message_processor
2551            .forwarder()
2552            .on_connection_established(conn, None)
2553            .unwrap();
2554
2555        let endpoint = "http://127.0.0.1:59999";
2556        let connection_config = serde_json::json!({
2557            "endpoint": endpoint,
2558            "link_id": link_id
2559        })
2560        .to_string();
2561
2562        let ctrl_msg = ControlMessage {
2563            message_id: uuid::Uuid::new_v4().to_string(),
2564            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2565                connections_to_create: vec![v1::Connection {
2566                    link_id: "reuse-existing-link".to_string(),
2567                    config_data: connection_config,
2568                }],
2569                connections_to_delete: vec![],
2570                routes_to_set: vec![],
2571                routes_to_delete: vec![],
2572                reconcile: false,
2573                connections_received: vec![],
2574            })),
2575        };
2576
2577        let (tx, mut rx) = mpsc::channel(1);
2578        controller
2579            .handle_new_control_message(ctrl_msg, &tx)
2580            .await
2581            .expect("config command must be handled");
2582
2583        let ack_msg = rx
2584            .recv()
2585            .await
2586            .expect("expected ack message")
2587            .expect("ack should be ok");
2588        let ack = match ack_msg.payload {
2589            Some(Payload::ConfigCommandAck(ack)) => ack,
2590            _ => panic!("expected ConfigCommandAck payload"),
2591        };
2592        assert_eq!(ack.connections_status.len(), 1);
2593        assert_eq!(ack.connections_status[0].link_id, "reuse-existing-link");
2594        assert!(ack.connections_status[0].success);
2595
2596        assert!(
2597            controller
2598                .inner
2599                .link_id_to_conn_id
2600                .read()
2601                .contains_key(&link_id),
2602            "expected link_id to be mapped to reused connection id"
2603        );
2604    }
2605
2606    #[tokio::test]
2607    #[traced_test]
2608    async fn test_subscription_set_unknown_link_id_fails_ack() {
2609        let (control_plane_server, control_plane_client, _client_cfg) = setup_control_planes(
2610            "127.0.0.1:50084",
2611            "sub-linkid-server-unknown",
2612            "sub-linkid-client-unknown",
2613        )
2614        .await;
2615
2616        let controller = control_plane_client.controller.clone();
2617        let ctrl_msg = ControlMessage {
2618            message_id: uuid::Uuid::new_v4().to_string(),
2619            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2620                connections_to_create: vec![],
2621                connections_to_delete: vec![],
2622                routes_to_set: vec![v1::Route {
2623                    name: Some(ProtoName::from_strings(["org", "ns", "agent"]).with_id(1u128)),
2624                    link_id: Some("missing-link-id".to_string()),
2625                    direction: None,
2626                }],
2627                routes_to_delete: vec![],
2628                reconcile: false,
2629                connections_received: vec![],
2630            })),
2631        };
2632        let (tx, mut rx) = mpsc::channel(1);
2633        controller
2634            .handle_new_control_message(ctrl_msg, &tx)
2635            .await
2636            .expect("config command must be handled");
2637
2638        let ack_msg = rx
2639            .recv()
2640            .await
2641            .expect("expected ack message")
2642            .expect("ack should be ok");
2643        let ack = match ack_msg.payload {
2644            Some(Payload::ConfigCommandAck(ack)) => ack,
2645            _ => panic!("expected ConfigCommandAck payload"),
2646        };
2647
2648        assert_eq!(ack.routes_status.len(), 1);
2649        assert!(!ack.routes_status[0].success);
2650        assert!(
2651            ack.routes_status[0]
2652                .error_msg
2653                .contains("Connection with link_id missing-link-id not found")
2654        );
2655
2656        drop(control_plane_server);
2657    }
2658
2659    #[tokio::test]
2660    #[traced_test]
2661    async fn test_create_connection_invalid_config_fails_ack() {
2662        let (_control_plane_server, control_plane_client, _client_cfg) = setup_control_planes(
2663            "127.0.0.1:50085",
2664            "create-invalid-config-server",
2665            "create-invalid-config-client",
2666        )
2667        .await;
2668
2669        let controller = control_plane_client.controller.clone();
2670        let ctrl_msg = ControlMessage {
2671            message_id: uuid::Uuid::new_v4().to_string(),
2672            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2673                connections_to_create: vec![v1::Connection {
2674                    link_id: "invalid-config-conn".to_string(),
2675                    config_data: "{invalid-json".to_string(),
2676                }],
2677                connections_to_delete: vec![],
2678                routes_to_set: vec![],
2679                routes_to_delete: vec![],
2680                reconcile: false,
2681                connections_received: vec![],
2682            })),
2683        };
2684        let (tx, mut rx) = mpsc::channel(1);
2685        controller
2686            .handle_new_control_message(ctrl_msg, &tx)
2687            .await
2688            .expect("config command must be handled");
2689
2690        let ack_msg = rx
2691            .recv()
2692            .await
2693            .expect("expected ack message")
2694            .expect("ack should be ok");
2695        let ack = match ack_msg.payload {
2696            Some(Payload::ConfigCommandAck(ack)) => ack,
2697            _ => panic!("expected ConfigCommandAck payload"),
2698        };
2699        assert_eq!(ack.connections_status.len(), 1);
2700        assert!(!ack.connections_status[0].success);
2701        assert!(
2702            ack.connections_status[0]
2703                .error_msg
2704                .contains("Failed to parse config")
2705        );
2706    }
2707
2708    #[tokio::test]
2709    #[traced_test]
2710    async fn test_subscription_delete_unknown_link_id_fails_ack() {
2711        let (control_plane_server, control_plane_client, _client_cfg) = setup_control_planes(
2712            "127.0.0.1:50086",
2713            "sub-del-linkid-server-unknown",
2714            "sub-del-linkid-client-unknown",
2715        )
2716        .await;
2717
2718        let controller = control_plane_client.controller.clone();
2719        let ctrl_msg = ControlMessage {
2720            message_id: uuid::Uuid::new_v4().to_string(),
2721            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2722                connections_to_create: vec![],
2723                connections_to_delete: vec![],
2724                routes_to_set: vec![],
2725                routes_to_delete: vec![v1::Route {
2726                    name: Some(ProtoName::from_strings(["org", "ns", "agent"]).with_id(1u128)),
2727                    link_id: Some("missing-link-id-delete".to_string()),
2728                    direction: None,
2729                }],
2730                reconcile: false,
2731                connections_received: vec![],
2732            })),
2733        };
2734        let (tx, mut rx) = mpsc::channel(1);
2735        controller
2736            .handle_new_control_message(ctrl_msg, &tx)
2737            .await
2738            .expect("config command must be handled");
2739
2740        let ack_msg = rx
2741            .recv()
2742            .await
2743            .expect("expected ack message")
2744            .expect("ack should be ok");
2745        let ack = match ack_msg.payload {
2746            Some(Payload::ConfigCommandAck(ack)) => ack,
2747            _ => panic!("expected ConfigCommandAck payload"),
2748        };
2749
2750        assert_eq!(ack.routes_status.len(), 1);
2751        assert!(!ack.routes_status[0].success);
2752        assert!(
2753            ack.routes_status[0]
2754                .error_msg
2755                .contains("Connection with link_id missing-link-id-delete not found")
2756        );
2757
2758        drop(control_plane_server);
2759    }
2760
2761    #[tokio::test]
2762    #[traced_test]
2763    async fn test_shutdown_drains_resources() {
2764        // Use a unique port to avoid conflicts with other tests.
2765        let (mut control_plane_server, mut control_plane_client, _client_cfg) =
2766            setup_control_planes(
2767                "127.0.0.1:50071",
2768                "shutdown-server-instance",
2769                "shutdown-client-instance",
2770            )
2771            .await;
2772
2773        // Run both ends to populate cancellation tokens.
2774        control_plane_server
2775            .run()
2776            .await
2777            .expect("server should start");
2778        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2779        control_plane_client
2780            .run()
2781            .await
2782            .expect("client should start");
2783        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2784
2785        // Ensure we have at least one cancellation token (server or client side tasks).
2786        let server_tokens_before = control_plane_server
2787            .controller
2788            .inner
2789            .cancellation_tokens
2790            .read()
2791            .len();
2792        assert!(
2793            server_tokens_before > 0,
2794            "expected server to have active cancellation tokens before shutdown"
2795        );
2796
2797        let client_tokens_before = control_plane_client
2798            .controller
2799            .inner
2800            .cancellation_tokens
2801            .read()
2802            .len();
2803        assert!(
2804            client_tokens_before > 0,
2805            "expected client to have active cancellation tokens before shutdown"
2806        );
2807
2808        // Perform shutdown on both.
2809        control_plane_client
2810            .shutdown()
2811            .await
2812            .expect("client shutdown ok");
2813        control_plane_server
2814            .shutdown()
2815            .await
2816            .expect("server shutdown ok");
2817
2818        // After shutdown, all cancellation tokens should be drained.
2819        let server_tokens_after = control_plane_server
2820            .controller
2821            .inner
2822            .cancellation_tokens
2823            .read()
2824            .len();
2825        assert_eq!(
2826            server_tokens_after, 0,
2827            "expected server cancellation tokens to be drained after shutdown"
2828        );
2829
2830        let client_tokens_after = control_plane_client
2831            .controller
2832            .inner
2833            .cancellation_tokens
2834            .read()
2835            .len();
2836        assert_eq!(
2837            client_tokens_after, 0,
2838            "expected client cancellation tokens to be drained after shutdown"
2839        );
2840
2841        // Second shutdown should error because drain_signal has been taken.
2842        assert!(
2843            control_plane_server.shutdown().await.is_err(),
2844            "second shutdown on server should return an error"
2845        );
2846        assert!(
2847            control_plane_client.shutdown().await.is_err(),
2848            "second shutdown on client should return an error"
2849        );
2850    }
2851
2852    #[tokio::test]
2853    #[traced_test]
2854    async fn test_shutdown_without_run() {
2855        // Build a control plane but do NOT call run()
2856        let (control_plane_server, mut _control_plane_client, _client_cfg) = setup_control_planes(
2857            "127.0.0.1:50072",
2858            "shutdown-no-run-server",
2859            "shutdown-no-run-client",
2860        )
2861        .await;
2862
2863        // No tasks should be registered yet
2864        assert_eq!(
2865            control_plane_server
2866                .controller
2867                .inner
2868                .cancellation_tokens
2869                .read()
2870                .len(),
2871            0,
2872            "expected zero cancellation tokens before shutdown when not run"
2873        );
2874
2875        // Shutdown should still succeed gracefully.
2876        control_plane_server
2877            .shutdown()
2878            .await
2879            .expect("shutdown without prior run should succeed");
2880
2881        // Tokens remain zero.
2882        assert_eq!(
2883            control_plane_server
2884                .controller
2885                .inner
2886                .cancellation_tokens
2887                .read()
2888                .len(),
2889            0,
2890            "expected zero cancellation tokens after shutdown when not run"
2891        );
2892
2893        // Second shutdown should fail.
2894        assert!(
2895            control_plane_server.shutdown().await.is_err(),
2896            "second shutdown should error due to missing drain signal"
2897        );
2898    }
2899
2900    fn make_reconcile_msg(link_id: &str, server_config: &ServerConnectionConfig) -> ControlMessage {
2901        ControlMessage {
2902            message_id: uuid::Uuid::new_v4().to_string(),
2903            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2904                connections_to_create: vec![v1::Connection {
2905                    link_id: link_id.to_string(),
2906                    config_data: serde_json::to_string(server_config).unwrap(),
2907                }],
2908                connections_to_delete: vec![],
2909                routes_to_set: vec![],
2910                routes_to_delete: vec![],
2911                reconcile: true,
2912                connections_received: vec![],
2913            })),
2914        }
2915    }
2916
2917    fn make_controller(outbound_clients: Vec<ClientConfig>) -> ControllerService {
2918        ControlPlane::new(ControlPlaneSettings {
2919            id: "test-node".to_string(),
2920            domain_name: None,
2921            servers: vec![],
2922            clients: vec![],
2923            outbound_clients,
2924            dataplane_clients: vec![],
2925            message_processor: MessageProcessor::new(),
2926            connection_details: vec![],
2927            auth_provider: None,
2928        })
2929        .controller
2930        .clone()
2931    }
2932
2933    #[tokio::test]
2934    async fn test_reconcile_basic_auth_missing_credentials_fails_ack() {
2935        let controller = make_controller(vec![]);
2936        let server_config = ServerConnectionConfig {
2937            endpoint: "http://target:8080".to_string(),
2938            tls_required: false,
2939            auth_method: RequiredAuthMethod::Basic,
2940            ..Default::default()
2941        };
2942        let (tx, mut rx) = mpsc::channel(1);
2943        controller
2944            .handle_new_control_message(make_reconcile_msg("link-1", &server_config), &tx)
2945            .await
2946            .unwrap();
2947        let ack = match rx.recv().await.unwrap().unwrap().payload {
2948            Some(Payload::ConfigCommandAck(a)) => a,
2949            _ => panic!("expected ConfigCommandAck"),
2950        };
2951        assert!(!ack.connections_status[0].success);
2952        assert!(ack.connections_status[0].error_msg.contains("target:8080"));
2953    }
2954
2955    #[tokio::test]
2956    async fn test_reconcile_jwt_auth_missing_credentials_fails_ack() {
2957        let controller = make_controller(vec![]);
2958        let server_config = ServerConnectionConfig {
2959            endpoint: "http://target:9090".to_string(),
2960            tls_required: false,
2961            auth_method: RequiredAuthMethod::Jwt,
2962            ..Default::default()
2963        };
2964        let (tx, mut rx) = mpsc::channel(1);
2965        controller
2966            .handle_new_control_message(make_reconcile_msg("link-2", &server_config), &tx)
2967            .await
2968            .unwrap();
2969        let ack = match rx.recv().await.unwrap().unwrap().payload {
2970            Some(Payload::ConfigCommandAck(a)) => a,
2971            _ => panic!("expected ConfigCommandAck"),
2972        };
2973        assert!(!ack.connections_status[0].success);
2974        assert!(ack.connections_status[0].error_msg.contains("target:9090"));
2975    }
2976}