Compare commits

...

2 Commits

Author SHA1 Message Date
Bogdan-Ștefan Neacşu f4bd48263d Apply cargo fmt 2026-06-08 15:32:57 +03:00
rachyandco 5c40052d39 Bugfix: set connection fd callback on LP registration socket
The LP registration client opened its TCP connection to the gateway
  control port with a plain TcpStream::connect, without invoking the
  connection_fd_callback used by the websocket registration path. On
  Linux the callback sets SO_MARK (fwmark) so the daemon's own firewall
  allows the connection during the connecting state; without it the
  unmarked SYN matched no allow rule and was rejected locally with
  ECONNREFUSED. The daemon blamed the gateway, blacklisted it, and
  cycled through every gateway the same way, making it impossible to
  connect when LP registration is enabled.

  Plumb the existing connection_fd_callback from the builder config
  through RegistrationClientConfig into the LP client via an optional
  dialer, which creates the socket, applies the callback before
  connect(), and only then dials. The fd callback must run before
  connect so the SYN itself carries the mark.

  The exit gateway needs no dialer: its registration is forwarded
  through the entry gateway's nested session and never opens its own
  TCP connection.
2026-06-05 23:36:52 +02:00
5 changed files with 91 additions and 16 deletions
@@ -97,6 +97,8 @@ impl BuilderConfig {
exit: self.exit_node.clone(),
mode: self.mode,
lp_registration_config: self.lp_registration_config,
#[cfg(unix)]
connection_fd_callback: self.connection_fd_callback.clone(),
}
}
+26
View File
@@ -80,6 +80,32 @@ impl LpBasedRegistrationClient {
self.config.lp_registration_config,
);
// Open the entry connection through a socket that has the connection
// fd callback applied before connecting (sets SO_MARK on Linux), so
// the connection is allowed through the VPN firewall during the
// connecting state.
#[cfg(unix)]
{
let fd_callback = self.config.connection_fd_callback.clone();
entry_client.set_dialer(Arc::new(move |addr| {
let fd_callback = fd_callback.clone();
Box::pin(async move {
let socket = if addr.is_ipv4() {
tokio::net::TcpSocket::new_v4()
} else {
tokio::net::TcpSocket::new_v6()
}
.map_err(|err| {
nym_lp::transport::LpTransportError::connection_failure(err.to_string())
})?;
fd_callback(std::os::fd::AsRawFd::as_raw_fd(&socket));
socket.connect(addr).await.map_err(|err| {
nym_lp::transport::LpTransportError::connection_failure(err.to_string())
})
})
}));
}
// Perform handshake with entry gateway (outer session now established)
entry_client.perform_handshake().await.map_err(|source| {
RegistrationClientError::EntryGatewayRegisterLp {
+5
View File
@@ -19,4 +19,9 @@ pub struct RegistrationClientConfig {
pub(crate) exit: NymNodeWithKeys,
pub(crate) mode: RegistrationMode,
pub(crate) lp_registration_config: LpRegistrationConfig,
/// Callback invoked with the raw fd of sockets opened for registration,
/// before connecting. Used to set `SO_MARK` on Linux so the connection is
/// allowed through the VPN firewall during the connecting state.
#[cfg(unix)]
pub(crate) connection_fd_callback: std::sync::Arc<dyn Fn(std::os::fd::RawFd) + Send + Sync>,
}
+53 -16
View File
@@ -33,6 +33,19 @@ use std::time::Duration;
use tokio::net::TcpStream;
use tracing::{debug, warn};
/// Custom dialer used to open the connection to the gateway.
///
/// Allows the caller to configure the socket before the connection is
/// initiated, e.g. set `SO_MARK` on Linux so the connection is allowed
/// through the VPN firewall during the connecting state.
pub type LpDialer<S> = Arc<
dyn Fn(
SocketAddr,
) -> futures::future::BoxFuture<'static, std::result::Result<S, LpTransportError>>
+ Send
+ Sync,
>;
/// LP (Lewes Protocol) registration client for direct gateway connections.
///
/// This client uses a persistent TCP connection model where a single TCP
@@ -70,6 +83,11 @@ pub struct LpRegistrationClient<S = TcpStream> {
/// Persistent TCP stream for the connection.
/// Opened on first use, closed after registration.
stream: Option<S>,
/// Optional custom dialer used to open the connection, allowing socket
/// configuration (e.g. `SO_MARK`) before the connection is initiated.
/// Falls back to `S::connect` when unset.
dialer: Option<LpDialer<S>>,
}
impl<S> LpRegistrationClient<S>
@@ -115,9 +133,18 @@ where
transport_session: None,
config,
stream: None,
dialer: None,
}
}
/// Sets a custom dialer used to open the connection to the gateway.
///
/// Allows socket configuration (e.g. setting `SO_MARK` on Linux so the
/// connection is allowed through the VPN firewall) before connecting.
pub fn set_dialer(&mut self, dialer: LpDialer<S>) {
self.dialer = Some(dialer);
}
/// Attempt to use this `LpRegistrationClient` as transport for `NestedSession`
pub fn as_nested_connection(&mut self, exit_address: SocketAddr) -> NestedConnection<'_, S> {
NestedConnection {
@@ -209,22 +236,32 @@ where
self.gateway_lp_address
);
let mut stream = tokio::time::timeout(
self.config.connect_timeout,
S::connect(self.gateway_lp_address),
)
.await
.map_err(|_| LpClientError::TcpConnection {
address: self.gateway_lp_address.to_string(),
source: LpTransportError::ConnectionFailure(format!(
"Connection timeout after {:?}",
self.config.connect_timeout
)),
})?
.map_err(|source| LpClientError::TcpConnection {
address: self.gateway_lp_address.to_string(),
source,
})?;
let connect_result = match &self.dialer {
Some(dialer) => {
tokio::time::timeout(self.config.connect_timeout, dialer(self.gateway_lp_address))
.await
}
None => {
tokio::time::timeout(
self.config.connect_timeout,
S::connect(self.gateway_lp_address),
)
.await
}
};
let mut stream = connect_result
.map_err(|_| LpClientError::TcpConnection {
address: self.gateway_lp_address.to_string(),
source: LpTransportError::ConnectionFailure(format!(
"Connection timeout after {:?}",
self.config.connect_timeout
)),
})?
.map_err(|source| LpClientError::TcpConnection {
address: self.gateway_lp_address.to_string(),
source,
})?;
// Set TCP_NODELAY for low latency
stream
@@ -0,0 +1,5 @@
{
"name": "@nymproject/nym-client-wasm",
"version": "1.0.0",
"sideEffects": false
}