Compare commits

..

No commits in common. "master" and "v2.0.3" have entirely different histories.

18 changed files with 455 additions and 1118 deletions

View File

@ -1,11 +0,0 @@
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,41 +1,34 @@
<!-- Спасибо за ваш вклад в проект! Пожалуйста, заполните эту форму -->
## Описание изменений
<!-- Опишите, что вы изменили и почему -->
## Тип изменений
<!-- Отметьте соответствующие пункты -->
- [ ] 🐛 Исправление бага
- [ ] ✨ Новая функция
- [ ] 📝 Обновление документации
- [ ] ⚡ Улучшение производительности
- [ ] 🔒 Исправление безопасности
- [ ] 🎨 Рефакторинг кода
- [ ] 🧪 Добавление тестов
- [ ] Другое: _______
## Проверка
<!-- Убедитесь, что вы выполнили следующие действия -->
- [ ] Я протестировал изменения локально
- [ ] Код следует стилю проекта
- [ ] Я добавил комментарии к сложным участкам кода
- [ ] Я обновил документацию (если необходимо)
- [ ] Я проверил, что нет конфликтов слияния
## Тестирование
<!-- Опишите, как вы тестировали изменения -->
**ОС:** Windows / macOS / Linux
**Шаги для тестирования:**
1.
2.
3.
## Скриншоты (если применимо)
<!-- Добавьте скриншоты, если изменения влияют на UI -->
## Дополнительные заметки
<!-- Любая дополнительная информация, которая может быть полезна -->
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

View File

@ -1,48 +0,0 @@
# 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

View File

@ -1,68 +0,0 @@
# 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
View File

@ -1,205 +0,0 @@
# ❓ Частые вопросы (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,187 +1,212 @@
# TG WS Proxy Go
[![Release](https://img.shields.io/github/v/release/y0sy4/tg-ws-proxy-go)](https://github.com/y0sy4/tg-ws-proxy-go/releases)
[![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)
**SOCKS5-прокси для Telegram Desktop на Go.** Ускоряет Telegram через WebSocket к серверам Telegram.
> **Go-переосмысление** [Flowseal/tg-ws-proxy](https://github.com/Flowseal/tg-ws-proxy)
---
**Локальный SOCKS5-прокси для Telegram Desktop на Go**
## 📥 Скачать (v2.0.5)
Ускоряет работу Telegram через WebSocket-соединения напрямую к серверам Telegram.
| 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) |
## Почему Go версия лучше
---
| Параметр | 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
TgWsProxy.exe [флаги]
# Скачать готовый бинарник из Releases
# Или собрать из исходников
go build -o TgWsProxy.exe ./cmd/proxy
```
| Флаг | Описание | По умолчанию |
|------|----------|--------------|
| `--port` | Порт SOCKS5 | 1080 |
| `--host` | Хост | 127.0.0.1 |
| `--dc-ip` | DC:IP (через запятую) | авто |
| `--auth` | Логин:пароль для прокси | — |
| `--http-port` | HTTP прокси (для браузеров) | 0 (выкл) |
| `--upstream-proxy` | Цепочка через другой прокси | — |
| `-v` | Подробные логи | false |
### Запуск
```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 Показать версию
```
### Примеры
**Просто запустить:**
```bash
TgWsProxy.exe
# Без аутентификации
./TgWsProxy -v
# С аутентификацией (защита от несанкционированного доступа)
./TgWsProxy --auth "myuser:mypassword"
# Настройка DC
./TgWsProxy --dc-ip "2:149.154.167.220,4:149.154.167.220"
```
**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-go/
├── cmd/proxy/ # CLI приложение
tg-ws-proxy/
├── cmd/
│ └── proxy/ # CLI приложение
├── internal/
│ ├── proxy/ # Ядро прокси
│ ├── socks5/ # SOCKS5 сервер
│ ├── websocket/ # WebSocket клиент
│ ├── mtproto/ # MTProto парсинг
│ ├── pool/ # WebSocket pooling
│ ├── config/ # Конфигурация
│ └── telegram/ # Авто-настройка Telegram
├── mobile/ # Android/iOS bindings
│ └── config/ # Конфигурация
├── 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 библиотека)
```
---
### Поддерживаемые платформы
## 📱 Android/iOS
| Платформа | Архитектуры | Статус |
|-----------|-------------|--------|
| Windows | x86_64 | ✅ Готово |
| Linux | x86_64 | ✅ Готово |
| macOS | Intel + Apple Silicon | ✅ Готово |
| Android | arm64, arm, x86_64 | 📝 См. [android/README.md](android/README.md) |
| iOS | arm64 | 🚧 В планах |
```bash
# AAR библиотека
gomobile bind -target android -o android/tgwsproxy.aar ./mobile
**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
}
```
Все оптимизации совместимы с gomobile (Go 1.21+).
## Особенности
---
- ✅ **WebSocket pooling** — пул соединений для уменьшения задержек
- ✅ **TCP fallback** — автоматическое переключение при недоступности WS
- ✅ **MTProto парсинг** — извлечение DC ID из init-пакета
- ✅ **SOCKS5** — полная поддержка RFC 1928
- ✅ **Логирование**с ротацией файлов
- ✅ **Zero-copy** — оптимизированные операции с памятью
## 🔍 Решение проблем
## 📱 Планы развития
**Прокси не подключается:**
1. Проверь, запущен ли `TgWsProxy.exe`
2. Убедись, Telegram настроен на `127.0.0.1:1080`
3. Проверь логи: `%APPDATA%\TgWsProxy\proxy.log`
- [ ] **Android APK** — нативное приложение с фоновой службой
- [ ] **iOS App** — Swift обёртка вокруг Go ядра
- [ ] **GUI для desktop** — системный трей для Windows/macOS/Linux
**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
---
## Ссылки
**v2.0.5** | Built with ❤️ using Go 1.21
- [Оригинальный проект на Python](https://github.com/Flowseal/tg-ws-proxy)
- [Документация Go](https://go.dev/)

View File

@ -7,12 +7,10 @@ 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"
@ -22,42 +20,7 @@ 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")
@ -68,13 +31,7 @@ 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)")
// 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)")
autoConfig := flag.Bool("auto-config", false, "Auto-configure Telegram Desktop on startup")
showVersion := flag.Bool("version", false, "Show version")
flag.Parse()
@ -116,89 +73,41 @@ func main() {
if *auth != "" {
cfg.Auth = *auth
}
if *upstreamProxy != "" {
cfg.UpstreamProxy = *upstreamProxy
}
// Setup logging - log to stdout if verbose, otherwise to file
var logger *log.Logger
// Setup logging - default to file if not specified
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)
if logPath == "" {
// Use default log file in app config directory
appDir := getAppDir()
logPath = filepath.Join(appDir, "proxy.log")
}
logger := setupLogging(logPath, cfg.LogMaxMB, cfg.Verbose)
// Create and start server
server, err := proxy.NewServer(cfg, logger, cfg.UpstreamProxy)
server, err := proxy.NewServer(cfg, logger)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
// 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]
// 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")
}
}
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)
// Check for updates (non-blocking)
go func() {
hasUpdate, latest, url, err := version.CheckUpdate()
if err != nil {
@ -206,18 +115,7 @@ func main() {
}
if hasUpdate {
log.Printf("⚡ NEW VERSION AVAILABLE: v%s (current: v%s)", latest, version.CurrentVersion)
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")
log.Printf(" Download: %s", url)
}
}()
@ -257,12 +155,8 @@ func getAppDir() string {
func setupLogging(logFile string, logMaxMB float64, verbose bool) *log.Logger {
flags := log.LstdFlags | 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)
if verbose {
flags |= log.Lshortfile
}
// Ensure directory exists
@ -273,8 +167,6 @@ 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)
}
@ -296,12 +188,38 @@ func splitDCIP(s string) []string {
if s == "" {
return nil
}
result := make([]string, 0)
for _, part := range strings.Split(s, ",") {
part = strings.TrimSpace(part)
result := []string{}
for _, part := range splitString(s, ",") {
part = 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,5 +1,3 @@
module github.com/Flowseal/tg-ws-proxy
go 1.25.0
require golang.org/x/net v0.52.0 // indirect
go 1.21

2
go.sum
View File

@ -1,2 +0,0 @@
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,16 +13,15 @@ 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
UpstreamProxy string `json:"upstream_proxy"`
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
}
// DefaultConfig returns the default configuration.

View File

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

View File

@ -1,139 +0,0 @@
// 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,19 +2,14 @@
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"
@ -22,7 +17,6 @@ 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 (
@ -85,144 +79,122 @@ var dcOverrides = map[int]int{
// Stats holds proxy statistics.
type Stats struct {
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
mu sync.Mutex
ConnectionsTotal int64
ConnectionsWS int64
ConnectionsTCP int64
ConnectionsHTTP int64
ConnectionsPass int64
WSErrors int64
BytesUp int64
BytesDown int64
PoolHits int64
PoolMisses int64
}
func (s *Stats) addConnectionsTotal(n int64) {
s.ConnectionsTotal.Add(n)
s.mu.Lock()
s.ConnectionsTotal += n
s.mu.Unlock()
}
func (s *Stats) addConnectionsWS(n int64) {
s.ConnectionsWS.Add(n)
s.mu.Lock()
s.ConnectionsWS += n
s.mu.Unlock()
}
func (s *Stats) addConnectionsTCP(n int64) {
s.ConnectionsTCP.Add(n)
s.mu.Lock()
s.ConnectionsTCP += n
s.mu.Unlock()
}
func (s *Stats) addConnectionsHTTP(n int64) {
s.ConnectionsHTTP.Add(n)
s.mu.Lock()
s.ConnectionsHTTP += n
s.mu.Unlock()
}
func (s *Stats) addConnectionsPass(n int64) {
s.ConnectionsPass.Add(n)
s.mu.Lock()
s.ConnectionsPass += n
s.mu.Unlock()
}
func (s *Stats) addWSErrors(n int64) {
s.WSErrors.Add(n)
s.mu.Lock()
s.WSErrors += n
s.mu.Unlock()
}
func (s *Stats) addBytesUp(n int64) {
s.BytesUp.Add(n)
s.mu.Lock()
s.BytesUp += n
s.mu.Unlock()
}
func (s *Stats) addBytesDown(n int64) {
s.BytesDown.Add(n)
s.mu.Lock()
s.BytesDown += n
s.mu.Unlock()
}
func (s *Stats) addPoolHits(n int64) {
s.PoolHits.Add(n)
s.mu.Lock()
s.PoolHits += n
s.mu.Unlock()
}
func (s *Stats) addPoolMisses(n int64) {
s.PoolMisses.Add(n)
s.mu.Lock()
s.PoolMisses += n
s.mu.Unlock()
}
func (s *Stats) Summary() string {
hits := s.PoolHits.Load()
misses := s.PoolMisses.Load()
s.mu.Lock()
defer s.mu.Unlock()
return fmt.Sprintf("total=%d ws=%d tcp=%d http=%d pass=%d err=%d pool=%d/%d up=%s down=%s",
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()))
s.ConnectionsTotal, s.ConnectionsWS, s.ConnectionsTCP,
s.ConnectionsHTTP, s.ConnectionsPass, s.WSErrors,
s.PoolHits, s.PoolHits+s.PoolMisses,
humanBytes(s.BytesUp), humanBytes(s.BytesDown))
}
// 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
upstreamProxy string
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
}
// NewServer creates a new proxy server.
func NewServer(cfg *config.Config, logger *log.Logger, upstreamProxy string) (*Server, error) {
func NewServer(cfg *config.Config, logger *log.Logger) (*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,
upstreamProxy: upstreamProxy,
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,
}
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))
@ -453,9 +425,7 @@ 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.ConnectWithDialer(targetIP, domain, "/apiws", wsTimeout, func(network, addr string) (net.Conn, error) {
return s.dialWithUpstream(network, addr, wsTimeout)
})
ws, wsErr = websocket.Connect(targetIP, domain, "/apiws", wsTimeout)
if wsErr == nil {
allRedirects = false
break
@ -498,7 +468,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 := s.dialWithUpstream("tcp", net.JoinHostPort(dst, fmt.Sprintf("%d", port)), 10*time.Second)
remoteConn, err := net.DialTimeout("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))
@ -513,7 +483,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 := s.dialWithUpstream("tcp6", net.JoinHostPort(ipv6Addr, fmt.Sprintf("%d", port)), 10*time.Second)
remoteConn, err := net.DialTimeout("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()
@ -557,23 +527,40 @@ 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 strings.TrimPrefix(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 ""
}
// extractIPv4FromNAT64 extracts IPv4 from NAT64 IPv6 address.
// Currently returns empty string as NAT64 is not fully supported.
func extractIPv4FromNAT64(ipv6, prefix string) string {
// NAT64 embeds IPv4 in last 32 bits of the IPv6 address
// This is a placeholder for future implementation
// 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
}
}
return ""
}
func (s *Server) handleTCPFallback(conn net.Conn, dst string, port uint16, init []byte, label string, dc int, isMedia bool) {
remoteConn, err := s.dialWithUpstream("tcp", net.JoinHostPort(dst, fmt.Sprintf("%d", port)), 10*time.Second)
remoteConn, err := net.DialTimeout("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
@ -720,9 +707,7 @@ 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.ConnectWithDialer(targetIP, domain, "/apiws", wsConnectTimeout, func(network, addr string) (net.Conn, error) {
return s.dialWithUpstream(network, addr, wsConnectTimeout)
})
ws, err := websocket.Connect(targetIP, domain, "/apiws", wsConnectTimeout)
if err == nil {
s.wsPool.Put(dcKey, ws)
break
@ -833,15 +818,17 @@ func (s *Server) logDebug(format string, args ...interface{}) {
// Helper functions
func ipToUint32(ip string) uint32 {
ipObj := net.ParseIP(ip)
if ipObj == nil {
parts := strings.Split(ip, ".")
if len(parts) != 4 {
return 0
}
ipObj = ipObj.To4()
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)
}
return binary.BigEndian.Uint32(ipObj)
return result
}
func isTelegramIP(ip string) bool {
@ -858,10 +845,22 @@ func isHTTPTransport(data []byte) bool {
if len(data) < 5 {
return false
}
return bytes.HasPrefix(data, []byte("POST ")) ||
bytes.HasPrefix(data, []byte("GET ")) ||
bytes.HasPrefix(data, []byte("HEAD ")) ||
bytes.HasPrefix(data, []byte("OPTIONS "))
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
}
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, &AuthConfig{})
nmethods, err := HandleGreeting(server)
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, &AuthConfig{})
_, err := HandleGreeting(server)
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, &AuthConfig{})
_, err := HandleGreeting(server)
if err != ErrNoAuthAccepted {
t.Errorf("Expected ErrNoAuthAccepted, got %v", err)
}

View File

@ -8,35 +8,20 @@ import (
"strings"
)
// ConfigureProxy opens Telegram's SOCKS5 proxy configuration URL.
// ConfigureProxy opens Telegram's proxy configuration URL.
// Returns true if successful, false otherwise.
func ConfigureProxy(host string, port int, username, password string) bool {
return ConfigureProxyWithType(host, port, username, password, "", "socks5")
}
// Build tg:// proxy URL
url := fmt.Sprintf("tg://socks?server=%s&port=%d", host, port)
// 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)
if username != "" {
url += fmt.Sprintf("&user=%s", username)
}
if password != "" {
url += fmt.Sprintf("&pass=%s", password)
}
// Open URL using system default handler
return openURL(proxyURL)
return openURL(url)
}
// openURL opens a URL in the default browser/application.
@ -46,19 +31,18 @@ func openURL(url string) bool {
switch runtime.GOOS {
case "windows":
// Use rundll32 to open URL - more reliable for protocol handlers
cmd = "rundll32"
args = []string{"url.dll,FileProtocolHandler", url}
cmd = "cmd"
args = []string{"/c", "start"}
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,30 +6,20 @@ import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
const (
CurrentVersion = "2.0.5"
CurrentVersion = "2.0.0"
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"`
Assets []Asset `json:"assets"`
}
type Asset struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
HTMLURL string `json:"html_url"`
}
// CheckUpdate checks for new version on GitHub.
@ -63,85 +53,6 @@ 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 {
@ -171,12 +82,7 @@ func splitVersion(v string) []int {
parts := strings.Split(v, ".")
result := make([]int, len(parts))
for i, p := range parts {
n, err := strconv.Atoi(p)
if err != nil {
result[i] = 0
} else {
result[i] = n
}
fmt.Sscanf(p, "%d", &result[i])
}
return result
}

View File

@ -44,12 +44,6 @@ 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"
}
@ -62,55 +56,18 @@ func ConnectWithDialer(ip, domain, path string, timeout time.Duration, dialFunc
wsKey := base64.StdEncoding.EncodeToString(keyBytes)
// Dial TLS connection
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)
}
dialer := &net.Dialer{Timeout: timeout}
tlsConfig := &tls.Config{
ServerName: domain,
InsecureSkipVerify: true,
}
// Clear deadline after handshake
if err := rawConn.SetDeadline(time.Time{}); err != nil {
rawConn.Close()
return nil, err
rawConn, err := tls.DialWithDialer(dialer, "tcp", net.JoinHostPort(ip, "443"), tlsConfig)
if err != nil {
return nil, fmt.Errorf("tls dial: %w", err)
}
// Set TCP_NODELAY and buffer sizes
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 {
if tcpConn, ok := rawConn.NetConn().(*net.TCPConn); ok {
tcpConn.SetNoDelay(true)
tcpConn.SetReadBuffer(256 * 1024)
tcpConn.SetWriteBuffer(256 * 1024)
@ -158,7 +115,7 @@ func ConnectWithDialer(ip, domain, path string, timeout time.Duration, dialFunc
}
return &WebSocket{
conn: rawConn.(*tls.Conn),
conn: rawConn,
reader: reader,
writer: bufio.NewWriter(rawConn),
maskKey: make([]byte, 4),

View File

@ -8,7 +8,6 @@ import (
"net"
"os"
"path/filepath"
"strings"
"github.com/Flowseal/tg-ws-proxy/internal/config"
"github.com/Flowseal/tg-ws-proxy/internal/proxy"
@ -39,23 +38,18 @@ func Start(host string, port int, dcIP string, verbose bool) string {
if err != nil {
return fmt.Sprintf("Failed to open log file: %v", err)
}
logger := log.New(f, "", log.Ldate|log.Ltime)
log.SetOutput(f)
log.SetFlags(log.Ldate | log.Ltime)
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
server, err = proxy.NewServer(cfg, logger, "")
if err != nil {
server = proxy.NewServer(cfg)
if err := server.Start(ctx); err != nil {
cancel()
return fmt.Sprintf("Failed to create server: %v", err)
return fmt.Sprintf("Failed to start proxy: %v", err)
}
go func() {
if err := server.Start(ctx); err != nil {
cancel()
}
}()
return "OK"
}
@ -64,6 +58,9 @@ func Stop() string {
if cancel != nil {
cancel()
}
if server != nil {
server.Stop()
}
return "OK"
}
@ -72,7 +69,13 @@ func GetStatus() string {
if server == nil {
return "Not running"
}
return "Running" // Simplified for mobile
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)
}
// parseDCIP parses DC IP configuration string.
@ -80,11 +83,11 @@ func parseDCIP(s string) []string {
if s == "" {
return nil
}
result := make([]string, 0)
for _, part := range strings.Split(s, ",") {
part = strings.TrimSpace(part)
if part != "" {
result = append(result, part)
result := []string{}
for _, part := range split(s, ",") {
trimmed := trim(part)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
@ -100,6 +103,32 @@ 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