From 903103bcacb9df5bd10129d5f4c0a5ff83a98ed3 Mon Sep 17 00:00:00 2001 From: doronz88 Date: Mon, 4 Nov 2024 14:25:44 +0200 Subject: [PATCH] remote: use TCP as the default tunneling protocol on python3.13+ iOS 18.2+ removed QUIC support for tunneling, so using TCP as the new default avoids this restriction. In addition, we now verify and raise `QuicProtocolNotSupportedError` if we failed to establish a QUIC connection to the target. --- pymobiledevice3/__main__.py | 6 ++-- pymobiledevice3/cli/remote.py | 9 ++--- pymobiledevice3/exceptions.py | 7 +++- pymobiledevice3/remote/common.py | 4 +++ pymobiledevice3/remote/tunnel_service.py | 43 +++++++++++++----------- pymobiledevice3/tunneld.py | 2 +- 6 files changed, 44 insertions(+), 27 deletions(-) diff --git a/pymobiledevice3/__main__.py b/pymobiledevice3/__main__.py index 4468f574..eaa57105 100644 --- a/pymobiledevice3/__main__.py +++ b/pymobiledevice3/__main__.py @@ -16,8 +16,8 @@ ConnectionFailedToUsbmuxdError, DeprecationError, DeveloperModeError, DeveloperModeIsNotEnabledError, \ DeviceHasPasscodeSetError, DeviceNotFoundError, FeatureNotSupportedError, InternalError, InvalidServiceError, \ MessageNotSupportedError, MissingValueError, NoDeviceConnectedError, NotEnoughDiskSpaceError, NotPairedError, \ - OSNotSupportedError, PairingDialogResponsePendingError, PasswordRequiredError, RSDRequiredError, \ - SetProhibitedError, TunneldConnectionError, UserDeniedPairingError + OSNotSupportedError, PairingDialogResponsePendingError, PasswordRequiredError, QuicProtocolNotSupportedError, \ + RSDRequiredError, SetProhibitedError, TunneldConnectionError, UserDeniedPairingError from pymobiledevice3.osu.os_utils import get_os_utils coloredlogs.install(level=logging.INFO) @@ -244,6 +244,8 @@ def main() -> None: logger.error( f'Missing implementation of `{e.feature}` on `{e.os_name}`. To add support, consider contributing at ' f'https://github.com/doronz88/pymobiledevice3.') + except QuicProtocolNotSupportedError as e: + logger.error(str(e)) if __name__ == '__main__': diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index bc683cad..b7d42f14 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -68,7 +68,8 @@ def remote_cli() -> None: @click.option('--port', type=click.INT, default=TUNNELD_DEFAULT_ADDRESS[1]) @click.option('-d', '--daemonize', is_flag=True) @click.option('-p', '--protocol', type=click.Choice([e.value for e in TunnelProtocol]), - default=TunnelProtocol.QUIC.value) + help='Transport protocol. If python version >= 3.13 will default to TCP. Otherwise will default to QUIC', + default=TunnelProtocol.DEFAULT.value) @click.option('--usb/--no-usb', default=True, help='Enable usb monitoring') @click.option('--wifi/--no-wifi', default=True, help='Enable wifi monitoring') @click.option('--usbmux/--no-usbmux', default=True, help='Enable usbmux monitoring') @@ -112,7 +113,7 @@ def rsd_info(service_provider: RemoteServiceDiscoveryService): async def tunnel_task( service, secrets: Optional[TextIO] = None, script_mode: bool = False, - max_idle_timeout: float = MAX_IDLE_TIMEOUT, protocol: TunnelProtocol = TunnelProtocol.QUIC) -> None: + max_idle_timeout: float = MAX_IDLE_TIMEOUT, protocol: TunnelProtocol = TunnelProtocol.DEFAULT) -> None: async with start_tunnel( service, secrets=secrets, max_idle_timeout=max_idle_timeout, protocol=protocol) as tunnel_result: logger.info('tunnel created') @@ -152,7 +153,7 @@ async def tunnel_task( async def start_tunnel_task( connection_type: ConnectionType, secrets: TextIO, udid: Optional[str] = None, script_mode: bool = False, - max_idle_timeout: float = MAX_IDLE_TIMEOUT, protocol: TunnelProtocol = TunnelProtocol.QUIC) -> None: + max_idle_timeout: float = MAX_IDLE_TIMEOUT, protocol: TunnelProtocol = TunnelProtocol.DEFAULT) -> None: if start_tunnel is None: raise NotImplementedError('failed to start the tunnel on your platform') get_tunnel_services = { @@ -185,7 +186,7 @@ async def start_tunnel_task( help='Maximum QUIC idle time (ping interval)') @click.option('-p', '--protocol', type=click.Choice([e.value for e in TunnelProtocol], case_sensitive=False), - default=TunnelProtocol.QUIC.value) + default=TunnelProtocol.DEFAULT.value) @sudo_required def cli_start_tunnel( connection_type: ConnectionType, udid: Optional[str], secrets: TextIO, script_mode: bool, diff --git a/pymobiledevice3/exceptions.py b/pymobiledevice3/exceptions.py index 99e9c264..298a9b7b 100644 --- a/pymobiledevice3/exceptions.py +++ b/pymobiledevice3/exceptions.py @@ -14,7 +14,7 @@ 'LaunchingApplicationError', 'BadCommandError', 'BadDevError', 'ConnectionFailedError', 'CoreDeviceError', 'AccessDeniedError', 'RSDRequiredError', 'SysdiagnoseTimeoutError', 'GetProhibitedError', 'FeatureNotSupportedError', 'OSNotSupportedError', 'DeprecationError', 'NotEnoughDiskSpaceError', - 'CloudConfigurationAlreadyPresentError' + 'CloudConfigurationAlreadyPresentError', 'QuicProtocolNotSupportedError', ] from typing import Optional @@ -390,3 +390,8 @@ class FeatureNotSupportedError(SupportError): def __init__(self, os_name, feature): super().__init__(os_name) self.feature = feature + + +class QuicProtocolNotSupportedError(PyMobileDevice3Exception): + """ QUIC tunnel support was removed on iOS 18.2+ """ + pass diff --git a/pymobiledevice3/remote/common.py b/pymobiledevice3/remote/common.py index 34e75ab4..80e8e686 100644 --- a/pymobiledevice3/remote/common.py +++ b/pymobiledevice3/remote/common.py @@ -1,3 +1,4 @@ +import sys from enum import Enum @@ -9,3 +10,6 @@ class ConnectionType(Enum): class TunnelProtocol(Enum): TCP = 'tcp' QUIC = 'quic' + + # TODO: make only TCP the default once 3.12 becomes deprecated + DEFAULT = TCP if sys.version_info >= (3, 13) else QUIC diff --git a/pymobiledevice3/remote/tunnel_service.py b/pymobiledevice3/remote/tunnel_service.py index bbd47090..1fc7777d 100644 --- a/pymobiledevice3/remote/tunnel_service.py +++ b/pymobiledevice3/remote/tunnel_service.py @@ -56,7 +56,8 @@ from pymobiledevice3.bonjour import DEFAULT_BONJOUR_TIMEOUT, browse_remotepairing from pymobiledevice3.ca import make_cert -from pymobiledevice3.exceptions import PairingError, PyMobileDevice3Exception, UserDeniedPairingError +from pymobiledevice3.exceptions import PairingError, PyMobileDevice3Exception, QuicProtocolNotSupportedError, \ + UserDeniedPairingError from pymobiledevice3.pair_records import PAIRING_RECORD_EXT, create_pairing_records_cache_folder, generate_host_id, \ get_remote_pairing_record_filename, iter_remote_paired_identifiers from pymobiledevice3.remote.common import TunnelProtocol @@ -417,24 +418,28 @@ async def start_quic_tunnel( port = parameters['port'] self.logger.debug(f'Connecting to {host}:{port}') - async with aioquic_connect( - host, - port, - configuration=configuration, - create_protocol=RemotePairingQuicTunnel, - ) as client: - self.logger.debug('quic connected') - client = cast(RemotePairingQuicTunnel, client) - await client.wait_connected() - handshake_response = await client.request_tunnel_establish() - client.start_tunnel(handshake_response['clientParameters']['address'], - handshake_response['clientParameters']['mtu']) - try: - yield TunnelResult( - client.tun.name, handshake_response['serverAddress'], handshake_response['serverRSDPort'], - TunnelProtocol.QUIC, client) - finally: - await client.stop_tunnel() + try: + async with aioquic_connect( + host, + port, + configuration=configuration, + create_protocol=RemotePairingQuicTunnel, + ) as client: + self.logger.debug('quic connected') + client = cast(RemotePairingQuicTunnel, client) + await client.wait_connected() + handshake_response = await client.request_tunnel_establish() + client.start_tunnel(handshake_response['clientParameters']['address'], + handshake_response['clientParameters']['mtu']) + try: + yield TunnelResult( + client.tun.name, handshake_response['serverAddress'], handshake_response['serverRSDPort'], + TunnelProtocol.QUIC, client) + finally: + await client.stop_tunnel() + except ConnectionError: + raise QuicProtocolNotSupportedError( + 'iOS 18.2+ removed QUIC protocol support. Use TCP instead (requires python3.13+)') @asynccontextmanager async def start_tcp_tunnel(self) -> AsyncGenerator[TunnelResult, None]: diff --git a/pymobiledevice3/tunneld.py b/pymobiledevice3/tunneld.py index d3e43156..659e26f9 100644 --- a/pymobiledevice3/tunneld.py +++ b/pymobiledevice3/tunneld.py @@ -53,7 +53,7 @@ class TunnelTask: class TunneldCore: - def __init__(self, protocol: TunnelProtocol = TunnelProtocol.QUIC, wifi_monitor: bool = True, + def __init__(self, protocol: TunnelProtocol = TunnelProtocol.DEFAULT, wifi_monitor: bool = True, usb_monitor: bool = True, usbmux_monitor: bool = True, mobdev2_monitor: bool = True) -> None: self.protocol = protocol self.tasks: list[asyncio.Task] = []