Initial commit

This commit is contained in:
Neil Alexander 2021-06-16 19:21:09 +01:00
commit 20ff7378e9
52 changed files with 2155 additions and 0 deletions

View file

@ -0,0 +1,47 @@
package eu.neilalexander.yggdrasil
import android.content.Context
import mobile.Mobile
import org.json.JSONObject
import java.io.File
object ConfigurationProxy {
private lateinit var json: JSONObject
private lateinit var file: File
operator fun invoke(applicationContext: Context): ConfigurationProxy {
file = File(applicationContext.filesDir, "yggdrasil.conf")
if (!file.exists()) {
val conf = Mobile.generateConfigJSON()
if (file.createNewFile()) {
file.writeBytes(conf)
}
}
fix()
return this
}
fun updateJSON(fn: (JSONObject) -> Unit) {
json = JSONObject(file.readText(Charsets.UTF_8))
fn(json)
val str = json.toString()
file.writeText(str, Charsets.UTF_8)
}
private fun fix() {
updateJSON { json ->
json.put("AdminListen", "none")
json.put("IfName", "none")
json.put("IfMTU", 65535)
}
}
fun getJSON(): JSONObject {
fix()
return json
}
fun getJSONByteArray(): ByteArray {
return json.toString().toByteArray(Charsets.UTF_8)
}
}

View file

@ -0,0 +1,25 @@
package eu.neilalexander.yggdrasil
import android.app.Application
import android.content.IntentFilter
import androidx.localbroadcastmanager.content.LocalBroadcastManager
class GlobalApplication: Application() {
private var state = PacketTunnelState
private lateinit var config: ConfigurationProxy
override fun onCreate() {
super.onCreate()
config = ConfigurationProxy(applicationContext)
LocalBroadcastManager.getInstance(this).registerReceiver(
state, IntentFilter(PacketTunnelProvider.RECEIVER_INTENT)
)
}
override fun onTerminate() {
super.onTerminate()
LocalBroadcastManager.getInstance(this).unregisterReceiver(state)
}
}

View file

@ -0,0 +1,132 @@
package eu.neilalexander.yggdrasil
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Color
import android.net.VpnService
import android.os.Bundle
import android.widget.Switch
import android.widget.TableRow
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import mobile.Mobile
import org.json.JSONArray
class MainActivity : AppCompatActivity() {
private var state = PacketTunnelState
private lateinit var enabledSwitch: Switch
private lateinit var enabledLabel: TextView
private lateinit var ipAddressLabel: TextView
private lateinit var subnetLabel: TextView
private lateinit var coordinatesLabel: TextView
private lateinit var peersLabel: TextView
private lateinit var peersRow: TableRow
private lateinit var settingsRow: TableRow
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.versionValue).text = Mobile.getVersion()
enabledSwitch = findViewById(R.id.enableMulticastSwitch)
enabledLabel = findViewById(R.id.yggdrasilStatusLabel)
ipAddressLabel = findViewById(R.id.ipAddressValue)
subnetLabel = findViewById(R.id.subnetValue)
coordinatesLabel = findViewById(R.id.coordinatesValue)
peersLabel = findViewById(R.id.peersValue)
peersRow = findViewById(R.id.peersTableRow)
settingsRow = findViewById(R.id.settingsTableRow)
enabledLabel.setTextColor(Color.GRAY)
VpnService.prepare(this)
enabledSwitch.setOnCheckedChangeListener { _, isChecked ->
when (isChecked) {
true -> {
val vpnintent = VpnService.prepare(this)
if (vpnintent != null) {
startActivityForResult(vpnintent, 0)
} else {
onActivityResult(0, RESULT_OK, vpnintent)
}
}
false -> {
val intent = Intent(this, PacketTunnelProvider::class.java)
intent.action = PacketTunnelProvider.ACTION_STOP
startService(intent)
}
}
}
peersRow.isClickable = true
peersRow.setOnClickListener {
val intent = Intent(this, PeersActivity::class.java)
startActivity(intent)
}
settingsRow.isClickable = true
settingsRow.setOnClickListener {
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
}
}
override fun onResume() {
super.onResume()
LocalBroadcastManager.getInstance(this).registerReceiver(
receiver, IntentFilter(PacketTunnelState.RECEIVER_INTENT)
)
}
private val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
when (intent.getStringExtra("type")) {
"state" -> {
enabledLabel.text = if (intent.getBooleanExtra("started", false)) {
if (state.dhtCount() == 0) {
enabledLabel.setTextColor(Color.RED)
"No connectivity"
} else {
enabledLabel.setTextColor(Color.GREEN)
"Enabled"
}
} else {
enabledLabel.setTextColor(Color.GRAY)
"Not enabled"
}
ipAddressLabel.text = intent.getStringExtra("ip") ?: "N/A"
subnetLabel.text = intent.getStringExtra("subnet") ?: "N/A"
coordinatesLabel.text = intent.getStringExtra("coords") ?: "[]"
peersLabel.text = when (val count = state.peerCount()) {
0 -> "No peers"
1 -> "1 peer"
else -> "$count peers"
}
}
}
}
}
override fun onPause() {
super.onPause()
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (resultCode) {
RESULT_OK -> {
val intent = Intent(this, PacketTunnelProvider::class.java)
intent.action = PacketTunnelProvider.ACTION_START
startService(intent)
}
}
}
}

