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

- [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
+ [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](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 @@

- [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
+ [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](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 @@

- [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
+ [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](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 @@

- [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
+ [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](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 @@

- [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
+ [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](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
#################################