Files
mrambossekandClaude Opus 5 7b676e666e
server-test / test (push) Successful in 27s
server-release / image (push) Successful in 14s
server-release / release (push) Successful in 27s
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>
2026-07-31 19:53:36 +02:00

138 lines
4.1 KiB
Go

// SPDX-FileCopyrightText: 2026 Echolot contributors
// SPDX-License-Identifier: GPL-3.0-or-later
package stun
import (
"encoding/binary"
"net"
"net/netip"
"testing"
"time"
)
// bindingRequest builds a minimal RFC 5389 binding request.
func bindingRequest(txid [12]byte, change byte) []byte {
var attrs []byte
if change != 0 {
attrs = append(attrs, attrHeader(attrChangeRequest, 4)...)
attrs = append(attrs, 0, 0, 0, change)
}
msg := make([]byte, 20, 20+len(attrs))
binary.BigEndian.PutUint16(msg[0:2], typeBindingRequest)
binary.BigEndian.PutUint16(msg[2:4], uint16(len(attrs)))
binary.BigEndian.PutUint32(msg[4:8], magicCookie)
copy(msg[8:20], txid[:])
return append(msg, attrs...)
}
// parseXorMapped extracts XOR-MAPPED-ADDRESS from a binding success.
func parseXorMapped(t *testing.T, resp []byte, txid [12]byte) netip.AddrPort {
t.Helper()
if binary.BigEndian.Uint16(resp[0:2]) != typeBindingSuccess {
t.Fatalf("type = %#x, want binding success", resp[0:2])
}
msgLen := int(binary.BigEndian.Uint16(resp[2:4]))
for off := 20; off+4 <= 20+msgLen; {
at := binary.BigEndian.Uint16(resp[off : off+2])
al := int(binary.BigEndian.Uint16(resp[off+2 : off+4]))
if at == attrXorMapped {
v := append([]byte(nil), resp[off+4:off+4+al]...)
port := binary.BigEndian.Uint16(v[2:4]) ^ uint16(magicCookie>>16)
var key [16]byte
binary.BigEndian.PutUint32(key[0:4], magicCookie)
copy(key[4:], txid[:])
for i := 4; i < len(v); i++ {
v[i] ^= key[i-4]
}
if v[1] == 0x01 {
return netip.AddrPortFrom(netip.AddrFrom4([4]byte(v[4:8])), port)
}
return netip.AddrPortFrom(netip.AddrFrom16([16]byte(v[4:20])), port)
}
off += 4 + al + (4-al%4)%4
}
t.Fatal("no XOR-MAPPED-ADDRESS in response")
return netip.AddrPort{}
}
func TestBindingAndChangePort(t *testing.T) {
// Two loopback "addresses" is not possible portably, so exercise one
// address (basic binding + change-port); the change-IP path needs the
// two-address grid of a real deployment.
srv, err := Listen([]string{"127.0.0.1:0"})
if err != nil {
t.Fatal(err)
}
// port 0 twice would collide at 1; rebind explicitly on free ports
srv.Close()
base := freePort(t)
srv, err = Listen([]string{netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), base).String()})
if err != nil {
t.Skipf("cannot bind %d/%d: %v", base, base+1, err)
}
defer srv.Close()
go srv.Serve()
client, err := net.DialUDP("udp", nil, srv.socks[0][0].conn.LocalAddr().(*net.UDPAddr))
if err != nil {
t.Fatal(err)
}
defer client.Close()
client.SetDeadline(time.Now().Add(2 * time.Second))
txid := NewTxID()
client.Write(bindingRequest(txid, 0))
buf := make([]byte, 1500)
n, err := client.Read(buf)
if err != nil {
t.Fatal(err)
}
mapped := parseXorMapped(t, buf[:n], txid)
want := client.LocalAddr().(*net.UDPAddr).AddrPort()
if mapped.Port() != want.Port() {
t.Fatalf("mapped port %d, want %d", mapped.Port(), want.Port())
}
// CHANGE-REQUEST(port): response must come from the alternate port.
// Dial-connected sockets drop packets from other sources, so use an
// unconnected socket and inspect the reply's source.
uc, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
if err != nil {
t.Fatal(err)
}
defer uc.Close()
uc.SetDeadline(time.Now().Add(2 * time.Second))
txid2 := NewTxID()
uc.WriteToUDPAddrPort(bindingRequest(txid2, changePort), srv.socks[0][0].addr)
n, from, err := uc.ReadFromUDPAddrPort(buf)
if err != nil {
t.Fatal(err)
}
if from.Port() != srv.socks[0][1].addr.Port() {
t.Fatalf("change-port reply came from %v, want alt port %d", from, srv.socks[0][1].addr.Port())
}
parseXorMapped(t, buf[:n], txid2)
}
func freePort(t *testing.T) uint16 {
t.Helper()
// Find two adjacent free ports for the primary/alternate pair.
for tries := 0; tries < 20; tries++ {
l, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
if err != nil {
continue
}
p := l.LocalAddr().(*net.UDPAddr).Port
l.Close()
l2, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: p + 1})
if err != nil {
continue
}
l2.Close()
return uint16(p)
}
t.Skip("no adjacent free UDP ports found")
return 0
}