diff --git a/packages/multicast_dns/CHANGELOG.md b/packages/multicast_dns/CHANGELOG.md index d1a8c8ed..587a0924 100644 --- a/packages/multicast_dns/CHANGELOG.md +++ b/packages/multicast_dns/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.3+2 + +* Sends IPv4 mDNS queries over each configured network interface. + ## 0.3.3+1 * Updates metadata for move to https://github.com/flutter/core-packages. diff --git a/packages/multicast_dns/lib/multicast_dns.dart b/packages/multicast_dns/lib/multicast_dns.dart index 98eeff77..7e7528c2 100644 --- a/packages/multicast_dns/lib/multicast_dns.dart +++ b/packages/multicast_dns/lib/multicast_dns.dart @@ -53,6 +53,7 @@ class MDnsClient { bool _starting = false; bool _started = false; RawDatagramSocket? _incomingIPv4; + final List _ipv4InterfaceSockets = []; final List _ipv6InterfaceSockets = []; final LookupResolver _resolver = LookupResolver(); final ResourceRecordCache _cache = ResourceRecordCache(); @@ -132,34 +133,56 @@ class MDnsClient { ? mDnsAddressIPv4 : mDnsAddressIPv6; - final List interfaces = (await interfacesFactory( - listenAddress.type, - )).toList(); - - for (final interface in interfaces) { - final InternetAddress targetAddress = interface.addresses[0]; - - // Ensure that we're using this address/interface for multicast. - 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, - ), - ); + try { + final List interfaces = (await interfacesFactory( + listenAddress.type, + )).toList(); + + for (final interface in interfaces) { + final InternetAddress targetAddress = interface.addresses[0]; + + // Ensure that we're using this address/interface for multicast. + 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, + ), + ); + } else if (targetAddress.type == InternetAddressType.IPv4) { + final RawDatagramSocket socket = await _rawDatagramSocketFactory( + targetAddress, + selectedMDnsPort, + reuseAddress: true, + reusePort: true, + ttl: 255, + ); + _ipv4InterfaceSockets.add(socket); + socket.setRawOption( + RawSocketOption( + RawSocketOption.levelIPv4, + RawSocketOption.IPv4MulticastInterface, + targetAddress.rawAddress, + ), + ); + } + + // Join multicast on this interface. + incoming.joinMulticast(_mDnsAddress!, interface); } - - // Join multicast on this interface. - incoming.joinMulticast(_mDnsAddress!, interface); + } catch (_) { + _closeSockets(); + _starting = false; + rethrow; } incoming.listen((RawSocketEvent event) => _handleIncoming(event, incoming), onError: onError); _started = true; @@ -175,17 +198,26 @@ class MDnsClient { throw StateError('Cannot stop mDNS client while it is starting.'); } + _closeSockets(); + + _resolver.clearPendingRequests(); + + _started = false; + } + + void _closeSockets() { _incomingIPv4?.close(); _incomingIPv4 = null; + for (final RawDatagramSocket socket in _ipv4InterfaceSockets) { + socket.close(); + } + _ipv4InterfaceSockets.clear(); + for (final RawDatagramSocket socket in _ipv6InterfaceSockets) { socket.close(); } _ipv6InterfaceSockets.clear(); - - _resolver.clearPendingRequests(); - - _started = false; } /// Lookup a [ResourceRecord], potentially from the cache. @@ -226,11 +258,25 @@ class MDnsClient { final List packet = query.encode(); if (_mDnsAddress?.type == InternetAddressType.IPv4) { - // Send and listen on same "ANY" interface - _incomingIPv4?.send(packet, _mDnsAddress!, selectedMDnsPort); + if (_ipv4InterfaceSockets.isEmpty) { + // Preserve the previous fallback if no interfaces were discovered. + _incomingIPv4?.send(packet, _mDnsAddress!, selectedMDnsPort); + } else { + for (final RawDatagramSocket socket in _ipv4InterfaceSockets) { + try { + socket.send(packet, _mDnsAddress!, selectedMDnsPort); + } on SocketException { + // An unavailable interface must not prevent querying others. + } + } + } } else { for (final RawDatagramSocket socket in _ipv6InterfaceSockets) { - socket.send(packet, _mDnsAddress!, selectedMDnsPort); + try { + socket.send(packet, _mDnsAddress!, selectedMDnsPort); + } on SocketException { + // An unavailable interface must not prevent querying others. + } } } diff --git a/packages/multicast_dns/pubspec.yaml b/packages/multicast_dns/pubspec.yaml index 9f095760..c23e67a5 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/core-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.3+1 +version: 0.3.3+2 environment: sdk: ^3.10.0 diff --git a/packages/multicast_dns/test/client_test.dart b/packages/multicast_dns/test/client_test.dart index 12508020..9a922194 100644 --- a/packages/multicast_dns/test/client_test.dart +++ b/packages/multicast_dns/test/client_test.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'dart:io'; -// import 'dart:typed_data'; +import 'dart:typed_data'; import 'package:multicast_dns/multicast_dns.dart'; import 'package:test/fake.dart'; @@ -106,6 +106,145 @@ void main() { await client.lookup(ResourceRecordQuery.serverPointer('_')).toList(); }); + test('Sends IPv4 multicast on each interface', () async { + final addresses = [ + InternetAddress('192.0.2.2'), + InternetAddress('198.51.100.2'), + ]; + final sockets = []; + final client = MDnsClient( + rawDatagramSocketFactory: + ( + dynamic host, + int port, { + bool reuseAddress = true, + bool reusePort = true, + int ttl = 1, + }) async { + final socket = FakeRawDatagramSocket(); + if (host is InternetAddress) { + socket.address = host; + } + sockets.add(socket); + return socket; + }, + ); + + await client.start( + interfacesFactory: (InternetAddressType type) async => [ + for (var i = 0; i < addresses.length; i++) + FakeNetworkInterface([FakeInterfaceAddress(addresses[i])], i + 1), + ], + ); + + expect(sockets, hasLength(addresses.length + 1)); + for (var i = 0; i < addresses.length; i++) { + final List options = sockets[i + 1].rawOptions; + expect(options, hasLength(1)); + expect(options.single.level, RawSocketOption.levelIPv4); + expect(options.single.option, RawSocketOption.IPv4MulticastInterface); + expect(options.single.value, addresses[i].rawAddress); + } + + await client.lookup(ResourceRecordQuery.serverPointer('_'), timeout: Duration.zero).toList(); + expect(sockets.skip(1).map((socket) => socket.sentDatagrams), [1, 1]); + + client.stop(); + expect(sockets.every((socket) => socket.closed), isTrue); + }); + + test('Continues sending IPv4 multicast if one interface fails', () async { + final addresses = [ + InternetAddress('192.0.2.2'), + InternetAddress('198.51.100.2'), + ]; + final sockets = []; + final client = MDnsClient( + rawDatagramSocketFactory: + ( + dynamic host, + int port, { + bool reuseAddress = true, + bool reusePort = true, + int ttl = 1, + }) async { + final socket = FakeRawDatagramSocket(); + if (host is InternetAddress) { + socket.address = host; + if (host.address == addresses.first.address) { + socket.sendException = const SocketException('Interface unavailable'); + } + } + sockets.add(socket); + return socket; + }, + ); + + await client.start( + interfacesFactory: (InternetAddressType type) async => [ + for (var i = 0; i < addresses.length; i++) + FakeNetworkInterface([FakeInterfaceAddress(addresses[i])], i + 1), + ], + ); + + await expectLater( + client.lookup(ResourceRecordQuery.serverPointer('_'), timeout: Duration.zero).toList(), + completes, + ); + expect(sockets.skip(1).map((socket) => socket.sentDatagrams), [0, 1]); + client.stop(); + }); + + test('Closes sockets and allows restart when interface socket creation fails', () async { + final addresses = [ + InternetAddress('192.0.2.2'), + InternetAddress('198.51.100.2'), + ]; + final sockets = []; + var failInterfaceSockets = true; + var interfaceSocketCount = 0; + final client = MDnsClient( + rawDatagramSocketFactory: + ( + dynamic host, + int port, { + bool reuseAddress = true, + bool reusePort = true, + int ttl = 1, + }) async { + final socket = FakeRawDatagramSocket(); + if (host is InternetAddress) { + socket.address = host; + interfaceSocketCount++; + if (failInterfaceSockets && interfaceSocketCount == 2) { + throw const SocketException('Interface unavailable'); + } + } + sockets.add(socket); + return socket; + }, + ); + + Future> interfacesFactory(InternetAddressType type) async => + [ + for (var i = 0; i < addresses.length; i++) + FakeNetworkInterface([FakeInterfaceAddress(addresses[i])], i + 1), + ]; + + await expectLater( + client.start(interfacesFactory: interfacesFactory), + throwsA(isA()), + ); + expect(sockets, hasLength(2)); + expect(sockets.every((socket) => socket.closed), isTrue); + + failInterfaceSockets = false; + await client.start(interfacesFactory: interfacesFactory); + expect(sockets.skip(2).every((socket) => !socket.closed), isTrue); + client.stop(); + expect(sockets.every((socket) => socket.closed), isTrue); + }); + // group('Bind a single socket to ANY IPv4 and more than one when IPv6', () { // final testCases = >[ // { @@ -235,6 +374,10 @@ class FakeRawDatagramSocket extends Fake implements RawDatagramSocket { bool closed = false; + final List rawOptions = []; + int sentDatagrams = 0; + SocketException? sendException; + @override void close() { closed = true; @@ -242,6 +385,10 @@ class FakeRawDatagramSocket extends Fake implements RawDatagramSocket { @override int send(List buffer, InternetAddress address, int port) { + if (sendException case final SocketException exception) { + throw exception; + } + sentDatagrams++; return buffer.length; } @@ -251,10 +398,35 @@ class FakeRawDatagramSocket extends Fake implements RawDatagramSocket { } @override void setRawOption(RawSocketOption option) { - // nothing to do here + rawOptions.add(option); } } +class FakeNetworkInterface extends Fake implements NetworkInterface { + FakeNetworkInterface(this.addresses, this.index); + + @override + final List addresses; + + @override + final int index; +} + +class FakeInterfaceAddress extends Fake implements InterfaceAddress { + FakeInterfaceAddress(this._address); + + final InternetAddress _address; + + @override + String get address => _address.address; + + @override + Uint8List get rawAddress => _address.rawAddress; + + @override + InternetAddressType get type => _address.type; +} + class FakeRawDatagramSocketThatSendsError extends Fake implements RawDatagramSocket { @override InternetAddress address = InternetAddress.anyIPv4;