Skip to content

Commit

Permalink
remote: add windows support (doronz88#569)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tomicyo authored and doronz88 committed Jan 25, 2024
1 parent 57aa99e commit 0214ea6
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 20 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,20 @@ with RemoteServiceDiscoveryService((host, port)) as rsd:

## Working with developer tools (iOS >= 17.0)

> **NOTE:** Currently, this is only supported on macOS
> **NOTE:** Currently, this is only supported on macOS & Windows
Starting at iOS 17.0, Apple introduced the new CoreDevice framework to work with iOS devices. This framework relies on
the [RemoteXPC](misc/RemoteXPC.md) protocol. In order to communicate with the developer services you'll be required to
first create [trusted tunnel](misc/RemoteXPC.md#trusted-tunnel) as follows:

```shell
# -- On macOS
sudo python3 -m pymobiledevice3 remote start-tunnel

# -- On windows
# You can either execute using "run as administrator"
# or you will be prompted a UAC dialog to confirm)
python3 -m pymobiledevice3 remote start-tunnel
```

The root permissions are required since this will create a new TUN/TAP device which is a high privilege operation.
Expand Down Expand Up @@ -218,7 +224,13 @@ device is connected.
To start the Tunneld Server, use the following command (with root privileges):

```bash
# -- On macOS
sudo python3 -m pymobiledevice3 remote tunneld

# -- On windows
# You can either execute using "run as administrator"
# or you will be prompted a UAC dialog to confirm)
python3 -m pymobiledevice3 remote tunneld
```

### Using Tunneld
Expand Down
17 changes: 10 additions & 7 deletions pymobiledevice3/cli/cli_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,17 @@ def wait_return():
UDID_ENV_VAR = 'PYMOBILEDEVICE3_UDID'


def sudo_required(func):
def wrapper(*args, **kwargs):
if sys.platform != 'win32' and os.geteuid() != 0:
raise AccessDeniedError()
else:
func(*args, **kwargs)
if sys.platform == 'win32':
from pyuac import main_requires_admin as sudo_required
else:
def sudo_required(func):
def wrapper(*args, **kwargs):
if sys.platform != 'win32' and os.geteuid() != 0:
raise AccessDeniedError()
else:
func(*args, **kwargs)

return wrapper
return wrapper


def prompt_device_list(device_list: List):
Expand Down
8 changes: 6 additions & 2 deletions pymobiledevice3/remote/bonjour.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import dataclasses
import sys
import time
from socket import AF_INET6, inet_ntop
from typing import List
Expand All @@ -7,7 +8,7 @@
from zeroconf import ServiceBrowser, ServiceListener, Zeroconf
from zeroconf.const import _TYPE_AAAA

DEFAULT_BONJOUR_TIMEOUT = 1
DEFAULT_BONJOUR_TIMEOUT = 1 if sys.platform != 'win32' else 2 # On Windows, it takes longer to get the addresses


class RemotedListener(ServiceListener):
Expand Down Expand Up @@ -46,7 +47,10 @@ def query_bonjour(ip: str) -> BonjourQuery:


def get_remoted_addresses(timeout: int = DEFAULT_BONJOUR_TIMEOUT) -> List[str]:
ips = [f'{adapter.ips[0].ip[0]}%{adapter.nice_name}' for adapter in get_adapters() if adapter.ips[0].is_IPv6]
if sys.platform == 'win32':
ips = [f'{adapter.ips[0].ip[0]}%{adapter.ips[0].ip[2]}' for adapter in get_adapters() if adapter.ips[0].is_IPv6]
else:
ips = [f'{adapter.ips[0].ip[0]}%{adapter.nice_name}' for adapter in get_adapters() if adapter.ips[0].is_IPv6]
bonjour_queries = [query_bonjour(adapter) for adapter in ips]
time.sleep(timeout)
addresses = []
Expand Down
37 changes: 29 additions & 8 deletions pymobiledevice3/remote/core_device_tunnel_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
from asyncio import CancelledError, StreamReader, StreamWriter
from collections import namedtuple
from contextlib import asynccontextmanager, suppress
from os import chown, getenv

if sys.platform != 'win32':
from os import chown

from os import getenv
from pathlib import Path
from socket import AF_INET6, create_connection
from ssl import VerifyMode
Expand All @@ -33,7 +37,12 @@
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from opack import dumps
from pytun_pmd3 import TunTapDevice

if sys.platform != 'win32':
from pytun_pmd3 import TunTapDevice
else:
from pywintunx_pmd3 import TunTapDevice, set_logger

from qh3.asyncio import QuicConnectionProtocol
from qh3.asyncio.client import connect as aioquic_connect
from qh3.asyncio.protocol import QuicStreamHandler
Expand Down Expand Up @@ -61,6 +70,12 @@
else:
LOOKBACK_HEADER = b'\x00\x00\x86\xdd'

if sys.platform == 'win32':
def wintun_logger(level: int, timestamp: int, message: str) -> None:
logging.getLogger('wintun').info(message)

set_logger(wintun_logger)

IPV6_HEADER_SIZE = 40
UDP_HEADER_SIZE = 8

Expand Down Expand Up @@ -140,12 +155,18 @@ async def wait_closed(self) -> None:
@asyncio_print_traceback
async def tun_read_task(self) -> None:
read_size = self.tun.mtu + len(LOOKBACK_HEADER)
async with aiofiles.open(self.tun.fileno(), 'rb', opener=lambda path, flags: path, buffering=0) as f:
if sys.platform != 'win32':
async with aiofiles.open(self.tun.fileno(), 'rb', opener=lambda path, flags: path, buffering=0) as f:
while True:
packet = await f.read(read_size)
assert packet.startswith(LOOKBACK_HEADER)
packet = packet[len(LOOKBACK_HEADER):]
await self.send_packet_to_device(packet)
else:
while True:
packet = await f.read(read_size)
assert packet.startswith(LOOKBACK_HEADER)
packet = packet[len(LOOKBACK_HEADER):]
await self.send_packet_to_device(packet)
packet = await asyncio.get_running_loop().run_in_executor(None, self.tun.read)
if packet:
await self.send_packet_to_device(packet)

def start_tunnel(self, address: str, mtu: int) -> None:
self.tun = TunTapDevice()
Expand Down Expand Up @@ -398,7 +419,7 @@ def save_pair_record(self) -> None:
'private_key': self.ed25519_private_key.private_bytes_raw(),
'remote_unlock_host_key': self.remote_unlock_host_key
}))
if getenv('SUDO_UID'):
if getenv('SUDO_UID') and sys.platform != 'win32':
chown(self.pair_record_path, int(getenv('SUDO_UID')), int(getenv('SUDO_GID')))

