Skip to content

Commit 0214ea6

Browse files
Tomicyodoronz88
authored andcommitted
remote: add windows support (doronz88#569)
1 parent 57aa99e commit 0214ea6

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
@@ -100,14 +100,17 @@ def wait_return():
100100
UDID_ENV_VAR = 'PYMOBILEDEVICE3_UDID'
101101

102102

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

110-
return wrapper
113+
return wrapper
111114

112115

113116
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
@@ -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

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)