InboundFormModal.vue 99 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371
  1. <script setup>
  2. import { computed, ref, watch } from 'vue';
  3. import { useI18n } from 'vue-i18n';
  4. import dayjs from 'dayjs';
  5. import { message } from 'ant-design-vue';
  6. import { SyncOutlined, PlusOutlined, MinusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
  7. import {
  8. HttpUtil,
  9. RandomUtil,
  10. NumberFormatter,
  11. SizeFormatter,
  12. Wireguard,
  13. } from '@/utils';
  14. import { getRandomRealityTarget } from '@/models/reality-targets';
  15. import {
  16. Inbound,
  17. Protocols,
  18. SSMethods,
  19. USERS_SECURITY,
  20. TLS_FLOW_CONTROL,
  21. SNIFFING_OPTION,
  22. TLS_VERSION_OPTION,
  23. TLS_CIPHER_OPTION,
  24. UTLS_FINGERPRINT,
  25. ALPN_OPTION,
  26. USAGE_OPTION,
  27. DOMAIN_STRATEGY_OPTION,
  28. TCP_CONGESTION_OPTION,
  29. MODE_OPTION,
  30. } from '@/models/inbound.js';
  31. import { DBInbound } from '@/models/dbinbound.js';
  32. import FinalMaskForm from '@/components/FinalMaskForm.vue';
  33. import DateTimePicker from '@/components/DateTimePicker.vue';
  34. import JsonEditor from '@/components/JsonEditor.vue';
  35. import { useNodeList } from '@/composables/useNodeList.js';
  36. const { t } = useI18n();
  37. // Node selector — Phase 1 multi-node deployment. Shows all enabled
  38. // nodes regardless of online state so the form is usable while a node
  39. // is briefly offline; the backend's fail-fast path will surface the
  40. // real error when the user submits.
  41. const { nodes: availableNodes } = useNodeList();
  42. const selectableNodes = computed(() => (availableNodes.value || []).filter((n) => n.enable));
  43. // Phase 5f-iii-b: structured per-protocol/per-transport forms instead
  44. // of raw JSON textareas. Edits a deeply-reactive Inbound + DBInbound
  45. // pair so the existing model helpers (.toString(), .canEnableTls(),
  46. // genAllLinks(), addPeer(), etc.) keep working unchanged. The
  47. // "Advanced" tab still exposes the full streamSettings JSON for
  48. // transport variants (KCP/XHTTP/sockopt/finalmask) we don't yet have
  49. // dedicated UI for.
  50. const props = defineProps({
  51. open: { type: Boolean, default: false },
  52. mode: { type: String, default: 'add', validator: (v) => ['add', 'edit'].includes(v) },
  53. dbInbound: { type: Object, default: null },
  54. });
  55. const emit = defineEmits(['update:open', 'saved']);
  56. const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'];
  57. const PROTOCOLS = Object.values(Protocols);
  58. const SECURITY_OPTIONS = Object.values(USERS_SECURITY);
  59. const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
  60. // === Reactive state ================================================
  61. // Cloned on every open so cancelling the modal doesn't mutate the row.
  62. const inbound = ref(null);
  63. const dbForm = ref(null);
  64. const saving = ref(false);
  65. const advancedStreamText = ref('');
  66. const advancedSniffingText = ref('');
  67. const advancedSettingsText = ref('');
  68. const activeTabKey = ref('basic');
  69. const advancedSectionKey = ref('all');
  70. // Cached default cert/key paths from /panel/setting/defaultSettings —
  71. // powers the "Set default cert" button on the TLS form.
  72. const defaultCert = ref('');
  73. const defaultKey = ref('');
  74. // Lookup tables for the option dropdowns.
  75. const TLS_VERSIONS = Object.values(TLS_VERSION_OPTION);
  76. const CIPHER_SUITES = Object.entries(TLS_CIPHER_OPTION); // [label, value]
  77. const FINGERPRINTS = Object.values(UTLS_FINGERPRINT);
  78. const ALPNS = Object.values(ALPN_OPTION);
  79. const USAGES = Object.values(USAGE_OPTION);
  80. const DOMAIN_STRATEGIES = Object.values(DOMAIN_STRATEGY_OPTION);
  81. const TCP_CONGESTIONS = Object.values(TCP_CONGESTION_OPTION);
  82. const MODE_OPTIONS = Object.values(MODE_OPTION);
  83. // External proxy is a single switch in the UI but a list in the model:
  84. // flipping it on seeds one row pre-filled with the current host:port.
  85. const externalProxy = computed({
  86. get: () => Array.isArray(inbound.value?.stream?.externalProxy)
  87. && inbound.value.stream.externalProxy.length > 0,
  88. set: (v) => {
  89. if (!inbound.value?.stream) return;
  90. if (v) {
  91. inbound.value.stream.externalProxy = [{
  92. forceTls: 'same',
  93. dest: window.location.hostname,
  94. port: inbound.value.port,
  95. remark: '',
  96. }];
  97. } else {
  98. inbound.value.stream.externalProxy = [];
  99. }
  100. },
  101. });
  102. // Derived helpers — each is a computed off `inbound` so flips of
  103. // protocol / network / security re-render the right blocks.
  104. const protocol = computed(() => inbound.value?.protocol);
  105. const network = computed({
  106. get: () => inbound.value?.stream?.network,
  107. set: (v) => onNetworkChange(v),
  108. });
  109. const security = computed({
  110. get: () => inbound.value?.stream?.security,
  111. set: (v) => { if (inbound.value?.stream) inbound.value.stream.security = v; },
  112. });
  113. const isMultiUser = computed(() => {
  114. if (!inbound.value) return false;
  115. switch (inbound.value.protocol) {
  116. case Protocols.VMESS:
  117. case Protocols.VLESS:
  118. case Protocols.TROJAN:
  119. case Protocols.HYSTERIA:
  120. return true;
  121. case Protocols.SHADOWSOCKS:
  122. return !!inbound.value.isSSMultiUser;
  123. default:
  124. return false;
  125. }
  126. });
  127. const clientsArray = computed(() => {
  128. if (!inbound.value) return [];
  129. switch (inbound.value.protocol) {
  130. case Protocols.VMESS: return inbound.value.settings.vmesses || [];
  131. case Protocols.VLESS: return inbound.value.settings.vlesses || [];
  132. case Protocols.TROJAN: return inbound.value.settings.trojans || [];
  133. case Protocols.SHADOWSOCKS: return inbound.value.settings.shadowsockses || [];
  134. case Protocols.HYSTERIA: return inbound.value.settings.hysterias || [];
  135. default: return [];
  136. }
  137. });
  138. const firstClient = computed(() => clientsArray.value[0] || null);
  139. const canEnableStream = computed(() => inbound.value?.canEnableStream?.() === true);
  140. const canEnableTls = computed(() => inbound.value?.canEnableTls?.() === true);
  141. const canEnableReality = computed(() => inbound.value?.canEnableReality?.() === true);
  142. const canEnableTlsFlow = computed(() => inbound.value?.canEnableTlsFlow?.() === true);
  143. // VLESS/Trojan TLS fallbacks — surfaced in the protocol tab when the
  144. // inbound is on TCP and (for VLESS) using no Xray-side encryption.
  145. const showFallbacks = computed(() => {
  146. if (!inbound.value) return false;
  147. if (inbound.value.stream?.network !== 'tcp') return false;
  148. if (inbound.value.protocol === Protocols.VLESS) {
  149. const enc = inbound.value.settings?.encryption;
  150. return !enc || enc === 'none';
  151. }
  152. return inbound.value.protocol === Protocols.TROJAN;
  153. });
  154. function addFallback() {
  155. inbound.value?.settings?.addFallback?.();
  156. }
  157. function delFallback(idx) {
  158. inbound.value?.settings?.delFallback?.(idx);
  159. }
  160. // Date / GB bridges (legacy used moment via _expiryTime; we go direct).
  161. const expiryDate = computed({
  162. get: () => (dbForm.value?.expiryTime > 0 ? dayjs(dbForm.value.expiryTime) : null),
  163. set: (next) => { if (dbForm.value) dbForm.value.expiryTime = next ? next.valueOf() : 0; },
  164. });
  165. const totalGB = computed({
  166. get: () => (dbForm.value?.total ? Math.round((dbForm.value.total / SizeFormatter.ONE_GB) * 100) / 100 : 0),
  167. set: (gb) => { if (dbForm.value) dbForm.value.total = NumberFormatter.toFixed((gb || 0) * SizeFormatter.ONE_GB, 0); },
  168. });
  169. // Client total/expiry bridges (only relevant in add mode for new clients)
  170. const clientExpiryDate = computed({
  171. get: () => (firstClient.value?.expiryTime > 0 ? dayjs(firstClient.value.expiryTime) : null),
  172. set: (next) => { if (firstClient.value) firstClient.value.expiryTime = next ? next.valueOf() : 0; },
  173. });
  174. const clientTotalGB = computed({
  175. get: () => firstClient.value?._totalGB ?? 0,
  176. set: (gb) => { if (firstClient.value) firstClient.value._totalGB = gb || 0; },
  177. });
  178. // === Open / state management =======================================
  179. function loadFromDbInbound(dbIn) {
  180. // Round-trip through Inbound.fromJson so subsequent edits get the
  181. // structured class hierarchy (StreamSettings, TLS, Reality, etc.).
  182. const parsed = Inbound.fromJson(dbIn.toInbound().toJson());
  183. inbound.value = parsed;
  184. // DBForm carries the persisted-fields the parsed Inbound doesn't:
  185. // remark, enable, total, expiryTime, trafficReset, etc.
  186. dbForm.value = new DBInbound(dbIn);
  187. primeAdvancedJson();
  188. }
  189. function makeFreshInbound(proto) {
  190. const ib = new Inbound();
  191. ib.protocol = proto;
  192. ib.settings = Inbound.Settings.getSettings(proto);
  193. ib.port = RandomUtil.randomInteger(10000, 60000);
  194. return ib;
  195. }
  196. function freshDbForm() {
  197. const next = new DBInbound();
  198. next.enable = true;
  199. next.remark = '';
  200. next.total = 0;
  201. next.expiryTime = 0;
  202. next.trafficReset = 'never';
  203. return next;
  204. }
  205. function primeAdvancedJson() {
  206. if (!inbound.value) return;
  207. // Only set stream text for protocols that support it
  208. if (canEnableStream.value) {
  209. try {
  210. advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
  211. } catch (_e) { /* keep prior text */ }
  212. } else {
  213. advancedStreamText.value = '{}';
  214. }
  215. try {
  216. advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
  217. } catch (_e) { /* keep prior text */ }
  218. try {
  219. advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
  220. } catch (_e) { /* keep prior text */ }
  221. }
  222. watch(() => props.open, (next) => {
  223. if (!next) return;
  224. if (props.mode === 'edit' && props.dbInbound) {
  225. loadFromDbInbound(props.dbInbound);
  226. } else {
  227. inbound.value = makeFreshInbound(Protocols.VLESS);
  228. dbForm.value = freshDbForm();
  229. primeAdvancedJson();
  230. }
  231. activeTabKey.value = 'basic';
  232. advancedSectionKey.value = 'all';
  233. fetchDefaultCertSettings();
  234. });
  235. function applyAdvancedJsonToBasic() {
  236. if (!inbound.value) return true;
  237. let parsedSettings;
  238. let parsedStream;
  239. let parsedSniffing;
  240. try {
  241. parsedSettings = advancedSettingsText.value.trim()
  242. ? JSON.parse(advancedSettingsText.value)
  243. : inbound.value.settings?.toJson?.();
  244. } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return false; }
  245. try {
  246. parsedStream = advancedStreamText.value.trim()
  247. ? JSON.parse(advancedStreamText.value)
  248. : inbound.value.stream?.toJson?.();
  249. } catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return false; }
  250. try {
  251. parsedSniffing = advancedSniffingText.value.trim()
  252. ? JSON.parse(advancedSniffingText.value)
  253. : inbound.value.sniffing?.toJson?.();
  254. } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return false; }
  255. try {
  256. inbound.value = Inbound.fromJson({
  257. port: inbound.value.port,
  258. listen: inbound.value.listen,
  259. protocol: inbound.value.protocol,
  260. settings: parsedSettings,
  261. streamSettings: parsedStream,
  262. tag: inbound.value.tag,
  263. sniffing: parsedSniffing,
  264. clientStats: inbound.value.clientStats,
  265. });
  266. } catch (e) {
  267. message.error(`Advanced JSON: ${e.message}`);
  268. return false;
  269. }
  270. return true;
  271. }
  272. let isRevertingTab = false;
  273. watch(activeTabKey, (next, prev) => {
  274. if (isRevertingTab) { isRevertingTab = false; return; }
  275. if (prev === 'advanced' && next !== 'advanced') {
  276. if (!applyAdvancedJsonToBasic()) {
  277. isRevertingTab = true;
  278. activeTabKey.value = 'advanced';
  279. }
  280. }
  281. });
  282. // In add mode, switching protocol restamps settings + re-syncs port.
  283. function onProtocolChange(next) {
  284. if (props.mode === 'edit' || !inbound.value) return;
  285. inbound.value.protocol = next;
  286. inbound.value.settings = Inbound.Settings.getSettings(next);
  287. primeAdvancedJson();
  288. }
  289. function onNetworkChange(next) {
  290. if (!inbound.value?.stream) return;
  291. inbound.value.stream.network = next;
  292. // Mirror legacy streamNetworkChange: clear flow when TLS/Reality
  293. // become unavailable; reset finalmask.udp when not KCP.
  294. if (!inbound.value.canEnableTls()) inbound.value.stream.security = 'none';
  295. if (!inbound.value.canEnableReality()) inbound.value.reality = false;
  296. if (
  297. inbound.value.protocol === Protocols.VLESS
  298. && !inbound.value.canEnableTlsFlow()
  299. && Array.isArray(inbound.value.settings.vlesses)
  300. ) {
  301. inbound.value.settings.vlesses.forEach((c) => { c.flow = ''; });
  302. }
  303. if (next !== 'kcp' && inbound.value.stream.finalmask) {
  304. inbound.value.stream.finalmask.udp = [];
  305. }
  306. }
  307. function parseAdvancedSliceOrFallback(rawText, fallbackValue) {
  308. if (!rawText?.trim()) return fallbackValue;
  309. return JSON.parse(rawText);
  310. }
  311. function unwrapWrappedObject(parsed, key) {
  312. if (
  313. parsed
  314. && typeof parsed === 'object'
  315. && !Array.isArray(parsed)
  316. && parsed[key] !== undefined
  317. ) {
  318. return parsed[key];
  319. }
  320. return parsed;
  321. }
  322. const advancedAllConfig = computed({
  323. get: () => {
  324. if (!inbound.value) return '';
  325. try {
  326. const settings = parseAdvancedSliceOrFallback(
  327. advancedSettingsText.value,
  328. inbound.value.settings?.toJson?.() || {},
  329. );
  330. const streamSettings = parseAdvancedSliceOrFallback(
  331. advancedStreamText.value,
  332. inbound.value.stream?.toJson?.() || {},
  333. );
  334. const sniffing = parseAdvancedSliceOrFallback(
  335. advancedSniffingText.value,
  336. inbound.value.sniffing?.toJson?.() || {},
  337. );
  338. const result = {
  339. listen: inbound.value.listen,
  340. port: inbound.value.port,
  341. protocol: inbound.value.protocol,
  342. settings,
  343. sniffing,
  344. tag: inbound.value.tag,
  345. };
  346. // Only include streamSettings for protocols that support it
  347. if (canEnableStream.value) {
  348. result.streamSettings = streamSettings;
  349. }
  350. return JSON.stringify(result, null, 2);
  351. } catch (_e) {
  352. return '';
  353. }
  354. },
  355. set: (next) => {
  356. let parsed;
  357. try {
  358. parsed = JSON.parse(next);
  359. } catch (e) {
  360. message.error(`All JSON invalid: ${e.message}`);
  361. return;
  362. }
  363. if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
  364. message.error('All JSON must be an inbound object.');
  365. return;
  366. }
  367. try {
  368. if (typeof parsed.listen === 'string') {
  369. inbound.value.listen = parsed.listen;
  370. }
  371. if (parsed.port !== undefined) {
  372. const parsedPort = Number(parsed.port);
  373. if (!Number.isNaN(parsedPort) && Number.isFinite(parsedPort)) {
  374. inbound.value.port = parsedPort;
  375. }
  376. }
  377. if (typeof parsed.protocol === 'string' && PROTOCOLS.includes(parsed.protocol)) {
  378. inbound.value.protocol = parsed.protocol;
  379. }
  380. if (typeof parsed.tag === 'string') {
  381. inbound.value.tag = parsed.tag;
  382. }
  383. const existingSettings = parseAdvancedSliceOrFallback(
  384. advancedSettingsText.value,
  385. inbound.value?.settings?.toJson?.() || {},
  386. );
  387. const settings = parsed.settings ?? existingSettings;
  388. const sniffing = parsed.sniffing ?? (inbound.value?.sniffing?.toJson?.() || {});
  389. advancedSettingsText.value = JSON.stringify(settings, null, 2);
  390. advancedSniffingText.value = JSON.stringify(sniffing, null, 2);
  391. // Only update stream settings if protocol supports it
  392. if (canEnableStream.value) {
  393. const streamSettings = parsed.streamSettings ?? (inbound.value?.stream?.toJson?.() || {});
  394. advancedStreamText.value = JSON.stringify(streamSettings, null, 2);
  395. } else {
  396. advancedStreamText.value = '{}';
  397. }
  398. } catch (e) {
  399. message.error(`All JSON invalid: ${e.message}`);
  400. }
  401. },
  402. });
  403. const advancedSettingsConfig = computed({
  404. get: () => {
  405. if (!inbound.value) return '';
  406. try {
  407. const settings = parseAdvancedSliceOrFallback(
  408. advancedSettingsText.value,
  409. inbound.value.settings?.toJson?.() || {},
  410. );
  411. return JSON.stringify({
  412. settings,
  413. }, null, 2);
  414. } catch (_e) {
  415. return '';
  416. }
  417. },
  418. set: (next) => {
  419. let parsed;
  420. try {
  421. parsed = JSON.parse(next);
  422. } catch (e) {
  423. message.error(`Settings JSON invalid: ${e.message}`);
  424. return;
  425. }
  426. const unwrapped = unwrapWrappedObject(parsed, 'settings');
  427. if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
  428. message.error('Settings JSON must be an object or { settings: { ... } }.');
  429. return;
  430. }
  431. try {
  432. advancedSettingsText.value = JSON.stringify(unwrapped, null, 2);
  433. } catch (e) {
  434. message.error(`Settings JSON invalid: ${e.message}`);
  435. }
  436. },
  437. });
  438. const advancedSniffingConfig = computed({
  439. get: () => {
  440. if (!inbound.value) return '';
  441. try {
  442. const sniffing = parseAdvancedSliceOrFallback(
  443. advancedSniffingText.value,
  444. inbound.value.sniffing?.toJson?.() || {},
  445. );
  446. return JSON.stringify({ sniffing }, null, 2);
  447. } catch (_e) {
  448. return '';
  449. }
  450. },
  451. set: (next) => {
  452. let parsed;
  453. try {
  454. parsed = JSON.parse(next);
  455. } catch (e) {
  456. message.error(`Sniffing JSON invalid: ${e.message}`);
  457. return;
  458. }
  459. const unwrapped = unwrapWrappedObject(parsed, 'sniffing');
  460. if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
  461. message.error('Sniffing JSON must be an object or { sniffing: { ... } }.');
  462. return;
  463. }
  464. try {
  465. advancedSniffingText.value = JSON.stringify(unwrapped, null, 2);
  466. } catch (e) {
  467. message.error(`Sniffing JSON invalid: ${e.message}`);
  468. }
  469. },
  470. });
  471. const advancedStreamConfig = computed({
  472. get: () => {
  473. if (!inbound.value) return '';
  474. try {
  475. const streamSettings = parseAdvancedSliceOrFallback(
  476. advancedStreamText.value,
  477. inbound.value.stream?.toJson?.() || {},
  478. );
  479. return JSON.stringify({ streamSettings }, null, 2);
  480. } catch (_e) {
  481. return '';
  482. }
  483. },
  484. set: (next) => {
  485. let parsed;
  486. try {
  487. parsed = JSON.parse(next);
  488. } catch (e) {
  489. message.error(`Stream JSON invalid: ${e.message}`);
  490. return;
  491. }
  492. const unwrapped = unwrapWrappedObject(parsed, 'streamSettings');
  493. if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
  494. message.error('Stream JSON must be an object or { streamSettings: { ... } }.');
  495. return;
  496. }
  497. try {
  498. advancedStreamText.value = JSON.stringify(unwrapped, null, 2);
  499. } catch (e) {
  500. message.error(`Stream JSON invalid: ${e.message}`);
  501. }
  502. },
  503. });
  504. // === Random helpers wired to the form's sync icons ==================
  505. function randomEmail(target) {
  506. if (target) target.email = RandomUtil.randomLowerAndNum(9);
  507. }
  508. function randomUuid(target) {
  509. if (target) target.id = RandomUtil.randomUUID();
  510. }
  511. function randomPasswordSeq(target, len = 10) {
  512. if (target) target.password = RandomUtil.randomSeq(len);
  513. }
  514. function randomSSPassword(target) {
  515. if (target) target.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
  516. }
  517. function randomAuth(target) {
  518. if (target) target.auth = RandomUtil.randomSeq(10);
  519. }
  520. function randomSubId(target) {
  521. if (target) target.subId = RandomUtil.randomLowerAndNum(16);
  522. }
  523. function regenWgKeypair(target) {
  524. const kp = Wireguard.generateKeypair();
  525. target.publicKey = kp.publicKey;
  526. target.privateKey = kp.privateKey;
  527. }
  528. function regenInboundWg() {
  529. const kp = Wireguard.generateKeypair();
  530. inbound.value.settings.pubKey = kp.publicKey;
  531. inbound.value.settings.secretKey = kp.privateKey;
  532. }
  533. // === Reality keygen via existing API =================================
  534. async function genRealityKeypair() {
  535. saving.value = true;
  536. try {
  537. const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
  538. if (msg?.success) {
  539. inbound.value.stream.reality.privateKey = msg.obj.privateKey;
  540. inbound.value.stream.reality.settings.publicKey = msg.obj.publicKey;
  541. }
  542. } finally {
  543. saving.value = false;
  544. }
  545. }
  546. function clearRealityKeypair() {
  547. if (!inbound.value?.stream?.reality) return;
  548. inbound.value.stream.reality.privateKey = '';
  549. inbound.value.stream.reality.settings.publicKey = '';
  550. }
  551. async function genMldsa65() {
  552. saving.value = true;
  553. try {
  554. const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
  555. if (msg?.success) {
  556. inbound.value.stream.reality.mldsa65Seed = msg.obj.seed;
  557. inbound.value.stream.reality.settings.mldsa65Verify = msg.obj.verify;
  558. }
  559. } finally {
  560. saving.value = false;
  561. }
  562. }
  563. function clearMldsa65() {
  564. if (!inbound.value?.stream?.reality) return;
  565. inbound.value.stream.reality.mldsa65Seed = '';
  566. inbound.value.stream.reality.settings.mldsa65Verify = '';
  567. }
  568. function randomizeRealityTarget() {
  569. if (!inbound.value?.stream?.reality) return;
  570. const t = getRandomRealityTarget();
  571. inbound.value.stream.reality.target = t.target;
  572. inbound.value.stream.reality.serverNames = t.sni;
  573. }
  574. function randomizeShortIds() {
  575. if (!inbound.value?.stream?.reality) return;
  576. inbound.value.stream.reality.shortIds = RandomUtil.randomShortIds();
  577. }
  578. // === ECH cert helpers ================================================
  579. async function getNewEchCert() {
  580. if (!inbound.value?.stream?.tls) return;
  581. saving.value = true;
  582. try {
  583. const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', {
  584. sni: inbound.value.stream.tls.sni,
  585. });
  586. if (msg?.success) {
  587. inbound.value.stream.tls.echServerKeys = msg.obj.echServerKeys;
  588. inbound.value.stream.tls.settings.echConfigList = msg.obj.echConfigList;
  589. }
  590. } finally {
  591. saving.value = false;
  592. }
  593. }
  594. function clearEchCert() {
  595. if (!inbound.value?.stream?.tls) return;
  596. inbound.value.stream.tls.echServerKeys = '';
  597. inbound.value.stream.tls.settings.echConfigList = '';
  598. }
  599. function setDefaultCertData(idx) {
  600. if (!inbound.value?.stream?.tls?.certs?.[idx]) return;
  601. inbound.value.stream.tls.certs[idx].certFile = defaultCert.value;
  602. inbound.value.stream.tls.certs[idx].keyFile = defaultKey.value;
  603. }
  604. async function fetchDefaultCertSettings() {
  605. try {
  606. const msg = await HttpUtil.post('/panel/setting/defaultSettings');
  607. if (msg?.success && msg.obj) {
  608. defaultCert.value = msg.obj.defaultCert || '';
  609. defaultKey.value = msg.obj.defaultKey || '';
  610. }
  611. } catch (_e) { /* non-fatal — leave Set Default disabled */ }
  612. }
  613. // === VLESS encryption helpers =======================================
  614. // `xray vlessenc` returns both X25519 and ML-KEM-768 auth variants every
  615. // call; the user clicks one button to pick which block goes into
  616. // decryption/encryption. Both generated strings share the same hybrid
  617. // mlkem768x25519plus prefix; the auth choice is the final key block.
  618. function normalizeVlessAuthLabel(label = '') {
  619. return label.toLowerCase().replace(/[-_\s]/g, '');
  620. }
  621. function matchesVlessAuth(block, authId) {
  622. if (block?.id === authId) return true;
  623. const label = normalizeVlessAuthLabel(block?.label);
  624. if (authId === 'mlkem768') return label.includes('mlkem768');
  625. if (authId === 'x25519') return label.includes('x25519');
  626. return false;
  627. }
  628. async function getNewVlessEnc(authId) {
  629. if (!authId || !inbound.value?.settings) return;
  630. saving.value = true;
  631. try {
  632. const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
  633. if (!msg?.success) return;
  634. const block = (msg.obj?.auths || []).find((a) => matchesVlessAuth(a, authId));
  635. if (!block) return;
  636. inbound.value.settings.decryption = block.decryption;
  637. inbound.value.settings.encryption = block.encryption;
  638. } finally {
  639. saving.value = false;
  640. }
  641. }
  642. function clearVlessEnc() {
  643. if (!inbound.value?.settings) return;
  644. inbound.value.settings.decryption = 'none';
  645. inbound.value.settings.encryption = 'none';
  646. }
  647. const selectedVlessAuth = computed(() => {
  648. const encryption = inbound.value?.settings?.encryption;
  649. if (!encryption || encryption === 'none') return 'None';
  650. const parts = encryption.split('.').filter(Boolean);
  651. const authKey = parts[parts.length - 1] || '';
  652. if (!authKey) return 'Custom';
  653. return authKey.length > 300 ? 'ML-KEM-768 auth' : 'X25519 auth';
  654. });
  655. // === SS method change tracks legacy semantics =========================
  656. function onSSMethodChange() {
  657. inbound.value.settings.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
  658. if (inbound.value.isSSMultiUser) {
  659. if (inbound.value.settings.shadowsockses.length === 0) {
  660. inbound.value.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
  661. }
  662. inbound.value.settings.shadowsockses.forEach((c) => {
  663. c.method = inbound.value.isSS2022 ? '' : inbound.value.settings.method;
  664. c.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
  665. });
  666. } else {
  667. inbound.value.settings.shadowsockses = [];
  668. }
  669. }
  670. // === Submit ==========================================================
  671. function close() {
  672. emit('update:open', false);
  673. }
  674. async function submit() {
  675. if (!inbound.value || !dbForm.value) return;
  676. saving.value = true;
  677. try {
  678. // Sniffing tab is structured; stream stays JSON for unsupported
  679. // transports — both go to wire as serialized JSON.
  680. let streamSettings;
  681. let sniffing;
  682. let settings;
  683. try {
  684. streamSettings = canEnableStream.value
  685. ? JSON.stringify(JSON.parse(advancedStreamText.value))
  686. : (inbound.value.stream?.sockopt
  687. ? JSON.stringify({ sockopt: inbound.value.stream.sockopt.toJson() })
  688. : '');
  689. } catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return; }
  690. try {
  691. sniffing = JSON.stringify(JSON.parse(advancedSniffingText.value || inbound.value.sniffing.toString()));
  692. } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return; }
  693. try {
  694. settings = JSON.stringify(JSON.parse(advancedSettingsText.value || inbound.value.settings.toString()));
  695. } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return; }
  696. // The structured form mutates `inbound.stream` directly when the
  697. // user edits TCP/WS/gRPC/HTTPUpgrade fields, but if they touched
  698. // the Advanced JSON tab their edits live there. Keep the JSON tab
  699. // authoritative — it was populated from the live model on open
  700. // and watch handlers below sync in either direction.
  701. const payload = {
  702. up: dbForm.value.up || 0,
  703. down: dbForm.value.down || 0,
  704. total: dbForm.value.total,
  705. remark: dbForm.value.remark,
  706. enable: dbForm.value.enable,
  707. expiryTime: dbForm.value.expiryTime,
  708. trafficReset: dbForm.value.trafficReset,
  709. lastTrafficResetTime: dbForm.value.lastTrafficResetTime || 0,
  710. listen: inbound.value.listen,
  711. port: inbound.value.port,
  712. protocol: inbound.value.protocol,
  713. settings: settings,
  714. streamSettings: streamSettings,
  715. sniffing: sniffing,
  716. };
  717. // Multi-node deployment: only include nodeId when the user picked a
  718. // remote node. Sending nodeId=null over qs.stringify becomes an
  719. // empty form value, which Go's form binding for *int parses as 0
  720. // — not nil — and we'd then try to look up node id 0 and fail with
  721. // "record not found". Omitting the key entirely keeps NodeID nil.
  722. if (dbForm.value.nodeId != null) {
  723. payload.nodeId = dbForm.value.nodeId;
  724. }
  725. const url = props.mode === 'edit'
  726. ? `/panel/api/inbounds/update/${props.dbInbound.id}`
  727. : '/panel/api/inbounds/add';
  728. const msg = await HttpUtil.post(url, payload);
  729. if (msg?.success) {
  730. emit('saved');
  731. close();
  732. }
  733. } finally {
  734. saving.value = false;
  735. }
  736. }
  737. const title = computed(() =>
  738. props.mode === 'edit'
  739. ? t('pages.inbounds.modifyInbound')
  740. : t('pages.inbounds.addInbound'),
  741. );
  742. const okText = computed(() =>
  743. props.mode === 'edit' ? t('pages.client.submitEdit') : t('create'),
  744. );
  745. // Whenever the structured form mutates stream / sniffing / settings,
  746. // refresh the matching slice of the Advanced JSON tab so the user
  747. // always sees the live state — flipping a switch in Sniffing or
  748. // editing encryption in Protocol now reflects in Advanced.
  749. watch(
  750. () => inbound.value && JSON.stringify(inbound.value.stream?.toJson?.() || {}),
  751. () => {
  752. if (!inbound.value?.stream) return;
  753. // Only update stream text for protocols that support it
  754. if (!canEnableStream.value) {
  755. advancedStreamText.value = '{}';
  756. return;
  757. }
  758. try {
  759. advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
  760. } catch (_e) { /* leave as is */ }
  761. },
  762. );
  763. watch(
  764. () => inbound.value && JSON.stringify(inbound.value.sniffing?.toJson?.() || {}),
  765. () => {
  766. if (!inbound.value?.sniffing) return;
  767. try {
  768. advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
  769. } catch (_e) { /* leave as is */ }
  770. },
  771. );
  772. watch(
  773. () => inbound.value && JSON.stringify(inbound.value.settings?.toJson?.() || {}),
  774. () => {
  775. if (!inbound.value?.settings) return;
  776. try {
  777. advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
  778. } catch (_e) { /* leave as is */ }
  779. },
  780. );
  781. // Watch protocol changes to clear stream settings for protocols that don't support it
  782. watch(
  783. () => inbound.value?.protocol,
  784. () => {
  785. if (!inbound.value) return;
  786. if (!canEnableStream.value) {
  787. advancedStreamText.value = '{}';
  788. }
  789. },
  790. );
  791. </script>
  792. <template>
  793. <a-modal :open="open" :title="title" :ok-text="okText" :cancel-text="t('close')" :confirm-loading="saving"
  794. :mask-closable="false" width="780px" @ok="submit" @cancel="close">
  795. <a-tabs v-if="inbound && dbForm" v-model:active-key="activeTabKey">
  796. <!-- ============================== BASICS ============================== -->
  797. <a-tab-pane key="basic" :tab="t('pages.xray.basicTemplate')">
  798. <a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
  799. <a-form-item :label="t('enable')">
  800. <a-switch v-model:checked="dbForm.enable" />
  801. </a-form-item>
  802. <a-form-item :label="t('pages.inbounds.remark')">
  803. <a-input v-model:value="dbForm.remark" />
  804. </a-form-item>
  805. <a-form-item v-if="selectableNodes.length > 0" :label="t('pages.inbounds.deployTo')">
  806. <a-select v-model:value="dbForm.nodeId" :disabled="mode === 'edit'"
  807. :placeholder="t('pages.inbounds.localPanel')" allow-clear>
  808. <a-select-option :value="null">{{ t('pages.inbounds.localPanel') }}</a-select-option>
  809. <a-select-option v-for="n in selectableNodes" :key="n.id" :value="n.id"
  810. :disabled="n.status === 'offline'">
  811. {{ n.name }}{{ n.status === 'offline' ? ' (offline)' : '' }}
  812. </a-select-option>
  813. </a-select>
  814. </a-form-item>
  815. <a-form-item :label="t('pages.inbounds.protocol')">
  816. <a-select :value="protocol" :disabled="mode === 'edit'" @change="onProtocolChange">
  817. <a-select-option v-for="p in PROTOCOLS" :key="p" :value="p">{{ p }}</a-select-option>
  818. </a-select>
  819. </a-form-item>
  820. <a-form-item :label="t('pages.inbounds.address')">
  821. <a-input v-model:value="inbound.listen" :placeholder="t('pages.inbounds.monitorDesc')" />
  822. </a-form-item>
  823. <a-form-item :label="t('pages.inbounds.port')">
  824. <a-input-number v-model:value="inbound.port" :min="1" :max="65535" />
  825. </a-form-item>
  826. <a-form-item>
  827. <template #label>
  828. <a-tooltip :title="t('pages.inbounds.meansNoLimit')">{{ t('pages.inbounds.totalFlow') }}</a-tooltip>
  829. </template>
  830. <a-input-number v-model:value="totalGB" :min="0" :step="0.1" />
  831. </a-form-item>
  832. <a-form-item :label="t('pages.inbounds.periodicTrafficResetTitle')">
  833. <a-select v-model:value="dbForm.trafficReset">
  834. <a-select-option v-for="r in TRAFFIC_RESETS" :key="r" :value="r">
  835. {{ t(`pages.inbounds.periodicTrafficReset.${r}`) }}
  836. </a-select-option>
  837. </a-select>
  838. </a-form-item>
  839. <a-form-item>
  840. <template #label>
  841. <a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate')
  842. }}</a-tooltip>
  843. </template>
  844. <DateTimePicker v-model:value="expiryDate" />
  845. </a-form-item>
  846. </a-form>
  847. </a-tab-pane>
  848. <!-- ============================== PROTOCOL ============================== -->
  849. <a-tab-pane key="protocol" :tab="t('pages.inbounds.protocol')">
  850. <!-- Multi-user inbounds: in add mode embed the first client form,
  851. in edit mode show a count summary. -->
  852. <template v-if="isMultiUser">
  853. <a-collapse v-if="mode === 'add' && firstClient" default-active-key="0">
  854. <a-collapse-panel key="0" header="Client">
  855. <a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
  856. <a-form-item label="Enable">
  857. <a-switch v-model:checked="firstClient.enable" />
  858. </a-form-item>
  859. <a-form-item>
  860. <template #label>
  861. <a-tooltip title="Friendly identifier">
  862. Email
  863. <SyncOutlined class="random-icon" @click="randomEmail(firstClient)" />
  864. </a-tooltip>
  865. </template>
  866. <a-input v-model:value="firstClient.email" />
  867. </a-form-item>
  868. <a-form-item v-if="protocol === Protocols.VMESS || protocol === Protocols.VLESS">
  869. <template #label>
  870. <a-tooltip title="Reset to a fresh UUID">
  871. ID
  872. <SyncOutlined class="random-icon" @click="randomUuid(firstClient)" />
  873. </a-tooltip>
  874. </template>
  875. <a-input v-model:value="firstClient.id" />
  876. </a-form-item>
  877. <a-form-item v-if="protocol === Protocols.VMESS" label="Security">
  878. <a-select v-model:value="firstClient.security">
  879. <a-select-option v-for="k in SECURITY_OPTIONS" :key="k" :value="k">{{ k }}</a-select-option>
  880. </a-select>
  881. </a-form-item>
  882. <a-form-item v-if="protocol === Protocols.TROJAN || protocol === Protocols.SHADOWSOCKS">
  883. <template #label>
  884. <a-tooltip title="Reset to a fresh random value">
  885. Password
  886. <SyncOutlined v-if="protocol === Protocols.SHADOWSOCKS" class="random-icon"
  887. @click="randomSSPassword(firstClient)" />
  888. <SyncOutlined v-else class="random-icon" @click="randomPasswordSeq(firstClient)" />
  889. </a-tooltip>
  890. </template>
  891. <a-input v-model:value="firstClient.password" />
  892. </a-form-item>
  893. <a-form-item v-if="protocol === Protocols.HYSTERIA">
  894. <template #label>
  895. <a-tooltip title="Reset"><span>Auth password</span>
  896. <SyncOutlined class="random-icon" @click="randomAuth(firstClient)" />
  897. </a-tooltip>
  898. </template>
  899. <a-input v-model:value="firstClient.auth" />
  900. </a-form-item>
  901. <a-form-item v-if="canEnableTlsFlow" label="Flow">
  902. <a-select v-model:value="firstClient.flow">
  903. <a-select-option value="">none</a-select-option>
  904. <a-select-option v-for="k in FLOW_OPTIONS" :key="k" :value="k">{{ k }}</a-select-option>
  905. </a-select>
  906. </a-form-item>
  907. <a-form-item v-if="protocol === Protocols.VLESS" label="Reverse tag">
  908. <a-input v-model:value="firstClient.reverseTag" placeholder="Optional reverse tag" />
  909. </a-form-item>
  910. <a-form-item label="Subscription">
  911. <a-input v-model:value="firstClient.subId">
  912. <template #addonAfter>
  913. <SyncOutlined class="random-icon" @click="randomSubId(firstClient)" />
  914. </template>
  915. </a-input>
  916. </a-form-item>
  917. <a-form-item label="Comment">
  918. <a-input v-model:value="firstClient.comment" />
  919. </a-form-item>
  920. <a-form-item label="Total traffic (GB)">
  921. <a-input-number v-model:value="clientTotalGB" :min="0" :step="0.1" />
  922. </a-form-item>
  923. <a-form-item label="Expiry">
  924. <DateTimePicker v-model:value="clientExpiryDate" />
  925. </a-form-item>
  926. </a-form>
  927. </a-collapse-panel>
  928. </a-collapse>
  929. <a-collapse v-else>
  930. <a-collapse-panel key="summary" :header="`Clients: ${clientsArray.length}`">
  931. <table class="client-summary">
  932. <thead>
  933. <tr>
  934. <th>Email</th>
  935. <th>{{ protocol === Protocols.TROJAN || protocol === Protocols.SHADOWSOCKS ? 'Password' : (protocol
  936. ===
  937. Protocols.HYSTERIA ? 'Auth' : 'ID') }}</th>
  938. </tr>
  939. </thead>
  940. <tbody>
  941. <tr v-for="(c, idx) in clientsArray" :key="idx">
  942. <td>{{ c.email }}</td>
  943. <td>{{ c.id || c.password || c.auth }}</td>
  944. </tr>
  945. </tbody>
  946. </table>
  947. </a-collapse-panel>
  948. </a-collapse>
  949. </template>
  950. <!-- VLess decryption / encryption -->
  951. <a-form v-if="protocol === Protocols.VLESS" :colon="false" :label-col="{ sm: { span: 8 } }"
  952. :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
  953. <a-form-item label="Decryption">
  954. <a-input v-model:value="inbound.settings.decryption" />
  955. </a-form-item>
  956. <a-form-item label="Encryption">
  957. <a-input v-model:value="inbound.settings.encryption" />
  958. </a-form-item>
  959. <a-form-item label=" ">
  960. <a-space :size="8" wrap>
  961. <a-button type="primary" :loading="saving" @click="getNewVlessEnc('x25519')">
  962. X25519 auth
  963. </a-button>
  964. <a-button type="primary" :loading="saving" @click="getNewVlessEnc('mlkem768')">
  965. ML-KEM-768 auth
  966. </a-button>
  967. <a-button danger @click="clearVlessEnc">Clear</a-button>
  968. </a-space>
  969. <a-typography-text type="secondary" class="vless-auth-state">
  970. Selected: {{ selectedVlessAuth }}
  971. </a-typography-text>
  972. </a-form-item>
  973. </a-form>
  974. <!-- Shadowsocks shared fields (method/network/ivCheck) -->
  975. <a-form v-if="protocol === Protocols.SHADOWSOCKS" :colon="false" :label-col="{ sm: { span: 8 } }"
  976. :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
  977. <a-form-item label="Encryption method">
  978. <a-select v-model:value="inbound.settings.method" @change="onSSMethodChange">
  979. <a-select-option v-for="(m, k) in SSMethods" :key="k" :value="m">{{ k }}</a-select-option>
  980. </a-select>
  981. </a-form-item>
  982. <a-form-item v-if="inbound.isSS2022">
  983. <template #label>
  984. Password
  985. <SyncOutlined class="random-icon" @click="randomSSPassword(inbound.settings)" />
  986. </template>
  987. <a-input v-model:value="inbound.settings.password" />
  988. </a-form-item>
  989. <a-form-item label="Network">
  990. <a-select v-model:value="inbound.settings.network" :style="{ width: '120px' }">
  991. <a-select-option value="tcp,udp">TCP, UDP</a-select-option>
  992. <a-select-option value="tcp">TCP</a-select-option>
  993. <a-select-option value="udp">UDP</a-select-option>
  994. </a-select>
  995. </a-form-item>
  996. <a-form-item label="ivCheck">
  997. <a-switch v-model:checked="inbound.settings.ivCheck" />
  998. </a-form-item>
  999. </a-form>
  1000. <!-- HTTP / Mixed accounts -->
  1001. <a-form v-if="protocol === Protocols.HTTP || protocol === Protocols.MIXED" :colon="false"
  1002. :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
  1003. <a-form-item label="Accounts">
  1004. <a-button size="small" @click="protocol === Protocols.HTTP
  1005. ? inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())
  1006. : inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())">
  1007. <template #icon>
  1008. <PlusOutlined />
  1009. </template>
  1010. Add
  1011. </a-button>
  1012. </a-form-item>
  1013. <a-form-item :wrapper-col="{ span: 24 }">
  1014. <a-input-group v-for="(account, idx) in inbound.settings.accounts" :key="idx" compact class="mb-8">
  1015. <a-input :style="{ width: '45%' }" v-model:value="account.user" placeholder="Username">
  1016. <template #addonBefore>{{ idx + 1 }}</template>
  1017. </a-input>
  1018. <a-input :style="{ width: '45%' }" v-model:value="account.pass" placeholder="Password" />
  1019. <a-button @click="inbound.settings.delAccount(idx)">
  1020. <template #icon>
  1021. <MinusOutlined />
  1022. </template>
  1023. </a-button>
  1024. </a-input-group>
  1025. </a-form-item>
  1026. <a-form-item v-if="protocol === Protocols.HTTP" label="Allow transparent">
  1027. <a-switch v-model:checked="inbound.settings.allowTransparent" />
  1028. </a-form-item>
  1029. <template v-if="protocol === Protocols.MIXED">
  1030. <a-form-item label="Auth">
  1031. <a-select v-model:value="inbound.settings.auth">
  1032. <a-select-option value="noauth">noauth</a-select-option>
  1033. <a-select-option value="password">password</a-select-option>
  1034. </a-select>
  1035. </a-form-item>
  1036. <a-form-item label="UDP">
  1037. <a-switch v-model:checked="inbound.settings.udp" />
  1038. </a-form-item>
  1039. <a-form-item v-if="inbound.settings.udp" label="UDP IP">
  1040. <a-input v-model:value="inbound.settings.ip" />
  1041. </a-form-item>
  1042. </template>
  1043. </a-form>
  1044. <!-- Tunnel -->
  1045. <a-form v-if="protocol === Protocols.TUNNEL" :colon="false" :label-col="{ sm: { span: 8 } }"
  1046. :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
  1047. <a-form-item label="Rewrite address">
  1048. <a-input v-model:value="inbound.settings.rewriteAddress" />
  1049. </a-form-item>
  1050. <a-form-item label="Rewrite port">
  1051. <a-input-number v-model:value="inbound.settings.rewritePort" :min="0" :max="65535" />
  1052. </a-form-item>
  1053. <a-form-item label="Allowed network">
  1054. <a-select v-model:value="inbound.settings.allowedNetwork">
  1055. <a-select-option value="tcp,udp">TCP, UDP</a-select-option>
  1056. <a-select-option value="tcp">TCP</a-select-option>
  1057. <a-select-option value="udp">UDP</a-select-option>
  1058. </a-select>
  1059. </a-form-item>
  1060. <a-form-item label="Port map">
  1061. <a-button size="small" @click="inbound.settings.addPortMap('', '')">
  1062. <template #icon>
  1063. <PlusOutlined />
  1064. </template>
  1065. </a-button>
  1066. </a-form-item>
  1067. <a-form-item v-if="inbound.settings.portMap.length > 0" :wrapper-col="{ span: 24 }">
  1068. <a-input-group v-for="(pm, idx) in inbound.settings.portMap" :key="`pm-${idx}`" compact class="mb-8">
  1069. <a-input :style="{ width: '30%' }" v-model:value="pm.name" placeholder="5555">
  1070. <template #addonBefore>{{ idx + 1 }}</template>
  1071. </a-input>
  1072. <a-input :style="{ width: '60%' }" v-model:value="pm.value" placeholder="1.1.1.1:7777" />
  1073. <a-button @click="inbound.settings.removePortMap(idx)">
  1074. <template #icon>
  1075. <MinusOutlined />
  1076. </template>
  1077. </a-button>
  1078. </a-input-group>
  1079. </a-form-item>
  1080. <a-form-item label="Follow redirect">
  1081. <a-switch v-model:checked="inbound.settings.followRedirect" />
  1082. </a-form-item>
  1083. </a-form>
  1084. <!-- TUN -->
  1085. <a-form v-if="protocol === Protocols.TUN" :colon="false" :label-col="{ sm: { span: 8 } }"
  1086. :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
  1087. <a-form-item label="Interface name">
  1088. <a-input v-model:value="inbound.settings.name" placeholder="xray0" />
  1089. </a-form-item>
  1090. <a-form-item label="MTU">
  1091. <a-input-number v-model:value="inbound.settings.mtu" :min="0" />
  1092. </a-form-item>
  1093. <a-form-item label="Gateway">
  1094. <a-button size="small" @click="inbound.settings.gateway.push('')">
  1095. <template #icon>
  1096. <PlusOutlined />
  1097. </template>
  1098. </a-button>
  1099. <a-input v-for="(_ip, j) in inbound.settings.gateway" :key="`tun-gw-${j}`"
  1100. v-model:value="inbound.settings.gateway[j]" class="mt-4"
  1101. :placeholder="j === 0 ? '10.0.0.1/16' : 'fc00::1/64'">
  1102. <template #addonAfter>
  1103. <a-button size="small" @click="inbound.settings.gateway.splice(j, 1)">
  1104. <template #icon>
  1105. <MinusOutlined />
  1106. </template>
  1107. </a-button>
  1108. </template>
  1109. </a-input>
  1110. </a-form-item>
  1111. <a-form-item label="DNS">
  1112. <a-button size="small" @click="inbound.settings.dns.push('')">
  1113. <template #icon>
  1114. <PlusOutlined />
  1115. </template>
  1116. </a-button>
  1117. <a-input v-for="(_ip, j) in inbound.settings.dns" :key="`tun-dns-${j}`"
  1118. v-model:value="inbound.settings.dns[j]" class="mt-4" :placeholder="j === 0 ? '1.1.1.1' : '8.8.8.8'">
  1119. <template #addonAfter>
  1120. <a-button size="small" @click="inbound.settings.dns.splice(j, 1)">
  1121. <template #icon>
  1122. <MinusOutlined />
  1123. </template>
  1124. </a-button>
  1125. </template>
  1126. </a-input>
  1127. </a-form-item>
  1128. <a-form-item label="User level">
  1129. <a-input-number v-model:value="inbound.settings.userLevel" :min="0" />
  1130. </a-form-item>
  1131. <a-form-item>
  1132. <template #label>
  1133. <a-tooltip
  1134. title="Windows-only. CIDRs added to the system routing table automatically so matching traffic goes through TUN.">
  1135. Auto system routes
  1136. </a-tooltip>
  1137. </template>
  1138. <a-button size="small" @click="inbound.settings.autoSystemRoutingTable.push('')">
  1139. <template #icon>
  1140. <PlusOutlined />
  1141. </template>
  1142. </a-button>
  1143. <a-input v-for="(_ip, j) in inbound.settings.autoSystemRoutingTable" :key="`tun-rt-${j}`"
  1144. v-model:value="inbound.settings.autoSystemRoutingTable[j]" class="mt-4"
  1145. :placeholder="j === 0 ? '0.0.0.0/0' : '::/0'">
  1146. <template #addonAfter>
  1147. <a-button size="small" @click="inbound.settings.autoSystemRoutingTable.splice(j, 1)">
  1148. <template #icon>
  1149. <MinusOutlined />
  1150. </template>
  1151. </a-button>
  1152. </template>
  1153. </a-input>
  1154. </a-form-item>
  1155. <a-form-item>
  1156. <template #label>
  1157. <a-tooltip
  1158. title="Physical interface for outbound traffic. Use 'auto' to detect; auto-enabled when Auto system routes is set.">
  1159. Auto outbounds interface
  1160. </a-tooltip>
  1161. </template>
  1162. <a-input v-model:value="inbound.settings.autoOutboundsInterface" placeholder="auto" />
  1163. </a-form-item>
  1164. </a-form>
  1165. <!-- WireGuard -->
  1166. <a-form v-if="protocol === Protocols.WIREGUARD" :colon="false" :label-col="{ sm: { span: 8 } }"
  1167. :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
  1168. <a-form-item>
  1169. <template #label>
  1170. Secret key
  1171. <SyncOutlined class="random-icon" @click="regenInboundWg" />
  1172. </template>
  1173. <a-input v-model:value="inbound.settings.secretKey" />
  1174. </a-form-item>
  1175. <a-form-item label="Public key">
  1176. <a-input v-model:value="inbound.settings.pubKey" disabled />
  1177. </a-form-item>
  1178. <a-form-item label="MTU">
  1179. <a-input-number v-model:value="inbound.settings.mtu" />
  1180. </a-form-item>
  1181. <a-form-item label="No-kernel TUN">
  1182. <a-switch v-model:checked="inbound.settings.noKernelTun" />
  1183. </a-form-item>
  1184. <a-form-item label="Peers">
  1185. <a-button size="small" @click="inbound.settings.addPeer()">
  1186. <template #icon>
  1187. <PlusOutlined />
  1188. </template>
  1189. Add peer
  1190. </a-button>
  1191. </a-form-item>
  1192. <div v-for="(peer, idx) in inbound.settings.peers" :key="idx" class="wg-peer">
  1193. <a-divider style="margin: 8px 0">
  1194. Peer {{ idx + 1 }}
  1195. <DeleteOutlined v-if="inbound.settings.peers.length > 1" class="danger-icon"
  1196. @click="inbound.settings.delPeer(idx)" />
  1197. </a-divider>
  1198. <a-form-item>
  1199. <template #label>
  1200. Secret key
  1201. <SyncOutlined class="random-icon" @click="regenWgKeypair(peer)" />
  1202. </template>
  1203. <a-input v-model:value="peer.privateKey" />
  1204. </a-form-item>
  1205. <a-form-item label="Public key">
  1206. <a-input v-model:value="peer.publicKey" />
  1207. </a-form-item>
  1208. <a-form-item label="PSK">
  1209. <a-input v-model:value="peer.psk" />
  1210. </a-form-item>
  1211. <a-form-item label="Allowed IPs">
  1212. <a-button size="small" @click="peer.allowedIPs.push('')">
  1213. <template #icon>
  1214. <PlusOutlined />
  1215. </template>
  1216. </a-button>
  1217. <a-input v-for="(_ip, j) in peer.allowedIPs" :key="j" v-model:value="peer.allowedIPs[j]" class="mt-4">
  1218. <template #addonAfter>
  1219. <a-button v-if="peer.allowedIPs.length > 1" size="small" @click="peer.allowedIPs.splice(j, 1)">
  1220. <template #icon>
  1221. <MinusOutlined />
  1222. </template>
  1223. </a-button>
  1224. </template>
  1225. </a-input>
  1226. </a-form-item>
  1227. <a-form-item label="Keep-alive">
  1228. <a-input-number v-model:value="peer.keepAlive" :min="0" />
  1229. </a-form-item>
  1230. </div>
  1231. </a-form>
  1232. <!-- ============== Fallbacks (VLESS/Trojan over TCP) ============== -->
  1233. <template v-if="showFallbacks">
  1234. <a-divider style="margin: 12px 0" />
  1235. <div class="fallbacks-header">
  1236. <a-tooltip
  1237. 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.">
  1238. <span class="fallbacks-title">
  1239. Fallbacks ({{ inbound.settings.fallbacks.length }})
  1240. </span>
  1241. </a-tooltip>
  1242. <a-button type="primary" size="small" @click="addFallback">
  1243. <template #icon>
  1244. <PlusOutlined />
  1245. </template>
  1246. Add
  1247. </a-button>
  1248. </div>
  1249. <a-form v-for="(fallback, idx) in inbound.settings.fallbacks" :key="idx" :colon="false"
  1250. :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
  1251. <a-divider style="margin: 0">
  1252. Fallback {{ idx + 1 }}
  1253. <DeleteOutlined class="danger-icon" @click="delFallback(idx)" />
  1254. </a-divider>
  1255. <a-form-item>
  1256. <template #label>
  1257. <a-tooltip title="Match TLS SNI (server name). Leave empty to match any SNI.">
  1258. SNI
  1259. </a-tooltip>
  1260. </template>
  1261. <a-input v-model:value.trim="fallback.name" placeholder="any (leave empty)" />
  1262. </a-form-item>
  1263. <a-form-item>
  1264. <template #label>
  1265. <a-tooltip
  1266. title="Match TLS ALPN. 'any' = no ALPN constraint. Use h2/http/1.1 split when the inbound advertises both.">
  1267. ALPN
  1268. </a-tooltip>
  1269. </template>
  1270. <a-select v-model:value="fallback.alpn">
  1271. <a-select-option value="">any</a-select-option>
  1272. <a-select-option value="h2">h2</a-select-option>
  1273. <a-select-option value="http/1.1">http/1.1</a-select-option>
  1274. </a-select>
  1275. </a-form-item>
  1276. <a-form-item :validate-status="fallback.path && !fallback.path.startsWith('/') ? 'error' : ''"
  1277. :help="fallback.path && !fallback.path.startsWith('/') ? 'Path must start with /' : ''">
  1278. <template #label>
  1279. <a-tooltip
  1280. title="Match the HTTP request path of the first packet. Must start with '/'. Leave empty to match any.">
  1281. Path
  1282. </a-tooltip>
  1283. </template>
  1284. <a-input v-model:value.trim="fallback.path" placeholder="any (leave empty) or /ws" />
  1285. </a-form-item>
  1286. <a-form-item :validate-status="!fallback.dest ? 'error' : ''"
  1287. :help="!fallback.dest ? 'Destination is required' : ''">
  1288. <template #label>
  1289. <a-tooltip
  1290. 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).">
  1291. Destination
  1292. </a-tooltip>
  1293. </template>
  1294. <a-input v-model:value.trim="fallback.dest" placeholder="80 | 127.0.0.1:8080 | /dev/shm/x.sock" />
  1295. </a-form-item>
  1296. <a-form-item>
  1297. <template #label>
  1298. <a-tooltip
  1299. title="PROXY protocol version sent to the destination. Off (0) for plain TCP; v1/v2 to preserve client IP if the backend supports it.">
  1300. PROXY
  1301. </a-tooltip>
  1302. </template>
  1303. <a-select v-model:value="fallback.xver">
  1304. <a-select-option :value="0">Off</a-select-option>
  1305. <a-select-option :value="1">v1</a-select-option>
  1306. <a-select-option :value="2">v2</a-select-option>
  1307. </a-select>
  1308. </a-form-item>
  1309. </a-form>
  1310. </template>
  1311. </a-tab-pane>
  1312. <!-- ============================== STREAM ============================== -->
  1313. <a-tab-pane v-if="canEnableStream" key="stream"
  1314. tab="Stream"><!-- "Stream" stays literal — it's a wire-format identifier -->
  1315. <a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
  1316. <a-form-item v-if="protocol !== Protocols.HYSTERIA" label="Transmission">
  1317. <a-select v-model:value="network" :style="{ width: '75%' }">
  1318. <a-select-option value="tcp">TCP (RAW)</a-select-option>
  1319. <a-select-option value="kcp">mKCP</a-select-option>
  1320. <a-select-option value="ws">WebSocket</a-select-option>
  1321. <a-select-option value="grpc">gRPC</a-select-option>
  1322. <a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
  1323. <a-select-option value="xhttp">XHTTP</a-select-option>
  1324. </a-select>
  1325. </a-form-item>
  1326. <!-- TCP (RAW) — proxy-protocol + optional HTTP camouflage with full request/response editor -->
  1327. <template v-if="network === 'tcp'">
  1328. <a-form-item v-if="canEnableTls" label="Proxy Protocol">
  1329. <a-switch v-model:checked="inbound.stream.tcp.acceptProxyProtocol" />
  1330. </a-form-item>
  1331. <a-form-item :label="`HTTP ${t('camouflage')}`">
  1332. <a-switch :checked="inbound.stream.tcp.type === 'http'"
  1333. @change="(v) => (inbound.stream.tcp.type = v ? 'http' : 'none')" />
  1334. </a-form-item>
  1335. <template v-if="inbound.stream.tcp.type === 'http'">
  1336. <!-- Request -->
  1337. <a-divider :style="{ margin: '0' }">{{ t('pages.inbounds.stream.general.request') }}</a-divider>
  1338. <a-form-item :label="t('pages.inbounds.stream.tcp.version')">
  1339. <a-input v-model:value="inbound.stream.tcp.request.version" />
  1340. </a-form-item>
  1341. <a-form-item :label="t('pages.inbounds.stream.tcp.method')">
  1342. <a-input v-model:value="inbound.stream.tcp.request.method" />
  1343. </a-form-item>
  1344. <a-form-item>
  1345. <template #label>
  1346. {{ t('pages.inbounds.stream.tcp.path') }}
  1347. <a-button size="small" :style="{ marginLeft: '6px' }"
  1348. @click="inbound.stream.tcp.request.addPath('/')">
  1349. <template #icon>
  1350. <PlusOutlined />
  1351. </template>
  1352. </a-button>
  1353. </template>
  1354. <template v-for="(_p, idx) in inbound.stream.tcp.request.path" :key="`tcp-path-${idx}`">
  1355. <a-input v-model:value="inbound.stream.tcp.request.path[idx]" class="mb-4">
  1356. <template #addonAfter>
  1357. <a-button v-if="inbound.stream.tcp.request.path.length > 1" size="small"
  1358. @click="inbound.stream.tcp.request.removePath(idx)">
  1359. <template #icon>
  1360. <MinusOutlined />
  1361. </template>
  1362. </a-button>
  1363. </template>
  1364. </a-input>
  1365. </template>
  1366. </a-form-item>
  1367. <a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
  1368. <a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')">
  1369. <template #icon>
  1370. <PlusOutlined />
  1371. </template>
  1372. </a-button>
  1373. </a-form-item>
  1374. <a-form-item v-if="inbound.stream.tcp.request.headers.length > 0" :wrapper-col="{ span: 24 }">
  1375. <a-input-group v-for="(h, idx) in inbound.stream.tcp.request.headers" :key="`tcp-rh-${idx}`" compact
  1376. class="mb-8">
  1377. <a-input :style="{ width: '45%' }" v-model:value="h.name"
  1378. :placeholder="t('pages.inbounds.stream.general.name')">
  1379. <template #addonBefore>{{ idx + 1 }}</template>
  1380. </a-input>
  1381. <a-input :style="{ width: '45%' }" v-model:value="h.value"
  1382. :placeholder="t('pages.inbounds.stream.general.value')" />
  1383. <a-button @click="inbound.stream.tcp.request.removeHeader(idx)">
  1384. <template #icon>
  1385. <MinusOutlined />
  1386. </template>
  1387. </a-button>
  1388. </a-input-group>
  1389. </a-form-item>
  1390. <!-- Response -->
  1391. <a-divider :style="{ margin: '0' }">{{ t('pages.inbounds.stream.general.response') }}</a-divider>
  1392. <a-form-item :label="t('pages.inbounds.stream.tcp.version')">
  1393. <a-input v-model:value="inbound.stream.tcp.response.version" />
  1394. </a-form-item>
  1395. <a-form-item :label="t('pages.inbounds.stream.tcp.status')">
  1396. <a-input v-model:value="inbound.stream.tcp.response.status" />
  1397. </a-form-item>
  1398. <a-form-item :label="t('pages.inbounds.stream.tcp.statusDescription')">
  1399. <a-input v-model:value="inbound.stream.tcp.response.reason" />
  1400. </a-form-item>
  1401. <a-form-item :label="t('pages.inbounds.stream.tcp.responseHeader')">
  1402. <a-button size="small"
  1403. @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
  1404. <template #icon>
  1405. <PlusOutlined />
  1406. </template>
  1407. </a-button>
  1408. </a-form-item>
  1409. <a-form-item v-if="inbound.stream.tcp.response.headers.length > 0" :wrapper-col="{ span: 24 }">
  1410. <a-input-group v-for="(h, idx) in inbound.stream.tcp.response.headers" :key="`tcp-rsh-${idx}`" compact
  1411. class="mb-8">
  1412. <a-input :style="{ width: '45%' }" v-model:value="h.name"
  1413. :placeholder="t('pages.inbounds.stream.general.name')">
  1414. <template #addonBefore>{{ idx + 1 }}</template>
  1415. </a-input>
  1416. <a-input :style="{ width: '45%' }" v-model:value="h.value"
  1417. :placeholder="t('pages.inbounds.stream.general.value')" />
  1418. <a-button @click="inbound.stream.tcp.response.removeHeader(idx)">
  1419. <template #icon>
  1420. <MinusOutlined />
  1421. </template>
  1422. </a-button>
  1423. </a-input-group>
  1424. </a-form-item>
  1425. </template>
  1426. </template>
  1427. <!-- mKCP -->
  1428. <template v-if="network === 'kcp'">
  1429. <a-form-item label="MTU">
  1430. <a-input-number v-model:value="inbound.stream.kcp.mtu" :min="576" :max="1460" />
  1431. </a-form-item>
  1432. <a-form-item label="TTI (ms)">
  1433. <a-input-number v-model:value="inbound.stream.kcp.tti" :min="10" :max="100" />
  1434. </a-form-item>
  1435. <a-form-item label="Uplink (MB/s)">
  1436. <a-input-number v-model:value="inbound.stream.kcp.upCap" :min="0" />
  1437. </a-form-item>
  1438. <a-form-item label="Downlink (MB/s)">
  1439. <a-input-number v-model:value="inbound.stream.kcp.downCap" :min="0" />
  1440. </a-form-item>
  1441. <a-form-item label="CWND Multiplier">
  1442. <a-input-number v-model:value="inbound.stream.kcp.cwndMultiplier" :min="1" />
  1443. </a-form-item>
  1444. <a-form-item label="Max Sending Window">
  1445. <a-input-number v-model:value="inbound.stream.kcp.maxSendingWindow" :min="0" />
  1446. </a-form-item>
  1447. </template>
  1448. <!-- WebSocket -->
  1449. <template v-if="network === 'ws'">
  1450. <a-form-item label="Proxy Protocol">
  1451. <a-switch v-model:checked="inbound.stream.ws.acceptProxyProtocol" />
  1452. </a-form-item>
  1453. <a-form-item :label="t('host')">
  1454. <a-input v-model:value="inbound.stream.ws.host" />
  1455. </a-form-item>
  1456. <a-form-item :label="t('path')">
  1457. <a-input v-model:value="inbound.stream.ws.path" />
  1458. </a-form-item>
  1459. <a-form-item label="Heartbeat Period">
  1460. <a-input-number v-model:value="inbound.stream.ws.heartbeatPeriod" :min="0" />
  1461. </a-form-item>
  1462. <a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
  1463. <a-button size="small" @click="inbound.stream.ws.addHeader('', '')">
  1464. <template #icon>
  1465. <PlusOutlined />
  1466. </template>
  1467. </a-button>
  1468. </a-form-item>
  1469. <a-form-item v-if="inbound.stream.ws.headers.length > 0" :wrapper-col="{ span: 24 }">
  1470. <a-input-group v-for="(h, idx) in inbound.stream.ws.headers" :key="`ws-h-${idx}`" compact class="mb-8">
  1471. <a-input :style="{ width: '45%' }" v-model:value="h.name"
  1472. :placeholder="t('pages.inbounds.stream.general.name')">
  1473. <template #addonBefore>{{ idx + 1 }}</template>
  1474. </a-input>
  1475. <a-input :style="{ width: '45%' }" v-model:value="h.value"
  1476. :placeholder="t('pages.inbounds.stream.general.value')" />
  1477. <a-button @click="inbound.stream.ws.removeHeader(idx)">
  1478. <template #icon>
  1479. <MinusOutlined />
  1480. </template>
  1481. </a-button>
  1482. </a-input-group>
  1483. </a-form-item>
  1484. </template>
  1485. <!-- gRPC -->
  1486. <template v-if="network === 'grpc'">
  1487. <a-form-item label="Service Name">
  1488. <a-input v-model:value="inbound.stream.grpc.serviceName" />
  1489. </a-form-item>
  1490. <a-form-item label="Authority">
  1491. <a-input v-model:value="inbound.stream.grpc.authority" />
  1492. </a-form-item>
  1493. <a-form-item label="Multi Mode">
  1494. <a-switch v-model:checked="inbound.stream.grpc.multiMode" />
  1495. </a-form-item>
  1496. </template>
  1497. <!-- HTTPUpgrade -->
  1498. <template v-if="network === 'httpupgrade'">
  1499. <a-form-item label="Proxy Protocol">
  1500. <a-switch v-model:checked="inbound.stream.httpupgrade.acceptProxyProtocol" />
  1501. </a-form-item>
  1502. <a-form-item :label="t('host')">
  1503. <a-input v-model:value="inbound.stream.httpupgrade.host" />
  1504. </a-form-item>
  1505. <a-form-item :label="t('path')">
  1506. <a-input v-model:value="inbound.stream.httpupgrade.path" />
  1507. </a-form-item>
  1508. <a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
  1509. <a-button size="small" @click="inbound.stream.httpupgrade.addHeader('', '')">
  1510. <template #icon>
  1511. <PlusOutlined />
  1512. </template>
  1513. </a-button>
  1514. </a-form-item>
  1515. <a-form-item v-if="inbound.stream.httpupgrade.headers.length > 0" :wrapper-col="{ span: 24 }">
  1516. <a-input-group v-for="(h, idx) in inbound.stream.httpupgrade.headers" :key="`hu-h-${idx}`" compact
  1517. class="mb-8">
  1518. <a-input :style="{ width: '45%' }" v-model:value="h.name"
  1519. :placeholder="t('pages.inbounds.stream.general.name')">
  1520. <template #addonBefore>{{ idx + 1 }}</template>
  1521. </a-input>
  1522. <a-input :style="{ width: '45%' }" v-model:value="h.value"
  1523. :placeholder="t('pages.inbounds.stream.general.value')" />
  1524. <a-button @click="inbound.stream.httpupgrade.removeHeader(idx)">
  1525. <template #icon>
  1526. <MinusOutlined />
  1527. </template>
  1528. </a-button>
  1529. </a-input-group>
  1530. </a-form-item>
  1531. </template>
  1532. <!-- XHTTP -->
  1533. <template v-if="network === 'xhttp'">
  1534. <a-form-item :label="t('host')">
  1535. <a-input v-model:value="inbound.stream.xhttp.host" />
  1536. </a-form-item>
  1537. <a-form-item :label="t('path')">
  1538. <a-input v-model:value="inbound.stream.xhttp.path" />
  1539. </a-form-item>
  1540. <a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
  1541. <a-button size="small" @click="inbound.stream.xhttp.addHeader('', '')">
  1542. <template #icon>
  1543. <PlusOutlined />
  1544. </template>
  1545. </a-button>
  1546. </a-form-item>
  1547. <a-form-item v-if="inbound.stream.xhttp.headers.length > 0" :wrapper-col="{ span: 24 }">
  1548. <a-input-group v-for="(h, idx) in inbound.stream.xhttp.headers" :key="`xh-h-${idx}`" compact class="mb-8">
  1549. <a-input :style="{ width: '45%' }" v-model:value="h.name"
  1550. :placeholder="t('pages.inbounds.stream.general.name')">
  1551. <template #addonBefore>{{ idx + 1 }}</template>
  1552. </a-input>
  1553. <a-input :style="{ width: '45%' }" v-model:value="h.value"
  1554. :placeholder="t('pages.inbounds.stream.general.value')" />
  1555. <a-button @click="inbound.stream.xhttp.removeHeader(idx)">
  1556. <template #icon>
  1557. <MinusOutlined />
  1558. </template>
  1559. </a-button>
  1560. </a-input-group>
  1561. </a-form-item>
  1562. <a-form-item label="Mode">
  1563. <a-select v-model:value="inbound.stream.xhttp.mode" :style="{ width: '50%' }">
  1564. <a-select-option v-for="m in MODE_OPTIONS" :key="m" :value="m">{{ m }}</a-select-option>
  1565. </a-select>
  1566. </a-form-item>
  1567. <a-form-item v-if="inbound.stream.xhttp.mode === 'packet-up'" label="Max Buffered Upload">
  1568. <a-input-number v-model:value="inbound.stream.xhttp.scMaxBufferedPosts" />
  1569. </a-form-item>
  1570. <a-form-item v-if="inbound.stream.xhttp.mode === 'packet-up'" label="Max Upload Size (Byte)">
  1571. <a-input v-model:value="inbound.stream.xhttp.scMaxEachPostBytes" />
  1572. </a-form-item>
  1573. <a-form-item v-if="inbound.stream.xhttp.mode === 'stream-up'" label="Stream-Up Server">
  1574. <a-input v-model:value="inbound.stream.xhttp.scStreamUpServerSecs" />
  1575. </a-form-item>
  1576. <a-form-item label="Server Max Header Bytes">
  1577. <a-input-number v-model:value="inbound.stream.xhttp.serverMaxHeaderBytes" :min="0"
  1578. placeholder="0 (default)" />
  1579. </a-form-item>
  1580. <a-form-item label="Padding Bytes">
  1581. <a-input v-model:value="inbound.stream.xhttp.xPaddingBytes" />
  1582. </a-form-item>
  1583. <a-form-item label="Padding Obfs Mode">
  1584. <a-switch v-model:checked="inbound.stream.xhttp.xPaddingObfsMode" />
  1585. </a-form-item>
  1586. <template v-if="inbound.stream.xhttp.xPaddingObfsMode">
  1587. <a-form-item label="Padding Key">
  1588. <a-input v-model:value="inbound.stream.xhttp.xPaddingKey" placeholder="x_padding" />
  1589. </a-form-item>
  1590. <a-form-item label="Padding Header">
  1591. <a-input v-model:value="inbound.stream.xhttp.xPaddingHeader" placeholder="X-Padding" />
  1592. </a-form-item>
  1593. <a-form-item label="Padding Placement">
  1594. <a-select v-model:value="inbound.stream.xhttp.xPaddingPlacement">
  1595. <a-select-option value="">Default (queryInHeader)</a-select-option>
  1596. <a-select-option value="queryInHeader">queryInHeader</a-select-option>
  1597. <a-select-option value="header">header</a-select-option>
  1598. <a-select-option value="cookie">cookie</a-select-option>
  1599. <a-select-option value="query">query</a-select-option>
  1600. </a-select>
  1601. </a-form-item>
  1602. <a-form-item label="Padding Method">
  1603. <a-select v-model:value="inbound.stream.xhttp.xPaddingMethod">
  1604. <a-select-option value="">Default (repeat-x)</a-select-option>
  1605. <a-select-option value="repeat-x">repeat-x</a-select-option>
  1606. <a-select-option value="tokenish">tokenish</a-select-option>
  1607. </a-select>
  1608. </a-form-item>
  1609. </template>
  1610. <a-form-item label="Session Placement">
  1611. <a-select v-model:value="inbound.stream.xhttp.sessionPlacement">
  1612. <a-select-option value="">Default (path)</a-select-option>
  1613. <a-select-option value="path">path</a-select-option>
  1614. <a-select-option value="header">header</a-select-option>
  1615. <a-select-option value="cookie">cookie</a-select-option>
  1616. <a-select-option value="query">query</a-select-option>
  1617. </a-select>
  1618. </a-form-item>
  1619. <a-form-item
  1620. v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'"
  1621. label="Session Key">
  1622. <a-input v-model:value="inbound.stream.xhttp.sessionKey" placeholder="x_session" />
  1623. </a-form-item>
  1624. <a-form-item label="Sequence Placement">
  1625. <a-select v-model:value="inbound.stream.xhttp.seqPlacement">
  1626. <a-select-option value="">Default (path)</a-select-option>
  1627. <a-select-option value="path">path</a-select-option>
  1628. <a-select-option value="header">header</a-select-option>
  1629. <a-select-option value="cookie">cookie</a-select-option>
  1630. <a-select-option value="query">query</a-select-option>
  1631. </a-select>
  1632. </a-form-item>
  1633. <a-form-item v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'"
  1634. label="Sequence Key">
  1635. <a-input v-model:value="inbound.stream.xhttp.seqKey" placeholder="x_seq" />
  1636. </a-form-item>
  1637. <a-form-item v-if="inbound.stream.xhttp.mode === 'packet-up'" label="Uplink Data Placement">
  1638. <a-select v-model:value="inbound.stream.xhttp.uplinkDataPlacement">
  1639. <a-select-option value="">Default (body)</a-select-option>
  1640. <a-select-option value="body">body</a-select-option>
  1641. <a-select-option value="header">header</a-select-option>
  1642. <a-select-option value="cookie">cookie</a-select-option>
  1643. <a-select-option value="query">query</a-select-option>
  1644. </a-select>
  1645. </a-form-item>
  1646. <a-form-item
  1647. v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'"
  1648. label="Uplink Data Key">
  1649. <a-input v-model:value="inbound.stream.xhttp.uplinkDataKey" placeholder="x_data" />
  1650. </a-form-item>
  1651. <a-form-item label="No SSE Header">
  1652. <a-switch v-model:checked="inbound.stream.xhttp.noSSEHeader" />
  1653. </a-form-item>
  1654. </template>
  1655. <!-- ====== Security section ====== -->
  1656. <a-form-item label="Security">
  1657. <a-select v-model:value="security" :style="{ width: '160px' }" :disabled="!canEnableTls">
  1658. <a-select-option value="none">none</a-select-option>
  1659. <a-select-option value="tls">tls</a-select-option>
  1660. <a-select-option v-if="canEnableReality" value="reality">reality</a-select-option>
  1661. </a-select>
  1662. </a-form-item>
  1663. <template v-if="security === 'tls' && inbound.stream.tls">
  1664. <a-form-item label="SNI">
  1665. <a-input v-model:value="inbound.stream.tls.sni" placeholder="Server Name Indication" />
  1666. </a-form-item>
  1667. <a-form-item label="Cipher Suites">
  1668. <a-select v-model:value="inbound.stream.tls.cipherSuites">
  1669. <a-select-option value="">Auto</a-select-option>
  1670. <a-select-option v-for="[label, val] in CIPHER_SUITES" :key="val" :value="val">{{ label
  1671. }}</a-select-option>
  1672. </a-select>
  1673. </a-form-item>
  1674. <a-form-item label="Min/Max Version">
  1675. <a-input-group compact>
  1676. <a-select v-model:value="inbound.stream.tls.minVersion" :style="{ width: '50%' }">
  1677. <a-select-option v-for="v in TLS_VERSIONS" :key="v" :value="v">{{ v }}</a-select-option>
  1678. </a-select>
  1679. <a-select v-model:value="inbound.stream.tls.maxVersion" :style="{ width: '50%' }">
  1680. <a-select-option v-for="v in TLS_VERSIONS" :key="v" :value="v">{{ v }}</a-select-option>
  1681. </a-select>
  1682. </a-input-group>
  1683. </a-form-item>
  1684. <a-form-item label="uTLS">
  1685. <a-select v-model:value="inbound.stream.tls.settings.fingerprint" :style="{ width: '100%' }">
  1686. <a-select-option value="">None</a-select-option>
  1687. <a-select-option v-for="fp in FINGERPRINTS" :key="fp" :value="fp">{{ fp }}</a-select-option>
  1688. </a-select>
  1689. </a-form-item>
  1690. <a-form-item label="ALPN">
  1691. <a-select v-model:value="inbound.stream.tls.alpn" mode="multiple" :style="{ width: '100%' }"
  1692. :token-separators="[',']">
  1693. <a-select-option v-for="a in ALPNS" :key="a" :value="a">{{ a }}</a-select-option>
  1694. </a-select>
  1695. </a-form-item>
  1696. <a-form-item label="Reject Unknown SNI">
  1697. <a-switch v-model:checked="inbound.stream.tls.rejectUnknownSni" />
  1698. </a-form-item>
  1699. <a-form-item label="Disable System Root">
  1700. <a-switch v-model:checked="inbound.stream.tls.disableSystemRoot" />
  1701. </a-form-item>
  1702. <a-form-item label="Session Resumption">
  1703. <a-switch v-model:checked="inbound.stream.tls.enableSessionResumption" />
  1704. </a-form-item>
  1705. <!-- Cert array — file path or inline content per row -->
  1706. <template v-for="(cert, idx) in inbound.stream.tls.certs" :key="`cert-${idx}`">
  1707. <a-form-item :label="t('certificate')">
  1708. <a-radio-group v-model:value="cert.useFile" button-style="solid">
  1709. <a-radio-button :value="true">{{ t('pages.inbounds.certificatePath') }}</a-radio-button>
  1710. <a-radio-button :value="false">{{ t('pages.inbounds.certificateContent') }}</a-radio-button>
  1711. </a-radio-group>
  1712. </a-form-item>
  1713. <a-form-item label=" ">
  1714. <a-space>
  1715. <a-button v-if="idx === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()">
  1716. <template #icon>
  1717. <PlusOutlined />
  1718. </template>
  1719. </a-button>
  1720. <a-button v-if="inbound.stream.tls.certs.length > 1" type="primary" size="small"
  1721. @click="inbound.stream.tls.removeCert(idx)">
  1722. <template #icon>
  1723. <MinusOutlined />
  1724. </template>
  1725. </a-button>
  1726. </a-space>
  1727. </a-form-item>
  1728. <template v-if="cert.useFile">
  1729. <a-form-item :label="t('pages.inbounds.publicKey')">
  1730. <a-input v-model:value="cert.certFile" />
  1731. </a-form-item>
  1732. <a-form-item :label="t('pages.inbounds.privatekey')">
  1733. <a-input v-model:value="cert.keyFile" />
  1734. </a-form-item>
  1735. <a-form-item label=" ">
  1736. <a-button type="primary" :disabled="!defaultCert && !defaultKey" @click="setDefaultCertData(idx)">
  1737. {{ t('pages.inbounds.setDefaultCert') }}
  1738. </a-button>
  1739. </a-form-item>
  1740. </template>
  1741. <template v-else>
  1742. <a-form-item :label="t('pages.inbounds.publicKey')">
  1743. <a-textarea v-model:value="cert.cert" :auto-size="{ minRows: 3, maxRows: 8 }" />
  1744. </a-form-item>
  1745. <a-form-item :label="t('pages.inbounds.privatekey')">
  1746. <a-textarea v-model:value="cert.key" :auto-size="{ minRows: 3, maxRows: 8 }" />
  1747. </a-form-item>
  1748. </template>
  1749. <a-form-item label="One Time Loading">
  1750. <a-switch v-model:checked="cert.oneTimeLoading" />
  1751. </a-form-item>
  1752. <a-form-item label="Usage Option">
  1753. <a-select v-model:value="cert.usage" :style="{ width: '50%' }">
  1754. <a-select-option v-for="u in USAGES" :key="u" :value="u">{{ u }}</a-select-option>
  1755. </a-select>
  1756. </a-form-item>
  1757. <a-form-item v-if="cert.usage === 'issue'" label="Build Chain">
  1758. <a-switch v-model:checked="cert.buildChain" />
  1759. </a-form-item>
  1760. </template>
  1761. <!-- ECH (Encrypted Client Hello) -->
  1762. <a-form-item label="ECH key">
  1763. <a-input v-model:value="inbound.stream.tls.echServerKeys" />
  1764. </a-form-item>
  1765. <a-form-item label="ECH config">
  1766. <a-input v-model:value="inbound.stream.tls.settings.echConfigList" />
  1767. </a-form-item>
  1768. <a-form-item label=" ">
  1769. <a-space>
  1770. <a-button type="primary" :loading="saving" @click="getNewEchCert">Get New ECH Cert</a-button>
  1771. <a-button danger @click="clearEchCert">Clear</a-button>
  1772. </a-space>
  1773. </a-form-item>
  1774. </template>
  1775. <template v-if="security === 'reality' && inbound.stream.reality">
  1776. <a-form-item label="Show">
  1777. <a-switch v-model:checked="inbound.stream.reality.show" />
  1778. </a-form-item>
  1779. <a-form-item label="Xver">
  1780. <a-input-number v-model:value="inbound.stream.reality.xver" :min="0" />
  1781. </a-form-item>
  1782. <a-form-item label="uTLS">
  1783. <a-select v-model:value="inbound.stream.reality.settings.fingerprint" :style="{ width: '100%' }">
  1784. <a-select-option v-for="fp in FINGERPRINTS" :key="fp" :value="fp">{{ fp }}</a-select-option>
  1785. </a-select>
  1786. </a-form-item>
  1787. <a-form-item>
  1788. <template #label>
  1789. Target
  1790. <SyncOutlined class="random-icon" @click="randomizeRealityTarget" />
  1791. </template>
  1792. <a-input v-model:value="inbound.stream.reality.target" />
  1793. </a-form-item>
  1794. <a-form-item>
  1795. <template #label>
  1796. SNI
  1797. <SyncOutlined class="random-icon" @click="randomizeRealityTarget" />
  1798. </template>
  1799. <a-input v-model:value="inbound.stream.reality.serverNames" />
  1800. </a-form-item>
  1801. <a-form-item label="Max Time Diff (ms)">
  1802. <a-input-number v-model:value="inbound.stream.reality.maxTimediff" :min="0" />
  1803. </a-form-item>
  1804. <a-form-item label="Min Client Ver">
  1805. <a-input v-model:value="inbound.stream.reality.minClientVer" placeholder="25.9.11" />
  1806. </a-form-item>
  1807. <a-form-item label="Max Client Ver">
  1808. <a-input v-model:value="inbound.stream.reality.maxClientVer" placeholder="25.9.11" />
  1809. </a-form-item>
  1810. <a-form-item>
  1811. <template #label>
  1812. Short IDs
  1813. <SyncOutlined class="random-icon" @click="randomizeShortIds" />
  1814. </template>
  1815. <a-textarea v-model:value="inbound.stream.reality.shortIds" :auto-size="{ minRows: 1, maxRows: 4 }" />
  1816. </a-form-item>
  1817. <a-form-item label="SpiderX">
  1818. <a-input v-model:value="inbound.stream.reality.settings.spiderX" />
  1819. </a-form-item>
  1820. <a-form-item :label="t('pages.inbounds.publicKey')">
  1821. <a-textarea v-model:value="inbound.stream.reality.settings.publicKey"
  1822. :auto-size="{ minRows: 1, maxRows: 4 }" />
  1823. </a-form-item>
  1824. <a-form-item :label="t('pages.inbounds.privatekey')">
  1825. <a-textarea v-model:value="inbound.stream.reality.privateKey" :auto-size="{ minRows: 1, maxRows: 4 }" />
  1826. </a-form-item>
  1827. <a-form-item label=" ">
  1828. <a-space>
  1829. <a-button type="primary" :loading="saving" @click="genRealityKeypair">Get New Cert</a-button>
  1830. <a-button danger @click="clearRealityKeypair">Clear</a-button>
  1831. </a-space>
  1832. </a-form-item>
  1833. <a-form-item label="mldsa65 Seed">
  1834. <a-textarea v-model:value="inbound.stream.reality.mldsa65Seed" :auto-size="{ minRows: 2, maxRows: 6 }" />
  1835. </a-form-item>
  1836. <a-form-item label="mldsa65 Verify">
  1837. <a-textarea v-model:value="inbound.stream.reality.settings.mldsa65Verify"
  1838. :auto-size="{ minRows: 2, maxRows: 6 }" />
  1839. </a-form-item>
  1840. <a-form-item label=" ">
  1841. <a-space>
  1842. <a-button type="primary" :loading="saving" @click="genMldsa65">Get New Seed</a-button>
  1843. <a-button danger @click="clearMldsa65">Clear</a-button>
  1844. </a-space>
  1845. </a-form-item>
  1846. </template>
  1847. <!-- ====== External Proxy ====== -->
  1848. <a-form-item label="External Proxy">
  1849. <a-switch v-model:checked="externalProxy" />
  1850. <a-button v-if="externalProxy" size="small" type="primary" :style="{ marginLeft: '10px' }"
  1851. @click="inbound.stream.externalProxy.push({ forceTls: 'same', dest: '', port: 443, remark: '' })">
  1852. <template #icon>
  1853. <PlusOutlined />
  1854. </template>
  1855. </a-button>
  1856. </a-form-item>
  1857. <a-form-item v-if="externalProxy" :wrapper-col="{ span: 24 }">
  1858. <a-input-group v-for="(row, idx) in inbound.stream.externalProxy" :key="`ep-${idx}`" compact
  1859. :style="{ margin: '8px 0' }">
  1860. <a-tooltip title="Force TLS">
  1861. <a-select v-model:value="row.forceTls" :style="{ width: '20%' }">
  1862. <a-select-option value="same">{{ t('pages.inbounds.same') }}</a-select-option>
  1863. <a-select-option value="none">{{ t('none') }}</a-select-option>
  1864. <a-select-option value="tls">TLS</a-select-option>
  1865. </a-select>
  1866. </a-tooltip>
  1867. <a-input v-model:value="row.dest" :style="{ width: '30%' }" :placeholder="t('host')" />
  1868. <a-tooltip :title="t('pages.inbounds.port')">
  1869. <a-input-number v-model:value="row.port" :style="{ width: '15%' }" :min="1" :max="65535" />
  1870. </a-tooltip>
  1871. <a-input v-model:value="row.remark" :style="{ width: '35%' }" :placeholder="t('pages.inbounds.remark')">
  1872. <template #addonAfter>
  1873. <MinusOutlined @click="inbound.stream.externalProxy.splice(idx, 1)" />
  1874. </template>
  1875. </a-input>
  1876. </a-input-group>
  1877. </a-form-item>
  1878. <!-- ====== Sockopt ====== -->
  1879. <a-form-item label="Sockopt">
  1880. <a-switch v-model:checked="inbound.stream.sockoptSwitch" />
  1881. </a-form-item>
  1882. <template v-if="inbound.stream.sockoptSwitch && inbound.stream.sockopt">
  1883. <a-form-item label="Route Mark">
  1884. <a-input-number v-model:value="inbound.stream.sockopt.mark" :min="0" />
  1885. </a-form-item>
  1886. <a-form-item label="TCP Keep Alive Interval">
  1887. <a-input-number v-model:value="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0" />
  1888. </a-form-item>
  1889. <a-form-item label="TCP Keep Alive Idle">
  1890. <a-input-number v-model:value="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0" />
  1891. </a-form-item>
  1892. <a-form-item label="TCP Max Seg">
  1893. <a-input-number v-model:value="inbound.stream.sockopt.tcpMaxSeg" :min="0" />
  1894. </a-form-item>
  1895. <a-form-item label="TCP User Timeout">
  1896. <a-input-number v-model:value="inbound.stream.sockopt.tcpUserTimeout" :min="0" />
  1897. </a-form-item>
  1898. <a-form-item label="TCP Window Clamp">
  1899. <a-input-number v-model:value="inbound.stream.sockopt.tcpWindowClamp" :min="0" />
  1900. </a-form-item>
  1901. <a-form-item label="Proxy Protocol">
  1902. <a-switch v-model:checked="inbound.stream.sockopt.acceptProxyProtocol" />
  1903. </a-form-item>
  1904. <a-form-item label="TCP Fast Open">
  1905. <a-switch v-model:checked="inbound.stream.sockopt.tcpFastOpen" />
  1906. </a-form-item>
  1907. <a-form-item label="Multipath TCP">
  1908. <a-switch v-model:checked="inbound.stream.sockopt.tcpMptcp" />
  1909. </a-form-item>
  1910. <a-form-item label="Penetrate">
  1911. <a-switch v-model:checked="inbound.stream.sockopt.penetrate" />
  1912. </a-form-item>
  1913. <a-form-item label="V6 Only">
  1914. <a-switch v-model:checked="inbound.stream.sockopt.V6Only" />
  1915. </a-form-item>
  1916. <a-form-item label="Domain Strategy">
  1917. <a-select v-model:value="inbound.stream.sockopt.domainStrategy" :style="{ width: '50%' }">
  1918. <a-select-option v-for="d in DOMAIN_STRATEGIES" :key="d" :value="d">{{ d }}</a-select-option>
  1919. </a-select>
  1920. </a-form-item>
  1921. <a-form-item label="TCP Congestion">
  1922. <a-select v-model:value="inbound.stream.sockopt.tcpcongestion" :style="{ width: '50%' }">
  1923. <a-select-option v-for="c in TCP_CONGESTIONS" :key="c" :value="c">{{ c }}</a-select-option>
  1924. </a-select>
  1925. </a-form-item>
  1926. <a-form-item label="TProxy">
  1927. <a-select v-model:value="inbound.stream.sockopt.tproxy" :style="{ width: '50%' }">
  1928. <a-select-option value="off">Off</a-select-option>
  1929. <a-select-option value="redirect">Redirect</a-select-option>
  1930. <a-select-option value="tproxy">TProxy</a-select-option>
  1931. </a-select>
  1932. </a-form-item>
  1933. <a-form-item label="Dialer Proxy">
  1934. <a-input v-model:value="inbound.stream.sockopt.dialerProxy" />
  1935. </a-form-item>
  1936. <a-form-item label="Interface Name">
  1937. <a-input v-model:value="inbound.stream.sockopt.interfaceName" />
  1938. </a-form-item>
  1939. <a-form-item label="Trusted X-Forwarded-For">
  1940. <a-select v-model:value="inbound.stream.sockopt.trustedXForwardedFor" mode="tags"
  1941. :style="{ width: '100%' }" :token-separators="[',']">
  1942. <a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
  1943. <a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
  1944. <a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
  1945. <a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
  1946. </a-select>
  1947. </a-form-item>
  1948. </template>
  1949. <!-- ====== Hysteria stream settings ====== -->
  1950. <!-- Per https://xtls.github.io/config/transports/hysteria.html -->
  1951. <template v-if="protocol === Protocols.HYSTERIA">
  1952. <a-form-item>
  1953. <template #label>
  1954. <a-tooltip title="Hysteria protocol version. Currently must be 2.">
  1955. Version
  1956. </a-tooltip>
  1957. </template>
  1958. <a-input-number v-model:value="inbound.stream.hysteria.version" :min="2" :max="2" />
  1959. </a-form-item>
  1960. <a-form-item>
  1961. <template #label>
  1962. <a-tooltip title="Idle timeout (seconds) for a single QUIC native UDP connection.">
  1963. UDP idle timeout
  1964. </a-tooltip>
  1965. </template>
  1966. <a-input-number v-model:value="inbound.stream.hysteria.udpIdleTimeout" :min="0" />
  1967. </a-form-item>
  1968. <a-form-item label="Masquerade">
  1969. <a-switch v-model:checked="inbound.stream.hysteria.masqueradeSwitch" />
  1970. </a-form-item>
  1971. <template v-if="inbound.stream.hysteria.masqueradeSwitch">
  1972. <a-form-item label="Type">
  1973. <a-select v-model:value="inbound.stream.hysteria.masquerade.type" :style="{ width: '50%' }">
  1974. <a-select-option value="proxy">Proxy</a-select-option>
  1975. <a-select-option value="file">File</a-select-option>
  1976. <a-select-option value="string">String</a-select-option>
  1977. </a-select>
  1978. </a-form-item>
  1979. <!-- Proxy type: url / rewriteHost / insecure -->
  1980. <template v-if="inbound.stream.hysteria.masquerade.type === 'proxy'">
  1981. <a-form-item label="URL">
  1982. <a-input v-model:value="inbound.stream.hysteria.masquerade.url" placeholder="https://example.com" />
  1983. </a-form-item>
  1984. <a-form-item label="Rewrite Host">
  1985. <a-switch v-model:checked="inbound.stream.hysteria.masquerade.rewriteHost" />
  1986. </a-form-item>
  1987. <a-form-item label="Insecure">
  1988. <a-switch v-model:checked="inbound.stream.hysteria.masquerade.insecure" />
  1989. </a-form-item>
  1990. </template>
  1991. <!-- File type: dir -->
  1992. <a-form-item v-if="inbound.stream.hysteria.masquerade.type === 'file'" label="Directory">
  1993. <a-input v-model:value="inbound.stream.hysteria.masquerade.dir" placeholder="/path/to/www" />
  1994. </a-form-item>
  1995. <!-- String type: content / statusCode / headers -->
  1996. <template v-if="inbound.stream.hysteria.masquerade.type === 'string'">
  1997. <a-form-item label="Content">
  1998. <a-textarea v-model:value="inbound.stream.hysteria.masquerade.content"
  1999. :auto-size="{ minRows: 2, maxRows: 6 }" />
  2000. </a-form-item>
  2001. <a-form-item label="Status Code">
  2002. <a-input-number v-model:value="inbound.stream.hysteria.masquerade.statusCode" :min="100" :max="599"
  2003. placeholder="200" />
  2004. </a-form-item>
  2005. <a-form-item label="Headers">
  2006. <a-button size="small" @click="inbound.stream.hysteria.masquerade.addHeader('', '')">
  2007. <template #icon>
  2008. <PlusOutlined />
  2009. </template>
  2010. </a-button>
  2011. </a-form-item>
  2012. <a-form-item v-if="inbound.stream.hysteria.masquerade.headers.length > 0" :wrapper-col="{ span: 24 }">
  2013. <a-input-group v-for="(h, idx) in inbound.stream.hysteria.masquerade.headers" :key="`mh-${idx}`"
  2014. compact class="mb-8">
  2015. <a-input :style="{ width: '45%' }" v-model:value="h.name" placeholder="Name">
  2016. <template #addonBefore>{{ idx + 1 }}</template>
  2017. </a-input>
  2018. <a-input :style="{ width: '45%' }" v-model:value="h.value" placeholder="Value" />
  2019. <a-button @click="inbound.stream.hysteria.masquerade.removeHeader(idx)">
  2020. <template #icon>
  2021. <MinusOutlined />
  2022. </template>
  2023. </a-button>
  2024. </a-input-group>
  2025. </a-form-item>
  2026. </template>
  2027. </template>
  2028. </template>
  2029. </a-form>
  2030. <!-- ====== FinalMask (TCP/UDP masks + QUIC params) ====== -->
  2031. <FinalMaskForm :stream="inbound.stream" :protocol="protocol" />
  2032. </a-tab-pane>
  2033. <!-- ============================== SNIFFING ============================== -->
  2034. <a-tab-pane key="sniffing" tab="Sniffing"><!-- "Sniffing" stays literal — xray config term -->
  2035. <a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
  2036. <a-form-item label="Enabled">
  2037. <a-switch v-model:checked="inbound.sniffing.enabled" />
  2038. </a-form-item>
  2039. <template v-if="inbound.sniffing.enabled">
  2040. <a-form-item :wrapper-col="{ span: 24 }">
  2041. <a-checkbox-group v-model:value="inbound.sniffing.destOverride">
  2042. <a-checkbox v-for="(value, key) in SNIFFING_OPTION" :key="key" :value="value">{{ key }}</a-checkbox>
  2043. </a-checkbox-group>
  2044. </a-form-item>
  2045. <a-form-item label="Metadata only">
  2046. <a-switch v-model:checked="inbound.sniffing.metadataOnly" />
  2047. </a-form-item>
  2048. <a-form-item label="Route only">
  2049. <a-switch v-model:checked="inbound.sniffing.routeOnly" />
  2050. </a-form-item>
  2051. <a-form-item label="IPs excluded">
  2052. <a-select v-model:value="inbound.sniffing.ipsExcluded" mode="tags" :token-separators="[',']"
  2053. placeholder="IP/CIDR/geoip:*/ext:*" :style="{ width: '100%' }" />
  2054. </a-form-item>
  2055. <a-form-item label="Domains excluded">
  2056. <a-select v-model:value="inbound.sniffing.domainsExcluded" mode="tags" :token-separators="[',']"
  2057. placeholder="domain:*/ext:*" :style="{ width: '100%' }" />
  2058. </a-form-item>
  2059. </template>
  2060. </a-form>
  2061. </a-tab-pane>
  2062. <!-- ============================== ADVANCED ============================== -->
  2063. <a-tab-pane key="advanced" :tab="t('pages.xray.advancedTemplate')">
  2064. <div class="advanced-shell">
  2065. <div class="advanced-panel">
  2066. <div class="advanced-panel__header">
  2067. <div>
  2068. <div class="advanced-panel__title">Inbound JSON sections</div>
  2069. <div class="advanced-panel__subtitle">
  2070. Full inbound JSON and focused editors for settings, sniffing, and streamSettings.
  2071. </div>
  2072. </div>
  2073. </div>
  2074. <a-tabs v-model:active-key="advancedSectionKey" class="advanced-inner-tabs">
  2075. <a-tab-pane key="all" tab="All">
  2076. <div class="advanced-editor-meta">
  2077. Full inbound object with all fields in one editor.
  2078. </div>
  2079. <JsonEditor v-model:value="advancedAllConfig" min-height="340px" max-height="560px" />
  2080. </a-tab-pane>
  2081. <a-tab-pane key="settings" tab="Settings">
  2082. <div class="advanced-editor-meta">
  2083. Xray settings block wrapper:
  2084. <code>{ settings: { ... } }</code>.
  2085. </div>
  2086. <JsonEditor v-model:value="advancedSettingsConfig" min-height="320px" max-height="540px" />
  2087. </a-tab-pane>
  2088. <a-tab-pane key="sniffingSection" tab="Sniffing">
  2089. <div class="advanced-editor-meta">
  2090. Xray sniffing block wrapper:
  2091. <code>{ sniffing: { ... } }</code>.
  2092. </div>
  2093. <JsonEditor v-model:value="advancedSniffingConfig" min-height="240px" max-height="420px" />
  2094. </a-tab-pane>
  2095. <a-tab-pane v-if="canEnableStream" key="streamSection" tab="Stream">
  2096. <div class="advanced-editor-meta">
  2097. Xray stream block wrapper:
  2098. <code>{ streamSettings: { ... } }</code>.
  2099. </div>
  2100. <JsonEditor v-model:value="advancedStreamConfig" min-height="320px" max-height="540px" />
  2101. </a-tab-pane>
  2102. </a-tabs>
  2103. </div>
  2104. </div>
  2105. </a-tab-pane>
  2106. </a-tabs>
  2107. </a-modal>
  2108. </template>
  2109. <style scoped>
  2110. .mt-4 {
  2111. margin-top: 4px;
  2112. }
  2113. .mt-8 {
  2114. margin-top: 8px;
  2115. }
  2116. .mt-12 {
  2117. margin-top: 12px;
  2118. }
  2119. .mb-4 {
  2120. margin-bottom: 4px;
  2121. }
  2122. .mb-8 {
  2123. margin-bottom: 8px;
  2124. }
  2125. .mb-12 {
  2126. margin-bottom: 12px;
  2127. }
  2128. .random-icon {
  2129. margin-left: 4px;
  2130. cursor: pointer;
  2131. color: var(--ant-primary-color, #1890ff);
  2132. }
  2133. .danger-icon {
  2134. margin-left: 6px;
  2135. cursor: pointer;
  2136. color: #ff4d4f;
  2137. }
  2138. .vless-auth-state {
  2139. display: block;
  2140. margin-top: 6px;
  2141. }
  2142. .client-summary {
  2143. width: 100%;
  2144. border-collapse: collapse;
  2145. }
  2146. .client-summary th,
  2147. .client-summary td {
  2148. padding: 4px 8px;
  2149. text-align: left;
  2150. border-bottom: 1px solid rgba(128, 128, 128, 0.15);
  2151. }
  2152. .fallbacks-header {
  2153. display: flex;
  2154. align-items: center;
  2155. gap: 8px;
  2156. margin: 8px 0;
  2157. }
  2158. .fallbacks-title {
  2159. font-weight: 500;
  2160. flex: 1;
  2161. }
  2162. .wg-peer {
  2163. margin-top: 4px;
  2164. }
  2165. .advanced-shell {
  2166. display: flex;
  2167. flex-direction: column;
  2168. gap: 12px;
  2169. }
  2170. .advanced-panel {
  2171. padding: 14px;
  2172. border: 1px solid rgba(128, 128, 128, 0.18);
  2173. border-radius: 12px;
  2174. background: rgba(128, 128, 128, 0.04);
  2175. }
  2176. .advanced-panel__header {
  2177. margin-bottom: 12px;
  2178. }
  2179. .advanced-panel__title {
  2180. font-size: 14px;
  2181. font-weight: 600;
  2182. line-height: 1.4;
  2183. }
  2184. .advanced-panel__subtitle {
  2185. margin-top: 4px;
  2186. color: rgba(0, 0, 0, 0.6);
  2187. line-height: 1.5;
  2188. }
  2189. .advanced-inner-tabs :deep(.ant-tabs-nav) {
  2190. margin-bottom: 12px;
  2191. }
  2192. .advanced-inner-tabs :deep(.ant-tabs-tab) {
  2193. padding-inline: 14px;
  2194. }
  2195. .advanced-editor-meta {
  2196. margin-bottom: 10px;
  2197. color: rgba(0, 0, 0, 0.65);
  2198. line-height: 1.5;
  2199. }
  2200. @media (max-width: 768px) {
  2201. .advanced-panel {
  2202. padding: 12px;
  2203. border-radius: 10px;
  2204. }
  2205. .advanced-inner-tabs :deep(.ant-tabs-tab) {
  2206. padding-inline: 10px;
  2207. }
  2208. }
  2209. :global(.dark) .advanced-panel__subtitle,
  2210. :global(.dark) .advanced-editor-meta,
  2211. :global(.ultra) .advanced-panel__subtitle,
  2212. :global(.ultra) .advanced-editor-meta {
  2213. color: rgba(255, 255, 255, 0.65);
  2214. }
  2215. :global(.dark) .advanced-panel,
  2216. :global(.ultra) .advanced-panel {
  2217. border-color: rgba(255, 255, 255, 0.12);
  2218. background: rgba(255, 255, 255, 0.03);
  2219. }
  2220. .section-heading {
  2221. font-weight: 500;
  2222. margin: 12px 0 6px;
  2223. opacity: 0.85;
  2224. }
  2225. </style>