Skip to content

feat(websocket): Add WebSocket transport and WASM support for the data-plane#1337

Open
hackeramitkumar wants to merge 47 commits into
agntcy:mainfrom
hackeramitkumar:feat/websocket-server-client
Open

feat(websocket): Add WebSocket transport and WASM support for the data-plane#1337
hackeramitkumar wants to merge 47 commits into
agntcy:mainfrom
hackeramitkumar:feat/websocket-server-client

Conversation

@hackeramitkumar

@hackeramitkumar hackeramitkumar commented Mar 7, 2026

Copy link
Copy Markdown
Member

Summary

This PR introduces WebSocket as a new transport protocol for the SLIM data-plane alongside the existing gRPC transport, and adds WebAssembly (WASM) support to enable running SLIM clients in the browser.

What's Changed

WebSocket Transport (Server & Client)

  • New websocket config module with server and client implementations (config/src/websocket/)
  • WebSocket server using hyper + fastwebsockets with TLS/WSS support
  • WebSocket client with TLS, auth (Bearer token, query-param), and reconnect support
  • WebSocket datapath stream adapter (datapath/src/websocket/stream.rs)
  • Sample configs: client-config-debug.yaml, server-config-debug.yaml, client-config-wss.yaml, server-config-wss.yaml

WASM Support

  • New slim-wasm crate (core/slim-wasm/) — a wasm-bindgen entry point for running SLIM in the browser
  • WASM-specific WebSocket client and common modules (client_wasm.rs, common_wasm.rs)
  • wasm and native feature flags across all core crates (auth, config, session, datapath, mls, service, tracing, signal, etc.)
  • Tracing split into native.rs (tokio-based) and wasm.rs (console-based)
  • MLS crypto abstraction (mls/src/crypto.rs) for WASM-compatible crypto backends
  • Async runtime abstraction (session/src/runtime.rs) for native vs WASM task spawning
  • Browser demo app (examples/browser/index.html)

Config Refactoring

  • Extracted transport-agnostic ClientConfig and ServerConfig into config/src/client.rs and config/src/server.rs (previously embedded in grpc/)
  • Schema files moved up from grpc/schema/ to schema/

CI / Toolchain

  • Upgraded LLVM toolchain from 19 to 20 (Dockerfile, .cargo/config.toml, GitHub Actions workflows) — LLVM 19 is no longer available on apt.llvm.org for Debian bookworm
  • Override LLVM toolchain env vars for legacy binary builds in integration tests
  • New Taskfile targets for wasm and websocket builds

Type of Change

  • Bugfix
  • New Feature
  • Breaking Change
  • Refactor
  • Documentation
  • Other (please describe)

Checklist

  • I have read the contributing guidelines
  • Existing issues have been referenced (where applicable)
  • I have verified this change is not present in other open pull requests
  • Functionality is documented
  • All code style checks pass
  • New code contribution is covered by automated tests
  • All new and existing tests pass

Fixes #1312

Comment thread data-plane/core/config/src/client.rs Fixed
Comment thread data-plane/core/config/src/client.rs Fixed
Comment thread data-plane/core/config/src/client.rs Fixed
@amitami2 amitami2 force-pushed the feat/websocket-server-client branch from eff05d7 to 3419b5e Compare March 9, 2026 14:05
Comment thread data-plane/core/config/src/websocket/common.rs Fixed
Comment thread data-plane/core/config/src/websocket/common.rs Fixed
Comment thread data-plane/core/config/src/websocket/common.rs Fixed
Comment thread data-plane/core/config/src/websocket/common.rs Fixed
Comment thread data-plane/core/config/src/websocket/server.rs Fixed
@amitami2 amitami2 force-pushed the feat/websocket-server-client branch from fec5693 to 4be697a Compare April 4, 2026 12:52
Comment thread .github/workflows/data-plane.yaml Fixed
Comment thread .github/workflows/data-plane.yaml Fixed
@amitami2 amitami2 force-pushed the feat/websocket-server-client branch from 18fe86d to 6be330a Compare April 6, 2026 17:16
@hackeramitkumar hackeramitkumar changed the title feat(websocket): add implementation of websocket client and server feat(websocket): Add WebSocket transport and WASM support for the data-plane Apr 6, 2026
@hackeramitkumar hackeramitkumar marked this pull request as ready for review April 6, 2026 18:21
@hackeramitkumar hackeramitkumar requested a review from a team as a code owner April 6, 2026 18:21
@amitami2 amitami2 force-pushed the feat/websocket-server-client branch 3 times, most recently from 73deffa to 200933a Compare April 17, 2026 04:10
})
.await;
}
Err(err) => {

Check notice

Code scanning / CodeQL

Unused variable Note

Variable 'err' is not used.

Ok::<Response<Empty<Bytes>>, Infallible>(response)
}
Err(err) => {

Check notice

Code scanning / CodeQL

Unused variable Note

Variable 'err' is not used.
}
};

