| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371 |
- <script setup>
- import { computed, ref, watch } from 'vue';
- import { useI18n } from 'vue-i18n';
- import dayjs from 'dayjs';
- import { message } from 'ant-design-vue';
- import { SyncOutlined, PlusOutlined, MinusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
- import {
- HttpUtil,
- RandomUtil,
- NumberFormatter,
- SizeFormatter,
- Wireguard,
- } from '@/utils';
- import { getRandomRealityTarget } from '@/models/reality-targets';
- import {
- Inbound,
- Protocols,
- SSMethods,
- USERS_SECURITY,
- TLS_FLOW_CONTROL,
- SNIFFING_OPTION,
- TLS_VERSION_OPTION,
- TLS_CIPHER_OPTION,
- UTLS_FINGERPRINT,
- ALPN_OPTION,
- USAGE_OPTION,
- DOMAIN_STRATEGY_OPTION,
- TCP_CONGESTION_OPTION,
- MODE_OPTION,
- } from '@/models/inbound.js';
- import { DBInbound } from '@/models/dbinbound.js';
- import FinalMaskForm from '@/components/FinalMaskForm.vue';
- import DateTimePicker from '@/components/DateTimePicker.vue';
- import JsonEditor from '@/components/JsonEditor.vue';
- import { useNodeList } from '@/composables/useNodeList.js';
- const { t } = useI18n();
- // Node selector — Phase 1 multi-node deployment. Shows all enabled
- // nodes regardless of online state so the form is usable while a node
- // is briefly offline; the backend's fail-fast path will surface the
- // real error when the user submits.
- const { nodes: availableNodes } = useNodeList();
- const selectableNodes = computed(() => (availableNodes.value || []).filter((n) => n.enable));
- // Phase 5f-iii-b: structured per-protocol/per-transport forms instead
- // of raw JSON textareas. Edits a deeply-reactive Inbound + DBInbound
- // pair so the existing model helpers (.toString(), .canEnableTls(),
- // genAllLinks(), addPeer(), etc.) keep working unchanged. The
- // "Advanced" tab still exposes the full streamSettings JSON for
- // transport variants (KCP/XHTTP/sockopt/finalmask) we don't yet have
- // dedicated UI for.
- const props = defineProps({
- open: { type: Boolean, default: false },
- mode: { type: String, default: 'add', validator: (v) => ['add', 'edit'].includes(v) },
- dbInbound: { type: Object, default: null },
- });
- const emit = defineEmits(['update:open', 'saved']);
- const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'];
- const PROTOCOLS = Object.values(Protocols);
- const SECURITY_OPTIONS = Object.values(USERS_SECURITY);
- const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
- // === Reactive state ================================================
- // Cloned on every open so cancelling the modal doesn't mutate the row.
- const inbound = ref(null);
- const dbForm = ref(null);
- const saving = ref(false);
- const advancedStreamText = ref('');
- const advancedSniffingText = ref('');
- const advancedSettingsText = ref('');
- const activeTabKey = ref('basic');
- const advancedSectionKey = ref('all');
- // Cached default cert/key paths from /panel/setting/defaultSettings —
- // powers the "Set default cert" button on the TLS form.
- const defaultCert = ref('');
- const defaultKey = ref('');
- // Lookup tables for the option dropdowns.
- const TLS_VERSIONS = Object.values(TLS_VERSION_OPTION);
- const CIPHER_SUITES = Object.entries(TLS_CIPHER_OPTION); // [label, value]
- const FINGERPRINTS = Object.values(UTLS_FINGERPRINT);
- const ALPNS = Object.values(ALPN_OPTION);
- const USAGES = Object.values(USAGE_OPTION);
- const DOMAIN_STRATEGIES = Object.values(DOMAIN_STRATEGY_OPTION);
- const TCP_CONGESTIONS = Object.values(TCP_CONGESTION_OPTION);
- const MODE_OPTIONS = Object.values(MODE_OPTION);
- // External proxy is a single switch in the UI but a list in the model:
- // flipping it on seeds one row pre-filled with the current host:port.
- const externalProxy = computed({
- get: () => Array.isArray(inbound.value?.stream?.externalProxy)
- && inbound.value.stream.externalProxy.length > 0,
- set: (v) => {
- if (!inbound.value?.stream) return;
- if (v) {
- inbound.value.stream.externalProxy = [{
- forceTls: 'same',
- dest: window.location.hostname,
- port: inbound.value.port,
- remark: '',
- }];
- } else {
- inbound.value.stream.externalProxy = [];
- }
- },
- });
- // Derived helpers — each is a computed off `inbound` so flips of
- // protocol / network / security re-render the right blocks.
- const protocol = computed(() => inbound.value?.protocol);
- const network = computed({
- get: () => inbound.value?.stream?.network,
- set: (v) => onNetworkChange(v),
- });
- const security = computed({
- get: () => inbound.value?.stream?.security,
- set: (v) => { if (inbound.value?.stream) inbound.value.stream.security = v; },
- });
- const isMultiUser = computed(() => {
- if (!inbound.value) return false;
- switch (inbound.value.protocol) {
- case Protocols.VMESS:
- case Protocols.VLESS:
- case Protocols.TROJAN:
- case Protocols.HYSTERIA:
- return true;
- case Protocols.SHADOWSOCKS:
- return !!inbound.value.isSSMultiUser;
- default:
- return false;
- }
- });
- const clientsArray = computed(() => {
- if (!inbound.value) return [];
- switch (inbound.value.protocol) {
- case Protocols.VMESS: return inbound.value.settings.vmesses || [];
- case Protocols.VLESS: return inbound.value.settings.vlesses || [];
- case Protocols.TROJAN: return inbound.value.settings.trojans || [];
- case Protocols.SHADOWSOCKS: return inbound.value.settings.shadowsockses || [];
- case Protocols.HYSTERIA: return inbound.value.settings.hysterias || [];
- default: return [];
- }
- });
- const firstClient = computed(() => clientsArray.value[0] || null);
- const canEnableStream = computed(() => inbound.value?.canEnableStream?.() === true);
- const canEnableTls = computed(() => inbound.value?.canEnableTls?.() === true);
- const canEnableReality = computed(() => inbound.value?.canEnableReality?.() === true);
- const canEnableTlsFlow = computed(() => inbound.value?.canEnableTlsFlow?.() === true);
- // VLESS/Trojan TLS fallbacks — surfaced in the protocol tab when the
- // inbound is on TCP and (for VLESS) using no Xray-side encryption.
- const showFallbacks = computed(() => {
- if (!inbound.value) return false;
- if (inbound.value.stream?.network !== 'tcp') return false;
- if (inbound.value.protocol === Protocols.VLESS) {
- const enc = inbound.value.settings?.encryption;
- return !enc || enc === 'none';
- }
- return inbound.value.protocol === Protocols.TROJAN;
- });
- function addFallback() {
- inbound.value?.settings?.addFallback?.();
- }
- function delFallback(idx) {
- inbound.value?.settings?.delFallback?.(idx);
- }
- // Date / GB bridges (legacy used moment via _expiryTime; we go direct).
- const expiryDate = computed({
- get: () => (dbForm.value?.expiryTime > 0 ? dayjs(dbForm.value.expiryTime) : null),
- set: (next) => { if (dbForm.value) dbForm.value.expiryTime = next ? next.valueOf() : 0; },
- });
- const totalGB = computed({
- get: () => (dbForm.value?.total ? Math.round((dbForm.value.total / SizeFormatter.ONE_GB) * 100) / 100 : 0),
- set: (gb) => { if (dbForm.value) dbForm.value.total = NumberFormatter.toFixed((gb || 0) * SizeFormatter.ONE_GB, 0); },
- });
- // Client total/expiry bridges (only relevant in add mode for new clients)
- const clientExpiryDate = computed({
- get: () => (firstClient.value?.expiryTime > 0 ? dayjs(firstClient.value.expiryTime) : null),
- set: (next) => { if (firstClient.value) firstClient.value.expiryTime = next ? next.valueOf() : 0; },
- });
- const clientTotalGB = computed({
- get: () => firstClient.value?._totalGB ?? 0,
- set: (gb) => { if (firstClient.value) firstClient.value._totalGB = gb || 0; },
- });
- // === Open / state management =======================================
- function loadFromDbInbound(dbIn) {
- // Round-trip through Inbound.fromJson so subsequent edits get the
- // structured class hierarchy (StreamSettings, TLS, Reality, etc.).
- const parsed = Inbound.fromJson(dbIn.toInbound().toJson());
- inbound.value = parsed;
- // DBForm carries the persisted-fields the parsed Inbound doesn't:
- // remark, enable, total, expiryTime, trafficReset, etc.
- dbForm.value = new DBInbound(dbIn);
- primeAdvancedJson();
- }
- function makeFreshInbound(proto) {
- const ib = new Inbound();
- ib.protocol = proto;
- ib.settings = Inbound.Settings.getSettings(proto);
- ib.port = RandomUtil.randomInteger(10000, 60000);
- return ib;
- }
- function freshDbForm() {
- const next = new DBInbound();
- next.enable = true;
- next.remark = '';
- next.total = 0;
- next.expiryTime = 0;
- next.trafficReset = 'never';
- return next;
- }
- function primeAdvancedJson() {
- if (!inbound.value) return;
- // Only set stream text for protocols that support it
- if (canEnableStream.value) {
- try {
- advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
- } catch (_e) { /* keep prior text */ }
- } else {
- advancedStreamText.value = '{}';
- }
- try {
- advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
- } catch (_e) { /* keep prior text */ }
- try {
- advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
- } catch (_e) { /* keep prior text */ }
- }
- watch(() => props.open, (next) => {
- if (!next) return;
- if (props.mode === 'edit' && props.dbInbound) {
- loadFromDbInbound(props.dbInbound);
- } else {
- inbound.value = makeFreshInbound(Protocols.VLESS);
- dbForm.value = freshDbForm();
- primeAdvancedJson();
- }
- activeTabKey.value = 'basic';
- advancedSectionKey.value = 'all';
- fetchDefaultCertSettings();
- });
- function applyAdvancedJsonToBasic() {
- if (!inbound.value) return true;
- let parsedSettings;
- let parsedStream;
- let parsedSniffing;
- try {
- parsedSettings = advancedSettingsText.value.trim()
- ? JSON.parse(advancedSettingsText.value)
- : inbound.value.settings?.toJson?.();
- } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return false; }
- try {
- parsedStream = advancedStreamText.value.trim()
- ? JSON.parse(advancedStreamText.value)
- : inbound.value.stream?.toJson?.();
- } catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return false; }
- try {
- parsedSniffing = advancedSniffingText.value.trim()
- ? JSON.parse(advancedSniffingText.value)
- : inbound.value.sniffing?.toJson?.();
- } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return false; }
- try {
- inbound.value = Inbound.fromJson({
- port: inbound.value.port,
- listen: inbound.value.listen,
- protocol: inbound.value.protocol,
- settings: parsedSettings,
- streamSettings: parsedStream,
- tag: inbound.value.tag,
- sniffing: parsedSniffing,
- clientStats: inbound.value.clientStats,
- });
- } catch (e) {
- message.error(`Advanced JSON: ${e.message}`);
- return false;
- }
- return true;
- }
- let isRevertingTab = false;
- watch(activeTabKey, (next, prev) => {
- if (isRevertingTab) { isRevertingTab = false; return; }
- if (prev === 'advanced' && next !== 'advanced') {
- if (!applyAdvancedJsonToBasic()) {
- isRevertingTab = true;
- activeTabKey.value = 'advanced';
- }
- }
- });
- // In add mode, switching protocol restamps settings + re-syncs port.
- function onProtocolChange(next) {
- if (props.mode === 'edit' || !inbound.value) return;
- inbound.value.protocol = next;
- inbound.value.settings = Inbound.Settings.getSettings(next);
- primeAdvancedJson();
- }
- function onNetworkChange(next) {
- if (!inbound.value?.stream) return;
- inbound.value.stream.network = next;
- // Mirror legacy streamNetworkChange: clear flow when TLS/Reality
- // become unavailable; reset finalmask.udp when not KCP.
- if (!inbound.value.canEnableTls()) inbound.value.stream.security = 'none';
- if (!inbound.value.canEnableReality()) inbound.value.reality = false;
- if (
- inbound.value.protocol === Protocols.VLESS
- && !inbound.value.canEnableTlsFlow()
- && Array.isArray(inbound.value.settings.vlesses)
- ) {
- inbound.value.settings.vlesses.forEach((c) => { c.flow = ''; });
- }
- if (next !== 'kcp' && inbound.value.stream.finalmask) {
- inbound.value.stream.finalmask.udp = [];
- }
- }
- function parseAdvancedSliceOrFallback(rawText, fallbackValue) {
- if (!rawText?.trim()) return fallbackValue;
- return JSON.parse(rawText);
- }
- function unwrapWrappedObject(parsed, key) {
- if (
- parsed
- && typeof parsed === 'object'
- && !Array.isArray(parsed)
- && parsed[key] !== undefined
- ) {
- return parsed[key];
- }
- return parsed;
- }
- const advancedAllConfig = computed({
- get: () => {
- if (!inbound.value) return '';
- try {
- const settings = parseAdvancedSliceOrFallback(
- advancedSettingsText.value,
- inbound.value.settings?.toJson?.() || {},
- );
- const streamSettings = parseAdvancedSliceOrFallback(
- advancedStreamText.value,
- inbound.value.stream?.toJson?.() || {},
- );
- const sniffing = parseAdvancedSliceOrFallback(
- advancedSniffingText.value,
- inbound.value.sniffing?.toJson?.() || {},
- );
- const result = {
- listen: inbound.value.listen,
- port: inbound.value.port,
- protocol: inbound.value.protocol,
- settings,
- sniffing,
- tag: inbound.value.tag,
- };
- // Only include streamSettings for protocols that support it
- if (canEnableStream.value) {
- result.streamSettings = streamSettings;
- }
- return JSON.stringify(result, null, 2);
- } catch (_e) {
- return '';
- }
- },
- set: (next) => {
- let parsed;
- try {
- parsed = JSON.parse(next);
- } catch (e) {
- message.error(`All JSON invalid: ${e.message}`);
- return;
- }
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
- message.error('All JSON must be an inbound object.');
- return;
- }
- try {
- if (typeof parsed.listen === 'string') {
- inbound.value.listen = parsed.listen;
- }
- if (parsed.port !== undefined) {
- const parsedPort = Number(parsed.port);
- if (!Number.isNaN(parsedPort) && Number.isFinite(parsedPort)) {
- inbound.value.port = parsedPort;
- }
- }
- if (typeof parsed.protocol === 'string' && PROTOCOLS.includes(parsed.protocol)) {
- inbound.value.protocol = parsed.protocol;
- }
- if (typeof parsed.tag === 'string') {
- inbound.value.tag = parsed.tag;
- }
- const existingSettings = parseAdvancedSliceOrFallback(
- advancedSettingsText.value,
- inbound.value?.settings?.toJson?.() || {},
- );
- const settings = parsed.settings ?? existingSettings;
- const sniffing = parsed.sniffing ?? (inbound.value?.sniffing?.toJson?.() || {});
- advancedSettingsText.value = JSON.stringify(settings, null, 2);
- advancedSniffingText.value = JSON.stringify(sniffing, null, 2);
- // Only update stream settings if protocol supports it
- if (canEnableStream.value) {
- const streamSettings = parsed.streamSettings ?? (inbound.value?.stream?.toJson?.() || {});
- advancedStreamText.value = JSON.stringify(streamSettings, null, 2);
- } else {
- advancedStreamText.value = '{}';
- }
- } catch (e) {
- message.error(`All JSON invalid: ${e.message}`);
- }
- },
- });
- const advancedSettingsConfig = computed({
- get: () => {
- if (!inbound.value) return '';
- try {
- const settings = parseAdvancedSliceOrFallback(
- advancedSettingsText.value,
- inbound.value.settings?.toJson?.() || {},
- );
- return JSON.stringify({
- settings,
- }, null, 2);
- } catch (_e) {
- return '';
- }
- },
- set: (next) => {
- let parsed;
- try {
- parsed = JSON.parse(next);
- } catch (e) {
- message.error(`Settings JSON invalid: ${e.message}`);
- return;
- }
- const unwrapped = unwrapWrappedObject(parsed, 'settings');
- if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
- message.error('Settings JSON must be an object or { settings: { ... } }.');
- return;
- }
- try {
- advancedSettingsText.value = JSON.stringify(unwrapped, null, 2);
- } catch (e) {
- message.error(`Settings JSON invalid: ${e.message}`);
- }
- },
- });
- const advancedSniffingConfig = computed({
- get: () => {
- if (!inbound.value) return '';
- try {
- const sniffing = parseAdvancedSliceOrFallback(
- advancedSniffingText.value,
- inbound.value.sniffing?.toJson?.() || {},
- );
- return JSON.stringify({ sniffing }, null, 2);
- } catch (_e) {
- return '';
- }
- },
- set: (next) => {
- let parsed;
- try {
- parsed = JSON.parse(next);
- } catch (e) {
- message.error(`Sniffing JSON invalid: ${e.message}`);
- return;
- }
- const unwrapped = unwrapWrappedObject(parsed, 'sniffing');
- if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
- message.error('Sniffing JSON must be an object or { sniffing: { ... } }.');
- return;
- }
- try {
- advancedSniffingText.value = JSON.stringify(unwrapped, null, 2);
- } catch (e) {
- message.error(`Sniffing JSON invalid: ${e.message}`);
- }
- },
- });
- const advancedStreamConfig = computed({
- get: () => {
- if (!inbound.value) return '';
- try {
- const streamSettings = parseAdvancedSliceOrFallback(
- advancedStreamText.value,
- inbound.value.stream?.toJson?.() || {},
- );
- return JSON.stringify({ streamSettings }, null, 2);
- } catch (_e) {
- return '';
- }
- },
- set: (next) => {
- let parsed;
- try {
- parsed = JSON.parse(next);
- } catch (e) {
- message.error(`Stream JSON invalid: ${e.message}`);
- return;
- }
- const unwrapped = unwrapWrappedObject(parsed, 'streamSettings');
- if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
- message.error('Stream JSON must be an object or { streamSettings: { ... } }.');
- return;
- }
- try {
- advancedStreamText.value = JSON.stringify(unwrapped, null, 2);
- } catch (e) {
- message.error(`Stream JSON invalid: ${e.message}`);
- }
- },
- });
- // === Random helpers wired to the form's sync icons ==================
- function randomEmail(target) {
- if (target) target.email = RandomUtil.randomLowerAndNum(9);
- }
- function randomUuid(target) {
- if (target) target.id = RandomUtil.randomUUID();
- }
- function randomPasswordSeq(target, len = 10) {
- if (target) target.password = RandomUtil.randomSeq(len);
- }
- function randomSSPassword(target) {
- if (target) target.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
- }
- function randomAuth(target) {
- if (target) target.auth = RandomUtil.randomSeq(10);
- }
- function randomSubId(target) {
- if (target) target.subId = RandomUtil.randomLowerAndNum(16);
- }
- function regenWgKeypair(target) {
- const kp = Wireguard.generateKeypair();
- target.publicKey = kp.publicKey;
- target.privateKey = kp.privateKey;
- }
- function regenInboundWg() {
- const kp = Wireguard.generateKeypair();
- inbound.value.settings.pubKey = kp.publicKey;
- inbound.value.settings.secretKey = kp.privateKey;
- }
- // === Reality keygen via existing API =================================
- async function genRealityKeypair() {
- saving.value = true;
- try {
- const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
- if (msg?.success) {
- inbound.value.stream.reality.privateKey = msg.obj.privateKey;
- inbound.value.stream.reality.settings.publicKey = msg.obj.publicKey;
- }
- } finally {
- saving.value = false;
- }
- }
- function clearRealityKeypair() {
- if (!inbound.value?.stream?.reality) return;
- inbound.value.stream.reality.privateKey = '';
- inbound.value.stream.reality.settings.publicKey = '';
- }
- async function genMldsa65() {
- saving.value = true;
- try {
- const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
- if (msg?.success) {
- inbound.value.stream.reality.mldsa65Seed = msg.obj.seed;
- inbound.value.stream.reality.settings.mldsa65Verify = msg.obj.verify;
- }
- } finally {
- saving.value = false;
- }
- }
- function clearMldsa65() {
- if (!inbound.value?.stream?.reality) return;
- inbound.value.stream.reality.mldsa65Seed = '';
- inbound.value.stream.reality.settings.mldsa65Verify = '';
- }
- function randomizeRealityTarget() {
- if (!inbound.value?.stream?.reality) return;
- const t = getRandomRealityTarget();
- inbound.value.stream.reality.target = t.target;
- inbound.value.stream.reality.serverNames = t.sni;
- }
- function randomizeShortIds() {
- if (!inbound.value?.stream?.reality) return;
- inbound.value.stream.reality.shortIds = RandomUtil.randomShortIds();
- }
- // === ECH cert helpers ================================================
- async function getNewEchCert() {
- if (!inbound.value?.stream?.tls) return;
- saving.value = true;
- try {
- const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', {
- sni: inbound.value.stream.tls.sni,
- });
- if (msg?.success) {
- inbound.value.stream.tls.echServerKeys = msg.obj.echServerKeys;
- inbound.value.stream.tls.settings.echConfigList = msg.obj.echConfigList;
- }
- } finally {
- saving.value = false;
- }
- }
- function clearEchCert() {
- if (!inbound.value?.stream?.tls) return;
- inbound.value.stream.tls.echServerKeys = '';
- inbound.value.stream.tls.settings.echConfigList = '';
- }
- function setDefaultCertData(idx) {
- if (!inbound.value?.stream?.tls?.certs?.[idx]) return;
- inbound.value.stream.tls.certs[idx].certFile = defaultCert.value;
- inbound.value.stream.tls.certs[idx].keyFile = defaultKey.value;
- }
- async function fetchDefaultCertSettings() {
- try {
- const msg = await HttpUtil.post('/panel/setting/defaultSettings');
- if (msg?.success && msg.obj) {
- defaultCert.value = msg.obj.defaultCert || '';
- defaultKey.value = msg.obj.defaultKey || '';
- }
- } catch (_e) { /* non-fatal — leave Set Default disabled */ }
- }
- // === VLESS encryption helpers =======================================
- // `xray vlessenc` returns both X25519 and ML-KEM-768 auth variants every
- // call; the user clicks one button to pick which block goes into
- // decryption/encryption. Both generated strings share the same hybrid
- // mlkem768x25519plus prefix; the auth choice is the final key block.
- function normalizeVlessAuthLabel(label = '') {
- return label.toLowerCase().replace(/[-_\s]/g, '');
- }
- function matchesVlessAuth(block, authId) {
- if (block?.id === authId) return true;
- const label = normalizeVlessAuthLabel(block?.label);
- if (authId === 'mlkem768') return label.includes('mlkem768');
- if (authId === 'x25519') return label.includes('x25519');
- return false;
- }
- async function getNewVlessEnc(authId) {
- if (!authId || !inbound.value?.settings) return;
- saving.value = true;
- try {
- const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
- if (!msg?.success) return;
- const block = (msg.obj?.auths || []).find((a) => matchesVlessAuth(a, authId));
- if (!block) return;
- inbound.value.settings.decryption = block.decryption;
- inbound.value.settings.encryption = block.encryption;
- } finally {
- saving.value = false;
- }
- }
- function clearVlessEnc() {
- if (!inbound.value?.settings) return;
- inbound.value.settings.decryption = 'none';
- inbound.value.settings.encryption = 'none';
- }
- const selectedVlessAuth = computed(() => {
- const encryption = inbound.value?.settings?.encryption;
- if (!encryption || encryption === 'none') return 'None';
- const parts = encryption.split('.').filter(Boolean);
- const authKey = parts[parts.length - 1] || '';
- if (!authKey) return 'Custom';
- return authKey.length > 300 ? 'ML-KEM-768 auth' : 'X25519 auth';
- });
- // === SS method change tracks legacy semantics =========================
- function onSSMethodChange() {
- inbound.value.settings.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
- if (inbound.value.isSSMultiUser) {
- if (inbound.value.settings.shadowsockses.length === 0) {
- inbound.value.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
- }
- inbound.value.settings.shadowsockses.forEach((c) => {
- c.method = inbound.value.isSS2022 ? '' : inbound.value.settings.method;
- c.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
- });
- } else {
- inbound.value.settings.shadowsockses = [];
- }
- }
- // === Submit ==========================================================
- function close() {
- emit('update:open', false);
- }
- async function submit() {
- if (!inbound.value || !dbForm.value) return;
- saving.value = true;
- try {
- // Sniffing tab is structured; stream stays JSON for unsupported
- // transports — both go to wire as serialized JSON.
- let streamSettings;
- let sniffing;
- let settings;
- try {
- streamSettings = canEnableStream.value
- ? JSON.stringify(JSON.parse(advancedStreamText.value))
- : (inbound.value.stream?.sockopt
- ? JSON.stringify({ sockopt: inbound.value.stream.sockopt.toJson() })
- : '');
- } catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return; }
- try {
- sniffing = JSON.stringify(JSON.parse(advancedSniffingText.value || inbound.value.sniffing.toString()));
- } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return; }
- try {
- settings = JSON.stringify(JSON.parse(advancedSettingsText.value || inbound.value.settings.toString()));
- } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return; }
- // The structured form mutates `inbound.stream` directly when the
- // user edits TCP/WS/gRPC/HTTPUpgrade fields, but if they touched
- // the Advanced JSON tab their edits live there. Keep the JSON tab
- // authoritative — it was populated from the live model on open
- // and watch handlers below sync in either direction.
- const payload = {
- up: dbForm.value.up || 0,
- down: dbForm.value.down || 0,
- total: dbForm.value.total,
- remark: dbForm.value.remark,
- enable: dbForm.value.enable,
- expiryTime: dbForm.value.expiryTime,
- trafficReset: dbForm.value.trafficReset,
- lastTrafficResetTime: dbForm.value.lastTrafficResetTime || 0,
- listen: inbound.value.listen,
- port: inbound.value.port,
- protocol: inbound.value.protocol,
- settings: settings,
- streamSettings: streamSettings,
- sniffing: sniffing,
- };
- // Multi-node deployment: only include nodeId when the user picked a
- // remote node. Sending nodeId=null over qs.stringify becomes an
- // empty form value, which Go's form binding for *int parses as 0
- // — not nil — and we'd then try to look up node id 0 and fail with
- // "record not found". Omitting the key entirely keeps NodeID nil.
- if (dbForm.value.nodeId != null) {
- payload.nodeId = dbForm.value.nodeId;
- }
- const url = props.mode === 'edit'
- ? `/panel/api/inbounds/update/${props.dbInbound.id}`
- : '/panel/api/inbounds/add';
- const msg = await HttpUtil.post(url, payload);
- if (msg?.success) {
- emit('saved');
- close();
- }
- } finally {
- saving.value = false;
- }
- }
- const title = computed(() =>
- props.mode === 'edit'
- ? t('pages.inbounds.modifyInbound')
- : t('pages.inbounds.addInbound'),
- );
- const okText = computed(() =>
- props.mode === 'edit' ? t('pages.client.submitEdit') : t('create'),
- );
- // Whenever the structured form mutates stream / sniffing / settings,
- // refresh the matching slice of the Advanced JSON tab so the user
- // always sees the live state — flipping a switch in Sniffing or
- // editing encryption in Protocol now reflects in Advanced.
- watch(
- () => inbound.value && JSON.stringify(inbound.value.stream?.toJson?.() || {}),
- () => {
- if (!inbound.value?.stream) return;
- // Only update stream text for protocols that support it
- if (!canEnableStream.value) {
- advancedStreamText.value = '{}';
- return;
- }
- try {
- advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
- } catch (_e) { /* leave as is */ }
- },
- );
- watch(
- () => inbound.value && JSON.stringify(inbound.value.sniffing?.toJson?.() || {}),
- () => {
- if (!inbound.value?.sniffing) return;
- try {
- advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
- } catch (_e) { /* leave as is */ }
- },
- );
- watch(
- () => inbound.value && JSON.stringify(inbound.value.settings?.toJson?.() || {}),
- () => {
- if (!inbound.value?.settings) return;
- try {
- advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
- } catch (_e) { /* leave as is */ }
- },
- );
- // Watch protocol changes to clear stream settings for protocols that don't support it
- watch(
- () => inbound.value?.protocol,
- () => {
- if (!inbound.value) return;
- if (!canEnableStream.value) {
- advancedStreamText.value = '{}';
- }
- },
- );
- </script>
- <template>
- <a-modal :open="open" :title="title" :ok-text="okText" :cancel-text="t('close')" :confirm-loading="saving"
- :mask-closable="false" width="780px" @ok="submit" @cancel="close">
- <a-tabs v-if="inbound && dbForm" v-model:active-key="activeTabKey">
- <!-- ============================== BASICS ============================== -->
- <a-tab-pane key="basic" :tab="t('pages.xray.basicTemplate')">
- <a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
- <a-form-item :label="t('enable')">
- <a-switch v-model:checked="dbForm.enable" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.remark')">
- <a-input v-model:value="dbForm.remark" />
- </a-form-item>
- <a-form-item v-if="selectableNodes.length > 0" :label="t('pages.inbounds.deployTo')">
- <a-select v-model:value="dbForm.nodeId" :disabled="mode === 'edit'"
- :placeholder="t('pages.inbounds.localPanel')" allow-clear>
- <a-select-option :value="null">{{ t('pages.inbounds.localPanel') }}</a-select-option>
- <a-select-option v-for="n in selectableNodes" :key="n.id" :value="n.id"
- :disabled="n.status === 'offline'">
- {{ n.name }}{{ n.status === 'offline' ? ' (offline)' : '' }}
- </a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.protocol')">
- <a-select :value="protocol" :disabled="mode === 'edit'" @change="onProtocolChange">
- <a-select-option v-for="p in PROTOCOLS" :key="p" :value="p">{{ p }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.address')">
- <a-input v-model:value="inbound.listen" :placeholder="t('pages.inbounds.monitorDesc')" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.port')">
- <a-input-number v-model:value="inbound.port" :min="1" :max="65535" />
- </a-form-item>
- <a-form-item>
- <template #label>
- <a-tooltip :title="t('pages.inbounds.meansNoLimit')">{{ t('pages.inbounds.totalFlow') }}</a-tooltip>
- </template>
- <a-input-number v-model:value="totalGB" :min="0" :step="0.1" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.periodicTrafficResetTitle')">
- <a-select v-model:value="dbForm.trafficReset">
- <a-select-option v-for="r in TRAFFIC_RESETS" :key="r" :value="r">
- {{ t(`pages.inbounds.periodicTrafficReset.${r}`) }}
- </a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item>
- <template #label>
- <a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate')
- }}</a-tooltip>
- </template>
- <DateTimePicker v-model:value="expiryDate" />
- </a-form-item>
- </a-form>
- </a-tab-pane>
- <!-- ============================== PROTOCOL ============================== -->
- <a-tab-pane key="protocol" :tab="t('pages.inbounds.protocol')">
- <!-- Multi-user inbounds: in add mode embed the first client form,
- in edit mode show a count summary. -->
- <template v-if="isMultiUser">
- <a-collapse v-if="mode === 'add' && firstClient" default-active-key="0">
- <a-collapse-panel key="0" header="Client">
- <a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
- <a-form-item label="Enable">
- <a-switch v-model:checked="firstClient.enable" />
- </a-form-item>
- <a-form-item>
- <template #label>
- <a-tooltip title="Friendly identifier">
- Email
- <SyncOutlined class="random-icon" @click="randomEmail(firstClient)" />
- </a-tooltip>
- </template>
- <a-input v-model:value="firstClient.email" />
- </a-form-item>
- <a-form-item v-if="protocol === Protocols.VMESS || protocol === Protocols.VLESS">
- <template #label>
- <a-tooltip title="Reset to a fresh UUID">
- ID
- <SyncOutlined class="random-icon" @click="randomUuid(firstClient)" />
- </a-tooltip>
- </template>
- <a-input v-model:value="firstClient.id" />
- </a-form-item>
- <a-form-item v-if="protocol === Protocols.VMESS" label="Security">
- <a-select v-model:value="firstClient.security">
- <a-select-option v-for="k in SECURITY_OPTIONS" :key="k" :value="k">{{ k }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item v-if="protocol === Protocols.TROJAN || protocol === Protocols.SHADOWSOCKS">
- <template #label>
- <a-tooltip title="Reset to a fresh random value">
- Password
- <SyncOutlined v-if="protocol === Protocols.SHADOWSOCKS" class="random-icon"
- @click="randomSSPassword(firstClient)" />
- <SyncOutlined v-else class="random-icon" @click="randomPasswordSeq(firstClient)" />
- </a-tooltip>
- </template>
- <a-input v-model:value="firstClient.password" />
- </a-form-item>
- <a-form-item v-if="protocol === Protocols.HYSTERIA">
- <template #label>
- <a-tooltip title="Reset"><span>Auth password</span>
- <SyncOutlined class="random-icon" @click="randomAuth(firstClient)" />
- </a-tooltip>
- </template>
- <a-input v-model:value="firstClient.auth" />
- </a-form-item>
- <a-form-item v-if="canEnableTlsFlow" label="Flow">
- <a-select v-model:value="firstClient.flow">
- <a-select-option value="">none</a-select-option>
- <a-select-option v-for="k in FLOW_OPTIONS" :key="k" :value="k">{{ k }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item v-if="protocol === Protocols.VLESS" label="Reverse tag">
- <a-input v-model:value="firstClient.reverseTag" placeholder="Optional reverse tag" />
- </a-form-item>
- <a-form-item label="Subscription">
- <a-input v-model:value="firstClient.subId">
- <template #addonAfter>
- <SyncOutlined class="random-icon" @click="randomSubId(firstClient)" />
- </template>
- </a-input>
- </a-form-item>
- <a-form-item label="Comment">
- <a-input v-model:value="firstClient.comment" />
- </a-form-item>
- <a-form-item label="Total traffic (GB)">
- <a-input-number v-model:value="clientTotalGB" :min="0" :step="0.1" />
- </a-form-item>
- <a-form-item label="Expiry">
- <DateTimePicker v-model:value="clientExpiryDate" />
- </a-form-item>
- </a-form>
- </a-collapse-panel>
- </a-collapse>
- <a-collapse v-else>
- <a-collapse-panel key="summary" :header="`Clients: ${clientsArray.length}`">
- <table class="client-summary">
- <thead>
- <tr>
- <th>Email</th>
- <th>{{ protocol === Protocols.TROJAN || protocol === Protocols.SHADOWSOCKS ? 'Password' : (protocol
- ===
- Protocols.HYSTERIA ? 'Auth' : 'ID') }}</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="(c, idx) in clientsArray" :key="idx">
- <td>{{ c.email }}</td>
- <td>{{ c.id || c.password || c.auth }}</td>
- </tr>
- </tbody>
- </table>
- </a-collapse-panel>
- </a-collapse>
- </template>
- <!-- VLess decryption / encryption -->
- <a-form v-if="protocol === Protocols.VLESS" :colon="false" :label-col="{ sm: { span: 8 } }"
- :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
- <a-form-item label="Decryption">
- <a-input v-model:value="inbound.settings.decryption" />
- </a-form-item>
- <a-form-item label="Encryption">
- <a-input v-model:value="inbound.settings.encryption" />
- </a-form-item>
- <a-form-item label=" ">
- <a-space :size="8" wrap>
- <a-button type="primary" :loading="saving" @click="getNewVlessEnc('x25519')">
- X25519 auth
- </a-button>
- <a-button type="primary" :loading="saving" @click="getNewVlessEnc('mlkem768')">
- ML-KEM-768 auth
- </a-button>
- <a-button danger @click="clearVlessEnc">Clear</a-button>
- </a-space>
- <a-typography-text type="secondary" class="vless-auth-state">
- Selected: {{ selectedVlessAuth }}
- </a-typography-text>
- </a-form-item>
- </a-form>
- <!-- Shadowsocks shared fields (method/network/ivCheck) -->
- <a-form v-if="protocol === Protocols.SHADOWSOCKS" :colon="false" :label-col="{ sm: { span: 8 } }"
- :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
- <a-form-item label="Encryption method">
- <a-select v-model:value="inbound.settings.method" @change="onSSMethodChange">
- <a-select-option v-for="(m, k) in SSMethods" :key="k" :value="m">{{ k }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item v-if="inbound.isSS2022">
- <template #label>
- Password
- <SyncOutlined class="random-icon" @click="randomSSPassword(inbound.settings)" />
- </template>
- <a-input v-model:value="inbound.settings.password" />
- </a-form-item>
- <a-form-item label="Network">
- <a-select v-model:value="inbound.settings.network" :style="{ width: '120px' }">
- <a-select-option value="tcp,udp">TCP, UDP</a-select-option>
- <a-select-option value="tcp">TCP</a-select-option>
- <a-select-option value="udp">UDP</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="ivCheck">
- <a-switch v-model:checked="inbound.settings.ivCheck" />
- </a-form-item>
- </a-form>
- <!-- HTTP / Mixed accounts -->
- <a-form v-if="protocol === Protocols.HTTP || protocol === Protocols.MIXED" :colon="false"
- :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
- <a-form-item label="Accounts">
- <a-button size="small" @click="protocol === Protocols.HTTP
- ? inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())
- : inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())">
- <template #icon>
- <PlusOutlined />
- </template>
- Add
- </a-button>
- </a-form-item>
- <a-form-item :wrapper-col="{ span: 24 }">
- <a-input-group v-for="(account, idx) in inbound.settings.accounts" :key="idx" compact class="mb-8">
- <a-input :style="{ width: '45%' }" v-model:value="account.user" placeholder="Username">
- <template #addonBefore>{{ idx + 1 }}</template>
- </a-input>
- <a-input :style="{ width: '45%' }" v-model:value="account.pass" placeholder="Password" />
- <a-button @click="inbound.settings.delAccount(idx)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </a-input-group>
- </a-form-item>
- <a-form-item v-if="protocol === Protocols.HTTP" label="Allow transparent">
- <a-switch v-model:checked="inbound.settings.allowTransparent" />
- </a-form-item>
- <template v-if="protocol === Protocols.MIXED">
- <a-form-item label="Auth">
- <a-select v-model:value="inbound.settings.auth">
- <a-select-option value="noauth">noauth</a-select-option>
- <a-select-option value="password">password</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="UDP">
- <a-switch v-model:checked="inbound.settings.udp" />
- </a-form-item>
- <a-form-item v-if="inbound.settings.udp" label="UDP IP">
- <a-input v-model:value="inbound.settings.ip" />
- </a-form-item>
- </template>
- </a-form>
- <!-- Tunnel -->
- <a-form v-if="protocol === Protocols.TUNNEL" :colon="false" :label-col="{ sm: { span: 8 } }"
- :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
- <a-form-item label="Rewrite address">
- <a-input v-model:value="inbound.settings.rewriteAddress" />
- </a-form-item>
- <a-form-item label="Rewrite port">
- <a-input-number v-model:value="inbound.settings.rewritePort" :min="0" :max="65535" />
- </a-form-item>
- <a-form-item label="Allowed network">
- <a-select v-model:value="inbound.settings.allowedNetwork">
- <a-select-option value="tcp,udp">TCP, UDP</a-select-option>
- <a-select-option value="tcp">TCP</a-select-option>
- <a-select-option value="udp">UDP</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="Port map">
- <a-button size="small" @click="inbound.settings.addPortMap('', '')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- </a-form-item>
- <a-form-item v-if="inbound.settings.portMap.length > 0" :wrapper-col="{ span: 24 }">
- <a-input-group v-for="(pm, idx) in inbound.settings.portMap" :key="`pm-${idx}`" compact class="mb-8">
- <a-input :style="{ width: '30%' }" v-model:value="pm.name" placeholder="5555">
- <template #addonBefore>{{ idx + 1 }}</template>
- </a-input>
- <a-input :style="{ width: '60%' }" v-model:value="pm.value" placeholder="1.1.1.1:7777" />
- <a-button @click="inbound.settings.removePortMap(idx)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </a-input-group>
- </a-form-item>
- <a-form-item label="Follow redirect">
- <a-switch v-model:checked="inbound.settings.followRedirect" />
- </a-form-item>
- </a-form>
- <!-- TUN -->
- <a-form v-if="protocol === Protocols.TUN" :colon="false" :label-col="{ sm: { span: 8 } }"
- :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
- <a-form-item label="Interface name">
- <a-input v-model:value="inbound.settings.name" placeholder="xray0" />
- </a-form-item>
- <a-form-item label="MTU">
- <a-input-number v-model:value="inbound.settings.mtu" :min="0" />
- </a-form-item>
- <a-form-item label="Gateway">
- <a-button size="small" @click="inbound.settings.gateway.push('')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- <a-input v-for="(_ip, j) in inbound.settings.gateway" :key="`tun-gw-${j}`"
- v-model:value="inbound.settings.gateway[j]" class="mt-4"
- :placeholder="j === 0 ? '10.0.0.1/16' : 'fc00::1/64'">
- <template #addonAfter>
- <a-button size="small" @click="inbound.settings.gateway.splice(j, 1)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </template>
- </a-input>
- </a-form-item>
- <a-form-item label="DNS">
- <a-button size="small" @click="inbound.settings.dns.push('')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- <a-input v-for="(_ip, j) in inbound.settings.dns" :key="`tun-dns-${j}`"
- v-model:value="inbound.settings.dns[j]" class="mt-4" :placeholder="j === 0 ? '1.1.1.1' : '8.8.8.8'">
- <template #addonAfter>
- <a-button size="small" @click="inbound.settings.dns.splice(j, 1)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </template>
- </a-input>
- </a-form-item>
- <a-form-item label="User level">
- <a-input-number v-model:value="inbound.settings.userLevel" :min="0" />
- </a-form-item>
- <a-form-item>
- <template #label>
- <a-tooltip
- title="Windows-only. CIDRs added to the system routing table automatically so matching traffic goes through TUN.">
- Auto system routes
- </a-tooltip>
- </template>
- <a-button size="small" @click="inbound.settings.autoSystemRoutingTable.push('')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- <a-input v-for="(_ip, j) in inbound.settings.autoSystemRoutingTable" :key="`tun-rt-${j}`"
- v-model:value="inbound.settings.autoSystemRoutingTable[j]" class="mt-4"
- :placeholder="j === 0 ? '0.0.0.0/0' : '::/0'">
- <template #addonAfter>
- <a-button size="small" @click="inbound.settings.autoSystemRoutingTable.splice(j, 1)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </template>
- </a-input>
- </a-form-item>
- <a-form-item>
- <template #label>
- <a-tooltip
- title="Physical interface for outbound traffic. Use 'auto' to detect; auto-enabled when Auto system routes is set.">
- Auto outbounds interface
- </a-tooltip>
- </template>
- <a-input v-model:value="inbound.settings.autoOutboundsInterface" placeholder="auto" />
- </a-form-item>
- </a-form>
- <!-- WireGuard -->
- <a-form v-if="protocol === Protocols.WIREGUARD" :colon="false" :label-col="{ sm: { span: 8 } }"
- :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
- <a-form-item>
- <template #label>
- Secret key
- <SyncOutlined class="random-icon" @click="regenInboundWg" />
- </template>
- <a-input v-model:value="inbound.settings.secretKey" />
- </a-form-item>
- <a-form-item label="Public key">
- <a-input v-model:value="inbound.settings.pubKey" disabled />
- </a-form-item>
- <a-form-item label="MTU">
- <a-input-number v-model:value="inbound.settings.mtu" />
- </a-form-item>
- <a-form-item label="No-kernel TUN">
- <a-switch v-model:checked="inbound.settings.noKernelTun" />
- </a-form-item>
- <a-form-item label="Peers">
- <a-button size="small" @click="inbound.settings.addPeer()">
- <template #icon>
- <PlusOutlined />
- </template>
- Add peer
- </a-button>
- </a-form-item>
- <div v-for="(peer, idx) in inbound.settings.peers" :key="idx" class="wg-peer">
- <a-divider style="margin: 8px 0">
- Peer {{ idx + 1 }}
- <DeleteOutlined v-if="inbound.settings.peers.length > 1" class="danger-icon"
- @click="inbound.settings.delPeer(idx)" />
- </a-divider>
- <a-form-item>
- <template #label>
- Secret key
- <SyncOutlined class="random-icon" @click="regenWgKeypair(peer)" />
- </template>
- <a-input v-model:value="peer.privateKey" />
- </a-form-item>
- <a-form-item label="Public key">
- <a-input v-model:value="peer.publicKey" />
- </a-form-item>
- <a-form-item label="PSK">
- <a-input v-model:value="peer.psk" />
- </a-form-item>
- <a-form-item label="Allowed IPs">
- <a-button size="small" @click="peer.allowedIPs.push('')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- <a-input v-for="(_ip, j) in peer.allowedIPs" :key="j" v-model:value="peer.allowedIPs[j]" class="mt-4">
- <template #addonAfter>
- <a-button v-if="peer.allowedIPs.length > 1" size="small" @click="peer.allowedIPs.splice(j, 1)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </template>
- </a-input>
- </a-form-item>
- <a-form-item label="Keep-alive">
- <a-input-number v-model:value="peer.keepAlive" :min="0" />
- </a-form-item>
- </div>
- </a-form>
- <!-- ============== Fallbacks (VLESS/Trojan over TCP) ============== -->
- <template v-if="showFallbacks">
- <a-divider style="margin: 12px 0" />
- <div class="fallbacks-header">
- <a-tooltip
- title="Route incoming TLS traffic to a backend when it doesn't match a valid VLESS/Trojan handshake. Match by SNI, ALPN, and HTTP path; the most precise rule wins. Fallbacks require TCP+TLS transport.">
- <span class="fallbacks-title">
- Fallbacks ({{ inbound.settings.fallbacks.length }})
- </span>
- </a-tooltip>
- <a-button type="primary" size="small" @click="addFallback">
- <template #icon>
- <PlusOutlined />
- </template>
- Add
- </a-button>
- </div>
- <a-form v-for="(fallback, idx) in inbound.settings.fallbacks" :key="idx" :colon="false"
- :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
- <a-divider style="margin: 0">
- Fallback {{ idx + 1 }}
- <DeleteOutlined class="danger-icon" @click="delFallback(idx)" />
- </a-divider>
- <a-form-item>
- <template #label>
- <a-tooltip title="Match TLS SNI (server name). Leave empty to match any SNI.">
- SNI
- </a-tooltip>
- </template>
- <a-input v-model:value.trim="fallback.name" placeholder="any (leave empty)" />
- </a-form-item>
- <a-form-item>
- <template #label>
- <a-tooltip
- title="Match TLS ALPN. 'any' = no ALPN constraint. Use h2/http/1.1 split when the inbound advertises both.">
- ALPN
- </a-tooltip>
- </template>
- <a-select v-model:value="fallback.alpn">
- <a-select-option value="">any</a-select-option>
- <a-select-option value="h2">h2</a-select-option>
- <a-select-option value="http/1.1">http/1.1</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item :validate-status="fallback.path && !fallback.path.startsWith('/') ? 'error' : ''"
- :help="fallback.path && !fallback.path.startsWith('/') ? 'Path must start with /' : ''">
- <template #label>
- <a-tooltip
- title="Match the HTTP request path of the first packet. Must start with '/'. Leave empty to match any.">
- Path
- </a-tooltip>
- </template>
- <a-input v-model:value.trim="fallback.path" placeholder="any (leave empty) or /ws" />
- </a-form-item>
- <a-form-item :validate-status="!fallback.dest ? 'error' : ''"
- :help="!fallback.dest ? 'Destination is required' : ''">
- <template #label>
- <a-tooltip
- title="Where matching traffic is forwarded. Accepts a port number (80), an addr:port (127.0.0.1:8080), or a Unix socket path (/dev/shm/x.sock or @abstract).">
- Destination
- </a-tooltip>
- </template>
- <a-input v-model:value.trim="fallback.dest" placeholder="80 | 127.0.0.1:8080 | /dev/shm/x.sock" />
- </a-form-item>
- <a-form-item>
- <template #label>
- <a-tooltip
- title="PROXY protocol version sent to the destination. Off (0) for plain TCP; v1/v2 to preserve client IP if the backend supports it.">
- PROXY
- </a-tooltip>
- </template>
- <a-select v-model:value="fallback.xver">
- <a-select-option :value="0">Off</a-select-option>
- <a-select-option :value="1">v1</a-select-option>
- <a-select-option :value="2">v2</a-select-option>
- </a-select>
- </a-form-item>
- </a-form>
- </template>
- </a-tab-pane>
- <!-- ============================== STREAM ============================== -->
- <a-tab-pane v-if="canEnableStream" key="stream"
- tab="Stream"><!-- "Stream" stays literal — it's a wire-format identifier -->
- <a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
- <a-form-item v-if="protocol !== Protocols.HYSTERIA" label="Transmission">
- <a-select v-model:value="network" :style="{ width: '75%' }">
- <a-select-option value="tcp">TCP (RAW)</a-select-option>
- <a-select-option value="kcp">mKCP</a-select-option>
- <a-select-option value="ws">WebSocket</a-select-option>
- <a-select-option value="grpc">gRPC</a-select-option>
- <a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
- <a-select-option value="xhttp">XHTTP</a-select-option>
- </a-select>
- </a-form-item>
- <!-- TCP (RAW) — proxy-protocol + optional HTTP camouflage with full request/response editor -->
- <template v-if="network === 'tcp'">
- <a-form-item v-if="canEnableTls" label="Proxy Protocol">
- <a-switch v-model:checked="inbound.stream.tcp.acceptProxyProtocol" />
- </a-form-item>
- <a-form-item :label="`HTTP ${t('camouflage')}`">
- <a-switch :checked="inbound.stream.tcp.type === 'http'"
- @change="(v) => (inbound.stream.tcp.type = v ? 'http' : 'none')" />
- </a-form-item>
- <template v-if="inbound.stream.tcp.type === 'http'">
- <!-- Request -->
- <a-divider :style="{ margin: '0' }">{{ t('pages.inbounds.stream.general.request') }}</a-divider>
- <a-form-item :label="t('pages.inbounds.stream.tcp.version')">
- <a-input v-model:value="inbound.stream.tcp.request.version" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.stream.tcp.method')">
- <a-input v-model:value="inbound.stream.tcp.request.method" />
- </a-form-item>
- <a-form-item>
- <template #label>
- {{ t('pages.inbounds.stream.tcp.path') }}
- <a-button size="small" :style="{ marginLeft: '6px' }"
- @click="inbound.stream.tcp.request.addPath('/')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- </template>
- <template v-for="(_p, idx) in inbound.stream.tcp.request.path" :key="`tcp-path-${idx}`">
- <a-input v-model:value="inbound.stream.tcp.request.path[idx]" class="mb-4">
- <template #addonAfter>
- <a-button v-if="inbound.stream.tcp.request.path.length > 1" size="small"
- @click="inbound.stream.tcp.request.removePath(idx)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </template>
- </a-input>
- </template>
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
- <a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- </a-form-item>
- <a-form-item v-if="inbound.stream.tcp.request.headers.length > 0" :wrapper-col="{ span: 24 }">
- <a-input-group v-for="(h, idx) in inbound.stream.tcp.request.headers" :key="`tcp-rh-${idx}`" compact
- class="mb-8">
- <a-input :style="{ width: '45%' }" v-model:value="h.name"
- :placeholder="t('pages.inbounds.stream.general.name')">
- <template #addonBefore>{{ idx + 1 }}</template>
- </a-input>
- <a-input :style="{ width: '45%' }" v-model:value="h.value"
- :placeholder="t('pages.inbounds.stream.general.value')" />
- <a-button @click="inbound.stream.tcp.request.removeHeader(idx)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </a-input-group>
- </a-form-item>
- <!-- Response -->
- <a-divider :style="{ margin: '0' }">{{ t('pages.inbounds.stream.general.response') }}</a-divider>
- <a-form-item :label="t('pages.inbounds.stream.tcp.version')">
- <a-input v-model:value="inbound.stream.tcp.response.version" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.stream.tcp.status')">
- <a-input v-model:value="inbound.stream.tcp.response.status" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.stream.tcp.statusDescription')">
- <a-input v-model:value="inbound.stream.tcp.response.reason" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.stream.tcp.responseHeader')">
- <a-button size="small"
- @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- </a-form-item>
- <a-form-item v-if="inbound.stream.tcp.response.headers.length > 0" :wrapper-col="{ span: 24 }">
- <a-input-group v-for="(h, idx) in inbound.stream.tcp.response.headers" :key="`tcp-rsh-${idx}`" compact
- class="mb-8">
- <a-input :style="{ width: '45%' }" v-model:value="h.name"
- :placeholder="t('pages.inbounds.stream.general.name')">
- <template #addonBefore>{{ idx + 1 }}</template>
- </a-input>
- <a-input :style="{ width: '45%' }" v-model:value="h.value"
- :placeholder="t('pages.inbounds.stream.general.value')" />
- <a-button @click="inbound.stream.tcp.response.removeHeader(idx)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </a-input-group>
- </a-form-item>
- </template>
- </template>
- <!-- mKCP -->
- <template v-if="network === 'kcp'">
- <a-form-item label="MTU">
- <a-input-number v-model:value="inbound.stream.kcp.mtu" :min="576" :max="1460" />
- </a-form-item>
- <a-form-item label="TTI (ms)">
- <a-input-number v-model:value="inbound.stream.kcp.tti" :min="10" :max="100" />
- </a-form-item>
- <a-form-item label="Uplink (MB/s)">
- <a-input-number v-model:value="inbound.stream.kcp.upCap" :min="0" />
- </a-form-item>
- <a-form-item label="Downlink (MB/s)">
- <a-input-number v-model:value="inbound.stream.kcp.downCap" :min="0" />
- </a-form-item>
- <a-form-item label="CWND Multiplier">
- <a-input-number v-model:value="inbound.stream.kcp.cwndMultiplier" :min="1" />
- </a-form-item>
- <a-form-item label="Max Sending Window">
- <a-input-number v-model:value="inbound.stream.kcp.maxSendingWindow" :min="0" />
- </a-form-item>
- </template>
- <!-- WebSocket -->
- <template v-if="network === 'ws'">
- <a-form-item label="Proxy Protocol">
- <a-switch v-model:checked="inbound.stream.ws.acceptProxyProtocol" />
- </a-form-item>
- <a-form-item :label="t('host')">
- <a-input v-model:value="inbound.stream.ws.host" />
- </a-form-item>
- <a-form-item :label="t('path')">
- <a-input v-model:value="inbound.stream.ws.path" />
- </a-form-item>
- <a-form-item label="Heartbeat Period">
- <a-input-number v-model:value="inbound.stream.ws.heartbeatPeriod" :min="0" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
- <a-button size="small" @click="inbound.stream.ws.addHeader('', '')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- </a-form-item>
- <a-form-item v-if="inbound.stream.ws.headers.length > 0" :wrapper-col="{ span: 24 }">
- <a-input-group v-for="(h, idx) in inbound.stream.ws.headers" :key="`ws-h-${idx}`" compact class="mb-8">
- <a-input :style="{ width: '45%' }" v-model:value="h.name"
- :placeholder="t('pages.inbounds.stream.general.name')">
- <template #addonBefore>{{ idx + 1 }}</template>
- </a-input>
- <a-input :style="{ width: '45%' }" v-model:value="h.value"
- :placeholder="t('pages.inbounds.stream.general.value')" />
- <a-button @click="inbound.stream.ws.removeHeader(idx)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </a-input-group>
- </a-form-item>
- </template>
- <!-- gRPC -->
- <template v-if="network === 'grpc'">
- <a-form-item label="Service Name">
- <a-input v-model:value="inbound.stream.grpc.serviceName" />
- </a-form-item>
- <a-form-item label="Authority">
- <a-input v-model:value="inbound.stream.grpc.authority" />
- </a-form-item>
- <a-form-item label="Multi Mode">
- <a-switch v-model:checked="inbound.stream.grpc.multiMode" />
- </a-form-item>
- </template>
- <!-- HTTPUpgrade -->
- <template v-if="network === 'httpupgrade'">
- <a-form-item label="Proxy Protocol">
- <a-switch v-model:checked="inbound.stream.httpupgrade.acceptProxyProtocol" />
- </a-form-item>
- <a-form-item :label="t('host')">
- <a-input v-model:value="inbound.stream.httpupgrade.host" />
- </a-form-item>
- <a-form-item :label="t('path')">
- <a-input v-model:value="inbound.stream.httpupgrade.path" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
- <a-button size="small" @click="inbound.stream.httpupgrade.addHeader('', '')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- </a-form-item>
- <a-form-item v-if="inbound.stream.httpupgrade.headers.length > 0" :wrapper-col="{ span: 24 }">
- <a-input-group v-for="(h, idx) in inbound.stream.httpupgrade.headers" :key="`hu-h-${idx}`" compact
- class="mb-8">
- <a-input :style="{ width: '45%' }" v-model:value="h.name"
- :placeholder="t('pages.inbounds.stream.general.name')">
- <template #addonBefore>{{ idx + 1 }}</template>
- </a-input>
- <a-input :style="{ width: '45%' }" v-model:value="h.value"
- :placeholder="t('pages.inbounds.stream.general.value')" />
- <a-button @click="inbound.stream.httpupgrade.removeHeader(idx)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </a-input-group>
- </a-form-item>
- </template>
- <!-- XHTTP -->
- <template v-if="network === 'xhttp'">
- <a-form-item :label="t('host')">
- <a-input v-model:value="inbound.stream.xhttp.host" />
- </a-form-item>
- <a-form-item :label="t('path')">
- <a-input v-model:value="inbound.stream.xhttp.path" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
- <a-button size="small" @click="inbound.stream.xhttp.addHeader('', '')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- </a-form-item>
- <a-form-item v-if="inbound.stream.xhttp.headers.length > 0" :wrapper-col="{ span: 24 }">
- <a-input-group v-for="(h, idx) in inbound.stream.xhttp.headers" :key="`xh-h-${idx}`" compact class="mb-8">
- <a-input :style="{ width: '45%' }" v-model:value="h.name"
- :placeholder="t('pages.inbounds.stream.general.name')">
- <template #addonBefore>{{ idx + 1 }}</template>
- </a-input>
- <a-input :style="{ width: '45%' }" v-model:value="h.value"
- :placeholder="t('pages.inbounds.stream.general.value')" />
- <a-button @click="inbound.stream.xhttp.removeHeader(idx)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </a-input-group>
- </a-form-item>
- <a-form-item label="Mode">
- <a-select v-model:value="inbound.stream.xhttp.mode" :style="{ width: '50%' }">
- <a-select-option v-for="m in MODE_OPTIONS" :key="m" :value="m">{{ m }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item v-if="inbound.stream.xhttp.mode === 'packet-up'" label="Max Buffered Upload">
- <a-input-number v-model:value="inbound.stream.xhttp.scMaxBufferedPosts" />
- </a-form-item>
- <a-form-item v-if="inbound.stream.xhttp.mode === 'packet-up'" label="Max Upload Size (Byte)">
- <a-input v-model:value="inbound.stream.xhttp.scMaxEachPostBytes" />
- </a-form-item>
- <a-form-item v-if="inbound.stream.xhttp.mode === 'stream-up'" label="Stream-Up Server">
- <a-input v-model:value="inbound.stream.xhttp.scStreamUpServerSecs" />
- </a-form-item>
- <a-form-item label="Server Max Header Bytes">
- <a-input-number v-model:value="inbound.stream.xhttp.serverMaxHeaderBytes" :min="0"
- placeholder="0 (default)" />
- </a-form-item>
- <a-form-item label="Padding Bytes">
- <a-input v-model:value="inbound.stream.xhttp.xPaddingBytes" />
- </a-form-item>
- <a-form-item label="Padding Obfs Mode">
- <a-switch v-model:checked="inbound.stream.xhttp.xPaddingObfsMode" />
- </a-form-item>
- <template v-if="inbound.stream.xhttp.xPaddingObfsMode">
- <a-form-item label="Padding Key">
- <a-input v-model:value="inbound.stream.xhttp.xPaddingKey" placeholder="x_padding" />
- </a-form-item>
- <a-form-item label="Padding Header">
- <a-input v-model:value="inbound.stream.xhttp.xPaddingHeader" placeholder="X-Padding" />
- </a-form-item>
- <a-form-item label="Padding Placement">
- <a-select v-model:value="inbound.stream.xhttp.xPaddingPlacement">
- <a-select-option value="">Default (queryInHeader)</a-select-option>
- <a-select-option value="queryInHeader">queryInHeader</a-select-option>
- <a-select-option value="header">header</a-select-option>
- <a-select-option value="cookie">cookie</a-select-option>
- <a-select-option value="query">query</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="Padding Method">
- <a-select v-model:value="inbound.stream.xhttp.xPaddingMethod">
- <a-select-option value="">Default (repeat-x)</a-select-option>
- <a-select-option value="repeat-x">repeat-x</a-select-option>
- <a-select-option value="tokenish">tokenish</a-select-option>
- </a-select>
- </a-form-item>
- </template>
- <a-form-item label="Session Placement">
- <a-select v-model:value="inbound.stream.xhttp.sessionPlacement">
- <a-select-option value="">Default (path)</a-select-option>
- <a-select-option value="path">path</a-select-option>
- <a-select-option value="header">header</a-select-option>
- <a-select-option value="cookie">cookie</a-select-option>
- <a-select-option value="query">query</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item
- v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'"
- label="Session Key">
- <a-input v-model:value="inbound.stream.xhttp.sessionKey" placeholder="x_session" />
- </a-form-item>
- <a-form-item label="Sequence Placement">
- <a-select v-model:value="inbound.stream.xhttp.seqPlacement">
- <a-select-option value="">Default (path)</a-select-option>
- <a-select-option value="path">path</a-select-option>
- <a-select-option value="header">header</a-select-option>
- <a-select-option value="cookie">cookie</a-select-option>
- <a-select-option value="query">query</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'"
- label="Sequence Key">
- <a-input v-model:value="inbound.stream.xhttp.seqKey" placeholder="x_seq" />
- </a-form-item>
- <a-form-item v-if="inbound.stream.xhttp.mode === 'packet-up'" label="Uplink Data Placement">
- <a-select v-model:value="inbound.stream.xhttp.uplinkDataPlacement">
- <a-select-option value="">Default (body)</a-select-option>
- <a-select-option value="body">body</a-select-option>
- <a-select-option value="header">header</a-select-option>
- <a-select-option value="cookie">cookie</a-select-option>
- <a-select-option value="query">query</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item
- v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'"
- label="Uplink Data Key">
- <a-input v-model:value="inbound.stream.xhttp.uplinkDataKey" placeholder="x_data" />
- </a-form-item>
- <a-form-item label="No SSE Header">
- <a-switch v-model:checked="inbound.stream.xhttp.noSSEHeader" />
- </a-form-item>
- </template>
- <!-- ====== Security section ====== -->
- <a-form-item label="Security">
- <a-select v-model:value="security" :style="{ width: '160px' }" :disabled="!canEnableTls">
- <a-select-option value="none">none</a-select-option>
- <a-select-option value="tls">tls</a-select-option>
- <a-select-option v-if="canEnableReality" value="reality">reality</a-select-option>
- </a-select>
- </a-form-item>
- <template v-if="security === 'tls' && inbound.stream.tls">
- <a-form-item label="SNI">
- <a-input v-model:value="inbound.stream.tls.sni" placeholder="Server Name Indication" />
- </a-form-item>
- <a-form-item label="Cipher Suites">
- <a-select v-model:value="inbound.stream.tls.cipherSuites">
- <a-select-option value="">Auto</a-select-option>
- <a-select-option v-for="[label, val] in CIPHER_SUITES" :key="val" :value="val">{{ label
- }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="Min/Max Version">
- <a-input-group compact>
- <a-select v-model:value="inbound.stream.tls.minVersion" :style="{ width: '50%' }">
- <a-select-option v-for="v in TLS_VERSIONS" :key="v" :value="v">{{ v }}</a-select-option>
- </a-select>
- <a-select v-model:value="inbound.stream.tls.maxVersion" :style="{ width: '50%' }">
- <a-select-option v-for="v in TLS_VERSIONS" :key="v" :value="v">{{ v }}</a-select-option>
- </a-select>
- </a-input-group>
- </a-form-item>
- <a-form-item label="uTLS">
- <a-select v-model:value="inbound.stream.tls.settings.fingerprint" :style="{ width: '100%' }">
- <a-select-option value="">None</a-select-option>
- <a-select-option v-for="fp in FINGERPRINTS" :key="fp" :value="fp">{{ fp }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="ALPN">
- <a-select v-model:value="inbound.stream.tls.alpn" mode="multiple" :style="{ width: '100%' }"
- :token-separators="[',']">
- <a-select-option v-for="a in ALPNS" :key="a" :value="a">{{ a }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="Reject Unknown SNI">
- <a-switch v-model:checked="inbound.stream.tls.rejectUnknownSni" />
- </a-form-item>
- <a-form-item label="Disable System Root">
- <a-switch v-model:checked="inbound.stream.tls.disableSystemRoot" />
- </a-form-item>
- <a-form-item label="Session Resumption">
- <a-switch v-model:checked="inbound.stream.tls.enableSessionResumption" />
- </a-form-item>
- <!-- Cert array — file path or inline content per row -->
- <template v-for="(cert, idx) in inbound.stream.tls.certs" :key="`cert-${idx}`">
- <a-form-item :label="t('certificate')">
- <a-radio-group v-model:value="cert.useFile" button-style="solid">
- <a-radio-button :value="true">{{ t('pages.inbounds.certificatePath') }}</a-radio-button>
- <a-radio-button :value="false">{{ t('pages.inbounds.certificateContent') }}</a-radio-button>
- </a-radio-group>
- </a-form-item>
- <a-form-item label=" ">
- <a-space>
- <a-button v-if="idx === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- <a-button v-if="inbound.stream.tls.certs.length > 1" type="primary" size="small"
- @click="inbound.stream.tls.removeCert(idx)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </a-space>
- </a-form-item>
- <template v-if="cert.useFile">
- <a-form-item :label="t('pages.inbounds.publicKey')">
- <a-input v-model:value="cert.certFile" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.privatekey')">
- <a-input v-model:value="cert.keyFile" />
- </a-form-item>
- <a-form-item label=" ">
- <a-button type="primary" :disabled="!defaultCert && !defaultKey" @click="setDefaultCertData(idx)">
- {{ t('pages.inbounds.setDefaultCert') }}
- </a-button>
- </a-form-item>
- </template>
- <template v-else>
- <a-form-item :label="t('pages.inbounds.publicKey')">
- <a-textarea v-model:value="cert.cert" :auto-size="{ minRows: 3, maxRows: 8 }" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.privatekey')">
- <a-textarea v-model:value="cert.key" :auto-size="{ minRows: 3, maxRows: 8 }" />
- </a-form-item>
- </template>
- <a-form-item label="One Time Loading">
- <a-switch v-model:checked="cert.oneTimeLoading" />
- </a-form-item>
- <a-form-item label="Usage Option">
- <a-select v-model:value="cert.usage" :style="{ width: '50%' }">
- <a-select-option v-for="u in USAGES" :key="u" :value="u">{{ u }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item v-if="cert.usage === 'issue'" label="Build Chain">
- <a-switch v-model:checked="cert.buildChain" />
- </a-form-item>
- </template>
- <!-- ECH (Encrypted Client Hello) -->
- <a-form-item label="ECH key">
- <a-input v-model:value="inbound.stream.tls.echServerKeys" />
- </a-form-item>
- <a-form-item label="ECH config">
- <a-input v-model:value="inbound.stream.tls.settings.echConfigList" />
- </a-form-item>
- <a-form-item label=" ">
- <a-space>
- <a-button type="primary" :loading="saving" @click="getNewEchCert">Get New ECH Cert</a-button>
- <a-button danger @click="clearEchCert">Clear</a-button>
- </a-space>
- </a-form-item>
- </template>
- <template v-if="security === 'reality' && inbound.stream.reality">
- <a-form-item label="Show">
- <a-switch v-model:checked="inbound.stream.reality.show" />
- </a-form-item>
- <a-form-item label="Xver">
- <a-input-number v-model:value="inbound.stream.reality.xver" :min="0" />
- </a-form-item>
- <a-form-item label="uTLS">
- <a-select v-model:value="inbound.stream.reality.settings.fingerprint" :style="{ width: '100%' }">
- <a-select-option v-for="fp in FINGERPRINTS" :key="fp" :value="fp">{{ fp }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item>
- <template #label>
- Target
- <SyncOutlined class="random-icon" @click="randomizeRealityTarget" />
- </template>
- <a-input v-model:value="inbound.stream.reality.target" />
- </a-form-item>
- <a-form-item>
- <template #label>
- SNI
- <SyncOutlined class="random-icon" @click="randomizeRealityTarget" />
- </template>
- <a-input v-model:value="inbound.stream.reality.serverNames" />
- </a-form-item>
- <a-form-item label="Max Time Diff (ms)">
- <a-input-number v-model:value="inbound.stream.reality.maxTimediff" :min="0" />
- </a-form-item>
- <a-form-item label="Min Client Ver">
- <a-input v-model:value="inbound.stream.reality.minClientVer" placeholder="25.9.11" />
- </a-form-item>
- <a-form-item label="Max Client Ver">
- <a-input v-model:value="inbound.stream.reality.maxClientVer" placeholder="25.9.11" />
- </a-form-item>
- <a-form-item>
- <template #label>
- Short IDs
- <SyncOutlined class="random-icon" @click="randomizeShortIds" />
- </template>
- <a-textarea v-model:value="inbound.stream.reality.shortIds" :auto-size="{ minRows: 1, maxRows: 4 }" />
- </a-form-item>
- <a-form-item label="SpiderX">
- <a-input v-model:value="inbound.stream.reality.settings.spiderX" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.publicKey')">
- <a-textarea v-model:value="inbound.stream.reality.settings.publicKey"
- :auto-size="{ minRows: 1, maxRows: 4 }" />
- </a-form-item>
- <a-form-item :label="t('pages.inbounds.privatekey')">
- <a-textarea v-model:value="inbound.stream.reality.privateKey" :auto-size="{ minRows: 1, maxRows: 4 }" />
- </a-form-item>
- <a-form-item label=" ">
- <a-space>
- <a-button type="primary" :loading="saving" @click="genRealityKeypair">Get New Cert</a-button>
- <a-button danger @click="clearRealityKeypair">Clear</a-button>
- </a-space>
- </a-form-item>
- <a-form-item label="mldsa65 Seed">
- <a-textarea v-model:value="inbound.stream.reality.mldsa65Seed" :auto-size="{ minRows: 2, maxRows: 6 }" />
- </a-form-item>
- <a-form-item label="mldsa65 Verify">
- <a-textarea v-model:value="inbound.stream.reality.settings.mldsa65Verify"
- :auto-size="{ minRows: 2, maxRows: 6 }" />
- </a-form-item>
- <a-form-item label=" ">
- <a-space>
- <a-button type="primary" :loading="saving" @click="genMldsa65">Get New Seed</a-button>
- <a-button danger @click="clearMldsa65">Clear</a-button>
- </a-space>
- </a-form-item>
- </template>
- <!-- ====== External Proxy ====== -->
- <a-form-item label="External Proxy">
- <a-switch v-model:checked="externalProxy" />
- <a-button v-if="externalProxy" size="small" type="primary" :style="{ marginLeft: '10px' }"
- @click="inbound.stream.externalProxy.push({ forceTls: 'same', dest: '', port: 443, remark: '' })">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- </a-form-item>
- <a-form-item v-if="externalProxy" :wrapper-col="{ span: 24 }">
- <a-input-group v-for="(row, idx) in inbound.stream.externalProxy" :key="`ep-${idx}`" compact
- :style="{ margin: '8px 0' }">
- <a-tooltip title="Force TLS">
- <a-select v-model:value="row.forceTls" :style="{ width: '20%' }">
- <a-select-option value="same">{{ t('pages.inbounds.same') }}</a-select-option>
- <a-select-option value="none">{{ t('none') }}</a-select-option>
- <a-select-option value="tls">TLS</a-select-option>
- </a-select>
- </a-tooltip>
- <a-input v-model:value="row.dest" :style="{ width: '30%' }" :placeholder="t('host')" />
- <a-tooltip :title="t('pages.inbounds.port')">
- <a-input-number v-model:value="row.port" :style="{ width: '15%' }" :min="1" :max="65535" />
- </a-tooltip>
- <a-input v-model:value="row.remark" :style="{ width: '35%' }" :placeholder="t('pages.inbounds.remark')">
- <template #addonAfter>
- <MinusOutlined @click="inbound.stream.externalProxy.splice(idx, 1)" />
- </template>
- </a-input>
- </a-input-group>
- </a-form-item>
- <!-- ====== Sockopt ====== -->
- <a-form-item label="Sockopt">
- <a-switch v-model:checked="inbound.stream.sockoptSwitch" />
- </a-form-item>
- <template v-if="inbound.stream.sockoptSwitch && inbound.stream.sockopt">
- <a-form-item label="Route Mark">
- <a-input-number v-model:value="inbound.stream.sockopt.mark" :min="0" />
- </a-form-item>
- <a-form-item label="TCP Keep Alive Interval">
- <a-input-number v-model:value="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0" />
- </a-form-item>
- <a-form-item label="TCP Keep Alive Idle">
- <a-input-number v-model:value="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0" />
- </a-form-item>
- <a-form-item label="TCP Max Seg">
- <a-input-number v-model:value="inbound.stream.sockopt.tcpMaxSeg" :min="0" />
- </a-form-item>
- <a-form-item label="TCP User Timeout">
- <a-input-number v-model:value="inbound.stream.sockopt.tcpUserTimeout" :min="0" />
- </a-form-item>
- <a-form-item label="TCP Window Clamp">
- <a-input-number v-model:value="inbound.stream.sockopt.tcpWindowClamp" :min="0" />
- </a-form-item>
- <a-form-item label="Proxy Protocol">
- <a-switch v-model:checked="inbound.stream.sockopt.acceptProxyProtocol" />
- </a-form-item>
- <a-form-item label="TCP Fast Open">
- <a-switch v-model:checked="inbound.stream.sockopt.tcpFastOpen" />
- </a-form-item>
- <a-form-item label="Multipath TCP">
- <a-switch v-model:checked="inbound.stream.sockopt.tcpMptcp" />
- </a-form-item>
- <a-form-item label="Penetrate">
- <a-switch v-model:checked="inbound.stream.sockopt.penetrate" />
- </a-form-item>
- <a-form-item label="V6 Only">
- <a-switch v-model:checked="inbound.stream.sockopt.V6Only" />
- </a-form-item>
- <a-form-item label="Domain Strategy">
- <a-select v-model:value="inbound.stream.sockopt.domainStrategy" :style="{ width: '50%' }">
- <a-select-option v-for="d in DOMAIN_STRATEGIES" :key="d" :value="d">{{ d }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="TCP Congestion">
- <a-select v-model:value="inbound.stream.sockopt.tcpcongestion" :style="{ width: '50%' }">
- <a-select-option v-for="c in TCP_CONGESTIONS" :key="c" :value="c">{{ c }}</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="TProxy">
- <a-select v-model:value="inbound.stream.sockopt.tproxy" :style="{ width: '50%' }">
- <a-select-option value="off">Off</a-select-option>
- <a-select-option value="redirect">Redirect</a-select-option>
- <a-select-option value="tproxy">TProxy</a-select-option>
- </a-select>
- </a-form-item>
- <a-form-item label="Dialer Proxy">
- <a-input v-model:value="inbound.stream.sockopt.dialerProxy" />
- </a-form-item>
- <a-form-item label="Interface Name">
- <a-input v-model:value="inbound.stream.sockopt.interfaceName" />
- </a-form-item>
- <a-form-item label="Trusted X-Forwarded-For">
- <a-select v-model:value="inbound.stream.sockopt.trustedXForwardedFor" mode="tags"
- :style="{ width: '100%' }" :token-separators="[',']">
- <a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
- <a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
- <a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
- <a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
- </a-select>
- </a-form-item>
- </template>
- <!-- ====== Hysteria stream settings ====== -->
- <!-- Per https://xtls.github.io/config/transports/hysteria.html -->
- <template v-if="protocol === Protocols.HYSTERIA">
- <a-form-item>
- <template #label>
- <a-tooltip title="Hysteria protocol version. Currently must be 2.">
- Version
- </a-tooltip>
- </template>
- <a-input-number v-model:value="inbound.stream.hysteria.version" :min="2" :max="2" />
- </a-form-item>
- <a-form-item>
- <template #label>
- <a-tooltip title="Idle timeout (seconds) for a single QUIC native UDP connection.">
- UDP idle timeout
- </a-tooltip>
- </template>
- <a-input-number v-model:value="inbound.stream.hysteria.udpIdleTimeout" :min="0" />
- </a-form-item>
- <a-form-item label="Masquerade">
- <a-switch v-model:checked="inbound.stream.hysteria.masqueradeSwitch" />
- </a-form-item>
- <template v-if="inbound.stream.hysteria.masqueradeSwitch">
- <a-form-item label="Type">
- <a-select v-model:value="inbound.stream.hysteria.masquerade.type" :style="{ width: '50%' }">
- <a-select-option value="proxy">Proxy</a-select-option>
- <a-select-option value="file">File</a-select-option>
- <a-select-option value="string">String</a-select-option>
- </a-select>
- </a-form-item>
- <!-- Proxy type: url / rewriteHost / insecure -->
- <template v-if="inbound.stream.hysteria.masquerade.type === 'proxy'">
- <a-form-item label="URL">
- <a-input v-model:value="inbound.stream.hysteria.masquerade.url" placeholder="https://example.com" />
- </a-form-item>
- <a-form-item label="Rewrite Host">
- <a-switch v-model:checked="inbound.stream.hysteria.masquerade.rewriteHost" />
- </a-form-item>
- <a-form-item label="Insecure">
- <a-switch v-model:checked="inbound.stream.hysteria.masquerade.insecure" />
- </a-form-item>
- </template>
- <!-- File type: dir -->
- <a-form-item v-if="inbound.stream.hysteria.masquerade.type === 'file'" label="Directory">
- <a-input v-model:value="inbound.stream.hysteria.masquerade.dir" placeholder="/path/to/www" />
- </a-form-item>
- <!-- String type: content / statusCode / headers -->
- <template v-if="inbound.stream.hysteria.masquerade.type === 'string'">
- <a-form-item label="Content">
- <a-textarea v-model:value="inbound.stream.hysteria.masquerade.content"
- :auto-size="{ minRows: 2, maxRows: 6 }" />
- </a-form-item>
- <a-form-item label="Status Code">
- <a-input-number v-model:value="inbound.stream.hysteria.masquerade.statusCode" :min="100" :max="599"
- placeholder="200" />
- </a-form-item>
- <a-form-item label="Headers">
- <a-button size="small" @click="inbound.stream.hysteria.masquerade.addHeader('', '')">
- <template #icon>
- <PlusOutlined />
- </template>
- </a-button>
- </a-form-item>
- <a-form-item v-if="inbound.stream.hysteria.masquerade.headers.length > 0" :wrapper-col="{ span: 24 }">
- <a-input-group v-for="(h, idx) in inbound.stream.hysteria.masquerade.headers" :key="`mh-${idx}`"
- compact class="mb-8">
- <a-input :style="{ width: '45%' }" v-model:value="h.name" placeholder="Name">
- <template #addonBefore>{{ idx + 1 }}</template>
- </a-input>
- <a-input :style="{ width: '45%' }" v-model:value="h.value" placeholder="Value" />
- <a-button @click="inbound.stream.hysteria.masquerade.removeHeader(idx)">
- <template #icon>
- <MinusOutlined />
- </template>
- </a-button>
- </a-input-group>
- </a-form-item>
- </template>
- </template>
- </template>
- </a-form>
- <!-- ====== FinalMask (TCP/UDP masks + QUIC params) ====== -->
- <FinalMaskForm :stream="inbound.stream" :protocol="protocol" />
- </a-tab-pane>
- <!-- ============================== SNIFFING ============================== -->
- <a-tab-pane key="sniffing" tab="Sniffing"><!-- "Sniffing" stays literal — xray config term -->
- <a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
- <a-form-item label="Enabled">
- <a-switch v-model:checked="inbound.sniffing.enabled" />
- </a-form-item>
- <template v-if="inbound.sniffing.enabled">
- <a-form-item :wrapper-col="{ span: 24 }">
- <a-checkbox-group v-model:value="inbound.sniffing.destOverride">
- <a-checkbox v-for="(value, key) in SNIFFING_OPTION" :key="key" :value="value">{{ key }}</a-checkbox>
- </a-checkbox-group>
- </a-form-item>
- <a-form-item label="Metadata only">
- <a-switch v-model:checked="inbound.sniffing.metadataOnly" />
- </a-form-item>
- <a-form-item label="Route only">
- <a-switch v-model:checked="inbound.sniffing.routeOnly" />
- </a-form-item>
- <a-form-item label="IPs excluded">
- <a-select v-model:value="inbound.sniffing.ipsExcluded" mode="tags" :token-separators="[',']"
- placeholder="IP/CIDR/geoip:*/ext:*" :style="{ width: '100%' }" />
- </a-form-item>
- <a-form-item label="Domains excluded">
- <a-select v-model:value="inbound.sniffing.domainsExcluded" mode="tags" :token-separators="[',']"
- placeholder="domain:*/ext:*" :style="{ width: '100%' }" />
- </a-form-item>
- </template>
- </a-form>
- </a-tab-pane>
- <!-- ============================== ADVANCED ============================== -->
- <a-tab-pane key="advanced" :tab="t('pages.xray.advancedTemplate')">
- <div class="advanced-shell">
- <div class="advanced-panel">
- <div class="advanced-panel__header">
- <div>
- <div class="advanced-panel__title">Inbound JSON sections</div>
- <div class="advanced-panel__subtitle">
- Full inbound JSON and focused editors for settings, sniffing, and streamSettings.
- </div>
- </div>
- </div>
- <a-tabs v-model:active-key="advancedSectionKey" class="advanced-inner-tabs">
- <a-tab-pane key="all" tab="All">
- <div class="advanced-editor-meta">
- Full inbound object with all fields in one editor.
- </div>
- <JsonEditor v-model:value="advancedAllConfig" min-height="340px" max-height="560px" />
- </a-tab-pane>
- <a-tab-pane key="settings" tab="Settings">
- <div class="advanced-editor-meta">
- Xray settings block wrapper:
- <code>{ settings: { ... } }</code>.
- </div>
- <JsonEditor v-model:value="advancedSettingsConfig" min-height="320px" max-height="540px" />
- </a-tab-pane>
- <a-tab-pane key="sniffingSection" tab="Sniffing">
- <div class="advanced-editor-meta">
- Xray sniffing block wrapper:
- <code>{ sniffing: { ... } }</code>.
- </div>
- <JsonEditor v-model:value="advancedSniffingConfig" min-height="240px" max-height="420px" />
- </a-tab-pane>
- <a-tab-pane v-if="canEnableStream" key="streamSection" tab="Stream">
- <div class="advanced-editor-meta">
- Xray stream block wrapper:
- <code>{ streamSettings: { ... } }</code>.
- </div>
- <JsonEditor v-model:value="advancedStreamConfig" min-height="320px" max-height="540px" />
- </a-tab-pane>
- </a-tabs>
- </div>
- </div>
- </a-tab-pane>
- </a-tabs>
- </a-modal>
- </template>
- <style scoped>
- .mt-4 {
- margin-top: 4px;
- }
- .mt-8 {
- margin-top: 8px;
- }
- .mt-12 {
- margin-top: 12px;
- }
- .mb-4 {
- margin-bottom: 4px;
- }
- .mb-8 {
- margin-bottom: 8px;
- }
- .mb-12 {
- margin-bottom: 12px;
- }
- .random-icon {
- margin-left: 4px;
- cursor: pointer;
- color: var(--ant-primary-color, #1890ff);
- }
- .danger-icon {
- margin-left: 6px;
- cursor: pointer;
- color: #ff4d4f;
- }
- .vless-auth-state {
- display: block;
- margin-top: 6px;
- }
- .client-summary {
- width: 100%;
- border-collapse: collapse;
- }
- .client-summary th,
- .client-summary td {
- padding: 4px 8px;
- text-align: left;
- border-bottom: 1px solid rgba(128, 128, 128, 0.15);
- }
- .fallbacks-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin: 8px 0;
- }
- .fallbacks-title {
- font-weight: 500;
- flex: 1;
- }
- .wg-peer {
- margin-top: 4px;
- }
- .advanced-shell {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- .advanced-panel {
- padding: 14px;
- border: 1px solid rgba(128, 128, 128, 0.18);
- border-radius: 12px;
- background: rgba(128, 128, 128, 0.04);
- }
- .advanced-panel__header {
- margin-bottom: 12px;
- }
- .advanced-panel__title {
- font-size: 14px;
- font-weight: 600;
- line-height: 1.4;
- }
- .advanced-panel__subtitle {
- margin-top: 4px;
- color: rgba(0, 0, 0, 0.6);
- line-height: 1.5;
- }
- .advanced-inner-tabs :deep(.ant-tabs-nav) {
- margin-bottom: 12px;
- }
- .advanced-inner-tabs :deep(.ant-tabs-tab) {
- padding-inline: 14px;
- }
- .advanced-editor-meta {
- margin-bottom: 10px;
- color: rgba(0, 0, 0, 0.65);
- line-height: 1.5;
- }
- @media (max-width: 768px) {
- .advanced-panel {
- padding: 12px;
- border-radius: 10px;
- }
- .advanced-inner-tabs :deep(.ant-tabs-tab) {
- padding-inline: 10px;
- }
- }
- :global(.dark) .advanced-panel__subtitle,
- :global(.dark) .advanced-editor-meta,
- :global(.ultra) .advanced-panel__subtitle,
- :global(.ultra) .advanced-editor-meta {
- color: rgba(255, 255, 255, 0.65);
- }
- :global(.dark) .advanced-panel,
- :global(.ultra) .advanced-panel {
- border-color: rgba(255, 255, 255, 0.12);
- background: rgba(255, 255, 255, 0.03);
- }
- .section-heading {
- font-weight: 500;
- margin: 12px 0 6px;
- opacity: 0.85;
- }
- </style>
|