Помощь с настройкой проксирования через CDN (Websocket / gRPC)

Всех приветствую! :sunglasses:
Решил овладеть методом “Websocket вместе с XTLS-Reality” по этой замечательной инструкции: Особенности проксирования через CDN/Websocket/gRPC для обхода блокировок.

Увы, что-то пошло не так и при выполнении теста Nekobox в журнале пишет: ошибка теста: Get “http://cp.cloudflare.com/”: websocket: bad handshake: HTTP 301 301 Moved Permanently.

Поиск в интернете ничего не дал :unamused:
Подскажите пожалуйста что может вызывать такую ошибку или в какую сторону “копать”?

xtls или reality не могут работать через cdn, особенности протоколов

Фраза переводится как “301 Перемещено перманентно/постоянно”.
Возможно, стоит откопать новый адрес, на который происходит перенаправление, и заменить им старый.

Зачем менять адрес, если на прямую, минуя CDN, всё исправно работает?

В статье это указано, если не читали, автор подключается к серверу без xtls или reality.

Так и напрямую может происходить перенаправление, которого вы не успеваете заметить.
Сравните изначальный адрес с конечным.

Я далёк от темы вопроса, не знаю, какой именно адрес перенаправляется --, может, это не тот, что вы ожидаете.

Конечно, я ожидал, что сразу будет всё работать :slightly_smiling_face:
Выходные адреса я не сравню, так как не работает интернет, пока не исправлю “websocket: bad handshake…”.
Т. е. вы ещё не настраивали таким методом, попробуйте, пригодится как-нибудь.

А вы настраивали таким способом, который описан в статье?

У вас перенаправление идёт на https://cp.cloudflare.com/.

нет, во первых там про вебсокет, а во вторых на этом домене нет перенаправления на https.
что хочет автор темы непонятно

В параметрах Cloudflare включено проксирование вебсокетов?

Если используете Nginx или Caddy, посмотрите ещё логи сервера в момент подключения. Либо попробуйте настроить вообще без веб-сервера, на Хабре пару месяцев назад статья была с этим вариантом (ее забаеил РКН, но VPN она всё ещё доступна)

Да, конечно, только у меня Gcore. Журнал теста в nekobox:

INFO[0000] dns: exchanged xxx.dns.com CNAME github.ddnsfree.com. 60 IN CNAME cl-gl5d09f09a.gcdn.co.
INFO[0000] dns: exchanged xxx.dns.com A cl-gl5d09f09a.gcdn.co. 60 IN A 81.28.12.12
INFO[0000] dns: exchanged xxx.dns.com CNAME github.ddnsfree.com. 60 IN CNAME cl-gl5d09f09a.gcdn.co.
INFO[0000] dns: exchanged xxx.dns.com AAAA cl-gl5d09f09a.gcdn.co. 60 IN AAAA 2a03:90c0:999c::12
[[VLESS] VLESS (SNI‑proxy over Websocket)] ошибка теста: Get "http://cp.cloudflare.com/": websocket: bad handshake: HTTP 301 301 Moved Permanently

Посмотрел, что-то не густо там:
/var/log/nginx/access.log вообще пустой.
/var/log/nginx/error.log видимо после перезагрузки пишет что-то:
2024/05/10 17:33:04 [notice] 649#649: signal 15 (SIGTERM) received from 1807, exiting
2024/05/10 17:33:04 [notice] 651#651: exiting
2024/05/10 17:33:04 [notice] 651#651: exit
2024/05/10 17:33:04 [notice] 649#649: signal 17 (SIGCHLD) received from 651
2024/05/10 17:33:04 [notice] 649#649: worker process 651 exited with code 0
2024/05/10 17:33:04 [notice] 649#649: exit
2024/05/10 17:33:32 [notice] 619#619: using the “epoll” event method
2024/05/10 17:33:32 [notice] 619#619: nginx/1.26.0
2024/05/10 17:33:32 [notice] 619#619: built by gcc 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.2)
2024/05/10 17:33:32 [notice] 619#619: OS: Linux 5.4.0-177-generic
2024/05/10 17:33:32 [notice] 619#619: getrlimit(RLIMIT_NOFILE): 1024:524288
2024/05/10 17:33:32 [notice] 631#631: start worker processes
2024/05/10 17:33:32 [notice] 631#631: start worker process 632

