JJ_MKVMaker/test/support/fake_dns.dart

91 lines
3.0 KiB
Dart

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<String, String> records;
final bool truncateUdp;
final int ttl;
late RawDatagramSocket _udp;
ServerSocket? _tcp;
final queries = <String>[];
int tcpQueries = 0;
FakeDns(this.records, {this.truncateUdp = false, this.ttl = 300});
int get port => _udp.port;
Future<void> 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;
break;
} on SocketException {
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 {
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<void> close() async {
_udp.close();
await _tcp?.close();
}
Uint8List _answer(Uint8List q, {required bool tc}) {
// 질문 이름 · 형식
var i = 12;
final labels = <String>[];
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();
}
}