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