server: DF-mode big_send + uploaded-run storage with an operator policy

big_send now forces the Don't-Fragment bit for the whole burst by default, so
the largest size that arrives IS the downstream path MTU rather than "fragments
got through" — two different measurements the schema already separates. Sizes
above our own egress MTU (from the startup self-test) are refused up front and
reported as max_df_bytes, because absence caused by our kernel must not be read
as a limit of the client's path.

Uploads: one JSON file per run under the state dir, with the policy the operator
actually cares about — who may upload (off / anonymous / account), how large,
how long to keep, and the least anonymization accepted. The profile advertises
all of it so the app can present the switch honestly instead of discovering the
rules by failing. `account` refuses today rather than falling back to anonymous:
picking the strict setting before OIDC lands must not silently mean the loose one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 10:26:19 +02:00
co-authored by Claude Fable 5
parent 7e1015c211
commit 2521d39989
19 changed files with 1172 additions and 31 deletions
@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2026 Echolot contributors
// SPDX-License-Identifier: GPL-3.0-or-later
package app.echo_lot.engine
import app.echo_lot.protocol.ControlClient
import app.echo_lot.protocol.ProbeSession
import app.echo_lot.protocol.Wire
import kotlin.test.Test
import kotlin.test.assertTrue
/**
* Exercises the server's §5 granted sends against a LIVE server: downtrain (downstream loss /
* ordering) and big_send (downstream MTU). Self-skips without ECHOLOT_LIVE_*.
*
* This is the direction a client cannot measure alone — only the far end can push large or
* numerous packets toward it — so it is also the direction that needs the anti-amplification
* grant, and this test is the proof that the grant path works end to end.
*/
class LiveGrantedTest {
private val url = System.getenv("ECHOLOT_LIVE_URL")
private val pin = System.getenv("ECHOLOT_LIVE_PIN")
private val cred = System.getenv("ECHOLOT_LIVE_CRED")
private val udp = System.getenv("ECHOLOT_LIVE_UDP")
private val target = System.getenv("ECHOLOT_LIVE_TARGET") ?: "fmr"
@Test
fun downstreamTrainAndBigSend() {
if (url == null || pin == null || cred == null || udp == null) {
println("LiveGrantedTest skipped (no ECHOLOT_LIVE_* env)"); return
}
val control = ControlClient(url, setOf(pin))
val profile = control.profile(cred)
println("capabilities: ${profile.capabilities}")
val session = control.createSession(cred, target)
val (host, port) = udp.split(":").let { it[0] to it[1].toInt() }
ProbeSession(cred, session, host, port).use { ps ->
// The grant is bound to the OBSERVED data-plane source, so we must be seen first.
val echo = ps.echo()
println("primed with echo rtt=${echo?.rttMs}")
// --- downtrain: 50 packets of 300 bytes, 5ms apart ---
val dtResp = control.action(
cred, session.sessionId,
"""{"action":"downtrain","count":50,"size_bytes":300,"interval_us":5000}""",
)
println("downtrain accepted: ${dtResp.take(160)}")
val down = ps.collectGranted(windowMs = 4000)
.filter { it.type == Wire.TYPE_DOWNTRAIN_DATA }
val seqs = down.map { it.seq }.toSet()
println("downtrain received ${down.size}/50 packets, distinct seqs=${seqs.size}, " +
"sizes=${down.map { it.sizeBytes }.distinct()}")
assertTrue(down.isNotEmpty(), "no DOWNTRAIN_DATA arrived — granted send path is broken")
// --- big_send: which downstream sizes survive? ---
val sizes = listOf(600, 1200, 1400, 1472, 1500, 2000, 4000)
val bsResp = control.action(
cred, session.sessionId,
"""{"action":"big_send","sizes_bytes":${sizes}}""",
)
println("big_send accepted: ${bsResp.take(160)}")
val big = ps.collectGranted(windowMs = 4000)
.filter { it.type == Wire.TYPE_BIG_SEND }
val arrived = big.map { it.sizeBytes }.sorted()
println("big_send arrived sizes: $arrived (requested $sizes)")
assertTrue(big.isNotEmpty(), "no BIG_SEND packets arrived")
println("largest downstream datagram delivered: ${arrived.maxOrNull()}")
}
control.deleteSession(cred, session.sessionId)
}
}
@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2026 Echolot contributors
// SPDX-License-Identifier: GPL-3.0-or-later
package app.echo_lot.engine
import app.echo_lot.protocol.ControlClient
import app.echo_lot.protocol.ProbeSession
import app.echo_lot.protocol.Wire
import kotlin.test.Test
import kotlin.test.assertTrue
/**
* Exercises the server's §5 granted sends against a LIVE server: downtrain (downstream loss /
* ordering) and big_send (downstream MTU). Self-skips without ECHOLOT_LIVE_*.
*
* This is the direction a client cannot measure alone — only the far end can push large or
* numerous packets toward it — so it is also the direction that needs the anti-amplification
* grant, and this test is the proof that the grant path works end to end.
*/
class LiveGrantedTest {
private val url = System.getenv("ECHOLOT_LIVE_URL")
private val pin = System.getenv("ECHOLOT_LIVE_PIN")
private val cred = System.getenv("ECHOLOT_LIVE_CRED")
private val udp = System.getenv("ECHOLOT_LIVE_UDP")
private val target = System.getenv("ECHOLOT_LIVE_TARGET") ?: "fmr"
@Test
fun downstreamTrainAndBigSend() {
if (url == null || pin == null || cred == null || udp == null) {
println("LiveGrantedTest skipped (no ECHOLOT_LIVE_* env)"); return
}
val control = ControlClient(url, setOf(pin))
val profile = control.profile(cred)
println("capabilities: ${profile.capabilities}")
val session = control.createSession(cred, target)
val (host, port) = udp.split(":").let { it[0] to it[1].toInt() }
ProbeSession(cred, session, host, port).use { ps ->
// The grant is bound to the OBSERVED data-plane source, so we must be seen first.
val echo = ps.echo()
println("primed with echo rtt=${echo?.rttMs}")
// --- downtrain: 50 packets of 300 bytes, 5ms apart ---
val dtResp = control.action(
cred, session.sessionId,
"""{"action":"downtrain","count":50,"size_bytes":300,"interval_us":5000}""",
)
println("downtrain accepted: ${dtResp.take(160)}")
val down = ps.collectGranted(windowMs = 4000)
.filter { it.type == Wire.TYPE_DOWNTRAIN_DATA }
val seqs = down.map { it.seq }.toSet()
println("downtrain received ${down.size}/50 packets, distinct seqs=${seqs.size}, " +
"sizes=${down.map { it.sizeBytes }.distinct()}")
assertTrue(down.isNotEmpty(), "no DOWNTRAIN_DATA arrived — granted send path is broken")
// --- big_send: which downstream sizes survive? ---
val sizes = listOf(600, 1200, 1400, 1472, 1500, 2000, 4000)
val bsResp = control.action(
cred, session.sessionId,
"""{"action":"big_send","sizes_bytes":${sizes}}""",
)
println("big_send accepted: ${bsResp.take(160)}")
val big = ps.collectGranted(windowMs = 4000)
.filter { it.type == Wire.TYPE_BIG_SEND }
val arrived = big.map { it.sizeBytes }.sorted()
println("big_send arrived sizes: $arrived (requested $sizes)")
assertTrue(big.isNotEmpty(), "no BIG_SEND packets arrived")
println("largest downstream datagram delivered: ${arrived.maxOrNull()}")
}
control.deleteSession(cred, session.sessionId)
}
}