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