[reland] Support wireless debugging (#118895)

* Reland "Support iOS wireless debugging (#118104)"

This reverts commit cbf2e16892eaf0fe81c01c01263daf5b1f7c602f.

* Remove device loading status
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index 82c6088..843306a 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -16,7 +16,7 @@
 import '../base/platform.dart';
 import '../base/signals.dart';
 import '../base/terminal.dart';
-import  '../build_info.dart';
+import '../build_info.dart';
 import '../commands/daemon.dart';
 import '../compile.dart';
 import '../daemon.dart';
@@ -24,6 +24,7 @@
 import '../device_port_forwarder.dart';
 import '../fuchsia/fuchsia_device.dart';
 import '../ios/devices.dart';
+import '../ios/iproxy.dart';
 import '../ios/simulators.dart';
 import '../macos/macos_ipad_device.dart';
 import '../mdns_discovery.dart';
@@ -229,7 +230,7 @@
     }
     if (debugPort != null && debugUri != null) {
       throwToolExit(
-        'Either --debugPort or --debugUri can be provided, not both.');
+        'Either --debug-port or --debug-url can be provided, not both.');
     }
 
     if (userIdentifier != null) {
@@ -282,8 +283,9 @@
     final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
     final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
     final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;
+    final bool isNetworkDevice = (device is IOSDevice) && device.interfaceType == IOSDeviceConnectionInterface.network;
 
-    if (debugPort == null && debugUri == null) {
+    if ((debugPort == null && debugUri == null) || isNetworkDevice) {
       if (device is FuchsiaDevice) {
         final String module = stringArgDeprecated('module')!;
         if (module == null) {
@@ -303,16 +305,73 @@
           rethrow;
         }
       } else if ((device is IOSDevice) || (device is IOSSimulator) || (device is MacOSDesignedForIPadDevice)) {
-        final Uri? uriFromMdns =
-          await MDnsObservatoryDiscovery.instance!.getObservatoryUri(
-            appId,
-            device,
-            usesIpv6: usesIpv6,
-            deviceVmservicePort: deviceVmservicePort,
+        // Protocol Discovery relies on logging. On iOS earlier than 13, logging is gathered using syslog.
+        // syslog is not available for iOS 13+. For iOS 13+, Protocol Discovery gathers logs from the VMService.
+        // Since we don't have access to the VMService yet, Protocol Discovery cannot be used for iOS 13+.
+        // Also, network devices must be found using mDNS and cannot use Protocol Discovery.
+        final bool compatibleWithProtocolDiscovery = (device is IOSDevice) &&
+          device.majorSdkVersion < IOSDeviceLogReader.minimumUniversalLoggingSdkVersion &&
+          !isNetworkDevice;
+
+        _logger.printStatus('Waiting for a connection from Flutter on ${device.name}...');
+        final Status discoveryStatus = _logger.startSpinner(
+          timeout: const Duration(seconds: 30),
+          slowWarningCallback: () {
+            // If relying on mDNS to find Dart VM Service, remind the user to allow local network permissions.
+            if (!compatibleWithProtocolDiscovery) {
+              return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n\n'
+                'Click "Allow" to the prompt asking if you would like to find and connect devices on your local network. '
+                'If you selected "Don\'t Allow", you can turn it on in Settings > Your App Name > Local Network. '
+                "If you don't see your app in the Settings, uninstall the app and rerun to see the prompt again.\n";
+            }
+
+            return 'The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...\n';
+          },
+        );
+
+        int? devicePort;
+        if (debugPort != null) {
+          devicePort = debugPort;
+        } else if (debugUri != null) {
+          devicePort = debugUri?.port;
+        } else if (deviceVmservicePort != null) {
+          devicePort = deviceVmservicePort;
+        }
+
+        final Future<Uri?> mDNSDiscoveryFuture = MDnsVmServiceDiscovery.instance!.getVMServiceUriForAttach(
+          appId,
+          device,
+          usesIpv6: usesIpv6,
+          isNetworkDevice: isNetworkDevice,
+          deviceVmservicePort: devicePort,
+        );
+
+        Future<Uri?>? protocolDiscoveryFuture;
+        if (compatibleWithProtocolDiscovery) {
+          final ProtocolDiscovery vmServiceDiscovery = ProtocolDiscovery.observatory(
+            device.getLogReader(),
+            portForwarder: device.portForwarder,
+            ipv6: ipv6!,
+            devicePort: devicePort,
+            hostPort: hostVmservicePort,
+            logger: _logger,
           );
-        observatoryUri = uriFromMdns == null
+          protocolDiscoveryFuture = vmServiceDiscovery.uri;
+        }
+
+        final Uri? foundUrl;
+        if (protocolDiscoveryFuture == null) {
+          foundUrl = await mDNSDiscoveryFuture;
+        } else {
+          foundUrl = await Future.any(
+            <Future<Uri?>>[mDNSDiscoveryFuture, protocolDiscoveryFuture]
+          );
+        }
+        discoveryStatus.stop();
+
+        observatoryUri = foundUrl == null
           ? null
-          : Stream<Uri>.value(uriFromMdns).asBroadcastStream();
+          : Stream<Uri>.value(foundUrl).asBroadcastStream();
       }
       // If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
       if (observatoryUri == null) {
@@ -335,7 +394,7 @@
     } else {
       observatoryUri = Stream<Uri>
         .fromFuture(
-          buildObservatoryUri(
+          buildVMServiceUri(
             device,
             debugUri?.host ?? hostname,
             debugPort ?? debugUri!.port,
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart
index 0c46922..95945a0 100644
--- a/packages/flutter_tools/lib/src/commands/drive.dart
+++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import 'package:args/args.dart';
 import 'package:meta/meta.dart';
 import 'package:package_config/package_config_types.dart';
 
@@ -21,6 +22,8 @@
 import '../device.dart';
 import '../drive/drive_service.dart';
 import '../globals.dart' as globals;
+import '../ios/devices.dart';
+import '../ios/iproxy.dart';
 import '../resident_runner.dart';
 import '../runner/flutter_command.dart' show FlutterCommandCategory, FlutterCommandResult, FlutterOptions;
 import '../web/web_device.dart';
@@ -203,6 +206,27 @@
   @override
   bool get cachePubGet => false;
 
+  String? get applicationBinaryPath => stringArgDeprecated(FlutterOptions.kUseApplicationBinary);
+
+  Future<Device?> get targetedDevice async {
+    return findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null);
+  }
+
+  // Network devices need `publish-port` to be enabled because it requires mDNS.
+  // If the flag wasn't provided as an actual argument and it's a network device,
+  // change it to be enabled.
+  @override
+  Future<bool> get disablePortPublication async {
+    final ArgResults? localArgResults = argResults;
+    final Device? device = await targetedDevice;
+    final bool isNetworkDevice = device is IOSDevice && device.interfaceType == IOSDeviceConnectionInterface.network;
+    if (isNetworkDevice && localArgResults != null && !localArgResults.wasParsed('publish-port')) {
+      _logger.printTrace('Network device is being used. Changing `publish-port` to be enabled.');
+      return false;
+    }
+    return !boolArgDeprecated('publish-port');
+  }
+
   @override
   Future<void> validateCommand() async {
     if (userIdentifier != null) {
@@ -223,8 +247,7 @@
     if (await _fileSystem.type(testFile) != FileSystemEntityType.file) {
       throwToolExit('Test file not found: $testFile');
     }
-    final String? applicationBinaryPath = stringArgDeprecated(FlutterOptions.kUseApplicationBinary);
-    final Device? device = await findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null);
+    final Device? device = await targetedDevice;
     if (device == null) {
       throwToolExit(null);
     }
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index be9a9a2..37c7dc4 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -254,7 +254,7 @@
         purgePersistentCache: purgePersistentCache,
         deviceVmServicePort: deviceVmservicePort,
         hostVmServicePort: hostVmservicePort,
-        disablePortPublication: disablePortPublication,
+        disablePortPublication: await disablePortPublication,
         ddsPort: ddsPort,
         devToolsServerAddress: devToolsServerAddress,
         verboseSystemLogs: boolArgDeprecated('verbose-system-logs'),
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index ba960c1..fffa29d 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -275,7 +275,7 @@
         featureFlags: featureFlags,
         platform: globals.platform,
       ),
-      MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery(
+      MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
         logger: globals.logger,
         flutterUsage: globals.flutterUsage,
       ),
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 67e6cab..fbd2686 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -17,6 +17,7 @@
 import 'build_info.dart';
 import 'devfs.dart';
 import 'device_port_forwarder.dart';
+import 'ios/iproxy.dart';
 import 'project.dart';
 import 'vmservice.dart';
 
@@ -917,7 +918,13 @@
   ///   * https://github.com/dart-lang/sdk/blob/main/sdk/lib/html/doc/NATIVE_NULL_ASSERTIONS.md
   final bool nativeNullAssertions;
 
-  List<String> getIOSLaunchArguments(EnvironmentType environmentType, String? route,  Map<String, Object?> platformArgs) {
+  List<String> getIOSLaunchArguments(
+    EnvironmentType environmentType,
+    String? route,
+    Map<String, Object?> platformArgs, {
+    bool ipv6 = false,
+    IOSDeviceConnectionInterface interfaceType = IOSDeviceConnectionInterface.none
+  }) {
     final String dartVmFlags = computeDartVmFlags(this);
     return <String>[
       if (enableDartProfiling) '--enable-dart-profiling',
@@ -954,6 +961,9 @@
       // Use the suggested host port.
       if (environmentType == EnvironmentType.simulator && hostVmServicePort != null)
         '--observatory-port=$hostVmServicePort',
+      // Tell the observatory to listen on all interfaces, don't restrict to the loopback.
+      if (interfaceType == IOSDeviceConnectionInterface.network)
+        '--observatory-host=${ipv6 ? '::0' : '0.0.0.0'}',
     ];
   }
 
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index b1606e1..c58de37 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -9,6 +9,7 @@
 import 'package:vm_service/vm_service.dart' as vm_service;
 
 import '../application_package.dart';
+import '../base/common.dart';
 import '../base/file_system.dart';
 import '../base/io.dart';
 import '../base/logger.dart';
@@ -21,6 +22,7 @@
 import '../device_port_forwarder.dart';
 import '../globals.dart' as globals;
 import '../macos/xcdevice.dart';
+import '../mdns_discovery.dart';
 import '../project.dart';
 import '../protocol_discovery.dart';
 import '../vmservice.dart';
@@ -190,15 +192,6 @@
   }
 
   @override
-  bool get supportsHotReload => interfaceType == IOSDeviceConnectionInterface.usb;
-
-  @override
-  bool get supportsHotRestart => interfaceType == IOSDeviceConnectionInterface.usb;
-
-  @override
-  bool get supportsFlutterExit => interfaceType == IOSDeviceConnectionInterface.usb;
-
-  @override
   final String name;
 
   @override
@@ -318,7 +311,11 @@
     @visibleForTesting Duration? discoveryTimeout,
   }) async {
     String? packageId;
-
+    if (interfaceType == IOSDeviceConnectionInterface.network &&
+        debuggingOptions.debuggingEnabled &&
+        debuggingOptions.disablePortPublication) {
+      throwToolExit('Cannot start app on wirelessly tethered iOS device. Try running again with the --publish-port flag');
+    }
     if (!prebuiltApplication) {
       _logger.printTrace('Building ${package.name} for $id');
 
@@ -353,8 +350,10 @@
       EnvironmentType.physical,
       route,
       platformArgs,
+      ipv6: ipv6,
+      interfaceType: interfaceType,
     );
-    final Status installStatus = _logger.startProgress(
+    Status startAppStatus = _logger.startProgress(
       'Installing and launching...',
     );
     try {
@@ -379,9 +378,10 @@
             deviceLogReader.debuggerStream = iosDeployDebugger;
           }
         }
+        // Don't port foward if debugging with a network device.
         observatoryDiscovery = ProtocolDiscovery.observatory(
           deviceLogReader,
-          portForwarder: portForwarder,
+          portForwarder: interfaceType == IOSDeviceConnectionInterface.network ? null : portForwarder,
           hostPort: debuggingOptions.hostVmServicePort,
           devicePort: debuggingOptions.deviceVmServicePort,
           ipv6: ipv6,
@@ -412,12 +412,59 @@
         return LaunchResult.succeeded();
       }
 
-      _logger.printTrace('Application launched on the device. Waiting for observatory url.');
-      final Timer timer = Timer(discoveryTimeout ?? const Duration(seconds: 30), () {
-        _logger.printError('iOS Observatory not discovered after 30 seconds. This is taking much longer than expected...');
-        iosDeployDebugger?.pauseDumpBacktraceResume();
+      _logger.printTrace('Application launched on the device. Waiting for Dart VM Service url.');
+
+      final int defaultTimeout = interfaceType == IOSDeviceConnectionInterface.network ? 45 : 30;
+      final Timer timer = Timer(discoveryTimeout ?? Duration(seconds: defaultTimeout), () {
+        _logger.printError('The Dart VM Service was not discovered after $defaultTimeout seconds. This is taking much longer than expected...');
+
+        // If debugging with a wireless device and the timeout is reached, remind the
+        // user to allow local network permissions.
+        if (interfaceType == IOSDeviceConnectionInterface.network) {
+          _logger.printError(
+            '\nClick "Allow" to the prompt asking if you would like to find and connect devices on your local network. '
+            'This is required for wireless debugging. If you selected "Don\'t Allow", '
+            'you can turn it on in Settings > Your App Name > Local Network. '
+            "If you don't see your app in the Settings, uninstall the app and rerun to see the prompt again."
+          );
+        } else {
+          iosDeployDebugger?.pauseDumpBacktraceResume();
+        }
       });
-      final Uri? localUri = await observatoryDiscovery?.uri;
+
+      Uri? localUri;
+      if (interfaceType == IOSDeviceConnectionInterface.network) {
+        // Wait for Dart VM Service to start up.
+        final Uri? serviceURL = await observatoryDiscovery?.uri;
+        if (serviceURL == null) {
+          await iosDeployDebugger?.stopAndDumpBacktrace();
+          return LaunchResult.failed();
+        }
+
+        // If Dart VM Service URL with the device IP is not found within 5 seconds,
+        // change the status message to prompt users to click Allow. Wait 5 seconds because it
+        // should only show this message if they have not already approved the permissions.
+        // MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
+        final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
+          startAppStatus.stop();
+          startAppStatus = _logger.startProgress(
+            'Waiting for approval of local network permissions...',
+          );
+        });
+
+        // Get Dart VM Service URL with the device IP as the host.
+        localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
+          packageId,
+          this,
+          usesIpv6: ipv6,
+          deviceVmservicePort: serviceURL.port,
+          isNetworkDevice: true,
+        );
+
+        mDNSLookupTimer.cancel();
+      } else {
+        localUri = await observatoryDiscovery?.uri;
+      }
       timer.cancel();
       if (localUri == null) {
         await iosDeployDebugger?.stopAndDumpBacktrace();
@@ -429,7 +476,7 @@
       _logger.printError(e.message);
       return LaunchResult.failed();
     } finally {
-      installStatus.stop();
+      startAppStatus.stop();
     }
   }
 
@@ -569,7 +616,6 @@
   }
 }
 
-@visibleForTesting
 class IOSDeviceLogReader extends DeviceLogReader {
   IOSDeviceLogReader._(
     this._iMobileDevice,
diff --git a/packages/flutter_tools/lib/src/macos/xcdevice.dart b/packages/flutter_tools/lib/src/macos/xcdevice.dart
index c6bde74..523e052 100644
--- a/packages/flutter_tools/lib/src/macos/xcdevice.dart
+++ b/packages/flutter_tools/lib/src/macos/xcdevice.dart
@@ -305,12 +305,6 @@
 
         final IOSDeviceConnectionInterface interface = _interfaceType(device);
 
-        // Only support USB devices, skip "network" interface (Xcode > Window > Devices and Simulators > Connect via network).
-        // TODO(jmagman): Remove this check once wirelessly detected devices can be observed and attached, https://github.com/flutter/flutter/issues/15072.
-        if (interface != IOSDeviceConnectionInterface.usb) {
-          continue;
-        }
-
         String? sdkVersion = _sdkVersion(device);
 
         if (sdkVersion != null) {
diff --git a/packages/flutter_tools/lib/src/mdns_discovery.dart b/packages/flutter_tools/lib/src/mdns_discovery.dart
index a1d0717..d466a83 100644
--- a/packages/flutter_tools/lib/src/mdns_discovery.dart
+++ b/packages/flutter_tools/lib/src/mdns_discovery.dart
@@ -13,161 +13,427 @@
 import 'device.dart';
 import 'reporting/reporting.dart';
 
-/// A wrapper around [MDnsClient] to find a Dart observatory instance.
-class MDnsObservatoryDiscovery {
-  /// Creates a new [MDnsObservatoryDiscovery] object.
+/// A wrapper around [MDnsClient] to find a Dart VM Service instance.
+class MDnsVmServiceDiscovery {
+  /// Creates a new [MDnsVmServiceDiscovery] object.
   ///
   /// The [_client] parameter will be defaulted to a new [MDnsClient] if null.
-  /// The [applicationId] parameter may be null, and can be used to
-  /// automatically select which application to use if multiple are advertising
-  /// Dart observatory ports.
-  MDnsObservatoryDiscovery({
+  MDnsVmServiceDiscovery({
     MDnsClient? mdnsClient,
+    MDnsClient? preliminaryMDnsClient,
     required Logger logger,
     required Usage flutterUsage,
-  }): _client = mdnsClient ?? MDnsClient(),
-      _logger = logger,
-      _flutterUsage = flutterUsage;
+  })  : _client = mdnsClient ?? MDnsClient(),
+        _preliminaryClient = preliminaryMDnsClient,
+        _logger = logger,
+        _flutterUsage = flutterUsage;
 
   final MDnsClient _client;
+
+  // Used when discovering VM services with `queryForAttach` to do a preliminary
+  // check for already running services so that results are not cached in _client.
+  final MDnsClient? _preliminaryClient;
+
   final Logger _logger;
   final Usage _flutterUsage;
 
   @visibleForTesting
-  static const String dartObservatoryName = '_dartobservatory._tcp.local';
+  static const String dartVmServiceName = '_dartobservatory._tcp.local';
 
-  static MDnsObservatoryDiscovery? get instance => context.get<MDnsObservatoryDiscovery>();
+  static MDnsVmServiceDiscovery? get instance => context.get<MDnsVmServiceDiscovery>();
 
-  /// Executes an mDNS query for a Dart Observatory.
+  /// Executes an mDNS query for Dart VM Services.
+  /// Checks for services that have already been launched.
+  /// If none are found, it will listen for new services to become active
+  /// and return the first it finds that match the parameters.
   ///
   /// The [applicationId] parameter may be used to specify which application
   /// to find. For Android, it refers to the package name; on iOS, it refers to
   /// the bundle ID.
   ///
-  /// If it is not null, this method will find the port and authentication code
-  /// of the Dart Observatory for that application. If it cannot find a Dart
-  /// Observatory matching that application identifier, it will call
-  /// [throwToolExit].
+  /// The [deviceVmservicePort] parameter may be used to specify which port
+  /// to find.
   ///
-  /// If it is null and there are multiple ports available, the user will be
-  /// prompted with a list of available observatory ports and asked to select
-  /// one.
+  /// The [isNetworkDevice] parameter flags whether to get the device IP
+  /// and the [ipv6] parameter flags whether to get an iPv6 address
+  /// (otherwise it will get iPv4).
   ///
-  /// If it is null and there is only one available instance of Observatory,
-  /// it will return that instance's information regardless of what application
-  /// the Observatory instance is for.
+  /// The [timeout] parameter determines how long to continue to wait for
+  /// services to become active.
+  ///
+  /// If [applicationId] is not null, this method will find the port and authentication code
+  /// of the Dart VM Service for that application. If it cannot find a service matching
+  /// that application identifier after the [timeout], it will call [throwToolExit].
+  ///
+  /// If [applicationId] is null and there are multiple Dart VM Services available,
+  /// the user will be prompted with a list of available services with the respective
+  /// app-id and device-vmservice-port to use and asked to select one.
+  ///
+  /// If it is null and there is only one available or it's the first found instance
+  /// of Dart VM Service, it will return that instance's information regardless of
+  /// what application the service instance is for.
   @visibleForTesting
-  Future<MDnsObservatoryDiscoveryResult?> query({String? applicationId, int? deviceVmservicePort}) async {
-    _logger.printTrace('Checking for advertised Dart observatories...');
-    try {
-      await _client.start();
-      final List<PtrResourceRecord> pointerRecords = await _client
-        .lookup<PtrResourceRecord>(
-          ResourceRecordQuery.serverPointer(dartObservatoryName),
-        )
-        .toList();
-      if (pointerRecords.isEmpty) {
-        _logger.printTrace('No pointer records found.');
-        return null;
+  Future<MDnsVmServiceDiscoveryResult?> queryForAttach({
+    String? applicationId,
+    int? deviceVmservicePort,
+    bool ipv6 = false,
+    bool isNetworkDevice = false,
+    Duration timeout = const Duration(minutes: 10),
+  }) async {
+    // Poll for 5 seconds to see if there are already services running.
+    // Use a new instance of MDnsClient so results don't get cached in _client.
+    // If no results are found, poll for a longer duration to wait for connections.
+    // If more than 1 result is found, throw an error since it can't be determined which to pick.
+    // If only one is found, return it.
+    final List<MDnsVmServiceDiscoveryResult> results = await _pollingVmService(
+      _preliminaryClient ?? MDnsClient(),
+      applicationId: applicationId,
+      deviceVmservicePort: deviceVmservicePort,
+      ipv6: ipv6,
+      isNetworkDevice: isNetworkDevice,
+      timeout: const Duration(seconds: 5),
+    );
+    if (results.isEmpty) {
+      return firstMatchingVmService(
+        _client,
+        applicationId: applicationId,
+        deviceVmservicePort: deviceVmservicePort,
+        ipv6: ipv6,
+        isNetworkDevice: isNetworkDevice,
+        timeout: timeout,
+      );
+    } else if (results.length > 1) {
+      final StringBuffer buffer = StringBuffer();
+      buffer.writeln('There are multiple Dart VM Services available.');
+      buffer.writeln('Rerun this command with one of the following passed in as the app-id and device-vmservice-port:');
+      buffer.writeln();
+      for (final MDnsVmServiceDiscoveryResult result in results) {
+        buffer.writeln(
+            '  flutter attach --app-id "${result.domainName.replaceAll('.$dartVmServiceName', '')}" --device-vmservice-port ${result.port}');
       }
-      // We have no guarantee that we won't get multiple hits from the same
-      // service on this.
-      final Set<String> uniqueDomainNames = pointerRecords
-        .map<String>((PtrResourceRecord record) => record.domainName)
-        .toSet();
+      throwToolExit(buffer.toString());
+    }
+    return results.first;
+  }
 
-      String? domainName;
-      if (applicationId != null) {
-        for (final String name in uniqueDomainNames) {
-          if (name.toLowerCase().startsWith(applicationId.toLowerCase())) {
-            domainName = name;
+  /// Executes an mDNS query for Dart VM Services.
+  /// Listens for new services to become active and returns the first it finds that
+  /// match the parameters.
+  ///
+  /// The [applicationId] parameter must be set to specify which application
+  /// to find. For Android, it refers to the package name; on iOS, it refers to
+  /// the bundle ID.
+  ///
+  /// The [deviceVmservicePort] parameter must be set to specify which port
+  /// to find.
+  ///
+  /// [applicationId] and [deviceVmservicePort] are required for launch so that
+  /// if multiple flutter apps are running on different devices, it will
+  /// only match with the device running the desired app.
+  ///
+  /// The [isNetworkDevice] parameter flags whether to get the device IP
+  /// and the [ipv6] parameter flags whether to get an iPv6 address
+  /// (otherwise it will get iPv4).
+  ///
+  /// The [timeout] parameter determines how long to continue to wait for
+  /// services to become active.
+  ///
+  /// If a Dart VM Service matching the [applicationId] and [deviceVmservicePort]
+  /// cannot be found after the [timeout], it will call [throwToolExit].
+  @visibleForTesting
+  Future<MDnsVmServiceDiscoveryResult?> queryForLaunch({
+    required String applicationId,
+    required int deviceVmservicePort,
+    bool ipv6 = false,
+    bool isNetworkDevice = false,
+    Duration timeout = const Duration(minutes: 10),
+  }) async {
+    // Query for a specific application and device port.
+    return firstMatchingVmService(
+      _client,
+      applicationId: applicationId,
+      deviceVmservicePort: deviceVmservicePort,
+      ipv6: ipv6,
+      isNetworkDevice: isNetworkDevice,
+      timeout: timeout,
+    );
+  }
+
+  /// Polls for Dart VM Services and returns the first it finds that match
+  /// the [applicationId]/[deviceVmservicePort] (if applicable).
+  /// Returns null if no results are found.
+  @visibleForTesting
+  Future<MDnsVmServiceDiscoveryResult?> firstMatchingVmService(
+    MDnsClient client, {
+    String? applicationId,
+    int? deviceVmservicePort,
+    bool ipv6 = false,
+    bool isNetworkDevice = false,
+    Duration timeout = const Duration(minutes: 10),
+  }) async {
+    final List<MDnsVmServiceDiscoveryResult> results = await _pollingVmService(
+      client,
+      applicationId: applicationId,
+      deviceVmservicePort: deviceVmservicePort,
+      ipv6: ipv6,
+      isNetworkDevice: isNetworkDevice,
+      timeout: timeout,
+      quitOnFind: true,
+    );
+    if (results.isEmpty) {
+      return null;
+    }
+    return results.first;
+  }
+
+  Future<List<MDnsVmServiceDiscoveryResult>> _pollingVmService(
+    MDnsClient client, {
+    String? applicationId,
+    int? deviceVmservicePort,
+    bool ipv6 = false,
+    bool isNetworkDevice = false,
+    required Duration timeout,
+    bool quitOnFind = false,
+  }) async {
+    _logger.printTrace('Checking for advertised Dart VM Services...');
+    try {
+      await client.start();
+
+      final List<MDnsVmServiceDiscoveryResult> results =
+          <MDnsVmServiceDiscoveryResult>[];
+      final Set<String> uniqueDomainNames = <String>{};
+
+      // Listen for mDNS connections until timeout.
+      final Stream<PtrResourceRecord> ptrResourceStream = client.lookup<PtrResourceRecord>(
+        ResourceRecordQuery.serverPointer(dartVmServiceName),
+        timeout: timeout
+      );
+      await for (final PtrResourceRecord ptr in ptrResourceStream) {
+        uniqueDomainNames.add(ptr.domainName);
+
+        String? domainName;
+        if (applicationId != null) {
+          // If applicationId is set, only use records that match it
+          if (ptr.domainName.toLowerCase().startsWith(applicationId.toLowerCase())) {
+            domainName = ptr.domainName;
+          } else {
+            continue;
+          }
+        } else {
+          domainName = ptr.domainName;
+        }
+
+        _logger.printTrace('Checking for available port on $domainName');
+        final List<SrvResourceRecord> srvRecords = await client
+          .lookup<SrvResourceRecord>(
+            ResourceRecordQuery.service(domainName),
+          )
+          .toList();
+        if (srvRecords.isEmpty) {
+          continue;
+        }
+
+        // If more than one SrvResourceRecord found, it should just be a duplicate.
+        final SrvResourceRecord srvRecord = srvRecords.first;
+        if (srvRecords.length > 1) {
+          _logger.printWarning(
+              'Unexpectedly found more than one Dart VM Service report for $domainName '
+              '- using first one (${srvRecord.port}).');
+        }
+
+        // If deviceVmservicePort is set, only use records that match it
+        if (deviceVmservicePort != null && srvRecord.port != deviceVmservicePort) {
+          continue;
+        }
+
+        // Get the IP address of the service if using a network device.
+        InternetAddress? ipAddress;
+        if (isNetworkDevice) {
+          List<IPAddressResourceRecord> ipAddresses = await client
+            .lookup<IPAddressResourceRecord>(
+              ipv6
+                  ? ResourceRecordQuery.addressIPv6(srvRecord.target)
+                  : ResourceRecordQuery.addressIPv4(srvRecord.target),
+            )
+            .toList();
+          if (ipAddresses.isEmpty) {
+            throwToolExit('Did not find IP for service ${srvRecord.target}.');
+          }
+
+          // Filter out link-local addresses.
+          if (ipAddresses.length > 1) {
+            ipAddresses = ipAddresses.where((IPAddressResourceRecord element) => !element.address.isLinkLocal).toList();
+          }
+
+          ipAddress = ipAddresses.first.address;
+          if (ipAddresses.length > 1) {
+            _logger.printWarning(
+                'Unexpectedly found more than one IP for Dart VM Service ${srvRecord.target} '
+                '- using first one ($ipAddress).');
+          }
+        }
+
+        _logger.printTrace('Checking for authentication code for $domainName');
+        final List<TxtResourceRecord> txt = await client
+          .lookup<TxtResourceRecord>(
+              ResourceRecordQuery.text(domainName),
+          )
+          .toList();
+        if (txt == null || txt.isEmpty) {
+          results.add(MDnsVmServiceDiscoveryResult(domainName, srvRecord.port, ''));
+          if (quitOnFind) {
+            return results;
+          }
+          continue;
+        }
+        const String authCodePrefix = 'authCode=';
+        String? raw;
+        for (final String record in txt.first.text.split('\n')) {
+          if (record.startsWith(authCodePrefix)) {
+            raw = record;
             break;
           }
         }
-        if (domainName == null) {
-          throwToolExit('Did not find a observatory port advertised for $applicationId.');
+        if (raw == null) {
+          results.add(MDnsVmServiceDiscoveryResult(domainName, srvRecord.port, ''));
+          if (quitOnFind) {
+            return results;
+          }
+          continue;
         }
-      } else if (uniqueDomainNames.length > 1) {
-        final StringBuffer buffer = StringBuffer();
-        buffer.writeln('There are multiple observatory ports available.');
-        buffer.writeln('Rerun this command with one of the following passed in as the appId:');
-        buffer.writeln();
-        for (final String uniqueDomainName in uniqueDomainNames) {
-          buffer.writeln('  flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}');
+        String authCode = raw.substring(authCodePrefix.length);
+        // The Dart VM Service currently expects a trailing '/' as part of the
+        // URI, otherwise an invalid authentication code response is given.
+        if (!authCode.endsWith('/')) {
+          authCode += '/';
         }
-        throwToolExit(buffer.toString());
-      } else {
-        domainName = pointerRecords[0].domainName;
-      }
-      _logger.printTrace('Checking for available port on $domainName');
-      // Here, if we get more than one, it should just be a duplicate.
-      final List<SrvResourceRecord> srv = await _client
-        .lookup<SrvResourceRecord>(
-          ResourceRecordQuery.service(domainName),
-        )
-        .toList();
-      if (srv.isEmpty) {
-        return null;
-      }
-      if (srv.length > 1) {
-        _logger.printWarning('Unexpectedly found more than one observatory report for $domainName '
-                   '- using first one (${srv.first.port}).');
-      }
-      _logger.printTrace('Checking for authentication code for $domainName');
-      final List<TxtResourceRecord> txt = await _client
-        .lookup<TxtResourceRecord>(
-            ResourceRecordQuery.text(domainName),
-        )
-        .toList();
-      if (txt == null || txt.isEmpty) {
-        return MDnsObservatoryDiscoveryResult(srv.first.port, '');
-      }
-      const String authCodePrefix = 'authCode=';
-      String? raw;
-      for (final String record in txt.first.text.split('\n')) {
-        if (record.startsWith(authCodePrefix)) {
-          raw = record;
-          break;
+
+        results.add(MDnsVmServiceDiscoveryResult(
+          domainName,
+          srvRecord.port,
+          authCode,
+          ipAddress: ipAddress
+        ));
+        if (quitOnFind) {
+          return results;
         }
       }
-      if (raw == null) {
-        return MDnsObservatoryDiscoveryResult(srv.first.port, '');
+
+      // If applicationId is set and quitOnFind is true and no results matching
+      // the applicationId were found but other results were found, throw an error.
+      if (applicationId != null &&
+          quitOnFind &&
+          results.isEmpty &&
+          uniqueDomainNames.isNotEmpty) {
+        String message = 'Did not find a Dart VM Service advertised for $applicationId';
+        if (deviceVmservicePort != null) {
+          message += ' on port $deviceVmservicePort';
+        }
+        throwToolExit('$message.');
       }
-      String authCode = raw.substring(authCodePrefix.length);
-      // The Observatory currently expects a trailing '/' as part of the
-      // URI, otherwise an invalid authentication code response is given.
-      if (!authCode.endsWith('/')) {
-        authCode += '/';
-      }
-      return MDnsObservatoryDiscoveryResult(srv.first.port, authCode);
+
+      return results;
     } finally {
-      _client.stop();
+      client.stop();
     }
   }
 
-  Future<Uri?> getObservatoryUri(String? applicationId, Device device, {
+  /// Gets Dart VM Service Uri for `flutter attach`.
+  /// Executes an mDNS query and waits until a Dart VM Service is found.
+  ///
+  /// Differs from `getVMServiceUriForLaunch` because it can search for any available Dart VM Service.
+  /// Since [applicationId] and [deviceVmservicePort] are optional, it can either look for any service
+  /// or a specific service matching [applicationId]/[deviceVmservicePort].
+  /// It may find more than one service, which will throw an error listing the found services.
+  Future<Uri?> getVMServiceUriForAttach(
+    String? applicationId,
+    Device device, {
     bool usesIpv6 = false,
     int? hostVmservicePort,
     int? deviceVmservicePort,
+    bool isNetworkDevice = false,
+    Duration timeout = const Duration(minutes: 10),
   }) async {
-    final MDnsObservatoryDiscoveryResult? result = await query(
+    final MDnsVmServiceDiscoveryResult? result = await queryForAttach(
       applicationId: applicationId,
       deviceVmservicePort: deviceVmservicePort,
+      ipv6: usesIpv6,
+      isNetworkDevice: isNetworkDevice,
+      timeout: timeout,
     );
+    return _handleResult(
+      result,
+      device,
+      applicationId: applicationId,
+      deviceVmservicePort: deviceVmservicePort,
+      hostVmservicePort: hostVmservicePort,
+      usesIpv6: usesIpv6,
+      isNetworkDevice: isNetworkDevice
+    );
+  }
+
+  /// Gets Dart VM Service Uri for `flutter run`.
+  /// Executes an mDNS query and waits until the Dart VM Service service is found.
+  ///
+  /// Differs from `getVMServiceUriForAttach` because it only searches for a specific service.
+  /// This is enforced by [applicationId] and [deviceVmservicePort] being required.
+  Future<Uri?> getVMServiceUriForLaunch(
+    String applicationId,
+    Device device, {
+    bool usesIpv6 = false,
+    int? hostVmservicePort,
+    required int deviceVmservicePort,
+    bool isNetworkDevice = false,
+    Duration timeout = const Duration(minutes: 10),
+  }) async {
+    final MDnsVmServiceDiscoveryResult? result = await queryForLaunch(
+      applicationId: applicationId,
+      deviceVmservicePort: deviceVmservicePort,
+      ipv6: usesIpv6,
+      isNetworkDevice: isNetworkDevice,
+      timeout: timeout,
+    );
+    return _handleResult(
+      result,
+      device,
+      applicationId: applicationId,
+      deviceVmservicePort: deviceVmservicePort,
+      hostVmservicePort: hostVmservicePort,
+      usesIpv6: usesIpv6,
+      isNetworkDevice: isNetworkDevice
+    );
+  }
+
+  Future<Uri?> _handleResult(
+    MDnsVmServiceDiscoveryResult? result,
+    Device device, {
+    String? applicationId,
+    int? deviceVmservicePort,
+    int? hostVmservicePort,
+    bool usesIpv6 = false,
+    bool isNetworkDevice = false,
+  }) async {
     if (result == null) {
       await _checkForIPv4LinkLocal(device);
       return null;
     }
+    final String host;
 
-    final String host = usesIpv6
+    final InternetAddress? ipAddress = result.ipAddress;
+    if (isNetworkDevice && ipAddress != null) {
+      host = ipAddress.address;
+    } else {
+      host = usesIpv6
       ? InternetAddress.loopbackIPv6.address
       : InternetAddress.loopbackIPv4.address;
-    return buildObservatoryUri(
+    }
+    return buildVMServiceUri(
       device,
       host,
       result.port,
       hostVmservicePort,
       result.authCode,
+      isNetworkDevice,
     );
   }
 
@@ -236,18 +502,26 @@
   }
 }
 
-class MDnsObservatoryDiscoveryResult {
-  MDnsObservatoryDiscoveryResult(this.port, this.authCode);
+class MDnsVmServiceDiscoveryResult {
+  MDnsVmServiceDiscoveryResult(
+    this.domainName,
+    this.port,
+    this.authCode, {
+    this.ipAddress
+  });
+  final String domainName;
   final int port;
   final String authCode;
+  final InternetAddress? ipAddress;
 }
 
-Future<Uri> buildObservatoryUri(
+Future<Uri> buildVMServiceUri(
   Device device,
   String host,
   int devicePort, [
   int? hostVmservicePort,
   String? authCode,
+  bool isNetworkDevice = false,
 ]) async {
   String path = '/';
   if (authCode != null) {
@@ -259,8 +533,16 @@
     path += '/';
   }
   hostVmservicePort ??= 0;
-  final int? actualHostPort = hostVmservicePort == 0 ?
+
+  final int? actualHostPort;
+  if (isNetworkDevice) {
+    // When debugging with a network device, port forwarding is not required
+    // so just use the device's port.
+    actualHostPort = devicePort;
+  } else {
+    actualHostPort = hostVmservicePort == 0 ?
     await device.portForwarder?.forward(devicePort) :
     hostVmservicePort;
+  }
   return Uri(scheme: 'http', host: host, port: actualHostPort, path: path);
 }
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index d0a963c..fd9c3ed 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -569,7 +569,7 @@
     );
   }
 
-  bool get disablePortPublication => !boolArgDeprecated('publish-port');
+  Future<bool> get disablePortPublication async => !boolArgDeprecated('publish-port');
 
   void usesIpv6Flag({required bool verboseHelp}) {
     argParser.addFlag(ipv6Flag,
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
index a725c0b..42d91c6 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
@@ -23,6 +23,7 @@
 import 'package:flutter_tools/src/device_port_forwarder.dart';
 import 'package:flutter_tools/src/ios/application_package.dart';
 import 'package:flutter_tools/src/ios/devices.dart';
+import 'package:flutter_tools/src/ios/iproxy.dart';
 import 'package:flutter_tools/src/macos/macos_ipad_device.dart';
 import 'package:flutter_tools/src/mdns_discovery.dart';
 import 'package:flutter_tools/src/project.dart';
@@ -83,6 +84,7 @@
     group('with one device and no specified target file', () {
       const int devicePort = 499;
       const int hostPort = 42;
+      final int future = DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch;
 
       late FakeDeviceLogReader fakeLogReader;
       late RecordingPortForwarder portForwarder;
@@ -102,17 +104,17 @@
         fakeLogReader.dispose();
       });
 
-      testUsingContext('succeeds with iOS device', () async {
+      testUsingContext('succeeds with iOS device with protocol discovery', () async {
         final FakeIOSDevice device = FakeIOSDevice(
           logReader: fakeLogReader,
           portForwarder: portForwarder,
+          majorSdkVersion: 12,
           onGetLogReader: () {
             fakeLogReader.addLine('Foo');
             fakeLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:$devicePort');
             return fakeLogReader;
           },
         );
-
         testDeviceManager.devices = <Device>[device];
         final Completer<void> completer = Completer<void>();
         final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
@@ -121,7 +123,20 @@
             completer.complete();
           }
         });
-        final Future<void> task = createTestCommandRunner(AttachCommand(
+        final FakeHotRunner hotRunner = FakeHotRunner();
+        hotRunner.onAttach = (
+          Completer<DebugConnectionInfo>? connectionInfoCompleter,
+          Completer<void>? appStartedCompleter,
+          bool allowExistingDdsInstance,
+          bool enableDevTools,
+        ) async => 0;
+        hotRunner.exited = false;
+        hotRunner.isWaitingForObservatory = false;
+        final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
+          ..hotRunner = hotRunner;
+
+        await createTestCommandRunner(AttachCommand(
+          hotRunnerFactory: hotRunnerFactory,
           artifacts: artifacts,
           stdio: stdio,
           logger: logger,
@@ -137,15 +152,309 @@
         expect(portForwarder.hostPort, hostPort);
 
         await fakeLogReader.dispose();
-        await expectLoggerInterruptEndsTask(task, logger);
         await loggerSubscription.cancel();
       }, overrides: <Type, Generator>{
         FileSystem: () => testFileSystem,
         ProcessManager: () => FakeProcessManager.any(),
         Logger: () => logger,
         DeviceManager: () => testDeviceManager,
-        MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery(
+        MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
           mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
+          preliminaryMDnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
+          logger: logger,
+          flutterUsage: TestUsage(),
+        ),
+      });
+
+      testUsingContext('succeeds with iOS device with mDNS', () async {
+        final FakeIOSDevice device = FakeIOSDevice(
+          logReader: fakeLogReader,
+          portForwarder: portForwarder,
+          majorSdkVersion: 16,
+          onGetLogReader: () {
+            fakeLogReader.addLine('Foo');
+            fakeLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:$devicePort');
+            return fakeLogReader;
+          },
+        );
+        testDeviceManager.devices = <Device>[device];
+        final FakeHotRunner hotRunner = FakeHotRunner();
+        hotRunner.onAttach = (
+          Completer<DebugConnectionInfo>? connectionInfoCompleter,
+          Completer<void>? appStartedCompleter,
+          bool allowExistingDdsInstance,
+          bool enableDevTools,
+        ) async => 0;
+        hotRunner.exited = false;
+        hotRunner.isWaitingForObservatory = false;
+        final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
+          ..hotRunner = hotRunner;
+
+        await createTestCommandRunner(AttachCommand(
+          hotRunnerFactory: hotRunnerFactory,
+          artifacts: artifacts,
+          stdio: stdio,
+          logger: logger,
+          terminal: terminal,
+          signals: signals,
+          platform: platform,
+          processInfo: processInfo,
+          fileSystem: testFileSystem,
+        )).run(<String>['attach']);
+        await fakeLogReader.dispose();
+
+        expect(portForwarder.devicePort, devicePort);
+        expect(portForwarder.hostPort, hostPort);
+        expect(hotRunnerFactory.devices, hasLength(1));
+        final FlutterDevice flutterDevice = hotRunnerFactory.devices.first;
+        final Uri? observatoryUri = await flutterDevice.observatoryUris?.first;
+        expect(observatoryUri.toString(), 'http://127.0.0.1:$hostPort/xyz/');
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+        ProcessManager: () => FakeProcessManager.any(),
+        Logger: () => logger,
+        DeviceManager: () => testDeviceManager,
+        MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
+          mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
+          preliminaryMDnsClient: FakeMDnsClient(
+            <PtrResourceRecord>[
+              PtrResourceRecord('foo', future, domainName: 'bar'),
+            ],
+            <String, List<SrvResourceRecord>>{
+              'bar': <SrvResourceRecord>[
+                SrvResourceRecord('bar', future, port: devicePort, weight: 1, priority: 1, target: 'appId'),
+              ],
+            },
+            txtResponse: <String, List<TxtResourceRecord>>{
+              'bar': <TxtResourceRecord>[
+                TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
+              ],
+            },
+          ),
+          logger: logger,
+          flutterUsage: TestUsage(),
+        ),
+      });
+
+      testUsingContext('succeeds with iOS device with mDNS network device', () async {
+        final FakeIOSDevice device = FakeIOSDevice(
+          logReader: fakeLogReader,
+          portForwarder: portForwarder,
+          majorSdkVersion: 16,
+          interfaceType: IOSDeviceConnectionInterface.network,
+        );
+        testDeviceManager.devices = <Device>[device];
+        final FakeHotRunner hotRunner = FakeHotRunner();
+        hotRunner.onAttach = (
+          Completer<DebugConnectionInfo>? connectionInfoCompleter,
+          Completer<void>? appStartedCompleter,
+          bool allowExistingDdsInstance,
+          bool enableDevTools,
+        ) async => 0;
+        hotRunner.exited = false;
+        hotRunner.isWaitingForObservatory = false;
+        final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
+          ..hotRunner = hotRunner;
+
+        await createTestCommandRunner(AttachCommand(
+          hotRunnerFactory: hotRunnerFactory,
+          artifacts: artifacts,
+          stdio: stdio,
+          logger: logger,
+          terminal: terminal,
+          signals: signals,
+          platform: platform,
+          processInfo: processInfo,
+          fileSystem: testFileSystem,
+        )).run(<String>['attach']);
+        await fakeLogReader.dispose();
+
+        expect(portForwarder.devicePort, null);
+        expect(portForwarder.hostPort, hostPort);
+        expect(hotRunnerFactory.devices, hasLength(1));
+
+        final FlutterDevice flutterDevice = hotRunnerFactory.devices.first;
+        final Uri? observatoryUri = await flutterDevice.observatoryUris?.first;
+        expect(observatoryUri.toString(), 'http://111.111.111.111:123/xyz/');
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+        ProcessManager: () => FakeProcessManager.any(),
+        Logger: () => logger,
+        DeviceManager: () => testDeviceManager,
+        MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
+          mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
+          preliminaryMDnsClient: FakeMDnsClient(
+            <PtrResourceRecord>[
+              PtrResourceRecord('foo', future, domainName: 'srv-foo'),
+            ],
+            <String, List<SrvResourceRecord>>{
+              'srv-foo': <SrvResourceRecord>[
+                SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
+              ],
+            },
+            ipResponse: <String, List<IPAddressResourceRecord>>{
+              'target-foo': <IPAddressResourceRecord>[
+                IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!),
+              ],
+            },
+            txtResponse: <String, List<TxtResourceRecord>>{
+              'srv-foo': <TxtResourceRecord>[
+                TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'),
+              ],
+            },
+          ),
+          logger: logger,
+          flutterUsage: TestUsage(),
+        ),
+      });
+
+      testUsingContext('succeeds with iOS device with mDNS network device with debug-port', () async {
+        final FakeIOSDevice device = FakeIOSDevice(
+          logReader: fakeLogReader,
+          portForwarder: portForwarder,
+          majorSdkVersion: 16,
+          interfaceType: IOSDeviceConnectionInterface.network,
+        );
+        testDeviceManager.devices = <Device>[device];
+        final FakeHotRunner hotRunner = FakeHotRunner();
+        hotRunner.onAttach = (
+          Completer<DebugConnectionInfo>? connectionInfoCompleter,
+          Completer<void>? appStartedCompleter,
+          bool allowExistingDdsInstance,
+          bool enableDevTools,
+        ) async => 0;
+        hotRunner.exited = false;
+        hotRunner.isWaitingForObservatory = false;
+        final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
+          ..hotRunner = hotRunner;
+
+        await createTestCommandRunner(AttachCommand(
+          hotRunnerFactory: hotRunnerFactory,
+          artifacts: artifacts,
+          stdio: stdio,
+          logger: logger,
+          terminal: terminal,
+          signals: signals,
+          platform: platform,
+          processInfo: processInfo,
+          fileSystem: testFileSystem,
+        )).run(<String>['attach', '--debug-port', '123']);
+        await fakeLogReader.dispose();
+
+        expect(portForwarder.devicePort, null);
+        expect(portForwarder.hostPort, hostPort);
+        expect(hotRunnerFactory.devices, hasLength(1));
+
+        final FlutterDevice flutterDevice = hotRunnerFactory.devices.first;
+        final Uri? observatoryUri = await flutterDevice.observatoryUris?.first;
+        expect(observatoryUri.toString(), 'http://111.111.111.111:123/xyz/');
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+        ProcessManager: () => FakeProcessManager.any(),
+        Logger: () => logger,
+        DeviceManager: () => testDeviceManager,
+        MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
+          mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
+          preliminaryMDnsClient: FakeMDnsClient(
+            <PtrResourceRecord>[
+              PtrResourceRecord('bar', future, domainName: 'srv-bar'),
+              PtrResourceRecord('foo', future, domainName: 'srv-foo'),
+            ],
+            <String, List<SrvResourceRecord>>{
+              'srv-bar': <SrvResourceRecord>[
+                SrvResourceRecord('srv-bar', future, port: 321, weight: 1, priority: 1, target: 'target-bar'),
+              ],
+              'srv-foo': <SrvResourceRecord>[
+                SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
+              ],
+            },
+            ipResponse: <String, List<IPAddressResourceRecord>>{
+              'target-foo': <IPAddressResourceRecord>[
+                IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!),
+              ],
+            },
+            txtResponse: <String, List<TxtResourceRecord>>{
+              'srv-foo': <TxtResourceRecord>[
+                TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'),
+              ],
+            },
+          ),
+          logger: logger,
+          flutterUsage: TestUsage(),
+        ),
+      });
+
+      testUsingContext('succeeds with iOS device with mDNS network device with debug-url', () async {
+        final FakeIOSDevice device = FakeIOSDevice(
+          logReader: fakeLogReader,
+          portForwarder: portForwarder,
+          majorSdkVersion: 16,
+          interfaceType: IOSDeviceConnectionInterface.network,
+        );
+        testDeviceManager.devices = <Device>[device];
+        final FakeHotRunner hotRunner = FakeHotRunner();
+        hotRunner.onAttach = (
+          Completer<DebugConnectionInfo>? connectionInfoCompleter,
+          Completer<void>? appStartedCompleter,
+          bool allowExistingDdsInstance,
+          bool enableDevTools,
+        ) async => 0;
+        hotRunner.exited = false;
+        hotRunner.isWaitingForObservatory = false;
+        final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
+          ..hotRunner = hotRunner;
+
+        await createTestCommandRunner(AttachCommand(
+          hotRunnerFactory: hotRunnerFactory,
+          artifacts: artifacts,
+          stdio: stdio,
+          logger: logger,
+          terminal: terminal,
+          signals: signals,
+          platform: platform,
+          processInfo: processInfo,
+          fileSystem: testFileSystem,
+        )).run(<String>['attach', '--debug-url', 'https://0.0.0.0:123']);
+        await fakeLogReader.dispose();
+
+        expect(portForwarder.devicePort, null);
+        expect(portForwarder.hostPort, hostPort);
+        expect(hotRunnerFactory.devices, hasLength(1));
+
+        final FlutterDevice flutterDevice = hotRunnerFactory.devices.first;
+        final Uri? observatoryUri = await flutterDevice.observatoryUris?.first;
+        expect(observatoryUri.toString(), 'http://111.111.111.111:123/xyz/');
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+        ProcessManager: () => FakeProcessManager.any(),
+        Logger: () => logger,
+        DeviceManager: () => testDeviceManager,
+        MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery(
+          mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
+          preliminaryMDnsClient: FakeMDnsClient(
+            <PtrResourceRecord>[
+              PtrResourceRecord('bar', future, domainName: 'srv-bar'),
+              PtrResourceRecord('foo', future, domainName: 'srv-foo'),
+            ],
+            <String, List<SrvResourceRecord>>{
+              'srv-bar': <SrvResourceRecord>[
+                SrvResourceRecord('srv-bar', future, port: 321, weight: 1, priority: 1, target: 'target-bar'),
+              ],
+              'srv-foo': <SrvResourceRecord>[
+                SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
+              ],
+            },
+            ipResponse: <String, List<IPAddressResourceRecord>>{
+              'target-foo': <IPAddressResourceRecord>[
+                IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!),
+              ],
+            },
+            txtResponse: <String, List<TxtResourceRecord>>{
+              'srv-foo': <TxtResourceRecord>[
+                TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'),
+              ],
+            },
+          ),
           logger: logger,
           flutterUsage: TestUsage(),
         ),
@@ -979,9 +1288,16 @@
     DevicePortForwarder? portForwarder,
     DeviceLogReader? logReader,
     this.onGetLogReader,
+    this.interfaceType = IOSDeviceConnectionInterface.none,
+    this.majorSdkVersion = 0,
   }) : _portForwarder = portForwarder, _logReader = logReader;
 
   final DevicePortForwarder? _portForwarder;
+  @override
+  int majorSdkVersion;
+
+  @override
+  final IOSDeviceConnectionInterface interfaceType;
 
   @override
   DevicePortForwarder get portForwarder => _portForwarder!;
@@ -1029,12 +1345,14 @@
 class FakeMDnsClient extends Fake implements MDnsClient {
   FakeMDnsClient(this.ptrRecords, this.srvResponse, {
     this.txtResponse = const <String, List<TxtResourceRecord>>{},
+    this.ipResponse = const <String, List<IPAddressResourceRecord>>{},
     this.osErrorOnStart = false,
   });
 
   final List<PtrResourceRecord> ptrRecords;
   final Map<String, List<SrvResourceRecord>> srvResponse;
   final Map<String, List<TxtResourceRecord>> txtResponse;
+  final Map<String, List<IPAddressResourceRecord>> ipResponse;
   final bool osErrorOnStart;
 
   @override
@@ -1054,7 +1372,7 @@
     ResourceRecordQuery query, {
     Duration timeout = const Duration(seconds: 5),
   }) {
-    if (T == PtrResourceRecord && query.fullyQualifiedName == MDnsObservatoryDiscovery.dartObservatoryName) {
+    if (T == PtrResourceRecord && query.fullyQualifiedName == MDnsVmServiceDiscovery.dartVmServiceName) {
       return Stream<PtrResourceRecord>.fromIterable(ptrRecords) as Stream<T>;
     }
     if (T == SrvResourceRecord) {
@@ -1065,6 +1383,10 @@
       final String key = query.fullyQualifiedName;
       return Stream<TxtResourceRecord>.fromIterable(txtResponse[key] ?? <TxtResourceRecord>[]) as Stream<T>;
     }
+    if (T == IPAddressResourceRecord) {
+      final String key = query.fullyQualifiedName;
+      return Stream<IPAddressResourceRecord>.fromIterable(ipResponse[key] ?? <IPAddressResourceRecord>[]) as Stream<T>;
+    }
     throw UnsupportedError('Unsupported query type $T');
   }
 
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
index 19f2eb2..e4d9e63 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
@@ -21,6 +21,8 @@
 import 'package:flutter_tools/src/dart/pub.dart';
 import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/drive/drive_service.dart';
+import 'package:flutter_tools/src/ios/devices.dart';
+import 'package:flutter_tools/src/ios/iproxy.dart';
 import 'package:flutter_tools/src/project.dart';
 import 'package:package_config/package_config.dart';
 import 'package:test/fake.dart';
@@ -406,6 +408,94 @@
     FileSystem: () => MemoryFileSystem.test(),
     ProcessManager: () => FakeProcessManager.any(),
   });
+
+  testUsingContext('Port publication not disabled for network device', () async {
+    final DriveCommand command = DriveCommand(
+      fileSystem: fileSystem,
+      logger: logger,
+      platform: platform,
+      signals: signals,
+    );
+
+    fileSystem.file('lib/main.dart').createSync(recursive: true);
+    fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
+    fileSystem.file('pubspec.yaml').createSync();
+
+    final Device networkDevice = FakeIosDevice()
+      ..interfaceType = IOSDeviceConnectionInterface.network;
+    fakeDeviceManager.devices = <Device>[networkDevice];
+
+    await expectLater(() => createTestCommandRunner(command).run(<String>[
+      'drive',
+    ]), throwsToolExit());
+
+    final DebuggingOptions options = await command.createDebuggingOptions(false);
+    expect(options.disablePortPublication, false);
+  }, overrides: <Type, Generator>{
+    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+    DeviceManager: () => fakeDeviceManager,
+  });
+
+  testUsingContext('Port publication is disabled for wired device', () async {
+    final DriveCommand command = DriveCommand(
+      fileSystem: fileSystem,
+      logger: logger,
+      platform: platform,
+      signals: signals,
+    );
+
+    fileSystem.file('lib/main.dart').createSync(recursive: true);
+    fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
+    fileSystem.file('pubspec.yaml').createSync();
+
+    await expectLater(() => createTestCommandRunner(command).run(<String>[
+      'drive',
+    ]), throwsToolExit());
+
+    final Device usbDevice = FakeIosDevice()
+      ..interfaceType = IOSDeviceConnectionInterface.usb;
+    fakeDeviceManager.devices = <Device>[usbDevice];
+
+    final DebuggingOptions options = await command.createDebuggingOptions(false);
+    expect(options.disablePortPublication, true);
+  }, overrides: <Type, Generator>{
+    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+    DeviceManager: () => fakeDeviceManager,
+  });
+
+  testUsingContext('Port publication does not default to enabled for network device if flag manually added', () async {
+    final DriveCommand command = DriveCommand(
+      fileSystem: fileSystem,
+      logger: logger,
+      platform: platform,
+      signals: signals,
+    );
+
+    fileSystem.file('lib/main.dart').createSync(recursive: true);
+    fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
+    fileSystem.file('pubspec.yaml').createSync();
+
+    final Device networkDevice = FakeIosDevice()
+      ..interfaceType = IOSDeviceConnectionInterface.network;
+    fakeDeviceManager.devices = <Device>[networkDevice];
+
+    await expectLater(() => createTestCommandRunner(command).run(<String>[
+      'drive',
+      '--no-publish-port'
+    ]), throwsToolExit());
+
+    final DebuggingOptions options = await command.createDebuggingOptions(false);
+    expect(options.disablePortPublication, true);
+  }, overrides: <Type, Generator>{
+    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+    DeviceManager: () => fakeDeviceManager,
+  });
 }
 
 // Unfortunately Device, despite not being immutable, has an `operator ==`.
@@ -577,3 +667,14 @@
   @override
   Stream<io.ProcessSignal> watch() => controller.stream;
 }
+
+// Unfortunately Device, despite not being immutable, has an `operator ==`.
+// Until we fix that, we have to also ignore related lints here.
+// ignore: avoid_implementing_value_types
+class FakeIosDevice extends Fake implements IOSDevice {
+  @override
+  IOSDeviceConnectionInterface interfaceType = IOSDeviceConnectionInterface.usb;
+
+  @override
+  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
+}
diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart
index e957f20..d97a6a2 100644
--- a/packages/flutter_tools/test/general.shard/device_test.dart
+++ b/packages/flutter_tools/test/general.shard/device_test.dart
@@ -11,6 +11,7 @@
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/convert.dart';
 import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/ios/iproxy.dart';
 import 'package:flutter_tools/src/project.dart';
 import 'package:test/fake.dart';
 
@@ -554,6 +555,53 @@
       );
     });
 
+    testWithoutContext('Get launch arguments for physical device with iPv4 network connection', () {
+      final DebuggingOptions original = DebuggingOptions.enabled(
+        BuildInfo.debug,
+      );
+
+      final List<String> launchArguments = original.getIOSLaunchArguments(
+        EnvironmentType.physical,
+        null,
+        <String, Object?>{},
+        interfaceType: IOSDeviceConnectionInterface.network,
+      );
+
+      expect(
+        launchArguments.join(' '),
+        <String>[
+          '--enable-dart-profiling',
+          '--enable-checked-mode',
+          '--verify-entry-points',
+          '--observatory-host=0.0.0.0',
+        ].join(' '),
+      );
+    });
+
+    testWithoutContext('Get launch arguments for physical device with iPv6 network connection', () {
+      final DebuggingOptions original = DebuggingOptions.enabled(
+        BuildInfo.debug,
+      );
+
+      final List<String> launchArguments = original.getIOSLaunchArguments(
+        EnvironmentType.physical,
+        null,
+        <String, Object?>{},
+        ipv6: true,
+        interfaceType: IOSDeviceConnectionInterface.network,
+      );
+
+      expect(
+        launchArguments.join(' '),
+        <String>[
+          '--enable-dart-profiling',
+          '--enable-checked-mode',
+          '--verify-entry-points',
+          '--observatory-host=::0',
+        ].join(' '),
+      );
+    });
+
     testWithoutContext('Get launch arguments for physical device with debugging disabled with available launch arguments', () {
       final DebuggingOptions original = DebuggingOptions.disabled(
         BuildInfo.debug,
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
index c212a08..e26ef2b 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
@@ -19,9 +19,11 @@
 import 'package:flutter_tools/src/ios/ios_deploy.dart';
 import 'package:flutter_tools/src/ios/iproxy.dart';
 import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/mdns_discovery.dart';
 import 'package:test/fake.dart';
 
 import '../../src/common.dart';
+import '../../src/context.dart';
 import '../../src/fake_devices.dart';
 import '../../src/fake_process_manager.dart';
 import '../../src/fakes.dart';
@@ -66,9 +68,10 @@
 FakeCommand attachDebuggerCommand({
   IOSink? stdin,
   Completer<void>? completer,
+  bool isNetworkDevice = false,
 }) {
   return FakeCommand(
-    command: const <String>[
+    command: <String>[
       'script',
       '-t',
       '0',
@@ -79,9 +82,12 @@
       '--bundle',
       '/',
       '--debug',
-      '--no-wifi',
+      if (!isNetworkDevice) '--no-wifi',
       '--args',
-      '--enable-dart-profiling --enable-checked-mode --verify-entry-points',
+      if (isNetworkDevice)
+        '--enable-dart-profiling --enable-checked-mode --verify-entry-points --observatory-host=0.0.0.0'
+      else
+        '--enable-dart-profiling --enable-checked-mode --verify-entry-points',
     ],
     completer: completer,
     environment: const <String, String>{
@@ -188,7 +194,7 @@
     expect(await device.stopApp(iosApp), false);
   });
 
-  testWithoutContext('IOSDevice.startApp prints warning message if discovery takes longer than configured timeout', () async {
+  testWithoutContext('IOSDevice.startApp prints warning message if discovery takes longer than configured timeout for wired device', () async {
     final FileSystem fileSystem = MemoryFileSystem.test();
     final BufferLogger logger = BufferLogger.test();
     final CompleterIOSink stdin = CompleterIOSink();
@@ -226,12 +232,59 @@
     expect(launchResult.started, true);
     expect(launchResult.hasObservatory, true);
     expect(await device.stopApp(iosApp), false);
-    expect(logger.errorText, contains('iOS Observatory not discovered after 30 seconds. This is taking much longer than expected...'));
+    expect(logger.errorText, contains('The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...'));
     expect(utf8.decoder.convert(stdin.writes.first), contains('process interrupt'));
     completer.complete();
     expect(processManager, hasNoRemainingExpectations);
   });
 
+  testUsingContext('IOSDevice.startApp prints warning message if discovery takes longer than configured timeout for wireless device', () async {
+    final FileSystem fileSystem = MemoryFileSystem.test();
+    final BufferLogger logger = BufferLogger.test();
+    final CompleterIOSink stdin = CompleterIOSink();
+    final Completer<void> completer = Completer<void>();
+    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
+      attachDebuggerCommand(stdin: stdin, completer: completer, isNetworkDevice: true),
+    ]);
+    final IOSDevice device = setUpIOSDevice(
+      processManager: processManager,
+      fileSystem: fileSystem,
+      logger: logger,
+      interfaceType: IOSDeviceConnectionInterface.network,
+    );
+    final IOSApp iosApp = PrebuiltIOSApp(
+      projectBundleId: 'app',
+      bundleName: 'Runner',
+      uncompressedBundle: fileSystem.currentDirectory,
+      applicationPackage: fileSystem.currentDirectory,
+    );
+    final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
+
+    device.portForwarder = const NoOpDevicePortForwarder();
+    device.setLogReader(iosApp, deviceLogReader);
+
+    // Start writing messages to the log reader.
+    deviceLogReader.addLine('Foo');
+    deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
+
+    final LaunchResult launchResult = await device.startApp(iosApp,
+      prebuiltApplication: true,
+      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+      platformArgs: <String, dynamic>{},
+      discoveryTimeout: Duration.zero,
+    );
+
+    expect(launchResult.started, true);
+    expect(launchResult.hasObservatory, true);
+    expect(await device.stopApp(iosApp), false);
+    expect(logger.errorText, contains('The Dart VM Service was not discovered after 45 seconds. This is taking much longer than expected...'));
+    expect(logger.errorText, contains('Click "Allow" to the prompt asking if you would like to find and connect devices on your local network.'));
+    completer.complete();
+    expect(processManager, hasNoRemainingExpectations);
+  }, overrides: <Type, Generator>{
+    MDnsVmServiceDiscovery: () => FakeMDnsVmServiceDiscovery(),
+  });
+
   testWithoutContext('IOSDevice.startApp succeeds in release mode', () async {
     final FileSystem fileSystem = MemoryFileSystem.test();
     final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
@@ -505,6 +558,7 @@
   Logger? logger,
   ProcessManager? processManager,
   IOSDeploy? iosDeploy,
+  IOSDeviceConnectionInterface interfaceType = IOSDeviceConnectionInterface.usb,
 }) {
   final Artifacts artifacts = Artifacts.test();
   final FakePlatform macPlatform = FakePlatform(
@@ -542,7 +596,7 @@
       cache: cache,
     ),
     cpuArchitecture: DarwinArch.arm64,
-    interfaceType: IOSDeviceConnectionInterface.usb,
+    interfaceType: interfaceType,
   );
 }
 
@@ -554,3 +608,18 @@
     disposed = true;
   }
 }
+
+class FakeMDnsVmServiceDiscovery extends Fake implements MDnsVmServiceDiscovery {
+  @override
+  Future<Uri?> getVMServiceUriForLaunch(
+    String applicationId,
+    Device device, {
+    bool usesIpv6 = false,
+    int? hostVmservicePort,
+    required int deviceVmservicePort,
+    bool isNetworkDevice = false,
+    Duration timeout = Duration.zero,
+  }) async {
+    return Uri.tryParse('http://0.0.0.0:1234');
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
index 3013f5f..ee97168 100644
--- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
@@ -479,7 +479,7 @@
             stdout: devicesOutput,
           ));
           final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
-          expect(devices, hasLength(3));
+          expect(devices, hasLength(4));
           expect(devices[0].id, '00008027-00192736010F802E');
           expect(devices[0].name, 'An iPhone (Space Gray)');
           expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
@@ -488,10 +488,14 @@
           expect(devices[1].name, 'iPad 1');
           expect(await devices[1].sdkNameAndVersion, 'iOS 10.1 14C54');
           expect(devices[1].cpuArchitecture, DarwinArch.armv7);
-          expect(devices[2].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
-          expect(devices[2].name, 'iPad 2');
+          expect(devices[2].id, '234234234234234234345445687594e089dede3c44');
+          expect(devices[2].name, 'A networked iPad');
           expect(await devices[2].sdkNameAndVersion, 'iOS 10.1 14C54');
           expect(devices[2].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture.
+          expect(devices[3].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
+          expect(devices[3].name, 'iPad 2');
+          expect(await devices[3].sdkNameAndVersion, 'iOS 10.1 14C54');
+          expect(devices[3].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture.
           expect(fakeProcessManager, hasNoRemainingExpectations);
         }, overrides: <Type, Generator>{
           Platform: () => macPlatform,
diff --git a/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart b/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart
index 42fae0e..8bba83f 100644
--- a/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart
+++ b/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart
@@ -17,7 +17,7 @@
 
 void main() {
   group('mDNS Discovery', () {
-    final int year3000 = DateTime(3000).millisecondsSinceEpoch;
+    final int future = DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch;
 
     setUp(() {
       setNetworkInterfaceLister(
@@ -33,209 +33,652 @@
       resetNetworkInterfaceLister();
     });
 
+    group('for attach', () {
+      late MDnsClient emptyClient;
 
-    testWithoutContext('No ports available', () async {
-      final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
+      setUp(() {
+        emptyClient = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
+      });
 
-      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(
-        mdnsClient: client,
-        logger: BufferLogger.test(),
-        flutterUsage: TestUsage(),
-      );
-      final int? port = (await portDiscovery.query())?.port;
-      expect(port, isNull);
+      testWithoutContext('Find result in preliminary client', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
+            ],
+          },
+        );
+
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: emptyClient,
+          preliminaryMDnsClient: client,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+
+        final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
+        expect(result, isNotNull);
+      });
+
+      testWithoutContext('Do not find result in preliminary client, but find in main client', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
+            ],
+          },
+        );
+
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+
+        final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
+        expect(result, isNotNull);
+      });
+
+      testWithoutContext('Find multiple in preliminary client', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+            PtrResourceRecord('baz', future, domainName: 'fiz'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
+            ],
+            'fiz': <SrvResourceRecord>[
+              SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
+            ],
+          },
+        );
+
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: emptyClient,
+          preliminaryMDnsClient: client,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+
+        expect(portDiscovery.queryForAttach, throwsToolExit());
+      });
+
+      testWithoutContext('No ports available', () async {
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: emptyClient,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+
+        final int? port = (await portDiscovery.queryForAttach())?.port;
+        expect(port, isNull);
+      });
+
+      testWithoutContext('Prints helpful message when there is no ipv4 link local address.', () async {
+        final BufferLogger logger = BufferLogger.test();
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: emptyClient,
+          preliminaryMDnsClient: emptyClient,
+          logger: logger,
+          flutterUsage: TestUsage(),
+        );
+        final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
+          '',
+          FakeIOSDevice(),
+        );
+        expect(uri, isNull);
+        expect(logger.errorText, contains('Personal Hotspot'));
+      });
+
+      testWithoutContext('One port available, no appId', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
+            ],
+          },
+        );
+
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        final int? port = (await portDiscovery.queryForAttach())?.port;
+        expect(port, 123);
+      });
+
+      testWithoutContext('One port available, no appId, with authCode', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
+            ],
+          },
+          txtResponse: <String, List<TxtResourceRecord>>{
+            'bar': <TxtResourceRecord>[
+              TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
+            ],
+          },
+        );
+
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
+        expect(result?.port, 123);
+        expect(result?.authCode, 'xyz/');
+      });
+
+      testWithoutContext('Multiple ports available, with appId', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+            PtrResourceRecord('baz', future, domainName: 'fiz'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
+            ],
+            'fiz': <SrvResourceRecord>[
+              SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
+            ],
+          },
+        );
+
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        final int? port = (await portDiscovery.queryForAttach(applicationId: 'fiz'))?.port;
+        expect(port, 321);
+      });
+
+      testWithoutContext('Multiple ports available per process, with appId', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+            PtrResourceRecord('baz', future, domainName: 'fiz'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
+              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
+            ],
+            'fiz': <SrvResourceRecord>[
+              SrvResourceRecord('fiz', future, port: 4321, weight: 1, priority: 1, target: 'local'),
+              SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
+            ],
+          },
+        );
+
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        final int? port = (await portDiscovery.queryForAttach(applicationId: 'bar'))?.port;
+        expect(port, 1234);
+      });
+
+      testWithoutContext('Throws Exception when client throws OSError on start', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[], <String, List<SrvResourceRecord>>{},
+          osErrorOnStart: true,
+        );
+
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        expect(
+          () async => portDiscovery.queryForAttach(),
+          throwsException,
+        );
+      });
+
+      testWithoutContext('Correctly builds VM Service URI with hostVmservicePort == 0', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
+            ],
+          },
+        );
+
+        final FakeIOSDevice device = FakeIOSDevice();
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        final Uri? uri = await portDiscovery.getVMServiceUriForAttach('bar', device, hostVmservicePort: 0);
+        expect(uri.toString(), 'http://127.0.0.1:123/');
+      });
+
+      testWithoutContext('Get network device IP (iPv4)', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
+            ],
+          },
+          ipResponse: <String, List<IPAddressResourceRecord>>{
+            'appId': <IPAddressResourceRecord>[
+              IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('111.111.111.111')!),
+            ],
+          },
+          txtResponse: <String, List<TxtResourceRecord>>{
+            'bar': <TxtResourceRecord>[
+              TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
+            ],
+          },
+        );
+
+        final FakeIOSDevice device = FakeIOSDevice();
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
+          'bar',
+          device,
+          isNetworkDevice: true,
+        );
+        expect(uri.toString(), 'http://111.111.111.111:1234/xyz/');
+      });
+
+      testWithoutContext('Get network device IP (iPv6)', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
+            ],
+          },
+          ipResponse: <String, List<IPAddressResourceRecord>>{
+            'appId': <IPAddressResourceRecord>[
+              IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('1111:1111:1111:1111:1111:1111:1111:1111')!),
+            ],
+          },
+          txtResponse: <String, List<TxtResourceRecord>>{
+            'bar': <TxtResourceRecord>[
+              TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
+            ],
+          },
+        );
+
+        final FakeIOSDevice device = FakeIOSDevice();
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
+          'bar',
+          device,
+          isNetworkDevice: true,
+        );
+        expect(uri.toString(), 'http://[1111:1111:1111:1111:1111:1111:1111:1111]:1234/xyz/');
+      });
+
+      testWithoutContext('Throw error if unable to find VM service with app id and device port', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'srv-foo'),
+            PtrResourceRecord('bar', future, domainName: 'srv-bar'),
+            PtrResourceRecord('baz', future, domainName: 'srv-boo'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'srv-foo': <SrvResourceRecord>[
+              SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
+            ],
+            'srv-bar': <SrvResourceRecord>[
+              SrvResourceRecord('srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar'),
+            ],
+            'srv-baz': <SrvResourceRecord>[
+              SrvResourceRecord('srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz'),
+            ],
+          },
+        );
+        final FakeIOSDevice device = FakeIOSDevice();
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        expect(
+          portDiscovery.getVMServiceUriForAttach(
+            'srv-bar',
+            device,
+            deviceVmservicePort: 321,
+          ),
+          throwsToolExit(
+            message: 'Did not find a Dart VM Service advertised for srv-bar on port 321.'
+          ),
+        );
+      });
+
+      testWithoutContext('Throw error if unable to find VM Service with app id', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'srv-foo'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'srv-foo': <SrvResourceRecord>[
+              SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
+            ],
+          },
+        );
+        final FakeIOSDevice device = FakeIOSDevice();
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          preliminaryMDnsClient: emptyClient,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        expect(
+          portDiscovery.getVMServiceUriForAttach(
+            'srv-asdf',
+            device,
+          ),
+          throwsToolExit(
+            message: 'Did not find a Dart VM Service advertised for srv-asdf.'
+          ),
+        );
+      });
     });
 
