@@ -708,11 +708,13 @@ class DnsVpnService : VpnService() {
708708 // Check for error conditions on TUN fd
709709 if (pollFds[0 ].revents.toInt() and (OsConstants .POLLERR or OsConstants .POLLHUP ) != 0 ) {
710710 Log .w(TAG , " packetLoop: TUN fd error/hangup" )
711+ fdErrorCount.incrementAndGet()
711712 break
712713 }
713714 } catch (e: ErrnoException ) {
714715 if (e.errno == OsConstants .EINTR ) continue // interrupted by signal, retry
715716 if (! isRunning) break
717+ fdErrorCount.incrementAndGet()
716718 Log .e(TAG , " Poll error: ${e.message} " )
717719 delay(10 )
718720 } catch (e: Exception ) {
@@ -722,6 +724,11 @@ class DnsVpnService : VpnService() {
722724 }
723725 }
724726 Log .i(TAG , " packetLoop exited after $count packets" )
727+ // Auto-restart on fd error (not clean shutdown)
728+ if (isRunning) {
729+ Log .w(TAG , " packetLoop exited unexpectedly while running — restarting VPN" )
730+ restartVpn()
731+ }
725732 }
726733
727734 private suspend fun processIpv4Dns (packet : ByteArray , length : Int ) {
@@ -757,9 +764,9 @@ class DnsVpnService : VpnService() {
757764 }
758765
759766 val blocked = isDomainBlocked(domain)
760- logAsync(domain, blocked, app, qtype)
761767
762768 if (blocked) {
769+ logAsync(domain, true , app, qtype)
763770 Log .d(TAG , " BLOCKED $domain ($qtype ) [${app.second.ifEmpty { " system" }} ]" )
764771 sendBlockResponse(dns, packet, ihl, false , qtype)
765772 } else {
@@ -769,15 +776,17 @@ class DnsVpnService : VpnService() {
769776 val cached = dnsCache.get(domain, qtypeNum, txId)
770777 if (cached != null ) {
771778 Log .d(TAG , " CACHE HIT $domain ($qtype )" )
779+ logAsync(domain, false , app, qtype) // basic log for cache hits
772780 wrapResponseV4(packet, ihl, cached)?.let { writeChannel.send(it) }
773781 allowedCount++
774782 return
775783 }
776784
785+ // Rich log with CNAME/IPs/latency happens inside forward methods
777786 Log .d(TAG , " ALLOWED $domain ($qtype )" )
778787 val pCopy = packet.copyOf(length)
779- if (useDoH) serviceScope.launch { forwardDoH(dns, domain, pCopy, ihl) }
780- else serviceScope.launch { forwardUdp(dns, domain, pCopy, ihl) }
788+ if (useDoH) serviceScope.launch { forwardDoH(dns, domain, pCopy, ihl, app ) }
789+ else serviceScope.launch { forwardUdp(dns, domain, pCopy, ihl, app ) }
781790 allowedCount++
782791 }
783792 }
@@ -819,16 +828,28 @@ class DnsVpnService : VpnService() {
819828 }
820829
821830 val blocked = isDomainBlocked(domain)
822- logAsync(domain, blocked, app, qtype)
823831
824832 if (blocked) {
833+ logAsync(domain, true , app, qtype)
825834 val resp = buildBlockResponse(dns, qtype) ? : return
826835 val wrapped = wrapResponseV6(packet, hdr, resp) ? : return
827836 writeChannel.send(wrapped); blockedCount++
828837 if (blockedCount % 100 == 0 ) updateNotification(blockedCount)
829838 } else {
839+ // Cache lookup
840+ val qtypeNum = DnsPacketBuilder .parseQueryType(dns)
841+ val txId = if (dns.size >= 2 ) byteArrayOf(dns[0 ], dns[1 ]) else byteArrayOf(0 , 0 )
842+ val cached = dnsCache.get(domain, qtypeNum, txId)
843+ if (cached != null ) {
844+ Log .d(TAG , " CACHE HIT (v6) $domain ($qtype )" )
845+ wrapResponseV6(packet, hdr, cached)?.let { writeChannel.send(it) }
846+ allowedCount++
847+ return
848+ }
849+
830850 val pCopy = packet.copyOf(length)
831- serviceScope.launch { forwardUdpV6(dns, domain, pCopy, hdr) }
851+ if (useDoH) serviceScope.launch { forwardDoH(dns, domain, pCopy, 0 , app, wrapV6 = true , v6Hdr = hdr) }
852+ else serviceScope.launch { forwardUdpV6(dns, domain, pCopy, hdr, app) }
832853 allowedCount++
833854 }
834855 }
@@ -862,12 +883,23 @@ class DnsVpnService : VpnService() {
862883 }
863884
864885 private fun logAsync (domain : String , blocked : Boolean , app : Pair <String , String >, qtype : String ) {
886+ logAsyncRich(domain, blocked, app, qtype)
887+ }
888+
889+ /* * Rich log entry with CNAME chain, resolved IPs, latency, and upstream server. */
890+ private fun logAsyncRich (
891+ domain : String , blocked : Boolean , app : Pair <String , String >, qtype : String ,
892+ cnameChain : String = "", resolvedIps : String = "",
893+ responseTimeMs : Int = 0, upstreamServer : String = ""
894+ ) {
865895 // Always count stats even if logging disabled
866896 if (blocked) pendingBlockedStats.incrementAndGet() else pendingAllowedStats.incrementAndGet()
867897
868898 val entry = DnsLogEntry (
869899 hostname = domain, blocked = blocked,
870- appPackage = app.first, appLabel = app.second, queryType = qtype
900+ appPackage = app.first, appLabel = app.second, queryType = qtype,
901+ cnameChain = cnameChain, resolvedIps = resolvedIps,
902+ responseTimeMs = responseTimeMs, upstreamServer = upstreamServer
871903 )
872904
873905 // Emit to live query stream (non-blocking, drops oldest if full)
@@ -1433,7 +1465,8 @@ class DnsVpnService : VpnService() {
14331465
14341466 // ── DNS Forwarding ───────────────────────────────────────
14351467
1436- private suspend fun forwardUdp (dns : ByteArray , domain : String , orig : ByteArray , ihl : Int ) {
1468+ private suspend fun forwardUdp (dns : ByteArray , domain : String , orig : ByteArray , ihl : Int ,
1469+ app : Pair <String , String > = Pair ("", "")) {
14371470 try {
14381471 val startMs = System .currentTimeMillis()
14391472 val sock = DatagramSocket (); protect(sock)
@@ -1445,11 +1478,15 @@ class DnsVpnService : VpnService() {
14451478 sock.receive(rp); sock.close()
14461479 val respBytes = buf.copyOf(rp.length)
14471480 val latencyMs = (System .currentTimeMillis() - startMs).toInt()
1481+ val qtype = parseDnsQueryType(dns)
14481482
14491483 // CNAME cloaking detection — block if any CNAME target is in blocklist
14501484 val cnameResult = CnameCloakDetector .inspect(respBytes, blocklist)
14511485 if (cnameResult.blocked) {
14521486 Log .i(TAG , " CNAME CLOAK blocked: $domain -> ${cnameResult.blockedCname} " )
1487+ logAsyncRich(domain, true , app, qtype,
1488+ cnameChain = cnameResult.cnameChain.joinToString(" ," ),
1489+ responseTimeMs = latencyMs, upstreamServer = primary)
14531490 val blockResp = buildBlockResponse(dns, DnsPacketBuilder .parseQueryType(dns).let {
14541491 when (it) { 1 -> " A" ; 28 -> " AAAA" ; else -> " A" }
14551492 })
@@ -1458,29 +1495,42 @@ class DnsVpnService : VpnService() {
14581495 return
14591496 }
14601497
1498+ // Log rich data for allowed queries
1499+ val resolvedIps = CnameCloakDetector .extractAnswerIps(respBytes)
1500+ logAsyncRich(domain, false , app, qtype,
1501+ cnameChain = cnameResult.cnameChain.joinToString(" ," ),
1502+ resolvedIps = resolvedIps.joinToString(" ," ),
1503+ responseTimeMs = latencyMs, upstreamServer = primary)
1504+
14611505 cacheDnsAnswerIps(domain, respBytes)
1462- // Cache the response
14631506 val qtypeNum = DnsPacketBuilder .parseQueryType(dns)
14641507 dnsCache.put(domain, qtypeNum, respBytes)
14651508
14661509 wrapResponseV4(orig, ihl, respBytes)?.let { writeChannel.send(it) }
14671510 } catch (_: java.net.SocketTimeoutException ) {
1468- sock.close(); forwardUdpFallback(dns, domain, orig, ihl)
1511+ sock.close(); forwardUdpFallback(dns, domain, orig, ihl, app )
14691512 }
14701513 } catch (_: Exception ) { }
14711514 }
14721515
1473- private suspend fun forwardUdpFallback (dns : ByteArray , domain : String , orig : ByteArray , ihl : Int ) {
1516+ private suspend fun forwardUdpFallback (dns : ByteArray , domain : String , orig : ByteArray , ihl : Int ,
1517+ app : Pair <String , String > = Pair ("", "")) {
14741518 try {
1519+ val startMs = System .currentTimeMillis()
14751520 val fallback = upstreamDnsServers.getOrElse(1 ) { UPSTREAM_DNS [1 ] }
14761521 val sock = DatagramSocket (); protect(sock); sock.soTimeout = 5000
14771522 sock.send(DatagramPacket (dns, dns.size, InetAddress .getByName(fallback), DNS_PORT ))
14781523 val buf = ByteArray (1500 ); val rp = DatagramPacket (buf, buf.size)
14791524 sock.receive(rp); sock.close()
14801525 val respBytes = buf.copyOf(rp.length)
1526+ val latencyMs = (System .currentTimeMillis() - startMs).toInt()
1527+ val qtype = parseDnsQueryType(dns)
14811528
14821529 val cnameResult = CnameCloakDetector .inspect(respBytes, blocklist)
14831530 if (cnameResult.blocked) {
1531+ logAsyncRich(domain, true , app, qtype,
1532+ cnameChain = cnameResult.cnameChain.joinToString(" ," ),
1533+ responseTimeMs = latencyMs, upstreamServer = " $fallback (fallback)" )
14841534 val blockResp = buildBlockResponse(dns, DnsPacketBuilder .parseQueryType(dns).let {
14851535 when (it) { 1 -> " A" ; 28 -> " AAAA" ; else -> " A" }
14861536 })
@@ -1489,49 +1539,88 @@ class DnsVpnService : VpnService() {
14891539 return
14901540 }
14911541
1542+ val resolvedIps = CnameCloakDetector .extractAnswerIps(respBytes)
1543+ logAsyncRich(domain, false , app, qtype,
1544+ cnameChain = cnameResult.cnameChain.joinToString(" ," ),
1545+ resolvedIps = resolvedIps.joinToString(" ," ),
1546+ responseTimeMs = latencyMs, upstreamServer = " $fallback (fallback)" )
1547+
14921548 cacheDnsAnswerIps(domain, respBytes)
14931549 dnsCache.put(domain, DnsPacketBuilder .parseQueryType(dns), respBytes)
14941550 wrapResponseV4(orig, ihl, respBytes)?.let { writeChannel.send(it) }
14951551 } catch (_: Exception ) { }
14961552 }
14971553
1498- private suspend fun forwardDoH (dns : ByteArray , domain : String , orig : ByteArray , ihl : Int ) {
1554+ private suspend fun forwardDoH (dns : ByteArray , domain : String , orig : ByteArray , ihl : Int ,
1555+ app : Pair <String , String > = Pair ("", ""),
1556+ wrapV6 : Boolean = false, v6Hdr : Int = 0) {
14991557 try {
1558+ val startMs = System .currentTimeMillis()
15001559 val resp = dohResolver.resolve(dns, dohProvider)
15011560 if (resp != null ) {
1561+ val latencyMs = (System .currentTimeMillis() - startMs).toInt()
1562+ val qtype = parseDnsQueryType(dns)
1563+ val upstreamLabel = " DoH:${dohProvider.name} "
1564+
15021565 // CNAME cloaking detection
15031566 val cnameResult = CnameCloakDetector .inspect(resp, blocklist)
15041567 if (cnameResult.blocked) {
15051568 Log .i(TAG , " CNAME CLOAK (DoH) blocked: $domain -> ${cnameResult.blockedCname} " )
1569+ logAsyncRich(domain, true , app, qtype,
1570+ cnameChain = cnameResult.cnameChain.joinToString(" ," ),
1571+ responseTimeMs = latencyMs, upstreamServer = upstreamLabel)
15061572 val blockResp = buildBlockResponse(dns, DnsPacketBuilder .parseQueryType(dns).let {
15071573 when (it) { 1 -> " A" ; 28 -> " AAAA" ; else -> " A" }
15081574 })
1509- if (blockResp != null ) wrapResponseV4(orig, ihl, blockResp)?.let { writeChannel.send(it) }
1575+ if (blockResp != null ) {
1576+ if (wrapV6) wrapResponseV6(orig, v6Hdr, blockResp)?.let { writeChannel.send(it) }
1577+ else wrapResponseV4(orig, ihl, blockResp)?.let { writeChannel.send(it) }
1578+ }
15101579 blockedCount++
15111580 return
15121581 }
15131582
1583+ val resolvedIps = CnameCloakDetector .extractAnswerIps(resp)
1584+ logAsyncRich(domain, false , app, qtype,
1585+ cnameChain = cnameResult.cnameChain.joinToString(" ," ),
1586+ resolvedIps = resolvedIps.joinToString(" ," ),
1587+ responseTimeMs = latencyMs, upstreamServer = upstreamLabel)
1588+
15141589 cacheDnsAnswerIps(domain, resp)
15151590 val qtypeNum = DnsPacketBuilder .parseQueryType(dns)
15161591 dnsCache.put(domain, qtypeNum, resp)
15171592
1518- wrapResponseV4(orig, ihl, resp)?.let { writeChannel.send(it) }
1593+ if (wrapV6) wrapResponseV6(orig, v6Hdr, resp)?.let { writeChannel.send(it) }
1594+ else wrapResponseV4(orig, ihl, resp)?.let { writeChannel.send(it) }
1595+ }
1596+ else {
1597+ if (wrapV6) forwardUdpV6(dns, domain, orig, v6Hdr, app)
1598+ else forwardUdp(dns, domain, orig, ihl, app)
15191599 }
1520- else forwardUdp(dns, domain, orig, ihl) // DoH failed, fallback to plaintext
1521- } catch (_: Exception ) { forwardUdp(dns, domain, orig, ihl) }
1600+ } catch (_: Exception ) {
1601+ if (wrapV6) forwardUdpV6(dns, domain, orig, v6Hdr, app)
1602+ else forwardUdp(dns, domain, orig, ihl, app)
1603+ }
15221604 }
15231605
1524- private suspend fun forwardUdpV6 (dns : ByteArray , domain : String , orig : ByteArray , hdr : Int ) {
1606+ private suspend fun forwardUdpV6 (dns : ByteArray , domain : String , orig : ByteArray , hdr : Int ,
1607+ app : Pair <String , String > = Pair ("", "")) {
15251608 try {
1609+ val startMs = System .currentTimeMillis()
15261610 val primary = upstreamDnsServers.firstOrNull() ? : UPSTREAM_DNS [0 ]
15271611 val sock = DatagramSocket (); protect(sock); sock.soTimeout = 5000
15281612 sock.send(DatagramPacket (dns, dns.size, InetAddress .getByName(primary), DNS_PORT ))
15291613 val buf = ByteArray (1500 ); val rp = DatagramPacket (buf, buf.size)
15301614 sock.receive(rp); sock.close()
15311615 val respBytes = buf.copyOf(rp.length)
1616+ val latencyMs = (System .currentTimeMillis() - startMs).toInt()
1617+ val qtype = parseDnsQueryType(dns)
15321618
15331619 val cnameResult = CnameCloakDetector .inspect(respBytes, blocklist)
15341620 if (cnameResult.blocked) {
1621+ logAsyncRich(domain, true , app, qtype,
1622+ cnameChain = cnameResult.cnameChain.joinToString(" ," ),
1623+ responseTimeMs = latencyMs, upstreamServer = primary)
15351624 val blockResp = buildBlockResponse(dns, DnsPacketBuilder .parseQueryType(dns).let {
15361625 when (it) { 1 -> " A" ; 28 -> " AAAA" ; else -> " A" }
15371626 })
@@ -1540,6 +1629,12 @@ class DnsVpnService : VpnService() {
15401629 return
15411630 }
15421631
1632+ val resolvedIps = CnameCloakDetector .extractAnswerIps(respBytes)
1633+ logAsyncRich(domain, false , app, qtype,
1634+ cnameChain = cnameResult.cnameChain.joinToString(" ," ),
1635+ resolvedIps = resolvedIps.joinToString(" ," ),
1636+ responseTimeMs = latencyMs, upstreamServer = primary)
1637+
15431638 cacheDnsAnswerIps(domain, respBytes)
15441639 dnsCache.put(domain, DnsPacketBuilder .parseQueryType(dns), respBytes)
15451640 wrapResponseV6(orig, hdr, respBytes)?.let { writeChannel.send(it) }
0 commit comments