Skip to content

fix(bonjour): send the CarPlay control receiver device ID in decimal - #52

Merged
shilapi merged 1 commit into
shilapi:masterfrom
BBleae:fix/carplay-ctrl-device-id-decimal
Oct 15, 2026
Merged

shilapi merged 1 commit into
shilapi:masterfrom
BBleae:fix/carplay-ctrl-device-id-decimal

Conversation

@BBleae

@BBleae BBleae commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The GET /ctrl-int/1/connect probe sends AirPlay-Receiver-Device-ID as colon-less hex. Apple's CarPlay control sender instead serializes the same 48-bit device ID as an unsigned decimal integer.

Device ID Before After
02:00:00:00:00:02 020000000002 2199023255554

Primary evidence: Apple-signed CarPlaySDK

This was checked directly against the CarPlaySDK shipped in Xcode 27.0 (27A266a), rather than inferred only from another open-source implementation:

/Applications/Xcode.app/Contents/SharedFrameworks/DeviceKit.framework/Versions/A/PlugIns/CarPlaySimulator.devicekitplugin/Contents/Frameworks/CarPlaySDK.framework/Versions/A/CarPlaySDK
  • Bundle identifier: com.apple.CarPlaySDK
  • Signature chain: Software Signing -> Apple Code Signing Certification Authority -> Apple Root CA
  • codesign --verify --deep --strict passes for CarPlaySimulator.devicekitplugin and its embedded CarPlaySDK.framework.
  • SHA-256: 574255f38be2b4841dabdb42ca03311a8ef6dc4b9e3834ba5e375195d8b8d7fd
  • CDHash: 1366efa42bbb3b7ba85a51fe3073bf4b37d2ba81

The signature and disassembly can be reproduced with standard macOS tools:

FRAMEWORK=/Applications/Xcode.app/Contents/SharedFrameworks/DeviceKit.framework/Versions/A/PlugIns/CarPlaySimulator.devicekitplugin/Contents/Frameworks/CarPlaySDK.framework
BIN="$FRAMEWORK/Versions/A/CarPlaySDK"
codesign --verify --strict --verbose=4 "$FRAMEWORK"
codesign -dv --verbose=4 "$BIN"
shasum -a 256 "$BIN"
otool -arch arm64e -tvV -p _AirPlayGetDeviceID "$BIN"
otool -arch arm64e -tvV -p __CarPlayControlClientEnsureStarted "$BIN"
otool -arch arm64e -tvV -p __CarPlayControlClientConnect "$BIN"
otool -arch arm64e -tvV -p __UpdateBonjourAirPlay "$BIN"
otool -arch arm64e -tvV -p _HardwareAddressToCString "$BIN"

The relevant ARM64e data flow is unambiguous:

  1. _AirPlayGetDeviceID at 0x4974 reads the six hardware-address bytes and returns
    b0 << 40 | b1 << 32 | b2 << 24 | b3 << 16 | b4 << 8 | b5.
  2. __CarPlayControlClientEnsureStarted calls it at 0x2806ac and stores the resulting unsigned 48-bit integer in the control client at +0x58.
  3. __CarPlayControlClientConnect constructs GET /ctrl-int/1/connect, loads that integer at 0x1a558, then passes the literal AirPlay-Receiver-Device-ID, the format string %llu, and the integer to _HTTPHeader_SetField at 0x1a578.

This produces unsigned decimal ASCII with neither colons nor a 0x prefix.

The same signed SDK itself uses field-specific serialization. _UpdateBonjourAirPlay passes the same six bytes through _HardwareAddressToCString before setting the _airplay._tcp TXT key deviceid; that helper emits uppercase hex and inserts : between bytes. The TXT record remaining 02:00:00:00:00:02 while this private HTTP header becomes 2199023255554 therefore matches the two code paths in Apple's implementation.

For comparison, a public source mirror of CarPlayControlClient contains the same operation (call site):

err = HTTPHeader_AddFieldF(
    &httpMessage->header,
    "AirPlay-Receiver-Device-ID",
    "%llu",
    inClient->deviceID);

Additional implementation examples also agree:

Physical-device cold-start ablation

The change was also tested end to end with a paired physical iPhone and an Android device using 5 GHz Wi-Fi P2P. Each counted run started a fresh app process, removed the previous P2P group, reset the Bluetooth ACL, and waited up to 90 seconds. Success required all of the following, not merely an HTTP response: reverse RTSP, pair verification, a type-110 H.265 screen stream, and a frame rendered by the decoder.

Variant Result Observation
Decimal Device-ID 2/2 passed HTTP/1.1 200 OK, reverse RTSP, pair-verify, type-110 stream, first frame rendered
Only revert Device-ID to colon-less hex 0/3 passed All three probes still returned HTTP/1.1 200 OK, but no reverse RTSP appeared during the 90-second harness observation window
Decimal Device-ID with the existing Connection: close 2/2 passed Both sessions completed; no keep-alive dependency was observed

A subsequent run after restoring the decimal value completed MFi authentication, wireless iAP2 handoff, pair verification, type-110 H.265 setup, and rendered the first video frame. In these trials, the hex variant still received 200 OK but did not trigger reverse RTSP, while the decimal runs completed the session; an HTTP success response alone was therefore not a sufficient end-to-end success criterion.

Representative log markers
# Decimal, successful cold start
10-13 23:21:46.329  wireless bonjour: ... statusLine=HTTP/1.1 200 OK
10-13 23:21:46.393  airplay rx POST /pair-verify cseq=0 body=37
10-13 23:21:47.072  airplay SETUP stream type=110 ...
10-13 23:21:47.205  video decoder rendered first frame bytes=1

# Colon-less hex, three separate cold starts
10-13 23:22:49.273  wireless bonjour: ... statusLine=HTTP/1.1 200 OK
10-13 23:24:37.006  wireless bonjour: ... statusLine=HTTP/1.1 200 OK
10-13 23:26:26.927  wireless bonjour: ... statusLine=HTTP/1.1 200 OK
# No later reverse RTSP, pair-verify, SETUP, or first-frame marker was captured
# during each 90 s harness observation window.

Evidence scope: the binary analysis proves Apple's official accessory/head-unit sender format, which is the same protocol role implemented here. It is not an iPhone-parser reverse engineering result and does not claim that every iOS release rejects every non-decimal representation. The physical-device trials show that this paired iPhone completed the session with decimal values and did not open reverse RTSP for the three hex runs under the stated harness conditions.

Changes

  • connectProbeRequest converts the 48-bit MAC to unsigned decimal. Colons are optional; the ID must have exactly 12 hex digits. Every current deviceId source (the fixed 02:00:00:00:00:02, the P2P/LocalOnly/manual BSSID formatters, and MacAddress.toString()) already produces this shape.
  • The probe socket is bound to the advertised local address only when it has the target's address family. Without the guard, IPv4-local/IPv6-remote failed with EAFNOSUPPORT, while IPv6-local/IPv4-remote failed with ENETUNREACH, both before connect could select a usable source address.

Automated verification

  • :shared:testDebugUnitTest: 145 tests, 0 failures, 0 errors
  • :common:testDebugUnitTest: 38 tests, 0 failures, 0 errors
  • :mobile:assembleDebug: passed
  • CarPlayBonjourTest covers:
    • de:ad:be:ef:ca:fe -> 244837814094590
    • ff:ff:ff:ff:ff:ff -> 281474976710655 (maximum unsigned 48-bit value)
    • deadbeefcafe -> 244837814094590
  • All expected values were independently checked with int(hex, 16).
  • :shared:lintDebug reports the same 63 pre-existing errors before and after this change; this PR introduces no new lint issue.

The /ctrl-int/1/connect probe sent AirPlay-Receiver-Device-ID as colon-less
hex. CarPlayControlClient formats this header with "%llu" from the 64-bit
device ID, so convert the 48-bit MAC to its unsigned decimal value and
require exactly 12 hex digits.

Only bind the probe socket to the advertised local address when it is the
same address family as the target, since a cross-family bind always fails.
Copilot AI balanced review requested due to automatic review settings October 14, 2026 06:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change is small and self-contained, the decimal conversion and family-matched bind logic are verified correct, all device-ID sources produce valid 12-hex-digit MACs, and the new behavior is covered by tests.

0 open findings

What changed in this PR

This PR fixes the GET /ctrl-int/1/connect CarPlay control probe so that the AirPlay-Receiver-Device-ID header is sent as the unsigned decimal value of the 48-bit device-ID MAC (e.g. 02:00:00:00:00:02 → 2199023255554) instead of colon-less hex. This matches Apple's CarPlayControlClient (%llu formatting) and other independent implementations, improving interoperability of the receiver-probe path in CarPlayBonjour. It also fixes a probe-socket bind bug where a cross-address-family local bind would fail before connect.

Changes:

  • Convert the device ID MAC to its unsigned decimal representation via a new macAddressToDecimal helper that strips optional colons and requires exactly 12 hex digits.
  • Bind the probe socket to the advertised local address only when its address family matches the target's.
  • Add unit tests for alphabetic hex, max 48-bit value, and colon-less device IDs, and update the expected header value.
File Description
shared/​src/​main/​java/​com/​shilapi/​xcertplay/​network/​CarPlayBonjour.kt Adds decimal MAC conversion for the probe header and gates the local-address bind on matching address family.
shared/​src/​test/​java/​com/​shilapi/​xcertplay/​network/​CarPlayBonjourTest.kt Updates the expected header value and adds cases covering alphabetic hex, the unsigned 48-bit max, and colon-less IDs.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@BBleae

BBleae commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

@shilapi 实测过做了这个改动之后才能让我的 iPhone 17 Pro 连上 xcertplay

@shilapi
shilapi merged commit bc30320 into shilapi:master Oct 15, 2026
2 checks passed
@BBleae
BBleae deleted the fix/carplay-ctrl-device-id-decimal branch October 15, 2026 08:05
@BBleae
BBleae restored the fix/carplay-ctrl-device-id-decimal branch October 17, 2026 21:44
@BBleae
BBleae deleted the fix/carplay-ctrl-device-id-decimal branch October 18, 2026 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants