Initial import: vojo proxy — native Android proxy client
Kotlin + Jetpack Compose VPN proxy client (Shadowsocks AEAD SIP004, SOCKS5 RFC1928/1929, HTTP/HTTPS CONNECT) with per-app whitelist via VpnService and the vendored hev-socks5-tunnel engine (JNI). Includes the post-review fixes to the connection lifecycle, reconnect teardown, journal, app-filter auto-reconnect, and the tab/timeline layout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
c3c10c92d6
599 changed files with 151414 additions and 0 deletions
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Gradle / build output
|
||||
.gradle/
|
||||
build/
|
||||
app/build/
|
||||
**/build/
|
||||
.kotlin/
|
||||
|
||||
# Android / IDE
|
||||
local.properties
|
||||
*.iml
|
||||
.idea/
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
|
||||
# Native build intermediates
|
||||
obj/
|
||||
app/src/main/jni/**/obj/
|
||||
|
||||
# Editor / OS
|
||||
.vscode/
|
||||
.DS_Store
|
||||
|
||||
# Local agent state
|
||||
.claude/
|
||||
|
||||
# Keystores / secrets (never commit signing material)
|
||||
*.jks
|
||||
*.keystore
|
||||
keystore.properties
|
||||
75
README.md
Normal file
75
README.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# vojo proxy
|
||||
|
||||
Нативный Android-клиент для прокси: **Shadowsocks**, **SOCKS5**, **HTTP**, **HTTPS**.
|
||||
Per-app whitelist, минимальный расход батареи, маленький размер (release APK ≈ 2.4 МБ).
|
||||
Дизайн — в стиле мессенджера vojo (Dawn/Fleet: тёмные поверхности, hairline-границы,
|
||||
один фиолетовый акцент, monospace для технических данных).
|
||||
|
||||
## Стек
|
||||
|
||||
- **Kotlin + Jetpack Compose + Material3**, Coroutines, DataStore, kotlinx.serialization.
|
||||
- **VpnService** для создания tun-интерфейса и per-app whitelist (`addAllowedApplication`).
|
||||
- **tun2socks**: нативный движок [`hev-socks5-tunnel`](https://github.com/heiher/hev-socks5-tunnel)
|
||||
(C, event-loop, ~320 КБ) — самый экономичный по CPU/батарее способ конвертировать
|
||||
IP-пакеты из tun в потоки.
|
||||
- minSdk 28, targetSdk 35, только `arm64-v8a` (Samsung S23).
|
||||
|
||||
## Архитектура потока данных
|
||||
|
||||
```
|
||||
Выбранные приложения
|
||||
│ (per-app whitelist через VpnService.addAllowedApplication)
|
||||
▼
|
||||
tun-интерфейс ──fd──► hev-socks5-tunnel (нативный, JNI)
|
||||
│ говорит SOCKS5 на 127.0.0.1:<port>
|
||||
▼
|
||||
LocalSocks5Server (Kotlin, loopback)
|
||||
│ мост на нужный апстрим
|
||||
┌─────────────────────┼─────────────────────┬───────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
Shadowsocks SOCKS5 HTTP CONNECT HTTPS CONNECT
|
||||
(AEAD SIP004) (RFC 1928/1929) (RFC 7231) (TLS + CONNECT)
|
||||
```
|
||||
|
||||
- **TCP** проксируется для всех 4 протоколов. Цель приходит уже как IP/домен в SOCKS5-форме.
|
||||
- **UDP/DNS**: `hev` отдаёт UDP через SOCKS5 UDP ASSOCIATE. Для SS/SOCKS5 — полноценный
|
||||
UDP-релей; для HTTP/HTTPS (UDP не поддерживается протоколом) — DNS-over-TCP fallback (порт 53).
|
||||
- Все апстрим-сокеты защищены `VpnService.protect()`, чтобы не зацикливаться обратно в tun.
|
||||
|
||||
## Ключевые модули
|
||||
|
||||
| Путь | Назначение |
|
||||
|------|-----------|
|
||||
| `core/Tun2Socks.kt`, `jni/tun2socks.c` | JNI-мост к нативному движку |
|
||||
| `core/crypto/ShadowsocksCrypto.kt`, `ShadowsocksStream.kt` | AEAD (HKDF-SHA1, AES-GCM, ChaCha20-Poly1305), TCP-framing, UDP seal/open |
|
||||
| `core/socks/LocalSocks5Server.kt` | Локальный SOCKS5-сервер: CONNECT + UDP ASSOCIATE |
|
||||
| `core/upstream/*` | Апстримы SS / SOCKS5 / HTTP(S) |
|
||||
| `service/ProxyVpnService.kt` | VpnService, whitelist, foreground-уведомление, статистика, журнал событий |
|
||||
| `service/ProxyTileService.kt` | Тайл быстрых настроек (вкл/выкл из шторки) |
|
||||
| `data/ConfigStore.kt`, `AppRepository.kt` | Персист конфигов (DataStore) и список приложений |
|
||||
| `core/ConfigImport.kt` | Импорт ссылок `ss://` (SIP002/legacy), `socks5://`, `http(s)://` |
|
||||
| `ui/*` | Compose-экраны: Туннель (журнал событий на timeline rail), Серверы, Приложения, Опции |
|
||||
|
||||
## Сборка
|
||||
|
||||
```bash
|
||||
./gradlew :app:assembleRelease # → app/build/outputs/apk/release/app-release.apk (~2.4 МБ)
|
||||
./gradlew :app:assembleDebug
|
||||
```
|
||||
|
||||
Требуется Android SDK (platform-35, build-tools 35), NDK 27.2.x. Путь к SDK — в `local.properties`.
|
||||
|
||||
## Оптимизация батареи
|
||||
|
||||
- Нативный event-loop tun2socks вместо userspace TCP/IP-стека на JVM (нет GC-давления на пакет).
|
||||
- Блокирующий I/O на эластичном пуле потоков — простаивающие соединения паркуют поток (0% CPU).
|
||||
- `setUnderlyingNetworks` + отслеживание смены сети, `setMetered(false)`.
|
||||
- Низкоприоритетное foreground-уведомление, поллинг статистики раз в 2 с (без wakelock).
|
||||
- Секундный тикер UI работает только при CONNECTED и только пока активити RESUMED.
|
||||
|
||||
## Безопасность / ограничения
|
||||
|
||||
- Не реализованы VLESS/Reality (намеренно).
|
||||
- Shadowsocks: AEAD-шифры (`aes-128-gcm`, `aes-256-gcm`, `chacha20-ietf-poly1305`).
|
||||
Legacy stream-шифры и SS-2022 не поддерживаются.
|
||||
- HTTP/HTTPS-прокси не могут передавать UDP — для них работает только DNS-over-TCP.
|
||||
102
app/build.gradle.kts
Normal file
102
app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "chat.vojo.proxy"
|
||||
compileSdk = 35
|
||||
ndkVersion = "27.2.12479018"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "chat.vojo.proxy"
|
||||
minSdk = 28
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0.0"
|
||||
|
||||
ndk {
|
||||
abiFilters += "arm64-v8a"
|
||||
}
|
||||
vectorDrawables { useSupportLibrary = true }
|
||||
resourceConfigurations += setOf("en", "ru")
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path = file("src/main/jni/Android.mk")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
// Debug keystore so CI/dev builds are installable; replace for production.
|
||||
storeFile = file(System.getProperty("user.home") + "/.android/debug.keystore")
|
||||
storePassword = "android"
|
||||
keyAlias = "androiddebugkey"
|
||||
keyPassword = "android"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += setOf(
|
||||
"/META-INF/{AL2.0,LGPL2.1}",
|
||||
"DebugProbesKt.bin",
|
||||
"kotlin-tooling-metadata.json",
|
||||
)
|
||||
}
|
||||
// hev needs 16KB-aligned shared libs (kept uncompressed for mmap on modern Android)
|
||||
jniLibs { useLegacyPackaging = false }
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val composeBom = platform("androidx.compose:compose-bom:2024.10.01")
|
||||
implementation(composeBom)
|
||||
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.activity:activity-compose:1.9.3")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
|
||||
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.foundation:foundation")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
|
||||
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
|
||||
}
|
||||
29
app/proguard-rules.pro
vendored
Normal file
29
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Keep JNI bridge — the native symbol name is derived from the full class+method
|
||||
# name, so the class must not be renamed and the native methods must be kept.
|
||||
-keep class chat.vojo.proxy.core.Tun2Socks {
|
||||
native <methods>;
|
||||
*;
|
||||
}
|
||||
|
||||
# Vendored libhev's JNI_OnLoad does FindClass + RegisterNatives on this exact
|
||||
# class/signature at load time; renaming or removing it aborts the process.
|
||||
-keep class hev.htproxy.TProxyService {
|
||||
native <methods>;
|
||||
*;
|
||||
}
|
||||
|
||||
# kotlinx.serialization
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.**
|
||||
-keepclassmembers class **$$serializer { *; }
|
||||
-keepclasseswithmembers,allowshrinking,allowobfuscation class chat.vojo.proxy.** {
|
||||
@kotlinx.serialization.Serializable <methods>;
|
||||
}
|
||||
-if @kotlinx.serialization.Serializable class chat.vojo.proxy.**
|
||||
-keep,allowobfuscation class <1> {
|
||||
static <1>$Companion Companion;
|
||||
*** Companion;
|
||||
}
|
||||
-keepclassmembers class chat.vojo.proxy.** {
|
||||
*** serializer(...);
|
||||
}
|
||||
60
app/src/main/AndroidManifest.xml
Normal file
60
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!-- Needed to enumerate installed apps for the per-app proxy whitelist. -->
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.VojoProxy"
|
||||
tools:targetApi="34">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|uiMode|smallestScreenSize|density"
|
||||
android:theme="@style/Theme.VojoProxy"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.ProxyVpnService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="per-app proxy VPN tunnel" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.ProxyTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_stat_shield"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
93
app/src/main/java/chat/vojo/proxy/MainActivity.kt
Normal file
93
app/src/main/java/chat/vojo/proxy/MainActivity.kt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package chat.vojo.proxy
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.content.ContextCompat
|
||||
import chat.vojo.proxy.service.ProxyVpnService
|
||||
import chat.vojo.proxy.service.VpnState
|
||||
import chat.vojo.proxy.ui.MainScreen
|
||||
import chat.vojo.proxy.ui.MainViewModel
|
||||
import chat.vojo.proxy.ui.theme.VojoTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private val viewModel: MainViewModel by viewModels()
|
||||
private var pendingConnect = false
|
||||
|
||||
private val vpnPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
ProxyVpnService.start(this)
|
||||
} else {
|
||||
// Make the refusal visible instead of silently doing nothing.
|
||||
VpnState.setStopped("Доступ к VPN не разрешён")
|
||||
}
|
||||
}
|
||||
|
||||
private val notificationPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) {
|
||||
// Best-effort; the tunnel works without it.
|
||||
if (pendingConnect) {
|
||||
pendingConnect = false
|
||||
launchVpnFlow()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// The app is dark-only: force light icons even on a light system theme.
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
|
||||
navigationBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
|
||||
)
|
||||
|
||||
setContent {
|
||||
VojoTheme {
|
||||
MainScreen(
|
||||
vm = viewModel,
|
||||
onConnect = ::requestConnect,
|
||||
onDisconnect = { ProxyVpnService.stop(this) },
|
||||
onReconnect = { viewModel.reconnect() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks for POST_NOTIFICATIONS in context — right before the first connect,
|
||||
* when the user understands why the tunnel notification matters.
|
||||
*/
|
||||
private fun requestConnect() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val granted = ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
if (!granted) {
|
||||
pendingConnect = true
|
||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
return
|
||||
}
|
||||
}
|
||||
launchVpnFlow()
|
||||
}
|
||||
|
||||
private fun launchVpnFlow() {
|
||||
val prepare = VpnService.prepare(this)
|
||||
if (prepare != null) {
|
||||
vpnPermissionLauncher.launch(prepare)
|
||||
} else {
|
||||
ProxyVpnService.start(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
126
app/src/main/java/chat/vojo/proxy/core/ConfigImport.kt
Normal file
126
app/src/main/java/chat/vojo/proxy/core/ConfigImport.kt
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
package chat.vojo.proxy.core
|
||||
|
||||
import android.util.Base64
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
|
||||
/**
|
||||
* Parses a proxy share link into a [ProxyConfig]:
|
||||
* - `ss://` SIP002 (`ss://base64url(method:password)@host:port#name`)
|
||||
* - `ss://` legacy (`ss://base64(method:password@host:port)#name`)
|
||||
* - `socks5://[user:pass@]host:port`
|
||||
* - `http(s)://[user:pass@]host:port`
|
||||
* Returns null when the text is not a recognisable link.
|
||||
*/
|
||||
fun parseProxyUri(raw: String): ProxyConfig? {
|
||||
val s = raw.trim()
|
||||
return runCatching {
|
||||
when {
|
||||
s.startsWith("ss://", ignoreCase = true) -> parseSs(s)
|
||||
s.startsWith("socks5://", ignoreCase = true) || s.startsWith("socks://", ignoreCase = true) -> parseUri(s, ProxyType.SOCKS5, 1080)
|
||||
s.startsWith("https://", ignoreCase = true) -> parseUri(s, ProxyType.HTTPS, 443)
|
||||
s.startsWith("http://", ignoreCase = true) -> parseUri(s, ProxyType.HTTP, 8080)
|
||||
else -> null
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* True if [raw] is an `ss://` link that depends on an unsupported transport
|
||||
* plugin (obfs / v2ray-plugin). Lets the UI explain *why* a recognisable link
|
||||
* was rejected instead of falling back to the generic "no link" message.
|
||||
*/
|
||||
fun ssRequiresPlugin(raw: String): Boolean {
|
||||
val s = raw.trim()
|
||||
if (!s.startsWith("ss://", ignoreCase = true)) return false
|
||||
val query = s.substringBefore('#').substringAfter('?', "")
|
||||
return Regex("(^|&)plugin=[^&]+").containsMatchIn(query)
|
||||
}
|
||||
|
||||
private fun decodeBase64(s: String): String? {
|
||||
val flagsList = listOf(
|
||||
Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP,
|
||||
Base64.DEFAULT,
|
||||
)
|
||||
for (flags in flagsList) {
|
||||
runCatching { return String(Base64.decode(s, flags), Charsets.UTF_8) }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun splitHostPort(hostPort: String): Pair<String, Int>? {
|
||||
val hp = hostPort.trim()
|
||||
return if (hp.startsWith("[")) { // [v6]:port
|
||||
val host = hp.substringAfter('[').substringBefore(']')
|
||||
val port = hp.substringAfterLast(':').toIntOrNull() ?: return null
|
||||
host to port
|
||||
} else {
|
||||
val host = hp.substringBeforeLast(':')
|
||||
val port = hp.substringAfterLast(':').toIntOrNull() ?: return null
|
||||
host to port
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseSs(s: String): ProxyConfig? {
|
||||
val name = s.substringAfter('#', "").let { runCatching { URLDecoder.decode(it, "UTF-8") }.getOrDefault(it) }
|
||||
// SIP002 transport plugins (v2ray-plugin, obfs, …) aren't supported — a
|
||||
// config that needs one won't tunnel, so reject it rather than import a
|
||||
// silently-broken server.
|
||||
if (ssRequiresPlugin(s)) return null
|
||||
// The scheme is matched case-insensitively at dispatch, so strip it the same
|
||||
// way — a mixed-case `Ss://` must not survive into the body and break parsing.
|
||||
val body = s.replaceFirst(Regex("^ss://", RegexOption.IGNORE_CASE), "")
|
||||
.substringBefore('#').substringBefore('?').trimEnd('/')
|
||||
|
||||
val method: String
|
||||
val password: String
|
||||
val host: String
|
||||
val port: Int
|
||||
if ('@' in body) {
|
||||
// SIP002: userinfo is base64url(method:password) or plain method:password
|
||||
val userinfo = body.substringBeforeLast('@')
|
||||
val decoded = decodeBase64(userinfo)?.takeIf { ':' in it } ?: userinfo
|
||||
if (':' !in decoded) return null
|
||||
method = decoded.substringBefore(':')
|
||||
password = decoded.substringAfter(':')
|
||||
val hp = splitHostPort(body.substringAfterLast('@')) ?: return null
|
||||
host = hp.first; port = hp.second
|
||||
} else {
|
||||
// Legacy: whole body is base64(method:password@host:port)
|
||||
val decoded = decodeBase64(body) ?: return null
|
||||
if ('@' !in decoded || ':' !in decoded) return null
|
||||
val cred = decoded.substringBeforeLast('@')
|
||||
method = cred.substringBefore(':')
|
||||
password = cred.substringAfter(':')
|
||||
val hp = splitHostPort(decoded.substringAfterLast('@')) ?: return null
|
||||
host = hp.first; port = hp.second
|
||||
}
|
||||
|
||||
val cipher = SsCipher.entries.firstOrNull { it.id.equals(method, ignoreCase = true) } ?: return null
|
||||
if (host.isBlank() || port !in 1..65535) return null
|
||||
return ProxyConfig(
|
||||
name = name.ifBlank { host },
|
||||
type = ProxyType.SHADOWSOCKS,
|
||||
host = host,
|
||||
port = port,
|
||||
password = password,
|
||||
cipher = cipher,
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseUri(s: String, type: ProxyType, defaultPort: Int): ProxyConfig? {
|
||||
val uri = URI(s)
|
||||
val host = uri.host ?: return null
|
||||
val port = if (uri.port > 0) uri.port else defaultPort
|
||||
val user = uri.userInfo?.substringBefore(':').orEmpty()
|
||||
val pass = uri.userInfo?.substringAfter(':', "").orEmpty()
|
||||
val name = uri.fragment?.let { runCatching { URLDecoder.decode(it, "UTF-8") }.getOrDefault(it) }.orEmpty()
|
||||
return ProxyConfig(
|
||||
name = name.ifBlank { host },
|
||||
type = type,
|
||||
host = host,
|
||||
port = port,
|
||||
username = user,
|
||||
password = pass,
|
||||
)
|
||||
}
|
||||
29
app/src/main/java/chat/vojo/proxy/core/Format.kt
Normal file
29
app/src/main/java/chat/vojo/proxy/core/Format.kt
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package chat.vojo.proxy.core
|
||||
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
fun formatBytes(b: Long): String {
|
||||
if (b < 1024) return "$b Б"
|
||||
val units = listOf("КБ", "МБ", "ГБ", "ТБ")
|
||||
var v = b / 1024.0
|
||||
var i = 0
|
||||
while (v >= 1024 && i < units.size - 1) { v /= 1024; i++ }
|
||||
return String.format(Locale.US, "%.1f %s", v, units[i])
|
||||
}
|
||||
|
||||
fun formatUptime(since: Long, nowMs: Long): String {
|
||||
if (since <= 0) return "—"
|
||||
val s = ((nowMs - since) / 1000).coerceAtLeast(0)
|
||||
val h = s / 3600
|
||||
val m = (s % 3600) / 60
|
||||
val sec = s % 60
|
||||
return if (h > 0) String.format(Locale.US, "%d:%02d:%02d", h, m, sec)
|
||||
else String.format(Locale.US, "%02d:%02d", m, sec)
|
||||
}
|
||||
|
||||
/** HH:MM for the timeline rail, matching the design's tabular-nums time column. */
|
||||
fun formatClock(epochMs: Long): String {
|
||||
val c = Calendar.getInstance().apply { timeInMillis = epochMs }
|
||||
return String.format(Locale.US, "%02d:%02d", c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE))
|
||||
}
|
||||
79
app/src/main/java/chat/vojo/proxy/core/ProxyConfig.kt
Normal file
79
app/src/main/java/chat/vojo/proxy/core/ProxyConfig.kt
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package chat.vojo.proxy.core
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
/** Supported upstream proxy protocols. No VLESS/Reality by design. */
|
||||
@Serializable
|
||||
enum class ProxyType {
|
||||
SHADOWSOCKS,
|
||||
SOCKS5,
|
||||
HTTP,
|
||||
HTTPS;
|
||||
|
||||
val label: String
|
||||
get() = when (this) {
|
||||
SHADOWSOCKS -> "Shadowsocks"
|
||||
SOCKS5 -> "SOCKS5"
|
||||
HTTP -> "HTTP"
|
||||
HTTPS -> "HTTPS"
|
||||
}
|
||||
|
||||
/** Two-letter badge used by the avatar tile in the UI. */
|
||||
val badge: String
|
||||
get() = when (this) {
|
||||
SHADOWSOCKS -> "SS"
|
||||
SOCKS5 -> "S5"
|
||||
HTTP -> "HT"
|
||||
HTTPS -> "HS"
|
||||
}
|
||||
}
|
||||
|
||||
/** Shadowsocks AEAD ciphers backed by platform JCA. */
|
||||
@Serializable
|
||||
enum class SsCipher(val id: String, val keySize: Int, val saltSize: Int, val tagSize: Int = 16) {
|
||||
AES_128_GCM("aes-128-gcm", 16, 16),
|
||||
AES_256_GCM("aes-256-gcm", 32, 32),
|
||||
CHACHA20_IETF_POLY1305("chacha20-ietf-poly1305", 32, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* A single saved proxy endpoint. One of these is "active" at a time.
|
||||
*
|
||||
* For HTTP/HTTPS/SOCKS5, [password]/[username] are the proxy credentials (optional).
|
||||
* For Shadowsocks, [password] is the SS password and [cipher] the AEAD method.
|
||||
*/
|
||||
@Serializable
|
||||
data class ProxyConfig(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val name: String,
|
||||
val type: ProxyType,
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val cipher: SsCipher = SsCipher.AES_256_GCM,
|
||||
) {
|
||||
/** `host:port` shown in monospace, mirroring the `@user:server` handle in the design. */
|
||||
val handle: String get() = "$host:$port"
|
||||
|
||||
fun validationError(): String? = when {
|
||||
name.isBlank() -> "Укажите имя"
|
||||
host.isBlank() -> "Укажите адрес сервера"
|
||||
port !in 1..65535 -> "Порт должен быть 1–65535"
|
||||
type == ProxyType.SHADOWSOCKS && password.isEmpty() -> "Укажите пароль Shadowsocks"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/** App-wide settings persisted alongside the server list. */
|
||||
@Serializable
|
||||
data class AppSettings(
|
||||
/** Packages whose traffic is routed through the proxy (whitelist). Empty = all apps routed. */
|
||||
val allowedApps: Set<String> = emptySet(),
|
||||
val activeConfigId: String? = null,
|
||||
val ipv6: Boolean = false,
|
||||
/** DNS server queried by routed apps; sent through the tunnel for privacy. */
|
||||
val dns: String = "1.1.1.1",
|
||||
val mtu: Int = 8500,
|
||||
)
|
||||
28
app/src/main/java/chat/vojo/proxy/core/Tun2Socks.kt
Normal file
28
app/src/main/java/chat/vojo/proxy/core/Tun2Socks.kt
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package chat.vojo.proxy.core
|
||||
|
||||
/**
|
||||
* Kotlin facade over the native hev-socks5-tunnel engine.
|
||||
*
|
||||
* The engine reads IP packets from the VPN tun fd and converts every TCP/UDP
|
||||
* flow into a SOCKS5 connection towards a local address (our [chat.vojo.proxy.core.socks.LocalSocks5Server]).
|
||||
* [nativeStart] blocks for the lifetime of the tunnel, so callers must run it on
|
||||
* a dedicated thread and stop it via [nativeStop].
|
||||
*/
|
||||
object Tun2Socks {
|
||||
init {
|
||||
// libtun2socks depends on libhev-socks5-tunnel; load the dependency first.
|
||||
// libhev's JNI_OnLoad fires on load and RegisterNatives onto the stub
|
||||
// [hev.htproxy.TProxyService] — which must exist or the process aborts.
|
||||
// We then drive the engine through our own C bridge below.
|
||||
System.loadLibrary("hev-socks5-tunnel")
|
||||
System.loadLibrary("tun2socks")
|
||||
}
|
||||
|
||||
/** Blocks until [nativeStop] is called or the engine fails. Returns 0 on clean exit. */
|
||||
external fun nativeStart(config: String, tunFd: Int): Int
|
||||
|
||||
external fun nativeStop()
|
||||
|
||||
/** @return [txPackets, txBytes, rxPackets, rxBytes] as observed by the engine. */
|
||||
external fun nativeStats(): LongArray
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package chat.vojo.proxy.core.crypto
|
||||
|
||||
import chat.vojo.proxy.core.SsCipher
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/**
|
||||
* Shadowsocks AEAD primitives (SIP004).
|
||||
*
|
||||
* Master key derivation, per-session subkey derivation (HKDF-SHA1), and the
|
||||
* AEAD seal/open operations. Implemented entirely on platform JCA so we pull in
|
||||
* no extra crypto dependency. ChaCha20-Poly1305 requires API 28+ (our minSdk).
|
||||
*/
|
||||
object ShadowsocksCrypto {
|
||||
|
||||
private val SUBKEY_INFO = "ss-subkey".toByteArray(Charsets.US_ASCII)
|
||||
|
||||
/**
|
||||
* OpenSSL EVP_BytesToKey(MD5) — the legacy KDF Shadowsocks uses to turn a
|
||||
* password into the master key. No salt, single iteration per block.
|
||||
*/
|
||||
fun deriveMasterKey(password: String, keyLen: Int): ByteArray {
|
||||
val pw = password.toByteArray(Charsets.UTF_8)
|
||||
val md5 = MessageDigest.getInstance("MD5")
|
||||
val out = ByteArray(keyLen)
|
||||
var generated = 0
|
||||
var prev = ByteArray(0)
|
||||
while (generated < keyLen) {
|
||||
md5.reset()
|
||||
md5.update(prev)
|
||||
md5.update(pw)
|
||||
prev = md5.digest()
|
||||
val n = minOf(prev.size, keyLen - generated)
|
||||
System.arraycopy(prev, 0, out, generated, n)
|
||||
generated += n
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** HKDF-SHA1 expand+extract producing [outLen] bytes of session subkey. */
|
||||
fun hkdfSha1(key: ByteArray, salt: ByteArray, info: ByteArray, outLen: Int): ByteArray {
|
||||
val mac = Mac.getInstance("HmacSHA1")
|
||||
|
||||
// Extract
|
||||
mac.init(SecretKeySpec(salt, "HmacSHA1"))
|
||||
val prk = mac.doFinal(key)
|
||||
|
||||
// Expand
|
||||
val out = ByteArray(outLen)
|
||||
var t = ByteArray(0)
|
||||
var generated = 0
|
||||
var counter = 1
|
||||
while (generated < outLen) {
|
||||
mac.reset()
|
||||
mac.init(SecretKeySpec(prk, "HmacSHA1"))
|
||||
mac.update(t)
|
||||
mac.update(info)
|
||||
mac.update(counter.toByte())
|
||||
t = mac.doFinal()
|
||||
val n = minOf(t.size, outLen - generated)
|
||||
System.arraycopy(t, 0, out, generated, n)
|
||||
generated += n
|
||||
counter++
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fun sessionSubkey(masterKey: ByteArray, salt: ByteArray, keyLen: Int): ByteArray =
|
||||
hkdfSha1(masterKey, salt, SUBKEY_INFO, keyLen)
|
||||
|
||||
/** Little-endian increment of the 12-byte AEAD nonce, in place. */
|
||||
fun incrementNonce(nonce: ByteArray) {
|
||||
var i = 0
|
||||
while (i < nonce.size) {
|
||||
val v = (nonce[i].toInt() and 0xff) + 1
|
||||
nonce[i] = v.toByte()
|
||||
if (v <= 0xff) return
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Abstracts the two AEAD families behind one seal/open surface. */
|
||||
sealed class Aead(val cipher: SsCipher) {
|
||||
val keySize: Int get() = cipher.keySize
|
||||
val saltSize: Int get() = cipher.saltSize
|
||||
val tagSize: Int get() = cipher.tagSize
|
||||
|
||||
abstract fun seal(subkey: ByteArray, nonce: ByteArray, plaintext: ByteArray, off: Int, len: Int): ByteArray
|
||||
abstract fun open(subkey: ByteArray, nonce: ByteArray, ciphertext: ByteArray, off: Int, len: Int): ByteArray
|
||||
|
||||
fun seal(subkey: ByteArray, nonce: ByteArray, plaintext: ByteArray): ByteArray =
|
||||
seal(subkey, nonce, plaintext, 0, plaintext.size)
|
||||
|
||||
fun open(subkey: ByteArray, nonce: ByteArray, ciphertext: ByteArray): ByteArray =
|
||||
open(subkey, nonce, ciphertext, 0, ciphertext.size)
|
||||
|
||||
companion object {
|
||||
fun of(cipher: SsCipher): Aead = when (cipher) {
|
||||
SsCipher.AES_128_GCM, SsCipher.AES_256_GCM -> AesGcmAead(cipher)
|
||||
SsCipher.CHACHA20_IETF_POLY1305 -> ChaChaAead(cipher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AesGcmAead(cipher: SsCipher) : Aead(cipher) {
|
||||
// Cipher.getInstance is a provider lookup + allocation; the relay hot path
|
||||
// runs four AEAD ops per 16 KB of traffic, so cache one instance per thread.
|
||||
private val local = ThreadLocal.withInitial { Cipher.getInstance("AES/GCM/NoPadding") }
|
||||
|
||||
override fun seal(subkey: ByteArray, nonce: ByteArray, plaintext: ByteArray, off: Int, len: Int): ByteArray {
|
||||
val c = local.get()!!
|
||||
c.init(Cipher.ENCRYPT_MODE, SecretKeySpec(subkey, "AES"), GCMParameterSpec(tagSize * 8, nonce))
|
||||
return c.doFinal(plaintext, off, len)
|
||||
}
|
||||
|
||||
override fun open(subkey: ByteArray, nonce: ByteArray, ciphertext: ByteArray, off: Int, len: Int): ByteArray {
|
||||
val c = local.get()!!
|
||||
c.init(Cipher.DECRYPT_MODE, SecretKeySpec(subkey, "AES"), GCMParameterSpec(tagSize * 8, nonce))
|
||||
return c.doFinal(ciphertext, off, len)
|
||||
}
|
||||
}
|
||||
|
||||
private class ChaChaAead(cipher: SsCipher) : Aead(cipher) {
|
||||
// "ChaCha20/Poly1305/NoPadding" is registered by Conscrypt since API 28;
|
||||
// the hyphenated alias only exists in newer Mainline Conscrypt builds.
|
||||
private val local = ThreadLocal.withInitial { Cipher.getInstance("ChaCha20/Poly1305/NoPadding") }
|
||||
|
||||
override fun seal(subkey: ByteArray, nonce: ByteArray, plaintext: ByteArray, off: Int, len: Int): ByteArray {
|
||||
val c = local.get()!!
|
||||
c.init(Cipher.ENCRYPT_MODE, SecretKeySpec(subkey, "ChaCha20"), IvParameterSpec(nonce))
|
||||
return c.doFinal(plaintext, off, len)
|
||||
}
|
||||
|
||||
override fun open(subkey: ByteArray, nonce: ByteArray, ciphertext: ByteArray, off: Int, len: Int): ByteArray {
|
||||
val c = local.get()!!
|
||||
c.init(Cipher.DECRYPT_MODE, SecretKeySpec(subkey, "ChaCha20"), IvParameterSpec(nonce))
|
||||
return c.doFinal(ciphertext, off, len)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package chat.vojo.proxy.core.crypto
|
||||
|
||||
import chat.vojo.proxy.core.net.readExact
|
||||
import chat.vojo.proxy.core.net.u16be
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.EOFException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.SecureRandom
|
||||
|
||||
/** Max plaintext bytes per Shadowsocks AEAD chunk (14-bit length field). */
|
||||
private const val MAX_PAYLOAD = 0x3FFF
|
||||
|
||||
private val rng = SecureRandom()
|
||||
|
||||
/**
|
||||
* Encrypts a byte stream into the Shadowsocks AEAD TCP framing:
|
||||
* `[salt][len-chunk][payload-chunk]...`, prepending the salt on the first write.
|
||||
*/
|
||||
class SsOutputStream(
|
||||
private val out: OutputStream,
|
||||
private val aead: Aead,
|
||||
masterKey: ByteArray,
|
||||
) : OutputStream() {
|
||||
|
||||
private val salt = ByteArray(aead.saltSize).also { rng.nextBytes(it) }
|
||||
private val subkey = ShadowsocksCrypto.sessionSubkey(masterKey, salt, aead.keySize)
|
||||
private val nonce = ByteArray(12)
|
||||
private var saltSent = false
|
||||
|
||||
override fun write(b: Int) = write(byteArrayOf(b.toByte()), 0, 1)
|
||||
|
||||
@Synchronized
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
if (len <= 0) return
|
||||
val buf = ByteArrayOutputStream(len + 64)
|
||||
if (!saltSent) {
|
||||
buf.write(salt)
|
||||
saltSent = true
|
||||
}
|
||||
var p = off
|
||||
var rem = len
|
||||
while (rem > 0) {
|
||||
val n = minOf(rem, MAX_PAYLOAD)
|
||||
val lenBytes = byteArrayOf((n ushr 8).toByte(), n.toByte())
|
||||
buf.write(aead.seal(subkey, nonce, lenBytes))
|
||||
ShadowsocksCrypto.incrementNonce(nonce)
|
||||
buf.write(aead.seal(subkey, nonce, b, p, n))
|
||||
ShadowsocksCrypto.incrementNonce(nonce)
|
||||
p += n
|
||||
rem -= n
|
||||
}
|
||||
out.write(buf.toByteArray())
|
||||
}
|
||||
|
||||
override fun flush() = out.flush()
|
||||
override fun close() = out.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a Shadowsocks AEAD TCP stream back into plaintext. Reads the salt
|
||||
* lazily on first access. A clean upstream close surfaces as normal EOF (-1).
|
||||
*/
|
||||
class SsInputStream(
|
||||
private val inp: InputStream,
|
||||
private val aead: Aead,
|
||||
private val masterKey: ByteArray,
|
||||
) : InputStream() {
|
||||
|
||||
private var subkey: ByteArray? = null
|
||||
private val nonce = ByteArray(12)
|
||||
private var plain: ByteArray = ByteArray(0)
|
||||
private var plainPos = 0
|
||||
|
||||
private fun fill(): Boolean {
|
||||
try {
|
||||
if (subkey == null) {
|
||||
val salt = inp.readExact(aead.saltSize)
|
||||
subkey = ShadowsocksCrypto.sessionSubkey(masterKey, salt, aead.keySize)
|
||||
}
|
||||
val key = subkey!!
|
||||
val encLen = inp.readExact(2 + aead.tagSize)
|
||||
val lenPlain = aead.open(key, nonce, encLen)
|
||||
ShadowsocksCrypto.incrementNonce(nonce)
|
||||
val payloadLen = u16be(lenPlain[0].toInt(), lenPlain[1].toInt())
|
||||
if (payloadLen <= 0 || payloadLen > MAX_PAYLOAD) {
|
||||
throw IOException("invalid shadowsocks chunk length $payloadLen")
|
||||
}
|
||||
val encPayload = inp.readExact(payloadLen + aead.tagSize)
|
||||
plain = aead.open(key, nonce, encPayload)
|
||||
ShadowsocksCrypto.incrementNonce(nonce)
|
||||
plainPos = 0
|
||||
return true
|
||||
} catch (e: EOFException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int {
|
||||
if (len <= 0) return 0
|
||||
while (plainPos >= plain.size) {
|
||||
if (!fill()) return -1
|
||||
}
|
||||
val n = minOf(len, plain.size - plainPos)
|
||||
System.arraycopy(plain, plainPos, b, off, n)
|
||||
plainPos += n
|
||||
return n
|
||||
}
|
||||
|
||||
override fun read(): Int {
|
||||
val one = ByteArray(1)
|
||||
val n = read(one, 0, 1)
|
||||
return if (n <= 0) -1 else one[0].toInt() and 0xff
|
||||
}
|
||||
|
||||
override fun close() = inp.close()
|
||||
}
|
||||
|
||||
/** Seals a single Shadowsocks UDP datagram: `[salt][AEAD(payload, nonce=0)]`. */
|
||||
fun ssUdpSeal(aead: Aead, masterKey: ByteArray, plaintext: ByteArray, off: Int, len: Int): ByteArray {
|
||||
val salt = ByteArray(aead.saltSize).also { rng.nextBytes(it) }
|
||||
val subkey = ShadowsocksCrypto.sessionSubkey(masterKey, salt, aead.keySize)
|
||||
val ct = aead.seal(subkey, ByteArray(12), plaintext, off, len)
|
||||
return salt + ct
|
||||
}
|
||||
|
||||
/** Opens a single Shadowsocks UDP datagram; returns the decrypted `[ADDR][PAYLOAD]`. */
|
||||
fun ssUdpOpen(aead: Aead, masterKey: ByteArray, packet: ByteArray, off: Int, len: Int): ByteArray {
|
||||
require(len >= aead.saltSize + aead.tagSize) { "short ss udp packet" }
|
||||
val salt = packet.copyOfRange(off, off + aead.saltSize)
|
||||
val subkey = ShadowsocksCrypto.sessionSubkey(masterKey, salt, aead.keySize)
|
||||
return aead.open(subkey, ByteArray(12), packet, off + aead.saltSize, len - aead.saltSize)
|
||||
}
|
||||
86
app/src/main/java/chat/vojo/proxy/core/net/SocksAddress.kt
Normal file
86
app/src/main/java/chat/vojo/proxy/core/net/SocksAddress.kt
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package chat.vojo.proxy.core.net
|
||||
|
||||
import java.io.InputStream
|
||||
import java.net.InetAddress
|
||||
|
||||
const val ATYP_IPV4 = 0x01
|
||||
const val ATYP_DOMAIN = 0x03
|
||||
const val ATYP_IPV6 = 0x04
|
||||
|
||||
/**
|
||||
* A connection target in SOCKS5 address form. [raw] is the exact wire encoding
|
||||
* `[ATYP][ADDR][PORT]`, reusable verbatim as a Shadowsocks address header or an
|
||||
* upstream SOCKS5 request body.
|
||||
*/
|
||||
class Destination(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val atyp: Int,
|
||||
val raw: ByteArray,
|
||||
) {
|
||||
override fun toString(): String = if (atyp == ATYP_IPV6) "[$host]:$port" else "$host:$port"
|
||||
}
|
||||
|
||||
/** Parses `[ATYP][ADDR][PORT]` from [input] (already positioned at ATYP). */
|
||||
fun readSocksAddress(input: InputStream): Destination {
|
||||
val atyp = input.read()
|
||||
if (atyp < 0) throw java.io.EOFException("eof reading atyp")
|
||||
val host: String
|
||||
val addrBytes: ByteArray
|
||||
when (atyp) {
|
||||
ATYP_IPV4 -> {
|
||||
addrBytes = input.readExact(4)
|
||||
host = InetAddress.getByAddress(addrBytes).hostAddress!!
|
||||
}
|
||||
ATYP_DOMAIN -> {
|
||||
val len = input.read()
|
||||
if (len < 0) throw java.io.EOFException("eof reading domain len")
|
||||
addrBytes = ByteArray(1 + len)
|
||||
addrBytes[0] = len.toByte()
|
||||
input.readFully(addrBytes, 1, len)
|
||||
host = String(addrBytes, 1, len, Charsets.US_ASCII)
|
||||
}
|
||||
ATYP_IPV6 -> {
|
||||
addrBytes = input.readExact(16)
|
||||
host = InetAddress.getByAddress(addrBytes).hostAddress!!
|
||||
}
|
||||
else -> throw IllegalArgumentException("unknown atyp $atyp")
|
||||
}
|
||||
val portBytes = input.readExact(2)
|
||||
val port = u16be(portBytes[0].toInt(), portBytes[1].toInt())
|
||||
|
||||
val raw = ByteArray(1 + addrBytes.size + 2)
|
||||
raw[0] = atyp.toByte()
|
||||
System.arraycopy(addrBytes, 0, raw, 1, addrBytes.size)
|
||||
raw[raw.size - 2] = portBytes[0]
|
||||
raw[raw.size - 1] = portBytes[1]
|
||||
return Destination(host, port, atyp, raw)
|
||||
}
|
||||
|
||||
/** Parses a SOCKS5 address from a raw datagram payload starting at [off]; returns dest + header length. */
|
||||
fun parseSocksAddress(data: ByteArray, off: Int): Pair<Destination, Int> {
|
||||
var p = off
|
||||
val atyp = data[p++].toInt() and 0xff
|
||||
val host: String
|
||||
when (atyp) {
|
||||
ATYP_IPV4 -> {
|
||||
host = InetAddress.getByAddress(data.copyOfRange(p, p + 4)).hostAddress!!
|
||||
p += 4
|
||||
}
|
||||
ATYP_DOMAIN -> {
|
||||
val len = data[p++].toInt() and 0xff
|
||||
host = String(data, p, len, Charsets.US_ASCII)
|
||||
p += len
|
||||
}
|
||||
ATYP_IPV6 -> {
|
||||
host = InetAddress.getByAddress(data.copyOfRange(p, p + 16)).hostAddress!!
|
||||
p += 16
|
||||
}
|
||||
else -> throw IllegalArgumentException("unknown atyp $atyp")
|
||||
}
|
||||
val port = u16be(data[p].toInt(), data[p + 1].toInt())
|
||||
p += 2
|
||||
val headerLen = p - off
|
||||
val raw = data.copyOfRange(off, off + headerLen)
|
||||
return Destination(host, port, atyp, raw) to headerLen
|
||||
}
|
||||
23
app/src/main/java/chat/vojo/proxy/core/net/StreamUtils.kt
Normal file
23
app/src/main/java/chat/vojo/proxy/core/net/StreamUtils.kt
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package chat.vojo.proxy.core.net
|
||||
|
||||
import java.io.EOFException
|
||||
import java.io.InputStream
|
||||
|
||||
/** Reads exactly [len] bytes into [buf] starting at [off] or throws [EOFException]. */
|
||||
fun InputStream.readFully(buf: ByteArray, off: Int = 0, len: Int = buf.size) {
|
||||
var read = 0
|
||||
while (read < len) {
|
||||
val n = read(buf, off + read, len - read)
|
||||
if (n < 0) throw EOFException("stream closed after $read/$len bytes")
|
||||
read += n
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads exactly [len] bytes into a fresh array. */
|
||||
fun InputStream.readExact(len: Int): ByteArray {
|
||||
val b = ByteArray(len)
|
||||
readFully(b, 0, len)
|
||||
return b
|
||||
}
|
||||
|
||||
fun u16be(hi: Int, lo: Int): Int = ((hi and 0xff) shl 8) or (lo and 0xff)
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
package chat.vojo.proxy.core.socks
|
||||
|
||||
import android.util.Log
|
||||
import chat.vojo.proxy.core.net.parseSocksAddress
|
||||
import chat.vojo.proxy.core.net.readExact
|
||||
import chat.vojo.proxy.core.net.readFully
|
||||
import chat.vojo.proxy.core.net.readSocksAddress
|
||||
import chat.vojo.proxy.core.net.u16be
|
||||
import chat.vojo.proxy.core.upstream.UdpAssociation
|
||||
import chat.vojo.proxy.core.upstream.Upstream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.Closeable
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.net.SocketAddress
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
private const val TAG = "LocalSocks5"
|
||||
private const val RELAY_BUFFER = 32 * 1024
|
||||
private const val SOCKS_VERSION = 0x05
|
||||
private const val CMD_CONNECT = 0x01
|
||||
private const val CMD_UDP_ASSOCIATE = 0x03
|
||||
private const val DNS_TCP_TIMEOUT_MS = 10_000
|
||||
|
||||
/**
|
||||
* A loopback SOCKS5 server the native tun2socks engine connects to. Each inbound
|
||||
* SOCKS5 connection is bridged to the configured [upstream] proxy. Supports TCP
|
||||
* CONNECT and UDP ASSOCIATE (with a DNS-over-TCP fallback for HTTP upstreams).
|
||||
*/
|
||||
class LocalSocks5Server(
|
||||
private val upstream: Upstream,
|
||||
private val onUpstreamError: (() -> Unit)? = null,
|
||||
) {
|
||||
private val serverSocket = ServerSocket().apply {
|
||||
reuseAddress = true
|
||||
bind(InetSocketAddress("127.0.0.1", 0), 256)
|
||||
}
|
||||
|
||||
/** Ephemeral loopback port the engine should target. */
|
||||
val port: Int get() = serverSocket.localPort
|
||||
|
||||
// Elastic IO threads; blocking reads park their thread, so idle costs nothing.
|
||||
private val dispatcher = Dispatchers.IO.limitedParallelism(512)
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
private val open = ConcurrentHashMap.newKeySet<Closeable>()
|
||||
|
||||
fun start() {
|
||||
scope.launch { acceptLoop() }
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
runCatching { serverSocket.close() }
|
||||
open.forEach { runCatching { it.close() } }
|
||||
open.clear()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private fun track(c: Closeable) { open.add(c) }
|
||||
private fun untrack(c: Closeable) { open.remove(c) }
|
||||
|
||||
private suspend fun acceptLoop() {
|
||||
while (scope.isActive) {
|
||||
val client = try {
|
||||
serverSocket.accept()
|
||||
} catch (e: Exception) {
|
||||
// Transient failures (e.g. EMFILE) must not kill the listener;
|
||||
// only a closed server socket ends the loop.
|
||||
if (serverSocket.isClosed || !scope.isActive) break
|
||||
Log.w(TAG, "accept failed: ${e.message}")
|
||||
delay(100)
|
||||
continue
|
||||
}
|
||||
scope.launch { handleClient(client) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleClient(client: Socket) {
|
||||
track(client)
|
||||
try {
|
||||
client.tcpNoDelay = true
|
||||
val cin = BufferedInputStream(client.getInputStream())
|
||||
val cout = client.getOutputStream()
|
||||
|
||||
// Greeting: VER, NMETHODS, METHODS...
|
||||
if (cin.read() != SOCKS_VERSION) return
|
||||
val nMethods = cin.read()
|
||||
if (nMethods < 0) return
|
||||
cin.readFully(ByteArray(nMethods))
|
||||
cout.write(byteArrayOf(SOCKS_VERSION.toByte(), 0x00)) // no-auth
|
||||
cout.flush()
|
||||
|
||||
// Request: VER, CMD, RSV, [ATYP+ADDR+PORT]
|
||||
val reqHead = cin.readExact(3)
|
||||
if (reqHead[0].toInt() != SOCKS_VERSION) return
|
||||
val cmd = reqHead[1].toInt() and 0xff
|
||||
val dest = try {
|
||||
readSocksAddress(cin)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
runCatching { writeReply(cout, rep = 0x08) } // address type not supported
|
||||
return
|
||||
}
|
||||
|
||||
when (cmd) {
|
||||
CMD_CONNECT -> handleConnect(client, cin, cout, dest)
|
||||
CMD_UDP_ASSOCIATE -> handleUdpAssociate(client, cin, cout)
|
||||
else -> writeReply(cout, rep = 0x07) // command not supported
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// fallthrough to cleanup
|
||||
} finally {
|
||||
closeQuietly(client)
|
||||
untrack(client)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleConnect(
|
||||
client: Socket,
|
||||
cin: InputStream,
|
||||
cout: OutputStream,
|
||||
dest: chat.vojo.proxy.core.net.Destination,
|
||||
) {
|
||||
val conn = try {
|
||||
upstream.connectTcp(dest)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "upstream connect failed for $dest: ${e.message}")
|
||||
onUpstreamError?.invoke()
|
||||
runCatching { writeReply(cout, rep = 0x05) } // connection refused
|
||||
return
|
||||
}
|
||||
track(conn)
|
||||
try {
|
||||
writeReply(cout, rep = 0x00) // success
|
||||
|
||||
// Half-close aware relay: EOF in one direction only shuts down the
|
||||
// peer's write side (hev forwards SHUT_WR from apps); the connection
|
||||
// dies once both directions finish or anything errors out.
|
||||
val finished = AtomicInteger(0)
|
||||
val closeBoth = {
|
||||
closeQuietly(client)
|
||||
runCatching { conn.close() }
|
||||
Unit
|
||||
}
|
||||
val doneOne = { clean: Boolean, shutdownPeer: () -> Unit ->
|
||||
if (clean) {
|
||||
// SSLSocket can't half-close — fall back to full close.
|
||||
runCatching { shutdownPeer() }.onFailure { closeBoth() }
|
||||
if (finished.incrementAndGet() >= 2) closeBoth()
|
||||
} else closeBoth()
|
||||
}
|
||||
val up = scope.launch {
|
||||
val clean = runCatching { pump(cin, conn.output) }.isSuccess
|
||||
doneOne(clean) { conn.shutdownOutput() }
|
||||
}
|
||||
val down = scope.launch {
|
||||
val clean = runCatching { pump(conn.input, cout) }.isSuccess
|
||||
doneOne(clean) { cout.flush(); client.shutdownOutput() }
|
||||
}
|
||||
up.join()
|
||||
down.join()
|
||||
} finally {
|
||||
runCatching { conn.close() }
|
||||
untrack(conn)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUdpAssociate(client: Socket, cin: InputStream, cout: OutputStream) {
|
||||
val relay = DatagramSocket(InetSocketAddress("127.0.0.1", 0))
|
||||
track(relay)
|
||||
val session = UdpSession(relay)
|
||||
try {
|
||||
// Tell the engine where to send UDP: 127.0.0.1:relayPort
|
||||
val rp = relay.localPort
|
||||
cout.write(
|
||||
byteArrayOf(
|
||||
SOCKS_VERSION.toByte(), 0x00, 0x00, 0x01,
|
||||
127, 0, 0, 1,
|
||||
(rp ushr 8).toByte(), rp.toByte(),
|
||||
)
|
||||
)
|
||||
cout.flush()
|
||||
session.start()
|
||||
|
||||
// Hold the association open until the engine closes the control connection.
|
||||
val drain = ByteArray(512)
|
||||
while (cin.read(drain) >= 0) { /* keep-alive */ }
|
||||
} catch (e: Exception) {
|
||||
// fallthrough to cleanup
|
||||
} finally {
|
||||
session.stop()
|
||||
closeQuietly(relay)
|
||||
untrack(relay)
|
||||
}
|
||||
}
|
||||
|
||||
/** Bridges UDP datagrams between the engine's relay socket and the upstream proxy. */
|
||||
private inner class UdpSession(private val relay: DatagramSocket) {
|
||||
private val assoc: UdpAssociation? = runCatching { upstream.openUdpAssociation() }.getOrNull()
|
||||
private val jobs = mutableListOf<Job>()
|
||||
@Volatile private var engineSource: SocketAddress? = null
|
||||
@Volatile private var running = true
|
||||
|
||||
fun start() {
|
||||
assoc?.let { track(it) }
|
||||
jobs += scope.launch { forwardLoop() }
|
||||
if (assoc != null) jobs += scope.launch { replyLoop() }
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
running = false
|
||||
assoc?.let { runCatching { it.close() }; untrack(it) }
|
||||
jobs.forEach { it.cancel() }
|
||||
}
|
||||
|
||||
private fun forwardLoop() {
|
||||
val buf = ByteArray(64 * 1024)
|
||||
while (running) {
|
||||
val dp = DatagramPacket(buf, buf.size)
|
||||
try { relay.receive(dp) } catch (e: Exception) { break }
|
||||
if (engineSource == null) {
|
||||
engineSource = dp.socketAddress
|
||||
// Lock the relay to the engine's source so another local app
|
||||
// can't inject datagrams or hijack the reply destination.
|
||||
runCatching { relay.connect(dp.socketAddress) }
|
||||
} else {
|
||||
engineSource = dp.socketAddress
|
||||
}
|
||||
try {
|
||||
// [RSV(2)][FRAG(1)][ATYP+ADDR+PORT][DATA]
|
||||
if (dp.length < 7 || buf[2].toInt() != 0) continue // FRAG unsupported
|
||||
val (dest, headerLen) = parseSocksAddress(buf, 3)
|
||||
val dataStart = 3 + headerLen
|
||||
val dataLen = dp.length - dataStart
|
||||
if (dataLen < 0) continue
|
||||
if (assoc != null) {
|
||||
assoc.send(dest, buf, dataStart, dataLen)
|
||||
} else if (dest.port == 53) {
|
||||
val q = buf.copyOfRange(dataStart, dp.length)
|
||||
scope.launch { dnsOverTcp(dest, q) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// malformed datagram; skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun replyLoop() {
|
||||
val a = assoc ?: return
|
||||
while (running) {
|
||||
val reply = try { a.receive() } catch (e: Exception) { break } ?: break
|
||||
sendToEngine(reply.from.raw, reply.data)
|
||||
}
|
||||
}
|
||||
|
||||
/** HTTP-upstream DNS fallback: tunnel a single query as DNS-over-TCP (RFC 7766). */
|
||||
private fun dnsOverTcp(dest: chat.vojo.proxy.core.net.Destination, query: ByteArray) {
|
||||
val conn = try { upstream.connectTcp(dest) } catch (e: Exception) { return }
|
||||
track(conn)
|
||||
try {
|
||||
conn.socket.soTimeout = DNS_TCP_TIMEOUT_MS
|
||||
val out = conn.output
|
||||
out.write((query.size ushr 8) and 0xff)
|
||||
out.write(query.size and 0xff)
|
||||
out.write(query)
|
||||
out.flush()
|
||||
val lenHdr = conn.input.readExact(2)
|
||||
val rlen = u16be(lenHdr[0].toInt(), lenHdr[1].toInt())
|
||||
if (rlen <= 0 || rlen > 65535) return
|
||||
val resp = conn.input.readExact(rlen)
|
||||
sendToEngine(dest.raw, resp)
|
||||
} catch (e: Exception) {
|
||||
// drop on failure
|
||||
} finally {
|
||||
runCatching { conn.close() }
|
||||
untrack(conn)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendToEngine(addrRaw: ByteArray, data: ByteArray) {
|
||||
val src = engineSource ?: return
|
||||
val pkt = ByteArray(3 + addrRaw.size + data.size)
|
||||
System.arraycopy(addrRaw, 0, pkt, 3, addrRaw.size)
|
||||
System.arraycopy(data, 0, pkt, 3 + addrRaw.size, data.size)
|
||||
runCatching { relay.send(DatagramPacket(pkt, pkt.size, src)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeReply(out: OutputStream, rep: Int) {
|
||||
// VER, REP, RSV, ATYP=IPv4, BND.ADDR=0.0.0.0, BND.PORT=0
|
||||
out.write(byteArrayOf(SOCKS_VERSION.toByte(), rep.toByte(), 0x00, 0x01, 0, 0, 0, 0, 0, 0))
|
||||
out.flush()
|
||||
}
|
||||
|
||||
private fun pump(input: InputStream, output: OutputStream) {
|
||||
val buf = ByteArray(RELAY_BUFFER)
|
||||
while (true) {
|
||||
val n = input.read(buf)
|
||||
if (n < 0) break
|
||||
if (n > 0) {
|
||||
output.write(buf, 0, n)
|
||||
output.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeQuietly(c: Closeable) {
|
||||
runCatching { c.close() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package chat.vojo.proxy.core.upstream
|
||||
|
||||
import chat.vojo.proxy.core.ProxyConfig
|
||||
import chat.vojo.proxy.core.ProxyType
|
||||
import chat.vojo.proxy.core.net.ATYP_IPV6
|
||||
import chat.vojo.proxy.core.net.Destination
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.Socket
|
||||
import java.util.Base64
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
|
||||
/** Upstream that tunnels through an HTTP(S) proxy using the CONNECT method. */
|
||||
class HttpUpstream(
|
||||
private val cfg: ProxyConfig,
|
||||
private val protector: SocketProtector,
|
||||
) : Upstream {
|
||||
|
||||
private val tls: Boolean get() = cfg.type == ProxyType.HTTPS
|
||||
|
||||
override fun connectTcp(dest: Destination): UpstreamConn {
|
||||
var socket = protector.connectedSocket(cfg.host, cfg.port)
|
||||
try {
|
||||
socket.soTimeout = HANDSHAKE_TIMEOUT_MS
|
||||
if (tls) {
|
||||
val factory = SSLSocketFactory.getDefault() as SSLSocketFactory
|
||||
val ssl = factory.createSocket(socket, cfg.host, cfg.port, true) as SSLSocket
|
||||
ssl.soTimeout = HANDSHAKE_TIMEOUT_MS
|
||||
// Raw SSLSocket validates the chain but NOT the hostname; without
|
||||
// this a valid cert for any domain could impersonate the proxy.
|
||||
val params = ssl.sslParameters
|
||||
params.endpointIdentificationAlgorithm = "HTTPS"
|
||||
ssl.sslParameters = params
|
||||
ssl.startHandshake()
|
||||
socket = ssl
|
||||
}
|
||||
val out = socket.getOutputStream()
|
||||
val inp = BufferedInputStream(socket.getInputStream())
|
||||
|
||||
val authority = if (dest.atyp == ATYP_IPV6) "[${dest.host}]:${dest.port}" else "${dest.host}:${dest.port}"
|
||||
val sb = StringBuilder()
|
||||
sb.append("CONNECT ").append(authority).append(" HTTP/1.1\r\n")
|
||||
sb.append("Host: ").append(authority).append("\r\n")
|
||||
if (cfg.username.isNotEmpty()) {
|
||||
val token = Base64.getEncoder()
|
||||
.encodeToString("${cfg.username}:${cfg.password}".toByteArray(Charsets.UTF_8))
|
||||
sb.append("Proxy-Authorization: Basic ").append(token).append("\r\n")
|
||||
}
|
||||
sb.append("Proxy-Connection: keep-alive\r\n")
|
||||
sb.append("\r\n")
|
||||
out.write(sb.toString().toByteArray(Charsets.US_ASCII))
|
||||
out.flush()
|
||||
|
||||
val status = readStatusLine(inp)
|
||||
if (status !in 200..299) throw IOException("http proxy CONNECT failed: $status")
|
||||
|
||||
socket.soTimeout = 0
|
||||
return UpstreamConn(socket, inp, out)
|
||||
} catch (e: Throwable) {
|
||||
runCatching { socket.close() }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads the response head up to the blank line and returns the status code. */
|
||||
private fun readStatusLine(inp: InputStream): Int {
|
||||
val head = StringBuilder()
|
||||
var last3 = 0 // rolling match for "\r\n\r\n"
|
||||
while (true) {
|
||||
val c = inp.read()
|
||||
if (c < 0) throw IOException("eof during http proxy response")
|
||||
head.append(c.toChar())
|
||||
last3 = when {
|
||||
c == '\r'.code -> if (last3 == 2) 3 else 1
|
||||
c == '\n'.code -> if (last3 == 1) 2 else if (last3 == 3) 4 else 0
|
||||
else -> 0
|
||||
}
|
||||
if (last3 == 4) break // saw \r\n\r\n
|
||||
if (head.length > 16 * 1024) throw IOException("http proxy response head too large")
|
||||
}
|
||||
val firstLine = head.lineSequence().firstOrNull()?.trim().orEmpty()
|
||||
val parts = firstLine.split(' ')
|
||||
return parts.getOrNull(1)?.toIntOrNull() ?: throw IOException("malformed status line: $firstLine")
|
||||
}
|
||||
|
||||
/** HTTP proxies can't carry UDP; the local server falls back to DNS-over-TCP. */
|
||||
override fun openUdpAssociation(): UdpAssociation? = null
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package chat.vojo.proxy.core.upstream
|
||||
|
||||
import chat.vojo.proxy.core.ProxyConfig
|
||||
import chat.vojo.proxy.core.crypto.Aead
|
||||
import chat.vojo.proxy.core.crypto.ShadowsocksCrypto
|
||||
import chat.vojo.proxy.core.crypto.SsInputStream
|
||||
import chat.vojo.proxy.core.crypto.SsOutputStream
|
||||
import chat.vojo.proxy.core.crypto.ssUdpOpen
|
||||
import chat.vojo.proxy.core.crypto.ssUdpSeal
|
||||
import chat.vojo.proxy.core.net.Destination
|
||||
import chat.vojo.proxy.core.net.parseSocksAddress
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
/** Upstream that tunnels through a Shadowsocks AEAD server (SIP004). */
|
||||
class ShadowsocksUpstream(
|
||||
private val cfg: ProxyConfig,
|
||||
private val protector: SocketProtector,
|
||||
) : Upstream {
|
||||
|
||||
private val aead: Aead = Aead.of(cfg.cipher)
|
||||
private val masterKey: ByteArray = ShadowsocksCrypto.deriveMasterKey(cfg.password, cfg.cipher.keySize)
|
||||
|
||||
override fun connectTcp(dest: Destination): UpstreamConn {
|
||||
val socket = protector.connectedSocket(cfg.host, cfg.port)
|
||||
try {
|
||||
val ssOut = SsOutputStream(socket.getOutputStream(), aead, masterKey)
|
||||
val ssIn = SsInputStream(socket.getInputStream(), aead, masterKey)
|
||||
// First plaintext bytes are the target address header.
|
||||
ssOut.write(dest.raw)
|
||||
ssOut.flush()
|
||||
return UpstreamConn(socket, ssIn, ssOut)
|
||||
} catch (e: Throwable) {
|
||||
runCatching { socket.close() }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun openUdpAssociation(): UdpAssociation = ShadowsocksUdpAssociation(cfg, protector, aead, masterKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shadowsocks UDP relay. One socket to the SS server carries every destination;
|
||||
* each datagram is independently `[salt][AEAD([ADDR][DATA], nonce=0)]`.
|
||||
*/
|
||||
private class ShadowsocksUdpAssociation(
|
||||
cfg: ProxyConfig,
|
||||
protector: SocketProtector,
|
||||
private val aead: Aead,
|
||||
private val masterKey: ByteArray,
|
||||
) : UdpAssociation {
|
||||
|
||||
private val server = InetSocketAddress(InetAddress.getByName(cfg.host), cfg.port)
|
||||
private val udp = DatagramSocket().also { protector.protect(it) }
|
||||
|
||||
override fun send(dest: Destination, data: ByteArray, off: Int, len: Int) {
|
||||
val plain = ByteArray(dest.raw.size + len)
|
||||
System.arraycopy(dest.raw, 0, plain, 0, dest.raw.size)
|
||||
System.arraycopy(data, off, plain, dest.raw.size, len)
|
||||
val sealed = ssUdpSeal(aead, masterKey, plain, 0, plain.size)
|
||||
udp.send(DatagramPacket(sealed, sealed.size, server))
|
||||
}
|
||||
|
||||
override fun receive(): UdpReply? {
|
||||
val buf = ByteArray(64 * 1024)
|
||||
// Skip undecryptable/malformed datagrams (spoofed or corrupt) instead of
|
||||
// letting one bad packet kill the reply loop.
|
||||
while (true) {
|
||||
val dp = DatagramPacket(buf, buf.size)
|
||||
udp.receive(dp)
|
||||
try {
|
||||
val plain = ssUdpOpen(aead, masterKey, buf, 0, dp.length)
|
||||
val (dest, headerLen) = parseSocksAddress(plain, 0)
|
||||
return UdpReply(dest, plain.copyOfRange(headerLen, plain.size))
|
||||
} catch (e: Exception) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
runCatching { udp.close() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
package chat.vojo.proxy.core.upstream
|
||||
|
||||
import chat.vojo.proxy.core.ProxyConfig
|
||||
import chat.vojo.proxy.core.net.ATYP_DOMAIN
|
||||
import chat.vojo.proxy.core.net.ATYP_IPV4
|
||||
import chat.vojo.proxy.core.net.ATYP_IPV6
|
||||
import chat.vojo.proxy.core.net.Destination
|
||||
import chat.vojo.proxy.core.net.readExact
|
||||
import chat.vojo.proxy.core.net.readFully
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
/** Upstream that speaks standard SOCKS5 (RFC 1928 / 1929) to the proxy server. */
|
||||
class Socks5Upstream(
|
||||
private val cfg: ProxyConfig,
|
||||
private val protector: SocketProtector,
|
||||
) : Upstream {
|
||||
|
||||
private val auth: Boolean get() = cfg.username.isNotEmpty()
|
||||
|
||||
override fun connectTcp(dest: Destination): UpstreamConn {
|
||||
val socket = protector.connectedSocket(cfg.host, cfg.port)
|
||||
try {
|
||||
socket.soTimeout = HANDSHAKE_TIMEOUT_MS
|
||||
val out = socket.getOutputStream()
|
||||
val inp = socket.getInputStream()
|
||||
negotiate(inp, out)
|
||||
|
||||
// CONNECT request: VER=5, CMD=1, RSV=0, then [ATYP][ADDR][PORT]
|
||||
val req = ByteArray(3 + dest.raw.size)
|
||||
req[0] = 0x05
|
||||
req[1] = 0x01
|
||||
req[2] = 0x00
|
||||
System.arraycopy(dest.raw, 0, req, 3, dest.raw.size)
|
||||
out.write(req)
|
||||
out.flush()
|
||||
readReply(inp)
|
||||
|
||||
socket.soTimeout = 0
|
||||
return UpstreamConn(socket, inp, out)
|
||||
} catch (e: Throwable) {
|
||||
runCatching { socket.close() }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun negotiate(inp: InputStream, out: OutputStream) {
|
||||
if (auth) {
|
||||
out.write(byteArrayOf(0x05, 0x02, 0x00, 0x02))
|
||||
} else {
|
||||
out.write(byteArrayOf(0x05, 0x01, 0x00))
|
||||
}
|
||||
out.flush()
|
||||
val sel = inp.readExact(2)
|
||||
if (sel[0].toInt() != 0x05) throw IOException("bad socks5 version ${sel[0]}")
|
||||
when (sel[1].toInt() and 0xff) {
|
||||
0x00 -> {} // no auth
|
||||
0x02 -> userPassAuth(inp, out)
|
||||
0xFF -> throw IOException("socks5 proxy rejected auth methods")
|
||||
else -> throw IOException("unsupported socks5 method ${sel[1]}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun userPassAuth(inp: InputStream, out: OutputStream) {
|
||||
val u = cfg.username.toByteArray(Charsets.UTF_8)
|
||||
val p = cfg.password.toByteArray(Charsets.UTF_8)
|
||||
// RFC 1929 encodes ULEN/PLEN as a single byte each — refuse anything that
|
||||
// would overflow rather than silently truncate the length prefix.
|
||||
if (u.size > 255 || p.size > 255) throw IOException("socks5 credentials exceed 255 bytes")
|
||||
val msg = ByteArray(3 + u.size + p.size)
|
||||
msg[0] = 0x01
|
||||
msg[1] = u.size.toByte()
|
||||
System.arraycopy(u, 0, msg, 2, u.size)
|
||||
msg[2 + u.size] = p.size.toByte()
|
||||
System.arraycopy(p, 0, msg, 3 + u.size, p.size)
|
||||
out.write(msg)
|
||||
out.flush()
|
||||
val res = inp.readExact(2)
|
||||
if (res[1].toInt() != 0x00) throw IOException("socks5 auth failed")
|
||||
}
|
||||
|
||||
private fun readReply(inp: InputStream) {
|
||||
val head = inp.readExact(4) // VER, REP, RSV, ATYP
|
||||
if (head[1].toInt() != 0x00) throw IOException("socks5 connect failed, rep=${head[1].toInt() and 0xff}")
|
||||
// consume BND.ADDR + BND.PORT
|
||||
when (head[3].toInt() and 0xff) {
|
||||
ATYP_IPV4 -> inp.readFully(ByteArray(4 + 2))
|
||||
ATYP_IPV6 -> inp.readFully(ByteArray(16 + 2))
|
||||
ATYP_DOMAIN -> {
|
||||
val len = inp.read()
|
||||
if (len < 0) throw IOException("eof reading bnd addr")
|
||||
inp.readFully(ByteArray(len + 2))
|
||||
}
|
||||
else -> throw IOException("bad atyp in socks5 reply")
|
||||
}
|
||||
}
|
||||
|
||||
override fun openUdpAssociation(): UdpAssociation? = try {
|
||||
Socks5UdpAssociation(cfg, protector, ::negotiate)
|
||||
} catch (e: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SOCKS5 UDP ASSOCIATE: a TCP control connection holds the association open while
|
||||
* datagrams flow over a side UDP socket to the relay endpoint the proxy returns.
|
||||
*/
|
||||
private class Socks5UdpAssociation(
|
||||
cfg: ProxyConfig,
|
||||
protector: SocketProtector,
|
||||
negotiate: (InputStream, OutputStream) -> Unit,
|
||||
) : UdpAssociation {
|
||||
|
||||
private val control = protector.connectedSocket(cfg.host, cfg.port)
|
||||
private val udp = DatagramSocket().also { protector.protect(it) }
|
||||
private val relay: InetSocketAddress
|
||||
|
||||
init {
|
||||
try {
|
||||
control.soTimeout = HANDSHAKE_TIMEOUT_MS
|
||||
val out = control.getOutputStream()
|
||||
val inp = control.getInputStream()
|
||||
negotiate(inp, out)
|
||||
// UDP ASSOCIATE with wildcard endpoint
|
||||
out.write(byteArrayOf(0x05, 0x03, 0x00, ATYP_IPV4.toByte(), 0, 0, 0, 0, 0, 0))
|
||||
out.flush()
|
||||
val head = inp.readExact(4)
|
||||
if (head[1].toInt() != 0x00) throw IOException("udp associate failed rep=${head[1].toInt() and 0xff}")
|
||||
val addr: InetAddress
|
||||
when (head[3].toInt() and 0xff) {
|
||||
ATYP_IPV4 -> addr = InetAddress.getByAddress(inp.readExact(4))
|
||||
ATYP_IPV6 -> addr = InetAddress.getByAddress(inp.readExact(16))
|
||||
ATYP_DOMAIN -> {
|
||||
val len = inp.read()
|
||||
if (len < 0) throw IOException("eof reading bnd domain len")
|
||||
addr = InetAddress.getByName(String(inp.readExact(len), Charsets.US_ASCII))
|
||||
}
|
||||
else -> throw IOException("bad atyp in udp associate reply")
|
||||
}
|
||||
val portBytes = inp.readExact(2)
|
||||
val port = ((portBytes[0].toInt() and 0xff) shl 8) or (portBytes[1].toInt() and 0xff)
|
||||
// Some proxies return 0.0.0.0 — fall back to the proxy host.
|
||||
val target = if (addr.isAnyLocalAddress) InetAddress.getByName(cfg.host) else addr
|
||||
relay = InetSocketAddress(target, port)
|
||||
control.soTimeout = 0
|
||||
watchControl()
|
||||
} catch (e: Throwable) {
|
||||
close()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC 1928: the association lives only while the control connection does.
|
||||
* When the proxy drops it, closing the UDP socket unblocks receive() so the
|
||||
* session ends instead of silently black-holing datagrams.
|
||||
*/
|
||||
private fun watchControl() {
|
||||
Thread({
|
||||
runCatching {
|
||||
val inp = control.getInputStream()
|
||||
val sink = ByteArray(64)
|
||||
while (inp.read(sink) >= 0) { /* drain */ }
|
||||
}
|
||||
close()
|
||||
}, "socks5-udp-control").apply { isDaemon = true; start() }
|
||||
}
|
||||
|
||||
override fun send(dest: Destination, data: ByteArray, off: Int, len: Int) {
|
||||
// [RSV(2)=0][FRAG(1)=0][ATYP+ADDR+PORT][DATA]
|
||||
val pkt = ByteArray(3 + dest.raw.size + len)
|
||||
System.arraycopy(dest.raw, 0, pkt, 3, dest.raw.size)
|
||||
System.arraycopy(data, off, pkt, 3 + dest.raw.size, len)
|
||||
udp.send(DatagramPacket(pkt, pkt.size, relay))
|
||||
}
|
||||
|
||||
override fun receive(): UdpReply? {
|
||||
val buf = ByteArray(64 * 1024)
|
||||
// Skip malformed/fragmented datagrams instead of letting one bad packet
|
||||
// kill the whole reply loop; only socket-level errors propagate.
|
||||
while (true) {
|
||||
val dp = DatagramPacket(buf, buf.size)
|
||||
udp.receive(dp)
|
||||
try {
|
||||
if (dp.length < 7 || buf[2].toInt() != 0) continue // FRAG unsupported
|
||||
val (dest, headerLen) = chat.vojo.proxy.core.net.parseSocksAddress(buf, 3)
|
||||
val dataStart = 3 + headerLen
|
||||
if (dataStart > dp.length) continue
|
||||
return UdpReply(dest, buf.copyOfRange(dataStart, dp.length))
|
||||
} catch (e: Exception) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
runCatching { udp.close() }
|
||||
runCatching { control.close() }
|
||||
}
|
||||
}
|
||||
75
app/src/main/java/chat/vojo/proxy/core/upstream/Upstream.kt
Normal file
75
app/src/main/java/chat/vojo/proxy/core/upstream/Upstream.kt
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package chat.vojo.proxy.core.upstream
|
||||
|
||||
import chat.vojo.proxy.core.ProxyConfig
|
||||
import chat.vojo.proxy.core.ProxyType
|
||||
import chat.vojo.proxy.core.net.Destination
|
||||
import java.io.Closeable
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.DatagramSocket
|
||||
import java.net.Socket
|
||||
|
||||
/** Excludes app-owned sockets from the VPN so upstream traffic doesn't loop back into the tun. */
|
||||
interface SocketProtector {
|
||||
fun protect(socket: Socket): Boolean
|
||||
fun protect(socket: DatagramSocket): Boolean
|
||||
}
|
||||
|
||||
/** A live TCP tunnel to a target, with the proxy framing already negotiated. */
|
||||
class UpstreamConn(
|
||||
val socket: Socket,
|
||||
val input: InputStream,
|
||||
val output: OutputStream,
|
||||
) : Closeable {
|
||||
/**
|
||||
* Half-closes the write side so the peer sees EOF while reads continue.
|
||||
* SSLSocket does not support shutdownOutput — throws, caller falls back to close().
|
||||
*/
|
||||
fun shutdownOutput() {
|
||||
output.flush()
|
||||
socket.shutdownOutput()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
runCatching { socket.close() }
|
||||
}
|
||||
}
|
||||
|
||||
class UdpReply(val from: Destination, val data: ByteArray)
|
||||
|
||||
/** A UDP relay through the proxy that multiplexes all destinations over one path. */
|
||||
interface UdpAssociation : Closeable {
|
||||
fun send(dest: Destination, data: ByteArray, off: Int, len: Int)
|
||||
|
||||
/** Blocks for the next inbound datagram; returns null when the association closes. */
|
||||
fun receive(): UdpReply?
|
||||
}
|
||||
|
||||
/** One configured proxy endpoint, able to open TCP tunnels (and maybe UDP) for targets. */
|
||||
interface Upstream {
|
||||
/** Connects to [dest] through the proxy. Throws on handshake failure. */
|
||||
fun connectTcp(dest: Destination): UpstreamConn
|
||||
|
||||
/** A UDP relay, or null if this proxy type can't carry UDP (HTTP/HTTPS). */
|
||||
fun openUdpAssociation(): UdpAssociation?
|
||||
|
||||
companion object {
|
||||
fun create(cfg: ProxyConfig, protector: SocketProtector): Upstream = when (cfg.type) {
|
||||
ProxyType.SHADOWSOCKS -> ShadowsocksUpstream(cfg, protector)
|
||||
ProxyType.SOCKS5 -> Socks5Upstream(cfg, protector)
|
||||
ProxyType.HTTP, ProxyType.HTTPS -> HttpUpstream(cfg, protector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const val CONNECT_TIMEOUT_MS = 10_000
|
||||
const val HANDSHAKE_TIMEOUT_MS = 15_000
|
||||
|
||||
/** Creates an unconnected, VPN-protected TCP socket and connects it to the proxy server. */
|
||||
internal fun SocketProtector.connectedSocket(host: String, port: Int): Socket {
|
||||
val socket = Socket()
|
||||
protect(socket)
|
||||
socket.tcpNoDelay = true
|
||||
socket.connect(java.net.InetSocketAddress(host, port), CONNECT_TIMEOUT_MS)
|
||||
return socket
|
||||
}
|
||||
62
app/src/main/java/chat/vojo/proxy/data/AppRepository.kt
Normal file
62
app/src/main/java/chat/vojo/proxy/data/AppRepository.kt
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package chat.vojo.proxy.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.LruCache
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/** A user-facing installed app that can use the network. */
|
||||
data class AppInfo(
|
||||
val packageName: String,
|
||||
val label: String,
|
||||
)
|
||||
|
||||
/** Lists installable apps for the whitelist picker and lazily renders their icons. */
|
||||
class AppRepository(context: Context) {
|
||||
|
||||
private val appContext = context.applicationContext
|
||||
private val pm: PackageManager = appContext.packageManager
|
||||
private val iconCache = LruCache<String, ImageBitmap>(256)
|
||||
|
||||
suspend fun installedApps(): List<AppInfo> = withContext(Dispatchers.IO) {
|
||||
val self = appContext.packageName
|
||||
val packages = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
||||
packages.asSequence()
|
||||
.filter { it.packageName != self }
|
||||
.filter { it.requestedPermissions?.contains(android.Manifest.permission.INTERNET) == true }
|
||||
.map { pkg ->
|
||||
val ai = pkg.applicationInfo
|
||||
AppInfo(
|
||||
packageName = pkg.packageName,
|
||||
label = ai?.let { pm.getApplicationLabel(it).toString() } ?: pkg.packageName,
|
||||
)
|
||||
}
|
||||
.sortedBy { it.label.lowercase() }
|
||||
.toList()
|
||||
}
|
||||
|
||||
/** Synchronous cache hit for composition-time initial values (no thread hop). */
|
||||
fun cachedIcon(packageName: String): ImageBitmap? = iconCache.get(packageName)
|
||||
|
||||
suspend fun icon(packageName: String): ImageBitmap? = withContext(Dispatchers.IO) {
|
||||
iconCache.get(packageName)?.let { return@withContext it }
|
||||
val drawable = runCatching { pm.getApplicationIcon(packageName) }.getOrNull() ?: return@withContext null
|
||||
val bmp = drawable.toImageBitmap()
|
||||
iconCache.put(packageName, bmp)
|
||||
bmp
|
||||
}
|
||||
|
||||
private fun Drawable.toImageBitmap(size: Int = 96): ImageBitmap {
|
||||
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
setBounds(0, 0, size, size)
|
||||
draw(canvas)
|
||||
return bitmap.asImageBitmap()
|
||||
}
|
||||
}
|
||||
76
app/src/main/java/chat/vojo/proxy/data/ConfigStore.kt
Normal file
76
app/src/main/java/chat/vojo/proxy/data/ConfigStore.kt
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package chat.vojo.proxy.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import java.io.IOException
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import chat.vojo.proxy.core.AppSettings
|
||||
import chat.vojo.proxy.core.ProxyConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/** Everything we persist: the saved servers plus app-wide settings. */
|
||||
@Serializable
|
||||
data class ProxyState(
|
||||
val configs: List<ProxyConfig> = emptyList(),
|
||||
val settings: AppSettings = AppSettings(),
|
||||
) {
|
||||
val activeConfig: ProxyConfig?
|
||||
get() = configs.firstOrNull { it.id == settings.activeConfigId }
|
||||
?: configs.firstOrNull()
|
||||
}
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vojo_proxy")
|
||||
private val STATE_KEY = stringPreferencesKey("state")
|
||||
private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }
|
||||
|
||||
/** Single source of truth for persisted configuration, backed by Preferences DataStore. */
|
||||
class ConfigStore(context: Context) {
|
||||
private val store = context.applicationContext.dataStore
|
||||
|
||||
val state: Flow<ProxyState> = store.data
|
||||
// Only swallow IO/corruption (DataStore's documented failure mode);
|
||||
// rethrow anything else so a real bug isn't hidden as "no data".
|
||||
.catch { e -> if (e is IOException) emit(emptyPreferences()) else throw e }
|
||||
.map { prefs ->
|
||||
prefs[STATE_KEY]?.let { runCatching { json.decodeFromString<ProxyState>(it) }.getOrNull() }
|
||||
?: ProxyState()
|
||||
}
|
||||
|
||||
suspend fun current(): ProxyState = state.first()
|
||||
|
||||
suspend fun update(transform: (ProxyState) -> ProxyState) {
|
||||
store.edit { prefs ->
|
||||
val cur = prefs[STATE_KEY]?.let {
|
||||
runCatching { json.decodeFromString<ProxyState>(it) }.getOrNull()
|
||||
} ?: ProxyState()
|
||||
prefs[STATE_KEY] = json.encodeToString(transform(cur))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun upsertConfig(config: ProxyConfig) = update { state ->
|
||||
val others = state.configs.filterNot { it.id == config.id }
|
||||
state.copy(configs = others + config)
|
||||
}
|
||||
|
||||
suspend fun deleteConfig(id: String) = update { state ->
|
||||
val configs = state.configs.filterNot { it.id == id }
|
||||
val active = if (state.settings.activeConfigId == id) configs.firstOrNull()?.id else state.settings.activeConfigId
|
||||
state.copy(configs = configs, settings = state.settings.copy(activeConfigId = active))
|
||||
}
|
||||
|
||||
suspend fun setActive(id: String) = update { it.copy(settings = it.settings.copy(activeConfigId = id)) }
|
||||
|
||||
suspend fun updateSettings(transform: (AppSettings) -> AppSettings) =
|
||||
update { it.copy(settings = transform(it.settings)) }
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package chat.vojo.proxy.service
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import chat.vojo.proxy.MainActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** Quick Settings tile: one-tap connect/disconnect from the shade. */
|
||||
class ProxyTileService : TileService() {
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate).also { sc ->
|
||||
sc.launch { VpnState.status.collect { render(it) } }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
scope?.cancel()
|
||||
scope = null
|
||||
}
|
||||
|
||||
private fun render(status: VpnState.Status) {
|
||||
val tile = qsTile ?: return
|
||||
tile.state = when (status) {
|
||||
VpnState.Status.CONNECTED, VpnState.Status.CONNECTING -> Tile.STATE_ACTIVE
|
||||
else -> Tile.STATE_INACTIVE
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
tile.subtitle = when (status) {
|
||||
VpnState.Status.CONNECTED -> "online"
|
||||
VpnState.Status.CONNECTING -> "connecting"
|
||||
VpnState.Status.STOPPING -> "stopping"
|
||||
VpnState.Status.ERROR -> "error"
|
||||
VpnState.Status.DISCONNECTED -> null
|
||||
}
|
||||
}
|
||||
tile.updateTile()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
when (VpnState.status.value) {
|
||||
VpnState.Status.CONNECTED, VpnState.Status.CONNECTING ->
|
||||
ProxyVpnService.stop(this)
|
||||
else -> {
|
||||
if (VpnService.prepare(this) == null) {
|
||||
ProxyVpnService.start(this)
|
||||
} else {
|
||||
// VPN consent needed — open the app to run the system dialog.
|
||||
openApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openApp() {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startActivityAndCollapse(
|
||||
PendingIntent.getActivity(this, 2, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION", "StartActivityAndCollapseDeprecated")
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
486
app/src/main/java/chat/vojo/proxy/service/ProxyVpnService.kt
Normal file
486
app/src/main/java/chat/vojo/proxy/service/ProxyVpnService.kt
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
package chat.vojo.proxy.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import chat.vojo.proxy.MainActivity
|
||||
import chat.vojo.proxy.R
|
||||
import chat.vojo.proxy.core.AppSettings
|
||||
import chat.vojo.proxy.core.ProxyConfig
|
||||
import chat.vojo.proxy.core.Tun2Socks
|
||||
import chat.vojo.proxy.core.formatBytes
|
||||
import chat.vojo.proxy.core.formatUptime
|
||||
import chat.vojo.proxy.core.socks.LocalSocks5Server
|
||||
import chat.vojo.proxy.core.upstream.SocketProtector
|
||||
import chat.vojo.proxy.core.upstream.Upstream
|
||||
import chat.vojo.proxy.data.ConfigStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.net.DatagramSocket
|
||||
import java.net.Socket
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class ProxyVpnService : VpnService(), SocketProtector {
|
||||
|
||||
// Every lifecycle transition (start/stop/restart/engine-exit) runs on this
|
||||
// single thread: no start/stop races, no blocking joins on the main thread.
|
||||
private val lifecycle = Executors.newSingleThreadExecutor { r -> Thread(r, "vojo-vpn-lifecycle") }
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private var vpnInterface: ParcelFileDescriptor? = null
|
||||
private var server: LocalSocks5Server? = null
|
||||
private var engineThread: Thread? = null
|
||||
private var statsJob: Job? = null
|
||||
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
||||
private var lastNetwork: Network? = null
|
||||
|
||||
// Mutated only on the lifecycle thread, but read from IO threads via
|
||||
// onUpstreamUnreachable, so it must be visible across threads.
|
||||
@Volatile private var running = false
|
||||
private var tornDown = true // idempotency guard for shutdown()
|
||||
@Volatile private var stopRequested = false
|
||||
@Volatile private var engineGen = 0
|
||||
@Volatile private var lastUpstreamWarn = 0L
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
lifecycle.execute {
|
||||
shutdown(errorMessage = null)
|
||||
stopSelf()
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
ACTION_RESTART -> {
|
||||
if (!goForeground()) return START_NOT_STICKY
|
||||
lifecycle.execute {
|
||||
if (running) {
|
||||
VpnState.event("Переподключение")
|
||||
shutdown(errorMessage = null, keepNotification = true)
|
||||
}
|
||||
startTunnel()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (!goForeground()) return START_NOT_STICKY
|
||||
lifecycle.execute { startTunnel() }
|
||||
}
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
/** Promotes to foreground immediately (the 5-second FGS window starts at onStartCommand). */
|
||||
private fun goForeground(): Boolean {
|
||||
return try {
|
||||
startForegroundNotification(getString(R.string.notif_connecting))
|
||||
true
|
||||
} catch (e: Throwable) {
|
||||
// ForegroundServiceStartNotAllowedException on a background sticky
|
||||
// restart (Android 12+) must not crash the process. Tear down on the
|
||||
// lifecycle thread (an in-flight RESTART may still have a live engine)
|
||||
// and only then mark the tunnel stopped.
|
||||
Log.e(TAG, "startForeground rejected", e)
|
||||
lifecycle.execute {
|
||||
shutdown(errorMessage = null)
|
||||
VpnState.setStopped(null)
|
||||
stopSelf()
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTunnel() { // lifecycle thread
|
||||
if (running) return
|
||||
stopRequested = false
|
||||
tornDown = false
|
||||
lastUpstreamWarn = 0L
|
||||
val gen = ++engineGen
|
||||
// A new attempt supersedes any prior failure — clear its lingering notification.
|
||||
runCatching { getSystemService(NotificationManager::class.java)?.cancel(NOTIF_ERROR_ID) }
|
||||
VpnState.setConnecting()
|
||||
|
||||
try {
|
||||
val state = runBlocking { ConfigStore(this@ProxyVpnService).current() }
|
||||
val config = state.activeConfig
|
||||
if (config == null) {
|
||||
fail(getString(R.string.error_no_server))
|
||||
return
|
||||
}
|
||||
VpnState.event("Подключение", "${config.name} · ${config.type.label}")
|
||||
updateNotification(getString(R.string.notif_connecting))
|
||||
|
||||
val upstream = Upstream.create(config, this)
|
||||
val socks = LocalSocks5Server(upstream, ::onUpstreamUnreachable).also { it.start() }
|
||||
server = socks
|
||||
|
||||
val pfd = establish(config, state.settings, socks.port)
|
||||
vpnInterface = pfd
|
||||
VpnState.event("Интерфейс поднят", "$TUN_IPV4 · mtu ${state.settings.mtu}")
|
||||
VpnState.event("SOCKS5-мост", "127.0.0.1:${socks.port}")
|
||||
VpnState.event("DNS через туннель", state.settings.dns)
|
||||
|
||||
val hevConfig = buildEngineConfig(socks.port, state.settings)
|
||||
val fd = pfd.fd
|
||||
engineThread = Thread({
|
||||
val ret = Tun2Socks.nativeStart(hevConfig, fd)
|
||||
Log.i(TAG, "engine returned $ret")
|
||||
lifecycle.execute { onEngineExit(gen, ret) }
|
||||
}, "vojo-tun2socks").apply { start() }
|
||||
|
||||
running = true
|
||||
VpnState.setConnected(VpnState.Applied(config.id, state.settings))
|
||||
VpnState.event("Подключено", "${config.name} · ${config.type.label}", VpnState.TunnelEvent.Tone.OK)
|
||||
updateNotification(getString(R.string.notif_connected, config.name))
|
||||
startStatsLoop()
|
||||
registerNetworkCallback()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "failed to start tunnel", e)
|
||||
fail(e.message ?: "Не удалось запустить туннель")
|
||||
}
|
||||
}
|
||||
|
||||
/** Throttled warning surfaced to the journal when upstream connects fail. */
|
||||
private fun onUpstreamUnreachable() {
|
||||
if (!running) return
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastUpstreamWarn > 30_000) {
|
||||
lastUpstreamWarn = now
|
||||
VpnState.event("Прокси не отвечает", "проверьте сервер или сеть", VpnState.TunnelEvent.Tone.WARN)
|
||||
}
|
||||
}
|
||||
|
||||
/** Engine thread exited on its own (not via a requested stop) — tear down loudly. */
|
||||
private fun onEngineExit(gen: Int, ret: Int) { // lifecycle thread
|
||||
if (gen != engineGen || stopRequested) return
|
||||
fail("Движок туннеля неожиданно завершился (код $ret)")
|
||||
}
|
||||
|
||||
private fun establish(config: ProxyConfig, settings: AppSettings, socksPort: Int): ParcelFileDescriptor {
|
||||
val builder = Builder()
|
||||
.setSession("vojo · ${config.name}")
|
||||
.setMtu(settings.mtu)
|
||||
.addAddress(TUN_IPV4, 32)
|
||||
.addRoute("0.0.0.0", 0)
|
||||
.addDnsServer(settings.dns.ifBlank { "1.1.1.1" })
|
||||
.setBlocking(false)
|
||||
|
||||
if (settings.ipv6) {
|
||||
builder.addAddress(TUN_IPV6, 128).addRoute("::", 0)
|
||||
}
|
||||
|
||||
applyAppFilter(builder, settings.allowedApps)
|
||||
builder.setUnderlyingNetworks(activeNetworkArray())
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
builder.setMetered(false)
|
||||
}
|
||||
builder.setConfigureIntent(activityPendingIntent())
|
||||
|
||||
return builder.establish() ?: throw IllegalStateException("VPN permission not granted")
|
||||
}
|
||||
|
||||
/** Whitelist mode when apps are selected; otherwise route everything except ourselves. */
|
||||
private fun applyAppFilter(builder: Builder, allowed: Set<String>) {
|
||||
val self = packageName
|
||||
if (allowed.isNotEmpty()) {
|
||||
var added = 0
|
||||
for (pkg in allowed) {
|
||||
if (pkg == self) continue
|
||||
runCatching { builder.addAllowedApplication(pkg); added++ }
|
||||
.onFailure { Log.w(TAG, "skip missing package $pkg") }
|
||||
}
|
||||
// All selected packages are gone: routing *everything* through the
|
||||
// proxy would silently violate the whitelist contract.
|
||||
if (added == 0) {
|
||||
throw IllegalStateException("Выбранные приложения не установлены — обновите список")
|
||||
}
|
||||
} else {
|
||||
runCatching { builder.addDisallowedApplication(self) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEngineConfig(socksPort: Int, settings: AppSettings): String = buildString {
|
||||
append("tunnel:\n")
|
||||
append(" mtu: ${settings.mtu}\n")
|
||||
append(" ipv4: $TUN_IPV4\n")
|
||||
if (settings.ipv6) append(" ipv6: '$TUN_IPV6'\n")
|
||||
append("socks5:\n")
|
||||
append(" address: 127.0.0.1\n")
|
||||
append(" port: $socksPort\n")
|
||||
append(" udp: 'udp'\n")
|
||||
append("misc:\n")
|
||||
append(" connect-timeout: 8000\n")
|
||||
append(" log-level: warn\n")
|
||||
}
|
||||
|
||||
private fun startStatsLoop() {
|
||||
statsJob?.cancel()
|
||||
statsJob = scope.launch {
|
||||
var tick = 0
|
||||
while (isActive) {
|
||||
runCatching {
|
||||
val s = Tun2Socks.nativeStats()
|
||||
if (s.size >= 4) {
|
||||
VpnState.setStats(VpnState.Stats(txBytes = s[1], rxBytes = s[3]))
|
||||
}
|
||||
}
|
||||
// Refresh the notification with live traffic once a minute.
|
||||
if (++tick % 30 == 0 && VpnState.status.value == VpnState.Status.CONNECTED) {
|
||||
runCatching { updateNotification(trafficSummary()) }
|
||||
}
|
||||
delay(2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun trafficSummary(): String {
|
||||
val st = VpnState.stats.value
|
||||
val up = formatUptime(VpnState.connectedSince.value, System.currentTimeMillis())
|
||||
return "↑ ${formatBytes(st.txBytes)} · ↓ ${formatBytes(st.rxBytes)} · $up"
|
||||
}
|
||||
|
||||
private fun registerNetworkCallback() {
|
||||
val cm = getSystemService(ConnectivityManager::class.java) ?: return
|
||||
lastNetwork = cm.activeNetwork
|
||||
val cb = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
runCatching { setUnderlyingNetworks(arrayOf(network)) }
|
||||
if (network != lastNetwork) {
|
||||
lastNetwork = network
|
||||
val caps = runCatching { cm.getNetworkCapabilities(network) }.getOrNull()
|
||||
val name = when {
|
||||
caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> "Wi-Fi"
|
||||
caps?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "мобильная сеть"
|
||||
caps?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) == true -> "Ethernet"
|
||||
else -> "другая сеть"
|
||||
}
|
||||
VpnState.event("Сеть изменилась", name, VpnState.TunnelEvent.Tone.WARN)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
runCatching { setUnderlyingNetworks(null) }
|
||||
}
|
||||
}
|
||||
networkCallback = cb
|
||||
runCatching { cm.registerDefaultNetworkCallback(cb) }
|
||||
}
|
||||
|
||||
private fun activeNetworkArray(): Array<Network>? {
|
||||
val cm = getSystemService(ConnectivityManager::class.java) ?: return null
|
||||
return cm.activeNetwork?.let { arrayOf(it) }
|
||||
}
|
||||
|
||||
// --- SocketProtector ---
|
||||
override fun protect(socket: Socket): Boolean = super<VpnService>.protect(socket)
|
||||
override fun protect(socket: DatagramSocket): Boolean = super<VpnService>.protect(socket)
|
||||
|
||||
private fun fail(message: String) { // lifecycle thread
|
||||
VpnState.event("Ошибка", message, VpnState.TunnelEvent.Tone.ERROR)
|
||||
shutdown(errorMessage = message)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
/**
|
||||
* Tears everything down on the lifecycle thread. A non-null [errorMessage]
|
||||
* keeps the ERROR state (and an explanatory notification) visible to the user.
|
||||
*/
|
||||
private fun shutdown(errorMessage: String?, keepNotification: Boolean = false) { // lifecycle thread
|
||||
// Idempotent: ACTION_STOP tears down then stopSelf() → onDestroy() would
|
||||
// otherwise run a second teardown (and log a duplicate "Отключено").
|
||||
if (tornDown) return
|
||||
tornDown = true
|
||||
stopRequested = true
|
||||
engineGen++ // invalidate pending engine-exit callbacks
|
||||
VpnState.setStopping()
|
||||
|
||||
statsJob?.cancel()
|
||||
statsJob = null
|
||||
|
||||
networkCallback?.let { cb ->
|
||||
runCatching { getSystemService(ConnectivityManager::class.java)?.unregisterNetworkCallback(cb) }
|
||||
}
|
||||
networkCallback = null
|
||||
|
||||
val engine = engineThread
|
||||
engineThread = null
|
||||
if (engine != null) {
|
||||
// nativeStop only signals the engine's event fd; the native `running`
|
||||
// flag and the tun fd aren't released until nativeStart() returns and
|
||||
// the thread body finishes. Wait for that here (re-kicking stop) so a
|
||||
// following reconnect can't hit the C double-start guard (ret == -2),
|
||||
// and so we never close the tun fd while the engine still reads it.
|
||||
var waited = 0L
|
||||
while (engine.isAlive && waited < ENGINE_JOIN_TIMEOUT_MS) {
|
||||
runCatching { Tun2Socks.nativeStop() }
|
||||
runCatching { engine.join(ENGINE_JOIN_STEP_MS) }
|
||||
waited += ENGINE_JOIN_STEP_MS
|
||||
}
|
||||
if (engine.isAlive) Log.w(TAG, "engine thread did not exit in time")
|
||||
}
|
||||
|
||||
server?.stop()
|
||||
server = null
|
||||
|
||||
// Only reclaim the tun fd once the engine has truly stopped touching it;
|
||||
// closing it out from under live native code is undefined behaviour.
|
||||
if (engine == null || !engine.isAlive) {
|
||||
runCatching { vpnInterface?.close() }
|
||||
vpnInterface = null
|
||||
} else {
|
||||
Log.w(TAG, "leaving tun fd open — engine still alive after stop")
|
||||
}
|
||||
|
||||
running = false
|
||||
VpnState.setStopped(errorMessage)
|
||||
// Don't log "Отключено" mid-reconnect (keepNotification) — that path
|
||||
// logs "Переподключение" → "Подключение" → "Подключено" instead.
|
||||
if (errorMessage == null && !keepNotification && VpnState.events.value.isNotEmpty()) {
|
||||
VpnState.event("Отключено")
|
||||
}
|
||||
if (!keepNotification) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
if (errorMessage != null) postErrorNotification(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRevoke() {
|
||||
// User revoked VPN permission or another VPN took over.
|
||||
lifecycle.execute {
|
||||
VpnState.event("VPN отозван системой", null, VpnState.TunnelEvent.Tone.WARN)
|
||||
shutdown(errorMessage = null)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
// System killed the service without ACTION_STOP: still release the
|
||||
// engine, the SOCKS server and the tun fd.
|
||||
lifecycle.execute { shutdown(errorMessage = null) }
|
||||
lifecycle.shutdown()
|
||||
scope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
// --- Notification ---
|
||||
private fun startForegroundNotification(text: String) {
|
||||
ensureChannel()
|
||||
val notification = buildNotification(text)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(NOTIF_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(NOTIF_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification(text: String) {
|
||||
val nm = getSystemService(NotificationManager::class.java) ?: return
|
||||
nm.notify(NOTIF_ID, buildNotification(text))
|
||||
}
|
||||
|
||||
private fun postErrorNotification(text: String) {
|
||||
val nm = getSystemService(NotificationManager::class.java) ?: return
|
||||
ensureChannel()
|
||||
val n = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.notif_error_title))
|
||||
.setContentText(text)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
||||
.setSmallIcon(R.drawable.ic_stat_shield)
|
||||
.setAutoCancel(true)
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(activityPendingIntent())
|
||||
.build()
|
||||
nm.notify(NOTIF_ERROR_ID, n)
|
||||
}
|
||||
|
||||
private fun buildNotification(text: String): Notification {
|
||||
val stopIntent = PendingIntent.getService(
|
||||
this, 1,
|
||||
Intent(this, ProxyVpnService::class.java).setAction(ACTION_STOP),
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentText(text)
|
||||
.setSmallIcon(R.drawable.ic_stat_shield)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setContentIntent(activityPendingIntent())
|
||||
.addAction(0, getString(R.string.action_disconnect), stopIntent)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun activityPendingIntent(): PendingIntent = PendingIntent.getActivity(
|
||||
this, 0,
|
||||
Intent(this, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
|
||||
private fun ensureChannel() {
|
||||
val nm = getSystemService(NotificationManager::class.java) ?: return
|
||||
if (nm.getNotificationChannel(CHANNEL_ID) == null) {
|
||||
nm.createNotificationChannel(
|
||||
NotificationChannel(CHANNEL_ID, getString(R.string.channel_name), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
setShowBadge(false)
|
||||
description = getString(R.string.channel_desc)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ProxyVpnService"
|
||||
const val ACTION_START = "chat.vojo.proxy.START"
|
||||
const val ACTION_STOP = "chat.vojo.proxy.STOP"
|
||||
const val ACTION_RESTART = "chat.vojo.proxy.RESTART"
|
||||
|
||||
private const val CHANNEL_ID = "vpn_status"
|
||||
private const val NOTIF_ID = 0x7C5C
|
||||
private const val NOTIF_ERROR_ID = 0x7C5D
|
||||
|
||||
// Bounded wait for the native engine thread to exit during teardown.
|
||||
private const val ENGINE_JOIN_TIMEOUT_MS = 5000L
|
||||
private const val ENGINE_JOIN_STEP_MS = 250L
|
||||
|
||||
const val TUN_IPV4 = "198.18.0.1"
|
||||
private const val TUN_IPV6 = "fc00::1"
|
||||
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, ProxyVpnService::class.java).setAction(ACTION_START)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
fun restart(context: Context) {
|
||||
val intent = Intent(context, ProxyVpnService::class.java).setAction(ACTION_RESTART)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
val intent = Intent(context, ProxyVpnService::class.java).setAction(ACTION_STOP)
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
92
app/src/main/java/chat/vojo/proxy/service/VpnState.kt
Normal file
92
app/src/main/java/chat/vojo/proxy/service/VpnState.kt
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package chat.vojo.proxy.service
|
||||
|
||||
import chat.vojo.proxy.core.AppSettings
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
/** Process-wide observable VPN state shared between the service and the UI. */
|
||||
object VpnState {
|
||||
|
||||
enum class Status { DISCONNECTED, CONNECTING, CONNECTED, STOPPING, ERROR }
|
||||
|
||||
data class Stats(
|
||||
val txBytes: Long = 0,
|
||||
val rxBytes: Long = 0,
|
||||
)
|
||||
|
||||
/** Config + settings snapshot the running tunnel was built from. */
|
||||
data class Applied(val configId: String, val settings: AppSettings)
|
||||
|
||||
/** One row of the connection journal rendered by the timeline rail. */
|
||||
data class TunnelEvent(
|
||||
/** Process-unique, monotonic — a stable LazyColumn key even when two events share a millisecond. */
|
||||
val id: Long,
|
||||
val at: Long,
|
||||
val label: String,
|
||||
val detail: String? = null,
|
||||
val tone: Tone = Tone.NEUTRAL,
|
||||
) {
|
||||
enum class Tone { NEUTRAL, OK, WARN, ERROR }
|
||||
}
|
||||
|
||||
private const val MAX_EVENTS = 30
|
||||
private val eventSeq = AtomicLong(0)
|
||||
|
||||
private val _status = MutableStateFlow(Status.DISCONNECTED)
|
||||
val status: StateFlow<Status> = _status.asStateFlow()
|
||||
|
||||
private val _stats = MutableStateFlow(Stats())
|
||||
val stats: StateFlow<Stats> = _stats.asStateFlow()
|
||||
|
||||
private val _error = MutableStateFlow<String?>(null)
|
||||
val error: StateFlow<String?> = _error.asStateFlow()
|
||||
|
||||
/** Epoch millis when the tunnel reached CONNECTED, or 0. */
|
||||
private val _connectedSince = MutableStateFlow(0L)
|
||||
val connectedSince: StateFlow<Long> = _connectedSince.asStateFlow()
|
||||
|
||||
private val _applied = MutableStateFlow<Applied?>(null)
|
||||
val applied: StateFlow<Applied?> = _applied.asStateFlow()
|
||||
|
||||
private val _events = MutableStateFlow<List<TunnelEvent>>(emptyList())
|
||||
val events: StateFlow<List<TunnelEvent>> = _events.asStateFlow()
|
||||
|
||||
internal fun setConnecting() {
|
||||
_error.value = null
|
||||
_status.value = Status.CONNECTING
|
||||
}
|
||||
|
||||
internal fun setConnected(applied: Applied) {
|
||||
_status.value = Status.CONNECTED
|
||||
_applied.value = applied
|
||||
if (_connectedSince.value == 0L) _connectedSince.value = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
internal fun setStopping() {
|
||||
_status.value = Status.STOPPING
|
||||
}
|
||||
|
||||
/** Final state after shutdown; a non-null [error] survives until the next connect. */
|
||||
internal fun setStopped(error: String? = null) {
|
||||
if (error != null) {
|
||||
_error.value = error
|
||||
_status.value = Status.ERROR
|
||||
} else {
|
||||
_status.value = Status.DISCONNECTED
|
||||
}
|
||||
_connectedSince.value = 0L
|
||||
_stats.value = Stats()
|
||||
_applied.value = null
|
||||
}
|
||||
|
||||
internal fun setStats(stats: Stats) {
|
||||
_stats.value = stats
|
||||
}
|
||||
|
||||
internal fun event(label: String, detail: String? = null, tone: TunnelEvent.Tone = TunnelEvent.Tone.NEUTRAL) {
|
||||
val ev = TunnelEvent(eventSeq.incrementAndGet(), System.currentTimeMillis(), label, detail, tone)
|
||||
_events.value = (_events.value + ev).takeLast(MAX_EVENTS)
|
||||
}
|
||||
}
|
||||
273
app/src/main/java/chat/vojo/proxy/ui/AppsScreen.kt
Normal file
273
app/src/main/java/chat/vojo/proxy/ui/AppsScreen.kt
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
package chat.vojo.proxy.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.vojo.proxy.data.AppInfo
|
||||
import chat.vojo.proxy.ui.theme.Vojo
|
||||
|
||||
@Composable
|
||||
fun AppsScreen(
|
||||
apps: List<AppInfo>,
|
||||
loading: Boolean,
|
||||
selected: Set<String>,
|
||||
loadIcon: suspend (String) -> ImageBitmap?,
|
||||
cachedIcon: (String) -> ImageBitmap?,
|
||||
onToggle: (String, Boolean) -> Unit,
|
||||
onClear: () -> Unit,
|
||||
onLoad: () -> Unit,
|
||||
) {
|
||||
LaunchedEffect(Unit) { onLoad() }
|
||||
var query by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
// All network-capable apps are shown — no system/user split.
|
||||
val filtered = remember(apps, query) {
|
||||
apps.asSequence()
|
||||
.filter { query.isBlank() || it.label.contains(query, true) || it.packageName.contains(query, true) }
|
||||
.toList()
|
||||
}
|
||||
// Selected apps pin to the top — the whitelist must be reviewable at a glance.
|
||||
val pinned = remember(apps, selected) { apps.filter { it.packageName in selected } }
|
||||
val missing = remember(apps, selected, loading) {
|
||||
if (apps.isEmpty()) emptyList()
|
||||
else (selected - apps.map { it.packageName }.toSet()).sorted()
|
||||
}
|
||||
val rest = remember(filtered, selected) { filtered.filter { it.packageName !in selected } }
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.padding(horizontal = 14.dp, vertical = 12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
if (selected.isEmpty()) "Проксируются: все приложения" else "Проксируются",
|
||||
color = Vojo.text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
if (selected.isNotEmpty()) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Box(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(99.dp))
|
||||
.background(Vojo.fleet)
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
) {
|
||||
Text("${selected.size}", color = Color(0xFF0C0C0E), fontSize = 11.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (selected.isNotEmpty()) {
|
||||
Text(
|
||||
"Сбросить",
|
||||
color = Vojo.fleetSoft, fontSize = 13.sp,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable { onClear() }
|
||||
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
"Пустой список = весь трафик идёт через прокси.",
|
||||
color = Vojo.faint, fontSize = 12.sp,
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
// Embedded search field (Fleet composer style)
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Vojo.bgPanel)
|
||||
.border(1.dp, Vojo.divider, RoundedCornerShape(12.dp))
|
||||
.padding(horizontal = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(VojoIcons.Search, contentDescription = null, tint = Vojo.faint, modifier = Modifier.size(17.dp))
|
||||
Spacer(Modifier.width(10.dp))
|
||||
BasicTextField(
|
||||
value = query,
|
||||
onValueChange = { query = it },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
textStyle = TextStyle(color = Vojo.text, fontSize = 15.sp),
|
||||
cursorBrush = SolidColor(Vojo.fleet),
|
||||
decorationBox = { inner ->
|
||||
Box(Modifier.padding(vertical = 14.dp)) {
|
||||
if (query.isEmpty()) Text("Поиск приложений…", color = Vojo.faint, fontSize = 15.sp)
|
||||
inner()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading && apps.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(color = Vojo.fleet, strokeWidth = 2.dp)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
Modifier.fillMaxSize(),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||
) {
|
||||
if (query.isBlank() && (pinned.isNotEmpty() || missing.isNotEmpty())) {
|
||||
item(key = "hdr-selected") {
|
||||
SectionLabel("Выбранные · ${selected.size}", Modifier.padding(start = 6.dp, top = 6.dp, bottom = 6.dp))
|
||||
}
|
||||
items(pinned, key = { "sel:" + it.packageName }) { app ->
|
||||
AppRow(app, true, loadIcon, cachedIcon) { on -> onToggle(app.packageName, on) }
|
||||
}
|
||||
items(missing, key = { "miss:$it" }) { pkg ->
|
||||
MissingAppRow(pkg) { onToggle(pkg, false) }
|
||||
}
|
||||
item(key = "hdr-all") {
|
||||
SectionLabel("Все приложения", Modifier.padding(start = 6.dp, top = 14.dp, bottom = 6.dp))
|
||||
}
|
||||
}
|
||||
val list = if (query.isBlank()) rest else filtered
|
||||
items(list, key = { it.packageName }) { app ->
|
||||
AppRow(app, app.packageName in selected, loadIcon, cachedIcon) { on -> onToggle(app.packageName, on) }
|
||||
}
|
||||
if (filtered.isEmpty() && query.isNotBlank()) {
|
||||
item(key = "empty") {
|
||||
Box(Modifier.fillMaxWidth().padding(vertical = 32.dp), contentAlignment = Alignment.Center) {
|
||||
Text("Ничего не найдено по «$query»", color = Vojo.faint, fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ProduceStateDoesNotAssignValue") // false positive: value is assigned via suspend lambda
|
||||
@Composable
|
||||
private fun AppRow(
|
||||
app: AppInfo,
|
||||
checked: Boolean,
|
||||
loadIcon: suspend (String) -> ImageBitmap?,
|
||||
cachedIcon: (String) -> ImageBitmap?,
|
||||
onToggle: (Boolean) -> Unit,
|
||||
) {
|
||||
// Cache hit renders instantly — no placeholder flash on fast scroll.
|
||||
val icon by produceState(initialValue = cachedIcon(app.packageName), app.packageName) {
|
||||
if (value == null) value = loadIcon(app.packageName)
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(if (checked) Vojo.surface else Color.Transparent)
|
||||
.toggleable(value = checked, role = Role.Checkbox) { onToggle(it) }
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// The active-channel idiom: a 2dp accent bar marks selected rows.
|
||||
Box(
|
||||
Modifier
|
||||
.width(2.dp)
|
||||
.height(34.dp)
|
||||
.clip(RoundedCornerShape(1.dp))
|
||||
.background(if (checked) Vojo.fleet else Color.Transparent)
|
||||
)
|
||||
Spacer(Modifier.width(9.dp))
|
||||
Box(Modifier.size(40.dp).clip(RoundedCornerShape(10.dp)).background(if (checked) Vojo.surface2 else Vojo.surface), contentAlignment = Alignment.Center) {
|
||||
val img = icon
|
||||
if (img != null) {
|
||||
Image(img, contentDescription = null, modifier = Modifier.size(34.dp))
|
||||
} else {
|
||||
Text(app.label.take(1).uppercase(), color = Vojo.muted, fontSize = 15.sp)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(app.label, color = Vojo.text, fontSize = 15.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(app.packageName, color = Vojo.faint, fontFamily = Vojo.mono, fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Checkbox(checked)
|
||||
}
|
||||
}
|
||||
|
||||
/** A selected package that is no longer installed — visible and removable. */
|
||||
@Composable
|
||||
private fun MissingAppRow(packageName: String, onRemove: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Vojo.surface.copy(alpha = 0.5f))
|
||||
.clickable { onRemove() }
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.width(2.dp)
|
||||
.height(34.dp)
|
||||
.background(Vojo.amber.copy(alpha = 0.6f))
|
||||
)
|
||||
Spacer(Modifier.width(9.dp))
|
||||
Box(Modifier.size(40.dp).clip(RoundedCornerShape(10.dp)).background(Vojo.surface), contentAlignment = Alignment.Center) {
|
||||
Text("?", color = Vojo.faint, fontSize = 15.sp)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(packageName, color = Vojo.muted, fontFamily = Vojo.mono, fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text("не установлено · нажмите, чтобы убрать", color = Vojo.faint, fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Checkbox(checked: Boolean) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(24.dp)
|
||||
.clip(RoundedCornerShape(7.dp))
|
||||
.background(if (checked) Vojo.fleet else Color.Transparent)
|
||||
.border(1.dp, if (checked) Vojo.fleet else Vojo.hairline, RoundedCornerShape(7.dp)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (checked) Icon(VojoIcons.Check, contentDescription = null, tint = Color(0xFF0C0C0E), modifier = Modifier.size(14.dp))
|
||||
}
|
||||
}
|
||||
191
app/src/main/java/chat/vojo/proxy/ui/Components.kt
Normal file
191
app/src/main/java/chat/vojo/proxy/ui/Components.kt
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
package chat.vojo.proxy.ui
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.pager.PagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.boundsInParent
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import chat.vojo.proxy.ui.theme.Vojo
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/** Darkens a colour for the avatar gradient, mirroring the design's `shade(c, -22)`. */
|
||||
fun Color.shade(amount: Float = 0.78f): Color = Color(red * amount, green * amount, blue * amount, alpha)
|
||||
|
||||
/** Square protocol tile with a 2-letter badge and a soft diagonal gradient. */
|
||||
@Composable
|
||||
fun ProtocolBadge(badge: String, color: Color, size: Int = 38) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(size.dp)
|
||||
.clip(RoundedCornerShape((size * 0.32f).dp))
|
||||
.background(Brush.linearGradient(listOf(color, color.shade()))),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
badge,
|
||||
color = Color(0xFF0C0C0E),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = (size * 0.36f).sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Uppercase, letter-spaced section label — the design's one monospace chrome accent. */
|
||||
@Composable
|
||||
fun SectionLabel(text: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text.uppercase(),
|
||||
modifier = modifier,
|
||||
color = Vojo.faint,
|
||||
fontFamily = Vojo.mono,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
letterSpacing = 1.4.sp,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The whole top chrome: the four tabs sit on the static backdrop (the row does
|
||||
* NOT move with the panes). Tabs are sized to their content and distributed with
|
||||
* even gaps — equal-width quarters left the centred labels with ragged spacing.
|
||||
* The active tab carries a violet underline that slides AND resizes between the
|
||||
* measured tab bounds; both the bounds and the pager position are read in the
|
||||
* layout phase so a swipe glides the line without recomposing the row.
|
||||
*/
|
||||
@Composable
|
||||
fun SegmentTabs(
|
||||
tabs: List<String>,
|
||||
pagerState: PagerState,
|
||||
onSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// [left, width] of each tab in px, captured in the layout phase. Updated only
|
||||
// on real change so onGloballyPositioned can't drive an endless relayout.
|
||||
val bounds = remember(tabs.size) {
|
||||
mutableStateListOf<IntArray>().apply { repeat(tabs.size) { add(intArrayOf(0, 0)) } }
|
||||
}
|
||||
Box(modifier.background(Vojo.bgPanel)) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(54.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
tabs.forEachIndexed { i, label ->
|
||||
val active = pagerState.currentPage == i
|
||||
Box(
|
||||
Modifier
|
||||
.onGloballyPositioned { c ->
|
||||
val r = c.boundsInParent()
|
||||
val x = r.left.roundToInt()
|
||||
val w = r.width.roundToInt()
|
||||
val cur = bounds[i]
|
||||
if (cur[0] != x || cur[1] != w) bounds[i] = intArrayOf(x, w)
|
||||
}
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.clickable { onSelect(i) }
|
||||
.padding(horizontal = 14.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
label,
|
||||
color = if (active) Vojo.text else Vojo.muted,
|
||||
fontSize = 14.5.sp,
|
||||
fontWeight = if (active) FontWeight.SemiBold else FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Underline spanning the active tab, interpolated toward the next tab by
|
||||
// the live swipe fraction. The outer node fills the width so the inner bar
|
||||
// can be placed at any x; both reads happen in the layout phase.
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth()
|
||||
.height(2.5.dp),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.layout { measurable, constraints ->
|
||||
val last = tabs.lastIndex.coerceAtLeast(0)
|
||||
val posF = (pagerState.currentPage + pagerState.currentPageOffsetFraction)
|
||||
.coerceIn(0f, last.toFloat())
|
||||
val lo = floor(posF).toInt().coerceIn(0, last)
|
||||
val hi = ceil(posF).toInt().coerceIn(0, last)
|
||||
val t = posF - lo
|
||||
val from = bounds[lo]
|
||||
val to = bounds[hi]
|
||||
val barW = lerp(from[1].toFloat(), to[1].toFloat(), t).roundToInt().coerceAtLeast(0)
|
||||
val left = lerp(from[0].toFloat(), to[0].toFloat(), t).roundToInt()
|
||||
val placeable = measurable.measure(constraints.copy(minWidth = barW, maxWidth = barW))
|
||||
layout(constraints.maxWidth, placeable.height) {
|
||||
placeable.place(left, 0)
|
||||
}
|
||||
}
|
||||
.height(2.5.dp)
|
||||
.clip(RoundedCornerShape(topStart = 2.dp, topEnd = 2.dp))
|
||||
.background(Vojo.fleet),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A flat Fleet-style card: panel surface, hairline border, rounded. */
|
||||
@Composable
|
||||
fun PanelCard(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null,
|
||||
accent: Color? = null,
|
||||
background: Color = Vojo.bgPanel,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val shape = RoundedCornerShape(14.dp)
|
||||
var m = modifier
|
||||
.clip(shape)
|
||||
.background(background)
|
||||
.border(BorderStroke(1.dp, accent ?: Vojo.divider), shape)
|
||||
if (onClick != null) m = m.clickable { onClick() }
|
||||
Box(m) { content() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnlineDot(color: Color, size: Int = 8) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(size.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
)
|
||||
}
|
||||
494
app/src/main/java/chat/vojo/proxy/ui/ConnectionScreen.kt
Normal file
494
app/src/main/java/chat/vojo/proxy/ui/ConnectionScreen.kt
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
package chat.vojo.proxy.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import chat.vojo.proxy.core.AppSettings
|
||||
import chat.vojo.proxy.core.ProxyConfig
|
||||
import chat.vojo.proxy.core.ProxyType
|
||||
import chat.vojo.proxy.core.formatBytes
|
||||
import chat.vojo.proxy.core.formatClock
|
||||
import chat.vojo.proxy.core.formatUptime
|
||||
import chat.vojo.proxy.service.VpnState
|
||||
import chat.vojo.proxy.ui.theme.Vojo
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.Calendar
|
||||
|
||||
private fun protoColor(config: ProxyConfig?): Color = when (config?.type) {
|
||||
ProxyType.SHADOWSOCKS -> Vojo.fleet
|
||||
ProxyType.SOCKS5 -> Vojo.blue
|
||||
ProxyType.HTTP -> Vojo.amber
|
||||
ProxyType.HTTPS -> Vojo.teal
|
||||
null -> Vojo.fleetSoft
|
||||
}
|
||||
|
||||
/** The Dawn hour palette: night → dawn → noon → dusk, colour lives on the time axis. */
|
||||
private fun hourColor(h: Int): Color = when {
|
||||
h < 5 -> Color(0xFF3A3D57)
|
||||
h < 7 -> Color(0xFF4A4A6B)
|
||||
h < 10 -> Color(0xFFC39A7A)
|
||||
h < 13 -> Color(0xFFD4B88A)
|
||||
h < 16 -> Color(0xFFA8B39A)
|
||||
h < 19 -> Color(0xFFB8A187)
|
||||
h < 21 -> Color(0xFFC08E7B)
|
||||
h < 23 -> Color(0xFF6E6587)
|
||||
else -> Color(0xFF3A3D57)
|
||||
}
|
||||
|
||||
private fun eventHour(at: Long): Int =
|
||||
Calendar.getInstance().apply { timeInMillis = at }.get(Calendar.HOUR_OF_DAY)
|
||||
|
||||
@Composable
|
||||
fun ConnectionScreen(
|
||||
activeConfig: ProxyConfig?,
|
||||
configs: List<ProxyConfig>,
|
||||
status: VpnState.Status,
|
||||
stats: VpnState.Stats,
|
||||
connectedSince: Long,
|
||||
error: String?,
|
||||
events: List<VpnState.TunnelEvent>,
|
||||
applied: VpnState.Applied?,
|
||||
settings: AppSettings,
|
||||
liveId: String?,
|
||||
onConnect: () -> Unit,
|
||||
onDisconnect: () -> Unit,
|
||||
onReconnect: () -> Unit,
|
||||
onSelectServer: (String) -> Unit,
|
||||
) {
|
||||
var pickerOpen by remember { mutableStateOf(false) }
|
||||
val connected = status == VpnState.Status.CONNECTED
|
||||
val active = status == VpnState.Status.CONNECTING || status == VpnState.Status.CONNECTED || status == VpnState.Status.STOPPING
|
||||
val live = activeConfig != null && activeConfig.id == liveId
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
// ── Fixed header — never reflows on connect/disconnect ──
|
||||
Column(Modifier.fillMaxWidth().padding(horizontal = 18.dp, vertical = 16.dp)) {
|
||||
if (error != null && status == VpnState.Status.ERROR) {
|
||||
ErrorBanner(error)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
}
|
||||
|
||||
ActiveServerCard(
|
||||
config = activeConfig,
|
||||
live = live,
|
||||
active = active,
|
||||
onPick = if (configs.size > 1 && !active) ({ pickerOpen = true }) else null,
|
||||
onDisconnect = onDisconnect,
|
||||
)
|
||||
|
||||
val stale = connected && applied != null &&
|
||||
(applied.configId != activeConfig?.id || applied.settings != settings)
|
||||
if (stale) {
|
||||
Spacer(Modifier.height(10.dp))
|
||||
PanelCard(
|
||||
Modifier.fillMaxWidth(),
|
||||
onClick = onReconnect,
|
||||
accent = Vojo.amber.copy(alpha = 0.45f),
|
||||
) {
|
||||
Row(Modifier.padding(horizontal = 16.dp, vertical = 13.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Настройки изменены", color = Vojo.muted, fontSize = 13.sp, modifier = Modifier.weight(1f))
|
||||
Text("переподключить", color = Vojo.fleetSoft, fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Fixed-height slot: button when down, stats when up — swapping in
|
||||
// place means the journal below never jumps on connect/disconnect.
|
||||
Box(Modifier.fillMaxWidth().height(72.dp), contentAlignment = Alignment.Center) {
|
||||
when {
|
||||
connected -> StatsRow(stats, connectedSince)
|
||||
status == VpnState.Status.CONNECTING || status == VpnState.Status.STOPPING ->
|
||||
ConnectingButton(status)
|
||||
else -> ConnectButton(status, onConnect, enabled = activeConfig != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scrollable journal — the only thing that scrolls; autoscrolls down ──
|
||||
Column(Modifier.fillMaxWidth().weight(1f).padding(horizontal = 18.dp)) {
|
||||
SectionLabel("Журнал", Modifier.padding(start = 2.dp, bottom = 10.dp))
|
||||
JournalList(
|
||||
events = events,
|
||||
pulseLast = status == VpnState.Status.CONNECTING,
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (pickerOpen) {
|
||||
ServerPickerSheet(
|
||||
configs = configs,
|
||||
activeId = activeConfig?.id,
|
||||
liveId = liveId,
|
||||
onSelect = { onSelectServer(it); pickerOpen = false },
|
||||
onDismiss = { pickerOpen = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorBanner(message: String) {
|
||||
PanelCard(
|
||||
Modifier.fillMaxWidth(),
|
||||
accent = Vojo.danger.copy(alpha = 0.5f),
|
||||
background = Vojo.danger.copy(alpha = 0.08f),
|
||||
) {
|
||||
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(8.dp).clip(CircleShape).background(Vojo.danger))
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(message, color = Vojo.danger, fontSize = 14.sp, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveServerCard(
|
||||
config: ProxyConfig?,
|
||||
live: Boolean,
|
||||
active: Boolean,
|
||||
onPick: (() -> Unit)?,
|
||||
onDisconnect: () -> Unit,
|
||||
) {
|
||||
PanelCard(
|
||||
Modifier.fillMaxWidth(),
|
||||
onClick = onPick,
|
||||
accent = if (live) protoColor(config).copy(alpha = 0.45f) else null,
|
||||
) {
|
||||
Row(Modifier.padding(14.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
ProtocolBadge(config?.type?.badge ?: "—", protoColor(config), size = 48)
|
||||
Spacer(Modifier.width(14.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
config?.name ?: "Сервер не выбран",
|
||||
color = if (live) Vojo.green else Vojo.text,
|
||||
fontSize = 17.sp, fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (live) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
OnlineDot(Vojo.green, 7)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Text(
|
||||
config?.handle ?: "выберите на вкладке «Серверы»",
|
||||
color = Vojo.muted, fontFamily = Vojo.mono, fontSize = 13.sp,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
if (active) {
|
||||
// Disconnect lives inside the card as a square ✕.
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Box(
|
||||
Modifier
|
||||
.size(46.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Vojo.danger.copy(alpha = 0.14f))
|
||||
.clickable { onDisconnect() },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(VojoIcons.Cross, contentDescription = "Отключить", tint = Vojo.danger, modifier = Modifier.size(18.dp))
|
||||
}
|
||||
} else if (config != null && onPick != null) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("сменить", color = Vojo.fleetSoft, fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ServerPickerSheet(
|
||||
configs: List<ProxyConfig>,
|
||||
activeId: String?,
|
||||
liveId: String?,
|
||||
onSelect: (String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
containerColor = Vojo.bgPanel,
|
||||
contentColor = Vojo.text,
|
||||
dragHandle = {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(top = 12.dp, bottom = 6.dp)
|
||||
.size(width = 38.dp, height = 4.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(Vojo.hairline)
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column(Modifier.padding(horizontal = 14.dp, vertical = 4.dp)) {
|
||||
SectionLabel("Сменить сервер", Modifier.padding(start = 4.dp, bottom = 10.dp))
|
||||
configs.forEach { cfg ->
|
||||
val isActive = cfg.id == activeId
|
||||
val isLive = cfg.id == liveId
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(if (isActive) Vojo.surface else Color.Transparent)
|
||||
.clickable { onSelect(cfg.id) }
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ProtocolBadge(cfg.type.badge, protoColor(cfg), size = 38)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
cfg.name,
|
||||
color = if (isLive) Vojo.green else Vojo.text,
|
||||
fontSize = 15.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(cfg.handle, color = Vojo.muted, fontFamily = Vojo.mono, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
if (isLive) OnlineDot(Vojo.green, 7)
|
||||
else if (isActive) OnlineDot(Vojo.fleetSoft, 7)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectButton(status: VpnState.Status, onConnect: () -> Unit, enabled: Boolean) {
|
||||
val bg = if (enabled) Vojo.fleet else Vojo.surface
|
||||
val fg = if (enabled) Color(0xFF0C0C0E) else Vojo.faint
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(58.dp)
|
||||
.clip(RoundedCornerShape(15.dp))
|
||||
.background(bg)
|
||||
.clickable(enabled = enabled) { onConnect() },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text("Подключиться", color = fg, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
|
||||
/** Non-interactive transition state shown while the tunnel comes up or tears down. */
|
||||
@Composable
|
||||
private fun ConnectingButton(status: VpnState.Status) {
|
||||
val label = if (status == VpnState.Status.STOPPING) "Отключение…" else "Подключение…"
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(58.dp)
|
||||
.clip(RoundedCornerShape(15.dp))
|
||||
.background(Vojo.surface),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CircularProgressIndicator(color = Vojo.fleetSoft, strokeWidth = 2.dp, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(label, color = Vojo.muted, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatsRow(stats: VpnState.Stats, connectedSince: Long) {
|
||||
// Only this leaf subscribes to the 1 Hz tick, so the rest of the screen (and
|
||||
// the pager) doesn't recompose every second while connected.
|
||||
val nowMs = rememberNowTicker()
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
StatTile("Отправлено", formatBytes(stats.txBytes), Modifier.weight(1f))
|
||||
StatTile("Получено", formatBytes(stats.rxBytes), Modifier.weight(1f))
|
||||
StatTile("Время", formatUptime(connectedSince, nowMs), Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ProduceStateDoesNotAssignValue") // false positive: value is assigned inside repeatOnLifecycle
|
||||
@Composable
|
||||
private fun rememberNowTicker(): Long {
|
||||
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||
val now by produceState(initialValue = System.currentTimeMillis(), lifecycle) {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
while (true) {
|
||||
value = System.currentTimeMillis()
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
return now
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatTile(label: String, value: String, modifier: Modifier = Modifier) {
|
||||
PanelCard(modifier) {
|
||||
Column(Modifier.padding(14.dp)) {
|
||||
Text(label, color = Vojo.faint, fontSize = 12.sp)
|
||||
Spacer(Modifier.height(5.dp))
|
||||
Text(value, color = Vojo.text, fontFamily = Vojo.mono, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Connection journal: a scrollable, auto-scrolling timeline rail of events. */
|
||||
@Composable
|
||||
private fun JournalList(
|
||||
events: List<VpnState.TunnelEvent>,
|
||||
pulseLast: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (events.isEmpty()) {
|
||||
Row(modifier) {
|
||||
Text(
|
||||
"—",
|
||||
Modifier.width(50.dp).padding(end = 12.dp),
|
||||
color = Vojo.faint, fontFamily = Vojo.mono, fontSize = 11.sp, textAlign = TextAlign.End,
|
||||
)
|
||||
Text("Готов к подключению", color = Vojo.muted, fontSize = 14.sp)
|
||||
}
|
||||
return
|
||||
}
|
||||
val listState = rememberLazyListState()
|
||||
// Auto-scroll to the newest event as it lands. Keyed on the last event's id,
|
||||
// NOT events.size — the ring buffer saturates at MAX_EVENTS, so size stops
|
||||
// changing while new events keep arriving.
|
||||
LaunchedEffect(events.lastOrNull()?.id) {
|
||||
if (events.isNotEmpty()) listState.animateScrollToItem(events.lastIndex)
|
||||
}
|
||||
LazyColumn(modifier, state = listState) {
|
||||
itemsIndexed(events, key = { _, e -> e.id }) { i, e ->
|
||||
val color = when (e.tone) {
|
||||
VpnState.TunnelEvent.Tone.OK -> Vojo.green
|
||||
VpnState.TunnelEvent.Tone.WARN -> Vojo.amber
|
||||
VpnState.TunnelEvent.Tone.ERROR -> Vojo.danger
|
||||
VpnState.TunnelEvent.Tone.NEUTRAL -> hourColor(eventHour(e.at))
|
||||
}
|
||||
RailRow(
|
||||
time = formatClock(e.at),
|
||||
label = e.label,
|
||||
sub = e.detail,
|
||||
color = color,
|
||||
error = e.tone == VpnState.TunnelEvent.Tone.ERROR,
|
||||
drawTop = i > 0,
|
||||
drawBottom = i < events.lastIndex,
|
||||
pulse = pulseLast && i == events.lastIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RailRow(
|
||||
time: String,
|
||||
label: String,
|
||||
sub: String?,
|
||||
color: Color,
|
||||
error: Boolean,
|
||||
drawTop: Boolean,
|
||||
drawBottom: Boolean,
|
||||
pulse: Boolean,
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth().height(if (sub.isNullOrBlank()) 42.dp else 50.dp)) {
|
||||
Text(
|
||||
time,
|
||||
Modifier.width(50.dp).padding(end = 12.dp, top = 3.dp),
|
||||
color = Vojo.faint, fontFamily = Vojo.mono, fontSize = 11.sp,
|
||||
textAlign = TextAlign.End, maxLines = 1,
|
||||
)
|
||||
Box(Modifier.width(16.dp).fillMaxHeight()) {
|
||||
// The rail line and the dot must share the same centre. The Column
|
||||
// fills the cell WIDTH so CenterHorizontally actually centres its 1dp
|
||||
// children; with only fillMaxHeight it shrink-wraps to 1dp and pins to
|
||||
// the cell's start, leaving the dot floating ~7dp to its right.
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(Modifier.width(1.dp).height(9.dp).background(if (drawTop) Vojo.rail else Color.Transparent))
|
||||
Box(Modifier.weight(1f).width(1.dp).background(if (drawBottom) Vojo.rail else Color.Transparent))
|
||||
}
|
||||
RailDot(color, pulse, Modifier.align(Alignment.TopCenter).padding(top = 2.dp))
|
||||
}
|
||||
Spacer(Modifier.width(14.dp))
|
||||
Column {
|
||||
Text(
|
||||
label,
|
||||
color = if (error) Vojo.danger else Vojo.text,
|
||||
fontSize = 14.5.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (!sub.isNullOrBlank()) {
|
||||
Text(sub, color = Vojo.muted, fontFamily = Vojo.mono, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RailDot(color: Color, pulse: Boolean, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier.size(15.dp).clip(CircleShape).background(Vojo.bgApp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
var dot = Modifier.size(9.dp)
|
||||
if (pulse) {
|
||||
val t = rememberInfiniteTransition(label = "rail-pulse")
|
||||
val scale by t.animateFloat(
|
||||
initialValue = 0.85f, targetValue = 1.2f,
|
||||
animationSpec = infiniteRepeatable(tween(900, easing = LinearEasing), RepeatMode.Reverse),
|
||||
label = "dot",
|
||||
)
|
||||
dot = dot.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
}
|
||||
Box(dot.clip(CircleShape).background(color))
|
||||
}
|
||||
}
|
||||
152
app/src/main/java/chat/vojo/proxy/ui/Fields.kt
Normal file
152
app/src/main/java/chat/vojo/proxy/ui/Fields.kt
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
package chat.vojo.proxy.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.vojo.proxy.ui.theme.Vojo
|
||||
|
||||
@Composable
|
||||
fun VojoTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
label: String,
|
||||
placeholder: String = "",
|
||||
modifier: Modifier = Modifier,
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
mono: Boolean = false,
|
||||
isPassword: Boolean = false,
|
||||
error: String? = null,
|
||||
) {
|
||||
var reveal by rememberSaveable { mutableStateOf(false) }
|
||||
Column(modifier) {
|
||||
SectionLabel(label)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(Vojo.bgApp)
|
||||
.border(1.dp, if (error != null) Vojo.danger.copy(alpha = 0.55f) else Vojo.hairline, RoundedCornerShape(10.dp))
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
textStyle = TextStyle(
|
||||
color = Vojo.text,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = if (mono) Vojo.mono else FontFamily.Default,
|
||||
),
|
||||
cursorBrush = SolidColor(Vojo.fleet),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = if (isPassword) KeyboardType.Password else keyboardType,
|
||||
),
|
||||
visualTransformation = if (isPassword && !reveal) PasswordVisualTransformation() else VisualTransformation.None,
|
||||
decorationBox = { inner ->
|
||||
Box(Modifier.padding(vertical = 15.dp)) {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
placeholder,
|
||||
color = Vojo.faint,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = if (mono) Vojo.mono else FontFamily.Default,
|
||||
)
|
||||
}
|
||||
inner()
|
||||
}
|
||||
},
|
||||
)
|
||||
if (isPassword) {
|
||||
Icon(
|
||||
if (reveal) VojoIcons.EyeOff else VojoIcons.Eye,
|
||||
contentDescription = if (reveal) "Скрыть пароль" else "Показать пароль",
|
||||
tint = Vojo.faint,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.clickable { reveal = !reveal }
|
||||
.padding(6.dp)
|
||||
.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (error != null) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(error, color = Vojo.danger, fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> VojoDropdown(
|
||||
label: String,
|
||||
options: List<T>,
|
||||
selected: T,
|
||||
optionLabel: (T) -> String,
|
||||
onSelect: (T) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Column(modifier) {
|
||||
SectionLabel(label)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Box {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(Vojo.bgApp)
|
||||
.border(1.dp, Vojo.hairline, RoundedCornerShape(10.dp))
|
||||
.clickable { expanded = true }
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(optionLabel(selected), color = Vojo.text, fontSize = 15.sp, modifier = Modifier.weight(1f))
|
||||
Icon(VojoIcons.ChevronDown, contentDescription = null, tint = Vojo.muted, modifier = Modifier.size(14.dp))
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
options.forEach { opt ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(optionLabel(opt), color = Vojo.text, fontSize = 14.sp) },
|
||||
onClick = { onSelect(opt); expanded = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
140
app/src/main/java/chat/vojo/proxy/ui/Icons.kt
Normal file
140
app/src/main/java/chat/vojo/proxy/ui/Icons.kt
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package chat.vojo.proxy.ui
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.PathBuilder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Thin stroke icons (1.7dp, round caps) matching the design bundle's shared.jsx
|
||||
* icon set — unicode glyphs render inconsistently across system fallback fonts.
|
||||
* Tinted via Icon(..., tint = ...).
|
||||
*/
|
||||
object VojoIcons {
|
||||
|
||||
private inline fun stroke(name: String, crossinline body: PathBuilder.() -> Unit): ImageVector =
|
||||
ImageVector.Builder(
|
||||
name = name, defaultWidth = 24.dp, defaultHeight = 24.dp,
|
||||
viewportWidth = 24f, viewportHeight = 24f,
|
||||
).apply {
|
||||
path(
|
||||
fill = null,
|
||||
stroke = SolidColor(Color.White),
|
||||
strokeLineWidth = 1.7f,
|
||||
strokeLineCap = StrokeCap.Round,
|
||||
strokeLineJoin = StrokeJoin.Round,
|
||||
) { body() }
|
||||
}.build()
|
||||
|
||||
private inline fun fill(name: String, crossinline body: PathBuilder.() -> Unit): ImageVector =
|
||||
ImageVector.Builder(
|
||||
name = name, defaultWidth = 24.dp, defaultHeight = 24.dp,
|
||||
viewportWidth = 24f, viewportHeight = 24f,
|
||||
).apply {
|
||||
path(fill = SolidColor(Color.White)) { body() }
|
||||
}.build()
|
||||
|
||||
private fun PathBuilder.circle(cx: Float, cy: Float, r: Float) {
|
||||
moveTo(cx + r, cy)
|
||||
arcTo(r, r, 0f, isMoreThanHalf = false, isPositiveArc = true, x1 = cx - r, y1 = cy)
|
||||
arcTo(r, r, 0f, isMoreThanHalf = false, isPositiveArc = true, x1 = cx + r, y1 = cy)
|
||||
close()
|
||||
}
|
||||
|
||||
val Search: ImageVector by lazy {
|
||||
stroke("search") {
|
||||
moveTo(18f, 11f)
|
||||
arcTo(7f, 7f, 0f, isMoreThanHalf = false, isPositiveArc = true, x1 = 4f, y1 = 11f)
|
||||
arcTo(7f, 7f, 0f, isMoreThanHalf = false, isPositiveArc = true, x1 = 18f, y1 = 11f)
|
||||
moveTo(16f, 16f)
|
||||
lineTo(20f, 20f)
|
||||
}
|
||||
}
|
||||
|
||||
val More: ImageVector by lazy {
|
||||
fill("more") {
|
||||
circle(5f, 12f, 1.6f)
|
||||
circle(12f, 12f, 1.6f)
|
||||
circle(19f, 12f, 1.6f)
|
||||
}
|
||||
}
|
||||
|
||||
val ChevronDown: ImageVector by lazy {
|
||||
stroke("chevron-down") {
|
||||
moveTo(6f, 9.5f)
|
||||
lineTo(12f, 15.5f)
|
||||
lineTo(18f, 9.5f)
|
||||
}
|
||||
}
|
||||
|
||||
val Check: ImageVector by lazy {
|
||||
stroke("check") {
|
||||
moveTo(5f, 12.5f)
|
||||
lineTo(10f, 17.5f)
|
||||
lineTo(19f, 6.5f)
|
||||
}
|
||||
}
|
||||
|
||||
val Plus: ImageVector by lazy {
|
||||
stroke("plus") {
|
||||
moveTo(12f, 5f)
|
||||
verticalLineTo(19f)
|
||||
moveTo(5f, 12f)
|
||||
horizontalLineTo(19f)
|
||||
}
|
||||
}
|
||||
|
||||
val Eye: ImageVector by lazy {
|
||||
stroke("eye") {
|
||||
moveTo(2f, 12f)
|
||||
curveTo(5f, 5.8f, 19f, 5.8f, 22f, 12f)
|
||||
curveTo(19f, 18.2f, 5f, 18.2f, 2f, 12f)
|
||||
close()
|
||||
circle(12f, 12f, 3f)
|
||||
}
|
||||
}
|
||||
|
||||
val EyeOff: ImageVector by lazy {
|
||||
stroke("eye-off") {
|
||||
moveTo(2f, 12f)
|
||||
curveTo(5f, 5.8f, 19f, 5.8f, 22f, 12f)
|
||||
curveTo(19f, 18.2f, 5f, 18.2f, 2f, 12f)
|
||||
close()
|
||||
moveTo(5f, 3.5f)
|
||||
lineTo(19f, 20.5f)
|
||||
}
|
||||
}
|
||||
|
||||
val Cross: ImageVector by lazy {
|
||||
stroke("cross") {
|
||||
moveTo(6f, 6f)
|
||||
lineTo(18f, 18f)
|
||||
moveTo(18f, 6f)
|
||||
lineTo(6f, 18f)
|
||||
}
|
||||
}
|
||||
|
||||
val Clipboard: ImageVector by lazy {
|
||||
stroke("clipboard") {
|
||||
moveTo(9f, 5f)
|
||||
horizontalLineTo(6f)
|
||||
arcTo(1.5f, 1.5f, 0f, isMoreThanHalf = false, isPositiveArc = false, x1 = 4.5f, y1 = 6.5f)
|
||||
verticalLineTo(19.5f)
|
||||
arcTo(1.5f, 1.5f, 0f, isMoreThanHalf = false, isPositiveArc = false, x1 = 6f, y1 = 21f)
|
||||
horizontalLineTo(18f)
|
||||
arcTo(1.5f, 1.5f, 0f, isMoreThanHalf = false, isPositiveArc = false, x1 = 19.5f, y1 = 19.5f)
|
||||
verticalLineTo(6.5f)
|
||||
arcTo(1.5f, 1.5f, 0f, isMoreThanHalf = false, isPositiveArc = false, x1 = 18f, y1 = 5f)
|
||||
horizontalLineTo(15f)
|
||||
moveTo(9f, 3.5f)
|
||||
horizontalLineTo(15f)
|
||||
verticalLineTo(6.5f)
|
||||
horizontalLineTo(9f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
174
app/src/main/java/chat/vojo/proxy/ui/MainScreen.kt
Normal file
174
app/src/main/java/chat/vojo/proxy/ui/MainScreen.kt
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
package chat.vojo.proxy.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.tappableElement
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerDefaults
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.vojo.proxy.service.VpnState
|
||||
import chat.vojo.proxy.ui.theme.Vojo
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
vm: MainViewModel,
|
||||
onConnect: () -> Unit,
|
||||
onDisconnect: () -> Unit,
|
||||
onReconnect: () -> Unit,
|
||||
) {
|
||||
val proxyState by vm.state.collectAsState()
|
||||
val status by vm.status.collectAsState()
|
||||
val stats by vm.stats.collectAsState()
|
||||
val error by vm.error.collectAsState()
|
||||
val connectedSince by vm.connectedSince.collectAsState()
|
||||
val events by vm.events.collectAsState()
|
||||
val applied by vm.applied.collectAsState()
|
||||
val apps by vm.apps.collectAsState()
|
||||
val appsLoading by vm.appsLoading.collectAsState()
|
||||
|
||||
val tabs = listOf("Туннель", "Серверы", "Приложения", "Опции")
|
||||
val pagerState = rememberPagerState(pageCount = { tabs.size })
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var editorOpen by rememberSaveable { mutableStateOf(false) }
|
||||
var editorInitialId by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
|
||||
val active = proxyState.activeConfig
|
||||
val settings = proxyState.settings
|
||||
val connected = status == VpnState.Status.CONNECTED
|
||||
// The config the running tunnel was actually built from (null when down).
|
||||
// Green/live styling keys off this, NOT the merely-selected server.
|
||||
val liveId = if (connected) applied?.configId else null
|
||||
|
||||
// Static backdrop (#181A20) stretched edge-to-edge over the whole window —
|
||||
// shows through the inter-pane gap during a swipe and behind the system bars.
|
||||
Box(Modifier.fillMaxSize().background(Vojo.bgPanel)) {
|
||||
// Only the TOP (status bar / cutout) and SIDES are padded here — the
|
||||
// sheets reach the very bottom so the backdrop never shows as a strip
|
||||
// below them. The bottom system inset is applied INSIDE each sheet.
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.windowInsetsPadding(
|
||||
WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
),
|
||||
) {
|
||||
SegmentTabs(
|
||||
tabs = tabs,
|
||||
pagerState = pagerState,
|
||||
onSelect = {
|
||||
scope.launch { pagerState.animateScrollToPage(it, animationSpec = tween(260, easing = FastOutSlowInEasing)) }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(8.dp)) // backdrop strip above the sheets
|
||||
|
||||
// The panes are sheets (#0D0E11) with a rounded top, sliding on the
|
||||
// backdrop like a gallery; pageSpacing surfaces the backdrop gap.
|
||||
// A short ease-out snap keeps the settle from feeling sluggish.
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
pageSpacing = 18.dp,
|
||||
beyondViewportPageCount = 1,
|
||||
flingBehavior = PagerDefaults.flingBehavior(
|
||||
state = pagerState,
|
||||
snapAnimationSpec = tween(260, easing = FastOutSlowInEasing),
|
||||
),
|
||||
) { page ->
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(topStart = 26.dp, topEnd = 26.dp))
|
||||
.background(Vojo.bgApp),
|
||||
) {
|
||||
// Sheet background reaches the bottom edge; its CONTENT clears
|
||||
// the 3-button nav bar via tappableElement (0 in gesture mode,
|
||||
// so gesture nav stays truly edge-to-edge).
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.windowInsetsPadding(WindowInsets.tappableElement.only(WindowInsetsSides.Bottom)),
|
||||
) {
|
||||
when (page) {
|
||||
0 -> ConnectionScreen(
|
||||
activeConfig = active,
|
||||
configs = proxyState.configs,
|
||||
status = status,
|
||||
stats = stats,
|
||||
connectedSince = connectedSince,
|
||||
error = error,
|
||||
events = events,
|
||||
applied = applied,
|
||||
settings = settings,
|
||||
liveId = liveId,
|
||||
onConnect = onConnect,
|
||||
onDisconnect = onDisconnect,
|
||||
onReconnect = onReconnect,
|
||||
onSelectServer = { vm.setActive(it) },
|
||||
)
|
||||
1 -> ServersScreen(
|
||||
configs = proxyState.configs,
|
||||
activeId = active?.id,
|
||||
liveId = liveId,
|
||||
onSelect = { vm.setActive(it) },
|
||||
onAdd = { editorInitialId = null; editorOpen = true },
|
||||
onEdit = { editorInitialId = it.id; editorOpen = true },
|
||||
onDelete = { vm.deleteConfig(it) },
|
||||
)
|
||||
2 -> AppsScreen(
|
||||
apps = apps,
|
||||
loading = appsLoading,
|
||||
selected = settings.allowedApps,
|
||||
loadIcon = { vm.iconFor(it) },
|
||||
cachedIcon = { vm.cachedIcon(it) },
|
||||
onToggle = { pkg, on -> vm.toggleApp(pkg, on) },
|
||||
onClear = { vm.clearApps() },
|
||||
onLoad = { vm.loadApps() },
|
||||
)
|
||||
3 -> SettingsScreen(
|
||||
settings = settings,
|
||||
onChange = { transform -> vm.updateSettings(transform) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (editorOpen) {
|
||||
ServerEditorDialog(
|
||||
initial = remember(editorInitialId, proxyState.configs) {
|
||||
proxyState.configs.firstOrNull { it.id == editorInitialId }
|
||||
},
|
||||
onSave = { vm.saveConfig(it); editorOpen = false },
|
||||
onDismiss = { editorOpen = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
98
app/src/main/java/chat/vojo/proxy/ui/MainViewModel.kt
Normal file
98
app/src/main/java/chat/vojo/proxy/ui/MainViewModel.kt
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package chat.vojo.proxy.ui
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import chat.vojo.proxy.core.AppSettings
|
||||
import chat.vojo.proxy.core.ProxyConfig
|
||||
import chat.vojo.proxy.data.AppInfo
|
||||
import chat.vojo.proxy.data.AppRepository
|
||||
import chat.vojo.proxy.data.ConfigStore
|
||||
import chat.vojo.proxy.data.ProxyState
|
||||
import chat.vojo.proxy.service.ProxyVpnService
|
||||
import chat.vojo.proxy.service.VpnState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
private val store = ConfigStore(app)
|
||||
private val appRepo = AppRepository(app)
|
||||
|
||||
val state: StateFlow<ProxyState> =
|
||||
store.state.stateIn(viewModelScope, SharingStarted.Eagerly, ProxyState())
|
||||
|
||||
val status: StateFlow<VpnState.Status> = VpnState.status
|
||||
val stats: StateFlow<VpnState.Stats> = VpnState.stats
|
||||
val error: StateFlow<String?> = VpnState.error
|
||||
val connectedSince: StateFlow<Long> = VpnState.connectedSince
|
||||
val events: StateFlow<List<VpnState.TunnelEvent>> = VpnState.events
|
||||
val applied: StateFlow<VpnState.Applied?> = VpnState.applied
|
||||
|
||||
private val _apps = MutableStateFlow<List<AppInfo>>(emptyList())
|
||||
val apps: StateFlow<List<AppInfo>> = _apps.asStateFlow()
|
||||
|
||||
private val _appsLoading = MutableStateFlow(false)
|
||||
val appsLoading: StateFlow<Boolean> = _appsLoading.asStateFlow()
|
||||
|
||||
/** Refreshes on every screen entry so newly installed apps show up. */
|
||||
fun loadApps() {
|
||||
if (_appsLoading.value) return
|
||||
_appsLoading.value = true
|
||||
viewModelScope.launch {
|
||||
_apps.value = appRepo.installedApps()
|
||||
_appsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun iconFor(packageName: String): ImageBitmap? = appRepo.icon(packageName)
|
||||
|
||||
fun cachedIcon(packageName: String): ImageBitmap? = appRepo.cachedIcon(packageName)
|
||||
|
||||
fun setActive(id: String) = viewModelScope.launch {
|
||||
val changing = id != state.value.activeConfig?.id
|
||||
store.setActive(id)
|
||||
// Switching servers while the tunnel runs must actually move the traffic.
|
||||
if (changing && VpnState.status.value in RUNNING_STATES) {
|
||||
ProxyVpnService.restart(getApplication())
|
||||
}
|
||||
}
|
||||
|
||||
fun reconnect() = ProxyVpnService.restart(getApplication())
|
||||
|
||||
fun saveConfig(config: ProxyConfig) = viewModelScope.launch { store.upsertConfig(config) }
|
||||
fun deleteConfig(id: String) = viewModelScope.launch { store.deleteConfig(id) }
|
||||
|
||||
fun toggleApp(packageName: String, enabled: Boolean) = viewModelScope.launch {
|
||||
store.updateSettings { s ->
|
||||
val apps = if (enabled) s.allowedApps + packageName else s.allowedApps - packageName
|
||||
s.copy(allowedApps = apps)
|
||||
}
|
||||
// Whitelist changes only take effect at establish() time — re-apply the
|
||||
// tunnel live so toggling an app on/off routes immediately, mirroring the
|
||||
// auto-reconnect on an active-server switch.
|
||||
reconnectIfRunning()
|
||||
}
|
||||
|
||||
fun clearApps() = viewModelScope.launch {
|
||||
store.updateSettings { it.copy(allowedApps = emptySet()) }
|
||||
reconnectIfRunning()
|
||||
}
|
||||
|
||||
private fun reconnectIfRunning() {
|
||||
if (VpnState.status.value in RUNNING_STATES) ProxyVpnService.restart(getApplication())
|
||||
}
|
||||
|
||||
fun updateSettings(transform: (AppSettings) -> AppSettings) = viewModelScope.launch {
|
||||
store.updateSettings(transform)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val RUNNING_STATES = setOf(VpnState.Status.CONNECTED, VpnState.Status.CONNECTING)
|
||||
}
|
||||
}
|
||||
421
app/src/main/java/chat/vojo/proxy/ui/ServersScreen.kt
Normal file
421
app/src/main/java/chat/vojo/proxy/ui/ServersScreen.kt
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
package chat.vojo.proxy.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import chat.vojo.proxy.core.ProxyConfig
|
||||
import chat.vojo.proxy.core.ProxyType
|
||||
import chat.vojo.proxy.core.SsCipher
|
||||
import chat.vojo.proxy.core.parseProxyUri
|
||||
import chat.vojo.proxy.core.ssRequiresPlugin
|
||||
import chat.vojo.proxy.ui.theme.Vojo
|
||||
|
||||
private fun typeColor(type: ProxyType): Color = when (type) {
|
||||
ProxyType.SHADOWSOCKS -> Vojo.fleet
|
||||
ProxyType.SOCKS5 -> Vojo.blue
|
||||
ProxyType.HTTP -> Vojo.amber
|
||||
ProxyType.HTTPS -> Vojo.teal
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ServersScreen(
|
||||
configs: List<ProxyConfig>,
|
||||
activeId: String?,
|
||||
liveId: String?,
|
||||
onSelect: (String) -> Unit,
|
||||
onAdd: () -> Unit,
|
||||
onEdit: (ProxyConfig) -> Unit,
|
||||
onDelete: (String) -> Unit,
|
||||
) {
|
||||
var confirmDelete by remember { mutableStateOf<ProxyConfig?>(null) }
|
||||
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
if (configs.isEmpty()) {
|
||||
EmptyServers(onAdd)
|
||||
} else {
|
||||
LazyColumn(
|
||||
Modifier.fillMaxSize().padding(horizontal = 14.dp),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(top = 14.dp, bottom = 28.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
item {
|
||||
SectionLabel("Серверы · ${configs.size}", Modifier.padding(start = 4.dp, bottom = 4.dp))
|
||||
}
|
||||
items(configs, key = { it.id }) { cfg ->
|
||||
ServerRow(
|
||||
cfg = cfg,
|
||||
selected = cfg.id == activeId,
|
||||
live = cfg.id == liveId,
|
||||
onClick = { onSelect(cfg.id) },
|
||||
onEdit = { onEdit(cfg) },
|
||||
onDelete = { confirmDelete = cfg },
|
||||
)
|
||||
}
|
||||
item {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(54.dp)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.border(1.dp, Vojo.hairline, RoundedCornerShape(14.dp))
|
||||
.clickable { onAdd() },
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(VojoIcons.Plus, contentDescription = null, tint = Vojo.fleetSoft, modifier = Modifier.size(17.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Добавить сервер", color = Vojo.fleetSoft, fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
confirmDelete?.let { cfg ->
|
||||
ConfirmDeleteDialog(
|
||||
config = cfg,
|
||||
onConfirm = { onDelete(cfg.id); confirmDelete = null },
|
||||
onDismiss = { confirmDelete = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerRow(
|
||||
cfg: ProxyConfig,
|
||||
selected: Boolean,
|
||||
live: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onEdit: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
var menu by remember { mutableStateOf(false) }
|
||||
PanelCard(
|
||||
Modifier.fillMaxWidth(),
|
||||
onClick = onClick,
|
||||
accent = when {
|
||||
live -> Vojo.green.copy(alpha = 0.5f)
|
||||
selected -> Vojo.fleet.copy(alpha = 0.5f)
|
||||
else -> null
|
||||
},
|
||||
background = if (selected) Vojo.surface else Vojo.bgPanel,
|
||||
) {
|
||||
Row(Modifier.padding(14.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
// Radio marks the SELECTED (active) server; the green name marks the
|
||||
// one that's actually LIVE in the tunnel right now.
|
||||
SelectRadio(selected)
|
||||
Spacer(Modifier.width(13.dp))
|
||||
ProtocolBadge(cfg.type.badge, typeColor(cfg.type), size = 42)
|
||||
Spacer(Modifier.width(13.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
cfg.name,
|
||||
color = if (live) Vojo.green else Vojo.text,
|
||||
fontSize = 15.sp, fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (live) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
OnlineDot(Vojo.green, 7)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Text(
|
||||
cfg.handle,
|
||||
color = Vojo.muted, fontFamily = Vojo.mono, fontSize = 13.sp,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Box {
|
||||
Icon(
|
||||
VojoIcons.More,
|
||||
contentDescription = "Меню сервера",
|
||||
tint = Vojo.muted,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.clickable { menu = true }
|
||||
.padding(12.dp)
|
||||
.size(20.dp),
|
||||
)
|
||||
DropdownMenu(expanded = menu, onDismissRequest = { menu = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Изменить", color = Vojo.text) },
|
||||
onClick = { menu = false; onEdit() },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Удалить", color = Vojo.danger) },
|
||||
onClick = { menu = false; onDelete() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectRadio(selected: Boolean) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(22.dp)
|
||||
.clip(CircleShape)
|
||||
.border(2.dp, if (selected) Vojo.fleet else Vojo.hairline, CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (selected) Box(Modifier.size(11.dp).clip(CircleShape).background(Vojo.fleet))
|
||||
}
|
||||
}
|
||||
|
||||
/** Deleting a server (and its password) is destructive — confirm explicitly. */
|
||||
@Composable
|
||||
private fun ConfirmDeleteDialog(config: ProxyConfig, onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(Vojo.bgPanel)
|
||||
.border(1.dp, Vojo.hairline, RoundedCornerShape(18.dp))
|
||||
.padding(20.dp),
|
||||
) {
|
||||
Text("Удалить сервер?", color = Vojo.text, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
"«${config.name}» и его пароль будут удалены безвозвратно.",
|
||||
color = Vojo.muted, fontSize = 14.sp,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(config.handle, color = Vojo.faint, fontFamily = Vojo.mono, fontSize = 13.sp)
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Box(
|
||||
Modifier.weight(1f).height(48.dp).clip(RoundedCornerShape(12.dp))
|
||||
.border(1.dp, Vojo.hairline, RoundedCornerShape(12.dp))
|
||||
.clickable { onDismiss() },
|
||||
contentAlignment = Alignment.Center,
|
||||
) { Text("Отмена", color = Vojo.muted, fontSize = 15.sp) }
|
||||
Box(
|
||||
Modifier.weight(1f).height(48.dp).clip(RoundedCornerShape(12.dp))
|
||||
.background(Vojo.danger)
|
||||
.clickable { onConfirm() },
|
||||
contentAlignment = Alignment.Center,
|
||||
) { Text("Удалить", color = Color(0xFF0C0C0E), fontWeight = FontWeight.SemiBold, fontSize = 15.sp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyServers(onAdd: () -> Unit) {
|
||||
Column(
|
||||
Modifier.fillMaxSize().padding(32.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text("Нет серверов", color = Vojo.text, fontSize = 20.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
"Добавьте Shadowsocks, SOCKS5 или HTTP(S) прокси, чтобы начать.",
|
||||
color = Vojo.muted, fontSize = 14.sp,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Row(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(13.dp))
|
||||
.background(Vojo.fleet)
|
||||
.clickable { onAdd() }
|
||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(VojoIcons.Plus, contentDescription = null, tint = Color(0xFF0C0C0E), modifier = Modifier.size(17.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Добавить сервер", color = Color(0xFF0C0C0E), fontWeight = FontWeight.SemiBold, fontSize = 15.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Add / edit form rendered as a dialog. */
|
||||
@Composable
|
||||
fun ServerEditorDialog(
|
||||
initial: ProxyConfig?,
|
||||
onSave: (ProxyConfig) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var name by rememberSaveable { mutableStateOf(initial?.name ?: "") }
|
||||
var type by rememberSaveable { mutableStateOf(initial?.type ?: ProxyType.SHADOWSOCKS) }
|
||||
var host by rememberSaveable { mutableStateOf(initial?.host ?: "") }
|
||||
var port by rememberSaveable { mutableStateOf(initial?.port?.toString() ?: "") }
|
||||
var username by rememberSaveable { mutableStateOf(initial?.username ?: "") }
|
||||
var password by rememberSaveable { mutableStateOf(initial?.password ?: "") }
|
||||
var cipher by rememberSaveable { mutableStateOf(initial?.cipher ?: SsCipher.AES_256_GCM) }
|
||||
var errorText by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val clipboard = LocalClipboardManager.current
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
// Typed-in credentials must survive an accidental tap outside.
|
||||
properties = DialogProperties(dismissOnClickOutside = false),
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(Vojo.bgPanel)
|
||||
.border(1.dp, Vojo.hairline, RoundedCornerShape(18.dp))
|
||||
.padding(20.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
if (initial == null) "Новый сервер" else "Изменить сервер",
|
||||
color = Vojo.text, fontSize = 19.sp, fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
// Most SS configs travel as ss:// links — fill the form from one.
|
||||
Row(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable {
|
||||
val text = clipboard.getText()?.text ?: ""
|
||||
val parsed = parseProxyUri(text)
|
||||
when {
|
||||
parsed != null -> {
|
||||
name = parsed.name
|
||||
type = parsed.type
|
||||
host = parsed.host
|
||||
port = parsed.port.toString()
|
||||
username = parsed.username
|
||||
password = parsed.password
|
||||
cipher = parsed.cipher
|
||||
errorText = null
|
||||
}
|
||||
ssRequiresPlugin(text) ->
|
||||
errorText = "Ссылка ss:// требует плагин (obfs/v2ray) — он не поддерживается"
|
||||
else ->
|
||||
errorText = "В буфере нет ссылки ss:// / socks5:// / http(s)://"
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(VojoIcons.Clipboard, contentDescription = null, tint = Vojo.fleetSoft, modifier = Modifier.size(15.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text("вставить", color = Vojo.fleetSoft, fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(18.dp))
|
||||
|
||||
VojoTextField(name, { name = it }, "Имя", "Мой сервер")
|
||||
Spacer(Modifier.height(14.dp))
|
||||
|
||||
VojoDropdown(
|
||||
label = "Протокол",
|
||||
options = ProxyType.entries,
|
||||
selected = type,
|
||||
optionLabel = { it.label },
|
||||
onSelect = { type = it },
|
||||
)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
VojoTextField(host, { host = it }, "Адрес", "example.com", Modifier.weight(2f), mono = true)
|
||||
VojoTextField(port, { port = it.filter(Char::isDigit).take(5) }, "Порт", "8388", Modifier.weight(1f), keyboardType = androidx.compose.ui.text.input.KeyboardType.Number, mono = true)
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
|
||||
when (type) {
|
||||
ProxyType.SHADOWSOCKS -> {
|
||||
VojoDropdown(
|
||||
label = "Шифр",
|
||||
options = SsCipher.entries,
|
||||
selected = cipher,
|
||||
optionLabel = { it.id },
|
||||
onSelect = { cipher = it },
|
||||
)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
VojoTextField(password, { password = it }, "Пароль", "пароль Shadowsocks", isPassword = true, mono = true)
|
||||
}
|
||||
else -> {
|
||||
VojoTextField(username, { username = it }, "Логин (необязательно)", "user", mono = true)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
VojoTextField(password, { password = it }, "Пароль (необязательно)", "••••", isPassword = true, mono = true)
|
||||
}
|
||||
}
|
||||
|
||||
if (errorText != null) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(errorText!!, color = Vojo.danger, fontSize = 13.sp)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(22.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Box(
|
||||
Modifier.weight(1f).height(50.dp).clip(RoundedCornerShape(12.dp))
|
||||
.border(1.dp, Vojo.hairline, RoundedCornerShape(12.dp))
|
||||
.clickable { onDismiss() },
|
||||
contentAlignment = Alignment.Center,
|
||||
) { Text("Отмена", color = Vojo.muted, fontSize = 15.sp) }
|
||||
Box(
|
||||
Modifier.weight(1f).height(50.dp).clip(RoundedCornerShape(12.dp))
|
||||
.background(Vojo.fleet)
|
||||
.clickable {
|
||||
val cfg = ProxyConfig(
|
||||
id = initial?.id ?: java.util.UUID.randomUUID().toString(),
|
||||
name = name.trim(),
|
||||
type = type,
|
||||
host = host.trim(),
|
||||
port = port.toIntOrNull() ?: 0,
|
||||
username = username.trim(),
|
||||
password = password,
|
||||
cipher = cipher,
|
||||
)
|
||||
val err = cfg.validationError()
|
||||
if (err != null) errorText = err else onSave(cfg)
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) { Text("Сохранить", color = Color(0xFF0C0C0E), fontWeight = FontWeight.SemiBold, fontSize = 15.sp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
144
app/src/main/java/chat/vojo/proxy/ui/SettingsScreen.kt
Normal file
144
app/src/main/java/chat/vojo/proxy/ui/SettingsScreen.kt
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package chat.vojo.proxy.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.vojo.proxy.core.AppSettings
|
||||
import chat.vojo.proxy.ui.theme.Vojo
|
||||
|
||||
/** Loose but useful: enough to keep garbage out of VpnService.addDnsServer. */
|
||||
private fun isValidIp(s: String): Boolean {
|
||||
if (s.isBlank()) return false
|
||||
return if (':' in s) {
|
||||
s.matches(Regex("^[0-9a-fA-F:]{2,45}$")) && s.count { it == ':' } in 1..8
|
||||
} else {
|
||||
val parts = s.split('.')
|
||||
parts.size == 4 && parts.all { p -> p.toIntOrNull()?.let { it in 0..255 } == true && p.length <= 3 }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
settings: AppSettings,
|
||||
onChange: ((AppSettings) -> AppSettings) -> Unit,
|
||||
) {
|
||||
var dnsEdit by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var mtuEdit by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
|
||||
val dnsValue = dnsEdit ?: settings.dns
|
||||
val mtuValue = mtuEdit ?: settings.mtu.toString()
|
||||
val dnsError = if (dnsEdit != null && !isValidIp(dnsValue.trim())) "Не похоже на IP-адрес — не сохранено" else null
|
||||
val mtuError = if (mtuEdit != null && (mtuValue.toIntOrNull() == null || mtuValue.toInt() !in 1000..9000)) "Допустимо 1000–9000 — не сохранено" else null
|
||||
|
||||
Column(
|
||||
// imePadding so the keyboard never covers the DNS/MTU fields under
|
||||
// edge-to-edge (the window isn't resized when decor fits system windows).
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).imePadding().padding(18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
SectionLabel("Сеть")
|
||||
|
||||
VojoTextField(
|
||||
value = dnsValue,
|
||||
onValueChange = {
|
||||
dnsEdit = it
|
||||
val v = it.trim()
|
||||
if (isValidIp(v)) onChange { s -> s.copy(dns = v) }
|
||||
},
|
||||
label = "DNS-сервер",
|
||||
placeholder = "1.1.1.1",
|
||||
mono = true,
|
||||
keyboardType = KeyboardType.Ascii,
|
||||
error = dnsError,
|
||||
)
|
||||
Text("DNS-запросы идут через туннель — провайдер их не видит.", color = Vojo.faint, fontSize = 12.sp)
|
||||
|
||||
VojoTextField(
|
||||
value = mtuValue,
|
||||
onValueChange = {
|
||||
val v = it.filter(Char::isDigit).take(4)
|
||||
mtuEdit = v
|
||||
v.toIntOrNull()?.let { n -> if (n in 1000..9000) onChange { s -> s.copy(mtu = n) } }
|
||||
},
|
||||
label = "MTU",
|
||||
placeholder = "8500",
|
||||
mono = true,
|
||||
keyboardType = KeyboardType.Number,
|
||||
error = mtuError,
|
||||
)
|
||||
|
||||
ToggleRow(
|
||||
title = "IPv6",
|
||||
subtitle = "Маршрутизировать IPv6-трафик через туннель",
|
||||
checked = settings.ipv6,
|
||||
onToggle = { onChange { s -> s.copy(ipv6 = it) } },
|
||||
)
|
||||
|
||||
Text(
|
||||
"Изменения применяются при следующем подключении.",
|
||||
color = Vojo.faint, fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleRow(title: String, subtitle: String, checked: Boolean, onToggle: (Boolean) -> Unit) {
|
||||
PanelCard(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
Modifier
|
||||
.toggleable(value = checked, role = Role.Switch) { onToggle(it) }
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(title, color = Vojo.text, fontSize = 15.sp, fontWeight = FontWeight.Medium)
|
||||
Text(subtitle, color = Vojo.muted, fontSize = 12.5.sp)
|
||||
}
|
||||
Switch(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Switch(checked: Boolean) {
|
||||
Box(
|
||||
Modifier
|
||||
.width(46.dp)
|
||||
.height(26.dp)
|
||||
.clip(RoundedCornerShape(13.dp))
|
||||
.background(if (checked) Vojo.fleet else Vojo.surface2)
|
||||
.padding(3.dp),
|
||||
contentAlignment = if (checked) Alignment.CenterEnd else Alignment.CenterStart,
|
||||
) {
|
||||
Box(Modifier.size(20.dp).clip(RoundedCornerShape(10.dp)).background(Color.White))
|
||||
}
|
||||
}
|
||||
84
app/src/main/java/chat/vojo/proxy/ui/theme/Theme.kt
Normal file
84
app/src/main/java/chat/vojo/proxy/ui/theme/Theme.kt
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package chat.vojo.proxy.ui.theme
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* The "Dawn / Fleet" palette ported from the vojo design bundle: near-black
|
||||
* surfaces, hairline dividers, a single violet accent, colour used only for
|
||||
* status. Monospace is reserved for technical metadata (host:port, ciphers).
|
||||
*/
|
||||
object Vojo {
|
||||
val bgApp = Color(0xFF0D0E11) // app background
|
||||
val bgPanel = Color(0xFF181A20) // cards / panels
|
||||
val surface = Color(0xFF21232B) // raised
|
||||
val surface2 = Color(0xFF2A2D36) // hover / active
|
||||
|
||||
val divider = Color(0x0FFFFFFF) // white 6%
|
||||
val hairline = Color(0x14FFFFFF) // white 8%
|
||||
val rail = Color(0x2EE6E6E9) // thin gray rail ~18%
|
||||
|
||||
val text = Color(0xFFE6E6E9)
|
||||
val muted = Color(0x8CE6E6E9) // 55%
|
||||
val faint = Color(0x52E6E6E9) // 32%
|
||||
|
||||
val fleet = Color(0xFF9580FF) // violet accent
|
||||
val fleetSoft = Color(0xFFA59CFF)
|
||||
val green = Color(0xFF7DD3A8)
|
||||
val amber = Color(0xFFD4B88A)
|
||||
val blue = Color(0xFF7AB6D9)
|
||||
val teal = Color(0xFF9BD1B3)
|
||||
val danger = Color(0xFFE0796B)
|
||||
|
||||
val mono = FontFamily.Monospace
|
||||
}
|
||||
|
||||
private val VojoColorScheme = darkColorScheme(
|
||||
primary = Vojo.fleet,
|
||||
onPrimary = Color(0xFF0C0C0E),
|
||||
secondary = Vojo.fleetSoft,
|
||||
background = Vojo.bgApp,
|
||||
onBackground = Vojo.text,
|
||||
surface = Vojo.bgPanel,
|
||||
onSurface = Vojo.text,
|
||||
surfaceVariant = Vojo.surface,
|
||||
onSurfaceVariant = Vojo.muted,
|
||||
error = Vojo.danger,
|
||||
outline = Vojo.hairline,
|
||||
// M3 popups (DropdownMenu, ModalBottomSheet) pick surfaceContainer* — keep
|
||||
// them inside the Vojo palette instead of the default violet-tinted tones.
|
||||
surfaceContainerLowest = Vojo.bgApp,
|
||||
surfaceContainerLow = Vojo.bgPanel,
|
||||
surfaceContainer = Vojo.bgPanel,
|
||||
surfaceContainerHigh = Vojo.surface,
|
||||
surfaceContainerHighest = Vojo.surface,
|
||||
surfaceDim = Vojo.bgApp,
|
||||
surfaceBright = Vojo.surface2,
|
||||
)
|
||||
|
||||
private val VojoTypography = Typography(
|
||||
titleLarge = TextStyle(fontSize = 22.sp, fontWeight = FontWeight.SemiBold, color = Vojo.text),
|
||||
titleMedium = TextStyle(fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = Vojo.text),
|
||||
bodyLarge = TextStyle(fontSize = 14.sp, color = Vojo.text),
|
||||
bodyMedium = TextStyle(fontSize = 13.sp, color = Vojo.text),
|
||||
bodySmall = TextStyle(fontSize = 11.5.sp, color = Vojo.muted),
|
||||
labelLarge = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Medium),
|
||||
labelSmall = TextStyle(fontSize = 10.5.sp, color = Vojo.faint),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun VojoTheme(content: @Composable () -> Unit) {
|
||||
// App is dark-only, like the design (no light-theme branch).
|
||||
MaterialTheme(
|
||||
colorScheme = VojoColorScheme,
|
||||
typography = VojoTypography,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
18
app/src/main/java/hev/htproxy/TProxyService.kt
Normal file
18
app/src/main/java/hev/htproxy/TProxyService.kt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package hev.htproxy
|
||||
|
||||
/**
|
||||
* Stub required by the vendored libhev-socks5-tunnel. Its JNI_OnLoad (hev-jni.c)
|
||||
* runs on library load and does FindClass("hev/htproxy/TProxyService") +
|
||||
* RegisterNatives against these exact instance methods; if the class is missing
|
||||
* the pending ClassNotFoundException aborts the whole process (SIGABRT).
|
||||
*
|
||||
* We drive the engine through our own [chat.vojo.proxy.core.Tun2Socks] C bridge
|
||||
* and never call these — the class only has to exist with matching signatures.
|
||||
* Kept verbatim in release by a -keep rule in proguard-rules.pro.
|
||||
*/
|
||||
@Suppress("unused", "FunctionName")
|
||||
class TProxyService {
|
||||
external fun TProxyStartService(configPath: String, fd: Int)
|
||||
external fun TProxyStopService()
|
||||
external fun TProxyGetStats(): LongArray
|
||||
}
|
||||
17
app/src/main/jni/Android.mk
Normal file
17
app/src/main/jni/Android.mk
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Capture our directory before including vendored makefiles (they reassign LOCAL_PATH).
|
||||
VOJO_JNI_PATH := $(call my-dir)
|
||||
|
||||
# Build hev-socks5-tunnel as a shared library plus its static dependencies
|
||||
# (yaml, lwip, hev-task-system). This is the tun2socks engine.
|
||||
include $(VOJO_JNI_PATH)/hev-socks5-tunnel/Android.mk
|
||||
|
||||
# Thin JNI bridge that exposes hev's C API to Kotlin.
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_PATH := $(VOJO_JNI_PATH)
|
||||
LOCAL_MODULE := tun2socks
|
||||
LOCAL_SRC_FILES := tun2socks.c
|
||||
LOCAL_C_INCLUDES := $(VOJO_JNI_PATH)/hev-socks5-tunnel/include
|
||||
LOCAL_SHARED_LIBRARIES := hev-socks5-tunnel
|
||||
LOCAL_LDLIBS := -llog
|
||||
LOCAL_CFLAGS += -O2 -Wall
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
115
app/src/main/jni/hev-socks5-tunnel/.clang-format
Normal file
115
app/src/main/jni/hev-socks5-tunnel/.clang-format
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
---
|
||||
Language: Cpp
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: false
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: TopLevel
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: MultiLine
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterClass: true
|
||||
AfterControlStatement: false
|
||||
AfterEnum: true
|
||||
AfterFunction: true
|
||||
AfterNamespace: true
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: true
|
||||
AfterUnion: true
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Custom
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakBeforeTernaryOperators: false
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: false
|
||||
ColumnLimit: 80
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: false
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: false
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IndentCaseLabels: false
|
||||
IndentPPDirectives: None
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: Inner
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 4
|
||||
ObjCSpaceAfterProperty: true
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 10
|
||||
PenaltyBreakBeforeFirstCallParameter: 30
|
||||
PenaltyBreakComment: 10
|
||||
PenaltyBreakFirstLessLess: 0
|
||||
PenaltyBreakString: 10
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 100
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Right
|
||||
ReflowComments: false
|
||||
SortIncludes: false
|
||||
SortUsingDeclarations: false
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: Always
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: Cpp03
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
...
|
||||
11
app/src/main/jni/hev-socks5-tunnel/.dockerignore
Normal file
11
app/src/main/jni/hev-socks5-tunnel/.dockerignore
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/.idea/
|
||||
/.vscode/
|
||||
|
||||
/.git/
|
||||
/.github/
|
||||
/README.md
|
||||
/License
|
||||
|
||||
/Dockerfile
|
||||
|
||||
/conf/
|
||||
408
app/src/main/jni/hev-socks5-tunnel/.github/workflows/build.yaml
vendored
Normal file
408
app/src/main/jni/hev-socks5-tunnel/.github/workflows/build.yaml
vendored
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
name: "Build"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
source:
|
||||
name: Source
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.base_ref || github.ref_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
- name: Gen Source
|
||||
run: |
|
||||
REV_ID=$(git rev-parse --short HEAD)
|
||||
mkdir -p hev-socks5-tunnel-${{ env.BRANCH_NAME }}
|
||||
git ls-files --recurse-submodules | tar c -O -T- | tar x -C hev-socks5-tunnel-${{ env.BRANCH_NAME }}
|
||||
echo ${REV_ID} > hev-socks5-tunnel-${{ env.BRANCH_NAME }}/.rev-id
|
||||
tar cJf hev-socks5-tunnel-${{ env.BRANCH_NAME }}.tar.xz hev-socks5-tunnel-${{ env.BRANCH_NAME }}
|
||||
- name: Upload source
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: hev-socks5-tunnel-${{ env.BRANCH_NAME }}.tar.xz
|
||||
path: hev-socks5-tunnel-${{ env.BRANCH_NAME }}.tar.xz
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
linux:
|
||||
name: Linux
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: arm64
|
||||
tool: aarch64-unknown-linux-musl
|
||||
- name: arm32
|
||||
tool: arm-unknown-linux-musleabi
|
||||
- name: arm32hf
|
||||
tool: arm-unknown-linux-musleabihf
|
||||
- name: arm32v7
|
||||
tool: armv7-unknown-linux-musleabi
|
||||
- name: arm32v7hf
|
||||
tool: armv7-unknown-linux-musleabihf
|
||||
- name: i586
|
||||
tool: i586-unknown-linux-musl
|
||||
- name: i686
|
||||
tool: i686-unknown-linux-musl
|
||||
- name: loong64
|
||||
tool: loongarch64-unknown-linux-musl
|
||||
- name: m68k
|
||||
tool: m68k-unknown-linux-musl
|
||||
- name: microblazeel
|
||||
tool: microblazeel-xilinx-linux-musl
|
||||
- name: microblaze
|
||||
tool: microblaze-xilinx-linux-musl
|
||||
- name: mips64el
|
||||
tool: mips64el-unknown-linux-musl
|
||||
- name: mips64
|
||||
tool: mips64-unknown-linux-musl
|
||||
- name: mips32el
|
||||
tool: mipsel-unknown-linux-musl
|
||||
- name: mips32elsf
|
||||
tool: mipsel-unknown-linux-muslsf
|
||||
- name: mips32
|
||||
tool: mips-unknown-linux-musl
|
||||
- name: mips32sf
|
||||
tool: mips-unknown-linux-muslsf
|
||||
- name: or1k
|
||||
tool: or1k-unknown-linux-musl
|
||||
- name: powerpc64le
|
||||
tool: powerpc64le-unknown-linux-musl
|
||||
- name: powerpc64
|
||||
tool: powerpc64-unknown-linux-musl
|
||||
- name: powerpcle
|
||||
tool: powerpcle-unknown-linux-musl
|
||||
- name: powerpc
|
||||
tool: powerpc-unknown-linux-musl
|
||||
- name: riscv32
|
||||
tool: riscv32-unknown-linux-musl
|
||||
- name: riscv64
|
||||
tool: riscv64-unknown-linux-musl
|
||||
- name: s390x
|
||||
tool: s390x-ibm-linux-musl
|
||||
- name: sh4
|
||||
tool: sh4-multilib-linux-musl
|
||||
- name: x86_64
|
||||
tool: x86_64-unknown-linux-musl
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
- name: Build ${{ matrix.name }}
|
||||
run: |
|
||||
sudo mkdir -p /opt/x-tools
|
||||
wget https://github.com/cross-tools/musl-cross/releases/download/20260515/${{ matrix.tool }}.tar.xz
|
||||
sudo tar xf ${{ matrix.tool }}.tar.xz -C /opt/x-tools
|
||||
make CROSS_PREFIX=/opt/x-tools/${{ matrix.tool }}/bin/${{ matrix.tool }}- CFLAGS=${{ matrix.env.CFLAGS }} ENABLE_STATIC=1 -j`nproc`
|
||||
cp bin/hev-socks5-tunnel hev-socks5-tunnel-linux-${{ matrix.name }}
|
||||
- name: Upload ${{ matrix.name }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: hev-socks5-tunnel-linux-${{ matrix.name }}
|
||||
path: hev-socks5-tunnel-linux-${{ matrix.name }}
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
macos:
|
||||
name: macOS
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: arm64
|
||||
flags: -arch arm64 -mmacosx-version-min=11.0
|
||||
- name: x86_64
|
||||
flags: -arch x86_64 -mmacosx-version-min=10.6
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
- name: Build ${{ matrix.name }}
|
||||
run: |
|
||||
make CC=clang CFLAGS="${{ matrix.flags }}" LFLAGS="${{ matrix.flags }}" -j $(sysctl -n hw.logicalcpu)
|
||||
cp bin/hev-socks5-tunnel hev-socks5-tunnel-darwin-${{ matrix.name }}
|
||||
- name: Upload ${{ matrix.name }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: hev-socks5-tunnel-darwin-${{ matrix.name }}
|
||||
path: hev-socks5-tunnel-darwin-${{ matrix.name }}
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
freebsd:
|
||||
name: FreeBSD
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
- name: Build
|
||||
uses: vmactions/freebsd-vm@v1
|
||||
with:
|
||||
usesh: true
|
||||
prepare: |
|
||||
pkg install -y gmake gcc
|
||||
run: |
|
||||
gmake
|
||||
cp bin/hev-socks5-tunnel hev-socks5-tunnel-freebsd-x86_64
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: hev-socks5-tunnel-freebsd-x86_64
|
||||
path: hev-socks5-tunnel-freebsd-x86_64
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
windows:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: cmd
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
- name: Setup MSYS2
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: MSYS
|
||||
location: D:\msys2
|
||||
update: true
|
||||
install: >-
|
||||
gcc
|
||||
git
|
||||
make
|
||||
wget
|
||||
zip
|
||||
- name: Build
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
export MSYS=winsymlinks:native
|
||||
git clone --depth=1 --recursive file://`pwd` work; cd work
|
||||
make -j`nproc`
|
||||
mkdir hev-socks5-tunnel
|
||||
cp bin/hev-socks5-tunnel* hev-socks5-tunnel
|
||||
cp third-part/wintun/bin/wintun.dll hev-socks5-tunnel
|
||||
wget -P hev-socks5-tunnel https://github.com/heiher/msys2/releases/latest/download/msys-2.0.dll
|
||||
zip -r ../hev-socks5-tunnel-win64.zip hev-socks5-tunnel
|
||||
- name: Upload ${{ matrix.name }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: hev-socks5-tunnel-win64.zip
|
||||
path: hev-socks5-tunnel-win64.zip
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- source
|
||||
- linux
|
||||
- macos
|
||||
- freebsd
|
||||
- windows
|
||||
if: github.event_name == 'release'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: release
|
||||
pattern: "hev-*"
|
||||
merge-multiple: true
|
||||
- name: Upload artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
for i in release/hev-*; do
|
||||
gh release upload ${{ github.event.release.tag_name }} $i
|
||||
done
|
||||
|
||||
apple:
|
||||
name: Apple
|
||||
runs-on: macos-latest
|
||||
if: github.event_name != 'release'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
- name: Build
|
||||
run: |
|
||||
./build-apple.sh
|
||||
|
||||
android:
|
||||
name: Android
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'release'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
- name: Prepare
|
||||
run: |
|
||||
wget https://dl.google.com/android/repository/android-ndk-r27d-linux.zip
|
||||
unzip android-ndk-r27d-linux.zip
|
||||
ln -sf . jni
|
||||
- name: Build
|
||||
run: |
|
||||
./android-ndk-r27d/ndk-build
|
||||
|
||||
llvm:
|
||||
name: LLVM
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'release'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
- name: Prepare
|
||||
run: |
|
||||
sudo apt install -y clang
|
||||
- name: Build
|
||||
run: |
|
||||
make CC=clang ENABLE_STATIC=1 -j`nproc`
|
||||
|
||||
docker:
|
||||
name: Build Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/386
|
||||
- linux/arm64/v8
|
||||
- linux/arm/v7
|
||||
- linux/arm/v6
|
||||
- linux/riscv64
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
- name: Prepare QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Prepare Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Prepare Repo Name
|
||||
id: repo
|
||||
run: |
|
||||
echo "repository=${GITHUB_REPOSITORY@L}" >> $GITHUB_OUTPUT
|
||||
- name: Prepare Digest
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
- name: Login GitHub Packages Docker Image Repository
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and Push Docker Image
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
provenance: false
|
||||
outputs: type=image,name=ghcr.io/${{ steps.repo.outputs.repository }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
- name: Export Digest
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Upload Digest
|
||||
uses: actions/upload-artifact@v7
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
docker-merge:
|
||||
name: Merge Docker Image Tags
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
needs:
|
||||
- docker
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Download Digests
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
- name: Prepare Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Prepare Repo Name
|
||||
id: repo
|
||||
run: |
|
||||
echo "repository=${GITHUB_REPOSITORY@L}" >> $GITHUB_OUTPUT
|
||||
- name: Login GitHub Packages Docker Image Repository
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Format Docker Image Meta
|
||||
uses: docker/metadata-action@v5
|
||||
id: docker_meta
|
||||
with:
|
||||
images: ghcr.io/${{ steps.repo.outputs.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=nightly,enable={{is_default_branch}}
|
||||
- name: Create Manifest List and Push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf 'ghcr.io/${{ steps.repo.outputs.repository }}@sha256:%s ' *)
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ghcr.io/${{ steps.repo.outputs.repository }}:${{ steps.docker_meta.outputs.version }}
|
||||
26
app/src/main/jni/hev-socks5-tunnel/.github/workflows/code-format.yaml
vendored
Normal file
26
app/src/main/jni/hev-socks5-tunnel/.github/workflows/code-format.yaml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: "Check code formatting"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
checker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Prepare
|
||||
run: |
|
||||
sudo apt install -y clang-format
|
||||
- name: Format
|
||||
run: |
|
||||
find src -iname "*.[h,c]" -exec clang-format -i {} \;
|
||||
- name: Check
|
||||
run: |
|
||||
git status
|
||||
git diff --quiet
|
||||
5
app/src/main/jni/hev-socks5-tunnel/.gitignore
vendored
Normal file
5
app/src/main/jni/hev-socks5-tunnel/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
bin
|
||||
build
|
||||
HevSocks5Tunnel.xcframework
|
||||
obj
|
||||
libs
|
||||
12
app/src/main/jni/hev-socks5-tunnel/.gitmodules
vendored
Normal file
12
app/src/main/jni/hev-socks5-tunnel/.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[submodule "third-part/hev-task-system"]
|
||||
path = third-part/hev-task-system
|
||||
url = https://github.com/heiher/hev-task-system
|
||||
[submodule "third-part/yaml"]
|
||||
path = third-part/yaml
|
||||
url = https://github.com/heiher/yaml
|
||||
[submodule "third-part/lwip"]
|
||||
path = third-part/lwip
|
||||
url = https://github.com/heiher/lwip
|
||||
[submodule "src/core"]
|
||||
path = src/core
|
||||
url = https://github.com/heiher/hev-socks5-core
|
||||
51
app/src/main/jni/hev-socks5-tunnel/Android.mk
Normal file
51
app/src/main/jni/hev-socks5-tunnel/Android.mk
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Copyright (C) 2021 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
TOP_PATH := $(call my-dir)
|
||||
|
||||
ifeq ($(filter $(modules-get-list),yaml),)
|
||||
include $(TOP_PATH)/third-part/yaml/Android.mk
|
||||
endif
|
||||
ifeq ($(filter $(modules-get-list),lwip),)
|
||||
include $(TOP_PATH)/third-part/lwip/Android.mk
|
||||
endif
|
||||
ifeq ($(filter $(modules-get-list),hev-task-system),)
|
||||
include $(TOP_PATH)/third-part/hev-task-system/Android.mk
|
||||
endif
|
||||
|
||||
LOCAL_PATH = $(TOP_PATH)
|
||||
SRCDIR := $(LOCAL_PATH)/src
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
include $(LOCAL_PATH)/build.mk
|
||||
LOCAL_MODULE := hev-socks5-tunnel
|
||||
LOCAL_SRC_FILES := $(patsubst $(SRCDIR)/%,src/%,$(SRCFILES))
|
||||
LOCAL_C_INCLUDES := \
|
||||
$(LOCAL_PATH)/src \
|
||||
$(LOCAL_PATH)/src/misc \
|
||||
$(LOCAL_PATH)/src/core/include \
|
||||
$(LOCAL_PATH)/third-part/yaml/include \
|
||||
$(LOCAL_PATH)/third-part/lwip/src/include \
|
||||
$(LOCAL_PATH)/third-part/lwip/src/ports/include \
|
||||
$(LOCAL_PATH)/third-part/hev-task-system/include
|
||||
LOCAL_CFLAGS += -DFD_SET_DEFINED -DSOCKLEN_T_DEFINED -DENABLE_LIBRARY
|
||||
LOCAL_CFLAGS += $(VERSION_CFLAGS)
|
||||
ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
|
||||
LOCAL_CFLAGS += -mfpu=neon
|
||||
endif
|
||||
LOCAL_STATIC_LIBRARIES := yaml lwip hev-task-system
|
||||
LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384
|
||||
LOCAL_LDFLAGS += -Wl,-z,common-page-size=16384
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
21
app/src/main/jni/hev-socks5-tunnel/Application.mk
Normal file
21
app/src/main/jni/hev-socks5-tunnel/Application.mk
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Copyright (C) 2013 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
APP_OPTIM := release
|
||||
APP_PLATFORM := android-29
|
||||
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
|
||||
APP_CFLAGS := -O3
|
||||
APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
|
||||
NDK_TOOLCHAIN_VERSION := clang
|
||||
43
app/src/main/jni/hev-socks5-tunnel/Dockerfile
Normal file
43
app/src/main/jni/hev-socks5-tunnel/Dockerfile
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
FROM alpine:latest AS builder
|
||||
|
||||
RUN apk add --update --no-cache \
|
||||
make \
|
||||
git \
|
||||
gcc \
|
||||
linux-headers \
|
||||
musl-dev
|
||||
|
||||
WORKDIR /src
|
||||
COPY . /src
|
||||
|
||||
RUN make clean && make
|
||||
|
||||
FROM alpine:latest
|
||||
LABEL org.opencontainers.image.source="https://github.com/heiher/hev-socks5-tunnel"
|
||||
|
||||
RUN apk add --update --no-cache \
|
||||
iproute2
|
||||
|
||||
ENV TUN=tun0 \
|
||||
MTU=8500 \
|
||||
IPV4=198.18.0.1 \
|
||||
IPV6='' \
|
||||
TABLE=20 \
|
||||
MARK=438 \
|
||||
SOCKS5_ADDR=172.17.0.1 \
|
||||
SOCKS5_PORT=1080 \
|
||||
SOCKS5_USERNAME='' \
|
||||
SOCKS5_PASSWORD='' \
|
||||
SOCKS5_UDP_MODE=udp \
|
||||
SOCKS5_UDP_ADDR='' \
|
||||
CONFIG_ROUTES=1 \
|
||||
IPV4_INCLUDED_ROUTES=0.0.0.0/0 \
|
||||
IPV4_EXCLUDED_ROUTES='' \
|
||||
LOG_LEVEL=warn
|
||||
|
||||
HEALTHCHECK --start-period=5s --interval=5s --timeout=2s --retries=3 CMD ["test", "-f", "/success"]
|
||||
|
||||
COPY --chmod=755 docker/entrypoint.sh /entrypoint.sh
|
||||
COPY --from=builder /src/bin/hev-socks5-tunnel /usr/bin/hev-socks5-tunnel
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
19
app/src/main/jni/hev-socks5-tunnel/LICENSE
Normal file
19
app/src/main/jni/hev-socks5-tunnel/LICENSE
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2022 hev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
146
app/src/main/jni/hev-socks5-tunnel/Makefile
Normal file
146
app/src/main/jni/hev-socks5-tunnel/Makefile
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# Makefile for hev-socks5-tunnel
|
||||
|
||||
PROJECT=hev-socks5-tunnel
|
||||
|
||||
CROSS_PREFIX :=
|
||||
PP=$(CROSS_PREFIX)cpp
|
||||
CC=$(CROSS_PREFIX)gcc
|
||||
AR=$(CROSS_PREFIX)ar
|
||||
STRIP=$(CROSS_PREFIX)strip
|
||||
CCFLAGS=-O3 -pipe -Wall -Werror $(CFLAGS) \
|
||||
-I$(SRCDIR) \
|
||||
-I$(SRCDIR)/misc \
|
||||
-I$(SRCDIR)/core/include \
|
||||
-I$(THIRDPARTDIR)/yaml/include \
|
||||
-I$(THIRDPARTDIR)/wintun/include \
|
||||
-I$(THIRDPARTDIR)/lwip/src/include \
|
||||
-I$(THIRDPARTDIR)/lwip/src/ports/include \
|
||||
-I$(THIRDPARTDIR)/hev-task-system/include
|
||||
LDFLAGS=-L$(THIRDPARTDIR)/yaml/bin -lyaml \
|
||||
-L$(THIRDPARTDIR)/lwip/bin -llwip \
|
||||
-L$(THIRDPARTDIR)/hev-task-system/bin -lhev-task-system \
|
||||
-lpthread $(LFLAGS)
|
||||
|
||||
SRCDIR=src
|
||||
BINDIR=bin
|
||||
CONFDIR=conf
|
||||
BUILDDIR=build
|
||||
INSTDIR=/usr/local
|
||||
THIRDPARTDIR=third-part
|
||||
|
||||
CONFIG=$(CONFDIR)/main.yml
|
||||
EXEC_TARGET=$(BINDIR)/hev-socks5-tunnel
|
||||
STATIC_TARGET=$(BINDIR)/lib$(PROJECT).a
|
||||
SHARED_TARGET=$(BINDIR)/lib$(PROJECT).so
|
||||
THIRDPARTS=$(THIRDPARTDIR)/yaml \
|
||||
$(THIRDPARTDIR)/lwip \
|
||||
$(THIRDPARTDIR)/hev-task-system
|
||||
|
||||
$(STATIC_TARGET) : CCFLAGS+=-DENABLE_LIBRARY
|
||||
$(SHARED_TARGET) : CCFLAGS+=-DENABLE_LIBRARY -fPIC
|
||||
$(SHARED_TARGET) : LDFLAGS+=-shared -pthread
|
||||
|
||||
-include build.mk
|
||||
CCFLAGS+=$(VERSION_CFLAGS)
|
||||
CCSRCS=$(filter %.c,$(SRCFILES))
|
||||
ASSRCS=$(filter %.S,$(SRCFILES))
|
||||
LDOBJS=$(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(CCSRCS)) \
|
||||
$(patsubst $(SRCDIR)/%.S,$(BUILDDIR)/%.o,$(ASSRCS))
|
||||
DEPEND=$(LDOBJS:.o=.dep)
|
||||
|
||||
BUILDMSG="\e[1;31mBUILD\e[0m %s\n"
|
||||
LINKMSG="\e[1;34mLINK\e[0m \e[1;32m%s\e[0m\n"
|
||||
STRIPMSG="\e[1;34mSTRIP\e[0m \e[1;32m%s\e[0m\n"
|
||||
CLEANMSG="\e[1;34mCLEAN\e[0m %s\n"
|
||||
INSTMSG="\e[1;34mINST\e[0m %s -> %s\n"
|
||||
UNINSMSG="\e[1;34mUNINS\e[0m %s\n"
|
||||
|
||||
ENABLE_DEBUG :=
|
||||
ifeq ($(ENABLE_DEBUG),1)
|
||||
CCFLAGS+=-g -O0 -DENABLE_DEBUG
|
||||
STRIP=true
|
||||
endif
|
||||
|
||||
ENABLE_STATIC :=
|
||||
ifeq ($(ENABLE_STATIC),1)
|
||||
CCFLAGS+=-static
|
||||
endif
|
||||
|
||||
ifeq ($(MSYSTEM),MSYS)
|
||||
LDFLAGS+=-lmsys-2.0 -lws2_32 -lIphlpapi
|
||||
endif
|
||||
|
||||
V :=
|
||||
ECHO_PREFIX := @
|
||||
ifeq ($(V),1)
|
||||
undefine ECHO_PREFIX
|
||||
endif
|
||||
|
||||
.PHONY: exec static shared clean install uninstall tp-static tp-shared tp-clean
|
||||
|
||||
exec : $(EXEC_TARGET)
|
||||
|
||||
static : $(STATIC_TARGET)
|
||||
|
||||
shared : $(SHARED_TARGET)
|
||||
|
||||
tp-static : $(THIRDPARTS)
|
||||
@$(foreach dir,$^,$(MAKE) --no-print-directory -C $(dir) static;)
|
||||
|
||||
tp-shared : $(THIRDPARTS)
|
||||
@$(foreach dir,$^,$(MAKE) --no-print-directory -C $(dir) shared;)
|
||||
|
||||
tp-clean : $(THIRDPARTS)
|
||||
@$(foreach dir,$^,$(MAKE) --no-print-directory -C $(dir) clean;)
|
||||
|
||||
clean : tp-clean
|
||||
$(ECHO_PREFIX) $(RM) -rf $(BINDIR) $(BUILDDIR)
|
||||
@printf $(CLEANMSG) $(PROJECT)
|
||||
|
||||
install : $(INSTDIR)/bin/$(PROJECT) $(INSTDIR)/etc/$(PROJECT).yml
|
||||
|
||||
uninstall :
|
||||
$(ECHO_PREFIX) $(RM) -rf $(INSTDIR)/bin/$(PROJECT)
|
||||
@printf $(UNINSMSG) $(INSTDIR)/bin/$(PROJECT)
|
||||
$(ECHO_PREFIX) $(RM) -rf $(INSTDIR)/etc/$(PROJECT).yml
|
||||
@printf $(UNINSMSG) $(INSTDIR)/etc/$(PROJECT).yml
|
||||
|
||||
$(INSTDIR)/bin/$(PROJECT) : $(EXEC_TARGET)
|
||||
$(ECHO_PREFIX) install -d -m 0755 $(dir $@)
|
||||
$(ECHO_PREFIX) install -m 0755 $< $@
|
||||
@printf $(INSTMSG) $< $@
|
||||
|
||||
$(INSTDIR)/etc/$(PROJECT).yml : $(CONFIG)
|
||||
$(ECHO_PREFIX) install -d -m 0755 $(dir $@)
|
||||
$(ECHO_PREFIX) install -m 0644 $< $@
|
||||
@printf $(INSTMSG) $< $@
|
||||
|
||||
$(EXEC_TARGET) : $(LDOBJS) tp-static
|
||||
$(ECHO_PREFIX) mkdir -p $(dir $@)
|
||||
$(ECHO_PREFIX) $(CC) $(CCFLAGS) -o $@ $(LDOBJS) $(LDFLAGS)
|
||||
@printf $(LINKMSG) $@
|
||||
$(ECHO_PREFIX) $(STRIP) $@
|
||||
@printf $(STRIPMSG) $@
|
||||
|
||||
$(STATIC_TARGET) : $(LDOBJS) tp-static
|
||||
$(ECHO_PREFIX) mkdir -p $(dir $@)
|
||||
$(ECHO_PREFIX) $(AR) csq $@ $(LDOBJS)
|
||||
@printf $(LINKMSG) $@
|
||||
|
||||
$(SHARED_TARGET) : $(LDOBJS) tp-shared
|
||||
$(ECHO_PREFIX) mkdir -p $(dir $@)
|
||||
$(ECHO_PREFIX) $(CC) $(CCFLAGS) -o $@ $(LDOBJS) $(LDFLAGS)
|
||||
@printf $(LINKMSG) $@
|
||||
|
||||
$(BUILDDIR)/%.dep : $(SRCDIR)/%.c
|
||||
$(ECHO_PREFIX) mkdir -p $(dir $@)
|
||||
$(ECHO_PREFIX) $(PP) $(CCFLAGS) -MM -MT$(@:.dep=.o) -MF$@ $< 2>/dev/null
|
||||
|
||||
$(BUILDDIR)/%.o : $(SRCDIR)/%.c
|
||||
$(ECHO_PREFIX) mkdir -p $(dir $@)
|
||||
$(ECHO_PREFIX) $(CC) $(CCFLAGS) -c -o $@ $<
|
||||
@printf $(BUILDMSG) $<
|
||||
|
||||
ifneq ($(MAKECMDGOALS),clean)
|
||||
-include $(DEPEND)
|
||||
endif
|
||||
398
app/src/main/jni/hev-socks5-tunnel/README.md
Normal file
398
app/src/main/jni/hev-socks5-tunnel/README.md
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
# HevSocks5Tunnel
|
||||
|
||||
[](https://github.com/heiher/hev-socks5-tunnel)
|
||||
|
||||
A simple, lightweight tunnel over Socks5 proxy (tun2socks).
|
||||
|
||||
## Features
|
||||
|
||||
* IPv4/IPv6. (dual stack)
|
||||
* Redirect TCP connections.
|
||||
* Redirect UDP packets. (Fullcone NAT, UDP-in-UDP and UDP-in-TCP [^1])
|
||||
* Linux/Android/FreeBSD/macOS/iOS/Windows.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
See [here](https://github.com/heiher/hev-socks5-tunnel/wiki/Benchmarks) for more details.
|
||||
|
||||
### Speed
|
||||
|
||||

|
||||

|
||||
|
||||
### CPU usage
|
||||
|
||||

|
||||

|
||||
|
||||
### Memory usage
|
||||
|
||||

|
||||

|
||||
|
||||
## How to Build
|
||||
|
||||
### Unix
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/heiher/hev-socks5-tunnel
|
||||
cd hev-socks5-tunnel
|
||||
make
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
```bash
|
||||
mkdir hev-socks5-tunnel
|
||||
cd hev-socks5-tunnel
|
||||
git clone --recursive https://github.com/heiher/hev-socks5-tunnel jni
|
||||
ndk-build
|
||||
```
|
||||
|
||||
### iOS and macOS
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/heiher/hev-socks5-tunnel
|
||||
cd hev-socks5-tunnel
|
||||
# will generate HevSocks5Tunnel.xcframework
|
||||
./build-apple.sh
|
||||
```
|
||||
|
||||
### Windows (MSYS2)
|
||||
```bash
|
||||
export MSYS=winsymlinks:native
|
||||
git clone --recursive https://github.com/heiher/hev-socks5-tunnel
|
||||
cd hev-socks5-tunnel
|
||||
make
|
||||
```
|
||||
|
||||
### Library
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/heiher/hev-socks5-tunnel
|
||||
cd hev-socks5-tunnel
|
||||
|
||||
# Static library
|
||||
make static
|
||||
|
||||
# Shared library
|
||||
make shared
|
||||
```
|
||||
|
||||
## How to Use
|
||||
|
||||
### Config
|
||||
|
||||
```yaml
|
||||
tunnel:
|
||||
# Interface name
|
||||
name: tun0
|
||||
# Interface MTU
|
||||
mtu: 8500
|
||||
# Multi-queue
|
||||
multi-queue: false
|
||||
# IPv4 address
|
||||
ipv4: 198.18.0.1
|
||||
# IPv6 address
|
||||
ipv6: 'fc00::1'
|
||||
# Post up script
|
||||
# post-up-script: up.sh
|
||||
# Pre down script
|
||||
# pre-down-script: down.sh
|
||||
|
||||
socks5:
|
||||
# Socks5 server port
|
||||
port: 1080
|
||||
# Socks5 server address (ipv4/ipv6)
|
||||
address: 127.0.0.1
|
||||
# Socks5 UDP relay mode (tcp|udp)
|
||||
udp: 'udp'
|
||||
# Override the UDP address provided by the Socks5 server (ipv4/ipv6)
|
||||
# udp-address: ''
|
||||
# Socks5 handshake using pipeline mode
|
||||
# pipeline: false
|
||||
# Socks5 server username
|
||||
# username: 'username'
|
||||
# Socks5 server password
|
||||
# password: 'password'
|
||||
# Socket mark
|
||||
# mark: 0
|
||||
# TCP fastopen
|
||||
# tcp-fastopen: false
|
||||
|
||||
#mapdns:
|
||||
# Mapped DNS address
|
||||
# address: 198.18.0.2
|
||||
# Mapped DNS port
|
||||
# port: 53
|
||||
# Mapped IP network base
|
||||
# network: 100.64.0.0
|
||||
# Mapped IP network mask
|
||||
# netmask: 255.192.0.0
|
||||
# Mapped DNS cache size
|
||||
# cache-size: 10000
|
||||
|
||||
#misc:
|
||||
# task stack size (bytes)
|
||||
# task-stack-size: 86016
|
||||
# tcp buffer size (bytes)
|
||||
# tcp-buffer-size: 65536
|
||||
# udp socket recv buffer (SO_RCVBUF) size (bytes)
|
||||
# udp-recv-buffer-size: 524288
|
||||
# number of udp buffers in splice, 1500 bytes per buffer.
|
||||
# udp-copy-buffer-nums: 10
|
||||
# maximum session count (0: unlimited)
|
||||
# max-session-count: 0
|
||||
# connect timeout (ms)
|
||||
# connect-timeout: 10000
|
||||
# TCP read-write timeout (ms)
|
||||
# tcp-read-write-timeout: 300000
|
||||
# UDP read-write timeout (ms)
|
||||
# udp-read-write-timeout: 60000
|
||||
# stdout, stderr or file-path
|
||||
# log-file: stderr
|
||||
# debug, info, warn or error
|
||||
# log-level: warn
|
||||
# If present, run as a daemon with this pid file
|
||||
# pid-file: /run/hev-socks5-tunnel.pid
|
||||
# If present, set rlimit nofile; else use default value
|
||||
# limit-nofile: 65535
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
# Set socks5.mark = 438
|
||||
bin/hev-socks5-tunnel conf/main.yml
|
||||
|
||||
# Disable reverse path filter
|
||||
sudo sysctl -w net.ipv4.conf.all.rp_filter=0
|
||||
sudo sysctl -w net.ipv4.conf.tun0.rp_filter=0
|
||||
|
||||
# Bypass upstream socks5 server
|
||||
sudo ip rule add fwmark 438 lookup main pref 10
|
||||
sudo ip -6 rule add fwmark 438 lookup main pref 10
|
||||
|
||||
# Route others
|
||||
sudo ip route add default dev tun0 table 20
|
||||
sudo ip rule add lookup 20 pref 20
|
||||
sudo ip -6 route add default dev tun0 table 20
|
||||
sudo ip -6 rule add lookup 20 pref 20
|
||||
```
|
||||
|
||||
#### FreeBSD/macOS
|
||||
|
||||
```zsh
|
||||
# Bypass upstream socks5 server
|
||||
# 10.0.0.1: socks5 server
|
||||
# 10.0.2.2: default gateway
|
||||
sudo route add -net 10.0.0.1/32 10.0.2.2
|
||||
|
||||
# Route others
|
||||
sudo route change -inet default -interface tun0
|
||||
sudo route change -inet6 default -interface tun0
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
```zsh
|
||||
# Bypass upstream socks5 server
|
||||
# 10.0.0.1: socks5 server
|
||||
# 10.0.2.2: default gateway
|
||||
route add 10.0.0.1/32 10.0.2.2
|
||||
|
||||
# Route others
|
||||
route change 0.0.0.0/0 0.0.0.0 if tun-index
|
||||
route change ::/0 :: if tun-index
|
||||
```
|
||||
|
||||
#### OpenWrt 24.10+
|
||||
|
||||
Repo: https://github.com/openwrt/packages/tree/master/net/hev-socks5-tunnel
|
||||
|
||||
```sh
|
||||
# Install package
|
||||
opkg install hev-socks5-tunnel
|
||||
|
||||
# Edit /etc/config/hev-socks5-tunnel
|
||||
|
||||
# Restart service
|
||||
/etc/init.d/hev-socks5-tunnel restart
|
||||
```
|
||||
|
||||
#### Low memory usage
|
||||
|
||||
On low-memory systems like iOS, reducing the size of the TCP buffer and
|
||||
task stack, as well as limiting the maximum session count, can help prevent
|
||||
out-of-memory issues.
|
||||
|
||||
```yaml
|
||||
misc:
|
||||
# task stack size (bytes)
|
||||
task-stack-size: 24576 # 20480 + tcp-buffer-size
|
||||
# tcp buffer size (bytes)
|
||||
tcp-buffer-size: 4096
|
||||
# maximum session count
|
||||
max-session-count: 1200
|
||||
```
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
```yaml
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
client:
|
||||
image: alpine:latest # just for network testing
|
||||
tty: true # you can test network in terminal
|
||||
depends_on:
|
||||
tun:
|
||||
condition: service_healthy
|
||||
network_mode: "service:tun"
|
||||
|
||||
tun:
|
||||
image: ghcr.io/heiher/hev-socks5-tunnel:latest # `latest` for the latest published version; `nightly` for the latest source build; `vX.Y.Z` for the specific version
|
||||
cap_add:
|
||||
- NET_ADMIN # needed
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun # needed
|
||||
environment:
|
||||
TUN: tun0 # optional, tun interface name, default `tun0`
|
||||
MTU: 8500 # optional, MTU is MTU, default `8500`
|
||||
IPV4: 198.18.0.1 # optional, tun interface ip, default `198.18.0.1`
|
||||
TABLE: 20 # optional, ip route table id, default `20`
|
||||
MARK: 438 # optional, ip route rule mark, dec or hex format, default `438`
|
||||
SOCKS5_ADDR: a.b.c.d # socks5 proxy server address
|
||||
SOCKS5_PORT: 1080 # socks5 proxy server port
|
||||
SOCKS5_USERNAME: user # optional, socks5 proxy username, only set when need to auth
|
||||
SOCKS5_PASSWORD: pass # optional, socks5 proxy password, only set when need to auth
|
||||
SOCKS5_UDP_MODE: udp # optional, UDP relay mode, default `udp`, other option `tcp`
|
||||
SOCKS5_UDP_ADDR: a.b.c.d # optional, override the UDP address provided by the Socks5 server
|
||||
CONFIG_ROUTES: 1 # optional, set 0 to ignore TABLE, IPV4_INCLUDED_ROUTES and IPV4_EXCLUDED_ROUTES, with MARK defaults to 0
|
||||
IPV4_INCLUDED_ROUTES: 0.0.0.0/0 # optional, demo means proxy all traffic. for multiple network segments, join with `,` or `\n`
|
||||
IPV4_EXCLUDED_ROUTES: a.b.c.d # optional, demo means exclude traffic from the proxy itself. for multiple network segments, join with `,` or `\n`
|
||||
LOG_LEVEL: warn # optional, default `warn`, other option `debug`/`info`/`error`
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
```
|
||||
|
||||
You can also set the route rules with multiple network segments like:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
IPV4_INCLUDED_ROUTES: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
IPV4_EXCLUDED_ROUTES: |-
|
||||
a.b.c.d/24
|
||||
a.b.c.f/24
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```c
|
||||
/**
|
||||
* hev_socks5_tunnel_main:
|
||||
* @config_path: config file path
|
||||
* @tun_fd: tunnel file descriptor
|
||||
*
|
||||
* Start and run the socks5 tunnel, this function will blocks until the
|
||||
* hev_socks5_tunnel_quit is called or an error occurs.
|
||||
*
|
||||
* Alias of hev_socks5_tunnel_main_from_file
|
||||
*
|
||||
* Returns: returns zero on successful, otherwise returns -1.
|
||||
*
|
||||
* Since: 2.4.6
|
||||
*/
|
||||
int hev_socks5_tunnel_main (const char *config_path, int tun_fd);
|
||||
|
||||
/**
|
||||
* hev_socks5_tunnel_main_from_file:
|
||||
* @config_path: config file path
|
||||
* @tun_fd: tunnel file descriptor
|
||||
*
|
||||
* Start and run the socks5 tunnel, this function will blocks until the
|
||||
* hev_socks5_tunnel_quit is called or an error occurs.
|
||||
*
|
||||
* Returns: returns zero on successful, otherwise returns -1.
|
||||
*
|
||||
* Since: 2.6.7
|
||||
*/
|
||||
int hev_socks5_tunnel_main_from_file (const char *config_path, int tun_fd);
|
||||
|
||||
/**
|
||||
* hev_socks5_tunnel_main_from_str:
|
||||
* @config_str: string config
|
||||
* @config_len: the byte length of string config
|
||||
* @tun_fd: tunnel file descriptor
|
||||
*
|
||||
* Start and run the socks5 tunnel, this function will blocks until the
|
||||
* hev_socks5_tunnel_quit is called or an error occurs.
|
||||
*
|
||||
* Returns: returns zero on successful, otherwise returns -1.
|
||||
*
|
||||
* Since: 2.6.7
|
||||
*/
|
||||
int hev_socks5_tunnel_main_from_str (const unsigned char *config_str,
|
||||
unsigned int config_len, int tun_fd);
|
||||
|
||||
/**
|
||||
* hev_socks5_tunnel_quit:
|
||||
*
|
||||
* Stop the socks5 tunnel.
|
||||
*
|
||||
* Since: 2.4.6
|
||||
*/
|
||||
void hev_socks5_tunnel_quit (void);
|
||||
|
||||
/**
|
||||
* hev_socks5_tunnel_stats:
|
||||
* @tx_packets (out): transmitted packets
|
||||
* @tx_bytes (out): transmitted bytes
|
||||
* @rx_packets (out): received packets
|
||||
* @rx_bytes (out): received bytes
|
||||
*
|
||||
* Retrieve tunnel interface traffic statistics.
|
||||
*
|
||||
* Since: 2.6.5
|
||||
*/
|
||||
void hev_socks5_tunnel_stats (size_t *tx_packets, size_t *tx_bytes,
|
||||
size_t *rx_packets, size_t *rx_bytes);
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Android VPN
|
||||
|
||||
* [SocksTun](https://github.com/heiher/sockstun)
|
||||
|
||||
### iOS
|
||||
|
||||
* [Tun2SocksKit](https://github.com/EbrahimTahernejad/Tun2SocksKit)
|
||||
|
||||
## Contributors
|
||||
|
||||
* **arror** - https://github.com/arror
|
||||
* **bazuchan** - https://github.com/bazuchan
|
||||
* **codewithtamim** - https://github.com/codewithtamim
|
||||
* **dovecoteescapee** - https://github.com/dovecoteescapee
|
||||
* **ebrahimtahernejad** - https://github.com/ebrahimtahernejad
|
||||
* **heiby** - https://github.com/heiby
|
||||
* **hev** - https://hev.cc
|
||||
* **katana** - https://github.com/officialkatana
|
||||
* **pronebird** - https://github.com/pronebird
|
||||
* **saeeddev94** - https://github.com/saeeddev94
|
||||
* **sskaje** - https://github.com/sskaje
|
||||
* **wankkoree** - https://github.com/wankkoree
|
||||
* **xiguagua** - https://github.com/xiguagua
|
||||
* **xz-dev** - https://github.com/xz-dev
|
||||
* **yiguous** - https://github.com/yiguous
|
||||
* **yujinpan** - https://github.com/yujinpan
|
||||
* **zheshinicheng** - https://github.com/zheshinicheng
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
[^1]: See [protocol specification](https://github.com/heiher/hev-socks5-core/tree/main?tab=readme-ov-file#udp-in-tcp). The [hev-socks5-server](https://github.com/heiher/hev-socks5-server) supports UDP relay over TCP.
|
||||
76
app/src/main/jni/hev-socks5-tunnel/build-apple.sh
Executable file
76
app/src/main/jni/hev-socks5-tunnel/build-apple.sh
Executable file
|
|
@ -0,0 +1,76 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
XCFRAMEWORK_DIR="./apple_xcframework"
|
||||
LIB_NAME="HevSocks5Tunnel"
|
||||
|
||||
# buildStatic iphoneos -mios-version-min=15.0 arm64
|
||||
buildStatic()
|
||||
{
|
||||
echo "build for $1, $2, min version $3"
|
||||
|
||||
local MIN_VERSION="-m$1-version-min=$3"
|
||||
make PP="xcrun --sdk $1 --toolchain $1 clang" \
|
||||
CC="xcrun --sdk $1 --toolchain $1 clang" \
|
||||
CFLAGS="-arch $2 $MIN_VERSION" \
|
||||
LFLAGS="-arch $2 $MIN_VERSION -Wl,-Bsymbolic-functions" static
|
||||
|
||||
local OUTPUT_DIR="$XCFRAMEWORK_DIR/$1-$2"
|
||||
mkdir -p $OUTPUT_DIR
|
||||
local OUTPUT_ARCH_FILE="$OUTPUT_DIR/libhev-socks5-tunnel.a"
|
||||
|
||||
libtool -static -o $OUTPUT_ARCH_FILE \
|
||||
bin/libhev-socks5-tunnel.a \
|
||||
third-part/lwip/bin/liblwip.a \
|
||||
third-part/yaml/bin/libyaml.a \
|
||||
third-part/hev-task-system/bin/libhev-task-system.a
|
||||
make clean
|
||||
}
|
||||
|
||||
mergeStatic()
|
||||
{
|
||||
echo "merge for $1, $2, $3"
|
||||
local FIRST_LIB_FILE="$XCFRAMEWORK_DIR/$1-$2/libhev-socks5-tunnel.a"
|
||||
local SECOND_LIB_FILE="$XCFRAMEWORK_DIR/$1-$3/libhev-socks5-tunnel.a"
|
||||
local OUTPUT_DIR="$XCFRAMEWORK_DIR/$1-$2-$3"
|
||||
mkdir -p $OUTPUT_DIR
|
||||
local OUTPUT_ARCH_FILE="$OUTPUT_DIR/libhev-socks5-tunnel.a"
|
||||
lipo -create \
|
||||
-arch $2 $FIRST_LIB_FILE \
|
||||
-arch $3 $SECOND_LIB_FILE \
|
||||
-output $OUTPUT_ARCH_FILE
|
||||
}
|
||||
|
||||
rm -rf $XCFRAMEWORK_DIR
|
||||
rm -rf HevSocks5Tunnel.xcframework
|
||||
mkdir $XCFRAMEWORK_DIR
|
||||
|
||||
buildStatic iphoneos arm64 15.0
|
||||
buildStatic iphonesimulator x86_64 15.0
|
||||
buildStatic iphonesimulator arm64 15.0
|
||||
mergeStatic iphonesimulator x86_64 arm64
|
||||
|
||||
# keep same with flutter
|
||||
buildStatic macosx x86_64 10.14
|
||||
buildStatic macosx arm64 10.14
|
||||
mergeStatic macosx x86_64 arm64
|
||||
|
||||
buildStatic appletvos arm64 17.0
|
||||
buildStatic appletvsimulator x86_64 17.0
|
||||
buildStatic appletvsimulator arm64 17.0
|
||||
mergeStatic appletvsimulator x86_64 arm64
|
||||
|
||||
INCLUDE_DIR="$XCFRAMEWORK_DIR/include"
|
||||
mkdir -p $INCLUDE_DIR/$LIB_NAME
|
||||
cp ./src/hev-main.h $INCLUDE_DIR/$LIB_NAME
|
||||
cp ./module.modulemap $INCLUDE_DIR/$LIB_NAME
|
||||
xcodebuild -create-xcframework \
|
||||
-library ./apple_xcframework/iphoneos-arm64/libhev-socks5-tunnel.a -headers $INCLUDE_DIR \
|
||||
-library ./apple_xcframework/iphonesimulator-x86_64-arm64/libhev-socks5-tunnel.a -headers $INCLUDE_DIR \
|
||||
-library ./apple_xcframework/macosx-x86_64-arm64/libhev-socks5-tunnel.a -headers $INCLUDE_DIR \
|
||||
-library ./apple_xcframework/appletvos-arm64/libhev-socks5-tunnel.a -headers $INCLUDE_DIR \
|
||||
-library ./apple_xcframework/appletvsimulator-x86_64-arm64/libhev-socks5-tunnel.a -headers $INCLUDE_DIR \
|
||||
-output ./HevSocks5Tunnel.xcframework
|
||||
|
||||
rm -rf ./apple_xcframework
|
||||
20
app/src/main/jni/hev-socks5-tunnel/build.mk
Normal file
20
app/src/main/jni/hev-socks5-tunnel/build.mk
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Build
|
||||
|
||||
rwildcard=$(foreach d,$(wildcard $1*), \
|
||||
$(call rwildcard,$d/,$2) \
|
||||
$(filter $(subst *,%,$2),$d))
|
||||
|
||||
SRCFILES=$(call rwildcard,$(SRCDIR)/,*.c *.S)
|
||||
|
||||
ifeq ($(REV_ID),)
|
||||
ifneq (,$(wildcard .rev-id))
|
||||
REV_ID=$(shell cat .rev-id)
|
||||
endif
|
||||
ifeq ($(REV_ID),)
|
||||
REV_ID=$(shell git -C $(SRCDIR) rev-parse --short HEAD)
|
||||
endif
|
||||
ifeq ($(REV_ID),)
|
||||
REV_ID=unknown
|
||||
endif
|
||||
endif
|
||||
VERSION_CFLAGS=-DCOMMIT_ID=\"$(REV_ID)\"
|
||||
75
app/src/main/jni/hev-socks5-tunnel/conf/main.yml
Normal file
75
app/src/main/jni/hev-socks5-tunnel/conf/main.yml
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Main configuration for hev-socks5-tunnel
|
||||
|
||||
tunnel:
|
||||
# Interface name
|
||||
name: tun0
|
||||
# Interface MTU
|
||||
mtu: 8500
|
||||
# Multi-queue
|
||||
multi-queue: false
|
||||
# IPv4 address
|
||||
ipv4: 198.18.0.1
|
||||
# IPv6 address
|
||||
ipv6: 'fc00::1'
|
||||
# Post up script
|
||||
# post-up-script: up.sh
|
||||
# Pre down script
|
||||
# pre-down-script: down.sh
|
||||
|
||||
socks5:
|
||||
# Socks5 server port
|
||||
port: 1080
|
||||
# Socks5 server address (ipv4/ipv6)
|
||||
address: 127.0.0.1
|
||||
# Socks5 UDP relay mode (tcp|udp)
|
||||
udp: 'udp'
|
||||
# Override the UDP address provided by the Socks5 server (ipv4/ipv6)
|
||||
# udp-address: ''
|
||||
# Socks5 handshake using pipeline mode
|
||||
# pipeline: false
|
||||
# Socks5 server username
|
||||
# username: 'username'
|
||||
# Socks5 server password
|
||||
# password: 'password'
|
||||
# Socket mark
|
||||
# mark: 0
|
||||
# TCP fastopen
|
||||
# tcp-fastopen: false
|
||||
|
||||
#mapdns:
|
||||
# Mapped DNS address
|
||||
# address: 198.18.0.2
|
||||
# Mapped DNS port
|
||||
# port: 53
|
||||
# Mapped IP network base
|
||||
# network: 100.64.0.0
|
||||
# Mapped IP network mask
|
||||
# netmask: 255.192.0.0
|
||||
# Mapped DNS cache size
|
||||
# cache-size: 10000
|
||||
|
||||
#misc:
|
||||
# task stack size (bytes)
|
||||
# task-stack-size: 86016
|
||||
# tcp buffer size (bytes)
|
||||
# tcp-buffer-size: 65536
|
||||
# udp socket recv buffer (SO_RCVBUF) size (bytes)
|
||||
# udp-recv-buffer-size: 524288
|
||||
# number of udp buffers in splice, 1500 bytes per buffer.
|
||||
# udp-copy-buffer-nums: 10
|
||||
# maximum session count (0: unlimited)
|
||||
# max-session-count: 0
|
||||
# connect timeout (ms)
|
||||
# connect-timeout: 10000
|
||||
# TCP read-write timeout (ms)
|
||||
# tcp-read-write-timeout: 300000
|
||||
# UDP read-write timeout (ms)
|
||||
# udp-read-write-timeout: 60000
|
||||
# stdout, stderr or file-path
|
||||
# log-file: stderr
|
||||
# debug, info, warn or error
|
||||
# log-level: warn
|
||||
# If present, run as a daemon with this pid file
|
||||
# pid-file: /run/hev-socks5-tunnel.pid
|
||||
# If present, set rlimit nofile; else use default value
|
||||
# limit-nofile: 65535
|
||||
89
app/src/main/jni/hev-socks5-tunnel/docker/entrypoint.sh
Normal file
89
app/src/main/jni/hev-socks5-tunnel/docker/entrypoint.sh
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#!/bin/sh
|
||||
|
||||
TUN="${TUN:-tun0}"
|
||||
MTU="${MTU:-8500}"
|
||||
IPV4="${IPV4:-198.18.0.1}"
|
||||
IPV6="${IPV6:-}"
|
||||
|
||||
CONFIG_ROUTES="${CONFIG_ROUTES:-1}"
|
||||
|
||||
TABLE="${TABLE:-20}"
|
||||
if [ "${CONFIG_ROUTES}" == "0" ]; then
|
||||
MARK="${MARK:-0}"
|
||||
else
|
||||
MARK="${MARK:-438}"
|
||||
fi
|
||||
|
||||
SOCKS5_ADDR="${SOCKS5_ADDR:-172.17.0.1}"
|
||||
SOCKS5_PORT="${SOCKS5_PORT:-1080}"
|
||||
SOCKS5_USERNAME="${SOCKS5_USERNAME:-}"
|
||||
SOCKS5_PASSWORD="${SOCKS5_PASSWORD:-}"
|
||||
SOCKS5_UDP_MODE="${SOCKS5_UDP_MODE:-udp}"
|
||||
SOCKS5_UDP_ADDR="${SOCKS5_UDP_ADDR:-}"
|
||||
|
||||
IPV4_INCLUDED_ROUTES="${IPV4_INCLUDED_ROUTES:-0.0.0.0/0}"
|
||||
IPV4_EXCLUDED_ROUTES="${IPV4_EXCLUDED_ROUTES:-}"
|
||||
|
||||
LOG_LEVEL="${LOG_LEVEL:-warn}"
|
||||
|
||||
config_file() {
|
||||
cat > /hs5t.yml << EOF
|
||||
misc:
|
||||
log-level: '${LOG_LEVEL}'
|
||||
tunnel:
|
||||
name: '${TUN}'
|
||||
mtu: ${MTU}
|
||||
ipv4: '${IPV4}'
|
||||
ipv6: '${IPV6}'
|
||||
post-up-script: '/route.sh'
|
||||
socks5:
|
||||
address: '${SOCKS5_ADDR}'
|
||||
port: ${SOCKS5_PORT}
|
||||
udp: '${SOCKS5_UDP_MODE}'
|
||||
mark: ${MARK}
|
||||
EOF
|
||||
|
||||
if [ -n "${SOCKS5_USERNAME}" ]; then
|
||||
echo " username: '${SOCKS5_USERNAME}'" >> /hs5t.yml
|
||||
fi
|
||||
|
||||
if [ -n "${SOCKS5_PASSWORD}" ]; then
|
||||
echo " password: '${SOCKS5_PASSWORD}'" >> /hs5t.yml
|
||||
fi
|
||||
|
||||
if [ -n "${SOCKS5_UDP_ADDR}" ]; then
|
||||
echo " udp-address: '${SOCKS5_UDP_ADDR}'" >> /hs5t.yml
|
||||
fi
|
||||
}
|
||||
|
||||
config_route() {
|
||||
echo "#!/bin/sh" > /route.sh
|
||||
chmod +x /route.sh
|
||||
|
||||
if [ "${CONFIG_ROUTES}" == "0" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "ip route add default dev ${TUN} table ${TABLE}" >> /route.sh
|
||||
|
||||
for addr in $(echo ${IPV4_INCLUDED_ROUTES} | tr ',' '\n'); do
|
||||
echo "ip rule add to ${addr} table ${TABLE}" >> /route.sh
|
||||
done
|
||||
|
||||
echo "ip rule add to $(ip -o -f inet address show eth0 | awk '/scope global/ {print $4}') table main" >> /route.sh
|
||||
|
||||
for addr in $(echo ${IPV4_EXCLUDED_ROUTES} | tr ',' '\n'); do
|
||||
echo "ip rule add to ${addr} table main" >> /route.sh
|
||||
done
|
||||
|
||||
echo "ip rule add fwmark ${MARK} table main pref 1" >> /route.sh
|
||||
}
|
||||
|
||||
run() {
|
||||
config_file
|
||||
config_route
|
||||
echo "echo 1 > /success" >> /route.sh
|
||||
hev-socks5-tunnel /hs5t.yml
|
||||
}
|
||||
|
||||
run || exit 1
|
||||
1
app/src/main/jni/hev-socks5-tunnel/include/hev-socks5-tunnel.h
Symbolic link
1
app/src/main/jni/hev-socks5-tunnel/include/hev-socks5-tunnel.h
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../src/hev-main.h
|
||||
4
app/src/main/jni/hev-socks5-tunnel/module.modulemap
Normal file
4
app/src/main/jni/hev-socks5-tunnel/module.modulemap
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module HevSocks5Tunnel {
|
||||
umbrella header "hev-main.h"
|
||||
export *
|
||||
}
|
||||
115
app/src/main/jni/hev-socks5-tunnel/src/core/.clang-format
Normal file
115
app/src/main/jni/hev-socks5-tunnel/src/core/.clang-format
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
---
|
||||
Language: Cpp
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: false
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: TopLevel
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: MultiLine
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterClass: true
|
||||
AfterControlStatement: false
|
||||
AfterEnum: true
|
||||
AfterFunction: true
|
||||
AfterNamespace: true
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: true
|
||||
AfterUnion: true
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Custom
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakBeforeTernaryOperators: false
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: false
|
||||
ColumnLimit: 80
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: false
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: false
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IndentCaseLabels: false
|
||||
IndentPPDirectives: None
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: Inner
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 4
|
||||
ObjCSpaceAfterProperty: true
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 10
|
||||
PenaltyBreakBeforeFirstCallParameter: 30
|
||||
PenaltyBreakComment: 10
|
||||
PenaltyBreakFirstLessLess: 0
|
||||
PenaltyBreakString: 10
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 100
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Right
|
||||
ReflowComments: false
|
||||
SortIncludes: false
|
||||
SortUsingDeclarations: false
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: Always
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: Cpp03
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
...
|
||||
26
app/src/main/jni/hev-socks5-tunnel/src/core/.github/workflows/code-format.yaml
vendored
Normal file
26
app/src/main/jni/hev-socks5-tunnel/src/core/.github/workflows/code-format.yaml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: "Check code formatting"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
checker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Prepare
|
||||
run: |
|
||||
sudo apt install -y clang-format
|
||||
- name: Format
|
||||
run: |
|
||||
find src -iname "*.[h,c]" -exec clang-format -i {} \;
|
||||
- name: Check
|
||||
run: |
|
||||
git status
|
||||
git diff --quiet
|
||||
19
app/src/main/jni/hev-socks5-tunnel/src/core/LICENSE
Normal file
19
app/src/main/jni/hev-socks5-tunnel/src/core/LICENSE
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2022 hev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
204
app/src/main/jni/hev-socks5-tunnel/src/core/README.md
Normal file
204
app/src/main/jni/hev-socks5-tunnel/src/core/README.md
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# HevSocks5Core
|
||||
|
||||
HevSocks5Core is a simple, lightweight socks5 library.
|
||||
|
||||
**Features**
|
||||
* IPv4/IPv6. (dual stack)
|
||||
* Standard `CONNECT` command.
|
||||
* Standard `UDP ASSOCIATE` command.
|
||||
* Extended `FWD UDP` command. (UDP in TCP)
|
||||
* Multiple username/password authentication.
|
||||
|
||||
**Dependencies**
|
||||
* HevTaskSystem - https://github.com/heiher/hev-task-system
|
||||
|
||||
## Examples
|
||||
|
||||
### Server
|
||||
|
||||
```c
|
||||
#include <unistd.h>
|
||||
|
||||
#include <hev-task.h>
|
||||
#include <hev-task-io.h>
|
||||
#include <hev-task-io-socket.h>
|
||||
#include <hev-task-dns.h>
|
||||
#include <hev-task-system.h>
|
||||
|
||||
#include <hev-socks5-server.h>
|
||||
|
||||
static void
|
||||
server_entry (void *data)
|
||||
{
|
||||
HevSocks5Server *server = data;
|
||||
hev_socks5_server_run (server);
|
||||
hev_object_unref (HEV_OBJECT (server));
|
||||
}
|
||||
|
||||
static void
|
||||
listener_entry (void *data)
|
||||
{
|
||||
struct addrinfo hints = { 0 };
|
||||
struct addrinfo *result;
|
||||
int fd;
|
||||
|
||||
hints.ai_family = AF_INET6;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_flags = AI_PASSIVE;
|
||||
|
||||
hev_task_dns_getaddrinfo (NULL, "1080", &hints, &result);
|
||||
fd = hev_task_io_socket_socket (AF_INET6, SOCK_STREAM, 0);
|
||||
bind (fd, result->ai_addr, result->ai_addrlen);
|
||||
freeaddrinfo (result);
|
||||
listen (fd, 5);
|
||||
|
||||
hev_task_add_fd (hev_task_self (), fd, POLLIN);
|
||||
|
||||
for (;;) {
|
||||
HevSocks5Server *server;
|
||||
HevTask *task;
|
||||
int nfd;
|
||||
|
||||
nfd = hev_task_io_socket_accept (fd, NULL, NULL, NULL, NULL);
|
||||
|
||||
task = hev_task_new (-1);
|
||||
server = hev_socks5_server_new (nfd);
|
||||
hev_task_run (task, server_entry, server);
|
||||
}
|
||||
|
||||
close (fd);
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
HevTask *task;
|
||||
|
||||
hev_task_system_init ();
|
||||
|
||||
task = hev_task_new (-1);
|
||||
hev_task_run (task, listener_entry, NULL);
|
||||
|
||||
hev_task_system_run ();
|
||||
|
||||
hev_task_system_fini ();
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```c
|
||||
#include <stddef.h>
|
||||
|
||||
#include <hev-task.h>
|
||||
#include <hev-task-system.h>
|
||||
#include <hev-socks5-client-tcp.h>
|
||||
#include <hev-socks5-client-udp.h>
|
||||
|
||||
static void
|
||||
tcp_client_entry (void *data)
|
||||
{
|
||||
HevSocks5ClientTCP *tcp;
|
||||
|
||||
tcp = hev_socks5_client_tcp_new_name ("www.google.com", 443);
|
||||
hev_socks5_client_connect (HEV_SOCKS5_CLIENT (tcp), "127.0.0.1", 1080);
|
||||
hev_socks5_client_handshake (HEV_SOCKS5_CLIENT (tcp));
|
||||
|
||||
/*
|
||||
* splice data to/from a socket fd:
|
||||
* hev_socks5_tcp_splice (HEV_SOCKS5_TCP (tcp), fd);
|
||||
*/
|
||||
|
||||
hev_object_unref (HEV_OBJECT (tcp));
|
||||
}
|
||||
|
||||
static void
|
||||
udp_client_entry (void *data)
|
||||
{
|
||||
HevSocks5ClientUDP *udp;
|
||||
|
||||
udp = hev_socks5_client_udp_new (HEV_SOCKS5_TYPE_UDP_IN_TCP);
|
||||
hev_socks5_client_connect (HEV_SOCKS5_CLIENT (udp), "127.0.0.1", 1080);
|
||||
hev_socks5_client_handshake (HEV_SOCKS5_CLIENT (udp));
|
||||
|
||||
/*
|
||||
* HevSocks5UDPMsg msgv[num];
|
||||
*
|
||||
* send udp packets:
|
||||
* hev_socks5_udp_sendmmsg (HEV_SOCKS5_UDP (udp), msgv, num);
|
||||
*
|
||||
* recv udp packets:
|
||||
* hev_socks5_udp_recvmmsg (HEV_SOCKS5_UDP (udp), msgv, num, 0);
|
||||
*/
|
||||
|
||||
hev_object_unref (HEV_OBJECT (udp));
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
HevTask *task;
|
||||
|
||||
hev_task_system_init ();
|
||||
|
||||
task = hev_task_new (-1);
|
||||
hev_task_run (task, tcp_client_entry, NULL);
|
||||
|
||||
task = hev_task_new (-1);
|
||||
hev_task_run (task, udp_client_entry, NULL);
|
||||
|
||||
hev_task_system_run ();
|
||||
|
||||
hev_task_system_fini ();
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## UDP in TCP
|
||||
|
||||
UDP-in-TCP mode is a proprietary extension based on RFC 1928, designed to
|
||||
forward UDP packets within the primary SOCKS5 TCP stream. The protocol is
|
||||
defined as follows:
|
||||
|
||||
### SOCKS5 Requests
|
||||
|
||||
```
|
||||
+----+-----+-------+------+----------+----------+
|
||||
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|
||||
+----+-----+-------+------+----------+----------+
|
||||
| 1 | 1 | X'00' | 1 | Variable | 2 |
|
||||
+----+-----+-------+------+----------+----------+
|
||||
```
|
||||
|
||||
* CMD
|
||||
* UDP IN TCP: X'05'
|
||||
|
||||
### UDP Relays
|
||||
|
||||
```
|
||||
+--------+--------+------+----------+----------+----------+
|
||||
| MSGLEN | HDRLEN | ATYP | DST.ADDR | DST.PORT | DATA |
|
||||
+--------+--------+------+----------+----------+----------+
|
||||
| 2 | 1 | 1 | Variable | 2 | Variable |
|
||||
+--------+--------+------+----------+----------+----------+
|
||||
```
|
||||
|
||||
- MSGLEN: The total length of the UDP relay message. `[MSGLEN, DATA]`
|
||||
- HDRLEN: The header length of the UDP relay message. `[MSGLEN, DST.PORT]`
|
||||
- ATYPE/DST.ADDR/DST.PORT: Fields follow the definitions specified in RFC 1928.
|
||||
|
||||
## Users
|
||||
|
||||
* **HevSocks5Server** - https://github.com/heiher/hev-socks5-server
|
||||
* **HevSocks5TProxy** - https://github.com/heiher/hev-socks5-tproxy
|
||||
* **HevSocks5Tunnel** - https://github.com/heiher/hev-socks5-tunnel
|
||||
|
||||
## Contributors
|
||||
* **hev** - https://hev.cc
|
||||
* **spider84** - https://github.com/spider84
|
||||
|
||||
## License
|
||||
MIT
|
||||
1
app/src/main/jni/hev-socks5-tunnel/src/core/include/hev-rbtree.h
Symbolic link
1
app/src/main/jni/hev-socks5-tunnel/src/core/include/hev-rbtree.h
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../src/hev-rbtree.h
|
||||
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5-authenticator.h
|
||||
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5-client-tcp.h
|
||||
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5-client-udp.h
|
||||
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5-client.h
|
||||
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5-logger.h
|
||||
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5-misc.h
|
||||
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5-proto.h
|
||||
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5-server.h
|
||||
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5-tcp.h
|
||||
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5-udp.h
|
||||
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5-user.h
|
||||
1
app/src/main/jni/hev-socks5-tunnel/src/core/include/hev-socks5.h
Symbolic link
1
app/src/main/jni/hev-socks5-tunnel/src/core/include/hev-socks5.h
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../src/hev-socks5.h
|
||||
106
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-compiler.h
Normal file
106
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-compiler.h
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-compiler.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2019 everyone.
|
||||
Description : Compiler
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_COMPILER_H__
|
||||
#define __HEV_COMPILER_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define ALIGN_UP(addr, align) \
|
||||
((addr + (typeof (addr))align - 1) & ~((typeof (addr))align - 1))
|
||||
|
||||
#define ALIGN_DOWN(addr, align) ((addr) & ~((typeof (addr))align - 1))
|
||||
|
||||
#ifndef ARRAY_SIZE
|
||||
#define ARRAY_SIZE(x) (sizeof (x) / sizeof (x[0]))
|
||||
#endif
|
||||
|
||||
#ifndef EXPORT_SYMBOL
|
||||
#define EXPORT_SYMBOL __attribute__ ((visibility ("default")))
|
||||
#endif
|
||||
|
||||
#define barrier() __asm__ __volatile__ ("" : : : "memory")
|
||||
|
||||
static inline void
|
||||
__read_once_size (void *dst, const volatile void *src, int size)
|
||||
{
|
||||
switch (size) {
|
||||
case sizeof (char):
|
||||
*(char *)dst = *(volatile char *)src;
|
||||
break;
|
||||
case sizeof (short):
|
||||
*(short *)dst = *(volatile short *)src;
|
||||
break;
|
||||
case sizeof (int):
|
||||
*(int *)dst = *(volatile int *)src;
|
||||
break;
|
||||
default:
|
||||
barrier ();
|
||||
__builtin_memcpy ((void *)dst, (const void *)src, size);
|
||||
barrier ();
|
||||
}
|
||||
}
|
||||
|
||||
static inline void
|
||||
__write_once_size (volatile void *dst, const void *src, int size)
|
||||
{
|
||||
switch (size) {
|
||||
case sizeof (char):
|
||||
*(volatile char *)dst = *(char *)src;
|
||||
break;
|
||||
case sizeof (short):
|
||||
*(volatile short *)dst = *(short *)src;
|
||||
break;
|
||||
case sizeof (int):
|
||||
*(volatile int *)dst = *(int *)src;
|
||||
break;
|
||||
default:
|
||||
barrier ();
|
||||
__builtin_memcpy ((void *)dst, (const void *)src, size);
|
||||
barrier ();
|
||||
}
|
||||
}
|
||||
|
||||
#define READ_ONCE(x) \
|
||||
({ \
|
||||
union \
|
||||
{ \
|
||||
typeof (x) __val; \
|
||||
char __c[1]; \
|
||||
} __u; \
|
||||
__read_once_size (__u.__c, &(x), sizeof (x)); \
|
||||
__u.__val; \
|
||||
})
|
||||
|
||||
#define WRITE_ONCE(x, val) \
|
||||
({ \
|
||||
union \
|
||||
{ \
|
||||
typeof (x) __val; \
|
||||
char __c[1]; \
|
||||
} __u = { .__val = (typeof (x))(val) }; \
|
||||
__write_once_size (&(x), __u.__c, sizeof (x)); \
|
||||
__u.__val; \
|
||||
})
|
||||
|
||||
#ifndef container_of
|
||||
#define container_of(ptr, type, member) \
|
||||
({ \
|
||||
void *__mptr = (void *)(ptr); \
|
||||
((type *)(__mptr - offsetof (type, member))); \
|
||||
})
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_COMPILER_H__ */
|
||||
635
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-rbtree.c
Normal file
635
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-rbtree.c
Normal file
|
|
@ -0,0 +1,635 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-rbtree.c
|
||||
Authors : Andrea Arcangeli <andrea@suse.de>
|
||||
David Woodhouse <dwmw2@infradead.org>
|
||||
Michel Lespinasse <walken@google.com>
|
||||
Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2018 everyone.
|
||||
Description : RedBlack Tree
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#include "hev-rbtree.h"
|
||||
|
||||
typedef enum _HevRBTreeColor HevRBTreeColor;
|
||||
|
||||
enum _HevRBTreeColor
|
||||
{
|
||||
HEV_RBTREE_RED = 0,
|
||||
HEV_RBTREE_BLACK,
|
||||
};
|
||||
|
||||
static inline int
|
||||
hev_rbtree_node_color (HevRBTreeNode *node)
|
||||
{
|
||||
return node->__parent_color & 1;
|
||||
}
|
||||
|
||||
static inline int
|
||||
hev_rbtree_node_is_red (HevRBTreeNode *node)
|
||||
{
|
||||
return !(hev_rbtree_node_color (node) & HEV_RBTREE_BLACK);
|
||||
}
|
||||
|
||||
static inline int
|
||||
hev_rbtree_node_is_black (HevRBTreeNode *node)
|
||||
{
|
||||
return hev_rbtree_node_color (node) & HEV_RBTREE_BLACK;
|
||||
}
|
||||
|
||||
static inline int
|
||||
_hev_rbtree_node_is_black (unsigned long pc)
|
||||
{
|
||||
return pc & HEV_RBTREE_BLACK;
|
||||
}
|
||||
|
||||
static inline HevRBTreeNode *
|
||||
_hev_rbtree_node_parent (unsigned long pc)
|
||||
{
|
||||
return ((HevRBTreeNode *)(pc & ~3));
|
||||
}
|
||||
|
||||
static inline HevRBTreeNode *
|
||||
hev_rbtree_node_red_parent (HevRBTreeNode *node)
|
||||
{
|
||||
return (HevRBTreeNode *)node->__parent_color;
|
||||
}
|
||||
|
||||
static inline void
|
||||
hev_rbtree_node_set_black (HevRBTreeNode *node)
|
||||
{
|
||||
node->__parent_color |= HEV_RBTREE_BLACK;
|
||||
}
|
||||
|
||||
static inline void
|
||||
hev_rbtree_node_set_parent (HevRBTreeNode *node, HevRBTreeNode *parent)
|
||||
{
|
||||
node->__parent_color = hev_rbtree_node_color (node) | (unsigned long)parent;
|
||||
}
|
||||
|
||||
static inline void
|
||||
hev_rbtree_node_set_parent_color (HevRBTreeNode *node, HevRBTreeNode *parent,
|
||||
int color)
|
||||
{
|
||||
node->__parent_color = (unsigned long)parent | color;
|
||||
}
|
||||
|
||||
static inline void
|
||||
hev_rbtree_change_child (HevRBTree *self, HevRBTreeNode *old,
|
||||
HevRBTreeNode *new, HevRBTreeNode *parent)
|
||||
{
|
||||
if (parent) {
|
||||
if (parent->left == old)
|
||||
parent->left = new;
|
||||
else
|
||||
parent->right = new;
|
||||
} else {
|
||||
self->root = new;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function for rotations:
|
||||
* - old's parent and color get assigned to new
|
||||
* - old gets assigned new as a parent and 'color' as a color.
|
||||
*/
|
||||
static inline void
|
||||
hev_rbtree_rotate_set_parents (HevRBTree *self, HevRBTreeNode *old,
|
||||
HevRBTreeNode *new, int color)
|
||||
{
|
||||
HevRBTreeNode *parent = hev_rbtree_node_parent (old);
|
||||
new->__parent_color = old->__parent_color;
|
||||
hev_rbtree_node_set_parent_color (old, new, color);
|
||||
hev_rbtree_change_child (self, old, new, parent);
|
||||
}
|
||||
|
||||
HevRBTreeNode *
|
||||
hev_rbtree_node_prev (HevRBTreeNode *node)
|
||||
{
|
||||
HevRBTreeNode *parent;
|
||||
|
||||
if (hev_rbtree_node_empty (node))
|
||||
return NULL;
|
||||
|
||||
/*
|
||||
* If we have a left-hand child, go down and then right as far
|
||||
* as we can.
|
||||
*/
|
||||
if (node->left) {
|
||||
node = node->left;
|
||||
while (node->right)
|
||||
node = node->right;
|
||||
return (HevRBTreeNode *)node;
|
||||
}
|
||||
|
||||
/*
|
||||
* No left-hand children. Go up till we find an ancestor which
|
||||
* is a right-hand child of its parent.
|
||||
*/
|
||||
while ((parent = hev_rbtree_node_parent (node)) && node == parent->left)
|
||||
node = parent;
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
HevRBTreeNode *
|
||||
hev_rbtree_node_next (HevRBTreeNode *node)
|
||||
{
|
||||
HevRBTreeNode *parent;
|
||||
|
||||
if (hev_rbtree_node_empty (node))
|
||||
return NULL;
|
||||
|
||||
/*
|
||||
* If we have a right-hand child, go down and then left as far
|
||||
* as we can.
|
||||
*/
|
||||
if (node->right) {
|
||||
node = node->right;
|
||||
while (node->left)
|
||||
node = node->left;
|
||||
return (HevRBTreeNode *)node;
|
||||
}
|
||||
|
||||
/*
|
||||
* No right-hand children. Everything down and left is smaller than us,
|
||||
* so any 'next' node must be in the general direction of our parent.
|
||||
* Go up the tree; any time the ancestor is a right-hand child of its
|
||||
* parent, keep going up. First time it's a left-hand child of its
|
||||
* parent, said parent is our 'next' node.
|
||||
*/
|
||||
while ((parent = hev_rbtree_node_parent (node)) && node == parent->right)
|
||||
node = parent;
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
HevRBTreeNode *
|
||||
hev_rbtree_first (HevRBTree *self)
|
||||
{
|
||||
HevRBTreeNode *n;
|
||||
|
||||
n = self->root;
|
||||
if (!n)
|
||||
return NULL;
|
||||
while (n->left)
|
||||
n = n->left;
|
||||
return n;
|
||||
}
|
||||
|
||||
HevRBTreeNode *
|
||||
hev_rbtree_last (HevRBTree *self)
|
||||
{
|
||||
HevRBTreeNode *n;
|
||||
|
||||
n = self->root;
|
||||
if (!n)
|
||||
return NULL;
|
||||
while (n->right)
|
||||
n = n->right;
|
||||
return n;
|
||||
}
|
||||
|
||||
void
|
||||
hev_rbtree_insert_color (HevRBTree *self, HevRBTreeNode *node)
|
||||
{
|
||||
HevRBTreeNode *parent = hev_rbtree_node_red_parent (node), *gparent, *tmp;
|
||||
|
||||
while (1) {
|
||||
/*
|
||||
* Loop invariant: node is red.
|
||||
*/
|
||||
if (!parent) {
|
||||
/*
|
||||
* The inserted node is root. Either this is the
|
||||
* first node, or we recursed at Case 1 below and
|
||||
* are no longer violating 4).
|
||||
*/
|
||||
hev_rbtree_node_set_parent_color (node, NULL, HEV_RBTREE_BLACK);
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* If there is a black parent, we are done.
|
||||
* Otherwise, take some corrective action as,
|
||||
* per 4), we don't want a red root or two
|
||||
* consecutive red nodes.
|
||||
*/
|
||||
if (hev_rbtree_node_is_black (parent))
|
||||
break;
|
||||
|
||||
gparent = hev_rbtree_node_red_parent (parent);
|
||||
|
||||
tmp = gparent->right;
|
||||
if (parent != tmp) { /* parent == gparent->left */
|
||||
if (tmp && hev_rbtree_node_is_red (tmp)) {
|
||||
/*
|
||||
* Case 1 - node's uncle is red (color flips).
|
||||
*
|
||||
* G g
|
||||
* / \ / \
|
||||
* p u --> P U
|
||||
* / /
|
||||
* n n
|
||||
*
|
||||
* However, since g's parent might be red, and
|
||||
* 4) does not allow this, we need to recurse
|
||||
* at g.
|
||||
*/
|
||||
hev_rbtree_node_set_parent_color (tmp, gparent,
|
||||
HEV_RBTREE_BLACK);
|
||||
hev_rbtree_node_set_parent_color (parent, gparent,
|
||||
HEV_RBTREE_BLACK);
|
||||
node = gparent;
|
||||
parent = hev_rbtree_node_parent (node);
|
||||
hev_rbtree_node_set_parent_color (node, parent, HEV_RBTREE_RED);
|
||||
continue;
|
||||
}
|
||||
|
||||
tmp = parent->right;
|
||||
if (node == tmp) {
|
||||
/*
|
||||
* Case 2 - node's uncle is black and node is
|
||||
* the parent's right child (left rotate at parent).
|
||||
*
|
||||
* G G
|
||||
* / \ / \
|
||||
* p U --> n U
|
||||
* \ /
|
||||
* n p
|
||||
*
|
||||
* This still leaves us in violation of 4), the
|
||||
* continuation into Case 3 will fix that.
|
||||
*/
|
||||
tmp = node->left;
|
||||
parent->right = tmp;
|
||||
node->left = parent;
|
||||
if (tmp)
|
||||
hev_rbtree_node_set_parent_color (tmp, parent,
|
||||
HEV_RBTREE_BLACK);
|
||||
hev_rbtree_node_set_parent_color (parent, node, HEV_RBTREE_RED);
|
||||
parent = node;
|
||||
tmp = node->right;
|
||||
}
|
||||
|
||||
/*
|
||||
* Case 3 - node's uncle is black and node is
|
||||
* the parent's left child (right rotate at gparent).
|
||||
*
|
||||
* G P
|
||||
* / \ / \
|
||||
* p U --> n g
|
||||
* / \
|
||||
* n U
|
||||
*/
|
||||
gparent->left = tmp; /* == parent->right */
|
||||
parent->right = gparent;
|
||||
if (tmp)
|
||||
hev_rbtree_node_set_parent_color (tmp, gparent,
|
||||
HEV_RBTREE_BLACK);
|
||||
hev_rbtree_rotate_set_parents (self, gparent, parent,
|
||||
HEV_RBTREE_RED);
|
||||
break;
|
||||
} else {
|
||||
tmp = gparent->left;
|
||||
if (tmp && hev_rbtree_node_is_red (tmp)) {
|
||||
/* Case 1 - color flips */
|
||||
hev_rbtree_node_set_parent_color (tmp, gparent,
|
||||
HEV_RBTREE_BLACK);
|
||||
hev_rbtree_node_set_parent_color (parent, gparent,
|
||||
HEV_RBTREE_BLACK);
|
||||
node = gparent;
|
||||
parent = hev_rbtree_node_parent (node);
|
||||
hev_rbtree_node_set_parent_color (node, parent, HEV_RBTREE_RED);
|
||||
continue;
|
||||
}
|
||||
|
||||
tmp = parent->left;
|
||||
if (node == tmp) {
|
||||
/* Case 2 - right rotate at parent */
|
||||
tmp = node->right;
|
||||
parent->left = tmp;
|
||||
node->right = parent;
|
||||
if (tmp)
|
||||
hev_rbtree_node_set_parent_color (tmp, parent,
|
||||
HEV_RBTREE_BLACK);
|
||||
hev_rbtree_node_set_parent_color (parent, node, HEV_RBTREE_RED);
|
||||
parent = node;
|
||||
tmp = node->left;
|
||||
}
|
||||
|
||||
/* Case 3 - left rotate at gparent */
|
||||
gparent->right = tmp; /* == parent->left */
|
||||
parent->left = gparent;
|
||||
if (tmp)
|
||||
hev_rbtree_node_set_parent_color (tmp, gparent,
|
||||
HEV_RBTREE_BLACK);
|
||||
hev_rbtree_rotate_set_parents (self, gparent, parent,
|
||||
HEV_RBTREE_RED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
hev_rbtree_replace (HevRBTree *self, HevRBTreeNode *victim, HevRBTreeNode *new)
|
||||
{
|
||||
HevRBTreeNode *parent = hev_rbtree_node_parent (victim);
|
||||
|
||||
/* Copy the pointers/colour from the victim to the replacement */
|
||||
*new = *victim;
|
||||
|
||||
/* Set the surrounding nodes to point to the replacement */
|
||||
if (victim->left)
|
||||
hev_rbtree_node_set_parent (victim->left, new);
|
||||
if (victim->right)
|
||||
hev_rbtree_node_set_parent (victim->right, new);
|
||||
hev_rbtree_change_child (self, victim, new, parent);
|
||||
}
|
||||
|
||||
static inline HevRBTreeNode *
|
||||
_hev_rbtree_erase (HevRBTree *self, HevRBTreeNode *node)
|
||||
{
|
||||
HevRBTreeNode *child = node->right;
|
||||
HevRBTreeNode *tmp = node->left;
|
||||
HevRBTreeNode *parent, *rebalance;
|
||||
unsigned long pc;
|
||||
|
||||
if (!tmp) {
|
||||
/*
|
||||
* Case 1: node to erase has no more than 1 child (easy!)
|
||||
*
|
||||
* Note that if there is one child it must be red due to 5)
|
||||
* and node must be black due to 4). We adjust colors locally
|
||||
* so as to bypass _hev_rbtree_erase_color() later on.
|
||||
*/
|
||||
pc = node->__parent_color;
|
||||
parent = _hev_rbtree_node_parent (pc);
|
||||
hev_rbtree_change_child (self, node, child, parent);
|
||||
if (child) {
|
||||
child->__parent_color = pc;
|
||||
rebalance = NULL;
|
||||
} else
|
||||
rebalance = _hev_rbtree_node_is_black (pc) ? parent : NULL;
|
||||
tmp = parent;
|
||||
} else if (!child) {
|
||||
/* Still case 1, but this time the child is node->left */
|
||||
tmp->__parent_color = pc = node->__parent_color;
|
||||
parent = _hev_rbtree_node_parent (pc);
|
||||
hev_rbtree_change_child (self, node, tmp, parent);
|
||||
rebalance = NULL;
|
||||
tmp = parent;
|
||||
} else {
|
||||
HevRBTreeNode *successor = child, *child2;
|
||||
|
||||
tmp = child->left;
|
||||
if (!tmp) {
|
||||
/*
|
||||
* Case 2: node's successor is its right child
|
||||
*
|
||||
* (n) (s)
|
||||
* / \ / \
|
||||
* (x) (s) -> (x) (c)
|
||||
* \
|
||||
* (c)
|
||||
*/
|
||||
parent = successor;
|
||||
child2 = successor->right;
|
||||
} else {
|
||||
/*
|
||||
* Case 3: node's successor is leftmost under
|
||||
* node's right child subtree
|
||||
*
|
||||
* (n) (s)
|
||||
* / \ / \
|
||||
* (x) (y) -> (x) (y)
|
||||
* / /
|
||||
* (p) (p)
|
||||
* / /
|
||||
* (s) (c)
|
||||
* \
|
||||
* (c)
|
||||
*/
|
||||
do {
|
||||
parent = successor;
|
||||
successor = tmp;
|
||||
tmp = tmp->left;
|
||||
} while (tmp);
|
||||
child2 = successor->right;
|
||||
parent->left = child2;
|
||||
successor->right = child;
|
||||
hev_rbtree_node_set_parent (child, successor);
|
||||
}
|
||||
|
||||
tmp = node->left;
|
||||
successor->left = tmp;
|
||||
hev_rbtree_node_set_parent (tmp, successor);
|
||||
|
||||
pc = node->__parent_color;
|
||||
tmp = _hev_rbtree_node_parent (pc);
|
||||
hev_rbtree_change_child (self, node, successor, tmp);
|
||||
|
||||
if (child2) {
|
||||
hev_rbtree_node_set_parent_color (child2, parent, HEV_RBTREE_BLACK);
|
||||
rebalance = NULL;
|
||||
} else {
|
||||
rebalance = hev_rbtree_node_is_black (successor) ? parent : NULL;
|
||||
}
|
||||
successor->__parent_color = pc;
|
||||
tmp = successor;
|
||||
}
|
||||
|
||||
return rebalance;
|
||||
}
|
||||
|
||||
/*
|
||||
* Inline version for hev_rbtree_erase() use - we want to be able to inline
|
||||
* and eliminate the dummy_rotate callback there
|
||||
*/
|
||||
static inline void
|
||||
_hev_rbtree_erase_color (HevRBTree *self, HevRBTreeNode *parent)
|
||||
{
|
||||
HevRBTreeNode *node = NULL, *sibling, *tmp1, *tmp2;
|
||||
|
||||
while (1) {
|
||||
/*
|
||||
* Loop invariants:
|
||||
* - node is black (or NULL on first iteration)
|
||||
* - node is not the root (parent is not NULL)
|
||||
* - All leaf paths going through parent and node have a
|
||||
* black node count that is 1 lower than other leaf paths.
|
||||
*/
|
||||
sibling = parent->right;
|
||||
if (node != sibling) { /* node == parent->left */
|
||||
if (hev_rbtree_node_is_red (sibling)) {
|
||||
/*
|
||||
* Case 1 - left rotate at parent
|
||||
*
|
||||
* P S
|
||||
* / \ / \
|
||||
* N s --> p Sr
|
||||
* / \ / \
|
||||
* Sl Sr N Sl
|
||||
*/
|
||||
tmp1 = sibling->left;
|
||||
parent->right = tmp1;
|
||||
sibling->left = parent;
|
||||
hev_rbtree_node_set_parent_color (tmp1, parent,
|
||||
HEV_RBTREE_BLACK);
|
||||
hev_rbtree_rotate_set_parents (self, parent, sibling,
|
||||
HEV_RBTREE_RED);
|
||||
sibling = tmp1;
|
||||
}
|
||||
tmp1 = sibling->right;
|
||||
if (!tmp1 || hev_rbtree_node_is_black (tmp1)) {
|
||||
tmp2 = sibling->left;
|
||||
if (!tmp2 || hev_rbtree_node_is_black (tmp2)) {
|
||||
/*
|
||||
* Case 2 - sibling color flip
|
||||
* (p could be either color here)
|
||||
*
|
||||
* (p) (p)
|
||||
* / \ / \
|
||||
* N S --> N s
|
||||
* / \ / \
|
||||
* Sl Sr Sl Sr
|
||||
*
|
||||
* This leaves us violating 5) which
|
||||
* can be fixed by flipping p to black
|
||||
* if it was red, or by recursing at p.
|
||||
* p is red when coming from Case 1.
|
||||
*/
|
||||
hev_rbtree_node_set_parent_color (sibling, parent,
|
||||
HEV_RBTREE_RED);
|
||||
if (hev_rbtree_node_is_red (parent))
|
||||
hev_rbtree_node_set_black (parent);
|
||||
else {
|
||||
node = parent;
|
||||
parent = hev_rbtree_node_parent (node);
|
||||
if (parent)
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
/*
|
||||
* Case 3 - right rotate at sibling
|
||||
* (p could be either color here)
|
||||
*
|
||||
* (p) (p)
|
||||
* / \ / \
|
||||
* N S --> N sl
|
||||
* / \ \
|
||||
* sl Sr S
|
||||
* \
|
||||
* Sr
|
||||
*
|
||||
* Note: p might be red, and then both
|
||||
* p and sl are red after rotation(which
|
||||
* breaks property 4). This is fixed in
|
||||
* Case 4 (in hev_rbtree_rotate_set_parents()
|
||||
* which set sl the color of p
|
||||
* and set p HEV_RBTREE_BLACK)
|
||||
*
|
||||
* (p) (sl)
|
||||
* / \ / \
|
||||
* N sl --> P S
|
||||
* \ / \
|
||||
* S N Sr
|
||||
* \
|
||||
* Sr
|
||||
*/
|
||||
tmp1 = tmp2->right;
|
||||
sibling->left = tmp1;
|
||||
tmp2->right = sibling;
|
||||
parent->right = tmp2;
|
||||
if (tmp1)
|
||||
hev_rbtree_node_set_parent_color (tmp1, sibling,
|
||||
HEV_RBTREE_BLACK);
|
||||
tmp1 = sibling;
|
||||
sibling = tmp2;
|
||||
}
|
||||
/*
|
||||
* Case 4 - left rotate at parent + color flips
|
||||
* (p and sl could be either color here.
|
||||
* After rotation, p becomes black, s acquires
|
||||
* p's color, and sl keeps its color)
|
||||
*
|
||||
* (p) (s)
|
||||
* / \ / \
|
||||
* N S --> P Sr
|
||||
* / \ / \
|
||||
* (sl) sr N (sl)
|
||||
*/
|
||||
tmp2 = sibling->left;
|
||||
parent->right = tmp2;
|
||||
sibling->left = parent;
|
||||
hev_rbtree_node_set_parent_color (tmp1, sibling, HEV_RBTREE_BLACK);
|
||||
if (tmp2)
|
||||
hev_rbtree_node_set_parent (tmp2, parent);
|
||||
hev_rbtree_rotate_set_parents (self, parent, sibling,
|
||||
HEV_RBTREE_BLACK);
|
||||
break;
|
||||
} else {
|
||||
sibling = parent->left;
|
||||
if (hev_rbtree_node_is_red (sibling)) {
|
||||
/* Case 1 - right rotate at parent */
|
||||
tmp1 = sibling->right;
|
||||
parent->left = tmp1;
|
||||
sibling->right = parent;
|
||||
hev_rbtree_node_set_parent_color (tmp1, parent,
|
||||
HEV_RBTREE_BLACK);
|
||||
hev_rbtree_rotate_set_parents (self, parent, sibling,
|
||||
HEV_RBTREE_RED);
|
||||
sibling = tmp1;
|
||||
}
|
||||
tmp1 = sibling->left;
|
||||
if (!tmp1 || hev_rbtree_node_is_black (tmp1)) {
|
||||
tmp2 = sibling->right;
|
||||
if (!tmp2 || hev_rbtree_node_is_black (tmp2)) {
|
||||
/* Case 2 - sibling color flip */
|
||||
hev_rbtree_node_set_parent_color (sibling, parent,
|
||||
HEV_RBTREE_RED);
|
||||
if (hev_rbtree_node_is_red (parent))
|
||||
hev_rbtree_node_set_black (parent);
|
||||
else {
|
||||
node = parent;
|
||||
parent = hev_rbtree_node_parent (node);
|
||||
if (parent)
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
/* Case 3 - left rotate at sibling */
|
||||
tmp1 = tmp2->left;
|
||||
sibling->right = tmp1;
|
||||
tmp2->left = sibling;
|
||||
parent->left = tmp2;
|
||||
if (tmp1)
|
||||
hev_rbtree_node_set_parent_color (tmp1, sibling,
|
||||
HEV_RBTREE_BLACK);
|
||||
tmp1 = sibling;
|
||||
sibling = tmp2;
|
||||
}
|
||||
/* Case 4 - right rotate at parent + color flips */
|
||||
tmp2 = sibling->right;
|
||||
parent->left = tmp2;
|
||||
sibling->right = parent;
|
||||
hev_rbtree_node_set_parent_color (tmp1, sibling, HEV_RBTREE_BLACK);
|
||||
if (tmp2)
|
||||
hev_rbtree_node_set_parent (tmp2, parent);
|
||||
hev_rbtree_rotate_set_parents (self, parent, sibling,
|
||||
HEV_RBTREE_BLACK);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
hev_rbtree_erase (HevRBTree *self, HevRBTreeNode *node)
|
||||
{
|
||||
HevRBTreeNode *rebalance;
|
||||
rebalance = _hev_rbtree_erase (self, node);
|
||||
if (rebalance)
|
||||
_hev_rbtree_erase_color (self, rebalance);
|
||||
}
|
||||
68
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-rbtree.h
Normal file
68
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-rbtree.h
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-rbtree.h
|
||||
Authors : Andrea Arcangeli <andrea@suse.de>
|
||||
David Woodhouse <dwmw2@infradead.org>
|
||||
Michel Lespinasse <walken@google.com>
|
||||
Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2018 everyone.
|
||||
Description : RedBlack Tree
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_RBTREE_H__
|
||||
#define __HEV_RBTREE_H__
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct _HevRBTree HevRBTree;
|
||||
typedef struct _HevRBTreeNode HevRBTreeNode;
|
||||
|
||||
struct _HevRBTree
|
||||
{
|
||||
HevRBTreeNode *root;
|
||||
};
|
||||
|
||||
struct _HevRBTreeNode
|
||||
{
|
||||
unsigned long __parent_color;
|
||||
HevRBTreeNode *right;
|
||||
HevRBTreeNode *left;
|
||||
} __attribute__ ((aligned (sizeof (long))));
|
||||
|
||||
static inline int
|
||||
hev_rbtree_node_empty (HevRBTreeNode *node)
|
||||
{
|
||||
return node->__parent_color == (unsigned long)node;
|
||||
}
|
||||
|
||||
static inline HevRBTreeNode *
|
||||
hev_rbtree_node_parent (HevRBTreeNode *node)
|
||||
{
|
||||
return (HevRBTreeNode *)(node->__parent_color & ~3);
|
||||
}
|
||||
|
||||
static inline void
|
||||
hev_rbtree_node_link (HevRBTreeNode *node, HevRBTreeNode *parent,
|
||||
HevRBTreeNode **link)
|
||||
{
|
||||
node->__parent_color = (unsigned long)parent;
|
||||
node->left = node->right = NULL;
|
||||
|
||||
*link = node;
|
||||
}
|
||||
|
||||
HevRBTreeNode *hev_rbtree_node_prev (HevRBTreeNode *node);
|
||||
HevRBTreeNode *hev_rbtree_node_next (HevRBTreeNode *node);
|
||||
|
||||
HevRBTreeNode *hev_rbtree_first (HevRBTree *self);
|
||||
HevRBTreeNode *hev_rbtree_last (HevRBTree *self);
|
||||
|
||||
void hev_rbtree_insert_color (HevRBTree *self, HevRBTreeNode *node);
|
||||
|
||||
void hev_rbtree_replace (HevRBTree *self, HevRBTreeNode *victim,
|
||||
HevRBTreeNode *new);
|
||||
|
||||
void hev_rbtree_erase (HevRBTree *self, HevRBTreeNode *node);
|
||||
|
||||
#endif /* __HEV_RBTREE_H__ */
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-authenticator.c
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2023 hev
|
||||
Description : Socks5 Authenticator
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "hev-compiler.h"
|
||||
#include "hev-socks5-logger-priv.h"
|
||||
|
||||
#include "hev-socks5-authenticator.h"
|
||||
|
||||
HevSocks5Authenticator *
|
||||
hev_socks5_authenticator_new (void)
|
||||
{
|
||||
HevSocks5Authenticator *self;
|
||||
int res;
|
||||
|
||||
self = calloc (1, sizeof (HevSocks5Authenticator));
|
||||
if (!self)
|
||||
return NULL;
|
||||
|
||||
res = hev_socks5_authenticator_construct (self);
|
||||
if (res < 0) {
|
||||
free (self);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_D ("%p socks5 authenticator new", self);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_authenticator_add (HevSocks5Authenticator *self, HevSocks5User *user)
|
||||
{
|
||||
HevRBTreeNode **new = &self->tree.root, *parent = NULL;
|
||||
|
||||
while (*new) {
|
||||
HevSocks5User *this;
|
||||
int res;
|
||||
|
||||
this = container_of (*new, HevSocks5User, node);
|
||||
|
||||
if (this->name_len < user->name_len)
|
||||
res = -1;
|
||||
else if (this->name_len > user->name_len)
|
||||
res = 1;
|
||||
else
|
||||
res = memcmp (this->name, user->name, this->name_len);
|
||||
|
||||
parent = *new;
|
||||
if (res < 0)
|
||||
new = &((*new)->left);
|
||||
else if (res > 0)
|
||||
new = &((*new)->right);
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
|
||||
hev_rbtree_node_link (&user->node, parent, new);
|
||||
hev_rbtree_insert_color (&self->tree, &user->node);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_authenticator_del (HevSocks5Authenticator *self, const char *name,
|
||||
unsigned int name_len)
|
||||
{
|
||||
HevRBTreeNode *node = self->tree.root;
|
||||
|
||||
while (node) {
|
||||
HevSocks5User *this;
|
||||
int res;
|
||||
|
||||
this = container_of (node, HevSocks5User, node);
|
||||
if (this->name_len < name_len)
|
||||
res = -1;
|
||||
else if (this->name_len > name_len)
|
||||
res = 1;
|
||||
else
|
||||
res = memcmp (this->name, name, name_len);
|
||||
|
||||
if (res < 0) {
|
||||
node = node->left;
|
||||
} else if (res > 0) {
|
||||
node = node->right;
|
||||
} else {
|
||||
hev_rbtree_erase (&self->tree, node);
|
||||
hev_object_unref (HEV_OBJECT (this));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
HevSocks5User *
|
||||
hev_socks5_authenticator_get (HevSocks5Authenticator *self, const char *name,
|
||||
unsigned int name_len)
|
||||
{
|
||||
HevRBTreeNode *node = self->tree.root;
|
||||
|
||||
while (node) {
|
||||
HevSocks5User *this;
|
||||
int res;
|
||||
|
||||
this = container_of (node, HevSocks5User, node);
|
||||
if (this->name_len < name_len)
|
||||
res = -1;
|
||||
else if (this->name_len > name_len)
|
||||
res = 1;
|
||||
else
|
||||
res = memcmp (this->name, name, name_len);
|
||||
|
||||
if (res < 0)
|
||||
node = node->left;
|
||||
else if (res > 0)
|
||||
node = node->right;
|
||||
else
|
||||
return this;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_authenticator_clear (HevSocks5Authenticator *self)
|
||||
{
|
||||
HevRBTreeNode *n;
|
||||
|
||||
while ((n = hev_rbtree_first (&self->tree))) {
|
||||
HevSocks5User *t;
|
||||
|
||||
t = container_of (n, HevSocks5User, node);
|
||||
hev_rbtree_erase (&self->tree, n);
|
||||
hev_object_unref (HEV_OBJECT (t));
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_authenticator_construct (HevSocks5Authenticator *self)
|
||||
{
|
||||
int res;
|
||||
|
||||
res = hev_object_atomic_construct (&self->base);
|
||||
if (res < 0)
|
||||
return res;
|
||||
|
||||
LOG_D ("%p socks5 authenticator construct", self);
|
||||
|
||||
HEV_OBJECT (self)->klass = HEV_SOCKS5_AUTHENTICATOR_TYPE;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
hev_socks5_authenticator_destruct (HevObject *base)
|
||||
{
|
||||
HevSocks5Authenticator *self = HEV_SOCKS5_AUTHENTICATOR (base);
|
||||
|
||||
LOG_D ("%p socks5 authenticator destruct", self);
|
||||
|
||||
hev_socks5_authenticator_clear (self);
|
||||
|
||||
HEV_OBJECT_ATOMIC_TYPE->destruct (base);
|
||||
free (base);
|
||||
}
|
||||
|
||||
HevObjectClass *
|
||||
hev_socks5_authenticator_class (void)
|
||||
{
|
||||
static HevSocks5AuthenticatorClass klass;
|
||||
HevSocks5AuthenticatorClass *kptr = &klass;
|
||||
HevObjectClass *okptr = HEV_OBJECT_CLASS (kptr);
|
||||
|
||||
if (!okptr->name) {
|
||||
memcpy (kptr, HEV_OBJECT_ATOMIC_TYPE, sizeof (HevObjectAtomicClass));
|
||||
|
||||
okptr->name = "HevSocks5Authenticator";
|
||||
okptr->destruct = hev_socks5_authenticator_destruct;
|
||||
}
|
||||
|
||||
return okptr;
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-authenticator.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2023 hev
|
||||
Description : Socks5 Authenticator
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_AUTHENTICATOR_H__
|
||||
#define __HEV_SOCKS5_AUTHENTICATOR_H__
|
||||
|
||||
#include <hev-object-atomic.h>
|
||||
|
||||
#include "hev-rbtree.h"
|
||||
#include "hev-socks5-user.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HEV_SOCKS5_AUTHENTICATOR(p) ((HevSocks5Authenticator *)p)
|
||||
#define HEV_SOCKS5_AUTHENTICATOR_CLASS(p) ((HevSocks5AuthenticatorClass *)p)
|
||||
#define HEV_SOCKS5_AUTHENTICATOR_TYPE (hev_socks5_authenticator_class ())
|
||||
|
||||
typedef struct _HevSocks5Authenticator HevSocks5Authenticator;
|
||||
typedef struct _HevSocks5AuthenticatorClass HevSocks5AuthenticatorClass;
|
||||
typedef enum _HevSocks5AuthenticatorType HevSocks5AuthenticatorType;
|
||||
|
||||
struct _HevSocks5Authenticator
|
||||
{
|
||||
HevObjectAtomic base;
|
||||
|
||||
HevRBTree tree;
|
||||
};
|
||||
|
||||
struct _HevSocks5AuthenticatorClass
|
||||
{
|
||||
HevObjectAtomicClass base;
|
||||
};
|
||||
|
||||
HevObjectClass *hev_socks5_authenticator_class (void);
|
||||
|
||||
int hev_socks5_authenticator_construct (HevSocks5Authenticator *self);
|
||||
|
||||
HevSocks5Authenticator *hev_socks5_authenticator_new (void);
|
||||
|
||||
int hev_socks5_authenticator_add (HevSocks5Authenticator *self,
|
||||
HevSocks5User *user);
|
||||
|
||||
int hev_socks5_authenticator_del (HevSocks5Authenticator *self,
|
||||
const char *name, unsigned int name_len);
|
||||
|
||||
HevSocks5User *hev_socks5_authenticator_get (HevSocks5Authenticator *self,
|
||||
const char *name,
|
||||
unsigned int name_len);
|
||||
|
||||
void hev_socks5_authenticator_clear (HevSocks5Authenticator *self);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_AUTHENTICATOR_H__ */
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-client-tcp.c
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2025 hev
|
||||
Description : Socks5 Client TCP
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <hev-memory-allocator.h>
|
||||
|
||||
#include "hev-socks5-misc-priv.h"
|
||||
#include "hev-socks5-logger-priv.h"
|
||||
|
||||
#include "hev-socks5-client-tcp.h"
|
||||
|
||||
HevSocks5ClientTCP *
|
||||
hev_socks5_client_tcp_new_name (const char *name, int port)
|
||||
{
|
||||
HevSocks5ClientTCP *self;
|
||||
HevSocks5Addr addr;
|
||||
int res;
|
||||
|
||||
self = hev_malloc0 (sizeof (HevSocks5ClientTCP));
|
||||
if (!self)
|
||||
return NULL;
|
||||
|
||||
hev_socks5_addr_from_name (&addr, name, port);
|
||||
res = hev_socks5_client_tcp_construct (self, &addr);
|
||||
if (res < 0) {
|
||||
hev_free (self);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_D ("%p socks5 client tcp new name", self);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
HevSocks5ClientTCP *
|
||||
hev_socks5_client_tcp_new_ipv4 (const void *ipv4, int port)
|
||||
{
|
||||
HevSocks5ClientTCP *self;
|
||||
HevSocks5Addr addr;
|
||||
int res;
|
||||
|
||||
self = hev_malloc0 (sizeof (HevSocks5ClientTCP));
|
||||
if (!self)
|
||||
return NULL;
|
||||
|
||||
hev_socks5_addr_from_ipv4 (&addr, ipv4, port);
|
||||
res = hev_socks5_client_tcp_construct (self, &addr);
|
||||
if (res < 0) {
|
||||
hev_free (self);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_D ("%p socks5 client tcp new ipv4", self);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
HevSocks5ClientTCP *
|
||||
hev_socks5_client_tcp_new_ipv6 (const void *ipv6, int port)
|
||||
{
|
||||
HevSocks5ClientTCP *self;
|
||||
HevSocks5Addr addr;
|
||||
int res;
|
||||
|
||||
self = hev_malloc0 (sizeof (HevSocks5ClientTCP));
|
||||
if (!self)
|
||||
return NULL;
|
||||
|
||||
hev_socks5_addr_from_ipv6 (&addr, ipv6, port);
|
||||
res = hev_socks5_client_tcp_construct (self, &addr);
|
||||
if (res < 0) {
|
||||
hev_free (self);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_D ("%p socks5 client tcp new ipv6", self);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
static HevSocks5Addr *
|
||||
hev_socks5_client_tcp_get_upstream_addr (HevSocks5Client *base)
|
||||
{
|
||||
HevSocks5ClientTCP *self = HEV_SOCKS5_CLIENT_TCP (base);
|
||||
HevSocks5Addr *addr;
|
||||
|
||||
addr = self->addr;
|
||||
self->addr = NULL;
|
||||
|
||||
return addr;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_client_tcp_set_upstream_addr (HevSocks5Client *base,
|
||||
HevSocks5Addr *addr)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_client_tcp_construct (HevSocks5ClientTCP *self,
|
||||
const HevSocks5Addr *addr)
|
||||
{
|
||||
int res;
|
||||
|
||||
res = hev_socks5_client_construct (&self->base, HEV_SOCKS5_TYPE_TCP);
|
||||
if (res < 0)
|
||||
return res;
|
||||
|
||||
LOG_D ("%p socks5 client tcp construct", self);
|
||||
|
||||
HEV_OBJECT (self)->klass = HEV_SOCKS5_CLIENT_TCP_TYPE;
|
||||
|
||||
res = hev_socks5_addr_len (addr);
|
||||
self->addr = hev_malloc (res);
|
||||
if (!self->addr)
|
||||
return -1;
|
||||
memcpy (self->addr, addr, res);
|
||||
|
||||
if (LOG_ON ()) {
|
||||
const char *str;
|
||||
char buf[272];
|
||||
|
||||
str = hev_socks5_addr_into_str (self->addr, buf, sizeof (buf));
|
||||
LOG_I ("%p socks5 client tcp -> %s", self, str);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
hev_socks5_client_tcp_destruct (HevObject *base)
|
||||
{
|
||||
HevSocks5ClientTCP *self = HEV_SOCKS5_CLIENT_TCP (base);
|
||||
|
||||
LOG_D ("%p socks5 client tcp destruct", self);
|
||||
|
||||
if (self->addr)
|
||||
hev_free (self->addr);
|
||||
|
||||
HEV_SOCKS5_CLIENT_TYPE->destruct (base);
|
||||
}
|
||||
|
||||
static void *
|
||||
hev_socks5_client_tcp_iface (HevObject *base, void *type)
|
||||
{
|
||||
HevSocks5ClientTCPClass *klass = HEV_OBJECT_GET_CLASS (base);
|
||||
|
||||
return &klass->tcp;
|
||||
}
|
||||
|
||||
HevObjectClass *
|
||||
hev_socks5_client_tcp_class (void)
|
||||
{
|
||||
static HevSocks5ClientTCPClass klass;
|
||||
HevSocks5ClientTCPClass *kptr = &klass;
|
||||
HevObjectClass *okptr = HEV_OBJECT_CLASS (kptr);
|
||||
|
||||
if (!okptr->name) {
|
||||
HevSocks5ClientClass *ckptr;
|
||||
HevSocks5TCPIface *tiptr;
|
||||
|
||||
memcpy (kptr, HEV_SOCKS5_CLIENT_TYPE, sizeof (HevSocks5ClientClass));
|
||||
|
||||
okptr->name = "HevSocks5ClientTCP";
|
||||
okptr->destruct = hev_socks5_client_tcp_destruct;
|
||||
okptr->iface = hev_socks5_client_tcp_iface;
|
||||
|
||||
ckptr = HEV_SOCKS5_CLIENT_CLASS (kptr);
|
||||
ckptr->get_upstream_addr = hev_socks5_client_tcp_get_upstream_addr;
|
||||
ckptr->set_upstream_addr = hev_socks5_client_tcp_set_upstream_addr;
|
||||
|
||||
tiptr = &kptr->tcp;
|
||||
memcpy (tiptr, HEV_SOCKS5_TCP_TYPE, sizeof (HevSocks5TCPIface));
|
||||
}
|
||||
|
||||
return okptr;
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-client-tcp.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2025 hev
|
||||
Description : Socks5 Client TCP
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_CLIENT_TCP_H__
|
||||
#define __HEV_SOCKS5_CLIENT_TCP_H__
|
||||
|
||||
#include "hev-socks5-tcp.h"
|
||||
#include "hev-socks5-proto.h"
|
||||
|
||||
#include "hev-socks5-client.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HEV_SOCKS5_CLIENT_TCP(p) ((HevSocks5ClientTCP *)p)
|
||||
#define HEV_SOCKS5_CLIENT_TCP_CLASS(p) ((HevSocks5ClientTCPClass *)p)
|
||||
#define HEV_SOCKS5_CLIENT_TCP_TYPE (hev_socks5_client_tcp_class ())
|
||||
|
||||
typedef struct _HevSocks5ClientTCP HevSocks5ClientTCP;
|
||||
typedef struct _HevSocks5ClientTCPClass HevSocks5ClientTCPClass;
|
||||
|
||||
struct _HevSocks5ClientTCP
|
||||
{
|
||||
HevSocks5Client base;
|
||||
|
||||
HevSocks5Addr *addr;
|
||||
};
|
||||
|
||||
struct _HevSocks5ClientTCPClass
|
||||
{
|
||||
HevSocks5ClientClass base;
|
||||
|
||||
HevSocks5TCPIface tcp;
|
||||
};
|
||||
|
||||
HevObjectClass *hev_socks5_client_tcp_class (void);
|
||||
|
||||
int hev_socks5_client_tcp_construct (HevSocks5ClientTCP *self,
|
||||
const HevSocks5Addr *addr);
|
||||
|
||||
HevSocks5ClientTCP *hev_socks5_client_tcp_new_name (const char *name, int port);
|
||||
HevSocks5ClientTCP *hev_socks5_client_tcp_new_ipv4 (const void *ipv4, int port);
|
||||
HevSocks5ClientTCP *hev_socks5_client_tcp_new_ipv6 (const void *ipv6, int port);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_CLIENT_TCP_H__ */
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-client-udp.c
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2025 hev
|
||||
Description : Socks5 Client UDP
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <hev-task.h>
|
||||
#include <hev-task-io.h>
|
||||
#include <hev-task-io-socket.h>
|
||||
#include <hev-memory-allocator.h>
|
||||
|
||||
#include "hev-socks5-misc-priv.h"
|
||||
#include "hev-socks5-logger-priv.h"
|
||||
|
||||
#include "hev-socks5-client-udp.h"
|
||||
|
||||
#define task_io_yielder hev_socks5_task_io_yielder
|
||||
|
||||
HevSocks5ClientUDP *
|
||||
hev_socks5_client_udp_new (HevSocks5Type type)
|
||||
{
|
||||
HevSocks5ClientUDP *self;
|
||||
int res;
|
||||
|
||||
self = hev_malloc0 (sizeof (HevSocks5ClientUDP));
|
||||
if (!self)
|
||||
return NULL;
|
||||
|
||||
res = hev_socks5_client_udp_construct (self, type);
|
||||
if (res < 0) {
|
||||
hev_free (self);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_D ("%p socks5 client udp new", self);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
static HevSocks5Addr *
|
||||
hev_socks5_client_udp_get_upstream_addr (HevSocks5Client *base)
|
||||
{
|
||||
HevSocks5AddrFamily family;
|
||||
HevSocks5Addr *addr;
|
||||
|
||||
family = hev_socks5_get_addr_family (HEV_SOCKS5 (base));
|
||||
|
||||
switch (family) {
|
||||
case HEV_SOCKS5_ADDR_FAMILY_IPV4:
|
||||
addr = hev_malloc0 (7);
|
||||
if (addr)
|
||||
addr->atype = HEV_SOCKS5_ADDR_TYPE_IPV4;
|
||||
break;
|
||||
case HEV_SOCKS5_ADDR_FAMILY_IPV6:
|
||||
addr = hev_malloc0 (19);
|
||||
if (addr)
|
||||
addr->atype = HEV_SOCKS5_ADDR_TYPE_IPV6;
|
||||
break;
|
||||
default:
|
||||
addr = NULL;
|
||||
}
|
||||
|
||||
return addr;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_client_udp_set_upstream_addr (HevSocks5Client *base,
|
||||
HevSocks5Addr *addr)
|
||||
{
|
||||
HevSocks5ClientUDP *self = HEV_SOCKS5_CLIENT_UDP (base);
|
||||
struct sockaddr_in6 saddr;
|
||||
struct sockaddr *sadp;
|
||||
HevSocks5Class *klass;
|
||||
int addr_family;
|
||||
int res;
|
||||
int fd;
|
||||
|
||||
if (HEV_SOCKS5 (base)->type != HEV_SOCKS5_TYPE_UDP_IN_UDP)
|
||||
return 0;
|
||||
|
||||
memset (&saddr, 0, sizeof (saddr));
|
||||
addr_family = hev_socks5_get_addr_family (HEV_SOCKS5 (self));
|
||||
res = hev_socks5_addr_into_sockaddr6 (addr, &saddr, &addr_family);
|
||||
if (res < 0) {
|
||||
LOG_W ("%p socks5 client udp addr", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
fd = hev_socks5_socket (SOCK_DGRAM);
|
||||
if (fd < 0) {
|
||||
LOG_E ("%p socks5 client udp socket", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
sadp = (struct sockaddr *)&saddr;
|
||||
klass = HEV_OBJECT_GET_CLASS (self);
|
||||
res = klass->binder (HEV_SOCKS5 (self), fd, sadp);
|
||||
if (res < 0) {
|
||||
LOG_W ("%p socks5 client udp bind", self);
|
||||
hev_task_del_fd (hev_task_self (), fd);
|
||||
close (fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = hev_task_io_socket_connect (fd, sadp, sizeof (saddr), task_io_yielder,
|
||||
self);
|
||||
if (res < 0) {
|
||||
LOG_I ("%p socks5 client udp connect", self);
|
||||
hev_task_del_fd (hev_task_self (), fd);
|
||||
close (fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
HEV_SOCKS5 (self)->udp_associated = 1;
|
||||
self->fd = fd;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_client_udp_get_fd (HevSocks5UDP *self)
|
||||
{
|
||||
int fd;
|
||||
|
||||
switch (HEV_SOCKS5 (self)->type) {
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_TCP:
|
||||
fd = HEV_SOCKS5 (self)->fd;
|
||||
break;
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_UDP:
|
||||
fd = HEV_SOCKS5_CLIENT_UDP (self)->fd;
|
||||
break;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_client_udp_construct (HevSocks5ClientUDP *self, HevSocks5Type type)
|
||||
{
|
||||
int res;
|
||||
|
||||
res = hev_socks5_client_construct (&self->base, type);
|
||||
if (res < 0)
|
||||
return res;
|
||||
|
||||
LOG_I ("%p socks5 client udp construct", self);
|
||||
|
||||
HEV_OBJECT (self)->klass = HEV_SOCKS5_CLIENT_UDP_TYPE;
|
||||
|
||||
self->fd = -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
hev_socks5_client_udp_destruct (HevObject *base)
|
||||
{
|
||||
HevSocks5ClientUDP *self = HEV_SOCKS5_CLIENT_UDP (base);
|
||||
|
||||
LOG_D ("%p socks5 client udp destruct", self);
|
||||
|
||||
if (self->fd >= 0) {
|
||||
hev_task_del_fd (hev_task_self (), self->fd);
|
||||
close (self->fd);
|
||||
}
|
||||
|
||||
HEV_SOCKS5_CLIENT_TYPE->destruct (base);
|
||||
}
|
||||
|
||||
static void *
|
||||
hev_socks5_client_udp_iface (HevObject *base, void *type)
|
||||
{
|
||||
HevSocks5ClientUDPClass *klass = HEV_OBJECT_GET_CLASS (base);
|
||||
|
||||
return &klass->udp;
|
||||
}
|
||||
|
||||
HevObjectClass *
|
||||
hev_socks5_client_udp_class (void)
|
||||
{
|
||||
static HevSocks5ClientUDPClass klass;
|
||||
HevSocks5ClientUDPClass *kptr = &klass;
|
||||
HevObjectClass *okptr = HEV_OBJECT_CLASS (kptr);
|
||||
|
||||
if (!okptr->name) {
|
||||
HevSocks5ClientClass *ckptr;
|
||||
HevSocks5UDPIface *uiptr;
|
||||
|
||||
memcpy (kptr, HEV_SOCKS5_CLIENT_TYPE, sizeof (HevSocks5ClientClass));
|
||||
|
||||
okptr->name = "HevSocks5ClientUDP";
|
||||
okptr->destruct = hev_socks5_client_udp_destruct;
|
||||
okptr->iface = hev_socks5_client_udp_iface;
|
||||
|
||||
ckptr = HEV_SOCKS5_CLIENT_CLASS (kptr);
|
||||
ckptr->get_upstream_addr = hev_socks5_client_udp_get_upstream_addr;
|
||||
ckptr->set_upstream_addr = hev_socks5_client_udp_set_upstream_addr;
|
||||
|
||||
uiptr = &kptr->udp;
|
||||
memcpy (uiptr, HEV_SOCKS5_UDP_TYPE, sizeof (HevSocks5UDPIface));
|
||||
uiptr->get_fd = hev_socks5_client_udp_get_fd;
|
||||
}
|
||||
|
||||
return okptr;
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-client-udp.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2023 hev
|
||||
Description : Socks5 Client UDP
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_CLIENT_UDP_H__
|
||||
#define __HEV_SOCKS5_CLIENT_UDP_H__
|
||||
|
||||
#include "hev-socks5-udp.h"
|
||||
|
||||
#include "hev-socks5-client.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HEV_SOCKS5_CLIENT_UDP(p) ((HevSocks5ClientUDP *)p)
|
||||
#define HEV_SOCKS5_CLIENT_UDP_CLASS(p) ((HevSocks5ClientUDPClass *)p)
|
||||
#define HEV_SOCKS5_CLIENT_UDP_TYPE (hev_socks5_client_udp_class ())
|
||||
|
||||
typedef struct _HevSocks5ClientUDP HevSocks5ClientUDP;
|
||||
typedef struct _HevSocks5ClientUDPClass HevSocks5ClientUDPClass;
|
||||
|
||||
struct _HevSocks5ClientUDP
|
||||
{
|
||||
HevSocks5Client base;
|
||||
|
||||
int fd;
|
||||
};
|
||||
|
||||
struct _HevSocks5ClientUDPClass
|
||||
{
|
||||
HevSocks5ClientClass base;
|
||||
|
||||
HevSocks5UDPIface udp;
|
||||
};
|
||||
|
||||
HevObjectClass *hev_socks5_client_udp_class (void);
|
||||
|
||||
int hev_socks5_client_udp_construct (HevSocks5ClientUDP *self,
|
||||
HevSocks5Type type);
|
||||
|
||||
HevSocks5ClientUDP *hev_socks5_client_udp_new (HevSocks5Type type);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_CLIENT_UDP_H__ */
|
||||
|
|
@ -0,0 +1,475 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-client.c
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2025 hev
|
||||
Description : Socks5 Client
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <hev-task.h>
|
||||
#include <hev-task-io.h>
|
||||
#include <hev-task-io-socket.h>
|
||||
#include <hev-memory-allocator.h>
|
||||
|
||||
#include "hev-socks5-misc-priv.h"
|
||||
#include "hev-socks5-logger-priv.h"
|
||||
|
||||
#include "hev-socks5-client.h"
|
||||
|
||||
#define task_io_yielder hev_socks5_task_io_yielder
|
||||
|
||||
static int
|
||||
hev_socks5_client_write_auth_methods (HevSocks5Client *self)
|
||||
{
|
||||
HevSocks5Auth auth;
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 client write auth methods", self);
|
||||
|
||||
auth.ver = HEV_SOCKS5_VERSION_5;
|
||||
auth.method_len = 1;
|
||||
if (!self->auth.user || !self->auth.pass)
|
||||
auth.methods[0] = HEV_SOCKS5_AUTH_METHOD_NONE;
|
||||
else
|
||||
auth.methods[0] = HEV_SOCKS5_AUTH_METHOD_USER;
|
||||
|
||||
res = hev_task_io_socket_send (HEV_SOCKS5 (self)->fd, &auth, 3, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (res <= 0) {
|
||||
LOG_I ("%p socks5 client write auth methods", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_client_write_auth_creds (HevSocks5Client *self)
|
||||
{
|
||||
struct msghdr mh = { 0 };
|
||||
struct iovec iov[4];
|
||||
unsigned char ub[3];
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 client write auth creds", self);
|
||||
|
||||
if (!self->auth.user || !self->auth.pass)
|
||||
return 0;
|
||||
|
||||
ub[0] = HEV_SOCKS5_AUTH_VERSION_1;
|
||||
ub[1] = strlen (self->auth.user);
|
||||
ub[2] = strlen (self->auth.pass);
|
||||
iov[0].iov_base = &ub[0];
|
||||
iov[0].iov_len = 2;
|
||||
iov[1].iov_base = (void *)self->auth.user;
|
||||
iov[1].iov_len = ub[1];
|
||||
iov[2].iov_base = &ub[2];
|
||||
iov[2].iov_len = 1;
|
||||
iov[3].iov_base = (void *)self->auth.pass;
|
||||
iov[3].iov_len = ub[2];
|
||||
|
||||
mh.msg_iov = iov;
|
||||
mh.msg_iovlen = 4;
|
||||
res = hev_task_io_socket_sendmsg (HEV_SOCKS5 (self)->fd, &mh, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (res <= 0) {
|
||||
LOG_I ("%p socks5 client write auth creds", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_client_write_request (HevSocks5Client *self)
|
||||
{
|
||||
HevSocks5ClientClass *klass;
|
||||
struct msghdr mh = { 0 };
|
||||
struct iovec iov[2];
|
||||
HevSocks5Addr *addr;
|
||||
HevSocks5ReqRes req;
|
||||
int addrlen;
|
||||
int ret;
|
||||
|
||||
LOG_D ("%p socks5 client write request", self);
|
||||
|
||||
req.ver = HEV_SOCKS5_VERSION_5;
|
||||
req.rsv = 0;
|
||||
|
||||
switch (HEV_SOCKS5 (self)->type) {
|
||||
case HEV_SOCKS5_TYPE_TCP:
|
||||
req.cmd = HEV_SOCKS5_REQ_CMD_CONNECT;
|
||||
break;
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_TCP:
|
||||
req.cmd = HEV_SOCKS5_REQ_CMD_FWD_UDP;
|
||||
break;
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_UDP:
|
||||
req.cmd = HEV_SOCKS5_REQ_CMD_UDP_ASC;
|
||||
break;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
||||
iov[0].iov_base = &req;
|
||||
iov[0].iov_len = 3;
|
||||
|
||||
klass = HEV_OBJECT_GET_CLASS (self);
|
||||
addr = klass->get_upstream_addr (self);
|
||||
|
||||
switch (addr->atype) {
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV4:
|
||||
addrlen = 7;
|
||||
break;
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV6:
|
||||
addrlen = 19;
|
||||
break;
|
||||
case HEV_SOCKS5_ADDR_TYPE_NAME:
|
||||
addrlen = 4 + addr->domain.len;
|
||||
break;
|
||||
default:
|
||||
LOG_I ("%p socks5 client req.atype %u", self, addr->atype);
|
||||
return -1;
|
||||
}
|
||||
|
||||
iov[1].iov_base = addr;
|
||||
iov[1].iov_len = addrlen;
|
||||
|
||||
mh.msg_iov = iov;
|
||||
mh.msg_iovlen = 2;
|
||||
ret = hev_task_io_socket_sendmsg (HEV_SOCKS5 (self)->fd, &mh, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (ret <= 0) {
|
||||
LOG_I ("%p socks5 client write request", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
hev_free (addr);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_client_read_auth_method (HevSocks5Client *self)
|
||||
{
|
||||
HevSocks5Auth auth;
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 client read auth method", self);
|
||||
|
||||
res = hev_task_io_socket_recv (HEV_SOCKS5 (self)->fd, &auth, 2, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (res != 2) {
|
||||
LOG_I ("%p socks5 client read auth", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (auth.ver != HEV_SOCKS5_VERSION_5) {
|
||||
LOG_I ("%p socks5 client auth.ver %u", self, auth.ver);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return auth.method;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_client_read_auth_creds (HevSocks5Client *self)
|
||||
{
|
||||
HevSocks5ReqRes res;
|
||||
int ret;
|
||||
|
||||
LOG_D ("%p socks5 client read auth creds", self);
|
||||
|
||||
ret = hev_task_io_socket_recv (HEV_SOCKS5 (self)->fd, &res, 2, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (ret != 2) {
|
||||
LOG_I ("%p socks5 client read auth creds", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (res.ver != HEV_SOCKS5_AUTH_VERSION_1) {
|
||||
LOG_I ("%p socks5 client auth.res.ver %u", self, res.ver);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (res.rep != HEV_SOCKS5_RES_REP_SUCC) {
|
||||
LOG_I ("%p socks5 client auth.res.rep %u", self, res.rep);
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOG_D ("%p socks5 client auth done", self);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_client_read_response (HevSocks5Client *self)
|
||||
{
|
||||
HevSocks5ClientClass *klass;
|
||||
HevSocks5ReqRes res;
|
||||
int addrlen;
|
||||
int ret;
|
||||
|
||||
LOG_D ("%p socks5 client read response", self);
|
||||
|
||||
ret = hev_task_io_socket_recv (HEV_SOCKS5 (self)->fd, &res, 4, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (ret != 4) {
|
||||
LOG_I ("%p socks5 client read response", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (res.ver != HEV_SOCKS5_VERSION_5) {
|
||||
LOG_I ("%p socks5 client res.ver %u", self, res.ver);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (res.rep != HEV_SOCKS5_RES_REP_SUCC) {
|
||||
LOG_I ("%p socks5 client res.rep %u", self, res.rep);
|
||||
return -1;
|
||||
}
|
||||
|
||||
switch (res.addr.atype) {
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV4:
|
||||
addrlen = 6;
|
||||
break;
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV6:
|
||||
addrlen = 18;
|
||||
break;
|
||||
default:
|
||||
LOG_I ("%p socks5 client res.atype %u", self, res.addr.atype);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = hev_task_io_socket_recv (HEV_SOCKS5 (self)->fd, &res.addr.ipv4,
|
||||
addrlen, MSG_WAITALL, task_io_yielder, self);
|
||||
if (ret != addrlen) {
|
||||
LOG_I ("%p socks5 client read addr", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
klass = HEV_OBJECT_GET_CLASS (self);
|
||||
ret = klass->set_upstream_addr (self, &res.addr);
|
||||
if (ret < 0) {
|
||||
LOG_W ("%p socks5 client set upstream addr", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_client_connect (HevSocks5Client *self, const char *addr, int port)
|
||||
{
|
||||
HevSocks5Class *klass;
|
||||
struct sockaddr_in6 saddr;
|
||||
struct sockaddr *sap;
|
||||
int addr_family;
|
||||
int timeout;
|
||||
int fd, res;
|
||||
|
||||
LOG_D ("%p socks5 client connect [%s]:%d", self, addr, port);
|
||||
|
||||
timeout = hev_socks5_get_connect_timeout ();
|
||||
hev_socks5_set_timeout (HEV_SOCKS5 (self), timeout);
|
||||
|
||||
memset (&saddr, 0, sizeof (saddr));
|
||||
addr_family = hev_socks5_get_addr_family (HEV_SOCKS5 (self));
|
||||
res = hev_socks5_name_into_sockaddr6 (addr, port, &saddr, &addr_family);
|
||||
if (res < 0) {
|
||||
LOG_I ("%p socks5 client resolve [%s]:%d", self, addr, port);
|
||||
return -1;
|
||||
}
|
||||
|
||||
fd = hev_socks5_socket (SOCK_STREAM);
|
||||
if (fd < 0) {
|
||||
LOG_E ("%p socks5 client socket", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
sap = (struct sockaddr *)&saddr;
|
||||
klass = HEV_OBJECT_GET_CLASS (self);
|
||||
res = klass->binder (HEV_SOCKS5 (self), fd, sap);
|
||||
if (res < 0) {
|
||||
LOG_W ("%p socks5 client bind", self);
|
||||
hev_task_del_fd (hev_task_self (), fd);
|
||||
close (fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = hev_task_io_socket_connect (fd, sap, sizeof (saddr), task_io_yielder,
|
||||
self);
|
||||
if (res < 0) {
|
||||
LOG_I ("%p socks5 client connect", self);
|
||||
hev_task_del_fd (hev_task_self (), fd);
|
||||
close (fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
HEV_SOCKS5 (self)->fd = fd;
|
||||
hev_socks5_set_addr_family (HEV_SOCKS5 (self), addr_family);
|
||||
LOG_D ("%p socks5 client connect server fd %d", self, fd);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_client_handshake_standard (HevSocks5Client *self)
|
||||
{
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 client handshake standard", self);
|
||||
|
||||
res = hev_socks5_client_write_auth_methods (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
res = hev_socks5_client_read_auth_method (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
if (res == HEV_SOCKS5_AUTH_METHOD_USER) {
|
||||
res = hev_socks5_client_write_auth_creds (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
res = hev_socks5_client_read_auth_creds (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
} else if (res != HEV_SOCKS5_AUTH_METHOD_NONE) {
|
||||
LOG_I ("%p socks5 client auth method %d", self, res);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = hev_socks5_client_write_request (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
res = hev_socks5_client_read_response (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_client_handshake_pipeline (HevSocks5Client *self)
|
||||
{
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 client handshake pipeline", self);
|
||||
|
||||
res = hev_socks5_client_write_auth_methods (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
res = hev_socks5_client_write_auth_creds (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
res = hev_socks5_client_write_request (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
res = hev_socks5_client_read_auth_method (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
if (res == HEV_SOCKS5_AUTH_METHOD_USER) {
|
||||
res = hev_socks5_client_read_auth_creds (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
} else if (res != HEV_SOCKS5_AUTH_METHOD_NONE) {
|
||||
LOG_I ("%p socks5 client auth method %d", self, res);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = hev_socks5_client_read_response (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_client_handshake (HevSocks5Client *self, int pipeline)
|
||||
{
|
||||
int timeout;
|
||||
int res;
|
||||
|
||||
timeout = hev_socks5_get_tcp_timeout ();
|
||||
hev_socks5_set_timeout (HEV_SOCKS5 (self), timeout);
|
||||
|
||||
if (pipeline)
|
||||
res = hev_socks5_client_handshake_pipeline (self);
|
||||
else
|
||||
res = hev_socks5_client_handshake_standard (self);
|
||||
|
||||
switch (HEV_SOCKS5 (self)->type) {
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_TCP:
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_UDP:
|
||||
timeout = hev_socks5_get_udp_timeout ();
|
||||
hev_socks5_set_timeout (HEV_SOCKS5 (self), timeout);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_client_set_auth (HevSocks5Client *self, const char *user,
|
||||
const char *pass)
|
||||
{
|
||||
LOG_D ("%p socks5 client set auth", self);
|
||||
|
||||
self->auth.user = user;
|
||||
self->auth.pass = pass;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_client_construct (HevSocks5Client *self, HevSocks5Type type)
|
||||
{
|
||||
int res;
|
||||
|
||||
res = hev_socks5_construct (&self->base, type);
|
||||
if (res < 0)
|
||||
return res;
|
||||
|
||||
LOG_D ("%p socks5 client construct", self);
|
||||
|
||||
HEV_OBJECT (self)->klass = HEV_SOCKS5_CLIENT_TYPE;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
hev_socks5_client_destruct (HevObject *base)
|
||||
{
|
||||
HevSocks5Client *self = HEV_SOCKS5_CLIENT (base);
|
||||
|
||||
LOG_D ("%p socks5 client destruct", self);
|
||||
|
||||
HEV_SOCKS5_TYPE->destruct (base);
|
||||
}
|
||||
|
||||
HevObjectClass *
|
||||
hev_socks5_client_class (void)
|
||||
{
|
||||
static HevSocks5ClientClass klass;
|
||||
HevSocks5ClientClass *kptr = &klass;
|
||||
HevObjectClass *okptr = HEV_OBJECT_CLASS (kptr);
|
||||
|
||||
if (!okptr->name) {
|
||||
memcpy (kptr, HEV_SOCKS5_TYPE, sizeof (HevSocks5Class));
|
||||
|
||||
okptr->name = "HevSocks5Client";
|
||||
okptr->destruct = hev_socks5_client_destruct;
|
||||
}
|
||||
|
||||
return okptr;
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-client.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2024 hev
|
||||
Description : Socks5 Client
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_CLIENT_H__
|
||||
#define __HEV_SOCKS5_CLIENT_H__
|
||||
|
||||
#include "hev-socks5.h"
|
||||
#include "hev-socks5-proto.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HEV_SOCKS5_CLIENT(p) ((HevSocks5Client *)p)
|
||||
#define HEV_SOCKS5_CLIENT_CLASS(p) ((HevSocks5ClientClass *)p)
|
||||
#define HEV_SOCKS5_CLIENT_TYPE (hev_socks5_client_class ())
|
||||
|
||||
typedef struct _HevSocks5Client HevSocks5Client;
|
||||
typedef struct _HevSocks5ClientClass HevSocks5ClientClass;
|
||||
|
||||
struct _HevSocks5Client
|
||||
{
|
||||
HevSocks5 base;
|
||||
|
||||
struct
|
||||
{
|
||||
const char *user;
|
||||
const char *pass;
|
||||
} auth;
|
||||
};
|
||||
|
||||
struct _HevSocks5ClientClass
|
||||
{
|
||||
HevSocks5Class base;
|
||||
|
||||
HevSocks5Addr *(*get_upstream_addr) (HevSocks5Client *self);
|
||||
int (*set_upstream_addr) (HevSocks5Client *self, HevSocks5Addr *addr);
|
||||
};
|
||||
|
||||
HevObjectClass *hev_socks5_client_class (void);
|
||||
|
||||
int hev_socks5_client_construct (HevSocks5Client *self, HevSocks5Type type);
|
||||
|
||||
int hev_socks5_client_connect (HevSocks5Client *self, const char *addr,
|
||||
int port);
|
||||
|
||||
int hev_socks5_client_handshake (HevSocks5Client *self, int pipeline);
|
||||
|
||||
void hev_socks5_client_set_auth (HevSocks5Client *self, const char *user,
|
||||
const char *pass);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_CLIENT_H__ */
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-logger-priv.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 hev
|
||||
Description : Socks5 Logger Private
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_LOGGER_PRIV_H__
|
||||
#define __HEV_SOCKS5_LOGGER_PRIV_H__
|
||||
|
||||
#include "hev-socks5-logger.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define LOG_D(fmt...) hev_socks5_logger_log (HEV_SOCKS5_LOGGER_DEBUG, fmt)
|
||||
#define LOG_I(fmt...) hev_socks5_logger_log (HEV_SOCKS5_LOGGER_INFO, fmt)
|
||||
#define LOG_W(fmt...) hev_socks5_logger_log (HEV_SOCKS5_LOGGER_WARN, fmt)
|
||||
#define LOG_E(fmt...) hev_socks5_logger_log (HEV_SOCKS5_LOGGER_ERROR, fmt)
|
||||
|
||||
#define LOG_ON() hev_socks5_logger_enabled (HEV_SOCKS5_LOGGER_UNSET)
|
||||
#define LOG_ON_D() hev_socks5_logger_enabled (HEV_SOCKS5_LOGGER_DEBUG)
|
||||
#define LOG_ON_I() hev_socks5_logger_enabled (HEV_SOCKS5_LOGGER_INFO)
|
||||
#define LOG_ON_W() hev_socks5_logger_enabled (HEV_SOCKS5_LOGGER_WARN)
|
||||
#define LOG_ON_E() hev_socks5_logger_enabled (HEV_SOCKS5_LOGGER_ERROR)
|
||||
|
||||
int hev_socks5_logger_enabled (HevSocks5LoggerLevel level);
|
||||
void hev_socks5_logger_log (HevSocks5LoggerLevel level, const char *fmt, ...);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_LOGGER_PRIV_H__ */
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-logger.c
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 hev
|
||||
Description : Socks5 Logger
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#include <time.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/uio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "hev-socks5-logger.h"
|
||||
#include "hev-socks5-logger-priv.h"
|
||||
|
||||
static int fd = -1;
|
||||
static HevSocks5LoggerLevel req_level;
|
||||
|
||||
int
|
||||
hev_socks5_logger_init (HevSocks5LoggerLevel level, const char *path)
|
||||
{
|
||||
req_level = level;
|
||||
|
||||
if (0 == strcmp (path, "stdout"))
|
||||
fd = dup (1);
|
||||
else if (0 == strcmp (path, "stderr"))
|
||||
fd = dup (2);
|
||||
else
|
||||
fd = open (path, O_WRONLY | O_APPEND | O_CREAT, 0640);
|
||||
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_logger_fini (void)
|
||||
{
|
||||
close (fd);
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_logger_enabled (HevSocks5LoggerLevel level)
|
||||
{
|
||||
if (level >= req_level && fd >= 0)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_logger_log (HevSocks5LoggerLevel level, const char *fmt, ...)
|
||||
{
|
||||
struct iovec iov[4];
|
||||
const char *ts_fmt;
|
||||
char msg[1024];
|
||||
struct tm *ti;
|
||||
char ts[32];
|
||||
time_t now;
|
||||
va_list ap;
|
||||
int len;
|
||||
|
||||
if (level < req_level || fd < 0)
|
||||
return;
|
||||
|
||||
time (&now);
|
||||
ti = localtime (&now);
|
||||
|
||||
ts_fmt = "[%04u-%02u-%02u %02u:%02u:%02u] ";
|
||||
len = snprintf (ts, sizeof (ts), ts_fmt, 1900 + ti->tm_year, 1 + ti->tm_mon,
|
||||
ti->tm_mday, ti->tm_hour, ti->tm_min, ti->tm_sec);
|
||||
|
||||
iov[0].iov_base = ts;
|
||||
iov[0].iov_len = len;
|
||||
|
||||
switch (level) {
|
||||
case HEV_SOCKS5_LOGGER_DEBUG:
|
||||
iov[1].iov_base = "[D] ";
|
||||
break;
|
||||
case HEV_SOCKS5_LOGGER_INFO:
|
||||
iov[1].iov_base = "[I] ";
|
||||
break;
|
||||
case HEV_SOCKS5_LOGGER_WARN:
|
||||
iov[1].iov_base = "[W] ";
|
||||
break;
|
||||
case HEV_SOCKS5_LOGGER_ERROR:
|
||||
iov[1].iov_base = "[E] ";
|
||||
break;
|
||||
case HEV_SOCKS5_LOGGER_UNSET:
|
||||
iov[1].iov_base = "[?] ";
|
||||
break;
|
||||
}
|
||||
iov[1].iov_len = 4;
|
||||
|
||||
va_start (ap, fmt);
|
||||
iov[2].iov_base = msg;
|
||||
iov[2].iov_len = vsnprintf (msg, 1024, fmt, ap);
|
||||
va_end (ap);
|
||||
|
||||
iov[3].iov_base = "\n";
|
||||
iov[3].iov_len = 1;
|
||||
|
||||
if (writev (fd, iov, 4)) {
|
||||
/* ignore return value */
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-logger.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 hev
|
||||
Description : Socks5 Logger
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_LOGGER_H__
|
||||
#define __HEV_SOCKS5_LOGGER_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum _HevSocks5LoggerLevel HevSocks5LoggerLevel;
|
||||
|
||||
enum _HevSocks5LoggerLevel
|
||||
{
|
||||
HEV_SOCKS5_LOGGER_DEBUG,
|
||||
HEV_SOCKS5_LOGGER_INFO,
|
||||
HEV_SOCKS5_LOGGER_WARN,
|
||||
HEV_SOCKS5_LOGGER_ERROR,
|
||||
HEV_SOCKS5_LOGGER_UNSET,
|
||||
};
|
||||
|
||||
int hev_socks5_logger_init (HevSocks5LoggerLevel level, const char *path);
|
||||
void hev_socks5_logger_fini (void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_LOGGER_H__ */
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-misc-priv.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2025 hev
|
||||
Description : Socks5 Misc Private
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_MISC_PRIV_H__
|
||||
#define __HEV_SOCKS5_MISC_PRIV_H__
|
||||
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include <hev-task.h>
|
||||
#include <hev-task-io.h>
|
||||
|
||||
#include "hev-socks5-misc.h"
|
||||
#include "hev-socks5-proto.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int hev_socks5_socket (int type);
|
||||
|
||||
const char *hev_socks5_addr_into_str (const HevSocks5Addr *addr, char *buf,
|
||||
int len);
|
||||
|
||||
int hev_socks5_get_connect_timeout (void);
|
||||
int hev_socks5_get_tcp_timeout (void);
|
||||
int hev_socks5_get_udp_timeout (void);
|
||||
|
||||
int hev_socks5_get_task_stack_size (void);
|
||||
int hev_socks5_get_udp_copy_buffer_nums (void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_MISC_PRIV_H__ */
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-misc.c
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2025 hev
|
||||
Description : Socks5 Misc
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include <hev-task.h>
|
||||
#include <hev-task-io.h>
|
||||
#include <hev-task-io-socket.h>
|
||||
#include <hev-task-dns.h>
|
||||
#include <hev-memory-allocator.h>
|
||||
|
||||
#include "hev-socks5.h"
|
||||
#include "hev-socks5-logger-priv.h"
|
||||
|
||||
#include "hev-socks5-misc.h"
|
||||
#include "hev-socks5-misc-priv.h"
|
||||
|
||||
static int connect_timeout = 10000;
|
||||
static int tcp_timeout = 300000;
|
||||
static int udp_timeout = 60000;
|
||||
|
||||
static int task_stack_size = 8192;
|
||||
static int udp_recv_buffer_size = 512 * 1024;
|
||||
static int udp_copy_buffer_nums = 10;
|
||||
|
||||
int
|
||||
hev_socks5_task_io_yielder (HevTaskYieldType type, void *data)
|
||||
{
|
||||
HevSocks5 *self = data;
|
||||
|
||||
if (type == HEV_TASK_YIELD) {
|
||||
hev_task_yield (HEV_TASK_YIELD);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (self->timeout < 0) {
|
||||
hev_task_yield (HEV_TASK_WAITIO);
|
||||
} else {
|
||||
int timeout = self->timeout;
|
||||
timeout = hev_task_sleep (timeout);
|
||||
if (timeout <= 0) {
|
||||
LOG_I ("%p io timeout", self);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_socket (int type)
|
||||
{
|
||||
HevTask *task = hev_task_self ();
|
||||
int fd, res, zero = 0;
|
||||
|
||||
fd = hev_task_io_socket_socket (AF_INET6, type, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
|
||||
res = setsockopt (fd, IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof (zero));
|
||||
if (res < 0) {
|
||||
close (fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = hev_task_add_fd (task, fd, POLLIN | POLLOUT);
|
||||
if (res < 0)
|
||||
hev_task_mod_fd (task, fd, POLLIN | POLLOUT);
|
||||
|
||||
if (type == SOCK_DGRAM) {
|
||||
res = udp_recv_buffer_size;
|
||||
setsockopt (fd, SOL_SOCKET, SO_RCVBUF, &res, sizeof (res));
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
const char *
|
||||
hev_socks5_addr_into_str (const HevSocks5Addr *addr, char *buf, int len)
|
||||
{
|
||||
const char *res = buf;
|
||||
uint16_t port;
|
||||
char sa[256];
|
||||
|
||||
switch (addr->atype) {
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV4:
|
||||
port = ntohs (addr->ipv4.port);
|
||||
inet_ntop (AF_INET, addr->ipv4.addr, sa, sizeof (sa));
|
||||
break;
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV6:
|
||||
port = ntohs (addr->ipv6.port);
|
||||
inet_ntop (AF_INET6, addr->ipv6.addr, sa, sizeof (sa));
|
||||
break;
|
||||
case HEV_SOCKS5_ADDR_TYPE_NAME:
|
||||
memcpy (sa, addr->domain.addr, addr->domain.len);
|
||||
sa[addr->domain.len] = '\0';
|
||||
memcpy (&port, addr->domain.addr + addr->domain.len, 2);
|
||||
port = ntohs (port);
|
||||
break;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
snprintf (buf, len, "[%s]:%u", sa, port);
|
||||
return res;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_addr_len (const HevSocks5Addr *addr)
|
||||
{
|
||||
switch (addr->atype) {
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV4:
|
||||
return 7;
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV6:
|
||||
return 19;
|
||||
case HEV_SOCKS5_ADDR_TYPE_NAME:
|
||||
return 4 + addr->domain.len;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_addr_from_name (HevSocks5Addr *addr, const char *name, int _port)
|
||||
{
|
||||
uint16_t port = _port;
|
||||
addr->atype = HEV_SOCKS5_ADDR_TYPE_NAME;
|
||||
strncpy ((char *)addr->domain.addr, name, 256);
|
||||
addr->domain.len = strlen ((char *)addr->domain.addr);
|
||||
memcpy ((char *)addr->domain.addr + addr->domain.len, &port, 2);
|
||||
return 4 + addr->domain.len;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_addr_from_ipv4 (HevSocks5Addr *addr, const void *ipv4, int port)
|
||||
{
|
||||
addr->atype = HEV_SOCKS5_ADDR_TYPE_IPV4;
|
||||
memcpy (addr->ipv4.addr, ipv4, sizeof (addr->ipv4.addr));
|
||||
addr->ipv4.port = port;
|
||||
return 7;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_addr_from_ipv6 (HevSocks5Addr *addr, const void *ipv6, int port)
|
||||
{
|
||||
addr->atype = HEV_SOCKS5_ADDR_TYPE_IPV6;
|
||||
memcpy (addr->ipv6.addr, ipv6, sizeof (addr->ipv6.addr));
|
||||
addr->ipv6.port = port;
|
||||
return 19;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_addr_from_sockaddr6 (HevSocks5Addr *addr, struct sockaddr_in6 *saddr)
|
||||
{
|
||||
if (IN6_IS_ADDR_V4MAPPED (&saddr->sin6_addr)) {
|
||||
addr->atype = HEV_SOCKS5_ADDR_TYPE_IPV4;
|
||||
addr->ipv4.port = saddr->sin6_port;
|
||||
memcpy (addr->ipv4.addr, &saddr->sin6_addr.s6_addr[12], 4);
|
||||
return 7;
|
||||
}
|
||||
|
||||
addr->atype = HEV_SOCKS5_ADDR_TYPE_IPV6;
|
||||
addr->ipv6.port = saddr->sin6_port;
|
||||
memcpy (addr->ipv6.addr, &saddr->sin6_addr, 16);
|
||||
return 19;
|
||||
}
|
||||
|
||||
static void
|
||||
hev_socks5_ipv4_into_sockaddr6 (const HevSocks5Addr *addr,
|
||||
struct sockaddr_in6 *saddr)
|
||||
{
|
||||
saddr->sin6_family = AF_INET6;
|
||||
saddr->sin6_port = addr->ipv4.port;
|
||||
memset (&saddr->sin6_addr, 0, 10);
|
||||
saddr->sin6_addr.s6_addr[10] = 0xff;
|
||||
saddr->sin6_addr.s6_addr[11] = 0xff;
|
||||
memcpy (&saddr->sin6_addr.s6_addr[12], addr->ipv4.addr, 4);
|
||||
}
|
||||
|
||||
static void
|
||||
hev_socks5_ipv6_into_sockaddr6 (const HevSocks5Addr *addr,
|
||||
struct sockaddr_in6 *saddr)
|
||||
{
|
||||
saddr->sin6_family = AF_INET6;
|
||||
saddr->sin6_port = addr->ipv6.port;
|
||||
memcpy (&saddr->sin6_addr, addr->ipv6.addr, 16);
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_addr_into_sockaddr6 (const HevSocks5Addr *addr,
|
||||
struct sockaddr_in6 *saddr, int *family)
|
||||
{
|
||||
switch (addr->atype) {
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV4: {
|
||||
hev_socks5_ipv4_into_sockaddr6 (addr, saddr);
|
||||
*family = HEV_SOCKS5_ADDR_FAMILY_IPV4;
|
||||
break;
|
||||
}
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV6: {
|
||||
hev_socks5_ipv6_into_sockaddr6 (addr, saddr);
|
||||
*family = HEV_SOCKS5_ADDR_FAMILY_IPV6;
|
||||
break;
|
||||
}
|
||||
case HEV_SOCKS5_ADDR_TYPE_NAME: {
|
||||
char name[256];
|
||||
uint16_t port;
|
||||
memcpy (name, addr->domain.addr, addr->domain.len);
|
||||
name[addr->domain.len] = '\0';
|
||||
memcpy (&port, addr->domain.addr + addr->domain.len, 2);
|
||||
return hev_socks5_name_into_sockaddr6 (name, ntohs (port), saddr,
|
||||
family);
|
||||
}
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_name_resolve_ipv4 (const char *name, struct sockaddr_in6 *saddr)
|
||||
{
|
||||
int res;
|
||||
|
||||
memset (&saddr->sin6_addr, 0, 10);
|
||||
res = inet_pton (AF_INET, name, &saddr->sin6_addr.s6_addr[12]);
|
||||
if (res == 0)
|
||||
return -1;
|
||||
|
||||
saddr->sin6_addr.s6_addr[10] = 0xff;
|
||||
saddr->sin6_addr.s6_addr[11] = 0xff;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_name_resolve_ipv6 (const char *name, struct sockaddr_in6 *saddr)
|
||||
{
|
||||
int res;
|
||||
|
||||
res = inet_pton (AF_INET6, name, &saddr->sin6_addr);
|
||||
if (res == 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_name_resolve_name (const char *name, struct sockaddr_in6 *saddr,
|
||||
int *family)
|
||||
{
|
||||
struct addrinfo *result = NULL;
|
||||
struct addrinfo hints = { 0 };
|
||||
int res = 0;
|
||||
|
||||
hints.ai_family = *family;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
hev_task_dns_getaddrinfo (name, NULL, &hints, &result);
|
||||
if (!result)
|
||||
return -1;
|
||||
|
||||
switch (result->ai_family) {
|
||||
case AF_INET: {
|
||||
struct sockaddr_in *sa = (struct sockaddr_in *)result->ai_addr;
|
||||
memset (&saddr->sin6_addr, 0, 10);
|
||||
saddr->sin6_addr.s6_addr[10] = 0xff;
|
||||
saddr->sin6_addr.s6_addr[11] = 0xff;
|
||||
memcpy (&saddr->sin6_addr.s6_addr[12], &sa->sin_addr, 4);
|
||||
*family = HEV_SOCKS5_ADDR_FAMILY_IPV4;
|
||||
break;
|
||||
}
|
||||
case AF_INET6: {
|
||||
struct sockaddr_in6 *sa = (struct sockaddr_in6 *)result->ai_addr;
|
||||
memcpy (&saddr->sin6_addr, &sa->sin6_addr, 16);
|
||||
*family = HEV_SOCKS5_ADDR_FAMILY_IPV6;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
res = -1;
|
||||
}
|
||||
|
||||
freeaddrinfo (result);
|
||||
return res;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_name_into_sockaddr6 (const char *name, int port,
|
||||
struct sockaddr_in6 *saddr, int *family)
|
||||
{
|
||||
int res;
|
||||
|
||||
saddr->sin6_family = AF_INET6;
|
||||
saddr->sin6_port = htons (port);
|
||||
|
||||
res = hev_socks5_name_resolve_ipv4 (name, saddr);
|
||||
if (res == 0) {
|
||||
*family = HEV_SOCKS5_ADDR_FAMILY_IPV4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
res = hev_socks5_name_resolve_ipv6 (name, saddr);
|
||||
if (res == 0) {
|
||||
*family = HEV_SOCKS5_ADDR_FAMILY_IPV6;
|
||||
return 0;
|
||||
}
|
||||
|
||||
res = hev_socks5_name_resolve_name (name, saddr, family);
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_set_connect_timeout (int timeout)
|
||||
{
|
||||
connect_timeout = timeout;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_get_connect_timeout (void)
|
||||
{
|
||||
return connect_timeout;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_set_tcp_timeout (int timeout)
|
||||
{
|
||||
tcp_timeout = timeout;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_get_tcp_timeout (void)
|
||||
{
|
||||
return tcp_timeout;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_set_udp_timeout (int timeout)
|
||||
{
|
||||
udp_timeout = timeout;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_get_udp_timeout (void)
|
||||
{
|
||||
return udp_timeout;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_set_task_stack_size (int stack_size)
|
||||
{
|
||||
task_stack_size = stack_size;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_get_task_stack_size (void)
|
||||
{
|
||||
return task_stack_size;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_set_udp_recv_buffer_size (int buffer_size)
|
||||
{
|
||||
udp_recv_buffer_size = buffer_size;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_set_udp_copy_buffer_nums (int nums)
|
||||
{
|
||||
udp_copy_buffer_nums = nums;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_get_udp_copy_buffer_nums (void)
|
||||
{
|
||||
return udp_copy_buffer_nums;
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-misc.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2025 hev
|
||||
Description : Socks5 Misc
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_MISC_H__
|
||||
#define __HEV_SOCKS5_MISC_H__
|
||||
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include <hev-task.h>
|
||||
|
||||
#include "hev-socks5-proto.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int hev_socks5_task_io_yielder (HevTaskYieldType type, void *data);
|
||||
|
||||
void hev_socks5_set_connect_timeout (int timeout);
|
||||
void hev_socks5_set_tcp_timeout (int timeout);
|
||||
void hev_socks5_set_udp_timeout (int timeout);
|
||||
|
||||
void hev_socks5_set_task_stack_size (int stack_size);
|
||||
void hev_socks5_set_udp_recv_buffer_size (int buffer_size);
|
||||
void hev_socks5_set_udp_copy_buffer_nums (int nums);
|
||||
|
||||
int hev_socks5_addr_len (const HevSocks5Addr *addr);
|
||||
int hev_socks5_addr_from_name (HevSocks5Addr *addr, const char *name, int port);
|
||||
int hev_socks5_addr_from_ipv4 (HevSocks5Addr *addr, const void *ipv4, int port);
|
||||
int hev_socks5_addr_from_ipv6 (HevSocks5Addr *addr, const void *ipv6, int port);
|
||||
int hev_socks5_addr_from_sockaddr6 (HevSocks5Addr *addr,
|
||||
struct sockaddr_in6 *saddr);
|
||||
int hev_socks5_addr_into_sockaddr6 (const HevSocks5Addr *addr,
|
||||
struct sockaddr_in6 *saddr, int *family);
|
||||
int hev_socks5_name_into_sockaddr6 (const char *host, int port,
|
||||
struct sockaddr_in6 *saddr, int *family);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_MISC_H__ */
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-proto.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2023 hev
|
||||
Description : Socks5 Proto
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_PROTO_H__
|
||||
#define __HEV_SOCKS5_PROTO_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum _HevSocks5Version HevSocks5Version;
|
||||
typedef enum _HevSocks5AuthMethod HevSocks5AuthMethod;
|
||||
typedef enum _HevSocks5AuthVersion HevSocks5AuthVersion;
|
||||
typedef enum _HevSocks5ReqCmd HevSocks5ReqCmd;
|
||||
typedef enum _HevSocks5ResRep HevSocks5ResRep;
|
||||
typedef enum _HevSocks5AddrType HevSocks5AddrType;
|
||||
|
||||
typedef struct _HevSocks5Auth HevSocks5Auth;
|
||||
typedef struct _HevSocks5Addr HevSocks5Addr;
|
||||
typedef struct _HevSocks5ReqRes HevSocks5ReqRes;
|
||||
typedef struct _HevSocks5UDPHdr HevSocks5UDPHdr;
|
||||
|
||||
enum _HevSocks5Version
|
||||
{
|
||||
HEV_SOCKS5_VERSION_5 = 5,
|
||||
};
|
||||
|
||||
enum _HevSocks5AuthMethod
|
||||
{
|
||||
HEV_SOCKS5_AUTH_METHOD_NONE = 0,
|
||||
HEV_SOCKS5_AUTH_METHOD_USER = 2,
|
||||
HEV_SOCKS5_AUTH_METHOD_DENY = 255,
|
||||
};
|
||||
|
||||
enum _HevSocks5AuthVersion
|
||||
{
|
||||
HEV_SOCKS5_AUTH_VERSION_1 = 1,
|
||||
};
|
||||
|
||||
enum _HevSocks5ReqCmd
|
||||
{
|
||||
HEV_SOCKS5_REQ_CMD_CONNECT = 1,
|
||||
HEV_SOCKS5_REQ_CMD_UDP_ASC = 3,
|
||||
HEV_SOCKS5_REQ_CMD_FWD_UDP = 5,
|
||||
};
|
||||
|
||||
enum _HevSocks5ResRep
|
||||
{
|
||||
HEV_SOCKS5_RES_REP_SUCC = 0,
|
||||
HEV_SOCKS5_RES_REP_FAIL = 1,
|
||||
HEV_SOCKS5_RES_REP_HOST = 4,
|
||||
HEV_SOCKS5_RES_REP_IMPL = 7,
|
||||
HEV_SOCKS5_RES_REP_ADDR = 8,
|
||||
};
|
||||
|
||||
enum _HevSocks5AddrType
|
||||
{
|
||||
HEV_SOCKS5_ADDR_TYPE_IPV4 = 1,
|
||||
HEV_SOCKS5_ADDR_TYPE_IPV6 = 4,
|
||||
HEV_SOCKS5_ADDR_TYPE_NAME = 3,
|
||||
};
|
||||
|
||||
struct _HevSocks5Auth
|
||||
{
|
||||
uint8_t ver;
|
||||
union
|
||||
{
|
||||
uint8_t method;
|
||||
uint8_t method_len;
|
||||
};
|
||||
uint8_t methods[256];
|
||||
} __attribute__ ((packed));
|
||||
|
||||
struct _HevSocks5Addr
|
||||
{
|
||||
uint8_t atype;
|
||||
union
|
||||
{
|
||||
struct
|
||||
{
|
||||
uint8_t addr[4];
|
||||
uint16_t port;
|
||||
} ipv4 __attribute__ ((packed));
|
||||
struct
|
||||
{
|
||||
uint8_t addr[16];
|
||||
uint16_t port;
|
||||
} ipv6 __attribute__ ((packed));
|
||||
struct
|
||||
{
|
||||
uint8_t len;
|
||||
uint8_t addr[256 + 2];
|
||||
} domain;
|
||||
};
|
||||
} __attribute__ ((packed));
|
||||
|
||||
struct _HevSocks5ReqRes
|
||||
{
|
||||
uint8_t ver;
|
||||
union
|
||||
{
|
||||
uint8_t cmd;
|
||||
uint8_t rep;
|
||||
};
|
||||
uint8_t rsv;
|
||||
HevSocks5Addr addr;
|
||||
} __attribute__ ((packed));
|
||||
|
||||
struct _HevSocks5UDPHdr
|
||||
{
|
||||
union
|
||||
{
|
||||
uint8_t rsv[3];
|
||||
struct
|
||||
{
|
||||
uint16_t datlen;
|
||||
uint8_t hdrlen;
|
||||
} __attribute__ ((packed));
|
||||
};
|
||||
HevSocks5Addr addr;
|
||||
} __attribute__ ((packed));
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_PROTO_H__ */
|
||||
|
|
@ -0,0 +1,684 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-server.c
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2025 hev
|
||||
Description : Socks5 Server
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <hev-task.h>
|
||||
#include <hev-task-io.h>
|
||||
#include <hev-task-io-socket.h>
|
||||
#include <hev-memory-allocator.h>
|
||||
|
||||
#include "hev-socks5-proto.h"
|
||||
#include "hev-socks5-misc-priv.h"
|
||||
#include "hev-socks5-logger-priv.h"
|
||||
|
||||
#include "hev-socks5-server.h"
|
||||
|
||||
#define task_io_yielder hev_socks5_task_io_yielder
|
||||
|
||||
HevSocks5Server *
|
||||
hev_socks5_server_new (int fd)
|
||||
{
|
||||
HevSocks5Server *self;
|
||||
int res;
|
||||
|
||||
self = hev_malloc0 (sizeof (HevSocks5Server));
|
||||
if (!self)
|
||||
return NULL;
|
||||
|
||||
res = hev_socks5_server_construct (self, fd);
|
||||
if (res < 0) {
|
||||
hev_free (self);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_D ("%p socks5 server new", self);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_server_set_auth (HevSocks5Server *self, HevSocks5Authenticator *auth)
|
||||
{
|
||||
if (self->auth)
|
||||
hev_object_unref (HEV_OBJECT (self->auth));
|
||||
|
||||
hev_object_ref (HEV_OBJECT (auth));
|
||||
self->auth = auth;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_read_auth_method (HevSocks5Server *self)
|
||||
{
|
||||
HevSocks5Auth auth;
|
||||
HevSocks5AuthMethod method;
|
||||
int res;
|
||||
int i;
|
||||
|
||||
LOG_D ("%p socks5 server read auth method", self);
|
||||
|
||||
res = hev_task_io_socket_recv (HEV_SOCKS5 (self)->fd, &auth, 2, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (res != 2) {
|
||||
LOG_I ("%p socks5 server read auth method", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (auth.ver != HEV_SOCKS5_VERSION_5) {
|
||||
LOG_I ("%p socks5 server auth.ver %u", self, auth.ver);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = hev_task_io_socket_recv (HEV_SOCKS5 (self)->fd, &auth.methods,
|
||||
auth.method_len, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (res != auth.method_len) {
|
||||
LOG_I ("%p socks5 server read auth methods", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (self->auth)
|
||||
method = HEV_SOCKS5_AUTH_METHOD_USER;
|
||||
else
|
||||
method = HEV_SOCKS5_AUTH_METHOD_NONE;
|
||||
|
||||
res = -1;
|
||||
for (i = 0; i < auth.method_len; i++) {
|
||||
if (auth.methods[i] == method) {
|
||||
res = method;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_write_auth_method (HevSocks5Server *self, int auth_method)
|
||||
{
|
||||
HevSocks5Auth auth;
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 server write auth method", self);
|
||||
|
||||
auth.ver = HEV_SOCKS5_VERSION_5;
|
||||
auth.method = auth_method;
|
||||
|
||||
res = hev_task_io_socket_send (HEV_SOCKS5 (self)->fd, &auth, 2, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (res <= 0) {
|
||||
LOG_I ("%p socks5 server write auth method", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_read_auth_user (HevSocks5Server *self)
|
||||
{
|
||||
HevSocks5User *user;
|
||||
uint8_t nlen, plen;
|
||||
uint8_t name[257];
|
||||
uint8_t pass[257];
|
||||
uint8_t head[2];
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 server read auth user", self);
|
||||
|
||||
res = hev_task_io_socket_recv (HEV_SOCKS5 (self)->fd, head, 2, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (res != 2) {
|
||||
LOG_I ("%p socks5 server read auth user.ver", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (head[0] != 1) {
|
||||
LOG_I ("%p socks5 server auth user.ver %u", self, head[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
nlen = head[1];
|
||||
if (nlen == 0) {
|
||||
LOG_I ("%p socks5 server auth user.nlen %u", self, nlen);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = hev_task_io_socket_recv (HEV_SOCKS5 (self)->fd, name, nlen + 1,
|
||||
MSG_WAITALL, task_io_yielder, self);
|
||||
if (res != (nlen + 1)) {
|
||||
LOG_I ("%p socks5 server read auth user.name", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
plen = name[nlen];
|
||||
if (plen == 0) {
|
||||
LOG_I ("%p socks5 server auth user.plen %u", self, plen);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = hev_task_io_socket_recv (HEV_SOCKS5 (self)->fd, pass, plen,
|
||||
MSG_WAITALL, task_io_yielder, self);
|
||||
if (res != plen) {
|
||||
LOG_I ("%p socks5 server read auth user.pass", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
user = hev_socks5_authenticator_get (self->auth, (char *)name, nlen);
|
||||
if (!user) {
|
||||
LOG_I ("%p socks5 server auth user: %s pass: %s", self, name, pass);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = hev_socks5_user_check (user, (char *)pass, plen);
|
||||
if (res < 0) {
|
||||
LOG_I ("%p socks5 server auth user: %s pass: %s", self, name, pass);
|
||||
return -1;
|
||||
}
|
||||
|
||||
hev_object_ref (HEV_OBJECT (user));
|
||||
hev_object_unref (HEV_OBJECT (self->auth));
|
||||
self->user = user;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_write_auth_user (HevSocks5Server *self, int auth_res)
|
||||
{
|
||||
uint8_t buf[2];
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 server write auth user", self);
|
||||
|
||||
buf[0] = 1;
|
||||
buf[1] = auth_res;
|
||||
|
||||
res = hev_task_io_socket_send (HEV_SOCKS5 (self)->fd, buf, 2, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (res <= 0) {
|
||||
LOG_I ("%p socks5 server write auth user", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_auth (HevSocks5Server *self)
|
||||
{
|
||||
int method;
|
||||
int res;
|
||||
|
||||
method = hev_socks5_server_read_auth_method (self);
|
||||
res = hev_socks5_server_write_auth_method (self, method);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
switch (method) {
|
||||
case HEV_SOCKS5_AUTH_METHOD_NONE:
|
||||
break;
|
||||
case HEV_SOCKS5_AUTH_METHOD_USER:
|
||||
res = hev_socks5_server_read_auth_user (self);
|
||||
res |= hev_socks5_server_write_auth_user (self, res);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
break;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_read_request (HevSocks5Server *self, int *cmd, int *rep,
|
||||
struct sockaddr_in6 *addr)
|
||||
{
|
||||
HevSocks5ReqRes req;
|
||||
int addr_family;
|
||||
int addrlen;
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 server read request", self);
|
||||
|
||||
res = hev_task_io_socket_recv (HEV_SOCKS5 (self)->fd, &req, 5, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
if (res != 5) {
|
||||
LOG_I ("%p socks5 server read request", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (req.ver != HEV_SOCKS5_VERSION_5) {
|
||||
*rep = HEV_SOCKS5_RES_REP_FAIL;
|
||||
LOG_I ("%p socks5 server req.ver %u", self, req.ver);
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (req.addr.atype) {
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV4:
|
||||
addrlen = 5;
|
||||
break;
|
||||
case HEV_SOCKS5_ADDR_TYPE_IPV6:
|
||||
addrlen = 17;
|
||||
break;
|
||||
case HEV_SOCKS5_ADDR_TYPE_NAME:
|
||||
addrlen = 2 + req.addr.domain.len;
|
||||
break;
|
||||
default:
|
||||
*rep = HEV_SOCKS5_RES_REP_ADDR;
|
||||
LOG_I ("%p socks5 server req.atype %u", self, req.addr.atype);
|
||||
return 0;
|
||||
}
|
||||
|
||||
res = hev_task_io_socket_recv (HEV_SOCKS5 (self)->fd, req.addr.domain.addr,
|
||||
addrlen, MSG_WAITALL, task_io_yielder, self);
|
||||
if (res != addrlen) {
|
||||
*rep = HEV_SOCKS5_RES_REP_ADDR;
|
||||
LOG_I ("%p socks5 server read addr", self);
|
||||
return 0;
|
||||
}
|
||||
|
||||
addr_family = hev_socks5_get_addr_family (HEV_SOCKS5 (self));
|
||||
res = hev_socks5_addr_into_sockaddr6 (&req.addr, addr, &addr_family);
|
||||
if (res < 0) {
|
||||
*rep = HEV_SOCKS5_RES_REP_ADDR;
|
||||
LOG_I ("%p socks5 server resolve addr", self);
|
||||
return 0;
|
||||
}
|
||||
hev_socks5_set_addr_family (HEV_SOCKS5 (self), addr_family);
|
||||
|
||||
if (LOG_ON ()) {
|
||||
const char *type;
|
||||
const char *str;
|
||||
char buf[272];
|
||||
|
||||
switch (req.cmd) {
|
||||
case HEV_SOCKS5_REQ_CMD_CONNECT:
|
||||
type = "tcp";
|
||||
break;
|
||||
case HEV_SOCKS5_REQ_CMD_UDP_ASC:
|
||||
case HEV_SOCKS5_REQ_CMD_FWD_UDP:
|
||||
type = "udp";
|
||||
break;
|
||||
default:
|
||||
type = "unknown";
|
||||
break;
|
||||
}
|
||||
|
||||
str = hev_socks5_addr_into_str (&req.addr, buf, sizeof (buf));
|
||||
LOG_I ("%p socks5 server %s %s", self, type, str);
|
||||
}
|
||||
|
||||
*cmd = req.cmd;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_write_response (HevSocks5Server *self, int rep,
|
||||
struct sockaddr_in6 *addr)
|
||||
{
|
||||
HevSocks5ReqRes res;
|
||||
int ret;
|
||||
|
||||
LOG_D ("%p socks5 server write response", self);
|
||||
|
||||
res.ver = HEV_SOCKS5_VERSION_5;
|
||||
res.rep = rep;
|
||||
res.rsv = 0;
|
||||
|
||||
ret = hev_socks5_addr_from_sockaddr6 (&res.addr, addr);
|
||||
ret = hev_task_io_socket_send (HEV_SOCKS5 (self)->fd, &res, 3 + ret,
|
||||
MSG_WAITALL, task_io_yielder, self);
|
||||
if (ret <= 0) {
|
||||
LOG_I ("%p socks5 server write response", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_connect (HevSocks5Server *self, struct sockaddr_in6 *addr)
|
||||
{
|
||||
HevSocks5Class *klass;
|
||||
int timeout;
|
||||
int res;
|
||||
int fd;
|
||||
|
||||
LOG_D ("%p socks5 server connect", self);
|
||||
|
||||
fd = hev_socks5_socket (SOCK_STREAM);
|
||||
if (fd < 0) {
|
||||
LOG_E ("%p socks5 server socket stream", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
klass = HEV_OBJECT_GET_CLASS (self);
|
||||
res = klass->binder (HEV_SOCKS5 (self), fd, (struct sockaddr *)addr);
|
||||
if (res < 0) {
|
||||
LOG_W ("%p socks5 server bind", self);
|
||||
hev_task_del_fd (hev_task_self (), fd);
|
||||
close (fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
timeout = hev_socks5_get_connect_timeout ();
|
||||
hev_socks5_set_timeout (HEV_SOCKS5 (self), timeout);
|
||||
|
||||
res = hev_task_io_socket_connect (fd, (struct sockaddr *)addr,
|
||||
sizeof (*addr), task_io_yielder, self);
|
||||
if (res < 0) {
|
||||
LOG_I ("%p socks5 server connect", self);
|
||||
hev_task_del_fd (hev_task_self (), fd);
|
||||
close (fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
timeout = hev_socks5_get_tcp_timeout ();
|
||||
hev_socks5_set_timeout (HEV_SOCKS5 (self), timeout);
|
||||
|
||||
self->fds[0] = fd;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_bind (HevSocks5Server *self, struct sockaddr_in6 *addr)
|
||||
{
|
||||
HevSocks5ServerClass *sskptr = HEV_OBJECT_GET_CLASS (self);
|
||||
int one = 1;
|
||||
int res;
|
||||
int fd;
|
||||
|
||||
LOG_D ("%p socks5 server bind", self);
|
||||
|
||||
fd = hev_socks5_socket (SOCK_DGRAM);
|
||||
if (fd < 0) {
|
||||
LOG_E ("%p socks5 server socket dgram", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->fds[0] = fd;
|
||||
|
||||
if (!addr)
|
||||
return 0;
|
||||
|
||||
fd = hev_socks5_socket (SOCK_DGRAM);
|
||||
if (fd < 0) {
|
||||
LOG_E ("%p socks5 server socket dgram", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof (one));
|
||||
if (res < 0) {
|
||||
LOG_W ("%p socks5 server socket reuse", self);
|
||||
hev_task_del_fd (hev_task_self (), fd);
|
||||
close (fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = sskptr->binder (self, fd, addr);
|
||||
if (res < 0) {
|
||||
LOG_W ("%p socks5 server bind", self);
|
||||
hev_task_del_fd (hev_task_self (), fd);
|
||||
close (fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->fds[1] = fd;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_udp_bind (HevSocks5Server *self, int sock,
|
||||
struct sockaddr_in6 *src)
|
||||
{
|
||||
socklen_t alen;
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 server udp bind", self);
|
||||
|
||||
alen = sizeof (struct sockaddr_in6);
|
||||
res = getsockname (HEV_SOCKS5 (self)->fd, (struct sockaddr *)src, &alen);
|
||||
if (res < 0) {
|
||||
LOG_W ("%p socks5 server tcp socket name", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
src->sin6_port = 0;
|
||||
res = bind (sock, (struct sockaddr *)src, alen);
|
||||
if (res < 0) {
|
||||
LOG_W ("%p socks5 server socket bind", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = getsockname (sock, (struct sockaddr *)src, &alen);
|
||||
if (res < 0) {
|
||||
LOG_W ("%p socks5 server udp socket name", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_handshake (HevSocks5Server *self)
|
||||
{
|
||||
struct sockaddr_in6 addr;
|
||||
int timeout;
|
||||
int cmd;
|
||||
int rep;
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 server handshake", self);
|
||||
|
||||
timeout = hev_socks5_get_tcp_timeout ();
|
||||
hev_socks5_set_timeout (HEV_SOCKS5 (self), timeout);
|
||||
|
||||
res = hev_socks5_server_auth (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
rep = HEV_SOCKS5_RES_REP_SUCC;
|
||||
res = hev_socks5_server_read_request (self, &cmd, &rep, &addr);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
if (rep == HEV_SOCKS5_RES_REP_SUCC) {
|
||||
switch (cmd) {
|
||||
case HEV_SOCKS5_REQ_CMD_CONNECT:
|
||||
res = hev_socks5_server_connect (self, &addr);
|
||||
if (res < 0)
|
||||
rep = HEV_SOCKS5_RES_REP_HOST;
|
||||
HEV_SOCKS5 (self)->type = HEV_SOCKS5_TYPE_TCP;
|
||||
break;
|
||||
case HEV_SOCKS5_REQ_CMD_UDP_ASC:
|
||||
res = hev_socks5_server_bind (self, &addr);
|
||||
if (res < 0)
|
||||
rep = HEV_SOCKS5_RES_REP_FAIL;
|
||||
HEV_SOCKS5 (self)->type = HEV_SOCKS5_TYPE_UDP_IN_UDP;
|
||||
break;
|
||||
case HEV_SOCKS5_REQ_CMD_FWD_UDP:
|
||||
res = hev_socks5_server_bind (self, NULL);
|
||||
if (res < 0)
|
||||
rep = HEV_SOCKS5_RES_REP_FAIL;
|
||||
HEV_SOCKS5 (self)->type = HEV_SOCKS5_TYPE_UDP_IN_TCP;
|
||||
break;
|
||||
default:
|
||||
rep = HEV_SOCKS5_RES_REP_IMPL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
res = hev_socks5_server_write_response (self, rep, &addr);
|
||||
if ((res < 0) || (rep != HEV_SOCKS5_RES_REP_SUCC))
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_service (HevSocks5Server *self)
|
||||
{
|
||||
int timeout;
|
||||
|
||||
LOG_D ("%p socks5 server service", self);
|
||||
|
||||
switch (HEV_SOCKS5 (self)->type) {
|
||||
case HEV_SOCKS5_TYPE_TCP:
|
||||
hev_socks5_tcp_splice (HEV_SOCKS5_TCP (self), self->fds[0]);
|
||||
break;
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_UDP:
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_TCP:
|
||||
timeout = hev_socks5_get_udp_timeout ();
|
||||
hev_socks5_set_timeout (HEV_SOCKS5 (self), timeout);
|
||||
hev_socks5_udp_splice (HEV_SOCKS5_UDP (self), self->fds[0]);
|
||||
break;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_server_run (HevSocks5Server *self)
|
||||
{
|
||||
HevTask *task = hev_task_self ();
|
||||
int res;
|
||||
int fd;
|
||||
|
||||
LOG_D ("%p socks5 server run", self);
|
||||
|
||||
fd = HEV_SOCKS5 (self)->fd;
|
||||
res = hev_task_add_fd (task, fd, POLLIN | POLLOUT);
|
||||
if (res < 0)
|
||||
hev_task_mod_fd (task, fd, POLLIN | POLLOUT);
|
||||
|
||||
res = hev_socks5_server_handshake (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
res = hev_socks5_server_service (self);
|
||||
if (res < 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_server_get_fd (HevSocks5UDP *self)
|
||||
{
|
||||
int fd;
|
||||
|
||||
switch (HEV_SOCKS5 (self)->type) {
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_TCP:
|
||||
fd = HEV_SOCKS5 (self)->fd;
|
||||
break;
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_UDP:
|
||||
fd = HEV_SOCKS5_SERVER (self)->fds[1];
|
||||
break;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_server_construct (HevSocks5Server *self, int fd)
|
||||
{
|
||||
int res;
|
||||
|
||||
res = hev_socks5_construct (&self->base, HEV_SOCKS5_TYPE_NONE);
|
||||
if (res < 0)
|
||||
return res;
|
||||
|
||||
LOG_D ("%p socks5 server construct", self);
|
||||
|
||||
HEV_OBJECT (self)->klass = HEV_SOCKS5_SERVER_TYPE;
|
||||
|
||||
HEV_SOCKS5 (self)->fd = fd;
|
||||
|
||||
self->fds[0] = -1;
|
||||
self->fds[1] = -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
hev_socks5_server_destruct (HevObject *base)
|
||||
{
|
||||
HevSocks5Server *self = HEV_SOCKS5_SERVER (base);
|
||||
HevTask *task = hev_task_self ();
|
||||
|
||||
LOG_D ("%p socks5 server destruct", self);
|
||||
|
||||
if (self->fds[0] >= 0) {
|
||||
hev_task_del_fd (task, self->fds[0]);
|
||||
close (self->fds[0]);
|
||||
}
|
||||
if (self->fds[1] >= 0) {
|
||||
hev_task_del_fd (task, self->fds[1]);
|
||||
close (self->fds[1]);
|
||||
}
|
||||
|
||||
if (self->obj)
|
||||
hev_object_unref (self->obj);
|
||||
|
||||
HEV_SOCKS5_TYPE->destruct (base);
|
||||
}
|
||||
|
||||
static void *
|
||||
hev_socks5_server_iface (HevObject *base, void *type)
|
||||
{
|
||||
HevSocks5ServerClass *klass = HEV_OBJECT_GET_CLASS (base);
|
||||
|
||||
if (type == HEV_SOCKS5_TCP_TYPE)
|
||||
return &klass->tcp;
|
||||
|
||||
if (type == HEV_SOCKS5_UDP_TYPE)
|
||||
return &klass->udp;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
HevObjectClass *
|
||||
hev_socks5_server_class (void)
|
||||
{
|
||||
static HevSocks5ServerClass klass;
|
||||
HevSocks5ServerClass *kptr = &klass;
|
||||
HevObjectClass *okptr = HEV_OBJECT_CLASS (kptr);
|
||||
|
||||
if (!okptr->name) {
|
||||
HevSocks5TCPIface *tiptr;
|
||||
HevSocks5UDPIface *uiptr;
|
||||
|
||||
memcpy (kptr, HEV_SOCKS5_TYPE, sizeof (HevSocks5Class));
|
||||
|
||||
okptr->name = "HevSocks5Server";
|
||||
okptr->destruct = hev_socks5_server_destruct;
|
||||
okptr->iface = hev_socks5_server_iface;
|
||||
|
||||
kptr->binder = hev_socks5_server_udp_bind;
|
||||
|
||||
tiptr = &kptr->tcp;
|
||||
memcpy (tiptr, HEV_SOCKS5_TCP_TYPE, sizeof (HevSocks5TCPIface));
|
||||
|
||||
uiptr = &kptr->udp;
|
||||
memcpy (uiptr, HEV_SOCKS5_UDP_TYPE, sizeof (HevSocks5UDPIface));
|
||||
uiptr->get_fd = hev_socks5_server_get_fd;
|
||||
}
|
||||
|
||||
return okptr;
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-server.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2023 hev
|
||||
Description : Socks5 Server
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_SERVER_H__
|
||||
#define __HEV_SOCKS5_SERVER_H__
|
||||
|
||||
#include <hev-object.h>
|
||||
|
||||
#include "hev-socks5.h"
|
||||
#include "hev-socks5-tcp.h"
|
||||
#include "hev-socks5-udp.h"
|
||||
#include "hev-socks5-user.h"
|
||||
#include "hev-socks5-authenticator.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HEV_SOCKS5_SERVER(p) ((HevSocks5Server *)p)
|
||||
#define HEV_SOCKS5_SERVER_CLASS(p) ((HevSocks5ServerClass *)p)
|
||||
#define HEV_SOCKS5_SERVER_TYPE (hev_socks5_server_class ())
|
||||
|
||||
typedef struct _HevSocks5Server HevSocks5Server;
|
||||
typedef struct _HevSocks5ServerClass HevSocks5ServerClass;
|
||||
|
||||
struct _HevSocks5Server
|
||||
{
|
||||
HevSocks5 base;
|
||||
|
||||
int fds[2];
|
||||
|
||||
union
|
||||
{
|
||||
HevObject *obj;
|
||||
HevSocks5User *user;
|
||||
HevSocks5Authenticator *auth;
|
||||
};
|
||||
};
|
||||
|
||||
struct _HevSocks5ServerClass
|
||||
{
|
||||
HevSocks5Class base;
|
||||
|
||||
int (*binder) (HevSocks5Server *self, int sock, struct sockaddr_in6 *src);
|
||||
|
||||
HevSocks5TCPIface tcp;
|
||||
HevSocks5UDPIface udp;
|
||||
};
|
||||
|
||||
HevObjectClass *hev_socks5_server_class (void);
|
||||
|
||||
int hev_socks5_server_construct (HevSocks5Server *self, int fd);
|
||||
|
||||
HevSocks5Server *hev_socks5_server_new (int fd);
|
||||
|
||||
void hev_socks5_server_set_auth (HevSocks5Server *self,
|
||||
HevSocks5Authenticator *auth);
|
||||
|
||||
int hev_socks5_server_run (HevSocks5Server *self);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_SERVER_H__ */
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-tcp.c
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 hev
|
||||
Description : Socks5 TCP
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#include "hev-socks5.h"
|
||||
#include "hev-socks5-misc-priv.h"
|
||||
#include "hev-socks5-logger-priv.h"
|
||||
|
||||
#include "hev-socks5-tcp.h"
|
||||
|
||||
#define task_io_yielder hev_socks5_task_io_yielder
|
||||
|
||||
static int
|
||||
hev_socks5_tcp_splicer (HevSocks5TCP *self, int fd)
|
||||
{
|
||||
HevTask *task = hev_task_self ();
|
||||
int cfd;
|
||||
int res;
|
||||
|
||||
LOG_D ("%p socks5 tcp splicer", self);
|
||||
|
||||
cfd = HEV_SOCKS5 (self)->fd;
|
||||
if (cfd < 0)
|
||||
return -1;
|
||||
|
||||
res = hev_task_add_fd (task, fd, POLLIN | POLLOUT);
|
||||
if (res < 0)
|
||||
hev_task_mod_fd (task, fd, POLLIN | POLLOUT);
|
||||
|
||||
hev_task_io_splice (cfd, cfd, fd, fd, 8192, task_io_yielder, self);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_tcp_splice (HevSocks5TCP *self, int fd)
|
||||
{
|
||||
HevSocks5TCPIface *iface;
|
||||
|
||||
iface = HEV_OBJECT_GET_IFACE (self, HEV_SOCKS5_TCP_TYPE);
|
||||
return iface->splicer (self, fd);
|
||||
}
|
||||
|
||||
void *
|
||||
hev_socks5_tcp_iface (void)
|
||||
{
|
||||
static HevSocks5TCPIface type = {
|
||||
.splicer = hev_socks5_tcp_splicer,
|
||||
};
|
||||
|
||||
return &type;
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-tcp.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 hev
|
||||
Description : Socks5 TCP
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_TCP_H__
|
||||
#define __HEV_SOCKS5_TCP_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HEV_SOCKS5_TCP(p) ((HevSocks5TCP *)p)
|
||||
#define HEV_SOCKS5_TCP_IFACE(p) ((HevSocks5TCPIface *)p)
|
||||
#define HEV_SOCKS5_TCP_TYPE (hev_socks5_tcp_iface ())
|
||||
|
||||
typedef void HevSocks5TCP;
|
||||
typedef struct _HevSocks5TCPIface HevSocks5TCPIface;
|
||||
|
||||
struct _HevSocks5TCPIface
|
||||
{
|
||||
int (*splicer) (HevSocks5TCP *self, int fd);
|
||||
};
|
||||
|
||||
void *hev_socks5_tcp_iface (void);
|
||||
|
||||
int hev_socks5_tcp_splice (HevSocks5TCP *self, int fd);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_TCP_H__ */
|
||||
508
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-socks5-udp.c
Normal file
508
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-socks5-udp.c
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-udp.c
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2025 hev
|
||||
Description : Socks5 UDP
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <hev-task.h>
|
||||
#include <hev-task-io.h>
|
||||
#include <hev-task-io-socket.h>
|
||||
#include <hev-memory-allocator.h>
|
||||
|
||||
#include "hev-socks5.h"
|
||||
#include "hev-socks5-misc-priv.h"
|
||||
#include "hev-socks5-logger-priv.h"
|
||||
|
||||
#include "hev-socks5-udp.h"
|
||||
|
||||
#define UDP_BUF_SIZE 1500
|
||||
|
||||
static int
|
||||
task_io_yielder (HevTaskYieldType type, void *data)
|
||||
{
|
||||
HevSocks5 *self = data;
|
||||
|
||||
if (self->type == HEV_SOCKS5_TYPE_UDP_IN_UDP) {
|
||||
ssize_t res;
|
||||
char buf;
|
||||
|
||||
res = recv (self->fd, &buf, sizeof (buf), 0);
|
||||
if ((res == 0) || ((res < 0) && (errno != EAGAIN))) {
|
||||
hev_socks5_set_timeout (self, 0);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return hev_socks5_task_io_yielder (type, data);
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_udp_get_fd (HevSocks5UDP *self)
|
||||
{
|
||||
HevSocks5UDPIface *iface;
|
||||
|
||||
iface = HEV_OBJECT_GET_IFACE (self, HEV_SOCKS5_UDP_TYPE);
|
||||
return iface->get_fd (self);
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_udp_sendmmsg_tcp (HevSocks5UDP *self, HevSocks5UDPMsg *msgv,
|
||||
unsigned int num)
|
||||
{
|
||||
struct iovec iov[num * 3];
|
||||
HevSocks5UDPHdr udp[num];
|
||||
struct msghdr mh;
|
||||
int i, res;
|
||||
|
||||
mh.msg_name = NULL;
|
||||
mh.msg_namelen = 0;
|
||||
mh.msg_control = NULL;
|
||||
mh.msg_controllen = 0;
|
||||
mh.msg_iov = iov;
|
||||
mh.msg_iovlen = num * 3;
|
||||
|
||||
for (i = 0; i < num; i++) {
|
||||
int addrlen;
|
||||
|
||||
addrlen = hev_socks5_addr_len (msgv[i].addr);
|
||||
if (addrlen <= 0) {
|
||||
LOG_D ("%p socks5 udp addr", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
udp[i].datlen = htons (msgv[i].len);
|
||||
udp[i].hdrlen = 3 + addrlen;
|
||||
|
||||
iov[i * 3].iov_base = &udp[i];
|
||||
iov[i * 3].iov_len = 3;
|
||||
iov[i * 3 + 1].iov_base = msgv[i].addr;
|
||||
iov[i * 3 + 1].iov_len = addrlen;
|
||||
iov[i * 3 + 2].iov_base = msgv[i].buf;
|
||||
iov[i * 3 + 2].iov_len = msgv[i].len;
|
||||
}
|
||||
|
||||
res = hev_task_io_socket_sendmsg (hev_socks5_udp_get_fd (self), &mh,
|
||||
MSG_WAITALL, task_io_yielder, self);
|
||||
if (res <= 0) {
|
||||
LOG_D ("%p socks5 udp write tcp", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_udp_sendmmsg_udp (HevSocks5UDP *self, HevSocks5UDPMsg *msgv,
|
||||
unsigned int num)
|
||||
{
|
||||
struct iovec iov[num * 3];
|
||||
struct mmsghdr mvec[num];
|
||||
HevSocks5UDPHdr udp[num];
|
||||
int i, res;
|
||||
|
||||
for (i = 0; i < num; i++) {
|
||||
int addrlen;
|
||||
|
||||
addrlen = hev_socks5_addr_len (msgv[i].addr);
|
||||
if (addrlen <= 0) {
|
||||
LOG_D ("%p socks5 udp addr", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
udp[i].datlen = 0;
|
||||
udp[i].hdrlen = 0;
|
||||
|
||||
iov[i * 3].iov_base = &udp[i];
|
||||
iov[i * 3].iov_len = 3;
|
||||
iov[i * 3 + 1].iov_base = msgv[i].addr;
|
||||
iov[i * 3 + 1].iov_len = addrlen;
|
||||
iov[i * 3 + 2].iov_base = msgv[i].buf;
|
||||
iov[i * 3 + 2].iov_len = msgv[i].len;
|
||||
|
||||
mvec[i].msg_hdr.msg_name = NULL;
|
||||
mvec[i].msg_hdr.msg_namelen = 0;
|
||||
mvec[i].msg_hdr.msg_control = NULL;
|
||||
mvec[i].msg_hdr.msg_controllen = 0;
|
||||
mvec[i].msg_hdr.msg_iov = &iov[i * 3];
|
||||
mvec[i].msg_hdr.msg_iovlen = 3;
|
||||
}
|
||||
|
||||
res = hev_task_io_socket_sendmmsg (hev_socks5_udp_get_fd (self), mvec, num,
|
||||
MSG_WAITALL, task_io_yielder, self);
|
||||
if (res <= 0)
|
||||
LOG_D ("%p socks5 udp write udp", self);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_udp_sendmmsg (HevSocks5UDP *self, HevSocks5UDPMsg *msgv,
|
||||
unsigned int num)
|
||||
{
|
||||
switch (HEV_SOCKS5 (self)->type) {
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_TCP:
|
||||
return hev_socks5_udp_sendmmsg_tcp (self, msgv, num);
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_UDP:
|
||||
return hev_socks5_udp_sendmmsg_udp (self, msgv, num);
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_udp_recvmmsg_tcp (HevSocks5UDP *self, HevSocks5UDPMsg *msgv,
|
||||
unsigned int num, int nonblock)
|
||||
{
|
||||
int i, fd, rlen = 0;
|
||||
|
||||
fd = hev_socks5_udp_get_fd (self);
|
||||
|
||||
if (nonblock)
|
||||
nonblock = MSG_DONTWAIT;
|
||||
|
||||
for (i = 0; i < num; i++) {
|
||||
HevSocks5UDPHdr udp;
|
||||
struct iovec iov[2];
|
||||
struct msghdr mh;
|
||||
int addrlen;
|
||||
int res;
|
||||
|
||||
res = hev_task_io_socket_recv (fd, &udp, 5, nonblock, task_io_yielder,
|
||||
self);
|
||||
if (res > 0 && res < 5)
|
||||
res += hev_task_io_socket_recv (fd, (void *)&udp + res, 5 - res,
|
||||
MSG_WAITALL, task_io_yielder, self);
|
||||
if (res != 5) {
|
||||
if (rlen > 0)
|
||||
break;
|
||||
if (res != -1 || errno != EAGAIN)
|
||||
LOG_D ("%p socks5 udp read udp head", self);
|
||||
return res;
|
||||
}
|
||||
|
||||
if (udp.hdrlen < 5) {
|
||||
LOG_D ("%p socks5 udp head len", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
addrlen = udp.hdrlen - 3;
|
||||
udp.datlen = ntohs (udp.datlen);
|
||||
if (udp.datlen > (msgv[i].len - addrlen)) {
|
||||
LOG_D ("%p socks5 udp data len", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
mh.msg_name = NULL;
|
||||
mh.msg_namelen = 0;
|
||||
mh.msg_control = NULL;
|
||||
mh.msg_controllen = 0;
|
||||
mh.msg_iov = iov;
|
||||
mh.msg_iovlen = 2;
|
||||
|
||||
iov[0].iov_base = msgv[i].buf + 2;
|
||||
iov[0].iov_len = addrlen - 2;
|
||||
iov[1].iov_base = msgv[i].buf + addrlen;
|
||||
iov[1].iov_len = udp.datlen;
|
||||
|
||||
res = hev_task_io_socket_recvmsg (fd, &mh, MSG_WAITALL, task_io_yielder,
|
||||
self);
|
||||
if (res != (addrlen - 2 + udp.datlen)) {
|
||||
LOG_D ("%p socks5 udp read udp data", self);
|
||||
return res;
|
||||
}
|
||||
|
||||
msgv[i].addr = msgv[i].buf;
|
||||
msgv[i].buf = iov[1].iov_base;
|
||||
msgv[i].len = udp.datlen;
|
||||
msgv[i].addr->atype = udp.addr.atype;
|
||||
msgv[i].addr->domain.len = udp.addr.domain.len;
|
||||
|
||||
rlen++;
|
||||
}
|
||||
|
||||
return rlen;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_udp_recvmmsg_udp (HevSocks5UDP *self, HevSocks5UDPMsg *msgv,
|
||||
unsigned int num, int nonblock)
|
||||
{
|
||||
struct sockaddr_in6 taddr;
|
||||
struct mmsghdr mvec[num];
|
||||
struct iovec iov[num];
|
||||
int i, fd, res;
|
||||
|
||||
fd = hev_socks5_udp_get_fd (self);
|
||||
|
||||
if (nonblock)
|
||||
nonblock = MSG_DONTWAIT;
|
||||
|
||||
for (i = 0; i < num; i++) {
|
||||
mvec[i].msg_hdr.msg_name = NULL;
|
||||
mvec[i].msg_hdr.msg_namelen = 0;
|
||||
mvec[i].msg_hdr.msg_control = NULL;
|
||||
mvec[i].msg_hdr.msg_controllen = 0;
|
||||
mvec[i].msg_hdr.msg_iov = &iov[i];
|
||||
mvec[i].msg_hdr.msg_iovlen = 1;
|
||||
|
||||
iov[i].iov_base = msgv[i].buf;
|
||||
iov[i].iov_len = msgv[i].len;
|
||||
}
|
||||
|
||||
if (!HEV_SOCKS5 (self)->udp_associated) {
|
||||
mvec[0].msg_hdr.msg_name = &taddr;
|
||||
mvec[0].msg_hdr.msg_namelen = sizeof (taddr);
|
||||
}
|
||||
|
||||
res = hev_task_io_socket_recvmmsg (fd, mvec, num, nonblock, task_io_yielder,
|
||||
self);
|
||||
if (res <= 0) {
|
||||
if (res != -1 || errno != EAGAIN)
|
||||
LOG_D ("%p socks5 udp read udp", self);
|
||||
return res;
|
||||
}
|
||||
|
||||
if (!HEV_SOCKS5 (self)->udp_associated) {
|
||||
struct sockaddr *saddr = mvec[0].msg_hdr.msg_name;
|
||||
socklen_t alen = mvec[0].msg_hdr.msg_namelen;
|
||||
if (connect (fd, saddr, alen) < 0)
|
||||
return -1;
|
||||
HEV_SOCKS5 (self)->udp_associated = 1;
|
||||
}
|
||||
|
||||
for (i = 0; i < res; i++) {
|
||||
HevSocks5UDPHdr *udp = msgv[i].buf;
|
||||
int addrlen = hev_socks5_addr_len (&udp->addr);
|
||||
int doff;
|
||||
|
||||
msgv[i].len = mvec[i].msg_len;
|
||||
if (msgv[i].len < 4) {
|
||||
msgv[i].addr = NULL;
|
||||
msgv[i].len = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (addrlen <= 0) {
|
||||
LOG_D ("%p socks5 udp addr", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
doff = 3 + addrlen;
|
||||
if (doff > msgv[i].len) {
|
||||
LOG_D ("%p socks5 udp data len", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
msgv[i].addr = &udp->addr;
|
||||
msgv[i].buf += doff;
|
||||
msgv[i].len -= doff;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_udp_recvmmsg (HevSocks5UDP *self, HevSocks5UDPMsg *msgv,
|
||||
unsigned int num, int nonblock)
|
||||
{
|
||||
switch (HEV_SOCKS5 (self)->type) {
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_TCP:
|
||||
return hev_socks5_udp_recvmmsg_tcp (self, msgv, num, nonblock);
|
||||
case HEV_SOCKS5_TYPE_UDP_IN_UDP:
|
||||
return hev_socks5_udp_recvmmsg_udp (self, msgv, num, nonblock);
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_udp_fwd_f (HevSocks5UDP *self, int fd, void *buf, unsigned int num,
|
||||
int *bind)
|
||||
{
|
||||
HevSocks5UDPMsg svec[num];
|
||||
int i, res;
|
||||
|
||||
for (i = 0; i < num; i++) {
|
||||
svec[i].buf = buf + UDP_BUF_SIZE * i;
|
||||
svec[i].len = UDP_BUF_SIZE;
|
||||
}
|
||||
|
||||
res = hev_socks5_udp_recvmmsg (self, svec, num, 1);
|
||||
if (res > 0) {
|
||||
struct sockaddr_in6 addr[res];
|
||||
struct mmsghdr dvec[res];
|
||||
struct iovec iov[res];
|
||||
int ret;
|
||||
|
||||
for (i = 0; i < res; i++) {
|
||||
int family;
|
||||
|
||||
if (!svec[i].len || !svec[i].addr) {
|
||||
LOG_D ("%p socks5 udp invalid", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
family = hev_socks5_get_addr_family (HEV_SOCKS5 (self));
|
||||
ret = hev_socks5_addr_into_sockaddr6 (svec[i].addr, &addr[i],
|
||||
&family);
|
||||
if (ret < 0) {
|
||||
LOG_D ("%p socks5 udp sockaddr", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
dvec[i].msg_hdr.msg_name = (struct sockaddr *)&addr[i];
|
||||
dvec[i].msg_hdr.msg_namelen = sizeof (struct sockaddr_in6);
|
||||
dvec[i].msg_hdr.msg_control = NULL;
|
||||
dvec[i].msg_hdr.msg_controllen = 0;
|
||||
dvec[i].msg_hdr.msg_iov = &iov[i];
|
||||
dvec[i].msg_hdr.msg_iovlen = 1;
|
||||
iov[i].iov_base = svec[i].buf;
|
||||
iov[i].iov_len = svec[i].len;
|
||||
}
|
||||
|
||||
if (!*bind) {
|
||||
HevSocks5Class *skptr = HEV_OBJECT_GET_CLASS (self);
|
||||
struct sockaddr *addr = dvec[0].msg_hdr.msg_name;
|
||||
ret = skptr->binder (HEV_SOCKS5 (self), fd, addr);
|
||||
if (ret < 0) {
|
||||
LOG_W ("%p socks5 udp bind", self);
|
||||
return -1;
|
||||
}
|
||||
*bind = 1;
|
||||
}
|
||||
|
||||
res = hev_task_io_socket_sendmmsg (fd, dvec, res, MSG_WAITALL,
|
||||
task_io_yielder, self);
|
||||
}
|
||||
if (res <= 0) {
|
||||
if (res == -1 && errno == EAGAIN)
|
||||
return 0;
|
||||
LOG_D ("%p socks5 udp fwd f recv send", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_udp_fwd_b (HevSocks5UDP *self, int fd, struct mmsghdr *svec,
|
||||
unsigned int num)
|
||||
{
|
||||
int i, res;
|
||||
|
||||
res = hev_task_io_socket_recvmmsg (fd, svec, num, MSG_DONTWAIT,
|
||||
task_io_yielder, self);
|
||||
if (res > 0) {
|
||||
HevSocks5UDPMsg dvec[res];
|
||||
char saddr[res][19];
|
||||
|
||||
for (i = 0; i < res; i++) {
|
||||
dvec[i].buf = svec[i].msg_hdr.msg_iov->iov_base;
|
||||
dvec[i].len = svec[i].msg_len;
|
||||
dvec[i].addr = (HevSocks5Addr *)&saddr[i];
|
||||
hev_socks5_addr_from_sockaddr6 (dvec[i].addr,
|
||||
svec[i].msg_hdr.msg_name);
|
||||
}
|
||||
res = hev_socks5_udp_sendmmsg (self, dvec, res);
|
||||
}
|
||||
if (res <= 0) {
|
||||
if (res == -1 && errno == EAGAIN)
|
||||
return 0;
|
||||
LOG_D ("%p socks5 udp fwd b recv send", self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_udp_splicer (HevSocks5UDP *self, int fd_b)
|
||||
{
|
||||
HevTask *task = hev_task_self ();
|
||||
int res_f = 1, res_b = 1;
|
||||
int bind = 0;
|
||||
void *buf;
|
||||
int fd_a;
|
||||
int num;
|
||||
|
||||
LOG_D ("%p socks5 udp splicer", self);
|
||||
|
||||
num = hev_socks5_get_udp_copy_buffer_nums ();
|
||||
buf = hev_malloc (UDP_BUF_SIZE * num * 2);
|
||||
if (!buf)
|
||||
return -1;
|
||||
|
||||
fd_a = hev_socks5_udp_get_fd (self);
|
||||
if (hev_task_mod_fd (task, fd_a, POLLIN | POLLOUT) < 0)
|
||||
hev_task_add_fd (task, fd_a, POLLIN | POLLOUT);
|
||||
if (hev_task_add_fd (task, fd_b, POLLIN | POLLOUT) < 0)
|
||||
hev_task_mod_fd (task, fd_b, POLLIN | POLLOUT);
|
||||
|
||||
{
|
||||
struct mmsghdr vec[num];
|
||||
struct sockaddr_in6 addr[num];
|
||||
struct iovec iov[num];
|
||||
int i;
|
||||
|
||||
for (i = 0; i < num; i++) {
|
||||
vec[i].msg_hdr.msg_name = (struct sockaddr *)&addr[i];
|
||||
vec[i].msg_hdr.msg_namelen = sizeof (struct sockaddr_in6);
|
||||
vec[i].msg_hdr.msg_control = NULL;
|
||||
vec[i].msg_hdr.msg_controllen = 0;
|
||||
vec[i].msg_hdr.msg_iov = &iov[i];
|
||||
vec[i].msg_hdr.msg_iovlen = 1;
|
||||
iov[i].iov_base = buf + UDP_BUF_SIZE * (num + i);
|
||||
iov[i].iov_len = UDP_BUF_SIZE;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
HevTaskYieldType type;
|
||||
|
||||
if (res_f >= 0)
|
||||
res_f = hev_socks5_udp_fwd_f (self, fd_b, buf, num, &bind);
|
||||
if (res_b >= 0)
|
||||
res_b = hev_socks5_udp_fwd_b (self, fd_b, vec, num);
|
||||
|
||||
if (res_f > 0 || res_b > 0)
|
||||
type = HEV_TASK_YIELD;
|
||||
else if ((res_f & res_b) == 0)
|
||||
type = HEV_TASK_WAITIO;
|
||||
else
|
||||
break;
|
||||
|
||||
if (task_io_yielder (type, self))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hev_free (buf);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_udp_splice (HevSocks5UDP *self, int fd)
|
||||
{
|
||||
HevSocks5UDPIface *iface;
|
||||
|
||||
iface = HEV_OBJECT_GET_IFACE (self, HEV_SOCKS5_UDP_TYPE);
|
||||
return iface->splicer (self, fd);
|
||||
}
|
||||
|
||||
void *
|
||||
hev_socks5_udp_iface (void)
|
||||
{
|
||||
static HevSocks5UDPIface type = {
|
||||
.splicer = hev_socks5_udp_splicer,
|
||||
};
|
||||
|
||||
return &type;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-udp.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2025 hev
|
||||
Description : Socks5 UDP
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_UDP_H__
|
||||
#define __HEV_SOCKS5_UDP_H__
|
||||
|
||||
#include "hev-socks5-proto.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HEV_SOCKS5_UDP(p) ((HevSocks5UDP *)p)
|
||||
#define HEV_SOCKS5_UDP_IFACE(p) ((HevSocks5UDPIface *)p)
|
||||
#define HEV_SOCKS5_UDP_TYPE (hev_socks5_udp_iface ())
|
||||
|
||||
typedef void HevSocks5UDP;
|
||||
typedef struct _HevSocks5UDPMsg HevSocks5UDPMsg;
|
||||
typedef struct _HevSocks5UDPIface HevSocks5UDPIface;
|
||||
|
||||
struct _HevSocks5UDPMsg
|
||||
{
|
||||
HevSocks5Addr *addr;
|
||||
void *buf;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
struct _HevSocks5UDPIface
|
||||
{
|
||||
int (*get_fd) (HevSocks5UDP *self);
|
||||
int (*splicer) (HevSocks5UDP *self, int fd);
|
||||
};
|
||||
|
||||
void *hev_socks5_udp_iface (void);
|
||||
|
||||
int hev_socks5_udp_get_fd (HevSocks5UDP *self);
|
||||
|
||||
int hev_socks5_udp_sendmmsg (HevSocks5UDP *self, HevSocks5UDPMsg *msgv,
|
||||
unsigned int num);
|
||||
int hev_socks5_udp_recvmmsg (HevSocks5UDP *self, HevSocks5UDPMsg *msgv,
|
||||
unsigned int num, int nonblock);
|
||||
|
||||
int hev_socks5_udp_splice (HevSocks5UDP *self, int fd);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_UDP_H__ */
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-user.c
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2023 hev
|
||||
Description : Socks5 User
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "hev-socks5-logger-priv.h"
|
||||
|
||||
#include "hev-socks5-user.h"
|
||||
|
||||
HevSocks5User *
|
||||
hev_socks5_user_new (const char *name, unsigned int name_len, const char *pass,
|
||||
unsigned int pass_len)
|
||||
{
|
||||
HevSocks5User *self;
|
||||
int res;
|
||||
|
||||
self = calloc (1, sizeof (HevSocks5User));
|
||||
if (!self)
|
||||
return NULL;
|
||||
|
||||
res = hev_socks5_user_construct (self, name, name_len, pass, pass_len);
|
||||
if (res < 0) {
|
||||
free (self);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
LOG_D ("%p socks5 user new", self);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_user_check (HevSocks5User *self, const char *pass,
|
||||
unsigned int pass_len)
|
||||
{
|
||||
HevSocks5UserClass *klass = HEV_OBJECT_GET_CLASS (self);
|
||||
|
||||
return klass->checker (self, pass, pass_len);
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_user_checker (HevSocks5User *self, const char *pass,
|
||||
unsigned int pass_len)
|
||||
{
|
||||
LOG_D ("%p socks5 user checker", self);
|
||||
|
||||
if (self->pass_len != pass_len)
|
||||
return -1;
|
||||
|
||||
if (memcmp (self->pass, pass, pass_len) != 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_user_construct (HevSocks5User *self, const char *name,
|
||||
unsigned int name_len, const char *pass,
|
||||
unsigned int pass_len)
|
||||
{
|
||||
int res;
|
||||
|
||||
res = hev_object_atomic_construct (&self->base);
|
||||
if (res < 0)
|
||||
return res;
|
||||
|
||||
LOG_D ("%p socks5 user construct", self);
|
||||
|
||||
HEV_OBJECT (self)->klass = HEV_SOCKS5_USER_TYPE;
|
||||
|
||||
self->name = malloc (name_len);
|
||||
self->name_len = name_len;
|
||||
memcpy (self->name, name, name_len);
|
||||
|
||||
self->pass = malloc (pass_len);
|
||||
self->pass_len = pass_len;
|
||||
memcpy (self->pass, pass, pass_len);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
hev_socks5_user_destruct (HevObject *base)
|
||||
{
|
||||
HevSocks5User *self = HEV_SOCKS5_USER (base);
|
||||
|
||||
LOG_D ("%p socks5 user destruct", self);
|
||||
|
||||
free (self->name);
|
||||
free (self->pass);
|
||||
|
||||
HEV_OBJECT_ATOMIC_TYPE->destruct (base);
|
||||
free (base);
|
||||
}
|
||||
|
||||
HevObjectClass *
|
||||
hev_socks5_user_class (void)
|
||||
{
|
||||
static HevSocks5UserClass klass;
|
||||
HevSocks5UserClass *kptr = &klass;
|
||||
HevObjectClass *okptr = HEV_OBJECT_CLASS (kptr);
|
||||
|
||||
if (!okptr->name) {
|
||||
memcpy (kptr, HEV_OBJECT_ATOMIC_TYPE, sizeof (HevObjectAtomicClass));
|
||||
|
||||
okptr->name = "HevSocks5User";
|
||||
okptr->destruct = hev_socks5_user_destruct;
|
||||
|
||||
kptr->checker = hev_socks5_user_checker;
|
||||
}
|
||||
|
||||
return okptr;
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5-user.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2023 hev
|
||||
Description : Socks5 User
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_USER_H__
|
||||
#define __HEV_SOCKS5_USER_H__
|
||||
|
||||
#include <hev-object-atomic.h>
|
||||
|
||||
#include "hev-rbtree.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HEV_SOCKS5_USER(p) ((HevSocks5User *)p)
|
||||
#define HEV_SOCKS5_USER_CLASS(p) ((HevSocks5UserClass *)p)
|
||||
#define HEV_SOCKS5_USER_TYPE (hev_socks5_user_class ())
|
||||
|
||||
typedef struct _HevSocks5User HevSocks5User;
|
||||
typedef struct _HevSocks5UserClass HevSocks5UserClass;
|
||||
|
||||
struct _HevSocks5User
|
||||
{
|
||||
HevObjectAtomic base;
|
||||
|
||||
HevRBTreeNode node;
|
||||
|
||||
char *name;
|
||||
char *pass;
|
||||
unsigned int name_len;
|
||||
unsigned int pass_len;
|
||||
};
|
||||
|
||||
struct _HevSocks5UserClass
|
||||
{
|
||||
HevObjectAtomicClass base;
|
||||
|
||||
int (*checker) (HevSocks5User *self, const char *pass,
|
||||
unsigned int pass_len);
|
||||
};
|
||||
|
||||
HevObjectClass *hev_socks5_user_class (void);
|
||||
|
||||
int hev_socks5_user_construct (HevSocks5User *self, const char *name,
|
||||
unsigned int name_len, const char *pass,
|
||||
unsigned int pass_len);
|
||||
|
||||
HevSocks5User *hev_socks5_user_new (const char *name, unsigned int name_len,
|
||||
const char *pass, unsigned int pass_len);
|
||||
|
||||
int hev_socks5_user_check (HevSocks5User *self, const char *pass,
|
||||
unsigned int pass_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_USER_H__ */
|
||||
109
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-socks5.c
Normal file
109
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-socks5.c
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5.c
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2023 hev
|
||||
Description : Socks5
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include <hev-task.h>
|
||||
#include <hev-task-io.h>
|
||||
#include <hev-task-io-socket.h>
|
||||
#include <hev-task-dns.h>
|
||||
#include <hev-memory-allocator.h>
|
||||
|
||||
#include "hev-socks5-logger-priv.h"
|
||||
|
||||
#include "hev-socks5.h"
|
||||
|
||||
int
|
||||
hev_socks5_get_timeout (HevSocks5 *self)
|
||||
{
|
||||
return self->timeout;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_set_timeout (HevSocks5 *self, int timeout)
|
||||
{
|
||||
self->timeout = timeout;
|
||||
}
|
||||
|
||||
HevSocks5AddrFamily
|
||||
hev_socks5_get_addr_family (HevSocks5 *self)
|
||||
{
|
||||
return self->addr_family;
|
||||
}
|
||||
|
||||
void
|
||||
hev_socks5_set_addr_family (HevSocks5 *self, HevSocks5AddrFamily family)
|
||||
{
|
||||
self->addr_family = family;
|
||||
}
|
||||
|
||||
static int
|
||||
hev_socks5_bind (HevSocks5 *self, int sock, const struct sockaddr *dest)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
hev_socks5_construct (HevSocks5 *self, HevSocks5Type type)
|
||||
{
|
||||
int res;
|
||||
|
||||
res = hev_object_construct (&self->base);
|
||||
if (res < 0)
|
||||
return res;
|
||||
|
||||
LOG_D ("%p socks5 construct", self);
|
||||
|
||||
HEV_OBJECT (self)->klass = HEV_SOCKS5_TYPE;
|
||||
|
||||
self->fd = -1;
|
||||
self->timeout = -1;
|
||||
self->type = type;
|
||||
self->addr_family = HEV_SOCKS5_ADDR_FAMILY_UNSPEC;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
hev_socks5_destruct (HevObject *base)
|
||||
{
|
||||
HevSocks5 *self = HEV_SOCKS5 (base);
|
||||
|
||||
LOG_D ("%p socks5 destruct", self);
|
||||
|
||||
if (self->fd >= 0) {
|
||||
hev_task_del_fd (hev_task_self (), self->fd);
|
||||
close (self->fd);
|
||||
}
|
||||
|
||||
HEV_OBJECT_TYPE->destruct (base);
|
||||
hev_free (base);
|
||||
}
|
||||
|
||||
HevObjectClass *
|
||||
hev_socks5_class (void)
|
||||
{
|
||||
static HevSocks5Class klass;
|
||||
HevSocks5Class *kptr = &klass;
|
||||
HevObjectClass *okptr = HEV_OBJECT_CLASS (kptr);
|
||||
|
||||
if (!okptr->name) {
|
||||
memcpy (kptr, HEV_OBJECT_TYPE, sizeof (HevObjectClass));
|
||||
|
||||
okptr->name = "HevSocks5";
|
||||
okptr->destruct = hev_socks5_destruct;
|
||||
|
||||
kptr->binder = hev_socks5_bind;
|
||||
}
|
||||
|
||||
return okptr;
|
||||
}
|
||||
78
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-socks5.h
Normal file
78
app/src/main/jni/hev-socks5-tunnel/src/core/src/hev-socks5.h
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-socks5.h
|
||||
Author : Heiher <r@hev.cc>
|
||||
Copyright : Copyright (c) 2021 - 2023 hev
|
||||
Description : Socks5
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_SOCKS5_H__
|
||||
#define __HEV_SOCKS5_H__
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include <hev-object.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HEV_SOCKS5(p) ((HevSocks5 *)p)
|
||||
#define HEV_SOCKS5_CLASS(p) ((HevSocks5Class *)p)
|
||||
#define HEV_SOCKS5_TYPE (hev_socks5_class ())
|
||||
|
||||
typedef struct _HevSocks5 HevSocks5;
|
||||
typedef struct _HevSocks5Class HevSocks5Class;
|
||||
typedef enum _HevSocks5Type HevSocks5Type;
|
||||
typedef enum _HevSocks5AddrFamily HevSocks5AddrFamily;
|
||||
|
||||
enum _HevSocks5Type
|
||||
{
|
||||
HEV_SOCKS5_TYPE_NONE,
|
||||
HEV_SOCKS5_TYPE_TCP,
|
||||
HEV_SOCKS5_TYPE_UDP_IN_TCP,
|
||||
HEV_SOCKS5_TYPE_UDP_IN_UDP,
|
||||
};
|
||||
|
||||
enum _HevSocks5AddrFamily
|
||||
{
|
||||
HEV_SOCKS5_ADDR_FAMILY_IPV4 = AF_INET,
|
||||
HEV_SOCKS5_ADDR_FAMILY_IPV6 = AF_INET6,
|
||||
HEV_SOCKS5_ADDR_FAMILY_UNSPEC = AF_UNSPEC,
|
||||
};
|
||||
|
||||
struct _HevSocks5
|
||||
{
|
||||
HevObject base;
|
||||
|
||||
int fd;
|
||||
int timeout;
|
||||
int udp_associated;
|
||||
HevSocks5Type type;
|
||||
HevSocks5AddrFamily addr_family;
|
||||
};
|
||||
|
||||
struct _HevSocks5Class
|
||||
{
|
||||
HevObjectClass base;
|
||||
|
||||
int (*binder) (HevSocks5 *self, int sock, const struct sockaddr *dest);
|
||||
};
|
||||
|
||||
HevObjectClass *hev_socks5_class (void);
|
||||
|
||||
int hev_socks5_construct (HevSocks5 *self, HevSocks5Type type);
|
||||
|
||||
int hev_socks5_get_timeout (HevSocks5 *self);
|
||||
void hev_socks5_set_timeout (HevSocks5 *self, int timeout);
|
||||
|
||||
HevSocks5AddrFamily hev_socks5_get_addr_family (HevSocks5 *self);
|
||||
void hev_socks5_set_addr_family (HevSocks5 *self, HevSocks5AddrFamily family);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __HEV_SOCKS5_H__ */
|
||||
21
app/src/main/jni/hev-socks5-tunnel/src/hev-config-const.h
Normal file
21
app/src/main/jni/hev-socks5-tunnel/src/hev-config-const.h
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
============================================================================
|
||||
Name : hev-config-const.h
|
||||
Author : hev <r@hev.cc>
|
||||
Copyright : Copyright (c) 2019 - 2024 hev
|
||||
Description : Config Constants
|
||||
============================================================================
|
||||
*/
|
||||
|
||||
#ifndef __HEV_CONFIG_CONST_H__
|
||||
#define __HEV_CONFIG_CONST_H__
|
||||
|
||||
#define MAJOR_VERSION (2)
|
||||
#define MINOR_VERSION (15)
|
||||
#define MICRO_VERSION (0)
|
||||
|
||||
static const int UDP_BUF_SIZE = 1500;
|
||||
static const int UDP_POOL_SIZE = 512;
|
||||
static const int TASK_STACK_SIZE = 20480;
|
||||
|
||||
#endif /* __HEV_CONFIG_CONST_H__ */
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue