diff --git a/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx b/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx index df2260bc..ef6af2b5 100644 --- a/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx +++ b/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx @@ -36,18 +36,11 @@ import { type OutboundFormValues, } from '@/schemas/forms/outbound-form'; import { - ALPN_OPTION, - Address_Port_Strategy, DNSRuleActions, DOMAIN_STRATEGY_OPTION, - MODE_OPTION, OutboundDomainStrategies, - OutboundProtocols as Protocols, SNIFFING_OPTION, TCP_CONGESTION_OPTION, - TLS_FLOW_CONTROL, - USERS_SECURITY, - UTLS_FINGERPRINT, WireguardDomainStrategy, } from '@/schemas/primitives'; import { @@ -62,6 +55,26 @@ import { } from '@/lib/xray/protocol-capabilities'; import { SSMethodSchema } from '@/schemas/protocols/shared/shadowsocks'; import { antdRule } from '@/utils/zodForm'; + +import { + ADDRESS_PORT_STRATEGY_OPTIONS, + ALPN_OPTIONS, + FLOW_OPTIONS, + HYSTERIA_NETWORK_OPTION, + MODE_OPTIONS, + NETWORK_OPTIONS, + PROTOCOL_OPTIONS, + SECURITY_OPTIONS, + SERVER_PROTOCOLS, + SS_METHOD_OPTIONS, + UTLS_OPTIONS, +} from './outbound-form-constants'; +import { + buildAddModeValues, + hysteriaStreamSlice, + isMuxAllowed, + newStreamSlice, +} from './outbound-form-helpers'; import './OutboundFormModal.css'; // Pattern A rewrite of OutboundFormModal. Built as a sibling `.new.tsx` @@ -77,116 +90,6 @@ interface OutboundFormModalProps { onConfirm: (outbound: Record) => void; } -const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p })); -const SECURITY_OPTIONS = Object.values(USERS_SECURITY).map((v) => ({ value: v, label: v })); -const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL).map((v) => ({ value: v, label: v })); -const SS_METHOD_OPTIONS = SSMethodSchema.options.map((v) => ({ value: v, label: v })); -const MODE_OPTIONS = Object.values(MODE_OPTION).map((v) => ({ value: v, label: v })); -const UTLS_OPTIONS = Object.values(UTLS_FINGERPRINT).map((v) => ({ value: v, label: v })); -const ALPN_OPTIONS = Object.values(ALPN_OPTION).map((v) => ({ value: v, label: v })); -const ADDRESS_PORT_STRATEGY_OPTIONS = Object.values(Address_Port_Strategy).map((v) => ({ - value: v, - label: v, -})); - -// canEnableMux mirrors the adapter's helper but lives here so the modal -// can show/hide the Mux section without going through the adapter. -const MUX_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'http', 'socks']); -function isMuxAllowed(protocol: string, flow: string, network: string): boolean { - if (!MUX_PROTOCOLS.has(protocol)) return false; - if (protocol === 'vless' && flow) return false; - if (network === 'xhttp') return false; - return true; -} - -const NETWORK_OPTIONS: { value: string; label: string }[] = [ - { value: 'tcp', label: 'RAW' }, - { value: 'kcp', label: 'mKCP' }, - { value: 'ws', label: 'WebSocket' }, - { value: 'grpc', label: 'gRPC' }, - { value: 'httpupgrade', label: 'HTTPUpgrade' }, - { value: 'xhttp', label: 'XHTTP' }, -]; - -// The hysteria protocol is locked to its own QUIC transport: the selector -// shows only this option when the parent protocol is hysteria. -const HYSTERIA_NETWORK_OPTION = { value: 'hysteria', label: 'Hysteria' }; - -// Per-network bootstrap. Mirrors the legacy class constructors so the -// initial state for each transport matches what xray-core expects. -function newStreamSlice(network: string): Record { - switch (network) { - case 'tcp': - return { network: 'tcp', tcpSettings: { header: { type: 'none' } } }; - case 'kcp': - return { - network: 'kcp', - kcpSettings: { - mtu: 1350, tti: 20, uplinkCapacity: 5, downlinkCapacity: 20, - cwndMultiplier: 1, maxSendingWindow: 2097152, - }, - }; - case 'ws': - return { - network: 'ws', - wsSettings: { path: '/', host: '', headers: {}, heartbeatPeriod: 0 }, - }; - case 'grpc': - return { - network: 'grpc', - grpcSettings: { serviceName: '', authority: '', multiMode: false }, - }; - case 'httpupgrade': - return { - network: 'httpupgrade', - httpupgradeSettings: { path: '/', host: '', headers: {} }, - }; - case 'xhttp': - return { - network: 'xhttp', - xhttpSettings: { - path: '/', host: '', mode: '', headers: [], - xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000', - }, - }; - case 'hysteria': - return { - network: 'hysteria', - hysteriaSettings: { - version: 2, - auth: '', - udpIdleTimeout: 60, - }, - }; - default: - return { network: 'tcp', tcpSettings: { header: { type: 'none' } } }; - } -} - -// Hysteria2 always rides its own QUIC transport with TLS — the panel never -// offers another transport or 'none' security for it. -function hysteriaStreamSlice(): Record { - return { - ...newStreamSlice('hysteria'), - security: 'tls', - tlsSettings: { - serverName: '', alpn: ['h3'], fingerprint: '', - echConfigList: '', verifyPeerCertByName: '', pinnedPeerCertSha256: '', - }, - }; -} - -// Protocols whose form schema carries a flat connect target — these all -// get the shared "server" sub-block (address + port) at the top of the -// protocol section. Wireguard has an address but no port. DNS/freedom/ -// blackhole/loopback have no connect target. -const SERVER_PROTOCOLS = new Set([ - 'vmess', 'vless', 'trojan', 'shadowsocks', 'socks', 'http', 'hysteria', -]); - -function buildAddModeValues(): OutboundFormValues { - return rawOutboundToFormValues({}); -} export default function OutboundFormModal({ open, diff --git a/frontend/src/pages/xray/outbounds/outbound-form-constants.ts b/frontend/src/pages/xray/outbounds/outbound-form-constants.ts new file mode 100644 index 00000000..291d2bc9 --- /dev/null +++ b/frontend/src/pages/xray/outbounds/outbound-form-constants.ts @@ -0,0 +1,47 @@ +import { + ALPN_OPTION, + Address_Port_Strategy, + MODE_OPTION, + OutboundProtocols as Protocols, + TLS_FLOW_CONTROL, + USERS_SECURITY, + UTLS_FINGERPRINT, +} from '@/schemas/primitives'; +import { SSMethodSchema } from '@/schemas/protocols/shared/shadowsocks'; + +export const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p })); +export const SECURITY_OPTIONS = Object.values(USERS_SECURITY).map((v) => ({ value: v, label: v })); +export const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL).map((v) => ({ value: v, label: v })); +export const SS_METHOD_OPTIONS = SSMethodSchema.options.map((v) => ({ value: v, label: v })); +export const MODE_OPTIONS = Object.values(MODE_OPTION).map((v) => ({ value: v, label: v })); +export const UTLS_OPTIONS = Object.values(UTLS_FINGERPRINT).map((v) => ({ value: v, label: v })); +export const ALPN_OPTIONS = Object.values(ALPN_OPTION).map((v) => ({ value: v, label: v })); +export const ADDRESS_PORT_STRATEGY_OPTIONS = Object.values(Address_Port_Strategy).map((v) => ({ + value: v, + label: v, +})); + +// canEnableMux mirrors the adapter's helper but lives here so the modal +// can show/hide the Mux section without going through the adapter. +export const MUX_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'http', 'socks']); + +export const NETWORK_OPTIONS: { value: string; label: string }[] = [ + { value: 'tcp', label: 'RAW' }, + { value: 'kcp', label: 'mKCP' }, + { value: 'ws', label: 'WebSocket' }, + { value: 'grpc', label: 'gRPC' }, + { value: 'httpupgrade', label: 'HTTPUpgrade' }, + { value: 'xhttp', label: 'XHTTP' }, +]; + +// The hysteria protocol is locked to its own QUIC transport: the selector +// shows only this option when the parent protocol is hysteria. +export const HYSTERIA_NETWORK_OPTION = { value: 'hysteria', label: 'Hysteria' }; + +// Protocols whose form schema carries a flat connect target — these all +// get the shared "server" sub-block (address + port) at the top of the +// protocol section. Wireguard has an address but no port. DNS/freedom/ +// blackhole/loopback have no connect target. +export const SERVER_PROTOCOLS = new Set([ + 'vmess', 'vless', 'trojan', 'shadowsocks', 'socks', 'http', 'hysteria', +]); diff --git a/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts b/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts new file mode 100644 index 00000000..be4475be --- /dev/null +++ b/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts @@ -0,0 +1,79 @@ +import { rawOutboundToFormValues } from '@/lib/xray/outbound-form-adapter'; +import type { OutboundFormValues } from '@/schemas/forms/outbound-form'; + +import { MUX_PROTOCOLS } from './outbound-form-constants'; + +export function isMuxAllowed(protocol: string, flow: string, network: string): boolean { + if (!MUX_PROTOCOLS.has(protocol)) return false; + if (protocol === 'vless' && flow) return false; + if (network === 'xhttp') return false; + return true; +} + +// Per-network bootstrap. Mirrors the legacy class constructors so the +// initial state for each transport matches what xray-core expects. +export function newStreamSlice(network: string): Record { + switch (network) { + case 'tcp': + return { network: 'tcp', tcpSettings: { header: { type: 'none' } } }; + case 'kcp': + return { + network: 'kcp', + kcpSettings: { + mtu: 1350, tti: 20, uplinkCapacity: 5, downlinkCapacity: 20, + cwndMultiplier: 1, maxSendingWindow: 2097152, + }, + }; + case 'ws': + return { + network: 'ws', + wsSettings: { path: '/', host: '', headers: {}, heartbeatPeriod: 0 }, + }; + case 'grpc': + return { + network: 'grpc', + grpcSettings: { serviceName: '', authority: '', multiMode: false }, + }; + case 'httpupgrade': + return { + network: 'httpupgrade', + httpupgradeSettings: { path: '/', host: '', headers: {} }, + }; + case 'xhttp': + return { + network: 'xhttp', + xhttpSettings: { + path: '/', host: '', mode: '', headers: [], + xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000', + }, + }; + case 'hysteria': + return { + network: 'hysteria', + hysteriaSettings: { + version: 2, + auth: '', + udpIdleTimeout: 60, + }, + }; + default: + return { network: 'tcp', tcpSettings: { header: { type: 'none' } } }; + } +} + +// Hysteria2 always rides its own QUIC transport with TLS — the panel never +// offers another transport or 'none' security for it. +export function hysteriaStreamSlice(): Record { + return { + ...newStreamSlice('hysteria'), + security: 'tls', + tlsSettings: { + serverName: '', alpn: ['h3'], fingerprint: '', + echConfigList: '', verifyPeerCertByName: '', pinnedPeerCertSha256: '', + }, + }; +} + +export function buildAddModeValues(): OutboundFormValues { + return rawOutboundToFormValues({}); +}