5 Commits

Author SHA1 Message Date
Den Piligrim 1b5ad11185 Merge branch 'main' into test 2026-06-11 11:16:07 +03:00
DenPiligrim 2e1cd86e99 Merge pull request #53 from zangetsuka/feature/happ-routing
Feature/happ routing
2026-06-04 10:48:09 +03:00
root 97a8c41fc0 chore: update install.sh to use zangetsuka images with happ-routing tag 2026-06-01 12:57:42 +03:00
root b2cc357dca feat: add Happ routing support to subscriptions
- Add routingProfile field to InboundConfigDto
- Update Subscription entity type
- Add happ-routing handler in RotationService
- Add UI for happ-routing inbound type in SubscriptionsPage
2026-06-01 12:53:46 +03:00
root fc8fbc976f feat: add Happ routing support to subscriptions
- Add routingProfile field to InboundConfigDto
- Update Subscription entity type
- Add happ-routing handler in RotationService
- Add UI for happ-routing inbound type in SubscriptionsPage
2026-06-01 12:40:51 +03:00
5 changed files with 79 additions and 37 deletions
+69 -34
View File
@@ -83,6 +83,7 @@ interface InboundConfigUI {
name?: string; name?: string;
certificateFile?: string; certificateFile?: string;
keyFile?: string; keyFile?: string;
routingProfile?: string;
} }
interface Domain { interface Domain {
@@ -106,6 +107,7 @@ const CONNECTION_OPTIONS = [
'shadowsocks-tcp', 'shadowsocks-tcp',
'trojan-tcp-reality', 'trojan-tcp-reality',
'custom', 'custom',
'happ-routing',
]; ];
const generateId = () => Math.random().toString(36).substring(7); const generateId = () => Math.random().toString(36).substring(7);
@@ -221,17 +223,30 @@ export default function SubscriptionsPage() {
const createInbound = (type = 'vless-tcp-reality'): InboundConfigUI => { const createInbound = (type = 'vless-tcp-reality'): InboundConfigUI => {
const nodeId = getDefaultNodeId(); const nodeId = getDefaultNodeId();
// HAPP ROUTING - специальная обработка
if (type === 'happ-routing') {
return {
id: generateId(),
type,
port: '0',
sni: '',
link: '',
routingProfile: '',
};
}
const certDefaults = type === 'hysteria2-udp' ? getHysteriaCertDefaults(nodeId) : {}; const certDefaults = type === 'hysteria2-udp' ? getHysteriaCertDefaults(nodeId) : {};
return { return {
id: generateId(), id: generateId(),
type, type,
port: 'random', port: 'random',
sni: type === 'hysteria2-udp' ? '' : 'random', sni: type === 'hysteria2-udp' ? '' : 'random',
link: '', link: '',
nodeId, nodeId,
flag: getNodeFlag(nodeId), flag: getNodeFlag(nodeId),
name: '', name: '',
...certDefaults, ...certDefaults,
}; };
}; };
@@ -280,6 +295,7 @@ export default function SubscriptionsPage() {
relayServerId: item.relayServerId ? item.relayServerId.toString() : '', relayServerId: item.relayServerId ? item.relayServerId.toString() : '',
flag: item.flag || getNodeFlag(nodeId), flag: item.flag || getNodeFlag(nodeId),
name: item.name || '', name: item.name || '',
routingProfile: item.routingProfile || '',
certificateFile: certificateFile:
item.type === 'hysteria2-udp' item.type === 'hysteria2-udp'
? item.certificateFile || certDefaults.certificateFile ? item.certificateFile || certDefaults.certificateFile
@@ -323,7 +339,7 @@ export default function SubscriptionsPage() {
next.keyFile = next.keyFile || defaults.keyFile; next.keyFile = next.keyFile || defaults.keyFile;
} }
if (field === 'type' && value !== 'custom' && !next.nodeId) { if (field === 'type' && value !== 'custom' && value !== 'happ-routing' && !next.nodeId) {
next.nodeId = getDefaultNodeId(); next.nodeId = getDefaultNodeId();
next.flag = getNodeFlag(next.nodeId); next.flag = getNodeFlag(next.nodeId);
} }
@@ -363,7 +379,7 @@ export default function SubscriptionsPage() {
const handleSave = async () => { const handleSave = async () => {
const nextPortErrors = inbounds.reduce<Record<string, string>>((acc, inbound) => { const nextPortErrors = inbounds.reduce<Record<string, string>>((acc, inbound) => {
if (inbound.type !== 'custom' && !isValidPort(inbound.port)) { if (inbound.type !== 'custom' && inbound.type !== 'happ-routing' && !isValidPort(inbound.port)) {
acc[inbound.id] = 'Порт: число 1-65535 или random'; acc[inbound.id] = 'Порт: число 1-65535 или random';
} }
return acc; return acc;
@@ -385,29 +401,36 @@ export default function SubscriptionsPage() {
const payload = { const payload = {
name, name,
inboundsConfig: inbounds.map((inbound) => inboundsConfig: inbounds.map((inbound) => {
inbound.type === 'custom' if (inbound.type === 'custom') {
? { type: inbound.type, link: inbound.link } return { type: inbound.type, link: inbound.link };
: { }
type: inbound.type, if (inbound.type === 'happ-routing') {
port: inbound.port === 'random' ? 'random' : parseInt(inbound.port, 10), return {
sni: inbound.type === 'hysteria2-udp' ? undefined : inbound.sni, type: inbound.type,
nodeId: inbound.nodeId || undefined, routingProfile: inbound.routingProfile,
relayServerId: inbound.relayServerId };
? parseInt(inbound.relayServerId, 10) }
: undefined, return {
flag: inbound.flag || getNodeFlag(inbound.nodeId) || undefined, type: inbound.type,
name: inbound.name?.trim() || undefined, port: inbound.port === 'random' ? 'random' : parseInt(inbound.port, 10),
certificateFile: sni: inbound.type === 'hysteria2-udp' ? undefined : inbound.sni,
inbound.type === 'hysteria2-udp' nodeId: inbound.nodeId || undefined,
? inbound.certificateFile?.trim() || undefined relayServerId: inbound.relayServerId
: undefined, ? parseInt(inbound.relayServerId, 10)
keyFile: : undefined,
inbound.type === 'hysteria2-udp' flag: inbound.flag || getNodeFlag(inbound.nodeId) || undefined,
? inbound.keyFile?.trim() || undefined name: inbound.name?.trim() || undefined,
: undefined, certificateFile:
}, inbound.type === 'hysteria2-udp'
), ? inbound.certificateFile?.trim() || undefined
: undefined,
keyFile:
inbound.type === 'hysteria2-udp'
? inbound.keyFile?.trim() || undefined
: undefined,
};
}),
}; };
try { try {
@@ -735,6 +758,18 @@ export default function SubscriptionsPage() {
</FormControl> </FormControl>
{inbound.type === 'custom' ? ( {inbound.type === 'custom' ? (
<TextField size="small" label="Ссылка на подключение" placeholder="vless://..." value={inbound.link || ''} onChange={(e) => handleInboundChange(inbound.id, 'link', e.target.value)} sx={{ width: 460, flexShrink: 0 }} /> <TextField size="small" label="Ссылка на подключение" placeholder="vless://..." value={inbound.link || ''} onChange={(e) => handleInboundChange(inbound.id, 'link', e.target.value)} sx={{ width: 460, flexShrink: 0 }} />
) : inbound.type === 'happ-routing' ? (
<TextField
size="small"
label="Routing профиль (JSON или base64)"
placeholder='{"Name":"MyProfile","GlobalProxy":"true","DirectSites":["geosite:cn"]}'
value={inbound.routingProfile || ''}
onChange={(e) => handleInboundChange(inbound.id, 'routingProfile', e.target.value)}
multiline
rows={4}
sx={{ width: 460, flexShrink: 0 }}
helperText="Вставьте JSON профиль или готовую ссылку happ://routing/..."
/>
) : ( ) : (
<> <>
<FormControl size="small" sx={{ width: 170, flexShrink: 0 }}> <FormControl size="small" sx={{ width: 170, flexShrink: 0 }}>
+2 -2
View File
@@ -5,8 +5,8 @@ set -euo pipefail
# КОНФИГУРАЦИЯ И ПЕРЕМЕННЫЕ # КОНФИГУРАЦИЯ И ПЕРЕМЕННЫЕ
################################# #################################
PROJECT_DIR="/opt/3dp-manager" PROJECT_DIR="/opt/3dp-manager"
DOCKER_USER="denpiligrim" DOCKER_USER="zangetsuka"
DOCKER_TAG="main" DOCKER_TAG="feature-happ-routing"
IMAGE_SERVER="ghcr.io/${DOCKER_USER}/3dp-manager-server:${DOCKER_TAG}" IMAGE_SERVER="ghcr.io/${DOCKER_USER}/3dp-manager-server:${DOCKER_TAG}"
IMAGE_CLIENT="ghcr.io/${DOCKER_USER}/3dp-manager-client:${DOCKER_TAG}" IMAGE_CLIENT="ghcr.io/${DOCKER_USER}/3dp-manager-client:${DOCKER_TAG}"
+1
View File
@@ -257,6 +257,7 @@ export class RotationService implements OnModuleInit {
sni = config.sni === 'random' ? this.pickDomain(domains) : config.sni; sni = config.sni === 'random' ? this.pickDomain(domains) : config.sni;
} }
// === 2. Обработка Hysteria2 === // === 2. Обработка Hysteria2 ===
if (type === 'hysteria2-udp') { if (type === 'hysteria2-udp') {
let port = 0; let port = 0;
@@ -82,6 +82,11 @@ export class InboundConfigDto {
@IsString() @IsString()
@IsOptional() @IsOptional()
keyFile?: string; keyFile?: string;
// ⬇️ НОВОЕ ПОЛЕ ДЛЯ HAPP ROUTING ⬇️
@IsString()
@IsOptional()
routingProfile?: string;
} }
export class CreateSubscriptionDto { export class CreateSubscriptionDto {
@@ -40,6 +40,7 @@ export class Subscription {
name?: string; name?: string;
certificateFile?: string; certificateFile?: string;
keyFile?: string; keyFile?: string;
routingProfile?: string;
}>; }>;
@Column({ nullable: true }) @Column({ nullable: true })