@property
Expand Down
9 changes: 7 additions & 2 deletions pymobiledevice3/tunneld.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import os
import signal
import sys
import traceback
from contextlib import asynccontextmanager, suppress
from typing import Dict, List, Optional, Tuple
Expand Down Expand Up @@ -45,8 +46,12 @@ def start(self) -> None:
async def monitor_adapters(self):
previous_ips = []
while True:
current_ips = [f'{adapter.ips[0].ip[0]}%{adapter.nice_name}' for adapter in get_adapters() if
adapter.ips[0].is_IPv6]
if sys.platform == 'win32':
current_ips = [f'{adapter.ips[0].ip[0]}%{adapter.ips[0].ip[2]}' for adapter in get_adapters() if
adapter.ips[0].is_IPv6]
else:
current_ips = [f'{adapter.ips[0].ip[0]}%{adapter.nice_name}' for adapter in get_adapters() if
adapter.ips[0].is_IPv6]

added = [ip for ip in current_ips if ip not in previous_ips]
removed = [ip for ip in previous_ips if ip not in current_ips]
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ developer_disk_image>=0.0.2
opack
psutil
pytun-pmd3>=1.0.0 ; platform_system != "Windows"
pywintunx-pmd3>=1.0.2 ; platform_system == "Windows"
pyuac ; platform_system == "Windows"
aiofiles
prompt_toolkit
sslpsk-pmd3>=1.0.2

0 comments on commit 0214ea6

Please sign in to comment.