Skip to content

Commit dcbc6ee

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

9 files changed

+92
-25
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
@@ -134,7 +135,10 @@ def main() -> None:
134135
except PasswordRequiredError:
135136
logger.error('Device is password protected. Please unlock and retry')
136137
except AccessDeniedError:
137-
logger.error('This command requires root privileges. Consider retrying with "sudo".')
138+
if sys.platform == 'win32':
139+
logger.error('This command requires admin privileges. Consider retrying with "run-as administrator".')
140+
else:
141+
logger.error('This command requires root privileges. Consider retrying with "sudo".')
138142
except BrokenPipeError:
139143
traceback.print_exc()
140144
except TunneldConnectionError:

pymobiledevice3/cli/cli_common.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import logging
44
import os
55
import signal
6-
import sys
76
import uuid
87
from typing import Callable, List, Mapping, Optional, Tuple
98

@@ -100,9 +99,29 @@ def wait_return():
10099
UDID_ENV_VAR = 'PYMOBILEDEVICE3_UDID'
101100

102101

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

pymobiledevice3/cli/remote.py

+11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import List, TextIO
77

88
import click
9+
import pywintunx_pmd3
910

1011
from pymobiledevice3.cli.cli_common import BaseCommand, RSDCommand, print_json, prompt_device_list, sudo_required
1112
from pymobiledevice3.common import get_home_folder
@@ -21,6 +22,11 @@
2122
logger = logging.getLogger(__name__)
2223

2324

25+
def install_driver_if_required() -> None:
26+
if sys.platform == 'win32':
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

+29-8
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
from asyncio import CancelledError, StreamReader, StreamWriter
1515
from collections import namedtuple
1616
from contextlib import asynccontextmanager, suppress
17-
from os import chown, getenv
17+
18+
if sys.platform != 'win32':
19+
from os import chown
20+
21+
from os import getenv
1822
from pathlib import Path
1923
from socket import AF_INET6, create_connection
2024
from ssl import VerifyMode
@@ -33,7 +37,12 @@
3337
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
3438
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
3539
from opack import dumps
36-
from pytun_pmd3 import TunTapDevice
40+
41+
if sys.platform != 'win32':
42+
from pytun_pmd3 import TunTapDevice
43+
else:
44+
from pywintunx_pmd3 import TunTapDevice, set_logger
45+
3746
from qh3.asyncio import QuicConnectionProtocol
3847
from qh3.asyncio.client import connect as aioquic_connect
3948
from qh3.asyncio.protocol import QuicStreamHandler
@@ -61,6 +70,12 @@
6170
else:
6271
LOOKBACK_HEADER = b'\x00\x00\x86\xdd'
6372

73+
if sys.platform == 'win32':
74+
def wintun_logger(level: int, timestamp: int, message: str) -> None:
75+
logging.getLogger('wintun').info(message)
76+
77+
set_logger(wintun_logger)
78+
6479
IPV6_HEADER_SIZE = 40
6580
UDP_HEADER_SIZE = 8
6681

@@ -140,12 +155,18 @@ async def wait_closed(self) -> None:
140155
@asyncio_print_traceback
141156
async def tun_read_task(self) -> None:
142157
read_size = self.tun.mtu + len(LOOKBACK_HEADER)
143-
async with aiofiles.open(self.tun.fileno(), 'rb', opener=lambda path, flags: path, buffering=0) as f:
158+
if sys.platform != 'win32':
159+
async with aiofiles.open(self.tun.fileno(), 'rb', opener=lambda path, flags: path, buffering=0) as f:
160+
while True:
161+
packet = await f.read(read_size)
162+
assert packet.startswith(LOOKBACK_HEADER)
163+
packet = packet[len(LOOKBACK_HEADER):]
164+
await self.send_packet_to_device(packet)
165+
else:
144166
while True:
145-
packet = await f.read(read_size)
146-
assert packet.startswith(LOOKBACK_HEADER)
147-
packet = packet[len(LOOKBACK_HEADER):]
148-
await self.send_packet_to_device(packet)
167+
packet = await asyncio.get_running_loop().run_in_executor(None, self.tun.read)
168+
if packet:
169+
await self.send_packet_to_device(packet)
149170

150171
def start_tunnel(self, address: str, mtu: int) -> None:
151172
self.tun = TunTapDevice()
@@ -398,7 +419,7 @@ def save_pair_record(self) -> None:
398419
'private_key': self.ed25519_private_key.private_bytes_raw(),
399420
'remote_unlock_host_key': self.remote_unlock_host_key
400421
}))
401-
if getenv('SUDO_UID'):
422+
if getenv('SUDO_UID') and sys.platform != 'win32':
402423
chown(self.pair_record_path, int(getenv('SUDO_UID')), int(getenv('SUDO_GID')))
403424

404425
@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)