Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/multicast_dns/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
114 changes: 80 additions & 34 deletions packages/multicast_dns/lib/multicast_dns.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class MDnsClient {
bool _starting = false;
bool _started = false;
RawDatagramSocket? _incomingIPv4;
final List<RawDatagramSocket> _ipv4InterfaceSockets = <RawDatagramSocket>[];
final List<RawDatagramSocket> _ipv6InterfaceSockets = <RawDatagramSocket>[];
final LookupResolver _resolver = LookupResolver();
final ResourceRecordCache _cache = ResourceRecordCache();
Expand Down Expand Up @@ -132,34 +133,56 @@ class MDnsClient {
? mDnsAddressIPv4
: mDnsAddressIPv6;

final List<NetworkInterface> 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<NetworkInterface> 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;
Expand All @@ -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.
Expand Down Expand Up @@ -226,11 +258,25 @@ class MDnsClient {
final List<int> 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.
}
}
Comment thread
NemesisDevX marked this conversation as resolved.
Resolved
}
} 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.
}
}
}

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/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
Expand Down
176 changes: 174 additions & 2 deletions packages/multicast_dns/test/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -106,6 +106,145 @@ void main() {
await client.lookup(ResourceRecordQuery.serverPointer('_')).toList();
});

test('Sends IPv4 multicast on each interface', () async {
final addresses = <InternetAddress>[
InternetAddress('192.0.2.2'),
InternetAddress('198.51.100.2'),
];
final sockets = <FakeRawDatagramSocket>[];
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 => <NetworkInterface>[
for (var i = 0; i < addresses.length; i++)
FakeNetworkInterface(<InterfaceAddress>[FakeInterfaceAddress(addresses[i])], i + 1),
],
);

expect(sockets, hasLength(addresses.length + 1));
for (var i = 0; i < addresses.length; i++) {
final List<RawSocketOption> 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), <int>[1, 1]);

client.stop();
expect(sockets.every((socket) => socket.closed), isTrue);
});

test('Continues sending IPv4 multicast if one interface fails', () async {
final addresses = <InternetAddress>[
InternetAddress('192.0.2.2'),
InternetAddress('198.51.100.2'),
];
final sockets = <FakeRawDatagramSocket>[];
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 => <NetworkInterface>[
for (var i = 0; i < addresses.length; i++)
FakeNetworkInterface(<InterfaceAddress>[FakeInterfaceAddress(addresses[i])], i + 1),
],
);

await expectLater(
client.lookup(ResourceRecordQuery.serverPointer('_'), timeout: Duration.zero).toList(),
completes,
);
expect(sockets.skip(1).map((socket) => socket.sentDatagrams), <int>[0, 1]);
client.stop();
});

test('Closes sockets and allows restart when interface socket creation fails', () async {
final addresses = <InternetAddress>[
InternetAddress('192.0.2.2'),
InternetAddress('198.51.100.2'),
];
final sockets = <FakeRawDatagramSocket>[];
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<Iterable<NetworkInterface>> interfacesFactory(InternetAddressType type) async =>
<NetworkInterface>[
for (var i = 0; i < addresses.length; i++)
FakeNetworkInterface(<InterfaceAddress>[FakeInterfaceAddress(addresses[i])], i + 1),
];

await expectLater(
client.start(interfacesFactory: interfacesFactory),
throwsA(isA<SocketException>()),
);
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 = <Map<String, Object>>[
// <String, Object>{
Expand Down Expand Up @@ -235,13 +374,21 @@ class FakeRawDatagramSocket extends Fake implements RawDatagramSocket {

bool closed = false;

final List<RawSocketOption> rawOptions = <RawSocketOption>[];
int sentDatagrams = 0;
SocketException? sendException;

@override
void close() {
closed = true;
}

@override
int send(List<int> buffer, InternetAddress address, int port) {
if (sendException case final SocketException exception) {
throw exception;
}
sentDatagrams++;
return buffer.length;
}

Expand All @@ -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<InterfaceAddress> 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;
Expand Down
Loading