feat(android): switch config and tg intent to mtproto model

This commit is contained in:
Dark_Avery 2026-03-30 16:14:54 +03:00
parent 1599b1126c
commit 810991ea18
9 changed files with 51 additions and 8 deletions

View File

@ -128,6 +128,7 @@ class MainActivity : AppCompatActivity() {
private fun renderConfig(config: ProxyConfig) { private fun renderConfig(config: ProxyConfig) {
binding.hostInput.setText(config.host) binding.hostInput.setText(config.host)
binding.portInput.setText(config.portText) binding.portInput.setText(config.portText)
binding.secretInput.setText(config.secretText)
binding.dcIpInput.setText(config.dcIpText) binding.dcIpInput.setText(config.dcIpText)
binding.logMaxMbInput.setText(config.logMaxMbText) binding.logMaxMbInput.setText(config.logMaxMbText)
binding.bufferKbInput.setText(config.bufferKbText) binding.bufferKbInput.setText(config.bufferKbText)
@ -141,6 +142,7 @@ class MainActivity : AppCompatActivity() {
return ProxyConfig( return ProxyConfig(
host = binding.hostInput.text?.toString().orEmpty(), host = binding.hostInput.text?.toString().orEmpty(),
portText = binding.portInput.text?.toString().orEmpty(), portText = binding.portInput.text?.toString().orEmpty(),
secretText = binding.secretInput.text?.toString().orEmpty(),
dcIpText = binding.dcIpInput.text?.toString().orEmpty(), dcIpText = binding.dcIpInput.text?.toString().orEmpty(),
logMaxMbText = binding.logMaxMbInput.text?.toString().orEmpty(), logMaxMbText = binding.logMaxMbInput.text?.toString().orEmpty(),
bufferKbText = binding.bufferKbInput.text?.toString().orEmpty(), bufferKbText = binding.bufferKbInput.text?.toString().orEmpty(),

View File

@ -1,8 +1,11 @@
package org.flowseal.tgwsproxy package org.flowseal.tgwsproxy
import java.security.SecureRandom
data class ProxyConfig( data class ProxyConfig(
val host: String = DEFAULT_HOST, val host: String = DEFAULT_HOST,
val portText: String = DEFAULT_PORT.toString(), val portText: String = DEFAULT_PORT.toString(),
val secretText: String = DEFAULT_SECRET,
val dcIpText: String = DEFAULT_DC_IP_LINES.joinToString("\n"), val dcIpText: String = DEFAULT_DC_IP_LINES.joinToString("\n"),
val logMaxMbText: String = formatDecimal(DEFAULT_LOG_MAX_MB), val logMaxMbText: String = formatDecimal(DEFAULT_LOG_MAX_MB),
val bufferKbText: String = DEFAULT_BUFFER_KB.toString(), val bufferKbText: String = DEFAULT_BUFFER_KB.toString(),
@ -22,6 +25,13 @@ data class ProxyConfig(
return ValidationResult(errorMessage = "Порт должен быть в диапазоне 1-65535.") return ValidationResult(errorMessage = "Порт должен быть в диапазоне 1-65535.")
} }
val secretValue = secretText.trim().lowercase()
if (secretValue.length != 32 || !secretValue.all { it in "0123456789abcdef" }) {
return ValidationResult(
errorMessage = "MTProto secret должен содержать ровно 32 hex-символа."
)
}
val lines = dcIpText val lines = dcIpText
.lineSequence() .lineSequence()
.map { it.trim() } .map { it.trim() }
@ -75,6 +85,7 @@ data class ProxyConfig(
normalized = NormalizedProxyConfig( normalized = NormalizedProxyConfig(
host = hostValue, host = hostValue,
port = portValue, port = portValue,
secret = secretValue,
dcIpList = lines, dcIpList = lines,
logMaxMb = logMaxMbValue, logMaxMb = logMaxMbValue,
bufferKb = bufferKbValue, bufferKb = bufferKbValue,
@ -87,10 +98,11 @@ data class ProxyConfig(
companion object { companion object {
const val DEFAULT_HOST = "127.0.0.1" const val DEFAULT_HOST = "127.0.0.1"
const val DEFAULT_PORT = 1080 const val DEFAULT_PORT = 1443
const val DEFAULT_LOG_MAX_MB = 5.0 const val DEFAULT_LOG_MAX_MB = 5.0
const val DEFAULT_BUFFER_KB = 256 const val DEFAULT_BUFFER_KB = 256
const val DEFAULT_POOL_SIZE = 4 const val DEFAULT_POOL_SIZE = 4
val DEFAULT_SECRET = generateSecret()
val DEFAULT_DC_IP_LINES = listOf( val DEFAULT_DC_IP_LINES = listOf(
"2:149.154.167.220", "2:149.154.167.220",
"4:149.154.167.220", "4:149.154.167.220",
@ -104,6 +116,12 @@ data class ProxyConfig(
} }
} }
private fun generateSecret(): String {
val bytes = ByteArray(16)
SecureRandom().nextBytes(bytes)
return bytes.joinToString(separator = "") { "%02x".format(it) }
}
private fun isIpv4Address(value: String): Boolean { private fun isIpv4Address(value: String): Boolean {
val octets = value.split(".") val octets = value.split(".")
if (octets.size != 4) { if (octets.size != 4) {
@ -128,6 +146,7 @@ data class ValidationResult(
data class NormalizedProxyConfig( data class NormalizedProxyConfig(
val host: String, val host: String,
val port: Int, val port: Int,
val secret: String,
val dcIpList: List<String>, val dcIpList: List<String>,
val logMaxMb: Double, val logMaxMb: Double,
val bufferKb: Int, val bufferKb: Int,

View File

@ -9,6 +9,7 @@ class ProxySettingsStore(context: Context) {
return ProxyConfig( return ProxyConfig(
host = preferences.getString(KEY_HOST, ProxyConfig.DEFAULT_HOST).orEmpty(), host = preferences.getString(KEY_HOST, ProxyConfig.DEFAULT_HOST).orEmpty(),
portText = preferences.getInt(KEY_PORT, ProxyConfig.DEFAULT_PORT).toString(), portText = preferences.getInt(KEY_PORT, ProxyConfig.DEFAULT_PORT).toString(),
secretText = preferences.getString(KEY_SECRET, ProxyConfig.DEFAULT_SECRET).orEmpty(),
dcIpText = preferences.getString( dcIpText = preferences.getString(
KEY_DC_IP_TEXT, KEY_DC_IP_TEXT,
ProxyConfig.DEFAULT_DC_IP_LINES.joinToString("\n"), ProxyConfig.DEFAULT_DC_IP_LINES.joinToString("\n"),
@ -36,6 +37,7 @@ class ProxySettingsStore(context: Context) {
preferences.edit() preferences.edit()
.putString(KEY_HOST, config.host) .putString(KEY_HOST, config.host)
.putInt(KEY_PORT, config.port) .putInt(KEY_PORT, config.port)
.putString(KEY_SECRET, config.secret)
.putString(KEY_DC_IP_TEXT, config.dcIpList.joinToString("\n")) .putString(KEY_DC_IP_TEXT, config.dcIpList.joinToString("\n"))
.putFloat(KEY_LOG_MAX_MB, config.logMaxMb.toFloat()) .putFloat(KEY_LOG_MAX_MB, config.logMaxMb.toFloat())
.putInt(KEY_BUFFER_KB, config.bufferKb) .putInt(KEY_BUFFER_KB, config.bufferKb)
@ -49,6 +51,7 @@ class ProxySettingsStore(context: Context) {
private const val PREFS_NAME = "proxy_settings" private const val PREFS_NAME = "proxy_settings"
private const val KEY_HOST = "host" private const val KEY_HOST = "host"
private const val KEY_PORT = "port" private const val KEY_PORT = "port"
private const val KEY_SECRET = "secret"
private const val KEY_DC_IP_TEXT = "dc_ip_text" private const val KEY_DC_IP_TEXT = "dc_ip_text"
private const val KEY_LOG_MAX_MB = "log_max_mb" private const val KEY_LOG_MAX_MB = "log_max_mb"
private const val KEY_BUFFER_KB = "buf_kb" private const val KEY_BUFFER_KB = "buf_kb"

View File

@ -17,6 +17,7 @@ object PythonProxyBridge {
File(context.filesDir, "tg-ws-proxy").absolutePath, File(context.filesDir, "tg-ws-proxy").absolutePath,
config.host, config.host,
config.port, config.port,
config.secret,
config.dcIpList, config.dcIpList,
config.logMaxMb, config.logMaxMb,
config.bufferKb, config.bufferKb,

View File

@ -8,7 +8,7 @@ import android.net.Uri
object TelegramProxyIntent { object TelegramProxyIntent {
fun open(context: Context, config: NormalizedProxyConfig): Boolean { fun open(context: Context, config: NormalizedProxyConfig): Boolean {
val uri = Uri.parse( val uri = Uri.parse(
"tg://socks?server=${Uri.encode(config.host)}&port=${config.port}" "tg://proxy?server=${Uri.encode(config.host)}&port=${config.port}&secret=dd${Uri.encode(config.secret)}"
) )
val intent = Intent(Intent.ACTION_VIEW, uri) val intent = Intent(Intent.ACTION_VIEW, uri)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

View File

@ -44,7 +44,7 @@ def _normalize_dc_ip_list(dc_ip_list: Iterable[object]) -> list[str]:
return [str(item).strip() for item in values if str(item).strip()] return [str(item).strip() for item in values if str(item).strip()]
def start_proxy(app_dir: str, host: str, port: int, def start_proxy(app_dir: str, host: str, port: int, secret: str,
dc_ip_list: Iterable[object], log_max_mb: float = 5.0, dc_ip_list: Iterable[object], log_max_mb: float = 5.0,
buf_kb: int = 256, pool_size: int = 4, buf_kb: int = 256, pool_size: int = 4,
verbose: bool = False) -> str: verbose: bool = False) -> str:
@ -70,6 +70,7 @@ def start_proxy(app_dir: str, host: str, port: int,
config = { config = {
"host": host, "host": host,
"port": int(port), "port": int(port),
"secret": str(secret).strip(),
"dc_ip": _normalize_dc_ip_list(dc_ip_list), "dc_ip": _normalize_dc_ip_list(dc_ip_list),
"log_max_mb": float(log_max_mb), "log_max_mb": float(log_max_mb),
"buf_kb": int(buf_kb), "buf_kb": int(buf_kb),

View File

@ -243,6 +243,20 @@
android:maxLines="1" /> android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/secret_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/secretInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">TG WS Proxy</string> <string name="app_name">TG WS Proxy</string>
<string name="subtitle">Android app for the local Telegram SOCKS5 proxy.</string> <string name="subtitle">Android app for the local Telegram MTProto proxy.</string>
<string name="status_label">Foreground service</string> <string name="status_label">Foreground service</string>
<string name="status_starting">Starting</string> <string name="status_starting">Starting</string>
<string name="status_running">Running</string> <string name="status_running">Running</string>
@ -33,6 +33,7 @@
<string name="updates_open_release_button">Open Release Page</string> <string name="updates_open_release_button">Open Release Page</string>
<string name="host_hint">Proxy host</string> <string name="host_hint">Proxy host</string>
<string name="port_hint">Proxy port</string> <string name="port_hint">Proxy port</string>
<string name="secret_hint">MTProto secret (32 hex characters)</string>
<string name="dc_ip_hint">DC to IP mappings (one DC:IP per line)</string> <string name="dc_ip_hint">DC to IP mappings (one DC:IP per line)</string>
<string name="log_max_mb_hint">Max log size before rotation (MB)</string> <string name="log_max_mb_hint">Max log size before rotation (MB)</string>
<string name="buffer_kb_hint">Socket buffer size (KB)</string> <string name="buffer_kb_hint">Socket buffer size (KB)</string>
@ -51,12 +52,12 @@
<string name="settings_saved">Settings saved</string> <string name="settings_saved">Settings saved</string>
<string name="service_start_requested">Foreground service start requested</string> <string name="service_start_requested">Foreground service start requested</string>
<string name="service_restart_requested">Foreground service restart requested</string> <string name="service_restart_requested">Foreground service restart requested</string>
<string name="telegram_not_found">Telegram app was not found for tg://socks.</string> <string name="telegram_not_found">Telegram app was not found for tg://proxy.</string>
<string name="notification_title">TG WS Proxy</string> <string name="notification_title">TG WS Proxy</string>
<string name="notification_channel_name">Proxy service</string> <string name="notification_channel_name">Proxy service</string>
<string name="notification_channel_description">Keeps the Telegram proxy service alive in the foreground.</string> <string name="notification_channel_description">Keeps the Telegram proxy service alive in the foreground.</string>
<string name="notification_starting">SOCKS5 %1$s:%2$d • starting embedded Python</string> <string name="notification_starting">MTProto %1$s:%2$d • starting embedded Python</string>
<string name="notification_running">SOCKS5 %1$s:%2$d • proxy active</string> <string name="notification_running">MTProto %1$s:%2$d • proxy active</string>
<string name="notification_endpoint">%1$s:%2$d</string> <string name="notification_endpoint">%1$s:%2$d</string>
<string name="notification_details">DC mappings: %1$d\nTraffic: ↑ %2$s/s ↓ %3$s/s\nTransferred: ↑ %4$s ↓ %5$s</string> <string name="notification_details">DC mappings: %1$d\nTraffic: ↑ %2$s/s ↓ %3$s/s\nTransferred: ↑ %4$s ↓ %5$s</string>
<string name="notification_action_stop">Stop</string> <string name="notification_action_stop">Stop</string>

View File

@ -107,7 +107,8 @@ class AndroidProxyBridgeTests(unittest.TestCase):
log_path = android_proxy_bridge.start_proxy( log_path = android_proxy_bridge.start_proxy(
"/tmp/app", "/tmp/app",
"127.0.0.1", "127.0.0.1",
1080, 1443,
"0123456789abcdef0123456789abcdef",
["2:149.154.167.220"], ["2:149.154.167.220"],
7.0, 7.0,
512, 512,
@ -118,6 +119,7 @@ class AndroidProxyBridgeTests(unittest.TestCase):
android_proxy_bridge.ProxyAppRuntime = original_runtime android_proxy_bridge.ProxyAppRuntime = original_runtime
self.assertEqual(log_path, "/tmp/proxy.log") self.assertEqual(log_path, "/tmp/proxy.log")
self.assertEqual(captured["config"]["secret"], "0123456789abcdef0123456789abcdef")
self.assertEqual(captured["config"]["log_max_mb"], 7.0) self.assertEqual(captured["config"]["log_max_mb"], 7.0)
self.assertEqual(captured["config"]["buf_kb"], 512) self.assertEqual(captured["config"]["buf_kb"], 512)
self.assertEqual(captured["config"]["pool_size"], 6) self.assertEqual(captured["config"]["pool_size"], 6)