InboundFormModal.vue 83 KB

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