Интеграция free-turn-proxy через gomobile
Пакет mobile предоставляет API для встраивания ядра прокси в нативные мобильные приложения (iOS/Android) через gomobile bind.
Сборка
iOS
Сборка универсального XCFramework (dist/Mobile.xcframework) с поддержкой iOS и симулятора:
task build:ios
Android
Сборка Android Archive (dist/freeturn.aar и freeturn-sources.jar) с JNI-обертками для Java/Kotlin:
task build:android
API
Хост-приложение передает конфигурацию в формате JSON, а события получает через интерфейс обратного вызова.
func Start(configJSON string) error // Запуск в режиме прокси
func StartTunnel(configJSON string, 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 Version() string
В Java класс экспортируется как com.freeturn.core.mobile.Mobile. int в Go транслируется в long на стороне Kotlin/Java.
Конфигурация JSON
Пример схемы JSON:
{
"peer": "1.2.3.4:56000",
"clientId": "0123456789abcdef0123456789abcdef",
"provider": "vk",
"turn": {"n": 12, "transport": "tcp", "host": "", "port": ""},
"proxy": {"listen": "127.0.0.1:9000"},
"vk": {"links": ["https://vk.ru/call/join/..."], "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 должен храниться в приложении.
Туннелирование
tunnel.mode принимает значения none, wg (WireGuard) или awg (AmneziaWG).
Текст WireGuard-конфигурации (wg-quick) передается в tunnel.config. В режиме wg параметры обфускации AmneziaWG игнорируются. Поле Endpoint из конфига не используется, так как трафик идет через TURN-релей.
Адреса, DNS, маршруты и MTU tun-интерфейса ядро не применяет - это работа платформы. Их отдаёт ParseTunnelConfig из того же текста, что уходит в tunnel.config (аргумент mtu - то же значение, что в tunnel.mtu; 0 - взять из конфига):
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:
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 подлежат только сокеты релея.
Обработка событий
Хост-приложение должно реализовать интерфейс EventSink:
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).