diff --git a/packages/multicast_dns/CHANGELOG.md b/packages/multicast_dns/CHANGELOG.md index 9cb48301ba2f6..6db175a0db9fa 100644 --- a/packages/multicast_dns/CHANGELOG.md +++ b/packages/multicast_dns/CHANGELOG.md @@ -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 diff --git a/packages/multicast_dns/README.md b/packages/multicast_dns/README.md index 10cd33e4bb636..c161b6fd98d42 100644 --- a/packages/multicast_dns/README.md +++ b/packages/multicast_dns/README.md @@ -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) diff --git a/packages/multicast_dns/lib/multicast_dns.dart b/packages/multicast_dns/lib/multicast_dns.dart index 336ac427930e8..d6e2e6b06c0b9 100644 --- a/packages/multicast_dns/lib/multicast_dns.dart +++ b/packages/multicast_dns/lib/multicast_dns.dart @@ -48,8 +48,8 @@ class MDnsClient { bool _starting = false; bool _started = false; - final List _sockets = []; - final List _toBeClosed = []; + RawDatagramSocket? _incomingIPv4; + final List _ipv6InterfaceSockets = []; final LookupResolver _resolver = LookupResolver(); final ResourceRecordCache _cache = ResourceRecordCache(); final RawDatagramSocketFactory _rawDatagramSocketFactory; @@ -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 @@ -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); } @@ -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(); @@ -219,11 +212,17 @@ class MDnsClient { final Stream results = _resolver.addPendingRequest( query.resourceRecordType, query.fullyQualifiedName, timeout); - // Send the request on all interfaces. final List 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; } diff --git a/packages/multicast_dns/pubspec.yaml b/packages/multicast_dns/pubspec.yaml index 1d6d4341e1ed4..6621352cc92c5 100644 --- a/packages/multicast_dns/pubspec.yaml +++ b/packages/multicast_dns/pubspec.yaml @@ -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 diff --git a/packages/multicast_dns/test/client_test.dart b/packages/multicast_dns/test/client_test.dart index 134d7518d86a0..f3047e5deeedf 100644 --- a/packages/multicast_dns/test/client_test.dart +++ b/packages/multicast_dns/test/client_test.dart @@ -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> testCases = >[ + { + 'name': 'IPv4', + 'datagramSocketType': InternetAddress.anyIPv4, + 'interfacePrefix': '192.168.2.' + }, + { + 'name': 'IPv6', + 'datagramSocketType': InternetAddress.anyIPv6, + 'interfacePrefix': '2001:0db8:85a3:0000:0000:8a2e:7335:030' + } + ]; + + for (final Map 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 selectedInterfacesForSendingPackets = []; + 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> fakeNetworkInterfacesFactory( + InternetAddressType type) async { + final List fakeInterfaces = []; + + // Generate "fake" interfaces + for (int i = 0; i < numberOfFakeInterfaces; i++) { + fakeInterfaces.add(FakeNetworkInterface( + 'inetfake$i', + [ + 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 { @@ -113,4 +185,30 @@ class FakeRawDatagramSocket extends Fake implements RawDatagramSocket { int send(List 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 _addresses; + final int _index; + + @override + List get addresses => _addresses; + + @override + String get name => _name; + + @override + int get index => _index; }