A connection opening with a TLS handshake (first byte 0x16) and ALPN elt-echo gets the ClientHello it sent back raw (b64) and as a JA4 fingerprint (sec.clienthello_echo), then a TLS byte-echo; plain connections are unchanged. One port, multiplexed by a timed peek: plain echo is server-speaks-first, so a silent client (peek timeout) is greeted, while a TLS client's immediate ClientHello (0x16) routes to the TLS path — 500ms tolerates ~1s RTT before misdetection. JA4 (FoxIO): full ClientHello parser (ciphers, extensions, ALPN, supported_versions, sig algs) with GREASE exclusion; a_b_c fingerprint, unit-tested for structure + GREASE invariance. Live-verified: elt-echo negotiated, JA4 t13d1712eo computed, 1530-byte ClientHello returned. Capability tls-echo. This completes spec §4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
54 lines
1.2 KiB
Go
54 lines
1.2 KiB
Go
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package tcpecho
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Plain echo must be server-speaks-first: a client that sends nothing still
|
|
// gets the greeting (via the peek timeout), then its bytes are echoed.
|
|
func TestPlainEchoServerSpeaksFirst(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
go (&Server{}).Serve(ln)
|
|
|
|
c, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer c.Close()
|
|
c.SetDeadline(time.Now().Add(3 * time.Second))
|
|
|
|
line, err := bufio.NewReader(c).ReadBytes('\n')
|
|
if err != nil {
|
|
t.Fatalf("no greeting: %v", err)
|
|
}
|
|
var g struct {
|
|
TLS bool `json:"tls"`
|
|
Src string `json:"observed_src"`
|
|
}
|
|
if err := json.Unmarshal(line, &g); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if g.TLS {
|
|
t.Fatal("plain connection reported tls=true")
|
|
}
|
|
if g.Src == "" {
|
|
t.Fatal("greeting missing observed_src")
|
|
}
|
|
c.Write([]byte("xyz"))
|
|
buf := make([]byte, 3)
|
|
if _, err := c.Read(buf); err != nil || string(buf) != "xyz" {
|
|
t.Fatalf("echo failed: %q %v", buf, err)
|
|
}
|
|
}
|