add: self-signed cert, docker, cmd rotate
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
||||
|
||||
 [](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
|
||||
|
||||
<sup>Краткое описание: запускает скрипт установки и разворачивает контейнеры и сервисы.</sup>
|
||||
|
||||
Если панель 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
|
||||
```
|
||||
|
||||
<sup>Краткое описание: добавляет ваш файл доменов в контейнер приложения. Чтобы сразу же сгенерировать инбаунды с новым списком, выполните `docker exec -it node sh` и затем `node index.js`.</sup>
|
||||
<sup>Краткое описание: добавляет ваш файл доменов в контейнер приложения. Чтобы сразу же сгенерировать инбаунды с новым списком, выполните `docker exec -it node sh` и затем `node rotate.js`.</sup>
|
||||
|
||||
## Ручной запуск генерации
|
||||
|
||||
Чтобы создать новые инбаунды, выполните команды поочередно:
|
||||
|
||||
```bash
|
||||
cd /opt/3dp-manager
|
||||
docker exec -it node sh
|
||||
node rotate.js
|
||||
```
|
||||
|
||||
<sup>Краткое описание: выполняет немедленную генерацию инбаундов, не влияя на заданный интервал ротации.</sup>
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
||||
|
||||
 [](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
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
||||
|
||||
 [](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
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
||||
|
||||
 [](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
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
||||
|
||||
 [](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
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ COPY package.json ./
|
||||
RUN npm install --production
|
||||
|
||||
COPY index.js .
|
||||
COPY rotate.js .
|
||||
COPY builders ./builders
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
+11
-8
@@ -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 {
|
||||
|
||||
+216
@@ -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();
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
#################################
|
||||
|
||||
Reference in New Issue
Block a user