Speak protocol 3.0 and fall back to 2.0 only when that is the reflector's maximum.

A reflector that has moved to 3.0 will not accept a station that always announces 2.0, and a 2.x reflector still needs one explicit plaintext session.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 14:24:00 +02:00
co-authored by Cursor
parent 0930d4b727
commit 3d1a2a1669
24 changed files with 3180 additions and 244 deletions
+42 -13
View File
@@ -45,17 +45,13 @@ func loginLog(t *testing.T, callsign, key string, tg int, id uint16, codecs []st
t.Cleanup(cancel)
go func() { _ = st.Run(ctx) }()
refl.accept(t)
typ, body := refl.readFrame(t)
if typ != 5 || len(body) != 4 || body[1] != 2 {
t.Fatalf("protocol frame type %d body %v", typ, body)
}
declineTo2(t, refl)
challenge := make([]byte, 20)
for i := range challenge {
challenge[i] = byte(i + 1)
}
refl.writeFrame(t, 10, appendBytes(nil, challenge))
typ, body = refl.readFrame(t)
typ, body := refl.readFrame(t)
if typ != 11 {
t.Fatalf("auth type %d", typ)
}
@@ -88,12 +84,41 @@ func loginLog(t *testing.T, callsign, key string, tg int, id uint16, codecs []st
if utyp != 1 || gotID != id || seq != 0 || len(ubody) != 0 {
t.Fatalf("udp heartbeat type %d id %d seq %d len %d", utyp, gotID, seq, len(ubody))
}
if _, err := st.Wait(ctx, func(s station.State) bool { return s.Connection == "Connected" }); err != nil {
got, err := st.Wait(ctx, func(s station.State) bool { return s.Connection == "Connected" })
if err != nil {
t.Fatal(err)
}
if got.Protocol != "2.0" {
t.Fatalf("Protocol = %q, want 2.0", got.Protocol)
}
return loggedIn{st: st, clock: clock, refl: refl, cancel: cancel, id: id}
}
func declineTo2(t *testing.T, refl *reflector) {
t.Helper()
typ, body := refl.readFrame(t)
if typ != 5 || !versionIs(body, 3, 0) {
t.Fatalf("first version type %d body %v, want 3.0", typ, body)
}
refl.writeFrame(t, 6, versionWire(2, 0))
refl.accept(t)
typ, body = refl.readFrame(t)
if typ != 5 || !versionIs(body, 2, 0) {
t.Fatalf("fallback version type %d body %v, want 2.0", typ, body)
}
}
func versionWire(major, minor uint16) []byte {
return appendU16(appendU16(nil, major), minor)
}
func versionIs(body []byte, major, minor uint16) bool {
if len(body) != 4 {
return false
}
return binary.BigEndian.Uint16(body[0:2]) == major && binary.BigEndian.Uint16(body[2:4]) == minor
}
func TestQuietSessionSendsHeartbeats(t *testing.T) {
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
@@ -127,12 +152,15 @@ func TestDropRetriesUntilQuit(t *testing.T) {
}); err != nil {
t.Fatal(err)
}
if got := in.st.State(); got.Protocol != "" {
t.Fatalf("Protocol = %q, want blank while disconnected", got.Protocol)
}
in.clock.BlockUntilWaiting(t)
in.clock.Advance(2 * time.Second)
in.refl.accept(t)
typ, body := in.refl.readFrame(t)
if typ != 5 || body[1] != 2 {
t.Fatalf("retry protocol type %d body %v", typ, body)
if typ != 5 || !versionIs(body, 3, 0) {
t.Fatalf("retry protocol type %d body %v, want 3.0", typ, body)
}
in.cancel()
in.clock.Advance(2 * time.Second)
@@ -240,7 +268,7 @@ func TestMissingOpusStopsRetries(t *testing.T) {
defer cancel()
go func() { _ = st.Run(ctx) }()
refl.accept(t)
refl.readFrame(t)
declineTo2(t, refl)
challenge := make([]byte, 20)
refl.writeFrame(t, 10, appendBytes(nil, challenge))
refl.readFrame(t)
@@ -508,14 +536,15 @@ func expectAudio(t *testing.T, refl *reflector, dec *opus.Decoder, id, seq uint1
}
func tryFrame(r *reflector) (uint16, []byte, bool) {
_ = r.tcp.SetReadDeadline(time.Now().Add(30 * time.Millisecond))
conn := r.conn()
_ = conn.SetReadDeadline(time.Now().Add(30 * time.Millisecond))
var hdr [4]byte
if _, err := io.ReadFull(r.tcp, hdr[:]); err != nil {
if _, err := io.ReadFull(conn, hdr[:]); err != nil {
return 0, nil, false
}
n := binary.BigEndian.Uint32(hdr[:])
buf := make([]byte, n)
if _, err := io.ReadFull(r.tcp, buf); err != nil {
if _, err := io.ReadFull(conn, buf); err != nil {
return 0, nil, false
}
if len(buf) < 2 {