Skip to main content

slim_datapath/
message_processing.rs

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