From 56e296e5875b10ed053cbdedbab08957fce2a461 Mon Sep 17 00:00:00 2001
From: Gab <24553253+gabrix73@users.noreply.github.com>
Date: Sun, 16 Aug 2026 19:24:16 +0200
Subject: Harden Usenet threading and ingress delivery
---
README.md | 5 ++++
about.html | 2 +-
index.php | 52 +++++++++++++++++++++++++++++++--
ingress/README.md | 5 ++++
ingress/src/main.rs | 46 ++++++++++++++++++++++++-----
yamn/encoder/encoder.go | 57 ++++++++++++++++++++++++++++++------
yamn/encoder/encoder_test.go | 69 +++++++++++++++++++++++++++++++++++++++++++-
7 files changed, 215 insertions(+), 21 deletions(-)
diff --git a/README.md b/README.md
index 745c373..da2acb2 100644
--- a/README.md
+++ b/README.md
@@ -120,6 +120,9 @@ The ingress is installed separately from `deploy/yamn-nym-ingress.service`.
Its environment file must contain the reviewed current YAMN Entry allowlist.
Public YAMN exits deliver to the clearnet Mail-to-News endpoint; they must not
be configured with an Onion-only destination they cannot resolve or reach.
+The ingress retries transient Tor connection failures only before beginning
+the SMTP transaction, avoiding both immediate message loss and unsafe retry
+after an ambiguous SMTP handoff.
## Delivery semantics
@@ -136,6 +139,8 @@ Nym payloads in logs.
## Security notes
- All form input is validated before encoding.
+- Usenet `References` accepts a bounded chain of Message-IDs and generates
+ `In-Reply-To` from the last ID; `Reply-To` is validated as an email address.
- The YAMN encoder reads only the local reviewed public keyring.
- The Nym recipient and Mail-to-News address are deployment configuration,
never browser-controlled destinations.
diff --git a/about.html b/about.html
index 766e12e..a3fa187 100644
--- a/about.html
+++ b/about.html
@@ -218,7 +218,7 @@
Email or Usenet
For email, the selected recipient is placed inside the encrypted message. For Usenet, the selected newsgroup is preserved and the server-configured Mail-to-News recipient performs SMTP-to-NNTP conversion.
-
References can link an article to an existing thread. It does not enable fetching or replies.
+
References accepts one or more Message-IDs and links the article to an existing Usenet thread. The last Message-ID is also emitted as In-Reply-To. The separate Reply-To field is an optional email response address; neither field enables message retrieval in this send-only service.
diff --git a/index.php b/index.php
index af646a6..bbab642 100644
--- a/index.php
+++ b/index.php
@@ -85,6 +85,43 @@ if (empty($_SESSION['csrf_token'])) {
// Store current token for validation BEFORE any regeneration
$currentCsrfToken = $_SESSION['csrf_token'];
+/**
+ * Return remailer names whose public keys are valid today.
+ *
+ * @return array Name lookup table.
+ */
+function getUsableRemailerKeyNames(): array {
+ static $usableNames = null;
+ if (is_array($usableNames)) {
+ return $usableNames;
+ }
+
+ $usableNames = [];
+ $keyring = yamnConfig('YAMN_PUBRING', '/opt/yamn-master/pubring.mix');
+ if (!is_readable($keyring)) {
+ return $usableNames;
+ }
+
+ $today = gmdate('Y-m-d');
+ foreach (file($keyring, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
+ $parts = preg_split('/\s+/', trim($line));
+ if (count($parts) !== 7) {
+ continue;
+ }
+ [$name, , , , , $validFrom, $validUntil] = $parts;
+ if (!preg_match('/^[a-z0-9_-]+$/', $name)
+ || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $validFrom)
+ || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $validUntil)) {
+ continue;
+ }
+ if ($validFrom <= $today && $today <= $validUntil) {
+ $usableNames[$name] = true;
+ }
+ }
+
+ return $usableNames;
+}
+
/**
* Parse remailers from file and return array by type
* Entry and Exit can use ANY remailer
@@ -95,6 +132,7 @@ $currentCsrfToken = $_SESSION['csrf_token'];
*/
function getRemailers($type) {
$remailers = ['*']; // Always include Random option
+ $usableKeyNames = getUsableRemailerKeyNames();
// Try multiple file locations
$files = [
@@ -140,6 +178,7 @@ function getRemailers($type) {
// Validate name: lowercase letters, numbers, hyphens only
if (!preg_match('/^[a-z0-9-]+$/', $remailerName)) continue;
+ if (!isset($usableKeyNames[$remailerName])) continue;
// Check if last field is 'D' (middle capability flag)
$lastField = end($parts);
@@ -185,6 +224,9 @@ function resolveRemailer($remailer, $availableRemailers) {
$randomIndex = array_rand($candidates);
return $candidates[$randomIndex];
}
+ if (!in_array($remailer, $availableRemailers, true)) {
+ throw new Exception("Selected remailer is no longer available. Reload the page and choose again.");
+ }
return $remailer;
}
@@ -246,11 +288,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$middleRemailer = isset($_POST['middle_remailer']) ? filter_var($_POST['middle_remailer'], FILTER_SANITIZE_STRING) : '';
$exitRemailer = isset($_POST['exit_remailer']) ? filter_var($_POST['exit_remailer'], FILTER_SANITIZE_STRING) : '';
$from = isset($_POST['from']) ? filter_var($_POST['from'], FILTER_SANITIZE_STRING) : '';
- $replyTo = isset($_POST['reply_to']) ? filter_var($_POST['reply_to'], FILTER_SANITIZE_STRING) : '';
+ $replyTo = isset($_POST['reply_to']) && is_string($_POST['reply_to'])
+ ? trim($_POST['reply_to'])
+ : '';
$to = isset($_POST['to']) ? filter_var($_POST['to'], FILTER_SANITIZE_EMAIL) : '';
$subject = isset($_POST['subject']) ? filter_var($_POST['subject'], FILTER_SANITIZE_STRING) : '';
$newsgroups = isset($_POST['newsgroups']) ? filter_var($_POST['newsgroups'], FILTER_SANITIZE_STRING) : '';
- $references = isset($_POST['references']) ? filter_var($_POST['references'], FILTER_SANITIZE_STRING) : '';
+ $references = isset($_POST['references']) && is_string($_POST['references'])
+ ? trim($_POST['references'])
+ : '';
$data = isset($_POST['data']) ? $_POST['data'] : ''; // Keep original formatting
$copies = isset($_POST['copies']) ? intval($_POST['copies']) : 1;
@@ -766,7 +812,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
Required for email delivery. Leave empty when publishing to a newsgroup.
Optional. When set, the message is routed through the configured Mail-to-News gateway. Leave empty for email.
-
Optional. Links the article to an existing thread.
+
Optional. Enter one or more space-separated Message-IDs, without the References: label. The last ID becomes In-Reply-To.
The server creates the encrypted YAMN envelope in memory and sends only that packet through Nym. Remailer queues may take several hours.