diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..7643783
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..1570329
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..90701f9
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index e1f20f9..d1da393 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,18 +4,19 @@
-
-
+
+
+
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 8978d23..f682ab2 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,8 @@
+
+
+
+
diff --git a/build.gradle b/build.gradle
index c4186bc..8ba3fb1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,6 +4,7 @@ plugins {
id 'com.android.library' version '8.1.1' apply false
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
id 'org.jetbrains.kotlin.plugin.serialization' version "1.9.0" apply false
+ id 'org.jetbrains.kotlin.jvm' version '1.9.0' apply false
}
ext.buildConfigProperties = new Properties()
@@ -16,7 +17,6 @@ group 'com.github.UstadMobile.Meshrabiya'
version '0.1d11-snapshot'
ext {
-
version_kotlin_mockito = "4.1.0"
version_android_mockito = "5.1.1"
version_turbine = "0.12.1"
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..5ce36f5
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,90 @@
+[versions]
+accompanistWebview = "0.34.0"
+acraHttp = "5.11.3"
+activityCompose = "1.8.2"
+androidx_core = "1.12.0"
+appcompat = "1.6.1"
+codeScanner = "2.3.2"
+coreKtx = "1.12.0"
+ipaddress = "5.4.0"
+junitVersion = "1.1.5"
+kodeinDiFrameworkAndroidX = "7.21.2"
+kotlinxCoroutinesTest = "1.7.3"
+lifecycleRuntimeKtx = "2.7.0"
+material = "1.12.0"
+mockitoCore = "5.1.0"
+mockitoCoreVersion = "5.1.1"
+nanohttpd = "2.3.1"
+navigationCompose = "2.7.7"
+okhttp = "4.10.0"
+okhttpVersion = "4.12.0"
+rawhttp = "2.6.0"
+kotlinx_serialization = "1.6.1"
+datastore = "1.0.0"
+bouncycastle = "1.75"
+android_desugaring = "2.0.4"
+junit = "4.13.2"
+kotlin_mockito = "5.1.0"
+turbine = "1.0.0"
+mockwebserver = "4.12.0"
+android_junit_runner = "1.5.2"
+androidx_test_rules = "1.5.0"
+android_test_ext_junit = "1.1.5"
+espresso_core = "3.5.1"
+play_services_nearby = "19.3.0"
+zxingAndroidEmbedded = "4.3.0"
+
+
+[libraries]
+accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanistWebview" }
+acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acraHttp" }
+acra-http = { module = "ch.acra:acra-http", version.ref = "acraHttp" }
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
+androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
+androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "androidx_core" }
+androidx-core-ktx-v1131 = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
+androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" }
+androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
+androidx-material3 = { module = "androidx.compose.material3:material3" }
+androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+androidx-ui = { module = "androidx.compose.ui:ui" }
+androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
+androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
+androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
+androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
+androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
+code-scanner = { module = "com.github.yuriy-budiyev:code-scanner", version.ref = "codeScanner" }
+core = { module = "androidx.core:core", version.ref = "coreKtx" }
+google-material = { module = "com.google.android.material:material", version.ref = "material" }
+ipaddress = { module = "com.github.seancfoley:ipaddress", version.ref = "ipaddress" }
+kodein-di-framework-android-x = { module = "org.kodein.di:kodein-di-framework-android-x", version.ref = "kodeinDiFrameworkAndroidX" }
+kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
+material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
+material3 = { module = "androidx.compose.material3:material3" }
+mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCoreVersion" }
+nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
+okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
+rawhttp = { group = "com.athaydes.rawhttp", name = "rawhttp-core", version.ref = "rawhttp" }
+kotlinx-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx_serialization" }
+datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
+bouncycastle-prov = { group = "org.bouncycastle", name = "bcprov-jdk18on", version.ref = "bouncycastle" }
+bouncycastle-pkix = { group = "org.bouncycastle", name = "bcpkix-jdk18on", version.ref = "bouncycastle" }
+desugaring = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "android_desugaring" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+mockito-kotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version.ref = "kotlin_mockito" }
+turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }
+mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "mockwebserver" }
+android-junit-runner = { group = "androidx.test", name = "runner", version.ref = "android_junit_runner" }
+androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidx_test_rules" }
+android-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "android_test_ext_junit" }
+mockito-android = { group = "org.mockito", name = "mockito-android", version.ref = "mockitoCore" }
+espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso_core" }
+google-play-services-nearby = { group = "com.google.android.gms", name = "play-services-nearby", version.ref = "play_services_nearby" }
+ui = { module = "androidx.compose.ui:ui" }
+ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
+ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
+ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
+ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
+ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
+zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
diff --git a/lib-meshrabiya-vpn/.gitignore b/lib-meshrabiya-vpn/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/lib-meshrabiya-vpn/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/lib-meshrabiya-vpn/build.gradle.kts b/lib-meshrabiya-vpn/build.gradle.kts
new file mode 100644
index 0000000..1e52364
--- /dev/null
+++ b/lib-meshrabiya-vpn/build.gradle.kts
@@ -0,0 +1,43 @@
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "com.meshrabiya.lib_vpn"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 26
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.google.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.espresso.core)
+}
\ No newline at end of file
diff --git a/lib-meshrabiya-vpn/consumer-rules.pro b/lib-meshrabiya-vpn/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/lib-meshrabiya-vpn/proguard-rules.pro b/lib-meshrabiya-vpn/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/lib-meshrabiya-vpn/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/lib-meshrabiya-vpn/src/androidTest/java/com/meshrabiya/lib_vpn/ExampleInstrumentedTest.kt b/lib-meshrabiya-vpn/src/androidTest/java/com/meshrabiya/lib_vpn/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..1da64ad
--- /dev/null
+++ b/lib-meshrabiya-vpn/src/androidTest/java/com/meshrabiya/lib_vpn/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.meshrabiya.lib_vpn
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.meshrabiya.lib_vpn.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/lib-meshrabiya-vpn/src/main/AndroidManifest.xml b/lib-meshrabiya-vpn/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/lib-meshrabiya-vpn/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/lib-meshrabiya-vpn/src/main/java/com/meshrabiya/lib_vpn/MeshrabiyaVpnService.kt b/lib-meshrabiya-vpn/src/main/java/com/meshrabiya/lib_vpn/MeshrabiyaVpnService.kt
new file mode 100644
index 0000000..5fa3d1c
--- /dev/null
+++ b/lib-meshrabiya-vpn/src/main/java/com/meshrabiya/lib_vpn/MeshrabiyaVpnService.kt
@@ -0,0 +1,364 @@
+package com.meshrabiya.lib_vpn
+
+
+import android.content.Intent
+import android.net.VpnService
+import android.os.ParcelFileDescriptor
+import android.system.OsConstants
+import android.util.Log
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.net.InetAddress
+import java.nio.ByteBuffer
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class MeshrabiyaVpnService : VpnService() {
+ private var vpnInterface: ParcelFileDescriptor? = null
+ private lateinit var fileInputStream: FileInputStream
+ private lateinit var fileOutputStream: FileOutputStream
+ private val builder = Builder()
+ private val executorService: ExecutorService = Executors.newSingleThreadExecutor()
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ logMessage("VPN Service started")
+ setupVpn()
+ return START_STICKY
+ }
+
+ /**
+ * Configures the VPN connection with address, routes, DNS servers, and session settings.
+ * Establishes the VPN interface and sets up input and output streams.
+ */
+ private fun setupVpn() {
+ try {
+ builder.apply {
+ addAddress(VPN_ADDRESS, VPN_ADDRESS_PREFIX_LENGTH)
+ addRoute(ROUTE_ADDRESS, ROUTE_PREFIX_LENGTH)
+ addDnsServer(PRIMARY_DNS)
+ addDnsServer(SECONDARY_DNS)
+ setSession(VPN_SESSION_NAME)
+ setMtu(MTU_VALUE)
+ allowFamily(OsConstants.AF_INET)
+ allowFamily(OsConstants.AF_INET6)
+ }
+
+ vpnInterface = builder.establish()
+
+ if (vpnInterface == null) {
+ logMessage("Failed to establish VPN interface: vpnInterface is null")
+ throw IllegalStateException("Failed to establish VPN interface.")
+ }
+
+ fileInputStream = FileInputStream(vpnInterface!!.fileDescriptor)
+ fileOutputStream = FileOutputStream(vpnInterface!!.fileDescriptor)
+ logMessage("VPN interface established successfully")
+ startHandlingTraffic()
+
+ } catch (e: Exception) {
+ logMessage("Error setting up VPN: ${e.message ?: "Unknown error"}")
+ }
+ }
+
+ /**
+ * Starts a new thread to handle traffic.
+ */
+ private fun startHandlingTraffic() {
+ executorService.submit { handleTraffic() }
+ }
+
+ /**
+ * Continuously reads and processes traffic from the VPN interface.
+ */
+ private fun handleTraffic() {
+ val buffer = ByteBuffer.allocate(BUFFER_SIZE)
+ while (true) {
+ try {
+ buffer.clear()
+ val length = readFromVpnInterface(buffer)
+ if (length > 0) {
+ processPacket(buffer, length)
+ }
+ } catch (e: IOException) {
+ logMessage("Error handling traffic: ${e.message}")
+ break
+ }
+ }
+ }
+
+ /**
+ * Reads data from the VPN interface into the provided buffer.
+ * @return the number of bytes read or -1 in case of an error.
+ */
+ private fun readFromVpnInterface(buffer: ByteBuffer): Int {
+ return try {
+ fileInputStream.channel.read(buffer)
+ } catch (e: IOException) {
+ logMessage("Error reading from VPN interface: ${e.message}")
+ -1
+ }
+ }
+
+ /**
+ * Processes a packet by determining its IP version and handling accordingly.
+ * @param buffer the buffer containing the packet data.
+ * @param length the length of the packet data.
+ */
+ private fun processPacket(buffer: ByteBuffer, length: Int) {
+ // The `flip()` method gets the buffer ready to read data.
+ // When we write to the buffer, it moves a pointer forward.
+ // `flip()` changes the buffer from writing mode to reading mode.
+ // It sets the end (limit) to where we stopped writing and resets the start (position) to zero,
+ // so we can read the data from the beginning.
+ buffer.flip()
+
+ // Check the first byte of the packet to figure out if it's an IPv4 or IPv6 packet.
+ // The first few bits of the first byte tell us the IP version (4 or 6).
+ when (val ipVersion = (buffer.get(0).toInt() ushr 4).toByte()) {
+ IPV4_VERSION -> handleIPv4Packet(buffer, length)
+ IPV6_VERSION -> handleIPv6Packet(buffer, length)
+ else -> logMessage("Unsupported IP version: $ipVersion")
+ }
+ }
+
+ /**
+ * Handles an IPv4 packet by extracting the destination IP and port, and processing it based on the network type.
+ * @param buffer the buffer containing the IPv4 packet data.
+ * @param length the length of the packet data.
+ */
+ private fun handleIPv4Packet(buffer: ByteBuffer, length: Int) {
+ val destinationIp = buildIPv4Address(buffer)
+ val header = parseIPv4Header(buffer)
+
+ logMessage("IPv4 Packet - Destination: ${header.destinationIp.hostAddress}:${header.destinationPort}, Protocol: ${header.protocolName}")
+
+ if (isVirtualNetwork(destinationIp)) {
+ handleVirtualNetworkPacket(buffer, length)
+ } else {
+ writeToVpnInterface(buffer)
+ }
+ }
+
+ /**
+ * Builds an IPv4 address from the provided buffer.
+ * @param buffer the buffer containing the packet data.
+ * @return the constructed IPv4 address.
+ */
+ private fun buildIPv4Address(buffer: ByteBuffer): InetAddress {
+ return InetAddress.getByAddress(ByteArray(4).also { buffer.get(it, 0, 4) })
+ }
+
+ /**
+ * Parses the IPv4 header to extract important information such as destination port, origin port, and protocol.
+ * Returns an IpHeader data class containing all relevant information.
+ */
+ private fun parseIPv4Header(buffer: ByteBuffer): IpHeader {
+ try {
+ val protocol = buffer.get(IPV4_PROTOCOL_OFFSET).toInt()
+ val isTcp = protocol == IPPROTO_TCP
+ val isUdp = protocol == IPPROTO_UDP
+
+ val ipHeaderLength = (buffer.get(IPV4_HEADER_LENGTH_OFFSET).toInt() and 0x0F) * 4
+ val destinationPort = if (isTcp || isUdp) {
+ ((buffer.get(ipHeaderLength + 2).toInt() and 0xFF) shl 8) or
+ (buffer.get(ipHeaderLength + 3).toInt() and 0xFF)
+ } else {
+ -1
+ }
+
+ val protocolName = when {
+ isTcp -> Protocol.TCP
+ isUdp -> Protocol.UDP
+ else -> Protocol.UNKNOWN
+ }
+
+ return IpHeader(
+ destinationIp = buildIPv4Address(buffer),
+ destinationPort = destinationPort,
+
+ // NOTE : ----> Placeholder, real origin logic needed
+ originIp = InetAddress.getByName("0.0.0.0"),
+ // Placeholder
+ originPort = -1,
+
+ protocolName = protocolName
+ )
+ } catch (e: Exception) {
+ logMessage("Error parsing IPv4 header: ${e.message}")
+ return IpHeader(InetAddress.getByName("0.0.0.0"), -1, InetAddress.getByName("0.0.0.0"), -1, Protocol.ERROR)
+ }
+ }
+
+ /**
+ * Handles an IPv6 packet by extracting the destination IP and port, and processing it based on the network type.
+ * @param buffer the buffer containing the IPv6 packet data.
+ * @param length the length of the packet data.
+ */
+ private fun handleIPv6Packet(buffer: ByteBuffer, length: Int) {
+ val destinationIp = buildIPv6Address(buffer)
+ val header = parseIPv6Header(buffer)
+
+ logMessage("IPv6 Packet - Destination: [${header.destinationIp.hostAddress}]:${header.destinationPort}, Protocol: ${header.protocolName}")
+
+ if (isVirtualNetwork(destinationIp)) {
+ handleVirtualNetworkPacket(buffer, length)
+ } else {
+ writeToVpnInterface(buffer)
+ }
+ }
+
+ /**
+ * Builds an IPv6 address from the provided buffer.
+ * @param buffer the buffer containing the packet data.
+ * @return the constructed IPv6 address.
+ */
+ private fun buildIPv6Address(buffer: ByteBuffer): InetAddress {
+ return InetAddress.getByAddress(ByteArray(16).also { buffer.get(it, 0, 16) })
+ }
+
+ /**
+ * Parses the IPv6 header to extract important information such as destination port, origin port, and protocol.
+ * Returns an IpHeader data class containing all relevant information.
+ */
+ private fun parseIPv6Header(buffer: ByteBuffer): IpHeader {
+ try {
+ val protocol = buffer.get(IPV6_PROTOCOL_OFFSET).toInt()
+ val isTcp = protocol == IPPROTO_TCP
+ val isUdp = protocol == IPPROTO_UDP
+
+ val destinationPort = if (isTcp || isUdp) {
+ ((buffer.get(IPV6_PORT_OFFSET).toInt() and 0xFF) shl 8) or
+ (buffer.get(IPV6_PORT_OFFSET + 1).toInt() and 0xFF)
+ } else {
+ -1
+ }
+
+ val protocolName = when {
+ isTcp -> Protocol.TCP
+ isUdp -> Protocol.UDP
+ else -> Protocol.UNKNOWN
+ }
+
+ return IpHeader(
+ destinationIp = buildIPv6Address(buffer),
+ destinationPort = destinationPort,
+
+ // NOTE : ----> Placeholder, real origin logic needed
+ originIp = InetAddress.getByName("::"),
+ // Placeholder
+ originPort = -1,
+ protocolName = protocolName
+ )
+ } catch (e: Exception) {
+ logMessage("Error parsing IPv6 header: ${e.message}")
+ return IpHeader(InetAddress.getByName("::"), -1, InetAddress.getByName("::"), -1, Protocol.ERROR)
+ }
+ }
+
+ /**
+ * Checks if the destination IP is part of the virtual network.
+ * @param destinationIp the destination IP address.
+ * @return true if the IP is part of the virtual network, false otherwise.
+ */
+ private fun isVirtualNetwork(destinationIp: InetAddress): Boolean {
+ return destinationIp.isSiteLocalAddress
+ }
+
+ /**
+ * Handles packets that are destined for the virtual network.
+ * @param buffer the buffer containing the packet data.
+ * @param length the length of the packet data.
+ */
+ private fun handleVirtualNetworkPacket(buffer: ByteBuffer, length: Int) {
+ logMessage("Handling packet for virtual network ")
+ }
+
+ /**
+ * Writes the provided buffer data to the VPN interface.
+ * @param buffer the buffer containing the packet data.
+ */
+ private fun writeToVpnInterface(buffer: ByteBuffer) {
+ try {
+ fileOutputStream.channel.write(buffer)
+ } catch (e: IOException) {
+ logMessage("Error writing to VPN interface: ${e.message}")
+ }
+ }
+
+ override fun onRevoke() {
+ logMessage("VPN Service revoked")
+ try {
+ fileInputStream.close()
+ fileOutputStream.close()
+ vpnInterface?.close()
+ } catch (e: IOException) {
+ logMessage("Error closing resources: ${e.message}")
+ }
+ super.onRevoke()
+ }
+
+ private fun logMessage(message: String) {
+ Log.d(TAG, message)
+ }
+
+ companion object {
+ private const val TAG = "MeshrabiyaVpnService"
+ private const val VPN_SESSION_NAME = "MeshrabiyaVPN"
+
+ // VPN_ADDRESS is the internal IP address used by the VPN for routing data within the VPN network.
+ private val VPN_ADDRESS = InetAddress.getByName("10.0.0.2")
+
+ // VPN_ADDRESS_PREFIX_LENGTH specifies the subnet mask for the VPN network. A value of 24 means the VPN uses a 255.255.255.0 subnet mask, allowing for up to 256 IP addresses.
+ private const val VPN_ADDRESS_PREFIX_LENGTH = 24
+
+ // ROUTE_ADDRESS is the IP address range for routing local network traffic. It’s used to determine which traffic should go through the local network.
+ private val ROUTE_ADDRESS = InetAddress.getByName("192.168.0.0")
+
+ // ROUTE_PREFIX_LENGTH specifies the subnet mask for local network routing. Similar to VPN_ADDRESS_PREFIX_LENGTH, this mask defines how many addresses are in the local network.
+ private const val ROUTE_PREFIX_LENGTH = 24
+
+ // PRIMARY_DNS is the IP address of the main DNS server used by the VPN for resolving domain names to IP addresses.
+ private val PRIMARY_DNS = InetAddress.getByName("1.1.1.1")
+
+ // SECONDARY_DNS is the IP address of a backup DNS server. If the primary DNS server is unavailable, the VPN will use this server instead.
+ private val SECONDARY_DNS = InetAddress.getByName("8.8.8.8")
+
+ // MTU_VALUE stands for Maximum Transmission Unit. It defines the largest size of a packet that can be sent through the VPN without needing to be fragmented. 1500 bytes is a common default size.
+ private const val MTU_VALUE = 1500
+
+ // BUFFER_SIZE determines the amount of data that can be read or written at once. 32767 bytes is a size chosen to handle large packets efficiently.
+ private const val BUFFER_SIZE = 32767
+
+ private const val IPV4_VERSION: Byte = 4
+ private const val IPV6_VERSION: Byte = 6
+
+ // Protocol numbers for different types of network traffic, based on the official IP protocol specifications.
+ // These numbers identify whether a packet is using TCP, UDP, or ICMPv6 protocol.
+ private const val IPPROTO_TCP = 6 // TCP is protocol number 6 (used for web traffic, file transfer, etc.)
+ private const val IPPROTO_UDP = 17 // UDP is protocol number 17 (used for streaming, video games, etc.)
+
+ private const val IPV4_PROTOCOL_OFFSET = 9 // For IPv4 packets, the type of protocol (e.g., TCP, UDP) is found at the 10th byte of the header. This is why the offset is 9.
+ private const val IPV4_HEADER_LENGTH_OFFSET = 0 // The length of the IPv4 header itself is encoded in the first byte, but only in the first 4 bits. This is why we use offset 0.
+ private const val IPV6_PROTOCOL_OFFSET = 6 // For IPv6 packets, the protocol type is found in the 7th byte of the header, which is why the offset is 6.
+ private const val IPV6_PORT_OFFSET = 40 // In IPv6 packets, information about source and destination ports starts after the first 40 bytes of the header, hence the offset is 40.
+ }
+}
+
+/**
+ * Enum class representing the valid protocols (TCP, UDP, ICMPv6, and Unknown).
+ */
+enum class Protocol {
+ TCP, UDP, UNKNOWN, ERROR
+}
+
+/**
+ * Data class to represent the parsed IP header with necessary information.
+ */
+data class IpHeader(
+ val destinationIp: InetAddress,
+ val destinationPort: Int,
+ val originIp: InetAddress,
+ val originPort: Int,
+ val protocolName: Protocol
+)
diff --git a/lib-meshrabiya-vpn/src/test/java/com/meshrabiya/lib_vpn/ExampleUnitTest.kt b/lib-meshrabiya-vpn/src/test/java/com/meshrabiya/lib_vpn/ExampleUnitTest.kt
new file mode 100644
index 0000000..664aeb3
--- /dev/null
+++ b/lib-meshrabiya-vpn/src/test/java/com/meshrabiya/lib_vpn/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.meshrabiya.lib_vpn
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/lib-meshrabiya/build.gradle b/lib-meshrabiya/build.gradle
index 1a27d73..f94f5b4 100644
--- a/lib-meshrabiya/build.gradle
+++ b/lib-meshrabiya/build.gradle
@@ -37,37 +37,39 @@ android {
}
dependencies {
- implementation "androidx.core:core-ktx:$version_androidx_core"
- implementation "androidx.appcompat:appcompat:$version_appcompat"
- implementation "com.athaydes.rawhttp:rawhttp-core:$version_rawhttp"
+ implementation(libs.google.play.services.nearby)
+ implementation libs.androidx.core
+ implementation libs.androidx.appcompat
+ implementation libs.rawhttp
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$version_kotlinx_serialization"
- implementation "androidx.datastore:datastore-preferences:$version_datastore"
+ implementation libs.datastore
- implementation "org.bouncycastle:bcprov-jdk18on:$version_bouncycastle"
- implementation "org.bouncycastle:bcpkix-jdk18on:$version_bouncycastle"
+ implementation libs.bouncycastle.prov
+ implementation libs.bouncycastle.pkix
- implementation "com.github.seancfoley:ipaddress:$version_ip_address"
+ implementation libs.ipaddress
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$version_android_desugaring"
testImplementation project(":test-shared")
- testImplementation "junit:junit:$version_junit"
- testImplementation "org.mockito.kotlin:mockito-kotlin:$version_kotlin_mockito"
+
+ testImplementation libs.junit
+ testImplementation libs.mockito.kotlin
testImplementation "app.cash.turbine:turbine:$version_turbine"
- testImplementation "com.squareup.okhttp3:mockwebserver:$version_mockwebserver"
- testImplementation "com.squareup.okhttp3:okhttp:$version_okhttp"
+ testImplementation libs.mockwebserver
+ testImplementation libs.okhttp
//As per: https://developer.android.com/topic/libraries/testing-support-library/packages.html#gradle-dependencies
- androidTestImplementation "androidx.test:runner:$version_android_junit_runner"
- androidTestImplementation "androidx.test:rules:$version_androidx_test_rules"
+ androidTestImplementation libs.android.junit.runner
+ androidTestImplementation libs.androidx.test.rules
androidTestImplementation project(":test-shared")
- androidTestImplementation "androidx.test.ext:junit:$version_android_test_ext_junit"
+ androidTestImplementation libs.androidx.junit
androidTestImplementation "app.cash.turbine:turbine:$version_turbine"
- androidTestImplementation "org.mockito:mockito-android:$version_android_mockito"
- androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$version_kotlin_mockito"
+ androidTestImplementation libs.mockito.android
+ androidTestImplementation libs.mockito.kotlin
}
publishing {
diff --git a/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/OriginatingMessageManager.kt b/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/OriginatingMessageManager.kt
index 09c9086..07eddcb 100644
--- a/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/OriginatingMessageManager.kt
+++ b/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/OriginatingMessageManager.kt
@@ -2,12 +2,14 @@ package com.ustadmobile.meshrabiya.vnet
import android.util.Log
import com.ustadmobile.meshrabiya.ext.addressToDotNotation
+import com.ustadmobile.meshrabiya.ext.asInetAddress
import com.ustadmobile.meshrabiya.ext.requireAddressAsInt
import com.ustadmobile.meshrabiya.log.MNetLogger
import com.ustadmobile.meshrabiya.mmcp.MmcpOriginatorMessage
import com.ustadmobile.meshrabiya.mmcp.MmcpPing
import com.ustadmobile.meshrabiya.mmcp.MmcpPong
import com.ustadmobile.meshrabiya.vnet.VirtualPacket.Companion.ADDR_BROADCAST
+import com.ustadmobile.meshrabiya.vnet.netinterface.VirtualNetworkInterface
import com.ustadmobile.meshrabiya.vnet.socket.ChainSocketNextHop
import com.ustadmobile.meshrabiya.vnet.wifi.state.MeshrabiyaWifiState
import kotlinx.coroutines.CoroutineScope
@@ -22,6 +24,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import java.net.DatagramPacket
@@ -33,8 +36,12 @@ import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
+/**
+ * @param virtualNetworkInterfaces function that provide a list of current virtual ip addresses for node.
+ */
+
class OriginatingMessageManager(
- localNodeInetAddr: InetAddress,
+ private val virtualNetworkInterfaces: () -> List,
private val logger: MNetLogger,
private val scheduledExecutorService: ScheduledExecutorService,
private val nextMmcpMessageId: () -> Int,
@@ -44,23 +51,22 @@ class OriginatingMessageManager(
lostNodeCheckInterval: Int = 1_000,
) {
- private val logPrefix ="[OriginatingMessageManager for ${localNodeInetAddr}] "
+ private val logPrefix = "[OriginatingMessageManager for ${virtualNetworkInterfaces}] "
private val scope = CoroutineScope(Dispatchers.IO + Job())
- private val localNodeAddress = localNodeInetAddr.requireAddressAsInt()
-
/**
* The currently known latest originator messages that can be used to route traffic.
*/
- private val originatorMessages: MutableMap = ConcurrentHashMap()
+ private val originatorMessages: MutableMap =
+ ConcurrentHashMap()
private val _state = MutableStateFlow