From 3bdc8e815af871feeea53946964917eaa3a6c0ce Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Thu, 4 Jun 2026 12:31:48 +0300 Subject: [PATCH] =?UTF-8?q?fix(client):=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8C=20client=5Fid=20=D0=B2=20=D0=B4=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=83=D0=BF=D0=BD=D1=8B=D0=B9=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20=D0=BA=D0=B0=D1=82?= =?UTF-8?q?=D0=B0=D0=BB=D0=BE=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit На Android каталог рядом с бинарём read-only, ID не сохранялся и ротировался на каждый запуск, ломая allowlist. Пробуем каталог бинаря, UserConfigDir и TempDir по порядку. --- cmd/client/main.go | 58 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index 8b496d5..15dc8f9 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -226,9 +226,14 @@ func resolveClientID(cliID string) string { ClientID string `json:"client_id"` } - path := filepath.Join(filepath.Dir(os.Args[0]), "client_config.json") - b, err := os.ReadFile(path) //nolint:gosec // фиксированное имя рядом с бинарём, не пользовательский ввод - if err == nil { + paths := clientConfigPaths() + + // Чтение: первый файл с непустым client_id. + for _, path := range paths { + b, err := os.ReadFile(path) //nolint:gosec // фиксированное имя из clientConfigPaths, не пользовательский ввод + if err != nil { + continue + } var lc localCfg if err := json.Unmarshal(b, &lc); err == nil && lc.ClientID != "" { return lc.ClientID @@ -243,10 +248,51 @@ func resolveClientID(cliID string) string { newID := hex.EncodeToString(idBytes) lc := localCfg{ClientID: newID} - b, _ = json.MarshalIndent(lc, "", " ") - 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 не пользовательский ввод + b, _ := json.MarshalIndent(lc, "", " ") + + // Запись: первый доступный для записи путь. На Android каталог рядом с + // бинарём (/data/app/.../lib/arm64) read-only — падаем на UserConfigDir, + // затем TempDir. Иначе ID не сохраняется и ротируется на каждый запуск, + // ломая allowlist (-clients-file) и статистику. + for _, path := range paths { + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + continue + } + if err := os.WriteFile(path, b, 0o600); err == nil { //nolint:gosec // 0o600 для auth-токена + return newID + } } + log.Printf("warning: failed to persist client ID to any writable path (%v); ID will rotate next launch", paths) return newID } + +// clientConfigPaths возвращает кандидатов client_config.json в порядке +// предпочтения: рядом с бинарём (desktop, переносимость), затем per-user +// UserConfigDir и TempDir (Android, где каталог бинаря read-only). +func clientConfigPaths() []string { + const name = "client_config.json" + seen := map[string]bool{} + var dirs []string + add := func(d string) { + if d == "" || seen[d] { + return + } + seen[d] = true + dirs = append(dirs, d) + } + if exe, err := os.Executable(); err == nil { + add(filepath.Dir(exe)) + } + add(filepath.Dir(os.Args[0])) + if cfgDir, err := os.UserConfigDir(); err == nil { + add(filepath.Join(cfgDir, "free-turn-proxy")) + } + add(os.TempDir()) + + paths := make([]string, 0, len(dirs)) + for _, d := range dirs { + paths = append(paths, filepath.Join(d, name)) + } + return paths +}