fix some warning

This commit is contained in:
asxalex 2025-07-29 14:10:04 +08:00 committed by stavid
parent f86069d165
commit 4b1bae0e55
26 changed files with 1438 additions and 906 deletions

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
punchnet

View File

@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-05-30T07:22:02.241059Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/stavid/.android/avd/Medium_Phone.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

13
.idea/deviceManager.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

8
.idea/markdown.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

View File

@ -90,12 +90,18 @@ dependencies {
ksp("androidx.room:room-compiler:2.7.2")
implementation("androidx.room:room-ktx:2.7.2")
// implementation(libs.bcprov.jdk18on)
// HTTP API Control Plane
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.google.code.gson:gson:2.10.1")
// QUIC Data Plane (Kwik)
implementation("tech.kwik:kwik:0.10.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") }
// }
// Security Crypto
implementation("androidx.security:security-crypto:1.1.0-alpha06")
}

View File

@ -5,6 +5,8 @@
<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"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<application
@ -16,6 +18,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Punchnet"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
@ -30,7 +33,7 @@
</activity>
<service android:name=".PunchnetService"
android:exported="false"
android:exported="true"
android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_VPN_SERVICE">

View File

@ -114,6 +114,10 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize AppAuthRepository for Split Tunneling
com.jihe.punchnet.data.AppAuthRepository.init(this)
enableEdgeToEdge()
Log.d("DIR", "filesdir = ${this.filesDir}")

View File

@ -12,12 +12,9 @@ import android.os.ParcelFileDescriptor
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.lifecycle.ViewModelProvider
import com.jihe.punchnet.data.ButtonRepository
import com.jihe.punchnet.data.ButtonState
import com.jihe.punchnet.data.ButtonViewModel
import com.jihe.punchnet.data.RouteItem
import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.sdlan.config.Arguments
import com.jihe.punchnet.sdlan.config.toIPV4String
import com.jihe.punchnet.sdlan.logs.TerminalLogger
@ -26,23 +23,17 @@ 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.RouteDetail
import com.jihe.punchnet.sdlan.network.RouteTable
import com.jihe.punchnet.sdlan.network.StartStopChanInfo
import com.jihe.punchnet.sdlan.network.cidrToRouteDetail
import com.jihe.punchnet.sdlan.network.ipInt2ByteArray
import com.jihe.punchnet.sdlan.network.maskIPToDigit
import com.jihe.punchnet.sdlan.network.run_sdlan
import com.jihe.punchnet.sdlan.utils.ipToString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.concurrent.atomic.AtomicBoolean
class PunchnetService : VpnService() , IfaceTun {
@ -79,7 +70,6 @@ class PunchnetService : VpnService() , IfaceTun {
try {
var size = input?.read(result)
if (size == null) {
println("xxx failed to read")
size = 0
} else {
println("xxx got $size bytes")
@ -90,6 +80,8 @@ class PunchnetService : VpnService() , IfaceTun {
result.copyOf(size)
// result.slice(0..<size).toByteArray()
} catch (e: Exception) {
println("xxx read exception: ${e.message}")
e.printStackTrace()
ByteArray(0)
}
}
@ -126,10 +118,45 @@ class PunchnetService : VpnService() , IfaceTun {
.addAddress(ip, config.ip.netBitLen.toInt())
.setBlocking(true)
// Automatically add route for the SDLAN network itself
val maskInt = com.jihe.punchnet.sdlan.network.netmaskBit2Int(config.ip.netBitLen)
val networkInt = config.ip.netAddr and maskInt
val networkStr = ipInt2ByteArray(networkInt).toIPV4String()
tempVpnDescriptor = tempVpnDescriptor.addRoute(networkStr, config.ip.netBitLen.toInt())
for (route in routes) {
tempVpnDescriptor = tempVpnDescriptor.addRoute(ipToString(route.net_ip), maskIPToDigit(route.mask_ip))
}
// Apply App Authorization (Split Tunneling)
val allowedApps = com.jihe.punchnet.data.AppAuthRepository.allowedApps.value
if (allowedApps.isNotEmpty()) {
for (pkg in allowedApps) {
// Ensure we do not add ourselves to the allowed list, otherwise it causes a routing loop
if (pkg == this.packageName) continue
try {
tempVpnDescriptor = tempVpnDescriptor.addAllowedApplication(pkg)
} catch (e: android.content.pm.PackageManager.NameNotFoundException) {
TerminalLogger.warning { "App not found when setting VPN allowed app: $pkg" }
}
}
} else {
// If no apps are specified, VPN intercepts everything matching the routes.
// We ONLY disallow ourselves if there's a 0.0.0.0/0 route, to avoid routing loops.
// If there's no global route, we want the app (e.g. in-app WebViews) to be able to access the VPN routes!
val hasGlobalRoute = routes.any { it.mask_ip == 0 }
if (hasGlobalRoute) {
try {
tempVpnDescriptor = tempVpnDescriptor.addDisallowedApplication(this.packageName)
TerminalLogger.infof { "Disallowed app itself to prevent loop on 0.0.0.0/0" }
} catch (e: Exception) {
TerminalLogger.warning { "Failed to disallow self: $e" }
}
} else {
TerminalLogger.infof { "Did not disallow self. In-app WebViews will be routed via VPN." }
}
}
vpnDescriptor = tempVpnDescriptor
.establish()?.apply {
input = FileInputStream(fileDescriptor)
@ -198,11 +225,10 @@ class PunchnetService : VpnService() , IfaceTun {
val argument = Arguments(
baseDir = this.filesDir.path,
sn = "$server:1265",
tcp = "$server:18083",
nat_server1 = "$server:1265",
nat_server2 = "47.98.178.3:1265",
sn = "$server:443",
stun = "$server:1365",
nat_server1 = "$server:1365",
nat_server2 = "$server:1366",
token = startArg?.token?:"",
name = "tau",
)
@ -210,8 +236,43 @@ class PunchnetService : VpnService() , IfaceTun {
val notification = createNotification()
startForeground(1, notification)
// Initialize UniqueNodeID baseDir to app's files directory
com.jihe.punchnet.sdlan.utils.UniqueNodeID.setBaseDir(this.filesDir.absolutePath)
scope.launch {
run_sdlan(scope, iface, argument, startArg)
val apiClient = com.jihe.punchnet.api.ApiClient()
val authDataJson = startArg?.token ?: ""
if (authDataJson.isEmpty()) {
TerminalLogger.errorf { "API connect failed: No auth data found" }
ButtonRepository.updateState(ButtonState.ButtonStopped)
return@launch
}
val authData = try {
com.google.gson.Gson().fromJson(authDataJson, com.jihe.punchnet.api.AuthResponseData::class.java)
} catch (e: Exception) {
TerminalLogger.errorf { "API connect failed: Invalid auth data" }
ButtonRepository.updateState(ButtonState.ButtonStopped)
return@launch
}
// 1. Connect directly using the accessToken obtained from LoginScreen
val connectReq = com.jihe.punchnet.api.ConnectRequest(
clientId = "punchnet_android_client_" + com.jihe.punchnet.sdlan.utils.UniqueNodeID.getUUID(this@PunchnetService),
accessToken = authData.accessToken
)
val connectRes = apiClient.connect(connectReq)
if (connectRes == null || connectRes.code != 0 || connectRes.data == null) {
TerminalLogger.errorf { "API connect failed: ${connectRes?.message}" }
ButtonRepository.updateState(ButtonState.ButtonStopped)
return@launch
}
// Update UI state to Started since connection was successful
ButtonRepository.updateState(ButtonState.ButtonStarted)
com.jihe.punchnet.data.NodeRepository.updateAll(connectRes.data)
run_sdlan(scope, iface, argument, authData, connectRes.data)
}
// isRunning.set(true)
@ -236,7 +297,7 @@ class PunchnetService : VpnService() , IfaceTun {
} else {
intent?.getParcelableExtra("argument")
}
println("argument = ${argument}")
TerminalLogger.debugf { "service argument received: routes=${argument?.routes?.size ?: 0}, hasToken=${!argument?.token.isNullOrEmpty()}" }
connect(argument)
START_STICKY
}
@ -265,6 +326,7 @@ class PunchnetService : VpnService() , IfaceTun {
// disconnect()
scope.cancel()
disconnect()
com.jihe.punchnet.data.NodeRepository.clear()
Log.e(TAG, "onDestroy is called")
// stopForeground(STOP_FOREGROUND_REMOVE)
super.onDestroy()

View File

@ -0,0 +1,81 @@
package com.jihe.punchnet.api
import android.util.Log
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.concurrent.TimeUnit
class ApiClient {
private val client = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(SignInterceptor())
.build()
private val gson = Gson()
private val JSON = "application/json; charset=utf-8".toMediaType()
private val BASE_URL = "https://root.punchsky.com/api"
suspend fun loginWithToken(request: AuthTokenRequest): BaseResponse<AuthResponseData>? {
return post("/auth/token", request)
}
suspend fun connect(request: ConnectRequest): BaseResponse<ConnectResponseData>? {
return post("/connect", request)
}
suspend fun disconnect(request: DisconnectRequest): BaseResponse<Any>? {
return post("/disconnect", request)
}
suspend fun loginWithAccount(request: AuthLoginRequest): BaseResponse<AuthResponseData>? {
return post("/auth/login", request)
}
suspend fun getNodeResources(request: GetNodeResourcesRequest): BaseResponse<GetNodeResourcesResponseData>? {
return post("/get_node_resources", request)
}
suspend fun getAcl(request: AclRequest): BaseResponse<AclResponseData>? {
return post("/acl", request)
}
private suspend inline fun <reified T, reified R> post(path: String, requestBody: T): BaseResponse<R>? {
return withContext(Dispatchers.IO) {
try {
val jsonBody = gson.toJson(requestBody)
Log.i("apiClient", "post [${BASE_URL + path}]")
val body = jsonBody.toRequestBody(JSON)
val request = Request.Builder()
.url(BASE_URL + path)
.post(body)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
response.body?.close()
Log.e("apiClient", "response error [${BASE_URL + path}]: HTTP ${response.code}")
return@withContext BaseResponse<R>(code = -1, message = "HTTP ${response.code}: ${response.message}", data = null)
}
val responseStr = response.body?.string() ?: return@withContext BaseResponse<R>(code = -1, message = "Empty response body", data = null)
Log.i("apiClient", "response [${BASE_URL + path}]: HTTP ${response.code}")
val type = object : TypeToken<BaseResponse<R>>() {}.type
gson.fromJson<BaseResponse<R>>(responseStr, type)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("apiClient", "response exception [${BASE_URL + path}]: ${e.message}")
BaseResponse<R>(code = -1, message = e.message ?: "Unknown error", data = null)
}
}
}
}

View File

@ -0,0 +1,115 @@
package com.jihe.punchnet.api
import com.google.gson.annotations.SerializedName
data class AuthTokenRequest(
@SerializedName("client_id") val clientId: String,
@SerializedName("token") val token: String,
@SerializedName("mac") val mac: String? = null,
@SerializedName("system") val system: String? = null,
@SerializedName("version") val version: String? = null,
@SerializedName("hostname") val hostname: String? = null
)
data class NetworkItem(
@SerializedName("network_id") val networkId: Int,
@SerializedName("network_name") val networkName: String
)
data class AuthResponseData(
@SerializedName("access_token") val accessToken: String,
@SerializedName("username") val username: String,
@SerializedName("user_type") val userType: String,
@SerializedName("audit") val audit: Int,
@SerializedName("network_id") val networkId: Int,
@SerializedName("network_name") val networkName: String,
@SerializedName("network_domain") val networkDomain: String,
@SerializedName("domain") val domain: String?,
@SerializedName("my_network_list") val myNetworkList: List<NetworkItem>?
)
data class BaseResponse<T>(
@SerializedName("code") val code: Int,
@SerializedName("message") val message: String,
@SerializedName("data") val data: T?
)
data class ConnectRequest(
@SerializedName("client_id") val clientId: String,
@SerializedName("access_token") val accessToken: String,
@SerializedName("version") val version: String? = null
)
data class ResourceItem(
@SerializedName("id") val id: Int,
@SerializedName("name") val name: String,
@SerializedName("url") val url: String,
@SerializedName("connection_status") val connectionStatus: String
)
data class NodeItem(
@SerializedName("id") val id: Int,
@SerializedName("name") val name: String,
@SerializedName("ip") val ip: String,
@SerializedName("system") val system: String?,
@SerializedName("connection_status") val connectionStatus: String
)
data class ExitNodeItem(
@SerializedName("node_id") val nodeId: Int,
@SerializedName("node_name") val nodeName: String,
@SerializedName("gateway") val gateway: String,
@SerializedName("target_network") val targetNetwork: String
)
data class ConnectResponseData(
@SerializedName("ip") val ip: String,
@SerializedName("mask_len") val maskLen: Int,
@SerializedName("hostname") val hostname: String,
@SerializedName("identity_id") val identityId: Int,
@SerializedName("resource_list") val resourceList: List<ResourceItem>?,
@SerializedName("node_list") val nodeList: List<NodeItem>?,
@SerializedName("acl") val acl: com.google.gson.JsonElement?,
@SerializedName("exit_node") val exitNode: List<ExitNodeItem>?
)
data class AuthLoginRequest(
@SerializedName("client_id") val clientId: String,
@SerializedName("username") val username: String,
@SerializedName("password") val password: String,
@SerializedName("mac") val mac: String? = null,
@SerializedName("system") val system: String? = null,
@SerializedName("version") val version: String? = null,
@SerializedName("hostname") val hostname: String? = null
)
data class GetNodeResourcesRequest(
@SerializedName("client_id") val clientId: String,
@SerializedName("access_token") val accessToken: String,
@SerializedName("id") val id: Int
)
data class GetNodeResourcesResponseData(
@SerializedName("id") val id: Int,
@SerializedName("name") val name: String,
@SerializedName("ip") val ip: String,
@SerializedName("system") val system: String?,
@SerializedName("connection_status") val connectionStatus: String,
@SerializedName("resource_list") val resourceList: List<ResourceItem>?
)
data class AclRequest(
@SerializedName("client_id") val clientId: String,
@SerializedName("access_token") val accessToken: String,
@SerializedName("network_id") val networkId: Int? = null
)
data class AclResponseData(
@SerializedName("tcp") val tcp: List<Int>?,
@SerializedName("udp") val udp: List<Int>?
)
data class DisconnectRequest(
@SerializedName("client_id") val clientId: String,
@SerializedName("access_token") val accessToken: String
)

View File

@ -61,7 +61,9 @@ abstract class AppDatabase: RoomDatabase() {
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}

View File

@ -2,6 +2,7 @@ package com.jihe.punchnet.helper
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import com.jihe.punchnet.data.RouteItem
import com.jihe.punchnet.sdlan.network.maskDigitToInt
@ -10,9 +11,41 @@ sealed class PreferenceName(val name: String) {
object PreferenceToken: PreferenceName("token")
}
private fun getEncryptedSharedPrefs(context: Context): SharedPreferences {
val masterKey = androidx.security.crypto.MasterKey.Builder(context)
.setKeyScheme(androidx.security.crypto.MasterKey.KeyScheme.AES256_GCM)
.build()
return androidx.security.crypto.EncryptedSharedPreferences.create(
context,
PreferenceRepositoryName,
masterKey,
androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
private fun getPlainSharedPrefs(context: Context): SharedPreferences {
return context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
}
private fun migratePlainPreference(context: Context, name: PreferenceName, encryptedPrefs: SharedPreferences) {
if (encryptedPrefs.contains(name.name)) return
val plainPrefs = getPlainSharedPrefs(context)
if (!plainPrefs.contains(name.name)) return
when (val value = plainPrefs.all[name.name]) {
is String -> encryptedPrefs.edit().putString(name.name, value).apply()
is Int -> encryptedPrefs.edit().putInt(name.name, value).apply()
}
plainPrefs.edit().remove(name.name).apply()
}
fun getPreferenceString(context: Context, name: PreferenceName): String? {
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
try {
val preference = getEncryptedSharedPrefs(context)
migratePlainPreference(context, name, preference)
return preference.getString(name.name, "")
} catch (e: Exception) {
return null
@ -20,8 +53,9 @@ fun getPreferenceString(context: Context, name: PreferenceName): String? {
}
fun getPreferenceInt(context: Context, name: PreferenceName): Int? {
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
try {
val preference = getEncryptedSharedPrefs(context)
migratePlainPreference(context, name, preference)
return preference.getInt(name.name, 0)
} catch (e: Exception) {
return null
@ -29,17 +63,21 @@ fun getPreferenceInt(context: Context, name: PreferenceName): Int? {
}
fun setPreferenceString(context: Context, name: PreferenceName, value: String) {
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
val editor = preference.edit()
editor.putString(name.name, value)
editor.apply()
try {
val preference = getEncryptedSharedPrefs(context)
preference.edit().putString(name.name, value).apply()
} catch (e: Exception) {
return
}
}
fun setPreferenceInt(context: Context, name: PreferenceName, value: Int) {
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
val editor = preference.edit()
editor.putInt(name.name, value)
editor.apply()
try {
val preference = getEncryptedSharedPrefs(context)
preference.edit().putInt(name.name, value).apply()
} catch (e: Exception) {
return
}
}

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
@ -50,33 +51,45 @@ fun AppNavHost2(
paddingValues: PaddingValues
) {
val context = androidx.compose.ui.platform.LocalContext.current
val token = com.jihe.punchnet.helper.getPreferenceString(context, com.jihe.punchnet.helper.PreferenceName.PreferenceToken)
val startDest = if (token.isNullOrEmpty()) Screen.LoginScreen.route else Screen.MainScreen.route
NavHost(
navController = navController,
startDestination = Screen.MainScreen.route,
startDestination = startDest,
modifier = Modifier.padding(paddingValues)
) {
composable(Screen.LoginScreen.route) {
LoginScreen(navController)
}
composable(Screen.MainScreen.route) {
HomeScreen(buttonViewModel, serviceViewModel, routeViewModel)
HomeScreen(buttonViewModel, serviceViewModel, routeViewModel, navController)
}
composable(Screen.RouteScreen.route) {
CustomHeaderScreen(
"路由设置"
) {
RouteScreen(ButtonRepository.buttonState.collectAsState().value != ButtonState.ButtonStarted, routeViewModel)
// LightScreen()
composable(Screen.DeviceScreen.route) {
CustomHeaderScreen("设备") {
DeviceScreen(navController)
}
}
composable(Screen.ProfileScreen.route) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text(
"TODO",
style = MaterialTheme.typography.titleLarge,
composable(Screen.AppAuthScreen.route) {
CustomHeaderScreen("应用授权") {
AppAuthScreen(navController)
}
}
composable(Screen.SettingsScreen.route) {
CustomHeaderScreen("设置") {
SettingsScreen(ButtonRepository.buttonState.collectAsState().value != ButtonState.ButtonStarted, routeViewModel)
}
}
composable(
route = Screen.WebViewScreen.route,
arguments = listOf(navArgument("url") { type = androidx.navigation.NavType.StringType })
) { backStackEntry ->
val url = backStackEntry.arguments?.getString("url") ?: ""
WebViewScreen(
url = url,
onBack = { navController.popBackStack() }
)
}
// Profile(dbdao, navController)
}
}
}

View File

@ -37,17 +37,21 @@ fun AppBottomNavigation(
Icon(
imageVector = when (screen) {
Screen.MainScreen -> Icons.Default.Home
Screen.RouteScreen -> Icons.Default.Menu
// Screen.Search -> Icons.Default.Search
Screen.ProfileScreen -> Icons.Default.Person
Screen.DeviceScreen -> Icons.Default.Menu
Screen.AppAuthScreen -> Icons.Default.Person
else -> Icons.Default.Home
},
contentDescription = screen.route
)
},
label = {
Text(
text = screen.route.replaceFirstChar { it.uppercase() },
)
val labelText = when (screen) {
Screen.MainScreen -> "首页"
Screen.DeviceScreen -> "设备"
Screen.AppAuthScreen -> "应用授权"
else -> screen.route
}
Text(text = labelText)
},
selected = currentRoute == screen.route,
onClick = {

View File

@ -6,12 +6,14 @@ import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.content.pm.PackageManager
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.core.content.ContextCompat
import androidx.compose.foundation.clickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -64,6 +66,12 @@ import com.jihe.punchnet.helper.PreferenceName
import com.jihe.punchnet.helper.getPreferenceString
import com.jihe.punchnet.helper.setPreferenceString
import kotlin.math.exp
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material.icons.filled.Person
import androidx.compose.ui.graphics.Color
import androidx.compose.foundation.background
data class HomeDropDownInfo(
@DrawableRes val icon: Int,
@ -72,32 +80,30 @@ data class HomeDropDownInfo(
)
@Composable
fun HomeDropdownMenu(
showTokenDialog: MutableState<Boolean>,
) {
fun HomeDropdownMenu(navController: androidx.navigation.NavHostController) {
var expanded by remember { mutableStateOf(false) }
// var showTokenDialog by remember { mutableStateOf(false) }
// val showSettingDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
val homeItems = arrayOf(
HomeDropDownInfo(
R.drawable.tag,
"修改token",
R.drawable.tag, // TODO: Use settings icon if available, or just standard icon
"设置",
{
showTokenDialog.value = true
navController.navigate(Screen.SettingsScreen.route)
}
),
/*
HomeDropDownInfo(
R.drawable.preferences,
"修改云端配置"
R.drawable.tag,
"退出登录",
{
// Clear token and go to LoginScreen
setPreferenceString(context, PreferenceName.PreferenceToken, "")
navController.navigate(Screen.LoginScreen.route) {
popUpTo(Screen.MainScreen.route) { inclusive = true }
}
}
)
*/
)
Box() {
IconButton(
interactionSource = remember { MutableInteractionSource() },
@ -125,7 +131,6 @@ fun HomeDropdownMenu(
item.callback?.invoke()
},
text = {
Row (
verticalAlignment = Alignment.CenterVertically
){
@ -142,12 +147,10 @@ fun HomeDropdownMenu(
Text(text=item.name)
}
}
)
}
}
}
}
@ -156,15 +159,13 @@ fun HomeScreen(
buttonViewModel: ButtonViewModel,
serviceViewModel: ServiceViewModel,
routeViewModel: RouteViewModel,
// started: MutableState<Boolean>,
navController: androidx.navigation.NavHostController,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val tkPref = getPreferenceString(context, PreferenceName.PreferenceToken)
var tokenvalue by remember {mutableStateOf(tkPref?:"")}
val allroutes = routeViewModel.allRoutes.collectAsState(emptyList())
val vpnLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
@ -174,7 +175,6 @@ fun HomeScreen(
// serviceViewModel.startService(context)
// serviceViewModel.setVPNPermission(true)
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
println("now token value2 = '${tokenvalue}'")
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
}
RESULT_CANCELED -> {
@ -183,60 +183,38 @@ fun HomeScreen(
}
}
val notificationPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
if (!isGranted) {
Toast.makeText(context, "未授予通知权限,连接可能会在后台被杀", Toast.LENGTH_SHORT).show()
}
val intent = VpnService.prepare(context.applicationContext)
if (intent != null) {
vpnLauncher.launch(intent)
} else {
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
}
}
val buttonState = buttonViewModel.buttonState.collectAsState()
val showTokenDialog = remember { mutableStateOf(false) }
val resourceList by com.jihe.punchnet.data.NodeRepository.resources.collectAsState()
CustomHeaderScreen(
"",
onBack = null,
onMenu = {
HomeDropdownMenu(showTokenDialog)
HomeDropdownMenu(navController)
}
) {
if (showTokenDialog.value) {
AlertDialog(
onDismissRequest = {showTokenDialog.value=false},
confirmButton = {
Button(
onClick = {
setPreferenceString(context, PreferenceName.PreferenceToken, tokenvalue)
println("now token value = '${tokenvalue}'")
showTokenDialog.value = false
}
LazyColumn(
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("确定")
}
},
title = {
Text("修改token")
},
text = {
TextField(
value = tokenvalue,
onValueChange = { newValue ->
tokenvalue = newValue.trim()
},
leadingIcon = {
Icon(
painter = painterResource(R.drawable.tag),
contentDescription = "token",
modifier = Modifier.size(24.dp)
)
},
)
}
item {
Spacer(modifier = Modifier.height(50.dp))
)
}
Column(
modifier = Modifier.fillMaxWidth()
.padding(top=50.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(R.drawable.punchnet_log),
contentDescription = "logo",
@ -260,20 +238,20 @@ fun HomeScreen(
Button(
onClick = {
if (buttonViewModel.buttonState.value == ButtonState.ButtonStarted) {
// if (serviceViewModel.isRunning.value) {
// if is running, should stop service
serviceViewModel.stopVpnService(context)
} else if (buttonViewModel.buttonState.value == ButtonState.ButtonStopped){
val intent = VpnService.prepare(context)
if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
notificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
} else {
val intent = VpnService.prepare(context.applicationContext)
if (intent != null) {
vpnLauncher.launch(intent)
} else {
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
println("now token value1 = '${tokenvalue}'")
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
}
}
// started.value = !started.value
}
},
enabled = buttonState.value.enabled,
shape = RoundedCornerShape(10.dp),
@ -281,20 +259,111 @@ fun HomeScreen(
.width(120.dp)
.height(40.dp)
) {
Text(
buttonState.value.text
// buttonViewModel.buttonText.value
/*
if (serviceViewModel.isRunning.value) {
"停止"
} else {
"启动"
Text(buttonState.value.text)
}
*/
Spacer(modifier = Modifier.height(48.dp))
}
if (resourceList.isNotEmpty()) {
item {
Text(
text = "企业资源 (${resourceList.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp, start = 24.dp).fillMaxWidth()
)
}
items(resourceList) { resource ->
Box(modifier = Modifier.padding(horizontal = 24.dp)) {
ResourceItemRow(resource, navController)
}
}
}
}
}
}
@Composable
fun ResourceItemRow(resource: com.jihe.punchnet.api.ResourceItem, navController: androidx.navigation.NavHostController) {
androidx.compose.material3.Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).clickable {
navController.navigate(Screen.WebViewScreen.createRoute(resource.url))
},
colors = androidx.compose.material3.CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.tag),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(resource.name, fontWeight = FontWeight.Bold, fontSize = 16.sp)
Text(resource.url, fontSize = 14.sp, color = MaterialTheme.colorScheme.primary)
}
Box(
modifier = Modifier
.size(12.dp)
.background(
color = if (resource.connectionStatus == "connected") Color(0xFF4CAF50) else Color.Gray,
shape = androidx.compose.foundation.shape.CircleShape
)
)
}
}
}
@Composable
fun NodeItemRow(node: com.jihe.punchnet.api.NodeItem) {
androidx.compose.material3.Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
colors = androidx.compose.material3.CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
// Icon
val isMobile = node.system?.contains("Android", true) == true || node.system?.contains("iOS", true) == true
Icon(
imageVector = if (isMobile) Icons.Default.Phone else Icons.Default.Person,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.width(16.dp))
// Texts
Column(modifier = Modifier.weight(1f)) {
Text(node.name, fontWeight = FontWeight.Bold, fontSize = 16.sp)
Text(node.ip, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
// Status dot
Box(
modifier = Modifier
.size(12.dp)
.background(
color = if (node.connectionStatus == "connected") Color(0xFF4CAF50) else Color.Gray,
shape = androidx.compose.foundation.shape.CircleShape
)
)
}
// BasicCardList(dbdao = dbdao, navController)
}
}

View File

@ -15,6 +15,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@ -25,15 +29,20 @@ import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.data.ServiceViewModel
sealed class Screen(val route: String) {
object MainScreen: Screen("main");
object RouteScreen: Screen("routes")
object ProfileScreen: Screen("profile")
object MainScreen: Screen("home") // 首页
object DeviceScreen: Screen("devices") // 设备 (取代原先的 Route)
object AppAuthScreen: Screen("appAuth") // 应用授权 (取代原先的 Profile)
object SettingsScreen: Screen("settings") // 设置
object WebViewScreen: Screen("webview/{url}") {
fun createRoute(url: String) = "webview/${android.net.Uri.encode(url)}"
}
object LoginScreen: Screen("login")
}
val bottomNavItems = listOf(
Screen.MainScreen,
Screen.RouteScreen,
Screen.ProfileScreen,
Screen.DeviceScreen,
Screen.AppAuthScreen,
)
@Composable
@ -44,10 +53,16 @@ fun MainApp(
navController: NavHostController,
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute by remember { derivedStateOf { navBackStackEntry?.destination?.route } }
val showBottomBar = currentRoute != Screen.LoginScreen.route
Scaffold(
bottomBar = {
if (showBottomBar) {
AppBottomNavigation(navController = navController)
}
}
){ paddingValues ->
/*
Image(

View File

@ -1,211 +0,0 @@
package com.jihe.punchnet.screen
import android.widget.Toast
import androidx.collection.emptyLongSet
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.room.util.TableInfo
import com.jihe.punchnet.data.RouteItem
import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.helper.IntToIPString
import com.jihe.punchnet.helper.parseCIDRAndGW
import com.jihe.punchnet.sdlan.network.maskIPToDigit
@Composable
fun AddOrModifyRoute(
routeViewModel: RouteViewModel,
initRoute: MutableState<RouteItem?>,
shouldShowRoute: MutableState<Boolean>,
) {
if (shouldShowRoute.value) {
val context = LocalContext.current
var cidr by remember {
mutableStateOf(
if (initRoute.value == null)
""
else
""
)
}
var gw by remember {
mutableStateOf(
if (initRoute.value == null)
""
else
""
)
}
AlertDialog(
onDismissRequest = {
shouldShowRoute.value = false
},
confirmButton = {
Button(
onClick = {
val item = parseCIDRAndGW(cidr, gw)
if (item == null) {
Toast.makeText(context, "数据解析出错", Toast.LENGTH_SHORT).show()
} else {
routeViewModel.insert(item)
shouldShowRoute.value = false
}
}
) {
Text("确定")
}
},
title = {
Text(
"添加新路由"
)
},
text = {
Column {
TextField(
value = cidr,
onValueChange = { newValue ->
cidr = newValue
},
placeholder = {
Text(
"192.168.0.0/24",
color = MaterialTheme.colorScheme.onBackground.copy(.5f)
)
},
label = {
Text("CIDR")
},
modifier = Modifier.padding(start = 16.dp)
)
TextField(
value = gw,
onValueChange = { newValue ->
gw = newValue
},
placeholder = {
Text(
"10.167.69.13",
color = MaterialTheme.colorScheme.onBackground.copy(.5f)
)
},
label = {
Text("gateway")
},
modifier = Modifier.padding(start = 16.dp, top = 8.dp)
)
}
}
)
}
}
@Composable
fun RouteScreen(
enabled: Boolean,
routeViewModel: RouteViewModel,
modifier: Modifier = Modifier,
) {
val showAddRoute: MutableState<RouteItem?> = remember { mutableStateOf(null) }
val shouldShow: MutableState<Boolean> = remember { mutableStateOf(false) }
AddOrModifyRoute(routeViewModel, showAddRoute, shouldShow)
val routeItems by routeViewModel.allRoutes.collectAsState(initial = emptyList())
LazyColumn {
item {
Row(
) {
}
}
items(routeItems) {item ->
RouteCard(routeViewModel, item, enabled)
}
item {
Button(
enabled = enabled,
shape = RectangleShape,
onClick = {
shouldShow.value = true
},
modifier = Modifier.fillMaxWidth()
) {
Text("添加路由")
}
}
}
}
@Composable
fun RouteCard(
routeViewModel: RouteViewModel,
item: RouteItem,
enabled: Boolean,
modifier: Modifier = Modifier,
) {
Card (
modifier = Modifier.fillMaxWidth()
){
Row (
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.height(36.dp)
){
Icon(
Icons.Default.Clear,
contentDescription = "delete route",
modifier = Modifier.size(30.dp).clickable(
enabled = enabled,
) {
routeViewModel.deleteById(item.id)
}
)
Text(
"${IntToIPString(item.net_ip)}/${maskIPToDigit(item.mask_ip)}",
modifier = Modifier.padding(start = 8.dp)
)
Text(
IntToIPString(item.gateway),
modifier = Modifier.padding(start = 8.dp)
)
}
}
}

View File

@ -5,7 +5,9 @@ data class Arguments(
// udp info of the super node
val sn: String,
// tcp info of the super node
val tcp: String,
// udp info of the STUN server
val stun: String,
val nat_server1: String,
val nat_server2: String,

View File

@ -52,8 +52,9 @@ class NodeConfig (
val localPort: Int,
val supernode: SDLanSock,
val natServer1: InetSocketAddress,
val natServer2: InetSocketAddress
val stunServer: SDLanSock,
val natServer1: java.net.InetSocketAddress,
val natServer2: java.net.InetSocketAddress
)
class NodeStats private constructor(
@ -148,8 +149,16 @@ class Node private constructor (
var startStopChannel: SendChannel<StartStopChanInfo>,
val mac: Mac = generateRandomMAC(),
var nat_type: NatType = NatType.Invalid,
var nat_type: NatType = NatType.PortRestricted,
val natProbeCookie: NatProbeCookie = NatProbeCookie(),
var authData: com.jihe.punchnet.api.AuthResponseData? = null,
var connectData: com.jihe.punchnet.api.ConnectResponseData? = null,
var clientId: String = "",
var sessionToken: com.google.protobuf.ByteString = com.google.protobuf.ByteString.EMPTY,
val queriedPolicies: ConcurrentHashMap<Int, Int> = ConcurrentHashMap(),
val queriedPolicyTargets: ConcurrentHashMap<Int, Int> = ConcurrentHashMap(),
val policyRules: ConcurrentHashMap<Int, ByteArray> = ConcurrentHashMap(),
) {
companion object {
private var instance: Node? = null
@ -176,11 +185,6 @@ class Node private constructor (
toSocket,
startStopChannel,
)
} else {
instance?.config = config
instance?.udpSockV4 = v4Sock
instance?.token = token
Log.e("Initialize", "not initialize instance")
}
return instance!!
}
@ -237,24 +241,51 @@ class Node private constructor (
}
suspend fun sendStunRequest() {
if (this.clientId.isEmpty()) {
this.clientId = "punchnet_android_client_" + UniqueNodeID.getUUID()
}
val req = SDLStunRequest.newBuilder()
.setCookie(0)
.setClientId(UniqueNodeID.getUUID())
.setClientId(this.clientId)
.setNetworkId(this.networkID.get())
.setIp(this.deviceConfig.ip.netAddr)
.setMac(this.mac)
.setNatType(this.nat_type.toByte().toInt())
.setV6Info(PunchProto.SDLV6Info.getDefaultInstance())
.setSessionToken(this.sessionToken)
.build()
val msg = encodeToUDPMessage(req, PacketType.StunRequest)
sendToSock(this, msg, config.supernode)
TerminalLogger.debugf {
"send STUN_REQUEST to ${config.stunServer}, clientIdTail=${clientId.takeLast(12)}, " +
"network=${req.networkId}, ip=${com.jihe.punchnet.sdlan.utils.ipToString(req.ip)}, " +
"mac=${com.jihe.punchnet.sdlan.utils.macToString(req.mac)}, natType=${nat_type}, " +
"sessionTokenBytes=${sessionToken.size()}"
}
sendToSock(this, msg, config.stunServer)
}
suspend fun ping_to_sn() {
val msg = encodeToControlMessage(null, PacketType.Ping).toByteArray()
_sendDataToSocket(msg)
}
suspend fun _sendDataToSocket(msg: ByteArray) {
if (aes.isAuthorized()) {
TerminalLogger.debugf{"authorized, ping to sn"}
toSocket.send(msg)
} else {
TerminalLogger.debugf{"unauthorized, not ping to sn"}
}
}
suspend fun probeNatType() {
val previousNatType = nat_type
val reply1 = this._sendAndWaitForProbeReply(StunProbeAttr.None, config.natServer1)
if (reply1 == null) {
nat_type = NatType.Blocked
TerminalLogger.warning { "STUN probe attr=None timed out; keeping natType=${nat_type}" }
return
}
if (reply1.ip == (outerIPV4.get())) {
@ -275,7 +306,14 @@ class Node private constructor (
val reply3 = this._sendAndWaitForProbeReply(StunProbeAttr.None, config.natServer2)
if (reply3 == null) {
nat_type = NatType.Blocked
nat_type = if (previousNatType == NatType.Invalid || previousNatType == NatType.Blocked) {
NatType.PortRestricted
} else {
previousNatType
}
TerminalLogger.warning {
"secondary STUN probe timed out after primary reply; keeping natType=${nat_type}"
}
return
}
@ -293,38 +331,28 @@ class Node private constructor (
}
suspend fun _sendAndWaitForProbeReply(attr: StunProbeAttr, toServer: SocketAddress): SDLStunProbeReply? {
val channel = Channel<SDLStunProbeReply>(100)
suspend fun _sendAndWaitForProbeReply(attr: StunProbeAttr, toServer: java.net.SocketAddress): SDLStunProbeReply? {
val channel = kotlinx.coroutines.channels.Channel<SDLStunProbeReply>(100)
val cookie = natProbeCookie.addChannel(channel)
val probe = SDLStunProbe.newBuilder()
val probe = com.jihe.punchnet.protobuf.PunchProto.SDLStunProbe.newBuilder()
.setAttr(attr.ordinal.toInt())
.setCookie(cookie)
.build()
val msg = encodeToUDPMessage(probe, PacketType.StunProbe)
TerminalLogger.debugf { "send STUN_PROBE attr=$attr cookie=$cookie to $toServer" }
this.udpSockV4.send_to(msg.toByteArray(), toServer)
val k = withTimeoutOrNull(5000) {
val k = kotlinx.coroutines.withTimeoutOrNull(5000) {
val response = channel.receive()
return@withTimeoutOrNull response
}
if (k == null) {
TerminalLogger.warning { "STUN_PROBE attr=$attr cookie=$cookie to $toServer timed out" }
} else {
TerminalLogger.debugf { "STUN_PROBE attr=$attr cookie=$cookie reply ip=${k.ip} port=${k.port}" }
}
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()) {
TerminalLogger.debugf{"authorized, ping to sn"}
toSocket.send(msg)
} else {
TerminalLogger.debugf{"unauthorized, not ping to sn"}
}
}
}

View File

@ -1,21 +1,29 @@
package com.jihe.punchnet.sdlan.network
import com.google.protobuf.kotlin.toByteString
import com.google.gson.JsonElement
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.protobuf.PunchProto.SDLArpRequest
import com.jihe.punchnet.protobuf.PunchProto.SDLArpResponse
import com.jihe.punchnet.protobuf.PunchProto.SDLExposedServiceRequest
import com.jihe.punchnet.protobuf.PunchProto.SDLPolicyRequest
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.ipStringToInt
import com.jihe.punchnet.sdlan.utils.ipToString
import com.jihe.punchnet.sdlan.utils.macToString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.net.URI
import java.nio.ByteBuffer
import java.nio.ByteOrder
@ -299,15 +307,311 @@ suspend fun sendRegister(
.build()
val msg = encodeToUDPMessage(register, PacketType.Register)
TerminalLogger.debugf { "send REGISTER to ${macToString(mac)} at $sock, natType=$natType" }
sendToSock(node, msg, sock)
// TODO: need guess port
if (natType == NatType.Symmetric || natType == NatType.PortRestricted) {
TerminalLogger.debugf { "guess ports for symmetric/restricted nat" }
val guessOffsets = intArrayOf(-1, 1, -2, 2)
for (offset in guessOffsets) {
val guessPort = sock.port + offset
if (guessPort in 1..65535) {
val guessSock = SDLanSock(sock.family, guessPort, sock.ip)
sendToSock(node, msg, guessSock)
}
}
}
}
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 sendPacketToNet(node: Node, dstmac: Mac, encrypted: ByteArray, size: Long) {
var is_p2p: Boolean = false
var is_multicast: Boolean = false
var destination: SDLanSock
if (isMultiBroadcast(dstmac)) {
node.stats.txSup.addAndGet(size)
node.stats.txBroadcast.addAndGet(size)
destination = node.config.stunServer
is_multicast = true
} else {
val peer = node.knownPeers.get(dstmac)
if (peer == null) {
node.stats.txSup.addAndGet(size)
destination = node.config.stunServer
} 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)
destination = node.config.stunServer
} else {
is_p2p = true
node.stats.txP2P.addAndGet(size)
destination = peer.sock
}
}
}
if (!is_p2p && !is_multicast) {
TerminalLogger.debugf { "check_query_peer_info" }
checkQueryPeerInfo(node, dstmac)
}
val transportIdentityId = node.connectData?.identityId ?: 0
val data = com.jihe.punchnet.protobuf.PunchProto.SDLData.newBuilder()
.setIsP2P(is_p2p)
.setNetworkId(node.networkID.get())
.setTtl(2)
.setSrcMac(node.mac)
.setDstMac(dstmac)
.setData(com.google.protobuf.ByteString.copyFrom(encrypted))
.setSessionToken(node.sessionToken)
.setIdentityId(transportIdentityId)
.build()
val msg = encodeToUDPMessage(data, PacketType.Data)
TerminalLogger.debugf {
"send PACKET to ${destination} with isP2P=$is_p2p, ttl=${data.ttl}, " +
"src=${macToString(data.srcMac)}, dst=${macToString(data.dstMac)}, " +
"network=${data.networkId}, identityId=${data.identityId}, apiIdentity=${node.connectData?.identityId ?: 0}, " +
"sessionTokenBytes=${data.sessionToken.size()}, encryptedBytes=${encrypted.size}"
}
sendToSock(node, msg, destination)
}
// Removed dummy findPeerDestination
fun describeIpv4Packet(data: ByteArray): String {
if (data.size < 20) {
return "short-ipv4 bytes=${data.size}"
}
val version = (data[0].toInt() ushr 4) and 0x0f
if (version != 4) {
return "non-ipv4 version=$version bytes=${data.size}"
}
val ihl = (data[0].toInt() and 0x0f) * 4
if (ihl < 20 || data.size < ihl) {
return "bad-ipv4-header ihl=$ihl bytes=${data.size}"
}
val proto = data[9].toInt() and 0xff
val srcIp = ByteBuffer.wrap(data, 12, 4).order(ByteOrder.BIG_ENDIAN).int
val dstIp = ByteBuffer.wrap(data, 16, 4).order(ByteOrder.BIG_ENDIAN).int
val protoName = when (proto) {
1 -> "ICMP"
6 -> "TCP"
17 -> "UDP"
else -> "proto-$proto"
}
val portInfo = if ((proto == 6 || proto == 17) && data.size >= ihl + 4) {
val srcPort = ByteBuffer.wrap(data, ihl, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
val dstPort = ByteBuffer.wrap(data, ihl + 2, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
" srcPort=$srcPort dstPort=$dstPort"
} else {
""
}
return "$protoName ${ipToString(srcIp)} -> ${ipToString(dstIp)}$portInfo bytes=${data.size}"
}
fun describePolicyRules(rules: ByteArray): String {
if (rules.isEmpty()) {
return "empty"
}
val result = mutableListOf<String>()
var offset = 0
while (offset + 3 <= rules.size) {
val proto = rules[offset].toInt() and 0xff
val port = ByteBuffer.wrap(rules, offset + 1, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
val protoName = when (proto) {
1 -> "ICMP"
6 -> "TCP"
17 -> "UDP"
else -> "proto-$proto"
}
result.add("$protoName/$port")
offset += 3
}
if (offset != rules.size) {
result.add("trailingBytes=${rules.size - offset}")
}
return result.joinToString(",")
}
data class PolicyDecision(
val state: PolicyDecisionState,
val reason: String,
)
enum class PolicyDecisionState {
Allowed,
Pending,
Denied,
}
fun policyDecisionForIpv4Packet(node: Node, data: ByteArray): PolicyDecision {
if (data.size < 20) {
return PolicyDecision(PolicyDecisionState.Denied, "too-short")
}
val version = (data[0].toInt() ushr 4) and 0x0f
if (version != 4) {
return PolicyDecision(PolicyDecisionState.Denied, "non-ipv4")
}
val ihl = (data[0].toInt() and 0x0f) * 4
if (ihl < 20 || data.size < ihl) {
return PolicyDecision(PolicyDecisionState.Denied, "bad-ipv4-header")
}
val proto = data[9].toInt() and 0xff
val targetIp = ByteBuffer.wrap(data, 16, 4).order(ByteOrder.BIG_ENDIAN).int
val port = if ((proto == 6 || proto == 17) && data.size >= ihl + 4) {
ByteBuffer.wrap(data, ihl + 2, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
} else {
0
}
val candidates = policyCandidateIdsForTarget(node, targetIp)
if (candidates.isEmpty()) {
return PolicyDecision(PolicyDecisionState.Denied, "no policy identity for ${ipToString(targetIp)}")
}
val missing = candidates.filterNot { node.policyRules.containsKey(it) }
if (missing.isNotEmpty()) {
return PolicyDecision(PolicyDecisionState.Pending, "waiting policy for ${ipToString(targetIp)}, ids=${missing.joinToString(",")}")
}
for (candidate in candidates) {
val rules = node.policyRules[candidate] ?: continue
if (policyRulesAllow(rules, proto, port)) {
return PolicyDecision(
PolicyDecisionState.Allowed,
"allowed by policy id=$candidate ${protocolName(proto)}/$port"
)
}
}
return PolicyDecision(
PolicyDecisionState.Denied,
"no allow rule for ${ipToString(targetIp)} ${protocolName(proto)}/$port, ids=${candidates.joinToString(",")}"
)
}
fun policyCandidateIdsForTarget(node: Node, targetIp: Int): List<Int> {
val result = mutableListOf<Int>()
node.connectData?.nodeList.orEmpty().firstOrNull {
runCatching { ipStringToInt(it.ip) == targetIp }.getOrDefault(false)
}?.let { result.add(it.id) }
result.addAll(matchingResourceIdsForTarget(node, targetIp))
return result.distinct()
}
fun policyRulesAllow(rules: ByteArray, proto: Int, port: Int): Boolean {
if (rules.isEmpty()) {
return false
}
var offset = 0
while (offset + 3 <= rules.size) {
val ruleProto = rules[offset].toInt() and 0xff
val rulePort = ByteBuffer.wrap(rules, offset + 1, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
if (ruleProto == proto && (rulePort == port || rulePort == 0)) {
return true
}
offset += 3
}
return false
}
fun protocolName(proto: Int): String {
return when (proto) {
1 -> "ICMP"
6 -> "TCP"
17 -> "UDP"
else -> "proto-$proto"
}
}
fun describeConnectDataForLog(node: Node): String {
val connectData = node.connectData ?: return "connect data is null"
val localNodeId = localNodeId(node) ?: 0
val nodeSummary = connectData.nodeList.orEmpty().joinToString(";") {
"id=${it.id},ip=${it.ip},status=${it.connectionStatus}"
}.ifEmpty { "none" }
val resourceSummary = connectData.resourceList.orEmpty().map { resource ->
val uri = runCatching { URI(resource.url) }.getOrNull()
val host = uri?.host ?: "invalid-host"
val scheme = uri?.scheme ?: "unknown"
val port = when {
uri == null -> -1
uri.port > 0 -> uri.port
scheme.equals("http", ignoreCase = true) -> 80
scheme.equals("https", ignoreCase = true) -> 443
else -> -1
}
"id=${resource.id},${scheme}://${host}:${port},status=${resource.connectionStatus}"
}.joinToString(";").ifEmpty { "none" }
return "connect summary: ip=${connectData.ip}/${connectData.maskLen}, identity=${connectData.identityId}, localNodeId=$localNodeId, " +
"nodes=${connectData.nodeList.orEmpty().size}[$nodeSummary], " +
"resources=${connectData.resourceList.orEmpty().size}[$resourceSummary], " +
"acl=${describeAclForLog(connectData.acl)}"
}
fun localNodeId(node: Node): Int? {
val localIp = node.connectData?.ip ?: return null
return node.connectData?.nodeList.orEmpty().firstOrNull { it.ip == localIp }?.id
}
fun describeAclForLog(acl: JsonElement?): String {
if (acl == null || acl.isJsonNull) {
return "null"
}
if (acl.isJsonArray) {
val array = acl.asJsonArray
if (array.size() == 0) {
return "array(size=0)"
}
val samples = array.take(3).mapIndexed { index, item ->
"[$index]=${describeAclElementForLog(item)}"
}
return "array(size=${array.size()}, ${samples.joinToString(",")})"
}
if (acl.isJsonObject) {
return describeAclObjectForLog(acl)
}
if (acl.isJsonPrimitive) {
return "primitive"
}
return acl.javaClass.simpleName
}
private fun describeAclElementForLog(element: JsonElement): String {
return when {
element.isJsonObject -> describeAclObjectForLog(element)
element.isJsonArray -> "array(size=${element.asJsonArray.size()})"
element.isJsonNull -> "null"
element.isJsonPrimitive -> "primitive"
else -> element.javaClass.simpleName
}
}
private fun describeAclObjectForLog(element: JsonElement): String {
val obj = element.asJsonObject
val fields = obj.entrySet().take(12).joinToString(",") { (key, value) ->
"$key=${describeAclValueForLog(value)}"
}
val suffix = if (obj.entrySet().size > 12) ",..." else ""
return "object($fields$suffix)"
}
private fun describeAclValueForLog(value: JsonElement): String {
return when {
value.isJsonNull -> "null"
value.isJsonArray -> "array(${value.asJsonArray.size()})"
value.isJsonObject -> "object(${value.asJsonObject.entrySet().joinToString(",") { it.key }})"
value.isJsonPrimitive -> {
val primitive = value.asJsonPrimitive
when {
primitive.isNumber -> primitive.asNumber.toString()
primitive.isBoolean -> primitive.asBoolean.toString()
primitive.isString -> primitive.asString.take(48)
else -> "primitive"
}
}
else -> value.javaClass.simpleName
}
}
@ -318,20 +622,20 @@ suspend fun findPeerDestination(node: Node, dstmac: Mac, size: Long): SDLanSock
if (isMultiBroadcast(dstmac)) {
node.stats.txSup.addAndGet(size)
node.stats.txBroadcast.addAndGet(size)
result = node.config.supernode
result = node.config.stunServer
is_multicast = true
} else {
val peer = node.knownPeers.get(dstmac)
if (peer == null) {
node.stats.txSup.addAndGet(size)
result = node.config.supernode
result = node.config.stunServer
} 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
result = node.config.stunServer
} else {
is_p2p = true
node.stats.txP2P.addAndGet(size)
@ -389,12 +693,12 @@ suspend fun sendQueryPeer(node: Node, dstmac: Mac) {
val query = SDLQueryInfo.newBuilder()
.setDstMac(dstmac)
.build()
val msg = encodeToTcpMessage(query, node.getNextPacketID(), PacketType.QueryInfo)
val msg = encodeToControlMessage(query, 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)
val buffer = ByteBuffer.allocate(14 + data.size).order(ByteOrder.BIG_ENDIAN)
buffer.put(dstmac)
buffer.put(srcmac)
buffer.putShort(EtherType.IPV4)
@ -407,54 +711,140 @@ fun formEthernetPacket(srcmac: ByteArray, dstmac: ByteArray, data: ByteArray): B
// 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())
if (!node.aes.isAuthorized()) {
TerminalLogger.errorf { "not authed for send arp request" }
return
}
println(6)
val arpReq = SDLArpRequest.newBuilder()
.setTargetIp(queryip)
.setOriginIp(node.deviceConfig.ip.netAddr)
.build()
val msg = encodeToControlMessage(arpReq, PacketType.ArpRequest)
node.toSocket.send(msg.toByteArray())
TerminalLogger.debugf { "sent QUIC ARP request for ip: ${com.jihe.punchnet.sdlan.utils.ipToString(queryip)}" }
}
suspend fun handleTcpCommand(node: Node, cmdtype: Byte, cmdprotobuf: ByteArray) {}
suspend fun handlePacketArpResponse(node: Node, content: ByteArray) {
val arpRes: SDLArpResponse
try {
arpRes = SDLArpResponse.parseFrom(content)
} catch (e: Exception) {
TerminalLogger.errorf { "failed to decode arp response: $e"}
return
}
val targetIp = arpRes.targetIp
val targetMac = arpRes.targetMac
if (targetMac.isEmpty) {
TerminalLogger.debugf { "ARP response returned empty MAC for IP: ${com.jihe.punchnet.sdlan.utils.ipToString(targetIp)}" }
return
}
TerminalLogger.debugf { "Rx QUIC ARP response for IP: ${com.jihe.punchnet.sdlan.utils.ipToString(targetIp)}, MAC: ${macToString(targetMac)}" }
sendPolicyRequestForTargetIp(node, targetIp)
if (node.iface is IfaceTun) {
val tun = node.iface as IfaceTun
tun.arpTable.addToARPTable(targetIp, targetMac.toByteArray())
tun.arpWaitList.arpArrived(node, targetIp, targetMac)
}
}
suspend fun sendExposedServiceRequest(node: Node) {
if (!node.aes.isAuthorized()) {
return
}
val req = SDLExposedServiceRequest.newBuilder()
.setVersion(0)
.build()
node.toSocket.send(encodeToControlMessage(req, PacketType.ExposedServiceRequest).toByteArray())
TerminalLogger.debugf { "sent ExposedServiceRequest version=0" }
}
suspend fun sendPolicyRequestForTargetIp(node: Node, targetIp: Int) {
if (!node.aes.isAuthorized()) {
return
}
val srcIdentityId = node.connectData?.identityId ?: 0
if (srcIdentityId == 0) {
TerminalLogger.warning { "skip PolicyRequest for ${ipToString(targetIp)}: missing src identity" }
return
}
val dstNode = node.connectData?.nodeList?.firstOrNull {
runCatching { ipStringToInt(it.ip) == targetIp }.getOrDefault(false)
}
if (dstNode == null) {
TerminalLogger.warning { "skip PolicyRequest for ${ipToString(targetIp)}: target not in node_list" }
return
}
TerminalLogger.debugf {
"policy target ${ipToString(targetIp)}: srcIdentity=$srcIdentityId, apiIdentity=${node.connectData?.identityId ?: 0}, " +
"nodeId=${dstNode.id}, nodeStatus=${dstNode.connectionStatus}, " +
describeResourcesForTarget(node, targetIp) + ", acl=${describeAclForLog(node.connectData?.acl)}"
}
sendPolicyRequest(node, srcIdentityId, dstNode.id, targetIp, "node:${dstNode.name}/${dstNode.connectionStatus}")
matchingResourceIdsForTarget(node, targetIp)
.filter { it != dstNode.id }
.forEach { resourceId ->
sendPolicyRequest(node, srcIdentityId, resourceId, targetIp, "resource")
}
}
suspend fun sendPolicyRequest(
node: Node,
srcIdentityId: Int,
dstIdentityId: Int,
targetIp: Int,
source: String,
) {
if (node.queriedPolicies.putIfAbsent(dstIdentityId, 0) != null) {
return
}
node.queriedPolicyTargets[dstIdentityId] = targetIp
val req = SDLPolicyRequest.newBuilder()
.setSrcIdentityId(srcIdentityId)
.setDstIdentityId(dstIdentityId)
.setVersion(0)
.build()
node.toSocket.send(encodeToControlMessage(req, PacketType.PolicyRequest).toByteArray())
TerminalLogger.debugf {
"sent PolicyRequest srcIdentity=$srcIdentityId, dstIdentity=$dstIdentityId, dstIp=${ipToString(targetIp)}, source=$source, version=0"
}
}
fun describeResourcesForTarget(node: Node, targetIp: Int): String {
val targetIpText = ipToString(targetIp)
val matches = node.connectData?.resourceList.orEmpty().mapNotNull { resource ->
val uri = runCatching { URI(resource.url) }.getOrNull() ?: return@mapNotNull null
if (uri.host != targetIpText) {
return@mapNotNull null
}
val port = when {
uri.port > 0 -> uri.port
uri.scheme.equals("http", ignoreCase = true) -> 80
uri.scheme.equals("https", ignoreCase = true) -> 443
else -> -1
}
"id=${resource.id}:${uri.scheme ?: "unknown"}/$port"
}.distinct()
return if (matches.isEmpty()) {
"no matching resource in connect resource_list for $targetIpText"
} else {
"matching resources for $targetIpText: ${matches.joinToString(",")}"
}
}
fun matchingResourceIdsForTarget(node: Node, targetIp: Int): List<Int> {
val targetIpText = ipToString(targetIp)
return node.connectData?.resourceList.orEmpty().mapNotNull { resource ->
val uri = runCatching { URI(resource.url) }.getOrNull() ?: return@mapNotNull null
if (uri.host == targetIpText) resource.id else null
}.distinct()
}
suspend fun handlePacketPeerInfo(node: Node, content: ByteArray) {
val pinfo: SDLPeerInfo
@ -470,6 +860,11 @@ suspend fun handlePacketPeerInfo(node: Node, content: ByteArray) {
return
}
if (!pinfo.hasV4Info() || pinfo.v4Info.v4.size() != 4 || pinfo.v4Info.port == 0) {
TerminalLogger.warning { "PeerInfo for ${macToString(pinfo.dstMac)} has no usable IPv4 endpoint" }
return
}
val remoteNat = NatType.fromUByte(pinfo.v4Info.natType.toUByte())
val pending = node.pendingPeers.get(pinfo.dstMac)
if (pending == null) {
@ -482,16 +877,18 @@ suspend fun handlePacketPeerInfo(node: Node, content: ByteArray) {
sendRegister(node, remoteNat, pending.sock, pinfo.dstMac)
}
suspend fun handleTcpEvent(node: Node, event: EventType, cmdprotobuf: ByteArray) {
when(event) {
EventType.SendRegister -> {
val reg: SDLSendRegisterEvent
suspend fun handleEvent(node: Node, cmdprotobuf: ByteArray) {
val evt: com.jihe.punchnet.protobuf.PunchProto.SDLEvent
try {
reg = SDLSendRegisterEvent.parseFrom(cmdprotobuf)
evt = com.jihe.punchnet.protobuf.PunchProto.SDLEvent.parseFrom(cmdprotobuf)
} catch (e: Exception) {
TerminalLogger.errorf {"failed to decode SendRegisterEvent: $e"}
TerminalLogger.errorf {"failed to decode SDLEvent: $e"}
return
}
when(evt.eventCase) {
com.jihe.punchnet.protobuf.PunchProto.SDLEvent.EventCase.SEND_REGISTER -> {
val reg = evt.sendRegister
val remoteNat = NatType.fromUByte(reg.natType.toUByte())
val ip = byteArrayOf(
(reg.natIp ushr 24).and(0xff).toByte(),
@ -502,7 +899,7 @@ suspend fun handleTcpEvent(node: Node, event: EventType, cmdprotobuf: ByteArray)
checkPeerRegistrationNeeded(node,false, reg.dstMac, remoteNat, SDLanSock(IPFamily.IPV4, reg.natPort, ip))
}
else -> {
TerminalLogger.warning { "unhandled event: $event" }
TerminalLogger.warning { "unhandled event: ${evt.eventCase}" }
}
}
}

View File

@ -4,10 +4,12 @@ import android.util.Log
import com.jihe.punchnet.PunchnetServiceArgument
import com.jihe.punchnet.data.ButtonRepository
import com.jihe.punchnet.data.ButtonState
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.SDLExposedServiceResponse
import com.jihe.punchnet.protobuf.PunchProto.SDLPolicyResponse
import com.jihe.punchnet.protobuf.PunchProto.SDLStunProbeReply
import com.jihe.punchnet.sdlan.config.Arguments
import com.jihe.punchnet.sdlan.config.RSAConfig
@ -47,52 +49,92 @@ import java.util.concurrent.atomic.AtomicLong
import kotlin.io.path.pathString
import kotlin.system.exitProcess
suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
suspend fun onMessage(scope: CoroutineScope, data: SDLanControl) {
val node = Node.getInstance()
TerminalLogger.debugf {"message received"}
when(data.packetType) {
PacketType.Welcome -> {
TerminalLogger.debugf { "got welcome, sending register super" }
if (node.clientId.isEmpty()) {
node.clientId = "punchnet_android_client_" + UniqueNodeID.getUUID()
}
val registerIp = com.jihe.punchnet.sdlan.utils.ipStringToInt(node.connectData?.ip ?: "0.0.0.0").toUInt().toInt()
val registerSuper = SDLRegisterSuper.newBuilder()
.setClientId(node.clientId)
.setNetworkId(node.authData?.networkId ?: 0)
.setMac(com.google.protobuf.ByteString.copyFrom(node.mac.toByteArray()))
.setIp(registerIp)
.setMaskLen(node.connectData?.maskLen ?: 0)
.setHostname(node.connectData?.hostname ?: "")
.setPubKey(node.rsa.getPublicKeyString())
.setAccessToken(node.authData?.accessToken ?: "")
.build()
val msg = encodeToControlMessage(registerSuper, PacketType.RegisterSuper)
node.toSocket.send(msg.toByteArray())
TerminalLogger.debugf {
"sent RegisterSuper clientIdTail=${node.clientId.takeLast(12)}, network=${registerSuper.networkId}, " +
"ip=${ipToString(registerSuper.ip)}, mask=${registerSuper.maskLen}, mac=${macToString(registerSuper.mac)}, " +
"identity=${node.connectData?.identityId ?: 0}"
}
}
PacketType.RegisterSuperACK -> {
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 algorithm = ack.algorithm.ifEmpty { "aes" }.lowercase()
TerminalLogger.debugf {
"got register super ack: algorithm=$algorithm, region=${ack.regionId}, sessionTokenBytes=${ack.sessionToken.size()}"
}
if (algorithm != "aes" && algorithm != "chacha20") {
TerminalLogger.errorf { "unsupported encryption algorithm from server: ${ack.algorithm}" }
node.startStopChannel.send(StartStopChanInfo(StartStopFlag.IsStop, null))
ButtonRepository.updateState(ButtonState.ButtonStopped)
scope.cancel()
return
}
val aeskey = node.rsa.decrypt(ack.aesKey.toByteArray())
val aeskey = node.rsa.decrypt(ack.key.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.aes.setSecret(aeskey, algorithm, ack.regionId.toLong())
node.sessionToken = ack.sessionToken
node.deviceConfig.ip.netAddr = com.jihe.punchnet.sdlan.utils.ipStringToInt(node.connectData?.ip ?: "0.0.0.0").toUInt().toInt()
node.deviceConfig.ip.netBitLen = (node.connectData?.maskLen ?: 24).toByte()
if (node.iface is IfaceTun) {
val tun = node.iface as IfaceTun
val maskVal = com.jihe.punchnet.sdlan.network.maskDigitToInt(node.deviceConfig.ip.netBitLen.toInt()) ?: 0xffffff00.toInt()
tun.arpTable.routeTable.addRoute(
com.jihe.punchnet.sdlan.network.RouteDetail(
mask = maskVal,
gw = 0,
maskedAddr = node.deviceConfig.ip.netAddr and maskVal
)
)
}
println("reloading config: node.iface = ${node.iface}")
node.iface?.reload_config(node.deviceConfig)
println("reloading config ok: node.iface = ${node.iface}")
println("reloading config ok: node.udp = ${node.udpSockV4}")
node.networkID.set(ack.devAddr.networkId)
// println("got aes key: ${aeskey.toList()}, length is ${aeskey.size}")
node.networkID.set(node.authData?.networkId ?: 0)
TerminalLogger.debugf {
"control registered: clientIdTail=${node.clientId.takeLast(12)}, network=${node.networkID.get()}, " +
"ip=${ipToString(node.deviceConfig.ip.netAddr)}, mask=${node.deviceConfig.ip.netBitLen}, " +
"mac=${macToString(node.mac)}, identity=${node.connectData?.identityId ?: 0}"
}
sendExposedServiceRequest(node)
node.sendStunRequest()
scope.launch {
try {
node.probeNatType()
TerminalLogger.debugf { "nat type is ${node.nat_type}"}
node.sendStunRequest()
TerminalLogger.debugf { "updated NAT type after probe: ${node.nat_type}" }
} catch (e: Exception) {
Log.e("NAT", "probe nat exited: $e")
TerminalLogger.warning { "NAT probe failed: $e" }
}
}
/*
CoroutineScope(Dispatchers.Default).async {
node.probeNatType()
TerminalLogger.debugf { "nat type is ${node.nat_type}"}
}
*/
}
PacketType.RegisterSuperNAK -> {
val nak = SDLRegisterSuperNak.parseFrom(data.currentPacket)
@ -117,7 +159,7 @@ suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
TerminalLogger.errorf { "malformed command received"}
return
}
handleTcpCommand(node, data.currentPacket[0], data.currentPacket.sliceArray(1..data.currentPacket.size-1))
// handleTcpCommand(node, data.currentPacket[0], data.currentPacket.sliceArray(1..data.currentPacket.size-1))
}
PacketType.PeerInfo -> {
TerminalLogger.debugf { "PeerInfo received" }
@ -125,19 +167,40 @@ suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
}
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)
handleEvent(node, data.currentPacket)
}
PacketType.Pong -> {
TerminalLogger.debugf { "tcp Pong Received" }
// node.ping_to_sn()
TerminalLogger.debugf { "Pong Received" }
}
PacketType.ArpResponse -> {
TerminalLogger.debugf { "ArpResponse received" }
handlePacketArpResponse(node, data.currentPacket)
}
PacketType.PolicyResponse -> {
val policy = SDLPolicyResponse.parseFrom(data.currentPacket)
node.queriedPolicies[policy.dstIdentityId] = policy.version
node.policyRules[policy.dstIdentityId] = policy.rules.toByteArray()
val ruleSummary = describePolicyRules(policy.rules.toByteArray())
val targetIp = node.queriedPolicyTargets[policy.dstIdentityId]
val targetText = targetIp?.let { ", dstIp=${ipToString(it)}, ${describeResourcesForTarget(node, it)}" } ?: ""
if (policy.rules.isEmpty) {
TerminalLogger.warning {
"PolicyResponse has no allow rules: srcIdentity=${policy.srcIdentityId}, " +
"dstIdentity=${policy.dstIdentityId}, version=${policy.version}$targetText"
}
} else {
TerminalLogger.debugf {
"PolicyResponse received srcIdentity=${policy.srcIdentityId}, dstIdentity=${policy.dstIdentityId}, " +
"version=${policy.version}, rules=$ruleSummary$targetText"
}
}
}
PacketType.ExposedServiceResponse -> {
val exposed = SDLExposedServiceResponse.parseFrom(data.currentPacket)
TerminalLogger.debugf {
"ExposedServiceResponse received version=${exposed.version}, " +
"tcpPorts=${exposed.tcpPortsList.size}, udpPorts=${exposed.udpPortsList.size}"
}
}
else -> {
println("error packet type: ${data.packetType.toUByte()}")
@ -145,7 +208,7 @@ suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
}
}
suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments, routeinfo: PunchnetServiceArgument?) {
suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments, authData: com.jihe.punchnet.api.AuthResponseData, connectData: com.jihe.punchnet.api.ConnectResponseData) {
UniqueNodeID.setBaseDir(argument.baseDir)
val edgeUUID = UniqueNodeID.getUUID()
val config = parseConfig(edgeUUID, argument)
@ -156,58 +219,24 @@ suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments,
return
}
val toSocket = Channel<ByteArray>(100)
val start_stop_channel = Channel<StartStopChanInfo>(100)
initEdge(scope, iface, argument.token, config, toSocket, start_stop_channel)
val tcp = argument.tcp.split(":")
val snParts = argument.sn.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()}"}
node.authData = authData
node.connectData = connectData
TerminalLogger.debugf { "self mac: ${macToString(node.mac)}"}
TerminalLogger.debugf { describeConnectDataForLog(node) }
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 onConnected: suspend (tech.kwik.core.QuicStream) -> Unit = { stream ->
TerminalLogger.debugf { "QUIC stream connected callback" }
// We wait for server's PACKET_WELCOME before sending RegisterSuper
}
/*
val onMessage: suspend (SDLanTCP) -> Unit = { data ->
}
*/
scope.launch {
try {
@ -219,15 +248,11 @@ suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments,
val data = node.iface?.recv()
Log.d("SDLAN", "async receive data from iface: ${data?.size} bytes")
if (data == null) {
//delay(1000)
delay(1000)
// println("got data is null")
continue
}
if (data.isEmpty()) {
delay(1000)
// println("got data size 0")
// delay(1000)
continue
}
Log.d("SDLAN", "handle data form device starts")
@ -241,33 +266,11 @@ suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments,
node.udpSockMulticast?.close()
}
}
/*
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 {
scope.launch {
initTCPConn(
initQUICConn(
scope,
tcp[0], tcp[1].toInt(),
snParts[0], snParts[1].toIntOrNull() ?: 1265,
start_stop_channel,
AtomicLong(now),
AtomicBoolean(false),
@ -281,7 +284,6 @@ suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments,
)
}
println("sending start")
start_stop_channel.send(StartStopChanInfo(StartStopFlag.IsStart, null))
println("sent start")
@ -308,6 +310,7 @@ suspend fun loopSocketV4(scope: CoroutineScope, node: Node, sock: SDLanSocket, c
try {
while(true) {
delay(10_000)
node.probeNatType()
node.sendStunRequest()
}
} catch (e: Exception) {
@ -387,6 +390,14 @@ suspend fun handleAPacket(scope: CoroutineScope, node: Node, from: SocketAddress
TerminalLogger.debugf { "got stun reply" }
return
}
PacketType.PolicyResponse -> {
TerminalLogger.debugf { "got policy response" }
return
}
PacketType.ExposedServiceResponse -> {
TerminalLogger.debugf { "got exposed service response" }
return
}
PacketType.Register -> {
TerminalLogger.debugf { "got REGISTER" }
if (from is InetSocketAddress) {
@ -447,213 +458,6 @@ fun initEdge(scope: CoroutineScope, iface: Iface, token: String, config: NodeCon
println(instance)
}
suspend fun initTCPConn(
scope: CoroutineScope,
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 (CoroutineScope, SDLanTCP)->Unit,
onDisconnected: suspend ()->Unit,
connectingChan: SendChannel<ConnectingState>?
) {
val started = AtomicBoolean(false)
// var started: Boolean = false
var startPktID: Int? = null
val node = Node.getInstance()
while (true) {
connectingChan?.send(ConnectingState.NotConnected)
if (!started.get()) {
while (true) {
println("waiting for start_stop")
val startStopInfo = start_stop.receive()
if (startStopInfo.flag == StartStopFlag.IsStart) {
started.set(true)
// started = true
startPktID = startStopInfo.packetID
break
}
TerminalLogger.debugf {
"start stop chan received ${startStopInfo}"
}
}
}
connectingChan?.send(ConnectingState.Connecting)
TerminalLogger.debugf { "try connecting tcp..." }
val socket: Socket
try {
withContext(Dispatchers.IO) {
TerminalLogger.debugf { "connecting to $tcpHost:$tcpPort" }
socket = Socket(tcpHost, tcpPort)
}
}catch(e: CancellationException) {
withContext(NonCancellable) {
node.iface?.close()
}
return
} catch (e: Exception) {
TerminalLogger.errorf { "failed to connect to ${tcpHost}:${tcpPort}: $e" }
delay(3000)
continue
}
try {
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 job_read_packet = scope.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(scope, tcpPacket)
}
} finally {
input.close()
TerminalLogger.errorf { "input closing" }
}
}
// val job_write_to_packet = CoroutineScope(Dispatchers.IO).async {
val job_write_to_packet = scope.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 {
val job_check_pong = scope.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 {
val job_check_stop = scope.async {
println("job check stop starts")
while (true) {
try {
val v = start_stop.receive()
if (v.flag == StartStopFlag.IsStop) {
started.set(false)
break
}
} catch (e: Exception) {
// started.set(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")
}
}
println("m1")
job_read_packet.cancelAndJoin()
println("m2")
job_write_to_packet.cancelAndJoin()
println("m3")
job_check_pong.cancelAndJoin()
println("m4")
job_check_stop.cancelAndJoin()
println("m5")
delay(1000)
} catch (e: Exception) {
withContext(NonCancellable) {
node.iface?.close()
socket.close()
}
}
}
}
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) {
@ -661,18 +465,23 @@ fun parseConfig(nodeuuid: String, argument: Arguments): NodeConfig? {
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
}
val stun = parseScoketAddressV4FromString(argument.stun)
if (stun == null) {
return null
}
val natServer1 = parseScoketAddressV4FromString(argument.nat_server1)
val natServer2 = parseScoketAddressV4FromString(argument.nat_server2)
if (natServer1 == null || natServer2 == null) {
return null
}
return NodeConfig(
baseDir = argument.baseDir,
name = argument.name,
@ -686,7 +495,8 @@ fun parseConfig(nodeuuid: String, argument: Arguments): NodeConfig? {
registerTTL = argument.registerTTL,
localPort = argument.localPort,
supernode = SDLanSock(IPFamily.IPV4, sn.port, sn.address.address),
stunServer = SDLanSock(IPFamily.IPV4, stun.port, stun.address.address),
natServer1 = natServer1,
natServer2 = natServer2,
natServer2 = natServer2
)
}

View File

@ -49,6 +49,15 @@ enum class PacketType(val id: UByte) {
StunProbe(0x32u),
StunProbeReply(0x33u),
Welcome(0x4fu),
ArpRequest(0x50u),
ArpResponse(0x51u),
PolicyRequest(0xb0u),
PolicyResponse(0xb1u),
ExposedServiceRequest(0xb2u),
ExposedServiceResponse(0xb3u),
Data(0xffu);
companion object {
@ -65,9 +74,8 @@ fun PacketType.toUByte(): UByte {
}
// tcp发送过来的通道里面的信息
class SDLanTCP(
val packetID: UInt,
// tcp/quic发送过来的通道里面的信息
class SDLanControl(
val packetType: PacketType,
val currentPacket: ByteArray,
)
@ -103,18 +111,13 @@ fun encodeToUDPMessage(msg: Message?, packetType: PacketType): List<Byte> {
return result
}
fun encodeToTcpMessage(msg: Message?, packetID: Int, packetType: PacketType): List<Byte> {
fun encodeToControlMessage(msg: Message?, 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)
.putShort((msgByte.size + 1).toShort())
.array().toList())
result.add(packetType.toUByte().toByte())

View File

@ -17,6 +17,8 @@ import kotlin.experimental.inv
import kotlin.experimental.or
import kotlin.random.Random
import android.content.Context
object UniqueNodeID {
var id: String = ""
private var baseDir: String = ""
@ -25,11 +27,18 @@ object UniqueNodeID {
baseDir = basedir
}
fun getUUID(): String {
if (baseDir.length == 0) {
baseDir = Environment.getExternalStorageDirectory().name
fun getUUID(context: Context? = null): String {
if (id.isEmpty() && context != null) {
val androidId = android.provider.Settings.Secure.getString(context.contentResolver, android.provider.Settings.Secure.ANDROID_ID)
if (!androidId.isNullOrEmpty() && androidId != "9774d56d682e549c") { // 9774d56d682e549c is a known bugged ID on old emulators
id = androidId
return id
}
}
if (id.isEmpty()) {
if (baseDir.isEmpty()) {
baseDir = Environment.getExternalStorageDirectory().absolutePath
}
if (id.length == 0) {
val dirpath = File(baseDir)
dirpath.mkdirs()
val f = File(baseDir, SDLanConfig.ID_FILENAME)
@ -71,6 +80,15 @@ fun ipToString(ip: Int): String {
return "$d1.$d2.$d3.$d4"
}
fun ipStringToInt(ipString: String): Int {
val parts = ipString.split(".")
if (parts.size != 4) return 0
return (parts[0].toInt() shl 24) or
(parts[1].toInt() shl 16) or
(parts[2].toInt() shl 8) or
(parts[3].toInt())
}
fun macToString(mac: Mac): String {
return mac.joinToString(separator = ":") { it.toUByte().toString(16) }
}

View File

@ -2,8 +2,8 @@ syntax = "proto3";
option java_package = "com.jihe.punchnet.protobuf";
option java_outer_classname = "PunchProto";
//
//
message SDLV4Info {
uint32 port = 1;
bytes v4 = 2;
@ -15,34 +15,30 @@ message SDLV6Info {
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 SDLWelcome {
uint32 version = 1;
uint32 max_bidi_streams = 2;
uint32 max_packet_size = 3;
uint32 heartbeat_sec = 4;
SDLV6Info ipv6_assist = 5;
}
message SDLRegisterSuper {
uint32 version = 1;
string installed_channel = 2;
string client_id = 3;
SDLDevAddr dev_addr = 4;
string pub_key = 5;
string token = 6;
string client_id = 1;
uint32 network_id = 2;
bytes mac = 3;
uint32 ip = 4;
uint32 mask_len = 5;
string hostname = 6;
string pub_key = 7;
string access_token = 8;
}
message SDLRegisterSuperAck {
SDLDevAddr dev_addr = 1;
bytes aes_key = 2;
uint32 upgrade_type = 3;
optional string upgrade_prompt = 4;
optional string upgrade_address = 5;
string algorithm = 1;
bytes key = 2;
uint32 region_id = 3;
bytes session_token = 4;
}
message SDLRegisterSuperNak {
@ -51,72 +47,115 @@ message SDLRegisterSuperNak {
}
//
message SDLQueryInfo {
bytes dst_mac = 1;
}
message SDLPeerInfo {
bytes dst_mac = 1;
SDLV4Info v4_info = 2;
optional SDLV4Info v4_info = 2;
optional SDLV6Info v6_info = 3;
}
//
message SDLNatChangedEvent {
bytes mac = 1;
uint32 ip = 2;
message SDLArpRequest {
uint32 target_ip = 1;
uint32 origin_ip = 2;
bytes context = 3;
}
message SDLSendRegisterEvent {
message SDLArpResponse {
uint32 target_ip = 1;
bytes target_mac = 2;
uint32 origin_ip = 3;
bytes context = 4;
}
message SDLPolicyRequest {
uint32 src_identity_id = 1;
uint32 dst_identity_id = 2;
uint32 version = 3;
}
message SDLPolicyResponse {
uint32 src_identity_id = 1;
uint32 dst_identity_id = 2;
uint32 version = 3;
bytes rules = 4;
}
message SDLExposedServiceRequest {
uint32 version = 1;
}
message SDLExposedServiceResponse {
uint32 version = 1;
repeated uint32 tcp_ports = 2;
repeated uint32 udp_ports = 3;
}
//
message SDLEvent {
message NatChanged {
bytes mac = 1;
uint32 ip = 2;
}
message SendRegister {
bytes dst_mac = 1;
uint32 nat_ip = 2;
uint32 nat_port = 3;
uint32 nat_type = 4;
optional SDLV6Info v6_info = 5;
}
}
message SDLNetworkShutdownEvent {
message ExposedServiceChanged {
}
message NetworkShutdown {
string message = 1;
}
oneof event {
NatChanged nat_changed = 1;
SendRegister send_register = 2;
NetworkShutdown shutdown = 3;
ExposedServiceChanged exposed_service_changed = 4;
}
}
//
message SDLCommand {
uint32 pkt_id = 1;
message SDLChangeNetworkCommand {
SDLDevAddr dev_addr = 1;
bytes aes_key = 2;
message ExitNodeControl {
int32 action = 1;
string remark = 2;
}
oneof command {
ExitNodeControl exit_node = 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;
uint32 pkt_id = 1;
int32 code = 2;
string message = 3;
bytes data = 4;
}
// 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;
string client_id = 1;
uint32 network_id = 2;
bytes mac = 3;
uint32 ip = 4;
uint32 nat_type = 5;
optional SDLV6Info v6_info = 6;
bytes session_token = 7;
}
message SDLStunReply {
uint32 cookie = 1;
}
message SDLData {
@ -126,6 +165,8 @@ message SDLData {
bool is_p2p = 4;
uint32 ttl = 5;
bytes data = 6;
bytes session_token = 7;
uint32 identity_id = 8;
}
message SDLRegister {
@ -141,14 +182,15 @@ message SDLRegisterAck {
}
//
message SDLStunProbe {
uint32 cookie = 1;
uint32 attr = 2;
uint32 step = 3;
}
message SDLStunProbeReply {
uint32 cookie = 1;
uint32 port = 2;
uint32 ip = 3;
uint32 step = 2;
uint32 port = 3;
uint32 ip = 4;
}

View File

@ -21,3 +21,4 @@ kotlin.code.style=official
# 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
org.gradle.java.home=/Users/stavid/Library/Java/JavaVirtualMachines/corretto-17.0.17/Contents/Home

0
gradlew vendored Normal file → Executable file
View File