From b4d91d534663bf08a9d234f3f76a8b68360f21de Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:11:11 -0600 Subject: [PATCH 01/77] feat: add one-tap iBridge Mobile bootstrap --- src-tauri/src/mobile_bootstrap.rs | 161 ++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 src-tauri/src/mobile_bootstrap.rs diff --git a/src-tauri/src/mobile_bootstrap.rs b/src-tauri/src/mobile_bootstrap.rs new file mode 100644 index 00000000..ebcbd162 --- /dev/null +++ b/src-tauri/src/mobile_bootstrap.rs @@ -0,0 +1,161 @@ +use std::sync::Mutex; + +use chrono::Utc; +use idevice::{IdeviceService, installation_proxy::InstallationProxyClient}; +use serde::Serialize; +use tauri::{AppHandle, Manager, State, Window}; + +use crate::{ + device::{DeviceInfo, DeviceInfoMutex, get_provider}, + error::AppError, + operation::Operation, + pairing::place_file, + sideload::{SideloaderMutex, download, sideload}, +}; + +const DEFAULT_MOBILE_IPA_URL: &str = + "https://github.com/DannyBaanks/iBridge/releases/latest/download/iBridge-Mobile.ipa"; +const MOBILE_DISPLAY_NAME: &str = "iBridge"; +const MOBILE_BUNDLE_PREFIX: &str = "com.dannybaanks.ibridge.mobile"; +const MOBILE_PAIRING_PATH: &str = "bootstrap/pairing.plist"; +const MOBILE_BOOTSTRAP_PATH: &str = "bootstrap/iBridgeBootstrap.json"; + +#[derive(Clone)] +pub struct MobileBootstrapAuth { + pub email: String, + pub anisette_server: String, +} + +pub type MobileBootstrapAuthMutex = Mutex>; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct MobileBootstrapPayload { + version: u8, + apple_id: String, + anisette_server: String, + device_udid: String, + device_name: String, + device_version: String, + created_at: String, +} + +async fn installed_mobile_bundle_id(device: &DeviceInfo) -> Result { + let provider = get_provider(device).await?; + let mut installation_proxy = InstallationProxyClient::connect(&provider) + .await + .map_err(|e| { + AppError::DeviceComsWithMessage( + "Failed to connect to installation proxy".into(), + e.to_string(), + ) + })?; + + let installed_apps = installation_proxy + .get_apps(Some("User"), None) + .await + .map_err(|e| { + AppError::DeviceComsWithMessage("Failed to get installed apps".into(), e.to_string()) + })?; + + for (bundle_id, app) in installed_apps { + let dictionary = match app.as_dictionary() { + Some(value) => value, + None => continue, + }; + let display_name = dictionary + .get("CFBundleDisplayName") + .or_else(|| dictionary.get("CFBundleName")) + .and_then(|value| value.as_string()); + + if display_name == Some(MOBILE_DISPLAY_NAME) || bundle_id.starts_with(MOBILE_BUNDLE_PREFIX) { + return Ok(bundle_id); + } + } + + Err(AppError::Misc( + "iBridge Mobile was installed but its bundle could not be found".into(), + )) +} + +#[tauri::command] +pub async fn install_ibridge_mobile_operation( + handle: AppHandle, + window: Window, + device_state: State<'_, DeviceInfoMutex>, + sideloader_state: State<'_, SideloaderMutex>, + mobile_auth_state: State<'_, MobileBootstrapAuthMutex>, +) -> Result<(), AppError> { + let op = Operation::new("install_ibridge_mobile".to_string(), &window); + + let device = { + let guard = device_state.lock().unwrap(); + guard.clone().ok_or(AppError::NoDeviceSelected)? + }; + let auth = { + let guard = mobile_auth_state.lock().unwrap(); + guard.clone().ok_or(AppError::NotLoggedIn)? + }; + + op.start("download")?; + let mobile_ipa_url = std::env::var("IBRIDGE_MOBILE_IPA_URL") + .unwrap_or_else(|_| DEFAULT_MOBILE_IPA_URL.to_string()); + let destination = handle + .path() + .temp_dir() + .map_err(|e| AppError::Filesystem("Failed to get temp dir".into(), e.to_string()))? + .join("iBridge-Mobile.ipa"); + op.fail_if_err("download", download(&mobile_ipa_url, &destination).await)?; + + op.move_on("download", "install")?; + op.fail_if_err( + "install", + sideload( + device_state, + sideloader_state, + destination.to_string_lossy().to_string(), + ) + .await, + )?; + + op.move_on("install", "bootstrap")?; + let bundle_id = op.fail_if_err("bootstrap", installed_mobile_bundle_id(&device.info).await)?; + let provider = op.fail_if_err("bootstrap", get_provider(&device.info).await)?; + + op.fail_if_err( + "bootstrap", + place_file( + device.pairing.clone(), + &provider, + bundle_id.clone(), + MOBILE_PAIRING_PATH.to_string(), + ) + .await, + )?; + + let payload = MobileBootstrapPayload { + version: 1, + apple_id: auth.email, + anisette_server: auth.anisette_server, + device_udid: device.info.udid, + device_name: device.info.name, + device_version: device.info.version, + created_at: Utc::now().to_rfc3339(), + }; + let payload = serde_json::to_vec(&payload) + .map_err(|e| AppError::Misc(format!("Failed to encode mobile bootstrap: {e}")))?; + + op.fail_if_err( + "bootstrap", + place_file( + payload, + &provider, + bundle_id, + MOBILE_BOOTSTRAP_PATH.to_string(), + ) + .await, + )?; + + op.complete("bootstrap")?; + Ok(()) +} From 7da7038e09f9e2687cd3b707980e1964d21d4abf Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:12:03 -0600 Subject: [PATCH 02/77] feat: retain mobile bootstrap identity in memory --- src-tauri/src/account.rs | 45 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/account.rs b/src-tauri/src/account.rs index 8069eabc..2db48ff1 100644 --- a/src-tauri/src/account.rs +++ b/src-tauri/src/account.rs @@ -20,6 +20,7 @@ use tracing::debug; use crate::{ error::AppError, + mobile_bootstrap::{MobileBootstrapAuth, MobileBootstrapAuthMutex}, secure_storage::create_sideloading_storage, sideload::{SideloaderGuard, SideloaderMutex}, }; @@ -29,14 +30,30 @@ pub async fn login_new( handle: AppHandle, window: Window, sideloader_state: State<'_, SideloaderMutex>, + mobile_auth_state: State<'_, MobileBootstrapAuthMutex>, email: String, password: String, anisette_server: String, save_credentials: bool, ) -> Result<(), AppError> { - let account = login(&handle, &window, &email, &password, anisette_server).await?; + let account = login( + &handle, + &window, + &email, + &password, + anisette_server.clone(), + ) + .await?; let mut sideloader_guard = sideloader_state.lock().unwrap(); *sideloader_guard = Some(account); + drop(sideloader_guard); + + let mut mobile_auth_guard = mobile_auth_state.lock().unwrap(); + *mobile_auth_guard = Some(MobileBootstrapAuth { + email: email.to_lowercase(), + anisette_server, + }); + drop(mobile_auth_guard); if save_credentials { let pass_entry = Entry::new("iloader", &email).map_err(|e| { @@ -73,6 +90,7 @@ pub async fn login_stored( email: String, anisette_server: String, sideloader_state: State<'_, SideloaderMutex>, + mobile_auth_state: State<'_, MobileBootstrapAuthMutex>, ) -> Result<(), AppError> { let pass_entry = Entry::new("iloader", &email).map_err(|e| { AppError::KeyringWithMessage( @@ -83,9 +101,23 @@ pub async fn login_stored( let password = pass_entry.get_password().map_err(|e| { AppError::KeyringWithMessage("Failed to get credentials".to_string(), e.to_string()) })?; - let account = login(&handle, &window, &email, &password, anisette_server).await?; + let account = login( + &handle, + &window, + &email, + &password, + anisette_server.clone(), + ) + .await?; let mut sideloader_guard = sideloader_state.lock().unwrap(); *sideloader_guard = Some(account); + drop(sideloader_guard); + + let mut mobile_auth_guard = mobile_auth_state.lock().unwrap(); + *mobile_auth_guard = Some(MobileBootstrapAuth { + email: email.to_lowercase(), + anisette_server, + }); Ok(()) } @@ -125,9 +157,16 @@ pub fn logged_in_as(sideloader_state: State<'_, SideloaderMutex>) -> Option) { +pub fn invalidate_account( + sideloader_state: State<'_, SideloaderMutex>, + mobile_auth_state: State<'_, MobileBootstrapAuthMutex>, +) { let mut sideloader_guard = sideloader_state.lock().unwrap(); *sideloader_guard = None; + drop(sideloader_guard); + + let mut mobile_auth_guard = mobile_auth_state.lock().unwrap(); + *mobile_auth_guard = None; } #[tauri::command] From 22d9272bb3a38478344a4c696c983c76d54870f2 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:12:26 -0600 Subject: [PATCH 03/77] feat: wire iBridge Mobile bootstrap into desktop --- src-tauri/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b217fe90..61f72db6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,6 +10,7 @@ mod pairing; mod secure_storage; mod error; mod logging; +mod mobile_bootstrap; mod operation; use crate::{ @@ -20,6 +21,7 @@ use crate::{ device::{ DeviceInfoMutex, PairingCancelToken, cancel_pairing, list_devices, set_selected_device, }, + mobile_bootstrap::{MobileBootstrapAuthMutex, install_ibridge_mobile_operation}, pairing::{ delete_stored_rppairing, export_pairing_cmd, has_stored_rppairing, installed_pairing_apps, place_pairing_cmd, @@ -101,6 +103,7 @@ pub fn run() { app.manage(DeviceInfoMutex::new(None)); app.manage(SideloaderMutex::new(None)); app.manage(PairingCancelToken::new(None)); + app.manage(MobileBootstrapAuthMutex::new(None)); Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -113,6 +116,7 @@ pub fn run() { sideload_operation, set_selected_device, install_sidestore_operation, + install_ibridge_mobile_operation, get_certificates, revoke_certificate, list_app_ids, From c5399e72d2cf495739e9384dee48fb8e8318a3b4 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:12:34 -0600 Subject: [PATCH 04/77] ci: add fast test gate for iBridge --- .github/workflows/test.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..a195322a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: Test iBridge + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + +jobs: + test: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v6 + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Install Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf perl make + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install frontend dependencies + run: bun i --frozen-lockfile + + - name: Rust tests + run: cargo test --manifest-path src-tauri/Cargo.toml + + - name: Frontend build + run: bun run build From 688f8c330b5c9ac37d69deff69d93e106b06cbab Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:12:47 -0600 Subject: [PATCH 05/77] feat: add iBridge Mobile install operation --- src/components/operations.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/components/operations.ts b/src/components/operations.ts index 3b5fdb46..0db4c447 100644 --- a/src/components/operations.ts +++ b/src/components/operations.ts @@ -36,6 +36,27 @@ type OperationFailedUpdate = { export type OperationUpdate = OperationInfoUpdate | OperationFailedUpdate; +export const installIBridgeMobileOperation: Operation = { + id: "install_ibridge_mobile", + titleKey: "operations.install_ibridge_mobile_title", + successTitleKey: "operations.install_ibridge_mobile_success_title", + successMessageKey: "operations.install_ibridge_mobile_success_message", + steps: [ + { + id: "download", + titleKey: "operations.install_ibridge_mobile_step_download", + }, + { + id: "install", + titleKey: "operations.install_ibridge_mobile_step_install", + }, + { + id: "bootstrap", + titleKey: "operations.install_ibridge_mobile_step_bootstrap", + }, + ], +}; + export const installSideStoreOperation: Operation = { id: "install_sidestore", titleKey: "operations.install_sidestore_title", From 524d222dce116d46875475866eeee21a99767409 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:12:53 -0600 Subject: [PATCH 06/77] test: define one-tap mobile bootstrap contract --- src-tauri/tests/mobile_bootstrap_contract.rs | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src-tauri/tests/mobile_bootstrap_contract.rs diff --git a/src-tauri/tests/mobile_bootstrap_contract.rs b/src-tauri/tests/mobile_bootstrap_contract.rs new file mode 100644 index 00000000..877ff3cb --- /dev/null +++ b/src-tauri/tests/mobile_bootstrap_contract.rs @@ -0,0 +1,47 @@ +use iloader_lib::mobile_bootstrap::{ + MobileBootstrapPayload, MOBILE_BOOTSTRAP_PATH, MOBILE_PAIRING_PATH, MOBILE_RELEASE_URL, + MOBILE_SECRET_PATH, is_ibridge_mobile_bundle_id, +}; + +#[test] +fn mobile_release_contract_points_to_ibridge_asset() { + assert_eq!( + MOBILE_RELEASE_URL, + "https://github.com/DannyBaanks/iBridge/releases/latest/download/iBridge-Mobile.ipa" + ); + assert_eq!(MOBILE_PAIRING_PATH, "iBridgeBootstrap/pairing.plist"); + assert_eq!(MOBILE_BOOTSTRAP_PATH, "iBridgeBootstrap/bootstrap.json"); + assert_eq!(MOBILE_SECRET_PATH, "iBridgeBootstrap/account-secret.json"); +} + +#[test] +fn mobile_bundle_match_survives_team_suffix_added_by_isideload() { + assert!(is_ibridge_mobile_bundle_id("me.dannybaanks.ibridge.mobile")); + assert!(is_ibridge_mobile_bundle_id( + "me.dannybaanks.ibridge.mobile.A1B2C3D4E5" + )); + assert!(!is_ibridge_mobile_bundle_id("com.SideStore.SideStore")); +} + +#[test] +fn bootstrap_payload_contains_only_non_secret_account_metadata() { + let payload = MobileBootstrapPayload::new( + "00008110-001234567890001E", + "Danny's iPhone", + "26.4", + "danny@example.com", + "https://ani.example.test", + ); + + let json = serde_json::to_value(&payload).expect("bootstrap should serialize"); + + assert_eq!(json["schema"], "ibridge.mobile-bootstrap/1"); + assert_eq!(json["deviceUdid"], "00008110-001234567890001E"); + assert_eq!(json["deviceName"], "Danny's iPhone"); + assert_eq!(json["deviceVersion"], "26.4"); + assert_eq!(json["appleId"], "danny@example.com"); + assert_eq!(json["anisetteServer"], "https://ani.example.test"); + assert_eq!(json["pairingPath"], MOBILE_PAIRING_PATH); + assert_eq!(json["secretPath"], MOBILE_SECRET_PATH); + assert!(json.get("password").is_none()); +} From 16b3b4379f45d02c9ce738f516bd0be7c3fc01bc Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:13:38 -0600 Subject: [PATCH 07/77] feat: add one-tap iBridge Mobile installer button --- src/App.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 85bcd910..0d76e0b8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { open as openFileDialog } from "@tauri-apps/plugin-dialog"; import { openUrl } from "@tauri-apps/plugin-opener"; import { sideloadOperation, + installIBridgeMobileOperation, installSideStoreOperation, installLiveContainerOperation, Operation, @@ -214,7 +215,6 @@ function App() {

