From a405048f2317e436872f9f68086335ada07bffc5 Mon Sep 17 00:00:00 2001 From: Konrad Neitzel Date: Wed, 7 Oct 2026 14:36:28 +0200 Subject: [PATCH] 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 --- station/cert.go | 22 +++++++++++++++++ station/protocol3_test.go | 52 +++++++++++++++++++++++++++++++++++++++ station/reflector_test.go | 26 ++++++++++++++------ station/session.go | 36 +++++++++++++++++++++------ 4 files changed, 120 insertions(+), 16 deletions(-) diff --git a/station/cert.go b/station/cert.go index 818c0a6..7017f4f 100644 --- a/station/cert.go +++ b/station/cert.go @@ -124,6 +124,9 @@ func prepareIdentity(dir, callsign string) (tls.Certificate, bool, string) { if msg != "" { return tls.Certificate{}, false, msg } + if msg := ensureCSR(dir, callsign); msg != "" { + return tls.Certificate{}, false, msg + } return pair, true, "" } if msg := ensureKeyAndCSR(dir, callsign, !keyExists); msg != "" { @@ -155,6 +158,25 @@ func loadPair(certPath, keyPath, callsign string) (tls.Certificate, string) { }, "" } +func ensureCSR(dir, callsign string) string { + csrPath := filepath.Join(dir, csrFileName) + _, err := os.Stat(csrPath) + if err == nil { + return "" + } + if !errors.Is(err, fs.ErrNotExist) { + return "Cannot create " + csrPath + } + key, err := loadKey(filepath.Join(dir, keyFileName)) + if err != nil { + return "Cannot create " + csrPath + } + if err := writeCSR(csrPath, key, callsign); err != nil { + return "Cannot create " + csrPath + } + return "" +} + func ensureKeyAndCSR(dir, callsign string, created bool) string { keyPath := filepath.Join(dir, keyFileName) var key *rsa.PrivateKey diff --git a/station/protocol3_test.go b/station/protocol3_test.go index 4e962a2..3c45047 100644 --- a/station/protocol3_test.go +++ b/station/protocol3_test.go @@ -697,6 +697,11 @@ func TestTLSUsesTheCallsignCertificate(t *testing.T) { t.Fatalf("presented %#v", peer) } v.refl.writeFrame(t, 12, nil) + typ, body := v.refl.readFrame(t) + if typ != 17 { + t.Fatalf("type %d, want the signing request written because none was stored", typ) + } + checkCSR(t, body, v.certDir) v.serverAndKey(t, []string{"OPUS"}) v.beat(t) } @@ -794,6 +799,53 @@ func TestHostMismatchRetriesOn3(t *testing.T) { } } +func TestMissingSigningRequestIsWritten(t *testing.T) { + v := startV3(t, "") + if err := os.Mkdir(v.certDir, 0o700); err != nil { + t.Fatal(err) + } + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + writeKey(t, filepath.Join(v.certDir, "key.pem"), key) + if err := os.WriteFile(filepath.Join(v.certDir, "cert.pem"), signClient(t, v.pki, key, "DL1ABC"), 0o644); err != nil { + t.Fatal(err) + } + v.throughBundle(t) + v.tls(t, v.pki.serverConfig()) + v.refl.writeFrame(t, 16, nil) + typ, body := v.refl.readFrame(t) + if typ != 17 { + t.Fatalf("type %d, want the signing request", typ) + } + checkCSR(t, body, v.certDir) +} + +func TestHandshakeThatDoesNotFinishRetriesOn3(t *testing.T) { + v := startV3(t, "") + v.throughBundle(t) + gen := v.clock.Generation() + v.refl.writeFrame(t, 15, nil) + waitClock(t, v.clock, gen) + v.clock.Advance(15 * time.Second) + if _, err := v.st.Wait(context.Background(), func(s station.State) bool { + return s.Detail == "TLS handshake failed" + }); err != nil { + t.Fatal(err) + } + v.clock.BlockUntilWaiting(t) + v.clock.Advance(2 * time.Second) + v.refl.accept(t) + typ, body := v.refl.readFrame(t) + if typ != 5 || !versionIs(body, 3, 0) { + t.Fatalf("retry type %d body %v, want 3.0", typ, body) + } + if v.st.State().Detail != "TLS handshake failed" { + t.Fatalf("Detail = %q", v.st.State().Detail) + } +} + func TestTLSHandshakeFailureRetriesOn3(t *testing.T) { v := startV3(t, "") v.throughBundle(t) diff --git a/station/reflector_test.go b/station/reflector_test.go index 0750bb0..06a2ead 100644 --- a/station/reflector_test.go +++ b/station/reflector_test.go @@ -148,15 +148,25 @@ func (r *reflector) conn() net.Conn { func newReflector(t *testing.T) *reflector { t.Helper() - 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 { + 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() - t.Fatal(err) + ln = nil + } + if udp == nil { + t.Fatal("udp port stayed in use") } t.Cleanup(func() { if ln != nil { diff --git a/station/session.go b/station/session.go index e49f41d..176c174 100644 --- a/station/session.go +++ b/station/session.go @@ -159,18 +159,34 @@ func (f *framed) Write(p []byte) (int, error) { func (f *framed) setMax(n uint32) { f.max.Store(n) } -func (f *framed) startTLS(ctx context.Context, cfg *tls.Config) error { +var errTLSTimeout = errors.New("tls timeout") + +func (s *Station) startTLS(ctx context.Context, f *framed, cfg *tls.Config, until time.Time) error { select { case f.upgrade <- cfg: case <-ctx.Done(): return ctx.Err() } _ = f.conn.SetReadDeadline(time.Now()) + wait := until.Sub(s.clock.Now()) + if wait < 0 { + wait = 0 + } + timer := s.clock.NewTimer(wait) + defer timer.Stop() select { case err := <-f.up: return err case <-ctx.Done(): + _ = f.conn.SetDeadline(time.Now()) return ctx.Err() + case <-timer.C(): + _ = f.conn.SetDeadline(time.Now()) + select { + case <-f.up: + case <-ctx.Done(): + } + return errTLSTimeout } } @@ -348,7 +364,11 @@ func (s *Station) session(ctx context.Context, conn net.Conn, major uint16) erro switch reason { case "tcp": tcpRx = now.Add(15 * time.Second) - err := s.onFrame(ctx, wire, major, tcpf, &ph, &authed, &client, &haveCert, done, &udpCh, &udpErr, &udpReady, &lastTCP) + limit := tcpRx + if udpRx.Before(limit) { + limit = udpRx + } + err := s.onFrame(ctx, wire, major, tcpf, &ph, &authed, &client, &haveCert, done, &udpCh, &udpErr, &udpReady, &lastTCP, limit) if err != nil { return err } @@ -428,7 +448,7 @@ func (s *Station) onClock(now time.Time) { } } -func (s *Station) onFrame(ctx context.Context, wire *framed, major uint16, f frame, ph *phase, authed *bool, client *tls.Certificate, haveCert *bool, done chan struct{}, udpCh *chan []byte, udpErr *chan error, udpReady *bool, lastTCP *time.Time) error { +func (s *Station) onFrame(ctx context.Context, wire *framed, major uint16, f frame, ph *phase, authed *bool, client *tls.Certificate, haveCert *bool, done chan struct{}, udpCh *chan []byte, udpErr *chan error, udpReady *bool, lastTCP *time.Time, limit time.Time) error { switch f.typ { case tcpHeartbeat: return nil @@ -443,7 +463,7 @@ func (s *Station) onFrame(ctx context.Context, wire *framed, major uint16, f fra if major < 3 { return s.onV2(wire, f, ph, authed, done, udpCh, udpErr, udpReady, lastTCP) } - return s.onV3(ctx, wire, f, ph, authed, client, haveCert, done, udpCh, udpErr, udpReady, lastTCP) + return s.onV3(ctx, wire, f, ph, authed, client, haveCert, done, udpCh, udpErr, udpReady, lastTCP, limit) } func (s *Station) onReflectorError(body []byte, major uint16) error { @@ -520,7 +540,7 @@ func (s *Station) answerChallenge(w io.Writer, body []byte, lastTCP *time.Time) return nil } -func (s *Station) onV3(ctx context.Context, wire *framed, f frame, ph *phase, authed *bool, client *tls.Certificate, haveCert *bool, done chan struct{}, udpCh *chan []byte, udpErr *chan error, udpReady *bool, lastTCP *time.Time) error { +func (s *Station) onV3(ctx context.Context, wire *framed, f frame, ph *phase, authed *bool, client *tls.Certificate, haveCert *bool, done chan struct{}, udpCh *chan []byte, udpErr *chan error, udpReady *bool, lastTCP *time.Time, limit time.Time) error { switch f.typ { case tcpCAInfo: if *ph != phCA { @@ -536,7 +556,7 @@ func (s *Station) onV3(ctx context.Context, wire *framed, f frame, ph *phase, au if *ph != phStart { return nil } - return s.onStartEncryption(ctx, wire, ph, client, haveCert) + return s.onStartEncryption(ctx, wire, ph, client, haveCert, limit) case tcpCsrReq: return s.sendCSR(wire, lastTCP) case tcpAuthChallenge: @@ -658,7 +678,7 @@ func (s *Station) onCABundle(w io.Writer, body []byte, ph *phase, lastTCP *time. return nil } -func (s *Station) onStartEncryption(ctx context.Context, wire *framed, ph *phase, client *tls.Certificate, haveCert *bool) error { +func (s *Station) onStartEncryption(ctx context.Context, wire *framed, ph *phase, client *tls.Certificate, haveCert *bool, limit time.Time) error { pair, present, fatal := prepareIdentity(s.cfg.certificateDir, s.cfg.callsign) if fatal != "" { s.endFatal(fatal, true) @@ -675,7 +695,7 @@ func (s *Station) onStartEncryption(ctx context.Context, wire *framed, ph *phase if err != nil { return s.retry("TLS handshake failed") } - if err := wire.startTLS(ctx, cfg); err != nil { + if err := s.startTLS(ctx, wire, cfg, limit); err != nil { if errors.Is(err, errHostMismatch) { return s.retry("Reflector certificate does not match the host") }