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