// 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> 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 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; RawDatagramSocket? _incomingIPv4; final List _ipv6InterfaceSockets = []; 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> 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`. /// /// If provided, [onError] will be called in case of a stream error. If /// omitted any errors on the stream are considered unhandled, and will be /// passed to the current [Zone]'s error handler. /// /// Subsequent calls to this method are ignored while the mDNS client is in /// started state. Future start({ InternetAddress? listenAddress, NetworkInterfacesFactory? interfacesFactory, int mDnsPort = mDnsPort, InternetAddress? mDnsAddress, Function? onError, }) 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) { _incomingIPv4 = incoming; } else { _ipv6InterfaceSockets.add(incoming); } _mDnsAddress ??= incoming.address.type == InternetAddressType.IPv4 ? mDnsAddressIPv4 : mDnsAddressIPv6; final List interfaces = (await interfacesFactory(listenAddress.type)).toList(); for (final NetworkInterface 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, )); } // Join multicast on this interface. incoming.joinMulticast(_mDnsAddress!, interface); } incoming.listen( (RawSocketEvent event) => _handleIncoming(event, incoming), onError: onError, ); _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.'); } _incomingIPv4?.close(); _incomingIPv4 = null; for (final RawDatagramSocket socket in _ipv6InterfaceSockets) { socket.close(); } _ipv6InterfaceSockets.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 lookup( 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 cached = []; _cache.lookup( query.fullyQualifiedName, query.resourceRecordType, cached); if (cached.isNotEmpty) { final StreamController controller = StreamController(); cached.forEach(controller.add); controller.close(); return controller.stream; } // Add the pending request before sending the query. final Stream results = _resolver.addPendingRequest( query.resourceRecordType, query.fullyQualifiedName, timeout); final List packet = query.encode(); 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; } // 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? response = decodeMDnsResponse(datagram.data); if (response != null) { _cache.updateRecords(response); _resolver.handleResponse(response); return; } // TODO(dnfield): Support queries coming in for published entries. } } }