-    testWithoutContext('Prints helpful message when there is no ipv4 link local address.', () async {
-      final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
-      final BufferLogger logger = BufferLogger.test();
-      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(
-        mdnsClient: client,
-        logger: logger,
-        flutterUsage: TestUsage(),
-      );
-      final Uri? uri = await portDiscovery.getObservatoryUri(
-        '',
-        FakeIOSDevice(),
-      );
-      expect(uri, isNull);
-      expect(logger.errorText, contains('Personal Hotspot'));
+    group('for launch', () {
+      testWithoutContext('No ports available', () async {
+        final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
+
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+
+        final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForLaunch(
+          applicationId: 'app-id',
+          deviceVmservicePort: 123,
+        );
+
+        expect(result, null);
+      });
+
+      testWithoutContext('Prints helpful message when there is no ipv4 link local address.', () async {
+        final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
+        final BufferLogger logger = BufferLogger.test();
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          logger: logger,
+          flutterUsage: TestUsage(),
+        );
+
+        final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
+          '',
+          FakeIOSDevice(),
+          deviceVmservicePort: 0,
+        );
+        expect(uri, isNull);
+        expect(logger.errorText, contains('Personal Hotspot'));
+      });
+
+      testWithoutContext('Throws Exception when client throws OSError on start', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[], <String, List<SrvResourceRecord>>{},
+          osErrorOnStart: true,
+        );
+
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        expect(
+          () async => portDiscovery.queryForLaunch(applicationId: 'app-id', deviceVmservicePort: 123),
+          throwsException,
+        );
+      });
+
+      testWithoutContext('Correctly builds VM Service URI with hostVmservicePort == 0', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
+            ],
+          },
+        );
+
+        final FakeIOSDevice device = FakeIOSDevice();
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
+          'bar',
+          device,
+          hostVmservicePort: 0,
+          deviceVmservicePort: 123,
+        );
+        expect(uri.toString(), 'http://127.0.0.1:123/');
+      });
+
+      testWithoutContext('Get network device IP (iPv4)', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
+            ],
+          },
+          ipResponse: <String, List<IPAddressResourceRecord>>{
+            'appId': <IPAddressResourceRecord>[
+              IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('111.111.111.111')!),
+            ],
+          },
+          txtResponse: <String, List<TxtResourceRecord>>{
+            'bar': <TxtResourceRecord>[
+              TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
+            ],
+          },
+        );
+
+        final FakeIOSDevice device = FakeIOSDevice();
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
+          'bar',
+          device,
+          isNetworkDevice: true,
+          deviceVmservicePort: 1234,
+        );
+        expect(uri.toString(), 'http://111.111.111.111:1234/xyz/');
+      });
+
+      testWithoutContext('Get network device IP (iPv6)', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'bar'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'bar': <SrvResourceRecord>[
+              SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
+            ],
+          },
+          ipResponse: <String, List<IPAddressResourceRecord>>{
+            'appId': <IPAddressResourceRecord>[
+              IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('1111:1111:1111:1111:1111:1111:1111:1111')!),
+            ],
+          },
+          txtResponse: <String, List<TxtResourceRecord>>{
+            'bar': <TxtResourceRecord>[
+              TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
+            ],
+          },
+        );
+
+        final FakeIOSDevice device = FakeIOSDevice();
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
+          'bar',
+          device,
+          isNetworkDevice: true,
+          deviceVmservicePort: 1234,
+        );
+        expect(uri.toString(), 'http://[1111:1111:1111:1111:1111:1111:1111:1111]:1234/xyz/');
+      });
+
+      testWithoutContext('Throw error if unable to find VM Service with app id and device port', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'srv-foo'),
+            PtrResourceRecord('bar', future, domainName: 'srv-bar'),
+            PtrResourceRecord('baz', future, domainName: 'srv-boo'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'srv-foo': <SrvResourceRecord>[
+              SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
+            ],
+            'srv-bar': <SrvResourceRecord>[
+              SrvResourceRecord('srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar'),
+            ],
+            'srv-baz': <SrvResourceRecord>[
+              SrvResourceRecord('srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz'),
+            ],
+          },
+        );
+        final FakeIOSDevice device = FakeIOSDevice();
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        expect(
+          portDiscovery.getVMServiceUriForLaunch(
+            'srv-bar',
+            device,
+            deviceVmservicePort: 321,
+          ),
+          throwsToolExit(
+              message:'Did not find a Dart VM Service advertised for srv-bar on port 321.'),
+        );
+      });
     });
 
