Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[multicast_dns] Optimized Socket Binding: Always bind to 0.0.0.0 for simplicity and efficiency - #79772 #6700

Merged
merged 49 commits into from
Jun 24, 2024

Conversation

biagiopietro
Copy link
Contributor

@biagiopietro biagiopietro commented May 9, 2024

I encountered this package long time ago (see linked issue below) and there were cases where it wasn't working.
After 3 years (yeah it's more time than expected) I managed to find the time to dust off wireshark and have look again.

Preamble

Considering the following setup

image

Where Raspberry pi runs a mDNS service using the following go:

main.go

package main

import (
	"fmt"
	"github.com/hashicorp/mdns"
	"os"
  "net/http"
)

func health(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "ok")
}

func main() {

	hostname, err := os.Hostname()

	if err != nil {
		panic(fmt.Sprintf("Error getting current hostname, description: %s", err.Error()))
	}

	info := []string{"mDNS get server"}

	service, err := mdns.NewMDNSService(hostname, "_test._tcp", "", "", 8080, nil, info)

	if err != nil {
		panic(fmt.Sprintf("Error while exporting the service, description: %s", err.Error()))
	}

	server, err := mdns.NewServer(&mdns.Config{Zone: service})

	if err != nil {
		panic(fmt.Sprintf("Error while setting the discover server up, description: %s", err.Error()))
	}

	defer server.Shutdown()
	
	http.HandleFunc("/", health)
	http.ListenAndServe(":8081",nil)
}

Considering the following client (which I got from here):

client.dart

// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Example script to illustrate how to use the mdns package to discover the port
// of a Dart observatory over mDNS.

// ignore_for_file: avoid_print

import 'package:multicast_dns/multicast_dns.dart';

Future<void> main() async {
  // Parse the command line arguments.

  const String name = '_test._tcp.local';
  final MDnsClient client = MDnsClient();
  // Start the client with default options.
  await client.start();

  // Get the PTR record for the service.
  await for (final PtrResourceRecord ptr in client
      .lookup<PtrResourceRecord>(ResourceRecordQuery.serverPointer(name))) {
    // Use the domainName from the PTR record to get the SRV record,
    // which will have the port and local hostname.
    // Note that duplicate messages may come through, especially if any
    // other mDNS queries are running elsewhere on the machine.
    await for (final SrvResourceRecord srv in client.lookup<SrvResourceRecord>(
        ResourceRecordQuery.service(ptr.domainName))) {
      // Domain name will be something like "io.flutter.example@some-iphone.local._dartobservatory._tcp.local"
      final String bundleId =
          ptr.domainName; //.substring(0, ptr.domainName.indexOf('@'));
      print('Dart observatory instance found at '
          '${srv.target}:${srv.port} for "$bundleId".');
    }
  }
  client.stop();

  print('Done.');
}

What happens

When running the client script (dart run client.dart so with the latest package of multicast_dns package) as is, a list of sockets is created which are bind to port 5353 and IPs:

  • 0.0.0.0;
  • 127.0.0.1;
  • 192.168.2.16;
  • 172.17.0.1;

a list of interfaces (see list below) are joined to the multicast socket which is bound to 0.0.0.0:5353:

  • lo (with address 127.0.0.1);
  • wlan0 (with address 192.168.2.16);
  • docker0 (with address 172.17.0.1).

and eventually when lookup function is being called QMqueries are being sent from ALL the sockets in the list; which means that for 0.0.0.0 the IP address chosen by the operating system will depend on various factors such as the routing table, the default network interface, and the specific configuration of the network interfaces on the machine. It could be sent from any of the IP addresses associated with the machine's network interfaces, including IP addresses assigned to physical network adapters or virtual interfaces.

Using Wireshark, I can see that 2 QM packets are being sent and I can see that mDNS service is responding to the client with proper packet but it seems that the socket opened at 0.0.0.0:5353 is not reading them at all even though the socket is still open.

Source	        Destination	Protocol	Length	Info
192.168.2.16	224.0.0.251	MDNS		    76	Standard query 0x0000 PTR _test._tcp.local, "QM" question
192.168.2.16	224.0.0.251	MDNS		    76	Standard query 0x0000 PTR _test._tcp.local, "QM" question
192.168.2.7	192.168.2.16	MDNS		   180	Standard query response 0x0000 PTR mdnsserv._test._tcp.local SRV 10 1 8080 mdnsserv A 127.0.1.1 TXT
192.168.2.7	192.168.2.16	MDNS		   180	Standard query response 0x0000 PTR mdnsserv._test._tcp.local SRV 10 1 8080 mdnsserv A 127.0.1.1 TXT

