Тестируем XHTTP

@MasterYoba А не затруднит пояснить за топологию этой схемы, пытаюсь понять, как это все работает. Вот есть клиент, который хочет сходить на экстремистский Твиттер. Обычно он идет на ваш прокси и там в туннеле сообщает вам, что хочет посетить Твиттер, наш прокси сервер ему его приносит в зашифрованном туннеле. А тут куда запрос клиента в вашем случае идет? На сервер Cloudflare или на ваш UP домен? Куда дальше идет ответ с Твиттера, как он понимает, что нужно ответить на DOWN домен? И какую роль тут играет Cloudflare?

Нет, разницы никакой, этот ipv6 служит только для стыковки сервера с cloudflare и цензору он не виден. Можно всё повесить на один ipv4 адрес, именно так китайцы в конфигах выше и делают. У меня просто был дополнительно свободный ipv6 адрес, cloudflare с ними дружит, и я захотел сделать вот так.

Минус пока в том, что все популярные клиенты на Sing-Box, а его авторы запрос на поддержку XHTTP дропнули и не известно, будут ли вообще его прикручивать. Поэтому нам остается только V2RayN.

Так подождите, а V2RayN поддержку 32бит выпилили что ли. В 6 версии была же вроде

Всё то же самое, просто соединение с прокси сервером строится не напрямую, а через cloudflare. Клиент сначала строит “честное” tls соединение с сервером Cloudflare с SNI вашего купленного домена, а внутрь него заворачивает соединение с экстремистским твиттером посредством vless с выбранным транспортом через ваш VPS прокси-сервер. VPS сервер как бы спрятан за CDN как за щитом, внешний наблюдатель видит только обращение к ip-адресам CF и SNI вашего домена, но про сам VPS ничего не знает. Всё что может сделать цензор - забанить ваш домен, который легко сменить.

Вот тут очень хорошая статья по этой теме:

Из минусов тут только характеристика трафика tls-in-tls, как раз с которой вроде как и должен помогать транспорт xhttp. Теперь с ним запросы от клиента идут по одному “каналу” к CDN с внешним SNI domain1, а ответы от сервера по второму каналу с внешним SNI domain2. Насколько это эффективно - черт его знает, но выглядит перспективнее, чем “простое” проксирование через CDN с транспортом вебсокет или grpc.

@MasterYoba Я правильно понял, что эти ваши домены просто висят в воздухе, получается, зарегистрированы на CF и все, на них нет никаких сайтов? А Xray сервер у вас на третьем домене или на голом IP? Или как?

Домен зарегистрированный в CF указывает на IP-адрес vps сервера (v4 или v6 - без разницы), весь трафик к этому домену на 443 порт CF проксирует через себя, т.е. при DNS резолвинге отдаёт клиенту не IP-адрес сервера, а IP-адреса своих “промежуточных” серверов.

На vps сервере на 443 порту слушает nginx, а там уже можно хостить любой контент, и веб сайты в том числе. То есть можно сделать domain1.com/mywebsite - ваша веб-страничка с котиками, domain1.com/proxy - grpc_pass на сокет, где слушает xray. Вообще, чем об этом так рассуждать проще самому посмотреть и поиграться, цена вопроса - рублей 100 за какой-нибудь самый дешевый домен (главное не .ru/su/рф), а cloudflare бесплатен.

А как можно не указывать порт при создании inbound в панели 3x ?
Я когда ввожу /dev/shm/xrxh.socket,0666 и выбираю порт 0, панель возвращает значение 1 вместо 0 )

Оставить как есть. В json файле конфигурации и в списке входящих соединений в панеле будет 0. Почему-то в настройках инбаунда он 1 показывает, недоработка UI.

Настроил раздельный XHTTP по китайским гайдам. С одним доменом даже обошелся и через 3X-UI по большей части. Домен регистрируем в Cloudflare, у регистратора, где покупали прописываем кастомные DNS сервера, которые выдаст CF, включаем ему gRPC (в СF кабинет настройки домена, закладка Network), отключаем ECH, который запрещен в РФ.

