A car that hangs up on the full service discovery request gets the short one next time

The 2019 Fiat 500's Uconnect drops the link within a second of the request with icons that 0.3.39
started sending, while a 2017 Hyundai needs them. Each car is remembered by its head unit's certificate.
This commit is contained in:
Seb3thehacker committed 2026-11-08 21:31:59 -08:00
1 parent 7fd19758a4
commit 5f87840e7e
3 files changed
+54 -4

No files matched your search

@@ -69,6 +69,8 @@ class GearslipRunner(
/** Waits for the certificate only when TLS is first needed; see [run]. */
private val tls: PhoneTls by lazy { identityTask.get() }
/** This session sent the short service discovery request. */
@Volatile private var plainDiscovery = false
private var authenticationFailed = false
private var drainWriterOnExit = false
@Volatile private var stopped = false
@@ -174,6 +176,9 @@ class GearslipRunner(
SessionReport.fail(link, "transport failed", t, state.name)
reportDisconnect()
} finally {
// The car hung up between our service discovery request and its answer: try the
// other kind of request with this car next time (see [ServiceDiscoveryQuirks]).
if (!stopped && state == State.WAIT_SDR) ServiceDiscoveryQuirks.droppedAfter(tls.headUnitKey, plainDiscovery)
running = false
// The car is gone, but nothing calls stop() when a wireless session drops: it waits
// for the next one. Close the music channel now, so the audio capture behind it,
@@ -496,14 +501,17 @@ class GearslipRunner(
* never answered the request when it had only the label and model (issue #47).
*/
private fun sendServiceDiscoveryRequest() {
val icons = runCatching(phoneIcons).getOrDefault(emptyList())
// A 2019 Fiat 500's Uconnect hangs up on the full request (issue #91), so a car that has
// done that before gets the short one Gearslip sent until 0.3.39: label and model only.
plainDiscovery = ServiceDiscoveryQuirks.wantsPlain(tls.headUnitKey)
val icons = if (plainDiscovery) emptyList() else runCatching(phoneIcons).getOrDefault(emptyList())
var body = ByteArray(0)
icons.take(3).forEachIndexed { i, png -> body += Protobuf.bytesField(i + 1, png) } // small, medium, large icon
body += Protobuf.stringField(4, "Gearslip") + // label_text
Protobuf.stringField(5, Build.MODEL) + // device_name
Protobuf.bytesField(6, Protobuf.stringField(1, java.util.UUID.randomUUID().toString())) // session ID
Protobuf.stringField(5, Build.MODEL) // device_name
if (!plainDiscovery) body += Protobuf.bytesField(6, Protobuf.stringField(1, java.util.UUID.randomUUID().toString())) // session ID
send(MSG_SERVICE_DISCOVERY_REQUEST, body, encrypted = true)
log.i("-> ServiceDiscoveryRequest (encrypted, ${icons.size} icons, ${body.size} bytes)")
log.i("-> ServiceDiscoveryRequest (encrypted, ${if (plainDiscovery) "short, as this car asked" else "${icons.size} icons"}, ${body.size} bytes)")
}
private fun onServiceDiscoveryResponse(body: ByteArray) {
@@ -25,6 +25,12 @@ class PhoneTls(keyStore: KeyStore, password: CharArray) {
private var netBuffer = ByteBuffer.allocate(32 * 1024)
@Volatile var handshakeComplete: Boolean = false
/**
* A short fingerprint of the head unit's certificate, known once TLS completes: the first
* thing that tells one car model from another, before it has described itself.
*/
@Volatile var headUnitKey: String? = null
private set
init {
@@ -166,6 +172,9 @@ class PhoneTls(keyStore: KeyStore, password: CharArray) {
try {
val peers = session.peerCertificates
log.i("head unit presented ${peers.size} certificate(s):")
headUnitKey = peers.firstOrNull()?.encoded?.let { bytes ->
java.security.MessageDigest.getInstance("SHA-256").digest(bytes).take(8).joinToString("") { "%02x".format(it) }
}
peers.forEachIndexed { index, certificate ->
if (certificate is X509Certificate) {
log.i(" [$index] subject=${certificate.subjectX500Principal}")
@@ -0,0 +1,33 @@
package app.seb3thehacker.gearslip
import app.seb3thehacker.gearslip.car.CarEnvironment
/**
* Which cars want the short service discovery request. Since 0.3.39 Gearslip sends what a real
* phone does: its icon at three sizes and a session ID. A 2017 Hyundai on protocol 1.0 ignored
* the request without them (issue #47), but a 2019 Fiat 500's Uconnect hangs up on it within a
* second (issue #91). A car that hangs up after one kind is sent the other next time, remembered
* by its head unit's certificate, so each car settles on whichever it answers.
*/
object ServiceDiscoveryQuirks {
private const val KEY = "sdr_plain_cars"
fun wantsPlain(car: String?): Boolean = car != null && car in cars()
/** The car hung up after [plain] (or the full) request: switch it to the other one. */
fun droppedAfter(car: String?, plain: Boolean) {
val context = CarEnvironment.appContext ?: return
car ?: return
val now = cars()
val next = if (plain) now - car else now + car
if (next == now) return
AppSettings.putString(context, KEY, next.joinToString(","))
GearslipLog.i("service discovery: the car hung up after the ${if (plain) "short" else "full"} request, so next time it gets the ${if (plain) "full" else "short"} one", "PROTO")
}
private fun cars(): Set<String> {
val context = CarEnvironment.appContext ?: return emptySet()
return AppSettings.getString(context, KEY, "").split(",").filter { it.isNotEmpty() }.toSet()
}
}