fix some warning
This commit is contained in:
parent
f86069d165
commit
4b1bae0e55
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
||||
punchnet
|
||||
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@ -4,6 +4,14 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-05-30T07:22:02.241059Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/stavid/.android/avd/Medium_Phone.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/markdown.xml
generated
Normal file
8
.idea/markdown.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@ -90,12 +90,18 @@ dependencies {
|
||||
ksp("androidx.room:room-compiler:2.7.2")
|
||||
implementation("androidx.room:room-ktx:2.7.2")
|
||||
|
||||
// implementation(libs.bcprov.jdk18on)
|
||||
// HTTP API Control Plane
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
|
||||
// QUIC Data Plane (Kwik)
|
||||
implementation("tech.kwik:kwik:0.10.3")
|
||||
|
||||
implementation(libs.bcprov.jdk18on)
|
||||
implementation(libs.bcpkix.jdk18on)
|
||||
// implementation(files("libs/org.asxalex.sdlan/sdlan-1.0-SNAPSHOT.jar"))
|
||||
// implementation(files("libs/org.asxalex.sdlan/1.0.0/sdlan-1.0.0.jar"))
|
||||
//implementation("org.jetbrains.kotlin:kotlin-stdlib:2.1.0") {
|
||||
//version { strictly("2.1.0") }
|
||||
// }
|
||||
|
||||
// Security Crypto
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
}
|
||||
@ -5,6 +5,8 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
|
||||
|
||||
|
||||
<application
|
||||
@ -16,6 +18,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Punchnet"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@ -30,7 +33,7 @@
|
||||
</activity>
|
||||
|
||||
<service android:name=".PunchnetService"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
|
||||
|
||||
@ -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}")
|
||||
|
||||
@ -12,12 +12,9 @@ import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.jihe.punchnet.data.ButtonRepository
|
||||
import com.jihe.punchnet.data.ButtonState
|
||||
import com.jihe.punchnet.data.ButtonViewModel
|
||||
import com.jihe.punchnet.data.RouteItem
|
||||
import com.jihe.punchnet.data.RouteViewModel
|
||||
import com.jihe.punchnet.sdlan.config.Arguments
|
||||
import com.jihe.punchnet.sdlan.config.toIPV4String
|
||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
||||
@ -26,23 +23,17 @@ import com.jihe.punchnet.sdlan.network.ARPWaitList
|
||||
import com.jihe.punchnet.sdlan.network.DeviceConfig
|
||||
import com.jihe.punchnet.sdlan.network.IfaceTun
|
||||
import com.jihe.punchnet.sdlan.network.RouteDetail
|
||||
import com.jihe.punchnet.sdlan.network.RouteTable
|
||||
import com.jihe.punchnet.sdlan.network.StartStopChanInfo
|
||||
import com.jihe.punchnet.sdlan.network.cidrToRouteDetail
|
||||
import com.jihe.punchnet.sdlan.network.ipInt2ByteArray
|
||||
import com.jihe.punchnet.sdlan.network.maskIPToDigit
|
||||
import com.jihe.punchnet.sdlan.network.run_sdlan
|
||||
import com.jihe.punchnet.sdlan.utils.ipToString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class PunchnetService : VpnService() , IfaceTun {
|
||||
|
||||
@ -79,7 +70,6 @@ class PunchnetService : VpnService() , IfaceTun {
|
||||
try {
|
||||
var size = input?.read(result)
|
||||
if (size == null) {
|
||||
println("xxx failed to read")
|
||||
size = 0
|
||||
} else {
|
||||
println("xxx got $size bytes")
|
||||
@ -90,6 +80,8 @@ class PunchnetService : VpnService() , IfaceTun {
|
||||
result.copyOf(size)
|
||||
// result.slice(0..<size).toByteArray()
|
||||
} catch (e: Exception) {
|
||||
println("xxx read exception: ${e.message}")
|
||||
e.printStackTrace()
|
||||
ByteArray(0)
|
||||
}
|
||||
}
|
||||
@ -126,10 +118,45 @@ class PunchnetService : VpnService() , IfaceTun {
|
||||
.addAddress(ip, config.ip.netBitLen.toInt())
|
||||
.setBlocking(true)
|
||||
|
||||
// Automatically add route for the SDLAN network itself
|
||||
val maskInt = com.jihe.punchnet.sdlan.network.netmaskBit2Int(config.ip.netBitLen)
|
||||
val networkInt = config.ip.netAddr and maskInt
|
||||
val networkStr = ipInt2ByteArray(networkInt).toIPV4String()
|
||||
tempVpnDescriptor = tempVpnDescriptor.addRoute(networkStr, config.ip.netBitLen.toInt())
|
||||
|
||||
for (route in routes) {
|
||||
tempVpnDescriptor = tempVpnDescriptor.addRoute(ipToString(route.net_ip), maskIPToDigit(route.mask_ip))
|
||||
}
|
||||
|
||||
// Apply App Authorization (Split Tunneling)
|
||||
val allowedApps = com.jihe.punchnet.data.AppAuthRepository.allowedApps.value
|
||||
if (allowedApps.isNotEmpty()) {
|
||||
for (pkg in allowedApps) {
|
||||
// Ensure we do not add ourselves to the allowed list, otherwise it causes a routing loop
|
||||
if (pkg == this.packageName) continue
|
||||
try {
|
||||
tempVpnDescriptor = tempVpnDescriptor.addAllowedApplication(pkg)
|
||||
} catch (e: android.content.pm.PackageManager.NameNotFoundException) {
|
||||
TerminalLogger.warning { "App not found when setting VPN allowed app: $pkg" }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no apps are specified, VPN intercepts everything matching the routes.
|
||||
// We ONLY disallow ourselves if there's a 0.0.0.0/0 route, to avoid routing loops.
|
||||
// If there's no global route, we want the app (e.g. in-app WebViews) to be able to access the VPN routes!
|
||||
val hasGlobalRoute = routes.any { it.mask_ip == 0 }
|
||||
if (hasGlobalRoute) {
|
||||
try {
|
||||
tempVpnDescriptor = tempVpnDescriptor.addDisallowedApplication(this.packageName)
|
||||
TerminalLogger.infof { "Disallowed app itself to prevent loop on 0.0.0.0/0" }
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.warning { "Failed to disallow self: $e" }
|
||||
}
|
||||
} else {
|
||||
TerminalLogger.infof { "Did not disallow self. In-app WebViews will be routed via VPN." }
|
||||
}
|
||||
}
|
||||
|
||||
vpnDescriptor = tempVpnDescriptor
|
||||
.establish()?.apply {
|
||||
input = FileInputStream(fileDescriptor)
|
||||
@ -198,11 +225,10 @@ class PunchnetService : VpnService() , IfaceTun {
|
||||
|
||||
val argument = Arguments(
|
||||
baseDir = this.filesDir.path,
|
||||
sn = "$server:1265",
|
||||
tcp = "$server:18083",
|
||||
|
||||
nat_server1 = "$server:1265",
|
||||
nat_server2 = "47.98.178.3:1265",
|
||||
sn = "$server:443",
|
||||
stun = "$server:1365",
|
||||
nat_server1 = "$server:1365",
|
||||
nat_server2 = "$server:1366",
|
||||
token = startArg?.token?:"",
|
||||
name = "tau",
|
||||
)
|
||||
@ -210,8 +236,43 @@ class PunchnetService : VpnService() , IfaceTun {
|
||||
val notification = createNotification()
|
||||
startForeground(1, notification)
|
||||
|
||||
// Initialize UniqueNodeID baseDir to app's files directory
|
||||
com.jihe.punchnet.sdlan.utils.UniqueNodeID.setBaseDir(this.filesDir.absolutePath)
|
||||
|
||||
scope.launch {
|
||||
run_sdlan(scope, iface, argument, startArg)
|
||||
val apiClient = com.jihe.punchnet.api.ApiClient()
|
||||
val authDataJson = startArg?.token ?: ""
|
||||
|
||||
if (authDataJson.isEmpty()) {
|
||||
TerminalLogger.errorf { "API connect failed: No auth data found" }
|
||||
ButtonRepository.updateState(ButtonState.ButtonStopped)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val authData = try {
|
||||
com.google.gson.Gson().fromJson(authDataJson, com.jihe.punchnet.api.AuthResponseData::class.java)
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf { "API connect failed: Invalid auth data" }
|
||||
ButtonRepository.updateState(ButtonState.ButtonStopped)
|
||||
return@launch
|
||||
}
|
||||
|
||||
// 1. Connect directly using the accessToken obtained from LoginScreen
|
||||
val connectReq = com.jihe.punchnet.api.ConnectRequest(
|
||||
clientId = "punchnet_android_client_" + com.jihe.punchnet.sdlan.utils.UniqueNodeID.getUUID(this@PunchnetService),
|
||||
accessToken = authData.accessToken
|
||||
)
|
||||
val connectRes = apiClient.connect(connectReq)
|
||||
if (connectRes == null || connectRes.code != 0 || connectRes.data == null) {
|
||||
TerminalLogger.errorf { "API connect failed: ${connectRes?.message}" }
|
||||
ButtonRepository.updateState(ButtonState.ButtonStopped)
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Update UI state to Started since connection was successful
|
||||
ButtonRepository.updateState(ButtonState.ButtonStarted)
|
||||
com.jihe.punchnet.data.NodeRepository.updateAll(connectRes.data)
|
||||
run_sdlan(scope, iface, argument, authData, connectRes.data)
|
||||
}
|
||||
|
||||
// isRunning.set(true)
|
||||
@ -236,7 +297,7 @@ class PunchnetService : VpnService() , IfaceTun {
|
||||
} else {
|
||||
intent?.getParcelableExtra("argument")
|
||||
}
|
||||
println("argument = ${argument}")
|
||||
TerminalLogger.debugf { "service argument received: routes=${argument?.routes?.size ?: 0}, hasToken=${!argument?.token.isNullOrEmpty()}" }
|
||||
connect(argument)
|
||||
START_STICKY
|
||||
}
|
||||
@ -265,6 +326,7 @@ class PunchnetService : VpnService() , IfaceTun {
|
||||
// disconnect()
|
||||
scope.cancel()
|
||||
disconnect()
|
||||
com.jihe.punchnet.data.NodeRepository.clear()
|
||||
Log.e(TAG, "onDestroy is called")
|
||||
// stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
super.onDestroy()
|
||||
|
||||
81
app/src/main/java/com/jihe/punchnet/api/ApiClient.kt
Normal file
81
app/src/main/java/com/jihe/punchnet/api/ApiClient.kt
Normal file
@ -0,0 +1,81 @@
|
||||
package com.jihe.punchnet.api
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ApiClient {
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.writeTimeout(15, TimeUnit.SECONDS)
|
||||
.addInterceptor(SignInterceptor())
|
||||
.build()
|
||||
|
||||
private val gson = Gson()
|
||||
private val JSON = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
private val BASE_URL = "https://root.punchsky.com/api"
|
||||
|
||||
suspend fun loginWithToken(request: AuthTokenRequest): BaseResponse<AuthResponseData>? {
|
||||
return post("/auth/token", request)
|
||||
}
|
||||
|
||||
suspend fun connect(request: ConnectRequest): BaseResponse<ConnectResponseData>? {
|
||||
return post("/connect", request)
|
||||
}
|
||||
|
||||
suspend fun disconnect(request: DisconnectRequest): BaseResponse<Any>? {
|
||||
return post("/disconnect", request)
|
||||
}
|
||||
|
||||
suspend fun loginWithAccount(request: AuthLoginRequest): BaseResponse<AuthResponseData>? {
|
||||
return post("/auth/login", request)
|
||||
}
|
||||
|
||||
suspend fun getNodeResources(request: GetNodeResourcesRequest): BaseResponse<GetNodeResourcesResponseData>? {
|
||||
return post("/get_node_resources", request)
|
||||
}
|
||||
|
||||
suspend fun getAcl(request: AclRequest): BaseResponse<AclResponseData>? {
|
||||
return post("/acl", request)
|
||||
}
|
||||
|
||||
private suspend inline fun <reified T, reified R> post(path: String, requestBody: T): BaseResponse<R>? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val jsonBody = gson.toJson(requestBody)
|
||||
Log.i("apiClient", "post [${BASE_URL + path}]")
|
||||
val body = jsonBody.toRequestBody(JSON)
|
||||
val request = Request.Builder()
|
||||
.url(BASE_URL + path)
|
||||
.post(body)
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.body?.close()
|
||||
Log.e("apiClient", "response error [${BASE_URL + path}]: HTTP ${response.code}")
|
||||
return@withContext BaseResponse<R>(code = -1, message = "HTTP ${response.code}: ${response.message}", data = null)
|
||||
}
|
||||
val responseStr = response.body?.string() ?: return@withContext BaseResponse<R>(code = -1, message = "Empty response body", data = null)
|
||||
Log.i("apiClient", "response [${BASE_URL + path}]: HTTP ${response.code}")
|
||||
|
||||
val type = object : TypeToken<BaseResponse<R>>() {}.type
|
||||
gson.fromJson<BaseResponse<R>>(responseStr, type)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e("apiClient", "response exception [${BASE_URL + path}]: ${e.message}")
|
||||
BaseResponse<R>(code = -1, message = e.message ?: "Unknown error", data = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
115
app/src/main/java/com/jihe/punchnet/api/ApiModels.kt
Normal file
115
app/src/main/java/com/jihe/punchnet/api/ApiModels.kt
Normal file
@ -0,0 +1,115 @@
|
||||
package com.jihe.punchnet.api
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class AuthTokenRequest(
|
||||
@SerializedName("client_id") val clientId: String,
|
||||
@SerializedName("token") val token: String,
|
||||
@SerializedName("mac") val mac: String? = null,
|
||||
@SerializedName("system") val system: String? = null,
|
||||
@SerializedName("version") val version: String? = null,
|
||||
@SerializedName("hostname") val hostname: String? = null
|
||||
)
|
||||
|
||||
data class NetworkItem(
|
||||
@SerializedName("network_id") val networkId: Int,
|
||||
@SerializedName("network_name") val networkName: String
|
||||
)
|
||||
|
||||
data class AuthResponseData(
|
||||
@SerializedName("access_token") val accessToken: String,
|
||||
@SerializedName("username") val username: String,
|
||||
@SerializedName("user_type") val userType: String,
|
||||
@SerializedName("audit") val audit: Int,
|
||||
@SerializedName("network_id") val networkId: Int,
|
||||
@SerializedName("network_name") val networkName: String,
|
||||
@SerializedName("network_domain") val networkDomain: String,
|
||||
@SerializedName("domain") val domain: String?,
|
||||
@SerializedName("my_network_list") val myNetworkList: List<NetworkItem>?
|
||||
)
|
||||
|
||||
data class BaseResponse<T>(
|
||||
@SerializedName("code") val code: Int,
|
||||
@SerializedName("message") val message: String,
|
||||
@SerializedName("data") val data: T?
|
||||
)
|
||||
|
||||
data class ConnectRequest(
|
||||
@SerializedName("client_id") val clientId: String,
|
||||
@SerializedName("access_token") val accessToken: String,
|
||||
@SerializedName("version") val version: String? = null
|
||||
)
|
||||
|
||||
data class ResourceItem(
|
||||
@SerializedName("id") val id: Int,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("url") val url: String,
|
||||
@SerializedName("connection_status") val connectionStatus: String
|
||||
)
|
||||
|
||||
data class NodeItem(
|
||||
@SerializedName("id") val id: Int,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("ip") val ip: String,
|
||||
@SerializedName("system") val system: String?,
|
||||
@SerializedName("connection_status") val connectionStatus: String
|
||||
)
|
||||
|
||||
data class ExitNodeItem(
|
||||
@SerializedName("node_id") val nodeId: Int,
|
||||
@SerializedName("node_name") val nodeName: String,
|
||||
@SerializedName("gateway") val gateway: String,
|
||||
@SerializedName("target_network") val targetNetwork: String
|
||||
)
|
||||
|
||||
data class ConnectResponseData(
|
||||
@SerializedName("ip") val ip: String,
|
||||
@SerializedName("mask_len") val maskLen: Int,
|
||||
@SerializedName("hostname") val hostname: String,
|
||||
@SerializedName("identity_id") val identityId: Int,
|
||||
@SerializedName("resource_list") val resourceList: List<ResourceItem>?,
|
||||
@SerializedName("node_list") val nodeList: List<NodeItem>?,
|
||||
@SerializedName("acl") val acl: com.google.gson.JsonElement?,
|
||||
@SerializedName("exit_node") val exitNode: List<ExitNodeItem>?
|
||||
)
|
||||
|
||||
data class AuthLoginRequest(
|
||||
@SerializedName("client_id") val clientId: String,
|
||||
@SerializedName("username") val username: String,
|
||||
@SerializedName("password") val password: String,
|
||||
@SerializedName("mac") val mac: String? = null,
|
||||
@SerializedName("system") val system: String? = null,
|
||||
@SerializedName("version") val version: String? = null,
|
||||
@SerializedName("hostname") val hostname: String? = null
|
||||
)
|
||||
|
||||
data class GetNodeResourcesRequest(
|
||||
@SerializedName("client_id") val clientId: String,
|
||||
@SerializedName("access_token") val accessToken: String,
|
||||
@SerializedName("id") val id: Int
|
||||
)
|
||||
|
||||
data class GetNodeResourcesResponseData(
|
||||
@SerializedName("id") val id: Int,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("ip") val ip: String,
|
||||
@SerializedName("system") val system: String?,
|
||||
@SerializedName("connection_status") val connectionStatus: String,
|
||||
@SerializedName("resource_list") val resourceList: List<ResourceItem>?
|
||||
)
|
||||
|
||||
data class AclRequest(
|
||||
@SerializedName("client_id") val clientId: String,
|
||||
@SerializedName("access_token") val accessToken: String,
|
||||
@SerializedName("network_id") val networkId: Int? = null
|
||||
)
|
||||
|
||||
data class AclResponseData(
|
||||
@SerializedName("tcp") val tcp: List<Int>?,
|
||||
@SerializedName("udp") val udp: List<Int>?
|
||||
)
|
||||
|
||||
data class DisconnectRequest(
|
||||
@SerializedName("client_id") val clientId: String,
|
||||
@SerializedName("access_token") val accessToken: String
|
||||
)
|
||||
@ -61,7 +61,9 @@ abstract class AppDatabase: RoomDatabase() {
|
||||
context.applicationContext,
|
||||
AppDatabase::class.java,
|
||||
"app_database"
|
||||
).build()
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
@ -50,33 +51,45 @@ fun AppNavHost2(
|
||||
paddingValues: PaddingValues
|
||||
) {
|
||||
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val token = com.jihe.punchnet.helper.getPreferenceString(context, com.jihe.punchnet.helper.PreferenceName.PreferenceToken)
|
||||
val startDest = if (token.isNullOrEmpty()) Screen.LoginScreen.route else Screen.MainScreen.route
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.MainScreen.route,
|
||||
startDestination = startDest,
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
) {
|
||||
composable(Screen.LoginScreen.route) {
|
||||
LoginScreen(navController)
|
||||
}
|
||||
composable(Screen.MainScreen.route) {
|
||||
HomeScreen(buttonViewModel, serviceViewModel, routeViewModel)
|
||||
HomeScreen(buttonViewModel, serviceViewModel, routeViewModel, navController)
|
||||
}
|
||||
composable(Screen.RouteScreen.route) {
|
||||
CustomHeaderScreen(
|
||||
"路由设置"
|
||||
) {
|
||||
RouteScreen(ButtonRepository.buttonState.collectAsState().value != ButtonState.ButtonStarted, routeViewModel)
|
||||
// LightScreen()
|
||||
composable(Screen.DeviceScreen.route) {
|
||||
CustomHeaderScreen("设备") {
|
||||
DeviceScreen(navController)
|
||||
}
|
||||
}
|
||||
composable(Screen.ProfileScreen.route) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"TODO",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
composable(Screen.AppAuthScreen.route) {
|
||||
CustomHeaderScreen("应用授权") {
|
||||
AppAuthScreen(navController)
|
||||
}
|
||||
}
|
||||
composable(Screen.SettingsScreen.route) {
|
||||
CustomHeaderScreen("设置") {
|
||||
SettingsScreen(ButtonRepository.buttonState.collectAsState().value != ButtonState.ButtonStarted, routeViewModel)
|
||||
}
|
||||
}
|
||||
composable(
|
||||
route = Screen.WebViewScreen.route,
|
||||
arguments = listOf(navArgument("url") { type = androidx.navigation.NavType.StringType })
|
||||
) { backStackEntry ->
|
||||
val url = backStackEntry.arguments?.getString("url") ?: ""
|
||||
WebViewScreen(
|
||||
url = url,
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
// Profile(dbdao, navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -6,12 +6,14 @@ import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.indication
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@ -64,6 +66,12 @@ import com.jihe.punchnet.helper.PreferenceName
|
||||
import com.jihe.punchnet.helper.getPreferenceString
|
||||
import com.jihe.punchnet.helper.setPreferenceString
|
||||
import kotlin.math.exp
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.filled.Phone
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.foundation.background
|
||||
|
||||
data class HomeDropDownInfo(
|
||||
@DrawableRes val icon: Int,
|
||||
@ -72,32 +80,30 @@ data class HomeDropDownInfo(
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun HomeDropdownMenu(
|
||||
showTokenDialog: MutableState<Boolean>,
|
||||
) {
|
||||
fun HomeDropdownMenu(navController: androidx.navigation.NavHostController) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
// var showTokenDialog by remember { mutableStateOf(false) }
|
||||
// val showSettingDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val homeItems = arrayOf(
|
||||
HomeDropDownInfo(
|
||||
R.drawable.tag,
|
||||
"修改token",
|
||||
R.drawable.tag, // TODO: Use settings icon if available, or just standard icon
|
||||
"设置",
|
||||
{
|
||||
showTokenDialog.value = true
|
||||
navController.navigate(Screen.SettingsScreen.route)
|
||||
}
|
||||
),
|
||||
/*
|
||||
HomeDropDownInfo(
|
||||
R.drawable.preferences,
|
||||
"修改云端配置"
|
||||
R.drawable.tag,
|
||||
"退出登录",
|
||||
{
|
||||
// Clear token and go to LoginScreen
|
||||
setPreferenceString(context, PreferenceName.PreferenceToken, "")
|
||||
navController.navigate(Screen.LoginScreen.route) {
|
||||
popUpTo(Screen.MainScreen.route) { inclusive = true }
|
||||
}
|
||||
}
|
||||
)
|
||||
*/
|
||||
)
|
||||
|
||||
|
||||
|
||||
Box() {
|
||||
IconButton(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
@ -125,7 +131,6 @@ fun HomeDropdownMenu(
|
||||
item.callback?.invoke()
|
||||
},
|
||||
text = {
|
||||
|
||||
Row (
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
){
|
||||
@ -142,12 +147,10 @@ fun HomeDropdownMenu(
|
||||
|
||||
Text(text=item.name)
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,15 +159,13 @@ fun HomeScreen(
|
||||
buttonViewModel: ButtonViewModel,
|
||||
serviceViewModel: ServiceViewModel,
|
||||
routeViewModel: RouteViewModel,
|
||||
// started: MutableState<Boolean>,
|
||||
navController: androidx.navigation.NavHostController,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
||||
val context = LocalContext.current
|
||||
val tkPref = getPreferenceString(context, PreferenceName.PreferenceToken)
|
||||
|
||||
var tokenvalue by remember {mutableStateOf(tkPref?:"")}
|
||||
|
||||
val allroutes = routeViewModel.allRoutes.collectAsState(emptyList())
|
||||
val vpnLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
@ -174,7 +175,6 @@ fun HomeScreen(
|
||||
// serviceViewModel.startService(context)
|
||||
// serviceViewModel.setVPNPermission(true)
|
||||
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
|
||||
println("now token value2 = '${tokenvalue}'")
|
||||
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
|
||||
}
|
||||
RESULT_CANCELED -> {
|
||||
@ -183,60 +183,38 @@ fun HomeScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val notificationPermissionLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (!isGranted) {
|
||||
Toast.makeText(context, "未授予通知权限,连接可能会在后台被杀", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val intent = VpnService.prepare(context.applicationContext)
|
||||
if (intent != null) {
|
||||
vpnLauncher.launch(intent)
|
||||
} else {
|
||||
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
|
||||
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
|
||||
}
|
||||
}
|
||||
|
||||
val buttonState = buttonViewModel.buttonState.collectAsState()
|
||||
val showTokenDialog = remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
val resourceList by com.jihe.punchnet.data.NodeRepository.resources.collectAsState()
|
||||
|
||||
CustomHeaderScreen(
|
||||
"",
|
||||
onBack = null,
|
||||
onMenu = {
|
||||
HomeDropdownMenu(showTokenDialog)
|
||||
HomeDropdownMenu(navController)
|
||||
}
|
||||
) {
|
||||
if (showTokenDialog.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {showTokenDialog.value=false},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
setPreferenceString(context, PreferenceName.PreferenceToken, tokenvalue)
|
||||
println("now token value = '${tokenvalue}'")
|
||||
showTokenDialog.value = false
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("确定")
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text("修改token")
|
||||
},
|
||||
text = {
|
||||
TextField(
|
||||
value = tokenvalue,
|
||||
onValueChange = { newValue ->
|
||||
tokenvalue = newValue.trim()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.tag),
|
||||
contentDescription = "token",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(top=50.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.punchnet_log),
|
||||
contentDescription = "logo",
|
||||
@ -260,20 +238,20 @@ fun HomeScreen(
|
||||
Button(
|
||||
onClick = {
|
||||
if (buttonViewModel.buttonState.value == ButtonState.ButtonStarted) {
|
||||
// if (serviceViewModel.isRunning.value) {
|
||||
// if is running, should stop service
|
||||
serviceViewModel.stopVpnService(context)
|
||||
} else if (buttonViewModel.buttonState.value == ButtonState.ButtonStopped){
|
||||
val intent = VpnService.prepare(context)
|
||||
if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
notificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
val intent = VpnService.prepare(context.applicationContext)
|
||||
if (intent != null) {
|
||||
vpnLauncher.launch(intent)
|
||||
} else {
|
||||
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
|
||||
println("now token value1 = '${tokenvalue}'")
|
||||
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
|
||||
}
|
||||
}
|
||||
// started.value = !started.value
|
||||
}
|
||||
},
|
||||
enabled = buttonState.value.enabled,
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
@ -281,20 +259,111 @@ fun HomeScreen(
|
||||
.width(120.dp)
|
||||
.height(40.dp)
|
||||
) {
|
||||
Text(
|
||||
buttonState.value.text
|
||||
// buttonViewModel.buttonText.value
|
||||
/*
|
||||
if (serviceViewModel.isRunning.value) {
|
||||
"停止"
|
||||
} else {
|
||||
"启动"
|
||||
Text(buttonState.value.text)
|
||||
}
|
||||
*/
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
|
||||
if (resourceList.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "企业资源 (${resourceList.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 16.dp, start = 24.dp).fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
items(resourceList) { resource ->
|
||||
Box(modifier = Modifier.padding(horizontal = 24.dp)) {
|
||||
ResourceItemRow(resource, navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResourceItemRow(resource: com.jihe.punchnet.api.ResourceItem, navController: androidx.navigation.NavHostController) {
|
||||
androidx.compose.material3.Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).clickable {
|
||||
navController.navigate(Screen.WebViewScreen.createRoute(resource.url))
|
||||
},
|
||||
colors = androidx.compose.material3.CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.tag),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(resource.name, fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||
Text(resource.url, fontSize = 14.sp, color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(
|
||||
color = if (resource.connectionStatus == "connected") Color(0xFF4CAF50) else Color.Gray,
|
||||
shape = androidx.compose.foundation.shape.CircleShape
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NodeItemRow(node: com.jihe.punchnet.api.NodeItem) {
|
||||
androidx.compose.material3.Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
colors = androidx.compose.material3.CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Icon
|
||||
val isMobile = node.system?.contains("Android", true) == true || node.system?.contains("iOS", true) == true
|
||||
Icon(
|
||||
imageVector = if (isMobile) Icons.Default.Phone else Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
// Texts
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(node.name, fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||
Text(node.ip, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
|
||||
// Status dot
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(
|
||||
color = if (node.connectionStatus == "connected") Color(0xFF4CAF50) else Color.Gray,
|
||||
shape = androidx.compose.foundation.shape.CircleShape
|
||||
)
|
||||
)
|
||||
}
|
||||
// BasicCardList(dbdao = dbdao, navController)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,10 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -25,15 +29,20 @@ import com.jihe.punchnet.data.RouteViewModel
|
||||
import com.jihe.punchnet.data.ServiceViewModel
|
||||
|
||||
sealed class Screen(val route: String) {
|
||||
object MainScreen: Screen("main");
|
||||
object RouteScreen: Screen("routes")
|
||||
object ProfileScreen: Screen("profile")
|
||||
object MainScreen: Screen("home") // 首页
|
||||
object DeviceScreen: Screen("devices") // 设备 (取代原先的 Route)
|
||||
object AppAuthScreen: Screen("appAuth") // 应用授权 (取代原先的 Profile)
|
||||
object SettingsScreen: Screen("settings") // 设置
|
||||
object WebViewScreen: Screen("webview/{url}") {
|
||||
fun createRoute(url: String) = "webview/${android.net.Uri.encode(url)}"
|
||||
}
|
||||
object LoginScreen: Screen("login")
|
||||
}
|
||||
|
||||
val bottomNavItems = listOf(
|
||||
Screen.MainScreen,
|
||||
Screen.RouteScreen,
|
||||
Screen.ProfileScreen,
|
||||
Screen.DeviceScreen,
|
||||
Screen.AppAuthScreen,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@ -44,10 +53,16 @@ fun MainApp(
|
||||
navController: NavHostController,
|
||||
) {
|
||||
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute by remember { derivedStateOf { navBackStackEntry?.destination?.route } }
|
||||
val showBottomBar = currentRoute != Screen.LoginScreen.route
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
if (showBottomBar) {
|
||||
AppBottomNavigation(navController = navController)
|
||||
}
|
||||
}
|
||||
){ paddingValues ->
|
||||
/*
|
||||
Image(
|
||||
|
||||
@ -1,211 +0,0 @@
|
||||
package com.jihe.punchnet.screen
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.collection.emptyLongSet
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.room.util.TableInfo
|
||||
import com.jihe.punchnet.data.RouteItem
|
||||
import com.jihe.punchnet.data.RouteViewModel
|
||||
import com.jihe.punchnet.helper.IntToIPString
|
||||
import com.jihe.punchnet.helper.parseCIDRAndGW
|
||||
import com.jihe.punchnet.sdlan.network.maskIPToDigit
|
||||
|
||||
@Composable
|
||||
fun AddOrModifyRoute(
|
||||
routeViewModel: RouteViewModel,
|
||||
initRoute: MutableState<RouteItem?>,
|
||||
shouldShowRoute: MutableState<Boolean>,
|
||||
|
||||
) {
|
||||
if (shouldShowRoute.value) {
|
||||
val context = LocalContext.current
|
||||
var cidr by remember {
|
||||
mutableStateOf(
|
||||
if (initRoute.value == null)
|
||||
""
|
||||
else
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
var gw by remember {
|
||||
mutableStateOf(
|
||||
if (initRoute.value == null)
|
||||
""
|
||||
else
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
shouldShowRoute.value = false
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
val item = parseCIDRAndGW(cidr, gw)
|
||||
if (item == null) {
|
||||
Toast.makeText(context, "数据解析出错", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
routeViewModel.insert(item)
|
||||
shouldShowRoute.value = false
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("确定")
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
"添加新路由"
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
TextField(
|
||||
value = cidr,
|
||||
onValueChange = { newValue ->
|
||||
cidr = newValue
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
"192.168.0.0/24",
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(.5f)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text("CIDR")
|
||||
},
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
|
||||
|
||||
TextField(
|
||||
value = gw,
|
||||
onValueChange = { newValue ->
|
||||
gw = newValue
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
"10.167.69.13",
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(.5f)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text("gateway")
|
||||
},
|
||||
modifier = Modifier.padding(start = 16.dp, top = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun RouteScreen(
|
||||
enabled: Boolean,
|
||||
routeViewModel: RouteViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val showAddRoute: MutableState<RouteItem?> = remember { mutableStateOf(null) }
|
||||
val shouldShow: MutableState<Boolean> = remember { mutableStateOf(false) }
|
||||
|
||||
AddOrModifyRoute(routeViewModel, showAddRoute, shouldShow)
|
||||
|
||||
val routeItems by routeViewModel.allRoutes.collectAsState(initial = emptyList())
|
||||
LazyColumn {
|
||||
item {
|
||||
Row(
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
items(routeItems) {item ->
|
||||
RouteCard(routeViewModel, item, enabled)
|
||||
}
|
||||
item {
|
||||
Button(
|
||||
enabled = enabled,
|
||||
shape = RectangleShape,
|
||||
onClick = {
|
||||
shouldShow.value = true
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("添加路由")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RouteCard(
|
||||
routeViewModel: RouteViewModel,
|
||||
item: RouteItem,
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card (
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
|
||||
){
|
||||
Row (
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.height(36.dp)
|
||||
){
|
||||
Icon(
|
||||
Icons.Default.Clear,
|
||||
contentDescription = "delete route",
|
||||
modifier = Modifier.size(30.dp).clickable(
|
||||
enabled = enabled,
|
||||
) {
|
||||
routeViewModel.deleteById(item.id)
|
||||
}
|
||||
)
|
||||
Text(
|
||||
"${IntToIPString(item.net_ip)}/${maskIPToDigit(item.mask_ip)}",
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
IntToIPString(item.gateway),
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -52,8 +52,9 @@ class NodeConfig (
|
||||
val localPort: Int,
|
||||
|
||||
val supernode: SDLanSock,
|
||||
val natServer1: InetSocketAddress,
|
||||
val natServer2: InetSocketAddress
|
||||
val stunServer: SDLanSock,
|
||||
val natServer1: java.net.InetSocketAddress,
|
||||
val natServer2: java.net.InetSocketAddress
|
||||
)
|
||||
|
||||
class NodeStats private constructor(
|
||||
@ -148,8 +149,16 @@ class Node private constructor (
|
||||
var startStopChannel: SendChannel<StartStopChanInfo>,
|
||||
|
||||
val mac: Mac = generateRandomMAC(),
|
||||
var nat_type: NatType = NatType.Invalid,
|
||||
var nat_type: NatType = NatType.PortRestricted,
|
||||
val natProbeCookie: NatProbeCookie = NatProbeCookie(),
|
||||
|
||||
var authData: com.jihe.punchnet.api.AuthResponseData? = null,
|
||||
var connectData: com.jihe.punchnet.api.ConnectResponseData? = null,
|
||||
var clientId: String = "",
|
||||
var sessionToken: com.google.protobuf.ByteString = com.google.protobuf.ByteString.EMPTY,
|
||||
val queriedPolicies: ConcurrentHashMap<Int, Int> = ConcurrentHashMap(),
|
||||
val queriedPolicyTargets: ConcurrentHashMap<Int, Int> = ConcurrentHashMap(),
|
||||
val policyRules: ConcurrentHashMap<Int, ByteArray> = ConcurrentHashMap(),
|
||||
) {
|
||||
companion object {
|
||||
private var instance: Node? = null
|
||||
@ -176,11 +185,6 @@ class Node private constructor (
|
||||
toSocket,
|
||||
startStopChannel,
|
||||
)
|
||||
} else {
|
||||
instance?.config = config
|
||||
instance?.udpSockV4 = v4Sock
|
||||
instance?.token = token
|
||||
Log.e("Initialize", "not initialize instance")
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
@ -237,24 +241,51 @@ class Node private constructor (
|
||||
}
|
||||
|
||||
suspend fun sendStunRequest() {
|
||||
if (this.clientId.isEmpty()) {
|
||||
this.clientId = "punchnet_android_client_" + UniqueNodeID.getUUID()
|
||||
}
|
||||
val req = SDLStunRequest.newBuilder()
|
||||
.setCookie(0)
|
||||
.setClientId(UniqueNodeID.getUUID())
|
||||
.setClientId(this.clientId)
|
||||
.setNetworkId(this.networkID.get())
|
||||
.setIp(this.deviceConfig.ip.netAddr)
|
||||
.setMac(this.mac)
|
||||
.setNatType(this.nat_type.toByte().toInt())
|
||||
.setV6Info(PunchProto.SDLV6Info.getDefaultInstance())
|
||||
.setSessionToken(this.sessionToken)
|
||||
.build()
|
||||
|
||||
val msg = encodeToUDPMessage(req, PacketType.StunRequest)
|
||||
sendToSock(this, msg, config.supernode)
|
||||
TerminalLogger.debugf {
|
||||
"send STUN_REQUEST to ${config.stunServer}, clientIdTail=${clientId.takeLast(12)}, " +
|
||||
"network=${req.networkId}, ip=${com.jihe.punchnet.sdlan.utils.ipToString(req.ip)}, " +
|
||||
"mac=${com.jihe.punchnet.sdlan.utils.macToString(req.mac)}, natType=${nat_type}, " +
|
||||
"sessionTokenBytes=${sessionToken.size()}"
|
||||
}
|
||||
sendToSock(this, msg, config.stunServer)
|
||||
}
|
||||
|
||||
|
||||
|
||||
suspend fun ping_to_sn() {
|
||||
val msg = encodeToControlMessage(null, PacketType.Ping).toByteArray()
|
||||
_sendDataToSocket(msg)
|
||||
}
|
||||
|
||||
suspend fun _sendDataToSocket(msg: ByteArray) {
|
||||
|
||||
if (aes.isAuthorized()) {
|
||||
TerminalLogger.debugf{"authorized, ping to sn"}
|
||||
toSocket.send(msg)
|
||||
} else {
|
||||
TerminalLogger.debugf{"unauthorized, not ping to sn"}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun probeNatType() {
|
||||
val previousNatType = nat_type
|
||||
val reply1 = this._sendAndWaitForProbeReply(StunProbeAttr.None, config.natServer1)
|
||||
if (reply1 == null) {
|
||||
nat_type = NatType.Blocked
|
||||
TerminalLogger.warning { "STUN probe attr=None timed out; keeping natType=${nat_type}" }
|
||||
return
|
||||
}
|
||||
if (reply1.ip == (outerIPV4.get())) {
|
||||
@ -275,7 +306,14 @@ class Node private constructor (
|
||||
|
||||
val reply3 = this._sendAndWaitForProbeReply(StunProbeAttr.None, config.natServer2)
|
||||
if (reply3 == null) {
|
||||
nat_type = NatType.Blocked
|
||||
nat_type = if (previousNatType == NatType.Invalid || previousNatType == NatType.Blocked) {
|
||||
NatType.PortRestricted
|
||||
} else {
|
||||
previousNatType
|
||||
}
|
||||
TerminalLogger.warning {
|
||||
"secondary STUN probe timed out after primary reply; keeping natType=${nat_type}"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -293,38 +331,28 @@ class Node private constructor (
|
||||
|
||||
}
|
||||
|
||||
suspend fun _sendAndWaitForProbeReply(attr: StunProbeAttr, toServer: SocketAddress): SDLStunProbeReply? {
|
||||
val channel = Channel<SDLStunProbeReply>(100)
|
||||
suspend fun _sendAndWaitForProbeReply(attr: StunProbeAttr, toServer: java.net.SocketAddress): SDLStunProbeReply? {
|
||||
val channel = kotlinx.coroutines.channels.Channel<SDLStunProbeReply>(100)
|
||||
val cookie = natProbeCookie.addChannel(channel)
|
||||
val probe = SDLStunProbe.newBuilder()
|
||||
val probe = com.jihe.punchnet.protobuf.PunchProto.SDLStunProbe.newBuilder()
|
||||
.setAttr(attr.ordinal.toInt())
|
||||
.setCookie(cookie)
|
||||
.build()
|
||||
|
||||
val msg = encodeToUDPMessage(probe, PacketType.StunProbe)
|
||||
TerminalLogger.debugf { "send STUN_PROBE attr=$attr cookie=$cookie to $toServer" }
|
||||
this.udpSockV4.send_to(msg.toByteArray(), toServer)
|
||||
|
||||
val k = withTimeoutOrNull(5000) {
|
||||
|
||||
val k = kotlinx.coroutines.withTimeoutOrNull(5000) {
|
||||
val response = channel.receive()
|
||||
return@withTimeoutOrNull response
|
||||
}
|
||||
if (k == null) {
|
||||
TerminalLogger.warning { "STUN_PROBE attr=$attr cookie=$cookie to $toServer timed out" }
|
||||
} else {
|
||||
TerminalLogger.debugf { "STUN_PROBE attr=$attr cookie=$cookie reply ip=${k.ip} port=${k.port}" }
|
||||
}
|
||||
natProbeCookie.remove(cookie)
|
||||
return k
|
||||
}
|
||||
|
||||
suspend fun ping_to_sn() {
|
||||
val msg = encodeToTcpMessage(null, 0, PacketType.Ping).toByteArray()
|
||||
_sendDataToSocket(msg)
|
||||
}
|
||||
|
||||
suspend fun _sendDataToSocket(msg: ByteArray) {
|
||||
|
||||
if (aes.isAuthorized()) {
|
||||
TerminalLogger.debugf{"authorized, ping to sn"}
|
||||
toSocket.send(msg)
|
||||
} else {
|
||||
TerminalLogger.debugf{"unauthorized, not ping to sn"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,29 @@
|
||||
package com.jihe.punchnet.sdlan.network
|
||||
|
||||
import com.google.protobuf.kotlin.toByteString
|
||||
import com.google.gson.JsonElement
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLData
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLPeerInfo
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLQueryInfo
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegister
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterAck
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLSendRegisterEvent
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLArpRequest
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLArpResponse
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLExposedServiceRequest
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLPolicyRequest
|
||||
|
||||
import com.jihe.punchnet.sdlan.config.EtherType
|
||||
import com.jihe.punchnet.sdlan.config.SDLanConfig
|
||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
||||
import com.jihe.punchnet.sdlan.utils.isMultiBroadcast
|
||||
import com.jihe.punchnet.sdlan.utils.ipStringToInt
|
||||
import com.jihe.punchnet.sdlan.utils.ipToString
|
||||
import com.jihe.punchnet.sdlan.utils.macToString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URI
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
@ -299,15 +307,311 @@ suspend fun sendRegister(
|
||||
.build()
|
||||
val msg = encodeToUDPMessage(register, PacketType.Register)
|
||||
|
||||
TerminalLogger.debugf { "send REGISTER to ${macToString(mac)} at $sock, natType=$natType" }
|
||||
sendToSock(node, msg, sock)
|
||||
|
||||
// TODO: need guess port
|
||||
if (natType == NatType.Symmetric || natType == NatType.PortRestricted) {
|
||||
TerminalLogger.debugf { "guess ports for symmetric/restricted nat" }
|
||||
val guessOffsets = intArrayOf(-1, 1, -2, 2)
|
||||
for (offset in guessOffsets) {
|
||||
val guessPort = sock.port + offset
|
||||
if (guessPort in 1..65535) {
|
||||
val guessSock = SDLanSock(sock.family, guessPort, sock.ip)
|
||||
sendToSock(node, msg, guessSock)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendPacketToNet(node: Node, dstmac: Mac, content: List<Byte>, size: Long) {
|
||||
val destination = findPeerDestination(node, dstmac, size)
|
||||
TerminalLogger.debugf { "send PACKET to ${destination}" }
|
||||
sendToSock(node, content, destination)
|
||||
suspend fun sendPacketToNet(node: Node, dstmac: Mac, encrypted: ByteArray, size: Long) {
|
||||
var is_p2p: Boolean = false
|
||||
var is_multicast: Boolean = false
|
||||
var destination: SDLanSock
|
||||
|
||||
if (isMultiBroadcast(dstmac)) {
|
||||
node.stats.txSup.addAndGet(size)
|
||||
node.stats.txBroadcast.addAndGet(size)
|
||||
destination = node.config.stunServer
|
||||
is_multicast = true
|
||||
} else {
|
||||
val peer = node.knownPeers.get(dstmac)
|
||||
if (peer == null) {
|
||||
node.stats.txSup.addAndGet(size)
|
||||
destination = node.config.stunServer
|
||||
} else {
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
if (now - peer.lastP2P.get() >= peer.timeout / 2) {
|
||||
TerminalLogger.warning { "last p2p is too old, deleting from known hosts" }
|
||||
node.knownPeers.remove(dstmac)
|
||||
node.stats.txSup.addAndGet(size)
|
||||
destination = node.config.stunServer
|
||||
} else {
|
||||
is_p2p = true
|
||||
node.stats.txP2P.addAndGet(size)
|
||||
destination = peer.sock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_p2p && !is_multicast) {
|
||||
TerminalLogger.debugf { "check_query_peer_info" }
|
||||
checkQueryPeerInfo(node, dstmac)
|
||||
}
|
||||
|
||||
val transportIdentityId = node.connectData?.identityId ?: 0
|
||||
val data = com.jihe.punchnet.protobuf.PunchProto.SDLData.newBuilder()
|
||||
.setIsP2P(is_p2p)
|
||||
.setNetworkId(node.networkID.get())
|
||||
.setTtl(2)
|
||||
.setSrcMac(node.mac)
|
||||
.setDstMac(dstmac)
|
||||
.setData(com.google.protobuf.ByteString.copyFrom(encrypted))
|
||||
.setSessionToken(node.sessionToken)
|
||||
.setIdentityId(transportIdentityId)
|
||||
.build()
|
||||
|
||||
val msg = encodeToUDPMessage(data, PacketType.Data)
|
||||
TerminalLogger.debugf {
|
||||
"send PACKET to ${destination} with isP2P=$is_p2p, ttl=${data.ttl}, " +
|
||||
"src=${macToString(data.srcMac)}, dst=${macToString(data.dstMac)}, " +
|
||||
"network=${data.networkId}, identityId=${data.identityId}, apiIdentity=${node.connectData?.identityId ?: 0}, " +
|
||||
"sessionTokenBytes=${data.sessionToken.size()}, encryptedBytes=${encrypted.size}"
|
||||
}
|
||||
sendToSock(node, msg, destination)
|
||||
}
|
||||
|
||||
// Removed dummy findPeerDestination
|
||||
|
||||
fun describeIpv4Packet(data: ByteArray): String {
|
||||
if (data.size < 20) {
|
||||
return "short-ipv4 bytes=${data.size}"
|
||||
}
|
||||
val version = (data[0].toInt() ushr 4) and 0x0f
|
||||
if (version != 4) {
|
||||
return "non-ipv4 version=$version bytes=${data.size}"
|
||||
}
|
||||
val ihl = (data[0].toInt() and 0x0f) * 4
|
||||
if (ihl < 20 || data.size < ihl) {
|
||||
return "bad-ipv4-header ihl=$ihl bytes=${data.size}"
|
||||
}
|
||||
val proto = data[9].toInt() and 0xff
|
||||
val srcIp = ByteBuffer.wrap(data, 12, 4).order(ByteOrder.BIG_ENDIAN).int
|
||||
val dstIp = ByteBuffer.wrap(data, 16, 4).order(ByteOrder.BIG_ENDIAN).int
|
||||
val protoName = when (proto) {
|
||||
1 -> "ICMP"
|
||||
6 -> "TCP"
|
||||
17 -> "UDP"
|
||||
else -> "proto-$proto"
|
||||
}
|
||||
val portInfo = if ((proto == 6 || proto == 17) && data.size >= ihl + 4) {
|
||||
val srcPort = ByteBuffer.wrap(data, ihl, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
|
||||
val dstPort = ByteBuffer.wrap(data, ihl + 2, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
|
||||
" srcPort=$srcPort dstPort=$dstPort"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
return "$protoName ${ipToString(srcIp)} -> ${ipToString(dstIp)}$portInfo bytes=${data.size}"
|
||||
}
|
||||
|
||||
fun describePolicyRules(rules: ByteArray): String {
|
||||
if (rules.isEmpty()) {
|
||||
return "empty"
|
||||
}
|
||||
val result = mutableListOf<String>()
|
||||
var offset = 0
|
||||
while (offset + 3 <= rules.size) {
|
||||
val proto = rules[offset].toInt() and 0xff
|
||||
val port = ByteBuffer.wrap(rules, offset + 1, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
|
||||
val protoName = when (proto) {
|
||||
1 -> "ICMP"
|
||||
6 -> "TCP"
|
||||
17 -> "UDP"
|
||||
else -> "proto-$proto"
|
||||
}
|
||||
result.add("$protoName/$port")
|
||||
offset += 3
|
||||
}
|
||||
if (offset != rules.size) {
|
||||
result.add("trailingBytes=${rules.size - offset}")
|
||||
}
|
||||
return result.joinToString(",")
|
||||
}
|
||||
|
||||
data class PolicyDecision(
|
||||
val state: PolicyDecisionState,
|
||||
val reason: String,
|
||||
)
|
||||
|
||||
enum class PolicyDecisionState {
|
||||
Allowed,
|
||||
Pending,
|
||||
Denied,
|
||||
}
|
||||
|
||||
fun policyDecisionForIpv4Packet(node: Node, data: ByteArray): PolicyDecision {
|
||||
if (data.size < 20) {
|
||||
return PolicyDecision(PolicyDecisionState.Denied, "too-short")
|
||||
}
|
||||
val version = (data[0].toInt() ushr 4) and 0x0f
|
||||
if (version != 4) {
|
||||
return PolicyDecision(PolicyDecisionState.Denied, "non-ipv4")
|
||||
}
|
||||
val ihl = (data[0].toInt() and 0x0f) * 4
|
||||
if (ihl < 20 || data.size < ihl) {
|
||||
return PolicyDecision(PolicyDecisionState.Denied, "bad-ipv4-header")
|
||||
}
|
||||
val proto = data[9].toInt() and 0xff
|
||||
val targetIp = ByteBuffer.wrap(data, 16, 4).order(ByteOrder.BIG_ENDIAN).int
|
||||
val port = if ((proto == 6 || proto == 17) && data.size >= ihl + 4) {
|
||||
ByteBuffer.wrap(data, ihl + 2, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val candidates = policyCandidateIdsForTarget(node, targetIp)
|
||||
if (candidates.isEmpty()) {
|
||||
return PolicyDecision(PolicyDecisionState.Denied, "no policy identity for ${ipToString(targetIp)}")
|
||||
}
|
||||
val missing = candidates.filterNot { node.policyRules.containsKey(it) }
|
||||
if (missing.isNotEmpty()) {
|
||||
return PolicyDecision(PolicyDecisionState.Pending, "waiting policy for ${ipToString(targetIp)}, ids=${missing.joinToString(",")}")
|
||||
}
|
||||
for (candidate in candidates) {
|
||||
val rules = node.policyRules[candidate] ?: continue
|
||||
if (policyRulesAllow(rules, proto, port)) {
|
||||
return PolicyDecision(
|
||||
PolicyDecisionState.Allowed,
|
||||
"allowed by policy id=$candidate ${protocolName(proto)}/$port"
|
||||
)
|
||||
}
|
||||
}
|
||||
return PolicyDecision(
|
||||
PolicyDecisionState.Denied,
|
||||
"no allow rule for ${ipToString(targetIp)} ${protocolName(proto)}/$port, ids=${candidates.joinToString(",")}"
|
||||
)
|
||||
}
|
||||
|
||||
fun policyCandidateIdsForTarget(node: Node, targetIp: Int): List<Int> {
|
||||
val result = mutableListOf<Int>()
|
||||
node.connectData?.nodeList.orEmpty().firstOrNull {
|
||||
runCatching { ipStringToInt(it.ip) == targetIp }.getOrDefault(false)
|
||||
}?.let { result.add(it.id) }
|
||||
result.addAll(matchingResourceIdsForTarget(node, targetIp))
|
||||
return result.distinct()
|
||||
}
|
||||
|
||||
fun policyRulesAllow(rules: ByteArray, proto: Int, port: Int): Boolean {
|
||||
if (rules.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
var offset = 0
|
||||
while (offset + 3 <= rules.size) {
|
||||
val ruleProto = rules[offset].toInt() and 0xff
|
||||
val rulePort = ByteBuffer.wrap(rules, offset + 1, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
|
||||
if (ruleProto == proto && (rulePort == port || rulePort == 0)) {
|
||||
return true
|
||||
}
|
||||
offset += 3
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun protocolName(proto: Int): String {
|
||||
return when (proto) {
|
||||
1 -> "ICMP"
|
||||
6 -> "TCP"
|
||||
17 -> "UDP"
|
||||
else -> "proto-$proto"
|
||||
}
|
||||
}
|
||||
|
||||
fun describeConnectDataForLog(node: Node): String {
|
||||
val connectData = node.connectData ?: return "connect data is null"
|
||||
val localNodeId = localNodeId(node) ?: 0
|
||||
val nodeSummary = connectData.nodeList.orEmpty().joinToString(";") {
|
||||
"id=${it.id},ip=${it.ip},status=${it.connectionStatus}"
|
||||
}.ifEmpty { "none" }
|
||||
val resourceSummary = connectData.resourceList.orEmpty().map { resource ->
|
||||
val uri = runCatching { URI(resource.url) }.getOrNull()
|
||||
val host = uri?.host ?: "invalid-host"
|
||||
val scheme = uri?.scheme ?: "unknown"
|
||||
val port = when {
|
||||
uri == null -> -1
|
||||
uri.port > 0 -> uri.port
|
||||
scheme.equals("http", ignoreCase = true) -> 80
|
||||
scheme.equals("https", ignoreCase = true) -> 443
|
||||
else -> -1
|
||||
}
|
||||
"id=${resource.id},${scheme}://${host}:${port},status=${resource.connectionStatus}"
|
||||
}.joinToString(";").ifEmpty { "none" }
|
||||
return "connect summary: ip=${connectData.ip}/${connectData.maskLen}, identity=${connectData.identityId}, localNodeId=$localNodeId, " +
|
||||
"nodes=${connectData.nodeList.orEmpty().size}[$nodeSummary], " +
|
||||
"resources=${connectData.resourceList.orEmpty().size}[$resourceSummary], " +
|
||||
"acl=${describeAclForLog(connectData.acl)}"
|
||||
}
|
||||
|
||||
fun localNodeId(node: Node): Int? {
|
||||
val localIp = node.connectData?.ip ?: return null
|
||||
return node.connectData?.nodeList.orEmpty().firstOrNull { it.ip == localIp }?.id
|
||||
}
|
||||
|
||||
fun describeAclForLog(acl: JsonElement?): String {
|
||||
if (acl == null || acl.isJsonNull) {
|
||||
return "null"
|
||||
}
|
||||
if (acl.isJsonArray) {
|
||||
val array = acl.asJsonArray
|
||||
if (array.size() == 0) {
|
||||
return "array(size=0)"
|
||||
}
|
||||
val samples = array.take(3).mapIndexed { index, item ->
|
||||
"[$index]=${describeAclElementForLog(item)}"
|
||||
}
|
||||
return "array(size=${array.size()}, ${samples.joinToString(",")})"
|
||||
}
|
||||
if (acl.isJsonObject) {
|
||||
return describeAclObjectForLog(acl)
|
||||
}
|
||||
if (acl.isJsonPrimitive) {
|
||||
return "primitive"
|
||||
}
|
||||
return acl.javaClass.simpleName
|
||||
}
|
||||
|
||||
private fun describeAclElementForLog(element: JsonElement): String {
|
||||
return when {
|
||||
element.isJsonObject -> describeAclObjectForLog(element)
|
||||
element.isJsonArray -> "array(size=${element.asJsonArray.size()})"
|
||||
element.isJsonNull -> "null"
|
||||
element.isJsonPrimitive -> "primitive"
|
||||
else -> element.javaClass.simpleName
|
||||
}
|
||||
}
|
||||
|
||||
private fun describeAclObjectForLog(element: JsonElement): String {
|
||||
val obj = element.asJsonObject
|
||||
val fields = obj.entrySet().take(12).joinToString(",") { (key, value) ->
|
||||
"$key=${describeAclValueForLog(value)}"
|
||||
}
|
||||
val suffix = if (obj.entrySet().size > 12) ",..." else ""
|
||||
return "object($fields$suffix)"
|
||||
}
|
||||
|
||||
private fun describeAclValueForLog(value: JsonElement): String {
|
||||
return when {
|
||||
value.isJsonNull -> "null"
|
||||
value.isJsonArray -> "array(${value.asJsonArray.size()})"
|
||||
value.isJsonObject -> "object(${value.asJsonObject.entrySet().joinToString(",") { it.key }})"
|
||||
value.isJsonPrimitive -> {
|
||||
val primitive = value.asJsonPrimitive
|
||||
when {
|
||||
primitive.isNumber -> primitive.asNumber.toString()
|
||||
primitive.isBoolean -> primitive.asBoolean.toString()
|
||||
primitive.isString -> primitive.asString.take(48)
|
||||
else -> "primitive"
|
||||
}
|
||||
}
|
||||
else -> value.javaClass.simpleName
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -318,20 +622,20 @@ suspend fun findPeerDestination(node: Node, dstmac: Mac, size: Long): SDLanSock
|
||||
if (isMultiBroadcast(dstmac)) {
|
||||
node.stats.txSup.addAndGet(size)
|
||||
node.stats.txBroadcast.addAndGet(size)
|
||||
result = node.config.supernode
|
||||
result = node.config.stunServer
|
||||
is_multicast = true
|
||||
} else {
|
||||
val peer = node.knownPeers.get(dstmac)
|
||||
if (peer == null) {
|
||||
node.stats.txSup.addAndGet(size)
|
||||
result = node.config.supernode
|
||||
result = node.config.stunServer
|
||||
} else {
|
||||
val now = System.currentTimeMillis()/1000
|
||||
if (now - peer.lastP2P.get() >= peer.timeout/2) {
|
||||
TerminalLogger.warning { "last p2p is too old, deleting from known hosts" }
|
||||
node.knownPeers.remove(dstmac)
|
||||
node.stats.txSup.addAndGet(size)
|
||||
result = node.config.supernode
|
||||
result = node.config.stunServer
|
||||
} else {
|
||||
is_p2p = true
|
||||
node.stats.txP2P.addAndGet(size)
|
||||
@ -389,12 +693,12 @@ suspend fun sendQueryPeer(node: Node, dstmac: Mac) {
|
||||
val query = SDLQueryInfo.newBuilder()
|
||||
.setDstMac(dstmac)
|
||||
.build()
|
||||
val msg = encodeToTcpMessage(query, node.getNextPacketID(), PacketType.QueryInfo)
|
||||
val msg = encodeToControlMessage(query, PacketType.QueryInfo)
|
||||
node.toSocket.send(msg.toByteArray())
|
||||
}
|
||||
|
||||
fun formEthernetPacket(srcmac: ByteArray, dstmac: ByteArray, data: ByteArray): ByteBuffer {
|
||||
val buffer = ByteBuffer.allocate(14 + data.size + 4).order(ByteOrder.BIG_ENDIAN)
|
||||
val buffer = ByteBuffer.allocate(14 + data.size).order(ByteOrder.BIG_ENDIAN)
|
||||
buffer.put(dstmac)
|
||||
buffer.put(srcmac)
|
||||
buffer.putShort(EtherType.IPV4)
|
||||
@ -407,54 +711,140 @@ fun formEthernetPacket(srcmac: ByteArray, dstmac: ByteArray, data: ByteArray): B
|
||||
// fromip is self ip
|
||||
// queryip is the targetip
|
||||
suspend fun sendArpRequest(node: Node, queryip: Int) {
|
||||
println(1)
|
||||
val frommac = node.mac.toByteArray()
|
||||
val fromip = node.deviceConfig.ip.netAddr
|
||||
val buffer = ByteBuffer.allocate(64).order(ByteOrder.BIG_ENDIAN)
|
||||
buffer.put(ByteArray(6){0xff.toByte()})
|
||||
buffer.put(frommac)
|
||||
println(2)
|
||||
// println("src mac: ${macToString(node.mac)}")
|
||||
buffer.putShort(EtherType.ARP)
|
||||
buffer.putShort(EtherType.HWTYPE_ETH)
|
||||
buffer.putShort(EtherType.IPV4)
|
||||
|
||||
buffer.put(6)
|
||||
buffer.put(4)
|
||||
buffer.putShort(EtherType.ARP_REQUEST)
|
||||
buffer.put(frommac)
|
||||
buffer.putInt(fromip)
|
||||
buffer.put(ByteArray(6))
|
||||
buffer.putInt(queryip)
|
||||
|
||||
buffer.flip()
|
||||
println(3)
|
||||
|
||||
val size = buffer.remaining()
|
||||
|
||||
println(3.1)
|
||||
val output = node.aes.encrypt(buffer)
|
||||
println(3.2)
|
||||
|
||||
println(4)
|
||||
if (output != null) {
|
||||
val d = output.toByteString()
|
||||
val data = SDLData.newBuilder()
|
||||
.setIsP2P(true)
|
||||
.setNetworkId(node.networkID.get())
|
||||
.setTtl(2)
|
||||
.setSrcMac(frommac.toByteString())
|
||||
.setDstMac(SDLanConfig.BROADCASTMAC)
|
||||
.setData(d)
|
||||
.build()
|
||||
println(5)
|
||||
val msg = encodeToUDPMessage(data, PacketType.Data)
|
||||
sendPacketToNet(node, SDLanConfig.BROADCASTMAC, msg, size.toLong())
|
||||
if (!node.aes.isAuthorized()) {
|
||||
TerminalLogger.errorf { "not authed for send arp request" }
|
||||
return
|
||||
}
|
||||
println(6)
|
||||
val arpReq = SDLArpRequest.newBuilder()
|
||||
.setTargetIp(queryip)
|
||||
.setOriginIp(node.deviceConfig.ip.netAddr)
|
||||
.build()
|
||||
val msg = encodeToControlMessage(arpReq, PacketType.ArpRequest)
|
||||
node.toSocket.send(msg.toByteArray())
|
||||
TerminalLogger.debugf { "sent QUIC ARP request for ip: ${com.jihe.punchnet.sdlan.utils.ipToString(queryip)}" }
|
||||
}
|
||||
|
||||
suspend fun handleTcpCommand(node: Node, cmdtype: Byte, cmdprotobuf: ByteArray) {}
|
||||
suspend fun handlePacketArpResponse(node: Node, content: ByteArray) {
|
||||
val arpRes: SDLArpResponse
|
||||
try {
|
||||
arpRes = SDLArpResponse.parseFrom(content)
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf { "failed to decode arp response: $e"}
|
||||
return
|
||||
}
|
||||
|
||||
val targetIp = arpRes.targetIp
|
||||
val targetMac = arpRes.targetMac
|
||||
|
||||
if (targetMac.isEmpty) {
|
||||
TerminalLogger.debugf { "ARP response returned empty MAC for IP: ${com.jihe.punchnet.sdlan.utils.ipToString(targetIp)}" }
|
||||
return
|
||||
}
|
||||
|
||||
TerminalLogger.debugf { "Rx QUIC ARP response for IP: ${com.jihe.punchnet.sdlan.utils.ipToString(targetIp)}, MAC: ${macToString(targetMac)}" }
|
||||
sendPolicyRequestForTargetIp(node, targetIp)
|
||||
if (node.iface is IfaceTun) {
|
||||
val tun = node.iface as IfaceTun
|
||||
tun.arpTable.addToARPTable(targetIp, targetMac.toByteArray())
|
||||
tun.arpWaitList.arpArrived(node, targetIp, targetMac)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendExposedServiceRequest(node: Node) {
|
||||
if (!node.aes.isAuthorized()) {
|
||||
return
|
||||
}
|
||||
val req = SDLExposedServiceRequest.newBuilder()
|
||||
.setVersion(0)
|
||||
.build()
|
||||
node.toSocket.send(encodeToControlMessage(req, PacketType.ExposedServiceRequest).toByteArray())
|
||||
TerminalLogger.debugf { "sent ExposedServiceRequest version=0" }
|
||||
}
|
||||
|
||||
suspend fun sendPolicyRequestForTargetIp(node: Node, targetIp: Int) {
|
||||
if (!node.aes.isAuthorized()) {
|
||||
return
|
||||
}
|
||||
val srcIdentityId = node.connectData?.identityId ?: 0
|
||||
if (srcIdentityId == 0) {
|
||||
TerminalLogger.warning { "skip PolicyRequest for ${ipToString(targetIp)}: missing src identity" }
|
||||
return
|
||||
}
|
||||
|
||||
val dstNode = node.connectData?.nodeList?.firstOrNull {
|
||||
runCatching { ipStringToInt(it.ip) == targetIp }.getOrDefault(false)
|
||||
}
|
||||
if (dstNode == null) {
|
||||
TerminalLogger.warning { "skip PolicyRequest for ${ipToString(targetIp)}: target not in node_list" }
|
||||
return
|
||||
}
|
||||
|
||||
TerminalLogger.debugf {
|
||||
"policy target ${ipToString(targetIp)}: srcIdentity=$srcIdentityId, apiIdentity=${node.connectData?.identityId ?: 0}, " +
|
||||
"nodeId=${dstNode.id}, nodeStatus=${dstNode.connectionStatus}, " +
|
||||
describeResourcesForTarget(node, targetIp) + ", acl=${describeAclForLog(node.connectData?.acl)}"
|
||||
}
|
||||
|
||||
sendPolicyRequest(node, srcIdentityId, dstNode.id, targetIp, "node:${dstNode.name}/${dstNode.connectionStatus}")
|
||||
|
||||
matchingResourceIdsForTarget(node, targetIp)
|
||||
.filter { it != dstNode.id }
|
||||
.forEach { resourceId ->
|
||||
sendPolicyRequest(node, srcIdentityId, resourceId, targetIp, "resource")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendPolicyRequest(
|
||||
node: Node,
|
||||
srcIdentityId: Int,
|
||||
dstIdentityId: Int,
|
||||
targetIp: Int,
|
||||
source: String,
|
||||
) {
|
||||
if (node.queriedPolicies.putIfAbsent(dstIdentityId, 0) != null) {
|
||||
return
|
||||
}
|
||||
node.queriedPolicyTargets[dstIdentityId] = targetIp
|
||||
val req = SDLPolicyRequest.newBuilder()
|
||||
.setSrcIdentityId(srcIdentityId)
|
||||
.setDstIdentityId(dstIdentityId)
|
||||
.setVersion(0)
|
||||
.build()
|
||||
node.toSocket.send(encodeToControlMessage(req, PacketType.PolicyRequest).toByteArray())
|
||||
TerminalLogger.debugf {
|
||||
"sent PolicyRequest srcIdentity=$srcIdentityId, dstIdentity=$dstIdentityId, dstIp=${ipToString(targetIp)}, source=$source, version=0"
|
||||
}
|
||||
}
|
||||
|
||||
fun describeResourcesForTarget(node: Node, targetIp: Int): String {
|
||||
val targetIpText = ipToString(targetIp)
|
||||
val matches = node.connectData?.resourceList.orEmpty().mapNotNull { resource ->
|
||||
val uri = runCatching { URI(resource.url) }.getOrNull() ?: return@mapNotNull null
|
||||
if (uri.host != targetIpText) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
val port = when {
|
||||
uri.port > 0 -> uri.port
|
||||
uri.scheme.equals("http", ignoreCase = true) -> 80
|
||||
uri.scheme.equals("https", ignoreCase = true) -> 443
|
||||
else -> -1
|
||||
}
|
||||
"id=${resource.id}:${uri.scheme ?: "unknown"}/$port"
|
||||
}.distinct()
|
||||
return if (matches.isEmpty()) {
|
||||
"no matching resource in connect resource_list for $targetIpText"
|
||||
} else {
|
||||
"matching resources for $targetIpText: ${matches.joinToString(",")}"
|
||||
}
|
||||
}
|
||||
|
||||
fun matchingResourceIdsForTarget(node: Node, targetIp: Int): List<Int> {
|
||||
val targetIpText = ipToString(targetIp)
|
||||
return node.connectData?.resourceList.orEmpty().mapNotNull { resource ->
|
||||
val uri = runCatching { URI(resource.url) }.getOrNull() ?: return@mapNotNull null
|
||||
if (uri.host == targetIpText) resource.id else null
|
||||
}.distinct()
|
||||
}
|
||||
|
||||
suspend fun handlePacketPeerInfo(node: Node, content: ByteArray) {
|
||||
val pinfo: SDLPeerInfo
|
||||
@ -470,6 +860,11 @@ suspend fun handlePacketPeerInfo(node: Node, content: ByteArray) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!pinfo.hasV4Info() || pinfo.v4Info.v4.size() != 4 || pinfo.v4Info.port == 0) {
|
||||
TerminalLogger.warning { "PeerInfo for ${macToString(pinfo.dstMac)} has no usable IPv4 endpoint" }
|
||||
return
|
||||
}
|
||||
|
||||
val remoteNat = NatType.fromUByte(pinfo.v4Info.natType.toUByte())
|
||||
val pending = node.pendingPeers.get(pinfo.dstMac)
|
||||
if (pending == null) {
|
||||
@ -482,16 +877,18 @@ suspend fun handlePacketPeerInfo(node: Node, content: ByteArray) {
|
||||
sendRegister(node, remoteNat, pending.sock, pinfo.dstMac)
|
||||
}
|
||||
|
||||
suspend fun handleTcpEvent(node: Node, event: EventType, cmdprotobuf: ByteArray) {
|
||||
when(event) {
|
||||
EventType.SendRegister -> {
|
||||
val reg: SDLSendRegisterEvent
|
||||
suspend fun handleEvent(node: Node, cmdprotobuf: ByteArray) {
|
||||
val evt: com.jihe.punchnet.protobuf.PunchProto.SDLEvent
|
||||
try {
|
||||
reg = SDLSendRegisterEvent.parseFrom(cmdprotobuf)
|
||||
evt = com.jihe.punchnet.protobuf.PunchProto.SDLEvent.parseFrom(cmdprotobuf)
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf {"failed to decode SendRegisterEvent: $e"}
|
||||
TerminalLogger.errorf {"failed to decode SDLEvent: $e"}
|
||||
return
|
||||
}
|
||||
|
||||
when(evt.eventCase) {
|
||||
com.jihe.punchnet.protobuf.PunchProto.SDLEvent.EventCase.SEND_REGISTER -> {
|
||||
val reg = evt.sendRegister
|
||||
val remoteNat = NatType.fromUByte(reg.natType.toUByte())
|
||||
val ip = byteArrayOf(
|
||||
(reg.natIp ushr 24).and(0xff).toByte(),
|
||||
@ -502,7 +899,7 @@ suspend fun handleTcpEvent(node: Node, event: EventType, cmdprotobuf: ByteArray)
|
||||
checkPeerRegistrationNeeded(node,false, reg.dstMac, remoteNat, SDLanSock(IPFamily.IPV4, reg.natPort, ip))
|
||||
}
|
||||
else -> {
|
||||
TerminalLogger.warning { "unhandled event: $event" }
|
||||
TerminalLogger.warning { "unhandled event: ${evt.eventCase}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,10 +4,12 @@ import android.util.Log
|
||||
import com.jihe.punchnet.PunchnetServiceArgument
|
||||
import com.jihe.punchnet.data.ButtonRepository
|
||||
import com.jihe.punchnet.data.ButtonState
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLDevAddr
|
||||
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuper
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuperAck
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuperNak
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLExposedServiceResponse
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLPolicyResponse
|
||||
import com.jihe.punchnet.protobuf.PunchProto.SDLStunProbeReply
|
||||
import com.jihe.punchnet.sdlan.config.Arguments
|
||||
import com.jihe.punchnet.sdlan.config.RSAConfig
|
||||
@ -47,52 +49,92 @@ import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
|
||||
suspend fun onMessage(scope: CoroutineScope, data: SDLanControl) {
|
||||
val node = Node.getInstance()
|
||||
|
||||
TerminalLogger.debugf {"message received"}
|
||||
when(data.packetType) {
|
||||
PacketType.Welcome -> {
|
||||
TerminalLogger.debugf { "got welcome, sending register super" }
|
||||
if (node.clientId.isEmpty()) {
|
||||
node.clientId = "punchnet_android_client_" + UniqueNodeID.getUUID()
|
||||
}
|
||||
val registerIp = com.jihe.punchnet.sdlan.utils.ipStringToInt(node.connectData?.ip ?: "0.0.0.0").toUInt().toInt()
|
||||
|
||||
val registerSuper = SDLRegisterSuper.newBuilder()
|
||||
.setClientId(node.clientId)
|
||||
.setNetworkId(node.authData?.networkId ?: 0)
|
||||
.setMac(com.google.protobuf.ByteString.copyFrom(node.mac.toByteArray()))
|
||||
.setIp(registerIp)
|
||||
.setMaskLen(node.connectData?.maskLen ?: 0)
|
||||
.setHostname(node.connectData?.hostname ?: "")
|
||||
.setPubKey(node.rsa.getPublicKeyString())
|
||||
.setAccessToken(node.authData?.accessToken ?: "")
|
||||
.build()
|
||||
|
||||
val msg = encodeToControlMessage(registerSuper, PacketType.RegisterSuper)
|
||||
node.toSocket.send(msg.toByteArray())
|
||||
TerminalLogger.debugf {
|
||||
"sent RegisterSuper clientIdTail=${node.clientId.takeLast(12)}, network=${registerSuper.networkId}, " +
|
||||
"ip=${ipToString(registerSuper.ip)}, mask=${registerSuper.maskLen}, mac=${macToString(registerSuper.mac)}, " +
|
||||
"identity=${node.connectData?.identityId ?: 0}"
|
||||
}
|
||||
}
|
||||
PacketType.RegisterSuperACK -> {
|
||||
val ack = SDLRegisterSuperAck.parseFrom(data.currentPacket)
|
||||
// TerminalLogger.debugf { "got register super ack: $ack" }
|
||||
println("ack is ${ack}")
|
||||
println("ack's key is ${ack.aesKey.size()}")
|
||||
val algorithm = ack.algorithm.ifEmpty { "aes" }.lowercase()
|
||||
TerminalLogger.debugf {
|
||||
"got register super ack: algorithm=$algorithm, region=${ack.regionId}, sessionTokenBytes=${ack.sessionToken.size()}"
|
||||
}
|
||||
if (algorithm != "aes" && algorithm != "chacha20") {
|
||||
TerminalLogger.errorf { "unsupported encryption algorithm from server: ${ack.algorithm}" }
|
||||
node.startStopChannel.send(StartStopChanInfo(StartStopFlag.IsStop, null))
|
||||
ButtonRepository.updateState(ButtonState.ButtonStopped)
|
||||
scope.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
val aeskey = node.rsa.decrypt(ack.aesKey.toByteArray())
|
||||
val aeskey = node.rsa.decrypt(ack.key.toByteArray())
|
||||
if (aeskey == null) {
|
||||
println("aes key error")
|
||||
return
|
||||
}
|
||||
println("decrypted aes is: ${aeskey.contentToString()}")
|
||||
node.aes.setSecret(aeskey)
|
||||
val ipStr = ipToString(ack.devAddr.netAddr)
|
||||
TerminalLogger.debugf { "assigned ip: $ipStr" }
|
||||
node.deviceConfig.ip.netAddr = ack.devAddr.netAddr
|
||||
node.deviceConfig.ip.netBitLen = ack.devAddr.netBitLen.toByte()
|
||||
node.aes.setSecret(aeskey, algorithm, ack.regionId.toLong())
|
||||
node.sessionToken = ack.sessionToken
|
||||
|
||||
node.deviceConfig.ip.netAddr = com.jihe.punchnet.sdlan.utils.ipStringToInt(node.connectData?.ip ?: "0.0.0.0").toUInt().toInt()
|
||||
node.deviceConfig.ip.netBitLen = (node.connectData?.maskLen ?: 24).toByte()
|
||||
|
||||
if (node.iface is IfaceTun) {
|
||||
val tun = node.iface as IfaceTun
|
||||
val maskVal = com.jihe.punchnet.sdlan.network.maskDigitToInt(node.deviceConfig.ip.netBitLen.toInt()) ?: 0xffffff00.toInt()
|
||||
tun.arpTable.routeTable.addRoute(
|
||||
com.jihe.punchnet.sdlan.network.RouteDetail(
|
||||
mask = maskVal,
|
||||
gw = 0,
|
||||
maskedAddr = node.deviceConfig.ip.netAddr and maskVal
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
println("reloading config: node.iface = ${node.iface}")
|
||||
node.iface?.reload_config(node.deviceConfig)
|
||||
println("reloading config ok: node.iface = ${node.iface}")
|
||||
println("reloading config ok: node.udp = ${node.udpSockV4}")
|
||||
|
||||
node.networkID.set(ack.devAddr.networkId)
|
||||
// println("got aes key: ${aeskey.toList()}, length is ${aeskey.size}")
|
||||
node.networkID.set(node.authData?.networkId ?: 0)
|
||||
TerminalLogger.debugf {
|
||||
"control registered: clientIdTail=${node.clientId.takeLast(12)}, network=${node.networkID.get()}, " +
|
||||
"ip=${ipToString(node.deviceConfig.ip.netAddr)}, mask=${node.deviceConfig.ip.netBitLen}, " +
|
||||
"mac=${macToString(node.mac)}, identity=${node.connectData?.identityId ?: 0}"
|
||||
}
|
||||
sendExposedServiceRequest(node)
|
||||
node.sendStunRequest()
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
node.probeNatType()
|
||||
TerminalLogger.debugf { "nat type is ${node.nat_type}"}
|
||||
node.sendStunRequest()
|
||||
TerminalLogger.debugf { "updated NAT type after probe: ${node.nat_type}" }
|
||||
} catch (e: Exception) {
|
||||
Log.e("NAT", "probe nat exited: $e")
|
||||
TerminalLogger.warning { "NAT probe failed: $e" }
|
||||
}
|
||||
}
|
||||
/*
|
||||
CoroutineScope(Dispatchers.Default).async {
|
||||
node.probeNatType()
|
||||
TerminalLogger.debugf { "nat type is ${node.nat_type}"}
|
||||
}
|
||||
*/
|
||||
}
|
||||
PacketType.RegisterSuperNAK -> {
|
||||
val nak = SDLRegisterSuperNak.parseFrom(data.currentPacket)
|
||||
@ -117,7 +159,7 @@ suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
|
||||
TerminalLogger.errorf { "malformed command received"}
|
||||
return
|
||||
}
|
||||
handleTcpCommand(node, data.currentPacket[0], data.currentPacket.sliceArray(1..data.currentPacket.size-1))
|
||||
// handleTcpCommand(node, data.currentPacket[0], data.currentPacket.sliceArray(1..data.currentPacket.size-1))
|
||||
}
|
||||
PacketType.PeerInfo -> {
|
||||
TerminalLogger.debugf { "PeerInfo received" }
|
||||
@ -125,19 +167,40 @@ suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
|
||||
}
|
||||
PacketType.Event -> {
|
||||
TerminalLogger.debugf { "Event received" }
|
||||
if (data.currentPacket.size < 1) {
|
||||
TerminalLogger.errorf { "malformed event received"}
|
||||
return
|
||||
}
|
||||
val eventType = EventType.fromByte(data.currentPacket[0])
|
||||
|
||||
val size = data.currentPacket.size
|
||||
val content = data.currentPacket.sliceArray(1..size-1)
|
||||
handleTcpEvent(node, eventType, content)
|
||||
handleEvent(node, data.currentPacket)
|
||||
}
|
||||
PacketType.Pong -> {
|
||||
TerminalLogger.debugf { "tcp Pong Received" }
|
||||
// node.ping_to_sn()
|
||||
TerminalLogger.debugf { "Pong Received" }
|
||||
}
|
||||
PacketType.ArpResponse -> {
|
||||
TerminalLogger.debugf { "ArpResponse received" }
|
||||
handlePacketArpResponse(node, data.currentPacket)
|
||||
}
|
||||
PacketType.PolicyResponse -> {
|
||||
val policy = SDLPolicyResponse.parseFrom(data.currentPacket)
|
||||
node.queriedPolicies[policy.dstIdentityId] = policy.version
|
||||
node.policyRules[policy.dstIdentityId] = policy.rules.toByteArray()
|
||||
val ruleSummary = describePolicyRules(policy.rules.toByteArray())
|
||||
val targetIp = node.queriedPolicyTargets[policy.dstIdentityId]
|
||||
val targetText = targetIp?.let { ", dstIp=${ipToString(it)}, ${describeResourcesForTarget(node, it)}" } ?: ""
|
||||
if (policy.rules.isEmpty) {
|
||||
TerminalLogger.warning {
|
||||
"PolicyResponse has no allow rules: srcIdentity=${policy.srcIdentityId}, " +
|
||||
"dstIdentity=${policy.dstIdentityId}, version=${policy.version}$targetText"
|
||||
}
|
||||
} else {
|
||||
TerminalLogger.debugf {
|
||||
"PolicyResponse received srcIdentity=${policy.srcIdentityId}, dstIdentity=${policy.dstIdentityId}, " +
|
||||
"version=${policy.version}, rules=$ruleSummary$targetText"
|
||||
}
|
||||
}
|
||||
}
|
||||
PacketType.ExposedServiceResponse -> {
|
||||
val exposed = SDLExposedServiceResponse.parseFrom(data.currentPacket)
|
||||
TerminalLogger.debugf {
|
||||
"ExposedServiceResponse received version=${exposed.version}, " +
|
||||
"tcpPorts=${exposed.tcpPortsList.size}, udpPorts=${exposed.udpPortsList.size}"
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
println("error packet type: ${data.packetType.toUByte()}")
|
||||
@ -145,7 +208,7 @@ suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments, routeinfo: PunchnetServiceArgument?) {
|
||||
suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments, authData: com.jihe.punchnet.api.AuthResponseData, connectData: com.jihe.punchnet.api.ConnectResponseData) {
|
||||
UniqueNodeID.setBaseDir(argument.baseDir)
|
||||
val edgeUUID = UniqueNodeID.getUUID()
|
||||
val config = parseConfig(edgeUUID, argument)
|
||||
@ -156,58 +219,24 @@ suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments,
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val toSocket = Channel<ByteArray>(100)
|
||||
val start_stop_channel = Channel<StartStopChanInfo>(100)
|
||||
initEdge(scope, iface, argument.token, config, toSocket, start_stop_channel)
|
||||
|
||||
val tcp = argument.tcp.split(":")
|
||||
val snParts = argument.sn.split(":")
|
||||
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
|
||||
val node = Node.getInstance()
|
||||
|
||||
val before = ubyteArrayOf(126u,162u,25u,63u,148u,147u,198u,41u,69u,165u,149u,101u,153u,82u,190u,21u,48u,120u,26u,64u,142u,103u,159u,60u,47u,129u,176u,17u,232u,210u,36u,56u)
|
||||
val encrypted = node.rsa.encrypt(before.toByteArray(), use_private_key = false)
|
||||
// println("encrypted: ${encrypted.contentToString()}")
|
||||
val origin = node.rsa.decrypt(encrypted, use_pub_key = false)
|
||||
// TerminalLogger.debugf { "got encrypted ${encrypted.size}"}
|
||||
// TerminalLogger.debugf { "got origin ${origin.contentToString()}"}
|
||||
|
||||
node.authData = authData
|
||||
node.connectData = connectData
|
||||
TerminalLogger.debugf { "self mac: ${macToString(node.mac)}"}
|
||||
TerminalLogger.debugf { describeConnectDataForLog(node) }
|
||||
|
||||
|
||||
val onConnected: suspend (Socket, Int?) -> Unit = { socket, pktID ->
|
||||
println("connected callback")
|
||||
val clientid = UniqueNodeID.getUUID()
|
||||
println("client id is $clientid")
|
||||
|
||||
val registerSuper = SDLRegisterSuper.newBuilder()
|
||||
.setVersion(1)
|
||||
.setToken(node.token)
|
||||
.setInstalledChannel("kotlin")
|
||||
.setClientId(clientid)
|
||||
.setDevAddr(
|
||||
SDLDevAddr.newBuilder().setMac(node.mac)
|
||||
.setNetAddr(0)
|
||||
.setNetBitLen(0)
|
||||
.setNetworkId(0)
|
||||
.build()
|
||||
)
|
||||
.setPubKey(node.rsa.getPublicKeyString())
|
||||
.build()
|
||||
|
||||
val packetID = pktID ?: node.getNextPacketID()
|
||||
|
||||
val msg = encodeToTcpMessage(registerSuper, packetID, PacketType.RegisterSuper)
|
||||
// TerminalLogger.debugf{"sent register super: $registerSuper"}
|
||||
toSocket.send(msg.toByteArray())
|
||||
TerminalLogger.debugf{"sent register super!"}
|
||||
val onConnected: suspend (tech.kwik.core.QuicStream) -> Unit = { stream ->
|
||||
TerminalLogger.debugf { "QUIC stream connected callback" }
|
||||
// We wait for server's PACKET_WELCOME before sending RegisterSuper
|
||||
}
|
||||
/*
|
||||
val onMessage: suspend (SDLanTCP) -> Unit = { data ->
|
||||
}
|
||||
*/
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
@ -219,15 +248,11 @@ suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments,
|
||||
val data = node.iface?.recv()
|
||||
Log.d("SDLAN", "async receive data from iface: ${data?.size} bytes")
|
||||
if (data == null) {
|
||||
//delay(1000)
|
||||
delay(1000)
|
||||
// println("got data is null")
|
||||
continue
|
||||
}
|
||||
if (data.isEmpty()) {
|
||||
delay(1000)
|
||||
// println("got data size 0")
|
||||
// delay(1000)
|
||||
continue
|
||||
}
|
||||
Log.d("SDLAN", "handle data form device starts")
|
||||
@ -241,33 +266,11 @@ suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments,
|
||||
node.udpSockMulticast?.close()
|
||||
}
|
||||
}
|
||||
/*
|
||||
CoroutineScope((Dispatchers.IO)).async {
|
||||
while(true) {
|
||||
val data = node.iface?.recv()
|
||||
Log.d("SDLAN", "async receive data from iface: ${data?.size} bytes")
|
||||
if (data == null) {
|
||||
//delay(1000)
|
||||
println("got data is null")
|
||||
continue
|
||||
}
|
||||
if (data.isEmpty()) {
|
||||
println("got data size 0")
|
||||
// delay(1000)
|
||||
continue
|
||||
}
|
||||
Log.d("SDLAN", "handle data form device starts")
|
||||
node.iface?.handleDataFromDevice(node, data)
|
||||
Log.d("SDLAN", "handle data form device stops")
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// CoroutineScope(Dispatchers.IO).async {
|
||||
scope.launch {
|
||||
initTCPConn(
|
||||
initQUICConn(
|
||||
scope,
|
||||
tcp[0], tcp[1].toInt(),
|
||||
snParts[0], snParts[1].toIntOrNull() ?: 1265,
|
||||
start_stop_channel,
|
||||
AtomicLong(now),
|
||||
AtomicBoolean(false),
|
||||
@ -281,7 +284,6 @@ suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
println("sending start")
|
||||
start_stop_channel.send(StartStopChanInfo(StartStopFlag.IsStart, null))
|
||||
println("sent start")
|
||||
@ -308,6 +310,7 @@ suspend fun loopSocketV4(scope: CoroutineScope, node: Node, sock: SDLanSocket, c
|
||||
try {
|
||||
while(true) {
|
||||
delay(10_000)
|
||||
node.probeNatType()
|
||||
node.sendStunRequest()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@ -387,6 +390,14 @@ suspend fun handleAPacket(scope: CoroutineScope, node: Node, from: SocketAddress
|
||||
TerminalLogger.debugf { "got stun reply" }
|
||||
return
|
||||
}
|
||||
PacketType.PolicyResponse -> {
|
||||
TerminalLogger.debugf { "got policy response" }
|
||||
return
|
||||
}
|
||||
PacketType.ExposedServiceResponse -> {
|
||||
TerminalLogger.debugf { "got exposed service response" }
|
||||
return
|
||||
}
|
||||
PacketType.Register -> {
|
||||
TerminalLogger.debugf { "got REGISTER" }
|
||||
if (from is InetSocketAddress) {
|
||||
@ -447,213 +458,6 @@ fun initEdge(scope: CoroutineScope, iface: Iface, token: String, config: NodeCon
|
||||
println(instance)
|
||||
}
|
||||
|
||||
suspend fun initTCPConn(
|
||||
scope: CoroutineScope,
|
||||
tcpHost: String,
|
||||
tcpPort: Int,
|
||||
start_stop: Channel<StartStopChanInfo>,
|
||||
pong_time: AtomicLong,
|
||||
connected: AtomicBoolean,
|
||||
toSocket: ReceiveChannel<ByteArray>,
|
||||
onConnected: suspend (stream: Socket, pktID: Int?)->Unit,
|
||||
onMessage: suspend (CoroutineScope, SDLanTCP)->Unit,
|
||||
onDisconnected: suspend ()->Unit,
|
||||
connectingChan: SendChannel<ConnectingState>?
|
||||
) {
|
||||
|
||||
val started = AtomicBoolean(false)
|
||||
// var started: Boolean = false
|
||||
var startPktID: Int? = null
|
||||
val node = Node.getInstance()
|
||||
|
||||
while (true) {
|
||||
connectingChan?.send(ConnectingState.NotConnected)
|
||||
|
||||
if (!started.get()) {
|
||||
while (true) {
|
||||
println("waiting for start_stop")
|
||||
val startStopInfo = start_stop.receive()
|
||||
if (startStopInfo.flag == StartStopFlag.IsStart) {
|
||||
started.set(true)
|
||||
// started = true
|
||||
startPktID = startStopInfo.packetID
|
||||
break
|
||||
}
|
||||
TerminalLogger.debugf {
|
||||
"start stop chan received ${startStopInfo}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectingChan?.send(ConnectingState.Connecting)
|
||||
TerminalLogger.debugf { "try connecting tcp..." }
|
||||
|
||||
val socket: Socket
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
TerminalLogger.debugf { "connecting to $tcpHost:$tcpPort" }
|
||||
socket = Socket(tcpHost, tcpPort)
|
||||
}
|
||||
}catch(e: CancellationException) {
|
||||
withContext(NonCancellable) {
|
||||
node.iface?.close()
|
||||
}
|
||||
return
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf { "failed to connect to ${tcpHost}:${tcpPort}: $e" }
|
||||
delay(3000)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
val node = Node.getInstance()
|
||||
val outIP = ByteBuffer.wrap(socket.localAddress.address).getInt()
|
||||
node.outerIPV4.set(outIP)
|
||||
|
||||
// val job_read_packet = CoroutineScope(Dispatchers.IO).async {
|
||||
val job_read_packet = scope.async {
|
||||
val input = DataInputStream(socket.getInputStream())
|
||||
try {
|
||||
println("job read packet starts")
|
||||
while (true) {
|
||||
val tcpPacket = readPacket(input)
|
||||
if (tcpPacket == null) {
|
||||
TerminalLogger.errorf { "tcp Packet is null" }
|
||||
break
|
||||
}
|
||||
onMessage(scope, tcpPacket)
|
||||
}
|
||||
} finally {
|
||||
input.close()
|
||||
TerminalLogger.errorf { "input closing" }
|
||||
}
|
||||
}
|
||||
|
||||
// val job_write_to_packet = CoroutineScope(Dispatchers.IO).async {
|
||||
val job_write_to_packet = scope.async {
|
||||
val output = DataOutputStream(socket.getOutputStream())
|
||||
try {
|
||||
TerminalLogger.debugf { "job write to packet starts" }
|
||||
while (true) {
|
||||
try {
|
||||
val msg = toSocket.receive()
|
||||
TerminalLogger.debugf { "received message" }
|
||||
output.write(msg)
|
||||
} catch (e: Exception) {
|
||||
output.close()
|
||||
TerminalLogger.errorf { "failed to receive message to tcp: $e" }
|
||||
break
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
output.close()
|
||||
}
|
||||
}
|
||||
|
||||
// val job_check_pong = CoroutineScope(Dispatchers.IO).async {
|
||||
val job_check_pong = scope.async {
|
||||
println("job check pong starts")
|
||||
while (true) {
|
||||
delay(10_000)
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
if (connected.get() && (now - pong_time.get() > SDLanConfig.TCP_PING_TIME * 2)) {
|
||||
TerminalLogger.errorf { "tcp pong check expired" }
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// val job_check_stop = CoroutineScope(Dispatchers.IO).async {
|
||||
val job_check_stop = scope.async {
|
||||
println("job check stop starts")
|
||||
while (true) {
|
||||
try {
|
||||
val v = start_stop.receive()
|
||||
if (v.flag == StartStopFlag.IsStop) {
|
||||
started.set(false)
|
||||
break
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// started.set(false)
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
TerminalLogger.debugf { "connected" }
|
||||
onConnected(socket, startPktID)
|
||||
connectingChan?.send(ConnectingState.Connected)
|
||||
|
||||
var cancelled: Boolean = false
|
||||
select<Unit> {
|
||||
job_read_packet.onAwait() {
|
||||
println("job read packet exited")
|
||||
}
|
||||
job_write_to_packet.onAwait() {
|
||||
println("job write to packet exited")
|
||||
}
|
||||
job_check_pong.onAwait() {
|
||||
println("job check pong exited")
|
||||
}
|
||||
job_check_stop.onAwait() {
|
||||
println("job check stop exited")
|
||||
}
|
||||
}
|
||||
|
||||
println("m1")
|
||||
job_read_packet.cancelAndJoin()
|
||||
println("m2")
|
||||
job_write_to_packet.cancelAndJoin()
|
||||
println("m3")
|
||||
job_check_pong.cancelAndJoin()
|
||||
println("m4")
|
||||
job_check_stop.cancelAndJoin()
|
||||
println("m5")
|
||||
|
||||
delay(1000)
|
||||
} catch (e: Exception) {
|
||||
withContext(NonCancellable) {
|
||||
node.iface?.close()
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun readPacket(input: DataInputStream): SDLanTCP? {
|
||||
try {
|
||||
val size = input.readShort()
|
||||
val packetID = input.readInt().toUInt()
|
||||
val packetTypeUByte = input.readByte().toUByte()
|
||||
val packetType = PacketType.fromValue(packetTypeUByte)
|
||||
if (packetType == null) {
|
||||
TerminalLogger.errorf{"packet type not found: $packetTypeUByte"}
|
||||
return null
|
||||
}
|
||||
|
||||
if (size < 5) {
|
||||
TerminalLogger.errorf {"got input stream size error: ${size}"}
|
||||
return null
|
||||
}
|
||||
|
||||
var buffsize = size - 5
|
||||
val data = ByteArray(buffsize)
|
||||
var toread = buffsize
|
||||
while(toread > 0) {
|
||||
val sizeGot = input.read(data, (buffsize-toread), toread)
|
||||
if (sizeGot <= 0) {
|
||||
TerminalLogger.errorf {"failed to read further: got ${sizeGot}"}
|
||||
return null
|
||||
}
|
||||
toread -= sizeGot
|
||||
}
|
||||
return SDLanTCP(packetID, packetType, data)
|
||||
} catch (e: Exception) {
|
||||
TerminalLogger.errorf { "failed to read: $e" }
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun parseConfig(nodeuuid: String, argument: Arguments): NodeConfig? {
|
||||
if (argument.sn.length == 0) {
|
||||
@ -661,18 +465,23 @@ fun parseConfig(nodeuuid: String, argument: Arguments): NodeConfig? {
|
||||
return null
|
||||
}
|
||||
|
||||
val natServer1 = parseScoketAddressV4FromString(argument.nat_server1)
|
||||
val natServer2 = parseScoketAddressV4FromString(argument.nat_server2)
|
||||
|
||||
if (natServer1 == null || natServer2 == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val sn = parseScoketAddressV4FromString(argument.sn)
|
||||
if (sn == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val stun = parseScoketAddressV4FromString(argument.stun)
|
||||
if (stun == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val natServer1 = parseScoketAddressV4FromString(argument.nat_server1)
|
||||
val natServer2 = parseScoketAddressV4FromString(argument.nat_server2)
|
||||
if (natServer1 == null || natServer2 == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return NodeConfig(
|
||||
baseDir = argument.baseDir,
|
||||
name = argument.name,
|
||||
@ -686,7 +495,8 @@ fun parseConfig(nodeuuid: String, argument: Arguments): NodeConfig? {
|
||||
registerTTL = argument.registerTTL,
|
||||
localPort = argument.localPort,
|
||||
supernode = SDLanSock(IPFamily.IPV4, sn.port, sn.address.address),
|
||||
stunServer = SDLanSock(IPFamily.IPV4, stun.port, stun.address.address),
|
||||
natServer1 = natServer1,
|
||||
natServer2 = natServer2,
|
||||
natServer2 = natServer2
|
||||
)
|
||||
}
|
||||
@ -49,6 +49,15 @@ enum class PacketType(val id: UByte) {
|
||||
StunProbe(0x32u),
|
||||
StunProbeReply(0x33u),
|
||||
|
||||
Welcome(0x4fu),
|
||||
ArpRequest(0x50u),
|
||||
ArpResponse(0x51u),
|
||||
|
||||
PolicyRequest(0xb0u),
|
||||
PolicyResponse(0xb1u),
|
||||
ExposedServiceRequest(0xb2u),
|
||||
ExposedServiceResponse(0xb3u),
|
||||
|
||||
Data(0xffu);
|
||||
|
||||
companion object {
|
||||
@ -65,9 +74,8 @@ fun PacketType.toUByte(): UByte {
|
||||
}
|
||||
|
||||
|
||||
// tcp发送过来的通道里面的信息
|
||||
class SDLanTCP(
|
||||
val packetID: UInt,
|
||||
// tcp/quic发送过来的通道里面的信息
|
||||
class SDLanControl(
|
||||
val packetType: PacketType,
|
||||
val currentPacket: ByteArray,
|
||||
)
|
||||
@ -103,18 +111,13 @@ fun encodeToUDPMessage(msg: Message?, packetType: PacketType): List<Byte> {
|
||||
return result
|
||||
}
|
||||
|
||||
fun encodeToTcpMessage(msg: Message?, packetID: Int, packetType: PacketType): List<Byte> {
|
||||
fun encodeToControlMessage(msg: Message?, packetType: PacketType): List<Byte> {
|
||||
val msgByte = msg?.toByteArray()?.toList()?:listOf<Byte>()
|
||||
val result: MutableList<Byte> = mutableListOf()
|
||||
|
||||
result.addAll(ByteBuffer.allocate(2)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
.putShort((msgByte.size + 5).toShort())
|
||||
.array().toList())
|
||||
|
||||
result.addAll(ByteBuffer.allocate(4)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
.putInt(packetID)
|
||||
.putShort((msgByte.size + 1).toShort())
|
||||
.array().toList())
|
||||
|
||||
result.add(packetType.toUByte().toByte())
|
||||
|
||||
@ -17,6 +17,8 @@ import kotlin.experimental.inv
|
||||
import kotlin.experimental.or
|
||||
import kotlin.random.Random
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object UniqueNodeID {
|
||||
var id: String = ""
|
||||
private var baseDir: String = ""
|
||||
@ -25,11 +27,18 @@ object UniqueNodeID {
|
||||
baseDir = basedir
|
||||
}
|
||||
|
||||
fun getUUID(): String {
|
||||
if (baseDir.length == 0) {
|
||||
baseDir = Environment.getExternalStorageDirectory().name
|
||||
fun getUUID(context: Context? = null): String {
|
||||
if (id.isEmpty() && context != null) {
|
||||
val androidId = android.provider.Settings.Secure.getString(context.contentResolver, android.provider.Settings.Secure.ANDROID_ID)
|
||||
if (!androidId.isNullOrEmpty() && androidId != "9774d56d682e549c") { // 9774d56d682e549c is a known bugged ID on old emulators
|
||||
id = androidId
|
||||
return id
|
||||
}
|
||||
}
|
||||
if (id.isEmpty()) {
|
||||
if (baseDir.isEmpty()) {
|
||||
baseDir = Environment.getExternalStorageDirectory().absolutePath
|
||||
}
|
||||
if (id.length == 0) {
|
||||
val dirpath = File(baseDir)
|
||||
dirpath.mkdirs()
|
||||
val f = File(baseDir, SDLanConfig.ID_FILENAME)
|
||||
@ -71,6 +80,15 @@ fun ipToString(ip: Int): String {
|
||||
return "$d1.$d2.$d3.$d4"
|
||||
}
|
||||
|
||||
fun ipStringToInt(ipString: String): Int {
|
||||
val parts = ipString.split(".")
|
||||
if (parts.size != 4) return 0
|
||||
return (parts[0].toInt() shl 24) or
|
||||
(parts[1].toInt() shl 16) or
|
||||
(parts[2].toInt() shl 8) or
|
||||
(parts[3].toInt())
|
||||
}
|
||||
|
||||
fun macToString(mac: Mac): String {
|
||||
return mac.joinToString(separator = ":") { it.toUByte().toString(16) }
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ syntax = "proto3";
|
||||
|
||||
option java_package = "com.jihe.punchnet.protobuf";
|
||||
option java_outer_classname = "PunchProto";
|
||||
// 基础公共类型定义
|
||||
|
||||
// 基础公共类型定义
|
||||
message SDLV4Info {
|
||||
uint32 port = 1;
|
||||
bytes v4 = 2;
|
||||
@ -15,34 +15,30 @@ message SDLV6Info {
|
||||
bytes v6 = 2;
|
||||
}
|
||||
|
||||
// 设备网络地址信息
|
||||
message SDLDevAddr {
|
||||
uint32 network_id = 1;
|
||||
bytes mac = 2;
|
||||
uint32 net_addr = 3;
|
||||
uint32 net_bit_len = 4;
|
||||
}
|
||||
|
||||
// tcp通讯消息
|
||||
message SDLEmpty {
|
||||
|
||||
message SDLWelcome {
|
||||
uint32 version = 1;
|
||||
uint32 max_bidi_streams = 2;
|
||||
uint32 max_packet_size = 3;
|
||||
uint32 heartbeat_sec = 4;
|
||||
SDLV6Info ipv6_assist = 5;
|
||||
}
|
||||
|
||||
message SDLRegisterSuper {
|
||||
uint32 version = 1;
|
||||
string installed_channel = 2;
|
||||
string client_id = 3;
|
||||
SDLDevAddr dev_addr = 4;
|
||||
string pub_key = 5;
|
||||
string token = 6;
|
||||
string client_id = 1;
|
||||
uint32 network_id = 2;
|
||||
bytes mac = 3;
|
||||
uint32 ip = 4;
|
||||
uint32 mask_len = 5;
|
||||
string hostname = 6;
|
||||
string pub_key = 7;
|
||||
string access_token = 8;
|
||||
}
|
||||
|
||||
message SDLRegisterSuperAck {
|
||||
SDLDevAddr dev_addr = 1;
|
||||
bytes aes_key = 2;
|
||||
uint32 upgrade_type = 3;
|
||||
optional string upgrade_prompt = 4;
|
||||
optional string upgrade_address = 5;
|
||||
string algorithm = 1;
|
||||
bytes key = 2;
|
||||
uint32 region_id = 3;
|
||||
bytes session_token = 4;
|
||||
}
|
||||
|
||||
message SDLRegisterSuperNak {
|
||||
@ -51,72 +47,115 @@ message SDLRegisterSuperNak {
|
||||
}
|
||||
|
||||
// 网络地址查询
|
||||
|
||||
message SDLQueryInfo {
|
||||
bytes dst_mac = 1;
|
||||
}
|
||||
|
||||
message SDLPeerInfo {
|
||||
bytes dst_mac = 1;
|
||||
SDLV4Info v4_info = 2;
|
||||
optional SDLV4Info v4_info = 2;
|
||||
optional SDLV6Info v6_info = 3;
|
||||
}
|
||||
|
||||
// 事件定义
|
||||
|
||||
message SDLNatChangedEvent {
|
||||
bytes mac = 1;
|
||||
uint32 ip = 2;
|
||||
message SDLArpRequest {
|
||||
uint32 target_ip = 1;
|
||||
uint32 origin_ip = 2;
|
||||
bytes context = 3;
|
||||
}
|
||||
|
||||
message SDLSendRegisterEvent {
|
||||
message SDLArpResponse {
|
||||
uint32 target_ip = 1;
|
||||
bytes target_mac = 2;
|
||||
uint32 origin_ip = 3;
|
||||
bytes context = 4;
|
||||
}
|
||||
|
||||
message SDLPolicyRequest {
|
||||
uint32 src_identity_id = 1;
|
||||
uint32 dst_identity_id = 2;
|
||||
uint32 version = 3;
|
||||
}
|
||||
|
||||
message SDLPolicyResponse {
|
||||
uint32 src_identity_id = 1;
|
||||
uint32 dst_identity_id = 2;
|
||||
uint32 version = 3;
|
||||
bytes rules = 4;
|
||||
}
|
||||
|
||||
message SDLExposedServiceRequest {
|
||||
uint32 version = 1;
|
||||
}
|
||||
|
||||
message SDLExposedServiceResponse {
|
||||
uint32 version = 1;
|
||||
repeated uint32 tcp_ports = 2;
|
||||
repeated uint32 udp_ports = 3;
|
||||
}
|
||||
|
||||
// 事件定义
|
||||
message SDLEvent {
|
||||
message NatChanged {
|
||||
bytes mac = 1;
|
||||
uint32 ip = 2;
|
||||
}
|
||||
|
||||
message SendRegister {
|
||||
bytes dst_mac = 1;
|
||||
uint32 nat_ip = 2;
|
||||
uint32 nat_port = 3;
|
||||
uint32 nat_type = 4;
|
||||
optional SDLV6Info v6_info = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message SDLNetworkShutdownEvent {
|
||||
message ExposedServiceChanged {
|
||||
}
|
||||
|
||||
message NetworkShutdown {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
oneof event {
|
||||
NatChanged nat_changed = 1;
|
||||
SendRegister send_register = 2;
|
||||
NetworkShutdown shutdown = 3;
|
||||
ExposedServiceChanged exposed_service_changed = 4;
|
||||
}
|
||||
}
|
||||
|
||||
// 命令定义
|
||||
message SDLCommand {
|
||||
uint32 pkt_id = 1;
|
||||
|
||||
message SDLChangeNetworkCommand {
|
||||
SDLDevAddr dev_addr = 1;
|
||||
bytes aes_key = 2;
|
||||
message ExitNodeControl {
|
||||
int32 action = 1;
|
||||
string remark = 2;
|
||||
}
|
||||
|
||||
oneof command {
|
||||
ExitNodeControl exit_node = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message SDLCommandAck {
|
||||
// status = true, 表示成功;status = false 表示失败,message是失败原因描述
|
||||
bool status = 1;
|
||||
optional string message = 2;
|
||||
}
|
||||
|
||||
message SDLFlows {
|
||||
// 服务器转发流量
|
||||
uint32 forward_num = 1;
|
||||
// p2p直接流量
|
||||
uint32 p2p_num = 2;
|
||||
// 接收的流量
|
||||
uint32 inbound_num = 3;
|
||||
uint32 pkt_id = 1;
|
||||
int32 code = 2;
|
||||
string message = 3;
|
||||
bytes data = 4;
|
||||
}
|
||||
|
||||
// UDP通讯消息
|
||||
|
||||
message SDLStunRequest {
|
||||
uint32 cookie = 1;
|
||||
string client_id = 2;
|
||||
uint32 network_id = 3;
|
||||
bytes mac = 4;
|
||||
uint32 ip = 5;
|
||||
uint32 nat_type = 6;
|
||||
optional SDLV6Info v6_info = 7;
|
||||
string client_id = 1;
|
||||
uint32 network_id = 2;
|
||||
bytes mac = 3;
|
||||
uint32 ip = 4;
|
||||
uint32 nat_type = 5;
|
||||
optional SDLV6Info v6_info = 6;
|
||||
bytes session_token = 7;
|
||||
}
|
||||
|
||||
message SDLStunReply {
|
||||
uint32 cookie = 1;
|
||||
}
|
||||
|
||||
message SDLData {
|
||||
@ -126,6 +165,8 @@ message SDLData {
|
||||
bool is_p2p = 4;
|
||||
uint32 ttl = 5;
|
||||
bytes data = 6;
|
||||
bytes session_token = 7;
|
||||
uint32 identity_id = 8;
|
||||
}
|
||||
|
||||
message SDLRegister {
|
||||
@ -141,14 +182,15 @@ message SDLRegisterAck {
|
||||
}
|
||||
|
||||
// 网络类型探测
|
||||
|
||||
message SDLStunProbe {
|
||||
uint32 cookie = 1;
|
||||
uint32 attr = 2;
|
||||
uint32 step = 3;
|
||||
}
|
||||
|
||||
message SDLStunProbeReply {
|
||||
uint32 cookie = 1;
|
||||
uint32 port = 2;
|
||||
uint32 ip = 3;
|
||||
uint32 step = 2;
|
||||
uint32 port = 3;
|
||||
uint32 ip = 4;
|
||||
}
|
||||
@ -21,3 +21,4 @@ kotlin.code.style=official
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
org.gradle.java.home=/Users/stavid/Library/Java/JavaVirtualMachines/corretto-17.0.17/Contents/Home
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user