xray.html 78 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275
  1. {{ template "page/head_start" .}}
  2. <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}">
  3. <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
  4. <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
  5. <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
  6. {{ template "page/head_end" .}}
  7. {{ template "page/body_start" .}}
  8. <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' xray-page'">
  9. <a-sidebar></a-sidebar>
  10. <a-layout id="content-layout">
  11. <a-layout-content>
  12. <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}' size="large">
  13. <transition name="list" appear>
  14. <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
  15. message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
  16. </a-alert>
  17. </transition>
  18. <transition name="list" appear>
  19. <a-row v-if="!loadingStates.fetched">
  20. <div :style="{ minHeight: 'calc(100vh - 120px)' }"></div>
  21. </a-row>
  22. <a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
  23. <a-col>
  24. <a-card hoverable>
  25. <a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
  26. <a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
  27. <a-space direction="horizontal">
  28. <a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">
  29. {{ i18n "pages.xray.save" }}
  30. </a-button>
  31. <a-button type="danger" :disabled="!saveBtnDisable" @click="restartXray">
  32. {{ i18n "pages.xray.restart" }}
  33. </a-button>
  34. <a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme">
  35. <span slot="title">{{ i18n
  36. "pages.index.xrayErrorPopoverTitle" }}</span>
  37. <template slot="content">
  38. <span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line
  39. ]]</span>
  40. </template>
  41. <a-icon type="question-circle"></a-icon>
  42. </a-popover>
  43. </a-space>
  44. </a-col>
  45. <a-col :xs="24" :sm="14">
  46. <template>
  47. <div>
  48. <a-back-top :target="() => document.getElementById('content-layout')"
  49. visibility-height="200"></a-back-top>
  50. <a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
  51. message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
  52. </a-alert>
  53. </div>
  54. </template>
  55. </a-col>
  56. </a-row>
  57. </a-card>
  58. </a-col>
  59. <a-col>
  60. <a-tabs default-active-key="tpl-basic" @change="(activeKey) => { this.changePage(activeKey); }"
  61. :class="themeSwitcher.currentTheme">
  62. <a-tab-pane key="tpl-basic" :style="{ paddingTop: '20px' }">
  63. <template #tab>
  64. <a-icon type="setting"></a-icon>
  65. <span>{{ i18n "pages.xray.basicTemplate"}}</span>
  66. </template>
  67. {{ template "settings/xray/basics" . }}
  68. </a-tab-pane>
  69. <a-tab-pane key="tpl-routing" :style="{ paddingTop: '20px' }">
  70. <template #tab>
  71. <a-icon type="swap"></a-icon>
  72. <span>{{ i18n "pages.xray.Routings"}}</span>
  73. </template>
  74. {{ template "settings/xray/routing" . }}
  75. </a-tab-pane>
  76. <a-tab-pane key="tpl-outbound" force-render="true">
  77. <template #tab>
  78. <a-icon type="upload"></a-icon>
  79. <span>{{ i18n "pages.xray.Outbounds"}}</span>
  80. </template>
  81. {{ template "settings/xray/outbounds" . }}
  82. </a-tab-pane>
  83. <a-tab-pane key="tpl-balancer" :style="{ paddingTop: '20px' }" force-render="true">
  84. <template #tab>
  85. <a-icon type="cluster"></a-icon>
  86. <span>{{ i18n "pages.xray.Balancers"}}</span>
  87. </template>
  88. {{ template "settings/xray/balancers" . }}
  89. </a-tab-pane>
  90. <a-tab-pane key="tpl-dns" :style="{ paddingTop: '20px' }" force-render="true">
  91. <template #tab>
  92. <a-icon type="database"></a-icon>
  93. <span>DNS</span>
  94. </template>
  95. {{ template "settings/xray/dns" . }}
  96. </a-tab-pane>
  97. <a-tab-pane key="tpl-advanced" force-render="true">
  98. <template #tab>
  99. <a-icon type="code"></a-icon>
  100. <span>{{ i18n "pages.xray.advancedTemplate"}}</span>
  101. </template>
  102. {{ template "settings/xray/advanced" . }}
  103. </a-tab-pane>
  104. </a-tabs>
  105. </a-col>
  106. </a-row>
  107. </transition>
  108. </a-spin>
  109. </a-layout-content>
  110. </a-layout>
  111. </a-layout>
  112. {{template "page/body_scripts" .}}
  113. <script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
  114. <script src="{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}"></script>
  115. <script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
  116. <script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
  117. <script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
  118. <script src="{{ .base_path }}assets/codemirror/lint/lint.js"></script>
  119. <script src="{{ .base_path }}assets/codemirror/lint/javascript-lint.js"></script>
  120. <script src="{{ .base_path }}assets/codemirror/hint/javascript-hint.js"></script>
  121. <script src="{{ .base_path }}assets/codemirror/fold/foldcode.js"></script>
  122. <script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script>
  123. <script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
  124. {{template "component/aSidebar" .}}
  125. {{template "component/aThemeSwitch" .}}
  126. {{template "component/aTableSortable" .}}
  127. {{template "component/aSettingListItem" .}}
  128. {{template "modals/ruleModal" .}}
  129. {{template "modals/outModal" .}}
  130. {{template "modals/balancerModal" .}}
  131. {{template "modals/dnsModal" .}}
  132. {{template "modals/dnsPresetsModal" .}}
  133. {{template "modals/fakednsModal" .}}
  134. {{template "modals/warpModal" .}}
  135. {{template "modals/nordModal" .}}
  136. <script>
  137. // Modernised rules layout — 6 cells (#, source, network, destination,
  138. // inbound, target). Each criterion renders as a single self-labelled
  139. // pill that shows the first value plus a "+N" remainder badge for the
  140. // rest; the full list is surfaced via tooltip on hover. The destination
  141. // column has no fixed width and absorbs leftover horizontal space so the
  142. // table fits typical viewports without a horizontal scrollbar.
  143. const rulesColumns = [
  144. { title: '#', align: 'center', width: 70, scopedSlots: { customRender: 'action' } },
  145. { title: '{{ i18n "pages.xray.rules.source"}}', align: 'left', width: 180, scopedSlots: { customRender: 'source' } },
  146. { title: '{{ i18n "pages.inbounds.network"}}', align: 'left', width: 180, scopedSlots: { customRender: 'network' } },
  147. { title: '{{ i18n "pages.xray.rules.dest"}}', align: 'left', scopedSlots: { customRender: 'destination' } },
  148. { title: '{{ i18n "pages.xray.rules.inbound"}}', align: 'left', width: 180, scopedSlots: { customRender: 'inbound' } },
  149. { title: '{{ i18n "pages.xray.rules.outbound"}}', align: 'left', width: 170, scopedSlots: { customRender: 'target' } },
  150. ];
  151. // Mobile: 3-column table — #, Inbound, Outbound. Source / Network /
  152. // Destination criteria are dropped to keep the table readable on
  153. // narrow viewports. Users see the rule's identity (Inbound) and
  154. // what it does (Outbound) at a glance; full criteria are accessible
  155. // by tapping Edit in the actions menu.
  156. // # column is wider than desktop (110 vs 70) to fit the touch-friendly
  157. // drag handle (padding: 6px → ~28px) alongside the index and dropdown.
  158. const rulesMobileColumns = [
  159. { title: '#', align: 'center', width: 110, scopedSlots: { customRender: 'action' } },
  160. { title: '{{ i18n "pages.xray.rules.inbound"}}', align: 'left', scopedSlots: { customRender: 'inbound' } },
  161. { title: '{{ i18n "pages.xray.rules.outbound"}}', align: 'left', width: 140, scopedSlots: { customRender: 'target' } },
  162. ];
  163. const outboundColumns = [
  164. { title: '#', align: 'center', width: 70, scopedSlots: { customRender: 'action' } },
  165. // Combined "Tag / Protocol" — saves a column. Tag stays on top, protocol +
  166. // network + security pills sit underneath it. Width chosen so the three
  167. // longest tonal pills (e.g. vless + httpupgrade + reality) fit on a
  168. // single line without wrapping.
  169. { title: '{{ i18n "pages.xray.outbound.tag"}}', align: 'left', width: 280, scopedSlots: { customRender: 'identity' } },
  170. { title: '{{ i18n "pages.xray.outbound.address"}}', align: 'left', scopedSlots: { customRender: 'address' } },
  171. { title: '{{ i18n "pages.inbounds.traffic" }}', align: 'left', width: 190, scopedSlots: { customRender: 'traffic' } },
  172. { title: '{{ i18n "pages.xray.outbound.testResult" }}', align: 'left', width: 130, scopedSlots: { customRender: 'testResult' } },
  173. { title: '{{ i18n "pages.xray.outbound.test" }}', align: 'center', width: 70, scopedSlots: { customRender: 'test' } },
  174. ];
  175. const balancerColumns = [{
  176. title: "#",
  177. align: 'center',
  178. width: 20,
  179. scopedSlots: {
  180. customRender: 'action'
  181. }
  182. },
  183. {
  184. title: '{{ i18n "pages.xray.balancer.tag"}}',
  185. dataIndex: 'tag',
  186. align: 'center',
  187. width: 50
  188. },
  189. {
  190. title: '{{ i18n "pages.xray.balancer.balancerStrategy"}}',
  191. align: 'center',
  192. width: 50,
  193. scopedSlots: {
  194. customRender: 'strategy'
  195. }
  196. },
  197. {
  198. title: '{{ i18n "pages.xray.balancer.balancerSelectors"}}',
  199. align: 'center',
  200. width: 100,
  201. scopedSlots: {
  202. customRender: 'selector'
  203. }
  204. },
  205. ];
  206. const dnsColumns = [{
  207. title: "#",
  208. align: 'center',
  209. width: 20,
  210. scopedSlots: {
  211. customRender: 'action'
  212. }
  213. },
  214. {
  215. title: '{{ i18n "pages.xray.outbound.address"}}',
  216. align: 'center',
  217. width: 50,
  218. scopedSlots: {
  219. customRender: 'address'
  220. }
  221. },
  222. {
  223. title: '{{ i18n "pages.xray.dns.domains"}}',
  224. align: 'center',
  225. width: 50,
  226. scopedSlots: {
  227. customRender: 'domain'
  228. }
  229. },
  230. {
  231. title: '{{ i18n "pages.xray.dns.expectIPs"}}',
  232. align: 'center',
  233. width: 50,
  234. scopedSlots: {
  235. customRender: 'expectIPs'
  236. }
  237. },
  238. ];
  239. const fakednsColumns = [{
  240. title: "#",
  241. align: 'center',
  242. width: 20,
  243. scopedSlots: {
  244. customRender: 'action'
  245. }
  246. },
  247. {
  248. title: '{{ i18n "pages.xray.fakedns.ipPool"}}',
  249. dataIndex: 'ipPool',
  250. align: 'center',
  251. width: 50
  252. },
  253. {
  254. title: '{{ i18n "pages.xray.fakedns.poolSize"}}',
  255. dataIndex: 'poolSize',
  256. align: 'center',
  257. width: 50
  258. },
  259. ];
  260. const app = new Vue({
  261. delimiters: ['[[', ']]'],
  262. mixins: [MediaQueryMixin],
  263. el: '#app',
  264. data: {
  265. themeSwitcher,
  266. isDarkTheme: themeSwitcher.isDarkTheme,
  267. loadingStates: {
  268. fetched: false,
  269. spinning: false
  270. },
  271. oldXraySetting: '',
  272. xraySetting: '',
  273. outboundTestUrl: 'https://www.google.com/generate_204',
  274. oldOutboundTestUrl: 'https://www.google.com/generate_204',
  275. inboundTags: [],
  276. outboundsTraffic: [],
  277. outboundTestStates: {}, // Track testing state and results for each outbound
  278. saveBtnDisable: true,
  279. refreshing: false,
  280. restartResult: '',
  281. showAlert: false,
  282. customGeoAliasLabelSuffix: '{{ i18n "pages.index.customGeoAliasLabelSuffix" }}',
  283. advSettings: 'xraySetting',
  284. obsSettings: '',
  285. cm: null,
  286. cmOptions: {
  287. lineNumbers: true,
  288. mode: "application/json",
  289. lint: true,
  290. styleActiveLine: true,
  291. matchBrackets: true,
  292. theme: "xq",
  293. autoCloseTags: true,
  294. lineWrapping: true,
  295. indentUnit: 2,
  296. indentWithTabs: true,
  297. smartIndent: true,
  298. tabSize: 2,
  299. lineWiseCopyCut: false,
  300. foldGutter: true,
  301. gutters: [
  302. "CodeMirror-lint-markers",
  303. "CodeMirror-linenumbers",
  304. "CodeMirror-foldgutter",
  305. ],
  306. },
  307. ipv4Settings: {
  308. tag: "IPv4",
  309. protocol: "freedom",
  310. settings: {
  311. domainStrategy: "UseIPv4"
  312. }
  313. },
  314. directSettings: {
  315. tag: "direct",
  316. protocol: "freedom"
  317. },
  318. routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
  319. log: {
  320. loglevel: ["none", "debug", "info", "warning", "error"],
  321. access: ["none", "./access.log"],
  322. error: ["none", "./error.log"],
  323. dnsLog: false,
  324. maskAddress: ["quarter", "half", "full"],
  325. },
  326. settingsData: {
  327. protocols: {
  328. bittorrent: ["bittorrent"],
  329. },
  330. IPsOptions: [{
  331. label: 'Private IPs',
  332. value: 'geoip:private'
  333. },
  334. {
  335. label: '🇮🇷 Iran',
  336. value: 'ext:geoip_IR.dat:ir'
  337. },
  338. {
  339. label: '🇨🇳 China',
  340. value: 'geoip:cn'
  341. },
  342. {
  343. label: '🇷🇺 Russia',
  344. value: 'ext:geoip_RU.dat:ru'
  345. },
  346. {
  347. label: '🇻🇳 Vietnam',
  348. value: 'geoip:vn'
  349. },
  350. {
  351. label: '🇪🇸 Spain',
  352. value: 'geoip:es'
  353. },
  354. {
  355. label: '🇮🇩 Indonesia',
  356. value: 'geoip:id'
  357. },
  358. {
  359. label: '🇺🇦 Ukraine',
  360. value: 'geoip:ua'
  361. },
  362. {
  363. label: '🇹🇷 Türkiye',
  364. value: 'geoip:tr'
  365. },
  366. {
  367. label: '🇧🇷 Brazil',
  368. value: 'geoip:br'
  369. },
  370. ],
  371. DomainsOptions: [{
  372. label: '🇮🇷 Iran',
  373. value: 'ext:geosite_IR.dat:ir'
  374. },
  375. {
  376. label: '🇮🇷 .ir',
  377. value: 'regexp:.*\\.ir$'
  378. },
  379. {
  380. label: '🇮🇷 .ایران',
  381. value: 'regexp:.*\\.xn--mgba3a4f16a$'
  382. },
  383. {
  384. label: '🇨🇳 China',
  385. value: 'geosite:cn'
  386. },
  387. {
  388. label: '🇨🇳 .cn',
  389. value: 'regexp:.*\\.cn$'
  390. },
  391. {
  392. label: '🇷🇺 Russia',
  393. value: 'ext:geosite_RU.dat:ru-available-only-inside'
  394. },
  395. {
  396. label: '🇷🇺 .ru',
  397. value: 'regexp:.*\\.ru$'
  398. },
  399. {
  400. label: '🇷🇺 .su',
  401. value: 'regexp:.*\\.su$'
  402. },
  403. {
  404. label: '🇷🇺 .рф',
  405. value: 'regexp:.*\\.xn--p1ai$'
  406. },
  407. {
  408. label: '🇻🇳 .vn',
  409. value: 'regexp:.*\\.vn$'
  410. },
  411. ],
  412. BlockDomainsOptions: [{
  413. label: 'Ads All',
  414. value: 'geosite:category-ads-all'
  415. },
  416. {
  417. label: 'Ads IR 🇮🇷',
  418. value: 'ext:geosite_IR.dat:category-ads-all'
  419. },
  420. {
  421. label: 'Ads RU 🇷🇺',
  422. value: 'ext:geosite_RU.dat:category-ads-all'
  423. },
  424. {
  425. label: 'Malware 🇮🇷',
  426. value: 'ext:geosite_IR.dat:malware'
  427. },
  428. {
  429. label: 'Phishing 🇮🇷',
  430. value: 'ext:geosite_IR.dat:phishing'
  431. },
  432. {
  433. label: 'Cryptominers 🇮🇷',
  434. value: 'ext:geosite_IR.dat:cryptominers'
  435. },
  436. {
  437. label: 'Adult +18',
  438. value: 'geosite:category-porn'
  439. },
  440. {
  441. label: '🇮🇷 Iran',
  442. value: 'ext:geosite_IR.dat:ir'
  443. },
  444. {
  445. label: '🇮🇷 .ir',
  446. value: 'regexp:.*\\.ir$'
  447. },
  448. {
  449. label: '🇮🇷 .ایران',
  450. value: 'regexp:.*\\.xn--mgba3a4f16a$'
  451. },
  452. {
  453. label: '🇨🇳 China',
  454. value: 'geosite:cn'
  455. },
  456. {
  457. label: '🇨🇳 .cn',
  458. value: 'regexp:.*\\.cn$'
  459. },
  460. {
  461. label: '🇷🇺 Russia',
  462. value: 'ext:geosite_RU.dat:ru-available-only-inside'
  463. },
  464. {
  465. label: '🇷🇺 .ru',
  466. value: 'regexp:.*\\.ru$'
  467. },
  468. {
  469. label: '🇷🇺 .su',
  470. value: 'regexp:.*\\.su$'
  471. },
  472. {
  473. label: '🇷🇺 .рф',
  474. value: 'regexp:.*\\.xn--p1ai$'
  475. },
  476. {
  477. label: '🇻🇳 .vn',
  478. value: 'regexp:.*\\.vn$'
  479. },
  480. ],
  481. ServicesOptions: [{
  482. label: 'Apple',
  483. value: 'geosite:apple'
  484. },
  485. {
  486. label: 'Meta',
  487. value: 'geosite:meta'
  488. },
  489. {
  490. label: 'Google',
  491. value: 'geosite:google'
  492. },
  493. {
  494. label: 'OpenAI',
  495. value: 'geosite:openai'
  496. },
  497. {
  498. label: 'Spotify',
  499. value: 'geosite:spotify'
  500. },
  501. {
  502. label: 'Netflix',
  503. value: 'geosite:netflix'
  504. },
  505. {
  506. label: 'Reddit',
  507. value: 'geosite:reddit'
  508. },
  509. {
  510. label: 'Speedtest',
  511. value: 'geosite:speedtest'
  512. },
  513. ]
  514. },
  515. defaultObservatory: {
  516. subjectSelector: [],
  517. probeURL: "https://www.google.com/generate_204",
  518. probeInterval: "1m",
  519. enableConcurrency: true
  520. },
  521. defaultBurstObservatory: {
  522. subjectSelector: [],
  523. pingConfig: {
  524. destination: "https://www.google.com/generate_204",
  525. interval: "1m",
  526. connectivity: "http://connectivitycheck.platform.hicloud.com/generate_204",
  527. timeout: "5s",
  528. sampling: 2
  529. }
  530. }
  531. },
  532. methods: {
  533. loading(spinning = true) {
  534. this.loadingStates.spinning = spinning;
  535. },
  536. async getOutboundsTraffic() {
  537. const msg = await HttpUtil.get("/panel/xray/getOutboundsTraffic");
  538. if (msg.success) {
  539. this.outboundsTraffic = msg.obj;
  540. }
  541. },
  542. async getXraySetting() {
  543. const msg = await HttpUtil.post("/panel/xray/");
  544. if (msg.success) {
  545. if (!this.loadingStates.fetched) {
  546. this.loadingStates.fetched = true
  547. }
  548. result = JSON.parse(msg.obj);
  549. xs = JSON.stringify(result.xraySetting, null, 2);
  550. this.oldXraySetting = xs;
  551. this.xraySetting = xs;
  552. this.inboundTags = result.inboundTags;
  553. this.outboundTestUrl = result.outboundTestUrl || 'https://www.google.com/generate_204';
  554. this.oldOutboundTestUrl = this.outboundTestUrl;
  555. this.saveBtnDisable = true;
  556. }
  557. },
  558. async updateXraySetting() {
  559. this.loading(true);
  560. const msg = await HttpUtil.post("/panel/xray/update", {
  561. xraySetting: this.xraySetting,
  562. outboundTestUrl: this.outboundTestUrl || 'https://www.google.com/generate_204'
  563. });
  564. this.loading(false);
  565. if (msg.success) {
  566. await this.getXraySetting();
  567. }
  568. },
  569. async restartXray() {
  570. this.loading(true);
  571. const msg = await HttpUtil.post("/panel/api/server/restartXrayService");
  572. this.loading(false);
  573. if (msg.success) {
  574. await PromiseUtil.sleep(500);
  575. await this.getXrayResult();
  576. }
  577. this.loading(false);
  578. },
  579. async getXrayResult() {
  580. const msg = await HttpUtil.get("/panel/xray/getXrayResult");
  581. if (msg.success) {
  582. this.restartResult = msg.obj;
  583. if (msg.obj.length > 1) Vue.prototype.$message.error(msg.obj);
  584. }
  585. },
  586. async resetXrayConfigToDefault() {
  587. this.loading(true);
  588. const msg = await HttpUtil.get("/panel/setting/getDefaultJsonConfig");
  589. this.loading(false);
  590. if (msg.success) {
  591. this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
  592. this.saveBtnDisable = true;
  593. }
  594. },
  595. changePage(pageKey) {
  596. if (pageKey == 'tpl-advanced') this.changeCode();
  597. if (pageKey == 'tpl-balancer') this.changeObsCode();
  598. },
  599. syncRulesWithOutbound(tag, setting) {
  600. const newTemplateSettings = this.templateSettings;
  601. const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);
  602. const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag);
  603. if (!haveRules && outboundIndex > 0) {
  604. newTemplateSettings.outbounds.splice(outboundIndex);
  605. }
  606. if (haveRules && outboundIndex < 0) {
  607. newTemplateSettings.outbounds.push(setting);
  608. }
  609. this.templateSettings = newTemplateSettings;
  610. },
  611. templateRuleGetter(routeSettings) {
  612. const {
  613. property,
  614. outboundTag
  615. } = routeSettings;
  616. let result = [];
  617. if (this.templateSettings != null) {
  618. this.templateSettings.routing.rules.forEach(
  619. (routingRule) => {
  620. if (
  621. routingRule.hasOwnProperty(property) &&
  622. routingRule.hasOwnProperty("outboundTag") &&
  623. routingRule.outboundTag === outboundTag
  624. ) {
  625. result.push(...routingRule[property]);
  626. }
  627. }
  628. );
  629. }
  630. return result;
  631. },
  632. templateRuleSetter(routeSettings) {
  633. const {
  634. data,
  635. property,
  636. outboundTag
  637. } = routeSettings;
  638. const oldTemplateSettings = this.templateSettings;
  639. const newTemplateSettings = oldTemplateSettings;
  640. currentProperty = this.templateRuleGetter({
  641. outboundTag,
  642. property
  643. })
  644. if (currentProperty.length == 0) {
  645. const propertyRule = {
  646. type: "field",
  647. outboundTag,
  648. [property]: data
  649. };
  650. newTemplateSettings.routing.rules.push(propertyRule);
  651. } else {
  652. const newRules = [];
  653. insertedOnce = false;
  654. newTemplateSettings.routing.rules.forEach(
  655. (routingRule) => {
  656. if (
  657. routingRule.hasOwnProperty(property) &&
  658. routingRule.hasOwnProperty("outboundTag") &&
  659. routingRule.outboundTag === outboundTag
  660. ) {
  661. if (!insertedOnce && data.length > 0) {
  662. insertedOnce = true;
  663. routingRule[property] = data;
  664. newRules.push(routingRule);
  665. }
  666. } else {
  667. newRules.push(routingRule);
  668. }
  669. }
  670. );
  671. newTemplateSettings.routing.rules = newRules;
  672. }
  673. this.templateSettings = newTemplateSettings;
  674. },
  675. changeCode() {
  676. if (this.cm != null) {
  677. this.cm.toTextArea();
  678. }
  679. textAreaObj = document.getElementById('xraySetting');
  680. textAreaObj.value = this[this.advSettings];
  681. this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions);
  682. this.cm.on('change', editor => {
  683. value = editor.getValue();
  684. if (this.isJsonString(value)) {
  685. this[this.advSettings] = value;
  686. }
  687. });
  688. },
  689. changeObsCode() {
  690. if (this.cm != null) {
  691. this.cm.toTextArea();
  692. }
  693. if (this.obsSettings == '') {
  694. this.cm = null;
  695. return
  696. }
  697. textAreaObj = document.getElementById('obsSetting');
  698. textAreaObj.value = this[this.obsSettings];
  699. this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions);
  700. this.cm.on('change', editor => {
  701. value = editor.getValue();
  702. if (this.isJsonString(value)) {
  703. this[this.obsSettings] = value;
  704. }
  705. });
  706. },
  707. isJsonString(str) {
  708. try {
  709. JSON.parse(str);
  710. } catch (e) {
  711. return false;
  712. }
  713. return true;
  714. },
  715. // outboundTrafficFor returns {up, down} for an outbound by tag,
  716. // defaulting to zeros when no traffic row has been reported yet.
  717. // Templates use the up/down accessors below — keeping the lookup in
  718. // one place avoids drift if the data shape changes.
  719. outboundTrafficFor(o) {
  720. const t = this.outboundsTraffic.find(t => t.tag == o.tag);
  721. return { up: t ? t.up : 0, down: t ? t.down : 0 };
  722. },
  723. findOutboundUp(o) { return this.outboundTrafficFor(o).up; },
  724. findOutboundDown(o) { return this.outboundTrafficFor(o).down; },
  725. // One tone per category instead of per-value. Adding a new protocol or
  726. // transport inherits the category colour — no styling work required.
  727. // Hierarchy: emerald (protocol — primary identity, matches brand) →
  728. // slate (network — transport is plumbing, sits back) → violet (security —
  729. // accent, only rendered for tls/reality so a stand-out hue is earned).
  730. outboundProtocolTone() { return 'tone-emerald'; },
  731. outboundNetworkTone() { return 'tone-slate'; },
  732. outboundSecurityTone() { return 'tone-violet'; },
  733. // Whether the security label is one we render as a pill in the table.
  734. isOutboundSecurityVisible(security) {
  735. return security === 'tls' || security === 'reality';
  736. },
  737. // Null-safe accessor for the address list — collapses null/undefined
  738. // returns from findOutboundAddress() into an empty array so the template
  739. // can rely on .length and v-for without extra guards.
  740. outboundAddresses(o) {
  741. return this.findOutboundAddress(o) || [];
  742. },
  743. // Test-state accessors — sparse arrays + per-row state make raw checks
  744. // verbose; these helpers keep the template readable and consistent.
  745. isOutboundTesting(index) {
  746. const s = this.outboundTestStates[index];
  747. return !!(s && s.testing);
  748. },
  749. outboundTestResult(index) {
  750. const s = this.outboundTestStates[index];
  751. return s ? s.result : null;
  752. },
  753. isOutboundUntestable(outbound) {
  754. return outbound.protocol === 'blackhole' || outbound.tag === 'blocked';
  755. },
  756. // csv splits a comma-separated rule field into trimmed non-empty values.
  757. // Routing rule data uses CSV strings for multi-value criteria (e.g.
  758. // sourceIP "1.2.3.0/24,4.5.6.0/24"); the modern table renders each
  759. // criterion as a single summary pill, so values are normally re-joined
  760. // via joinCsv() but this helper is kept for callers that need an array.
  761. csv(value) {
  762. if (!value) return [];
  763. return String(value)
  764. .split(',')
  765. .map(v => v.trim())
  766. .filter(v => v.length > 0);
  767. },
  768. // joinCsv normalises a CSV-style rule field into a single comma-space
  769. // separated string suitable for tooltips. Returns '' for empty inputs
  770. // so v-if guards can short-circuit on the raw rule field.
  771. joinCsv(value) {
  772. return this.csv(value).join(', ');
  773. },
  774. findOutboundAddress(o) {
  775. serverObj = null;
  776. switch (o.protocol) {
  777. case Protocols.VMess:
  778. serverObj = o.settings.vnext;
  779. break;
  780. case Protocols.VLESS:
  781. return [o.settings?.address + ':' + o.settings?.port];
  782. case Protocols.HTTP:
  783. case Protocols.Socks:
  784. case Protocols.Shadowsocks:
  785. case Protocols.Trojan:
  786. serverObj = o.settings.servers;
  787. break;
  788. case Protocols.DNS:
  789. return [o.settings?.address + ':' + o.settings?.port];
  790. case Protocols.Wireguard:
  791. return o.settings.peers.map(peer => peer.endpoint);
  792. default:
  793. return null;
  794. }
  795. return serverObj ? serverObj.map(obj => obj.address + ':' + obj.port) : null;
  796. },
  797. addOutbound() {
  798. outModal.show({
  799. title: '{{ i18n "pages.xray.outbound.addOutbound"}}',
  800. okText: '{{ i18n "pages.xray.outbound.addOutbound" }}',
  801. confirm: (outbound) => {
  802. outModal.loading();
  803. if (outbound.tag.length > 0) {
  804. this.templateSettings.outbounds.push(outbound);
  805. this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);
  806. }
  807. outModal.close();
  808. },
  809. isEdit: false,
  810. tags: this.templateSettings.outbounds.map(obj => obj.tag)
  811. });
  812. },
  813. editOutbound(index) {
  814. outModal.show({
  815. title: '{{ i18n "pages.xray.outbound.editOutbound"}} ' + (index + 1),
  816. outbound: app.templateSettings.outbounds[index],
  817. confirm: (outbound) => {
  818. outModal.loading();
  819. this.templateSettings.outbounds[index] = outbound;
  820. this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);
  821. outModal.close();
  822. },
  823. isEdit: true,
  824. tags: this.outboundData.filter((o) => o.key != index).map(obj => obj.tag)
  825. });
  826. },
  827. deleteOutbound(index) {
  828. outbounds = this.templateSettings.outbounds;
  829. outbounds.splice(index, 1);
  830. this.outboundSettings = JSON.stringify(outbounds);
  831. },
  832. setFirstOutbound(index) {
  833. outbounds = this.templateSettings.outbounds;
  834. outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
  835. this.outboundSettings = JSON.stringify(outbounds);
  836. },
  837. async testOutbound(index) {
  838. const outbound = this.templateSettings.outbounds[index];
  839. if (!outbound) {
  840. Vue.prototype.$message.error('{{ i18n "pages.xray.outbound.testError" }}');
  841. return;
  842. }
  843. if (outbound.protocol === 'blackhole' || outbound.tag === 'blocked') {
  844. Vue.prototype.$message.warning(
  845. '{{ i18n "pages.xray.outbound.testError" }}: blocked/blackhole outbound');
  846. return;
  847. }
  848. // Initialize test state for this outbound if not exists
  849. if (!this.outboundTestStates[index]) {
  850. this.$set(this.outboundTestStates, index, {
  851. testing: false,
  852. result: null
  853. });
  854. }
  855. // Set testing state
  856. this.$set(this.outboundTestStates[index], 'testing', true);
  857. this.$set(this.outboundTestStates[index], 'result', null);
  858. try {
  859. const outboundJSON = JSON.stringify(outbound);
  860. const allOutboundsJSON = JSON.stringify(this.templateSettings.outbounds || []);
  861. const msg = await HttpUtil.post("/panel/xray/testOutbound", {
  862. outbound: outboundJSON,
  863. allOutbounds: allOutboundsJSON
  864. });
  865. // Update test state
  866. this.$set(this.outboundTestStates[index], 'testing', false);
  867. if (msg.success && msg.obj) {
  868. const result = msg.obj;
  869. this.$set(this.outboundTestStates[index], 'result', result);
  870. if (result.success) {
  871. Vue.prototype.$message.success(
  872. `{{ i18n "pages.xray.outbound.testSuccess" }}: ${result.delay}ms (${result.statusCode})`
  873. );
  874. } else {
  875. Vue.prototype.$message.error(
  876. `{{ i18n "pages.xray.outbound.testFailed" }}: ${result.error || 'Unknown error'}`
  877. );
  878. }
  879. } else {
  880. this.$set(this.outboundTestStates[index], 'result', {
  881. success: false,
  882. error: msg.msg || '{{ i18n "pages.xray.outbound.testError" }}'
  883. });
  884. Vue.prototype.$message.error(msg.msg || '{{ i18n "pages.xray.outbound.testError" }}');
  885. }
  886. } catch (error) {
  887. this.$set(this.outboundTestStates[index], 'testing', false);
  888. this.$set(this.outboundTestStates[index], 'result', {
  889. success: false,
  890. error: error.message || '{{ i18n "pages.xray.outbound.testError" }}'
  891. });
  892. Vue.prototype.$message.error('{{ i18n "pages.xray.outbound.testError" }}: ' + error.message);
  893. }
  894. },
  895. async refreshOutboundTraffic() {
  896. if (!this.refreshing) {
  897. this.refreshing = true;
  898. await this.getOutboundsTraffic();
  899. data = []
  900. if (this.templateSettings != null) {
  901. this.templateSettings.outbounds.forEach((o, index) => {
  902. data.push({
  903. 'key': index,
  904. ...o
  905. });
  906. });
  907. }
  908. this.outboundData = data;
  909. this.refreshing = false;
  910. }
  911. },
  912. async resetOutboundTraffic(index) {
  913. let tag = "-alltags-";
  914. if (index >= 0) {
  915. tag = this.outboundData[index].tag ? this.outboundData[index].tag : ""
  916. }
  917. const msg = await HttpUtil.post("/panel/xray/resetOutboundsTraffic", {
  918. tag: tag
  919. });
  920. if (msg.success) {
  921. await this.refreshOutboundTraffic();
  922. }
  923. },
  924. addBalancer() {
  925. balancerModal.show({
  926. title: '{{ i18n "pages.xray.balancer.addBalancer"}}',
  927. okText: '{{ i18n "pages.xray.balancer.addBalancer"}}',
  928. balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
  929. balancer: {
  930. tag: '',
  931. strategy: 'random',
  932. selector: [],
  933. fallbackTag: ''
  934. },
  935. confirm: (balancer) => {
  936. balancerModal.loading();
  937. newTemplateSettings = this.templateSettings;
  938. if (newTemplateSettings.routing.balancers == undefined) {
  939. newTemplateSettings.routing.balancers = [];
  940. }
  941. let tmpBalancer = {
  942. 'tag': balancer.tag,
  943. 'selector': balancer.selector,
  944. 'fallbackTag': balancer.fallbackTag
  945. };
  946. if (balancer.strategy && balancer.strategy != 'random') {
  947. tmpBalancer.strategy = {
  948. 'type': balancer.strategy
  949. };
  950. }
  951. newTemplateSettings.routing.balancers.push(tmpBalancer);
  952. this.templateSettings = newTemplateSettings;
  953. this.updateObservatorySelectors();
  954. balancerModal.close();
  955. this.changeObsCode();
  956. },
  957. isEdit: false
  958. });
  959. },
  960. editBalancer(index) {
  961. const oldTag = this.balancersData[index].tag;
  962. balancerModal.show({
  963. title: '{{ i18n "pages.xray.balancer.editBalancer"}}',
  964. okText: '{{ i18n "sure" }}',
  965. balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
  966. balancer: this.balancersData[index],
  967. confirm: (balancer) => {
  968. balancerModal.loading();
  969. newTemplateSettings = this.templateSettings;
  970. let tmpBalancer = {
  971. 'tag': balancer.tag,
  972. 'selector': balancer.selector,
  973. 'fallbackTag': balancer.fallbackTag
  974. };
  975. // Remove old tag
  976. if (newTemplateSettings.observatory) {
  977. newTemplateSettings.observatory.subjectSelector = newTemplateSettings.observatory
  978. .subjectSelector.filter(s => s != oldTag);
  979. }
  980. if (newTemplateSettings.burstObservatory) {
  981. newTemplateSettings.burstObservatory.subjectSelector = newTemplateSettings.burstObservatory
  982. .subjectSelector.filter(s => s != oldTag);
  983. }
  984. if (balancer.strategy && balancer.strategy != 'random') {
  985. tmpBalancer.strategy = {
  986. 'type': balancer.strategy
  987. };
  988. }
  989. newTemplateSettings.routing.balancers[index] = tmpBalancer;
  990. // change edited tag if used in rule section
  991. if (oldTag != balancer.tag) {
  992. newTemplateSettings.routing.rules.forEach((rule) => {
  993. if (rule.balancerTag && rule.balancerTag == oldTag) {
  994. rule.balancerTag = balancer.tag;
  995. }
  996. });
  997. }
  998. this.templateSettings = newTemplateSettings;
  999. this.updateObservatorySelectors();
  1000. balancerModal.close();
  1001. this.changeObsCode();
  1002. },
  1003. isEdit: true
  1004. });
  1005. },
  1006. updateObservatorySelectors() {
  1007. newTemplateSettings = this.templateSettings;
  1008. const leastPings = this.balancersData.filter((b) => b.strategy == 'leastPing');
  1009. const leastLoads = this.balancersData.filter((b) =>
  1010. b.strategy === 'leastLoad' ||
  1011. b.strategy === 'roundRobin' ||
  1012. b.strategy === 'random'
  1013. );
  1014. if (leastPings.length > 0) {
  1015. if (!newTemplateSettings.observatory)
  1016. newTemplateSettings.observatory = this.defaultObservatory;
  1017. newTemplateSettings.observatory.subjectSelector = [];
  1018. leastPings.forEach((b) => {
  1019. b.selector.forEach((s) => {
  1020. if (!newTemplateSettings.observatory.subjectSelector.includes(s))
  1021. newTemplateSettings.observatory.subjectSelector.push(s);
  1022. });
  1023. });
  1024. } else {
  1025. delete newTemplateSettings.observatory
  1026. }
  1027. if (leastLoads.length > 0) {
  1028. if (!newTemplateSettings.burstObservatory)
  1029. newTemplateSettings.burstObservatory = this.defaultBurstObservatory;
  1030. newTemplateSettings.burstObservatory.subjectSelector = [];
  1031. leastLoads.forEach((b) => {
  1032. b.selector.forEach((s) => {
  1033. if (!newTemplateSettings.burstObservatory.subjectSelector.includes(s))
  1034. newTemplateSettings.burstObservatory.subjectSelector.push(s);
  1035. });
  1036. });
  1037. } else {
  1038. delete newTemplateSettings.burstObservatory
  1039. }
  1040. this.templateSettings = newTemplateSettings;
  1041. this.changeObsCode();
  1042. },
  1043. deleteBalancer(index) {
  1044. newTemplateSettings = this.templateSettings;
  1045. // Remove from balancers
  1046. const removedBalancer = this.balancersData.splice(index, 1)[0];
  1047. // Remove from settings
  1048. let realIndex = newTemplateSettings.routing.balancers.findIndex((b) => b.tag === removedBalancer.tag);
  1049. newTemplateSettings.routing.balancers.splice(realIndex, 1);
  1050. // Update balancers property to an empty array if there are no more balancers
  1051. if (newTemplateSettings.routing.balancers.length === 0) {
  1052. delete newTemplateSettings.routing.balancers;
  1053. }
  1054. // Remove orphaned balancer references from routing rules
  1055. if (newTemplateSettings.routing.rules) {
  1056. newTemplateSettings.routing.rules.forEach((rule) => {
  1057. if (rule.balancerTag && rule.balancerTag === removedBalancer.tag) {
  1058. delete rule.balancerTag;
  1059. }
  1060. });
  1061. }
  1062. this.templateSettings = newTemplateSettings;
  1063. this.updateObservatorySelectors();
  1064. this.obsSettings = '';
  1065. this.changeObsCode()
  1066. },
  1067. openDNSPresets() {
  1068. dnsPresetsModal.show({
  1069. title: '{{ i18n "pages.xray.dns.dnsPresetTitle" }}',
  1070. selected: (selectedPreset) => {
  1071. this.dnsServers = selectedPreset;
  1072. dnsPresetsModal.close();
  1073. }
  1074. });
  1075. },
  1076. addDNSServer() {
  1077. dnsModal.show({
  1078. title: '{{ i18n "pages.xray.dns.add" }}',
  1079. confirm: (dnsServer) => {
  1080. dnsServers = this.dnsServers;
  1081. dnsServers.push(dnsServer);
  1082. this.dnsServers = dnsServers;
  1083. dnsModal.close();
  1084. },
  1085. isEdit: false
  1086. });
  1087. },
  1088. editDNSServer(index) {
  1089. dnsModal.show({
  1090. title: '{{ i18n "pages.xray.dns.edit" }} #' + (index + 1),
  1091. dnsServer: this.dnsServers[index],
  1092. confirm: (dnsServer) => {
  1093. dnsServers = this.dnsServers;
  1094. dnsServers[index] = dnsServer;
  1095. this.dnsServers = dnsServers;
  1096. dnsModal.close();
  1097. },
  1098. isEdit: true
  1099. });
  1100. },
  1101. deleteDNSServer(index) {
  1102. newDnsServers = this.dnsServers;
  1103. newDnsServers.splice(index, 1);
  1104. this.dnsServers = newDnsServers;
  1105. },
  1106. addFakedns() {
  1107. fakednsModal.show({
  1108. title: '{{ i18n "pages.xray.fakedns.add" }}',
  1109. confirm: (item) => {
  1110. fakeDns = this.fakeDns ?? [];
  1111. fakeDns.push(item);
  1112. this.fakeDns = fakeDns;
  1113. fakednsModal.close();
  1114. },
  1115. isEdit: false
  1116. });
  1117. },
  1118. editFakedns(index) {
  1119. fakednsModal.show({
  1120. title: '{{ i18n "pages.xray.fakedns.edit" }} #' + (index + 1),
  1121. fakeDns: this.fakeDns[index],
  1122. confirm: (item) => {
  1123. fakeDns = this.fakeDns;
  1124. fakeDns[index] = item;
  1125. this.fakeDns = fakeDns;
  1126. fakednsModal.close();
  1127. },
  1128. isEdit: true
  1129. });
  1130. },
  1131. deleteFakedns(index) {
  1132. fakeDns = this.fakeDns;
  1133. fakeDns.splice(index, 1);
  1134. this.fakeDns = fakeDns;
  1135. },
  1136. addRule() {
  1137. ruleModal.show({
  1138. title: '{{ i18n "pages.xray.rules.add"}}',
  1139. okText: '{{ i18n "pages.xray.rules.add" }}',
  1140. confirm: (rule) => {
  1141. ruleModal.loading();
  1142. if (JSON.stringify(rule).length > 3) {
  1143. this.templateSettings.routing.rules.push(rule);
  1144. this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
  1145. }
  1146. ruleModal.close();
  1147. },
  1148. isEdit: false
  1149. });
  1150. },
  1151. editRule(index) {
  1152. ruleModal.show({
  1153. title: '{{ i18n "pages.xray.rules.edit"}} ' + (index + 1),
  1154. rule: app.templateSettings.routing.rules[index],
  1155. confirm: (rule) => {
  1156. ruleModal.loading();
  1157. if (JSON.stringify(rule).length > 3) {
  1158. this.templateSettings.routing.rules[index] = rule;
  1159. this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
  1160. }
  1161. ruleModal.close();
  1162. },
  1163. isEdit: true
  1164. });
  1165. },
  1166. replaceRule(old_index, new_index) {
  1167. rules = this.templateSettings.routing.rules;
  1168. if (new_index >= rules.length) rules.push(undefined);
  1169. rules.splice(new_index, 0, rules.splice(old_index, 1)[0]);
  1170. this.routingRuleSettings = JSON.stringify(rules);
  1171. },
  1172. deleteRule(index) {
  1173. rules = this.templateSettings.routing.rules;
  1174. rules.splice(index, 1);
  1175. this.routingRuleSettings = JSON.stringify(rules);
  1176. },
  1177. showWarp() {
  1178. warpModal.show();
  1179. },
  1180. showNord() {
  1181. nordModal.show();
  1182. },
  1183. async loadCustomGeoAliases() {
  1184. try {
  1185. const msg = await HttpUtil.get('/panel/api/custom-geo/aliases');
  1186. if (!msg.success) {
  1187. console.warn('Failed to load custom geo aliases:', msg.msg || 'request failed');
  1188. return;
  1189. }
  1190. if (!msg.obj) return;
  1191. const geoip = msg.obj.geoip ?? [];
  1192. const geosite = msg.obj.geosite ?? [];
  1193. const geoSuffix = this.customGeoAliasLabelSuffix || '';
  1194. geoip.forEach((x) => {
  1195. this.settingsData.IPsOptions.push({
  1196. label: x.alias + geoSuffix,
  1197. value: x.extExample,
  1198. });
  1199. });
  1200. geosite.forEach((x) => {
  1201. const opt = {
  1202. label: x.alias + geoSuffix,
  1203. value: x.extExample
  1204. };
  1205. this.settingsData.DomainsOptions.push(opt);
  1206. this.settingsData.BlockDomainsOptions.push(opt);
  1207. });
  1208. } catch (e) {
  1209. console.error('Failed to load custom geo aliases:', e);
  1210. }
  1211. }
  1212. },
  1213. async mounted() {
  1214. if (window.location.protocol !== "https:") {
  1215. this.showAlert = true;
  1216. }
  1217. await this.getXraySetting();
  1218. await this.loadCustomGeoAliases();
  1219. await this.getXrayResult();
  1220. await this.getOutboundsTraffic();
  1221. if (window.wsClient) {
  1222. window.wsClient.connect();
  1223. window.wsClient.on('outbounds', (payload) => {
  1224. if (payload) {
  1225. this.outboundsTraffic = payload;
  1226. this.$forceUpdate();
  1227. }
  1228. });
  1229. // Handle invalidate signals (sent when payload is too large for WebSocket,
  1230. // or when traffic job notifies about data changes)
  1231. window.wsClient.on('invalidate', (payload) => {
  1232. if (payload && payload.type === 'outbounds') {
  1233. this.refreshOutboundTraffic();
  1234. }
  1235. });
  1236. }
  1237. while (true) {
  1238. await PromiseUtil.sleep(800);
  1239. this.saveBtnDisable = this.oldXraySetting === this.xraySetting && this.oldOutboundTestUrl === this
  1240. .outboundTestUrl;
  1241. }
  1242. },
  1243. computed: {
  1244. templateSettings: {
  1245. get: function() {
  1246. const parsedSettings = this.xraySetting ? JSON.parse(this.xraySetting) : null;
  1247. return parsedSettings;
  1248. },
  1249. set: function(newValue) {
  1250. if (newValue) {
  1251. this.xraySetting = JSON.stringify(newValue, null, 2);
  1252. }
  1253. },
  1254. },
  1255. inboundSettings: {
  1256. get: function() {
  1257. return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null;
  1258. },
  1259. set: function(newValue) {
  1260. newTemplateSettings = this.templateSettings;
  1261. newTemplateSettings.inbounds = JSON.parse(newValue);
  1262. this.templateSettings = newTemplateSettings;
  1263. },
  1264. },
  1265. outboundSettings: {
  1266. get: function() {
  1267. return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null;
  1268. },
  1269. set: function(newValue) {
  1270. newTemplateSettings = this.templateSettings;
  1271. newTemplateSettings.outbounds = JSON.parse(newValue);
  1272. this.templateSettings = newTemplateSettings;
  1273. },
  1274. },
  1275. outboundData: {
  1276. get: function() {
  1277. data = []
  1278. if (this.templateSettings != null) {
  1279. this.templateSettings.outbounds.forEach((o, index) => {
  1280. data.push({
  1281. 'key': index,
  1282. ...o
  1283. });
  1284. });
  1285. }
  1286. return data;
  1287. },
  1288. },
  1289. routingRuleSettings: {
  1290. get: function() {
  1291. return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null;
  1292. },
  1293. set: function(newValue) {
  1294. newTemplateSettings = this.templateSettings;
  1295. newTemplateSettings.routing.rules = JSON.parse(newValue);
  1296. this.templateSettings = newTemplateSettings;
  1297. },
  1298. },
  1299. routingRuleData: {
  1300. get: function() {
  1301. data = [];
  1302. if (this.templateSettings != null) {
  1303. this.templateSettings.routing.rules.forEach((r, index) => {
  1304. data.push({
  1305. 'key': index,
  1306. ...r
  1307. });
  1308. });
  1309. // Make rules readable
  1310. data.forEach(r => {
  1311. if (r.domain) r.domain = r.domain.join(',')
  1312. if (r.ip) r.ip = r.ip.join(',')
  1313. if (r.source) r.source = r.source.join(',');
  1314. if (r.user) r.user = r.user.join(',')
  1315. if (r.inboundTag) r.inboundTag = r.inboundTag.join(',')
  1316. if (r.protocol) r.protocol = r.protocol.join(',')
  1317. if (r.attrs) r.attrs = JSON.stringify(r.attrs, null, 2)
  1318. });
  1319. }
  1320. return data;
  1321. }
  1322. },
  1323. balancersData: {
  1324. get: function() {
  1325. data = []
  1326. if (this.templateSettings != null && this.templateSettings.routing != null && this.templateSettings
  1327. .routing.balancers != null) {
  1328. this.templateSettings.routing.balancers.forEach((o, index) => {
  1329. data.push({
  1330. 'key': index,
  1331. 'tag': o.tag ? o.tag : "",
  1332. 'strategy': o.strategy?.type ?? "random",
  1333. 'selector': o.selector ? o.selector : [],
  1334. 'fallbackTag': o.fallbackTag ?? '',
  1335. });
  1336. });
  1337. }
  1338. return data;
  1339. }
  1340. },
  1341. observatory: {
  1342. get: function() {
  1343. return this.templateSettings?.observatory ? JSON.stringify(this.templateSettings.observatory, null, 2) :
  1344. null;
  1345. },
  1346. set: function(newValue) {
  1347. newTemplateSettings = this.templateSettings;
  1348. newTemplateSettings.observatory = JSON.parse(newValue);
  1349. this.templateSettings = newTemplateSettings;
  1350. },
  1351. },
  1352. burstObservatory: {
  1353. get: function() {
  1354. return this.templateSettings?.burstObservatory ? JSON.stringify(this.templateSettings.burstObservatory,
  1355. null, 2) : null;
  1356. },
  1357. set: function(newValue) {
  1358. newTemplateSettings = this.templateSettings;
  1359. newTemplateSettings.burstObservatory = JSON.parse(newValue);
  1360. this.templateSettings = newTemplateSettings;
  1361. },
  1362. },
  1363. observatoryEnable: function() {
  1364. return this.templateSettings != null && this.templateSettings.observatory != undefined
  1365. },
  1366. burstObservatoryEnable: function() {
  1367. return this.templateSettings != null && this.templateSettings.burstObservatory != undefined
  1368. },
  1369. freedomStrategy: {
  1370. get: function() {
  1371. if (!this.templateSettings) return "AsIs";
  1372. freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && o.tag ==
  1373. "direct");
  1374. if (!freedomOutbound) return "AsIs";
  1375. if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs";
  1376. return freedomOutbound.settings.domainStrategy;
  1377. },
  1378. set: function(newValue) {
  1379. newTemplateSettings = this.templateSettings;
  1380. freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && o
  1381. .tag == "direct");
  1382. if (freedomOutboundIndex == -1) {
  1383. newTemplateSettings.outbounds.push({
  1384. protocol: "freedom",
  1385. tag: "direct",
  1386. settings: {
  1387. "domainStrategy": newValue
  1388. }
  1389. });
  1390. } else if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {
  1391. newTemplateSettings.outbounds[freedomOutboundIndex].settings = {
  1392. "domainStrategy": newValue
  1393. };
  1394. } else {
  1395. newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
  1396. }
  1397. this.templateSettings = newTemplateSettings;
  1398. }
  1399. },
  1400. routingStrategy: {
  1401. get: function() {
  1402. if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing
  1403. .domainStrategy) return "AsIs";
  1404. return this.templateSettings.routing.domainStrategy;
  1405. },
  1406. set: function(newValue) {
  1407. newTemplateSettings = this.templateSettings;
  1408. newTemplateSettings.routing.domainStrategy = newValue;
  1409. this.templateSettings = newTemplateSettings;
  1410. }
  1411. },
  1412. logLevel: {
  1413. get: function() {
  1414. if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.loglevel)
  1415. return "warning";
  1416. return this.templateSettings.log.loglevel;
  1417. },
  1418. set: function(newValue) {
  1419. newTemplateSettings = this.templateSettings;
  1420. newTemplateSettings.log.loglevel = newValue;
  1421. this.templateSettings = newTemplateSettings;
  1422. }
  1423. },
  1424. accessLog: {
  1425. get: function() {
  1426. if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.access)
  1427. return "";
  1428. return this.templateSettings.log.access;
  1429. },
  1430. set: function(newValue) {
  1431. newTemplateSettings = this.templateSettings;
  1432. newTemplateSettings.log.access = newValue;
  1433. this.templateSettings = newTemplateSettings;
  1434. }
  1435. },
  1436. errorLog: {
  1437. get: function() {
  1438. if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.error) return "";
  1439. return this.templateSettings.log.error;
  1440. },
  1441. set: function(newValue) {
  1442. newTemplateSettings = this.templateSettings;
  1443. newTemplateSettings.log.error = newValue;
  1444. this.templateSettings = newTemplateSettings;
  1445. }
  1446. },
  1447. dnslog: {
  1448. get: function() {
  1449. if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.dnsLog)
  1450. return false;
  1451. return this.templateSettings.log.dnsLog;
  1452. },
  1453. set: function(newValue) {
  1454. newTemplateSettings = this.templateSettings;
  1455. newTemplateSettings.log.dnsLog = newValue;
  1456. this.templateSettings = newTemplateSettings;
  1457. }
  1458. },
  1459. statsInboundUplink: {
  1460. get: function() {
  1461. if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy
  1462. .system.statsInboundUplink) return false;
  1463. return this.templateSettings.policy.system.statsInboundUplink;
  1464. },
  1465. set: function(newValue) {
  1466. newTemplateSettings = this.templateSettings;
  1467. newTemplateSettings.policy.system.statsInboundUplink = newValue;
  1468. this.templateSettings = newTemplateSettings;
  1469. }
  1470. },
  1471. statsInboundDownlink: {
  1472. get: function() {
  1473. if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy
  1474. .system.statsInboundDownlink) return false;
  1475. return this.templateSettings.policy.system.statsInboundDownlink;
  1476. },
  1477. set: function(newValue) {
  1478. newTemplateSettings = this.templateSettings;
  1479. newTemplateSettings.policy.system.statsInboundDownlink = newValue;
  1480. this.templateSettings = newTemplateSettings;
  1481. }
  1482. },
  1483. statsOutboundUplink: {
  1484. get: function() {
  1485. if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy
  1486. .system.statsOutboundUplink) return false;
  1487. return this.templateSettings.policy.system.statsOutboundUplink;
  1488. },
  1489. set: function(newValue) {
  1490. newTemplateSettings = this.templateSettings;
  1491. newTemplateSettings.policy.system.statsOutboundUplink = newValue;
  1492. this.templateSettings = newTemplateSettings;
  1493. }
  1494. },
  1495. statsOutboundDownlink: {
  1496. get: function() {
  1497. if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy
  1498. .system.statsOutboundDownlink) return false;
  1499. return this.templateSettings.policy.system.statsOutboundDownlink;
  1500. },
  1501. set: function(newValue) {
  1502. newTemplateSettings = this.templateSettings;
  1503. newTemplateSettings.policy.system.statsOutboundDownlink = newValue;
  1504. this.templateSettings = newTemplateSettings;
  1505. }
  1506. },
  1507. maskAddressLog: {
  1508. get: function() {
  1509. if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.maskAddress)
  1510. return "";
  1511. return this.templateSettings.log.maskAddress;
  1512. },
  1513. set: function(newValue) {
  1514. newTemplateSettings = this.templateSettings;
  1515. newTemplateSettings.log.maskAddress = newValue;
  1516. this.templateSettings = newTemplateSettings;
  1517. }
  1518. },
  1519. blockedIPs: {
  1520. get: function() {
  1521. return this.templateRuleGetter({
  1522. outboundTag: "blocked",
  1523. property: "ip"
  1524. });
  1525. },
  1526. set: function(newValue) {
  1527. this.templateRuleSetter({
  1528. outboundTag: "blocked",
  1529. property: "ip",
  1530. data: newValue
  1531. });
  1532. }
  1533. },
  1534. blockedDomains: {
  1535. get: function() {
  1536. return this.templateRuleGetter({
  1537. outboundTag: "blocked",
  1538. property: "domain"
  1539. });
  1540. },
  1541. set: function(newValue) {
  1542. this.templateRuleSetter({
  1543. outboundTag: "blocked",
  1544. property: "domain",
  1545. data: newValue
  1546. });
  1547. }
  1548. },
  1549. blockedProtocols: {
  1550. get: function() {
  1551. return this.templateRuleGetter({
  1552. outboundTag: "blocked",
  1553. property: "protocol"
  1554. });
  1555. },
  1556. set: function(newValue) {
  1557. this.templateRuleSetter({
  1558. outboundTag: "blocked",
  1559. property: "protocol",
  1560. data: newValue
  1561. });
  1562. }
  1563. },
  1564. directIPs: {
  1565. get: function() {
  1566. return this.templateRuleGetter({
  1567. outboundTag: "direct",
  1568. property: "ip"
  1569. });
  1570. },
  1571. set: function(newValue) {
  1572. this.templateRuleSetter({
  1573. outboundTag: "direct",
  1574. property: "ip",
  1575. data: newValue
  1576. });
  1577. this.syncRulesWithOutbound("direct", this.directSettings);
  1578. }
  1579. },
  1580. directDomains: {
  1581. get: function() {
  1582. return this.templateRuleGetter({
  1583. outboundTag: "direct",
  1584. property: "domain"
  1585. });
  1586. },
  1587. set: function(newValue) {
  1588. this.templateRuleSetter({
  1589. outboundTag: "direct",
  1590. property: "domain",
  1591. data: newValue
  1592. });
  1593. this.syncRulesWithOutbound("direct", this.directSettings);
  1594. }
  1595. },
  1596. ipv4Domains: {
  1597. get: function() {
  1598. return this.templateRuleGetter({
  1599. outboundTag: "IPv4",
  1600. property: "domain"
  1601. });
  1602. },
  1603. set: function(newValue) {
  1604. this.templateRuleSetter({
  1605. outboundTag: "IPv4",
  1606. property: "domain",
  1607. data: newValue
  1608. });
  1609. this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
  1610. }
  1611. },
  1612. warpDomains: {
  1613. get: function() {
  1614. return this.templateRuleGetter({
  1615. outboundTag: "warp",
  1616. property: "domain"
  1617. });
  1618. },
  1619. set: function(newValue) {
  1620. this.templateRuleSetter({
  1621. outboundTag: "warp",
  1622. property: "domain",
  1623. data: newValue
  1624. });
  1625. }
  1626. },
  1627. nordTag: {
  1628. get: function() {
  1629. return this.templateSettings ? (this.templateSettings.outbounds.find((o) => o.tag.startsWith(
  1630. "nord-")) || {
  1631. tag: "nord"
  1632. }).tag : "nord";
  1633. }
  1634. },
  1635. nordDomains: {
  1636. get: function() {
  1637. return this.templateRuleGetter({
  1638. outboundTag: this.nordTag,
  1639. property: "domain"
  1640. });
  1641. },
  1642. set: function(newValue) {
  1643. this.templateRuleSetter({
  1644. outboundTag: this.nordTag,
  1645. property: "domain",
  1646. data: newValue
  1647. });
  1648. }
  1649. },
  1650. torrentSettings: {
  1651. get: function() {
  1652. return ArrayUtils.doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
  1653. },
  1654. set: function(newValue) {
  1655. if (newValue) {
  1656. this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent];
  1657. } else {
  1658. this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent
  1659. .includes(data));
  1660. }
  1661. },
  1662. },
  1663. WarpExist: {
  1664. get: function() {
  1665. return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag == "warp") >= 0 :
  1666. false;
  1667. },
  1668. },
  1669. NordExist: {
  1670. get: function() {
  1671. return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag.startsWith(
  1672. "nord-")) >= 0 : false;
  1673. },
  1674. },
  1675. enableDNS: {
  1676. get: function() {
  1677. return this.templateSettings ? this.templateSettings.dns != null : false;
  1678. },
  1679. set: function(newValue) {
  1680. newTemplateSettings = this.templateSettings;
  1681. if (newValue) {
  1682. newTemplateSettings.dns = {
  1683. servers: [],
  1684. queryStrategy: "UseIP",
  1685. tag: "dns_inbound",
  1686. enableParallelQuery: false
  1687. };
  1688. newTemplateSettings.fakedns = null;
  1689. } else {
  1690. delete newTemplateSettings.dns;
  1691. delete newTemplateSettings.fakedns;
  1692. }
  1693. this.templateSettings = newTemplateSettings;
  1694. }
  1695. },
  1696. dnsTag: {
  1697. get: function() {
  1698. return this.enableDNS ? this.templateSettings.dns.tag : "";
  1699. },
  1700. set: function(newValue) {
  1701. newTemplateSettings = this.templateSettings;
  1702. newTemplateSettings.dns.tag = newValue;
  1703. this.templateSettings = newTemplateSettings;
  1704. }
  1705. },
  1706. dnsClientIp: {
  1707. get: function() {
  1708. return this.enableDNS ? this.templateSettings.dns.clientIp : null;
  1709. },
  1710. set: function(newValue) {
  1711. newTemplateSettings = this.templateSettings;
  1712. if (newValue) {
  1713. newTemplateSettings.dns.clientIp = newValue;
  1714. } else {
  1715. delete newTemplateSettings.dns.clientIp;
  1716. }
  1717. this.templateSettings = newTemplateSettings;
  1718. }
  1719. },
  1720. dnsDisableCache: {
  1721. get: function() {
  1722. return this.enableDNS ? this.templateSettings.dns.disableCache : false;
  1723. },
  1724. set: function(newValue) {
  1725. newTemplateSettings = this.templateSettings;
  1726. if (newValue) {
  1727. newTemplateSettings.dns.disableCache = newValue;
  1728. } else {
  1729. delete newTemplateSettings.dns.disableCache
  1730. }
  1731. this.templateSettings = newTemplateSettings;
  1732. }
  1733. },
  1734. dnsDisableFallback: {
  1735. get: function() {
  1736. return this.enableDNS ? this.templateSettings.dns.disableFallback : false;
  1737. },
  1738. set: function(newValue) {
  1739. newTemplateSettings = this.templateSettings;
  1740. if (newValue) {
  1741. newTemplateSettings.dns.disableFallback = newValue;
  1742. } else {
  1743. delete newTemplateSettings.dns.disableFallback
  1744. }
  1745. this.templateSettings = newTemplateSettings;
  1746. }
  1747. },
  1748. dnsDisableFallbackIfMatch: {
  1749. get: function() {
  1750. return this.enableDNS ? this.templateSettings.dns.disableFallbackIfMatch : false;
  1751. },
  1752. set: function(newValue) {
  1753. newTemplateSettings = this.templateSettings;
  1754. if (newValue) {
  1755. newTemplateSettings.dns.disableFallbackIfMatch = newValue;
  1756. } else {
  1757. delete newTemplateSettings.dns.disableFallbackIfMatch
  1758. }
  1759. this.templateSettings = newTemplateSettings;
  1760. }
  1761. },
  1762. dnsEnableParallelQuery: {
  1763. get: function() {
  1764. return this.enableDNS ? (this.templateSettings.dns.enableParallelQuery || false) : false;
  1765. },
  1766. set: function(newValue) {
  1767. newTemplateSettings = this.templateSettings;
  1768. if (newValue) {
  1769. newTemplateSettings.dns.enableParallelQuery = newValue;
  1770. } else {
  1771. delete newTemplateSettings.dns.enableParallelQuery
  1772. }
  1773. this.templateSettings = newTemplateSettings;
  1774. }
  1775. },
  1776. dnsUseSystemHosts: {
  1777. get: function() {
  1778. return this.enableDNS ? this.templateSettings.dns.useSystemHosts : false;
  1779. },
  1780. set: function(newValue) {
  1781. newTemplateSettings = this.templateSettings;
  1782. if (newValue) {
  1783. newTemplateSettings.dns.useSystemHosts = newValue;
  1784. } else {
  1785. delete newTemplateSettings.dns.useSystemHosts
  1786. }
  1787. this.templateSettings = newTemplateSettings;
  1788. }
  1789. },
  1790. dnsStrategy: {
  1791. get: function() {
  1792. return this.enableDNS ? this.templateSettings.dns.queryStrategy : null;
  1793. },
  1794. set: function(newValue) {
  1795. newTemplateSettings = this.templateSettings;
  1796. newTemplateSettings.dns.queryStrategy = newValue;
  1797. this.templateSettings = newTemplateSettings;
  1798. }
  1799. },
  1800. dnsServers: {
  1801. get: function() {
  1802. return this.enableDNS ? this.templateSettings.dns.servers : [];
  1803. },
  1804. set: function(newValue) {
  1805. newTemplateSettings = this.templateSettings;
  1806. newTemplateSettings.dns.servers = newValue;
  1807. this.templateSettings = newTemplateSettings;
  1808. }
  1809. },
  1810. fakeDns: {
  1811. get: function() {
  1812. return this.templateSettings && this.templateSettings.fakedns ? this.templateSettings.fakedns : [];
  1813. },
  1814. set: function(newValue) {
  1815. newTemplateSettings = this.templateSettings;
  1816. if (this.enableDNS) {
  1817. newTemplateSettings.fakedns = newValue.length > 0 ? newValue : null;
  1818. } else {
  1819. delete newTemplateSettings.fakedns;
  1820. }
  1821. this.templateSettings = newTemplateSettings;
  1822. }
  1823. }
  1824. },
  1825. });
  1826. </script>
  1827. <style>
  1828. /* ───────── Modern outbounds table ─────────
  1829. Visual goals:
  1830. • flat surface, no inner cell borders, only subtle row dividers
  1831. • rounded pill badges for protocol / tag / addresses
  1832. • dual-arrow traffic widget that aligns across rows
  1833. • consistent hover/loading/result states
  1834. Scoped under .xray-page .outbounds-modern so it doesn't bleed into other tables. */
  1835. .xray-page .outbounds-modern { width: 100%; }
  1836. .xray-page .outbounds-toolbar-right { text-align: right; }
  1837. /* Table chrome */
  1838. .xray-page .outbounds-table .ant-table {
  1839. background: transparent;
  1840. border-radius: 14px;
  1841. overflow: hidden;
  1842. }
  1843. .xray-page .outbounds-table .ant-table-thead > tr > th {
  1844. background: rgba(255, 255, 255, 0.025);
  1845. color: rgba(255, 255, 255, 0.55);
  1846. font-weight: 500;
  1847. font-size: 12px;
  1848. letter-spacing: 0.04em;
  1849. text-transform: uppercase;
  1850. white-space: nowrap;
  1851. border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  1852. padding: 14px 18px;
  1853. }
  1854. .light .xray-page .outbounds-table .ant-table-thead > tr > th {
  1855. background: rgba(0, 0, 0, 0.02);
  1856. color: rgba(0, 0, 0, 0.55);
  1857. border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  1858. }
  1859. .xray-page .outbounds-table .ant-table-tbody > tr > td {
  1860. border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  1861. padding: 16px 18px;
  1862. transition: background-color 0.15s ease;
  1863. vertical-align: middle;
  1864. }
  1865. /* Force every cell to honour its column width — long content (especially
  1866. long tags) must clip via cell-level ellipsis instead of pushing the row
  1867. taller. */
  1868. .xray-page .outbounds-table .ant-table-tbody > tr > td,
  1869. .xray-page .outbounds-table .ant-table-thead > tr > th {
  1870. overflow: hidden;
  1871. }
  1872. .light .xray-page .outbounds-table .ant-table-tbody > tr > td {
  1873. border-bottom: 1px solid rgba(0, 0, 0, 0.04);
  1874. }
  1875. .xray-page .outbounds-table .ant-table-tbody > tr:last-child > td {
  1876. border-bottom: none;
  1877. }
  1878. .xray-page .outbounds-table .ant-table-tbody > tr:hover > td {
  1879. background: rgba(255, 255, 255, 0.035) !important;
  1880. }
  1881. .light .xray-page .outbounds-table .ant-table-tbody > tr:hover > td {
  1882. background: rgba(0, 0, 0, 0.025) !important;
  1883. }
  1884. /* Index + actions column */
  1885. .xray-page .outbound-action-cell {
  1886. display: inline-flex;
  1887. align-items: center;
  1888. gap: 8px;
  1889. }
  1890. .xray-page .outbound-index {
  1891. font-weight: 600;
  1892. color: rgba(255, 255, 255, 0.7);
  1893. font-variant-numeric: tabular-nums;
  1894. min-width: 18px;
  1895. text-align: end;
  1896. }
  1897. .light .xray-page .outbound-index { color: rgba(0, 0, 0, 0.7); }
  1898. .xray-page .outbound-action-btn {
  1899. border: none;
  1900. background: rgba(255, 255, 255, 0.05);
  1901. color: rgba(255, 255, 255, 0.75);
  1902. transition: background 0.15s ease;
  1903. }
  1904. .xray-page .outbound-action-btn:hover {
  1905. background: rgba(255, 255, 255, 0.12);
  1906. color: #fff;
  1907. }
  1908. .light .xray-page .outbound-action-btn {
  1909. background: rgba(0, 0, 0, 0.05);
  1910. color: rgba(0, 0, 0, 0.75);
  1911. }
  1912. .light .xray-page .outbound-action-btn:hover {
  1913. background: rgba(0, 0, 0, 0.1);
  1914. color: #000;
  1915. }
  1916. /* Identity cell — tag on top, protocol/network/security pills underneath.
  1917. Combining the two columns lets the table fit common viewports without
  1918. a horizontal scrollbar. */
  1919. .xray-page .outbound-identity-cell {
  1920. display: flex;
  1921. flex-direction: column;
  1922. gap: 8px;
  1923. min-width: 0;
  1924. }
  1925. /* Tag — inherits the table's font for visual parity, single line with
  1926. ellipsis on overflow. A long tag (e.g. "vless_jphttp-ksjpnggl") would
  1927. otherwise wrap and inflate the row's height; the inline tooltip surfaces
  1928. the full value on hover. */
  1929. .xray-page .outbound-tag {
  1930. font-size: 13px;
  1931. color: rgba(255, 255, 255, 0.92);
  1932. font-weight: 500;
  1933. display: block;
  1934. max-width: 100%;
  1935. white-space: nowrap;
  1936. overflow: hidden;
  1937. text-overflow: ellipsis;
  1938. }
  1939. .light .xray-page .outbound-tag { color: rgba(0, 0, 0, 0.85); }
  1940. /* Address pills (monospace, monoline) */
  1941. .xray-page .outbound-address-list {
  1942. display: flex;
  1943. flex-wrap: wrap;
  1944. gap: 6px;
  1945. align-items: center;
  1946. }
  1947. .xray-page .outbound-address-pill {
  1948. font-family: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, monospace;
  1949. font-size: 12px;
  1950. padding: 3px 10px;
  1951. border-radius: 8px;
  1952. background: rgba(255, 255, 255, 0.045);
  1953. color: rgba(255, 255, 255, 0.78);
  1954. line-height: 1.5;
  1955. border: 1px solid rgba(255, 255, 255, 0.06);
  1956. display: inline-block;
  1957. max-width: 240px;
  1958. overflow: hidden;
  1959. text-overflow: ellipsis;
  1960. white-space: nowrap;
  1961. vertical-align: middle;
  1962. }
  1963. .light .xray-page .outbound-address-pill {
  1964. background: rgba(0, 0, 0, 0.035);
  1965. color: rgba(0, 0, 0, 0.78);
  1966. border: 1px solid rgba(0, 0, 0, 0.06);
  1967. }
  1968. .xray-page .outbound-address-empty {
  1969. color: rgba(255, 255, 255, 0.3);
  1970. font-style: italic;
  1971. }
  1972. /* Protocol/network/tls pills — shared "outbound-pill" with tonal modifiers.
  1973. The pill row stays on a single line; if the column is somehow too narrow
  1974. for all pills it overflows out of view (rare — column width is sized to
  1975. fit the worst case) but never pushes the row taller. */
  1976. .xray-page .outbound-protocol-cell {
  1977. display: flex;
  1978. flex-wrap: nowrap;
  1979. gap: 6px;
  1980. align-items: center;
  1981. overflow: hidden;
  1982. }
  1983. .xray-page .outbound-pill {
  1984. display: inline-flex;
  1985. align-items: center;
  1986. min-height: 22px;
  1987. padding: 2px 9px;
  1988. border-radius: 11px;
  1989. font-size: 12px;
  1990. font-weight: 500;
  1991. line-height: 1.4;
  1992. letter-spacing: 0.01em;
  1993. border: 1px solid transparent;
  1994. white-space: nowrap;
  1995. flex: 0 0 auto;
  1996. }
  1997. /* Outbound pill tones: emerald = protocol, slate = network, violet = security.
  1998. tone-emerald and tone-violet are also consumed by routing.html for the
  1999. outboundTag / balancerTag pills. */
  2000. .xray-page .outbound-pill.tone-emerald { background: rgba(0, 191, 165, 0.14); color: #4dd4be; border-color: rgba(0, 191, 165, 0.28); }
  2001. .xray-page .outbound-pill.tone-slate { background: rgba(160, 174, 192, 0.14); color: #b8c2d0; border-color: rgba(160, 174, 192, 0.26); }
  2002. .xray-page .outbound-pill.tone-violet { background: rgba(155, 89, 219, 0.16); color: #b489e8; border-color: rgba(155, 89, 219, 0.32); }
  2003. /* Traffic — dual arrow widget, fixed columns so all rows align */
  2004. .xray-page .outbound-traffic-cell {
  2005. display: inline-grid;
  2006. grid-template-columns: 1fr auto 1fr;
  2007. align-items: center;
  2008. gap: 10px;
  2009. padding: 5px 12px;
  2010. border-radius: 100px;
  2011. background: rgba(255, 255, 255, 0.04);
  2012. font-variant-numeric: tabular-nums;
  2013. font-size: 13px;
  2014. min-width: 0;
  2015. }
  2016. .light .xray-page .outbound-traffic-cell {
  2017. background: rgba(0, 0, 0, 0.035);
  2018. }
  2019. .xray-page .outbound-traffic-up,
  2020. .xray-page .outbound-traffic-down {
  2021. display: inline-flex;
  2022. align-items: center;
  2023. gap: 6px;
  2024. white-space: nowrap;
  2025. }
  2026. .xray-page .outbound-traffic-up { justify-content: flex-end; color: #4dd4be; }
  2027. .xray-page .outbound-traffic-down { justify-content: flex-start; color: #82a7ee; }
  2028. .xray-page .outbound-traffic-up .anticon,
  2029. .xray-page .outbound-traffic-down .anticon { font-size: 11px; }
  2030. .xray-page .outbound-traffic-sep {
  2031. width: 1px;
  2032. height: 14px;
  2033. background: rgba(255, 255, 255, 0.12);
  2034. border-radius: 1px;
  2035. }
  2036. .light .xray-page .outbound-traffic-sep { background: rgba(0, 0, 0, 0.12); }
  2037. /* Test result pills */
  2038. .xray-page .outbound-result-cell { display: inline-flex; }
  2039. .xray-page .outbound-result-pill {
  2040. display: inline-flex;
  2041. align-items: center;
  2042. gap: 6px;
  2043. padding: 4px 10px;
  2044. border-radius: 100px;
  2045. font-size: 12px;
  2046. font-weight: 500;
  2047. font-variant-numeric: tabular-nums;
  2048. white-space: nowrap;
  2049. border: 1px solid transparent;
  2050. }
  2051. .xray-page .outbound-result-pill .anticon { font-size: 12px; }
  2052. .xray-page .outbound-result-ok {
  2053. background: rgba(0, 191, 165, 0.14);
  2054. color: #4dd4be;
  2055. border-color: rgba(0, 191, 165, 0.28);
  2056. }
  2057. .xray-page .outbound-result-fail {
  2058. background: rgba(255, 77, 79, 0.14);
  2059. color: #ff7a7c;
  2060. border-color: rgba(255, 77, 79, 0.32);
  2061. }
  2062. .xray-page .outbound-result-status { opacity: 0.75; }
  2063. .xray-page .outbound-result-loading,
  2064. .xray-page .outbound-result-idle {
  2065. color: rgba(255, 255, 255, 0.4);
  2066. font-size: 13px;
  2067. }
  2068. .light .xray-page .outbound-result-loading,
  2069. .light .xray-page .outbound-result-idle { color: rgba(0, 0, 0, 0.4); }
  2070. /* Test button — sleek circular with subtle glow */
  2071. .xray-page .outbound-test-btn {
  2072. box-shadow: 0 2px 8px rgba(0, 191, 165, 0.18);
  2073. transition: transform 0.12s ease, box-shadow 0.18s ease;
  2074. }
  2075. .xray-page .outbound-test-btn:hover:not([disabled]) {
  2076. transform: translateY(-1px);
  2077. box-shadow: 0 4px 14px rgba(0, 191, 165, 0.32);
  2078. }
  2079. .xray-page .outbound-test-btn[disabled] {
  2080. box-shadow: none;
  2081. opacity: 0.45;
  2082. }
  2083. /* ───────── Modern routing-rules table ─────────
  2084. Reuses the .outbound-pill tonal primitive (identical visual) so the
  2085. routing tab feels like the same panel as outbounds. Each cell groups
  2086. a routing criterion (Source / Network / Destination / Inbound) and
  2087. shows its values as labelled pills. */
  2088. .xray-page .routing-modern { width: 100%; }
  2089. .xray-page .routing-table .ant-table {
  2090. background: transparent;
  2091. border-radius: 14px;
  2092. overflow: hidden;
  2093. }
  2094. .xray-page .routing-table .ant-table-thead > tr > th {
  2095. background: rgba(255, 255, 255, 0.025);
  2096. color: rgba(255, 255, 255, 0.55);
  2097. font-weight: 500;
  2098. font-size: 12px;
  2099. letter-spacing: 0.04em;
  2100. text-transform: uppercase;
  2101. white-space: nowrap;
  2102. border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  2103. padding: 14px 18px;
  2104. }
  2105. .light .xray-page .routing-table .ant-table-thead > tr > th {
  2106. background: rgba(0, 0, 0, 0.02);
  2107. color: rgba(0, 0, 0, 0.55);
  2108. border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  2109. }
  2110. .xray-page .routing-table .ant-table-tbody > tr > td {
  2111. border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  2112. padding: 16px 18px;
  2113. transition: background-color 0.15s ease;
  2114. vertical-align: top;
  2115. }
  2116. .light .xray-page .routing-table .ant-table-tbody > tr > td {
  2117. border-bottom: 1px solid rgba(0, 0, 0, 0.04);
  2118. }
  2119. .xray-page .routing-table .ant-table-tbody > tr:last-child > td {
  2120. border-bottom: none;
  2121. }
  2122. .xray-page .routing-table .ant-table-tbody > tr:hover > td {
  2123. background: rgba(255, 255, 255, 0.035) !important;
  2124. }
  2125. .light .xray-page .routing-table .ant-table-tbody > tr:hover > td {
  2126. background: rgba(0, 0, 0, 0.025) !important;
  2127. }
  2128. /* Sort handle / # / actions */
  2129. .xray-page .routing-action-cell {
  2130. display: inline-flex;
  2131. align-items: center;
  2132. gap: 8px;
  2133. }
  2134. .xray-page .routing-index {
  2135. font-weight: 600;
  2136. color: rgba(255, 255, 255, 0.7);
  2137. font-variant-numeric: tabular-nums;
  2138. min-width: 18px;
  2139. text-align: end;
  2140. }
  2141. .light .xray-page .routing-index { color: rgba(0, 0, 0, 0.7); }
  2142. .xray-page .routing-action-btn {
  2143. border: none;
  2144. background: rgba(255, 255, 255, 0.05);
  2145. color: rgba(255, 255, 255, 0.75);
  2146. transition: background 0.15s ease;
  2147. }
  2148. .xray-page .routing-action-btn:hover {
  2149. background: rgba(255, 255, 255, 0.12);
  2150. color: #fff;
  2151. }
  2152. .light .xray-page .routing-action-btn {
  2153. background: rgba(0, 0, 0, 0.05);
  2154. color: rgba(0, 0, 0, 0.75);
  2155. }
  2156. .light .xray-page .routing-action-btn:hover {
  2157. background: rgba(0, 0, 0, 0.1);
  2158. color: #000;
  2159. }
  2160. /* Plain-text criterion rows — replaces pill primitives in condition
  2161. columns. Each criterion is a row of "label value (+N)" with form-label
  2162. styling on the label. No bg, no border, no color tones — keeps cells
  2163. light and lets the column header carry the type semantic. The cell's
  2164. visual weight is now proportional only to the data length, not to
  2165. decoration. The single colored pill in Outbound/Balancer remains as
  2166. the row's focal point. */
  2167. .xray-page .criterion-flow {
  2168. display: flex;
  2169. flex-direction: column;
  2170. gap: 3px;
  2171. min-width: 0;
  2172. }
  2173. .xray-page .criterion-row {
  2174. display: flex;
  2175. align-items: baseline;
  2176. gap: 8px;
  2177. min-width: 0;
  2178. font-size: 13px;
  2179. line-height: 1.5;
  2180. }
  2181. .xray-page .criterion-label {
  2182. flex: 0 0 auto;
  2183. font-size: 11px;
  2184. color: rgba(255, 255, 255, 0.42);
  2185. font-weight: 400;
  2186. letter-spacing: 0;
  2187. text-transform: none;
  2188. }
  2189. .light .xray-page .criterion-label { color: rgba(0, 0, 0, 0.45); }
  2190. .xray-page .criterion-value {
  2191. flex: 1 1 auto;
  2192. min-width: 0;
  2193. overflow: hidden;
  2194. text-overflow: ellipsis;
  2195. white-space: nowrap;
  2196. color: rgba(255, 255, 255, 0.85);
  2197. }
  2198. .light .xray-page .criterion-value { color: rgba(0, 0, 0, 0.85); }
  2199. .xray-page .criterion-more {
  2200. flex: 0 0 auto;
  2201. font-size: 11px;
  2202. color: rgba(255, 255, 255, 0.42);
  2203. font-weight: 500;
  2204. }
  2205. .light .xray-page .criterion-more { color: rgba(0, 0, 0, 0.45); }
  2206. .xray-page .routing-criteria-empty {
  2207. color: rgba(255, 255, 255, 0.3);
  2208. font-style: italic;
  2209. }
  2210. .light .xray-page .routing-criteria-empty { color: rgba(0, 0, 0, 0.3); }
  2211. /* Target cell (outbound / balancer) — vertically stacked rows of icon + pill */
  2212. .xray-page .routing-target-cell {
  2213. display: flex;
  2214. flex-direction: column;
  2215. gap: 4px;
  2216. min-width: 0;
  2217. }
  2218. .xray-page .routing-target-row {
  2219. display: inline-flex;
  2220. align-items: center;
  2221. gap: 8px;
  2222. }
  2223. .xray-page .routing-target-icon {
  2224. color: rgba(255, 255, 255, 0.45);
  2225. font-size: 13px;
  2226. }
  2227. .light .xray-page .routing-target-icon { color: rgba(0, 0, 0, 0.45); }
  2228. </style>
  2229. {{ template "page/body_end" .}}