├── .github └── workflows │ └── build-release.yml ├── .gitignore ├── LICENSE ├── README.md ├── core ├── Makefile └── root │ ├── etc │ ├── config │ │ └── xray_core │ ├── hotplug.d │ │ └── iface │ │ │ └── 01-transparent-proxy-ipset │ ├── init.d │ │ └── xray_core │ ├── ssl │ │ └── certs │ │ │ └── origin_ca_ecc_root.pem │ └── uci-defaults │ │ └── xray_core │ ├── usr │ ├── libexec │ │ └── rpcd │ │ │ └── xray │ └── share │ │ ├── luci │ │ └── menu.d │ │ │ └── luci-app-xray.json │ │ ├── nftables.d │ │ └── table-pre │ │ │ └── xray_core.nft │ │ ├── rpcd │ │ └── acl.d │ │ │ └── luci-app-xray.json │ │ └── xray │ │ ├── common │ │ ├── config.mjs │ │ ├── stream.mjs │ │ └── tls.mjs │ │ ├── default_gateway.uc │ │ ├── dnsmasq_include.ut │ │ ├── feature │ │ ├── bridge.mjs │ │ ├── dns.mjs │ │ ├── fake_dns.mjs │ │ ├── inbound.mjs │ │ ├── manual_tproxy.mjs │ │ ├── outbound.mjs │ │ └── system.mjs │ │ ├── firewall_include.ut │ │ ├── gen_config.uc │ │ ├── geoip_list.pb │ │ ├── ignore_tp_spec_def_gw │ │ ├── protocol │ │ ├── http.mjs │ │ ├── shadowsocks.mjs │ │ ├── socks.mjs │ │ ├── trojan.mjs │ │ ├── vless.mjs │ │ └── vmess.mjs │ │ ├── restart_dnsmasq_on_iface_change │ │ ├── rlimit_data_large │ │ ├── rlimit_data_small │ │ └── rlimit_nofile_large │ └── www │ └── luci-static │ └── resources │ └── view │ └── xray │ ├── core.js │ ├── preview.js │ ├── protocol.js │ ├── shared.js │ └── transport.js ├── geodata ├── Makefile └── root │ ├── usr │ └── share │ │ ├── luci │ │ └── menu.d │ │ │ └── luci-app-xray-geodata.json │ │ └── rpcd │ │ └── acl.d │ │ └── luci-app-xray-geodata.json │ └── www │ └── luci-static │ └── resources │ └── view │ └── xray │ └── geodata.js └── status ├── Makefile └── root ├── usr └── share │ ├── luci │ └── menu.d │ │ └── luci-app-xray-status.json │ └── rpcd │ └── acl.d │ └── luci-app-xray-status.json └── www └── luci-static └── resources └── view └── xray └── status.js /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | workflow_dispatch: 8 | 9 | env: 10 | PACKAGE_NAME: luci-app-xray 11 | CACHE_DIR: ~/cache 12 | 13 | jobs: 14 | release: 15 | name: Build for ${{ matrix.arch }} 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | include: 21 | - arch: x86_64 22 | sdk_url_path: https://downloads.openwrt.org/releases/23.05.5/targets/x86/64 23 | sdk_name: -sdk-23.05.5-x86-64_ 24 | 25 | env: 26 | SDK_URL_PATH: ${{ matrix.sdk_url_path }} 27 | SDK_NAME: ${{ matrix.sdk_name }} 28 | CCACHE_DIR: ~/.ccache 29 | CONFIG_CCACHE: y 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | 34 | - name: Prepare Cache Key 35 | id: cache_key 36 | run: echo "::set-output name=timestamp::$(date +"%s")" 37 | 38 | - name: Setup Cache 39 | uses: actions/cache@v4 40 | with: 41 | path: | 42 | ${{ env.CACHE_DIR }} 43 | ${{ env.CCACHE_DIR }} 44 | key: openwrt-${{ matrix.arch }}-${{ env.PACKAGE_NAME }}-${{ steps.cache_key.outputs.timestamp }} 45 | restore-keys: | 46 | openwrt-${{ matrix.arch }}-${{ env.PACKAGE_NAME }}- 47 | - name: Install Dependencies 48 | run: | 49 | DEBIAN_FRONTEND=noninteractive sudo apt-get install -y ccache gettext libncurses5-dev xsltproc p7zip-full 50 | - name: Create Directories 51 | run: | 52 | CACHE_DIR_SDK="$(eval echo "$CACHE_DIR/sdk")" 53 | CACHE_DIR_DL="$(eval echo "$CACHE_DIR/dl")" 54 | CACHE_DIR_FEEDS="$(eval echo "$CACHE_DIR/feeds")" 55 | echo "CACHE_DIR_SDK: $CACHE_DIR_SDK" 56 | echo "CACHE_DIR_DL: $CACHE_DIR_DL" 57 | echo "CACHE_DIR_FEEDS: $CACHE_DIR_FEEDS" 58 | test -d "$CACHE_DIR_SDK" || mkdir -p "$CACHE_DIR_SDK" 59 | test -d "$CACHE_DIR_DL" || mkdir -p "$CACHE_DIR_DL" 60 | test -d "$CACHE_DIR_FEEDS" || mkdir -p "$CACHE_DIR_FEEDS" 61 | echo "CACHE_DIR_SDK=$CACHE_DIR_SDK" >> $GITHUB_ENV 62 | echo "CACHE_DIR_DL=$CACHE_DIR_DL" >> $GITHUB_ENV 63 | echo "CACHE_DIR_FEEDS=$CACHE_DIR_FEEDS" >> $GITHUB_ENV 64 | echo "SDK_HOME=$(mktemp -d)" >> $GITHUB_ENV 65 | - name: Download and Unzip SDK 66 | run: | 67 | cd "$CACHE_DIR_SDK" 68 | if ! ( wget -q -O - "$SDK_URL_PATH/sha256sums" | grep -- "$SDK_NAME" > sha256sums.small 2>/dev/null ) ; then 69 | echo "::error::Can not find ${SDK_NAME} file in sha256sums." 70 | exit 1 71 | fi 72 | SDK_FILE="$(cat sha256sums.small | cut -d' ' -f2 | sed 's/*//g')" 73 | if ! sha256sum -c ./sha256sums.small >/dev/null 2>&1 ; then 74 | wget -q -O "$SDK_FILE" "$SDK_URL_PATH/$SDK_FILE" 75 | if ! sha256sum -c ./sha256sums.small >/dev/null 2>&1 ; then 76 | echo "::error::SDK can not be verified!" 77 | exit 1 78 | fi 79 | fi 80 | cd - 81 | file "$CACHE_DIR_SDK/$SDK_FILE" 82 | 7z x "$CACHE_DIR_SDK/$SDK_FILE" -so | tar -C "$SDK_HOME" -xvf - --strip=1 83 | cd "$SDK_HOME" 84 | test -d "dl" && rm -rf "dl" || true 85 | test -d "feeds" && rm -rf "feeds" || true 86 | ln -s "$CACHE_DIR_DL" "dl" 87 | ln -s "$CACHE_DIR_FEEDS" "feeds" 88 | cp feeds.conf.default feeds.conf 89 | sed -i 's#git.openwrt.org/openwrt/openwrt#github.com/openwrt/openwrt#' feeds.conf 90 | sed -i 's#git.openwrt.org/feed/packages#github.com/openwrt/packages#' feeds.conf 91 | sed -i 's#git.openwrt.org/project/luci#github.com/openwrt/luci#' feeds.conf 92 | sed -i 's#git.openwrt.org/feed/telephony#github.com/openwrt/telephony#' feeds.conf 93 | cd - 94 | - name: Update and Install Packages 95 | run: | 96 | cd "$SDK_HOME" 97 | ./scripts/feeds update -a 98 | ln -s "${{ github.workspace }}" "package/$PACKAGE_NAME" 99 | ./scripts/feeds install -a 100 | cd - 101 | - name: Build Packages 102 | run: | 103 | cd "$SDK_HOME" 104 | make defconfig 105 | make package/${PACKAGE_NAME}/status/{clean,compile} V=s 106 | find "$SDK_HOME/bin/" -type f -name "*.ipk" -exec ls -lh {} \; 107 | cd - 108 | - name: Copy Bin Files 109 | run: | 110 | find "$SDK_HOME/bin/" -type f -name "${PACKAGE_NAME}*.ipk" -exec cp {} "${{ github.workspace }}" \; 111 | find "${{ github.workspace }}" -type f -name "*.ipk" -exec ls -lh {} \; 112 | - name: Release and Upload Assets 113 | uses: softprops/action-gh-release@v1 114 | with: 115 | files: "*.ipk" 116 | env: 117 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 118 | 119 | notify: 120 | name: Notify Release Publish 121 | runs-on: ubuntu-latest 122 | env: 123 | TRIGGER_URL: ${{ secrets.TRIGGER_URL }} 124 | TRIGGER_TOKEN: ${{ secrets.TRIGGER_TOKEN }} 125 | needs: release 126 | steps: 127 | - name: Notify Jenkins 128 | run: | 129 | if [ -z "$TRIGGER_URL" ] ; then 130 | echo "::warning::No trigger url found, skip..." 131 | exit 0 132 | fi 133 | curl -X POST \ 134 | -H "Content-Type: application/json; charset=utf-8" \ 135 | -H "Authorization: Bearer $TRIGGER_TOKEN" \ 136 | -d "{\"event\":\"release\",\"repository\":\"${{ github.repository }}\",\"ref\":\"${{ github.ref }}\"}" \ 137 | "$TRIGGER_URL" || true 138 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # luci-app-xray 2 | 3 | Focus on making the most of Xray (HTTP/HTTPS/Socks/TProxy inbounds, multiple protocols support, DNS server, bridge (reverse proxy), even HTTPS proxy server for actual HTTP services) while keeping thin and elegant. 4 | 5 | ## Warnings 6 | 7 | * For OpenWrt 24.10 and SNAPSHOT users, dnsmasq integration options need to be changed. See the last comment in [#425](https://github.com/yichya/luci-app-xray/issues/425#issuecomment-2494295834) for details. 8 | * For security concerns, global SOCKS / HTTP inbound (listen on 0.0.0.0, port 1080 / 1081 by default) is deprecated and will be removed in next major version (4.0.0). 9 | * These settings are moved to preview app 10 | * Use Extra Inbound to manually add ports (avoid using common ports like 1080, also set listen addresses carefully) and adjust related workloads to use that. 11 | * Since version 3.2.0 sniffing and global custom settings are deprecated. 12 | * These settings are moved to preview app. See below for details to enable preview app. 13 | * Global custom settings will be removed in version 4.0.0. Use "Custom Configuration Hook" for global custom settings. 14 | * Sniffing might get completely reimplemented later. Use FakeDNS instead of sniffing to avoid incompatibilities. 15 | * This project **DOES NOT SUPPORT** the following versions of OpenWrt because of the requirements of firewall4 and cilent-side rendering LuCI: 16 | * LEDE / OpenWrt prior to 22.03 17 | * [Lean's OpenWrt Source](https://github.com/coolsnowwolf/lede) (which uses a variant of LuCI shipped with OpenWrt 18.06) 18 | 19 | If this is your case, use Passwall or similar projects instead (you could find links in [XTLS/Xray-core](https://github.com/XTLS/Xray-core/)). 20 | * About experimental REALITY support 21 | * it may change quite frequently (before the release of official documents about the protocol). Keep in mind for (maybe) breaking changes. 22 | * If you see `WARNING: at least one of asset files (geoip.dat, geosite.dat) is not found under /usr/share/xray. Xray may not work properly` and don't know what to do: 23 | * try `opkg update && opkg install v2ray-geoip v2ray-geosite` 24 | * if that doesn't work, see [#52](https://github.com/yichya/luci-app-xray/issues/52#issuecomment-856059905) 25 | * This project may change its code structure, configuration files format, user interface or dependencies quite frequently since it is still in its very early stage. 26 | 27 | ## Installation (Manually building OpenWrt) 28 | 29 | Choose one below: 30 | 31 | * Add `src-git-full luci_app_xray https://github.com/yichya/luci-app-xray` to `feeds.conf.default` and run `./scripts/feeds update -a; ./scripts/feeds install -a` 32 | * Clone this repository under `package` 33 | 34 | Then find `luci-app-xray` under `Extra Packages`. 35 | 36 | ## Installation (Use GitHub actions to build ipks) 37 | 38 | Fork this repository and: 39 | 40 | * Create a release by pushing a tag 41 | * Wait until actions finish 42 | * Use `opkg -i *` to install both ipks from Releases. 43 | 44 | ## Enable preview app 45 | 46 | Some features are deprecated / unstable so they are placed in preview app. To enable preview app: 47 | 48 | * Select `Preview or Deprecated` in `Extra Settings` tab 49 | * Reboot your router 50 | * There will be a new menu option `Xray (preview)` in `Services` 51 | 52 | ## Changelog since 3.6.0 53 | 54 | * 2025-05-13 feat: geodata reader 55 | 56 | ## Changelog since 3.5.0 57 | 58 | * 2024-11-26 chore: bump status version 59 | * 2024-11-27 chore: use OpenWrt SDK 23.05.5 to avoid using apk 60 | * 2025-01-05 fix: direct output; remove unused check of geoip direct list 61 | 62 | ## Changelog since 3.4.0 63 | 64 | * 2024-02-18 chore: optimize code style; bump version 65 | * 2024-02-19 fix: several DNS related validation 66 | * 2024-02-20 fix: domain match priority; stricter resolve options; socks / http auth 67 | * 2024-02-23 chore: bump version to 3.4.1 68 | * 2024-02-27 fix: block requests when leastPing not ready 69 | * 2024-03-08 feat: multiple ports for one outbound 70 | * 2024-03-11 feat: expectIPs for outbound domain resolving; restart on dnsmasq config change 71 | * 2024-03-14 feat: add version info display in status 72 | * 2024-03-17 fix: add ACL for version info 73 | * 2024-04-23 fix: use zstd for OpenWrt SDK decompression 74 | * 2024-05-03 fix: reality deserialization issue 75 | * 2024-06-26 feat: support ext in geoip direct codes 76 | * 2024-07-26 chore: minor code and style fixes 77 | * 2024-11-22 fix: dnsmasq jail adaptation; firewall improvements 78 | * 2024-11-25 feat: basic splithttp support 79 | * 2024-11-26 feat: httpupgrade support; avoid overriding mark 80 | 81 | ## Changelog since 3.3.0 82 | 83 | * 2024-01-19 chore: bump version 84 | * 2024-01-24 feat: add alias to LAN Hosts Access Control 85 | * 2024-02-04 fix: avoid firewall restart failure & some minor adjustments 86 | * 2024-02-16 feat: dns hijacking preview; deprecate global http / socks inbound 87 | * 2024-02-17 feat: add username / password for extra socks / http inbound 88 | 89 | ## Changelog since 3.2.0 90 | 91 | * 2023-12-20 chore: bump version 92 | * 2023-12-22 chore: optimize list folded format; add roundRobin balancer 93 | * 2024-01-04 chore: start later than sysntpd; change firewall include file path 94 | * 2024-01-18 feat: make "Resolve Domain via DNS" available to all outbounds 95 | * 2024-01-19 feat: socks / http outbound 96 | 97 | ## Changelog since 3.1.0 98 | 99 | * 2023-10-24 chore: bump version 100 | * 2023-10-25 fix: set required for some fields; remove unused code 101 | * 2023-10-26 fix: allow empty selection for extra inbound outbound balancer 102 | * 2023-10-30 fix: blocked as nxdomain for IPv6 103 | * 2023-10-31 chore: bump version to 3.1.1 104 | * 2023-11-01 feat: custom configuration hook 105 | * 2023-11-02 feat: specify DNS to resolve outbound server name 106 | * 2023-11-30 fix: dialer proxy tag 107 | * 2023-12-14 fix: default gateway 108 | * 2023-12-20 chore: deprecate sniffing; move some preview features to main app; add custom configuration hook; refactor web files 109 | 110 | ## Changelog since 3.0.0 111 | 112 | * 2023-09-26 Version 3.0.0 merge master 113 | * 2023-09-27 fix: sniffing inboundTag; fix: upstream_domain_names 114 | * 2023-10-01 fix: default configuration 115 | * 2023-10-06 chore: code cleanup 116 | * 2023-10-19 feat: detailed status page via metrics 117 | * 2023-10-20 feat: better network interface control. **Requires reselection of LAN interfaces in** `Xray (preview)` -> `LAN Hosts Access Control` 118 | 119 | ## Star History 120 | 121 | [![Star History Chart](https://api.star-history.com/svg?repos=yichya/luci-app-xray&type=Date)](https://star-history.com/#yichya/luci-app-xray&Date) 122 | -------------------------------------------------------------------------------- /core/Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | PKG_NAME:=luci-app-xray 4 | PKG_VERSION:=3.6.0 5 | PKG_RELEASE:=1 6 | 7 | PKG_LICENSE:=MPLv2 8 | PKG_LICENSE_FILES:=LICENSE 9 | PKG_MAINTAINER:=yichya 10 | PKG_BUILD_PARALLEL:=1 11 | 12 | include $(INCLUDE_DIR)/package.mk 13 | 14 | define Package/$(PKG_NAME) 15 | SECTION:=Custom 16 | CATEGORY:=Extra packages 17 | TITLE:=LuCI Support for Xray 18 | DEPENDS:=firewall4 +kmod-nft-tproxy +luci-base +xray-core +dnsmasq +ca-bundle 19 | PKGARCH:=all 20 | endef 21 | 22 | define Package/$(PKG_NAME)/description 23 | LuCI Support for Xray (Client-side Rendered). 24 | endef 25 | 26 | define Package/$(PKG_NAME)/config 27 | menu "luci-app-xray Configuration" 28 | depends on PACKAGE_$(PKG_NAME) 29 | 30 | config PACKAGE_XRAY_INCLUDE_CLOUDFLARE_ORIGIN_ROOT_CA 31 | bool "Include Cloudflare Origin Root CA" 32 | default n 33 | 34 | config PACKAGE_XRAY_RLIMIT_NOFILE_LARGE 35 | bool "Increase Max Open Files Limit (recommended)" 36 | default y 37 | 38 | config PACKAGE_XRAY_RESTART_DNSMASQ_ON_IFACE_CHANGE 39 | bool "Restart dnsmasq on interface change (select this if using dnsmasq v2.87)" 40 | default n 41 | 42 | config PACKAGE_XRAY_IGNORE_TP_SPEC_DEF_GW 43 | bool "Ignore TP_SPEC_DEF_GW (select this if using private IPv4 address)" 44 | default n 45 | 46 | choice 47 | prompt "Limit memory use by setting rlimit_data (experimental)" 48 | default PACKAGE_XRAY_RLIMIT_DATA_UNLIMITED 49 | config PACKAGE_XRAY_RLIMIT_DATA_UNLIMITED 50 | bool "Not limited" 51 | config PACKAGE_XRAY_RLIMIT_DATA_SMALL 52 | bool "Small limit (about 50MB)" 53 | config PACKAGE_XRAY_RLIMIT_DATA_LARGE 54 | bool "Large limit (about 321MB)" 55 | endchoice 56 | 57 | endmenu 58 | endef 59 | 60 | define Build/Compile 61 | endef 62 | 63 | define Package/$(PKG_NAME)/postinst 64 | #!/bin/sh 65 | if [[ -z "$${IPKG_INSTROOT}" ]]; then 66 | if [[ -f /etc/uci-defaults/xray_core ]]; then 67 | ( . /etc/uci-defaults/xray_core ) && rm -f /etc/uci-defaults/xray_core 68 | fi 69 | rm -rf /tmp/luci-indexcache* /tmp/luci-modulecache 70 | fi 71 | exit 0 72 | endef 73 | 74 | define Package/$(PKG_NAME)/conffiles 75 | /etc/config/xray_core 76 | endef 77 | 78 | define Package/$(PKG_NAME)/install 79 | $(INSTALL_DIR) $(1)/etc/luci-uploads/xray 80 | $(INSTALL_DIR) $(1)/etc/ssl/certs 81 | ifdef CONFIG_PACKAGE_XRAY_INCLUDE_CLOUDFLARE_ORIGIN_ROOT_CA 82 | $(INSTALL_DATA) ./root/etc/ssl/certs/origin_ca_ecc_root.pem $(1)/etc/ssl/certs/origin_ca_ecc_root.pem 83 | endif 84 | $(INSTALL_DIR) $(1)/etc/init.d 85 | $(INSTALL_BIN) ./root/etc/init.d/xray_core $(1)/etc/init.d/xray_core 86 | $(INSTALL_DIR) $(1)/etc/config 87 | $(INSTALL_DATA) ./root/etc/config/xray_core $(1)/etc/config/xray_core 88 | $(INSTALL_DIR) $(1)/etc/uci-defaults 89 | $(INSTALL_BIN) ./root/etc/uci-defaults/xray_core $(1)/etc/uci-defaults/xray_core 90 | $(INSTALL_DIR) $(1)/etc/hotplug.d/iface 91 | $(INSTALL_BIN) ./root/etc/hotplug.d/iface/01-transparent-proxy-ipset $(1)/etc/hotplug.d/iface/01-transparent-proxy-ipset 92 | $(INSTALL_DIR) $(1)/www/luci-static/resources/view/xray 93 | $(INSTALL_DATA) ./root/www/luci-static/resources/view/xray/core.js $(1)/www/luci-static/resources/view/xray/core.js 94 | $(INSTALL_DATA) ./root/www/luci-static/resources/view/xray/preview.js $(1)/www/luci-static/resources/view/xray/preview.js 95 | $(INSTALL_DATA) ./root/www/luci-static/resources/view/xray/protocol.js $(1)/www/luci-static/resources/view/xray/protocol.js 96 | $(INSTALL_DATA) ./root/www/luci-static/resources/view/xray/shared.js $(1)/www/luci-static/resources/view/xray/shared.js 97 | $(INSTALL_DATA) ./root/www/luci-static/resources/view/xray/transport.js $(1)/www/luci-static/resources/view/xray/transport.js 98 | $(INSTALL_DIR) $(1)/usr/libexec/rpcd 99 | $(INSTALL_BIN) ./root/usr/libexec/rpcd/xray $(1)/usr/libexec/rpcd/xray 100 | $(INSTALL_DIR) $(1)/usr/share/luci/menu.d 101 | $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-xray.json $(1)/usr/share/luci/menu.d/luci-app-xray.json 102 | $(INSTALL_DIR) $(1)/usr/share/nftables.d/table-pre 103 | $(INSTALL_DATA) ./root/usr/share/nftables.d/table-pre/xray_core.nft $(1)/usr/share/nftables.d/table-pre/xray_core.nft 104 | $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d 105 | $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-xray.json $(1)/usr/share/rpcd/acl.d/luci-app-xray.json 106 | $(INSTALL_DIR) $(1)/usr/share/xray 107 | $(LN) /var/run/xray.pid $(1)/usr/share/xray/xray.pid 108 | $(LN) /usr/bin/xray $(1)/usr/share/xray/xray 109 | ifdef CONFIG_PACKAGE_XRAY_IGNORE_TP_SPEC_DEF_GW 110 | $(INSTALL_DATA) ./root/usr/share/xray/ignore_tp_spec_def_gw $(1)/usr/share/xray/ignore_tp_spec_def_gw 111 | endif 112 | ifdef CONFIG_PACKAGE_XRAY_RESTART_DNSMASQ_ON_IFACE_CHANGE 113 | $(INSTALL_DATA) ./root/usr/share/xray/restart_dnsmasq_on_iface_change $(1)/usr/share/xray/restart_dnsmasq_on_iface_change 114 | endif 115 | ifdef CONFIG_PACKAGE_XRAY_RLIMIT_NOFILE_LARGE 116 | $(INSTALL_DATA) ./root/usr/share/xray/rlimit_nofile_large $(1)/usr/share/xray/rlimit_nofile 117 | endif 118 | ifdef CONFIG_PACKAGE_XRAY_RLIMIT_DATA_SMALL 119 | $(INSTALL_DATA) ./root/usr/share/xray/rlimit_data_small $(1)/usr/share/xray/rlimit_data 120 | endif 121 | ifdef CONFIG_PACKAGE_XRAY_RLIMIT_DATA_LARGE 122 | $(INSTALL_DATA) ./root/usr/share/xray/rlimit_data_large $(1)/usr/share/xray/rlimit_data 123 | endif 124 | $(INSTALL_BIN) ./root/usr/share/xray/default_gateway.uc $(1)/usr/share/xray/default_gateway.uc 125 | $(INSTALL_BIN) ./root/usr/share/xray/dnsmasq_include.ut $(1)/usr/share/xray/dnsmasq_include.ut 126 | $(INSTALL_BIN) ./root/usr/share/xray/firewall_include.ut $(1)/usr/share/xray/firewall_include.ut 127 | $(INSTALL_BIN) ./root/usr/share/xray/gen_config.uc $(1)/usr/share/xray/gen_config.uc 128 | $(INSTALL_DIR) $(1)/usr/share/xray/common 129 | $(INSTALL_DATA) ./root/usr/share/xray/common/config.mjs $(1)/usr/share/xray/common/config.mjs 130 | $(INSTALL_DATA) ./root/usr/share/xray/common/stream.mjs $(1)/usr/share/xray/common/stream.mjs 131 | $(INSTALL_DATA) ./root/usr/share/xray/common/tls.mjs $(1)/usr/share/xray/common/tls.mjs 132 | $(INSTALL_DIR) $(1)/usr/share/xray/feature 133 | $(INSTALL_DATA) ./root/usr/share/xray/feature/bridge.mjs $(1)/usr/share/xray/feature/bridge.mjs 134 | $(INSTALL_DATA) ./root/usr/share/xray/feature/dns.mjs $(1)/usr/share/xray/feature/dns.mjs 135 | $(INSTALL_DATA) ./root/usr/share/xray/feature/fake_dns.mjs $(1)/usr/share/xray/feature/fake_dns.mjs 136 | $(INSTALL_DATA) ./root/usr/share/xray/feature/inbound.mjs $(1)/usr/share/xray/feature/inbound.mjs 137 | $(INSTALL_DATA) ./root/usr/share/xray/feature/manual_tproxy.mjs $(1)/usr/share/xray/feature/manual_tproxy.mjs 138 | $(INSTALL_DATA) ./root/usr/share/xray/feature/outbound.mjs $(1)/usr/share/xray/feature/outbound.mjs 139 | $(INSTALL_DATA) ./root/usr/share/xray/feature/system.mjs $(1)/usr/share/xray/feature/system.mjs 140 | $(INSTALL_DIR) $(1)/usr/share/xray/protocol 141 | $(INSTALL_DATA) ./root/usr/share/xray/protocol/shadowsocks.mjs $(1)/usr/share/xray/protocol/shadowsocks.mjs 142 | $(INSTALL_DATA) ./root/usr/share/xray/protocol/trojan.mjs $(1)/usr/share/xray/protocol/trojan.mjs 143 | $(INSTALL_DATA) ./root/usr/share/xray/protocol/vless.mjs $(1)/usr/share/xray/protocol/vless.mjs 144 | $(INSTALL_DATA) ./root/usr/share/xray/protocol/vmess.mjs $(1)/usr/share/xray/protocol/vmess.mjs 145 | $(INSTALL_DATA) ./root/usr/share/xray/protocol/socks.mjs $(1)/usr/share/xray/protocol/socks.mjs 146 | $(INSTALL_DATA) ./root/usr/share/xray/protocol/http.mjs $(1)/usr/share/xray/protocol/http.mjs 147 | endef 148 | 149 | $(eval $(call BuildPackage,$(PKG_NAME))) 150 | -------------------------------------------------------------------------------- /core/root/etc/config/xray_core: -------------------------------------------------------------------------------- 1 | config general 2 | list blocked_domain_rules 'geosite:category-ads' 3 | list bypassed_domain_rules 'geosite:cn' 4 | list forwarded_domain_rules 'geosite:geolocation-!cn' 5 | list geoip_direct_code_list 'cn' 6 | list geoip_direct_code_list_v6 'cn' 7 | list tproxy_ifaces_v4 'br-lan' 8 | list tproxy_ifaces_v6 'br-lan' 9 | option transparent_proxy_enable '1' 10 | option xray_bin '/usr/bin/xray' 11 | -------------------------------------------------------------------------------- /core/root/etc/hotplug.d/iface/01-transparent-proxy-ipset: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | logger -st transparent-proxy-ipset[$$] -p6 "$(ucode /usr/share/xray/default_gateway.uc)" 3 | -------------------------------------------------------------------------------- /core/root/etc/init.d/xray_core: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=99 4 | STOP=15 5 | USE_PROCD=1 6 | NAME=xray_core 7 | 8 | setup_firewall() { 9 | ip rule add fwmark 251 lookup 251 10 | ip route add local default dev lo table 251 11 | ip -6 rule add fwmark 251 lookup 251 12 | ip -6 route add local default dev lo table 251 13 | 14 | logger -st xray[$$] -p4 "Generating firewall4 rules..." 15 | /usr/bin/utpl /usr/share/xray/firewall_include.ut > /var/etc/xray/01_firewall_include.nft 16 | 17 | logger -st xray[$$] -p4 "Triggering firewall4 restart..." 18 | /etc/init.d/firewall restart 19 | } 20 | 21 | flush_firewall() { 22 | ip rule del fwmark 251 lookup 251 23 | ip route del local default dev lo table 251 24 | ip -6 rule del fwmark 251 lookup 251 25 | ip -6 route del local default dev lo table 251 26 | 27 | logger -st xray[$$] -p4 "Flushing firewall4 rules..." 28 | rm -f /var/etc/xray/*.nft 29 | 30 | logger -st xray[$$] -p4 "Triggering firewall4 restart..." 31 | /etc/init.d/firewall restart 32 | } 33 | 34 | uci_get_by_type() { 35 | local ret=$(uci get ${NAME}.@$1[0].$2 2>/dev/null) 36 | echo ${ret:=$3} 37 | } 38 | 39 | log_procd_set_param() { 40 | local type="$1" 41 | shift 42 | logger -st xray[$$] -p4 "Using procd_set_param $type" "$@" 43 | } 44 | 45 | start_xray() { 46 | logger -st xray[$$] -p4 "Starting Xray from $1" 47 | procd_open_instance 48 | procd_set_param respawn 1 1 0 49 | procd_set_param command $1 50 | procd_append_param command run 51 | procd_append_param command -confdir 52 | procd_append_param command /var/etc/xray 53 | 54 | local rlimit_nofile 55 | if [ -s /usr/share/xray/rlimit_nofile ] ; then 56 | rlimit_nofile="nofile=""$(cat /usr/share/xray/rlimit_nofile)" 57 | fi 58 | 59 | local rlimit_data 60 | if [ -s /usr/share/xray/rlimit_data ] ; then 61 | rlimit_data="data=""$(cat /usr/share/xray/rlimit_data)" 62 | fi 63 | 64 | # this param passing method is just so fucking weird 65 | if [ -z "${rlimit_nofile}" ] ; then 66 | if [ ! -z "${rlimit_data}" ]; then 67 | log_procd_set_param limits "${rlimit_data}" 68 | procd_set_param limits "${rlimit_data}" 69 | fi 70 | else 71 | if [ -z "${rlimit_data}" ]; then 72 | log_procd_set_param limits "${rlimit_nofile}" 73 | procd_set_param limits "${rlimit_nofile}" 74 | else 75 | log_procd_set_param limits "${rlimit_data}" "${rlimit_nofile}" 76 | procd_set_param limits "${rlimit_data}" "${rlimit_nofile}" 77 | fi 78 | fi 79 | 80 | procd_set_param env XRAY_LOCATION_ASSET=/usr/share/xray 81 | procd_set_param stdout 1 82 | procd_set_param stderr 1 83 | procd_set_param file /etc/config/xray 84 | procd_set_param pidfile /var/run/xray.pid 85 | procd_close_instance 86 | } 87 | 88 | gen_config_file() { 89 | rm -f /etc/nftables.d/99-xray.nft /var/etc/xray/* 90 | for gen_config_file_attempt in 1 2 3 4 5; do 91 | logger -st xray[$$] -p4 "Generating Xray configuration files (attempt ${gen_config_file_attempt})..." 92 | /usr/bin/ucode /usr/share/xray/gen_config.uc > /var/etc/xray/config.json 93 | if [ -s /var/etc/xray/config.json ] ; then 94 | break 95 | else 96 | sleep 1 97 | fi 98 | done 99 | local custom_config=$(uci_get_by_type general custom_config) 100 | [ ! "${#custom_config}" == "0" ] && echo ${custom_config} > /var/etc/xray/config_custom.json 101 | } 102 | 103 | setup_dnsmasq_instance() { 104 | mkdir -p $1 105 | utpl /usr/share/xray/dnsmasq_include.ut > $1/xray.conf 106 | logger -st xray[$$] -p4 $(cat $1/xray.conf) 107 | } 108 | 109 | setup_dnsmasq() { 110 | if [ "$(uci_get_by_type general dnsmasq_integration_mode)" == "per_instance" ]; then 111 | for instance in $(uci_get_by_type general dnsmasq_instances); do 112 | setup_dnsmasq_instance /tmp/dnsmasq.${instance}.d 113 | done 114 | else 115 | setup_dnsmasq_instance /tmp/dnsmasq.d 116 | fi 117 | /etc/init.d/dnsmasq restart > /dev/null 2>&1 118 | } 119 | 120 | flush_dnsmasq() { 121 | rm -f /tmp/dnsmasq.d/xray.conf /tmp/dnsmasq.*.d/xray.conf 122 | /etc/init.d/dnsmasq restart > /dev/null 2>&1 123 | } 124 | 125 | create_when_enable() { 126 | [ "$(uci_get_by_type general transparent_proxy_enable)" == "1" ] || return 0 127 | logger -st xray[$$] -p4 "Setting dnsmasq and firewall for transparent proxy..." 128 | setup_dnsmasq 129 | setup_firewall 130 | logger -st xray[$$] -p4 $(ucode /usr/share/xray/default_gateway.uc) 131 | } 132 | 133 | flush_when_disable() { 134 | logger -st xray[$$] -p4 "Resetting dnsmasq and firewall configurations..." 135 | flush_dnsmasq 136 | flush_firewall 137 | } 138 | 139 | start_service() { 140 | config_load $NAME 141 | mkdir -p /var/run /var/etc/xray 142 | local xray_bin=$(uci_get_by_type general xray_bin) 143 | command -v ${xray_bin} > /dev/null 2>&1 || return 1 144 | gen_config_file 145 | start_xray ${xray_bin} 146 | create_when_enable || flush_when_disable 147 | } 148 | 149 | stop_service() { 150 | flush_when_disable 151 | } 152 | 153 | reload_service() { 154 | stop 155 | start 156 | } 157 | 158 | service_triggers() { 159 | procd_add_reload_trigger "xray_core" "dhcp" 160 | } 161 | -------------------------------------------------------------------------------- /core/root/etc/ssl/certs/origin_ca_ecc_root.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICiTCCAi6gAwIBAgIUXZP3MWb8MKwBE1Qbawsp1sfA/Y4wCgYIKoZIzj0EAwIw 3 | gY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T 4 | YW4gRnJhbmNpc2NvMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYDVQQL 5 | Ey9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhvcml0 6 | eTAeFw0xOTA4MjMyMTA4MDBaFw0yOTA4MTUxNzAwMDBaMIGPMQswCQYDVQQGEwJV 7 | UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEZ 8 | MBcGA1UEChMQQ2xvdWRGbGFyZSwgSW5jLjE4MDYGA1UECxMvQ2xvdWRGbGFyZSBP 9 | cmlnaW4gU1NMIEVDQyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwWTATBgcqhkjOPQIB 10 | BggqhkjOPQMBBwNCAASR+sGALuaGshnUbcxKry+0LEXZ4NY6JUAtSeA6g87K3jaA 11 | xpIg9G50PokpfWkhbarLfpcZu0UAoYy2su0EhN7wo2YwZDAOBgNVHQ8BAf8EBAMC 12 | AQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUhTBdOypw1O3VkmcH/es5 13 | tBoOOKcwHwYDVR0jBBgwFoAUhTBdOypw1O3VkmcH/es5tBoOOKcwCgYIKoZIzj0E 14 | AwIDSQAwRgIhAKilfntP2ILGZjwajktkBtXE1pB4Y/fjAfLkIRUzrI15AiEA5UCL 15 | XYZZ9m2c3fKwIenMMojL1eqydsgqj/wK4p5kagQ= 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /core/root/etc/uci-defaults/xray_core: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | uci get xray_core.@general[-1] >/dev/null 2>&1 || uci add xray_core general >/dev/null 2>&1 3 | uci commit xray_core 4 | uci -q batch <<-EOF >/dev/null 5 | delete ucitrack.@xray_core[-1] 6 | add ucitrack xray_core 7 | set ucitrack.@xray_core[-1].init=xray_core 8 | commit ucitrack 9 | EOF 10 | exit 0 11 | -------------------------------------------------------------------------------- /core/root/usr/libexec/rpcd/xray: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | main() { 4 | case "$1" in 5 | list) 6 | echo '{"statsquery":{},"statssys":{},"restartlogger":{}}' 7 | ;; 8 | call) 9 | shift 10 | xray api $@ 11 | esac 12 | } 13 | 14 | main "$@" 15 | -------------------------------------------------------------------------------- /core/root/usr/share/luci/menu.d/luci-app-xray.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin/services/xray_core": { 3 | "title": "Xray", 4 | "action": { 5 | "type": "view", 6 | "path": "xray/core" 7 | }, 8 | "depends": { 9 | "acl": [ 10 | "luci-app-xray" 11 | ], 12 | "uci": { 13 | "xray_core": true 14 | } 15 | } 16 | }, 17 | "admin/services/xray_preview": { 18 | "title": "Xray (preview)", 19 | "action": { 20 | "type": "view", 21 | "path": "xray/preview" 22 | }, 23 | "depends": { 24 | "acl": [ 25 | "luci-app-xray" 26 | ], 27 | "uci": { 28 | "xray_core": { 29 | "@general[0]": { 30 | "preview_or_deprecated": true 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /core/root/usr/share/nftables.d/table-pre/xray_core.nft: -------------------------------------------------------------------------------- 1 | include "/var/etc/xray/*.nft" 2 | -------------------------------------------------------------------------------- /core/root/usr/share/rpcd/acl.d/luci-app-xray.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-xray": { 3 | "description": "Grant access to xray configurations", 4 | "read": { 5 | "uci": [ 6 | "xray_core" 7 | ] 8 | }, 9 | "write": { 10 | "uci": [ 11 | "xray_core" 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/common/config.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { cursor } from "uci"; 4 | 5 | export function load_config() { 6 | const uci = cursor(); 7 | uci.load("xray_core"); 8 | return uci.get_all("xray_core") || {}; 9 | }; 10 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/common/stream.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { reality_outbound_settings, tls_outbound_settings } from "./tls.mjs"; 4 | 5 | function stream_tcp_fake_http_request(server) { 6 | if (server["tcp_guise"] == "http") { 7 | return { 8 | version: "1.1", 9 | method: "GET", 10 | path: server["http_path"], 11 | headers: { 12 | "Host": server["http_host"], 13 | "User-Agent": [ 14 | "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", 15 | "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46" 16 | ], 17 | "Accept-Encoding": ["gzip, deflate"], 18 | "Connection": ["keep-alive"], 19 | "Pragma": "no-cache" 20 | } 21 | }; 22 | } 23 | return null; 24 | } 25 | 26 | function stream_tcp_fake_http_response(server) { 27 | if (server["tcp_guise"] == "http") { 28 | return { 29 | version: "1.1", 30 | status: "200", 31 | reason: "OK", 32 | headers: { 33 | Content_Type: ["application/octet-stream", "video/mpeg"], 34 | Transfer_Encoding: ["chunked"], 35 | Connection: ["keep-alive"], 36 | Pragma: "no-cache" 37 | } 38 | }; 39 | } 40 | return null; 41 | } 42 | 43 | function stream_tcp(server) { 44 | if (server["transport"] == "tcp") { 45 | return { 46 | header: { 47 | type: server["tcp_guise"], 48 | request: stream_tcp_fake_http_request(server), 49 | response: stream_tcp_fake_http_response(server) 50 | } 51 | }; 52 | } 53 | return null; 54 | } 55 | 56 | function stream_h2(server) { 57 | if (server["transport"] == "h2") { 58 | return { 59 | path: server["h2_path"], 60 | host: server["h2_host"], 61 | read_idle_timeout: server["h2_health_check"] == "1" ? int(server["h2_read_idle_timeout"] || 10) : null, 62 | health_check_timeout: server["h2_health_check"] == "1" ? int(server["h2_health_check_timeout"] || 20) : null, 63 | }; 64 | } 65 | return null; 66 | } 67 | 68 | function stream_grpc(server) { 69 | if (server["transport"] == "grpc") { 70 | return { 71 | serviceName: server["grpc_service_name"], 72 | multiMode: server["grpc_multi_mode"] == "1", 73 | initial_windows_size: int(server["grpc_initial_windows_size"] || 0), 74 | idle_timeout: server["grpc_health_check"] == "1" ? int(server["grpc_idle_timeout"] || 10) : null, 75 | health_check_timeout: server["grpc_health_check"] == "1" ? int(server["grpc_health_check_timeout"] || 20) : null, 76 | permit_without_stream: server["grpc_health_check"] == "1" ? (server["grpc_permit_without_stream"] == "1") : null 77 | }; 78 | } 79 | return null; 80 | } 81 | 82 | function stream_ws(server) { 83 | if (server["transport"] == "ws") { 84 | let headers = null; 85 | if (server["ws_host"] != null) { 86 | headers = { 87 | Host: server["ws_host"] 88 | }; 89 | } 90 | return { 91 | path: server["ws_path"], 92 | headers: headers 93 | }; 94 | } 95 | return null; 96 | } 97 | 98 | function stream_kcp(server) { 99 | if (server["transport"] == "mkcp") { 100 | let mkcp_seed = null; 101 | if (server["mkcp_seed"] != "") { 102 | mkcp_seed = server["mkcp_seed"]; 103 | } 104 | return { 105 | mtu: int(server["mkcp_mtu"] || 1350), 106 | tti: int(server["mkcp_tti"] || 50), 107 | uplinkCapacity: int(server["mkcp_uplink_capacity"] || 5), 108 | downlinkCapacity: int(server["mkcp_downlink_capacity"] || 20), 109 | congestion: server["mkcp_congestion"] == "1", 110 | readBufferSize: int(server["mkcp_read_buffer_size"] || 2), 111 | writeBufferSize: int(server["mkcp_write_buffer_size"] || 2), 112 | seed: mkcp_seed, 113 | header: { 114 | type: server["mkcp_guise"] || "none" 115 | } 116 | }; 117 | } 118 | return null; 119 | } 120 | 121 | function stream_quic(server) { 122 | if (server["transport"] == "quic") { 123 | return { 124 | security: server["quic_security"], 125 | key: server["quic_key"], 126 | header: { 127 | type: server["quic_guise"] 128 | } 129 | }; 130 | } 131 | return null; 132 | } 133 | 134 | function stream_splithttp(server) { 135 | if (server["transport"] == "splithttp") { 136 | return { 137 | path: server["splithttp_path"], 138 | host: server["splithttp_host"], 139 | }; 140 | } 141 | return null; 142 | } 143 | 144 | function stream_httpupgrade(server) { 145 | if (server["transport"] == "httpupgrade") { 146 | return { 147 | path: server["httpupgrade_path"], 148 | host: server["httpupgrade_host"], 149 | }; 150 | } 151 | return null; 152 | } 153 | 154 | export function port_array(i) { 155 | if (type(i) === 'array') { 156 | return map(i, v => int(v)); 157 | } 158 | return [int(i)]; 159 | }; 160 | 161 | export function stream_settings(server, protocol, tag) { 162 | const security = server[protocol + "_tls"]; 163 | let tlsSettings = null; 164 | let realitySettings = null; 165 | if (security == "tls") { 166 | tlsSettings = tls_outbound_settings(server, protocol); 167 | } else if (security == "reality") { 168 | realitySettings = reality_outbound_settings(server, protocol); 169 | } 170 | 171 | let dialer_proxy = null; 172 | let dialer_proxy_tag = null; 173 | if (server["dialer_proxy"] != null && server["dialer_proxy"] != "disabled") { 174 | dialer_proxy = server["dialer_proxy"]; 175 | dialer_proxy_tag = tag + `@dialer_proxy:${dialer_proxy}`; 176 | } 177 | return { 178 | stream_settings: { 179 | network: server["transport"], 180 | sockopt: { 181 | domainStrategy: server["domain_strategy"] || "UseIP", 182 | dialerProxy: dialer_proxy_tag 183 | }, 184 | security: security, 185 | tlsSettings: tlsSettings, 186 | realitySettings: realitySettings, 187 | quicSettings: stream_quic(server), 188 | tcpSettings: stream_tcp(server), 189 | kcpSettings: stream_kcp(server), 190 | wsSettings: stream_ws(server), 191 | grpcSettings: stream_grpc(server), 192 | httpSettings: stream_h2(server), 193 | splithttpSettings: stream_splithttp(server), 194 | httpupgradeSettings: stream_httpupgrade(server) 195 | }, 196 | dialer_proxy: dialer_proxy 197 | }; 198 | }; 199 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/common/tls.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export function fallbacks(proxy, config) { 4 | const fallback = filter(keys(config), k => config[k][".type"] == "fallback") || []; 5 | let f = []; 6 | for (let key in fallback) { 7 | const s = config[key]; 8 | if (s["dest"] != null) { 9 | push(f, { 10 | dest: s["dest"], 11 | alpn: s["alpn"], 12 | name: s["name"], 13 | xver: s["xver"], 14 | path: s["path"] 15 | }); 16 | } 17 | } 18 | push(f, { 19 | dest: proxy["web_server_address"] 20 | }); 21 | return f; 22 | }; 23 | 24 | export function tls_outbound_settings(server, protocol) { 25 | let result = { 26 | serverName: server[protocol + "_tls_host"], 27 | allowInsecure: server[protocol + "_tls_insecure"] != "0", 28 | fingerprint: server[protocol + "_tls_fingerprint"] || "" 29 | }; 30 | 31 | if (server[protocol + "_tls_alpn"] != null) { 32 | result["alpn"] = server[protocol + "_tls_alpn"]; 33 | } 34 | 35 | return result; 36 | }; 37 | 38 | export function tls_inbound_settings(proxy, protocol_name) { 39 | let wscert = proxy[protocol_name + "_tls_cert_file"]; 40 | if (wscert == null) { 41 | wscert = proxy["web_server_cert_file"]; 42 | } 43 | let wskey = proxy[protocol_name + "_tls_key_file"]; 44 | if (wskey == null) { 45 | wskey = proxy["web_server_key_file"]; 46 | } 47 | return { 48 | alpn: [ 49 | "http/1.1" 50 | ], 51 | certificates: [ 52 | { 53 | certificateFile: wscert, 54 | keyFile: wskey 55 | } 56 | ] 57 | }; 58 | }; 59 | 60 | export function reality_outbound_settings(server, protocol) { 61 | let result = { 62 | show: server[protocol + "_reality_show"] === "1", 63 | fingerprint: server[protocol + "_reality_fingerprint"], 64 | serverName: server[protocol + "_reality_server_name"], 65 | publicKey: server[protocol + "_reality_public_key"], 66 | shortId: server[protocol + "_reality_short_id"], 67 | spiderX: server[protocol + "_reality_spider_x"], 68 | }; 69 | 70 | return result; 71 | }; 72 | 73 | export function reality_inbound_settings(proxy, protocol_name) { 74 | return { 75 | show: proxy[protocol_name + "_reality_show"], 76 | dest: proxy[protocol_name + "_reality_dest"], 77 | xver: proxy[protocol_name + "_reality_xver"], 78 | serverNames: proxy[protocol_name + "_reality_server_names"], 79 | privateKey: proxy[protocol_name + "_reality_private_key"], 80 | minClientVer: proxy[protocol_name + "_reality_min_client_ver"], 81 | maxClientVer: proxy[protocol_name + "_reality_max_client_ver"], 82 | maxTimeDiff: proxy[protocol_name + "_reality_max_time_diff"], 83 | shortIds: proxy[protocol_name + "_reality_short_ids"], 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/default_gateway.uc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ucode 2 | "use strict"; 3 | 4 | import { open, popen, stat } from "fs"; 5 | import { connect } from "ubus"; 6 | 7 | function network_dump() { 8 | const ubus = connect(); 9 | if (ubus) { 10 | const result = ubus.call("network.interface", "dump"); 11 | ubus.disconnect(); 12 | return result; 13 | } 14 | return { 15 | "interface": [] 16 | }; 17 | } 18 | 19 | function get_default_gateway(dump) { 20 | let dgs = {}; 21 | for (let i in dump["interface"] || []) { 22 | for (let j in i["route"] || []) { 23 | if (j["target"] == "0.0.0.0") { 24 | dgs[j["nexthop"]] = true; 25 | if (j["source"] != "0.0.0.0/0") { 26 | dgs[j["source"]] = true; 27 | } 28 | } 29 | } 30 | }; 31 | return keys(dgs); 32 | } 33 | 34 | function get_prefix_delegate(dump) { 35 | let pds = {}; 36 | for (let i in dump["interface"] || []) { 37 | for (let j in i["ipv6-prefix"] || []) { 38 | if (j["assigned"]) { 39 | pds[`${j["address"]}/${j["mask"]}`] = true; 40 | } 41 | } 42 | } 43 | return keys(pds); 44 | } 45 | 46 | function gen_tp_spec_dv4_dg(dg) { 47 | if (stat("/usr/share/xray/ignore_tp_spec_def_gw")) { 48 | return ""; 49 | } 50 | if (length(dg) > 0) { 51 | return `set tp_spec_dv4_dg { 52 | type ipv4_addr 53 | size 16 54 | flags interval 55 | elements = { ${join(", ", dg)} } 56 | }\n`; 57 | } 58 | return ""; 59 | } 60 | 61 | function gen_tp_spec_dv6_dg(pd) { 62 | if (length(pd) > 0) { 63 | return `set tp_spec_dv6_dg { 64 | type ipv6_addr 65 | size 16 66 | flags interval 67 | elements = { ${join(", ", pd)} } 68 | }\n`; 69 | } 70 | return ""; 71 | } 72 | 73 | function generate_include(rule_dg, rule_pd, file_path) { 74 | const handle = open(file_path, "w"); 75 | handle.write(rule_dg); 76 | handle.write(rule_pd); 77 | handle.flush(); 78 | handle.close(); 79 | } 80 | 81 | function update_nft(rule_dg, rule_pd) { 82 | const handle = popen("nft -f -", "w"); 83 | handle.write(`table inet fw4 { 84 | ${rule_dg} 85 | ${rule_pd} 86 | }`); 87 | handle.flush(); 88 | handle.close(); 89 | } 90 | 91 | function restart_dnsmasq_if_necessary() { 92 | if (stat("/usr/share/xray/restart_dnsmasq_on_iface_change")) { 93 | system("service dnsmasq restart"); 94 | } 95 | } 96 | 97 | const dump = network_dump(); 98 | const dg = get_default_gateway(dump); 99 | const pd = get_prefix_delegate(dump); 100 | const log = join(", ", [...dg, ...pd]); 101 | if (log == "") { 102 | print("default gateway not available, please wait for interface ready"); 103 | } else { 104 | print(`default gateway available at ${log}\n`); 105 | const rule_dg = gen_tp_spec_dv4_dg(dg); 106 | const rule_pd = gen_tp_spec_dv6_dg(pd); 107 | update_nft(rule_dg, rule_pd); 108 | generate_include(rule_dg, rule_pd, "/var/etc/xray/02_default_gateway_include.nft"); 109 | } 110 | restart_dnsmasq_if_necessary(); 111 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/dnsmasq_include.ut: -------------------------------------------------------------------------------- 1 | #!/usr/bin/utpl 2 | {% 3 | "use strict"; 4 | import { load_config } from "./common/config.mjs"; 5 | const config = load_config(); 6 | const general = config[filter(keys(config), k => config[k][".type"] == "general")[0]]; 7 | const dns_port = int(general["dns_port"] || 5300); 8 | const dns_count = int(general["dns_count"] || 3); 9 | const manual_tproxy = filter(keys(config), k => config[k][".type"] == "manual_tproxy") || []; 10 | %} 11 | # Generated dnsmasq configurations by luci-app-xray 12 | strict-order 13 | server=/#/127.0.0.1#{{ dns_port }} 14 | {% for (let i = dns_port; i <= dns_port + dns_count; i++): %} 15 | server=127.0.0.1#{{ i }} 16 | {% endfor %} 17 | {% for (let i in manual_tproxy): %} 18 | {% if (config[i]["rebind_domain_ok"] == "1"): %} 19 | {% for (let j in config[i]["domain_names"]): %} 20 | rebind-domain-ok={{ j }} 21 | {% endfor %} 22 | {% endif %} 23 | {% endfor %} 24 | {% if (general["blocked_to_loopback"] != "1"): %} 25 | bogus-nxdomain=127.127.127.127 26 | bogus-nxdomain=100::6c62:636f:656b:2164 27 | {% endif %} 28 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/feature/bridge.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { server_outbound } from "./outbound.mjs"; 4 | 5 | export function bridges(bridge) { 6 | let result = []; 7 | for (let v in bridge) { 8 | push(result, { 9 | tag: sprintf("bridge_inbound:%s", v[".name"]), 10 | domain: v["domain"] 11 | }); 12 | } 13 | return result; 14 | }; 15 | 16 | export function bridge_outbounds(config, bridge) { 17 | let result = []; 18 | for (let v in bridge) { 19 | const bridge_server = config[v["upstream"]]; 20 | push(result, { 21 | tag: sprintf("bridge_freedom_outbound:%s", v[".name"]), 22 | protocol: "freedom", 23 | settings: { 24 | redirect: v["redirect"] 25 | } 26 | }, ...server_outbound(bridge_server, sprintf("bridge_upstream_outbound:%s", v[".name"]), config)); 27 | } 28 | return result; 29 | }; 30 | 31 | export function bridge_rules(bridge) { 32 | let result = []; 33 | for (let v in bridge) { 34 | push(result, { 35 | type: "field", 36 | inboundTag: [sprintf("bridge_inbound:%s", v[".name"])], 37 | domain: [sprintf("full:%s", v["domain"])], 38 | outboundTag: sprintf("bridge_upstream_outbound:%s", v[".name"]) 39 | }, { 40 | type: "field", 41 | inboundTag: [sprintf("bridge_inbound:%s", v[".name"])], 42 | outboundTag: sprintf("bridge_freedom_outbound:%s", v[".name"]) 43 | }); 44 | } 45 | return result; 46 | }; 47 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/feature/dns.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { access } from "fs"; 4 | import { fake_dns_domains } from "./fake_dns.mjs"; 5 | import { direct_outbound } from "./outbound.mjs"; 6 | 7 | const fallback_fast_dns = "223.5.5.5:53"; 8 | const fallback_secure_dns = "8.8.8.8:53"; 9 | const fallback_default_dns = "1.1.1.1:53"; 10 | const geoip_existence = access("/usr/share/xray/geoip.dat") || false; 11 | const geosite_existence = access("/usr/share/xray/geosite.dat") || false; 12 | 13 | function parse_ip_port(val, port_default) { 14 | const split_dot = split(val, "."); 15 | if (length(split_dot) > 1) { 16 | const split_ipv4 = split(val, ":"); 17 | return { 18 | ip: split_ipv4[0], 19 | port: int(split_ipv4[1]) 20 | }; 21 | } 22 | const split_ipv6_port = split(val, "]:"); 23 | if (length(split_ipv6_port) == 2) { 24 | return { 25 | ip: ltrim(split_ipv6_port[0], "["), 26 | port: int(split_ipv6_port[1]), 27 | }; 28 | } 29 | return { 30 | ip: val, 31 | port: port_default 32 | }; 33 | } 34 | 35 | function format_dns(method, val) { 36 | const parsed = parse_ip_port(val, 53); 37 | if (method == "udp") { 38 | return { 39 | address: parsed["ip"], 40 | port: parsed["port"] 41 | }; 42 | } 43 | let url_suffix = ""; 44 | if (substr(method, 0, 5) == "https") { 45 | url_suffix = "/dns-query"; 46 | } 47 | return { 48 | address: `${method}://${val}${url_suffix}` 49 | }; 50 | } 51 | 52 | function domain_rules(proxy, k) { 53 | if (proxy[k] == null) { 54 | return []; 55 | } 56 | return filter(proxy[k], function (x) { 57 | if (substr(x, 0, 8) == "geosite:") { 58 | return geosite_existence; 59 | } 60 | return true; 61 | }); 62 | } 63 | 64 | export function secure_domain_rules(proxy) { 65 | return domain_rules(proxy, "forwarded_domain_rules"); 66 | }; 67 | 68 | export function fast_domain_rules(proxy) { 69 | return domain_rules(proxy, "bypassed_domain_rules"); 70 | }; 71 | 72 | export function blocked_domain_rules(proxy) { 73 | return domain_rules(proxy, "blocked_domain_rules"); 74 | }; 75 | 76 | export function dns_server_inbounds(proxy) { 77 | let result = []; 78 | const dns_port = int(proxy["dns_port"] || 5300); 79 | const dns_count = int(proxy["dns_count"] || 3); 80 | const default_dns = format_dns("udp", proxy["default_dns"] || fallback_default_dns); 81 | for (let i = dns_port; i <= dns_port + dns_count; i++) { 82 | push(result, { 83 | port: i, 84 | protocol: "dokodemo-door", 85 | tag: sprintf("dns_server_inbound:%d", i), 86 | settings: { 87 | address: default_dns["address"], 88 | port: default_dns["port"], 89 | network: "tcp,udp" 90 | } 91 | }); 92 | } 93 | return result; 94 | }; 95 | 96 | export function dns_rules(proxy, tcp_hijack_inbound_tags, udp_hijack_inbound_tags) { 97 | const dns_port = int(proxy["dns_port"] || 5300); 98 | const dns_count = int(proxy["dns_count"] || 3); 99 | let dns_server_tags = []; 100 | for (let i = dns_port; i <= dns_port + dns_count; i++) { 101 | push(dns_server_tags, sprintf("dns_server_inbound:%d", i)); 102 | } 103 | let result = [ 104 | { 105 | type: "field", 106 | inboundTag: dns_server_tags, 107 | outboundTag: "dns_server_outbound" 108 | }, 109 | ]; 110 | if (proxy.dns_tcp_hijack) { 111 | push(result, { 112 | type: "field", 113 | port: "53", 114 | inboundTag: tcp_hijack_inbound_tags, 115 | outboundTag: "dns_tcp_hijack_outbound" 116 | }); 117 | } 118 | if (proxy.dns_udp_hijack) { 119 | push(result, { 120 | type: "field", 121 | port: "53", 122 | inboundTag: udp_hijack_inbound_tags, 123 | outboundTag: "dns_udp_hijack_outbound" 124 | }); 125 | } 126 | return result; 127 | }; 128 | 129 | export function dns_server_outbounds(proxy) { 130 | let result = [ 131 | { 132 | protocol: "dns", 133 | settings: { 134 | nonIPQuery: "skip" 135 | }, 136 | streamSettings: { 137 | sockopt: { 138 | mark: 254 139 | } 140 | }, 141 | tag: "dns_server_outbound" 142 | } 143 | ]; 144 | if (proxy.dns_tcp_hijack) { 145 | push(result, direct_outbound("dns_tcp_hijack_outbound", proxy.dns_tcp_hijack)); 146 | } 147 | if (proxy.dns_udp_hijack) { 148 | push(result, direct_outbound("dns_udp_hijack_outbound", proxy.dns_udp_hijack)); 149 | } 150 | return result; 151 | }; 152 | 153 | export function dns_conf(proxy, config, manual_tproxy, fakedns) { 154 | const fast_dns_object = format_dns("udp", proxy["fast_dns"] || fallback_fast_dns); 155 | const default_dns_object = format_dns("udp", proxy["default_dns"] || fallback_default_dns); 156 | 157 | let domain_names_set = {}; 158 | let domain_extra_options = {}; 159 | 160 | for (let server in filter(values(config), i => i[".type"] == "servers")) { 161 | if (iptoarr(server["server"])) { 162 | continue; 163 | } 164 | if (server["domain_resolve_dns"]) { 165 | domain_extra_options[server["server"]] = `${server["domain_resolve_dns_method"] || "udp"};${server["domain_resolve_dns"]};${join(",", server["domain_resolve_expect_ips"] || [])}`; 166 | } else { 167 | domain_names_set[`domain:${server["server"]}`] = true; 168 | } 169 | } 170 | 171 | let resolve_merged = {}; 172 | for (let k in keys(domain_extra_options)) { 173 | const v = domain_extra_options[k]; 174 | let original = resolve_merged[v] || []; 175 | push(original, `domain:${k}`); 176 | resolve_merged[v] = original; 177 | } 178 | 179 | let servers = [ 180 | ...fake_dns_domains(fakedns), 181 | ...map(keys(resolve_merged), function (k) { 182 | const dns_split = split(k, ";"); 183 | const resolve_dns_object = format_dns(dns_split[0], dns_split[1]); 184 | let result = { 185 | address: resolve_dns_object["address"], 186 | port: resolve_dns_object["port"], 187 | domains: uniq(resolve_merged[k]), 188 | skipFallback: true, 189 | }; 190 | if (length(dns_split[2]) > 0) { 191 | const expect_ips = filter(split(dns_split[2], ",") || [], function (i) { 192 | if (!geoip_existence) { 193 | if (substr(i, 0, 6) === "geoip:") { 194 | return false; 195 | } 196 | } 197 | return true; 198 | }); 199 | result["expectIPs"] = expect_ips; 200 | } 201 | return result; 202 | }), 203 | default_dns_object, 204 | { 205 | address: fast_dns_object["address"], 206 | port: fast_dns_object["port"], 207 | domains: [...keys(domain_names_set), ...fast_domain_rules(proxy)], 208 | skipFallback: true, 209 | }, 210 | ]; 211 | 212 | if (length(secure_domain_rules(proxy)) > 0) { 213 | const secure_dns_object = format_dns("udp", proxy["secure_dns"] || fallback_secure_dns); 214 | push(servers, { 215 | address: secure_dns_object["address"], 216 | port: secure_dns_object["port"], 217 | domains: secure_domain_rules(proxy), 218 | }); 219 | } 220 | 221 | let hosts = {}; 222 | if (length(blocked_domain_rules(proxy)) > 0) { 223 | for (let rule in (blocked_domain_rules(proxy))) { 224 | hosts[rule] = ["127.127.127.127", "100::6c62:636f:656b:2164"]; // blocked! 225 | } 226 | } 227 | 228 | for (let v in manual_tproxy) { 229 | if (v.domain_names != null) { 230 | for (let d in v.domain_names) { 231 | hosts[d] = [v.source_addr]; 232 | } 233 | } 234 | } 235 | 236 | return { 237 | hosts: hosts, 238 | servers: servers, 239 | tag: "dns_conf_inbound", 240 | queryStrategy: "UseIP" 241 | }; 242 | }; 243 | 244 | export function dns_direct_servers(config) { 245 | let result = []; 246 | for (let server in filter(values(config), i => i[".type"] == "servers")) { 247 | if (iptoarr(server["server"])) { 248 | continue; 249 | } 250 | if (server["domain_resolve_dns"]) { 251 | if (index(server["domain_resolve_dns_method"], "local") > 1) { 252 | push(result, parse_ip_port(server["domain_resolve_dns"])["ip"]); 253 | } 254 | } 255 | } 256 | return result; 257 | }; 258 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/feature/fake_dns.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { balancer } from "./system.mjs"; 4 | 5 | export function fake_dns_domains(fakedns) { 6 | let domains = []; 7 | for (let f in fakedns) { 8 | push(domains, ...f["fake_dns_domain_names"]); 9 | } 10 | if (length(domains) == 0) { 11 | return []; 12 | } 13 | return [ 14 | { 15 | "address": "fakedns", 16 | "domains": domains, 17 | "skipFallback": true 18 | } 19 | ]; 20 | }; 21 | 22 | export function fake_dns_rules(fakedns) { 23 | let result = []; 24 | for (let f in fakedns) { 25 | push(result, { 26 | type: "field", 27 | inboundTag: ["tproxy_tcp_inbound_f4", "tproxy_tcp_inbound_f6"], 28 | domain: f["fake_dns_domain_names"], 29 | balancerTag: `fake_dns_balancer:${f[".name"]}@tcp_balancer` 30 | }, { 31 | type: "field", 32 | inboundTag: ["tproxy_udp_inbound_f4", "tproxy_udp_inbound_f6"], 33 | domain: f["fake_dns_domain_names"], 34 | balancerTag: `fake_dns_balancer:${f[".name"]}@udp_balancer` 35 | }); 36 | } 37 | return result; 38 | }; 39 | 40 | export function fake_dns_balancers(fakedns) { 41 | let result = []; 42 | for (let f in fakedns) { 43 | push(result, { 44 | "tag": `fake_dns_balancer:${f[".name"]}@tcp_balancer`, 45 | "selector": balancer(f, "fake_dns_forward_server_tcp", `fake_dns_tcp:${f[".name"]}`), 46 | "strategy": { 47 | "type": f["fake_dns_balancer_strategy"] || "random" 48 | } 49 | }, { 50 | "tag": `fake_dns_balancer:${f[".name"]}@udp_balancer`, 51 | "selector": balancer(f, "fake_dns_forward_server_udp", `fake_dns_udp:${f[".name"]}`), 52 | "strategy": { 53 | "type": f["fake_dns_balancer_strategy"] || "random" 54 | } 55 | }); 56 | } 57 | return result; 58 | }; 59 | 60 | export function fake_dns_conf(proxy) { 61 | return [ 62 | { 63 | "ipPool": proxy.pool_v4 || "198.18.0.0/15", 64 | "poolSize": int(proxy.pool_v4_size) || 65535 65 | }, 66 | { 67 | "ipPool": proxy.pool_v6 || "fc00::/18", 68 | "poolSize": int(proxy.pool_v6_size) || 65535 69 | } 70 | ]; 71 | }; 72 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/feature/inbound.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { https_trojan_inbound } from "../protocol/trojan.mjs"; 4 | import { https_vless_inbound } from "../protocol/vless.mjs"; 5 | import { balancer } from "./system.mjs"; 6 | 7 | export function dokodemo_inbound(listen, port, tag, sniffing, sniffing_route_only, sniffing_dest_override, sniffing_metadata_only, network, tproxy, timeout) { 8 | let result = { 9 | port: int(port), 10 | protocol: "dokodemo-door", 11 | tag: tag, 12 | sniffing: sniffing == "1" ? { 13 | enabled: true, 14 | routeOnly: sniffing_route_only == "1", 15 | destOverride: sniffing_dest_override, 16 | metadataOnly: sniffing_metadata_only == "1" 17 | } : null, 18 | settings: { 19 | network: network, 20 | followRedirect: true, 21 | timeout: int(timeout || 300), 22 | }, 23 | streamSettings: { 24 | sockopt: { 25 | tproxy: tproxy 26 | } 27 | } 28 | }; 29 | if (listen) { 30 | result["listen"] = listen; 31 | } 32 | return result; 33 | }; 34 | 35 | export function http_inbound(addr, port, tag, username, password) { 36 | let accounts = null; 37 | if (username && password) { 38 | accounts = [ 39 | { 40 | "user": username, 41 | "pass": password 42 | } 43 | ]; 44 | } 45 | return { 46 | listen: addr || "0.0.0.0", 47 | port: port, 48 | protocol: "http", 49 | tag: tag, 50 | settings: { 51 | accounts: accounts, 52 | allowTransparent: false 53 | } 54 | }; 55 | }; 56 | 57 | export function socks_inbound(addr, port, tag, username, password) { 58 | let auth = "noauth"; 59 | let accounts = null; 60 | if (username && password) { 61 | auth = "password"; 62 | accounts = [ 63 | { 64 | "user": username, 65 | "pass": password 66 | } 67 | ]; 68 | } 69 | return { 70 | listen: addr || "0.0.0.0", 71 | port: port, 72 | protocol: "socks", 73 | tag: tag, 74 | settings: { 75 | auth: auth, 76 | accounts: accounts, 77 | udp: true 78 | } 79 | }; 80 | }; 81 | 82 | export function https_inbound(proxy, config) { 83 | if (proxy["web_server_protocol"] == "vless") { 84 | return https_vless_inbound(proxy, config); 85 | } 86 | if (proxy["web_server_protocol"] == "trojan") { 87 | return https_trojan_inbound(proxy, config); 88 | } 89 | return null; 90 | }; 91 | 92 | export function extra_inbounds(proxy, extra_inbound) { 93 | let result = []; 94 | for (let v in extra_inbound) { 95 | const tag = `extra_inbound:${v[".name"]}`; 96 | if (v["inbound_type"] == "http") { 97 | push(result, http_inbound(v["inbound_addr"] || "0.0.0.0", v["inbound_port"], tag, v["inbound_username"], v["inbound_password"])); 98 | } else if (v["inbound_type"] == "socks5") { 99 | push(result, socks_inbound(v["inbound_addr"] || "0.0.0.0", v["inbound_port"], tag, v["inbound_username"], v["inbound_password"])); 100 | } else if (v["inbound_type"] == "tproxy_tcp") { 101 | push(result, dokodemo_inbound(v["inbound_addr"] || "0.0.0.0", v["inbound_port"], tag, proxy["tproxy_sniffing"], proxy["route_only"], ["http", "tls"], "0", "tcp", "tproxy")); 102 | } else if (v["inbound_type"] == "tproxy_udp") { 103 | push(result, dokodemo_inbound(v["inbound_addr"] || "0.0.0.0", v["inbound_port"], tag, proxy["tproxy_sniffing"], proxy["route_only"], ["quic"], "0", "udp", "tproxy")); 104 | } else { 105 | die(`unknown inbound type ${v["inbound_type"]}`); 106 | } 107 | } 108 | return result; 109 | }; 110 | 111 | export function extra_inbound_rules(extra_inbound) { 112 | let result = []; 113 | for (let v in extra_inbound) { 114 | if (v["specify_outbound"] == "1") { 115 | push(result, { 116 | type: "field", 117 | inboundTag: [`extra_inbound:${v[".name"]}`], 118 | balancerTag: `extra_inbound_outbound:${v[".name"]}` 119 | }); 120 | } 121 | } 122 | return result; 123 | }; 124 | 125 | export function extra_inbound_balancers(extra_inbound) { 126 | let result = []; 127 | for (let e in extra_inbound) { 128 | if (e["specify_outbound"] == "1") { 129 | push(result, { 130 | "tag": `extra_inbound_outbound:${e[".name"]}`, 131 | "selector": balancer(e, "destination", `extra_inbound:${e[".name"]}`), 132 | "strategy": { 133 | "type": e["balancer_strategy"] || "random" 134 | } 135 | }); 136 | } 137 | } 138 | return result; 139 | }; 140 | 141 | export function extra_inbound_global(extra_inbound) { 142 | const global_tags = filter(extra_inbound, v => v["specify_outbound"] != "1"); 143 | return { 144 | "tproxy_tcp": map(filter(global_tags, v => v["inbound_type"] == "tproxy_tcp"), v => `extra_inbound_${v[".name"]}`), 145 | "tproxy_udp": map(filter(global_tags, v => v["inbound_type"] == "tproxy_udp"), v => `extra_inbound_${v[".name"]}`), 146 | "http": map(filter(global_tags, v => v["inbound_type"] == "http"), v => `extra_inbound_${v[".name"]}`), 147 | "socks5": map(filter(global_tags, v => v["inbound_type"] == "socks5"), v => `extra_inbound_${v[".name"]}`), 148 | }; 149 | }; -------------------------------------------------------------------------------- /core/root/usr/share/xray/feature/manual_tproxy.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { server_outbound } from "./outbound.mjs"; 4 | 5 | export function manual_tproxy_outbounds(config, manual_tproxy) { 6 | let result = []; 7 | for (let v in manual_tproxy) { 8 | let tcp_tag = "direct"; 9 | if (v["force_forward_tcp"] == "1") { 10 | if (v["force_forward_server_tcp"] != null) { 11 | tcp_tag = `manual_tproxy:${v[".name"]}@tcp_outbound@force_forward:${v["force_forward_server_tcp"]}`; 12 | const force_forward_server_tcp = config[v["force_forward_server_tcp"]]; 13 | push(result, ...server_outbound(force_forward_server_tcp, tcp_tag, config)); 14 | } 15 | } 16 | push(result, { 17 | protocol: "freedom", 18 | tag: sprintf("manual_tproxy:%s@tcp_outbound", v[".name"]), 19 | settings: { 20 | redirect: sprintf("%s:%d", v["dest_addr"] || "", v["dest_port"] || 0), 21 | domainStrategy: "AsIs" 22 | }, 23 | proxySettings: { 24 | tag: tcp_tag 25 | } 26 | }); 27 | 28 | let udp_tag = "direct"; 29 | if (v["force_forward_udp"] == "1") { 30 | if (v["force_forward_server_udp"] != null) { 31 | udp_tag = `manual_tproxy:${v[".name"]}@udp_outbound@force_forward:${v["force_forward_server_udp"]}`; 32 | const force_forward_server_udp = config[v["force_forward_server_udp"]]; 33 | push(result, ...server_outbound(force_forward_server_udp, udp_tag, config)); 34 | } 35 | } 36 | push(result, { 37 | protocol: "freedom", 38 | tag: sprintf("manual_tproxy:%s@udp_outbound", v[".name"]), 39 | settings: { 40 | redirect: sprintf("%s:%d", v["dest_addr"] || "", v["dest_port"] || 0), 41 | domainStrategy: "AsIs" 42 | }, 43 | proxySettings: { 44 | tag: udp_tag 45 | } 46 | }); 47 | } 48 | return result; 49 | }; 50 | 51 | export function manual_tproxy_outbound_tags(manual_tproxy) { 52 | let result = []; 53 | for (let v in manual_tproxy) { 54 | if (v["force_forward_tcp"] == "1") { 55 | push(result, `manual_tproxy:${v[".name"]}@tcp_outbound@force_forward:${v["force_forward_server_tcp"]}`); 56 | } 57 | if (v["force_forward_udp"] == "1") { 58 | push(result, `manual_tproxy:${v[".name"]}@udp_outbound@force_forward:${v["force_forward_server_udp"]}`); 59 | } 60 | } 61 | return result; 62 | }; 63 | 64 | export function manual_tproxy_rules(manual_tproxy) { 65 | let result = []; 66 | for (let v in manual_tproxy) { 67 | splice(result, 0, 0, { 68 | type: "field", 69 | inboundTag: ["tproxy_tcp_inbound_v4", "socks_inbound", "https_inbound", "http_inbound"], 70 | ip: [v["source_addr"]], 71 | port: v["source_port"], 72 | outboundTag: sprintf("manual_tproxy:%s@tcp_outbound", v[".name"]) 73 | }); 74 | splice(result, 0, 0, { 75 | type: "field", 76 | inboundTag: ["tproxy_udp_inbound_v4"], 77 | ip: [v["source_addr"]], 78 | port: v["source_port"], 79 | outboundTag: sprintf("manual_tproxy:%s@udp_outbound", v[".name"]) 80 | }); 81 | } 82 | return result; 83 | }; 84 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/feature/outbound.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { http_outbound } from "../protocol/http.mjs"; 4 | import { shadowsocks_outbound } from "../protocol/shadowsocks.mjs"; 5 | import { socks_outbound } from "../protocol/socks.mjs"; 6 | import { trojan_outbound } from "../protocol/trojan.mjs"; 7 | import { vless_outbound } from "../protocol/vless.mjs"; 8 | import { vmess_outbound } from "../protocol/vmess.mjs"; 9 | 10 | const direct_mark = 252; 11 | const outbound_mark = 253; 12 | 13 | function override_custom_config_recursive(x, y) { 14 | if (type(x) != "object" || type(y) != "object") { 15 | return y; 16 | } 17 | for (let k in y) { 18 | x[k] = override_custom_config_recursive(x[k], y[k]); 19 | } 20 | return x; 21 | } 22 | 23 | function server_outbound_recursive(t, server, tag, config) { 24 | let outbound_result = null; 25 | if (server["protocol"] == "vmess") { 26 | outbound_result = vmess_outbound(server, tag); 27 | } else if (server["protocol"] == "vless") { 28 | outbound_result = vless_outbound(server, tag); 29 | } else if (server["protocol"] == "shadowsocks") { 30 | outbound_result = shadowsocks_outbound(server, tag); 31 | } else if (server["protocol"] == "trojan") { 32 | outbound_result = trojan_outbound(server, tag); 33 | } else if (server["protocol"] == "http") { 34 | outbound_result = http_outbound(server, tag); 35 | } else if (server["protocol"] == "socks") { 36 | outbound_result = socks_outbound(server, tag); 37 | } 38 | if (outbound_result == null) { 39 | die(`unknown outbound server protocol ${server["protocol"]}`); 40 | } 41 | let outbound = outbound_result["outbound"]; 42 | const custom_config_outbound_string = server["custom_config"]; 43 | 44 | if (custom_config_outbound_string != null && custom_config_outbound_string != "") { 45 | const custom_config_outbound = json(custom_config_outbound_string); 46 | for (let k in custom_config_outbound) { 47 | outbound[k] = override_custom_config_recursive(outbound[k], custom_config_outbound[k]); 48 | } 49 | } 50 | outbound["tag"] = tag; 51 | if (type(outbound["streamSettings"]) != "object") { 52 | outbound["streamSettings"] = {}; 53 | } 54 | if (type(outbound["streamSettings"]["sockopt"]) != "object") { 55 | outbound["streamSettings"]["sockopt"] = {}; 56 | } 57 | outbound["streamSettings"]["sockopt"]["mark"] = outbound_mark; 58 | 59 | const dialer_proxy = outbound_result["dialer_proxy"]; 60 | const result = [...t, outbound]; 61 | 62 | if (dialer_proxy != null) { 63 | const dialer_proxy_section = config[dialer_proxy]; 64 | return server_outbound_recursive(result, dialer_proxy_section, `${tag}@dialer_proxy:${dialer_proxy}`, config); 65 | } 66 | return result; 67 | } 68 | 69 | export function direct_outbound(tag, redirect) { 70 | return { 71 | protocol: "freedom", 72 | tag: tag, 73 | settings: { 74 | domainStrategy: "UseIPv4", 75 | redirect: redirect || "" 76 | }, 77 | streamSettings: { 78 | sockopt: { 79 | mark: direct_mark, 80 | } 81 | } 82 | }; 83 | }; 84 | 85 | export function blackhole_outbound() { 86 | return { 87 | tag: "blackhole_outbound", 88 | protocol: "blackhole" 89 | }; 90 | }; 91 | 92 | export function server_outbound(server, tag, config) { 93 | if (server == null) { 94 | return [direct_outbound(tag, null)]; 95 | } 96 | return server_outbound_recursive([], server, tag, config); 97 | }; 98 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/feature/system.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export function balancer(ref, x, prefix) { 4 | const v = ref[x] || []; 5 | if (length(v) == 0) { 6 | return ["direct"]; 7 | } 8 | return map(v, (k) => `${prefix}@balancer_outbound:${k}`); 9 | }; 10 | 11 | export function api_conf(proxy) { 12 | if (proxy["xray_api"] == '1') { 13 | return { 14 | tag: "api", 15 | services: [ 16 | "HandlerService", 17 | "LoggerService", 18 | "StatsService" 19 | ] 20 | }; 21 | } 22 | return null; 23 | }; 24 | 25 | export function metrics_conf(proxy) { 26 | if (proxy["metrics_server_enable"] == "1") { 27 | return { 28 | tag: "metrics" 29 | }; 30 | } 31 | return null; 32 | }; 33 | 34 | export function policy(proxy) { 35 | const stats = proxy["stats"] == "1"; 36 | return { 37 | levels: { 38 | "0": { 39 | handshake: int(proxy["handshake"] || 4), 40 | connIdle: int(proxy["conn_idle"] || 300), 41 | uplinkOnly: int(proxy["uplink_only"] || 2), 42 | downlinkOnly: int(proxy["downlink_only"] || 5), 43 | bufferSize: int(proxy["buffer_size"] || 4), 44 | statsUserUplink: stats, 45 | statsUserDownlink: stats, 46 | } 47 | }, 48 | system: { 49 | statsInboundUplink: stats, 50 | statsInboundDownlink: stats, 51 | statsOutboundUplink: stats, 52 | statsOutboundDownlink: stats 53 | } 54 | }; 55 | }; 56 | 57 | export function logging(proxy) { 58 | return { 59 | access: proxy["access_log"] == "1" ? "" : "none", 60 | loglevel: proxy["loglevel"] || "warning", 61 | dnsLog: proxy["dns_log"] == "1" 62 | }; 63 | }; 64 | 65 | export function system_route_rules(proxy) { 66 | let result = []; 67 | if (proxy["xray_api"] == '1') { 68 | push(result, { 69 | type: "field", 70 | inboundTag: ["api"], 71 | outboundTag: "api" 72 | }); 73 | } 74 | if (proxy["metrics_server_enable"] == "1") { 75 | push(result, { 76 | type: "field", 77 | inboundTag: ["metrics"], 78 | outboundTag: "metrics" 79 | }); 80 | } 81 | return result; 82 | }; 83 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/firewall_include.ut: -------------------------------------------------------------------------------- 1 | #!/usr/bin/utpl 2 | {% 3 | "use strict"; 4 | import { stat } from "fs"; 5 | import { load_config } from "./common/config.mjs"; 6 | import { dns_direct_servers } from "./feature/dns.mjs"; 7 | const ignore_tp_spec_def_gw = stat("/usr/share/xray/ignore_tp_spec_def_gw"); 8 | const config = load_config(); 9 | const general = config[filter(keys(config), k => config[k][".type"] == "general")[0]]; 10 | const general_mark = general.mark || 255; 11 | const tcp4_enabled = length(general.tcp_balancer_v4 || []) > 0; 12 | const udp4_enabled = length(general.udp_balancer_v4 || []) > 0; 13 | const tcp6_enabled = length(general.tcp_balancer_v6 || []) > 0; 14 | const udp6_enabled = length(general.udp_balancer_v6 || []) > 0; 15 | const uids_direct = uniq(general.uids_direct || []); 16 | const gids_direct = uniq(general.gids_direct || []); 17 | let wan_bp_ips_no_dns = general.wan_bp_ips || []; 18 | let wan_fw_ips_no_dns = general.wan_fw_ips || []; 19 | push(wan_bp_ips_no_dns, split(general.fast_dns || "223.5.5.5:53", ":")[0]); 20 | for (let i in dns_direct_servers(config)) { 21 | push(wan_bp_ips_no_dns, i); 22 | } 23 | push(wan_fw_ips_no_dns, split(general.secure_dns || "8.8.8.8:53", ":")[0]); 24 | const wan_bp_ips_v4 = filter(uniq(wan_bp_ips_no_dns), v => index(v, ":") == -1); 25 | const wan_bp_ips_v6 = filter(uniq(wan_bp_ips_no_dns), v => index(v, ":") != -1); 26 | const wan_fw_ips_v4 = filter(uniq(wan_fw_ips_no_dns), v => index(v, ":") == -1); 27 | const wan_fw_ips_v6 = filter(uniq(wan_fw_ips_no_dns), v => index(v, ":") != -1); 28 | const transparent_default_port_policy = general.transparent_default_port_policy || "forwarded"; 29 | const wan_fw_tcp_ports = general.wan_fw_tcp_ports || []; 30 | const wan_fw_udp_ports = general.wan_fw_udp_ports || []; 31 | const wan_bp_tcp_ports = general.wan_bp_tcp_ports || []; 32 | const wan_bp_udp_ports = general.wan_bp_udp_ports || []; 33 | const counter = function () { 34 | if (general.fw4_counter == "1") { 35 | return "counter packets 0 bytes 0"; 36 | } 37 | return ""; 38 | }(); 39 | const firewall_priority = function () { 40 | if (general.firewall_priority == null) { 41 | return "+10"; 42 | } 43 | if (int(general.firewall_priority) > 0) { 44 | return sprintf("+%s", general.firewall_priority); 45 | }; 46 | return sprintf("%s", general.firewall_priority); 47 | }(); 48 | const fakedns = map(filter(keys(config), k => config[k][".type"] == "fakedns") || [], k => config[k]); 49 | const manual_tproxy = filter(keys(config), k => config[k][".type"] == "manual_tproxy") || []; 50 | const manual_tproxy_source_ips = map(manual_tproxy, k => config[k]["source_addr"]) || []; 51 | 52 | const tp_spec_sm4_tp = uniq(map(filter(keys(config), k => config[k][".type"] == "lan_hosts" && config[k].access_control_strategy_v4 == "tproxy"), k => config[k].macaddr) || []); 53 | const tp_spec_sm6_tp = uniq(map(filter(keys(config), k => config[k][".type"] == "lan_hosts" && config[k].access_control_strategy_v6 == "tproxy"), k => config[k].macaddr) || []); 54 | const tp_spec_sm4_bp = uniq(map(filter(keys(config), k => config[k][".type"] == "lan_hosts" && config[k].access_control_strategy_v4 == "bypass"), k => config[k].macaddr) || []); 55 | const tp_spec_sm6_bp = uniq(map(filter(keys(config), k => config[k][".type"] == "lan_hosts" && config[k].access_control_strategy_v6 == "bypass"), k => config[k].macaddr) || []); 56 | const tp_spec_sm4_fw = map(filter(keys(config), k => config[k][".type"] == "lan_hosts" && config[k].access_control_strategy_v4 == "forward"), k => config[k]); 57 | const tp_spec_sm6_fw = map(filter(keys(config), k => config[k][".type"] == "lan_hosts" && config[k].access_control_strategy_v6 == "forward"), k => config[k]); 58 | 59 | const used_extra_inbound = uniq([ 60 | ...map(tp_spec_sm4_fw, k => k["access_control_forward_tcp_v4"]), 61 | ...map(tp_spec_sm4_fw, k => k["access_control_forward_udp_v4"]), 62 | ...map(tp_spec_sm6_fw, k => k["access_control_forward_tcp_v6"]), 63 | ...map(tp_spec_sm6_fw, k => k["access_control_forward_udp_v6"]), 64 | ]); 65 | let extra_inbound_tcp_v4_map = {}; 66 | let extra_inbound_udp_v4_map = {}; 67 | let extra_inbound_tcp_v6_map = {}; 68 | let extra_inbound_udp_v6_map = {}; 69 | for (let i in used_extra_inbound) { 70 | let tcp_v4_items = []; 71 | let udp_v4_items = []; 72 | let tcp_v6_items = []; 73 | let udp_v6_items = []; 74 | for (let j in tp_spec_sm4_fw) { 75 | if (j["access_control_forward_tcp_v4"] == i) { 76 | push(tcp_v4_items, j["macaddr"]); 77 | } 78 | if (j["access_control_forward_udp_v4"] == i) { 79 | push(udp_v4_items, j["macaddr"]); 80 | } 81 | } 82 | if (length(tcp_v4_items) > 0) { 83 | extra_inbound_tcp_v4_map[i] = tcp_v4_items; 84 | } 85 | if (length(udp_v4_items) > 0) { 86 | extra_inbound_udp_v4_map[i] = udp_v4_items; 87 | } 88 | for (let j in tp_spec_sm6_fw) { 89 | if (j["access_control_forward_tcp_v6"] == i) { 90 | push(tcp_v6_items, j["macaddr"]); 91 | } 92 | if (j["access_control_forward_udp_v6"] == i) { 93 | push(udp_v6_items, j["macaddr"]); 94 | } 95 | } 96 | if (length(tcp_v6_items) > 0) { 97 | extra_inbound_tcp_v6_map[i] = tcp_v6_items; 98 | } 99 | if (length(udp_v6_items) > 0) { 100 | extra_inbound_udp_v6_map[i] = udp_v6_items; 101 | } 102 | } 103 | const ttl_override = int(general.ttl_override); 104 | const hop_limit_override = int(general.hop_limit_override); 105 | const ttl_hop_limit_match = int(general.ttl_hop_limit_match); 106 | const dynamic_direct_tcp4 = function () { 107 | if (general.dynamic_direct_tcp4 == "1") { 108 | return `update @tp_spec_dv4_dt { ip daddr timeout ${general.dynamic_direct_timeout || 300}s }`; 109 | } 110 | return ""; 111 | }(); 112 | const dynamic_direct_tcp6 = function () { 113 | if (general.dynamic_direct_tcp6 == "1") { 114 | return `update @tp_spec_dv6_dt { ip6 daddr timeout ${general.dynamic_direct_timeout || 300}s }`; 115 | } 116 | return ""; 117 | }(); 118 | const dynamic_direct_udp4 = function () { 119 | if (general.dynamic_direct_udp4 == "1") { 120 | return `update @tp_spec_dv4_du { ip daddr timeout ${general.dynamic_direct_timeout || 300}s }`; 121 | } 122 | return ""; 123 | }(); 124 | const dynamic_direct_udp6 = function () { 125 | if (general.dynamic_direct_udp6 == "1") { 126 | return `update @tp_spec_dv6_du { ip6 daddr timeout ${general.dynamic_direct_timeout || 300}s }`; 127 | } 128 | return ""; 129 | }(); 130 | %} 131 | 132 | set tp_spec_dv4_sp { 133 | type ipv4_addr 134 | size 32 135 | flags interval 136 | elements = { 0.0.0.0/8, 10.0.0.0/8, 137 | 100.64.0.0/10, 127.0.0.0/8, 138 | 169.254.0.0/16, 172.16.0.0/12, 139 | 192.0.0.0/24, 192.52.193.0/24, 140 | 192.168.0.0/16, 224.0.0.0/3 } 141 | } 142 | 143 | set tp_spec_dv6_sp { 144 | type ipv6_addr 145 | size 32 146 | flags interval 147 | elements = { ::, 148 | ::1, 149 | ::ffff:0.0.0.0/96, 150 | ::ffff:0:0:0/96, 151 | 64:ff9b::/96, 152 | 100::/64, 153 | 2001::/32, 154 | 2001:20::/28, 155 | 2001:db8::/32, 156 | 2002::/16, 157 | fc00::/7, 158 | fe80::/10, 159 | ff00::/8 } 160 | } 161 | 162 | {% if (length(tp_spec_sm4_bp) > 0): %} 163 | set tp_spec_sm4_bp { 164 | type ether_addr 165 | size {{ length(tp_spec_sm4_bp) * 2 + 1 }} 166 | elements = { {{ join(", ", tp_spec_sm4_bp) }} } 167 | } 168 | {% endif %} 169 | 170 | {% if (length(tp_spec_sm6_bp) > 0): %} 171 | set tp_spec_sm6_bp { 172 | type ether_addr 173 | size {{ length(tp_spec_sm6_bp) * 2 + 1 }} 174 | elements = { {{ join(", ", tp_spec_sm6_bp) }} } 175 | } 176 | {% endif %} 177 | 178 | {% if (length(tp_spec_sm4_tp) > 0): %} 179 | set tp_spec_sm4_tp { 180 | type ether_addr 181 | size {{ length(tp_spec_sm4_tp) * 2 + 1 }} 182 | elements = { {{ join(", ", tp_spec_sm4_tp) }} } 183 | } 184 | {% endif %} 185 | 186 | {% if (length(tp_spec_sm6_tp) > 0): %} 187 | set tp_spec_sm6_tp { 188 | type ether_addr 189 | size {{ length(tp_spec_sm6_tp) * 2 + 1 }} 190 | elements = { {{ join(", ", tp_spec_sm6_tp) }} } 191 | } 192 | {% endif %} 193 | 194 | {% for (let i in extra_inbound_tcp_v4_map): %} 195 | set tp_spec_sm4_ft_{{ i }} { 196 | type ether_addr 197 | size {{ length(extra_inbound_tcp_v4_map) * 2 + 1 }} 198 | elements = { {{ join(", ", extra_inbound_tcp_v4_map[i]) }} } 199 | } 200 | {% endfor %} 201 | 202 | {% for (let i in extra_inbound_udp_v4_map): %} 203 | set tp_spec_sm4_fu_{{ i }} { 204 | type ether_addr 205 | size {{ length(extra_inbound_udp_v4_map) * 2 + 1 }} 206 | elements = { {{ join(", ", extra_inbound_udp_v4_map[i]) }} } 207 | } 208 | {% endfor %} 209 | 210 | {% for (let i in extra_inbound_tcp_v6_map): %} 211 | set tp_spec_sm6_ft_{{ i }} { 212 | type ether_addr 213 | size {{ length(extra_inbound_tcp_v6_map) * 2 + 1 }} 214 | elements = { {{ join(", ", extra_inbound_tcp_v6_map[i]) }} } 215 | } 216 | {% endfor %} 217 | 218 | {% for (let i in extra_inbound_udp_v6_map): %} 219 | set tp_spec_sm6_fu_{{ i }} { 220 | type ether_addr 221 | size {{ length(extra_inbound_udp_v6_map) * 2 + 1 }} 222 | elements = { {{ join(", ", extra_inbound_udp_v6_map[i]) }} } 223 | } 224 | {% endfor %} 225 | 226 | {% if (length(manual_tproxy_source_ips) > 0): %} 227 | set tp_spec_dv4_mt { 228 | type ipv4_addr 229 | size {{ length(manual_tproxy_source_ips) * 2 + 1 }} 230 | elements = { {{ join(", ", manual_tproxy_source_ips) }} } 231 | } 232 | {% endif %} 233 | 234 | {% if (length(wan_bp_ips_v4) > 0): %} 235 | set tp_spec_dv4_bp { 236 | type ipv4_addr 237 | size {{ length(wan_bp_ips_v4) * 2 + 1 }} 238 | flags interval 239 | elements = { {{ join(", ", wan_bp_ips_v4)}} } 240 | } 241 | {% endif %} 242 | 243 | {% if (length(wan_bp_ips_v6) > 0): %} 244 | set tp_spec_dv6_bp { 245 | type ipv6_addr 246 | size {{ length(wan_bp_ips_v6) * 2 + 1 }} 247 | flags interval 248 | elements = { {{ join(", ", wan_bp_ips_v6)}} } 249 | } 250 | {% endif %} 251 | 252 | {% if (length(wan_fw_ips_v4) > 0): %} 253 | set tp_spec_dv4_fw { 254 | type ipv4_addr 255 | size {{ length(wan_fw_ips_v4) * 2 + 1 }} 256 | flags interval 257 | elements = { {{ join(", ", wan_fw_ips_v4)}} } 258 | } 259 | {% endif %} 260 | 261 | {% if (length(wan_fw_ips_v6) > 0): %} 262 | set tp_spec_dv6_fw { 263 | type ipv6_addr 264 | size {{ length(wan_fw_ips_v6) * 2 + 1 }} 265 | flags interval 266 | elements = { {{ join(", ", wan_fw_ips_v6)}} } 267 | } 268 | {% endif %} 269 | 270 | {% if (ignore_tp_spec_def_gw == null): %} 271 | set tp_spec_dv4_dg { 272 | type ipv4_addr 273 | size 16 274 | flags interval 275 | } 276 | {% endif %} 277 | 278 | set tp_spec_dv6_dg { 279 | type ipv6_addr 280 | size 16 281 | flags interval 282 | } 283 | 284 | {% if (general.dynamic_direct_tcp4 == "1"): %} 285 | set tp_spec_dv4_dt { 286 | type ipv4_addr 287 | size 65536 288 | flags timeout 289 | } 290 | {% endif %} 291 | 292 | {% if (general.dynamic_direct_tcp6 == "1"): %} 293 | set tp_spec_dv6_dt { 294 | type ipv6_addr 295 | size 65536 296 | flags timeout 297 | } 298 | {% endif %} 299 | 300 | {% if (general.dynamic_direct_udp4 == "1"): %} 301 | set tp_spec_dv4_du { 302 | type ipv4_addr 303 | size 65536 304 | flags timeout 305 | } 306 | {% endif %} 307 | 308 | {% if (general.dynamic_direct_udp6 == "1"): %} 309 | set tp_spec_dv6_du { 310 | type ipv6_addr 311 | size 65536 312 | flags timeout 313 | } 314 | {% endif %} 315 | 316 | chain xray_transparent_proxy { 317 | type filter hook prerouting priority filter {{ firewall_priority }}; policy accept; 318 | mark 0x000000fb {{ counter }} goto tp_spec_wan_fw 319 | ip protocol tcp {{ counter }} accept 320 | ip protocol udp {{ counter }} accept 321 | ip6 nexthdr tcp {{ counter }} accept 322 | ip6 nexthdr udp {{ counter }} accept 323 | {{ counter }} accept 324 | } 325 | 326 | chain tp_spec_wan_fw { 327 | {% if (length(fakedns) > 0): %} 328 | ip protocol tcp ip daddr {{ general.pool_v4 || "198.18.0.0/15" }} {{ counter }} tproxy ip to :{{ general.tproxy_port_tcp_f4 || 1086 }} accept 329 | ip protocol udp ip daddr {{ general.pool_v4 || "198.18.0.0/15" }} {{ counter }} tproxy ip to :{{ general.tproxy_port_udp_f4 || 1088 }} accept 330 | ip6 nexthdr tcp ip6 daddr {{ general.pool_v6 || "fc00::/18" }} {{ counter }} tproxy ip6 to :{{ general.tproxy_port_tcp_f6 || 1087 }} accept 331 | ip6 nexthdr udp ip6 daddr {{ general.pool_v6 || "fc00::/18" }} {{ counter }} tproxy ip6 to :{{ general.tproxy_port_udp_f6 || 1089 }} accept 332 | {% endif %} 333 | {% if (length(manual_tproxy_source_ips) > 0): %} 334 | ip protocol tcp ip daddr @tp_spec_dv4_mt {{ counter }} tproxy ip to :{{ general.tproxy_port_tcp_v4 || 1082 }} accept 335 | ip protocol udp ip daddr @tp_spec_dv4_mt {{ counter }} tproxy ip to :{{ general.tproxy_port_udp_v4 || 1084 }} accept 336 | {% endif %} 337 | {% for (let i in extra_inbound_tcp_v4_map): %} 338 | ip protocol tcp ether saddr @tp_spec_sm4_ft_{{ i }} {{ counter }} tproxy ip to :{{ config[i].inbound_port }} accept 339 | {% endfor %} 340 | {% for (let i in extra_inbound_udp_v4_map): %} 341 | ip protocol udp ether saddr @tp_spec_sm4_fu_{{ i }} {{ counter }} tproxy ip to :{{ config[i].inbound_port }} accept 342 | {% endfor %} 343 | {% for (let i in extra_inbound_tcp_v6_map): %} 344 | ip6 nexthdr tcp ether saddr @tp_spec_sm6_ft_{{ i }} {{ counter }} tproxy ip6 to :{{ config[i].inbound_port }} accept 345 | {% endfor %} 346 | {% for (let i in extra_inbound_udp_v6_map): %} 347 | ip6 nexthdr udp ether saddr @tp_spec_sm6_fu_{{ i }} {{ counter }} tproxy ip6 to :{{ config[i].inbound_port }} accept 348 | {% endfor %} 349 | {% if (tcp4_enabled): %} 350 | ip protocol tcp {{ counter }} tproxy ip to :{{ general.tproxy_port_tcp_v4 || 1082 }} accept 351 | {% else %} 352 | ip protocol tcp {{ counter }} meta mark set {{ sprintf("0x%08x", general_mark) }} accept 353 | {% endif %} 354 | {% if (udp4_enabled): %} 355 | ip protocol udp {{ counter }} tproxy ip to :{{ general.tproxy_port_udp_v4 || 1084 }} accept 356 | {% else %} 357 | ip protocol udp {{ counter }} meta mark set {{ sprintf("0x%08x", general_mark) }} accept 358 | {% endif %} 359 | {% if (tcp6_enabled): %} 360 | ip6 nexthdr tcp {{ counter }} tproxy ip6 to :{{ general.tproxy_port_tcp_v6 || 1083 }} accept 361 | {% else %} 362 | ip6 nexthdr tcp {{ counter }} meta mark set {{ sprintf("0x%08x", general_mark) }} accept 363 | {% endif %} 364 | {% if (udp6_enabled): %} 365 | ip6 nexthdr udp {{ counter }} tproxy ip6 to :{{ general.tproxy_port_udp_v6 || 1085 }} accept 366 | {% else %} 367 | ip6 nexthdr udp {{ counter }} meta mark set {{ sprintf("0x%08x", general_mark) }} accept 368 | {% endif %} 369 | {{ counter }} accept 370 | } 371 | 372 | chain xray_prerouting { 373 | type filter hook prerouting priority mangle {{ firewall_priority }}; policy accept; 374 | {% if (length(general.ttl_override_bypass_ports) > 0): %} 375 | tcp dport { {{ join(', ', general.ttl_override_bypass_ports) }} } {{ counter }} accept 376 | udp dport { {{ join(', ', general.ttl_override_bypass_ports) }} } {{ counter }} accept 377 | {% endif %} 378 | {% if (ttl_override > 0): %} 379 | ip ttl {{ ttl_hop_limit_match }} {{ counter }} ip ttl set {{ ttl_override }} 380 | {% endif %} 381 | {% if (hop_limit_override > 0): %} 382 | ip6 hoplimit {{ ttl_hop_limit_match }} {{ counter }} ip6 hoplimit set {{ hop_limit_override }} 383 | {% endif %} 384 | {{ counter }} mark set ct mark 385 | {% if (general.dynamic_direct_tcp4 == "1"): %} 386 | ip protocol tcp meta mark 0x000000fa {{ counter }} accept comment "Xray dynamic direct TCP4" 387 | {% endif %} 388 | {% if (general.dynamic_direct_udp4 == "1"): %} 389 | ip protocol udp meta mark 0x000000fa {{ counter }} accept comment "Xray dynamic direct UDP4" 390 | {% endif %} 391 | {% if (general.dynamic_direct_tcp6 == "1"): %} 392 | ip6 nexthdr tcp meta mark 0x000000fa {{ counter }} accept comment "Xray dynamic direct TCP6" 393 | {% endif %} 394 | {% if (general.dynamic_direct_udp6 == "1"): %} 395 | ip6 nexthdr udp meta mark 0x000000fa {{ counter }} accept comment "Xray dynamic direct UDP6" 396 | {% endif %} 397 | mark 0x000000fb {{ counter }} accept comment "Xray remarked from output" 398 | {{ counter }} jump tp_spec_lan_mf comment "Xray FakeDNS / manual transparent proxy" 399 | {% if (length(general.bypass_ifaces_v4 || []) > 0): %} 400 | ip protocol tcp iifname { "{{ join('", "', general.bypass_ifaces_v4) }}" } {{ counter }} accept 401 | ip protocol udp iifname { "{{ join('", "', general.bypass_ifaces_v4) }}" } {{ counter }} accept 402 | {% endif %} 403 | {% if (length(general.bypass_ifaces_v6 || []) > 0): %} 404 | ip6 nexthdr tcp iifname { "{{ join('", "', general.bypass_ifaces_v6) }}" } {{ counter }} accept 405 | ip6 nexthdr udp iifname { "{{ join('", "', general.bypass_ifaces_v6) }}" } {{ counter }} accept 406 | {% endif %} 407 | {% if (length(tp_spec_sm4_bp) > 0): %} 408 | ip protocol tcp ether saddr @tp_spec_sm4_bp {{ counter }} accept 409 | ip protocol udp ether saddr @tp_spec_sm4_bp {{ counter }} accept 410 | {% endif %} 411 | {% if (length(tp_spec_sm6_bp) > 0): %} 412 | ip6 nexthdr tcp ether saddr @tp_spec_sm6_bp {{ counter }} accept 413 | ip6 nexthdr udp ether saddr @tp_spec_sm6_bp {{ counter }} accept 414 | {% endif %} 415 | {% for (let i in extra_inbound_tcp_v4_map): %} 416 | ip protocol tcp ether saddr @tp_spec_sm4_ft_{{ i }} {{ counter }} goto tp_spec_lan_ac 417 | {% endfor %} 418 | {% for (let i in extra_inbound_udp_v4_map): %} 419 | ip protocol udp ether saddr @tp_spec_sm4_fu_{{ i }} {{ counter }} goto tp_spec_lan_ac 420 | {% endfor %} 421 | {% for (let i in extra_inbound_tcp_v6_map): %} 422 | ip6 nexthdr tcp ether saddr @tp_spec_sm6_ft_{{ i }} {{ counter }} goto tp_spec_lan_ac 423 | {% endfor %} 424 | {% for (let i in extra_inbound_udp_v6_map): %} 425 | ip6 nexthdr udp ether saddr @tp_spec_sm6_fu_{{ i }} {{ counter }} goto tp_spec_lan_ac 426 | {% endfor %} 427 | {% if (length(tp_spec_sm4_tp) > 0): %} 428 | ip protocol tcp ether saddr @tp_spec_sm4_tp {{ counter }} goto tp_spec_lan_ac 429 | ip protocol udp ether saddr @tp_spec_sm4_tp {{ counter }} goto tp_spec_lan_ac 430 | {% endif %} 431 | {% if (length(tp_spec_sm6_tp) > 0): %} 432 | ip6 nexthdr tcp ether saddr @tp_spec_sm6_tp {{ counter }} goto tp_spec_lan_ac 433 | ip6 nexthdr udp ether saddr @tp_spec_sm6_tp {{ counter }} goto tp_spec_lan_ac 434 | {% endif %} 435 | {% if (length(general.tproxy_ifaces_v4 || []) > 0): %} 436 | ip protocol tcp iifname { "{{ join('", "', general.tproxy_ifaces_v4) }}" } {{ counter }} goto tp_spec_lan_ac 437 | ip protocol udp iifname { "{{ join('", "', general.tproxy_ifaces_v4) }}" } {{ counter }} goto tp_spec_lan_ac 438 | {% endif %} 439 | {% if (length(general.tproxy_ifaces_v6 || []) > 0): %} 440 | ip6 nexthdr tcp iifname { "{{ join('", "', general.tproxy_ifaces_v6) }}" } {{ counter }} goto tp_spec_lan_ac 441 | ip6 nexthdr udp iifname { "{{ join('", "', general.tproxy_ifaces_v6) }}" } {{ counter }} goto tp_spec_lan_ac 442 | {% endif %} 443 | ip protocol tcp {{ counter }} accept 444 | ip protocol udp {{ counter }} accept 445 | ip6 nexthdr tcp {{ counter }} accept 446 | ip6 nexthdr udp {{ counter }} accept 447 | {{ counter }} accept 448 | } 449 | 450 | chain xray_output { 451 | type route hook output priority mangle {{ firewall_priority }}; policy accept; 452 | {% if (length(uids_direct) > 0): %} 453 | meta skuid { {{ join(", ", uids_direct) }} } {{ counter }} accept 454 | {% endif %} 455 | {% if (length(gids_direct) > 0): %} 456 | meta skgid { {{ join(", ", gids_direct) }} } {{ counter }} accept 457 | {% endif %} 458 | ip protocol tcp {{ counter }} goto tp_spec_wan_ac 459 | ip protocol udp {{ counter }} goto tp_spec_wan_ac 460 | ip6 nexthdr tcp {{ counter }} goto tp_spec_wan_ac 461 | ip6 nexthdr udp {{ counter }} goto tp_spec_wan_ac 462 | {{ counter }} accept 463 | } 464 | 465 | chain tp_spec_wan_ac { 466 | ip protocol tcp mark 0x000000fc {{ counter }} {{ dynamic_direct_tcp4 }} accept comment "Xray direct outbound TCP4" 467 | ip protocol udp mark 0x000000fc {{ counter }} {{ dynamic_direct_udp4 }} accept comment "Xray direct outbound UDP4" 468 | ip6 nexthdr tcp mark 0x000000fc {{ counter }} {{ dynamic_direct_tcp6 }} accept comment "Xray direct outbound TCP6" 469 | ip6 nexthdr udp mark 0x000000fc {{ counter }} {{ dynamic_direct_udp6 }} accept comment "Xray direct outbound UDP6" 470 | meta mark 0x000000fd {{ counter }} accept comment "Xray transparent proxy outbound" 471 | meta mark 0x000000fe {{ counter }} accept comment "Xray non-IP DNS query outbound" 472 | meta mark {{ sprintf("0x%08x", general_mark) }} {{ counter }} accept comment "Xray specified mark {{ general_mark }} outbound" 473 | {{ counter }} jump tp_spec_lan_mf 474 | {% if (!tcp4_enabled): %} 475 | ip protocol tcp {{ counter }} accept 476 | {% endif %} 477 | {% if (!udp4_enabled): %} 478 | ip protocol udp {{ counter }} accept 479 | {% endif %} 480 | {% if (!tcp6_enabled): %} 481 | ip6 nexthdr tcp {{ counter }} accept 482 | {% endif %} 483 | {% if (!udp6_enabled): %} 484 | ip6 nexthdr udp {{ counter }} accept 485 | {% endif %} 486 | {{ counter }} goto tp_spec_lan_ac 487 | } 488 | 489 | chain tp_spec_lan_mf { 490 | {% if (length(fakedns) > 0): %} 491 | ip protocol tcp ip daddr {{ general.pool_v4 || "198.18.0.0/15" }} {{ counter }} goto tp_spec_lan_fw comment "Xray FakeDNS IPv4 Pool TCP" 492 | ip protocol udp ip daddr {{ general.pool_v4 || "198.18.0.0/15" }} {{ counter }} goto tp_spec_lan_fw comment "Xray FakeDNS IPv4 Pool UDP" 493 | ip6 nexthdr tcp ip6 daddr {{ general.pool_v6 || "fc00::/18" }} {{ counter }} goto tp_spec_lan_fw comment "Xray FakeDNS IPv6 Pool TCP" 494 | ip6 nexthdr udp ip6 daddr {{ general.pool_v6 || "fc00::/18" }} {{ counter }} goto tp_spec_lan_fw comment "Xray FakeDNS IPv6 Pool UDP" 495 | {% endif %} 496 | {% if (length(manual_tproxy_source_ips) > 0): %} 497 | ip protocol tcp ip daddr @tp_spec_dv4_mt {{ counter }} goto tp_spec_lan_fw comment "Xray manual transparent proxy TCP" 498 | ip protocol udp ip daddr @tp_spec_dv4_mt {{ counter }} goto tp_spec_lan_fw comment "Xray manual transparent proxy UDP" 499 | {% endif %} 500 | {{ counter }} return 501 | } 502 | 503 | chain tp_spec_lan_ac { 504 | {% if (length(wan_fw_ips_v4) > 0): %} 505 | ip daddr @tp_spec_dv4_fw {{ counter }} goto tp_spec_lan_fw 506 | {% endif %} 507 | {% if (length(wan_fw_ips_v6) > 0): %} 508 | ip6 daddr @tp_spec_dv6_fw {{ counter }} goto tp_spec_lan_fw 509 | {% endif %} 510 | {% if (ignore_tp_spec_def_gw == null): %} 511 | ip daddr @tp_spec_dv4_dg {{ counter }} accept 512 | {% endif %} 513 | ip6 daddr @tp_spec_dv6_dg {{ counter }} accept 514 | {% if (length(wan_bp_ips_v4) > 0): %} 515 | ip daddr @tp_spec_dv4_bp {{ counter }} accept 516 | {% endif %} 517 | {% if (length(wan_bp_ips_v6) > 0): %} 518 | ip6 daddr @tp_spec_dv6_bp {{ counter }} accept 519 | {% endif %} 520 | ip daddr @tp_spec_dv4_sp {{ counter }} accept 521 | ip6 daddr @tp_spec_dv6_sp {{ counter }} accept 522 | {{ counter }} goto tp_spec_lan_re 523 | } 524 | 525 | chain tp_spec_lan_re { 526 | {% if (transparent_default_port_policy == "bypassed"): %} 527 | {% if (length(wan_fw_tcp_ports) > 0): %} 528 | tcp dport { {{ join(", ", wan_fw_tcp_ports) }} } {{ counter }} goto tp_spec_lan_fw 529 | {% endif %} 530 | {% if (length(wan_fw_udp_ports) > 0): %} 531 | udp dport { {{ join(", ", wan_fw_udp_ports) }} } {{ counter }} goto tp_spec_lan_fw 532 | {% endif %} 533 | {% else %} 534 | {% if (length(wan_bp_tcp_ports) > 0): %} 535 | tcp dport { {{ join(", ", wan_bp_tcp_ports) }} } {{ counter }} accept 536 | {% endif %} 537 | {% if (length(wan_bp_udp_ports) > 0): %} 538 | udp dport { {{ join(", ", wan_bp_udp_ports) }} } {{ counter }} accept 539 | {% endif %} 540 | meta l4proto tcp {{ counter }} goto tp_spec_lan_dd 541 | meta l4proto udp {{ counter }} goto tp_spec_lan_dd 542 | {% endif %} 543 | {{ counter }} accept 544 | } 545 | 546 | chain tp_spec_lan_dd { 547 | {% if (general.dynamic_direct_tcp4 == "1"): %} 548 | ip protocol tcp ip daddr @tp_spec_dv4_dt {{ counter }} mark set 0x000000fa goto tp_spec_lan_ct comment "Xray dynamic direct TCP4" 549 | {% endif %} 550 | {% if (general.dynamic_direct_udp4 == "1"): %} 551 | ip protocol udp ip daddr @tp_spec_dv4_du {{ counter }} mark set 0x000000fa goto tp_spec_lan_ct comment "Xray dynamic direct UDP4" 552 | {% endif %} 553 | {% if (general.dynamic_direct_tcp6 == "1"): %} 554 | ip6 nexthdr tcp ip6 daddr @tp_spec_dv6_dt {{ counter }} mark set 0x000000fa goto tp_spec_lan_ct comment "Xray dynamic direct TCP6" 555 | {% endif %} 556 | {% if (general.dynamic_direct_udp6 == "1"): %} 557 | ip6 nexthdr udp ip6 daddr @tp_spec_dv6_du {{ counter }} mark set 0x000000fa goto tp_spec_lan_ct comment "Xray dynamic direct UDP6" 558 | {% endif %} 559 | {{ counter }} goto tp_spec_lan_fw 560 | } 561 | 562 | chain tp_spec_lan_fw { 563 | {{ counter }} mark set 0x000000fb goto tp_spec_lan_ct 564 | } 565 | 566 | chain tp_spec_lan_ct { 567 | {{ counter }} ct mark set mark accept 568 | } 569 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/gen_config.uc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ucode 2 | "use strict"; 3 | 4 | import { access } from "fs"; 5 | import { load_config } from "./common/config.mjs"; 6 | import { bridge_outbounds, bridge_rules, bridges } from "./feature/bridge.mjs"; 7 | import { blocked_domain_rules, dns_conf, dns_rules, dns_server_inbounds, dns_server_outbounds, fast_domain_rules, secure_domain_rules } from "./feature/dns.mjs"; 8 | import { fake_dns_balancers, fake_dns_conf, fake_dns_rules } from "./feature/fake_dns.mjs"; 9 | import { dokodemo_inbound, extra_inbound_balancers, extra_inbound_global, extra_inbound_rules, extra_inbounds, http_inbound, https_inbound, socks_inbound } from "./feature/inbound.mjs"; 10 | import { manual_tproxy_outbound_tags, manual_tproxy_outbounds, manual_tproxy_rules } from "./feature/manual_tproxy.mjs"; 11 | import { blackhole_outbound, direct_outbound, server_outbound } from "./feature/outbound.mjs"; 12 | import { api_conf, balancer, logging, metrics_conf, policy, system_route_rules } from "./feature/system.mjs"; 13 | 14 | function inbounds(proxy, config, extra_inbound) { 15 | const tproxy_sniffing = proxy["tproxy_sniffing"]; 16 | const route_only = proxy["route_only"]; 17 | const conn_idle = proxy["conn_idle"]; 18 | 19 | let i = [ 20 | socks_inbound("0.0.0.0", proxy["socks_port"] || 1080, "socks_inbound"), 21 | http_inbound("0.0.0.0", proxy["http_port"] || 1081, "http_inbound"), 22 | dokodemo_inbound("0.0.0.0", proxy["tproxy_port_tcp_v4"] || 1082, "tproxy_tcp_inbound_v4", tproxy_sniffing, route_only, ["http", "tls"], "0", "tcp", "tproxy", conn_idle), 23 | dokodemo_inbound("0.0.0.0", proxy["tproxy_port_tcp_v6"] || 1083, "tproxy_tcp_inbound_v6", tproxy_sniffing, route_only, ["http", "tls"], "0", "tcp", "tproxy", conn_idle), 24 | dokodemo_inbound("0.0.0.0", proxy["tproxy_port_udp_v4"] || 1084, "tproxy_udp_inbound_v4", tproxy_sniffing, route_only, ["quic"], "0", "udp", "tproxy", conn_idle), 25 | dokodemo_inbound("0.0.0.0", proxy["tproxy_port_udp_v6"] || 1085, "tproxy_udp_inbound_v6", tproxy_sniffing, route_only, ["quic"], "0", "udp", "tproxy", conn_idle), 26 | ...extra_inbounds(proxy, extra_inbound), 27 | ...dns_server_inbounds(proxy), 28 | dokodemo_inbound("0.0.0.0", proxy["tproxy_port_tcp_f4"] || 1086, "tproxy_tcp_inbound_f4", "1", "0", ["fakedns"], "1", "tcp", "tproxy", conn_idle), 29 | dokodemo_inbound("0.0.0.0", proxy["tproxy_port_tcp_f6"] || 1087, "tproxy_tcp_inbound_f6", "1", "0", ["fakedns"], "1", "tcp", "tproxy", conn_idle), 30 | dokodemo_inbound("0.0.0.0", proxy["tproxy_port_udp_f4"] || 1088, "tproxy_udp_inbound_f4", "1", "0", ["fakedns"], "1", "udp", "tproxy", conn_idle), 31 | dokodemo_inbound("0.0.0.0", proxy["tproxy_port_udp_f6"] || 1089, "tproxy_udp_inbound_f6", "1", "0", ["fakedns"], "1", "udp", "tproxy", conn_idle), 32 | ]; 33 | if (proxy["web_server_enable"] == "1") { 34 | push(i, https_inbound(proxy, config)); 35 | } 36 | if (proxy["metrics_server_enable"] == '1') { 37 | push(i, { 38 | listen: "0.0.0.0", 39 | port: int(proxy["metrics_server_port"]) || 18888, 40 | protocol: "dokodemo-door", 41 | settings: { 42 | address: "127.0.0.1" 43 | }, 44 | tag: "metrics" 45 | }); 46 | } 47 | if (proxy["xray_api"] == '1') { 48 | push(i, { 49 | listen: "127.0.0.1", 50 | port: 8080, 51 | protocol: "dokodemo-door", 52 | settings: { 53 | address: "127.0.0.1" 54 | }, 55 | tag: "api" 56 | }); 57 | } 58 | return i; 59 | } 60 | 61 | function outbounds(proxy, config, manual_tproxy, bridge, extra_inbound, fakedns) { 62 | let result = [ 63 | blackhole_outbound(), 64 | direct_outbound("direct", null), 65 | ...dns_server_outbounds(proxy), 66 | ...manual_tproxy_outbounds(config, manual_tproxy), 67 | ...bridge_outbounds(config, bridge) 68 | ]; 69 | let outbound_balancers_all = {}; 70 | for (let b in ["tcp_balancer_v4", "udp_balancer_v4", "tcp_balancer_v6", "udp_balancer_v6"]) { 71 | for (let i in balancer(proxy, b, b)) { 72 | if (i != "direct") { 73 | outbound_balancers_all[i] = true; 74 | } 75 | } 76 | } 77 | for (let e in extra_inbound) { 78 | if (e["specify_outbound"] == "1") { 79 | for (let i in balancer(e, "destination", `extra_inbound:${e[".name"]}`)) { 80 | if (i != "direct") { 81 | outbound_balancers_all[i] = true; 82 | } 83 | } 84 | } 85 | } 86 | for (let f in fakedns) { 87 | for (let i in balancer(f, "fake_dns_forward_server_tcp", `fake_dns_tcp:${f[".name"]}`)) { 88 | if (i != "direct") { 89 | outbound_balancers_all[i] = true; 90 | } 91 | } 92 | for (let i in balancer(f, "fake_dns_forward_server_udp", `fake_dns_udp:${f[".name"]}`)) { 93 | if (i != "direct") { 94 | outbound_balancers_all[i] = true; 95 | } 96 | } 97 | } 98 | for (let i in keys(outbound_balancers_all)) { 99 | push(result, ...server_outbound(config[substr(i, -9)], i, config)); 100 | } 101 | return result; 102 | } 103 | 104 | function rules(proxy, bridge, manual_tproxy, extra_inbound, fakedns) { 105 | const geoip_existence = access("/usr/share/xray/geoip.dat") || false; 106 | const tproxy_tcp_inbound_v4_tags = ["tproxy_tcp_inbound_v4"]; 107 | const tproxy_udp_inbound_v4_tags = ["tproxy_udp_inbound_v4"]; 108 | const tproxy_tcp_inbound_v6_tags = ["tproxy_tcp_inbound_v6"]; 109 | const tproxy_udp_inbound_v6_tags = ["tproxy_udp_inbound_v6"]; 110 | const extra_inbound_global_tags = extra_inbound_global(); 111 | const extra_inbound_global_tcp_tags = extra_inbound_global_tags["tproxy_tcp"] || []; 112 | const extra_inbound_global_udp_tags = extra_inbound_global_tags["tproxy_udp"] || []; 113 | const extra_inbound_global_http_tags = extra_inbound_global_tags["http"] || []; 114 | const extra_inbound_global_socks5_tags = extra_inbound_global_tags["socks5"] || []; 115 | const built_in_tcp_inbounds = [...tproxy_tcp_inbound_v4_tags, ...extra_inbound_global_tcp_tags, ...extra_inbound_global_http_tags, ...extra_inbound_global_socks5_tags, "socks_inbound", "https_inbound", "http_inbound"]; 116 | const built_in_udp_inbounds = [...tproxy_udp_inbound_v4_tags, ...extra_inbound_global_udp_tags, "dns_conf_inbound"]; 117 | let result = [ 118 | ...fake_dns_rules(fakedns), 119 | ...manual_tproxy_rules(manual_tproxy), 120 | ...extra_inbound_rules(extra_inbound), 121 | ...system_route_rules(proxy), 122 | ...bridge_rules(bridge), 123 | ...dns_rules(proxy, [...tproxy_tcp_inbound_v6_tags, ...tproxy_tcp_inbound_v4_tags, ...extra_inbound_global_tcp_tags], [...tproxy_udp_inbound_v6_tags, ...tproxy_udp_inbound_v4_tags, ...extra_inbound_global_udp_tags]), 124 | ...function () { 125 | let direct_rules = []; 126 | if (geoip_existence) { 127 | const geoip_direct_code_list = map(proxy["geoip_direct_code_list"] || [], v => index(v, ":") > 0 ? v : `geoip:${v}`); 128 | if (length(geoip_direct_code_list) > 0) { 129 | push(direct_rules, { 130 | type: "field", 131 | inboundTag: [...built_in_tcp_inbounds, ...built_in_udp_inbounds], 132 | outboundTag: "direct", 133 | ip: geoip_direct_code_list 134 | }); 135 | } 136 | const geoip_direct_code_list_v6 = map(proxy["geoip_direct_code_list_v6"] || [], v => index(v, ":") > 0 ? v : `geoip:${v}`); 137 | if (length(geoip_direct_code_list_v6) > 0) { 138 | push(direct_rules, { 139 | type: "field", 140 | inboundTag: [...tproxy_tcp_inbound_v6_tags, ...tproxy_udp_inbound_v6_tags], 141 | outboundTag: "direct", 142 | ip: geoip_direct_code_list_v6 143 | }); 144 | } 145 | push(direct_rules, { 146 | type: "field", 147 | inboundTag: [...tproxy_tcp_inbound_v6_tags, ...tproxy_udp_inbound_v6_tags, ...built_in_tcp_inbounds, ...built_in_udp_inbounds], 148 | outboundTag: "direct", 149 | ip: ["geoip:private"] 150 | }); 151 | } 152 | return direct_rules; 153 | }(), 154 | { 155 | type: "field", 156 | inboundTag: tproxy_tcp_inbound_v6_tags, 157 | balancerTag: "tcp_outbound_v6" 158 | }, 159 | { 160 | type: "field", 161 | inboundTag: tproxy_udp_inbound_v6_tags, 162 | balancerTag: "udp_outbound_v6" 163 | }, 164 | { 165 | type: "field", 166 | inboundTag: built_in_tcp_inbounds, 167 | balancerTag: "tcp_outbound_v4" 168 | }, 169 | { 170 | type: "field", 171 | inboundTag: built_in_udp_inbounds, 172 | balancerTag: "udp_outbound_v4" 173 | }, 174 | ]; 175 | if (proxy["tproxy_sniffing"] == "1") { 176 | if (length(secure_domain_rules(proxy)) > 0) { 177 | splice(result, 0, 0, { 178 | type: "field", 179 | inboundTag: [...tproxy_tcp_inbound_v4_tags, ...extra_inbound_global_tcp_tags], 180 | balancerTag: "tcp_outbound_v4", 181 | domain: secure_domain_rules(proxy), 182 | }, { 183 | type: "field", 184 | inboundTag: [...tproxy_udp_inbound_v4_tags, ...extra_inbound_global_udp_tags], 185 | balancerTag: "udp_outbound_v4", 186 | domain: secure_domain_rules(proxy), 187 | }, { 188 | type: "field", 189 | inboundTag: [...tproxy_tcp_inbound_v6_tags], 190 | balancerTag: "tcp_outbound_v6", 191 | domain: secure_domain_rules(proxy), 192 | }, { 193 | type: "field", 194 | inboundTag: [...tproxy_udp_inbound_v6_tags], 195 | balancerTag: "udp_outbound_v6", 196 | domain: secure_domain_rules(proxy), 197 | }); 198 | } 199 | if (length(blocked_domain_rules(proxy)) > 0) { 200 | splice(result, 0, 0, { 201 | type: "field", 202 | inboundTag: [...tproxy_tcp_inbound_v4_tags, ...tproxy_udp_inbound_v4_tags, ...tproxy_tcp_inbound_v6_tags, ...tproxy_udp_inbound_v6_tags, ...extra_inbound_global_tcp_tags, ...extra_inbound_global_udp_tags], 203 | outboundTag: "blackhole_outbound", 204 | domain: blocked_domain_rules(proxy), 205 | }); 206 | } 207 | splice(result, 0, 0, { 208 | type: "field", 209 | inboundTag: [...tproxy_tcp_inbound_v4_tags, ...tproxy_udp_inbound_v4_tags, ...tproxy_tcp_inbound_v6_tags, ...tproxy_udp_inbound_v6_tags, ...extra_inbound_global_tcp_tags, ...extra_inbound_global_udp_tags], 210 | outboundTag: "direct", 211 | domain: fast_domain_rules(proxy) 212 | }); 213 | if (proxy["direct_bittorrent"] == "1") { 214 | splice(result, 0, 0, { 215 | type: "field", 216 | outboundTag: "direct", 217 | protocol: ["bittorrent"] 218 | }); 219 | } 220 | } 221 | return result; 222 | } 223 | 224 | function balancers(proxy, extra_inbound, fakedns) { 225 | const general_balancer_strategy = proxy["general_balancer_strategy"] || "random"; 226 | const built_in_outbounds = ["tcp_outbound_v4", "udp_outbound_v4", "tcp_outbound_v6", "udp_outbound_v6"]; 227 | const built_in_balancers = ["tcp_balancer_v4", "udp_balancer_v4", "tcp_balancer_v6", "udp_balancer_v6"]; 228 | return [ 229 | ...map(built_in_balancers, function (balancer_tag, index) { 230 | return { 231 | "tag": built_in_outbounds[index], 232 | "selector": balancer(proxy, balancer_tag, balancer_tag), 233 | "strategy": { 234 | "type": general_balancer_strategy 235 | } 236 | }; 237 | }), 238 | ...extra_inbound_balancers(extra_inbound), 239 | ...fake_dns_balancers(fakedns), 240 | ]; 241 | }; 242 | 243 | function observatory(proxy, manual_tproxy) { 244 | if (proxy["observatory"] == "1") { 245 | return { 246 | subjectSelector: ["tcp_balancer_v4@balancer_outbound", "udp_balancer_v4@balancer_outbound", "tcp_balancer_v6@balancer_outbound", "udp_balancer_v6@balancer_outbound", "extra_inbound", "fake_dns", "direct", ...manual_tproxy_outbound_tags(manual_tproxy)], 247 | probeInterval: "100ms", 248 | probeUrl: "http://www.apple.com/library/test/success.html" 249 | }; 250 | } 251 | return null; 252 | } 253 | 254 | function gen_config() { 255 | const config = load_config(); 256 | const bridge = filter(values(config), v => v[".type"] == "bridge") || []; 257 | const fakedns = filter(values(config), v => v[".type"] == "fakedns") || []; 258 | const extra_inbound = filter(values(config), v => v[".type"] == "extra_inbound") || []; 259 | const manual_tproxy = filter(values(config), v => v[".type"] == "manual_tproxy") || []; 260 | 261 | const general = filter(values(config), k => k[".type"] == "general")[0] || {}; 262 | const custom_configuration_hook = loadstring(general["custom_configuration_hook"] || "return i => i;")(); 263 | return custom_configuration_hook({ 264 | inbounds: inbounds(general, config, extra_inbound), 265 | outbounds: outbounds(general, config, manual_tproxy, bridge, extra_inbound, fakedns), 266 | dns: dns_conf(general, config, manual_tproxy, fakedns), 267 | fakedns: fake_dns_conf(general), 268 | api: api_conf(general), 269 | metrics: metrics_conf(general), 270 | policy: policy(general), 271 | log: logging(general), 272 | stats: general["stats"] == "1" ? { 273 | place: "holder" 274 | } : null, 275 | observatory: observatory(general, manual_tproxy), 276 | reverse: { 277 | bridges: bridges(bridge) 278 | }, 279 | routing: { 280 | domainStrategy: general["routing_domain_strategy"] || "AsIs", 281 | rules: rules(general, bridge, manual_tproxy, extra_inbound, fakedns), 282 | balancers: balancers(general, extra_inbound, fakedns) 283 | } 284 | }); 285 | } 286 | 287 | print(gen_config()); 288 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/geoip_list.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yichya/luci-app-xray/98a37133e40eaac29440cffcce038066622c754a/core/root/usr/share/xray/geoip_list.pb -------------------------------------------------------------------------------- /core/root/usr/share/xray/ignore_tp_spec_def_gw: -------------------------------------------------------------------------------- 1 | Ignore TP_SPEC_DEF_GW (nftables only). TP_SPEC_DEF_GW is only useful when you have publicly routable IPv4 address on your router. 2 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/protocol/http.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { port_array, stream_settings } from "../common/stream.mjs"; 4 | 5 | export function http_outbound(server, tag) { 6 | const stream_settings_object = stream_settings(server, "http", tag); 7 | const stream_settings_result = stream_settings_object["stream_settings"]; 8 | const dialer_proxy = stream_settings_object["dialer_proxy"]; 9 | let users = null; 10 | if (server["username"] && server["password"]) { 11 | users = [ 12 | { 13 | user: server["username"], 14 | pass: server["password"], 15 | } 16 | ]; 17 | } 18 | return { 19 | outbound: { 20 | protocol: "http", 21 | tag: tag, 22 | settings: { 23 | servers: map(port_array(server["server_port"]), function (v) { 24 | return { 25 | address: server["server"], 26 | port: v, 27 | users: users 28 | }; 29 | }) 30 | }, 31 | streamSettings: stream_settings_result 32 | }, 33 | dialer_proxy: dialer_proxy 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/protocol/shadowsocks.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { port_array, stream_settings } from "../common/stream.mjs"; 4 | 5 | export function shadowsocks_outbound(server, tag) { 6 | const stream_settings_object = stream_settings(server, "shadowsocks", tag); 7 | const stream_settings_result = stream_settings_object["stream_settings"]; 8 | const dialer_proxy = stream_settings_object["dialer_proxy"]; 9 | return { 10 | outbound: { 11 | protocol: "shadowsocks", 12 | tag: tag, 13 | settings: { 14 | servers: map(port_array(server["server_port"]), function (v) { 15 | return { 16 | address: server["server"], 17 | port: v, 18 | email: server["username"], 19 | password: server["password"], 20 | method: server["shadowsocks_security"], 21 | uot: server["shadowsocks_udp_over_tcp"] == '1' 22 | }; 23 | }) 24 | }, 25 | streamSettings: stream_settings_result 26 | }, 27 | dialer_proxy: dialer_proxy 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/protocol/socks.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { port_array, stream_settings } from "../common/stream.mjs"; 4 | 5 | export function socks_outbound(server, tag) { 6 | const stream_settings_object = stream_settings(server, "socks", tag); 7 | const stream_settings_result = stream_settings_object["stream_settings"]; 8 | const dialer_proxy = stream_settings_object["dialer_proxy"]; 9 | let users = null; 10 | if (server["username"] && server["password"]) { 11 | users = [ 12 | { 13 | user: server["username"], 14 | pass: server["password"], 15 | } 16 | ]; 17 | } 18 | return { 19 | outbound: { 20 | protocol: "socks", 21 | tag: tag, 22 | settings: { 23 | servers: map(port_array(server["server_port"]), function (v) { 24 | return { 25 | address: server["server"], 26 | port: v, 27 | users: users 28 | }; 29 | }) 30 | }, 31 | streamSettings: stream_settings_result 32 | }, 33 | dialer_proxy: dialer_proxy 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/protocol/trojan.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { port_array, stream_settings } from "../common/stream.mjs"; 4 | import { fallbacks, tls_inbound_settings } from "../common/tls.mjs"; 5 | 6 | function trojan_inbound_user(k) { 7 | return { 8 | email: k, 9 | password: k, 10 | }; 11 | } 12 | 13 | export function trojan_outbound(server, tag) { 14 | const stream_settings_object = stream_settings(server, "trojan", tag); 15 | const stream_settings_result = stream_settings_object["stream_settings"]; 16 | const dialer_proxy = stream_settings_object["dialer_proxy"]; 17 | return { 18 | outbound: { 19 | protocol: "trojan", 20 | tag: tag, 21 | settings: { 22 | servers: map(port_array(server["server_port"]), function (v) { 23 | return { 24 | address: server["server"], 25 | port: v, 26 | email: server["username"], 27 | password: server["password"] 28 | }; 29 | }) 30 | }, 31 | streamSettings: stream_settings_result 32 | }, 33 | dialer_proxy: dialer_proxy 34 | }; 35 | }; 36 | 37 | export function https_trojan_inbound(proxy, config) { 38 | return { 39 | port: proxy["web_server_port"] || 443, 40 | protocol: "trojan", 41 | tag: "https_inbound", 42 | settings: { 43 | clients: map(proxy["web_server_password"], trojan_inbound_user), 44 | fallbacks: fallbacks(proxy, config) 45 | }, 46 | streamSettings: { 47 | network: "tcp", 48 | security: proxy["trojan_tls"], 49 | tlsSettings: proxy["trojan_tls"] == "tls" ? tls_inbound_settings(proxy, "trojan") : null 50 | } 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/protocol/vless.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { port_array, stream_settings } from "../common/stream.mjs"; 4 | import { fallbacks, reality_inbound_settings, tls_inbound_settings } from "../common/tls.mjs"; 5 | 6 | function vless_inbound_user(k, flow) { 7 | return { 8 | id: k, 9 | email: k, 10 | flow: flow, 11 | }; 12 | } 13 | 14 | export function vless_outbound(server, tag) { 15 | let flow = null; 16 | if (server["vless_tls"] == "tls") { 17 | flow = server["vless_flow_tls"]; 18 | } else if (server["vless_tls"] == "reality") { 19 | flow = server["vless_flow_reality"]; 20 | } 21 | if (flow == "none") { 22 | flow = null; 23 | } 24 | const stream_settings_object = stream_settings(server, "vless", tag); 25 | const stream_settings_result = stream_settings_object["stream_settings"]; 26 | const dialer_proxy = stream_settings_object["dialer_proxy"]; 27 | return { 28 | outbound: { 29 | protocol: "vless", 30 | tag: tag, 31 | settings: { 32 | vnext: map(port_array(server["server_port"]), function (v) { 33 | return { 34 | address: server["server"], 35 | port: v, 36 | users: [ 37 | { 38 | email: server["username"], 39 | id: server["password"], 40 | flow: flow, 41 | encryption: server["vless_encryption"] 42 | } 43 | ] 44 | }; 45 | }) 46 | }, 47 | streamSettings: stream_settings_result 48 | }, 49 | dialer_proxy: dialer_proxy 50 | }; 51 | }; 52 | 53 | export function https_vless_inbound(proxy, config) { 54 | let flow = null; 55 | if (proxy["vless_tls"] == "tls") { 56 | flow = proxy["vless_flow_tls"]; 57 | } else if (proxy["vless_tls"] == "reality") { 58 | flow = proxy["vless_flow_reality"]; 59 | } 60 | if (flow == "none") { 61 | flow = null; 62 | } 63 | return { 64 | port: proxy["web_server_port"] || 443, 65 | protocol: "vless", 66 | tag: "https_inbound", 67 | settings: { 68 | clients: map(proxy["web_server_password"], k => vless_inbound_user(k, flow)), 69 | decryption: "none", 70 | fallbacks: fallbacks(proxy, config) 71 | }, 72 | streamSettings: { 73 | network: "tcp", 74 | security: proxy["vless_tls"], 75 | tlsSettings: proxy["vless_tls"] == "tls" ? tls_inbound_settings(proxy, "vless") : null, 76 | realitySettings: proxy["vless_tls"] == "reality" ? reality_inbound_settings(proxy, "vless") : null, 77 | } 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/protocol/vmess.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { port_array, stream_settings } from "../common/stream.mjs"; 4 | 5 | export function vmess_outbound(server, tag) { 6 | const stream_settings_object = stream_settings(server, "vmess", tag); 7 | const stream_settings_result = stream_settings_object["stream_settings"]; 8 | const dialer_proxy = stream_settings_object["dialer_proxy"]; 9 | return { 10 | outbound: { 11 | protocol: "vmess", 12 | tag: tag, 13 | settings: { 14 | vnext: map(port_array(server["server_port"]), function (v) { 15 | return { 16 | address: server["server"], 17 | port: v, 18 | users: [ 19 | { 20 | email: server["username"], 21 | id: server["password"], 22 | alterId: int(server["alter_id"]), 23 | security: server["vmess_security"] 24 | } 25 | ] 26 | }; 27 | }) 28 | }, 29 | streamSettings: stream_settings_result 30 | }, 31 | dialer_proxy: dialer_proxy 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/restart_dnsmasq_on_iface_change: -------------------------------------------------------------------------------- 1 | If you are using dnsmasq v2.87, keep this file. 2 | See https://thekelleys.org.uk/dnsmasq/CHANGELOG for fixes in version 2.88 about the bug introduced in version 2.87, which could result in DNS servers being removed from the configuration when reloading server configuration. 3 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/rlimit_data_large: -------------------------------------------------------------------------------- 1 | 300000000 333333333 2 | -------------------------------------------------------------------------------- /core/root/usr/share/xray/rlimit_data_small: -------------------------------------------------------------------------------- 1 | 44444444 55555555 -------------------------------------------------------------------------------- /core/root/usr/share/xray/rlimit_nofile_large: -------------------------------------------------------------------------------- 1 | 8192 16384 -------------------------------------------------------------------------------- /core/root/www/luci-static/resources/view/xray/preview.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require form'; 3 | 'require uci'; 4 | 'require view'; 5 | 'require view.xray.shared as shared'; 6 | 7 | return view.extend({ 8 | load: function () { 9 | return uci.load("dhcp"); 10 | }, 11 | 12 | render: function (result) { 13 | const m = new form.Map(shared.variant, _('Xray (preview)'), _("WARNING: These features are experimental, may cause a lot of problems and are not guaranteed to be compatible across minor versions.")); 14 | 15 | let s = m.section(form.TypedSection, 'general'); 16 | s.addremove = false; 17 | s.anonymous = true; 18 | 19 | s.tab("dns_hijack", _("DNS Hijacking")); 20 | 21 | let dnsmasq_integration_mode = s.taboption('dns_hijack', form.ListValue, 'dnsmasq_integration_mode', _('Dnsmasq Integration Mode'), _('Global mode may not work on OpenWrt 24.10 or later; per instance mode is NOT supported on OpenWrt 23.05 or earlier.')); 22 | dnsmasq_integration_mode.value("global", _("Global")); 23 | dnsmasq_integration_mode.value("per_instance", _("Per Instance")); 24 | dnsmasq_integration_mode.default = "global"; 25 | 26 | let dnsmasq_instances = s.taboption('dns_hijack', form.MultiValue, 'dnsmasq_instances', _('Integrated Instances'), _('Select none to disable dnsmasq integration. This could also be used to avoid conflicts with other DNS services, for example
AdGuard Home. Some features like manual transparent proxy with associated domain names still need dnsmasq integration.')); 27 | dnsmasq_instances.depends("dnsmasq_integration_mode", "per_instance"); 28 | for (let i of uci.sections("dhcp", "dnsmasq")) { 29 | dnsmasq_instances.value(i[".name"], function () { 30 | if (i[".anonymous"]) { 31 | return _("Default instance"); 32 | } 33 | return `${_("Instance")} "${i[".name"]}"`; 34 | }()); 35 | } 36 | 37 | let dns_tcp_hijack = s.taboption('dns_hijack', form.Value, 'dns_tcp_hijack', _('Hijack TCP DNS Requests'), _("Redirect all outgoing TCP requests with destination port 53 to the address specified. In most cases not necessary.")); 38 | dns_tcp_hijack.datatype = 'ip4addrport'; 39 | 40 | let dns_udp_hijack = s.taboption('dns_hijack', form.Value, 'dns_udp_hijack', _('Hijack UDP DNS Requests'), _("Redirect all outgoing UDP requests with destination port 53 to the address specified. Recommended to use 127.0.0.1:53.")); 41 | dns_udp_hijack.datatype = 'ip4addrport'; 42 | 43 | s.tab("firewall", _("Extra Firewall Options")); 44 | 45 | let mark = s.taboption('firewall', form.Value, 'mark', _('Socket Mark Number'), _('Avoid proxy loopback problems with local (gateway) traffic')); 46 | mark.datatype = 'range(1, 255)'; 47 | mark.placeholder = 255; 48 | 49 | let firewall_priority = s.taboption('firewall', form.Value, 'firewall_priority', _('Priority for Firewall Rules'), _('See firewall status page for rules Xray used and Netfilter Internal Priority for reference.')); 50 | firewall_priority.datatype = 'range(-49, 49)'; 51 | firewall_priority.placeholder = 10; 52 | 53 | let ttl_override = s.taboption('firewall', form.Value, 'ttl_override', _('Override IPv4 TTL'), _("Strongly not recommended. Only used for some network environments with specific restrictions.")); 54 | ttl_override.datatype = 'uinteger'; 55 | 56 | let hop_limit_override = s.taboption('firewall', form.Value, 'hop_limit_override', _('Override IPv6 Hop Limit'), _("Strongly not recommended. Only used for some network environments with specific restrictions.")); 57 | hop_limit_override.datatype = 'uinteger'; 58 | 59 | let ttl_hop_limit_match = s.taboption('firewall', form.Value, 'ttl_hop_limit_match', _('TTL / Hop Limit Match'), _("Only override TTL / hop limit for packets with specific TTL / hop limit.")); 60 | ttl_hop_limit_match.datatype = 'uinteger'; 61 | 62 | let ttl_override_bypass_ports = s.taboption('firewall', form.DynamicList, 'ttl_override_bypass_ports', _('Ports to bypass TTL override'), _("Do not override TTL for packets with these destination TCP / UDP ports.")); 63 | ttl_override_bypass_ports.datatype = 'port'; 64 | 65 | s.tab("sniffing", _("Sniffing")); 66 | 67 | s.taboption('sniffing', form.Flag, 'tproxy_sniffing', _('Enable Sniffing'), _('Route requests according to domain settings in "DNS Settings" tab in core settings. Deprecated; use FakeDNS instead.')); 68 | 69 | let route_only = s.taboption('sniffing', form.Flag, 'route_only', _('Route Only'), _('Use sniffed domain for routing only but still access through IP. Reduces unnecessary DNS requests. See here for help.')); 70 | route_only.depends("tproxy_sniffing", "1"); 71 | 72 | let direct_bittorrent = s.taboption('sniffing', form.Flag, 'direct_bittorrent', _('Bittorrent Direct'), _("If enabled, no bittorrent request will be forwarded through Xray.")); 73 | direct_bittorrent.depends("tproxy_sniffing", "1"); 74 | 75 | s.tab('dynamic_direct', _('Dynamic Direct')); 76 | 77 | s.taboption('dynamic_direct', form.Flag, 'dynamic_direct_tcp4', _('Enable for IPv4 TCP'), _("This should improve performance with large number of connections.")); 78 | s.taboption('dynamic_direct', form.Flag, 'dynamic_direct_tcp6', _('Enable for IPv4 UDP'), _("This may cause problems but worth a try.")); 79 | s.taboption('dynamic_direct', form.Flag, 'dynamic_direct_udp4', _('Enable for IPv6 TCP'), _("This may not be very useful but it should be good enough for a try.")); 80 | s.taboption('dynamic_direct', form.Flag, 'dynamic_direct_udp6', _('Enable for IPv6 UDP'), _("This may cause problems and is not very useful at the same time. Not recommended.")); 81 | 82 | let dynamic_direct_timeout = s.taboption('dynamic_direct', form.Value, 'dynamic_direct_timeout', _('Dynamic Direct Timeout'), _("Larger value consumes more memory and performs generally better. Unit in seconds.")); 83 | dynamic_direct_timeout.datatype = 'uinteger'; 84 | dynamic_direct_timeout.placeholder = 300; 85 | 86 | s.tab('deprecated', _('Deprecated Features')); 87 | 88 | let socks_port = s.taboption('deprecated', form.Value, 'socks_port', _('Socks5 proxy port'), _("Deprecated for security concerns and will be removed in next major version. Use Extra Inbound instead.")); 89 | socks_port.datatype = 'port'; 90 | socks_port.placeholder = 1080; 91 | 92 | let http_port = s.taboption('deprecated', form.Value, 'http_port', _('HTTP proxy port'), _("Deprecated for security concerns and will be removed in next major version. Use Extra Inbound instead.")); 93 | http_port.datatype = 'port'; 94 | http_port.placeholder = 1081; 95 | 96 | let custom_config = s.taboption('deprecated', form.TextValue, 'custom_config', _('Custom Configurations'), _('See here for help. Deprecated and will be removed in next major version.')); 97 | custom_config.monospace = true; 98 | custom_config.rows = 20; 99 | custom_config.validate = shared.validate_object; 100 | 101 | return m.render(); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /core/root/www/luci-static/resources/view/xray/protocol.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require baseclass'; 3 | 'require form'; 4 | 5 | function fingerprints(o) { 6 | o.value("chrome", "chrome"); 7 | o.value("firefox", "firefox"); 8 | o.value("safari", "safari"); 9 | o.value("ios", "ios"); 10 | o.value("android", "android"); 11 | o.value("edge", "edge"); 12 | o.value("360", "360"); 13 | o.value("qq", "qq"); 14 | o.value("random", "random"); 15 | o.value("randomized", "randomized"); 16 | } 17 | 18 | function add_flow_and_stream_security_conf(s, tab_name, depends_field_name, protocol_name, have_tls_flow, server_side) { 19 | let o = s.taboption(tab_name, form.ListValue, `${protocol_name}_tls`, _(`[${protocol_name}] Stream Security`)); 20 | let odep = {}; 21 | odep[depends_field_name] = protocol_name; 22 | if (server_side) { 23 | odep["web_server_enable"] = "1"; 24 | } else { 25 | o.depends(depends_field_name, protocol_name); 26 | o.value("none", "None"); 27 | } 28 | o.value("tls", "TLS"); 29 | if (have_tls_flow) { 30 | o.value("reality", "REALITY (Experimental)"); 31 | } 32 | o.depends(odep); 33 | o.rmempty = false; 34 | o.modalonly = true; 35 | 36 | if (have_tls_flow) { 37 | let flow_tls = s.taboption(tab_name, form.ListValue, `${protocol_name}_flow_tls`, _(`[${protocol_name}][tls] Flow`)); 38 | let flow_tls_dep = {}; 39 | flow_tls_dep[depends_field_name] = protocol_name; 40 | flow_tls_dep[`${protocol_name}_tls`] = "tls"; 41 | flow_tls.value("none", "none"); 42 | flow_tls.value("xtls-rprx-vision", "xtls-rprx-vision"); 43 | flow_tls.value("xtls-rprx-vision-udp443", "xtls-rprx-vision-udp443"); 44 | if (server_side) { 45 | flow_tls_dep["web_server_enable"] = "1"; 46 | } 47 | flow_tls.depends(flow_tls_dep); 48 | flow_tls.rmempty = false; 49 | flow_tls.modalonly = true; 50 | 51 | let flow_reality = s.taboption(tab_name, form.ListValue, `${protocol_name}_flow_reality`, _(`[${protocol_name}][reality] Flow`)); 52 | let flow_reality_dep = {}; 53 | flow_reality_dep[depends_field_name] = protocol_name; 54 | flow_reality_dep[`${protocol_name}_tls`] = "reality"; 55 | flow_reality.value("none", "none"); 56 | flow_reality.value("xtls-rprx-vision", "xtls-rprx-vision"); 57 | flow_reality.value("xtls-rprx-vision-udp443", "xtls-rprx-vision-udp443"); 58 | if (server_side) { 59 | flow_reality_dep["web_server_enable"] = "1"; 60 | } 61 | flow_reality.depends(flow_reality_dep); 62 | flow_reality.rmempty = false; 63 | flow_reality.modalonly = true; 64 | 65 | o = s.taboption(tab_name, form.Flag, `${protocol_name}_reality_show`, _(`[${protocol_name}][reality] Show`)); 66 | o.depends(`${protocol_name}_tls`, "reality"); 67 | o.modalonly = true; 68 | } 69 | 70 | if (server_side) { 71 | let tls_cert_key_dep = { "web_server_enable": "1" }; 72 | tls_cert_key_dep[`${protocol_name}_tls`] = "tls"; 73 | o = s.taboption(tab_name, form.FileUpload, `${protocol_name}_tls_cert_file`, _(`[${protocol_name}][tls] Certificate File`)); 74 | o.root_directory = "/etc/luci-uploads/xray"; 75 | o.depends(tls_cert_key_dep); 76 | 77 | o = s.taboption(tab_name, form.FileUpload, `${protocol_name}_tls_key_file`, _(`[${protocol_name}][tls] Private Key File`)); 78 | o.root_directory = "/etc/luci-uploads/xray"; 79 | o.depends(tls_cert_key_dep); 80 | 81 | if (have_tls_flow) { 82 | o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_dest`, _(`[${protocol_name}][reality] Dest`)); 83 | o.depends(`${protocol_name}_tls`, "reality"); 84 | o.datatype = "hostport"; 85 | o.modalonly = true; 86 | 87 | o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_xver`, _(`[${protocol_name}][reality] Xver`)); 88 | o.depends(`${protocol_name}_tls`, "reality"); 89 | o.datatype = "integer"; 90 | o.modalonly = true; 91 | 92 | o = s.taboption(tab_name, form.DynamicList, `${protocol_name}_reality_server_names`, _(`[${protocol_name}][reality] Server Names`)); 93 | o.depends(`${protocol_name}_tls`, "reality"); 94 | o.modalonly = true; 95 | 96 | o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_private_key`, _(`[${protocol_name}][reality] Private Key`)); 97 | o.depends(`${protocol_name}_tls`, "reality"); 98 | o.modalonly = true; 99 | 100 | o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_min_client_ver`, _(`[${protocol_name}][reality] Min Client Ver`)); 101 | o.depends(`${protocol_name}_tls`, "reality"); 102 | o.modalonly = true; 103 | 104 | o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_max_client_ver`, _(`[${protocol_name}][reality] Max Client Ver`)); 105 | o.depends(`${protocol_name}_tls`, "reality"); 106 | o.modalonly = true; 107 | 108 | o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_max_time_diff`, _(`[${protocol_name}][reality] Max Time Diff`)); 109 | o.depends(`${protocol_name}_tls`, "reality"); 110 | o.datatype = "integer"; 111 | o.modalonly = true; 112 | 113 | o = s.taboption(tab_name, form.DynamicList, `${protocol_name}_reality_short_ids`, _(`[${protocol_name}][reality] Short Ids`)); 114 | o.depends(`${protocol_name}_tls`, "reality"); 115 | o.modalonly = true; 116 | } 117 | } else { 118 | o = s.taboption(tab_name, form.Value, `${protocol_name}_tls_host`, _(`[${protocol_name}][tls] Server Name`)); 119 | o.depends(`${protocol_name}_tls`, "tls"); 120 | o.modalonly = true; 121 | 122 | o = s.taboption(tab_name, form.Flag, `${protocol_name}_tls_insecure`, _(`[${protocol_name}][tls] Allow Insecure`)); 123 | o.depends(`${protocol_name}_tls`, "tls"); 124 | o.rmempty = false; 125 | o.modalonly = true; 126 | 127 | o = s.taboption(tab_name, form.Value, `${protocol_name}_tls_fingerprint`, _(`[${protocol_name}][tls] Fingerprint`)); 128 | o.depends(`${protocol_name}_tls`, "tls"); 129 | o.value("", "(not set)"); 130 | fingerprints(o); 131 | o.modalonly = true; 132 | 133 | o = s.taboption(tab_name, form.DynamicList, `${protocol_name}_tls_alpn`, _(`[${protocol_name}][tls] ALPN`)); 134 | o.depends(`${protocol_name}_tls`, "tls"); 135 | o.value("h2", "h2"); 136 | o.value("http/1.1", "http/1.1"); 137 | o.modalonly = true; 138 | 139 | if (have_tls_flow) { 140 | o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_fingerprint`, _(`[${protocol_name}][reality] Fingerprint`)); 141 | o.depends(`${protocol_name}_tls`, "reality"); 142 | fingerprints(o); 143 | o.rmempty = false; 144 | o.modalonly = true; 145 | 146 | o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_server_name`, _(`[${protocol_name}][reality] Server Name`)); 147 | o.depends(`${protocol_name}_tls`, "reality"); 148 | o.modalonly = true; 149 | 150 | o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_public_key`, _(`[${protocol_name}][reality] Public Key`)); 151 | o.depends(`${protocol_name}_tls`, "reality"); 152 | o.modalonly = true; 153 | 154 | o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_short_id`, _(`[${protocol_name}][reality] Short Id`)); 155 | o.depends(`${protocol_name}_tls`, "reality"); 156 | o.modalonly = true; 157 | 158 | o = s.taboption(tab_name, form.Value, `${protocol_name}_spider_x`, _(`[${protocol_name}][reality] SpiderX`)); 159 | o.depends(`${protocol_name}_tls`, "reality"); 160 | o.modalonly = true; 161 | } 162 | } 163 | } 164 | 165 | function shadowsocks_client(protocol, sub_section, tab_name) { 166 | protocol.value("shadowsocks", "Shadowsocks"); 167 | 168 | let shadowsocks_security = sub_section.taboption(tab_name, form.ListValue, "shadowsocks_security", _("[shadowsocks] Encrypt Method")); 169 | shadowsocks_security.depends("protocol", "shadowsocks"); 170 | shadowsocks_security.value("none", "none"); 171 | shadowsocks_security.value("aes-256-gcm", "aes-256-gcm"); 172 | shadowsocks_security.value("aes-128-gcm", "aes-128-gcm"); 173 | shadowsocks_security.value("chacha20-poly1305", "chacha20-poly1305"); 174 | shadowsocks_security.value("2022-blake3-aes-128-gcm", "2022-blake3-aes-128-gcm"); 175 | shadowsocks_security.value("2022-blake3-aes-256-gcm", "2022-blake3-aes-256-gcm"); 176 | shadowsocks_security.value("2022-blake3-chacha20-poly1305", "2022-blake3-chacha20-poly1305"); 177 | shadowsocks_security.rmempty = false; 178 | shadowsocks_security.modalonly = true; 179 | 180 | let shadowsocks_udp_over_tcp = sub_section.taboption(tab_name, form.Flag, 'shadowsocks_udp_over_tcp', _('[shadowsocks] UDP over TCP'), _('Only available for shadowsocks-2022 ciphers (2022-*)')); 181 | shadowsocks_udp_over_tcp.depends("shadowsocks_security", /2022/); 182 | shadowsocks_udp_over_tcp.rmempty = false; 183 | shadowsocks_udp_over_tcp.modalonly = true; 184 | 185 | add_flow_and_stream_security_conf(sub_section, tab_name, "protocol", "shadowsocks", false, false); 186 | } 187 | 188 | function vmess_client(protocol, sub_section, tab_name) { 189 | protocol.value("vmess", "VMess"); 190 | 191 | let vmess_security = sub_section.taboption(tab_name, form.ListValue, "vmess_security", _("[vmess] Encrypt Method")); 192 | vmess_security.depends("protocol", "vmess"); 193 | vmess_security.value("none", "none"); 194 | vmess_security.value("auto", "auto"); 195 | vmess_security.value("aes-128-gcm", "aes-128-gcm"); 196 | vmess_security.value("chacha20-poly1305", "chacha20-poly1305"); 197 | vmess_security.rmempty = false; 198 | vmess_security.modalonly = true; 199 | 200 | let vmess_alter_id = sub_section.taboption(tab_name, form.ListValue, "vmess_alter_id", _("[vmess] AlterId"), _("Deprecated. Make sure you always use VMessAEAD.")); 201 | vmess_alter_id.depends("protocol", "vmess"); 202 | vmess_alter_id.value(0, "0 (this enables VMessAEAD)"); 203 | vmess_alter_id.value(1, "1"); 204 | vmess_alter_id.value(4, "4"); 205 | vmess_alter_id.value(16, "16"); 206 | vmess_alter_id.value(64, "64"); 207 | vmess_alter_id.value(256, "256"); 208 | vmess_alter_id.rmempty = false; 209 | vmess_alter_id.modalonly = true; 210 | 211 | add_flow_and_stream_security_conf(sub_section, tab_name, "protocol", "vmess", false, false); 212 | } 213 | 214 | function vless_client(protocol, sub_section, tab_name) { 215 | protocol.value("vless", "VLESS"); 216 | 217 | let vless_encryption = sub_section.taboption(tab_name, form.ListValue, "vless_encryption", _("[vless] Encrypt Method")); 218 | vless_encryption.depends("protocol", "vless"); 219 | vless_encryption.value("none", "none"); 220 | vless_encryption.rmempty = false; 221 | vless_encryption.modalonly = true; 222 | 223 | add_flow_and_stream_security_conf(sub_section, tab_name, "protocol", "vless", true, false); 224 | } 225 | 226 | function socks_client(protocol, sub_section, tab_name) { 227 | protocol.value("socks", "SOCKS"); 228 | add_flow_and_stream_security_conf(sub_section, tab_name, "protocol", "socks", false, false); 229 | } 230 | 231 | function http_client(protocol, sub_section, tab_name) { 232 | protocol.value("http", "HTTP"); 233 | add_flow_and_stream_security_conf(sub_section, tab_name, "protocol", "http", false, false); 234 | } 235 | 236 | function vless_server(protocol, section, tab_name) { 237 | protocol.value("vless", "VLESS"); 238 | add_flow_and_stream_security_conf(section, tab_name, "web_server_protocol", "vless", true, true); 239 | } 240 | 241 | function trojan_client(protocol, sub_section, tab_name) { 242 | protocol.value("trojan", "Trojan"); 243 | add_flow_and_stream_security_conf(sub_section, tab_name, "protocol", "trojan", false, false); 244 | } 245 | 246 | function trojan_server(protocol, section, tab_name) { 247 | protocol.value("trojan", "Trojan"); 248 | add_flow_and_stream_security_conf(section, tab_name, "web_server_protocol", "trojan", false, true); 249 | } 250 | 251 | return baseclass.extend({ 252 | add_client_protocol: function (protocol, sub_section, tab_name) { 253 | vmess_client(protocol, sub_section, tab_name); 254 | vless_client(protocol, sub_section, tab_name); 255 | trojan_client(protocol, sub_section, tab_name); 256 | shadowsocks_client(protocol, sub_section, tab_name); 257 | http_client(protocol, sub_section, tab_name); 258 | socks_client(protocol, sub_section, tab_name); 259 | }, 260 | add_server_protocol: function (protocol, section, tab_name) { 261 | vless_server(protocol, section, tab_name); 262 | trojan_server(protocol, section, tab_name); 263 | }, 264 | }); 265 | -------------------------------------------------------------------------------- /core/root/www/luci-static/resources/view/xray/shared.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require baseclass'; 3 | 'require uci'; 4 | 5 | const variant = "xray_core"; 6 | 7 | function badge(text, tooltip) { 8 | let options = { 'class': 'ifacebadge' }; 9 | if (tooltip) { 10 | options["data-tooltip"] = tooltip; 11 | } 12 | return E('span', options, text); 13 | } 14 | 15 | return baseclass.extend({ 16 | badge: badge, 17 | validate_object: function (id, a) { 18 | if (a == "") { 19 | return true; 20 | } 21 | try { 22 | const t = JSON.parse(a); 23 | if (Array.isArray(t)) { 24 | return "TypeError: Requires an object here, got an array"; 25 | } 26 | if (t instanceof Object) { 27 | return true; 28 | } 29 | return "TypeError: Requires an object here, got a " + typeof t; 30 | } catch (e) { 31 | return e; 32 | } 33 | }, 34 | variant: variant 35 | }); 36 | -------------------------------------------------------------------------------- /core/root/www/luci-static/resources/view/xray/transport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require baseclass'; 3 | 'require form'; 4 | 5 | function transport_tcp(transport, sub_section, tab_name) { 6 | transport.value("tcp", "TCP"); 7 | 8 | let tcp_guise = sub_section.taboption(tab_name, form.ListValue, "tcp_guise", _("[tcp] Fake Header Type")); 9 | tcp_guise.depends("transport", "tcp"); 10 | tcp_guise.value("none", _("None")); 11 | tcp_guise.value("http", "HTTP"); 12 | tcp_guise.modalonly = true; 13 | 14 | let http_host = sub_section.taboption(tab_name, form.DynamicList, "http_host", _("[tcp][fake_http] Host")); 15 | http_host.depends("tcp_guise", "http"); 16 | http_host.rmempty = false; 17 | http_host.modalonly = true; 18 | 19 | let http_path = sub_section.taboption(tab_name, form.DynamicList, "http_path", _("[tcp][fake_http] Path")); 20 | http_path.depends("tcp_guise", "http"); 21 | http_path.modalonly = true; 22 | } 23 | 24 | function transport_mkcp(transport, sub_section, tab_name) { 25 | transport.value("mkcp", "mKCP"); 26 | 27 | let mkcp_guise = sub_section.taboption(tab_name, form.ListValue, "mkcp_guise", _("[mkcp] Fake Header Type")); 28 | mkcp_guise.depends("transport", "mkcp"); 29 | mkcp_guise.value("none", _("None")); 30 | mkcp_guise.value("srtp", _("VideoCall (SRTP)")); 31 | mkcp_guise.value("utp", _("BitTorrent (uTP)")); 32 | mkcp_guise.value("wechat-video", _("WechatVideo")); 33 | mkcp_guise.value("dtls", "DTLS 1.2"); 34 | mkcp_guise.value("wireguard", "WireGuard"); 35 | mkcp_guise.modalonly = true; 36 | 37 | let mkcp_mtu = sub_section.taboption(tab_name, form.Value, "mkcp_mtu", _("[mkcp] Maximum Transmission Unit")); 38 | mkcp_mtu.datatype = "uinteger"; 39 | mkcp_mtu.depends("transport", "mkcp"); 40 | mkcp_mtu.placeholder = 1350; 41 | mkcp_mtu.modalonly = true; 42 | 43 | let mkcp_tti = sub_section.taboption(tab_name, form.Value, "mkcp_tti", _("[mkcp] Transmission Time Interval")); 44 | mkcp_tti.datatype = "uinteger"; 45 | mkcp_tti.depends("transport", "mkcp"); 46 | mkcp_tti.placeholder = 50; 47 | mkcp_tti.modalonly = true; 48 | 49 | let mkcp_uplink_capacity = sub_section.taboption(tab_name, form.Value, "mkcp_uplink_capacity", _("[mkcp] Uplink Capacity")); 50 | mkcp_uplink_capacity.datatype = "uinteger"; 51 | mkcp_uplink_capacity.depends("transport", "mkcp"); 52 | mkcp_uplink_capacity.placeholder = 5; 53 | mkcp_uplink_capacity.modalonly = true; 54 | 55 | let mkcp_downlink_capacity = sub_section.taboption(tab_name, form.Value, "mkcp_downlink_capacity", _("[mkcp] Downlink Capacity")); 56 | mkcp_downlink_capacity.datatype = "uinteger"; 57 | mkcp_downlink_capacity.depends("transport", "mkcp"); 58 | mkcp_downlink_capacity.placeholder = 20; 59 | mkcp_downlink_capacity.modalonly = true; 60 | 61 | let mkcp_read_buffer_size = sub_section.taboption(tab_name, form.Value, "mkcp_read_buffer_size", _("[mkcp] Read Buffer Size")); 62 | mkcp_read_buffer_size.datatype = "uinteger"; 63 | mkcp_read_buffer_size.depends("transport", "mkcp"); 64 | mkcp_read_buffer_size.placeholder = 2; 65 | mkcp_read_buffer_size.modalonly = true; 66 | 67 | let mkcp_write_buffer_size = sub_section.taboption(tab_name, form.Value, "mkcp_write_buffer_size", _("[mkcp] Write Buffer Size")); 68 | mkcp_write_buffer_size.datatype = "uinteger"; 69 | mkcp_write_buffer_size.depends("transport", "mkcp"); 70 | mkcp_write_buffer_size.placeholder = 2; 71 | mkcp_write_buffer_size.modalonly = true; 72 | 73 | let mkcp_congestion = sub_section.taboption(tab_name, form.Flag, "mkcp_congestion", _("[mkcp] Congestion Control")); 74 | mkcp_congestion.depends("transport", "mkcp"); 75 | mkcp_congestion.modalonly = true; 76 | 77 | let mkcp_seed = sub_section.taboption(tab_name, form.Value, "mkcp_seed", _("[mkcp] Seed")); 78 | mkcp_seed.depends("transport", "mkcp"); 79 | mkcp_seed.modalonly = true; 80 | } 81 | 82 | function transport_ws(transport, sub_section, tab_name) { 83 | transport.value("ws", "WebSocket"); 84 | 85 | let ws_host = sub_section.taboption(tab_name, form.Value, "ws_host", _("[websocket] Host")); 86 | ws_host.depends("transport", "ws"); 87 | ws_host.modalonly = true; 88 | 89 | let ws_path = sub_section.taboption(tab_name, form.Value, "ws_path", _("[websocket] Path")); 90 | ws_path.depends("transport", "ws"); 91 | ws_path.modalonly = true; 92 | } 93 | 94 | function transport_h2(transport, sub_section, tab_name) { 95 | transport.value("h2", "HTTP/2"); 96 | 97 | let h2_host = sub_section.taboption(tab_name, form.DynamicList, "h2_host", _("[http2] Host")); 98 | h2_host.depends("transport", "h2"); 99 | h2_host.modalonly = true; 100 | 101 | let h2_path = sub_section.taboption(tab_name, form.Value, "h2_path", _("[http2] Path")); 102 | h2_path.depends("transport", "h2"); 103 | h2_path.modalonly = true; 104 | 105 | let h2_health_check = sub_section.taboption(tab_name, form.Flag, "h2_health_check", _("[h2] Health Check")); 106 | h2_health_check.depends("transport", "h2"); 107 | h2_health_check.modalonly = true; 108 | 109 | let h2_read_idle_timeout = sub_section.taboption(tab_name, form.Value, "h2_read_idle_timeout", _("[h2] Read Idle Timeout")); 110 | h2_read_idle_timeout.depends({ "transport": "h2", "h2_health_check": "1" }); 111 | h2_read_idle_timeout.modalonly = true; 112 | h2_read_idle_timeout.placeholder = 10; 113 | h2_read_idle_timeout.datatype = 'integer'; 114 | 115 | let h2_health_check_timeout = sub_section.taboption(tab_name, form.Value, "h2_health_check_timeout", _("[h2] Health Check Timeout")); 116 | h2_health_check_timeout.depends({ "transport": "h2", "h2_health_check": "1" }); 117 | h2_health_check_timeout.modalonly = true; 118 | h2_health_check_timeout.placeholder = 20; 119 | h2_health_check_timeout.datatype = 'integer'; 120 | } 121 | 122 | function transport_quic(transport, sub_section, tab_name) { 123 | transport.value("quic", "QUIC"); 124 | 125 | let quic_security = sub_section.taboption(tab_name, form.ListValue, "quic_security", _("[quic] Security")); 126 | quic_security.depends("transport", "quic"); 127 | quic_security.value("none", "none"); 128 | quic_security.value("aes-128-gcm", "aes-128-gcm"); 129 | quic_security.value("chacha20-poly1305", "chacha20-poly1305"); 130 | quic_security.rmempty = false; 131 | quic_security.modalonly = true; 132 | 133 | let quic_key = sub_section.taboption(tab_name, form.Value, "quic_key", _("[quic] Key")); 134 | quic_key.depends("transport", "quic"); 135 | quic_key.modalonly = true; 136 | 137 | let quic_guise = sub_section.taboption(tab_name, form.ListValue, "quic_guise", _("[quic] Fake Header Type")); 138 | quic_guise.depends("transport", "quic"); 139 | quic_guise.value("none", _("None")); 140 | quic_guise.value("srtp", _("VideoCall (SRTP)")); 141 | quic_guise.value("utp", _("BitTorrent (uTP)")); 142 | quic_guise.value("wechat-video", _("WechatVideo")); 143 | quic_guise.value("dtls", "DTLS 1.2"); 144 | quic_guise.value("wireguard", "WireGuard"); 145 | quic_guise.default = "none"; 146 | quic_guise.modalonly = true; 147 | } 148 | 149 | function transport_grpc(transport, sub_section, tab_name) { 150 | transport.value("grpc", "gRPC"); 151 | 152 | let grpc_service_name = sub_section.taboption(tab_name, form.Value, "grpc_service_name", _("[grpc] Service Name")); 153 | grpc_service_name.depends("transport", "grpc"); 154 | grpc_service_name.modalonly = true; 155 | 156 | let grpc_multi_mode = sub_section.taboption(tab_name, form.Flag, "grpc_multi_mode", _("[grpc] Multi Mode")); 157 | grpc_multi_mode.depends("transport", "grpc"); 158 | grpc_multi_mode.modalonly = true; 159 | 160 | let grpc_health_check = sub_section.taboption(tab_name, form.Flag, "grpc_health_check", _("[grpc] Health Check")); 161 | grpc_health_check.depends("transport", "grpc"); 162 | grpc_health_check.modalonly = true; 163 | 164 | let grpc_idle_timeout = sub_section.taboption(tab_name, form.Value, "grpc_idle_timeout", _("[grpc] Idle Timeout")); 165 | grpc_idle_timeout.depends({ "transport": "grpc", "grpc_health_check": "1" }); 166 | grpc_idle_timeout.modalonly = true; 167 | grpc_idle_timeout.placeholder = 10; 168 | grpc_idle_timeout.datatype = 'integer'; 169 | 170 | let grpc_health_check_timeout = sub_section.taboption(tab_name, form.Value, "grpc_health_check_timeout", _("[grpc] Health Check Timeout")); 171 | grpc_health_check_timeout.depends({ "transport": "grpc", "grpc_health_check": "1" }); 172 | grpc_health_check_timeout.modalonly = true; 173 | grpc_health_check_timeout.placeholder = 20; 174 | grpc_health_check_timeout.datatype = 'integer'; 175 | 176 | let grpc_permit_without_stream = sub_section.taboption(tab_name, form.Flag, "grpc_permit_without_stream", _("[grpc] Permit Without Stream")); 177 | grpc_permit_without_stream.depends({ "transport": "grpc", "grpc_health_check": "1" }); 178 | grpc_permit_without_stream.modalonly = true; 179 | 180 | let grpc_initial_windows_size = sub_section.taboption(tab_name, form.Value, "grpc_initial_windows_size", _("[grpc] Initial Windows Size"), _("Set to 524288 to avoid Cloudflare sending ENHANCE_YOUR_CALM.")); 181 | grpc_initial_windows_size.depends("transport", "grpc"); 182 | grpc_initial_windows_size.modalonly = true; 183 | grpc_initial_windows_size.placeholder = 0; 184 | grpc_initial_windows_size.datatype = 'integer'; 185 | } 186 | 187 | function transport_httpupgrade(transport, sub_section, tab_name) { 188 | transport.value("httpupgrade", "HTTPUpgrade"); 189 | 190 | let httpupgrade_host = sub_section.taboption(tab_name, form.Value, "httpupgrade_host", _("[httpupgrade] Host")); 191 | httpupgrade_host.depends("transport", "httpupgrade"); 192 | httpupgrade_host.modalonly = true; 193 | 194 | let httpupgrade_path = sub_section.taboption(tab_name, form.Value, "httpupgrade_path", _("[httpupgrade] Path")); 195 | httpupgrade_path.depends("transport", "httpupgrade"); 196 | httpupgrade_path.modalonly = true; 197 | } 198 | 199 | function transport_splithttp(transport, sub_section, tab_name) { 200 | transport.value("splithttp", "SplitHTTP"); 201 | 202 | let splithttp_host = sub_section.taboption(tab_name, form.Value, "splithttp_host", _("[splithttp] Host")); 203 | splithttp_host.depends("transport", "splithttp"); 204 | splithttp_host.modalonly = true; 205 | 206 | let splithttp_path = sub_section.taboption(tab_name, form.Value, "splithttp_path", _("[splithttp] Path")); 207 | splithttp_path.depends("transport", "splithttp"); 208 | splithttp_path.modalonly = true; 209 | } 210 | 211 | return baseclass.extend({ 212 | init: function (transport, sub_section, tab_name) { 213 | transport_tcp(transport, sub_section, tab_name); 214 | transport_mkcp(transport, sub_section, tab_name); 215 | transport_ws(transport, sub_section, tab_name); 216 | transport_h2(transport, sub_section, tab_name); 217 | transport_quic(transport, sub_section, tab_name); 218 | transport_grpc(transport, sub_section, tab_name); 219 | transport_splithttp(transport, sub_section, tab_name); 220 | transport_httpupgrade(transport, sub_section, tab_name); 221 | } 222 | }); 223 | -------------------------------------------------------------------------------- /geodata/Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | PKG_NAME:=luci-app-xray-geodata 4 | PKG_VERSION:=3.6.0 5 | PKG_RELEASE:=1 6 | 7 | PKG_LICENSE:=MPLv2 8 | PKG_LICENSE_FILES:=LICENSE 9 | PKG_MAINTAINER:=yichya 10 | PKG_BUILD_PARALLEL:=1 11 | 12 | include $(INCLUDE_DIR)/package.mk 13 | 14 | define Package/$(PKG_NAME) 15 | SECTION:=Custom 16 | CATEGORY:=Extra packages 17 | TITLE:=LuCI Support for Xray (geodata page) 18 | DEPENDS:=luci-app-xray +xray-geodata 19 | PKGARCH:=all 20 | endef 21 | 22 | define Package/$(PKG_NAME)/description 23 | LuCI Support for Xray (Client-side Rendered) (geodata page). 24 | endef 25 | 26 | define Build/Compile 27 | echo "luci-app-xray $(PKG_VERSION)-$(PKG_RELEASE) `git rev-parse HEAD` `date +%s`" > $(PKG_BUILD_DIR)/version.txt 28 | endef 29 | 30 | define Package/$(PKG_NAME)/install 31 | $(INSTALL_DIR) $(1)/www/luci-static/resources/view/xray 32 | $(INSTALL_DATA) ./root/www/luci-static/resources/view/xray/geodata.js $(1)/www/luci-static/resources/view/xray/geodata.js 33 | $(INSTALL_DIR) $(1)/usr/share/luci/menu.d 34 | $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-xray-geodata.json $(1)/usr/share/luci/menu.d/luci-app-xray-geodata.json 35 | $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d 36 | $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-xray-geodata.json $(1)/usr/share/rpcd/acl.d/luci-app-xray-geodata.json 37 | $(INSTALL_DIR) $(1)/www/xray 38 | $(LN) /usr/share/xray/geoip.dat $(1)/www/xray/geoip.dat 39 | $(LN) /usr/share/xray/geosite.dat $(1)/www/xray/geosite.dat 40 | endef 41 | 42 | $(eval $(call BuildPackage,$(PKG_NAME))) 43 | -------------------------------------------------------------------------------- /geodata/root/usr/share/luci/menu.d/luci-app-xray-geodata.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin/services/xray_geodata": { 3 | "title": "Xray (geodata)", 4 | "action": { 5 | "type": "view", 6 | "path": "xray/geodata" 7 | }, 8 | "depends": { 9 | "acl": [ 10 | "luci-app-xray-geodata" 11 | ], 12 | "uci": { 13 | "xray_core": true 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /geodata/root/usr/share/rpcd/acl.d/luci-app-xray-geodata.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-xray-geodata": { 3 | "description": "Grant access to xray configurations", 4 | "read": { 5 | "uci": [ 6 | "xray_core" 7 | ] 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /geodata/root/www/luci-static/resources/view/xray/geodata.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require dom'; 3 | 'require uci'; 4 | 'require ui'; 5 | 'require view'; 6 | 'require view.xray.shared as shared'; 7 | 8 | const maxResults = 2048; 9 | 10 | const WireType = { 11 | VARINT: 0, 12 | FIXED64: 1, 13 | LENGTH_DELIMITED: 2, 14 | FIXED32: 5 15 | }; 16 | 17 | class ProtoReader { 18 | constructor(buffer) { 19 | this.buffer = new Uint8Array(buffer); 20 | this.pos = 0; 21 | } 22 | 23 | readVarint() { 24 | let result = 0; 25 | let shift = 0; 26 | 27 | while (this.pos < this.buffer.length) { 28 | const byte = this.buffer[this.pos++]; 29 | result |= (byte & 0x7F) << shift; 30 | if ((byte & 0x80) === 0) { 31 | return result; 32 | } 33 | shift += 7; 34 | } 35 | throw new Error('Malformed varint'); 36 | } 37 | 38 | readString() { 39 | const length = this.readVarint(); 40 | const value = new TextDecoder().decode( 41 | this.buffer.slice(this.pos, this.pos + length) 42 | ); 43 | this.pos += length; 44 | return value; 45 | } 46 | 47 | readBytes() { 48 | const length = this.readVarint(); 49 | const bytes = this.buffer.slice(this.pos, this.pos + length); 50 | this.pos += length; 51 | return bytes; 52 | } 53 | 54 | readTag() { 55 | const tag = this.readVarint(); 56 | return { 57 | fieldNumber: tag >>> 3, 58 | wireType: tag & 0x7 59 | }; 60 | } 61 | } 62 | 63 | // Domain Type enum 64 | const DomainType = { 65 | Plain: 0, 66 | Regex: 1, 67 | Domain: 2, 68 | Full: 3 69 | }; 70 | 71 | function decodeDomainAttribute(reader) { 72 | const attribute = { 73 | key: '', 74 | typedValue: null 75 | }; 76 | 77 | while (reader.pos < reader.buffer.length) { 78 | const tag = reader.readTag(); 79 | 80 | switch (tag.fieldNumber) { 81 | case 1: // key 82 | attribute.key = reader.readString(); 83 | break; 84 | case 2: // bool_value 85 | attribute.typedValue = reader.readVarint() !== 0; 86 | break; 87 | case 3: // int_value 88 | attribute.typedValue = reader.readVarint(); 89 | break; 90 | default: 91 | throw new Error(`Unknown field number: ${tag.fieldNumber}`); 92 | } 93 | } 94 | 95 | return attribute; 96 | } 97 | 98 | function decodeDomain(reader) { 99 | const domain = { 100 | type: DomainType.Plain, 101 | value: '', 102 | attribute: [] 103 | }; 104 | 105 | while (reader.pos < reader.buffer.length) { 106 | const tag = reader.readTag(); 107 | 108 | switch (tag.fieldNumber) { 109 | case 1: // type 110 | domain.type = reader.readVarint(); 111 | break; 112 | case 2: // value 113 | domain.value = reader.readString(); 114 | break; 115 | case 3: // attribute 116 | const attrBytes = reader.readBytes(); 117 | domain.attribute.push( 118 | decodeDomainAttribute(new ProtoReader(attrBytes)) 119 | ); 120 | break; 121 | default: 122 | throw new Error(`Unknown field number: ${tag.fieldNumber}`); 123 | } 124 | } 125 | 126 | return domain; 127 | } 128 | 129 | function decodeCIDR(reader) { 130 | const cidr = { 131 | ip: new Uint8Array(), 132 | prefix: 0 133 | }; 134 | 135 | while (reader.pos < reader.buffer.length) { 136 | const tag = reader.readTag(); 137 | 138 | switch (tag.fieldNumber) { 139 | case 1: // ip 140 | cidr.ip = reader.readBytes(); 141 | break; 142 | case 2: // prefix 143 | cidr.prefix = reader.readVarint(); 144 | break; 145 | default: 146 | throw new Error(`Unknown field number: ${tag.fieldNumber}`); 147 | } 148 | } 149 | 150 | return cidr; 151 | } 152 | 153 | function decodeGeoIP(reader) { 154 | const geoIP = { 155 | countryCode: '', 156 | cidr: [], 157 | reverseMatch: false 158 | }; 159 | 160 | while (reader.pos < reader.buffer.length) { 161 | const tag = reader.readTag(); 162 | 163 | switch (tag.fieldNumber) { 164 | case 1: // country_code 165 | geoIP.countryCode = reader.readString(); 166 | break; 167 | case 2: // cidr 168 | const cidrBytes = reader.readBytes(); 169 | geoIP.cidr.push( 170 | decodeCIDR(new ProtoReader(cidrBytes)) 171 | ); 172 | break; 173 | case 3: // reverse_match 174 | geoIP.reverseMatch = reader.readVarint() !== 0; 175 | break; 176 | default: 177 | throw new Error(`Unknown field number: ${tag.fieldNumber}`); 178 | } 179 | } 180 | 181 | return geoIP; 182 | } 183 | 184 | function decodeGeoSite(reader) { 185 | const geoSite = { 186 | countryCode: '', 187 | domain: [] 188 | }; 189 | 190 | while (reader.pos < reader.buffer.length) { 191 | const tag = reader.readTag(); 192 | 193 | switch (tag.fieldNumber) { 194 | case 1: // country_code 195 | geoSite.countryCode = reader.readString(); 196 | break; 197 | case 2: // domain 198 | const domainBytes = reader.readBytes(); 199 | geoSite.domain.push( 200 | decodeDomain(new ProtoReader(domainBytes)) 201 | ); 202 | break; 203 | default: 204 | throw new Error(`Unknown field number: ${tag.fieldNumber}`); 205 | } 206 | } 207 | 208 | return geoSite; 209 | } 210 | 211 | function decodeGeoSiteList(buffer) { 212 | const reader = new ProtoReader(buffer); 213 | const geoSiteList = { 214 | entry: [] 215 | }; 216 | 217 | while (reader.pos < reader.buffer.length) { 218 | const tag = reader.readTag(); 219 | 220 | if (tag.fieldNumber === 1) { // entry 221 | const entryBytes = reader.readBytes(); 222 | geoSiteList.entry.push( 223 | decodeGeoSite(new ProtoReader(entryBytes)) 224 | ); 225 | } else { 226 | throw new Error(`Unknown field number: ${tag.fieldNumber}`); 227 | } 228 | } 229 | 230 | return geoSiteList; 231 | } 232 | 233 | function decodeGeoIPList(buffer) { 234 | const reader = new ProtoReader(buffer); 235 | const geoIPList = { 236 | entry: [] 237 | }; 238 | 239 | while (reader.pos < reader.buffer.length) { 240 | const tag = reader.readTag(); 241 | 242 | if (tag.fieldNumber === 1) { // entry 243 | const entryBytes = reader.readBytes(); 244 | geoIPList.entry.push( 245 | decodeGeoIP(new ProtoReader(entryBytes)) 246 | ); 247 | } else { 248 | throw new Error(`Unknown field number: ${tag.fieldNumber}`); 249 | } 250 | } 251 | 252 | return geoIPList; 253 | } 254 | 255 | function matchesCIDR(cidrIp, prefix, queryIp) { 256 | // Check if input IP matches CIDR IP version 257 | if ((cidrIp.length === 4 && queryIp.length !== 4) || (cidrIp.length === 16 && queryIp.length !== 16)) { 258 | return false; 259 | } 260 | 261 | // Compare full bytes first 262 | const prefixBytes = Math.floor(prefix / 8); 263 | for (let i = 0; i < prefixBytes; i++) { 264 | if (cidrIp[i] !== queryIp[i]) return false; 265 | } 266 | 267 | // Compare remaining bits if any 268 | const remainingBits = prefix % 8; 269 | if (remainingBits > 0) { 270 | const mask = 0xFF << (8 - remainingBits); 271 | return (cidrIp[prefixBytes] & mask) === (queryIp[prefixBytes] & mask); 272 | } 273 | 274 | return true; 275 | } 276 | 277 | function renderGeoIPResults(results) { 278 | const container = document.getElementById('geoip-results'); 279 | container.innerHTML = '
'; 280 | 281 | const flatResults = results.flatMap(entry => entry.cidr.map(cidr => ({ entry, cidr }))); 282 | 283 | const table = E('table', { 'class': 'table' }, [ 284 | E('thead', {}, [ 285 | E('tr', { 'class': 'tr table-titles' }, [ 286 | E('th', { 'class': 'th' }, _('Country Code')), 287 | E('th', { 'class': 'th' }, _(`CIDR (${flatResults.length} total)`)) 288 | ]) 289 | ]), 290 | E('tbody', {}, flatResults.slice(0, maxResults).map(({ entry, cidr }, index) => { 291 | const ip = cidr.ip.length === 4 ? Array.from(cidr.ip).join('.') : Array.from(cidr.ip).reduce((arr, byte, i) => { 292 | if (i % 2 === 0) { 293 | arr.push((byte << 8) | cidr.ip[i + 1]); 294 | } 295 | return arr; 296 | }, []).map(part => part.toString(16).padStart(4, '0')).join(':').replace(/\b(?:0+:){2,}/, ':').split(':').map(octet => octet.replace(/\b0+/g, '')).join(':'); 297 | return E('tr', { 'class': `tr cbi-rowstyle-${index % 2 + 1}` }, [ 298 | E('td', { 'class': 'td' }, entry.countryCode), 299 | E('td', { 'class': 'td' }, `${ip}/${cidr.prefix}`) 300 | ]); 301 | })) 302 | ]); 303 | 304 | container.appendChild(table); 305 | } 306 | 307 | function renderGeoSiteResults(results) { 308 | const container = document.getElementById('geosite-results'); 309 | container.innerHTML = '
'; 310 | 311 | const flatResults = results.flatMap(entry => entry.domain.map(domain => ({ entry, domain }))); 312 | 313 | const table = E('table', { 'class': 'table' }, [ 314 | E('thead', {}, [ 315 | E('tr', { 'class': 'tr table-titles' }, [ 316 | E('th', { 'class': 'th' }, _('Country Code')), 317 | E('th', { 'class': 'th' }, _(`Domain (${flatResults.length} total)`)) 318 | ]) 319 | ]), 320 | E('tbody', {}, flatResults.slice(0, maxResults).map(({ entry, domain }, index) => 321 | E('tr', { 'class': `tr cbi-rowstyle-${index % 2 + 1}` }, [ 322 | E('td', { 'class': 'td' }, entry.countryCode), 323 | E('td', { 'class': 'td' }, domain.value) 324 | ]) 325 | )) 326 | ]); 327 | 328 | container.appendChild(table); 329 | } 330 | 331 | return view.extend({ 332 | load: function () { 333 | return Promise.all([ 334 | new Date(), 335 | uci.load(shared.variant), 336 | fetch("/xray/geoip.dat").then(v => v.arrayBuffer()), 337 | fetch("/xray/geosite.dat").then(v => v.arrayBuffer()), 338 | ]); 339 | }, 340 | 341 | render: function (load_result) { 342 | const geoip_result = decodeGeoIPList(load_result[2]); 343 | const geosite_result = decodeGeoSiteList(load_result[3]); 344 | const result = E([], {}, [ 345 | E('div', {}, [ 346 | E('div', { 'class': 'cbi-section', 'data-tab': 'geoip', 'data-tab-title': _('GeoIP') }, [ 347 | E('select', { 348 | 'id': 'geoip-select', 349 | 'change': function () { 350 | const selectedCode = document.getElementById('geoip-select').value; 351 | const results = selectedCode ? geoip_result.entry.filter(entry => entry.countryCode === selectedCode) : []; 352 | renderGeoIPResults(results); 353 | } 354 | }, [ 355 | E('option', { 'value': '' }, _('Filter GeoIP by Country Code')), 356 | ...Array.from(new Set(geoip_result.entry.map(entry => entry.countryCode))) 357 | .sort() 358 | .map(code => { 359 | const count = geoip_result.entry 360 | .find(entry => entry.countryCode === code) 361 | .cidr.length; 362 | return E('option', { 'value': code }, `${code} (${count} items)`); 363 | }) 364 | ]), 365 | E('div', { 'class': 'cbi-section-create' }, [ 366 | E('input', { 367 | 'type': 'text', 368 | 'id': 'geoip-search', 369 | 'class': 'cbi-input-text', 370 | 'placeholder': _('Search GeoIP...') 371 | }), 372 | E('button', { 373 | 'class': 'cbi-button', 374 | 'click': function () { 375 | const query = document.getElementById('geoip-search').value.trim(); 376 | let queryIp = null; 377 | if (query.includes('.')) { 378 | queryIp = query.split('.').map(Number); 379 | } else if (query.includes(':')) { 380 | queryIp = query.split(':').reduce((acc, part, i, arr) => { 381 | if (part === '') { 382 | const padding = new Array((8 - arr.filter(x => x !== '').length) * 2).fill(0); 383 | return acc.concat(padding); 384 | } 385 | const hex = part.padStart(4, '0'); 386 | return acc.concat([parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16)]); 387 | }, []); 388 | console.log(queryIp); 389 | } 390 | const selectedCode = document.getElementById('geoip-select').value; 391 | const results = geoip_result.entry.map(entry => ({ 392 | ...entry, 393 | cidr: entry.cidr.filter(cidr => { 394 | return queryIp && (selectedCode === '' || entry.countryCode === selectedCode) && matchesCIDR(cidr.ip, cidr.prefix, queryIp); 395 | }) 396 | })).filter(entry => entry.cidr.length > 0); 397 | renderGeoIPResults(results); 398 | } 399 | }, _('Search GeoIP')), 400 | ]), 401 | E('div', { 'id': 'geoip-results', 'class': 'results-container' }), 402 | ]), 403 | E('div', { 'class': 'cbi-section', 'data-tab': 'geosite', 'data-tab-title': _('GeoSite') }, [ 404 | E('select', { 405 | 'id': 'geosite-select', 406 | 'change': function () { 407 | const selectedCode = document.getElementById('geosite-select').value; 408 | const results = selectedCode ? geosite_result.entry.filter(entry => entry.countryCode === selectedCode) : []; 409 | renderGeoSiteResults(results); 410 | } 411 | }, [ 412 | E('option', { 'value': '' }, _('Filter GeoSite by Country Code')), 413 | ...Array.from(new Set(geosite_result.entry.map(entry => entry.countryCode))) 414 | .sort() 415 | .map(code => { 416 | const count = geosite_result.entry 417 | .find(entry => entry.countryCode === code) 418 | .domain.length; 419 | return E('option', { 'value': code }, `${code} (${count} items)`); 420 | }) 421 | ]), 422 | E('div', { 'class': 'cbi-section-create' }, [ 423 | E('input', { 424 | 'type': 'text', 425 | 'id': 'geosite-search', 426 | 'class': 'cbi-input-text', 427 | 'placeholder': _('Search GeoSite...') 428 | }), 429 | E('button', { 430 | 'class': 'cbi-button', 431 | 'click': function () { 432 | const query = document.getElementById('geosite-search').value.toLowerCase(); 433 | const selectedCode = document.getElementById('geosite-select').value; 434 | const results = geosite_result.entry.map(entry => ({ 435 | ...entry, 436 | domain: entry.domain.filter(domain => { 437 | return query && (selectedCode === '' || entry.countryCode === selectedCode) && domain.value.toLowerCase().includes(query); 438 | }) 439 | })).filter(entry => entry.domain.length > 0); 440 | renderGeoSiteResults(results); 441 | } 442 | }, _('Search GeoSite')), 443 | ]), 444 | E('div', { 'id': 'geosite-results', 'class': 'results-container' }) 445 | ]), 446 | ]) 447 | ]); 448 | ui.tabs.initTabGroup(result.lastElementChild.childNodes); 449 | 450 | return E([], [ 451 | E('h2', _('Xray (geodata)')), 452 | E('p', { 'class': 'cbi-map-descr' }, `${_("Only first")} ${maxResults} ${_("results will be shown. Load GeoData files cost")} ${new Date().getTime() - load_result[0].getTime()} ${_("ms")}; ${geoip_result.entry.length} ${_("GeoIP entries")}, ${geosite_result.entry.length} ${_("GeoSite entries")}.`), 453 | result 454 | ]); 455 | }, 456 | 457 | handleSaveApply: null, 458 | handleSave: null, 459 | handleReset: null 460 | }); 461 | -------------------------------------------------------------------------------- /status/Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | PKG_NAME:=luci-app-xray-status 4 | PKG_VERSION:=3.6.0 5 | PKG_RELEASE:=1 6 | 7 | PKG_LICENSE:=MPLv2 8 | PKG_LICENSE_FILES:=LICENSE 9 | PKG_MAINTAINER:=yichya 10 | PKG_BUILD_PARALLEL:=1 11 | 12 | include $(INCLUDE_DIR)/package.mk 13 | 14 | define Package/$(PKG_NAME) 15 | SECTION:=Custom 16 | CATEGORY:=Extra packages 17 | TITLE:=LuCI Support for Xray (status page) 18 | DEPENDS:=luci-app-xray +wget 19 | PKGARCH:=all 20 | endef 21 | 22 | define Package/$(PKG_NAME)/description 23 | LuCI Support for Xray (Client-side Rendered) (status page). 24 | endef 25 | 26 | define Build/Compile 27 | echo "luci-app-xray $(PKG_VERSION)-$(PKG_RELEASE) `git rev-parse HEAD` `date +%s`" > $(PKG_BUILD_DIR)/version.txt 28 | endef 29 | 30 | define Package/$(PKG_NAME)/install 31 | $(INSTALL_DIR) $(1)/www/luci-static/resources/view/xray 32 | $(INSTALL_DATA) ./root/www/luci-static/resources/view/xray/status.js $(1)/www/luci-static/resources/view/xray/status.js 33 | $(INSTALL_DIR) $(1)/usr/share/luci/menu.d 34 | $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-xray-status.json $(1)/usr/share/luci/menu.d/luci-app-xray-status.json 35 | $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d 36 | $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-xray-status.json $(1)/usr/share/rpcd/acl.d/luci-app-xray-status.json 37 | $(INSTALL_DIR) $(1)/usr/share/xray 38 | $(INSTALL_DATA) $(PKG_BUILD_DIR)/version.txt $(1)/usr/share/xray/version.txt 39 | endef 40 | 41 | $(eval $(call BuildPackage,$(PKG_NAME))) 42 | -------------------------------------------------------------------------------- /status/root/usr/share/luci/menu.d/luci-app-xray-status.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin/status/xray_status": { 3 | "title": "Xray", 4 | "action": { 5 | "type": "view", 6 | "path": "xray/status" 7 | }, 8 | "depends": { 9 | "acl": [ 10 | "luci-app-xray-status" 11 | ], 12 | "uci": { 13 | "xray_core": true 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /status/root/usr/share/rpcd/acl.d/luci-app-xray-status.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-xray-status": { 3 | "description": "Grant access to xray configurations", 4 | "read": { 5 | "uci": [ 6 | "xray_core" 7 | ], 8 | "file": { 9 | "/usr/bin/wget": [ 10 | "exec" 11 | ], 12 | "/usr/share/xray/version.txt": [ 13 | "read" 14 | ] 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /status/root/www/luci-static/resources/view/xray/status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require dom'; 3 | 'require fs'; 4 | 'require poll'; 5 | 'require uci'; 6 | 'require ui'; 7 | 'require view'; 8 | 'require view.xray.shared as shared'; 9 | 10 | function bool_translate(v) { 11 | if (v === "1") { 12 | return _("available"); 13 | } 14 | return _("unavailable"); 15 | } 16 | 17 | function greater_than_zero(n) { 18 | if (n < 0) { 19 | return 0; 20 | } 21 | return n; 22 | } 23 | 24 | function get_inbound_uci_description(config, key) { 25 | const ks = key.split(":"); 26 | switch (ks[0]) { 27 | case "https_inbound": { 28 | return E([], [key, " ", shared.badge(`{ listen: https://0.0.0.0:443 }`)]); 29 | } 30 | case "http_inbound": { 31 | return E([], [key, " ", shared.badge(`{ listen: http://0.0.0.0:${uci.get_first(config, "general", "http_port") || 1081} }`)]); 32 | } 33 | case "socks_inbound": { 34 | return E([], [key, " ", shared.badge(`{ listen: socks5://0.0.0.0:${uci.get_first(config, "general", "socks_port") || 1080} }`)]); 35 | } 36 | case "tproxy_tcp_inbound_v4": { 37 | return E([], [key, " ", shared.badge(`{ listen: tproxy_tcp://0.0.0.0:${uci.get_first(config, "general", "tproxy_port_tcp_v4") || 1082} }`)]); 38 | } 39 | case "tproxy_udp_inbound_v4": { 40 | return E([], [key, " ", shared.badge(`{ listen: tproxy_udp://0.0.0.0:${uci.get_first(config, "general", "tproxy_port_udp_v4") || 1084} }`)]); 41 | } 42 | case "tproxy_tcp_inbound_v6": { 43 | return E([], [key, " ", shared.badge(`{ listen: tproxy_tcp://[::]:${uci.get_first(config, "general", "tproxy_port_tcp_v6") || 1083} }`)]); 44 | } 45 | case "tproxy_udp_inbound_v6": { 46 | return E([], [key, " ", shared.badge(`{ listen: tproxy_udp://[::]:${uci.get_first(config, "general", "tproxy_port_udp_v6") || 1085} }`)]); 47 | } 48 | case "tproxy_tcp_inbound_f4": { 49 | return E([], [key, " ", shared.badge(`{ listen: tproxy_tcp://0.0.0.0:${uci.get_first(config, "general", "tproxy_port_tcp_f4") || 1086} }`)]); 50 | } 51 | case "tproxy_udp_inbound_f4": { 52 | return E([], [key, " ", shared.badge(`{ listen: tproxy_udp://0.0.0.0:${uci.get_first(config, "general", "tproxy_port_udp_f4") || 1088} }`)]); 53 | } 54 | case "tproxy_tcp_inbound_f6": { 55 | return E([], [key, " ", shared.badge(`{ listen: tproxy_tcp://[::]:${uci.get_first(config, "general", "tproxy_port_tcp_f6") || 1087} }`)]); 56 | } 57 | case "tproxy_udp_inbound_f6": { 58 | return E([], [key, " ", shared.badge(`{ listen: tproxy_udp://[::]:${uci.get_first(config, "general", "tproxy_port_udp_f6") || 1089} }`)]); 59 | } 60 | case "metrics": { 61 | return E([], [key, " ", shared.badge(`{ listen: http://0.0.0.0:${uci.get_first(config, "general", "metrics_server_port") || 18888} }`)]); 62 | } 63 | case "api": { 64 | return E([], [key, " ", shared.badge(`{ listen: grpc://127.0.0.1:8080 }`)]); 65 | } 66 | case "dns_server_inbound": { 67 | return E([], [key, " ", shared.badge(`{ listen: dns://0.0.0.0:${ks[1]} }`)]); 68 | } 69 | } 70 | const uci_key = key.slice(-9); 71 | const uci_item = uci.get(config, uci_key); 72 | if (uci_item === null) { 73 | return key; 74 | } 75 | switch (uci_item[".type"]) { 76 | case "extra_inbound": { 77 | return E([], [key, " ", shared.badge(`{ listen: ${uci_item["inbound_type"]}://${uci_item["inbound_addr"]}:${uci_item["inbound_port"]} }`)]); 78 | } 79 | } 80 | return key; 81 | } 82 | 83 | function outbound_format(server) { 84 | if (server["alias"]) { 85 | return server["alias"]; 86 | } 87 | if (server["server"].includes(":")) { 88 | return `${server["transport"]},[${server["server"]}]:${server["server_port"]}`; 89 | } 90 | return `${server["transport"]},${server["server"]}:${server["server_port"]}`; 91 | } 92 | 93 | function get_outbound_uci_description(config, key) { 94 | if (!key) { 95 | return "direct"; 96 | } 97 | const uci_key = key.slice(-9); 98 | const uci_item = uci.get(config, uci_key); 99 | if (uci_item === null) { 100 | return "direct"; 101 | } 102 | switch (uci_item[".type"]) { 103 | case "servers": { 104 | return outbound_format(uci_item); 105 | } 106 | case "extra_inbound": { 107 | return `${uci_item["inbound_type"]}://${uci_item["inbound_addr"]}:${uci_item["inbound_port"]}`; 108 | } 109 | case "manual_tproxy": { 110 | return `${uci_item["source_addr"]}:${uci_item["source_port"]} -> ${uci_item["dest_addr"] || "{sniffing}"}:${uci_item["dest_port"]}`; 111 | } 112 | case "fakedns": { 113 | return `${uci_item["fake_dns_domain_names"].length} ${_("domains")}\n${uci_item["fake_dns_domain_names"].join("\n")}`; 114 | } 115 | } 116 | return "direct"; 117 | } 118 | 119 | function outbound_first_tag_format(tag_split, first_uci_description) { 120 | let result = [tag_split[0]]; 121 | 122 | const first_tag = tag_split[0].split(":"); 123 | if (first_tag.length === 1) { 124 | return result; 125 | } 126 | 127 | if (tag_split.length > 1) { 128 | switch (first_tag[0]) { 129 | case "extra_inbound": { 130 | if (tag_split.length < 3) { 131 | result.push(" ", shared.badge(`{ listen: ${first_uci_description} }`)); 132 | } else { 133 | result.push(" ", shared.badge(`{ listen ... }`, first_uci_description)); 134 | } 135 | break; 136 | } 137 | case "force_forward": { 138 | result.push(" ", shared.badge(`{ force_forward ... }`, first_uci_description)); 139 | break; 140 | } 141 | case "balancer_outbound": { 142 | if (tag_split.length < 4) { 143 | result.push(" ", shared.badge(`{ balancer_outbound ... }`, first_uci_description)); 144 | } 145 | break; 146 | } 147 | case "fake_dns_tcp": 148 | case "fake_dns_udp": { 149 | result.push(" ", shared.badge(`{ fake_dns ... }`, first_uci_description)); 150 | break; 151 | } 152 | case "manual_tproxy": { 153 | break; 154 | } 155 | default: { 156 | result.push(" ", shared.badge(`{ ... }`, first_uci_description)); 157 | break; 158 | } 159 | } 160 | } else { 161 | result.push(" ", shared.badge(`{ ${first_uci_description} }`, first_tag[0])); 162 | } 163 | return result; 164 | } 165 | 166 | function outbound_middle_tag_format(tag_split, first_uci_description, current_tag, current_uci_description) { 167 | switch (current_tag[0]) { 168 | case "extra_inbound": { 169 | if (tag_split.length < 3) { 170 | return shared.badge(`{ listen: ${current_uci_description} }`, `${current_tag[0]}: ${current_uci_description} (${current_tag[1]})`); 171 | } 172 | return shared.badge(`{ listen ... }`, `${current_tag[0]}: ${current_uci_description} (${current_tag[1]})`); 173 | } 174 | case "force_forward": { 175 | return shared.badge(`{ force_forward ... }`, `${current_tag[0]}: ${current_uci_description} (${current_tag[1]})`); 176 | } 177 | case "balancer_outbound": { 178 | if (tag_split.length < 4) { 179 | return shared.badge(`{ balancer_outbound ... }`, `${current_tag[0]}: ${current_uci_description} (${current_tag[1]})`); 180 | } 181 | } 182 | case "tcp_outbound": { 183 | if (tag_split.length < 4) { 184 | return shared.badge(`{ tcp: ${first_uci_description} }`, current_tag[0]); 185 | } 186 | return shared.badge(`{ tcp ... }`, `tcp: ${first_uci_description}`); 187 | } 188 | case "udp_outbound": { 189 | if (tag_split.length < 4) { 190 | return shared.badge(`{ udp: ${first_uci_description} }`, current_tag[0]); 191 | } 192 | return shared.badge(`{ udp ... }`, `udp: ${first_uci_description}`); 193 | } 194 | case "fake_dns_tcp": 195 | case "fake_dns_udp": { 196 | break; 197 | } 198 | } 199 | return shared.badge(`{ ... }`, current_uci_description); 200 | } 201 | 202 | function outbound_last_tag_format(first_uci_description, last_tag, last_uci_description) { 203 | if (last_tag[0] === "tcp_outbound") { 204 | return shared.badge(`{ tcp: ${first_uci_description} }`); 205 | } else if (last_tag[0] === "udp_outbound") { 206 | return shared.badge(`{ udp: ${first_uci_description} }`); 207 | } 208 | return shared.badge(`{ ${last_tag[0]}: ${last_uci_description} }`, last_tag[1]); 209 | } 210 | 211 | function get_outbound_description(config, tag) { 212 | const tag_split = tag.split("@"); 213 | const first_uci_description = get_outbound_uci_description(config, tag_split[0].split(":")[1]); 214 | 215 | let result = outbound_first_tag_format(tag_split, first_uci_description); 216 | for (let i = 1; i < tag_split.length; i++) { 217 | const current_tag = tag_split[i].split(":"); 218 | const current_uci_description = get_outbound_uci_description(config, current_tag[1]); 219 | if (i === tag_split.length - 1) { 220 | result.push(" ", outbound_last_tag_format(first_uci_description, current_tag, current_uci_description)); 221 | } else { 222 | result.push(" ", outbound_middle_tag_format(tag_split, first_uci_description, current_tag, current_uci_description)); 223 | } 224 | } 225 | return result; 226 | } 227 | 228 | function observatory(vars, config) { 229 | if (!vars["observatory"]) { 230 | return []; 231 | } 232 | const now_timestamp = new Date().getTime() / 1000; 233 | return [ 234 | E('h3', _('Outbound Observatory')), 235 | E('div', { 'class': 'cbi-map-descr' }, _("Availability of outbound servers are probed every few seconds.")), 236 | E('table', { 'class': 'table' }, [ 237 | E('tr', { 'class': 'tr table-titles' }, [ 238 | E('th', { 'class': 'th' }, _('Tag')), 239 | E('th', { 'class': 'th' }, _('Latency')), 240 | E('th', { 'class': 'th' }, _('Last seen')), 241 | E('th', { 'class': 'th' }, _('Last check')), 242 | ]), ...Object.entries(vars["observatory"]).map((v, index, arr) => E('tr', { 'class': `tr cbi-rowstyle-${index % 2 + 1}` }, [ 243 | E('td', { 'class': 'td' }, get_outbound_description(config, v[0])), 244 | E('td', { 'class': 'td' }, function (c) { 245 | if (c[1]["alive"]) { 246 | return c[1]["delay"] + ' ' + _("ms"); 247 | } 248 | return _("unreachable"); 249 | }(v)), 250 | E('td', { 'class': 'td' }, function (c) { 251 | if (c[1]["last_seen_time"] === undefined) { 252 | return _("never"); 253 | } 254 | return '%d'.format(greater_than_zero(now_timestamp - c[1]["last_seen_time"])) + _('s ago'); 255 | }(v)), 256 | E('td', { 'class': 'td' }, '%d'.format(greater_than_zero(now_timestamp - v[1]["last_try_time"])) + _('s ago')), 257 | ])) 258 | ]) 259 | ]; 260 | }; 261 | 262 | function outbound_stats(vars, config) { 263 | if (!vars["stats"]) { 264 | return []; 265 | } 266 | if (!vars["stats"]["outbound"]) { 267 | return []; 268 | } 269 | return [ 270 | E('h3', _('Outbound Statistics')), 271 | E('div', { 'class': 'cbi-map-descr' }, _("Data transferred for outbounds since Xray start.")), 272 | E('table', { 'class': 'table' }, [ 273 | E('tr', { 'class': 'tr table-titles' }, [ 274 | E('th', { 'class': 'th' }, _('Tag')), 275 | E('th', { 'class': 'th' }, _('Downlink')), 276 | E('th', { 'class': 'th' }, _('Uplink')), 277 | ]), ...Object.entries(vars["stats"]["outbound"]).map((v, index, arr) => E('tr', { 'class': `tr cbi-rowstyle-${index % 2 + 1}` }, [ 278 | E('td', { 'class': 'td' }, get_outbound_description(config, v[0])), 279 | E('td', { 'class': 'td' }, '%.2mB'.format(v[1]["downlink"])), 280 | E('td', { 'class': 'td' }, '%.2mB'.format(v[1]["uplink"])), 281 | ])) 282 | ]) 283 | ]; 284 | }; 285 | 286 | function inbound_stats(vars, config) { 287 | if (!vars["stats"]) { 288 | return []; 289 | } 290 | if (!vars["stats"]["inbound"]) { 291 | return []; 292 | } 293 | return [ 294 | E('h3', _('Inbound Statistics')), 295 | E('div', { 'class': 'cbi-map-descr' }, _("Data transferred for inbounds since Xray start.")), 296 | E('table', { 'class': 'table' }, [ 297 | E('tr', { 'class': 'tr table-titles' }, [ 298 | E('th', { 'class': 'th' }, _('Tag')), 299 | E('th', { 'class': 'th' }, _('Downlink')), 300 | E('th', { 'class': 'th' }, _('Uplink')), 301 | ]), ...Object.entries(vars["stats"]["inbound"]).map((v, index, arr) => E('tr', { 'class': `tr cbi-rowstyle-${index % 2 + 1}` }, [ 302 | E('td', { 'class': 'td' }, get_inbound_uci_description(config, v[0])), 303 | E('td', { 'class': 'td' }, '%.2mB'.format(v[1]["downlink"])), 304 | E('td', { 'class': 'td' }, '%.2mB'.format(v[1]["uplink"])), 305 | ])) 306 | ]) 307 | ]; 308 | }; 309 | 310 | return view.extend({ 311 | load: function () { 312 | return Promise.all([ 313 | uci.load(shared.variant), 314 | fs.read("/usr/share/xray/version.txt") 315 | ]); 316 | }, 317 | 318 | render: function (load_result) { 319 | const config = load_result[0]; 320 | if (uci.get_first(config, "general", "metrics_server_enable") !== "1") { 321 | return E([], [ 322 | E('h2', _('Xray (status)')), 323 | E('p', { 'class': 'cbi-map-descr' }, _("Xray metrics server not enabled. Enable Xray metrics server to see details.")) 324 | ]); 325 | } 326 | const version = load_result[1].split(" "); 327 | const stats_available = bool_translate(uci.get_first(config, "general", "stats")); 328 | const observatory_available = bool_translate(uci.get_first(config, "general", "observatory")); 329 | const info = E('p', { 'class': 'cbi-map-descr' }, `${version[0]} Version ${version[1]} (${version[2]}) Built ${new Date(version[3] * 1000).toLocaleString()}. Statistics: ${stats_available}. Observatory: ${observatory_available}.`); 330 | const detail = E('div', {}); 331 | poll.add(function () { 332 | fs.exec_direct("/usr/bin/wget", ["-O", "-", `http://127.0.0.1:${uci.get_first(config, "general", "metrics_server_port") || 18888}/debug/vars`], "json").then(function (vars) { 333 | const result = E([], [ 334 | E('div', {}, [ 335 | E('div', { 'class': 'cbi-section', 'data-tab': 'observatory', 'data-tab-title': _('Observatory') }, observatory(vars, config)), 336 | E('div', { 'class': 'cbi-section', 'data-tab': 'outbounds', 'data-tab-title': _('Outbounds') }, outbound_stats(vars, config)), 337 | E('div', { 'class': 'cbi-section', 'data-tab': 'inbounds', 'data-tab-title': _('Inbounds') }, inbound_stats(vars, config)) 338 | ]) 339 | ]); 340 | ui.tabs.initTabGroup(result.lastElementChild.childNodes); 341 | dom.content(detail, result); 342 | }); 343 | }); 344 | 345 | return E([], [ 346 | E('h2', _('Xray (status)')), 347 | info, 348 | detail 349 | ]); 350 | }, 351 | 352 | handleSaveApply: null, 353 | handleSave: null, 354 | handleReset: null 355 | }); 356 | --------------------------------------------------------------------------------