From b5065e275d8e8e4e8ddf219b2b4bf36788a1061d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Andr=C3=A9=20Vadla=20Ravn=C3=A5s?= Date: Mon, 2 Dec 2024 22:27:30 +0100 Subject: [PATCH] fruity: Improve USB device handling on non-macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Do not open a given USB device if kernel NCM is available, as opening may fail and won't actually do anything useful in the end. - Throw when a kernel NCM interface lacks an IPv6 address, instead of silently degrading to legacy mode. - Replace retry logic with a fixed 250 ms delay, applied when a device arrives after starting, or during a mode-switch. - Change USB device configuration if needed. - Wait for kernel NCM interface to be ready. For now this is limited to Linux systems with either NetworkManager or networkd. Co-authored-by: Håvard Sørbø --- src/fruity/device-monitor-linux.vala | 162 ++++++++++++++++- src/fruity/device-monitor.vala | 226 ++++++++++++++--------- src/fruity/ncm.vala | 258 ++++++++++++++------------- src/fruity/usb.vala | 2 +- 4 files changed, 437 insertions(+), 211 deletions(-) diff --git a/src/fruity/device-monitor-linux.vala b/src/fruity/device-monitor-linux.vala index 2bef363d1..f67d4974f 100644 --- a/src/fruity/device-monitor-linux.vala +++ b/src/fruity/device-monitor-linux.vala @@ -19,7 +19,7 @@ namespace Frida.Fruity { return null; var iface_stream = new DataInputStream (iface.read ()); string iface_name = iface_stream.read_line (); - if (iface_name != "NCM Control" && iface_name != "AppleUSBEthernet") + if (iface_name != "NCM Control") return null; var serial = File.new_build_filename (dev_path, "..", "..", "..", "serial"); @@ -27,7 +27,7 @@ namespace Frida.Fruity { return null; var serial_stream = new DataInputStream (serial.read ()); - return serial_stream.read_line (); + return UsbDevice.udid_from_serial_number (serial_stream.read_line ()); } catch (GLib.Error e) { throw new Error.NOT_SUPPORTED ("%s", e.message); } @@ -195,6 +195,164 @@ namespace Frida.Fruity { } } + namespace Network { + public async void wait_until_interfaces_ready (Gee.Collection interface_names, Cancellable? cancellable) + throws Error, IOError { + try { + var connection = yield GLib.Bus.get (BusType.SYSTEM, cancellable); + + NetworkManager.Service? nm = null; + Networkd.Service? netd = null; + if (yield system_has_service (NetworkManager.SERVICE_NAME, connection, cancellable)) { + nm = yield connection.get_proxy (NetworkManager.SERVICE_NAME, NetworkManager.SERVICE_PATH, + DO_NOT_LOAD_PROPERTIES, cancellable); + } else if (yield system_has_service (Networkd.SERVICE_NAME, connection, cancellable)) { + netd = yield connection.get_proxy (Networkd.SERVICE_NAME, Networkd.SERVICE_PATH, + DO_NOT_LOAD_PROPERTIES, cancellable); + } else { + return; + } + + var remaining = interface_names.size + 1; + + NotifyCompleteFunc on_complete = () => { + remaining--; + if (remaining == 0) + wait_until_interfaces_ready.callback (); + }; + + foreach (var name in interface_names) { + if (nm != null) { + NetworkManager.wait_until_interface_ready.begin (name, nm, connection, cancellable, + on_complete); + } else { + Networkd.wait_until_interface_ready.begin (name, netd, connection, cancellable, + on_complete); + } + } + + var source = new IdleSource (); + source.set_callback (() => { + on_complete (); + return Source.REMOVE; + }); + source.attach (MainContext.get_thread_default ()); + + yield; + } catch (GLib.Error e) { + } + } + + private async bool system_has_service (string name, DBusConnection connection, Cancellable? cancellable) throws GLib.Error { + var v = yield connection.call ( + "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", + "NameHasOwner", + new Variant.tuple ({ name }), + new VariantType.tuple ({ VariantType.BOOLEAN }), + DBusCallFlags.NONE, -1, cancellable); + + bool has_owner; + v.get ("(b)", out has_owner); + return has_owner; + } + } + + private delegate void NotifyCompleteFunc (); + + namespace NetworkManager { + private async void wait_until_interface_ready (string name, Service service, DBusConnection connection, + Cancellable? cancellable, NotifyCompleteFunc on_complete) { + try { + string device_path = yield service.get_device_by_ip_iface (name); + + Device device = yield connection.get_proxy (SERVICE_NAME, device_path, DBusProxyFlags.NONE, cancellable); + + var device_proxy = (DBusProxy) device; + + ulong handler = device_proxy.g_properties_changed.connect ((changed, invalidated) => { + if (changed.lookup_value ("StateReason", null) != null) + wait_until_interface_ready.callback (); + }); + + while (!cancellable.is_cancelled ()) { + uint32 state, reason; + device_proxy.get_cached_property ("StateReason").get ("(uu)", out state, out reason); + if (state == DEVICE_STATE_ACTIVATED) + break; + if (state == DEVICE_STATE_DISCONNECTED && reason != DEVICE_STATE_REASON_NONE) + break; + yield; + } + + device_proxy.disconnect (handler); + } catch (GLib.Error e) { + } + + on_complete (); + } + + private const string SERVICE_NAME = "org.freedesktop.NetworkManager"; + private const string SERVICE_PATH = "/org/freedesktop/NetworkManager"; + + [DBus (name = "org.freedesktop.NetworkManager")] + private interface Service : Object { + public abstract async string get_device_by_ip_iface (string iface) throws GLib.Error; + } + + [DBus (name = "org.freedesktop.NetworkManager.Device")] + private interface Device : Object { + } + + private const uint32 DEVICE_STATE_DISCONNECTED = 30; + private const uint32 DEVICE_STATE_ACTIVATED = 100; + + private const uint32 DEVICE_STATE_REASON_NONE = 0; + } + + namespace Networkd { + private async void wait_until_interface_ready (string name, Service service, DBusConnection connection, + Cancellable? cancellable, NotifyCompleteFunc on_complete) { + try { + int32 ifindex; + string link_path; + yield service.get_link_by_name (name, out ifindex, out link_path); + + Link link = yield connection.get_proxy (SERVICE_NAME, link_path, DBusProxyFlags.NONE, cancellable); + + var link_proxy = (DBusProxy) link; + + ulong handler = link_proxy.g_properties_changed.connect ((changed, invalidated) => { + wait_until_interface_ready.callback (); + }); + + while (!cancellable.is_cancelled ()) { + string operational_state; + link_proxy.get_cached_property ("OperationalState").get ("s", out operational_state); + if (operational_state != "carrier") + break; + yield; + } + + link_proxy.disconnect (handler); + } catch (GLib.Error e) { + } + + on_complete (); + } + + private const string SERVICE_NAME = "org.freedesktop.network1"; + private const string SERVICE_PATH = "/org/freedesktop/network1"; + + [DBus (name = "org.freedesktop.network1.Manager")] + private interface Service : Object { + public abstract async void get_link_by_name (string name, out int32 ifindex, out string path) throws GLib.Error; + } + + [DBus (name = "org.freedesktop.network1.Link")] + private interface Link : Object { + } + } + namespace Resolved { public const string SERVICE_NAME = "org.freedesktop.resolve1"; public const string SERVICE_PATH = "/org/freedesktop/resolve1"; diff --git a/src/fruity/device-monitor.vala b/src/fruity/device-monitor.vala index 1356f44a8..7d8aa2c6d 100644 --- a/src/fruity/device-monitor.vala +++ b/src/fruity/device-monitor.vala @@ -993,62 +993,50 @@ namespace Frida.Fruity { } private async void handle_usb_device_arrival (LibUSB.Device raw_device) { - UsbDevice usb_device; try { - usb_device = new UsbDevice (raw_device, this); - } catch (Error e) { - return; - } - - unowned string udid = usb_device.udid; - var transport = usb_transports.first_match (t => t.udid == udid); - if (transport != null) { - if (!transport.try_complete_modeswitch (raw_device)) - transport = null; - } + UsbDevice usb_device; + try { + usb_device = new UsbDevice (raw_device, this); + } catch (Error e) { + return; + } - if (transport == null) { - transport = new PortableCoreDeviceUsbTransport (this, usb_device, pairing_store); - usb_transports.add (transport); - - if (state != STARTING) { - uint delays[] = { 0, 50, 250 }; - for (uint attempts = 0; attempts != delays.length; attempts++) { - uint delay = delays[attempts]; - if (delay != 0) { - var timeout_source = new TimeoutSource (delay); - timeout_source.set_callback (handle_usb_device_arrival.callback); - timeout_source.attach (main_context); + unowned string udid = usb_device.udid; + var transport = usb_transports.first_match (t => t.udid == udid); - var cancel_source = new CancellableSource (io_cancellable); - cancel_source.set_callback (handle_usb_device_arrival.callback); - cancel_source.attach (main_context); + bool may_need_time_to_settle = state != STARTING && (transport == null || transport.modeswitch_in_progress); + if (may_need_time_to_settle) { + try { + yield sleep (250, io_cancellable); + } catch (IOError e) { + } + } - yield; + if (transport != null) { + if (!transport.try_complete_modeswitch (raw_device)) + transport = null; + } - cancel_source.destroy (); - timeout_source.destroy (); + if (io_cancellable.is_cancelled ()) + return; - if (io_cancellable.is_cancelled ()) - break; - } + if (transport == null) { + transport = new PortableCoreDeviceUsbTransport (this, usb_device, pairing_store); + usb_transports.add (transport); + if (state != STARTING) { try { yield transport.open (io_cancellable); - break; } catch (GLib.Error e) { - // We might still be waiting for a udev rule to run... - if (!(e is Error.PERMISSION_DENIED)) - break; } } - } - transport_attached (transport); + transport_attached (transport); + } + } finally { + if (AtomicUint.dec_and_test (ref pending_usb_device_arrivals) && state == STARTING) + usb_started.resolve (true); } - - if (AtomicUint.dec_and_test (ref pending_usb_device_arrivals) && state == STARTING) - usb_started.resolve (true); } private async void handle_usb_device_departure (LibUSB.Device raw_device) { @@ -1232,6 +1220,8 @@ namespace Frida.Fruity { private unowned PortableCoreDeviceBackend parent; private UsbDevice _usb_device; + private Gee.List ncm_ifaddrs; + private UsbNcmConfig? ncm_config; private string? _name; private Promise? device_request; @@ -1266,32 +1256,46 @@ namespace Frida.Fruity { device_request = new Promise (); try { - if (NcmPeer.detect_ncm_ifaddrs_on_system (_usb_device).is_empty && parent.modeswitch_allowed) { + ncm_ifaddrs = yield NcmPeer.detect_ncm_ifaddrs_on_system (_usb_device, cancellable); + if (ncm_ifaddrs.is_empty) { _usb_device.ensure_open (); - modeswitch_request = new Promise (); - if (yield _usb_device.maybe_modeswitch (cancellable)) { - var source = new TimeoutSource.seconds (2); - source.set_callback (() => { - if (modeswitch_request != null) { - modeswitch_request.reject (new Error.TRANSPORT ("Modeswitch timed out")); - modeswitch_request = null; + if (parent.modeswitch_allowed) { + modeswitch_request = new Promise (); + if (yield _usb_device.maybe_modeswitch (cancellable)) { + var source = new TimeoutSource.seconds (2); + source.set_callback (() => { + if (modeswitch_request != null) { + modeswitch_request.reject (new Error.TRANSPORT ("Modeswitch timed out")); + modeswitch_request = null; + } + return Source.REMOVE; + }); + source.attach (MainContext.get_thread_default ()); + + LibUSB.Device raw_device = null; + try { + raw_device = yield modeswitch_request.future.wait_async (cancellable); + } finally { + source.destroy (); } - return Source.REMOVE; - }); - source.attach (MainContext.get_thread_default ()); - LibUSB.Device raw_device = null; - try { - raw_device = yield modeswitch_request.future.wait_async (cancellable); - } finally { - source.destroy (); + _usb_device = new UsbDevice (raw_device, parent); + _usb_device.ensure_open (); + } else { + modeswitch_request = null; } + } - _usb_device = new UsbDevice (raw_device, parent); - } else { - modeswitch_request = null; + bool device_configuration_changed; + try { + ncm_config = UsbNcmConfig.prepare (_usb_device, out device_configuration_changed); + if (device_configuration_changed) + yield sleep (250, cancellable); + } catch (Error e) { } + + ncm_ifaddrs = yield NcmPeer.detect_ncm_ifaddrs_on_system (_usb_device, cancellable); } device_request.resolve (_usb_device); @@ -1371,18 +1375,26 @@ namespace Frida.Fruity { PortableUsbTunnel? tunnel = null; if (supported_by_os) { - if (ncm_peer == null) - ncm_peer = yield NcmPeer.locate (usb_device, cancellable); + if (ncm_peer == null) { + if (!ncm_ifaddrs.is_empty) { + ncm_peer = yield NcmPeer.locate_on_system_netifs (ncm_ifaddrs, cancellable); + } else if (ncm_config != null) { + ncm_peer = yield NcmPeer.establish_using_our_driver (usb_device, ncm_config, + cancellable); + } + } - tunnel = new PortableUsbTunnel (usb_device, ncm_peer, pairing_store); - tunnel.lost.connect (on_tunnel_lost); - try { - yield tunnel.open (cancellable); - } catch (Error e) { - if (e is Error.NOT_SUPPORTED) - tunnel = null; - else - throw e; + if (ncm_peer != null) { + tunnel = new PortableUsbTunnel (usb_device, ncm_peer, pairing_store); + tunnel.lost.connect (on_tunnel_lost); + try { + yield tunnel.open (cancellable); + } catch (Error e) { + if (e is Error.NOT_SUPPORTED) + tunnel = null; + else + throw e; + } } } @@ -1412,38 +1424,59 @@ namespace Frida.Fruity { ncm.close (); } - public static async NcmPeer locate (UsbDevice usb_device, Cancellable? cancellable) throws Error, IOError { - var device_ifaddrs = detect_ncm_ifaddrs_on_system (usb_device); - if (!device_ifaddrs.is_empty) - return yield locate_on_system_netifs (device_ifaddrs, cancellable); - return yield establish_using_our_driver (usb_device, cancellable); - } - - public static Gee.List detect_ncm_ifaddrs_on_system (UsbDevice usb_device) throws Error { + public static async Gee.List detect_ncm_ifaddrs_on_system (UsbDevice usb_device, + Cancellable? cancellable) throws Error, IOError { var device_ifaddrs = new Gee.ArrayList (); #if LINUX var fruit_finder = FruitFinder.make_default (); unowned string udid = usb_device.udid; - string raw_udid = udid.replace ("-", ""); + + var ncm_interfaces = new Gee.HashSet (); + var names = if_nameindex (); + try { + for (Linux.Network.IfNameindex * cur = names; cur->if_index != 0; cur++) { + string? candidate_udid = fruit_finder.udid_from_iface (cur->if_name); + if (candidate_udid != udid) + continue; + + ncm_interfaces.add (cur->if_name); + } + } finally { + if_freenameindex (names); + } + if (ncm_interfaces.is_empty) + return device_ifaddrs; + + yield Network.wait_until_interfaces_ready (ncm_interfaces, cancellable); + Linux.Network.IfAddrs ifaddrs; Linux.Network.getifaddrs (out ifaddrs); for (unowned Linux.Network.IfAddrs candidate = ifaddrs; candidate != null; candidate = candidate.ifa_next) { if (candidate.ifa_addr.sa_family != Posix.AF_INET6) continue; - string? candidate_udid = fruit_finder.udid_from_iface (candidate.ifa_name); - if (candidate_udid != raw_udid) + if (!ncm_interfaces.contains (candidate.ifa_name)) continue; device_ifaddrs.add ((InetSocketAddress) SocketAddress.from_native ((void *) candidate.ifa_addr, sizeof (Posix.SockAddrIn6))); } + if (device_ifaddrs.is_empty && !ncm_interfaces.is_empty) + throw new Error.NOT_SUPPORTED ("no IPv6 address on NCM network interface"); #endif return device_ifaddrs; } - private static async NcmPeer locate_on_system_netifs (Gee.List ifaddrs, Cancellable? cancellable) +#if LINUX + [CCode (cheader_filename = "net/if.h", cname = "if_nameindex")] + private extern static Linux.Network.IfNameindex* if_nameindex (); + + [CCode (cheader_filename = "net/if.h", cname = "if_freenameindex")] + private extern static void if_freenameindex (Linux.Network.IfNameindex* index); +#endif + + public static async NcmPeer locate_on_system_netifs (Gee.List ifaddrs, Cancellable? cancellable) throws Error, IOError { var main_context = MainContext.ref_thread_default (); @@ -1544,9 +1577,9 @@ namespace Frida.Fruity { } } - private static async NcmPeer establish_using_our_driver (UsbDevice usb_device, Cancellable? cancellable) - throws Error, IOError { - var ncm = yield UsbNcmDriver.open (usb_device, cancellable); + public static async NcmPeer establish_using_our_driver (UsbDevice usb_device, UsbNcmConfig ncm_config, + Cancellable? cancellable) throws Error, IOError { + var ncm = yield UsbNcmDriver.open (usb_device, ncm_config, cancellable); if (ncm.remote_ipv6_address == null) { ulong change_handler = ncm.notify["remote-ipv6-address"].connect ((obj, pspec) => { @@ -1952,4 +1985,23 @@ namespace Frida.Fruity { public InetSocketAddress endpoint; public InetSocketAddress interface_address; } + + private async void sleep (uint duration_msec, Cancellable? cancellable) throws IOError { + var main_context = MainContext.get_thread_default (); + + var delay_source = new TimeoutSource (duration_msec); + delay_source.set_callback (sleep.callback); + delay_source.attach (main_context); + + var cancel_source = new CancellableSource (cancellable); + cancel_source.set_callback (sleep.callback); + cancel_source.attach (main_context); + + yield; + + cancel_source.destroy (); + delay_source.destroy (); + + cancellable.set_error_if_cancelled (); + } } diff --git a/src/fruity/ncm.vala b/src/fruity/ncm.vala index 2d13b8df8..4be074513 100644 --- a/src/fruity/ncm.vala +++ b/src/fruity/ncm.vala @@ -6,6 +6,11 @@ namespace Frida.Fruity { construct; } + public UsbNcmConfig config { + get; + construct; + } + public VirtualNetworkStack netstack { get { return _netstack; @@ -18,11 +23,6 @@ namespace Frida.Fruity { } } - private uint8 data_iface; - private int data_altsetting; - private uint8 rx_address; - private uint8 tx_address; - private VirtualNetworkStack? _netstack; private uint16 next_outgoing_sequence = 1; @@ -30,22 +30,6 @@ namespace Frida.Fruity { private Cancellable io_cancellable = new Cancellable (); - private enum UsbDescriptorType { - INTERFACE = 0x04, - } - - private enum UsbCommSubclass { - NCM = 0x0d, - } - - private enum UsbDataSubclass { - UNDEFINED = 0x00, - } - - private enum UsbCdcDescriptorSubtype { - ETHERNET = 0x0f, - } - private enum EtherType { IPV6 = 0x86dd, } @@ -54,8 +38,9 @@ namespace Frida.Fruity { UDP = 0x11, } - public static async UsbNcmDriver open (UsbDevice device, Cancellable? cancellable = null) throws Error, IOError { - var driver = new UsbNcmDriver (device); + public static async UsbNcmDriver open (UsbDevice device, UsbNcmConfig config, Cancellable? cancellable = null) + throws Error, IOError { + var driver = new UsbNcmDriver (device, config); try { yield driver.init_async (Priority.DEFAULT, cancellable); @@ -66,72 +51,15 @@ namespace Frida.Fruity { return driver; } - private UsbNcmDriver (UsbDevice device) { - Object (device: device); + private UsbNcmDriver (UsbDevice device, UsbNcmConfig config) { + Object (device: device, config: config); } private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError { - device.ensure_open (); - - unowned LibUSB.Device raw_device = device.raw_device; - unowned LibUSB.DeviceHandle handle = device.handle; - - var dev_desc = LibUSB.DeviceDescriptor (raw_device); - - LibUSB.ConfigDescriptor current_config; - Usb.check (raw_device.get_active_config_descriptor (out current_config), "Failed to get active config descriptor"); - - int desired_config_value = -1; - bool found_cdc_header = false; - bool found_data_interface = false; - uint8 mac_address_index = 0; - for (uint8 config_value = dev_desc.bNumConfigurations; config_value != 0; config_value--) { - LibUSB.ConfigDescriptor config; - Usb.check (raw_device.get_config_descriptor_by_value (config_value, out config), - "Failed to get config descriptor"); - - foreach (var iface in config.@interface) { - foreach (var setting in iface.altsetting) { - if (setting.bInterfaceClass == LibUSB.ClassCode.COMM && - setting.bInterfaceSubClass == UsbCommSubclass.NCM) { - try { - parse_cdc_header (setting.extra, out mac_address_index); - found_cdc_header = true; - } catch (Error e) { - break; - } - } else if (setting.bInterfaceClass == LibUSB.ClassCode.DATA && - setting.bInterfaceSubClass == UsbDataSubclass.UNDEFINED && - setting.endpoint.length == 2) { - found_data_interface = true; - - data_iface = setting.bInterfaceNumber; - data_altsetting = setting.bAlternateSetting; - - foreach (var ep in setting.endpoint) { - if ((ep.bEndpointAddress & LibUSB.EndpointDirection.MASK) == - LibUSB.EndpointDirection.IN) { - rx_address = ep.bEndpointAddress; - } else { - tx_address = ep.bEndpointAddress; - } - } - } - } - } - - if (found_cdc_header || found_data_interface) { - desired_config_value = config_value; - break; - } - } - if (!found_cdc_header || !found_data_interface) - throw new Error.NOT_SUPPORTED ("%s", make_user_error_message ("No USB CDC-NCM interface found")); - var language_id = yield device.query_default_language_id (cancellable); uint8 mac_address[6]; - string mac_address_str = yield device.read_string_descriptor (mac_address_index, language_id, cancellable); + string mac_address_str = yield device.read_string_descriptor (config.mac_address_index, language_id, cancellable); if (mac_address_str.length != 12) throw new Error.PROTOCOL ("Invalid MAC address"); for (uint i = 0; i != 6; i++) { @@ -140,22 +68,14 @@ namespace Frida.Fruity { mac_address[i] = (uint8) v; } - if (current_config.bConfigurationValue != desired_config_value) { - foreach (var iface in current_config.@interface) { - unowned LibUSB.InterfaceDescriptor setting = iface.altsetting[0]; - var res = handle.kernel_driver_active (setting.bInterfaceNumber); - if (res == 1) - handle.detach_kernel_driver (setting.bInterfaceNumber); - } - Usb.check (handle.set_configuration (desired_config_value), "Failed to set configuration"); - } + unowned LibUSB.DeviceHandle handle = device.handle; try { - Usb.check (handle.claim_interface (data_iface), "Failed to claim USB interface"); + Usb.check (handle.claim_interface (config.data_iface), "Failed to claim USB interface"); } catch (Error e) { throw new Error.PERMISSION_DENIED ("%s", make_user_error_message (@"Unable to claim USB CDC-NCM interface ($(e.message))")); } - Usb.check (handle.set_interface_alt_setting (data_iface, data_altsetting), + Usb.check (handle.set_interface_alt_setting (config.data_iface, config.data_altsetting), "Failed to set USB interface alt setting"); _netstack = new VirtualNetworkStack (new Bytes (mac_address), null, 1500); @@ -166,15 +86,6 @@ namespace Frida.Fruity { return true; } - private string make_user_error_message (string message) { -#if WINDOWS - return message + "; use https://zadig.akeo.ie to switch from Apple's official driver onto Microsoft's WinUSB " + - "driver, so libusb can access it"; -#else - return message; -#endif - } - public void close () { io_cancellable.cancel (); _netstack.stop (); @@ -185,7 +96,7 @@ namespace Frida.Fruity { while (true) { try { - size_t n = yield device.bulk_transfer (rx_address, data, uint.MAX, io_cancellable); + size_t n = yield device.bulk_transfer (config.rx_address, data, uint.MAX, io_cancellable); handle_ncm_frame (data[:n]); } catch (GLib.Error e) { return; @@ -278,11 +189,124 @@ namespace Frida.Fruity { .build (); try { - yield device.bulk_transfer (tx_address, frame.get_data (), uint.MAX, io_cancellable); + yield device.bulk_transfer (config.tx_address, frame.get_data (), uint.MAX, io_cancellable); } catch (GLib.Error e) { } } + private static InetAddress? try_infer_remote_address_from_datagram (Bytes datagram) { + if (datagram.get_size () < 0x3e) + return null; + + var buf = new Buffer (datagram, BIG_ENDIAN); + + var ethertype = (EtherType) buf.read_uint16 (12); + if (ethertype != IPV6) + return null; + + var next_header = (IPV6NextHeader) buf.read_uint8 (20); + if (next_header != UDP) + return null; + + return new InetAddress.from_bytes (datagram[22:22 + 16].get_data (), IPV6); + } + } + + internal class UsbNcmConfig { + public uint8 data_iface; + public int data_altsetting; + public uint8 rx_address; + public uint8 tx_address; + public uint8 mac_address_index; + + private enum UsbDescriptorType { + INTERFACE = 0x04, + } + + private enum UsbCommSubclass { + NCM = 0x0d, + } + + private enum UsbDataSubclass { + UNDEFINED = 0x00, + } + + private enum UsbCdcDescriptorSubtype { + ETHERNET = 0x0f, + } + + public static UsbNcmConfig prepare (UsbDevice device, out bool device_configuration_changed) throws Error { + unowned LibUSB.Device raw_device = device.raw_device; + + var dev_desc = LibUSB.DeviceDescriptor (raw_device); + + LibUSB.ConfigDescriptor current_config; + Usb.check (raw_device.get_active_config_descriptor (out current_config), "Failed to get active config descriptor"); + + var config = new UsbNcmConfig (); + int desired_config_value = -1; + bool found_cdc_header = false; + bool found_data_interface = false; + for (uint8 config_value = dev_desc.bNumConfigurations; config_value != 0; config_value--) { + LibUSB.ConfigDescriptor config_desc; + Usb.check (raw_device.get_config_descriptor_by_value (config_value, out config_desc), + "Failed to get config descriptor"); + + foreach (var iface in config_desc.@interface) { + foreach (var setting in iface.altsetting) { + if (setting.bInterfaceClass == LibUSB.ClassCode.COMM && + setting.bInterfaceSubClass == UsbCommSubclass.NCM) { + try { + parse_cdc_header (setting.extra, out config.mac_address_index); + found_cdc_header = true; + } catch (Error e) { + break; + } + } else if (setting.bInterfaceClass == LibUSB.ClassCode.DATA && + setting.bInterfaceSubClass == UsbDataSubclass.UNDEFINED && + setting.endpoint.length == 2) { + found_data_interface = true; + + config.data_iface = setting.bInterfaceNumber; + config.data_altsetting = setting.bAlternateSetting; + + foreach (var ep in setting.endpoint) { + if ((ep.bEndpointAddress & LibUSB.EndpointDirection.MASK) == + LibUSB.EndpointDirection.IN) { + config.rx_address = ep.bEndpointAddress; + } else { + config.tx_address = ep.bEndpointAddress; + } + } + } + } + } + + if (found_cdc_header || found_data_interface) { + desired_config_value = config_value; + break; + } + } + if (!found_cdc_header || !found_data_interface) + throw new Error.NOT_SUPPORTED ("%s", make_user_error_message ("No USB CDC-NCM interface found")); + + if (current_config.bConfigurationValue != desired_config_value) { + unowned LibUSB.DeviceHandle handle = device.handle; + foreach (var iface in current_config.@interface) { + unowned LibUSB.InterfaceDescriptor setting = iface.altsetting[0]; + var res = handle.kernel_driver_active (setting.bInterfaceNumber); + if (res == 1) + handle.detach_kernel_driver (setting.bInterfaceNumber); + } + Usb.check (handle.set_configuration (desired_config_value), "Failed to set configuration"); + device_configuration_changed = true; + } else { + device_configuration_changed = false; + } + + return config; + } + private static void parse_cdc_header (uint8[] header, out uint8 mac_address_index) throws Error { var input = new DataInputStream (new MemoryInputStream.from_data (header)); input.set_byte_order (LITTLE_ENDIAN); @@ -312,22 +336,14 @@ namespace Frida.Fruity { throw new Error.PROTOCOL ("CDC Ethernet descriptor not found"); } + } - private static InetAddress? try_infer_remote_address_from_datagram (Bytes datagram) { - if (datagram.get_size () < 0x3e) - return null; - - var buf = new Buffer (datagram, BIG_ENDIAN); - - var ethertype = (EtherType) buf.read_uint16 (12); - if (ethertype != IPV6) - return null; - - var next_header = (IPV6NextHeader) buf.read_uint8 (20); - if (next_header != UDP) - return null; - - return new InetAddress.from_bytes (datagram[22:22 + 16].get_data (), IPV6); - } + private string make_user_error_message (string message) { +#if WINDOWS + return message + "; use https://zadig.akeo.ie to switch from Apple's official driver onto Microsoft's WinUSB " + + "driver, so libusb can access it"; +#else + return message; +#endif } } diff --git a/src/fruity/usb.vala b/src/fruity/usb.vala index 2617f80b0..e2e26392e 100644 --- a/src/fruity/usb.vala +++ b/src/fruity/usb.vala @@ -109,7 +109,7 @@ namespace Frida.Fruity { return result.str; } - private static string udid_from_serial_number (string serial) { + public static string udid_from_serial_number (string serial) { if (serial.length == 24) return serial[:8] + "-" + serial[8:]; return serial;