From f1b047e9e558df762e240db223b9daa7767400d7 Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Sun, 24 May 2026 23:29:42 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Client=20ID=20=D1=88=D0=BB=D1=91=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=B2=D1=81=D0=B5=D0=B3=D0=B4=D0=B0,=20=D1=81?= =?UTF-8?q?=D0=B8=D0=BC=D0=BC=D0=B5=D1=82=D1=80=D0=B8=D1=87=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20wire=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Клиент всегда отправляет 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 теперь всегда в первой записи туннеля — старые клиенты/серверы несовместимы. --- cmd/client/main.go | 21 ++++++++------------- cmd/server/main.go | 16 +++++++++------- docs/deploy.md | 2 +- docs/flags.md | 5 ++--- docs/sub.md | 4 ++-- docs/uri.md | 5 ++--- internal/clientsdb/db.go | 8 ++++---- internal/clientsdb/db_test.go | 18 +++++++++++------- internal/config/config.go | 6 ------ internal/proxy/tcpfwd/tcpfwd.go | 11 +++++------ internal/proxy/udprelay/loop.go | 8 ++++---- internal/proxy/udprelay/run.go | 1 - internal/sub/sub.go | 2 +- internal/uri/uri.go | 5 ----- 14 files changed, 49 insertions(+), 63 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index 12bdc2b..8b496d5 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -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 diff --git a/cmd/server/main.go b/cmd/server/main.go index 6414a74..ff130a8 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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 { diff --git a/docs/deploy.md b/docs/deploy.md index 153f2f0..bbe3020 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -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 ` при запуске клиента. +4. На клиенте задайте `-client-id ` (либо используйте автогенерируемый) и добавьте этот ID в `clients.json` на сервере. Клиент отправляет ID автоматически. --- diff --git a/docs/flags.md b/docs/flags.md index cb59054..d816dae 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -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` (горячая перезагрузка поддерживается автоматически, перезапускать сервер после изменений не нужно). diff --git a/docs/sub.md b/docs/sub.md index e6de550..d02d312 100644 --- a/docs/sub.md +++ b/docs/sub.md @@ -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 просто игнорируется. ## Формат файла diff --git a/docs/uri.md b/docs/uri.md index 98af20a..bec5cba 100644 --- a/docs/uri.md +++ b/docs/uri.md @@ -9,7 +9,7 @@ ## Формат ```text -freeturn://?@#$ +freeturn://?@#$ ``` ### Поля @@ -21,7 +21,6 @@ freeturn://?@ | `` | Параметры туннеля (`mode=udp` или `mode=tcp`). | | `` | Профиль обфускации (например, `rtpopus`). | | `` | Включить ли bonding для TCP (`1` или `true`). | -| `` | Включить отправку Client ID для авторизации (`1` или `true`). | | `` | Адрес сервера на VPS (`ip:port`). | | `` | Ключ обфускации (hex, 64 символа). | | `` | Свободный комментарий (например, название сервера). | @@ -29,7 +28,7 @@ freeturn://?@ ## Пример использования ```bash -./client "freeturn://vk?tcp@1.2.3.4:56000#d823fa...$MyServer" -link "https://vk.com/call/join/..." +./client "freeturn://vk?tcp@1.2.3.4:56000#d823fa...$MyServer" -link "https://vk.com/call/join/..." ``` Параметры из URI автоматически переопределят базовые флаги (такие как `-peer`, `-mode`, `-obf-key`). diff --git a/internal/clientsdb/db.go b/internal/clientsdb/db.go index f9a8754..ba07fa4 100644 --- a/internal/clientsdb/db.go +++ b/internal/clientsdb/db.go @@ -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) diff --git a/internal/clientsdb/db_test.go b/internal/clientsdb/db_test.go index fe02da8..2aee6bf 100644 --- a/internal/clientsdb/db_test.go +++ b/internal/clientsdb/db_test.go @@ -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) } }() diff --git a/internal/config/config.go b/internal/config/config.go index 9dee8a2..c966afa 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 - } } } diff --git a/internal/proxy/tcpfwd/tcpfwd.go b/internal/proxy/tcpfwd/tcpfwd.go index bc35752..c91f97a 100644 --- a/internal/proxy/tcpfwd/tcpfwd.go +++ b/internal/proxy/tcpfwd/tcpfwd.go @@ -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 diff --git a/internal/proxy/udprelay/loop.go b/internal/proxy/udprelay/loop.go index 01e9065..2dda333 100644 --- a/internal/proxy/udprelay/loop.go +++ b/internal/proxy/udprelay/loop.go @@ -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 { diff --git a/internal/proxy/udprelay/run.go b/internal/proxy/udprelay/run.go index d88dbfa..eb2e0bc 100644 --- a/internal/proxy/udprelay/run.go +++ b/internal/proxy/udprelay/run.go @@ -40,7 +40,6 @@ type Params struct { ObfKey []byte GetCreds GetCredsFunc ClientID string - Auth bool } // ErrFatal возвращается из Run, когда поток встречает условие, требующее diff --git a/internal/sub/sub.go b/internal/sub/sub.go index 782c3dd..5714709 100644 --- a/internal/sub/sub.go +++ b/internal/sub/sub.go @@ -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) diff --git a/internal/uri/uri.go b/internal/uri/uri.go index ece7c5c..220bfac 100644 --- a/internal/uri/uri.go +++ b/internal/uri/uri.go @@ -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 {