mirror of
https://github.com/by-sonic/tglock.git
synced 2026-09-06 02:26:08 +03:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cb72e4acc | |||
| 60ae065938 | |||
| 1a56e1c96e | |||
| a571b4289a | |||
| fe85784550 |
@@ -64,13 +64,20 @@ jobs:
|
||||
with:
|
||||
tagName: v__VERSION__
|
||||
releaseName: TGLock v__VERSION__
|
||||
# Только инструкция по скачиванию: она одна и та же для любой версии.
|
||||
# Список изменений дописывается в release notes при выпуске — иначе
|
||||
# выходит как в бетах 1-7, где каждая несла один и тот же текст «новый
|
||||
# TGLock, полностью обновлённый интерфейс» и понять, что поменялось,
|
||||
# было невозможно.
|
||||
releaseBody: |
|
||||
Новый TGLock: адаптивный MTProto/WebSocket-транспорт и полностью обновлённый интерфейс на Tauri 2.
|
||||
## Что скачивать
|
||||
|
||||
- macOS: скачайте универсальный `.dmg` или `.app.tar.gz`
|
||||
- Windows: скачайте `.exe` установщик
|
||||
- Linux: скачайте `.AppImage` или `.deb`
|
||||
- Сервер или машина без монитора: скачайте `tglock-cli-*` — там нет графического интерфейса
|
||||
- **Windows** — `.exe`, установщик
|
||||
- **macOS** — универсальный `.dmg` (или `.app.tar.gz`)
|
||||
- **Linux** — `.AppImage` или `.deb`
|
||||
- **Сервер, контейнер, машина без монитора** — `tglock-cli-*`: то же самое, но без графического интерфейса
|
||||
|
||||
Если Windows ругается на файл — [почему так и что с этим делать](https://github.com/by-sonic/tglock#-антивирус-ругается-smartscreen-предупреждает-virustotal-показывает-детекты).
|
||||
releaseDraft: false
|
||||
prerelease: false
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
Generated
+1
-1
@@ -3553,7 +3553,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tglock"
|
||||
version = "2.0.0-beta.6"
|
||||
version = "2.0.0-beta.8"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"cipher",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tglock"
|
||||
version = "2.0.0-beta.6"
|
||||
version = "2.0.0-beta.8"
|
||||
edition = "2021"
|
||||
rust-version = "1.88"
|
||||
description = "Telegram unblock via local WebSocket tunnel"
|
||||
|
||||
@@ -155,7 +155,11 @@ Telegram → Настройки → **Продвинутые** → Тип сое
|
||||
|
||||
В окне TGLock включи галочку **LAN** — приложение начнёт слушать на `0.0.0.0`. Все устройства в домашней сети (телефон, планшет, ноутбук, телевизор) смогут подключиться к `<IP-компьютера>:1080` и тоже получить рабочий Telegram.
|
||||
|
||||
Ссылку `tg://proxy` с уже подставленным адресом TGLock открывает сам при включении — её достаточно переслать себе в Telegram и открыть на телефоне. Если нужен адрес руками: `ipconfig` на Windows, `ip a` на Linux, `ifconfig` на macOS.
|
||||
Когда LAN-режим включён, TGLock показывает в окне **готовый адрес** вида `192.168.1.7:1080` — нажми на него, чтобы скопировать. Плюс он сам открывает ссылку `tg://proxy`, где уже подставлены адрес, порт и секрет: её достаточно переслать себе в Telegram и открыть на телефоне.
|
||||
|
||||
> ⚠️ На другом устройстве **не вводи `127.0.0.1`** — для него это оно само, а не твой компьютер. Нужен именно тот адрес, который показан в окне TGLock. Это же касается телефона в эмуляторе (MuMuPlayer, BlueStacks и подобных): внутри эмулятора Android — отдельная машина со своей сетью.
|
||||
|
||||
Если адрес нужен руками: `ipconfig` на Windows, `ip a` на Linux, `ifconfig` на macOS.
|
||||
|
||||
Удобно, если дома один компьютер всегда включён — он становится «домашним Telegram-роутером».
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tglock-ui",
|
||||
"private": true,
|
||||
"version": "2.0.0-beta.6",
|
||||
"version": "2.0.0-beta.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 1420",
|
||||
|
||||
+13
-2
@@ -142,6 +142,15 @@ async fn serve(args: Args) -> Result<(), String> {
|
||||
"Ссылка для Telegram: {}",
|
||||
listen.telegram_link(&stats.telegram_secret())
|
||||
));
|
||||
// Запись секрета могла провалиться — тогда после перезапуска ссылка
|
||||
// изменится и Telegram скажет «прокси настроен неверно». Раньше это
|
||||
// происходило молча (by-sonic/tglock#37).
|
||||
if let Some(error) = stats.secret_write_error() {
|
||||
say(&format!(
|
||||
"Внимание: секрет НЕ сохранён ({error}). После перезапуска ссылка \
|
||||
изменится, и Telegram откажется подключаться к старой"
|
||||
));
|
||||
}
|
||||
if matches!(settings.secret, config::SecretSource::Ephemeral) {
|
||||
say(
|
||||
"Внимание: секрет не закреплён и будет новым после перезапуска — \
|
||||
@@ -202,13 +211,15 @@ async fn watch_status(stats: Arc<proxy::Stats>) {
|
||||
stats.last_dc.load(Ordering::Relaxed),
|
||||
stats.last_route.load(Ordering::Relaxed),
|
||||
stats.ws_failures.load(Ordering::Relaxed),
|
||||
stats.route_failures(),
|
||||
);
|
||||
if previous.as_ref() == Some(¤t) {
|
||||
continue;
|
||||
}
|
||||
let (active, tunnels, dc, route, failures) = current;
|
||||
let (active, tunnels, dc, route, failures, route_failures) = current;
|
||||
let line = format!(
|
||||
"соединений {active} · туннелей {tunnels} · {} · {} · сбоев {failures}",
|
||||
"соединений {active} · туннелей {tunnels} · {} · {} · сбоев {failures} · \
|
||||
падений маршрутов {route_failures}",
|
||||
if dc > 0 {
|
||||
format!("DC{dc}")
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ impl Resolved {
|
||||
let stats = match &self.secret {
|
||||
SecretSource::Inline(secret) => proxy::Stats::with_secret(*secret),
|
||||
SecretSource::File(path) => {
|
||||
proxy::Stats::with_secret(mtproto::load_or_create_secret_at(path))
|
||||
proxy::Stats::with_stored_secret(mtproto::load_or_create_secret_at(path))
|
||||
}
|
||||
SecretSource::Ephemeral => proxy::Stats::new(),
|
||||
};
|
||||
|
||||
+72
-1
@@ -44,8 +44,18 @@ struct StatusSnapshot {
|
||||
data_center: Option<u16>,
|
||||
route: String,
|
||||
failures: u32,
|
||||
/// Падения отдельных маршрутов. Растёт даже когда соединение в итоге
|
||||
/// состоялось через запасной адрес (by-sonic/tglock#32).
|
||||
route_failures: u32,
|
||||
uptime_seconds: u64,
|
||||
port: u16,
|
||||
/// Адрес, который нужно вписать в Telegram на другом устройстве.
|
||||
///
|
||||
/// В LAN-режиме это адрес этого компьютера в локальной сети. Люди искали
|
||||
/// его в интерфейсе и не находили: вписывали `127.0.0.1`, который на
|
||||
/// телефоне или в эмуляторе означает само устройство, и подключение не
|
||||
/// работало (by-sonic/tglock#36).
|
||||
share_address: Option<String>,
|
||||
logs: Vec<LogLine>,
|
||||
}
|
||||
|
||||
@@ -53,6 +63,10 @@ struct AppState {
|
||||
stats: Arc<proxy::Stats>,
|
||||
settings: Mutex<Settings>,
|
||||
active_port: Mutex<u16>,
|
||||
/// Слушатель работающего прокси. Нужен, чтобы показать адрес для других
|
||||
/// устройств именно тот, на котором прокси реально поднят, а не тот, что
|
||||
/// сейчас выбран в настройках.
|
||||
active_listen: Mutex<Option<ListenConfig>>,
|
||||
started_at: Mutex<Option<Instant>>,
|
||||
logs: Arc<Mutex<Vec<LogLine>>>,
|
||||
settings_path: PathBuf,
|
||||
@@ -68,6 +82,7 @@ impl AppState {
|
||||
stats: proxy::Stats::new(),
|
||||
settings: Mutex::new(settings),
|
||||
active_port: Mutex::new(proxy::DEFAULT_PORT),
|
||||
active_listen: Mutex::new(None),
|
||||
started_at: Mutex::new(None),
|
||||
logs: Arc::new(Mutex::new(Vec::new())),
|
||||
settings_path,
|
||||
@@ -96,12 +111,14 @@ impl AppState {
|
||||
data_center: (data_center > 0).then_some(data_center),
|
||||
route: route.to_owned(),
|
||||
failures: self.stats.ws_failures.load(Ordering::Relaxed),
|
||||
route_failures: self.stats.route_failures(),
|
||||
uptime_seconds: self
|
||||
.started_at
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map_or(0, |started| started.elapsed().as_secs()),
|
||||
port: *self.active_port.lock().unwrap(),
|
||||
share_address: share_address(*self.active_listen.lock().unwrap()),
|
||||
logs: self.logs.lock().unwrap().clone(),
|
||||
}
|
||||
}
|
||||
@@ -118,6 +135,18 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Адрес, который нужно вписать в Telegram на другом устройстве.
|
||||
///
|
||||
/// Только для слушателя на `0.0.0.0`: на loopback делиться нечем, туда никто
|
||||
/// извне не достучится. Возвращается адрес этой машины в сети, а не `0.0.0.0`
|
||||
/// и не `127.0.0.1` — последний на телефоне или в эмуляторе означает само
|
||||
/// устройство, и именно на этом спотыкались (by-sonic/tglock#36).
|
||||
fn share_address(listen: Option<ListenConfig>) -> Option<String> {
|
||||
listen
|
||||
.filter(|listen| listen.addr.ip().is_unspecified())
|
||||
.map(|listen| format!("{}:{}", listen.advertised_host(), listen.addr.port()))
|
||||
}
|
||||
|
||||
fn current_time() -> String {
|
||||
let seconds = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -173,6 +202,8 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
||||
ListenConfig::loopback(settings.port)
|
||||
};
|
||||
|
||||
*state.active_listen.lock().unwrap() = Some(listen);
|
||||
|
||||
let stats = state.stats.clone();
|
||||
let logs = state.logs.clone();
|
||||
std::thread::spawn(move || {
|
||||
@@ -190,6 +221,7 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(220));
|
||||
if !state.stats.running.load(Ordering::SeqCst) {
|
||||
*state.active_listen.lock().unwrap() = None;
|
||||
*state.started_at.lock().unwrap() = None;
|
||||
return Err(state
|
||||
.logs
|
||||
@@ -209,6 +241,7 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
||||
#[tauri::command]
|
||||
fn stop_proxy(state: State<'_, AppState>) -> StatusSnapshot {
|
||||
state.stats.stop();
|
||||
*state.active_listen.lock().unwrap() = None;
|
||||
*state.started_at.lock().unwrap() = None;
|
||||
state.log("Защита выключена", false);
|
||||
state.snapshot()
|
||||
@@ -279,7 +312,17 @@ fn main() {
|
||||
.app_config_dir()
|
||||
.map_err(|error| error.to_string())?
|
||||
.join("settings.json");
|
||||
app.manage(AppState::new(settings_path));
|
||||
let state = AppState::new(settings_path);
|
||||
// Если секрет не удалось записать, ссылка tg://proxy изменится после
|
||||
// перезапуска и Telegram откажется подключаться к сохранённой.
|
||||
// Раньше это происходило молча (by-sonic/tglock#37).
|
||||
if let Some(error) = state.stats.secret_write_error() {
|
||||
state.log(
|
||||
format!("Секрет не сохранён ({error}). После перезапуска ссылка изменится"),
|
||||
true,
|
||||
);
|
||||
}
|
||||
app.manage(state);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@@ -297,6 +340,34 @@ fn main() {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn nothing_to_share_when_the_proxy_is_off_or_local() {
|
||||
assert_eq!(
|
||||
share_address(None),
|
||||
None,
|
||||
"выключенный прокси нечего делить"
|
||||
);
|
||||
assert_eq!(
|
||||
share_address(Some(ListenConfig::loopback(1080))),
|
||||
None,
|
||||
"на loopback снаружи никто не подключится"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lan_mode_shares_a_reachable_address() {
|
||||
let shown = share_address(Some(ListenConfig::lan(1443))).expect("в LAN-режиме адрес нужен");
|
||||
assert!(shown.ends_with(":1443"), "порт должен быть виден: {shown}");
|
||||
assert!(
|
||||
!shown.starts_with("0.0.0.0"),
|
||||
"0.0.0.0 нельзя вписать в Telegram: {shown}"
|
||||
);
|
||||
assert!(
|
||||
!shown.starts_with("127.0.0.1"),
|
||||
"127.0.0.1 на другом устройстве означает само устройство: {shown}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn software_rendering_is_requested_by_default() {
|
||||
let vars = software_rendering_vars(false, |_| false);
|
||||
|
||||
+105
-21
@@ -51,31 +51,64 @@ pub fn generate_secret() -> [u8; 16] {
|
||||
secret
|
||||
}
|
||||
|
||||
/// Reuse the secret stored at `path`, creating it if it is missing or unusable.
|
||||
/// Секрет прокси и то, лежит ли он на диске.
|
||||
pub struct StoredSecret {
|
||||
pub value: [u8; 16],
|
||||
/// Ошибка, из-за которой секрет не удалось сохранить.
|
||||
///
|
||||
/// Если она есть, при следующем запуске секрет будет другим, ссылка
|
||||
/// `tg://proxy` перестанет совпадать с сохранённой в Telegram, и Telegram
|
||||
/// скажет «прокси настроен неверно и будет отключён». Раньше запись
|
||||
/// провалившись молчала, и понять причину было невозможно
|
||||
/// (by-sonic/tglock#37).
|
||||
pub write_error: Option<String>,
|
||||
}
|
||||
|
||||
impl StoredSecret {
|
||||
/// Секрет действительно переживёт перезапуск.
|
||||
pub fn is_persistent(&self) -> bool {
|
||||
self.write_error.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Взять секрет из файла, создав его, если файла нет или он испорчен.
|
||||
///
|
||||
/// A daemon needs this: the secret is half of the `tg://proxy` link, so a
|
||||
/// service that invents a new one on every restart silently invalidates every
|
||||
/// client that was already configured.
|
||||
pub fn load_or_create_secret_at(path: &Path) -> [u8; 16] {
|
||||
/// Секрет — половина ссылки `tg://proxy`, поэтому сервис, придумывающий новый
|
||||
/// при каждом старте, отключает всех уже настроенных клиентов.
|
||||
pub fn load_or_create_secret_at(path: &Path) -> StoredSecret {
|
||||
if let Ok(value) = std::fs::read_to_string(path) {
|
||||
if let Some(secret) = parse_secret_hex(value.trim()) {
|
||||
return secret;
|
||||
if let Some(value) = parse_secret_hex(value.trim()) {
|
||||
return StoredSecret {
|
||||
value,
|
||||
write_error: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let secret = generate_secret();
|
||||
let value = generate_secret();
|
||||
let write_error = store_secret(path, &secret_hex(&value))
|
||||
.err()
|
||||
.map(|error| format!("{}: {error}", path.display()));
|
||||
StoredSecret { value, write_error }
|
||||
}
|
||||
|
||||
fn store_secret(path: &Path, value: &str) -> std::io::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
if !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
write_secret_file(path, &secret_hex(&secret));
|
||||
secret
|
||||
write_secret_file(path, value)
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub fn load_or_create_secret() -> [u8; 16] {
|
||||
pub fn load_or_create_secret() -> StoredSecret {
|
||||
match secret_path() {
|
||||
Some(path) => load_or_create_secret_at(&path),
|
||||
None => generate_secret(),
|
||||
None => StoredSecret {
|
||||
value: generate_secret(),
|
||||
write_error: Some("не удалось определить папку для секрета в этой системе".to_owned()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,23 +138,21 @@ fn secret_path() -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn write_secret_file(path: &Path, value: &str) {
|
||||
fn write_secret_file(path: &Path, value: &str) -> std::io::Result<()> {
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.mode(0o600)
|
||||
.open(path)
|
||||
{
|
||||
let _ = file.write_all(value.as_bytes());
|
||||
}
|
||||
.open(path)?;
|
||||
file.write_all(value.as_bytes())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn write_secret_file(path: &Path, value: &str) {
|
||||
let _ = std::fs::write(path, value);
|
||||
fn write_secret_file(path: &Path, value: &str) -> std::io::Result<()> {
|
||||
std::fs::write(path, value)
|
||||
}
|
||||
|
||||
pub fn secret_hex(secret: &[u8; 16]) -> String {
|
||||
@@ -514,6 +545,59 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_write_is_reported_instead_of_swallowed() {
|
||||
// Раньше ошибка записи выбрасывалась, секрет генерировался заново при
|
||||
// каждом запуске, и Telegram говорил «прокси настроен неверно» без
|
||||
// единой подсказки почему (by-sonic/tglock#37).
|
||||
let blocker = std::env::temp_dir().join(format!(
|
||||
"tglock-not-a-dir-{}-{:?}",
|
||||
std::process::id(),
|
||||
std::thread::current().id()
|
||||
));
|
||||
std::fs::write(&blocker, "я файл, а не папка").unwrap();
|
||||
|
||||
// Родитель пути — обычный файл, поэтому создать каталог невозможно.
|
||||
let stored = load_or_create_secret_at(&blocker.join("secret"));
|
||||
|
||||
assert!(
|
||||
!stored.is_persistent(),
|
||||
"неудачная запись обязана быть видна"
|
||||
);
|
||||
let error = stored.write_error.expect("должно быть сообщение об ошибке");
|
||||
assert!(
|
||||
error.contains("secret"),
|
||||
"в сообщении должен быть путь, получено: {error}"
|
||||
);
|
||||
// Секрет всё равно выдан: прокси работает, просто до перезапуска.
|
||||
assert_ne!(stored.value, [0; 16]);
|
||||
|
||||
let _ = std::fs::remove_file(&blocker);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_successful_write_reports_no_error() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"tglock-secret-ok-{}-{:?}",
|
||||
std::process::id(),
|
||||
std::thread::current().id()
|
||||
));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let first = load_or_create_secret_at(&path);
|
||||
assert!(first.is_persistent(), "запись в temp должна удаваться");
|
||||
|
||||
// Второй запуск читает готовый файл и тоже не жалуется.
|
||||
let second = load_or_create_secret_at(&path);
|
||||
assert!(second.is_persistent());
|
||||
assert_eq!(
|
||||
first.value, second.value,
|
||||
"секрет должен переживать перезапуск"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_a_secret_copied_from_a_tg_link() {
|
||||
let expected = [
|
||||
|
||||
+38
-4
@@ -27,12 +27,23 @@ pub struct Stats {
|
||||
pub last_route: AtomicU8,
|
||||
transport: crate::transport::TransportEngine,
|
||||
secret: [u8; 16],
|
||||
/// Почему секрет не удалось сохранить, если не удалось.
|
||||
secret_write_error: Option<String>,
|
||||
shutdown: Mutex<Option<tokio::sync::watch::Sender<bool>>>,
|
||||
}
|
||||
|
||||
impl Stats {
|
||||
pub fn new() -> Arc<Self> {
|
||||
Self::with_secret(initial_secret())
|
||||
Self::with_stored_secret(initial_secret())
|
||||
}
|
||||
|
||||
/// Построить с секретом, про который известно, сохранился он на диск или нет.
|
||||
///
|
||||
/// Если не сохранился, при следующем запуске ссылка `tg://proxy` изменится и
|
||||
/// Telegram скажет «прокси настроен неверно и будет отключён». Раньше это
|
||||
/// происходило молча (by-sonic/tglock#37).
|
||||
pub fn with_stored_secret(stored: crate::mtproto::StoredSecret) -> Arc<Self> {
|
||||
Self::build(stored.value, stored.write_error)
|
||||
}
|
||||
|
||||
/// Build with an explicit proxy secret.
|
||||
@@ -40,6 +51,15 @@ impl Stats {
|
||||
/// A daemon must pin this: the secret is half of the `tg://proxy` link, so
|
||||
/// generating a fresh one on restart breaks every configured client.
|
||||
pub fn with_secret(secret: [u8; 16]) -> Arc<Self> {
|
||||
Self::build(secret, None)
|
||||
}
|
||||
|
||||
/// Сообщение о том, почему секрет не сохранён, если он не сохранён.
|
||||
pub fn secret_write_error(&self) -> Option<&str> {
|
||||
self.secret_write_error.as_deref()
|
||||
}
|
||||
|
||||
fn build(secret: [u8; 16], secret_write_error: Option<String>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
running: AtomicBool::new(false),
|
||||
active: AtomicU32::new(0),
|
||||
@@ -50,6 +70,7 @@ impl Stats {
|
||||
last_route: AtomicU8::new(0),
|
||||
transport: crate::transport::TransportEngine::new(),
|
||||
secret,
|
||||
secret_write_error,
|
||||
shutdown: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
@@ -58,6 +79,16 @@ impl Stats {
|
||||
crate::mtproto::telegram_secret(&self.secret)
|
||||
}
|
||||
|
||||
/// Сколько отдельных маршрутов не ответило.
|
||||
///
|
||||
/// Отличается от `ws_failures`: тот растёт только когда упали все маршруты
|
||||
/// и соединение не состоялось. Этот показывает перебор, который прошёл
|
||||
/// незаметно — например, когда закреплённый адрес мёртв, а запасной
|
||||
/// работает (by-sonic/tglock#32).
|
||||
pub fn route_failures(&self) -> u32 {
|
||||
self.transport.route_failures()
|
||||
}
|
||||
|
||||
pub fn set_worker_domain(&self, domain: &str) {
|
||||
let domains = domain
|
||||
.split([',', ';', ' '])
|
||||
@@ -75,13 +106,16 @@ impl Stats {
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn initial_secret() -> [u8; 16] {
|
||||
fn initial_secret() -> crate::mtproto::StoredSecret {
|
||||
crate::mtproto::load_or_create_secret()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn initial_secret() -> [u8; 16] {
|
||||
crate::mtproto::generate_secret()
|
||||
fn initial_secret() -> crate::mtproto::StoredSecret {
|
||||
crate::mtproto::StoredSecret {
|
||||
value: crate::mtproto::generate_secret(),
|
||||
write_error: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Claim the local port.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -113,6 +114,14 @@ struct HealthState {
|
||||
pub struct TransportEngine {
|
||||
health: Mutex<HealthState>,
|
||||
worker_domains: Mutex<Vec<String>>,
|
||||
/// Сколько раз отдельный маршрут не ответил.
|
||||
///
|
||||
/// Считается отдельно от `Stats::ws_failures`, который растёт только когда
|
||||
/// упали ВСЕ маршруты. Из-за этого диагностика показывала «сбоев 0», пока
|
||||
/// закреплённый адрес был недоступен и каждое холодное соединение молча
|
||||
/// откатывалось на следующий маршрут, тратя на это до восьми секунд
|
||||
/// (by-sonic/tglock#32).
|
||||
route_failures: AtomicU32,
|
||||
#[cfg(test)]
|
||||
forced_routes: Mutex<Vec<Route>>,
|
||||
}
|
||||
@@ -258,7 +267,13 @@ impl TransportEngine {
|
||||
health.preferred.insert(key, route.clone());
|
||||
}
|
||||
|
||||
/// Сколько отдельных маршрутов не ответило за время работы.
|
||||
pub fn route_failures(&self) -> u32 {
|
||||
self.route_failures.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn record_failure(&self, route: &Route) {
|
||||
self.route_failures.fetch_add(1, Ordering::Relaxed);
|
||||
let mut health = self.health.lock().unwrap();
|
||||
let failures = health
|
||||
.routes
|
||||
@@ -656,6 +671,38 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_route_failure_is_counted() {
|
||||
// Диагностика показывала «сбоев 0», пока закреплённый адрес был мёртв и
|
||||
// соединения молча откатывались на запасной. Счётчик маршрутов должен
|
||||
// видеть каждое такое падение.
|
||||
let engine = TransportEngine::new();
|
||||
let routes = routes_for_dc(2, false);
|
||||
assert_eq!(engine.route_failures(), 0);
|
||||
|
||||
engine.record_failure(&routes[0]);
|
||||
assert_eq!(engine.route_failures(), 1);
|
||||
|
||||
engine.record_failure(&routes[0]);
|
||||
engine.record_failure(&routes[1]);
|
||||
assert_eq!(
|
||||
engine.route_failures(),
|
||||
3,
|
||||
"считаются все падения, включая повторные по тому же маршруту"
|
||||
);
|
||||
|
||||
// Успех не обнуляет историю: она нужна, чтобы понять, что маршруты
|
||||
// перебирались, даже когда в итоге всё соединилось.
|
||||
engine.record_success(
|
||||
DcKey {
|
||||
dc: 2,
|
||||
media: false,
|
||||
},
|
||||
&routes[1],
|
||||
);
|
||||
assert_eq!(engine.route_failures(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documented_worker_contract_matches_the_requested_path() {
|
||||
// docs/CLOUDFLARE_WORKER.md promises exactly this shape.
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "TGLock",
|
||||
"version": "2.0.0-beta.6",
|
||||
"version": "2.0.0-beta.8",
|
||||
"identifier": "com.bysonic.tglock",
|
||||
"mainBinaryName": "tglock",
|
||||
"build": {
|
||||
|
||||
+40
@@ -10,8 +10,11 @@ type Status = {
|
||||
dataCenter: number | null;
|
||||
route: string;
|
||||
failures: number;
|
||||
routeFailures: number;
|
||||
uptimeSeconds: number;
|
||||
port: number;
|
||||
/// Адрес для других устройств. Приходит только в LAN-режиме.
|
||||
shareAddress: string | null;
|
||||
logs: LogLine[];
|
||||
};
|
||||
|
||||
@@ -37,8 +40,10 @@ let status: Status = {
|
||||
dataCenter: null,
|
||||
route: "Маршрут ещё не выбран",
|
||||
failures: 0,
|
||||
routeFailures: 0,
|
||||
uptimeSeconds: 0,
|
||||
port: 1080,
|
||||
shareAddress: null,
|
||||
logs: [],
|
||||
};
|
||||
let settings: Settings = { lanMode: false, port: 1080, workerDomain: "" };
|
||||
@@ -145,6 +150,18 @@ function renderHome(): void {
|
||||
<span>порт ${status.port}</span>
|
||||
</div>
|
||||
|
||||
${status.shareAddress ? `
|
||||
<div class="share-card">
|
||||
<div class="share-label">Адрес для других устройств</div>
|
||||
<button id="copy-address" class="share-address" title="Нажмите, чтобы скопировать">
|
||||
${escapeHtml(status.shareAddress)}
|
||||
</button>
|
||||
<div class="share-hint">
|
||||
Впишите его в Telegram на телефоне. Не <code>127.0.0.1</code> — на другом
|
||||
устройстве это означает само устройство.
|
||||
</div>
|
||||
</div>` : ""}
|
||||
|
||||
<nav class="bottom-nav" aria-label="Разделы">
|
||||
<button id="settings-nav" class="nav-button">
|
||||
<span class="nav-icon">${icons.settings}</span>
|
||||
@@ -160,6 +177,7 @@ function renderHome(): void {
|
||||
`, "home-page");
|
||||
|
||||
document.querySelector("#power")?.addEventListener("click", toggleProtection);
|
||||
document.querySelector("#copy-address")?.addEventListener("click", copyShareAddress);
|
||||
document.querySelector("#settings-nav")?.addEventListener("click", () => navigate("settings"));
|
||||
document.querySelector("#diagnostics-nav")?.addEventListener("click", () => navigate("diagnostics"));
|
||||
}
|
||||
@@ -261,8 +279,18 @@ function renderDiagnostics(): void {
|
||||
<span>Время работы</span>
|
||||
<strong class="time">${formatUptime(status.uptimeSeconds)}</strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Падений маршрутов</span>
|
||||
<strong>${status.routeFailures}</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p class="field-hint">
|
||||
«Падений маршрутов» больше нуля при работающем Telegram — это норма:
|
||||
значит закреплённый адрес недоступен и подключение идёт через запасной.
|
||||
Число в багрепорте помогает понять, что именно перебиралось.
|
||||
</p>
|
||||
|
||||
<div class="log-panel">
|
||||
<div class="log-heading">
|
||||
<span>Последние события</span>
|
||||
@@ -356,6 +384,18 @@ async function saveSettings(event: Event): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyShareAddress(): Promise<void> {
|
||||
const address = status.shareAddress;
|
||||
if (!address) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(address);
|
||||
showToast("Адрес скопирован");
|
||||
} catch {
|
||||
// Буфер обмена может быть недоступен — адрес и так виден на экране.
|
||||
showToast("Скопируйте адрес вручную", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message: string, error = false): void {
|
||||
window.clearTimeout(toastTimer);
|
||||
window.requestAnimationFrame(() => {
|
||||
|
||||
@@ -847,3 +847,53 @@ input:disabled {
|
||||
0 19px 52px rgba(9, 182, 133, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* Адрес для подключения других устройств. Показывается только в LAN-режиме:
|
||||
люди искали его в интерфейсе, не находили и вписывали 127.0.0.1, который на
|
||||
телефоне означает сам телефон (by-sonic/tglock#36). */
|
||||
.share-card {
|
||||
margin-top: 14px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: rgba(42, 171, 238, 0.08);
|
||||
border: 1px solid rgba(42, 171, 238, 0.24);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.share-label {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text-muted, #8b949e);
|
||||
}
|
||||
|
||||
.share-address {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 6px 0 8px;
|
||||
padding: 6px 8px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: var(--accent, #2aabee);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
|
||||
.share-address:hover,
|
||||
.share-address:focus-visible {
|
||||
background: rgba(42, 171, 238, 0.14);
|
||||
}
|
||||
|
||||
.share-hint {
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-muted, #8b949e);
|
||||
}
|
||||
|
||||
.share-hint code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user