{t("app.section_account")}

- {/* here to ensure spacing and stuff is correct */} @@ -305,6 +305,17 @@ function App() {
+ + + +
+
+
+ + {operationState && ( + setOperationState(null)} + /> + )} + + + + ); +} From 0b10a9bdc7dc7913b73e204c27ce7d873c892888 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:29:37 -0600 Subject: [PATCH 40/77] fix: align desktop discovery with mobile bundle identifier --- src-tauri/src/mobile_bootstrap.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/mobile_bootstrap.rs b/src-tauri/src/mobile_bootstrap.rs index e36ac659..48d0dd45 100644 --- a/src-tauri/src/mobile_bootstrap.rs +++ b/src-tauri/src/mobile_bootstrap.rs @@ -16,7 +16,7 @@ use crate::{ pub const MOBILE_RELEASE_URL: &str = "https://github.com/DannyBaanks/iBridge/releases/latest/download/iBridge-Mobile.ipa"; const MOBILE_DISPLAY_NAME: &str = "iBridge"; -pub const MOBILE_BUNDLE_PREFIX: &str = "me.dannybaanks.ibridge.mobile"; +pub const MOBILE_BUNDLE_PREFIX: &str = "com.dannybaanks.ibridge.mobile"; pub const MOBILE_PAIRING_PATH: &str = "iBridgeBootstrap/pairing.plist"; pub const MOBILE_BOOTSTRAP_PATH: &str = "iBridgeBootstrap/bootstrap.json"; pub const MOBILE_SECRET_PATH: &str = "iBridgeBootstrap/account-secret.json"; From 2baf79dfbfaf7f25a0b10b50fd55dd4020727c65 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:29:42 -0600 Subject: [PATCH 41/77] feat: boot directly into iBridge Mobile on iPhone and iPad --- src/main.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main.tsx b/src/main.tsx index ec8c10cf..b26e9a9d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; +import MobileApp from "./MobileApp"; import { Toaster } from "sonner"; import { StoreProvider } from "./StoreContext"; import { LogProvider } from "./LogContext"; @@ -9,6 +10,12 @@ import { DialogProvider } from "./DialogContext"; import "./i18next"; import { PlatformProvider } from "./PlatformContext"; +const ua = navigator.userAgent || ""; +const platformName = navigator.platform || ""; +const isTouchMac = platformName === "MacIntel" && navigator.maxTouchPoints > 1; +const isIOS = /iPhone|iPad|iPod/i.test(ua) || isTouchMac; +const RootApp = isIOS ? MobileApp : App; + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( @@ -16,7 +23,7 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + From 01906c15cbfdc48862e511470b869d7fb9da56b6 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:32:20 -0600 Subject: [PATCH 42/77] feat: define iBridge iOS identity for Tauri mobile --- src-tauri/tauri.ios.conf.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src-tauri/tauri.ios.conf.json diff --git a/src-tauri/tauri.ios.conf.json b/src-tauri/tauri.ios.conf.json new file mode 100644 index 00000000..da733db6 --- /dev/null +++ b/src-tauri/tauri.ios.conf.json @@ -0,0 +1,9 @@ +{ + "productName": "iBridge", + "identifier": "com.dannybaanks.ibridge.mobile", + "bundle": { + "iOS": { + "minimumSystemVersion": "17.0" + } + } +} From 86d2aae57625415308f0a8997d8970b9425b31a0 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:32:44 -0600 Subject: [PATCH 43/77] ci: build the actual Tauri iBridge fork as an iOS IPA --- .github/workflows/mobile.yml | 55 ++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index c75934c6..60a13a96 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -25,43 +25,44 @@ jobs: with: targets: aarch64-apple-ios - - name: Install XcodeGen - run: brew install xcodegen + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest - - name: Build Rust signing core - run: | - cargo build \ - --manifest-path mobile/ibridge-core/Cargo.toml \ - --release \ - --target aarch64-apple-ios - mkdir -p mobile/build - cp mobile/ibridge-core/target/aarch64-apple-ios/release/libibridge_core.a mobile/build/libridge_core.a + - name: Install frontend dependencies + run: bun i --frozen-lockfile + + - name: Initialize Tauri iOS project + run: bun tauri ios init --ci - - name: Generate Xcode project - working-directory: mobile - run: xcodegen generate --spec project.yml + - name: Build frontend assets + run: bun run build - - name: Compile unsigned iOS app - working-directory: mobile + - name: Compile unsigned Tauri iOS app run: | - mkdir -p build/app + PROJECT=$(find src-tauri/gen/apple -maxdepth 1 -name '*.xcodeproj' -print -quit) + test -n "$PROJECT" + SCHEME=$(xcodebuild -project "$PROJECT" -list -json | python3 -c 'import json,sys; data=json.load(sys.stdin); print(data["project"]["schemes"][0])') + mkdir -p mobile-ci/app xcodebuild \ - -project iBridgeMobile.xcodeproj \ - -scheme iBridgeMobile \ - -configuration Release \ + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -configuration release \ -sdk iphoneos \ -destination 'generic/platform=iOS' \ CODE_SIGNING_ALLOWED=NO \ - CONFIGURATION_BUILD_DIR="$PWD/build/app" \ + CODE_SIGNING_REQUIRED=NO \ + CONFIGURATION_BUILD_DIR="$PWD/mobile-ci/app" \ build - name: Package resignable IPA - working-directory: mobile run: | - test -d build/app/iBridge.app - mkdir -p build/Payload - cp -R build/app/iBridge.app build/Payload/iBridge.app - cd build + APP=$(find mobile-ci/app -maxdepth 1 -name '*.app' -print -quit) + test -n "$APP" + rm -rf mobile-ci/Payload + mkdir -p mobile-ci/Payload + cp -R "$APP" mobile-ci/Payload/iBridge.app + cd mobile-ci zip -qry iBridge-Mobile.ipa Payload test -s iBridge-Mobile.ipa @@ -69,10 +70,10 @@ jobs: uses: actions/upload-artifact@v7 with: name: iBridge-Mobile - path: mobile/build/iBridge-Mobile.ipa + path: mobile-ci/iBridge-Mobile.ipa - name: Attach IPA to release if: github.event_name == 'release' env: GH_TOKEN: ${{ github.token }} - run: gh release upload "${{ github.event.release.tag_name }}" mobile/build/iBridge-Mobile.ipa --clobber + run: gh release upload "${{ github.event.release.tag_name }}" mobile-ci/iBridge-Mobile.ipa --clobber From 5f52dc50e33e2c94b9280972697750f64c0fd4a2 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:33:26 -0600 Subject: [PATCH 44/77] build: add libc for iOS tunnel peer discovery --- src-tauri/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 634114c4..344dc254 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -43,6 +43,7 @@ tokio-util = "0.7.18" rootcause = "0.12.1" thiserror = "2.0.18" strum = {version = "0.28.0", features = ["derive"] } +libc = "0.2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" From 1dae95ba6a47a9869697473ee054c680be9781c4 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:34:12 -0600 Subject: [PATCH 45/77] feat: autodetect iOS tunnel peer for one-tap refresh --- src-tauri/src/mobile_runtime.rs | 92 ++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/mobile_runtime.rs b/src-tauri/src/mobile_runtime.rs index 2711cbc8..1b3bb82a 100644 --- a/src-tauri/src/mobile_runtime.rs +++ b/src-tauri/src/mobile_runtime.rs @@ -20,15 +20,14 @@ use crate::{ sideload::{SideloaderGuard, SideloaderMutex, download}, }; -pub const MOBILE_TUNNEL_DEVICE_IP: &str = "10.7.0.0"; -pub const MOBILE_TUNNEL_HOST_IP: &str = "10.7.0.1"; +pub const MOBILE_TUNNEL_FALLBACK_IP: &str = "10.7.0.1"; #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct MobileRuntimeStatus { pub pairing_ready: bool, pub tunnel_reachable: bool, - pub tunnel_host: &'static str, + pub tunnel_host: String, } fn document_path(handle: &AppHandle, relative: &str) -> Result { @@ -39,6 +38,83 @@ fn document_path(handle: &AppHandle, relative: &str) -> Result String { + #[cfg(any(target_os = "ios", target_os = "macos"))] + if let Some(host) = discover_point_to_point_peer() { + return host; + } + + MOBILE_TUNNEL_FALLBACK_IP.to_string() +} + +#[cfg(any(target_os = "ios", target_os = "macos"))] +fn discover_point_to_point_peer() -> Option { + use std::{ffi::CStr, ptr}; + + unsafe { + let mut interfaces: *mut libc::ifaddrs = ptr::null_mut(); + if libc::getifaddrs(&mut interfaces) != 0 || interfaces.is_null() { + return None; + } + + struct Guard(*mut libc::ifaddrs); + impl Drop for Guard { + fn drop(&mut self) { + unsafe { libc::freeifaddrs(self.0) }; + } + } + let _guard = Guard(interfaces); + + let mut current = interfaces; + while !current.is_null() { + let interface = &*current; + current = interface.ifa_next; + + let flags = interface.ifa_flags as i32; + if flags & libc::IFF_UP == 0 || flags & libc::IFF_POINTOPOINT == 0 { + continue; + } + if interface.ifa_dstaddr.is_null() { + continue; + } + + let name = CStr::from_ptr(interface.ifa_name).to_string_lossy(); + if !name.starts_with("utun") { + continue; + } + + let address = interface.ifa_dstaddr; + let family = (*address).sa_family as i32; + let length = match family { + libc::AF_INET => std::mem::size_of::() as libc::socklen_t, + libc::AF_INET6 => std::mem::size_of::() as libc::socklen_t, + _ => continue, + }; + + let mut host = [0_i8; libc::NI_MAXHOST as usize]; + if libc::getnameinfo( + address, + length, + host.as_mut_ptr(), + host.len() as libc::socklen_t, + ptr::null_mut(), + 0, + libc::NI_NUMERICHOST, + ) != 0 + { + continue; + } + + let value = CStr::from_ptr(host.as_ptr()).to_string_lossy().into_owned(); + if value != "0.0.0.0" && value != "::" && !value.is_empty() { + return Some(value); + } + } + } + + None +} + fn mobile_provider(handle: &AppHandle) -> Result { let pairing_path = document_path(handle, MOBILE_PAIRING_PATH)?; let pairing_bytes = std::fs::read(&pairing_path).map_err(|e| { @@ -49,8 +125,9 @@ fn mobile_provider(handle: &AppHandle) -> Result { })?; let pairing_file = PairingFile::from_bytes(&pairing_bytes) .map_err(|e| AppError::LockdownPairing("Failed to parse mobile pairing bootstrap".into(), e.to_string()))?; - let addr = IpAddr::from_str(MOBILE_TUNNEL_HOST_IP) - .map_err(|e| AppError::Misc(format!("Invalid mobile tunnel address: {e}")))?; + let tunnel_host = tunnel_host(); + let addr = IpAddr::from_str(&tunnel_host) + .map_err(|e| AppError::Misc(format!("Invalid mobile tunnel address {tunnel_host}: {e}")))?; Ok(TcpProvider { addr, @@ -129,11 +206,12 @@ pub async fn mobile_restore_bootstrap_account( #[tauri::command] pub async fn mobile_runtime_status(handle: AppHandle) -> Result { let pairing_ready = document_path(&handle, MOBILE_PAIRING_PATH)?.is_file(); + let host = tunnel_host(); if !pairing_ready { return Ok(MobileRuntimeStatus { pairing_ready: false, tunnel_reachable: false, - tunnel_host: MOBILE_TUNNEL_HOST_IP, + tunnel_host: host, }); } @@ -143,7 +221,7 @@ pub async fn mobile_runtime_status(handle: AppHandle) -> Result Date: Fri, 2 Oct 2026 00:34:47 -0600 Subject: [PATCH 46/77] feat: scaffold native iBridge tunnel plugin --- src-tauri/plugins/ibridge-tunnel/Cargo.toml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src-tauri/plugins/ibridge-tunnel/Cargo.toml diff --git a/src-tauri/plugins/ibridge-tunnel/Cargo.toml b/src-tauri/plugins/ibridge-tunnel/Cargo.toml new file mode 100644 index 00000000..d783f0e6 --- /dev/null +++ b/src-tauri/plugins/ibridge-tunnel/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tauri-plugin-ibridge-tunnel" +version = "0.1.0" +edition = "2024" +links = "tauri-plugin-ibridge-tunnel" + +[build-dependencies] +tauri-plugin = { version = "2", features = ["build"] } + +[dependencies] +serde = { version = "1", features = ["derive"] } +tauri = "2" From 85957f61e48bbb7fe4e0927b70efcc368bde7345 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:34:54 -0600 Subject: [PATCH 47/77] feat: wire iOS sources for tunnel plugin --- src-tauri/plugins/ibridge-tunnel/build.rs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src-tauri/plugins/ibridge-tunnel/build.rs diff --git a/src-tauri/plugins/ibridge-tunnel/build.rs b/src-tauri/plugins/ibridge-tunnel/build.rs new file mode 100644 index 00000000..ad00b326 --- /dev/null +++ b/src-tauri/plugins/ibridge-tunnel/build.rs @@ -0,0 +1,8 @@ +const COMMANDS: &[&str] = &["start", "status", "stop"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .ios_path("ios") + .try_build() + .expect("failed to build iBridge tunnel plugin"); +} From 061e8202042838659335f18a6ad06e961e781190 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:35:05 -0600 Subject: [PATCH 48/77] feat: expose native tunnel controls to Rust --- src-tauri/plugins/ibridge-tunnel/src/lib.rs | 52 +++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src-tauri/plugins/ibridge-tunnel/src/lib.rs diff --git a/src-tauri/plugins/ibridge-tunnel/src/lib.rs b/src-tauri/plugins/ibridge-tunnel/src/lib.rs new file mode 100644 index 00000000..68b6c5bd --- /dev/null +++ b/src-tauri/plugins/ibridge-tunnel/src/lib.rs @@ -0,0 +1,52 @@ +#![cfg(target_os = "ios")] + +use serde::Deserialize; +use tauri::{ + Manager, Runtime, + plugin::{Builder, PluginHandle, TauriPlugin}, +}; + +tauri::ios_plugin_binding!(init_plugin_ibridge_tunnel); + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TunnelStatus { + pub connected: bool, + pub status: String, +} + +pub struct IBridgeTunnel(PluginHandle); + +impl IBridgeTunnel { + pub fn start(&self) -> Result { + self.0.run_mobile_plugin("start", ()) + } + + pub fn status(&self) -> Result { + self.0.run_mobile_plugin("status", ()) + } + + pub fn stop(&self) -> Result { + self.0.run_mobile_plugin("stop", ()) + } +} + +pub trait IBridgeTunnelExt { + fn ibridge_tunnel(&self) -> &IBridgeTunnel; +} + +impl> IBridgeTunnelExt for T { + fn ibridge_tunnel(&self) -> &IBridgeTunnel { + self.state::>().inner() + } +} + +pub fn init() -> TauriPlugin { + Builder::new("ibridge-tunnel") + .setup(|app, api| { + let handle = api.register_ios_plugin(init_plugin_ibridge_tunnel)?; + app.manage(IBridgeTunnel(handle)); + Ok(()) + }) + .build() +} From 63da875a3535a3259d74c6099637785af729afe5 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:35:16 -0600 Subject: [PATCH 49/77] feat: package Swift tunnel controller with Tauri --- .../plugins/ibridge-tunnel/ios/Package.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src-tauri/plugins/ibridge-tunnel/ios/Package.swift diff --git a/src-tauri/plugins/ibridge-tunnel/ios/Package.swift b/src-tauri/plugins/ibridge-tunnel/ios/Package.swift new file mode 100644 index 00000000..a5929efb --- /dev/null +++ b/src-tauri/plugins/ibridge-tunnel/ios/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "tauri-plugin-ibridge-tunnel", + platforms: [.iOS(.v17)], + products: [ + .library( + name: "tauri-plugin-ibridge-tunnel", + type: .static, + targets: ["tauri-plugin-ibridge-tunnel"]) + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + .target( + name: "tauri-plugin-ibridge-tunnel", + dependencies: [.byName(name: "Tauri")], + path: "Sources") + ] +) From 3952bf9e16bb84839af01bdd42a6bf4ceb8a7367 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:35:23 -0600 Subject: [PATCH 50/77] fix: add iBridge desktop copy with English and Spanish fallback --- src/i18next.ts | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/i18next.ts b/src/i18next.ts index 2953d667..de7d223b 100644 --- a/src/i18next.ts +++ b/src/i18next.ts @@ -29,7 +29,7 @@ const languages = [ ["hu", "Magyar"], ["kh", "ភាសាខ្មែរ"], ["id", "Bahasa Indonesia"], - ["pt_br", "Português (Brasileiro)"] + ["pt_br", "Português (Brasileiro)"], ] as const; export const sortedLanguages = [...languages].sort((a, b) => @@ -37,6 +37,7 @@ export const sortedLanguages = [...languages].sort((a, b) => ); type TranslationResource = Record; +type ResourceMap = Record; const localeModules = import.meta.glob<{ default: TranslationResource }>( "./locales/*.json", @@ -52,7 +53,45 @@ const resources = Object.fromEntries( return [[lang, { translation: module.default }]]; }), -); +) as ResourceMap; + +const iBridgeCopy = { + en: { + app: { ibridge_mobile: "Install iBridge Mobile" }, + operations: { + install_ibridge_mobile_title: "Installing iBridge Mobile", + install_ibridge_mobile_success_title: "iBridge Mobile Installed!", + install_ibridge_mobile_success_message: + "Open iBridge on your iPhone. Pairing and account bootstrap are already in place.", + install_ibridge_mobile_step_download: "Download iBridge Mobile", + install_ibridge_mobile_step_install: "Sign & Install iBridge Mobile", + install_ibridge_mobile_step_bootstrap: "Configure This iPhone", + }, + }, + es: { + app: { ibridge_mobile: "Instalar iBridge Mobile" }, + operations: { + install_ibridge_mobile_title: "Instalando iBridge Mobile", + install_ibridge_mobile_success_title: "¡iBridge Mobile instalado!", + install_ibridge_mobile_success_message: + "Abre iBridge en tu iPhone. El pairing y la cuenta ya quedaron configurados.", + install_ibridge_mobile_step_download: "Descargar iBridge Mobile", + install_ibridge_mobile_step_install: "Firmar e instalar iBridge Mobile", + install_ibridge_mobile_step_bootstrap: "Configurar este iPhone", + }, + }, +} satisfies Record>>; + +for (const [language, sections] of Object.entries(iBridgeCopy)) { + const translation = resources[language]?.translation; + if (!translation) continue; + for (const [section, copy] of Object.entries(sections)) { + translation[section] = { + ...((translation[section] as Record | undefined) ?? {}), + ...copy, + }; + } +} i18n .use(LanguageDetector) From 29a6ae5bde7f20044e8ef5c2fd68e1a7e0beec0e Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:35:36 -0600 Subject: [PATCH 51/77] feat: manage embedded packet tunnel from iBridge Mobile --- .../ios/Sources/IBridgeTunnelPlugin.swift | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src-tauri/plugins/ibridge-tunnel/ios/Sources/IBridgeTunnelPlugin.swift diff --git a/src-tauri/plugins/ibridge-tunnel/ios/Sources/IBridgeTunnelPlugin.swift b/src-tauri/plugins/ibridge-tunnel/ios/Sources/IBridgeTunnelPlugin.swift new file mode 100644 index 00000000..1f50e2da --- /dev/null +++ b/src-tauri/plugins/ibridge-tunnel/ios/Sources/IBridgeTunnelPlugin.swift @@ -0,0 +1,114 @@ +import NetworkExtension +import Tauri + +private let providerBundleIdentifier = "me.dannybaanks.ibridge.mobile.tunnel" +private let tunnelDeviceIP = "10.7.0.0" +private let tunnelFakeIP = "10.7.0.1" + +private func statusName(_ status: NEVPNStatus) -> String { + switch status { + case .invalid: return "invalid" + case .disconnected: return "disconnected" + case .connecting: return "connecting" + case .connected: return "connected" + case .reasserting: return "reasserting" + case .disconnecting: return "disconnecting" + @unknown default: return "unknown" + } +} + +private func response(_ manager: NETunnelProviderManager?) -> [String: Any] { + let status = manager?.connection.status ?? .invalid + return [ + "connected": status == .connected, + "status": statusName(status), + ] +} + +private func loadManager(_ completion: @escaping (Result) -> Void) { + NETunnelProviderManager.loadAllFromPreferences { managers, error in + if let error { + completion(.failure(error)) + return + } + + if let existing = managers?.first(where: { + ($0.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier + == providerBundleIdentifier + }) { + completion(.success(existing)) + return + } + + let manager = NETunnelProviderManager() + let configuration = NETunnelProviderProtocol() + configuration.providerBundleIdentifier = providerBundleIdentifier + configuration.serverAddress = "iBridge Local Tunnel" + manager.protocolConfiguration = configuration + manager.localizedDescription = "iBridge Local Tunnel" + manager.isEnabled = true + + manager.saveToPreferences { saveError in + if let saveError { + completion(.failure(saveError)) + return + } + manager.loadFromPreferences { loadError in + if let loadError { + completion(.failure(loadError)) + } else { + completion(.success(manager)) + } + } + } + } +} + +final class IBridgeTunnelPlugin: Plugin { + @objc public func start(_ invoke: Invoke) throws { + loadManager { result in + switch result { + case .failure(let error): + invoke.reject(error.localizedDescription) + case .success(let manager): + do { + try manager.connection.startVPNTunnel(options: [ + "TunnelDeviceIP": tunnelDeviceIP as NSString, + "TunnelFakeIP": tunnelFakeIP as NSString, + ]) + invoke.resolve(response(manager)) + } catch { + invoke.reject(error.localizedDescription) + } + } + } + } + + @objc public func status(_ invoke: Invoke) throws { + loadManager { result in + switch result { + case .failure(let error): + invoke.reject(error.localizedDescription) + case .success(let manager): + invoke.resolve(response(manager)) + } + } + } + + @objc public func stop(_ invoke: Invoke) throws { + loadManager { result in + switch result { + case .failure(let error): + invoke.reject(error.localizedDescription) + case .success(let manager): + manager.connection.stopVPNTunnel() + invoke.resolve(response(manager)) + } + } + } +} + +@_cdecl("init_plugin_ibridge_tunnel") +func initPlugin() -> Plugin { + IBridgeTunnelPlugin() +} From 7f91f8609fc08ad41fa8f3d88fedeb6f267c8458 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:35:56 -0600 Subject: [PATCH 52/77] feat: embed local packet reflector extension source --- .../PacketTunnelProvider.swift | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src-tauri/ios/IBridgeTunnelProvider/PacketTunnelProvider.swift diff --git a/src-tauri/ios/IBridgeTunnelProvider/PacketTunnelProvider.swift b/src-tauri/ios/IBridgeTunnelProvider/PacketTunnelProvider.swift new file mode 100644 index 00000000..f496a697 --- /dev/null +++ b/src-tauri/ios/IBridgeTunnelProvider/PacketTunnelProvider.swift @@ -0,0 +1,98 @@ +// iBridge local packet reflector. +// +// Based on the packet-reflection technique from SideStore/StosVPN: +// https://github.com/SideStore/StosVPN +// Copyright (c) 2025 SideStore Team. +// See THIRD_PARTY_NOTICES.md for license and attribution. + +import NetworkExtension + +final class PacketTunnelProvider: NEPacketTunnelProvider { + private var tunnelDeviceIP = "10.7.0.0" + private var tunnelFakeIP = "10.7.0.1" + private let tunnelSubnetMask = "255.255.255.0" + + private var deviceIPValue: UInt32 = 0 + private var fakeIPValue: UInt32 = 0 + + override func startTunnel( + options: [String: NSObject]?, + completionHandler: @escaping (Error?) -> Void + ) { + if let deviceIP = options?["TunnelDeviceIP"] as? String { + tunnelDeviceIP = deviceIP + } + if let fakeIP = options?["TunnelFakeIP"] as? String { + tunnelFakeIP = fakeIP + } + + deviceIPValue = ipToUInt32(tunnelDeviceIP) + fakeIPValue = ipToUInt32(tunnelFakeIP) + + let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: tunnelDeviceIP) + let ipv4 = NEIPv4Settings( + addresses: [tunnelDeviceIP], + subnetMasks: [tunnelSubnetMask] + ) + ipv4.includedRoutes = [ + NEIPv4Route(destinationAddress: tunnelDeviceIP, subnetMask: tunnelSubnetMask) + ] + ipv4.excludedRoutes = [.default()] + settings.ipv4Settings = ipv4 + + setTunnelNetworkSettings(settings) { [weak self] error in + guard error == nil else { + completionHandler(error) + return + } + self?.pumpPackets() + completionHandler(nil) + } + } + + override func stopTunnel( + with reason: NEProviderStopReason, + completionHandler: @escaping () -> Void + ) { + completionHandler() + } + + private func pumpPackets() { + packetFlow.readPackets { [weak self] packets, protocols in + guard let self else { return } + + var modified = packets + for index in modified.indices + where protocols[index].int32Value == AF_INET && modified[index].count >= 20 { + modified[index].withUnsafeMutableBytes { bytes in + guard let words = bytes.baseAddress?.assumingMemoryBound(to: UInt32.self) else { + return + } + let source = UInt32(bigEndian: words[3]) + let destination = UInt32(bigEndian: words[4]) + if source == self.deviceIPValue { + words[3] = self.fakeIPValue.bigEndian + } + if destination == self.fakeIPValue { + words[4] = self.deviceIPValue.bigEndian + } + } + } + + self.packetFlow.writePackets(modified, withProtocols: protocols) + self.pumpPackets() + } + } + + private func ipToUInt32(_ address: String) -> UInt32 { + let parts = address.split(separator: ".") + guard parts.count == 4, + let a = UInt32(parts[0]), + let b = UInt32(parts[1]), + let c = UInt32(parts[2]), + let d = UInt32(parts[3]) else { + return 0 + } + return (a << 24) | (b << 16) | (c << 8) | d + } +} From 90decc3d94469d24b7fb5371023e3d9292ec0f91 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:36:42 -0600 Subject: [PATCH 53/77] feat: link native tunnel plugin only on iOS --- src-tauri/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 344dc254..c7c737fe 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -45,5 +45,8 @@ thiserror = "2.0.18" strum = {version = "0.28.0", features = ["derive"] } libc = "0.2" +[target.'cfg(target_os = "ios")'.dependencies] +tauri-plugin-ibridge-tunnel = { path = "plugins/ibridge-tunnel" } + [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" From b3905ce952d203c4cd32c58bab4b7bca961fdf00 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:37:20 -0600 Subject: [PATCH 54/77] feat: expose embedded tunnel controls to mobile UI --- src-tauri/src/tunnel_control.rs | 83 +++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src-tauri/src/tunnel_control.rs diff --git a/src-tauri/src/tunnel_control.rs b/src-tauri/src/tunnel_control.rs new file mode 100644 index 00000000..536d77d7 --- /dev/null +++ b/src-tauri/src/tunnel_control.rs @@ -0,0 +1,83 @@ +use serde::Serialize; +use tauri::AppHandle; + +use crate::error::AppError; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TunnelControlStatus { + pub connected: bool, + pub status: String, +} + +#[tauri::command] +pub fn mobile_tunnel_start(handle: AppHandle) -> Result { + #[cfg(target_os = "ios")] + { + use tauri_plugin_ibridge_tunnel::IBridgeTunnelExt; + let status = handle + .ibridge_tunnel() + .start() + .map_err(|e| AppError::Misc(format!("Failed to start iBridge tunnel: {e}")))?; + return Ok(TunnelControlStatus { + connected: status.connected, + status: status.status, + }); + } + + #[cfg(not(target_os = "ios"))] + { + let _ = handle; + Err(AppError::Misc( + "The embedded iBridge tunnel is only available on iOS".into(), + )) + } +} + +#[tauri::command] +pub fn mobile_tunnel_status(handle: AppHandle) -> Result { + #[cfg(target_os = "ios")] + { + use tauri_plugin_ibridge_tunnel::IBridgeTunnelExt; + let status = handle + .ibridge_tunnel() + .status() + .map_err(|e| AppError::Misc(format!("Failed to query iBridge tunnel: {e}")))?; + return Ok(TunnelControlStatus { + connected: status.connected, + status: status.status, + }); + } + + #[cfg(not(target_os = "ios"))] + { + let _ = handle; + Err(AppError::Misc( + "The embedded iBridge tunnel is only available on iOS".into(), + )) + } +} + +#[tauri::command] +pub fn mobile_tunnel_stop(handle: AppHandle) -> Result { + #[cfg(target_os = "ios")] + { + use tauri_plugin_ibridge_tunnel::IBridgeTunnelExt; + let status = handle + .ibridge_tunnel() + .stop() + .map_err(|e| AppError::Misc(format!("Failed to stop iBridge tunnel: {e}")))?; + return Ok(TunnelControlStatus { + connected: status.connected, + status: status.status, + }); + } + + #[cfg(not(target_os = "ios"))] + { + let _ = handle; + Err(AppError::Misc( + "The embedded iBridge tunnel is only available on iOS".into(), + )) + } +} From da3ffb02f5b28d535669e35dd1c31226dbdf8ecf Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:37:27 -0600 Subject: [PATCH 55/77] ci: cancel stale iBridge test runs --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a195322a..3b1f759a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,10 @@ on: branches: - "*" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-22.04 From d116360fc41776cc08ef953c4f3c3dbd9a3d673f Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:37:44 -0600 Subject: [PATCH 56/77] feat: register embedded iOS tunnel plugin and commands --- src-tauri/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e1c4b5e9..a8d5921d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,6 +13,7 @@ mod logging; pub mod mobile_bootstrap; mod mobile_runtime; mod operation; +mod tunnel_control; use crate::{ account::{ @@ -33,6 +34,7 @@ use crate::{ }, secure_storage::{force_disable_keyring, keyring_available}, sideload::{SideloaderMutex, install_sidestore_operation, sideload_operation}, + tunnel_control::{mobile_tunnel_start, mobile_tunnel_status, mobile_tunnel_stop}, }; use tauri::Manager; use tracing_subscriber::{Layer, Registry, fmt, layer::SubscriberExt, util::SubscriberInitExt}; @@ -48,6 +50,9 @@ pub fn run() { #[cfg(not(mobile))] let builder = builder.plugin(tauri_plugin_updater::Builder::new().build()); + #[cfg(target_os = "ios")] + let builder = builder.plugin(tauri_plugin_ibridge_tunnel::init()); + builder .setup(|app| { let log_dir = app @@ -130,6 +135,9 @@ pub fn run() { mobile_runtime_status, mobile_sideload_operation, mobile_refresh_self_operation, + mobile_tunnel_start, + mobile_tunnel_status, + mobile_tunnel_stop, get_certificates, revoke_certificate, list_app_ids, From 38fd8b0143fa70487e437d4569e05e17e9da5150 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:37:50 -0600 Subject: [PATCH 57/77] ci: cancel stale mobile builds --- .github/workflows/mobile.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index 60a13a96..4aee06fc 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -11,6 +11,10 @@ on: release: types: [published] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + permissions: contents: write From 8fc6345ed76ea1122913955a24e8be7ee3b40e32 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:38:26 -0600 Subject: [PATCH 58/77] feat: auto-start embedded tunnel in iBridge Mobile --- src/MobileApp.tsx | 76 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/src/MobileApp.tsx b/src/MobileApp.tsx index c3f5bd20..d6c4e7bb 100644 --- a/src/MobileApp.tsx +++ b/src/MobileApp.tsx @@ -17,16 +17,17 @@ import logo from "./iloader.svg"; const mobileSideloadOperation: Operation = { id: "mobile_sideload", - titleKey: "operations.sideload_title", - steps: [{ id: "install", titleKey: "operations.sideload_step_install" }], + titleKey: "operations.mobile_sideload_title", + steps: [{ id: "install", titleKey: "operations.mobile_sideload_step_install" }], }; const mobileRefreshOperation: Operation = { id: "mobile_refresh_self", - titleKey: "operations.sideload_title", + titleKey: "operations.mobile_refresh_title", + successTitleKey: "operations.mobile_refresh_success_title", steps: [ - { id: "download", titleKey: "operations.install_ibridge_mobile_step_download" }, - { id: "install", titleKey: "operations.sideload_step_install" }, + { id: "download", titleKey: "operations.mobile_refresh_step_download" }, + { id: "install", titleKey: "operations.mobile_refresh_step_install" }, ], }; @@ -36,10 +37,19 @@ type RuntimeStatus = { tunnelHost: string; }; +type TunnelControlStatus = { + connected: boolean; + status: string; +}; + +const delay = (milliseconds: number) => + new Promise((resolve) => window.setTimeout(resolve, milliseconds)); + export default function MobileApp() { const [loggedInAs, setLoggedInAs] = useState(null); const [noKeyringAvailable, setNoKeyringAvailable] = useState(false); const [status, setStatus] = useState(null); + const [tunnelState, setTunnelState] = useState("starting"); const [restoring, setRestoring] = useState(true); const [operationState, setOperationState] = useState(null); @@ -47,11 +57,31 @@ export default function MobileApp() { try { const next = await invoke("mobile_runtime_status"); setStatus(next); + if (next.tunnelReachable) setTunnelState("connected"); } catch (error) { console.error("Unable to read mobile runtime status", error); } }, []); + const startTunnel = useCallback(async () => { + try { + const tunnel = await invoke("mobile_tunnel_start"); + setTunnelState(tunnel.status); + for (const wait of [250, 500, 1000]) { + await delay(wait); + const next = await invoke("mobile_runtime_status"); + setStatus(next); + if (next.tunnelReachable) { + setTunnelState("connected"); + return; + } + } + } catch (error) { + console.error("Unable to start iBridge Tunnel", error); + setTunnelState("disconnected"); + } + }, []); + const startOperation = useCallback( async (operation: Operation, params: Record) => { setOperationState({ @@ -101,7 +131,8 @@ export default function MobileApp() { .catch(() => setNoKeyringAvailable(true)); refreshStatus(); - }, [refreshStatus]); + startTunnel(); + }, [refreshStatus, startTunnel]); useEffect(() => { let cancelled = false; @@ -127,12 +158,17 @@ export default function MobileApp() { return; } if (!status?.pairingReady) { - toast.error("This iPhone has not been bootstrapped by iBridge Desktop."); + toast.error("Connect this iPhone to iBridge Desktop once to finish setup."); return; } if (!status.tunnelReachable) { - toast.error("iBridge Tunnel is not reachable yet."); - return; + await startTunnel(); + const next = await invoke("mobile_runtime_status"); + setStatus(next); + if (!next.tunnelReachable) { + toast.error("iBridge Tunnel could not connect. Tap Start Tunnel and try again."); + return; + } } const path = await openFileDialog({ @@ -142,7 +178,7 @@ export default function MobileApp() { if (!path) return; await startOperation(mobileSideloadOperation, { appPath: path as string }); - }, [loggedInAs, status, startOperation]); + }, [loggedInAs, status, startOperation, startTunnel]); return (
@@ -152,7 +188,7 @@ export default function MobileApp() { iBridge

iBridge Mobile

-

One-tap sideloading from this iPhone

+

Choose an IPA. iBridge handles the rest.

@@ -161,26 +197,30 @@ export default function MobileApp() {
-

This iPhone

+

Ready

- Pairing: {status?.pairingReady ? "Ready" : "Missing"} -
-
- Tunnel: {status?.tunnelReachable ? "Connected" : "Disconnected"} + Device setup: {status?.pairingReady ? "Ready" : "Needs desktop setup"}
- Host: {status?.tunnelHost ?? "10.7.0.1"} + Local tunnel: {status?.tunnelReachable ? "Connected" : tunnelState}
+ {!status?.tunnelReachable && ( +
+ +
+ )}

Apple ID

- {restoring &&

Restoring desktop bootstrap…

} + {restoring &&

Restoring desktop setup…

} Date: Fri, 2 Oct 2026 00:39:17 -0600 Subject: [PATCH 59/77] feat: add iBridge Mobile Network Extension entitlement --- src-tauri/ios/iBridge.entitlements | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src-tauri/ios/iBridge.entitlements diff --git a/src-tauri/ios/iBridge.entitlements b/src-tauri/ios/iBridge.entitlements new file mode 100644 index 00000000..afa5a4d7 --- /dev/null +++ b/src-tauri/ios/iBridge.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + + From e5f4f9ff450708096afc6ffe416a4dfded05de63 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:39:23 -0600 Subject: [PATCH 60/77] feat: add tunnel provider entitlement --- .../IBridgeTunnelProvider.entitlements | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src-tauri/ios/IBridgeTunnelProvider/IBridgeTunnelProvider.entitlements diff --git a/src-tauri/ios/IBridgeTunnelProvider/IBridgeTunnelProvider.entitlements b/src-tauri/ios/IBridgeTunnelProvider/IBridgeTunnelProvider.entitlements new file mode 100644 index 00000000..afa5a4d7 --- /dev/null +++ b/src-tauri/ios/IBridgeTunnelProvider/IBridgeTunnelProvider.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + + From e0e24e3d0e0c632e9bb54f99c9ac4ca2c24c1959 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:40:13 -0600 Subject: [PATCH 61/77] feat: embed packet tunnel extension in generated iOS project --- src-tauri/ios/project.yml.hbs | 200 ++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 src-tauri/ios/project.yml.hbs diff --git a/src-tauri/ios/project.yml.hbs b/src-tauri/ios/project.yml.hbs new file mode 100644 index 00000000..b1c24f4f --- /dev/null +++ b/src-tauri/ios/project.yml.hbs @@ -0,0 +1,200 @@ +name: {{app.name}} +options: + bundleIdPrefix: {{app.identifier}} + deploymentTarget: + iOS: {{apple.ios-version}} + macOS: {{apple.macos-version}} +fileGroups: [{{join file-groups}}] +configs: + debug: debug + release: release +settingGroups: + app: + base: + PRODUCT_NAME: {{app.stylized-name}} + PRODUCT_BUNDLE_IDENTIFIER: {{app.identifier}} + {{#if apple.development-team}} + DEVELOPMENT_TEAM: {{apple.development-team}} + {{/if}} +targetTemplates: + app: + type: application + sources: + - path: Sources + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + settings: + groups: [app] +targets: + {{app.name}}_iOS: + type: application + platform: iOS + sources: + - path: Sources + - path: {{app.asset-dir}} + buildPhase: resources + type: folder + {{#if asset-catalogs}}{{~#each asset-catalogs}} + - {{prefix-path this}}{{/each}}{{/if}} + {{#if ios-additional-targets}}{{~#each ios-additional-targets}} + - path: {{prefix-path this}}{{/each}}{{/if}} + info: + path: {{app.name}}_iOS/Info.plist + properties: + LSRequiresIPhoneOS: true + UILaunchStoryboardName: LaunchScreen + UIRequiredDeviceCapabilities: [arm64, metal] + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + UISupportedInterfaceOrientations~ipad: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationPortraitUpsideDown + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + CFBundleShortVersionString: {{apple.bundle-version-short}} + CFBundleVersion: {{apple.bundle-version}} + {{#if apple.plist-pairs}}{{~#each apple.plist-pairs}} + {{this.key}}: {{this.value}}{{/each}}{{/if}} + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + {{~#if ios-command-line-arguments}} + commandLineArguments: + {{~#each ios-command-line-arguments}} + "{{this}}": true + {{/each}}{{~/if}} + settings: + base: + ENABLE_BITCODE: {{apple.enable-bitcode}} + ARCHS: [{{join ios-valid-archs}}] + VALID_ARCHS: {{#if ios-valid-archs}}{{~#each ios-valid-archs}} {{this}} {{/each}}{{/if}} + LIBRARY_SEARCH_PATHS[sdk=iphoneos*]: $(inherited) $(PROJECT_DIR)/Sources/aarch64-apple-ios/$(CONFIGURATION) + LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]: $(inherited) "{{prefix-path "target/"}}{{ios-sim-arch}}/$(CONFIGURATION)" + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true + CODE_SIGN_ENTITLEMENTS: {{prefix-path "ios/iBridge.entitlements"}} + groups: [app] + dependencies: + - framework: lib{{#if app.lib-name}}{{app.lib-name}}{{else}}{{snake-case app.name}}{{/if}}.a + embed: false + {{#if ios-libraries}}{{~#each ios-libraries}} + - framework: {{this}} + embed: false{{/each}}{{/if}} + {{#if ios-vendor-frameworks}}{{~#each ios-vendor-frameworks}} + - framework: {{prefix-path this}}{{/each}}{{/if}} + {{#if ios-vendor-sdks}}{{~#each ios-vendor-sdks}} + - sdk: {{prefix-path this}}{{/each}}{{/if}} + - sdk: CoreGraphics.framework + - sdk: Metal.framework + - sdk: MetalKit.framework + - sdk: QuartzCore.framework + - sdk: Security.framework + - sdk: UIKit.framework + - sdk: NetworkExtension.framework + {{#if ios-frameworks}}{{~#each ios-frameworks}} + - sdk: {{this}}.framework{{/each}}{{/if}} + - target: IBridgeTunnelProvider + embed: true + preBuildScripts: + {{#if ios-pre-build-scripts}}{{~#each ios-pre-build-scripts}}{{#if this.path}} + - path: {{this.path}}{{/if}}{{#if this.script}} + - script: {{this.script}}{{/if}}{{#if this.name}} + name: {{this.name}}{{/if}}{{#if this.input-files}} + inputFiles: {{~#each this.input-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-files}} + outputFiles: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.input-file-lists}} + inputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-file-lists}} + outputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.shell}} + shell: {{this.shell}}{{/if}}{{#if this.show-env-vars}} + showEnvVars: {{this.show_env_vars}}{{/if}}{{#if this.run-only-when-installing}} + runOnlyWhenInstalling: {{this.run-only-when-installing}}{{/if}}{{#if this.based-on-dependency-analysis}} + basedOnDependencyAnalysis: {{this.based-on-dependency-analysis}}{{/if}}{{#if this.discovered-dependency-file}} + discoveredDependencyFile: {{this.discovered-dependency-file}}{{/if}} + {{~/each}}{{/if}} + - script: |- + PATH=${PATH}:${HOME:?}/.cargo/bin + CARGO_APPLE=`which cargo-apple` + ${CARGO_APPLE} xcode-script -v --platform "${PLATFORM_DISPLAY_NAME:?}" --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:?}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?} ${FEATURES} + name: Build Rust Code + basedOnDependencyAnalysis: false + outputFiles: + - $(SRCROOT)/target/aarch64-apple-ios/${CONFIGURATION}/deps/lib{{#if app.lib-name}}{{app.lib-name}}{{else}}{{snake-case app.name}}{{/if}}.a + - $(SRCROOT)/target/x86_64-apple-ios/${CONFIGURATION}/deps/lib{{#if app.lib-name}}{{app.lib-name}}{{else}}{{snake-case app.name}}{{/if}}.a + {{~#if ios-post-compile-scripts}} + postCompileScripts: + {{~#each ios-post-compile-scripts}}{{#if this.path}} + - path: {{this.path}}{{/if}}{{#if this.script}} + - script: {{this.script}}{{/if}}{{#if this.name}} + name: {{this.name}}{{/if}}{{#if this.input-files}} + inputFiles: {{~#each this.input-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-files}} + outputFiles: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.input-file-lists}} + inputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-file-lists}} + outputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.shell}} + shell: {{this.shell}}{{/if}}{{#if this.show-env-vars}} + showEnvVars: {{this.show_env_vars}}{{/if}}{{#if this.run-only-when-installing}} + runOnlyWhenInstalling: {{this.run-only-when-installing}}{{/if}}{{#if this.based-on-dependency-analysis}} + basedOnDependencyAnalysis: {{this.based-on-dependency-analysis}}{{/if}}{{#if this.discovered-dependency-file}} + discoveredDependencyFile: {{this.discovered-dependency-file}}{{/if}} + {{~/each~}} + {{~/if~}} + {{~#if ios-post-build-scripts}} + postBuildScripts: + {{~#each ios-post-build-scripts}}{{#if this.path}} + - path: {{this.path}}{{/if}}{{#if this.script}} + - script: {{this.script}}{{/if}}{{#if this.name}} + name: {{this.name}}{{/if}}{{#if this.input-files}} + inputFiles: {{~#each this.input-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-files}} + outputFiles: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.input-file-lists}} + inputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-file-lists}} + outputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.shell}} + shell: {{this.shell}}{{/if}}{{#if this.show-env-vars}} + showEnvVars: {{this.show_env_vars}}{{/if}}{{#if this.run-only-when-installing}} + runOnlyWhenInstalling: {{this.run-only-when-installing}}{{/if}}{{#if this.based-on-dependency-analysis}} + basedOnDependencyAnalysis: {{this.based-on-dependency-analysis}}{{/if}}{{#if this.discovered-dependency-file}} + discoveredDependencyFile: {{this.discovered-dependency-file}}{{/if}} + {{~/each~}} + {{~/if}} + + IBridgeTunnelProvider: + type: app-extension + platform: iOS + deploymentTarget: {{apple.ios-version}} + sources: + - path: {{prefix-path "ios/IBridgeTunnelProvider"}} + info: + path: IBridgeTunnelProvider/Info.plist + properties: + CFBundleDisplayName: iBridge Tunnel + CFBundleShortVersionString: {{apple.bundle-version-short}} + CFBundleVersion: {{apple.bundle-version}} + NSExtension: + NSExtensionPointIdentifier: com.apple.networkextension.packet-tunnel + NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).PacketTunnelProvider + settings: + base: + PRODUCT_NAME: iBridgeTunnelProvider + PRODUCT_BUNDLE_IDENTIFIER: me.dannybaanks.ibridge.mobile.tunnel + CODE_SIGN_ENTITLEMENTS: {{prefix-path "ios/IBridgeTunnelProvider/IBridgeTunnelProvider.entitlements"}} + APPLICATION_EXTENSION_API_ONLY: true + SKIP_INSTALL: true + TARGETED_DEVICE_FAMILY: "1,2" + {{#if apple.development-team}} + DEVELOPMENT_TEAM: {{apple.development-team}} + {{/if}} + dependencies: + - sdk: NetworkExtension.framework From 353b4e8042f72728f48b371dc591ed8158b87eaf Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:40:48 -0600 Subject: [PATCH 62/77] feat: bind iBridge Mobile to embedded tunnel Xcode template --- src-tauri/tauri.ios.conf.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src-tauri/tauri.ios.conf.json b/src-tauri/tauri.ios.conf.json index da733db6..2eb4c7df 100644 --- a/src-tauri/tauri.ios.conf.json +++ b/src-tauri/tauri.ios.conf.json @@ -1,9 +1,12 @@ { + "$schema": "https://schema.tauri.app/config/2", "productName": "iBridge", - "identifier": "com.dannybaanks.ibridge.mobile", + "identifier": "me.dannybaanks.ibridge.mobile", "bundle": { "iOS": { - "minimumSystemVersion": "17.0" + "minimumSystemVersion": "17.0", + "frameworks": ["NetworkExtension"], + "template": "ios/project.yml.hbs" } } } From 30e94a2103e58a19ffe5b53469ae40c375c52d44 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:41:25 -0600 Subject: [PATCH 63/77] docs: attribute embedded StosVPN packet reflector --- THIRD_PARTY_NOTICES.md | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 THIRD_PARTY_NOTICES.md diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 00000000..6598b2e1 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,50 @@ +# Third-party notices + +## StosVPN packet reflection technique + +iBridge Mobile contains an adapted packet-reflection implementation in +`src-tauri/ios/IBridgeTunnelProvider/PacketTunnelProvider.swift` based on +[SideStore/StosVPN](https://github.com/SideStore/StosVPN), specifically the +`PacketTunnelProvider.swift` implementation authored by the SideStore Team. + +iBridge is not StosVPN and does not imply endorsement by the SideStore Team. +The adapted code is used only as the local `NEPacketTunnelProvider` transport +inside iBridge Mobile. + +Source reference: SideStore/StosVPN commit +`010bc141e6c2303b218924686f830e3249c66842`. + +### StosVPN License + +Copyright (c) 2025 SideStore Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +**Attribution is clearly given** to the original project and author(s) in a +prominent place (e.g., README, About page, or documentation). + +Any derived project **must clearly state** that it is **based on** or **uses +code from** the original project. + +Redistribution, rebranding, or publishing of the **entire project** (or a +substantially similar copy) under the same or similar name or branding, +**without explicit written permission**, is **prohibited**. + +This clause is intended to prevent unauthorized re-uploads, clones, or +misrepresentation of the original work. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 2e6abfadbdb647f52bfee89c12ba9c85df7b6fcd Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:42:26 -0600 Subject: [PATCH 64/77] ci: verify embedded tunnel inside iBridge Mobile IPA --- .github/workflows/mobile.yml | 48 ++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index 4aee06fc..7d132c4f 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -43,10 +43,25 @@ jobs: run: bun run build - name: Compile unsigned Tauri iOS app + shell: bash run: | + set -euo pipefail PROJECT=$(find src-tauri/gen/apple -maxdepth 1 -name '*.xcodeproj' -print -quit) - test -n "$PROJECT" - SCHEME=$(xcodebuild -project "$PROJECT" -list -json | python3 -c 'import json,sys; data=json.load(sys.stdin); print(data["project"]["schemes"][0])') + if [ -z "$PROJECT" ]; then + echo "Generated Xcode project not found" >&2 + exit 1 + fi + + SCHEME=$(xcodebuild -project "$PROJECT" -list -json | python3 -c ' + import json, sys + data = json.load(sys.stdin) + schemes = data.get("project", {}).get("schemes", []) + scheme = next((value for value in schemes if value.endswith("_iOS")), None) + if not scheme: + raise SystemExit("No generated iOS scheme found") + print(scheme) + ') + mkdir -p mobile-ci/app xcodebuild \ -project "$PROJECT" \ @@ -56,25 +71,42 @@ jobs: -destination 'generic/platform=iOS' \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO \ + DEVELOPMENT_TEAM= \ + ARCHS=arm64 \ + ONLY_ACTIVE_ARCH=YES \ CONFIGURATION_BUILD_DIR="$PWD/mobile-ci/app" \ build - name: Package resignable IPA + shell: bash run: | - APP=$(find mobile-ci/app -maxdepth 1 -name '*.app' -print -quit) - test -n "$APP" - rm -rf mobile-ci/Payload + set -euo pipefail + APP=$(find mobile-ci/app -maxdepth 1 -type d -name '*.app' -print -quit) + if [ -z "$APP" ]; then + echo "Built iOS app not found" >&2 + exit 1 + fi + + if [ ! -d "$APP/PlugIns/IBridgeTunnelProvider.appex" ]; then + echo "Embedded IBridgeTunnelProvider.appex not found" >&2 + find "$APP" -maxdepth 3 -print + exit 1 + fi + mkdir -p mobile-ci/Payload cp -R "$APP" mobile-ci/Payload/iBridge.app - cd mobile-ci - zip -qry iBridge-Mobile.ipa Payload - test -s iBridge-Mobile.ipa + ( + cd mobile-ci + zip -qry iBridge-Mobile.ipa Payload + ) + test -s mobile-ci/iBridge-Mobile.ipa - name: Upload IPA artifact uses: actions/upload-artifact@v7 with: name: iBridge-Mobile path: mobile-ci/iBridge-Mobile.ipa + if-no-files-found: error - name: Attach IPA to release if: github.event_name == 'release' From 1c20c139fce181d2b31eea2f3be9dc28eeb4086a Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:43:30 -0600 Subject: [PATCH 65/77] feat: add iBridge Mobile operation copy --- src/i18next.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/i18next.ts b/src/i18next.ts index de7d223b..2bf2df64 100644 --- a/src/i18next.ts +++ b/src/i18next.ts @@ -66,6 +66,12 @@ const iBridgeCopy = { install_ibridge_mobile_step_download: "Download iBridge Mobile", install_ibridge_mobile_step_install: "Sign & Install iBridge Mobile", install_ibridge_mobile_step_bootstrap: "Configure This iPhone", + mobile_sideload_title: "Installing IPA", + mobile_sideload_step_install: "Sign & Install IPA", + mobile_refresh_title: "Refreshing iBridge", + mobile_refresh_success_title: "iBridge Refreshed!", + mobile_refresh_step_download: "Download iBridge Mobile", + mobile_refresh_step_install: "Sign & Refresh iBridge", }, }, es: { @@ -78,6 +84,12 @@ const iBridgeCopy = { install_ibridge_mobile_step_download: "Descargar iBridge Mobile", install_ibridge_mobile_step_install: "Firmar e instalar iBridge Mobile", install_ibridge_mobile_step_bootstrap: "Configurar este iPhone", + mobile_sideload_title: "Instalando IPA", + mobile_sideload_step_install: "Firmar e instalar IPA", + mobile_refresh_title: "Refrescando iBridge", + mobile_refresh_success_title: "¡iBridge refrescado!", + mobile_refresh_step_download: "Descargar iBridge Mobile", + mobile_refresh_step_install: "Firmar y refrescar iBridge", }, }, } satisfies Record>>; From 80790aa0fc6e989305a9187a06a7ad25ab95c6ab Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:48:33 -0600 Subject: [PATCH 66/77] feat: preconfigure Network Extensions before signing iBridge Mobile --- src-tauri/src/mobile_bootstrap.rs | 64 ++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/mobile_bootstrap.rs b/src-tauri/src/mobile_bootstrap.rs index 48d0dd45..edfbd68e 100644 --- a/src-tauri/src/mobile_bootstrap.rs +++ b/src-tauri/src/mobile_bootstrap.rs @@ -2,6 +2,8 @@ use std::sync::Mutex; use chrono::Utc; use idevice::{IdeviceService, installation_proxy::InstallationProxyClient}; +use isideload::dev::app_ids::AppIdsApi; +use plist_macro::plist; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager, State, Window}; @@ -10,7 +12,7 @@ use crate::{ error::AppError, operation::Operation, pairing::place_file, - sideload::{SideloaderMutex, download, sideload}, + sideload::{SideloaderGuard, SideloaderMutex, download, sideload}, }; pub const MOBILE_RELEASE_URL: &str = @@ -21,6 +23,7 @@ pub const MOBILE_PAIRING_PATH: &str = "iBridgeBootstrap/pairing.plist"; pub const MOBILE_BOOTSTRAP_PATH: &str = "iBridgeBootstrap/bootstrap.json"; pub const MOBILE_SECRET_PATH: &str = "iBridgeBootstrap/account-secret.json"; const MOBILE_RELEASE_FILENAME: &str = "iBridge-Mobile.ipa"; +const NETWORK_EXTENSION_FEATURE: &str = "NWEXT04537"; #[derive(Clone)] pub struct MobileBootstrapAuth { @@ -92,6 +95,61 @@ pub fn is_ibridge_mobile_bundle_id(bundle_id: &str) -> bool { .is_some_and(|suffix| suffix.starts_with('.')) } +async fn prepare_mobile_app_ids(sideloader_state: &SideloaderMutex) -> Result<(), AppError> { + let mut sideloader = SideloaderGuard::take(sideloader_state)?; + let team = sideloader.get_mut().get_team().await?; + let dev_session = sideloader.get_mut().get_dev_session(); + let existing = dev_session.list_app_ids(&team, None).await?.app_ids; + + let main_bundle_id = format!("{}.{}", MOBILE_BUNDLE_PREFIX, team.team_id); + let tunnel_bundle_id = format!("{}.tunnel", main_bundle_id); + + for (name, identifier) in [ + ("iBridge Mobile", main_bundle_id), + ("iBridge Tunnel", tunnel_bundle_id), + ] { + let mut app_id = if let Some(found) = existing + .iter() + .find(|app_id| app_id.identifier == identifier) + .cloned() + { + found + } else { + dev_session + .add_app_id(&team, name, &identifier, None) + .await? + }; + + let network_extension_enabled = app_id + .features + .get(NETWORK_EXTENSION_FEATURE) + .and_then(|value| value.as_boolean()) + .unwrap_or(false); + + if !network_extension_enabled { + let features = plist!(dict { + NETWORK_EXTENSION_FEATURE => true, + }); + app_id = dev_session + .update_app_id(&team, &app_id, features, None) + .await?; + + if !app_id + .features + .get(NETWORK_EXTENSION_FEATURE) + .and_then(|value| value.as_boolean()) + .unwrap_or(false) + { + return Err(AppError::Developer(format!( + "Apple did not enable Network Extensions for {identifier}. A developer team with the Network Extension capability is required." + ))); + } + } + } + + Ok(()) +} + async fn installed_mobile_bundle_id(device: &DeviceInfo) -> Result { let provider = get_provider(device).await?; let mut installation_proxy = InstallationProxyClient::connect(&provider) @@ -160,6 +218,10 @@ pub async fn install_ibridge_mobile_operation( op.fail_if_err("download", download(&mobile_ipa_url, &destination).await)?; op.move_on("download", "install")?; + op.fail_if_err( + "install", + prepare_mobile_app_ids(&sideloader_state).await, + )?; op.fail_if_err( "install", sideload( From f1e59581891999737c47fcf5d0f6101e6faaf2ee Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:51:09 -0600 Subject: [PATCH 67/77] fix: canonicalize iBridge Mobile bundle identifier --- src-tauri/tauri.ios.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.ios.conf.json b/src-tauri/tauri.ios.conf.json index 2eb4c7df..90661482 100644 --- a/src-tauri/tauri.ios.conf.json +++ b/src-tauri/tauri.ios.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "iBridge", - "identifier": "me.dannybaanks.ibridge.mobile", + "identifier": "com.dannybaanks.ibridge.mobile", "bundle": { "iOS": { "minimumSystemVersion": "17.0", From c81475e1f54666b53f0ff71e3ac03c873ccbf1a2 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:51:24 -0600 Subject: [PATCH 68/77] fix: align embedded tunnel bundle identifier --- .../ibridge-tunnel/ios/Sources/IBridgeTunnelPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/plugins/ibridge-tunnel/ios/Sources/IBridgeTunnelPlugin.swift b/src-tauri/plugins/ibridge-tunnel/ios/Sources/IBridgeTunnelPlugin.swift index 1f50e2da..4c6162ef 100644 --- a/src-tauri/plugins/ibridge-tunnel/ios/Sources/IBridgeTunnelPlugin.swift +++ b/src-tauri/plugins/ibridge-tunnel/ios/Sources/IBridgeTunnelPlugin.swift @@ -1,7 +1,7 @@ import NetworkExtension import Tauri -private let providerBundleIdentifier = "me.dannybaanks.ibridge.mobile.tunnel" +private let providerBundleIdentifier = "com.dannybaanks.ibridge.mobile.tunnel" private let tunnelDeviceIP = "10.7.0.0" private let tunnelFakeIP = "10.7.0.1" From 5657e971ca9f7101d85eece974b843dded575bb8 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:52:11 -0600 Subject: [PATCH 69/77] fix: align Xcode tunnel extension bundle identifier --- src-tauri/ios/project.yml.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/ios/project.yml.hbs b/src-tauri/ios/project.yml.hbs index b1c24f4f..3da3b60c 100644 --- a/src-tauri/ios/project.yml.hbs +++ b/src-tauri/ios/project.yml.hbs @@ -188,7 +188,7 @@ targets: settings: base: PRODUCT_NAME: iBridgeTunnelProvider - PRODUCT_BUNDLE_IDENTIFIER: me.dannybaanks.ibridge.mobile.tunnel + PRODUCT_BUNDLE_IDENTIFIER: com.dannybaanks.ibridge.mobile.tunnel CODE_SIGN_ENTITLEMENTS: {{prefix-path "ios/IBridgeTunnelProvider/IBridgeTunnelProvider.entitlements"}} APPLICATION_EXTENSION_API_ONLY: true SKIP_INSTALL: true From 6842bf755da4f7a3ed23d934fddf86a4fb7dc1c2 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:53:00 -0600 Subject: [PATCH 70/77] fix: build Network Extension feature dictionary explicitly --- src-tauri/src/mobile_bootstrap.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/mobile_bootstrap.rs b/src-tauri/src/mobile_bootstrap.rs index edfbd68e..88f06d98 100644 --- a/src-tauri/src/mobile_bootstrap.rs +++ b/src-tauri/src/mobile_bootstrap.rs @@ -3,7 +3,7 @@ use std::sync::Mutex; use chrono::Utc; use idevice::{IdeviceService, installation_proxy::InstallationProxyClient}; use isideload::dev::app_ids::AppIdsApi; -use plist_macro::plist; +use plist::Dictionary; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager, State, Window}; @@ -127,9 +127,8 @@ async fn prepare_mobile_app_ids(sideloader_state: &SideloaderMutex) -> Result<() .unwrap_or(false); if !network_extension_enabled { - let features = plist!(dict { - NETWORK_EXTENSION_FEATURE => true, - }); + let mut features = Dictionary::new(); + features.insert(NETWORK_EXTENSION_FEATURE.to_string(), true.into()); app_id = dev_session .update_app_id(&team, &app_id, features, None) .await?; From 550590ceb8885876050fe976eaffff8371006a23 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:55:59 -0600 Subject: [PATCH 71/77] chore: present desktop fork as iBridge --- src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0d76e0b8..1adcab02 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -186,7 +186,7 @@ function App() {
{t("app.logo_alt")}
-

iloader

+

iBridge

{t("subtitle")}

@@ -199,7 +199,7 @@ function App() { className="toolbar-button" onClick={async () => { try { - await openUrl("https://github.com/nab138/iloader"); + await openUrl("https://github.com/DannyBaanks/iBridge"); } catch (error) { console.error("Failed to open GitHub link", error); toast.error(t("app.open_github_failed")); From 9cc93da57a3c57809b1541e2890d011b2f1798cc Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:58:46 -0600 Subject: [PATCH 72/77] fix: pin mobile runtime to iBridge tunnel peer --- src-tauri/src/mobile_runtime.rs | 89 ++------------------------------- 1 file changed, 5 insertions(+), 84 deletions(-) diff --git a/src-tauri/src/mobile_runtime.rs b/src-tauri/src/mobile_runtime.rs index 1b3bb82a..0d103978 100644 --- a/src-tauri/src/mobile_runtime.rs +++ b/src-tauri/src/mobile_runtime.rs @@ -20,7 +20,7 @@ use crate::{ sideload::{SideloaderGuard, SideloaderMutex, download}, }; -pub const MOBILE_TUNNEL_FALLBACK_IP: &str = "10.7.0.1"; +pub const MOBILE_TUNNEL_HOST_IP: &str = "10.7.0.1"; #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -38,83 +38,6 @@ fn document_path(handle: &AppHandle, relative: &str) -> Result String { - #[cfg(any(target_os = "ios", target_os = "macos"))] - if let Some(host) = discover_point_to_point_peer() { - return host; - } - - MOBILE_TUNNEL_FALLBACK_IP.to_string() -} - -#[cfg(any(target_os = "ios", target_os = "macos"))] -fn discover_point_to_point_peer() -> Option { - use std::{ffi::CStr, ptr}; - - unsafe { - let mut interfaces: *mut libc::ifaddrs = ptr::null_mut(); - if libc::getifaddrs(&mut interfaces) != 0 || interfaces.is_null() { - return None; - } - - struct Guard(*mut libc::ifaddrs); - impl Drop for Guard { - fn drop(&mut self) { - unsafe { libc::freeifaddrs(self.0) }; - } - } - let _guard = Guard(interfaces); - - let mut current = interfaces; - while !current.is_null() { - let interface = &*current; - current = interface.ifa_next; - - let flags = interface.ifa_flags as i32; - if flags & libc::IFF_UP == 0 || flags & libc::IFF_POINTOPOINT == 0 { - continue; - } - if interface.ifa_dstaddr.is_null() { - continue; - } - - let name = CStr::from_ptr(interface.ifa_name).to_string_lossy(); - if !name.starts_with("utun") { - continue; - } - - let address = interface.ifa_dstaddr; - let family = (*address).sa_family as i32; - let length = match family { - libc::AF_INET => std::mem::size_of::() as libc::socklen_t, - libc::AF_INET6 => std::mem::size_of::() as libc::socklen_t, - _ => continue, - }; - - let mut host = [0_i8; libc::NI_MAXHOST as usize]; - if libc::getnameinfo( - address, - length, - host.as_mut_ptr(), - host.len() as libc::socklen_t, - ptr::null_mut(), - 0, - libc::NI_NUMERICHOST, - ) != 0 - { - continue; - } - - let value = CStr::from_ptr(host.as_ptr()).to_string_lossy().into_owned(); - if value != "0.0.0.0" && value != "::" && !value.is_empty() { - return Some(value); - } - } - } - - None -} - fn mobile_provider(handle: &AppHandle) -> Result { let pairing_path = document_path(handle, MOBILE_PAIRING_PATH)?; let pairing_bytes = std::fs::read(&pairing_path).map_err(|e| { @@ -125,9 +48,8 @@ fn mobile_provider(handle: &AppHandle) -> Result { })?; let pairing_file = PairingFile::from_bytes(&pairing_bytes) .map_err(|e| AppError::LockdownPairing("Failed to parse mobile pairing bootstrap".into(), e.to_string()))?; - let tunnel_host = tunnel_host(); - let addr = IpAddr::from_str(&tunnel_host) - .map_err(|e| AppError::Misc(format!("Invalid mobile tunnel address {tunnel_host}: {e}")))?; + let addr = IpAddr::from_str(MOBILE_TUNNEL_HOST_IP) + .map_err(|e| AppError::Misc(format!("Invalid mobile tunnel address: {e}")))?; Ok(TcpProvider { addr, @@ -206,12 +128,11 @@ pub async fn mobile_restore_bootstrap_account( #[tauri::command] pub async fn mobile_runtime_status(handle: AppHandle) -> Result { let pairing_ready = document_path(&handle, MOBILE_PAIRING_PATH)?.is_file(); - let host = tunnel_host(); if !pairing_ready { return Ok(MobileRuntimeStatus { pairing_ready: false, tunnel_reachable: false, - tunnel_host: host, + tunnel_host: MOBILE_TUNNEL_HOST_IP.to_string(), }); } @@ -221,7 +142,7 @@ pub async fn mobile_runtime_status(handle: AppHandle) -> Result Date: Fri, 2 Oct 2026 00:59:10 -0600 Subject: [PATCH 73/77] chore: remove obsolete tunnel discovery dependency --- src-tauri/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c7c737fe..a5cf4b5d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -43,7 +43,6 @@ tokio-util = "0.7.18" rootcause = "0.12.1" thiserror = "2.0.18" strum = {version = "0.28.0", features = ["derive"] } -libc = "0.2" [target.'cfg(target_os = "ios")'.dependencies] tauri-plugin-ibridge-tunnel = { path = "plugins/ibridge-tunnel" } From 2d40b15f17b696e2c109fb6a01832070053b31fa Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 00:59:58 -0600 Subject: [PATCH 74/77] chore: align experimental native mobile bundle id --- mobile/project.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/project.yml b/mobile/project.yml index 97586697..5726db02 100644 --- a/mobile/project.yml +++ b/mobile/project.yml @@ -1,6 +1,6 @@ name: iBridgeMobile options: - bundleIdPrefix: me.dannybaanks.ibridge + bundleIdPrefix: com.dannybaanks.ibridge deploymentTarget: iOS: "17.0" settings: @@ -17,7 +17,7 @@ targets: path: iBridgeMobile/Info.plist settings: base: - PRODUCT_BUNDLE_IDENTIFIER: me.dannybaanks.ibridge.mobile + PRODUCT_BUNDLE_IDENTIFIER: com.dannybaanks.ibridge.mobile PRODUCT_NAME: iBridge SWIFT_OBJC_BRIDGING_HEADER: iBridgeMobile/BridgingHeader.h LIBRARY_SEARCH_PATHS: "$(inherited) $(SRCROOT)/build" From 3d9d4c8118db57a6cc04df0e9201aca48c286004 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 01:03:11 -0600 Subject: [PATCH 75/77] fix: allow error suggestions on iOS --- src/errors.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/errors.tsx b/src/errors.tsx index b4baa74b..44b85363 100644 --- a/src/errors.tsx +++ b/src/errors.tsx @@ -45,6 +45,7 @@ export const errorSuggestionKeys = { } as const; export type ErrorVariant = keyof typeof errorSuggestionKeys; +export type ErrorSuggestionPlatform = "mac" | "windows" | "linux" | "ios"; export type AppError = { type: ErrorVariant; @@ -69,7 +70,7 @@ const dedupeSuggestions = (suggestions: string[]) => { const getSuggestionBlock = ( t: TFunction, key: string, - platform: "mac" | "windows" | "linux", + platform: ErrorSuggestionPlatform, anisetteServer: string, ) => { const rawSuggestions = t(key, { @@ -117,7 +118,7 @@ const getSuggestionBlock = ( export const getErrorSuggestions = ( t: TFunction, type: ErrorVariant, - platform: "mac" | "windows" | "linux", + platform: ErrorSuggestionPlatform, anisetteServer: string, ): string[] => { return dedupeSuggestions( From 0c4a9b855ceb10f41656382ee0d541f3d5e5b29e Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 02:19:02 -0600 Subject: [PATCH 76/77] fix: unblock mobile and desktop release builds --- src-tauri/Cargo.lock | 12 +++- src-tauri/src/account.rs | 74 ++++++++------------ src-tauri/src/mobile_bootstrap.rs | 34 ++++----- src-tauri/src/mobile_runtime.rs | 24 ++++--- src-tauri/tauri.ios.conf.json | 2 +- src-tauri/tests/mobile_bootstrap_contract.rs | 8 ++- src/MobileApp.tsx | 1 + 7 files changed, 77 insertions(+), 78 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e6790abb..8ee9c4e4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2618,7 +2618,7 @@ dependencies = [ [[package]] name = "iloader" -version = "2.3.2" +version = "2.3.3" dependencies = [ "chrono", "futures", @@ -2637,6 +2637,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", + "tauri-plugin-ibridge-tunnel", "tauri-plugin-opener", "tauri-plugin-process", "tauri-plugin-store", @@ -5737,6 +5738,15 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-ibridge-tunnel" +version = "0.1.0" +dependencies = [ + "serde", + "tauri", + "tauri-plugin", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.5" diff --git a/src-tauri/src/account.rs b/src-tauri/src/account.rs index 8e7fa106..22d2118f 100644 --- a/src-tauri/src/account.rs +++ b/src-tauri/src/account.rs @@ -36,14 +36,7 @@ pub async fn login_new( anisette_server: String, save_credentials: bool, ) -> Result<(), AppError> { - let account = login( - &handle, - &window, - &email, - &password, - anisette_server.clone(), - ) - .await?; + let account = login(&handle, &window, &email, &password, anisette_server.clone()).await?; let mut sideloader_guard = sideloader_state.lock().unwrap(); *sideloader_guard = Some(account); drop(sideloader_guard); @@ -102,14 +95,7 @@ pub async fn login_stored( let password = pass_entry.get_password().map_err(|e| { AppError::KeyringWithMessage("Failed to get credentials".to_string(), e.to_string()) })?; - let account = login( - &handle, - &window, - &email, - &password, - anisette_server.clone(), - ) - .await?; + let account = login(&handle, &window, &email, &password, anisette_server.clone()).await?; let mut sideloader_guard = sideloader_state.lock().unwrap(); *sideloader_guard = Some(account); drop(sideloader_guard); @@ -202,7 +188,7 @@ async fn login( email: &str, password: &str, anisette_server: String, -) -> Result, AppError> { +) -> Result { let tfa_closure = { let window_clone = window.clone(); move |params: TwoFactorCallbackParams| { @@ -251,32 +237,34 @@ async fn login( debug!("Created developer session"); - let max_certs_callback: MaxCertsCallbackBox = Box::new(move |certs: Vec| { - let window_clone = window.clone(); - Box::pin(async move { - let cert_infos: Vec = certs - .iter() - .map(|cert| CertificateInfo { - name: cert.name.clone(), - certificate_id: cert.certificate_id.clone(), - serial_number: cert.serial_number.clone(), - machine_name: cert.machine_name.clone(), - machine_id: cert.machine_id.clone(), - }) - .collect(); - window_clone.emit("max-certs-reached", cert_infos)?; - - let (tx, rx) = std::sync::mpsc::channel::>>(); - let handler_id = window_clone.listen("max-certs-response", move |event| { - let certs = event.payload(); - let certs = serde_json::from_str::>>(certs).unwrap_or(None); - let _ = tx.send(certs); - }); - - let result = rx.recv_timeout(Duration::from_secs(300)); - window_clone.unlisten(handler_id); - Ok(result?) - }) + let max_certs_window = window.clone(); + let max_certs_callback = Box::new(move |certs: &Vec| { + let window_clone = max_certs_window.clone(); + let cert_infos: Vec = certs + .iter() + .map(|cert| CertificateInfo { + name: cert.name.clone(), + certificate_id: cert.certificate_id.clone(), + serial_number: cert.serial_number.clone(), + machine_name: cert.machine_name.clone(), + machine_id: cert.machine_id.clone(), + }) + .collect(); + if let Err(error) = window_clone.emit("max-certs-reached", cert_infos) { + debug!("Failed to show certificate selection: {error}"); + return None; + } + + let (tx, rx) = std::sync::mpsc::channel::>>(); + let handler_id = window_clone.listen("max-certs-response", move |event| { + let certs = + serde_json::from_str::>>(event.payload()).unwrap_or(None); + let _ = tx.send(certs); + }); + + let result = rx.recv_timeout(Duration::from_secs(300)).ok().flatten(); + window_clone.unlisten(handler_id); + result }); // TODO: Team Selection diff --git a/src-tauri/src/mobile_bootstrap.rs b/src-tauri/src/mobile_bootstrap.rs index 88f06d98..97c58f56 100644 --- a/src-tauri/src/mobile_bootstrap.rs +++ b/src-tauri/src/mobile_bootstrap.rs @@ -151,14 +151,15 @@ async fn prepare_mobile_app_ids(sideloader_state: &SideloaderMutex) -> Result<() async fn installed_mobile_bundle_id(device: &DeviceInfo) -> Result { let provider = get_provider(device).await?; - let mut installation_proxy = InstallationProxyClient::connect(&provider) - .await - .map_err(|e| { - AppError::DeviceComsWithMessage( - "Failed to connect to installation proxy".into(), - e.to_string(), - ) - })?; + let mut installation_proxy = + InstallationProxyClient::connect(&provider) + .await + .map_err(|e| { + AppError::DeviceComsWithMessage( + "Failed to connect to installation proxy".into(), + e.to_string(), + ) + })?; let installed_apps = installation_proxy .get_apps(Some("User"), None) @@ -207,8 +208,8 @@ pub async fn install_ibridge_mobile_operation( }; op.start("download")?; - let mobile_ipa_url = std::env::var("IBRIDGE_MOBILE_IPA_URL") - .unwrap_or_else(|_| MOBILE_RELEASE_URL.to_string()); + let mobile_ipa_url = + std::env::var("IBRIDGE_MOBILE_IPA_URL").unwrap_or_else(|_| MOBILE_RELEASE_URL.to_string()); let destination = handle .path() .temp_dir() @@ -217,10 +218,7 @@ pub async fn install_ibridge_mobile_operation( op.fail_if_err("download", download(&mobile_ipa_url, &destination).await)?; op.move_on("download", "install")?; - op.fail_if_err( - "install", - prepare_mobile_app_ids(&sideloader_state).await, - )?; + op.fail_if_err("install", prepare_mobile_app_ids(&sideloader_state).await)?; op.fail_if_err( "install", sideload( @@ -273,13 +271,7 @@ pub async fn install_ibridge_mobile_operation( op.fail_if_err( "bootstrap", - place_file( - secret, - &provider, - bundle_id, - MOBILE_SECRET_PATH.to_string(), - ) - .await, + place_file(secret, &provider, bundle_id, MOBILE_SECRET_PATH.to_string()).await, )?; op.complete("bootstrap")?; diff --git a/src-tauri/src/mobile_runtime.rs b/src-tauri/src/mobile_runtime.rs index 0d103978..d589396b 100644 --- a/src-tauri/src/mobile_runtime.rs +++ b/src-tauri/src/mobile_runtime.rs @@ -1,10 +1,7 @@ use std::{net::IpAddr, path::PathBuf, str::FromStr}; use idevice::{ - IdeviceService, - lockdown::LockdownClient, - pairing_file::PairingFile, - provider::TcpProvider, + IdeviceService, lockdown::LockdownClient, pairing_file::PairingFile, provider::TcpProvider, }; use serde::Serialize; use tauri::{AppHandle, Manager, State, Window}; @@ -34,7 +31,12 @@ fn document_path(handle: &AppHandle, relative: &str) -> Result Result { format!("{}: {e}", pairing_path.display()), ) })?; - let pairing_file = PairingFile::from_bytes(&pairing_bytes) - .map_err(|e| AppError::LockdownPairing("Failed to parse mobile pairing bootstrap".into(), e.to_string()))?; + let pairing_file = PairingFile::from_bytes(&pairing_bytes).map_err(|e| { + AppError::LockdownPairing( + "Failed to parse mobile pairing bootstrap".into(), + e.to_string(), + ) + })?; let addr = IpAddr::from_str(MOBILE_TUNNEL_HOST_IP) .map_err(|e| AppError::Misc(format!("Invalid mobile tunnel address: {e}")))?; @@ -191,8 +197,8 @@ pub async fn mobile_refresh_self_operation( let op = Operation::new("mobile_refresh_self".to_string(), &window); op.start("download")?; - let source = std::env::var("IBRIDGE_MOBILE_IPA_URL") - .unwrap_or_else(|_| MOBILE_RELEASE_URL.to_string()); + let source = + std::env::var("IBRIDGE_MOBILE_IPA_URL").unwrap_or_else(|_| MOBILE_RELEASE_URL.to_string()); let destination = handle .path() .temp_dir() diff --git a/src-tauri/tauri.ios.conf.json b/src-tauri/tauri.ios.conf.json index 90661482..1c34227b 100644 --- a/src-tauri/tauri.ios.conf.json +++ b/src-tauri/tauri.ios.conf.json @@ -6,7 +6,7 @@ "iOS": { "minimumSystemVersion": "17.0", "frameworks": ["NetworkExtension"], - "template": "ios/project.yml.hbs" + "template": "src-tauri/ios/project.yml.hbs" } } } diff --git a/src-tauri/tests/mobile_bootstrap_contract.rs b/src-tauri/tests/mobile_bootstrap_contract.rs index 4ffa9256..4645ca6f 100644 --- a/src-tauri/tests/mobile_bootstrap_contract.rs +++ b/src-tauri/tests/mobile_bootstrap_contract.rs @@ -1,6 +1,6 @@ use iloader_lib::mobile_bootstrap::{ - MobileBootstrapPayload, MOBILE_BOOTSTRAP_PATH, MOBILE_PAIRING_PATH, MOBILE_RELEASE_URL, - MOBILE_SECRET_PATH, is_ibridge_mobile_bundle_id, + MOBILE_BOOTSTRAP_PATH, MOBILE_PAIRING_PATH, MOBILE_RELEASE_URL, MOBILE_SECRET_PATH, + MobileBootstrapPayload, is_ibridge_mobile_bundle_id, }; #[test] @@ -16,7 +16,9 @@ fn mobile_release_contract_points_to_ibridge_asset() { #[test] fn mobile_bundle_match_survives_team_suffix_added_by_isideload() { - assert!(is_ibridge_mobile_bundle_id("com.dannybaanks.ibridge.mobile")); + assert!(is_ibridge_mobile_bundle_id( + "com.dannybaanks.ibridge.mobile" + )); assert!(is_ibridge_mobile_bundle_id( "com.dannybaanks.ibridge.mobile.A1B2C3D4E5" )); diff --git a/src/MobileApp.tsx b/src/MobileApp.tsx index d6c4e7bb..a93783fd 100644 --- a/src/MobileApp.tsx +++ b/src/MobileApp.tsx @@ -101,6 +101,7 @@ export default function MobileApp() { if (event.payload.updateType === "finished") { return { ...old, completed: [...old.completed, event.payload.stepId] }; } + if (event.payload.updateType !== "failed") return old; return { ...old, failed: [ From a5e69e6364c2a69fc1d4842f5a297b5903778407 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Fri, 2 Oct 2026 02:23:12 -0600 Subject: [PATCH 77/77] fix: deduplicate iOS NetworkExtension framework --- src-tauri/ios/project.yml.hbs | 1 - 1 file changed, 1 deletion(-) diff --git a/src-tauri/ios/project.yml.hbs b/src-tauri/ios/project.yml.hbs index 3da3b60c..fe06483e 100644 --- a/src-tauri/ios/project.yml.hbs +++ b/src-tauri/ios/project.yml.hbs @@ -94,7 +94,6 @@ targets: - sdk: QuartzCore.framework - sdk: Security.framework - sdk: UIKit.framework - - sdk: NetworkExtension.framework {{#if ios-frameworks}}{{~#each ios-frameworks}} - sdk: {{this}}.framework{{/each}}{{/if}} - target: IBridgeTunnelProvider