server: upstream trains, observed TTL/DSCP/ECN, rate limits, action ids
Types 0x03/0x04/0x05 land with a bounded columnar train buffer (head kept, truncation declared) and grant-free multi-part reports - a report row is smaller than the packet it answers, so $3.4 holds without a grant. The read loop now collects TTL/TOS cmsgs on Linux, replacing the 0xFF stubs in the observation block with what the kernel saw; downtrain gained a dscp parameter, so DSCP survival is measurable in both directions. Rate limiting ($2.5) exists now: per-credential AND per-source buckets, 429 on the control plane, silent drop on the data plane after the HMAC gate and before the replay window. UDP ceilings default above the largest legitimate run - a limit that clips a real measurement produces a confidently wrong number. Every granted packet carries its action_id at payload[8:16]; overlapping actions were unattributable before. Canary DNS logs now honor the stated 24h privacy default. /admin/enroll-tokens answers the spec's JSON shape. protocol_version 1.0.1 (additive). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f6849f8e6a
commit
8118e213ae
@@ -117,6 +117,10 @@ func (s *Server) Handler() http.Handler {
|
||||
// affects the whole server, so those stay with the admin.
|
||||
mux.HandleFunc("POST /devices/{id}/revoke", s.guard(s.adminOnly(s.revokeDevice)))
|
||||
mux.HandleFunc("POST /enroll-tokens", s.guard(s.adminOnly(s.mintToken)))
|
||||
// The spec-shaped mint endpoint (§2.1: {token, expires_in_s, enroll_uri}), for curl and
|
||||
// scripts. Authenticates its own way — see apiAdmin — because guard's redirect-to-login is
|
||||
// useless to a caller without a browser.
|
||||
mux.HandleFunc("POST /admin/enroll-tokens", s.enrollTokensAPI)
|
||||
|
||||
return mux
|
||||
}
|
||||
@@ -235,6 +239,41 @@ func (s *Server) loginSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// apiAdmin authenticates a programmatic admin request: the normal session cookie, or HTTP Basic
|
||||
// against the break-glass credential for callers without a cookie jar (the README's curl).
|
||||
//
|
||||
// The cookie path keeps CSRF, exactly like guard: a cookie is an ambient credential and this
|
||||
// endpoint changes state. Basic auth is exempt — the password is supplied explicitly per
|
||||
// request, so there is nothing for a cross-site form to ride on — and a wrong guess pays the
|
||||
// same throttle as the login form, so this is no better a password oracle than that is.
|
||||
func (s *Server) apiAdmin(w http.ResponseWriter, r *http.Request) (subject string, ok bool) {
|
||||
if sess := s.session(r); sess != nil {
|
||||
if !sess.Admin {
|
||||
http.Error(w, "that action needs an administrator account", http.StatusForbidden)
|
||||
return "", false
|
||||
}
|
||||
if !s.csrfOK(r, sess) {
|
||||
http.Error(w, "stale form — reload the page and try again", http.StatusForbidden)
|
||||
return "", false
|
||||
}
|
||||
return sess.Subject, true
|
||||
}
|
||||
if user, pass, hasBasic := r.BasicAuth(); hasBasic {
|
||||
if d := s.Throttle.Delay(); d > 0 {
|
||||
time.Sleep(d)
|
||||
}
|
||||
if cred := s.Store.LocalAdmin(); cred != nil && cred.Verify(user, pass) {
|
||||
s.Throttle.Succeeded()
|
||||
return "local:" + user, true
|
||||
}
|
||||
s.Throttle.Failed()
|
||||
slog.Info("admin api auth failed", "user", user, "from", clientIP(r))
|
||||
}
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="echolot-admin"`)
|
||||
http.Error(w, "authentication required", http.StatusUnauthorized)
|
||||
return "", false
|
||||
}
|
||||
|
||||
// ---- OIDC -------------------------------------------------------------------------------
|
||||
|
||||
func (s *Server) oidcAvailable() bool {
|
||||
|
||||
Reference in New Issue
Block a user