Skip to content

Commit adf5a52

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

File tree

6 files changed

+67
-20
lines changed

6 files changed

+67
-20
lines changed

README.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,20 @@ 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+
# You can either execute using "run as administrator"
191+
# or you will be prompted a UAC dialog to confirm)
192+
python3 -m pymobiledevice3 remote start-tunnel
187193
```
188194

189195
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.
218224
To start the Tunneld Server, use the following command (with root privileges):
219225

220226
```bash
227+
# -- On macOS
221228
sudo python3 -m pymobiledevice3 remote tunneld
229+
230+
# -- On windows
231+
# You can either execute using "run as administrator"
232+
# or you will be prompted a UAC dialog to confirm)
233+
python3 -m pymobiledevice3 remote tunneld
222234
```
223235

224236
### Using Tunneld

pymobiledevice3/cli/cli_common.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,17 @@ def wait_return():
102102
UDID_ENV_VAR = 'PYMOBILEDEVICE3_UDID'
103103

104104

105-
def sudo_required(func):
106-
def wrapper(*args, **kwargs):
107-
if sys.platform != 'win32' and os.geteuid() != 0:
108-
raise AccessDeniedError()
109-
else:
110-
func(*args, **kwargs)
105+
if sys.platform == 'win32':
106+
from pyuac import main_requires_admin as sudo_required
107+
else:
108+
def sudo_required(func):
109+
def wrapper(*args, **kwargs):
110+
if sys.platform != 'win32' and os.geteuid() != 0:
111+
raise AccessDeniedError()
112+
else:
113+
func(*args, **kwargs)
111114

112-
return wrapper
115+
return wrapper
113116

114117

115118
def prompt_device_list(device_list: List):

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
@@ -65,6 +74,12 @@
6574
else:
6675
LOOKBACK_HEADER = b'\x00\x00\x86\xdd'
6776

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+
6883
IPV6_HEADER_SIZE = 40
6984
UDP_HEADER_SIZE = 8
7085

@@ -144,12 +159,18 @@ async def wait_closed(self) -> None:
144159
@asyncio_print_traceback
145160
async def tun_read_task(self) -> None:
146161
read_size = self.tun.mtu + len(LOOKBACK_HEADER)
147-
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:
148170
while True:
149-
packet = await f.read(read_size)
150-
assert packet.startswith(LOOKBACK_HEADER)
151-
packet = packet[len(LOOKBACK_HEADER):]
152-
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)
153174

154175
def start_tunnel(self, address: str, mtu: int) -> None:
155176
self.tun = TunTapDevice()
@@ -402,7 +423,7 @@ def save_pair_record(self) -> None:
402423
'private_key': self.ed25519_private_key.private_bytes_raw(),
403424
'remote_unlock_host_key': self.remote_unlock_host_key
404425
}))
405-
if getenv('SUDO_UID'):
426+
if getenv('SUDO_UID') and sys.platform != 'win32':
406427
chown(self.pair_record_path, int(getenv('SUDO_UID')), int(getenv('SUDO_GID')))
407428

408429
@property

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

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ 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"
42+
pyuac ; platform_system == "Windows"
4143
aiofiles
4244
prompt_toolkit
4345
sslpsk-pmd3>=1.0.2

0 commit comments

Comments
 (0)