В процессе настройки было как-то, заходил через браузер и видел Ngix страницу, думаю это ничего не даст.

В настройках nekobox - url теста задержки прописан http://cp.cloudflare.com, так что всё нормально здесь.

После подключения клиента nekobox к серверу nginx, должна произойти переадресация на сервер x-ray, который пропустит во внешний инет. Вот содержимое файлов конфигурации nginx:
/etc/nginx/nginx.conf:

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;
   include /etc/nginx/conf.d/*.conf;
}

stream {
  map $ssl_preread_server_name $backend {
     XXXXXX.com               reality;
      www.XXXXXX.com             local;
      default                  reality;
  }

  upstream reality {
      server 127.0.0.1:8443;
  }

  upstream local {
      server 127.0.0.1:8444;
  }

  server {
      listen          443 reuseport so_keepalive=on;
      ssl_preread     on;
      proxy_pass      $backend;
  }
}

/etc/nginx/conf.d/default.conf:

server {
	listen 127.0.0.1:8444 so_keepalive=on;
        http2 on;

# ваш домен    
server_name www.XXXXXX.com;

    #access_log  /var/log/nginx/host.access.log  main;

    # сюда можно положить какие-нибудь странички фейкового сайта, или использовать proxy_pass чтобы переадресовать запрос на другой сервер
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    #error_page   500 502 503 504  /50x.html;
    #location = /50x.html {
    #    root   /usr/share/nginx/html;
    #}

# путь к сертификатам, самоподписанным либо от Cloudflare
	ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
	ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

client_header_timeout 52w;
    keepalive_timeout 52w;
# замените TestChatGRPC на какую-нибудь секретную строку
	location /TestChatGRPC {
		if ($content_type !~ "application/grpc") {
			return 404;
		}
		client_max_body_size 0;
		client_body_buffer_size 512k;
		grpc_set_header X-Real-IP $remote_addr;
		client_body_timeout 52w;
		grpc_read_timeout 52w;
		grpc_pass grpc://127.0.0.1:8888;
	}

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}

# аналогично замените TestChatWS на какую-нибудь другую секретную строку
    location /TestChatWS {
		if ($http_upgrade != "websocket") {
			return 404;
		}
		proxy_pass http://127.0.0.1:8889;
		proxy_redirect off;
	    proxy_http_version 1.1;
	    proxy_set_header Upgrade $http_upgrade;
	    proxy_set_header Connection "upgrade";
	    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_read_timeout 52w;
	}
}

Смотрите какая фигня. Судя по логам веб-сервера, до него ничего не долетает. НО - секция stream по умолчанию ничего не логгирует, а я подозреваю что дело где-то там. У вас стоит условие, что запросы с определенным SNI должны обрабатываться веб-сервером, а все остальное уходить на Reality, то есть на чужой маскировочный сайт.

Можно проверить, сходить curl -v на ваш адрес и URL, куда у вас пытается подключиться Nekoray с вебсокетами и посмотреть заголовки ответа. Если вы увидите там домен reality вместо вашего, то истина где-то рядом.

Нужно проверить параметры GCore, там вроде можно чтобы он изменял SNI при проксирование запроса, проверьте что там то же самое, что у вас в Nginx в условии.
Более тупой вариант (для теста, но можно и на постоянку) - сделать так, чтобы веб-сервер с вебсокетами слушал не на 127.0.0.1, а на 0.0.0.0 (задать какой-нибудь нестандартный порт повыше), а в GCore узадать этот порт в параметрах Origin (там можно так делать), чтобы он сразу шел туда напрямую, и посмотреть что получится

На адрес сайта я зашёл, вот:

 *   Trying XX.XX.12.12:443...
* Connected to ХХХХХddns.com (XX.XX.12.12) port 443
> GET / HTTP/1.1
> Host: ХХХХХddns.com:443
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Server: nginx
< Date: Sat, 11 May 2024 01:51:06 GMT
< Content-Type: text/html
< Content-Length: 248
< Connection: close
<
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx</center>
</body>
</html>
* Closing connection

С заходом по URL пришлось повозится, ссылка, которую выдаёт Nekobox, неполная, без нескольких параметров. Я немного по импровизировал с ней, добавил некоторые параметры, и что-то выдалось:

rdp-user@XXX:~$ vless://XXXX-XXXX-XXXX-XXXX-46e69149af25@ХХХХХddns.com:443?encryption=none&type=ws&path=TestChatWS &ed=2048&eh=Sec-Websocket-Protocol&security=tls&fp=chrome&packetEncoding=xudpс
[87] 3100
bash: vless://XXXX-XXXX-XXXX-XXXX-46e69149af25@ХХХХХddns.com:443?encryption=none: No such file or directory
[88] 3101
[89] 3102
[90] 3103
[91] 3104
[92] 3105
[93] 3106
[84]   Done                    eh=Sec-Websocket-Protocol
[85]   Done                    security=tls
[86]   Done                    fp=chrome
[87]   Exit 127                vless://XXXX-XXXX-XXXX-XXXX-46e69149af25@ХХХХХddns.com:443?encryption=none
[88]   Done                    type=ws
[89]   Done                    path=TestChatWS
[90]   Done                    ed=2048
[91]   Done                    eh=Sec-Websocket-Protocol

Скриншот окна конфига в nekobox прикрепляю:

В Gcore был установлен параметр динамическое имя хоста. С учётом того что автор реализовал sni через backend, может так и оставить?

Также, пока ковырялся с параметрами, подправил некоторые значения в конфиге nginx и стала выходить ошибка уже с другим номером: ошибка теста: Get “http://cp.cloudflare.com/”: websocket: bad handshake: HTTP 502 502 Bad Gateway.

Ну тут ошибка говорит сама за себя. Проверяйте, что у вас там в параметрах Origin на стороне gCore стоит, должен быть HTTPS, а не HTTP.

Под URL я имел в виду host + path (с https://).

Host пропишите правильный тоже.

Он должен совпадать с тем что указано в конфиге Nginx, иначе не будет работать правило stream.

Какие именно?

На скриншоте не видно, т.к. замазано, поэтому хочу упомянуть грабли, на которые довелось наступить мне. В той статье, на которую ты опираешься, Path указан что-то вроде /mypath, у меня так заработало, только если early data length ставить 0. Если early data хочется, то нужно уазывать, например, и early data length 2048 и в пути прописывать /mypath?ed=2048

Извиняюсь, я не прописал https вначале сайта :frowning:
Вот правильный вариант:

C:\Windows\System32> curl -v https://ХХХХХddns.com:443
*   Trying XX.XX.12.12:443...
* Connected to ХХХХХddns.com (XX.XX.12.12) port 443
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* schannel: next InitializeSecurityContext failed: SEC_E_ILLEGAL_MESSAGE (0x80090326) - This error usually occurs when a fatal SSL/TLS alert is received (e.g. handshake failed). More detail may be available in the Windows System event log.
* Closing connection
* schannel: shutting down SSL/TLS connection with ХХХХХddns.com port 443
curl: (35) schannel: next InitializeSecurityContext failed: SEC_E_ILLEGAL_MESSAGE (0x80090326) - This error usually occurs when a fatal SSL/TLS alert is received (e.g. handshake failed). More detail may be available in the Windows System event log.

Протокол HTTPS всегда включён был:

Такого варианта достаточно будет: scheme://username:password@host:port/path?query#fragment ?

Кстати, у автора тоже не прописан host на скриншоте и ведь строка адрес = строке Host. Хорошо, себе пропишу.

Я только за, в конфиге Nginx указан реалити сайт и мой домен, а также параметр $backend, какой из них прописать?

В stream был прописан вместо моего домена реалити сайт.
В разделе server_name был прописан не мой домен а реалити сайт.

Я тоже обратил внимание, но странно, у автора статьи работало с 2048, хоть и времени немного прошло. Я так понимаю, что наклонная черта в начале пароля обязательна, о чём умолчали в статье. Но тогда давайте “сверим часы”, этот пароль упоминается 3 раза и конечно же в строке перед патчем стоит другая наклонная черта, которая, непонятно, влияет или нет на него:
Например, мой патч будет такой: /12345 и я его подставляю в строчки кода:

/etc/nginx/conf.d/default.conf
location //12345 {

/opt/xray/config.json
“path”: “/12345

Окно конфигурации Nekobox:
path: /12345

Нормально всё? :see_no_evil:

Да нет же, просто https://domain/path. Больше ничего, вообще ничего. Просто как обычный URL в
браузере.

Ваш домен. То, что совпадает с ним, Nginx будет отправлять на веб-сайт с вебсокетами, все остальное - на reality-обманку. Поэтому в SNI должен обязательно быть ваш домен.

Слеш должен быть только один

Так, давайте проще. Добейтесь сначала, чтобы у вас подключение к прокси через вебсокеты хотя бы заработало без GCore, а уже потом добавляйте GCore. В Nekobox в адресе сервера задайте не ваш домен, а IP-адрес сервера, а домен пусть будет в Host. Пробуйте подключиться и смотрите логи Nginx. Когда заработает так - будем разбираться с GCore (если в Nginx сертификат самоподписанный, не забудьте галочку allow insecure)

Но судя по тому, что у вас сейчас по вашему домену открывается гитхаб вместо сайта Nginx, проблема где-то именно с SNI. Попробуйте в Nekobox явно заполнить поле SNI вашим доменом, кстати.

А так, в целом, инструкция по которой вы настраиваете, реально переусложненная, и в ней слишком много вещей где что-то можно сделать не так. Варианты с IPv6 (для GCore не подойдёт) или с альтернативным портом (а вот этот очень даже подойдёт) описанные в других статьях, гораздо проще и надёжнее.

Ну это не совсем пароль, это URL путь, хотя в данном случае выполняет в некотором роде роль пароля тоже, т.к. не зная его на ws попасть не получится.
И того, если путь будет 12345, то в:
/etc/nginx/conf.d/default.conf
location /12345 {

/opt/xray/config.json
“path”: “12345

Окно конфигурации Nekobox:
Path* /12345?ed=2048
EarlyData Length 2048
EarlyData Name Sec-Websocket-Protocol

Хорошо, вот:

C:\Windows\System32>curl -v https://XXXXX.com/XXXXX
*   Trying XX.XX.12.12:443...
* Connected to XXXXX.com (XX.XX.12.12) port 443
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* schannel: next InitializeSecurityContext failed: SEC_E_ILLEGAL_MESSAGE (0x80090326) - This error usually occurs when a fatal SSL/TLS alert is received (e.g. handshake failed). More detail may be available in the Windows System event log.
* Closing connection
* schannel: shutting down SSL/TLS connection with github.ddnsfree.com port 443
curl: (35) schannel: next InitializeSecurityContext failed: SEC_E_ILLEGAL_MESSAGE (0x80090326) - This error usually occurs when a fatal SSL/TLS alert is received (e.g. handshake failed). More detail may be available in the Windows System event log.

Ок, давай попробуем напрямки, какие ещё настройки поменять в nekobox?

При таких настройках: ошибка теста: Get “http://cp.cloudflare.com/”: tls: first record does not look like a TLS handshake.

По сути, добавить в конфиги Nginx несколько строчек :slight_smile:

Отказался в пароле от слэша, сделал как у вас в примере, спасибо.

Интересно было бы все же удостовериться что проксируется что надо и куда надо.
Предлагаю добавить логирование в секцию стрима

log_format custom_stream_log 'proxy: $status $protocol remote_addr $remote_addr, sent_to $upstream_addr';
  server {
      listen          443 reuseport so_keepalive=on;
      access_log      syslog:server=unix:/dev/log custom_stream_log;
      proxy_pass      $backend;
  }

И посмотреть, что запрос улетел на слушателя сайта
Логи будут в системном журнале.