refactor: update DoH resolution method to use hostname first; improve circuit breaker logic

This commit is contained in:
Moustapha Kodjo Amadou 2026-02-25 11:52:18 +01:00
parent cc69c9f275
commit 9d829a16e7
2 changed files with 31 additions and 66 deletions

View file

@ -105,8 +105,7 @@ class DoHResolver {
} }
} }
/// Resolve using specific provider via bootstrap IP /// Resolve using specific provider via hostname
/// This avoids circular DNS dependency by connecting to bootstrap IP directly
static Future<List<String>> _resolveFromProvider( static Future<List<String>> _resolveFromProvider(
String host, String host,
DoHProvider provider, { DoHProvider provider, {
@ -119,58 +118,34 @@ class DoHResolver {
return []; return [];
} }
final uri = Uri.parse(provider.url); try {
final providerHost = uri.host; final directResult = await _queryDoHViaHostname(
host,
// Validate bootstrap IPs format provider,
final validBootstrapIps = provider.bootstrapIPs recordType,
.where((ip) => _isValidIp(ip)) );
.toList(); if (directResult.isNotEmpty) {
if (validBootstrapIps.isEmpty) { _log('Resolved $host via ${provider.name} (hostname mode)');
_log('No valid bootstrap IPs for ${provider.name}', isError: true); stats.successCount++;
return []; return directResult;
} }
} catch (e) {
// Try each bootstrap IP until one succeeds _log('Hostname mode failed for ${provider.name}: $e');
for (int i = 0; i < validBootstrapIps.length; i++) { stats.failureCount++;
final bootstrapIp = validBootstrapIps[i]; stats.lastFailure = DateTime.now();
try { if (stats.failureCount >= 3) {
final result = await _queryDoHViaBootstrapIp( stats.isCircuitOpen = true;
host, _log('Circuit breaker opened for ${provider.name}', isError: true);
provider,
bootstrapIp,
providerHost,
recordType,
);
if (result.isNotEmpty) {
_log(
'Resolved $host via ${provider.name} (bootstrap IP $i+1/${validBootstrapIps.length})',
);
return result;
}
} catch (e) {
_log('Bootstrap IP $i failed for ${provider.name}: $e');
continue;
} }
}
stats.failureCount++;
stats.lastFailure = DateTime.now();
// Open circuit after 3 consecutive failures
if (stats.failureCount >= 3) {
stats.isCircuitOpen = true;
_log('Circuit breaker opened for ${provider.name}', isError: true);
} }
return []; return [];
} }
/// Query DoH via specific bootstrap IP (direct IP connection, no DNS lookup) /// Query DoH by provider hostname (relies on platform DNS for provider host)
static Future<List<String>> _queryDoHViaBootstrapIp( static Future<List<String>> _queryDoHViaHostname(
String targetHost, String targetHost,
DoHProvider provider, DoHProvider provider,
String bootstrapIp,
String providerHost,
String recordType, String recordType,
) async { ) async {
final uri = Uri.parse(provider.url); final uri = Uri.parse(provider.url);
@ -178,19 +153,16 @@ class DoHResolver {
client.connectionTimeout = _requestTimeout; client.connectionTimeout = _requestTimeout;
try { try {
// Create request with bootstrap IP instead of hostname
final request = await client.getUrl( final request = await client.getUrl(
Uri( Uri(
scheme: uri.scheme, scheme: uri.scheme,
host: bootstrapIp, // Use bootstrap IP directly host: uri.host,
port: uri.port, port: uri.port,
path: uri.path, path: uri.path,
query: 'name=$targetHost&type=$recordType', query: 'name=$targetHost&type=$recordType',
), ),
); );
// Set Host header to the actual provider hostname (required for HTTPS SNI)
request.headers.set('Host', providerHost);
request.headers.set('Accept', 'application/dns-json'); request.headers.set('Accept', 'application/dns-json');
request.headers.set('User-Agent', 'Mangayomi/1.0'); request.headers.set('User-Agent', 'Mangayomi/1.0');
@ -201,18 +173,10 @@ class DoHResolver {
return _parseDoHResponse(body); return _parseDoHResponse(body);
} }
_log('HTTP ${response.statusCode} from $bootstrapIp (${provider.name})'); _log('HTTP ${response.statusCode} from hostname (${provider.name})');
return []; return [];
} on SocketException {
// Connection failed with this bootstrap IP
_log('Socket error on $bootstrapIp');
rethrow;
} on TimeoutException {
// Timeout with this bootstrap IP
_log('Timeout on $bootstrapIp');
rethrow;
} finally { } finally {
client.close(); client.close(force: true);
} }
} }
@ -272,6 +236,7 @@ class DoHResolver {
return null; return null;
}) })
.whereType<String>() .whereType<String>()
.where(_isValidIp)
.toList(); .toList();
if (ips.isNotEmpty) { if (ips.isNotEmpty) {

View file

@ -1699,26 +1699,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test name: test
sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae" sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.28.0" version: "1.29.0"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.8" version: "0.7.9"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4 sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.14" version: "0.6.15"
time: time:
dependency: transitive dependency: transitive
description: description: