Repository navigation
fix(bonjour): send the CarPlay control receiver device ID in decimal - #52
Conversation
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.
There was a problem hiding this comment.
🟢 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
macAddressToDecimalhelper 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.
|
@shilapi 实测过做了这个改动之后才能让我的 iPhone 17 Pro 连上 xcertplay |
Problem
The
GET /ctrl-int/1/connectprobe sendsAirPlay-Receiver-Device-IDas colon-less hex. Apple's CarPlay control sender instead serializes the same 48-bit device ID as an unsigned decimal integer.02:00:00:00:00:020200000000022199023255554Primary 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:com.apple.CarPlaySDKSoftware Signing -> Apple Code Signing Certification Authority -> Apple Root CAcodesign --verify --deep --strictpasses forCarPlaySimulator.devicekitpluginand its embeddedCarPlaySDK.framework.574255f38be2b4841dabdb42ca03311a8ef6dc4b9e3834ba5e375195d8b8d7fd1366efa42bbb3b7ba85a51fe3073bf4b37d2ba81The signature and disassembly can be reproduced with standard macOS tools:
The relevant ARM64e data flow is unambiguous:
_AirPlayGetDeviceIDat0x4974reads the six hardware-address bytes and returnsb0 << 40 | b1 << 32 | b2 << 24 | b3 << 16 | b4 << 8 | b5.__CarPlayControlClientEnsureStartedcalls it at0x2806acand stores the resulting unsigned 48-bit integer in the control client at+0x58.__CarPlayControlClientConnectconstructsGET /ctrl-int/1/connect, loads that integer at0x1a558, then passes the literalAirPlay-Receiver-Device-ID, the format string%llu, and the integer to_HTTPHeader_SetFieldat0x1a578.This produces unsigned decimal ASCII with neither colons nor a
0xprefix.The same signed SDK itself uses field-specific serialization.
_UpdateBonjourAirPlaypasses the same six bytes through_HardwareAddressToCStringbefore setting the_airplay._tcpTXT keydeviceid; that helper emits uppercase hex and inserts:between bytes. The TXT record remaining02:00:00:00:00:02while this private HTTP header becomes2199023255554therefore matches the two code paths in Apple's implementation.For comparison, a public source mirror of CarPlayControlClient contains the same operation (call site):
Additional implementation examples also agree:
str(int(device_id.replace(":", ""), 16))AirPlay-Receiver-Device-ID: 963347909223Physical-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.
HTTP/1.1 200 OK, reverse RTSP, pair-verify, type-110 stream, first frame renderedHTTP/1.1 200 OK, but no reverse RTSP appeared during the 90-second harness observation windowConnection: closeA 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 OKbut 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
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
connectProbeRequestconverts the 48-bit MAC to unsigned decimal. Colons are optional; the ID must have exactly 12 hex digits. Every currentdeviceIdsource (the fixed02:00:00:00:00:02, the P2P/LocalOnly/manual BSSID formatters, andMacAddress.toString()) already produces this shape.EAFNOSUPPORT, while IPv6-local/IPv4-remote failed withENETUNREACH, both beforeconnectcould 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: passedCarPlayBonjourTestcovers:de:ad:be:ef:ca:fe->244837814094590ff:ff:ff:ff:ff:ff->281474976710655(maximum unsigned 48-bit value)deadbeefcafe->244837814094590int(hex, 16).:shared:lintDebugreports the same 63 pre-existing errors before and after this change; this PR introduces no new lint issue.