diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..9b34f35
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+punchnet
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..4bb82bc 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 0000000..c61ea33
--- /dev/null
+++ b/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 38b4873..6be08cd 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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") }
- // }
-}
\ No newline at end of file
+ // Security Crypto
+ implementation("androidx.security:security-crypto:1.1.0-alpha06")
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6e2b630..8677415 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,8 @@
+
+
diff --git a/app/src/main/java/com/jihe/punchnet/MainActivity.kt b/app/src/main/java/com/jihe/punchnet/MainActivity.kt
index cce9d91..6a7d0d0 100644
--- a/app/src/main/java/com/jihe/punchnet/MainActivity.kt
+++ b/app/src/main/java/com/jihe/punchnet/MainActivity.kt
@@ -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}")
diff --git a/app/src/main/java/com/jihe/punchnet/PunchnetService.kt b/app/src/main/java/com/jihe/punchnet/PunchnetService.kt
index 830cfec..e436e6f 100644
--- a/app/src/main/java/com/jihe/punchnet/PunchnetService.kt
+++ b/app/src/main/java/com/jihe/punchnet/PunchnetService.kt
@@ -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..? {
+ return post("/auth/token", request)
+ }
+
+ suspend fun connect(request: ConnectRequest): BaseResponse? {
+ return post("/connect", request)
+ }
+
+ suspend fun disconnect(request: DisconnectRequest): BaseResponse? {
+ return post("/disconnect", request)
+ }
+
+ suspend fun loginWithAccount(request: AuthLoginRequest): BaseResponse? {
+ return post("/auth/login", request)
+ }
+
+ suspend fun getNodeResources(request: GetNodeResourcesRequest): BaseResponse? {
+ return post("/get_node_resources", request)
+ }
+
+ suspend fun getAcl(request: AclRequest): BaseResponse? {
+ return post("/acl", request)
+ }
+
+ private suspend inline fun post(path: String, requestBody: T): BaseResponse? {
+ 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(code = -1, message = "HTTP ${response.code}: ${response.message}", data = null)
+ }
+ val responseStr = response.body?.string() ?: return@withContext BaseResponse(code = -1, message = "Empty response body", data = null)
+ Log.i("apiClient", "response [${BASE_URL + path}]: HTTP ${response.code}")
+
+ val type = object : TypeToken>() {}.type
+ gson.fromJson>(responseStr, type)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Log.e("apiClient", "response exception [${BASE_URL + path}]: ${e.message}")
+ BaseResponse(code = -1, message = e.message ?: "Unknown error", data = null)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/jihe/punchnet/api/ApiModels.kt b/app/src/main/java/com/jihe/punchnet/api/ApiModels.kt
new file mode 100644
index 0000000..2007a0e
--- /dev/null
+++ b/app/src/main/java/com/jihe/punchnet/api/ApiModels.kt
@@ -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?
+)
+
+data class BaseResponse(
+ @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?,
+ @SerializedName("node_list") val nodeList: List?,
+ @SerializedName("acl") val acl: com.google.gson.JsonElement?,
+ @SerializedName("exit_node") val exitNode: List?
+)
+
+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?
+)
+
+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?,
+ @SerializedName("udp") val udp: List?
+)
+
+data class DisconnectRequest(
+ @SerializedName("client_id") val clientId: String,
+ @SerializedName("access_token") val accessToken: String
+)
diff --git a/app/src/main/java/com/jihe/punchnet/data/RouteItem.kt b/app/src/main/java/com/jihe/punchnet/data/RouteItem.kt
index fe36a27..8633e67 100644
--- a/app/src/main/java/com/jihe/punchnet/data/RouteItem.kt
+++ b/app/src/main/java/com/jihe/punchnet/data/RouteItem.kt
@@ -61,7 +61,9 @@ abstract class AppDatabase: RoomDatabase() {
context.applicationContext,
AppDatabase::class.java,
"app_database"
- ).build()
+ )
+ .fallbackToDestructiveMigration()
+ .build()
INSTANCE = instance
instance
}
diff --git a/app/src/main/java/com/jihe/punchnet/helper/Screens.kt b/app/src/main/java/com/jihe/punchnet/helper/Screens.kt
index c781d64..c98e5b5 100644
--- a/app/src/main/java/com/jihe/punchnet/helper/Screens.kt
+++ b/app/src/main/java/com/jihe/punchnet/helper/Screens.kt
@@ -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
+ }
}
@@ -117,4 +155,4 @@ fun parseCIDRAndGW(cidr: String, gw: String): RouteItem? {
gateway = gateway,
mask_ip = mask,
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/jihe/punchnet/screen/AppNavigationItems.kt b/app/src/main/java/com/jihe/punchnet/screen/AppNavigationItems.kt
index 54477dd..2906b7e 100644
--- a/app/src/main/java/com/jihe/punchnet/screen/AppNavigationItems.kt
+++ b/app/src/main/java/com/jihe/punchnet/screen/AppNavigationItems.kt
@@ -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)
}
- // Profile(dbdao, 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() }
+ )
}
}
}
diff --git a/app/src/main/java/com/jihe/punchnet/screen/BottomNavBar.kt b/app/src/main/java/com/jihe/punchnet/screen/BottomNavBar.kt
index 2d10170..378da2f 100644
--- a/app/src/main/java/com/jihe/punchnet/screen/BottomNavBar.kt
+++ b/app/src/main/java/com/jihe/punchnet/screen/BottomNavBar.kt
@@ -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 = {
diff --git a/app/src/main/java/com/jihe/punchnet/screen/HomeScreen.kt b/app/src/main/java/com/jihe/punchnet/screen/HomeScreen.kt
index ebe50d2..0451c01 100644
--- a/app/src/main/java/com/jihe/punchnet/screen/HomeScreen.kt
+++ b/app/src/main/java/com/jihe/punchnet/screen/HomeScreen.kt
@@ -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,
-) {
+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,
+ 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,118 +183,187 @@ 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
+ ) {
+ item {
+ Spacer(modifier = Modifier.height(50.dp))
+
+ Image(
+ painter = painterResource(R.drawable.punchnet_log),
+ contentDescription = "logo",
+ modifier = Modifier.size(150.dp)
+ )
+
+ Text(
+ "Connecting the Infinite",
+ fontSize = 30.sp,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(top=24.dp)
+ )
+
+ Text(
+ "Welcome to PunchNet",
+ style = MaterialTheme.typography.titleSmall,
+ modifier = Modifier.padding(top=8.dp)
+ )
+
+ Button(
+ onClick = {
+ if (buttonViewModel.buttonState.value == ButtonState.ButtonStarted) {
+ serviceViewModel.stopVpnService(context)
+ } else if (buttonViewModel.buttonState.value == ButtonState.ButtonStopped){
+ 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)?:""
+ serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
+ }
+ }
}
- ) {
- 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)
- )
- },
+ },
+ enabled = buttonState.value.enabled,
+ shape = RoundedCornerShape(10.dp),
+ modifier = Modifier.padding(top=48.dp)
+ .width(120.dp)
+ .height(40.dp)
+ ) {
+ 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()
)
}
-
- )
- }
-
- Column(
- modifier = Modifier.fillMaxWidth()
- .padding(top=50.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
-
- ) {
- Image(
- painter = painterResource(R.drawable.punchnet_log),
- contentDescription = "logo",
- modifier = Modifier.size(150.dp)
- )
-
- Text(
- "Connecting the Infinite",
- fontSize = 30.sp,
- style = MaterialTheme.typography.titleLarge,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(top=24.dp)
- )
-
- Text(
- "Welcome to PunchNet",
- style = MaterialTheme.typography.titleSmall,
- modifier = Modifier.padding(top=8.dp)
- )
-
- 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 (intent != null) {
- vpnLauncher.launch(intent)
- } else {
- val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
- println("now token value1 = '${tokenvalue}'")
- serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
- }
+
+ items(resourceList) { resource ->
+ Box(modifier = Modifier.padding(horizontal = 24.dp)) {
+ ResourceItemRow(resource, navController)
}
- // started.value = !started.value
- },
- enabled = buttonState.value.enabled,
- shape = RoundedCornerShape(10.dp),
- modifier = Modifier.padding(top=48.dp)
- .width(120.dp)
- .height(40.dp)
- ) {
- Text(
- buttonState.value.text
- // buttonViewModel.buttonText.value
- /*
- if (serviceViewModel.isRunning.value) {
- "停止"
- } else {
- "启动"
- }
- */
- )
+ }
}
}
- // BasicCardList(dbdao = dbdao, 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
+ )
+ )
+ }
}
}
diff --git a/app/src/main/java/com/jihe/punchnet/screen/MainScreen.kt b/app/src/main/java/com/jihe/punchnet/screen/MainScreen.kt
index fccd210..bc5d8ae 100644
--- a/app/src/main/java/com/jihe/punchnet/screen/MainScreen.kt
+++ b/app/src/main/java/com/jihe/punchnet/screen/MainScreen.kt
@@ -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,9 +53,15 @@ 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 = {
- AppBottomNavigation(navController = navController)
+ if (showBottomBar) {
+ AppBottomNavigation(navController = navController)
+ }
}
){ paddingValues ->
/*
diff --git a/app/src/main/java/com/jihe/punchnet/screen/RouteScreen.kt b/app/src/main/java/com/jihe/punchnet/screen/RouteScreen.kt
deleted file mode 100644
index d46dd9b..0000000
--- a/app/src/main/java/com/jihe/punchnet/screen/RouteScreen.kt
+++ /dev/null
@@ -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,
- shouldShowRoute: MutableState,
-
-) {
- 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 = remember { mutableStateOf(null) }
- val shouldShow: MutableState = 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)
- )
-
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/jihe/punchnet/sdlan/config/arguments.kt b/app/src/main/java/com/jihe/punchnet/sdlan/config/arguments.kt
index a3dde7f..40d573a 100644
--- a/app/src/main/java/com/jihe/punchnet/sdlan/config/arguments.kt
+++ b/app/src/main/java/com/jihe/punchnet/sdlan/config/arguments.kt
@@ -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,
diff --git a/app/src/main/java/com/jihe/punchnet/sdlan/network/edge.kt b/app/src/main/java/com/jihe/punchnet/sdlan/network/edge.kt
index 14d7e8a..cc72790 100644
--- a/app/src/main/java/com/jihe/punchnet/sdlan/network/edge.kt
+++ b/app/src/main/java/com/jihe/punchnet/sdlan/network/edge.kt
@@ -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,
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 = ConcurrentHashMap(),
+ val queriedPolicyTargets: ConcurrentHashMap = ConcurrentHashMap(),
+ val policyRules: ConcurrentHashMap = 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(100)
+ suspend fun _sendAndWaitForProbeReply(attr: StunProbeAttr, toServer: java.net.SocketAddress): SDLStunProbeReply? {
+ val channel = kotlinx.coroutines.channels.Channel(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"}
- }
- }
}
diff --git a/app/src/main/java/com/jihe/punchnet/sdlan/network/helper.kt b/app/src/main/java/com/jihe/punchnet/sdlan/network/helper.kt
index 739c4f0..895456c 100644
--- a/app/src/main/java/com/jihe/punchnet/sdlan/network/helper.kt
+++ b/app/src/main/java/com/jihe/punchnet/sdlan/network/helper.kt
@@ -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, 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()
+ 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 {
+ val result = mutableListOf()
+ 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 {
+ 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
- try {
- reg = SDLSendRegisterEvent.parseFrom(cmdprotobuf)
- } catch (e: Exception) {
- TerminalLogger.errorf {"failed to decode SendRegisterEvent: $e"}
- return
- }
+suspend fun handleEvent(node: Node, cmdprotobuf: ByteArray) {
+ val evt: com.jihe.punchnet.protobuf.PunchProto.SDLEvent
+ try {
+ evt = com.jihe.punchnet.protobuf.PunchProto.SDLEvent.parseFrom(cmdprotobuf)
+ } catch (e: Exception) {
+ 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}" }
}
}
}
@@ -514,4 +911,4 @@ fun ipInt2ByteArray(ip: Int): ByteArray {
(ip ushr 8).and(0xff).toByte(),
ip.and(0xff).toByte(),
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/jihe/punchnet/sdlan/network/libs.kt b/app/src/main/java/com/jihe/punchnet/sdlan/network/libs.kt
index 00b3856..80822e4 100644
--- a/app/src/main/java/com/jihe/punchnet/sdlan/network/libs.kt
+++ b/app/src/main/java/com/jihe/punchnet/sdlan/network/libs.kt
@@ -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(100)
val start_stop_channel = Channel(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) {
@@ -362,7 +365,7 @@ suspend fun handleAPacket(scope: CoroutineScope, node: Node, from: SocketAddress
}
// val buffer = ByteBuffer.wrap(data, 1, size-1)
- when (pktType) {
+ when (pktType) {
PacketType.Data -> {
TerminalLogger.debugf { "got DATA" }
if (!node.aes.isAuthorized()) {
@@ -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,
- pong_time: AtomicLong,
- connected: AtomicBoolean,
- toSocket: ReceiveChannel,
- onConnected: suspend (stream: Socket, pktID: Int?)->Unit,
- onMessage: suspend (CoroutineScope, SDLanTCP)->Unit,
- onDisconnected: suspend ()->Unit,
- connectingChan: SendChannel?
-) {
-
- 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 {
- 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
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/jihe/punchnet/sdlan/network/packet_info.kt b/app/src/main/java/com/jihe/punchnet/sdlan/network/packet_info.kt
index ae20691..4852bb1 100644
--- a/app/src/main/java/com/jihe/punchnet/sdlan/network/packet_info.kt
+++ b/app/src/main/java/com/jihe/punchnet/sdlan/network/packet_info.kt
@@ -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 {
return result
}
-fun encodeToTcpMessage(msg: Message?, packetID: Int, packetType: PacketType): List {
+fun encodeToControlMessage(msg: Message?, packetType: PacketType): List {
val msgByte = msg?.toByteArray()?.toList()?:listOf()
val result: MutableList = 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())
@@ -127,4 +130,4 @@ enum class StunProbeAttr {
None,
Port,
Peer,
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/jihe/punchnet/sdlan/utils/utils.kt b/app/src/main/java/com/jihe/punchnet/sdlan/utils/utils.kt
index 1586adb..512bc1e 100644
--- a/app/src/main/java/com/jihe/punchnet/sdlan/utils/utils.kt
+++ b/app/src/main/java/com/jihe/punchnet/sdlan/utils/utils.kt
@@ -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.length == 0) {
+ if (id.isEmpty()) {
+ if (baseDir.isEmpty()) {
+ baseDir = Environment.getExternalStorageDirectory().absolutePath
+ }
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) }
}
diff --git a/app/src/main/proto/message.proto b/app/src/main/proto/message.proto
index af64196..ee3c381 100644
--- a/app/src/main/proto/message.proto
+++ b/app/src/main/proto/message.proto
@@ -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 SDLArpRequest {
+ uint32 target_ip = 1;
+ uint32 origin_ip = 2;
+ bytes context = 3;
+}
+
+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 SDLNatChangedEvent {
- 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 SDLSendRegisterEvent {
- bytes dst_mac = 1;
- uint32 nat_ip = 2;
- uint32 nat_port = 3;
- uint32 nat_type = 4;
- optional SDLV6Info v6_info = 5;
-}
+ message ExposedServiceChanged {
+ }
-message SDLNetworkShutdownEvent {
- string message = 1;
+ 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;
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 20e2a01..b01726c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,4 +20,5 @@ 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
\ No newline at end of file
+android.nonTransitiveRClass=true
+org.gradle.java.home=/Users/stavid/Library/Java/JavaVirtualMachines/corretto-17.0.17/Contents/Home
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755