Files
2026-09-22 22:33:56 +03:00

160 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Интеграция free-turn-proxy через gomobile
Пакет `mobile` предоставляет API для встраивания ядра прокси в нативные мобильные приложения (iOS/Android) через `gomobile bind`.
## Сборка
### iOS
Сборка универсального XCFramework (`dist/Mobile.xcframework`) с поддержкой iOS и симулятора:
```bash
task build:ios
```
### Android
Сборка Android Archive (`dist/freeturn.aar` и `freeturn-sources.jar`) с JNI-обертками для Java/Kotlin:
```bash
task build:android
```
## API
Хост-приложение передает конфигурацию в формате JSON, а события получает через интерфейс обратного вызова.
```go
func Start(configJSON string) error // Запуск в режиме прокси
func StartTunnel(configJSON string, tunFD int) error // Запуск прокси + WireGuard/AmneziaWG
func StartDirectTunnel(wgText string, mtu, tunFD int) error // WireGuard/AmneziaWG напрямую, без релея
func Restart(configJSON string, tunFD int) error // Перезапуск с ожиданием остановки (tunFD 0 - без туннеля)
func Stop() // Остановка сессии
func GetState() *Snapshot // Метрики сессии (State, Rates, Streams)
func TunnelStats() *TunnelSnapshot // Статистика туннеля
func ParseTunnelConfig(wgText string, mtu int) (*TunnelParams, error) // Параметры tun для платформы
func SetEventSink(s EventSink) // Установка коллбека событий
func SetProtect(p Protector) // Защита сокетов (VpnService.protect)
func DefaultConfigJSON() string // Конфиг с дефолтами
func ValidateConfig(configJSON string) string // Валидация JSON
func ConfigToArgs(configJSON string) (string, error) // Вывод эквивалентной CLI-команды
func DumpLogs() string
func ClearLogs()
func SetLogBuffer(enabled bool)
func Version() string
```
В Java класс экспортируется как `com.freeturn.core.mobile.Mobile`. `int` в Go транслируется в `long` на стороне Kotlin/Java.
### Конфигурация JSON
Пример схемы JSON:
```json
{
"peer": "1.2.3.4:56000",
"clientId": "0123456789abcdef0123456789abcdef",
"provider": "vk",
"turn": {"n": 12, "transport": "tcp", "host": "", "port": ""},
"proxy": {"mode": "udp", "listen": "127.0.0.1:9000"},
"vk": {"links": ["<call-link>"], "streamsPerCred": 12,
"manualCaptcha": false, "platform": "mobile"},
"obf": {"profile": "rtpopus3", "key": "<64 hex>", "timingMs": 0},
"dns": {"mode": "auto", "servers": ["8.8.8.8"]},
"log": {"debug": false},
"tunnel": {"mode": "none", "config": "", "mtu": 1280},
"subUrl": ""
}
```
* `clientId` - обязателен. Ядро на мобиле не пишет файлы, ID должен храниться в приложении.
* `provider`: `vk` или `direct` (без реле, прямо на `peer`; блок `vk` игнорируется). `turn.n` = 0 - дефолт провайдера (12 / 1).
### Режим туннеля
`proxy.mode`: `udp` (по умолчанию, UDP-релей для WireGuard) или `tcp` (TCP-форвардер для Xray/sing-box).
`proxy.bond: true` включает объединение сессий для каждого TCP-соединения; допускается только с `proxy.mode: "tcp"`, по умолчанию выключено. Нужны обновлённые сервер и AAR. При отказе одной участвующей сессии соединение закрывается; новое использует оставшиеся сессии. Публичные методы gomobile не меняются.
В `tcp` ядро слушает `proxy.listen` как TCP-порт, и VLESS-клиент ходит туда своим outbound. Своего tun при этом нет:
* запускать через `Start(configJSON)`, не через `StartTunnel`; `VpnService.prepare`/`establish` звать не нужно - иначе tun отбирается у чужого VPN;
* `protect()` не применяется, поэтому трафик до TURN пойдёт через активный системный VPN, если он включён;
* `TunnelStats()` не наполняется, счётчики отдаёт `GetState()`, `connected`/`total` считаются по сессиям пула так же, как в udp-режиме;
* `proxy.mode: "tcp"` вместе с `tunnel.mode` `wg`/`awg` отклоняется валидацией: встроенный WG гонит датаграммы;
* `StartTunnel` при `proxy.mode: "tcp"` возвращает `ErrTCPModeRequiresStart` - ядро tun не читает, и принятый fd остался бы установленным вхолостую.
Опциональная секция `kcp` (`noDelay`, `interval`, `resend`, `nc`, `sndWnd`, `rcvWnd`, `mtu`, `ackNoDelay`) тюнит ARQ tcp-режима; в `udp` любое отличие от дефолта - ошибка. Дефолт агрессивный (`interval: 20`, окна 512, `ackNoDelay: true`), для мобильной сети щадящий вариант - `interval: 40`, окна 256, `ackNoDelay: false`.
JSON парсится с `DisallowUnknownFields`, поэтому конфиг с `proxy.mode`/`kcp` требует aar с этим изменением; старый JSON без них новое ядро читает как `udp`.
### Туннелирование
`tunnel.mode` принимает значения `none`, `wg` (WireGuard) или `awg` (AmneziaWG).
Текст WireGuard-конфигурации (`wg-quick`) передается в `tunnel.config`. В режиме `wg` параметры обфускации AmneziaWG игнорируются. Поле `Endpoint` из конфига не используется, так как трафик идет через TURN-релей.
В секции `[Interface]` понимаются параметры AmneziaWG: `Jc`/`Jmin`/`Jmax`, `S1`-`S4`, `H1`-`H4`, `I1`-`I5`, а также AWG 3+:
| Ключ | Значение | Должен совпадать с сервером |
|---|---|---|
| `HeaderProtectionKey` | base64-ключ 32 байта | да |
| `ContentPaddingAddition` | `N` или `LO-HI` | нет |
| `RekeyAfterTime`, `RekeyTimeout`, `RejectAfterTime`, `KeepaliveTimeout` | `N` или `LO-HI`, секунды | нет |
| `MaxHandshakeAttempts` | `N` или `LO-HI`, попытки | нет |
| `RandomTrailers` | bool | да |
| `DisableCookies` | bool | нет |
`HeaderProtectionKey` берёт nonce из crypto-паддинга, поэтому требует `S1`-`S4` не меньше 12 - иначе конфиг отклоняется.
Адреса, DNS, маршруты и MTU tun-интерфейса ядро не применяет - это работа платформы. Их отдаёт `ParseTunnelConfig` из того же текста, что уходит в `tunnel.config` (аргумент `mtu` - то же значение, что в `tunnel.mtu`; `0` - взять из конфига):
```kotlin
val p = Mobile.parseTunnelConfig(wgText, 1280)
val builder = VpnService.Builder().setMtu(p.mtu.toInt())
p.addresses.split(",").forEach { builder.addAddress(it.substringBefore("/"), it.substringAfter("/").toInt()) }
p.dns.split(",").filter { it.isNotEmpty() }.forEach(builder::addDnsServer)
p.allowedIPs.split(",").forEach { builder.addRoute(it.substringBefore("/"), it.substringAfter("/").toInt()) }
```
Для `wg`/`awg` нужен запуск через `StartTunnel`:
```kotlin
pfd = builder.establish()!! // хранится в сервисе, живёт дольше сессии
Mobile.startTunnel(configJson, pfd.dup().detachFd().toLong())
```
Ядро закрывает переданный дескриптор при остановке, поэтому отдаётся копия (`dup`), а оригинал остаётся у приложения: tun переживает `Restart` при смене сети, интерфейс не пересоздаётся и VPN не мигает. На каждый `StartTunnel`/`Restart` с туннелем нужна новая копия; закрывать отданный fd самостоятельно нельзя - ядро закрывает его и когда старт не удался.
`Restart(configJSON, 0)` - перезапуск в режиме прокси: ядро дескриптор не берёт и ничего не закрывает. Конфиг с `tunnel.mode` `wg`/`awg` в этом виде отклоняется (`ErrTunnelRequiresStartTunnel`) - для туннеля передаётся `tunFD > 0`.
WireGuard общается с релеем через in-memory `netconn.PacketPipe` напрямую. Петля `127.0.0.1:9000` не используется, поэтому исключать приложение из VPN-маршрутов не требуется. Защите через `SetProtect` подлежат только сокеты релея.
### Прямой туннель (без релея)
`StartDirectTunnel(wgText, mtu, tunFD)` поднимает тот же WireGuard/AmneziaWG, но пакеты идут прямо на сервер, минуя TURN. Конфиг - текст wg-quick, а не JSON: полей релея тут нет.
```kotlin
pfd = builder.establish()!!
Mobile.startDirectTunnel(wgConf, 1376L, pfd.dup().detachFd().toLong())
```
Отличия от `StartTunnel`:
* каждый `[Peer]` обязан нести `Endpoint` в виде `ip:port` - bind устройства не резолвит имена (если в конфиге указан домен, хост должен отрезолвить его до вызова);
* `SetProtect` обязателен: сокеты открывает bind устройства, без защиты трафик заворачивается обратно в туннель;
* состояние и метрики отслеживаются через `GetState()` (polling); push-события `OnState` в `EventSink` генерируются только для релейных сессий;
* `GetState()` держит `connected` с первого handshake и уходит в `connecting`, только если handshake старше 180 с при идущем tx; `Streams`/`Total` всегда 1;
* `Wake()` в прямом режиме - no-op; при смене сети или выходе устройства из сна вызывается `Reconnect()`;
* `Reconnect()` пересоздаёт и заново защищает сокеты - этого хватает при смене сети, перезапуск не нужен;
* `Restart` работает только с JSON-конфигурацией: для смены параметров прямого туннеля нужен `Stop` и новый `StartDirectTunnel`;
* `mtu` идёт только в валидацию и логи: tun создаёт платформа, MTU для неё даёт `ParseTunnelConfig`.
### Обработка событий
Хост-приложение должно реализовать интерфейс `EventSink`:
```go
type EventSink interface {
OnState(state string, streams, total int, errMsg string)
OnLog(level, msg string, unixMillis int64)
OnCaptcha(url string)
}
```
`OnState` вызывается строго последовательно из одной горутины при изменении статуса. `OnLog` может вызываться параллельно.
Хост обязан удерживать ссылку на объект `EventSink` в памяти (JNI-ссылка со стороны Go не защищает его от GC).