Skip to content

Commit 6f7e157

Browse files
committed
cli,remote,service_connection: Implement userspace TUN RSD provider
1 parent 5cd5f08 commit 6f7e157

File tree

4 files changed

+52
-9
lines changed

4 files changed

+52
-9
lines changed

pymobiledevice3/cli/cli_common.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ def wrap_callback_calling(**kwargs: dict) -> None:
149149
lockdown_service_provider = kwargs.pop('lockdown_service_provider', None)
150150
rsd_service_provider_manually = kwargs.pop('rsd_service_provider_manually', None)
151151
rsd_service_provider_using_tunneld = kwargs.pop('rsd_service_provider_using_tunneld', None)
152+
del kwargs['rsd_use_userspace_tun']
152153
if lockdown_service_provider is not None:
153154
service_provider = lockdown_service_provider
154155
if rsd_service_provider_manually is not None:
@@ -224,23 +225,36 @@ def udid(self, ctx, param: str, value: str) -> Optional[LockdownClient]:
224225
class RSDCommand(BaseServiceProviderCommand):
225226
def __init__(self, *args, **kwargs):
226227
super().__init__(*args, **kwargs)
228+
self.userspace_tun_address = None
227229
self.params[:0] = [
230+
RSDOption(('rsd_use_userspace_tun', '--userspace-tun'), type=(str, int),
231+
callback=self.userspace_tun,
232+
mutually_exclusive=['rsd_service_provider_using_tunneld'],
233+
help='\b\nhostname and port number of userspace tunnel server '
234+
'(as provided by a `start-tunnel` subcommand).'),
228235
RSDOption(('rsd_service_provider_manually', '--rsd'), type=(str, int), callback=self.rsd,
229236
mutually_exclusive=['rsd_service_provider_using_tunneld'],
230237
help='\b\n'
231238
'RSD hostname and port number (as provided by a `start-tunnel` subcommand).'),
232239
RSDOption(('rsd_service_provider_using_tunneld', '--tunnel'), callback=self.tunneld,
233-
mutually_exclusive=['rsd_service_provider_manually'], envvar=TUNNEL_ENV_VAR,
240+
mutually_exclusive=[
241+
'rsd_service_provider_manually',
242+
'rsd_use_userspace_tun'
243+
],
244+
envvar=TUNNEL_ENV_VAR,
234245
help='\b\n'
235246
'Either an empty string to force tunneld device selection, or a UDID of a tunneld '
236247
'discovered device.\n'
237248
'The string may be suffixed with :PORT in case tunneld is not serving at the default port.\n'
238249
f'This option may also be transferred as an environment variable: {TUNNEL_ENV_VAR}')
239250
]
240251

252+
def userspace_tun(self, ctx, param: str, value: Optional[tuple[str, int]]) -> None:
253+
self.userspace_tun_address = value
254+
241255
def rsd(self, ctx, param: str, value: Optional[tuple[str, int]]) -> Optional[RemoteServiceDiscoveryService]:
242256
if value is not None:
243-
rsd = RemoteServiceDiscoveryService(value)
257+
rsd = RemoteServiceDiscoveryService(value, userspace_tun_address=self.userspace_tun_address)
244258
asyncio.run(rsd.connect(), debug=True)
245259
self.service_provider = rsd
246260
return self.service_provider

pymobiledevice3/remote/remote_service_discovery.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@ class RSDDevice:
2828

2929

3030
class RemoteServiceDiscoveryService(LockdownServiceProvider):
31-
def __init__(self, address: tuple[str, int], name: Optional[str] = None) -> None:
31+
def __init__(
32+
self,
33+
address: tuple[str, int],
34+
userspace_tun_address: "tuple[str, int] | None" = None,
35+
name: Optional[str] = None
36+
) -> None:
3237
super().__init__()
3338
self.name = name
34-
self.service = RemoteXPCConnection(address)
39+
self.service = RemoteXPCConnection(address, userspace_tun_address)
40+
self.userspace_tun_address = userspace_tun_address
3541
self.peer_info: Optional[dict] = None
3642
self.lockdown: Optional[LockdownClient] = None
3743
self.all_values: Optional[dict] = None
@@ -75,7 +81,11 @@ def get_value(self, domain: Optional[str] = None, key: Optional[str] = None) ->
7581
return self.lockdown.get_value(domain, key)
7682

