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