Обрыв ws/httpupgrade/grpc/httpsplit/xhttp сессий через Российские CDN (Timeweb)

Проблема с маскировкой за ру cdn по типу timeweb и яша.

cап, ntcparty. уже несколько дней убил на реверс-инжиниринг и чтение китайских ишью в репо xtls, так что пришел с просьбой о помощи коллективного разума сюда.

Что я хочу сделать:

пытаюсь собрать для личного использования (семья/друзья) максимально надежный fallback-канал (разумеется надёжный с учётом текущих условий :melting_face:) для условий жестких бс, в частности по Мск и МО на мобильном инете.

Входные данные:

  • впска в нидерландах (тестил vdsina и 62yun).
  • привязанный домен с валидными SSL (делал как обычно через certbot тобишь let’s encrypt)
  • архитектура: Client (Happ) ➝ RU CDN (Timeweb) ➝ Origin VPS (нидерланды) ➝ Nginx (TLS Termination) ➝ Xray-core (VLESS)

В чём самая соль:

без cdn, тобишь если идти напрямую на origin-сервер, всё идеально ворк. проблемы начинаются как раз при использовании подключения за cdn timeweb и яши через CNAME записи, начинаются танцы с бубном, на сколько я понял но L7. при попытке коннекта (ws/xhttp/и др что писал в заголовке) туннель вроде даже начинает пробиваться, и даже в логах на origin-серваке есть некоторые признаки работы в логах nginx, но потом сразу идут 101 ошибки Switching Protocols. затем трафик либо не идёт совсем, хотя туннель формально есть, либо просто терминируется сессия. если пустить браузер через всю эту свистопляску, то ловлю классический PR_END_OF_FILE_ERROR. проблема на сколько я понял комплексная и тд, поэтому ошибок было много, как и перепробованных конфигов. думаю понятно что я не смогу приложить всё сюда, поэтому выделю самое важное из проделанной работы.

Более подробно про проблемы с которыми я сталкивался:

  1. борьба с кэширующими Edge-нодами (403 Edge HIT)
    сначала при использовании xhttp ловил от CDN глухой 403 Forbidden. прострел через curl показал заголовок < x-cdn-edge-cache: HIT, то есть балансировщик Timeweb один раз словил отбой от моего Nginx, закэшировал ошибку прямо на Edge-узле и дальше даже не пытался стучаться на Origin.
    что сделал: сбросил кэш в панелию. в корень / положил белый index.html с нужными правами (www-data), чтобы анти-фрод сканнеры CDN успокоились и видели легитимный сайт.

  2. TLS Handshake и Double TLS конфликты
    сначала ловил SSL_ERROR_BAD_CERT_DOMAIN и x509: certificate is valid for *.a.abcdef.net… это классика: нода терминирует TLS и отдает свой дефолтный серт.
    что сделал: чтобы исключить отвал по соображениям безопасности на клиенте, пробовал два пути:
    а) на стороне, в частности Happ, ставил allowInsecure: true.
    б) заливал свои ключи fullchain.pem и Let’s Encrypt прямо в панель Timeweb и привязывал к ресурсу.
    хэндшейки чинятся, но от разрыва самой сессии это не спасает. также убрал security: “tls” на inbound-е Xray origin-сервера, чтобы не ловить ошибку двойного шифрования, так как трафик ко мне приходит уже расшифрованным от Nginx.

  3. ошибки буферизации (104 и PR_END_OF_FILE)
    пытался завести модный xhttp через grpc_pass (http2) в Nginx. ловил в error.log сплошные таймауты: upstream timed out (110: Unknown error) while reading upstream.
    что сделал: вернулся на классический ws через дефолтный proxy_pass. добавил proxy_buffering off; и proxy_request_buffering off;, выкрутил таймауты на 3600s. только после этого в access-логах стабильно пошли статусы 101 Switching Protocols. но радовался недолго - соединение висит пару секунд и падает с 499 Client Closed Request в Nginx и 104: Unknown error внутри проксирующего слоя.

  4. исключение клиентских петель (Routing Loops)
    у меня была гипотеза, что Happ в режиме TUN чудит с маршрутизацией (захватывает пакеты, идущие до IP CDN-ноды, и пытается пихнуть их внутрь самого туннеля).
    что сделал: отказался от Happ для чистоты эксперимента. cобрал голый xray-core на ПК, благо у меня линух, дело пары минут. запустил чистый локальный SOCKS5. в качестве Address хардкодом вписал технический домен ноды abcdefg.cdn.abcdefg.ru, а Host и SNI жестко привязал к cdn.example.com. результат тот же: 101-й статус есть, трафика 0.

