Skip to content

Instantly share code, notes, and snippets.

@stdStudent
Created August 5, 2025 12:05
Show Gist options
  • Save stdStudent/d26909ed8ece7f28ebdef5d3639c455d to your computer and use it in GitHub Desktop.
Save stdStudent/d26909ed8ece7f28ebdef5d3639c455d to your computer and use it in GitHub Desktop.
Get public IP address via DNS (Android, Kotlin)
/**
* License: MPL 2.0
* Text: https://www.mozilla.org/en-US/MPL/2.0/
*/
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
class PublicIp {
companion object {
const val TAG = "PublicIp"
}
private fun isValidIp(ip: String): Boolean {
val parts = ip.split(".")
if (parts.size != 4)
return false
return parts.all { part ->
try {
val num = part.toInt()
num in 0..255
} catch (e: NumberFormatException) {
Log.w(TAG, "Invalid IP part: $part", e)
false
} catch (e: Throwable) {
Log.w(TAG, "Unexpected error validating IP part: $part", e)
false
}
}
}
private fun isValidPublicIp(ip: String): Boolean {
if (isValidIp(ip).not())
return false
val parts = ip.split(".").map { it.toInt() }
val first = parts[0]
val second = parts[1]
// Filter out private/reserved IP ranges and Google server ranges
return when {
first == 10 -> false // 10.0.0.0/8
first == 172 && second in 16..31 -> false // 172.16.0.0/12
first == 192 && second == 168 -> false // 192.168.0.0/16
first == 127 -> false // 127.0.0.0/8 (localhost)
first == 169 && second == 254 -> false // 169.254.0.0/16 (link-local)
first in 224..255 -> false // Multicast and reserved
first == 0 -> false // 0.0.0.0/8
// Filter out common Google server IP ranges
first == 74 && second == 125 -> false
first == 216 && second == 239 -> false
first == 172 && second == 217 -> false
ip.endsWith(".0") -> false // Network addresses
ip.endsWith(".255") -> false // Broadcast addresses
else -> true
}
}
private suspend fun resolveNameServer(hostname: String): String? =
withContext(Dispatchers.IO) {
if (hostname.isEmpty()) {
Log.w(TAG, "Hostname is empty")
return@withContext null
}
try {
// Use system DNS to resolve the name server IP
val address = InetAddress.getByName(hostname)
Log.d(TAG, "Resolved $hostname to ${address.hostAddress}")
return@withContext address.hostAddress
} catch (e: Throwable) {
Log.w(TAG, "Failed to resolve $hostname", e)
// Hardcoded fallback IPs for Google's name servers
val fallbackIps = mapOf(
"ns1.google.com" to "216.239.32.10",
"ns2.google.com" to "216.239.34.10",
"ns3.google.com" to "216.239.36.10",
"ns4.google.com" to "216.239.38.10",
)
Log.d(TAG, "Using fallback IPs for $hostname: ${fallbackIps[hostname]}")
return@withContext fallbackIps[hostname]
}
}
private fun extractClientIpFromRecord(record: String): String? {
// Skip EDNS subnet records
if (record.contains("edns0-client-subnet") || record.contains("/"))
return null
// Skip Google server IPs (they typically start with certain ranges)
if (record.matches(Regex("^74\\.125\\..*")) ||
record.matches(Regex("^216\\.239\\..*")) ||
record.matches(Regex("^172\\.217\\..*"))) {
Log.d(TAG, "Skipping Google server IP: $record")
return null
}
// Extract and validate IP
val ipPattern = Regex("^([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})$")
val match = ipPattern.find(record.trim())
return match?.groupValues?.get(1)
}
private fun createDnsTxtQuery(hostname: String): ByteArray {
val buffer = ByteBuffer.allocate(512)
// DNS Header
buffer.putShort((Math.random() * 65535).toInt().toShort()) // Random transaction ID
buffer.putShort(0x0100.toShort()) // Standard query, recursion desired
buffer.putShort(1) // Questions
buffer.putShort(0) // Answers
buffer.putShort(0) // Authority
buffer.putShort(0) // Additional
// Question section
val parts = hostname.split(".")
for (part in parts) {
buffer.put(part.length.toByte())
buffer.put(part.toByteArray(StandardCharsets.UTF_8))
}
buffer.put(0) // End of name
buffer.putShort(16) // TXT record type
buffer.putShort(1) // Class IN
val result = ByteArray(buffer.position())
buffer.rewind()
buffer.get(result)
return result
}
private fun createDnsAQuery(hostname: String): ByteArray {
val buffer = ByteBuffer.allocate(512)
// DNS Header
buffer.putShort((Math.random() * 65535).toInt().toShort())
buffer.putShort(0x0100.toShort())
buffer.putShort(1) // Questions
buffer.putShort(0) // Answers
buffer.putShort(0) // Authority
buffer.putShort(0) // Additional
// Question section
val parts = hostname.split(".")
for (part in parts) {
buffer.put(part.length.toByte())
buffer.put(part.toByteArray(StandardCharsets.UTF_8))
}
buffer.put(0) // End of name
buffer.putShort(1) // A record type
buffer.putShort(1) // Class IN
val result = ByteArray(buffer.position())
buffer.rewind()
buffer.get(result)
return result
}
private suspend fun queryTxtRecordDirect(hostname: String, dnsServerIp: String): String? =
withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Querying $hostname directly from $dnsServerIp")
val resolver = InetAddress.getByName(dnsServerIp)
val socket = DatagramSocket()
socket.soTimeout = 15000
val query = createDnsTxtQuery(hostname)
val packet = DatagramPacket(query, query.size, resolver, 53)
socket.send(packet)
val buffer = ByteArray(1024)
val response = DatagramPacket(buffer, buffer.size)
socket.receive(response)
socket.close()
// Parse response and look for client IP
val txtRecords = parseAllTxtRecords(response.data, response.length)
Log.d(TAG, "TXT records from $dnsServerIp: $txtRecords")
// Look for the actual client IP (not EDNS subnet)
for (record in txtRecords) {
val ip = extractClientIpFromRecord(record)
if (ip != null && isValidPublicIp(ip)) {
Log.d(TAG, "Found client IP in record '$record': $ip")
return@withContext ip
}
}
null
} catch (e: Throwable) {
Log.w(TAG, "Failed to query $hostname from $dnsServerIp", e)
null
}
}
private suspend fun queryOpenDnsFallback(): String? = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Trying OpenDNS fallback")
// OpenDNS returns your IP as an A record for myip.opendns.com
val openDnsServers = listOf(
"208.67.222.222",
"208.67.220.220",
"208.67.222.220",
"208.67.220.222",
).shuffled()
for (dnsServer in openDnsServers) {
try {
val result = queryARecord("myip.opendns.com", dnsServer)
if (result != null && isValidPublicIp(result)) {
Log.i(TAG, "Got IP from OpenDNS: $result")
return@withContext result
}
} catch (e: Throwable) {
Log.w(TAG, "OpenDNS server $dnsServer failed", e)
}
}
null
} catch (e: Throwable) {
Log.w(TAG, "OpenDNS fallback failed", e)
null
}
}
private fun queryARecord(hostname: String, dnsServer: String): String? {
try {
val resolver = InetAddress.getByName(dnsServer)
val socket = DatagramSocket()
socket.soTimeout = 10000
val query = createDnsAQuery(hostname)
val packet = DatagramPacket(query, query.size, resolver, 53)
socket.send(packet)
val buffer = ByteArray(512)
val response = DatagramPacket(buffer, buffer.size)
socket.receive(response)
socket.close()
return parseARecord(response.data, response.length)
} catch (e: Throwable) {
Log.w(TAG, "A record query failed", e)
return null
}
}
private fun skipDnsName(buffer: ByteBuffer) {
while (true) {
if (buffer.remaining() < 1)
break
val length = buffer.get().toInt() and 0xFF
if (length == 0)
break
if ((length and 0xC0) == 0xC0) {
if (buffer.remaining() >= 1)
buffer.get()
break
}
val skipBytes = minOf(length, buffer.remaining())
buffer.position(buffer.position() + skipBytes)
}
}
private fun readTxtRecord(buffer: ByteBuffer, dataLength: Int): String {
val startPos = buffer.position()
val endPos = startPos + dataLength
val result = StringBuilder()
while (buffer.position() < endPos && buffer.hasRemaining()) {
val txtLength = buffer.get().toInt() and 0xFF
if (txtLength > 0 && buffer.remaining() >= txtLength) {
val txtData = ByteArray(txtLength)
buffer.get(txtData)
val txtString = String(txtData, StandardCharsets.UTF_8)
if (result.isNotEmpty()) result.append(" ")
result.append(txtString)
} else if (txtLength == 0)
break
else
break
}
return result.toString()
}
private fun parseAllTxtRecords(data: ByteArray, length: Int): List<String> {
val records = mutableListOf<String>()
try {
val buffer = ByteBuffer.wrap(data, 0, length)
// Skip header
buffer.getShort() // id
buffer.getShort() // flags
val questionsCount = buffer.getShort()
val answersCount = buffer.getShort()
buffer.getShort() // authority
buffer.getShort() // additional
if (answersCount.toInt() == 0)
return records
// Skip questions
for (i in 0 until questionsCount) {
skipDnsName(buffer)
buffer.getShort() // type
buffer.getShort() // class
}
// Parse answers
for (i in 0 until answersCount) {
skipDnsName(buffer)
val type = buffer.getShort()
buffer.getShort() // class
buffer.getInt() // ttl
val dataLength = buffer.getShort()
if (type.toInt() == 16) { // TXT record
val txtRecord = readTxtRecord(buffer, dataLength.toInt())
if (txtRecord.isNotEmpty())
records.add(txtRecord)
} else
buffer.position(buffer.position() + dataLength)
}
} catch (e: Throwable) {
Log.e(TAG, "Failed to parse TXT records", e)
}
return records
}
private fun parseARecord(data: ByteArray, length: Int): String? {
try {
val buffer = ByteBuffer.wrap(data, 0, length)
// Skip header
buffer.getShort() // id
buffer.getShort() // flags
val questionsCount = buffer.getShort()
val answersCount = buffer.getShort()
buffer.getShort() // authority
buffer.getShort() // additional
if (answersCount.toInt() == 0)
return null
// Skip questions
for (i in 0 until questionsCount) {
skipDnsName(buffer)
buffer.getShort() // type
buffer.getShort() // class
}
// Parse answers
for (i in 0 until answersCount) {
skipDnsName(buffer)
val type = buffer.getShort()
buffer.getShort() // class
buffer.getInt() // ttl
val dataLength = buffer.getShort()
if (type.toInt() == 1 && dataLength.toInt() == 4) { // A record
val ip = ByteArray(4)
buffer.get(ip)
return "${ip[0].toUByte()}.${ip[1].toUByte()}.${ip[2].toUByte()}.${ip[3].toUByte()}"
} else
buffer.position(buffer.position() + dataLength)
}
} catch (e: Throwable) {
Log.e(TAG, "Failed to parse A record", e)
}
return null
}
/**
* Get public IP using Google's authoritative name servers directly
* This bypasses local/private IP address and gets public/external IP address
*/
suspend fun getViaDNS(): String? = withContext(Dispatchers.IO) {
// Google's authoritative name servers
val googleNameServers = listOf(
"ns1.google.com",
"ns2.google.com",
"ns3.google.com",
"ns4.google.com",
).shuffled()
for (nsHostname in googleNameServers) {
try {
Log.d(TAG, "Trying Google NS: $nsHostname")
// First resolve the name server IP
val nsIp = resolveNameServer(nsHostname)
if (nsIp != null) {
// Query o-o.myaddr.l.google.com directly from this NS
val result = queryTxtRecordDirect("o-o.myaddr.l.google.com", nsIp)
if (result != null && isValidPublicIp(result)) {
Log.i(TAG, "Got valid public IP from $nsHostname: $result")
return@withContext result
}
}
} catch (e: Throwable) {
Log.w(TAG, "Failed to query via $nsHostname", e)
}
}
// Fallback to OpenDNS method
Log.i(TAG, "Google NS failed, trying OpenDNS fallback")
return@withContext queryOpenDnsFallback()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment