From 606329ce9b43011051581c3b4a52292108f2b234 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 12 Oct 2026 14:41:53 -0500 Subject: [PATCH] Hang up on a message the pod can't use instead of exiting Pairing, session setup and the command loop exited the pod with log.Fatal when the phone's message didn't parse, decrypt or unmarshal, and the packet layer exited on an unexpected command. In the Simulator nothing restarts it, so one bad message cost the pod. The pod now logs the error, hangs up and waits for the phone to reconnect and set up a new session. The packet layer logs and carries on. Errors in the pod's own work (generating keys, saving state, building responses) still exit. --- pkg/bluetooth/bluetooth.go | 14 ++++--- pkg/pod/pod.go | 64 ++++++++++++++++++++-------- pkg/pod/pod_test.go | 85 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 pkg/pod/pod_test.go diff --git a/pkg/bluetooth/bluetooth.go b/pkg/bluetooth/bluetooth.go index db1b6a2..b46aa24 100644 --- a/pkg/bluetooth/bluetooth.go +++ b/pkg/bluetooth/bluetooth.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "encoding/hex" "errors" + "fmt" "hash/crc32" "sync" "time" @@ -323,8 +324,11 @@ func (b *Ble) loop(stop chan bool, in *input) { case <-stop: return case msg := <-b.messageOutput: - if err := b.writeMessage(msg, in, stop); err != nil { + if err := b.writeMessage(msg, in, stop); err == errStopped { return + } else if err != nil { + // The phone gets no answer and asks again. + log.Errorf("pkg bluetooth; error writing message: %s", err) } case cmd := <-in.cmd: msg, err := b.readMessage(cmd, in, stop) @@ -373,8 +377,8 @@ func (b *Ble) expectCommand(expected Packet, in *input, stop chan bool) error { if err != nil { return err } - if !bytes.Equal(expected[:1], cmd[:1]) { - log.Fatalf("pkg bluetooth; expected command: %s. received command: %s", expected, cmd) + if len(cmd) == 0 || cmd[0] != expected[0] { + return fmt.Errorf("expected command: %x. received command: %x", expected, cmd) } return nil } @@ -467,8 +471,8 @@ func (b *Ble) readMessage(cmd Packet, in *input, stop chan bool) (*message.Messa var checksum []byte log.Trace("pkg bluetooth; Reading RTS") - if !bytes.Equal(CmdRTS[:1], cmd[:1]) { - log.Fatalf("pkg bluetooth; expected command: %x. received command: %x", CmdRTS, cmd) + if len(cmd) == 0 || cmd[0] != CmdRTS[0] { + return nil, fmt.Errorf("expected command: %x. received command: %x", CmdRTS, cmd) } log.Trace("pkg bluetooth; Sending CTS") diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index a32bced..a50d1c8 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -2,6 +2,7 @@ package pod import ( "encoding/json" + "fmt" "sync" "time" @@ -109,6 +110,14 @@ func (p *Pod) readSetup() (*message.Message, bool) { } } +// startOver hangs up on a phone whose message the pod can't use. The phone +// reconnects and sets up a new session. +func (p *Pod) startOver(format string, args ...interface{}) { + log.Errorf("pkg pod; "+format+"; hanging up", args...) + p.ble.ShutdownConnection() + go p.StartAcceptingCommands() +} + func (p *Pod) StartActivation() { pair := &pair.Pair{} @@ -117,14 +126,16 @@ func (p *Pod) StartActivation() { return } if err := pair.ParseSP1SP2(msg); err != nil { - log.Fatalf("pkg pod; error parsing SP1SP2 %s", err) + p.startOver("error parsing SP1SP2 %s", err) + return } // read PDM public key and nonce if msg, ok = p.readSetup(); !ok { return } if err := pair.ParseSPS1(msg); err != nil { - log.Fatalf("pkg pod; error parsing SPS1 %s", err) + p.startOver("error parsing SPS1 %s", err) + return } msg, err := pair.GenerateSPS1() @@ -138,7 +149,10 @@ func (p *Pod) StartActivation() { if msg, ok = p.readSetup(); !ok { return } - pair.ParseSPS2(msg) + if err := pair.ParseSPS2(msg); err != nil { + p.startOver("error parsing SPS2 %s", err) + return + } // send POD conf value msg, err = pair.GenerateSPS2() @@ -153,7 +167,8 @@ func (p *Pod) StartActivation() { } err = pair.ParseSP0GP0(msg) if err != nil { - log.Fatalf("pkg pod; could not parse SP0GP0: %s", err) + p.startOver("could not parse SP0GP0: %s", err) + return } // send P0 constant @@ -184,7 +199,8 @@ func (p *Pod) EapAka() { } err := session.ParseChallenge(msg) if err != nil { - log.Fatalf("pkg pod; error parsing the EAP-AKA challenge: %s", err) + p.startOver("error parsing the EAP-AKA challenge: %s", err) + return } msg, err = session.GenerateChallengeResponse() @@ -199,7 +215,8 @@ func (p *Pod) EapAka() { log.Debugf("pkg pod; success? %x", msg.Payload) // TODO: figure out how error looks like err = session.ParseSuccess(msg) if err != nil { - log.Fatalf("pkg pod; error parsing the EAP-AKA Success packet: %s", err) + p.startOver("error parsing the EAP-AKA Success packet: %s", err) + return } p.state.CK, p.state.NoncePrefix = session.CKNoncePrefix() @@ -272,17 +289,28 @@ func (p *Pod) CommandLoop(pMsg PodMsgBody) { decrypted, err := encrypt.DecryptMessage(p.state.CK, p.state.NoncePrefix, p.state.NonceSeq, msg) if err != nil { - log.Fatalf("pkg pod; could not decrypt message: %s", err) + p.mtx.Unlock() + p.startOver("could not decrypt message: %s", err) + return } p.state.NonceSeq++ + if len(decrypted.Payload) < 16 { + p.mtx.Unlock() + p.startOver("decrypted payload too short: %x", decrypted.Payload) + return + } cmd, err := command.Unmarshal(decrypted.Payload) if err != nil { - log.Fatalf("pkg pod; could not unmarshal command: %s", err) + p.mtx.Unlock() + p.startOver("could not unmarshal command: %s", err) + return } cmdSeq, requestID, err := cmd.GetHeaderData() if err != nil { - log.Fatalf("pkg pod; could not get command header data: %s", err) + p.mtx.Unlock() + p.startOver("could not get command header data: %s", err) + return } p.state.CmdSeq = cmdSeq @@ -290,9 +318,6 @@ func (p *Pod) CommandLoop(pMsg PodMsgBody) { data = decrypted.Payload n = len(data) log.Debugf("pkg pod; len = %d", n) - if n < 16 { - log.Fatalf("pkg pod; decrypted. Payload too short") - } pMsg.MsgBodyCommand = data[13 : n-5] if data[13] == 0x1c { pMsg.DeactivateFlag = true @@ -370,16 +395,19 @@ func (p *Pod) CommandLoop(pMsg PodMsgBody) { } // TODO check for SEQ numbers here and the Ack flag decrypted, err = encrypt.DecryptMessage(p.state.CK, p.state.NoncePrefix, p.state.NonceSeq, msg) - if err != nil { - log.Fatalf("pkg pod; could not decrypt message: %s", err) - } - p.state.NonceSeq++ - if len(decrypted.Payload) != 0 { - log.Fatalf("pkg pod; this should be empty message with ACK header %s", spew.Sdump(msg)) + if err == nil { + p.state.NonceSeq++ + if len(decrypted.Payload) != 0 { + err = fmt.Errorf("not an empty ACK: %s", spew.Sdump(msg)) + } } p.state.Save() p.takeSnapshot() p.mtx.Unlock() + if err != nil { + p.startOver("could not read the response ACK: %s", err) + return + } log.Debugf("notifyingStateChange") p.notifyStateChange() diff --git a/pkg/pod/pod_test.go b/pkg/pod/pod_test.go new file mode 100644 index 0000000..274033a --- /dev/null +++ b/pkg/pod/pod_test.go @@ -0,0 +1,85 @@ +package pod + +import ( + "path/filepath" + "sync" + "testing" + "time" + + "github.com/avereha/pod/pkg/bluetooth" + "github.com/avereha/pod/pkg/message" +) + +// radio carries one side's notifications to the other side, and records +// when the pod hangs up. +type radio struct { + mu sync.Mutex + peer *bluetooth.Ble + hungUp chan bool +} + +func (r *radio) to() *bluetooth.Ble { + r.mu.Lock() + defer r.mu.Unlock() + return r.peer +} + +func (r *radio) NotifyCmd(p bluetooth.Packet) error { + if b := r.to(); b != nil { + b.CmdWritten(p) + } + return nil +} + +func (r *radio) NotifyData(p bluetooth.Packet) error { + if b := r.to(); b != nil { + b.DataWritten(p) + } + return nil +} + +func (r *radio) Advertise([]byte) error { return nil } +func (r *radio) AdvertiseState(bluetooth.AdvertState) error { return nil } + +func (r *radio) Disconnect() { + select { + case r.hungUp <- true: + default: + } +} + +func newBle(t *testing.T, r *radio) *bluetooth.Ble { + b, err := bluetooth.New(func(*bluetooth.Ble) (bluetooth.Radio, error) { return r, nil }) + if err != nil { + t.Fatal(err) + } + return b +} + +// A session message the pod can't parse ends the connection, not the pod. +func TestAGarbledSessionMessageHangsUp(t *testing.T) { + toPhone, toPod := &radio{hungUp: make(chan bool, 1)}, &radio{} + podBle, phone := newBle(t, toPhone), newBle(t, toPod) + toPod.peer = podBle + toPhone.peer = phone + + state := NewFreshState(filepath.Join(t.TempDir(), "state.toml")) + state.LTK = make([]byte, 16) + p := New(podBle, state) + + podBle.Connected() + go p.StartAcceptingCommands() + podBle.CmdWritten(bluetooth.Packet{6, 1, 4}) + phone.Connected() + phone.StartMessageLoop() + + garbled := message.NewMessage(message.MessageTypeSessionEstablishment, []byte{1, 2, 3, 4}, []byte{5, 6, 7, 8}) + garbled.Payload = []byte{0x2b, 0x97, 0x1c, 0x90, 0xb9, 0xdc, 0x43, 0x9b} + phone.WriteMessage(garbled) + + select { + case <-toPhone.hungUp: + case <-time.After(2 * time.Second): + t.Fatal("the pod did not hang up") + } +}