doc: находки — как та же сетевая болезнь платформы обойдена в drhider (буфер на ВМ, pull вместо push)

Собраны и проверены по источникам все факты из ~/nubes/drhider:
- диагноз: ingress рвёт входящие тела >64КБ, egress не ограничен;
- схема пофайлового транзита через ВМ-буфер + egress-GET (pull);
- применённые фиксы платформы (cilium mtu:1400, keep-alive 10->75,
  proxy-body-size 1024m, HTTP/2 off) через ShturvalServiceConfig;
- ложные следы, лимиты, SSRF/path-traversal защиты, тесты 25/25;
- вывод: тикет в поддержку — не решение, нужен архитектурный обход.
This commit is contained in:
Repinoid
2026-10-08 16:50:36 +03:00
parent b947f1d467
commit a1d72b61d4
@@ -0,0 +1,176 @@
# Находки: как та же сетевая болезнь платформы уже решена в drhider (буфер на ВМ)
**Дата:** 2026-10-08
**Источник находок:** репозиторий `~/nubes/drhider` (+ `~/nubes/drhider/upload-platform`)
**Зачем:** у shared-sqs та же проблема (таймауты внешнего пути, большие тела), и
наивный путь «отправить тикет в Nubes» оказался тупиком — явление признано
болезнью платформы. В drhider ту же болезнь **обошли архитектурно**, а не починили.
> ## ⚠️ ГЛАВНЫЙ ВЫВОД
> **Ingress managed-кластера обрывает ВХОДЯЩИЕ тела больше порога, а ИСХОДЯЩИЙ
> трафик (egress) из кластера не ограничен.** Отсюда рабочий приём: не «толкать»
> большие данные в кластер (push), а **«вытягивать» их извне (pull)** — данные
> кладутся на внешний хост-буфер, а приложение в кластере забирает их исходящим
> запросом. Тикет в поддержку проблему не решает.
---
## 1. Диагноз проблемы (цитаты из drhider)
`upload-platform/HISTORY/2026-09-06-layer2-architecture-and-api-boundaries.md:18`:
> «**Причина существования**: Ingress-контроллер k8s-кластера (`pythonk8s`)
> обрывает входящие запросы с телом более 64 КБ. Прямой `POST` больших файлов в
> кластер невозможен; **исходящий трафик (egress) из кластера не ограничен**.»
`PLAN-upload-layers.md:27` (контекст, «что уже проверено»):
> «Паттерн загрузки (**шлюз managed-кластера рвёт тела >64КБ, egress не
> ограничен**): браузер → `PUT` на ВМ-буфер → Flask `POST /api/upload_refs` →
> egress `GET` (pull) → сессия.»
Это ровно тот же класс, что явление A у shared-sqs (большие POST), только у
drhider порог «тело >64 КБ» на ingress, а у SQS — MSS/MTU-порог ~1.4 КБ на
внешнем шлюзе (`doc/thinking/nubes-ticket.md`, явление A). Причина на стороне
платформы в обоих случаях.
## 2. Схема обхода, применённая в drhider (пошагово)
`upload-platform/HISTORY/2026-09-06-layer2-architecture-and-api-boundaries.md:19-27`
— «Пофайловый транзит (Per-File Transit)»:
1. Файл №k браузер отправляет методом `PUT` **в буфер на ВМ** (вне кластера).
2. Браузер сразу шлёт в приложение (Flask в кластере) `POST /api/upload_refs` —
тело запроса маленькое (только ссылка на файл), поэтому ingress его пропускает.
3. Приложение **само забирает** файл исходящим потоковым `GET` прямо в память
сессии — egress не режется.
4. Приложение немедленно отправляет `DELETE` на ВМ — буфер очищен.
5. Браузер переходит к файлу №k+1. **В буфере на ВМ в любой момент максимум один файл.**
Дополнительные принципы оттуда же:
- **RAM-only**: без постоянной записи на диск ни в кластере, ни на ВМ.
- **Zero hardcode**: адрес буфера и префикс — параметры конфигурации
(`vmUploadUrl` на фронте, `vmUploadPrefix` на бэке для обязательной SSRF-проверки).
- **Автономный mock-буфер** (in-memory WebDAV) для тестов без внешней сети.
`upload/README.md:13` подтверждает ту же механику:
> «Пофайловый стриминг: для каждого файла выполняется `PUT` в буфер на ВМ,
> немедленный `POST /api/upload_refs` в бэкенд, и после подтверждения приёма
> буфер на ВМ очищается.»
## 3. Что нельзя потерять при переносе (накоплено в drhider)
`PLAN-upload-layers.md` (раздел «Требования/что НЕЛЬЗЯ потерять»):
- дедупликация «имя+размер» (иначе суффикс `_2`);
- раскрытие ZIP с путями и фильтр расширений (`.pdf .doc .docx .txt .md`);
- рекурсивный обход папок (`webkitdirectory`);
- **SSRF-валидация URL** (`url.startswith(VM_UPLOAD_PREFIX)`);
- `_safe_name` — защита от path traversal с сохранением подпапок;
- **повторы pull** (3 попытки, пауза 2 с);
- лимиты **50 МБ на файл / 500 МБ на сессию**;
- различение «сессия не найдена» и «превышен лимит»;
- статусы «пропущен (лимит)» / «не извлечён»;
- экранирование имён для защиты от self-XSS.
## 4. Фиксы платформы, которые уже применили (и их границы)
`PROBLEM-AND-SOLUTION.md` (drhider, v0.0.31, 2026-07-16):
**Проблема 1 — пауза 51 с (MTU).** Underlay MTU 1450, Cilium Geneve добавляет
50 байт, дефолт пода 1500 → пакет 1550 → фрагментация → дроп → ядро ждёт
повторной передачи 51 с. **Решение:** `cilium-config` → `mtu: 1400`
(1400+50 = 1450). Задокументировано 2026-07-13, применено 2026-07-14.
**Проблема 2 — `ERR_CONNECTION_RESET` в браузере.** Истинная причина —
`keep-alive: 10` вместо дефолта 75: пока пользователь выбирал файлы (>10 с),
nginx закрывал idle-соединение, браузер не замечал FIN и слал POST в закрытый
сокет → RST. **Решение** (в платформе Штурвала, 2026-07-15) — через
`ShturvalServiceConfig`, а не через kubectl:
```yaml
controller:
config:
keep-alive: "75"
proxy-body-size: "1024m"
client-body-timeout: "60"
client-header-timeout: "30"
hostPort:
enabled: true
replicaCount: 2
service:
type: LoadBalancer
```
> Важно: правки ingress на платформе делаются через `ShturvalServiceConfig` —
> **через kubectl нельзя, ArgoCD откатывает**.
**Ложные следы, которые опровергли** (та же таблица): kube-vip ARP flapping,
hostPort на 2 из 4 нод, upstream keepalive, conntrack, MTU (для проблемы 2),
HTTP/2.
**Результаты после фиксов (v0.0.29, 2026-07-15):** 10× POST 63 КБ — 10/10 OK;
полный UI-цикл (5 файлов + SSE + ZIP + CSV) — OK; 5× POST с интервалом 75 с —
5/5 OK; PDF 5 МБ через curl — 200 OK за 0.62 с. **Итого 25/25, 0 RST.**
⚠️ Но это вылечило ingress-таймер и MTU-фрагментацию, **а не порог размера
тела**: обходной слой через ВМ-буфер остался нужен и является рабочим решением.
## 5. Обходы «не по сети» — тот же принцип pull/вынос
`PROBLEM-AND-SOLUTION.md:71`: `doc_to_markdown()` переписали — вместо локального
`soffice` (LibreOffice на ВМ, тяжёлый и не тянул) **HTTP-вызов внешнего сервиса**
`liberta.containerk8s.dev.nubes.ru/convert` через `httpx`. То есть тяжёлую/медленную
работу тоже вынесли наружу и ходят к ней по HTTP.
Смежное (из кода drhider): конвертация `.doc` через `liberta` выполняется
**последовательно** (`_LLM_CONCURRENCY = 1`) — сервис не выдерживает
конкурентность (`drhider/extractor.py:160-194`, `drhider/scanner.py:122`).
## 6. Готовый переиспользуемый модуль (для справки)
`PLAN-upload-layers.md` — слои выделены так, чтобы их можно было перенести в
любой проект копированием (без общей репы):
| Слой | Где | Ответственность |
|---|---|---|
| 0. Конфиг | общий | адрес ВМ-буфера, расширения, лимиты |
| 1. Выбор файлов | фронт | таблица, дедуп, ZIP, дерево папок, статусы |
| 2. Закачка через ВМ | фронт + бэк | `PUT` на ВМ → `upload_refs` pull → сессия |
| 3. Логика приложения | бэк | обработка (у каждого своя) — **не выносится** |
Реализация-эталон: `~/nubes/drhider/upload-platform/` (Слой 1 v0.1.13 готов,
Слой 2 в работе на 2026-09-06), а также рабочий образец `~/nubes/drhider/upload/`.
## 7. Инфраструктурные факты
- **ВМ-буфер:** `5.172.178.213` (вне кластера). Nginx WebDAV, каталоги
`/var/www/drhider-upload/`, `/var/www/contracts-upload/`, cron-очистка каждые
5 минут (`find … -mmin +30 -delete`). Для целевого прода — перевод в
RAM/in-memory буфер.
- **SSH к ВМ:** `ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213`
(`README.md:141`).
- **Кластеры/неймспейсы:** `pythonk8s` (ingress режет >64 КБ); сервисы drhider,
contractor, uploader-dev.
## 8. Выводы (что берём из чужого опыта)
1. **Тикет в Nubes — не решение.** Это болезнь платформы; в drhider её не «починили»,
а обошли. Тикет (`doc/thinking/nubes-ticket.md`) имеет смысл только как
фиксация для истории, не как способ получить рабочий сервис.
2. **Рабочий приём — pull вместо push:** внешний буфер + исходящий `GET` из
кластера. Egress свободен, ingress режется.
3. **Правки ingress на платформе** — только через `ShturvalServiceConfig`
(kubectl откатывается ArgoCD).
4. **MTU-фикс (`cilium mtu: 1400`)** нужен как гигиена, но порога больших тел не
снимает.
5. **Тяжёлые/медленные операции** тоже выносятся наружу и вызываются по HTTP
(пример: `liberta`).
## Источники (файл:строка)
| Источник | Что |
|---|---|
| `~/nubes/drhider/PROBLEM-AND-SOLUTION.md` | MTU-фикс, keep-alive 10→75, ложные следы, тесты 25/25 |
| `~/nubes/drhider/PLAN-upload-layers.md:27` | паттерн «шлюз рвёт >64 КБ, egress свободен» |
| `~/nubes/drhider/upload/README.md:13` | пофайловый стриминг через ВМ-буфер |
| `~/nubes/drhider/upload-platform/HISTORY/2026-09-06-layer2-architecture-and-api-boundaries.md:18-27` | диагноз + пошаговый пофайловый транзит, RAM-only, mock-буфер |
| `~/nubes/drhider/docs/MSS-CLAMPING-PLAN.md`, `docs/about1500.md`, `docs/INGRESS-SOLUTIONS.md` | сопутствующие материалы по MTU/MSS/ingress |
| `~/nubes/drhider/drhider/extractor.py:160-194`, `drhider/scanner.py:122` | вынос конвертации в сервис `liberta`, последовательные вызовы |