Skip to content
This repository was archived by the owner on Sep 15, 2022. It is now read-only.

Commit f0d30ec

Browse files
committed
Add a mDNS package
Implementation of mDNS one-shot query. This package uses dart:io and runs only on the Dart VM. R=karlklose@google.com BUG= Review URL: https://codereview.chromium.org/1426863003 .
1 parent 19b4c1d commit f0d30ec

File tree

8 files changed

+559
-0
lines changed

8 files changed

+559
-0
lines changed

.packages

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ isolate:pkg/isolate/lib
2020
js_ast:third_party/dart/pkg/js_ast/lib
2121
js_runtime:third_party/dart/sdk/lib/_internal/js_runtime/lib/
2222
lk:pkg/lk/lib
23+
mdns:pkg/mdns/lib
2324
os:pkg/os/lib
2425
package_config:third_party/package_config/lib
2526
path:third_party/path/lib

pkg/mdns/lib/mdns.dart

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:collection';
7+
import 'dart:io';
8+
import 'dart:typed_data';
9+
10+
import 'package:mdns/src/constants.dart';
11+
import 'package:mdns/src/lookup_resolver.dart';
12+
import 'package:mdns/src/packet.dart';
13+
14+
/// Client for DNS lookup using the mDNS protocol.
15+
///
16+
/// This client only support "One-Shot Multicast DNS Queries" as described in
17+
/// section 5.1 of https://tools.ietf.org/html/rfc6762
18+
class MDnsClient {
19+
bool _starting = false;
20+
bool _started = false;
21+
RawDatagramSocket _incoming;
22+
final List<RawDatagramSocket> _sockets = <RawDatagramSocket>[];
23+
final LookupResolver _resolver = new LookupResolver();
24+
25+
/// Start the mDNS client.
26+
Future start() async {
27+
if (_started && _starting) {
28+
throw new StateError('mDNS client already started');
29+
}
30+
_starting = true;
31+
32+
// Listen on all addresses.
33+
_incoming = await RawDatagramSocket.bind(
34+
InternetAddress.ANY_IP_V4, mDnsPort, reuseAddress: true);
35+
36+
// Find all network interfaces with an IPv4 address.
37+
var interfaces =
38+
await NetworkInterface.list(type: InternetAddressType.IP_V4);
39+
for (NetworkInterface interface in interfaces) {
40+
// Create a socket for sending on each adapter.
41+
var socket = await RawDatagramSocket.bind(
42+
interface.addresses[0], mDnsPort, reuseAddress: true);
43+
_sockets.add(socket);
44+
45+
// Join multicast on this interface.
46+
_incoming.joinMulticast(mDnsAddress, interface);
47+
}
48+
_incoming.listen(_handleIncoming);
49+
50+
_starting = false;
51+
_started = true;
52+
}
53+
54+
/// Stop the mDNS client.
55+
void stop() {
56+
if (!_started) return;
57+
if (_starting) {
58+
throw new StateError('Cannot stop mDNS client wile it is starting');
59+
}
60+
61+
_sockets.forEach((socket) => socket.close());
62+
_incoming.close();
63+
64+
_started = false;
65+
}
66+
67+
/// Lookup [hostname] using mDNS.
68+
///
69+
/// The `hostname` must have the form `single-dns-label.local`,
70+
/// e.g. `printer.local`.
71+
///
72+
/// If no answer has been received within the specified [timeout]
73+
/// this method will complete with the value `null`.
74+
Future<InternetAddress> lookup(
75+
String hostname, {Duration timeout: const Duration(seconds: 5)}) {
76+
if (!_started) {
77+
throw new StateError('mDNS client is not started');
78+
}
79+
80+
// Add the pending request before sending the query.
81+
var future = _resolver.addPendingRequest(hostname, timeout);
82+
83+
// Send the request on all interfaces.
84+
List<int> packet = encodeMDnsQuery(hostname);
85+
for (int i = 0; i < _sockets.length; i++) {
86+
_sockets[i].send(packet, mDnsAddress, mDnsPort);
87+
}
88+
89+
return future;
90+
}
91+
92+
// Process incoming datagrams.
93+
_handleIncoming(event) {
94+
if (event == RawSocketEvent.READ) {
95+
var data = _incoming.receive();
96+
var response = decodeMDnsResponse(data.data);
97+
if (response != null) {
98+
_resolver.handleResponse(response);
99+
}
100+
}
101+
}
102+
}
103+
104+
// Simple standalone test.
105+
main() async {
106+
var client = new MDnsClient();
107+
await client.start();
108+
var address = await client.lookup('raspberrypi.local');
109+
client.stop();
110+
print(address);
111+
}