7783
def start_lockdown_service_without_checkin(self, name: str) -> ServiceConnection:
78-
return ServiceConnection.create_using_tcp(self.service.address[0], self.get_service_port(name))
84+
return ServiceConnection.create_using_tcp(
85+
self.service.address[0],
86+
self.get_service_port(name),
87+
userspace_tun_address=self.userspace_tun_address
88+
)
7989

8090
def start_lockdown_service(self, name: str, include_escrow_bag: bool = False) -> ServiceConnection:
8191
service = self.start_lockdown_service_without_checkin(name)
@@ -112,7 +122,10 @@ def start_lockdown_developer_service(self, name, include_escrow_bag: bool = Fals
112122
raise
113123

114124
def start_remote_service(self, name: str) -> RemoteXPCConnection:
115-
service = RemoteXPCConnection((self.service.address[0], self.get_service_port(name)))
125+
service = RemoteXPCConnection(
126+
(self.service.address[0], self.get_service_port(name)),
127+
self.userspace_tun_address
128+
)
116129
return service
117130

118131
def start_service(self, name: str) -> Union[RemoteXPCConnection, ServiceConnection]:

pymobiledevice3/remote/remotexpc.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import socket
23
import sys
34
from asyncio import IncompleteReadError
45
from typing import AsyncIterable, Optional
@@ -37,9 +38,10 @@
3738

3839

3940
class RemoteXPCConnection:
40-
def __init__(self, address: tuple[str, int]):
41+
def __init__(self, address: tuple[str, int], userspace_tun_address: "tuple[str, int] | None" = None):
4142
self._previous_frame_data = b''
4243
self.address = address
44+
self.userspace_tun_address = userspace_tun_address
4345
self.next_message_id: dict[int, int] = {ROOT_CHANNEL: 0, REPLY_CHANNEL: 0}
4446
self.peer_info = None
4547
self._reader: Optional[asyncio.StreamReader] = None
@@ -53,7 +55,15 @@ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
5355
await self.close()
5456

5557
async def connect(self) -> None:
56-
self._reader, self._writer = await asyncio.open_connection(self.address[0], self.address[1])
58+
if self.userspace_tun_address:
59+
self._reader, self._writer = await asyncio.open_connection(*self.userspace_tun_address)
60+
self._writer.write(
61+
socket.inet_pton(socket.AF_INET6, self.address[0])
62+
+ self.address[1].to_bytes(4, "little")
63+
)
64+
await self._writer.drain()
65+
else:
66+
self._reader, self._writer = await asyncio.open_connection(self.address[0], self.address[1])
5767
try:
5868
await self._do_handshake()
5969
except Exception: # noqa: E722

pymobiledevice3/service_connection.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,23 @@ def __init__(self, sock: socket.socket, mux_device: MuxDevice = None):
116116

117117
@staticmethod
118118
def create_using_tcp(hostname: str, port: int, keep_alive: bool = True,
119+
userspace_tun_address: "None | tuple[str, int]" = None,
119120
create_connection_timeout: int = DEFAULT_TIMEOUT) -> 'ServiceConnection':
120121
"""
121122
Create a ServiceConnection using a TCP connection.
122123
123124
:param hostname: The hostname of the server to connect to.
124125
:param port: The port to connect to.
125126
:param keep_alive: Whether to enable TCP keep-alive.
127+
:param userspace_tun_address: Address of userspace TUN server.
126128
:param create_connection_timeout: The timeout for creating the connection.
127129
:return: A ServiceConnection object.
128130
"""
129-
sock = socket.create_connection((hostname, port), timeout=create_connection_timeout)
131+
if userspace_tun_address:
132+
sock = socket.create_connection(userspace_tun_address, timeout=create_connection_timeout)
133+
sock.sendall(socket.inet_pton(socket.AF_INET6, hostname) + port.to_bytes(4, "little"))
134+
else:
135+
sock = socket.create_connection((hostname, port), timeout=create_connection_timeout)
130136
sock.settimeout(None)
131137
if keep_alive:
132138
OSUTIL.set_keepalive(sock)

0 commit comments

Comments
 (0)