Волк ищет ip сервера? Передайте пирожок через рабочего

от автора

Проблема с мобильными устройствами в том, что они по умолчанию находятся в агрессивно-недружелюбной среде, и их подключения могут стать известны кому угодно. Как минимум они известны персоналу провайдеров wi-fi или LTE, а это может поставить под угрозу атаки сервер приложений (или входящий прокси этого сервера).

Хорошо если сервер можно спрятать за Cloudflare и организовать доступ через оптимальный «чистый» ip Cloudflare, без использования dns запросов. Маршрутизация при этом осуществляется самим cloudflare по SNI специально выделенному для него, и handshake остаётся открытым — чтобы никого не смущать.

В случае, когда этот вариант не подходит мы можем сделать аналогичный механизм самиж и как вы уже наверное догадались по картинке — я предлагаю вариант через мост с использованием websockets и с cloudflare worker.

Не думаю, что открою страшный секрет, но для SNI в этом случае подходит любой домен с dns cloudflare, в том числе например и бесплатные поддомены от DigitalPlat.

Входной websocket поток этот worker может перебрасывать уже и на сервер приложений с фиксированным ip адресом, тем-более, что Lets Encrypt теперь выдаёт сертификаты на ip, и шифрование не пострадает.

А чтобы ни у кого не возникло желания обидеть сервер приложений, узнав его адрес из исходящих запросов — для них при желании выделяется отдельный сервер с отдельным ip и они делаются через него.

Таким образом — никакой связи между исходящими соединениями мобильного устройства и запросами с сервера приложений, как и собственно ip сервера приложений, как мне кажется, установить невозможно, но остаётся открытым вопрос — а какие ещё системы кроме Cloudflare позволяют делать подобное на уровне pet-проектов бесплатно или почти бесплатно, и нет ли более изящного решения?

Для желающих повторить мой эксперимент — вот пример (прошу простить за сырость) кода worker с фильтрацией разрешенных адресов для подключения и dummy адресом сервера приложений — поменять не забудьте их пожалуйста.

import { connect } from 'cloudflare:sockets';export default {  async fetch(request, env, ctx) {    // 1. Извлекаем IP    const clientIP = request.headers.get("CF-Connecting-IP");    // 2. Проверяем const allowedIPv4 = "127.0.0.1";  // Поменять не забудьтеconst allowedIPv6Prefix = "fe80:"; // Обратите внимание на двоеточие в конце и поменять не забудьтеif (clientIP !== allowedIPv4 && !clientIP.startsWith(allowedIPv6Prefix)) {  return new Response(clientIP + " -> Вы кто такие?", { status: 403 });}    const upgradeHeader = request.headers.get('Upgrade');    if (!upgradeHeader || upgradeHeader !== 'websocket') {      return new Response('Bridge Active.', { status: 200 });    }    const vlConfig = {      address: '10.0.0.1',      port: 8080,    };  // не забудьте поменять адрес и порт на ваш    const webSocketPair = new WebSocketPair();    const [client, server] = Object.values(webSocketPair);    server.accept();    handleProxy(server, vlConfig);    return new Response(null, {      status: 101,      webSocket: client,    });  }};async function handleProxy(ws, config) {  let tcpSocket = null;  let writer = null;  ws.addEventListener('message', async (event) => {    try {      const data = event.data instanceof ArrayBuffer ? event.data : await new Response(event.data).arrayBuffer();      if (!tcpSocket) {        // Инициализация соединения        tcpSocket = connect({ hostname: config.address, port: config.port });        writer = tcpSocket.writable.getWriter();        // Запуск высокопроизводительного чтения        copyTcpToWs(tcpSocket.readable, ws);      }      await writer.write(new Uint8Array(data));    } catch (err) {      ws.close(1011);    }  });  ws.addEventListener('close', () => {    if (tcpSocket) tcpSocket.close();  });  ws.addEventListener('error', () => {    if (tcpSocket) tcpSocket.close();  });}/** * Оптимизированная передача данных из TCP в WebSocket  */async function copyTcpToWs(tcpReadable, ws) {  try {    // Используем встроенный механизм стримов для минимизации нагрузки на CPU    await tcpReadable.pipeTo(new WritableStream({      write(chunk) {        ws.send(chunk);      },      close() {        ws.close();      },      abort(reason) {        ws.close(1011);      }    }));  } catch (err) {    try { ws.close(); } catch (e) { }  }}

С мобильного устройства к worker можно подключиться например используя hiddify с вот таким конфигом

{ "outbounds": [  {   "type": "vless",   "tag": "Worker-Bridge",   "server": "yoursubdomain.dpdns.org",   "server_port": 443,   "uuid": "UUID4REALPLZ",   "tls": {    "enabled": true,    "server_name": "yoursubdomain.dpdns.org",    "alpn": "http/1.1"   },   "transport": {    "type": "ws",    "path": "/",    "headers": {     "Host": "yoursubdomain.dpdns.org"    },    "early_data_header_name": "Sec-WebSocket-Protocol"   },   "packet_encoding": "xudp"  } ], "endpoints": []

Трафик между worker и следующим сервером в цепочке — для уменьшения оверхеда и от бесстыдной лени автора статьи не шифруется, но его и читать некому, а после сервера зашифровать никто не мешает, уникальность подключения обеспечивает UUID.

ссылка на оригинал статьи https://habr.com/ru/articles/1033620/