server: STUN, TCP echo, observations API, delayed-echo + connect-back actions
- stun: RFC 5389 binding responder + RFC 5780 attributes (OTHER-ADDRESS,
RESPONSE-ORIGIN, CHANGE-REQUEST) on a primary/alt-port socket grid per
address; advertises stun-5780 with >=2 same-family addrs, else
stun-basic. Unmodified framing for tooling interop. Tested.
- tcpecho: JSON greeting with observed src + TCP_INFO MSS/options
(Linux getsockopt; zeroed elsewhere via build tags), then byte echo.
- session: per-packet UDP observations + connect-back results, ByID lookup.
- control: GET /v1/sessions/{id}/observations, POST .../actions
(delayed_echo → DELAYED_ECHO at the observed data-plane source;
connect_back → dial the control-plane source, record connected/refused/
timeout+rtt). Capabilities computed from what is actually wired.
- config/main: comma-separated STUN listeners; all planes bind explicit
addresses; graceful shutdown of the new listeners.
Full flow smoke-tested; go test green (stun binding/change-port,
dataplane wire format).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
507a8bfc1f
commit
7b676e666e
@@ -0,0 +1,91 @@
|
||||
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// Package tcpecho implements the spec §4 TCP echo: after connect the server
|
||||
// sends one JSON line with what it observed (source address/port, negotiated
|
||||
// MSS and TCP options from TCP_INFO), then byte-echoes until FIN. This is the
|
||||
// evidence source for mtu.mss_observed.
|
||||
//
|
||||
// The TLS/ALPN "elt-echo" variant (ClientHello capture + JA4) is not
|
||||
// implemented yet.
|
||||
package tcpecho
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConnRecord is what the observations API reports per connection (spec §6).
|
||||
type ConnRecord struct {
|
||||
ConnectedAt time.Time `json:"connected_at"`
|
||||
Src string `json:"src"`
|
||||
MSS int `json:"mss"`
|
||||
Options []string `json:"options"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
mu sync.Mutex
|
||||
recent []ConnRecord // ring, newest last
|
||||
}
|
||||
|
||||
const recentCap = 1024
|
||||
|
||||
func (s *Server) record(r ConnRecord) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(s.recent) >= recentCap {
|
||||
s.recent = s.recent[1:]
|
||||
}
|
||||
s.recent = append(s.recent, r)
|
||||
}
|
||||
|
||||
// RecentFor returns records whose source IP matches ip.
|
||||
func (s *Server) RecentFor(ip string) []ConnRecord {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var out []ConnRecord
|
||||
for _, r := range s.recent {
|
||||
if h, _, err := net.SplitHostPort(r.Src); err == nil && h == ip {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Server) Serve(ln net.Listener) error {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go s.handle(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(5 * time.Minute))
|
||||
|
||||
info := tcpInfo(conn) // platform-specific; zero values off-Linux
|
||||
rec := ConnRecord{
|
||||
ConnectedAt: time.Now().UTC(),
|
||||
Src: conn.RemoteAddr().String(),
|
||||
MSS: info.MSS,
|
||||
Options: info.Options,
|
||||
}
|
||||
s.record(rec)
|
||||
|
||||
greeting, _ := json.Marshal(map[string]any{
|
||||
"observed_src": rec.Src,
|
||||
"mss": rec.MSS,
|
||||
"options": rec.Options,
|
||||
})
|
||||
if _, err := conn.Write(append(greeting, '\n')); err != nil {
|
||||
return
|
||||
}
|
||||
// Byte-echo until FIN; the client's data is its own to interpret.
|
||||
_, _ = io.Copy(conn, conn)
|
||||
}
|
||||
Reference in New Issue
Block a user