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; /// 그 번호로 UDP 를 연다 (못 열면 null). 전체 시험 (10/20) 에서 막힌 포트 (10013) 의 오류가 try 밖으로 (잡히지 않은 비동기 오류로) /// 나와 시험이 실패한 적이 있어, 열기 하나를 따로 영역 (zone) 에서 돌려 어느 쪽으로 오든 "못 열었음" 으로 받는다 Future _tryUdp(int port) { final done = Completer(); runZonedGuarded(() async { final s = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, port); if (done.isCompleted) { s.close(); } else { done.complete(s); } }, (e, _) { events.add('udp bind $port failed: ${e is SocketException ? '${e.osError?.errorCode} ${e.message}' : e}'); if (!done.isCompleted) done.complete(null); }); return done.future; } 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++) { // 10/21 전체 실행: OS 가 TCP 번호를 차례로 주는데 (62345, 62346 …) 그 둘레가 통째로 UDP 막힌 범위라 50번 모두 실패했다 - // 홀수 번째는 거꾸로 UDP 를 OS 가 고른 (열 수 있는) 번호로 먼저 열고 그 번호로 TCP 를 연다 if (i.isOdd) { final udp = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0); try { _tcp = await ServerSocket.bind(InternetAddress.loopbackIPv4, udp.port); _udp = udp; events.add('bound udp+tcp ${udp.port} (try $i)'); break; } on SocketException catch (e) { events.add('tcp bind ${udp.port} failed: ${e.osError?.errorCode} ${e.message}'); udp.close(); } } else { final tcp = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); final udp = await _tryUdp(tcp.port); if (udp != null) { _udp = udp; _tcp = tcp; events.add('bound tcp+udp ${tcp.port} (try $i)'); break; } await tcp.close(); } if (i > 50) throw StateError('UDP · TCP 같은 포트를 열지 못함: $events'); } } _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(); } }