diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 00000000..54882910 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,137 @@ +name: Build and Release + +on: + push: + tags: + - "*" + workflow_dispatch: + +env: + PACKAGE_NAME: luci-app-xray + CACHE_DIR: ~/cache + +jobs: + release: + name: Build for ${{ matrix.arch }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + sdk_url_path: https://downloads.openwrt.org/snapshots/targets/x86/64 + sdk_name: -sdk-x86-64_ + + env: + SDK_URL_PATH: ${{ matrix.sdk_url_path }} + SDK_NAME: ${{ matrix.sdk_name }} + CCACHE_DIR: ~/.ccache + CONFIG_CCACHE: y + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Prepare Cache Key + id: cache_key + run: echo "::set-output name=timestamp::$(date +"%s")" + + - name: Setup Cache + uses: actions/cache@v2 + with: + path: | + ${{ env.CACHE_DIR }} + ${{ env.CCACHE_DIR }} + key: openwrt-${{ matrix.arch }}-${{ env.PACKAGE_NAME }}-${{ steps.cache_key.outputs.timestamp }} + restore-keys: | + openwrt-${{ matrix.arch }}-${{ env.PACKAGE_NAME }}- + - name: Install Dependencies + run: | + DEBIAN_FRONTEND=noninteractive sudo apt-get install -y ccache gettext libncurses5-dev xsltproc + - name: Create Directories + run: | + CACHE_DIR_SDK="$(eval echo "$CACHE_DIR/sdk")" + CACHE_DIR_DL="$(eval echo "$CACHE_DIR/dl")" + CACHE_DIR_FEEDS="$(eval echo "$CACHE_DIR/feeds")" + echo "CACHE_DIR_SDK: $CACHE_DIR_SDK" + echo "CACHE_DIR_DL: $CACHE_DIR_DL" + echo "CACHE_DIR_FEEDS: $CACHE_DIR_FEEDS" + test -d "$CACHE_DIR_SDK" || mkdir -p "$CACHE_DIR_SDK" + test -d "$CACHE_DIR_DL" || mkdir -p "$CACHE_DIR_DL" + test -d "$CACHE_DIR_FEEDS" || mkdir -p "$CACHE_DIR_FEEDS" + echo "CACHE_DIR_SDK=$CACHE_DIR_SDK" >> $GITHUB_ENV + echo "CACHE_DIR_DL=$CACHE_DIR_DL" >> $GITHUB_ENV + echo "CACHE_DIR_FEEDS=$CACHE_DIR_FEEDS" >> $GITHUB_ENV + echo "SDK_HOME=$(mktemp -d)" >> $GITHUB_ENV + - name: Download and Unzip SDK + run: | + cd "$CACHE_DIR_SDK" + if ! ( wget -q -O - "$SDK_URL_PATH/sha256sums" | grep -- "$SDK_NAME" > sha256sums.small 2>/dev/null ) ; then + echo "::error::Can not find ${SDK_NAME} file in sha256sums." + exit 1 + fi + SDK_FILE="$(cat sha256sums.small | cut -d' ' -f2 | sed 's/*//g')" + if ! sha256sum -c ./sha256sums.small >/dev/null 2>&1 ; then + wget -q -O "$SDK_FILE" "$SDK_URL_PATH/$SDK_FILE" + if ! sha256sum -c ./sha256sums.small >/dev/null 2>&1 ; then + echo "::error::SDK can not be verified!" + exit 1 + fi + fi + cd - + file "$CACHE_DIR_SDK/$SDK_FILE" + tar -Jxf "$CACHE_DIR_SDK/$SDK_FILE" -C "$SDK_HOME" --strip=1 + cd "$SDK_HOME" + test -d "dl" && rm -rf "dl" || true + test -d "feeds" && rm -rf "feeds" || true + ln -s "$CACHE_DIR_DL" "dl" + ln -s "$CACHE_DIR_FEEDS" "feeds" + cp feeds.conf.default feeds.conf + sed -i 's#git.openwrt.org/openwrt/openwrt#github.com/openwrt/openwrt#' feeds.conf + sed -i 's#git.openwrt.org/feed/packages#github.com/openwrt/packages#' feeds.conf + sed -i 's#git.openwrt.org/project/luci#github.com/openwrt/luci#' feeds.conf + sed -i 's#git.openwrt.org/feed/telephony#github.com/openwrt/telephony#' feeds.conf + cd - + - name: Update and Install Packages + run: | + cd "$SDK_HOME" + ./scripts/feeds update -a + ln -s "${{ github.workspace }}" "package/$PACKAGE_NAME" + ./scripts/feeds install -a + cd - + - name: Build Packages + run: | + cd "$SDK_HOME" + make defconfig + make package/${PACKAGE_NAME}/status/{clean,compile} V=s + find "$SDK_HOME/bin/" -type f -name "*.ipk" -exec ls -lh {} \; + cd - + - name: Copy Bin Files + run: | + find "$SDK_HOME/bin/" -type f -name "${PACKAGE_NAME}*.ipk" -exec cp {} "${{ github.workspace }}" \; + find "${{ github.workspace }}" -type f -name "*.ipk" -exec ls -lh {} \; + - name: Release and Upload Assets + uses: softprops/action-gh-release@v1 + with: + files: "*.ipk" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + notify: + name: Notify Release Publish + runs-on: ubuntu-latest + env: + TRIGGER_URL: ${{ secrets.TRIGGER_URL }} + TRIGGER_TOKEN: ${{ secrets.TRIGGER_TOKEN }} + needs: release + steps: + - name: Notify Jenkins + run: | + if [ -z "$TRIGGER_URL" ] ; then + echo "::warning::No trigger url found, skip..." + exit 0 + fi + curl -X POST \ + -H "Content-Type: application/json; charset=utf-8" \ + -H "Authorization: Bearer $TRIGGER_TOKEN" \ + -d "{\"event\":\"release\",\"repository\":\"${{ github.repository }}\",\"ref\":\"${{ github.ref }}\"}" \ + "$TRIGGER_URL" || true diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a44f6335 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode/ +.idea/ +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..a612ad98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 00000000..5e9f2058 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# luci-app-xray + +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. + +## Warnings + +* Version 3.0.0 involves a lot of breaking changes and is completely not compatible with older versions. Configurations needs to be filled in again. +* Since the last OpenWrt version with `firewall3` as default firewall implementation (which is OpenWrt 21.02.7) is now EoL, the `fw3` variant of this project is dropped. + * Check out [tag v2.1.2](https://github.com/yichya/luci-app-xray/tree/v2.1.2) and compile fw3 variant yourself if you really need that. +* About experimental REALITY support + * may change quite frequently so keep in mind about following warnings + * server role support **involves breaking changes if you use HTTPS server**: certificate settings are now bound to stream security, so previously uploaded certificate and key files will disappear in LuCI, but this won't prevent Xray from using them. Your previously uploaded file are still there, just select them again in LuCI. If Xray fails to start up and complains about missing certificate files, also try picking them again. + * legacy XTLS support has already been removed in version 1.8.0 and is also removed by this project since version 2.0.0. +* 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: + * try `opkg update && opkg install xray-geodata` (at least OpenWrt 21.02 releases) + * if that doesn't work or you are using OpenWrt 19.07 releases, see [#52](https://github.com/yichya/luci-app-xray/issues/52#issuecomment-856059905) +* This project **DOES NOT SUPPORT** the following versions of OpenWrt because of the requirements of firewall4 and cilent-side rendering LuCI: + * LEDE / OpenWrt prior to 22.03 + * [Lean's OpenWrt Source](https://github.com/coolsnowwolf/lede) (which uses a variant of LuCI shipped with OpenWrt 18.06) + + 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/)). +* 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. + +## Installation (Fw4 only) + +Just use `opkg -i *` to install both ipks from Releases. + +## Installation (Manually building OpenWrt) + +Choose one below: + +* 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` +* Clone this repository under `package` + +Then find `luci-app-xray` under `Extra Packages`. + +## Changelog since 3.1.0 + +* 2023-10-24 chore: bump version +* 2023-10-25 fix: set required for some fields; remove unused code +* 2023-10-26 fix: allow empty selection for extra inbound outbound balancer +* 2023-10-30 fix: blocked as nxdomain for IPv6 +* 2023-10-31 chore: bump version to 3.1.1 +* 2023-11-01 feat: custom configuration hook +* 2023-11-02 feat: specify DNS to resolve outbound server name + +## Changelog since 3.0.0 + +* 2023-09-26 Version 3.0.0 merge master +* 2023-09-27 fix: sniffing inboundTag; fix: upstream_domain_names +* 2023-10-01 fix: default configuration +* 2023-10-06 chore: code cleanup +* 2023-10-19 feat: detailed status page via metrics +* 2023-10-20 feat: better network interface control. **Requires reselection of LAN interfaces in** `Xray (preview)` -> `LAN Hosts Access Control` + +## Star History + +[![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) diff --git a/core/Makefile b/core/Makefile new file mode 100644 index 00000000..2071503a --- /dev/null +++ b/core/Makefile @@ -0,0 +1,153 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-xray +PKG_VERSION:=3.1.1 +PKG_RELEASE:=1 + +PKG_LICENSE:=MPLv2 +PKG_LICENSE_FILES:=LICENSE +PKG_MAINTAINER:=yichya +PKG_BUILD_PARALLEL:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/$(PKG_NAME) + SECTION:=Custom + CATEGORY:=Extra packages + TITLE:=LuCI Support for Xray + DEPENDS:=firewall4 +kmod-nft-tproxy +luci-base +xray-core +dnsmasq +ca-bundle + PKGARCH:=all +endef + +define Package/$(PKG_NAME)/description + LuCI Support for Xray (Client-side Rendered). +endef + +define Package/$(PKG_NAME)/config +menu "luci-app-xray Configuration" + depends on PACKAGE_$(PKG_NAME) + +config PACKAGE_XRAY_INCLUDE_CLOUDFLARE_ORIGIN_ROOT_CA + bool "Include Cloudflare Origin Root CA" + default n + +config PACKAGE_XRAY_INFINITE_RETRY_ON_STARTUP + bool "Retry infinitely on Xray startup (may solve some startup problems)" + default n + +config PACKAGE_XRAY_RLIMIT_NOFILE_LARGE + bool "Increase Max Open Files Limit (recommended)" + default y + +config PACKAGE_XRAY_RESTART_DNSMASQ_ON_IFACE_CHANGE + bool "Restart dnsmasq on interface change (select this if using dnsmasq v2.87)" + default n + +config PACKAGE_XRAY_IGNORE_TP_SPEC_DEF_GW + bool "Ignore TP_SPEC_DEF_GW (select this if using private IPv4 address)" + default n + +choice + prompt "Limit memory use by setting rlimit_data (experimental)" + default PACKAGE_XRAY_RLIMIT_DATA_UNLIMITED + config PACKAGE_XRAY_RLIMIT_DATA_UNLIMITED + bool "Not limited" + config PACKAGE_XRAY_RLIMIT_DATA_SMALL + bool "Small limit (about 50MB)" + config PACKAGE_XRAY_RLIMIT_DATA_LARGE + bool "Large limit (about 321MB)" +endchoice + +endmenu +endef + +define Build/Compile +endef + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +if [[ -z "$${IPKG_INSTROOT}" ]]; then + if [[ -f /etc/uci-defaults/xray_core ]]; then + ( . /etc/uci-defaults/xray_core ) && rm -f /etc/uci-defaults/xray_core + fi + rm -rf /tmp/luci-indexcache* /tmp/luci-modulecache +fi +exit 0 +endef + +define Package/$(PKG_NAME)/conffiles +/etc/config/xray_core +endef + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/etc/luci-uploads/xray + $(INSTALL_DIR) $(1)/etc/ssl/certs +ifdef CONFIG_PACKAGE_XRAY_INCLUDE_CLOUDFLARE_ORIGIN_ROOT_CA + $(INSTALL_DATA) ./root/etc/ssl/certs/origin_ca_ecc_root.pem $(1)/etc/ssl/certs/origin_ca_ecc_root.pem +endif + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./root/etc/init.d/xray_core $(1)/etc/init.d/xray_core + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_DATA) ./root/etc/config/xray_core $(1)/etc/config/xray_core + $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_BIN) ./root/etc/uci-defaults/xray_core $(1)/etc/uci-defaults/xray_core + $(INSTALL_DIR) $(1)/etc/hotplug.d/iface + $(INSTALL_BIN) ./root/etc/hotplug.d/iface/01-transparent-proxy-ipset $(1)/etc/hotplug.d/iface/01-transparent-proxy-ipset + $(INSTALL_DIR) $(1)/etc/nftables.d + $(LN) /usr/share/xray/include.nft $(1)/etc/nftables.d/99-xray.nft + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/xray + $(INSTALL_DATA) ./root/www/luci-static/resources/view/xray/core.js $(1)/www/luci-static/resources/view/xray/core.js + $(INSTALL_DATA) ./root/www/luci-static/resources/view/xray/preview.js $(1)/www/luci-static/resources/view/xray/preview.js + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/xray $(1)/usr/libexec/rpcd/xray + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-xray.json $(1)/usr/share/luci/menu.d/luci-app-xray.json + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-xray.json $(1)/usr/share/rpcd/acl.d/luci-app-xray.json + $(INSTALL_DIR) $(1)/usr/share/xray + $(LN) /var/run/xray.pid $(1)/usr/share/xray/xray.pid + $(LN) /usr/bin/xray $(1)/usr/share/xray/xray +ifdef CONFIG_PACKAGE_XRAY_IGNORE_TP_SPEC_DEF_GW + $(INSTALL_DATA) ./root/usr/share/xray/ignore_tp_spec_def_gw $(1)/usr/share/xray/ignore_tp_spec_def_gw +endif +ifdef CONFIG_PACKAGE_XRAY_RESTART_DNSMASQ_ON_IFACE_CHANGE + $(INSTALL_DATA) ./root/usr/share/xray/restart_dnsmasq_on_iface_change $(1)/usr/share/xray/restart_dnsmasq_on_iface_change +endif +ifdef CONFIG_PACKAGE_XRAY_INFINITE_RETRY_ON_STARTUP + $(INSTALL_DATA) ./root/usr/share/xray/infinite_retry $(1)/usr/share/xray/infinite_retry +endif +ifdef CONFIG_PACKAGE_XRAY_RLIMIT_NOFILE_LARGE + $(INSTALL_DATA) ./root/usr/share/xray/rlimit_nofile_large $(1)/usr/share/xray/rlimit_nofile +endif +ifdef CONFIG_PACKAGE_XRAY_RLIMIT_DATA_SMALL + $(INSTALL_DATA) ./root/usr/share/xray/rlimit_data_small $(1)/usr/share/xray/rlimit_data +endif +ifdef CONFIG_PACKAGE_XRAY_RLIMIT_DATA_LARGE + $(INSTALL_DATA) ./root/usr/share/xray/rlimit_data_large $(1)/usr/share/xray/rlimit_data +endif + $(INSTALL_DATA) ./root/usr/share/xray/include.nft $(1)/usr/share/xray/include.nft + $(INSTALL_BIN) ./root/usr/share/xray/default_gateway.uc $(1)/usr/share/xray/default_gateway.uc + $(INSTALL_BIN) ./root/usr/share/xray/dnsmasq_include.ut $(1)/usr/share/xray/dnsmasq_include.ut + $(INSTALL_BIN) ./root/usr/share/xray/firewall_include.ut $(1)/usr/share/xray/firewall_include.ut + $(INSTALL_BIN) ./root/usr/share/xray/gen_config.uc $(1)/usr/share/xray/gen_config.uc + $(INSTALL_DIR) $(1)/usr/share/xray/common + $(INSTALL_DATA) ./root/usr/share/xray/common/config.mjs $(1)/usr/share/xray/common/config.mjs + $(INSTALL_DATA) ./root/usr/share/xray/common/stream.mjs $(1)/usr/share/xray/common/stream.mjs + $(INSTALL_DATA) ./root/usr/share/xray/common/tls.mjs $(1)/usr/share/xray/common/tls.mjs + $(INSTALL_DIR) $(1)/usr/share/xray/feature + $(INSTALL_DATA) ./root/usr/share/xray/feature/bridge.mjs $(1)/usr/share/xray/feature/bridge.mjs + $(INSTALL_DATA) ./root/usr/share/xray/feature/dns.mjs $(1)/usr/share/xray/feature/dns.mjs + $(INSTALL_DATA) ./root/usr/share/xray/feature/extra_inbound.mjs $(1)/usr/share/xray/feature/extra_inbound.mjs + $(INSTALL_DATA) ./root/usr/share/xray/feature/fake_dns.mjs $(1)/usr/share/xray/feature/fake_dns.mjs + $(INSTALL_DATA) ./root/usr/share/xray/feature/inbound.mjs $(1)/usr/share/xray/feature/inbound.mjs + $(INSTALL_DATA) ./root/usr/share/xray/feature/manual_tproxy.mjs $(1)/usr/share/xray/feature/manual_tproxy.mjs + $(INSTALL_DATA) ./root/usr/share/xray/feature/outbound.mjs $(1)/usr/share/xray/feature/outbound.mjs + $(INSTALL_DATA) ./root/usr/share/xray/feature/system.mjs $(1)/usr/share/xray/feature/system.mjs + $(INSTALL_DIR) $(1)/usr/share/xray/protocol + $(INSTALL_DATA) ./root/usr/share/xray/protocol/shadowsocks.mjs $(1)/usr/share/xray/protocol/shadowsocks.mjs + $(INSTALL_DATA) ./root/usr/share/xray/protocol/trojan.mjs $(1)/usr/share/xray/protocol/trojan.mjs + $(INSTALL_DATA) ./root/usr/share/xray/protocol/vless.mjs $(1)/usr/share/xray/protocol/vless.mjs + $(INSTALL_DATA) ./root/usr/share/xray/protocol/vmess.mjs $(1)/usr/share/xray/protocol/vmess.mjs +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/core/root/etc/config/xray_core b/core/root/etc/config/xray_core new file mode 100644 index 00000000..dca23681 --- /dev/null +++ b/core/root/etc/config/xray_core @@ -0,0 +1,10 @@ +config general + list blocked_domain_rules 'geosite:category-ads' + list bypassed_domain_rules 'geosite:cn' + list forwarded_domain_rules 'geosite:geolocation-!cn' + list geoip_direct_code_list 'cn' + list geoip_direct_code_list_v6 'cn' + list tproxy_ifaces_v4 'br-lan' + list tproxy_ifaces_v6 'br-lan' + option transparent_proxy_enable '1' + option xray_bin '/usr/bin/xray' diff --git a/core/root/etc/hotplug.d/iface/01-transparent-proxy-ipset b/core/root/etc/hotplug.d/iface/01-transparent-proxy-ipset new file mode 100644 index 00000000..cb7006ea --- /dev/null +++ b/core/root/etc/hotplug.d/iface/01-transparent-proxy-ipset @@ -0,0 +1,2 @@ +#!/bin/sh +logger -st transparent-proxy-ipset[$$] -p6 "$(ucode /usr/share/xray/default_gateway.uc)" diff --git a/core/root/etc/init.d/xray_core b/core/root/etc/init.d/xray_core new file mode 100644 index 00000000..b23253af --- /dev/null +++ b/core/root/etc/init.d/xray_core @@ -0,0 +1,155 @@ +#!/bin/sh /etc/rc.common + +START=90 +STOP=15 +USE_PROCD=1 +NAME=xray_core + +FIREWALL_INCLUDE="/usr/share/xray/firewall_include.ut" + +setup_firewall() { + ip rule add fwmark 251 lookup 251 + ip route add local default dev lo table 251 + ip -6 rule add fwmark 251 lookup 251 + ip -6 route add local default dev lo table 251 + + logger -st xray[$$] -p4 "Generating firewall4 rules..." + /usr/bin/utpl ${FIREWALL_INCLUDE} > /var/etc/xray/firewall_include.nft + + logger -st xray[$$] -p4 "Triggering firewall4 restart..." + /etc/init.d/firewall restart +} + +flush_firewall() { + ip rule del fwmark 251 lookup 251 + ip route del local default dev lo table 251 + ip -6 rule del fwmark 251 lookup 251 + ip -6 route del local default dev lo table 251 + + logger -st xray[$$] -p4 "Flushing firewall4 rules..." + rm -f /var/etc/xray/firewall_include.nft + + logger -st xray[$$] -p4 "Triggering firewall4 restart..." + /etc/init.d/firewall restart +} + +impl_gen_config_file() { + /usr/bin/ucode /usr/share/xray/gen_config.uc > /var/etc/xray/config.json +} + +uci_get_by_type() { + local ret=$(uci get ${NAME}.@$1[0].$2 2>/dev/null) + echo ${ret:=$3} +} + +log_procd_set_param() { + local type="$1" + shift + logger -st xray[$$] -p4 "Using procd_set_param $type" "$@" +} + +start_xray() { + logger -st xray[$$] -p4 "Starting Xray from $1" + procd_open_instance + procd_set_param respawn 1 1 0 + procd_set_param command $1 + procd_append_param command run + procd_append_param command -confdir + procd_append_param command /var/etc/xray + + local rlimit_nofile + if [ -s /usr/share/xray/rlimit_nofile ] ; then + rlimit_nofile="nofile=""$(cat /usr/share/xray/rlimit_nofile)" + fi + + local rlimit_data + if [ -s /usr/share/xray/rlimit_data ] ; then + rlimit_data="data=""$(cat /usr/share/xray/rlimit_data)" + fi + + # this param passing method is just so fucking weird + if [ -z "${rlimit_nofile}" ] ; then + if [ ! -z "${rlimit_data}" ]; then + log_procd_set_param limits "${rlimit_data}" + procd_set_param limits "${rlimit_data}" + fi + else + if [ -z "${rlimit_data}" ]; then + log_procd_set_param limits "${rlimit_nofile}" + procd_set_param limits "${rlimit_nofile}" + else + log_procd_set_param limits "${rlimit_data}" "${rlimit_nofile}" + procd_set_param limits "${rlimit_data}" "${rlimit_nofile}" + fi + fi + + procd_set_param env XRAY_LOCATION_ASSET=/usr/share/xray + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param file /etc/config/xray + procd_set_param pidfile /var/run/xray.pid + procd_close_instance +} + +gen_config_file() { + rm -f /var/etc/xray/* + if [ -s /usr/share/xray/infinite_retry ] ; then + while [ ! -s /var/etc/xray/config.json ] ; do + logger -st xray[$$] -p4 "(Re)generating Xray configuration files..." + impl_gen_config_file + done + else + logger -st xray[$$] -p4 "Generating Xray configuration files..." + impl_gen_config_file + fi + local custom_config=$(uci_get_by_type general custom_config) + [ ! "${#custom_config}" == "0" ] && echo ${custom_config} > /var/etc/xray/config_custom.json +} + +setup_dnsmasq() { + utpl /usr/share/xray/dnsmasq_include.ut > /tmp/dnsmasq.d/xray.conf + logger -st xray[$$] -p4 $(cat /tmp/dnsmasq.d/xray.conf) + /etc/init.d/dnsmasq restart > /dev/null 2>&1 +} + +flush_dnsmasq() { + rm -f /tmp/dnsmasq.d/xray.conf + /etc/init.d/dnsmasq restart > /dev/null 2>&1 +} + +create_when_enable() { + [ "$(uci_get_by_type general transparent_proxy_enable)" == "1" ] || return 0 + logger -st xray[$$] -p4 "Setting dnsmasq and firewall for transparent proxy..." + setup_dnsmasq + setup_firewall + logger -st xray[$$] -p4 $(ucode /usr/share/xray/default_gateway.uc) +} + +flush_when_disable() { + logger -st xray[$$] -p4 "Resetting dnsmasq and firewall configurations..." + flush_dnsmasq + flush_firewall +} + +start_service() { + config_load $NAME + mkdir -p /var/run /var/etc/xray + local xray_bin=$(uci_get_by_type general xray_bin) + command -v ${xray_bin} > /dev/null 2>&1 || return 1 + gen_config_file + start_xray ${xray_bin} + create_when_enable || flush_when_disable +} + +stop_service() { + flush_when_disable +} + +reload_service() { + stop + start +} + +service_triggers() { + procd_add_reload_trigger "xray_core" +} diff --git a/core/root/etc/ssl/certs/origin_ca_ecc_root.pem b/core/root/etc/ssl/certs/origin_ca_ecc_root.pem new file mode 100644 index 00000000..4cf3d872 --- /dev/null +++ b/core/root/etc/ssl/certs/origin_ca_ecc_root.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICiTCCAi6gAwIBAgIUXZP3MWb8MKwBE1Qbawsp1sfA/Y4wCgYIKoZIzj0EAwIw +gY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T +YW4gRnJhbmNpc2NvMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYDVQQL +Ey9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhvcml0 +eTAeFw0xOTA4MjMyMTA4MDBaFw0yOTA4MTUxNzAwMDBaMIGPMQswCQYDVQQGEwJV +UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEZ +MBcGA1UEChMQQ2xvdWRGbGFyZSwgSW5jLjE4MDYGA1UECxMvQ2xvdWRGbGFyZSBP +cmlnaW4gU1NMIEVDQyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAASR+sGALuaGshnUbcxKry+0LEXZ4NY6JUAtSeA6g87K3jaA +xpIg9G50PokpfWkhbarLfpcZu0UAoYy2su0EhN7wo2YwZDAOBgNVHQ8BAf8EBAMC +AQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUhTBdOypw1O3VkmcH/es5 +tBoOOKcwHwYDVR0jBBgwFoAUhTBdOypw1O3VkmcH/es5tBoOOKcwCgYIKoZIzj0E +AwIDSQAwRgIhAKilfntP2ILGZjwajktkBtXE1pB4Y/fjAfLkIRUzrI15AiEA5UCL +XYZZ9m2c3fKwIenMMojL1eqydsgqj/wK4p5kagQ= +-----END CERTIFICATE----- diff --git a/core/root/etc/uci-defaults/xray_core b/core/root/etc/uci-defaults/xray_core new file mode 100644 index 00000000..50cf2dac --- /dev/null +++ b/core/root/etc/uci-defaults/xray_core @@ -0,0 +1,10 @@ +#!/bin/sh +uci get xray_core.@general[-1] >/dev/null 2>&1 || uci add xray_core general >/dev/null 2>&1 +uci commit xray_core +uci -q batch <<-EOF >/dev/null + delete ucitrack.@xray_core[-1] + add ucitrack xray_core + set ucitrack.@xray_core[-1].init=xray_core + commit ucitrack +EOF +exit 0 diff --git a/core/root/usr/libexec/rpcd/xray b/core/root/usr/libexec/rpcd/xray new file mode 100644 index 00000000..e6cbca8a --- /dev/null +++ b/core/root/usr/libexec/rpcd/xray @@ -0,0 +1,14 @@ +#!/bin/sh + +main() { + case "$1" in + list) + echo '{"statsquery":{},"statssys":{},"restartlogger":{}}' + ;; + call) + shift + xray api $@ + esac +} + +main "$@" diff --git a/core/root/usr/share/luci/menu.d/luci-app-xray.json b/core/root/usr/share/luci/menu.d/luci-app-xray.json new file mode 100644 index 00000000..ba24e039 --- /dev/null +++ b/core/root/usr/share/luci/menu.d/luci-app-xray.json @@ -0,0 +1,32 @@ +{ + "admin/services/xray_core": { + "title": "Xray (core)", + "action": { + "type": "view", + "path": "xray/core" + }, + "depends": { + "acl": [ + "luci-app-xray" + ], + "uci": { + "xray_core": true + } + } + }, + "admin/services/xray_preview": { + "title": "Xray (preview)", + "action": { + "type": "view", + "path": "xray/preview" + }, + "depends": { + "acl": [ + "luci-app-xray" + ], + "uci": { + "xray_core": true + } + } + } +} \ No newline at end of file diff --git a/core/root/usr/share/rpcd/acl.d/luci-app-xray.json b/core/root/usr/share/rpcd/acl.d/luci-app-xray.json new file mode 100644 index 00000000..b00d3465 --- /dev/null +++ b/core/root/usr/share/rpcd/acl.d/luci-app-xray.json @@ -0,0 +1,15 @@ +{ + "luci-app-xray": { + "description": "Grant access to xray configurations", + "read": { + "uci": [ + "xray_core" + ] + }, + "write": { + "uci": [ + "xray_core" + ] + } + } +} diff --git a/core/root/usr/share/xray/common/config.mjs b/core/root/usr/share/xray/common/config.mjs new file mode 100644 index 00000000..da102e4c --- /dev/null +++ b/core/root/usr/share/xray/common/config.mjs @@ -0,0 +1,9 @@ +"use strict"; + +import { cursor } from "uci"; + +export function load_config() { + const uci = cursor(); + uci.load("xray_core"); + return uci.get_all("xray_core"); +}; diff --git a/core/root/usr/share/xray/common/stream.mjs b/core/root/usr/share/xray/common/stream.mjs new file mode 100644 index 00000000..f372f99b --- /dev/null +++ b/core/root/usr/share/xray/common/stream.mjs @@ -0,0 +1,170 @@ +"use strict"; + +import { tls_outbound_settings, reality_outbound_settings } from "./tls.mjs"; + +function stream_tcp_fake_http_request(server) { + if (server["tcp_guise"] == "http") { + return { + version: "1.1", + method: "GET", + path: server["http_path"], + headers: { + Host: server["http_host"], + User_Agent: [ + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36", + "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" + ], + Accept_Encoding: ["gzip, deflate"], + Connection: ["keep-alive"], + Pragma: "no-cache" + } + }; + } + return null; +} + +function stream_tcp_fake_http_response(server) { + if (server["tcp_guise"] == "http") { + return { + version: "1.1", + status: "200", + reason: "OK", + headers: { + Content_Type: ["application/octet-stream", "video/mpeg"], + Transfer_Encoding: ["chunked"], + Connection: ["keep-alive"], + Pragma: "no-cache" + } + }; + } + return null; +} + +function stream_tcp(server) { + if (server["transport"] == "tcp") { + return { + header: { + type: server["tcp_guise"], + request: stream_tcp_fake_http_request(server), + response: stream_tcp_fake_http_response(server) + } + }; + } + return null; +} + +function stream_h2(server) { + if (server["transport"] == "h2") { + return { + path: server["h2_path"], + host: server["h2_host"], + read_idle_timeout: server["h2_health_check"] == "1" ? int(server["h2_read_idle_timeout"] || 10) : null, + health_check_timeout: server["h2_health_check"] == "1" ? int(server["h2_health_check_timeout"] || 20) : null, + }; + } + return null; +} + +function stream_grpc(server) { + if (server["transport"] == "grpc") { + return { + serviceName: server["grpc_service_name"], + multiMode: server["grpc_multi_mode"] == "1", + initial_windows_size: int(server["grpc_initial_windows_size"] || 0), + idle_timeout: server["grpc_health_check"] == "1" ? int(server["grpc_idle_timeout"] || 10) : null, + health_check_timeout: server["grpc_health_check"] == "1" ? int(server["grpc_health_check_timeout"] || 20) : null, + permit_without_stream: server["grpc_health_check"] == "1" ? (server["grpc_permit_without_stream"] == "1") : null + }; + } + return null; +} + +function stream_ws(server) { + if (server["transport"] == "ws") { + let headers = null; + if (server["ws_host"] != null) { + headers = { + Host: server["ws_host"] + }; + } + return { + path: server["ws_path"], + headers: headers + }; + } + return null; +} + +function stream_kcp(server) { + if (server["transport"] == "mkcp") { + let mkcp_seed = null; + if (server["mkcp_seed"] != "") { + mkcp_seed = server["mkcp_seed"]; + } + return { + mtu: int(server["mkcp_mtu"] || 1350), + tti: int(server["mkcp_tti"] || 50), + uplinkCapacity: int(server["mkcp_uplink_capacity"] || 5), + downlinkCapacity: int(server["mkcp_downlink_capacity"] || 20), + congestion: server["mkcp_congestion"] == "1", + readBufferSize: int(server["mkcp_read_buffer_size"] || 2), + writeBufferSize: int(server["mkcp_write_buffer_size"] || 2), + seed: mkcp_seed, + header: { + type: server["mkcp_guise"] || "none" + } + }; + } + return null; +} + +function stream_quic(server) { + if (server["transport"] == "quic") { + return { + security: server["quic_security"], + key: server["quic_key"], + header: { + type: server["quic_guise"] + } + }; + } + return null; +} + +export function stream_settings(server, protocol, tag) { + const security = server[protocol + "_tls"]; + let tlsSettings = null; + let realitySettings = null; + if (security == "tls") { + tlsSettings = tls_outbound_settings(server, protocol); + } else if (security == "reality") { + realitySettings = reality_outbound_settings(server, protocol); + } + + let dialer_proxy = null; + let dialer_proxy_tag = null; + if (server["dialer_proxy"] != null && server["dialer_proxy"] != "disabled") { + dialer_proxy = server["dialer_proxy"]; + dialer_proxy_tag = "dialer_proxy:" + tag; + } + return { + stream_settings: { + network: server["transport"], + sockopt: { + mark: 253, + domainStrategy: server["domain_strategy"] || "UseIP", + dialerProxy: dialer_proxy_tag + }, + security: security, + tlsSettings: tlsSettings, + realitySettings: realitySettings, + quicSettings: stream_quic(server), + tcpSettings: stream_tcp(server), + kcpSettings: stream_kcp(server), + wsSettings: stream_ws(server), + grpcSettings: stream_grpc(server), + httpSettings: stream_h2(server) + }, + dialer_proxy: dialer_proxy + }; +}; diff --git a/core/root/usr/share/xray/common/tls.mjs b/core/root/usr/share/xray/common/tls.mjs new file mode 100644 index 00000000..f51ea35c --- /dev/null +++ b/core/root/usr/share/xray/common/tls.mjs @@ -0,0 +1,85 @@ +"use strict"; + +export function fallbacks(proxy, config) { + const fallback = filter(keys(config), k => config[k][".type"] == "fallback") || []; + let f = []; + for (let key in fallback) { + const s = config[key]; + if (s["dest"] != null) { + push(f, { + dest: s["dest"], + alpn: s["alpn"], + name: s["name"], + xver: s["xver"], + path: s["path"] + }); + } + } + push(f, { + dest: proxy["web_server_address"] + }); + return f; +}; + +export function tls_outbound_settings(server, protocol) { + let result = { + serverName: server[protocol + "_tls_host"], + allowInsecure: server[protocol + "_tls_insecure"] != "0", + fingerprint: server[protocol + "_tls_fingerprint"] || "" + }; + + if (server[protocol + "_tls_alpn"] != null) { + result["alpn"] = server[protocol + "_tls_alpn"]; + } + + return result; +}; + +export function tls_inbound_settings(proxy, protocol_name) { + let wscert = proxy[protocol_name + "_tls_cert_file"]; + if (wscert == null) { + wscert = proxy["web_server_cert_file"]; + } + let wskey = proxy[protocol_name + "_tls_key_file"]; + if (wskey == null) { + wskey = proxy["web_server_key_file"]; + } + return { + alpn: [ + "http/1.1" + ], + certificates: [ + { + certificateFile: wscert, + keyFile: wskey + } + ] + }; +}; + +export function reality_outbound_settings(server, protocol) { + let result = { + show: server[protocol + "_reality_show"], + fingerprint: server[protocol + "_reality_fingerprint"], + serverName: server[protocol + "_reality_server_name"], + publicKey: server[protocol + "_reality_public_key"], + shortId: server[protocol + "_reality_short_id"], + spiderX: server[protocol + "_reality_spider_x"], + }; + + return result; +}; + +export function reality_inbound_settings(proxy, protocol_name) { + return { + show: proxy[protocol_name + "_reality_show"], + dest: proxy[protocol_name + "_reality_dest"], + xver: proxy[protocol_name + "_reality_xver"], + serverNames: proxy[protocol_name + "_reality_server_names"], + privateKey: proxy[protocol_name + "_reality_private_key"], + minClientVer: proxy[protocol_name + "_reality_min_client_ver"], + maxClientVer: proxy[protocol_name + "_reality_max_client_ver"], + maxTimeDiff: proxy[protocol_name + "_reality_max_time_diff"], + shortIds: proxy[protocol_name + "_reality_short_ids"], + }; +}; diff --git a/core/root/usr/share/xray/default_gateway.uc b/core/root/usr/share/xray/default_gateway.uc new file mode 100644 index 00000000..f3b0f4eb --- /dev/null +++ b/core/root/usr/share/xray/default_gateway.uc @@ -0,0 +1,85 @@ +#!/usr/bin/ucode +"use strict"; + +import { popen, stat } from "fs"; +import { connect } from "ubus"; + +function network_dump() { + const ubus = connect(); + if (ubus) { + const result = ubus.call("network.interface", "dump"); + ubus.disconnect(); + return result; + } + return { + "interface": [] + }; +} + +function get_default_gateway(dump) { + let dgs = {}; + for (let i in dump["interface"] || []) { + for (let j in i["route"] || []) { + if (j["target"] == "0.0.0.0") { + dgs[j["nexthop"]] = true; + dgs[j["source"]] = true; + } + } + }; + return keys(dgs); +} + +function get_prefix_delegate(dump) { + let pds = {}; + for (let i in dump["interface"] || []) { + for (let j in i["ipv6-prefix"] || []) { + if (j["assigned"]) { + pds[`${j["address"]}/${j["mask"]}`] = true; + } + } + } + return keys(pds); +} + +function gen_tp_spec_dv4_dg(dg) { + if (stat("/usr/share/xray/ignore_tp_spec_def_gw")) { + return ""; + } + if (length(dg) > 0) { + return `flush set inet fw4 tp_spec_dv4_dg\nadd element inet fw4 tp_spec_dv4_dg { ${join(", ", dg)} }\n`; + } + return ""; +} + +function gen_tp_spec_dv6_dg(pd) { + if (length(pd) > 0) { + return `flush set inet fw4 tp_spec_dv6_dg\nadd element inet fw4 tp_spec_dv6_dg { ${join(", ", pd)} }\n`; + } + return ""; +} + +function update_nft(dg, pd) { + const process = popen("nft -f -", "w"); + process.write(gen_tp_spec_dv4_dg(dg)); + process.write(gen_tp_spec_dv6_dg(pd)); + process.flush(); + process.close(); +} + +function restart_dnsmasq_if_necessary() { + if (stat("/usr/share/xray/restart_dnsmasq_on_iface_change")) { + system("service dnsmasq restart"); + } +} + +const dump = network_dump(); +const dg = get_default_gateway(dump); +const pd = get_prefix_delegate(dump); +const log = join(", ", [...dg, ...pd]); +if (log == "") { + print("default gateway not available, please wait for interface ready"); +} else { + print(`default gateway available at ${log}\n`); + update_nft(dg, pd); +} +restart_dnsmasq_if_necessary(); diff --git a/core/root/usr/share/xray/dnsmasq_include.ut b/core/root/usr/share/xray/dnsmasq_include.ut new file mode 100644 index 00000000..cca156a2 --- /dev/null +++ b/core/root/usr/share/xray/dnsmasq_include.ut @@ -0,0 +1,27 @@ +#!/usr/bin/utpl +{% + "use strict"; + import { load_config } from "./common/config.mjs"; + const config = load_config(); + const general = config[filter(keys(config), k => config[k][".type"] == "general")[0]]; + const dns_port = int(general["dns_port"] || 5300); + const dns_count = int(general["dns_count"] || 3); + const manual_tproxy = filter(keys(config), k => config[k][".type"] == "manual_tproxy") || []; +%} +# Generated dnsmasq configurations by luci-app-xray +strict-order +server=/#/127.0.0.1#{{ dns_port }} +{% for (let i = dns_port; i <= dns_port + dns_count; i++): %} +server=127.0.0.1#{{ i }} +{% endfor %} +{% for (let i in manual_tproxy): %} +{% if (config[i]["rebind_domain_ok"] == "1"): %} +{% for (let j in config[i]["domain_names"]): %} +rebind-domain-ok={{ j }} +{% endfor %} +{% endif %} +{% endfor %} +{% if (general["blocked_to_loopback"] != "1"): %} +bogus-nxdomain=127.127.127.127 +bogus-nxdomain=100::6c62:636f:656b:2164 +{% endif %} diff --git a/core/root/usr/share/xray/feature/bridge.mjs b/core/root/usr/share/xray/feature/bridge.mjs new file mode 100644 index 00000000..ee8d6762 --- /dev/null +++ b/core/root/usr/share/xray/feature/bridge.mjs @@ -0,0 +1,46 @@ +"use strict"; + +import { server_outbound } from "./outbound.mjs"; + +export function bridges(bridge) { + let result = []; + for (let v in bridge) { + push(result, { + tag: sprintf("bridge_inbound:%s", v[".name"]), + domain: v["domain"] + }); + } + return result; +}; + +export function bridge_outbounds(config, bridge) { + let result = []; + for (let v in bridge) { + const bridge_server = config[v["upstream"]]; + push(result, { + tag: sprintf("bridge_freedom_outbound:%s", v[".name"]), + protocol: "freedom", + settings: { + redirect: v["redirect"] + } + }, ...server_outbound(bridge_server, sprintf("bridge_upstream_outbound:%s", v[".name"]), config)); + } + return result; +}; + +export function bridge_rules(bridge) { + let result = []; + for (let v in bridge) { + push(result, { + type: "field", + inboundTag: [sprintf("bridge_inbound:%s", v[".name"])], + domain: [sprintf("full:%s", v["domain"])], + outboundTag: sprintf("bridge_upstream_outbound:%s", v[".name"]) + }, { + type: "field", + inboundTag: [sprintf("bridge_inbound:%s", v[".name"])], + outboundTag: sprintf("bridge_freedom_outbound:%s", v[".name"]) + }); + } + return result; +}; diff --git a/core/root/usr/share/xray/feature/dns.mjs b/core/root/usr/share/xray/feature/dns.mjs new file mode 100644 index 00000000..cd9318bd --- /dev/null +++ b/core/root/usr/share/xray/feature/dns.mjs @@ -0,0 +1,167 @@ +"use strict"; + +import { lsdir } from "fs"; +import { balancer } from "./system.mjs"; +import { fake_dns_domains } from "./fake_dns.mjs"; + +const fallback_fast_dns = "223.5.5.5:53"; +const fallback_secure_dns = "8.8.8.8:53"; +const fallback_default_dns = "1.1.1.1:53"; +const share_dir = lsdir("/usr/share/xray"); +const geosite_existence = index(share_dir, "geosite.dat") > 0; + +function split_ipv4_host_port(val, port_default) { + const result = match(val, /([0-9\.]+):([0-9]+)/); + if (result == null) { + return { + address: val, + port: int(port_default) + }; + } + + return { + address: result[1], + port: int(result[2]) + }; +} + +function upstream_domain_names(proxy, config) { + let domain_names_set = {}; + let domain_extra_options = {}; + for (let b in ["tcp_balancer_v4", "tcp_balancer_v6", "udp_balancer_v4", "udp_balancer_v6"]) { + for (let i in balancer(proxy, b, b)) { + const server = config[substr(i, -9)]; + if (server) { + if (!server["domain_resolve_dns"]) { + domain_names_set[server["server"]] = true; + } else { + domain_extra_options[server["server"]] = server["domain_resolve_dns"]; + } + } + } + } + // todo: add dialer proxy references here + return [keys(domain_names_set), domain_extra_options]; +} + +function domain_rules(proxy, k) { + if (proxy[k] == null) { + return []; + } + return filter(proxy[k], function (x) { + if (substr(x, 0, 8) == "geosite:") { + return geosite_existence; + } + return true; + }); +} + +export function secure_domain_rules(proxy) { + return domain_rules(proxy, "forwarded_domain_rules"); +}; + +export function fast_domain_rules(proxy) { + return domain_rules(proxy, "bypassed_domain_rules"); +}; + +export function blocked_domain_rules(proxy) { + return domain_rules(proxy, "blocked_domain_rules"); +}; + +export function dns_server_inbounds(proxy) { + let result = []; + const dns_port = int(proxy["dns_port"] || 5300); + const dns_count = int(proxy["dns_count"] || 3); + const default_dns = split_ipv4_host_port(proxy["default_dns"] || fallback_default_dns, 53); + for (let i = dns_port; i <= dns_port + dns_count; i++) { + push(result, { + port: i, + protocol: "dokodemo-door", + tag: sprintf("dns_server_inbound:%d", i), + settings: { + address: default_dns["address"], + port: default_dns["port"], + network: "tcp,udp" + } + }); + } + return result; +}; + +export function dns_server_tags(proxy) { + let result = []; + const dns_port = int(proxy["dns_port"] || 5300); + const dns_count = int(proxy["dns_count"] || 3); + for (let i = dns_port; i <= dns_port + dns_count; i++) { + push(result, sprintf("dns_server_inbound:%d", i)); + } + return result; +}; + +export function dns_server_outbound() { + return { + protocol: "dns", + settings: { + nonIPQuery: "skip" + }, + streamSettings: { + sockopt: { + mark: 254 + } + }, + tag: "dns_server_outbound" + }; +}; + +export function dns_conf(proxy, config, manual_tproxy, fakedns) { + const fast_dns_object = split_ipv4_host_port(proxy["fast_dns"] || fallback_fast_dns, 53); + const default_dns_object = split_ipv4_host_port(proxy["default_dns"] || fallback_default_dns, 53); + const upstream_domain_options = upstream_domain_names(proxy, config); + let servers = [ + default_dns_object, + ...fake_dns_domains(fakedns), + ...map(keys(upstream_domain_options[1]), function (k) { + const i = split_ipv4_host_port(upstream_domain_options[1][k]); + i["domains"] = [k]; + i["skipFallback"] = true; + return i; + }), + { + address: fast_dns_object["address"], + port: fast_dns_object["port"], + domains: [...upstream_domain_options[0], ...fast_domain_rules(proxy)], + skipFallback: true, + }, + ]; + + if (length(secure_domain_rules(proxy)) > 0) { + const secure_dns_object = split_ipv4_host_port(proxy["secure_dns"] || fallback_secure_dns, 53); + push(servers, { + address: secure_dns_object["address"], + port: secure_dns_object["port"], + domains: secure_domain_rules(proxy), + }); + } + + let hosts = {}; + if (length(blocked_domain_rules(proxy)) > 0) { + for (let rule in (blocked_domain_rules(proxy))) { + hosts[rule] = ["127.127.127.127", "100::6c62:636f:656b:2164"]; // blocked! + } + } + + for (let v in manual_tproxy) { + if (v.domain_names != null) { + for (let d in v.domain_names) { + hosts[d] = [v.source_addr]; + } + } + } + + return { + hosts: hosts, + servers: servers, + tag: "dns_conf_inbound", + queryStrategy: "UseIP" + }; +}; diff --git a/core/root/usr/share/xray/feature/extra_inbound.mjs b/core/root/usr/share/xray/feature/extra_inbound.mjs new file mode 100644 index 00000000..504d4984 --- /dev/null +++ b/core/root/usr/share/xray/feature/extra_inbound.mjs @@ -0,0 +1,61 @@ +"use strict"; + +import { balancer } from "./system.mjs"; +import { socks_inbound, http_inbound, dokodemo_inbound } from "./inbound.mjs"; + +export function extra_inbounds(proxy, extra_inbound) { + let result = []; + for (let v in extra_inbound) { + const tag = `extra_inbound:${v[".name"]}`; + if (v["inbound_type"] == "http") { + push(result, http_inbound(v["inbound_addr"] || "0.0.0.0", v["inbound_port"], tag)); + } else if (v["inbound_type"] == "socks5") { + push(result, socks_inbound(v["inbound_addr"] || "0.0.0.0", v["inbound_port"], tag)); + } else if (v["inbound_type"] == "tproxy_tcp") { + 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")); + } else if (v["inbound_type"] == "tproxy_udp") { + 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")); + } else { + die(`unknown inbound type ${v["inbound_type"]}`); + } + } + return result; +}; + +export function extra_inbound_rules(extra_inbound) { + let result = []; + for (let v in extra_inbound) { + if (v["specify_outbound"] == "1") { + push(result, { + type: "field", + inboundTag: [`extra_inbound:${v[".name"]}`], + balancerTag: `extra_inbound_outbound:${v[".name"]}` + }); + } + } + return result; +}; + +export function extra_inbound_balancers(extra_inbound, balancer_strategy) { + let result = []; + for (let e in extra_inbound) { + if (e["specify_outbound"] == "1") { + push(result, { + "tag": `extra_inbound_outbound:${e[".name"]}`, + "selector": balancer(e, "destination", `extra_inbound:${e[".name"]}`), + "strategy": { + "type": balancer_strategy + } + }); + } + } + return result; +}; + +export function extra_inbound_global_tcp_tags(extra_inbound) { + return map(filter(extra_inbound, v => v["specify_outbound"] != "1" && v["inbound_type"] != "tproxy_udp"), v => `extra_inbound_${v[".name"]}`); +}; + +export function extra_inbound_global_udp_tags(extra_inbound) { + return map(filter(extra_inbound, v => v["specify_outbound"] != "1" && v["inbound_type"] == "tproxy_udp"), v => `extra_inbound_${v[".name"]}`); +}; diff --git a/core/root/usr/share/xray/feature/fake_dns.mjs b/core/root/usr/share/xray/feature/fake_dns.mjs new file mode 100644 index 00000000..c823d777 --- /dev/null +++ b/core/root/usr/share/xray/feature/fake_dns.mjs @@ -0,0 +1,71 @@ +"use strict"; + +import { balancer } from "./system.mjs"; + +export function fake_dns_domains(fakedns) { + let domains = []; + for (let f in fakedns) { + push(domains, ...f["fake_dns_domain_names"]); + } + if (length(domains) == 0) { + return []; + } + return [ + { + "address": "fakedns", + "domains": domains, + "skipFallback": true + } + ]; +}; + +export function fake_dns_rules(fakedns) { + let result = []; + for (let f in fakedns) { + push(result, { + type: "field", + inboundTag: ["tproxy_tcp_inbound_f4", "tproxy_tcp_inbound_f6"], + domain: f["fake_dns_domain_names"], + balancerTag: `fake_dns_balancer:${f[".name"]}@tcp_balancer` + }, { + type: "field", + inboundTag: ["tproxy_udp_inbound_f4", "tproxy_udp_inbound_f6"], + domain: f["fake_dns_domain_names"], + balancerTag: `fake_dns_balancer:${f[".name"]}@udp_balancer` + }); + } + return result; +}; + +export function fake_dns_balancers(fakedns, balancer_strategy) { + let result = []; + for (let f in fakedns) { + push(result, { + "tag": `fake_dns_balancer:${f[".name"]}@tcp_balancer`, + "selector": balancer(f, "fake_dns_forward_server_tcp", `fake_dns_tcp:${f[".name"]}`), + "strategy": { + "type": balancer_strategy + } + }, { + "tag": `fake_dns_balancer:${f[".name"]}@udp_balancer`, + "selector": balancer(f, "fake_dns_forward_server_udp", `fake_dns_udp:${f[".name"]}`), + "strategy": { + "type": balancer_strategy + } + }); + } + return result; +}; + +export function fake_dns_conf(proxy) { + return [ + { + "ipPool": proxy.pool_v4 || "198.18.0.0/15", + "poolSize": int(proxy.pool_v4_size) || 65535 + }, + { + "ipPool": proxy.pool_v6 || "fc00::/18", + "poolSize": int(proxy.pool_v6_size) || 65535 + } + ]; +}; diff --git a/core/root/usr/share/xray/feature/inbound.mjs b/core/root/usr/share/xray/feature/inbound.mjs new file mode 100644 index 00000000..4bda4e27 --- /dev/null +++ b/core/root/usr/share/xray/feature/inbound.mjs @@ -0,0 +1,66 @@ +"use strict"; + +import { https_trojan_inbound } from "../protocol/trojan.mjs"; +import { https_vless_inbound } from "../protocol/vless.mjs"; + +export function dokodemo_inbound(listen, port, tag, sniffing, sniffing_route_only, sniffing_dest_override, sniffing_metadata_only, network, tproxy, timeout) { + let result = { + port: int(port), + protocol: "dokodemo-door", + tag: tag, + sniffing: sniffing == "1" ? { + enabled: true, + routeOnly: sniffing_route_only == "1", + destOverride: sniffing_dest_override, + metadataOnly: sniffing_metadata_only == "1" + } : null, + settings: { + network: network, + followRedirect: true, + timeout: int(timeout || 300), + }, + streamSettings: { + sockopt: { + tproxy: tproxy + } + } + }; + if (listen) { + result["listen"] = listen; + } + return result; +}; + +export function http_inbound(addr, port, tag) { + return { + listen: addr || "0.0.0.0", + port: port, + protocol: "http", + tag: tag, + settings: { + allowTransparent: false + } + }; +}; + +export function socks_inbound(addr, port, tag) { + return { + listen: addr || "0.0.0.0", + port: port, + protocol: "socks", + tag: tag, + settings: { + udp: true + } + }; +}; + +export function https_inbound(proxy, config) { + if (proxy["web_server_protocol"] == "vless") { + return https_vless_inbound(proxy, config); + } + if (proxy["web_server_protocol"] == "trojan") { + return https_trojan_inbound(proxy, config); + } + return null; +}; diff --git a/core/root/usr/share/xray/feature/manual_tproxy.mjs b/core/root/usr/share/xray/feature/manual_tproxy.mjs new file mode 100644 index 00000000..0e7080ac --- /dev/null +++ b/core/root/usr/share/xray/feature/manual_tproxy.mjs @@ -0,0 +1,83 @@ +"use strict"; + +import { server_outbound } from "./outbound.mjs"; + +export function manual_tproxy_outbounds(config, manual_tproxy) { + let result = []; + for (let v in manual_tproxy) { + let tcp_tag = "direct"; + if (v["force_forward_tcp"] == "1") { + if (v["force_forward_server_tcp"] != null) { + tcp_tag = `manual_tproxy:${v[".name"]}@tcp_outbound@force_forward:${v["force_forward_server_tcp"]}`; + const force_forward_server_tcp = config[v["force_forward_server_tcp"]]; + push(result, ...server_outbound(force_forward_server_tcp, tcp_tag, config)); + } + } + push(result, { + protocol: "freedom", + tag: sprintf("manual_tproxy:%s@tcp_outbound", v[".name"]), + settings: { + redirect: sprintf("%s:%d", v["dest_addr"] || "", v["dest_port"] || 0), + domainStrategy: "AsIs" + }, + proxySettings: { + tag: tcp_tag + } + }); + + let udp_tag = "direct"; + if (v["force_forward_udp"] == "1") { + if (v["force_forward_server_udp"] != null) { + udp_tag = `manual_tproxy:${v[".name"]}@udp_outbound@force_forward:${v["force_forward_server_udp"]}`; + const force_forward_server_udp = config[v["force_forward_server_udp"]]; + push(result, ...server_outbound(force_forward_server_udp, udp_tag, config)); + } + } + push(result, { + protocol: "freedom", + tag: sprintf("manual_tproxy:%s@udp_outbound", v[".name"]), + settings: { + redirect: sprintf("%s:%d", v["dest_addr"] || "", v["dest_port"] || 0), + domainStrategy: "AsIs" + }, + proxySettings: { + tag: udp_tag + } + }); + } + return result; +}; + +export function manual_tproxy_outbound_tags(manual_tproxy) { + let result = []; + for (let v in manual_tproxy) { + if (v["force_forward_tcp"] == "1") { + push(result, `manual_tproxy:${v[".name"]}@tcp_outbound@force_forward:${v["force_forward_server_tcp"]}`); + } + if (v["force_forward_udp"] == "1") { + push(result, `manual_tproxy:${v[".name"]}@udp_outbound@force_forward:${v["force_forward_server_udp"]}`); + } + } + return result; +}; + +export function manual_tproxy_rules(manual_tproxy) { + let result = []; + for (let v in manual_tproxy) { + splice(result, 0, 0, { + type: "field", + inboundTag: ["tproxy_tcp_inbound_v4", "socks_inbound", "https_inbound", "http_inbound"], + ip: [v["source_addr"]], + port: v["source_port"], + outboundTag: sprintf("manual_tproxy:%s@tcp_outbound", v[".name"]) + }); + splice(result, 0, 0, { + type: "field", + inboundTag: ["tproxy_udp_inbound_v4"], + ip: [v["source_addr"]], + port: v["source_port"], + outboundTag: sprintf("manual_tproxy:%s@udp_outbound", v[".name"]) + }); + } + return result; +}; diff --git a/core/root/usr/share/xray/feature/outbound.mjs b/core/root/usr/share/xray/feature/outbound.mjs new file mode 100644 index 00000000..4feb4cc7 --- /dev/null +++ b/core/root/usr/share/xray/feature/outbound.mjs @@ -0,0 +1,82 @@ +"use strict"; + +import { shadowsocks_outbound } from "../protocol/shadowsocks.mjs"; +import { trojan_outbound } from "../protocol/trojan.mjs"; +import { vless_outbound } from "../protocol/vless.mjs"; +import { vmess_outbound } from "../protocol/vmess.mjs"; + +function override_custom_config_recursive(x, y) { + if (type(x) != "object" || type(y) != "object") { + return y; + } + for (let k in y) { + x[k] = override_custom_config_recursive(x[k], y[k]); + } + return x; +} + +function server_outbound_recursive(t, server, tag, config) { + let outbound_result = null; + if (server["protocol"] == "vmess") { + outbound_result = vmess_outbound(server, tag); + } else if (server["protocol"] == "vless") { + outbound_result = vless_outbound(server, tag); + } else if (server["protocol"] == "shadowsocks") { + outbound_result = shadowsocks_outbound(server, tag); + } else if (server["protocol"] == "trojan") { + outbound_result = trojan_outbound(server, tag); + } + if (outbound_result == null) { + die(`unknown outbound server protocol ${server["protocol"]}`); + } + let outbound = outbound_result["outbound"]; + const custom_config_outbound_string = server["custom_config"]; + + if (custom_config_outbound_string != null && custom_config_outbound_string != "") { + const custom_config_outbound = json(custom_config_outbound_string); + for (let k in custom_config_outbound) { + if (k == "tag") { + continue; + } + outbound[k] = override_custom_config_recursive(outbound[k], custom_config_outbound[k]); + } + } + + const dialer_proxy = outbound_result["dialer_proxy"]; + const result = [...t, outbound]; + + if (dialer_proxy != null) { + const dialer_proxy_section = config[dialer_proxy]; + return server_outbound_recursive(result, dialer_proxy_section, `${tag}@dialer_proxy:${dialer_proxy}`, config); + } + return result; +} + +export function direct_outbound(tag) { + return { + protocol: "freedom", + tag: tag, + settings: { + domainStrategy: "UseIPv4" + }, + streamSettings: { + sockopt: { + mark: 252, + } + } + }; +}; + +export function blackhole_outbound() { + return { + tag: "blackhole_outbound", + protocol: "blackhole" + }; +}; + +export function server_outbound(server, tag, config) { + if (server == null) { + return [direct_outbound(tag)]; + } + return server_outbound_recursive([], server, tag, config); +}; diff --git a/core/root/usr/share/xray/feature/system.mjs b/core/root/usr/share/xray/feature/system.mjs new file mode 100644 index 00000000..6033ee98 --- /dev/null +++ b/core/root/usr/share/xray/feature/system.mjs @@ -0,0 +1,82 @@ +"use strict"; + +export function balancer(ref, x, prefix) { + const v = ref[x] || []; + if (length(v) == 0) { + return ["direct"]; + } + return map(v, (k) => `${prefix}@balancer_outbound:${k}`); +}; + +export function api_conf(proxy) { + if (proxy["xray_api"] == '1') { + return { + tag: "api", + services: [ + "HandlerService", + "LoggerService", + "StatsService" + ] + }; + } + return null; +}; + +export function metrics_conf(proxy) { + if (proxy["metrics_server_enable"] == "1") { + return { + tag: "metrics" + }; + } + return null; +}; + +export function policy(proxy) { + const stats = proxy["stats"] == "1"; + return { + levels: { + "0": { + handshake: int(proxy["handshake"] || 4), + connIdle: int(proxy["conn_idle"] || 300), + uplinkOnly: int(proxy["uplink_only"] || 2), + downlinkOnly: int(proxy["downlink_only"] || 5), + bufferSize: int(proxy["buffer_size"] || 4), + statsUserUplink: stats, + statsUserDownlink: stats, + } + }, + system: { + statsInboundUplink: stats, + statsInboundDownlink: stats, + statsOutboundUplink: stats, + statsOutboundDownlink: stats + } + }; +}; + +export function logging(proxy) { + return { + access: proxy["access_log"] == "1" ? "" : "none", + loglevel: proxy["loglevel"] || "warning", + dnsLog: proxy["dns_log"] == "1" + }; +}; + +export function system_route_rules(proxy) { + let result = []; + if (proxy["xray_api"] == '1') { + push(result, { + type: "field", + inboundTag: ["api"], + outboundTag: "api" + }); + } + if (proxy["metrics_server_enable"] == "1") { + push(result, { + type: "field", + inboundTag: ["metrics"], + outboundTag: "metrics" + }); + } + return result; +}; diff --git a/core/root/usr/share/xray/firewall_include.ut b/core/root/usr/share/xray/firewall_include.ut new file mode 100644 index 00000000..06cf12ed --- /dev/null +++ b/core/root/usr/share/xray/firewall_include.ut @@ -0,0 +1,546 @@ +#!/usr/bin/utpl +{% + "use strict"; + import { stat } from "fs"; + import { load_config } from "./common/config.mjs"; + const ignore_tp_spec_def_gw = stat("/usr/share/xray/ignore_tp_spec_def_gw"); + const config = load_config(); + const general = config[filter(keys(config), k => config[k][".type"] == "general")[0]]; + const general_mark = general.mark || 255; + const tcp4_enabled = length(general.tcp_balancer_v4 || []) > 0; + const udp4_enabled = length(general.udp_balancer_v4 || []) > 0; + const tcp6_enabled = length(general.tcp_balancer_v6 || []) > 0; + const udp6_enabled = length(general.udp_balancer_v6 || []) > 0; + const uids_direct = uniq(general.uids_direct || []); + const gids_direct = uniq(general.gids_direct || []); + let wan_bp_ips_no_dns = general.wan_bp_ips || []; + let wan_fw_ips_no_dns = general.wan_fw_ips || []; + push(wan_bp_ips_no_dns, split(general.fast_dns || "223.5.5.5:53", ":")[0]); + push(wan_fw_ips_no_dns, split(general.secure_dns || "8.8.8.8:53", ":")[0]); + const wan_bp_ips_v4 = filter(uniq(wan_bp_ips_no_dns), v => index(v, ":") == -1); + const wan_bp_ips_v6 = filter(uniq(wan_bp_ips_no_dns), v => index(v, ":") != -1); + const wan_fw_ips_v4 = filter(uniq(wan_fw_ips_no_dns), v => index(v, ":") == -1); + const wan_fw_ips_v6 = filter(uniq(wan_fw_ips_no_dns), v => index(v, ":") != -1); + const transparent_default_port_policy = general.transparent_default_port_policy || "forwarded"; + const wan_fw_tcp_ports = general.wan_fw_tcp_ports || []; + const wan_fw_udp_ports = general.wan_fw_udp_ports || []; + const wan_bp_tcp_ports = general.wan_bp_tcp_ports || []; + const wan_bp_udp_ports = general.wan_bp_udp_ports || []; + const counter = function () { + if (general.fw4_counter == "1") { + return "counter packets 0 bytes 0"; + } + return ""; + }(); + const firewall_priority = function () { + if (general.firewall_priority == null) { + return "+10"; + } + if (int(general.firewall_priority) > 0) { + return sprintf("+%s", general.firewall_priority); + }; + return sprintf("%s", general.firewall_priority); + }(); + const fakedns = map(filter(keys(config), k => config[k][".type"] == "fakedns") || [], k => config[k]); + const manual_tproxy = filter(keys(config), k => config[k][".type"] == "manual_tproxy") || []; + const manual_tproxy_source_ips = map(manual_tproxy, k => config[k]["source_addr"]) || []; + + 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) || []); + 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) || []); + 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) || []); + 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) || []); + 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]); + 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]); + + const used_extra_inbound = uniq([ + ...map(tp_spec_sm4_fw, k => k["access_control_forward_tcp_v4"]), + ...map(tp_spec_sm4_fw, k => k["access_control_forward_udp_v4"]), + ...map(tp_spec_sm6_fw, k => k["access_control_forward_tcp_v6"]), + ...map(tp_spec_sm6_fw, k => k["access_control_forward_udp_v6"]), + ]); + let extra_inbound_tcp_v4_map = {}; + let extra_inbound_udp_v4_map = {}; + let extra_inbound_tcp_v6_map = {}; + let extra_inbound_udp_v6_map = {}; + for (let i in used_extra_inbound) { + let tcp_v4_items = []; + let udp_v4_items = []; + let tcp_v6_items = []; + let udp_v6_items = []; + for (let j in tp_spec_sm4_fw) { + if (j["access_control_forward_tcp_v4"] == i) { + push(tcp_v4_items, j["macaddr"]); + } + if (j["access_control_forward_udp_v4"] == i) { + push(udp_v4_items, j["macaddr"]); + } + } + if (length(tcp_v4_items) > 0) { + extra_inbound_tcp_v4_map[i] = tcp_v4_items; + } + if (length(udp_v4_items) > 0) { + extra_inbound_udp_v4_map[i] = udp_v4_items; + } + for (let j in tp_spec_sm6_fw) { + if (j["access_control_forward_tcp_v6"] == i) { + push(tcp_v6_items, j["macaddr"]); + } + if (j["access_control_forward_udp_v6"] == i) { + push(udp_v6_items, j["macaddr"]); + } + } + if (length(tcp_v6_items) > 0) { + extra_inbound_tcp_v6_map[i] = tcp_v6_items; + } + if (length(udp_v6_items) > 0) { + extra_inbound_udp_v6_map[i] = udp_v6_items; + } + } + const ttl_override = int(general.ttl_override); + const hop_limit_override = int(general.hop_limit_override); + const ttl_hop_limit_match = int(general.ttl_hop_limit_match); + const dynamic_direct_tcp4 = function () { + if (general.dynamic_direct_tcp4 == "1") { + return `add @tp_spec_dv4_dt { ip daddr timeout ${general.dynamic_direct_timeout || 300}s }`; + } + return ""; + }(); + const dynamic_direct_tcp6 = function () { + if (general.dynamic_direct_tcp6 == "1") { + return `add @tp_spec_dv6_dt { ip6 daddr timeout ${general.dynamic_direct_timeout || 300}s }`; + } + return ""; + }(); + const dynamic_direct_udp4 = function () { + if (general.dynamic_direct_udp4 == "1") { + return `add @tp_spec_dv4_du { ip daddr timeout ${general.dynamic_direct_timeout || 300}s }`; + } + return ""; + }(); + const dynamic_direct_udp6 = function () { + if (general.dynamic_direct_udp6 == "1") { + return `add @tp_spec_dv6_du { ip6 daddr timeout ${general.dynamic_direct_timeout || 300}s }`; + } + return ""; + }(); +%} + + set tp_spec_dv4_sp { + type ipv4_addr + flags constant,interval + elements = { 0.0.0.0/8, 10.0.0.0/8, + 100.64.0.0/10, 127.0.0.0/8, + 169.254.0.0/16, 172.16.0.0/12, + 192.0.0.0/24, 192.52.193.0/24, + 192.168.0.0/16, 224.0.0.0/3 } + } + + set tp_spec_dv6_sp { + type ipv6_addr + flags constant,interval + elements = { ::, + ::1, + ::ffff:0.0.0.0/96, + ::ffff:0:0:0/96, + 64:ff9b::/96, + 100::/64, + 2001::/32, + 2001:20::/28, + 2001:db8::/32, + 2002::/16, + fc00::/7, + fe80::/10, + ff00::/8 } + } + +{% if (length(tp_spec_sm4_bp) > 0): %} + set tp_spec_sm4_bp { + type ether_addr + flags constant + elements = { {{ join(", ", tp_spec_sm4_bp) }} } + } +{% endif %} + +{% if (length(tp_spec_sm6_bp) > 0): %} + set tp_spec_sm6_bp { + type ether_addr + flags constant + elements = { {{ join(", ", tp_spec_sm6_bp) }} } + } +{% endif %} + +{% if (length(tp_spec_sm4_tp) > 0): %} + set tp_spec_sm4_tp { + type ether_addr + flags constant + elements = { {{ join(", ", tp_spec_sm4_tp) }} } + } +{% endif %} + +{% if (length(tp_spec_sm6_tp) > 0): %} + set tp_spec_sm6_tp { + type ether_addr + flags constant + elements = { {{ join(", ", tp_spec_sm6_tp) }} } + } +{% endif %} + +{% for (let i in extra_inbound_tcp_v4_map): %} + set tp_spec_sm4_ft_{{ i }} { + type ether_addr + flags constant + elements = { {{ join(", ", extra_inbound_tcp_v4_map[i]) }} } + } +{% endfor %} + +{% for (let i in extra_inbound_udp_v4_map): %} + set tp_spec_sm4_fu_{{ i }} { + type ether_addr + flags constant + elements = { {{ join(", ", extra_inbound_udp_v4_map[i]) }} } + } +{% endfor %} + +{% for (let i in extra_inbound_tcp_v6_map): %} + set tp_spec_sm6_ft_{{ i }} { + type ether_addr + flags constant + elements = { {{ join(", ", extra_inbound_tcp_v6_map[i]) }} } + } +{% endfor %} + +{% for (let i in extra_inbound_udp_v6_map): %} + set tp_spec_sm6_fu_{{ i }} { + type ether_addr + flags constant + elements = { {{ join(", ", extra_inbound_udp_v6_map[i]) }} } + } +{% endfor %} + +{% if (length(manual_tproxy_source_ips) > 0): %} + set tp_spec_dv4_mt { + type ipv4_addr + flags constant, interval + elements = { {{ join(", ", manual_tproxy_source_ips) }} } + } +{% endif %} + +{% if (length(wan_bp_ips_v4) > 0): %} + set tp_spec_dv4_bp { + type ipv4_addr + size {{ length(wan_bp_ips_v4) }} + flags constant, interval + elements = { {{ join(", ", wan_bp_ips_v4)}} } + } +{% endif %} + +{% if (length(wan_bp_ips_v6) > 0): %} + set tp_spec_dv6_bp { + type ipv6_addr + size {{ length(wan_bp_ips_v6) }} + flags constant, interval + elements = { {{ join(", ", wan_bp_ips_v6)}} } + } +{% endif %} + +{% if (length(wan_fw_ips_v4) > 0): %} + set tp_spec_dv4_fw { + type ipv4_addr + size {{ length(wan_fw_ips_v4) }} + flags constant, interval + elements = { {{ join(", ", wan_fw_ips_v4)}} } + } +{% endif %} + +{% if (length(wan_fw_ips_v6) > 0): %} + set tp_spec_dv6_fw { + type ipv6_addr + size {{ length(wan_fw_ips_v6) }} + flags constant, interval + elements = { {{ join(", ", wan_fw_ips_v6)}} } + } +{% endif %} + +{% if (ignore_tp_spec_def_gw == null): %} + set tp_spec_dv4_dg { + type ipv4_addr + size 16 + flags interval + } +{% endif %} + + set tp_spec_dv6_dg { + type ipv6_addr + size 16 + flags interval + } + +{% if (general.dynamic_direct_tcp4 == "1"): %} + set tp_spec_dv4_dt { + type ipv4_addr + size 65536 + flags timeout + } +{% endif %} + +{% if (general.dynamic_direct_tcp6 == "1"): %} + set tp_spec_dv6_dt { + type ipv6_addr + size 65536 + flags timeout + } +{% endif %} + +{% if (general.dynamic_direct_udp4 == "1"): %} + set tp_spec_dv4_du { + type ipv4_addr + size 65536 + flags timeout + } +{% endif %} + +{% if (general.dynamic_direct_udp6 == "1"): %} + set tp_spec_dv6_du { + type ipv6_addr + size 65536 + flags timeout + } +{% endif %} + + chain xray_transparent_proxy { + type filter hook prerouting priority filter {{ firewall_priority }}; policy accept; + mark 0x000000fb {{ counter }} goto tp_spec_wan_fw + ip protocol tcp {{ counter }} accept + ip protocol udp {{ counter }} accept + ip6 nexthdr tcp {{ counter }} accept + ip6 nexthdr udp {{ counter }} accept + {{ counter }} accept + } + + chain tp_spec_wan_fw { +{% if (length(fakedns) > 0): %} + ip protocol tcp ip daddr {{ general.pool_v4 || "198.18.0.0/15" }} {{ counter }} tproxy ip to :{{ general.tproxy_port_tcp_f4 || 1086 }} accept + ip protocol udp ip daddr {{ general.pool_v4 || "198.18.0.0/15" }} {{ counter }} tproxy ip to :{{ general.tproxy_port_udp_f4 || 1088 }} accept + ip6 nexthdr tcp ip6 daddr {{ general.pool_v6 || "fc00::/18" }} {{ counter }} tproxy ip6 to :{{ general.tproxy_port_tcp_f6 || 1087 }} accept + ip6 nexthdr udp ip6 daddr {{ general.pool_v6 || "fc00::/18" }} {{ counter }} tproxy ip6 to :{{ general.tproxy_port_udp_f6 || 1089 }} accept +{% endif %} +{% if (length(manual_tproxy_source_ips) > 0): %} + ip protocol tcp ip daddr @tp_spec_dv4_mt {{ counter }} tproxy ip to :{{ general.tproxy_port_tcp_v4 || 1082 }} accept + ip protocol udp ip daddr @tp_spec_dv4_mt {{ counter }} tproxy ip to :{{ general.tproxy_port_udp_v4 || 1084 }} accept +{% endif %} +{% for (let i in extra_inbound_tcp_v4_map): %} + ip protocol tcp ether saddr @tp_spec_sm4_ft_{{ i }} {{ counter }} tproxy ip to :{{ config[i].inbound_port }} accept +{% endfor %} +{% for (let i in extra_inbound_udp_v4_map): %} + ip protocol udp ether saddr @tp_spec_sm4_fu_{{ i }} {{ counter }} tproxy ip to :{{ config[i].inbound_port }} accept +{% endfor %} +{% for (let i in extra_inbound_tcp_v6_map): %} + ip6 nexthdr tcp ether saddr @tp_spec_sm6_ft_{{ i }} {{ counter }} tproxy ip6 to :{{ config[i].inbound_port }} accept +{% endfor %} +{% for (let i in extra_inbound_udp_v6_map): %} + ip6 nexthdr udp ether saddr @tp_spec_sm6_fu_{{ i }} {{ counter }} tproxy ip6 to :{{ config[i].inbound_port }} accept +{% endfor %} +{% if (tcp4_enabled): %} + ip protocol tcp {{ counter }} tproxy ip to :{{ general.tproxy_port_tcp_v4 || 1082 }} accept +{% else %} + ip protocol tcp {{ counter }} meta mark set {{ sprintf("0x%08x", general_mark) }} accept +{% endif %} +{% if (udp4_enabled): %} + ip protocol udp {{ counter }} tproxy ip to :{{ general.tproxy_port_udp_v4 || 1084 }} accept +{% else %} + ip protocol udp {{ counter }} meta mark set {{ sprintf("0x%08x", general_mark) }} accept +{% endif %} +{% if (tcp6_enabled): %} + ip6 nexthdr tcp {{ counter }} tproxy ip6 to :{{ general.tproxy_port_tcp_v6 || 1083 }} accept +{% else %} + ip6 nexthdr tcp {{ counter }} meta mark set {{ sprintf("0x%08x", general_mark) }} accept +{% endif %} +{% if (udp6_enabled): %} + ip6 nexthdr udp {{ counter }} tproxy ip6 to :{{ general.tproxy_port_udp_v6 || 1085 }} accept +{% else %} + ip6 nexthdr udp {{ counter }} meta mark set {{ sprintf("0x%08x", general_mark) }} accept +{% endif %} + {{ counter }} accept + } + + chain xray_prerouting { + type filter hook prerouting priority mangle {{ firewall_priority }}; policy accept; +{% if (ttl_override > 0): %} + ip ttl {{ ttl_hop_limit_match }} {{ counter }} ip ttl set {{ ttl_override }} +{% endif %} +{% if (hop_limit_override > 0): %} + ip6 hoplimit {{ ttl_hop_limit_match }} {{ counter }} ip6 hoplimit set {{ hop_limit_override }} +{% endif %} + {{ counter }} mark set ct mark +{% if (general.dynamic_direct_tcp4 == "1"): %} + ip protocol tcp meta mark 0x000000fa {{ counter }} accept comment "Xray dynamic direct TCP4" +{% endif %} +{% if (general.dynamic_direct_udp4 == "1"): %} + ip protocol udp meta mark 0x000000fa {{ counter }} accept comment "Xray dynamic direct UDP4" +{% endif %} +{% if (general.dynamic_direct_tcp6 == "1"): %} + ip6 nexthdr tcp meta mark 0x000000fa {{ counter }} accept comment "Xray dynamic direct TCP6" +{% endif %} +{% if (general.dynamic_direct_udp6 == "1"): %} + ip6 nexthdr udp meta mark 0x000000fa {{ counter }} accept comment "Xray dynamic direct UDP6" +{% endif %} + mark 0x000000fb {{ counter }} accept comment "Xray remarked from output" + {{ counter }} jump tp_spec_lan_mf comment "Xray FakeDNS / manual transparent proxy" +{% if (length(general.bypass_ifaces_v4 || []) > 0): %} + ip protocol tcp iifname { "{{ join('", "', general.bypass_ifaces_v4) }}" } {{ counter }} accept + ip protocol udp iifname { "{{ join('", "', general.bypass_ifaces_v4) }}" } {{ counter }} accept +{% endif %} +{% if (length(general.bypass_ifaces_v6 || []) > 0): %} + ip6 nexthdr tcp iifname { "{{ join('", "', general.bypass_ifaces_v6) }}" } {{ counter }} accept + ip6 nexthdr udp iifname { "{{ join('", "', general.bypass_ifaces_v6) }}" } {{ counter }} accept +{% endif %} +{% if (length(tp_spec_sm4_bp) > 0): %} + ip protocol tcp ether saddr @tp_spec_sm4_bp {{ counter }} accept + ip protocol udp ether saddr @tp_spec_sm4_bp {{ counter }} accept +{% endif %} +{% if (length(tp_spec_sm6_bp) > 0): %} + ip6 nexthdr tcp ether saddr @tp_spec_sm6_bp {{ counter }} accept + ip6 nexthdr udp ether saddr @tp_spec_sm6_bp {{ counter }} accept +{% endif %} +{% for (let i in extra_inbound_tcp_v4_map): %} + ip protocol tcp ether saddr @tp_spec_sm4_ft_{{ i }} {{ counter }} goto tp_spec_lan_ac +{% endfor %} +{% for (let i in extra_inbound_udp_v4_map): %} + ip protocol udp ether saddr @tp_spec_sm4_fu_{{ i }} {{ counter }} goto tp_spec_lan_ac +{% endfor %} +{% for (let i in extra_inbound_tcp_v6_map): %} + ip6 nexthdr tcp ether saddr @tp_spec_sm6_ft_{{ i }} {{ counter }} goto tp_spec_lan_ac +{% endfor %} +{% for (let i in extra_inbound_udp_v6_map): %} + ip6 nexthdr udp ether saddr @tp_spec_sm6_fu_{{ i }} {{ counter }} goto tp_spec_lan_ac +{% endfor %} +{% if (length(tp_spec_sm4_tp) > 0): %} + ip protocol tcp ether saddr @tp_spec_sm4_tp {{ counter }} goto tp_spec_lan_ac + ip protocol udp ether saddr @tp_spec_sm4_tp {{ counter }} goto tp_spec_lan_ac +{% endif %} +{% if (length(tp_spec_sm6_tp) > 0): %} + ip6 nexthdr tcp ether saddr @tp_spec_sm6_tp {{ counter }} goto tp_spec_lan_ac + ip6 nexthdr udp ether saddr @tp_spec_sm6_tp {{ counter }} goto tp_spec_lan_ac +{% endif %} +{% if (length(general.tproxy_ifaces_v4 || []) > 0): %} + ip protocol tcp iifname { "{{ join('", "', general.tproxy_ifaces_v4) }}" } {{ counter }} goto tp_spec_lan_ac + ip protocol udp iifname { "{{ join('", "', general.tproxy_ifaces_v4) }}" } {{ counter }} goto tp_spec_lan_ac +{% endif %} +{% if (length(general.tproxy_ifaces_v6 || []) > 0): %} + ip6 nexthdr tcp iifname { "{{ join('", "', general.tproxy_ifaces_v6) }}" } {{ counter }} goto tp_spec_lan_ac + ip6 nexthdr udp iifname { "{{ join('", "', general.tproxy_ifaces_v6) }}" } {{ counter }} goto tp_spec_lan_ac +{% endif %} + ip protocol tcp {{ counter }} accept + ip protocol udp {{ counter }} accept + ip6 nexthdr tcp {{ counter }} accept + ip6 nexthdr udp {{ counter }} accept + {{ counter }} accept + } + + chain xray_output { + type route hook output priority mangle {{ firewall_priority }}; policy accept; +{% if (length(uids_direct) > 0): %} + meta skuid { {{ join(", ", uids_direct) }} } {{ counter }} accept +{% endif %} +{% if (length(gids_direct) > 0): %} + meta skgid { {{ join(", ", gids_direct) }} } {{ counter }} accept +{% endif %} + ip protocol tcp {{ counter }} goto tp_spec_wan_ac + ip protocol udp {{ counter }} goto tp_spec_wan_ac + ip6 nexthdr tcp {{ counter }} goto tp_spec_wan_ac + ip6 nexthdr udp {{ counter }} goto tp_spec_wan_ac + {{ counter }} accept + } + + chain tp_spec_wan_ac { + ip protocol tcp mark 0x000000fc {{ counter }} {{ dynamic_direct_tcp4 }} accept comment "Xray direct outbound TCP4" + ip protocol udp mark 0x000000fc {{ counter }} {{ dynamic_direct_udp4 }} accept comment "Xray direct outbound UDP4" + ip6 nexthdr tcp mark 0x000000fc {{ counter }} {{ dynamic_direct_tcp6 }} accept comment "Xray direct outbound TCP6" + ip6 nexthdr udp mark 0x000000fc {{ counter }} {{ dynamic_direct_udp6 }} accept comment "Xray direct outbound UDP6" + meta mark 0x000000fd {{ counter }} accept comment "Xray transparent proxy outbound" + meta mark 0x000000fe {{ counter }} accept comment "Xray non-IP DNS query outbound" + meta mark {{ sprintf("0x%08x", general_mark) }} {{ counter }} accept comment "Xray specified mark {{ general_mark }} outbound" + {{ counter }} jump tp_spec_lan_mf + {{ counter }} goto tp_spec_lan_ac + } + + chain tp_spec_lan_mf { +{% if (length(fakedns) > 0): %} + 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" + 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" + ip6 nexthdr tcp ip6 daddr {{ general.pool_v6 || "fc00::/18" }} {{ counter }} goto tp_spec_lan_fw comment "Xray FakeDNS IPv6 Pool TCP" + ip6 nexthdr udp ip6 daddr {{ general.pool_v6 || "fc00::/18" }} {{ counter }} goto tp_spec_lan_fw comment "Xray FakeDNS IPv6 Pool UDP" +{% endif %} +{% if (length(manual_tproxy_source_ips) > 0): %} + ip protocol tcp ip daddr @tp_spec_dv4_mt {{ counter }} goto tp_spec_lan_fw comment "Xray manual transparent proxy TCP" + ip protocol udp ip daddr @tp_spec_dv4_mt {{ counter }} goto tp_spec_lan_fw comment "Xray manual transparent proxy UDP" +{% endif %} + {{ counter }} return + } + + chain tp_spec_lan_ac { +{% if (length(wan_fw_ips_v4) > 0): %} + ip daddr @tp_spec_dv4_fw {{ counter }} goto tp_spec_lan_fw +{% endif %} +{% if (length(wan_fw_ips_v6) > 0): %} + ip6 daddr @tp_spec_dv6_fw {{ counter }} goto tp_spec_lan_fw +{% endif %} +{% if (ignore_tp_spec_def_gw == null): %} + ip daddr @tp_spec_dv4_dg {{ counter }} accept +{% endif %} + ip6 daddr @tp_spec_dv6_dg {{ counter }} accept +{% if (length(wan_bp_ips_v4) > 0): %} + ip daddr @tp_spec_dv4_bp {{ counter }} accept +{% endif %} +{% if (length(wan_bp_ips_v6) > 0): %} + ip6 daddr @tp_spec_dv6_bp {{ counter }} accept +{% endif %} + ip daddr @tp_spec_dv4_sp {{ counter }} accept + ip6 daddr @tp_spec_dv6_sp {{ counter }} accept + {{ counter }} goto tp_spec_lan_re + } + + chain tp_spec_lan_re { +{% if (transparent_default_port_policy == "bypassed"): %} + {% if (length(wan_fw_tcp_ports) > 0): %} + tcp dport { {{ join(", ", wan_fw_tcp_ports) }} } {{ counter }} goto tp_spec_lan_fw + {% endif %} + {% if (length(wan_fw_udp_ports) > 0): %} + udp dport { {{ join(", ", wan_fw_udp_ports) }} } {{ counter }} goto tp_spec_lan_fw + {% endif %} +{% else %} + {% if (length(wan_bp_tcp_ports) > 0): %} + tcp dport { {{ join(", ", wan_bp_tcp_ports) }} } {{ counter }} accept + {% endif %} + {% if (length(wan_bp_udp_ports) > 0): %} + udp dport { {{ join(", ", wan_bp_udp_ports) }} } {{ counter }} accept + {% endif %} + meta l4proto tcp {{ counter }} goto tp_spec_lan_dd + meta l4proto udp {{ counter }} goto tp_spec_lan_dd +{% endif %} + {{ counter }} accept + } + + chain tp_spec_lan_dd { +{% if (general.dynamic_direct_tcp4 == "1"): %} + ip protocol tcp ip daddr @tp_spec_dv4_dt {{ counter }} mark set 0x000000fa goto tp_spec_lan_ct comment "Xray dynamic direct TCP4" +{% endif %} +{% if (general.dynamic_direct_udp4 == "1"): %} + ip protocol udp ip daddr @tp_spec_dv4_du {{ counter }} mark set 0x000000fa goto tp_spec_lan_ct comment "Xray dynamic direct UDP4" +{% endif %} +{% if (general.dynamic_direct_tcp6 == "1"): %} + ip6 nexthdr tcp ip6 daddr @tp_spec_dv6_dt {{ counter }} mark set 0x000000fa goto tp_spec_lan_ct comment "Xray dynamic direct TCP6" +{% endif %} +{% if (general.dynamic_direct_udp6 == "1"): %} + ip6 nexthdr udp ip6 daddr @tp_spec_dv6_du {{ counter }} mark set 0x000000fa goto tp_spec_lan_ct comment "Xray dynamic direct UDP6" +{% endif %} + {{ counter }} goto tp_spec_lan_fw + } + + chain tp_spec_lan_fw { + {{ counter }} mark set 0x000000fb goto tp_spec_lan_ct + } + + chain tp_spec_lan_ct { + {{ counter }} ct mark set mark accept + } diff --git a/core/root/usr/share/xray/gen_config.uc b/core/root/usr/share/xray/gen_config.uc new file mode 100644 index 00000000..3810d116 --- /dev/null +++ b/core/root/usr/share/xray/gen_config.uc @@ -0,0 +1,309 @@ +#!/usr/bin/ucode +"use strict"; + +import { lsdir } from "fs"; +import { load_config } from "./common/config.mjs"; +import { balancer, api_conf, metrics_conf, logging, policy, system_route_rules } from "./feature/system.mjs"; +import { blocked_domain_rules, fast_domain_rules, secure_domain_rules, dns_server_tags, dns_server_inbounds, dns_server_outbound, dns_conf } from "./feature/dns.mjs"; +import { socks_inbound, http_inbound, https_inbound, dokodemo_inbound } from "./feature/inbound.mjs"; +import { blackhole_outbound, direct_outbound, server_outbound } from "./feature/outbound.mjs"; +import { bridges, bridge_outbounds, bridge_rules } from "./feature/bridge.mjs"; +import { extra_inbounds, extra_inbound_rules, extra_inbound_global_tcp_tags, extra_inbound_global_udp_tags, extra_inbound_balancers } from "./feature/extra_inbound.mjs"; +import { manual_tproxy_outbounds, manual_tproxy_outbound_tags, manual_tproxy_rules } from "./feature/manual_tproxy.mjs"; +import { fake_dns_balancers, fake_dns_conf, fake_dns_rules } from "./feature/fake_dns.mjs"; + +function inbounds(proxy, config, extra_inbound) { + let i = [ + socks_inbound("0.0.0.0", proxy["socks_port"] || 1080, "socks_inbound"), + http_inbound("0.0.0.0", proxy["http_port"] || 1081, "http_inbound"), + dokodemo_inbound("0.0.0.0", proxy["tproxy_port_tcp_v4"] || 1082, "tproxy_tcp_inbound_v4", proxy["tproxy_sniffing"], proxy["route_only"], ["http", "tls"], "0", "tcp", "tproxy", proxy["conn_idle"]), + dokodemo_inbound("0.0.0.0", proxy["tproxy_port_tcp_v6"] || 1083, "tproxy_tcp_inbound_v6", proxy["tproxy_sniffing"], proxy["route_only"], ["http", "tls"], "0", "tcp", "tproxy", proxy["conn_idle"]), + dokodemo_inbound("0.0.0.0", proxy["tproxy_port_udp_v4"] || 1084, "tproxy_udp_inbound_v4", proxy["tproxy_sniffing"], proxy["route_only"], ["quic"], "0", "udp", "tproxy", proxy["conn_idle"]), + dokodemo_inbound("0.0.0.0", proxy["tproxy_port_udp_v6"] || 1085, "tproxy_udp_inbound_v6", proxy["tproxy_sniffing"], proxy["route_only"], ["quic"], "0", "udp", "tproxy", proxy["conn_idle"]), + ...extra_inbounds(proxy, extra_inbound), + ...dns_server_inbounds(proxy), + dokodemo_inbound("0.0.0.0", proxy["tproxy_port_tcp_f4"] || 1086, "tproxy_tcp_inbound_f4", "1", "0", ["fakedns"], "1", "tcp", "tproxy", proxy["fake_dns_timeout"]), + dokodemo_inbound("0.0.0.0", proxy["tproxy_port_tcp_f6"] || 1087, "tproxy_tcp_inbound_f6", "1", "0", ["fakedns"], "1", "tcp", "tproxy", proxy["fake_dns_timeout"]), + dokodemo_inbound("0.0.0.0", proxy["tproxy_port_udp_f4"] || 1088, "tproxy_udp_inbound_f4", "1", "0", ["fakedns"], "1", "udp", "tproxy", proxy["fake_dns_timeout"]), + dokodemo_inbound("0.0.0.0", proxy["tproxy_port_udp_f6"] || 1089, "tproxy_udp_inbound_f6", "1", "0", ["fakedns"], "1", "udp", "tproxy", proxy["fake_dns_timeout"]), + ]; + if (proxy["web_server_enable"] == "1") { + push(i, https_inbound(proxy, config)); + } + if (proxy["metrics_server_enable"] == '1') { + push(i, { + listen: "0.0.0.0", + port: int(proxy["metrics_server_port"]) || 18888, + protocol: "dokodemo-door", + settings: { + address: "127.0.0.1" + }, + tag: "metrics" + }); + } + if (proxy["xray_api"] == '1') { + push(i, { + listen: "127.0.0.1", + port: 8080, + protocol: "dokodemo-door", + settings: { + address: "127.0.0.1" + }, + tag: "api" + }); + } + return i; +} + +function outbounds(proxy, config, manual_tproxy, bridge, extra_inbound, fakedns) { + let result = [ + direct_outbound("direct"), + blackhole_outbound(), + dns_server_outbound(), + ...manual_tproxy_outbounds(config, manual_tproxy), + ...bridge_outbounds(config, bridge) + ]; + let outbound_balancers_all = {}; + for (let b in ["tcp_balancer_v4", "udp_balancer_v4", "tcp_balancer_v6", "udp_balancer_v6"]) { + for (let i in balancer(proxy, b, b)) { + if (i != "direct") { + outbound_balancers_all[i] = true; + } + } + } + for (let e in extra_inbound) { + if (e["specify_outbound"] == "1") { + for (let i in balancer(e, "destination", `extra_inbound:${e[".name"]}`)) { + if (i != "direct") { + outbound_balancers_all[i] = true; + } + } + } + } + for (let f in fakedns) { + for (let i in balancer(f, "fake_dns_forward_server_tcp", `fake_dns_tcp:${f[".name"]}`)) { + if (i != "direct") { + outbound_balancers_all[i] = true; + } + } + for (let i in balancer(f, "fake_dns_forward_server_udp", `fake_dns_udp:${f[".name"]}`)) { + if (i != "direct") { + outbound_balancers_all[i] = true; + } + } + } + for (let i in keys(outbound_balancers_all)) { + push(result, ...server_outbound(config[substr(i, -9)], i, config)); + } + return result; +} + +function rules(geoip_existence, proxy, bridge, manual_tproxy, extra_inbound, fakedns) { + const tproxy_tcp_inbound_v4_tags = ["tproxy_tcp_inbound_v4"]; + const tproxy_udp_inbound_v4_tags = ["tproxy_udp_inbound_v4"]; + const tproxy_tcp_inbound_v6_tags = ["tproxy_tcp_inbound_v6"]; + const tproxy_udp_inbound_v6_tags = ["tproxy_udp_inbound_v6"]; + const built_in_tcp_inbounds = [...tproxy_tcp_inbound_v4_tags, "socks_inbound", "https_inbound", "http_inbound"]; + const built_in_udp_inbounds = [...tproxy_udp_inbound_v4_tags, "dns_conf_inbound"]; + const extra_inbound_global_tcp = extra_inbound_global_tcp_tags() || []; + const extra_inbound_global_udp = extra_inbound_global_udp_tags() || []; + let result = [ + ...fake_dns_rules(fakedns), + ...manual_tproxy_rules(manual_tproxy), + ...extra_inbound_rules(extra_inbound), + ...system_route_rules(proxy), + ...bridge_rules(bridge), + ...function () { + let direct_rules = []; + if (geoip_existence) { + if (proxy["geoip_direct_code_list"] != null) { + const geoip_direct_code_list = map(proxy["geoip_direct_code_list"] || [], v => "geoip:" + v); + if (length(geoip_direct_code_list) > 0) { + push(direct_rules, { + type: "field", + inboundTag: [...built_in_tcp_inbounds, ...built_in_udp_inbounds, ...extra_inbound_global_tcp, ...extra_inbound_global_udp], + outboundTag: "direct", + ip: geoip_direct_code_list + }); + } + const geoip_direct_code_list_v6 = map(proxy["geoip_direct_code_list_v6"] || [], v => "geoip:" + v); + if (length(geoip_direct_code_list_v6) > 0) { + push(direct_rules, { + type: "field", + inboundTag: [...tproxy_tcp_inbound_v6_tags, ...tproxy_udp_inbound_v6_tags], + outboundTag: "direct", + ip: geoip_direct_code_list_v6 + }); + } + } + push(direct_rules, { + type: "field", + inboundTag: [...tproxy_tcp_inbound_v6_tags, ...tproxy_udp_inbound_v6_tags, ...built_in_tcp_inbounds, ...built_in_udp_inbounds, ...extra_inbound_global_tcp, ...extra_inbound_global_udp], + outboundTag: "direct", + ip: ["geoip:private"] + }); + } + return direct_rules; + }(), + { + type: "field", + inboundTag: [...tproxy_tcp_inbound_v6_tags], + balancerTag: "tcp_outbound_v6" + }, + { + type: "field", + inboundTag: [...tproxy_udp_inbound_v6_tags], + balancerTag: "udp_outbound_v6" + }, + { + type: "field", + inboundTag: [...built_in_tcp_inbounds, ...extra_inbound_global_tcp], + balancerTag: "tcp_outbound_v4" + }, + { + type: "field", + inboundTag: [...built_in_udp_inbounds, ...extra_inbound_global_udp], + balancerTag: "udp_outbound_v4" + }, + { + type: "field", + inboundTag: dns_server_tags(proxy), + outboundTag: "dns_server_outbound" + }, + ]; + if (proxy["tproxy_sniffing"] == "1") { + if (length(secure_domain_rules(proxy)) > 0) { + splice(result, 0, 0, { + type: "field", + inboundTag: [...tproxy_tcp_inbound_v4_tags, ...extra_inbound_global_tcp], + balancerTag: "tcp_outbound_v4", + domain: secure_domain_rules(proxy), + }, { + type: "field", + inboundTag: [...tproxy_udp_inbound_v4_tags, ...extra_inbound_global_udp], + balancerTag: "udp_outbound_v4", + domain: secure_domain_rules(proxy), + }, { + type: "field", + inboundTag: [...tproxy_tcp_inbound_v6_tags], + balancerTag: "tcp_outbound_v6", + domain: secure_domain_rules(proxy), + }, { + type: "field", + inboundTag: [...tproxy_udp_inbound_v6_tags], + balancerTag: "udp_outbound_v6", + domain: secure_domain_rules(proxy), + }); + } + if (length(blocked_domain_rules(proxy)) > 0) { + splice(result, 0, 0, { + type: "field", + 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, ...extra_inbound_global_udp], + outboundTag: "blackhole_outbound", + domain: blocked_domain_rules(proxy), + }); + } + splice(result, 0, 0, { + type: "field", + 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, ...extra_inbound_global_udp], + outboundTag: "direct", + domain: fast_domain_rules(proxy) + }); + if (proxy["direct_bittorrent"] == "1") { + splice(result, 0, 0, { + type: "field", + outboundTag: "direct", + protocol: ["bittorrent"] + }); + } + } + return result; +} + +function balancers(proxy, extra_inbound, fakedns, balancer_strategy) { + let result = [ + { + "tag": "tcp_outbound_v4", + "selector": balancer(proxy, "tcp_balancer_v4", "tcp_balancer_v4"), + "strategy": { + "type": balancer_strategy + } + }, + { + "tag": "udp_outbound_v4", + "selector": balancer(proxy, "udp_balancer_v4", "udp_balancer_v4"), + "strategy": { + "type": balancer_strategy + } + }, + { + "tag": "tcp_outbound_v6", + "selector": balancer(proxy, "tcp_balancer_v6", "tcp_balancer_v6"), + "strategy": { + "type": balancer_strategy + } + }, + { + "tag": "udp_outbound_v6", + "selector": balancer(proxy, "udp_balancer_v6", "udp_balancer_v6"), + "strategy": { + "type": balancer_strategy + } + }, + ...extra_inbound_balancers(extra_inbound, balancer_strategy), + ...fake_dns_balancers(fakedns, balancer_strategy), + ]; + + return result; +}; + +function observatory(proxy, manual_tproxy) { + if (proxy["observatory"] == "1") { + return { + 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)], + probeInterval: "100ms", + probeUrl: "http://www.apple.com/library/test/success.html" + }; + } + return null; +} + +function gen_config() { + const balancer_strategy = "random"; + + const share_dir = lsdir("/usr/share/xray"); + const geoip_existence = index(share_dir, "geoip.dat") > 0; + + const config = load_config(); + const general = config[filter(keys(config), k => config[k][".type"] == "general")[0]]; + const bridge = map(filter(keys(config), k => config[k][".type"] == "bridge") || [], k => config[k]); + const fakedns = map(filter(keys(config), k => config[k][".type"] == "fakedns") || [], k => config[k]); + const extra_inbound = map(filter(keys(config), k => config[k][".type"] == "extra_inbound") || [], k => config[k]); + const manual_tproxy = map(filter(keys(config), k => config[k][".type"] == "manual_tproxy") || [], k => config[k]); + const custom_configuration_hook = loadstring(general["custom_configuration_hook"] || "return i => i;")(); + + return custom_configuration_hook({ + inbounds: inbounds(general, config, extra_inbound), + outbounds: outbounds(general, config, manual_tproxy, bridge, extra_inbound, fakedns), + dns: dns_conf(general, config, manual_tproxy, fakedns), + fakedns: fake_dns_conf(general), + api: api_conf(general), + metrics: metrics_conf(general), + policy: policy(general), + log: logging(general), + stats: general["stats"] == "1" ? { + place: "holder" + } : null, + observatory: observatory(general, manual_tproxy), + reverse: { + bridges: bridges(bridge) + }, + routing: { + domainStrategy: general["routing_domain_strategy"] || "AsIs", + rules: rules(geoip_existence, general, bridge, manual_tproxy, extra_inbound, fakedns), + balancers: balancers(general, extra_inbound, fakedns, balancer_strategy) + } + }); +} + +print(gen_config()); diff --git a/core/root/usr/share/xray/geoip_list.pb b/core/root/usr/share/xray/geoip_list.pb new file mode 100644 index 00000000..1100406c Binary files /dev/null and b/core/root/usr/share/xray/geoip_list.pb differ diff --git a/core/root/usr/share/xray/ignore_tp_spec_def_gw b/core/root/usr/share/xray/ignore_tp_spec_def_gw new file mode 100644 index 00000000..b42f1678 --- /dev/null +++ b/core/root/usr/share/xray/ignore_tp_spec_def_gw @@ -0,0 +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. diff --git a/core/root/usr/share/xray/include.nft b/core/root/usr/share/xray/include.nft new file mode 100644 index 00000000..1c40e8f5 --- /dev/null +++ b/core/root/usr/share/xray/include.nft @@ -0,0 +1 @@ +include "/var/etc/xray/*.nft" diff --git a/core/root/usr/share/xray/infinite_retry b/core/root/usr/share/xray/infinite_retry new file mode 100644 index 00000000..2350ac21 --- /dev/null +++ b/core/root/usr/share/xray/infinite_retry @@ -0,0 +1 @@ +Remove this file to disable infinite retry on Xray startup. diff --git a/core/root/usr/share/xray/protocol/shadowsocks.mjs b/core/root/usr/share/xray/protocol/shadowsocks.mjs new file mode 100644 index 00000000..b7cd2526 --- /dev/null +++ b/core/root/usr/share/xray/protocol/shadowsocks.mjs @@ -0,0 +1,28 @@ +"use strict"; + +import { stream_settings } from "../common/stream.mjs"; + +export function shadowsocks_outbound(server, tag) { + const stream_settings_object = stream_settings(server, "shadowsocks", tag); + const stream_settings_result = stream_settings_object["stream_settings"]; + const dialer_proxy = stream_settings_object["dialer_proxy"]; + return { + outbound: { + protocol: "shadowsocks", + tag: tag, + settings: { + servers: [ + { + address: server["server"], + port: int(server["server_port"]), + password: server["password"], + method: server["shadowsocks_security"], + uot: server["shadowsocks_udp_over_tcp"] == '1' + } + ] + }, + streamSettings: stream_settings_result + }, + dialer_proxy: dialer_proxy + }; +}; diff --git a/core/root/usr/share/xray/protocol/trojan.mjs b/core/root/usr/share/xray/protocol/trojan.mjs new file mode 100644 index 00000000..ab2783c7 --- /dev/null +++ b/core/root/usr/share/xray/protocol/trojan.mjs @@ -0,0 +1,50 @@ +"use strict"; + +import { stream_settings } from "../common/stream.mjs"; +import { tls_inbound_settings, fallbacks } from "../common/tls.mjs"; + +function trojan_inbound_user(k) { + return { + password: k, + }; +} + +export function trojan_outbound(server, tag) { + const stream_settings_object = stream_settings(server, "trojan", tag); + const stream_settings_result = stream_settings_object["stream_settings"]; + const dialer_proxy = stream_settings_object["dialer_proxy"]; + return { + outbound: { + protocol: "trojan", + tag: tag, + settings: { + servers: [ + { + address: server["server"], + port: int(server["server_port"]), + password: server["password"] + } + ] + }, + streamSettings: stream_settings_result + }, + dialer_proxy: dialer_proxy + }; +}; + +export function https_trojan_inbound(proxy, config) { + return { + port: proxy["web_server_port"] || 443, + protocol: "trojan", + tag: "https_inbound", + settings: { + clients: map(proxy["web_server_password"], trojan_inbound_user), + fallbacks: fallbacks(proxy, config) + }, + streamSettings: { + network: "tcp", + security: proxy["trojan_tls"], + tlsSettings: proxy["trojan_tls"] == "tls" ? tls_inbound_settings(proxy, "trojan") : null + } + }; +}; diff --git a/core/root/usr/share/xray/protocol/vless.mjs b/core/root/usr/share/xray/protocol/vless.mjs new file mode 100644 index 00000000..7f223b47 --- /dev/null +++ b/core/root/usr/share/xray/protocol/vless.mjs @@ -0,0 +1,77 @@ +"use strict"; + +import { stream_settings } from "../common/stream.mjs"; +import { tls_inbound_settings, reality_inbound_settings, fallbacks } from "../common/tls.mjs"; + +function vless_inbound_user(k, flow) { + return { + id: k, + flow: flow, + }; +} + +export function vless_outbound(server, tag) { + let flow = null; + if (server["vless_tls"] == "tls") { + flow = server["vless_flow_tls"]; + } else if (server["vless_tls"] == "reality") { + flow = server["vless_flow_reality"]; + } + if (flow == "none") { + flow = null; + } + const stream_settings_object = stream_settings(server, "vless", tag); + const stream_settings_result = stream_settings_object["stream_settings"]; + const dialer_proxy = stream_settings_object["dialer_proxy"]; + return { + outbound: { + protocol: "vless", + tag: tag, + settings: { + vnext: [ + { + address: server["server"], + port: int(server["server_port"]), + users: [ + { + id: server["password"], + flow: flow, + encryption: server["vless_encryption"] + } + ] + } + ] + }, + streamSettings: stream_settings_result + }, + dialer_proxy: dialer_proxy + }; +}; + +export function https_vless_inbound(proxy, config) { + let flow = null; + if (proxy["vless_tls"] == "tls") { + flow = proxy["vless_flow_tls"]; + } else if (proxy["vless_tls"] == "reality") { + flow = proxy["vless_flow_reality"]; + } + if (flow == "none") { + flow = null; + } + return { + port: proxy["web_server_port"] || 443, + protocol: "vless", + tag: "https_inbound", + settings: { + clients: map(proxy["web_server_password"], k => vless_inbound_user(k, flow)), + decryption: "none", + fallbacks: fallbacks(proxy, config) + }, + streamSettings: { + network: "tcp", + security: proxy["vless_tls"], + tlsSettings: proxy["vless_tls"] == "tls" ? tls_inbound_settings(proxy, "vless") : null, + realitySettings: proxy["vless_tls"] == "reality" ? reality_inbound_settings(proxy, "vless") : null, + } + }; +}; diff --git a/core/root/usr/share/xray/protocol/vmess.mjs b/core/root/usr/share/xray/protocol/vmess.mjs new file mode 100644 index 00000000..10356b7b --- /dev/null +++ b/core/root/usr/share/xray/protocol/vmess.mjs @@ -0,0 +1,32 @@ +"use strict"; + +import { stream_settings } from "../common/stream.mjs"; + +export function vmess_outbound(server, tag) { + const stream_settings_object = stream_settings(server, "vmess", tag); + const stream_settings_result = stream_settings_object["stream_settings"]; + const dialer_proxy = stream_settings_object["dialer_proxy"]; + return { + outbound: { + protocol: "vmess", + tag: tag, + settings: { + vnext: [ + { + address: server["server"], + port: int(server["server_port"]), + users: [ + { + id: server["password"], + alterId: int(server["alter_id"]), + security: server["vmess_security"] + } + ] + } + ] + }, + streamSettings: stream_settings_result + }, + dialer_proxy: dialer_proxy + }; +}; diff --git a/core/root/usr/share/xray/restart_dnsmasq_on_iface_change b/core/root/usr/share/xray/restart_dnsmasq_on_iface_change new file mode 100644 index 00000000..ee98f8b8 --- /dev/null +++ b/core/root/usr/share/xray/restart_dnsmasq_on_iface_change @@ -0,0 +1,2 @@ +If you are using dnsmasq v2.87, keep this file. +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. diff --git a/core/root/usr/share/xray/rlimit_data_large b/core/root/usr/share/xray/rlimit_data_large new file mode 100644 index 00000000..5b08ac71 --- /dev/null +++ b/core/root/usr/share/xray/rlimit_data_large @@ -0,0 +1 @@ +300000000 333333333 diff --git a/core/root/usr/share/xray/rlimit_data_small b/core/root/usr/share/xray/rlimit_data_small new file mode 100644 index 00000000..f4645fb0 --- /dev/null +++ b/core/root/usr/share/xray/rlimit_data_small @@ -0,0 +1 @@ +44444444 55555555 \ No newline at end of file diff --git a/core/root/usr/share/xray/rlimit_nofile_large b/core/root/usr/share/xray/rlimit_nofile_large new file mode 100644 index 00000000..eb753e47 --- /dev/null +++ b/core/root/usr/share/xray/rlimit_nofile_large @@ -0,0 +1 @@ +8192 16384 \ No newline at end of file diff --git a/core/root/www/luci-static/resources/view/xray/core.js b/core/root/www/luci-static/resources/view/xray/core.js new file mode 100644 index 00000000..4ab55edd --- /dev/null +++ b/core/root/www/luci-static/resources/view/xray/core.js @@ -0,0 +1,891 @@ +'use strict'; +'require form'; +'require fs'; +'require uci'; +'require view'; + +const variant = "xray_core"; + +function validate_object(id, a) { + if (a == "") { + return true; + } + try { + const t = JSON.parse(a); + if (Array.isArray(t)) { + return "TypeError: Requires an object here, got an array"; + } + if (t instanceof Object) { + return true; + } + return "TypeError: Requires an object here, got a " + typeof t; + } catch (e) { + return e; + } +} + +function fingerprints(o) { + o.value("chrome", "chrome"); + o.value("firefox", "firefox"); + o.value("safari", "safari"); + o.value("ios", "ios"); + o.value("android", "android"); + o.value("edge", "edge"); + o.value("360", "360"); + o.value("qq", "qq"); + o.value("random", "random"); + o.value("randomized", "randomized"); +} + +function add_flow_and_stream_security_conf(s, tab_name, depends_field_name, protocol_name, have_tls_flow, client_side) { + let o = s.taboption(tab_name, form.ListValue, `${protocol_name}_tls`, _(`[${protocol_name}] Stream Security`)); + let odep = {}; + odep[depends_field_name] = protocol_name; + if (client_side) { + o.depends(depends_field_name, protocol_name); + o.value("none", "None"); + } else { + odep["web_server_enable"] = "1"; + } + o.value("tls", "TLS"); + if (have_tls_flow) { + o.value("reality", "REALITY (Experimental)"); + } + o.depends(odep); + o.rmempty = false; + o.modalonly = true; + + if (have_tls_flow) { + let flow_tls = s.taboption(tab_name, form.ListValue, `${protocol_name}_flow_tls`, _(`[${protocol_name}][tls] Flow`)); + let flow_tls_dep = {}; + flow_tls_dep[depends_field_name] = protocol_name; + flow_tls_dep[`${protocol_name}_tls`] = "tls"; + flow_tls.value("none", "none"); + flow_tls.value("xtls-rprx-vision", "xtls-rprx-vision"); + flow_tls.value("xtls-rprx-vision-udp443", "xtls-rprx-vision-udp443"); + if (client_side) { + // wait for some other things + } else { + flow_tls_dep["web_server_enable"] = "1"; + } + flow_tls.depends(flow_tls_dep); + flow_tls.rmempty = false; + flow_tls.modalonly = true; + + let flow_reality = s.taboption(tab_name, form.ListValue, `${protocol_name}_flow_reality`, _(`[${protocol_name}][reality] Flow`)); + let flow_reality_dep = {}; + flow_reality_dep[depends_field_name] = protocol_name; + flow_reality_dep[`${protocol_name}_tls`] = "reality"; + flow_reality.value("none", "none"); + flow_reality.value("xtls-rprx-vision", "xtls-rprx-vision"); + flow_reality.value("xtls-rprx-vision-udp443", "xtls-rprx-vision-udp443"); + if (client_side) { + // wait for some other things + } else { + flow_reality_dep["web_server_enable"] = "1"; + } + flow_reality.depends(flow_reality_dep); + flow_reality.rmempty = false; + flow_reality.modalonly = true; + + o = s.taboption(tab_name, form.Flag, `${protocol_name}_reality_show`, _(`[${protocol_name}][reality] Show`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.modalonly = true; + } + + if (client_side) { + o = s.taboption(tab_name, form.Value, `${protocol_name}_tls_host`, _(`[${protocol_name}][tls] Server Name`)); + o.depends(`${protocol_name}_tls`, "tls"); + o.modalonly = true; + + o = s.taboption(tab_name, form.Flag, `${protocol_name}_tls_insecure`, _(`[${protocol_name}][tls] Allow Insecure`)); + o.depends(`${protocol_name}_tls`, "tls"); + o.rmempty = false; + o.modalonly = true; + + o = s.taboption(tab_name, form.Value, `${protocol_name}_tls_fingerprint`, _(`[${protocol_name}][tls] Fingerprint`)); + o.depends(`${protocol_name}_tls`, "tls"); + o.value("", "(not set)"); + fingerprints(o); + o.modalonly = true; + + o = s.taboption(tab_name, form.DynamicList, `${protocol_name}_tls_alpn`, _(`[${protocol_name}][tls] ALPN`)); + o.depends(`${protocol_name}_tls`, "tls"); + o.value("h2", "h2"); + o.value("http/1.1", "http/1.1"); + o.modalonly = true; + + if (have_tls_flow) { + o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_fingerprint`, _(`[${protocol_name}][reality] Fingerprint`)); + o.depends(`${protocol_name}_tls`, "reality"); + fingerprints(o); + o.rmempty = false; + o.modalonly = true; + + o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_server_name`, _(`[${protocol_name}][reality] Server Name`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.modalonly = true; + + o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_public_key`, _(`[${protocol_name}][reality] Public Key`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.modalonly = true; + + o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_short_id`, _(`[${protocol_name}][reality] Short Id`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.modalonly = true; + + o = s.taboption(tab_name, form.Value, `${protocol_name}_spider_x`, _(`[${protocol_name}][reality] SpiderX`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.modalonly = true; + } + } else { + let tls_cert_key_dep = { "web_server_enable": "1" }; + tls_cert_key_dep[`${protocol_name}_tls`] = "tls"; + o = s.taboption(tab_name, form.FileUpload, `${protocol_name}_tls_cert_file`, _(`[${protocol_name}][tls] Certificate File`)); + o.root_directory = "/etc/luci-uploads/xray"; + o.depends(tls_cert_key_dep); + + o = s.taboption(tab_name, form.FileUpload, `${protocol_name}_tls_key_file`, _(`[${protocol_name}][tls] Private Key File`)); + o.root_directory = "/etc/luci-uploads/xray"; + o.depends(tls_cert_key_dep); + + if (have_tls_flow) { + o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_dest`, _(`[${protocol_name}][reality] Dest`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.datatype = "hostport"; + o.modalonly = true; + + o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_xver`, _(`[${protocol_name}][reality] Xver`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.datatype = "integer"; + o.modalonly = true; + + o = s.taboption(tab_name, form.DynamicList, `${protocol_name}_reality_server_names`, _(`[${protocol_name}][reality] Server Names`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.modalonly = true; + + o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_private_key`, _(`[${protocol_name}][reality] Private Key`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.modalonly = true; + + o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_min_client_ver`, _(`[${protocol_name}][reality] Min Client Ver`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.modalonly = true; + + o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_max_client_ver`, _(`[${protocol_name}][reality] Max Client Ver`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.modalonly = true; + + o = s.taboption(tab_name, form.Value, `${protocol_name}_reality_max_time_diff`, _(`[${protocol_name}][reality] Max Time Diff`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.datatype = "integer"; + o.modalonly = true; + + o = s.taboption(tab_name, form.DynamicList, `${protocol_name}_reality_short_ids`, _(`[${protocol_name}][reality] Short Ids`)); + o.depends(`${protocol_name}_tls`, "reality"); + o.modalonly = true; + } + } +} + +function check_resource_files(load_result) { + let geoip_existence = false; + let geoip_size = 0; + let geosite_existence = false; + let geosite_size = 0; + let xray_bin_default = false; + let xray_running = false; + for (const f of load_result) { + if (f.name == "xray") { + xray_bin_default = true; + } + if (f.name == "xray.pid") { + xray_running = true; + } + if (f.name == "geoip.dat") { + geoip_existence = true; + geoip_size = '%.2mB'.format(f.size); + } + if (f.name == "geosite.dat") { + geosite_existence = true; + geosite_size = '%.2mB'.format(f.size); + } + } + return { + geoip_existence: geoip_existence, + geoip_size: geoip_size, + geosite_existence: geosite_existence, + geosite_size: geosite_size, + xray_bin_default: xray_bin_default, + xray_running: xray_running, + }; +} + +return view.extend({ + load: function () { + return Promise.all([ + uci.load(variant), + fs.list("/usr/share/xray") + ]); + }, + + render: function (load_result) { + const config_data = load_result[0]; + const { geoip_existence, geoip_size, geosite_existence, geosite_size, xray_bin_default, xray_running } = check_resource_files(load_result[1]); + const status_text = xray_running ? _("[Xray is running]") : _("[Xray is stopped]"); + + let asset_file_status = _('WARNING: at least one of asset files (geoip.dat, geosite.dat) is not found under /usr/share/xray. Xray may not work properly. See here for help.'); + if (geoip_existence) { + if (geosite_existence) { + asset_file_status = _('Asset files check: ') + `geoip.dat ${geoip_size}; geosite.dat ${geosite_size}. ` + _('Report issues or request for features here.'); + } + } + + const m = new form.Map(variant, _('Xray (core)'), status_text + " " + asset_file_status); + + let s, o, ss; + + s = m.section(form.TypedSection, 'general'); + s.addremove = false; + s.anonymous = true; + + s.tab('general', _('General Settings')); + + o = s.taboption('general', form.Value, 'xray_bin', _('Xray Executable Path')); + o.rmempty = false; + if (xray_bin_default) { + o.value("/usr/bin/xray", _("/usr/bin/xray (default, exist)")); + } + + let tcp_balancer_v4 = s.taboption('general', form.MultiValue, 'tcp_balancer_v4', _('TCP Server (IPv4)'), _("Select multiple outbound servers to enable load balancing. Select none to disable TCP Outbound.")); + tcp_balancer_v4.datatype = "uciname"; + + let udp_balancer_v4 = s.taboption('general', form.MultiValue, 'udp_balancer_v4', _('UDP Server (IPv4)'), _("Select multiple outbound servers to enable load balancing. Select none to disable UDP Outbound.")); + udp_balancer_v4.datatype = "uciname"; + + let tcp_balancer_v6 = s.taboption('general', form.MultiValue, 'tcp_balancer_v6', _('TCP Server (IPv6)'), _("Select multiple outbound servers to enable load balancing. Select none to disable TCP Outbound.")); + tcp_balancer_v6.datatype = "uciname"; + + let udp_balancer_v6 = s.taboption('general', form.MultiValue, 'udp_balancer_v6', _('UDP Server (IPv6)'), _("Select multiple outbound servers to enable load balancing. Select none to disable UDP Outbound.")); + udp_balancer_v6.datatype = "uciname"; + + const servers = uci.sections(config_data, "servers"); + if (servers.length == 0) { + tcp_balancer_v4.value("direct", _("No server configured")); + udp_balancer_v4.value("direct", _("No server configured")); + tcp_balancer_v6.value("direct", _("No server configured")); + udp_balancer_v6.value("direct", _("No server configured")); + + tcp_balancer_v4.readonly = true; + udp_balancer_v4.readonly = true; + tcp_balancer_v6.readonly = true; + udp_balancer_v6.readonly = true; + } else { + for (const v of servers) { + tcp_balancer_v4.value(v[".name"], v.alias || v.server + ":" + v.server_port); + udp_balancer_v4.value(v[".name"], v.alias || v.server + ":" + v.server_port); + tcp_balancer_v6.value(v[".name"], v.alias || v.server + ":" + v.server_port); + udp_balancer_v6.value(v[".name"], v.alias || v.server + ":" + v.server_port); + } + } + + o = s.taboption('general', form.Flag, 'transparent_proxy_enable', _('Enable Transparent Proxy'), _('This enables DNS query forwarding and TProxy for both TCP and UDP connections.')); + + o = s.taboption('general', form.Flag, 'tproxy_sniffing', _('Enable Sniffing'), _('If sniffing is enabled, requests will be routed according to domain settings in "DNS Settings" tab.')); + o.depends("transparent_proxy_enable", "1"); + + o = s.taboption('general', 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.')); + o.depends({ "transparent_proxy_enable": "1", "tproxy_sniffing": "1" }); + + o = s.taboption('general', form.Flag, 'direct_bittorrent', _('Bittorrent Direct'), _("If enabled, no bittorrent request will be forwarded through Xray.")); + o.depends({ "transparent_proxy_enable": "1", "tproxy_sniffing": "1" }); + + o = s.taboption('general', form.SectionValue, "xray_servers", form.GridSection, 'servers', _('Xray Servers'), _("Servers are referenced by index (order in the following list). Deleting servers may result in changes of upstream servers actually used by proxy and bridge.")); + ss = o.subsection; + ss.sortable = false; + ss.anonymous = true; + ss.addremove = true; + + ss.tab('general', _('General Settings')); + + o = ss.taboption('general', form.Value, "alias", _("Alias (optional)")); + o.optional = true; + + o = ss.taboption('general', form.Value, 'server', _('Server Hostname')); + o.datatype = 'host'; + o.rmempty = false; + + o = ss.taboption('general', form.ListValue, 'domain_strategy', _('Domain Strategy'), _("Whether to use IPv4 or IPv6 address if Server Hostname is a domain.")); + o.value("UseIP"); + o.value("UseIPv4"); + o.value("UseIPv6"); + o.default = "UseIP"; + o.modalonly = true; + + o = ss.taboption('general', form.Value, 'domain_resolve_dns', _('Resolve Domain via DNS'), _("Specify a DNS to resolve server hostname. Only works for main balancers (those on General Settings tab).")); + o.datatype = 'hostport'; + o.modalonly = true; + + o = ss.taboption('general', form.Value, 'server_port', _('Server Port')); + o.datatype = 'port'; + o.rmempty = false; + + o = ss.taboption('general', form.Value, 'password', _('UserId / Password'), _('Fill user_id for vmess / VLESS, or password for shadowsocks / trojan (also supports Xray UUID Mapping)')); + o.modalonly = true; + o.rmempty = false; + + ss.tab('protocol', _('Protocol Settings')); + + o = ss.taboption('protocol', form.ListValue, "protocol", _("Protocol")); + o.value("vmess", "VMess"); + o.value("vless", "VLESS"); + o.value("trojan", "Trojan"); + o.value("shadowsocks", "Shadowsocks"); + o.rmempty = false; + + add_flow_and_stream_security_conf(ss, "protocol", "protocol", "trojan", false, true); + + o = ss.taboption('protocol', form.ListValue, "shadowsocks_security", _("[shadowsocks] Encrypt Method")); + o.depends("protocol", "shadowsocks"); + o.value("none", "none"); + o.value("aes-256-gcm", "aes-256-gcm"); + o.value("aes-128-gcm", "aes-128-gcm"); + o.value("chacha20-poly1305", "chacha20-poly1305"); + o.value("2022-blake3-aes-128-gcm", "2022-blake3-aes-128-gcm"); + o.value("2022-blake3-aes-256-gcm", "2022-blake3-aes-256-gcm"); + o.value("2022-blake3-chacha20-poly1305", "2022-blake3-chacha20-poly1305"); + o.rmempty = false; + o.modalonly = true; + + o = ss.taboption('protocol', form.Flag, 'shadowsocks_udp_over_tcp', _('[shadowsocks] UDP over TCP'), _('Only available for shadowsocks-2022 ciphers (2022-*)')); + o.depends("shadowsocks_security", /2022/); + o.rmempty = false; + o.modalonly = true; + + add_flow_and_stream_security_conf(ss, "protocol", "protocol", "shadowsocks", false, true); + + o = ss.taboption('protocol', form.ListValue, "vmess_security", _("[vmess] Encrypt Method")); + o.depends("protocol", "vmess"); + o.value("none", "none"); + o.value("auto", "auto"); + o.value("aes-128-gcm", "aes-128-gcm"); + o.value("chacha20-poly1305", "chacha20-poly1305"); + o.rmempty = false; + o.modalonly = true; + + o = ss.taboption('protocol', form.ListValue, "vmess_alter_id", _("[vmess] AlterId"), _("Deprecated. Make sure you always use VMessAEAD.")); + o.depends("protocol", "vmess"); + o.value(0, "0 (this enables VMessAEAD)"); + o.value(1, "1"); + o.value(4, "4"); + o.value(16, "16"); + o.value(64, "64"); + o.value(256, "256"); + o.rmempty = false; + o.modalonly = true; + + add_flow_and_stream_security_conf(ss, "protocol", "protocol", "vmess", false, true); + + o = ss.taboption('protocol', form.ListValue, "vless_encryption", _("[vless] Encrypt Method")); + o.depends("protocol", "vless"); + o.value("none", "none"); + o.rmempty = false; + o.modalonly = true; + + add_flow_and_stream_security_conf(ss, "protocol", "protocol", "vless", true, true); + + ss.tab('transport', _('Transport Settings')); + + o = ss.taboption('transport', form.ListValue, 'transport', _('Transport')); + o.value("tcp", "TCP"); + o.value("mkcp", "mKCP"); + o.value("ws", "WebSocket"); + o.value("h2", "HTTP/2"); + o.value("quic", "QUIC"); + o.value("grpc", "gRPC"); + o.rmempty = false; + + o = ss.taboption('transport', form.ListValue, "tcp_guise", _("[tcp] Fake Header Type")); + o.depends("transport", "tcp"); + o.value("none", _("None")); + o.value("http", "HTTP"); + o.modalonly = true; + + o = ss.taboption('transport', form.DynamicList, "http_host", _("[tcp][fake_http] Host")); + o.depends("tcp_guise", "http"); + o.rmempty = false; + o.modalonly = true; + + o = ss.taboption('transport', form.DynamicList, "http_path", _("[tcp][fake_http] Path")); + o.depends("tcp_guise", "http"); + o.modalonly = true; + + o = ss.taboption('transport', form.ListValue, "mkcp_guise", _("[mkcp] Fake Header Type")); + o.depends("transport", "mkcp"); + o.value("none", _("None")); + o.value("srtp", _("VideoCall (SRTP)")); + o.value("utp", _("BitTorrent (uTP)")); + o.value("wechat-video", _("WechatVideo")); + o.value("dtls", "DTLS 1.2"); + o.value("wireguard", "WireGuard"); + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "mkcp_mtu", _("[mkcp] Maximum Transmission Unit")); + o.datatype = "uinteger"; + o.depends("transport", "mkcp"); + o.placeholder = 1350; + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "mkcp_tti", _("[mkcp] Transmission Time Interval")); + o.datatype = "uinteger"; + o.depends("transport", "mkcp"); + o.placeholder = 50; + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "mkcp_uplink_capacity", _("[mkcp] Uplink Capacity")); + o.datatype = "uinteger"; + o.depends("transport", "mkcp"); + o.placeholder = 5; + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "mkcp_downlink_capacity", _("[mkcp] Downlink Capacity")); + o.datatype = "uinteger"; + o.depends("transport", "mkcp"); + o.placeholder = 20; + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "mkcp_read_buffer_size", _("[mkcp] Read Buffer Size")); + o.datatype = "uinteger"; + o.depends("transport", "mkcp"); + o.placeholder = 2; + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "mkcp_write_buffer_size", _("[mkcp] Write Buffer Size")); + o.datatype = "uinteger"; + o.depends("transport", "mkcp"); + o.placeholder = 2; + o.modalonly = true; + + o = ss.taboption('transport', form.Flag, "mkcp_congestion", _("[mkcp] Congestion Control")); + o.depends("transport", "mkcp"); + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "mkcp_seed", _("[mkcp] Seed")); + o.depends("transport", "mkcp"); + o.modalonly = true; + + o = ss.taboption('transport', form.ListValue, "quic_security", _("[quic] Security")); + o.depends("transport", "quic"); + o.value("none", "none"); + o.value("aes-128-gcm", "aes-128-gcm"); + o.value("chacha20-poly1305", "chacha20-poly1305"); + o.rmempty = false; + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "quic_key", _("[quic] Key")); + o.depends("transport", "quic"); + o.modalonly = true; + + o = ss.taboption('transport', form.ListValue, "quic_guise", _("[quic] Fake Header Type")); + o.depends("transport", "quic"); + o.value("none", _("None")); + o.value("srtp", _("VideoCall (SRTP)")); + o.value("utp", _("BitTorrent (uTP)")); + o.value("wechat-video", _("WechatVideo")); + o.value("dtls", "DTLS 1.2"); + o.value("wireguard", "WireGuard"); + o.default = "none"; + o.modalonly = true; + + o = ss.taboption('transport', form.DynamicList, "h2_host", _("[http2] Host")); + o.depends("transport", "h2"); + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "h2_path", _("[http2] Path")); + o.depends("transport", "h2"); + o.modalonly = true; + + o = ss.taboption('transport', form.Flag, "h2_health_check", _("[h2] Health Check")); + o.depends("transport", "h2"); + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "h2_read_idle_timeout", _("[h2] Read Idle Timeout")); + o.depends({ "transport": "h2", "h2_health_check": "1" }); + o.modalonly = true; + o.placeholder = 10; + o.datatype = 'integer'; + + o = ss.taboption('transport', form.Value, "h2_health_check_timeout", _("[h2] Health Check Timeout")); + o.depends({ "transport": "h2", "h2_health_check": "1" }); + o.modalonly = true; + o.placeholder = 20; + o.datatype = 'integer'; + + o = ss.taboption('transport', form.Value, "grpc_service_name", _("[grpc] Service Name")); + o.depends("transport", "grpc"); + o.modalonly = true; + + o = ss.taboption('transport', form.Flag, "grpc_multi_mode", _("[grpc] Multi Mode")); + o.depends("transport", "grpc"); + o.modalonly = true; + + o = ss.taboption('transport', form.Flag, "grpc_health_check", _("[grpc] Health Check")); + o.depends("transport", "grpc"); + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "grpc_idle_timeout", _("[grpc] Idle Timeout")); + o.depends({ "transport": "grpc", "grpc_health_check": "1" }); + o.modalonly = true; + o.placeholder = 10; + o.datatype = 'integer'; + + o = ss.taboption('transport', form.Value, "grpc_health_check_timeout", _("[grpc] Health Check Timeout")); + o.depends({ "transport": "grpc", "grpc_health_check": "1" }); + o.modalonly = true; + o.placeholder = 20; + o.datatype = 'integer'; + + o = ss.taboption('transport', form.Flag, "grpc_permit_without_stream", _("[grpc] Permit Without Stream")); + o.depends({ "transport": "grpc", "grpc_health_check": "1" }); + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "grpc_initial_windows_size", _("[grpc] Initial Windows Size"), _("Set to 524288 to avoid Cloudflare sending ENHANCE_YOUR_CALM.")); + o.depends("transport", "grpc"); + o.modalonly = true; + o.placeholder = 0; + o.datatype = 'integer'; + + o = ss.taboption('transport', form.Value, "ws_host", _("[websocket] Host")); + o.depends("transport", "ws"); + o.modalonly = true; + + o = ss.taboption('transport', form.Value, "ws_path", _("[websocket] Path")); + o.depends("transport", "ws"); + o.modalonly = true; + + o = ss.taboption('transport', form.ListValue, 'dialer_proxy', _('Dialer Proxy'), _('Similar to ProxySettings.Tag')); + o.datatype = "uciname"; + o.value("disabled", _("Disabled")); + for (const v of uci.sections(config_data, "servers")) { + o.value(v[".name"], v.alias || v.server + ":" + v.server_port); + } + o.modalonly = true; + + ss.tab('custom', _('Custom Options')); + + o = ss.taboption('custom', form.TextValue, 'custom_config', _('Custom Configurations'), _('Configurations here override settings in the previous tabs with the following rules:
  1. Object values will be replaced recursively so settings in previous tabs matter.
  2. Arrays will be replaced entirely instead of being merged.
  3. Tag tag is ignored.
Override rules here may be changed later. Use this only for experimental or pre-release features.')); + o.modalonly = true; + o.monospace = true; + o.rows = 10; + o.validate = validate_object; + + s.tab('proxy', _('Proxy Settings')); + + o = s.taboption('proxy', form.Value, 'socks_port', _('Socks5 proxy port')); + o.datatype = 'port'; + o.placeholder = 1080; + + o = s.taboption('proxy', form.Value, 'http_port', _('HTTP proxy port')); + o.datatype = 'port'; + o.placeholder = 1081; + + o = s.taboption('proxy', form.Value, 'tproxy_port_tcp_v4', _('Transparent proxy port (TCP4)')); + o.datatype = 'port'; + o.placeholder = 1082; + + o = s.taboption('proxy', form.Value, 'tproxy_port_tcp_v6', _('Transparent proxy port (TCP6)')); + o.datatype = 'port'; + o.placeholder = 1083; + + o = s.taboption('proxy', form.Value, 'tproxy_port_udp_v4', _('Transparent proxy port (UDP4)')); + o.datatype = 'port'; + o.placeholder = 1084; + + o = s.taboption('proxy', form.Value, 'tproxy_port_udp_v6', _('Transparent proxy port (UDP6)')); + o.datatype = 'port'; + o.placeholder = 1085; + + o = s.taboption('proxy', form.DynamicList, 'uids_direct', _('Bypass tproxy for uids'), _("Processes started by users with these uids won't be forwarded through Xray.")); + o.datatype = "integer"; + + o = s.taboption('proxy', form.DynamicList, 'gids_direct', _('Bypass tproxy for gids'), _("Processes started by users in groups with these gids won't be forwarded through Xray.")); + o.datatype = "integer"; + + o = s.taboption('proxy', form.Value, 'firewall_priority', _('Priority for firewall rules'), _('See firewall status page for rules Xray used and Netfilter Internal Priority for reference.')); + o.datatype = 'range(-49, 49)'; + o.placeholder = 10; + + s.tab('dns', _('DNS Settings')); + + o = s.taboption('dns', form.Value, 'fast_dns', _('Fast DNS'), _("DNS for resolving outbound domains and following bypassed domains")); + o.datatype = 'or(ip4addr, ip4addrport)'; + o.placeholder = "223.5.5.5:53"; + + if (geosite_existence) { + o = s.taboption('dns', form.DynamicList, "bypassed_domain_rules", _('Bypassed domain rules'), _('Specify rules like geosite:cn or domain:bilibili.com. See documentation for details.')); + } else { + o = s.taboption('dns', form.DynamicList, 'bypassed_domain_rules', _('Bypassed domain rules'), _('Specify rules like domain:bilibili.com or see documentation for details.
In order to use Geosite rules you need a valid resource file /usr/share/xray/geosite.dat.
Compile your firmware again with data files to use Geosite rules, or download one and upload it to your router.')); + } + + o = s.taboption('dns', form.Value, 'secure_dns', _('Secure DNS'), _("DNS for resolving known polluted domains (specify forwarded domain rules here)")); + o.datatype = 'or(ip4addr, ip4addrport)'; + o.placeholder = "8.8.8.8:53"; + + if (geosite_existence) { + o = s.taboption('dns', form.DynamicList, "forwarded_domain_rules", _('Forwarded domain rules'), _('Specify rules like geosite:geolocation-!cn or domain:youtube.com. See documentation for details.')); + } else { + o = s.taboption('dns', form.DynamicList, 'forwarded_domain_rules', _('Forwarded domain rules'), _('Specify rules like domain:youtube.com or see documentation for details.
In order to use Geosite rules you need a valid resource file /usr/share/xray/geosite.dat.
Compile your firmware again with data files to use Geosite rules, or download one and upload it to your router.')); + } + + o = s.taboption('dns', form.Value, 'default_dns', _('Default DNS'), _("DNS for resolving other sites (not in the rules above) and DNS records other than A or AAAA (TXT and MX for example)")); + o.datatype = 'or(ip4addr, ip4addrport)'; + o.placeholder = "1.1.1.1:53"; + + if (geosite_existence) { + o = s.taboption('dns', form.DynamicList, "blocked_domain_rules", _('Blocked domain rules'), _('Specify rules like geosite:category-ads or domain:baidu.com. See documentation for details.')); + } else { + o = s.taboption('dns', form.DynamicList, 'blocked_domain_rules', _('Blocked domain rules'), _('Specify rules like domain:baidu.com or see documentation for details.
In order to use Geosite rules you need a valid resource file /usr/share/xray/geosite.dat.
Compile your firmware again with data files to use Geosite rules, or download one and upload it to your router.')); + } + + o = s.taboption('dns', form.Flag, 'blocked_to_loopback', _('Blocked to loopback'), _('Return 127.127.127.127 as response for blocked domain rules. If not selected, NXDOMAIN will be returned.')); + o.modalonly = true; + + o = s.taboption('dns', form.Value, 'dns_port', _('Xray DNS Server Port'), _("Do not use port 53 (dnsmasq), port 5353 (mDNS) or other common ports")); + o.datatype = 'port'; + o.placeholder = 5300; + + o = s.taboption('dns', form.Value, 'dns_count', _('Extra DNS Server Ports'), _('Listen for DNS Requests on multiple ports (all of which serves as dnsmasq upstream servers).
For example if Xray DNS Server Port is 5300 and use 3 extra ports, 5300 - 5303 will be used for DNS requests.
Increasing this value may help reduce the possibility of temporary DNS lookup failures.')); + o.datatype = 'range(0, 50)'; + o.placeholder = 3; + + o = s.taboption('dns', form.ListValue, 'routing_domain_strategy', _('Routing Domain Strategy'), _("Domain resolution strategy when matching domain against rules. (For tproxy, this is effective only when sniffing is enabled.)")); + o.value("AsIs", "AsIs"); + o.value("IPIfNonMatch", "IPIfNonMatch"); + o.value("IPOnDemand", "IPOnDemand"); + o.default = "AsIs"; + o.rmempty = false; + + s.tab('transparent_proxy_rules', _('Transparent Proxy Rules')); + + if (geoip_existence) { + let geoip_direct_code_list = s.taboption('transparent_proxy_rules', form.DynamicList, 'geoip_direct_code_list', _('GeoIP Direct Code List (IPv4)'), _("Hosts in these GeoIP sets will not be forwarded through Xray. Remove all items to forward all non-private hosts.")); + geoip_direct_code_list.datatype = "string"; + geoip_direct_code_list.value("cn", "cn"); + geoip_direct_code_list.value("telegram", "telegram"); + + let geoip_direct_code_list_v6 = s.taboption('transparent_proxy_rules', form.DynamicList, 'geoip_direct_code_list_v6', _('GeoIP Direct Code List (IPv6)'), _("Hosts in these GeoIP sets will not be forwarded through Xray. Remove all items to forward all non-private hosts.")); + geoip_direct_code_list_v6.datatype = "string"; + geoip_direct_code_list_v6.value("cn", "cn"); + geoip_direct_code_list_v6.value("telegram", "telegram"); + } else { + let geoip_direct_code_list = s.taboption('transparent_proxy_rules', form.DynamicList, 'geoip_direct_code_list', _('GeoIP Direct Code List (IPv4)'), _("Resource file /usr/share/xray/geoip.dat not exist. All network traffic will be forwarded.
Compile your firmware again with data files to use this feature, or
download one (maybe disable transparent proxy first) and upload it to your router.")); + geoip_direct_code_list.readonly = true; + geoip_direct_code_list.datatype = "string"; + + let geoip_direct_code_list_v6 = s.taboption('transparent_proxy_rules', form.DynamicList, 'geoip_direct_code_list_v6', _('GeoIP Direct Code List (IPv6)'), _("Resource file /usr/share/xray/geoip.dat not exist. All network traffic will be forwarded.
Compile your firmware again with data files to use this feature, or
download one (maybe disable transparent proxy first) and upload it to your router.")); + geoip_direct_code_list_v6.readonly = true; + geoip_direct_code_list_v6.datatype = "string"; + } + + o = s.taboption('transparent_proxy_rules', form.DynamicList, "wan_bp_ips", _("Bypassed IP"), _("Requests to these IPs won't be forwarded through Xray.")); + o.datatype = "ipaddr"; + + o = s.taboption('transparent_proxy_rules', form.DynamicList, "wan_fw_ips", _("Forwarded IP"), _("Requests to these IPs will always be handled by Xray (but still might be bypassed by Xray itself, like private addresses).
Useful for some really strange network. If you really need to forward private addresses, try Manual Transparent Proxy below.")); + o.datatype = "ipaddr"; + + o = s.taboption('transparent_proxy_rules', form.ListValue, 'transparent_default_port_policy', _('Default Ports Policy')); + o.value("forwarded", _("Forwarded")); + o.value("bypassed", _("Bypassed")); + o.default = "forwarded"; + + o = s.taboption('transparent_proxy_rules', form.DynamicList, "wan_fw_tcp_ports", _("Forwarded TCP Ports"), _("Requests to these TCP Ports will be forwarded through Xray. Recommended ports: 80, 443, 853")); + o.depends("transparent_default_port_policy", "bypassed"); + o.datatype = "portrange"; + + o = s.taboption('transparent_proxy_rules', form.DynamicList, "wan_fw_udp_ports", _("Forwarded UDP Ports"), _("Requests to these UDP Ports will be forwarded through Xray. Recommended ports: 53, 443")); + o.depends("transparent_default_port_policy", "bypassed"); + o.datatype = "portrange"; + + o = s.taboption('transparent_proxy_rules', form.DynamicList, "wan_bp_tcp_ports", _("Bypassed TCP Ports"), _("Requests to these TCP Ports won't be forwarded through Xray.")); + o.depends("transparent_default_port_policy", "forwarded"); + o.datatype = "portrange"; + + o = s.taboption('transparent_proxy_rules', form.DynamicList, "wan_bp_udp_ports", _("Bypassed UDP Ports"), _("Requests to these UDP Ports won't be forwarded through Xray.")); + o.depends("transparent_default_port_policy", "forwarded"); + o.datatype = "portrange"; + + o = s.taboption('transparent_proxy_rules', form.Value, 'mark', _('Socket Mark Number'), _('Avoid proxy loopback problems with local (gateway) traffic')); + o.datatype = 'range(1, 255)'; + o.placeholder = 255; + + o = s.taboption('transparent_proxy_rules', form.SectionValue, "access_control_manual_tproxy", form.GridSection, 'manual_tproxy', _('Manual Transparent Proxy'), _('Compared to iptables REDIRECT, Xray could do NAT46 / NAT64 (for example accessing IPv6 only sites). See FakeDNS for details.')); + + ss = o.subsection; + ss.sortable = false; + ss.anonymous = true; + ss.addremove = true; + + o = ss.option(form.Value, "source_addr", _("Source Address")); + o.datatype = "ipaddr"; + + o = ss.option(form.Value, "source_port", _("Source Port")); + + o = ss.option(form.Value, "dest_addr", _("Destination Address")); + o.datatype = "host"; + + o = ss.option(form.Value, "dest_port", _("Destination Port")); + o.datatype = "port"; + + o = ss.option(form.DynamicList, "domain_names", _("Domain names to associate")); + + o = ss.option(form.Flag, 'rebind_domain_ok', _('Exempt rebind protection'), _('Avoid dnsmasq filtering RFC1918 IP addresses (and some TESTNET addresses as well) from result.
Must be enabled for TESTNET addresses (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24). Addresses like AS112 Project (192.31.196.0/24, 192.175.48.0/24) or NYIIX RTBH (198.32.160.7) can avoid that.')); + o.modalonly = true; + + o = ss.option(form.Flag, 'force_forward_tcp', _('Force Forward (TCP)'), _('This destination must be forwarded through an outbound server.')); + o.modalonly = true; + + o = ss.option(form.ListValue, 'force_forward_server_tcp', _('Force Forward server (TCP)')); + o.depends("force_forward_tcp", "1"); + o.datatype = "uciname"; + for (const v of uci.sections(config_data, "servers")) { + o.value(v[".name"], v.alias || v.server + ":" + v.server_port); + } + o.modalonly = true; + + o = ss.option(form.Flag, 'force_forward_udp', _('Force Forward (UDP)'), _('This destination must be forwarded through an outbound server.')); + o.modalonly = true; + + o = ss.option(form.ListValue, 'force_forward_server_udp', _('Force Forward server (UDP)')); + o.depends("force_forward_udp", "1"); + o.datatype = "uciname"; + for (const v of uci.sections(config_data, "servers")) { + o.value(v[".name"], v.alias || v.server + ":" + v.server_port); + } + o.modalonly = true; + + s.tab('xray_server', _('HTTPS Server')); + + o = s.taboption('xray_server', form.Flag, 'web_server_enable', _('Enable Xray HTTPS Server'), _("This will start a HTTPS server which serves both as an inbound for Xray and a reverse proxy web server.")); + + o = s.taboption('xray_server', form.Value, 'web_server_port', _('Xray HTTPS Server Port'), _("This port needs to be set accept input manually in firewall settings.")); + o.datatype = 'port'; + o.placeholder = 443; + o.depends("web_server_enable", "1"); + + o = s.taboption('xray_server', form.ListValue, "web_server_protocol", _("Protocol"), _("Only protocols which support fallback are available. Note that REALITY does not support fallback right now.")); + o.value("vless", "VLESS"); + o.value("trojan", "Trojan"); + o.rmempty = false; + o.depends("web_server_enable", "1"); + + add_flow_and_stream_security_conf(s, "xray_server", "web_server_protocol", "vless", true, false); + + add_flow_and_stream_security_conf(s, "xray_server", "web_server_protocol", "trojan", false, false); + + o = s.taboption('xray_server', form.DynamicList, 'web_server_password', _('UserId / Password'), _('Fill user_id for vmess / VLESS, or password for shadowsocks / trojan (also supports Xray UUID Mapping)')); + o.depends("web_server_enable", "1"); + + o = s.taboption('xray_server', form.Value, 'web_server_address', _('Default Fallback HTTP Server'), _("Only HTTP/1.1 supported here. For HTTP/2 upstream, use Fallback Servers below")); + o.datatype = 'hostport'; + o.depends("web_server_enable", "1"); + + o = s.taboption('xray_server', form.SectionValue, "xray_server_fallback", form.GridSection, 'fallback', _('Fallback Servers'), _("Specify upstream servers here.")); + o.depends({ "web_server_enable": "1", "web_server_protocol": "trojan" }); + o.depends({ "web_server_enable": "1", "web_server_protocol": "vless", "vless_tls": "tls" }); + o.depends({ "web_server_enable": "1", "web_server_protocol": "vless", "vless_tls": "xtls" }); + + ss = o.subsection; + ss.sortable = false; + ss.anonymous = true; + ss.addremove = true; + + o = ss.option(form.Value, "name", _("SNI")); + + o = ss.option(form.Value, "alpn", _("ALPN")); + + o = ss.option(form.Value, "path", _("Path")); + + o = ss.option(form.Value, "xver", _("Xver")); + o.datatype = "uinteger"; + + o = ss.option(form.Value, "dest", _("Destination Address")); + o.datatype = 'hostport'; + + s.tab('extra_options', _('Extra Options')); + + o = s.taboption('extra_options', form.ListValue, 'loglevel', _('Log Level'), _('Read Xray log in "System Log" or use logread command.')); + o.value("debug"); + o.value("info"); + o.value("warning"); + o.value("error"); + o.value("none"); + o.default = "warning"; + + o = s.taboption('extra_options', form.Flag, 'access_log', _('Enable Access Log'), _('Access log will also be written to System Log.')); + + o = s.taboption('extra_options', form.Flag, 'dns_log', _('Enable DNS Log'), _('DNS log will also be written to System Log.')); + + o = s.taboption('extra_options', form.Flag, 'xray_api', _('Enable Xray API Service'), _('Xray API Service uses port 8080 and GRPC protocol. Also callable via xray api or ubus call xray. See here for help.')); + + o = s.taboption('extra_options', form.Flag, 'stats', _('Enable Statistics'), _('Enable statistics of inbounds / outbounds data. Use Xray API to query values.')); + + o = s.taboption('extra_options', form.Flag, 'observatory', _('Enable Observatory'), _('Enable latency measurement for TCP and UDP outbounds. Support for balancers and strategy will be added later.')); + + o = s.taboption('extra_options', form.Flag, 'fw4_counter', _('Enable firewall4 counters'), _('Add counters to firewall4 for transparent proxy rules. (Not supported in all OpenWrt versions. )')); + + o = s.taboption('extra_options', form.Flag, 'metrics_server_enable', _('Enable Xray Metrics Server'), _("Enable built-in metrics server for pprof and expvar. See here for details.")); + + o = s.taboption('extra_options', form.Value, 'metrics_server_port', _('Xray Metrics Server Port'), _("Metrics may be sensitive so think twice before setting it as Default Fallback HTTP Server.")); + o.depends("metrics_server_enable", "1"); + o.datatype = 'port'; + o.placeholder = '18888'; + + o = s.taboption('extra_options', form.Value, 'handshake', _('Handshake Timeout'), _('Policy: Handshake timeout when connecting to upstream. See here for help.')); + o.datatype = 'uinteger'; + o.placeholder = 4; + + o = s.taboption('extra_options', form.Value, 'conn_idle', _('Connection Idle Timeout'), _('Policy: Close connection if no data is transferred within given timeout. See here for help.')); + o.datatype = 'uinteger'; + o.placeholder = 300; + + o = s.taboption('extra_options', form.Value, 'uplink_only', _('Uplink Only Timeout'), _('Policy: How long to wait before closing connection after server closed connection. See here for help.')); + o.datatype = 'uinteger'; + o.placeholder = 2; + + o = s.taboption('extra_options', form.Value, 'downlink_only', _('Downlink Only Timeout'), _('Policy: How long to wait before closing connection after client closed connection. See here for help.')); + o.datatype = 'uinteger'; + o.placeholder = 5; + + o = s.taboption('extra_options', form.Value, 'buffer_size', _('Buffer Size'), _('Policy: Internal cache size per connection. See here for help.')); + o.datatype = 'uinteger'; + o.placeholder = 512; + + o = s.taboption('extra_options', form.SectionValue, "xray_bridge", form.TableSection, 'bridge', _('Bridge'), _('Reverse proxy tool. Currently only client role (bridge) is supported. See here for help.')); + + ss = o.subsection; + ss.sortable = false; + ss.anonymous = true; + ss.addremove = true; + + o = ss.option(form.ListValue, "upstream", _("Upstream")); + o.datatype = "uciname"; + for (const v of uci.sections(config_data, "servers")) { + o.value(v[".name"], v.alias || v.server + ":" + v.server_port); + } + + o = ss.option(form.Value, "domain", _("Domain")); + o.rmempty = false; + + o = ss.option(form.Value, "redirect", _("Redirect address")); + o.datatype = "hostport"; + o.rmempty = false; + + s.tab('custom_options', _('Custom Options')); + o = s.taboption('custom_options', form.TextValue, 'custom_config', _('Custom Configurations'), _('Check /var/etc/xray/config.json for tags of generated inbounds and outbounds. See here for help')); + o.monospace = true; + o.rows = 20; + o.validate = validate_object; + + return m.render(); + } +}); diff --git a/core/root/www/luci-static/resources/view/xray/preview.js b/core/root/www/luci-static/resources/view/xray/preview.js new file mode 100644 index 00000000..f50cebc9 --- /dev/null +++ b/core/root/www/luci-static/resources/view/xray/preview.js @@ -0,0 +1,270 @@ +'use strict'; +'require form'; +'require network'; +'require tools.widgets as widgets'; +'require uci'; +'require view'; + +const variant = "xray_core"; + +function destination_format(k) { + return function (s) { + const dest = uci.get(variant, s, k) || []; + if (dest.length == 0) { + return "direct"; + } + return dest.map(v => uci.get(variant, v, "alias")).join(", "); + }; +} + +function extra_outbound_format(config_data, s, with_desc) { + const inbound_addr = uci.get(config_data, s, "inbound_addr") || ""; + const inbound_port = uci.get(config_data, s, "inbound_port") || ""; + if (inbound_addr == "" && inbound_port == "") { + return "-"; + } + if (with_desc) { + return `${inbound_addr}:${inbound_port} (${destination_format("destination")(s)})`; + } + return `${inbound_addr}:${inbound_port}`; +} + +function access_control_format(config_data, s, t) { + return function (v) { + switch (uci.get(config_data, v, s)) { + case "tproxy": { + return _("Enable tproxy"); + } + case "bypass": { + return _("Disable tproxy"); + } + } + return extra_outbound_format(config_data, uci.get(config_data, v, t)); + }; +} + +return view.extend({ + load: function () { + return Promise.all([ + uci.load(variant), + network.getHostHints() + ]); + }, + + render: function (load_result) { + const m = new form.Map(variant, _('Xray (preview)'), _("WARNING: These features are experimental, may cause a lot of problems and are not guaranteed to be compatible across minor versions.")); + const config_data = load_result[0]; + const hosts = load_result[1].hosts; + + let s = m.section(form.TypedSection, 'general'); + s.addremove = false; + s.anonymous = true; + + s.tab('fake_dns', _('FakeDNS')); + + let tproxy_port_tcp_f4 = s.taboption('fake_dns', form.Value, 'tproxy_port_tcp_f4', _('Transparent proxy port (TCP4)')); + tproxy_port_tcp_f4.datatype = 'port'; + tproxy_port_tcp_f4.placeholder = 1086; + + let tproxy_port_tcp_f6 = s.taboption('fake_dns', form.Value, 'tproxy_port_tcp_f6', _('Transparent proxy port (TCP6)')); + tproxy_port_tcp_f6.datatype = 'port'; + tproxy_port_tcp_f6.placeholder = 1087; + + let tproxy_port_udp_f4 = s.taboption('fake_dns', form.Value, 'tproxy_port_udp_f4', _('Transparent proxy port (UDP4)')); + tproxy_port_udp_f4.datatype = 'port'; + tproxy_port_udp_f4.placeholder = 1088; + + let tproxy_port_udp_f6 = s.taboption('fake_dns', form.Value, 'tproxy_port_udp_f6', _('Transparent proxy port (UDP6)')); + tproxy_port_udp_f6.datatype = 'port'; + tproxy_port_udp_f6.placeholder = 1089; + + let pool_v4 = s.taboption('fake_dns', form.Value, 'pool_v4', _('Address Pool (IPv4)')); + pool_v4.datatype = 'ip4addr'; + pool_v4.placeholder = "198.18.0.0/15"; + + let pool_v4_size = s.taboption('fake_dns', form.Value, 'pool_v4_size', _('Address Pool Size (IPv4)')); + pool_v4_size.datatype = 'integer'; + pool_v4_size.placeholder = 65535; + + let pool_v6 = s.taboption('fake_dns', form.Value, 'pool_v6', _('Address Pool (IPv6)')); + pool_v6.datatype = 'ip6addr'; + pool_v6.placeholder = "fc00::/18"; + + let pool_v6_size = s.taboption('fake_dns', form.Value, 'pool_v6_size', _('Address Pool Size (IPv6)')); + pool_v6_size.datatype = 'integer'; + pool_v6_size.placeholder = 65535; + + let fake_dns_timeout = s.taboption('fake_dns', form.Value, 'fake_dns_timeout', _('Connection Idle Timeout'), _('Policy: Close connection if no data is transferred within given timeout. See here for help.')); + fake_dns_timeout.datatype = 'uinteger'; + fake_dns_timeout.placeholder = 300; + + let fs = s.taboption('fake_dns', form.SectionValue, "fake_dns_section", form.GridSection, 'fakedns', _('FakeDNS Routing'), _('See FakeDNS for details.')).subsection; + fs.sortable = false; + fs.anonymous = true; + fs.addremove = true; + + let fake_dns_domain_names = fs.option(form.DynamicList, "fake_dns_domain_names", _("Domain names to associate")); + fake_dns_domain_names.rmempty = true; + + let fake_dns_forward_server_tcp = fs.option(form.MultiValue, 'fake_dns_forward_server_tcp', _('Force Forward server (TCP)')); + fake_dns_forward_server_tcp.datatype = "uciname"; + fake_dns_forward_server_tcp.textvalue = destination_format("fake_dns_forward_server_tcp"); + + let fake_dns_forward_server_udp = fs.option(form.MultiValue, 'fake_dns_forward_server_udp', _('Force Forward server (UDP)')); + fake_dns_forward_server_udp.datatype = "uciname"; + fake_dns_forward_server_udp.textvalue = destination_format("fake_dns_forward_server_udp"); + + s.tab("extra_inbounds", "Extra Inbounds"); + + let extra_inbounds = s.taboption('extra_inbounds', form.SectionValue, "extra_inbound_section", form.GridSection, 'extra_inbound', _('Extra Inbounds'), _("Add more socks5 / http inbounds and redirect to other outbounds.")).subsection; + extra_inbounds.sortable = false; + extra_inbounds.anonymous = true; + extra_inbounds.addremove = true; + extra_inbounds.nodescriptions = true; + + let inbound_addr = extra_inbounds.option(form.Value, "inbound_addr", _("Listen Address")); + inbound_addr.datatype = "ip4addr"; + + let inbound_port = extra_inbounds.option(form.Value, "inbound_port", _("Listen Port")); + inbound_port.datatype = "port"; + + let inbound_type = extra_inbounds.option(form.ListValue, "inbound_type", _("Inbound Type")); + inbound_type.value("socks5", _("Socks5 Proxy")); + inbound_type.value("http", _("HTTP Proxy")); + inbound_type.value("tproxy_tcp", _("Transparent Proxy (TCP)")); + inbound_type.value("tproxy_udp", _("Transparent Proxy (UDP)")); + inbound_type.rmempty = false; + + let specify_outbound = extra_inbounds.option(form.Flag, 'specify_outbound', _('Specify Outbound'), _('If not selected, this inbound will use global settings (including sniffing settings). ')); + specify_outbound.modalonly = true; + + let destination = extra_inbounds.option(form.MultiValue, 'destination', _('Destination'), _("Select multiple outbounds for load balancing. If none selected, requests will be sent via direct outbound.")); + destination.depends("specify_outbound", "1"); + destination.datatype = "uciname"; + destination.textvalue = destination_format("destination"); + + const servers = uci.sections(config_data, "servers"); + if (servers.length == 0) { + destination.value("direct", _("No server configured")); + fake_dns_forward_server_tcp.value("direct", _("No server configured")); + fake_dns_forward_server_udp.value("direct", _("No server configured")); + + destination.readonly = true; + fake_dns_forward_server_tcp.readonly = true; + fake_dns_forward_server_udp.readonly = true; + } else { + for (const v of uci.sections(config_data, "servers")) { + destination.value(v[".name"], v.alias || v.server + ":" + v.server_port); + fake_dns_forward_server_tcp.value(v[".name"], v.alias || v.server + ":" + v.server_port); + fake_dns_forward_server_udp.value(v[".name"], v.alias || v.server + ":" + v.server_port); + } + } + + s.tab("lan_hosts_access_control", _("LAN Hosts Access Control")); + + let tproxy_ifaces_v4 = s.taboption('lan_hosts_access_control', widgets.DeviceSelect, 'tproxy_ifaces_v4', _("Devices to enable IPv4 tproxy"), _("Enable IPv4 transparent proxy on these interfaces / network devices.")); + tproxy_ifaces_v4.noaliases = true; + tproxy_ifaces_v4.nocreate = true; + tproxy_ifaces_v4.multiple = true; + + let tproxy_ifaces_v6 = s.taboption('lan_hosts_access_control', widgets.DeviceSelect, 'tproxy_ifaces_v6', _("Devices to enable IPv6 tproxy"), _("Enable IPv6 transparent proxy on these interfaces / network devices.")); + tproxy_ifaces_v6.noaliases = true; + tproxy_ifaces_v6.nocreate = true; + tproxy_ifaces_v6.multiple = true; + + let bypass_ifaces_v4 = s.taboption('lan_hosts_access_control', widgets.DeviceSelect, 'bypass_ifaces_v4', _("Devices to disable IPv4 tproxy"), _("This overrides per-device settings below. FakeDNS and manual transparent proxy won't be affected by this option.")); + bypass_ifaces_v4.noaliases = true; + bypass_ifaces_v4.nocreate = true; + bypass_ifaces_v4.multiple = true; + + let bypass_ifaces_v6 = s.taboption('lan_hosts_access_control', widgets.DeviceSelect, 'bypass_ifaces_v6', _("Devices to disable IPv6 tproxy"), _("This overrides per-device settings below. FakeDNS and manual transparent proxy won't be affected by this option.")); + bypass_ifaces_v6.noaliases = true; + bypass_ifaces_v6.nocreate = true; + bypass_ifaces_v6.multiple = true; + + let lan_hosts = s.taboption('lan_hosts_access_control', form.SectionValue, "lan_hosts_section", form.GridSection, 'lan_hosts', _('LAN Hosts Access Control'), _("Per-device settings here override per-interface enabling settings above. FakeDNS and manual transparent proxy won't be affected by these options.")).subsection; + lan_hosts.sortable = false; + lan_hosts.anonymous = true; + lan_hosts.addremove = true; + + let macaddr = lan_hosts.option(form.Value, "macaddr", _("MAC Address")); + macaddr.datatype = "macaddr"; + macaddr.rmempty = false; + L.sortedKeys(hosts).forEach(function (mac) { + macaddr.value(mac, E([], [mac, ' (', E('strong', [hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0] || L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6)[0] || '?']), ')'])); + }); + + let access_control_strategy_v4 = lan_hosts.option(form.ListValue, "access_control_strategy_v4", _("Access Control Strategy (IPv4)")); + access_control_strategy_v4.value("tproxy", _("Enable transparent proxy")); + access_control_strategy_v4.value("forward", _("Forward via extra inbound")); + access_control_strategy_v4.value("bypass", _("Disable transparent proxy")); + access_control_strategy_v4.modalonly = true; + access_control_strategy_v4.rmempty = false; + + let access_control_forward_tcp_v4 = lan_hosts.option(form.ListValue, "access_control_forward_tcp_v4", _("Extra inbound (TCP4)")); + access_control_forward_tcp_v4.depends("access_control_strategy_v4", "forward"); + access_control_forward_tcp_v4.textvalue = access_control_format(config_data, "access_control_strategy_v4", "access_control_forward_tcp_v4"); + + let access_control_forward_udp_v4 = lan_hosts.option(form.ListValue, "access_control_forward_udp_v4", _("Extra inbound (UDP4)")); + access_control_forward_udp_v4.depends("access_control_strategy_v4", "forward"); + access_control_forward_udp_v4.textvalue = access_control_format(config_data, "access_control_strategy_v4", "access_control_forward_udp_v4"); + + let access_control_strategy_v6 = lan_hosts.option(form.ListValue, "access_control_strategy_v6", _("Access Control Strategy (IPv6)")); + access_control_strategy_v6.value("tproxy", _("Enable transparent proxy")); + access_control_strategy_v6.value("forward", _("Forward via extra inbound")); + access_control_strategy_v6.value("bypass", _("Disable transparent proxy")); + access_control_strategy_v6.modalonly = true; + access_control_strategy_v6.rmempty = false; + + let access_control_forward_tcp_v6 = lan_hosts.option(form.ListValue, "access_control_forward_tcp_v6", _("Extra inbound (TCP6)")); + access_control_forward_tcp_v6.depends("access_control_strategy_v6", "forward"); + access_control_forward_tcp_v6.textvalue = access_control_format(config_data, "access_control_strategy_v6", "access_control_forward_tcp_v6"); + + let access_control_forward_udp_v6 = lan_hosts.option(form.ListValue, "access_control_forward_udp_v6", _("Extra inbound (UDP6)")); + access_control_forward_udp_v6.depends("access_control_strategy_v6", "forward"); + access_control_forward_udp_v6.textvalue = access_control_format(config_data, "access_control_strategy_v6", "access_control_forward_udp_v6"); + + for (const v of uci.sections(config_data, "extra_inbound")) { + switch (v["inbound_type"]) { + case "tproxy_tcp": { + access_control_forward_tcp_v4.value(v[".name"], `${extra_outbound_format(config_data, v[".name"], true)}`); + access_control_forward_tcp_v6.value(v[".name"], `${extra_outbound_format(config_data, v[".name"], true)}`); + break; + } + case "tproxy_udp": { + access_control_forward_udp_v4.value(v[".name"], `${extra_outbound_format(config_data, v[".name"], true)}`); + access_control_forward_udp_v6.value(v[".name"], `${extra_outbound_format(config_data, v[".name"], true)}`); + break; + } + } + } + + s.tab('dynamic_direct', _('Dynamic Direct')); + + s.taboption('dynamic_direct', form.Flag, 'dynamic_direct_tcp4', _('Enable for IPv4 TCP'), _("This should improve performance with large number of connections.")); + s.taboption('dynamic_direct', form.Flag, 'dynamic_direct_tcp6', _('Enable for IPv4 UDP'), _("This may cause problems but worth a try.")); + 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.")); + 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.")); + + 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.")); + dynamic_direct_timeout.datatype = 'uinteger'; + dynamic_direct_timeout.placeholder = 300; + + let ttl_override = s.taboption('dynamic_direct', form.Value, 'ttl_override', _('Override IPv4 TTL'), _("Strongly not recommended. Only used for some network environments with specific restrictions.")); + ttl_override.datatype = 'uinteger'; + + let hop_limit_override = s.taboption('dynamic_direct', form.Value, 'hop_limit_override', _('Override IPv6 Hop Limit'), _("Strongly not recommended. Only used for some network environments with specific restrictions.")); + hop_limit_override.datatype = 'uinteger'; + + let ttl_hop_limit_match = s.taboption('dynamic_direct', form.Value, 'ttl_hop_limit_match', _('TTL / Hop Limit Match'), _("Only override TTL / hop limit for packets with specific TTL / hop limit.")); + ttl_hop_limit_match.datatype = 'uinteger'; + + s.tab('custom_options', _('Custom Options')); + let custom_configuration_hook = s.taboption('custom_options', form.TextValue, 'custom_configuration_hook', _('Custom Configuration Hook'), _('Read ucode Documentation for the language used. Code filled here may need to change after upgrading luci-app-xray.')); + custom_configuration_hook.placeholder = "return function(config) {\n return config;\n};"; + custom_configuration_hook.monospace = true; + custom_configuration_hook.rows = 20; + + return m.render(); + } +}); diff --git a/status/Makefile b/status/Makefile new file mode 100644 index 00000000..a1e130fd --- /dev/null +++ b/status/Makefile @@ -0,0 +1,38 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-xray-status +PKG_VERSION:=3.1.1 +PKG_RELEASE:=1 + +PKG_LICENSE:=MPLv2 +PKG_LICENSE_FILES:=LICENSE +PKG_MAINTAINER:=yichya +PKG_BUILD_PARALLEL:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/$(PKG_NAME) + SECTION:=Custom + CATEGORY:=Extra packages + TITLE:=LuCI Support for Xray (status page) + DEPENDS:=luci-app-xray +wget + PKGARCH:=all +endef + +define Package/$(PKG_NAME)/description + LuCI Support for Xray (Client-side Rendered) (status page). +endef + +define Build/Compile +endef + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/xray + $(INSTALL_DATA) ./root/www/luci-static/resources/view/xray/status.js $(1)/www/luci-static/resources/view/xray/status.js + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-xray-status.json $(1)/usr/share/luci/menu.d/luci-app-xray-status.json + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-xray-status.json $(1)/usr/share/rpcd/acl.d/luci-app-xray-status.json +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/status/root/usr/share/luci/menu.d/luci-app-xray-status.json b/status/root/usr/share/luci/menu.d/luci-app-xray-status.json new file mode 100644 index 00000000..4c9a16f1 --- /dev/null +++ b/status/root/usr/share/luci/menu.d/luci-app-xray-status.json @@ -0,0 +1,17 @@ +{ + "admin/status/xray_status": { + "title": "Xray", + "action": { + "type": "view", + "path": "xray/status" + }, + "depends": { + "acl": [ + "luci-app-xray-status" + ], + "uci": { + "xray_core": true + } + } + } +} \ No newline at end of file diff --git a/status/root/usr/share/rpcd/acl.d/luci-app-xray-status.json b/status/root/usr/share/rpcd/acl.d/luci-app-xray-status.json new file mode 100644 index 00000000..5044b0da --- /dev/null +++ b/status/root/usr/share/rpcd/acl.d/luci-app-xray-status.json @@ -0,0 +1,15 @@ +{ + "luci-app-xray-status": { + "description": "Grant access to xray configurations", + "read": { + "uci": [ + "xray_core" + ], + "file": { + "/usr/bin/wget": [ + "exec" + ] + } + } + } +} \ No newline at end of file diff --git a/status/root/www/luci-static/resources/view/xray/status.js b/status/root/www/luci-static/resources/view/xray/status.js new file mode 100644 index 00000000..b80626f6 --- /dev/null +++ b/status/root/www/luci-static/resources/view/xray/status.js @@ -0,0 +1,392 @@ +'use strict'; +'require dom'; +'require fs'; +'require poll'; +'require uci'; +'require ui'; +'require view'; + +const variant = "xray_core"; + +function greater_than_zero(n) { + if (n < 0) { + return 0; + } + return n; +} + +function get_inbound_uci_description(config, key) { + const ks = key.split(":"); + switch (ks[0]) { + case "https_inbound": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: https://0.0.0.0:443 }`)]); + } + case "http_inbound": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: http://0.0.0.0:${uci.get_first(config, "general", "http_port") || 1081} }`)]); + } + case "socks_inbound": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: socks5://0.0.0.0:${uci.get_first(config, "general", "socks_port") || 1080} }`)]); + } + case "tproxy_tcp_inbound_v4": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: tproxy_tcp://0.0.0.0:${uci.get_first(config, "general", "tproxy_port_tcp_v4") || 1082} }`)]); + } + case "tproxy_udp_inbound_v4": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: tproxy_udp://0.0.0.0:${uci.get_first(config, "general", "tproxy_port_udp_v4") || 1084} }`)]); + } + case "tproxy_tcp_inbound_v6": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: tproxy_tcp://[::]:${uci.get_first(config, "general", "tproxy_port_tcp_v6") || 1083} }`)]); + } + case "tproxy_udp_inbound_v6": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: tproxy_udp://[::]:${uci.get_first(config, "general", "tproxy_port_udp_v6") || 1085} }`)]); + } + case "tproxy_tcp_inbound_f4": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: tproxy_tcp://0.0.0.0:${uci.get_first(config, "general", "tproxy_port_tcp_f4") || 1086} }`)]); + } + case "tproxy_udp_inbound_f4": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: tproxy_udp://0.0.0.0:${uci.get_first(config, "general", "tproxy_port_udp_f4") || 1088} }`)]); + } + case "tproxy_tcp_inbound_f6": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: tproxy_tcp://[::]:${uci.get_first(config, "general", "tproxy_port_tcp_f6") || 1087} }`)]); + } + case "tproxy_udp_inbound_f6": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: tproxy_udp://[::]:${uci.get_first(config, "general", "tproxy_port_udp_f6") || 1089} }`)]); + } + case "metrics": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: http://0.0.0.0:${uci.get_first(config, "general", "metrics_server_port") || 18888} }`)]); + } + case "api": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: grpc://127.0.0.1:8080 }`)]); + } + case "dns_server_inbound": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: dns://0.0.0.0:${ks[1]} }`)]); + } + } + const uci_key = key.slice(-9); + const uci_item = uci.get(config, uci_key); + if (uci_item == null) { + return key; + } + switch (uci_item[".type"]) { + case "extra_inbound": { + return E([], [key, " ", E('span', { 'class': 'ifacebadge' }, `{ listen: ${uci_item["inbound_type"]}://${uci_item["inbound_addr"]}:${uci_item["inbound_port"]} }`)]); + } + } + return key; +} + +function outbound_format(server) { + if (server["alias"]) { + return server["alias"]; + } + if (server["server"].includes(":")) { + return `${server["transport"]},[${server["server"]}]:${server["server_port"]}`; + } + return `${server["transport"]},${server["server"]}:${server["server_port"]}`; +} + +function get_outbound_uci_description(config, key) { + if (!key) { + return "direct"; + } + const uci_key = key.slice(-9); + const uci_item = uci.get(config, uci_key); + if (uci_item == null) { + return "direct"; + } + switch (uci_item[".type"]) { + case "servers": { + return outbound_format(uci_item); + } + case "extra_inbound": { + return `${uci_item["inbound_type"]}://${uci_item["inbound_addr"]}:${uci_item["inbound_port"]}`; + } + case "manual_tproxy": { + return `${uci_item["source_addr"]}:${uci_item["source_port"]} -> ${uci_item["dest_addr"] || "{sniffing}"}:${uci_item["dest_port"]}`; + } + case "fakedns": { + return `${uci_item["fake_dns_domain_names"].length} ${_("domains")}\n${uci_item["fake_dns_domain_names"].join("\n")}`; + } + } + return "direct"; +} + +function outbound_first_tag_format(tag_split, first_uci_description) { + let result = [tag_split[0]]; + + const first_tag = tag_split[0].split(":"); + if (first_tag.length == 1) { + return result; + } + + if (tag_split.length > 1) { + switch (first_tag[0]) { + case "extra_inbound": { + if (tag_split.length < 3) { + result.push(" ", E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${first_uci_description}`, + }, `{ listen: ${first_uci_description} }`)); + } else { + result.push(" ", E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${first_uci_description}`, + }, `{ listen ... }`)); + } + break; + } + case "force_forward": { + result.push(" ", E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${first_uci_description}`, + }, `{ force_forward ... }`)); + break; + } + case "balancer_outbound": { + if (tag_split.length < 4) { + result.push(" ", E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${first_uci_description}`, + }, `{ balancer_outbound ... }`)); + } + break; + } + case "fake_dns_tcp": + case "fake_dns_udp": { + result.push(" ", E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${first_uci_description}`, + }, `{ fake_dns ... }`)); + } + case "manual_tproxy": { + break; + } + default: { + result.push(" ", E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${first_uci_description}`, + }, `{ ... }`)); + break; + } + } + } else { + result.push(" ", E('span', { + 'class': 'ifacebadge', + 'data-tooltip': first_tag[0], + }, `{ ${first_uci_description} }`)); + } + return result; +} + +function outbound_middle_tag_format(tag_split, first_uci_description, current_tag, current_uci_description) { + switch (current_tag[0]) { + case "extra_inbound": { + if (tag_split.length < 3) { + return E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${current_tag[0]}: ${current_uci_description} (${current_tag[1]})`, + }, `{ listen: ${current_uci_description} }`); + } + return E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${current_tag[0]}: ${current_uci_description} (${current_tag[1]})`, + }, `{ listen ... }`); + } + case "force_forward": { + return E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${current_tag[0]}: ${current_uci_description} (${current_tag[1]})`, + }, `{ force_forward ... }`); + } + case "balancer_outbound": { + if (tag_split.length < 4) { + return E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${current_tag[0]}: ${current_uci_description} (${current_tag[1]})`, + }, `{ balancer_outbound ... }`); + } + } + case "tcp_outbound": { + if (tag_split.length < 4) { + return E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${current_tag[0]}`, + }, `{ tcp: ${first_uci_description} }`); + } + return E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `tcp: ${first_uci_description}`, + }, `{ tcp ... }`); + } + case "udp_outbound": { + if (tag_split.length < 4) { + return E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${current_tag[0]}`, + }, `{ udp: ${first_uci_description} }`); + } + return E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `udp: ${first_uci_description}`, + }, `{ udp ... }`); + } + case "fake_dns_tcp": + case "fake_dns_udp": { + break; + } + } + return E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${current_uci_description}`, + }, `{ ... }`); +} + +function outbound_last_tag_format(first_uci_description, last_tag, last_uci_description) { + if (last_tag[0] == "tcp_outbound") { + return E('span', { + 'class': 'ifacebadge', + }, `{ tcp: ${first_uci_description} }`); + } else if (last_tag[0] == "udp_outbound") { + return E('span', { + 'class': 'ifacebadge', + }, `{ udp: ${first_uci_description} }`); + } + return E('span', { + 'class': 'ifacebadge', + 'data-tooltip': `${last_tag[1]}`, + }, `{ ${last_tag[0]}: ${last_uci_description} }`); +} + +function get_outbound_description(config, tag) { + const tag_split = tag.split("@"); + const first_uci_description = get_outbound_uci_description(config, tag_split[0].split(":")[1]); + + let result = outbound_first_tag_format(tag_split, first_uci_description); + for (let i = 1; i < tag_split.length; i++) { + const current_tag = tag_split[i].split(":"); + const current_uci_description = get_outbound_uci_description(config, current_tag[1]); + if (i == tag_split.length - 1) { + result.push(" ", outbound_last_tag_format(first_uci_description, current_tag, current_uci_description)); + } else { + result.push(" ", outbound_middle_tag_format(tag_split, first_uci_description, current_tag, current_uci_description)); + } + } + return result; +} + +function observatory(vars, config) { + if (!vars["observatory"]) { + return []; + } + const now_timestamp = new Date().getTime() / 1000; + return [ + E('h3', _('Outbound Observatory')), + E('div', { 'class': 'cbi-map-descr' }, _("Availability of outbound servers are probed every few seconds.")), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Tag')), + E('th', { 'class': 'th' }, _('Latency')), + E('th', { 'class': 'th' }, _('Last seen')), + E('th', { 'class': 'th' }, _('Last check')), + ]), ...Object.entries(vars["observatory"]).map((v, index, arr) => E('tr', { 'class': `tr cbi-rowstyle-${index % 2 + 1}` }, [ + E('td', { 'class': 'td' }, get_outbound_description(config, v[0])), + E('td', { 'class': 'td' }, function (c) { + if (c[1]["alive"]) { + return c[1]["delay"] + ' ' + _("ms"); + } + return _("unreachable"); + }(v)), + E('td', { 'class': 'td' }, '%d'.format(greater_than_zero(now_timestamp - v[1]["last_seen_time"])) + _('s ago')), + E('td', { 'class': 'td' }, '%d'.format(greater_than_zero(now_timestamp - v[1]["last_try_time"])) + _('s ago')), + ])) + ]) + ]; +}; + +function outbound_stats(vars, config) { + if (!vars["stats"]) { + return []; + } + if (!vars["stats"]["outbound"]) { + return []; + } + return [ + E('h3', _('Outbound Statistics')), + E('div', { 'class': 'cbi-map-descr' }, _("Data transferred for outbounds since Xray start.")), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Tag')), + E('th', { 'class': 'th' }, _('Downlink')), + E('th', { 'class': 'th' }, _('Uplink')), + ]), ...Object.entries(vars["stats"]["outbound"]).map((v, index, arr) => E('tr', { 'class': `tr cbi-rowstyle-${index % 2 + 1}` }, [ + E('td', { 'class': 'td' }, get_outbound_description(config, v[0])), + E('td', { 'class': 'td' }, '%.2mB'.format(v[1]["downlink"])), + E('td', { 'class': 'td' }, '%.2mB'.format(v[1]["uplink"])), + ])) + ]) + ]; +}; + +function inbound_stats(vars, config) { + if (!vars["stats"]) { + return []; + } + if (!vars["stats"]["inbound"]) { + return []; + } + return [ + E('h3', _('Inbound Statistics')), + E('div', { 'class': 'cbi-map-descr' }, _("Data transferred for inbounds since Xray start.")), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Tag')), + E('th', { 'class': 'th' }, _('Downlink')), + E('th', { 'class': 'th' }, _('Uplink')), + ]), ...Object.entries(vars["stats"]["inbound"]).map((v, index, arr) => E('tr', { 'class': `tr cbi-rowstyle-${index % 2 + 1}` }, [ + E('td', { 'class': 'td' }, get_inbound_uci_description(config, v[0])), + E('td', { 'class': 'td' }, '%.2mB'.format(v[1]["downlink"])), + E('td', { 'class': 'td' }, '%.2mB'.format(v[1]["uplink"])), + ])) + ]) + ]; +}; + +return view.extend({ + load: function () { + return uci.load(variant); + }, + + render: function (config) { + if (uci.get_first(config, "general", "metrics_server_enable") != "1") { + return E([], [ + E('h2', _('Xray (status)')), + E('p', { 'class': 'cbi-map-descr' }, _("Xray metrics server not enabled. Enable Xray metrics server to see details.")) + ]); + } + const info = E('p', { 'class': 'cbi-map-descr' }, _("Collecting data. If any error occurs, check if wget is installed correctly.")); + const detail = E('div', {}); + poll.add(function () { + 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) { + const result = E([], [ + E('div', {}, [ + E('div', { 'class': 'cbi-section', 'data-tab': 'observatory', 'data-tab-title': _('Observatory') }, observatory(vars, config)), + E('div', { 'class': 'cbi-section', 'data-tab': 'outbounds', 'data-tab-title': _('Outbounds') }, outbound_stats(vars, config)), + E('div', { 'class': 'cbi-section', 'data-tab': 'inbounds', 'data-tab-title': _('Inbounds') }, inbound_stats(vars, config)) + ]) + ]); + ui.tabs.initTabGroup(result.lastElementChild.childNodes); + dom.content(info, _("Show some statistics of Xray. If nothing here, enable statistics and / or observatory for Xray.")); + dom.content(detail, result); + }); + }); + + return E([], [ + E('h2', _('Xray (status)')), + info, + detail + ]); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +});