Skip to content

Commit

Permalink
[multicast_dns] Optimized Socket Binding: Always bind to 0.0.0.0 for …
Browse files Browse the repository at this point in the history
…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
biagiopietro authored Jun 24, 2024
1 parent fdb78fc commit b4d79b5
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 31 deletions.
3 changes: 2 additions & 1 deletion packages/multicast_dns/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 0.3.2+7

* Optimized Socket Binding: Always bind to 0.0.0.0 for simplicity and efficiency.
* Updates minimum supported SDK version to Flutter 3.16/Dart 3.2.

## 0.3.2+6
Expand Down
2 changes: 2 additions & 0 deletions packages/multicast_dns/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Multicast DNS package

Based on [RFC 6762 Multicast DNS](https://datatracker.ietf.org/doc/html/rfc6762).

[![pub package](https://img.shields.io/pub/v/multicast_dns.svg)](
https://pub.dartlang.org/packages/multicast_dns)

Expand Down
57 changes: 28 additions & 29 deletions packages/multicast_dns/lib/multicast_dns.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ class MDnsClient {

bool _starting = false;
bool _started = false;
final List<RawDatagramSocket> _sockets = <RawDatagramSocket>[];
final List<RawDatagramSocket> _toBeClosed = <RawDatagramSocket>[];
RawDatagramSocket? _incomingIPv4;
final List<RawDatagramSocket> _ipv6InterfaceSockets = <RawDatagramSocket>[];
final LookupResolver _resolver = LookupResolver();
final ResourceRecordCache _cache = ResourceRecordCache();
final RawDatagramSocketFactory _rawDatagramSocketFactory;
Expand Down Expand Up @@ -117,9 +117,9 @@ class MDnsClient {

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

_mDnsAddress ??= incoming.address.type == InternetAddressType.IPv4
Expand All @@ -130,30 +130,25 @@ class MDnsClient {
(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];
final RawDatagramSocket socket = await _rawDatagramSocketFactory(
targetAddress,
selectedMDnsPort,
reuseAddress: true,
reusePort: true,
ttl: 255,
);
_sockets.add(socket);

// Ensure that we're using this address/interface for multicast.
if (targetAddress.type == InternetAddressType.IPv4) {
socket.setRawOption(RawSocketOption(
RawSocketOption.levelIPv4,
RawSocketOption.IPv4MulticastInterface,
targetAddress.rawAddress,
));
} else {
if (targetAddress.type == InternetAddressType.IPv6) {
final RawDatagramSocket socket = await _rawDatagramSocketFactory(
targetAddress,
selectedMDnsPort,
reuseAddress: true,
reusePort: true,
ttl: 255,
);
_ipv6InterfaceSockets.add(socket);
socket.setRawOption(RawSocketOption.fromInt(
RawSocketOption.levelIPv6,
RawSocketOption.IPv6MulticastInterface,
interface.index,
));
}

// Join multicast on this interface.
incoming.joinMulticast(_mDnsAddress!, interface);
}
Expand All @@ -171,15 +166,13 @@ class MDnsClient {
throw StateError('Cannot stop mDNS client while it is starting.');
}

for (final RawDatagramSocket socket in _sockets) {
socket.close();
}
_sockets.clear();
_incomingIPv4?.close();
_incomingIPv4 = null;

for (final RawDatagramSocket socket in _toBeClosed) {
for (final RawDatagramSocket socket in _ipv6InterfaceSockets) {
socket.close();
}
_toBeClosed.clear();
_ipv6InterfaceSockets.clear();

_resolver.clearPendingRequests();

Expand Down Expand Up @@ -219,11 +212,17 @@ class MDnsClient {
final Stream<T> results = _resolver.addPendingRequest<T>(
query.resourceRecordType, query.fullyQualifiedName, timeout);

// Send the request on all interfaces.
final List<int> packet = query.encode();
for (final RawDatagramSocket socket in _sockets) {
socket.send(packet, _mDnsAddress!, selectedMDnsPort);

if (_mDnsAddress?.type == InternetAddressType.IPv4) {
// Send and listen on same "ANY" interface
_incomingIPv4?.send(packet, _mDnsAddress!, selectedMDnsPort);
} else {
for (final RawDatagramSocket socket in _ipv6InterfaceSockets) {
socket.send(packet, _mDnsAddress!, selectedMDnsPort);
}
}

return results;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/multicast_dns/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: multicast_dns
description: Dart package for performing mDNS queries (e.g. Bonjour, Avahi).
repository: https://github.com/flutter/packages/tree/main/packages/multicast_dns
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+multicast_dns%22
version: 0.3.2+6
version: 0.3.2+7

environment:
sdk: ^3.2.0
Expand Down
98 changes: 98 additions & 0 deletions packages/multicast_dns/test/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,78 @@ void main() {
await client.start();
await client.lookup(ResourceRecordQuery.serverPointer('_')).toList();
});

group('Bind a single socket to ANY IPv4 and more than one when IPv6', () {
final List<Map<String, Object>> testCases = <Map<String, Object>>[
<String, Object>{
'name': 'IPv4',
'datagramSocketType': InternetAddress.anyIPv4,
'interfacePrefix': '192.168.2.'
},
<String, Object>{
'name': 'IPv6',
'datagramSocketType': InternetAddress.anyIPv6,
'interfacePrefix': '2001:0db8:85a3:0000:0000:8a2e:7335:030'
}
];

for (final Map<String, Object> testCase in testCases) {
test('Bind a single socket to ANY ${testCase["name"]}', () async {
final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();

datagramSocket.address =
testCase['datagramSocketType']! as InternetAddress;

final List<dynamic> selectedInterfacesForSendingPackets = <dynamic>[];
final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
(dynamic host, int port,
{bool reuseAddress = true,
bool reusePort = true,
int ttl = 1}) async {
selectedInterfacesForSendingPackets.add(host);
return datagramSocket;
});

const int numberOfFakeInterfaces = 10;
Future<Iterable<NetworkInterface>> fakeNetworkInterfacesFactory(
InternetAddressType type) async {
final List<NetworkInterface> fakeInterfaces = <NetworkInterface>[];

// Generate "fake" interfaces
for (int i = 0; i < numberOfFakeInterfaces; i++) {
fakeInterfaces.add(FakeNetworkInterface(
'inetfake$i',
<InternetAddress>[
InternetAddress("${testCase['interfacePrefix']! as String}$i")
],
0,
));
}

// ignore: always_specify_types
return Future.value(fakeInterfaces);
}

final InternetAddress listenAddress =
testCase['datagramSocketType']! as InternetAddress;

await client.start(
listenAddress: listenAddress,
mDnsPort: 1234,
interfacesFactory: fakeNetworkInterfacesFactory);
client.stop();

if (testCase['datagramSocketType'] == InternetAddress.anyIPv4) {
expect(selectedInterfacesForSendingPackets.length, 1);
} else {
// + 1 because of unspecified address (::)
expect(selectedInterfacesForSendingPackets.length,
numberOfFakeInterfaces + 1);
}
expect(selectedInterfacesForSendingPackets[0], listenAddress.address);
});
}
});
}

class FakeRawDatagramSocket extends Fake implements RawDatagramSocket {
Expand Down Expand Up @@ -113,4 +185,30 @@ class FakeRawDatagramSocket extends Fake implements RawDatagramSocket {
int send(List<int> buffer, InternetAddress address, int port) {
return buffer.length;
}

@override
void joinMulticast(InternetAddress group, [NetworkInterface? interface]) {
// nothing to do here
}
@override
void setRawOption(RawSocketOption option) {
// nothing to do here
}
}

class FakeNetworkInterface implements NetworkInterface {
FakeNetworkInterface(this._name, this._addresses, this._index);

final String _name;
final List<InternetAddress> _addresses;
final int _index;

@override
List<InternetAddress> get addresses => _addresses;

@override
String get name => _name;

@override
int get index => _index;
}

0 comments on commit b4d79b5

Please sign in to comment.