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
@@ -11,9 +11,11 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"echo-lot.app/server/internal/session"
|
||||
@@ -27,6 +29,7 @@ const (
|
||||
TypeEchoResp = 0x02
|
||||
TypeTimesyncReq = 0x07
|
||||
TypeTimesyncRsp = 0x08
|
||||
TypeDelayedEcho = 0x0B
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
@@ -34,13 +37,22 @@ type Server struct {
|
||||
// Epoch for server-side t_rx/t_tx: process start; observation consumers
|
||||
// only need differences plus the timesync exchange, not absolute time.
|
||||
start time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
conns []*net.UDPConn
|
||||
}
|
||||
|
||||
// Serve runs the read loop for one socket; call once per bound address.
|
||||
// The socket is retained so actions (delayed echo) can pick a family-matching
|
||||
// sender later.
|
||||
func (s *Server) Serve(conn *net.UDPConn) error {
|
||||
s.start = time.Now()
|
||||
s.mu.Lock()
|
||||
if s.start.IsZero() {
|
||||
s.start = time.Now()
|
||||
}
|
||||
s.conns = append(s.conns, conn)
|
||||
s.mu.Unlock()
|
||||
buf := make([]byte, 65535)
|
||||
oob := make([]byte, 0)
|
||||
_ = oob // TODO: recvmsg w/ IP_RECVTOS+IP_RECVTTL via golang.org/x/net for TTL/DSCP/ECN observation
|
||||
for {
|
||||
n, raddr, err := conn.ReadFromUDPAddrPort(buf)
|
||||
if err != nil {
|
||||
@@ -51,6 +63,36 @@ func (s *Server) Serve(conn *net.UDPConn) error {
|
||||
}
|
||||
}
|
||||
|
||||
// connFor picks a retained socket whose family matches the target.
|
||||
func (s *Server) connFor(target netip.AddrPort) *net.UDPConn {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
want4 := target.Addr().Unmap().Is4()
|
||||
for _, c := range s.conns {
|
||||
la := c.LocalAddr().(*net.UDPAddr).AddrPort()
|
||||
if la.Addr().Unmap().Is4() == want4 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendDelayedEcho fires one DELAYED_ECHO packet at the session's observed
|
||||
// data-plane source (spec §5: the NAT-mapping-lifetime primitive). The
|
||||
// payload carries the action id for correlation.
|
||||
func (s *Server) SendDelayedEcho(sess *session.Session, actionID string) error {
|
||||
target := sess.DataSource()
|
||||
if !target.IsValid() {
|
||||
return fmt.Errorf("session has no observed data-plane source yet")
|
||||
}
|
||||
conn := s.connFor(target)
|
||||
if conn == nil {
|
||||
return fmt.Errorf("no data-plane socket matches target family")
|
||||
}
|
||||
s.send(conn, target, sess, TypeDelayedEcho, 0, []byte(actionID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// handle enforces spec §3.1/§3.4: unknown prefix, bad HMAC, expired session,
|
||||
// replayed seq → silent drop, never a response.
|
||||
func (s *Server) handle(conn *net.UDPConn, raddr netip.AddrPort, pkt []byte, tRxNs int64) {
|
||||
@@ -80,6 +122,10 @@ func (s *Server) handle(conn *net.UDPConn, raddr netip.AddrPort, pkt []byte, tRx
|
||||
return
|
||||
}
|
||||
sess.NoteDataSource(raddr)
|
||||
sess.RecordUDP(session.UDPObservation{
|
||||
Seq: seq, TRxNs: tRxNs, TTxNs: time.Since(s.start).Nanoseconds(),
|
||||
Src: raddr.String(), Size: len(pkt), Type: typ,
|
||||
})
|
||||
|
||||
switch typ {
|
||||
case TypeEchoReq:
|
||||
|
||||
Reference in New Issue
Block a user