Skip to content
Capture Device Pairing Traffic

Capture Device Pairing Traffic

Capture Device Pairing Traffic

Step-by-step runbook for factory-resetting a SoundTouch speaker, pairing it to a Bose cloud account, and capturing every cloud request via mitmproxy. Tested on Apple Silicon Mac.

Goal: obtain a full .mitm recording of the account-pairing flow (streaming.bose.com) triggered by the official Android app.


Overview

Phase 0  Pre-flight checks
Phase 1  Factory reset speaker
Phase 2  Provision speaker Wi-Fi (AP mode, console)
Phase 3  Start mitmproxy + emulator + Frida
Phase 4  Pair speaker via Bose app (adb-driven)
Phase 5  Save & inspect recording

The Android emulator does not have Bluetooth, so the standard BLE setup path is unavailable. Instead:

  1. Provision Wi-Fi directly over the speaker’s AP web server (Phase 2).
  2. Once the speaker is on the LAN, the Bose app discovers it via mDNS — no BLE needed.

Phase 0 — Pre-flight

The emulator setup is fully scripted. Run once per machine:

# Place the Bose APK at scripts/android/bose.apk first (see BOSE-APP-ADB-Emulator.md § 1)
# Run mitmweb once to generate the CA: mitmweb --listen-port 8080 (Ctrl-C after it starts)

scripts/android/setup-mitm-avd.sh

This installs the system image, creates an AVD named bose-mitm, installs the mitmproxy cert and Bose APK, and saves an emulator snapshot mitm-ready — so subsequent sessions never repeat the cert/reboot cycle.

For subsequent sessions, Phase 3 below is replaced by a single command:

scripts/android/start-mitm-session.sh

Manual steps are only needed if you want to understand the internals; see BOSE-APP-ADB-Emulator.md for the full manual walkthrough.


Phase 1 — Factory Reset Speaker

Perform the reset for your model (see DEVICE-INITIAL-SETUP.md § 5 for full table):

ModelSequence
SoundTouch 10Power on; hold Preset 1 + Vol − ~10 s → solid amber Wi-Fi LED
SoundTouch 20Power on; hold Preset 1 + Vol − ~10 s → lights blink L→R, amber
SoundTouch 20/30 Series IIIHold Preset 1 + Preset 6 ~10 s
SoundTouch 300Hold Vol − ~15 s until light bar blinks

Wait until the white LED sweep / restart animation completes (~30 s). The speaker is now in setup mode and broadcasting its own Wi-Fi AP.


Phase 2 — Provision Speaker Wi-Fi (AP Mode)

2.1 Connect Mac to Speaker AP

# Find the SSID — use System Settings → Wi-Fi or:
#   sudo wdutil info   (macOS Sequoia+, airport command removed)

# Connect (replace SSID with actual value)
SPEAKER_SSID="Bose SoundTouch XXXX"
networksetup -setairportnetwork en0 "$SPEAKER_SSID"

# Confirm: speaker web UI reachable at 192.0.2.1 (client gets 192.0.2.2)
curl -s --connect-timeout 5 http://192.0.2.1/ | head -3

2.2 Push Home Wi-Fi Credentials

The setup UI at http://192.0.2.1/ uses the SoundTouch API on port 8090. Push credentials directly:

HOME_SSID="MyHomeNetwork"
HOME_PASS="MyPassword"

# Optional: trigger a site survey first so the speaker finds your SSID
curl -s -X POST http://192.0.2.1:8090/performWirelessSiteSurvey \
  -H 'Content-Type: text/xml' \
  --data-raw '<PerformWirelessSiteSurvey timeout="5"/>'

curl -s -X POST http://192.0.2.1:8090/addWirelessProfile \
  -H 'Content-Type: text/xml' \
  --data-raw "<AddWirelessProfile><profile ssid=\"${HOME_SSID}\" password=\"${HOME_PASS}\" securityType=\"wpa_or_wpa2\" /></AddWirelessProfile>"

Expected response: <AddWirelessProfileResponse />

2.3 Reconnect Mac to Home Network

HOME_SSID="MyHomeNetwork"
HOME_PASS="MyPassword"

networksetup -setairportnetwork en0 "$HOME_SSID" "$HOME_PASS"

2.4 Wait for Speaker to Join LAN

# Poll mDNS until the speaker appears (~15-30 s)
echo "Waiting for speaker on LAN..."
until dns-sd -B _soundtouch._tcp local 2>&1 | grep -m1 "Add"; do sleep 2; done
echo "Speaker is online"

