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