diff --git a/README.md b/README.md index c88465a2e..5abb5d315 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/pymobiledevice3/cli/cli_common.py b/pymobiledevice3/cli/cli_common.py index 6c488d988..8ff7ae9f3 100644 --- a/pymobiledevice3/cli/cli_common.py +++ b/pymobiledevice3/cli/cli_common.py @@ -102,14 +102,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): diff --git a/pymobiledevice3/remote/bonjour.py b/pymobiledevice3/remote/bonjour.py index 349673d08..82b5749a2 100644 --- a/pymobiledevice3/remote/bonjour.py +++ b/pymobiledevice3/remote/bonjour.py @@ -1,4 +1,5 @@ import dataclasses +import sys import time from socket import AF_INET6, inet_ntop from typing import List @@ -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): @@ -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 = [] diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index 1b65fc801..cbe8cd32b 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -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 @@ -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 @@ -65,6 +74,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 @@ -144,12 +159,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() @@ -402,7 +423,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 diff --git a/pymobiledevice3/tunneld.py b/pymobiledevice3/tunneld.py index 1f57357e2..77b283822 100644 --- a/pymobiledevice3/tunneld.py +++ b/pymobiledevice3/tunneld.py @@ -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 @@ -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] diff --git a/requirements.txt b/requirements.txt index f87dd9056..790861f2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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