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>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<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>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</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")
|
ksp("androidx.room:room-compiler:2.7.2")
|
||||||
implementation("androidx.room:room-ktx: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(libs.bcpkix.jdk18on)
|
||||||
// implementation(files("libs/org.asxalex.sdlan/sdlan-1.0-SNAPSHOT.jar"))
|
// 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(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.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
<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
|
<application
|
||||||
@ -16,6 +18,7 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Punchnet"
|
android:theme="@style/Theme.Punchnet"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
@ -30,7 +33,7 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service android:name=".PunchnetService"
|
<service android:name=".PunchnetService"
|
||||||
android:exported="false"
|
android:exported="true"
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||||
|
|
||||||
|
|||||||
@ -114,6 +114,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Initialize AppAuthRepository for Split Tunneling
|
||||||
|
com.jihe.punchnet.data.AppAuthRepository.init(this)
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
Log.d("DIR", "filesdir = ${this.filesDir}")
|
Log.d("DIR", "filesdir = ${this.filesDir}")
|
||||||
|
|||||||
@ -12,12 +12,9 @@ import android.os.ParcelFileDescriptor
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import com.jihe.punchnet.data.ButtonRepository
|
import com.jihe.punchnet.data.ButtonRepository
|
||||||
import com.jihe.punchnet.data.ButtonState
|
import com.jihe.punchnet.data.ButtonState
|
||||||
import com.jihe.punchnet.data.ButtonViewModel
|
|
||||||
import com.jihe.punchnet.data.RouteItem
|
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.Arguments
|
||||||
import com.jihe.punchnet.sdlan.config.toIPV4String
|
import com.jihe.punchnet.sdlan.config.toIPV4String
|
||||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
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.DeviceConfig
|
||||||
import com.jihe.punchnet.sdlan.network.IfaceTun
|
import com.jihe.punchnet.sdlan.network.IfaceTun
|
||||||
import com.jihe.punchnet.sdlan.network.RouteDetail
|
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.ipInt2ByteArray
|
||||||
import com.jihe.punchnet.sdlan.network.maskIPToDigit
|
import com.jihe.punchnet.sdlan.network.maskIPToDigit
|
||||||
import com.jihe.punchnet.sdlan.network.run_sdlan
|
import com.jihe.punchnet.sdlan.network.run_sdlan
|
||||||
import com.jihe.punchnet.sdlan.utils.ipToString
|
import com.jihe.punchnet.sdlan.utils.ipToString
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
class PunchnetService : VpnService() , IfaceTun {
|
class PunchnetService : VpnService() , IfaceTun {
|
||||||
|
|
||||||
@ -79,7 +70,6 @@ class PunchnetService : VpnService() , IfaceTun {
|
|||||||
try {
|
try {
|
||||||
var size = input?.read(result)
|
var size = input?.read(result)
|
||||||
if (size == null) {
|
if (size == null) {
|
||||||
println("xxx failed to read")
|
|
||||||
size = 0
|
size = 0
|
||||||
} else {
|
} else {
|
||||||
println("xxx got $size bytes")
|
println("xxx got $size bytes")
|
||||||
@ -90,6 +80,8 @@ class PunchnetService : VpnService() , IfaceTun {
|
|||||||
result.copyOf(size)
|
result.copyOf(size)
|
||||||
// result.slice(0..<size).toByteArray()
|
// result.slice(0..<size).toByteArray()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
println("xxx read exception: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
ByteArray(0)
|
ByteArray(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,10 +118,45 @@ class PunchnetService : VpnService() , IfaceTun {
|
|||||||
.addAddress(ip, config.ip.netBitLen.toInt())
|
.addAddress(ip, config.ip.netBitLen.toInt())
|
||||||
.setBlocking(true)
|
.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) {
|
for (route in routes) {
|
||||||
tempVpnDescriptor = tempVpnDescriptor.addRoute(ipToString(route.net_ip), maskIPToDigit(route.mask_ip))
|
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
|
vpnDescriptor = tempVpnDescriptor
|
||||||
.establish()?.apply {
|
.establish()?.apply {
|
||||||
input = FileInputStream(fileDescriptor)
|
input = FileInputStream(fileDescriptor)
|
||||||
@ -198,11 +225,10 @@ class PunchnetService : VpnService() , IfaceTun {
|
|||||||
|
|
||||||
val argument = Arguments(
|
val argument = Arguments(
|
||||||
baseDir = this.filesDir.path,
|
baseDir = this.filesDir.path,
|
||||||
sn = "$server:1265",
|
sn = "$server:443",
|
||||||
tcp = "$server:18083",
|
stun = "$server:1365",
|
||||||
|
nat_server1 = "$server:1365",
|
||||||
nat_server1 = "$server:1265",
|
nat_server2 = "$server:1366",
|
||||||
nat_server2 = "47.98.178.3:1265",
|
|
||||||
token = startArg?.token?:"",
|
token = startArg?.token?:"",
|
||||||
name = "tau",
|
name = "tau",
|
||||||
)
|
)
|
||||||
@ -210,8 +236,43 @@ class PunchnetService : VpnService() , IfaceTun {
|
|||||||
val notification = createNotification()
|
val notification = createNotification()
|
||||||
startForeground(1, notification)
|
startForeground(1, notification)
|
||||||
|
|
||||||
|
// Initialize UniqueNodeID baseDir to app's files directory
|
||||||
|
com.jihe.punchnet.sdlan.utils.UniqueNodeID.setBaseDir(this.filesDir.absolutePath)
|
||||||
|
|
||||||
scope.launch {
|
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)
|
// isRunning.set(true)
|
||||||
@ -236,7 +297,7 @@ class PunchnetService : VpnService() , IfaceTun {
|
|||||||
} else {
|
} else {
|
||||||
intent?.getParcelableExtra("argument")
|
intent?.getParcelableExtra("argument")
|
||||||
}
|
}
|
||||||
println("argument = ${argument}")
|
TerminalLogger.debugf { "service argument received: routes=${argument?.routes?.size ?: 0}, hasToken=${!argument?.token.isNullOrEmpty()}" }
|
||||||
connect(argument)
|
connect(argument)
|
||||||
START_STICKY
|
START_STICKY
|
||||||
}
|
}
|
||||||
@ -265,6 +326,7 @@ class PunchnetService : VpnService() , IfaceTun {
|
|||||||
// disconnect()
|
// disconnect()
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
disconnect()
|
disconnect()
|
||||||
|
com.jihe.punchnet.data.NodeRepository.clear()
|
||||||
Log.e(TAG, "onDestroy is called")
|
Log.e(TAG, "onDestroy is called")
|
||||||
// stopForeground(STOP_FOREGROUND_REMOVE)
|
// stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
super.onDestroy()
|
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,
|
context.applicationContext,
|
||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
"app_database"
|
"app_database"
|
||||||
).build()
|
)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
INSTANCE = instance
|
INSTANCE = instance
|
||||||
instance
|
instance
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.jihe.punchnet.helper
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import com.jihe.punchnet.data.RouteItem
|
import com.jihe.punchnet.data.RouteItem
|
||||||
import com.jihe.punchnet.sdlan.network.maskDigitToInt
|
import com.jihe.punchnet.sdlan.network.maskDigitToInt
|
||||||
|
|
||||||
@ -10,9 +11,41 @@ sealed class PreferenceName(val name: String) {
|
|||||||
object PreferenceToken: PreferenceName("token")
|
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? {
|
fun getPreferenceString(context: Context, name: PreferenceName): String? {
|
||||||
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
|
|
||||||
try {
|
try {
|
||||||
|
val preference = getEncryptedSharedPrefs(context)
|
||||||
|
migratePlainPreference(context, name, preference)
|
||||||
return preference.getString(name.name, "")
|
return preference.getString(name.name, "")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
@ -20,8 +53,9 @@ fun getPreferenceString(context: Context, name: PreferenceName): String? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getPreferenceInt(context: Context, name: PreferenceName): Int? {
|
fun getPreferenceInt(context: Context, name: PreferenceName): Int? {
|
||||||
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
|
|
||||||
try {
|
try {
|
||||||
|
val preference = getEncryptedSharedPrefs(context)
|
||||||
|
migratePlainPreference(context, name, preference)
|
||||||
return preference.getInt(name.name, 0)
|
return preference.getInt(name.name, 0)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
@ -29,17 +63,21 @@ fun getPreferenceInt(context: Context, name: PreferenceName): Int? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setPreferenceString(context: Context, name: PreferenceName, value: String) {
|
fun setPreferenceString(context: Context, name: PreferenceName, value: String) {
|
||||||
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
|
try {
|
||||||
val editor = preference.edit()
|
val preference = getEncryptedSharedPrefs(context)
|
||||||
editor.putString(name.name, value)
|
preference.edit().putString(name.name, value).apply()
|
||||||
editor.apply()
|
} catch (e: Exception) {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPreferenceInt(context: Context, name: PreferenceName, value: Int) {
|
fun setPreferenceInt(context: Context, name: PreferenceName, value: Int) {
|
||||||
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
|
try {
|
||||||
val editor = preference.edit()
|
val preference = getEncryptedSharedPrefs(context)
|
||||||
editor.putInt(name.name, value)
|
preference.edit().putInt(name.name, value).apply()
|
||||||
editor.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.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@ -50,33 +51,45 @@ fun AppNavHost2(
|
|||||||
paddingValues: PaddingValues
|
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(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Screen.MainScreen.route,
|
startDestination = startDest,
|
||||||
modifier = Modifier.padding(paddingValues)
|
modifier = Modifier.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
|
composable(Screen.LoginScreen.route) {
|
||||||
|
LoginScreen(navController)
|
||||||
|
}
|
||||||
composable(Screen.MainScreen.route) {
|
composable(Screen.MainScreen.route) {
|
||||||
HomeScreen(buttonViewModel, serviceViewModel, routeViewModel)
|
HomeScreen(buttonViewModel, serviceViewModel, routeViewModel, navController)
|
||||||
}
|
}
|
||||||
composable(Screen.RouteScreen.route) {
|
composable(Screen.DeviceScreen.route) {
|
||||||
CustomHeaderScreen(
|
CustomHeaderScreen("设备") {
|
||||||
"路由设置"
|
DeviceScreen(navController)
|
||||||
) {
|
|
||||||
RouteScreen(ButtonRepository.buttonState.collectAsState().value != ButtonState.ButtonStarted, routeViewModel)
|
|
||||||
// LightScreen()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(Screen.ProfileScreen.route) {
|
composable(Screen.AppAuthScreen.route) {
|
||||||
Column(
|
CustomHeaderScreen("应用授权") {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
AppAuthScreen(navController)
|
||||||
modifier = Modifier.fillMaxWidth()
|
}
|
||||||
) {
|
}
|
||||||
Text(
|
composable(Screen.SettingsScreen.route) {
|
||||||
"TODO",
|
CustomHeaderScreen("设置") {
|
||||||
style = MaterialTheme.typography.titleLarge,
|
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(
|
Icon(
|
||||||
imageVector = when (screen) {
|
imageVector = when (screen) {
|
||||||
Screen.MainScreen -> Icons.Default.Home
|
Screen.MainScreen -> Icons.Default.Home
|
||||||
Screen.RouteScreen -> Icons.Default.Menu
|
Screen.DeviceScreen -> Icons.Default.Menu
|
||||||
// Screen.Search -> Icons.Default.Search
|
Screen.AppAuthScreen -> Icons.Default.Person
|
||||||
Screen.ProfileScreen -> Icons.Default.Person
|
else -> Icons.Default.Home
|
||||||
},
|
},
|
||||||
contentDescription = screen.route
|
contentDescription = screen.route
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
label = {
|
label = {
|
||||||
Text(
|
val labelText = when (screen) {
|
||||||
text = screen.route.replaceFirstChar { it.uppercase() },
|
Screen.MainScreen -> "首页"
|
||||||
)
|
Screen.DeviceScreen -> "设备"
|
||||||
|
Screen.AppAuthScreen -> "应用授权"
|
||||||
|
else -> screen.route
|
||||||
|
}
|
||||||
|
Text(text = labelText)
|
||||||
},
|
},
|
||||||
selected = currentRoute == screen.route,
|
selected = currentRoute == screen.route,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|||||||
@ -6,12 +6,14 @@ import android.app.Activity.RESULT_OK
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.indication
|
import androidx.compose.foundation.indication
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
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.getPreferenceString
|
||||||
import com.jihe.punchnet.helper.setPreferenceString
|
import com.jihe.punchnet.helper.setPreferenceString
|
||||||
import kotlin.math.exp
|
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(
|
data class HomeDropDownInfo(
|
||||||
@DrawableRes val icon: Int,
|
@DrawableRes val icon: Int,
|
||||||
@ -72,32 +80,30 @@ data class HomeDropDownInfo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeDropdownMenu(
|
fun HomeDropdownMenu(navController: androidx.navigation.NavHostController) {
|
||||||
showTokenDialog: MutableState<Boolean>,
|
|
||||||
) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
// var showTokenDialog by remember { mutableStateOf(false) }
|
|
||||||
// val showSettingDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val homeItems = arrayOf(
|
val homeItems = arrayOf(
|
||||||
HomeDropDownInfo(
|
HomeDropDownInfo(
|
||||||
R.drawable.tag,
|
R.drawable.tag, // TODO: Use settings icon if available, or just standard icon
|
||||||
"修改token",
|
"设置",
|
||||||
{
|
{
|
||||||
showTokenDialog.value = true
|
navController.navigate(Screen.SettingsScreen.route)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
/*
|
|
||||||
HomeDropDownInfo(
|
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() {
|
Box() {
|
||||||
IconButton(
|
IconButton(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
@ -125,7 +131,6 @@ fun HomeDropdownMenu(
|
|||||||
item.callback?.invoke()
|
item.callback?.invoke()
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
|
|
||||||
Row (
|
Row (
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
){
|
){
|
||||||
@ -142,12 +147,10 @@ fun HomeDropdownMenu(
|
|||||||
|
|
||||||
Text(text=item.name)
|
Text(text=item.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,15 +159,13 @@ fun HomeScreen(
|
|||||||
buttonViewModel: ButtonViewModel,
|
buttonViewModel: ButtonViewModel,
|
||||||
serviceViewModel: ServiceViewModel,
|
serviceViewModel: ServiceViewModel,
|
||||||
routeViewModel: RouteViewModel,
|
routeViewModel: RouteViewModel,
|
||||||
// started: MutableState<Boolean>,
|
navController: androidx.navigation.NavHostController,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val tkPref = getPreferenceString(context, PreferenceName.PreferenceToken)
|
val tkPref = getPreferenceString(context, PreferenceName.PreferenceToken)
|
||||||
|
|
||||||
var tokenvalue by remember {mutableStateOf(tkPref?:"")}
|
|
||||||
|
|
||||||
val allroutes = routeViewModel.allRoutes.collectAsState(emptyList())
|
val allroutes = routeViewModel.allRoutes.collectAsState(emptyList())
|
||||||
val vpnLauncher = rememberLauncherForActivityResult(
|
val vpnLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
@ -174,7 +175,6 @@ fun HomeScreen(
|
|||||||
// serviceViewModel.startService(context)
|
// serviceViewModel.startService(context)
|
||||||
// serviceViewModel.setVPNPermission(true)
|
// serviceViewModel.setVPNPermission(true)
|
||||||
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
|
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
|
||||||
println("now token value2 = '${tokenvalue}'")
|
|
||||||
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
|
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
|
||||||
}
|
}
|
||||||
RESULT_CANCELED -> {
|
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 buttonState = buttonViewModel.buttonState.collectAsState()
|
||||||
val showTokenDialog = remember { mutableStateOf(false) }
|
val resourceList by com.jihe.punchnet.data.NodeRepository.resources.collectAsState()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CustomHeaderScreen(
|
CustomHeaderScreen(
|
||||||
"",
|
"",
|
||||||
onBack = null,
|
onBack = null,
|
||||||
onMenu = {
|
onMenu = {
|
||||||
HomeDropdownMenu(showTokenDialog)
|
HomeDropdownMenu(navController)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if (showTokenDialog.value) {
|
LazyColumn(
|
||||||
AlertDialog(
|
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
|
||||||
onDismissRequest = {showTokenDialog.value=false},
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
confirmButton = {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
setPreferenceString(context, PreferenceName.PreferenceToken, tokenvalue)
|
|
||||||
println("now token value = '${tokenvalue}'")
|
|
||||||
showTokenDialog.value = false
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Text("确定")
|
item {
|
||||||
}
|
Spacer(modifier = Modifier.height(50.dp))
|
||||||
},
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
.padding(top=50.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
|
|
||||||
) {
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.punchnet_log),
|
painter = painterResource(R.drawable.punchnet_log),
|
||||||
contentDescription = "logo",
|
contentDescription = "logo",
|
||||||
@ -260,20 +238,20 @@ fun HomeScreen(
|
|||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (buttonViewModel.buttonState.value == ButtonState.ButtonStarted) {
|
if (buttonViewModel.buttonState.value == ButtonState.ButtonStarted) {
|
||||||
// if (serviceViewModel.isRunning.value) {
|
|
||||||
// if is running, should stop service
|
|
||||||
serviceViewModel.stopVpnService(context)
|
serviceViewModel.stopVpnService(context)
|
||||||
} else if (buttonViewModel.buttonState.value == ButtonState.ButtonStopped){
|
} 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) {
|
if (intent != null) {
|
||||||
vpnLauncher.launch(intent)
|
vpnLauncher.launch(intent)
|
||||||
} else {
|
} else {
|
||||||
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
|
val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
|
||||||
println("now token value1 = '${tokenvalue}'")
|
|
||||||
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
|
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// started.value = !started.value
|
}
|
||||||
},
|
},
|
||||||
enabled = buttonState.value.enabled,
|
enabled = buttonState.value.enabled,
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
@ -281,20 +259,111 @@ fun HomeScreen(
|
|||||||
.width(120.dp)
|
.width(120.dp)
|
||||||
.height(40.dp)
|
.height(40.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(buttonState.value.text)
|
||||||
buttonState.value.text
|
|
||||||
// buttonViewModel.buttonText.value
|
|
||||||
/*
|
|
||||||
if (serviceViewModel.isRunning.value) {
|
|
||||||
"停止"
|
|
||||||
} else {
|
|
||||||
"启动"
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
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.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -25,15 +29,20 @@ import com.jihe.punchnet.data.RouteViewModel
|
|||||||
import com.jihe.punchnet.data.ServiceViewModel
|
import com.jihe.punchnet.data.ServiceViewModel
|
||||||
|
|
||||||
sealed class Screen(val route: String) {
|
sealed class Screen(val route: String) {
|
||||||
object MainScreen: Screen("main");
|
object MainScreen: Screen("home") // 首页
|
||||||
object RouteScreen: Screen("routes")
|
object DeviceScreen: Screen("devices") // 设备 (取代原先的 Route)
|
||||||
object ProfileScreen: Screen("profile")
|
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(
|
val bottomNavItems = listOf(
|
||||||
Screen.MainScreen,
|
Screen.MainScreen,
|
||||||
Screen.RouteScreen,
|
Screen.DeviceScreen,
|
||||||
Screen.ProfileScreen,
|
Screen.AppAuthScreen,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -44,10 +53,16 @@ fun MainApp(
|
|||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
val currentRoute by remember { derivedStateOf { navBackStackEntry?.destination?.route } }
|
||||||
|
val showBottomBar = currentRoute != Screen.LoginScreen.route
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
if (showBottomBar) {
|
||||||
AppBottomNavigation(navController = navController)
|
AppBottomNavigation(navController = navController)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
){ paddingValues ->
|
){ paddingValues ->
|
||||||
/*
|
/*
|
||||||
Image(
|
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
|
// udp info of the super node
|
||||||
val sn: String,
|
val sn: String,
|
||||||
// tcp info of the super node
|
// tcp info of the super node
|
||||||
val tcp: String,
|
|
||||||
|
// udp info of the STUN server
|
||||||
|
val stun: String,
|
||||||
|
|
||||||
val nat_server1: String,
|
val nat_server1: String,
|
||||||
val nat_server2: String,
|
val nat_server2: String,
|
||||||
|
|||||||
@ -52,8 +52,9 @@ class NodeConfig (
|
|||||||
val localPort: Int,
|
val localPort: Int,
|
||||||
|
|
||||||
val supernode: SDLanSock,
|
val supernode: SDLanSock,
|
||||||
val natServer1: InetSocketAddress,
|
val stunServer: SDLanSock,
|
||||||
val natServer2: InetSocketAddress
|
val natServer1: java.net.InetSocketAddress,
|
||||||
|
val natServer2: java.net.InetSocketAddress
|
||||||
)
|
)
|
||||||
|
|
||||||
class NodeStats private constructor(
|
class NodeStats private constructor(
|
||||||
@ -148,8 +149,16 @@ class Node private constructor (
|
|||||||
var startStopChannel: SendChannel<StartStopChanInfo>,
|
var startStopChannel: SendChannel<StartStopChanInfo>,
|
||||||
|
|
||||||
val mac: Mac = generateRandomMAC(),
|
val mac: Mac = generateRandomMAC(),
|
||||||
var nat_type: NatType = NatType.Invalid,
|
var nat_type: NatType = NatType.PortRestricted,
|
||||||
val natProbeCookie: NatProbeCookie = NatProbeCookie(),
|
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 {
|
companion object {
|
||||||
private var instance: Node? = null
|
private var instance: Node? = null
|
||||||
@ -176,11 +185,6 @@ class Node private constructor (
|
|||||||
toSocket,
|
toSocket,
|
||||||
startStopChannel,
|
startStopChannel,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
instance?.config = config
|
|
||||||
instance?.udpSockV4 = v4Sock
|
|
||||||
instance?.token = token
|
|
||||||
Log.e("Initialize", "not initialize instance")
|
|
||||||
}
|
}
|
||||||
return instance!!
|
return instance!!
|
||||||
}
|
}
|
||||||
@ -237,24 +241,51 @@ class Node private constructor (
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendStunRequest() {
|
suspend fun sendStunRequest() {
|
||||||
|
if (this.clientId.isEmpty()) {
|
||||||
|
this.clientId = "punchnet_android_client_" + UniqueNodeID.getUUID()
|
||||||
|
}
|
||||||
val req = SDLStunRequest.newBuilder()
|
val req = SDLStunRequest.newBuilder()
|
||||||
.setCookie(0)
|
.setClientId(this.clientId)
|
||||||
.setClientId(UniqueNodeID.getUUID())
|
|
||||||
.setNetworkId(this.networkID.get())
|
.setNetworkId(this.networkID.get())
|
||||||
.setIp(this.deviceConfig.ip.netAddr)
|
.setIp(this.deviceConfig.ip.netAddr)
|
||||||
.setMac(this.mac)
|
.setMac(this.mac)
|
||||||
.setNatType(this.nat_type.toByte().toInt())
|
.setNatType(this.nat_type.toByte().toInt())
|
||||||
.setV6Info(PunchProto.SDLV6Info.getDefaultInstance())
|
.setV6Info(PunchProto.SDLV6Info.getDefaultInstance())
|
||||||
|
.setSessionToken(this.sessionToken)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val msg = encodeToUDPMessage(req, PacketType.StunRequest)
|
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() {
|
suspend fun probeNatType() {
|
||||||
|
val previousNatType = nat_type
|
||||||
val reply1 = this._sendAndWaitForProbeReply(StunProbeAttr.None, config.natServer1)
|
val reply1 = this._sendAndWaitForProbeReply(StunProbeAttr.None, config.natServer1)
|
||||||
if (reply1 == null) {
|
if (reply1 == null) {
|
||||||
nat_type = NatType.Blocked
|
TerminalLogger.warning { "STUN probe attr=None timed out; keeping natType=${nat_type}" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (reply1.ip == (outerIPV4.get())) {
|
if (reply1.ip == (outerIPV4.get())) {
|
||||||
@ -275,7 +306,14 @@ class Node private constructor (
|
|||||||
|
|
||||||
val reply3 = this._sendAndWaitForProbeReply(StunProbeAttr.None, config.natServer2)
|
val reply3 = this._sendAndWaitForProbeReply(StunProbeAttr.None, config.natServer2)
|
||||||
if (reply3 == null) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,38 +331,28 @@ class Node private constructor (
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun _sendAndWaitForProbeReply(attr: StunProbeAttr, toServer: SocketAddress): SDLStunProbeReply? {
|
suspend fun _sendAndWaitForProbeReply(attr: StunProbeAttr, toServer: java.net.SocketAddress): SDLStunProbeReply? {
|
||||||
val channel = Channel<SDLStunProbeReply>(100)
|
val channel = kotlinx.coroutines.channels.Channel<SDLStunProbeReply>(100)
|
||||||
val cookie = natProbeCookie.addChannel(channel)
|
val cookie = natProbeCookie.addChannel(channel)
|
||||||
val probe = SDLStunProbe.newBuilder()
|
val probe = com.jihe.punchnet.protobuf.PunchProto.SDLStunProbe.newBuilder()
|
||||||
.setAttr(attr.ordinal.toInt())
|
.setAttr(attr.ordinal.toInt())
|
||||||
.setCookie(cookie)
|
.setCookie(cookie)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val msg = encodeToUDPMessage(probe, PacketType.StunProbe)
|
val msg = encodeToUDPMessage(probe, PacketType.StunProbe)
|
||||||
|
TerminalLogger.debugf { "send STUN_PROBE attr=$attr cookie=$cookie to $toServer" }
|
||||||
this.udpSockV4.send_to(msg.toByteArray(), toServer)
|
this.udpSockV4.send_to(msg.toByteArray(), toServer)
|
||||||
|
|
||||||
val k = withTimeoutOrNull(5000) {
|
val k = kotlinx.coroutines.withTimeoutOrNull(5000) {
|
||||||
|
|
||||||
val response = channel.receive()
|
val response = channel.receive()
|
||||||
return@withTimeoutOrNull response
|
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)
|
natProbeCookie.remove(cookie)
|
||||||
return k
|
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
|
package com.jihe.punchnet.sdlan.network
|
||||||
|
|
||||||
import com.google.protobuf.kotlin.toByteString
|
import com.google.protobuf.kotlin.toByteString
|
||||||
|
import com.google.gson.JsonElement
|
||||||
import com.jihe.punchnet.protobuf.PunchProto.SDLData
|
import com.jihe.punchnet.protobuf.PunchProto.SDLData
|
||||||
import com.jihe.punchnet.protobuf.PunchProto.SDLPeerInfo
|
import com.jihe.punchnet.protobuf.PunchProto.SDLPeerInfo
|
||||||
import com.jihe.punchnet.protobuf.PunchProto.SDLQueryInfo
|
import com.jihe.punchnet.protobuf.PunchProto.SDLQueryInfo
|
||||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegister
|
import com.jihe.punchnet.protobuf.PunchProto.SDLRegister
|
||||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterAck
|
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.EtherType
|
||||||
import com.jihe.punchnet.sdlan.config.SDLanConfig
|
import com.jihe.punchnet.sdlan.config.SDLanConfig
|
||||||
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
import com.jihe.punchnet.sdlan.logs.TerminalLogger
|
||||||
import com.jihe.punchnet.sdlan.utils.isMultiBroadcast
|
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 com.jihe.punchnet.sdlan.utils.macToString
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.URI
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
@ -299,15 +307,311 @@ suspend fun sendRegister(
|
|||||||
.build()
|
.build()
|
||||||
val msg = encodeToUDPMessage(register, PacketType.Register)
|
val msg = encodeToUDPMessage(register, PacketType.Register)
|
||||||
|
|
||||||
|
TerminalLogger.debugf { "send REGISTER to ${macToString(mac)} at $sock, natType=$natType" }
|
||||||
sendToSock(node, msg, sock)
|
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) {
|
suspend fun sendPacketToNet(node: Node, dstmac: Mac, encrypted: ByteArray, size: Long) {
|
||||||
val destination = findPeerDestination(node, dstmac, size)
|
var is_p2p: Boolean = false
|
||||||
TerminalLogger.debugf { "send PACKET to ${destination}" }
|
var is_multicast: Boolean = false
|
||||||
sendToSock(node, content, destination)
|
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)) {
|
if (isMultiBroadcast(dstmac)) {
|
||||||
node.stats.txSup.addAndGet(size)
|
node.stats.txSup.addAndGet(size)
|
||||||
node.stats.txBroadcast.addAndGet(size)
|
node.stats.txBroadcast.addAndGet(size)
|
||||||
result = node.config.supernode
|
result = node.config.stunServer
|
||||||
is_multicast = true
|
is_multicast = true
|
||||||
} else {
|
} else {
|
||||||
val peer = node.knownPeers.get(dstmac)
|
val peer = node.knownPeers.get(dstmac)
|
||||||
if (peer == null) {
|
if (peer == null) {
|
||||||
node.stats.txSup.addAndGet(size)
|
node.stats.txSup.addAndGet(size)
|
||||||
result = node.config.supernode
|
result = node.config.stunServer
|
||||||
} else {
|
} else {
|
||||||
val now = System.currentTimeMillis()/1000
|
val now = System.currentTimeMillis()/1000
|
||||||
if (now - peer.lastP2P.get() >= peer.timeout/2) {
|
if (now - peer.lastP2P.get() >= peer.timeout/2) {
|
||||||
TerminalLogger.warning { "last p2p is too old, deleting from known hosts" }
|
TerminalLogger.warning { "last p2p is too old, deleting from known hosts" }
|
||||||
node.knownPeers.remove(dstmac)
|
node.knownPeers.remove(dstmac)
|
||||||
node.stats.txSup.addAndGet(size)
|
node.stats.txSup.addAndGet(size)
|
||||||
result = node.config.supernode
|
result = node.config.stunServer
|
||||||
} else {
|
} else {
|
||||||
is_p2p = true
|
is_p2p = true
|
||||||
node.stats.txP2P.addAndGet(size)
|
node.stats.txP2P.addAndGet(size)
|
||||||
@ -389,12 +693,12 @@ suspend fun sendQueryPeer(node: Node, dstmac: Mac) {
|
|||||||
val query = SDLQueryInfo.newBuilder()
|
val query = SDLQueryInfo.newBuilder()
|
||||||
.setDstMac(dstmac)
|
.setDstMac(dstmac)
|
||||||
.build()
|
.build()
|
||||||
val msg = encodeToTcpMessage(query, node.getNextPacketID(), PacketType.QueryInfo)
|
val msg = encodeToControlMessage(query, PacketType.QueryInfo)
|
||||||
node.toSocket.send(msg.toByteArray())
|
node.toSocket.send(msg.toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formEthernetPacket(srcmac: ByteArray, dstmac: ByteArray, data: ByteArray): ByteBuffer {
|
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(dstmac)
|
||||||
buffer.put(srcmac)
|
buffer.put(srcmac)
|
||||||
buffer.putShort(EtherType.IPV4)
|
buffer.putShort(EtherType.IPV4)
|
||||||
@ -407,54 +711,140 @@ fun formEthernetPacket(srcmac: ByteArray, dstmac: ByteArray, data: ByteArray): B
|
|||||||
// fromip is self ip
|
// fromip is self ip
|
||||||
// queryip is the targetip
|
// queryip is the targetip
|
||||||
suspend fun sendArpRequest(node: Node, queryip: Int) {
|
suspend fun sendArpRequest(node: Node, queryip: Int) {
|
||||||
println(1)
|
if (!node.aes.isAuthorized()) {
|
||||||
val frommac = node.mac.toByteArray()
|
TerminalLogger.errorf { "not authed for send arp request" }
|
||||||
val fromip = node.deviceConfig.ip.netAddr
|
return
|
||||||
val buffer = ByteBuffer.allocate(64).order(ByteOrder.BIG_ENDIAN)
|
}
|
||||||
buffer.put(ByteArray(6){0xff.toByte()})
|
val arpReq = SDLArpRequest.newBuilder()
|
||||||
buffer.put(frommac)
|
.setTargetIp(queryip)
|
||||||
println(2)
|
.setOriginIp(node.deviceConfig.ip.netAddr)
|
||||||
// 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()
|
.build()
|
||||||
println(5)
|
val msg = encodeToControlMessage(arpReq, PacketType.ArpRequest)
|
||||||
val msg = encodeToUDPMessage(data, PacketType.Data)
|
node.toSocket.send(msg.toByteArray())
|
||||||
sendPacketToNet(node, SDLanConfig.BROADCASTMAC, msg, size.toLong())
|
TerminalLogger.debugf { "sent QUIC ARP request for ip: ${com.jihe.punchnet.sdlan.utils.ipToString(queryip)}" }
|
||||||
}
|
|
||||||
println(6)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
suspend fun handlePacketPeerInfo(node: Node, content: ByteArray) {
|
||||||
val pinfo: SDLPeerInfo
|
val pinfo: SDLPeerInfo
|
||||||
@ -470,6 +860,11 @@ suspend fun handlePacketPeerInfo(node: Node, content: ByteArray) {
|
|||||||
return
|
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 remoteNat = NatType.fromUByte(pinfo.v4Info.natType.toUByte())
|
||||||
val pending = node.pendingPeers.get(pinfo.dstMac)
|
val pending = node.pendingPeers.get(pinfo.dstMac)
|
||||||
if (pending == null) {
|
if (pending == null) {
|
||||||
@ -482,16 +877,18 @@ suspend fun handlePacketPeerInfo(node: Node, content: ByteArray) {
|
|||||||
sendRegister(node, remoteNat, pending.sock, pinfo.dstMac)
|
sendRegister(node, remoteNat, pending.sock, pinfo.dstMac)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun handleTcpEvent(node: Node, event: EventType, cmdprotobuf: ByteArray) {
|
suspend fun handleEvent(node: Node, cmdprotobuf: ByteArray) {
|
||||||
when(event) {
|
val evt: com.jihe.punchnet.protobuf.PunchProto.SDLEvent
|
||||||
EventType.SendRegister -> {
|
|
||||||
val reg: SDLSendRegisterEvent
|
|
||||||
try {
|
try {
|
||||||
reg = SDLSendRegisterEvent.parseFrom(cmdprotobuf)
|
evt = com.jihe.punchnet.protobuf.PunchProto.SDLEvent.parseFrom(cmdprotobuf)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
TerminalLogger.errorf {"failed to decode SendRegisterEvent: $e"}
|
TerminalLogger.errorf {"failed to decode SDLEvent: $e"}
|
||||||
return
|
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 remoteNat = NatType.fromUByte(reg.natType.toUByte())
|
||||||
val ip = byteArrayOf(
|
val ip = byteArrayOf(
|
||||||
(reg.natIp ushr 24).and(0xff).toByte(),
|
(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))
|
checkPeerRegistrationNeeded(node,false, reg.dstMac, remoteNat, SDLanSock(IPFamily.IPV4, reg.natPort, ip))
|
||||||
}
|
}
|
||||||
else -> {
|
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.PunchnetServiceArgument
|
||||||
import com.jihe.punchnet.data.ButtonRepository
|
import com.jihe.punchnet.data.ButtonRepository
|
||||||
import com.jihe.punchnet.data.ButtonState
|
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.SDLRegisterSuper
|
||||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuperAck
|
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuperAck
|
||||||
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuperNak
|
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.protobuf.PunchProto.SDLStunProbeReply
|
||||||
import com.jihe.punchnet.sdlan.config.Arguments
|
import com.jihe.punchnet.sdlan.config.Arguments
|
||||||
import com.jihe.punchnet.sdlan.config.RSAConfig
|
import com.jihe.punchnet.sdlan.config.RSAConfig
|
||||||
@ -47,52 +49,92 @@ import java.util.concurrent.atomic.AtomicLong
|
|||||||
import kotlin.io.path.pathString
|
import kotlin.io.path.pathString
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
|
suspend fun onMessage(scope: CoroutineScope, data: SDLanControl) {
|
||||||
val node = Node.getInstance()
|
val node = Node.getInstance()
|
||||||
|
|
||||||
TerminalLogger.debugf {"message received"}
|
TerminalLogger.debugf {"message received"}
|
||||||
when(data.packetType) {
|
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 -> {
|
PacketType.RegisterSuperACK -> {
|
||||||
val ack = SDLRegisterSuperAck.parseFrom(data.currentPacket)
|
val ack = SDLRegisterSuperAck.parseFrom(data.currentPacket)
|
||||||
// TerminalLogger.debugf { "got register super ack: $ack" }
|
val algorithm = ack.algorithm.ifEmpty { "aes" }.lowercase()
|
||||||
println("ack is ${ack}")
|
TerminalLogger.debugf {
|
||||||
println("ack's key is ${ack.aesKey.size()}")
|
"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) {
|
if (aeskey == null) {
|
||||||
println("aes key error")
|
println("aes key error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
println("decrypted aes is: ${aeskey.contentToString()}")
|
node.aes.setSecret(aeskey, algorithm, ack.regionId.toLong())
|
||||||
node.aes.setSecret(aeskey)
|
node.sessionToken = ack.sessionToken
|
||||||
val ipStr = ipToString(ack.devAddr.netAddr)
|
|
||||||
TerminalLogger.debugf { "assigned ip: $ipStr" }
|
node.deviceConfig.ip.netAddr = com.jihe.punchnet.sdlan.utils.ipStringToInt(node.connectData?.ip ?: "0.0.0.0").toUInt().toInt()
|
||||||
node.deviceConfig.ip.netAddr = ack.devAddr.netAddr
|
node.deviceConfig.ip.netBitLen = (node.connectData?.maskLen ?: 24).toByte()
|
||||||
node.deviceConfig.ip.netBitLen = ack.devAddr.netBitLen.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)
|
node.iface?.reload_config(node.deviceConfig)
|
||||||
println("reloading config ok: node.iface = ${node.iface}")
|
node.networkID.set(node.authData?.networkId ?: 0)
|
||||||
println("reloading config ok: node.udp = ${node.udpSockV4}")
|
TerminalLogger.debugf {
|
||||||
|
"control registered: clientIdTail=${node.clientId.takeLast(12)}, network=${node.networkID.get()}, " +
|
||||||
node.networkID.set(ack.devAddr.networkId)
|
"ip=${ipToString(node.deviceConfig.ip.netAddr)}, mask=${node.deviceConfig.ip.netBitLen}, " +
|
||||||
// println("got aes key: ${aeskey.toList()}, length is ${aeskey.size}")
|
"mac=${macToString(node.mac)}, identity=${node.connectData?.identityId ?: 0}"
|
||||||
|
}
|
||||||
|
sendExposedServiceRequest(node)
|
||||||
node.sendStunRequest()
|
node.sendStunRequest()
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
node.probeNatType()
|
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) {
|
} 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 -> {
|
PacketType.RegisterSuperNAK -> {
|
||||||
val nak = SDLRegisterSuperNak.parseFrom(data.currentPacket)
|
val nak = SDLRegisterSuperNak.parseFrom(data.currentPacket)
|
||||||
@ -117,7 +159,7 @@ suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
|
|||||||
TerminalLogger.errorf { "malformed command received"}
|
TerminalLogger.errorf { "malformed command received"}
|
||||||
return
|
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 -> {
|
PacketType.PeerInfo -> {
|
||||||
TerminalLogger.debugf { "PeerInfo received" }
|
TerminalLogger.debugf { "PeerInfo received" }
|
||||||
@ -125,19 +167,40 @@ suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
|
|||||||
}
|
}
|
||||||
PacketType.Event -> {
|
PacketType.Event -> {
|
||||||
TerminalLogger.debugf { "Event received" }
|
TerminalLogger.debugf { "Event received" }
|
||||||
if (data.currentPacket.size < 1) {
|
handleEvent(node, data.currentPacket)
|
||||||
TerminalLogger.errorf { "malformed event received"}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val eventType = EventType.fromByte(data.currentPacket[0])
|
|
||||||
|
|
||||||
val size = data.currentPacket.size
|
|
||||||
val content = data.currentPacket.sliceArray(1..size-1)
|
|
||||||
handleTcpEvent(node, eventType, content)
|
|
||||||
}
|
}
|
||||||
PacketType.Pong -> {
|
PacketType.Pong -> {
|
||||||
TerminalLogger.debugf { "tcp Pong Received" }
|
TerminalLogger.debugf { "Pong Received" }
|
||||||
// node.ping_to_sn()
|
}
|
||||||
|
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 -> {
|
else -> {
|
||||||
println("error packet type: ${data.packetType.toUByte()}")
|
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)
|
UniqueNodeID.setBaseDir(argument.baseDir)
|
||||||
val edgeUUID = UniqueNodeID.getUUID()
|
val edgeUUID = UniqueNodeID.getUUID()
|
||||||
val config = parseConfig(edgeUUID, argument)
|
val config = parseConfig(edgeUUID, argument)
|
||||||
@ -156,58 +219,24 @@ suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val toSocket = Channel<ByteArray>(100)
|
val toSocket = Channel<ByteArray>(100)
|
||||||
val start_stop_channel = Channel<StartStopChanInfo>(100)
|
val start_stop_channel = Channel<StartStopChanInfo>(100)
|
||||||
initEdge(scope, iface, argument.token, config, toSocket, start_stop_channel)
|
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 now = System.currentTimeMillis() / 1000
|
||||||
|
|
||||||
val node = Node.getInstance()
|
val node = Node.getInstance()
|
||||||
|
node.authData = authData
|
||||||
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)
|
node.connectData = connectData
|
||||||
val encrypted = node.rsa.encrypt(before.toByteArray(), use_private_key = false)
|
|
||||||
// println("encrypted: ${encrypted.contentToString()}")
|
|
||||||
val origin = node.rsa.decrypt(encrypted, use_pub_key = false)
|
|
||||||
// TerminalLogger.debugf { "got encrypted ${encrypted.size}"}
|
|
||||||
// TerminalLogger.debugf { "got origin ${origin.contentToString()}"}
|
|
||||||
|
|
||||||
TerminalLogger.debugf { "self mac: ${macToString(node.mac)}"}
|
TerminalLogger.debugf { "self mac: ${macToString(node.mac)}"}
|
||||||
|
TerminalLogger.debugf { describeConnectDataForLog(node) }
|
||||||
|
|
||||||
|
val onConnected: suspend (tech.kwik.core.QuicStream) -> Unit = { stream ->
|
||||||
val onConnected: suspend (Socket, Int?) -> Unit = { socket, pktID ->
|
TerminalLogger.debugf { "QUIC stream connected callback" }
|
||||||
println("connected callback")
|
// We wait for server's PACKET_WELCOME before sending RegisterSuper
|
||||||
val clientid = UniqueNodeID.getUUID()
|
|
||||||
println("client id is $clientid")
|
|
||||||
|
|
||||||
val registerSuper = SDLRegisterSuper.newBuilder()
|
|
||||||
.setVersion(1)
|
|
||||||
.setToken(node.token)
|
|
||||||
.setInstalledChannel("kotlin")
|
|
||||||
.setClientId(clientid)
|
|
||||||
.setDevAddr(
|
|
||||||
SDLDevAddr.newBuilder().setMac(node.mac)
|
|
||||||
.setNetAddr(0)
|
|
||||||
.setNetBitLen(0)
|
|
||||||
.setNetworkId(0)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.setPubKey(node.rsa.getPublicKeyString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val packetID = pktID ?: node.getNextPacketID()
|
|
||||||
|
|
||||||
val msg = encodeToTcpMessage(registerSuper, packetID, PacketType.RegisterSuper)
|
|
||||||
// TerminalLogger.debugf{"sent register super: $registerSuper"}
|
|
||||||
toSocket.send(msg.toByteArray())
|
|
||||||
TerminalLogger.debugf{"sent register super!"}
|
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
val onMessage: suspend (SDLanTCP) -> Unit = { data ->
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
@ -219,15 +248,11 @@ suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments,
|
|||||||
val data = node.iface?.recv()
|
val data = node.iface?.recv()
|
||||||
Log.d("SDLAN", "async receive data from iface: ${data?.size} bytes")
|
Log.d("SDLAN", "async receive data from iface: ${data?.size} bytes")
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
//delay(1000)
|
|
||||||
delay(1000)
|
delay(1000)
|
||||||
// println("got data is null")
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (data.isEmpty()) {
|
if (data.isEmpty()) {
|
||||||
delay(1000)
|
delay(1000)
|
||||||
// println("got data size 0")
|
|
||||||
// delay(1000)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
Log.d("SDLAN", "handle data form device starts")
|
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()
|
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 {
|
scope.launch {
|
||||||
initTCPConn(
|
initQUICConn(
|
||||||
scope,
|
scope,
|
||||||
tcp[0], tcp[1].toInt(),
|
snParts[0], snParts[1].toIntOrNull() ?: 1265,
|
||||||
start_stop_channel,
|
start_stop_channel,
|
||||||
AtomicLong(now),
|
AtomicLong(now),
|
||||||
AtomicBoolean(false),
|
AtomicBoolean(false),
|
||||||
@ -281,7 +284,6 @@ suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
println("sending start")
|
println("sending start")
|
||||||
start_stop_channel.send(StartStopChanInfo(StartStopFlag.IsStart, null))
|
start_stop_channel.send(StartStopChanInfo(StartStopFlag.IsStart, null))
|
||||||
println("sent start")
|
println("sent start")
|
||||||
@ -308,6 +310,7 @@ suspend fun loopSocketV4(scope: CoroutineScope, node: Node, sock: SDLanSocket, c
|
|||||||
try {
|
try {
|
||||||
while(true) {
|
while(true) {
|
||||||
delay(10_000)
|
delay(10_000)
|
||||||
|
node.probeNatType()
|
||||||
node.sendStunRequest()
|
node.sendStunRequest()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -387,6 +390,14 @@ suspend fun handleAPacket(scope: CoroutineScope, node: Node, from: SocketAddress
|
|||||||
TerminalLogger.debugf { "got stun reply" }
|
TerminalLogger.debugf { "got stun reply" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
PacketType.PolicyResponse -> {
|
||||||
|
TerminalLogger.debugf { "got policy response" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
PacketType.ExposedServiceResponse -> {
|
||||||
|
TerminalLogger.debugf { "got exposed service response" }
|
||||||
|
return
|
||||||
|
}
|
||||||
PacketType.Register -> {
|
PacketType.Register -> {
|
||||||
TerminalLogger.debugf { "got REGISTER" }
|
TerminalLogger.debugf { "got REGISTER" }
|
||||||
if (from is InetSocketAddress) {
|
if (from is InetSocketAddress) {
|
||||||
@ -447,213 +458,6 @@ fun initEdge(scope: CoroutineScope, iface: Iface, token: String, config: NodeCon
|
|||||||
println(instance)
|
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? {
|
fun parseConfig(nodeuuid: String, argument: Arguments): NodeConfig? {
|
||||||
if (argument.sn.length == 0) {
|
if (argument.sn.length == 0) {
|
||||||
@ -661,18 +465,23 @@ fun parseConfig(nodeuuid: String, argument: Arguments): NodeConfig? {
|
|||||||
return null
|
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)
|
val sn = parseScoketAddressV4FromString(argument.sn)
|
||||||
if (sn == null) {
|
if (sn == null) {
|
||||||
return 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(
|
return NodeConfig(
|
||||||
baseDir = argument.baseDir,
|
baseDir = argument.baseDir,
|
||||||
name = argument.name,
|
name = argument.name,
|
||||||
@ -686,7 +495,8 @@ fun parseConfig(nodeuuid: String, argument: Arguments): NodeConfig? {
|
|||||||
registerTTL = argument.registerTTL,
|
registerTTL = argument.registerTTL,
|
||||||
localPort = argument.localPort,
|
localPort = argument.localPort,
|
||||||
supernode = SDLanSock(IPFamily.IPV4, sn.port, sn.address.address),
|
supernode = SDLanSock(IPFamily.IPV4, sn.port, sn.address.address),
|
||||||
|
stunServer = SDLanSock(IPFamily.IPV4, stun.port, stun.address.address),
|
||||||
natServer1 = natServer1,
|
natServer1 = natServer1,
|
||||||
natServer2 = natServer2,
|
natServer2 = natServer2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -49,6 +49,15 @@ enum class PacketType(val id: UByte) {
|
|||||||
StunProbe(0x32u),
|
StunProbe(0x32u),
|
||||||
StunProbeReply(0x33u),
|
StunProbeReply(0x33u),
|
||||||
|
|
||||||
|
Welcome(0x4fu),
|
||||||
|
ArpRequest(0x50u),
|
||||||
|
ArpResponse(0x51u),
|
||||||
|
|
||||||
|
PolicyRequest(0xb0u),
|
||||||
|
PolicyResponse(0xb1u),
|
||||||
|
ExposedServiceRequest(0xb2u),
|
||||||
|
ExposedServiceResponse(0xb3u),
|
||||||
|
|
||||||
Data(0xffu);
|
Data(0xffu);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -65,9 +74,8 @@ fun PacketType.toUByte(): UByte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// tcp发送过来的通道里面的信息
|
// tcp/quic发送过来的通道里面的信息
|
||||||
class SDLanTCP(
|
class SDLanControl(
|
||||||
val packetID: UInt,
|
|
||||||
val packetType: PacketType,
|
val packetType: PacketType,
|
||||||
val currentPacket: ByteArray,
|
val currentPacket: ByteArray,
|
||||||
)
|
)
|
||||||
@ -103,18 +111,13 @@ fun encodeToUDPMessage(msg: Message?, packetType: PacketType): List<Byte> {
|
|||||||
return result
|
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 msgByte = msg?.toByteArray()?.toList()?:listOf<Byte>()
|
||||||
val result: MutableList<Byte> = mutableListOf()
|
val result: MutableList<Byte> = mutableListOf()
|
||||||
|
|
||||||
result.addAll(ByteBuffer.allocate(2)
|
result.addAll(ByteBuffer.allocate(2)
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
.order(ByteOrder.BIG_ENDIAN)
|
||||||
.putShort((msgByte.size + 5).toShort())
|
.putShort((msgByte.size + 1).toShort())
|
||||||
.array().toList())
|
|
||||||
|
|
||||||
result.addAll(ByteBuffer.allocate(4)
|
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
|
||||||
.putInt(packetID)
|
|
||||||
.array().toList())
|
.array().toList())
|
||||||
|
|
||||||
result.add(packetType.toUByte().toByte())
|
result.add(packetType.toUByte().toByte())
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import kotlin.experimental.inv
|
|||||||
import kotlin.experimental.or
|
import kotlin.experimental.or
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
object UniqueNodeID {
|
object UniqueNodeID {
|
||||||
var id: String = ""
|
var id: String = ""
|
||||||
private var baseDir: String = ""
|
private var baseDir: String = ""
|
||||||
@ -25,11 +27,18 @@ object UniqueNodeID {
|
|||||||
baseDir = basedir
|
baseDir = basedir
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUUID(): String {
|
fun getUUID(context: Context? = null): String {
|
||||||
if (baseDir.length == 0) {
|
if (id.isEmpty() && context != null) {
|
||||||
baseDir = Environment.getExternalStorageDirectory().name
|
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)
|
val dirpath = File(baseDir)
|
||||||
dirpath.mkdirs()
|
dirpath.mkdirs()
|
||||||
val f = File(baseDir, SDLanConfig.ID_FILENAME)
|
val f = File(baseDir, SDLanConfig.ID_FILENAME)
|
||||||
@ -71,6 +80,15 @@ fun ipToString(ip: Int): String {
|
|||||||
return "$d1.$d2.$d3.$d4"
|
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 {
|
fun macToString(mac: Mac): String {
|
||||||
return mac.joinToString(separator = ":") { it.toUByte().toString(16) }
|
return mac.joinToString(separator = ":") { it.toUByte().toString(16) }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,8 @@ syntax = "proto3";
|
|||||||
|
|
||||||
option java_package = "com.jihe.punchnet.protobuf";
|
option java_package = "com.jihe.punchnet.protobuf";
|
||||||
option java_outer_classname = "PunchProto";
|
option java_outer_classname = "PunchProto";
|
||||||
// 基础公共类型定义
|
|
||||||
|
|
||||||
|
// 基础公共类型定义
|
||||||
message SDLV4Info {
|
message SDLV4Info {
|
||||||
uint32 port = 1;
|
uint32 port = 1;
|
||||||
bytes v4 = 2;
|
bytes v4 = 2;
|
||||||
@ -15,34 +15,30 @@ message SDLV6Info {
|
|||||||
bytes v6 = 2;
|
bytes v6 = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备网络地址信息
|
message SDLWelcome {
|
||||||
message SDLDevAddr {
|
uint32 version = 1;
|
||||||
uint32 network_id = 1;
|
uint32 max_bidi_streams = 2;
|
||||||
bytes mac = 2;
|
uint32 max_packet_size = 3;
|
||||||
uint32 net_addr = 3;
|
uint32 heartbeat_sec = 4;
|
||||||
uint32 net_bit_len = 4;
|
SDLV6Info ipv6_assist = 5;
|
||||||
}
|
|
||||||
|
|
||||||
// tcp通讯消息
|
|
||||||
message SDLEmpty {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SDLRegisterSuper {
|
message SDLRegisterSuper {
|
||||||
uint32 version = 1;
|
string client_id = 1;
|
||||||
string installed_channel = 2;
|
uint32 network_id = 2;
|
||||||
string client_id = 3;
|
bytes mac = 3;
|
||||||
SDLDevAddr dev_addr = 4;
|
uint32 ip = 4;
|
||||||
string pub_key = 5;
|
uint32 mask_len = 5;
|
||||||
string token = 6;
|
string hostname = 6;
|
||||||
|
string pub_key = 7;
|
||||||
|
string access_token = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SDLRegisterSuperAck {
|
message SDLRegisterSuperAck {
|
||||||
SDLDevAddr dev_addr = 1;
|
string algorithm = 1;
|
||||||
bytes aes_key = 2;
|
bytes key = 2;
|
||||||
uint32 upgrade_type = 3;
|
uint32 region_id = 3;
|
||||||
optional string upgrade_prompt = 4;
|
bytes session_token = 4;
|
||||||
optional string upgrade_address = 5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SDLRegisterSuperNak {
|
message SDLRegisterSuperNak {
|
||||||
@ -51,25 +47,60 @@ message SDLRegisterSuperNak {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 网络地址查询
|
// 网络地址查询
|
||||||
|
|
||||||
message SDLQueryInfo {
|
message SDLQueryInfo {
|
||||||
bytes dst_mac = 1;
|
bytes dst_mac = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SDLPeerInfo {
|
message SDLPeerInfo {
|
||||||
bytes dst_mac = 1;
|
bytes dst_mac = 1;
|
||||||
SDLV4Info v4_info = 2;
|
optional SDLV4Info v4_info = 2;
|
||||||
optional SDLV6Info v6_info = 3;
|
optional SDLV6Info v6_info = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件定义
|
message SDLArpRequest {
|
||||||
|
uint32 target_ip = 1;
|
||||||
|
uint32 origin_ip = 2;
|
||||||
|
bytes context = 3;
|
||||||
|
}
|
||||||
|
|
||||||
message SDLNatChangedEvent {
|
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;
|
bytes mac = 1;
|
||||||
uint32 ip = 2;
|
uint32 ip = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SDLSendRegisterEvent {
|
message SendRegister {
|
||||||
bytes dst_mac = 1;
|
bytes dst_mac = 1;
|
||||||
uint32 nat_ip = 2;
|
uint32 nat_ip = 2;
|
||||||
uint32 nat_port = 3;
|
uint32 nat_port = 3;
|
||||||
@ -77,46 +108,54 @@ message SDLSendRegisterEvent {
|
|||||||
optional SDLV6Info v6_info = 5;
|
optional SDLV6Info v6_info = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SDLNetworkShutdownEvent {
|
message ExposedServiceChanged {
|
||||||
|
}
|
||||||
|
|
||||||
|
message NetworkShutdown {
|
||||||
string message = 1;
|
string message = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 命令定义
|
oneof event {
|
||||||
|
NatChanged nat_changed = 1;
|
||||||
|
SendRegister send_register = 2;
|
||||||
|
NetworkShutdown shutdown = 3;
|
||||||
|
ExposedServiceChanged exposed_service_changed = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message SDLChangeNetworkCommand {
|
// 命令定义
|
||||||
SDLDevAddr dev_addr = 1;
|
message SDLCommand {
|
||||||
bytes aes_key = 2;
|
uint32 pkt_id = 1;
|
||||||
|
|
||||||
|
message ExitNodeControl {
|
||||||
|
int32 action = 1;
|
||||||
|
string remark = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
oneof command {
|
||||||
|
ExitNodeControl exit_node = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message SDLCommandAck {
|
message SDLCommandAck {
|
||||||
// status = true, 表示成功;status = false 表示失败,message是失败原因描述
|
uint32 pkt_id = 1;
|
||||||
bool status = 1;
|
int32 code = 2;
|
||||||
optional string message = 2;
|
string message = 3;
|
||||||
}
|
bytes data = 4;
|
||||||
|
|
||||||
message SDLFlows {
|
|
||||||
// 服务器转发流量
|
|
||||||
uint32 forward_num = 1;
|
|
||||||
// p2p直接流量
|
|
||||||
uint32 p2p_num = 2;
|
|
||||||
// 接收的流量
|
|
||||||
uint32 inbound_num = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UDP通讯消息
|
// UDP通讯消息
|
||||||
|
|
||||||
message SDLStunRequest {
|
message SDLStunRequest {
|
||||||
uint32 cookie = 1;
|
string client_id = 1;
|
||||||
string client_id = 2;
|
uint32 network_id = 2;
|
||||||
uint32 network_id = 3;
|
bytes mac = 3;
|
||||||
bytes mac = 4;
|
uint32 ip = 4;
|
||||||
uint32 ip = 5;
|
uint32 nat_type = 5;
|
||||||
uint32 nat_type = 6;
|
optional SDLV6Info v6_info = 6;
|
||||||
optional SDLV6Info v6_info = 7;
|
bytes session_token = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SDLStunReply {
|
message SDLStunReply {
|
||||||
uint32 cookie = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SDLData {
|
message SDLData {
|
||||||
@ -126,6 +165,8 @@ message SDLData {
|
|||||||
bool is_p2p = 4;
|
bool is_p2p = 4;
|
||||||
uint32 ttl = 5;
|
uint32 ttl = 5;
|
||||||
bytes data = 6;
|
bytes data = 6;
|
||||||
|
bytes session_token = 7;
|
||||||
|
uint32 identity_id = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SDLRegister {
|
message SDLRegister {
|
||||||
@ -141,14 +182,15 @@ message SDLRegisterAck {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 网络类型探测
|
// 网络类型探测
|
||||||
|
|
||||||
message SDLStunProbe {
|
message SDLStunProbe {
|
||||||
uint32 cookie = 1;
|
uint32 cookie = 1;
|
||||||
uint32 attr = 2;
|
uint32 attr = 2;
|
||||||
|
uint32 step = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SDLStunProbeReply {
|
message SDLStunProbeReply {
|
||||||
uint32 cookie = 1;
|
uint32 cookie = 1;
|
||||||
uint32 port = 2;
|
uint32 step = 2;
|
||||||
uint32 ip = 3;
|
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,
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
android.nonTransitiveRClass=true
|
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