mirror of
https://github.com/by-sonic/tglock.git
synced 2026-09-19 00:38:33 +03:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cb72e4acc | |||
| 60ae065938 | |||
| 1a56e1c96e |
@@ -64,13 +64,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tagName: v__VERSION__
|
tagName: v__VERSION__
|
||||||
releaseName: TGLock v__VERSION__
|
releaseName: TGLock v__VERSION__
|
||||||
|
# Только инструкция по скачиванию: она одна и та же для любой версии.
|
||||||
|
# Список изменений дописывается в release notes при выпуске — иначе
|
||||||
|
# выходит как в бетах 1-7, где каждая несла один и тот же текст «новый
|
||||||
|
# TGLock, полностью обновлённый интерфейс» и понять, что поменялось,
|
||||||
|
# было невозможно.
|
||||||
releaseBody: |
|
releaseBody: |
|
||||||
Новый TGLock: адаптивный MTProto/WebSocket-транспорт и полностью обновлённый интерфейс на Tauri 2.
|
## Что скачивать
|
||||||
|
|
||||||
- macOS: скачайте универсальный `.dmg` или `.app.tar.gz`
|
- **Windows** — `.exe`, установщик
|
||||||
- Windows: скачайте `.exe` установщик
|
- **macOS** — универсальный `.dmg` (или `.app.tar.gz`)
|
||||||
- Linux: скачайте `.AppImage` или `.deb`
|
- **Linux** — `.AppImage` или `.deb`
|
||||||
- Сервер или машина без монитора: скачайте `tglock-cli-*` — там нет графического интерфейса
|
- **Сервер, контейнер, машина без монитора** — `tglock-cli-*`: то же самое, но без графического интерфейса
|
||||||
|
|
||||||
|
Если Windows ругается на файл — [почему так и что с этим делать](https://github.com/by-sonic/tglock#-антивирус-ругается-smartscreen-предупреждает-virustotal-показывает-детекты).
|
||||||
releaseDraft: false
|
releaseDraft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
args: ${{ matrix.args }}
|
args: ${{ matrix.args }}
|
||||||
|
|||||||
Generated
+1
-1
@@ -3553,7 +3553,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tglock"
|
name = "tglock"
|
||||||
version = "2.0.0-beta.7"
|
version = "2.0.0-beta.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"cipher",
|
"cipher",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tglock"
|
name = "tglock"
|
||||||
version = "2.0.0-beta.7"
|
version = "2.0.0-beta.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
description = "Telegram unblock via local WebSocket tunnel"
|
description = "Telegram unblock via local WebSocket tunnel"
|
||||||
|
|||||||
@@ -155,7 +155,11 @@ Telegram → Настройки → **Продвинутые** → Тип сое
|
|||||||
|
|
||||||
В окне TGLock включи галочку **LAN** — приложение начнёт слушать на `0.0.0.0`. Все устройства в домашней сети (телефон, планшет, ноутбук, телевизор) смогут подключиться к `<IP-компьютера>:1080` и тоже получить рабочий 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-роутером».
|
Удобно, если дома один компьютер всегда включён — он становится «домашним Telegram-роутером».
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tglock-ui",
|
"name": "tglock-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.0-beta.7",
|
"version": "2.0.0-beta.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 1420",
|
"dev": "vite --port 1420",
|
||||||
|
|||||||
+57
@@ -49,6 +49,13 @@ struct StatusSnapshot {
|
|||||||
route_failures: u32,
|
route_failures: u32,
|
||||||
uptime_seconds: u64,
|
uptime_seconds: u64,
|
||||||
port: u16,
|
port: u16,
|
||||||
|
/// Адрес, который нужно вписать в Telegram на другом устройстве.
|
||||||
|
///
|
||||||
|
/// В LAN-режиме это адрес этого компьютера в локальной сети. Люди искали
|
||||||
|
/// его в интерфейсе и не находили: вписывали `127.0.0.1`, который на
|
||||||
|
/// телефоне или в эмуляторе означает само устройство, и подключение не
|
||||||
|
/// работало (by-sonic/tglock#36).
|
||||||
|
share_address: Option<String>,
|
||||||
logs: Vec<LogLine>,
|
logs: Vec<LogLine>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +63,10 @@ struct AppState {
|
|||||||
stats: Arc<proxy::Stats>,
|
stats: Arc<proxy::Stats>,
|
||||||
settings: Mutex<Settings>,
|
settings: Mutex<Settings>,
|
||||||
active_port: Mutex<u16>,
|
active_port: Mutex<u16>,
|
||||||
|
/// Слушатель работающего прокси. Нужен, чтобы показать адрес для других
|
||||||
|
/// устройств именно тот, на котором прокси реально поднят, а не тот, что
|
||||||
|
/// сейчас выбран в настройках.
|
||||||
|
active_listen: Mutex<Option<ListenConfig>>,
|
||||||
started_at: Mutex<Option<Instant>>,
|
started_at: Mutex<Option<Instant>>,
|
||||||
logs: Arc<Mutex<Vec<LogLine>>>,
|
logs: Arc<Mutex<Vec<LogLine>>>,
|
||||||
settings_path: PathBuf,
|
settings_path: PathBuf,
|
||||||
@@ -71,6 +82,7 @@ impl AppState {
|
|||||||
stats: proxy::Stats::new(),
|
stats: proxy::Stats::new(),
|
||||||
settings: Mutex::new(settings),
|
settings: Mutex::new(settings),
|
||||||
active_port: Mutex::new(proxy::DEFAULT_PORT),
|
active_port: Mutex::new(proxy::DEFAULT_PORT),
|
||||||
|
active_listen: Mutex::new(None),
|
||||||
started_at: Mutex::new(None),
|
started_at: Mutex::new(None),
|
||||||
logs: Arc::new(Mutex::new(Vec::new())),
|
logs: Arc::new(Mutex::new(Vec::new())),
|
||||||
settings_path,
|
settings_path,
|
||||||
@@ -106,6 +118,7 @@ impl AppState {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.map_or(0, |started| started.elapsed().as_secs()),
|
.map_or(0, |started| started.elapsed().as_secs()),
|
||||||
port: *self.active_port.lock().unwrap(),
|
port: *self.active_port.lock().unwrap(),
|
||||||
|
share_address: share_address(*self.active_listen.lock().unwrap()),
|
||||||
logs: self.logs.lock().unwrap().clone(),
|
logs: self.logs.lock().unwrap().clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,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 {
|
fn current_time() -> String {
|
||||||
let seconds = SystemTime::now()
|
let seconds = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -177,6 +202,8 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
|||||||
ListenConfig::loopback(settings.port)
|
ListenConfig::loopback(settings.port)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
*state.active_listen.lock().unwrap() = Some(listen);
|
||||||
|
|
||||||
let stats = state.stats.clone();
|
let stats = state.stats.clone();
|
||||||
let logs = state.logs.clone();
|
let logs = state.logs.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
@@ -194,6 +221,7 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
|||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(220));
|
std::thread::sleep(std::time::Duration::from_millis(220));
|
||||||
if !state.stats.running.load(Ordering::SeqCst) {
|
if !state.stats.running.load(Ordering::SeqCst) {
|
||||||
|
*state.active_listen.lock().unwrap() = None;
|
||||||
*state.started_at.lock().unwrap() = None;
|
*state.started_at.lock().unwrap() = None;
|
||||||
return Err(state
|
return Err(state
|
||||||
.logs
|
.logs
|
||||||
@@ -213,6 +241,7 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn stop_proxy(state: State<'_, AppState>) -> StatusSnapshot {
|
fn stop_proxy(state: State<'_, AppState>) -> StatusSnapshot {
|
||||||
state.stats.stop();
|
state.stats.stop();
|
||||||
|
*state.active_listen.lock().unwrap() = None;
|
||||||
*state.started_at.lock().unwrap() = None;
|
*state.started_at.lock().unwrap() = None;
|
||||||
state.log("Защита выключена", false);
|
state.log("Защита выключена", false);
|
||||||
state.snapshot()
|
state.snapshot()
|
||||||
@@ -311,6 +340,34 @@ fn main() {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn software_rendering_is_requested_by_default() {
|
fn software_rendering_is_requested_by_default() {
|
||||||
let vars = software_rendering_vars(false, |_| false);
|
let vars = software_rendering_vars(false, |_| false);
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "TGLock",
|
"productName": "TGLock",
|
||||||
"version": "2.0.0-beta.7",
|
"version": "2.0.0-beta.8",
|
||||||
"identifier": "com.bysonic.tglock",
|
"identifier": "com.bysonic.tglock",
|
||||||
"mainBinaryName": "tglock",
|
"mainBinaryName": "tglock",
|
||||||
"build": {
|
"build": {
|
||||||
|
|||||||
+28
@@ -13,6 +13,8 @@ type Status = {
|
|||||||
routeFailures: number;
|
routeFailures: number;
|
||||||
uptimeSeconds: number;
|
uptimeSeconds: number;
|
||||||
port: number;
|
port: number;
|
||||||
|
/// Адрес для других устройств. Приходит только в LAN-режиме.
|
||||||
|
shareAddress: string | null;
|
||||||
logs: LogLine[];
|
logs: LogLine[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ let status: Status = {
|
|||||||
routeFailures: 0,
|
routeFailures: 0,
|
||||||
uptimeSeconds: 0,
|
uptimeSeconds: 0,
|
||||||
port: 1080,
|
port: 1080,
|
||||||
|
shareAddress: null,
|
||||||
logs: [],
|
logs: [],
|
||||||
};
|
};
|
||||||
let settings: Settings = { lanMode: false, port: 1080, workerDomain: "" };
|
let settings: Settings = { lanMode: false, port: 1080, workerDomain: "" };
|
||||||
@@ -147,6 +150,18 @@ function renderHome(): void {
|
|||||||
<span>порт ${status.port}</span>
|
<span>порт ${status.port}</span>
|
||||||
</div>
|
</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="Разделы">
|
<nav class="bottom-nav" aria-label="Разделы">
|
||||||
<button id="settings-nav" class="nav-button">
|
<button id="settings-nav" class="nav-button">
|
||||||
<span class="nav-icon">${icons.settings}</span>
|
<span class="nav-icon">${icons.settings}</span>
|
||||||
@@ -162,6 +177,7 @@ function renderHome(): void {
|
|||||||
`, "home-page");
|
`, "home-page");
|
||||||
|
|
||||||
document.querySelector("#power")?.addEventListener("click", toggleProtection);
|
document.querySelector("#power")?.addEventListener("click", toggleProtection);
|
||||||
|
document.querySelector("#copy-address")?.addEventListener("click", copyShareAddress);
|
||||||
document.querySelector("#settings-nav")?.addEventListener("click", () => navigate("settings"));
|
document.querySelector("#settings-nav")?.addEventListener("click", () => navigate("settings"));
|
||||||
document.querySelector("#diagnostics-nav")?.addEventListener("click", () => navigate("diagnostics"));
|
document.querySelector("#diagnostics-nav")?.addEventListener("click", () => navigate("diagnostics"));
|
||||||
}
|
}
|
||||||
@@ -368,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 {
|
function showToast(message: string, error = false): void {
|
||||||
window.clearTimeout(toastTimer);
|
window.clearTimeout(toastTimer);
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
|
|||||||
@@ -847,3 +847,53 @@ input:disabled {
|
|||||||
0 19px 52px rgba(9, 182, 133, 0.4);
|
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