Compare commits

..

2 Commits

Author SHA1 Message Date
2ro b9a6ec2abc 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.
2026-07-02 19:58:10 -04:00
2ro f6ed17d949 http-api-client: preconfigured webpki roots on Android
ci-binary-config-checker / publish-nym (arc-ubuntu-22.04) (push) Has been cancelled
ci-contracts / build (push) Has been cancelled
The default rustls platform verifier needs the app JNI context, which a
standalone client process (Goblin's bundled SOCKS5 sidecar) lacks — it panics
on the first nym-api HTTPS call. Pin webpki_roots::TLS_SERVER_ROOTS on Android
per Nym's own troubleshooting docs.
2026-06-13 19:57:24 -04:00
7 changed files with 81 additions and 8 deletions
Generated
+1
View File
@@ -7020,6 +7020,7 @@ dependencies = [
"tracing-subscriber",
"url",
"wasmtimer",
"webpki-roots 0.26.11",
]
[[package]]
+4 -1
View File
@@ -38,7 +38,10 @@ itertools = { workspace = true }
inventory = { workspace = true }
fastrand = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "time"] }
rustls = { workspace=true }
rustls = { workspace = true, features = ["aws_lc_rs"] }
# Android: preconfigured webpki roots replace the JNI-bound platform verifier
# (see registry.rs); a standalone sidecar process can't init the platform store.
webpki-roots = { workspace = true }
# used for decoding text responses (they were already implicitly included)
bytes = { workspace = true }
encoding_rs = { workspace = true }
+22
View File
@@ -66,6 +66,28 @@ pub fn default_builder() -> ReqwestClientBuilder {
}
}
// On Android the default rustls verifier (rustls-platform-verifier) reaches
// the system trust store through JNI and must be initialized with the app's
// Java context. A standalone client process (e.g. Goblin's bundled SOCKS5
// sidecar) has no such context, so the verifier panics
// ("Expect rustls-platform-verifier to be initialized") the moment it makes
// its first HTTPS call to the nym-api. Per Nym's own troubleshooting docs,
// pin preconfigured webpki roots instead so HTTPS verifies without the
// platform store. Desktop/Windows keep the default verifier.
#[cfg(target_os = "android")]
{
let mut roots = rustls::RootCertStore::empty();
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let tls = rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(
rustls::crypto::aws_lc_rs::default_provider(),
))
.with_safe_default_protocol_versions()
.expect("aws-lc-rs provides the safe default protocol versions")
.with_root_certificates(roots)
.with_no_client_auth();
b = b.use_preconfigured_tls(tls);
}
b
}
@@ -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.