First approach (not sure if it's RFC 6762 friendly)

I had the "feeling" that sending QM packets to 0.0.0.0:5353 and other interfaces on the same port would generate some sort of unexpected behavior due to the nature of 0.0.0.0 which IP selections depends on multiple factors.

Therefore I tried initially to change the incoming socket (the one bound to 0.0.0.0) from:

final RawDatagramSocket incoming = await _rawDatagramSocketFactory(
      listenAddress.address,
      selectedMDnsPort,
      reuseAddress: true,
      reusePort: true,
      ttl: 255,
    );

to

final RawDatagramSocket incoming = await _rawDatagramSocketFactory(
      listenAddress.address,
      0,
      reuseAddress: true,
      reusePort: true,
      ttl: 255,
    );

which essentially delegates to OS to choose a random port (instead of forcing 5353).

In this case the client managed to process correctly all the packages for discovering the mDNS service, indeed in Wireshark I could see:

Source	        Destination	Protocol	Length	Info
192.168.2.16	224.0.0.251	MDNS		    76	Standard query 0x0000 PTR _test._tcp.local, "QM" question
192.168.2.16	224.0.0.251	MDNS		    76	Standard query 0x0000 PTR _test._tcp.local, "QM" question
192.168.2.7	192.168.2.16	MDNS		   180	Standard query response 0x0000 PTR mdnsserv._test._tcp.local SRV 10 1 8080 mdnsserv A 127.0.1.1 TXT
192.168.2.7	192.168.2.16	MDNS		   180	Standard query response 0x0000 PTR mdnsserv._test._tcp.local SRV 10 1 8080 mdnsserv A 127.0.1.1 TXT
192.168.2.16	224.0.0.251	MDNS		    85	Standard query 0x0000 SRV mdnsserv._test._tcp.local, "QM" question
192.168.2.16	224.0.0.251	MDNS		    85	Standard query 0x0000 SRV mdnsserv._test._tcp.local, "QM" question
192.168.2.7	192.168.2.16	MDNS		   123	Standard query response 0x0000 SRV 10 1 8080 mdnsserv A 127.0.1.1
192.168.2.7	192.168.2.16	MDNS		   123	Standard query response 0x0000 SRV 10 1 8080 mdnsserv A 127.0.1.1

and on the client I could see the message:

Dart observatory instance found at mdnsserv:8080 for "mdnsserv._test._tcp.local"

⚠️ : Again, I'm not sure if it can be considered a solution because I dunno isRFC 6762 friendly, I checked some packages which implement mDNS clients and I saw some of them doing what I proposed. I would like to hear comments about it.

Second approach (which it's what is presented in this PR)

After trying the first approach I realized that maybe there is no need to open sockets on more interfaces (and therefore send QM messages) it maybe be enough to send and listen only on a socket bound to 0.0.0.0 since, again, listen on ANY IP and send packets from a selected IP address chosen by the OS.

Also in this case the client managed to process correctly all the packages for discovering the mDNS service, indeed in Wireshark I could see:

Source	        Destination	Protocol	Length	Info
192.168.2.16	224.0.0.251	MDNS		    76	Standard query 0x0000 PTR _test._tcp.local, "QM" question
192.168.2.7	192.168.2.16	MDNS		   180	Standard query response 0x0000 PTR mdnsserv._test._tcp.local SRV 10 1 8080 mdnsserv A 127.0.1.1 TXT
192.168.2.16	224.0.0.251	MDNS		    85	Standard query 0x0000 SRV mdnsserv._test._tcp.local, "QM" question
192.168.2.7	192.168.2.16	MDNS		   123	Standard query response 0x0000 SRV 10 1 8080 mdnsserv A 127.0.1.1

and on the client I could see the message:

Dart observatory instance found at mdnsserv:8080 for "mdnsserv._test._tcp.local"

Third approach (It did not work but mentioning for completeness)

The idea here is to don't send QM packets via 0.0.0.0 but just listen on possible response/s since packets would be send via the following IPs and 0.0.0.0 should represent ANY IP.

  • 127.0.0.1;
  • 192.168.2.16;
  • 172.17.0.1.

Fourth approach (It did not work but mentioning for completeness)

Another solution that I tried but unfortunately it did not work, was to put 0.0.0.0 as last item in the socket list so QM packets would be sent according to the following order:

  • 127.0.0.1;
  • 192.168.2.16;
  • 172.17.0.1;
  • 0.0.0.0.
multicast_dns.start() function

  Future<void> start({
    InternetAddress? listenAddress,
    NetworkInterfacesFactory? interfacesFactory,
    int mDnsPort = mDnsPort,
    InternetAddress? mDnsAddress,
  }) async {
    listenAddress ??= InternetAddress.anyIPv4;
    interfacesFactory ??= allInterfacesFactory;

    assert(listenAddress.address == InternetAddress.anyIPv4.address ||
        listenAddress.address == InternetAddress.anyIPv6.address);

    if (_started || _starting) {
      return;
    }
    _starting = true;

    final int selectedMDnsPort = _mDnsPort = mDnsPort;
    _mDnsAddress = mDnsAddress;

    // Listen on all addresses.
    final RawDatagramSocket incoming = await _rawDatagramSocketFactory(
      listenAddress.address,
      selectedMDnsPort,
      reuseAddress: true,
      reusePort: true,
      ttl: 255,
    );

    _mDnsAddress ??= incoming.address.type == InternetAddressType.IPv4
        ? mDnsAddressIPv4
        : mDnsAddressIPv6;

    final List<NetworkInterface> interfaces =
        (await interfacesFactory(listenAddress.type)).toList();

    for (final NetworkInterface interface in interfaces) {
      // Create a socket for sending on each adapter.
      final InternetAddress targetAddress = interface.addresses[0];
      // Join multicast on this interface.
      incoming.joinMulticast(_mDnsAddress!, interface);
    }

    // Can't send to IPv6 any address.
    if (incoming.address != InternetAddress.anyIPv6) {
      _sockets.add(incoming);
    } else {
      _toBeClosed.add(incoming);
    }

    incoming.listen((RawSocketEvent event) => _handleIncoming(event, incoming));
    _started = true;
    _starting = false;
  }

The idea is indeed to let the first 3 IPs to send the QM packets which response should be hopefully captured by the incoming socket before the socket on 0.0.0.0 would send the QM packet too.

Wireshark filter

(ip.src==192.168.2.7 || ip.src==192.168.2.16) && udp.port eq 5353

Related Issue

  • It should resolves issue #79772

Disclaimers

  • I'm not expert in flutter/dart, I pulled the code and I tried to debug it with help of uncle google and print();
  • I don't have a huge expertise in networking but I know how to play a bit with Wireshark, inspect the networks and craft packets.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@biagiopietro biagiopietro marked this pull request as draft May 9, 2024 08:00
@biagiopietro biagiopietro marked this pull request as ready for review May 9, 2024 11:52
@jmagman jmagman changed the title [multicast_mdns] Optimized Socket Binding: Consolidated to 0.0.0.0 for Simplicity and Efficiency - #79772 [multicast_dns] Optimized Socket Binding: Consolidated to 0.0.0.0 for Simplicity and Efficiency - #79772 May 9, 2024
@jmagman jmagman linked an issue May 9, 2024 that may be closed by this pull request
@jmagman jmagman requested a review from cbracken May 23, 2024 19:27
Copy link
Member

@jmagman jmagman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the delay reviewing this.

@vashworth kindly tested this out against the flutter CLI and confirmed it didn't break wireless debugging for flutter run or flutter attach.

This change generally LGTM, though I would like a second opinion from @cbracken who might remember more about why the original code was written that way.

  1. This needs a pubspec version bump and CHANGELOG update. See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#version-and-changelog-updates
  2. Can you add a test to https://github.com/flutter/packages/blob/main/packages/multicast_dns/test/client_test.dart?

@cbracken
Copy link
Member

Taking a look!

@cbracken
Copy link
Member

I did a bit of exploratory git blameing to see if I could figure out the history of the code in question in case there's any additional useful info in log comments (there wasn't), but for the record:

The existing code was derived from the (long-dead, now) Dartino code and was added in
a062a28

It's derived from older Dartino code from the Dart SDK. The most recent version of that can be viewed here:
https://github.com/dart-archive/sdk/tree/master/pkg/mdns

The Dartino mdns package was originally added in this commit:
dart-archive/sdk@f0d30ec

Looking at the actual logic in the context of the rest of the code now.

@cbracken
Copy link
Member

cbracken commented May 23, 2024

Thanks for sending the patch.

Thinking about this a bit, I agree that setting up a single socket on 0.0.0.0 should be sufficient for listening on all network interfaces. As you note, for sending, we'll rely on the operating system's routing to determine which network interface is used for sending. That all seems fine.

The current package doesn't really allow for interface-specific tuning/filtering so I don't think we're losing anything here and the code and resource management is significantly simpler.

@sgjesse wrote the original code and while he's moved on to other projects, may have thoughts. :)

The existing tests should cover the majority of the behaviour here, but as @jmagman says, please add a test that verifies the change in behaviour. If you check the existing tests, you can see how to use the rawDatagramSocketFactory and interfacesFactory constructor parameters to hook socket creation and listing network interfaces.

You could probably write a test where the interfacesFactory lists a bunch of interfaces and the rawDatagramSocketFactor:

  1. increments a counter each time it's called
  2. records the targetAddress in a list

then verify that the counter is == 1 and that the only targetAddress passed in was InternetAddress.anyIPv4.address or InternetAddress.anyIPv6.address, whichever you passed in for mDnsAddress in the constructor.

Copy link
Member

@cbracken cbracken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good modulo comments + as @jmagman says, please add a test (suggestion above).

packages/multicast_dns/CHANGELOG.md Outdated Show resolved Hide resolved
packages/multicast_dns/CHANGELOG.md Outdated Show resolved Hide resolved
packages/multicast_dns/lib/multicast_dns.dart Show resolved Hide resolved
Copy link
Member

@cbracken cbracken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this looks great for IPv4. We'll want to either document the removal of IPv6 support (if the previous code didn't exist) or add it back (if it did).

Copy link
Member

@cbracken cbracken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the delay while I was out on vacation and thank you so much for your patience in landing this improvement! Looks great to me.

LGTM stamp from a Japanese personal seal

@jtmcdole
Copy link

Just a drive by comment; thanks for looking into this problem!

First observation: 0.0.0.0 is not a multicast address; its a signal to bind to any local address (unicast). The reuseaddr/reuseport I believe will not share data with other listeners and its up to the OS to deliver the packets to {random|first} listener.

In the wireshark captures:

  1. It looks like we are successfully sending out a QM - query multicast; the response should be made to the multicast address
  2. The responses look unicast and probably only delivered to the first listening socket.
192.168.2.7	192.168.2.16	MDNS		   180	Standard query response 0x0000 PTR mdnsserv._test._tcp.local SRV 10 1 8080 mdnsserv A 127.0.1.1 TXT
192.168.2.7	192.168.2.16	MDNS		   180	Standard query response 0x0000 PTR mdnsserv._test._tcp.local SRV 10 1 8080 mdnsserv A 127.0.1.1 TXT

Are there any other clients listening on your system (e.g. avahi)?

For direct-unicode messages, I believe it is OK to use a random listening port. Your first attempt looked fine. I believe this falls under 5.1 for one-shot multicast DNS queries and specifically called out:

