Files
funknetz/station/reflector_test.go
T
konradandCursor a405048f23 Write a missing signing request and give up on a handshake that never returns.
A certificate already on disk still has to be renewable, and a reflector that never finishes TLS must not hold the session past the login timers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 14:36:28 +02:00

408 lines
8.6 KiB
Go

package station_test
import (
"crypto/tls"
"encoding/binary"
"encoding/json"
"io"
"net"
"os"
"sync"
"testing"
"time"
"gitea.neitzel.de/konrad/funknetz/station"
)
type fakeClock struct {
mu sync.Mutex
now time.Time
waiters []*fakeWaiter
gen int
}
type fakeWaiter struct {
deadline time.Time
ch chan time.Time
done bool
}
func newFakeClock() *fakeClock {
return &fakeClock{now: time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC)}
}
func (c *fakeClock) Now() time.Time {
c.mu.Lock()
defer c.mu.Unlock()
return c.now
}
func (c *fakeClock) NewTimer(d time.Duration) station.Timer {
c.mu.Lock()
defer c.mu.Unlock()
if d < 0 {
d = 0
}
ch := make(chan time.Time, 1)
c.gen++
w := &fakeWaiter{deadline: c.now.Add(d), ch: ch}
if d == 0 {
ch <- c.now
w.done = true
return &fakeTimer{c: c, w: w}
}
c.waiters = append(c.waiters, w)
return &fakeTimer{c: c, w: w}
}
func (c *fakeClock) Advance(d time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.now = c.now.Add(d)
kept := c.waiters[:0]
for _, w := range c.waiters {
if w.done {
continue
}
if !w.deadline.After(c.now) {
w.done = true
w.ch <- c.now
continue
}
kept = append(kept, w)
}
c.waiters = kept
}
func (c *fakeClock) Generation() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.gen
}
func waitClock(t *testing.T, c *fakeClock, after int) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for c.Generation() <= after {
if time.Now().After(deadline) {
t.Fatal("station did not rearm the clock")
}
time.Sleep(time.Millisecond)
}
}
func (c *fakeClock) BlockUntilWaiting(t *testing.T) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for {
c.mu.Lock()
n := 0
for _, w := range c.waiters {
if !w.done {
n++
}
}
c.mu.Unlock()
if n > 0 {
return
}
if time.Now().After(deadline) {
t.Fatal("station did not wait on the clock")
}
time.Sleep(time.Millisecond)
}
}
type fakeTimer struct {
c *fakeClock
w *fakeWaiter
}
func (t *fakeTimer) C() <-chan time.Time { return t.w.ch }
func (t *fakeTimer) Stop() bool {
t.c.mu.Lock()
defer t.c.mu.Unlock()
if t.w.done {
return false
}
t.w.done = true
return true
}
type reflector struct {
ln net.Listener
udp *net.UDPConn
tcp net.Conn
tls *tls.Conn
peer *net.UDPAddr
port int
}
func (r *reflector) conn() net.Conn {
if r.tls != nil {
return r.tls
}
return r.tcp
}
func newReflector(t *testing.T) *reflector {
t.Helper()
var ln net.Listener
var udp *net.UDPConn
var port int
for range 8 {
var err error
ln, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
port = ln.Addr().(*net.TCPAddr).Port
udp, err = net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: port})
if err == nil {
break
}
ln.Close()
ln = nil
}
if udp == nil {
t.Fatal("udp port stayed in use")
}
t.Cleanup(func() {
if ln != nil {
ln.Close()
}
udp.Close()
})
return &reflector{ln: ln, udp: udp, port: port}
}
func (r *reflector) accept(t *testing.T) {
t.Helper()
r.ln.(*net.TCPListener).SetDeadline(time.Now().Add(2 * time.Second))
conn, err := r.ln.Accept()
if err != nil {
t.Fatal(err)
}
r.tcp = conn
r.tls = nil
t.Cleanup(func() { conn.Close() })
}
func (r *reflector) readFrame(t *testing.T) (uint16, []byte) {
t.Helper()
conn := r.conn()
if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
t.Fatal(err)
}
var hdr [4]byte
if _, err := io.ReadFull(conn, hdr[:]); err != nil {
t.Fatal(err)
}
n := binary.BigEndian.Uint32(hdr[:])
if n > 32768 {
t.Fatalf("frame length %d", n)
}
buf := make([]byte, n)
if _, err := io.ReadFull(conn, buf); err != nil {
t.Fatal(err)
}
if len(buf) < 2 {
t.Fatalf("short frame %d", len(buf))
}
return binary.BigEndian.Uint16(buf[:2]), buf[2:]
}
func (r *reflector) writeFrame(t *testing.T, typ uint16, body []byte) {
t.Helper()
payload := make([]byte, 2+len(body))
binary.BigEndian.PutUint16(payload, typ)
copy(payload[2:], body)
var hdr [4]byte
binary.BigEndian.PutUint32(hdr[:], uint32(len(payload)))
conn := r.conn()
if err := conn.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil {
t.Fatal(err)
}
if _, err := conn.Write(append(hdr[:], payload...)); err != nil {
t.Fatal(err)
}
}
func (r *reflector) handshake(t *testing.T, cfg *tls.Config) error {
t.Helper()
r.tls = tls.Server(r.tcp, cfg)
if err := r.tls.SetDeadline(time.Now().Add(2 * time.Second)); err != nil {
return err
}
err := r.tls.Handshake()
_ = r.tls.SetDeadline(time.Time{})
if err != nil {
r.tls = nil
}
return err
}
func (r *reflector) readUDP(t *testing.T) (typ, id, seq uint16, body []byte) {
t.Helper()
if err := r.udp.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
t.Fatal(err)
}
buf := make([]byte, 2048)
n, addr, err := r.udp.ReadFromUDP(buf)
if err != nil {
t.Fatal(err)
}
r.peer = addr
if n < 6 {
t.Fatalf("short datagram %d", n)
}
typ = binary.BigEndian.Uint16(buf[0:2])
id = binary.BigEndian.Uint16(buf[2:4])
seq = binary.BigEndian.Uint16(buf[4:6])
body = append([]byte(nil), buf[6:n]...)
return typ, id, seq, body
}
func (r *reflector) readRawUDP(t *testing.T) []byte {
t.Helper()
if err := r.udp.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
t.Fatal(err)
}
buf := make([]byte, 2048)
n, addr, err := r.udp.ReadFromUDP(buf)
if err != nil {
t.Fatal(err)
}
r.peer = addr
return append([]byte(nil), buf[:n]...)
}
func (r *reflector) writeRawUDP(t *testing.T, pkt []byte) {
t.Helper()
if r.peer == nil {
t.Fatal("no udp peer")
}
if err := r.udp.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil {
t.Fatal(err)
}
if _, err := r.udp.WriteToUDP(pkt, r.peer); err != nil {
t.Fatal(err)
}
}
func (r *reflector) writeUDP(t *testing.T, typ, id, seq uint16, body []byte) {
t.Helper()
if r.peer == nil {
t.Fatal("no udp peer")
}
buf := make([]byte, 6+len(body))
binary.BigEndian.PutUint16(buf[0:2], typ)
binary.BigEndian.PutUint16(buf[2:4], id)
binary.BigEndian.PutUint16(buf[4:6], seq)
copy(buf[6:], body)
if err := r.udp.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil {
t.Fatal(err)
}
if _, err := r.udp.WriteToUDP(buf, r.peer); err != nil {
t.Fatal(err)
}
}
func appendU16(b []byte, v uint16) []byte {
var buf [2]byte
binary.BigEndian.PutUint16(buf[:], v)
return append(b, buf[:]...)
}
func appendU32(b []byte, v uint32) []byte {
var buf [4]byte
binary.BigEndian.PutUint32(buf[:], v)
return append(b, buf[:]...)
}
func appendStr(b []byte, s string) []byte {
b = appendU16(b, uint16(len(s)))
return append(b, s...)
}
func appendBytes(b []byte, p []byte) []byte {
b = appendU16(b, uint16(len(p)))
return append(b, p...)
}
func takeU16(t *testing.T, b []byte) (uint16, []byte) {
t.Helper()
if len(b) < 2 {
t.Fatalf("need 2 bytes, have %d", len(b))
}
return binary.BigEndian.Uint16(b[:2]), b[2:]
}
func takeU32(t *testing.T, b []byte) (uint32, []byte) {
t.Helper()
if len(b) < 4 {
t.Fatalf("need 4 bytes, have %d", len(b))
}
return binary.BigEndian.Uint32(b[:4]), b[4:]
}
func requireSoftwareIdentity(t *testing.T, body []byte) {
t.Helper()
raw, rest := takeStr(t, body)
if len(rest) != 0 {
t.Fatalf("software identity has %d trailing bytes", len(rest))
}
var got map[string]string
if err := json.Unmarshal([]byte(raw), &got); err != nil {
t.Fatalf("software identity %q: %v", raw, err)
}
if len(got) != 3 || got["sw"] != station.SoftwareName || got["swVer"] != station.SoftwareVersion || got["projVer"] != station.SoftwareVersion {
t.Fatalf("software identity = %#v, want sw, swVer, and projVer %q", got, station.SoftwareName)
}
if _, ok := got["machineArch"]; ok {
t.Fatal("protocol 2.0 node information includes machineArch")
}
}
func takeStr(t *testing.T, b []byte) (string, []byte) {
t.Helper()
n, rest := takeU16(t, b)
if len(rest) < int(n) {
t.Fatalf("string needs %d bytes, have %d", n, len(rest))
}
return string(rest[:n]), rest[n:]
}
func takeBytes(t *testing.T, b []byte) ([]byte, []byte) {
t.Helper()
n, rest := takeU16(t, b)
if len(rest) < int(n) {
t.Fatalf("bytes need %d, have %d", n, len(rest))
}
return append([]byte(nil), rest[:n]...), rest[n:]
}
func serverInfo(clientID uint16, nodes, codecs []string) []byte {
b := appendU16(nil, 0)
b = appendU16(b, clientID)
b = appendU16(b, uint16(len(nodes)))
for _, n := range nodes {
b = appendStr(b, n)
}
b = appendU16(b, uint16(len(codecs)))
for _, c := range codecs {
b = appendStr(b, c)
}
return b
}
func writeConfigFile(t *testing.T, body string) string {
t.Helper()
path := t.TempDir() + "/funknetz.json"
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
return path
}