v5.0.73: req.Close=true — force new TCP per request
This commit is contained in:
@@ -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 "^>|^<"
|
||||
```
|
||||
|
||||
Разница в одну строку в дампе покажет причину.
|
||||
|
||||
Reference in New Issue
Block a user