Kivuos
Kivuos

Reputation: 13

Android VPN service || Able to capture packets but failing to do DNS resolution

Goal

Things that are happening -

Thing where I am stuck -

This is my complete code for capturing and logging my packets.

package com.example.vpn
import android.net.VpnService
import android.os.ParcelFileDescriptor
import android.util.Log
import java.io.FileDescriptor
import java.io.FileInputStream
import java.io.FileOutputStream
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.SocketTimeoutException
import java.util.concurrent.SynchronousQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit

class VpnNetworkException(message: String, cause: Throwable? = null) : Exception(message, cause)

class MyVpnService : VpnService(){
    private var TAG = "SS"
    private var vpnInterface: ParcelFileDescriptor? = null

    override fun onCreate() {
        super.onCreate()
        startVpn()
    }

    private fun startVpn() {
        val builder = Builder()
        val ip = getLocalIpAddress()

        Log.d("SS", "DeviceIP $ip")

        builder.addAddress("192.168.30.221", 24)
        builder.addDnsServer("8.8.8.8")
        builder.addRoute("0.0.0.0",0 )
        builder.setBlocking(true)

        vpnInterface = builder.setSession("vpn").establish()

        vpnInterface?.let {
            Thread {
                capturePackets(it.fileDescriptor)
            }.start()
        }
    }

    private fun capturePackets(fd: FileDescriptor) {
        val input = FileInputStream(fd)
        val buffer = ByteArray(32767)
        val dnsSocket = DatagramSocket()
        val executor = ThreadPoolExecutor(0, 32, 60L, TimeUnit.SECONDS, SynchronousQueue<Runnable>())
        protect(dnsSocket)

        while (true) {
            val length = input.read(buffer)

            if (length == 0) {
                continue
            }

            val packetData = buffer.copyOf(length)

            executor.execute {
                Log.d("SS","Packet: ${packetData.toHexString()}")
                parsePacket(packetData)
            }
        }
    }

    private fun ByteArray.toHexString(): String {
        return joinToString(separator = " ") { byte -> "%02x".format(byte) }
    }

    private fun parsePacket(packetData: ByteArray) {
        // Parse the IP Header (assuming IPv4)
        val ipVersion = (packetData[0].toInt() shr 4) and 0xF

        if (ipVersion == 4) {
            Log.d("SS","IPv4 Packet Detected ${packetData.toHexString()}")

            // Extract the source and destination IP addresses (IPv4)
            val sourceIp = packetData.slice(12..15).toByteArray().toIpString()
            val destinationIp = packetData.slice(16..19).toByteArray().toIpString()
            Log.d("SS","Source IP: $sourceIp")
            Log.d("SS","Destination IP: $destinationIp")

            // Extract the Protocol (TCP/UDP/ICMP)
            when (packetData[9].toInt()) {
                6 -> { // TCP Protocol
                    Log.d("SS","TCP Packet Detected")
                    parseTcpHeader(packetData)
                }
                17 -> { // UDP Protocol
                    Log.d("SS","UDP Packet Detected")
                    parseUdpHeader(packetData, destinationIp)
                }
                else -> Log.d("SS","Other Protocol: ${packetData[9].toInt()}")
            }
        } else {
            Log.d("SS","Non-IPv4 Packet")
        }
    }

    // Parse TCP Header
    private fun parseTcpHeader(packetData: ByteArray) {
        // IP header length is in the lower 4 bits of the first byte (multiply by 4 to get byte size)
        val ipHeaderLength = (packetData[0].toInt() and 0x0F) * 4

        // TCP header starts right after the IP header
        val tcpHeaderStart = ipHeaderLength
        val sourcePort = (packetData[tcpHeaderStart].toInt() shl 8) or packetData[tcpHeaderStart + 1].toInt()
        val destinationPort = (packetData[tcpHeaderStart + 2].toInt() shl 8) or packetData[tcpHeaderStart + 3].toInt()

        Log.d("SS", "Source Port: $sourcePort")
        Log.d("SS", "Destination Port: $destinationPort")
    }

    // Parse UDP Header
    private fun parseUdpHeader(packetData: ByteArray, destinationIP: String) {
        Log.w(TAG, "=================================== REQUEST ======================================")
        // IP header length is in the lower 4 bits of the first byte (multiply by 4 to get byte size)
        val ipHeaderLength = (packetData[0].toInt() and 0x0F) * 4
        Log.d("SS", "IP header length $ipHeaderLength")

        if (packetData.size <= ipHeaderLength){
            return
        }

        if (packetData.size <ipHeaderLength + 8){
            return
        }

        // UDP header starts right after the IP header
        val udpHeader = packetData.copyOfRange(ipHeaderLength, ipHeaderLength+8)
        val udpPayload = packetData.copyOfRange(ipHeaderLength+8, packetData.size)

        Log.d("SS", "UDP HEADER, ${udpHeader.toHexString()}")
        Log.d("SS", "UDP PAYLOAD, ${udpPayload.toHexString()}")

        val sourcePort = ((udpHeader[0].toInt() and 0xFF) shl 8) or (udpHeader[1].toInt() and 0xFF)
        val destinationPort = ((udpHeader[2].toInt() and 0xFF) shl 8) or (udpHeader[3].toInt() and 0xFF)

        Log.d("SS","Source Port: $sourcePort")
        Log.d("SS","Destination Port: $destinationPort")

        val txID = ((udpPayload[0].toInt() and 0xFF) shl 8) or (udpPayload[1].toInt() and 0xFF)
        val flags = ((udpPayload[2].toInt() and 0xFF) shl 8) or (udpPayload[3].toInt() and 0xFF)
        val questionCount = ((udpPayload[4].toInt() and 0xFF) shl 8) or (udpPayload[5].toInt() and 0xFF)
        val answerRRCount = ((udpPayload[6].toInt() and 0xFF) shl 8) or (udpPayload[7].toInt() and 0xFF)
        val authorityRRCount = ((udpPayload[8].toInt() and 0xFF) shl 8) or (udpPayload[9].toInt() and 0xFF)


        Log.d("SS", "transaction id: ${txID.toInt() and 0xFF}")
        Log.d("SS", "flags: ${flags.toInt() and 0xFF}")
        Log.d("SS", "question count: ${questionCount.toInt() and 0xFF}")
        Log.d("SS", "answerRRCount: ${answerRRCount.toInt() and 0xFF}")
        Log.d("SS", "authorityRRCount: ${authorityRRCount.toInt() and 0xFF}")

        val domainParts = mutableListOf<String>()
        var index = 12
        while (true){
            if (index >= udpPayload.size) {
                Log.e("SS", "Index out of bounds while parsing domain. Current index: $index, payload size: ${udpPayload.size}")
                break
            }

            val length = udpPayload[index].toInt() and 0xFF  // Get the length byte as an unsigned int
            if (length == 0) {  // Termination byte indicating the end of the domain name
                break
            }

            if (index + 1 + length > udpPayload.size) {
                Log.e("SS", "Label length exceeds UDP payload size. index: $index, length: $length, payload size: ${udpPayload.size}")
                break
            }

            // Extract the domain label and add it to the list
            val label = udpPayload.copyOfRange(index + 1, index + 1 + length)

            domainParts.add(String(label, Charsets.US_ASCII))  // Convert byte array to string
            index += length + 1  // Move index forward
        }

        val domainName = domainParts.joinToString(".")
        Log.d("SS", "Querying for domain $domainName")

        Log.w(TAG, "=================================== ******* ======================================")
        // Sending packets to destination
        sendUDPPackets(packetData, udpPayload, destinationIP, destinationPort)
    }

    // Extension function to convert bytes to an IPv4 string
    private fun ByteArray.toIpString(): String {
        return joinToString(separator = ".") { byte -> (byte.toInt() and 0xFF).toString() }
    }

    // get the local IP of the device
    private fun getLocalIpAddress(): String {
        try {
            val interfaces = NetworkInterface.getNetworkInterfaces()
            for (networkInterface in interfaces) {
                val addresses = networkInterface.inetAddresses
                for (address in addresses) {
                    // Exclude loopback addresses
                    if (!address.isLoopbackAddress && address is InetAddress) {
                        val ipAddress = address.hostAddress
                        // Check if the IP is IPv4 (ignore IPv6 addresses)
                        if (ipAddress.indexOf(':') < 0) {
                            return ipAddress
                        }
                    }
                }
            }
        } catch (e: Exception) {
            Log.e("SS", "Error getting IP address: ${e.message}")
        }
        return "192.168.30.1"  // Return null if no IP address found
    }

    // send UDP packet to the destination
    private fun sendUDPPackets(packet:ByteArray, udppayload: ByteArray, destinationIp:String, destinationPort:Int) {
        val socket = DatagramSocket()
        protect(socket)

        try {
            val dnsServer: InetAddress = InetAddress.getByName("8.8.8.8")
            val dnsPacket = DatagramPacket(udppayload, udppayload.size, dnsServer, 53)
            socket.send(dnsPacket)
            Log.d("SS", "Packet sent to $destinationIp:$destinationPort")

            // Set a timeout for receiving replies
            socket.soTimeout = 10000 // Timeout after 2 seconds

            // Attempt to receive a reply
            val datagramData = ByteArray(1024)
            val replyPacket = DatagramPacket(datagramData, datagramData.size)
            socket.receive(replyPacket)
            val response = datagramData.copyOf(replyPacket.length)

            val hexResponse = response.joinToString(" ") { String.format("%02X", it) }
            Log.i(TAG, "Received DNS response in hex: $hexResponse")

            injectPacket(packet, response)
        } catch (e: SocketTimeoutException) {
            Log.e("SS", "No reply received within the timeout period.")
        } catch (e: Exception) {
            Log.e("SS", "Error sending UDP packet: ${e.message}")
        } finally {
            socket.close()
        }
    }

    private fun injectPacket(packet: ByteArray, dnsResponse: ByteArray) {
        val ipOutPacket = packet.copyOf()
        // swap ipv4
        //12,13,14,15
        //16,17,18,19
        var temp = ipOutPacket[12]
        ipOutPacket[12] = ipOutPacket[16]
        ipOutPacket[16] = temp

        temp = ipOutPacket[13]
        ipOutPacket[13] = ipOutPacket[17]
        ipOutPacket[17] = temp

        temp = ipOutPacket[14]
        ipOutPacket[14] = ipOutPacket[18]
        ipOutPacket[18] = temp

        temp = ipOutPacket[15]
        ipOutPacket[15] = ipOutPacket[19]
        ipOutPacket[19] = temp

        // swaping ports
        // 20,21
        // 22,23
        val ipHeaderLength = (packet[0].toInt() and 0x0F) * 4
        //
        temp = ipOutPacket[ipHeaderLength]
        ipOutPacket[ipHeaderLength] = ipOutPacket[ipHeaderLength+2]
        ipOutPacket[ipHeaderLength+2] = temp

        temp = ipOutPacket[ipHeaderLength+1]
        ipOutPacket[ipHeaderLength+1] = ipOutPacket[ipHeaderLength+3]
        ipOutPacket[ipHeaderLength+3] = temp

        val newArray = ipOutPacket.sliceArray(0 until ipHeaderLength+8)
        val outArray = newArray + dnsResponse

        val totalBytes = outArray.size
        Log.d("SS","New byte length $totalBytes")

        outArray[3] = (totalBytes and 0xFF).toByte() // Lower byte
        outArray[2] = (totalBytes shr 8).toByte() // Upper byte

        val hexResponse = outArray.joinToString(" ") { String.format("%02X", it) }
        Log.i(TAG, "ipOutPacket response in hex: $hexResponse")

        parseDnsResponse(outArray)
        try {
            val output = FileOutputStream(vpnInterface!!.fileDescriptor)
            output.write(outArray)
            output.flush()
        } catch (e: Exception) {
            Log.e("VPN", "Error injecting packet: ${e.message}")
        }
    }

    private fun parseDnsResponse(packetData:ByteArray) {
        Log.w(TAG, "=================================== RESPONSE ======================================")
        val ipHeaderLength = (packetData[0].toInt() and 0x0F) * 4
        if (packetData.size <= ipHeaderLength){
            return
        }

        if (packetData.size <ipHeaderLength + 8){
            return
        }

        // UDP header starts right after the IP header
        val udpHeader = packetData.copyOfRange(ipHeaderLength, ipHeaderLength+8)
        val udpPayload = packetData.copyOfRange(ipHeaderLength+8, packetData.size)

        Log.d("SS", "UDP HEADER, ${udpHeader.toHexString()}")
        Log.d("SS", "UDP PAYLOAD, ${udpPayload.toHexString()}")

        val sourcePort = ((udpHeader[0].toInt() and 0xFF) shl 8) or (udpHeader[1].toInt() and 0xFF)
        val destinationPort = ((udpHeader[2].toInt() and 0xFF) shl 8) or (udpHeader[3].toInt() and 0xFF)

        Log.d("SS","Source Port: $sourcePort")
        Log.d("SS","Destination Port: $destinationPort")

        val txID = ((udpPayload[0].toInt() and 0xFF) shl 8) or (udpPayload[1].toInt() and 0xFF)
        val flags = ((udpPayload[2].toInt() and 0xFF) shl 8) or (udpPayload[3].toInt() and 0xFF)
        val questionCount = ((udpPayload[4].toInt() and 0xFF) shl 8) or (udpPayload[5].toInt() and 0xFF)
        val answerRRCount = ((udpPayload[6].toInt() and 0xFF) shl 8) or (udpPayload[7].toInt() and 0xFF)
        val authorityRRCount = ((udpPayload[8].toInt() and 0xFF) shl 8) or (udpPayload[9].toInt() and 0xFF)


        Log.d("SS", "transaction id: ${txID.toInt() and 0xFF}")
        Log.d("SS", "flags: ${flags.toInt() and 0xFF}")
        Log.d("SS", "question count: ${questionCount.toInt() and 0xFF}")
        Log.d("SS", "answerRRCount: ${answerRRCount.toInt() and 0xFF}")
        Log.d("SS", "authorityRRCount: ${authorityRRCount.toInt() and 0xFF}")

        val domainParts = mutableListOf<String>()
        var index = 12
        while (true){
            if (index >= udpPayload.size) {
                Log.e("SS", "Index out of bounds while parsing domain. Current index: $index, payload size: ${udpPayload.size}")
                break
            }

            val length = udpPayload[index].toInt() and 0xFF  // Get the length byte as an unsigned int
            if (length == 0) {  // Termination byte indicating the end of the domain name
                break
            }

            if (index + 1 + length > udpPayload.size) {
                Log.e("SS", "Label length exceeds UDP payload size. index: $index, length: $length, payload size: ${udpPayload.size}")
                break
            }

            // Extract the domain label and add it to the list
            val label = udpPayload.copyOfRange(index + 1, index + 1 + length)

            domainParts.add(String(label, Charsets.US_ASCII))  // Convert byte array to string
            index += length + 1  // Move index forward
        }

        val domainName = domainParts.joinToString(".")
        Log.d("SS", "Querying for domain $domainName")

        index += 5 // skip QTYPE (2 bytes) and QCLASS (2 bytes)

        // Parse the answer section
        for (i in 0 until answerRRCount) {
            // Answer Name (can be a pointer)
            val namePointer = ((udpPayload[index].toInt() and 0xFF) shl 8) or (udpPayload[index + 1].toInt() and 0xFF)
            index += 2 // Move past the pointer

            // Type (A, CNAME, etc.)
            val type = ((udpPayload[index].toInt() and 0xFF) shl 8) or (udpPayload[index + 1].toInt() and 0xFF)
            index += 2

            // Class (Internet = 1)
            val classType = ((udpPayload[index].toInt() and 0xFF) shl 8) or (udpPayload[index + 1].toInt() and 0xFF)
            index += 2

            // TTL (Time to live)
            val ttl = ((udpPayload[index].toInt() and 0xFF) shl 24) or
                    ((udpPayload[index + 1].toInt() and 0xFF) shl 16) or
                    ((udpPayload[index + 2].toInt() and 0xFF) shl 8) or
                    (udpPayload[index + 3].toInt() and 0xFF)
            index += 4

            // Data length
            val dataLength = ((udpPayload[index].toInt() and 0xFF) shl 8) or (udpPayload[index + 1].toInt() and 0xFF)
            index += 2

            // Data (IP address or CNAME)
            val data: String = when (type) {
                1 -> { // A record (IPv4 address)
                    val ip = "${udpPayload[index].toInt() and 0xFF}.${udpPayload[index + 1].toInt() and 0xFF}.${udpPayload[index + 2].toInt() and 0xFF}.${udpPayload[index + 3].toInt() and 0xFF}"
                    index += 4 // Move past the IP address
                    ip
                }
                5 -> { // CNAME record
                    val cnameParts = mutableListOf<String>()
                    var cnameIndex = index
                    while (udpPayload[cnameIndex].toInt() != 0) {
                        val labelLength = udpPayload[cnameIndex].toInt() and 0xFF
                        val cnameLabel = udpPayload.copyOfRange(cnameIndex + 1, cnameIndex + 1 + labelLength)
                        cnameParts.add(String(cnameLabel, Charsets.US_ASCII))
                        cnameIndex += labelLength + 1
                    }
                    index = cnameIndex + 1 // Move past the CNAME
                    cnameParts.joinToString(".")
                }
                else -> {
                    "Unknown type"
                }
            }

            Log.d("SS", "Answer #$i - Name: $namePointer, Type: $type, Class: $classType, TTL: $ttl, Data: $data")
        }

        Log.w(TAG, "=================================== ***** ======================================")
    }

    override fun onDestroy() {
        super.onDestroy()
        vpnInterface?.close()
    }
}

Logs:

 W  =================================== REQUEST ======================================
2024-09-28 19:31:57.244  5366-5550  SS                      com.example.vpn                      D  IP header length 20
2024-09-28 19:31:57.245  5366-5550  SS                      com.example.vpn                      D  UDP HEADER, 76 c5 00 35 00 28 07 75
2024-09-28 19:31:57.248  5366-5550  SS                      com.example.vpn                      D  UDP PAYLOAD, 00 f5 01 00 00 01 00 00 00 00 00 00 03 77 77 77 06 72 65 64 64 69 74 03 63 6f 6d 00 00 01 00 01
2024-09-28 19:31:57.249  5366-5550  SS                      com.example.vpn                      D  Source Port: 30405
2024-09-28 19:31:57.249  5366-5550  SS                      com.example.vpn                      D  Destination Port: 53
2024-09-28 19:31:57.249  5366-5550  SS                      com.example.vpn                      D  transaction id: 245
2024-09-28 19:31:57.249  5366-5550  SS                      com.example.vpn                      D  flags: 0
2024-09-28 19:31:57.249  5366-5550  SS                      com.example.vpn                      D  question count: 1
2024-09-28 19:31:57.249  5366-5550  SS                      com.example.vpn                      D  answerRRCount: 0
2024-09-28 19:31:57.249  5366-5550  SS                      com.example.vpn                      D  authorityRRCount: 0
2024-09-28 19:31:57.250  5366-5550  SS                      com.example.vpn                      D  Querying for domain www.reddit.com
2024-09-28 19:31:57.250  5366-5550  SS                      com.example.vpn                      W  =================================== ******* ======================================


 =================================== RESPONSE ======================================
ipOutPacket response in hex: 45 00 00 9F 4A 30 40 00 40 11 00 EC 08 08 08 08 C0 A8 1E DD 00 35 76 C5 00 28 07 75 00 F5 81 80 00 01 00 05 00 00 00 00 03 77 77 77 06 72 65 64 64 69 74 03 63 6F 6D 00 00 01 00 01 C0 0C 00 05 00 01 00 00 13 05 00 17 06 72 65 64 64 69 74 03 6D 61 70 06 66 61 73 74 6C 79 03 6E 65 74 00 C0 2C 00 01 00 01 00 00 00 18 00 04 97 65 01 8C C0 2C 00 01 00 01 00 00 00 18 00 04 97 65 C1 8C C0 2C 00 01 00 01 00 00 00 18 00 04 97 65 41 8C C0 2C 00 01 00 01 00 00 00 18 00 04 97 65 81 8C
2024-09-28 19:31:57.341  5366-5551  SS                      com.example.vpn                      D  UDP HEADER, 00 35 d5 5b 00 2e 10 c2
2024-09-28 19:31:57.341  5366-5489  SS                      com.example.vpn                      I  Received DNS response in hex: E4 23 81 80 00 01 00 05 00 00 00 00 07 70 72 65 76 69 65 77 04 72 65 64 64 02 69 74 00 00 01 00 01 C0 0C 00 05 00 01 00 00 00 23 00 21 09 64 75 61 6C 73 74 61 63 6B 06 72 65 64 64 69 74 03 6D 61 70 06 66 61 73 74 6C 79 03 6E 65 74 00 C0 2D 00 01 00 01 00 00 00 17 00 04 97 65 41 8C C0 2D 00 01 00 01 00 00 00 17 00 04 97 65 C1 8C C0 2D 00 01 00 01 00 00 00 17 00 04 97 65 81 8C C0 2D 00 01 00 01 00 00 00 17 00 04 97 65 01 8C
2024-09-28 19:31:57.341  5366-5489  SS                      com.example.vpn                      D  New byte length 170
2024-09-28 19:31:57.345  5366-5550  SS                      com.example.vpn                      D  UDP PAYLOAD, 00 f5 81 80 00 01 00 05 00 00 00 00 03 77 77 77 06 72 65 64 64 69 74 03 63 6f 6d 00 00 01 00 01 c0 0c 00 05 00 01 00 00 13 05 00 17 06 72 65 64 64 69 74 03 6d 61 70 06 66 61 73 74 6c 79 03 6e 65 74 00 c0 2c 00 01 00 01 00 00 00 18 00 04 97 65 01 8c c0 2c 00 01 00 01 00 00 00 18 00 04 97 65 c1 8c c0 2c 00 01 00 01 00 00 00 18 00 04 97 65 41 8c c0 2c 00 01 00 01 00 00 00 18 00 04 97 65 81 8c
2024-09-28 19:31:57.345  5366-5550  SS                      com.example.vpn                      D  Source Port: 53
2024-09-28 19:31:57.345  5366-5550  SS                      com.example.vpn                      D  Destination Port: 30405
2024-09-28 19:31:57.345  5366-5550  SS                      com.example.vpn                      D  transaction id: 245
2024-09-28 19:31:57.345  5366-5550  SS                      com.example.vpn                      D  flags: 128
2024-09-28 19:31:57.345  5366-5550  SS                      com.example.vpn                      D  question count: 1
2024-09-28 19:31:57.345  5366-5550  SS                      com.example.vpn                      D  answerRRCount: 5
2024-09-28 19:31:57.345  5366-5550  SS                      com.example.vpn                      D  authorityRRCount: 0
2024-09-28 19:31:57.345  5366-5550  SS                      com.example.vpn                      D  Querying for domain www.reddit.com
2024-09-28 19:31:57.346  5366-5550  SS                      com.example.vpn                      D  Answer #0 - Name: 49164, Type: 5, Class: 1, TTL: 4869, Data: reddit.map.fastly.net
2024-09-28 19:31:57.346  5366-5550  SS                      com.example.vpn                      D  Answer #1 - Name: 49196, Type: 1, Class: 1, TTL: 24, Data: 151.101.1.140
2024-09-28 19:31:57.346  5366-5550  SS                      com.example.vpn                      D  Answer #2 - Name: 49196, Type: 1, Class: 1, TTL: 24, Data: 151.101.193.140
2024-09-28 19:31:57.346  5366-5550  SS                      com.example.vpn                      D  Answer #3 - Name: 49196, Type: 1, Class: 1, TTL: 24, Data: 151.101.65.140
2024-09-28 19:31:57.346  5366-5550  SS                      com.example.vpn                      D  Answer #4 - Name: 49196, Type: 1, Class: 1, TTL: 24, Data: 151.101.129.140
2024-09-28 19:31:57.346  5366-5550  SS                      com.example.vpn                      W  =================================== ***** ======================================


- As there are very limited resource on this any help will be appreciated.
- Have been stuck in this issue for a week now. 😢

Upvotes: 0

Views: 79

Answers (0)

Related Questions