View file

@ -0,0 +1,181 @@
package eu.neilalexander.yggdrasil
import android.content.Intent
import android.net.Uri
import android.net.VpnService
import android.os.Handler
import android.os.Message
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.localbroadcastmanager.content.LocalBroadcastManager
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
class PacketTunnelProvider: VpnService() {
companion object {
const val RECEIVER_INTENT = "eu.neilalexander.yggdrasil.PacketTunnelProvider.MESSAGE"
const val ACTION_START = "eu.neilalexander.yggdrasil.PacketTunnelProvider.START"
const val ACTION_STOP = "eu.neilalexander.yggdrasil.PacketTunnelProvider.STOP"
}
private var yggdrasil = Yggdrasil()
private var started = AtomicBoolean()
private lateinit var config: ConfigurationProxy
private lateinit var parcel: ParcelFileDescriptor
private lateinit var readerThread: Thread
private lateinit var writerThread: Thread
private lateinit var updateThread: Thread
private lateinit var readerStream: FileInputStream
private lateinit var writerStream: FileOutputStream
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) {
return START_NOT_STICKY
}
return when (intent.action ?: ACTION_STOP) {
ACTION_START -> {
start(); START_STICKY
}
ACTION_STOP -> {
stop(); START_NOT_STICKY
}
else -> {
stop(); START_NOT_STICKY
}
}
}
private fun start() {
if (!started.compareAndSet(false, true)) {
return
}
Log.d("PacketTunnelProvider", config.getJSON().toString())
yggdrasil.startJSON(config.getJSONByteArray())
val address = yggdrasil.addressString
var builder = Builder()
.addAddress(address, 7)
.addRoute("200::", 7)
.setBlocking(true)
.setMtu(yggdrasil.mtu.toInt())
.setSession("Yggdrasil")
parcel = builder.establish()
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()
}
val intent = Intent(RECEIVER_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", JSONArray(yggdrasil.peersJSON).length())
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
private fun stop() {
if (!started.compareAndSet(true, false)) {
return
}
if (readerThread != null) {
readerStream.close()
readerThread.interrupt()
}
if (writerThread != null) {
writerStream.close()
writerThread.interrupt()
}
if (updateThread != null) {
updateThread.interrupt()
}
parcel.close()
yggdrasil.stop()
stopSelf()
val intent = Intent(RECEIVER_INTENT)
intent.putExtra("type", "state")
intent.putExtra("started", false)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
private fun updater() {
updates@ while (!updateThread.isInterrupted) {
val intent = Intent(RECEIVER_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)
try {
Thread.sleep(2000)
} catch (e: java.lang.InterruptedException) {
return
}
}
}
private fun writer() {
writes@ while (!writerThread.isInterrupted && writerStream.fd.valid()) {
try {
val b = yggdrasil.recv()
writerStream.write(b)
} catch (e: Exception) {
break@writes
}
}
stop()
}
private fun reader() {
var b = ByteArray(65535)
reads@ while (!readerThread.isInterrupted && readerStream.fd.valid()) {
try {
val n = readerStream.read(b)
yggdrasil.send(b.sliceArray(0..n))
} catch (e: Exception) {
break@reads
}
}
stop()
}
}

View file

@ -0,0 +1,53 @@
package eu.neilalexander.yggdrasil
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import org.json.JSONArray
object PacketTunnelState: BroadcastReceiver() {
var dhtState: JSONArray? = null
private set
var peersState: JSONArray? = null
private set
const val RECEIVER_INTENT = "eu.neilalexander.yggdrasil.PacketTunnelState.MESSAGE"
fun peerCount(): Int {
if (peersState == null) {
return 0
}
return peersState!!.length()
}
fun dhtCount(): Int {
if (dhtState == null) {
return 0
}
return dhtState!!.length()
}
override fun onReceive(context: Context?, intent: Intent) {
when (intent.getStringExtra("type")) {
"state" -> {
var dht = intent.getStringExtra("dht")
var peers = intent.getStringExtra("peers")
if (dht == null || dht == "null") {
dht = "[]"
}
if (peers == null || peers == "null") {
peers = "[]"
}
peersState = JSONArray(peers)
dhtState = JSONArray(dht)
intent.action = RECEIVER_INTENT
LocalBroadcastManager.getInstance(context!!).sendBroadcast(intent)
}
}
}
}

View file

@ -0,0 +1,158 @@
package eu.neilalexander.yggdrasil
import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.Layout
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import com.google.android.material.textfield.TextInputEditText
import org.json.JSONArray
class PeersActivity : AppCompatActivity() {
private var state = PacketTunnelState
private lateinit var config: ConfigurationProxy
private lateinit var inflater: LayoutInflater
private lateinit var connectedTableLayout: TableLayout
private lateinit var connectedTableLabel: TextView
private lateinit var configuredTableLayout: TableLayout
private lateinit var configuredTableLabel: TextView
private lateinit var multicastSwitch: Switch
private lateinit var addPeerButton: ImageButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_peers)
config = ConfigurationProxy(applicationContext)
inflater = LayoutInflater.from(this)
connectedTableLayout = findViewById(R.id.connectedPeersTableLayout)
connectedTableLabel = findViewById(R.id.connectedPeersLabel)
configuredTableLayout = findViewById(R.id.configuredPeersTableLayout)
configuredTableLabel = findViewById(R.id.configuredPeersLabel)
multicastSwitch = findViewById(R.id.enableMulticastSwitch)
multicastSwitch.setOnCheckedChangeListener { button, _ ->
when (button.isChecked) {
true -> {
config.updateJSON { json ->
json.put("MulticastInterfaces", JSONArray("[\"lo\", \".*\"]"))
}
}
false -> {
config.updateJSON { json ->
json.put("MulticastInterfaces", JSONArray("[\"lo\"]"))
}
}
}
}
var multicastInterfaceFound = false
val multicastInterfaces = config.getJSON().getJSONArray("MulticastInterfaces")
(0 until multicastInterfaces.length()).forEach {
if (multicastInterfaces[it] == ".*") {
multicastInterfaceFound = true
}
}
multicastSwitch.isChecked = multicastInterfaceFound
addPeerButton = findViewById(R.id.addPeerButton)
addPeerButton.setOnClickListener {
var view = inflater.inflate(R.layout.dialog_addpeer, null)
var input = view.findViewById<TextInputEditText>(R.id.addPeerInput)
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setTitle("Add Configured Peer")
builder.setView(view)
builder.setPositiveButton("Add") { dialog, _ ->
config.updateJSON { json ->
json.getJSONArray("Peers").put(input.text)
}
dialog.dismiss()
updateConfiguredPeers()
}
builder.setNegativeButton("Cancel") { dialog, _ ->
dialog.cancel()
}
builder.show()
}
}
override fun onResume() {
super.onResume()
updateConfiguredPeers()
updateConnectedPeers()
}
private fun updateConfiguredPeers() {
val peers = config.getJSON().getJSONArray("Peers")
when (peers.length()) {
0 -> {
configuredTableLayout.visibility = View.GONE
configuredTableLabel.text = "No peers currently configured"
}
else -> {
configuredTableLayout.visibility = View.VISIBLE
configuredTableLabel.text = "Configured Peers"
configuredTableLayout.removeAllViewsInLayout()
for (i in 0 until peers.length()) {
val peer = peers[i].toString()
var view = inflater.inflate(R.layout.peers_configured, null)
view.findViewById<TextView>(R.id.addressValue).text = peer
view.findViewById<ImageButton>(R.id.deletePeerButton).tag = i
view.findViewById<ImageButton>(R.id.deletePeerButton).setOnClickListener { button ->
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setTitle("Remove ${peer}?")
builder.setPositiveButton("Remove") { dialog, _ ->
config.updateJSON { json ->
json.getJSONArray("Peers").remove(button.tag as Int)
}
dialog.dismiss()
updateConfiguredPeers()
}
builder.setNegativeButton("Cancel") { dialog, _ ->
dialog.cancel()
}
builder.show()
}
configuredTableLayout.addView(view)
}
}
}
}
private fun updateConnectedPeers() {
val peers = state.peersState ?: JSONArray("[]")
when (peers.length()) {
0 -> {
connectedTableLayout.visibility = View.GONE
connectedTableLabel.text = "No peers currently connected"
}
else -> {
connectedTableLayout.visibility = View.VISIBLE
connectedTableLabel.text = "Connected Peers"
connectedTableLayout.removeAllViewsInLayout()
for (i in 0 until peers.length()) {
val peer = peers.getJSONObject(i)
var view = inflater.inflate(R.layout.peers_connected, null)
val ip = peer.getString("IP")
view.findViewById<TextView>(R.id.addressLabel).text = ip
view.findViewById<TextView>(R.id.detailsLabel).text = peer.getString("Remote")
connectedTableLayout.addView(view)
}
}
}
}
}

View file

@ -0,0 +1,11 @@
package eu.neilalexander.yggdrasil
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
}
}