doronz88 / pymobiledevice3

Pure python3 implementation for working with iDevices (iPhone, etc...).
https://discord.gg/52mZGC3JXJ
GNU General Public License v3.0
1.43k stars 198 forks source link

Presense of older iOS versions causes rsd tunnel creation to fail, even when --udid specifies a valid device (workaround included) #912

Closed briankrznarich closed 6 months ago

briankrznarich commented 6 months ago

Test environment

Describe the bug Even when targeting a fully-supported device, tunnel creation will fail when older, unsupported devices are present. This appears to be because pymobiledevice3 enumerates services on all connected devices, even when --udid is specified. If one device throws an error, the whole process fails with that error.

To Reproduce Run, for example: sudo python3 -m pymobiledevice3 remote start-tunnel --udid [your udid]

The udid should be for a valid device. If this is the only device connected, the command should succeed. If you attach some older devices (it's a lottery, some minor 15.x and 16.x versions pass by unnoticed, some don't), you'll get one of a collection of errors.

From the command-line, you may see only No such option: --tunnel. (this reflects a second bug, which I will report separately)

Two devices that trigger problems: SE2 iOS16.3.1: If not yet pairing, triggers a pairing dialog. When pairing accepted, Exception: Got RstStreamFrame(stream_id=1, flags=[]): error_code=5 (Known issue, see: #623 ) If already pairing: Exception: The device is already in the process of pairing with a host.

SE2 iOS15.5 pymobiledevice3.exceptions.InvalidServiceError: No such service: com.apple.internal.dt.coredevice.untrusted.tunnelservice (Reported issue, closed without diagnosis, see #862 )

Expected behavior Unsupported devices are ignored. Supported devices (17.x, primarily, work as advertised)

Logs I am actually executing tunnel creation via a call to cli() ,which doesn't swallow the exception below. Command-line execution catches all InvalidServiceError exceptions and prints "No such option: --tunnel" instead...

For a similar stack trace, see also #862


  File "/opt/run_quic_tunnel", line 100, in <module>
    sys.exit(cli())
             ^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/pymobiledevice3/cli/cli_common.py", line 149, in wrapper
    func(*args, **kwargs)
  File "/opt/cfpyenv/lib/python3.12/site-packages/pymobiledevice3/cli/remote.py", line 209, in cli_start_tunnel
    asyncio.run(
  File "/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/base_events.py", line 685, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/pymobiledevice3/cli/remote.py", line 162, in start_tunnel_task
    tunnel_services = await get_tunnel_services[connection_type]()
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/pymobiledevice3/remote/tunnel_service.py", line 972, in get_core_device_tunnel_services
    result.append(create_core_device_tunnel_service_using_rsd(rsd))
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/pymobiledevice3/remote/tunnel_service.py", line 900, in create_core_device_tunnel_service_using_rsd
    service.connect(autopair=autopair)
  File "/opt/cfpyenv/lib/python3.12/site-packages/pymobiledevice3/remote/tunnel_service.py", line 798, in connect
    RemoteService.connect(self)
  File "/opt/cfpyenv/lib/python3.12/site-packages/pymobiledevice3/remote/remote_service.py", line 16, in connect
    self.service = self.rsd.start_remote_service(self.service_name)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/pymobiledevice3/remote/remote_service_discovery.py", line 99, in start_remote_service
    service = RemoteXPCConnection((self.service.address[0], self.get_service_port(name)))
                                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/pymobiledevice3/remote/remote_service_discovery.py", line 112, in get_service_port
    raise InvalidServiceError(f'No such service: {name}')
pymobiledevice3.exceptions.InvalidServiceError: No such service: com.apple.internal.dt.coredevice.untrusted.tunnelservice

Additional context Here is one potential workaround. It's so small, and I thought that you might prefer a different approach, that I did not submit a pull request. :

#pymobiledevice3/remote/tunnel_service.py
# Add udid param, skip rsds that don't match udid
async def get_core_device_tunnel_services(bonjour_timeout: float = DEFAULT_BONJOUR_TIMEOUT, udid: str = None) \
        -> List[CoreDeviceTunnelService]:
    result = []
    for rsd in await get_rsds(bonjour_timeout=bonjour_timeout):
        if udid != rsd.udid:
            continue
        result.append(create_core_device_tunnel_service_using_rsd(rsd))
    return result

# pymobiledevice3/cli/remote.py
# pass udid to get_core_device_tunnel_services
tunnel_services = await get_tunnel_services[connection_type](udid=udid)

This solves the problem for when --udid is specified on the command line. It might be preferable to either:

I'm not sure what would work best. Thanks for taking a look. I really appreciate this project.

⬇️ Please click the πŸ‘ reaction instead of leaving a +1 or πŸ‘ comment

doronz88 commented 6 months ago

Actually the better workarround would be to just skip devices with the InvalidServiceError. If you can, submit PR for that. Otherwise, I'll also get to it in the next few days

briankrznarich commented 6 months ago

I could do the PR, and I do have the devices to validate it here.

I would like to ask, do you have an objection to a hybrid approach where the udid is passed through as above, and a try/catch is added (at the same spot, I imagine). I don't know the time/resource cost of the service lookups for each device, but it would be nice to skip them if we know they are unneeded, not merely as a crash workaround.

My inclination would be to:

Honoring --udid here would allow logging "bad devices", while keeping pymobiledevice3 from logging errors related to devices that weren't even requested.

I haven't looked at how you do your logging calls, but I'd try to emulate what your doing. What I would want to see as a user is something like "Skipping Device [UDID/model/version/whatever is handy] + Simple Error Message". Otherwise users wonder why devices are missing...

I've only thought about this as far as start-tunnel, I don't know what the impact is off-hand on other commands.

doronz88 commented 6 months ago

Sounds good to me πŸ™‚

doronz88 commented 6 months ago

I just tested the code as-is with both iOS 17 and iOS 14 and wasn't able to reproduce any errors.

briankrznarich commented 6 months ago

My understanding is that your iOS 14 device is filtered out here:

#./pymobiledevice3/remote/utils.py
# async def get_rsds
                rsd = RemoteServiceDiscoveryService((ip, RSD_PORT))
                try:
                    rsd.connect()
                except ConnectionRefusedError:  #<---- here
                    continue
                except OSError:
                    continue

iOS14 and older, and older iOS 15.x will raise ConnectionRefused and go no further. Newer iOS 15.x will get returned from get_rsd, and then fail with InvalidServiceError. iOS 16 devices are the worst, because they will not return InvalidServiceError, but will fail when you attempt to interact with them like iOS 17 devices. So you get random pairing errors, network connection errors, timeouts, etc.

I don't have a 15.4 device handy, but 15.3 throws ConnectionRefusedError, and 15.5 does not. InvalidServiceError('No such service: com.apple.internal.dt.coredevice.untrusted.tunnelservice' is confirmed on 15.5 and 15.6.

There is a spot in the tunnel creation logic where we already have access to the iOS version. I'm going to complete and pitch a pull-request that does a version check, and skips all devices that are not iOS 17 or newer. Maybe some of this could be reverted with further testing. I don't know if you are aware of anyone successfully using QUIC tunnels with iOS 16.x devices.

Filtering out devices seems like the most practical approach, as it seems they don't work anyway, and merely being plugged to the computer breaks pymobiledevice3 for all other valid devices. Two 16.x examples are:

But I'll look at your comments here/on the pull request when done. These are all suggestions on my part. The iOS 16.6.1 exception/stack trace is below for reference. Even if we caught it and continued, it would still introduce a 60-second pause (or whatever the timeout is).

Granted, I am changing enough that if you specify --udid, most of this becomes a non-issue.

I was not fully aware of all of these details when I filed the ticket. I've been testing various patch options, and am just working all of this out.

It's surpising, I think I got lucky with an older version of pymobiledevice3 that does alright in a mixed iOS17/iOS16 environment, at least where --udid is specified. I did a code diff, and it seems a lot has chanced even in the last few months : )


  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/Users/briank/git/pymobiledevice3/pymobiledevice3/__main__.py", line 159, in <module>
    main()
  File "/Users/briank/git/pymobiledevice3/pymobiledevice3/__main__.py", line 98, in main
    cli()
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/briank/git/pymobiledevice3/pymobiledevice3/cli/cli_common.py", line 149, in wrapper
    func(*args, **kwargs)
  File "/Users/briank/git/pymobiledevice3/pymobiledevice3/cli/remote.py", line 209, in cli_start_tunnel
    asyncio.run(
  File "/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/base_events.py", line 685, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/Users/briank/git/pymobiledevice3/pymobiledevice3/cli/remote.py", line 184, in start_tunnel_task
    await tunnel_task(service, secrets=secrets, script_mode=script_mode, max_idle_timeout=max_idle_timeout,
  File "/Users/briank/git/pymobiledevice3/pymobiledevice3/cli/remote.py", line 116, in tunnel_task
    async with start_tunnel(
  File "/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 210, in __aenter__
    return await anext(self.gen)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/briank/git/pymobiledevice3/pymobiledevice3/remote/tunnel_service.py", line 970, in start_tunnel
    async with start_tunnel_over_core_device(
  File "/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 210, in __aenter__
    return await anext(self.gen)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/briank/git/pymobiledevice3/pymobiledevice3/remote/tunnel_service.py", line 954, in start_tunnel_over_core_device
    async with service_provider.start_quic_tunnel(
  File "/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 210, in __aenter__
    return await anext(self.gen)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/briank/git/pymobiledevice3/pymobiledevice3/remote/tunnel_service.py", line 424, in start_quic_tunnel
    async with aioquic_connect(
  File "/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 210, in __aenter__
    return await anext(self.gen)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/opt/cfpyenv/lib/python3.12/site-packages/qh3/asyncio/client.py", line 97, in connect
    await protocol.wait_connected()
  File "/opt/cfpyenv/lib/python3.12/site-packages/qh3/asyncio/protocol.py", line 127, in wait_connected
    await asyncio.shield(self._connected_waiter)
ConnectionError```
doronz88 commented 6 months ago

I still don't get the issue you are currently reporting. Let's keep it focused on one flow at a time. iOS 16 devices tunnels aren't supported on-device. That is why you're getting an error from them. The pairing part is supported, but they don't support the actual tunnel establishment - so that's why you're getting a very descriptive error from what I understand and am getting.

The error raised is: pymobiledevice3.exceptions.StreamClosedError, since the device is reporting:

[C40 fe80::d4a0:59ff:fe68:29a5%anpi0.64200 tcp, local: fe80::d4a0:59ff:fe68:295a%anpi0.50309, definite, attribution: developer, server, prohibit joining] is already cancelled, ignoring cancel

As you can see, the device prohibits joining and therefore closes the connection. Is this not the expected behavior from what u understand?

NOTE: Some iOS 16 device do support this, but for TCP tunneling only. I think it's starting only at iOS 16.8

briankrznarich commented 6 months ago

The stack trace I showed you is in a valid environment. I did nothing to point pymbobiledevice3 at that iOS16 device, there just happens to be one plugged in. What I think should have happened is that pymobiledevice3 should have skipped that device, and presented a list of iOS17 devices on the system to choose from. Instead it crashed. This makes my iOS17 devices unusable, unless I unplug the iOS16 devices first( no way to create an RSD tunnel)

My smaller goal is to be able to run sudo python3 -m pymobiledevice3 remote start-tunnel --udid [your udid] on a machine that has a mix of device versions attached, as long as udid references an iOS 17 device. That is not currently possible. It used to be possible before some changes in the last couple of months.

I'll have a PR in not too long, and you can take a look. I'm happy to provide whatever information/answer whatever questions/concerns I can. I have a fair collection of devices versions here I can validate against.

Edit: (It seems my description has been unclear. I have a fair collection of devices I can mix&match and plug in to one machine simultaneously, while validating that the iOS17 devices and start-tunnel continue to work)

doronz88 commented 6 months ago

As I cannot reproduce your described issue in any scenario I'll wait for a PR

dokisha commented 6 months ago

Seems like it got fixed, creating a tunnel with command: sudo pymobiledevice3 remote start-tunnel --script-mode --udid <IPHONE_v17.3_UDID> when having multiple iphones with versions [17.3, 16.7.5, 15.8 and 15.6.1] plugged in:

Pmd3 Version 3.3.1 (current?) - Command above works! Pmd3 Version 3.3.0 - Command above doesn't work Pmd3 Version 3.2.0 - Command above doesn't work Pmd3 Version 2.46.1 - Command above works!

More info: Version 3.2.0 Only Iphone v17.3 plugged in - creating tunnel to <v17.3_UDID> works! Iphone v16.7.5 plugged in - creating tunnel to <v17.3_UDID> works! Iphone v15.8 plugged in - creating tunnel to <v17.3_UDID> Error: No such option: --tunnel Iphone v15.6.1 plugged in - creating tunnel to <v17.3_UDID> Error: No such option: --tunnel

Version 3.3.0 Only Iphone v17.3 plugged in - creating tnnel to <v17.3_UDID> works Iphone v16.7.5 plugged in - creating tunnel to <v17.3_UDID> works Iphone v15.8 plugged in - creating tunnel to <v17.3_UDID> Error: pymobiledevice3.main[23591] ERROR Failed to start service. Iphone v15.6.1 plugged in - creating tunnel to <v17.3_UDID> Error: pymobiledevice3.main[23743] ERROR Failed to start service. Possible reasons are: ...

doronz88 commented 6 months ago

@dokisha thanks for reporting πŸ˜ƒ