commit df6d3111314642750e39ffa26d142a93be389b99 Author: Denis Vasilev <89912505+vasiljevdenis@users.noreply.github.com> Date: Sat Dec 20 22:15:57 2025 +0300 beta release v1.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..fec42b0 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +[English](https://github.com/denpiligrim/test_3dp-manager/blob/main/README_EN.md) + +# 3DP-MANAGER + +> [!WARNING] +> **Это бета-версия!** +> +> Программа находится в активной разработке. Возможны баги, нестабильность и изменения API. +> Используйте с осторожностью. + +3DP-MANAGER — утилита, которая позволяет регулярно генерировать входящие подключения для панели 3X-UI на основе списка разрешённых доменов (whitelist). Белый список общий, но вы также можете добавить свой собственный список, назвав его `my_whitelist.txt` и поместив его в папку `/opt/3dp-manager/app` на вашем сервере. +Обсуждения доступны в Телеграм канале: [@denpiligrim_web](https://t.me/denpiligrim_web/719) + +### Установка +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main/install.sh) +``` + +### Обновление +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main/update.sh) +``` + +### Удаление +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main/delete.sh) +``` + +--- + +Сервис подписок и перенаправления входящего трафика работает совместно с **3DP-MANAGER** и позволяет перенаправлять весь входящий трафик с промежуточного сервера на основной сервер. Перенаправляются те же порты: `443`, `8443` и диапазон `10000-60000`. Сервис также создаёт ссылку на подписку, автоматически заменяя IP-адрес или домен в конфигурациях. Перенаправления настраиваются добавлением правил iptables в конфигурационный файл ufw, что обеспечивает стабильную работу в сочетании с файрволом. Рекомендуется устанавливать сервис на чистый сервер без ранее настроенных правил. + +### Установка перенаправления +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main/forwarding_install.sh) +``` + +### Удаление перенаправления +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main/forwarding_delete.sh) +``` + +--- + +### Показать URL подписки +```bash +cd /opt/3dp-manager && docker compose exec node env | grep SUB_URL | cut -d'=' -f2 +``` + +--- + +### Утилита для сбора доменов из мульти-подписок +Позволяет собирать домены из подписок с несколькими конфигурациями. В интернете встречается много подобных списков конфигураций, использующих белый SNI. Инструмент позволяет извлечь домены и подготовить готовый список для дальнейшего использования в генераторе входящих подключений. +Вставьте ссылку на подписку в скрипт и запустите команду в окружении `Node.js`. +```bash +node get_domains.js +``` + +### Использовать свой белый список +Файл должен иметь структуру аналогичную `whitelist.txt`. Переименуйте имя файла на `my_whitelist.txt`. Загрузите файл в папку `/opt/3dp-manager/app` на сервере и выполните команду: +```bash +cd /opt/3dp-manager && docker cp ./app/my_whitelist.txt node:/app/my_whitelist.txt +``` \ No newline at end of file diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..df086cd --- /dev/null +++ b/README_EN.md @@ -0,0 +1,62 @@ +[Русский](https://github.com/denpiligrim/test_3dp-manager/blob/main/README.md) + +# 3DP-MANAGER + +> [!WARNING] +> **This is a beta version!** +> +> The program is currently under active development. Bugs, instability, and API changes are possible. +> Use with caution. + +3DP-MANAGER is a utility that allows you to regularly generate inbound connections for the 3X-UI panel based on a whitelist of domains. The whitelist is shared, but you can also add your own list by naming it `my_whitelist.txt` and adding it to the `/opt/3dp-manager/app` folder on your server. +Discussions are available on the Telegram channel: [@denpiligrim_web](https://t.me/denpiligrim_web/719) + +### Install +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main/install.sh) +``` + +### Update +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main/update.sh) +``` + +### Delete +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main/delete.sh) +``` + +--- + +The subscription and inbound redirection service works in conjunction with **3DP-MANAGER** and allows you to redirect all inbound traffic from the intermediate server to the main server. The same ports are redirected, namely `443`, `8443`, and the range `10000-60000`. The service also creates a link to the subscription, automatically replacing the IP address or domain in the configurations. Redirects are configured by adding iptables rules to the ufw configuration file, which ensures stable operation in conjunction with the firewall. It is recommended to install the service on a clean server without any previously installed rules. +### Install forwarding +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main/forwarding_install.sh) +``` + +### Delete forwarding +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main/forwarding_delete.sh) +``` + +--- + +### Show sub URL +```bash +cd /opt/3dp-manager && docker compose exec node env | grep SUB_URL | cut -d'=' -f2 +``` + +--- + +### Utility for collecting domains from multi-subscriptions +Allows you to collect domains from subscriptions with multiple configurations. There are many similar configuration lists circulating on the internet that use white SNI. The tool allows you to extract domains and prepare a ready-made list for further use in the inbound generator. +Insert a subscription link into the script and run the command in the `Node.js` environment. +```bash +node get_domains.js +``` + +### Use your whitelist +The file should have a structure similar to `whitelist.txt`. Rename the file to `my_whitelist.txt`. Upload the file to the `/opt/3dp-manager/app` folder on the server and run the command: +```bash +cd /opt/3dp-manager && docker cp ./app/my_whitelist.txt node:/app/my_whitelist.txt +``` \ No newline at end of file diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 0000000..caf9611 --- /dev/null +++ b/app/.env.example @@ -0,0 +1,9 @@ +SUB_TOKEN=$SUB_TOKEN +UI_URL=$UI_URL +UI_LOGIN=$UI_LOGIN +UI_PASSWORD=$UI_PASSWORD +NGINX_PORT=$NGINX_PORT +UI_HOST=$UI_HOST +UI_PROTO=$UI_PROTO +ROTATE_INTERVAL=$ROTATE_INTERVAL +SUB_URL=$SUB_URL \ No newline at end of file diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..4a63d4e --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,11 @@ +FROM node:20-alpine + +WORKDIR /app +COPY package.json ./ + +RUN npm install --production + +COPY index.js . +COPY builders ./builders + +CMD ["node", "index.js"] \ No newline at end of file diff --git a/app/builders/buildInboundLink.js b/app/builders/buildInboundLink.js new file mode 100644 index 0000000..ad5d620 --- /dev/null +++ b/app/builders/buildInboundLink.js @@ -0,0 +1,160 @@ +export function buildInboundLink(inbound, domain, idOrPass) { + let link = ""; + + switch (inbound.protocol) { + case "vless": { + link = buildVlessLink(inbound, domain, idOrPass); + break; + } + + case "vmess": { + link = buildVmessLink(inbound, domain, idOrPass); + break; + } + + case "shadowsocks": + link = buildSsLink(inbound, domain, idOrPass); + break; + + case "trojan": + link = buildTrojanLink(inbound, domain, idOrPass); + break; + } + + return link; +} + +function buildVlessLink(inbound, domain, uuid) { + const stream = JSON.parse(inbound.streamSettings); + const settings = JSON.parse(inbound.settings); + + const network = stream.network; + const security = stream.security || "none"; + + const params = new URLSearchParams(); + + // Базовые параметры + params.set("type", network); + params.set("encryption", "none"); + params.set("security", security); + + /** + * ===== REALITY ===== + */ + if (security === "reality") { + const r = stream.realitySettings; + + params.set("pbk", r.settings.publicKey); + params.set("fp", r.settings.fingerprint || "random"); + params.set("sni", r.serverNames?.[0] || ""); + params.set("sid", r.shortIds?.[0] || ""); + params.set("spx", '%2F'); + + // TCP Reality flow + if (network === "tcp") { + const client = settings.clients?.[0]; + if (client?.flow) { + params.set("flow", client.flow); + } + } + + // XHTTP Reality + if (network === "xhttp") { + const x = stream.xhttpSettings || {}; + params.set("path", x.path || "/"); + params.set("host", x.host || r.serverNames?.[0]); + params.set("mode", x.mode || "auto"); + } + + // gRPC Reality + if (network === "grpc") { + const g = stream.grpcSettings || {}; + params.set("serviceName", g.serviceName || "grpc"); + params.set("authority", g.authority || r.serverNames?.[0]); + } + } + + /** + * ===== WS NONE ===== + */ + if (network === "ws") { + const ws = stream.wsSettings || {}; + params.set("path", ws.path || "/"); + if (ws.headers?.Host) { + params.set("host", ws.headers.Host); + } + } + + return ( + `vless://${uuid}@${domain}:${inbound.port}` + + `?${params.toString()}` + + `#${encodeURIComponent(inbound.remark)}` + ); +} + +function buildVmessLink(inbound, domain, uuid) { + const stream = JSON.parse(inbound.streamSettings); + + const vmessObj = { + add: domain, + aid: '', + alpn: "", + fp: "", + host: "", + id: uuid, + net: stream.network || "tcp", + path: "/", + port: inbound.port, + ps: inbound.remark, + scy: "", + sni: "", + tls: stream.security || "none", + type: "none", + v: "2" + }; + + const base64 = Buffer + .from(JSON.stringify(vmessObj), "utf8") + .toString("base64"); + + return `vmess://${base64}`; +} + +function buildSsLink(inbound, domain) { + const settings = JSON.parse(inbound.settings); + + const method = settings.method; + const serverPassword = settings.password; + const clientPassword = settings.clients[0].password; + + // method:serverPassword:clientPassword + const userInfo = `${method}:${serverPassword}:${clientPassword}`; + + const base64 = Buffer + .from(userInfo, "utf8") + .toString("base64"); + + return `ss://${base64}@${domain}:${inbound.port}?type=tcp#${inbound.remark}`; +} + +function buildTrojanLink(inbound, domain, password) { + const stream = JSON.parse(inbound.streamSettings); + const reality = stream.realitySettings; + + const pbk = reality.settings.publicKey; + const sni = reality.serverNames?.[0] || domain; + const sid = reality.shortIds?.[0] || ""; + const spx = '%2F'; + + return ( + `trojan://${password}@${domain}:${inbound.port}` + + `?type=tcp` + + `&security=reality` + + `&pbk=${pbk}` + + `&fp=random` + + `&sni=${sni}` + + `&sid=${sid}` + + `&spx=${spx}` + + `#${inbound.remark}-${password}` + ); +} \ No newline at end of file diff --git a/app/builders/buildShadowsocksTcp.js b/app/builders/buildShadowsocksTcp.js new file mode 100644 index 0000000..cf0fd16 --- /dev/null +++ b/app/builders/buildShadowsocksTcp.js @@ -0,0 +1,43 @@ +import crypto from "crypto"; + +export function buildShadowsocksTcp({ port, uuid }) { + return { + enable: true, + port, + protocol: "shadowsocks", + remark: "shadowsocks-tcp", + settings: JSON.stringify({ + clients: [{ + id: "", + flow: "", + email: uuid, + password: crypto.randomBytes(32).toString("base64"), + enable: true, + limitIp: 0, + totalGB: 0, + expiryTime: 0, + tgId: "", + subId: "", + reset: 0 + }], + ivCheck: false, + method: "2022-blake3-aes-256-gcm", + network: "tcp", + password: crypto.randomBytes(32).toString("base64") + }), + streamSettings: JSON.stringify({ + network: "tcp", + security: "none", + tcpSettings: { + acceptProxyProtocol: false, + header: { type: "none" } + } + }), + sniffing: JSON.stringify({ + enabled: false, + destOverride: ["http", "tls", "quic", "fakedns"], + metadataOnly: false, + routeOnly: false + }) + }; +} \ No newline at end of file diff --git a/app/builders/buildTrojanRealityTcp.js b/app/builders/buildTrojanRealityTcp.js new file mode 100644 index 0000000..0c74d5d --- /dev/null +++ b/app/builders/buildTrojanRealityTcp.js @@ -0,0 +1,65 @@ +import crypto from "crypto"; + +export function buildTrojanRealityTcp({ port, uuid, domain, keys }) { + return { + enable: true, + port, + protocol: "trojan", + remark: "trojan-reality-tcp", + settings: JSON.stringify({ + clients: [{ + id: uuid, + email: uuid, + password: crypto.randomBytes(8).toString("hex"), + enable: true, + flow: "", + limitIp: 0, + totalGB: 0, + expiryTime: 0, + tgId: "", + subId: "", + reset: 0 + }], + fallbacks: [] + }), + streamSettings: JSON.stringify({ + network: "tcp", + security: "reality", + externalProxy: [], + realitySettings: { + show: false, + xver: 0, + target: `${domain}:443`, + dest: `${domain}:443`, + serverNames: [domain], + privateKey: keys.privateKey, + shortIds: [ + crypto.randomBytes(4).toString("hex"), + crypto.randomBytes(3).toString("hex"), + crypto.randomBytes(8).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(4).toString("hex") + ], + settings: { + publicKey: keys.publicKey, + fingerprint: "random", + serverName: "", + spiderX: "/" + } + }, + tcpSettings: { + acceptProxyProtocol: false, + header: { type: "none" } + } + }), + sniffing: JSON.stringify({ + enabled: false, + destOverride: ["http", "tls", "quic", "fakedns"], + metadataOnly: false, + routeOnly: false + }) + }; +} \ No newline at end of file diff --git a/app/builders/buildVlessRealityGrpc.js b/app/builders/buildVlessRealityGrpc.js new file mode 100644 index 0000000..9faa640 --- /dev/null +++ b/app/builders/buildVlessRealityGrpc.js @@ -0,0 +1,67 @@ +import crypto from "crypto"; + +export function buildVlessRealityGrpc({ port, uuid, domain, keys }) { + return { + enable: true, + port, + protocol: "vless", + remark: "vless-reality-grpc", + settings: JSON.stringify({ + clients: [{ + id: uuid, + email: uuid, + enable: true, + flow: "", + limitIp: 0, + totalGB: 0, + expiryTime: 0, + tgId: "", + subId: "", + reset: 0 + }], + decryption: "none", + encryption: "none", + fallbacks: [] + }), + streamSettings: JSON.stringify({ + network: "grpc", + security: "reality", + externalProxy: [], + realitySettings: { + show: false, + xver: 0, + target: `${domain}:443`, + dest: `${domain}:443`, + serverNames: [domain], + privateKey: keys.privateKey, + shortIds: [ + crypto.randomBytes(4).toString("hex"), + crypto.randomBytes(3).toString("hex"), + crypto.randomBytes(8).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(4).toString("hex") + ], + settings: { + publicKey: keys.publicKey, + fingerprint: "random", + serverName: "", + spiderX: "/" + } + }, + grpcSettings: { + serviceName: "myservice", + authority: domain, + multiMode: false, + } + }), + sniffing: JSON.stringify({ + enabled: false, + destOverride: ["http", "tls", "quic", "fakedns"], + metadataOnly: false, + routeOnly: false + }) + }; +} \ No newline at end of file diff --git a/app/builders/buildVlessRealityTcp.js b/app/builders/buildVlessRealityTcp.js new file mode 100644 index 0000000..a6e9f3c --- /dev/null +++ b/app/builders/buildVlessRealityTcp.js @@ -0,0 +1,66 @@ +import crypto from "crypto"; + +export function buildVlessRealityTcp({ port, uuid, domain, keys }) { + return { + enable: true, + port, + protocol: "vless", + remark: "vless-reality-tcp", + settings: JSON.stringify({ + clients: [{ + id: uuid, + flow: "xtls-rprx-vision", + email: uuid, + enable: true, + limitIp: 0, + totalGB: 0, + expiryTime: 0, + tgId: "", + subId: "", + reset: 0 + }], + decryption: "none", + encryption: "none", + fallbacks: [] + }), + streamSettings: JSON.stringify({ + network: "tcp", + security: "reality", + externalProxy: [], + realitySettings: { + show: false, + xver: 0, + target: `${domain}:443`, + dest: `${domain}:443`, + serverNames: [domain], + privateKey: keys.privateKey, + shortIds: [ + crypto.randomBytes(4).toString("hex"), + crypto.randomBytes(3).toString("hex"), + crypto.randomBytes(8).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(4).toString("hex") + ], + settings: { + publicKey: keys.publicKey, + fingerprint: "random", + serverName: "", + spiderX: "/" + } + }, + tcpSettings: { + acceptProxyProtocol: false, + header: { type: "none" } + } + }), + sniffing: JSON.stringify({ + enabled: false, + destOverride: ["http", "tls", "quic", "fakedns"], + metadataOnly: false, + routeOnly: false + }) + }; +} \ No newline at end of file diff --git a/app/builders/buildVlessRealityXhttp.js b/app/builders/buildVlessRealityXhttp.js new file mode 100644 index 0000000..4dae865 --- /dev/null +++ b/app/builders/buildVlessRealityXhttp.js @@ -0,0 +1,72 @@ +import crypto from "crypto"; + +export function buildVlessRealityXhttp({ port, uuid, domain, keys }) { + return { + enable: true, + port, + protocol: "vless", + remark: "vless-reality-xhttp", + settings: JSON.stringify({ + clients: [{ + id: uuid, + email: uuid, + enable: true, + flow: "", + limitIp: 0, + totalGB: 0, + expiryTime: 0, + tgId: "", + subId: "", + reset: 0 + }], + decryption: "none", + encryption: "none", + fallbacks: [] + }), + streamSettings: JSON.stringify({ + network: "xhttp", + security: "reality", + externalProxy: [], + realitySettings: { + show: false, + xver: 0, + target: `${domain}:443`, + dest: `${domain}:443`, + serverNames: [domain], + privateKey: keys.privateKey, + shortIds: [ + crypto.randomBytes(4).toString("hex"), + crypto.randomBytes(3).toString("hex"), + crypto.randomBytes(8).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(2).toString("hex"), + crypto.randomBytes(4).toString("hex") + ], + settings: { + publicKey: keys.publicKey, + fingerprint: "random", + serverName: "", + spiderX: "/" + } + }, + xhttpSettings: { + host: domain, + path: "/", + mode: "auto", + noSSEHeader: false, + scMaxBufferedPosts: 30, + scMaxEachPostBytes: "1000000", + scStreamUpServerSecs: "20-80", + xPaddingBytes: "100-1000" + } + }), + sniffing: JSON.stringify({ + enabled: false, + destOverride: ["http", "tls", "quic", "fakedns"], + metadataOnly: false, + routeOnly: false + }) + }; +} \ No newline at end of file diff --git a/app/builders/buildVlessWs.js b/app/builders/buildVlessWs.js new file mode 100644 index 0000000..2886130 --- /dev/null +++ b/app/builders/buildVlessWs.js @@ -0,0 +1,44 @@ +import crypto from "crypto"; + +export function buildVlessWs({ port, uuid, domain }) { + return { + enable: true, + port, + protocol: "vless", + remark: "vless-reality-ws", + settings: JSON.stringify({ + clients: [{ + id: uuid, + email: uuid, + enable: true, + flow: "", + limitIp: 0, + totalGB: 0, + expiryTime: 0, + tgId: "", + subId: "", + reset: 0 + }], + decryption: "none", + encryption: "none", + fallbacks: [] + }), + streamSettings: JSON.stringify({ + network: "ws", + security: "none", + externalProxy: [], + wsSettings: { + host: domain, + path: "/", + acceptProxyProtocol: false, + heartbeatPeriod: 0, + } + }), + sniffing: JSON.stringify({ + enabled: false, + destOverride: ["http", "tls", "quic", "fakedns"], + metadataOnly: false, + routeOnly: false + }) + }; +} \ No newline at end of file diff --git a/app/builders/buildVmessTcp.js b/app/builders/buildVmessTcp.js new file mode 100644 index 0000000..0b71e8d --- /dev/null +++ b/app/builders/buildVmessTcp.js @@ -0,0 +1,36 @@ +export function buildVmessTcp({ port, uuid }) { + return { + enable: true, + port, + protocol: "vmess", + remark: "vmess-tcp", + settings: JSON.stringify({ + clients: [{ + id: uuid, + flow: "", + email: uuid, + enable: true, + limitIp: 0, + totalGB: 0, + expiryTime: 0, + tgId: "", + subId: "", + reset: 0 + }], + }), + streamSettings: JSON.stringify({ + network: "tcp", + security: "none", + tcpSettings: { + acceptProxyProtocol: false, + header: { type: "none" } + } + }), + sniffing: JSON.stringify({ + enabled: false, + destOverride: ["http", "tls", "quic", "fakedns"], + metadataOnly: false, + routeOnly: false + }) + }; +} \ No newline at end of file diff --git a/app/docker-compose.example.yml b/app/docker-compose.example.yml new file mode 100644 index 0000000..f33bc59 --- /dev/null +++ b/app/docker-compose.example.yml @@ -0,0 +1,17 @@ +services: + node: + build: ./app + env_file: .env + volumes: + - ./subscriptions:/subscriptions + - ./whitelist.txt:/app/whitelist.txt:ro + restart: unless-stopped + + nginx: + image: nginx:alpine + depends_on: [node] + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./subscriptions:/subscriptions \ No newline at end of file diff --git a/app/index.js b/app/index.js new file mode 100644 index 0000000..3688ec1 --- /dev/null +++ b/app/index.js @@ -0,0 +1,189 @@ +import axios from "axios"; +import cron from "node-cron"; +import fs from "fs"; +import net from "net"; +import { buildVlessRealityTcp } from "./builders/buildVlessRealityTcp.js"; +import { buildVlessRealityXhttp } from "./builders/buildVlessRealityXhttp.js"; +import { buildTrojanRealityTcp } from "./builders/buildTrojanRealityTcp.js"; +import { buildShadowsocksTcp } from "./builders/buildShadowsocksTcp.js"; +import { buildVmessTcp } from "./builders/buildVmessTcp.js"; +import { buildVlessRealityGrpc } from "./builders/buildVlessRealityGrpc.js"; +import { buildVlessWs } from "./builders/buildVlessWs.js"; +import { buildInboundLink } from "./builders/buildInboundLink.js"; + +const { UI_URL, UI_LOGIN, UI_PASSWORD, UI_HOST } = process.env; +const SUB_FILE = "/subscriptions/list.txt"; + +const cookieJar = {}; +const api = axios.create({ baseURL: UI_URL, timeout: 15000, withCredentials: true }); + +api.interceptors.request.use(config => { + if (cookieJar.value) config.headers['Cookie'] = cookieJar.value; + return config; +}); + +const WHITELIST_FILE = "/app/whitelist.txt"; +const WHITELIST_REPO_URL = "https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main/whitelist.txt"; + +// Вспомогательные функции +async function updateWhitelist() { + try { + const res = await axios.get(WHITELIST_REPO_URL, { timeout: 10000 }); + fs.writeFileSync(WHITELIST_FILE, res.data, "utf8"); + console.log("✔ whitelist.txt обновлен из репозитория"); + } catch (err) { + console.warn("⚠ Не удалось обновить whitelist.txt, будет использоваться локальный файл:", err.message); + } +} + +function loadWhitelist() { + let fileToUse = WHITELIST_FILE; + + if (fs.existsSync("/app/my_whitelist.txt")) { + console.log('Найден кастомный whitelist: /app/my_whitelist.txt. Используется он.'); + fileToUse = "/app/my_whitelist.txt"; + } else { + console.log(`Кастомный whitelist не найден. Используется дефолтный: ${WHITELIST_FILE}`); + if (!fs.existsSync(WHITELIST_FILE)) { + throw new Error("Дефолтный whitelist.txt не найден"); + } + } + + return fs.readFileSync(fileToUse, "utf8") + .split("\n") + .map(v => v.trim()) + .filter(Boolean); +} + +function pickDomain(list) { + return list[Math.floor(Math.random() * list.length)]; +} + +async function isPortFree(port) { + return new Promise(resolve => { + const s = net.createServer() + .once("error", () => resolve(false)) + .once("listening", () => { + s.close(); + resolve(true); + }) + .listen(port, "0.0.0.0"); + }); +} + +async function getFreePort(used) { + while (true) { + const p = Math.floor(Math.random() * (60000 - 10000)) + 10000; + if (used.has(p)) continue; + if (await isPortFree(p)) return p; + } +} + +async function generateRealityKeys() { + try { + const res = await api.get("/panel/api/server/getNewX25519Cert"); + if (res.data?.success && res.data?.obj) { + return { + privateKey: res.data.obj.privateKey, + publicKey: res.data.obj.publicKey + }; + } + throw new Error("Invalid key response"); + } catch (e) { + console.error("Failed to get Reality keys:", e.message); + throw e; + } +} + +async function uuid() { + try { + const res = await api.get("/panel/api/server/getNewUUID"); + if (res.data?.success && res.data?.obj?.uuid) { + return res.data.obj.uuid; + } + throw new Error("Invalid UUID response"); + } catch (e) { + console.error("Failed to get UUID:", e.message); + throw e; + } +} + +// API 3x-ui +async function login() { + try { + const res = await api.post("/login", { username: UI_LOGIN, password: UI_PASSWORD }); + if (res.headers['set-cookie']) cookieJar.value = res.headers['set-cookie'].join('; '); + console.log("Login success"); + } catch (e) { console.error("Login failed:", e.message); throw e; } +} + +async function getInbounds() { + try { + const res = await api.get("/panel/api/inbounds/list"); + return res.status === 200 ? res.data.obj || [] : []; + } catch (e) { console.error("Get inbounds failed:", e.message); return []; } +} + +async function deleteInbound(id) { + try { await api.post(`/panel/api/inbounds/del/${id}`); } catch { } +} + +async function addInbound(config) { + try { + const res = await api.post("/panel/api/inbounds/add", config); + return res.data?.id || null; + } catch (e) { + console.error("Add inbound failed:", e.message); + return null; + } +} + +// Основная ротация inbound'ов +async function rotate() { + await login(); + + await updateWhitelist(); + const whitelist = loadWhitelist(); + const usedPorts = new Set(); + const subs = []; + + const existing = await getInbounds(); + for (const i of existing) await deleteInbound(i.id); + + const builders = [ + async (d) => buildVlessRealityTcp({ port: await isPortFree(8443) ? 8443 : await getFreePort(usedPorts), uuid: await uuid(), domain: d, keys: await generateRealityKeys() }), + async (d) => buildVlessRealityXhttp({ port: await isPortFree(443) ? 443 : await getFreePort(usedPorts), uuid: await uuid(), domain: d, keys: await generateRealityKeys() }), + async (d) => buildVlessRealityGrpc({ port: await getFreePort(usedPorts), uuid: await uuid(), domain: d, keys: await generateRealityKeys() }), + async (d) => buildVlessWs({ port: await getFreePort(usedPorts), uuid: await uuid(), domain: d }), + async (d) => buildVlessRealityTcp({ port: await getFreePort(usedPorts), uuid: await uuid(), domain: d, keys: await generateRealityKeys() }), + async (d) => buildVlessRealityTcp({ port: await getFreePort(usedPorts), uuid: await uuid(), domain: d, keys: await generateRealityKeys() }), + async (d) => buildVlessRealityTcp({ port: await getFreePort(usedPorts), uuid: await uuid(), domain: d, keys: await generateRealityKeys() }), + async (d) => buildVmessTcp({ port: await getFreePort(usedPorts), uuid: await uuid() }), + async (d) => buildShadowsocksTcp({ port: await getFreePort(usedPorts), uuid: await uuid() }), + async (d) => buildTrojanRealityTcp({ port: await getFreePort(usedPorts), uuid: await uuid(), domain: d, keys: await generateRealityKeys() }), + ]; + + for (const b of builders) { + const domain = pickDomain(whitelist); + const inbound = await b(domain); + usedPorts.add(inbound.port); + const idOrPass = inbound.settings ? JSON.parse(inbound.settings).clients?.[0]?.id || JSON.parse(inbound.settings).clients?.[0]?.password : ""; + + // Формируем ссылку в зависимости от протокола + const link = buildInboundLink(inbound, UI_HOST, idOrPass); + if (link) subs.push(link); + await addInbound(inbound); + } + + fs.writeFileSync(SUB_FILE, subs.join("\n") + "\n", "utf8"); + console.log("✔ 10 inbound created, подписка обновлена"); +} + +// Первый запуск +rotate(); + +let interval = parseInt(process.env.ROTATE_INTERVAL) || 30; +if (interval < 10) interval = 10; + +const cronExpression = `*/${interval} * * * *`; +cron.schedule(cronExpression, rotate); \ No newline at end of file diff --git a/app/nginx.example.conf b/app/nginx.example.conf new file mode 100644 index 0000000..fcdc8a1 --- /dev/null +++ b/app/nginx.example.conf @@ -0,0 +1,16 @@ +events {} +http { + server { + listen $NGINX_PORT; + server_name $UI_HOST; + + location = /bus/$SUB_TOKEN { + alias /subscriptions/list.txt; + default_type text/plain; + } + + location / { + return 404; + } + } +} \ No newline at end of file diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..a14da41 --- /dev/null +++ b/app/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "dependencies": { + "axios": "^1.13.2", + "node-cron": "^4.2.1" + } +} \ No newline at end of file diff --git a/delete.sh b/delete.sh new file mode 100644 index 0000000..14ab825 --- /dev/null +++ b/delete.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +################################# +# TRAP +################################# +trap 'echo -e "\033[1;31m[ERROR]\033[0m Ошибка в строке $LINENO"; exit 1' ERR + +################################# +# HELPERS +################################# + +log() { echo -e "\033[1;32m[INFO]\033[0m $1"; } +warn() { echo -e "\033[1;33m[WARN]\033[0m $1"; } +die() { echo -e "\033[1;31m[ERROR]\033[0m $1"; exit 1; } + +need_root() { + [[ $EUID -eq 0 ]] || die "Запускать только от root" +} + +################################# +# START +################################# + +need_root + +PROJECT_DIR="/opt/3dp-manager" + +log "Начинаем удаление 3dp-manager" + +if [[ ! -d "$PROJECT_DIR" ]]; then + warn "Директория $PROJECT_DIR не найдена — удалять нечего" + exit 0 +fi + +cd "$PROJECT_DIR" + +################################# +# DOCKER COMPOSE DOWN +################################# + +if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then + if [[ -f docker-compose.yml ]]; then + log "Останавливаем контейнеры 3dp-manager" + docker compose down --volumes --remove-orphans || warn "Ошибка при docker compose down" + else + warn "docker-compose.yml не найден" + fi +else + warn "Docker или docker compose не установлен — пропуск" +fi + +################################# +# CLEAN IMAGES (ТОЛЬКО НАШИ) +################################# + +log "Удаляем образы 3dp-manager (если есть)" + +docker images --format '{{.Repository}} {{.ID}}' \ + | grep 3dp-manager \ + | awk '{print $2}' \ + | xargs -r docker rmi -f || true + +################################# +# REMOVE DIRECTORY +################################# + +log "Удаляем $PROJECT_DIR" +rm -rf "$PROJECT_DIR" + +################################# +# DONE +################################# + +log "✔ 3dp-manager полностью удалён" \ No newline at end of file diff --git a/forwarding_delete.sh b/forwarding_delete.sh new file mode 100644 index 0000000..9c6fb42 --- /dev/null +++ b/forwarding_delete.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +################################# +# TRAP +################################# +trap 'echo -e "\033[1;31m[ERROR]\033[0m Ошибка в строке $LINENO"; exit 1' ERR + +################################# +# HELPERS +################################# +log() { echo -e "\033[1;32m[INFO]\033[0m $1"; } +warn() { echo -e "\033[1;33m[WARN]\033[0m $1"; } + +[[ $EUID -eq 0 ]] || { echo "Запускать нужно от root"; exit 1; } + +################################# +# PATHS +################################# +BASE_DIR="/opt/3dp-manager" +UFW_BEFORE="/etc/ufw/before.rules" +UFW_NAT_MARKER="3dp-manager NAT" +NGINX_PORT=$(cd "$BASE_DIR" && docker compose exec -T node printenv ORIGIN_PORT | tr -d '\r') + +################################# +# DOCKER CLEAN +################################# +if [[ -d "$BASE_DIR" ]]; then + log "Остановка Docker" + cd "$BASE_DIR" + docker compose down --remove-orphans || true +else + warn "Каталог проекта не найден" +fi + +################################# +# REMOVE FILES +################################# +log "Удаление файлов проекта" +rm -rf "$BASE_DIR" + +################################# +# UFW NAT CLEAN +################################# +echo "--- 1. Удаление оптимизаций ядра ---" +rm -f /etc/sysctl.d/99-relay-optimization.conf +# Применяем стандартные настройки (отключаем BBR и forward, если они не были включены до этого) +sysctl net.ipv4.ip_forward=0 +sysctl net.core.default_qdisc=pfifo_fast +sysctl net.ipv4.tcp_congestion_control=cubic + +echo "--- 2. Восстановление правил UFW из бэкапа ---" +if [ -f /etc/ufw/before.rules.bak ]; then + mv /etc/ufw/before.rules.bak /etc/ufw/before.rules + echo "Файл before.rules восстановлен из бэкапа." +else + echo "ВНИМАНИЕ: Бэкап before.rules.bak не найден. Правила NAT придется удалять вручную." +fi + +echo "--- 3. Удаление разрешающих правил портов ---" +ufw delete allow 443/tcp +ufw delete allow 8443/tcp +ufw delete allow "$NGINX_PORT"/tcp +ufw delete allow 10000:60000/tcp +ufw delete allow 10000:60000/udp + +echo "--- 4. Возврат политики FORWARD по умолчанию (DROP) ---" +sed -i 's/DEFAULT_FORWARD_POLICY="ACCEPT"/DEFAULT_FORWARD_POLICY="DROP"/' /etc/default/ufw + +echo "--- 5. Перезапуск фаервола ---" +ufw reload + +################################# +# RESULT +################################# + +echo "Docker контейнеры удалены" +echo "UFW NAT очищен" +echo "ip_forward отключён" +log "Откат завершён. Для применения изменений перезагрузите систему!" \ No newline at end of file diff --git a/forwarding_install.sh b/forwarding_install.sh new file mode 100644 index 0000000..d786ddc --- /dev/null +++ b/forwarding_install.sh @@ -0,0 +1,435 @@ +#!/usr/bin/env bash +set -euo pipefail + +################################# +# TRAP +################################# +trap 'echo -e "\033[1;31m[ERROR]\033[0m Ошибка в строке $LINENO"; exit 1' ERR + +################################# +# HELPERS +################################# +log() { echo -e "\033[1;32m[INFO]\033[0m $1"; } +warn() { echo -e "\033[1;33m[WARN]\033[0m $1"; } +die() { echo -e "\033[1;31m[ERROR]\033[0m $1"; exit 1; } + +[[ $EUID -eq 0 ]] || die "Запускать нужно от root" + +# --- ASCII-баннер --- +echo "===================================================" +echo " ____ ____ _ ___ _ " +echo " / __ \___ ____ / __ \(_) (_)___ ______(_)___ ___ " +echo " / / / / _ \/ __ \/ /_/ / / / / __ \/ ___/ / __ \`__ \ " +echo " / /_/ / __/ / / / ____/ / / / /_/ / / / / / / / / /" +echo "/_____/\___/_/ /_/_/ /_/_/_/\__, /_/ /_/_/ /_/ /_/ " +echo " /____/ " +echo "" +echo " 3DP-MANAGER SUBSCRIPTION FORWARDER " +echo "===================================================" +echo "" +# -------------------- + +################################# +# INPUT +################################# +read -rp "URL исходной подписки: " ORIGIN_SUB_URL +[[ "$ORIGIN_SUB_URL" =~ ^https?:// ]] || die "Некорректный URL подписки" + +read -rp "Домен или IP этого сервера (Enter = авто IP): " LOCAL_HOST +if [[ -z "$LOCAL_HOST" ]]; then + LOCAL_HOST=$(hostname -I | awk '{print $1}' | tr -d '[:space:]') +fi + +################################# +# PARSE URL +################################# +ORIGIN_PROTO=$(echo "$ORIGIN_SUB_URL" | awk -F: '{print $1}') +ORIGIN_HOST=$(echo "$ORIGIN_SUB_URL" | awk -F[/:] '{print $4}') +ORIGIN_PORT=$(echo "$ORIGIN_SUB_URL" | awk -F[:] '{print $3}' | awk -F/ '{print $1}') +SUB_PATH="/$(echo "$ORIGIN_SUB_URL" | cut -d/ -f4-)" + +################################# +# Определяем ORIGIN_IP +################################# + +if [[ "$ORIGIN_HOST" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + # Это уже IP-адрес + ORIGIN_IP="$ORIGIN_HOST" + log "Используем прямой IP из URL: $ORIGIN_IP" +else + # Это домен — разрешаем в IP + log "Разрешаем домен $ORIGIN_HOST в IP..." + + # Пробуем getent ahosts (IPv4 приоритет) + ORIGIN_IP=$(getent ahosts "$ORIGIN_HOST" | awk '/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ {print $1; exit}') + + # Если не вышло — fallback на hosts + [[ -z "$ORIGIN_IP" ]] && ORIGIN_IP=$(getent hosts "$ORIGIN_HOST" | awk '{print $1; exit}') + + # Последний шанс — dig (если установлен) + if [[ -z "$ORIGIN_IP" ]] && command -v dig >/dev/null; then + ORIGIN_IP=$(dig +short "$ORIGIN_HOST" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1) + fi + + [[ -n "$ORIGIN_IP" ]] || die "Не удалось разрешить домен в IP: $ORIGIN_HOST. Проверьте DNS или укажите IP вручную." + + log "Домен $ORIGIN_HOST разрешён в IP: $ORIGIN_IP" +fi + +# Финальная проверка +[[ -n "$ORIGIN_IP" && -n "$ORIGIN_PORT" && -n "$SUB_PATH" ]] || die "Ошибка парсинга URL подписки" + +################################# +# HTTPS CHECK +################################# +USE_HTTPS=false +CERT_PATH="" +KEY_PATH="" + +if [[ "$LOCAL_HOST" != "$(hostname -I | awk '{print $1}' | tr -d '[:space:]')" ]]; then + if [[ -d "/etc/letsencrypt/live/$LOCAL_HOST" ]]; then + CERT_PATH="/etc/letsencrypt/live/$LOCAL_HOST/fullchain.pem" + KEY_PATH="/etc/letsencrypt/live/$LOCAL_HOST/privkey.pem" + USE_HTTPS=true + log "Найдены сертификаты Let's Encrypt" + else + warn "Сертификаты Let's Encrypt не найдены" + read -rp "Путь к fullchain.pem (Enter = HTTP): " CERT_PATH + if [[ -n "$CERT_PATH" ]]; then + read -rp "Путь к privkey.pem: " KEY_PATH + [[ -f "$CERT_PATH" && -f "$KEY_PATH" ]] || die "Файлы сертификатов не найдены" + USE_HTTPS=true + fi + fi +fi + +################################# +# PROJECT DIR +################################# +BASE_DIR="/opt/3dp-manager" +NODE_DIR="$BASE_DIR/node" + +mkdir -p "$NODE_DIR" +cd "$BASE_DIR" + +################################# +# DOWNLOAD FILES +################################# +REPO="https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main" +NGINX_PORT=$ORIGIN_PORT +SUB_URL="$ORIGIN_PROTO://$LOCAL_HOST:$NGINX_PORT$SUB_PATH" + +################################# +# ENV +################################# +cat > .env < node/package.json < node/Dockerfile < node/index.js <<'EOF' +import express from "express"; +import axios from "axios"; + +const app = express(); + +const { + ORIGIN_SUB_URL, + LOCAL_HOST +} = process.env; + +app.get("/bus/:token", async (req, res) => { + try { + const url = ORIGIN_SUB_URL; + const r = await axios.get(url, { timeout: 15000 }); + let data = r.data; + + // vmess base64 + const lines = data.split("\n").map(l => { + if (l.startsWith("vmess://")) { + const obj = JSON.parse(Buffer.from(l.slice(8), "base64").toString()); + obj.add = LOCAL_HOST; + return "vmess://" + Buffer.from(JSON.stringify(obj)).toString("base64"); + } + // остальные протоколы — замена хоста + return l.replace(/@([^:/?#]+)/, `@${LOCAL_HOST}`); + }); + + res.type("text/plain").send(lines.join("\n")); + } catch (e) { + res.status(500).send("subscription error"); + } +}); + +app.listen(3000, () => { + console.log("sub-forwarder started"); +}); +EOF + +################################# +# NGINX & DOCKER COMPOSE +################################# + +if $USE_HTTPS; then +cat > nginx.conf < docker-compose.yml < nginx.conf < docker-compose.yml </dev/null 2>&1; then + log "Docker уже установлен" +else + log "Docker не найден, будет установлен" + apt update + apt install -y docker.io || die "Ошибка установки docker.io" + systemctl enable docker + systemctl restart docker +fi + +# Проверка docker compose v2 +if docker compose version >/dev/null 2>&1; then + log "docker compose v2 доступен" +else + log "Устанавливаем docker-compose-v2" + # решаем конфликт с docker-compose-plugin + apt install -y --allow-downgrades --allow-remove-essential --allow-change-held-packages \ + docker-compose-v2 || warn "docker-compose-v2 не установлен, возможно уже есть плагин" +fi + +################################# +# START +################################# +docker compose up -d --build + +################################# +# UFW NAT +################################# +LOCAL_IP=$(hostname -I | awk '{print $1}') + +# Проверка и установка UFW, если не установлен +if ! command -v ufw >/dev/null 2>&1; then + echo "UFW не установлен. Устанавливаю..." + apt update -qq && apt install -y ufw +fi + +# Проверка статуса UFW (независимо от локали, на английском) +if LC_ALL=C ufw status 2>/dev/null | grep -q "Status: active"; then + echo "UFW уже активен." +else + echo "ВНИМАНИЕ: UFW выключен или не настроен. Включаю..." + + # Разрешаем SSH (или другой порт, если используете не 22) + # OpenSSH — стандартный профиль, обычно порт 22 + ufw allow OpenSSH >/dev/null 2>&1 || true + # Если SSH на другом порту, добавьте: ufw allow 2222/tcp + + # Включаем UFW без подтверждения + ufw --force enable >/dev/null 2>&1 + + # Проверяем, успешно ли включён + if LC_ALL=C ufw status 2>/dev/null | grep -q "Status: active"; then + echo "UFW успешно включён." + else + echo "ОШИБКА: Не удалось включить UFW. Проверьте вручную!" + exit 1 + fi +fi + +echo "--- 1. Оптимизация сетевого стека ядра ---" +cat < /etc/sysctl.d/99-relay-optimization.conf +net.ipv4.ip_forward = 1 +net.core.default_qdisc = fq +net.ipv4.tcp_congestion_control = bbr +net.netfilter.nf_conntrack_max = 2000000 +net.core.rmem_max = 16777216 +net.core.wmem_max = 16777216 +net.ipv4.tcp_rmem = 4096 87380 16777216 +net.ipv4.tcp_wmem = 4096 65536 16777216 +net.ipv4.tcp_mtu_probing = 1 +net.ipv4.conf.all.accept_local = 1 +net.ipv4.conf.all.route_localnet = 1 +net.core.netdev_max_backlog=250000 +net.ipv4.tcp_slow_start_after_idle=0 +net.ipv4.tcp_tw_reuse=1 +EOF +sysctl --system + +echo "--- 2. Настройка правил перенаправления (before.rules) ---" +cp /etc/ufw/before.rules /etc/ufw/before.rules.bak + +# Создаем временный файл с вашими правилами +cat < /tmp/ufw_nat_rules +*nat +:PREROUTING ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +# Исключаем порт Nginx менеджера +-A PREROUTING -p tcp --dport $NGINX_PORT -j RETURN +# Проброс портов +-A PREROUTING -p tcp -m multiport --dports 443,8443,10000:60000 -j DNAT --to-destination $ORIGIN_IP +-A PREROUTING -p udp -m multiport --dports 443,8443,10000:60000 -j DNAT --to-destination $ORIGIN_IP +# Маскировка под локальный IP сервера +-A POSTROUTING -p tcp -d $ORIGIN_IP -j SNAT --to-source $LOCAL_IP +-A POSTROUTING -p udp -d $ORIGIN_IP -j SNAT --to-source $LOCAL_IP +COMMIT + +*filter +:FORWARD ACCEPT [0:0] +:INPUT ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] + +# Разрешаем пересылку для уже установленных соединений +-A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT + +# Явно разрешаем прохождение трафика на твой VPN сервер +-A FORWARD -d $ORIGIN_IP -j ACCEPT +-A FORWARD -s $ORIGIN_IP -j ACCEPT + +COMMIT + +*mangle +:FORWARD ACCEPT [0:0] +-A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu +COMMIT + +EOF + +# Удаляем старые блоки *nat и *mangle из оригинала, если они там были (от прошлых запусков) +# И создаем чистую копию без них +sed -i '/\*nat/,/COMMIT/d' /etc/ufw/before.rules +sed -i '/\*mangle/,/COMMIT/d' /etc/ufw/before.rules + +# Склеиваем: Сначала NAT/Mangle, потом остаток оригинального before.rules +cat /tmp/ufw_nat_rules /etc/ufw/before.rules > /etc/ufw/before.rules.new +mv /etc/ufw/before.rules.new /etc/ufw/before.rules + +echo "--- 3. Открытие портов в самом фаерволе ---" +# Разрешаем входящий трафик на эти порты +ufw allow 443/tcp +ufw allow 8443/tcp +ufw allow "$NGINX_PORT"/tcp +ufw allow 10000:60000/tcp +ufw allow 10000:60000/udp + +# Разрешаем FORWARD (пересылку) +# Это критически важный шаг для корректной работы DNAT в UFW +sed -i 's/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw + +echo "--- 4. Перезапуск ---" +ufw reload + +echo "Готово! Система оптимизирована, порты открыты, трафик перенаправлен." + +################################# +# RESULT +################################# +echo +log "Готово" +echo "Подписка:" +echo "$SUB_URL" \ No newline at end of file diff --git a/get_domains.js b/get_domains.js new file mode 100644 index 0000000..7f64e84 --- /dev/null +++ b/get_domains.js @@ -0,0 +1,62 @@ +const fs = require('fs'); + +// Вставьте сюда вашу ссылку на подписку +const SUBSCRIPTION_URL = ''; + +// Регулярное выражение для проверки домена (минимум одна точка, без спецсимволов) +const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i; + +async function processSubscription() { + try { + console.log('Загрузка данных подписки...'); + const response = await fetch(SUBSCRIPTION_URL); + + if (!response.ok) { + throw new Error(`Ошибка загрузки: ${response.statusText}`); + } + + let rawData = await response.text(); + + // Если подписка в Base64 (часто для vless), раскомментируйте строку ниже: + // if (!rawData.startsWith('vless://')) rawData = Buffer.from(rawData, 'base64').toString('utf8'); + + const links = rawData.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0); + const domains = new Set(); + + links.forEach(link => { + try { + const url = new URL(link); + const params = url.searchParams; + + // Извлекаем значения из sni и host + const candidates = [params.get('sni'), params.get('host')]; + + candidates.forEach(val => { + if (val) { + const cleanVal = val.trim().toLowerCase(); + // Валидация: проверяем через Regex и исключаем пустые строки + if (cleanVal && domainRegex.test(cleanVal)) { + domains.add(cleanVal); + } + } + }); + } catch (e) { + // Пропускаем строки, которые не являются ссылками + } + }); + + const result = Array.from(domains).sort(); // Сортируем для удобства + + if (result.length > 0) { + fs.writeFileSync('domains.txt', result.join('\n'), 'utf8'); + console.log(`Успешно! Сохранено уникальных доменов: ${result.length}`); + } else { + console.log('Валидные домены не найдены.'); + } + + } catch (error) { + console.error('Произошла ошибка:', error.message); + } +} + +processSubscription(); \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..7741e51 --- /dev/null +++ b/install.sh @@ -0,0 +1,330 @@ +#!/usr/bin/env bash +set -euo pipefail + +################################# +# TRAP ДЛЯ ОТЛАДКИ +################################# +trap 'echo -e "\033[1;31m[ERROR]\033[0m Ошибка в строке $LINENO"; exit 1' ERR + +################################# +# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ +################################# + +log() { echo -e "\033[1;32m[INFO]\033[0m $1"; } +die() { echo -e "\033[1;31m[ERROR]\033[0m $1"; exit 1; } + +need_root() { + [[ $EUID -eq 0 ]] || die "Запускать только от root" +} + +validate_url() { + [[ "$1" =~ ^https?://[^/]+:[0-9]+/.+ ]] +} + +################################# +# ПРОВЕРКИ +################################# + +need_root + +# --- ASCII-баннер --- +echo "===================================================" +echo " ____ ____ _ ___ _ " +echo " / __ \___ ____ / __ \(_) (_)___ ______(_)___ ___ " +echo " / / / / _ \/ __ \/ /_/ / / / / __ \/ ___/ / __ \`__ \ " +echo " / /_/ / __/ / / / ____/ / / / /_/ / / / / / / / / /" +echo "/_____/\___/_/ /_/_/ /_/_/_/\__, /_/ /_/_/ /_/ /_/ " +echo " /____/ " +echo "" +echo " 3DP-MANAGER FOR 3X-UI " +echo "===================================================" +echo "" +# -------------------- + +################################# +# ВВОД ДАННЫХ +################################# + +read -rp "URL панели 3x-ui (с портом и путем): " UI_URL +UI_URL="${UI_URL%/}" +validate_url "$UI_URL" || die "Некорректный URL 3x-ui" + +read -rp "Логин 3x-ui: " UI_LOGIN +read -rsp "Пароль 3x-ui: " UI_PASSWORD +echo +[[ -z "$UI_LOGIN" || -z "$UI_PASSWORD" ]] && die "Логин/пароль обязательны" + +# Проверка логина +LOGIN_RESPONSE=$(curl -s -X POST "$UI_URL/login" -H "Content-Type: application/json" -d "{\"username\":\"$UI_LOGIN\",\"password\":\"$UI_PASSWORD\"}") + +# Проверка успешного логина по ключевому полю "success" или наличию куки +if ! echo "$LOGIN_RESPONSE" | grep -q '"success":true'; then + echo "Не удалось залогиниться в 3x-ui. Проверьте URL, логин и пароль." + exit 1 +fi + +echo "✔ Успешный логин в 3x-ui" + +# Разбор UI_URL +UI_HOST=$(echo "$UI_URL" | awk -F[/:] '{print $4}') # домен или IP +UI_PROTO=$(echo "$UI_URL" | awk -F: '{print $1}') # http или https + +if [[ "$UI_PROTO" == "https" ]]; then + log "HTTPS панель обнаружена, проверяем SSL сертификаты" + + DEFAULT_CERT="/etc/letsencrypt/live/$UI_HOST/fullchain.pem" + DEFAULT_KEY="/etc/letsencrypt/live/$UI_HOST/privkey.pem" + + if [[ -f "$DEFAULT_CERT" && -f "$DEFAULT_KEY" ]]; then + CERT_PATH="$DEFAULT_CERT" + KEY_PATH="$DEFAULT_KEY" + log "Найдены сертификаты Let's Encrypt для $UI_HOST" + else + log "⚠ Сертификаты Let's Encrypt для $UI_HOST не найдены" + read -rp "Введите полный путь к SSL сертификату (fullchain.pem): " CERT_PATH + read -rp "Введите полный путь к SSL ключу (privkey.pem): " KEY_PATH + + [[ -f "$CERT_PATH" ]] || die "Файл сертификата не найден: $CERT_PATH" + [[ -f "$KEY_PATH" ]] || die "Файл ключа не найден: $KEY_PATH" + fi +fi + + +# Ввод интервала ротации +read -rp "Интервал генерации инбаундов в минутах (от 10, по умолчанию 30): " ROTATE_INTERVAL +ROTATE_INTERVAL="${ROTATE_INTERVAL:-30}" # по умолчанию 30 + +# Проверка, что это число и ≥10 +if ! [[ "$ROTATE_INTERVAL" =~ ^[0-9]+$ ]] || [ "$ROTATE_INTERVAL" -lt 10 ]; then + echo "Неверное значение. Используется значение по умолчанию 30 минут." + ROTATE_INTERVAL=30 +fi + +echo "Интервал ротации установлен: $ROTATE_INTERVAL минут" + +PROJECT_DIR="/opt/3dp-manager" +log "Используется директория проекта: $PROJECT_DIR" + +mkdir -p "$PROJECT_DIR" +cd "$PROJECT_DIR" + +################################# +# Белый лист +################################# + +REPO_BASE="https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main" + +curl -fsSL "$REPO_BASE/whitelist.txt" -o whitelist.txt +log "whitelist.txt скопирован" + +################################# +# ГЕНЕРАЦИЯ ТОКЕНА +################################# + +SUB_TOKEN="$(openssl rand -hex 24)" +log "Сгенерирован токен подписки" + +################################# +# DOCKER +################################# + +log "Проверка Docker" + +if command -v docker >/dev/null 2>&1; then + log "Docker уже установлен" +else + log "Docker не найден, будет установлен" + apt update + apt install -y docker.io || die "Ошибка установки docker.io" + systemctl enable docker + systemctl restart docker +fi + +# Проверка docker compose v2 +if docker compose version >/dev/null 2>&1; then + log "docker compose v2 доступен" +else + log "Устанавливаем docker-compose-v2" + # решаем конфликт с docker-compose-plugin + apt install -y --allow-downgrades --allow-remove-essential --allow-change-held-packages \ + docker-compose-v2 || warn "docker-compose-v2 не установлен, возможно уже есть плагин" +fi + +################################# +# СТРУКТУРА +################################# + +mkdir -p app app/builders subscriptions + +# Генерация случайного свободного порта для подписки/Nginx +get_random_port() { + while :; do + PORT=$((RANDOM % 50000 + 10000)) # диапазон 10000-60000 + if ! ss -ltn | awk '{print $4}' | grep -q ":$PORT\$"; then + echo "$PORT" + return + fi + done +} +NGINX_PORT=$(get_random_port) + +# Генерация URL подписки +SUB_URL="$UI_PROTO://$UI_HOST:$NGINX_PORT/bus/$SUB_TOKEN" + +################################# +# ENV +################################# + +cat > .env < docker-compose.yml < nginx.conf < docker-compose.yml < nginx.conf </dev/null || die "Backend не запущен" +docker compose ps | grep nginx >/dev/null || die "Nginx не запущен" + +################################# +# РЕЗУЛЬТАТ +################################# + +log "✔ Установка завершена" +echo +echo "URL подписки:" +echo "$SUB_URL" \ No newline at end of file diff --git a/update.sh b/update.sh new file mode 100644 index 0000000..5df7c6a --- /dev/null +++ b/update.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +################################# +# TRAP +################################# +trap 'echo -e "\033[1;31m[ERROR]\033[0m Ошибка в строке $LINENO"; exit 1' ERR + +################################# +# HELPERS +################################# + +log() { echo -e "\033[1;32m[INFO]\033[0m $1"; } +warn() { echo -e "\033[1;33m[WARN]\033[0m $1"; } +die() { echo -e "\033[1;31m[ERROR]\033[0m $1"; exit 1; } + +need_root() { + [[ $EUID -eq 0 ]] || die "Запускать только от root" +} + +################################# +# CONFIG +################################# + +PROJECT_DIR="/opt/3dp-manager" +REPO_RAW="https://raw.githubusercontent.com/denpiligrim/test_3dp-manager/main" + +################################# +# START +################################# + +need_root + +log "Обновление 3dp-manager" + +[[ -d "$PROJECT_DIR" ]] || die "3dp-manager не установлен ($PROJECT_DIR не найден)" + +cd "$PROJECT_DIR" + +################################# +# CHECK DOCKER +################################# + +command -v docker >/dev/null 2>&1 || die "Docker не установлен" +docker compose version >/dev/null 2>&1 || die "docker compose v2 недоступен" + +################################# +# DOWNLOAD FILES +################################# + +log "Загружаем обновлённые файлы из репозитория" + +mkdir -p app + +curl -fsSL "$REPO_RAW/app/Dockerfile" -o app/Dockerfile +curl -fsSL "$REPO_RAW/app/package.json" -o app/package.json +curl -fsSL "$REPO_RAW/app/index.js" -o app/index.js +curl -fsSL "$REPO_RAW/app/builders/buildVlessRealityTcp.js" -o app/builders/buildVlessRealityTcp.js +curl -fsSL "$REPO_RAW/app/builders/buildVlessRealityXhttp.js" -o app/builders/buildVlessRealityXhttp.js +curl -fsSL "$REPO_RAW/app/builders/buildTrojanRealityTcp.js" -o app/builders/buildTrojanRealityTcp.js +curl -fsSL "$REPO_RAW/app/builders/buildShadowsocksTcp.js" -o app/builders/buildShadowsocksTcp.js +curl -fsSL "$REPO_RAW/app/builders/buildVmessTcp.js" -o app/builders/buildVmessTcp.js +curl -fsSL "$REPO_RAW/app/builders/buildVlessRealityGrpc.js" -o app/builders/buildVlessRealityGrpc.js +curl -fsSL "$REPO_RAW/app/builders/buildVlessWs.js" -o app/builders/buildVlessWs.js +curl -fsSL "$REPO_RAW/app/builders/buildInboundLink.js" -o app/builders/buildInboundLink.js +curl -fsSL "$REPO_RAW/whitelist.txt" -o whitelist.txt + +log "Файлы обновлены" + +################################# +# REBUILD BACKEND +################################# + +log "Пересобираем backend" +docker compose build node + +################################# +# RESTART CONTAINERS +################################# + +log "Перезапускаем контейнеры" +docker compose up -d + +################################# +# HEALTH CHECK +################################# + +sleep 2 + +docker compose ps | grep node >/dev/null || die "Backend не запущен" +docker compose ps | grep nginx >/dev/null || die "Nginx не запущен" + +################################# +# DONE +################################# + +log "3dp-manager успешно обновлён ✅" diff --git a/whitelist.txt b/whitelist.txt new file mode 100644 index 0000000..856a652 --- /dev/null +++ b/whitelist.txt @@ -0,0 +1,452 @@ +2gis.com +2gis.ru +300.ya.ru +3475482542.mc.yandex.ru +742231.ms.ok.ru +a.auth-nsdi.ru +a.res-nsdi.ru +a.wb.ru +adm.digital.gov.ru +adm.mp.rzd.ru +admin.cs7777.vk.ru +admin.tau.vk.ru +ads.vk.ru +ads.x5.ru +adv.ozon.ru +ai-gpt1.vepene.site +ai4.vepene.site +akashi.vk-portal.net +alfabank.ru +aliusfilms.ru +ams2-cdn.2gis.com +an.yandex.ru +analytics.vk.ru +api-maps.yandex.ru +api.2gis.ru +api.avito.ru +api.browser.yandex.com +api.browser.yandex.ru +api.cs7777.vk.ru +api.events.plus.yandex.net +api.fastload.site +api.furypay.ru +api.max.ru +api.nodesecure.ru +api.ok.ru +api.photo.2gis.com +api.plus.kinopoisk.ru +api.reviews.2gis.com +api.s3.yandex.net +api.speedload.online +api.speedload.ru +api.tau.vk.ru +api.uxfeedback.yandex.net +api.vk.ru +apiok.ru +apple.com +at.ultima.foundation +auto.ru +autodiscover.ord.ozon.ru +avatars.mds.yandex.com +avatars.mds.yandex.net +avito.ru +avito.st +away.cs7777.vk.ru +away.tau.vk.ru +b.auth-nsdi.ru +b.res-nsdi.ru +bank.ozon.ru +banners-website.wildberries.ru +bfds.sberbank.ru +bg.fasssst.ru +bot.gosuslugi.ru +botapi.max.ru +bro-bg-store.s3.yandex.com +bro-bg-store.s3.yandex.net +bro-bg-store.s3.yandex.ru +brontp-pre.yandex.ru +browser.yandex.com +browser.yandex.ru +business.vk.ru +cargo.rzd.ru +catalog.api.2gis.com +cdn-de.maviks.ru +cdn.fastload.site +cdn.glorypay.ru +cdn.kickdown.click +cdn.lemanapro.ru +cdn.m1space.ru +cdn.s3.yandex.net +cdn.speedload.ru +cdn.tbank.ru +cdn.yandex.ru +cdncloudone.serverstats.ru +cdncloudthree.serverstats.ru +cdncloudtwo.serverstats.ru +cdnfive.serverstats.ru +cdnmsk.serverstats.ru +cdnrhkgfkkpupuotntfj.svc.cdn.yandex.net +cdnsix.vk-com.ru +cdntwo.dnsttlocate.ru +cdntwo.speedstream.ru +ch.fjdjdjfjd.icu +chat-prod.wildberries.ru +chat.speedload.ru +chat3.vtb.ru +cloud.cdn.yandex.com +cloud.cdn.yandex.net +cloud.cdn.yandex.ru +cloud.vk.com +cloud.vk.ru +cloudcdn-ams19.cdn.yandex.net +cloudcdn-m9-10.cdn.yandex.net +cloudcdn-m9-12.cdn.yandex.net +cloudcdn-m9-13.cdn.yandex.net +cloudcdn-m9-14.cdn.yandex.net +cloudcdn-m9-15.cdn.yandex.net +cloudcdn-m9-2.cdn.yandex.net +cloudcdn-m9-3.cdn.yandex.net +cloudcdn-m9-4.cdn.yandex.net +cloudcdn-m9-5.cdn.yandex.net +cloudcdn-m9-6.cdn.yandex.net +cloudcdn-m9-7.cdn.yandex.net +cloudcdn-m9-9.cdn.yandex.net +cloudlane.xyz +cms-res-web.online.sberbank.ru +cobrowsing.tbank.ru +collections.yandex.com +collections.yandex.ru +company.rzd.ru +connect.cs7777.vk.ru +contacts.rzd.ru +contract.gosuslugi.ru +crowdtest.payment-widget-smarttv.plus.tst.kinopoisk.ru +crowdtest.payment-widget.plus.tst.kinopoisk.ru +cs.avito.ru +cs7777.vk.ru +csp.yandex.net +d-assets.2gis.ru +dating.ok.ru +de1.vepene.site +dev.cs7777.vk.ru +dev.max.ru +dev.tau.vk.ru +digital.gov.ru +disk.2gis.com +disk.rzd.ru +dmp.dmpkit.lemanapro.ru +dnd.wb.ru +download.max.ru +dr.yandex.net +dr2.yandex.net +dropbox.com +dzen.ru +edgeport.xyz +egress.yandex.net +eh.vk.com +eh.vk.ru +ekmp-a-51.rzd.ru +enterprise.api-maps.yandex.ru +esa-res.online.sberbank.ru +esia.gosuslugi.ru +event.yandex.ru +expert.vk.ru +external-api.mediabilling.kinopoisk.ru +external-api.plus.kinopoisk.ru +fastsync.xyz +favicon.yandex.com +favicon.yandex.net +favicon.yandex.ru +favorites.api.2gis.com +filekeeper-vod.2gis.com +finance.wb.ru +fr.connectvpn.tech +frontend.vh.yandex.ru +fw.wb.ru +gbr.fasssst.online +germany1.cdn.yandexdns.ru +germany2.cdn.yandexdns.ru +gos9.portal-guard.com +gosuslugi.ru +gosweb.gosuslugi.ru +gotraf.nodesecure.ru +goya.rutube.ru +gpt-plus.vepene.site +gpt3.vepene.site +graphql-web.kinopoisk.ru +graphql.kinopoisk.ru +greone.dnsttlocate.ru +gretwo.dnsttlocate.ru +gu-st.ru +hd.kinopoisk.ru +hello.opengate.tech +help.max.ru +hkg.tunnelguard.ru +hlb.tunnelguard.ru +hls-svod.itunes.apple.com +hrc.tbank.ru +http-check-headers.yandex.ru +i.max.ru +i0.photo.2gis.com +i1.photo.2gis.com +i1ugrwg2dj.a.trbcdn.net +i2.photo.2gis.com +i3.photo.2gis.com +i4.photo.2gis.com +i5.photo.2gis.com +i6.photo.2gis.com +i7.photo.2gis.com +i8.photo.2gis.com +i9.photo.2gis.com +id.cs7777.vk.ru +id.sber.ru +id.tau.vk.ru +id.tbank.ru +id.vk.ru +ietf.org +images.apple.com +imgproxy.cdn-tinkoff.ru +informer.yandex.ru +invest.ozon.ru +io.ozone.ru +ir.ozone.ru +izbirkom.ru +jam.api.2gis.com +jitsi.wb.ru +keys.api.2gis.com +kiks.yandex.com +kiks.yandex.ru +kinopoisk.ru +knix.fasssst.online +le.tbank.ru +learning.ozon.ru +legal.max.ru +lemanapro.ru +link.max.ru +link.mp.rzd.ru +live.com +live.ok.ru +lk.gosuslugi.ru +log.strm.yandex.ru +login.cs7777.vk.ru +login.tau.vk.ru +login.vk.com +login.vk.ru +lv2.ultima.foundation +lva.tunnelguard.ru +m.47news.ru +m.avito.ru +m.cs7777.vk.ru +m.ok.ru +m.tau.vk.ru +m.vk.com +m.vk.ru +m.vkvideo.cs7777.vk.ru +ma.kinopoisk.ru +mail.yandex.com +mail.yandex.ru +map.gosuslugi.ru +mapgl.2gis.com +mapi.learning.ozon.ru +market.rzd.ru +maviks.ru +max.ru +mc.yandex.com +mc.yandex.ru +me.cs7777.vk.ru +mediafeeds.yandex.com +mediafeeds.yandex.ru +mediarush.xyz +megafon.ru +metrics.alfabank.ru +microapps.kinopoisk.ru +mp.rzd.ru +ms.cs7777.vk.ru +msk.t2.ru +mts.ru +multitest.ok.ru +music.vk.ru +music.yandex.ru +my.oversecure.pro +my.rzd.ru +mynode.nodesecure.ru +mynoderu.nodesecure.ru +neself.nodesecure.ru +neuro.translate.yandex.ru +nl.ultima.foundation +nl3.truservice.top +nla.tunnelguard.ru +novorossiya.gosuslugi.ru +nr9.strelkavpn.ru +oauth.cs7777.vk.ru +oauth.tau.vk.ru +oauth2.cs7777.vk.ru +ok.ufavpn.ru +oneclick-payment.kinopoisk.ru +online.sberbank.ru +ord.ozon.ru +ord.vk.ru +oversecure.pro +owa.ozon.ru +ozon.ru +partners.gosuslugi.ru +partners.lemanapro.ru +pay.ozon.ru +payment-widget-smarttv.plus.kinopoisk.ru +payment-widget.kinopoisk.ru +payment-widget.plus.kinopoisk.ru +pic.rutubelist.ru +pl-res.online.sberbank.ru +playself.nodesecure.ru +plb.tunnelguard.ru +plr.strelkavpn.ru +pos.gosuslugi.ru +powodzenia.xyz +pptest.userapi.com +preview.rutube.ru +prodvizhenie.rzd.ru +public-api.reviews.2gis.com +pulse.mp.rzd.ru +push.vk.ru +queuev4.vk.com +quiz.kinopoisk.ru +r.vk.ru +r0.mradx.net +receive-sentry.lmru.tech +reserve.ayagut.ovh +rontgen.fasssst.ru +ru.ultima.foundation +runstream.xyz +rutube.ru +rzd.ru +s.vtb.ru +s0.bss.2gis.com +s1.bss.2gis.com +s3.t2.ru +s3.yandex.net +sba.yandex.com +sba.yandex.net +sba.yandex.ru +sberbank.ru +secure-cloud.rzd.ru +secure.rzd.ru +securepay.ozon.ru +selfpl.speedload.online +seller.ozon.ru +sfd.gosuslugi.ru +shiftline.xyz +smartcaptcha.yandexcloud.net +sntr.avito.ru +softpipe.xyz +speller.yandex.net +sso-app4.vtb.ru +sso-app5.vtb.ru +sso.auto.ru +sso.dzen.ru +sso.passport.yandex.ru +ssp.rutube.ru +st-im.kinopoisk.ru +st-ok.cdn-vk.ru +st.avito.ru +st.kinopoisk.ru +st.max.ru +st.okcdn.ru +st.ozone.ru +st.vk.com +static-mon.yandex.net +static.lemanapro.ru +static.rutube.ru +stats.avito.ru +stats.vk-portal.net +sto.eu-ffast.com +storage.ape.yandex.net +streamport.xyz +strm-rad-23.strm.yandex.net +strm.yandex.net +strm.yandex.ru +styles.api.2gis.com +suggest.dzen.ru +suggest.sso.dzen.ru +sun6-20.userapi.com +sun6-21.userapi.com +sun6-22.userapi.com +sun9-101.userapi.com +sun9-38.userapi.com +surveys.yandex.ru +sync.browser.yandex.net +tamtam.ok.ru +target.vk.ru +team.rzd.ru +tech.vk.ru +tgju.org +ticket.rzd.ru +tickets.widget.kinopoisk.ru +tile0.maps.2gis.com +tile1.maps.2gis.com +tile2.maps.2gis.com +tile3.maps.2gis.com +tile4.maps.2gis.com +tradingview.com +travel.rzd.ru +travel.yandex.ru +travel.yastatic.net +ui.cs7777.vk.ru +ui.tau.vk.ru +usa1.vepene.site +user-geo-data.wildberries.ru +uslugi.yandex.ru +uxfeedback-cdn.s3.yandex.net +uxfeedback.yandex.ru +vk-portal.net +vkvideo.cs7777.vk.ru +vkvideo.ru +vless.hubp.de +vless.muzheng.top +voter.gosuslugi.ru +vt-1.ozone.ru +wap.yandex.com +wap.yandex.ru +wb.ru +web.max.ru +wechat.com +welcome.rzd.ru +widgets.kinopoisk.ru +wikiportal.su +ws-api.oneme.ru +ws.seller.ozon.ru +wtvp7ujn9v.a.trbcdn.net +www.abingdon.org.uk +www.apple.com +www.avito.ru +www.avito.st +www.bing.com +www.cikrf.ru +www.gosuslugi.ru +www.kinopoisk.ru +www.microsoft.com +www.ok.ru +www.ozon.ru +www.rutube.ru +www.rzd.ru +www.sberbank.ru +www.t2.ru +www.trustnav.com +www.vk.com +www.vtb.ru +xapi.ozon.ru +ya.ru +yabro-wbplugin.edadeal.yandex.ru +yabs.yandex.ru +yahoo.com +yandex.com +yandex.net +yandex.ru +yandexcdn.yandexdns.ru +yandexcdn2.yandexdns.ru +yastatic.net +yt.fasssst.ru +zen-yabro-morda.mediascope.mc.yandex.ru +zen.yandex.com +zen.yandex.net +zen.yandex.ru +zoomzoom.xyz \ No newline at end of file