From 42210c225009abe4966fc219cd4f8bc463a35ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Wed, 1 Jul 2026 16:09:54 +0400 Subject: [PATCH] =?UTF-8?q?v5.0.73:=20req.Close=3Dtrue=20=E2=80=94=20force?= =?UTF-8?q?=20new=20TCP=20per=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HISTORY/SONNET/0107.md | 207 ++++++++++++++++++++++ TEST_STAND/buck0/main.tf | 2 +- universal_rebuild/internal/core/client.go | 3 + universal_rebuild/main.go | 2 +- 4 files changed, 212 insertions(+), 2 deletions(-) diff --git a/HISTORY/SONNET/0107.md b/HISTORY/SONNET/0107.md index 4c7ea0a..b0f6f24 100644 --- a/HISTORY/SONNET/0107.md +++ b/HISTORY/SONNET/0107.md @@ -290,3 +290,210 @@ transport.DisableCompression = true Начинать с Fix #1 — вероятность ~70% что это именно оно. Создано задач: 4 + + +Вопрос Соннету: + +--- + +**Контекст**: Terraform Provider Go. API за DDoS-Guard. GET работает, POST — 403. + +**Проверено**: +- `TLSNextProto = make(...)` — ✅ GET 200 +- `?endpoint=/instances` (не `%2F`) — ✅ +- `User-Agent: Mozilla/5.0` — ✅ +- `Accept: */*` — ✅ +- curl POST (те же URL, тело, заголовки) — ✅ 200/400 +- Go-тест POST (отдельная программа, тот же TLS-конфиг) — ✅ 201 + +**Провайдер**: `doRequest()` → `json.Marshal(payload)` → `bytes.NewBuffer(b)` → `http.NewRequestWithContext(ctx, "POST", url, body)` → `c.HttpClient.Do(req)` → **403**. + +**Код**: +```go +// provider.go +transport := http.DefaultTransport.(*http.Transport).Clone() +transport.TLSNextProto = make(map[string]func(...)) +transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12} +client := &core.UniversalClient{ + HttpClient: &http.Client{Transport: transport, Timeout: 300 * time.Second}, + ... +} + +// client.go doRequest() +b, _ := json.Marshal(payload) +body := bytes.NewBuffer(b) +req, _ := http.NewRequestWithContext(ctx, "POST", c.ApiEndpoint, body) +req.URL.RawQuery = "endpoint=/instances" +req.Header.Set("User-Agent", "Mozilla/5.0") +req.Header.Set("Accept", "*/*") +req.Header.Set("Content-Type", "application/json") +req.Header.Set("Authorization", "Bearer "+token) +resp, err := c.HttpClient.Do(req) +``` + +**Вопрос**: Что в Go `http.Client` с кастомным `Transport` (пустой `TLSNextProto` + `TLSClientConfig`) может вызывать 403 только на POST, при том что: +1. Отдельная Go-программа с идентичным TLS-конфигом делает POST успешно (201) +2. curl делает POST успешно +3. GET-запросы через тот же `doRequest` работают + +Есть ли способ дампнуть сырые байты TLS-хендшейка или HTTP-запроса из провайдера (не из тестовой программы), чтобы сравнить с curl? + +Список задач обновлен + +Ключевой факт: **standalone Go-программа с тем же кодом → 201, провайдер → 403**. Значит причина не в TLS-конфиге и не в заголовках — она в окружении или состоянии самого провайдера. + +--- + +## Четыре кандидата + +### #1 (самый вероятный): `HTTPS_PROXY` в окружении Terraform + +`http.DefaultTransport.Clone()` копирует `Proxy: http.ProxyFromEnvironment`. Если при `terraform apply` выставлена переменная `HTTPS_PROXY`, трафик пойдёт через прокси. Прокси может GET пропускать (кэш), а POST — блокировать или трансформировать. + +Standalone-тест работает? Проверьте, запускался ли он в том же shell с теми же env: + +```bash +env | grep -i proxy +``` + +Фикс — убрать прокси из transport: + +```go +transport.Proxy = nil // добавить в provider.go +``` + +--- + +### #2: `DefaultTransport` уже модифицирован до `Clone()` + +Terraform Plugin Framework инициализирует gRPC-транспорт и **может изменить глобальный `http.DefaultTransport`** до того, как ваш `Configure` его клонирует. Standalone-тест стартует с чистым DefaultTransport. + +Фикс — не клонировать DefaultTransport, создать с нуля: + +```go +transport := &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 60 * time.Second, + ResponseHeaderTimeout: 60 * time.Second, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper), + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: insecureSkipVerify, + MinVersion: tls.VersionTLS12, + }, +} +``` + +--- + +### #3: Повторное использование TCP-соединения (keep-alive) + +Провайдер: клиент создаётся один раз → GET занимает соединение → POST переиспользует то же соединение из пула. DDoS-Guard или ColdFusion может иметь правило: POST на keep-alive соединении после GET = подозрительно. + +Standalone-тест: свежий клиент → каждый запрос на новом соединении. + +Быстрая проверка — добавить `req.Close = true` (форсирует новое соединение на каждый запрос): + +```go +req.Close = true +``` + +Если после этого POST пройдёт — причина именно в connection reuse. + +--- + +### #4: Параллельность в Terraform + +Terraform может запускать несколько `Create` параллельно. Rapid-fire POST'ы с одного IP → DDoS-Guard rate limit → 403. Standalone-тест делает один POST. + +--- + +## Как дампить реальные байты из провайдера + +### Уровень HTTP (уже есть, но улучшить) + +Добавить дамп и ответа: + +```go +if os.Getenv("NUBES_DEBUG_HTTP") == "1" { + dump, _ := httputil.DumpRequestOut(req, true) + fmt.Fprintf(os.Stderr, "\n>>> REQ %s %s\n%s\n", method, req.URL.String(), dump) + + resp, err := c.HttpClient.Do(req) + + respDump, _ := httputil.DumpResponse(resp, false) // false = без тела + fmt.Fprintf(os.Stderr, "\n<<< RESP %d\n%s\n", resp.StatusCode, respDump) + return resp, err +} +``` + +### Уровень TLS (сырые байты + расшифровка через Wireshark) + +```go +// В provider.go при создании transport: +if keylogPath := os.Getenv("SSLKEYLOGFILE"); keylogPath != "" { + f, _ := os.OpenFile(keylogPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) + transport.TLSClientConfig.KeyLogWriter = f +} +``` + +Запуск: +```bash +# Терминал 1: захват трафика +sudo tcpdump -i any -w /tmp/cap.pcap 'host deck-api-test.ngcloud.ru' + +# Терминал 2: terraform apply +SSLKEYLOGFILE=/tmp/sslkeys.log terraform apply + +# После: открыть cap.pcap в Wireshark +# Edit → Preferences → TLS → (Pre)-Master-Secret log: /tmp/sslkeys.log +``` + +### Уровень TCP (кастомный DialContext — логировать сырые байты) + +```go +transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + c, err := (&net.Dialer{Timeout: 30 * time.Second}).DialContext(ctx, network, addr) + if err != nil || os.Getenv("NUBES_DEBUG_HTTP") != "1" { + return c, err + } + return &loggingConn{Conn: c, tag: addr}, nil +} + +type loggingConn struct { + net.Conn + tag string +} +func (lc *loggingConn) Write(b []byte) (int, error) { + fmt.Fprintf(os.Stderr, "TCP→%s WRITE %d bytes: %q\n", lc.tag, len(b), b) + return lc.Conn.Write(b) +} +func (lc *loggingConn) Read(b []byte) (int, error) { + n, err := lc.Conn.Read(b) + fmt.Fprintf(os.Stderr, "TCP←%s READ %d bytes: %q\n", lc.tag, n, b[:n]) + return n, err +} +``` + +--- + +## Что делать прямо сейчас + +```bash +# Шаг 1: проверить прокси +env | grep -iE "proxy|https_proxy|http_proxy" + +# Шаг 2: если прокси нет, тест без connection reuse +# добавить req.Close = true в doRequest, пересобрать, apply + +# Шаг 3: если не помогло — дамп +NUBES_DEBUG_HTTP=1 terraform apply 2>&1 | grep -A 40 ">>> REQ POST" +# сравнить с: +curl -v -X POST ... 2>&1 | grep -E "^>|^<" +``` + +Разница в одну строку в дампе покажет причину. diff --git a/TEST_STAND/buck0/main.tf b/TEST_STAND/buck0/main.tf index 2466062..e340cb0 100644 --- a/TEST_STAND/buck0/main.tf +++ b/TEST_STAND/buck0/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { nubes = { source = "terra.k8c.ru/nubes-test/nubes" - version = "5.0.72" + version = "5.0.73" } } } diff --git a/universal_rebuild/internal/core/client.go b/universal_rebuild/internal/core/client.go index 3840dd3..7ef4e78 100644 --- a/universal_rebuild/internal/core/client.go +++ b/universal_rebuild/internal/core/client.go @@ -847,6 +847,9 @@ func (c *UniversalClient) doRequest(ctx context.Context, method, path string, pa } req.URL.RawQuery = rawQuery + // Форсируем новое TCP-соединение: DDoS-Guard может блочить POST на keep-alive + req.Close = true + req.Header.Set("User-Agent", userAgent) req.Header.Set("Accept", "*/*") if body != nil { diff --git a/universal_rebuild/main.go b/universal_rebuild/main.go index d759ddb..55b3a2a 100644 --- a/universal_rebuild/main.go +++ b/universal_rebuild/main.go @@ -10,7 +10,7 @@ import ( ) var ( - version string = "5.0.72" + version string = "5.0.73" ) func main() {