server: STUN, TCP echo, observations API, delayed-echo + connect-back actions
server-test / test (push) Successful in 27s
server-release / image (push) Successful in 14s
server-release / release (push) Successful in 27s

- 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:
mrambossek
2026-07-31 19:53:36 +02:00
co-authored by Claude Opus 5
parent 507a8bfc1f
commit 7b676e666e
10 changed files with 901 additions and 18 deletions
+91
View File
@@ -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)
}
+69
View File
@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2026 Echolot contributors
// SPDX-License-Identifier: GPL-3.0-or-later
//go:build linux
package tcpecho
import (
"encoding/binary"
"net"
"syscall"
"unsafe"
)
type connInfo struct {
MSS int
Options []string
}
// tcpInfo reads TCP_INFO via getsockopt. Only the head of struct tcp_info is
// needed: 8 header bytes (state..wscale flags) then u32 rto, ato, snd_mss,
// rcv_mss — layout is part of the kernel ABI and stable.
func tcpInfo(conn net.Conn) connInfo {
tc, ok := conn.(*net.TCPConn)
if !ok {
return connInfo{}
}
raw, err := tc.SyscallConn()
if err != nil {
return connInfo{}
}
var buf [104]byte
var got bool
_ = raw.Control(func(fd uintptr) {
l := uint32(len(buf))
_, _, errno := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd,
uintptr(syscall.SOL_TCP), uintptr(syscall.TCP_INFO),
uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&l)), 0)
got = errno == 0 && l >= 24
})
if !got {
return connInfo{}
}
// tcpi_options bit flags (include/uapi/linux/tcp.h)
const (
optTimestamps = 1
optSACK = 2
optWscale = 4
optECN = 8
)
var opts []string
ob := buf[5]
if ob&optTimestamps != 0 {
opts = append(opts, "timestamps")
}
if ob&optSACK != 0 {
opts = append(opts, "sack")
}
if ob&optWscale != 0 {
opts = append(opts, "wscale")
}
if ob&optECN != 0 {
opts = append(opts, "ecn")
}
return connInfo{
MSS: int(binary.LittleEndian.Uint32(buf[16:20])), // tcpi_snd_mss
Options: opts,
}
}
+17
View File
@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2026 Echolot contributors
// SPDX-License-Identifier: GPL-3.0-or-later
//go:build !linux
package tcpecho
import "net"
type connInfo struct {
MSS int
Options []string
}
// tcpInfo: TCP_INFO is Linux-only; other platforms report zero values and
// the greeting says mss:0 — honest absence rather than a guess.
func tcpInfo(net.Conn) connInfo { return connInfo{} }