Skip to content

Commit ac699e7

Browse files
Tomicyodoronz88
authored andcommitted
remote: add windows support (doronz88#569)
1 parent cbbb04d commit ac699e7

9 files changed

+83
-23
lines changed

README.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,19 @@ with RemoteServiceDiscoveryService((host, port)) as rsd:
176176

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

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

185185
```shell
186+
# -- On macOS
186187
sudo python3 -m pymobiledevice3 remote start-tunnel
188+
189+
# -- On windows
190+
# Use a "run as administrator" shell
191+
python3 -m pymobiledevice3 remote start-tunnel
187192
```
188193

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

220225
```bash
226+
# -- On macOS
221227
sudo python3 -m pymobiledevice3 remote tunneld
228+
229+
# -- On windows
230+
# Use a "run as administrator" shell
231+
python3 -m pymobiledevice3 remote tunneld
222232
```
223233

224234
### Using Tunneld

pymobiledevice3/__main__.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import sys
23
import traceback
34

45
import click
@@ -130,7 +131,10 @@ def main() -> None:
130131
except PasswordRequiredError:
131132
logger.error('Device is password protected. Please unlock and retry')
132133
except AccessDeniedError:
133-
logger.error('This command requires root privileges. Consider retrying with "sudo".')
134+
if sys.platform == 'win32':
135+
logger.error('This command requires admin privileges. Consider retrying with "run-as administrator".')
136+
else:
137+
logger.error('This command requires root privileges. Consider retrying with "sudo".')
134138
except BrokenPipeError:
135139
traceback.print_exc()
136140
except TunneldConnectionError:

pymobiledevice3/cli/cli_common.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,29 @@ def wait_return() -> None:
105105
UDID_ENV_VAR = 'PYMOBILEDEVICE3_UDID'
106106

107107

108+
def is_admin_user() -> bool:
109+
""" Check if the current OS user is an Administrator or root.
110+
111+
See: https://github.com/Preston-Landers/pyuac/blob/master/pyuac/admin.py
112+
113+
:return: True if the current user is an 'Administrator', otherwise False.
114+
"""
115+
if os.name == 'nt':
116+
import win32security
117+
118+
try:
119+
admin_sid = win32security.CreateWellKnownSid(win32security.WinBuiltinAdministratorsSid, None)
120+
return win32security.CheckTokenMembership(None, admin_sid)
121+
except Exception:
122+
return False
123+
else:
124+
# Check for root on Posix
125+
return os.getuid() == 0
126+
127+
108128
def sudo_required(func):
109129
def wrapper(*args, **kwargs):
110-
if sys.platform != 'win32' and os.geteuid() != 0:
130+
if not is_admin_user():
111131
raise AccessDeniedError()
112132
else:
113133
func(*args, **kwargs)

pymobiledevice3/cli/remote.py

+11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
logger = logging.getLogger(__name__)
2222

2323

24+
def install_driver_if_required() -> None:
25+
if sys.platform == 'win32':
26+
import pywintunx_pmd3
27+
pywintunx_pmd3.install_wetest_driver()
28+
29+
2430
def get_device_list() -> List[RemoteServiceDiscoveryService]:
2531
result = []
2632
with stop_remoted():
@@ -57,6 +63,7 @@ def cli_tunneld(host: str, port: int, daemonize: bool, protocol: str):
5763
""" Start Tunneld service for remote tunneling """
5864
if not verify_tunnel_imports():
5965
return
66+
install_driver_if_required()
6067
protocol = TunnelProtocol(protocol)
6168
tunneld_runner = partial(TunneldRunner.create, host, port, protocol)
6269
if daemonize:
@@ -77,6 +84,7 @@ def cli_tunneld(host: str, port: int, daemonize: bool, protocol: str):
7784
@click.option('--color/--no-color', default=True)
7885
def browse(color: bool):
7986
""" browse devices using bonjour """
87+
install_driver_if_required()
8088
devices = []
8189
for rsd in get_device_list():
8290
devices.append({'address': rsd.service.address[0],
@@ -91,6 +99,7 @@ def browse(color: bool):
9199
@click.option('--color/--no-color', default=True)
92100
def rsd_info(service_provider: RemoteServiceDiscoveryService, color: bool):
93101
""" show info extracted from RSD peer """
102+
install_driver_if_required()
94103
print_json(service_provider.peer_info, colored=color)
95104

96105

@@ -168,6 +177,7 @@ def select_device(udid: str) -> RemoteServiceDiscoveryService:
168177
@sudo_required
169178
def cli_start_tunnel(udid: str, secrets: TextIO, script_mode: bool, max_idle_timeout: float, protocol: str):
170179
""" start quic tunnel """
180+
install_driver_if_required()
171181
protocol = TunnelProtocol(protocol)
172182
if not verify_tunnel_imports():
173183
return
@@ -190,5 +200,6 @@ def cli_delete_pair(udid: str):
190200
@click.argument('service_name')
191201
def cli_service(service_provider: RemoteServiceDiscoveryService, service_name: str):
192202
""" start an ipython shell for interacting with given service """
203+
install_driver_if_required()
193204
with service_provider.start_remote_service(service_name) as service:
194205
service.shell()

pymobiledevice3/remote/bonjour.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import dataclasses
2+
import sys
23
import time
34
from socket import AF_INET6, inet_ntop
45
from typing import List
@@ -7,7 +8,7 @@
78
from zeroconf import ServiceBrowser, ServiceListener, Zeroconf
89
from zeroconf.const import _TYPE_AAAA
910

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

1213

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

4748

4849
def get_remoted_addresses(timeout: int = DEFAULT_BONJOUR_TIMEOUT) -> List[str]:
49-
ips = [f'{adapter.ips[0].ip[0]}%{adapter.nice_name}' for adapter in get_adapters() if adapter.ips[0].is_IPv6]
50+
if sys.platform == 'win32':
51+
ips = [f'{adapter.ips[0].ip[0]}%{adapter.ips[0].ip[2]}' for adapter in get_adapters() if adapter.ips[0].is_IPv6]
52+
else:
53+
ips = [f'{adapter.ips[0].ip[0]}%{adapter.nice_name}' for adapter in get_adapters() if adapter.ips[0].is_IPv6]
5054
bonjour_queries = [query_bonjour(adapter) for adapter in ips]
5155
time.sleep(timeout)
5256
addresses = []

pymobiledevice3/remote/core_device_tunnel_service.py

+20-7
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
if sys.platform != 'win32':
4242
from pytun_pmd3 import TunTapDevice
4343
else:
44-
TunTapDevice = None
44+
from pywintunx_pmd3 import TunTapDevice, set_logger
45+
4546
from qh3.asyncio import QuicConnectionProtocol
4647
from qh3.asyncio.client import connect as aioquic_connect
4748
from qh3.asyncio.protocol import QuicStreamHandler
@@ -73,6 +74,12 @@
7374
else:
7475
LOOKBACK_HEADER = b'\x00\x00\x86\xdd'
7576

77+
if sys.platform == 'win32':
78+
def wintun_logger(level: int, timestamp: int, message: str) -> None:
79+
logging.getLogger('wintun').info(message)
80+
81+
set_logger(wintun_logger)
82+
7683
IPV6_HEADER_SIZE = 40
7784
UDP_HEADER_SIZE = 8
7885

@@ -152,12 +159,18 @@ async def wait_closed(self) -> None:
152159
@asyncio_print_traceback
153160
async def tun_read_task(self) -> None:
154161
read_size = self.tun.mtu + len(LOOKBACK_HEADER)
155-
async with aiofiles.open(self.tun.fileno(), 'rb', opener=lambda path, flags: path, buffering=0) as f:
162+
if sys.platform != 'win32':
163+
async with aiofiles.open(self.tun.fileno(), 'rb', opener=lambda path, flags: path, buffering=0) as f:
164+
while True:
165+
packet = await f.read(read_size)
166+
assert packet.startswith(LOOKBACK_HEADER)
167+
packet = packet[len(LOOKBACK_HEADER):]
168+
await self.send_packet_to_device(packet)
169+
else:
156170
while True:
157-
packet = await f.read(read_size)
158-
assert packet.startswith(LOOKBACK_HEADER)
159-
packet = packet[len(LOOKBACK_HEADER):]
160-
await self.send_packet_to_device(packet)
171+
packet = await asyncio.get_running_loop().run_in_executor(None, self.tun.read)
172+
if packet:
173+
await self.send_packet_to_device(packet)
161174

162175
def start_tunnel(self, address: str, mtu: int) -> None:
163176
self.tun = TunTapDevice()
@@ -410,7 +423,7 @@ def save_pair_record(self) -> None:
410423
'private_key': self.ed25519_private_key.private_bytes_raw(),
411424
'remote_unlock_host_key': self.remote_unlock_host_key
412425
}))
413-
if getenv('SUDO_UID'):
426+
if getenv('SUDO_UID') and sys.platform != 'win32':
414427
chown(self.pair_record_path, int(getenv('SUDO_UID')), int(getenv('SUDO_GID')))
415428

