chore(sanitize): remove real device ID and personal LAN IPs from tracked files

Per the repo's no-real-data rule (CLAUDE.md), scrub committed files only (the
gitignored _/ local captures are left as-is):

- Real Bose-OUI device ID 08DF1F0BA325 -> placeholder AABBCCDDEE0A across 4 docs
  and 8 Go test files (consistent 1:1 rename; affected packages tested green).
- Personal/topology LAN IPs -> RFC-5737: the lab runbook's AP subnet
  192.168.10.x -> 198.51.100.x (192.0.2.x is already used contrastively there)
  and 192.168.100.1 -> 203.0.113.1; illustrative example IPs in
  ANONYMIZATION-SUMMARY / spotify-overview / TROUBLESHOOTING -> 192.0.2.x.
- Kept factual RFC-1918 range citations (10.0.0.0/8 trusted-proxy example,
  192.168.0.0/16 "all private subnets") since they name the ranges themselves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-06-06 19:11:24 +02:00
co-authored by Claude Opus 4.8
parent 02be18c19c
commit 7e0573032c
16 changed files with 54 additions and 54 deletions
@@ -42,7 +42,7 @@ pattern) but stay numeric for parsers that expect integer-looking IDs.
An earlier anonymisation pass used `192.168.1.x` as its target. That
range is RFC-1918 private space — perfectly valid on real networks,
which means a reader can't tell whether `192.168.1.10` is a
which means a reader can't tell whether `192.0.2.10` is a
placeholder or a documented LAN address. RFC-5737 ranges fix that:
because they're reserved for documentation only, any reader knows on
sight that they don't represent a real device.
+15 -15
View File
@@ -84,7 +84,7 @@ sudo tee /etc/systemd/network/08-wlan0.network << 'EOF'
Name=wlan0
[Network]
Address=192.168.10.1/24
Address=198.51.100.1/24
IPForward=yes
ConfigureWithoutCarrier=yes
DHCP=no
@@ -104,7 +104,7 @@ sudo systemctl mask wpa_supplicant@wlan0
**Verify:**
```bash
ip addr show wlan0
# Expected: ONLY inet 192.168.10.1/24 (NO second DHCP IP)
# Expected: ONLY inet 198.51.100.1/24 (NO second DHCP IP)
```
---
@@ -151,9 +151,9 @@ sudo mv /etc/dnsmasq.conf /etc/dnsmasq.conf.bak
sudo tee /etc/dnsmasq.conf << 'EOF'
interface=wlan0
dhcp-range=192.168.10.100,192.168.10.200,24h
dhcp-option=3,192.168.10.1
dhcp-option=6,192.168.10.1
dhcp-range=198.51.100.100,198.51.100.200,24h
dhcp-option=3,198.51.100.1
dhcp-option=6,198.51.100.1
# DNS Upstream: custom server on localhost (adjust port if necessary)
server=127.0.0.1#5353 # Example: custom server on port 5353
@@ -237,7 +237,7 @@ If you cannot see the `Bose-Lab` SSID on your phone:
```bash
sudo nmcli device set wlan0 managed no
```
7. **Ghost IP Conflict:** If `ip addr show wlan0` shows both `192.168.10.1` and another IP (like `192.0.2.x`), `hostapd` will fail. This is usually caused by NetworkManager managing the interface. Ensure you've run:
7. **Ghost IP Conflict:** If `ip addr show wlan0` shows both `198.51.100.1` and another IP (like `192.0.2.x`), `hostapd` will fail. This is usually caused by NetworkManager managing the interface. Ensure you've run:
```bash
sudo nmcli device set wlan0 managed no
# If the ghost IP is still there, remove it manually:
@@ -259,13 +259,13 @@ If you haven't created a CA yet, follow **Appendix A** first.
# Temporarily make reachable via HTTP for easy download:
cd /etc/my-dns-ca/
python3 -m http.server 8080
# → Reachable at http://192.168.10.1:8080/ca.crt
# → Reachable at http://198.51.100.1:8080/ca.crt
```
### Install on Android
1. Connect phone to `Bose-Lab`
2. Open browser → `http://192.168.10.1:8080/ca.crt`
2. Open browser → `http://198.51.100.1:8080/ca.crt`
3. Download certificate
4. **Settings → Security → Credentials → Install CA Certificate**
5. Select certificate and confirm
@@ -322,7 +322,7 @@ sudo tcpdump -i wlan0 -n 'not port 53' -w /tmp/bose-nodns.pcap
# Traffic of a specific host only (filter by phone IP)
# Read phone IP from dnsmasq.leases beforehand (see below)
sudo tcpdump -i wlan0 -n host 192.168.10.101
sudo tcpdump -i wlan0 -n host 198.51.100.101
```
### Read SNI from TLS Traffic (without decryption)
@@ -351,7 +351,7 @@ Transfer `.pcap` files from the Pi to the PC:
```bash
# From the PC (scp)
scp pi@192.168.10.1:/tmp/bose-*.pcap ~/Desktop/
scp pi@198.51.100.1:/tmp/bose-*.pcap ~/Desktop/
```
**Important Wireshark Filters:**
@@ -607,7 +607,7 @@ You can either configure the macOS system proxy manually or use `mitmproxy`'s au
**Method 1: System Proxy (Manual)**
1. Go to **System Settings → Network → Wi-Fi → Details... → Proxies**.
2. Enable **HTTP Proxy** and **HTTPS Proxy**.
3. Set Server to your Pi's IP (`192.168.10.1`) and Port to `8080`.
3. Set Server to your Pi's IP (`198.51.100.1`) and Port to `8080`.
4. Click **OK** and **Apply**.
**Method 2: mitmproxy Local Redirect (Automatic)**
@@ -667,7 +667,7 @@ If the app uses **Certificate Pinning** (hardcoded hashes), even moving the CA t
If the **Transparent AP** setup (Steps 16) is too complex or you are experiencing routing issues, you can use `mitmproxy` as a **Regular HTTP Proxy**.
### 1. How it works
In this mode, the Pi acts as a simple server on port 8080. You tell your phone's Wi-Fi settings to send all traffic to `192.168.10.1:8080`.
In this mode, the Pi acts as a simple server on port 8080. You tell your phone's Wi-Fi settings to send all traffic to `198.51.100.1:8080`.
* **Pros:** No complex `nftables` or NAT rules required.
* **Cons:** Many Android apps (and background processes) ignore system-wide proxy settings. **HTTPS still requires a trusted CA for decryption.**
@@ -683,7 +683,7 @@ mitmproxy --listen-port 8080
1. Go to **Settings → Wi-Fi → Bose-Lab**.
2. Select **Modify Network** (or the "i" icon).
3. Set **Proxy** to **Manual**.
4. **Proxy hostname:** `192.168.10.1`
4. **Proxy hostname:** `198.51.100.1`
5. **Proxy port:** `8080`
6. Save and try to browse a site.
@@ -706,7 +706,7 @@ go get github.com/google/gopacket
go run scripts/extract-ws.go your_capture.pcap [filter_ip]
# Example: Filter for a specific speaker's IP in WebSocket messages
go run scripts/extract-ws.go capture.pcap 192.168.100.1
go run scripts/extract-ws.go capture.pcap 203.0.113.1
```
### 2. Manual Extraction with tshark
@@ -889,5 +889,5 @@ pgrep -a tcpdump
dig @127.0.0.1 -p 5353 global.api.bose.io
# Check network connectivity from the phone (from the Pi)
ping 192.168.10.101 # Phone IP from dnsmasq.leases
ping 198.51.100.101 # Phone IP from dnsmasq.leases
```
@@ -116,7 +116,7 @@ The system has distinct phases where device information is collected and enhance
**Endpoint**: `POST /streaming/account/{accountId}/devices`
**Request Format**:
```xml
<device deviceid="08DF1F0BA325">
<device deviceid="AABBCCDDEE0A">
<name>Living Room Speaker</name>
</device>
```
@@ -91,7 +91,7 @@ that delegates to AfterTouch for these names). The implementation lives in
> **IP-based `--server-url` is incompatible with OAuth (both Spotify and Amazon
> Music).** The speaker's hostname construction appends `oauth` to the first
> label only, so `192.168.0.30` would produce `192oauth.168.0.30` — malformed,
> label only, so `192.0.2.30` would produce `192oauth.0.2.30` — malformed,
> no DNS resolver will answer for it, and there is no clean workaround on the
> AfterTouch side. **Use a real LAN hostname** before configuring Spotify or
> Amazon Music. The Health-tab `oauth_target_reachable` check warns when this
@@ -390,7 +390,7 @@ Lists all discovered SoundTouch devices with their current status.
```json
[
{
"device_id": "08DF1F0BA325",
"device_id": "AABBCCDDEE0A",
"name": "Living Room Speaker",
"ip_address": "192.0.2.100",
"product_code": "SoundTouch 20",
+2 -2
View File
@@ -1121,10 +1121,10 @@ In `vi`, find `block_remote_traffic()`. Press `i` to enter insert mode. After th
first `done` line in that function, add:
```
echo -A INPUT -i $IFACE -s 192.168.10.0/24 -j ACCEPT
echo -A INPUT -i $IFACE -s 192.0.2.0/24 -j ACCEPT
```
Replace `192.168.10.0/24` with the subnet your AfterTouch host is on. Press `Esc`,
Replace `192.0.2.0/24` with the subnet your AfterTouch host is on. Press `Esc`,
type `:wq`, press `Enter`, then reboot:
```bash
@@ -71,7 +71,7 @@ All subsequent messages (except `selectLastWiFiSource`, see below) use this enve
```
`requestID` is a monotonically increasing integer per connection (client-side sequence).
`{device_id}` is the speaker's MAC address with colons removed (e.g. `08DF1F0BA325`).
`{device_id}` is the speaker's MAC address with colons removed (e.g. `AABBCCDDEE0A`).
---
@@ -84,7 +84,7 @@ Device system settings:
$ soundtouch-cli --host 192.0.2.100 analyze
🔍 Device Capability Analysis:
Device ID: 08DF1F0BA325
Device ID: AABBCCDDEE0A
Feature Coverage: 87% (13/15 features)
Device Type: Premium SoundTouch Speaker (Full Feature Set)
+4 -4
View File
@@ -13,7 +13,7 @@ import (
func TestClient_Post_ErrorsResponse(t *testing.T) {
// Mock speaker error response
errorXML := `<?xml version="1.0" encoding="UTF-8" ?>
<errors deviceID="08DF1F0BA325">
<errors deviceID="AABBCCDDEE0A">
<error value="1029" name="UNKNOWN_ACTION_ERROR" severity="Unknown">This version of SCM does not support spotify create account functionality.</error>
</errors>`
@@ -38,8 +38,8 @@ func TestClient_Post_ErrorsResponse(t *testing.T) {
t.Fatalf("expected models.ErrorsResponse, got %T: %v", err, err)
}
if errs.DeviceID != "08DF1F0BA325" {
t.Errorf("expected DeviceID 08DF1F0BA325, got %s", errs.DeviceID)
if errs.DeviceID != "AABBCCDDEE0A" {
t.Errorf("expected DeviceID AABBCCDDEE0A, got %s", errs.DeviceID)
}
if len(errs.Errors) != 1 {
@@ -67,7 +67,7 @@ func TestClient_Post_ErrorsResponse(t *testing.T) {
func TestClient_PostWithResponse_ErrorsResponse(t *testing.T) {
// Mock speaker error response
errorXML := `<?xml version="1.0" encoding="UTF-8" ?>
<errors deviceID="08DF1F0BA325">
<errors deviceID="AABBCCDDEE0A">
<error value="1029" name="UNKNOWN_ACTION_ERROR" severity="Unknown">This version of SCM does not support spotify create account functionality.</error>
</errors>`
+1 -1
View File
@@ -307,7 +307,7 @@ func ExampleClient_GetSupportedURLs_concept() {
}
// Expected output with a real device:
// Device 08DF1F0BA325 supports 103 endpoints
// Device AABBCCDDEE0A supports 103 endpoints
// Core functionality: true
// Multiroom support: true
// Streaming support: true
+2 -2
View File
@@ -22,7 +22,7 @@ func TestClient_GetSupportedURLs(t *testing.T) {
{
name: "successful_supported_urls_retrieval",
responseXML: `<?xml version="1.0" encoding="UTF-8"?>
<supportedURLs deviceID="08DF1F0BA325">
<supportedURLs deviceID="AABBCCDDEE0A">
<URL location="/info" />
<URL location="/capabilities" />
<URL location="/supportedURLs" />
@@ -50,7 +50,7 @@ func TestClient_GetSupportedURLs(t *testing.T) {
<URL location="/bassCapabilities" />
</supportedURLs>`,
expectedError: false,
expectedDeviceID: "08DF1F0BA325",
expectedDeviceID: "AABBCCDDEE0A",
expectedURLCount: 25,
expectedURLs: []string{
"/info", "/capabilities", "/supportedURLs", "/volume", "/bass",
+1 -1
View File
@@ -175,7 +175,7 @@ func TestListAllDevices(t *testing.T) {
ds := NewDataStore(tempDir)
account := "default"
deviceID := "BO5EBO5E-F00D-F00D-FEED-08DF1F0BA325"
deviceID := "BO5EBO5E-F00D-F00D-FEED-AABBCCDDEE0A"
info := &models.ServiceDeviceInfo{
DeviceID: deviceID,
+2 -2
View File
@@ -300,13 +300,13 @@ func TestMargeAccountFullExcludesEmptyAmazonSource(t *testing.T) {
account := "1000001"
// First device (alphabetically): has Amazon in Sources.xml
firstDeviceID := "08DF1F0BA325"
firstDeviceID := "AABBCCDDEE0A"
firstDir := filepath.Join(tempDir, "accounts", account, "devices", firstDeviceID)
if err := os.MkdirAll(firstDir, 0755); err != nil {
t.Fatalf("Failed to create first device dir: %v", err)
}
if err := os.WriteFile(filepath.Join(firstDir, "DeviceInfo.xml"), []byte(`
<info deviceID="08DF1F0BA325">
<info deviceID="AABBCCDDEE0A">
<name>Kitchen SoundTouch</name>
<type>SoundTouch 20 scm</type>
</info>
+6 -6
View File
@@ -89,7 +89,7 @@ func TestAccountFullToXML_Structure(t *testing.T) {
ds := datastore.NewDataStore(tempDir)
account := "1234567"
device := "08DF1F0BA325"
device := "AABBCCDDEE0A"
// 1. Setup Device Info with Components
info := &models.ServiceDeviceInfo{
@@ -183,14 +183,14 @@ func TestAccountFullToXML_Structure(t *testing.T) {
}
// Device structure
if !strings.Contains(xmlStr, `<device deviceid="08DF1F0BA325">`) {
if !strings.Contains(xmlStr, `<device deviceid="AABBCCDDEE0A">`) {
t.Errorf("Expected device attribute deviceid, got %s", xmlStr)
}
if !strings.Contains(xmlStr, `<name>Kitchen SoundTouch</name>`) {
t.Errorf("Expected <name>Kitchen SoundTouch</name> under device, got %s", xmlStr)
}
if !strings.Contains(xmlStr, `<serialNumber>08DF1F0BA325</serialNumber>`) {
t.Errorf("Expected <serialNumber>08DF1F0BA325</serialNumber> under device, got %s", xmlStr)
if !strings.Contains(xmlStr, `<serialNumber>AABBCCDDEE0A</serialNumber>`) {
t.Errorf("Expected <serialNumber>AABBCCDDEE0A</serialNumber> under device, got %s", xmlStr)
}
if !strings.Contains(xmlStr, `<updatedOn>`) {
t.Errorf("Expected <updatedOn> under device, got %s", xmlStr)
@@ -217,8 +217,8 @@ func TestAccountFullToXML_Structure(t *testing.T) {
if !strings.Contains(xmlStr, `<firmware-version>1.2.3</firmware-version>`) {
t.Errorf("Expected firmware-version 1.2.3, got %s", xmlStr)
}
if !strings.Contains(xmlStr, `<serialnumber>08DF1F0BA325</serialnumber>`) {
t.Errorf("Expected <serialnumber>08DF1F0BA325</serialnumber> under attachedProduct, got %s", xmlStr)
if !strings.Contains(xmlStr, `<serialnumber>AABBCCDDEE0A</serialnumber>`) {
t.Errorf("Expected <serialnumber>AABBCCDDEE0A</serialnumber> under attachedProduct, got %s", xmlStr)
}
if !strings.Contains(xmlStr, `<updatedOn>`) {
t.Errorf("Expected <updatedOn> under attachedProduct, got %s", xmlStr)
+10 -10
View File
@@ -93,16 +93,16 @@ func TestReproduceMissingName(t *testing.T) {
accountID := "1234567"
// Create device folders
// 08DF1F0BA325 (has name)
// AABBCCDDEE0A (has name)
// 001122334455 (missing name in full_local.xml)
// Device 1: 08DF1F0BA325
dev1Dir := filepath.Join(tempBaseDir, "accounts", accountID, "devices", "08DF1F0BA325")
// Device 1: AABBCCDDEE0A
dev1Dir := filepath.Join(tempBaseDir, "accounts", accountID, "devices", "AABBCCDDEE0A")
err = os.MkdirAll(dev1Dir, 0755)
if err != nil {
t.Fatal(err)
}
dev1Info := `<info deviceID="08DF1F0BA325">
dev1Info := `<info deviceID="AABBCCDDEE0A">
<name>Kitchen SoundTouch</name>
<type>SoundTouch</type>
<moduleType>20</moduleType>
@@ -229,10 +229,10 @@ func TestReproduceMissingName(t *testing.T) {
for _, d := range resp.Devices {
t.Logf("Checking device in response: ID=%s, Name='%s'\n", d.DeviceID, d.Name)
if d.DeviceID == "08DF1F0BA325" {
if d.DeviceID == "AABBCCDDEE0A" {
found08 = true
if d.Name == "" {
t.Error("Device 08DF1F0BA325 name should not be empty")
t.Error("Device AABBCCDDEE0A name should not be empty")
}
}
if d.DeviceID == "001122334455" || d.DeviceID == "I6332527703739342000020" {
@@ -243,7 +243,7 @@ func TestReproduceMissingName(t *testing.T) {
}
if !found08 {
t.Error("Device 08DF1F0BA325 not found in response")
t.Error("Device AABBCCDDEE0A not found in response")
}
if !foundA8 {
t.Error("Device 001122334455 not found in response")
@@ -334,7 +334,7 @@ func TestSyncSourcesAttributes(t *testing.T) {
xmlData := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<account id="1234567">
<devices>
<device deviceid="08DF1F0BA325">
<device deviceid="AABBCCDDEE0A">
<presets>
<preset buttonNumber="1">
<name>test-playlist</name>
@@ -405,7 +405,7 @@ func TestSyncSourcesAttributes(t *testing.T) {
}
// Check datastore
presets, err := ds.GetPresets("1234567", "08DF1F0BA325")
presets, err := ds.GetPresets("1234567", "AABBCCDDEE0A")
if err != nil {
t.Fatal(err)
}
@@ -419,7 +419,7 @@ func TestSyncSourcesAttributes(t *testing.T) {
t.Errorf("Synced preset source ID mismatch: expected 10863533, got '%s'", lp.SourceID)
}
recents, err := ds.GetRecents("1234567", "08DF1F0BA325")
recents, err := ds.GetRecents("1234567", "AABBCCDDEE0A")
if err != nil {
t.Fatal(err)
}
+5 -5
View File
@@ -280,14 +280,14 @@ func TestGetLiveDeviceInfo(t *testing.T) {
w.Header().Set("Content-Type", "application/xml")
_, _ = fmt.Fprint(w, `<?xml version="1.0" encoding="UTF-8"?>
<info deviceID="08DF1F0BA325">
<info deviceID="AABBCCDDEE0A">
<name>Test Speaker</name>
<type>SoundTouch 20</type>
<components>
<component>
<componentCategory>SCM</componentCategory>
<softwareVersion>19.0.5</softwareVersion>
<serialNumber>08DF1F0BA325</serialNumber>
<serialNumber>AABBCCDDEE0A</serialNumber>
</component>
</components>
</info>`)
@@ -313,8 +313,8 @@ func TestGetLiveDeviceInfo(t *testing.T) {
t.Errorf("Expected SoftwareVer '19.0.5', got '%s'", info.SoftwareVer)
}
if info.SerialNumber != "08DF1F0BA325" {
t.Errorf("Expected SerialNumber '08DF1F0BA325', got '%s'", info.SerialNumber)
if info.SerialNumber != "AABBCCDDEE0A" {
t.Errorf("Expected SerialNumber 'AABBCCDDEE0A', got '%s'", info.SerialNumber)
}
}
@@ -1441,7 +1441,7 @@ func TestBackupConfigOffDevice(t *testing.T) {
m := NewManager("http://localhost:8000", ds, nil)
serial := "08DF1F0BA325"
serial := "AABBCCDDEE0A"
accountID := "1000001"
// Mock info server