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")
|
||||||