first commit
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
6
.idea/compiler.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/deploymentTargetSelector.xml
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
20
.idea/gradle.xml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/kotlinc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.0.0" />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/migrations.xml
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/misc.xml
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
17
.idea/runConfigurations.xml
generated
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
||||
93
app/build.gradle.kts
Normal file
@ -0,0 +1,93 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
id("com.google.protobuf") version "0.9.4"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.jihe.punchnet"
|
||||
compileSdk = 35
|
||||
|
||||
packagingOptions {
|
||||
exclude("META-INF/versions/9/OSGI-INF/MANIFEST.MF")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.jihe.punchnet"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = "com.google.protobuf:protoc:4.29.3"
|
||||
}
|
||||
|
||||
|
||||
generateProtoTasks {
|
||||
all().forEach {
|
||||
task -> task.builtins {
|
||||
create("kotlin")
|
||||
create("java")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
implementation("com.google.protobuf:protobuf-java:4.29.3")
|
||||
implementation("com.google.protobuf:protobuf-kotlin:4.29.3")
|
||||
|
||||
// implementation(libs.bcprov.jdk18on)
|
||||
implementation(libs.bcpkix.jdk18on)
|
||||
// implementation(files("libs/org.asxalex.sdlan/sdlan-1.0-SNAPSHOT.jar"))
|
||||
// implementation(files("libs/org.asxalex.sdlan/1.0.0/sdlan-1.0.0.jar"))
|
||||
//implementation("org.jetbrains.kotlin:kotlin-stdlib:2.1.0") {
|
||||
//version { strictly("2.1.0") }
|
||||
// }
|
||||
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@ -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
|
||||
@ -0,0 +1,22 @@
|
||||
package com.jihe.punchnet
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* 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.jihe.punchnet", appContext.packageName)
|
||||
}
|
||||
}
|
||||
43
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Punchnet"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Punchnet">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service android:name=".PunchnetService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
153
app/src/main/java/com/jihe/punchnet/MainActivity.kt
Normal file
@ -0,0 +1,153 @@
|
||||
package com.jihe.punchnet
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.jihe.punchnet.ui.theme.PunchnetTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private val TAG = "MainActivity"
|
||||
|
||||
private val vpnPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
when(result.resultCode) {
|
||||
RESULT_OK -> {
|
||||
// granted permission, start service
|
||||
}
|
||||
RESULT_CANCELED -> {
|
||||
Toast.makeText(this, "VPN permission denied", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareAndStartVPN() {
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent != null) {
|
||||
vpnPermissionLauncher.launch(intent)
|
||||
} else {
|
||||
startVpnService()
|
||||
}
|
||||
}
|
||||
|
||||
fun stopVpnService() {
|
||||
Log.d("STOP PUNCHNET", "stopping PUNCHNET")
|
||||
startService(Intent(this@MainActivity, PunchnetService::class.java).also { it.action=PunchnetService.ACTION_DISCONNECT })
|
||||
// stopService(Intent(this, PunchnetService::class.java))
|
||||
Log.d("STOPPED PUNCHNET", "stopping PUNCHNET")
|
||||
// Toast.makeText(this, "VPN service stopped", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun startVpnService() {
|
||||
val intent = Intent(this, PunchnetService::class.java)
|
||||
intent.putExtra("argument", PunchnetServiceArgument(
|
||||
"",
|
||||
arrayOf(
|
||||
RouteInfo(
|
||||
"192.168.80.0/24", "10.211.188.2"
|
||||
)
|
||||
)
|
||||
))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
Toast.makeText(this, "VPN service started", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
Log.d("DIR", "filesdir = ${this.filesDir}")
|
||||
setContent {
|
||||
PunchnetTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
StartStop(
|
||||
R.string.start_vpn,
|
||||
R.string.stop_vpn,
|
||||
onStart = {
|
||||
prepareAndStartVPN()
|
||||
},
|
||||
onStop = {
|
||||
stopVpnService()
|
||||
},
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StartStop(
|
||||
@StringRes start_name_id: Int,
|
||||
@StringRes stop_name_id: Int,
|
||||
onStart: ()->Unit,
|
||||
onStop: ()->Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val start_text = stringResource(start_name_id)
|
||||
val stop_text = stringResource(stop_name_id)
|
||||
var button_state by rememberSaveable { mutableStateOf(false) }
|
||||
var button_text by rememberSaveable() { mutableStateOf(start_text) }
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
button_state = !button_state
|
||||
if (button_state) {
|
||||
onStart()
|
||||
button_text = stop_text
|
||||
} else {
|
||||
Log.d("STOP", "call on stop")
|
||||
onStop()
|
||||
button_text = start_text
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text (
|
||||
text = button_text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
PunchnetTheme {
|
||||
Greeting("Android")
|
||||
}
|
||||
}
|
||||
181
app/src/main/java/com/jihe/punchnet/PunchnetService.kt
Normal file
@ -0,0 +1,181 @@
|
||||
package com.jihe.punchnet
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.jihe.punchnet.sdlan.config.Arguments
|
||||
import com.jihe.punchnet.sdlan.config.toIPV4String
|
||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
||||
import com.jihe.punchnet.sdlan.network.ARPTable
|
||||
import com.jihe.punchnet.sdlan.network.ARPWaitList
|
||||
import com.jihe.punchnet.sdlan.network.DeviceConfig
|
||||
import com.jihe.punchnet.sdlan.network.IfaceTun
|
||||
import com.jihe.punchnet.sdlan.network.ipInt2ByteArray
|
||||
import com.jihe.punchnet.sdlan.network.run_sdlan
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class PunchnetService : VpnService() , IfaceTun {
|
||||
private val TAG = "PunchnetService"
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
private var vpnDescriptor: ParcelFileDescriptor? = null
|
||||
|
||||
var input: FileInputStream? = null
|
||||
var output: FileOutputStream? = null
|
||||
|
||||
override val arpTable = ARPTable()
|
||||
override val arpWaitList = ARPWaitList()
|
||||
|
||||
var config: DeviceConfig = DeviceConfig(1400)
|
||||
|
||||
companion object {
|
||||
const val ACTION_CONNECT = "com.jihe.punchnet.punchnetservice.CONNECT"
|
||||
const val ACTION_DISCONNECT = "com.jihe.punchnet.punchnetservice.DISCONNECT"
|
||||
}
|
||||
|
||||
override suspend fun recv(): ByteArray {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
val result = ByteArray(1500)
|
||||
var size = input?.read(result)
|
||||
if (size == null) {
|
||||
println("xxx failed to read")
|
||||
size = 0
|
||||
} else {
|
||||
println("xxx got $size bytes")
|
||||
}
|
||||
|
||||
// val size = input?.read(result)?:0
|
||||
// Log.d(TAG, "RECEIVED $size bytes")
|
||||
result.copyOf(size)
|
||||
// result.slice(0..<size).toByteArray()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun send(content: ByteArray) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "WROTE bytes to vpn service")
|
||||
output?.write(content)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopVpn() {
|
||||
}
|
||||
|
||||
override suspend fun reload_config(config: DeviceConfig) {
|
||||
config.mtu = 1400
|
||||
this.config = config
|
||||
val ip = ipInt2ByteArray(config.ip.netAddr).toIPV4String()
|
||||
TerminalLogger.debugf {"got ip address from remote: ${ip}"}
|
||||
withContext(Dispatchers.IO) {
|
||||
input?.close()
|
||||
output?.close()
|
||||
}
|
||||
vpnDescriptor?.close()
|
||||
|
||||
vpnDescriptor = Builder()
|
||||
.setMtu(config.mtu)
|
||||
.addAddress(ip, config.ip.netBitLen.toInt())
|
||||
.setBlocking(true)
|
||||
.establish()
|
||||
|
||||
input = FileInputStream(vpnDescriptor!!.fileDescriptor)
|
||||
output = FileOutputStream(vpnDescriptor!!.fileDescriptor)
|
||||
}
|
||||
|
||||
private fun disconnect() {
|
||||
Toast.makeText(this, "stop vpn called", Toast.LENGTH_LONG).show()
|
||||
input?.close()
|
||||
output?.close()
|
||||
vpnDescriptor?.close()
|
||||
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun connect(startArg: PunchnetServiceArgument?) {
|
||||
val iface = this
|
||||
|
||||
val server = "punchnet.aioe.tech"
|
||||
|
||||
Log.d("DIR", "datadir = ${Environment.getDataDirectory().name}")
|
||||
Log.d("DIR", "external storage = ${Environment.getExternalStorageDirectory().name}")
|
||||
Log.d("DIR", "filesdir = ${this.filesDir.path}")
|
||||
|
||||
val argument = Arguments(
|
||||
baseDir = this.filesDir.path,
|
||||
sn = "$server:1265",
|
||||
tcp = "$server:18083",
|
||||
|
||||
nat_server1 = "$server:1265",
|
||||
nat_server2 = "47.98.178.3:1265",
|
||||
token = "",
|
||||
name = "tau",
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
|
||||
run_sdlan(iface, argument)
|
||||
}
|
||||
|
||||
val notification = createNotification()
|
||||
startForeground(1, notification)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
return if (intent?.action == ACTION_DISCONNECT) {
|
||||
disconnect()
|
||||
START_NOT_STICKY
|
||||
} else {
|
||||
val argument = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent?.getParcelableExtra("argument", PunchnetServiceArgument::class.java)
|
||||
} else {
|
||||
intent?.getParcelableExtra("argument")
|
||||
}
|
||||
println("argument = ${argument}")
|
||||
connect(argument)
|
||||
START_STICKY
|
||||
}
|
||||
// return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
private fun createNotification(): Notification {
|
||||
val channelId = "punchnet_channel"
|
||||
val channel = NotificationChannel (
|
||||
channelId,
|
||||
"punchnet",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.createNotificationChannel(channel)
|
||||
|
||||
return NotificationCompat.Builder(this, channelId)
|
||||
.setContentTitle("Punchnet Service")
|
||||
.setContentText("Punchnet is running")
|
||||
.setSmallIcon(R.drawable.ic_vpn)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
scope.cancel()
|
||||
disconnect()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package com.jihe.punchnet
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
data class RouteInfo(
|
||||
// 192.168.80.0/24
|
||||
val targetNetCIDR: String,
|
||||
// 10.167.69.2
|
||||
val gateway: String,
|
||||
)
|
||||
|
||||
data class PunchnetServiceArgument(
|
||||
val token: String,
|
||||
val routes: Array<RouteInfo>,
|
||||
): Parcelable {
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeString(token)
|
||||
dest.writeInt(routes.size)
|
||||
routes.forEach { item ->
|
||||
dest.writeString(item.targetNetCIDR)
|
||||
dest.writeString(item.gateway)
|
||||
}
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR: Parcelable.Creator<PunchnetServiceArgument> {
|
||||
override fun createFromParcel(source: Parcel): PunchnetServiceArgument {
|
||||
val token = source.readString()!!
|
||||
val size = source.readInt()
|
||||
var routes = mutableListOf<RouteInfo>()
|
||||
for (i in 0..<size) {
|
||||
val net = source.readString()!!
|
||||
val gw = source.readString()!!
|
||||
routes.add(RouteInfo(
|
||||
targetNetCIDR = net,
|
||||
gateway = gw,
|
||||
))
|
||||
}
|
||||
return PunchnetServiceArgument(token, routes.toTypedArray())
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<PunchnetServiceArgument?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.jihe.punchnet.sdlan.config
|
||||
|
||||
data class Arguments(
|
||||
val baseDir: String,
|
||||
// udp info of the super node
|
||||
val sn: String,
|
||||
// tcp info of the super node
|
||||
val tcp: String,
|
||||
|
||||
val nat_server1: String,
|
||||
val nat_server2: String,
|
||||
|
||||
val mtu: Int = 1400,
|
||||
val token: String,
|
||||
|
||||
val allowRouting: Boolean = true,
|
||||
val dropMulticast: Boolean = true,
|
||||
|
||||
val registerTTL: Int = 1,
|
||||
|
||||
val name: String,
|
||||
// type of service
|
||||
val tos: Int = 0,
|
||||
// local udp port, default use 0, for random
|
||||
val localPort: Int = 0,
|
||||
|
||||
val allowP2P: Boolean = true,
|
||||
)
|
||||
44
app/src/main/java/com/jihe/punchnet/sdlan/config/config.kt
Normal file
@ -0,0 +1,44 @@
|
||||
package com.jihe.punchnet.sdlan.config
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
|
||||
object SDLanConfig {
|
||||
const val RegisterSuperInterval: Byte = 20
|
||||
const val RegisterInterval: Int = 5
|
||||
|
||||
const val ID_FILENAME: String = ".id"
|
||||
|
||||
const val TCP_PING_TIME: Long = 7
|
||||
|
||||
val BROADCASTMAC: ByteString = ByteString.copyFrom(byteArrayOf(0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte()))
|
||||
var BROADCASTMAC_BYTEARRAY = BROADCASTMAC.toByteArray()
|
||||
}
|
||||
|
||||
object SDLanMulticastConfig {
|
||||
const val MULTICAST_PORT: Int = 1070
|
||||
val MULTICAST_V4: ByteArray = byteArrayOf(224.toByte(),0u.toByte(),0.toByte(),70.toByte())
|
||||
}
|
||||
|
||||
fun ByteArray.toIPV4String(): String {
|
||||
if (this.size != 4) {
|
||||
return "0.0.0.0"
|
||||
}
|
||||
return "${this[0].toUByte()}.${this[1].toUByte()}.${this[2].toUByte()}.${this[3].toUByte()}"
|
||||
}
|
||||
|
||||
object RSAConfig {
|
||||
const val BASE_DIR: String = ".keys"
|
||||
|
||||
const val RSAMODE: String = "RSA/ECB/PKCS1Padding"
|
||||
|
||||
const val PUBLIC_FILE_NAME: String = "id_rsa.pub"
|
||||
const val PRIVATE_FILE_NAME: String = "id_rsa"
|
||||
|
||||
const val ENCRYPT_MAX_SIZE: Int = 245
|
||||
const val DECRYPT_MAX_SIZE: Int = 256
|
||||
}
|
||||
|
||||
object AESConfig {
|
||||
const val CIPHER_ALGORITHM: String = "AES/CBC/PKCS7Padding"
|
||||
const val KEY_SPEC: String = "AES"
|
||||
}
|
||||
17
app/src/main/java/com/jihe/punchnet/sdlan/config/ethernet.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package com.jihe.punchnet.sdlan.config
|
||||
|
||||
object EtherType {
|
||||
const val IPV4: Short = 0x0800.toShort()
|
||||
const val IPV6: Short = 0x86dd.toShort()
|
||||
|
||||
const val ARP: Short = 0x0806.toShort()
|
||||
|
||||
const val HWTYPE_ETH: Short = 1
|
||||
|
||||
const val ARP_REQUEST: Short = 1
|
||||
const val ARP_REPLY: Short = 2
|
||||
|
||||
const val ARP_TABLE_SIZE: Int = 100
|
||||
const val ARP_EXPIRE_TIME: Int = 60
|
||||
const val ARP_DETECT_TIMES: Int = 3
|
||||
}
|
||||
39
app/src/main/java/com/jihe/punchnet/sdlan/logs/logs.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package com.jihe.punchnet.sdlan.logs
|
||||
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
const val ColorPrefix = "\u001b"
|
||||
const val GreenColor = "${ColorPrefix}[0;32m"
|
||||
const val BlueColor = "${ColorPrefix}[0;34m"
|
||||
const val YellowColor = "${ColorPrefix}[0;33m"
|
||||
const val RedColor = "${ColorPrefix}[0;31m"
|
||||
const val ColorReset = "${ColorPrefix}[0;0m"
|
||||
|
||||
interface Logger {
|
||||
val format: DateTimeFormatter
|
||||
fun debugf(formatter: ()->String) {
|
||||
val now = LocalDateTime.now().format(format)
|
||||
println("${GreenColor}DEBU:${ColorReset}[$now] ${formatter()}")
|
||||
}
|
||||
fun infof(formatter: ()->String) {
|
||||
val now = LocalDateTime.now().format(format)
|
||||
println("${BlueColor}INFO:${ColorReset}[$now] ${formatter()}")
|
||||
}
|
||||
fun warning(formatter: ()->String) {
|
||||
val now = LocalDateTime.now().format(format)
|
||||
println("${YellowColor}WARN:${ColorReset}[$now] ${formatter()}")
|
||||
}
|
||||
fun errorf(formatter: ()->String) {
|
||||
val now = LocalDateTime.now().format(format)
|
||||
println("${RedColor}ERRO:${ColorReset}[$now] ${formatter()}")
|
||||
}
|
||||
fun criticalf(formatter: ()->String) {
|
||||
val now = LocalDateTime.now().format(format)
|
||||
println("${RedColor}CRIT:${ColorReset}[$now] ${formatter()}")
|
||||
}
|
||||
}
|
||||
|
||||
object TerminalLogger: Logger {
|
||||
override val format: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm::s")
|
||||
}
|
||||
139
app/src/main/java/com/jihe/punchnet/sdlan/network/arp.kt
Normal file
@ -0,0 +1,139 @@
|
||||
package com.jihe.punchnet.sdlan.network
|
||||
|
||||
import com.google.protobuf.kotlin.toByteString
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLData
|
||||
import com.jihe.punchnet.sdlan.config.EtherType
|
||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
||||
import com.jihe.punchnet.sdlan.utils.ipToString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class ARPTable() {
|
||||
val content = ConcurrentHashMap<Int, ARPInfo>()
|
||||
|
||||
suspend fun agingARP() {
|
||||
CoroutineScope(Dispatchers.Default).async {
|
||||
while(true) {
|
||||
delay(20_000)
|
||||
val now = System.currentTimeMillis()/1000
|
||||
val toRemove = mutableListOf<Int>()
|
||||
for ((k, v) in content) {
|
||||
if (now-v.timestamp > EtherType.ARP_EXPIRE_TIME) {
|
||||
toRemove.add(k)
|
||||
}
|
||||
}
|
||||
|
||||
for (item in toRemove) {
|
||||
content.remove(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getMacFromIP(ip: Int): ByteArray? {
|
||||
val value = content.get(ip)
|
||||
if (value == null) {
|
||||
println("content table size is: ${content.size}")
|
||||
for (k in content.keys()) {
|
||||
println("content has key: ${ipToString(k)}")
|
||||
}
|
||||
}
|
||||
return value?.mac
|
||||
}
|
||||
|
||||
fun addToARPTable(ip: Int, mac: ByteArray) {
|
||||
val origin = content.get(ip)
|
||||
val now = System.currentTimeMillis()/1000
|
||||
if (origin != null) {
|
||||
TerminalLogger.debugf { "adding ${ipToString(ip)} to arptable" }
|
||||
origin.timestamp = now
|
||||
origin.mac = mac
|
||||
return
|
||||
}
|
||||
if (content.size >= EtherType.ARP_TABLE_SIZE) {
|
||||
// loop and clear the old aged item, or if no one expires,
|
||||
// remove the oldest one
|
||||
var maxage = -1L
|
||||
|
||||
val toRemove = mutableListOf<Int>()
|
||||
var maxindex = -1
|
||||
val expireage = EtherType.ARP_EXPIRE_TIME + EtherType.ARP_DETECT_TIMES*5
|
||||
for ((k, v) in content) {
|
||||
if (now-v.timestamp > expireage) {
|
||||
toRemove.add(k)
|
||||
continue
|
||||
}
|
||||
if (now-v.timestamp > maxage) {
|
||||
maxage = now-v.timestamp
|
||||
maxindex = k
|
||||
}
|
||||
}
|
||||
if (toRemove.size == 0 && maxindex != -1) {
|
||||
toRemove.add(maxindex)
|
||||
}
|
||||
|
||||
for (item in toRemove) {
|
||||
content.remove(item)
|
||||
}
|
||||
println("adding ${ipToString(ip)}")
|
||||
}
|
||||
content[ip] = ARPInfo(now, mac)
|
||||
}
|
||||
}
|
||||
|
||||
class ARPInfo(var timestamp: Long, var mac: ByteArray)
|
||||
|
||||
|
||||
class ARPWaitList {
|
||||
val content = ConcurrentHashMap<Int, MutableList<ARPWaitInfo>>()
|
||||
|
||||
fun addToWaitList(ip: Int, originData: ByteArray) {
|
||||
val origin = content.getOrPut(ip, {mutableListOf<ARPWaitInfo>()})
|
||||
|
||||
val now = System.currentTimeMillis()/1000
|
||||
if (origin.size > 5) {
|
||||
origin.removeAt(0)
|
||||
// origin.removeFirst()
|
||||
}
|
||||
origin.add(ARPWaitInfo(now, originData))
|
||||
}
|
||||
|
||||
suspend fun arpArrived(node: Node, ip: Int, mac: Mac) {
|
||||
val waitlist = content.remove(ip)
|
||||
if (waitlist == null) {
|
||||
return
|
||||
}
|
||||
if (!node.aes.isAuthorized()) {
|
||||
return
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()/1000
|
||||
val networkid = node.networkID.get()
|
||||
for (item in waitlist) {
|
||||
if (now - item.timestamp > 5) {
|
||||
// just skip the packet
|
||||
continue
|
||||
}
|
||||
val packet = formEthernetPacket(node.mac.toByteArray(), mac.toByteArray(), item.originData)
|
||||
val size = packet.remaining()
|
||||
val encrypted = node.aes.encrypt(packet)
|
||||
if (encrypted != null) {
|
||||
val data = SDLData.newBuilder()
|
||||
.setIsP2P(true)
|
||||
.setNetworkId(networkid)
|
||||
.setTtl(2)
|
||||
.setSrcMac(node.mac)
|
||||
.setDstMac(mac)
|
||||
.setData(encrypted.toByteString())
|
||||
.build()
|
||||
val msg = encodeToUDPMessage(data, PacketType.Data)
|
||||
sendPacketToNet(node, mac, msg, size.toLong())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
class ARPWaitInfo(val timestamp: Long, val originData: ByteArray)
|
||||
319
app/src/main/java/com/jihe/punchnet/sdlan/network/edge.kt
Normal file
@ -0,0 +1,319 @@
|
||||
package com.jihe.punchnet.sdlan.network
|
||||
|
||||
// import sdlanproto.Message.*
|
||||
import com.google.protobuf.kotlin.toByteString
|
||||
import com.jihe.punchnet.protobuf.PunchProto
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLStunProbe
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLStunProbeReply
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLStunRequest
|
||||
import com.jihe.punchnet.sdlan.config.SDLanMulticastConfig
|
||||
import com.jihe.punchnet.sdlan.utils.AES
|
||||
import com.jihe.punchnet.sdlan.utils.RSA
|
||||
import com.jihe.punchnet.sdlan.utils.UniqueNodeID
|
||||
import com.jihe.punchnet.sdlan.utils.generateRandomMAC
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.SocketAddress
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class DeviceConfig (
|
||||
var mtu: Int,
|
||||
var mac: Mac,
|
||||
var ip: IPSubnet,
|
||||
) {
|
||||
constructor(mtu: Int): this(
|
||||
mtu,
|
||||
ByteArray(6).toByteString(),
|
||||
IPSubnet(0, 0),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class NodeConfig (
|
||||
val baseDir: String,
|
||||
val name: String,
|
||||
val nodeUUID: String,
|
||||
|
||||
val allowRouting: Boolean,
|
||||
val dropMulticast: Boolean,
|
||||
|
||||
val allowP2P: Boolean,
|
||||
val mtu: Int,
|
||||
val tos: Int,
|
||||
val registerSuperInterval: Byte,
|
||||
|
||||
val registerTTL: Int,
|
||||
val localPort: Int,
|
||||
|
||||
val supernode: SDLanSock,
|
||||
val natServer1: InetSocketAddress,
|
||||
val natServer2: InetSocketAddress
|
||||
)
|
||||
|
||||
class NodeStats private constructor(
|
||||
// communicate with p2p
|
||||
val txP2P: AtomicLong,
|
||||
val rxP2P: AtomicLong,
|
||||
// communicate with server
|
||||
val txSup: AtomicLong,
|
||||
val rxSup: AtomicLong,
|
||||
// communicate through broadcast
|
||||
val txBroadcast: AtomicLong,
|
||||
val rxBroadcast: AtomicLong,
|
||||
|
||||
// last time communicate with server
|
||||
val lastSup: AtomicLong,
|
||||
// last time communicate with the edge
|
||||
val lastP2P: AtomicLong,
|
||||
) {
|
||||
constructor(): this(
|
||||
AtomicLong(0),
|
||||
AtomicLong(0),
|
||||
AtomicLong(0),
|
||||
AtomicLong(0),
|
||||
AtomicLong(0),
|
||||
AtomicLong(0),
|
||||
AtomicLong(0),
|
||||
AtomicLong(0),
|
||||
)
|
||||
}
|
||||
|
||||
class NatProbeCookie {
|
||||
val cookieid: AtomicInteger = AtomicInteger(1)
|
||||
val cookiemap = ConcurrentHashMap<Int, SendChannel<SDLStunProbeReply>>()
|
||||
|
||||
fun getChannelWithID(id: Int): SendChannel<SDLStunProbeReply>? {
|
||||
return this.cookiemap.get(id)
|
||||
}
|
||||
|
||||
suspend fun sendMessage(id: Int, value: SDLStunProbeReply) {
|
||||
cookiemap.remove(id)?.send(value)
|
||||
}
|
||||
|
||||
fun addChannel(channel: SendChannel<SDLStunProbeReply>): Int {
|
||||
val ck = cookieid.getAndAdd(1)
|
||||
cookiemap.set(ck, channel)
|
||||
return ck
|
||||
}
|
||||
|
||||
fun remove(id: Int) {
|
||||
cookiemap.remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
class Node private constructor (
|
||||
// 当前的packet id
|
||||
val packetID: AtomicInteger,
|
||||
// 自身所处的network的id
|
||||
val networkID: AtomicInteger,
|
||||
// 所处网络的token
|
||||
var token: String,
|
||||
|
||||
// tun设备(vpnservice)的配置
|
||||
val deviceConfig: DeviceConfig,
|
||||
// interface的抽象
|
||||
var iface: Iface?,
|
||||
|
||||
// val authorized: AtomicBoolean,
|
||||
|
||||
// rsa info
|
||||
val rsa: RSA,
|
||||
// aes info
|
||||
val aes: AES,
|
||||
// var encryptKey: ByteArray,
|
||||
|
||||
val config: NodeConfig,
|
||||
|
||||
val pendingPeers: PeerMap,
|
||||
val knownPeers: PeerMap,
|
||||
|
||||
// 自身的公网ip
|
||||
val outerIPV4: AtomicInteger,
|
||||
val udpSockV4: SDLanSocket,
|
||||
|
||||
val udpSockMulticast: SDLanSocket?,
|
||||
val multicastSock: SDLanSock,
|
||||
|
||||
val stats: NodeStats,
|
||||
|
||||
// send message to socket, if connected, and authorized
|
||||
var toSocket: SendChannel<ByteArray>,
|
||||
|
||||
val startStopChannel: SendChannel<StartStopChanInfo>,
|
||||
|
||||
val mac: Mac = generateRandomMAC(),
|
||||
var nat_type: NatType = NatType.Invalid,
|
||||
val natProbeCookie: NatProbeCookie = NatProbeCookie(),
|
||||
) {
|
||||
companion object {
|
||||
private var instance: Node? = null
|
||||
|
||||
@Synchronized
|
||||
fun initialize(
|
||||
config: NodeConfig,
|
||||
v4Sock: SDLanSocket,
|
||||
multicastSock: SDLanSocket?,
|
||||
token: String,
|
||||
rsa: RSA,
|
||||
iface: Iface,
|
||||
toSocket: SendChannel<ByteArray>,
|
||||
startStopChannel: SendChannel<StartStopChanInfo>,
|
||||
) : Node{
|
||||
if (instance == null) {
|
||||
instance = Node(
|
||||
config,
|
||||
v4Sock,
|
||||
multicastSock,
|
||||
token,
|
||||
rsa,
|
||||
iface,
|
||||
toSocket,
|
||||
startStopChannel,
|
||||
)
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
|
||||
fun getInstance(): Node {
|
||||
return instance ?: throw IllegalStateException("Node is not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(
|
||||
// pubkey: String,
|
||||
config: NodeConfig,
|
||||
v4Sock: SDLanSocket,
|
||||
multicastSock: SDLanSocket?,
|
||||
token: String,
|
||||
rsa: RSA,
|
||||
iface: Iface,
|
||||
toSocket: SendChannel<ByteArray>,
|
||||
startStopChannel: SendChannel<StartStopChanInfo>,
|
||||
// privateKey: ByteArray,
|
||||
// mtu: Int
|
||||
): this (
|
||||
packetID = AtomicInteger(1),
|
||||
networkID = AtomicInteger(0),
|
||||
token = token,
|
||||
deviceConfig = DeviceConfig(config.mtu),
|
||||
iface = iface,
|
||||
rsa = rsa,
|
||||
aes = AES.getAES(),
|
||||
config = config,
|
||||
pendingPeers = PeerMap(),
|
||||
knownPeers = PeerMap(),
|
||||
|
||||
outerIPV4 = AtomicInteger(0),
|
||||
udpSockV4 = v4Sock,
|
||||
udpSockMulticast = multicastSock,
|
||||
|
||||
toSocket = toSocket,
|
||||
startStopChannel = startStopChannel,
|
||||
|
||||
multicastSock = SDLanSock(
|
||||
IPFamily.IPV4,
|
||||
SDLanMulticastConfig.MULTICAST_PORT,
|
||||
SDLanMulticastConfig.MULTICAST_V4,
|
||||
),
|
||||
|
||||
stats = NodeStats(),
|
||||
) {
|
||||
this.deviceConfig.mac = this.mac
|
||||
}
|
||||
|
||||
fun getNextPacketID(): Int {
|
||||
return packetID.getAndAdd(1)
|
||||
}
|
||||
|
||||
suspend fun sendStunRequest() {
|
||||
val req = SDLStunRequest.newBuilder()
|
||||
.setCookie(0)
|
||||
.setClientId(UniqueNodeID.getUUID())
|
||||
.setNetworkId(this.networkID.get())
|
||||
.setIp(this.deviceConfig.ip.netAddr)
|
||||
.setMac(this.mac)
|
||||
.setNatType(this.nat_type.toByte().toInt())
|
||||
.setV6Info(PunchProto.SDLV6Info.getDefaultInstance())
|
||||
.build()
|
||||
|
||||
val msg = encodeToUDPMessage(req, PacketType.StunRequest)
|
||||
sendToSock(this, msg, config.supernode)
|
||||
}
|
||||
|
||||
suspend fun probeNatType() {
|
||||
val reply1 = this._sendAndWaitForProbeReply(StunProbeAttr.None, config.natServer1)
|
||||
if (reply1 == null) {
|
||||
nat_type = NatType.Blocked
|
||||
return
|
||||
}
|
||||
if (reply1.ip == (outerIPV4.get())) {
|
||||
if (this._sendAndWaitForProbeReply(StunProbeAttr.Peer, config.natServer1) == null) {
|
||||
// failed to get with peer, just symmetric
|
||||
nat_type = NatType.Symmetric
|
||||
return
|
||||
}
|
||||
nat_type = NatType.NoNat
|
||||
return
|
||||
}
|
||||
|
||||
val reply2 = this._sendAndWaitForProbeReply(StunProbeAttr.Peer, config.natServer1)
|
||||
if (reply2 != null) {
|
||||
nat_type = NatType.FullCone
|
||||
return
|
||||
}
|
||||
|
||||
val reply3 = this._sendAndWaitForProbeReply(StunProbeAttr.None, config.natServer2)
|
||||
if (reply3 == null) {
|
||||
nat_type = NatType.Blocked
|
||||
return
|
||||
}
|
||||
|
||||
if ((reply1.ip != reply3.ip) || (reply1.port != reply3.port)) {
|
||||
nat_type = NatType.Symmetric
|
||||
return
|
||||
}
|
||||
|
||||
val reply4 = this._sendAndWaitForProbeReply(StunProbeAttr.Port, config.natServer1)
|
||||
if (reply4 == null) {
|
||||
nat_type = NatType.PortRestricted
|
||||
} else {
|
||||
nat_type = NatType.ConeRestrict
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun _sendAndWaitForProbeReply(attr: StunProbeAttr, toServer: SocketAddress): SDLStunProbeReply? {
|
||||
val channel = Channel<SDLStunProbeReply>(100)
|
||||
val cookie = natProbeCookie.addChannel(channel)
|
||||
val probe = SDLStunProbe.newBuilder()
|
||||
.setAttr(attr.ordinal.toInt())
|
||||
.setCookie(cookie)
|
||||
.build()
|
||||
|
||||
val msg = encodeToUDPMessage(probe, PacketType.StunProbe)
|
||||
this.udpSockV4.send_to(msg.toByteArray(), toServer)
|
||||
|
||||
val k = withTimeoutOrNull(5000) {
|
||||
|
||||
val response = channel.receive()
|
||||
return@withTimeoutOrNull response
|
||||
}
|
||||
natProbeCookie.remove(cookie)
|
||||
return k
|
||||
}
|
||||
|
||||
suspend fun ping_to_sn() {
|
||||
val msg = encodeToTcpMessage(null, 0, PacketType.Ping).toByteArray()
|
||||
_sendDataToSocket(msg)
|
||||
}
|
||||
|
||||
suspend fun _sendDataToSocket(msg: ByteArray) {
|
||||
if (aes.isAuthorized()) {
|
||||
toSocket.send(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
514
app/src/main/java/com/jihe/punchnet/sdlan/network/helper.kt
Normal file
@ -0,0 +1,514 @@
|
||||
package com.jihe.punchnet.sdlan.network
|
||||
|
||||
import com.google.protobuf.kotlin.toByteString
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLData
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLPeerInfo
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLQueryInfo
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegister
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterAck
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLSendRegisterEvent
|
||||
import com.jihe.punchnet.sdlan.config.EtherType
|
||||
import com.jihe.punchnet.sdlan.config.SDLanConfig
|
||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
||||
import com.jihe.punchnet.sdlan.utils.isMultiBroadcast
|
||||
import com.jihe.punchnet.sdlan.utils.macToString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
|
||||
suspend fun handlePacketData(
|
||||
node: Node,
|
||||
body: ByteBuffer,
|
||||
senderSock: SDLanSock
|
||||
) {
|
||||
val data: SDLData
|
||||
try {
|
||||
data = SDLData.parseFrom(body)
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf { "failed to decode DATA: $e" }
|
||||
return
|
||||
}
|
||||
|
||||
if (data.isP2P) {
|
||||
TerminalLogger.debugf {
|
||||
"[P2P] Rx data from ${senderSock}"
|
||||
}
|
||||
CoroutineScope(Dispatchers.Default).async {
|
||||
checkPeerRegistrationNeeded(
|
||||
node,
|
||||
false,
|
||||
data.srcMac,
|
||||
NatType.NoNat,
|
||||
senderSock,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TerminalLogger.debugf {
|
||||
"[PsP] Rx data from ${macToString(data.srcMac)} via ${senderSock}"
|
||||
}
|
||||
}
|
||||
handleTunPacket(node, senderSock, !data.isP2P, data)
|
||||
}
|
||||
|
||||
suspend fun handleTunPacket(
|
||||
node: Node,
|
||||
senderSock: SDLanSock,
|
||||
fromSN: Boolean,
|
||||
data: SDLData,
|
||||
) {
|
||||
val payload = data.data
|
||||
val origin = node.aes.decrypt(payload.toByteArray())
|
||||
if (origin == null) {
|
||||
TerminalLogger.errorf { "error to decrypt data" }
|
||||
return
|
||||
}
|
||||
TerminalLogger.debugf {"handle data from net"}
|
||||
node.iface?.handleDataFromNet(node, origin)
|
||||
}
|
||||
|
||||
fun handlePacketRegisterACK(
|
||||
node: Node,
|
||||
body: ByteBuffer,
|
||||
senderSock: SDLanSock
|
||||
) {
|
||||
|
||||
val ack: SDLRegisterAck
|
||||
try {
|
||||
ack = SDLRegisterAck.parseFrom(body)
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf { "failed to decode REGISTERACK: $e" }
|
||||
return
|
||||
}
|
||||
|
||||
val viaMulticast = isMultiBroadcast(ack.dstMac)
|
||||
if (viaMulticast && ack.srcMac == node.mac) {
|
||||
TerminalLogger.debugf { "skip register from self" }
|
||||
return
|
||||
}
|
||||
|
||||
TerminalLogger.infof {
|
||||
"Rx REGISTERACK from ${macToString(ack.srcMac)} [${senderSock}] to ${macToString(ack.dstMac)}"
|
||||
}
|
||||
|
||||
peerSetP2PConfirmed(node, ack.srcMac, senderSock)
|
||||
}
|
||||
|
||||
fun peerSetP2PConfirmed(node: Node, mac: Mac, sock: SDLanSock) {
|
||||
val scan = node.pendingPeers.remove(mac)
|
||||
if (scan == null) {
|
||||
TerminalLogger.errorf { "failed to find sender in pending peers: ${sock}" }
|
||||
return
|
||||
}
|
||||
val now = System.currentTimeMillis()/1000
|
||||
var scan2 = node.knownPeers.get(mac)
|
||||
if (scan2 == null) {
|
||||
node.knownPeers[mac] = scan
|
||||
scan2 = node.knownPeers.get(mac)
|
||||
}
|
||||
scan2?.sock = sock
|
||||
scan2?.lastP2P?.set(now)
|
||||
scan2?.lastSeen?.set(now)
|
||||
|
||||
TerminalLogger.debugf {
|
||||
"P2P connection established: ${macToString(mac)} [${sock}]"
|
||||
}
|
||||
|
||||
TerminalLogger.debugf {
|
||||
"===> new Peer: ${macToString(mac)} -> ${sock}"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun handlePacketRegister(
|
||||
node: Node,
|
||||
body: ByteBuffer,
|
||||
senderSock: SDLanSock
|
||||
) {
|
||||
if(!node.aes.isAuthorized()) {
|
||||
TerminalLogger.errorf { "drop register due to not authorized"}
|
||||
return
|
||||
}
|
||||
val reg: SDLRegister
|
||||
try {
|
||||
reg = SDLRegister.parseFrom(body)
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf { "failed to parse REGISTER: $e"}
|
||||
return
|
||||
}
|
||||
val srcMac = reg.srcMac
|
||||
|
||||
val viaMulticast = isMultiBroadcast(reg.dstMac)
|
||||
if (viaMulticast && reg.srcMac == node.mac) {
|
||||
TerminalLogger.debugf { "skip register from self" }
|
||||
return
|
||||
}
|
||||
|
||||
// must be from peer
|
||||
|
||||
TerminalLogger.infof {
|
||||
"[P2P] Rx REGISTER from ${senderSock}, deleting from pending"
|
||||
}
|
||||
val remoteNAT = node.pendingPeers.remove(srcMac)?.natType?:NatType.Invalid
|
||||
|
||||
sendRegisterACK(node,senderSock, reg)
|
||||
|
||||
checkPeerRegistrationNeeded(node, false, reg.srcMac, remoteNAT, senderSock)
|
||||
}
|
||||
|
||||
suspend fun sendRegisterACK(
|
||||
node: Node,
|
||||
sender: SDLanSock,
|
||||
reg: SDLRegister,
|
||||
) {
|
||||
if (!node.config.allowP2P) {
|
||||
TerminalLogger.debugf {
|
||||
"Skipping REGISTER ACK as P2P is not allowed"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// must be authed
|
||||
val ack = SDLRegisterAck.newBuilder()
|
||||
.setNetworkId(node.networkID.get())
|
||||
.setSrcMac(node.deviceConfig.mac)
|
||||
.setDstMac(reg.srcMac)
|
||||
.build()
|
||||
|
||||
val data = encodeToUDPMessage(ack, PacketType.RegisterACK)
|
||||
sendToSock(node, data, sender)
|
||||
}
|
||||
|
||||
suspend fun checkPeerRegistrationNeeded(
|
||||
node: Node,
|
||||
fromSN: Boolean,
|
||||
srcMac: Mac,
|
||||
remoteNat: NatType,
|
||||
peerSock: SDLanSock,
|
||||
) {
|
||||
val peer = node.knownPeers.get(srcMac)
|
||||
if (peer == null) {
|
||||
TerminalLogger.debugf {
|
||||
"not found in known, send REGISTER to peer"
|
||||
}
|
||||
registerWithNewPeer(node,srcMac, remoteNat, peerSock, System.currentTimeMillis()/1000)
|
||||
return
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()/1000
|
||||
if (!fromSN) {
|
||||
peer.lastP2P.set(now)
|
||||
}
|
||||
|
||||
if (peerSock.family != peer.sock.family) {
|
||||
TerminalLogger.errorf { "family changed, just ignore it"}
|
||||
return
|
||||
}
|
||||
if (now - peer.lastSeen.get() > 3) {
|
||||
checkKnownPeerSockChanged(node, fromSN, srcMac, peerSock, now)
|
||||
}
|
||||
|
||||
}
|
||||
suspend fun checkKnownPeerSockChanged(
|
||||
node: Node,
|
||||
fromSN: Boolean,
|
||||
srcMac: Mac,
|
||||
peerSock: SDLanSock,
|
||||
now: Long,
|
||||
) {
|
||||
if (isMultiBroadcast(srcMac)) {
|
||||
return
|
||||
}
|
||||
val peer = node.knownPeers.get(srcMac)
|
||||
if (peer == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!peerSock.is_equal(peer.sock)) {
|
||||
if (!fromSN) {
|
||||
TerminalLogger.infof {
|
||||
"peer changed: ${srcMac}: ${peer.sock} -> $peerSock"
|
||||
}
|
||||
val remoteNAT = peer.natType
|
||||
node.knownPeers.remove(srcMac)
|
||||
|
||||
registerWithNewPeer(node, srcMac, remoteNAT, peerSock, now)
|
||||
}
|
||||
} else {
|
||||
peer.lastSeen.set(now)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun registerWithNewPeer(
|
||||
node: Node,
|
||||
// fromSN: Boolean,
|
||||
mac: Mac,
|
||||
remoteNat: NatType,
|
||||
peerSock: SDLanSock,
|
||||
now: Long,
|
||||
) {
|
||||
var peer = node.pendingPeers.get(mac)
|
||||
if (peer == null) {
|
||||
TerminalLogger.debugf {
|
||||
"===> new pending: ${macToString(mac)} => ${peerSock}"
|
||||
}
|
||||
node.pendingPeers[mac] = EdgePeer(0, node.deviceConfig.ip.netBitLen, peerSock, null, now)
|
||||
peer = node.pendingPeers.get(mac)
|
||||
peer?.lastSeen?.set(now)
|
||||
|
||||
sendRegister(node, remoteNat, peerSock, mac)
|
||||
registerWithLocalPeers(node)
|
||||
} else {
|
||||
peer.sock = peerSock
|
||||
peer.lastSeen.set(now)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun registerWithLocalPeers(node: Node) {
|
||||
if (!node.config.dropMulticast) {
|
||||
sendRegister(node, NatType.NoNat, node.multicastSock, SDLanConfig.BROADCASTMAC)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendRegister(
|
||||
node: Node,
|
||||
natType: NatType,
|
||||
sock: SDLanSock,
|
||||
mac: Mac,
|
||||
) {
|
||||
if (!node.config.allowP2P) {
|
||||
TerminalLogger.debugf { "skipping REGISTER as p2p is disabled" }
|
||||
return
|
||||
}
|
||||
|
||||
if (!node.aes.isAuthorized()) {
|
||||
TerminalLogger.debugf { "skipping REGISTER as not authed" }
|
||||
return
|
||||
}
|
||||
|
||||
val register = SDLRegister.newBuilder()
|
||||
.setNetworkId(node.networkID.get())
|
||||
.setSrcMac(node.deviceConfig.mac)
|
||||
.setDstMac(mac)
|
||||
.build()
|
||||
val msg = encodeToUDPMessage(register, PacketType.Register)
|
||||
|
||||
sendToSock(node, msg, sock)
|
||||
|
||||
// TODO: need guess port
|
||||
}
|
||||
|
||||
suspend fun sendPacketToNet(node: Node, dstmac: Mac, content: List<Byte>, size: Long) {
|
||||
val destination = findPeerDestination(node, dstmac, size)
|
||||
TerminalLogger.debugf { "send PACKET to ${destination}" }
|
||||
sendToSock(node, content, destination)
|
||||
}
|
||||
|
||||
|
||||
suspend fun findPeerDestination(node: Node, dstmac: Mac, size: Long): SDLanSock {
|
||||
var is_p2p: Boolean = false
|
||||
var is_multicast: Boolean = false
|
||||
var result: SDLanSock
|
||||
if (isMultiBroadcast(dstmac)) {
|
||||
node.stats.txSup.addAndGet(size)
|
||||
node.stats.txBroadcast.addAndGet(size)
|
||||
result = node.config.supernode
|
||||
is_multicast = true
|
||||
} else {
|
||||
val peer = node.knownPeers.get(dstmac)
|
||||
if (peer == null) {
|
||||
node.stats.txSup.addAndGet(size)
|
||||
result = node.config.supernode
|
||||
} else {
|
||||
val now = System.currentTimeMillis()/1000
|
||||
if (now - peer.lastP2P.get() >= peer.timeout/2) {
|
||||
TerminalLogger.warning { "last p2p is too old, deleting from known hosts" }
|
||||
node.knownPeers.remove(dstmac)
|
||||
node.stats.txSup.addAndGet(size)
|
||||
result = node.config.supernode
|
||||
} else {
|
||||
is_p2p = true
|
||||
node.stats.txP2P.addAndGet(size)
|
||||
result = peer.sock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!is_p2p && !is_multicast) {
|
||||
TerminalLogger.debugf { "check_query_peer_info" }
|
||||
checkQueryPeerInfo(node, dstmac)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun checkQueryPeerInfo(node: Node, dstmac: Mac) {
|
||||
val now = System.currentTimeMillis()/1000
|
||||
val peer = node.pendingPeers.get(dstmac)
|
||||
|
||||
val needSendQuery: Boolean
|
||||
if (peer != null) {
|
||||
if (now - peer.lastSentQuery.get() > SDLanConfig.RegisterInterval) {
|
||||
needSendQuery = true
|
||||
peer.lastSentQuery.set(now)
|
||||
} else {
|
||||
needSendQuery = false
|
||||
}
|
||||
} else {
|
||||
val sock = SDLanSock(IPFamily.IPV4, 0, ByteArray(4))
|
||||
val peer = EdgePeer(
|
||||
0,
|
||||
node.deviceConfig.ip.netBitLen,
|
||||
sock,
|
||||
null,
|
||||
now,
|
||||
)
|
||||
node.pendingPeers[dstmac] = peer
|
||||
needSendQuery = true
|
||||
}
|
||||
|
||||
if (needSendQuery) {
|
||||
TerminalLogger.debugf { "send query for ${macToString(dstmac)}" }
|
||||
registerWithLocalPeers(node)
|
||||
sendQueryPeer(node, dstmac)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun sendQueryPeer(node: Node, dstmac: Mac) {
|
||||
if (!node.aes.isAuthorized()) {
|
||||
TerminalLogger.errorf { "not authed for send query" }
|
||||
return
|
||||
}
|
||||
val query = SDLQueryInfo.newBuilder()
|
||||
.setDstMac(dstmac)
|
||||
.build()
|
||||
val msg = encodeToTcpMessage(query, node.getNextPacketID(), PacketType.QueryInfo)
|
||||
node.toSocket.send(msg.toByteArray())
|
||||
}
|
||||
|
||||
fun formEthernetPacket(srcmac: ByteArray, dstmac: ByteArray, data: ByteArray): ByteBuffer {
|
||||
val buffer = ByteBuffer.allocate(14 + data.size + 4).order(ByteOrder.BIG_ENDIAN)
|
||||
buffer.put(dstmac)
|
||||
buffer.put(srcmac)
|
||||
buffer.putShort(EtherType.IPV4)
|
||||
buffer.put(data)
|
||||
buffer.flip()
|
||||
return buffer
|
||||
}
|
||||
|
||||
// frommac is self's mac
|
||||
// fromip is self ip
|
||||
// queryip is the targetip
|
||||
suspend fun sendArpRequest(node: Node, queryip: Int) {
|
||||
println(1)
|
||||
val frommac = node.mac.toByteArray()
|
||||
val fromip = node.deviceConfig.ip.netAddr
|
||||
val buffer = ByteBuffer.allocate(64).order(ByteOrder.BIG_ENDIAN)
|
||||
buffer.put(ByteArray(6){0xff.toByte()})
|
||||
buffer.put(frommac)
|
||||
println(2)
|
||||
// println("src mac: ${macToString(node.mac)}")
|
||||
buffer.putShort(EtherType.ARP)
|
||||
buffer.putShort(EtherType.HWTYPE_ETH)
|
||||
buffer.putShort(EtherType.IPV4)
|
||||
|
||||
buffer.put(6)
|
||||
buffer.put(4)
|
||||
buffer.putShort(EtherType.ARP_REQUEST)
|
||||
buffer.put(frommac)
|
||||
buffer.putInt(fromip)
|
||||
buffer.put(ByteArray(6))
|
||||
buffer.putInt(queryip)
|
||||
|
||||
buffer.flip()
|
||||
println(3)
|
||||
|
||||
val size = buffer.remaining()
|
||||
|
||||
println(3.1)
|
||||
val output = node.aes.encrypt(buffer)
|
||||
println(3.2)
|
||||
|
||||
println(4)
|
||||
if (output != null) {
|
||||
val d = output.toByteString()
|
||||
val data = SDLData.newBuilder()
|
||||
.setIsP2P(true)
|
||||
.setNetworkId(node.networkID.get())
|
||||
.setTtl(2)
|
||||
.setSrcMac(frommac.toByteString())
|
||||
.setDstMac(SDLanConfig.BROADCASTMAC)
|
||||
.setData(d)
|
||||
.build()
|
||||
println(5)
|
||||
val msg = encodeToUDPMessage(data, PacketType.Data)
|
||||
sendPacketToNet(node, SDLanConfig.BROADCASTMAC, msg, size.toLong())
|
||||
}
|
||||
println(6)
|
||||
}
|
||||
|
||||
suspend fun handleTcpCommand(node: Node, cmdtype: Byte, cmdprotobuf: ByteArray) {}
|
||||
|
||||
suspend fun handlePacketPeerInfo(node: Node, content: ByteArray) {
|
||||
val pinfo: SDLPeerInfo
|
||||
try {
|
||||
pinfo = SDLPeerInfo.parseFrom(content)
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf { "failed to decode peer info: $e"}
|
||||
return
|
||||
}
|
||||
|
||||
if (pinfo.dstMac == SDLanConfig.BROADCASTMAC) {
|
||||
// pong from sn
|
||||
return
|
||||
}
|
||||
|
||||
val remoteNat = NatType.fromUByte(pinfo.v4Info.natType.toUByte())
|
||||
val pending = node.pendingPeers.get(pinfo.dstMac)
|
||||
if (pending == null) {
|
||||
TerminalLogger.debugf { "Rx PEERINFO unknown peer: ${macToString(pinfo.dstMac)}" }
|
||||
return
|
||||
}
|
||||
pending.sock = SDLanSock(IPFamily.IPV4, pinfo.v4Info.port, pinfo.v4Info.v4.toByteArray())
|
||||
pending.natType = remoteNat
|
||||
TerminalLogger.debugf { "Rx PEERINFO for ${macToString(pinfo.dstMac)} is at ${pending.sock}" }
|
||||
sendRegister(node, remoteNat, pending.sock, pinfo.dstMac)
|
||||
}
|
||||
|
||||
suspend fun handleTcpEvent(node: Node, event: EventType, cmdprotobuf: ByteArray) {
|
||||
when(event) {
|
||||
EventType.SendRegister -> {
|
||||
val reg: SDLSendRegisterEvent
|
||||
try {
|
||||
reg = SDLSendRegisterEvent.parseFrom(cmdprotobuf)
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf {"failed to decode SendRegisterEvent: $e"}
|
||||
return
|
||||
}
|
||||
val remoteNat = NatType.fromUByte(reg.natType.toUByte())
|
||||
val ip = byteArrayOf(
|
||||
(reg.natIp ushr 24).and(0xff).toByte(),
|
||||
(reg.natIp ushr 16).and(0xff).toByte(),
|
||||
(reg.natIp ushr 8).and(0xff).toByte(),
|
||||
(reg.natIp).and(0xff).toByte(),
|
||||
)
|
||||
checkPeerRegistrationNeeded(node,false, reg.dstMac, remoteNat, SDLanSock(IPFamily.IPV4, reg.natPort, ip))
|
||||
}
|
||||
else -> {
|
||||
TerminalLogger.warning { "unhandled event: $event" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ipInt2ByteArray(ip: Int): ByteArray {
|
||||
return byteArrayOf(
|
||||
(ip ushr 24).and(0xff).toByte(),
|
||||
(ip ushr 16).and(0xff).toByte(),
|
||||
(ip ushr 8).and(0xff).toByte(),
|
||||
ip.and(0xff).toByte(),
|
||||
)
|
||||
}
|
||||
305
app/src/main/java/com/jihe/punchnet/sdlan/network/iface.kt
Normal file
@ -0,0 +1,305 @@
|
||||
package com.jihe.punchnet.sdlan.network
|
||||
|
||||
import com.google.protobuf.kotlin.toByteString
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLData
|
||||
import com.jihe.punchnet.sdlan.config.EtherType
|
||||
import com.jihe.punchnet.sdlan.config.SDLanConfig
|
||||
import com.jihe.punchnet.sdlan.config.toIPV4String
|
||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
||||
import com.jihe.punchnet.sdlan.utils.ArpHdr
|
||||
import com.jihe.punchnet.sdlan.utils.EthHdr
|
||||
import com.jihe.punchnet.sdlan.utils.ipToString
|
||||
import com.jihe.punchnet.sdlan.utils.isMultiBroadcast
|
||||
import com.jihe.punchnet.sdlan.utils.macToString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.net.Inet4Address
|
||||
import java.net.Socket
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.zip.CRC32
|
||||
|
||||
interface Iface {
|
||||
suspend fun doInit()
|
||||
suspend fun recv(): ByteArray
|
||||
suspend fun send(content: ByteArray)
|
||||
suspend fun reload_config(config: DeviceConfig)
|
||||
|
||||
suspend fun handleDataFromNet(node: Node, data: ByteArray)
|
||||
suspend fun handleDataFromDevice(node: Node, data: ByteArray)
|
||||
}
|
||||
|
||||
interface IfaceTap: Iface {
|
||||
override suspend fun handleDataFromNet(node: Node, data: ByteArray) {
|
||||
TerminalLogger.debugf { "in tap mode" }
|
||||
}
|
||||
|
||||
override suspend fun handleDataFromDevice(node: Node, data: ByteArray) {
|
||||
}
|
||||
}
|
||||
|
||||
interface IfaceTun: Iface {
|
||||
/*
|
||||
override suspend fun recv(): ByteArray
|
||||
override suspend fun send(content: ByteArray)
|
||||
override suspend fun reload_config(config: DeviceConfig)
|
||||
*/
|
||||
val arpTable: ARPTable
|
||||
val arpWaitList: ARPWaitList
|
||||
|
||||
override suspend fun doInit() {
|
||||
arpTable.agingARP()
|
||||
}
|
||||
|
||||
override suspend fun handleDataFromNet(node: Node, data: ByteArray) {
|
||||
TerminalLogger.debugf { "in tun mode" }
|
||||
var size = data.size
|
||||
if (size < 14) {
|
||||
TerminalLogger.errorf { "packet size error: ${data.size}" }
|
||||
}
|
||||
val buff = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN)
|
||||
val dstmac = ByteArray(6)
|
||||
val srcmac = ByteArray(6)
|
||||
buff.get(dstmac)
|
||||
buff.get(srcmac)
|
||||
val k = dstmac.toByteString()
|
||||
val etherType = buff.getShort()
|
||||
|
||||
if (k != node.mac && !isMultiBroadcast(k)) {
|
||||
TerminalLogger.errorf { "packet to ${macToString(k)} is not direct to us"}
|
||||
return
|
||||
}
|
||||
|
||||
if (etherType == EtherType.ARP) {
|
||||
if (size < 42) {
|
||||
TerminalLogger.errorf { "packet size error: invalid arp length" }
|
||||
return
|
||||
}
|
||||
val arp = ArpHdr.parseFrom(EthHdr(dstmac, srcmac, etherType), buff)
|
||||
when (arp.opcode) {
|
||||
EtherType.ARP_REQUEST -> {
|
||||
TerminalLogger.debugf { "got arp request: dstmac=${macToString(dstmac.toByteString())}" }
|
||||
|
||||
if (!dstmac.contentEquals(SDLanConfig.BROADCASTMAC_BYTEARRAY)) {
|
||||
TerminalLogger.errorf { "arp request should be broadcast" }
|
||||
}
|
||||
if (arp.dipaddr == node.deviceConfig.ip.netAddr) {
|
||||
val macBytes = node.mac.toByteArray()
|
||||
arpTable.addToARPTable(arp.sipaddr, arp.shwaddr)
|
||||
arp.opcode = EtherType.ARP_REPLY
|
||||
arp.dhwaddr = arp.shwaddr
|
||||
arp.shwaddr = macBytes
|
||||
arp.ethhdr.src = macBytes
|
||||
arp.ethhdr.dest = arp.dhwaddr
|
||||
|
||||
arp.dipaddr = arp.sipaddr
|
||||
arp.sipaddr = node.deviceConfig.ip.netAddr
|
||||
|
||||
val bytes = arp.marshalToBytes()
|
||||
val encrypted = node.aes.encrypt(bytes)
|
||||
if (encrypted != null) {
|
||||
val dstmac = arp.dhwaddr.toByteString()
|
||||
val data = SDLData.newBuilder()
|
||||
.setIsP2P(true)
|
||||
.setTtl(2)
|
||||
.setNetworkId(node.networkID.get())
|
||||
.setSrcMac(node.mac)
|
||||
.setDstMac(dstmac)
|
||||
.setData(encrypted.toByteString())
|
||||
.build()
|
||||
|
||||
TerminalLogger.debugf {
|
||||
"send arp reply to ${macToString(dstmac)}"
|
||||
}
|
||||
val content = encodeToUDPMessage(data, PacketType.Data)
|
||||
sendPacketToNet(node, dstmac, content, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
EtherType.ARP_REPLY -> {
|
||||
TerminalLogger.debugf { "got arp shwaddr: ${macToString(arp.shwaddr.toByteString())}" }
|
||||
TerminalLogger.debugf { "got arp dhwaddr: ${macToString(arp.dhwaddr.toByteString())}" }
|
||||
|
||||
TerminalLogger.debugf { "got arp sipaddr: ${ipToString(arp.sipaddr)}" }
|
||||
TerminalLogger.debugf { "got arp dipaddr: ${ipToString(arp.dipaddr)}" }
|
||||
|
||||
println("self_ip = ${ipToString(node.deviceConfig.ip.netAddr)}")
|
||||
if (arp.dipaddr == node.deviceConfig.ip.netAddr) {
|
||||
TerminalLogger.debugf {"arp reply"}
|
||||
arpTable.addToARPTable(arp.sipaddr, arp.shwaddr)
|
||||
arpWaitList.arpArrived(node, arp.sipaddr, arp.shwaddr.toByteString())
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
TerminalLogger.errorf { "unknown ARP type" }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (data.size < 34) {
|
||||
TerminalLogger.errorf { "packet size error: ${data.size}" }
|
||||
return
|
||||
}
|
||||
|
||||
val rest = data.sliceArray(14..data.size-1)
|
||||
val buffer = ByteBuffer.wrap(rest, 12, 8)
|
||||
val srcip = buffer.getInt()
|
||||
if (!isMultiBroadcast(srcmac.toByteString())) {
|
||||
arpTable.addToARPTable(srcip, srcmac)
|
||||
}
|
||||
|
||||
send(rest)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleDataFromDevice(node: Node, data: ByteArray) {
|
||||
if (data.size < 20) {
|
||||
TerminalLogger.errorf { "too short ip packet" }
|
||||
return
|
||||
}
|
||||
if (!node.aes.isAuthorized()) {
|
||||
TerminalLogger.infof { "dropping tun packet due to not authed" }
|
||||
return
|
||||
}
|
||||
val buffer = ByteBuffer.wrap(data, 12, 8).order(ByteOrder.BIG_ENDIAN)
|
||||
val srcip = buffer.getInt()
|
||||
val dstip = buffer.getInt()
|
||||
|
||||
TerminalLogger.debugf { "got ${data.size} bytes from tun" }
|
||||
|
||||
if (!node.config.allowRouting && (srcip != node.deviceConfig.ip.netAddr)) {
|
||||
TerminalLogger.infof { "dropping routed packet from tun" }
|
||||
return
|
||||
}
|
||||
|
||||
val arpinfo = arpTable.getMacFromIP(dstip)
|
||||
if (arpinfo == null) {
|
||||
println("arp info is null")
|
||||
// arp not found
|
||||
arpWaitList.addToWaitList(dstip, data)
|
||||
TerminalLogger.debugf { "added to wait list" }
|
||||
sendArpRequest(node, dstip)
|
||||
TerminalLogger.debugf { "sent arp request" }
|
||||
} else {
|
||||
println("mac is ${macToString(arpinfo.toByteString())}")
|
||||
val buffer = arpinfo + node.mac.toByteArray() +
|
||||
byteArrayOf((EtherType.IPV4.toInt() shr 8).toByte(), EtherType.IPV4.toByte()) +
|
||||
data
|
||||
|
||||
val crc = CRC32()
|
||||
crc.update(buffer)
|
||||
val cksum = ByteBuffer.allocate(4)
|
||||
.putInt(crc.value.toInt())
|
||||
.array()
|
||||
val packet = buffer + cksum
|
||||
val size = packet.size
|
||||
val encrypted = node.aes.encrypt(packet)
|
||||
if (encrypted != null) {
|
||||
val mac = arpinfo.toByteString()
|
||||
val data = SDLData.newBuilder()
|
||||
.setIsP2P(true)
|
||||
.setNetworkId(node.networkID.get())
|
||||
.setTtl(2)
|
||||
.setSrcMac(node.mac)
|
||||
.setDstMac(mac)
|
||||
.setData(encrypted.toByteString())
|
||||
.build()
|
||||
val msg = encodeToUDPMessage(data, PacketType.Data)
|
||||
sendPacketToNet(node, mac, msg, size.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IfaceMock: IfaceTun {
|
||||
val deviceName = "dev0"
|
||||
override val arpTable = ARPTable()
|
||||
override val arpWaitList = ARPWaitList()
|
||||
|
||||
val sock = Socket(Inet4Address.getByName("127.0.0.1"), 1234)
|
||||
|
||||
val input: DataInputStream = DataInputStream(sock.getInputStream())
|
||||
val output: DataOutputStream = DataOutputStream(sock.getOutputStream())
|
||||
|
||||
var config: DeviceConfig = DeviceConfig(0)
|
||||
|
||||
override suspend fun recv(): ByteArray {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
val size = input.readInt()
|
||||
var result = ByteArray(size)
|
||||
input.read(result)
|
||||
result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun send(content: ByteArray) {
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val size = content.size
|
||||
TerminalLogger.debugf {"sending ${content.size} bytes to tun"}
|
||||
output.writeInt(size)
|
||||
output.write(content)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reload_config(config: DeviceConfig) {
|
||||
this.config = config
|
||||
val ip = ipInt2ByteArray(config.ip.netAddr).toIPV4String()
|
||||
var command = "ifconfig ${deviceName} $ip"
|
||||
command += " netmask ${ipInt2ByteArray(netmaskBit2Int(config.ip.netBitLen)).toIPV4String()}"
|
||||
command += " mtu 1400"
|
||||
command += " up"
|
||||
|
||||
TerminalLogger.debugf { "executing command: ${command}" }
|
||||
runCommand(command)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.execute(): Process {
|
||||
val runtime = Runtime.getRuntime()
|
||||
return runtime.exec(this)
|
||||
}
|
||||
|
||||
fun Process.text(): String {
|
||||
val inputStream = this.inputStream
|
||||
val insReader = InputStreamReader(inputStream)
|
||||
val bufReader = BufferedReader(insReader)
|
||||
|
||||
var output = ""
|
||||
var line: String? = ""
|
||||
while(line != null) {
|
||||
line = bufReader.readLine()
|
||||
output += line + "\n"
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
fun runCommand(command: String): String {
|
||||
val process = command.execute()
|
||||
val exitCode = process.waitFor()
|
||||
val text = process.text()
|
||||
println("exit code: $exitCode")
|
||||
return text
|
||||
}
|
||||
|
||||
fun netmaskBit2Int(len: Byte): Int {
|
||||
var res = 0
|
||||
for (i in 1..len) {
|
||||
res = res or (1 shl (32 - i))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/*
|
||||
fun macToString(mac: Mac): String {
|
||||
var result = mutableListOf<String>()
|
||||
for (m in mac) {
|
||||
result.add(m.toString(16))
|
||||
}
|
||||
return result.joinToString(":")
|
||||
}
|
||||
*/
|
||||
590
app/src/main/java/com/jihe/punchnet/sdlan/network/libs.kt
Normal file
@ -0,0 +1,590 @@
|
||||
package com.jihe.punchnet.sdlan.network
|
||||
|
||||
import android.util.Log
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLDevAddr
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuper
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuperAck
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuperNak
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLStunProbeReply
|
||||
import com.jihe.punchnet.sdlan.config.Arguments
|
||||
import com.jihe.punchnet.sdlan.config.RSAConfig
|
||||
import com.jihe.punchnet.sdlan.config.SDLanConfig
|
||||
import com.jihe.punchnet.sdlan.config.SDLanMulticastConfig
|
||||
import com.jihe.punchnet.sdlan.config.toIPV4String
|
||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
||||
import com.jihe.punchnet.sdlan.utils.RSA
|
||||
import com.jihe.punchnet.sdlan.utils.UniqueNodeID
|
||||
import com.jihe.punchnet.sdlan.utils.ipToString
|
||||
import com.jihe.punchnet.sdlan.utils.macToString
|
||||
import com.jihe.punchnet.sdlan.utils.parseScoketAddressV4FromString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.selects.select
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.net.SocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.file.Paths
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
suspend fun onMessage(data: SDLanTCP) {
|
||||
val node = Node.getInstance()
|
||||
|
||||
TerminalLogger.debugf {"message received"}
|
||||
when(data.packetType) {
|
||||
PacketType.RegisterSuperACK -> {
|
||||
println("11")
|
||||
val ack = SDLRegisterSuperAck.parseFrom(data.currentPacket)
|
||||
// TerminalLogger.debugf { "got register super ack: $ack" }
|
||||
println("ack is ${ack}")
|
||||
println("ack's key is ${ack.aesKey.size()}")
|
||||
|
||||
val aeskey = node.rsa.decrypt(ack.aesKey.toByteArray())
|
||||
if (aeskey == null) {
|
||||
println("aes key error")
|
||||
return
|
||||
}
|
||||
println("decrypted aes is: ${aeskey.contentToString()}")
|
||||
node.aes.setSecret(aeskey)
|
||||
val ipStr = ipToString(ack.devAddr.netAddr)
|
||||
TerminalLogger.debugf { "assigned ip: $ipStr" }
|
||||
node.deviceConfig.ip.netAddr = ack.devAddr.netAddr
|
||||
node.deviceConfig.ip.netBitLen = ack.devAddr.netBitLen.toByte()
|
||||
|
||||
node.iface?.reload_config(node.deviceConfig)
|
||||
|
||||
node.networkID.set(ack.devAddr.networkId)
|
||||
// println("got aes key: ${aeskey.toList()}, length is ${aeskey.size}")
|
||||
node.sendStunRequest()
|
||||
|
||||
CoroutineScope(Dispatchers.Default).async {
|
||||
node.probeNatType()
|
||||
TerminalLogger.debugf { "nat type is ${node.nat_type}"}
|
||||
}
|
||||
}
|
||||
PacketType.RegisterSuperNAK -> {
|
||||
println("21")
|
||||
val nak = SDLRegisterSuperNak.parseFrom(data.currentPacket)
|
||||
TerminalLogger.debugf { "got register super nak: $nak" }
|
||||
val nakcode = NakMsgCode.fromByte(nak.errorCode.toByte())
|
||||
when(nakcode) {
|
||||
NakMsgCode.InvalidToken, NakMsgCode.NodeDisabled-> {
|
||||
node.startStopChannel.send(StartStopChanInfo(StartStopFlag.IsStop, null))
|
||||
exitProcess(-1)
|
||||
}
|
||||
else -> {
|
||||
node.startStopChannel.send(StartStopChanInfo(StartStopFlag.IsStop, null))
|
||||
}
|
||||
}
|
||||
node.aes.setSecret(null)
|
||||
}
|
||||
PacketType.Command -> {
|
||||
TerminalLogger.debugf {"Command received"}
|
||||
if (data.currentPacket.size < 1) {
|
||||
TerminalLogger.errorf { "malformed command received"}
|
||||
return
|
||||
}
|
||||
handleTcpCommand(node, data.currentPacket[0], data.currentPacket.sliceArray(1..data.currentPacket.size-1))
|
||||
}
|
||||
PacketType.PeerInfo -> {
|
||||
TerminalLogger.debugf { "PeerInfo received" }
|
||||
handlePacketPeerInfo(node, data.currentPacket)
|
||||
}
|
||||
PacketType.Event -> {
|
||||
TerminalLogger.debugf { "Event received" }
|
||||
if (data.currentPacket.size < 1) {
|
||||
TerminalLogger.errorf { "malformed event received"}
|
||||
return
|
||||
}
|
||||
val eventType = EventType.fromByte(data.currentPacket[0])
|
||||
|
||||
val size = data.currentPacket.size
|
||||
val content = data.currentPacket.sliceArray(1..size-1)
|
||||
handleTcpEvent(node, eventType, content)
|
||||
}
|
||||
PacketType.Pong -> {
|
||||
TerminalLogger.debugf { "tcp Pong Received" }
|
||||
// node.ping_to_sn()
|
||||
}
|
||||
else -> {
|
||||
println("error packet type: ${data.packetType.toUByte()}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun run_sdlan(iface: Iface, argument: Arguments) {
|
||||
UniqueNodeID.setBaseDir(argument.baseDir)
|
||||
val edgeUUID = UniqueNodeID.getUUID()
|
||||
val config = parseConfig(edgeUUID, argument)
|
||||
if (config == null) {
|
||||
TerminalLogger.errorf {
|
||||
"parse config failed"
|
||||
}
|
||||
return
|
||||
}
|
||||
val toSocket = Channel<ByteArray>(100)
|
||||
val start_stop_channel = Channel<StartStopChanInfo>(100)
|
||||
initEdge(iface, argument.token, config, toSocket, start_stop_channel)
|
||||
|
||||
val tcp = argument.tcp.split(":")
|
||||
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
|
||||
val node = Node.getInstance()
|
||||
|
||||
|
||||
val before = ubyteArrayOf(126u,162u,25u,63u,148u,147u,198u,41u,69u,165u,149u,101u,153u,82u,190u,21u,48u,120u,26u,64u,142u,103u,159u,60u,47u,129u,176u,17u,232u,210u,36u,56u)
|
||||
val encrypted = node.rsa.encrypt(before.toByteArray(), use_private_key = false)
|
||||
println("encrypted: ${encrypted.contentToString()}")
|
||||
val origin = node.rsa.decrypt(encrypted, use_pub_key = false)
|
||||
TerminalLogger.debugf { "got encrypted ${encrypted.size}"}
|
||||
TerminalLogger.debugf { "got origin ${origin.contentToString()}"}
|
||||
|
||||
TerminalLogger.debugf { "self mac: ${macToString(node.mac)}"}
|
||||
|
||||
|
||||
val onConnected: suspend (Socket, Int?) -> Unit = { socket, pktID ->
|
||||
println("connected callback")
|
||||
val clientid = UniqueNodeID.getUUID()
|
||||
println("client id is $clientid")
|
||||
|
||||
val registerSuper = SDLRegisterSuper.newBuilder()
|
||||
.setVersion(1)
|
||||
.setToken(node.token)
|
||||
.setInstalledChannel("kotlin")
|
||||
.setClientId(clientid)
|
||||
.setDevAddr(
|
||||
SDLDevAddr.newBuilder().setMac(node.mac)
|
||||
.setNetAddr(0)
|
||||
.setNetBitLen(0)
|
||||
.setNetworkId(0)
|
||||
.build()
|
||||
)
|
||||
.setPubKey(node.rsa.getPublicKeyString())
|
||||
.build()
|
||||
|
||||
val packetID = pktID ?: node.getNextPacketID()
|
||||
|
||||
val msg = encodeToTcpMessage(registerSuper, packetID, PacketType.RegisterSuper)
|
||||
// TerminalLogger.debugf{"sent register super: $registerSuper"}
|
||||
toSocket.send(msg.toByteArray())
|
||||
TerminalLogger.debugf{"sent register super!"}
|
||||
}
|
||||
/*
|
||||
val onMessage: suspend (SDLanTCP) -> Unit = { data ->
|
||||
}
|
||||
*/
|
||||
|
||||
CoroutineScope((Dispatchers.IO)).async {
|
||||
while(true) {
|
||||
val data = node.iface?.recv()
|
||||
Log.d("SDLAN", "async receive data from iface: ${data?.size} bytes")
|
||||
if (data == null) {
|
||||
//delay(1000)
|
||||
println("got data is null")
|
||||
continue
|
||||
}
|
||||
if (data.isEmpty()) {
|
||||
println("got data size 0")
|
||||
// delay(1000)
|
||||
continue
|
||||
}
|
||||
Log.d("SDLAN", "handle data form device starts")
|
||||
node.iface?.handleDataFromDevice(node, data)
|
||||
Log.d("SDLAN", "handle data form device stops")
|
||||
}
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).async {
|
||||
initTCPConn(
|
||||
tcp[0], tcp[1].toInt(),
|
||||
start_stop_channel,
|
||||
AtomicLong(now),
|
||||
AtomicBoolean(false),
|
||||
toSocket as ReceiveChannel<ByteArray>,
|
||||
onConnected,
|
||||
::onMessage,
|
||||
suspend {
|
||||
node.aes.setSecret(null)
|
||||
},
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
start_stop_channel.send(StartStopChanInfo(StartStopFlag.IsStart, null))
|
||||
|
||||
val cancel = Channel<Boolean>(100)
|
||||
runEdgeLoop(node, cancel)
|
||||
|
||||
while(true) {
|
||||
TerminalLogger.debugf {"ping to sn"}
|
||||
delay(SDLanConfig.TCP_PING_TIME*1000)
|
||||
node.ping_to_sn()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun runEdgeLoop(node: Node, cancel: ReceiveChannel<Boolean>) {
|
||||
node.ping_to_sn()
|
||||
CoroutineScope(Dispatchers.Default).async {
|
||||
loopSocketV4(node, node.udpSockV4, cancel)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loopSocketV4(node: Node, sock: SDLanSocket, cancel: ReceiveChannel<Boolean>) {
|
||||
val job_stun_request = CoroutineScope(Dispatchers.Default).async {
|
||||
while(true) {
|
||||
delay(10_000)
|
||||
node.sendStunRequest()
|
||||
}
|
||||
}
|
||||
|
||||
val job_handle_packet = CoroutineScope(Dispatchers.Default).async {
|
||||
while(true) {
|
||||
readAndParsePacket(node, sock)
|
||||
}
|
||||
}
|
||||
|
||||
select<Unit> {
|
||||
job_stun_request.onAwait() {
|
||||
|
||||
}
|
||||
cancel.onReceive() {
|
||||
|
||||
}
|
||||
job_handle_packet.onAwait() {
|
||||
|
||||
}
|
||||
}
|
||||
job_stun_request.cancelAndJoin()
|
||||
job_handle_packet.cancelAndJoin()
|
||||
}
|
||||
|
||||
suspend fun readAndParsePacket(node: Node, sock: SDLanSocket) {
|
||||
val packet = sock.receive()
|
||||
val from = packet.socketAddress
|
||||
|
||||
if (packet.length < 1) {
|
||||
TerminalLogger.errorf { "got zero-sized packet"}
|
||||
return
|
||||
}
|
||||
val data = ByteBuffer.wrap(packet.data, 0, packet.length)
|
||||
// val data = packet.data.toByteString(0, packet.length)
|
||||
handleAPacket(node, from, data)
|
||||
}
|
||||
|
||||
suspend fun handleAPacket(node: Node, from: SocketAddress, data: ByteBuffer) {
|
||||
val pktType = PacketType.fromValue(data.get().toUByte())
|
||||
if (pktType == null) {
|
||||
TerminalLogger.errorf { "invalid packet type" }
|
||||
return
|
||||
}
|
||||
// val buffer = ByteBuffer.wrap(data, 1, size-1)
|
||||
|
||||
when (pktType) {
|
||||
PacketType.Data -> {
|
||||
TerminalLogger.debugf { "got DATA" }
|
||||
if (!node.aes.isAuthorized()) {
|
||||
TerminalLogger.errorf { "drop DATA before authed" }
|
||||
return
|
||||
}
|
||||
if (from is InetSocketAddress) {
|
||||
TerminalLogger.debugf {"got data"}
|
||||
val sock = SDLanSock(IPFamily.IPV4, from.port, from.address.address)
|
||||
handlePacketData(node, data, sock)
|
||||
}
|
||||
}
|
||||
PacketType.StunProbeReply -> {
|
||||
try {
|
||||
val reply = SDLStunProbeReply.parseFrom(data)
|
||||
node.natProbeCookie.sendMessage(reply.cookie, reply)
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf { "failed to decode Probe Reply"}
|
||||
}
|
||||
}
|
||||
PacketType.StunReply -> {
|
||||
TerminalLogger.debugf { "got stun reply" }
|
||||
return
|
||||
}
|
||||
PacketType.Register -> {
|
||||
if (from is InetSocketAddress) {
|
||||
val sock = SDLanSock(IPFamily.IPV4, from.port, from.address.address)
|
||||
handlePacketRegister(node, data, sock)
|
||||
}
|
||||
}
|
||||
PacketType.RegisterACK -> {
|
||||
TerminalLogger.debugf { "got REGISTERACK" }
|
||||
if (!node.aes.isAuthorized()) {
|
||||
TerminalLogger.errorf { "drop REGISTERACK before authed" }
|
||||
return
|
||||
}
|
||||
if (from is InetSocketAddress) {
|
||||
val sock = SDLanSock(IPFamily.IPV4, from.port, from.address.address)
|
||||
handlePacketRegisterACK(node, data, sock)
|
||||
}
|
||||
|
||||
}
|
||||
else -> {
|
||||
TerminalLogger.debugf {"ignore packet type: $pktType"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun initEdge(iface: Iface, token: String, config: NodeConfig, toSocket: SendChannel<ByteArray>, startStopChannel: SendChannel<StartStopChanInfo>) {
|
||||
val rsa = RSA.getRSA()
|
||||
val pathname = Paths.get(config.baseDir, RSAConfig.BASE_DIR).pathString
|
||||
Log.d("DIR", "pathname = $pathname")
|
||||
rsa.generateKeyPair(pathname)
|
||||
//rsa.generateKeyPair(Path.of(config.baseDir, RSAConfig.BASE_DIR).name)
|
||||
|
||||
val sockV4 = SDLanSocket("0.0.0.0", config.localPort)
|
||||
|
||||
var sockMulticast: SDLanSocket? = null
|
||||
if (!config.dropMulticast) {
|
||||
sockMulticast = SDLanSocket(SDLanMulticastConfig.MULTICAST_V4.toIPV4String(), SDLanMulticastConfig.MULTICAST_PORT)
|
||||
}
|
||||
|
||||
Node.initialize(
|
||||
config,
|
||||
sockV4,
|
||||
sockMulticast,
|
||||
token,
|
||||
rsa,
|
||||
iface,
|
||||
toSocket,
|
||||
startStopChannel,
|
||||
)
|
||||
val instance = Node.getInstance()
|
||||
println(instance)
|
||||
}
|
||||
|
||||
suspend fun initTCPConn(
|
||||
tcpHost: String,
|
||||
tcpPort: Int,
|
||||
start_stop: Channel<StartStopChanInfo>,
|
||||
pong_time: AtomicLong,
|
||||
connected: AtomicBoolean,
|
||||
toSocket: ReceiveChannel<ByteArray>,
|
||||
onConnected: suspend (stream: Socket, pktID: Int?)->Unit,
|
||||
onMessage: suspend (SDLanTCP)->Unit,
|
||||
onDisconnected: suspend ()->Unit,
|
||||
connectingChan: SendChannel<ConnectingState>?
|
||||
) {
|
||||
var started: Boolean = false
|
||||
var startPktID: Int? = null
|
||||
|
||||
while(true) {
|
||||
connectingChan?.send(ConnectingState.NotConnected)
|
||||
|
||||
if (!started) {
|
||||
while(true) {
|
||||
val startStopInfo = start_stop.receive()
|
||||
if (startStopInfo.flag == StartStopFlag.IsStart) {
|
||||
started = true
|
||||
startPktID = startStopInfo.packetID
|
||||
break
|
||||
}
|
||||
TerminalLogger.debugf {
|
||||
"start stop chan received ${startStopInfo}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectingChan?.send(ConnectingState.Connecting)
|
||||
TerminalLogger.debugf {"try connecting..."}
|
||||
|
||||
val socket: Socket
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
TerminalLogger.debugf { "connecting to $tcpHost:$tcpPort" }
|
||||
socket = Socket(tcpHost, tcpPort)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf { "failed to connect to ${tcpHost}:${tcpPort}: $e"}
|
||||
delay(3000)
|
||||
continue
|
||||
}
|
||||
|
||||
val node = Node.getInstance()
|
||||
val outIP = ByteBuffer.wrap(socket.localAddress.address).getInt()
|
||||
node.outerIPV4.set(outIP)
|
||||
|
||||
val job_read_packet = CoroutineScope(Dispatchers.IO).async {
|
||||
val input = DataInputStream(socket.getInputStream())
|
||||
try {
|
||||
println("job read packet starts")
|
||||
while(true) {
|
||||
val tcpPacket = readPacket(input)
|
||||
if (tcpPacket == null) {
|
||||
TerminalLogger.errorf {"tcp Packet is null"}
|
||||
break
|
||||
}
|
||||
onMessage(tcpPacket)
|
||||
}
|
||||
} finally {
|
||||
TerminalLogger.errorf {"input closing"}
|
||||
input.close()
|
||||
}
|
||||
}
|
||||
|
||||
val job_write_to_packet = CoroutineScope(Dispatchers.IO).async {
|
||||
val output = DataOutputStream(socket.getOutputStream())
|
||||
try {
|
||||
TerminalLogger.debugf {"job write to packet starts"}
|
||||
while(true) {
|
||||
try {
|
||||
val msg = toSocket.receive()
|
||||
TerminalLogger.debugf{"received message"}
|
||||
output.write(msg)
|
||||
} catch (e: Exception) {
|
||||
output.close()
|
||||
TerminalLogger.errorf {"failed to receive message to tcp: $e"}
|
||||
break
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
output.close()
|
||||
}
|
||||
}
|
||||
|
||||
val job_check_pong = CoroutineScope(Dispatchers.IO).async {
|
||||
println("job check pong starts")
|
||||
while(true) {
|
||||
delay(10_000)
|
||||
val now = System.currentTimeMillis()/1000
|
||||
if (connected.get() && (now-pong_time.get()>SDLanConfig.TCP_PING_TIME*2)) {
|
||||
TerminalLogger.errorf {"tcp pong check expired"}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val job_check_stop = CoroutineScope(Dispatchers.IO).async {
|
||||
println("job check stop starts")
|
||||
while(true) {
|
||||
try {
|
||||
val v = start_stop.receive()
|
||||
if (v.flag == StartStopFlag.IsStop) {
|
||||
started = false
|
||||
break
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
started = false
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
TerminalLogger.debugf { "connected" }
|
||||
onConnected(socket, startPktID)
|
||||
connectingChan?.send(ConnectingState.Connected)
|
||||
|
||||
var cancelled: Boolean = false
|
||||
select<Unit> {
|
||||
job_read_packet.onAwait() {
|
||||
println("job read packet exited")
|
||||
}
|
||||
job_write_to_packet.onAwait() {
|
||||
println("job write to packet exited")
|
||||
}
|
||||
job_check_pong.onAwait() {
|
||||
println("job check pong exited")
|
||||
}
|
||||
job_check_stop.onAwait() {
|
||||
println("job check stop exited")
|
||||
}
|
||||
}
|
||||
|
||||
job_read_packet.cancelAndJoin()
|
||||
job_write_to_packet.cancelAndJoin()
|
||||
job_check_pong.cancelAndJoin()
|
||||
job_check_stop.cancelAndJoin()
|
||||
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
fun readPacket(input: DataInputStream): SDLanTCP? {
|
||||
try {
|
||||
val size = input.readShort()
|
||||
val packetID = input.readInt().toUInt()
|
||||
val packetTypeUByte = input.readByte().toUByte()
|
||||
val packetType = PacketType.fromValue(packetTypeUByte)
|
||||
if (packetType == null) {
|
||||
TerminalLogger.errorf{"packet type not found: $packetTypeUByte"}
|
||||
return null
|
||||
}
|
||||
|
||||
if (size < 5) {
|
||||
TerminalLogger.errorf {"got input stream size error: ${size}"}
|
||||
return null
|
||||
}
|
||||
|
||||
var buffsize = size - 5
|
||||
val data = ByteArray(buffsize)
|
||||
var toread = buffsize
|
||||
while(toread > 0) {
|
||||
val sizeGot = input.read(data, (buffsize-toread), toread)
|
||||
if (sizeGot <= 0) {
|
||||
TerminalLogger.errorf {"failed to read further: got ${sizeGot}"}
|
||||
return null
|
||||
}
|
||||
toread -= sizeGot
|
||||
}
|
||||
return SDLanTCP(packetID, packetType, data)
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf { "failed to read: $e" }
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun parseConfig(nodeuuid: String, argument: Arguments): NodeConfig? {
|
||||
if (argument.sn.length == 0) {
|
||||
println("no sn is specified")
|
||||
return null
|
||||
}
|
||||
|
||||
val natServer1 = parseScoketAddressV4FromString(argument.nat_server1)
|
||||
val natServer2 = parseScoketAddressV4FromString(argument.nat_server2)
|
||||
|
||||
if (natServer1 == null || natServer2 == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val sn = parseScoketAddressV4FromString(argument.sn)
|
||||
if (sn == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return NodeConfig(
|
||||
baseDir = argument.baseDir,
|
||||
name = argument.name,
|
||||
nodeUUID = nodeuuid,
|
||||
allowRouting = argument.allowRouting,
|
||||
dropMulticast = argument.dropMulticast,
|
||||
allowP2P = argument.allowP2P,
|
||||
mtu = argument.mtu,
|
||||
tos = argument.tos,
|
||||
registerSuperInterval = SDLanConfig.RegisterSuperInterval,
|
||||
registerTTL = argument.registerTTL,
|
||||
localPort = argument.localPort,
|
||||
supernode = SDLanSock(IPFamily.IPV4, sn.port, sn.address.address),
|
||||
natServer1 = natServer1,
|
||||
natServer2 = natServer2,
|
||||
)
|
||||
}
|
||||
48
app/src/main/java/com/jihe/punchnet/sdlan/network/message.kt
Normal file
@ -0,0 +1,48 @@
|
||||
package com.jihe.punchnet.sdlan.network
|
||||
|
||||
enum class NakMsgCode(val code: Byte) {
|
||||
InvalidMsg(0),
|
||||
InvalidToken(1),
|
||||
NodeDisabled(2),
|
||||
NoIPAddress(3),
|
||||
NetworkFault(4),
|
||||
InternalFault(5);
|
||||
|
||||
fun toByte(): Byte {
|
||||
return this.code
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromByte(code: Byte): NakMsgCode {
|
||||
return when(code) {
|
||||
1.toByte() -> InvalidToken
|
||||
2.toByte() -> NodeDisabled
|
||||
3.toByte() -> NoIPAddress
|
||||
4.toByte() -> NetworkFault
|
||||
5.toByte() -> InternalFault
|
||||
else -> InvalidMsg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class EventType(val code: Byte) {
|
||||
KnownIP(1),
|
||||
DropIP(2),
|
||||
NatChanged(3),
|
||||
SendRegister(4),
|
||||
NetworkShutdown(0xff.toByte());
|
||||
|
||||
companion object {
|
||||
fun fromByte(code: Byte): EventType {
|
||||
return when(code) {
|
||||
1.toByte() -> KnownIP
|
||||
2.toByte() -> DropIP
|
||||
3.toByte() -> NatChanged
|
||||
4.toByte() -> SendRegister
|
||||
else -> NetworkShutdown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
130
app/src/main/java/com/jihe/punchnet/sdlan/network/packet_info.kt
Normal file
@ -0,0 +1,130 @@
|
||||
package com.jihe.punchnet.sdlan.network
|
||||
|
||||
import com.google.protobuf.Message
|
||||
import java.net.Inet4Address
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.SocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
enum class StartStopFlag {
|
||||
IsStart,
|
||||
IsStop,
|
||||
}
|
||||
|
||||
// 本地启动或者停止服务的信息
|
||||
class StartStopChanInfo(
|
||||
val flag: StartStopFlag,
|
||||
val packetID: Int?,
|
||||
)
|
||||
|
||||
enum class PacketType(val id: UByte) {
|
||||
Empty(0x00u),
|
||||
RegisterSuper(0x01u),
|
||||
RegisterSuperACK(0x02u),
|
||||
RegisterSuperNAK(0x04u),
|
||||
|
||||
UnRegisterSuper(0x05u),
|
||||
|
||||
QueryInfo(0x06u),
|
||||
PeerInfo(0x07u),
|
||||
|
||||
Ping(0x08u),
|
||||
Pong(0x09u),
|
||||
|
||||
Event(0x10u),
|
||||
|
||||
Command(0x11u),
|
||||
CommandACK(0x12u),
|
||||
|
||||
FlowTracer(0x15u),
|
||||
|
||||
Register(0x20u),
|
||||
RegisterACK(0x21u),
|
||||
|
||||
StunRequest(0x30u),
|
||||
StunReply(0x31u),
|
||||
|
||||
StunProbe(0x32u),
|
||||
StunProbeReply(0x33u),
|
||||
|
||||
Data(0xffu);
|
||||
|
||||
companion object {
|
||||
private val innermap = PacketType.entries.map {it.id to it}.toMap()
|
||||
|
||||
fun fromValue(value: UByte): PacketType? {
|
||||
return innermap[value]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun PacketType.toUByte(): UByte {
|
||||
return this.id
|
||||
}
|
||||
|
||||
|
||||
// tcp发送过来的通道里面的信息
|
||||
class SDLanTCP(
|
||||
val packetID: UInt,
|
||||
val packetType: PacketType,
|
||||
val currentPacket: ByteArray,
|
||||
)
|
||||
|
||||
enum class ConnectingState {
|
||||
NotConnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
}
|
||||
|
||||
suspend fun sendToSock(node: Node, content: ByteArray, sock: SDLanSock) {
|
||||
val target: SocketAddress
|
||||
when (sock.family) {
|
||||
IPFamily.IPV4 -> {
|
||||
target = InetSocketAddress(Inet4Address.getByAddress(sock.ip), sock.port)
|
||||
}
|
||||
IPFamily.IPV6 -> {
|
||||
target = InetSocketAddress(Inet6Address.getByAddress(sock.ip), sock.port)
|
||||
}
|
||||
}
|
||||
node.udpSockV4.send_to(content, target)
|
||||
}
|
||||
|
||||
suspend fun sendToSock(node: Node, content: List<Byte>, sock: SDLanSock) {
|
||||
sendToSock(node, content.toByteArray(), sock)
|
||||
}
|
||||
|
||||
fun encodeToUDPMessage(msg: Message?, packetType: PacketType): List<Byte> {
|
||||
val result: MutableList<Byte> = mutableListOf()
|
||||
val msgByte = msg?.toByteArray()?.toList()?:listOf<Byte>()
|
||||
result.add(packetType.toUByte().toByte())
|
||||
result.addAll(msgByte)
|
||||
return result
|
||||
}
|
||||
|
||||
fun encodeToTcpMessage(msg: Message?, packetID: Int, packetType: PacketType): List<Byte> {
|
||||
val msgByte = msg?.toByteArray()?.toList()?:listOf<Byte>()
|
||||
val result: MutableList<Byte> = mutableListOf()
|
||||
|
||||
result.addAll(ByteBuffer.allocate(2)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
.putShort((msgByte.size + 5).toShort())
|
||||
.array().toList())
|
||||
|
||||
result.addAll(ByteBuffer.allocate(4)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
.putInt(packetID)
|
||||
.array().toList())
|
||||
|
||||
result.add(packetType.toUByte().toByte())
|
||||
result.addAll(msgByte)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
enum class StunProbeAttr {
|
||||
None,
|
||||
Port,
|
||||
Peer,
|
||||
}
|
||||
107
app/src/main/java/com/jihe/punchnet/sdlan/network/peermap.kt
Normal file
@ -0,0 +1,107 @@
|
||||
package com.jihe.punchnet.sdlan.network
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import com.jihe.punchnet.sdlan.config.SDLanConfig
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
typealias Mac = ByteString
|
||||
// typealias PeerMap = ConcurrentMap<Mac, EdgePeer>
|
||||
|
||||
class PeerMap private constructor (val inner: ConcurrentMap<Mac, EdgePeer>): ConcurrentMap<Mac, EdgePeer> by inner {
|
||||
constructor(): this(ConcurrentHashMap<Mac, EdgePeer>())
|
||||
}
|
||||
|
||||
|
||||
class IPSubnet(var netAddr: Int, var netBitLen: Byte) {
|
||||
}
|
||||
|
||||
enum class NatType(val value: UByte) {
|
||||
Blocked(0u),
|
||||
NoNat(1u),
|
||||
FullCone(2u),
|
||||
PortRestricted(3u),
|
||||
ConeRestrict(4u),
|
||||
Symmetric(5u),
|
||||
Invalid(0xffu);
|
||||
|
||||
companion object {
|
||||
fun fromUByte(code: UByte): NatType {
|
||||
return when(code) {
|
||||
0.toUByte() -> Blocked
|
||||
1.toUByte() -> NoNat
|
||||
2.toUByte() -> FullCone
|
||||
3.toUByte() -> PortRestricted
|
||||
4.toUByte() -> ConeRestrict
|
||||
5.toUByte() -> Symmetric
|
||||
else -> Invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun NatType.toByte(): UByte {
|
||||
return value
|
||||
}
|
||||
|
||||
enum class IPFamily {
|
||||
IPV4, IPV6
|
||||
}
|
||||
|
||||
class SDLanSock constructor(var family: IPFamily, var port: Int, var ip: ByteArray) {
|
||||
override fun toString(): String {
|
||||
when(family) {
|
||||
IPFamily.IPV6 -> {
|
||||
assert(ip.size == 16)
|
||||
val ipstr = ip.map {
|
||||
it.toString(16)
|
||||
}.joinToString(":")
|
||||
return "[$ipstr]:${port}"
|
||||
}
|
||||
IPFamily.IPV4 -> {
|
||||
assert(ip.size == 4)
|
||||
val digit = ip[0]
|
||||
return "${ip[0].toUByte()}.${ip[1].toUByte()}.${ip[2].toUByte()}.${ip[3].toUByte()}:${port}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun is_equal(other: SDLanSock): Boolean {
|
||||
return (family==other.family)
|
||||
&& (port == other.port)
|
||||
&& (ip.contentEquals(other.ip))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class EdgePeer private constructor (
|
||||
var devAddress: IPSubnet,
|
||||
var natType: NatType,
|
||||
var sock: SDLanSock,
|
||||
var ipv6Info: SDLanSock?,
|
||||
// timeout of the family
|
||||
val timeout: Byte,
|
||||
|
||||
// 最近一次与该edge通信
|
||||
val lastSeen: AtomicLong,
|
||||
// 最近一次与该edge有P2P通信
|
||||
val lastP2P: AtomicLong,
|
||||
// 最近一次向服务器查询该edge信息
|
||||
val lastSentQuery: AtomicLong,
|
||||
) {
|
||||
// var dev_addr: IPSubnet = IPSubnet(0, 0)
|
||||
|
||||
constructor(netAddress: Int, netBitLen: Byte, sock: SDLanSock, v6Info: SDLanSock?, now: Long): this(
|
||||
IPSubnet(netAddress, netBitLen),
|
||||
NatType.Blocked,
|
||||
sock,
|
||||
v6Info,
|
||||
SDLanConfig.RegisterSuperInterval,
|
||||
AtomicLong(now),
|
||||
AtomicLong(now),
|
||||
AtomicLong(now),
|
||||
) {
|
||||
}
|
||||
}
|
||||
65
app/src/main/java/com/jihe/punchnet/sdlan/network/socket.kt
Normal file
@ -0,0 +1,65 @@
|
||||
package com.jihe.punchnet.sdlan.network
|
||||
|
||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.Inet4Address
|
||||
import java.net.SocketAddress
|
||||
|
||||
class SDLanSocket(val addr: String, val port: Int, val reuseAddress: Boolean = false) {
|
||||
private val connection: DatagramSocket = run {
|
||||
val sock = DatagramSocket(port, Inet4Address.getByName(addr))
|
||||
if (reuseAddress) {
|
||||
sock.reuseAddress = true
|
||||
}
|
||||
sock
|
||||
}
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
fun loop(): ReceiveChannel<DatagramPacket> {
|
||||
val channel = Channel<DatagramPacket>(100)
|
||||
job = CoroutineScope(Dispatchers.Default).launch {
|
||||
while (true) {
|
||||
val msg = receive()
|
||||
channel.send(msg)
|
||||
}
|
||||
|
||||
}
|
||||
return channel
|
||||
}
|
||||
|
||||
suspend fun send_to(content: ByteArray, target: SocketAddress) {
|
||||
val packet = DatagramPacket(content, content.size, target)
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
connection.send(packet)
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
TerminalLogger.errorf {"Failed to send to: $e"}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun receive(): DatagramPacket {
|
||||
val buffer = ByteArray(1500)
|
||||
val packet = DatagramPacket(buffer, buffer.size)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
connection.receive(packet)
|
||||
}
|
||||
return packet
|
||||
}
|
||||
|
||||
suspend fun close() {
|
||||
job?.cancelAndJoin()
|
||||
connection.close()
|
||||
}
|
||||
}
|
||||
4
app/src/main/java/com/jihe/punchnet/sdlan/sdlan.kt
Normal file
@ -0,0 +1,4 @@
|
||||
package com.jihe.punchnet.sdlan
|
||||
|
||||
fun test() {
|
||||
}
|
||||
124
app/src/main/java/com/jihe/punchnet/sdlan/utils/aes.kt
Normal file
@ -0,0 +1,124 @@
|
||||
package com.jihe.punchnet.sdlan.utils
|
||||
|
||||
import com.jihe.punchnet.sdlan.config.AESConfig
|
||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
||||
import java.nio.ByteBuffer
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class AES private constructor () {
|
||||
private var _secret: ByteArray = ByteArray(0)
|
||||
private var _iv: ByteArray = ByteArray(0)
|
||||
companion object {
|
||||
private var instance: AES? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
field = AES()
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getAES(): AES {
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
|
||||
fun isAuthorized(): Boolean {
|
||||
return _secret.size != 0
|
||||
}
|
||||
|
||||
fun setSecret(secret: ByteArray?) {
|
||||
if (secret == null) {
|
||||
_secret = ByteArray(0)
|
||||
_iv = ByteArray(0)
|
||||
} else {
|
||||
_secret = secret
|
||||
_iv = _secret.sliceArray(0..<16)
|
||||
}
|
||||
}
|
||||
|
||||
fun encrypt(content: ByteBuffer): ByteBuffer? {
|
||||
if (!isAuthorized()) {
|
||||
return null
|
||||
}
|
||||
println(11.0)
|
||||
val cipher = Cipher.getInstance(AESConfig.CIPHER_ALGORITHM)
|
||||
println(11.1)
|
||||
val keyspec = SecretKeySpec(_secret, AESConfig.KEY_SPEC)
|
||||
println(11.2)
|
||||
println(11.3)
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keyspec, IvParameterSpec(_iv))
|
||||
println(10.0)
|
||||
val output = ByteBuffer.allocate(cipher.getOutputSize(content.remaining()))
|
||||
println(10.1)
|
||||
cipher.doFinal(content, output)
|
||||
println(10.2)
|
||||
output.flip()
|
||||
println(10.3)
|
||||
return output
|
||||
} catch (e: Exception) {
|
||||
println("failed to encrypt: ${e.toString()}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun encrypt(content: ByteArray): ByteArray? {
|
||||
if (!isAuthorized()) {
|
||||
TerminalLogger.errorf { "not authed, so not encrypting" }
|
||||
return null
|
||||
}
|
||||
val cipher = Cipher.getInstance(AESConfig.CIPHER_ALGORITHM)
|
||||
val keyspec = SecretKeySpec(_secret, AESConfig.KEY_SPEC)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keyspec, IvParameterSpec(_iv))
|
||||
try {
|
||||
val encrypted = cipher.doFinal(content)
|
||||
return encrypted
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf {"encrypt failed: $e"}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun decrypt(ciphered: ByteBuffer): ByteBuffer? {
|
||||
if (!isAuthorized()) {
|
||||
return null
|
||||
}
|
||||
val cipher = Cipher.getInstance(AESConfig.CIPHER_ALGORITHM)
|
||||
val keyspec = SecretKeySpec(_secret, AESConfig.KEY_SPEC)
|
||||
cipher.init(Cipher.DECRYPT_MODE, keyspec, IvParameterSpec(_iv))
|
||||
try {
|
||||
val output = ByteBuffer.allocate(cipher.getOutputSize(ciphered.remaining()))
|
||||
cipher.doFinal(ciphered, output)
|
||||
output.flip()
|
||||
return output
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun decrypt(ciphered: ByteArray): ByteArray? {
|
||||
if (!isAuthorized()) {
|
||||
return null
|
||||
}
|
||||
val cipher = Cipher.getInstance(AESConfig.CIPHER_ALGORITHM)
|
||||
val keyspec = SecretKeySpec(_secret, AESConfig.KEY_SPEC)
|
||||
cipher.init(Cipher.DECRYPT_MODE, keyspec, IvParameterSpec(_iv))
|
||||
try {
|
||||
val decrypted = cipher.doFinal(ciphered)
|
||||
return decrypted
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun byteArray2Hex(array: ByteArray): String {
|
||||
var result = mutableListOf<String>()
|
||||
for (item in array) {
|
||||
result.add(String.format("0x%02x", item.toInt() and 0xff))
|
||||
}
|
||||
return result.joinToString(" ")
|
||||
}
|
||||
167
app/src/main/java/com/jihe/punchnet/sdlan/utils/rsa.kt
Normal file
@ -0,0 +1,167 @@
|
||||
package com.jihe.punchnet.sdlan.utils
|
||||
|
||||
import com.jihe.punchnet.sdlan.config.RSAConfig
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.openssl.PEMParser
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import java.io.FileWriter
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.security.Security
|
||||
import javax.crypto.Cipher
|
||||
|
||||
// 添加BouncyCastle提供者
|
||||
fun setupBouncyCastle() {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
|
||||
fun savePublicKeyToPem(publicKey: PublicKey, fileName: String) {
|
||||
FileWriter(fileName).use { fileWriter ->
|
||||
JcaPEMWriter(fileWriter).use { pemWriter ->
|
||||
pemWriter.writeObject(publicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun savePrivateKeyToPem(privateKey: PrivateKey, fileName: String) {
|
||||
FileWriter(fileName).use { fileWriter ->
|
||||
JcaPEMWriter(fileWriter).use { pemWriter ->
|
||||
pemWriter.writeObject(privateKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadPublicKeyFromPem(fileName: String): PublicKey {
|
||||
FileReader(fileName).use { fileReader ->
|
||||
PEMParser(fileReader).use { pemParser ->
|
||||
val converter = JcaPEMKeyConverter()
|
||||
val keypair = pemParser.readObject()
|
||||
return converter.getPublicKey(keypair as SubjectPublicKeyInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadPrivateKeyFromPem(fileName: String): PrivateKey {
|
||||
FileReader(fileName).use { fileReader ->
|
||||
PEMParser(fileReader).use { pemParser ->
|
||||
val converter = JcaPEMKeyConverter()
|
||||
val keypair = pemParser.readObject() as org.bouncycastle.openssl.PEMKeyPair
|
||||
return converter.getPrivateKey(keypair.privateKeyInfo)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 生成RSA密钥对
|
||||
fun generateRSAKeyPair(): KeyPair {
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC")
|
||||
keyPairGenerator.initialize(2048)
|
||||
return keyPairGenerator.generateKeyPair()
|
||||
}
|
||||
|
||||
class RSA private constructor () {
|
||||
private var pubkey: PublicKey? = null
|
||||
private var prikey: PrivateKey? = null
|
||||
private var pubkeyStr: String? = null
|
||||
|
||||
companion object {
|
||||
private var instance: RSA? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
field = RSA()
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getRSA(): RSA {
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
|
||||
fun generateKeyPair(dirname: String) {
|
||||
setupBouncyCastle()
|
||||
val pubFile = File(dirname, RSAConfig.PUBLIC_FILE_NAME)
|
||||
val priFile = File(dirname, RSAConfig.PRIVATE_FILE_NAME)
|
||||
|
||||
val dirpath = File(dirname)
|
||||
dirpath.mkdirs()
|
||||
|
||||
if (pubFile.exists() && priFile.exists()) {
|
||||
pubkey = loadPublicKeyFromPem(pubFile.path)
|
||||
prikey = loadPrivateKeyFromPem(priFile.path)
|
||||
} else {
|
||||
// generate public and private file
|
||||
val keypair = generateRSAKeyPair()
|
||||
|
||||
savePublicKeyToPem(keypair.public, pubFile.path)
|
||||
savePrivateKeyToPem(keypair.private, priFile.path)
|
||||
|
||||
pubkey = keypair.public
|
||||
prikey = keypair.private
|
||||
}
|
||||
|
||||
pubkeyStr = pubFile.readText()
|
||||
println("pub key is ${pubkeyStr}")
|
||||
}
|
||||
|
||||
fun encrypt(input: ByteArray, use_private_key: Boolean = true): ByteArray {
|
||||
val cipher = Cipher.getInstance(RSAConfig.RSAMODE)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, if (use_private_key) prikey else pubkey)
|
||||
|
||||
var temp: ByteArray? = null
|
||||
var offset = 0
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
while(input.size - offset > 0) {
|
||||
if (input.size - offset >= RSAConfig.ENCRYPT_MAX_SIZE) {
|
||||
temp = cipher.doFinal(input, offset, RSAConfig.ENCRYPT_MAX_SIZE)
|
||||
offset += RSAConfig.ENCRYPT_MAX_SIZE
|
||||
} else {
|
||||
temp = cipher.doFinal(input, offset, input.size - offset)
|
||||
offset = input.size
|
||||
}
|
||||
outputStream.write(temp!!)
|
||||
}
|
||||
outputStream.close()
|
||||
return outputStream.toByteArray()
|
||||
}
|
||||
|
||||
fun decrypt(ciphered: ByteArray, use_pub_key: Boolean = false): ByteArray? {
|
||||
val cipher = Cipher.getInstance(RSAConfig.RSAMODE)
|
||||
cipher.init(Cipher.DECRYPT_MODE, if (use_pub_key) pubkey else prikey)
|
||||
|
||||
var temp: ByteArray? = null
|
||||
var offset = 0
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
try {
|
||||
while(ciphered.size - offset > 0) {
|
||||
if (ciphered.size - offset >= RSAConfig.DECRYPT_MAX_SIZE) {
|
||||
temp = cipher.doFinal(ciphered, offset, RSAConfig.DECRYPT_MAX_SIZE)
|
||||
offset += RSAConfig.DECRYPT_MAX_SIZE
|
||||
} else {
|
||||
temp = cipher.doFinal(ciphered, offset, ciphered.size - offset)
|
||||
offset = ciphered.size
|
||||
}
|
||||
outputStream.write(temp!!)
|
||||
}
|
||||
outputStream.close()
|
||||
return outputStream.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getPublicKeyString(): String {
|
||||
return pubkeyStr!!
|
||||
}
|
||||
|
||||
}
|
||||
169
app/src/main/java/com/jihe/punchnet/sdlan/utils/utils.kt
Normal file
@ -0,0 +1,169 @@
|
||||
package com.jihe.punchnet.sdlan.utils
|
||||
|
||||
import android.os.Environment
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.kotlin.toByteString
|
||||
import com.jihe.punchnet.sdlan.config.SDLanConfig
|
||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
||||
import com.jihe.punchnet.sdlan.network.Mac
|
||||
import java.io.File
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.UUID
|
||||
import kotlin.experimental.and
|
||||
import kotlin.experimental.inv
|
||||
import kotlin.experimental.or
|
||||
import kotlin.random.Random
|
||||
|
||||
object UniqueNodeID {
|
||||
var id: String = ""
|
||||
private var baseDir: String = ""
|
||||
|
||||
fun setBaseDir(basedir: String) {
|
||||
baseDir = basedir
|
||||
}
|
||||
|
||||
fun getUUID(): String {
|
||||
if (baseDir.length == 0) {
|
||||
baseDir = Environment.getExternalStorageDirectory().name
|
||||
}
|
||||
if (id.length == 0) {
|
||||
val dirpath = File(baseDir)
|
||||
dirpath.mkdirs()
|
||||
val f = File(baseDir, SDLanConfig.ID_FILENAME)
|
||||
if (f.exists()) {
|
||||
val value = f.readText()
|
||||
id = value
|
||||
} else {
|
||||
// file not exists
|
||||
val uuid = UUID.randomUUID()
|
||||
id = uuid.toString().replace("-", "")
|
||||
f.writeText(id)
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
fun parseScoketAddressV4FromString(data: String): InetSocketAddress? {
|
||||
val pieces = data.split(":")
|
||||
if (pieces.size != 2) {
|
||||
TerminalLogger.errorf {"socker format error: $data"}
|
||||
return null
|
||||
}
|
||||
val host = Inet4Address.getByName(pieces[0])
|
||||
val port = pieces[1].toIntOrNull()
|
||||
|
||||
if (port != null) {
|
||||
return InetSocketAddress(host, port)
|
||||
}
|
||||
TerminalLogger.errorf {"invalid port: ${port}"}
|
||||
return null
|
||||
}
|
||||
|
||||
fun ipToString(ip: Int): String {
|
||||
val d1 = ip.ushr(24).and(0x000000ff).toUByte()
|
||||
val d2 = ip.ushr(16).and(0x000000ff).toUByte()
|
||||
val d3 = ip.ushr(8).and(0x000000ff).toUByte()
|
||||
val d4 = ip.and(0x000000ff).toUByte()
|
||||
return "$d1.$d2.$d3.$d4"
|
||||
}
|
||||
|
||||
fun macToString(mac: Mac): String {
|
||||
return mac.joinToString(separator = ":") { it.toUByte().toString(16) }
|
||||
}
|
||||
|
||||
fun generateRandomMAC(): Mac {
|
||||
var result = Random.nextBytes(6)
|
||||
val k: Byte = 0x01
|
||||
|
||||
result[0] = result[0].and(k.inv())
|
||||
result[0] = result[0].or(0x02)
|
||||
return result.toByteString()
|
||||
}
|
||||
|
||||
fun isMultiBroadcast(mac: ByteString): Boolean {
|
||||
return isBroadcast(mac) || isMulticast(mac) || isIP6Multicast(mac)
|
||||
}
|
||||
|
||||
inline fun isBroadcast(mac: ByteString): Boolean {
|
||||
return mac.all {it == 0xff.toByte()}
|
||||
}
|
||||
|
||||
inline fun isMulticast(mac: ByteString): Boolean {
|
||||
return mac.size()==6 && mac.byteAt(0) == 0x01.toByte()
|
||||
&& mac.byteAt(1) == 0x00.toByte()
|
||||
&& mac.byteAt(2) == 0x5e.toByte()
|
||||
&& (mac.byteAt(3).and(0x80.toByte()) == 0.toByte())
|
||||
}
|
||||
|
||||
inline fun isIP6Multicast(mac: ByteString): Boolean {
|
||||
return mac.size() == 6 && mac.byteAt(0) == 0x33.toByte()
|
||||
&& mac.byteAt(1) == 0x33.toByte()
|
||||
}
|
||||
|
||||
|
||||
class EthHdr(
|
||||
var dest: ByteArray,
|
||||
var src: ByteArray,
|
||||
var etherType: Short,
|
||||
)
|
||||
|
||||
class ArpHdr(
|
||||
val ethhdr: EthHdr,
|
||||
val hwtype: Short,
|
||||
val protocol: Short,
|
||||
val hwlen: Byte,
|
||||
val protolen: Byte,
|
||||
var opcode: Short,
|
||||
|
||||
var shwaddr: ByteArray,
|
||||
var sipaddr: Int,
|
||||
var dhwaddr: ByteArray,
|
||||
var dipaddr: Int,
|
||||
) {
|
||||
companion object {
|
||||
// 需要保证,长度够
|
||||
fun parseFrom(ethhdr: EthHdr, data: ByteBuffer): ArpHdr {
|
||||
val hwtype = data.getShort()
|
||||
val protocol = data.getShort()
|
||||
val hwlen = data.get()
|
||||
val protolen = data.get()
|
||||
val opcode = data.getShort()
|
||||
|
||||
val shwaddr = ByteArray(6)
|
||||
data.get(shwaddr)
|
||||
val sipaddr = data.getInt()
|
||||
|
||||
val dhwaddr = ByteArray(6)
|
||||
data.get(dhwaddr)
|
||||
val dipaddr = data.getInt()
|
||||
|
||||
return ArpHdr(ethhdr, hwtype, protocol, hwlen, protolen, opcode, shwaddr, sipaddr, dhwaddr, dipaddr)
|
||||
}
|
||||
}
|
||||
|
||||
fun marshalToBytes(): ByteBuffer {
|
||||
val buff = ByteBuffer.allocate(64).order(ByteOrder.BIG_ENDIAN)
|
||||
buff.put(ethhdr.dest)
|
||||
buff.put(ethhdr.src)
|
||||
buff.putShort(ethhdr.etherType)
|
||||
|
||||
buff.putShort(hwtype)
|
||||
buff.putShort(protocol)
|
||||
buff.put(hwlen)
|
||||
buff.put(protolen)
|
||||
buff.putShort(opcode)
|
||||
|
||||
buff.put(shwaddr)
|
||||
buff.putInt(sipaddr)
|
||||
|
||||
buff.put(dhwaddr)
|
||||
buff.putInt(dipaddr)
|
||||
|
||||
buff.flip()
|
||||
return buff
|
||||
}
|
||||
}
|
||||
11
app/src/main/java/com/jihe/punchnet/ui/theme/Color.kt
Normal file
@ -0,0 +1,11 @@
|
||||
package com.jihe.punchnet.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
57
app/src/main/java/com/jihe/punchnet/ui/theme/Theme.kt
Normal file
@ -0,0 +1,57 @@
|
||||
package com.jihe.punchnet.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun PunchnetTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
34
app/src/main/java/com/jihe/punchnet/ui/theme/Type.kt
Normal file
@ -0,0 +1,34 @@
|
||||
package com.jihe.punchnet.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
154
app/src/main/proto/message.proto
Normal file
@ -0,0 +1,154 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "com.jihe.punchnet.protobuf";
|
||||
option java_outer_classname = "PunchProto";
|
||||
// 基础公共类型定义
|
||||
|
||||
message SDLV4Info {
|
||||
uint32 port = 1;
|
||||
bytes v4 = 2;
|
||||
uint32 nat_type = 3;
|
||||
}
|
||||
|
||||
message SDLV6Info {
|
||||
uint32 port = 1;
|
||||
bytes v6 = 2;
|
||||
}
|
||||
|
||||
// 设备网络地址信息
|
||||
message SDLDevAddr {
|
||||
uint32 network_id = 1;
|
||||
bytes mac = 2;
|
||||
uint32 net_addr = 3;
|
||||
uint32 net_bit_len = 4;
|
||||
}
|
||||
|
||||
// tcp通讯消息
|
||||
message SDLEmpty {
|
||||
|
||||
}
|
||||
|
||||
message SDLRegisterSuper {
|
||||
uint32 version = 1;
|
||||
string installed_channel = 2;
|
||||
string client_id = 3;
|
||||
SDLDevAddr dev_addr = 4;
|
||||
string pub_key = 5;
|
||||
string token = 6;
|
||||
}
|
||||
|
||||
message SDLRegisterSuperAck {
|
||||
SDLDevAddr dev_addr = 1;
|
||||
bytes aes_key = 2;
|
||||
uint32 upgrade_type = 3;
|
||||
optional string upgrade_prompt = 4;
|
||||
optional string upgrade_address = 5;
|
||||
}
|
||||
|
||||
message SDLRegisterSuperNak {
|
||||
uint32 error_code = 1;
|
||||
string error_message = 2;
|
||||
}
|
||||
|
||||
// 网络地址查询
|
||||
|
||||
message SDLQueryInfo {
|
||||
bytes dst_mac = 1;
|
||||
}
|
||||
|
||||
message SDLPeerInfo {
|
||||
bytes dst_mac = 1;
|
||||
SDLV4Info v4_info = 2;
|
||||
optional SDLV6Info v6_info = 3;
|
||||
}
|
||||
|
||||
// 事件定义
|
||||
|
||||
message SDLNatChangedEvent {
|
||||
bytes mac = 1;
|
||||
uint32 ip = 2;
|
||||
}
|
||||
|
||||
message SDLSendRegisterEvent {
|
||||
bytes dst_mac = 1;
|
||||
uint32 nat_ip = 2;
|
||||
uint32 nat_port = 3;
|
||||
uint32 nat_type = 4;
|
||||
optional SDLV6Info v6_info = 5;
|
||||
}
|
||||
|
||||
message SDLNetworkShutdownEvent {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
// 命令定义
|
||||
|
||||
message SDLChangeNetworkCommand {
|
||||
SDLDevAddr dev_addr = 1;
|
||||
bytes aes_key = 2;
|
||||
}
|
||||
|
||||
message SDLCommandAck {
|
||||
// status = true, 表示成功;status = false 表示失败,message是失败原因描述
|
||||
bool status = 1;
|
||||
optional string message = 2;
|
||||
}
|
||||
|
||||
message SDLFlows {
|
||||
// 服务器转发流量
|
||||
uint32 forward_num = 1;
|
||||
// p2p直接流量
|
||||
uint32 p2p_num = 2;
|
||||
// 接收的流量
|
||||
uint32 inbound_num = 3;
|
||||
}
|
||||
|
||||
// UDP通讯消息
|
||||
|
||||
message SDLStunRequest {
|
||||
uint32 cookie = 1;
|
||||
string client_id = 2;
|
||||
uint32 network_id = 3;
|
||||
bytes mac = 4;
|
||||
uint32 ip = 5;
|
||||
uint32 nat_type = 6;
|
||||
optional SDLV6Info v6_info = 7;
|
||||
}
|
||||
|
||||
message SDLStunReply {
|
||||
uint32 cookie = 1;
|
||||
}
|
||||
|
||||
message SDLData {
|
||||
uint32 network_id = 1;
|
||||
bytes src_mac = 2;
|
||||
bytes dst_mac = 3;
|
||||
bool is_p2p = 4;
|
||||
uint32 ttl = 5;
|
||||
bytes data = 6;
|
||||
}
|
||||
|
||||
message SDLRegister {
|
||||
uint32 network_id = 1;
|
||||
bytes src_mac = 2;
|
||||
bytes dst_mac = 3;
|
||||
}
|
||||
|
||||
message SDLRegisterAck {
|
||||
uint32 network_id = 1;
|
||||
bytes src_mac = 2;
|
||||
bytes dst_mac = 3;
|
||||
}
|
||||
|
||||
// 网络类型探测
|
||||
|
||||
message SDLStunProbe {
|
||||
uint32 cookie = 1;
|
||||
uint32 attr = 2;
|
||||
}
|
||||
|
||||
message SDLStunProbeReply {
|
||||
uint32 cookie = 1;
|
||||
uint32 port = 2;
|
||||
uint32 ip = 3;
|
||||
}
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
30
app/src/main/res/drawable/ic_vpn.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M19.35,10.04 C18.67,6.59,15.64,4,12,4 C9.11,4,6.6,5.64,5.35,8.04
|
||||
C2.34,8.36,0,10.91,0,14 C0,17.31,2.69,20,6,20 L19,20 C21.76,20,24,17.76,24,15
|
||||
C24,12.36,21.95,10.22,19.35,10.04 Z M19,18 L6,18 C3.79,18,2,16.21,2,14
|
||||
S3.79,10,6,10 L6.71,10 C7.37,7.69,9.48,6,12,6 C15.04,6,17.5,8.46,17.5,11.5
|
||||
L17.5,12 L19,12 C20.66,12,22,13.34,22,15 S20.66,18,19,18 Z" />
|
||||
<path
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="2"
|
||||
android:pathData="M6.58994,13.1803 C6.58994,13.1803,8.59173,15.8724,12.011,15.8726
|
||||
C15.2788,15.8728,17.3696,13.2502,17.3696,13.2502" />
|
||||
</vector>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
10
app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
6
app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<string name="app_name">punchnet</string>
|
||||
|
||||
<string name="start_vpn">开启</string>
|
||||
<string name="stop_vpn">关闭</string>
|
||||
</resources>
|
||||
5
app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Punchnet" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
16
app/src/test/java/com/jihe/punchnet/ExampleUnitTest.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package com.jihe.punchnet
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
6
build.gradle.kts
Normal file
@ -0,0 +1,6 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
}
|
||||
23
gradle.properties
Normal file
@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
35
gradle/libs.versions.toml
Normal file
@ -0,0 +1,35 @@
|
||||
[versions]
|
||||
agp = "8.8.2"
|
||||
bcpkixJdk18on = "1.80"
|
||||
kotlin = "2.0.0"
|
||||
coreKtx = "1.16.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
lifecycleRuntimeKtx = "2.8.7"
|
||||
activityCompose = "1.10.1"
|
||||
composeBom = "2024.04.01"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bcpkixJdk18on" }
|
||||
bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcpkixJdk18on" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
#Thu May 15 10:18:58 CST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
185
gradlew
vendored
Normal file
@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
gradlew.bat
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
23
settings.gradle.kts
Normal file
@ -0,0 +1,23 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "punchnet"
|
||||
include(":app")
|
||||