Reputation: 841
I've been trying to get the DNS resolution time using the next code:
val url = URL(dataExperienceTestResult.pingUrl)
val host: String = url.host
val currentTime: Long = SystemClock.elapsedRealtime()
val address: InetAddress = InetAddress.getByName(host)
val dnsTime: Long = SystemClock.elapsedRealtime() - currentTime
Which works as expected providing me with a reasonable resolution time (100ms using Data), however, this is just the case of the first try because the next resolution times are too low (0-2ms using Data). After reading the documentation, I could find the reason for this is because it is cached for 10 mins if successful.
I tried to call the hidden method clearDnsCache()
of the class InerAddress
using reflection having slightly higher results (2-4ms using Data) so the cache doesn't seem to be cleared completely:
//The position 0 method of the list is clearDnsCache()
val method = Class.forName(InetAddress::class.java.name).methods[0]
method.invoke(Class.forName(InetAddress::class.java.name))
I also tried a solution that I read in other StackOverflow questions which consist of eating a security property of the JVM machine. It didn't work, I guess this is because it would require root.
Security.setProperty("networkaddress.cache.ttl", "0")
The last option that I'm currently working on consist of sending a query using the DnsResolver class but I'm getting to high results (300ms - first true, 200ms next tries both using Data).
private static final char[] HEX_CHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
long currentTime;
@RequiresApi(api = Build.VERSION_CODES.Q)
public void method(Context context){
URL url;
Executor executor = new Handler(Looper.getMainLooper())::post;
try {
url = new URL("https://ee-uk.metricelltestcloud.com/SpeedTest/latency.txt");
//
String host = url.getHost();
final String msg = "RawQuery " + host;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
Network[] networks = connectivityManager.getAllNetworks();
currentTime = SystemClock.elapsedRealtime();
for (Network network : networks){
final VerifyCancelCallback callback = new VerifyCancelCallback(msg);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
DnsResolver resolver = DnsResolver.getInstance();
resolver.rawQuery(network, host, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP, executor, null, callback);
}
}
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
private static String byteArrayToHexString(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; ++i) {
int b = bytes[i] & 0xFF;
hexChars[i * 2] = HEX_CHARS[b >>> 4];
hexChars[i * 2 + 1] = HEX_CHARS[b & 0x0F];
}
return new String(hexChars);
}
@RequiresApi(api = Build.VERSION_CODES.Q)
class VerifyCancelCallback implements DnsResolver.Callback<byte[]> {
private String mMsg;
VerifyCancelCallback(@NonNull String msg) {
this.mMsg = msg;
// this(msg, null);
}
@Override
public void onAnswer(@NonNull byte[] answer, int rcode) {
long dnsTime = SystemClock.elapsedRealtime() - currentTime;
Log.v("Kanto_Resolver", "Answer " + dnsTime + " ms");
Log.v("Kanto_resolver", answer.toString());
Log.d("Kanto_resolver", "Reported rcode: " + rcode);
Log.d("Kanto_resolver", "Reported blob: " + byteArrayToHexString(answer));
}
@Override
public void onError(@NonNull DnsResolver.DnsException error) {
Log.v("Kanto_Resolver", "Error");
}
}
Question: Do you know a way to resolve the DNS without using "InetAddress.getByName()" or a way to clear completely the DNS cache?
I need: Get the real (not cached) DNS resolution time every time I check it without considering when I did the last check.
I'm aware that there are already some questions about same topic in StackOverflow but most of them are too old and none of them could solve my question at all.
Upvotes: 4
Views: 2192
Reputation: 841
I could find another way to get the DNS resolution time and the resolved IP address avoiding caching thanks to the VisualBasic code from this post
The solution consists of sending a DatagramPacket with a specific query to the DNS IP resolver through the socket, then we wait for the answer, which is the resolution time and we analyze the answer to find the resolved IP.
See the code:
In a new thread we create the packet, we send it, we receive it and we decode it:
public void getDnsStuff() {
backgroundHandler.post(new Runnable() {
@Override
public void run() {
byte [] lololo;
try {
DatagramPacket sendPacket;
String string = "linkedin.com";
lololo = giveMeX3(urlToUse);
sendPacket = new DatagramPacket(lololo, lololo.length, InetAddress.getByName("8.8.8.8"), 53);
Log.e("kanto_extra", "host: " + string + ", DNS: GoogleDNS");
socket = new DatagramSocket();
socket.send(sendPacket);
byte[] buf = new byte[512];
DatagramPacket receivePacket = new DatagramPacket(buf, buf.length);
Long currentTime = SystemClock.elapsedRealtime();
socket.setSoTimeout(1000);
socket.receive(receivePacket);
Long now = SystemClock.elapsedRealtime() - currentTime;
Log.v("Kanto_time", now.toString());
int[] bufUnsigned = new int[receivePacket.getLength()];
for (int x = 0; x < receivePacket.getLength(); x++){
bufUnsigned[x] = (int) receivePacket.getData()[x] & 0xFF;
}
Log.v("Kanto_unsigned", bufUnsigned.toString());
letsDoSomethingWIthThoseBytes(bufUnsigned, receivePacket.getData(), lololo, now);
} catch (IOException e) {
e.printStackTrace();
}
socket.close();
socket.disconnect();
}
});
}
The method that encodes the query to be sent (giveMeX3):
private byte[] giveMeX3(String host){
String TransactionID1="Q1";
String TypeString="\u0001"+"\u0000"+"\u0000"+"\u0001"+"\u0000"+"\u0000"+"\u0000"+"\u0000"+"\u0000"+"\u0000";
String TrailerString="\u0000"+"\u0000"+"\u0001"+"\u0000"+"\u0001";
String URLNameStart = host.substring(0, host.indexOf("."));
String DomainName = host.substring(host.indexOf(".") + 1);
String QueryString = TransactionID1 + TypeString + (char)URLNameStart.length() + URLNameStart + (char)DomainName.length() + DomainName + TrailerString;
byte[] buffer = new byte[0];
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
buffer = QueryString.getBytes(StandardCharsets.US_ASCII);
}
return buffer;
}
The method that decodes the byte array of the answer (letsDoSomethingWIthThoseBytes):
public void letsDoSomethingWIthThoseBytes(int[] bytesList, byte[] bytesListTrue, byte[] sentBytes, Long time){
int index = 0;
if (bytesList[0] == sentBytes[0] && (bytesList[1] == 0x31) || (bytesList[1] == 0x32)) {
if (bytesList[2] == 0x81 && bytesList[3] == 0x80) {
// Decode the answers
// Find the URL that was returned
int TransactionDNS = bytesList[1];
String ReceiveString = "";// = Encoding.ASCII.GetString(Receivebytes);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
ReceiveString = new String(bytesListTrue, StandardCharsets.US_ASCII);
}
index=12;
int URLNameStartLength = bytesListTrue[index];
index++;
String URLNameStart = ReceiveString.substring(index,URLNameStartLength + index);
index=index+URLNameStartLength;
int DomainNameLength = bytesListTrue[index];
index++;
String DomainName = ReceiveString.substring(index,DomainNameLength + index);
index=index+DomainNameLength;
index=index+8;
// Get the record type
int ResponseType = bytesListTrue[index];
index=index+9;
int listLenght = bytesList.length;
String IPResponse = String.valueOf(bytesList[index])+"."
+ String.valueOf(bytesList[index + 1])+"."
+ String.valueOf(bytesList[index + 2])+"."
+ String.valueOf(bytesList[index + 3]);
this.resultString = URLNameStart + "." + DomainName + " - " + IPResponse + " - " + time.toString();
Log.v("Kanto DNS answer", URLNameStart + "." + DomainName + " - " + IPResponse + " - " + time.toString());
}
}
}
Additional information:
As far as I know, the query to be sent to the server can be modified depending on what you need to get from the DNS server. You can learn more about the DNS protocol here
In this example, I'm communicating with Google DNS (8.8.8.8 / 8.8.4.4) but I tested quite a few more and all of them with port 53 so they should work. Check some DNS server:
("Google", "8.8.8.8", "8.8.4.4", "https://developers.google.com/speed/public-dns");
("Quad9", "9.9.9.9", "149.112.112.112", "https://www.quad9.net/");
("Level 3", "209.244.0.3", "209.244.0.4", "https://www.centurylink.com/business.html?rid=lvltmigration");
("Yandex", "77.88.8.8", "77.88.8.1", "https://dns.yandex.com/");
("DNSWatch", "84.200.69.80", "84.200.70.40", "https://dns.watch/index");
("Verisign", "64.6.64.6", "64.6.65.6", "https://www.verisign.com/en_GB/security-services/public-dns/index.xhtml");
("OpenDNS", "208.67.222.222", "208.67.220.220", "https://www.opendns.com/");
("FreeDNS", "37.235.1.174", "37.235.1.177", "https://freedns.zone");
("Cloudflare", "1.1.1.1", "1.0.0.1", "https://1.1.1.1");
("AdGuard", "176.103.130.130", "176.103.130.131", "https://adguard.com/en/adguard-dns/overview.html#instruction");
("French Data Network", "80.67.169.12", "80.67.169.40", "https://www.fdn.fr/actions/dns/");
("Comodo", "8.26.56.26", "8.20.247.20", "https://www.comodo.com/secure-dns/");
("Alternate DNS", "23.253.163.53", "198.101.242.72", "https://alternate-dns.com/");
("Freenom World", "80.80.80.80", "80.80.81.81", "https://www.freenom.com");
("Keweon", "176.9.62.58", "176.9.62.62", "https://forum.xda-developers.com/android/software-hacking/keweon-privacy-online-security-t3681139");
("Quad101", "101.101.101.101", "101.102.103.104", "https://101.101.101.101/index_en.html");
("SafeDNS", "195.46.39.39", "195.46.39.40", "https://www.safedns.com/en/");
("UncensoredDNS", "91.239.100.100", "89.233.43.71", "https://blog.uncensoreddns.org/");
Hopefully, this solution will be helpful for some.
Upvotes: 3