Files
T
samosvalishe a7798cd851 feat(proxy): встроенное управление маршрутами к TURN-серверам
- Добавлен пакет `internal/routemgr` для автоматического создания
  host-маршрутов (/32) через шлюз ОС (Windows, Linux, macOS).
- В `config.Client` и CLI добавлен флаг `-routes` (по умолчанию отключен).
- Интегрирован callback `OnTURNServer` в `udprelay` и `tcpfwd`
  для динамического добавления маршрутов при старте стримов.
- Для Android (и переименованных JNI-бинарей Linux) добавлено
  graceful-отключение менеджера маршрутов.
- Обновлена документация (`quickstart.md`): добавлены примеры с
  использованием `-routes` и `-client-id`.
2026-08-01 15:07:37 +03:00

213 lines
6.2 KiB
Go
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.
package config
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/samosvalishe/free-turn-proxy/internal/uri"
)
// ClientJSON - схема конфига для хостов, которые не собирают командную строку
// (мобильное приложение через gomobile). Группы зеркалят Client, поэтому
// добавление опции не требует изобретать, где ей место.
//
// Декодирование строгое: неизвестное поле - ошибка. AAR и приложение собираются
// вместе, поэтому опечатка в хосте должна падать сразу, а не молча теряться.
// Отсутствующее поле сохраняет значение из Defaults(): DTO перед разбором
// заполняется дефолтами, а json.Unmarshal не трогает то, чего нет во входе.
type ClientJSON struct {
Peer string `json:"peer"`
ClientID string `json:"clientId"`
SubURL string `json:"subUrl"`
Provider string `json:"provider"`
Routes bool `json:"routes"`
TURN turnJSON `json:"turn"`
Proxy proxyJSON `json:"proxy"`
VK vkJSON `json:"vk"`
Obf obfJSON `json:"obf"`
DNS dnsJSON `json:"dns"`
Log logJSON `json:"log"`
Tunnel tunnelJSON `json:"tunnel"`
}
// tunnelJSON - userspace-туннель внутри процесса. config передаётся текстом в
// формате wg-quick: параметры AmneziaWG (Jc, S1, H1...) хост берёт из него же,
// дублировать их отдельными полями незачем.
type tunnelJSON struct {
Mode string `json:"mode"`
Config string `json:"config"`
MTU int `json:"mtu"`
}
type turnJSON struct {
N int `json:"n"`
Transport string `json:"transport"`
Host string `json:"host"`
Port string `json:"port"`
}
type proxyJSON struct {
Mode string `json:"mode"`
Bond bool `json:"bond"`
Listen string `json:"listen"`
}
type vkJSON struct {
Links []string `json:"links"`
StreamsPerCred int `json:"streamsPerCred"`
ManualCaptcha bool `json:"manualCaptcha"`
Platform string `json:"platform"`
}
type obfJSON struct {
Profile string `json:"profile"`
Key string `json:"key"`
TimingMs int `json:"timingMs"`
}
type dnsJSON struct {
Mode string `json:"mode"`
Servers []string `json:"servers"`
}
type logJSON struct {
Debug bool `json:"debug"`
}
// DefaultClientJSON - дефолты в виде JSON. Хост строит из них стартовое
// состояние формы и не повторяет литералы у себя.
func DefaultClientJSON() string {
b, err := json.MarshalIndent(defaultClientJSON(), "", " ")
if err != nil {
// Схема состоит из строк, чисел и срезов строк - маршалиться обязана.
panic("config: default JSON must marshal: " + err.Error())
}
return string(b)
}
// PeekSubURLJSON достаёт subUrl из конфига, не разбирая остального. Нужен до
// ParseClientJSON: подписка отдаёт peer, без которого валидация падает.
// Некорректный JSON - не ошибка здесь, о ней сообщит полноценный разбор.
func PeekSubURLJSON(data []byte) string {
var peek struct {
SubURL string `json:"subUrl"`
}
if json.Unmarshal(data, &peek) != nil {
return ""
}
return peek.SubURL
}
// ParseClientJSON разбирает конфиг хоста тем же путём, что и CLI: DTO -> raw ->
// assemble -> Validate.
//
// overlayURI (если не пуст) накладывается поверх, как позиционный freeturn:// у
// CLI: так узел из подписки применяется тем же кодом, что и вручную вставленная
// ссылка.
func ParseClientJSON(data []byte, overlayURI string) (*Client, error) {
dto := defaultClientJSON()
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&dto); err != nil {
return nil, fmt.Errorf("config json: %w", err)
}
r := dto.toRaw()
if overlayURI != "" {
u, err := uri.Parse(overlayURI)
if err != nil {
return nil, fmt.Errorf("failed to parse freeturn:// URI: %w", err)
}
r.applyURI(u)
}
c, err := assemble(r)
if err != nil {
return nil, err
}
if err := Validate(c); err != nil {
return nil, err
}
return c, nil
}
func defaultClientJSON() ClientJSON {
r := defaultRaw()
return ClientJSON{
Peer: r.Peer,
ClientID: r.ClientID,
SubURL: r.SubURL,
Provider: r.Provider,
Routes: r.Routes,
TURN: turnJSON{
N: r.N,
Transport: r.Transport,
Host: r.Turn,
Port: r.Port,
},
Proxy: proxyJSON{
Mode: r.Mode,
Bond: r.Bond,
Listen: r.Listen,
},
VK: vkJSON{
StreamsPerCred: r.StreamsPerCred,
ManualCaptcha: r.ManualCaptcha,
Platform: r.Platform,
},
Obf: obfJSON{
Profile: r.ObfProfile,
Key: r.ObfKey,
TimingMs: int(r.ObfTiming / time.Millisecond),
},
DNS: dnsJSON{Mode: r.DNSMode},
Log: logJSON{Debug: r.Debug},
Tunnel: tunnelJSON{
Mode: r.TunnelMode,
Config: r.TunnelConfig,
MTU: r.TunnelMTU,
},
}
}
func (j ClientJSON) toRaw() raw {
return raw{
Turn: j.TURN.Host,
Port: j.TURN.Port,
Listen: j.Proxy.Listen,
Peer: j.Peer,
Provider: j.Provider,
Links: strings.Join(j.VK.Links, ","),
N: j.TURN.N,
StreamsPerCred: j.VK.StreamsPerCred,
Transport: j.TURN.Transport,
Mode: j.Proxy.Mode,
Bond: j.Proxy.Bond,
ObfProfile: j.Obf.Profile,
ObfKey: j.Obf.Key,
ObfTiming: time.Duration(j.Obf.TimingMs) * time.Millisecond,
ManualCaptcha: j.VK.ManualCaptcha,
Platform: j.VK.Platform,
DNSMode: j.DNS.Mode,
DNSServers: strings.Join(j.DNS.Servers, ","),
Debug: j.Log.Debug,
ClientID: j.ClientID,
SubURL: j.SubURL,
Routes: j.Routes,
TunnelMode: j.Tunnel.Mode,
TunnelConfig: j.Tunnel.Config,
TunnelMTU: j.Tunnel.MTU,
}
}