first commit

This commit is contained in:
asxalex 2025-07-03 10:54:53 +08:00
commit f04b16f7c4
68 changed files with 4410 additions and 0 deletions

15
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

6
.idea/compiler.xml generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
/build

93
app/build.gradle.kts Normal file
View 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
View 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

View File

@ -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)
}
}

View 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>

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

View 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()
}
}

View File

@ -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)
}
}
}

View File

@ -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,
)

View 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"
}

View 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
}

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

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

View 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)
}
}
}

View 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(),
)
}

View 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(":")
}
*/

View 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,
)
}

View 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
}
}
}
}

View 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,
}

View 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),
) {
}
}

View 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()
}
}

View File

@ -0,0 +1,4 @@
package com.jihe.punchnet.sdlan
fun test() {
}

View 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(" ")
}

View 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!!
}
}

View 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
}
}

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

View 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
)
}

View 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
)
*/
)

View 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;
}

View 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>

View 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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View 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>

View File

@ -0,0 +1,6 @@
<resources>
<string name="app_name">punchnet</string>
<string name="start_vpn">开启</string>
<string name="stop_vpn">关闭</string>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Punchnet" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View 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>

View 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>

View 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
View 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
View 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
View 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

Binary file not shown.

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