13 KiB
Вот, отправь Соннету:
Контекст: Пишем Terraform Provider для Nubes Cloud (ColdFusion API). Провайдер на Go, версия 5.0.66, опубликован в registry terra.k8c.ru/nubes-test/nubes.
Суть проблемы: terraform apply для создания S3-бакета падает с ошибкой. Прошли путь от 403 до EOF.
Хронология:
HTTP 403— DDoS-Guard блокирует Gocrypto/tls(JA3 fingerprint). curl с того же сервера работает.- Поменяли URL с
index.cfm/instancesнаindex.cfm?endpoint=/instances— 403 остался. - Добавили
utls.HelloFirefox_120— 403 ушёл, но сталEOF(сервер закрывает соединение без ответа). - Убрали utls, оставили
InsecureSkipVerify: true—EOF. - Вернули REST-путь (
index.cfm/instances) +InsecureSkipVerify: true—EOF.
Что работает: curl с токеном напрямую с того же сервера:
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}.
Вопросы:
- Почему Go HTTP клиент (с идентичными заголовками) получает EOF там, где curl получает JSON?
- Может ли ColdFusion-сервер требовать определённый порядок заголовков или HTTP-метод (POST вместо GET)?
- Как заставить Go
http.Clientотправлять точно такие же запросы как curl? Может, черезnet/http/httputilдампить запрос?
Файлы:
- client.go
- provider.go
Список задач обновлен
Searched for files matching **/client.go, 2 matches
Searched for text doRequest|DoRequest (**/universal_rebuild/internal/core/client.go), 20 results
Searched for text doRequest|DoRequest (**/internal/core/client.go), 20 results
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
Теперь посмотрю на S3-ресурс и полный URL, чтобы понять что именно запрашивается:
Searched for text s3|bucket|S3|Bucket (**/universal_rebuild/internal/core/client.go), 8 results
Теперь у меня достаточно контекста. Сформирую план.
Анализ и план
Корневая причина 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() добавить две строки:
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:
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
Шаг 3 — Debug dump (если 1+2 не помогают)
Добавить через env-флаг NUBES_DEBUG_HTTP=1:
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 ..." ....
Верификация:
terraform applyдля S3-бакета — нет EOFcurl -v --http2 <url>— проверить что с--http2curl тоже получает EOF (подтвердит гипотезу)terraform planпосле успешного apply
Ждать «делай» для реализации?
Создано задач: 3
Вопрос Соннету:
Контекст: Terraform Provider на Go. API: ColdFusion за DDoS-Guard.
Что сделали:
TLSNextProto = make(map[string]...)— GET работает (200 JSON)?endpoint=формат URL — как у генератораUser-Agent: Mozilla/5.0
Проблема: GET → 200. POST → 403. curl (GET и POST) → оба 200.
Код POST:
func (c *UniversalClient) doRequest(ctx context.Context, method, path string, payload interface{}) ([]byte, http.Header, error) {
// payload — json-сериализованная структура
req, _ := http.NewRequestWithContext(ctx, method, c.ApiEndpoint, body)
// ?endpoint=/instances
q := req.URL.Query()
q.Set("endpoint", path)
req.URL.RawQuery = q.Encode()
req.Header.Set("User-Agent", "Mozilla/5.0")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Authorization", "Bearer "+token)
}
Работающий curl:
curl -X POST \
-H "User-Agent: Mozilla/5.0" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"serviceId":13}' \
"https://deck-api-test.ngcloud.ru/api/v1/index.cfm?endpoint=/instances"
Вопрос: Что в Go http.Client (c TLSNextProto = make(...)) может вызывать 403 на POST, при том что GET работает, и curl POST тоже работает? Может ли http.NewRequestWithContext добавлять заголовки/байты, которые триггерят DDoS-Guard на POST но не на GET?
Список задач обновлен
Прочитаю актуальный код doRequest и инициализацию transport, чтобы ответить точно, а не по памяти:
Диагноз: 3 кандидата, #1 самый вероятный
Код виден полностью. Вот что реально отличает Go POST от curl POST.
#1 (главный подозреваемый): url.Values.Encode() кодирует / как %2F
q.Set("endpoint", endpointPath) // endpointPath = "/instances"
req.URL.RawQuery = q.Encode() // → endpoint=%2Finstances
curl отправляет: ?endpoint=/instances
Go отправляет: ?endpoint=%2Finstances
Почему GET проходит, а POST нет? DDoS-Guard WAF для POST-запросов почти всегда имеет allowlist эндпоинтов — проверяет, что endpoint соответствует разрешённым путям. /instances совпадает, %2Finstances — нет → 403. GET-запросы проверяются слабее (read-only, меньший риск).
#2: Нет заголовка Accept
curl всегда добавляет Accept: */* автоматически.
Ваш Go-код его не ставит.
DDoS-Guard fingerprinting видит: POST с телом + нет Accept → нет Accept = нет браузера = потенциальный бот.
#3: Accept-Encoding: gzip добавляет transport
Т.к. DisableCompression не выставлен, transport добавляет Accept-Encoding: gzip ко всем запросам. Для GET это тоже есть → GET проходит → вероятно, не причина. Но на всякий случай стоит проверить.
Как проверить немедленно
DumpRequestOut уже в коде:
NUBES_DEBUG_HTTP=1 terraform apply 2>&1 | grep -A 30 ">>> REQ POST"
И сравнить с:
curl -v -X POST -H "User-Agent: Mozilla/5.0" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"serviceId":13}' \
"https://deck-api-test.ngcloud.ru/api/v1/index.cfm?endpoint=/instances" 2>&1 | grep -E "^>|^<"
Что менять
Fix #1 — не использовать q.Encode() для endpoint:
// Вместо q.Set("endpoint",...) + q.Encode()
rawQuery := "endpoint=" + endpointPath // слэш не кодируется
if extraQuery != "" {
rawQuery += "&" + extraQuery
}
req.URL.RawQuery = rawQuery
Fix #2 — добавить Accept: */* (как curl):
req.Header.Set("Accept", "*/*")
Fix #3 (опционально) — отключить автоматическое gzip в transport:
transport.DisableCompression = true
Итог
| Причина | Почему GET ок | Почему POST 403 |
|---|---|---|
%2Finstances в URL |
WAF для GET мягче | WAF для POST проверяет allowlist путей |
Нет Accept |
То же | Browser fingerprint не проходит |
Connection: close |
- | (здесь req.Close = true нет, уже убрано) |
Начинать с Fix #1 — вероятность ~70% что это именно оно.
Создано задач: 4