mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
feat: Client ID шлётся всегда, симметричный wire авторизации
Клиент всегда отправляет Client ID первой DTLS app-record, сервер всегда читает. Флаг -auth и URI-параметр auth удалены — рассинхрон client/server невозможен. -clients-file на сервере включает только проверку ID по allowlist clients.json. Также: - clients.json и client_config.json пишутся с правами 0o600 - исправлены все находки golangci-lint (errcheck, gosec, govet shadow) Примечание: удалён флаг -auth и URI-параметр auth; Client ID теперь всегда в первой записи туннеля — старые клиенты/серверы несовместимы.
This commit is contained in:
+8
-13
@@ -51,9 +51,9 @@ func main() {
|
||||
defer cancel()
|
||||
|
||||
if cfg.SubURL != "" {
|
||||
s, err := sub.Fetch(ctx, cfg.SubURL)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to fetch subscription: %v", err)
|
||||
s, ferr := sub.Fetch(ctx, cfg.SubURL)
|
||||
if ferr != nil {
|
||||
log.Fatalf("failed to fetch subscription: %v", ferr)
|
||||
}
|
||||
if len(s.Nodes) == 0 {
|
||||
log.Fatalf("no nodes found in subscription")
|
||||
@@ -71,17 +71,14 @@ func main() {
|
||||
if ucfg.Mode != "" {
|
||||
cfg.Proxy.Mode = config.ClientProxyMode(ucfg.Mode, ucfg.Bond)
|
||||
}
|
||||
if ucfg.Auth {
|
||||
cfg.Auth = true
|
||||
}
|
||||
if ucfg.ObfProfile != "" {
|
||||
cfg.Obf.Profile = config.ObfProfile(ucfg.ObfProfile)
|
||||
}
|
||||
if ucfg.ObfKey != "" {
|
||||
if k, err := hex.DecodeString(ucfg.ObfKey); err == nil {
|
||||
if k, derr := hex.DecodeString(ucfg.ObfKey); derr == nil {
|
||||
cfg.Obf.Key = k
|
||||
} else {
|
||||
log.Fatalf("invalid hex in obf-key: %v", err)
|
||||
log.Fatalf("invalid hex in obf-key: %v", derr)
|
||||
}
|
||||
}
|
||||
if ucfg.Peer != "" {
|
||||
@@ -171,7 +168,6 @@ func main() {
|
||||
KCPProfile: cfg.KCP.Profile,
|
||||
KCPFEC: cfg.KCP.FEC,
|
||||
ClientID: cfg.ClientID,
|
||||
Auth: cfg.Auth,
|
||||
}
|
||||
if err := tcpfwd.Run(ctx, tcpDeps, tcpParams, peer, cfg.Proxy.Listen, cfg.TURN.N, cfg.Proxy.Mode == config.ProxyModeTCPFwdBond); err != nil {
|
||||
logger.Errorf("tcpfwd: %v", err)
|
||||
@@ -191,7 +187,6 @@ func main() {
|
||||
ObfKey: cfg.Obf.Key,
|
||||
GetCreds: udprelay.GetCredsFunc(getCreds),
|
||||
ClientID: cfg.ClientID,
|
||||
Auth: cfg.Auth,
|
||||
}
|
||||
if err := udprelay.Run(ctx, udpDtlsDialer, prov, logger, &connectedStreams, udpParams, peer, cfg.Proxy.Listen, cfg.TURN.N); err != nil {
|
||||
if errors.Is(err, udprelay.ErrFatal) {
|
||||
@@ -232,7 +227,7 @@ func resolveClientID(cliID string) string {
|
||||
}
|
||||
|
||||
path := filepath.Join(filepath.Dir(os.Args[0]), "client_config.json")
|
||||
b, err := os.ReadFile(path)
|
||||
b, err := os.ReadFile(path) //nolint:gosec // фиксированное имя рядом с бинарём, не пользовательский ввод
|
||||
if err == nil {
|
||||
var lc localCfg
|
||||
if err := json.Unmarshal(b, &lc); err == nil && lc.ClientID != "" {
|
||||
@@ -249,8 +244,8 @@ func resolveClientID(cliID string) string {
|
||||
|
||||
lc := localCfg{ClientID: newID}
|
||||
b, _ = json.MarshalIndent(lc, "", " ")
|
||||
if err := os.WriteFile(path, b, 0644); err != nil {
|
||||
log.Printf("warning: failed to save client ID to %s: %v", path, err)
|
||||
if err := os.WriteFile(path, b, 0o600); err != nil { //nolint:gosec // path фиксирован рядом с бинарём; 0o600 для auth-токена
|
||||
log.Printf("warning: failed to save client ID to %s: %v", path, err) //nolint:gosec // path не пользовательский ввод
|
||||
}
|
||||
|
||||
return newID
|
||||
|
||||
+9
-7
@@ -179,19 +179,21 @@ func handleAccepted(ctx context.Context, logger logx.Logger, registry *bondserve
|
||||
}
|
||||
logger.Debugf("Handshake done")
|
||||
|
||||
// Client ID читается всегда (клиент всегда шлёт его первой app-record) —
|
||||
// wire-контракт симметричен. -clients-file включает только enforce по allowlist.
|
||||
clientID, err := clientsdb.ReadClientID(dtlsConn)
|
||||
if err != nil {
|
||||
logger.Warnf("Read Client ID failed: %v", err)
|
||||
return
|
||||
}
|
||||
if db != nil {
|
||||
clientID, err := clientsdb.ReadClientID(dtlsConn)
|
||||
if err != nil {
|
||||
logger.Warnf("Read Client ID failed: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Debugf("Client ID received: %s", clientID)
|
||||
|
||||
if !db.IsAuthorized(clientID) {
|
||||
logger.Warnf("Unauthorized Client ID: %s. Dropping connection.", clientID)
|
||||
return
|
||||
}
|
||||
logger.Debugf("Client %s authorized", clientID)
|
||||
} else {
|
||||
logger.Debugf("Client ID received (no allowlist): %s", clientID)
|
||||
}
|
||||
|
||||
if cfg.Proxy.Mode == config.ProxyModeTCPFwd {
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ sudo netfilter-persistent save # если установлен
|
||||
- **В Docker:** раскомментируйте переменные `CLIENTS_FILE` и `volumes` в `docker-compose.yml`, затем выполните `docker compose up -d`.
|
||||
- **В systemd:** добавьте флаг `-clients-file /opt/free-turn-proxy/clients.json` в `ExecStart`, затем `systemctl daemon-reload && systemctl restart free-turn-proxy`.
|
||||
3. Управляйте клиентами (см. раздел [Управление Client ID в flags.md](flags.md#управление-client-id-команды-сервера)).
|
||||
4. **Не забудьте** передать флаг `-auth` и `-client-id <id>` при запуске клиента.
|
||||
4. На клиенте задайте `-client-id <id>` (либо используйте автогенерируемый) и добавьте этот ID в `clients.json` на сервере. Клиент отправляет ID автоматически.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-3
@@ -23,7 +23,6 @@
|
||||
| `-dns-servers` | пусто | свои UDP/53 резолверы, `ip[:port][,ip[:port]...]` |
|
||||
| `-client-id` | авто | уникальный ID клиента (автогенерация если не задан) |
|
||||
| `-sub` | пусто | URL подписки (sub.md) для получения списка серверов |
|
||||
| `-auth` | `false` | отправлять Client ID для авторизации на сервере |
|
||||
| `-debug` | `false` | debug-логи |
|
||||
|
||||
## Сервер
|
||||
@@ -41,8 +40,8 @@
|
||||
|
||||
## Управление Client ID (Команды Сервера)
|
||||
|
||||
> [!WARNING]
|
||||
> **Важно про Авторизацию:** Флаги `-auth` (на клиенте) и `-clients-file` (на сервере) должны быть включены либо **на обоих концах одновременно**, либо ни на одном. Рассинхронизация приведёт к обрыву соединений без внятных ошибок, так как ломается порядок байтов в туннеле.
|
||||
> [!NOTE]
|
||||
> **Про авторизацию:** клиент **всегда** отправляет свой Client ID первой записью после DTLS-handshake, сервер **всегда** его читает — wire-контракт симметричен. Флаг `-clients-file` на сервере включает **проверку** ID по allowlist (`clients.json`). Без `-clients-file` ID читается и игнорируется.
|
||||
|
||||
Сервер содержит встроенные команды для управления файлом `clients.json` (горячая перезагрузка поддерживается автоматически, перезапускать сервер после изменений не нужно).
|
||||
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@
|
||||
|
||||
Клиент скачает подписку, найдет первый доступный сервер и автоматически применит его конфигурацию. Уникальная ссылка `-link` по-прежнему требуется.
|
||||
|
||||
> [!WARNING]
|
||||
> **Важно про Авторизацию:** Если вы раздаёте ссылки с включенным `auth=1` (или запускаете клиента с флагом `-auth`), убедитесь, что сервер **обязательно** запущен с флагом `-clients-file`. Иначе клиент отправит свой ID первым пакетом, а сервер воспримет это как некорректные данные туннеля и разорвёт соединение. И наоборот: если сервер ждёт Client ID, а клиент его не шлёт (нет `auth=1`), сервер тоже отбросит соединение.
|
||||
> [!NOTE]
|
||||
> **Про авторизацию:** клиент всегда шлёт Client ID, сервер всегда его читает. Чтобы ограничить доступ, запустите сервер с `-clients-file` и добавьте нужные ID в `clients.json`; без этого флага ID просто игнорируется.
|
||||
|
||||
## Формат файла
|
||||
|
||||
|
||||
+2
-3
@@ -9,7 +9,7 @@
|
||||
## Формат
|
||||
|
||||
```text
|
||||
freeturn://<Provider>?<Transport><mode=...&obf-profile=...&bond=1&auth=1>@<Peer>#<ObfKey>$<Comment>
|
||||
freeturn://<Provider>?<Transport><mode=...&obf-profile=...&bond=1>@<Peer>#<ObfKey>$<Comment>
|
||||
```
|
||||
|
||||
### Поля
|
||||
@@ -21,7 +21,6 @@ freeturn://<Provider>?<Transport><mode=...&obf-profile=...&bond=1&auth=1>@<Peer>
|
||||
| `<mode=...>` | Параметры туннеля (`mode=udp` или `mode=tcp`). |
|
||||
| `<obf-profile=...>` | Профиль обфускации (например, `rtpopus`). |
|
||||
| `<bond=1>` | Включить ли bonding для TCP (`1` или `true`). |
|
||||
| `<auth=1>` | Включить отправку Client ID для авторизации (`1` или `true`). |
|
||||
| `<Peer>` | Адрес сервера на VPS (`ip:port`). |
|
||||
| `<ObfKey>` | Ключ обфускации (hex, 64 символа). |
|
||||
| `<Comment>` | Свободный комментарий (например, название сервера). |
|
||||
@@ -29,7 +28,7 @@ freeturn://<Provider>?<Transport><mode=...&obf-profile=...&bond=1&auth=1>@<Peer>
|
||||
## Пример использования
|
||||
|
||||
```bash
|
||||
./client "freeturn://vk?tcp<mode=udp&obf-profile=rtpopus&auth=1>@1.2.3.4:56000#d823fa...$MyServer" -link "https://vk.com/call/join/..."
|
||||
./client "freeturn://vk?tcp<mode=udp&obf-profile=rtpopus>@1.2.3.4:56000#d823fa...$MyServer" -link "https://vk.com/call/join/..."
|
||||
```
|
||||
|
||||
Параметры из URI автоматически переопределят базовые флаги (такие как `-peer`, `-mode`, `-obf-key`).
|
||||
|
||||
@@ -143,7 +143,7 @@ func (db *DB) save() error {
|
||||
}
|
||||
|
||||
tmpFile := db.path + ".tmp"
|
||||
err = os.WriteFile(tmpFile, b, 0644)
|
||||
err = os.WriteFile(tmpFile, b, 0o600) // содержит Client ID (auth-токены) — не world-readable
|
||||
if err == nil {
|
||||
err = os.Rename(tmpFile, db.path)
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func WriteClientID(conn net.Conn, clientID string) error {
|
||||
b = b[:255] // Усекаем до 255 байт
|
||||
}
|
||||
buf := make([]byte, 1+len(b))
|
||||
buf[0] = byte(len(b))
|
||||
buf[0] = byte(len(b)) //nolint:gosec // len(b) усечён до ≤255 выше
|
||||
copy(buf[1:], b)
|
||||
_, err := conn.Write(buf)
|
||||
return err
|
||||
@@ -171,8 +171,8 @@ func WriteClientID(conn net.Conn, clientID string) error {
|
||||
|
||||
// ReadClientID читает Client ID из соединения. Учитывает DTLS record-orientated поведение.
|
||||
func ReadClientID(conn net.Conn) (string, error) {
|
||||
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
defer conn.SetReadDeadline(time.Time{})
|
||||
_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
defer func() { _ = conn.SetReadDeadline(time.Time{}) }()
|
||||
|
||||
buf := make([]byte, 256)
|
||||
n, err := conn.Read(buf)
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestClientsDB(t *testing.T) {
|
||||
t.Fatalf("Failed to create db: %v", err)
|
||||
}
|
||||
|
||||
if err := db.Add("client-123", "Test 1"); err != nil {
|
||||
if err = db.Add("client-123", "Test 1"); err != nil {
|
||||
t.Fatalf("Failed to add client: %v", err)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestClientsDB(t *testing.T) {
|
||||
t.Errorf("Expected client-456 not to be authorized")
|
||||
}
|
||||
|
||||
if err := db.Remove("client-123"); err != nil {
|
||||
if err = db.Remove("client-123"); err != nil {
|
||||
t.Fatalf("Failed to remove client: %v", err)
|
||||
}
|
||||
|
||||
@@ -67,20 +67,24 @@ func TestClientIDRoundTrip(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ListenUDP: %v", err)
|
||||
}
|
||||
defer serverConn.Close()
|
||||
defer func() { _ = serverConn.Close() }()
|
||||
|
||||
clientConn, err := net.DialUDP("udp", nil, serverConn.LocalAddr().(*net.UDPAddr))
|
||||
udpAddr, ok := serverConn.LocalAddr().(*net.UDPAddr)
|
||||
if !ok {
|
||||
t.Fatalf("LocalAddr is not *net.UDPAddr")
|
||||
}
|
||||
clientConn, err := net.DialUDP("udp", nil, udpAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("DialUDP: %v", err)
|
||||
}
|
||||
defer clientConn.Close()
|
||||
defer func() { _ = clientConn.Close() }()
|
||||
|
||||
expectedID := "client-test-uuid-123"
|
||||
|
||||
// Client writes
|
||||
go func() {
|
||||
if err := WriteClientID(clientConn, expectedID); err != nil {
|
||||
t.Errorf("WriteClientID failed: %v", err)
|
||||
if werr := WriteClientID(clientConn, expectedID); werr != nil {
|
||||
t.Errorf("WriteClientID failed: %v", werr)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -120,7 +120,6 @@ type Client struct {
|
||||
KCP KCPOpts
|
||||
ClientID string
|
||||
SubURL string
|
||||
Auth bool
|
||||
}
|
||||
|
||||
// Server — разобранные и провалидированные CLI-опции сервера.
|
||||
@@ -160,7 +159,6 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) {
|
||||
dnsServers := fs.String("dns-servers", "", "свои UDP/53 DNS через запятую: ip[:port][,ip[:port]...]")
|
||||
clientID := fs.String("client-id", "", "уникальный ID клиента (автогенерация если не задан)")
|
||||
subURL := fs.String("sub", "", "URL подписки (sub.md) для получения списка серверов")
|
||||
auth := fs.Bool("auth", false, "отправлять Client ID для авторизации на сервере")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, err
|
||||
@@ -201,7 +199,6 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) {
|
||||
},
|
||||
ClientID: *clientID,
|
||||
SubURL: *subURL,
|
||||
Auth: *auth,
|
||||
}
|
||||
|
||||
// Обработка позиционного аргумента URI
|
||||
@@ -233,9 +230,6 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) {
|
||||
if ucfg.Peer != "" {
|
||||
c.Proxy.Peer = ucfg.Peer
|
||||
}
|
||||
if ucfg.Auth {
|
||||
c.Auth = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ type Params struct {
|
||||
KCPProfile kcptun.Profile
|
||||
KCPFEC kcptun.FEC
|
||||
ClientID string
|
||||
Auth bool
|
||||
}
|
||||
|
||||
// BondHandler распределяет одно принятое TCP-соединение по всем активным сессиям пула.
|
||||
@@ -258,11 +257,11 @@ func createSmuxSession(ctx context.Context, deps *Deps, params *Params, peer *ne
|
||||
cleanupFns = append(cleanupFns, func() { _ = dtlsConn.Close() })
|
||||
deps.log().Debugf("DTLS connection established")
|
||||
|
||||
if params.Auth {
|
||||
if err := clientsdb.WriteClientID(dtlsConn, params.ClientID); err != nil {
|
||||
cleanup()
|
||||
return nil, nil, fmt.Errorf("send client ID: %w", err)
|
||||
}
|
||||
// Client ID шлётся всегда первой DTLS app-record; сервер всегда читает.
|
||||
// -clients-file на сервере решает только, проверять ли ID по allowlist.
|
||||
if werr := clientsdb.WriteClientID(dtlsConn, params.ClientID); werr != nil {
|
||||
cleanup()
|
||||
return nil, nil, fmt.Errorf("send client ID: %w", werr)
|
||||
}
|
||||
|
||||
statsCtx, statsCancel := context.WithCancel(ctx) //nolint:gosec // cancel is held in cleanupFns and invoked via cleanup() LIFO
|
||||
|
||||
@@ -152,10 +152,10 @@ func oneDTLS(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr,
|
||||
}()
|
||||
deps.log().Infof("[STREAM %d] Established DTLS connection", streamID)
|
||||
|
||||
if params.Auth {
|
||||
if err := clientsdb.WriteClientID(dtlsConn, params.ClientID); err != nil {
|
||||
return fmt.Errorf("failed to write client ID: %w", err)
|
||||
}
|
||||
// Client ID шлётся всегда (первой DTLS app-record); сервер всегда читает.
|
||||
// -clients-file на сервере решает только, проверять ли ID по allowlist.
|
||||
if err := clientsdb.WriteClientID(dtlsConn, params.ClientID); err != nil {
|
||||
return fmt.Errorf("failed to write client ID: %w", err)
|
||||
}
|
||||
|
||||
if okchan != nil {
|
||||
|
||||
@@ -40,7 +40,6 @@ type Params struct {
|
||||
ObfKey []byte
|
||||
GetCreds GetCredsFunc
|
||||
ClientID string
|
||||
Auth bool
|
||||
}
|
||||
|
||||
// ErrFatal возвращается из Run, когда поток встречает условие, требующее
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ func Fetch(ctx context.Context, url string) (*Sub, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
|
||||
@@ -12,7 +12,6 @@ type Config struct {
|
||||
Transport string
|
||||
Mode string
|
||||
Bond bool
|
||||
Auth bool
|
||||
ObfProfile string
|
||||
ObfKey string
|
||||
Peer string
|
||||
@@ -71,7 +70,6 @@ func Parse(s string) (*Config, error) {
|
||||
cfg.Mode = vals.Get("mode")
|
||||
cfg.ObfProfile = vals.Get("obf-profile")
|
||||
cfg.Bond = vals.Get("bond") == "true" || vals.Get("bond") == "1"
|
||||
cfg.Auth = vals.Get("auth") == "true" || vals.Get("auth") == "1"
|
||||
} else {
|
||||
cfg.Transport = transportPart
|
||||
}
|
||||
@@ -95,9 +93,6 @@ func (c *Config) String() string {
|
||||
if c.Bond {
|
||||
payload = append(payload, "bond=1")
|
||||
}
|
||||
if c.Auth {
|
||||
payload = append(payload, "auth=1")
|
||||
}
|
||||
|
||||
transportStr := c.Transport
|
||||
if len(payload) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user