smolmix + sdk: throughput and latency tuning for the Goblin read tunnel
ci-binary-config-checker / publish-nym (arc-ubuntu-22.04) (push) Has been cancelled

256KB TCP buffers (was 8KB) + burst 64 (was 1) in smolmix; an
IpMixStream::from_client constructor + best_ipr helper and reply-SURBs
10->4 in the ipr wrapper so a caller can back the tunnel with a
high-traffic-profile MixnetClient. No behavior change to the default
connect_new path used by the scoped exit.
This commit is contained in:
2ro
2026-07-02 19:58:10 -04:00
parent f6ed17d949
commit b9a6ec2abc
4 changed files with 54 additions and 7 deletions
@@ -53,22 +53,52 @@ impl IpMixStream {
///
/// Returns a ready-to-use tunnel with allocated IP addresses.
pub async fn new() -> Result<Self, Error> {
let ipr_address = Self::best_ipr().await?;
Self::new_with_ipr(ipr_address).await
}
/// Auto-discover the best available IPR exit for the mainnet.
///
/// Exposed so a caller that wants to back the tunnel with its own tuned
/// [`MixnetClient`] (via [`IpMixStream::from_client`]) can still get the
/// auto-selected exit that [`IpMixStream::new`] would have used.
pub async fn best_ipr() -> Result<Recipient, Error> {
let network_defaults = NymNetworkDetails::new_mainnet();
let api_client =
create_nym_api_client(network_defaults.nym_api_urls.ok_or(Error::NoNymAPIUrl)?)?;
let ipr_address = get_best_ipr(api_client).await?;
Self::new_with_ipr(ipr_address).await
get_best_ipr(api_client).await
}
/// Connect to a specific IPR address.
///
/// Use this when you already know the IPR `Recipient` address (e.g. for
/// testing against a specific exit node). For automatic discovery, use
/// [`IpMixStream::new`] instead.
/// [`IpMixStream::new`] instead. Backs the tunnel with a default
/// [`MixnetClient::connect_new`] client (full cover traffic, poisson per-hop
/// delays); for a tuned client use [`IpMixStream::from_client`].
pub async fn new_with_ipr(ipr_address: Recipient) -> Result<Self, Error> {
nym_network_defaults::setup_env(None::<&str>);
let mut client = MixnetClient::connect_new().await?;
let mut stream = client.open_stream(ipr_address, Some(10)).await?;
let client = MixnetClient::connect_new().await?;
Self::from_client(client, ipr_address).await
}
/// Establish the IP tunnel over a caller-provided, already-connected
/// [`MixnetClient`].
///
/// This is the low-anonymity-tuning seam: the caller builds the client with
/// whatever traffic/anonymity [`DebugConfig`](crate::mixnet::config) it wants
/// (e.g. a higher-throughput / lower-cover-traffic preset for a public read
/// tunnel) and passes it in. Everything after the client — the stream open,
/// the IPR connect handshake and the allocated-IP bookkeeping — is identical
/// to [`new_with_ipr`](Self::new_with_ipr).
pub async fn from_client(
mut client: MixnetClient,
ipr_address: Recipient,
) -> Result<Self, Error> {
// 4 reply-SURBs per stream (was 10): the IPR replies steadily, so a
// smaller SURB budget still keeps the return path fed while cutting the
// per-write SURB overhead on this public read tunnel.
let mut stream = client.open_stream(ipr_address, Some(4)).await?;
info!("Connecting to IP packet router at {ipr_address}");
let allocated_ips = Self::connect_tunnel(&mut stream).await?;
+5
View File
@@ -141,6 +141,11 @@ pub use nym_network_defaults::NymContracts;
/// println!("API: {:?}", network.endpoints);
/// ```
pub use nym_network_defaults::NymNetworkDetails;
/// Export the network (mainnet by default) environment configuration. Callers
/// that build a [`MixnetClient`](mixnet::MixnetClient) themselves (rather than
/// via [`MixnetClient::connect_new`](mixnet::MixnetClient::connect_new)) should
/// call this first, mirroring what the higher-level constructors do internally.
pub use nym_network_defaults::setup_env;
/// Validator/API endpoint configuration.
pub use nym_network_defaults::ValidatorDetails;
+5 -1
View File
@@ -33,7 +33,11 @@ impl NymAsyncDevice {
let mut capabilities = DeviceCapabilities::default();
capabilities.medium = Medium::Ip;
capabilities.max_transmission_unit = 1500;
capabilities.max_burst_size = Some(1);
// Let the reactor process up to 64 packets per poll loop instead of one.
// max_burst_size = Some(1) serialized all I/O to a single packet per
// reactor wake, which throttles throughput badly over the mixnet's long
// RTT; 64 lets a burst drain in one pass.
capabilities.max_burst_size = Some(64);
Self {
rx,
+9 -1
View File
@@ -217,11 +217,19 @@ impl Tunnel {
// Configure smoltcp: raw IP mode (no Ethernet), /32 for the allocated IP,
// default route via unspecified (the IPR does the actual routing).
let iface_config = Config::new(HardwareAddress::Ip);
let net_config = NetConfig::new(
let mut net_config = NetConfig::new(
iface_config,
IpCidr::new(IpAddress::from(allocated_ips.ipv4), 32),
vec![IpAddress::from(Ipv4Address::UNSPECIFIED)],
);
// Lift the per-socket TCP window from tokio-smoltcp's 8 KB default to
// 256 KB. The mixnet RTT is large (5 hops + deliberate per-hop delay),
// so an 8 KB window caps bulk throughput at ~8 KB per round trip; 256 KB
// lets many segments stay in flight, which is what makes relay/NIP-11/
// price reads fast. Only the two TCP buffers change — every other
// `BufferSize` field keeps its default.
net_config.buffer_size.tcp_rx_size = 262144;
net_config.buffer_size.tcp_tx_size = 262144;
// Net::new spawns the smoltcp reactor as a background task. After this,
// tcp_connect/udp_bind create sockets managed by that reactor.