summaryrefslogtreecommitdiffstats
path: root/ingress
diff options
context:
space:
mode:
authorGab <24553253+gabrix73@users.noreply.github.com>2026-08-16 19:24:16 +0200
committerGab <24553253+gabrix73@users.noreply.github.com>2026-08-16 19:24:16 +0200
commit56e296e5875b10ed053cbdedbab08957fce2a461 (patch)
treee1c49b328f1fe5142f7e2b27593b0905c5e3668a /ingress
parent43fbddf016f94f4ba006d82c9a67dca61b5852a1 (diff)
downloadyamnweb-main.tar.gz
yamnweb-main.tar.xz
yamnweb-main.zip
Harden Usenet threading and ingress deliveryHEADmain
Diffstat (limited to 'ingress')
-rw-r--r--ingress/README.md5
-rw-r--r--ingress/src/main.rs46
2 files changed, 44 insertions, 7 deletions
diff --git a/ingress/README.md b/ingress/README.md
index d4a8338..e9c5295 100644
--- a/ingress/README.md
+++ b/ingress/README.md
@@ -3,6 +3,11 @@
This service receives an envelope over the Nym mixnet and delivers the
already encrypted YAMN packet to an allowlisted SMTP entry through Tor.
+Transient Tor connection failures are retried three times with bounded
+backoff before the SMTP transaction begins. Errors after SMTP commands start
+are not retried automatically because the remote acceptance state may be
+ambiguous.
+
The Nym address is generated by the persistent client identity and printed at
startup. It must be copied to `YAMN_NYM_RECIPIENT` on the sending web service.
diff --git a/ingress/src/main.rs b/ingress/src/main.rs
index 21da30f..39047ee 100644
--- a/ingress/src/main.rs
+++ b/ingress/src/main.rs
@@ -11,6 +11,7 @@ use tokio_socks::tcp::Socks5Stream;
const MAX_REQUEST_BYTES: usize = 128 * 1024;
const MAX_PAYLOAD_BYTES: usize = 96 * 1024;
const SMTP_TIMEOUT: Duration = Duration::from_secs(60);
+const TOR_CONNECT_ATTEMPTS: u32 = 3;
#[derive(Debug, Deserialize)]
struct Envelope {
@@ -123,13 +124,29 @@ async fn deliver(entry_address: &str, payload: &[u8]) -> Result<()> {
let (proxy_host, proxy_port) = proxy.rsplit_once(':').context("invalid YAMN_TOR_SOCKS")?;
let proxy_port: u16 = proxy_port.parse().context("invalid YAMN_TOR_SOCKS port")?;
let target = format!("{host}:25");
- let stream = tokio::time::timeout(
- SMTP_TIMEOUT,
- Socks5Stream::connect((proxy_host, proxy_port), target.as_str()),
- )
- .await
- .context("Tor SMTP connection timed out")?
- .context("connect to YAMN SMTP entry through Tor")?;
+ let mut attempt = 1;
+ let stream = loop {
+ let connection = tokio::time::timeout(
+ SMTP_TIMEOUT,
+ Socks5Stream::connect((proxy_host, proxy_port), target.as_str()),
+ )
+ .await;
+ match connection {
+ Ok(Ok(stream)) => break stream,
+ Ok(Err(_)) if attempt < TOR_CONNECT_ATTEMPTS => {
+ eprintln!("Tor SMTP connection attempt {attempt} failed; retrying");
+ }
+ Err(_) if attempt < TOR_CONNECT_ATTEMPTS => {
+ eprintln!("Tor SMTP connection attempt {attempt} timed out; retrying");
+ }
+ Ok(Err(error)) => {
+ return Err(error).context("connect to YAMN SMTP entry through Tor");
+ }
+ Err(error) => return Err(error).context("Tor SMTP connection timed out"),
+ }
+ tokio::time::sleep(tor_retry_delay(attempt)).await;
+ attempt += 1;
+ };
let mut reader = BufReader::new(stream);
let greeting = smtp_read_response(&mut reader).await?;
if greeting / 100 != 2 {
@@ -189,6 +206,10 @@ async fn deliver(entry_address: &str, payload: &[u8]) -> Result<()> {
Ok(())
}
+fn tor_retry_delay(failed_attempt: u32) -> Duration {
+ Duration::from_secs(5 * u64::from(failed_attempt))
+}
+
async fn run() -> Result<()> {
let disconnected = MixnetClientBuilder::new_with_default_storage(storage_paths()?)
.await
@@ -243,3 +264,14 @@ async fn main() {
std::process::exit(1);
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn tor_retry_delay_uses_bounded_linear_backoff() {
+ assert_eq!(tor_retry_delay(1), Duration::from_secs(5));
+ assert_eq!(tor_retry_delay(2), Duration::from_secs(10));
+ }
+}