Проблема с мобильными устройствами в том, что они по умолчанию находятся в агрессивно-недружелюбной среде, и их подключения могут стать известны кому угодно. Как минимум они известны персоналу провайдеров 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.

Комментарии (5)


  1. linux-over
    11.05.2026 08:20

    curl https://ifconfig.me

    с мобильника показывает случайный айпи CF?

    update: а, понял, таким образом можем избавиться от необходимости точки в России.

    правда, если начнут тарифицировать импортный траф, то всё равно придётся его делать


    1. 3aky Автор
      11.05.2026 08:20

      Да, в первую очередь для этого (один сервер вообще оставить можно, и через другой worker конфигурацию обновлять клиентом - чтобы людям SNI руками менять не приходилось "если-что")


    1. 3aky Автор
      11.05.2026 08:20

      правда, если начнут тарифицировать импортный траф, то всё равно придётся его делать

      в этом случае - после местной точки и перед далеким сервером поставить можно -чтобы никто не смог обоснованно к местной точке придраться, и ip сервера вычислить - особенно если исходящие всё-таки с .2 а не .1 оставить


  1. anyagixx
    11.05.2026 08:20

    Ничего не понятно. Для элиты видимо


    1. 3aky Автор
      11.05.2026 08:20

      художника каждый обидеть может (с)