├── .gitignore ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bin └── multi_doh_server.dart ├── pubspec.lock └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | 5 | # Conventional directory for build outputs 6 | build/ 7 | 8 | # Directory created by dartdoc 9 | doc/api/ 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 redsolver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi-DoH Server 2 | 3 | Warning: This code is highly experimental and I don't recommend running it in production. 4 | 5 | The [SkyDroid App](https://github.com/redsolver/skydroid) uses this server to efficiently query a lot of domain names at once in a single request. 6 | 7 | It is part of my submission to the [‘Own The Internet’ Hackathon](https://gitcoin.co/hackathon/own-the-internet) 8 | 9 | This server requires a [HSD Node](https://github.com/handshake-org/hsd) running locally with `./bin/hsd --rs-port 53` 10 | 11 | Enabling Unbound support is recommended. 12 | 13 | This server listens on port 8053 and accepts HTTP connections. HTTPS can be enabled using a reverse proxy like Nginx. 14 | 15 | ## Limitations 16 | 17 | - Only TXT Records are supported 18 | - The server currently uses the `dig` command-line tool to run queries against the local resolver and parses the `stdout`. :D 19 | 20 | ## Example 21 | 22 | `POST` to `/multi-dns-query` 23 | ```json 24 | { 25 | "type": 16, 26 | "names": [ 27 | "example.com", 28 | "redsolver", 29 | "papagei" 30 | ] 31 | } 32 | ``` 33 | 34 | Response 35 | ```json 36 | { 37 | "type": 16, 38 | "names": { 39 | "example.com": [], 40 | "redsolver": [ 41 | "TXT Record 1", 42 | "TXT Record 2" 43 | ], 44 | "papagei": [ 45 | "something something" 46 | ] 47 | } 48 | } 49 | ``` 50 | 51 | ## How to deploy (if you really want to) 52 | 53 | 1. Get the [Dart SDK](https://dart.dev/get-dart) 54 | 2. `dart2native bin/multi_doh_server.dart` to produce a binary 55 | 3. Something like `scp bin/multi_doh_server.exe root@YOUR_SERVER_IP:/root/multi_doh_server/multi_doh_server.exe` to copy the binary to your server 56 | 4. Run the binary -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:pedantic/analysis_options.yaml 5 | 6 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 7 | # Uncomment to specify additional rules. 8 | # linter: 9 | # rules: 10 | # - camel_case_types 11 | 12 | analyzer: 13 | # exclude: 14 | # - path/to/excluded/files/** 15 | -------------------------------------------------------------------------------- /bin/multi_doh_server.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:math'; 4 | 5 | Map relevance = {}; 6 | 7 | final reFile = File('relevance.json'); 8 | 9 | void refreshLoop() async { 10 | while (true) { 11 | print('Refresh Loop'); 12 | 13 | for (var key in relevance.keys.toList()) { 14 | if (DateTime.now() 15 | .difference(DateTime.fromMillisecondsSinceEpoch(relevance[key])) > 16 | Duration(days: 7)) { 17 | print('no longer relevant, removing ${key}'); 18 | relevance.remove(key); 19 | } else { 20 | if (nameToTXTRecordsCache.containsKey(key)) { 21 | if (DateTime.now().difference(cacheTime[key]) > 22 | Duration(minutes: cacheTimeMinutes[key] - 3)) { 23 | 24 | await getName(key, useCache: false); 25 | } 26 | } else { 27 | await getName(key); 28 | } 29 | } 30 | } 31 | reFile.writeAsStringSync(json.encode(relevance)); 32 | await Future.delayed(Duration(seconds: 60)); 33 | } 34 | } 35 | 36 | Future main() async { 37 | if (reFile.existsSync()) { 38 | relevance = json.decode(reFile.readAsStringSync()).cast(); 39 | } 40 | 41 | refreshLoop(); 42 | 43 | final server = await HttpServer.bind( 44 | InternetAddress.loopbackIPv4, 45 | 8053, 46 | ); 47 | server.listen((event) { 48 | handleRequest(event); 49 | }); 50 | } 51 | 52 | Map> nameToTXTRecordsCache = {}; 53 | 54 | Map cacheTime = {}; 55 | 56 | Map cacheTimeMinutes = {}; 57 | 58 | void handleRequest(HttpRequest request) async { 59 | print(request.uri.path); 60 | 61 | try { 62 | if (request.method == 'POST') { 63 | if (request.uri.path == '/multi-dns-query') { 64 | final content = await utf8.decoder.bind(request).join(); 65 | var data = json.decode(content) as Map; 66 | print(data); 67 | 68 | if (data['type'] == 16 && data['names'].length <= 64) { 69 | final futures = []; 70 | 71 | final res = >{}; 72 | 73 | Future loadName(String name) async { 74 | relevance[name] = DateTime.now().millisecondsSinceEpoch; 75 | 76 | res[name] = await getName(name); 77 | } 78 | 79 | for (var name in data['names']) { 80 | futures.add(loadName(name)); 81 | } 82 | await Future.wait(futures); 83 | request.response.write(json.encode({'type': 16, 'names': res})); 84 | } else { 85 | request.response.statusCode = HttpStatus.badRequest; 86 | } 87 | } else { 88 | request.response.statusCode = HttpStatus.notFound; 89 | } 90 | } else { 91 | request.response.statusCode = HttpStatus.methodNotAllowed; 92 | } 93 | } catch (e) { 94 | request.response.statusCode = HttpStatus.internalServerError; 95 | print('Exception in handleRequest: $e'); 96 | } 97 | await request.response.close(); 98 | } 99 | 100 | Map retryCount = {}; 101 | 102 | Future> getName(String name, {bool useCache = true}) async { 103 | if (useCache) { 104 | if (nameToTXTRecordsCache.containsKey(name)) { 105 | if (DateTime.now().difference(cacheTime[name]) > 106 | Duration(minutes: cacheTimeMinutes[name])) { 107 | nameToTXTRecordsCache.remove(name); 108 | cacheTime.remove(name); 109 | cacheTimeMinutes.remove(name); 110 | } else { 111 | return nameToTXTRecordsCache[name]; 112 | } 113 | } 114 | } 115 | 116 | print('Getting $name'); 117 | 118 | try { 119 | List answers = []; 120 | int i = 0; 121 | 122 | while (true) { 123 | i++; 124 | final res = await Process.run( 125 | 'dig', ['+noall', '+answer', '@127.0.0.1', 'TXT', name]); // hmm... 126 | 127 | for (final String line in res.stdout.split('\n')) { 128 | if (line.isNotEmpty) { 129 | answers.add(line.split('"')[1]); 130 | } 131 | } 132 | if (answers.isNotEmpty) { 133 | break; 134 | } 135 | if (i > 5) { 136 | break; 137 | } 138 | await Future.delayed(Duration(milliseconds: 500)); 139 | } 140 | 141 | print(answers); 142 | 143 | if (answers.isNotEmpty) { 144 | nameToTXTRecordsCache[name] = answers; 145 | cacheTime[name] = DateTime.now(); 146 | cacheTimeMinutes[name] = Random().nextInt(30) + 30; 147 | } else { 148 | final count = retryCount[name] ?? 0; 149 | if (count > 3) { 150 | relevance.remove(name); 151 | retryCount.remove(name); 152 | } else { 153 | retryCount[name] = count + 1; 154 | } 155 | } 156 | 157 | return answers; 158 | } catch (e, st) { 159 | print(e); 160 | 161 | return null; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | args: 5 | dependency: transitive 6 | description: 7 | name: args 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "1.6.0" 11 | charcode: 12 | dependency: transitive 13 | description: 14 | name: charcode 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.1.3" 18 | collection: 19 | dependency: transitive 20 | description: 21 | name: collection 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.14.13" 25 | convert: 26 | dependency: transitive 27 | description: 28 | name: convert 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "2.1.1" 32 | crypto: 33 | dependency: "direct main" 34 | description: 35 | name: crypto 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.1.5" 39 | dns: 40 | dependency: "direct main" 41 | description: 42 | name: dns 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "0.1.3" 46 | fixnum: 47 | dependency: transitive 48 | description: 49 | name: fixnum 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "0.10.11" 53 | http: 54 | dependency: "direct main" 55 | description: 56 | name: http 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "0.12.2" 60 | http_parser: 61 | dependency: transitive 62 | description: 63 | name: http_parser 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "3.1.4" 67 | ip: 68 | dependency: transitive 69 | description: 70 | name: ip 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "0.1.1" 74 | matcher: 75 | dependency: transitive 76 | description: 77 | name: matcher 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "0.12.9" 81 | meta: 82 | dependency: transitive 83 | description: 84 | name: meta 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "1.2.2" 88 | path: 89 | dependency: transitive 90 | description: 91 | name: path 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "1.7.0" 95 | pedantic: 96 | dependency: "direct dev" 97 | description: 98 | name: pedantic 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "1.9.2" 102 | raw: 103 | dependency: transitive 104 | description: 105 | name: raw 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "0.2.0" 109 | source_span: 110 | dependency: transitive 111 | description: 112 | name: source_span 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "1.7.0" 116 | stack_trace: 117 | dependency: transitive 118 | description: 119 | name: stack_trace 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "1.9.5" 123 | string_scanner: 124 | dependency: transitive 125 | description: 126 | name: string_scanner 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "1.0.5" 130 | term_glyph: 131 | dependency: transitive 132 | description: 133 | name: term_glyph 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "1.1.0" 137 | typed_data: 138 | dependency: transitive 139 | description: 140 | name: typed_data 141 | url: "https://pub.dartlang.org" 142 | source: hosted 143 | version: "1.2.0" 144 | universal_io: 145 | dependency: transitive 146 | description: 147 | name: universal_io 148 | url: "https://pub.dartlang.org" 149 | source: hosted 150 | version: "0.8.6" 151 | zone_local: 152 | dependency: transitive 153 | description: 154 | name: zone_local 155 | url: "https://pub.dartlang.org" 156 | source: hosted 157 | version: "0.1.2" 158 | sdks: 159 | dart: ">=2.8.1 <3.0.0" 160 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: multi_doh_server 2 | description: A DoH-server which handles multiple queries at once. 3 | # version: 1.0.0 4 | # homepage: https://www.example.com 5 | 6 | environment: 7 | sdk: ">=2.8.1 <3.0.0" 8 | 9 | dependencies: 10 | http: ^0.12.2 11 | crypto: ^2.1.5 12 | dns: ^0.1.3 13 | 14 | dev_dependencies: 15 | pedantic: ^1.9.0 16 | --------------------------------------------------------------------------------