v5.0.73: req.Close=true — force new TCP per request

This commit is contained in:
“Naeel”
2026-07-01 16:09:54 +04:00
parent 11f5464367
commit 42210c2250
4 changed files with 212 additions and 2 deletions
+207
View File
@@ -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 "^>|^<"
```
Разница в одну строку в дампе покажет причину.
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "terra.k8c.ru/nubes-test/nubes"
version = "5.0.72"
version = "5.0.73"
}
}
}
@@ -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 {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
)
var (
version string = "5.0.72"
version string = "5.0.73"
)
func main() {