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
@@ -21,7 +21,11 @@ import (
|
||||
// client measures downstream loss, reordering and jitter from what arrives — the direction an
|
||||
// upstream-only train cannot see. Returns how many packets actually went out (the grant may cut
|
||||
// it short, which is itself reportable).
|
||||
func (s *Server) DownTrain(sess *session.Session, g *session.Grant, count, sizeBytes, intervalUs int) (int, error) {
|
||||
//
|
||||
// dscp ≥ 0 marks the burst (spec §5 downtrain `dscp`): downstream DSCP survival is the half the
|
||||
// client cannot produce itself. Best-effort off Linux — see withTOS/TOSSupported; the action
|
||||
// response has already told the client whether the marking was applied.
|
||||
func (s *Server) DownTrain(sess *session.Session, g *session.Grant, count, sizeBytes, intervalUs, dscp int) (int, error) {
|
||||
target := sess.DataSource()
|
||||
if !target.IsValid() {
|
||||
return 0, fmt.Errorf("no observed data-plane source")
|
||||
@@ -30,26 +34,41 @@ func (s *Server) DownTrain(sess *session.Session, g *session.Grant, count, sizeB
|
||||
if conn == nil {
|
||||
return 0, fmt.Errorf("no data-plane socket matches target family")
|
||||
}
|
||||
if sizeBytes < HeaderSize+8 {
|
||||
sizeBytes = HeaderSize + 8
|
||||
if sizeBytes < HeaderSize+16 {
|
||||
sizeBytes = HeaderSize + 16
|
||||
}
|
||||
payload := make([]byte, sizeBytes-HeaderSize)
|
||||
// Payload [8:16] carries the action id on every granted packet, so arriving traffic can be
|
||||
// attributed to the action that caused it (spec §5/§9: test.params.action_id).
|
||||
putActionID(payload, g.ActionID)
|
||||
sent := 0
|
||||
for i := 0; i < count; i++ {
|
||||
if !g.Allow(sizeBytes) {
|
||||
break // budget or rate exhausted — stop, do not sleep it off
|
||||
}
|
||||
// Sequence + send timestamp in the payload head so the client can order and time them
|
||||
// even when packets arrive out of order.
|
||||
binary.BigEndian.PutUint32(payload[0:4], uint32(i))
|
||||
binary.BigEndian.PutUint32(payload[4:8], uint32(time.Since(s.start).Microseconds()))
|
||||
s.send(conn, target, sess, TypeDownTrainData, uint32(i), payload)
|
||||
sent++
|
||||
if intervalUs > 0 && i < count-1 {
|
||||
time.Sleep(time.Duration(intervalUs) * time.Microsecond)
|
||||
burst := func() error {
|
||||
for i := 0; i < count; i++ {
|
||||
if !g.Allow(sizeBytes) {
|
||||
break // budget or rate exhausted — stop, do not sleep it off
|
||||
}
|
||||
// Sequence + send timestamp in the payload head so the client can order and time them
|
||||
// even when packets arrive out of order.
|
||||
binary.BigEndian.PutUint32(payload[0:4], uint32(i))
|
||||
binary.BigEndian.PutUint32(payload[4:8], uint32(time.Since(s.start).Microseconds()))
|
||||
s.send(conn, target, sess, TypeDownTrainData, uint32(i), payload)
|
||||
sent++
|
||||
if intervalUs > 0 && i < count-1 {
|
||||
time.Sleep(time.Duration(intervalUs) * time.Microsecond)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return sent, nil
|
||||
if dscp >= 0 && TOSSupported {
|
||||
// Same shared-socket borrow as the DF window: dfMu keeps a concurrent burst from riding
|
||||
// along with — or clearing — this marking.
|
||||
s.dfMu.Lock()
|
||||
defer s.dfMu.Unlock()
|
||||
err := withTOS(conn, dscp, burst) // sent must be read after the burst ran, not before
|
||||
return sent, err
|
||||
}
|
||||
err := burst()
|
||||
return sent, err
|
||||
}
|
||||
|
||||
// BigSendResult records what happened to one requested size. `Sent` false with an EMSGSIZE-ish
|
||||
@@ -83,8 +102,8 @@ func (s *Server) BigSend(sess *session.Session, g *session.Grant, sizes []int, d
|
||||
results := make([]BigSendResult, 0, len(sizes))
|
||||
burst := func() error {
|
||||
for i, size := range sizes {
|
||||
if size < HeaderSize+8 {
|
||||
size = HeaderSize + 8
|
||||
if size < HeaderSize+16 {
|
||||
size = HeaderSize + 16
|
||||
}
|
||||
if size > 9000 { // jumbo ceiling; beyond this the kernel will refuse anyway
|
||||
size = 9000
|
||||
@@ -96,6 +115,8 @@ func (s *Server) BigSend(sess *session.Session, g *session.Grant, sizes []int, d
|
||||
// Echo the intended size into the payload so a truncated/fragmented arrival is
|
||||
// still attributable to the size we meant to send.
|
||||
binary.BigEndian.PutUint32(payload[0:4], uint32(size))
|
||||
// [8:16]: the action id, as on every granted packet (spec §5 correlation).
|
||||
putActionID(payload, g.ActionID)
|
||||
err := s.sendErr(conn, target, sess, TypeBigSend, uint32(i), payload)
|
||||
results = append(results, BigSendResult{
|
||||
SizeBytes: size, Seq: i, Sent: err == nil, Err: errString(err),
|
||||
|
||||
Reference in New Issue
Block a user