# Resolve its IP
dns-sd -L "$(dns-sd -B _soundtouch._tcp local 2>&1 | grep Add | awk '{print $7}')" \
  _soundtouch._tcp local 2>&1 | grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}'

Or just scan for open port 8090:

# Quick scan of your /24 subnet for port 8090
SUBNET="192.168.1"   # adjust to your local subnet
for i in $(seq 1 254); do
  (ping -c1 -W1 ${SUBNET}.$i &>/dev/null && \
   nc -z -w1 ${SUBNET}.$i 8090 2>/dev/null && \
   echo "${SUBNET}.$i") &
done
wait

Phase 3 — Start mitmproxy, Emulator, Frida

Run each block in a separate terminal tab.

3.1 Start mitmweb (native macOS app)

Note: Docker mitmproxy does not work here — its NAT layer prevents the emulator from reaching it. Use the native macOS app instead (download: https://downloads.mitmproxy.org/12.2.2/mitmproxy-12.2.2-macos-arm64.tar.gz).

CAPTURE="bose-pairing-$(date +%Y%m%d-%H%M%S).mitm"
/Applications/mitmproxy.app/Contents/MacOS/mitmweb \
  --web-host 0.0.0.0 --listen-port 8080 --mode regular \
  --set web_password=bose \
  -w "scripts/android/captures/${CAPTURE}"
# Captures → scripts/android/captures/
# Web UI   → http://127.0.0.1:8081/?token=bose

3.2 Start Emulator

~/Library/Android/sdk/emulator/emulator -avd Pixel_6_API33 -writable-system &
echo "Waiting for emulator boot..."
adb wait-for-device
adb -s emulator-5554 wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done'
echo "Emulator ready"

Enable root and install certificate (only needed once per emulator session):

adb -s emulator-5554 root
adb -s emulator-5554 shell avbctl disable-verification
adb -s emulator-5554 reboot
adb -s emulator-5554 wait-for-device
adb -s emulator-5554 root

HASH=$(openssl x509 -inform PEM -subject_hash_old \
  -in ~/.mitmproxy/mitmproxy-ca-cert.pem | head -1)

adb -s emulator-5554 push ~/.mitmproxy/mitmproxy-ca-cert.pem /data/local/tmp/mitmproxy.pem
adb -s emulator-5554 shell su 0 mkdir -p /data/misc/user/0/cacerts-added
adb -s emulator-5554 shell su 0 \
  cp /data/local/tmp/mitmproxy.pem /data/misc/user/0/cacerts-added/${HASH}.0
adb -s emulator-5554 shell su 0 \
  chmod 644 /data/misc/user/0/cacerts-added/${HASH}.0

Set proxy to Mac IP:

MAC_IP=$(ipconfig getifaddr en0)
adb -s emulator-5554 shell settings put global http_proxy "${MAC_IP}:8080"
echo "Proxy set to ${MAC_IP}:8080"

Confirm emulator can reach the speaker:

SPEAKER_IP=192.0.2.50   # adjust to your speaker's LAN IP
adb -s emulator-5554 shell ping -c 3 "$SPEAKER_IP"

3.3 Start frida-server

adb -s emulator-5554 push scripts/android/frida-server /data/local/tmp/frida-server
adb -s emulator-5554 shell su 0 chmod 755 /data/local/tmp/frida-server
adb -s emulator-5554 shell su 0 "nohup /data/local/tmp/frida-server > /dev/null 2>&1 &"
sleep 2
echo "frida-server running"

3.4 Configure config.js

start-mitm-session.sh patches scripts/android/frida/config.js automatically with the current Mac IP and mitmproxy cert. No manual step needed.

3.5 Launch App with SSL Unpinning

scripts/android/frida-venv/bin/frida \
  -U \
  -f com.bose.soundtouch \
  -l scripts/android/frida/config.js \
  -l scripts/android/frida/native-connect-hook.js \
  -l scripts/android/frida/android/android-system-certificate-injection.js \
  -l scripts/android/frida/android/android-proxy-override.js \
  -l scripts/android/frida/android/android-certificate-unpinning.js \
  -l scripts/android/frida/android/android-certificate-unpinning-fallback.js

native-connect-hook.js is required — the Bose app uses native networking that bypasses Java proxy settings.

Expected Frida output:

== System certificate trust injected ==
== Proxy system configuration overridden to <IP>:8080 ==
== Proxy configuration overridden to <IP>:8080 ==
== Certificate unpinning completed ==
== Unpinning fallback auto-patcher installed ==

Phase 4 — Pair Speaker via App

The Bose app should now be running in the emulator with all traffic going through mitmproxy.

4.1 Inspect UI to Find Interactive Elements

# Dump current screen
adb -s emulator-5554 shell uiautomator dump /sdcard/ui.xml
adb -s emulator-5554 pull /sdcard/ui.xml /tmp/ui.xml

# Helper: list all clickable elements with their text + bounds
grep -o 'text="[^"]*" resource-id="[^"]*" \.\.\. clickable="true"[^/]*' /tmp/ui.xml \
  || python3 -c "
import xml.etree.ElementTree as ET
tree = ET.parse('/tmp/ui.xml')
for n in tree.iter('node'):
    if n.get('clickable') == 'true' and n.get('text'):
        print(n.get('bounds'), n.get('resource-id'), repr(n.get('text')))
"

4.2 Navigate Setup Flow (adb)

# Take a screenshot at any point to see current state
adb -s emulator-5554 shell screencap /sdcard/screen.png
adb -s emulator-5554 pull /sdcard/screen.png /tmp/screen.png
open /tmp/screen.png

# Tap by resource-id (find IDs from ui.xml dump)
adb -s emulator-5554 shell uiautomator runtest ... # or input tap

# Tap by screen coordinates
adb -s emulator-5554 shell input tap X Y

# Type into the focused field
adb -s emulator-5554 shell input text "your@email.com"

# Press Enter / Next
adb -s emulator-5554 shell input keyevent 66

4.3 Expected Setup Steps in the App

Follow the on-screen flow; mitmproxy captures everything automatically.

  1. Sign in — enter email + password → triggers POST /streaming/account/login
  2. Add speaker — tap “Set Up a New Speaker” or equivalent
  3. App discovers speaker via mDNS on LAN (no BLE required)
  4. Wi-Fi already configured — app skips the Wi-Fi step since speaker is online
  5. Name speaker — type a name → WebSocket name message to speaker port 8080
  6. Pairing — app sends setMargeAccount WebSocket to speaker → speaker POSTs to streaming.bose.com/{accountId}/devices

All cloud requests (steps 1, 6) will appear in mitmweb at http://127.0.0.1:8081.


Phase 5 — Save & Inspect Recording

# Stop mitmweb (Ctrl-C in its terminal) — file is already written continuously

# Inspect offline in the web UI
mitmweb -r "$CAPTURE"

# Filter to streaming.bose.com only
mitmdump -r "$CAPTURE" --flow-filter '~u streaming.bose.com' -w bose-cloud-only.mitm

# Quick text summary
mitmdump -r "$CAPTURE" --flow-filter '~u streaming.bose.com' 2>/dev/null \
  | grep -E "POST|GET" | head -30

Convert to .http files (IntelliJ-compatible)

Use scripts/convert_mitm_script.py to extract each flow as a .http file, organized by path:

NAME=$(basename "$CAPTURE" .mitm)
OUT="scripts/android/mitm/${NAME}"

/Applications/mitmproxy.app/Contents/MacOS/mitmdump \
  -n -r "$CAPTURE" \
  -s scripts/convert_mitm_script.py \
  --set out_dir="${OUT}"

Output lands in scripts/android/mitm/<name>/mirror/ as numbered .http files plus *-websocket/ subdirectories for WebSocket frames. The directory is gitignored.


Cleanup

# Remove proxy from emulator
adb -s emulator-5554 shell settings delete global http_proxy

# Kill emulator
adb -s emulator-5554 emu kill

Frida artefacts live in scripts/android/ (gitignored) and persist between sessions — no cleanup needed unless you want to force a fresh setup.


Troubleshooting

SymptomLikely causeFix
curl http://192.0.2.1 times outMac not on speaker APRe-run networksetup -setairportnetwork; speaker AP gateway is 192.0.2.1
addWirelessProfile returns errorSpeaker not in AP mode or wrong IPConfirm speaker AP is active; use http://192.0.2.1:8090/addWirelessProfile
Speaker not found via mDNSSpeaker still on AP (not home LAN yet)Wait ~30 s, retry; check router DHCP leases
Emulator can’t ping speakerDifferent subnet or emulator proxy misconfiguredadb shell ping the Mac IP first; check proxy setting
App shows “No speakers found”App not detecting mDNSEnsure emulator is on same /24 as speaker; disable emulator Wi-Fi and re-enable
No traffic in mitmwebFrida not running or cert mismatchCheck Frida output for == Certificate unpinning ==; verify issuer in config.js

See Also


Session Trace (2026-05-02, ST10)

Raw log of the first interactive run. To be cleaned up into the runbook above.

Setup

  • Ran scripts/android/setup-mitm-avd.sh — all 9 steps completed:
    • Steps 1–5 were already done from a prior session (idempotent skips)
    • Step 6: Docker image built, frida-server extracted to scripts/android/frida-server
    • Steps 7–9: Emulator started fresh (-no-snapshot-load), AVB disabled, rebooted, cert + APK + frida-server installed, snapshot mitm-ready saved
  • Emulator running at emulator-5554

Factory Reset (ST10)

  • Correct sequence confirmed from official Bose guide (firmware/FirmwareUpdateGuide/): Power on → hold Preset 1 + Volume − for 10 s → Wi-Fi LED glows solid amber
  • Note: original docs said “Vol− + Mute” — corrected in DEVICE-INITIAL-SETUP.md and this file

Pre-reset note

  • User inserted USB device containing remote_services before rebooting — device needs to be on home Wi-Fi before the USB config takes effect

Wi-Fi Provisioning (AP mode)

  • airport command not available on this macOS version (removed in recent releases)
  • Connect Mac to speaker AP via System Settings → Wi-Fi (SSID: “Bose SoundTouch XXXX”)
  • Speaker AP gateway confirmed: 192.0.2.1 (client gets 192.0.2.2), not 192.0.2.1 as previously assumed
  • /gabbo_wifi endpoint was hallucinated — actual endpoint verified from browser network capture (_/device-reset/wifi-setup.txt):
    • Site survey: POST http://192.0.2.1:8090/performWirelessSiteSurvey with <PerformWirelessSiteSurvey timeout="5"/>
    • Add profile: POST http://192.0.2.1:8090/addWirelessProfile with XML body, securityType="wpa_or_wpa2"
    • Both use the standard SoundTouch API port 8090, same as normal device operation
    • Response: <AddWirelessProfileResponse />
  • Reconnect Mac to home Wi-Fi; emulator stays running throughout (routes through Mac interface)

Wi-Fi Provisioning Result

  • POST http://192.0.2.1:8090/addWirelessProfile succeeded → <AddWirelessProfileResponse />
  • Speaker joined home LAN at 192.168.x.y
  • SSH access confirmed: ssh -oHostKeyAlgorithms=ssh-rsa root@192.168.x.y
  • Device name: Bose SoundTouch XXXXXX
  • Network interfaces on device:
    • wlan0192.168.x.y (home LAN)
    • wlan1192.0.2.1 (AP mode interface, stays up after provisioning)
    • usb0203.0.113.1 (USB gadget — remote_services USB device inserted before reboot)

MITM Session Start

  • start-mitm-session.sh run: snapshot restored, proxy set, frida-server started, config.js patched
  • Issues encountered and fixed:
    • frida-server start command hung (adb shell su 0 ... &) → fixed with nohup ... > /dev/null 2>&1 &
    • Frida SSL scripts download via curl from GitHub timed out → moved to Dockerfile (extracted alongside frida-server)
    • Python heredoc cert injection syntax error (multiline cert in string) → fixed using env vars + single-quoted 'PYEOF' heredoc
    • mitmweb --port deprecated → --listen-port
    • frida script paths missing android/ subdirectory → fixed in session script
  • Docker mitmproxy (-p 8080:8080) received no traffic from emulator — Docker NAT layer prevented the emulator reaching it
  • Fix: use native macOS mitmproxy app (/Users/gesellix/Downloads/mitmproxy.app) — binds directly to Mac’s real network interfaces, traffic flows immediately
  • Android system traffic visible (connectivity checks to gstatic.com, www.google.com) — TLS failures for system processes expected since only Bose app has Frida cert injection

Capture Working

  • Full flow confirmed working with:
    • Native mitmproxy app (~/Downloads/mitmproxy.app, v12.2.2)
    • native-connect-hook.js added to Frida launch (required for Bose app’s native networking)
    • All 5 Frida scripts loaded: config.js, native-connect-hook.js, android-system-certificate-injection.js, android-proxy-override.js, android-certificate-unpinning.js, android-certificate-unpinning-fallback.js
  • Bose app traffic visible in mitmweb — pairing flow captured successfully
Last updated on