// 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) }