xray.html 78 KB

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