├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── feature_request.md │ └── question-.md ├── dependabot.yml ├── docker │ ├── Dockerfile │ └── files │ │ ├── DockerInit.sh │ │ ├── cc-name.sh │ │ └── file-name.sh └── workflows │ ├── docker.yml │ └── release.yml ├── .gitignore ├── Cloudflare优选IP.md ├── DockerEntrypoint.sh ├── LICENSE ├── Nginx前置SNI分流.md ├── README.md ├── config ├── config.go ├── name └── version ├── database ├── db.go └── model │ └── model.go ├── go.mod ├── go.sum ├── install.sh ├── logger └── logger.go ├── main.go ├── media ├── 2022-04-04_141259.png ├── APIKey1.PNG ├── APIKey2.png ├── DetailEnter.png ├── any_SNI_No_SNI.png ├── bda84fbc2ede834deaba1c173a932223.png ├── config.json ├── d13ffd6a73f938d1037d0708e31433bf.png ├── reality.png ├── sysctl.conf ├── trojan+grpc+reality.png ├── vless+H2+reality.png ├── vless+grpc+reality.png ├── vless+grpc+sni.png ├── vless+xtls+tcp+reality+sni.png ├── vless+xtls+tcp+reality.png └── xray.json ├── reality.md ├── util ├── common │ ├── err.go │ ├── format.go │ ├── multi_error.go │ ├── network.go │ └── stringUtil.go ├── context.go ├── json_util │ └── json.go ├── random │ └── random.go ├── reflect_util │ └── reflect.go └── sys │ ├── a.s │ ├── psutil.go │ ├── sys_darwin.go │ └── sys_linux.go ├── v2ui ├── db.go ├── models.go └── v2ui.go ├── web ├── assets │ ├── ant-design-vue@1.7.2 │ │ ├── antd-with-locales.min.js │ │ ├── antd.less │ │ ├── antd.min.css │ │ └── antd.min.js │ ├── axios │ │ └── axios.min.js │ ├── base64 │ │ └── base64.min.js │ ├── clipboard │ │ └── clipboard.min.js │ ├── css │ │ └── custom.css │ ├── element-ui@2.15.0 │ │ └── theme-chalk │ │ │ └── display.css │ ├── favicon.ico │ ├── js │ │ ├── axios-init.js │ │ ├── model │ │ │ ├── models.js │ │ │ └── xray.js │ │ └── util │ │ │ ├── common.js │ │ │ ├── date-util.js │ │ │ └── utils.js │ ├── moment │ │ └── moment.min.js │ ├── qrcode │ │ └── qrious.min.js │ ├── qs │ │ └── qs.min.js │ ├── uri │ │ └── URI.min.js │ └── vue@2.6.12 │ │ ├── vue.common.dev.js │ │ ├── vue.common.js │ │ ├── vue.common.prod.js │ │ ├── vue.esm.browser.min.js │ │ ├── vue.esm.js │ │ ├── vue.min.js │ │ ├── vue.runtime.common.dev.js │ │ ├── vue.runtime.common.js │ │ ├── vue.runtime.common.prod.js │ │ ├── vue.runtime.esm.js │ │ ├── vue.runtime.js │ │ └── vue.runtime.min.js ├── controller │ ├── base.go │ ├── inbound.go │ ├── index.go │ ├── server.go │ ├── setting.go │ ├── util.go │ └── xui.go ├── entity │ └── entity.go ├── global │ └── global.go ├── html │ ├── common │ │ ├── head.html │ │ ├── js.html │ │ ├── prompt_modal.html │ │ ├── qrcode_modal.html │ │ └── text_modal.html │ ├── login.html │ └── xui │ │ ├── common_sider.html │ │ ├── component │ │ ├── inbound_info.html │ │ ├── setting.html │ │ └── themeSwitch.html │ │ ├── form │ │ ├── inbound.html │ │ ├── protocol │ │ │ ├── dokodemo.html │ │ │ ├── http.html │ │ │ ├── shadowsocks.html │ │ │ ├── socks.html │ │ │ ├── trojan.html │ │ │ ├── vless.html │ │ │ └── vmess.html │ │ ├── reality_settings.html │ │ ├── sniffing.html │ │ ├── stream │ │ │ ├── stream_grpc.html │ │ │ ├── stream_httpupgrade.html │ │ │ ├── stream_kcp.html │ │ │ ├── stream_raw.html │ │ │ ├── stream_settings.html │ │ │ ├── stream_sockopt.html │ │ │ ├── stream_tcp.html │ │ │ ├── stream_ws.html │ │ │ └── stream_xhttp.html │ │ └── tls_settings.html │ │ ├── inbound_info_modal.html │ │ ├── inbound_modal.html │ │ ├── inbounds.html │ │ ├── index.html │ │ └── setting.html ├── job │ ├── check_inbound_job.go │ ├── check_xray_running_job.go │ ├── stats_notify_job.go │ └── xray_traffic_job.go ├── network │ ├── auto_https_listener.go │ └── autp_https_conn.go ├── service │ ├── config.json │ ├── inbound.go │ ├── panel.go │ ├── server.go │ ├── setting.go │ ├── telegram.go │ ├── user.go │ ├── version.go │ └── xray.go ├── session │ └── session.go ├── translation │ ├── translate.en_US.toml │ ├── translate.zh_Hans.toml │ └── translate.zh_Hant.toml └── web.go ├── wireguard.md ├── xray-ui.service ├── xray-ui.sh └── xray ├── config.go ├── inbound.go ├── process.go └── traffic.go /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Issue Report 2 | description: "Create a report to help us improve." 3 | body: 4 | - type: checkboxes 5 | id: terms 6 | attributes: 7 | label: Welcome 8 | options: 9 | - label: Yes, I'm using the latest major release. Only such installations are supported. 10 | required: true 11 | - label: Yes, I'm using the supported system. Only such systems are supported. 12 | required: true 13 | - label: Yes, I have read all WIKI document,nothing can help me in my problem. 14 | required: true 15 | - label: Yes, I've searched similar issues on GitHub and didn't find any. 16 | required: true 17 | - label: Yes, I've included all information below (version, config, log, etc). 18 | required: true 19 | 20 | - type: textarea 21 | id: problem 22 | attributes: 23 | label: Description of the problem,screencshot would be good 24 | placeholder: Your problem description 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: version 30 | attributes: 31 | label: Version of xray-ui 32 | value: |- 33 |
34 | 35 | ```console 36 | # Paste here 37 | ``` 38 | 39 |
40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | id: log 45 | attributes: 46 | label: xray-ui log reports or xray log 47 | value: |- 48 |
49 | 50 | ```console 51 | # paste log here 52 | ``` 53 | 54 |
55 | validations: 56 | required: true 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Question ' 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # ======================================================== 2 | # Stage: build 3 | # ======================================================== 4 | FROM golang:1.24.0-alpine AS build 5 | WORKDIR /app 6 | ARG TARGETARCH 7 | 8 | RUN apk --no-cache --update add \ 9 | build-base \ 10 | gcc \ 11 | wget \ 12 | unzip 13 | 14 | COPY . . 15 | COPY .github/docker/files/DockerInit.sh /app/DockerInit.sh 16 | ENV CGO_ENABLED=1 17 | ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" 18 | RUN go build -o build/xray-ui main.go 19 | RUN chmod +x ./DockerInit.sh && \ 20 | ./DockerInit.sh "$TARGETARCH" 21 | 22 | # ======================================================== 23 | # Stage: Final Image of xray-ui 24 | # ======================================================== 25 | FROM alpine 26 | ENV TZ=Asia/Shanghai 27 | WORKDIR /app 28 | 29 | RUN apk add --no-cache --update \ 30 | ca-certificates \ 31 | tzdata \ 32 | fail2ban \ 33 | bash 34 | 35 | COPY --from=build /app/build/ /app/ 36 | COPY --from=build /app/DockerEntrypoint.sh /app/ 37 | COPY --from=build /app/xray-ui.sh /usr/bin/xray-ui 38 | 39 | 40 | # Configure fail2ban 41 | RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \ 42 | && cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \ 43 | && sed -i "s/^\[ssh\]$/&\nenabled = false/" /etc/fail2ban/jail.local \ 44 | && sed -i "s/^\[sshd\]$/&\nenabled = false/" /etc/fail2ban/jail.local \ 45 | && sed -i "s/#allowipv6 = auto/allowipv6 = auto/g" /etc/fail2ban/fail2ban.conf 46 | 47 | RUN chmod +x \ 48 | /app/DockerEntrypoint.sh \ 49 | /app/xray-ui \ 50 | /usr/bin/xray-ui 51 | 52 | VOLUME [ "/etc/xray-ui" ] 53 | CMD [ "./xray-ui" ] 54 | ENTRYPOINT [ "/app/DockerEntrypoint.sh" ] -------------------------------------------------------------------------------- /.github/docker/files/DockerInit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | case $1 in 3 | amd64) 4 | ARCH="64" 5 | FNAME="amd64" 6 | ;; 7 | i386) 8 | ARCH="32" 9 | FNAME="i386" 10 | ;; 11 | armv8 | arm64 | aarch64) 12 | ARCH="arm64-v8a" 13 | FNAME="arm64" 14 | ;; 15 | armv7 | arm | arm32) 16 | ARCH="arm32-v7a" 17 | FNAME="arm" 18 | ;; 19 | armv6) 20 | ARCH="arm32-v6" 21 | FNAME="arm" 22 | ;; 23 | *) 24 | ARCH="64" 25 | FNAME="amd64" 26 | ;; 27 | esac 28 | mkdir -p build/bin 29 | cd build/bin 30 | wget "https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-${ARCH}.zip" 31 | unzip "Xray-linux-${ARCH}.zip" 32 | rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat 33 | mv xray "xray-linux-${FNAME}" 34 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat 35 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat 36 | cd ../../ -------------------------------------------------------------------------------- /.github/docker/files/cc-name.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | case $TARGETARCH in 3 | "amd64") 4 | cc='' 5 | ;; 6 | "arm64") 7 | cc='aarch64-linux-gnu-gcc' 8 | ;; 9 | *) 10 | echo "Unknown architecture" 11 | exit 1 12 | ;; 13 | esac 14 | cc_name="$cc" 15 | echo $cc_name -------------------------------------------------------------------------------- /.github/docker/files/file-name.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | case $TARGETARCH in 3 | "amd64") 4 | arch="64" 5 | ;; 6 | "arm64") 7 | arch="arm64-v8a" 8 | ;; 9 | *) 10 | echo "Unknown architecture" 11 | exit 1 12 | ;; 13 | esac 14 | file_name="$arch" 15 | echo $file_name -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | workflow_dispatch: 8 | 9 | env: 10 | REGISTRY: docker.io 11 | 12 | jobs: 13 | build-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | packages: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | submodules: true 21 | 22 | - name: Docker metadata 23 | id: meta 24 | uses: docker/metadata-action@v5 25 | with: 26 | images: juestnow/xray-ui 27 | flavor: latest=true 28 | tags: | 29 | type=ref,event=branch 30 | type=ref,event=pr 31 | type=semver,pattern={{version}} 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: Login to dokcer Container Registry 40 | uses: docker/login-action@v3 41 | with: 42 | registry: ${{ env.REGISTRY }} 43 | username: ${{ secrets.DOCKER_HUB_USER }} 44 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 45 | 46 | - name: Build and push 47 | uses: docker/build-push-action@v6 48 | with: 49 | context: . 50 | platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386 51 | file: .github/docker/Dockerfile 52 | push: true 53 | tags: ${{ steps.meta.outputs.tags }} 54 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release xray-ui 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | platform: 14 | - amd64 15 | - arm64 16 | - armv7 17 | - armv6 18 | - 386 19 | - armv5 20 | - s390x 21 | runs-on: ubuntu-20.04 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup Go 27 | uses: actions/setup-go@v5.5.0 28 | with: 29 | go-version: "stable" 30 | 31 | - name: Install dependencies 32 | run: | 33 | sudo apt-get update 34 | if [ "${{ matrix.platform }}" == "arm64" ]; then 35 | sudo apt install gcc-aarch64-linux-gnu 36 | elif [ "${{ matrix.platform }}" == "armv7" ]; then 37 | sudo apt install gcc-arm-linux-gnueabihf 38 | elif [ "${{ matrix.platform }}" == "armv6" ]; then 39 | sudo apt install gcc-arm-linux-gnueabihf 40 | elif [ "${{ matrix.platform }}" == "386" ]; then 41 | sudo apt install gcc-i686-linux-gnu 42 | elif [ "${{ matrix.platform }}" == "armv5" ]; then 43 | sudo apt install gcc-arm-linux-gnueabi 44 | elif [ "${{ matrix.platform }}" == "s390x" ]; then 45 | sudo apt install gcc-s390x-linux-gnu 46 | fi 47 | 48 | - name: Build xray-ui 49 | run: | 50 | export CGO_ENABLED=1 51 | export GOOS=linux 52 | export GOARCH=${{ matrix.platform }} 53 | if [ "${{ matrix.platform }}" == "arm64" ]; then 54 | export GOARCH=arm64 55 | export CC=aarch64-linux-gnu-gcc 56 | elif [ "${{ matrix.platform }}" == "armv7" ]; then 57 | export GOARCH=arm 58 | export GOARM=7 59 | export CC=arm-linux-gnueabihf-gcc 60 | elif [ "${{ matrix.platform }}" == "armv6" ]; then 61 | export GOARCH=arm 62 | export GOARM=6 63 | export CC=arm-linux-gnueabihf-gcc 64 | elif [ "${{ matrix.platform }}" == "386" ]; then 65 | export GOARCH=386 66 | export CC=i686-linux-gnu-gcc 67 | elif [ "${{ matrix.platform }}" == "armv5" ]; then 68 | export GOARCH=arm 69 | export GOARM=5 70 | export CC=arm-linux-gnueabi-gcc 71 | elif [ "${{ matrix.platform }}" == "s390x" ]; then 72 | export GOARCH=s390x 73 | export CC=s390x-linux-gnu-gcc 74 | fi 75 | go build -o xray-release -trimpath -ldflags '-s -w -buildid= -linkmode "external" -extldflags "-static"' main.go 76 | 77 | mkdir xray-ui 78 | cp xray-release xray-ui/xray-release 79 | cp xray-ui.service xray-ui/xray-ui.service 80 | cd xray-ui 81 | mv xray-release xray-ui 82 | mkdir bin 83 | cd bin 84 | 85 | # Download dependencies 86 | Xray_URL="https://github.com/XTLS/Xray-core/releases/latest/download/" 87 | if [ "${{ matrix.platform }}" == "amd64" ]; then 88 | wget ${Xray_URL}Xray-linux-64.zip 89 | unzip Xray-linux-64.zip 90 | rm -f Xray-linux-64.zip 91 | elif [ "${{ matrix.platform }}" == "arm64" ]; then 92 | wget ${Xray_URL}Xray-linux-arm64-v8a.zip 93 | unzip Xray-linux-arm64-v8a.zip 94 | rm -f Xray-linux-arm64-v8a.zip 95 | elif [ "${{ matrix.platform }}" == "armv7" ]; then 96 | wget ${Xray_URL}Xray-linux-arm32-v7a.zip 97 | unzip Xray-linux-arm32-v7a.zip 98 | rm -f Xray-linux-arm32-v7a.zip 99 | elif [ "${{ matrix.platform }}" == "armv6" ]; then 100 | wget ${Xray_URL}Xray-linux-arm32-v6.zip 101 | unzip Xray-linux-arm32-v6.zip 102 | rm -f Xray-linux-arm32-v6.zip 103 | elif [ "${{ matrix.platform }}" == "386" ]; then 104 | wget ${Xray_URL}Xray-linux-32.zip 105 | unzip Xray-linux-32.zip 106 | rm -f Xray-linux-32.zip 107 | elif [ "${{ matrix.platform }}" == "armv5" ]; then 108 | wget ${Xray_URL}Xray-linux-arm32-v5.zip 109 | unzip Xray-linux-arm32-v5.zip 110 | rm -f Xray-linux-arm32-v5.zip 111 | elif [ "${{ matrix.platform }}" == "s390x" ]; then 112 | wget ${Xray_URL}Xray-linux-s390x.zip 113 | unzip Xray-linux-s390x.zip 114 | rm -f Xray-linux-s390x.zip 115 | fi 116 | rm -f geoip.dat geosite.dat 117 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat 118 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat 119 | mv xray xray-linux-${{ matrix.platform }} 120 | cd ../.. 121 | 122 | - name: Package 123 | run: tar -zcvf xray-ui-linux-${{ matrix.platform }}.tar.gz xray-ui 124 | 125 | - name: Upload files to GH release 126 | uses: svenstaro/upload-release-action@v2 127 | with: 128 | repo_token: ${{ secrets.GITHUB_TOKEN }} 129 | tag: ${{ github.ref }} 130 | file: xray-ui-linux-${{ matrix.platform }}.tar.gz 131 | asset_name: xray-ui-linux-${{ matrix.platform }}.tar.gz 132 | prerelease: true 133 | overwrite: true 134 | - name: Release Changelog Builder 135 | uses: mikepenz/release-changelog-builder-action@v5 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | tmp 3 | dist/ 4 | xray-ui-*.tar.gz 5 | xray-ui/ 6 | bin/ 7 | release.sh 8 | .sync* 9 | xray-ui 10 | -------------------------------------------------------------------------------- /Cloudflare优选IP.md: -------------------------------------------------------------------------------- 1 | # Cloudflare优选IP 2 | 3 | `https://github.com/XIU2/CloudflareSpeedTest/releases` 下载对应环境版本 4 | 5 | ## 下载 CloudflareSpeedTest 6 | 7 | ```bash 8 | # 创建目录 9 | mkdir -p /opt/CloudflareST 10 | # 进入新创建目录 11 | cd /opt/CloudflareST 12 | # 下载对应架构的文件 最新的latest版本 13 | wget https://github.com/XIU2/CloudflareSpeedTest/releases/latest/download/CloudflareST_linux_amd64.tar.gz 14 | # 解压文件 15 | tar -xvf CloudflareST_linux_amd64.tar.gz 16 | ``` 17 | 18 | ## 手动使用CloudflareSpeedTest 19 | 20 | ```bash 21 | # 官方文档 https://github.com/XIU2/CloudflareSpeedTest 22 | # 获取最快10 ip 23 | ./CloudflareST -dn 10 -tll 40 -tl 200 -httping 24 | ``` 25 | 26 | ## 安装coredns 27 | 28 | `https://github.com/coredns/coredns/releases` 选择你要下载版本 29 | 30 | ```bash 31 | # 创建目录 32 | mkdir -p /opt/coredns 33 | # 进入新创建目录 34 | cd /opt/coredns 35 | # 下载对应架构的文件 36 | wget https://github.com/coredns/coredns/releases/download/v1.10.1/coredns_1.10.1_linux_amd64.tgz 37 | # 解压文件 38 | tar -xvf coredns_1.10.1_linux_amd64.tgz 39 | # 创建配置文件 40 | cat >Corefile << EOF 41 | .:443 { 42 | forward . 223.5.5.5:53 114.114.114.114:53 119.29.29.29:53 43 | loadbalance 44 | log . "{remote} {type} {name} {class} {size} {rcode} {duration}" 45 | cache 30 46 | reload 6s 47 | hosts { 48 | 172.64.229.37 test1.example.com 49 | 172.64.229.37 test2.example.com 50 | 172.64.229.37 test3.example.com 51 | 172.64.229.37 test4.example.com 52 | fallthrough 53 | } 54 | } 55 | EOF 56 | # 配置说明: 57 | 443 监听端口 同时监听tcp/udp 支持tls https grpc 等方案 https://coredns.io/plugins/tls/ 58 | forward 本地无解析查询上游dns 59 | loadbalance 负载均衡 60 | log 日志格式 61 | cache 缓存 62 | reload 配置重新加载时间 63 | hosts 默认读取/etc/hosts 也可以自己写 这里我直接写配置文件就可以不动hosts 文件 64 | fallthrough 查询不到域名继续向下查 走入forward 65 | 172.64.229.37 Cloudflare 优选ip 最快速度的 66 | 67 | # 配置启动脚本 68 | 69 | cat >/etc/systemd/system/coredns.service << EOF 70 | [Unit] 71 | Description=smart dns server 72 | After=network.target 73 | 74 | [Service] 75 | ExecStart=/opt/coredns/coredns -conf=/opt/coredns/Corefile 76 | LimitNOFILE=65535 77 | LimitNPROC=65535 78 | LimitCORE=infinity 79 | LimitMEMLOCK=infinity 80 | KillMode=process 81 | Restart=always 82 | StartLimitInterval=0 83 | RestartSec=10 84 | [Install] 85 | WantedBy=multi-user.target 86 | EOF 87 | # 配置开机启动并启动程序 88 | systemctl enable coredns --now 89 | # 查看启动状态 90 | systemctl status coredns 91 | ``` 92 | 93 | ## 配置CloudflareST 并定时执行修改优选IP 94 | 95 | `https://github.com/XIU2/CloudflareSpeedTest/discussions/312` 官方默认修改 /etc/hosts 文件 96 | 97 | ```bash 98 | # 进入CloudflareST 目录 99 | 100 | cd /opt/CloudflareST 101 | 102 | # 使用coredns 实现优选ip 解析需要修改cfst_hosts.sh 要使用hosts 就不用修改 103 | # 下面是修改使用coredns 解析脚本 104 | #!/usr/bin/env bash 105 | PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin 106 | export PATH 107 | # -------------------------------------------------------------- 108 | # 项目: CloudflareSpeedTest 自动更新 Hosts 109 | # 版本: 1.0.4 110 | # 作者: XIU2 111 | # 项目: https://github.com/XIU2/CloudflareSpeedTest 112 | # -------------------------------------------------------------- 113 | 114 | _CHECK() { 115 | while true 116 | do 117 | if [[ ! -e "nowip_hosts.txt" ]]; then 118 | echo -e "该脚本的作用为 CloudflareST 测速后获取最快 IP 并替换 Hosts 中的 Cloudflare CDN IP。\n使用前请先阅读:https://github.com/XIU2/CloudflareSpeedTest/issues/42#issuecomment-768273848" 119 | echo -e "第一次使用,请先将 Hosts 中所有 Cloudflare CDN IP 统一改为一个 IP。" 120 | read -e -p "输入该 Cloudflare CDN IP 并回车(后续不再需要该步骤):" NOWIP 121 | if [[ ! -z "${NOWIP}" ]]; then 122 | echo ${NOWIP} > nowip_hosts.txt 123 | break 124 | else 125 | echo "该 IP 不能是空!" 126 | fi 127 | else 128 | break 129 | fi 130 | done 131 | } 132 | 133 | _UPDATE() { 134 | echo -e "开始测速..." 135 | NOWIP=$(head -1 nowip_hosts.txt) 136 | 137 | # 这里可以自己添加、修改 CloudflareST 的运行参数 138 | ./CloudflareST -dn 10 -tll 40 -tl 200 -httping -o "result_hosts.txt" 139 | 140 | # 如果需要 "找不到满足条件的 IP 就一直循环测速下去",那么可以将下面的两个 exit 0 改为 _UPDATE 即可 141 | [[ ! -e "result_hosts.txt" ]] && echo "CloudflareST 测速结果 IP 数量为 0,跳过下面步骤..." && exit 0 142 | 143 | # 下面这行代码是 "找不到满足条件的 IP 就一直循环测速下去" 才需要的代码 144 | # 考虑到当指定了下载速度下限,但一个满足全部条件的 IP 都没找到时,CloudflareST 就会输出所有 IP 结果 145 | # 因此当你指定 -sl 参数时,需要移除下面这段代码开头的 # 井号注释符,来做文件行数判断(比如下载测速数量:10 个,那么下面的值就设在为 11) 146 | #[[ $(cat result_hosts.txt|wc -l) > 11 ]] && echo "CloudflareST 测速结果没有找到一个完全满足条件的 IP,重新测速..." && _UPDATE 147 | 148 | 149 | BESTIP=$(sed -n "2,1p" result_hosts.txt | awk -F, '{print $1}') 150 | if [[ -z "${BESTIP}" ]]; then 151 | echo "CloudflareST 测速结果 IP 数量为 0,跳过下面步骤..." 152 | exit 0 153 | fi 154 | echo ${BESTIP} > nowip_hosts.txt 155 | echo -e "\n旧 IP 为 ${NOWIP}\n新 IP 为 ${BESTIP}\n" 156 | 157 | echo "开始备份 Hosts 文件(hosts_backup)..." 158 | \cp -f /opt/coredns/Corefile /opt/coredns/Corefile_backup 159 | 160 | echo -e "开始替换..." 161 | sed -i 's/'${NOWIP}'/'${BESTIP}'/g' /opt/coredns/Corefile 162 | echo -e "完成..." 163 | } 164 | 165 | _CHECK 166 | _UPDATE 167 | 168 | # 第一次运行记得给/opt/coredns/Corefile 里面配置优选IP 169 | 170 | ./cfst_hosts.sh 171 | 172 | #输入172.64.229.37 等待 执行结束 173 | # 可以看到/opt/coredns/ 目录生成了备份文件Corefile_backup 更新了的配置文件Corefile 174 | 175 | # 创建定时任务调用脚本 176 | cat >crontab.sh << EOF 177 | #!/bin/bash 178 | cd /opt/CloudflareST && ./cfst_hosts.sh 179 | EOF 180 | # crontab.sh 可执行权限 181 | chmod +x crontab.sh 182 | 183 | # 测试crontab.sh 脚本能否正常运行 记得查看/opt/coredns/Corefile 是不是 改变如果改变证明成功 184 | 185 | /opt/CloudflareST/crontab.sh 186 | 187 | # 每天凌晨 5 点 0 分,执行一次脚本 188 | 0 5 * * * root bash /opt/CloudflareST/crontab.sh >/dev/null 2>&1 189 | 190 | # 每天凌晨 5 点 30 分,执行一次脚本 191 | 30 5 * * * root bash /opt/CloudflareST/crontab.sh >/dev/null 2>&1 192 | 193 | # 每 6 个小时(0 分时),执行一次脚本 194 | 0 */6 * * * root bash /opt/CloudflareST/crontab.sh >/dev/null 2>&1 195 | 196 | # 每小时 0 分,执行一次脚本 197 | # 创建定时任务 198 | # 说明 0 5 * * * 每天凌晨5点运行 199 | vim /etc/crontab 200 | 201 | 0 5 * * * root bash /opt/CloudflareST/crontab.sh >/dev/null 2>&1 202 | 203 | # 保存 wq! 204 | ``` 205 | 206 | ## 客户端配置 207 | 208 | ```bash 209 | 210 | # Clash.Meta 配置 211 | default-nameserver: # 解析rule-providers跟proxy-providers url的域名或者其它dns httpdns 域名解析 212 | - tcp://coredns ip:443 213 | - coredns ip:443 214 | nameserver-policy: # 浏览器 curl 网络工具使用解析 域名 dns 215 | "+.example.com": 216 | - tcp://coredns ip:443 217 | - coredns ip:443 218 | proxy-server-nameserver: # 解析代理节点服务器域名就是 ss trojan vless 等域名 219 | - tcp://coredns ip:443 220 | - coredns ip:443 221 | # linux 系统 win 系统 222 | # 配置 coredns ip 记得监听53 端口 不然就要进行配置 223 | # 当然也支持在你的路由器里面配置。 224 | # 其它客户端的程序根据官方文档修改。 225 | ``` -------------------------------------------------------------------------------- /DockerEntrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Start fail2ban 4 | fail2ban-client -x start 5 | 6 | # Run xray-ui 7 | exec /app/xray-ui -------------------------------------------------------------------------------- /Nginx前置SNI分流.md: -------------------------------------------------------------------------------- 1 | # Nginx前置SNI分流 2 | 3 | xray-ui 面板配置 4 | 5 | 面板配置示例: 6 | 7 | ## 说明:Nginx前置SNI分流 没做nginx 代理时必须打开sockopt 下面的acceptProxyProtocol 如果使用nginx 转发过例如 grpc ws 不需要开启acceptProxyProtocol 8 | 9 | ## reality 配置 dest serverNames 每个配置不能重复别使用www.bing.com这个域名 10 | 11 | ![vless+xtls+tcp+reality+sni](./media/vless+xtls+tcp+reality+sni.png) 12 | 13 | ![vless+grpc+sni](./media/vless+grpc+sni.png) 14 | 15 | any_SNI_No_SNI 配置 16 | 17 | ![any_SNI_No_SNI](./media/any_SNI_No_SNI.png) 18 | 19 | 20 | ## nginx 配置 21 | 22 | nginx.conf 23 | 24 | ```nginx 25 | user nginx; 26 | worker_processes auto; 27 | worker_priority 1; 28 | worker_shutdown_timeout 10s; 29 | error_log /var/log/nginx/error.log notice; 30 | pid /var/run/nginx.pid; 31 | 32 | events { 33 | worker_connections 1024; 34 | multi_accept on; 35 | use epoll; 36 | } 37 | 38 | http { 39 | log_format main '[$time_local] $proxy_protocol_addr "$http_referer" "$http_user_agent"'; 40 | access_log /var/log/nginx/access.log main; 41 | 42 | map $http_upgrade $connection_upgrade { 43 | default upgrade; 44 | "" close; 45 | } 46 | 47 | map $proxy_protocol_addr $proxy_forwarded_elem { 48 | ~^[0-9.]+$ "for=$proxy_protocol_addr"; 49 | ~^[0-9A-Fa-f:.]+$ "for=\"[$proxy_protocol_addr]\""; 50 | default "for=unknown"; 51 | } 52 | 53 | map $http_forwarded $proxy_add_forwarded { 54 | "~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem"; 55 | default "$proxy_forwarded_elem"; 56 | } 57 | 58 | include /etc/nginx/conf.d/*.conf; 59 | } 60 | 61 | stream { 62 | log_format basic '$remote_addr [$time_local] ' 63 | '$protocol $status $bytes_sent $bytes_received ' 64 | '$session_time $ssl_preread_server_name'; 65 | access_log /var/log/nginx/stream-access.log basic buffer=32k; 66 | include /etc/nginx/conf.d/*.stream; 67 | } 68 | 69 | ``` 70 | 71 | vless+grpc+sni 配置 72 | 73 | ```nginx 74 | cd /etc/nginx/conf.d 75 | 76 | # vlessgrpc.conf 文件内容 77 | server { 78 | listen 80; 79 | server_name example.com; 80 | root /usr/share/nginx/html; 81 | index index.php index.html index.htm; 82 | location / { 83 | root /usr/share/nginx/html; 84 | index index.php index.html index.htm; 85 | } 86 | } 87 | 88 | server { 89 | 90 | listen 127.0.0.1:443 ssl http2 proxy_protocol; 91 | set_real_ip_from 127.0.0.1; 92 | real_ip_header proxy_protocol; 93 | server_name example.com; 94 | 95 | index index.html; 96 | root /var/www/html; 97 | 98 | ssl_certificate /path/to/example.cer; 99 | ssl_certificate_key /path/to/example.key; 100 | ssl_protocols TLSv1.2 TLSv1.3; 101 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; 102 | 103 | client_header_timeout 52w; 104 | keepalive_timeout 52w; 105 | # 在 location 后填写 /你的 ServiceName 106 | location /你的 ServiceName { 107 | if ($content_type !~ "application/grpc") { 108 | return 404; 109 | } 110 | client_max_body_size 0; 111 | client_body_buffer_size 512k; 112 | grpc_set_header X-Real-IP $remote_addr; 113 | grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 114 | client_body_timeout 52w; 115 | grpc_read_timeout 52w; 116 | grpc_pass grpc://127.0.0.1:22090; 117 | } 118 | } 119 | 120 | # 默认站点配置 121 | # default.conf 文件内容 122 | server { 123 | listen 80 default_server reuseport so_keepalive=on backlog=4096; 124 | server_name _; 125 | root /usr/share/nginx/html; 126 | index index.php index.html index.htm; 127 | location / { 128 | root /usr/share/nginx/html; 129 | index index.php index.html index.htm; 130 | } 131 | } 132 | server { 133 | listen 127.0.0.1:443 ssl http2 proxy_protocol reuseport default_server so_keepalive=on backlog=4096; 134 | set_real_ip_from 127.0.0.1; 135 | real_ip_header proxy_protocol; 136 | server_name _; 137 | index index.html; 138 | root /var/www/html; 139 | 140 | ssl_certificate /path/to/example.cer; 141 | ssl_certificate_key /path/to/example.key; 142 | ssl_protocols TLSv1.2 TLSv1.3; 143 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; 144 | access_log off; 145 | proxy_ssl_server_name on; 146 | location / { 147 | root /usr/share/nginx/html; 148 | index index.php index.html index.htm; 149 | } 150 | } 151 | ``` 152 | 153 | sni 分流配置 154 | 155 | ```nginx 156 | # default.stream 文件内容 157 | map $ssl_preread_server_name $stream_map { 158 | example.com web; # vless+grpc+sni 配置 159 | www.apple.com vlesstcpreality; # vless+xtls+tcp+reality+sni配置 reality 配置 dest serverNames 每个配置不能重复别使用www.bing.com这个域名 160 | default sni; # 默认转发到any_SNI_No_SNI 161 | } 162 | 163 | upstream web { 164 | server 127.0.0.1:443; 165 | } 166 | 167 | 168 | upstream vlesstcpreality { 169 | server 127.0.0.1:36712; 170 | } 171 | # 下面是转发到any_SNI_No_SNI 172 | upstream sni { 173 | server 127.0.0.1:49026; 174 | } 175 | 176 | 177 | server { 178 | listen [服务器公网IP]:443 reuseport so_keepalive=on backlog=4096; 179 | proxy_pass $stream_map; 180 | ssl_preread on; 181 | proxy_protocol on; 182 | } 183 | 184 | ``` 185 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | //go:embed version 11 | var version string 12 | 13 | //go:embed name 14 | var name string 15 | 16 | type LogLevel string 17 | 18 | const ( 19 | Debug LogLevel = "debug" 20 | Info LogLevel = "info" 21 | Warn LogLevel = "warn" 22 | Error LogLevel = "error" 23 | ) 24 | 25 | func GetVersion() string { 26 | return strings.TrimSpace(version) 27 | } 28 | 29 | func GetName() string { 30 | return strings.TrimSpace(name) 31 | } 32 | 33 | func GetLogLevel() LogLevel { 34 | if IsDebug() { 35 | return Debug 36 | } 37 | logLevel := os.Getenv("XUI_LOG_LEVEL") 38 | if logLevel == "" { 39 | return Info 40 | } 41 | return LogLevel(logLevel) 42 | } 43 | 44 | func IsDebug() bool { 45 | return os.Getenv("XUI_DEBUG") == "true" 46 | } 47 | 48 | func GetBinFolderPath() string { 49 | binFolderPath := os.Getenv("XUI_BIN_FOLDER") 50 | if binFolderPath == "" { 51 | binFolderPath = "bin" 52 | } 53 | return binFolderPath 54 | } 55 | 56 | func GetDBFolderPath() string { 57 | dbFolderPath := os.Getenv("XUI_DB_FOLDER") 58 | if dbFolderPath == "" { 59 | dbFolderPath = "/etc/xray-ui" 60 | } 61 | return dbFolderPath 62 | } 63 | func GetDBPath() string { 64 | return fmt.Sprintf("/etc/%s/%s.db", GetName(), GetName()) 65 | } 66 | -------------------------------------------------------------------------------- /config/name: -------------------------------------------------------------------------------- 1 | xray-ui -------------------------------------------------------------------------------- /config/version: -------------------------------------------------------------------------------- 1 | 25.03.31 -------------------------------------------------------------------------------- /database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "gorm.io/driver/sqlite" 7 | "gorm.io/gorm" 8 | "gorm.io/gorm/logger" 9 | "io/fs" 10 | "os" 11 | "path" 12 | "xray-ui/config" 13 | "xray-ui/database/model" 14 | ) 15 | 16 | var db *gorm.DB 17 | 18 | func initUser() error { 19 | err := db.AutoMigrate(&model.User{}) 20 | if err != nil { 21 | return err 22 | } 23 | var count int64 24 | err = db.Model(&model.User{}).Count(&count).Error 25 | if err != nil { 26 | return err 27 | } 28 | if count == 0 { 29 | user := &model.User{ 30 | Username: "admin", 31 | Password: "admin", 32 | } 33 | return db.Create(user).Error 34 | } 35 | return nil 36 | } 37 | func initVersion() error { 38 | err := db.AutoMigrate(&model.VersionStatus{}) 39 | if err != nil { 40 | return err 41 | } 42 | var count int64 43 | err = db.Model(&model.VersionStatus{}).Count(&count).Error 44 | if err != nil { 45 | return err 46 | } 47 | if count == 0 { 48 | VersionStatus := &model.VersionStatus{ 49 | Version: "未更新", 50 | } 51 | return db.Create(VersionStatus).Error 52 | } 53 | return nil 54 | } 55 | func initInbound() error { 56 | return db.AutoMigrate(&model.Inbound{}) 57 | } 58 | 59 | func initSetting() error { 60 | return db.AutoMigrate(&model.Setting{}) 61 | } 62 | 63 | func InitDB(dbPath string) error { 64 | dir := path.Dir(dbPath) 65 | err := os.MkdirAll(dir, fs.ModeDir) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | var gormLogger logger.Interface 71 | 72 | if config.IsDebug() { 73 | gormLogger = logger.Default 74 | } else { 75 | gormLogger = logger.Discard 76 | } 77 | 78 | c := &gorm.Config{ 79 | Logger: gormLogger, 80 | } 81 | db, err = gorm.Open(sqlite.Open(dbPath), c) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | err = initUser() 87 | if err != nil { 88 | return err 89 | } 90 | 91 | err = initVersion() 92 | if err != nil { 93 | return err 94 | } 95 | err = initInbound() 96 | if err != nil { 97 | return err 98 | } 99 | err = initSetting() 100 | if err != nil { 101 | return err 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func GetDB() *gorm.DB { 108 | return db 109 | } 110 | 111 | func IsNotFound(err error) bool { 112 | return err == gorm.ErrRecordNotFound 113 | } 114 | 115 | func IsSQLiteDB(file io.Reader) (bool, error) { 116 | signature := []byte("SQLite format 3\x00") 117 | buf := make([]byte, len(signature)) 118 | _, err := file.Read(buf) 119 | if err != nil { 120 | return false, err 121 | } 122 | return bytes.Equal(buf, signature), nil 123 | } 124 | -------------------------------------------------------------------------------- /database/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "xray-ui/util/json_util" 6 | "xray-ui/xray" 7 | ) 8 | 9 | type Protocol string 10 | 11 | const ( 12 | VMess Protocol = "vmess" 13 | VLESS Protocol = "vless" 14 | Dokodemo Protocol = "Dokodemo-door" 15 | Http Protocol = "http" 16 | Trojan Protocol = "trojan" 17 | Shadowsocks Protocol = "shadowsocks" 18 | ) 19 | 20 | type User struct { 21 | Id int `json:"id" gorm:"primaryKey;autoIncrement"` 22 | Username string `json:"username"` 23 | Password string `json:"password"` 24 | } 25 | type VersionStatus struct { 26 | Id int `json:"id" gorm:"primaryKey;autoIncrement"` 27 | Version string `json:"version"` 28 | } 29 | type Inbound struct { 30 | Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` 31 | UserId int `json:"-"` 32 | Up int64 `json:"up" form:"up"` 33 | Down int64 `json:"down" form:"down"` 34 | Total int64 `json:"total" form:"total"` 35 | Remark string `json:"remark" form:"remark"` 36 | Enable bool `json:"enable" form:"enable"` 37 | ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` 38 | 39 | // config part 40 | Listen string `json:"listen" form:"listen"` 41 | Port int `json:"port" form:"port" gorm:"unique"` 42 | Protocol Protocol `json:"protocol" form:"protocol"` 43 | Settings string `json:"settings" form:"settings"` 44 | StreamSettings string `json:"streamSettings" form:"streamSettings"` 45 | Tag string `json:"tag" form:"tag" gorm:"unique"` 46 | Sniffing string `json:"sniffing" form:"sniffing"` 47 | } 48 | 49 | func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { 50 | listen := i.Listen 51 | if listen != "" { 52 | listen = fmt.Sprintf("\"%v\"", listen) 53 | } 54 | return &xray.InboundConfig{ 55 | Listen: json_util.RawMessage(listen), 56 | Port: i.Port, 57 | Protocol: string(i.Protocol), 58 | Settings: json_util.RawMessage(i.Settings), 59 | StreamSettings: json_util.RawMessage(i.StreamSettings), 60 | Tag: i.Tag, 61 | Sniffing: json_util.RawMessage(i.Sniffing), 62 | } 63 | } 64 | 65 | type Setting struct { 66 | Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` 67 | Key string `json:"key" form:"key"` 68 | Value string `json:"value" form:"value"` 69 | } 70 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module xray-ui 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.5.0 7 | github.com/Workiva/go-datastructures v1.1.5 8 | github.com/gin-contrib/sessions v1.0.4 9 | github.com/gin-gonic/gin v1.10.1 10 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 11 | github.com/nicksnyder/go-i18n/v2 v2.6.0 12 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 13 | github.com/robfig/cron/v3 v3.0.1 14 | github.com/shirou/gopsutil/v3 v3.24.5 15 | github.com/xtls/xray-core v1.250516.0 16 | go.uber.org/atomic v1.11.0 17 | golang.org/x/crypto v0.38.0 18 | golang.org/x/text v0.25.0 19 | google.golang.org/grpc v1.72.2 20 | gorm.io/driver/sqlite v1.5.7 21 | gorm.io/gorm v1.30.0 22 | ) 23 | 24 | require ( 25 | github.com/bytedance/sonic v1.13.2 // indirect 26 | github.com/bytedance/sonic/loader v0.2.4 // indirect 27 | github.com/cloudwego/base64x v0.1.5 // indirect 28 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 29 | github.com/gin-contrib/sse v1.0.0 // indirect 30 | github.com/go-ole/go-ole v1.2.6 // indirect 31 | github.com/go-playground/locales v0.14.1 // indirect 32 | github.com/go-playground/universal-translator v0.18.1 // indirect 33 | github.com/go-playground/validator/v10 v10.26.0 // indirect 34 | github.com/goccy/go-json v0.10.5 // indirect 35 | github.com/gorilla/context v1.1.2 // indirect 36 | github.com/gorilla/securecookie v1.1.2 // indirect 37 | github.com/gorilla/sessions v1.4.0 // indirect 38 | github.com/jinzhu/inflection v1.0.0 // indirect 39 | github.com/jinzhu/now v1.1.5 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 42 | github.com/leodido/go-urn v1.4.0 // indirect 43 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 44 | github.com/mattn/go-isatty v0.0.20 // indirect 45 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 47 | github.com/modern-go/reflect2 v1.0.2 // indirect 48 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 49 | github.com/pires/go-proxyproto v0.8.1 // indirect 50 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 51 | github.com/sagernet/sing v0.5.1 // indirect 52 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 53 | github.com/tklauser/go-sysconf v0.3.12 // indirect 54 | github.com/tklauser/numcpus v0.6.1 // indirect 55 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 56 | github.com/ugorji/go/codec v1.2.12 // indirect 57 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 58 | golang.org/x/arch v0.16.0 // indirect 59 | golang.org/x/net v0.40.0 // indirect 60 | golang.org/x/sys v0.33.0 // indirect 61 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 62 | google.golang.org/protobuf v1.36.6 // indirect 63 | gopkg.in/yaml.v3 v3.0.1 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/op/go-logging" 5 | "os" 6 | ) 7 | 8 | var logger *logging.Logger 9 | 10 | func init() { 11 | InitLogger(logging.INFO) 12 | } 13 | 14 | func InitLogger(level logging.Level) { 15 | format := logging.MustStringFormatter( 16 | `%{time:2006/01/02 15:04:05} %{level} - %{message}`, 17 | ) 18 | newLogger := logging.MustGetLogger("xray-ui") 19 | backend := logging.NewLogBackend(os.Stderr, "", 0) 20 | backendFormatter := logging.NewBackendFormatter(backend, format) 21 | backendLeveled := logging.AddModuleLevel(backendFormatter) 22 | backendLeveled.SetLevel(level, "") 23 | newLogger.SetBackend(backendLeveled) 24 | 25 | logger = newLogger 26 | } 27 | 28 | func Debug(args ...interface{}) { 29 | logger.Debug(args...) 30 | } 31 | 32 | func Debugf(format string, args ...interface{}) { 33 | logger.Debugf(format, args...) 34 | } 35 | 36 | func Info(args ...interface{}) { 37 | logger.Info(args...) 38 | } 39 | 40 | func Infof(format string, args ...interface{}) { 41 | logger.Infof(format, args...) 42 | } 43 | 44 | func Warning(args ...interface{}) { 45 | logger.Warning(args...) 46 | } 47 | 48 | func Warningf(format string, args ...interface{}) { 49 | logger.Warningf(format, args...) 50 | } 51 | 52 | func Error(args ...interface{}) { 53 | logger.Error(args...) 54 | } 55 | 56 | func Errorf(format string, args ...interface{}) { 57 | logger.Errorf(format, args...) 58 | } 59 | -------------------------------------------------------------------------------- /media/2022-04-04_141259.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/2022-04-04_141259.png -------------------------------------------------------------------------------- /media/APIKey1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/APIKey1.PNG -------------------------------------------------------------------------------- /media/APIKey2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/APIKey2.png -------------------------------------------------------------------------------- /media/DetailEnter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/DetailEnter.png -------------------------------------------------------------------------------- /media/any_SNI_No_SNI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/any_SNI_No_SNI.png -------------------------------------------------------------------------------- /media/bda84fbc2ede834deaba1c173a932223.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/bda84fbc2ede834deaba1c173a932223.png -------------------------------------------------------------------------------- /media/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "services": [ 4 | "HandlerService", 5 | "LoggerService", 6 | "StatsService" 7 | ], 8 | "tag": "api" 9 | }, 10 | "inbounds": [ 11 | { 12 | "listen": "127.0.0.1", 13 | "port": 62789, 14 | "protocol": "dokodemo-door", 15 | "settings": { 16 | "address": "127.0.0.1" 17 | }, 18 | "tag": "api" 19 | } 20 | ], 21 | "outbounds": [ 22 | { 23 | "protocol": "freedom", 24 | "settings": {} 25 | }, 26 | { 27 | "protocol": "blackhole", 28 | "tag": "blocked" 29 | } 30 | ], 31 | "policy": { 32 | "system": { 33 | "statsInboundDownlink": true, 34 | "statsInboundUplink": true, 35 | "statsOutboundDownlink": true, 36 | "statsOutboundUplink": true 37 | }, 38 | "levels": { 39 | "0": { 40 | "handshake": 2, 41 | "connIdle": 120, 42 | "uplinkOnly": 1, 43 | "downlinkOnly": 1 44 | } 45 | } 46 | }, 47 | "routing": { 48 | "domainStrategy": "IPIfNonMatch", 49 | "rules": [ 50 | { 51 | "inboundTag": [ 52 | "api" 53 | ], 54 | "outboundTag": "api", 55 | "type": "field" 56 | }, 57 | { 58 | "type": "field", 59 | "domain": [ 60 | "www.gstatic.com" 61 | ], 62 | "outboundTag": "direct" 63 | }, 64 | { 65 | "type": "field", 66 | "domain": [ 67 | "geosite:category-ads-all", 68 | "geosite:cn", 69 | "geosite:geolocation-cn" 70 | ], 71 | "outboundTag": "blocked" 72 | }, 73 | { 74 | "type": "field", 75 | "ip": [ 76 | "geoip:cn" 77 | ], 78 | "outboundTag": "blocked" 79 | } 80 | ] 81 | }, 82 | "stats": {} 83 | } -------------------------------------------------------------------------------- /media/d13ffd6a73f938d1037d0708e31433bf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/d13ffd6a73f938d1037d0708e31433bf.png -------------------------------------------------------------------------------- /media/reality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/reality.png -------------------------------------------------------------------------------- /media/sysctl.conf: -------------------------------------------------------------------------------- 1 | fs.file-max = 1024000 2 | net.core.rmem_max = 26214400 3 | net.core.wmem_max = 26214400 4 | net.core.rmem_default = 26214400 5 | net.core.wmem_default = 26214400 6 | net.core.netdev_max_backlog = 4096 7 | net.core.somaxconn = 4096 8 | net.ipv4.tcp_syncookies = 1 9 | net.ipv4.tcp_tw_reuse = 1 10 | net.ipv4.tcp_fin_timeout = 2 11 | net.ipv4.ip_local_port_range = 1024 65535 12 | net.ipv4.tcp_max_syn_backlog = 4096 13 | net.ipv4.tcp_max_tw_buckets = 5000 14 | net.ipv4.tcp_rmem = 4096 87380 67108864 15 | net.ipv4.tcp_wmem = 4096 65536 67108864 16 | net.ipv4.tcp_mtu_probing = 1 17 | net.ipv4.tcp_fastopen = 3 18 | kernel.msgmnb = 65536 19 | kernel.msgmax = 65536 20 | kernel.shmmax = 68719476736 21 | kernel.shmall = 4294967296 22 | net.ipv4.tcp_keepalive_intvl = 30 23 | net.ipv4.tcp_keepalive_probes = 10 24 | net.ipv4.tcp_keepalive_time = 600 25 | net.ipv4.ip_forward = 1 26 | net.ipv4.conf.all.accept_source_route = 0 27 | net.ipv4.conf.all.accept_redirects = 0 28 | net.ipv4.conf.all.send_redirects = 0 29 | net.ipv4.conf.all.rp_filter = 0 30 | net.ipv4.conf.default.accept_source_route = 0 31 | net.ipv4.conf.default.accept_redirects = 0 32 | net.ipv4.conf.default.send_redirects = 0 33 | net.ipv4.conf.default.rp_filter = 0 34 | net.ipv4.conf.eth0.send_redirects = 0 35 | net.ipv4.conf.eth0.rp_filter = 0 36 | net.ipv6.conf.eth0.accept_ra=0 37 | net.core.default_qdisc = fq 38 | net.ipv4.tcp_congestion_control = bbr 39 | net.netfilter.nf_conntrack_max = 655360 40 | net.nf_conntrack_max = 655350 41 | net.netfilter.nf_conntrack_tcp_timeout_established = 180 42 | net.netfilter.nf_conntrack_tcp_timeout_last_ack=10 43 | net.ipv4.tcp_notsent_lowat=16384 44 | net.ipv4.tcp_slow_start_after_idle=0 45 | net.ipv6.conf.all.forwarding=1 46 | net.ipv4.conf.all.proxy_arp = 1 47 | net.ipv4.conf.default.proxy_arp = 1 -------------------------------------------------------------------------------- /media/trojan+grpc+reality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/trojan+grpc+reality.png -------------------------------------------------------------------------------- /media/vless+H2+reality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/vless+H2+reality.png -------------------------------------------------------------------------------- /media/vless+grpc+reality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/vless+grpc+reality.png -------------------------------------------------------------------------------- /media/vless+grpc+sni.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/vless+grpc+sni.png -------------------------------------------------------------------------------- /media/vless+xtls+tcp+reality+sni.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/vless+xtls+tcp+reality+sni.png -------------------------------------------------------------------------------- /media/vless+xtls+tcp+reality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/media/vless+xtls+tcp+reality.png -------------------------------------------------------------------------------- /media/xray.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "loglevel": "debug", 4 | "dnsLog": false 5 | }, 6 | "inbounds": [ 7 | { 8 | "tag": "socks", 9 | "port": 17889, 10 | "listen": "0.0.0.0", 11 | "protocol": "socks", 12 | "sniffing": { 13 | "enabled": false, 14 | "destOverride": [ 15 | "http", 16 | "tls", 17 | "quic" 18 | ], 19 | "routeOnly": false 20 | }, 21 | "settings": { 22 | "auth": "noauth", 23 | "udp": true, 24 | "allowTransparent": false 25 | } 26 | } 27 | ], 28 | "outbounds": [ 29 | { 30 | "tag": "proxy", 31 | "protocol": "vless", 32 | "settings": { 33 | "vnext": [ 34 | { 35 | "address": "xtls.github.io", 36 | "port": 443, 37 | "users": [ 38 | { 39 | "id": "963e2fe3-2b3c-4fb8-a008-99facaae99c2", 40 | "alterId": 0, 41 | "email": "t@t.tt", 42 | "security": "auto", 43 | "encryption": "none" 44 | } 45 | ] 46 | } 47 | ] 48 | }, 49 | "streamSettings": { 50 | "network": "h2", 51 | "security": "reality", 52 | "httpSettings": { 53 | "path": "" 54 | }, 55 | "realitySettings": { 56 | "serverName": "xtls.github.io", 57 | "fingerprint": "chrome", 58 | "show": false, 59 | "publicKey": "o90V5sTqwkda1c2nGYJ10EYttO-1pvR_1nlLDrvnWzI", 60 | "shortId": "b76e3dee045e4f4d", 61 | "spiderX": "/" 62 | } 63 | }, 64 | "mux": { 65 | "enabled": true, 66 | "concurrency": 8 67 | }, 68 | "sockopt": { 69 | "dialerProxy": "fragment" 70 | } 71 | }, 72 | { 73 | "protocol": "freedom", 74 | "settings": { 75 | "fragment": { 76 | "packets": "tlshello", 77 | "length": "100-200", 78 | "interval": "10-20" 79 | } 80 | }, 81 | "streamSettings": { 82 | "sockopt": { 83 | "tcpNoDelay": true 84 | } 85 | }, 86 | "tag": "fragment" 87 | } 88 | ], 89 | "stats": {} 90 | } -------------------------------------------------------------------------------- /reality.md: -------------------------------------------------------------------------------- 1 | xray-ui 面板配置 reality 2 | 3 | ```bash 4 | # 生成 Private key 与 Public key 5 | # 两种方案 6 | # 1、 xray 客户端生成 amd64 arm64 7 | /usr/local/xray-ui/bin/xray-linux-amd64 x25519 8 | # 更新xray-ui 到最新版本 9 | xray-ui x25519 10 | # shortIds 生成 0 到 f,长度为 2 的倍数,长度上限为 16,或执行 openssl rand -hex 8 生成 可以为空不填 可以多行 11 | "shortIds": [ // 客户端可用的 shortId 列表,可用于区分不同的客户端 12 | "", // 若有此项,客户端 shortId 可为空 13 | "a1", // 0 到 f,长度为 2 的倍数,长度上限为 16,或执行 openssl rand -hex 8 生成 14 | "bc19", 15 | "b2da06", 16 | "2d940fe6", 17 | "b85e293fa1", 18 | "4a9f72b5c803", 19 | "19f70b462cea5d", 20 | "6ba85179e30d4fc2" 21 | ] 22 | ``` 23 | 24 | dest 回源到自己nginx 服务器 用自己域名访问不会报证书问题 25 | nginx.conf 配置 26 | 27 | ```nginx 28 | user nginx; 29 | worker_processes auto; 30 | 31 | error_log /var/log/nginx/error.log notice; 32 | pid /var/run/nginx.pid; 33 | 34 | events { 35 | worker_connections 1024; 36 | } 37 | 38 | http { 39 | log_format main '[$time_local] $proxy_protocol_addr "$http_referer" "$http_user_agent"'; 40 | access_log /var/log/nginx/access.log main; 41 | 42 | map $http_upgrade $connection_upgrade { 43 | default upgrade; 44 | "" close; 45 | } 46 | 47 | map $proxy_protocol_addr $proxy_forwarded_elem { 48 | ~^[0-9.]+$ "for=$proxy_protocol_addr"; 49 | ~^[0-9A-Fa-f:.]+$ "for=\"[$proxy_protocol_addr]\""; 50 | default "for=unknown"; 51 | } 52 | 53 | map $http_forwarded $proxy_add_forwarded { 54 | "~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem"; 55 | default "$proxy_forwarded_elem"; 56 | } 57 | 58 | server { 59 | listen 80; 60 | return 301 https://$host$request_uri; 61 | } 62 | 63 | server { 64 | #listen 127.0.0.1:8003 ssl http2 proxy_protocol; 65 | #set_real_ip_from 127.0.0.1; 66 | listen unix:/dev/shm/nginx.sock ssl http2 proxy_protocol; 67 | set_real_ip_from unix:; 68 | 69 | ssl_certificate /etc/ssl/private/fullchain.cer; 70 | ssl_certificate_key /etc/ssl/private/private.key; 71 | 72 | ssl_protocols TLSv1.2 TLSv1.3; 73 | ssl_ciphers TLS13_AES_128_GCM_SHA256:TLS13_AES_256_GCM_SHA384:TLS13_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305; 74 | 75 | ssl_session_timeout 1d; 76 | ssl_session_cache shared:SSL:10m; 77 | ssl_session_tickets off; 78 | 79 | ssl_stapling on; 80 | ssl_stapling_verify on; 81 | resolver 1.1.1.1 valid=60s; 82 | resolver_timeout 2s; 83 | 84 | location / { 85 | sub_filter $proxy_host $host; # xray 非标准端口 改成 $proxy_host $http_host; 这样机可以域名加端口实现完整访问 86 | sub_filter_once off; 87 | 88 | proxy_pass https://www.lovelive-anime.jp; 89 | proxy_set_header Host $proxy_host; 90 | 91 | proxy_http_version 1.1; 92 | proxy_cache_bypass $http_upgrade; 93 | 94 | proxy_ssl_server_name on; 95 | 96 | proxy_set_header Upgrade $http_upgrade; 97 | proxy_set_header Connection $connection_upgrade; 98 | proxy_set_header X-Real-IP $proxy_protocol_addr; 99 | proxy_set_header Forwarded $proxy_add_forwarded; 100 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 101 | proxy_set_header X-Forwarded-Proto $scheme; 102 | proxy_set_header X-Forwarded-Host $host; 103 | proxy_set_header X-Forwarded-Port $server_port; 104 | 105 | proxy_connect_timeout 60s; 106 | proxy_send_timeout 60s; 107 | proxy_read_timeout 60s; 108 | 109 | resolver 1.1.1.1; 110 | } 111 | } 112 | } 113 | 114 | ``` 115 | 116 | 面板配置示例: 117 | 118 | ![vless+xtls+tcp+reality](./media/vless+xtls+tcp+reality.png) 119 | ![vless+grpc+reality](./media/vless+grpc+reality.png) 120 | ![vless+H2+reality](./media/vless+H2+reality.png) 121 | ![trojan+grpc+reality](./media/trojan+grpc+reality.png) 122 | 123 | 客户端配置 124 | 125 | 1、Clash.Meta 126 | 127 | Clash.Meta 不能对vless-h2-reality测速 128 | 129 | ```yaml 130 | 131 | proxies: 132 | - name: "vless-reality-vision" 133 | type: vless 134 | server: 127.0.0.1 135 | port: 36712 136 | uuid: uuid 137 | network: tcp 138 | tls: true 139 | udp: true 140 | flow: xtls-rprx-vision 141 | servername: www.lovelive-anime.jp 142 | reality-opts: 143 | public-key: publicKey 144 | short-id: shortIds 145 | 146 | - name: "vless-reality-grpc" 147 | type: vless 148 | server: 127.0.0.1 149 | port: 51878 150 | uuid: uuid 151 | network: grpc 152 | tls: true 153 | udp: true 154 | flow: 155 | # skip-cert-verify: true 156 | servername: www.lovelive-anime.jp 157 | grpc-opts: 158 | grpc-service-name: "path" 159 | reality-opts: 160 | public-key: publicKey 161 | short-id: shortIds 162 | 163 | - name: trojan-reality-grpc 164 | server: 127.0.0.1 165 | port: 52310 166 | type: trojan 167 | password: "password" 168 | network: grpc 169 | alpn: 170 | - h2 171 | sni: www.lovelive-anime.jp 172 | flow: 173 | skip-cert-verify: false 174 | udp: true 175 | grpc-opts: 176 | grpc-service-name: "path" 177 | reality-opts: 178 | public-key: publicKey 179 | short-id: shortIds 180 | 181 | ``` 182 | 183 | 2 xray 原生配置参考: 184 | 185 | `https://github.com/chika0801/Xray-examples` 186 | -------------------------------------------------------------------------------- /util/common/err.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "xray-ui/logger" 7 | ) 8 | 9 | var CtxDone = errors.New("context done") 10 | 11 | func NewErrorf(format string, a ...interface{}) error { 12 | msg := fmt.Sprintf(format, a...) 13 | return errors.New(msg) 14 | } 15 | 16 | func NewError(a ...interface{}) error { 17 | msg := fmt.Sprintln(a...) 18 | return errors.New(msg) 19 | } 20 | 21 | func Recover(msg string) interface{} { 22 | panicErr := recover() 23 | if panicErr != nil { 24 | if msg != "" { 25 | logger.Error(msg, "panic:", panicErr) 26 | } 27 | } 28 | return panicErr 29 | } 30 | -------------------------------------------------------------------------------- /util/common/format.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func FormatTraffic(trafficBytes int64) (size string) { 8 | if trafficBytes < 1024 { 9 | return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1)) 10 | } else if trafficBytes < (1024 * 1024) { 11 | return fmt.Sprintf("%.2fKB", float64(trafficBytes)/float64(1024)) 12 | } else if trafficBytes < (1024 * 1024 * 1024) { 13 | return fmt.Sprintf("%.2fMB", float64(trafficBytes)/float64(1024*1024)) 14 | } else if trafficBytes < (1024 * 1024 * 1024 * 1024) { 15 | return fmt.Sprintf("%.2fGB", float64(trafficBytes)/float64(1024*1024*1024)) 16 | } else if trafficBytes < (1024 * 1024 * 1024 * 1024 * 1024) { 17 | return fmt.Sprintf("%.2fTB", float64(trafficBytes)/float64(1024*1024*1024*1024)) 18 | } else { 19 | return fmt.Sprintf("%.2fEB", float64(trafficBytes)/float64(1024*1024*1024*1024*1024)) 20 | } 21 | } 22 | 23 | func FormatTime(timeseconds uint64) (timeStr string) { 24 | if timeseconds < 60 { 25 | return fmt.Sprintf("%d seconds", timeseconds) 26 | } else if timeseconds < 60*60 { 27 | return fmt.Sprintf("%d minutes", timeseconds/(60)) 28 | } else if timeseconds < 60*60*24 { 29 | return fmt.Sprintf("%d hours", timeseconds/(60*60)) 30 | } else { 31 | return fmt.Sprintf("%d days", timeseconds/(60*60*24)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /util/common/multi_error.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type multiError []error 8 | 9 | func (e multiError) Error() string { 10 | var r strings.Builder 11 | r.WriteString("multierr: ") 12 | for _, err := range e { 13 | r.WriteString(err.Error()) 14 | r.WriteString(" | ") 15 | } 16 | return r.String() 17 | } 18 | 19 | func Combine(maybeError ...error) error { 20 | var errs multiError 21 | for _, err := range maybeError { 22 | if err != nil { 23 | errs = append(errs, err) 24 | } 25 | } 26 | if len(errs) == 0 { 27 | return nil 28 | } 29 | return errs 30 | } 31 | -------------------------------------------------------------------------------- /util/common/network.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | ) 7 | 8 | func GetMyIpAddr() string { 9 | resp, err := http.Get("https://api64.ipify.org") 10 | if err != nil { 11 | resp, _ = http.Get("http://ip.sb") 12 | } 13 | defer resp.Body.Close() 14 | s, _ := ioutil.ReadAll(resp.Body) 15 | return string(s) 16 | } 17 | -------------------------------------------------------------------------------- /util/common/stringUtil.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "sort" 6 | ) 7 | 8 | func IsSubString(target string, str_array []string) bool { 9 | sort.Strings(str_array) 10 | index := sort.SearchStrings(str_array, target) 11 | return index < len(str_array) && str_array[index] == target 12 | } 13 | 14 | func ByteToString(p []byte) string { 15 | for i := 0; i < len(p); i++ { 16 | if p[i] == '\n' { 17 | return string(p[0:i]) 18 | } 19 | } 20 | return string(p) 21 | } 22 | 23 | /* 24 | * if some byte slice have the '\n' we need clear these special characters 25 | * to get a standard string 26 | */ 27 | func ByteToStringWithOutNewLine(p []byte) string { 28 | return string(bytes.Replace(p, []byte("\n"), []byte(""), 1)) 29 | } 30 | -------------------------------------------------------------------------------- /util/context.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "context" 4 | 5 | func IsDone(ctx context.Context) bool { 6 | select { 7 | case <-ctx.Done(): 8 | return true 9 | default: 10 | return false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /util/json_util/json.go: -------------------------------------------------------------------------------- 1 | package json_util 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type RawMessage []byte 8 | 9 | // MarshalJSON 自定义 json.RawMessage 默认行为 10 | func (m RawMessage) MarshalJSON() ([]byte, error) { 11 | if len(m) == 0 { 12 | return []byte("null"), nil 13 | } 14 | return m, nil 15 | } 16 | 17 | // UnmarshalJSON sets *m to a copy of data. 18 | func (m *RawMessage) UnmarshalJSON(data []byte) error { 19 | if m == nil { 20 | return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") 21 | } 22 | *m = append((*m)[0:0], data...) 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /util/random/random.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | var numSeq [10]rune 9 | var lowerSeq [26]rune 10 | var upperSeq [26]rune 11 | var numLowerSeq [36]rune 12 | var numUpperSeq [36]rune 13 | var allSeq [62]rune 14 | 15 | func init() { 16 | rand.Seed(time.Now().UnixNano()) 17 | 18 | for i := 0; i < 10; i++ { 19 | numSeq[i] = rune('0' + i) 20 | } 21 | for i := 0; i < 26; i++ { 22 | lowerSeq[i] = rune('a' + i) 23 | upperSeq[i] = rune('A' + i) 24 | } 25 | 26 | copy(numLowerSeq[:], numSeq[:]) 27 | copy(numLowerSeq[len(numSeq):], lowerSeq[:]) 28 | 29 | copy(numUpperSeq[:], numSeq[:]) 30 | copy(numUpperSeq[len(numSeq):], upperSeq[:]) 31 | 32 | copy(allSeq[:], numSeq[:]) 33 | copy(allSeq[len(numSeq):], lowerSeq[:]) 34 | copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:]) 35 | } 36 | 37 | func Seq(n int) string { 38 | runes := make([]rune, n) 39 | for i := 0; i < n; i++ { 40 | runes[i] = allSeq[rand.Intn(len(allSeq))] 41 | } 42 | return string(runes) 43 | } 44 | -------------------------------------------------------------------------------- /util/reflect_util/reflect.go: -------------------------------------------------------------------------------- 1 | package reflect_util 2 | 3 | import "reflect" 4 | 5 | func GetFields(t reflect.Type) []reflect.StructField { 6 | num := t.NumField() 7 | fields := make([]reflect.StructField, 0, num) 8 | for i := 0; i < num; i++ { 9 | fields = append(fields, t.Field(i)) 10 | } 11 | return fields 12 | } 13 | 14 | func GetFieldValues(v reflect.Value) []reflect.Value { 15 | num := v.NumField() 16 | fields := make([]reflect.Value, 0, num) 17 | for i := 0; i < num; i++ { 18 | fields = append(fields, v.Field(i)) 19 | } 20 | return fields 21 | } 22 | -------------------------------------------------------------------------------- /util/sys/a.s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/util/sys/a.s -------------------------------------------------------------------------------- /util/sys/psutil.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | _ "unsafe" 5 | ) 6 | 7 | //go:linkname HostProc github.com/shirou/gopsutil/v3/internal/common.HostProc 8 | func HostProc(combineWith ...string) string 9 | -------------------------------------------------------------------------------- /util/sys/sys_darwin.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | 3 | package sys 4 | 5 | import ( 6 | "github.com/shirou/gopsutil/v3/net" 7 | ) 8 | 9 | func GetTCPCount() (int, error) { 10 | stats, err := net.Connections("tcp") 11 | if err != nil { 12 | return 0, err 13 | } 14 | return len(stats), nil 15 | } 16 | 17 | func GetUDPCount() (int, error) { 18 | stats, err := net.Connections("udp") 19 | if err != nil { 20 | return 0, err 21 | } 22 | return len(stats), nil 23 | } 24 | -------------------------------------------------------------------------------- /util/sys/sys_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package sys 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "os" 10 | ) 11 | 12 | func getLinesNum(filename string) (int, error) { 13 | file, err := os.Open(filename) 14 | if err != nil { 15 | return 0, err 16 | } 17 | defer file.Close() 18 | 19 | sum := 0 20 | buf := make([]byte, 8192) 21 | for { 22 | n, err := file.Read(buf) 23 | 24 | var buffPosition int 25 | for { 26 | i := bytes.IndexByte(buf[buffPosition:], '\n') 27 | if i < 0 || n == buffPosition { 28 | break 29 | } 30 | buffPosition += i + 1 31 | sum++ 32 | } 33 | 34 | if err == io.EOF { 35 | return sum, nil 36 | } else if err != nil { 37 | return sum, err 38 | } 39 | } 40 | } 41 | 42 | func GetTCPCount() (int, error) { 43 | root := HostProc() 44 | 45 | tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root)) 46 | if err != nil { 47 | return tcp4, err 48 | } 49 | tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root)) 50 | if err != nil { 51 | return tcp4 + tcp6, nil 52 | } 53 | 54 | return tcp4 + tcp6, nil 55 | } 56 | 57 | func GetUDPCount() (int, error) { 58 | root := HostProc() 59 | 60 | udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root)) 61 | if err != nil { 62 | return udp4, err 63 | } 64 | udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root)) 65 | if err != nil { 66 | return udp4 + udp6, nil 67 | } 68 | 69 | return udp4 + udp6, nil 70 | } 71 | -------------------------------------------------------------------------------- /v2ui/db.go: -------------------------------------------------------------------------------- 1 | package v2ui 2 | 3 | import ( 4 | "gorm.io/driver/sqlite" 5 | "gorm.io/gorm" 6 | "gorm.io/gorm/logger" 7 | ) 8 | 9 | var v2db *gorm.DB 10 | 11 | func initDB(dbPath string) error { 12 | c := &gorm.Config{ 13 | Logger: logger.Discard, 14 | } 15 | var err error 16 | v2db, err = gorm.Open(sqlite.Open(dbPath), c) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return nil 22 | } 23 | 24 | func getV2Inbounds() ([]*V2Inbound, error) { 25 | inbounds := make([]*V2Inbound, 0) 26 | err := v2db.Model(V2Inbound{}).Find(&inbounds).Error 27 | return inbounds, err 28 | } 29 | -------------------------------------------------------------------------------- /v2ui/models.go: -------------------------------------------------------------------------------- 1 | package v2ui 2 | 3 | import "xray-ui/database/model" 4 | 5 | type V2Inbound struct { 6 | Id int `gorm:"primaryKey;autoIncrement"` 7 | Port int `gorm:"unique"` 8 | Listen string 9 | Protocol string 10 | Settings string 11 | StreamSettings string 12 | Tag string `gorm:"unique"` 13 | Sniffing string 14 | Remark string 15 | Up int64 16 | Down int64 17 | Enable bool 18 | } 19 | 20 | func (i *V2Inbound) TableName() string { 21 | return "inbound" 22 | } 23 | 24 | func (i *V2Inbound) ToInbound(userId int) *model.Inbound { 25 | return &model.Inbound{ 26 | UserId: userId, 27 | Up: i.Up, 28 | Down: i.Down, 29 | Total: 0, 30 | Remark: i.Remark, 31 | Enable: i.Enable, 32 | ExpiryTime: 0, 33 | Listen: i.Listen, 34 | Port: i.Port, 35 | Protocol: model.Protocol(i.Protocol), 36 | Settings: i.Settings, 37 | StreamSettings: i.StreamSettings, 38 | Tag: i.Tag, 39 | Sniffing: i.Sniffing, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /v2ui/v2ui.go: -------------------------------------------------------------------------------- 1 | package v2ui 2 | 3 | import ( 4 | "fmt" 5 | "xray-ui/config" 6 | "xray-ui/database" 7 | "xray-ui/database/model" 8 | "xray-ui/util/common" 9 | "xray-ui/web/service" 10 | ) 11 | 12 | func MigrateFromV2UI(dbPath string) error { 13 | err := initDB(dbPath) 14 | if err != nil { 15 | return common.NewError("init v2-ui database failed:", err) 16 | } 17 | err = database.InitDB(config.GetDBPath()) 18 | if err != nil { 19 | return common.NewError("init xray-ui database failed:", err) 20 | } 21 | 22 | v2Inbounds, err := getV2Inbounds() 23 | if err != nil { 24 | return common.NewError("get v2-ui inbounds failed:", err) 25 | } 26 | if len(v2Inbounds) == 0 { 27 | fmt.Println("migrate v2-ui inbounds success: 0") 28 | return nil 29 | } 30 | 31 | userService := service.UserService{} 32 | user, err := userService.GetFirstUser() 33 | if err != nil { 34 | return common.NewError("get xray-ui user failed:", err) 35 | } 36 | 37 | inbounds := make([]*model.Inbound, 0) 38 | for _, v2inbound := range v2Inbounds { 39 | inbounds = append(inbounds, v2inbound.ToInbound(user.Id)) 40 | } 41 | 42 | inboundService := service.InboundService{} 43 | err = inboundService.AddInbounds(inbounds) 44 | if err != nil { 45 | return common.NewError("add xray-ui inbounds failed:", err) 46 | } 47 | 48 | fmt.Println("migrate v2-ui inbounds success:", len(inbounds)) 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /web/assets/ant-design-vue@1.7.2/antd.less: -------------------------------------------------------------------------------- 1 | @import "../lib/style/index.less"; 2 | @import "../lib/style/components.less"; -------------------------------------------------------------------------------- /web/assets/base64/base64.min.js: -------------------------------------------------------------------------------- 1 | (function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory(global):typeof define==="function"&&define.amd?define(factory):factory(global)})(typeof self!=="undefined"?self:typeof window!=="undefined"?window:typeof global!=="undefined"?global:this,function(global){"use strict";var _Base64=global.Base64;var version="2.5.0";var buffer;if(typeof module!=="undefined"&&module.exports){try{buffer=eval("require('buffer').Buffer")}catch(err){buffer=undefined}}var b64chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var b64tab=function(bin){var t={};for(var i=0,l=bin.length;i>>6)+fromCharCode(128|cc&63):fromCharCode(224|cc>>>12&15)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}else{var cc=65536+(c.charCodeAt(0)-55296)*1024+(c.charCodeAt(1)-56320);return fromCharCode(240|cc>>>18&7)+fromCharCode(128|cc>>>12&63)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}};var re_utob=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;var utob=function(u){return u.replace(re_utob,cb_utob)};var cb_encode=function(ccc){var padlen=[0,2,1][ccc.length%3],ord=ccc.charCodeAt(0)<<16|(ccc.length>1?ccc.charCodeAt(1):0)<<8|(ccc.length>2?ccc.charCodeAt(2):0),chars=[b64chars.charAt(ord>>>18),b64chars.charAt(ord>>>12&63),padlen>=2?"=":b64chars.charAt(ord>>>6&63),padlen>=1?"=":b64chars.charAt(ord&63)];return chars.join("")};var btoa=global.btoa?function(b){return global.btoa(b)}:function(b){return b.replace(/[\s\S]{1,3}/g,cb_encode)};var _encode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(u){return(u.constructor===buffer.constructor?u:buffer.from(u)).toString("base64")}:function(u){return(u.constructor===buffer.constructor?u:new buffer(u)).toString("base64")}:function(u){return btoa(utob(u))};var encode=function(u,urisafe){return!urisafe?_encode(String(u)):_encode(String(u)).replace(/[+\/]/g,function(m0){return m0=="+"?"-":"_"}).replace(/=/g,"")};var encodeURI=function(u){return encode(u,true)};var re_btou=new RegExp(["[À-ß][€-¿]","[à-ï][€-¿]{2}","[ð-÷][€-¿]{3}"].join("|"),"g");var cb_btou=function(cccc){switch(cccc.length){case 4:var cp=(7&cccc.charCodeAt(0))<<18|(63&cccc.charCodeAt(1))<<12|(63&cccc.charCodeAt(2))<<6|63&cccc.charCodeAt(3),offset=cp-65536;return fromCharCode((offset>>>10)+55296)+fromCharCode((offset&1023)+56320);case 3:return fromCharCode((15&cccc.charCodeAt(0))<<12|(63&cccc.charCodeAt(1))<<6|63&cccc.charCodeAt(2));default:return fromCharCode((31&cccc.charCodeAt(0))<<6|63&cccc.charCodeAt(1))}};var btou=function(b){return b.replace(re_btou,cb_btou)};var cb_decode=function(cccc){var len=cccc.length,padlen=len%4,n=(len>0?b64tab[cccc.charAt(0)]<<18:0)|(len>1?b64tab[cccc.charAt(1)]<<12:0)|(len>2?b64tab[cccc.charAt(2)]<<6:0)|(len>3?b64tab[cccc.charAt(3)]:0),chars=[fromCharCode(n>>>16),fromCharCode(n>>>8&255),fromCharCode(n&255)];chars.length-=[0,0,2,1][padlen];return chars.join("")};var _atob=global.atob?function(a){return global.atob(a)}:function(a){return a.replace(/\S{1,4}/g,cb_decode)};var atob=function(a){return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g,""))};var _decode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(a){return(a.constructor===buffer.constructor?a:buffer.from(a,"base64")).toString()}:function(a){return(a.constructor===buffer.constructor?a:new buffer(a,"base64")).toString()}:function(a){return btou(_atob(a))};var decode=function(a){return _decode(String(a).replace(/[-_]/g,function(m0){return m0=="-"?"+":"/"}).replace(/[^A-Za-z0-9\+\/]/g,""))};var noConflict=function(){var Base64=global.Base64;global.Base64=_Base64;return Base64};global.Base64={VERSION:version,atob:atob,btoa:btoa,fromBase64:decode,toBase64:encode,utob:utob,encode:encode,encodeURI:encodeURI,btou:btou,decode:decode,noConflict:noConflict,__buffer__:buffer};if(typeof Object.defineProperty==="function"){var noEnum=function(v){return{value:v,enumerable:false,writable:true,configurable:true}};global.Base64.extendString=function(){Object.defineProperty(String.prototype,"fromBase64",noEnum(function(){return decode(this)}));Object.defineProperty(String.prototype,"toBase64",noEnum(function(urisafe){return encode(this,urisafe)}));Object.defineProperty(String.prototype,"toBase64URI",noEnum(function(){return encode(this,true)}))}}if(global["Meteor"]){Base64=global.Base64}if(typeof module!=="undefined"&&module.exports){module.exports.Base64=global.Base64}else if(typeof define==="function"&&define.amd){define([],function(){return global.Base64})}return{Base64:global.Base64}}); -------------------------------------------------------------------------------- /web/assets/element-ui@2.15.0/theme-chalk/display.css: -------------------------------------------------------------------------------- 1 | @media only screen and (max-width:767px){.hidden-xs-only{display:none!important}}@media only screen and (min-width:768px){.hidden-sm-and-up{display:none!important}}@media only screen and (min-width:768px) and (max-width:991px){.hidden-sm-only{display:none!important}}@media only screen and (max-width:991px){.hidden-sm-and-down{display:none!important}}@media only screen and (min-width:992px){.hidden-md-and-up{display:none!important}}@media only screen and (min-width:992px) and (max-width:1199px){.hidden-md-only{display:none!important}}@media only screen and (max-width:1199px){.hidden-md-and-down{display:none!important}}@media only screen and (min-width:1200px){.hidden-lg-and-up{display:none!important}}@media only screen and (min-width:1200px) and (max-width:1919px){.hidden-lg-only{display:none!important}}@media only screen and (max-width:1919px){.hidden-lg-and-down{display:none!important}}@media only screen and (min-width:1920px){.hidden-xl-only{display:none!important}} -------------------------------------------------------------------------------- /web/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qist/xray-ui/a4fb9c51b86a88b2699045c5c6122ae72ae91c54/web/assets/favicon.ico -------------------------------------------------------------------------------- /web/assets/js/axios-init.js: -------------------------------------------------------------------------------- 1 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; 2 | axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 3 | 4 | axios.interceptors.request.use( 5 | (config) => { 6 | if (config.data instanceof FormData) { 7 | config.headers['Content-Type'] = 'multipart/form-data'; 8 | } else { 9 | config.data = Qs.stringify(config.data, { 10 | arrayFormat: 'repeat', 11 | }); 12 | } 13 | return config; 14 | }, 15 | (error) => Promise.reject(error), 16 | ); -------------------------------------------------------------------------------- /web/assets/js/model/models.js: -------------------------------------------------------------------------------- 1 | class User { 2 | 3 | constructor() { 4 | this.username = ""; 5 | this.password = ""; 6 | } 7 | } 8 | 9 | class Msg { 10 | 11 | constructor(success, msg, obj) { 12 | this.success = false; 13 | this.msg = ""; 14 | this.obj = null; 15 | 16 | if (success != null) { 17 | this.success = success; 18 | } 19 | if (msg != null) { 20 | this.msg = msg; 21 | } 22 | if (obj != null) { 23 | this.obj = obj; 24 | } 25 | } 26 | } 27 | 28 | class DBInbound { 29 | 30 | constructor(data) { 31 | this.id = 0; 32 | this.userId = 0; 33 | this.up = 0; 34 | this.down = 0; 35 | this.total = 0; 36 | this.remark = ""; 37 | this.enable = true; 38 | this.expiryTime = 0; 39 | 40 | this.listen = ""; 41 | this.port = 0; 42 | this.protocol = ""; 43 | this.settings = ""; 44 | this.streamSettings = ""; 45 | this.tag = ""; 46 | this.sniffing = ""; 47 | 48 | if (data == null) { 49 | return; 50 | } 51 | ObjectUtil.cloneProps(this, data); 52 | } 53 | 54 | get totalGB() { 55 | return toFixed(this.total / ONE_GB, 2); 56 | } 57 | 58 | set totalGB(gb) { 59 | this.total = toFixed(gb * ONE_GB, 0); 60 | } 61 | 62 | get isVMess() { 63 | return this.protocol === Protocols.VMESS; 64 | } 65 | 66 | get isVLess() { 67 | return this.protocol === Protocols.VLESS; 68 | } 69 | 70 | get isTrojan() { 71 | return this.protocol === Protocols.TROJAN; 72 | } 73 | 74 | get isSS() { 75 | return this.protocol === Protocols.SHADOWSOCKS; 76 | } 77 | 78 | get isSocks() { 79 | return this.protocol === Protocols.SOCKS; 80 | } 81 | 82 | get isHTTP() { 83 | return this.protocol === Protocols.HTTP; 84 | } 85 | 86 | get address() { 87 | let address = location.hostname; 88 | if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") { 89 | address = this.listen; 90 | } 91 | return address; 92 | } 93 | 94 | get _expiryTime() { 95 | if (this.expiryTime === 0) { 96 | return null; 97 | } 98 | return moment(this.expiryTime); 99 | } 100 | 101 | set _expiryTime(t) { 102 | if (t == null) { 103 | this.expiryTime = 0; 104 | } else { 105 | this.expiryTime = t.valueOf(); 106 | } 107 | } 108 | 109 | get isExpiry() { 110 | return this.expiryTime < new Date().getTime(); 111 | } 112 | 113 | toInbound() { 114 | let settings = {}; 115 | if (!ObjectUtil.isEmpty(this.settings)) { 116 | settings = JSON.parse(this.settings); 117 | } 118 | 119 | let streamSettings = {}; 120 | if (!ObjectUtil.isEmpty(this.streamSettings)) { 121 | streamSettings = JSON.parse(this.streamSettings); 122 | } 123 | 124 | let sniffing = {}; 125 | if (!ObjectUtil.isEmpty(this.sniffing)) { 126 | sniffing = JSON.parse(this.sniffing); 127 | } 128 | const config = { 129 | port: this.port, 130 | listen: this.listen, 131 | protocol: this.protocol, 132 | settings: settings, 133 | streamSettings: streamSettings, 134 | tag: this.tag, 135 | sniffing: sniffing, 136 | }; 137 | return Inbound.fromJson(config); 138 | } 139 | 140 | hasLink() { 141 | switch (this.protocol) { 142 | case Protocols.VMESS: 143 | case Protocols.VLESS: 144 | case Protocols.TROJAN: 145 | case Protocols.SHADOWSOCKS: 146 | return true; 147 | default: 148 | return false; 149 | } 150 | } 151 | 152 | genLink() { 153 | const inbound = this.toInbound(); 154 | return inbound.genLink(this.address, this.remark); 155 | } 156 | } 157 | 158 | class AllSetting { 159 | 160 | constructor(data) { 161 | this.webListen = ""; 162 | this.webPort = 54321; 163 | this.webCertFile = ""; 164 | this.webKeyFile = ""; 165 | this.webCAFile = ""; 166 | this.webBasePath = "/"; 167 | this.tgBotEnable = false; 168 | this.tgBotToken = ""; 169 | this.tgBotChatId = 0; 170 | this.tgRunTime = ""; 171 | this.xrayTemplateConfig = ""; 172 | 173 | this.timeLocation = "Asia/Shanghai"; 174 | 175 | if (data == null) { 176 | return 177 | } 178 | ObjectUtil.cloneProps(this, data); 179 | } 180 | 181 | equals(other) { 182 | return ObjectUtil.equals(this, other); 183 | } 184 | } -------------------------------------------------------------------------------- /web/assets/js/util/common.js: -------------------------------------------------------------------------------- 1 | const ONE_KB = 1024; 2 | const ONE_MB = ONE_KB * 1024; 3 | const ONE_GB = ONE_MB * 1024; 4 | const ONE_TB = ONE_GB * 1024; 5 | const ONE_PB = ONE_TB * 1024; 6 | 7 | function sizeFormat(size) { 8 | if (size < ONE_KB) { 9 | return size.toFixed(0) + " B"; 10 | } else if (size < ONE_MB) { 11 | return (size / ONE_KB).toFixed(2) + " KB"; 12 | } else if (size < ONE_GB) { 13 | return (size / ONE_MB).toFixed(2) + " MB"; 14 | } else if (size < ONE_TB) { 15 | return (size / ONE_GB).toFixed(2) + " GB"; 16 | } else if (size < ONE_PB) { 17 | return (size / ONE_TB).toFixed(2) + " TB"; 18 | } else { 19 | return (size / ONE_PB).toFixed(2) + " PB"; 20 | } 21 | } 22 | 23 | function base64(str) { 24 | return Base64.encode(str); 25 | } 26 | 27 | function safeBase64(str) { 28 | return base64(str) 29 | .replace(/\+/g, '-') 30 | .replace(/=/g, '') 31 | .replace(/\//g, '_'); 32 | } 33 | 34 | function formatSecond(second) { 35 | if (second < 60) { 36 | return second.toFixed(0) + ' 秒'; 37 | } else if (second < 3600) { 38 | return (second / 60).toFixed(0) + ' 分钟'; 39 | } else if (second < 3600 * 24) { 40 | return (second / 3600).toFixed(0) + ' 小时'; 41 | } else { 42 | return (second / 3600 / 24).toFixed(0) + ' 天'; 43 | } 44 | } 45 | 46 | function addZero(num) { 47 | if (num < 10) { 48 | return "0" + num; 49 | } else { 50 | return num; 51 | } 52 | } 53 | 54 | function toFixed(num, n) { 55 | n = Math.pow(10, n); 56 | return Math.round(num * n) / n; 57 | } -------------------------------------------------------------------------------- /web/assets/js/util/date-util.js: -------------------------------------------------------------------------------- 1 | const oneMinute = 1000 * 60; // 一分钟的毫秒数 2 | const oneHour = oneMinute * 60; // 一小时的毫秒数 3 | const oneDay = oneHour * 24; // 一天的毫秒数 4 | const oneWeek = oneDay * 7; // 一星期的毫秒数 5 | const oneMonth = oneDay * 30; // 一个月的毫秒数 6 | 7 | /** 8 | * 按天数减少 9 | * 10 | * @param days 要减少的天数 11 | */ 12 | Date.prototype.minusDays = function (days) { 13 | return this.minusMillis(oneDay * days); 14 | }; 15 | 16 | /** 17 | * 按天数增加 18 | * 19 | * @param days 要增加的天数 20 | */ 21 | Date.prototype.plusDays = function (days) { 22 | return this.plusMillis(oneDay * days); 23 | }; 24 | 25 | /** 26 | * 按小时减少 27 | * 28 | * @param hours 要减少的小时数 29 | */ 30 | Date.prototype.minusHours = function (hours) { 31 | return this.minusMillis(oneHour * hours); 32 | }; 33 | 34 | /** 35 | * 按小时增加 36 | * 37 | * @param hours 要增加的小时数 38 | */ 39 | Date.prototype.plusHours = function (hours) { 40 | return this.plusMillis(oneHour * hours); 41 | }; 42 | 43 | /** 44 | * 按分钟减少 45 | * 46 | * @param minutes 要减少的分钟数 47 | */ 48 | Date.prototype.minusMinutes = function (minutes) { 49 | return this.minusMillis(oneMinute * minutes); 50 | }; 51 | 52 | /** 53 | * 按分钟增加 54 | * 55 | * @param minutes 要增加的分钟数 56 | */ 57 | Date.prototype.plusMinutes = function (minutes) { 58 | return this.plusMillis(oneMinute * minutes); 59 | }; 60 | 61 | /** 62 | * 按毫秒减少 63 | * 64 | * @param millis 要减少的毫秒数 65 | */ 66 | Date.prototype.minusMillis = function(millis) { 67 | let time = this.getTime() - millis; 68 | let newDate = new Date(); 69 | newDate.setTime(time); 70 | return newDate; 71 | }; 72 | 73 | /** 74 | * 按毫秒增加 75 | * 76 | * @param millis 要增加的毫秒数 77 | */ 78 | Date.prototype.plusMillis = function(millis) { 79 | let time = this.getTime() + millis; 80 | let newDate = new Date(); 81 | newDate.setTime(time); 82 | return newDate; 83 | }; 84 | 85 | /** 86 | * 设置时间为当天的 00:00:00.000 87 | */ 88 | Date.prototype.setMinTime = function () { 89 | this.setHours(0); 90 | this.setMinutes(0); 91 | this.setSeconds(0); 92 | this.setMilliseconds(0); 93 | return this; 94 | }; 95 | 96 | /** 97 | * 设置时间为当天的 23:59:59.999 98 | */ 99 | Date.prototype.setMaxTime = function () { 100 | this.setHours(23); 101 | this.setMinutes(59); 102 | this.setSeconds(59); 103 | this.setMilliseconds(999); 104 | return this; 105 | }; 106 | 107 | /** 108 | * 格式化日期 109 | */ 110 | Date.prototype.formatDate = function () { 111 | return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate()); 112 | }; 113 | 114 | /** 115 | * 格式化时间 116 | */ 117 | Date.prototype.formatTime = function () { 118 | return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds()); 119 | }; 120 | 121 | /** 122 | * 格式化日期加时间 123 | * 124 | * @param split 日期和时间之间的分隔符,默认是一个空格 125 | */ 126 | Date.prototype.formatDateTime = function (split = ' ') { 127 | return this.formatDate() + split + this.formatTime(); 128 | }; 129 | 130 | class DateUtil { 131 | 132 | // 字符串转 Date 对象 133 | static parseDate(str) { 134 | return new Date(str.replace(/-/g, '/')); 135 | } 136 | 137 | static formatMillis(millis) { 138 | return moment(millis).format('YYYY-M-D H:m:s') 139 | } 140 | 141 | static firstDayOfMonth() { 142 | const date = new Date(); 143 | date.setDate(1); 144 | date.setMinTime(); 145 | return date; 146 | } 147 | } -------------------------------------------------------------------------------- /web/assets/qs/qs.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).Qs=e()}}(function(){return function i(a,l,c){function f(r,e){if(!l[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(s)return s(r,!0);var o=new Error("Cannot find module '"+r+"'");throw o.code="MODULE_NOT_FOUND",o}var n=l[r]={exports:{}};a[r][0].call(n.exports,function(e){return f(a[r][1][e]||e)},n,n.exports,i,a,l,c)}return l[r].exports}for(var s="function"==typeof require&&require,e=0;e>6]+i[128|63&n]:n<55296||57344<=n?t+=i[224|n>>12]+i[128|n>>6&63]+i[128|63&n]:(o+=1,n=65536+((1023&n)<<10|1023&r.charCodeAt(o)),t+=i[240|n>>18]+i[128|n>>12&63]+i[128|n>>6&63]+i[128|63&n])}return t},isBuffer:function(e){return null!=e&&!!(e.constructor&&e.constructor.isBuffer&&e.constructor.isBuffer(e))},isRegExp:function(e){return"[object RegExp]"===Object.prototype.toString.call(e)},merge:function o(t,n,i){if(!n)return t;if("object"!=typeof n){if(Array.isArray(t))t.push(n);else{if("object"!=typeof t)return[t,n];(i.plainObjects||i.allowPrototypes||!a.call(Object.prototype,n))&&(t[n]=!0)}return t}if("object"!=typeof t)return[t].concat(n);var e=t;return Array.isArray(t)&&!Array.isArray(n)&&(e=l(t,i)),Array.isArray(t)&&Array.isArray(n)?(n.forEach(function(e,r){a.call(t,r)?t[r]&&"object"==typeof t[r]?t[r]=o(t[r],e,i):t.push(e):t[r]=e}),t):Object.keys(n).reduce(function(e,r){var t=n[r];return a.call(e,r)?e[r]=o(e[r],t,i):e[r]=t,e},e)}}},{}]},{},[2])(2)}); -------------------------------------------------------------------------------- /web/assets/vue@2.6.12/vue.common.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./vue.common.prod.js') 3 | } else { 4 | module.exports = require('./vue.common.dev.js') 5 | } 6 | -------------------------------------------------------------------------------- /web/assets/vue@2.6.12/vue.runtime.common.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./vue.runtime.common.prod.js') 3 | } else { 4 | module.exports = require('./vue.runtime.common.dev.js') 5 | } 6 | -------------------------------------------------------------------------------- /web/controller/base.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | "xray-ui/web/session" 7 | ) 8 | 9 | type BaseController struct { 10 | } 11 | 12 | func (a *BaseController) checkLogin(c *gin.Context) { 13 | if !session.IsLogin(c) { 14 | if isAjax(c) { 15 | pureJsonMsg(c, false, "登录时效已过,请重新登录") 16 | } else { 17 | c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) 18 | } 19 | c.Abort() 20 | } else { 21 | c.Next() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/controller/inbound.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "strconv" 7 | "xray-ui/database/model" 8 | "xray-ui/logger" 9 | "xray-ui/web/global" 10 | "xray-ui/web/service" 11 | "xray-ui/web/session" 12 | ) 13 | 14 | type InboundController struct { 15 | inboundService service.InboundService 16 | xrayService service.XrayService 17 | } 18 | 19 | func NewInboundController(g *gin.RouterGroup) *InboundController { 20 | a := &InboundController{} 21 | a.initRouter(g) 22 | a.startTask() 23 | return a 24 | } 25 | 26 | func (a *InboundController) initRouter(g *gin.RouterGroup) { 27 | g = g.Group("/inbound") 28 | 29 | g.POST("/list", a.getInbounds) 30 | g.POST("/add", a.addInbound) 31 | g.POST("/del/:id", a.delInbound) 32 | g.POST("/update/:id", a.updateInbound) 33 | } 34 | 35 | func (a *InboundController) startTask() { 36 | webServer := global.GetWebServer() 37 | c := webServer.GetCron() 38 | c.AddFunc("@every 10s", func() { 39 | if a.xrayService.IsNeedRestartAndSetFalse() { 40 | err := a.xrayService.RestartXray(false) 41 | if err != nil { 42 | logger.Error("restart xray failed:", err) 43 | } 44 | } 45 | }) 46 | } 47 | 48 | func (a *InboundController) getInbounds(c *gin.Context) { 49 | user := session.GetLoginUser(c) 50 | inbounds, err := a.inboundService.GetInbounds(user.Id) 51 | if err != nil { 52 | jsonMsg(c, "获取", err) 53 | return 54 | } 55 | jsonObj(c, inbounds, nil) 56 | } 57 | 58 | func (a *InboundController) addInbound(c *gin.Context) { 59 | inbound := &model.Inbound{} 60 | err := c.ShouldBind(inbound) 61 | if err != nil { 62 | jsonMsg(c, "添加", err) 63 | return 64 | } 65 | user := session.GetLoginUser(c) 66 | inbound.UserId = user.Id 67 | inbound.Enable = true 68 | inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) 69 | err = a.inboundService.AddInbound(inbound) 70 | jsonMsg(c, "添加", err) 71 | if err == nil { 72 | a.xrayService.SetToNeedRestart() 73 | } 74 | } 75 | 76 | func (a *InboundController) delInbound(c *gin.Context) { 77 | id, err := strconv.Atoi(c.Param("id")) 78 | if err != nil { 79 | jsonMsg(c, "删除", err) 80 | return 81 | } 82 | err = a.inboundService.DelInbound(id) 83 | jsonMsg(c, "删除", err) 84 | if err == nil { 85 | a.xrayService.SetToNeedRestart() 86 | } 87 | } 88 | 89 | func (a *InboundController) updateInbound(c *gin.Context) { 90 | id, err := strconv.Atoi(c.Param("id")) 91 | if err != nil { 92 | jsonMsg(c, "修改", err) 93 | return 94 | } 95 | inbound := &model.Inbound{ 96 | Id: id, 97 | } 98 | err = c.ShouldBind(inbound) 99 | if err != nil { 100 | jsonMsg(c, "修改", err) 101 | return 102 | } 103 | err = a.inboundService.UpdateInbound(inbound) 104 | jsonMsg(c, "修改", err) 105 | if err == nil { 106 | a.xrayService.SetToNeedRestart() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /web/controller/index.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | "xray-ui/logger" 7 | "xray-ui/web/job" 8 | "xray-ui/web/service" 9 | "xray-ui/web/session" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type LoginForm struct { 15 | Username string `json:"username" form:"username"` 16 | Password string `json:"password" form:"password"` 17 | } 18 | 19 | type IndexController struct { 20 | BaseController 21 | 22 | userService service.UserService 23 | } 24 | 25 | func NewIndexController(g *gin.RouterGroup) *IndexController { 26 | a := &IndexController{} 27 | a.initRouter(g) 28 | return a 29 | } 30 | 31 | func (a *IndexController) initRouter(g *gin.RouterGroup) { 32 | g.GET("/", a.index) 33 | g.POST("/login", a.login) 34 | g.GET("/logout", a.logout) 35 | } 36 | 37 | func (a *IndexController) index(c *gin.Context) { 38 | if session.IsLogin(c) { 39 | c.Redirect(http.StatusTemporaryRedirect, "xui/") 40 | return 41 | } 42 | html(c, "login.html", "登录", nil) 43 | } 44 | 45 | func (a *IndexController) login(c *gin.Context) { 46 | var form LoginForm 47 | err := c.ShouldBind(&form) 48 | if err != nil { 49 | pureJsonMsg(c, false, "数据格式错误") 50 | return 51 | } 52 | if form.Username == "" { 53 | pureJsonMsg(c, false, "请输入用户名") 54 | return 55 | } 56 | if form.Password == "" { 57 | pureJsonMsg(c, false, "请输入密码") 58 | return 59 | } 60 | user := a.userService.CheckUser(form.Username, form.Password) 61 | timeStr := time.Now().Format("2006-01-02 15:04:05") 62 | if user == nil { 63 | job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0) 64 | logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password) 65 | pureJsonMsg(c, false, "用户名或密码错误") 66 | return 67 | } else { 68 | logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c)) 69 | job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1) 70 | } 71 | 72 | err = session.SetLoginUser(c, user) 73 | logger.Info("user", user.Id, "login success") 74 | jsonMsg(c, "登录", err) 75 | } 76 | 77 | func (a *IndexController) logout(c *gin.Context) { 78 | user := session.GetLoginUser(c) 79 | if user != nil { 80 | logger.Info("user", user.Id, "logout") 81 | } 82 | session.ClearSession(c) 83 | c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) 84 | } 85 | -------------------------------------------------------------------------------- /web/controller/setting.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | "github.com/gin-gonic/gin" 6 | "time" 7 | "xray-ui/web/entity" 8 | "xray-ui/web/service" 9 | "xray-ui/web/session" 10 | ) 11 | 12 | type updateUserForm struct { 13 | OldUsername string `json:"oldUsername" form:"oldUsername"` 14 | OldPassword string `json:"oldPassword" form:"oldPassword"` 15 | NewUsername string `json:"newUsername" form:"newUsername"` 16 | NewPassword string `json:"newPassword" form:"newPassword"` 17 | } 18 | 19 | type SettingController struct { 20 | settingService service.SettingService 21 | userService service.UserService 22 | panelService service.PanelService 23 | } 24 | 25 | func NewSettingController(g *gin.RouterGroup) *SettingController { 26 | a := &SettingController{} 27 | a.initRouter(g) 28 | return a 29 | } 30 | 31 | func (a *SettingController) initRouter(g *gin.RouterGroup) { 32 | g = g.Group("/setting") 33 | 34 | g.POST("/all", a.getAllSetting) 35 | g.POST("/update", a.updateSetting) 36 | g.POST("/updateUser", a.updateUser) 37 | g.POST("/restartPanel", a.restartPanel) 38 | } 39 | 40 | func (a *SettingController) getAllSetting(c *gin.Context) { 41 | allSetting, err := a.settingService.GetAllSetting() 42 | if err != nil { 43 | jsonMsg(c, "获取设置", err) 44 | return 45 | } 46 | jsonObj(c, allSetting, nil) 47 | } 48 | 49 | func (a *SettingController) updateSetting(c *gin.Context) { 50 | allSetting := &entity.AllSetting{} 51 | err := c.ShouldBind(allSetting) 52 | if err != nil { 53 | jsonMsg(c, "修改设置", err) 54 | return 55 | } 56 | err = a.settingService.UpdateAllSetting(allSetting) 57 | jsonMsg(c, "修改设置", err) 58 | } 59 | 60 | func (a *SettingController) updateUser(c *gin.Context) { 61 | form := &updateUserForm{} 62 | err := c.ShouldBind(form) 63 | if err != nil { 64 | jsonMsg(c, "修改用户", err) 65 | return 66 | } 67 | user := session.GetLoginUser(c) 68 | if user.Username != form.OldUsername || user.Password != form.OldPassword { 69 | jsonMsg(c, "修改用户", errors.New("原用户名或原密码错误")) 70 | return 71 | } 72 | if form.NewUsername == "" || form.NewPassword == "" { 73 | jsonMsg(c, "修改用户", errors.New("新用户名和新密码不能为空")) 74 | return 75 | } 76 | err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword) 77 | if err == nil { 78 | user.Username = form.NewUsername 79 | user.Password = form.NewPassword 80 | session.SetLoginUser(c, user) 81 | } 82 | jsonMsg(c, "修改用户", err) 83 | } 84 | 85 | func (a *SettingController) restartPanel(c *gin.Context) { 86 | err := a.panelService.RestartPanel(time.Second * 3) 87 | jsonMsg(c, "重启面板", err) 88 | } 89 | -------------------------------------------------------------------------------- /web/controller/util.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net" 6 | "net/http" 7 | "strings" 8 | "xray-ui/config" 9 | "xray-ui/logger" 10 | "xray-ui/web/entity" 11 | ) 12 | 13 | func getUriId(c *gin.Context) int64 { 14 | s := struct { 15 | Id int64 `uri:"id"` 16 | }{} 17 | 18 | _ = c.BindUri(&s) 19 | return s.Id 20 | } 21 | 22 | func getRemoteIp(c *gin.Context) string { 23 | value := c.GetHeader("X-Forwarded-For") 24 | if value != "" { 25 | ips := strings.Split(value, ",") 26 | return ips[0] 27 | } else { 28 | addr := c.Request.RemoteAddr 29 | ip, _, _ := net.SplitHostPort(addr) 30 | return ip 31 | } 32 | } 33 | 34 | func jsonMsg(c *gin.Context, msg string, err error) { 35 | jsonMsgObj(c, msg, nil, err) 36 | } 37 | 38 | func jsonObj(c *gin.Context, obj interface{}, err error) { 39 | jsonMsgObj(c, "", obj, err) 40 | } 41 | 42 | func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) { 43 | m := entity.Msg{ 44 | Obj: obj, 45 | } 46 | if err == nil { 47 | m.Success = true 48 | if msg != "" { 49 | m.Msg = msg + "成功" 50 | } 51 | } else { 52 | m.Success = false 53 | m.Msg = msg + "失败: " + err.Error() 54 | logger.Warning(msg+"失败: ", err) 55 | } 56 | c.JSON(http.StatusOK, m) 57 | } 58 | 59 | func pureJsonMsg(c *gin.Context, success bool, msg string) { 60 | if success { 61 | c.JSON(http.StatusOK, entity.Msg{ 62 | Success: true, 63 | Msg: msg, 64 | }) 65 | } else { 66 | c.JSON(http.StatusOK, entity.Msg{ 67 | Success: false, 68 | Msg: msg, 69 | }) 70 | } 71 | } 72 | 73 | func html(c *gin.Context, name string, title string, data gin.H) { 74 | if data == nil { 75 | data = gin.H{} 76 | } 77 | data["title"] = title 78 | data["request_uri"] = c.Request.RequestURI 79 | data["base_path"] = c.GetString("base_path") 80 | c.HTML(http.StatusOK, name, getContext(data)) 81 | } 82 | 83 | func getContext(h gin.H) gin.H { 84 | a := gin.H{ 85 | "cur_ver": config.GetVersion(), 86 | } 87 | if h != nil { 88 | for key, value := range h { 89 | a[key] = value 90 | } 91 | } 92 | return a 93 | } 94 | 95 | func isAjax(c *gin.Context) bool { 96 | return c.GetHeader("X-Requested-With") == "XMLHttpRequest" 97 | } 98 | -------------------------------------------------------------------------------- /web/controller/xui.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | type XUIController struct { 8 | BaseController 9 | 10 | inboundController *InboundController 11 | settingController *SettingController 12 | } 13 | 14 | func NewXUIController(g *gin.RouterGroup) *XUIController { 15 | a := &XUIController{} 16 | a.initRouter(g) 17 | return a 18 | } 19 | 20 | func (a *XUIController) initRouter(g *gin.RouterGroup) { 21 | g = g.Group("/xui") 22 | g.Use(a.checkLogin) 23 | 24 | g.GET("/", a.index) 25 | g.GET("/inbounds", a.inbounds) 26 | g.GET("/setting", a.setting) 27 | 28 | a.inboundController = NewInboundController(g) 29 | a.settingController = NewSettingController(g) 30 | } 31 | 32 | func (a *XUIController) index(c *gin.Context) { 33 | html(c, "index.html", "系统状态", nil) 34 | } 35 | 36 | func (a *XUIController) inbounds(c *gin.Context) { 37 | html(c, "inbounds.html", "入站列表", nil) 38 | } 39 | 40 | func (a *XUIController) setting(c *gin.Context) { 41 | html(c, "setting.html", "设置", nil) 42 | } 43 | -------------------------------------------------------------------------------- /web/entity/entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "net" 7 | "strings" 8 | "time" 9 | "xray-ui/util/common" 10 | "xray-ui/xray" 11 | ) 12 | 13 | type Msg struct { 14 | Success bool `json:"success"` 15 | Msg string `json:"msg"` 16 | Obj interface{} `json:"obj"` 17 | } 18 | 19 | type Pager struct { 20 | Current int `json:"current"` 21 | PageSize int `json:"page_size"` 22 | Total int `json:"total"` 23 | OrderBy string `json:"order_by"` 24 | Desc bool `json:"desc"` 25 | Key string `json:"key"` 26 | List interface{} `json:"list"` 27 | } 28 | 29 | type AllSetting struct { 30 | WebListen string `json:"webListen" form:"webListen"` 31 | WebPort int `json:"webPort" form:"webPort"` 32 | WebCertFile string `json:"webCertFile" form:"webCertFile"` 33 | WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` 34 | WebCAFile string `json:"webCAFile" form:"webCAFile"` 35 | WebBasePath string `json:"webBasePath" form:"webBasePath"` 36 | TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` 37 | TgBotToken string `json:"tgBotToken" form:"tgBotToken"` 38 | TgBotChatId int `json:"tgBotChatId" form:"tgBotChatId"` 39 | TgRunTime string `json:"tgRunTime" form:"tgRunTime"` 40 | XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"` 41 | 42 | TimeLocation string `json:"timeLocation" form:"timeLocation"` 43 | } 44 | 45 | func (s *AllSetting) CheckValid() error { 46 | if s.WebListen != "" { 47 | ip := net.ParseIP(s.WebListen) 48 | if ip == nil { 49 | return common.NewError("web listen is not valid ip:", s.WebListen) 50 | } 51 | } 52 | 53 | if s.WebPort <= 0 || s.WebPort > 65535 { 54 | return common.NewError("web port is not a valid port:", s.WebPort) 55 | } 56 | 57 | if s.WebCertFile != "" || s.WebKeyFile != "" { 58 | _, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile) 59 | if err != nil { 60 | return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.WebCertFile, s.WebKeyFile, err) 61 | } 62 | } 63 | 64 | if !strings.HasPrefix(s.WebBasePath, "/") { 65 | s.WebBasePath = "/" + s.WebBasePath 66 | } 67 | if !strings.HasSuffix(s.WebBasePath, "/") { 68 | s.WebBasePath += "/" 69 | } 70 | 71 | xrayConfig := &xray.Config{} 72 | err := json.Unmarshal([]byte(s.XrayTemplateConfig), xrayConfig) 73 | if err != nil { 74 | return common.NewError("xray template config invalid:", err) 75 | } 76 | 77 | _, err = time.LoadLocation(s.TimeLocation) 78 | if err != nil { 79 | return common.NewError("time location not exist:", s.TimeLocation) 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /web/global/global.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "context" 5 | "github.com/robfig/cron/v3" 6 | _ "unsafe" 7 | ) 8 | 9 | var webServer WebServer 10 | 11 | type WebServer interface { 12 | GetCron() *cron.Cron 13 | GetCtx() context.Context 14 | } 15 | 16 | func SetWebServer(s WebServer) { 17 | webServer = s 18 | } 19 | 20 | func GetWebServer() WebServer { 21 | return webServer 22 | } 23 | -------------------------------------------------------------------------------- /web/html/common/head.html: -------------------------------------------------------------------------------- 1 | {{define "head"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | {{.title}} 18 | 19 | {{end}} -------------------------------------------------------------------------------- /web/html/common/js.html: -------------------------------------------------------------------------------- 1 | {{define "js"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | {{end}} -------------------------------------------------------------------------------- /web/html/common/prompt_modal.html: -------------------------------------------------------------------------------- 1 | {{define "promptModal"}} 2 | 5 | 10 | 11 | 12 | 67 | {{end}} -------------------------------------------------------------------------------- /web/html/common/qrcode_modal.html: -------------------------------------------------------------------------------- 1 | {{define "qrcodeModal"}} 2 | 5 | 6 | 7 | 8 | 59 | {{end}} -------------------------------------------------------------------------------- /web/html/common/text_modal.html: -------------------------------------------------------------------------------- 1 | {{define "textModal"}} 2 | 6 | 8 | {{ i18n "download" }} [[ txtModal.fileName ]] 9 | 10 | 12 | 13 | 14 | 51 | {{end}} -------------------------------------------------------------------------------- /web/html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "head" .}} 4 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |

{{ .title }}

37 |
38 |
39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | {{ i18n "login" }} 56 | 57 | 58 | 59 | 60 |
61 |
62 |
63 | {{template "js" .}} 64 | 89 | 90 | -------------------------------------------------------------------------------- /web/html/xui/common_sider.html: -------------------------------------------------------------------------------- 1 | {{define "menuItems"}} 2 | 3 | 4 | 系统状态 5 | 6 | 7 | 8 | 入站列表 9 | 10 | 11 | 12 | 面板设置 13 | 14 | 18 | 27 | 28 | 29 | 30 | 31 | 退出登录 32 | 33 | {{end}} 34 | 35 | 36 | {{define "commonSider"}} 37 | 38 | 40 | {{template "menuItems" .}} 41 | 42 | 43 | 46 |
47 | 48 |
49 | 51 | {{template "menuItems" .}} 52 | 53 |
54 | 70 | {{end}} 71 | -------------------------------------------------------------------------------- /web/html/xui/component/inbound_info.html: -------------------------------------------------------------------------------- 1 | {{define "inboundInfoStream"}} 2 |

传输: [[ inbound.network ]]

3 | 4 | 11 | 12 | 13 | 17 | 18 | 21 | 22 | 26 | 27 | 28 | 31 |

32 | tls域名: [[ inbound.serverName ? inbound.serverName : "无" ]] 33 |

34 | {{end}} 35 | 36 | 37 | {{define "component/inboundInfoComponent"}} 38 |
39 |

协议: [[ dbInbound.protocol ]]

40 |

地址: [[ dbInbound.address ]]

41 |

端口: [[ dbInbound.port ]]

42 | 43 | 46 | 47 | 52 | 53 | 56 | 57 | 61 | 62 | 66 | 67 | 71 | 72 | 75 |
76 | {{end}} 77 | 78 | {{define "component/inboundInfo"}} 79 | 86 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/component/setting.html: -------------------------------------------------------------------------------- 1 | {{define "component/settingListItem"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 14 | 17 | 20 | 21 | 22 | 23 | {{end}} 24 | 25 | {{define "component/setting"}} 26 | 32 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/component/themeSwitch.html: -------------------------------------------------------------------------------- 1 | {{define "component/themeSwitchTemplate"}} 2 | 9 | {{end}} 10 | 11 | {{define "component/themeSwitcher"}} 12 | 58 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/inbound.html: -------------------------------------------------------------------------------- 1 | {{define "form/inbound"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | [[ p ]] 13 | 14 | 15 | 16 | 17 | 监听 IP 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 总流量(GB) 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 到期时间 45 | 46 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 58 | 61 | 62 | 63 | 66 | 67 | 68 | 71 | 72 | 73 | 76 | 77 | 78 | 81 | 82 | 83 | 86 | 87 | 88 | 91 | 92 | 93 | 96 | 97 | 98 | 101 | 102 | 103 | 106 | 107 | 108 | 111 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/protocol/dokodemo.html: -------------------------------------------------------------------------------- 1 | {{define "form/dokodemo"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | tcp+udp 12 | tcp 13 | udp 14 | 15 | 16 | 17 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/protocol/http.html: -------------------------------------------------------------------------------- 1 | {{define "form/http"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/protocol/shadowsocks.html: -------------------------------------------------------------------------------- 1 | {{define "form/shadowsocks"}} 2 | 3 | 4 | 5 | [[ method ]] 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | tcp+udp 14 | tcp 15 | udp 16 | 17 | 18 | 19 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/protocol/socks.html: -------------------------------------------------------------------------------- 1 | {{define "form/socks"}} 2 | 3 | 4 | 6 | 7 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/protocol/trojan.html: -------------------------------------------------------------------------------- 1 | {{define "form/trojan"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | + 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | fallback[[ index + 1 ]] 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/protocol/vless.html: -------------------------------------------------------------------------------- 1 | {{define "form/vless"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | [[ key ]] 11 | 12 | 13 | 14 | 15 | 16 | [[ key ]] 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | + 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | fallback[[ index + 1 ]] 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/protocol/vmess.html: -------------------------------------------------------------------------------- 1 | {{define "form/vmess"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/reality_settings.html: -------------------------------------------------------------------------------- 1 | {{define "form/realitySettings"}} 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | [[ key ]] 30 | 31 | 32 | 33 | 34 | 35 | 36 | Get New Cert 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/sniffing.html: -------------------------------------------------------------------------------- 1 | {{define "form/sniffing"}} 2 | 3 | 4 | 5 | sniffing 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | [[ value ]] 18 | 19 | 20 | 21 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_grpc.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamGRPC"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_httpupgrade.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamHTTPUpgrade"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | + 17 | 18 | 19 | 20 | 22 | 24 | 30 | 31 | 32 | 33 | 34 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_kcp.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamKCP"}} 2 | 3 | 4 | 5 | none(not camouflage) 6 | srtp(camouflage video call) 7 | utp(camouflage BT download) 8 | wechat-video(camouflage WeChat video) 9 | dtls(camouflage DTLS 1.2 packages) 10 | wireguard(camouflage wireguard packages) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_raw.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamRaw"}} 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | + 31 | 32 | 33 | 34 | 36 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 65 | + 66 | 67 | 68 | 69 | 71 | 73 | 79 | 80 | 81 | 82 | 83 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_settings.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamSettings"}} 2 | 3 | 4 | 5 | 6 | tcp 7 | raw 8 | kcp 9 | ws 10 | grpc 11 | HTTPUpgrade 12 | xHTTP 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 26 | 27 | 28 | 31 | 32 | 33 | 36 | 37 | 38 | 41 | 42 | 43 | 46 | 47 | 48 | 51 | 52 | 55 | 56 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_sockopt.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamSOCKOPT"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | OFF 24 | Redirect 25 | T-Proxy 26 | 27 | 28 | 29 | 30 | [[ key ]] 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 系统默认 55 | [[ key ]] 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_tcp.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamTCP"}} 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | + 31 | 32 | 33 | 34 | 36 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 65 | + 66 | 67 | 68 | 69 | 71 | 73 | 79 | 80 | 81 | 82 | 83 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_ws.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamWS"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | + 11 | 12 | 13 | 14 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_xhttp.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamXHTTP"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | + 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | [[ key ]] 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | xPadding Bytes (Byte) 46 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Max Connections 60 | 61 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | Max Concurrency 75 | 76 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | {{end}} 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /web/html/xui/form/tls_settings.html: -------------------------------------------------------------------------------- 1 | {{define "form/tlsSettings"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | reality 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | CipherSuites 36 | 37 | 40 | 41 | 42 | 43 | 44 | auto 45 | [[ key ]] 46 | 47 | 48 | 49 | 51 | [[ key ]] 52 | 53 | 54 | 55 | 57 | [[ key ]] 58 | 59 | 60 | 61 | 62 | None 63 | [[ key ]] 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | [[ key ]] 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | certificate file path 80 | certificate file content 81 | 82 | 83 | 94 | 105 | 106 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/inbound_info_modal.html: -------------------------------------------------------------------------------- 1 | {{define "inboundInfoModal"}} 2 | {{template "component/inboundInfo"}} 3 | 6 | 7 | 8 | 61 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/inbound_modal.html: -------------------------------------------------------------------------------- 1 | {{define "inboundModal"}} 2 | 5 | {{template "form/inbound"}} 6 | 7 | 106 | {{end}} -------------------------------------------------------------------------------- /web/job/check_inbound_job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "xray-ui/logger" 5 | "xray-ui/web/service" 6 | ) 7 | 8 | type CheckInboundJob struct { 9 | xrayService service.XrayService 10 | inboundService service.InboundService 11 | } 12 | 13 | func NewCheckInboundJob() *CheckInboundJob { 14 | return new(CheckInboundJob) 15 | } 16 | 17 | func (j *CheckInboundJob) Run() { 18 | count, err := j.inboundService.DisableInvalidInbounds() 19 | if err != nil { 20 | logger.Warning("disable invalid inbounds err:", err) 21 | } else if count > 0 { 22 | logger.Debugf("disabled %v inbounds", count) 23 | j.xrayService.SetToNeedRestart() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/job/check_xray_running_job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import "xray-ui/web/service" 4 | 5 | type CheckXrayRunningJob struct { 6 | xrayService service.XrayService 7 | 8 | checkTime int 9 | } 10 | 11 | func NewCheckXrayRunningJob() *CheckXrayRunningJob { 12 | return new(CheckXrayRunningJob) 13 | } 14 | 15 | func (j *CheckXrayRunningJob) Run() { 16 | if j.xrayService.IsXrayRunning() { 17 | j.checkTime = 0 18 | return 19 | } 20 | j.checkTime++ 21 | if j.checkTime < 2 { 22 | return 23 | } 24 | j.xrayService.SetToNeedRestart() 25 | } 26 | -------------------------------------------------------------------------------- /web/job/stats_notify_job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strconv" 8 | "time" 9 | "xray-ui/logger" 10 | "xray-ui/util/common" 11 | "xray-ui/web/service" 12 | ) 13 | 14 | var SSHLoginUser int 15 | 16 | type LoginStatus byte 17 | 18 | const ( 19 | LoginSuccess LoginStatus = 1 20 | LoginFail LoginStatus = 0 21 | ) 22 | 23 | type StatsNotifyJob struct { 24 | enable bool 25 | telegramService service.TelegramService 26 | xrayService service.XrayService 27 | inboundService service.InboundService 28 | settingService service.SettingService 29 | } 30 | 31 | func NewStatsNotifyJob() *StatsNotifyJob { 32 | return new(StatsNotifyJob) 33 | } 34 | 35 | //Here run is a interface method of Job interface 36 | func (j *StatsNotifyJob) Run() { 37 | if !j.xrayService.IsXrayRunning() { 38 | return 39 | } 40 | var info string 41 | info = j.GetsystemStatus() 42 | j.telegramService.SendMsgToTgbot(info) 43 | } 44 | 45 | func (j *StatsNotifyJob) UserLoginNotify(username string, ip string, time string, status LoginStatus) { 46 | if username == "" || ip == "" || time == "" { 47 | logger.Warning("UserLoginNotify failed, invalid info") 48 | return 49 | } 50 | var msg string 51 | //get hostname 52 | name, err := os.Hostname() 53 | if err != nil { 54 | fmt.Println("get hostname error: ", err) 55 | return 56 | } 57 | if status == LoginSuccess { 58 | msg = fmt.Sprintf("面板登录成功提醒\r\n主机名称: %s\r\n", name) 59 | } else if status == LoginFail { 60 | msg = fmt.Sprintf("面板登录失败提醒\r\n主机名称: %s\r\n", name) 61 | } 62 | msg += fmt.Sprintf("时间: %s\r\n", time) 63 | msg += fmt.Sprintf("用户: %s\r\n", username) 64 | msg += fmt.Sprintf("IP: %s\r\n", ip) 65 | j.telegramService.SendMsgToTgbot(msg) 66 | } 67 | 68 | func (j *StatsNotifyJob) SSHStatusLoginNotify(xuiStartTime string) { 69 | getSSHUserNumber, error := exec.Command("bash", "-c", "who | awk '{print $1}' | wc -l").Output() 70 | if error != nil { 71 | fmt.Println("getSSHUserNumber error: ", error) 72 | return 73 | } 74 | var numberInt int 75 | numberInt, error = strconv.Atoi(common.ByteToString(getSSHUserNumber)) 76 | if error != nil { 77 | return 78 | } 79 | if numberInt > SSHLoginUser { 80 | var SSHLoginInfo string 81 | SSHLoginUser = numberInt 82 | //hostname 83 | name, err := os.Hostname() 84 | if err != nil { 85 | fmt.Println("get hostname error: ", err) 86 | return 87 | } 88 | //Time compare,need if xray-ui got restart while ssh already exist users 89 | SSHLoginTime, error := exec.Command("bash", "-c", "who | awk '{print $3,$4}' | tail -n 1 ").Output() 90 | if error != nil { 91 | fmt.Println("getLoginTime error: ", error.Error()) 92 | return 93 | } 94 | /* 95 | //TODO:time compare if xray-ui get restart and there exist logging users 96 | XUIRunTime, error := exec.Command("bash", "-c", " systemctl status xray-ui | grep Active| tail -n 1 | awk '{print $6,$7}' ").Output() 97 | if error != nil { 98 | fmt.Println("getXUIRunTime error:", error.Error()) 99 | return 100 | } 101 | */ 102 | var SSHLoginTimeStr string 103 | SSHLoginTimeStr = common.ByteToString(SSHLoginTime) 104 | t1, err := time.Parse("2006-01-02 15:04:05", SSHLoginTimeStr) 105 | t2, err := time.Parse("2006-01-02 15:04:05", xuiStartTime) 106 | if t1.Before(t2) || err != nil { 107 | fmt.Printf("SSHLogin[%s] early than XRAY-UI start[%s]\r\n", SSHLoginTimeStr, xuiStartTime) 108 | } 109 | 110 | SSHLoginUserName, error := exec.Command("bash", "-c", "who | awk '{print $1}'| tail -n 1").Output() 111 | if error != nil { 112 | fmt.Println("getSSHLoginUserName error: ", error.Error()) 113 | return 114 | } 115 | 116 | SSHLoginIpAddr, error := exec.Command("bash", "-c", "who | awk -F [\\(\\)] 'NR==1 {print $2}'").Output() 117 | if error != nil { 118 | fmt.Println("getSSHLoginIpAddr error: ", error) 119 | return 120 | } 121 | 122 | SSHLoginInfo = fmt.Sprintf("新用户登录提醒:\r\n") 123 | SSHLoginInfo += fmt.Sprintf("主机名称:%s\r\n", name) 124 | SSHLoginInfo += fmt.Sprintf("SSH登录用户:%s", SSHLoginUserName) 125 | SSHLoginInfo += fmt.Sprintf("SSH登录时间:%s", SSHLoginTime) 126 | SSHLoginInfo += fmt.Sprintf("SSH登录IP:%s", SSHLoginIpAddr) 127 | SSHLoginInfo += fmt.Sprintf("当前SSH登录用户数:%s", getSSHUserNumber) 128 | j.telegramService.SendMsgToTgbot(SSHLoginInfo) 129 | } else { 130 | SSHLoginUser = numberInt 131 | } 132 | } 133 | 134 | func (j *StatsNotifyJob) GetsystemStatus() string { 135 | var info string 136 | //get hostname 137 | name, err := os.Hostname() 138 | if err != nil { 139 | fmt.Println("get hostname error:", err) 140 | return "" 141 | } 142 | info = fmt.Sprintf("主机名称: %s\r\n", name) 143 | //get ip address 144 | var ip string 145 | ip = common.GetMyIpAddr() 146 | info += fmt.Sprintf("IP地址: %s\r\n \r\n", ip) 147 | 148 | //get traffic 149 | inbouds, err := j.inboundService.GetAllInbounds() 150 | if err != nil { 151 | logger.Warning("StatsNotifyJob run failed: ", err) 152 | return "" 153 | } 154 | //NOTE:If there no any sessions here,need to notify here 155 | //TODO:分节点推送,自动转化格式 156 | for _, inbound := range inbouds { 157 | info += fmt.Sprintf("节点名称: %s\r\n端口: %d\r\n上行流量↑: %s\r\n下行流量↓: %s\r\n总流量: %s\r\n", inbound.Remark, inbound.Port, common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down), common.FormatTraffic((inbound.Up + inbound.Down))) 158 | if inbound.ExpiryTime == 0 { 159 | info += fmt.Sprintf("到期时间: 无限期\r\n \r\n") 160 | } else { 161 | info += fmt.Sprintf("到期时间: %s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) 162 | } 163 | } 164 | return info 165 | } 166 | -------------------------------------------------------------------------------- /web/job/xray_traffic_job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "xray-ui/logger" 5 | "xray-ui/web/service" 6 | ) 7 | 8 | type XrayTrafficJob struct { 9 | xrayService service.XrayService 10 | inboundService service.InboundService 11 | } 12 | 13 | func NewXrayTrafficJob() *XrayTrafficJob { 14 | return new(XrayTrafficJob) 15 | } 16 | 17 | func (j *XrayTrafficJob) Run() { 18 | if !j.xrayService.IsXrayRunning() { 19 | return 20 | } 21 | traffics, err := j.xrayService.GetXrayTraffic() 22 | if err != nil { 23 | logger.Warning("get xray traffic failed:", err) 24 | return 25 | } 26 | err = j.inboundService.AddTraffic(traffics) 27 | if err != nil { 28 | logger.Warning("add traffic failed:", err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /web/network/auto_https_listener.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import "net" 4 | 5 | type AutoHttpsListener struct { 6 | net.Listener 7 | } 8 | 9 | func NewAutoHttpsListener(listener net.Listener) net.Listener { 10 | return &AutoHttpsListener{ 11 | Listener: listener, 12 | } 13 | } 14 | 15 | func (l *AutoHttpsListener) Accept() (net.Conn, error) { 16 | conn, err := l.Listener.Accept() 17 | if err != nil { 18 | return nil, err 19 | } 20 | return NewAutoHttpsConn(conn), nil 21 | } 22 | -------------------------------------------------------------------------------- /web/network/autp_https_conn.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "sync" 10 | ) 11 | 12 | type AutoHttpsConn struct { 13 | net.Conn 14 | 15 | firstBuf []byte 16 | bufStart int 17 | 18 | readRequestOnce sync.Once 19 | } 20 | 21 | func NewAutoHttpsConn(conn net.Conn) net.Conn { 22 | return &AutoHttpsConn{ 23 | Conn: conn, 24 | } 25 | } 26 | 27 | func (c *AutoHttpsConn) readRequest() bool { 28 | c.firstBuf = make([]byte, 2048) 29 | n, err := c.Conn.Read(c.firstBuf) 30 | c.firstBuf = c.firstBuf[:n] 31 | if err != nil { 32 | return false 33 | } 34 | reader := bytes.NewReader(c.firstBuf) 35 | bufReader := bufio.NewReader(reader) 36 | request, err := http.ReadRequest(bufReader) 37 | if err != nil { 38 | return false 39 | } 40 | resp := http.Response{ 41 | Header: http.Header{}, 42 | } 43 | resp.StatusCode = http.StatusTemporaryRedirect 44 | location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI) 45 | resp.Header.Set("Location", location) 46 | resp.Write(c.Conn) 47 | c.Close() 48 | c.firstBuf = nil 49 | return true 50 | } 51 | 52 | func (c *AutoHttpsConn) Read(buf []byte) (int, error) { 53 | c.readRequestOnce.Do(func() { 54 | c.readRequest() 55 | }) 56 | 57 | if c.firstBuf != nil { 58 | n := copy(buf, c.firstBuf[c.bufStart:]) 59 | c.bufStart += n 60 | if c.bufStart >= len(c.firstBuf) { 61 | c.firstBuf = nil 62 | } 63 | return n, nil 64 | } 65 | 66 | return c.Conn.Read(buf) 67 | } 68 | -------------------------------------------------------------------------------- /web/service/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "access": "none", 4 | "error": "none", 5 | "loglevel": "info" 6 | }, 7 | "api": { 8 | "services": [ 9 | "HandlerService", 10 | "LoggerService", 11 | "StatsService" 12 | ], 13 | "tag": "api" 14 | }, 15 | "inbounds": [ 16 | { 17 | "listen": "127.0.0.1", 18 | "port": 62789, 19 | "protocol": "dokodemo-door", 20 | "settings": { 21 | "address": "127.0.0.1" 22 | }, 23 | "tag": "api" 24 | } 25 | ], 26 | "outbounds": [ 27 | { 28 | "protocol": "freedom", 29 | "tag": "direct" 30 | }, 31 | { 32 | "protocol": "blackhole", 33 | "tag": "blocked" 34 | } 35 | ], 36 | "policy": { 37 | "system": { 38 | "statsInboundDownlink": true, 39 | "statsInboundUplink": true, 40 | "statsOutboundDownlink": true, 41 | "statsOutboundUplink": true 42 | }, 43 | "levels": { 44 | "0": { 45 | "handshake": 2, 46 | "connIdle": 120, 47 | "uplinkOnly": 1, 48 | "downlinkOnly": 1 49 | } 50 | } 51 | }, 52 | "routing": { 53 | "domainStrategy": "IPIfNonMatch", 54 | "rules": [ 55 | { 56 | "inboundTag": [ 57 | "api" 58 | ], 59 | "outboundTag": "api", 60 | "type": "field" 61 | }, 62 | { 63 | "type": "field", 64 | "domain": [ 65 | "www.gstatic.com" 66 | ], 67 | "outboundTag": "direct" 68 | }, 69 | { 70 | "type": "field", 71 | "domain": [ 72 | "geosite:category-ads-all", 73 | "geosite:cn", 74 | "geosite:geolocation-cn" 75 | ], 76 | "outboundTag": "blocked" 77 | }, 78 | { 79 | "type": "field", 80 | "ip": [ 81 | "geoip:cn" 82 | ], 83 | "outboundTag": "blocked" 84 | }, 85 | { 86 | "type": "field", 87 | "protocol": [ 88 | "bittorrent" 89 | ], 90 | "outboundTag": "blocked" 91 | } 92 | ] 93 | }, 94 | "stats": {} 95 | } -------------------------------------------------------------------------------- /web/service/inbound.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | "xray-ui/database" 7 | "xray-ui/database/model" 8 | "xray-ui/util/common" 9 | "xray-ui/xray" 10 | 11 | "gorm.io/gorm" 12 | ) 13 | 14 | type InboundService struct { 15 | } 16 | 17 | func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { 18 | db := database.GetDB() 19 | var inbounds []*model.Inbound 20 | err := db.Model(model.Inbound{}).Where("user_id = ?", userId).Find(&inbounds).Error 21 | if err != nil && err != gorm.ErrRecordNotFound { 22 | return nil, err 23 | } 24 | return inbounds, nil 25 | } 26 | 27 | func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) { 28 | db := database.GetDB() 29 | var inbounds []*model.Inbound 30 | err := db.Model(model.Inbound{}).Find(&inbounds).Error 31 | if err != nil && err != gorm.ErrRecordNotFound { 32 | return nil, err 33 | } 34 | return inbounds, nil 35 | } 36 | 37 | func (s *InboundService) checkPortExist(port int, ignoreId int) (bool, error) { 38 | db := database.GetDB() 39 | db = db.Model(model.Inbound{}).Where("port = ?", port) 40 | if ignoreId > 0 { 41 | db = db.Where("id != ?", ignoreId) 42 | } 43 | var count int64 44 | err := db.Count(&count).Error 45 | if err != nil { 46 | return false, err 47 | } 48 | return count > 0, nil 49 | } 50 | 51 | func (s *InboundService) AddInbound(inbound *model.Inbound) error { 52 | exist, err := s.checkPortExist(inbound.Port, 0) 53 | if err != nil { 54 | return err 55 | } 56 | if exist { 57 | return common.NewError("端口已存在:", inbound.Port) 58 | } 59 | db := database.GetDB() 60 | return db.Save(inbound).Error 61 | } 62 | 63 | func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error { 64 | for _, inbound := range inbounds { 65 | exist, err := s.checkPortExist(inbound.Port, 0) 66 | if err != nil { 67 | return err 68 | } 69 | if exist { 70 | return common.NewError("端口已存在:", inbound.Port) 71 | } 72 | } 73 | 74 | db := database.GetDB() 75 | tx := db.Begin() 76 | var err error 77 | defer func() { 78 | if err == nil { 79 | tx.Commit() 80 | } else { 81 | tx.Rollback() 82 | } 83 | }() 84 | 85 | for _, inbound := range inbounds { 86 | err = tx.Save(inbound).Error 87 | if err != nil { 88 | return err 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func (s *InboundService) DelInbound(id int) error { 96 | db := database.GetDB() 97 | return db.Delete(model.Inbound{}, id).Error 98 | } 99 | 100 | func (s *InboundService) DelInboundByPort(port int) error { 101 | db := database.GetDB() 102 | var inbound model.Inbound 103 | db.First(&inbound, "port = ?", port) 104 | return db.Delete(&inbound).Error 105 | } 106 | 107 | func (s *InboundService) GetInbound(id int) (*model.Inbound, error) { 108 | db := database.GetDB() 109 | inbound := &model.Inbound{} 110 | err := db.Model(model.Inbound{}).First(inbound, id).Error 111 | if err != nil { 112 | return nil, err 113 | } 114 | return inbound, nil 115 | } 116 | 117 | func (s *InboundService) UpdateInbound(inbound *model.Inbound) error { 118 | exist, err := s.checkPortExist(inbound.Port, inbound.Id) 119 | if err != nil { 120 | return err 121 | } 122 | if exist { 123 | return common.NewError("端口已存在:", inbound.Port) 124 | } 125 | 126 | oldInbound, err := s.GetInbound(inbound.Id) 127 | if err != nil { 128 | return err 129 | } 130 | oldInbound.Up = inbound.Up 131 | oldInbound.Down = inbound.Down 132 | oldInbound.Total = inbound.Total 133 | oldInbound.Remark = inbound.Remark 134 | oldInbound.Enable = inbound.Enable 135 | oldInbound.ExpiryTime = inbound.ExpiryTime 136 | oldInbound.Listen = inbound.Listen 137 | oldInbound.Port = inbound.Port 138 | oldInbound.Protocol = inbound.Protocol 139 | oldInbound.Settings = inbound.Settings 140 | oldInbound.StreamSettings = inbound.StreamSettings 141 | oldInbound.Sniffing = inbound.Sniffing 142 | oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) 143 | 144 | db := database.GetDB() 145 | return db.Save(oldInbound).Error 146 | } 147 | 148 | func (s *InboundService) ClearTrafficByPort(port int) error { 149 | db := database.GetDB() 150 | Uperr := db.Model(model.Inbound{}).Where("port = ?", port).Update("up", 0).Error 151 | if Uperr != nil { 152 | fmt.Println("ClearTrafficByPort error:clear up failed") 153 | return Uperr 154 | } 155 | Downerr := db.Model(model.Inbound{}).Where("port = ?", port).Update("down", 0).Error 156 | if Downerr != nil { 157 | fmt.Println("ClearTrafficByPort error:clear down failed") 158 | return Downerr 159 | } 160 | return nil 161 | } 162 | 163 | func (s *InboundService) ClearAllInboundTraffic() error { 164 | inbounds, _ := s.GetAllInbounds() 165 | for _, inbound := range inbounds { 166 | err := s.ClearTrafficByPort(inbound.Port) 167 | if err != nil { 168 | fmt.Printf("ClearAllInboundTraffic error,ClearTrafficByPort port %d fail", inbound.Port) 169 | continue 170 | } 171 | } 172 | return nil 173 | } 174 | 175 | func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) { 176 | if len(traffics) == 0 { 177 | return nil 178 | } 179 | db := database.GetDB() 180 | db = db.Model(model.Inbound{}) 181 | tx := db.Begin() 182 | defer func() { 183 | if err != nil { 184 | tx.Rollback() 185 | } else { 186 | tx.Commit() 187 | } 188 | }() 189 | for _, traffic := range traffics { 190 | if traffic.IsInbound { 191 | err = tx.Where("tag = ?", traffic.Tag). 192 | UpdateColumns(map[string]interface{}{ 193 | "up": gorm.Expr("up + ?", traffic.Up), 194 | "down": gorm.Expr("down + ?", traffic.Down)}).Error 195 | if err != nil { 196 | return 197 | } 198 | } 199 | } 200 | return 201 | } 202 | 203 | func (s *InboundService) DisableInvalidInbounds() (int64, error) { 204 | db := database.GetDB() 205 | now := time.Now().Unix() * 1000 206 | result := db.Model(model.Inbound{}). 207 | Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true). 208 | Update("enable", false) 209 | err := result.Error 210 | count := result.RowsAffected 211 | return count, err 212 | } 213 | 214 | func (s *InboundService) DisableInboundByPort(port int) error { 215 | db := database.GetDB() 216 | return db.Model(model.Inbound{}).Where("port = ?", port).Update("enable", false).Error 217 | } 218 | func (s *InboundService) EnableInboundByPort(port int) error { 219 | db := database.GetDB() 220 | return db.Model(model.Inbound{}).Where("port = ?", port).Update("enable", true).Error 221 | } 222 | -------------------------------------------------------------------------------- /web/service/panel.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "time" 7 | "xray-ui/logger" 8 | ) 9 | 10 | type PanelService struct { 11 | } 12 | 13 | func (s *PanelService) RestartPanel(delay time.Duration) error { 14 | p, err := os.FindProcess(syscall.Getpid()) 15 | if err != nil { 16 | return err 17 | } 18 | go func() { 19 | time.Sleep(delay) 20 | err := p.Signal(syscall.SIGHUP) 21 | if err != nil { 22 | logger.Error("send signal SIGHUP failed:", err) 23 | } 24 | }() 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /web/service/user.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "xray-ui/database" 6 | "xray-ui/database/model" 7 | "xray-ui/logger" 8 | 9 | "gorm.io/gorm" 10 | ) 11 | 12 | type UserService struct { 13 | } 14 | 15 | func (s *UserService) GetFirstUser() (*model.User, error) { 16 | db := database.GetDB() 17 | 18 | user := &model.User{} 19 | err := db.Model(model.User{}). 20 | First(user). 21 | Error 22 | if err != nil { 23 | return nil, err 24 | } 25 | return user, nil 26 | } 27 | 28 | func (s *UserService) CheckUser(username string, password string) *model.User { 29 | db := database.GetDB() 30 | 31 | user := &model.User{} 32 | err := db.Model(model.User{}). 33 | Where("username = ? and password = ?", username, password). 34 | First(user). 35 | Error 36 | if err == gorm.ErrRecordNotFound { 37 | return nil 38 | } else if err != nil { 39 | logger.Warning("check user err:", err) 40 | return nil 41 | } 42 | return user 43 | } 44 | 45 | func (s *UserService) UpdateUser(id int, username string, password string) error { 46 | db := database.GetDB() 47 | return db.Model(model.User{}). 48 | Where("id = ?", id). 49 | Update("username", username). 50 | Update("password", password). 51 | Error 52 | } 53 | 54 | func (s *UserService) UpdateFirstUser(username string, password string) error { 55 | if username == "" { 56 | return errors.New("username can not be empty") 57 | } else if password == "" { 58 | return errors.New("password can not be empty") 59 | } 60 | db := database.GetDB() 61 | user := &model.User{} 62 | err := db.Model(model.User{}).First(user).Error 63 | if database.IsNotFound(err) { 64 | user.Username = username 65 | user.Password = password 66 | return db.Model(model.User{}).Create(user).Error 67 | } else if err != nil { 68 | return err 69 | } 70 | user.Username = username 71 | user.Password = password 72 | return db.Save(user).Error 73 | } 74 | -------------------------------------------------------------------------------- /web/service/version.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | //"fmt" 5 | "xray-ui/database" 6 | "xray-ui/database/model" 7 | ) 8 | 9 | type GeoipVersion struct { 10 | 11 | 12 | } 13 | func (s *GeoipVersion) GetVersion() (*model.VersionStatus, error) { 14 | db := database.GetDB() 15 | 16 | version := &model.VersionStatus{} 17 | err := db.Model(model.VersionStatus{}). 18 | First(version). 19 | Error 20 | if err != nil { 21 | return nil, err 22 | } 23 | // fmt.Println(version) 24 | return version, nil 25 | } 26 | func (s *GeoipVersion) UpVersion( version string) error { 27 | db := database.GetDB() 28 | return db.Model(model.VersionStatus{}). 29 | Where("id = 1", ). 30 | Update("version", version). 31 | Error 32 | } -------------------------------------------------------------------------------- /web/service/xray.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "sync" 7 | "xray-ui/logger" 8 | "xray-ui/xray" 9 | 10 | "go.uber.org/atomic" 11 | ) 12 | 13 | var p *xray.Process 14 | var lock sync.Mutex 15 | var isNeedXrayRestart atomic.Bool 16 | var result string 17 | 18 | type XrayService struct { 19 | inboundService InboundService 20 | settingService SettingService 21 | } 22 | 23 | func (s *XrayService) IsXrayRunning() bool { 24 | return p != nil && p.IsRunning() 25 | } 26 | 27 | func (s *XrayService) GetXrayErr() error { 28 | if p == nil { 29 | return nil 30 | } 31 | return p.GetErr() 32 | } 33 | 34 | func (s *XrayService) GetXrayResult() string { 35 | if result != "" { 36 | return result 37 | } 38 | if s.IsXrayRunning() { 39 | return "" 40 | } 41 | if p == nil { 42 | return "" 43 | } 44 | result = p.GetResult() 45 | return result 46 | } 47 | 48 | func (s *XrayService) GetXrayVersion() string { 49 | if p == nil { 50 | return "Unknown" 51 | } 52 | return p.GetVersion() 53 | } 54 | 55 | func (s *XrayService) GetXrayConfig() (*xray.Config, error) { 56 | templateConfig, err := s.settingService.GetXrayConfigTemplate() 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | xrayConfig := &xray.Config{} 62 | err = json.Unmarshal([]byte(templateConfig), xrayConfig) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | inbounds, err := s.inboundService.GetAllInbounds() 68 | if err != nil { 69 | return nil, err 70 | } 71 | for _, inbound := range inbounds { 72 | if !inbound.Enable { 73 | continue 74 | } 75 | inboundConfig := inbound.GenXrayInboundConfig() 76 | xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig) 77 | } 78 | return xrayConfig, nil 79 | } 80 | 81 | func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, error) { 82 | if !s.IsXrayRunning() { 83 | return nil, errors.New("xray is not running") 84 | } 85 | return p.GetTraffic(true) 86 | } 87 | 88 | func (s *XrayService) RestartXray(isForce bool) error { 89 | lock.Lock() 90 | defer lock.Unlock() 91 | logger.Debug("restart xray, force:", isForce) 92 | 93 | xrayConfig, err := s.GetXrayConfig() 94 | if err != nil { 95 | return err 96 | } 97 | 98 | if p != nil && p.IsRunning() { 99 | if !isForce && p.GetConfig().Equals(xrayConfig) { 100 | logger.Debug("not need to restart xray") 101 | return nil 102 | } 103 | p.Stop() 104 | } 105 | 106 | p = xray.NewProcess(xrayConfig) 107 | result = "" 108 | return p.Start() 109 | } 110 | 111 | func (s *XrayService) StopXray() error { 112 | lock.Lock() 113 | defer lock.Unlock() 114 | logger.Debug("stop xray") 115 | if s.IsXrayRunning() { 116 | return p.Stop() 117 | } 118 | return errors.New("xray is not running") 119 | } 120 | 121 | func (s *XrayService) SetToNeedRestart() { 122 | isNeedXrayRestart.Store(true) 123 | } 124 | 125 | func (s *XrayService) IsNeedRestartAndSetFalse() bool { 126 | return isNeedXrayRestart.CompareAndSwap(true, false) 127 | } 128 | -------------------------------------------------------------------------------- /web/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "encoding/gob" 5 | "xray-ui/database/model" 6 | 7 | "github.com/gin-contrib/sessions" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | const ( 12 | loginUser = "LOGIN_USER" 13 | ) 14 | 15 | func init() { 16 | gob.Register(model.User{}) 17 | } 18 | 19 | func SetLoginUser(c *gin.Context, user *model.User) error { 20 | s := sessions.Default(c) 21 | s.Set(loginUser, user) 22 | return s.Save() 23 | } 24 | 25 | func GetLoginUser(c *gin.Context) *model.User { 26 | s := sessions.Default(c) 27 | obj := s.Get(loginUser) 28 | if obj == nil { 29 | return nil 30 | } 31 | user := obj.(model.User) 32 | return &user 33 | } 34 | 35 | func IsLogin(c *gin.Context) bool { 36 | return GetLoginUser(c) != nil 37 | } 38 | 39 | func ClearSession(c *gin.Context) { 40 | s := sessions.Default(c) 41 | s.Clear() 42 | s.Options(sessions.Options{ 43 | Path: "/", 44 | MaxAge: -1, 45 | }) 46 | s.Save() 47 | } 48 | -------------------------------------------------------------------------------- /web/translation/translate.en_US.toml: -------------------------------------------------------------------------------- 1 | "username" = "username" 2 | "password" = "password" 3 | "login" = "login" 4 | "confirm" = "confirm" 5 | "cancel" = "cancel" 6 | "close" = "close" 7 | "copy" = "copy" 8 | "copied" = "copied" 9 | "download" = "download" 10 | "remark" = "remark" 11 | "enable" = "enable" 12 | "protocol" = "protocol" -------------------------------------------------------------------------------- /web/translation/translate.zh_Hans.toml: -------------------------------------------------------------------------------- 1 | "username" = "用户名" 2 | "password" = "密码" 3 | "login" = "登录" 4 | "confirm" = "确定" 5 | "cancel" = "取消" 6 | "close" = "关闭" 7 | "copy" = "复制" 8 | "copied" = "已复制" 9 | "download" = "下载" 10 | "remark" = "备注" 11 | "enable" = "启用" 12 | "protocol" = "协议" -------------------------------------------------------------------------------- /web/translation/translate.zh_Hant.toml: -------------------------------------------------------------------------------- 1 | "username" = "用戶名" 2 | "password" = "密碼" 3 | "login" = "登錄" 4 | "confirm" = "確定" 5 | "cancel" = "取消" 6 | "close" = "關閉" 7 | "copy" = "複製" 8 | "copied" = "已複製" 9 | "download" = "下載" 10 | "remark" = "備註" 11 | "enable" = "啟用" 12 | "protocol" = "協議" -------------------------------------------------------------------------------- /wireguard.md: -------------------------------------------------------------------------------- 1 | 使用 **warp-reg**,注册warp,导出wireguard配置 2 | 3 | ```bash 4 | curl -sLo warp-reg https://github.com/badafans/warp-reg/releases/download/v1.0/main-linux-amd64 && chmod +x warp-reg && ./warp-reg && rm warp-reg 5 | ``` 6 | 7 | 使用 **api.zeroteam.top**,获取warp账号 8 | ```bash 9 | curl -sLo /root/warp "https://api.zeroteam.top/warp?format=xray" > /dev/null && grep -Eo --color=never '"2606:4700:[0-9a-f:]+/128"|"secretKey":"[0-9a-zA-Z\/+]+="|"reserved":\[[0-9]+(,[0-9]+){2}\]' warp && rm warp 10 | ``` 11 | 12 | 打开 **wireguard.json**,复制"private_key"的值,粘贴到"secretKey": "",处,复制"reserved"的值,粘贴到"reserved":[0, 0, 0],处, 复制"peer_public_key"的值,粘贴到"publicKey: "",处 13 | 14 | ```json 15 | "outbounds": [ 16 | { 17 | "protocol": "wireguard", 18 | "settings": { 19 | "secretKey": "", 20 | "address": [ 21 | "172.16.0.2/32" 22 | ], 23 | "peers": [ 24 | { 25 | "publicKey": "", 26 | "allowedIPs": [ 27 | "0.0.0.0/0" 28 | ], 29 | "endpoint": "engage.cloudflareclient.com:2408" 30 | } 31 | ], 32 | "reserved":[0, 0, 0], 33 | "mtu": 1280 34 | }, 35 | "tag": "wireguard" 36 | } 37 | ] 38 | ``` 39 | 40 | 编辑 ****,按需增加"routing"和"outbounds"的内容(注意检查json语法), 选择保存配置 重启面板,访问ip.sb查看是否为Cloudflare的IP 41 | 42 | ```json 43 | "routing": { 44 | "domainStrategy": "IPIfNonMatch", 45 | "rules": [ 46 | { 47 | "inboundTag": [ 48 | "api" 49 | ], 50 | "outboundTag": "api", 51 | "type": "field" 52 | }, 53 | { 54 | "type": "field", 55 | "domain": [ 56 | "www.gstatic.com" 57 | ], 58 | "outboundTag": "direct" 59 | }, 60 | { 61 | "type": "field", 62 | "domain": [ 63 | "ip.sb", 64 | "geosite:openai", 65 | "geosite:geolocation-cn", 66 | "geosite:cn" 67 | ], 68 | "outboundTag": "wireguard" 69 | }, 70 | { 71 | "type": "field", 72 | "ip": [ 73 | "geoip:cn" 74 | ], 75 | "outboundTag": "wireguard" 76 | } 77 | ] 78 | } 79 | ``` 80 | 81 | **模板** 配置示例 82 | 83 | ```json 84 | { 85 | "log": { 86 | "loglevel": "info" 87 | }, 88 | "api": { 89 | "services": [ 90 | "HandlerService", 91 | "LoggerService", 92 | "StatsService" 93 | ], 94 | "tag": "api" 95 | }, 96 | "inbounds": [ 97 | { 98 | "listen": "127.0.0.1", 99 | "port": 62789, 100 | "protocol": "dokodemo-door", 101 | "settings": { 102 | "address": "127.0.0.1" 103 | }, 104 | "tag": "api" 105 | } 106 | ], 107 | "outbounds": [ 108 | { 109 | "protocol": "freedom", 110 | "settings": {} 111 | }, 112 | { 113 | "protocol": "blackhole", 114 | "settings": { 115 | "response": { 116 | "type": "http" 117 | } 118 | }, 119 | "tag": "blocked" 120 | }, 121 | { 122 | "protocol": "wireguard", 123 | "settings": { 124 | "secretKey": "", 125 | "address": [ 126 | "172.16.0.2/32" 127 | ], 128 | "network": "tcp,udp", 129 | "peers": [ 130 | { 131 | "publicKey": "", 132 | "allowedIPs": [ 133 | "0.0.0.0/0" 134 | ], 135 | "endpoint": "engage.cloudflareclient.com:2408" 136 | } 137 | ], 138 | "reserved": [ 139 | 209, 140 | 123, 141 | 109 142 | ], 143 | "mtu": 1280 144 | }, 145 | "tag": "wireguard" 146 | } 147 | ], 148 | "policy": { 149 | "system": { 150 | "statsInboundDownlink": true, 151 | "statsInboundUplink": true, 152 | "statsOutboundDownlink": true, 153 | "statsOutboundUplink": true 154 | }, 155 | "levels": { 156 | "0": { 157 | "handshake": 2, 158 | "connIdle": 120, 159 | "uplinkOnly": 1, 160 | "downlinkOnly": 1 161 | } 162 | } 163 | }, 164 | "routing": { 165 | "domainStrategy": "IPIfNonMatch", 166 | "rules": [ 167 | { 168 | "inboundTag": [ 169 | "api" 170 | ], 171 | "outboundTag": "api", 172 | "type": "field" 173 | }, 174 | { 175 | "type": "field", 176 | "domain": [ 177 | "www.gstatic.com" 178 | ], 179 | "outboundTag": "direct" 180 | }, 181 | { 182 | "type": "field", 183 | "domain": [ 184 | "ip.sb", 185 | "geosite:openai", 186 | "geosite:geolocation-cn", 187 | "geosite:cn" 188 | ], 189 | "outboundTag": "wireguard" 190 | }, 191 | { 192 | "type": "field", 193 | "ip": [ 194 | "geoip:cn" 195 | ], 196 | "outboundTag": "wireguard" 197 | } 198 | ] 199 | }, 200 | "stats": {} 201 | } 202 | ``` 203 | -------------------------------------------------------------------------------- /xray-ui.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=xray-ui Service 3 | After=network.target 4 | Wants=network.target 5 | 6 | [Service] 7 | Type=simple 8 | WorkingDirectory=/usr/local/xray-ui/ 9 | ExecStart=/usr/local/xray-ui/xray-ui 10 | Restart=on-failure 11 | RestartSec=5s 12 | 13 | [Install] 14 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /xray/config.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "bytes" 5 | "xray-ui/util/json_util" 6 | ) 7 | 8 | type Config struct { 9 | LogConfig json_util.RawMessage `json:"log"` 10 | RouterConfig json_util.RawMessage `json:"routing"` 11 | DNSConfig json_util.RawMessage `json:"dns"` 12 | InboundConfigs []InboundConfig `json:"inbounds"` 13 | OutboundConfigs json_util.RawMessage `json:"outbounds"` 14 | Transport json_util.RawMessage `json:"transport"` 15 | Policy json_util.RawMessage `json:"policy"` 16 | API json_util.RawMessage `json:"api"` 17 | Stats json_util.RawMessage `json:"stats"` 18 | Reverse json_util.RawMessage `json:"reverse"` 19 | FakeDNS json_util.RawMessage `json:"fakeDns"` 20 | } 21 | 22 | func (c *Config) Equals(other *Config) bool { 23 | if len(c.InboundConfigs) != len(other.InboundConfigs) { 24 | return false 25 | } 26 | for i, inbound := range c.InboundConfigs { 27 | if !inbound.Equals(&other.InboundConfigs[i]) { 28 | return false 29 | } 30 | } 31 | if !bytes.Equal(c.LogConfig, other.LogConfig) { 32 | return false 33 | } 34 | if !bytes.Equal(c.RouterConfig, other.RouterConfig) { 35 | return false 36 | } 37 | if !bytes.Equal(c.DNSConfig, other.DNSConfig) { 38 | return false 39 | } 40 | if !bytes.Equal(c.OutboundConfigs, other.OutboundConfigs) { 41 | return false 42 | } 43 | if !bytes.Equal(c.Transport, other.Transport) { 44 | return false 45 | } 46 | if !bytes.Equal(c.Policy, other.Policy) { 47 | return false 48 | } 49 | if !bytes.Equal(c.API, other.API) { 50 | return false 51 | } 52 | if !bytes.Equal(c.Stats, other.Stats) { 53 | return false 54 | } 55 | if !bytes.Equal(c.Reverse, other.Reverse) { 56 | return false 57 | } 58 | if !bytes.Equal(c.FakeDNS, other.FakeDNS) { 59 | return false 60 | } 61 | return true 62 | } 63 | -------------------------------------------------------------------------------- /xray/inbound.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "bytes" 5 | "xray-ui/util/json_util" 6 | ) 7 | 8 | type InboundConfig struct { 9 | Listen json_util.RawMessage `json:"listen"` // listen 不能为空字符串 10 | Port int `json:"port"` 11 | Protocol string `json:"protocol"` 12 | Settings json_util.RawMessage `json:"settings"` 13 | StreamSettings json_util.RawMessage `json:"streamSettings"` 14 | Tag string `json:"tag"` 15 | Sniffing json_util.RawMessage `json:"sniffing"` 16 | } 17 | 18 | func (c *InboundConfig) Equals(other *InboundConfig) bool { 19 | if !bytes.Equal(c.Listen, other.Listen) { 20 | return false 21 | } 22 | if c.Port != other.Port { 23 | return false 24 | } 25 | if c.Protocol != other.Protocol { 26 | return false 27 | } 28 | if !bytes.Equal(c.Settings, other.Settings) { 29 | return false 30 | } 31 | if !bytes.Equal(c.StreamSettings, other.StreamSettings) { 32 | return false 33 | } 34 | if c.Tag != other.Tag { 35 | return false 36 | } 37 | if !bytes.Equal(c.Sniffing, other.Sniffing) { 38 | return false 39 | } 40 | return true 41 | } 42 | -------------------------------------------------------------------------------- /xray/process.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io/fs" 11 | "os" 12 | "os/exec" 13 | "regexp" 14 | "syscall" 15 | "runtime" 16 | "strings" 17 | "time" 18 | "xray-ui/util/common" 19 | 20 | "github.com/Workiva/go-datastructures/queue" 21 | statsservice "github.com/xtls/xray-core/app/stats/command" 22 | "google.golang.org/grpc" 23 | ) 24 | 25 | var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)") 26 | 27 | func GetBinaryName() string { 28 | return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH) 29 | } 30 | 31 | func GetBinaryPath() string { 32 | return "bin/" + GetBinaryName() 33 | } 34 | 35 | func GetConfigPath() string { 36 | return "bin/config.json" 37 | } 38 | 39 | func GetGeositePath() string { 40 | return "bin/geosite.dat" 41 | } 42 | 43 | func GetGeoipPath() string { 44 | return "bin/geoip.dat" 45 | } 46 | 47 | func stopProcess(p *Process) { 48 | p.Stop() 49 | } 50 | 51 | type Process struct { 52 | *process 53 | } 54 | 55 | func NewProcess(xrayConfig *Config) *Process { 56 | p := &Process{newProcess(xrayConfig)} 57 | runtime.SetFinalizer(p, stopProcess) 58 | return p 59 | } 60 | 61 | type process struct { 62 | cmd *exec.Cmd 63 | 64 | version string 65 | apiPort int 66 | 67 | config *Config 68 | lines *queue.Queue 69 | exitErr error 70 | } 71 | 72 | func newProcess(config *Config) *process { 73 | return &process{ 74 | version: "Unknown", 75 | config: config, 76 | lines: queue.New(100), 77 | } 78 | } 79 | 80 | func (p *process) IsRunning() bool { 81 | if p.cmd == nil || p.cmd.Process == nil { 82 | return false 83 | } 84 | if p.cmd.ProcessState == nil { 85 | return true 86 | } 87 | return false 88 | } 89 | 90 | func (p *process) GetErr() error { 91 | return p.exitErr 92 | } 93 | 94 | func (p *process) GetResult() string { 95 | if p.lines.Empty() && p.exitErr != nil { 96 | return p.exitErr.Error() 97 | } 98 | items, _ := p.lines.TakeUntil(func(item interface{}) bool { 99 | return true 100 | }) 101 | lines := make([]string, 0, len(items)) 102 | for _, item := range items { 103 | lines = append(lines, item.(string)) 104 | } 105 | return strings.Join(lines, "\n") 106 | } 107 | 108 | func (p *process) GetVersion() string { 109 | return p.version 110 | } 111 | 112 | func (p *Process) GetAPIPort() int { 113 | return p.apiPort 114 | } 115 | 116 | func (p *Process) GetConfig() *Config { 117 | return p.config 118 | } 119 | 120 | func (p *process) refreshAPIPort() { 121 | for _, inbound := range p.config.InboundConfigs { 122 | if inbound.Tag == "api" { 123 | p.apiPort = inbound.Port 124 | break 125 | } 126 | } 127 | } 128 | 129 | func (p *process) refreshVersion() { 130 | cmd := exec.Command(GetBinaryPath(), "-version") 131 | data, err := cmd.Output() 132 | if err != nil { 133 | p.version = "Unknown" 134 | } else { 135 | datas := bytes.Split(data, []byte(" ")) 136 | if len(datas) <= 1 { 137 | p.version = "Unknown" 138 | } else { 139 | p.version = string(datas[1]) 140 | } 141 | } 142 | } 143 | 144 | func (p *process) Start() (err error) { 145 | if p.IsRunning() { 146 | return errors.New("xray is already running") 147 | } 148 | 149 | defer func() { 150 | if err != nil { 151 | p.exitErr = err 152 | } 153 | }() 154 | 155 | data, err := json.MarshalIndent(p.config, "", " ") 156 | if err != nil { 157 | return common.NewErrorf("生成 xray 配置文件失败: %v", err) 158 | } 159 | configPath := GetConfigPath() 160 | err = os.WriteFile(configPath, data, fs.ModePerm) 161 | if err != nil { 162 | return common.NewErrorf("写入配置文件失败: %v", err) 163 | } 164 | 165 | cmd := exec.Command(GetBinaryPath(), "-c", configPath) 166 | p.cmd = cmd 167 | 168 | stdReader, err := cmd.StdoutPipe() 169 | if err != nil { 170 | return err 171 | } 172 | errReader, err := cmd.StderrPipe() 173 | if err != nil { 174 | return err 175 | } 176 | 177 | go func() { 178 | defer func() { 179 | common.Recover("") 180 | stdReader.Close() 181 | }() 182 | reader := bufio.NewReaderSize(stdReader, 8192) 183 | for { 184 | line, _, err := reader.ReadLine() 185 | if err != nil { 186 | return 187 | } 188 | if p.lines.Len() >= 100 { 189 | p.lines.Get(1) 190 | } 191 | p.lines.Put(string(line)) 192 | } 193 | }() 194 | 195 | go func() { 196 | defer func() { 197 | common.Recover("") 198 | errReader.Close() 199 | }() 200 | reader := bufio.NewReaderSize(errReader, 8192) 201 | for { 202 | line, _, err := reader.ReadLine() 203 | if err != nil { 204 | return 205 | } 206 | if p.lines.Len() >= 100 { 207 | p.lines.Get(1) 208 | } 209 | p.lines.Put(string(line)) 210 | } 211 | }() 212 | 213 | go func() { 214 | err := cmd.Run() 215 | if err != nil { 216 | p.exitErr = err 217 | } 218 | }() 219 | 220 | p.refreshVersion() 221 | p.refreshAPIPort() 222 | 223 | return nil 224 | } 225 | 226 | func (p *process) Stop() error { 227 | if !p.IsRunning() { 228 | return errors.New("xray is not running") 229 | } 230 | return p.cmd.Process.Signal(syscall.SIGTERM) 231 | } 232 | 233 | func (p *process) GetTraffic(reset bool) ([]*Traffic, error) { 234 | if p.apiPort == 0 { 235 | return nil, common.NewError("xray api port wrong:", p.apiPort) 236 | } 237 | conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%v", p.apiPort), grpc.WithInsecure()) 238 | if err != nil { 239 | return nil, err 240 | } 241 | defer conn.Close() 242 | 243 | client := statsservice.NewStatsServiceClient(conn) 244 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 245 | defer cancel() 246 | request := &statsservice.QueryStatsRequest{ 247 | Reset_: reset, 248 | } 249 | resp, err := client.QueryStats(ctx, request) 250 | if err != nil { 251 | return nil, err 252 | } 253 | tagTrafficMap := map[string]*Traffic{} 254 | traffics := make([]*Traffic, 0) 255 | for _, stat := range resp.GetStat() { 256 | matchs := trafficRegex.FindStringSubmatch(stat.Name) 257 | isInbound := matchs[1] == "inbound" 258 | tag := matchs[2] 259 | isDown := matchs[3] == "downlink" 260 | if tag == "api" { 261 | continue 262 | } 263 | traffic, ok := tagTrafficMap[tag] 264 | if !ok { 265 | traffic = &Traffic{ 266 | IsInbound: isInbound, 267 | Tag: tag, 268 | } 269 | tagTrafficMap[tag] = traffic 270 | traffics = append(traffics, traffic) 271 | } 272 | if isDown { 273 | traffic.Down = stat.Value 274 | } else { 275 | traffic.Up = stat.Value 276 | } 277 | } 278 | 279 | return traffics, nil 280 | } 281 | -------------------------------------------------------------------------------- /xray/traffic.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | type Traffic struct { 4 | IsInbound bool 5 | Tag string 6 | Up int64 7 | Down int64 8 | } 9 | --------------------------------------------------------------------------------