import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; /// 시험용 DNS 서버 (127.0.0.1, UDP · TCP 같은 포트). [records] 의 이름이면 A 로 답하고, 없으면 NXDOMAIN. /// [truncateUdp] 면 UDP 는 TC 표시만 하고 TCP 로 답한다. class FakeDns { final Map records; final bool truncateUdp; final int ttl; late RawDatagramSocket _udp; ServerSocket? _tcp; final queries = []; int tcpQueries = 0; /// 시작 · 응답 중에 생긴 일 (시험이 실패하면 남겨 원인을 본다) final events = []; FakeDns(this.records, {this.truncateUdp = false, this.ttl = 300}); int get port => _udp.port; Future start() async { if (!truncateUdp) { _udp = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0); } else { // TCP 로 다시 묻는 시험: UDP 와 TCP 가 같은 포트. Windows 는 TCP · UDP 마다 막아 둔 포트 범위 (Hyper-V · WSL, // netsh int ipv4 show excludedportrange) 가 달라, TCP 를 먼저 OS 가 고른 번호로 열고 그 번호로 UDP 를 연다. // 그 번호의 UDP 가 막혀 있거나 (10013) 쓰는 중이면 다른 번호로 다시 for (var i = 0;; i++) { final tcp = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); try { _udp = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, tcp.port); _tcp = tcp; events.add('bound tcp+udp ${tcp.port} (try $i)'); break; } on SocketException catch (e) { events.add('udp bind ${tcp.port} failed: ${e.osError?.errorCode} ${e.message}'); await tcp.close(); if (i > 50) rethrow; } } } _udp.listen((e) { if (e != RawSocketEvent.read) return; final d = _udp.receive(); if (d == null) return; _udp.send(_answer(d.data, tc: truncateUdp), d.address, d.port); }); _tcp?.listen((s) async { events.add('tcp accepted ${s.remotePort}'); final buf = BytesBuilder(); await for (final c in s) { buf.add(c); final b = buf.toBytes(); if (b.length >= 2 && b.length >= 2 + ((b[0] << 8) | b[1])) { tcpQueries++; final a = _answer(Uint8List.sublistView(b, 2), tc: false); s.add([a.length >> 8, a.length & 0xff, ...a]); await s.flush(); await s.close(); break; } } }); } Future close() async { _udp.close(); await _tcp?.close(); } Uint8List _answer(Uint8List q, {required bool tc}) { // 질문 이름 · 형식 var i = 12; final labels = []; while (q[i] != 0) { labels.add(String.fromCharCodes(q.sublist(i + 1, i + 1 + q[i]))); i += q[i] + 1; } final type = (q[i + 1] << 8) | q[i + 2]; final qEnd = i + 5; final name = labels.join('.'); queries.add('$name/$type'); final ip = records[name]; final b = BytesBuilder() ..add([q[0], q[1], 0x81 | (tc ? 0x02 : 0), ip == null ? 0x83 : 0x80, 0, 1, 0, (!tc && ip != null && type == 1) ? 1 : 0, 0, 0, 0, 0]) ..add(q.sublist(12, qEnd)); if (!tc && ip != null && type == 1) { b ..add([0xc0, 12, 0, 1, 0, 1, (ttl >> 24) & 0xff, (ttl >> 16) & 0xff, (ttl >> 8) & 0xff, ttl & 0xff, 0, 4]) ..add(InternetAddress(ip).rawAddress); } return b.toBytes(); } }