Skip to main content

Federate iPhones across Macs by registering one tunneld as an upstream of another.

Project description

tunneldup

Federate iPhones connected to one Mac into another Mac's pymobiledevice3 tunneld, over WireGuard.

You plug an iPhone into Mac A. Mac B runs one command and now pymobiledevice3 lockdown info --tunnel <UDID> on B works against A's iPhone, exactly as if the device were attached locally. The trick is a small REST extension to pymobiledevice3's tunneld (an /upstream registry) and a thin WireGuard layer so B can actually route to the device's tunnel address.

tunneldup does not rewrite or replace tunneld — it composes with the tunneld you already have running. A's tunneld serves A's iPhones; B's tunneld serves B's iPhones plus A's (after federation). You can chain more machines (C, D, …) the same way.

Architecture

  ┌─ Mac A (iPhones plugged in) ───────────┐         ┌─ Mac B (no iPhones) ────────────────┐
  │ pymobiledevice3 remote tunneld         │         │  pymobiledevice3 remote tunneld     │
  │   ← serves A's devices on :49151       │         │    ← upstream registry includes     │
  │                                        │         │      http://A:9246/tunneld          │
  │ tunneldup host --web                   │ <─WG──> │  tunneldup add A                    │
  │   ← WireGuard server :51820            │         │    ← installs WG client + POSTs     │
  │   ← web UI :9246 (config, devices,     │         │      /upstream to local tunneld     │
  │     /tunneld JSON passthrough)         │         │                                     │
  └────────────────────────────────────────┘         └─────────────────────────────────────┘
                                                       ↓
                                  pymobiledevice3 lockdown info --tunnel <A's UDID>
                                  ↳ tunneld GET / merges local + every /upstream
                                  ↳ TCP connect to A's iPhone tunnel address routes over WG

The federation is REST-only: POST /upstream {url} registers, DELETE /upstream {url} removes, GET / on tunneld fetches every registered upstream in parallel and merges the device entries by UDID. The WireGuard tunnel only exists so B can route to the per-device ULA addresses A's tunneld hands out (fd…::1/64 per session); the federation handshake itself is plain HTTP.

Requirements

  • macOS, Python ≥ 3.10.
  • brew install wireguard-tools wireguard-go on every Mac that will run tunneldup host or tunneldup add.
  • pymobiledevice3 ≥ the version that includes the /upstream REST endpoints (the fork at github.com/doronz88/pymobiledevice3, branch feature/tunneld-upstreams).

Install

# either:
uv tool install tunneldup --from git+https://github.com/doronz88/tunneldup.git
# or:
pip install git+https://github.com/doronz88/tunneldup.git

Usage

On the Mac with iPhones plugged in (A)

In separate shells:

sudo pymobiledevice3 remote tunneld          # your normal tunneld
sudo tunneldup host --web                    # WG server + tunneld bridge + web UI

tunneldup host autodetects the LAN endpoint (192.168.x.x if present), falling back to the public WAN IP via ifconfig.me only when no private IP is available. To force a specific endpoint, pass --endpoint.

The web UI is at http://<A's-IP>:9246. It serves

path what
/ HTML dashboard — device cards, per-device action buttons, command runner
/devices flat JSON device list (UDID, tunnel address, transport, interface)
/tunneld verbatim pass-through of A's tunneld GET / — this is the URL another machine registers as an upstream
/config A's WireGuard client config (used by tunneldup add)
POST /exec runs pymobiledevice3 <args> on A, returns stdout/stderr/exit code

On a remote Mac (B)

sudo pymobiledevice3 remote tunneld          # B's normal tunneld
sudo tunneldup add 192.168.0.175             # connect to A

tunneldup add does exactly four things, then holds until Ctrl-C:

  1. GET http://A:9246/devices to populate the interactive picker.
  2. GET http://A:9246/config and brings up WireGuard on B.
  3. POST http://127.0.0.1:49151/upstream {"url":"http://A:9246/tunneld"} against B's local tunneld.
  4. Prints pymobiledevice3 lockdown info --tunnel <UDID> snippets for the device you picked.

On Ctrl-C / SIGTERM, the cleanup finally block runs: DELETE /upstream then wg-quick down. No stale upstream entry, no leaked WG interface.

After that, on B:

pymobiledevice3 lockdown info --tunnel <A's-UDID>
pymobiledevice3 apps list --tunnel <A's-UDID>
pymobiledevice3 syslog live --tunnel <A's-UDID>

Manual upstream management (no WireGuard, REST only)

If A and B already share a network and A's tunneld is bound to a routable address, you don't need WireGuard. Just register manually:

