├── .github ├── ISSUE_TEMPLATE │ └── bug_report.yml └── workflows │ ├── docker.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── bin ├── geoip.dat ├── geosite.dat ├── wxray.exe └── xray-windows-amd64.exe ├── config ├── config.go ├── name └── version ├── database ├── db.go └── model │ └── model.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── install.sh ├── logger └── logger.go ├── main.go ├── media ├── 2022-04-04_141259.png ├── 2022-04-17_110907.png ├── 2022-04-17_111321.png ├── 2022-04-17_111705.png ├── 2022-04-17_111910.png ├── bda84fbc2ede834deaba1c173a932223.png └── d13ffd6a73f938d1037d0708e31433bf.png ├── self_certificate ├── Win64OpenSSL_Light-3_0_7.exe ├── apple.crt ├── apple.key ├── google.crt ├── google.key ├── help.txt ├── yahoo.crt └── yahoo.key ├── util ├── common │ ├── err.go │ ├── format.go │ ├── multi_error.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 │ ├── js │ │ ├── axios-init.js │ │ ├── langs.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 │ ├── api.go │ ├── 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 │ │ ├── form │ │ ├── inbound.html │ │ ├── protocol │ │ │ ├── dokodemo.html │ │ │ ├── http.html │ │ │ ├── shadowsocks.html │ │ │ ├── socks.html │ │ │ ├── trojan.html │ │ │ ├── vless.html │ │ │ └── vmess.html │ │ ├── sniffing.html │ │ ├── stream │ │ │ ├── stream_grpc.html │ │ │ ├── stream_http.html │ │ │ ├── stream_kcp.html │ │ │ ├── stream_quic.html │ │ │ ├── stream_settings.html │ │ │ ├── stream_tcp.html │ │ │ └── stream_ws.html │ │ └── tls_settings.html │ │ ├── inbound_info_modal.html │ │ ├── inbound_modal.html │ │ ├── inbounds.html │ │ ├── index.html │ │ └── setting.html ├── job │ ├── check_clinet_ip_job.go │ ├── 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 │ ├── user.go │ └── xray.go ├── session │ └── session.go ├── translation │ ├── translate.en_US.toml │ ├── translate.zh_Hans.toml │ └── translate.zh_Hant.toml └── web.go ├── x-ui.service ├── x-ui.sh ├── xray ├── client_traffic.go ├── config.go ├── inbound.go ├── process.go └── traffic.go └── xray_core_config ├── xray core config.txt └── xui panel config block ir ads pushiran.txt /.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 x-ui 32 | value: |- 33 |
34 | 35 | ```console 36 | $ x-ui version 37 | # Paste output here 38 | ``` 39 | 40 |
41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | id: log 46 | attributes: 47 | label: x-ui log or xray log 48 | value: |- 49 |
50 | 51 | ```console 52 | # paste log here 53 | ``` 54 | 55 |
56 | validations: 57 | required: true 58 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | workflow_dispatch: 6 | inputs: 7 | project: 8 | description: 'Project' 9 | required: true 10 | default: 11 | 12 | jobs: 13 | 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | submodules: true 22 | 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v1 25 | 26 | 27 | - name: Docker Hub login 28 | env: 29 | DOCKERHUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} 30 | DOCKERHUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }} 31 | run: | 32 | echo "${DOCKERHUB_TOKEN}" | docker login --username ${DOCKERHUB_USERNAME} --password-stdin 33 | 34 | - name: Set up Docker Buildx 35 | id: buildx 36 | uses: crazy-max/ghaction-docker-buildx@v1 37 | with: 38 | buildx-version: latest 39 | 40 | - name: Build Dockerfile 41 | env: 42 | DOCKERHUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} 43 | DOCKERHUB_REPO: ${{ secrets.DOCKER_HUB_REPOSITORY }} 44 | run: | 45 | docker buildx build \ 46 | --platform=linux/amd64,linux/arm64 \ 47 | --output "type=image,push=true" \ 48 | --file ./Dockerfile ./ \ 49 | --tag $(echo "${DOCKERHUB_USERNAME}" | tr '[:upper:]' '[:lower:]')/${{ github.event.inputs.project }}:latest 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release X-ui 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | workflow_dispatch: 7 | jobs: 8 | release: 9 | runs-on: ubuntu-18.04 10 | outputs: 11 | upload_url: ${{ steps.create_release.outputs.upload_url }} 12 | steps: 13 | - name: Create Release 14 | id: create_release 15 | uses: actions/create-release@v1 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }} 18 | with: 19 | tag_name: ${{ github.ref }} 20 | release_name: ${{ github.ref }} 21 | draft: true 22 | prerelease: true 23 | linuxamd64build: 24 | name: build x-ui amd64 version 25 | needs: release 26 | runs-on: ubuntu-18.04 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Set up Go 30 | uses: actions/setup-go@v2 31 | with: 32 | go-version: 1.18 33 | - name: build linux amd64 version 34 | run: | 35 | CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go 36 | mkdir x-ui 37 | cp xui-release x-ui/xui-release 38 | cp x-ui.service x-ui/x-ui.service 39 | cp x-ui.sh x-ui/x-ui.sh 40 | cd x-ui 41 | mv xui-release x-ui 42 | mkdir bin 43 | cd bin 44 | wget https://github.com/hossinasaadi/Xray-core/releases/latest/download/Xray-linux-64.zip 45 | unzip Xray-linux-64.zip 46 | rm -f Xray-linux-64.zip geoip.dat geosite.dat 47 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat 48 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat 49 | mv xray xray-linux-amd64 50 | cd .. 51 | cd .. 52 | - name: package 53 | run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui 54 | - name: upload 55 | uses: actions/upload-release-asset@v1 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }} 58 | with: 59 | upload_url: ${{ needs.release.outputs.upload_url }} 60 | asset_path: x-ui-linux-amd64.tar.gz 61 | asset_name: x-ui-linux-amd64.tar.gz 62 | asset_content_type: application/gzip 63 | linuxarm64build: 64 | name: build x-ui arm64 version 65 | needs: release 66 | runs-on: ubuntu-18.04 67 | steps: 68 | - uses: actions/checkout@v2 69 | - name: Set up Go 70 | uses: actions/setup-go@v2 71 | with: 72 | go-version: 1.18 73 | - name: build linux arm64 version 74 | run: | 75 | sudo apt-get update 76 | sudo apt install gcc-aarch64-linux-gnu 77 | CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc go build -o xui-release -v main.go 78 | mkdir x-ui 79 | cp xui-release x-ui/xui-release 80 | cp x-ui.service x-ui/x-ui.service 81 | cp x-ui.sh x-ui/x-ui.sh 82 | cd x-ui 83 | mv xui-release x-ui 84 | mkdir bin 85 | cd bin 86 | wget https://github.com/hossinasaadi/Xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip 87 | unzip Xray-linux-arm64-v8a.zip 88 | rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat 89 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat 90 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat 91 | mv xray xray-linux-arm64 92 | cd .. 93 | cd .. 94 | - name: package 95 | run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui 96 | - name: upload 97 | uses: actions/upload-release-asset@v1 98 | env: 99 | GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }} 100 | with: 101 | upload_url: ${{ needs.release.outputs.upload_url }} 102 | asset_path: x-ui-linux-arm64.tar.gz 103 | asset_name: x-ui-linux-arm64.tar.gz 104 | asset_content_type: application/gzip 105 | linuxs390xbuild: 106 | name: build x-ui s390x version 107 | needs: release 108 | runs-on: ubuntu-18.04 109 | steps: 110 | - uses: actions/checkout@v2 111 | - name: Set up Go 112 | uses: actions/setup-go@v2 113 | with: 114 | go-version: 1.18 115 | - name: build linux s390x version 116 | run: | 117 | sudo apt-get update 118 | sudo apt install gcc-s390x-linux-gnu -y 119 | CGO_ENABLED=1 GOOS=linux GOARCH=s390x CC=s390x-linux-gnu-gcc go build -o xui-release -v main.go 120 | mkdir x-ui 121 | cp xui-release x-ui/xui-release 122 | cp x-ui.service x-ui/x-ui.service 123 | cp x-ui.sh x-ui/x-ui.sh 124 | cd x-ui 125 | mv xui-release x-ui 126 | mkdir bin 127 | cd bin 128 | wget https://github.com/hossinasaadi/Xray-core/releases/latest/download/Xray-linux-s390x.zip 129 | unzip Xray-linux-s390x.zip 130 | rm -f Xray-linux-s390x.zip geoip.dat geosite.dat 131 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat 132 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat 133 | mv xray xray-linux-s390x 134 | cd .. 135 | cd .. 136 | - name: package 137 | run: tar -zcvf x-ui-linux-s390x.tar.gz x-ui 138 | - name: upload 139 | uses: actions/upload-release-asset@v1 140 | env: 141 | GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }} 142 | with: 143 | upload_url: ${{ needs.release.outputs.upload_url }} 144 | asset_path: x-ui-linux-s390x.tar.gz 145 | asset_name: x-ui-linux-s390x.tar.gz 146 | asset_content_type: application/gzip 147 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | tmp 3 | bin/xray-darwin-arm64 4 | bin/config.json 5 | dist/ 6 | x-ui-*.tar.gz 7 | /x-ui 8 | /release.sh 9 | .sync* 10 | main 11 | release/ 12 | access.log 13 | .cache 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest AS builder 2 | WORKDIR /root 3 | COPY . . 4 | RUN go build main.go 5 | 6 | 7 | FROM debian:11-slim 8 | LABEL org.opencontainers.image.authors="hossin.asaadi77@gmail.com" 9 | ENV DEBIAN_FRONTEND noninteractive 10 | RUN apt-get update && apt-get install -y --no-install-recommends -y ca-certificates \ 11 | && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 12 | ENV TZ=Asia/Shanghai 13 | WORKDIR /root 14 | COPY --from=builder /root/main /root/x-ui 15 | COPY ./bin/. /root/bin/. 16 | VOLUME [ "/etc/x-ui" ] 17 | CMD [ "./x-ui" ] 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # x-ui-windows 2 | **binary executable of v2ray x-ui web panel for windows 64-bit** 3 | 4 | download from : 5 | 6 | https://github.com/kyumath/x-ui-windows/releases 7 | 8 | forked from : 9 | 10 | https://github.com/HexaSoftwareTech/x-ui (V0.5.4) 11 | 12 | 13 | ------------------------------------------------------------------ 14 | # how to run : 15 | 16 | - download from "release" 17 | - double click on xui-main.exe 18 | - open browser [http://127.0.0.1:54321] 19 | - user = admin 20 | - pass = admin 21 | 22 | **windows firewall:** 23 | 24 | - allow xui-main.exe in windows firewall 25 | - allow xray-windows-amd64.exe in windows firewall 26 | 27 | **(optional) Block IR/China/Bittorrent/Ads/P0*n:** 28 | 29 | - paste configuration inside xray_core_config.txt into "panel setting" -> "xray configuration" to block these IPs/Sites 30 | 31 | **(optional) self-signed certificate:** 32 | 33 | - you can use fake cert inside self_certificate to bypass GFW censorship or make your own certificate using openssl.exe 34 | - client must check "allow Insecure" to accept these certificate because its not authorized by Root-CA 35 | 36 | **other usefull commands:** 37 | 38 | - xui-main.exe /? 39 | - xui-main.exe -h 40 | - xui-main.exe setting -show 41 | - xui-main.exe setting -port 12345 42 | - xui-main.exe setting -reset 43 | 44 | ------------------------------------------------------------------ 45 | # how to compile form source : 46 | 47 | - download mingw[w64devkit] and extract to C:\ 48 | - add "C:\w64devkit\bin" to path 49 | - download and install Go 1.19.3 50 | - add "C:\Program Files\Go\bin" to path 51 | - download and extract xui-windows source 52 | - cd to xui-windows directory and run : 53 | - go build -o main.exe main.go 54 | - rename main.exe to xui-main.exe 55 | 56 | -------------------------------------------------------------------- 57 | # changelog 58 | some minor changes applied in these file to get things ready for windows. 59 | - \web\service\server.go 60 | - \web\service\setting.go 61 | - \web\entity\entity.go 62 | 63 | --------------------------------------------------------------------- 64 | # known issue: 65 | - resetting xraycore from web panel isnt working (you should manually close app to restart core) 66 | - tcp/udp count set to 1 (linux api isnt available on windows) 67 | 68 | 69 | -------------------------------------------------------------------------------- /bin/geoip.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/bin/geoip.dat -------------------------------------------------------------------------------- /bin/geosite.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/bin/geosite.dat -------------------------------------------------------------------------------- /bin/wxray.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/bin/wxray.exe -------------------------------------------------------------------------------- /bin/xray-windows-amd64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/bin/xray-windows-amd64.exe -------------------------------------------------------------------------------- /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 GetDBPath() string { 49 | return fmt.Sprintf("/etc/%s/%s.db", GetName(), GetName()) 50 | } 51 | -------------------------------------------------------------------------------- /config/name: -------------------------------------------------------------------------------- 1 | x-ui -------------------------------------------------------------------------------- /config/version: -------------------------------------------------------------------------------- 1 | 0.5.3 2 | -------------------------------------------------------------------------------- /database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "gorm.io/driver/sqlite" 5 | "gorm.io/gorm" 6 | "gorm.io/gorm/logger" 7 | "io/fs" 8 | "os" 9 | "path" 10 | "x-ui/config" 11 | "x-ui/xray" 12 | "x-ui/database/model" 13 | ) 14 | 15 | var db *gorm.DB 16 | 17 | func initUser() error { 18 | err := db.AutoMigrate(&model.User{}) 19 | if err != nil { 20 | return err 21 | } 22 | var count int64 23 | err = db.Model(&model.User{}).Count(&count).Error 24 | if err != nil { 25 | return err 26 | } 27 | if count == 0 { 28 | user := &model.User{ 29 | Username: "admin", 30 | Password: "admin", 31 | } 32 | return db.Create(user).Error 33 | } 34 | return nil 35 | } 36 | 37 | func initInbound() error { 38 | return db.AutoMigrate(&model.Inbound{}) 39 | } 40 | 41 | func initSetting() error { 42 | return db.AutoMigrate(&model.Setting{}) 43 | } 44 | func initInboundClientIps() error { 45 | return db.AutoMigrate(&model.InboundClientIps{}) 46 | } 47 | func initClientTraffic() error { 48 | return db.AutoMigrate(&xray.ClientTraffic{}) 49 | } 50 | 51 | func InitDB(dbPath string) error { 52 | dir := path.Dir(dbPath) 53 | err := os.MkdirAll(dir, fs.ModeDir) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | var gormLogger logger.Interface 59 | 60 | if config.IsDebug() { 61 | gormLogger = logger.Default 62 | } else { 63 | gormLogger = logger.Discard 64 | } 65 | 66 | c := &gorm.Config{ 67 | Logger: gormLogger, 68 | } 69 | db, err = gorm.Open(sqlite.Open(dbPath), c) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | err = initUser() 75 | if err != nil { 76 | return err 77 | } 78 | err = initInbound() 79 | if err != nil { 80 | return err 81 | } 82 | err = initSetting() 83 | if err != nil { 84 | return err 85 | } 86 | err = initInboundClientIps() 87 | if err != nil { 88 | return err 89 | } 90 | err = initClientTraffic() 91 | if err != nil { 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | 98 | func GetDB() *gorm.DB { 99 | return db 100 | } 101 | 102 | func IsNotFound(err error) bool { 103 | return err == gorm.ErrRecordNotFound 104 | } 105 | -------------------------------------------------------------------------------- /database/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "x-ui/util/json_util" 6 | "x-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 | 26 | type Inbound struct { 27 | Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` 28 | UserId int `json:"-"` 29 | Up int64 `json:"up" form:"up"` 30 | Down int64 `json:"down" form:"down"` 31 | Total int64 `json:"total" form:"total"` 32 | Remark string `json:"remark" form:"remark"` 33 | Enable bool `json:"enable" form:"enable"` 34 | ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` 35 | ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` 36 | 37 | // config part 38 | Listen string `json:"listen" form:"listen"` 39 | Port int `json:"port" form:"port" gorm:"unique"` 40 | Protocol Protocol `json:"protocol" form:"protocol"` 41 | Settings string `json:"settings" form:"settings"` 42 | StreamSettings string `json:"streamSettings" form:"streamSettings"` 43 | Tag string `json:"tag" form:"tag" gorm:"unique"` 44 | Sniffing string `json:"sniffing" form:"sniffing"` 45 | } 46 | type InboundClientIps struct { 47 | Id int `json:"id" gorm:"primaryKey;autoIncrement"` 48 | ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"` 49 | Ips string `json:"ips" form:"ips"` 50 | } 51 | 52 | func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { 53 | listen := i.Listen 54 | if listen != "" { 55 | listen = fmt.Sprintf("\"%v\"", listen) 56 | } 57 | return &xray.InboundConfig{ 58 | Listen: json_util.RawMessage(listen), 59 | Port: i.Port, 60 | Protocol: string(i.Protocol), 61 | Settings: json_util.RawMessage(i.Settings), 62 | StreamSettings: json_util.RawMessage(i.StreamSettings), 63 | Tag: i.Tag, 64 | Sniffing: json_util.RawMessage(i.Sniffing), 65 | } 66 | } 67 | 68 | type Setting struct { 69 | Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` 70 | Key string `json:"key" form:"key"` 71 | Value string `json:"value" form:"value"` 72 | } 73 | type Client struct { 74 | ID string `json:"id"` 75 | AlterIds uint16 `json:"alterId"` 76 | Email string `json:"email"` 77 | LimitIP int `json:"limitIp"` 78 | Security string `json:"security"` 79 | TotalGB int64 `json:"totalGB" form:"totalGB"` 80 | ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` 81 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | xui: 4 | image: hossinasaadi/x-ui 5 | container_name: x-ui 6 | volumes: 7 | - $PWD/db/:/etc/x-ui/ 8 | - $PWD/cert/:/root/cert/ 9 | restart: unless-stopped 10 | network_mode: host 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module x-ui 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect 8 | github.com/Workiva/go-datastructures v1.0.53 9 | github.com/gin-contrib/sessions v0.0.3 10 | github.com/gin-gonic/gin v1.7.1 11 | github.com/go-cmd/cmd v1.4.1 // indirect 12 | github.com/go-ole/go-ole v1.2.5 // indirect 13 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 14 | github.com/nicksnyder/go-i18n/v2 v2.1.2 15 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 16 | github.com/robfig/cron/v3 v3.0.1 17 | github.com/shirou/gopsutil v3.21.3+incompatible 18 | github.com/tklauser/go-sysconf v0.3.5 // indirect 19 | github.com/xtls/xray-core v1.4.2 20 | go.uber.org/atomic v1.7.0 21 | golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 // indirect 22 | golang.org/x/text v0.3.6 23 | google.golang.org/grpc v1.38.0 24 | gopkg.in/yaml.v2 v2.4.0 // indirect 25 | gorm.io/driver/sqlite v1.1.4 26 | gorm.io/gorm v1.21.9 27 | ) 28 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | red='\033[0;31m' 4 | green='\033[0;32m' 5 | yellow='\033[0;33m' 6 | plain='\033[0m' 7 | 8 | cur_dir=$(pwd) 9 | 10 | # check root 11 | [[ $EUID -ne 0 ]] && echo -e "${red}Fatal error:${plain} Please run this script with root privilege \n " && exit 1 12 | 13 | # check os 14 | if [[ -f /etc/redhat-release ]]; then 15 | release="centos" 16 | elif cat /etc/issue | grep -Eqi "debian"; then 17 | release="debian" 18 | elif cat /etc/issue | grep -Eqi "ubuntu"; then 19 | release="ubuntu" 20 | elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then 21 | release="centos" 22 | elif cat /proc/version | grep -Eqi "debian"; then 23 | release="debian" 24 | elif cat /proc/version | grep -Eqi "ubuntu"; then 25 | release="ubuntu" 26 | elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then 27 | release="centos" 28 | else 29 | echo -e "${red} check system OS failed,please contact with author! ${plain}\n" && exit 1 30 | fi 31 | 32 | arch=$(arch) 33 | 34 | if [[ $arch == "x86_64" || $arch == "x64" || $arch == "amd64" ]]; then 35 | arch="amd64" 36 | elif [[ $arch == "aarch64" || $arch == "arm64" ]]; then 37 | arch="arm64" 38 | elif [[ $arch == "s390x" ]]; then 39 | arch="s390x" 40 | else 41 | arch="amd64" 42 | echo -e "${red} Fail to check system arch,will use default arch here: ${arch}${plain}" 43 | fi 44 | 45 | echo "arch: ${arch}" 46 | 47 | if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then 48 | echo "x-ui dosen't support 32bit(x86) system,please use 64 bit operating system(x86_64) instead,if there is something wrong,plz let me know" 49 | exit -1 50 | fi 51 | 52 | os_version="" 53 | 54 | # os version 55 | if [[ -f /etc/os-release ]]; then 56 | os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release) 57 | fi 58 | if [[ -z "$os_version" && -f /etc/lsb-release ]]; then 59 | os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release) 60 | fi 61 | 62 | if [[ x"${release}" == x"centos" ]]; then 63 | if [[ ${os_version} -le 6 ]]; then 64 | echo -e "${red} please use CentOS 7 or higher version ${plain}\n" && exit 1 65 | fi 66 | elif [[ x"${release}" == x"ubuntu" ]]; then 67 | if [[ ${os_version} -lt 16 ]]; then 68 | echo -e "${red} please use Ubuntu 16 or higher version ${plain}\n" && exit 1 69 | fi 70 | elif [[ x"${release}" == x"debian" ]]; then 71 | if [[ ${os_version} -lt 8 ]]; then 72 | echo -e "${red} please use Debian 8 or higher version ${plain}\n" && exit 1 73 | fi 74 | fi 75 | 76 | install_base() { 77 | if [[ x"${release}" == x"centos" ]]; then 78 | yum install wget curl tar -y 79 | else 80 | apt install wget curl tar -y 81 | fi 82 | } 83 | 84 | #This function will be called when user installed x-ui out of sercurity 85 | config_after_install() { 86 | echo -e "${yellow} Install/update finished need to modify panel settings out of security ${plain}" 87 | read -p "are you continue,if you type n will skip this at this time[y/n]": config_confirm 88 | if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then 89 | read -p "please set up your username:" config_account 90 | echo -e "${yellow}your username will be:${config_account}${plain}" 91 | read -p "please set up your password:" config_password 92 | echo -e "${yellow}your password will be:${config_password}${plain}" 93 | read -p "please set up the panel port:" config_port 94 | echo -e "${yellow}your panel port is:${config_port}${plain}" 95 | echo -e "${yellow}initializing,wait some time here...${plain}" 96 | /usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} 97 | echo -e "${yellow}account name and password set down!${plain}" 98 | /usr/local/x-ui/x-ui setting -port ${config_port} 99 | echo -e "${yellow}panel port set down!${plain}" 100 | else 101 | echo -e "${red}Canceled, all setting items are default settings${plain}" 102 | fi 103 | } 104 | 105 | install_x-ui() { 106 | systemctl stop x-ui 107 | cd /usr/local/ 108 | 109 | if [ $# == 0 ]; then 110 | last_version=$(curl -Ls "https://api.github.com/repos/hossinasaadi/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') 111 | if [[ ! -n "$last_version" ]]; then 112 | echo -e "${red}refresh x-ui version failed,it may due to Github API restriction,please try it later${plain}" 113 | exit 1 114 | fi 115 | echo -e "get x-ui latest version succeed: ${last_version}, begin to install..." 116 | wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/hossinasaadi/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz 117 | if [[ $? -ne 0 ]]; then 118 | echo -e "${red}dowanload x-ui failed,please be sure that your server can access Github ${plain}" 119 | exit 1 120 | fi 121 | else 122 | last_version=$1 123 | url="https://github.com/hossinasaadi/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz" 124 | echo -e "begin to install x-ui v$1" 125 | wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url} 126 | if [[ $? -ne 0 ]]; then 127 | echo -e "${red}dowanload x-ui v$1 failed,please check the verison exists${plain}" 128 | exit 1 129 | fi 130 | fi 131 | 132 | if [[ -e /usr/local/x-ui/ ]]; then 133 | rm /usr/local/x-ui/ -rf 134 | fi 135 | 136 | tar zxvf x-ui-linux-${arch}.tar.gz 137 | rm x-ui-linux-${arch}.tar.gz -f 138 | cd x-ui 139 | chmod +x x-ui bin/xray-linux-${arch} 140 | cp -f x-ui.service /etc/systemd/system/ 141 | wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/hossinasaadi/x-ui/main/x-ui.sh 142 | chmod +x /usr/local/x-ui/x-ui.sh 143 | chmod +x /usr/bin/x-ui 144 | config_after_install 145 | #echo -e "如果是全新安装,默认网页端口为 ${green}54321${plain},用户名和密码默认都是 ${green}admin${plain}" 146 | #echo -e "请自行确保此端口没有被其他程序占用,${yellow}并且确保 54321 端口已放行${plain}" 147 | # echo -e "若想将 54321 修改为其它端口,输入 x-ui 命令进行修改,同样也要确保你修改的端口也是放行的" 148 | #echo -e "" 149 | #echo -e "如果是更新面板,则按你之前的方式访问面板" 150 | #echo -e "" 151 | systemctl daemon-reload 152 | systemctl enable x-ui 153 | systemctl start x-ui 154 | echo -e "${green}x-ui v${last_version}${plain} install finished,it is working now..." 155 | echo -e "" 156 | echo -e "x-ui control menu usages: " 157 | echo -e "----------------------------------------------" 158 | echo -e "x-ui - Enter Admin menu" 159 | echo -e "x-ui start - Start x-ui" 160 | echo -e "x-ui stop - Stop x-ui" 161 | echo -e "x-ui restart - Restart x-ui" 162 | echo -e "x-ui status - Show x-ui status" 163 | echo -e "x-ui enable - Enable x-ui on system startup" 164 | echo -e "x-ui disable - Disable x-ui on system startup" 165 | echo -e "x-ui log - Check x-ui logs" 166 | echo -e "x-ui v2-ui - Migrate v2-ui Account data to x-ui" 167 | echo -e "x-ui update - Update x-ui" 168 | echo -e "x-ui install - Install x-ui" 169 | echo -e "x-ui uninstall - Uninstall x-ui" 170 | echo -e "----------------------------------------------" 171 | } 172 | 173 | echo -e "${green}excuting...${plain}" 174 | install_base 175 | install_x-ui $1 176 | -------------------------------------------------------------------------------- /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("x-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/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/media/2022-04-04_141259.png -------------------------------------------------------------------------------- /media/2022-04-17_110907.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/media/2022-04-17_110907.png -------------------------------------------------------------------------------- /media/2022-04-17_111321.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/media/2022-04-17_111321.png -------------------------------------------------------------------------------- /media/2022-04-17_111705.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/media/2022-04-17_111705.png -------------------------------------------------------------------------------- /media/2022-04-17_111910.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/media/2022-04-17_111910.png -------------------------------------------------------------------------------- /media/bda84fbc2ede834deaba1c173a932223.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/media/bda84fbc2ede834deaba1c173a932223.png -------------------------------------------------------------------------------- /media/d13ffd6a73f938d1037d0708e31433bf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/media/d13ffd6a73f938d1037d0708e31433bf.png -------------------------------------------------------------------------------- /self_certificate/Win64OpenSSL_Light-3_0_7.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/self_certificate/Win64OpenSSL_Light-3_0_7.exe -------------------------------------------------------------------------------- /self_certificate/apple.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEATCCAumgAwIBAgIUN05XUlOsLYCkQpwgC+SeWUllE/4wDQYJKoZIhvcNAQEL 3 | BQAwgY8xCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApjYWxpZm9ybmlhMRAwDgYDVQQH 4 | DAdzaWxpY29uMRIwEAYDVQQKDAlhcHBsZSBJbmMxETAPBgNVBAsMCHNvZnR3YXJl 5 | MRIwEAYDVQQDDAlhcHBsZS5jb20xHjAcBgkqhkiG9w0BCQEWD2FkbWluQGFwcGxl 6 | LmNvbTAeFw0yMzAxMDMxNjMxMTdaFw0zMjEyMzExNjMxMTdaMIGPMQswCQYDVQQG 7 | EwJ1czETMBEGA1UECAwKY2FsaWZvcm5pYTEQMA4GA1UEBwwHc2lsaWNvbjESMBAG 8 | A1UECgwJYXBwbGUgSW5jMREwDwYDVQQLDAhzb2Z0d2FyZTESMBAGA1UEAwwJYXBw 9 | bGUuY29tMR4wHAYJKoZIhvcNAQkBFg9hZG1pbkBhcHBsZS5jb20wggEiMA0GCSqG 10 | SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2B5U5lrAY19eSYqcsN31nJ4z6IXnjFS5x 11 | FpHblk3wlJvZrnBVPWl05OF9C53qoZIW/1TjDqwBpVbElIh2jcio3RvVOb8yTtOV 12 | +NmLIhO44bxJCtKnvBd2fXwPzfL2qMUFKTNvahwtbYUcD5mwX9jGMkIbtueAQduq 13 | Nr0fdNA1eNEt7DobLvyAjJJItOOx3GbaTr9Ugigou5BDXd1O+M1Sg5O+2fJtnq1E 14 | RXg3aCr56ah8vb95Tipe00OgyHrfTtt31a8oN+z4sN11B7zn8mD51V3Ugw66n1WL 15 | mMP6hQuy7BFyHQSWix/UAhBKvugFZKbMIdYC9UyejsU5Y3+AYY0NAgMBAAGjUzBR 16 | MB0GA1UdDgQWBBRcW1lnJmWDLAzl2JaTAaUqhb3XJTAfBgNVHSMEGDAWgBRcW1ln 17 | JmWDLAzl2JaTAaUqhb3XJTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA 18 | A4IBAQCkHMftxwnz9f0JdjphB3QKEq3Da+KLxPE5XfxjWdkqKajtkJb4nNKDwn6B 19 | CDKLg8MyoKngr7JYFtt/+HUSc/QjifWN27Rs/8u5IDawiRt+C9Sx+o0gOakGzwIo 20 | JubQR843/flx/OwoZg1R7QVOHN3pyqJt/RKFA1ckiRQGJBqXgiyBNfQCj+htkpRB 21 | glDdRiJVW+EAO88BX/mIVmFRR5tr8bMg86hck2lEDa5zvMRa+gfaitB6c4WMIpfA 22 | Ehpu5ccbCU1ExaXKHSFO2X0BIAC1GedR+/inqDjNkyPZCVIWsNwKJmr6gUT5uQmJ 23 | ZGvRcJE6R6v5d05O1T1x5hiwE4jl 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /self_certificate/apple.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC2B5U5lrAY19eS 3 | YqcsN31nJ4z6IXnjFS5xFpHblk3wlJvZrnBVPWl05OF9C53qoZIW/1TjDqwBpVbE 4 | lIh2jcio3RvVOb8yTtOV+NmLIhO44bxJCtKnvBd2fXwPzfL2qMUFKTNvahwtbYUc 5 | D5mwX9jGMkIbtueAQduqNr0fdNA1eNEt7DobLvyAjJJItOOx3GbaTr9Ugigou5BD 6 | Xd1O+M1Sg5O+2fJtnq1ERXg3aCr56ah8vb95Tipe00OgyHrfTtt31a8oN+z4sN11 7 | B7zn8mD51V3Ugw66n1WLmMP6hQuy7BFyHQSWix/UAhBKvugFZKbMIdYC9UyejsU5 8 | Y3+AYY0NAgMBAAECggEABc5vHaOnYVt9Nl+sOnZYqzIxRodL1okF7riMFYeRW+O5 9 | pvNwBOANHZvxb2Yv9tyJhu9FG2sL5I+MGYAtbC6woGZRLXeGmEm4aRfDhLMlEugs 10 | c94DAzO2QeLV0sE8h06x3izGxb6KQI62SMNcoSolhYjz3+NxNf+jzKWrWbYFsDbo 11 | +Qc2tom83kZOJ5XOJ+MaztBhZ0drpgPGCNr5V/H9xT2ifTLjk1nl+VGQ3jFMBgqd 12 | A6GWu908ydzEzDTKc/ymLrCJOP/DTgycFSS+hUcGdmcyLSj46NmPXGlf8bKKx899 13 | rC7mYtubCoxxZqhJobcmoxU8iNq4GXW6oGolnsnRhwKBgQC7p+gHEaKmKG0j8Jpd 14 | mL/mjtkrbPSGMaZ5tb8jHwOjvcBsJVXbTTXOJpfyddzuGwvgBqYvXYX50KZh4WqF 15 | /cq3VdySGR3/mPp2Odnefcme7u0m2ILtlTFamTts6CKKdkC6GXLE6VmG+7TLW1mc 16 | tKLtVYlwIZht2/8zpTvwG+N14wKBgQD4UxypXOMsCIDbCinc5dSYD8x/MH++E875 17 | dBYZp2AK4HQUVTLthb9ZTZ2Uewx3bPaDTasFD/feX5KgHNp6RAWPLm/ow8p5vA6O 18 | sW8uMaOOAaRc5yoQJW7vIgiOG7F+UOcqE5bkSm6IWvedlT4xyE2XJWuvpo0vj5Pn 19 | xafU1abkTwKBgE4Z18v5XwRgluv6W4TQ6i1/DouwUST4zRyis+ZeJhmkA83+y/j1 20 | 7YrbLSpQO58tyod2vPvBpN0QJe9e7gLOWexr4906bu3OMY+7DB45pAxtzG36dqOD 21 | EINwevcctiOCi/XoWYfyxrwTTm/EmUtLMvQsQYNVuCchBNntz0BjmwuZAoGBAJa2 22 | 2s5v8fDnuE04/e90DnO8IwGPDfjvTM9aBY/MZh4z3D/69c3zPw+Ua++SPx68o2FN 23 | 4l5QMsBkgTlsSa2rWzRrmsBLFLiNysgQKMqAliO6G2fHZWAnWA0LOuYFKNThhWuk 24 | OBCWnD9l9qNN0VGloHH75D/XdhlHqx0geZxTj9dBAoGAOjBTkB8mRP85VopBgOah 25 | 1b74RDv+U7xZUjBD5wEUTtafWEr+I0PnwoWC4WSKHNrsBSO57LaYZHx0vhABf+gN 26 | JenhVyivTbX796Mt+V/JQX2DOplfv7ZxH57EGKBHwNyeTDiznNrRrdBkhabirRgX 27 | GM2t+mT8dQ8DtCjXaIpJ6Ao= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /self_certificate/google.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEBTCCAu2gAwIBAgIUexR4jILPur+lLUbNo4vYblGIZ/UwDQYJKoZIhvcNAQEL 3 | BQAwgZExCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApjYWxpZm9ybmlhMRAwDgYDVQQH 4 | DAdzaWxpY29uMRMwEQYDVQQKDApnb29nbGUgSW5jMRAwDgYDVQQLDAdzb2Z3YXJl 5 | MRMwEQYDVQQDDApnb29nbGUuY29tMR8wHQYJKoZIhvcNAQkBFhBhZG1pbkBnb29n 6 | bGUuY29tMB4XDTIzMDEwMzE2MjgxOFoXDTMyMTIzMTE2MjgxOFowgZExCzAJBgNV 7 | BAYTAnVzMRMwEQYDVQQIDApjYWxpZm9ybmlhMRAwDgYDVQQHDAdzaWxpY29uMRMw 8 | EQYDVQQKDApnb29nbGUgSW5jMRAwDgYDVQQLDAdzb2Z3YXJlMRMwEQYDVQQDDApn 9 | b29nbGUuY29tMR8wHQYJKoZIhvcNAQkBFhBhZG1pbkBnb29nbGUuY29tMIIBIjAN 10 | BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsxG9qYZpKhhP9vSPKWk5RQII/0T9 11 | qcJc3febizsEybODWUFVlBoWPD6U62eaGvp3wBnDtvfX+Y5pui8VyRarN/coNbZZ 12 | fEPnwqRmTlHDBditktKDoqjadXHJk8dZ6DBdSOFayuvizK0C0830IVDcIuPun9Aw 13 | T8OG9Njy2TJVMVoYJYvWg49sCI231vPk693CU8yFoIhsefog3KkqmcrKGXJw/7q+ 14 | LORoPF/xzNkA+9fQeiUcHZfJM1sp4DB6GBhHW/qNGPW45OIW6+KfDdB+AfK+xD8V 15 | M4nQQFdMaj0z/i8iANgneCwSePEUq8YgiNVKvsvgy7s1ktbeyqydutjLzQIDAQAB 16 | o1MwUTAdBgNVHQ4EFgQUfxhkKSXN8hLq3FhCuAYTyqmQ0j0wHwYDVR0jBBgwFoAU 17 | fxhkKSXN8hLq3FhCuAYTyqmQ0j0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B 18 | AQsFAAOCAQEAg4LY0s+RLBQbfrNq6xh65ZbAC20DfXbsp1cbwClcZDq76/XNJW0k 19 | 4nsoJMMsVgdBCW5Jo8pE9xT3skJI6ImQ56ZtiJ/SeeU25Ldz3yX0ZzeX2r0Wk3d9 20 | EwBR1ZfmmiTSzhebv8HwervpkCKo1rn8YfeCvjwxHrGwzZaiJ55f1hyuC0B88ywg 21 | XMiTkuSzyaklZ/2xNp2iQwMqdhFeBT+x6/CcggsxZydkQW0tNMBT32idgyrD0VUz 22 | tbPEjveKeTq1FVRF0PYFJCQocVpwkuS5E2t4u3JCrk7jT6gmjmhFU1sBvMjtAW1G 23 | 2Dz2pcjgvmsCL1TzHNw8CDYk8VoyFJEMMg== 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /self_certificate/google.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzEb2phmkqGE/2 3 | 9I8paTlFAgj/RP2pwlzd95uLOwTJs4NZQVWUGhY8PpTrZ5oa+nfAGcO299f5jmm6 4 | LxXJFqs39yg1tll8Q+fCpGZOUcMF2K2S0oOiqNp1ccmTx1noMF1I4VrK6+LMrQLT 5 | zfQhUNwi4+6f0DBPw4b02PLZMlUxWhgli9aDj2wIjbfW8+Tr3cJTzIWgiGx5+iDc 6 | qSqZysoZcnD/ur4s5Gg8X/HM2QD719B6JRwdl8kzWyngMHoYGEdb+o0Y9bjk4hbr 7 | 4p8N0H4B8r7EPxUzidBAV0xqPTP+LyIA2Cd4LBJ48RSrxiCI1Uq+y+DLuzWS1t7K 8 | rJ262MvNAgMBAAECggEAArwVh4VsiHX8Kxva1ccf26TNkBpyTG+5ZHeSOMJoHdTY 9 | ZvfDo1+2YqR+kTna4ItAk7w3JfnczwnYTSnb4iQp91pbdYV1kjQMb2n8cXQuAyci 10 | JrSohPLwUhPOMXXRJA0wkudNsDhQMvwzMX0Tinp7Inptdbd3W+AuKNCahybSpnEP 11 | e1yulgs2C40dK7wFJ3ednBxAJfl6N+hKBB2VkUU1A42UlYo86ZsTU2zAKepeRp1C 12 | jbL5MHQqbWgiaMa/ifGE/Xzm05WhAj23TLDL13gCfQm1iyfilGcSkuFpKfQgXFNJ 13 | UJUhao98VDYHdKK/Mi9rSHX02c2Hep6Pr/Rem8k74QKBgQDFI7eyEyrOv4r2ehO9 14 | l83ZvNn/Oqm4SxyJ7YVTbaEef/SxP4R29SSx03fIRJ1z9NmXhtPYybdIAmSoRvi2 15 | DgBLBR3cNZASSUomG3mYxdTd1+hDwrDrIG1Um7Rbae9OwkEHmvNDV+O9vuWW3laq 16 | Cj3YNtAIA4IV3/QuWr1+RTr7oQKBgQDoiNU+Hxjx8x81P+dyfHrdBFGQcE4V4C6l 17 | j//F70+JVl/XiXY9a4wQD2TwnQQrjjZMYWOKVa4O4Qav4L8TYBCOOsiZzYjTd1us 18 | u2RoR6gsjgoJYGOFmrBppvjSjxFRiMYNfDcGoIWHmdkhFvldnOB4cJ39Y/lNx5Vc 19 | VgvpTsTArQKBgAEMwKStH3Q8ZuQLIGHJFiqLq+mFCXRWyg8d+bDJC6ua6NWszerI 20 | QWrFAoVJgEEm06XP7P7hjB1RDfIrdpWZm3zKyS7sBhli1IdBbDNZElogFTePKNwH 21 | ry1dKue1t7WGqUv+Ej+Qj0DqbFSSWInNRikJmbABPMcbCzTX/NxYSAkhAoGAK5FP 22 | mtcKvnJV241AmRO3lWd0vMI97x1UwBJt0rEWzt2PEXwg2jsnP+8Sc36HxH1dsQvi 23 | MY4CEamZriRGyKOMFonvAdofk7deRsvJcTxTxx1Jxh34J5UnDnSJVDIYBZXBvc8T 24 | JDu5KQHrS6bZS+/Tot0+zLYJhMgrNOSuUKVAefUCgYEAwm5I/mNWx+BBpVEhF/a/ 25 | tzlqwlFd3MInXQV34l71hzSok12sTndEjwxwaNbN9E13t7dmn9zULpfE6kWe8j/N 26 | YV+50zexJEasErvX6hhonYYn7uPVe2iuQssJV0W++nADmDEIsWO7HwTbL5gpzJug 27 | ja2xVowwHWs5HdzZSVxGpvI= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /self_certificate/help.txt: -------------------------------------------------------------------------------- 1 | :: https://www.baeldung.com/openssl-self-signed-cert 2 | :: https://www.digitalocean.com/community/tutorials/how-to-create-a-self-signed-ssl-certificate-for-nginx-in-ubuntu-20-04-1 3 | 4 | first install openssl on windows and add "C:\Program Files\OpenSSL-Win64\bin\" to path 5 | then run this single-line command and follow instruction: 6 | 7 | 8 | openssl req -newkey rsa:2048 -nodes -keyout domain.key -x509 -days 3650 -out domain.crt 9 | 10 | 11 | 12 | rename domain.key and domain.crt with whatever you want 13 | note: .crt and .cer extension are equivalent (no difference between but 14 | you can convert between encodings by double-click->details->copy to file) 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /self_certificate/yahoo.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID+TCCAuGgAwIBAgIUNOQLXAV9jLFNYnF8DAQH1p4EXx8wDQYJKoZIhvcNAQEL 3 | BQAwgYsxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApjYWxpZm9ybmlhMRAwDgYDVQQH 4 | DAdzaWxpY29uMQ4wDAYDVQQKDAV5YWhvbzERMA8GA1UECwwIc29mdHdhcmUxEjAQ 5 | BgNVBAMMCXlhaG9vLmNvbTEeMBwGCSqGSIb3DQEJARYPYWRtaW5AeWFob28uY29t 6 | MB4XDTIzMDEwMzE2Mjk1MloXDTMyMTIzMTE2Mjk1MlowgYsxCzAJBgNVBAYTAnVz 7 | MRMwEQYDVQQIDApjYWxpZm9ybmlhMRAwDgYDVQQHDAdzaWxpY29uMQ4wDAYDVQQK 8 | DAV5YWhvbzERMA8GA1UECwwIc29mdHdhcmUxEjAQBgNVBAMMCXlhaG9vLmNvbTEe 9 | MBwGCSqGSIb3DQEJARYPYWRtaW5AeWFob28uY29tMIIBIjANBgkqhkiG9w0BAQEF 10 | AAOCAQ8AMIIBCgKCAQEAnT3ZAtLnuG3/Qe4iqltpJw2OVUMm6SecRy6AQ/sdBk9K 11 | RNbJ+nXr1bWSknnJDqKpNxnasUYAX5kcrUtfxiFY4661KK5matjqs9nxCaTF+2+g 12 | Zc+bu/cqKfsw8ZMAr/nRDkcx+cFoolN7HnjT6eycFEMwnWctdlKYg/3dq6PRhb3m 13 | hhNfgcBzZ3JcJ1fbHCtp53eLBsYKunO1A9JryAm0lTgMfUOm4GhmYaHjAXklm0JA 14 | h0D+JXx9FufoffobY7/GEuWWiUYk1KWfuv5aYxrd61N74vB5TJOKA6C9HCTapnOK 15 | m8vIankVdVRqnveXWE7gE7mK9gUU+vtT4gT9FMpN+wIDAQABo1MwUTAdBgNVHQ4E 16 | FgQUxF69W2HF953o9HS/WW0cEa497gMwHwYDVR0jBBgwFoAUxF69W2HF953o9HS/ 17 | WW0cEa497gMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHR3K 18 | T6AiT8GdzMLCPZDK1Zfs6T5o3A16GVfuxdZ3aoHikRJjz+Dr9B2sTLx7PtEHjFIk 19 | DtovgB2dlZpmrtC8CQ6u7BbHbkaT3YFDTjArgkTbZV6K3+RRiN4AXFEjQNB60eZ3 20 | cojkRjf+Gzkg0EnOMMys55Zb5YSPZJakQK3oyjnZ57yEXEkVbj2ls3VLJJZJTzMo 21 | eqvY5GGj6l67latyk5PWcj1QxKK1VH4fMd31PxyCBV9nY02NzKMFoJNxaJpvuipp 22 | sJED4P7p08pbEHVKrNBQBHVRG5pNjlSA7AQd3d4ZGSBVfxhKECGON33ooozaxOVy 23 | O67UWmU3W2QfOdI3Aw== 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /self_certificate/yahoo.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCdPdkC0ue4bf9B 3 | 7iKqW2knDY5VQybpJ5xHLoBD+x0GT0pE1sn6devVtZKSeckOoqk3GdqxRgBfmRyt 4 | S1/GIVjjrrUormZq2Oqz2fEJpMX7b6Blz5u79yop+zDxkwCv+dEORzH5wWiiU3se 5 | eNPp7JwUQzCdZy12UpiD/d2ro9GFveaGE1+BwHNnclwnV9scK2nnd4sGxgq6c7UD 6 | 0mvICbSVOAx9Q6bgaGZhoeMBeSWbQkCHQP4lfH0W5+h9+htjv8YS5ZaJRiTUpZ+6 7 | /lpjGt3rU3vi8HlMk4oDoL0cJNqmc4qby8hqeRV1VGqe95dYTuATuYr2BRT6+1Pi 8 | BP0Uyk37AgMBAAECggEAAQpDMLcXbEth+FwsY4cWQ3ld+ydmDv3BlWDz3T9AemW0 9 | 3o6zKFJ2XGQGk6x1jYC68M/dd/W4E+nN3EGW4RcgYOUrcpjwuX001Dq5UvCBlL+w 10 | xmugopYoc72jHb30r7VN0jPbW0URvZ0iv9lZwX7cq2+kVy4zCUjTlG/t8vAy9xj/ 11 | UBgx9pcR4UTbwrL9cKvHBYN0b2PWw1cbmS6J/hLBDyqKnYIufuDttDo6R8AhmE0E 12 | IcqDgNWZ5Ldi6f/gTgfsSOrr6n5aVgxTFyku1w0n+sJ/GQXR5bqVB+xRyCiRr3VP 13 | 66M069pKYKLCNLpXr0Dv9mabz55PU+7O21w4sVVNLQKBgQDVUoo8nfyB1GpeBo6s 14 | mowix5aD5QL1SLq1dozRVGVBAsuhW4ZQKHk/kEdTor9k+O+vRzdCGKNiNtUXXXKu 15 | vyxNBJfWRh8IC4t3sWxJJzcOjeSJGOWZp+BhaK7+62fc/LN3iRo8cR1d6m5DE/yw 16 | WqSrH3tZejq0EvkQ8IGM6aqLJwKBgQC8sxaJeb5Z5nO/yrbK56hRSYa0jDFsvRfR 17 | BfTkNANzUuF5khADHwdQp3cyX15PSkQjhe5lVImK5ArEf29T5Rscb1N1m97fIeJE 18 | r/Ek4PLZv8XXnOolTG0GpV38gW9tQmgnxY19LOfVtCgX5sffQczr0oo/hcIkhxRl 19 | r58pkP77DQKBgQCll1n5bgYJ3hLnhB2SfLGVeesr3UTrlJZisHDlOacTzRC2464i 20 | YRilr+C+WpJJ1jdwj06kvy1s2vWVGpQgdAP2kT8wNOR3wUZ1FkPhkVBauuiRR8NL 21 | AJbb5oAZIDFSyNKjUskL7B6Ivabz6Pr5BpPWxixhK9GP4T532Mon0WbydwKBgQCA 22 | JfNJO5eNABddibJny7kgMxm7JXYvTZ+OAUiVbclmGUQsxof/RqQ2HvSxDdlDacEq 23 | irsir62U2FeDqLOXOwEVZ3k7ZraNvDoowX+3ttjWDgmy6sqMuAFWwRk58UEV50JK 24 | uDvlZYiwNeAAk2brdIlRIbNnflQhcGbmw8UIeoLfRQKBgQDGhk2MNYbItIvEiT3Z 25 | 4TdXT5mLIG6MXeaukvGiq1AVe4d3cc9Xbrgiv75tELJdIvK0EjkfrMMrmCjvDOvD 26 | Fle6XJzii1mi/8uojKvpru8hwHmNxX8Duqs/89cYSo/pwjQYIM6N64Wsz78nlK0u 27 | 3pdT2uUWikeSyizUTIHdrbmx8A== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /util/common/err.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "x-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 | -------------------------------------------------------------------------------- /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/stringUtil.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "sort" 4 | 5 | func IsSubString(target string, str_array []string) bool { 6 | sort.Strings(str_array) 7 | index := sort.SearchStrings(str_array, target) 8 | return index < len(str_array) && str_array[index] == target 9 | } 10 | -------------------------------------------------------------------------------- /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/GFW-knocker/x-ui-windows/f3ac0ce4b55e05fe796689236ddc9d51ef279ec6/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/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/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 "x-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 | "x-ui/config" 6 | "x-ui/database" 7 | "x-ui/database/model" 8 | "x-ui/util/common" 9 | "x-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 x-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 x-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 x-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/css/custom.css: -------------------------------------------------------------------------------- 1 | #app { 2 | height: 100%; 3 | } 4 | 5 | .ant-space { 6 | width: 100%; 7 | } 8 | 9 | .ant-layout-sider-zero-width-trigger { 10 | display: none; 11 | } 12 | 13 | .ant-card { 14 | border-radius: 30px; 15 | } 16 | 17 | .ant-card-hoverable { 18 | cursor: auto; 19 | } 20 | 21 | .ant-card+.ant-card { 22 | margin-top: 20px; 23 | } 24 | 25 | .drawer-handle { 26 | position: absolute; 27 | top: 72px; 28 | width: 41px; 29 | height: 40px; 30 | cursor: pointer; 31 | z-index: 0; 32 | text-align: center; 33 | line-height: 40px; 34 | font-size: 16px; 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | background: #fff; 39 | right: -40px; 40 | box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15); 41 | border-radius: 0 4px 4px 0; 42 | } 43 | 44 | @media (min-width: 769px) { 45 | .drawer-handle { 46 | display: none; 47 | } 48 | } 49 | 50 | .fade-in-enter, .fade-in-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active { 51 | opacity: 0 52 | } 53 | 54 | .fade-in-linear-enter-active, .fade-in-linear-leave-active { 55 | -webkit-transition: opacity .2s linear; 56 | transition: opacity .2s linear 57 | } 58 | 59 | .fade-in-linear-enter-active, .fade-in-linear-leave-active { 60 | -webkit-transition: opacity .2s linear; 61 | transition: opacity .2s linear 62 | } 63 | 64 | .fade-in-enter-active, .fade-in-leave-active { 65 | -webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1); 66 | transition: all .3s cubic-bezier(.55, 0, .1, 1) 67 | } 68 | 69 | .zoom-in-center-enter-active, .zoom-in-center-leave-active { 70 | -webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1); 71 | transition: all .3s cubic-bezier(.55, 0, .1, 1) 72 | } 73 | 74 | .zoom-in-center-enter, .zoom-in-center-leave-active { 75 | opacity: 0; 76 | -webkit-transform: scaleX(0); 77 | transform: scaleX(0) 78 | } 79 | 80 | .zoom-in-top-enter-active, .zoom-in-top-leave-active { 81 | opacity: 1; 82 | -webkit-transform: scaleY(1); 83 | transform: scaleY(1); 84 | -webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1); 85 | transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1); 86 | transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1); 87 | transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1); 88 | -webkit-transform-origin: center top; 89 | transform-origin: center top 90 | } 91 | 92 | .zoom-in-top-enter, .zoom-in-top-leave-active { 93 | opacity: 0; 94 | -webkit-transform: scaleY(0); 95 | transform: scaleY(0) 96 | } 97 | 98 | .zoom-in-bottom-enter-active, .zoom-in-bottom-leave-active { 99 | opacity: 1; 100 | -webkit-transform: scaleY(1); 101 | transform: scaleY(1); 102 | -webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1); 103 | transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1); 104 | transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1); 105 | transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1); 106 | -webkit-transform-origin: center bottom; 107 | transform-origin: center bottom 108 | } 109 | 110 | .zoom-in-bottom-enter, .zoom-in-bottom-leave-active { 111 | opacity: 0; 112 | -webkit-transform: scaleY(0); 113 | transform: scaleY(0) 114 | } 115 | 116 | .zoom-in-left-enter-active, .zoom-in-left-leave-active { 117 | opacity: 1; 118 | -webkit-transform: scale(1, 1); 119 | transform: scale(1, 1); 120 | -webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1); 121 | transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1); 122 | transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1); 123 | transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1); 124 | -webkit-transform-origin: top left; 125 | transform-origin: top left 126 | } 127 | 128 | .zoom-in-left-enter, .zoom-in-left-leave-active { 129 | opacity: 0; 130 | -webkit-transform: scale(.45, .45); 131 | transform: scale(.45, .45) 132 | } 133 | 134 | .list-enter-active, .list-leave-active { 135 | -webkit-transition: all .3s; 136 | transition: all .3s 137 | } 138 | 139 | .list-enter, .list-leave-active { 140 | opacity: 0; 141 | -webkit-transform: translateY(-30px); 142 | transform: translateY(-30px) 143 | } 144 | 145 | .ant-progress-inner { 146 | background-color: #EBEEF5; 147 | } -------------------------------------------------------------------------------- /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/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 | config.data = Qs.stringify(config.data, { 7 | arrayFormat: 'repeat' 8 | }); 9 | return config; 10 | }, 11 | error => Promise.reject(error) 12 | ); -------------------------------------------------------------------------------- /web/assets/js/langs.js: -------------------------------------------------------------------------------- 1 | supportLangs = [ 2 | { 3 | name : "English", 4 | value : "en-US", 5 | icon : "🇺🇸" 6 | }, 7 | { 8 | name : "汉语", 9 | value : "zh-Hans", 10 | icon : "🇨🇳" 11 | }, 12 | ] 13 | 14 | function getLang(){ 15 | let lang = getCookie('lang') 16 | 17 | if (! lang){ 18 | if (window.navigator){ 19 | lang = window.navigator.language || window.navigator.userLanguage; 20 | 21 | if (isSupportLang(lang)){ 22 | setCookie('lang' , lang , 150) 23 | }else{ 24 | setCookie('lang' , 'en-US' , 150) 25 | window.location.reload(); 26 | } 27 | }else{ 28 | setCookie('lang' , 'en-US' , 150) 29 | window.location.reload(); 30 | } 31 | } 32 | 33 | return lang; 34 | } 35 | 36 | function setLang(lang){ 37 | 38 | if (!isSupportLang(lang)){ 39 | lang = 'en-US'; 40 | } 41 | 42 | setCookie('lang' , lang , 150) 43 | window.location.reload(); 44 | } 45 | 46 | function isSupportLang(lang){ 47 | for (l of supportLangs){ 48 | if (l.value === lang){ 49 | return true; 50 | } 51 | } 52 | 53 | return false; 54 | } 55 | 56 | 57 | 58 | function getCookie(cname) { 59 | let name = cname + "="; 60 | let decodedCookie = decodeURIComponent(document.cookie); 61 | let ca = decodedCookie.split(';'); 62 | for(let i = 0; i >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/api.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | type APIController struct { 7 | BaseController 8 | 9 | inboundController *InboundController 10 | settingController *SettingController 11 | } 12 | 13 | func NewAPIController(g *gin.RouterGroup) *APIController { 14 | a := &APIController{} 15 | a.initRouter(g) 16 | return a 17 | } 18 | 19 | func (a *APIController) initRouter(g *gin.RouterGroup) { 20 | g = g.Group("/xui/API/inbounds") 21 | g.Use(a.checkLogin) 22 | 23 | g.GET("/", a.inbounds) 24 | g.GET("/get/:id", a.inbound) 25 | g.POST("/add", a.addInbound) 26 | g.POST("/del/:id", a.delInbound) 27 | g.POST("/update/:id", a.updateInbound) 28 | 29 | 30 | a.inboundController = NewInboundController(g) 31 | } 32 | 33 | 34 | func (a *APIController) inbounds(c *gin.Context) { 35 | a.inboundController.getInbounds(c) 36 | } 37 | func (a *APIController) inbound(c *gin.Context) { 38 | a.inboundController.getInbound(c) 39 | } 40 | func (a *APIController) addInbound(c *gin.Context) { 41 | a.inboundController.addInbound(c) 42 | } 43 | func (a *APIController) delInbound(c *gin.Context) { 44 | a.inboundController.delInbound(c) 45 | } 46 | func (a *APIController) updateInbound(c *gin.Context) { 47 | a.inboundController.updateInbound(c) 48 | } 49 | -------------------------------------------------------------------------------- /web/controller/base.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | "x-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, I18n(c , "pages.login.loginAgain")) 16 | } else { 17 | c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) 18 | } 19 | c.Abort() 20 | } else { 21 | c.Next() 22 | } 23 | } 24 | 25 | 26 | func I18n(c *gin.Context , name string) string{ 27 | anyfunc, _ := c.Get("I18n") 28 | i18n, _ := anyfunc.(func(key string, params ...string) (string, error)) 29 | 30 | message, _ := i18n(name) 31 | 32 | return message; 33 | } 34 | -------------------------------------------------------------------------------- /web/controller/inbound.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "strconv" 7 | "x-ui/database/model" 8 | "x-ui/logger" 9 | "x-ui/web/global" 10 | "x-ui/web/service" 11 | "x-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 | g.POST("/clientIps/:email", a.getClientIps) 35 | g.POST("/clearClientIps/:email", a.clearClientIps) 36 | g.POST("/resetClientTraffic/:email", a.resetClientTraffic) 37 | 38 | 39 | } 40 | 41 | func (a *InboundController) startTask() { 42 | webServer := global.GetWebServer() 43 | c := webServer.GetCron() 44 | c.AddFunc("@every 10s", func() { 45 | if a.xrayService.IsNeedRestartAndSetFalse() { 46 | err := a.xrayService.RestartXray(false) 47 | if err != nil { 48 | logger.Error("restart xray failed:", err) 49 | } 50 | } 51 | }) 52 | } 53 | 54 | func (a *InboundController) getInbounds(c *gin.Context) { 55 | user := session.GetLoginUser(c) 56 | inbounds, err := a.inboundService.GetInbounds(user.Id) 57 | if err != nil { 58 | jsonMsg(c, I18n(c , "pages.inbounds.toasts.obtain"), err) 59 | return 60 | } 61 | jsonObj(c, inbounds, nil) 62 | } 63 | func (a *InboundController) getInbound(c *gin.Context) { 64 | id, err := strconv.Atoi(c.Param("id")) 65 | if err != nil { 66 | jsonMsg(c, I18n(c , "get"), err) 67 | return 68 | } 69 | inbound, err := a.inboundService.GetInbound(id) 70 | if err != nil { 71 | jsonMsg(c, I18n(c , "pages.inbounds.toasts.obtain"), err) 72 | return 73 | } 74 | jsonObj(c, inbound, nil) 75 | } 76 | 77 | func (a *InboundController) addInbound(c *gin.Context) { 78 | inbound := &model.Inbound{} 79 | err := c.ShouldBind(inbound) 80 | if err != nil { 81 | jsonMsg(c, I18n(c , "pages.inbounds.addTo"), err) 82 | return 83 | } 84 | user := session.GetLoginUser(c) 85 | inbound.UserId = user.Id 86 | inbound.Enable = true 87 | inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) 88 | inbound, err = a.inboundService.AddInbound(inbound) 89 | jsonMsgObj(c, I18n(c , "pages.inbounds.addTo"), inbound, err) 90 | if err == nil { 91 | a.xrayService.SetToNeedRestart() 92 | } 93 | } 94 | 95 | func (a *InboundController) delInbound(c *gin.Context) { 96 | id, err := strconv.Atoi(c.Param("id")) 97 | if err != nil { 98 | jsonMsg(c, I18n(c , "delete"), err) 99 | return 100 | } 101 | err = a.inboundService.DelInbound(id) 102 | jsonMsgObj(c, I18n(c , "delete"), id, err) 103 | if err == nil { 104 | a.xrayService.SetToNeedRestart() 105 | } 106 | } 107 | 108 | func (a *InboundController) updateInbound(c *gin.Context) { 109 | id, err := strconv.Atoi(c.Param("id")) 110 | if err != nil { 111 | jsonMsg(c, I18n(c , "pages.inbounds.revise"), err) 112 | return 113 | } 114 | inbound := &model.Inbound{ 115 | Id: id, 116 | } 117 | err = c.ShouldBind(inbound) 118 | if err != nil { 119 | jsonMsg(c, I18n(c , "pages.inbounds.revise"), err) 120 | return 121 | } 122 | inbound, err = a.inboundService.UpdateInbound(inbound) 123 | jsonMsgObj(c, I18n(c , "pages.inbounds.revise"), inbound, err) 124 | if err == nil { 125 | a.xrayService.SetToNeedRestart() 126 | } 127 | } 128 | func (a *InboundController) getClientIps(c *gin.Context) { 129 | email := c.Param("email") 130 | 131 | ips , err := a.inboundService.GetInboundClientIps(email) 132 | if err != nil { 133 | jsonObj(c, "No IP Record", nil) 134 | return 135 | } 136 | jsonObj(c, ips, nil) 137 | } 138 | func (a *InboundController) clearClientIps(c *gin.Context) { 139 | email := c.Param("email") 140 | 141 | err := a.inboundService.ClearClientIps(email) 142 | if err != nil { 143 | jsonMsg(c, "修改", err) 144 | return 145 | } 146 | jsonMsg(c, "Log Cleared", nil) 147 | } 148 | func (a *InboundController) resetClientTraffic(c *gin.Context) { 149 | email := c.Param("email") 150 | 151 | err := a.inboundService.ResetClientTraffic(email) 152 | if err != nil { 153 | jsonMsg(c, "something worng!", err) 154 | return 155 | } 156 | jsonMsg(c, "traffic reseted", nil) 157 | } 158 | -------------------------------------------------------------------------------- /web/controller/index.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | "x-ui/logger" 7 | "x-ui/web/job" 8 | "x-ui/web/service" 9 | "x-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", "pages.login.title", 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, I18n(c , "pages.login.toasts.invalidFormData")) 50 | return 51 | } 52 | if form.Username == "" { 53 | pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyUsername")) 54 | return 55 | } 56 | if form.Password == "" { 57 | pureJsonMsg(c, false, I18n(c , "pages.login.toasts.emptyPassword")) 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, I18n(c , "pages.login.toasts.wrongUsernameOrPassword")) 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, I18n(c , "pages.login.toasts.successLogin"), 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/server.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "time" 6 | "x-ui/web/global" 7 | "x-ui/web/service" 8 | ) 9 | 10 | type ServerController struct { 11 | BaseController 12 | 13 | serverService service.ServerService 14 | 15 | lastStatus *service.Status 16 | lastGetStatusTime time.Time 17 | 18 | lastVersions []string 19 | lastGetVersionsTime time.Time 20 | } 21 | 22 | func NewServerController(g *gin.RouterGroup) *ServerController { 23 | a := &ServerController{ 24 | lastGetStatusTime: time.Now(), 25 | } 26 | a.initRouter(g) 27 | a.startTask() 28 | return a 29 | } 30 | 31 | func (a *ServerController) initRouter(g *gin.RouterGroup) { 32 | g = g.Group("/server") 33 | 34 | g.Use(a.checkLogin) 35 | g.POST("/status", a.status) 36 | g.POST("/getXrayVersion", a.getXrayVersion) 37 | g.POST("/installXray/:version", a.installXray) 38 | } 39 | 40 | func (a *ServerController) refreshStatus() { 41 | a.lastStatus = a.serverService.GetStatus(a.lastStatus) 42 | } 43 | 44 | func (a *ServerController) startTask() { 45 | webServer := global.GetWebServer() 46 | c := webServer.GetCron() 47 | c.AddFunc("@every 2s", func() { 48 | now := time.Now() 49 | if now.Sub(a.lastGetStatusTime) > time.Minute*3 { 50 | return 51 | } 52 | a.refreshStatus() 53 | }) 54 | } 55 | 56 | func (a *ServerController) status(c *gin.Context) { 57 | a.lastGetStatusTime = time.Now() 58 | 59 | jsonObj(c, a.lastStatus, nil) 60 | } 61 | 62 | func (a *ServerController) getXrayVersion(c *gin.Context) { 63 | now := time.Now() 64 | if now.Sub(a.lastGetVersionsTime) <= time.Minute { 65 | jsonObj(c, a.lastVersions, nil) 66 | return 67 | } 68 | 69 | versions, err := a.serverService.GetXrayVersions() 70 | if err != nil { 71 | jsonMsg(c, I18n(c , "getVersion"), err) 72 | return 73 | } 74 | 75 | a.lastVersions = versions 76 | a.lastGetVersionsTime = time.Now() 77 | 78 | jsonObj(c, versions, nil) 79 | } 80 | 81 | func (a *ServerController) installXray(c *gin.Context) { 82 | version := c.Param("version") 83 | err := a.serverService.UpdateXray(version) 84 | jsonMsg(c, I18n(c , "install") + " xray", err) 85 | } 86 | -------------------------------------------------------------------------------- /web/controller/setting.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | "github.com/gin-gonic/gin" 6 | "time" 7 | "x-ui/web/entity" 8 | "x-ui/web/service" 9 | "x-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, I18n(c , "pages.setting.toasts.getSetting"), 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, I18n(c , "pages.setting.toasts.modifySetting"), err) 54 | return 55 | } 56 | err = a.settingService.UpdateAllSetting(allSetting) 57 | jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), 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, I18n(c , "pages.setting.toasts.modifySetting"), err) 65 | return 66 | } 67 | user := session.GetLoginUser(c) 68 | if user.Username != form.OldUsername || user.Password != form.OldPassword { 69 | jsonMsg(c, I18n(c , "pages.setting.toasts.modifyUser"), errors.New(I18n(c , "pages.setting.toasts.originalUserPassIncorrect"))) 70 | return 71 | } 72 | if form.NewUsername == "" || form.NewPassword == "" { 73 | jsonMsg(c,I18n(c , "pages.setting.toasts.modifyUser"), errors.New(I18n(c , "pages.setting.toasts.userPassMustBeNotEmpty"))) 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, I18n(c , "pages.setting.toasts.modifyUser"), err) 83 | } 84 | 85 | func (a *SettingController) restartPanel(c *gin.Context) { 86 | err := a.panelService.RestartPanel(time.Second * 3) 87 | jsonMsg(c, I18n(c , "pages.setting.restartPanel"), 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 | "x-ui/config" 9 | "x-ui/logger" 10 | "x-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 + I18n(c , "success") 50 | } 51 | } else { 52 | m.Success = false 53 | m.Msg = msg + I18n(c , "fail") + ": " + err.Error() 54 | logger.Warning(msg + I18n(c , "fail") + ": ", 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", "pages.index.title", nil) 34 | } 35 | 36 | func (a *XUIController) inbounds(c *gin.Context) { 37 | html(c, "inbounds.html", "pages.inbounds.title", nil) 38 | } 39 | 40 | func (a *XUIController) setting(c *gin.Context) { 41 | html(c, "setting.html", "pages.setting.title", 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 | _ "time/tzdata" 10 | "x-ui/util/common" 11 | "x-ui/xray" 12 | ) 13 | 14 | type Msg struct { 15 | Success bool `json:"success"` 16 | Msg string `json:"msg"` 17 | Obj interface{} `json:"obj"` 18 | } 19 | 20 | type Pager struct { 21 | Current int `json:"current"` 22 | PageSize int `json:"page_size"` 23 | Total int `json:"total"` 24 | OrderBy string `json:"order_by"` 25 | Desc bool `json:"desc"` 26 | Key string `json:"key"` 27 | List interface{} `json:"list"` 28 | } 29 | 30 | type AllSetting struct { 31 | WebListen string `json:"webListen" form:"webListen"` 32 | WebPort int `json:"webPort" form:"webPort"` 33 | WebCertFile string `json:"webCertFile" form:"webCertFile"` 34 | WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` 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 | 15 | {{ i18n .title}} 16 | 17 | {{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 | 18 | 22 | {{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 | click on QR Code to Copy 6 | 7 | 8 | 13 | 14 | 19 | 20 | 21 | 120 | {{end}} -------------------------------------------------------------------------------- /web/html/common/text_modal.html: -------------------------------------------------------------------------------- 1 | {{define "textModal"}} 2 | 5 | 7 | {{ i18n "download" }} [[ txtModal.fileName ]] 8 | 9 | 11 | 12 | 13 | 58 | {{end}} -------------------------------------------------------------------------------- /web/html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "head" .}} 4 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |

