Commit c4f6f0ee authored by Daniel Wolf's avatar Daniel Wolf
Browse files

First try of using iptables instead of a dummy vpn

parent 79fd4523
...@@ -91,6 +91,12 @@ android { ...@@ -91,6 +91,12 @@ android {
} }
} }
configurations.all {
resolutionStrategy {
force 'com.frostnerd.utilskt:dnstunnelproxy:1.5.54-test4'
}
}
dependencies { dependencies {
def room_version = "2.1.0" def room_version = "2.1.0"
......
...@@ -207,6 +207,7 @@ ...@@ -207,6 +207,7 @@
</service> </service>
<service android:name=".service.RuleImportService"/> <service android:name=".service.RuleImportService"/>
<service android:name=".service.RuleExportService"/> <service android:name=".service.RuleExportService"/>
<service android:name=".service.RootDnsService"/>
<receiver <receiver
android:name=".receiver.AutostartReceiver" android:name=".receiver.AutostartReceiver"
......
...@@ -19,14 +19,12 @@ import androidx.appcompat.app.AppCompatActivity ...@@ -19,14 +19,12 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.frostnerd.dnstunnelproxy.DnsServerInformation import com.frostnerd.dnstunnelproxy.DnsServerInformation
import com.frostnerd.general.service.isServiceRunning import com.frostnerd.general.service.isServiceRunning
import com.frostnerd.smokescreen.R import com.frostnerd.smokescreen.*
import com.frostnerd.smokescreen.activity.SpeedTestActivity import com.frostnerd.smokescreen.activity.SpeedTestActivity
import com.frostnerd.smokescreen.dialog.ServerChoosalDialog import com.frostnerd.smokescreen.dialog.ServerChoosalDialog
import com.frostnerd.smokescreen.getPreferences
import com.frostnerd.smokescreen.registerLocalReceiver
import com.frostnerd.smokescreen.service.Command import com.frostnerd.smokescreen.service.Command
import com.frostnerd.smokescreen.service.DnsVpnService import com.frostnerd.smokescreen.service.DnsVpnService
import com.frostnerd.smokescreen.unregisterLocalReceiver import com.frostnerd.smokescreen.service.RootDnsService
import kotlinx.android.synthetic.main.fragment_main.* import kotlinx.android.synthetic.main.fragment_main.*
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
...@@ -80,6 +78,10 @@ class MainFragment : Fragment() { ...@@ -80,6 +78,10 @@ class MainFragment : Fragment() {
} }
updateVpnIndicators() updateVpnIndicators()
} }
startButton.setOnLongClickListener {
context!!.startForegroundServiceCompat(Intent(context!!, RootDnsService::class.java))
true
}
speedTest.setOnClickListener { speedTest.setOnClickListener {
startActivity(Intent(context!!, SpeedTestActivity::class.java)) startActivity(Intent(context!!, SpeedTestActivity::class.java))
} }
......
package com.frostnerd.smokescreen.service
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.IBinder
import android.os.StrictMode
import android.util.Base64
import androidx.core.app.NotificationCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.frostnerd.dnstunnelproxy.*
import com.frostnerd.encrypteddnstunnelproxy.HttpsDnsServerInformation
import com.frostnerd.encrypteddnstunnelproxy.tls.TLSUpstreamAddress
import com.frostnerd.smokescreen.*
import com.frostnerd.smokescreen.BuildConfig
import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.activity.BackgroundVpnConfigureActivity
import com.frostnerd.smokescreen.activity.PinActivity
import com.frostnerd.smokescreen.database.entities.CachedResponse
import com.frostnerd.smokescreen.database.getDatabase
import com.frostnerd.smokescreen.util.Notifications
import com.frostnerd.smokescreen.util.proxy.ProxyBypassHandler
import com.frostnerd.smokescreen.util.proxy.ProxyHttpsHandler
import com.frostnerd.smokescreen.util.proxy.ProxyTlsHandler
import com.frostnerd.smokescreen.util.proxy.SmokeProxy
import com.frostnerd.vpntunnelproxy.IpWrappingUdpTunnel
import com.frostnerd.vpntunnelproxy.Proxy
import com.frostnerd.vpntunnelproxy.UdpTunnel
import com.frostnerd.vpntunnelproxy.VPNTunnelProxy
import kotlinx.coroutines.*
import org.minidns.dnsmessage.DnsMessage
import org.minidns.dnsmessage.Question
import org.minidns.dnsname.DnsName
import org.minidns.record.A
import org.minidns.record.AAAA
import org.minidns.record.Record
import java.io.ByteArrayInputStream
import java.io.DataInputStream
import java.net.*
import java.util.concurrent.TimeoutException
import java.util.logging.Level
/*
* Copyright (C) 2019 Daniel Wolf (Ch4t4r)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class RootDnsService:Service() {
private lateinit var notificationBuilder: NotificationCompat.Builder
private var pauseNotificationAction:NotificationCompat.Action? = null
private lateinit var serverConfig:DnsServerConfiguration
private var queryCountOffset: Long = 0
private var packageBypassAmount = 0
private var dnsProxy: SmokeProxy? = null
private var vpnProxy: VPNTunnelProxy? = null
/*
URLs passed to the Service, which haven't been retrieved from the settings.
Null if the current servers are from the settings
*/
private var userServerConfig:DnsServerInformation<*>? = null
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
createNotification()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
println("STARTED")
setServerConfiguration(intent)
updateNotification(0)
run()
return START_STICKY
}
private fun createNotification() {
log("Creating notification")
notificationBuilder = NotificationCompat.Builder(this, Notifications.servicePersistentNotificationChannel(this))
if(getPreferences().hideNotificationIcon)
notificationBuilder.priority = NotificationCompat.PRIORITY_MIN
if(!getPreferences().showNotificationOnLockscreen)
notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_SECRET)
notificationBuilder.setContentTitle(getString(R.string.app_name))
notificationBuilder.setSmallIcon(R.drawable.ic_mainnotification)
notificationBuilder.setOngoing(true)
notificationBuilder.setAutoCancel(false)
notificationBuilder.setSound(null)
notificationBuilder.setOnlyAlertOnce(true)
notificationBuilder.setUsesChronometer(true)
notificationBuilder.setContentIntent(
PendingIntent.getActivity(
this, 1,
Intent(this, PinActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT
)
)
if(getPreferences().allowStopInNotification) {
val stopPendingIntent =
PendingIntent.getService(this, 1, DnsVpnService.commandIntent(this, Command.STOP), PendingIntent.FLAG_CANCEL_CURRENT)
val stopAction = NotificationCompat.Action(R.drawable.ic_stop, getString(R.string.all_stop), stopPendingIntent)
notificationBuilder.addAction(stopAction)
}
if(getPreferences().allowPauseInNotification) {
val pausePendingIntent =
PendingIntent.getService(this, 2,
DnsVpnService.commandIntent(this, Command.PAUSE_RESUME), PendingIntent.FLAG_CANCEL_CURRENT)
pauseNotificationAction = NotificationCompat.Action(R.drawable.ic_stat_pause, getString(R.string.all_pause), pausePendingIntent)
notificationBuilder.addAction(pauseNotificationAction)
}
updateNotification(0)
log("Notification created and posted.")
}
private fun updateNotification(queryCount: Int? = null) {
if (queryCount != null) notificationBuilder.setSubText(
getString(
R.string.notification_main_subtext,
queryCount
)
)
startForeground(1, notificationBuilder.build())
}
private fun setServerConfiguration(intent: Intent?) {
log("Updating server configuration..")
userServerConfig = BackgroundVpnConfigureActivity.readServerInfoFromIntent(intent)
serverConfig = getServerConfig()
serverConfig.forEachAddress { _, address ->
if(!address.addressCreator.isCurrentlyResolving()) address.addressCreator.resolveOrGetResultOrNull(true)
address.addressCreator.whenResolveFailed {
if(it is TimeoutException) {
address.addressCreator.resolveOrGetResultOrNull(true)
address.addressCreator.whenResolveFinishedSuccessfully {
}
}
}
address.addressCreator.whenResolveFinishedSuccessfully {
}
}
log("Server configuration updated to $serverConfig")
}
private fun getServerConfig(): DnsServerConfiguration {
TLSUpstreamAddress
return if(userServerConfig != null) {
userServerConfig!!.let {config ->
return if(config is HttpsDnsServerInformation) DnsServerConfiguration(config.serverConfigurations.values.toList(), null)
else {
DnsServerConfiguration(null, config.servers.map {
it.address as TLSUpstreamAddress
})
}
}
}
else {
val config = getPreferences().dnsServerConfig
if(config.hasTlsServer()) {
DnsServerConfiguration(null, config.servers.map {
it.address as TLSUpstreamAddress
})
} else {
DnsServerConfiguration((config as HttpsDnsServerInformation).serverConfigurations.map {
it.value
}, null)
}
}
}
private fun setNotificationText() {
val primaryServer:String
val secondaryServer:String?
if(serverConfig.httpsConfiguration != null) {
notificationBuilder.setContentTitle(getString(R.string.notification_main_title_https))
primaryServer = serverConfig.httpsConfiguration!![0].urlCreator.address.getUrl(true)
secondaryServer = serverConfig.httpsConfiguration!!.getOrNull(1)?.urlCreator?.address?.getUrl(true)
} else {
notificationBuilder.setContentTitle(getString(R.string.notification_main_title_tls))
primaryServer = serverConfig.tlsConfiguration!![0].formatToString()
secondaryServer = serverConfig.tlsConfiguration!!.getOrNull(1)?.formatToString()
}
val text = if (secondaryServer != null) {
getString(
if(getPreferences().isBypassBlacklist) R.string.notification_main_text_with_secondary else R.string.notification_main_text_with_secondary_whitelist,
primaryServer,
secondaryServer,
packageBypassAmount,
dnsProxy?.cache?.livingCachedEntries() ?: 0
)
} else {
getString(
if(getPreferences().isBypassBlacklist) R.string.notification_main_text else R.string.notification_main_text_whitelist,
primaryServer,
packageBypassAmount,
dnsProxy?.cache?.livingCachedEntries() ?: 0
)
}
notificationBuilder.setStyle(NotificationCompat.BigTextStyle(notificationBuilder).bigText(text))
}
private fun Byte.toIntWithUnsigned():Int {
return toUByte().toInt()
}
fun run() {
log("run() called")
log("Starting with config: $serverConfig")
log("Creating handle.")
val handle: DnsHandle
if(serverConfig.httpsConfiguration != null) {
handle = ProxyHttpsHandler(
serverConfig.httpsConfiguration!!,
connectTimeout = 5000,
queryCountCallback = {
setNotificationText()
updateNotification(it)
}
)
} else {
handle = ProxyTlsHandler(serverConfig.tlsConfiguration!!,
connectTimeout = 2000,
queryCountCallback = {
setNotificationText()
updateNotification(it)
})
}
log("Handle created, creating DNS proxy")
handle.ipv6Enabled = getPreferences().enableIpv6 && (getPreferences().forceIpv6 || hasDeviceIpv6Address())
handle.ipv4Enabled =
!handle.ipv6Enabled || (getPreferences().enableIpv4 && (getPreferences().forceIpv4 || hasDeviceIpv4Address()))
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().permitAll().build())
dnsProxy = SmokeProxy(handle, createProxyBypassHandlers(), createDnsCache(), createQueryLogger(), createLocalResolver())
dnsProxy!!.parseDevicePacketOverride = { arr, orig ->
val pPacket = orig(arr)
ParsedPacket(pPacket.sourceAddress, pPacket.destinationAddress, pPacket.sourcePort, pPacket.destinationPort, pPacket.dnsPayload)
}
log("DnsProxy created, creating VPN proxy")
vpnProxy = VPNTunnelProxy(dnsProxy!!, socketProtector = object:Proxy.SocketProtector {
override fun protectDatagramSocket(socket: DatagramSocket) {
}
override fun protectSocket(socket: Socket) {
}
override fun protectSocket(socket: Int) {
}
}, coroutineScope = CoroutineScope(
newFixedThreadPoolContext(2, "proxy-pool")
), logger = object:com.frostnerd.vpntunnelproxy.Logger() {
override fun logException(ex: Exception, terminal: Boolean, level: Level) {
if(terminal) log(ex)
else log(Logger.stacktraceToString(ex), "VPN-LIBRARY, $level")
}
override fun logMessage(message: String, level: Level) {
if(level >= Level.INFO || (BuildConfig.DEBUG && level >= Level.FINE)) {
log(message, "VPN-LIBRARY, $level")
}
}
})
log("VPN proxy creating, trying to run...")
val port = 45001
Runtime.getRuntime().exec("su && iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:$port").waitFor()
GlobalScope.launch {
IpWrappingUdpTunnel(bindPort = port, socketReadCoroutineContext = newSingleThreadContext(""), tunnelReadCoroutineContext = newSingleThreadContext("")).start(vpnProxy!!)
log("VPN proxy started.")
LocalBroadcastManager.getInstance(this@RootDnsService).sendBroadcast(Intent(DnsVpnService.BROADCAST_VPN_ACTIVE))
}
}
private fun createQueryLogger(): QueryListener? {
return if(getPreferences().loggingEnabled || getPreferences().queryLoggingEnabled) {
com.frostnerd.smokescreen.util.proxy.QueryListener(this)
} else null
}
/**
* Creates bypass handlers for each network and its associated search domains
* Requests for .*SEARCHDOMAIN won't use doh and are sent to the DNS servers of the network they originated from.
*/
private fun createProxyBypassHandlers(): MutableList<DnsHandle> {
val bypassHandlers = mutableListOf<DnsHandle>()
if(getPreferences().bypassSearchdomains) {
log("Creating bypass handlers for search domains of connected networks.")
val mgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
for (network in mgr.allNetworks) {
if(network == null) continue
val networkInfo = mgr.getNetworkInfo(network) ?: continue
if (networkInfo.isConnected && !mgr.isVpnNetwork(network)) {
val linkProperties = mgr.getLinkProperties(network) ?: continue
if (!linkProperties.domains.isNullOrBlank()) {
log("Bypassing domains ${linkProperties.domains} for network of type ${networkInfo.typeName}")
val domains = linkProperties.domains.split(",").toList()
bypassHandlers.add(
ProxyBypassHandler(
domains,
linkProperties.dnsServers[0]!!
)
)
}
}
}
log("${bypassHandlers.size} bypass handlers created.")
} else log("Not creating bypass handlers for search domains, bypass is disabled.")
if(getPreferences().pauseOnCaptivePortal) {
val dhcpServers = getDhcpDnsServers()
if(!dhcpServers.isEmpty()) bypassHandlers.add(CaptivePortalUdpDnsHandle(targetDnsServer = { dhcpServers.first() }))
}
return bypassHandlers
}
private fun createDnsCache(): SimpleDnsCache? {
val dnsCache: SimpleDnsCache?
dnsCache = if (getPreferences().useDnsCache) {
log("Creating DNS Cache.")
val cacheControl: CacheControl = if (!getPreferences().useDefaultDnsCacheTime) {
val cacheTime = getPreferences().customDnsCacheTime.toLong()
val nxDomainCacheTime = getPreferences().nxDomainCacheTime.toLong()
object : CacheControl {
override suspend fun getTtl(
answerMessage: DnsMessage,
dnsName: DnsName,
type: Record.TYPE,
record: Record<*>
): Long {
return if(answerMessage.responseCode == DnsMessage.RESPONSE_CODE.NX_DOMAIN) nxDomainCacheTime else cacheTime
}
override suspend fun getTtl(question: Question, record: Record<*>): Long = cacheTime
override fun shouldCache(question: Question): Boolean = true
}
} else DefaultCacheControl(getPreferences().minimumCacheTime.toLong())
val onClearCache:((currentCache:Map<String, Map<Record.TYPE, Map<Record<*>, Long>>>) -> Unit)? = if(getPreferences().keepDnsCacheAcrossLaunches) {
{ cache ->
log("Persisting current cache to Database.")
getDatabase().cachedResponseDao().deleteAll()
var persisted = 0
val entries = mutableListOf<CachedResponse>()
for(entry in cache) {
for(cachedType in entry.value) {
val recordsToPersist:MutableMap<Record<*>, Long> = mutableMapOf()
for(cachedRecord in cachedType.value) {
if(cachedRecord.value > System.currentTimeMillis()) {
recordsToPersist[cachedRecord.key] = cachedRecord.value
persisted++
}
}
if(!recordsToPersist.isEmpty()) {
entries.add(createPersistedCacheEntry(entry.key, cachedType.key, recordsToPersist))
}
}
}
GlobalScope.launch {
val dao = getDatabase().cachedResponseDao()
dao.insertAll(entries)
}
log("Cache persisted [$persisted records]")
}
} else null
SimpleDnsCache(cacheControl, CacheStrategy(getPreferences().maxCacheSize), onClearCache = onClearCache)
} else {
log("Not creating DNS cache, is disabled.")
null
}
if(dnsCache != null) log("Cache created.")
// Restores persisted cache
if(dnsCache != null && getPreferences().keepDnsCacheAcrossLaunches) {
log("Restoring old cache")
var restored = 0
var tooOld = 0
GlobalScope.launch {
for (cachedResponse in getDatabase().cachedResponseRepository().getAllAsync(GlobalScope)) {
val records = mutableMapOf<Record<*>, Long>()
for (record in cachedResponse.records) {
if(record.value > System.currentTimeMillis()) {
val bytes = Base64.decode(record.key, Base64.NO_WRAP)
val stream = DataInputStream(ByteArrayInputStream(bytes))
records[Record.parse(stream, bytes)] = record.value
restored++
} else tooOld++
}
dnsCache.addToCache(DnsName.from(cachedResponse.dnsName), cachedResponse.type, records)
}
getDatabase().cachedResponseDao().deleteAll()
}
log("$restored old records restored, deleting persisted cache. $tooOld records were too old.")
log("Persisted cache deleted.")
}
return dnsCache
}
private fun createLocalResolver(): LocalResolver? {
if(getPreferences().dnsRulesEnabled) {
return object: LocalResolver(false) {
private val dao = getDatabase().dnsRuleDao()
private val resolveResults = mutableMapOf<Question, String>()
private val wwwRegex = Regex("^www\\.")
private val useUserRules = getPreferences().customHostsEnabled
override suspend fun canResolve(question: Question): Boolean {
return if(question.type != Record.TYPE.A && question.type != Record.TYPE.AAAA) {
false
} else {
val uniformQuestion = question.name.toString().replace(wwwRegex, "")
val resolveResult = dao.findRuleTarget(uniformQuestion, question.type, useUserRules)?.let {
when (it) {
"0" -> "0.0.0.0"
"1" -> {
if (question.type == Record.TYPE.AAAA) "::1"
else "127.0.0.1"
}
else -> it
}
}
if (resolveResult != null) {
resolveResults[question] = resolveResult
true
} else false
}
}
override suspend fun resolve(question: Question): List<Record<*>> {
val result = resolveResults.remove(question)
return result?.let {
val data = if(question.type == Record.TYPE.A) {
A(it)
} else {
AAAA(it)
}
listOf(Record(question.name.toString(), question.type, question.clazz.value, 9999, data))
} ?: throw IllegalStateException()
}
override fun cleanup() {}
}
} else {
return null
}
}
private fun createPersistedCacheEntry(
dnsName: String,
type: Record.TYPE,
recordsToPersist: MutableMap<Record<*>, Long>
): CachedResponse {
val entity = CachedResponse(
dnsName,
type,
mutableMapOf()
)
for (record in recordsToPersist) {
entity.records[Base64.encodeToString(record.key.toByteArray(), Base64.NO_WRAP)] = record.value
}
return entity
}
private fun isPackageInstalled(packageName: String): Boolean {
return try {
packageManager.getApplicationInfo(packageName, 0).enabled
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
private fun getDhcpDnsServers():List<InetAddress> {
val mgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
for (network in mgr.allNetworks) {
if(network == null) continue
val info = mgr.getNetworkInfo(network) ?: continue
val capabilities = mgr.getNetworkCapabilities(network) ?: continue
if (info.isConnected && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) {
val linkProperties = mgr.getLinkProperties(network) ?: continue
return linkProperties.dnsServers
}
}
return emptyList()
}
private fun hasDeviceIpv4Address(): Boolean {
val mgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager