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:
co-authored by
Claude Fable 5
parent
7e1015c211
commit
2521d39989
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user