Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[multicast_dns] Optimized Socket Binding: Always bind to 0.0.0.0 for …
…simplicity and efficiency - flutter#79772 (flutter#6700) 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 <img width="565" alt="image" src="https://github.com/flutter/packages/assets/36191829/357c4d75-cc04-4848-ad88-757c9df25ad4"> Where Raspberry pi runs a `mDNS` service using the following `go`: <details><summary>main.go</summary> <p> ```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) } ``` </p> </details> Considering the following client (which I got from [here](https://pub.dev/packages/multicast_dns/example)): <details><summary>client.dart</summary> <p> ```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 "[email protected]._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.'); } ``` </p> </details> # 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 `IP`s: - `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 `QM`queries 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. ```shell 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: ```dart final RawDatagramSocket incoming = await _rawDatagramSocketFactory( listenAddress.address, selectedMDnsPort, reuseAddress: true, reusePort: true, ttl: 255, ); ``` to ``` dart 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: ```shell 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: ```shell 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 is`RFC 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: ```shell 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: ```shell 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 `IP`s 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`. <details><summary>multicast_dns.start() function</summary> <p> ```dart 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; } ``` </p> </details> The idea is indeed to let the first 3 `IP`s 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 ```shell (ip.src==192.168.2.7 || ip.src==192.168.2.16) && udp.port eq 5353 ``` # Related Issue - It should resolves issue [flutter#79772](flutter#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.
- Loading branch information