if let Err(err) = processor.process_stream(

Check notice

Code scanning / CodeQL

Unused variable Note

Variable 'err' is not used.
warn!(%session_id, "requested to delete unknown session");
}
}
Some(Ok(m)) => {

Check notice

Code scanning / CodeQL

Unused variable Note

Variable 'm' is not used.
Some(Ok(m)) => {
error!(?m, "received unexpected message");
}
Some(Err(e)) => {

Check notice

Code scanning / CodeQL

Unused variable Note

Variable 'e' is not used.
@amitami2 amitami2 force-pushed the feat/websocket-server-client branch 3 times, most recently from d7634d1 to bae4e58 Compare April 25, 2026 12:13
@amitami2 amitami2 force-pushed the feat/websocket-server-client branch from bae4e58 to de0dc5a Compare May 4, 2026 03:51
break;
}
}
Err(err) => {

Check notice

Code scanning / CodeQL

Unused variable Note

Variable 'err' is not used.
Some(Ok(gloo_net::websocket::Message::Text(_))) => {
warn!("ignoring text websocket frame, expected binary protobuf frame");
}
Some(Err(err)) => {

Check notice

Code scanning / CodeQL

Unused variable Note

Variable 'err' is not used.
};

let payload = msg.encode_to_vec();
if let Err(err) = sink

Check notice

Code scanning / CodeQL

Unused variable Note

Variable 'err' is not used.
@amitami2 amitami2 force-pushed the feat/websocket-server-client branch from 0009130 to c6d2bde Compare May 8, 2026 02:10
amitami2 added 10 commits May 11, 2026 01:24
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
amitami2 added 28 commits May 11, 2026 01:26
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Made-with: Cursor
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
Signed-off-by: amitami2 <amitami2@cisco.com>
@amitami2 amitami2 force-pushed the feat/websocket-server-client branch from 0ea19c2 to ba70104 Compare May 10, 2026 21:49

@msardara msardara left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work Amit! Some nits to fix but it looks good overall.

) -> WebSocketStreams {
let (mut read_half, mut write_half) = websocket.split(tokio::io::split);
read_half.set_auto_close(false);
read_half.set_auto_pong(false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to set this to true? Having it set to true will be beneficial if there are middleboxes suvh as ingresses or forward proxies, as they might interrupt the connection in case of no traffic.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understood. I will set it to true.

cancellation_token: CancellationToken,
) -> WebSocketStreams {
let (mut read_half, mut write_half) = websocket.split(tokio::io::split);
read_half.set_auto_close(false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be set to true I think, otherwise we need to handle the websocket close frames

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure will do that


let read_cancel = cancellation_token.clone();
tokio::spawn(async move {
let mut noop_send = |_frame: Frame<'_>| async move { Result::<(), WebSocketError>::Ok(()) };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This closure should send auto close and auto pong frames. A noop_send will not be able to send the control frames.

You can do something in this line:

let (tx_control, mut rx_control) = mpsc::channel::<Vec<u8>>(4);

// In the read task:
let mut send_pong = |frame: Frame<'_>| {
    let tx = tx_control.clone();
    let payload = frame.payload.to_vec();
    async move {
        let _ = tx.send(payload).await;
        Ok(())
    }
};
reader.read_frame(&mut send_pong)

// In the write task, select on both rx_outbound and rx_control:
tokio::select! {
    Some(pong) = rx_control.recv() => {
        write_half.write_frame(Frame::pong(pong.into())).await?;
    }
    Some(msg) = rx_outbound.recv() => {
        // existing write logic
    }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense. I think we can handle both close/pong types of control signals here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this modified because of some limitation on the wasm side?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible I'd try to integrate the wasm part also in the data plane and reuse SLIM as it is without creating a custom session layer using a websocket as SLIM connection. Do you think it is feasible?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I willl check it.

Comment on lines +147 to +156
/// Replace the MLS signature key pair with externally-generated keys.
/// Used by WASM builds where keys must be generated by the MLS crypto
/// provider (WebCrypto) rather than the identity provider.
fn set_signature_keys(
&mut self,
_private_key: Vec<u8>,
_public_key: Vec<u8>,
) -> Result<(), AuthError> {
Err(AuthError::MlsNotSupported)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

return Ok(());
}

#[cfg(any(feature = "native", feature = "wasm"))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed I think.

Comment on lines 4 to 17
pub mod basic;
#[cfg(feature = "native")]
pub mod identity;
#[cfg(feature = "native")]
pub mod jwt;
#[cfg(feature = "native")]
pub mod oidc;
#[cfg(not(target_family = "windows"))]
#[cfg(all(not(target_family = "windows"), feature = "native"))]
pub mod spire;
#[cfg(feature = "native")]
pub mod static_jwt;

#[cfg(feature = "native")]
use slim_auth::errors::AuthError as SlimAuthError;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cfg_if::cfg_if! {
      if #[cfg(not(target_arch = "wasm32"))] {
          pub mod identity;
          pub mod jwt;
          pub mod oidc;
          pub mod static_jwt;

          #[cfg(not(target_family = "windows"))]
          pub mod spire;

          use slim_auth::errors::AuthError as SlimAuthError;
      }
  }

Comment on lines +4 to +74
use duration_string::DurationString;
#[cfg(feature = "native")]
use rustls_pki_types::ServerName;
#[cfg(feature = "native")]
use tokio_retry::RetryIf;

#[cfg(feature = "native")]
use display_error_chain::ErrorChainExt;
#[cfg(any(feature = "native", feature = "wasm"))]
use std::str::FromStr;
use std::{collections::HashMap, time::Duration};
#[cfg(feature = "native")]
use tower::ServiceExt;
#[cfg(all(feature = "native", target_family = "unix"))]
use {
hyper_util::rt::TokioIo,
std::{error::Error as StdErrorTrait, path::PathBuf, sync::Arc},
tokio::net::UnixStream,
tower::service_fn,
};

#[cfg(feature = "native")]
use base64::prelude::*;
#[cfg(feature = "native")]
use http::header::{HeaderMap, HeaderName, HeaderValue};
#[cfg(feature = "native")]
use hyper_rustls;
#[cfg(feature = "native")]
use hyper_util::client::legacy::connect::HttpConnector;
#[cfg(feature = "native")]
use hyper_util::client::legacy::connect::proxy::Tunnel;
#[cfg(feature = "native")]
use hyper_util::client::proxy::matcher::Intercept;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cfg(feature = "native")]
use tonic::codegen::{Body, Bytes, StdError};
#[cfg(feature = "native")]
use tonic::transport::{Channel, Uri};
#[cfg(feature = "native")]
use tracing::warn;

#[cfg(feature = "native")]
use slim_auth::metadata::MetadataMap;
#[cfg(not(feature = "native"))]
type MetadataMap = HashMap<String, serde_json::Value>;

#[cfg(feature = "native")]
use crate::auth::ClientAuthenticator;
use crate::auth::basic::Config as BasicAuthenticationConfig;
#[cfg(feature = "native")]
use crate::auth::jwt::Config as JwtAuthenticationConfig;
#[cfg(all(feature = "native", not(target_family = "windows")))]
use crate::auth::spire::SpireConfig as SpireAuthConfig;
#[cfg(feature = "native")]
use crate::auth::static_jwt::Config as BearerAuthenticationConfig;
use crate::backoff::Strategy;
use crate::backoff::exponential::Config as ExponentialBackoff;
use crate::backoff::fixedinterval::Config as FixedIntervalBackoff;
use crate::component::configuration::Configuration;
use crate::grpc::compression::CompressionType;
use crate::grpc::errors::ConfigError;
#[cfg(feature = "native")]
use crate::grpc::headers_middleware::SetRequestHeaderLayer;
use crate::grpc::proxy::ProxyConfig;
use crate::tls::client::TlsClientConfig as TLSSetting;
#[cfg(feature = "native")]
use crate::tls::common::RustlsConfigLoader;
use crate::transport::TransportProtocol;
#[cfg(any(feature = "native", feature = "wasm"))]
use crate::websocket::client::WebSocketClientChannel;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use duration_string::DurationString;
#[cfg(feature = "native")]
use rustls_pki_types::ServerName;
#[cfg(feature = "native")]
use tokio_retry::RetryIf;
#[cfg(feature = "native")]
use display_error_chain::ErrorChainExt;
#[cfg(any(feature = "native", feature = "wasm"))]
use std::str::FromStr;
use std::{collections::HashMap, time::Duration};
#[cfg(feature = "native")]
use tower::ServiceExt;
#[cfg(all(feature = "native", target_family = "unix"))]
use {
hyper_util::rt::TokioIo,
std::{error::Error as StdErrorTrait, path::PathBuf, sync::Arc},
tokio::net::UnixStream,
tower::service_fn,
};
#[cfg(feature = "native")]
use base64::prelude::*;
#[cfg(feature = "native")]
use http::header::{HeaderMap, HeaderName, HeaderValue};
#[cfg(feature = "native")]
use hyper_rustls;
#[cfg(feature = "native")]
use hyper_util::client::legacy::connect::HttpConnector;
#[cfg(feature = "native")]
use hyper_util::client::legacy::connect::proxy::Tunnel;
#[cfg(feature = "native")]
use hyper_util::client::proxy::matcher::Intercept;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cfg(feature = "native")]
use tonic::codegen::{Body, Bytes, StdError};
#[cfg(feature = "native")]
use tonic::transport::{Channel, Uri};
#[cfg(feature = "native")]
use tracing::warn;
#[cfg(feature = "native")]
use slim_auth::metadata::MetadataMap;
#[cfg(not(feature = "native"))]
type MetadataMap = HashMap<String, serde_json::Value>;
#[cfg(feature = "native")]
use crate::auth::ClientAuthenticator;
use crate::auth::basic::Config as BasicAuthenticationConfig;
#[cfg(feature = "native")]
use crate::auth::jwt::Config as JwtAuthenticationConfig;
#[cfg(all(feature = "native", not(target_family = "windows")))]
use crate::auth::spire::SpireConfig as SpireAuthConfig;
#[cfg(feature = "native")]
use crate::auth::static_jwt::Config as BearerAuthenticationConfig;
use crate::backoff::Strategy;
use crate::backoff::exponential::Config as ExponentialBackoff;
use crate::backoff::fixedinterval::Config as FixedIntervalBackoff;
use crate::component::configuration::Configuration;
use crate::grpc::compression::CompressionType;
use crate::grpc::errors::ConfigError;
#[cfg(feature = "native")]
use crate::grpc::headers_middleware::SetRequestHeaderLayer;
use crate::grpc::proxy::ProxyConfig;
use crate::tls::client::TlsClientConfig as TLSSetting;
#[cfg(feature = "native")]
use crate::tls::common::RustlsConfigLoader;
use crate::transport::TransportProtocol;
#[cfg(any(feature = "native", feature = "wasm"))]
use crate::websocket::client::WebSocketClientChannel;
use std::{collections::HashMap, str::FromStr, time::Duration};
use duration_string::DurationString;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::auth::basic::Config as BasicAuthenticationConfig;
use crate::backoff::Strategy;
use crate::backoff::exponential::Config as ExponentialBackoff;
use crate::backoff::fixedinterval::Config as FixedIntervalBackoff;
use crate::component::configuration::Configuration;
use crate::grpc::compression::CompressionType;
use crate::grpc::errors::ConfigError;
use crate::grpc::proxy::ProxyConfig;
use crate::tls::client::TlsClientConfig as TLSSetting;
use crate::transport::TransportProtocol;
use crate::websocket::client::WebSocketClientChannel;
cfg_if::cfg_if! {
if #[cfg(not(target_arch = "wasm32"))] {
use std::sync::Arc;
use base64::prelude::*;
use display_error_chain::ErrorChainExt;
use http::header::{HeaderMap, HeaderName, HeaderValue};
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::connect::proxy::Tunnel;
use hyper_util::client::proxy::matcher::Intercept;
use rustls_pki_types::ServerName;
use tokio_retry::RetryIf;
use tonic::codegen::{Body, Bytes, StdError};
use tonic::transport::{Channel, Uri};
use tower::ServiceExt;
use tracing::warn;
use slim_auth::metadata::MetadataMap;
use crate::auth::ClientAuthenticator;
use crate::auth::jwt::Config as JwtAuthenticationConfig;
use crate::auth::static_jwt::Config as BearerAuthenticationConfig;
use crate::grpc::headers_middleware::SetRequestHeaderLayer;
use crate::tls::common::RustlsConfigLoader;
#[cfg(not(target_family = "windows"))]
use crate::auth::spire::SpireConfig as SpireAuthConfig;
#[cfg(target_family = "unix")]
use {
hyper_util::rt::TokioIo,
std::{error::Error as StdErrorTrait, path::PathBuf},
tokio::net::UnixStream,
tower::service_fn,
};
} else {
type MetadataMap = HashMap<String, serde_json::Value>;
}
}

/// timeout settings, buffer size settings, and origin settings.
pub async fn to_channel(
/// Converts the client configuration to a gRPC-only channel.
pub async fn to_grpc_channel(

@msardara msardara May 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should generalize this. Here it implies that who is using the config knows whether the config will generate a grpc or a websocket channel.

We should create an enum wrapping the grpc and the websockert channels, and we should offer a common set of methods to call on the channel, which should be transparent to the caller.

We should so keep a single to_channel, that returns a Channel which is

enum Channel {
  GrpcChannel
  WebsocketChannel
}

Alternatively we could use a generic function, but that makes life more difficult if we need to store the channel in a container.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I did in that way only. So outside grpc folder there is one client.rs file in that we have ClientConfig that will be generic for both Websocket/gRPC. And there we have a function to_channel() that returns an enum. So that function will call this to_grpc_channel / to_websocket_channel on the basis of the transport type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WebSocket server and client implementation

4 participants