Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
cargo audit --file src-tauri/Cargo.lock
cargo audit --file src-tauri/startup-launcher/Cargo.lock
cargo audit --file tools/updater-signature-verifier/Cargo.lock
cargo audit --file tools/linux-ble-probe/Cargo.lock

frontend:
runs-on: ubuntu-latest
Expand All @@ -40,7 +41,7 @@ jobs:
- name: Reject retired product identity
run: |
retired_identity="pre""view"
if git grep -n -i "$retired_identity" -- ':!src-tauri/Cargo.lock'; then
if git grep -n -i "$retired_identity" -- ':!src-tauri/Cargo.lock' ':!tools/linux-ble-probe/Cargo.lock'; then
exit 1
fi
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
Expand Down Expand Up @@ -99,6 +100,8 @@ jobs:
bundles: nsis
- os: macos-14
bundles: app,dmg
- os: ubuntu-24.04
bundles: none
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
Expand All @@ -111,10 +114,22 @@ jobs:
with:
toolchain: 1.97.1
components: rustfmt, clippy
- name: Install Linux development libraries
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev libxkbcommon-dev libdbus-1-dev
- run: npm ci
- run: cargo fmt --manifest-path src-tauri/Cargo.toml --check
- run: cargo clippy --locked --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings
- run: cargo test --locked --manifest-path src-tauri/Cargo.toml
- name: Validate Linux Bluetooth probe without opening Bluetooth
if: runner.os == 'Linux'
run: |
cargo fmt --manifest-path tools/linux-ble-probe/Cargo.toml --check
cargo clippy --locked --manifest-path tools/linux-ble-probe/Cargo.toml --all-targets -- -D warnings
cargo test --locked --manifest-path tools/linux-ble-probe/Cargo.toml
cargo run --locked --manifest-path tools/linux-ble-probe/Cargo.toml -- --help
- run: cargo fmt --manifest-path tools/updater-signature-verifier/Cargo.toml --check
- run: cargo clippy --locked --manifest-path tools/updater-signature-verifier/Cargo.toml --all-targets -- -D warnings
- run: cargo test --locked --manifest-path tools/updater-signature-verifier/Cargo.toml
Expand All @@ -126,6 +141,9 @@ jobs:
run: cargo test --locked --manifest-path src-tauri/startup-launcher/Cargo.toml
- if: runner.os == 'macOS'
run: npm run tauri build -- --bundles ${{ matrix.bundles }} --no-sign
- name: Build Linux development application
if: runner.os == 'Linux'
run: npm run tauri build -- --debug --no-bundle
- if: runner.os == 'Windows'
run: pwsh ./scripts/Build-WindowsUiAccess.ps1 -SkipSign
- if: runner.os == 'Windows'
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: CodeQL

on:
pull_request:
branches:
- main
push:
branches:
- main
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ src-tauri/target/
src-tauri/binaries/
src-tauri/startup-launcher/target/
tools/updater-signature-verifier/target/
tools/linux-ble-probe/target/
src-tauri/gen/
.certs/
.env
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,34 @@ npm run tauri dev

## Checks

### Linux development foundation

Linux currently builds a development UI only. Bluetooth pairing, keyboard/mouse input,
and native feedback overlays are not implemented. X11 and Wayland control are both
unavailable at this stage; this is not a supported Linux release. See the
[Linux implementation plan](docs/linux-support-plan.md) for the remaining milestones.

On Ubuntu 24.04 x86_64, install Node.js 24, Rust 1.97.1 and the native dependencies:

```bash
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev libxkbcommon-dev libdbus-1-dev
npm ci
npm run tauri dev
```

Use `npm run tauri build -- --debug --no-bundle` to build the development executable.
Linux reports Bluetooth and input unavailable and does not open feedback windows.
It always opens the main window, including at startup, and closing that window quits
after any profile-edit confirmation. Tray/background behavior and Linux installers
are deferred until qualified. CI builds and tests on Ubuntu without injecting input;
physical Bluetooth and graphical session testing are still required before support
can be enabled. The next implementation milestone is BlueZ peripheral interoperability.
An opt-in [BlueZ transport probe](docs/linux-bluez-probe.md) is available for that
hardware investigation; it does not enable pairing or input in the desktop app.