Какая-то часть конфигов которая у меня осталась от экспериментов

Вариант 1: конфиги для xhttp:

  • настройки панели CDN:

    • источник: 185.0.0.0, Порт: 443, Протокол: HTTPS
    • заголовки запроса: cdn.example.com
  • Nginx (origin сервер):

server {
    listen 443 ssl http2;
    server_name cdn.example.com;

    ssl_certificate /etc/letsencrypt/live/cdn.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cdn.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;

    # декой для сканеров антифрода 
    location / {
        root /var/www/html;
        index index.html;
    }

    # сам туннель
    location /api/v1/update {
        client_max_body_size 0;
        grpc_read_timeout 315s;
        grpc_send_timeout 5m;
        grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        grpc_pass grpc://127.0.0.1:10000;
    }
}
  • Xray (origin сервер):
{
  "log": {
    "loglevel": "debug"
  },
  "inbounds": [
    {
      "port": 10000,
      "listen": "127.0.0.1",
      "protocol": "vless",
      "settings": {
        "clients": [
          {
            "id": "00000000-0000-0000-0000-000000000000",
            "flow": ""
          }
        ],
        "decryption": "none"
      },
      "streamSettings": {
        "network": "xhttp",
        "security": "none",
        "xhttpSettings": {
          "path": "/api/v1/update",
          "mode": "auto"
        }
      }
    }
  ],
  "outbounds": [
    {
      "protocol": "freedom",
      "tag": "direct"
    }
  ]
}
  • Конфиг клиента
vless://00000000-0000-0000-0000-000000000000@node-123.cdn.provider.ru:443?encryption=none&security=tls&sni=cdn.example.com&alpn=h2&type=xhttp&host=cdn.example.com&path=%2Fapi%2Fv1%2Fupdate#CDN_XHTTP_Test
  • Логи:
    • Лог Nginx (error.log):
[error] 785338#785338: *645 upstream timed out (110: Unknown error) while reading upstream, client: 89.0.X.X, server: cdn.example.com, request: "GET /api/v1/update/95e445f2-0f93-4ce8-9fc7-413ce408449b HTTP/2.0", upstream: "grpc://127.0.0.1:10000", host: "cdn.example.com"...
  • Ответ CDN (через curl):
> GET /api/v1/update HTTP/2
> Host: cdn.example.com
> Upgrade: websocket
> Connection: Upgrade
* Request completely sent off
< HTTP/2 403 
< server: nginx
< x-cdn-edge-id: 2018
< x-cdn-edge-cache: HIT
< x-cdn-request-id: 845931565f08efb41c88fc72578db376
<html><body><center><h1>403 Forbidden</h1></center></body></html>

Вариант 2: конфиги для ws (nginx в режиме unbuffered):

  • Настройки панели CDN как в варианте выше

  • Конфиг Nginx (ogrigin сервер):

