Compare commits

..

No commits in common. "master" and "v2.0.2" 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 @@
<!-- Спасибо за ваш вклад в проект! Пожалуйста, заполните эту форму --> 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
**ОС:** Windows / macOS / Linux options:
- label: I have tested this locally
**Шаги для тестирования:** required: true
1. - label: Code follows project guidelines
2. required: true
3.
## Скриншоты (если применимо)
<!-- Добавьте скриншоты, если изменения влияют на UI -->
## Дополнительные заметки
<!-- Любая дополнительная информация, которая может быть полезна -->

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 # 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) [![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 | ## Почему Go версия лучше
|---------|-------|-------|
| [⬇️ .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 ```bash
TgWsProxy.exe [флаги] # Скачать готовый бинарник из Releases
# Или собрать из исходников
go build -o TgWsProxy.exe ./cmd/proxy
``` ```
| Флаг | Описание | По умолчанию | ### Запуск
|------|----------|--------------|
| `--port` | Порт SOCKS5 | 1080 | ```bash
| `--host` | Хост | 127.0.0.1 | # Windows
| `--dc-ip` | DC:IP (через запятую) | авто | start run.bat
| `--auth` | Логин:пароль для прокси | — |
| `--http-port` | HTTP прокси (для браузеров) | 0 (выкл) | # Windows с авто-настройкой Telegram
| `--upstream-proxy` | Цепочка через другой прокси | — | TgWsProxy.exe --auto-config
| `-v` | Подробные логи | false |
# 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 ```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/ tg-ws-proxy/
├── cmd/proxy/ # CLI приложение ├── cmd/
│ └── proxy/ # CLI приложение
├── internal/ ├── internal/
│ ├── proxy/ # Ядро прокси │ ├── proxy/ # Ядро прокси
│ ├── socks5/ # SOCKS5 сервер │ ├── socks5/ # SOCKS5 сервер
│ ├── websocket/ # WebSocket клиент │ ├── websocket/ # WebSocket клиент
│ ├── mtproto/ # MTProto парсинг │ ├── mtproto/ # MTProto парсинг
│ ├── pool/ # WebSocket pooling │ └── config/ # Конфигурация
│ ├── config/ # Конфигурация
│ └── telegram/ # Авто-настройка Telegram
├── mobile/ # Android/iOS bindings
├── go.mod ├── go.mod
├── Makefile ├── Makefile
└── README.md └── README.md
``` ```
--- ## Сборка
## 🛠️ Сборка
```bash ```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 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 **macOS Catalina (10.15)** — поддерживается! Используйте `TgWsProxy_macos_amd64`.
# AAR библиотека
gomobile bind -target android -o android/tgwsproxy.aar ./mobile ## Конфигурация
Файл конфигурации:
- **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** — оптимизированные операции с памятью
## 🔍 Решение проблем ## 📱 Планы развития
**Прокси не подключается:** - [ ] **Android APK** — нативное приложение с фоновой службой
1. Проверь, запущен ли `TgWsProxy.exe` - [ ] **iOS App** — Swift обёртка вокруг Go ядра
2. Убедись, Telegram настроен на `127.0.0.1:1080` - [ ] **GUI для desktop** — системный трей для Windows/macOS/Linux
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 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" "fmt"
"log" "log"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall" "syscall"
"time"
"github.com/Flowseal/tg-ws-proxy/internal/config" "github.com/Flowseal/tg-ws-proxy/internal/config"
"github.com/Flowseal/tg-ws-proxy/internal/proxy" "github.com/Flowseal/tg-ws-proxy/internal/proxy"
@ -22,42 +20,7 @@ import (
var appVersion = "2.0.0" 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() { func main() {
// Check for existing instances and terminate them (Windows only)
if os.PathSeparator == '\\' {
checkAndKillExisting()
}
// Parse flags // Parse flags
port := flag.Int("port", 1080, "Listen port") port := flag.Int("port", 1080, "Listen port")
host := flag.String("host", "127.0.0.1", "Listen host") 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") bufKB := flag.Int("buf-kb", 256, "Socket buffer size in KB")
poolSize := flag.Int("pool-size", 4, "WS pool size per DC") poolSize := flag.Int("pool-size", 4, "WS pool size per DC")
auth := flag.String("auth", "", "SOCKS5 authentication (username:password)") 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") showVersion := flag.Bool("version", false, "Show version")
flag.Parse() flag.Parse()
@ -116,89 +73,41 @@ func main() {
if *auth != "" { if *auth != "" {
cfg.Auth = *auth cfg.Auth = *auth
} }
if *upstreamProxy != "" {
cfg.UpstreamProxy = *upstreamProxy
}
// Setup logging - log to stdout if verbose, otherwise to file // Setup logging - default to file if not specified
var logger *log.Logger
logPath := *logFile logPath := *logFile
if cfg.Verbose && logPath == "" { if logPath == "" {
// Verbose mode: log to stdout // Use default log file in app config directory
logger = setupLogging("", cfg.LogMaxMB, cfg.Verbose) appDir := getAppDir()
} else { logPath = filepath.Join(appDir, "proxy.log")
// 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 // Create and start server
server, err := proxy.NewServer(cfg, logger, cfg.UpstreamProxy) server, err := proxy.NewServer(cfg, logger)
if err != nil { if err != nil {
log.Fatalf("Failed to create server: %v", err) log.Fatalf("Failed to create server: %v", err)
} }
// Auto-configure Telegram Desktop with correct proxy type // Auto-configure Telegram Desktop
log.Println("Attempting to configure Telegram Desktop...") if *autoConfig {
log.Println("Attempting to auto-configure Telegram Desktop...")
// Determine proxy type and configure Telegram accordingly username, password := "", ""
// Note: Our local proxy only supports SOCKS5 if cfg.Auth != "" {
// HTTP port is for other applications (browsers, etc.) parts := strings.SplitN(cfg.Auth, ":", 2)
// MTProto requires external MTProxy server if len(parts) == 2 {
proxyType := "socks5" // Always SOCKS5 for our local proxy username, password = parts[0], parts[1]
proxyPort := cfg.Port }
proxySecret := "" }
if telegram.ConfigureProxy(cfg.Host, cfg.Port, username, password) {
// Log HTTP mode if enabled (for other apps, not Telegram) log.Println("✓ Telegram Desktop proxy configuration opened")
if *httpPort != 0 { } else {
log.Printf("⚙ HTTP proxy enabled on port %d (for browsers/other apps)", *httpPort) log.Println("✗ Failed to open Telegram Desktop. Please configure manually.")
} log.Println(" Open in browser: tg://socks?server=127.0.0.1&port=1080")
// 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]
} }
} }
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() { go func() {
hasUpdate, latest, url, err := version.CheckUpdate() hasUpdate, latest, url, err := version.CheckUpdate()
if err != nil { if err != nil {
@ -206,18 +115,7 @@ func main() {
} }
if hasUpdate { if hasUpdate {
log.Printf("⚡ NEW VERSION AVAILABLE: v%s (current: v%s)", latest, version.CurrentVersion) log.Printf("⚡ NEW VERSION AVAILABLE: v%s (current: v%s)", latest, version.CurrentVersion)
log.Printf(" Downloading update...") log.Printf(" Download: %s", url)
// 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")
} }
}() }()
@ -257,12 +155,8 @@ func getAppDir() string {
func setupLogging(logFile string, logMaxMB float64, verbose bool) *log.Logger { func setupLogging(logFile string, logMaxMB float64, verbose bool) *log.Logger {
flags := log.LstdFlags | log.Lshortfile flags := log.LstdFlags | log.Lshortfile
if verbose {
// If verbose and no log file specified, log to stdout flags |= log.Lshortfile
if verbose && logFile == "" {
log.SetOutput(os.Stdout)
log.SetFlags(flags)
return log.New(os.Stdout, "", flags)
} }
// Ensure directory exists // 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) f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { if err != nil {
log.Printf("Warning: failed to open log file %s: %v, using stdout", logFile, err) 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) return log.New(os.Stdout, "", flags)
} }
@ -296,12 +188,38 @@ func splitDCIP(s string) []string {
if s == "" { if s == "" {
return nil return nil
} }
result := make([]string, 0) result := []string{}
for _, part := range strings.Split(s, ",") { for _, part := range splitString(s, ",") {
part = strings.TrimSpace(part) part = trimSpace(part)
if part != "" { if part != "" {
result = append(result, part) result = append(result, part)
} }
} }
return result 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 module github.com/Flowseal/tg-ws-proxy
go 1.25.0 go 1.21
require golang.org/x/net v0.52.0 // indirect

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. // Config holds the proxy configuration.
type Config struct { type Config struct {
Port int `json:"port"` Port int `json:"port"`
Host string `json:"host"` Host string `json:"host"`
DCIP []string `json:"dc_ip"` DCIP []string `json:"dc_ip"`
Verbose bool `json:"verbose"` Verbose bool `json:"verbose"`
AutoStart bool `json:"autostart"` AutoStart bool `json:"autostart"`
LogMaxMB float64 `json:"log_max_mb"` LogMaxMB float64 `json:"log_max_mb"`
BufKB int `json:"buf_kb"` BufKB int `json:"buf_kb"`
PoolSize int `json:"pool_size"` PoolSize int `json:"pool_size"`
Auth string `json:"auth"` // username:password Auth string `json:"auth"` // username:password
UpstreamProxy string `json:"upstream_proxy"`
} }
// DefaultConfig returns the default configuration. // DefaultConfig returns the default configuration.

View File

@ -91,13 +91,15 @@ func PatchInitDC(data []byte, dc int) ([]byte, bool) {
keystream := make([]byte, 8) keystream := make([]byte, 8)
stream.XORKeyStream(keystream, zero64[56:64]) 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)) patched := make([]byte, len(data))
copy(patched, data) copy(patched, data)
// Patch bytes 60-61 directly newDC := make([]byte, 2)
patched[60] = keystream[0] ^ byte(dc) binary.LittleEndian.PutUint16(newDC, uint16(dc))
patched[61] = keystream[1] ^ byte(dc>>8)
patched[60] = keystream[0] ^ newDC[0]
patched[61] = keystream[1] ^ newDC[1]
return patched, true 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 package proxy
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"fmt" "fmt"
"io" "io"
"log" "log"
"net" "net"
"net/http"
"net/url"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/Flowseal/tg-ws-proxy/internal/config" "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/pool"
"github.com/Flowseal/tg-ws-proxy/internal/socks5" "github.com/Flowseal/tg-ws-proxy/internal/socks5"
"github.com/Flowseal/tg-ws-proxy/internal/websocket" "github.com/Flowseal/tg-ws-proxy/internal/websocket"
"golang.org/x/net/proxy"
) )
const ( const (
@ -85,144 +79,122 @@ var dcOverrides = map[int]int{
// Stats holds proxy statistics. // Stats holds proxy statistics.
type Stats struct { type Stats struct {
ConnectionsTotal atomic.Int64 mu sync.Mutex
ConnectionsWS atomic.Int64 ConnectionsTotal int64
ConnectionsTCP atomic.Int64 ConnectionsWS int64
ConnectionsHTTP atomic.Int64 ConnectionsTCP int64
ConnectionsPass atomic.Int64 ConnectionsHTTP int64
WSErrors atomic.Int64 ConnectionsPass int64
BytesUp atomic.Int64 WSErrors int64
BytesDown atomic.Int64 BytesUp int64
PoolHits atomic.Int64 BytesDown int64
PoolMisses atomic.Int64 PoolHits int64
PoolMisses int64
} }
func (s *Stats) addConnectionsTotal(n 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { func (s *Stats) addPoolMisses(n int64) {
s.PoolMisses.Add(n) s.mu.Lock()
s.PoolMisses += n
s.mu.Unlock()
} }
func (s *Stats) Summary() string { func (s *Stats) Summary() string {
hits := s.PoolHits.Load() s.mu.Lock()
misses := s.PoolMisses.Load() 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", 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.ConnectionsTotal, s.ConnectionsWS, s.ConnectionsTCP,
s.ConnectionsHTTP.Load(), s.ConnectionsPass.Load(), s.WSErrors.Load(), s.ConnectionsHTTP, s.ConnectionsPass, s.WSErrors,
hits, hits+misses, s.PoolHits, s.PoolHits+s.PoolMisses,
humanBytes(s.BytesUp.Load()), humanBytes(s.BytesDown.Load())) humanBytes(s.BytesUp), humanBytes(s.BytesDown))
} }
// Server represents the TG WS Proxy server. // Server represents the TG WS Proxy server.
type Server struct { type Server struct {
config *config.Config config *config.Config
dcOpt map[int]string dcOpt map[int]string
wsPool *pool.WSPool wsPool *pool.WSPool
stats *Stats stats *Stats
wsBlacklist map[pool.DCKey]bool wsBlacklist map[pool.DCKey]bool
dcFailUntil map[pool.DCKey]time.Time dcFailUntil map[pool.DCKey]time.Time
mu sync.RWMutex mu sync.RWMutex
listener net.Listener listener net.Listener
logger *log.Logger logger *log.Logger
upstreamProxy string
} }
// NewServer creates a new proxy server. // 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) dcOpt, err := config.ParseDCIPList(cfg.DCIP)
if err != nil { if err != nil {
return nil, err return nil, err
} }
s := &Server{ s := &Server{
config: cfg, config: cfg,
dcOpt: dcOpt, dcOpt: dcOpt,
wsPool: pool.NewWSPool(cfg.PoolSize, defaultPoolMaxAge), wsPool: pool.NewWSPool(cfg.PoolSize, defaultPoolMaxAge),
stats: &Stats{}, stats: &Stats{},
wsBlacklist: make(map[pool.DCKey]bool), wsBlacklist: make(map[pool.DCKey]bool),
dcFailUntil: make(map[pool.DCKey]time.Time), dcFailUntil: make(map[pool.DCKey]time.Time),
logger: logger, logger: logger,
upstreamProxy: upstreamProxy,
} }
return s, nil 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. // Start starts the proxy server.
func (s *Server) Start(ctx context.Context) error { func (s *Server) Start(ctx context.Context) error {
addr := net.JoinHostPort(s.config.Host, fmt.Sprintf("%d", s.config.Port)) 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) 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 // Connect using targetIP, but use domain for TLS handshake
ws, wsErr = websocket.ConnectWithDialer(targetIP, domain, "/apiws", wsTimeout, func(network, addr string) (net.Conn, error) { ws, wsErr = websocket.Connect(targetIP, domain, "/apiws", wsTimeout)
return s.dialWithUpstream(network, addr, wsTimeout)
})
if wsErr == nil { if wsErr == nil {
allRedirects = false allRedirects = false
break 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) { 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 { if err != nil {
s.logWarning("[%s] passthrough failed to %s: %v", label, dst, err) s.logWarning("[%s] passthrough failed to %s: %v", label, dst, err)
conn.Write(socks5.Reply(socks5.ReplyFail)) 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. // handleIPv6Connection handles IPv6 connections via dual-stack or IPv4-mapped addresses.
func (s *Server) handleIPv6Connection(conn net.Conn, ipv6Addr string, port uint16, label string) { func (s *Server) handleIPv6Connection(conn net.Conn, ipv6Addr string, port uint16, label string) {
// Try direct IPv6 first // 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 { if err == nil {
s.logInfo("[%s] IPv6 direct connection successful", label) s.logInfo("[%s] IPv6 direct connection successful", label)
defer remoteConn.Close() 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. // extractIPv4 tries to extract IPv4 from IPv4-mapped IPv6 address.
func extractIPv4(ipv6 string) string { func extractIPv4(ipv6 string) string {
// Check for ::ffff: prefix (IPv4-mapped) // Check for ::ffff: prefix (IPv4-mapped)
// Example: ::ffff:192.0.2.1
if strings.HasPrefix(strings.ToLower(ipv6), "::ffff:") { 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 "" return ""
} }
// extractIPv4FromNAT64 extracts IPv4 from NAT64 IPv6 address. // extractIPv4FromNAT64 extracts IPv4 from NAT64 IPv6 address.
// Currently returns empty string as NAT64 is not fully supported.
func extractIPv4FromNAT64(ipv6, prefix string) string { func extractIPv4FromNAT64(ipv6, prefix string) string {
// NAT64 embeds IPv4 in last 32 bits of the IPv6 address // Remove prefix
// This is a placeholder for future implementation 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 "" return ""
} }
func (s *Server) handleTCPFallback(conn net.Conn, dst string, port uint16, init []byte, label string, dc int, isMedia bool) { 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 { if err != nil {
s.logWarning("[%s] TCP fallback to %s:%d failed: %v", label, dst, port, err) s.logWarning("[%s] TCP fallback to %s:%d failed: %v", label, dst, port, err)
return return
@ -720,9 +707,7 @@ func (s *Server) warmupPool() {
go func(dcKey pool.DCKey, targetIP string, domains []string) { go func(dcKey pool.DCKey, targetIP string, domains []string) {
for s.wsPool.NeedRefill(dcKey) { for s.wsPool.NeedRefill(dcKey) {
for _, domain := range domains { for _, domain := range domains {
ws, err := websocket.ConnectWithDialer(targetIP, domain, "/apiws", wsConnectTimeout, func(network, addr string) (net.Conn, error) { ws, err := websocket.Connect(targetIP, domain, "/apiws", wsConnectTimeout)
return s.dialWithUpstream(network, addr, wsConnectTimeout)
})
if err == nil { if err == nil {
s.wsPool.Put(dcKey, ws) s.wsPool.Put(dcKey, ws)
break break
@ -833,15 +818,17 @@ func (s *Server) logDebug(format string, args ...interface{}) {
// Helper functions // Helper functions
func ipToUint32(ip string) uint32 { func ipToUint32(ip string) uint32 {
ipObj := net.ParseIP(ip) parts := strings.Split(ip, ".")
if ipObj == nil { if len(parts) != 4 {
return 0 return 0
} }
ipObj = ipObj.To4() var result uint32
if ipObj == nil { for i, part := range parts {
return 0 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 { func isTelegramIP(ip string) bool {
@ -858,10 +845,22 @@ func isHTTPTransport(data []byte) bool {
if len(data) < 5 { if len(data) < 5 {
return false return false
} }
return bytes.HasPrefix(data, []byte("POST ")) || return bytesEqual(data[:5], []byte("POST ")) ||
bytes.HasPrefix(data, []byte("GET ")) || bytesEqual(data[:4], []byte("GET ")) ||
bytes.HasPrefix(data, []byte("HEAD ")) || bytesEqual(data[:5], []byte("HEAD ")) ||
bytes.HasPrefix(data, []byte("OPTIONS ")) 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 { func humanBytes(n int64) string {

View File

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

View File

@ -8,35 +8,20 @@ import (
"strings" "strings"
) )
// ConfigureProxy opens Telegram's SOCKS5 proxy configuration URL. // ConfigureProxy opens Telegram's proxy configuration URL.
// Returns true if successful, false otherwise. // Returns true if successful, false otherwise.
func ConfigureProxy(host string, port int, username, password string) bool { 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 { if username != "" {
case "mtproto": url += fmt.Sprintf("&user=%s", username)
// 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 password != "" {
// Open URL using system default handler url += fmt.Sprintf("&pass=%s", password)
return openURL(proxyURL) }
return openURL(url)
} }
// openURL opens a URL in the default browser/application. // openURL opens a URL in the default browser/application.
@ -46,19 +31,18 @@ func openURL(url string) bool {
switch runtime.GOOS { switch runtime.GOOS {
case "windows": case "windows":
// Use rundll32 to open URL - more reliable for protocol handlers cmd = "cmd"
cmd = "rundll32" args = []string{"/c", "start"}
args = []string{"url.dll,FileProtocolHandler", url}
case "darwin": case "darwin":
cmd = "open" cmd = "open"
args = []string{url}
case "linux": case "linux":
cmd = "xdg-open" cmd = "xdg-open"
args = []string{url}
default: default:
return false return false
} }
args = append(args, url)
err := exec.Command(cmd, args...).Start() err := exec.Command(cmd, args...).Start()
return err == nil return err == nil
} }

View File

@ -6,30 +6,20 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings" "strings"
"time" "time"
) )
const ( const (
CurrentVersion = "2.0.5" CurrentVersion = "2.0.0"
RepoURL = "https://api.github.com/repos/y0sy4/tg-ws-proxy-go/releases/latest" RepoURL = "https://api.github.com/repos/y0sy4/tg-ws-proxy-go/releases/latest"
) )
type Release struct { type Release struct {
TagName string `json:"tag_name"` TagName string `json:"tag_name"`
Name string `json:"name"` Name string `json:"name"`
Body string `json:"body"` Body string `json:"body"`
HTMLURL string `json:"html_url"` 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. // CheckUpdate checks for new version on GitHub.
@ -63,85 +53,6 @@ func CheckUpdate() (bool, string, string, error) {
return false, current, "", nil 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. // compareVersions compares two semantic versions.
// Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal. // Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal.
func compareVersions(v1, v2 string) int { func compareVersions(v1, v2 string) int {
@ -171,12 +82,7 @@ func splitVersion(v string) []int {
parts := strings.Split(v, ".") parts := strings.Split(v, ".")
result := make([]int, len(parts)) result := make([]int, len(parts))
for i, p := range parts { for i, p := range parts {
n, err := strconv.Atoi(p) fmt.Sscanf(p, "%d", &result[i])
if err != nil {
result[i] = 0
} else {
result[i] = n
}
} }
return result return result
} }

View File

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

View File

@ -8,7 +8,6 @@ import (
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/Flowseal/tg-ws-proxy/internal/config" "github.com/Flowseal/tg-ws-proxy/internal/config"
"github.com/Flowseal/tg-ws-proxy/internal/proxy" "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 { if err != nil {
return fmt.Sprintf("Failed to open log file: %v", err) 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 var ctx context.Context
ctx, cancel = context.WithCancel(context.Background()) ctx, cancel = context.WithCancel(context.Background())
server, err = proxy.NewServer(cfg, logger, "") server = proxy.NewServer(cfg)
if err != nil { if err := server.Start(ctx); err != nil {
cancel() 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" return "OK"
} }
@ -64,6 +58,9 @@ func Stop() string {
if cancel != nil { if cancel != nil {
cancel() cancel()
} }
if server != nil {
server.Stop()
}
return "OK" return "OK"
} }
@ -72,7 +69,13 @@ func GetStatus() string {
if server == nil { if server == nil {
return "Not running" 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. // parseDCIP parses DC IP configuration string.
@ -80,11 +83,11 @@ func parseDCIP(s string) []string {
if s == "" { if s == "" {
return nil return nil
} }
result := make([]string, 0) result := []string{}
for _, part := range strings.Split(s, ",") { for _, part := range split(s, ",") {
part = strings.TrimSpace(part) trimmed := trim(part)
if part != "" { if trimmed != "" {
result = append(result, part) result = append(result, trimmed)
} }
} }
return result return result
@ -100,6 +103,32 @@ func getLogDir() string {
return os.TempDir() 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) // Dummy function to use net package (required for SOCKS5)
func init() { func init() {
_ = net.Dial _ = net.Dial