{{ i18n "pages.login.title" }}

43 |
44 |
45 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | {{ i18n "login" }} 62 | 63 | 64 | 65 | 66 | Language : 67 | 68 | 69 | 74 | 75 | 76 |    77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
90 |
91 |
92 | {{template "js" .}} 93 | 122 | 123 | -------------------------------------------------------------------------------- /web/html/xui/common_sider.html: -------------------------------------------------------------------------------- 1 | {{define "menuItems"}} 2 | 3 | 4 | {{ i18n "menu.dashboard"}} 5 | 6 | 7 | 8 | {{ i18n "menu.inbounds"}} 9 | 10 | 11 | 12 | {{ i18n "menu.setting"}} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | Github 26 | 27 | 28 | 29 | 30 | {{ i18n "menu.logout"}} 31 | 32 | {{end}} 33 | 34 | 35 | {{define "commonSider"}} 36 | 37 | 39 | {{template "menuItems" .}} 40 | 41 | 42 | 45 |
46 | 47 |
48 | 50 | {{template "menuItems" .}} 51 | 52 |
53 | 69 | {{end}} 70 | -------------------------------------------------------------------------------- /web/html/xui/component/inbound_info.html: -------------------------------------------------------------------------------- 1 | {{define "inboundInfoStream"}} 2 |

{{ i18n "transmission" }}: [[ inbound.network ]]

3 | 4 | 11 | 12 | 17 | 18 | 22 | 23 | 26 | 27 | 31 | 34 |

35 | tls {{ i18n "domainName" }}: [[ inbound.serverName ? inbound.serverName : '' ]] 36 |

37 |

38 | xtls {{ i18n "domainName" }}: [[ inbound.serverName ? inbound.serverName : '' ]] 39 |

40 | {{end}} 41 | 42 | 43 | {{define "component/inboundInfoComponent"}} 44 |
45 |

{{ i18n "protocol"}}: [[ dbInbound.protocol ]]

46 |

{{ i18n "pages.inbounds.address"}}: [[ dbInbound.address ]]

47 |

{{ i18n "pages.inbounds.port"}}: [[ dbInbound.port ]]

48 | 49 | 54 | 55 | 60 | 61 | 64 | 65 | 69 | 70 | 74 | 75 | 79 | 80 | 83 |
84 | {{end}} 85 | 86 | {{define "component/inboundInfo"}} 87 | 94 | {{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/form/inbound.html: -------------------------------------------------------------------------------- 1 | {{define "form/inbound"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | [[ p ]] 13 | 14 | 15 | 16 | 17 | {{ i18n "monitor" }} 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{ i18n "pages.inbounds.totalFlow" }}(GB) 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {{ i18n "pages.inbounds.expireDate" }} 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 | {{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 | 5 | 7 | 8 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/protocol/trojan.html: -------------------------------------------------------------------------------- 1 | {{define "form/trojan"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ i18n "none" }} 9 | [[ key ]] 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | + 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | fallback[[ index + 1 ]] 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {{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 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_grpc.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamGRPC"}} 2 | 3 | 4 | 5 | 6 | 7 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_http.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamHTTP"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{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_quic.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamQUIC"}} 2 | 3 | 4 | 5 | none 6 | aes-128-gcm 7 | chacha20-poly1305 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | none(not camouflage) 16 | srtp(camouflage video call) 17 | utp(camouflage BT download) 18 | wechat-video(camouflage WeChat video) 19 | dtls(camouflage DTLS 1.2 packages) 20 | wireguard(camouflage wireguard packages) 21 | 22 | 23 | 24 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_settings.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamSettings"}} 2 | 3 | 4 | 5 | 6 | tcp 7 | kcp 8 | ws 9 | http 10 | quic 11 | grpc 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 25 | 26 | 27 | 30 | 31 | 32 | 35 | 36 | 37 | 40 | 41 | 42 | 45 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_tcp.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamTCP"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | + 34 | 35 | 36 | 37 | 39 | 41 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 68 | + 69 | 70 | 71 | 72 | 74 | 76 | 82 | 83 | 84 | 85 | 86 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_ws.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamWS"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | + 16 | 17 | 18 | 19 | 21 | 23 | 29 | 30 | 31 | 32 | 33 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/tls_settings.html: -------------------------------------------------------------------------------- 1 | {{define "form/tlsSettings"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | {{ i18n "pages.inbounds.certificatePath" }} 26 | {{ i18n "pages.inbounds.certificateContent" }} 27 | 28 | 29 | 37 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {{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 | 197 | {{end}} 198 | -------------------------------------------------------------------------------- /web/job/check_inbound_job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "x-ui/logger" 5 | "x-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.DisableInvalidClients() 19 | if err != nil { 20 | logger.Warning("disable invalid Client err:", err) 21 | } else if count > 0 { 22 | logger.Debugf("disabled %v Client", count) 23 | j.xrayService.SetToNeedRestart() 24 | } 25 | 26 | count, err = j.inboundService.DisableInvalidInbounds() 27 | if err != nil { 28 | logger.Warning("disable invalid inbounds err:", err) 29 | } else if count > 0 { 30 | logger.Debugf("disabled %v inbounds", count) 31 | j.xrayService.SetToNeedRestart() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/job/check_xray_running_job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import "x-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/xray_traffic_job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "x-ui/logger" 5 | "x-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 | 22 | traffics, clientTraffics, err := j.xrayService.GetXrayTraffic() 23 | if err != nil { 24 | logger.Warning("get xray traffic failed:", err) 25 | return 26 | } 27 | err = j.inboundService.AddTraffic(traffics) 28 | if err != nil { 29 | logger.Warning("add traffic failed:", err) 30 | } 31 | 32 | err = j.inboundService.AddClientTraffic(clientTraffics) 33 | if err != nil { 34 | logger.Warning("add client traffic failed:", err) 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /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 | "loglevel": "warning", 4 | "access": "./access.log" 5 | }, 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 | "settings": {} 30 | }, 31 | { 32 | "protocol": "blackhole", 33 | "settings": {}, 34 | "tag": "blocked" 35 | } 36 | ], 37 | "policy": { 38 | "levels": { 39 | "0": { 40 | "statsUserUplink": true, 41 | "statsUserDownlink": true 42 | } 43 | }, 44 | "system": { 45 | "statsInboundDownlink": true, 46 | "statsInboundUplink": true 47 | } 48 | }, 49 | "routing": { 50 | "rules": [ 51 | { 52 | "inboundTag": [ 53 | "api" 54 | ], 55 | "outboundTag": "api", 56 | "type": "field" 57 | }, 58 | { 59 | "ip": [ 60 | "geoip:private" 61 | ], 62 | "outboundTag": "blocked", 63 | "type": "field" 64 | }, 65 | { 66 | "outboundTag": "blocked", 67 | "protocol": [ 68 | "bittorrent" 69 | ], 70 | "type": "field" 71 | } 72 | ] 73 | }, 74 | "stats": {} 75 | } 76 | -------------------------------------------------------------------------------- /web/service/panel.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "time" 7 | "x-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/server.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/shirou/gopsutil/cpu" 9 | "github.com/shirou/gopsutil/disk" 10 | "github.com/shirou/gopsutil/host" 11 | "github.com/shirou/gopsutil/load" 12 | "github.com/shirou/gopsutil/mem" 13 | "github.com/shirou/gopsutil/net" 14 | "io" 15 | "io/fs" 16 | "net/http" 17 | "os" 18 | "runtime" 19 | "time" 20 | "x-ui/logger" 21 | // "x-ui/util/sys" 22 | "x-ui/xray" 23 | ) 24 | 25 | type ProcessState string 26 | 27 | const ( 28 | Running ProcessState = "running" 29 | Stop ProcessState = "stop" 30 | Error ProcessState = "error" 31 | ) 32 | 33 | type Status struct { 34 | T time.Time `json:"-"` 35 | Cpu float64 `json:"cpu"` 36 | Mem struct { 37 | Current uint64 `json:"current"` 38 | Total uint64 `json:"total"` 39 | } `json:"mem"` 40 | Swap struct { 41 | Current uint64 `json:"current"` 42 | Total uint64 `json:"total"` 43 | } `json:"swap"` 44 | Disk struct { 45 | Current uint64 `json:"current"` 46 | Total uint64 `json:"total"` 47 | } `json:"disk"` 48 | Xray struct { 49 | State ProcessState `json:"state"` 50 | ErrorMsg string `json:"errorMsg"` 51 | Version string `json:"version"` 52 | } `json:"xray"` 53 | Uptime uint64 `json:"uptime"` 54 | Loads []float64 `json:"loads"` 55 | TcpCount int `json:"tcpCount"` 56 | UdpCount int `json:"udpCount"` 57 | NetIO struct { 58 | Up uint64 `json:"up"` 59 | Down uint64 `json:"down"` 60 | } `json:"netIO"` 61 | NetTraffic struct { 62 | Sent uint64 `json:"sent"` 63 | Recv uint64 `json:"recv"` 64 | } `json:"netTraffic"` 65 | } 66 | 67 | type Release struct { 68 | TagName string `json:"tag_name"` 69 | } 70 | 71 | type ServerService struct { 72 | xrayService XrayService 73 | } 74 | 75 | func (s *ServerService) GetStatus(lastStatus *Status) *Status { 76 | now := time.Now() 77 | status := &Status{ 78 | T: now, 79 | } 80 | 81 | percents, err := cpu.Percent(0, false) 82 | if err != nil { 83 | logger.Warning("get cpu percent failed:", err) 84 | } else { 85 | status.Cpu = percents[0] 86 | } 87 | 88 | upTime, err := host.Uptime() 89 | if err != nil { 90 | logger.Warning("get uptime failed:", err) 91 | } else { 92 | status.Uptime = upTime 93 | } 94 | 95 | memInfo, err := mem.VirtualMemory() 96 | if err != nil { 97 | logger.Warning("get virtual memory failed:", err) 98 | } else { 99 | status.Mem.Current = memInfo.Used 100 | status.Mem.Total = memInfo.Total 101 | } 102 | 103 | swapInfo, err := mem.SwapMemory() 104 | if err != nil { 105 | logger.Warning("get swap memory failed:", err) 106 | } else { 107 | status.Swap.Current = swapInfo.Used 108 | status.Swap.Total = swapInfo.Total 109 | } 110 | 111 | distInfo, err := disk.Usage("/") 112 | if err != nil { 113 | logger.Warning("get dist usage failed:", err) 114 | } else { 115 | status.Disk.Current = distInfo.Used 116 | status.Disk.Total = distInfo.Total 117 | } 118 | 119 | avgState, err := load.Avg() 120 | if err != nil { 121 | logger.Warning("get load avg failed:", err) 122 | } else { 123 | status.Loads = []float64{avgState.Load1, avgState.Load5, avgState.Load15} 124 | } 125 | 126 | ioStats, err := net.IOCounters(false) 127 | if err != nil { 128 | logger.Warning("get io counters failed:", err) 129 | } else if len(ioStats) > 0 { 130 | ioStat := ioStats[0] 131 | status.NetTraffic.Sent = ioStat.BytesSent 132 | status.NetTraffic.Recv = ioStat.BytesRecv 133 | 134 | if lastStatus != nil { 135 | duration := now.Sub(lastStatus.T) 136 | seconds := float64(duration) / float64(time.Second) 137 | up := uint64(float64(status.NetTraffic.Sent-lastStatus.NetTraffic.Sent) / seconds) 138 | down := uint64(float64(status.NetTraffic.Recv-lastStatus.NetTraffic.Recv) / seconds) 139 | status.NetIO.Up = up 140 | status.NetIO.Down = down 141 | } 142 | } else { 143 | logger.Warning("can not find io counters") 144 | } 145 | 146 | // status.TcpCount, err = sys.GetTCPCount() 147 | status.TcpCount = 1 148 | err = nil 149 | if err != nil { 150 | logger.Warning("get tcp connections failed:", err) 151 | } 152 | 153 | // status.UdpCount, err = sys.GetUDPCount() 154 | status.UdpCount = 1 155 | err = nil 156 | if err != nil { 157 | logger.Warning("get udp connections failed:", err) 158 | } 159 | 160 | if s.xrayService.IsXrayRunning() { 161 | status.Xray.State = Running 162 | status.Xray.ErrorMsg = "" 163 | } else { 164 | err := s.xrayService.GetXrayErr() 165 | if err != nil { 166 | status.Xray.State = Error 167 | } else { 168 | status.Xray.State = Stop 169 | } 170 | status.Xray.ErrorMsg = s.xrayService.GetXrayResult() 171 | } 172 | status.Xray.Version = s.xrayService.GetXrayVersion() 173 | 174 | return status 175 | } 176 | 177 | func (s *ServerService) GetXrayVersions() ([]string, error) { 178 | url := "https://api.github.com/repos/hossinasaadi/Xray-core/releases" 179 | resp, err := http.Get(url) 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | defer resp.Body.Close() 185 | buffer := bytes.NewBuffer(make([]byte, 8192)) 186 | buffer.Reset() 187 | _, err = buffer.ReadFrom(resp.Body) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | releases := make([]Release, 0) 193 | err = json.Unmarshal(buffer.Bytes(), &releases) 194 | if err != nil { 195 | return nil, err 196 | } 197 | versions := make([]string, 0, len(releases)) 198 | for _, release := range releases { 199 | versions = append(versions, release.TagName) 200 | } 201 | return versions, nil 202 | } 203 | 204 | func (s *ServerService) downloadXRay(version string) (string, error) { 205 | osName := runtime.GOOS 206 | arch := runtime.GOARCH 207 | 208 | switch osName { 209 | case "darwin": 210 | osName = "macos" 211 | } 212 | 213 | switch arch { 214 | case "amd64": 215 | arch = "64" 216 | case "arm64": 217 | arch = "arm64-v8a" 218 | } 219 | 220 | fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch) 221 | url := fmt.Sprintf("https://github.com/hossinasaadi/Xray-core/releases/download/%s/%s", version, fileName) 222 | resp, err := http.Get(url) 223 | if err != nil { 224 | return "", err 225 | } 226 | defer resp.Body.Close() 227 | 228 | os.Remove(fileName) 229 | file, err := os.Create(fileName) 230 | if err != nil { 231 | return "", err 232 | } 233 | defer file.Close() 234 | 235 | _, err = io.Copy(file, resp.Body) 236 | if err != nil { 237 | return "", err 238 | } 239 | 240 | return fileName, nil 241 | } 242 | 243 | func (s *ServerService) UpdateXray(version string) error { 244 | zipFileName, err := s.downloadXRay(version) 245 | if err != nil { 246 | return err 247 | } 248 | 249 | zipFile, err := os.Open(zipFileName) 250 | if err != nil { 251 | return err 252 | } 253 | defer func() { 254 | zipFile.Close() 255 | os.Remove(zipFileName) 256 | }() 257 | 258 | stat, err := zipFile.Stat() 259 | if err != nil { 260 | return err 261 | } 262 | reader, err := zip.NewReader(zipFile, stat.Size()) 263 | if err != nil { 264 | return err 265 | } 266 | 267 | s.xrayService.StopXray() 268 | defer func() { 269 | err := s.xrayService.RestartXray(true) 270 | if err != nil { 271 | logger.Error("start xray failed:", err) 272 | } 273 | }() 274 | 275 | copyZipFile := func(zipName string, fileName string) error { 276 | zipFile, err := reader.Open(zipName) 277 | if err != nil { 278 | return err 279 | } 280 | os.Remove(fileName) 281 | file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm) 282 | if err != nil { 283 | return err 284 | } 285 | defer file.Close() 286 | _, err = io.Copy(file, zipFile) 287 | return err 288 | } 289 | 290 | err = copyZipFile("xray", xray.GetBinaryPath()) 291 | if err != nil { 292 | return err 293 | } 294 | err = copyZipFile("geosite.dat", xray.GetGeositePath()) 295 | if err != nil { 296 | return err 297 | } 298 | err = copyZipFile("geoip.dat", xray.GetGeoipPath()) 299 | if err != nil { 300 | return err 301 | } 302 | 303 | return nil 304 | 305 | } 306 | -------------------------------------------------------------------------------- /web/service/user.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "x-ui/database" 6 | "x-ui/database/model" 7 | "x-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/xray.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "sync" 7 | "x-ui/logger" 8 | "x-ui/xray" 9 | "go.uber.org/atomic" 10 | ) 11 | 12 | var p *xray.Process 13 | var lock sync.Mutex 14 | var isNeedXrayRestart atomic.Bool 15 | var result string 16 | 17 | type XrayService struct { 18 | inboundService InboundService 19 | settingService SettingService 20 | } 21 | 22 | func (s *XrayService) IsXrayRunning() bool { 23 | return p != nil && p.IsRunning() 24 | } 25 | 26 | func (s *XrayService) GetXrayErr() error { 27 | if p == nil { 28 | return nil 29 | } 30 | return p.GetErr() 31 | } 32 | 33 | func (s *XrayService) GetXrayResult() string { 34 | if result != "" { 35 | return result 36 | } 37 | if s.IsXrayRunning() { 38 | return "" 39 | } 40 | if p == nil { 41 | return "" 42 | } 43 | result = p.GetResult() 44 | return result 45 | } 46 | 47 | func (s *XrayService) GetXrayVersion() string { 48 | if p == nil { 49 | return "Unknown" 50 | } 51 | return p.GetVersion() 52 | } 53 | func RemoveIndex(s []interface{}, index int) []interface{} { 54 | return append(s[:index], s[index+1:]...) 55 | } 56 | 57 | func (s *XrayService) GetXrayConfig() (*xray.Config, error) { 58 | templateConfig, err := s.settingService.GetXrayConfigTemplate() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | xrayConfig := &xray.Config{} 64 | err = json.Unmarshal([]byte(templateConfig), xrayConfig) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | s.inboundService.DisableInvalidClients() 70 | 71 | inbounds, err := s.inboundService.GetAllInbounds() 72 | if err != nil { 73 | return nil, err 74 | } 75 | for _, inbound := range inbounds { 76 | if !inbound.Enable { 77 | continue 78 | } 79 | // get settings clients 80 | settings := map[string]interface{}{} 81 | json.Unmarshal([]byte(inbound.Settings), &settings) 82 | clients, ok := settings["clients"].([]interface{}) 83 | if ok { 84 | // check users active or not 85 | 86 | clientStats := inbound.ClientStats 87 | for _, clientTraffic := range clientStats { 88 | 89 | for index, client := range clients { 90 | c := client.(map[string]interface{}) 91 | if c["email"] == clientTraffic.Email { 92 | if ! clientTraffic.Enable { 93 | clients = RemoveIndex(clients,index) 94 | logger.Info("Remove Inbound User",c["email"] ,"due the expire or traffic limit") 95 | 96 | } 97 | 98 | } 99 | } 100 | 101 | 102 | } 103 | settings["clients"] = clients 104 | modifiedSettings, err := json.Marshal(settings) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | inbound.Settings = string(modifiedSettings) 110 | } 111 | inboundConfig := inbound.GenXrayInboundConfig() 112 | xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig) 113 | } 114 | return xrayConfig, nil 115 | } 116 | 117 | func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) { 118 | if !s.IsXrayRunning() { 119 | return nil, nil, errors.New("xray is not running") 120 | } 121 | return p.GetTraffic(true) 122 | } 123 | 124 | func (s *XrayService) RestartXray(isForce bool) error { 125 | lock.Lock() 126 | defer lock.Unlock() 127 | logger.Debug("restart xray, force:", isForce) 128 | 129 | xrayConfig, err := s.GetXrayConfig() 130 | if err != nil { 131 | return err 132 | } 133 | 134 | if p != nil && p.IsRunning() { 135 | if !isForce && p.GetConfig().Equals(xrayConfig) { 136 | logger.Debug("not need to restart xray") 137 | return nil 138 | } 139 | p.Stop() 140 | } 141 | 142 | p = xray.NewProcess(xrayConfig) 143 | result = "" 144 | return p.Start() 145 | } 146 | 147 | func (s *XrayService) StopXray() error { 148 | lock.Lock() 149 | defer lock.Unlock() 150 | logger.Debug("stop xray") 151 | if s.IsXrayRunning() { 152 | return p.Stop() 153 | } 154 | return errors.New("xray is not running") 155 | } 156 | 157 | func (s *XrayService) SetToNeedRestart() { 158 | isNeedXrayRestart.Store(true) 159 | } 160 | 161 | func (s *XrayService) IsNeedRestartAndSetFalse() bool { 162 | return isNeedXrayRestart.CAS(true, false) 163 | } 164 | -------------------------------------------------------------------------------- /web/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "encoding/gob" 5 | "github.com/gin-contrib/sessions" 6 | "github.com/gin-gonic/gin" 7 | "x-ui/database/model" 8 | ) 9 | 10 | const ( 11 | loginUser = "LOGIN_USER" 12 | ) 13 | 14 | func init() { 15 | gob.Register(model.User{}) 16 | } 17 | 18 | func SetLoginUser(c *gin.Context, user *model.User) error { 19 | s := sessions.Default(c) 20 | s.Set(loginUser, user) 21 | return s.Save() 22 | } 23 | 24 | func GetLoginUser(c *gin.Context) *model.User { 25 | s := sessions.Default(c) 26 | obj := s.Get(loginUser) 27 | if obj == nil { 28 | return nil 29 | } 30 | user := obj.(model.User) 31 | return &user 32 | } 33 | 34 | func IsLogin(c *gin.Context) bool { 35 | return GetLoginUser(c) != nil 36 | } 37 | 38 | func ClearSession(c *gin.Context) { 39 | s := sessions.Default(c) 40 | s.Clear() 41 | s.Options(sessions.Options{ 42 | Path: "/", 43 | MaxAge: -1, 44 | }) 45 | s.Save() 46 | } 47 | -------------------------------------------------------------------------------- /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" 13 | 14 | "loading" = "Loading" 15 | "second" = "second" 16 | "minute" = "minute" 17 | "hour" = "hour" 18 | "day" = "day" 19 | "check" = "check" 20 | "indefinitely" = "indefinitely" 21 | "unlimited" = "unlimited" 22 | "none" = "none" 23 | "qrCode" = "QR Code" 24 | "edit" = "edit" 25 | "delete" = "delete" 26 | "reset" = "reset" 27 | "copySuccess" = "Copy successfully" 28 | "sure" = "Sure" 29 | "encryption" = "encryption" 30 | "transmission" = "transmission" 31 | "host" = "host" 32 | "path" = "path" 33 | "camouflage" = "camouflage" 34 | "turnOn" = "turn on" 35 | "closure" = "closure" 36 | "domainName" = "domain name" 37 | "additional" = "alter" 38 | "monitor" = "Listen IP" 39 | "certificate" = "certificat" 40 | "fail" = "fail" 41 | "success" = " success" 42 | "getVersion" = "get version" 43 | "install" = "install" 44 | 45 | [menu] 46 | "dashboard" = "System Status" 47 | "inbounds" = "Inbounds" 48 | "setting" = "Panel Setting" 49 | "logout" = "LogOut" 50 | "link" = "Other" 51 | 52 | [pages.login] 53 | "title" = "Login" 54 | "loginAgain" = "The login time limit has expired, please log in again" 55 | 56 | [pages.login.toasts] 57 | "invalidFormData" = "Input Data Format Is Invalid" 58 | "emptyUsername" = "please Enter Username" 59 | "emptyPassword" = "please Enter Password" 60 | "wrongUsernameOrPassword" = "invalid username or password" 61 | "successLogin" = "Login" 62 | 63 | 64 | [pages.index] 65 | "title" = "system status" 66 | "memory" = "memory" 67 | "hard" = "hard disk" 68 | "xrayStatus" = "xray Status" 69 | "xraySwitch" = "Switch Version" 70 | "xraySwitchClick" = "Click on the version you want to switch" 71 | "xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations" 72 | "operationHours" = "Operation Hours" 73 | "operationHoursDesc" = "The running time of the system since it was started" 74 | "systemLoad" = "System Load" 75 | "connectionCount" = "Connection Count" 76 | "connectionCountDesc" = "The total number of connections for all network cards" 77 | "upSpeed" = "Total upload speed for all network cards" 78 | "downSpeed" = "Total download speed for all network cards" 79 | "totalSent" = "Total upload traffic of all network cards since system startup" 80 | "totalReceive" = "Total download traffic of all network cards since system startup" 81 | "xraySwitchVersionDialog" = "switch xray version" 82 | "xraySwitchVersionDialogDesc" = "whether to switch the xray version to" 83 | "dontRefreshh" = "Installation is in progress, please do not refresh this page" 84 | 85 | 86 | [pages.inbounds] 87 | "title" = "Inbounds" 88 | "totalDownUp" = "Total uploads/downloads" 89 | "totalUsage" = "Total usage" 90 | "inboundCount" = "Number of inbound" 91 | "operate" = "operate" 92 | "enable" = "enable" 93 | "remark" = "remark" 94 | "protocol" = "protocol" 95 | "port" = "port" 96 | "traffic" = "traffic" 97 | "details" = "details" 98 | "transportConfig" = "transport config" 99 | "expireDate" = "expire date" 100 | "resetTraffic" = "reset traffic" 101 | "addInbound" = "addInbound" 102 | "addTo" = "Add To" 103 | "revise" = "Revise" 104 | "modifyInbound" = "Modify InBound" 105 | "deleteInbound" = "Delete Inbound" 106 | "deleteInboundContent" = "Are you sure you want to delete inbound?" 107 | "resetTrafficContent" = "Are you sure you want to reset traffic?" 108 | "copyLink" = "Copy Link" 109 | "address" = "address" 110 | "network" = "network" 111 | "destinationPort" = "destination port" 112 | "targetAddress" = "target address" 113 | "disableInsecureEncryption" = "Disable insecure encryption" 114 | "monitorDesc" = "Leave blank by default" 115 | "meansNoLimit" = "means no limit" 116 | "totalFlow" = "total flow" 117 | "leaveBlankToNeverExpire" = "Leave blank to never expire" 118 | "noRecommendKeepDefault" = "There are no special requirements to keep the default" 119 | "certificatePath" = "certificate file path" 120 | "certificateContent" = "certificate file content" 121 | "publicKeyPath" = "public key file path" 122 | "publicKeyContent" = "public key content" 123 | "keyPath" = "key file path" 124 | "keyContent" = "key content" 125 | 126 | [pages.inbounds.toasts] 127 | "obtain" = "Obtain" 128 | 129 | [pages.inbounds.stream.general] 130 | "requestHeader" = "request header" 131 | "name" = "name" 132 | "value" = "value" 133 | 134 | [pages.inbounds.stream.tcp] 135 | "requestVersion" = "request version" 136 | "requestMethod" = "request method" 137 | "requestPath" = "request path" 138 | "responseVersion" = "response version" 139 | "responseStatus" = "response status" 140 | "responseStatusDescription" = "response status description" 141 | "responseHeader" = "response header" 142 | 143 | [pages.inbounds.stream.quic] 144 | "encryption" = "encryption" 145 | 146 | 147 | [pages.setting] 148 | "title" = "Setting" 149 | "save" = "Save" 150 | "restartPanel" = "Restart Panel" 151 | "restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information" 152 | "panelConfig" = "Panel Configuration" 153 | "userSetting" = "User Setting" 154 | "xrayConfiguration" = "xray Configuration" 155 | "TGReminder" = "TG Reminder Related Settings" 156 | "otherSetting" = "Other Setting" 157 | "panelListeningIP" = "Panel listening IP" 158 | "panelListeningIPDesc" = "Leave blank by default to monitor all IPs, restart the panel to take effect" 159 | "panelPort" = "Panel Port" 160 | "panelPortDesc" = "Restart the panel to take effect" 161 | "publicKeyPath" = "Panel certificate public key file path" 162 | "publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect" 163 | "privateKeyPath" = "Panel certificate key file path" 164 | "privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect" 165 | "panelUrlPath" = "panel url root path" 166 | "panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect" 167 | "oldUsername" = "Current Username" 168 | "currentPassword" = "Current Password" 169 | "newUsername" = "New Username" 170 | "newPassword" = "New Password" 171 | "xrayConfigTemplate" = "xray Configuration Template" 172 | "xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect" 173 | "telegramBotEnable" = "Enable telegram bot" 174 | "telegramBotEnableDesc" = "Restart the panel to take effect" 175 | "telegramToken" = "Telegram Token" 176 | "telegramTokenDesc" = "Restart the panel to take effect" 177 | "telegramChatId" = "Telegram ChatId" 178 | "telegramChatIdDesc" = "Restart the panel to take effect" 179 | "telegramNotifyTime" = "Telegram bot notification time" 180 | "telegramNotifyTimeDesc" = "Using Crontab timing format, restart the panel to take effect" 181 | "timeZonee" = "Time Zone" 182 | "timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect" 183 | 184 | [pages.setting.toasts] 185 | "modifySetting" = "modify setting" 186 | "getSetting" = "get setting" 187 | "modifyUser" = "modify user" 188 | "originalUserPassIncorrect" = "The original user name or original password is incorrect" 189 | "userPassMustBeNotEmpty" = "New username and new password cannot be empty" -------------------------------------------------------------------------------- /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" = "协议" 13 | 14 | "loading" = "加载中" 15 | "second" = "秒" 16 | "minute" = "分钟" 17 | "hour" = "小时" 18 | "day" = "天" 19 | "check" = "查看" 20 | "indefinitely" = "无限期" 21 | "unlimited" = "无限制" 22 | "none" = "无" 23 | "qrCode" = "二维码" 24 | "edit" = "编辑" 25 | "delete" = "删除" 26 | "reset" = "重置" 27 | "copySuccess" = "复制成功" 28 | "sure" = "确定" 29 | "encryption" = "加密" 30 | "transmission" = "传输" 31 | "host" = "主持人" 32 | "path" = "小路" 33 | "camouflage" = "伪装" 34 | "turnOn" = "开启" 35 | "closure" = "关闭" 36 | "domainName" = "域名" 37 | "additional" = "额外" 38 | "monitor" = "监听" 39 | "certificate" = "证书" 40 | "fail" = "失败" 41 | "success" = "成功" 42 | "getVersion" = "获取版本" 43 | "install" = "安装" 44 | 45 | [menu] 46 | "dashboard" = "系统状态" 47 | "inbounds" = "入站列表" 48 | "setting" = "面板设置" 49 | "logout" = "退出登录" 50 | "link" = "其他" 51 | 52 | [pages.login] 53 | "title" = "登录" 54 | "loginAgain" = "登录时效已过,请重新登录" 55 | 56 | [pages.login.toasts] 57 | "invalidFormData" = "数据格式错误" 58 | "emptyUsername" = "请输入用户名" 59 | "emptyPassword" = "请输入密码" 60 | "wrongUsernameOrPassword" = "用户名或密码错误" 61 | "successLogin" = "登录" 62 | 63 | [pages.index] 64 | "title" = "系统状态" 65 | "memory" = "内存" 66 | "hard" = "硬盘" 67 | "xrayStatus" = "xray 状态" 68 | "xraySwitch" = "切换版本" 69 | "xraySwitchClick" = "点击你想切换的版本" 70 | "xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容" 71 | "operationHours" = "运行时间" 72 | "operationHoursDesc" = "系统自启动以来的运行时间" 73 | "systemLoad" = "系统负载" 74 | "connectionCount" = "连接数" 75 | "connectionCountDesc" = "所有网卡的总连接数" 76 | "upSpeed" = "所有网卡的总上传速度" 77 | "downSpeed" = "所有网卡的总下载速度" 78 | "totalSent" = "系统启动以来所有网卡的总上传流量" 79 | "totalReceive" = "系统启动以来所有网卡的总下载流量" 80 | "xraySwitchVersionDialog" = "切换 xray 版本" 81 | "xraySwitchVersionDialogDesc" = "是否切换 xray 版本至" 82 | "dontRefreshh" = "安装中,请不要刷新此页面" 83 | 84 | 85 | [pages.inbounds] 86 | "title" = "入站列表" 87 | "totalDownUp" = "总上传 / 下载" 88 | "totalUsage" = "总用量" 89 | "inboundCount" = "入站数量" 90 | "operate" = "操作" 91 | "enable" = "启用" 92 | "remark" = "备注" 93 | "protocol" = "协议" 94 | "port" = "端口" 95 | "traffic" = "流量" 96 | "details" = "详细信息" 97 | "transportConfig" = "传输配置" 98 | "expireDate" = "到期时间" 99 | "resetTraffic" = "重置流量" 100 | "addInbound" = "添加入" 101 | "addTo" = "添加" 102 | "revise" = "修改" 103 | "modifyInbound" = "修改入站" 104 | "deleteInbound" = "删除入站" 105 | "deleteInboundContent" = "确定要删除入站吗?" 106 | "resetTrafficContent" = "确定要重置流量吗?" 107 | "copyLink" = "复制链接" 108 | "address" = "地址" 109 | "network" = "网络" 110 | "destinationPort" = "目标端口" 111 | "targetAddress" = "目标地址" 112 | "disableInsecureEncryption" = "禁用不安全加密" 113 | "monitorDesc" = "默认留空即可" 114 | "meansNoLimit" = "表示不限制" 115 | "totalFlow" = "总流量" 116 | "leaveBlankToNeverExpire" = "留空则永不到期" 117 | "noRecommendKeepDefault" = "没有特殊需求保持默认即可" 118 | "certificatePath" = "证书文件路径" 119 | "certificateContent" = "证书文件内容" 120 | "publicKeyPath" = "公钥文件路径" 121 | "publicKeyContent" = "公钥内容" 122 | "keyPath" = "密钥文件路径" 123 | "keyContent" = "密钥内容" 124 | 125 | [pages.inbounds.toasts] 126 | "obtain" = "获取" 127 | 128 | [pages.inbounds.stream.general] 129 | "requestHeader" = "请求头" 130 | "name" = "名称" 131 | "value" = "值" 132 | 133 | [pages.inbounds.stream.tcp] 134 | "requestVersion" = "请求版本" 135 | "requestMethod" = "请求方法" 136 | "requestPath" = "请求路径" 137 | "responseVersion" = "响应版本" 138 | "responseStatus" = "响应状态" 139 | "responseStatusDescription" = "响应状态说明" 140 | "responseHeader" = "响应头" 141 | 142 | [pages.inbounds.stream.quic] 143 | "encryption" = "加密" 144 | 145 | 146 | [pages.setting] 147 | "title" = "设置" 148 | "save" = "保存配置" 149 | "restartPanel" = "重启面板" 150 | "restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息" 151 | "panelConfig" = "面板配置" 152 | "userSetting" = "用户设置" 153 | "xrayConfiguration" = "xray 相关设置" 154 | "TGReminder" = "TG提醒相关设置" 155 | "otherSetting" = "其他设置" 156 | "panelListeningIP" = "面板监听 IP" 157 | "panelListeningIPDesc" = "默认留空监听所有 IP,重启面板生效" 158 | "panelPort" = "面板监听端口" 159 | "panelPortDesc" = "重启面板生效" 160 | "publicKeyPath" = "面板证书公钥文件路径" 161 | "publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效" 162 | "privateKeyPath" = "面板证书密钥文件路径" 163 | "privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效" 164 | "panelUrlPath" = "面板 url 根路径" 165 | "panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾,重启面板生效" 166 | "oldUsername" = "原用户名" 167 | "currentPassword" = "原密码" 168 | "newUsername" = "新用户名" 169 | "newPassword" = "新密码" 170 | "xrayConfigTemplate" = "xray 配置模版" 171 | "xrayConfigTemplateDesc" = "以该模版为基础生成最终的 xray 配置文件,重启面板生效" 172 | "telegramBotEnable" = "启用电报机器人" 173 | "telegramBotEnableDesc" = "重启面板生效" 174 | "telegramToken" = "电报机器人TOKEN" 175 | "telegramTokenDesc" = "重启面板生效" 176 | "telegramChatId" = "电报机器人ChatId" 177 | "telegramChatIdDesc" = "重启面板生效" 178 | "telegramNotifyTime" = "电报机器人通知时间" 179 | "telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效" 180 | "timeZonee" = "时区" 181 | "timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效" 182 | 183 | [pages.setting.toasts] 184 | "modifySetting" = "修改设置" 185 | "getSetting" = "获取设置" 186 | "modifyUser" = "修改用户" 187 | "originalUserPassIncorrect" = "原用户名或原密码错误" 188 | "userPassMustBeNotEmpty" = "新用户名和新密码不能为空" 189 | 190 | -------------------------------------------------------------------------------- /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" = "協議" 13 | 14 | [menu] 15 | "dashboard" = "系统状态" 16 | "inbounds" = "入站列表" 17 | "setting" = "面板设置" 18 | "logout" = "退出登录" 19 | "link" = "其他" 20 | 21 | [pages.login] 22 | "title" = "登錄" 23 | 24 | [pages.login.toasts] 25 | "invalidFormData" = "数据格式错误" 26 | "emptyUsername" = "请输入用户名" 27 | "emptyPassword" = "请输入密码" 28 | "wrongUsernameOrPassword" = "用户名或密码错误" 29 | "successLogin" = "登录" 30 | 31 | [pages.index] 32 | "title" = "系统状态" 33 | 34 | [pages.inbounds] 35 | "title" = "入站列表" 36 | 37 | [pages.inbounds.stream.general] 38 | "requestHeader" = "request header" 39 | "name" = "name" 40 | "value" = "value" 41 | 42 | [pages.inbounds.stream.tcp] 43 | "requestVersion" = "request version" 44 | "requestMethod" = "request method" 45 | "requestPath" = "request path" 46 | "responseVersion" = "response version" 47 | "responseStatus" = "response status" 48 | "responseStatusDescription" = "response status description" 49 | "responseHeader" = "response header" 50 | 51 | [pages.inbounds.stream.quic] 52 | "encryption" = "encryption" 53 | 54 | [pages.setting] 55 | "title" = "设置" -------------------------------------------------------------------------------- /x-ui.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=x-ui Service 3 | After=network.target 4 | Wants=network.target 5 | 6 | [Service] 7 | Environment="XRAY_VMESS_AEAD_FORCED=false" 8 | Type=simple 9 | WorkingDirectory=/usr/local/x-ui/ 10 | ExecStart=/usr/local/x-ui/x-ui 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /xray/client_traffic.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | type ClientTraffic struct { 4 | Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` 5 | InboundId int `json:"inboundId" form:"inboundId"` 6 | Enable bool `json:"enable" form:"enable"` 7 | Email string `json:"email" form:"email" gorm:"unique"` 8 | Up int64 `json:"up" form:"up"` 9 | Down int64 `json:"down" form:"down"` 10 | ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` 11 | Total int64 `json:"total" form:"total"` 12 | } 13 | -------------------------------------------------------------------------------- /xray/config.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "bytes" 5 | "x-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 | "x-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 | "runtime" 15 | "strings" 16 | "time" 17 | "x-ui/util/common" 18 | 19 | "github.com/Workiva/go-datastructures/queue" 20 | statsservice "github.com/xtls/xray-core/app/stats/command" 21 | "google.golang.org/grpc" 22 | ) 23 | 24 | var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)") 25 | var ClientTrafficRegex = regexp.MustCompile("(user)>>>([^>]+)>>>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, "-restrictedIPsPath", "./bin/blockedIPs") 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.Kill() 231 | } 232 | 233 | func (p *process) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { 234 | if p.apiPort == 0 { 235 | return nil, 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, 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, nil, err 252 | } 253 | tagTrafficMap := map[string]*Traffic{} 254 | emailTrafficMap := map[string]*ClientTraffic{} 255 | 256 | clientTraffics := make([]*ClientTraffic, 0) 257 | traffics := make([]*Traffic, 0) 258 | for _, stat := range resp.GetStat() { 259 | matchs := trafficRegex.FindStringSubmatch(stat.Name) 260 | if len(matchs) < 3 { 261 | 262 | matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name) 263 | if len(matchs) < 3 { 264 | continue 265 | }else { 266 | 267 | isUser := matchs[1] == "user" 268 | email := matchs[2] 269 | isDown := matchs[3] == "downlink" 270 | if ! isUser { 271 | continue 272 | } 273 | traffic, ok := emailTrafficMap[email] 274 | if !ok { 275 | traffic = &ClientTraffic{ 276 | Email: email, 277 | } 278 | emailTrafficMap[email] = traffic 279 | clientTraffics = append(clientTraffics, traffic) 280 | } 281 | if isDown { 282 | traffic.Down = stat.Value 283 | } else { 284 | traffic.Up = stat.Value 285 | } 286 | 287 | } 288 | continue 289 | } 290 | isInbound := matchs[1] == "inbound" 291 | tag := matchs[2] 292 | isDown := matchs[3] == "downlink" 293 | if tag == "api" { 294 | continue 295 | } 296 | traffic, ok := tagTrafficMap[tag] 297 | if !ok { 298 | traffic = &Traffic{ 299 | IsInbound: isInbound, 300 | Tag: tag, 301 | } 302 | tagTrafficMap[tag] = traffic 303 | traffics = append(traffics, traffic) 304 | } 305 | if isDown { 306 | traffic.Down = stat.Value 307 | } else { 308 | traffic.Up = stat.Value 309 | } 310 | } 311 | 312 | return traffics, clientTraffics, nil 313 | } 314 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /xray_core_config/xray core config.txt: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "loglevel": "warning", 4 | "access": "./access.log" 5 | }, 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 | "settings": {} 30 | }, 31 | { 32 | "protocol": "blackhole", 33 | "settings": {}, 34 | "tag": "blocked" 35 | } 36 | ], 37 | "policy": { 38 | "levels": { 39 | "0": { 40 | "statsUserUplink": true, 41 | "statsUserDownlink": true 42 | } 43 | }, 44 | "system": { 45 | "statsInboundDownlink": true, 46 | "statsInboundUplink": true 47 | } 48 | }, 49 | "routing": { 50 | "domainStrategy": "IPIfNonMatch", 51 | "rules": [ 52 | { 53 | "inboundTag": [ 54 | "api" 55 | ], 56 | "outboundTag": "api", 57 | "type": "field" 58 | }, 59 | 60 | { 61 | "domain": [ 62 | "geosite:category-ads-all", 63 | "geosite:category-porn", 64 | "geosite:category-ir", 65 | "geosite:cn" 66 | ], 67 | "outboundTag": "blocked", 68 | "type": "field" 69 | }, 70 | 71 | { 72 | "ip": [ 73 | "geoip:cn", 74 | "geoip:ir", 75 | "geoip:private" 76 | ], 77 | "outboundTag": "blocked", 78 | "type": "field" 79 | }, 80 | { 81 | "protocol": [ 82 | "bittorrent" 83 | ], 84 | "outboundTag": "blocked", 85 | "type": "field" 86 | } 87 | ] 88 | }, 89 | "stats": {} 90 | } 91 | -------------------------------------------------------------------------------- /xray_core_config/xui panel config block ir ads pushiran.txt: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "loglevel": "none", 4 | "access": "./access.log" 5 | }, 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 | "settings": {}, 30 | "tag": "direct" 31 | }, 32 | { 33 | "protocol": "blackhole", 34 | "settings": {}, 35 | "tag": "blocked" 36 | } 37 | ], 38 | "policy": { 39 | "levels": { 40 | "0": { 41 | "statsUserUplink": true, 42 | "statsUserDownlink": true 43 | } 44 | }, 45 | "system": { 46 | "statsInboundDownlink": true, 47 | "statsInboundUplink": true 48 | } 49 | }, 50 | "routing": { 51 | "domainStrategy": "IPIfNonMatch", 52 | "rules": [ 53 | { 54 | "inboundTag": [ 55 | "api" 56 | ], 57 | "outboundTag": "api", 58 | "type": "field" 59 | }, 60 | 61 | { 62 | "domain": [ 63 | "geosite:category-ads-all", 64 | "geosite:category-porn", 65 | "geosite:category-ir", 66 | "geosite:cn", 67 | "dl2.learnasan.ir", 68 | "pushiran.com", 69 | "rtellservers.com", 70 | "p86.ir", 71 | "adsnovin.net", 72 | "adsnovin.ir", 73 | "adsnovin.com", 74 | "vira-s1.com", 75 | "vira-tel.ir", 76 | "paydane.ir", 77 | "ccibp.ir", 78 | "adnamaa.ir", 79 | "raz-network.ir", 80 | "raaz.co", 81 | "pushekhtesasi.com", 82 | "pushnotificationws.com", 83 | "vira-tel.ir", 84 | "pushfa.com" 85 | ], 86 | "outboundTag": "blocked", 87 | "type": "field" 88 | }, 89 | 90 | { 91 | "ip": [ 92 | "geoip:cn", 93 | "geoip:ir", 94 | "geoip:private", 95 | "141.8.224.183", 96 | "52.213.114.86", 97 | "51.38.11.229", 98 | "141.105.69.168", 99 | "199.127.99.12", 100 | "141.105.69.162", 101 | "148.251.189.249", 102 | "176.31.82.42", 103 | "185.55.226.20", 104 | "185.94.97.54", 105 | "109.169.76.38", 106 | "206.191.152.58" 107 | ], 108 | "outboundTag": "blocked", 109 | "type": "field" 110 | }, 111 | { 112 | "protocol": [ 113 | "bittorrent" 114 | ], 115 | "outboundTag": "blocked", 116 | "type": "field" 117 | } 118 | ] 119 | }, 120 | "stats": {} 121 | } 122 | --------------------------------------------------------------------------------