416429
@property
+1-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
import sys
32

43
logger = logging.getLogger(__name__)
54

@@ -11,11 +10,7 @@
1110
start_tunnel = None
1211
MAX_IDLE_TIMEOUT = None
1312

14-
WIN32_IMPORT_ERROR = """Windows platforms are not yet supported for this command. For more info:
15-
https://github.com/doronz88/pymobiledevice3/issues/569
16-
"""
17-
18-
GENERAL_IMPORT_ERROR = """Failed to import `start_tunnel`. Possible reasons are:
13+
GENERAL_IMPORT_ERROR = """Failed to import `start_tunnel`.
1914
Please file an issue at:
2015
https://github.com/doronz88/pymobiledevice3/issues/new?assignees=&labels=&projects=&template=bug_report.md&title=
2116
@@ -28,8 +23,5 @@
2823
def verify_tunnel_imports() -> bool:
2924
if start_tunnel is not None:
3025
return True
31-
if sys.platform == 'win32':
32-
logger.error(WIN32_IMPORT_ERROR)
33-
return False
3426
logger.error(GENERAL_IMPORT_ERROR)
3527
return False

pymobiledevice3/tunneld.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import os
55
import signal
6+
import sys
67
import traceback
78
from contextlib import asynccontextmanager, suppress
89
from typing import Dict, List, Optional, Tuple
@@ -45,8 +46,12 @@ def start(self) -> None:
4546
async def monitor_adapters(self):
4647
previous_ips = []
4748
while True:
48-
current_ips = [f'{adapter.ips[0].ip[0]}%{adapter.nice_name}' for adapter in get_adapters() if
49-
adapter.ips[0].is_IPv6]
49+
if sys.platform == 'win32':
50+
current_ips = [f'{adapter.ips[0].ip[0]}%{adapter.ips[0].ip[2]}' for adapter in get_adapters() if
51+
adapter.ips[0].is_IPv6]
52+
else:
53+
current_ips = [f'{adapter.ips[0].ip[0]}%{adapter.nice_name}' for adapter in get_adapters() if
54+
adapter.ips[0].is_IPv6]
5055

5156
added = [ip for ip in current_ips if ip not in previous_ips]
5257
removed = [ip for ip in previous_ips if ip not in current_ips]

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ developer_disk_image>=0.0.2
3838
opack
3939
psutil
4040
pytun-pmd3>=1.0.0 ; platform_system != "Windows"
41+
pywintunx-pmd3>=1.0.2 ; platform_system == "Windows"
4142
aiofiles
4243
prompt_toolkit
4344
sslpsk-pmd3>=1.0.2

0 commit comments

Comments
 (0)