init
This commit is contained in:
20
apps/android/service/gateway/build.gradle.kts
Normal file
20
apps/android/service/gateway/build.gradle.kts
Normal file
@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.vpnshare.gateway"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":apps:android:core:domain"))
|
||||
implementation(project(":apps:android:core:engine"))
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest />
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.vpnshare.gateway
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LinkProperties
|
||||
import android.net.NetworkCapabilities
|
||||
import org.vpnshare.domain.model.VpnStatus
|
||||
|
||||
class VpnDetector(context: Context) {
|
||||
private val connectivityManager =
|
||||
context.getSystemService(ConnectivityManager::class.java)
|
||||
|
||||
fun snapshot(): VpnStatus {
|
||||
val activeNetwork = connectivityManager.activeNetwork
|
||||
val activeCapabilities = activeNetwork?.let(connectivityManager::getNetworkCapabilities)
|
||||
val vpnNetwork = connectivityManager.allNetworks.firstOrNull { network ->
|
||||
connectivityManager.getNetworkCapabilities(network)
|
||||
?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
|
||||
}
|
||||
val linkProperties: LinkProperties? = (vpnNetwork ?: activeNetwork)
|
||||
?.let(connectivityManager::getLinkProperties)
|
||||
|
||||
return VpnStatus(
|
||||
active = activeCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true ||
|
||||
vpnNetwork != null,
|
||||
networkName = linkProperties?.interfaceName,
|
||||
supportsIpv4 = linkProperties?.linkAddresses
|
||||
?.any { it.address.address.size == IPV4_BYTES } ?: false,
|
||||
supportsIpv6 = linkProperties?.linkAddresses
|
||||
?.any { it.address.address.size == IPV6_BYTES } ?: false
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val IPV4_BYTES = 4
|
||||
const val IPV6_BYTES = 16
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.vpnshare.gateway
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.vpnshare.domain.model.GatewayConfig
|
||||
import org.vpnshare.engine.RustVpnShareEngine
|
||||
|
||||
class VpnShareGatewayService : Service() {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val engine = RustVpnShareEngine()
|
||||
private lateinit var vpnDetector: VpnDetector
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
vpnDetector = VpnDetector(this)
|
||||
ensureNotificationChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
scope.launch { engine.stopGateway() }
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
else -> startGateway()
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
scope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun startGateway() {
|
||||
val notification = buildNotification()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val vpn = vpnDetector.snapshot()
|
||||
engine.startGateway(GatewayConfig())
|
||||
if (!vpn.active) {
|
||||
// The UI layer will render this event once event collection is wired.
|
||||
// The gateway still accepts local pairing so users can start their VPN.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(): Notification {
|
||||
val stopIntent = Intent(this, VpnShareGatewayService::class.java).setAction(ACTION_STOP)
|
||||
val stopPendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
stopIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_upload)
|
||||
.setContentTitle("VPN Share is active")
|
||||
.setContentText("Sharing through the phone VPN")
|
||||
.setOngoing(true)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopPendingIntent)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun ensureNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"VPN Share sessions",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_START = "org.vpnshare.action.START"
|
||||
const val ACTION_STOP = "org.vpnshare.action.STOP"
|
||||
private const val CHANNEL_ID = "vpnshare.gateway"
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.vpnshare.gateway.discovery
|
||||
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
|
||||
class NsdDiscoveryPublisher(context: Context) {
|
||||
private val nsdManager = context.getSystemService(NsdManager::class.java)
|
||||
private var listener: NsdManager.RegistrationListener? = null
|
||||
|
||||
fun publish(instanceName: String, port: Int) {
|
||||
stop()
|
||||
val serviceInfo = NsdServiceInfo().apply {
|
||||
serviceName = instanceName
|
||||
serviceType = SERVICE_TYPE
|
||||
setPort(port)
|
||||
setAttribute("v", "1")
|
||||
setAttribute("caps", "vshp,qr,resume")
|
||||
}
|
||||
val registrationListener = object : NsdManager.RegistrationListener {
|
||||
override fun onServiceRegistered(serviceInfo: NsdServiceInfo) = Unit
|
||||
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) = Unit
|
||||
override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) = Unit
|
||||
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) = Unit
|
||||
}
|
||||
listener = registrationListener
|
||||
nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
listener?.let {
|
||||
runCatching { nsdManager.unregisterService(it) }
|
||||
}
|
||||
listener = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SERVICE_TYPE = "_vpnshare._udp."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.vpnshare.gateway.transport
|
||||
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
class LocalOnlyHotspotController(
|
||||
private val wifiManager: WifiManager
|
||||
) {
|
||||
private var reservation: WifiManager.LocalOnlyHotspotReservation? = null
|
||||
|
||||
fun start(callback: Callback) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
callback.onFailed("Local-only hotspot requires Android 8.0 or newer")
|
||||
return
|
||||
}
|
||||
wifiManager.startLocalOnlyHotspot(
|
||||
object : WifiManager.LocalOnlyHotspotCallback() {
|
||||
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation) {
|
||||
this@LocalOnlyHotspotController.reservation = reservation
|
||||
val config = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
reservation.softApConfiguration?.ssid.orEmpty()
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
reservation.wifiConfiguration?.SSID.orEmpty()
|
||||
}
|
||||
callback.onStarted(config)
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
this@LocalOnlyHotspotController.reservation = null
|
||||
callback.onStopped()
|
||||
}
|
||||
|
||||
override fun onFailed(reason: Int) {
|
||||
callback.onFailed("Local-only hotspot failed: $reason")
|
||||
}
|
||||
},
|
||||
Handler(Looper.getMainLooper())
|
||||
)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
reservation?.close()
|
||||
reservation = null
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onStarted(ssid: String)
|
||||
fun onStopped()
|
||||
fun onFailed(reason: String)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.vpnshare.gateway.transport
|
||||
|
||||
import android.hardware.usb.UsbAccessory
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.ParcelFileDescriptor
|
||||
import java.io.Closeable
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class UsbAccessoryTransport(
|
||||
private val usbManager: UsbManager
|
||||
) {
|
||||
fun open(accessory: UsbAccessory): Session? {
|
||||
val descriptor = usbManager.openAccessory(accessory) ?: return null
|
||||
return Session(descriptor)
|
||||
}
|
||||
|
||||
class Session(
|
||||
private val descriptor: ParcelFileDescriptor
|
||||
) : Closeable {
|
||||
val input = FileInputStream(descriptor.fileDescriptor)
|
||||
val output = FileOutputStream(descriptor.fileDescriptor)
|
||||
|
||||
override fun close() {
|
||||
runCatching { input.close() }
|
||||
runCatching { output.close() }
|
||||
descriptor.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user