From a6e615f00fa15a94854ba17767efe718fb37ada4 Mon Sep 17 00:00:00 2001 From: NemesisDev Date: Tue, 10 Nov 2026 00:32:37 +0200 Subject: [PATCH 1/4] fix(multicast_dns): send IPv4 queries on each interface --- packages/multicast_dns/CHANGELOG.md | 4 + packages/multicast_dns/lib/multicast_dns.dart | 32 +++++++- packages/multicast_dns/pubspec.yaml | 2 +- packages/multicast_dns/test/client_test.dart | 80 ++++++++++++++++++- 4 files changed, 113 insertions(+), 5 deletions(-) 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..cbf5f303 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(); @@ -156,6 +157,22 @@ class MDnsClient { 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. @@ -178,6 +195,11 @@ class MDnsClient { _incomingIPv4?.close(); _incomingIPv4 = null; + for (final RawDatagramSocket socket in _ipv4InterfaceSockets) { + socket.close(); + } + _ipv4InterfaceSockets.clear(); + for (final RawDatagramSocket socket in _ipv6InterfaceSockets) { socket.close(); } @@ -226,8 +248,14 @@ 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) { + socket.send(packet, _mDnsAddress!, selectedMDnsPort); + } + } } else { for (final RawDatagramSocket socket in _ipv6InterfaceSockets) { socket.send(packet, _mDnsAddress!, selectedMDnsPort); 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..d8587af1 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,53 @@ 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); + }); + // group('Bind a single socket to ANY IPv4 and more than one when IPv6', () { // final testCases = >[ // { @@ -235,6 +282,9 @@ class FakeRawDatagramSocket extends Fake implements RawDatagramSocket { bool closed = false; + final List rawOptions = []; + int sentDatagrams = 0; + @override void close() { closed = true; @@ -242,6 +292,7 @@ class FakeRawDatagramSocket extends Fake implements RawDatagramSocket { @override int send(List buffer, InternetAddress address, int port) { + sentDatagrams++; return buffer.length; } @@ -251,10 +302,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; From 204e931fa17af03e8bf67b4e79bd250f937e430d Mon Sep 17 00:00:00 2001 From: NemesisDev Date: Tue, 10 Nov 2026 00:49:23 +0200 Subject: [PATCH 2/4] fix(multicast_dns): continue sends after interface errors --- packages/multicast_dns/lib/multicast_dns.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/multicast_dns/lib/multicast_dns.dart b/packages/multicast_dns/lib/multicast_dns.dart index cbf5f303..a583bb9b 100644 --- a/packages/multicast_dns/lib/multicast_dns.dart +++ b/packages/multicast_dns/lib/multicast_dns.dart @@ -253,12 +253,20 @@ class MDnsClient { _incomingIPv4?.send(packet, _mDnsAddress!, selectedMDnsPort); } else { for (final RawDatagramSocket socket in _ipv4InterfaceSockets) { - socket.send(packet, _mDnsAddress!, selectedMDnsPort); + 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. + } } } From 01e47d96d783b9779eea4822e22db6add88e32e8 Mon Sep 17 00:00:00 2001 From: NemesisDev Date: Tue, 10 Nov 2026 00:49:35 +0200 Subject: [PATCH 3/4] test(multicast_dns): continue after interface send failure --- packages/multicast_dns/test/client_test.dart | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/multicast_dns/test/client_test.dart b/packages/multicast_dns/test/client_test.dart index d8587af1..ac334026 100644 --- a/packages/multicast_dns/test/client_test.dart +++ b/packages/multicast_dns/test/client_test.dart @@ -153,6 +153,48 @@ void main() { 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 == addresses.first) { + socket.sendException = 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(); + }); + // group('Bind a single socket to ANY IPv4 and more than one when IPv6', () { // final testCases = >[ // { @@ -284,6 +326,7 @@ class FakeRawDatagramSocket extends Fake implements RawDatagramSocket { final List rawOptions = []; int sentDatagrams = 0; + SocketException? sendException; @override void close() { @@ -292,6 +335,9 @@ 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; } From b1cffbbec97d2a869c020c51a541d25cd6af70a0 Mon Sep 17 00:00:00 2001 From: NemesisDevX Date: Thu, 12 Nov 2026 08:39:59 +0200 Subject: [PATCH 4/4] fix(multicast_dns): clean up sockets when interface setup fails If creating an interface socket throws partway through start(), the sockets already created were leaked and _starting stayed true, leaving the client permanently unable to start. Close all sockets, reset the starting flag, and rethrow so the client can be retried. Also fix the send-failure test so the SocketException is actually armed: host is the InterfaceAddress wrapper, so compare by address string rather than object identity. --- packages/multicast_dns/lib/multicast_dns.dart | 104 ++++++++++-------- packages/multicast_dns/test/client_test.dart | 54 ++++++++- 2 files changed, 109 insertions(+), 49 deletions(-) diff --git a/packages/multicast_dns/lib/multicast_dns.dart b/packages/multicast_dns/lib/multicast_dns.dart index a583bb9b..7e7528c2 100644 --- a/packages/multicast_dns/lib/multicast_dns.dart +++ b/packages/multicast_dns/lib/multicast_dns.dart @@ -133,50 +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, - ), - ); - } 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, - ), - ); - } + 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; @@ -192,6 +198,14 @@ class MDnsClient { throw StateError('Cannot stop mDNS client while it is starting.'); } + _closeSockets(); + + _resolver.clearPendingRequests(); + + _started = false; + } + + void _closeSockets() { _incomingIPv4?.close(); _incomingIPv4 = null; @@ -204,10 +218,6 @@ class MDnsClient { socket.close(); } _ipv6InterfaceSockets.clear(); - - _resolver.clearPendingRequests(); - - _started = false; } /// Lookup a [ResourceRecord], potentially from the cache. diff --git a/packages/multicast_dns/test/client_test.dart b/packages/multicast_dns/test/client_test.dart index ac334026..9a922194 100644 --- a/packages/multicast_dns/test/client_test.dart +++ b/packages/multicast_dns/test/client_test.dart @@ -171,8 +171,8 @@ void main() { final socket = FakeRawDatagramSocket(); if (host is InternetAddress) { socket.address = host; - if (host == addresses.first) { - socket.sendException = SocketException('Interface unavailable'); + if (host.address == addresses.first.address) { + socket.sendException = const SocketException('Interface unavailable'); } } sockets.add(socket); @@ -195,6 +195,56 @@ void main() { 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 = >[ // {