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
+167 -4
View File
@@ -15,6 +15,7 @@ import (
"encoding/hex"
"encoding/json"
"errors"
"io"
"log/slog"
"net"
"net/http"
@@ -23,6 +24,8 @@ import (
"strings"
"time"
"echo-lot.app/server/internal/dataplane"
"echo-lot.app/server/internal/runs"
"echo-lot.app/server/internal/session"
"echo-lot.app/server/internal/store"
)
@@ -51,7 +54,13 @@ type Server struct {
DelayedEcho func(sess *session.Session, actionID string) error
// Granted server->client sends (spec §5). Both consume an asymmetric grant.
DownTrain func(sess *session.Session, g *session.Grant, count, sizeBytes, intervalUs int) (int, error)
BigSend func(sess *session.Session, g *session.Grant, sizes []int) ([]int, error)
BigSend func(sess *session.Session, g *session.Grant, sizes []int, df bool) ([]dataplane.BigSendResult, error)
// Runs stores uploaded measurement documents (may be nil: uploads unsupported).
Runs *runs.Store
// EgressMTU reports the server's own measured egress path MTU (0 = unknown). With DF set
// we cannot emit a datagram larger than this, so requested sizes above it are refused up
// front and reported as such — the client must not read that as a downstream path limit.
EgressMTU func() int
// CanaryQueries returns logged canary lookups for a session prefix (may be nil).
CanaryQueries func(sessionPrefix string) any
// CanaryZone is surfaced in the profile so the app knows what to query.
@@ -73,6 +82,10 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("POST /v1/sessions/{id}/actions", s.actions)
mux.HandleFunc("POST /v1/echo", s.httpEcho)
mux.HandleFunc("GET /v1/tls-reference", s.tlsReference)
mux.HandleFunc("POST /v1/runs", s.uploadRun)
mux.HandleFunc("GET /v1/runs", s.listRuns)
mux.HandleFunc("GET /v1/runs/{id}", s.getRun)
mux.HandleFunc("DELETE /v1/runs/{id}", s.deleteRun)
// TODO(spec §5): frag_send, throughput (both build on the same grant machinery)
return mux
}
@@ -156,6 +169,7 @@ func (s *Server) actions(w http.ResponseWriter, r *http.Request) {
SizeBytes int `json:"size_bytes"`
IntervalUs int `json:"interval_us"`
SizesBytes []int `json:"sizes_bytes"`
DF *bool `json:"df"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "bad body"})
@@ -241,6 +255,35 @@ func (s *Server) actions(w http.ResponseWriter, r *http.Request) {
if len(sizes) > 32 {
sizes = sizes[:32]
}
// DF on by default: an unfragmented burst is what makes the result a path-MTU
// measurement rather than a fragment-delivery one. Callers opt out explicitly.
df := true
if req.DF != nil {
df = *req.DF
}
// With DF we can only emit up to our own egress MTU minus IP+UDP headers. Drop the
// rest here and say so, rather than sending nothing and letting the client blame
// the path.
maxDF := 0
if df {
maxDF = s.maxDFPayload(sess)
if maxDF > 0 {
kept := sizes[:0]
for _, x := range sizes {
if x <= maxDF {
kept = append(kept, x)
}
}
sizes = kept
}
}
if len(sizes) == 0 {
writeJSON(w, http.StatusBadRequest, map[string]any{
"error": "every requested size exceeds the server's own egress MTU with DF set",
"max_df_bytes": maxDF,
})
return
}
total := 0
for _, x := range sizes {
total += clamp(x, dataMinPacket, 9000)
@@ -251,11 +294,11 @@ func (s *Server) actions(w http.ResponseWriter, r *http.Request) {
return
}
go func() {
attempted, err := s.BigSend(sess, g, sizes)
slog.Info("big_send finished", "action", actionID, "attempted", attempted, "err", err)
results, err := s.BigSend(sess, g, sizes, df)
slog.Info("big_send finished", "action", actionID, "results", results, "df", df, "err", err)
}()
writeJSON(w, http.StatusAccepted, map[string]any{
"action_id": actionID, "sizes_bytes": sizes,
"action_id": actionID, "sizes_bytes": sizes, "df": df, "max_df_bytes": maxDF,
"grant": map[string]any{"max_bytes": g.MaxBytes, "max_kbps": g.MaxKbps},
})
@@ -264,6 +307,24 @@ func (s *Server) actions(w http.ResponseWriter, r *http.Request) {
}
}
// maxDFPayload is the largest UDP payload the server can emit toward this session without
// fragmenting: its own egress MTU less the IP and UDP headers of the session's address family.
// Returns 0 when the egress MTU is unknown, meaning "do not clamp".
func (s *Server) maxDFPayload(sess *session.Session) int {
if s.EgressMTU == nil {
return 0
}
mtu := s.EgressMTU()
if mtu <= 0 {
return 0
}
overhead := 28 // IPv4 (20) + UDP (8)
if src := sess.DataSource(); src.IsValid() && !src.Addr().Unmap().Is4() {
overhead = 48 // IPv6 (40) + UDP (8)
}
return mtu - overhead
}
// dataMinPacket is the smallest datagram that still carries a header + a little payload.
const dataMinPacket = 40
@@ -360,6 +421,9 @@ func (s *Server) profile(w http.ResponseWriter, r *http.Request) {
"canary_zone": s.CanaryZone,
"server_selftest": selftestSignal(s.ProvenGood),
"limits": map[string]any{"max_kbps": 50000, "max_session_s": 900},
// The app needs the upload rules before it offers the switch: whether uploads are
// accepted at all, and how much identifying detail it must strip first.
"uploads": s.uploadPolicy(),
})
}
@@ -411,3 +475,102 @@ func SpkiPinB64(cert tls.Certificate) (string, error) {
sum := sha256.Sum256(leaf.RawSubjectPublicKeyInfo)
return base64.StdEncoding.EncodeToString(sum[:]), nil
}
// uploadPolicy is the profile's advertisement of the operator's upload rules.
func (s *Server) uploadPolicy() map[string]any {
if s.Runs == nil {
return map[string]any{"mode": string(runs.ModeOff), "reason": "not configured"}
}
p := s.Runs.Policy()
return map[string]any{
"mode": string(p.Mode),
"max_bytes": p.MaxBytes,
"retention_days": p.RetentionDays,
"max_runs_per_device": p.MaxRunsPerDevice,
"min_anonymization": p.MinAnonymization,
}
}
// uploadRun stores one measurement document for the calling device.
func (s *Server) uploadRun(w http.ResponseWriter, r *http.Request) {
dev := s.Store.DeviceByCredential(bearer(r))
if dev == nil {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unknown credential"})
return
}
if s.Runs == nil {
writeJSON(w, http.StatusForbidden, map[string]string{"error": runs.ErrDisabled.Error()})
return
}
limit := s.Runs.Policy().MaxBytes
if limit <= 0 {
limit = 4 << 20
}
// +1 so a body exactly at the limit is distinguishable from one over it.
body, err := io.ReadAll(io.LimitReader(r.Body, limit+1))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "read failed"})
return
}
meta, err := s.Runs.Put(dev.ID, body)
switch {
case err == nil:
slog.Info("run uploaded", "device", dev.ID, "run", meta.ID,
"bytes", meta.SizeBytes, "anon", meta.Anonymization, "findings", meta.FindingCount)
writeJSON(w, http.StatusCreated, meta)
case errors.Is(err, runs.ErrDisabled), errors.Is(err, runs.ErrNeedAccount),
errors.Is(err, runs.ErrNotAnonEnough):
writeJSON(w, http.StatusForbidden, map[string]any{
"error": err.Error(), "uploads": s.uploadPolicy(),
})
case errors.Is(err, runs.ErrTooLarge):
writeJSON(w, http.StatusRequestEntityTooLarge, map[string]any{
"error": err.Error(), "max_bytes": s.Runs.Policy().MaxBytes,
})
default:
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
}
}
func (s *Server) listRuns(w http.ResponseWriter, r *http.Request) {
dev := s.Store.DeviceByCredential(bearer(r))
if dev == nil || s.Runs == nil {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unknown credential"})
return
}
list := s.Runs.List(dev.ID)
if list == nil {
list = []runs.Meta{}
}
writeJSON(w, http.StatusOK, map[string]any{"runs": list})
}
func (s *Server) getRun(w http.ResponseWriter, r *http.Request) {
dev := s.Store.DeviceByCredential(bearer(r))
if dev == nil || s.Runs == nil {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unknown credential"})
return
}
// Scoped to the calling device's own directory: one device cannot read another's runs by
// guessing a run id.
b, err := s.Runs.Get(dev.ID, r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "no such run"})
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(b)
}
func (s *Server) deleteRun(w http.ResponseWriter, r *http.Request) {
dev := s.Store.DeviceByCredential(bearer(r))
if dev == nil || s.Runs == nil {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unknown credential"})
return
}
if err := s.Runs.Delete(dev.ID, r.PathValue("id")); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusNoContent)
}