Compare commits
No commits in common. "master" and "v2.0.1" have entirely different histories.
|
|
@ -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: Полная документация проекта
|
|
||||||
|
|
@ -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 -->
|
|
||||||
|
|
||||||
## Дополнительные заметки
|
|
||||||
<!-- Любая дополнительная информация, которая может быть полезна -->
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
205
FAQ.md
|
|
@ -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
287
README.md
|
|
@ -1,187 +1,212 @@
|
||||||
# TG WS Proxy Go
|
# TG WS Proxy Go
|
||||||
|
|
||||||
[](https://github.com/y0sy4/tg-ws-proxy-go/releases)
|
[](go.mod)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
[](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/)
|
||||||
|
|
|
||||||
|
|
@ -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
4
go.mod
|
|
@ -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
2
go.sum
|
|
@ -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=
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
|
||||||
}
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue