v5.0.68: TLSNextProto fix — disable HTTP/2 ALPN (DDoS-Guard EOF)
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
Вот, отправь Соннету:
|
||||
|
||||
---
|
||||
|
||||
**Контекст**: Пишем Terraform Provider для Nubes Cloud (ColdFusion API). Провайдер на Go, версия 5.0.66, опубликован в registry `terra.k8c.ru/nubes-test/nubes`.
|
||||
|
||||
**Суть проблемы**: `terraform apply` для создания S3-бакета падает с ошибкой. Прошли путь от 403 до EOF.
|
||||
|
||||
**Хронология**:
|
||||
1. `HTTP 403` — DDoS-Guard блокирует Go `crypto/tls` (JA3 fingerprint). curl с того же сервера работает.
|
||||
2. Поменяли URL с `index.cfm/instances` на `index.cfm?endpoint=/instances` — 403 остался.
|
||||
3. Добавили `utls.HelloFirefox_120` — **403 ушёл**, но стал `EOF` (сервер закрывает соединение без ответа).
|
||||
4. Убрали utls, оставили `InsecureSkipVerify: true` — `EOF`.
|
||||
5. Вернули REST-путь (`index.cfm/instances`) + `InsecureSkipVerify: true` — `EOF`.
|
||||
|
||||
**Что работает**: curl с токеном напрямую с того же сервера:
|
||||
```bash
|
||||
curl -H "Authorization: Bearer $TOKEN" -H "User-Agent: Mozilla/5.0" \
|
||||
"https://deck-api-test.ngcloud.ru/api/v1/index.cfm?endpoint=/instances&page=1&size=100"
|
||||
# → JSON, 200 OK
|
||||
```
|
||||
|
||||
**Код**: `doRequest()` в client.go (строка 831). Использует `http.DefaultTransport.Clone()` с `TLSClientConfig{InsecureSkipVerify: true, MinVersion: TLS1.2}`.
|
||||
|
||||
**Вопросы**:
|
||||
1. Почему Go HTTP клиент (с идентичными заголовками) получает EOF там, где curl получает JSON?
|
||||
2. Может ли ColdFusion-сервер требовать определённый порядок заголовков или HTTP-метод (POST вместо GET)?
|
||||
3. Как заставить Go `http.Client` отправлять точно такие же запросы как curl? Может, через `net/http/httputil` дампить запрос?
|
||||
|
||||
**Файлы**:
|
||||
- client.go
|
||||
- provider.go
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
Searched for files matching `**/client.go`, 2 matches
|
||||
|
||||
Read memory [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/memory-tool/memories/repo/tf_provider_inventory.md)
|
||||
|
||||
Searched for text `doRequest|DoRequest` (`**/universal_rebuild/internal/core/client.go`), 20 results
|
||||
|
||||
Searched for text `doRequest|DoRequest` (`**/internal/core/client.go`), 20 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#800-800), lines 800 to 900
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/internal/core/client.go#177-177), lines 177 to 260
|
||||
|
||||
Searched for text `HttpClient|NewClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport` (`**/universal_rebuild/internal/core/client.go`), 5 results
|
||||
|
||||
Searched for text `HttpClient|NewClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport` (`**/internal/core/client.go`), 7 results
|
||||
|
||||
Searched for text `HttpClient|NewUniversalClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport|http.Client{` (`**/universal_rebuild/internal/provider/**`), 11 results
|
||||
|
||||
Searched for text `HttpClient|NewUniversalClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport|http.Client{` (`**/internal/provider/**`), 20 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/provider/provider.go#105-105), lines 105 to 165
|
||||
|
||||
Теперь посмотрю на S3-ресурс и полный URL, чтобы понять что именно запрашивается:
|
||||
|
||||
Searched for text `s3|bucket|S3|Bucket` (`**/universal_rebuild/internal/core/client.go`), 8 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#1-1), lines 1 to 50
|
||||
|
||||
Теперь у меня достаточно контекста. Сформирую план.
|
||||
|
||||
Created memory file [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/memory-tool/memories/MjE3NzcyMzEtOGRkYy00ODA4LWI0OGItOGEyZDliNmU5YjFl/plan.md)
|
||||
|
||||
## Анализ и план
|
||||
|
||||
### Корневая причина EOF
|
||||
|
||||
**Главный виновник — HTTP/2.**
|
||||
|
||||
`http.DefaultTransport.Clone()` копирует ссылку на `TLSNextProto` map, в которой зарегистрирован HTTP/2 (`h2`) handler. При TLS-хендшейке Go предлагает `h2` в ALPN-расширении. ColdFusion + DDoS-Guard не поддерживают HTTP/2 — сервер принимает TLS-соединение, но сразу закрывает его без HTTP-ответа → **EOF**.
|
||||
|
||||
Именно поэтому curl работает: он по умолчанию использует HTTP/1.1 и не предлагает `h2` в ALPN.
|
||||
|
||||
`InsecureSkipVerify` здесь роли не играет — он влияет только на проверку сертификата, не на TLS-хендшейк.
|
||||
|
||||
---
|
||||
|
||||
### Ответы на вопросы
|
||||
|
||||
**1. Почему Go получает EOF, а curl — нет?**
|
||||
Go через ALPN предлагает `["h2", "http/1.1"]`. Сервер видит `h2` и закрывает соединение (ColdFusion не понимает HTTP/2). curl предлагает только `["http/1.1"]` — сервер отвечает нормально.
|
||||
|
||||
**2. Может ли ColdFusion требовать особый порядок заголовков?**
|
||||
Маловероятно причина. Порядок заголовков важен для WAAP/DDoS-Guard fingerprinting, но если 403 ушёл, то DDoS-Guard вас пропускает, а EOF — это именно HTTP/2 на уровне приложения.
|
||||
|
||||
**3. Как заставить Go отправлять то же, что curl?**
|
||||
Двумя действиями ниже.
|
||||
|
||||
---
|
||||
|
||||
### Plan: Fix EOF (2 изменения)
|
||||
|
||||
**Файлы к изменению:**
|
||||
- provider.go — строка ~124
|
||||
- client.go — строка ~836 (`doRequest`)
|
||||
|
||||
**Шаг 1 — Отключить HTTP/2** (приоритет #1, наиболее вероятный фикс)
|
||||
|
||||
В provider.go после `transport := http.DefaultTransport.(*http.Transport).Clone()` добавить две строки:
|
||||
|
||||
```go
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.TLSHandshakeTimeout = 60 * time.Second
|
||||
// Отключаем HTTP/2: ColdFusion + DDoS-Guard не поддерживают h2 → EOF
|
||||
transport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
|
||||
transport.ForceAttemptHTTP2 = false // запрещаем upgrade
|
||||
```
|
||||
|
||||
> `ForceAttemptHTTP2 = false` одного недостаточно — нужен именно пустой `TLSNextProto`, иначе `h2` всё равно остаётся в ALPN.
|
||||
|
||||
**Шаг 2 — Убрать Content-Type на GET** (приоритет #2, возможная причина WAF-блокировки)
|
||||
|
||||
В `doRequest()` в client.go заголовок `Content-Type` выставлять только если payload != nil:
|
||||
|
||||
```go
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
```
|
||||
|
||||
**Шаг 3 — Debug dump** (если 1+2 не помогают)
|
||||
|
||||
Добавить через env-флаг `NUBES_DEBUG_HTTP=1`:
|
||||
|
||||
```go
|
||||
if os.Getenv("NUBES_DEBUG_HTTP") == "1" {
|
||||
dump, _ := httputil.DumpRequestOut(req, true)
|
||||
fmt.Fprintf(os.Stderr, ">>> REQUEST:\n%s\n", dump)
|
||||
}
|
||||
```
|
||||
|
||||
Потом сравнить вывод с `curl -v --http1.1 -H "Authorization: Bearer ..." ...`.
|
||||
|
||||
---
|
||||
|
||||
**Верификация:**
|
||||
1. `terraform apply` для S3-бакета — нет EOF
|
||||
2. `curl -v --http2 <url>` — проверить что с `--http2` curl тоже получает EOF (подтвердит гипотезу)
|
||||
3. `terraform plan` после успешного apply
|
||||
|
||||
---
|
||||
|
||||
Ждать «делай» для реализации?
|
||||
|
||||
Создано задач: 3
|
||||
|
||||
Reference in New Issue
Block a user