Skip to main content

slim_datapath/
message_processing.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::{HashMap, HashSet};
5use std::net::SocketAddr;
6use std::sync::Arc;
7
8use display_error_chain::ErrorChainExt;
9use parking_lot::RwLock;
10use slim_config::client::ClientConfig;
11use slim_config::client::TransportChannel;
12use slim_config::component::configuration::Configuration;
13use tokio::sync::mpsc::{self, Sender};
14use tokio::sync::oneshot;
15use tokio::task::JoinHandle;
16use tokio_stream::wrappers::ReceiverStream;
17use tokio_stream::{Stream, StreamExt};
18use tokio_util::sync::CancellationToken;
19
20use tonic::Status;
21use tracing::{Instrument, debug, error, info, warn};
22
23// The gRPC server/client stubs, the server-side config + WebSocket-accept path,
24// and h2 error inspection are native-only. The browser build is a client that
25// dials out over `wss://` and never runs the gRPC service.
26cfg_if::cfg_if! {
27    if #[cfg(not(target_arch = "wasm32"))] {
28        use std::pin::Pin;
29
30        use crate::api::DataPlaneServiceServer;
31        use crate::api::proto::dataplane::v1::data_plane_service_client::DataPlaneServiceClient;
32        use crate::api::proto::dataplane::v1::data_plane_service_server::DataPlaneService;
33        use slim_config::server::ServerConfig;
34        use slim_config::server_handler::ServerHandler;
35        use slim_config::websocket::server as websocket_server;
36        use slim_config::websocket::server::AcceptedWebSocketConnection;
37        use tonic::{Request, Response};
38    }
39}
40
41#[cfg(feature = "otel_tracing")]
42use crate::otel_tracing;
43
44use crate::api::ProtoPublishType as PublishType;
45use crate::api::ProtoSubscribeType as SubscribeType;
46use crate::api::ProtoSubscriptionAckType as SubscriptionAckType;
47use crate::api::ProtoUnsubscribeType as UnsubscribeType;
48use crate::api::proto::dataplane::v1::Message;
49
50use crate::api::{
51    LinkNegotiationPayload, ProtoLink, ProtoLinkMessageType as LinkType, ProtoLinkType,
52    ProtoMessage, ProtoName,
53};
54use crate::connection::{Channel, Connection};
55use crate::errors::{DataPathError, MessageContext};
56use crate::forwarder::Forwarder;
57use crate::messages::utils::SlimHeaderFlags;
58use crate::sync::peer as sync_peer;
59use crate::sync::remote::{RemoteSync, SubscriptionInfo};
60use crate::tables::connection_table::ConnectionTable;
61use crate::tables::subscription_table::SubscriptionTableImpl;
62use crate::tables::{ConnType, MatchFilter};
63use crate::websocket;
64
65/// Result of updating subscription state (pure state change, no forwarding).
66struct SubscriptionOutcome {
67    /// Whether an aggregate transition occurred (0→1 or 1→0).
68    transition: bool,
69    /// Whether the source connection is a peer connection.
70    is_peer_conn: bool,
71    /// The forward-to connection (controller), if any.
72    forward_conn: Option<u64>,
73}
74
75// Sync tests using environment variables
76#[cfg(test)]
77static ENV_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
78
79#[derive(Debug)]
80struct MessageProcessorInternal {
81    /// The forwarder to handle processing events
82    forwarder: Forwarder<Connection>,
83
84    /// Drain signal to gracefully close all pending tasks
85    drain_signal: parking_lot::RwLock<Option<drain::Signal>>,
86
87    ///Drain watch to receive drain signal
88    drain_watch: parking_lot::RwLock<Option<drain::Watch>>,
89
90    /// Tx channel towards control plane
91    tx_control_plane: RwLock<Option<Sender<Result<Message, Status>>>>,
92
93    /// Tracks subscriptions forwarded to remote connections and handles restore on reconnect.
94    remote_sync: RemoteSync,
95
96    /// Service ID for tracing
97    service_id: String,
98
99    /// Peer group this node belongs to. Used during link negotiation to verify
100    /// that both sides of a peer connection belong to the same deployment.
101    /// Empty when no peer config is set.
102    deployment_name: String,
103
104    /// Default strict header MAC policy for server-accepted inter-node connections (see [`ServerConfig::require_header_mac`]).
105    // Only read by the native gRPC/WebSocket-accept server paths.
106    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
107    server_require_header_mac: bool,
108
109    /// Timeout for link negotiation to complete.
110    negotiation_timeout: std::time::Duration,
111
112    /// Whether peer-originated publishes should be relayed to other peers.
113    /// False for full-mesh (peers deliver directly — 1-hop rule).
114    /// True for standalone/generic multi-hop topologies.
115    relay_peer_publishes: bool,
116
117    /// Peer sync component for subscription forwarding and peer lifecycle.
118    /// Initialized as standalone; replaced with a peer-aware instance when peers are configured.
119    peer_sync: parking_lot::RwLock<crate::sync::PeerSync>,
120
121    server_enforce_pqc: bool,
122}
123
124#[derive(Debug, Clone)]
125pub struct MessageProcessor {
126    internal: Arc<MessageProcessorInternal>,
127}
128
129impl Default for MessageProcessor {
130    fn default() -> Self {
131        Self::new_with_service_id(String::new(), false)
132    }
133}
134
135/// Describes how a connection enters [`MessageProcessor::process_stream`].
136///
137/// Local connections are pre-registered in the table; remote connections are
138/// only inserted after the mandatory link negotiation completes.
139enum StreamSetup {
140    /// Connection already in the table (local connections).
141    Registered(u64),
142    /// Remote connection not yet in the table; will be inserted after negotiation.
143    Pending {
144        connection: Box<Connection>,
145        existing_index: Option<u64>,
146    },
147}
148
149impl MessageProcessor {
150    pub fn new_with_service_id(service_id: String, enforce_pqc: bool) -> Self {
151        Self::new_internal(
152            service_id,
153            String::new(),
154            false,
155            enforce_pqc,
156            std::time::Duration::from_secs(5),
157            false,
158        )
159    }
160
161    /// Create a processor with the server strict header MAC policy from `server_config`.
162    #[cfg(not(target_arch = "wasm32"))]
163    pub fn new_with_server_config(
164        service_id: String,
165        deployment_name: String,
166        server_config: &ServerConfig,
167        enforce_pqc: bool,
168        relay_peer_publishes: bool,
169    ) -> Self {
170        Self::new_internal(
171            service_id,
172            deployment_name,
173            server_config.require_header_mac,
174            enforce_pqc,
175            std::time::Duration::from_secs(server_config.negotiation_timeout_secs),
176            relay_peer_publishes,
177        )
178    }
179
180    fn new_internal(
181        service_id: String,
182        deployment_name: String,
183        server_require_header_mac: bool,
184        server_enforce_pqc: bool,
185        negotiation_timeout: std::time::Duration,
186        relay_peer_publishes: bool,
187    ) -> Self {
188        let (signal, watch) = drain::channel();
189        let internal = MessageProcessorInternal {
190            forwarder: Forwarder::new(),
191            drain_signal: RwLock::new(Some(signal)),
192            drain_watch: RwLock::new(Some(watch)),
193            tx_control_plane: RwLock::new(None),
194            remote_sync: RemoteSync::default(),
195            service_id,
196            deployment_name,
197            server_require_header_mac,
198            server_enforce_pqc,
199            negotiation_timeout,
200            relay_peer_publishes,
201            peer_sync: parking_lot::RwLock::new(crate::sync::PeerSync::standalone()),
202        };
203        Self {
204            internal: Arc::new(internal),
205        }
206    }
207
208    pub fn new() -> Self {
209        Self::default()
210    }
211
212    /// Run a data plane server using this message processor's drain watch.
213    /// Dispatch on the configured transport happens inside slim-config via the
214    /// [`ServerHandler`] trait below. Returns a cancellation token that can be
215    /// used to stop the server task.
216    #[cfg(not(target_arch = "wasm32"))]
217    pub async fn run_server(
218        &self,
219        config: &ServerConfig,
220    ) -> Result<CancellationToken, DataPathError> {
221        debug!(%config, "starting dataplane server");
222
223        if config.require_header_mac != self.internal.server_require_header_mac {
224            warn!(
225                configured = config.require_header_mac,
226                processor = self.internal.server_require_header_mac,
227                "server require_header_mac differs from MessageProcessor; inbound connections use the processor value set at construction (prefer MessageProcessor::new_with_server_config)",
228            );
229        }
230
231        if config.tls_setting.config.enforce_pqc != self.internal.server_enforce_pqc {
232            warn!(
233                configured = config.tls_setting.config.enforce_pqc,
234                processor = self.internal.server_enforce_pqc,
235                "server enforce_pqc differs from MessageProcessor; inbound connections use the processor value set at construction"
236            );
237        }
238
239        let watch = self.get_drain_watch()?;
240        config
241            .run_server(watch, Arc::new(self.clone()))
242            .await
243            .map_err(Into::into)
244    }
245
246    #[cfg(not(target_arch = "wasm32"))]
247    async fn handle_websocket_accepted(&self, accepted: AcceptedWebSocketConnection) {
248        let cancellation_token = CancellationToken::new();
249        let streams =
250            websocket::spawn_transport_tasks(accepted.websocket, cancellation_token.clone());
251
252        let connection = Connection::new(ConnType::Remote, Channel::Client(streams.outbound))
253            .with_remote_addr(accepted.remote_addr)
254            .with_local_addr(accepted.local_addr)
255            .with_require_header_mac(self.internal.server_require_header_mac)
256            .with_cancellation_token(Some(cancellation_token.clone()));
257
258        debug!(
259            remote = ?connection.remote_addr(),
260            local = ?connection.local_addr(),
261            "new websocket connection received from remote",
262        );
263        info!(telemetry = true, counter.num_active_connections = 1);
264
265        if let Err(err) = self.process_stream(
266            streams.inbound,
267            StreamSetup::Pending {
268                connection: Box::new(connection),
269                existing_index: None,
270            },
271            None,
272            cancellation_token,
273            ConnType::Remote,
274            false,
275        ) {
276            error!(error = %err.chain(), "error starting websocket processing stream");
277        }
278    }
279
280    /// Signal all spawned tasks (process_stream, etc.) to begin shutting down.
281    ///
282    /// Unlike [`shutdown`], this is synchronous: it drops the drain signal (which
283    /// notifies all drain watches) and the drain watch, but does NOT wait for the
284    /// tasks to finish.  Safe to call from a synchronous `Drop` implementation.
285    pub fn signal_drain(&self) {
286        self.internal.drain_signal.write().take();
287        self.internal.drain_watch.write().take();
288    }
289
290    pub async fn shutdown(&self) -> Result<(), DataPathError> {
291        // Take the drain signal
292        let signal = self
293            .internal
294            .drain_signal
295            .write()
296            .take()
297            .ok_or(DataPathError::AlreadyClosedError)?;
298
299        // Take drain watch
300        self.internal.drain_watch.write().take();
301
302        // Signal completion to all tasks
303        signal.drain().await;
304
305        Ok(())
306    }
307
308    fn set_tx_control_plane(&self, tx: Sender<Result<Message, Status>>) {
309        let mut tx_guard = self.internal.tx_control_plane.write();
310        *tx_guard = Some(tx);
311    }
312
313    fn get_tx_control_plane(&self) -> Option<Sender<Result<Message, Status>>> {
314        let tx_guard = self.internal.tx_control_plane.read();
315        tx_guard.clone()
316    }
317
318    pub fn forwarder(&self) -> &Forwarder<Connection> {
319        &self.internal.forwarder
320    }
321
322    pub(crate) fn remote_sync(&self) -> &RemoteSync {
323        &self.internal.remote_sync
324    }
325
326    /// Verify SLIM header MAC for inter-node traffic only (local app connections skip this).
327    pub(crate) fn verify_remote_header_mac(
328        &self,
329        conn_index: u64,
330        message: &Message,
331        enforce_strict_verification: bool,
332    ) -> Result<(), DataPathError> {
333        let conn = self
334            .forwarder()
335            .get_connection(conn_index)
336            .ok_or(DataPathError::ConnectionNotFound(conn_index))?;
337        if !matches!(conn.connection_type(), ConnType::Remote | ConnType::Edge) {
338            return Ok(());
339        }
340        let header = message
341            .try_get_slim_header()
342            .ok_or(DataPathError::UnknownMsgType)?;
343
344        let has_wire_mac = header.header_mac.as_ref().is_some_and(|m| !m.is_empty());
345
346        // Publishes must carry a MAC once the inter-node session has derived a key.  Control
347        // messages (subscribe / unsubscribe) may still traverse the same gRPC stream without a
348        // tag on some federation paths; skipping verification only when the tag is absent keeps
349        // tamper detection for application traffic.
350        if (message.is_subscribe() || message.is_unsubscribe()) && !has_wire_mac {
351            if enforce_strict_verification {
352                return Err(DataPathError::NegotiationError(
353                    "empty HMAC is not allowed in strict verification mode".to_string(),
354                ));
355            }
356            return Ok(());
357        }
358
359        let Some(mac) = conn.header_hmac() else {
360            if enforce_strict_verification {
361                return Err(DataPathError::NegotiationError(
362                    "strict header MAC required but link HMAC session is not installed".to_string(),
363                ));
364            }
365            // Do not accept inter-node publishes that already carry an integrity tag until this
366            // side has derived the link MAC; otherwise verification is silently skipped and peers
367            // never see `HeaderIntegrity` failures (including tampered test traffic).
368            if message.is_publish() && has_wire_mac {
369                return Err(DataPathError::HeaderMacAwaitingLinkNegotiation(conn_index));
370            }
371            return Ok(());
372        };
373        let link_id = conn
374            .link_id()
375            .filter(|id| !id.is_empty())
376            .ok_or(DataPathError::HeaderMacAwaitingLinkNegotiation(conn_index))?;
377        mac.verify_slim_header(header, &link_id)
378            .map_err(DataPathError::HeaderIntegrity)
379    }
380
381    pub(crate) fn get_drain_watch(&self) -> Result<drain::Watch, DataPathError> {
382        self.internal
383            .drain_watch
384            .read()
385            .clone()
386            .ok_or(DataPathError::AlreadyClosedError)
387    }
388
389    /// Re-send `remote_subs` as subscribe messages to `conn_index`.
390    /// Delegates to [`RemoteSync::restore`].
391    async fn restore_remote_subscriptions(
392        &self,
393        remote_subs: &HashSet<SubscriptionInfo>,
394        conn_index: u64,
395        restore_tracking: bool,
396    ) {
397        self.remote_sync()
398            .restore(self, remote_subs, conn_index, restore_tracking)
399            .await;
400    }
401
402    async fn try_to_connect(
403        &self,
404        client_config: ClientConfig,
405        local: Option<SocketAddr>,
406        remote: Option<SocketAddr>,
407        existing_conn_index: Option<u64>,
408    ) -> Result<(JoinHandle<()>, u64), DataPathError> {
409        client_config.validate()?;
410
411        let mut watch = std::pin::pin!(self.get_drain_watch()?.signaled());
412        let channel = tokio::select! {
413            _ = &mut watch => {
414                return Err(DataPathError::ShuttingDownError);
415            }
416            res = client_config.to_channel() => {
417                res?
418            }
419        };
420
421        let cancellation_token = CancellationToken::new();
422        let link_id = client_config.link_id.clone();
423
424        match channel {
425            // On wasm the gRPC arm is uninhabited (`to_channel` returns
426            // `TransportChannel<Infallible>`); only the WebSocket arm runs.
427            #[cfg(target_arch = "wasm32")]
428            TransportChannel::Grpc(never) => match never {},
429            #[cfg(not(target_arch = "wasm32"))]
430            TransportChannel::Grpc(grpc_channel) => {
431                let mut client = DataPlaneServiceClient::new(grpc_channel);
432                let (tx, rx) = mpsc::channel(128);
433                let stream = client
434                    .open_channel(Request::new(ReceiverStream::new(rx)))
435                    .await?;
436
437                let (handle, conn_index_rx) = self.register_remote_connection(
438                    stream.into_inner(),
439                    Channel::Client(tx),
440                    &client_config,
441                    local,
442                    remote,
443                    existing_conn_index,
444                    cancellation_token,
445                    Some(link_id.clone()),
446                )?;
447
448                let conn_index = conn_index_rx.await.map_err(|_| {
449                    DataPathError::NegotiationError(
450                        "negotiation task terminated unexpectedly".to_string(),
451                    )
452                })??;
453
454                // For peer connections established via client config (generic topology),
455                // auto-register in the forwarder and perform full sync.
456                if matches!(client_config.connection_type, ConnType::Peer) {
457                    let fwd = self.peer_sync();
458                    if !fwd.has_peer_state() {
459                        fwd.add_peer_conn_and_sync(self, conn_index);
460                    }
461                }
462
463                Ok((handle, conn_index))
464            }
465            TransportChannel::Websocket(ws_channel) => {
466                let websocket = ws_channel
467                    .take_websocket()
468                    .expect("websocket channel already consumed");
469                let streams =
470                    websocket::spawn_transport_tasks(websocket, cancellation_token.clone());
471
472                let (handle, conn_index_rx) = self.register_remote_connection(
473                    streams.inbound,
474                    Channel::Client(streams.outbound),
475                    &client_config,
476                    local.or(ws_channel.local_addr()),
477                    remote.or(ws_channel.remote_addr()),
478                    existing_conn_index,
479                    cancellation_token,
480                    Some(link_id.clone()),
481                )?;
482
483                let conn_index = conn_index_rx.await.map_err(|_| {
484                    DataPathError::NegotiationError(
485                        "negotiation task terminated unexpectedly".to_string(),
486                    )
487                })??;
488
489                // For peer connections established via client config (generic topology),
490                // auto-register in the forwarder and perform full sync.
491                if matches!(client_config.connection_type, ConnType::Peer) {
492                    let fwd = self.peer_sync();
493                    if !fwd.has_peer_state() {
494                        fwd.add_peer_conn_and_sync(self, conn_index);
495                    }
496                }
497
498                Ok((handle, conn_index))
499            }
500        }
501    }
502
503    /// Common post-connect plumbing shared by every transport: register the
504    /// new [`Connection`] in the forwarder and spawn the per-stream processor.
505    /// Transport-specific code only has to produce the inbound stream + outbound
506    /// channel and call this — see [`Self::try_to_connect`] for client-side
507    /// usage and [`Self::handle_websocket_accepted`] for the server side.
508    #[allow(clippy::too_many_arguments)]
509    fn register_remote_connection<S>(
510        &self,
511        inbound: S,
512        outbound: Channel,
513        client_config: &ClientConfig,
514        local: Option<SocketAddr>,
515        remote: Option<SocketAddr>,
516        existing_conn_index: Option<u64>,
517        cancellation_token: CancellationToken,
518        link_id: Option<String>,
519    ) -> Result<
520        (
521            JoinHandle<()>,
522            oneshot::Receiver<Result<u64, DataPathError>>,
523        ),
524        DataPathError,
525    >
526    where
527        S: Stream<Item = Result<Message, Status>> + Unpin + Send + 'static,
528    {
529        let mut connection = Connection::new(client_config.connection_type, outbound)
530            .with_local_addr(local)
531            .with_remote_addr(remote)
532            .with_config_data(Some(client_config.clone()))
533            .with_require_header_mac(client_config.require_header_mac)
534            .with_cancellation_token(Some(cancellation_token.clone()));
535        if let Some(link_id) = link_id {
536            connection = connection.with_link_id(link_id);
537        }
538
539        debug!(
540            remote = ?connection.remote_addr(),
541            local = ?connection.local_addr(),
542            ?client_config.connection_type,
543            "new connection initiated locally",
544        );
545
546        let (handle, conn_index_rx) = self.process_stream(
547            inbound,
548            StreamSetup::Pending {
549                connection: Box::new(connection),
550                existing_index: existing_conn_index,
551            },
552            Some(client_config.clone()),
553            cancellation_token,
554            client_config.connection_type,
555            false,
556        )?;
557
558        Ok((handle, conn_index_rx))
559    }
560
561    pub async fn connect(
562        &self,
563        client_config: ClientConfig,
564        local: Option<SocketAddr>,
565        remote: Option<SocketAddr>,
566    ) -> Result<(JoinHandle<()>, u64), DataPathError> {
567        self.try_to_connect(client_config, local, remote, None)
568            .await
569    }
570
571    pub fn disconnect(&self, conn: u64) -> Result<ClientConfig, DataPathError> {
572        let connection = match self.forwarder().get_connection(conn) {
573            Some(c) => c,
574            None => {
575                error!(%conn, "error handling disconnect: connection unknown");
576                return Err(DataPathError::DisconnectionError(conn));
577            }
578        };
579
580        let token = match connection.cancellation_token() {
581            Some(t) => t,
582            None => {
583                error!(%conn, "error handling disconnect: missing cancellation token");
584                return Err(DataPathError::DisconnectionError(conn));
585            }
586        };
587
588        // Cancel receiving loop; this triggers deletion of connection state.
589        token.cancel();
590
591        connection
592            .config_data()
593            .cloned()
594            .ok_or(DataPathError::DisconnectionError(conn))
595    }
596
597    #[tracing::instrument(skip_all, fields(service_id = %self.internal.service_id))]
598    pub fn register_local_connection(
599        &self,
600        from_control_plane: bool,
601    ) -> Result<
602        (
603            u64,
604            tokio::sync::mpsc::Sender<Result<Message, Status>>,
605            tokio::sync::mpsc::Receiver<Result<Message, Status>>,
606        ),
607        DataPathError,
608    > {
609        // create a pair tx, rx to be able to send messages with the standard processing loop
610        let (tx1, rx1) = mpsc::channel(512);
611
612        debug!("establishing new local app connection");
613
614        // create a pair tx, rx to be able to receive messages and insert it into the connection table
615        let (tx2, rx2) = mpsc::channel(512);
616
617        // if the call is coming from the control plane set the tx channel
618        // we assume to talk to a single control plane so set the channel only once
619        if from_control_plane && self.get_tx_control_plane().is_none() {
620            self.set_tx_control_plane(tx2.clone());
621        }
622
623        // create a connection
624        let cancellation_token = CancellationToken::new();
625        let connection = Connection::new(ConnType::Local, Channel::Server(tx2))
626            .with_cancellation_token(Some(cancellation_token.clone()));
627
628        // add it to the connection table
629        let conn_id = self
630            .forwarder()
631            .on_connection_established(connection, None)
632            .unwrap();
633
634        debug!(%conn_id, "local connection established");
635        info!(telemetry = true, counter.num_active_connections = 1);
636
637        // this loop will process messages from the local app
638        self.process_stream(
639            ReceiverStream::new(rx1),
640            StreamSetup::Registered(conn_id),
641            None,
642            cancellation_token,
643            ConnType::Local,
644            from_control_plane,
645        )?;
646
647        // return the conn_id and  handles to be used to send and receive messages
648        Ok((conn_id, tx1, rx2))
649    }
650
651    pub async fn send_msg(
652        &self,
653        #[cfg(feature = "otel_tracing")] mut msg: Message,
654        #[cfg(not(feature = "otel_tracing"))] msg: Message,
655        out_conn: u64,
656    ) -> Result<(), DataPathError> {
657        #[cfg(feature = "otel_tracing")]
658        otel_tracing::prepare_outbound_msg(
659            &mut msg,
660            "send_message",
661            &self.internal.service_id,
662            otel_tracing::SpanTarget::Connection(out_conn),
663        );
664        self.send_msg_raw(msg, out_conn).await
665    }
666
667    async fn send_msg_raw(&self, mut msg: Message, out_conn: u64) -> Result<(), DataPathError> {
668        let connection = self.forwarder().get_connection(out_conn);
669        match connection {
670            Some(conn) => {
671                // Link and SubscriptionAck messages have no SLIM header: skip header
672                // manipulation and telemetry span creation.
673                if !msg.is_link() && !msg.is_subscription_ack() {
674                    msg.clear_slim_header();
675                }
676
677                if !msg.is_link()
678                    && !msg.is_subscription_ack()
679                    && matches!(conn.connection_type(), ConnType::Remote | ConnType::Edge)
680                    && conn.require_header_mac()
681                    && conn.header_hmac().is_none()
682                {
683                    return Err(DataPathError::NegotiationError(
684                        "strict header MAC required but link HMAC session is not installed"
685                            .to_string(),
686                    ));
687                }
688
689                if !msg.is_link()
690                    && !msg.is_subscription_ack()
691                    && matches!(conn.connection_type(), ConnType::Remote | ConnType::Edge)
692                    && let Some(mac) = conn.header_hmac()
693                {
694                    let link_id = conn
695                        .link_id()
696                        .or_else(|| conn.config_data().map(|c| c.link_id.clone()))
697                        .filter(|id| !id.is_empty());
698                    if let Some(ref id) = link_id {
699                        let header = msg.get_slim_header_mut();
700
701                        mac.sign_slim_header(header, id.as_str())
702                            .map_err(DataPathError::HeaderIntegrity)?;
703
704                        // Debug / integration-test builds only (`--release` omits this; env var is inert).
705                        // Must run *after* sign so the tag does not cover the mutated preimage fields.
706                        #[cfg(debug_assertions)]
707                        if std::env::var("SLIM_TEST_TAMPER_DESTINATION").is_ok()
708                            && let Some(dest) = header.destination.as_mut()
709                            && let Some(sn) = dest.str_name.as_mut()
710                        {
711                            sn.str_component_2.push_str("-integrity-test-tamper");
712                        }
713                    } else {
714                        return Err(DataPathError::HeaderMacAwaitingLinkNegotiation(out_conn));
715                    }
716                }
717
718                if !msg.is_link()
719                    && !msg.is_subscription_ack()
720                    && matches!(conn.channel(), Channel::Server(_))
721                    && matches!(conn.connection_type(), ConnType::Local)
722                {
723                    msg.get_slim_header_mut().header_mac = None;
724                }
725
726                match conn.channel() {
727                    Channel::Server(s) => {
728                        s.send(Ok(msg))
729                            .await
730                            .map_err(|e| DataPathError::MessageProcessingError {
731                                source: Box::new(DataPathError::ConnectionNotFound(out_conn)),
732                                msg: Box::new(e.0.unwrap_or_default()),
733                            })
734                    }
735                    Channel::Client(s) => {
736                        s.send(msg)
737                            .await
738                            .map_err(|e| DataPathError::MessageProcessingError {
739                                source: Box::new(DataPathError::ConnectionNotFound(out_conn)),
740                                msg: Box::new(e.0),
741                            })
742                    }
743                }
744            }
745            None => Err(DataPathError::ConnectionNotFound(out_conn)),
746        }
747    }
748
749    /// Send a gRPC status error on a server-side connection.
750    /// This causes the client's stream to yield `Err(status)`.
751    async fn send_status(&self, conn_index: u64, status: Status) {
752        if let Some(conn) = self.forwarder().get_connection(conn_index)
753            && let Channel::Server(tx) = conn.channel()
754        {
755            let _ = tx.send(Err(status)).await;
756        }
757    }
758
759    async fn match_and_forward_msg(
760        &self,
761        #[cfg(feature = "otel_tracing")] mut msg: Message,
762        #[cfg(not(feature = "otel_tracing"))] msg: Message,
763        in_connection: u64,
764        fanout: u32,
765        filter: MatchFilter,
766    ) -> Result<(), DataPathError> {
767        let header = msg.get_slim_header();
768        debug!(name = %header.get_dst(), %fanout, "match and forward message");
769
770        // if the message already contains an output connection, use that one
771        // without performing any match in the subscription table
772        if let Some(val) = msg.get_forward_to() {
773            debug!(conn = %val, "forwarding message to connection");
774            return self.send_msg(msg, val).await;
775        }
776
777        let encoded = header.get_encoded_dst();
778
779        match self
780            .forwarder()
781            .on_publish_msg_match(encoded, in_connection, fanout, filter)
782        {
783            Ok(out_vec) => {
784                let len = out_vec.len();
785                // Single destination: preserve per-connection span attributes.
786                if len == 1 {
787                    return self.send_msg(msg, out_vec[0]).await;
788                }
789
790                #[cfg(feature = "otel_tracing")]
791                otel_tracing::prepare_fanout_msg(
792                    &mut msg,
793                    "send_message",
794                    &self.internal.service_id,
795                    len as u32,
796                );
797
798                let mut i = 0usize;
799                while i < len - 1 {
800                    self.send_msg_raw(msg.clone(), out_vec[i]).await?;
801                    i += 1;
802                }
803                self.send_msg_raw(msg, out_vec[i]).await?;
804                Ok(())
805            }
806            Err(e) => {
807                debug!(name = %header.get_dst(), %fanout, error = %e, "no match for publish destination");
808                Err(DataPathError::MessageProcessingError {
809                    source: Box::new(e),
810                    msg: Box::new(msg),
811                })
812            }
813        }
814    }
815
816    /// Dispatch an inbound Link message to the appropriate handler.
817    ///
818    /// Link messages are link-local and must never be processed for local connections
819    /// (they are only exchanged between SLIM nodes).
820    async fn handle_link_message(
821        &self,
822        link: ProtoLink,
823        conn_index: u64,
824        category: ConnType,
825    ) -> Result<(), DataPathError> {
826        if category.is_local() {
827            debug!(%conn_index, "ignoring link message received on local connection");
828            return Ok(());
829        }
830        match link.link_type {
831            Some(ProtoLinkType::LinkNegotiation(payload)) => {
832                self.handle_link_negotiation(&payload, conn_index).await
833            }
834            None => {
835                debug!(%conn_index, "received link message with unset link_type");
836                Ok(())
837            }
838        }
839    }
840
841    /// Handle an inbound link negotiation message arriving in the main loop.
842    ///
843    /// Since negotiation is mandatory and completes before the connection is
844    /// inserted into the table, any link negotiation message arriving here is
845    /// either a duplicate or a protocol error — we log and ignore it.
846    async fn handle_link_negotiation(
847        &self,
848        payload: &LinkNegotiationPayload,
849        in_connection: u64,
850    ) -> Result<(), DataPathError> {
851        debug!(
852            %in_connection,
853            link_id = %payload.link_id,
854            is_reply = payload.is_reply,
855            "ignoring link negotiation message on already-negotiated connection",
856        );
857
858        Ok(())
859    }
860
861    /// Upgrade a server-side connection to Peer after validating identity and deployment_name.
862    /// Notifies PeerSyncManager or auto-registers in the forwarder (generic topology).
863    pub(crate) async fn handle_peer_upgrade(
864        &self,
865        remote_node_id: &str,
866        remote_deployment_name: &str,
867        in_connection: u64,
868        link_id: &str,
869    ) -> Result<(), DataPathError> {
870        // Reject self-connections (can happen when all replicas share the same config).
871        if remote_node_id == self.internal.service_id {
872            warn!(
873                %in_connection, %link_id,
874                "rejecting peer connection from self (same node_id)"
875            );
876            self.send_status(
877                in_connection,
878                Status::permission_denied("self-connection rejected: same node_id"),
879            )
880            .await;
881            let _ = self.disconnect(in_connection);
882            return Ok(());
883        }
884
885        // Verify deployment_name: if we have a deployment_name configured, the remote must match.
886        if !self.internal.deployment_name.is_empty()
887            && remote_deployment_name != self.internal.deployment_name
888        {
889            warn!(
890                %in_connection, %link_id,
891                local_group = %self.internal.deployment_name,
892                remote_group = %remote_deployment_name,
893                "rejecting peer upgrade: deployment_name mismatch"
894            );
895            self.send_status(
896                in_connection,
897                Status::permission_denied("deployment_name mismatch"),
898            )
899            .await;
900            let _ = self.disconnect(in_connection);
901            return Ok(());
902        }
903
904        info!(
905            %in_connection, %link_id, %remote_node_id,
906            "upgrading server-side connection to Peer (negotiation)"
907        );
908        self.connection_table().update(in_connection, |conn| {
909            conn.set_connection_type(ConnType::Peer)
910        });
911
912        self.peer_sync()
913            .on_incoming_peer(self, remote_node_id.to_string(), in_connection);
914
915        Ok(())
916    }
917
918    async fn process_publish(
919        &self,
920        msg: Message,
921        in_connection: u64,
922        filter: MatchFilter,
923    ) -> Result<(), DataPathError> {
924        debug!(
925            %in_connection,
926            ?msg,
927            "received publication"
928        );
929
930        // telemetry /////////////////////////////////////////
931        info!(
932            telemetry = true,
933            monotonic_counter.num_messages_by_type = 1,
934            method = "publish"
935        );
936        //////////////////////////////////////////////////////
937
938        // this function may panic, but at this point we are sure we are processing
939        // a publish message
940        let fanout = msg.get_fanout();
941
942        self.match_and_forward_msg(msg, in_connection, fanout, filter)
943            .await
944    }
945
946    pub(crate) async fn send_subscription_ack(
947        &self,
948        in_connection: u64,
949        subscription_id: u64,
950        result: &Result<(), DataPathError>,
951    ) {
952        let (success, error_msg) = match result {
953            Ok(()) => (true, String::new()),
954            Err(e) => (false, e.to_string()),
955        };
956
957        let ack_msg =
958            Message::builder().build_subscription_ack(subscription_id, success, error_msg);
959
960        if let Err(e) = self.send_msg(ack_msg, in_connection).await {
961            error!(error = %e.chain(), "failed to send subscription ack");
962        }
963    }
964
965    /// Pure state update for a subscription: updates the subscription table
966    /// and returns the outcome (whether a transition occurred, connection type, forward target).
967    /// Does NOT perform any forwarding or event emission.
968    fn update_subscription_state(
969        &self,
970        msg: &Message,
971        conn: u64,
972        forward: Option<u64>,
973        add: bool,
974        subscription_id: u64,
975    ) -> Result<SubscriptionOutcome, DataPathError> {
976        let dst = msg.get_dst();
977
978        // As connection is deleted only after processing, at this point it must exist.
979        let connection = if let Some(c) = self.forwarder().get_connection(conn) {
980            c
981        } else {
982            return Err(DataPathError::ConnectionNotFound(conn));
983        };
984
985        debug!(
986            %conn,
987            %dst,
988            is_local = connection.is_local_connection(),
989            "processing {}subscription state",
990            if add { "" } else { "un" }
991        );
992
993        let is_peer_conn = connection.is_peer_connection();
994
995        let transition = self.forwarder().on_subscription_msg(
996            dst,
997            conn,
998            connection.connection_type(),
999            add,
1000            subscription_id,
1001        )?;
1002
1003        Ok(SubscriptionOutcome {
1004            transition,
1005            is_peer_conn,
1006            forward_conn: forward,
1007        })
1008    }
1009
1010    // Use a single function to process subscription and unsubscription packets.
1011    // The flag add = true is used to add a new subscription while add = false
1012    // is used to remove existing state.
1013    //
1014    // This is the SINGLE entry point for all subscription handling.
1015    // All forwarding (to peers, to controller, hub relay) goes through
1016    // the PeerSync — no inline forwarding anywhere else.
1017    async fn process_subscription(
1018        &self,
1019        msg: Message,
1020        in_connection: u64,
1021        add: bool,
1022    ) -> Result<(), DataPathError> {
1023        debug!(
1024            %in_connection,
1025            ?msg,
1026            "received {}subscription",
1027            if add { "" } else { "un" }
1028        );
1029
1030        // telemetry /////////////////////////////////////////
1031        info!(
1032            telemetry = true,
1033            monotonic_counter.num_messages_by_type = 1,
1034            message_type = { if add { "subscribe" } else { "unsubscribe" } }
1035        );
1036        //////////////////////////////////////////////////////
1037
1038        let subscription_id = msg.get_subscription_id();
1039
1040        debug!(?subscription_id, "received subscription id");
1041
1042        // get header
1043        let header = msg.get_slim_header();
1044
1045        // get in and out connections
1046        let (in_conn, recv_from, forward) = header.get_connections();
1047        let in_conn = recv_from.unwrap_or(in_conn);
1048
1049        // Never forward subscriptions to local connections (they are local apps whose
1050        // routes are already set locally).
1051        let forward = forward.filter(|&out| {
1052            self.forwarder()
1053                .get_connection(out)
1054                .map(|c| !c.is_local_connection())
1055                .unwrap_or(true)
1056        });
1057
1058        // As connection is deleted only after processing, at this point it must exist.
1059        let Some(connection) = self.forwarder().get_connection(in_conn) else {
1060            if let Some(id) = subscription_id {
1061                debug!(%in_conn, "connection not found, sending error ack");
1062                self.send_subscription_ack(
1063                    in_connection,
1064                    id,
1065                    &Err(DataPathError::ConnectionNotFound(in_conn)),
1066                )
1067                .await;
1068            }
1069            return Err(DataPathError::MessageProcessingError {
1070                source: Box::new(DataPathError::ConnectionNotFound(in_conn)),
1071                msg: Box::new(msg),
1072            });
1073        };
1074
1075        // Do not process subscriptions forwarded back to local connections.
1076        if recv_from.is_some() && connection.is_local_connection() {
1077            if let Some(id) = subscription_id {
1078                debug!(%in_conn, "subscription looped back to local connection, acking ok");
1079                self.send_subscription_ack(in_connection, id, &Ok(())).await;
1080            }
1081            return Ok(());
1082        }
1083
1084        // Loop prevention: check if this subscription_id has already been forwarded
1085        // by this node. This prevents loops in ring/mesh topologies where a subscription
1086        // could travel around and come back. Only applies to subscribes (add=true);
1087        // unsubscribes with a seen sub_id are expected (they cancel a prior forwarded sub).
1088        // Unsubscribe loops are bounded by TTL and don't cause state corruption since
1089        // remove operations are idempotent.
1090        let sub_id = subscription_id.unwrap_or(0);
1091        if add && sub_id != 0 && self.peer_sync().has_seen_sub_id(sub_id) {
1092            debug!(
1093                %in_conn,
1094                %sub_id,
1095                "dropping subscription already forwarded by this node (loop prevention)"
1096            );
1097            if let Some(id) = subscription_id {
1098                self.send_subscription_ack(in_connection, id, &Ok(())).await;
1099            }
1100            return Ok(());
1101        }
1102
1103        // Update local state (subscription table) — pure state change, no forwarding.
1104        let outcome = match self.update_subscription_state(&msg, in_conn, forward, add, sub_id) {
1105            Ok(o) => o,
1106            Err(e) => {
1107                if let Some(id) = subscription_id {
1108                    self.send_subscription_ack(in_connection, id, &Err(e)).await;
1109                    // Return Ok since we already sent the error ACK.
1110                    return Ok(());
1111                }
1112                return Err(DataPathError::MessageProcessingError {
1113                    source: Box::new(e),
1114                    msg: Box::new(msg),
1115                });
1116            }
1117        };
1118
1119        // Notify the control plane of local subscription transitions so it can
1120        // create inter-group routes (SPT expansion). Only for local app connections
1121        // that cause an aggregate transition (0→1 for subscribe, 1→0 for unsubscribe).
1122        // Remote connection subscribes are already forwarded to the control plane
1123        // by the process_stream loop.
1124        if connection.is_local_connection()
1125            && !outcome.is_peer_conn
1126            && outcome.transition
1127            && let Some(txcp) = self.get_tx_control_plane()
1128        {
1129            let _ = txcp.send(Ok(msg.clone())).await;
1130        }
1131
1132        // Determine forwarding targets:
1133        // - Peers (All): non-peer subscription with aggregate transition (0→1 or 1→0)
1134        // - Peers (ExcludeConn): peer subscription with remaining TTL >= 2 (relay)
1135        // - Forward conn: controller/remote node when header.forward_to is set
1136        //
1137        // TTL controls propagation depth:
1138        // - TTL=2 on initial send → peer decrements to 1, sees 1 < 2, no relay (full mesh)
1139        // - TTL=3 on initial send → hub decrements to 2, relays; spoke decrements to 1, stops
1140        // - TTL=6 on initial send → allows up to 5 hops of relay (generic topology)
1141        let remaining_ttl = msg.get_ttl();
1142
1143        let (peer_target, peer_ttl) = if !outcome.is_peer_conn && outcome.transition {
1144            // Local/remote subscription transition → forward to ALL peers with configured TTL
1145            let ttl = self.peer_sync().subscription_ttl();
1146            (Some(crate::sync::PeerTarget::All), ttl)
1147        } else if outcome.is_peer_conn && remaining_ttl >= 2 {
1148            // Peer subscription relay: TTL allows further propagation.
1149            // Forward to all peers except the source, using remaining TTL.
1150            (
1151                Some(crate::sync::PeerTarget::ExcludeConn(in_conn)),
1152                remaining_ttl,
1153            )
1154        } else {
1155            (None, 0)
1156        };
1157
1158        let targets = crate::sync::ForwardTargets {
1159            peers: peer_target,
1160            forward_conn: outcome.forward_conn,
1161        };
1162
1163        // If there are forwarding targets, spawn the forwarder task (non-blocking).
1164        // The forwarder will wait for ACKs and then ACK the upstream client.
1165        if targets.has_any() {
1166            let fwd = self.peer_sync();
1167            let dst = msg.get_dst();
1168            debug!(
1169                %in_connection,
1170                %dst,
1171                %remaining_ttl,
1172                %peer_ttl,
1173                ?targets,
1174                "spawning subscription forwarder task"
1175            );
1176            let drain = self.get_drain_watch().ok();
1177            if let Some(drain) = drain {
1178                fwd.spawn_forward_and_ack(
1179                    self.clone(),
1180                    msg,
1181                    dst,
1182                    sub_id,
1183                    add,
1184                    targets,
1185                    in_connection,
1186                    subscription_id,
1187                    peer_ttl,
1188                    drain,
1189                );
1190                return Ok(());
1191            }
1192            // Fallback: drain not available (shutting down).
1193            // ACK immediately as best-effort.
1194        }
1195
1196        // No forwarding needed (or no forwarder) — ACK immediately.
1197        if let Some(id) = subscription_id {
1198            debug!(%in_connection, "sending immediate subscription ack (no forwarding)");
1199            self.send_subscription_ack(in_connection, id, &Ok(())).await;
1200        }
1201
1202        Ok(())
1203    }
1204
1205    pub async fn process_message(
1206        &self,
1207        msg: Message,
1208        in_connection: u64,
1209        category: ConnType,
1210    ) -> Result<(), DataPathError> {
1211        match msg.message_type {
1212            Some(SubscribeType(_)) => self.process_subscription(msg, in_connection, true).await,
1213            Some(UnsubscribeType(_)) => self.process_subscription(msg, in_connection, false).await,
1214            Some(PublishType(_)) => {
1215                let filter = match category {
1216                    ConnType::Peer => {
1217                        if self.internal.relay_peer_publishes {
1218                            MatchFilter::ALL
1219                        } else {
1220                            MatchFilter::EXCLUDE_PEER
1221                        }
1222                    }
1223                    _ => MatchFilter::ALL,
1224                };
1225                self.process_publish(msg, in_connection, filter).await
1226            }
1227            Some(LinkType(link)) => {
1228                self.handle_link_message(link, in_connection, category)
1229                    .await
1230            }
1231            Some(SubscriptionAckType(ack)) => {
1232                let result = if ack.success {
1233                    Ok(())
1234                } else {
1235                    Err(DataPathError::RemoteSubscriptionAckError(ack.error))
1236                };
1237
1238                self.peer_sync().resolve_ack(ack.subscription_id, result);
1239                Ok(())
1240            }
1241            None => unreachable!(
1242                "message type not set; validate() must be called before process_message"
1243            ),
1244        }
1245    }
1246
1247    pub(crate) async fn handle_new_message(
1248        &self,
1249        conn_index: u64,
1250        category: ConnType,
1251        mut msg: Message,
1252    ) -> Result<(), DataPathError> {
1253        debug!(%conn_index, "received message from connection");
1254        info!(
1255            telemetry = true,
1256            monotonic_counter.num_processed_messages = 1
1257        );
1258
1259        // validate message
1260        if let Err(err) = msg.validate() {
1261            info!(
1262                telemetry = true,
1263                monotonic_counter.num_messages_by_type = 1,
1264                message_type = "none"
1265            );
1266
1267            let ret_err = DataPathError::MessageProcessingError {
1268                source: Box::new(err.into()),
1269                msg: Box::new(msg),
1270            };
1271
1272            return Err(ret_err);
1273        }
1274
1275        // Link and SubscriptionAck messages have no SLIM header: skip header processing and telemetry span.
1276        if !msg.is_link() && !msg.is_subscription_ack() {
1277            // add incoming connection to the SLIM header
1278            msg.set_incoming_conn(Some(conn_index));
1279
1280            // TTL processing: decrement for remote messages (hop-by-hop)
1281            if !category.is_local() && msg.decrement_ttl() == 0 {
1282                debug!(%conn_index, "dropping message: TTL expired");
1283                return Err(DataPathError::TtlExpired);
1284            }
1285
1286            #[cfg(feature = "otel_tracing")]
1287            otel_tracing::prepare_inbound_msg(
1288                &mut msg,
1289                "process_local",
1290                &self.internal.service_id,
1291                conn_index,
1292                category.is_local(),
1293            );
1294        }
1295
1296        match self.process_message(msg, conn_index, category).await {
1297            Ok(_) => Ok(()),
1298            Err(e) => {
1299                // telemetry /////////////////////////////////////////
1300                info!(
1301                    telemetry = true,
1302                    monotonic_counter.num_message_process_errors = 1
1303                );
1304                //////////////////////////////////////////////////////
1305
1306                // drop message
1307                Err(e)
1308            }
1309        }
1310    }
1311
1312    #[tracing::instrument(skip_all, fields(service_id = %self.internal.service_id, conn_index))]
1313    async fn send_error_to_local_app(&self, conn_index: u64, err: DataPathError) {
1314        debug!(%conn_index, "sending error to local application");
1315        let connection = self.forwarder().get_connection(conn_index);
1316        match connection {
1317            Some(conn) => {
1318                debug!("try to notify the error to the local application");
1319                if let Channel::Server(tx) = conn.channel() {
1320                    // If the error contains the message, try to extract some session information
1321                    let session_ctx = match &err {
1322                        DataPathError::MessageProcessingError { msg, .. } => {
1323                            MessageContext::from_msg(msg)
1324                        }
1325                        _ => None,
1326                    };
1327
1328                    // Make error message with optional session context using shared type
1329                    let payload = crate::errors::ErrorPayload::new(err.to_string(), session_ctx);
1330                    let error_message = payload.to_json_string();
1331
1332                    // create Status error
1333                    let status = Status::new(tonic::Code::Internal, error_message);
1334
1335                    if tx.send(Err(status)).await.is_err() {
1336                        debug!(error = %err.chain(), "unable to notify the error to the local app");
1337                    }
1338                }
1339            }
1340            None => {
1341                error!(
1342                    "error sending error to local app: connection {:?} not found",
1343                    conn_index
1344                );
1345            }
1346        }
1347    }
1348
1349    #[tracing::instrument(skip_all, fields(service_id = %self.internal.service_id, conn_index))]
1350    async fn reconnect(
1351        &self,
1352        client_conf: ClientConfig,
1353        conn_index: u64,
1354        cancellation_token: &CancellationToken,
1355    ) -> bool {
1356        info!("connection lost with remote endpoint, attempting to reconnect");
1357
1358        let is_peer = self
1359            .forwarder()
1360            .get_connection(conn_index)
1361            .map(|c| c.connection_type() == ConnType::Peer)
1362            .unwrap_or(false);
1363
1364        // For remote/controller connections: save the subscriptions we forwarded to this
1365        // connection so we can replay them after reconnecting.
1366        // For peer connections: we do a full sync instead (no need to save).
1367        let remote_subscriptions = if !is_peer {
1368            self.remote_sync()
1369                .get_subscriptions_for_reconnect(conn_index)
1370        } else {
1371            Default::default()
1372        };
1373
1374        tokio::select! {
1375            _ = cancellation_token.cancelled() => {
1376                debug!("cancellation token signaled, stopping reconnection process");
1377                false
1378            }
1379            res = self.try_to_connect(client_conf, None, None, Some(conn_index)) => {
1380                match res {
1381                    Ok(_) => {
1382                        info!("connection re-established successfully");
1383                        if is_peer {
1384                            // Peer connection: full sync (send local + remote subscriptions).
1385                            let ttl = self.peer_sync().subscription_ttl();
1386                            if let Err(e) = sync_peer::send_local_remote_sync(
1387                                self, conn_index, ttl,
1388                            )
1389                            .await
1390                            {
1391                                warn!(
1392                                    error = %e,
1393                                    "failed to send full sync after peer reconnect"
1394                                );
1395                            }
1396                        } else {
1397                            // Remote/controller: restore only what was previously forwarded.
1398                            self.restore_remote_subscriptions(
1399                                &remote_subscriptions,
1400                                conn_index,
1401                                false,
1402                            )
1403                            .await;
1404                        }
1405                        true
1406                    }
1407                    Err(e) => {
1408                        error!(error = %e.chain(), "unable to reconnect to remote node");
1409                        false
1410                    }
1411                }
1412            }
1413        }
1414    }
1415
1416    /// Send an UNSUBSCRIBE message to the control plane for each subscription in `local_subs`.
1417    ///
1418    /// This is the single authoritative place that constructs and delivers CP unsubscribe
1419    /// notifications on connection loss, used by both the immediate cleanup path and the deferred
1420    /// TTL-expiry path.
1421    async fn notify_control_plane_subscriptions_lost(
1422        tx_cp: Option<Sender<Result<Message, Status>>>,
1423        local_subs: HashMap<ProtoName, HashSet<u64>>,
1424        conn_index: u64,
1425    ) {
1426        let Some(tx) = tx_cp else { return };
1427        for local_sub in local_subs.into_keys() {
1428            debug!(
1429                %local_sub,
1430                "notify control plane about lost subscription",
1431            );
1432            let msg = Message::builder()
1433                .source(local_sub.clone())
1434                .destination(local_sub.clone())
1435                .flags(SlimHeaderFlags::default().with_recv_from(conn_index))
1436                .build_unsubscribe()
1437                .unwrap();
1438            if let Err(e) = tx.send(Ok(msg)).await {
1439                debug!(
1440                    %local_sub,
1441                    error = %e.chain(),
1442                    "failed to send unsubscribe to control plane",
1443                );
1444            }
1445        }
1446    }
1447
1448    /// Resolve the connection index for a new stream.
1449    ///
1450    /// For local (already-registered) connections, returns immediately.
1451    /// For remote connections, runs mandatory link negotiation, inserts the
1452    /// connection into the table, and handles peer upgrade if negotiated.
1453    ///
1454    /// Returns `Some((conn_index, category))` on success or `None` if the
1455    /// connection setup failed (the error is sent on `conn_index_tx`).
1456    async fn resolve_connection(
1457        &self,
1458        stream: &mut (impl Stream<Item = Result<Message, Status>> + Unpin + Send),
1459        setup: StreamSetup,
1460        category: ConnType,
1461        conn_index_tx: oneshot::Sender<Result<u64, DataPathError>>,
1462        watch: &drain::Watch,
1463        token: &CancellationToken,
1464    ) -> Option<(u64, ConnType)> {
1465        match setup {
1466            StreamSetup::Registered(idx) => {
1467                let _ = conn_index_tx.send(Ok(idx));
1468                Some((idx, category))
1469            }
1470            StreamSetup::Pending {
1471                connection,
1472                existing_index,
1473            } => {
1474                self.negotiate_and_register(
1475                    stream,
1476                    *connection,
1477                    existing_index,
1478                    category,
1479                    conn_index_tx,
1480                    watch,
1481                    token,
1482                )
1483                .await
1484            }
1485        }
1486    }
1487
1488    /// Perform link negotiation, register the connection, and handle peer upgrade.
1489    ///
1490    /// Returns `Some((conn_index, category))` on success or `None` on failure.
1491    #[allow(clippy::too_many_arguments)]
1492    async fn negotiate_and_register(
1493        &self,
1494        stream: &mut (impl Stream<Item = Result<Message, Status>> + Unpin + Send),
1495        mut connection: Connection,
1496        existing_index: Option<u64>,
1497        category: ConnType,
1498        conn_index_tx: oneshot::Sender<Result<u64, DataPathError>>,
1499        watch: &drain::Watch,
1500        token: &CancellationToken,
1501    ) -> Option<(u64, ConnType)> {
1502        let enforce_pqc = Self::resolve_enforce_pqc(&connection, &self.internal);
1503        let timeout = self.internal.negotiation_timeout;
1504        let params = crate::negotiation::NegotiationParams {
1505            node_id: &self.internal.service_id,
1506            deployment_name: &self.internal.deployment_name,
1507            connection_type: category,
1508            enforce_pqc,
1509        };
1510
1511        let negotiation_result = tokio::select! {
1512            result = tokio::time::timeout(
1513                timeout,
1514                crate::negotiation::run_negotiation(&mut connection, stream, &params),
1515            ) => match result {
1516                Ok(r) => r,
1517                Err(_) => Err(DataPathError::NegotiationError(
1518                    "timed out waiting for link negotiation".to_string(),
1519                )),
1520            },
1521            _ = watch.clone().signaled() => {
1522                info!("shutting down during link negotiation");
1523                let _ = conn_index_tx.send(Err(DataPathError::ShuttingDownError));
1524                return None;
1525            }
1526            _ = token.cancelled() => {
1527                info!("connection cancelled during link negotiation");
1528                let _ = conn_index_tx.send(Err(DataPathError::ShuttingDownError));
1529                return None;
1530            }
1531        };
1532
1533        let result = match negotiation_result {
1534            Ok(r) => r,
1535            Err(e) => {
1536                error!(error = %e.chain(), "link negotiation failed, closing connection");
1537                let _ = conn_index_tx.send(Err(e));
1538                info!(telemetry = true, counter.num_active_connections = -1);
1539                return None;
1540            }
1541        };
1542
1543        // Insert the fully-negotiated connection into the table.
1544        let idx = match self
1545            .forwarder()
1546            .on_connection_established(connection, existing_index)
1547        {
1548            Some(idx) => idx,
1549            None => {
1550                let _ = conn_index_tx.send(Err(DataPathError::ConnectionTableAddError));
1551                info!(telemetry = true, counter.num_active_connections = -1);
1552                return None;
1553            }
1554        };
1555
1556        debug!(%idx, "connection registered after link negotiation");
1557
1558        // Handle connection-type-specific post-negotiation logic.
1559        let link_id = self
1560            .forwarder()
1561            .get_connection(idx)
1562            .and_then(|c| c.link_id())
1563            .unwrap_or_default();
1564        let category = match result.connection_type {
1565            ConnType::Peer => {
1566                if let Err(e) = self
1567                    .handle_peer_upgrade(
1568                        &result.remote_node_id,
1569                        &result.remote_deployment_name,
1570                        idx,
1571                        &link_id,
1572                    )
1573                    .await
1574                {
1575                    error!(error = %e.chain(), "peer upgrade failed after negotiation");
1576                    let _ = conn_index_tx.send(Err(e));
1577                    info!(telemetry = true, counter.num_active_connections = -1);
1578                    return None;
1579                }
1580                ConnType::Peer
1581            }
1582            ConnType::Remote => {
1583                // Notify the controller that we received a remote incoming connection
1584                // so it can claim the link on the control-plane side.
1585                if let Some(tx) = self.get_tx_control_plane() {
1586                    let link = ProtoLink {
1587                        link_type: Some(ProtoLinkType::LinkNegotiation(LinkNegotiationPayload {
1588                            link_id,
1589                            ..Default::default()
1590                        })),
1591                    };
1592                    let msg = ProtoMessage {
1593                        metadata: Default::default(),
1594                        message_type: Some(LinkType(link)),
1595                    };
1596                    let _ = tx.send(Ok(msg)).await;
1597                }
1598                ConnType::Remote
1599            }
1600            ConnType::Edge => {
1601                self.connection_table().update(idx, |conn| {
1602                    conn.set_connection_type(ConnType::Edge);
1603                });
1604                ConnType::Edge
1605            }
1606            other => other,
1607        };
1608
1609        let _ = conn_index_tx.send(Ok(idx));
1610        Some((idx, category))
1611    }
1612
1613    fn process_stream(
1614        &self,
1615        mut stream: impl Stream<Item = Result<Message, Status>> + Unpin + Send + 'static,
1616        setup: StreamSetup,
1617        client_config: Option<ClientConfig>,
1618        cancellation_token: CancellationToken,
1619        category: ConnType,
1620        from_control_plane: bool,
1621    ) -> Result<
1622        (
1623            JoinHandle<()>,
1624            oneshot::Receiver<Result<u64, DataPathError>>,
1625        ),
1626        DataPathError,
1627    > {
1628        // Clone self to be able to move it into the spawned task
1629        let self_clone = self.clone();
1630        let token_clone = cancellation_token.clone();
1631        let client_conf_clone = client_config.clone();
1632        let tx_cp: Option<Sender<Result<Message, Status>>> = self.get_tx_control_plane();
1633        let watch = self.get_drain_watch()?;
1634        let is_local = category.is_local();
1635
1636        let (conn_index_tx, conn_index_rx) = oneshot::channel();
1637
1638        let require_header_mac = match &setup {
1639            StreamSetup::Registered(idx) => self
1640                .forwarder()
1641                .get_connection(*idx)
1642                .map(|c| c.require_header_mac())
1643                .unwrap_or(false),
1644            StreamSetup::Pending { connection, .. } => connection.require_header_mac(),
1645        };
1646
1647        let span = tracing::info_span!(
1648            "process_stream",
1649            service_id = %self.internal.service_id,
1650            conn_index = match &setup {
1651                StreamSetup::Registered(idx) => *idx,
1652                _ => 0,
1653            },
1654            is_local,
1655        );
1656
1657        let handle = crate::runtime::spawn(async move {
1658            let mut try_to_reconnect = true;
1659
1660            // Resolve the conn_index: either already registered (local) or
1661            // perform negotiation + table insertion (remote).
1662            let Some((conn_index, category)) = self_clone
1663                .resolve_connection(
1664                    &mut stream,
1665                    setup,
1666                    category,
1667                    conn_index_tx,
1668                    &watch,
1669                    &token_clone,
1670                )
1671                .await
1672            else {
1673                return;
1674            };
1675
1676            let mut watch = std::pin::pin!(watch.signaled());
1677            loop {
1678                tokio::select! {
1679                    next = stream.next() => {
1680                        match next {
1681                            Some(result) => {
1682                                match result {
1683                                    Ok(msg) => {
1684                                        if !is_local
1685                                            && !msg.is_link()
1686                                            && !msg.is_subscription_ack()
1687                                            && let Err(e) = self_clone
1688                                                .verify_remote_header_mac(conn_index, &msg, require_header_mac)
1689                                        {
1690                                            error!(
1691                                                %conn_index,
1692                                                error = %e.chain(),
1693                                                "SLIM header integrity verification failed",
1694                                            );
1695                                            continue;
1696                                        }
1697                                        // check if we need to send the message to the control plane
1698                                        // we send the message if
1699                                        // 1. the message is not coming from a local connection
1700                                        // 2. the connection is currently Remote (inter-group).
1701                                        // 3. it is not coming from the control plane itself
1702                                        // 4. the control plane exists
1703                                        let is_remote = !is_local
1704                                            && self_clone
1705                                                .forwarder()
1706                                                .get_connection(conn_index)
1707                                                .map(|c| matches!(c.connection_type(), ConnType::Remote | ConnType::Edge))
1708                                                .unwrap_or(false);
1709                                        if is_remote
1710                                            && !from_control_plane
1711                                            && let Some(txcp) = &tx_cp
1712                                        {
1713                                            let cp_msg = match msg.get_type() {
1714                                                // send subscription and unsubscription source
1715                                                // and destination to the control-plane
1716                                                SubscribeType(_) => ProtoMessage::builder()
1717                                                    .source(msg.get_source())
1718                                                    .destination(msg.get_dst())
1719                                                    .build_subscribe()
1720                                                    .ok(),
1721                                                UnsubscribeType(_) => ProtoMessage::builder()
1722                                                    .source(msg.get_source())
1723                                                    .destination(msg.get_dst())
1724                                                    .build_unsubscribe()
1725                                                    .ok(),
1726                                                _ => None
1727                                            };
1728                                            if let Some(m) = cp_msg {
1729                                                let _ = txcp.send(Ok(m)).await;
1730                                            }
1731                                        }
1732
1733                                        if let Err(e) = self_clone.handle_new_message(conn_index, category, msg).await {
1734                                            // Checking if NegotiationError occurred
1735                                            if matches!(e, DataPathError::NegotiationError(_)) {
1736                                                error!(%conn_index, "fatal link negotiation error, closing connection");
1737                                                try_to_reconnect = false;
1738                                                break;
1739                                            }
1740                                            debug!(%conn_index, error = %e.chain(), "error processing incoming message");
1741                                            // If the message is coming from a local app, notify it
1742                                            if is_local {
1743                                                // try to forward error to the local app
1744                                                self_clone.send_error_to_local_app(conn_index, e).await;
1745                                            }
1746                                        }
1747                                    }
1748                                    Err(e) => {
1749                                        if e.code() == tonic::Code::PermissionDenied {
1750                                            warn!(
1751                                                %conn_index,
1752                                                message = %e.message(),
1753                                                "connection rejected by remote, will not reconnect"
1754                                            );
1755                                            try_to_reconnect = false;
1756                                        } else if let Some(io_err) = MessageProcessor::match_for_io_error(&e) {
1757                                            if io_err.kind() == std::io::ErrorKind::BrokenPipe {
1758                                                info!(%conn_index, "connection closed by peer");
1759                                            }
1760                                        } else {
1761                                            error!(error = %e.chain(), "error receiving messages");
1762                                        }
1763                                        break;
1764                                    }
1765                                }
1766                            }
1767                            None => {
1768                                debug!(%conn_index, "end of stream");
1769                                break;
1770                            }
1771                        }
1772                    }
1773                    _ = &mut watch => {
1774                        info!(%conn_index, "shutting down stream on drain");
1775                        try_to_reconnect = false;
1776                        break;
1777                    }
1778                    _ = token_clone.cancelled() => {
1779                        info!(%conn_index, "shutting down stream on cancellation token");
1780                        try_to_reconnect = false;
1781                        break;
1782                    }
1783                }
1784            }
1785
1786            // we drop rx now as otherwise the connection will be closed only
1787            // when the task is dropped and we want to make sure that the rx
1788            // stream is closed as soon as possible
1789            drop(stream);
1790
1791            let mut connected = false;
1792
1793            if try_to_reconnect
1794                && !matches!(category, ConnType::Remote)
1795                && let Some(config) = client_conf_clone
1796            {
1797                // Break the span chain: reconnect → try_to_connect → process_stream
1798                // would otherwise nest under the current process_stream span on every
1799                // reconnection, growing the span hierarchy unboundedly.
1800                connected = self_clone.reconnect(config, conn_index, &token_clone)
1801                    .instrument(tracing::Span::none())
1802                    .await;
1803            } else {
1804                debug!(%conn_index, "close connection")
1805            }
1806
1807            if !connected {
1808                // Delete connection state from all tables.
1809                let local_subs = self_clone
1810                    .forwarder()
1811                    .on_connection_drop(conn_index, category);
1812                let _remote_subs = self_clone
1813                    .remote_sync()
1814                    .on_connection_drop(conn_index);
1815
1816                // Remove peer connection from forwarder's peer list if applicable.
1817                if matches!(category, ConnType::Peer) {
1818                    self_clone.peer_sync().remove_peer_conn(conn_index);
1819                }
1820
1821                // Notify peer sync about names that are no longer reachable.
1822                // For generic topologies (TTL-based relay), we also need to notify
1823                // when a peer drops — the seen_sub_ids tracking ensures we only
1824                // send unsubscribes for subscriptions we actually forwarded.
1825                {
1826                    let fwd = self_clone.peer_sync();
1827                    for name in local_subs.keys() {
1828                        let still_reachable = name.name.is_some_and(|enc| {
1829                            self_clone
1830                                .forwarder()
1831                                .on_publish_msg_match(enc, u64::MAX, u32::MAX, MatchFilter::ALL)
1832                                .is_ok()
1833                        });
1834                        if !still_reachable {
1835                            debug!(
1836                                %name,
1837                                %conn_index,
1838                                ?category,
1839                                "notifying peers of unsubscription (connection drop)"
1840                            );
1841                            fwd.notify_peers_unsubscribe(&self_clone, name).await;
1842                        } else {
1843                            debug!(
1844                                %name,
1845                                %conn_index,
1846                                ?category,
1847                                "name still reachable, not emitting removal"
1848                            );
1849                        }
1850                    }
1851                }
1852
1853
1854                // Notify the control plane about lost subscriptions.
1855                if !is_local {
1856                    MessageProcessor::notify_control_plane_subscriptions_lost(
1857                        tx_cp, local_subs, conn_index,
1858                    )
1859                    .await;
1860                }
1861
1862                info!(telemetry = true, counter.num_active_connections = -1);
1863            }
1864        }.instrument(span));
1865
1866        Ok((handle, conn_index_rx))
1867    }
1868
1869    fn match_for_io_error(err_status: &Status) -> Option<&std::io::Error> {
1870        let mut err: &(dyn std::error::Error + 'static) = err_status;
1871
1872        loop {
1873            if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
1874                return Some(io_err);
1875            }
1876
1877            // h2::Error do not expose std::io::Error with `source()`
1878            // https://github.com/hyperium/h2/pull/462
1879            // h2 is part of the native gRPC stack only.
1880            #[cfg(not(target_arch = "wasm32"))]
1881            if let Some(h2_err) = err.downcast_ref::<h2::Error>()
1882                && let Some(io_err) = h2_err.get_io()
1883            {
1884                return Some(io_err);
1885            }
1886
1887            err = err.source()?;
1888        }
1889    }
1890
1891    pub fn subscription_table(&self) -> &SubscriptionTableImpl {
1892        &self.internal.forwarder.subscription_table
1893    }
1894
1895    pub fn connection_table(&self) -> &ConnectionTable<Connection> {
1896        &self.internal.forwarder.connection_table
1897    }
1898
1899    /// The node identity used for cross-node communication.
1900    pub fn service_id(&self) -> &str {
1901        &self.internal.service_id
1902    }
1903
1904    /// Whether peer-originated publishes are relayed to other peers.
1905    pub fn relay_peer_publishes(&self) -> bool {
1906        self.internal.relay_peer_publishes
1907    }
1908
1909    /// Set the peer sync component.
1910    pub fn set_peer_sync(&self, peer_sync: crate::sync::PeerSync) {
1911        *self.internal.peer_sync.write() = peer_sync;
1912    }
1913
1914    /// Get a clone of the peer sync component.
1915    pub(crate) fn peer_sync(&self) -> crate::sync::PeerSync {
1916        self.internal.peer_sync.read().clone()
1917    }
1918
1919    fn resolve_enforce_pqc(connection: &Connection, internal: &MessageProcessorInternal) -> bool {
1920        #[cfg(not(target_arch = "wasm32"))]
1921        {
1922            connection
1923                .config_data()
1924                .map(|c| c.tls_setting.config.enforce_pqc)
1925                .unwrap_or(internal.server_enforce_pqc)
1926        }
1927        #[cfg(target_arch = "wasm32")]
1928        {
1929            // ponytail: wasm has no tls_setting; deployment policy lives on MessageProcessor.
1930            let _ = connection;
1931            internal.server_enforce_pqc
1932        }
1933    }
1934}
1935
1936#[cfg(not(target_arch = "wasm32"))]
1937impl ServerHandler for MessageProcessor {
1938    fn grpc_routes(&self) -> Option<tonic::service::Routes> {
1939        let svc = DataPlaneServiceServer::from_arc(Arc::new(self.clone()));
1940        Some(tonic::service::Routes::new(svc))
1941    }
1942
1943    fn on_websocket_accepted(&self) -> Option<websocket_server::OnAcceptedWebSocket> {
1944        let processor = self.clone();
1945        Some(Arc::new(move |accepted| {
1946            let processor = processor.clone();
1947            Box::pin(async move { processor.handle_websocket_accepted(accepted).await })
1948        }))
1949    }
1950}
1951
1952#[cfg(not(target_arch = "wasm32"))]
1953#[tonic::async_trait]
1954impl DataPlaneService for MessageProcessor {
1955    type OpenChannelStream = Pin<Box<dyn Stream<Item = Result<Message, Status>> + Send + 'static>>;
1956
1957    async fn open_channel(
1958        &self,
1959        request: Request<tonic::Streaming<Message>>,
1960    ) -> Result<Response<Self::OpenChannelStream>, Status> {
1961        let remote_addr = request.remote_addr();
1962        let local_addr = request.local_addr();
1963
1964        let stream = request.into_inner();
1965        let (tx, rx) = mpsc::channel(128);
1966
1967        let connection = Connection::new(ConnType::Remote, Channel::Server(tx))
1968            .with_remote_addr(remote_addr)
1969            .with_local_addr(local_addr)
1970            .with_require_header_mac(self.internal.server_require_header_mac);
1971
1972        debug!(
1973            remote = ?connection.remote_addr(),
1974            local = ?connection.local_addr(),
1975            "new connection received from remote",
1976        );
1977        info!(telemetry = true, counter.num_active_connections = 1);
1978
1979        self.process_stream(
1980            stream,
1981            StreamSetup::Pending {
1982                connection: Box::new(connection),
1983                existing_index: None,
1984            },
1985            None,
1986            CancellationToken::new(),
1987            ConnType::Remote,
1988            false,
1989        )
1990        .map_err(|e| {
1991            error!(error = %e.chain(), "error starting new processing stream");
1992            Status::unavailable(format!("error processing stream: {:?}", e))
1993        })?;
1994
1995        let out_stream = ReceiverStream::new(rx);
1996        Ok(Response::new(
1997            Box::pin(out_stream) as Self::OpenChannelStream
1998        ))
1999    }
2000}
2001
2002#[cfg(test)]
2003mod tests {
2004    use std::time::Duration;
2005
2006    use super::*;
2007    use crate::api::{ProtoMessage, ProtoName, ProtoSubscriptionAck};
2008    use crate::header_mac::HeaderMacSession;
2009    use crate::sync::remote::SubscriptionInfo;
2010    use tonic::Status;
2011
2012    async fn assert_failed_subscription_ack_is_sent(add: bool) {
2013        let processor = MessageProcessor::new();
2014        let (in_connection, _tx, mut rx) = processor
2015            .register_local_connection(false)
2016            .expect("failed to create local connection");
2017
2018        let source = ProtoName::from_strings(["org", "ns", "source"]).with_id(1);
2019        let destination = ProtoName::from_strings(["org", "ns", "destination"]).with_id(2);
2020        let ack_id: u64 = if add { 1 } else { 2 };
2021        let invalid_connection = u64::MAX - 1;
2022
2023        let builder = Message::builder()
2024            .source(source.clone())
2025            .destination(destination.clone())
2026            .incoming_conn(invalid_connection)
2027            .subscription_id(ack_id);
2028
2029        let msg = if add {
2030            builder.build_subscribe().unwrap()
2031        } else {
2032            builder.build_unsubscribe().unwrap()
2033        };
2034
2035        let result = processor
2036            .process_subscription(msg, in_connection, add)
2037            .await;
2038        assert!(matches!(
2039            result,
2040            Err(DataPathError::MessageProcessingError { .. })
2041        ));
2042
2043        let ack_msg = tokio::time::timeout(Duration::from_secs(1), rx.recv())
2044            .await
2045            .expect("timeout waiting for ack")
2046            .expect("ack channel closed")
2047            .expect("failed to receive ack message");
2048
2049        assert!(matches!(ack_msg.get_type(), SubscriptionAckType(_)));
2050        let ack = ack_msg.get_subscription_ack();
2051        assert_eq!(ack.subscription_id, ack_id);
2052        assert!(!ack.success, "failed ack should have success=false");
2053        assert!(
2054            !ack.error.is_empty(),
2055            "failed ack should include an error message"
2056        );
2057    }
2058
2059    #[tokio::test]
2060    async fn test_process_subscription_sends_failed_ack_on_subscribe_error() {
2061        assert_failed_subscription_ack_is_sent(true).await;
2062    }
2063
2064    #[tokio::test]
2065    async fn test_process_subscription_sends_failed_ack_on_unsubscribe_error() {
2066        assert_failed_subscription_ack_is_sent(false).await;
2067    }
2068
2069    // ── handle_link_message ───────────────────────────────────────────────────
2070
2071    #[tokio::test]
2072    async fn test_handle_link_message_is_local_ignored() {
2073        let processor = MessageProcessor::new();
2074        let link = ProtoLink { link_type: None };
2075        assert!(
2076            processor
2077                .handle_link_message(link, 0, ConnType::Local)
2078                .await
2079                .is_ok()
2080        );
2081    }
2082
2083    #[tokio::test]
2084    async fn test_handle_link_message_none_link_type_ignored() {
2085        let processor = MessageProcessor::new();
2086        let link = ProtoLink { link_type: None };
2087        assert!(
2088            processor
2089                .handle_link_message(link, 0, ConnType::Remote)
2090                .await
2091                .is_ok()
2092        );
2093    }
2094
2095    // ── handle_link_negotiation ───────────────────────────────────────────────
2096
2097    /// After negotiation completes and the connection is inserted into the table,
2098    /// any further link negotiation messages arriving in the main loop are simply
2099    /// logged and ignored (the handler is a no-op). This test verifies that.
2100    #[tokio::test]
2101    async fn test_handle_link_negotiation_post_negotiation_is_noop() {
2102        let processor = MessageProcessor::new();
2103        let payload = LinkNegotiationPayload {
2104            link_id: uuid::Uuid::new_v4().to_string(),
2105            slim_version: "1.0.0".into(),
2106            is_reply: false,
2107            link_ecdh_public_key: vec![],
2108            link_kem_payload: None,
2109            connection_type: 0,
2110            node_id: String::new(),
2111            deployment_name: String::new(),
2112        };
2113        // Unknown connection: handler returns Ok without panic.
2114        assert!(
2115            processor
2116                .handle_link_negotiation(&payload, u64::MAX)
2117                .await
2118                .is_ok()
2119        );
2120        // Known connection: handler still returns Ok (noop).
2121        let (conn_id, _rx) = make_negotiated_server_conn(&processor, "1.2.0");
2122        assert!(
2123            processor
2124                .handle_link_negotiation(&payload, conn_id)
2125                .await
2126                .is_ok()
2127        );
2128    }
2129
2130    // ── process_subscription: remote ack path ─────────────────────────────────
2131
2132    /// Helper: create a server connection that is already negotiated with given version and
2133    /// a test HMAC session, suitable for testing routing and MAC verification.
2134    fn make_negotiated_server_conn(
2135        processor: &MessageProcessor,
2136        version: &str,
2137    ) -> (u64, tokio::sync::mpsc::Receiver<Result<Message, Status>>) {
2138        let (tx, rx) = mpsc::channel(16);
2139        let conn = Connection::new(ConnType::Remote, Channel::Server(tx))
2140            .with_require_header_mac(processor.internal.server_require_header_mac)
2141            .with_negotiation(&uuid::Uuid::new_v4().to_string(), version)
2142            .with_header_hmac(HeaderMacSession::new(b"01234567890123456789012345678901").unwrap());
2143        let conn_id = processor
2144            .forwarder()
2145            .on_connection_established(conn, None)
2146            .unwrap();
2147        (conn_id, rx)
2148    }
2149
2150    #[tokio::test]
2151    async fn test_negotiation_timeout_configurable() {
2152        let server_config = ServerConfig {
2153            endpoint: "localhost:12345".to_string(),
2154            negotiation_timeout_secs: 1, // 1 second timeout
2155            ..Default::default()
2156        };
2157        let processor = MessageProcessor::new_with_server_config(
2158            "test_service".to_string(),
2159            String::new(),
2160            &server_config,
2161            false,
2162            false,
2163        );
2164
2165        assert_eq!(
2166            processor.internal.negotiation_timeout,
2167            std::time::Duration::from_secs(1)
2168        );
2169    }
2170
2171    #[test]
2172    fn verify_remote_header_mac_strict_rejects_publish_without_mac_session() {
2173        let processor = MessageProcessor::new();
2174        // Create a negotiated connection WITHOUT header HMAC installed.
2175        let (tx, _rx) = mpsc::channel(16);
2176        let conn = Connection::new(ConnType::Remote, Channel::Server(tx))
2177            .with_require_header_mac(true)
2178            .with_negotiation(&uuid::Uuid::new_v4().to_string(), "1.2.0");
2179        let remote_conn = processor
2180            .forwarder()
2181            .on_connection_established(conn, None)
2182            .unwrap();
2183        let c = processor.forwarder().get_connection(remote_conn).unwrap();
2184        assert!(c.header_hmac().is_none());
2185
2186        let source = ProtoName::from_strings(["org", "default", "a"]).with_id(1);
2187        let dest = ProtoName::from_strings(["org", "default", "b"]).with_id(2);
2188        let msg = ProtoMessage::builder()
2189            .source(source)
2190            .destination(dest)
2191            .application_payload("text/plain", b"hey".to_vec())
2192            .build_publish()
2193            .expect("publish");
2194
2195        let err = processor
2196            .verify_remote_header_mac(remote_conn, &msg, true)
2197            .expect_err("unsigned publish must fail in strict mode without MAC session");
2198        assert!(matches!(err, DataPathError::NegotiationError(_)));
2199    }
2200
2201    #[test]
2202    fn verify_remote_header_mac_accepts_signed_inter_node_publish() {
2203        let processor = MessageProcessor::new();
2204        let (remote_conn, _rx) = make_negotiated_server_conn(&processor, "1.2.0");
2205        let link_id = processor
2206            .forwarder()
2207            .get_connection(remote_conn)
2208            .unwrap()
2209            .link_id()
2210            .expect("link id after negotiation");
2211
2212        let source = ProtoName::from_strings(["org", "default", "a"]).with_id(1);
2213        let dest = ProtoName::from_strings(["org", "default", "b"]).with_id(2);
2214        let require_header_mac = true;
2215        let mut msg = ProtoMessage::builder()
2216            .source(source)
2217            .destination(dest)
2218            .application_payload("text/plain", b"hey".to_vec())
2219            .build_publish()
2220            .expect("publish");
2221
2222        let mac = HeaderMacSession::new(b"01234567890123456789012345678901").unwrap();
2223        mac.sign_slim_header(msg.get_slim_header_mut(), &link_id)
2224            .expect("sign header");
2225
2226        assert!(
2227            processor
2228                .verify_remote_header_mac(remote_conn, &msg, require_header_mac)
2229                .is_ok()
2230        );
2231    }
2232
2233    #[test]
2234    fn verify_remote_header_mac_rejects_destination_tamper_after_sign() {
2235        let processor = MessageProcessor::new();
2236        let (remote_conn, _rx) = make_negotiated_server_conn(&processor, "1.2.0");
2237        let link_id = processor
2238            .forwarder()
2239            .get_connection(remote_conn)
2240            .unwrap()
2241            .link_id()
2242            .expect("link id after negotiation");
2243
2244        let source = ProtoName::from_strings(["org", "default", "a"]).with_id(1);
2245        let dest = ProtoName::from_strings(["org", "default", "b"]).with_id(2);
2246        let mut msg = ProtoMessage::builder()
2247            .source(source)
2248            .destination(dest)
2249            .application_payload("text/plain", b"hey".to_vec())
2250            .build_publish()
2251            .expect("publish");
2252
2253        let mac = HeaderMacSession::new(b"01234567890123456789012345678901").unwrap();
2254        let require_header_mac = true;
2255        mac.sign_slim_header(msg.get_slim_header_mut(), &link_id)
2256            .expect("sign header");
2257
2258        let header = msg.get_slim_header_mut();
2259        if let Some(dest) = header.destination.as_mut()
2260            && let Some(sn) = dest.str_name.as_mut()
2261        {
2262            sn.str_component_2.push_str("-integrity-test-tamper");
2263        }
2264
2265        let err = processor
2266            .verify_remote_header_mac(remote_conn, &msg, require_header_mac)
2267            .expect_err("tampered header must fail MAC verify");
2268        assert!(matches!(err, DataPathError::HeaderIntegrity(_)));
2269    }
2270
2271    #[tokio::test]
2272    #[allow(clippy::disallowed_methods)]
2273    async fn test_send_msg_raw_tamper_destination_env_var() {
2274        let _guard = ENV_LOCK.lock().await;
2275        unsafe {
2276            std::env::set_var("SLIM_TEST_TAMPER_DESTINATION", "1");
2277        }
2278
2279        let processor = MessageProcessor::new();
2280        let (conn_id, mut rx) = make_negotiated_server_conn(&processor, "1.2.0");
2281
2282        let source = ProtoName::from_strings(["org", "default", "a"]).with_id(1);
2283        let dest = ProtoName::from_strings(["org", "default", "b"]).with_id(2);
2284        let msg = ProtoMessage::builder()
2285            .source(source)
2286            .destination(dest)
2287            .application_payload("text/plain", b"hey".to_vec())
2288            .build_publish()
2289            .expect("publish");
2290
2291        processor
2292            .send_msg_raw(msg, conn_id)
2293            .await
2294            .expect("send_msg_raw failed");
2295
2296        let sent_msg = rx.recv().await.unwrap().unwrap();
2297        let header = sent_msg.get_slim_header();
2298        let dest_name = header.destination.as_ref().expect("destination");
2299        let str_name = dest_name.str_name.as_ref().expect("str_name");
2300        let require_header_mac = true;
2301
2302        // The tampering happens in send_msg_raw if the env var is set.
2303        assert!(str_name.str_component_2.ends_with("-integrity-test-tamper"));
2304
2305        // Also verify that verify_remote_header_mac rejects it.
2306        let err = processor
2307            .verify_remote_header_mac(conn_id, &sent_msg, require_header_mac)
2308            .expect_err("tampered header must fail MAC verify");
2309        assert!(matches!(err, DataPathError::HeaderIntegrity(_)));
2310
2311        unsafe {
2312            std::env::remove_var("SLIM_TEST_TAMPER_DESTINATION");
2313        }
2314    }
2315
2316    #[tokio::test]
2317    async fn test_process_subscription_remote_ack_path_success() {
2318        // Arrange: relay processor, local app connection, and a "remote" server
2319        // connection whose version is ≥ 1.2.0.
2320        let processor = MessageProcessor::new();
2321        let (local_conn, _tx_local, mut rx_local) = processor
2322            .register_local_connection(false)
2323            .expect("failed to create local connection");
2324
2325        let (remote_conn, mut rx_remote) = make_negotiated_server_conn(&processor, "1.2.0");
2326
2327        let source = ProtoName::from_strings(["org", "ns", "src"]).with_id(1);
2328        let destination = ProtoName::from_strings(["org", "ns", "dst"]).with_id(2);
2329        let upstream_ack_id: u64 = 100;
2330
2331        // Build subscribe: forward_to = remote_conn, with upstream ack ID.
2332        let sub_msg = Message::builder()
2333            .source(source.clone())
2334            .destination(destination.clone())
2335            .incoming_conn(local_conn)
2336            .forward_to(remote_conn)
2337            .subscription_id(upstream_ack_id)
2338            .build_subscribe()
2339            .unwrap();
2340
2341        // Act: process_subscription should spawn the retry task and return Ok(()).
2342        let result = processor
2343            .process_subscription(sub_msg, local_conn, true)
2344            .await;
2345        assert!(result.is_ok());
2346
2347        // The relay must have forwarded the subscribe to the remote connection.
2348        // Give the spawned task a moment to send the message.
2349        let forwarded = tokio::time::timeout(Duration::from_secs(1), rx_remote.recv())
2350            .await
2351            .expect("timeout waiting for forwarded subscribe")
2352            .expect("forwarded subscribe channel closed")
2353            .unwrap();
2354        assert!(matches!(forwarded.get_type(), SubscribeType(_)));
2355
2356        // The forwarded message must carry the same subscription_id as the original.
2357        let forwarded_sub_id = forwarded
2358            .get_subscription_id()
2359            .expect("forwarded subscribe must carry the same subscription_id");
2360        assert_eq!(
2361            forwarded_sub_id, upstream_ack_id,
2362            "subscription_id must not change when forwarding"
2363        );
2364
2365        // Simulate the remote node sending back a success SubscriptionAck.
2366        let ack = ProtoSubscriptionAck {
2367            subscription_id: upstream_ack_id,
2368            success: true,
2369            error: String::new(),
2370        };
2371        processor.peer_sync().resolve_ack(
2372            ack.subscription_id,
2373            if ack.success {
2374                Ok(())
2375            } else {
2376                Err(DataPathError::RemoteSubscriptionAckError(ack.error.clone()))
2377            },
2378        );
2379
2380        // The relay must now forward the upstream ACK to the local connection.
2381        let upstream_ack = tokio::time::timeout(Duration::from_secs(2), rx_local.recv())
2382            .await
2383            .expect("timeout waiting for upstream ack")
2384            .expect("upstream ack channel closed")
2385            .expect("upstream ack should be Ok");
2386
2387        assert!(matches!(upstream_ack.get_type(), SubscriptionAckType(_)));
2388        let ack_inner = upstream_ack.get_subscription_ack();
2389        assert_eq!(ack_inner.subscription_id, upstream_ack_id);
2390        assert!(ack_inner.success);
2391    }
2392
2393    #[tokio::test]
2394    async fn test_process_subscription_remote_ack_error_forwarded_upstream() {
2395        // Remote node (v1.2.0) sends back a failure ACK; relay must forward it upstream.
2396        let processor = MessageProcessor::new();
2397        let (local_conn, _tx_local, mut rx_local) = processor
2398            .register_local_connection(false)
2399            .expect("failed to create local connection");
2400
2401        let (remote_conn, mut rx_remote) = make_negotiated_server_conn(&processor, "1.2.0");
2402
2403        let source = ProtoName::from_strings(["org", "ns", "src"]).with_id(1);
2404        let destination = ProtoName::from_strings(["org", "ns", "dst"]).with_id(2);
2405        let upstream_ack_id: u64 = 102;
2406
2407        let sub_msg = Message::builder()
2408            .source(source.clone())
2409            .destination(destination.clone())
2410            .incoming_conn(local_conn)
2411            .forward_to(remote_conn)
2412            .subscription_id(upstream_ack_id)
2413            .build_subscribe()
2414            .unwrap();
2415
2416        processor
2417            .process_subscription(sub_msg, local_conn, true)
2418            .await
2419            .unwrap();
2420
2421        let forwarded = tokio::time::timeout(Duration::from_secs(1), rx_remote.recv())
2422            .await
2423            .expect("timeout")
2424            .expect("channel closed")
2425            .unwrap();
2426
2427        let forwarded_sub_id = forwarded
2428            .get_subscription_id()
2429            .expect("forwarded subscribe must carry the same subscription_id");
2430        assert_eq!(
2431            forwarded_sub_id, upstream_ack_id,
2432            "subscription_id must not change when forwarding"
2433        );
2434
2435        // Simulate remote failure via SubscriptionAck.
2436        let ack = ProtoSubscriptionAck {
2437            subscription_id: upstream_ack_id,
2438            success: false,
2439            error: "remote error".to_string(),
2440        };
2441        processor.peer_sync().resolve_ack(
2442            ack.subscription_id,
2443            if ack.success {
2444                Ok(())
2445            } else {
2446                Err(DataPathError::RemoteSubscriptionAckError(ack.error.clone()))
2447            },
2448        );
2449
2450        let upstream_ack = tokio::time::timeout(Duration::from_secs(2), rx_local.recv())
2451            .await
2452            .expect("timeout")
2453            .expect("channel closed")
2454            .expect("must be Ok");
2455
2456        assert!(matches!(upstream_ack.get_type(), SubscriptionAckType(_)));
2457        let ack_inner = upstream_ack.get_subscription_ack();
2458        assert_eq!(ack_inner.subscription_id, upstream_ack_id);
2459        assert!(!ack_inner.success);
2460        assert!(!ack_inner.error.is_empty());
2461    }
2462
2463    // ── notify_control_plane_subscriptions_lost ───────────────────────────────
2464
2465    #[tokio::test]
2466    async fn test_notify_cp_subs_lost_sends_unsubscribes() {
2467        let (tx, mut rx) = mpsc::channel::<Result<Message, Status>>(16);
2468        let mut subs = HashMap::new();
2469        let name = ProtoName::from_strings(["org", "default", "svc"]);
2470        subs.insert(name.clone(), HashSet::from([1u64, 2u64]));
2471
2472        MessageProcessor::notify_control_plane_subscriptions_lost(Some(tx), subs, 42).await;
2473
2474        let msg = rx.recv().await.unwrap().unwrap();
2475        assert!(matches!(msg.get_type(), UnsubscribeType(_)));
2476        assert_eq!(msg.get_source(), name.clone());
2477    }
2478
2479    #[tokio::test]
2480    async fn test_notify_cp_subs_lost_no_tx_is_noop() {
2481        let subs = HashMap::from([(
2482            ProtoName::from_strings(["org", "default", "svc"]),
2483            HashSet::from([1u64]),
2484        )]);
2485        // Should not panic or hang.
2486        MessageProcessor::notify_control_plane_subscriptions_lost(None, subs, 1).await;
2487    }
2488
2489    #[tokio::test]
2490    async fn test_notify_cp_subs_lost_empty_subs() {
2491        let (tx, mut rx) = mpsc::channel::<Result<Message, Status>>(16);
2492        MessageProcessor::notify_control_plane_subscriptions_lost(Some(tx), HashMap::new(), 1)
2493            .await;
2494        // No messages should be sent.
2495        assert!(rx.try_recv().is_err());
2496    }
2497
2498    // ── restore_remote_subscriptions ──────────────────────────────────────────
2499
2500    #[tokio::test]
2501    async fn test_restore_remote_subscriptions_with_tracking() {
2502        let processor = MessageProcessor::new();
2503        let (conn_id, mut rx) = make_negotiated_server_conn(&processor, "1.2.0");
2504
2505        let source = ProtoName::from_strings(["org", "default", "src"]);
2506        let dest = ProtoName::from_strings(["org", "default", "dst"]);
2507        let sub = SubscriptionInfo::new(source.clone(), dest.clone(), "id1".into(), conn_id, 7);
2508        let subs = HashSet::from([sub]);
2509
2510        processor
2511            .restore_remote_subscriptions(&subs, conn_id, true)
2512            .await;
2513
2514        // The subscribe message should have been sent.
2515        let msg = rx.recv().await.unwrap().unwrap();
2516        assert!(matches!(msg.get_type(), SubscribeType(_)));
2517
2518        // With restore_tracking=true, the forwarded subscription should be tracked.
2519        let tracked = processor
2520            .remote_sync()
2521            .get_subscriptions_for_reconnect(conn_id);
2522        assert_eq!(tracked.len(), 1);
2523    }
2524
2525    #[tokio::test]
2526    async fn test_restore_remote_subscriptions_without_tracking() {
2527        let processor = MessageProcessor::new();
2528        let (conn_id, mut rx) = make_negotiated_server_conn(&processor, "1.2.0");
2529
2530        let source = ProtoName::from_strings(["org", "default", "src"]);
2531        let dest = ProtoName::from_strings(["org", "default", "dst"]);
2532        let sub = SubscriptionInfo::new(source.clone(), dest.clone(), "id1".into(), conn_id, 7);
2533        let subs = HashSet::from([sub]);
2534
2535        processor
2536            .restore_remote_subscriptions(&subs, conn_id, false)
2537            .await;
2538
2539        // Message sent.
2540        let msg = rx.recv().await.unwrap().unwrap();
2541        assert!(matches!(msg.get_type(), SubscribeType(_)));
2542
2543        // With restore_tracking=false, forwarded subscription table should NOT be updated.
2544        let tracked = processor
2545            .remote_sync()
2546            .get_subscriptions_for_reconnect(conn_id);
2547        assert!(tracked.is_empty());
2548    }
2549}