### Repository validation

```bash
npm run lint
npm test
Expand All @@ -56,7 +84,7 @@ cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings
cargo test --manifest-path src-tauri/Cargo.toml
```

Rust tests use fake input adapters and never control the local pointer or keyboard. Native checks and unsigned bundles run on Windows and macOS in `.github/workflows/ci.yml`.
Rust tests use fake input adapters and never control the local pointer or keyboard. Native checks and unsigned bundles run on Windows and macOS in `.github/workflows/ci.yml`; Ubuntu also runs native checks and an unbundled development build.

## Production releases and signed updates

Expand Down
62 changes: 62 additions & 0 deletions docs/linux-bluez-probe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Linux BlueZ transport probe

Issue: #716. Stack: #711 → #714 → #715 → this increment.

This is the hardware-test harness for milestone 2 of the [Linux plan](linux-support-plan.md), **not completion of that milestone**. The desktop runtime remains unavailable on Linux. This standalone tool is not packaged, launched by the app, or added to releases. It cannot approve pairing, read stored credentials or inject input. It discards incoming messages and never echoes their contents or identifiers.

## Binding and isolation decision

Use BlueR (`bluer` pinned to 0.17.4, `bluetoothd` feature) for this experiment. It is the BlueZ project's Rust interface and exposes local GATT registration, advertising, request device identity and file-descriptor notifications. A central-only library is unsuitable. Direct D-Bus would still face the same BlueZ delivery constraints while adding object registration and descriptor-lifecycle code to maintain. No new dependency is added to the shipping application; reconsider the binding after the physical tests.

RX uses callback writes (request and command); status uses callback reads. TX uses `AcquireNotify` through BlueR's IO mode so the probe can observe the device address and payload limit associated with a notification writer. It does not use a `StartNotify` broadcast callback for sensitive replies. Notifications contain only a fixed public `linux_transport_probe_only` error envelope, framed by the exact same source as the desktop protocol. The desktop's framing implementation was moved unchanged into `src-tauri/src/ble_wire.rs` and re-exported from `protocol.rs`; existing protocol tests still exercise it.

**Device identity on a writer is not proof of exclusive over-the-air delivery.** BlueZ acknowledges IO subscriptions before the application receives the event. This harness accepts one writer and one RX peer at a time, drops competing writers and never combines fragments from different peers. That is a diagnostic policy, not a qualified security boundary. A refused second subscriber may affect the first session at the daemon level. Do not connect this transport to pairing approval or tokens until the competing-central test proves isolation on supported BlueZ versions. If exclusivity cannot be established, design a separately reviewed enforcement mechanism; do not rely on stopping advertisements or polling connected devices as proof.

The probe stores at most the shared protocol's bounded partial-message set for one peer, plus one active writer. Incoming GATT values are capped at 512 bytes; nonzero write offsets and prepared/reliable writes are rejected. Notifications are generated at a fixed two-second cadence, with a two-second send timeout and no unbounded outbound queue. Only counters, payload limits and fixed diagnostic messages are logged; no payloads, tokens, signatures, device addresses or raw D-Bus errors are printed.

## Build and run

On Linux, install Rust 1.97.1, a C build toolchain, pkg-config, `libdbus-1-dev`, BlueZ and an advertising-capable Bluetooth adapter. The probe does not need GTK/WebKit. Use your ordinary desktop user and the system BlueZ service; do not run it as root or install permissive D-Bus policies to bypass a failure.

```bash
cargo test --locked --manifest-path tools/linux-ble-probe/Cargo.toml
cargo run --locked --manifest-path tools/linux-ble-probe/Cargo.toml -- --help
# Explicitly enables connectable advertising on this already-powered adapter:
cargo run --locked --manifest-path tools/linux-ble-probe/Cargo.toml -- --serve hci0
```

For compilation/testing on hosts without D-Bus headers, add `--features vendored-dbus` before `--`. CI uses the system library. The default invocation and `--help` do not connect to D-Bus or open Bluetooth. Automated tests use in-memory requests only.

Close the desktop companion first to avoid duplicate service UUIDs during the experiment. The probe advertises `Switchify probe`, the existing Switchify service UUID, and the existing RX (write/write-without-response), TX (notify), and status (read) UUIDs. Its status has a fresh `probe-` desktop ID on each run, protocol version 1 and platform `linux`; it does not impersonate a saved pairing identity. It does not change radio power, remove Bluetooth pairings or touch another process's registrations. Ctrl-C or the five-minute deadline drops its registration handles. Crashes also lose the owning D-Bus connection. No automatic daemon/radio recovery is implemented yet; restart the probe after disruptions.

Use the existing Android app to check discovery, status reads, subscription and outgoing framing. Pairing intentionally cannot complete. For writes, small-MTU tests, offset reads and a competing subscriber, also use a GATT test client capable of selecting MTU and write mode. The fixed error response ID is diagnostic, not correlated to the Android request. A UI error alone does not prove notification delivery: record the characteristic traffic using test data only.

Protocol v1's JSON/base64 frame cannot fit the default 20-byte ATT payload. The probe uses BlueR's writer payload limit directly (do not subtract ATT overhead again), refuses an insufficient limit, and asks the operator to test a larger negotiated MTU. This exposes an interoperability constraint rather than inventing a second wire-fragmentation format.

If registration fails, check system D-Bus and bluetoothd availability, ordinary-user permissions, adapter power/rfkill, GATT/peripheral support and available advertising instances. The tool never powers an adapter on or disconnects unrelated peers to make room. A host with a locked-down D-Bus policy remains an unsupported test configuration until its normal-user configuration is understood.

## Required physical evidence (not yet performed)

Record Ubuntu/BlueZ version, kernel, adapter model/firmware, Android model/app version, client tooling and outcome for each test. Use at least two independent adapters and two Android devices. Do not include typed text, secrets or packet dumps containing credentials in evidence.

| Test | Expected observation | Result |
| --- | --- | --- |
| Ordinary user, powered adapter | Advertising and GATT registration; Android sees status and correct UUID/properties | Not run |
| RX request and write-without-response | Complete test messages counted; no execution or echo | Not run |
| Fragmented messages and interleaved IDs | Shared framing limits and expiry enforced | Not run |
| Status offsets and long reads | Reconstructed status matches protocol v1 JSON | Not run |
| Default/small and larger MTUs | Explicit failure at insufficient payload; complete public response at sufficient payload | Not run |
| Subscribe, unsubscribe, reconnect | Channel cleanup and fresh reassembly; no stale fragments | Not run |
| Second Android/GATT subscriber | Record whether either peer receives the other's public test stream, and effect of refusing the second writer | Not run; security gate |
| No adapter/daemon, power off, rfkill, advertising exhaustion | Sanitized failure; no radio or unrelated-device changes | Not run |
| Daemon restart, adapter removal, suspend, slow reader | No unbounded queue; restart needed; no persisted state | Not run |
| Ctrl-C, deadline, process termination | Owned service/advertisement disappear; fresh run registers cleanly | Not run |

Only successful hardware evidence can close the interoperability/peer-isolation checkpoint. Secure session integration, persistent Linux credentials, reconnect recovery and X11 input remain separate increments.

## Primary references

- [BlueR 0.17.4 local GATT API](https://docs.rs/bluer/0.17.4/bluer/gatt/local/index.html)
- [BlueR notification writer API](https://docs.rs/bluer/0.17.4/bluer/gatt/struct.CharacteristicWriter.html)
- [BlueZ characteristic API: request options and AcquireNotify](https://github.com/bluez/bluez/blob/master/doc/org.bluez.GattCharacteristic.rst)
- [BlueZ advertising registration API](https://github.com/bluez/bluez/blob/master/doc/org.bluez.LEAdvertisingManager.rst)
Loading