-    testWithoutContext('One port available, no appId', () async {
+    testWithoutContext('Find firstMatchingVmService with many available and no application id', () async {
       final MDnsClient client = FakeMDnsClient(
         <PtrResourceRecord>[
-          PtrResourceRecord('foo', year3000, domainName: 'bar'),
+          PtrResourceRecord('foo', future, domainName: 'srv-foo'),
+          PtrResourceRecord('bar', future, domainName: 'srv-bar'),
+          PtrResourceRecord('baz', future, domainName: 'srv-boo'),
         ],
         <String, List<SrvResourceRecord>>{
-          'bar': <SrvResourceRecord>[
-            SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
+          'srv-foo': <SrvResourceRecord>[
+            SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
+          ],
+          'srv-bar': <SrvResourceRecord>[
+            SrvResourceRecord('srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar'),
+          ],
+          'srv-baz': <SrvResourceRecord>[
+            SrvResourceRecord('srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz'),
           ],
         },
       );
 
-      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(
+      final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
         mdnsClient: client,
         logger: BufferLogger.test(),
         flutterUsage: TestUsage(),
       );
-      final int? port = (await portDiscovery.query())?.port;
-      expect(port, 123);
+      final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(client);
+      expect(result?.domainName, 'srv-foo');
     });
 
-    testWithoutContext('One port available, no appId, with authCode', () async {
+    testWithoutContext('Find firstMatchingVmService app id', () async {
       final MDnsClient client = FakeMDnsClient(
         <PtrResourceRecord>[
-          PtrResourceRecord('foo', year3000, domainName: 'bar'),
+          PtrResourceRecord('foo', future, domainName: 'srv-foo'),
+          PtrResourceRecord('bar', future, domainName: 'srv-bar'),
+          PtrResourceRecord('baz', future, domainName: 'srv-boo'),
         ],
         <String, List<SrvResourceRecord>>{
-          'bar': <SrvResourceRecord>[
-            SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
+          'srv-foo': <SrvResourceRecord>[
+            SrvResourceRecord('srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo'),
           ],
-        },
-        txtResponse: <String, List<TxtResourceRecord>>{
-          'bar': <TxtResourceRecord>[
-            TxtResourceRecord('bar', year3000, text: 'authCode=xyz\n'),
+          'srv-bar': <SrvResourceRecord>[
+            SrvResourceRecord('srv-bar', future, port: 222, weight: 1, priority: 1, target: 'target-bar'),
+            SrvResourceRecord('srv-bar', future, port: 333, weight: 1, priority: 1, target: 'target-bar-2'),
+          ],
+          'srv-baz': <SrvResourceRecord>[
+            SrvResourceRecord('srv-baz', future, port: 444, weight: 1, priority: 1, target: 'target-baz'),
           ],
         },
       );
 
-      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(
+      final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
         mdnsClient: client,
         logger: BufferLogger.test(),
         flutterUsage: TestUsage(),
       );
-      final MDnsObservatoryDiscoveryResult? result = await portDiscovery.query();
-      expect(result?.port, 123);
-      expect(result?.authCode, 'xyz/');
-    });
-
-    testWithoutContext('Multiple ports available, without appId', () async {
-      final MDnsClient client = FakeMDnsClient(
-        <PtrResourceRecord>[
-          PtrResourceRecord('foo', year3000, domainName: 'bar'),
-          PtrResourceRecord('baz', year3000, domainName: 'fiz'),
-        ],
-        <String, List<SrvResourceRecord>>{
-          'bar': <SrvResourceRecord>[
-            SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
-          ],
-          'fiz': <SrvResourceRecord>[
-            SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
-          ],
-        },
+      final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
+        client,
+        applicationId: 'srv-bar'
       );
-
-      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(
-        mdnsClient: client,
-        logger: BufferLogger.test(),
-        flutterUsage: TestUsage(),
-      );
-      expect(portDiscovery.query, throwsToolExit());
-    });
-
-    testWithoutContext('Multiple ports available, with appId', () async {
-      final MDnsClient client = FakeMDnsClient(
-        <PtrResourceRecord>[
-          PtrResourceRecord('foo', year3000, domainName: 'bar'),
-          PtrResourceRecord('baz', year3000, domainName: 'fiz'),
-        ],
-        <String, List<SrvResourceRecord>>{
-          'bar': <SrvResourceRecord>[
-            SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
-          ],
-          'fiz': <SrvResourceRecord>[
-            SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
-          ],
-        },
-      );
-
-      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(
-        mdnsClient: client,
-        logger: BufferLogger.test(),
-        flutterUsage: TestUsage(),
-      );
-      final int? port = (await portDiscovery.query(applicationId: 'fiz'))?.port;
-      expect(port, 321);
-    });
-
-    testWithoutContext('Multiple ports available per process, with appId', () async {
-      final MDnsClient client = FakeMDnsClient(
-        <PtrResourceRecord>[
-          PtrResourceRecord('foo', year3000, domainName: 'bar'),
-          PtrResourceRecord('baz', year3000, domainName: 'fiz'),
-        ],
-        <String, List<SrvResourceRecord>>{
-          'bar': <SrvResourceRecord>[
-            SrvResourceRecord('bar', year3000, port: 1234, weight: 1, priority: 1, target: 'appId'),
-            SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
-          ],
-          'fiz': <SrvResourceRecord>[
-            SrvResourceRecord('fiz', year3000, port: 4321, weight: 1, priority: 1, target: 'local'),
-            SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
-          ],
-        },
-      );
-
-      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(
-        mdnsClient: client,
-        logger: BufferLogger.test(),
-        flutterUsage: TestUsage(),
-      );
-      final int? port = (await portDiscovery.query(applicationId: 'bar'))?.port;
-      expect(port, 1234);
-    });
-
-    testWithoutContext('Query returns null', () async {
-      final MDnsClient client = FakeMDnsClient(
-        <PtrResourceRecord>[],
-         <String, List<SrvResourceRecord>>{},
-      );
-
-      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(
-        mdnsClient: client,
-        logger: BufferLogger.test(),
-        flutterUsage: TestUsage(),
-      );
-      final int? port = (await portDiscovery.query(applicationId: 'bar'))?.port;
-      expect(port, isNull);
-    });
-
-    testWithoutContext('Throws Exception when client throws OSError on start', () async {
-      final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}, osErrorOnStart: true);
-
-
-      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(
-        mdnsClient: client,
-        logger: BufferLogger.test(),
-        flutterUsage: TestUsage(),
-      );
-      expect(
-        () async => portDiscovery.query(),
-        throwsException,
-      );
-    });
-
-    testWithoutContext('Correctly builds Observatory URI with hostVmservicePort == 0', () async {
-      final MDnsClient client = FakeMDnsClient(
-        <PtrResourceRecord>[
-          PtrResourceRecord('foo', year3000, domainName: 'bar'),
-        ],
-        <String, List<SrvResourceRecord>>{
-          'bar': <SrvResourceRecord>[
-            SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
-          ],
-        },
-      );
-
-      final FakeIOSDevice device = FakeIOSDevice();
-      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(
-        mdnsClient: client,
-        logger: BufferLogger.test(),
-        flutterUsage: TestUsage(),
-      );
-      final Uri? uri = await portDiscovery.getObservatoryUri('bar', device, hostVmservicePort: 0);
-      expect(uri.toString(), 'http://127.0.0.1:123/');
+      expect(result?.domainName, 'srv-bar');
+      expect(result?.port, 222);
     });
   });
 }
@@ -243,12 +686,14 @@
 class FakeMDnsClient extends Fake implements MDnsClient {
   FakeMDnsClient(this.ptrRecords, this.srvResponse, {
     this.txtResponse = const <String, List<TxtResourceRecord>>{},
+    this.ipResponse = const <String, List<IPAddressResourceRecord>>{},
     this.osErrorOnStart = false,
   });
 
   final List<PtrResourceRecord> ptrRecords;
   final Map<String, List<SrvResourceRecord>> srvResponse;
   final Map<String, List<TxtResourceRecord>> txtResponse;
+  final Map<String, List<IPAddressResourceRecord>> ipResponse;
   final bool osErrorOnStart;
 
   @override
@@ -268,7 +713,7 @@
     ResourceRecordQuery query, {
     Duration timeout = const Duration(seconds: 5),
   }) {
-    if (T == PtrResourceRecord && query.fullyQualifiedName == MDnsObservatoryDiscovery.dartObservatoryName) {
+    if (T == PtrResourceRecord && query.fullyQualifiedName == MDnsVmServiceDiscovery.dartVmServiceName) {
       return Stream<PtrResourceRecord>.fromIterable(ptrRecords) as Stream<T>;
     }
     if (T == SrvResourceRecord) {
@@ -279,6 +724,10 @@
       final String key = query.fullyQualifiedName;
       return Stream<TxtResourceRecord>.fromIterable(txtResponse[key] ?? <TxtResourceRecord>[]) as Stream<T>;
     }
+    if (T == IPAddressResourceRecord) {
+      final String key = query.fullyQualifiedName;
+      return Stream<IPAddressResourceRecord>.fromIterable(ipResponse[key] ?? <IPAddressResourceRecord>[]) as Stream<T>;
+    }
     throw UnsupportedError('Unsupported query type $T');
   }