pkg/mdns/lib/src/constants.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
library mdns.src.constants;
6+
7+
import 'dart:io';
8+
9+
InternetAddress mDnsAddress = new InternetAddress('224.0.0.251');
10+
const int mDnsPort = 5353;
11+
12+
// Offsets into the header. See https://tools.ietf.org/html/rfc1035.
13+
const int idOffset = 0;
14+
const int flagsOffset = 2;
15+
const int qdcountOffset = 4;
16+
const int ancountOffset = 6;
17+
const int nscountOffset = 8;
18+
const int arcountOffset = 10;
19+
20+
const int headerSize = 12;
21+
22+
const int responseFlags = 0x8400;
23+
24+
const int ipV4AddressType = 0x0001;
25+
const int ipV4Class = 0x8001;
26+
const int ipV6AddressType = 0x001c;
27+
const int ipV6Class = 0x8001;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
library mdns.src.lookup_resolver;
6+
7+
import 'dart:async';
8+
import 'dart:collection';
9+
10+
import 'package:mdns/src/packet.dart';
11+
12+
class PendingRequest extends LinkedListEntry {
13+
final String hostname;
14+
final Completer completer;
15+
PendingRequest(this.hostname, this.completer);
16+
}
17+
18+
/// Class for keeping track of pending lookups and process incoming
19+
/// query responses.
20+
///
21+
/// Currently the responses are no cached.
22+
class LookupResolver {
23+
LinkedList pendingRequests = new LinkedList();
24+
25+
Future addPendingRequest(String hostname, Duration timeout) {
26+
var completer = new Completer();
27+
var request = new PendingRequest(hostname, completer);
28+
pendingRequests.add(request);
29+
return completer.future.timeout(timeout, onTimeout: () {
30+
request.unlink();
31+
return null;
32+
});
33+
}
34+
35+
void handleResponse(List<DecodeResult> response) {
36+
for (var r in response) {
37+
pendingRequests
38+
.where((pendingRequest) => pendingRequest.hostname == r.name)
39+
.forEach((pendingRequest) {
40+
pendingRequest.completer.complete(r.address);
41+
pendingRequest.unlink();
42+
});
43+
}
44+
}
45+
}
46+

pkg/mdns/lib/src/packet.dart

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
library mdns.src.packet;
6+
7+
import 'dart:convert';
8+
import 'dart:io';
9+
import 'dart:typed_data';
10+
11+
import 'package:mdns/src/constants.dart';
12+
13+
// Encode a mDNS query packet.
14+
List<int> encodeMDnsQuery(String hostname) {
15+
List parts = hostname.split('.');
16+
17+
// Calculate the size of the packet.
18+
int size = headerSize;
19+
for (int i = 0; i < parts.length; i++) {
20+
parts[i] = UTF8.encode(parts[i]);
21+
size += 1 + parts[i].length;
22+
}
23+
size += 1; // End with empty part
24+
size += 4; // Trailer (QTYPE and QCLASS).
25+
Uint8List data = new Uint8List(size);
26+
ByteData bd = new ByteData.view(data.buffer);
27+
// Query identifier - just use 0.
28+
bd.setUint16(idOffset, 0);
29+
// Flags - 0 for query.
30+
bd.setUint16(flagsOffset, 0);
31+
// Query count.
32+
bd.setUint16(qdcountOffset, 1);
33+
// Number of answers - 0 for query.
34+
bd.setUint16(ancountOffset, 0);
35+
// Number of name server records - 0 for query.
36+
bd.setUint16(nscountOffset, 0);
37+
// Number of resource records - 0 for query.
38+
bd.setUint16(arcountOffset, 0);
39+
int offset = headerSize;
40+
for (int i = 0; i < parts.length; i++) {
41+
data[offset++] = parts[i].length;
42+
data.setRange(offset, offset + parts[i].length, parts[i]);
43+
offset += parts[i].length;
44+
}
45+
data[offset] = 0; // Empty part.
46+
offset++;
47+
bd.setUint16(offset, 1); // QTYPE.
48+
offset += 2;
49+
bd.setUint16(offset, 1); // QCLASS.
50+
51+
return data;
52+
}
53+
54+
/// FQDN and address decoded from response.
55+
class DecodeResult {
56+
final String name;
57+
final InternetAddress address;
58+
DecodeResult(this.name, this.address);
59+
}
60+
61+
/// Result of reading a FQDN. The FQDN parts and the bytes consumed.
62+
class FQDNReadResult {
63+
final List<String> fqdn;
64+
final int bytesRead;
65+
FQDNReadResult(this.fqdn, this.bytesRead);
66+
}
67+
68+
/// Decode a mDNS package.
69+
///
70+
/// If decoding fails (e.g. due to an invalid packet) `null` is returned.
71+
///
72+
/// See See https://tools.ietf.org/html/rfc1035 for the format.
73+
List<DecodeResult> decodeMDnsResponse(List<int> packet) {
74+
int length = packet.length;
75+
if (length < headerSize) return null;
76+
77+
Uint8List data =
78+
packet is Uint8List ? packet : new Uint8List.fromList(packet);
79+
ByteData bd = new ByteData.view(data.buffer);
80+
// Query identifier.
81+
int id = bd.getUint16(idOffset);
82+
// Flags.
83+
int flags = bd.getUint16(flagsOffset);
84+
85+
if (flags != responseFlags) return;
86+
// Query count.
87+
int qdcount = bd.getUint16(qdcountOffset);
88+
// Number of answers.
89+
int ancount = bd.getUint16(ancountOffset);
90+
// Number of name server records.
91+
int nscount = bd.getUint16(nscountOffset);
92+
// Number of resource records.
93+
int arcount = bd.getUint16(arcountOffset);
94+
int offset = headerSize;
95+
96+
void checkLength(int required) {
97+
if (length < required) throw new MDnsDecodeException(required);
98+
}
99+
100+
// Read a FQDN at the given offset. Returns a pair with the FQDN
101+
// parts and the number of bytes consumed.
102+
//
103+
// If decoding fails (e.g. die to an invalid packet) `null` is returned.
104+
FQDNReadResult readFQDN(int offset) {
105+
List<String> parts = [];
106+
int prevOffset = offset;
107+
while (true) {
108+
// At least two bytes required.
109+
checkLength(offset + 2);
110+
111+
// Check for compressed.
112+
if (data[offset] & 0xc0 == 0xc0) {
113+
// A compressed FQDN has a new offset in the lower 14 bits.
114+
FQDNReadResult result = readFQDN(bd.getUint16(offset) & ~0xc000);
115+
parts.addAll(result.fqdn);
116+
offset += 2;
117+
break;
118+
} else {
119+
// A normal FQDN part has a length and a UTF-8 encoded name
120+
// part. If the length is 0 this is the end of the FQDN.
121+
var partLength = data[offset];
122+
offset++;
123+
if (partLength != 0) {
124+
checkLength(offset + partLength);
125+
var partBytes = new Uint8List.view(data.buffer, offset, partLength);
126+
offset += partLength;
127+
parts.add(UTF8.decode(partBytes));
128+
} else {
129+
break;
130+
}
131+
}
132+
}
133+
return new FQDNReadResult(parts, offset - prevOffset);
134+
}
135+
136+
DecodeResult readAddress() {
137+
// First read the FQDN.
138+
FQDNReadResult result = readFQDN(offset);
139+
var fqdn = result.fqdn.join('.');
140+
offset += result.bytesRead;
141+
checkLength(offset + 2);
142+
int type = bd.getUint16(offset);
143+
offset += 2;
144+
checkLength(offset + 2);
145+
int cls = bd.getUint16(offset);
146+
offset += 2;
147+
checkLength(offset + 4);
148+
int ttl = bd.getInt32(offset);
149+
offset += 4;
150+
checkLength(offset + 2);
151+
int addressLength = bd.getUint16(offset);
152+
offset += 2;
153+
checkLength(offset + addressLength);
154+
var addressBytes = new Uint8List.view(data.buffer, offset, addressLength);
155+
offset += addressLength;
156+
157+
if (type == ipV4AddressType && cls == ipV4Class && addressLength == 4) {
158+
String addr = addressBytes.map((n) => n.toString()).join('.');
159+
return new DecodeResult(fqdn, new InternetAddress(addr));
160+
} else {
161+
return null;
162+
}
163+
}
164+
165+
// We don't use the number of records - just read through all
166+
// resource records and filter.
167+
var result = [];
168+
try {
169+
while (data.length - offset >= 16) {
170+
var address = readAddress();
171+
if (address != null) result.add(address);
172+
}
173+
} on MDnsDecodeException catch (e, s) {
174+
// If decoding fails return null.
175+
return null;
176+
}
177+
178+
return result;
179+
}
180+
181+
/// Exceptions thrown by decoder when the packet is invalid.
182+
class MDnsDecodeException implements Exception {
183+
/// Exception message.
184+
final int offset;
185+
const MDnsDecodeException(this.offset);
186+
String toString() => 'Decoding error at $offset';
187+
}

0 commit comments

Comments
 (0)