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