Refactored DnsHelper and added DNS testing for the public config hostname instead of a hardcoded URL

Refactored DnsHelper for maintainability and added automatic DNS testing for the public config hostname after creation/update of vrp-public.json to automatically enable fallback DNS and proxy if system DNS fails for that
This commit is contained in:
jp64k
2025-12-30 16:54:00 +01:00
parent 4383b9d398
commit 5819bc8083
2 changed files with 277 additions and 213 deletions

View File

@@ -348,6 +348,9 @@ namespace AndroidSideloader
{
PublicConfigFile = config;
hasPublicConfig = true;
// Test DNS for the public config hostname after it's been created/updated
DnsHelper.TestPublicConfigDns();
}
}
}

View File

@@ -2,11 +2,13 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace AndroidSideloader.Utilities
{
@@ -20,49 +22,161 @@ namespace AndroidSideloader.Utilities
"raw.githubusercontent.com",
"downloads.rclone.org",
"vrpirates.wiki",
"go.vrpyourself.online",
"github.com"
};
private static readonly ConcurrentDictionary<string, IPAddress> _dnsCache =
new ConcurrentDictionary<string, IPAddress>(StringComparer.OrdinalIgnoreCase);
private static bool _initialized;
private static bool _useFallbackDns;
private static readonly object _lock = new object();
// Local proxy for rclone
private static TcpListener _proxyListener;
private static CancellationTokenSource _proxyCts;
private static int _proxyPort;
private static bool _initialized;
private static bool _proxyRunning;
public static bool UseFallbackDns
{
get { if (!_initialized) Initialize(); return _useFallbackDns; }
}
public static bool UseFallbackDns { get; private set; }
// Gets the proxy URL for rclone to use, or empty string if not needed
public static string ProxyUrl => _proxyRunning ? $"http://127.0.0.1:{_proxyPort}" : string.Empty;
// Called after vrp-public.json is created/updated to test the hostname
// Enable fallback DNS if the hostname fails on system DNS but works with fallback DNS
public static void TestPublicConfigDns()
{
string hostname = GetPublicConfigHostname();
if (string.IsNullOrEmpty(hostname))
return;
lock (_lock)
{
// If already using fallback, just pre-resolve the new hostname
if (UseFallbackDns)
{
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
_dnsCache[hostname] = ip;
Logger.Log($"Pre-resolved public config hostname {hostname} -> {ip}");
}
return;
}
// Test if system DNS can resolve the public config hostname
bool systemDnsWorks = TestHostnameWithSystemDns(hostname);
if (!systemDnsWorks)
{
Logger.Log($"System DNS failed for {hostname}. Testing fallback...", LogLevel.WARNING);
// Test if fallback DNS works for this hostname
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
UseFallbackDns = true;
_dnsCache[hostname] = ip;
Logger.Log($"Enabled fallback DNS for {hostname} -> {ip}", LogLevel.INFO);
ServicePointManager.DnsRefreshTimeout = 0;
// Pre-resolve base hostnames too
PreResolveHostnames(CriticalHostnames);
// Start proxy if not already running
if (!_proxyRunning)
{
StartProxy();
}
}
else
{
Logger.Log($"Both system and fallback DNS failed for {hostname}", LogLevel.ERROR);
}
}
else
{
Logger.Log($"System DNS works for public config hostname: {hostname}");
}
}
}
private static string GetPublicConfigHostname()
{
try
{
string configPath = Path.Combine(Environment.CurrentDirectory, "vrp-public.json");
if (!File.Exists(configPath))
return null;
var config = JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(configPath));
if (config != null && config.TryGetValue("baseUri", out string baseUri))
{
return ExtractHostname(baseUri);
}
}
catch (Exception ex)
{
Logger.Log($"Failed to get hostname from vrp-public.json: {ex.Message}", LogLevel.WARNING);
}
return null;
}
private static string[] GetCriticalHostnames()
{
var hostnames = new List<string>(CriticalHostnames);
string host = GetPublicConfigHostname();
if (!string.IsNullOrWhiteSpace(host) && !hostnames.Contains(host))
{
hostnames.Add(host);
Logger.Log($"Added {host} from vrp-public.json to critical hostnames");
}
return hostnames.ToArray();
}
private static string ExtractHostname(string uriString)
{
if (string.IsNullOrWhiteSpace(uriString)) return null;
if (!uriString.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!uriString.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
uriString = "https://" + uriString;
}
if (Uri.TryCreate(uriString, UriKind.Absolute, out Uri uri))
return uri.Host;
// Fallback: manual extraction
string hostname = uriString.Replace("https://", "").Replace("http://", "");
int idx = hostname.IndexOfAny(new[] { '/', ':' });
return idx > 0 ? hostname.Substring(0, idx) : hostname;
}
public static void Initialize()
{
lock (_lock)
{
if (_initialized) return;
Logger.Log("Testing DNS resolution for critical hostnames...");
if (!TestSystemDns())
Logger.Log("Testing DNS resolution for critical hostnames...");
var hostnames = GetCriticalHostnames();
if (TestDns(hostnames, useSystem: true))
{
Logger.Log("System DNS is working correctly.");
}
else
{
Logger.Log("System DNS failed. Testing Cloudflare DNS fallback...", LogLevel.WARNING);
if (TestFallbackDns())
if (TestDns(hostnames, useSystem: false))
{
_useFallbackDns = true;
UseFallbackDns = true;
Logger.Log("Using Cloudflare DNS fallback.", LogLevel.INFO);
PreResolveHostnames();
PreResolveHostnames(hostnames);
ServicePointManager.DnsRefreshTimeout = 0;
// Start local proxy for rclone
StartProxy();
}
else
@@ -70,77 +184,65 @@ namespace AndroidSideloader.Utilities
Logger.Log("Both system and fallback DNS failed.", LogLevel.ERROR);
}
}
else
{
Logger.Log("System DNS is working correctly.");
}
_initialized = true;
}
}
// Cleans up resources. Called on application exit
public static void Cleanup()
{
StopProxy();
}
public static void Cleanup() => StopProxy();
private static void PreResolveHostnames()
private static bool TestHostnameWithSystemDns(string hostname)
{
foreach (string hostname in CriticalHostnames)
try
{
try
{
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
_dnsCache[hostname] = ip;
Logger.Log($"Pre-resolved {hostname} -> {ip}");
}
}
catch (Exception ex)
{
Logger.Log($"Failed to pre-resolve {hostname}: {ex.Message}", LogLevel.WARNING);
}
var addresses = Dns.GetHostAddresses(hostname);
return addresses?.Length > 0;
}
catch
{
return false;
}
}
private static bool TestSystemDns()
private static bool TestDns(string[] hostnames, bool useSystem)
{
foreach (string hostname in CriticalHostnames)
if (useSystem)
{
try
return hostnames.All(h =>
{
var addresses = Dns.GetHostAddresses(hostname);
if (addresses == null || addresses.Length == 0) return false;
}
try { return Dns.GetHostAddresses(h)?.Length > 0; }
catch { return false; }
});
}
return FallbackDnsServers.Any(server =>
{
try { return ResolveWithDns(hostnames[0], server)?.Count > 0; }
catch { return false; }
}
return true;
});
}
private static bool TestFallbackDns()
private static void PreResolveHostnames(string[] hostnames)
{
foreach (string dnsServer in FallbackDnsServers)
foreach (string hostname in hostnames)
{
try
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
var addresses = ResolveWithDns(CriticalHostnames[0], dnsServer);
if (addresses != null && addresses.Count > 0) return true;
_dnsCache[hostname] = ip;
Logger.Log($"Pre-resolved {hostname} -> {ip}");
}
catch { }
}
return false;
}
private static IPAddress ResolveWithFallbackDns(string hostname)
{
foreach (string dnsServer in FallbackDnsServers)
foreach (string server in FallbackDnsServers)
{
try
{
var addresses = ResolveWithDns(hostname, dnsServer);
if (addresses != null && addresses.Count > 0)
return addresses[0];
var addresses = ResolveWithDns(hostname, server);
if (addresses?.Count > 0) return addresses[0];
}
catch { }
}
@@ -149,56 +251,57 @@ namespace AndroidSideloader.Utilities
private static List<IPAddress> ResolveWithDns(string hostname, string dnsServer, int timeoutMs = 5000)
{
byte[] query = BuildDnsQuery(hostname);
using (var udp = new UdpClient())
using (var udp = new UdpClient { Client = { ReceiveTimeout = timeoutMs, SendTimeout = timeoutMs } })
{
udp.Client.ReceiveTimeout = timeoutMs;
udp.Client.SendTimeout = timeoutMs;
byte[] query = BuildDnsQuery(hostname);
udp.Send(query, query.Length, new IPEndPoint(IPAddress.Parse(dnsServer), 53));
IPEndPoint remoteEp = null;
byte[] response = udp.Receive(ref remoteEp);
return ParseDnsResponse(response);
return ParseDnsResponse(udp.Receive(ref remoteEp));
}
}
private static byte[] BuildDnsQuery(string hostname)
{
var ms = new MemoryStream();
var writer = new BinaryWriter(ms);
writer.Write(IPAddress.HostToNetworkOrder((short)new Random().Next(0, ushort.MaxValue)));
writer.Write(IPAddress.HostToNetworkOrder((short)0x0100));
writer.Write(IPAddress.HostToNetworkOrder((short)1));
writer.Write(IPAddress.HostToNetworkOrder((short)0));
writer.Write(IPAddress.HostToNetworkOrder((short)0));
writer.Write(IPAddress.HostToNetworkOrder((short)0));
foreach (string label in hostname.Split('.'))
using (var ms = new MemoryStream())
using (var writer = new BinaryWriter(ms))
{
writer.Write((byte)label.Length);
writer.Write(Encoding.ASCII.GetBytes(label));
writer.Write(IPAddress.HostToNetworkOrder((short)new Random().Next(0, ushort.MaxValue)));
writer.Write(IPAddress.HostToNetworkOrder((short)0x0100)); // Flags
writer.Write(IPAddress.HostToNetworkOrder((short)1)); // Questions
writer.Write(new byte[6]); // Answer/Authority/Additional counts
foreach (string label in hostname.Split('.'))
{
writer.Write((byte)label.Length);
writer.Write(Encoding.ASCII.GetBytes(label));
}
writer.Write((byte)0);
writer.Write(IPAddress.HostToNetworkOrder((short)1)); // Type A
writer.Write(IPAddress.HostToNetworkOrder((short)1)); // Class IN
return ms.ToArray();
}
writer.Write((byte)0);
writer.Write(IPAddress.HostToNetworkOrder((short)1));
writer.Write(IPAddress.HostToNetworkOrder((short)1));
return ms.ToArray();
}
private static List<IPAddress> ParseDnsResponse(byte[] response)
{
var addresses = new List<IPAddress>();
if (response.Length < 12) return addresses;
int pos = 12;
while (pos < response.Length && response[pos] != 0) pos += response[pos] + 1;
pos += 5;
int answerCount = (response[6] << 8) | response[7];
for (int i = 0; i < answerCount && pos + 12 <= response.Length; i++)
{
if ((response[pos] & 0xC0) == 0xC0) pos += 2;
else { while (pos < response.Length && response[pos] != 0) pos += response[pos] + 1; pos++; }
pos += (response[pos] & 0xC0) == 0xC0 ? 2 : SkipName(response, pos);
if (pos + 10 > response.Length) break;
ushort type = (ushort)((response[pos] << 8) | response[pos + 1]);
pos += 8;
ushort rdLength = (ushort)((response[pos] << 8) | response[pos + 1]);
pos += 2;
ushort rdLength = (ushort)((response[pos + 8] << 8) | response[pos + 9]);
pos += 10;
if (pos + rdLength > response.Length) break;
if (type == 1 && rdLength == 4)
addresses.Add(new IPAddress(new[] { response[pos], response[pos + 1], response[pos + 2], response[pos + 3] }));
@@ -207,7 +310,73 @@ namespace AndroidSideloader.Utilities
return addresses;
}
#region Local HTTP CONNECT Proxy for rclone
private static int SkipName(byte[] data, int pos)
{
int start = pos;
while (pos < data.Length && data[pos] != 0) pos += data[pos] + 1;
return pos - start + 1;
}
public static IPAddress ResolveHostname(string hostname, bool alwaysTryFallback = false)
{
if (_dnsCache.TryGetValue(hostname, out IPAddress cached))
return cached;
try
{
var addresses = Dns.GetHostAddresses(hostname);
if (addresses?.Length > 0)
{
_dnsCache[hostname] = addresses[0];
return addresses[0];
}
}
catch { }
if (alwaysTryFallback || UseFallbackDns || !_initialized)
{
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
_dnsCache[hostname] = ip;
return ip;
}
}
return null;
}
public static HttpWebRequest CreateWebRequest(string url)
{
var uri = new Uri(url);
if (!UseFallbackDns)
{
try
{
Dns.GetHostAddresses(uri.Host);
return (HttpWebRequest)WebRequest.Create(url);
}
catch
{
if (!_initialized) Initialize();
}
}
if (UseFallbackDns)
{
var ip = ResolveHostname(uri.Host, alwaysTryFallback: true);
if (ip != null)
{
var builder = new UriBuilder(uri) { Host = ip.ToString() };
var request = (HttpWebRequest)WebRequest.Create(builder.Uri);
request.Host = uri.Host;
return request;
}
}
return (HttpWebRequest)WebRequest.Create(url);
}
private static void StartProxy()
{
@@ -246,7 +415,7 @@ namespace AndroidSideloader.Utilities
try
{
var client = await _proxyListener.AcceptTcpClientAsync();
_ = Task.Run(() => HandleProxyClient(client, ct));
_ = HandleProxyClient(client, ct);
}
catch (ObjectDisposedException) { break; }
catch (Exception ex)
@@ -264,10 +433,8 @@ namespace AndroidSideloader.Utilities
using (client)
using (var stream = client.GetStream())
{
client.ReceiveTimeout = 30000;
client.SendTimeout = 30000;
client.ReceiveTimeout = client.SendTimeout = 30000;
// Read the HTTP request
var buffer = new byte[8192];
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, ct);
if (bytesRead == 0) return;
@@ -279,19 +446,12 @@ namespace AndroidSideloader.Utilities
string[] requestLine = lines[0].Split(' ');
if (requestLine.Length < 2) return;
string method = requestLine[0];
string target = requestLine[1];
if (method == "CONNECT")
{
if (requestLine[0] == "CONNECT")
// HTTPS proxy - tunnel mode
await HandleConnectRequest(stream, target, ct);
}
await HandleConnectRequest(stream, requestLine[1], ct);
else
{
// HTTP proxy - forward mode
await HandleHttpRequest(stream, request, target, ct);
}
await HandleHttpRequest(stream, request, requestLine[1], ct);
}
}
catch (Exception ex)
@@ -306,14 +466,13 @@ namespace AndroidSideloader.Utilities
// Parse host:port
string[] parts = target.Split(':');
string host = parts[0];
int port = parts.Length > 1 ? int.Parse(parts[1]) : 443;
int port = parts.Length > 1 && int.TryParse(parts[1], out int p) ? p : 443;
// Resolve hostname using our DNS
IPAddress ip = ResolveAnyHostname(host);
// Resolve hostname
IPAddress ip = ResolveHostname(host, alwaysTryFallback: true);
if (ip == null)
{
byte[] errorResponse = Encoding.ASCII.GetBytes("HTTP/1.1 502 Bad Gateway\r\n\r\n");
await clientStream.WriteAsync(errorResponse, 0, errorResponse.Length, ct);
await SendResponse(clientStream, "HTTP/1.1 502 Bad Gateway\r\n\r\n", ct);
return;
}
@@ -326,21 +485,16 @@ namespace AndroidSideloader.Utilities
using (var targetStream = targetClient.GetStream())
{
// Send 200 OK to client
byte[] okResponse = Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n");
await clientStream.WriteAsync(okResponse, 0, okResponse.Length, ct);
await SendResponse(clientStream, "HTTP/1.1 200 Connection Established\r\n\r\n", ct);
// Tunnel data bidirectionally
var clientToTarget = RelayData(clientStream, targetStream, ct);
var targetToClient = RelayData(targetStream, clientStream, ct);
await Task.WhenAny(clientToTarget, targetToClient);
await Task.WhenAny(RelayData(clientStream, targetStream, ct), RelayData(targetStream, clientStream, ct));
}
}
}
catch (Exception ex)
{
Logger.Log($"CONNECT tunnel error to {host}: {ex.Message}", LogLevel.WARNING);
byte[] errorResponse = Encoding.ASCII.GetBytes("HTTP/1.1 502 Bad Gateway\r\n\r\n");
try { await clientStream.WriteAsync(errorResponse, 0, errorResponse.Length, ct); } catch { }
await SendResponse(clientStream, "HTTP/1.1 502 Bad Gateway\r\n\r\n", ct);
}
}
@@ -349,19 +503,16 @@ namespace AndroidSideloader.Utilities
try
{
var uri = new Uri(url);
IPAddress ip = ResolveAnyHostname(uri.Host);
IPAddress ip = ResolveHostname(uri.Host, alwaysTryFallback: true);
if (ip == null)
{
byte[] errorResponse = Encoding.ASCII.GetBytes("HTTP/1.1 502 Bad Gateway\r\n\r\n");
await clientStream.WriteAsync(errorResponse, 0, errorResponse.Length, ct);
await SendResponse(clientStream, "HTTP/1.1 502 Bad Gateway\r\n\r\n", ct);
return;
}
int port = uri.Port > 0 ? uri.Port : 80;
using (var targetClient = new TcpClient())
{
await targetClient.ConnectAsync(ip, port);
await targetClient.ConnectAsync(ip, uri.Port > 0 ? uri.Port : 80);
using (var targetStream = targetClient.GetStream())
{
// Modify request to use relative path
@@ -380,6 +531,12 @@ namespace AndroidSideloader.Utilities
}
}
private static async Task SendResponse(NetworkStream stream, string response, CancellationToken ct)
{
byte[] bytes = Encoding.ASCII.GetBytes(response);
try { await stream.WriteAsync(bytes, 0, bytes.Length, ct); } catch { }
}
private static async Task RelayData(NetworkStream from, NetworkStream to, CancellationToken ct)
{
byte[] buffer = new byte[8192];
@@ -387,105 +544,9 @@ namespace AndroidSideloader.Utilities
{
int bytesRead;
while ((bytesRead = await from.ReadAsync(buffer, 0, buffer.Length, ct)) > 0)
{
await to.WriteAsync(buffer, 0, bytesRead, ct);
}
}
catch { }
}
#endregion
public static IPAddress ResolveHostname(string hostname)
{
if (_dnsCache.TryGetValue(hostname, out IPAddress cached))
return cached;
try
{
var addresses = Dns.GetHostAddresses(hostname);
if (addresses != null && addresses.Length > 0)
{
_dnsCache[hostname] = addresses[0];
return addresses[0];
}
}
catch { }
if (_useFallbackDns || !_initialized)
{
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
_dnsCache[hostname] = ip;
return ip;
}
}
return null;
}
public static IPAddress ResolveAnyHostname(string hostname)
{
if (_dnsCache.TryGetValue(hostname, out IPAddress cached))
return cached;
try
{
var addresses = Dns.GetHostAddresses(hostname);
if (addresses != null && addresses.Length > 0)
{
_dnsCache[hostname] = addresses[0];
return addresses[0];
}
}
catch { }
var ip = ResolveWithFallbackDns(hostname);
if (ip != null)
{
_dnsCache[hostname] = ip;
return ip;
}
return null;
}
public static HttpWebRequest CreateWebRequest(string url)
{
var uri = new Uri(url);
if (!_useFallbackDns)
{
try
{
Dns.GetHostAddresses(uri.Host);
return (HttpWebRequest)WebRequest.Create(url);
}
catch
{
if (!_initialized) Initialize();
}
}
if (_useFallbackDns)
{
var ip = ResolveHostname(uri.Host);
if (ip == null)
{
ip = ResolveAnyHostname(uri.Host);
}
if (ip != null)
{
var builder = new UriBuilder(uri) { Host = ip.ToString() };
var request = (HttpWebRequest)WebRequest.Create(builder.Uri);
request.Host = uri.Host;
return request;
}
}
return (HttpWebRequest)WebRequest.Create(url);
}
}
}