These queries are typically done using a high-numbered ephemeral UDP source port, but regardless of whether they are sent from a dynamic port or from a fixed port, these queries MUST NOT be sent using UDP source port 5353, since using UDP source port 5353 signals the presence of a fully compliant Multicast DNS querier, as described below.

Let me dig a little deeper?

@jtmcdole
Copy link

OK, yes: In the class description:

/// This client only supports "One-Shot Multicast DNS Queries" as described in
/// section 5.1 of RFC 6762.

Which that section specifically states:

these queries MUST NOT be sent using UDP source port 5353

We are not in compliance with this and the port should be random.

@biagiopietro
Copy link
Contributor Author

biagiopietro commented Jun 24, 2024

Thanks for looking into it @jtmcdole.

The class states that it's "one shot" but it has never been so since the port is always 5353.

I don't feel this should be changed in this PR (I would rather add the option to be "one shot" mode and keep as default what we have now 🤷‍♂️ ).

Note: could you please point me to the comment where I state that 0.0.0.0 is a multicast address? It's an old PR 😅

@jtmcdole
Copy link

caveat emptor: I'm not an mdns expert.

The class states that it's "one shot" but it has never been so since the port is always 5353.

I missed the incoming.joinMulticast() at the bottom of the interface loop - so you're right on that point. We should update our documentation on that point. For it to be 5.1 One-Shot Multicast DNS Queries, we would be listening to a unicast only socket and send QU queries.

What's interesting and might just be a quirk of the mdns server - we get non-multicast responses back in your wireshark logs. Or maybe wireshark is just not showing the destination multicast address on received packets (I'm also not a wireshark expert)?

I don't feel this should be changed in this PR (I would rather add the option to be "one shot" mode and keep as default what we have now 🤷‍♂️ ).

Sounds good to me. the rest of the PR is LGTM.

Note: could you please point me to the comment where I state that 0.0.0.0 is a multicast address? It's an old PR 😅

A misreading of your description and our documentation.

@jmagman jmagman added autosubmit Merge PR when tree becomes green via auto submit App labels Jun 24, 2024
@auto-submit auto-submit bot merged commit b4d79b5 into flutter:main Jun 24, 2024
74 checks passed
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Jun 25, 2024
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Jun 25, 2024
flutter/packages@711b4ac...03f5f6d

2024-06-24 stuartmorgan@google.com [interactive_media_ads] Fix README badge image URL (flutter/packages#6979)
2024-06-24 36191829+biagiopietro@users.noreply.github.com [multicast_dns] Optimized Socket Binding: Always bind to 0.0.0.0 for simplicity and efficiency - #79772 (flutter/packages#6700)
2024-06-24 92950982+oravecz-jpmc@users.noreply.github.com [flutter_adaptive_scaffold] Allows for the animation duration to be adjusted using SlotLayout.from() (flutter/packages#6510)
2024-06-22 louisehsu@google.com [in_app_purchase_storekit] Remove OCMock (flutter/packages#6862)
2024-06-22 stuartmorgan@google.com [google_maps_flutter] Add iOS SDK 9.x support (flutter/packages#6902)
2024-06-21 stuartmorgan@google.com [google_maps_flutter] Partial Android host API Pigeon conversion (flutter/packages#6967)
2024-06-21 github@alexv525.com Revert "Migrate `camera/android` from `SurfaceTexture`->`SurfaceProducer`." (flutter/packages#6964)
2024-06-21 stuartmorgan@google.com [quick_actions] Update to Pigeon 20 (flutter/packages#6961)
2024-06-20 stuartmorgan@google.com [google_maps_flutter] Move Android inspector to Pigeon (flutter/packages#6958)
2024-06-20 engine-flutter-autoroll@skia.org Manual roll Flutter from ccf3abe to 6c06abb (21 revisions) (flutter/packages#6954)
2024-06-20 34871572+gmackall@users.noreply.github.com [many] More v1 embedding deletion that was missed in flutter/packages#6494 (flutter/packages#6923)
2024-06-20 joonas.kerttula@codemate.com [google_maps_flutter] deprecate old BitmapDescriptor methods (flutter/packages#6905)
2024-06-18 magder@google.com [pigeon] Fully-qualify types in Equatable extension test (flutter/packages#6946)
2024-06-18 jimmyxx@gmail.com [flutter_markdown] fixes null check operator used on null value if onSelectionChanged is� (flutter/packages#6883)
2024-06-17 engine-flutter-autoroll@skia.org Roll Flutter from 5187cab to ccf3abe (6 revisions) (flutter/packages#6940)
2024-06-17 50375243+Jerinji2016@users.noreply.github.com [google_sign_in_web] README.md typo (flutter/packages#6642)
2024-06-17 49699333+dependabot[bot]@users.noreply.github.com [camera]: Bump com.google.guava:guava from 32.0.1-android to 33.2.1-android and CameraX version to 1.3.4 in /packages/camera/camera_android_camerax/android (flutter/packages#6847)
2024-06-17 49699333+dependabot[bot]@users.noreply.github.com [sign_in]: Bump com.google.guava:guava from 32.0.1-android to 33.2.1-android in /packages/google_sign_in/google_sign_in_android/android (flutter/packages#6846)
2024-06-17 49699333+dependabot[bot]@users.noreply.github.com [quick_actions]: Bump com.android.tools.build:gradle from 7.2.1 to 8.4.1 in /packages/quick_actions/quick_actions_android/android (flutter/packages#6799)
2024-06-17 49699333+dependabot[bot]@users.noreply.github.com [path_provider]: Bump androidx.annotation:annotation from 1.7.1 to 1.8.0 in /packages/path_provider/path_provider_android/android (flutter/packages#6773)
2024-06-17 49699333+dependabot[bot]@users.noreply.github.com [camera]: Bump androidx.annotation:annotation from 1.7.1 to 1.8.0 in /packages/camera/camera_android/android (flutter/packages#6766)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC flutter-ecosystem@google.com,rmistry@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
autosubmit Merge PR when tree becomes green via auto submit App p: multicast_dns
Projects
None yet
Development

Successfully merging this pull request may close these issues.

multicast_dns 0.2.0 not detecting mDNS packets
5 participants