На 3Х-UI сервере запускаем меню командой x-ui и выбираем там Cloudflare SSL Certificate. Нужно ввести свой имейл аккаунта CF, Global API Key (настройки домена в CF кабинете, вкладка Overview, внизу справа) и имя домена. Он создаст автоматом родной сертификат CloudFlare на 15 лет и импортирует его в панель. В вкладке SSL/TLS Encryption для домена в кабинете CF выбираем режим Full (Strict). Теперь вход только по CF сертификату.

В панеле 3Х-UI создаем новый инбаунд. Назовем, чтобы не запутаться Remark: Vision-Reality. Порт 443. Дальше именно щас важно в Fallback в поле Dest вписать порт 2023, на котором нас будет слушать следующий инбаунд. Transmission TCP. Дальше выбираем Security: Reality. Fallback исчезает, не пугайтесь, его настройки в JSON конфиге сохранятся. В Dest (Target) прописываем порт 7443, на котором будет слушать Nginx. Xver = 1, общаются они через Proxy Protocol. В SNI указываем наш cdn-domain.com. Возвращаемся в раздел Client и ставим Flow: xtls-rprx-vision. Возвращаемся в настройки Reality. Генерируем ключи. Жмем Create. Готово.

Создаем второй инбаунд. Remark: XHTTP. Listen 127.0.0.1. Port 2023. Transmission: XHTTP. Path: xhttp-path (такой же как в конфиге Nginx). Security: TLS. SNI: cdn-domain.com - ваш домен. Жмем Set Cert from Panel, он подставит сгенерированные ранее сертификаты CloudFlare. Create. Готово.

Код для сервера Nginx.

server {

	root /var/www/html;
	index index.html index.htm index.nginx-debian.html;

	server_name cdn-domain.com www.cdn-domain.com; // Укажите свой домен на CloudFlare

	location / {
		try_files $uri $uri/ =404;
	}

	listen 127.0.0.1:7443 ssl http2 proxy_protocol;
	set_real_ip_from 127.0.0.1;
	real_ip_header proxy_protocol;

	ssl_certificate /root/cert-CF/cdn-domain.com/fullchain.pem; // Путь к сертификатам CloudFlare
	ssl_certificate_key /root/cert-CF/cdn-domain.com/privkey.pem; // Путь к сертификатам CloudFlare

	location /xhttp-path {
		proxy_http_version 1.1;
		proxy_pass http://127.0.0.1:2023;
		proxy_redirect off;
		proxy_request_buffering off;
		proxy_set_header Host $host;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	}
}

server {
    listen 80;
    listen [::]:80;
    server_name cdn-domain.com www.cdn-domain.com; // Укажите свой домен на CloudFlare
    return 302 https://$server_name$request_uri;
}