server {
    listen 443 ssl http2;
    server_name cdn.example.com;

    ssl_certificate /etc/letsencrypt/live/cdn.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cdn.example.com/privkey.pem;

    location / {
        root /var/www/html;
        index index.html;
    }

    location /api/v1/update {
        if ($http_upgrade != "websocket") {
            return 404;
        }
        proxy_pass http://127.0.0.1:10000;
        
        # хэдеры для ws
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;

        #  отрубаем buffering
        proxy_buffering off;
        proxy_request_buffering off;
        
        # конские таймауты
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}
  • Конфиг Xray (origin server):
{
  "inbounds": [
    {
      "port": 10000,
      "listen": "127.0.0.1",
      "protocol": "vless",
      "settings": {
        "clients": [
          {
            "id": "00000000-0000-0000-0000-000000000000",
            "flow": ""
          }
        ],
        "decryption": "none"
      },
      "streamSettings": {
        "network": "ws",
        "security": "none",
        "wsSettings": {
          "path": "/api/v1/update",
          "headers": {
            "Host": "cdn.example.com"
          }
        }
      }
    }
  ],
  "outbounds": [{"protocol": "freedom","tag": "direct"}]
}
  • Клиентский конфиг (Happ):
{
    "inbounds": [{"listen": "127.0.0.1", "port": 10808, "protocol": "socks"}],
    "outbounds": [
        {
            "protocol": "vless",
            "settings": {
                "vnext": [{
                    "address": "node-123.cdn.provider.ru",
                    "port": 443,
                    "users": [{"id": "00000000-0000-0000-0000-000000000000", "encryption": "none", "level": 8}]
                }]
            },
            "streamSettings": {
                "network": "ws",
                "security": "tls",
                "tlsSettings": {
                    "allowInsecure": true,
                    "serverName": "cdn.example.com",
                    "fingerprint": "chrome"
                },
                "wsSettings": {
                    "host": "cdn.example.com",
                    "path": "/api/v1/update"
                }
            }
        }
    ]
}
  • Логи:
    тут можно наблюдать пробой L7 фильтрации, видно в логах Nginx (access.log) что хоть что-то пришло на сервер:
193.39.X.X - - [14/May/2026:14:56:51 +0300] "GET /api/v1/update HTTP/1.1" 101 13907 "-" "Go-http-client/1.1"
193.39.X.X - - [14/May/2026:14:56:51 +0300] "GET /api/v1/update HTTP/1.1" 101 61009 "-" "Go-http-client/1.1"

но тем не менее, клиент выдаёт одну из двух ошибок:

    • ошибка 1: PR_END_OF_FILE_ERROR;
    • ошибка 2 (access.log в Nginx на origin сервере): 499 Client Closed Request
193.39.X.X - - [14/May/2026:14:56:51 +0300] "GET /api/v1/update HTTP/1.1" 499 0 "-" "Go-http-client/1.1"

Варик номер 3: попытка обхода кэширующих аномалий WAF

  • Настройки панели CDN как и в остальных вариантах

  • Конфиг Nginx (origin сервер):

server {
    listen 443 ssl http2;
    server_name cdn.example.com;

    ssl_certificate /etc/letsencrypt/live/cdn.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cdn.example.com/privkey.pem;

    location / {
        # проверка на ws
        if ($http_upgrade = "websocket") {
            proxy_pass http://127.0.0.1:10000;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            break;
        }

        # выдаём декой
        root /var/www/html;
        index index.html;
    }
} 
  • Конфиг Xray (origin сервер и клиент):
    по факту от предыдущего отличается только этим:
"wsSettings": {
          "path": "/",
          "headers": { "Host": "cdn.example.com" }
        }

для этого конфига сохранившихся логов не нашёл, но, по-моему, проблема была в том, что трафик до cdn шёл, а на сервер просто не приходил, т.е. на сервере логов вообще не было никаких, а на клиенте вроде как всё было адекватно, ну разумеется кроме статуса N/A.

Недо вариант 4: сервер без Nginx, онли Xray

  • Конфиг Xray:
{
  "inbounds": [
    {
      "port": 443,
      "protocol": "vless",
      "settings": {
        "clients": [{"id": "00000000-0000-0000-0000-000000000000", "flow": ""}],
        "decryption": "none"
      },
      "streamSettings": {
        "network": "xhttp",
        "security": "tls",
        "tlsSettings": {
          "certificates": [
            {
              "certificateFile": "/path/to/cert/fullchain.pem",
              "keyFile": "/path/to/cert/privkey.pem"
            }
          ]
        },
        "xhttpSettings": {
          "path": "/api/v1/update",
          "mode": "auto",
          "scMaxConcurrentPosts": 10,
          "scMaxEachPostBytes": 1000000,
          "scMinPostsIntervalMs": 30
        }
      }
    }
  ]
}

проблема идентичная варианту выше.


Собственно, вопросы к гуру:

глядя на все эти танцы с бубнами, которые я перепробовал, и дропы сессий, у меня складывается стойкое ощущение, что я уперся в L7 WAF отечественных CDN. похоже, их фильтры каким-то образом палят, что внутри ws, xhttp или (подставить любой друго протокол) трубы бегает не типичный веб-трафик, и они агрессивно делают сеппуку long-lived сессиям.

тот же MWS, например, у коммерсов живет, но хрен знает каким образом.

Подскажите, плз:

  1. как сейчас модно обманывать анти-фрод / WAF того же Timeweb’а или Яши? Режут ли они сейчас ws сессии по таймаутам на обычных тарифах, или я банально где-то обосрался на стыке Nginx ↔ Xray в плане хэдеров/буферов?

  2. нужны ли сейчас какие-то специфические настройки мультика/чанкинга (mux, smux, ограничение scMaxEachPostBytes) именно под ру-балансировщики, чтобы маскировать VLESS внутри?

  3. или без Enterprise-аккаунта “для своих” (где можно вырубить инспекцию трафика на Edge-нодах) ловить за такими CDN сейчас вообще нечего и надо искать другие пути диверсификации?

буду чертовски благодарен за любые пинки в нужное русло или примеры живых конфигов под эти условия. заранее выражаю благодарность!

Так и есть. Я скажу больше - достаточно в логах парочки POST’ов с кодом 200 и отчетливыми характеристиками xhttp (а они у вас как я понял уже есть) что бы аккаунт отлетал в бан. Попробуйте сначала вместо xhttp инбаунда поднять обычный веб-сервер и посмотрите на путь (в частности его параметры) и заголовки запроса - сразу поймете о чем я. Собственно у вас как раз отсутствует в конфигурации поле extra, которое позволяет частично защитится от такого детекта.

P.S. не совсем понимаю, почему вы считаете CDN за

максимально надежный fallback-канал

В подсетях CDN точно также стоит ТСПУ, а если где-то его нет, то это вопрос времени, пока он там появится. На текущий момент самые надежные протоколы - xicmp (потому что ICMP не фильтруется (и кстати, не только он :wink: ), xdns он же dnstt. От детекта CDN бы малыми трудами врят-ли сможете защитится, все равно спалитись по характерным признакам:

  1. Параметры в URL
  2. Хедеры в запросах
  3. 0% кэшхит
  4. JA3/JA4 не соответствующий браузерному
  5. Наличие крайне большого количества POST/upgrade.
  1. reality прогнать через cdn не получится из-за tls termination (вы этого не пробуете, но для будущих поколений оставлю). Пока cdn не передаёт tls поток чистым (что он и не делает), reality будет ломаться. Поэтому выбираем xhttp, который для cdn и делался
  2. xhttp активно палится провайдерами из-за характерного признака - padding. На фоне этого появились параметры, позволяющие изменить место, внешний вид и размеры этого самого паддинга - читайте мануал
  3. Отдельного внимания заслуживает tls. Если вы боритесь с белыми списками, вам нужен белый ip + белый sni. Первое у вас есть, а со вторым могут быть проблемы. Почему? На примере одного из cdn - для tls поверх xhttp вам надо серт для белого sni. Через встроенный в cdn let’s encrypt у вас не получится (cname не настроите), а самоподписанный серт прогрузить - cdn его проверяет. Поэтому делаем жизнь проще - гуляем до cdn по голому http: белый домен в http host есть, и этого достаточно. А дальше уже пофигу, будет cdn до вас гулять по http или https
  4. Отвечая на вопрос “зачем cdn” товарища выше - задача обойти не тспу, а белые списки. Всё, что мы хотим здесь - построить канал до контролируемого сервера. А дальше уже с него можно гонять трафик как угодно

padding_obfs настраивал в XHTTP?
padding_key / header / placement и т.д. ?

В условиях белых списков фильтруется, даже для диапазонов под белым списком не факт, что ICMP сработает. Это уже наблюдалось на МегаФоне.

Я конечно не утверждаю, но такое ощущение, что такой трафик будет под более пристальным наблюдением.

ща ради интереса поднял vless+xhttp через RU-CDN, попробуй поправить

  1. extra в xhttp у тебя пустой. без xPaddingBytes/xmux/scStreamUpServerSecs xhttp ловится на сигнатурах и WAF

  2. в панели timeweb CDN доп. HTTP-методы on, Secure token off, Всегда онлайн off, gzip off

  3. eсли нужен мульти-хост, ставь host: "" и фильтруй на nginx server_name.

  4. reality через cdn не поднимется в принципе (tls termination на edge)

  5. nginx на origin: proxy_buffering off, proxy_request_buffering off, таймауты 3600s, Cache-Control: no-store на туннельный path. allowInsecure не нужен — настоящий LE на origin работает нормально.

ну и поднимай слоями. сначала vless+xhttp без CDN, убедись что туннель встаёт потом CDN сверху

Так товарищи я разобрался в чём была проблема. Не буду выкладывать в открытый доступ и давать подробную инфу по настройке, но дам наводки (т.к. тут сидят боты от ркн скорее всего и читают всё).
Во первых делайте curl и читайте подробную информацию о заголовках.
Во вторых сидите с wireshark и анализируйте дампы трафика по фильтру http2.
В третьих почитайте issue и офф документацию:

Большое спасибо всем, кто меня натолкнул на правильный путь!

прикольно, у меня видимо не такой агрессивный waf. спс что поделился

streisand на ios если что поддерживает ядро нужное

Я бы с радостью накрутил tls, но тогда либо sni не белый, либо серт не пустят. Конечно, есть варианты, например использовать другие CDN и т.д. Но пока что работает, а там посмотрим

VLESS уже давно умеет шифрование заголовков именно для таких кейсов.

Подскажешь пожалуйста конфигурацию nginx чтобы запросы корректно отлавливались или в какую сторону копать?

Тоже удалось запустить через CDN (и сквозь Nginx и напрямую в xray). Но только в packet-up. Звонки через FaceTime при этом не работают. Padding_obfs в режиме stream-up не работает?

подскажите, а как в таком случае CDN понимает куда на самом деле отправлять трафик?

Обычно это указывается в настройках самого cdn

Cdn использует 1 входной айпи адрес для всех сайтов, роутинг на ваш выходной айпи адрес vps происходит как раз по имени домена в host header (если его менять на чужой, то соответственно выходной айпи будет на чужую впс). О чем вы непонятно вообще, это просто какие-то предположения или у вас действительно что-то работает?

Возможно, у вашего cdn это так и работает, но не все такие одинаковые, не стоит грести всех под одну гребёнку. Всё, что я описываю, активно пользуется мной и друзьями. Если что-то непонятно - можно, например, задать соответствующие вопросы. Не стройте из себя всезнайку с наездами на всех подряд. Регулярно замечаю это у вас в разных темах.

В 3 раз спрашивать одно и то же это как-то странно… Расскажите, каким образом cdn отправит запрос на вашу впс если вы будете указывать http host: max.ru (без sni)?

Процитируйте пожалуйста мой наезд, я не специально

О дааа, я прям вижу, что всезнайки задают вопросы из разряда

Вам конеретно сказали, что ваша схема непонятна, спросили работает ли это или вы предполагаете. На последнее вы ответили, правда с неуместной критикой нормального общения, а вот подробнее описать схему, и какой cdn позволяет проксировать http с чужими SNI вы так и не соизволили. Описали только какую то мифическую настройку на CDN, которая непонятно как работает с технической точки зрения. После такого у меня лично выводы по поводу высокомерного общения складываются обратные от тех, которые вы пытаетесь озвучить.