tunneldup upstreams                              # list registered upstreams
tunneldup remove http://A:9246/tunneld           # deregister one

add is the "give me the full lifecycle" command; upstreams / remove are the bare REST primitives.

CLI reference

command what
tunneldup host [--web] [--endpoint <ip>] bring up WG server + tunneld bridge (+ web UI); regenerates client.conf
tunneldup client <client.conf> bring up WG using a manually-shipped config (alternative to add)
tunneldup add <host>[:<port>] one-shot: fetch conf, WG up, register upstream, picker, hold until Ctrl-C
tunneldup upstreams list registered upstream URLs
tunneldup remove <host> deregister an upstream
tunneldup web run only the web UI (use host --web if you want WG too)
tunneldup devices list iPhones reachable on this Mac via USB (requires sudo)
tunneldup status show WG state + tunneld reachability
tunneldup config print the host-side client config
tunneldup down tear down WG on either side

All commands respect TUNNELDUP_DIR (default ~/.config/tunneldup) for configs and keys, and TUNNELDUP_TUNNELD_URL to point the web UI at a non-default tunneld.

Configuration

file purpose
~/.config/tunneldup/meta.json persisted WireGuard keypair for the host. Do not commit this.
~/.config/tunneldup/server.conf WireGuard server config (regenerated each tunneldup host run)
~/.config/tunneldup/client.conf WireGuard client config served at GET /config

WireGuard networking:

IPv4 IPv6
WG net 10.42.0.0/24 fdaa:1234::/64
server 10.42.0.1 fdaa:1234::1
client 10.42.0.2 fdaa:1234::2
listen port UDP 51820
client AllowedIPs 10.42.0.0/24, fdaa:1234::/64, fd00::/8

fd00::/8 is in AllowedIPs so the client can route to per-device iPhone tunnel ULAs (fd97:...::1/64 etc., generated fresh by pymobiledevice3 per session). It's wide on purpose: per-session prefixes can't be predicted in advance.

Development

uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
pre-commit install                # one-time per checkout
pytest                            # 24 tests

The repo uses ruff for lint + format with a target-version = "py310" config matching pymobile's. pre-commit-config.yaml runs ruff-check and ruff-format on every commit.

Tests stand up real in-process TunneldRunner instances on random ports and exercise the actual REST surface — the federation + upstream-cleanup contract is covered end-to-end, not just at the mock layer.

Security notes

  • tunneldup host exposes a WireGuard endpoint. The keypair is generated locally and never leaves the machine; sharing the client config with a peer is what grants them access.
  • The web UI binds to 0.0.0.0:9246 by default, so it's reachable from your LAN. The POST /exec endpoint runs pymobiledevice3 <args> with the privileges of the process serving the UI (sudo, if you started it with sudo). Don't run tunneldup host --web on a network you don't trust.
  • The web UI has no built-in authentication. If you need it accessible only over WG, pass --host 10.42.0.1 to bind only to the WG interface.

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tunneldup-0.0.2.tar.gz (33.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tunneldup-0.0.2-py3-none-any.whl (26.0 kB view details)

Uploaded Python 3

File details

Details for the file tunneldup-0.0.2.tar.gz.

File metadata

  • Download URL: tunneldup-0.0.2.tar.gz
  • Upload date:
  • Size: 33.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tunneldup-0.0.2.tar.gz
Algorithm Hash digest
SHA256 547b8c70e73b949ebde3761f3dc53ed9b20bd4781fc752084f9fe6a04bc03ec6
MD5 7bbb62b73f9e1f58121ebb9c7aa92a73
BLAKE2b-256 96bff5f640fd81c145c54b1777a8a46f81c4acb515b6dcb80b0f05ee903f2fbd

See more details on using hashes here.

Provenance

The following attestation bundles were made for tunneldup-0.0.2.tar.gz:

Publisher: python-publish.yml on doronz88/tunneldup

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tunneldup-0.0.2-py3-none-any.whl.

File metadata

  • Download URL: tunneldup-0.0.2-py3-none-any.whl
  • Upload date:
  • Size: 26.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tunneldup-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9856d4d9322726b99b1d98609883923f27662d69ce01496e8c97f1d45629bda7
MD5 df1ec52ebaa150f8bc22650c115a0fd4
BLAKE2b-256 0c7d5fc3acf05b2719396f6210c44e4617e792432f8948366d50fe58924c75c7

See more details on using hashes here.

Provenance

The following attestation bundles were made for tunneldup-0.0.2-py3-none-any.whl:

Publisher: python-publish.yml on doronz88/tunneldup

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page