refactor(provider): убрать static-провайдер, оставить абстракцию

Сейчас нужен только VK-провайдер. internal/provider/static и связанные флаги
-static-user/-static-pass/-static-addr удалены, валидация и тесты обновлены.
Интерфейс provider.Provider остаётся — слот для будущих реализаций сохранён в
нейтральных формулировках doc-комментариев.
This commit is contained in:
samosvalishe
2026-05-23 14:54:46 +03:00
parent 2b85d937ea
commit cf47f374b3
9 changed files with 10 additions and 222 deletions
-7
View File
@@ -16,7 +16,6 @@ import (
"github.com/samosvalishe/btp/internal/config"
"github.com/samosvalishe/btp/internal/logx"
"github.com/samosvalishe/btp/internal/provider"
"github.com/samosvalishe/btp/internal/provider/static"
"github.com/samosvalishe/btp/internal/provider/vk"
"github.com/samosvalishe/btp/internal/proxy/bondclient"
"github.com/samosvalishe/btp/internal/proxy/tcpfwd"
@@ -161,12 +160,6 @@ func buildProvider(cfg *config.Client, dialer net.Dialer, connected *atomic.Int3
Log: logger,
Debug: cfg.Log.Debug,
}, vk.DefaultManualSolver)
case config.ProviderStatic:
return static.New(static.Config{
User: cfg.Static.User,
Pass: cfg.Static.Pass,
Addr: cfg.Static.Addr,
})
default:
return nil, fmt.Errorf("unknown provider %q", cfg.Provider.Name)
}
+1 -4
View File
@@ -6,11 +6,8 @@
| --- | --- | --- |
| `-listen` | `127.0.0.1:9000` | локальный адрес `ip:port`, куда подключается WireGuard или Xray клиент |
| `-peer` | **обязательный** | адрес сервера на VPS, `host:port` |
| `-provider` | `vk` | источник TURN-creds: `vk` \| `static` (см. `docs/providers.md`) |
| `-provider` | `vk` | источник TURN-creds: `vk` (см. `docs/providers.md`) |
| `-link` | **обязательный для `-provider vk`** | ссылка VK Calls `https://vk.com/call/join/...` |
| `-static-user` | **обязательный для `-provider static`** | TURN username |
| `-static-pass` | **обязательный для `-provider static`** | TURN password |
| `-static-addr` | **обязательный для `-provider static`** | TURN server `host:port` |
| `-n` | `10` | параллельных TURN-потоков |
| `-transport` | `tcp` | транспорт до TURN-реле: `tcp` (TCP/TLS) \| `udp` |
| `-mode` | `udp` | режим туннеля: `udp` (UDP-релей для WireGuard) \| `tcp` (TCP-форвардер для Xray/sing-box) |
+1 -2
View File
@@ -30,9 +30,8 @@ client ←→ VK TURN-relay
Standard relay protocol (RFC 5766/8656). Источник реле выбирается флагом `-provider` (см. ниже). Мы используем реле как транзит к собственному серверу. Транспорт до TURN выбирается флагом `-transport` (UDP/TCP).
### Provider
Источник TURN-реквизитов (user/pass/server-addr). Интерфейс `internal/provider.Provider`. Текущие реализации:
Источник TURN-реквизитов (user/pass/server-addr). Интерфейс `internal/provider.Provider`. Реализации:
- **`vk`** (default) — `internal/provider/vk`, обёртка над VK Calls API (требует `-link`).
- **`static`** — `internal/provider/static`, фиксированные `-static-user/-static-pass/-static-addr` (для coturn, metered, любого совместимого TURN).
Pipeline (proxy/udprelay, proxy/tcpfwd) работает только через интерфейс — без VK-specific импортов. См. `docs/providers.md`.
+2 -15
View File
@@ -15,20 +15,7 @@ VK Calls API. Перебирает встроенные `app_id/app_secret`, п
- `-streams-per-cred` (default 10) — сколько TURN-стримов делят один кеш креденшалов.
- `-manual-captcha` — пропустить auto-solver, сразу открыть браузер.
Пакет: `internal/provider/vk/`. Внутренние зависимости: `internal/client/{vkauth,captcha,captcha/manual,browserprofile,namegen,ish,dnsdial}`.
### `static`
Фиксированные creds для любого RFC 5766/8656-совместимого TURN: self-hosted coturn, metered.ca с long-term creds, корпоративный TURN.
**Обязательные флаги:**
- `-static-user` — TURN username.
- `-static-pass` — TURN password.
- `-static-addr` — TURN server host:port.
Без сетевого control-plane: никаких captcha, throttling, lockout. Если TURN вернёт 401, pipeline крутит retry с теми же creds — провайдер не умеет их ротировать.
Пакет: `internal/provider/static/`.
Пакет: `internal/provider/vk/`. VK-специфичные зависимости (`vkauth`, `captcha`, `captcha/manual`, `browserprofile`, `namegen`) живут под `internal/provider/vk/internal/*` и недоступны извне `provider/vk`. Общие: `internal/client/{ish,dnsdial}`.
## Как добавить новый провайдер
@@ -66,5 +53,5 @@ VK Calls API. Перебирает встроенные `app_id/app_secret`, п
## Известные ограничения текущей абстракции
1. **Per-process один провайдер** — нет fallback chain (VK → static). Можно добавить wrapper-провайдер позже.
1. **Per-process один провайдер** — нет fallback chain. Можно добавить wrapper-провайдер позже.
2. **`StreamsAlive` callback** — VK-провайдер использует для решения fatal-vs-throttle. Другие провайдеры могут игнорировать (поле `Config.StreamsAlive` nil-safe).
+4 -25
View File
@@ -67,22 +67,14 @@ type VKOpts struct {
ManualCaptcha bool // -manual-captcha
}
// StaticOpts — фиксированные TURN-реквизиты (только клиент, провайдер "static").
type StaticOpts struct {
User string // -static-user
Pass string // -static-pass
Addr string // -static-addr
}
// ProviderOpts выбирает реализацию provider.Provider.
type ProviderOpts struct {
Name string // -provider: vk (default) | static
Name string // -provider: vk (default)
}
// Известные имена провайдеров.
const (
ProviderVK = "vk"
ProviderStatic = "static"
ProviderVK = "vk"
)
// DNSOpts — опции DNS-резолвинга (только клиент).
@@ -109,7 +101,6 @@ type Client struct {
Proxy ProxyOpts
Provider ProviderOpts
VK VKOpts
Static StaticOpts
DNS DNSOpts
Log LogOpts
KCP KCPOpts
@@ -134,11 +125,8 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) {
host := fs.String("turn", "", "переопределить IP TURN-сервера (по умолчанию берётся из credentials провайдера)")
port := fs.String("port", "", "переопределить порт TURN-сервера (по умолчанию берётся из credentials провайдера)")
listen := fs.String("listen", "127.0.0.1:9000", "локальный адрес ip:port, куда подключается WireGuard или Xray клиент")
providerFlag := fs.String("provider", ProviderVK, "источник TURN-реквизитов: vk (default, VK Calls API) | static (фиксированные -static-* флаги)")
providerFlag := fs.String("provider", ProviderVK, "источник TURN-реквизитов: vk (default, VK Calls API)")
vklink := fs.String("link", "", "ссылка VK Calls вида https://vk.com/call/join/... (обязательно для -provider vk)")
staticUser := fs.String("static-user", "", "TURN username (обязательно для -provider static)")
staticPass := fs.String("static-pass", "", "TURN password (обязательно для -provider static)")
staticAddr := fs.String("static-addr", "", "TURN server host:port (обязательно для -provider static)")
peerAddr := fs.String("peer", "", "адрес сервера VK TURN Proxy на VPS, host:port (обязательно)")
n := fs.Int("n", 10, "количество параллельных TURN-потоков (соединений к TURN-реле)")
transportFlag := fs.String("transport", "tcp", "транспорт до TURN-реле: tcp (TCP/TLS, default) | udp")
@@ -180,11 +168,6 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) {
StreamsPerCred: *streamsPerCredFlag,
ManualCaptcha: *manualCaptchaFlag,
},
Static: StaticOpts{
User: *staticUser,
Pass: *staticPass,
Addr: *staticAddr,
},
DNS: DNSOpts{
Mode: *dnsFlag,
},
@@ -240,12 +223,8 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) {
link = link[:idx]
}
c.VK.Link = link
case ProviderStatic:
if c.Static.User == "" || c.Static.Pass == "" || c.Static.Addr == "" {
return nil, errors.New("-provider static requires -static-user, -static-pass, -static-addr")
}
default:
return nil, fmt.Errorf("invalid -provider value %q: must be %s | %s", c.Provider.Name, ProviderVK, ProviderStatic)
return nil, fmt.Errorf("invalid -provider value %q: must be %s", c.Provider.Name, ProviderVK)
}
key, err := srtpmimicry.DecodeKey(c.Obf.Mode, *wrapKeyHex)
if err != nil {
-28
View File
@@ -129,34 +129,6 @@ func TestParseClient_NClampedToTen(t *testing.T) {
}
}
func TestParseClient_ProviderStaticOK(t *testing.T) {
args := []string{
"-peer", "1.2.3.4:5000",
"-provider", "static",
"-static-user", "u",
"-static-pass", "p",
"-static-addr", "turn.example.com:3478",
}
c, err := ParseClient(args, io.Discard)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if c.Provider.Name != ProviderStatic {
t.Errorf("Provider.Name: %q (expected %q)", c.Provider.Name, ProviderStatic)
}
if c.Static.User != "u" || c.Static.Pass != "p" || c.Static.Addr != "turn.example.com:3478" {
t.Errorf("Static: %+v", c.Static)
}
}
func TestParseClient_ProviderStaticMissingFlags(t *testing.T) {
args := []string{"-peer", "1.2.3.4:5000", "-provider", "static"}
_, err := ParseClient(args, io.Discard)
if err == nil || !strings.Contains(err.Error(), "static-user") {
t.Errorf("expected static flags error, got %v", err)
}
}
func TestParseClient_ProviderUnknown(t *testing.T) {
args := []string{"-peer", "1.2.3.4:5000", "-provider", "bogus"}
_, err := ParseClient(args, io.Discard)
+2 -2
View File
@@ -1,6 +1,6 @@
// Package provider определяет абстракцию источника TURN-реквизитов.
//
// Реализации (internal/provider/vk, internal/provider/static, ...) поставляют
// Реализации (internal/provider/vk, ...) поставляют
// короткоживущие user/pass/addr для TURN-allocate. Pipeline (proxy/udprelay,
// proxy/tcpfwd) работает только через этот интерфейс — никаких VK-specific
// типов в общем коде.
@@ -51,7 +51,7 @@ type Provider interface {
// Используется только при возврате ошибки, обёрнутой ErrBackoffActive.
BackoffUntilUnix() int64
// Name — короткое имя провайдера для логов ("vk", "static", "metered").
// Name — короткое имя провайдера для логов ("vk", ...).
Name() string
}
-72
View File
@@ -1,72 +0,0 @@
// Package static — провайдер фиксированных TURN-реквизитов из CLI-флагов.
//
// Используется для подключения к любому совместимому TURN-серверу (coturn,
// metered, self-hosted) с долгоживущими credentials. Без сетевого control-plane:
// никаких captcha, throttling, lockout. Проверяет, что абстракция provider.Provider
// действительно достаточна для не-VK источников.
package static
import (
"context"
"fmt"
"strings"
"github.com/samosvalishe/btp/internal/provider"
)
// Config — параметры static-провайдера.
type Config struct {
User string // TURN username (обязателен)
Pass string // TURN password (обязателен)
Addr string // TURN server host:port (обязателен)
}
// Provider реализует provider.Provider, всегда возвращает одни и те же реквизиты.
type Provider struct {
creds provider.Credentials
}
// New создаёт static-провайдер. Валидирует обязательные поля.
func New(cfg Config) (*Provider, error) {
if cfg.User == "" {
return nil, fmt.Errorf("static: empty User")
}
if cfg.Pass == "" {
return nil, fmt.Errorf("static: empty Pass")
}
if cfg.Addr == "" {
return nil, fmt.Errorf("static: empty Addr")
}
if !strings.Contains(cfg.Addr, ":") {
return nil, fmt.Errorf("static: Addr must be host:port, got %q", cfg.Addr)
}
return &Provider{
creds: provider.Credentials{
User: cfg.User,
Pass: cfg.Pass,
ServerAddr: cfg.Addr,
},
}, nil
}
// GetCredentials возвращает фиксированные реквизиты.
func (p *Provider) GetCredentials(_ context.Context, _ int) (provider.Credentials, error) {
return p.creds, nil
}
// IsAuthError — для static-провайдера auth-ошибки не приводят к ротации
// реквизитов (их просто негде взять). Возвращаем false всегда: pipeline сам
// сделает retry с тем же кешем.
func (*Provider) IsAuthError(error) bool { return false }
// HandleAuthError ничего не делает (нет кеша для инвалидации).
func (*Provider) HandleAuthError(int) bool { return false }
// ResetErrors ничего не делает.
func (*Provider) ResetErrors(int) {}
// BackoffUntilUnix всегда 0 — backoff не реализован.
func (*Provider) BackoffUntilUnix() int64 { return 0 }
// Name реализует provider.Provider.
func (*Provider) Name() string { return "static" }
-67
View File
@@ -1,67 +0,0 @@
package static
import (
"context"
"errors"
"strings"
"testing"
"github.com/samosvalishe/btp/internal/provider"
)
func TestNew_Validation(t *testing.T) {
t.Parallel()
cases := []struct {
name string
cfg Config
want string
}{
{"empty user", Config{Pass: "p", Addr: "h:1"}, "User"},
{"empty pass", Config{User: "u", Addr: "h:1"}, "Pass"},
{"empty addr", Config{User: "u", Pass: "p"}, "Addr"},
{"addr without port", Config{User: "u", Pass: "p", Addr: "host"}, "host:port"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := New(tc.cfg)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Errorf("got %v, want substring %q", err, tc.want)
}
})
}
}
func TestProvider_GetCredentials(t *testing.T) {
t.Parallel()
p, err := New(Config{User: "alice", Pass: "secret", Addr: "turn.example.com:3478"})
if err != nil {
t.Fatal(err)
}
got, err := p.GetCredentials(context.Background(), 0)
if err != nil {
t.Fatal(err)
}
want := provider.Credentials{User: "alice", Pass: "secret", ServerAddr: "turn.example.com:3478"}
if got != want {
t.Errorf("got %+v, want %+v", got, want)
}
}
func TestProvider_InterfaceContract(t *testing.T) {
t.Parallel()
p, _ := New(Config{User: "u", Pass: "p", Addr: "h:1"})
if p.IsAuthError(errors.New("anything")) {
t.Error("IsAuthError should always return false")
}
if p.HandleAuthError(0) {
t.Error("HandleAuthError should always return false")
}
p.ResetErrors(0)
if p.BackoffUntilUnix() != 0 {
t.Error("BackoffUntilUnix should always return 0")
}
if p.Name() != "static" {
t.Errorf("Name=%q, want static", p.Name())
}
var _ provider.Provider = p
}