├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── PROXY.md ├── README.md ├── analysis_options.yaml ├── bin └── dns-proxy.dart ├── dart_test.yaml ├── example └── dns.dart ├── lib ├── dns.dart └── src │ ├── dns_client.dart │ ├── dns_packet.dart │ ├── dns_server.dart │ ├── dns_settings.dart │ ├── filtering_dns_client.dart │ ├── http_dns_client.dart │ └── udp_dns_client.dart ├── pubspec.yaml └── test ├── dns_client_test.dart ├── dns_packet_test.dart └── dns_proxy_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | .pub/ 7 | build/ 8 | # If you're building an application, you may want to check-in your pubspec.lock 9 | pubspec.lock 10 | 11 | # Directory created by dartdoc 12 | # If you don't generate documentation locally you can remove this line. 13 | doc/api/ 14 | 15 | .idea 16 | .VSCode -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.3 2 | * Updated dependencies -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PROXY.md: -------------------------------------------------------------------------------- 1 | # Built-in proxy 2 | ## Install 3 | In the command line: 4 | ``` 5 | pub global activate dns 6 | ``` 7 | 8 | ## Start 9 | In the command line: 10 | ``` 11 | sudo dns_proxy start --configure 12 | ``` 13 | 14 | Some common options are: 15 | * --https=(URL) 16 | * TLS-over-HTTP URL 17 | * --configure 18 | * The proxy attempts to temporarily configure your operating system to use the 19 | proxy. Normal settings will be restored when you close the proxy. 20 | 21 | If you are developing, you can run the proxy with `pub run bin/dns_proxy.dart`. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Implements DNS protocol (over UDP) and modern [DNS-over-HTTP](https://developers.google.com/speed/public-dns/docs/dns-over-https). 4 | 5 | The package uses [package:universal_io](https://pub.dev/packages/universal_io), which means that 6 | you can use it all platforms. 7 | 8 | Licensed under the [Apache License 2.0](LICENSE). 9 | 10 | __IMPORTANT: This is an early release that has known bugs.__ 11 | 12 | ## Issues? Contributing? 13 | * For your first contribution, you should [create a pull request](https://github.com/terrier989/dns/pull/new/master). 14 | * Contributors can receive project administrator rights. 15 | * Discuss issues at the [Github issue tracker](https://github.com/terrier989/dns/issues). -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml -------------------------------------------------------------------------------- /bin/dns-proxy.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:args/command_runner.dart'; 16 | import 'package:dns/dns.dart'; 17 | import 'package:dns/src/dns_settings.dart'; 18 | import 'package:universal_io/io.dart'; 19 | import 'dart:convert'; 20 | import 'dart:async'; 21 | import 'package:ip/ip.dart'; 22 | 23 | List _mainArgs = []; 24 | 25 | /// You can run this function with: 26 | /// ``` 27 | /// pub run bin/dns_proxy.dart 28 | /// ``` 29 | void main(List args) async { 30 | _mainArgs = args; 31 | final runner = CommandRunner("dns_proxy", "DNS proxy"); 32 | runner.addCommand(ServeCommand()); 33 | await runner.run(args); 34 | } 35 | 36 | class ServeCommand extends Command { 37 | @override 38 | String get name => "serve"; 39 | 40 | @override 41 | String get description => "Starts DNS proxy"; 42 | 43 | ServeCommand() { 44 | argParser.addOption( 45 | "https", 46 | help: "DNS-over-HTTPS service URL", 47 | ); 48 | argParser.addOption( 49 | "host", 50 | defaultsTo: "127.0.0.1", 51 | help: "Local IP interface", 52 | ); 53 | argParser.addOption( 54 | "port", 55 | defaultsTo: "53", 56 | help: "Local UDP port", 57 | ); 58 | argParser.addFlag( 59 | "silent", 60 | defaultsTo: false, 61 | help: "Disable debug messages", 62 | ); 63 | if (Platform.isMacOS) { 64 | argParser.addFlag("configure", defaultsTo: false); 65 | } 66 | } 67 | 68 | @override 69 | void run() async { 70 | final host = InternetAddress(argResults["host"]); 71 | final port = int.parse(argResults["port"]); 72 | final dnsOverHttpsUrl = argResults["https"]; 73 | final isSilent = argResults["silent"]; 74 | 75 | // Define client 76 | var client = HttpDnsClient.google(maximalPrivacy: true); 77 | if (dnsOverHttpsUrl != null) { 78 | client = HttpDnsClient(dnsOverHttpsUrl, maximalPrivacy: true); 79 | } 80 | 81 | // Add logging 82 | final filteringClient = 83 | FilteringDnsClient(client, beforeOperation: (DnsPacket packet) { 84 | if (!isSilent) { 85 | for (var question in packet.questions) { 86 | final typeName = DnsQuestion.stringFromType(question.type); 87 | print("Lookup: ${question.name} ($typeName)"); 88 | } 89 | } 90 | }, afterOperation: (packet) { 91 | if (!isSilent) { 92 | var first = true; 93 | for (var answer in packet.answers) { 94 | final typeName = DnsResourceRecord.stringFromType(answer.type); 95 | final ip = IpAddress.fromBytes(answer.data); 96 | if (first) { 97 | first = false; 98 | print("Answer: ${answer.name} ($typeName)"); 99 | } 100 | print(" --> $ip"); 101 | } 102 | } 103 | }); 104 | 105 | // By default, we are not configuring systems settings for you 106 | 107 | // In OS X, we support changing system DNS server (temporarily) 108 | if (Platform.isMacOS) { 109 | final isConfigureFlag = argResults["configure"] as bool; 110 | if (isConfigureFlag) { 111 | // Port must the default port 112 | if (!isSilent && port != DnsServer.defaultPort) { 113 | print("'--configure' requires that port is ${DnsServer.defaultPort}"); 114 | } 115 | 116 | // We need root permissions 117 | if (_whoami() != "root") { 118 | final future = _startSudoProcess(host, port); 119 | 120 | // Check every 100ms whether the server is already running 121 | Timer.periodic(const Duration(milliseconds: 100), (timer) { 122 | if (_processOut.toString().contains("\nResolving with:")) { 123 | timer.cancel(); 124 | _configureMacOS(host); 125 | } 126 | }); 127 | await future; 128 | return; 129 | } 130 | } 131 | } 132 | 133 | // Start server 134 | if (!isSilent) { 135 | print(""); 136 | print("Starting local DNS proxy at: 127.0.0.1:$port"); 137 | print("Resolving with: ${client.url}"); 138 | print(""); 139 | } 140 | await DnsServer.bind(filteringClient, address: host, port: port); 141 | } 142 | 143 | void _startSudoProcess(InternetAddress address, int port) async { 144 | final executable = "sudo"; 145 | final executableArgs = [Platform.executable] 146 | ..addAll(Platform.executableArguments) 147 | ..add(Platform.script.path) 148 | ..addAll(_mainArgs); 149 | final i = executableArgs.lastIndexOf("--configure"); 150 | if (i < 0) { 151 | final commandString = "$executable ${executableArgs.join(' ')}"; 152 | throw StateError("Failed to remove '--configure' from: $commandString"); 153 | } 154 | executableArgs.removeAt(i); 155 | print(""" 156 | -------------------------------------------------------------------------------- 157 | Setting up DNS server at port $port requires administrator permissions. 158 | -------------------------------------------------------------------------------- 159 | 160 | Therefore, we run: 161 | $executable ${executableArgs.join(' ')} 162 | 163 | Command 'sudo' usually asks your password. 164 | -------------------------------------------------------------------------------- 165 | """); 166 | final process = await Process.start(executable, executableArgs); 167 | _pipeToTerminal(process); 168 | } 169 | 170 | final StringBuffer _processOut = StringBuffer(); 171 | 172 | void _pipeToTerminal(Process process) { 173 | stdin.pipe(process.stdin); 174 | process.stdout.listen((data) { 175 | _processOut.write(utf8.decode(data)); 176 | stdout.add(data); 177 | }); 178 | process.stderr.pipe(stderr); 179 | } 180 | 181 | static void _configureMacOS(InternetAddress address) async { 182 | final client = MacNetworkSettings(); 183 | final oldServers = await client.getDnsServers(); 184 | final oldServerString = oldServers.map((item) => item.address).join(', '); 185 | print(""); 186 | print("Old DNS servers: [$oldServerString]"); 187 | var isRestored = false; 188 | final onSignal = (ProcessSignal signal) async { 189 | if (!isRestored) { 190 | isRestored = true; 191 | print(""); 192 | print("Restoring old DNS servers: [$oldServerString]"); 193 | print(""); 194 | await client.setDnsServers(oldServers); 195 | } 196 | exit(0); 197 | }; 198 | ProcessSignal.sigint.watch().listen(onSignal); 199 | print("New DNS servers: [${address.address}]"); 200 | print(""); 201 | await client.setDnsServers([address]); 202 | } 203 | } 204 | 205 | String _whoami() { 206 | final result = Process.runSync("whoami", []); 207 | return (result.stdout as String).trim(); 208 | } 209 | -------------------------------------------------------------------------------- /dart_test.yaml: -------------------------------------------------------------------------------- 1 | platforms: [vm] -------------------------------------------------------------------------------- /example/dns.dart: -------------------------------------------------------------------------------- 1 | import 'package:dns/dns.dart'; 2 | import 'dart:async'; 3 | 4 | Future main(List args) async { 5 | for (var arg in args) { 6 | final client = HttpDnsClient.google(); 7 | final result = await client.lookup("google.com"); 8 | print("$arg --> ${result.join(' | ')}"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/dns.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library dns; 16 | 17 | export 'src/dns_client.dart'; 18 | export 'src/dns_packet.dart'; 19 | export 'src/dns_server.dart'; 20 | export 'src/filtering_dns_client.dart'; 21 | export 'src/http_dns_client.dart'; 22 | export 'src/udp_dns_client.dart'; 23 | -------------------------------------------------------------------------------- /lib/src/dns_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:ip/ip.dart'; 18 | import 'package:universal_io/io.dart'; 19 | 20 | import 'dns_packet.dart'; 21 | import 'http_dns_client.dart'; 22 | import 'udp_dns_client.dart'; 23 | 24 | /// Abstract superclass of DNS clients. 25 | /// 26 | /// Commonly used implementations: 27 | /// * [UdpDnsClient] 28 | /// * [HttpDnsClient] 29 | abstract class DnsClient { 30 | static const Duration defaultTimeout = Duration(seconds: 5); 31 | 32 | /// Queries IP address of the host and returns the list of all answers. 33 | Future> lookup(String name, 34 | {InternetAddressType type = InternetAddressType.any}); 35 | 36 | /// Queries IP address of the host and returns the full DNS packet. 37 | Future lookupPacket(String name, 38 | {InternetAddressType type = InternetAddressType.any}) async { 39 | final list = await lookup(name); 40 | final result = DnsPacket.withResponse(); 41 | result.answers = list.map((ipAddress) { 42 | final type = ipAddress is Ip4Address 43 | ? DnsResourceRecord.typeIp4 44 | : DnsResourceRecord.typeIp6; 45 | return DnsResourceRecord.withAnswer( 46 | name: name, type: type, data: ipAddress.toImmutableBytes()); 47 | }).toList(); 48 | return result; 49 | } 50 | 51 | Future handlePacket(DnsPacket packet, {Duration timeout}) async { 52 | if (packet.questions.isEmpty) { 53 | return null; 54 | } 55 | if (packet.questions.length == 1) { 56 | final question = packet.questions.single; 57 | switch (question.type) { 58 | case DnsQuestion.typeIp4: 59 | return lookupPacket(packet.questions.single.name, 60 | type: InternetAddressType.IPv4); 61 | case DnsQuestion.typeIp6: 62 | return lookupPacket(packet.questions.single.name, 63 | type: InternetAddressType.IPv4); 64 | default: 65 | return null; 66 | } 67 | } 68 | final result = DnsPacket.withResponse(); 69 | result.id = packet.id; 70 | result.answers = []; 71 | final futures = []; 72 | for (var question in packet.questions) { 73 | var type = InternetAddressType.any; 74 | switch (question.type) { 75 | case DnsQuestion.typeIp4: 76 | type = InternetAddressType.IPv4; 77 | break; 78 | case DnsQuestion.typeIp6: 79 | type = InternetAddressType.IPv6; 80 | break; 81 | } 82 | futures.add(lookupPacket(question.name, type: type).then((packet) { 83 | result.answers.addAll(packet.answers); 84 | })); 85 | } 86 | await Future.wait(futures).timeout(timeout ?? defaultTimeout); 87 | return result; 88 | } 89 | } 90 | 91 | /// Uses system DNS lookup method. 92 | class SystemDnsClient extends DnsClient { 93 | @override 94 | Future> lookup(String host, 95 | {InternetAddressType type = InternetAddressType.any}) async { 96 | final addresses = await InternetAddress.lookup(host, type: type); 97 | return addresses 98 | .map((item) => IpAddress.fromBytes(item.rawAddress)) 99 | .toList(); 100 | } 101 | } 102 | 103 | /// Superclass of packet-based clients. 104 | /// 105 | /// See: 106 | /// * [UdpDnsClient] 107 | /// * [HttpDnsClient] 108 | abstract class PacketBasedDnsClient extends DnsClient { 109 | Future lookupPacket(String host, 110 | {InternetAddressType type = InternetAddressType.any}); 111 | 112 | @override 113 | Future> lookup(String host, 114 | {InternetAddressType type = InternetAddressType.any}) async { 115 | final packet = await lookupPacket(host, type: type); 116 | final result = []; 117 | for (var answer in packet.answers) { 118 | if (answer.name == host) { 119 | final ipAddress = IpAddress.fromBytes(answer.data); 120 | result.add(ipAddress); 121 | } 122 | } 123 | return result; 124 | } 125 | } 126 | 127 | /// An exception that indicates failure by [DnsClient]. 128 | class DnsClientException implements Exception { 129 | final String message; 130 | 131 | DnsClientException(this.message); 132 | 133 | @override 134 | String toString() => message; 135 | } 136 | 137 | /// A DNS client that delegates operations to another client. 138 | class DelegatingDnsClient implements DnsClient { 139 | final DnsClient client; 140 | 141 | DelegatingDnsClient(this.client); 142 | 143 | @override 144 | Future> lookup(String host, 145 | {InternetAddressType type = InternetAddressType.any}) { 146 | return client.lookup(host, type: type); 147 | } 148 | 149 | @override 150 | Future handlePacket(DnsPacket packet, {Duration timeout}) { 151 | return client.handlePacket(packet, timeout: timeout); 152 | } 153 | 154 | @override 155 | Future lookupPacket(String host, 156 | {InternetAddressType type = InternetAddressType.any}) { 157 | return client.lookupPacket(host, type: type); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib/src/dns_packet.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:ip/foundation.dart'; 16 | import 'package:raw/raw.dart'; 17 | import 'package:meta/meta.dart'; 18 | 19 | const Protocol dns = Protocol("DNS"); 20 | 21 | void _writeDnsName(RawWriter writer, List parts, int startIndex, 22 | Map offsets) { 23 | // Store pointer in the map 24 | if (offsets != null) { 25 | final key = parts.join("."); 26 | final existingPointer = offsets[key]; 27 | if (existingPointer != null) { 28 | writer.writeUint16(0xC000 | existingPointer); 29 | return; 30 | } 31 | offsets[key] = writer.length - startIndex; 32 | } 33 | 34 | for (var i = 0; i < parts.length; i++) { 35 | final part = parts[i]; 36 | 37 | // Find pointer 38 | if (i >= 1 && offsets != null) { 39 | final offset = offsets[parts.skip(i).join(".")]; 40 | if (offset != null) { 41 | // Write pointer 42 | writer.writeUint16(0xc000 | offset); 43 | return; 44 | } 45 | } 46 | 47 | // Write length and string bytes 48 | writer.writeUint8(part.length); 49 | writer.writeUtf8Simple(part); 50 | } 51 | 52 | // Zero-length part means end of name parts 53 | writer.writeUint8(0); 54 | } 55 | 56 | List _readDnsName(RawReader reader, int startIndex) { 57 | var name = []; 58 | while (reader.availableLengthInBytes > 0) { 59 | // Read length 60 | final length = reader.readUint8(); 61 | 62 | if (length == 0) { 63 | // End of name 64 | break; 65 | } else if (length < 64) { 66 | // A label 67 | final value = reader.readUtf8(length); 68 | name.add(value); 69 | } else { 70 | // This is a pointer 71 | 72 | // Validate we received start index, 73 | // so we can actually handle pointers 74 | if (startIndex == null) { 75 | throw ArgumentError.notNull("startIndex"); 76 | } 77 | 78 | // Calculate and validate index in the data 79 | final byte1 = reader.readUint8(); 80 | final pointedIndex = startIndex + (((0x3F & length) << 8) | byte1); 81 | if (pointedIndex > reader.bufferAsByteData.lengthInBytes || 82 | reader.bufferAsByteData.getUint8(pointedIndex) >= 64) { 83 | final index = reader.index - 2; 84 | throw StateError( 85 | "invalid pointer from index 0x${index.toRadixString(16)} (decimal: $index) to index 0x${pointedIndex.toRadixString(16)} ($pointedIndex)", 86 | ); 87 | } 88 | 89 | final oldIndex = reader.index; 90 | reader.index = pointedIndex; 91 | 92 | // Read name 93 | final result = _readDnsName(reader, startIndex); 94 | 95 | reader.index = oldIndex; 96 | 97 | // Concatenate 98 | name.addAll(result); 99 | 100 | // End 101 | break; 102 | } 103 | } 104 | return name; 105 | } 106 | 107 | class DnsResourceRecord extends SelfCodec { 108 | static const int responseCodeNoError = 0; 109 | static const int responseCodeFormatError = 1; 110 | static const int responseCodeServerFailure = 2; 111 | static const int responseCodeNonExistentDomain = 3; 112 | static const int responseCodeNotImplemented = 4; 113 | static const int responseCodeQueryRefused = 5; 114 | static const int responseCodeNotInZone = 10; 115 | 116 | static String stringFromResponseCode(int code) { 117 | switch (code) { 118 | case responseCodeNoError: 119 | return "No error"; 120 | case responseCodeFormatError: 121 | return "Format error"; 122 | case responseCodeServerFailure: 123 | return "Server failure"; 124 | case responseCodeNonExistentDomain: 125 | return "Non-existent domain"; 126 | case responseCodeNotImplemented: 127 | return "Not implemented"; 128 | case responseCodeQueryRefused: 129 | return "Query refused"; 130 | case responseCodeNotInZone: 131 | return "Not in the zone"; 132 | default: 133 | return "Unknown"; 134 | } 135 | } 136 | 137 | /// A host address ("A" record). 138 | static const int typeIp4 = 1; 139 | 140 | /// Authoritative name server ("NS" record). 141 | static const int typeNameServer = 2; 142 | 143 | /// The canonical name for an alias ("CNAME" record). 144 | static const int typeCanonicalName = 5; 145 | 146 | /// Domain name pointer ("PTR" record). 147 | static const int typeDomainNamePointer = 12; 148 | 149 | /// Mail server ("MX" record) record. 150 | static const int typeMailServer = 15; 151 | 152 | /// Text record ("TXT" record). 153 | static const int typeText = 15; 154 | 155 | /// IPv6 host address record. 156 | static const int typeIp6 = 28; 157 | 158 | /// Server discovery ("SRV" record). 159 | static const int typeServerDiscovery = 33; 160 | 161 | static String stringFromType(int value) { 162 | return DnsQuestion.stringFromType(value); 163 | } 164 | 165 | static const int classInternetAddress = 1; 166 | 167 | static String stringFromClass(int value) { 168 | return DnsQuestion.stringFromClass(value); 169 | } 170 | 171 | /// List of name parts. 172 | /// 173 | /// It can be an immutable value. 174 | List nameParts = const []; 175 | 176 | set name(String value) { 177 | this.nameParts = value.split("."); 178 | } 179 | 180 | String get name => nameParts.join("."); 181 | 182 | /// 16-bit type 183 | int type = typeIp4; 184 | 185 | /// 16-it class 186 | int classy = classInternetAddress; 187 | 188 | /// 32-bit TTL 189 | int ttl = 0; 190 | 191 | /// Data 192 | List data = const []; 193 | 194 | DnsResourceRecord(); 195 | 196 | DnsResourceRecord.withAnswer( 197 | {@required String name, @required this.type, @required this.data}) { 198 | this.name = name; 199 | ttl = 600; 200 | } 201 | 202 | @override 203 | void encodeSelf(RawWriter writer, 204 | {int startIndex, Map pointers}) { 205 | // Write name 206 | // (a list of labels/pointers) 207 | _writeDnsName( 208 | writer, 209 | nameParts, 210 | startIndex, 211 | pointers, 212 | ); 213 | 214 | // 2-byte type 215 | writer.writeUint16(type); 216 | 217 | // 2-byte class 218 | writer.writeUint16(classy); 219 | 220 | // 4-byte time-to-live 221 | writer.writeUint32(ttl); 222 | 223 | // 2-byte length of answer data 224 | writer.writeUint16(data.length); 225 | 226 | // Answer data 227 | writer.writeBytes(data); 228 | } 229 | 230 | @override 231 | void decodeSelf(RawReader reader, {int startIndex}) { 232 | startIndex ??= 0; 233 | // Read name 234 | // (a list of labels/pointers) 235 | nameParts = _readDnsName(reader, startIndex); 236 | 237 | // 2-byte type 238 | type = reader.readUint16(); 239 | 240 | // 2-byte class 241 | classy = reader.readUint16(); 242 | 243 | // 4-byte time-to-live 244 | ttl = reader.readUint32(); 245 | 246 | // 2-byte length 247 | final dataLength = reader.readUint16(); 248 | 249 | // N-byte data 250 | data = reader.readUint8ListViewOrCopy(dataLength); 251 | } 252 | 253 | @override 254 | int encodeSelfCapacity() { 255 | var n = 64; 256 | for (var part in nameParts) { 257 | n += 1 + part.length; 258 | } 259 | return n; 260 | } 261 | } 262 | 263 | class DnsPacket extends Packet { 264 | static const int opQuery = 0; 265 | static const int opInverseQuery = 1; 266 | static const int opStatus = 2; 267 | static const int opNotify = 3; 268 | static const int opUpdate = 4; 269 | 270 | int _v0 = 0; 271 | 272 | List questions = const []; 273 | List answers = const []; 274 | List authorities = const []; 275 | List additionalRecords = const []; 276 | 277 | DnsPacket() { 278 | op = opQuery; 279 | isRecursionDesired = true; 280 | } 281 | 282 | DnsPacket.withResponse({DnsPacket request}) { 283 | op = opQuery; 284 | isResponse = true; 285 | if (request != null) { 286 | this.questions = []; 287 | } 288 | } 289 | 290 | int get id => extractUint32Bits(_v0, 16, 0xFFFF); 291 | 292 | set id(int value) { 293 | _v0 = transformUint32Bits(_v0, 16, 0xFFFF, value); 294 | } 295 | 296 | bool get isAuthorativeAnswer => extractUint32Bool(_v0, 10); 297 | 298 | set isAuthorativeAnswer(bool value) { 299 | _v0 = transformUint32Bool(_v0, 10, value); 300 | } 301 | 302 | bool get isRecursionAvailable => extractUint32Bool(_v0, 7); 303 | 304 | set isRecursionAvailable(bool value) { 305 | _v0 = transformUint32Bool(_v0, 7, value); 306 | } 307 | 308 | bool get isRecursionDesired => extractUint32Bool(_v0, 8); 309 | 310 | set isRecursionDesired(bool value) { 311 | _v0 = transformUint32Bool(_v0, 8, value); 312 | } 313 | 314 | bool get isResponse => extractUint32Bool(_v0, 15); 315 | 316 | set isResponse(bool value) { 317 | _v0 = transformUint32Bool(_v0, 15, value); 318 | } 319 | 320 | bool get isTruncated => extractUint32Bool(_v0, 9); 321 | 322 | set isTruncated(bool value) { 323 | _v0 = transformUint32Bool(_v0, 9, value); 324 | } 325 | 326 | int get op => extractUint32Bits(_v0, 11, 0xF); 327 | 328 | set op(int value) { 329 | _v0 = transformUint32Bits(_v0, 11, 0xF, value); 330 | } 331 | 332 | @override 333 | Protocol get protocol => dns; 334 | 335 | int get reservedBits => 0x3 & (_v0 >> 4); 336 | 337 | int get responseCode => extractUint32Bits(_v0, 0, 0xF); 338 | 339 | set responseCode(int value) { 340 | _v0 = transformUint32Bits(_v0, 0, 0xF, value); 341 | } 342 | 343 | @override 344 | void encodeSelf(RawWriter writer) { 345 | final startIndex = writer.length; 346 | 347 | // 4-byte span at index 0 348 | writer.writeUint32(_v0); 349 | 350 | // 2-byte span at index 4 351 | writer.writeUint16(questions.length); 352 | 353 | // 2-byte span at index 6 354 | writer.writeUint16(answers.length); 355 | 356 | // 2-byte span at index 8 357 | writer.writeUint16(authorities.length); 358 | 359 | // 2-byte span at index 10 360 | writer.writeUint16(additionalRecords.length); 361 | 362 | // Name -> pointer 363 | final pointers = {}; 364 | 365 | for (var item in questions) { 366 | item.encodeSelf( 367 | writer, 368 | startIndex: startIndex, 369 | pointers: pointers, 370 | ); 371 | } 372 | 373 | for (var item in answers) { 374 | item.encodeSelf( 375 | writer, 376 | startIndex: startIndex, 377 | pointers: pointers, 378 | ); 379 | } 380 | 381 | for (var item in authorities) { 382 | item.encodeSelf( 383 | writer, 384 | startIndex: startIndex, 385 | pointers: pointers, 386 | ); 387 | } 388 | 389 | for (var item in additionalRecords) { 390 | item.encodeSelf( 391 | writer, 392 | startIndex: startIndex, 393 | pointers: pointers, 394 | ); 395 | } 396 | } 397 | 398 | @override 399 | void decodeSelf(RawReader reader) { 400 | // Clear existing values 401 | questions = []; 402 | answers = []; 403 | authorities = []; 404 | additionalRecords = []; 405 | 406 | // Fixed header 407 | final startIndex = reader.index; 408 | 409 | // 4-byte span at index 0 410 | _v0 = reader.readUint32(); 411 | 412 | // 2-byte span at index 4 413 | var questionsLength = reader.readUint16(); 414 | 415 | // 2-byte span at index 6 416 | var answersLength = reader.readUint16(); 417 | 418 | // 2-byte span at index 8 419 | var nameServerResourcesLength = reader.readUint16(); 420 | 421 | // 2-byte span at index 10 422 | var additionalResourcesLength = reader.readUint16(); 423 | 424 | for (; questionsLength > 0; questionsLength--) { 425 | final item = DnsQuestion(); 426 | item.decodeSelf(reader, startIndex: startIndex); 427 | questions.add(item); 428 | } 429 | 430 | for (; answersLength > 0; answersLength--) { 431 | final item = DnsResourceRecord(); 432 | item.decodeSelf(reader, startIndex: startIndex); 433 | answers.add(item); 434 | } 435 | 436 | for (; nameServerResourcesLength > 0; nameServerResourcesLength--) { 437 | final item = DnsResourceRecord(); 438 | item.decodeSelf(reader, startIndex: startIndex); 439 | authorities.add(item); 440 | } 441 | 442 | for (; additionalResourcesLength > 0; additionalResourcesLength--) { 443 | final item = DnsResourceRecord(); 444 | item.decodeSelf(reader, startIndex: startIndex); 445 | additionalRecords.add(item); 446 | } 447 | } 448 | 449 | @override 450 | int encodeSelfCapacity() { 451 | var n = 64; 452 | for (var item in questions) { 453 | n += item.encodeSelfCapacity(); 454 | } 455 | for (var item in answers) { 456 | n += item.encodeSelfCapacity(); 457 | } 458 | for (var item in authorities) { 459 | n += item.encodeSelfCapacity(); 460 | } 461 | for (var item in additionalRecords) { 462 | n += item.encodeSelfCapacity(); 463 | } 464 | return n; 465 | } 466 | } 467 | 468 | class DnsQuestion extends SelfCodec { 469 | // ----- 470 | // Types 471 | // ----- 472 | 473 | static const int typeIp4 = 1; 474 | static const int typeNameServer = 2; 475 | static const int typeCanonicalName = 5; 476 | static const int typeMailServer = 15; 477 | static const int typeTxt = 16; 478 | static const int typeIp6 = 28; 479 | 480 | static String stringFromType(int type) { 481 | switch (type) { 482 | case typeIp4: 483 | return "IPv4"; 484 | case typeNameServer: 485 | return "name server"; 486 | case typeCanonicalName: 487 | return "Canonical name"; 488 | case typeIp6: 489 | return "IPv6"; 490 | case typeMailServer: 491 | return "MX"; 492 | case typeTxt: 493 | return "TXT"; 494 | default: 495 | return "type $type"; 496 | } 497 | } 498 | 499 | // ------- 500 | // Classes 501 | // ------- 502 | 503 | static const int classInternetAddress = 1; 504 | 505 | static String stringFromClass(int type) { 506 | switch (type) { 507 | case classInternetAddress: 508 | return "Internet address"; 509 | default: 510 | return "class $type"; 511 | } 512 | } 513 | 514 | /// List of name parts. 515 | /// 516 | /// It can be an immutable value. 517 | List nameParts = []; 518 | 519 | set name(String value) { 520 | this.nameParts = value.split("."); 521 | } 522 | 523 | String get name => nameParts.join("."); 524 | 525 | /// 16-bit type 526 | int type = typeIp4; 527 | 528 | /// 16-bit class 529 | int classy = classInternetAddress; 530 | 531 | DnsQuestion({String host}) { 532 | if (host != null) { 533 | nameParts = host.split("."); 534 | } 535 | } 536 | 537 | @override 538 | void encodeSelf(RawWriter writer, 539 | {int startIndex, Map pointers}) { 540 | // Write name 541 | _writeDnsName( 542 | writer, 543 | nameParts, 544 | startIndex, 545 | pointers, 546 | ); 547 | 548 | // 2-byte type 549 | writer.writeUint16(type); 550 | 551 | // 2-byte class 552 | writer.writeUint16(classy); 553 | } 554 | 555 | @override 556 | void decodeSelf(RawReader reader, {int startIndex}) { 557 | // Name 558 | this.nameParts = _readDnsName(reader, startIndex); 559 | 560 | // 2-byte question type 561 | type = reader.readUint16(); 562 | 563 | // 2-byte question class 564 | classy = reader.readUint16(); 565 | } 566 | 567 | @override 568 | int encodeSelfCapacity() { 569 | var n = 16; 570 | for (var part in nameParts) { 571 | n += 1 + part.length; 572 | } 573 | return n; 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /lib/src/dns_server.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:raw/raw.dart'; 18 | import 'package:universal_io/io.dart'; 19 | 20 | import 'dns_client.dart'; 21 | import 'dns_packet.dart'; 22 | 23 | class DnsServer { 24 | static const int defaultPort = 53; 25 | 26 | final RawDatagramSocket socket; 27 | final DnsClient client; 28 | 29 | DnsServer(this.socket, this.client) 30 | : assert(socket != null), 31 | assert(client != null); 32 | 33 | void close() { 34 | socket.close(); 35 | } 36 | 37 | static Future bind(DnsClient client, 38 | {InternetAddress address, int port = defaultPort}) async { 39 | address ??= InternetAddress.loopbackIPv4; 40 | final socket = await RawDatagramSocket.bind(address, port); 41 | final server = DnsServer(socket, client); 42 | socket.listen((event) { 43 | if (event == RawSocketEvent.read) { 44 | while (true) { 45 | final datagram = socket.receive(); 46 | if (datagram == null) { 47 | break; 48 | } 49 | server._receivedDatagram(datagram); 50 | } 51 | } 52 | }); 53 | return server; 54 | } 55 | 56 | void _receivedDatagram(Datagram datagram) async { 57 | // Decode packet 58 | final dnsPacket = DnsPacket(); 59 | dnsPacket.decodeSelf(RawReader.withBytes(datagram.data)); 60 | receivedDnsPacket(dnsPacket, datagram.address, datagram.port); 61 | } 62 | 63 | void receivedDnsPacket( 64 | DnsPacket packet, InternetAddress address, int port) async { 65 | // Handle packet 66 | final result = await client.handlePacket(packet); 67 | 68 | if (result != null) { 69 | // Send response back 70 | result.id = packet.id; 71 | socket.send(result.toImmutableBytes(), address, port); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/dns_settings.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:universal_io/io.dart'; 18 | 19 | /// Provides access to Mac OS X network settings. 20 | class MacNetworkSettings { 21 | /// Returns DNS servers. 22 | Future> getDnsServers() async { 23 | final result = await _networkSetup(["-getdnsservers", "Wi-Fi"]); 24 | if (result.contains("There aren't any")) { 25 | return []; 26 | } 27 | return result 28 | .split("\n") 29 | .map((line) { 30 | line = line.trim(); 31 | if (line.isEmpty) { 32 | return null; 33 | } 34 | try { 35 | return InternetAddress(line); 36 | } catch (e) { 37 | return null; 38 | } 39 | }) 40 | .where((item) => item != null) 41 | .toList(); 42 | } 43 | 44 | /// Sets DNS servers. 45 | Future setDnsServers(List addresses) async { 46 | final args = ["-setdnsservers", "Wi-Fi"]; 47 | if (addresses.isEmpty) { 48 | args.add("Empty"); 49 | } else { 50 | args.addAll(addresses.map((item) => item.address)); 51 | } 52 | return _networkSetup(args); 53 | } 54 | 55 | Future _networkSetup(List args) async { 56 | if (Platform.isMacOS == false) { 57 | throw StateError("The current operating system is not Mac OS X"); 58 | } 59 | const executable = "networksetup"; 60 | final process = await Process.run( 61 | executable, 62 | args, 63 | stdoutEncoding: systemEncoding, 64 | ); 65 | final stderr = process.stderr as String; 66 | if (stderr.isNotEmpty) { 67 | throw StateError("Error: $stderr"); 68 | } 69 | return process.stdout as String; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/filtering_dns_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:ip/ip.dart'; 18 | import 'package:universal_io/io.dart'; 19 | 20 | import 'dns_client.dart'; 21 | import 'dns_packet.dart'; 22 | 23 | typedef Callback = void Function(T argument); 24 | 25 | /// A DNS client that can log or modify questions and answers. 26 | class FilteringDnsClient extends DelegatingDnsClient { 27 | final Callback beforeOperation; 28 | final Callback afterOperation; 29 | 30 | FilteringDnsClient(DnsClient client, 31 | {this.beforeOperation, this.afterOperation}) 32 | : super(client); 33 | 34 | @override 35 | Future> lookup(String host, 36 | {InternetAddressType type = InternetAddressType.any}) async { 37 | if (beforeOperation != null) { 38 | final packet = DnsPacket(); 39 | packet.questions.add(DnsQuestion(host: host)); 40 | beforeOperation(packet); 41 | } 42 | final result = await super.lookup(host); 43 | if (afterOperation != null) { 44 | final packet = DnsPacket(); 45 | for (var ip in result) { 46 | final answer = DnsResourceRecord(); 47 | answer.name = host; 48 | if (ip.isIpv4) { 49 | answer.type = DnsResourceRecord.typeIp4; 50 | } else { 51 | answer.type = DnsResourceRecord.typeIp6; 52 | } 53 | answer.data = ip.toImmutableBytes(); 54 | packet.answers.add(answer); 55 | } 56 | afterOperation(packet); 57 | } 58 | return result; 59 | } 60 | 61 | @override 62 | Future handlePacket(DnsPacket packet, {Duration timeout}) async { 63 | if (beforeOperation != null) { 64 | beforeOperation(packet); 65 | } 66 | final result = await super.handlePacket(packet, timeout: timeout); 67 | if (result != null && afterOperation != null) { 68 | afterOperation(result); 69 | } 70 | return result; 71 | } 72 | 73 | @override 74 | Future lookupPacket(String host, 75 | {InternetAddressType type = InternetAddressType.any}) async { 76 | if (beforeOperation != null) { 77 | final packet = DnsPacket(); 78 | packet.questions.add(DnsQuestion(host: host)); 79 | beforeOperation(packet); 80 | } 81 | final result = await super.lookupPacket(host, type: type); 82 | if (afterOperation != null) { 83 | afterOperation(result); 84 | } 85 | return result; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/http_dns_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:ip/ip.dart'; 18 | import 'package:meta/meta.dart'; 19 | 20 | import 'dns_client.dart'; 21 | import 'dns_packet.dart'; 22 | import 'udp_dns_client.dart'; 23 | import 'package:universal_io/io.dart'; 24 | import 'dart:convert' as convert; 25 | 26 | /// DNS client that uses DNS-over-HTTPS protocol supported by Google and 27 | /// Cloudflare. 28 | /// 29 | /// See: 30 | /// * [Google DNS-over-HTTPS documentation](https://developers.google.com/speed/public-dns/docs/dns-over-https) 31 | /// * [Cloudflare DNS-over-HTTPS documentation](https://developers.cloudflare.com/1.1.1.1/dns-over-https/json-format/) 32 | /// * [IETF working group](https://datatracker.ietf.org/wg/doh/about/) 33 | /// 34 | class HttpDnsClient extends PacketBasedDnsClient { 35 | /// URL of the service (without query). 36 | final String url; 37 | final String _urlHost; 38 | 39 | /// Resolves host of the the URL. 40 | final DnsClient urlClient; 41 | 42 | /// Whether to hide client IP address from the authoritative server. 43 | final bool maximalPrivacy; 44 | 45 | /// Default timeout for operations. 46 | final Duration timeout; 47 | 48 | /// Constructs a DNS-over-HTTPS client. 49 | /// 50 | /// If `maximalPrivacy` is true, then DNS client should do its best to hide 51 | /// IP address of the client from the authoritative DNS server. 52 | /// 53 | /// If `urlClient` is non-null, it will be used to resolve IP of the 54 | /// host of the URL. Otherwise DNS-over-UDP server at 8.8.8.8 will be used. 55 | HttpDnsClient(this.url, 56 | {this.timeout, this.maximalPrivacy = false, this.urlClient}) 57 | : this._urlHost = Uri.parse(url).host { 58 | if (url.contains("?")) { 59 | throw ArgumentError.value(url, "url"); 60 | } 61 | } 62 | 63 | /// Constructs a DNS-over-HTTPS client that uses Google's free servers. 64 | /// 65 | /// If `maximalPrivacy` is true, we will ask Google to hide our IP from the 66 | /// authoritative DNS server. Default is false, which enables the DNS server 67 | /// to return us physically close IPs, resulting in potentially much better 68 | /// throughput/latency. 69 | /// 70 | /// If `urlClient` is non-null, it will be used to resolve 'dns.google.com'. 71 | /// Otherwise DNS-over-UDP server at 8.8.8.8 will be used. 72 | /// 73 | /// See [documentation at developers.google.com](https://developers.google.com/speed/public-dns/docs/dns-over-https). 74 | HttpDnsClient.google({ 75 | Duration timeout, 76 | maximalPrivacy = false, 77 | DnsClient urlClient, 78 | }) : this( 79 | "https://dns.google.com/resolve", 80 | timeout: timeout, 81 | maximalPrivacy: maximalPrivacy, 82 | urlClient: urlClient, 83 | ); 84 | 85 | @override 86 | Future lookupPacket(String host, 87 | {InternetAddressType type = InternetAddressType.any}) async { 88 | // Are we are resolving host of the DNS-over-HTTPS service? 89 | if (host == _urlHost) { 90 | final selfClient = this.urlClient ?? UdpDnsClient.google(); 91 | return selfClient.lookupPacket(host, type: type); 92 | } 93 | 94 | // Build URL 95 | var url = "${this.url}?name=${Uri.encodeQueryComponent(host)}"; 96 | 97 | // Add: IPv4 or IPv6? 98 | if (type == null) { 99 | throw ArgumentError.notNull("type"); 100 | } else if (type == InternetAddressType.any || 101 | type == InternetAddressType.IPv4) { 102 | url += "&type=A"; 103 | } else { 104 | url += "&type=AAAA"; 105 | } 106 | 107 | // Hide my IP? 108 | if (maximalPrivacy) { 109 | url += "&edns_client_subnet=0.0.0.0/0"; 110 | } 111 | 112 | // Fetch using 'universal_io' HttpClient 113 | final request = await HttpClient().getUrl(Uri.parse(url)); 114 | final response = await request.close(); 115 | try { 116 | if (response.statusCode != 200) { 117 | throw StateError( 118 | "HTTP response was ${response.statusCode} (${response.reasonPhrase}). URL was: $url"); 119 | } 120 | final contentType = response.headers.contentType; 121 | switch (contentType.mimeType) { 122 | case "application/json": 123 | break; 124 | case "application/x-javascript": // <-- Google's server returns this? 125 | break; 126 | default: 127 | throw StateError( 128 | "HTTP response content type was $contentType'. URL was: $url"); 129 | } 130 | } catch (e) { 131 | // ignore: unawaited_futures 132 | response.listen((_){}).cancel(); 133 | rethrow; 134 | } 135 | 136 | // Decode JSON 137 | final data = await convert.utf8.decodeStream(response); 138 | final json = convert.json.decode(data); 139 | 140 | // Decode DNS packet from JSON 141 | return decodeDnsPacket(json); 142 | } 143 | 144 | /// Converts JSON object to [DnsPacket]. 145 | @visibleForTesting 146 | DnsPacket decodeDnsPacket(Object json) { 147 | if (json is Map) { 148 | final result = DnsPacket.withResponse(); 149 | for (var key in json.keys) { 150 | final value = json[key]; 151 | 152 | switch (key) { 153 | case "Status": 154 | result.responseCode = (value as num).toInt(); 155 | break; 156 | 157 | case "AA": 158 | result.isAuthorativeAnswer = value as bool; 159 | break; 160 | 161 | case "ID": 162 | result.id = (value as num).toInt(); 163 | break; 164 | 165 | case "QR": 166 | result.isResponse = value as bool; 167 | break; 168 | 169 | case "RA": 170 | result.isRecursionAvailable = value as bool; 171 | break; 172 | 173 | case "RD": 174 | result.isRecursionDesired = value as bool; 175 | break; 176 | 177 | case "TC": 178 | result.isTruncated = value as bool; 179 | break; 180 | 181 | case "Question": 182 | final questions = []; 183 | result.questions = questions; 184 | if (value is List) { 185 | for (var item in value) { 186 | questions.add(decodeDnsQuestion(item)); 187 | } 188 | } 189 | break; 190 | 191 | case "Answer": 192 | final answers = []; 193 | result.answers = answers; 194 | if (value is List) { 195 | for (var item in value) { 196 | answers.add(decodeDnsResourceRecord(item)); 197 | } 198 | } 199 | break; 200 | 201 | case "Additional": 202 | final additionalRecords = []; 203 | result.additionalRecords = additionalRecords; 204 | if (value is List) { 205 | for (var item in value) { 206 | additionalRecords.add(decodeDnsResourceRecord(item)); 207 | } 208 | } 209 | break; 210 | } 211 | } 212 | return result; 213 | } else { 214 | throw ArgumentError.value(json); 215 | } 216 | } 217 | 218 | /// Converts JSON object to [DnsQuestion]. 219 | @visibleForTesting 220 | DnsQuestion decodeDnsQuestion(Object json) { 221 | if (json is Map) { 222 | final result = DnsQuestion(); 223 | for (var key in json.keys) { 224 | final value = json[key]; 225 | switch (key) { 226 | case "name": 227 | result.name = _trimDotSuffix(value as String); 228 | break; 229 | } 230 | } 231 | return result; 232 | } else { 233 | throw ArgumentError.value(json); 234 | } 235 | } 236 | 237 | /// Converts JSON object to [DnsResourceRecord]. 238 | @visibleForTesting 239 | DnsResourceRecord decodeDnsResourceRecord(Object json) { 240 | if (json is Map) { 241 | final result = DnsResourceRecord(); 242 | for (var key in json.keys) { 243 | final value = json[key]; 244 | switch (key) { 245 | case "name": 246 | result.name = _trimDotSuffix(value as String); 247 | break; 248 | 249 | case "type": 250 | result.type = (value as num).toInt(); 251 | break; 252 | 253 | case "TTL": 254 | result.ttl = (value as num).toInt(); 255 | break; 256 | 257 | case "data": 258 | result.data = IpAddress.parse(value).toImmutableBytes(); 259 | break; 260 | } 261 | } 262 | return result; 263 | } else { 264 | throw ArgumentError.value(json); 265 | } 266 | } 267 | 268 | static String _trimDotSuffix(String s) { 269 | if (s.endsWith(".")) { 270 | return s.substring(0, s.length - 1); 271 | } 272 | return s; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /lib/src/udp_dns_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | import 'dart:collection'; 17 | import 'dart:math'; 18 | 19 | import 'package:ip/ip.dart'; 20 | import 'package:meta/meta.dart'; 21 | import 'package:raw/raw.dart'; 22 | import 'package:universal_io/io.dart'; 23 | 24 | import 'dns_client.dart'; 25 | import 'dns_packet.dart'; 26 | 27 | /// A standard DNS-over-UDP client implementation. 28 | class UdpDnsClient extends PacketBasedDnsClient { 29 | static final _portRandom = Random.secure(); 30 | final InternetAddress remoteAddress; 31 | final int remotePort; 32 | final InternetAddress localAddress; 33 | final int localPort; 34 | final Duration timeout; 35 | Future _socket; 36 | 37 | final LinkedList<_DnsResponseWaiter> _responseWaiters = 38 | LinkedList<_DnsResponseWaiter>(); 39 | 40 | UdpDnsClient( 41 | {@required this.remoteAddress, 42 | this.remotePort = 53, 43 | this.localAddress, 44 | this.localPort, 45 | this.timeout}) { 46 | if (remoteAddress == null) { 47 | throw ArgumentError.notNull("remoteAddress"); 48 | } 49 | if (remotePort == null) { 50 | throw ArgumentError.notNull("remotePort"); 51 | } 52 | } 53 | 54 | factory UdpDnsClient.google() { 55 | return UdpDnsClient(remoteAddress: InternetAddress("8.8.8.8")); 56 | } 57 | 58 | @override 59 | Future lookupPacket(String host, 60 | {InternetAddressType type = InternetAddressType.any}) async { 61 | final socket = await _getSocket(); 62 | final dnsPacket = DnsPacket(); 63 | dnsPacket.questions = [DnsQuestion(host: host)]; 64 | 65 | // Add query to list of unfinished queries 66 | final responseWaiter = _DnsResponseWaiter(host); 67 | _responseWaiters.add(responseWaiter); 68 | 69 | // Send query 70 | socket.send( 71 | dnsPacket.toImmutableBytes(), 72 | remoteAddress, 73 | remotePort, 74 | ); 75 | 76 | // Get timeout for response 77 | final timeout = this.timeout ?? DnsClient.defaultTimeout; 78 | 79 | // Set timer 80 | responseWaiter.timer = Timer(timeout, () { 81 | // Ignore if already completed 82 | if (responseWaiter.completer.isCompleted) { 83 | return; 84 | } 85 | 86 | // Remove from the list of response waiters 87 | responseWaiter.unlink(); 88 | 89 | // Complete the future 90 | responseWaiter.completer.completeError( 91 | TimeoutException("DNS query '$host' timeout"), 92 | ); 93 | }); 94 | 95 | // Return future 96 | return responseWaiter.completer.future; 97 | } 98 | 99 | Future _getSocket() async { 100 | if (_socket != null) { 101 | return _socket; 102 | } 103 | final localAddress = this.localAddress; 104 | final localPort = this.localPort; 105 | final socket = await _bindSocket( 106 | localAddress, 107 | localPort, 108 | ); 109 | socket.listen((event) { 110 | if (event == RawSocketEvent.read) { 111 | // Read UDP packet 112 | final datagram = socket.receive(); 113 | if (datagram == null) { 114 | return; 115 | } 116 | _receiveUdpPacket(datagram); 117 | } 118 | }); 119 | return socket; 120 | } 121 | 122 | void _receiveUdpPacket(Datagram datagram) { 123 | // Read DNS packet 124 | final dnsPacket = DnsPacket(); 125 | dnsPacket.decodeSelf(RawReader.withBytes(datagram.data)); 126 | 127 | // Read answers 128 | for (var answer in dnsPacket.answers) { 129 | final host = answer.name; 130 | var removedResponseWaiters = <_DnsResponseWaiter>[]; 131 | for (var query in _responseWaiters) { 132 | if (query.completer.isCompleted == false && query.host == host) { 133 | removedResponseWaiters.add(query); 134 | query.timer.cancel(); 135 | query.completer.complete(dnsPacket); 136 | break; 137 | } 138 | } 139 | for (var removed in removedResponseWaiters) { 140 | removed.unlink(); 141 | } 142 | } 143 | } 144 | 145 | /// Binds socket. If port is null, attempts 3 random ports before giving up. 146 | static Future _bindSocket( 147 | InternetAddress address, int port) async { 148 | address ??= InternetAddress.anyIPv4; 149 | for (var n = 3; n > 0; n--) { 150 | try { 151 | return await RawDatagramSocket.bind(address, port ?? _randomPort()); 152 | } catch (e) { 153 | if (port == null && n > 1 && e.toString().contains("port")) { 154 | return null; 155 | } 156 | rethrow; 157 | } 158 | } 159 | throw StateError("impossible state"); 160 | } 161 | 162 | static int _randomPort() { 163 | const min = 10000; 164 | return min + _portRandom.nextInt((1 << 16) - min); 165 | } 166 | } 167 | 168 | class _DnsResponseWaiter extends LinkedListEntry<_DnsResponseWaiter> { 169 | final String host; 170 | final Completer completer = Completer(); 171 | Timer timer; 172 | final List result = []; 173 | 174 | _DnsResponseWaiter(this.host); 175 | } 176 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dns 2 | version: 0.1.3 3 | author: terrier989 4 | homepage: https://github.com/terrier989/dns 5 | description: A package for working with the DNS protocol. 6 | 7 | environment: 8 | sdk: ">=2.4.0 <3.0.0" 9 | 10 | executables: 11 | dns-proxy: dns-proxy 12 | 13 | dependencies: 14 | args: ^1.5.2 15 | meta: ^1.1.7 16 | ip: ^0.1.1 17 | raw: ^0.2.0 18 | universal_io: '>=0.7.0 <0.9.0' 19 | 20 | dev_dependencies: 21 | pedantic: ^1.8.0 22 | test: ^1.6.5 23 | -------------------------------------------------------------------------------- /test/dns_client_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:dns/dns.dart'; 16 | import 'package:test/test.dart'; 17 | import 'package:universal_io/io.dart'; 18 | 19 | void main() { 20 | group("DnsClient.system", () { 21 | test("lookupPacket('google.com')", () async { 22 | final client = SystemDnsClient(); 23 | final packet = await client.lookupPacket("google.com"); 24 | expect(packet, isNotNull); 25 | expect(packet.isResponse, isTrue); 26 | expect(packet.answers, hasLength(greaterThan(0))); 27 | expect(packet.answers[0].name, "google.com"); 28 | expect(packet.answers[0].data, hasLength(greaterThan(1))); 29 | }, testOn: "vm"); 30 | 31 | test("lookup('google.com')", () async { 32 | final client = SystemDnsClient(); 33 | final response = await client.lookup("google.com"); 34 | expect(response, hasLength(greaterThan(0))); 35 | }, testOn: "vm"); 36 | }); 37 | 38 | group("UdpDnsClient", () { 39 | test("lookupPacket('google.com')", () async { 40 | final client = UdpDnsClient( 41 | remoteAddress: InternetAddress("8.8.8.8"), 42 | ); 43 | final packet = await client.lookupPacket("google.com"); 44 | expect(packet, isNotNull); 45 | expect(packet.isResponse, isTrue); 46 | expect(packet.answers, hasLength(greaterThan(0))); 47 | expect(packet.answers[0].name, "google.com"); 48 | expect(packet.answers[0].data, hasLength(greaterThan(1))); 49 | }, testOn: "vm"); 50 | 51 | test("lookup('google.com')", () async { 52 | final client = UdpDnsClient( 53 | remoteAddress: InternetAddress("8.8.8.8"), 54 | ); 55 | final response = await client.lookup("google.com"); 56 | expect(response, hasLength(greaterThan(0))); 57 | }, testOn: "vm"); 58 | }); 59 | 60 | group("HttpDnsClient", () { 61 | test("lookupPacket('google.com')", () async { 62 | final client = HttpDnsClient.google(); 63 | final packet = await client.lookupPacket("google.com"); 64 | expect(packet, isNotNull); 65 | expect(packet.isResponse, isTrue); 66 | expect(packet.answers, hasLength(greaterThan(0))); 67 | expect(packet.answers[0].name, "google.com"); 68 | expect(packet.answers[0].data, hasLength(greaterThan(1))); 69 | }); 70 | 71 | test("lookup('google.com')", () async { 72 | final client = HttpDnsClient.google(); 73 | final addresses = await client.lookup("google.com"); 74 | expect(addresses, hasLength(greaterThan(0))); 75 | for (var address in addresses) { 76 | expect(address, isNotNull); 77 | expect(address.toImmutableBytes(), hasLength(greaterThan(3))); 78 | } 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /test/dns_packet_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:dns/dns.dart'; 16 | import 'package:ip/foundation.dart'; 17 | import 'package:raw/raw.dart'; 18 | import 'package:raw/test_helpers.dart'; 19 | import 'package:test/test.dart'; 20 | 21 | void main() { 22 | group("DnsPacket", () { 23 | group("default", () { 24 | final example = DnsPacket(); 25 | 26 | test("encode, decode", () { 27 | final reader = RawReader.withBytes(example.toImmutableBytes()); 28 | final decoded = DnsPacket(); 29 | decoded.decodeSelf(reader); 30 | expect(decoded, selfEncoderEquals(example)); 31 | expect(reader.availableLengthInBytes, 0); 32 | }); 33 | }); 34 | 35 | group("example #1", () { 36 | List exampleBytes; 37 | DnsPacket example; 38 | 39 | setUp(() { 40 | exampleBytes = const DebugHexDecoder().convert(""" 41 | 0x0000: db42 8180 0001 0001 0000 0000 0377 7777 42 | 0x0010: 0765 7861 6d70 6c65 0363 6f6d 0000 0100 43 | 0x0020: 01c0 0c00 0100 0100 0002 5800 049b 2111 44 | 0x0030: 44 45 | """); 46 | 47 | final question = DnsQuestion(); 48 | question.nameParts.addAll(["www", "example", "com"]); 49 | question.type = 1; 50 | question.classy = 1; 51 | 52 | final answer = DnsResourceRecord(); 53 | answer.nameParts = const ["www", "example", "com"]; 54 | answer.type = 1; 55 | answer.classy = 1; 56 | answer.ttl = 600; 57 | answer.data = const [0x9b, 0x21, 0x11, 0x44]; 58 | 59 | example = DnsPacket(); 60 | example.id = 0xdb42; 61 | example.isResponse = true; 62 | example.op = 0; 63 | example.isTruncated = false; 64 | example.isRecursionDesired = true; 65 | example.isRecursionAvailable = true; 66 | example.responseCode = 0; 67 | example.questions = [question]; 68 | example.answers = [answer]; 69 | }); 70 | 71 | test("decoded properties", () { 72 | final decoded = DnsPacket(); 73 | decoded.decodeSelf(RawReader.withBytes(exampleBytes)); 74 | 75 | // 76 | // First bytes 77 | // 78 | 79 | expect(decoded.isResponse, equals(true)); 80 | expect(decoded.op, equals(0)); 81 | expect(decoded.isTruncated, equals(false)); 82 | expect(decoded.isRecursionDesired, equals(true)); 83 | expect(decoded.isRecursionAvailable, equals(true)); 84 | expect(decoded.responseCode, equals(0)); 85 | 86 | // 87 | // Questions 88 | // 89 | 90 | expect(decoded.questions, hasLength(1)); 91 | 92 | final decodedQuestion = decoded.questions.single; 93 | final expectedQuestion = example.questions.single; 94 | 95 | expect(decodedQuestion.nameParts, equals(expectedQuestion.nameParts)); 96 | expect(decodedQuestion.type, equals(expectedQuestion.type)); 97 | expect(decodedQuestion.classy, equals(expectedQuestion.classy)); 98 | expect(decoded.questions, orderedEquals([expectedQuestion])); 99 | 100 | // 101 | // Answers 102 | // 103 | 104 | expect(decoded.answers, hasLength(1)); 105 | 106 | final decodedAnswer = decoded.answers.single; 107 | final expectedAnswer = example.answers.single; 108 | 109 | expect(decodedAnswer.nameParts, equals(expectedAnswer.nameParts)); 110 | expect(decodedAnswer.type, equals(expectedAnswer.type)); 111 | expect(decodedAnswer.classy, equals(expectedAnswer.classy)); 112 | expect(decodedAnswer.ttl, equals(expectedAnswer.ttl)); 113 | expect(decodedAnswer.data, equals(expectedAnswer.data)); 114 | expect(decoded.answers, orderedEquals([expectedAnswer])); 115 | 116 | // 117 | // Other 118 | // 119 | 120 | expect(decoded.authorities, hasLength(0)); 121 | expect(decoded.additionalRecords, hasLength(0)); 122 | }); 123 | 124 | test("encode, decode, encode", () { 125 | // encode 126 | final writer = RawWriter.withCapacity(500); 127 | example.encodeSelf(writer); 128 | final encoded = writer.toUint8ListView(); 129 | expect(encoded, byteListEquals(exampleBytes)); 130 | final encodedReader = RawReader.withBytes(encoded); 131 | 132 | // encode -> decode 133 | final decoded = DnsPacket(); 134 | decoded.decodeSelf(encodedReader); 135 | 136 | // encode -> decode -> encode 137 | // (the next two lines should both encode) 138 | expect(decoded.toImmutableBytes(), byteListEquals(exampleBytes)); 139 | expect(decoded, selfEncoderEquals(example)); 140 | expect(encodedReader.availableLengthInBytes, 0); 141 | }); 142 | 143 | test("decode", () { 144 | final reader = RawReader.withBytes(exampleBytes); 145 | final decoded = DnsPacket(); 146 | decoded.decodeSelf(reader); 147 | expect(decoded, selfEncoderEquals(example)); 148 | expect(reader.availableLengthInBytes, 0); 149 | }); 150 | }); 151 | }); 152 | } 153 | -------------------------------------------------------------------------------- /test/dns_proxy_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Gohilla.com team. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:dns/dns.dart'; 18 | import 'package:test/test.dart'; 19 | import 'package:universal_io/io.dart'; 20 | 21 | import '../bin/dns-proxy.dart' as proxy; 22 | 23 | void main() { 24 | test("DNS proxy executable", () async { 25 | // Start server 26 | const port = 4242; 27 | proxy.main( 28 | ["serve", "--silent", "--host=127.0.0.1", "--port=${port.toString()}"]); 29 | 30 | // Wait 100ms 31 | await Future.delayed(const Duration(milliseconds: 100)); 32 | 33 | // Query "google.com" 34 | final client = UdpDnsClient( 35 | remoteAddress: InternetAddress.loopbackIPv4, 36 | remotePort: port, 37 | ); 38 | final result = await client.lookup("google.com"); 39 | 40 | // Expect at least 1 IP address 41 | expect(result, hasLength(greaterThan(0))); 42 | }); 43 | } 44 | --------------------------------------------------------------------------------