diff --git a/README.md b/README.md index 93a19a4..b1f97c7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

3dp-manager preview

-![Version](https://img.shields.io/badge/version-1.0.1-blue.svg) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0) [![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=flat&logo=telegram&logoColor=white)](https://t.me/denpiligrim_web) [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCOv2tFFYDY4mXOM60PVz8zw)](https://www.youtube.com/@denpiligrim) +![Version](https://img.shields.io/badge/version-1.0.2-blue.svg) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0) [![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=flat&logo=telegram&logoColor=white)](https://t.me/denpiligrim_web) [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCOv2tFFYDY4mXOM60PVz8zw)](https://www.youtube.com/@denpiligrim) # 3DP-MANAGER @@ -61,6 +61,9 @@ ## Установка +> [!WARNING] +> Утилита работает на основном сервере + У вас должны быть установлены пакеты на сервере `curl`, `jq` командой: `apt install curl jq` и панель управления `3x-ui`, которую можно установить командой: `bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)` Установите проект на сервер командой: @@ -70,7 +73,7 @@ bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main Краткое описание: запускает скрипт установки и разворачивает контейнеры и сервисы. -Если панель 3x-ui находится в Docker контейнере или на другом сервере, установите командой: +Если панель 3x-ui находится в Docker контейнере, установите командой: ```bash REMOTE_PANEL=true bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main/install.sh) @@ -101,7 +104,7 @@ bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main ## Установка сервиса перенаправления (forwarding) > [!WARNING] -> Сервис перенаправления работает на промежуточном сервере. +> Сервис перенаправления работает на промежуточном сервере Сервис перенаправления позволяет проксировать входящие порты с промежуточного сервера на основной. @@ -150,7 +153,19 @@ node get_domains.js cd /opt/3dp-manager && docker cp ./app/my_whitelist.txt node:/app/my_whitelist.txt ``` -Краткое описание: добавляет ваш файл доменов в контейнер приложения. Чтобы сразу же сгенерировать инбаунды с новым списком, выполните `docker exec -it node sh` и затем `node index.js`. +Краткое описание: добавляет ваш файл доменов в контейнер приложения. Чтобы сразу же сгенерировать инбаунды с новым списком, выполните `docker exec -it node sh` и затем `node rotate.js`. + +## Ручной запуск генерации + +Чтобы создать новые инбаунды, выполните команды поочередно: + +```bash +cd /opt/3dp-manager +docker exec -it node sh +node rotate.js +``` + +Краткое описание: выполняет немедленную генерацию инбаундов, не влияя на заданный интервал ротации. --- diff --git a/README_CN.md b/README_CN.md index 76f986a..ffbeb83 100644 --- a/README_CN.md +++ b/README_CN.md @@ -2,7 +2,7 @@

3dp-manager preview

-![Version](https://img.shields.io/badge/version-1.0.1.1-blue.svg) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0) [![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=flat&logo=telegram&logoColor=white)](https://t.me/denpiligrim_web) [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCOv2tFFYDY4mXOM60PVz8zw)](https://www.youtube.com/@denpiligrim) +![Version](https://img.shields.io/badge/version-1.0.2.1-blue.svg) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0) [![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=flat&logo=telegram&logoColor=white)](https://t.me/denpiligrim_web) [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCOv2tFFYDY4mXOM60PVz8zw)](https://www.youtube.com/@denpiligrim) # 3DP-MANAGER diff --git a/README_EN.md b/README_EN.md index 11feb04..6f86786 100644 --- a/README_EN.md +++ b/README_EN.md @@ -2,7 +2,7 @@

3dp-manager preview

-![Version](https://img.shields.io/badge/version-1.0.1-blue.svg) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0) [![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=flat&logo=telegram&logoColor=white)](https://t.me/denpiligrim_web) [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCOv2tFFYDY4mXOM60PVz8zw)](https://www.youtube.com/@denpiligrim) +![Version](https://img.shields.io/badge/version-1.0.2-blue.svg) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0) [![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=flat&logo=telegram&logoColor=white)](https://t.me/denpiligrim_web) [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCOv2tFFYDY4mXOM60PVz8zw)](https://www.youtube.com/@denpiligrim) # 3DP-MANAGER diff --git a/README_IR.md b/README_IR.md index 54d5375..fa84383 100644 --- a/README_IR.md +++ b/README_IR.md @@ -2,7 +2,7 @@

3dp-manager preview

-![Version](https://img.shields.io/badge/version-1.0.1-blue.svg) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0) [![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=flat&logo=telegram&logoColor=white)](https://t.me/denpiligrim_web) [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCOv2tFFYDY4mXOM60PVz8zw)](https://www.youtube.com/@denpiligrim) +![Version](https://img.shields.io/badge/version-1.0.2-blue.svg) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0) [![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=flat&logo=telegram&logoColor=white)](https://t.me/denpiligrim_web) [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCOv2tFFYDY4mXOM60PVz8zw)](https://www.youtube.com/@denpiligrim) # 3DP-MANAGER diff --git a/README_TK.md b/README_TK.md index e302b89..70cf6d8 100644 --- a/README_TK.md +++ b/README_TK.md @@ -2,7 +2,7 @@

3dp-manager preview

-![Version](https://img.shields.io/badge/version-1.0.1-blue.svg) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0) [![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=flat&logo=telegram&logoColor=white)](https://t.me/denpiligrim_web) [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCOv2tFFYDY4mXOM60PVz8zw)](https://www.youtube.com/@denpiligrim) +![Version](https://img.shields.io/badge/version-1.0.2-blue.svg) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0) [![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=flat&logo=telegram&logoColor=white)](https://t.me/denpiligrim_web) [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCOv2tFFYDY4mXOM60PVz8zw)](https://www.youtube.com/@denpiligrim) # 3DP-MANAGER diff --git a/app/Dockerfile b/app/Dockerfile index 4a63d4e..d50bd86 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -6,6 +6,7 @@ COPY package.json ./ RUN npm install --production COPY index.js . +COPY rotate.js . COPY builders ./builders CMD ["node", "index.js"] \ No newline at end of file diff --git a/app/index.js b/app/index.js index 0ef17cc..510e2ed 100644 --- a/app/index.js +++ b/app/index.js @@ -1,5 +1,5 @@ import axios from "axios"; -import cron from "node-cron"; +import https from "https"; import fs from "fs"; import net from "net"; import { buildVlessRealityTcp } from "./builders/buildVlessRealityTcp.js"; @@ -16,7 +16,10 @@ const SUB_FILE = "/subscriptions/list.txt"; const LAST_INBOUNDS = '/subscriptions/latestInbounds.json'; const cookieJar = {}; -const api = axios.create({ baseURL: UI_URL, timeout: 15000, withCredentials: true }); +const agent = new https.Agent({ + rejectUnauthorized: false +}); +const api = axios.create({ baseURL: UI_URL, timeout: 15000, withCredentials: true, httpsAgent: agent }); api.interceptors.request.use(config => { if (cookieJar.value) config.headers['Cookie'] = cookieJar.value; @@ -121,12 +124,12 @@ async function login() { } 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 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 deleteInbounds() { try { diff --git a/app/rotate.js b/app/rotate.js new file mode 100644 index 0000000..7f583de --- /dev/null +++ b/app/rotate.js @@ -0,0 +1,216 @@ +import axios from "axios"; +import https from "https"; +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 LAST_INBOUNDS = '/subscriptions/latestInbounds.json'; + +const cookieJar = {}; +const agent = new https.Agent({ + rejectUnauthorized: false +}); +const api = axios.create({ baseURL: UI_URL, timeout: 15000, withCredentials: true, httpsAgent: agent }); + +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/3dp-manager/main/whitelist.txt"; + +// Helper functions +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)) { + used.add(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 deleteInbounds() { + try { + const data = fs.readFileSync(LAST_INBOUNDS, 'utf-8'); + const oldIds = JSON.parse(data); + + if (Array.isArray(oldIds) && oldIds.length > 0) { + console.log(`Удаляем старые инбаунды: ${oldIds.length} шт.`); + for (const id of oldIds) { + try { + await deleteInbound(id); + } catch (err) { + console.error(`Ошибка при удалении инбаунда ${id}:`, err.message); + } + } + } + } catch (error) { + console.log("Старых ID не найдено, пропускаем удаление."); + } +} + +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?.obj?.id || null; + } catch (e) { + console.error("Add inbound failed:", e.message); + return null; + } +} + +// Main rotation of inbound entries +async function rotate() { + await login(); + + await updateWhitelist(); + const whitelist = loadWhitelist(); + const usedPorts = new Set(); + const subs = []; + + await deleteInbounds(); + + 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() }), + ]; + + const inboundIds = []; + + for (const b of builders) { + const domain = pickDomain(whitelist); + const inbound = await b(domain); + const idOrPass = inbound.settings ? JSON.parse(inbound.settings).clients?.[0]?.id || JSON.parse(inbound.settings).clients?.[0]?.password : ""; + + // Build the link depending on the protocol + const link = buildInboundLink(inbound, UI_HOST, idOrPass); + if (link) subs.push(link); + const id = await addInbound(inbound); + inboundIds.push(id); + } + + if (inboundIds.length > 0) { + fs.writeFileSync(LAST_INBOUNDS, JSON.stringify(inboundIds, null, 2)); + console.log(`Новые ID (${inboundIds.length} шт.) сохранены в ${LAST_INBOUNDS}`); + } + + fs.writeFileSync(SUB_FILE, subs.join("\n") + "\n", "utf8"); + console.log("✔ 10 inbound created, подписка обновлена"); +} + +// Initial run +rotate(); \ No newline at end of file diff --git a/install.sh b/install.sh index 96dbac6..21b4c24 100644 --- a/install.sh +++ b/install.sh @@ -285,6 +285,7 @@ curl -fsSL "$REPO_BASE/app/package.json" -o app/package.json # JS Files ################################# curl -fsSL "$REPO_BASE/app/index.js" -o app/index.js +curl -fsSL "$REPO_BASE/app/rotate.js" -o app/rotate.js curl -fsSL "$REPO_BASE/app/builders/buildVlessRealityTcp.js" -o app/builders/buildVlessRealityTcp.js curl -fsSL "$REPO_BASE/app/builders/buildVlessRealityXhttp.js" -o app/builders/buildVlessRealityXhttp.js curl -fsSL "$REPO_BASE/app/builders/buildTrojanRealityTcp.js" -o app/builders/buildTrojanRealityTcp.js diff --git a/update.sh b/update.sh index 3b4eda0..6c7c399 100644 --- a/update.sh +++ b/update.sh @@ -80,6 +80,11 @@ docker compose build node log "Перезапускаем контейнеры" docker compose up -d +if [ -f "app/my_whitelist.txt" ]; then + log "✔ Копируем my_whitelist.txt в контейнер..." + docker cp app/my_whitelist.txt node:/app/my_whitelist.txt +fi + ################################# # HEALTH CHECK #################################