Make starting VPN service and running Yggdrasil (traffic hooks into Yggdrasil dummy interface)

This commit is contained in:
ChronosX88 2020-02-02 22:34:58 +04:00
parent 4e750afca7
commit 28b72376a7
Signed by: ChronosXYZ
GPG Key ID: 085A69A82C8C511A
6 changed files with 105 additions and 138 deletions

View File

@ -2,7 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="io.github.chronosx88.yggdrasil"> package="io.github.chronosx88.yggdrasil">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
@ -14,17 +15,12 @@
<service <service
android:name=".YggdrasilTunService" android:name=".YggdrasilTunService"
android:enabled="true" android:enabled="true"
android:exported="true"></service>
<service
android:name=".YggdrasilService"
android:enabled="true"
android:exported="true" android:exported="true"
android:permission="android.permission.BIND_VPN_SERVICE"> android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService"/> <action android:name="android.net.VpnService"/>
</intent-filter> </intent-filter>
</service> </service>
<activity android:name=".MainActivity"> <activity android:name=".MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -1,11 +1,13 @@
package io.github.chronosx88.yggdrasil package io.github.chronosx88.yggdrasil
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.os.Bundle import android.os.Bundle
import android.widget.Button import android.widget.Button
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private var isYggStarted = false private var isYggStarted = false
@ -16,12 +18,18 @@ class MainActivity : AppCompatActivity() {
val connectButton = findViewById<Button>(R.id.connect_button) val connectButton = findViewById<Button>(R.id.connect_button)
connectButton.setOnClickListener { connectButton.setOnClickListener {
if(!isYggStarted) { if(!isYggStarted) {
VpnService.prepare(this) // Prepare to establish a VPN connection.
val intent = Intent(this, YggdrasilTunService::class.java) // This method returns null if the VPN application is already prepared
startService(intent) // or if the user has previously consented to the VPN application.
connectButton.text = "Disconnect" // Otherwise, it returns an Intent to a system activity.
isYggStarted = true val vpnIntent = VpnService.prepare(this)
if (vpnIntent != null) startActivityForResult(
vpnIntent,
0x0F
)
else onActivityResult(0x0F, Activity.RESULT_OK, null)
} else { } else {
// FIXME fix this shit, this code doesn't stop service for some reasons
val intent = Intent(this, YggdrasilTunService::class.java) val intent = Intent(this, YggdrasilTunService::class.java)
stopService(intent) stopService(intent)
connectButton.text = "Connect" connectButton.text = "Connect"
@ -29,4 +37,15 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 0x0F && resultCode == Activity.RESULT_OK) {
val intent = Intent(this, YggdrasilTunService::class.java)
startService(intent)
val connectButton = findViewById<Button>(R.id.connect_button)
connectButton.text = "Disconnect"
isYggStarted = true
}
}
} }

View File

@ -13,39 +13,6 @@ import java.lang.Runtime.getRuntime
val gson = Gson() val gson = Gson()
fun Context.execYgg(cmd: String) = getRuntime().exec(
"${yggBin.absolutePath} $cmd"
)
val Context.yggBin get() = File(filesDir, "yggdrasil-$YGGDRASIL_VERSION-linux-${CPU_ABI.let {
when {
it.contains("v7") -> "armhf"
it.contains("v8") -> "arm64"
else -> throw Exception("Unsupported ABI")
}
}
}")
@Throws(RuntimeException::class)
fun Context.getYggConfig(): Config {
val configFile = File(filesDir, "yggdrasil.conf")
if(!configFile.exists()) {
generateYggConfig()
}
val configStr = configFile.readText()
val configHjsonObject = JsonValue.readHjson(configStr)
return gson.fromJson(configHjsonObject.toString(Stringify.PLAIN), Config::class.java)
}
fun Context.generateYggConfig() {
val process = execYgg("-genconf > ${filesDir.absolutePath}/yggdrasil.conf")
process.waitFor()
val configBytes = process.inputStream.readBytes()
val configStr = String(configBytes)
val configFile = File(filesDir, "yggdrasil.conf")
configFile.writeText(configStr)
}
fun createNativeYggConfig(config: Config): NodeConfig { fun createNativeYggConfig(config: Config): NodeConfig {
val nativeConfig = NodeConfig() val nativeConfig = NodeConfig()
nativeConfig.adminListen = config.adminListen nativeConfig.adminListen = config.adminListen
@ -64,36 +31,4 @@ fun Context.saveYggConfig(config: Config) {
val configJson = gson.toJson(config) val configJson = gson.toJson(config)
val configFile = File(filesDir, "yggdrasil.conf") val configFile = File(filesDir, "yggdrasil.conf")
configFile.writeText(configJson) configFile.writeText(configJson)
}
fun Context.installBinary() {
val type = "yggdrasil-$YGGDRASIL_VERSION-linux-${CPU_ABI.let {
when{
it.contains("v8") -> "arm64"
it.contains("v7") -> "armhf"
else -> throw Exception("Unsupported ABI")
}
}}"
yggBin.apply {
delete()
createNewFile()
}
val input = assets.open(type)
val output = yggBin.outputStream()
try {
input.copyTo(output)
} finally {
input.close(); output.close()
}
yggBin.setExecutable(true)
val yggConfig = getYggConfig() // it generates config automatically
yggConfig.ifName = "tun0"
saveYggConfig(yggConfig)
Log.i("Utils", "# Binary installed successfully")
} }

View File

@ -1,44 +0,0 @@
package io.github.chronosx88.yggdrasil
import android.app.Service
import android.content.Intent
import kotlin.system.exitProcess
class YggdrasilService : Service() {
private val LOG_TAG: String = YggdrasilService::class.java.simpleName
override fun onBind(intent: Intent) = null
companion object {
var daemon: Process? = null
}
private fun start() {
val process = execYgg("-useconffile ${filesDir.absolutePath}/yggdrasil.conf")
process.waitFor()
daemon = process
}
private fun stop() {
daemon?.destroy()
daemon = null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
when (intent?.action) {
"start" -> start()
"stop" -> stop()
"restart" -> {
stop(); start()
}
"exit" -> exitProcess(0)
}
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
stop()
}
}

View File

@ -5,7 +5,18 @@ import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import com.google.gson.Gson import com.google.gson.Gson
import dummy.ConduitEndpoint
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import mobile.Mobile
import mobile.Yggdrasil import mobile.Yggdrasil
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import kotlin.coroutines.CoroutineContext
import kotlin.experimental.or
class YggdrasilTunService : VpnService() { class YggdrasilTunService : VpnService() {
@ -13,6 +24,11 @@ class YggdrasilTunService : VpnService() {
private var isRunning: Boolean = false private var isRunning: Boolean = false
} }
private var tunInterface: ParcelFileDescriptor? = null private var tunInterface: ParcelFileDescriptor? = null
private lateinit var yggConduitEndpoint: ConduitEndpoint
private var tunInputStream: InputStream? = null
private var tunOutputStream: OutputStream? = null
private lateinit var readCoroutine: CoroutineContext
private lateinit var writeCoroutine: CoroutineContext
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
@ -26,41 +42,85 @@ class YggdrasilTunService : VpnService() {
} }
private fun setupTunInterface() { private fun setupTunInterface() {
if(!yggBin.exists()) {
installBinary()
}
val builder = Builder() val builder = Builder()
val ygg = Yggdrasil()
var configJson = Mobile.generateConfigJSON()
val gson = Gson()
val yggTmp = Yggdrasil() var config = gson.fromJson(String(configJson), Map::class.java).toMutableMap()
val tempYggConfig = getYggConfig() config = fixConfig(config)
tempYggConfig.ifName = "none" configJson = gson.toJson(config).toByteArray()
yggTmp.startJSON(Gson().toJson(tempYggConfig).toByteArray())
val address = yggTmp.addressString // hack for getting generic ipv6 string from NodeID yggConduitEndpoint = ygg.startJSON(configJson)
val address = ygg.addressString // hack for getting generic ipv6 string from NodeID
tunInterface = builder tunInterface = builder
.addAddress(address, 7) .addAddress(address, 7)
.addRoute("0200::", 7) .addRoute("0200::", 7)
.establish() .establish()
createYggdrasilService() tunInputStream = FileInputStream(tunInterface!!.fileDescriptor)
tunOutputStream = FileOutputStream(tunInterface!!.fileDescriptor)
readCoroutine = GlobalScope.launch {
// FIXME it will throw exception (bad file descriptor) when coroutine will be canceled
while (true) {
readPacketsFromTun()
}
}
writeCoroutine = GlobalScope.launch {
while (true) {
writePacketsToTun()
}
}
} }
private fun createYggdrasilService() { private fun fixConfig(config: MutableMap<Any?, Any?>): MutableMap<Any?, Any?> {
val intent = Intent(this, YggdrasilService::class.java) config["Listen"] = ""
intent.action = "start" config["AdminListen"] = "tcp://localhost:9001"
startService(intent) config["IfName"] = "dummy"
(config["SessionFirewall"] as MutableMap<Any, Any>)["Enable"] = true
(config["SwitchOptions"] as MutableMap<Any, Any>)["MaxTotalQueueSize"] = 1048576
if (config["AutoStart"] == null) {
val tmpMap = emptyMap<String, Boolean>().toMutableMap()
tmpMap["WiFi"] = false
tmpMap["Mobile"] = false
config["AutoStart"] = tmpMap
}
return config
} }
private fun stopYggdrasilService() { private fun readPacketsFromTun() {
val intent = Intent(this, YggdrasilService::class.java) if(tunInputStream != null) {
stopService(intent) val buffer = ByteArray(1024)
tunInputStream!!.read(buffer)
if (!isBufferEmpty(buffer)) {
yggConduitEndpoint.send(buffer)
}
}
}
private fun isBufferEmpty(buffer: ByteArray): Boolean {
var sum: Byte = 0
for (i in buffer) {
sum.or(i)
}
return sum == 0.toByte()
}
private fun writePacketsToTun() {
if(tunOutputStream != null) {
val buffer = yggConduitEndpoint.recv()
if (!isBufferEmpty(buffer)) {
tunOutputStream!!.write(buffer)
}
}
} }
override fun onRevoke() { override fun onRevoke() {
super.onRevoke() super.onRevoke()
isRunning = false isRunning = false
stopYggdrasilService() readCoroutine.cancel()
writeCoroutine.cancel()
tunInterface!!.close() tunInterface!!.close()
tunInterface = null tunInterface = null
} }

View File

@ -2,6 +2,7 @@ package io.github.chronosx88.yggdrasil.models.config
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
// FIXME This is old config scheme
data class Config ( data class Config (
@SerializedName("peers") var peers : List<String>, @SerializedName("peers") var peers : List<String>,
@SerializedName("interfacePeers") var interfacePeers : Map<String, List<String>>, @SerializedName("interfacePeers") var interfacePeers : Map<String, List<String>>,