blob: 336ac427930e89d893dd713ab73307dc8215fa52 [file] [log] [blame]
// 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.
import 'dart:async';
import 'dart:io';
import 'src/constants.dart';
import 'src/lookup_resolver.dart';
import 'src/native_protocol_client.dart';
import 'src/packet.dart';
import 'src/resource_record.dart';
export 'package:multicast_dns/src/resource_record.dart';
/// A callback type for [MDnsQuerier.start] to iterate available network
/// interfaces.
///
/// Implementations must ensure they return interfaces appropriate for the
/// [type] parameter.
///
/// See also:
/// * [MDnsQuerier.allInterfacesFactory]
typedef NetworkInterfacesFactory = Future<Iterable<NetworkInterface>> Function(
InternetAddressType type);
/// A factory for construction of datagram sockets.
///
/// This can be injected into the [MDnsClient] to provide alternative
/// implementations of [RawDatagramSocket.bind].
typedef RawDatagramSocketFactory = Future<RawDatagramSocket> Function(
dynamic host, int port,
{bool reuseAddress, bool reusePort, int ttl});
/// Client for DNS lookup and publishing using the mDNS protocol.
///
/// Users should call [MDnsQuerier.start] when ready to start querying and
/// listening. [MDnsQuerier.stop] must be called when done to clean up
/// resources.
///
/// This client only supports "One-Shot Multicast DNS Queries" as described in
/// section 5.1 of [RFC 6762](https://tools.ietf.org/html/rfc6762).
class MDnsClient {
/// Create a new [MDnsClient].
MDnsClient({
RawDatagramSocketFactory rawDatagramSocketFactory = RawDatagramSocket.bind,
}) : _rawDatagramSocketFactory = rawDatagramSocketFactory;
bool _starting = false;
bool _started = false;
final List<RawDatagramSocket> _sockets = <RawDatagramSocket>[];
final List<RawDatagramSocket> _toBeClosed = <RawDatagramSocket>[];
final LookupResolver _resolver = LookupResolver();
final ResourceRecordCache _cache = ResourceRecordCache();
final RawDatagramSocketFactory _rawDatagramSocketFactory;
InternetAddress? _mDnsAddress;
int? _mDnsPort;
/// Find all network interfaces with an the [InternetAddressType] specified.
Future<Iterable<NetworkInterface>> allInterfacesFactory(
InternetAddressType type) {
return NetworkInterface.list(
includeLinkLocal: true,
type: type,
includeLoopback: true,
);
}
/// Start the mDNS client.
///
/// With no arguments, this method will listen on the IPv4 multicast address
/// on all IPv4 network interfaces.
///
/// The [listenAddress] parameter must be either [InternetAddress.anyIPv4] or
/// [InternetAddress.anyIPv6], and will default to anyIPv4.
///
/// The [interfaceFactory] defaults to [allInterfacesFactory].
///
/// The [mDnsPort] allows configuring what port is used for the mDNS
/// query. If not provided, defaults to `5353`.
///
/// The [mDnsAddress] allows configuring what internet address is used
/// for the mDNS query. If not provided, defaults to either `224.0.0.251` or
/// or `FF02::FB`.
///
/// Subsequent calls to this method are ignored while the mDNS client is in
/// started state.
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,
);
// Can't send to IPv6 any address.
if (incoming.address != InternetAddress.anyIPv6) {
_sockets.add(incoming);
} else {
_toBeClosed.add(incoming);
}
_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];
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 {
socket.setRawOption(RawSocketOption.fromInt(
RawSocketOption.levelIPv6,
RawSocketOption.IPv6MulticastInterface,
interface.index,
));
}
// Join multicast on this interface.
incoming.joinMulticast(_mDnsAddress!, interface);
}
incoming.listen((RawSocketEvent event) => _handleIncoming(event, incoming));
_started = true;
_starting = false;
}
/// Stop the client and close any associated sockets.
void stop() {
if (!_started) {
return;
}
if (_starting) {
throw StateError('Cannot stop mDNS client while it is starting.');
}
for (final RawDatagramSocket socket in _sockets) {
socket.close();
}
_sockets.clear();
for (final RawDatagramSocket socket in _toBeClosed) {
socket.close();
}
_toBeClosed.clear();
_resolver.clearPendingRequests();
_started = false;
}
/// Lookup a [ResourceRecord], potentially from the cache.
///
/// The [type] parameter must be a valid [ResourceRecordType]. The [fullyQualifiedName]
/// parameter is the name of the service to lookup, and must not be null. The
/// [timeout] parameter specifies how long the internal cache should hold on
/// to the record. The [multicast] parameter specifies whether the query
/// should be sent as unicast (QU) or multicast (QM).
///
/// Some publishers have been observed to not respond to unicast requests
/// properly, so the default is true.
Stream<T> lookup<T extends ResourceRecord>(
ResourceRecordQuery query, {
Duration timeout = const Duration(seconds: 5),
}) {
final int? selectedMDnsPort = _mDnsPort;
if (!_started || selectedMDnsPort == null) {
throw StateError('mDNS client must be started before calling lookup.');
}
// Look for entries in the cache.
final List<T> cached = <T>[];
_cache.lookup<T>(
query.fullyQualifiedName, query.resourceRecordType, cached);
if (cached.isNotEmpty) {
final StreamController<T> controller = StreamController<T>();
cached.forEach(controller.add);
controller.close();
return controller.stream;
}
// Add the pending request before sending the query.
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);
}
return results;
}
// Process incoming datagrams.
void _handleIncoming(RawSocketEvent event, RawDatagramSocket incoming) {
if (event == RawSocketEvent.read) {
final Datagram? datagram = incoming.receive();
if (datagram == null) {
return;
}
// Check for published responses.
final List<ResourceRecord>? response = decodeMDnsResponse(datagram.data);
if (response != null) {
_cache.updateRecords(response);
_resolver.handleResponse(response);
return;
}
// TODO(dnfield): Support queries coming in for published entries.
}
}
}