Дальше нужно клиентскую часть загнать в V2RayN (единственный на сегодня клиент, который умеет в XHTTP). Здесь придется поколдовать руками, два наших соединения надо сшить в одного хитрого Франкенштейна. Экспортируем ключик от Vision-Reality в V2RayN. Он как есть работать не будет! Экспортируем его оттуда в буфер и допиливаем секцию “outbounds” в блокноте. Если у вас нет второго, “прямого” домена, вместо direct-domain.com просто вбиваем IP вашего Xray сервера.

  "outbounds": [
    {
      "tag": "proxy",
      "protocol": "vless",
      "settings": {
        "vnext": [
          {
            "address": "cdn-domain.com", // Ваш домен CloudFlare
            "port": 443,
            "users": [
              {
                "id": "6cc4e521-e0db-4c7d-a4c6-453f5d8c64e8", // поменять на ID инбаунда XHTTP
                "email": "t@t.tt",
                "security": "auto",
                "encryption": "none",
                "flow": "xtls-rprx-vision"
              }
            ]
          }
        ]
      },
      "streamSettings": {
        "network": "xhttp", //поменять на "xhttp"

        // добавить объект "xhttpSettings"

        "xhttpSettings": {
          "mode": "auto",
          "path": "/xhttp-path", // Path из инбаунда XHTTP
          "extra": {
            "downloadSettings": {
              "address": "direct-domain.com", // прямой домен или IP Xray сервера
              "port": 443,
              "network": "xhttp",
              "xhttpSettings": {
                "mode": "auto",
                "path": "/xhttp-path" // Path из инбаунда XHTTP
              }
            }
          }
        },

        "security": "reality",
        "realitySettings": {
          "serverName": "direct-domain.com", // меняем на прямой домен или IP Xray сервера
          "fingerprint": "chrome",
          "show": false,
          "publicKey": "ob_ZLNraBpBrEi5YuedCt_eg69IfXWBWANy86-lVKg0", // Публичный ключ инбаунда Vision-Reality, оставить как есть
          "shortId": "4ef078", // Short ID инбаунда Vision-Reality, оставить как есть
          "spiderX": "/"
        }
      },
      "mux": {
        "enabled": false,
        "concurrency": -1
      }
    }

Импортируем снова в V2RayN. Запускаем, работаем!

Плюсы. Гиперпаранойность. Все запросы идут в восходящем потоке через CloudFlare, цензор не видит IP вашего VPS, вы обращаетесь к IP CDN. Нисходящий поток идет к вам с VPS, тут его IP видно, но связать домен с IP цензор не может. Детектировать такую схему намного сложнее.

Минусы. Сложность в настройке. Вырастут задержки из-за того, что у нас еще CDN появляется в цепочке. Также из-за того, что XHTTP шинкует трафик, снижается полезная полоса, т.е. несколько снизится скорость загрузок.

Гипотетически, если использовать VLESS+TLS со своим доменом с Ngnix на 80 и 443 порту для сайта-заглушки, то пока нет смысла прятать IP VPS?

Пока все работает. Это сильно паранойный сценарий на гипотетическое северокорейское будущее. Но и тут прибегут другие параноики, которые скажут, да этот ваш CloudFlare забанят к чертям. Нельзя исключать и такие варианты. Но как по мне, тут уже не настройки надо будет искать, а чемодан паковать.

По какой причине не сможет ?
Сама сеть выглядит подозрительно когда исходящие запросы идут в цдн и тут же возвращаются с какого-то непонятного айпишника, который на проверку окажется ещё и в пуле хост провайдера, тут же очевидно наш прокси хитрюля спрятался.
Не понятны такие телодвиженя, они рассчитаны на то что роскомнадзор будет честно себя вести ?

Тут как раз все безобидно выглядит. Я отправляю какие-то запросы на CDN, т.е. как будто члены семьи посещают какие-то сайты с котиками, за этим CDN сидящие. А параллельно с одного IP качаю файло, например, и на этом IP легальный файлообменник к тому же. Как цензор может это вскрыть и увязать, что это одна сессия? Как от свяжет моит параллельные запросы к эсктремисткому фейсбуку и к легальному новостному сайту, сидящими за одним CDN? Никак. За CDN он не видит, куда я хожу. На этом CDN и строят свою безопасность. На этом и китайский трюк строится. Как раз когда вы сидите постоянно в одном туннеле, этот трафик, хотя и тоже нельзя вскрыть, и посмотреть куда вы идете, но его можно хотя бы статистически анализировать. А тут что с чем коррелирует? Как это по пакетам вынюхивать? Тут головоломка на порядок сложнее.

и возвращаются оттуда же…

NowAndThen выше пишет что не оттуда, кто из вас прав ?
Мне казалось что в этом и смысл возни с Xhhtp - гонять трафик через цднку и маскировать айпи своего сервера.

цитата? я ничего не понял

Еще глюк обнаружился у этого сетапа. Хитрый юзерский ручной конфиг не стартует у меня при запуске V2RayN, нужно сначала выбрать какой-то другой конфиг, а потом переключиться на этот раздельный XHTTP. Т.е. в автозагрузке он не работает. И на Андроиде в V2RayNG запустить этот конфиг вообще не удалось. Вобщем, юзабельность этой технологии пока сильно в бета стадии.

Приветствую. Извиняюсь если подобный вопрос был, я не нашёл ответов.

Ловлю следующую ошибку:
[Info] transport/internet/splithttp: invalid x_padding length:0

Версии софта:

  • nginx/1.26.2
  • Xray 24.12.31

Кто-нибудь сталкивался с подобным? Ниже приведу свои конфигурации nginx’а и xray’я как для сервера, так и для клиента.

На всякий случай сообщу что использую path поверх уже существующей конфигурации nginx’а под приложение. Однако, хочу заверить что схема с vless-nginx через websocket работала и работает, проблемы возникли при пробе XHTTP.

Некоторые переменные заменил, даю им здесь определение:

  • myhost - алиас сервера, не особо важно, просто строка для всяких директорий
  • my.host.com - домен для доступа к серверу через HTTP (ex: https://my.host.com/)
  • mypath - путь в nginx’е перенаправляющий на xray инстанс
  • myuuid - мой uuid

Конфигурация nginx:

upstream myhost {
    server unix:/srv/.venvs/myhost/run/gunicorn.sock fail_timeout=0;
}

server {
    server_name my.host.com;
    listen 80;
    return 301 https://my.host.com$request_uri;
}

server {
    server_name my.host.com;
    listen 443 ssl;
    listen 443 quic reuseport ipv6only=off;

    ssl_protocols TLSv1.3 TLSv1.2;
    ssl_certificate /etc/letsencrypt/live/my.host.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/my.host.com/privkey.pem;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GC    M-SHA384;

    client_header_timeout 5m;
    keepalive_timeout 5m;

    client_max_body_size 15M;

    access_log /var/log/myhost/nginx/access/access.log;
    error_log /var/log/myhost/nginx/error/error.log;

    location /mypath {
        client_max_body_size 0;
        grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        client_body_timeout 5m;
        grpc_read_timeout 315;
        grpc_send_timeout 5m;
        grpc_pass unix:/dev/shm/xrxh.socket;
    }

    location /static/ {
        alias /srv/myhost/static/;
    }

    location / {
        proxy_pass http://myhost/;
        proxy_http_version 1.1;

        proxy_connect_timeout 1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Конфигурация для xray server:

{
    "log": {
        "loglevel": "debug",
        "access": "/var/log/xray/access.log",
        "error": "/var/log/xray/error.log",
        "dnsLog": false
    },
    "inbounds": [
        {
            "listen": "/dev/shm/xrxh.socket,0666",
            "protocol": "vless",
            "settings": {
                "clients": [
                    {
                        "id": "myuuid"
                    }
                ],
                "decryption": "none"
            },
            "streamSettings": {
                "network": "xhttp",
                "xhttpSettings": {
                    "mode": "stream-up",
                    "path": "/mypath"
                }
            }
        }
    ],
    "outbounds": [
        {
            "tag": "direct",
            "protocol": "freedom",
            "settings": {}
        },
        {
            "tag": "blocked",
            "protocol": "blackhole",
            "settings": {}
        }
    ],
    "routing": {
        "domainStrategy": "AsIs",
        "rules": [
            {
                "type": "field",
                "ip": [
                    "geoip:private"
                ],
                "outboundTag": "blocked"
            },
            {
                "type": "field",
                "domain": ["geosite:category-ads-all"],
                "outboundTag": "block"
            }
        ]
    }
}

Конфигурация для xray client:

{
    "log": {},
    "inbounds": [
        {
            "port": "1080",
            "listen": "127.0.0.1",
            "protocol": "socks",
            "settings": {
                "udp": true
            }
        }
    ],
    "outbounds": [
        {
            "protocol": "vless",
            "settings": {
                "vnext": [
                    {
                        "address": "my.host.com",
                        "port": 443,
                        "users": [
                            {
                                "id": "myuuid",
                                "encryption": "none"
                            }
                        ]
                    }
                ]
            },
            "streamSettings": {
                "network": "xhttp",
                "xhttpSettings": {
                    "path": "/mypath",
                    "mode": "stream-up",
                    "#xmux": {
                        "maxConcurrency": 128,
                        "hMaxRequestTimes": 1000,
                        "hMaxReusableSecs": 3600
                    },
                    "#downloadSettings": {
                        "address": "my.host.com",
                        "port": 443,
                        "network": "xhttp",
                        "xhttpSettings": {
                            "path": "/mypath",
                            "#xmux": {
                                "maxConcurrency": 128,
                                "hMaxRequestTimes": 1000,
                                "hMaxReusableSecs": 3600
                            }
                        },
                        "security": "tls"
                    }
                },
                "security": "tls",
                "tlsSettings": {
                    "alpn": [
                        "h3"
                    ]
                }
            }
        },
        {
            "tag": "direct",
            "protocol": "freedom",
            "settings": {}
        },
        {
            "tag": "blocked",
            "protocol": "blackhole",
            "settings": {}
        }
    ],
    "routing": {
        "domainStrategy": "IPOnDemand",
        "rules": [
            {
                "type": "field",
                "ip": [
                    "geoip:private"
                ],
                "outboundTag": "direct"
            }
        ]
    }
}

Попробуйте в xhttpSettings добавить такие параметры, это панель 3X-UI ставит по умолчанию, там, я смотрел, автор тупо рекомендуемые параметры из документации по умолчанию подставляет.

          "scMaxBufferedPosts": 30,
          "scMaxEachPostBytes": "1000000",
          "xPaddingBytes": "100-1000"

Спасибо за ответ и приношу извинения за глупый вопрос, оказывается invalid x_padding length:0 не актуальная ошибка.

С моего клиента не приходили запросы на мой сервер, я решил попробовать пингануть path через curl, а в дальнейшем принял логи с curl’а как за те, что пришли с клиента. Спутал их. Очевидно что с курла будет ошибка авторизации и вообще там не был отправлен padding.

Настоящая проблема в том что запросы с клиента не доходят по какой-то причине, по всей видимости слишком криворукая конфигурация клиента/nginx’а. Попробую потом как-то решить.

2025/01/04 01:37:17 [Debug] [2049168422] transport/internet: dialing to udp:my.host.com:443
2025/01/04 01:37:17 [Info] [2049168422] transport/internet/splithttp: failed to POST https://my.host.com/mypath/7300f7be-ced5-411d-b209-ba16096ac392?x_padding=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 > Post "https://my.host.com/mypath/7300f7be-ced5-411d-b209-ba16096ac392?x_padding=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": lookup my.host.com: no such host
2025/01/04 01:37:17 [Info] [2049168422] proxy/vless/outbound: tunneling request to tcp:130.255.77.28:443 via my.host.com:443
2025/01/04 01:37:18 [Info] [2049168422] app/proxyman/inbound: connection ends > proxy/socks: connection ends > context canceled
2025/01/04 01:37:18 [Info] [1788824348] proxy/socks: TCP Connect request to tcp:130.255.77.28:443
2025/01/04 01:37:18 [Info] [1788824348] app/dispatcher: default route for tcp:130.255.77.28:443
2025/01/04 01:37:18 [Info] [1788824348] transport/internet/splithttp: XHTTP is dialing to udp:my.host.com:443, mode stream-up, HTTP version 3, host my.host.com
2025/01/04 01:37:18 [Debug] [1788824348] transport/internet: dialing to udp:my.host.com:443
2025/01/04 01:37:18 from tcp:127.0.0.1:65297 accepted tcp:130.255.77.28:443
2025/01/04 01:37:18 [Info] [1788824348] transport/internet/splithttp: failed to GET https://my.host.com/mypath/c6edd6a5-bf19-4a60-9c8a-681ea555b57e?x_padding=00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 > Get "https://my.host.com/mypath/c6edd6a5-bf19-4a60-9c8a-681ea555b57e?x_padding=00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": lookup my.host.com: no such host
2025/01/04 01:37:18 [Debug] [1788824348] transport/internet: dialing to udp:my.host.com:443
2025/01/04 01:37:18 [Info] [1788824348] transport/internet/splithttp: failed to POST https://my.host.com/mypath/c6edd6a5-bf19-4a60-9c8a-681ea555b57e?x_padding=00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 > Post "https://my.host.com/mypath/c6edd6a5-bf19-4a60-9c8a-681ea555b57e?x_padding=00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000": lookup my.host.com: no such host
2025/01/04 01:37:18 [Info] [1788824348] proxy/vless/outbound: tunneling request to tcp:130.255.77.28:443 via my.host.com:443
2025/01/04 01:37:19 [Info] [1788824348] app/proxyman/inbound: connection ends > proxy/socks: connection ends > context canceled
2025/01/04 01:37:22 [Debug] app/log: Logger closing