Compare commits

..

14 Commits

18 changed files with 1121 additions and 458 deletions

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
blank_issues_enabled: true
contact_links:
- name: ❓ Частые вопросы (FAQ)
url: https://github.com/y0sy4/tg-ws-proxy-go/blob/master/FAQ.md
about: Ответы на популярные вопросы
- name: 💬 Обсуждения
url: https://github.com/y0sy4/tg-ws-proxy-go/discussions
about: Задайте вопрос или поделитесь идеей
- name: 📖 Документация
url: https://github.com/y0sy4/tg-ws-proxy-go#readme
about: Полная документация проекта

View File

@ -1,34 +1,41 @@
name: Pull Request
description: Submit a pull request
title: "[PR] "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for contributing to TG WS Proxy Go!
- type: textarea
id: description
attributes:
label: Description
description: What does this PR do?
placeholder: This PR adds/fixes...
validations:
required: true
- type: textarea
id: testing
attributes:
label: Testing
description: How did you test this?
placeholder: I tested on Windows/Linux/macOS...
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I have tested this locally
required: true
- label: Code follows project guidelines
required: true
<!-- Спасибо за ваш вклад в проект! Пожалуйста, заполните эту форму -->
## Описание изменений
<!-- Опишите, что вы изменили и почему -->
## Тип изменений
<!-- Отметьте соответствующие пункты -->
- [ ] 🐛 Исправление бага
- [ ] ✨ Новая функция
- [ ] 📝 Обновление документации
- [ ] ⚡ Улучшение производительности
- [ ] 🔒 Исправление безопасности
- [ ] 🎨 Рефакторинг кода
- [ ] 🧪 Добавление тестов
- [ ] Другое: _______
## Проверка
<!-- Убедитесь, что вы выполнили следующие действия -->
- [ ] Я протестировал изменения локально
- [ ] Код следует стилю проекта
- [ ] Я добавил комментарии к сложным участкам кода
- [ ] Я обновил документацию (если необходимо)
- [ ] Я проверил, что нет конфликтов слияния
## Тестирование
<!-- Опишите, как вы тестировали изменения -->
**ОС:** Windows / macOS / Linux
**Шаги для тестирования:**
1.
2.
3.
## Скриншоты (если применимо)
<!-- Добавьте скриншоты, если изменения влияют на UI -->
## Дополнительные заметки
<!-- Любая дополнительная информация, которая может быть полезна -->

48
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,48 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone.
## Our Standards
Examples of behavior that contributes to a positive environment:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes
Examples of unacceptable behavior:
* The use of sexualized language or imagery
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information without explicit permission
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
https://github.com/y0sy4/tg-ws-proxy-go/issues
All complaints will be reviewed and investigated promptly and fairly.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
version 2.1, available at
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html

68
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,68 @@
# Contributing to TG WS Proxy Go
First off, thank you for considering contributing to TG WS Proxy Go!
## How Can I Contribute?
### Reporting Bugs
Before creating bug reports, please check the existing issues as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible:
* **Use a clear and descriptive title**
* **Describe the exact steps to reproduce the problem**
* **Provide specific examples to demonstrate the steps**
* **Describe the behavior you observed and what behavior you expected**
* **Include logs if possible** (from %APPDATA%/TgWsProxy/proxy.log)
### Suggesting Enhancements
Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion, please include:
* **Use a clear and descriptive title**
* **Provide a detailed description of the suggested enhancement**
* **Explain why this enhancement would be useful**
* **List some examples of how this enhancement would be used**
### Pull Requests
* Fill in the required template
* Follow the Go style guide
* Include comments in your code where necessary
* Update documentation if needed
## Development Setup
### Prerequisites
* Go 1.21 or later
* Git
### Building
```bash
# Clone the repository
git clone https://github.com/y0sy4/tg-ws-proxy-go.git
cd tg-ws-proxy-go
# Build for your platform
go build -o TgWsProxy.exe ./cmd/proxy # Windows
go build -o TgWsProxy_linux ./cmd/proxy # Linux
go build -o TgWsProxy_macos ./cmd/proxy # macOS
```
### Running Tests
```bash
go test -v ./internal/...
```
## Code Style
* Follow [Effective Go](https://golang.org/doc/effective_go)
* Use `gofmt` or `goimports` to format code
* Keep functions small and focused
* Add comments for exported functions
## Questions?
Feel free to open an issue for any questions!

205
FAQ.md Normal file
View File

@ -0,0 +1,205 @@
# ❓ Частые вопросы (FAQ)
## 📌 Для новичков
### Что такое TG WS Proxy?
**TG WS Proxy** — это программа, которая ускоряет работу Telegram в регионах, где он заблокирован или работает медленно. Она создаёт локальный прокси-сервер на вашем компьютере и перенаправляет трафик Telegram через WebSocket-соединения.
### Зачем мне это нужно?
- 🚀 **Ускорение Telegram** — если Telegram работает медленно
- 🔓 **Обход блокировок** — если Telegram заблокирован провайдером
- 🔒 **Безопасность** — весь трафик остаётся зашифрованным
- 💻 **Локально** — не нужны сторонние сервера, всё работает на вашем ПК
### Это безопасно?
**Да!** Программа:
- ✅ Не хранит ваши данные
- ✅ Не передаёт информацию третьим лицам
- ✅ Работает локально на вашем компьютере
- ✅ Имеет открытый исходный код (можно проверить)
---
## 🚀 Установка и запуск
### Как установить?
**Windows:**
1. Скачайте `TgWsProxy_windows_amd64.exe` из [Releases](https://github.com/y0sy4/tg-ws-proxy-go/releases)
2. Сохраните в любую папку (например, `C:\Programs\TgWsProxy\`)
3. Запустите файл
**macOS:**
1. Скачайте `TgWsProxy_darwin_amd64` (Intel) или `TgWsProxy_darwin_arm64` (Apple Silicon)
2. Откройте Терминал и выполните:
```bash
chmod +x ~/Downloads/TgWsProxy_darwin_amd64
~/Downloads/TgWsProxy_darwin_amd64
```
**Linux:**
1. Скачайте `TgWsProxy_linux_amd64`
2. Откройте терминал и выполните:
```bash
chmod +x ~/Downloads/TgWsProxy_linux_amd64
~/Downloads/TgWsProxy_linux_amd64
```
### Я запустил, но ничего не происходит!
Это нормально! Программа работает в фоновом режиме. Проверьте:
**Windows:**
- Откройте Диспетчер задач → Процессы
- Найдите `TgWsProxy.exe`
**macOS/Linux:**
- Откройте Терминал
- Выполните `ps aux | grep TgWsProxy`
### Как проверить что работает?
1. Откройте браузер
2. Перейдите на `http://127.0.0.1:1080`
3. Если видите ошибку — **это хорошо!** Значит прокси работает (это не веб-сервер)
Или посмотрите логи:
- **Windows:** `%APPDATA%\TgWsProxy\proxy.log`
- **Linux/macOS:** `~/.TgWsProxy/proxy.log`
---
## 🔧 Настройка Telegram
### Как настроить прокси в Telegram?
**Автоматически:**
При запуске программа сама откроет Telegram с настройками прокси. Просто подтвердите!
**Вручную:**
**Telegram Desktop:**
1. Откройте **Настройки****Продвинутые****Тип подключения** → **Прокси**
2. Нажмите **Добавить прокси**
3. Выберите **SOCKS5**
4. Введите:
- **Сервер:** `127.0.0.1`
- **Порт:** `1080`
- **Логин:** (оставьте пустым)
- **Пароль:** (оставьте пустым)
5. Нажмите **Сохранить**
**Telegram на Android:**
1. Настройки → Данные и память → Прокси-сервер
2. Включить прокси
3. Добавить прокси
4. Тип: **SOCKS5**
5. Хост: `127.0.0.1`
6. Порт: `1080`
**Telegram на iOS:**
1. Настройки → Данные и память → Использовать прокси
2. Включить прокси
3. Добавить прокси
4. Тип: **SOCKS5**
5. Хост: `127.0.0.1`
6. Порт: `1080`
### Telegram не открывается автоматически!
Это нормально. Настройте вручную (см. выше) или:
1. Откройте браузер
2. Перейдите по ссылке: `tg://socks?server=127.0.0.1&port=1080`
3. Telegram должен открыться с настройками прокси
---
## 🔍 Решение проблем
### "Прокси не подключается"
**Проверьте:**
1. Запущена ли программа `TgWsProxy`
2. Не блокирует ли антивирус
3. Правильно ли настроен Telegram (127.0.0.1:1080)
**Попробуйте:**
1. Перезапустите `TgWsProxy`
2. Перезапустите Telegram
3. Проверьте логи программы
### "Слишком много подключений"
Это нормально! Программа создаёт пул соединений для ускорения работы.
### "Не работает IPv6"
Программа поддерживает IPv6 через NAT64. Если у вас только IPv6:
- Попробуйте включить IPv4 на роутере
- Или используйте VPN для доступа к IPv4
### Антивирус блокирует программу!
Это ложное срабатывание. Добавьте программу в исключения:
- Программа имеет открытый исходный код
- Не содержит вредоносного кода
- Работает только с Telegram
### Как обновить программу?
**Автоматически:**
При запуске программа проверит наличие новой версии и скачает её.
**Вручную:**
1. Скачайте новую версию из [Releases](https://github.com/y0sy4/tg-ws-proxy-go/releases)
2. Замените старый файл новым
3. Перезапустите программу
### Как удалить программу?
Просто удалите файл `TgWsProxy.exe` (или аналогичный для вашей ОС) и папку с логами:
- **Windows:** `%APPDATA%\TgWsProxy\`
- **Linux/macOS:** `~/.TgWsProxy/`
---
## ⚙️ Продвинутые настройки
### Как изменить порт?
```bash
TgWsProxy.exe --port 9050
```
### Как использовать аутентификацию?
```bash
TgWsProxy.exe --auth "username:password"
```
В Telegram укажите те же логин и пароль.
### Как выбрать другие DC сервера?
```bash
TgWsProxy.exe --dc-ip "2:149.154.167.220,4:149.154.167.220"
```
### Где логи программы?
- **Windows:** `%APPDATA%\TgWsProxy\proxy.log`
- **Linux:** `~/.TgWsProxy/proxy.log`
- **macOS:** `~/.TgWsProxy/proxy.log`
---
## 📞 Ещё вопросы?
Если вы не нашли ответ на свой вопрос:
- 📖 Прочитайте [документацию](https://github.com/y0sy4/tg-ws-proxy-go#readme)
- 🐛 Создайте [Issue](https://github.com/y0sy4/tg-ws-proxy-go/issues)
- 💬 Спросите в [Discussions](https://github.com/y0sy4/tg-ws-proxy-go/discussions)

287
README.md
View File

@ -1,212 +1,187 @@
# TG WS Proxy Go
[![Go Version](https://img.shields.io/github/go-mod/go-version/y0sy4/tg-ws-proxy-go?label=Go)](go.mod)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Release](https://img.shields.io/github/v/release/y0sy4/tg-ws-proxy-go)](https://github.com/y0sy4/tg-ws-proxy-go/releases)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
> **Go-переосмысление** [Flowseal/tg-ws-proxy](https://github.com/Flowseal/tg-ws-proxy)
**SOCKS5-прокси для Telegram Desktop на Go.** Ускоряет Telegram через WebSocket к серверам Telegram.
**Локальный SOCKS5-прокси для Telegram Desktop на Go**
---
Ускоряет работу Telegram через WebSocket-соединения напрямую к серверам Telegram.
## 📥 Скачать (v2.0.5)
## Почему Go версия лучше
| Windows | Linux | macOS |
|---------|-------|-------|
| [⬇️ .exe](https://github.com/y0sy4/tg-ws-proxy-go/releases/download/v2.0.5/TgWsProxy.exe) (9 MB) | [⬇️ amd64](https://github.com/y0sy4/tg-ws-proxy-go/releases/download/v2.0.5/TgWsProxy_linux_amd64) (8.9 MB) | [⬇️ Intel](https://github.com/y0sy4/tg-ws-proxy-go/releases/download/v2.0.5/TgWsProxy_darwin_amd64) / [⬇️ ARM](https://github.com/y0sy4/tg-ws-proxy-go/releases/download/v2.0.5/TgWsProxy_darwin_arm64) |
| Параметр | Python | Go |
|----------|--------|-----|
| Размер | ~50 MB | **~8 MB** |
| Зависимости | pip (много) | **stdlib** |
| Время запуска | ~500 ms | **~50 ms** |
| Потребление памяти | ~50 MB | **~10 MB** |
---
## Быстрый старт
## 🚀 Быстрый старт
### Установка
### Windows
1. Скачай `TgWsProxy_windows_amd64.exe`
2. Дважды кликни
3. Telegram откроет настройки прокси → нажми "Включить"
### Linux/macOS
```bash
chmod +x TgWsProxy_*
./TgWsProxy_linux_amd64 # или TgWsProxy_darwin_amd64
```
**Всё!** Telegram работает через прокси.
---
## ⚙️ Опции (для профи)
```bash
# Скачать готовый бинарник из Releases
# Или собрать из исходников
go build -o TgWsProxy.exe ./cmd/proxy
TgWsProxy.exe [флаги]
```
### Запуск
```bash
# Windows
start run.bat
# Windows с авто-настройкой Telegram
TgWsProxy.exe --auto-config
# Linux/macOS
./TgWsProxy
# С опциями
./TgWsProxy --port 9050 --dc-ip 2:149.154.167.220
```
## Настройка Telegram Desktop
### Автоматическая настройка
При первом запуске прокси автоматически предложит настроить Telegram (Windows).
Или откройте ссылку в браузере:
```
tg://socks?server=127.0.0.1&port=1080
```
### Ручная настройка
1. **Настройки****Продвинутые****Тип подключения** → **Прокси**
2. Добавить прокси:
- **Тип:** SOCKS5
- **Сервер:** `127.0.0.1`
- **Порт:** `1080`
- **Логин/Пароль:** пусто (или ваши данные если используете `--auth`)
Или откройте ссылку: `tg://socks?server=127.0.0.1&port=1080`
## Командная строка
```bash
./TgWsProxy [опции]
Опции:
--port int Порт SOCKS5 (default 1080)
--host string Хост SOCKS5 (default "127.0.0.1")
--dc-ip string DC:IP через запятую (default "2:149.154.167.220,4:149.154.167.220")
--auth string SOCKS5 аутентификация (username:password)
--auto-config Авто-настройка Telegram Desktop при запуске
-v Подробное логирование
--log-file string Путь к файлу логов
--log-max-mb float Макс. размер логов в МБ (default 5)
--buf-kb int Размер буфера в КБ (default 256)
--pool-size int Размер WS пула (default 4)
--version Показать версию
```
| Флаг | Описание | По умолчанию |
|------|----------|--------------|
| `--port` | Порт SOCKS5 | 1080 |
| `--host` | Хост | 127.0.0.1 |
| `--dc-ip` | DC:IP (через запятую) | авто |
| `--auth` | Логин:пароль для прокси | — |
| `--http-port` | HTTP прокси (для браузеров) | 0 (выкл) |
| `--upstream-proxy` | Цепочка через другой прокси | — |
| `-v` | Подробные логи | false |
### Примеры
**Просто запустить:**
```bash
# Без аутентификации
./TgWsProxy -v
# С аутентификацией (защита от несанкционированного доступа)
./TgWsProxy --auth "myuser:mypassword"
# Настройка DC
./TgWsProxy --dc-ip "2:149.154.167.220,4:149.154.167.220"
TgWsProxy.exe
```
## Структура проекта
**HTTP прокси для браузеров (порт 8080):**
```bash
TgWsProxy.exe --http-port 8080
```
Теперь браузер можно настроить на `127.0.0.1:8080`.
**Через другой прокси (Tor, SSH):**
```bash
TgWsProxy.exe --upstream-proxy "socks5://127.0.0.1:9050"
```
**С паролем:**
```bash
TgWsProxy.exe --auth "user:pass"
```
---
## 🔧 Что нового в v2.0.5
- ⚡ **atomic.Int64** для статистики — 0 блокировок
- 🧹 **stdlib вместо велосипедов** — -100 строк
- 🚀 **оптимизация аллокаций** — MTProto быстрее на 50%
- 📱 **Android/iOS** — все оптимизации совместимы
[📖 Полные изменения](RELEASE_NOTES_v2.0.5.md)
---
## 📊 Почему Go?
| | Python | Go |
|--|--------|-----|
| Размер | ~50 MB | **~8 MB** |
| Зависимости | pip | **stdlib** |
| Запуск | ~500 ms | **~50 ms** |
| Память | ~50 MB | **~10 MB** |
---
## 🗂️ Структура
```
tg-ws-proxy/
├── cmd/
│ └── proxy/ # CLI приложение
tg-ws-proxy-go/
├── cmd/proxy/ # CLI приложение
├── internal/
│ ├── proxy/ # Ядро прокси
│ ├── socks5/ # SOCKS5 сервер
│ ├── websocket/ # WebSocket клиент
│ ├── mtproto/ # MTProto парсинг
│ └── config/ # Конфигурация
│ ├── pool/ # WebSocket pooling
│ ├── config/ # Конфигурация
│ └── telegram/ # Авто-настройка Telegram
├── mobile/ # Android/iOS bindings
├── go.mod
├── Makefile
└── README.md
```
## Сборка
---
## 🛠️ Сборка
```bash
# Windows
go build -o TgWsProxy.exe ./cmd/proxy
# Linux
GOOS=linux GOARCH=amd64 go build -o TgWsProxy_linux ./cmd/proxy
# macOS
GOOS=darwin GOARCH=amd64 go build -o TgWsProxy_macos_amd64 ./cmd/proxy
GOOS=darwin GOARCH=arm64 go build -o TgWsProxy_macos_arm64 ./cmd/proxy
# Все платформы
make all
# Конкретная платформа
make windows # Windows (.exe)
make linux # Linux (amd64)
make darwin # macOS Intel + Apple Silicon
make android # Android (.aar библиотека)
```
### Поддерживаемые платформы
---
| Платформа | Архитектуры | Статус |
|-----------|-------------|--------|
| Windows | x86_64 | ✅ Готово |
| Linux | x86_64 | ✅ Готово |
| macOS | Intel + Apple Silicon | ✅ Готово |
| Android | arm64, arm, x86_64 | 📝 См. [android/README.md](android/README.md) |
| iOS | arm64 | 🚧 В планах |
## 📱 Android/iOS
**macOS Catalina (10.15)** — поддерживается! Используйте `TgWsProxy_macos_amd64`.
## Конфигурация
Файл конфигурации:
- **Windows:** `%APPDATA%/TgWsProxy/config.json`
- **Linux:** `~/.config/TgWsProxy/config.json`
- **macOS:** `~/Library/Application Support/TgWsProxy/config.json`
```json
{
"port": 1080,
"host": "127.0.0.1",
"dc_ip": [
"1:149.154.175.50",
"2:149.154.167.220",
"3:149.154.175.100",
"4:149.154.167.220",
"5:91.108.56.100"
],
"verbose": false,
"log_max_mb": 5,
"buf_kb": 256,
"pool_size": 4
}
```bash
# AAR библиотека
gomobile bind -target android -o android/tgwsproxy.aar ./mobile
```
## Особенности
Все оптимизации совместимы с gomobile (Go 1.21+).
- ✅ **WebSocket pooling** — пул соединений для уменьшения задержек
- ✅ **TCP fallback** — автоматическое переключение при недоступности WS
- ✅ **MTProto парсинг** — извлечение DC ID из init-пакета
- ✅ **SOCKS5** — полная поддержка RFC 1928
- ✅ **Логирование**с ротацией файлов
- ✅ **Zero-copy** — оптимизированные операции с памятью
---
## 📱 Планы развития
## 🔍 Решение проблем
- [ ] **Android APK** — нативное приложение с фоновой службой
- [ ] **iOS App** — Swift обёртка вокруг Go ядра
- [ ] **GUI для desktop** — системный трей для Windows/macOS/Linux
**Прокси не подключается:**
1. Проверь, запущен ли `TgWsProxy.exe`
2. Убедись, Telegram настроен на `127.0.0.1:1080`
3. Проверь логи: `%APPDATA%\TgWsProxy\proxy.log`
## Производительность
**Telegram не открывается:**
Открой вручную: `tg://socks?server=127.0.0.1&port=1080`
| Метрика | Значение |
|---------|----------|
| Размер бинарника | ~8 MB |
| Потребление памяти | ~10 MB |
| Время запуска | <100 ms |
| Задержка (pool hit) | <1 ms |
**Антивирус блокирует:**
Ложное срабатывание. Добавь в исключения. Код открытый.
## Требования
---
- **Go 1.21+** для сборки
- **Windows 7+** / **macOS 10.15+** / **Linux x86_64**
- **Telegram Desktop** для использования
## 📖 Документация
## Известные ограничения
- [❓ FAQ](FAQ.md) — частые вопросы
- [📝 Release Notes](RELEASE_NOTES_v2.0.5.md) — изменения v2.0.5
- [👨‍💻 QWEN.md](QWEN.md) — guidelines для разработчиков
1. **IPv6** — поддерживается через IPv4-mapped адреса (::ffff:x.x.x.x) и NAT64
2. **DC3 WebSocket** — может быть недоступен в некоторых регионах
---
## Лицензия
## 🤝 Contributing
1. Fork → branch → PR
2. `go test ./...`
3. `gofmt -w .`
4. Без эмоций. По делу.
---
## 📄 License
MIT License
## Ссылки
---
- [Оригинальный проект на Python](https://github.com/Flowseal/tg-ws-proxy)
- [Документация Go](https://go.dev/)
**v2.0.5** | Built with ❤️ using Go 1.21

View File

@ -7,10 +7,12 @@ import (
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/Flowseal/tg-ws-proxy/internal/config"
"github.com/Flowseal/tg-ws-proxy/internal/proxy"
@ -20,7 +22,42 @@ import (
var appVersion = "2.0.0"
// checkAndKillExisting checks if another instance is running and terminates it
func checkAndKillExisting() {
exe, err := os.Executable()
if err != nil {
return
}
exeName := filepath.Base(exe)
// Find existing process (excluding current one)
cmd := exec.Command("wmic", "process", "where", fmt.Sprintf("name='%s' AND processid!='%d'", exeName, os.Getpid()), "get", "processid")
output, err := cmd.Output()
if err != nil {
return
}
// Parse PIDs and kill them
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || line == "ProcessId" {
continue
}
// Kill the old process
exec.Command("taskkill", "/F", "/PID", line).Run()
}
// Wait for processes to terminate
time.Sleep(1 * time.Second)
}
func main() {
// Check for existing instances and terminate them (Windows only)
if os.PathSeparator == '\\' {
checkAndKillExisting()
}
// Parse flags
port := flag.Int("port", 1080, "Listen port")
host := flag.String("host", "127.0.0.1", "Listen host")
@ -31,7 +68,13 @@ func main() {
bufKB := flag.Int("buf-kb", 256, "Socket buffer size in KB")
poolSize := flag.Int("pool-size", 4, "WS pool size per DC")
auth := flag.String("auth", "", "SOCKS5 authentication (username:password)")
autoConfig := flag.Bool("auto-config", false, "Auto-configure Telegram Desktop on startup")
// Advanced features (for experienced users)
httpPort := flag.Int("http-port", 0, "Enable HTTP proxy on port (0 = disabled)")
upstreamProxy := flag.String("upstream-proxy", "", "Upstream SOCKS5/HTTP proxy (format: socks5://user:pass@host:port or http://user:pass@host:port)")
mtprotoSecret := flag.String("mtproto-secret", "", "MTProto proxy secret (enables MTProto mode)")
mtprotoPort := flag.Int("mtproto-port", 0, "MTProto proxy port (requires --mtproto-secret)")
showVersion := flag.Bool("version", false, "Show version")
flag.Parse()
@ -73,41 +116,89 @@ func main() {
if *auth != "" {
cfg.Auth = *auth
}
// Setup logging - default to file if not specified
logPath := *logFile
if logPath == "" {
// Use default log file in app config directory
appDir := getAppDir()
logPath = filepath.Join(appDir, "proxy.log")
if *upstreamProxy != "" {
cfg.UpstreamProxy = *upstreamProxy
}
// Setup logging - log to stdout if verbose, otherwise to file
var logger *log.Logger
logPath := *logFile
if cfg.Verbose && logPath == "" {
// Verbose mode: log to stdout
logger = setupLogging("", cfg.LogMaxMB, cfg.Verbose)
} else {
// File mode: log to file (default to app dir if not specified)
if logPath == "" {
appDir := getAppDir()
logPath = filepath.Join(appDir, "proxy.log")
}
logger = setupLogging(logPath, cfg.LogMaxMB, cfg.Verbose)
}
// Log advanced features usage and start HTTP proxy
if *httpPort != 0 {
log.Printf("⚙ HTTP proxy enabled on port %d", *httpPort)
// Start HTTP proxy in background
go func() {
httpProxy, err := proxy.NewHTTPProxy(*httpPort, cfg.Verbose, logger, *upstreamProxy)
if err != nil {
log.Printf("Failed to create HTTP proxy: %v", err)
return
}
if err := httpProxy.Start(); err != nil {
log.Printf("HTTP proxy error: %v", err)
}
}()
}
if *upstreamProxy != "" {
log.Printf("⚙ Upstream proxy: %s", *upstreamProxy)
}
logger := setupLogging(logPath, cfg.LogMaxMB, cfg.Verbose)
// Create and start server
server, err := proxy.NewServer(cfg, logger)
server, err := proxy.NewServer(cfg, logger, cfg.UpstreamProxy)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
// Auto-configure Telegram Desktop
if *autoConfig {
log.Println("Attempting to auto-configure Telegram Desktop...")
username, password := "", ""
if cfg.Auth != "" {
parts := strings.SplitN(cfg.Auth, ":", 2)
if len(parts) == 2 {
username, password = parts[0], parts[1]
}
}
if telegram.ConfigureProxy(cfg.Host, cfg.Port, username, password) {
log.Println("✓ Telegram Desktop proxy configuration opened")
} else {
log.Println("✗ Failed to open Telegram Desktop. Please configure manually.")
log.Println(" Open in browser: tg://socks?server=127.0.0.1&port=1080")
// Auto-configure Telegram Desktop with correct proxy type
log.Println("Attempting to configure Telegram Desktop...")
// Determine proxy type and configure Telegram accordingly
// Note: Our local proxy only supports SOCKS5
// HTTP port is for other applications (browsers, etc.)
// MTProto requires external MTProxy server
proxyType := "socks5" // Always SOCKS5 for our local proxy
proxyPort := cfg.Port
proxySecret := ""
// Log HTTP mode if enabled (for other apps, not Telegram)
if *httpPort != 0 {
log.Printf("⚙ HTTP proxy enabled on port %d (for browsers/other apps)", *httpPort)
}
// Log MTProto mode info
if *mtprotoPort != 0 && *mtprotoSecret != "" {
log.Printf("⚙ MTProto mode: Use external MTProxy or configure manually")
log.Printf(" tg://proxy?server=%s&port=%d&secret=%s", cfg.Host, *mtprotoPort, *mtprotoSecret)
}
username, password := "", ""
if cfg.Auth != "" {
parts := strings.SplitN(cfg.Auth, ":", 2)
if len(parts) == 2 {
username, password = parts[0], parts[1]
}
}
// Check for updates (non-blocking)
if telegram.ConfigureProxyWithType(cfg.Host, proxyPort, username, password, proxySecret, proxyType) {
log.Printf("✓ Telegram Desktop %s proxy configuration opened", strings.ToUpper(proxyType))
} else {
log.Println("✗ Failed to auto-configure Telegram.")
log.Println(" Manual setup: Settings → Advanced → Connection Type → Proxy")
log.Printf(" Or open: tg://socks?server=%s&port=%d", cfg.Host, proxyPort)
}
// Check for updates and auto-download (non-blocking)
go func() {
hasUpdate, latest, url, err := version.CheckUpdate()
if err != nil {
@ -115,7 +206,18 @@ func main() {
}
if hasUpdate {
log.Printf("⚡ NEW VERSION AVAILABLE: v%s (current: v%s)", latest, version.CurrentVersion)
log.Printf(" Download: %s", url)
log.Printf(" Downloading update...")
// Try to download update
downloadedPath, err := version.DownloadUpdate(latest)
if err != nil {
log.Printf(" Download failed: %v", err)
log.Printf(" Manual download: %s", url)
return
}
log.Printf(" ✓ Downloaded to: %s", downloadedPath)
log.Printf(" Restart the proxy to apply update")
}
}()
@ -155,8 +257,12 @@ func getAppDir() string {
func setupLogging(logFile string, logMaxMB float64, verbose bool) *log.Logger {
flags := log.LstdFlags | log.Lshortfile
if verbose {
flags |= log.Lshortfile
// If verbose and no log file specified, log to stdout
if verbose && logFile == "" {
log.SetOutput(os.Stdout)
log.SetFlags(flags)
return log.New(os.Stdout, "", flags)
}
// Ensure directory exists
@ -167,6 +273,8 @@ func setupLogging(logFile string, logMaxMB float64, verbose bool) *log.Logger {
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Printf("Warning: failed to open log file %s: %v, using stdout", logFile, err)
log.SetOutput(os.Stdout)
log.SetFlags(flags)
return log.New(os.Stdout, "", flags)
}
@ -188,38 +296,12 @@ func splitDCIP(s string) []string {
if s == "" {
return nil
}
result := []string{}
for _, part := range splitString(s, ",") {
part = trimSpace(part)
result := make([]string, 0)
for _, part := range strings.Split(s, ",") {
part = strings.TrimSpace(part)
if part != "" {
result = append(result, part)
}
}
return result
}
func splitString(s, sep string) []string {
result := []string{}
start := 0
for i := 0; i <= len(s)-len(sep); i++ {
if s[i:i+len(sep)] == sep {
result = append(result, s[start:i])
start = i + len(sep)
i = start - 1
}
}
result = append(result, s[start:])
return result
}
func trimSpace(s string) string {
start := 0
end := len(s)
for start < end && (s[start] == ' ' || s[start] == '\t') {
start++
}
for end > start && (s[end-1] == ' ' || s[end-1] == '\t') {
end--
}
return s[start:end]
}

4
go.mod
View File

@ -1,3 +1,5 @@
module github.com/Flowseal/tg-ws-proxy
go 1.21
go 1.25.0
require golang.org/x/net v0.52.0 // indirect

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=

View File

@ -13,15 +13,16 @@ import (
// Config holds the proxy configuration.
type Config struct {
Port int `json:"port"`
Host string `json:"host"`
DCIP []string `json:"dc_ip"`
Verbose bool `json:"verbose"`
AutoStart bool `json:"autostart"`
LogMaxMB float64 `json:"log_max_mb"`
BufKB int `json:"buf_kb"`
PoolSize int `json:"pool_size"`
Auth string `json:"auth"` // username:password
Port int `json:"port"`
Host string `json:"host"`
DCIP []string `json:"dc_ip"`
Verbose bool `json:"verbose"`
AutoStart bool `json:"autostart"`
LogMaxMB float64 `json:"log_max_mb"`
BufKB int `json:"buf_kb"`
PoolSize int `json:"pool_size"`
Auth string `json:"auth"` // username:password
UpstreamProxy string `json:"upstream_proxy"`
}
// DefaultConfig returns the default configuration.

View File

@ -91,15 +91,13 @@ func PatchInitDC(data []byte, dc int) ([]byte, bool) {
keystream := make([]byte, 8)
stream.XORKeyStream(keystream, zero64[56:64])
// Patch bytes 60-61 with the correct DC ID
// Patch in-place to avoid allocation
patched := make([]byte, len(data))
copy(patched, data)
newDC := make([]byte, 2)
binary.LittleEndian.PutUint16(newDC, uint16(dc))
patched[60] = keystream[0] ^ newDC[0]
patched[61] = keystream[1] ^ newDC[1]
// Patch bytes 60-61 directly
patched[60] = keystream[0] ^ byte(dc)
patched[61] = keystream[1] ^ byte(dc>>8)
return patched, true
}

View File

@ -0,0 +1,139 @@
// Package proxy provides HTTP proxy server functionality.
package proxy
import (
"bufio"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"strings"
"golang.org/x/net/proxy"
)
// HTTPProxy represents an HTTP proxy server.
type HTTPProxy struct {
port int
verbose bool
logger *log.Logger
upstreamProxy *url.URL
}
// dialWithUpstream creates a connection, optionally routing through an upstream proxy.
func (h *HTTPProxy) dialWithUpstream(network, addr string) (net.Conn, error) {
if h.upstreamProxy == nil {
return net.Dial(network, addr)
}
switch h.upstreamProxy.Scheme {
case "socks5", "socks":
// Use proxy package for SOCKS5
proxyDialer, err := proxy.FromURL(h.upstreamProxy, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("create SOCKS5 dialer: %w", err)
}
return proxyDialer.Dial(network, addr)
case "http", "https":
// Use http.Transport with Proxy for HTTP CONNECT
transport := &http.Transport{
Proxy: http.ProxyURL(h.upstreamProxy),
}
return transport.Dial(network, addr)
default:
return nil, fmt.Errorf("unsupported upstream proxy scheme: %s", h.upstreamProxy.Scheme)
}
}
// NewHTTPProxy creates a new HTTP proxy server.
func NewHTTPProxy(port int, verbose bool, logger *log.Logger, upstreamProxyURL string) (*HTTPProxy, error) {
var upstreamProxy *url.URL
var err error
if upstreamProxyURL != "" {
upstreamProxy, err = url.Parse(upstreamProxyURL)
if err != nil {
return nil, fmt.Errorf("invalid upstream proxy URL: %v", err)
}
}
return &HTTPProxy{
port: port,
verbose: verbose,
logger: logger,
upstreamProxy: upstreamProxy,
}, nil
}
// Start starts the HTTP proxy server.
func (h *HTTPProxy) Start() error {
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", h.port))
if err != nil {
return err
}
defer listener.Close()
if h.verbose {
h.logger.Printf("[HTTP] Listening on port %d", h.port)
}
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go h.handleConnection(conn)
}
}
func (h *HTTPProxy) handleConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
req, err := http.ReadRequest(reader)
if err != nil {
return
}
defer req.Body.Close()
// Handle CONNECT method (for HTTPS)
if req.Method == http.MethodConnect {
h.handleConnect(conn, req)
return
}
// Handle HTTP requests
h.handleHTTP(conn, req)
}
func (h *HTTPProxy) handleConnect(conn net.Conn, req *http.Request) {
// Parse host:port
host := req.URL.Host
if !strings.Contains(host, ":") {
host = host + ":80"
}
// Connect to target (with upstream proxy if configured)
target, err := h.dialWithUpstream("tcp", host)
if err != nil {
conn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
return
}
defer target.Close()
// Send success response
conn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
// Bridge connections
go io.Copy(target, conn)
io.Copy(conn, target)
}
func (h *HTTPProxy) handleHTTP(conn net.Conn, req *http.Request) {
// For now, just return error - full HTTP proxy is complex
conn.Write([]byte("HTTP/1.1 501 Not Implemented\r\n\r\n"))
}

View File

@ -2,14 +2,19 @@
package proxy
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/Flowseal/tg-ws-proxy/internal/config"
@ -17,6 +22,7 @@ import (
"github.com/Flowseal/tg-ws-proxy/internal/pool"
"github.com/Flowseal/tg-ws-proxy/internal/socks5"
"github.com/Flowseal/tg-ws-proxy/internal/websocket"
"golang.org/x/net/proxy"
)
const (
@ -79,122 +85,144 @@ var dcOverrides = map[int]int{
// Stats holds proxy statistics.
type Stats struct {
mu sync.Mutex
ConnectionsTotal int64
ConnectionsWS int64
ConnectionsTCP int64
ConnectionsHTTP int64
ConnectionsPass int64
WSErrors int64
BytesUp int64
BytesDown int64
PoolHits int64
PoolMisses int64
ConnectionsTotal atomic.Int64
ConnectionsWS atomic.Int64
ConnectionsTCP atomic.Int64
ConnectionsHTTP atomic.Int64
ConnectionsPass atomic.Int64
WSErrors atomic.Int64
BytesUp atomic.Int64
BytesDown atomic.Int64
PoolHits atomic.Int64
PoolMisses atomic.Int64
}
func (s *Stats) addConnectionsTotal(n int64) {
s.mu.Lock()
s.ConnectionsTotal += n
s.mu.Unlock()
s.ConnectionsTotal.Add(n)
}
func (s *Stats) addConnectionsWS(n int64) {
s.mu.Lock()
s.ConnectionsWS += n
s.mu.Unlock()
s.ConnectionsWS.Add(n)
}
func (s *Stats) addConnectionsTCP(n int64) {
s.mu.Lock()
s.ConnectionsTCP += n
s.mu.Unlock()
s.ConnectionsTCP.Add(n)
}
func (s *Stats) addConnectionsHTTP(n int64) {
s.mu.Lock()
s.ConnectionsHTTP += n
s.mu.Unlock()
s.ConnectionsHTTP.Add(n)
}
func (s *Stats) addConnectionsPass(n int64) {
s.mu.Lock()
s.ConnectionsPass += n
s.mu.Unlock()
s.ConnectionsPass.Add(n)
}
func (s *Stats) addWSErrors(n int64) {
s.mu.Lock()
s.WSErrors += n
s.mu.Unlock()
s.WSErrors.Add(n)
}
func (s *Stats) addBytesUp(n int64) {
s.mu.Lock()
s.BytesUp += n
s.mu.Unlock()
s.BytesUp.Add(n)
}
func (s *Stats) addBytesDown(n int64) {
s.mu.Lock()
s.BytesDown += n
s.mu.Unlock()
s.BytesDown.Add(n)
}
func (s *Stats) addPoolHits(n int64) {
s.mu.Lock()
s.PoolHits += n
s.mu.Unlock()
s.PoolHits.Add(n)
}
func (s *Stats) addPoolMisses(n int64) {
s.mu.Lock()
s.PoolMisses += n
s.mu.Unlock()
s.PoolMisses.Add(n)
}
func (s *Stats) Summary() string {
s.mu.Lock()
defer s.mu.Unlock()
hits := s.PoolHits.Load()
misses := s.PoolMisses.Load()
return fmt.Sprintf("total=%d ws=%d tcp=%d http=%d pass=%d err=%d pool=%d/%d up=%s down=%s",
s.ConnectionsTotal, s.ConnectionsWS, s.ConnectionsTCP,
s.ConnectionsHTTP, s.ConnectionsPass, s.WSErrors,
s.PoolHits, s.PoolHits+s.PoolMisses,
humanBytes(s.BytesUp), humanBytes(s.BytesDown))
s.ConnectionsTotal.Load(), s.ConnectionsWS.Load(), s.ConnectionsTCP.Load(),
s.ConnectionsHTTP.Load(), s.ConnectionsPass.Load(), s.WSErrors.Load(),
hits, hits+misses,
humanBytes(s.BytesUp.Load()), humanBytes(s.BytesDown.Load()))
}
// Server represents the TG WS Proxy server.
type Server struct {
config *config.Config
dcOpt map[int]string
wsPool *pool.WSPool
stats *Stats
wsBlacklist map[pool.DCKey]bool
dcFailUntil map[pool.DCKey]time.Time
mu sync.RWMutex
listener net.Listener
logger *log.Logger
config *config.Config
dcOpt map[int]string
wsPool *pool.WSPool
stats *Stats
wsBlacklist map[pool.DCKey]bool
dcFailUntil map[pool.DCKey]time.Time
mu sync.RWMutex
listener net.Listener
logger *log.Logger
upstreamProxy string
}
// NewServer creates a new proxy server.
func NewServer(cfg *config.Config, logger *log.Logger) (*Server, error) {
func NewServer(cfg *config.Config, logger *log.Logger, upstreamProxy string) (*Server, error) {
dcOpt, err := config.ParseDCIPList(cfg.DCIP)
if err != nil {
return nil, err
}
s := &Server{
config: cfg,
dcOpt: dcOpt,
wsPool: pool.NewWSPool(cfg.PoolSize, defaultPoolMaxAge),
stats: &Stats{},
wsBlacklist: make(map[pool.DCKey]bool),
dcFailUntil: make(map[pool.DCKey]time.Time),
logger: logger,
config: cfg,
dcOpt: dcOpt,
wsPool: pool.NewWSPool(cfg.PoolSize, defaultPoolMaxAge),
stats: &Stats{},
wsBlacklist: make(map[pool.DCKey]bool),
dcFailUntil: make(map[pool.DCKey]time.Time),
logger: logger,
upstreamProxy: upstreamProxy,
}
return s, nil
}
// dialWithUpstream creates a connection, optionally routing through an upstream proxy.
func (s *Server) dialWithUpstream(network, addr string, timeout time.Duration) (net.Conn, error) {
if s.upstreamProxy == "" {
return net.DialTimeout(network, addr, timeout)
}
// Parse upstream proxy URL
u, err := url.Parse(s.upstreamProxy)
if err != nil {
return nil, fmt.Errorf("parse upstream proxy: %w", err)
}
switch u.Scheme {
case "socks5", "socks":
var auth *proxy.Auth
if u.User != nil {
password, _ := u.User.Password()
auth = &proxy.Auth{
User: u.User.Username(),
Password: password,
}
}
dialer, err := proxy.SOCKS5(network, u.Host, auth, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("create SOCKS5 dialer: %w", err)
}
return dialer.Dial(network, addr)
case "http", "https":
// Use http.Transport with Proxy for HTTP CONNECT
transport := &http.Transport{
Proxy: http.ProxyURL(u),
TLSHandshakeTimeout: timeout,
}
return transport.Dial(network, addr)
default:
return nil, fmt.Errorf("unsupported upstream proxy scheme: %s", u.Scheme)
}
}
// Start starts the proxy server.
func (s *Server) Start(ctx context.Context) error {
addr := net.JoinHostPort(s.config.Host, fmt.Sprintf("%d", s.config.Port))
@ -425,7 +453,9 @@ func (s *Server) getWebSocket(dcKey pool.DCKey, targetIP string, domains []strin
s.logInfo("[%s] DC%d%s (%s:%d) -> %s via %s", label, dc, mediaTag, dst, port, url, targetIP)
// Connect using targetIP, but use domain for TLS handshake
ws, wsErr = websocket.Connect(targetIP, domain, "/apiws", wsTimeout)
ws, wsErr = websocket.ConnectWithDialer(targetIP, domain, "/apiws", wsTimeout, func(network, addr string) (net.Conn, error) {
return s.dialWithUpstream(network, addr, wsTimeout)
})
if wsErr == nil {
allRedirects = false
break
@ -468,7 +498,7 @@ func (s *Server) getWebSocket(dcKey pool.DCKey, targetIP string, domains []strin
}
func (s *Server) handlePassthrough(conn net.Conn, dst string, port uint16, label string) {
remoteConn, err := net.DialTimeout("tcp", net.JoinHostPort(dst, fmt.Sprintf("%d", port)), 10*time.Second)
remoteConn, err := s.dialWithUpstream("tcp", net.JoinHostPort(dst, fmt.Sprintf("%d", port)), 10*time.Second)
if err != nil {
s.logWarning("[%s] passthrough failed to %s: %v", label, dst, err)
conn.Write(socks5.Reply(socks5.ReplyFail))
@ -483,7 +513,7 @@ func (s *Server) handlePassthrough(conn net.Conn, dst string, port uint16, label
// handleIPv6Connection handles IPv6 connections via dual-stack or IPv4-mapped addresses.
func (s *Server) handleIPv6Connection(conn net.Conn, ipv6Addr string, port uint16, label string) {
// Try direct IPv6 first
remoteConn, err := net.DialTimeout("tcp6", net.JoinHostPort(ipv6Addr, fmt.Sprintf("%d", port)), 10*time.Second)
remoteConn, err := s.dialWithUpstream("tcp6", net.JoinHostPort(ipv6Addr, fmt.Sprintf("%d", port)), 10*time.Second)
if err == nil {
s.logInfo("[%s] IPv6 direct connection successful", label)
defer remoteConn.Close()
@ -527,40 +557,23 @@ func (s *Server) handleIPv6Connection(conn net.Conn, ipv6Addr string, port uint1
// extractIPv4 tries to extract IPv4 from IPv4-mapped IPv6 address.
func extractIPv4(ipv6 string) string {
// Check for ::ffff: prefix (IPv4-mapped)
// Example: ::ffff:192.0.2.1
if strings.HasPrefix(strings.ToLower(ipv6), "::ffff:") {
return ipv6[7:]
}
// Check for other IPv4-mapped formats
parts := strings.Split(ipv6, ":")
if len(parts) >= 6 {
// Try to parse last 2 parts as hex IPv4
if len(parts[6]) == 4 && len(parts[7]) == 4 {
// This is a more complex case, skip for now
}
return strings.TrimPrefix(ipv6, "::ffff:")
}
return ""
}
// extractIPv4FromNAT64 extracts IPv4 from NAT64 IPv6 address.
// Currently returns empty string as NAT64 is not fully supported.
func extractIPv4FromNAT64(ipv6, prefix string) string {
// Remove prefix
suffix := strings.TrimPrefix(ipv6, prefix)
// NAT64 embeds IPv4 in last 32 bits
parts := strings.Split(suffix, ":")
if len(parts) >= 2 {
lastParts := parts[len(parts)-2:]
if len(lastParts) == 2 {
// Parse hex to decimal
// Format: :xxxx:yyyy where xxxx.yyyy is IPv4 in hex
// This is simplified - real implementation would parse properly
return "" // For now, return empty to indicate not supported
}
}
// NAT64 embeds IPv4 in last 32 bits of the IPv6 address
// This is a placeholder for future implementation
return ""
}
func (s *Server) handleTCPFallback(conn net.Conn, dst string, port uint16, init []byte, label string, dc int, isMedia bool) {
remoteConn, err := net.DialTimeout("tcp", net.JoinHostPort(dst, fmt.Sprintf("%d", port)), 10*time.Second)
remoteConn, err := s.dialWithUpstream("tcp", net.JoinHostPort(dst, fmt.Sprintf("%d", port)), 10*time.Second)
if err != nil {
s.logWarning("[%s] TCP fallback to %s:%d failed: %v", label, dst, port, err)
return
@ -707,7 +720,9 @@ func (s *Server) warmupPool() {
go func(dcKey pool.DCKey, targetIP string, domains []string) {
for s.wsPool.NeedRefill(dcKey) {
for _, domain := range domains {
ws, err := websocket.Connect(targetIP, domain, "/apiws", wsConnectTimeout)
ws, err := websocket.ConnectWithDialer(targetIP, domain, "/apiws", wsConnectTimeout, func(network, addr string) (net.Conn, error) {
return s.dialWithUpstream(network, addr, wsConnectTimeout)
})
if err == nil {
s.wsPool.Put(dcKey, ws)
break
@ -818,17 +833,15 @@ func (s *Server) logDebug(format string, args ...interface{}) {
// Helper functions
func ipToUint32(ip string) uint32 {
parts := strings.Split(ip, ".")
if len(parts) != 4 {
ipObj := net.ParseIP(ip)
if ipObj == nil {
return 0
}
var result uint32
for i, part := range parts {
var n uint32
fmt.Sscanf(part, "%d", &n)
result |= n << (24 - uint(i)*8)
ipObj = ipObj.To4()
if ipObj == nil {
return 0
}
return result
return binary.BigEndian.Uint32(ipObj)
}
func isTelegramIP(ip string) bool {
@ -845,22 +858,10 @@ func isHTTPTransport(data []byte) bool {
if len(data) < 5 {
return false
}
return bytesEqual(data[:5], []byte("POST ")) ||
bytesEqual(data[:4], []byte("GET ")) ||
bytesEqual(data[:5], []byte("HEAD ")) ||
bytesEqual(data[:8], []byte("OPTIONS "))
}
func bytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
return bytes.HasPrefix(data, []byte("POST ")) ||
bytes.HasPrefix(data, []byte("GET ")) ||
bytes.HasPrefix(data, []byte("HEAD ")) ||
bytes.HasPrefix(data, []byte("OPTIONS "))
}
func humanBytes(n int64) string {

View File

@ -34,7 +34,7 @@ func TestHandleGreeting_Success(t *testing.T) {
// Send valid greeting with no-auth method
go client.Write([]byte{0x05, 0x01, 0x00})
nmethods, err := HandleGreeting(server)
nmethods, err := HandleGreeting(server, &AuthConfig{})
if err != nil {
t.Fatalf("HandleGreeting failed: %v", err)
}
@ -58,7 +58,7 @@ func TestHandleGreeting_UnsupportedVersion(t *testing.T) {
// Send SOCKS4 greeting
go client.Write([]byte{0x04, 0x01, 0x00})
_, err := HandleGreeting(server)
_, err := HandleGreeting(server, &AuthConfig{})
if err != ErrUnsupportedVersion {
t.Errorf("Expected ErrUnsupportedVersion, got %v", err)
}
@ -72,7 +72,7 @@ func TestHandleGreeting_NoAuthNotSupported(t *testing.T) {
// Send greeting without no-auth method
go client.Write([]byte{0x05, 0x01, 0x01})
_, err := HandleGreeting(server)
_, err := HandleGreeting(server, &AuthConfig{})
if err != ErrNoAuthAccepted {
t.Errorf("Expected ErrNoAuthAccepted, got %v", err)
}

View File

@ -8,20 +8,35 @@ import (
"strings"
)
// ConfigureProxy opens Telegram's proxy configuration URL.
// ConfigureProxy opens Telegram's SOCKS5 proxy configuration URL.
// Returns true if successful, false otherwise.
func ConfigureProxy(host string, port int, username, password string) bool {
// Build tg:// proxy URL
url := fmt.Sprintf("tg://socks?server=%s&port=%d", host, port)
return ConfigureProxyWithType(host, port, username, password, "", "socks5")
}
if username != "" {
url += fmt.Sprintf("&user=%s", username)
}
if password != "" {
url += fmt.Sprintf("&pass=%s", password)
// ConfigureProxyWithType opens Telegram's proxy configuration URL with specified type.
// proxyType: "socks5" or "mtproto"
// For MTProto, provide secret parameter
// Note: HTTP proxy is NOT supported by Telegram Desktop via tg:// URLs
// Returns true if successful, false otherwise.
func ConfigureProxyWithType(host string, port int, username, password, secret, proxyType string) bool {
var proxyURL string
switch proxyType {
case "mtproto":
// MTProto proxy format: tg://proxy?server=host&port=port&secret=secret
if secret == "" {
secret = "ee000000000000000000000000000000" // default dummy secret
}
proxyURL = fmt.Sprintf("tg://proxy?server=%s&port=%d&secret=%s", host, port, secret)
default:
// SOCKS5 proxy format: tg://socks?server=host&port=port
// This is the only type our local proxy supports
proxyURL = fmt.Sprintf("tg://socks?server=%s&port=%d", host, port)
}
return openURL(url)
// Open URL using system default handler
return openURL(proxyURL)
}
// openURL opens a URL in the default browser/application.
@ -31,18 +46,19 @@ func openURL(url string) bool {
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
// Use rundll32 to open URL - more reliable for protocol handlers
cmd = "rundll32"
args = []string{"url.dll,FileProtocolHandler", url}
case "darwin":
cmd = "open"
args = []string{url}
case "linux":
cmd = "xdg-open"
args = []string{url}
default:
return false
}
args = append(args, url)
err := exec.Command(cmd, args...).Start()
return err == nil
}

View File

@ -6,20 +6,30 @@ import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
const (
CurrentVersion = "2.0.0"
CurrentVersion = "2.0.5"
RepoURL = "https://api.github.com/repos/y0sy4/tg-ws-proxy-go/releases/latest"
)
type Release struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
HTMLURL string `json:"html_url"`
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
HTMLURL string `json:"html_url"`
Assets []Asset `json:"assets"`
}
type Asset struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
}
// CheckUpdate checks for new version on GitHub.
@ -53,6 +63,85 @@ func CheckUpdate() (bool, string, string, error) {
return false, current, "", nil
}
// DownloadUpdate downloads the latest version for current platform.
// Returns path to downloaded file or error.
func DownloadUpdate(latestVersion string) (string, error) {
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(RepoURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var release Release
if err := json.Unmarshal(body, &release); err != nil {
return "", err
}
// Find asset for current platform
assetName := getAssetName()
for _, asset := range release.Assets {
if asset.Name == assetName {
return downloadAsset(client, asset.BrowserDownloadURL, assetName)
}
}
return "", fmt.Errorf("no asset found for %s", runtime.GOOS)
}
func getAssetName() string {
switch runtime.GOOS {
case "windows":
return "TgWsProxy_windows_amd64.exe"
case "linux":
return "TgWsProxy_linux_amd64"
case "darwin":
if runtime.GOARCH == "arm64" {
return "TgWsProxy_darwin_arm64"
}
return "TgWsProxy_darwin_amd64"
default:
return ""
}
}
func downloadAsset(client *http.Client, url, filename string) (string, error) {
resp, err := client.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
// Get executable directory
exe, err := os.Executable()
if err != nil {
return "", err
}
exeDir := filepath.Dir(exe)
// Download to temp file first
tempPath := filepath.Join(exeDir, filename+".new")
out, err := os.Create(tempPath)
if err != nil {
return "", err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
os.Remove(tempPath)
return "", err
}
return tempPath, nil
}
// compareVersions compares two semantic versions.
// Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal.
func compareVersions(v1, v2 string) int {
@ -82,7 +171,12 @@ func splitVersion(v string) []int {
parts := strings.Split(v, ".")
result := make([]int, len(parts))
for i, p := range parts {
fmt.Sscanf(p, "%d", &result[i])
n, err := strconv.Atoi(p)
if err != nil {
result[i] = 0
} else {
result[i] = n
}
}
return result
}

View File

@ -44,6 +44,12 @@ type WebSocket struct {
// Connect establishes a WebSocket connection to the given domain via IP.
func Connect(ip, domain, path string, timeout time.Duration) (*WebSocket, error) {
return ConnectWithDialer(ip, domain, path, timeout, nil)
}
// ConnectWithDialer establishes a WebSocket connection using a custom dialer.
// If dialer is nil, it uses direct connection.
func ConnectWithDialer(ip, domain, path string, timeout time.Duration, dialFunc func(network, addr string) (net.Conn, error)) (*WebSocket, error) {
if path == "" {
path = "/apiws"
}
@ -56,18 +62,55 @@ func Connect(ip, domain, path string, timeout time.Duration) (*WebSocket, error)
wsKey := base64.StdEncoding.EncodeToString(keyBytes)
// Dial TLS connection
dialer := &net.Dialer{Timeout: timeout}
tlsConfig := &tls.Config{
ServerName: domain,
InsecureSkipVerify: true,
var rawConn net.Conn
var err error
if dialFunc != nil {
// Use custom dialer
rawConn, err = dialFunc("tcp", net.JoinHostPort(ip, "443"))
if err != nil {
return nil, fmt.Errorf("dial: %w", err)
}
// Wrap with TLS
tlsConfig := &tls.Config{
ServerName: domain,
InsecureSkipVerify: true,
}
rawConn = tls.Client(rawConn, tlsConfig)
// Set handshake timeout
if err := rawConn.SetDeadline(time.Now().Add(timeout)); err != nil {
rawConn.Close()
return nil, err
}
} else {
// Direct connection
dialer := &net.Dialer{Timeout: timeout}
tlsConfig := &tls.Config{
ServerName: domain,
InsecureSkipVerify: true,
}
rawConn, err = tls.DialWithDialer(dialer, "tcp", net.JoinHostPort(ip, "443"), tlsConfig)
if err != nil {
return nil, fmt.Errorf("tls dial: %w", err)
}
}
rawConn, err := tls.DialWithDialer(dialer, "tcp", net.JoinHostPort(ip, "443"), tlsConfig)
if err != nil {
return nil, fmt.Errorf("tls dial: %w", err)
// Clear deadline after handshake
if err := rawConn.SetDeadline(time.Time{}); err != nil {
rawConn.Close()
return nil, err
}
// Set TCP_NODELAY and buffer sizes
if tcpConn, ok := rawConn.NetConn().(*net.TCPConn); ok {
if tcpConn, ok := rawConn.(*tls.Conn); ok {
if netConn := tcpConn.NetConn(); netConn != nil {
if tcpNetConn, ok := netConn.(*net.TCPConn); ok {
tcpNetConn.SetNoDelay(true)
tcpNetConn.SetReadBuffer(256 * 1024)
tcpNetConn.SetWriteBuffer(256 * 1024)
}
}
} else if tcpConn, ok := rawConn.(*net.TCPConn); ok {
tcpConn.SetNoDelay(true)
tcpConn.SetReadBuffer(256 * 1024)
tcpConn.SetWriteBuffer(256 * 1024)
@ -115,7 +158,7 @@ func Connect(ip, domain, path string, timeout time.Duration) (*WebSocket, error)
}
return &WebSocket{
conn: rawConn,
conn: rawConn.(*tls.Conn),
reader: reader,
writer: bufio.NewWriter(rawConn),
maskKey: make([]byte, 4),

View File

@ -8,6 +8,7 @@ import (
"net"
"os"
"path/filepath"
"strings"
"github.com/Flowseal/tg-ws-proxy/internal/config"
"github.com/Flowseal/tg-ws-proxy/internal/proxy"
@ -38,18 +39,23 @@ func Start(host string, port int, dcIP string, verbose bool) string {
if err != nil {
return fmt.Sprintf("Failed to open log file: %v", err)
}
log.SetOutput(f)
log.SetFlags(log.Ldate | log.Ltime)
logger := log.New(f, "", log.Ldate|log.Ltime)
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
server = proxy.NewServer(cfg)
if err := server.Start(ctx); err != nil {
server, err = proxy.NewServer(cfg, logger, "")
if err != nil {
cancel()
return fmt.Sprintf("Failed to start proxy: %v", err)
return fmt.Sprintf("Failed to create server: %v", err)
}
go func() {
if err := server.Start(ctx); err != nil {
cancel()
}
}()
return "OK"
}
@ -58,9 +64,6 @@ func Stop() string {
if cancel != nil {
cancel()
}
if server != nil {
server.Stop()
}
return "OK"
}
@ -69,13 +72,7 @@ func GetStatus() string {
if server == nil {
return "Not running"
}
stats := server.GetStats()
return fmt.Sprintf("Connections: %d | WS: %d | TCP: %d | Bytes Up: %d | Bytes Down: %d",
stats.ConnectionsTotal,
stats.ConnectionsWS,
stats.ConnectionsTCP,
stats.BytesUp,
stats.BytesDown)
return "Running" // Simplified for mobile
}
// parseDCIP parses DC IP configuration string.
@ -83,11 +80,11 @@ func parseDCIP(s string) []string {
if s == "" {
return nil
}
result := []string{}
for _, part := range split(s, ",") {
trimmed := trim(part)
if trimmed != "" {
result = append(result, trimmed)
result := make([]string, 0)
for _, part := range strings.Split(s, ",") {
part = strings.TrimSpace(part)
if part != "" {
result = append(result, part)
}
}
return result
@ -103,32 +100,6 @@ func getLogDir() string {
return os.TempDir()
}
// Helper functions for string manipulation (avoiding strings package issues with gomobile)
func split(s, sep string) []string {
result := []string{}
start := 0
for i := 0; i <= len(s)-len(sep); i++ {
if s[i:i+len(sep)] == sep {
result = append(result, s[start:i])
start = i + len(sep)
}
}
result = append(result, s[start:])
return result
}
func trim(s string) string {
start := 0
end := len(s)
for start < end && (s[start] == ' ' || s[start] == '\t' || s[start] == '\n' || s[start] == '\r') {
start++
}
for end > start && (s[end-1] == ' ' || s[end-1] == '\t' || s[end-1] == '\n' || s[end-1] == '\r') {
end--
}
return s[start:end]
}
// Dummy function to use net package (required for SOCKS5)
func init() {
_ = net.Dial