diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index 1b46c91..b50b509 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -2,7 +2,7 @@ name: Docker Build & Publish
on:
push:
- branches: [ "main" ]
+ branches: [ "main", "dp-new-release" ]
workflow_dispatch:
env:
diff --git a/README.md b/README.md
index 96b79ff..7e65106 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@
Главная цель утилиты — сделать так, чтобы ваш трафик не выглядел одинаковым. Бот генерирует по заданному интервалу 10 подключений с разными параметрами:
-- протоколы: `vless`, `vmess`, `shadowsocks`, `trojan`;
+- протоколы: `vless`, `vmess`, `shadowsocks`, `hysteria2`, `trojan`;
- порты: `443`, `8443` (фиксированные) и случайные из диапазона `10000-60000`;
- транспорт: `tcp`, `websocket`, `grpc`, `xhttp`;
- SNI берутся из белого списка доменов (whitelist); можно использовать свой список.
diff --git a/client/src/api.ts b/client/src/api.ts
index 8f7592a..fc8e45c 100644
--- a/client/src/api.ts
+++ b/client/src/api.ts
@@ -1,7 +1,7 @@
import axios from 'axios';
const api = axios.create({
- baseURL: `${location.protocol}//${location.hostname}:3000/api`,
+ baseURL: `${location.protocol}//${location.hostname}:${location.port}/api`,
});
export default api;
\ No newline at end of file
diff --git a/client/src/pages/SubscriptionsPage.tsx b/client/src/pages/SubscriptionsPage.tsx
index 2235891..bfdb1bf 100644
--- a/client/src/pages/SubscriptionsPage.tsx
+++ b/client/src/pages/SubscriptionsPage.tsx
@@ -18,6 +18,7 @@ interface Subscription {
name: string;
uuid: string;
inbounds: any[];
+ inboundsConfig?: any[];
}
interface Tunnel {
@@ -33,6 +34,7 @@ interface InboundConfigUI {
type: string;
port: string;
sni: string;
+ link?: string;
}
interface Domain { id: number; name: string; }
@@ -46,6 +48,7 @@ const CONNECTION_OPTIONS = [
'vmess-tcp',
'shadowsocks-tcp',
'trojan-tcp-reality',
+ 'custom',
];
const patchLink = function (link: string, newHost: string): string {
@@ -124,16 +127,16 @@ export default function SubscriptionsPage() {
setEditingId(null);
setName('');
setInbounds([
- { id: generateId(), type: 'hysteria2-udp', port: 'random', sni: 'random' },
- { id: generateId(), type: 'vless-xhttp-reality', port: 'random', sni: 'random' },
- { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' },
- { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' },
- { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' },
- { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' },
- { id: generateId(), type: 'vless-grpc-reality', port: 'random', sni: 'random' },
- { id: generateId(), type: 'vless-ws', port: 'random', sni: 'random' },
- { id: generateId(), type: 'vmess-tcp', port: 'random', sni: 'random' },
- { id: generateId(), type: 'shadowsocks-tcp', port: 'random', sni: 'random' },
+ { id: generateId(), type: 'hysteria2-udp', port: 'random', sni: 'random', link: '' },
+ { id: generateId(), type: 'vless-xhttp-reality', port: 'random', sni: 'random', link: '' },
+ { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' },
+ { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' },
+ { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' },
+ { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' },
+ { id: generateId(), type: 'vless-grpc-reality', port: 'random', sni: 'random', link: '' },
+ { id: generateId(), type: 'vless-ws', port: 'random', sni: 'random', link: '' },
+ { id: generateId(), type: 'vmess-tcp', port: 'random', sni: 'random', link: '' },
+ { id: generateId(), type: 'shadowsocks-tcp', port: 'random', sni: 'random', link: '' },
]);
setPortErrors({});
setOpen(true);
@@ -143,79 +146,58 @@ export default function SubscriptionsPage() {
setEditingId(sub.id);
setName(sub.name);
- // Пытаемся маппить существующие инбаунды, или даем дефолтные
- if (sub.inbounds && sub.inbounds.length > 0) {
- setInbounds(sub.inbounds.map(i => ({
+ if (sub.inboundsConfig && sub.inboundsConfig.length > 0) {
+ setInbounds(sub.inboundsConfig.map(i => ({
id: generateId(),
type: i.type || 'vless-tcp-reality',
port: i.port ? i.port.toString() : 'random',
- sni: i.sni || 'random'
+ sni: i.sni || 'random',
+ link: i.link || ''
})));
} else {
- setInbounds([{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' }]);
+ setInbounds([{ id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' }]);
}
+
setPortErrors({});
setOpen(true);
};
- // Проверка порта
- const handlePortCheck = async (portValue: string, id: string) => {
- if (portValue === 'random' || portValue.trim() === '') {
- setPortErrors(prev => { const n = { ...prev }; delete n[id]; return n; });
- return;
- }
- const portNum = parseInt(portValue);
- if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
- setPortErrors(prev => ({ ...prev, [id]: 'Некорректный порт' }));
- return;
- }
- try {
- const { data } = await api.get(`/tunnels/check-port/${portNum}`);
- if (!data.isFree) {
- setPortErrors(prev => ({ ...prev, [id]: 'Порт занят' }));
- } else {
- setPortErrors(prev => { const n = { ...prev }; delete n[id]; return n; });
- }
- } catch (e) {
- console.error('Ошибка проверки порта', e);
- }
- };
-
const handleInboundChange = (id: string, field: keyof InboundConfigUI, value: string) => {
setInbounds(prev => prev.map(inb => inb.id === id ? { ...inb, [field]: value } : inb));
- if (field === 'port') {
+ if (field === 'port' || (field === 'type' && value === 'custom')) {
setPortErrors(prev => { const n = { ...prev }; delete n[id]; return n; });
}
};
const addInbound = () => {
if (inbounds.length < 20) {
- setInbounds([...inbounds, { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random' }]);
+ setInbounds([...inbounds, { id: generateId(), type: 'vless-tcp-reality', port: 'random', sni: 'random', link: '' }]);
}
};
-const removeInbound = (id?: string) => {
- if (id) {
- if (inbounds.length > 1) {
- setInbounds(inbounds.filter(inb => inb.id !== id));
- setPortErrors(prev => {
- const n = { ...prev };
- delete n[id];
- return n;
- });
- }
- } else {
- setInbounds([
- {
- id: crypto.randomUUID(),
- type: 'vless-tcp-reality',
- port: 'random',
- sni: 'random'
+ const removeInbound = (id?: string) => {
+ if (id) {
+ if (inbounds.length > 1) {
+ setInbounds(inbounds.filter(inb => inb.id !== id));
+ setPortErrors(prev => {
+ const n = { ...prev };
+ delete n[id];
+ return n;
+ });
}
- ]);
- setPortErrors({});
- }
-};
+ } else {
+ setInbounds([
+ {
+ id: crypto.randomUUID(),
+ type: 'vless-tcp-reality',
+ port: 'random',
+ sni: 'random',
+ link: ''
+ }
+ ]);
+ setPortErrors({});
+ }
+ };
const handleSave = async () => {
if (Object.keys(portErrors).length > 0) {
@@ -229,11 +211,16 @@ const removeInbound = (id?: string) => {
const payload = {
name,
- inboundsConfig: inbounds.map(i => ({
- type: i.type,
- port: i.port === 'random' ? 'random' : parseInt(i.port),
- sni: i.sni
- }))
+ inboundsConfig: inbounds.map(i => {
+ if (i.type === 'custom') {
+ return { type: i.type, link: i.link };
+ }
+ return {
+ type: i.type,
+ port: i.port === 'random' ? 'random' : parseInt(i.port),
+ sni: i.sni
+ };
+ })
};
try {
@@ -421,31 +408,46 @@ const removeInbound = (id?: string) => {
-
- handleInboundChange(inb.id, 'port', e.target.value)}
- onBlur={(e) => handlePortCheck(e.target.value, inb.id)}
- error={!!portErrors[inb.id]}
- helperText={portErrors[inb.id] || ""}
- sx={{ width: 140 }}
- />
-
+ {inb.type === 'custom' ? (
+ // Поле для кастомной ссылки
+
+ handleInboundChange(inb.id, 'link', e.target.value)}
+ fullWidth
+ />
+
+ ) : (
+ <>
+
+ handleInboundChange(inb.id, 'port', e.target.value)}
+ error={!!portErrors[inb.id]}
+ helperText={portErrors[inb.id] || ""}
+ sx={{ width: 140 }}
+ />
+
-
- SNI
-
-
+
+ SNI
+
+
+ >
+ )}
(null);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+ const [authMethod, setAuthMethod] = useState<'password' | 'key'>('password');
const [form, setForm] = useState({
- name: '', ip: '', sshPort: 22, username: 'root', password: '', domain: ''
+ name: '', ip: '', sshPort: 22, username: 'root', password: '', privateKey: '', domain: ''
});
useEffect(() => { loadTunnels(); }, []);
@@ -39,9 +44,16 @@ export default function TunnelsPage() {
};
const handleCreate = async () => {
- await api.post('/tunnels', form);
+ const payload = {
+ ...form,
+ password: authMethod === 'password' ? form.password : null,
+ privateKey: authMethod === 'key' ? form.privateKey : null,
+ };
+
+ await api.post('/tunnels', payload);
setOpen(false);
- setForm({ name: '', ip: '', sshPort: 22, username: 'root', password: '', domain: '' });
+ setForm({ name: '', ip: '', sshPort: 22, username: 'root', password: '', privateKey: '', domain: '' });
+ setAuthMethod('password');
loadTunnels();
};
@@ -149,7 +161,28 @@ export default function TunnelsPage() {
-
+
+ setAuthMethod(e.target.value as 'password' | 'key')}>
+ } label="По паролю" />
+ } label="По SSH ключу" />
+
+
+
+ {authMethod === 'password' ? (
+
+ ) : (
+
+ )}
diff --git a/install.sh b/install.sh
index a65da63..5b1565a 100644
--- a/install.sh
+++ b/install.sh
@@ -6,7 +6,7 @@ set -euo pipefail
#################################
PROJECT_DIR="/opt/3dp-manager"
DOCKER_USER="denpiligrim"
-DOCKER_TAG="main"
+DOCKER_TAG="dp-new-release"
IMAGE_SERVER="ghcr.io/${DOCKER_USER}/3dp-manager-server:${DOCKER_TAG}"
IMAGE_CLIENT="ghcr.io/${DOCKER_USER}/3dp-manager-client:${DOCKER_TAG}"
@@ -400,6 +400,63 @@ networks:
EOF
fi
+#################################
+# Hysteria 2
+#################################
+
+# Проверка установки Hysteria 2 через наличие systemd сервиса
+if ! systemctl cat hysteria-server.service &> /dev/null; then
+ echo "Сервис Hysteria 2 не найден. Начинаем установку..."
+
+ # Установка Hysteria 2 согласно документации
+ bash <(curl -fsSL https://get.hy2.sh/)
+
+ RANDOM_FREE_PORT=$(get_random_port)
+
+ # Генерация надежных паролей
+ GENERATED_PASSWORD=$(tr -dc A-Za-z0-9 /etc/hysteria/config.yaml < 0) {
for (const inbound of sub.inbounds) {
- await this.xuiService.deleteInbound(inbound.xuiId);
+ if (inbound.xuiId && inbound.xuiId > 0) {
+ await this.xuiService.deleteInbound(inbound.xuiId);
+ }
await this.inboundRepo.delete(inbound.id);
}
}
@@ -119,55 +122,105 @@ export class RotationService implements OnModuleInit {
}
const usedPorts = new Set();
-
- const tasks = [
- () => this.inboundBuilder.buildVlessRealityTcp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }),
- () => this.inboundBuilder.buildVlessRealityXhttp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }),
- () => this.inboundBuilder.buildVlessRealityGrpc({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }),
- () => this.inboundBuilder.buildVlessWs({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains) }),
- () => this.inboundBuilder.buildVlessRealityTcp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }),
- () => this.inboundBuilder.buildVlessRealityTcp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }),
- () => this.inboundBuilder.buildVlessRealityTcp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }),
- () => this.inboundBuilder.buildVmessTcp({ port: 0, uuid: uuidv4() }),
- () => this.inboundBuilder.buildShadowsocksTcp({ port: 0, uuid: uuidv4() }),
- () => this.inboundBuilder.buildTrojanRealityTcp({ port: 0, uuid: uuidv4(), domain: this.pickDomain(domains), ...keys }),
- ];
-
const host = await this.settingRepo.findOne({ where: { key: 'xui_host' } });
const serverAddress = host?.value || 'localhost';
const flag = await this.settingRepo.findOne({ where: { key: 'xui_geo_flag' } });
const flagEmoji = flag?.value ?? '%F0%9F%92%AF';
- for (const [index, task] of tasks.entries()) {
- let config = task();
+ // Получаем конфиг или пустой массив
+ const inboundsConfig = sub.inboundsConfig || [];
+ for (const config of inboundsConfig) {
+ const type = config.type;
+ const uuid = uuidv4();
+
+ // Определяем SNI
+ let sni = 'unknown';
+ if (type !== 'custom' && type !== 'vmess-tcp' && type !== 'shadowsocks-tcp') {
+ sni = (!config.sni || config.sni === 'random') ? this.pickDomain(domains) : config.sni;
+ }
+
+ // === 1. Обработка Custom ===
+ if (type === 'custom') {
+ const newInbound = this.inboundRepo.create({
+ xuiId: 0, // Не привязано к 3x-ui
+ port: 0,
+ protocol: 'custom',
+ remark: 'custom-link',
+ link: config.link || '',
+ subscription: sub
+ });
+ await this.inboundRepo.save(newInbound);
+ continue;
+ }
+
+ // === 2. Обработка Hysteria2 ===
+ if (type === 'hysteria2-udp') {
+ const link = this.inboundBuilder.buildHysteria2Link(serverAddress, sni, 'Hysteria2');
+ const newInbound = this.inboundRepo.create({
+ xuiId: 0,
+ port: 0, // Обычно Hysteria висит на 443, фактический порт вытаскивается в билдере
+ protocol: 'hysteria2',
+ remark: 'hysteria2-udp',
+ link: link,
+ subscription: sub
+ });
+ await this.inboundRepo.save(newInbound);
+ continue;
+ }
+
+ // === 3. Обработка стандартных инбаундов Xray (3x-ui) ===
+
+ // Определяем порт
let port = 0;
- if (index === 0) port = await this.getFreePort(8443, usedPorts);
- else if (index === 1) port = await this.getFreePort(443, usedPorts);
- else port = await this.getFreePort(0, usedPorts);
-
- config.port = port;
+ if (config.port === 'random' || !config.port) {
+ port = await this.getFreePort(0, usedPorts);
+ } else {
+ // Если передан конкретный порт строкой или числом
+ port = typeof config.port === 'string' ? parseInt(config.port, 10) : config.port;
+ }
usedPorts.add(port);
- const xuiId = await this.xuiService.addInbound(config);
+ let xuiConfig: any;
+
+ switch (type) {
+ case 'vless-tcp-reality':
+ xuiConfig = this.inboundBuilder.buildVlessRealityTcp({ port, uuid, sni, ...keys });
+ break;
+ case 'vless-xhttp-reality':
+ xuiConfig = this.inboundBuilder.buildVlessRealityXhttp({ port, uuid, sni, ...keys });
+ break;
+ case 'vless-grpc-reality':
+ xuiConfig = this.inboundBuilder.buildVlessRealityGrpc({ port, uuid, sni, ...keys });
+ break;
+ case 'vless-ws':
+ xuiConfig = this.inboundBuilder.buildVlessWs({ port, uuid, sni });
+ break;
+ case 'vmess-tcp':
+ xuiConfig = this.inboundBuilder.buildVmessTcp({ port, uuid });
+ break;
+ case 'shadowsocks-tcp':
+ xuiConfig = this.inboundBuilder.buildShadowsocksTcp({ port, uuid });
+ break;
+ case 'trojan-tcp-reality':
+ xuiConfig = this.inboundBuilder.buildTrojanRealityTcp({ port, uuid, sni, ...keys });
+ break;
+ default:
+ this.logger.warn(`Неизвестный тип инбаунда: ${type}`);
+ continue;
+ }
+
+ const xuiId = await this.xuiService.addInbound(xuiConfig);
if (xuiId) {
- const remarkParts = config.remark.split('-');
- let domainForLink = 'unknown';
- try {
- const ss = JSON.parse(config.streamSettings || '{}');
- if (ss.realitySettings?.serverNames?.[0]) domainForLink = ss.realitySettings.serverNames[0];
- else if (ss.wsSettings?.headers?.Host) domainForLink = ss.wsSettings.headers.Host;
- else if (ss.tcpSettings?.header?.request?.headers?.Host?.[0]) domainForLink = ss.tcpSettings.header.request.headers.Host[0];
- } catch (e) { }
- const idOrPass = config.settings ? JSON.parse(config.settings).clients?.[0]?.id || JSON.parse(config.settings).clients?.[0]?.password : "";
- const fullLink = this.inboundBuilder.buildInboundLink(config, serverAddress, idOrPass, flagEmoji);
+ const idOrPass = xuiConfig.settings ? JSON.parse(xuiConfig.settings).clients?.[0]?.id || JSON.parse(xuiConfig.settings).clients?.[0]?.password : "";
+ const fullLink = this.inboundBuilder.buildInboundLink(xuiConfig, serverAddress, idOrPass, flagEmoji);
const newInbound = this.inboundRepo.create({
xuiId: xuiId,
port: port,
- protocol: config.protocol,
- remark: config.remark,
+ protocol: xuiConfig.protocol,
+ remark: xuiConfig.remark,
link: fullLink,
subscription: sub
});
@@ -175,6 +228,7 @@ export class RotationService implements OnModuleInit {
}
}
}
+
private pickDomain(list: Domain[]): string {
return list[Math.floor(Math.random() * list.length)].name;
}
diff --git a/server/src/settings/settings.controller.ts b/server/src/settings/settings.controller.ts
index 849380e..199e8bd 100644
--- a/server/src/settings/settings.controller.ts
+++ b/server/src/settings/settings.controller.ts
@@ -2,6 +2,7 @@ import { Controller, Get, Post, Body } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Setting } from './entities/setting.entity';
+import * as net from 'net';
import * as dns from 'dns/promises';
import { COUNTRIES } from './countries';
import { XuiService } from 'src/xui/xui.service';
@@ -32,8 +33,14 @@ export class SettingsController {
try {
const parsed = new URL(settings.xui_url);
settings['xui_host'] = parsed.hostname;
-
- const { address } = await dns.lookup(parsed.hostname);
+
+ let address = '';
+ if (net.isIP(parsed.hostname) === 0) {
+ const result = await dns.lookup(parsed.hostname);
+ address = result.address;
+ } else {
+ address = parsed.hostname;
+ }
settings['xui_ip'] = address;
console.log(`Extracted host: ${parsed.hostname} from ${settings.xui_url}`);
diff --git a/server/src/subscriptions/dto/create-subscription.dto.ts b/server/src/subscriptions/dto/create-subscription.dto.ts
index ccb0f5a..dfb996c 100644
--- a/server/src/subscriptions/dto/create-subscription.dto.ts
+++ b/server/src/subscriptions/dto/create-subscription.dto.ts
@@ -11,6 +11,10 @@ export class InboundConfigDto {
@IsString()
@IsOptional()
sni?: string | 'random';
+
+ @IsString()
+ @IsOptional()
+ link?: string;
}
export class CreateSubscriptionDto {
diff --git a/server/src/subscriptions/entities/subscription.entity.ts b/server/src/subscriptions/entities/subscription.entity.ts
index 737ab06..41a3eea 100644
--- a/server/src/subscriptions/entities/subscription.entity.ts
+++ b/server/src/subscriptions/entities/subscription.entity.ts
@@ -15,6 +15,9 @@ export class Subscription {
@Column({ default: true })
isEnabled: boolean;
+ @Column({ type: 'simple-json', nullable: true })
+ inboundsConfig: any[];
+
@OneToMany(() => Inbound, (inbound) => inbound.subscription)
inbounds: Inbound[];
diff --git a/server/src/subscriptions/subscriptions.controller.ts b/server/src/subscriptions/subscriptions.controller.ts
index 812600f..c122a02 100644
--- a/server/src/subscriptions/subscriptions.controller.ts
+++ b/server/src/subscriptions/subscriptions.controller.ts
@@ -1,5 +1,6 @@
-import { Controller, Get, Post, Delete, Body, Param } from '@nestjs/common';
+import { Controller, Get, Post, Delete, Body, Param, Put } from '@nestjs/common';
import { SubscriptionsService } from './subscriptions.service';
+import { CreateSubscriptionDto } from './dto/create-subscription.dto';
@Controller('subscriptions')
export class SubscriptionsController {
@@ -11,8 +12,13 @@ export class SubscriptionsController {
}
@Post()
- create(@Body('name') name: string) {
- return this.subscriptionsService.create(name);
+ create(@Body() createSubscriptionDto: CreateSubscriptionDto) {
+ return this.subscriptionsService.create(createSubscriptionDto);
+ }
+
+ @Put(':id')
+ update(@Param('id') id: string, @Body() updateSubscriptionDto: CreateSubscriptionDto) {
+ return this.subscriptionsService.update(id, updateSubscriptionDto);
}
@Delete(':id')
diff --git a/server/src/subscriptions/subscriptions.service.ts b/server/src/subscriptions/subscriptions.service.ts
index 0c1528a..fa7a8fe 100644
--- a/server/src/subscriptions/subscriptions.service.ts
+++ b/server/src/subscriptions/subscriptions.service.ts
@@ -1,9 +1,9 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Subscription } from './entities/subscription.entity';
-import { Inbound } from '../inbounds/entities/inbound.entity';
import { XuiService } from '../xui/xui.service';
+import { CreateSubscriptionDto } from './dto/create-subscription.dto';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
@@ -18,11 +18,32 @@ export class SubscriptionsService {
return this.subRepo.find({ relations: ['inbounds'], order: { createdAt: 'DESC' } });
}
- async create(name: string) {
+ async create(dto: CreateSubscriptionDto) {
const sub = this.subRepo.create({
- name,
+ name: dto.name,
uuid: uuidv4(),
+ inboundsConfig: dto.inboundsConfig || [],
});
+
+ return this.subRepo.save(sub);
+ }
+
+ async update(id: string, dto: CreateSubscriptionDto) {
+ const sub = await this.subRepo.findOne({
+ where: { id },
+ relations: ['inbounds']
+ });
+
+ if (!sub) {
+ throw new NotFoundException(`Subscription with ID ${id} not found`);
+ }
+
+ sub.name = dto.name;
+
+ if (dto.inboundsConfig) {
+ sub.inboundsConfig = dto.inboundsConfig;
+ }
+
return this.subRepo.save(sub);
}
@@ -30,7 +51,7 @@ export class SubscriptionsService {
const sub = await this.subRepo.findOne({ where: { id }, relations: ['inbounds'] });
if (!sub) return;
- if (sub.inbounds) {
+ if (sub.inbounds && sub.inbounds.length > 0) {
for (const inbound of sub.inbounds) {
await this.xuiService.deleteInbound(inbound.xuiId);
}
diff --git a/server/src/tunnels/entities/tunnel.entity.ts b/server/src/tunnels/entities/tunnel.entity.ts
index c8a1821..53e61e4 100644
--- a/server/src/tunnels/entities/tunnel.entity.ts
+++ b/server/src/tunnels/entities/tunnel.entity.ts
@@ -17,8 +17,11 @@ export class Tunnel {
@Column()
username: string;
- @Column({ select: false })
- password: string;
+ @Column({ select: false, nullable: true })
+ password?: string;
+
+ @Column({ type: 'text', select: false, nullable: true })
+ privateKey?: string;
@Column({ nullable: true })
domain: string;
diff --git a/server/src/tunnels/ssh.service.ts b/server/src/tunnels/ssh.service.ts
index ec677c8..2240a3f 100644
--- a/server/src/tunnels/ssh.service.ts
+++ b/server/src/tunnels/ssh.service.ts
@@ -6,7 +6,7 @@ export class SshService {
private readonly logger = new Logger(SshService.name);
async executeCommand(
- config: { host: string; port: number; username: string; password?: string },
+ config: { host: string; port: number; username: string; password?: string, privateKey?: string },
command: string
): Promise {
return new Promise((resolve, reject) => {
@@ -42,6 +42,7 @@ export class SshService {
port: config.port,
username: config.username,
password: config.password,
+ privateKey: config.privateKey,
readyTimeout: 20000,
});
});
diff --git a/server/src/tunnels/tunnels.service.ts b/server/src/tunnels/tunnels.service.ts
index feadc2a..fed287e 100644
--- a/server/src/tunnels/tunnels.service.ts
+++ b/server/src/tunnels/tunnels.service.ts
@@ -31,6 +31,7 @@ export class TunnelsService {
async installScript(id: number) {
const tunnel = await this.tunnelRepo.createQueryBuilder('tunnel')
.addSelect('tunnel.password')
+ .addSelect('tunnel.privateKey')
.where('tunnel.id = :id', { id })
.getOne();
@@ -55,7 +56,8 @@ export class TunnelsService {
host: tunnel.ip,
port: tunnel.sshPort,
username: tunnel.username,
- password: tunnel.password
+ password: tunnel.password,
+ privateKey: tunnel.privateKey
}, command);
this.logger.log(`Скрипт выполнен успешно:\n${output}`);