Позвольте мне ПРЕДУПРЕЖДЕНИЕ, сказав, что я определенно не знаю здесь с точки зрения знаний. Я пытаюсь внедрить VPNService, который пользователи моего приложения могут включить, чтобы любой трафик не был сброшен к белым доменам, чтобы быть отброшенным. Эта реализация должна быть полностью настройкой, поэтому без использования внешних или самостоятельных серверов VPN. Мой мыслительный процесс был следующим: < /p>
Добавить маршруты IPv4 и IPv6 Catch-All в строитель, чтобы получить весь трафик от сети в мой интерфейс TUN. Получен пакет UDP/Port-53, когда я определяю, направляется ли он к белостисковому домену или нет. Если это так, я пропускаю его и пересылаю ответ DNS -сервера, в противном случае я синтезирую поддельный, чтобы «провалить» запрос на поиск. Я экспериментировал с различными подходами, похожими на то, что вы видите ниже, но самое близкое, что я получил,-это каким-то образом привлекать вещи для работы на Wi-Fi, но не на сотовой связи.class SecureThread(
private val vpnService: VpnService,
) : Runnable {
companion object {
var allowBypass = false
// may need to be marked as @volatile (?)
var injectedWhitelistedHosts = mutableSetOf()
private const val DEFAULT_TTL_SECONDS = 60
private const val PACKET_IHL_MIN_LENGTH = 5
private const val PACKET_IPV4_VERSION = 4
private const val PACKET_IPV6_VERSION = 6
private const val THREAD_INTERRUPTION_TIMEOUT = 10_000L
private const val PACKET_LENGTH = 32767
private const val DATAGRAM_PACKET_SIZE = 1024
private const val THREAD_POOL_EXECUTOR_TASK_CAPACITY = 50
private const val THREAD_POOL_EXECUTOR_CORE_POOL_SIZE = 4
private const val THREAD_POOL_EXECUTOR_MAX_POOL_SIZE = 32
private const val THREAD_POOL_EXECUTOR_KEEP_ALIVE = 60L
}
/**
* A Volatile field used by threads of the
* worker pool in order to be notified when
* to begin their tear-down process.
*/
@Volatile
private var isShuttingDown = false
private val dnsCache = DnsCache()
private var dnsServer: InetAddress? = null
private var fileDescriptor: ParcelFileDescriptor? = null
private var thread: Thread? = null
private var whitelistedHosts =
mutableSetOf(
"sentry.io",
"sentry.dev",
"mapbox.com",
"posthog.com",
"time.android.com",
"fonts.google.com",
"cloudflare-dns.com", // needed?
"wikipedia.org",
// maybe googleapis.com ?
)
private val secureVpnServiceBuilder = SecureVpnServiceBuilder()
/**
* Checks if the current string is a subdomain of a given domain.
* @param domain The domain for which to check against
* @return True if the current string is a subdomain of the given domain,
* false otherwise.
*/
private fun String.isSubdomainOf(domain: String): Boolean {
val host = trimEnd('.').lowercase()
val d = domain.trimEnd('.').lowercase()
return host == d || host.endsWith(".$d")
}
/**
* Starts the execution of the main thread and updates the
* SecureDataModeVpnService's status accordingly.
* @see SecureDataModeVpnService
*/
fun startThread() {
SecureDataModeVpnService.status = SecureDataModeVpnService.Status.STARTING
debugLog("Starting VPN thread")
thread =
Thread(this, "Secure Data").apply {
start()
SecureDataModeVpnService.status = SecureDataModeVpnService.Status.RUNNING
}
}
/**
* Starts the tear-down process of the main thread and updates the
* SecureDataModeVpnService's status accordingly. This function is
* responsible for shutting down all threads gracefully, clearing resources
* and general clean up.
* @see SecureDataModeVpnService
*/
fun stopThread() {
debugLog("Stopping VPN thread & cleaning resources")
SecureDataModeVpnService.status = SecureDataModeVpnService.Status.STOPPING
isShuttingDown = true // flip first, so handlers bail out
thread?.interrupt()
// only wait if we’re not inside that same thread
if (Thread.currentThread() !== thread) {
try {
thread?.join(THREAD_INTERRUPTION_TIMEOUT)
} catch (ie: InterruptedException) {
Thread.currentThread().interrupt()
}
}
thread = null
// Now safe—no more handlers and no self-join
fileDescriptor?.close()
fileDescriptor = null
SecureDataModeVpnService.status = SecureDataModeVpnService.Status.STOPPED
debugLog("Stopped VPN thread")
}
/**
* Main entry-point for this thread, responsible for executing
* the VPN's packet processing process.
*/
override fun run() {
try {
runVpn()
} catch (ie: InterruptedException) {
debugLog("VPN thread interrupted — exiting cleanly")
Thread.currentThread().interrupt()
} catch (securityEx: SecurityException) {
Timber.e(securityEx, "SecurityException: Current thread could not modify this thread")
} catch (interruptEx: ClosedByInterruptException) {
Timber.e(interruptEx, "ClosedByInterruptException: Thread blocked in I/O operation")
} catch (e: Exception) {
Timber.e(e, "Unexpected error in VPN thread")
} finally {
stopThread()
}
}
/**
* Responsible for configuring the file descriptor, initializing
* resources and executing the main processing loop.
*/
private fun runVpn() {
configure()
val inStream = FileInputStream(fileDescriptor!!.fileDescriptor)
val outStream = FileOutputStream(fileDescriptor!!.fileDescriptor)
val dnsSocket = DatagramSocket().also { vpnService.protect(it) }
val buffer = ByteArray(PACKET_LENGTH)
val executorTasksQueue = LinkedBlockingQueue(THREAD_POOL_EXECUTOR_TASK_CAPACITY)
val executor =
ThreadPoolExecutor(
THREAD_POOL_EXECUTOR_CORE_POOL_SIZE,
THREAD_POOL_EXECUTOR_MAX_POOL_SIZE,
THREAD_POOL_EXECUTOR_KEEP_ALIVE,
TimeUnit.SECONDS,
executorTasksQueue,
SecureThreadFactory(),
).apply {
allowCoreThreadTimeOut(true)
}
try {
debugLog("checking is current thread interrupted = ${Thread.currentThread().isInterrupted}")
while (!Thread.currentThread().isInterrupted) {
val len =
try {
inStream.read(buffer)
} catch (e: InterruptedIOException) {
Timber.e(e, "Failed to read from input stream")
break
} catch (ioe: IOException) {
Timber.e(ioe, "Failed to read from input stream")
break
}
when {
len < 0 -> {
// EOF: underlying TUN FD was closed
debugLog("Stream closed (EOF), exiting VPN loop")
break
}
len == 0 -> {
// No bytes this iteration, but this should be rare.
// We can skip without exiting.
continue
}
else -> {
if (isShuttingDown) break
val packetBytes = buffer.copyOf(len)
try {
executor.execute {
handleDnsRequest(packetBytes, dnsSocket, outStream)
}
} catch (_: RejectedExecutionException) {
debugLog("pool is shutting down or saturated, dropping packet")
}
}
}
}
} finally {
debugLog("Closing resources...")
isShuttingDown = true
debugLog("Shutting down executor...")
// signal no new tasks
executor.shutdown()
try {
// wait up to, say, 5s for all in-flight tasks to finish
debugLog("Awaiting tasks termination for executor.")
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
// still tasks running? cancel them
debugLog("Forcing shutDownNow().")
executor.shutdownNow()
}
} catch (ie: InterruptedException) {
debugLog("executor.awaitTermination was interrupted. Forcing shutDownNow().")
executor.shutdownNow()
Thread.currentThread().interrupt()
}
debugLog("Closing sockets & streams")
dnsSocket.close()
outStream.close()
fileDescriptor?.close()
inStream.close()
}
}
/**
* Send the raw IP packet straight back to the TUN
* (i.e. let it go to the real network unmodified).
*/
private fun passthrough(
packet: ByteArray,
outStream: FileOutputStream,
) {
synchronized(outStream) {
outStream.write(packet)
}
}
/**
* Main function responsible for packet processing
* and delegation based on the packet's IHL and IP version.
* @param packet A ByteArray containing the packet payload to process
* @param dnsSocket The DNS socket towards which any allowed packets will be forwarded to
* @param outStream The output stream to which the payload will be written to
* @see DatagramPacket
* @see DatagramSocket
*/
private fun handleDnsRequest(
packet: ByteArray,
dnsSocket: DatagramSocket,
outStream: FileOutputStream,
) {
if (isShuttingDown) {
debugLog("Dropping request because shutdown in progress")
return
}
// 20 bytes check for ihl
if (packet.size < 20) {
debugLog("Skipping packet with less than 20 bytes")
return
}
// header length >= 5?
val ihl = packet[0].toInt() and 0xF
if (ihl < PACKET_IHL_MIN_LENGTH) {
debugLog("Skipping packet with invalid IHL=$ihl")
return
}
// check for IPv4 - version == 4
val version = (packet[0].toInt() ushr PACKET_IPV4_VERSION) and 0xF
when (version) { // todo:sp consider reusing code for packet processing
PACKET_IPV4_VERSION -> handleV4(packet, dnsSocket, outStream)
PACKET_IPV6_VERSION -> handleV6(packet, dnsSocket, outStream)
else -> debugLog("Skipping non ipV4/ipV6 packet")
}
}
/**
* Processes IPV4 packets by allowing ones that are directed towards
* whitelisted domains and blocking the rest.
* @param packet A ByteArray containing the IPV4 packet payload to process
* @param dnsSocket The DNS socket towards which any allowed packets will be forwarded to
* @param outStream The output stream to which the payload will be written to
*/
private fun handleV4(
packet: ByteArray,
dnsSocket: DatagramSocket,
outStream: FileOutputStream,
) {
debugLog("Processing IPV4 packet...")
try {
// todo:sp surround with try catch?
val parsedPacket = IpV4Packet.newPacket(packet, 0, packet.size)
if (parsedPacket.payload !is UdpPacket) {
debugLog("Passthrough non-UDP packet")
passthrough(packet, outStream)
return
}
// 2) If it is UDP but not port 53, let it through
val udpPacket = parsedPacket.payload as UdpPacket
val dstPort = udpPacket.header.dstPort.valueAsInt()
if (dstPort != 53) {
debugLog("Passthrough UDP port $dstPort")
passthrough(packet, outStream)
return
}
// from here on we know it's a DNS query (port 53)
val dnsRawData = (parsedPacket.payload as UdpPacket).payload.rawData
val dnsMsg = Message(dnsRawData)
if (dnsMsg.question == null) {
debugLog("Dropping non DNS question message.")
return
}
// Pull the ANSWER section records
val answerRecords: MutableList? = dnsMsg.getSection(Section.ANSWER)
// Find the minimum positive TTL
val ttl: Long =
answerRecords
?.map { it.ttl }
?.filter { it > 0 }
?.minOrNull()
?: DEFAULT_TTL_SECONDS.toLong()
val dnsQueryName = dnsMsg.question.name.toString(true)
val cacheKey = "$dnsQueryName:${dnsMsg.question.type}"
val shouldPacketBeAllowed =
whitelistedHosts.plus(injectedWhitelistedHosts).any { dnsQueryName.isSubdomainOf(it) } ||
allowBypass
val response: ByteArray =
dnsCache.get(cacheKey) {
if (isShuttingDown) {
// immediate fallback: synthesize a “blocked” or empty response
debugLog("isShuttingDown was true in dnsCache loader.")
return@get DnsCacheEntry(data = byteArrayOf(), expiresAt = 0)
}
val rawData =
if (shouldPacketBeAllowed) {
debugLog("Allowing request for $dnsQueryName, bypass: $allowBypass")
val outPacket = DatagramPacket(dnsRawData, 0, dnsRawData.size, dnsServer!!, 53)
try {
dnsSocket.send(outPacket)
} catch (e: Exception) {
Timber.e(e, "Failed to send outPacket via dnsSocket")
}
val datagramData = ByteArray(DATAGRAM_PACKET_SIZE)
val replyPacket = DatagramPacket(datagramData, datagramData.size)
dnsSocket.receive(replyPacket)
// trim the data to the actual reply's length instead of the DATAGRAM_PACKET_SIZE
// since that could contain "useless" zeroes that when stored and retrieved via the cache
// can confuse the DNS servers and cause issues with their responses.
datagramData.copyOf(replyPacket.length)
} else {
debugLog("Blocking request for $dnsQueryName")
dnsMsg.header.setFlag(Flags.QR.toInt())
dnsMsg.addRecord(
ARecord(
dnsMsg.question.name,
dnsMsg.question.dClass,
10.toLong(),
Inet4Address.getLocalHost(),
),
Section.ANSWER,
)
dnsMsg.toWire()
}
DnsCacheEntry(
data = rawData,
expiresAt = System.currentTimeMillis() + ttl * 1_000,
)
}
val udpOutPacket = parsedPacket.payload as UdpPacket
val ipOutPacket =
IpV4Packet
.Builder(parsedPacket)
.srcAddr(parsedPacket.header.dstAddr)
.dstAddr(parsedPacket.header.srcAddr)
.correctChecksumAtBuild(true)
.correctLengthAtBuild(true)
.payloadBuilder(
UdpPacket
.Builder(udpOutPacket)
.srcPort(udpOutPacket.header.dstPort)
.dstPort(udpOutPacket.header.srcPort)
.srcAddr(parsedPacket.header.dstAddr)
.dstAddr(parsedPacket.header.srcAddr)
.correctChecksumAtBuild(true)
.correctLengthAtBuild(true)
.payloadBuilder(
UnknownPacket
.Builder()
.rawData(response),
),
).build()
try {
synchronized(outStream) {
outStream.write(ipOutPacket.rawData)
}
} catch (e: Exception) {
Timber.e(e, "Something went wrong in handleDnsRequest() while writing to the output stream")
}
} catch (e: Exception) {
Timber.e(e, "Failed to process IPV4 packet.")
}
}
/**
* Processes IPV6 packets by allowing ones that are directed towards
* whitelisted domains and blocking the rest.
* @param packet A ByteArray containing the IPV6 packet payload to process
* @param dnsSocket The DNS socket towards which any allowed packets will be forwarded to
* @param outStream The output stream to which the payload will be written to
*/
private fun handleV6(
raw: ByteArray,
dnsSocket: DatagramSocket,
out: FileOutputStream,
) {
debugLog("Processing IPV6 packet...")
// parse IPv6
val ipv6 = IpV6Packet.newPacket(raw, 0, raw.size)
// If not UDP at all, passthrough
val udp = ipv6.payload as? UdpPacket
if (udp == null) {
debugLog("Passthrough non-UDP IPv6")
passthrough(raw, out)
return
}
// If UDP but not port 53, passthrough
if (udp.header.dstPort.valueAsInt() != 53) {
debugLog("Passthrough UDP/IPv6 port ${udp.header.dstPort.valueAsInt()}")
passthrough(raw, out)
return
}
// extract DNS question
val dnsRaw = udp.payload.rawData
val dnsMsg = Message(dnsRaw)
val name = dnsMsg.question?.name?.toString(true) ?: return
// Pull the ANSWER section records
val answerRecords: MutableList? = dnsMsg.getSection(Section.ANSWER)
// Find the minimum positive TTL
val ttl: Long =
answerRecords
?.map { it.ttl }
?.filter { it > 0 }
?.minOrNull()
?: DEFAULT_TTL_SECONDS.toLong()
val cacheKey = "$name:${dnsMsg.question.type}"
val shouldPacketBeAllowed =
whitelistedHosts.plus(injectedWhitelistedHosts).any { name.isSubdomainOf(it) } ||
allowBypass
val responseData =
dnsCache.get(cacheKey) {
if (isShuttingDown) {
// immediate fallback: synthesize a “blocked” or empty response
return@get DnsCacheEntry(data = byteArrayOf(), expiresAt = 0)
}
val rawData =
if (shouldPacketBeAllowed) {
// forward query to real DNS server (v4 socket still fine)
debugLog("Allowing request for $name, bypass: $allowBypass")
val outPacket = DatagramPacket(dnsRaw, 0, dnsRaw.size, dnsServer!!, 53)
try {
dnsSocket.send(outPacket)
} catch (e: Exception) {
Timber.e(e)
}
val datagramData = ByteArray(DATAGRAM_PACKET_SIZE)
val replyPacket = DatagramPacket(datagramData, datagramData.size)
dnsSocket.receive(replyPacket)
datagramData.copyOf(replyPacket.length)
} else {
debugLog("Blocking request for $name")
dnsMsg.header.setFlag(Flags.QR.toInt())
dnsMsg.addRecord(
ARecord(
dnsMsg.question.name,
dnsMsg.question.dClass,
10.toLong(),
Inet4Address.getLocalHost(),
),
Section.ANSWER,
)
dnsMsg.toWire()
}
DnsCacheEntry(
data = rawData,
expiresAt = System.currentTimeMillis() + ttl * 1_000,
)
}
// rebuild an IPv6/UDP packet with `responseData` as payload
val udpReply =
UdpPacket
.Builder(udp)
.payloadBuilder(UnknownPacket.Builder().rawData(responseData))
.correctChecksumAtBuild(true)
.correctLengthAtBuild(true)
val replyPkt =
IpV6Packet
.Builder(ipv6)
.payloadBuilder(udpReply)
.correctLengthAtBuild(true)
.build()
synchronized(out) {
try {
out.write(replyPkt.rawData)
} catch (ioe: IOException) {
Timber.e(ioe, "Failed to write reply packet to the out stream for ipv6.")
}
}
}
/**
* Configures the file descriptor used to
* read/write packets.
* @see SecureVpnServiceBuilder
* @see InetUtils
* @see ParcelFileDescriptor
*/
private fun configure() {
debugLog("Configuring ParcelFileDescriptor...")
val builder = secureVpnServiceBuilder.configure(vpnService, vpnService.Builder())
dnsServer = secureVpnServiceBuilder.upstreamDnsServers.firstOrNull()
if (dnsServer == null) {
debugLog("Device dns server was null. Falling back to CloudFlare")
dnsServer = InetAddress.getByName("1.1.1.1")
}
fileDescriptor =
builder
.setSession("Secure Data")
.establish()
debugLog("ParcelFileDescriptor configured")
}
private fun debugLog(msg: String) {
Timber.d("${SecureDataModeVpnService.TAG}-${SecureThread::class.simpleName}:: $msg")
}
}
< /code>
Тогда у нас есть фактическая реализация vpnservice.internal class DnsCache(
private val maxSize: Int = DEFAULT_CACHE_MAX_SIZE,
) {
private val map =
object : LinkedHashMap(maxSize, CACHE_LOAD_FACTOR, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = size > maxSize
}
/**
* Retrieves the value associated with the
* given key. If the associated entry has
* expired or isn't found, the loader callback
* will be executed and its result will be stored.
* @param key The key whose value to retrieve
* @param loader A callback that will be run when the key
* isn't found or the TTL associated with the value has expired.
*/
@Synchronized
fun get(
key: K,
loader: () -> DnsCacheEntry,
): ByteArray {
val now = System.currentTimeMillis()
map[key]?.let { entry ->
if (entry.expiresAt > now) {
debugLog("Cache-Hit for $key")
return entry.data
}
debugLog("Key $key has expired.")
map.remove(key)
} ?: Timber.d("Cache-Miss for $key")
debugLog("Loading new value into cache for $key")
val fresh =
loader().also {
map[key] = it
}
return fresh.data
}
private fun debugLog(msg: String) {
Timber.d("${SecureDataModeVpnService.TAG}-${DnsCache::class.simpleName}:: $msg")
}
}
internal data class DnsCacheEntry(
val data: ByteArray,
val expiresAt: Long,
)
< /code>
Я в основном проиграл на этом этапе и хотел бы получить отзыв о том, что я делаю неправильно, или, возможно, забыл справиться здесь. Я искал аналогичные реализации, но не нашел ничего, что делает что -то подобное локально, только те, которые используют VPN -сервер, размещенный где -то еще.
Подробнее здесь: https://stackoverflow.com/questions/796 ... load-any-w
Реализация локального VPNService, который позволяет белую часть, не загружает никаких веб -сайтов ⇐ Android
-
- Похожие темы
- Ответы
- Просмотры
- Последнее сообщение