package eu.neilalexander.yggdrasil import android.content.* import android.net.VpnService import android.os.Build import android.os.ParcelFileDescriptor import android.system.OsConstants import android.util.Log import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.preference.PreferenceManager import eu.neilalexander.yggdrasil.YggStateReceiver.Companion.YGG_STATE_INTENT import mobile.Yggdrasil import org.json.JSONArray import java.io.FileInputStream import java.io.FileOutputStream import java.util.concurrent.atomic.AtomicBoolean import kotlin.concurrent.thread private const val TAG = "PacketTunnelProvider" const val SERVICE_NOTIFICATION_ID = 1000 open class PacketTunnelProvider: VpnService() { companion object { const val STATE_INTENT = "eu.neilalexander.yggdrasil.PacketTunnelProvider.STATE_MESSAGE" const val ACTION_START = "eu.neilalexander.yggdrasil.PacketTunnelProvider.START" const val ACTION_STOP = "eu.neilalexander.yggdrasil.PacketTunnelProvider.STOP" const val ACTION_TOGGLE = "eu.neilalexander.yggdrasil.PacketTunnelProvider.TOGGLE" const val ACTION_CONNECT = "eu.neilalexander.yggdrasil.PacketTunnelProvider.CONNECT" } private var yggdrasil = Yggdrasil() private var started = AtomicBoolean() private lateinit var config: ConfigurationProxy private var readerThread: Thread? = null private var writerThread: Thread? = null private var updateThread: Thread? = null private var parcel: ParcelFileDescriptor? = null private var readerStream: FileInputStream? = null private var writerStream: FileOutputStream? = null override fun onCreate() { super.onCreate() config = ConfigurationProxy(applicationContext) } override fun onDestroy() { super.onDestroy() stop() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null) { Log.d(TAG, "Intent is null") return START_NOT_STICKY } val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext) val enabled = preferences.getBoolean(PREF_KEY_ENABLED, false) return when (intent.action ?: ACTION_STOP) { ACTION_STOP -> { Log.d(TAG, "Stopping...") stop(); START_NOT_STICKY } ACTION_CONNECT -> { Log.d(TAG, "Connecting...") if (started.get()) { connect() } else { start() } START_STICKY } ACTION_TOGGLE -> { Log.d(TAG, "Toggling...") if (started.get()) { stop(); START_NOT_STICKY } else { start(); START_STICKY } } else -> { if (!enabled) { Log.d(TAG, "Service is disabled") return START_NOT_STICKY } Log.d(TAG, "Starting...") start(); START_STICKY } } } private fun start() { if (!started.compareAndSet(false, true)) { return } val notification = createServiceNotification(this, State.Enabled) startForeground(SERVICE_NOTIFICATION_ID, notification) Log.d(TAG, config.getJSON().toString()) yggdrasil.startJSON(config.getJSONByteArray()) val address = yggdrasil.addressString val builder = Builder() .addAddress(address, 7) .addRoute("200::", 7) // We do this to trick the DNS-resolver into thinking that we have "regular" IPv6, // and therefore we need to resolve AAAA DNS-records. // See: https://android.googlesource.com/platform/bionic/+/refs/heads/master/libc/dns/net/getaddrinfo.c#1935 // and: https://android.googlesource.com/platform/bionic/+/refs/heads/master/libc/dns/net/getaddrinfo.c#365 // If we don't do this the DNS-resolver just doesn't do DNS-requests with record type AAAA, // and we can't use DNS with Yggdrasil addresses. .addRoute("2000::", 128) .allowFamily(OsConstants.AF_INET) .allowBypass() .setBlocking(true) .setMtu(yggdrasil.mtu.toInt()) .setSession("Yggdrasil") // On Android API 29+ apps can opt-in/out to using metered networks. // If we don't set metered status of VPN it is considered as metered. // If we set it to false, then it will inherit this status from underlying network. // See: https://developer.android.com/reference/android/net/VpnService.Builder#setMetered(boolean) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { builder.setMetered(false) } val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext) val serverString = preferences.getString(KEY_DNS_SERVERS, "") if (serverString!!.isNotEmpty()) { val servers = serverString.split(",") if (servers.isNotEmpty()) { servers.forEach { Log.i(TAG, "Using DNS server $it") builder.addDnsServer(it) } } } if (preferences.getBoolean(KEY_ENABLE_CHROME_FIX, false)) { builder.addRoute("2001:4860:4860::8888", 128) } parcel = builder.establish() val parcel = parcel if (parcel == null || !parcel.fileDescriptor.valid()) { stop() return } readerStream = FileInputStream(parcel.fileDescriptor) writerStream = FileOutputStream(parcel.fileDescriptor) readerThread = thread { reader() } writerThread = thread { writer() } updateThread = thread { updater() } var intent = Intent(STATE_INTENT) intent.putExtra("type", "state") intent.putExtra("started", true) intent.putExtra("ip", yggdrasil.addressString) intent.putExtra("subnet", yggdrasil.subnetString) intent.putExtra("coords", yggdrasil.coordsString) intent.putExtra("peers", yggdrasil.peersJSON) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) intent = Intent(YGG_STATE_INTENT) intent.putExtra("state", STATE_ENABLED) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) } private fun stop() { if (!started.compareAndSet(true, false)) { return } yggdrasil.stop() readerStream?.let { it.close() readerStream = null } writerStream?.let { it.close() writerStream = null } parcel?.let { it.close() parcel = null } readerThread?.let { it.interrupt() readerThread = null } writerThread?.let { it.interrupt() writerThread = null } updateThread?.let { it.interrupt() updateThread = null } var intent = Intent(STATE_INTENT) intent.putExtra("type", "state") intent.putExtra("started", false) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) intent = Intent(YGG_STATE_INTENT) intent.putExtra("state", STATE_DISABLED) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) stopForeground(true) stopSelf() } private fun connect() { if (!started.get()) { return } yggdrasil.retryPeersNow() } private fun updater() { var lastStateUpdate = System.currentTimeMillis() updates@ while (started.get()) { if ((application as GlobalApplication).needUiUpdates()) { val intent = Intent(STATE_INTENT) intent.putExtra("type", "state") intent.putExtra("started", true) intent.putExtra("ip", yggdrasil.addressString) intent.putExtra("subnet", yggdrasil.subnetString) intent.putExtra("coords", yggdrasil.coordsString) intent.putExtra("peers", yggdrasil.peersJSON) intent.putExtra("dht", yggdrasil.dhtjson) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) } val curTime = System.currentTimeMillis() if (lastStateUpdate + 10000 < curTime) { val intent = Intent(YGG_STATE_INTENT) var state = STATE_ENABLED val dht = yggdrasil.dhtjson if (dht != null && dht != "null") { val dhtState = JSONArray(dht) val count = dhtState.length() if (count > 1) state = STATE_CONNECTED } intent.putExtra("state", state) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) lastStateUpdate = curTime } if (Thread.currentThread().isInterrupted) { break@updates } if (sleep()) return } } private fun sleep(): Boolean { try { Thread.sleep(1000) } catch (e: InterruptedException) { return true } return false } private fun writer() { val buf = ByteArray(65535) writes@ while (started.get()) { val writerStream = writerStream val writerThread = writerThread if (writerThread == null || writerStream == null) { Log.i(TAG, "Write thread or stream is null") break@writes } if (Thread.currentThread().isInterrupted || !writerStream.fd.valid()) { Log.i(TAG, "Write thread interrupted or file descriptor is invalid") break@writes } try { val len = yggdrasil.recvBuffer(buf) if (len > 0) { writerStream.write(buf, 0, len.toInt()) } } catch (e: Exception) { Log.i(TAG, "Error in write: $e") if (e.toString().contains("ENOBUFS")) { //TODO Check this by some error code //More info about this: https://github.com/AdguardTeam/AdguardForAndroid/issues/724 continue } break@writes } } writerStream?.let { it.close() writerStream = null } } private fun reader() { val b = ByteArray(65535) reads@ while (started.get()) { val readerStream = readerStream val readerThread = readerThread if (readerThread == null || readerStream == null) { Log.i(TAG, "Read thread or stream is null") break@reads } if (Thread.currentThread().isInterrupted ||!readerStream.fd.valid()) { Log.i(TAG, "Read thread interrupted or file descriptor is invalid") break@reads } try { val n = readerStream.read(b) yggdrasil.sendBuffer(b, n.toLong()) } catch (e: Exception) { Log.i(TAG, "Error in sendBuffer: $e") break@reads } } readerStream?.let { it.close() readerStream = null } } }