diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d56cfdf..08112c7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,7 +85,7 @@ jobs: cd x-ui/bin # Download dependencies - Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.9.11/" + Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.10.15/" if [ "${{ matrix.platform }}" == "amd64" ]; then wget -q ${Xray_URL}Xray-linux-64.zip unzip Xray-linux-64.zip @@ -183,7 +183,7 @@ jobs: cd x-ui\bin # Download Xray for Windows - $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/" + $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.10.15/" Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip" Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath . Remove-Item "Xray-windows-64.zip" diff --git a/DockerInit.sh b/DockerInit.sh index db391359..fb603fb8 100755 --- a/DockerInit.sh +++ b/DockerInit.sh @@ -27,7 +27,7 @@ case $1 in esac mkdir -p build/bin cd build/bin -wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/Xray-linux-${ARCH}.zip" +wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.10.15/Xray-linux-${ARCH}.zip" unzip "Xray-linux-${ARCH}.zip" rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat mv xray "xray-linux-${FNAME}" diff --git a/config/version b/config/version index f3ac133c..7f04bb11 100644 --- a/config/version +++ b/config/version @@ -1 +1 @@ -2.9.0 \ No newline at end of file +2.8.5 \ No newline at end of file diff --git a/database/db.go b/database/db.go index 3e3e845c..6257d649 100644 --- a/database/db.go +++ b/database/db.go @@ -4,6 +4,7 @@ package database import ( "bytes" + "errors" "io" "io/fs" "log" @@ -200,3 +201,29 @@ func Checkpoint() error { } return nil } + +// ValidateSQLiteDB opens the provided sqlite DB path with a throw-away connection +// and runs a PRAGMA integrity_check to ensure the file is structurally sound. +// It does not mutate global state or run migrations. +func ValidateSQLiteDB(dbPath string) error { + if _, err := os.Stat(dbPath); err != nil { // file must exist + return err + } + gdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard}) + if err != nil { + return err + } + sqlDB, err := gdb.DB() + if err != nil { + return err + } + defer sqlDB.Close() + var res string + if err := gdb.Raw("PRAGMA integrity_check;").Scan(&res).Error; err != nil { + return err + } + if res != "ok" { + return errors.New("sqlite integrity check failed: " + res) + } + return nil +} diff --git a/go.mod b/go.mod index 04ef7f84..5fabc4d4 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/mhsanaei/3x-ui/v2 -go 1.25.1 +go 1.25.2 require ( - github.com/gin-contrib/gzip v1.2.3 + github.com/gin-contrib/gzip v1.2.4 github.com/gin-contrib/sessions v1.0.4 github.com/gin-gonic/gin v1.11.0 github.com/goccy/go-json v0.10.5 @@ -16,15 +16,14 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil/v4 v4.25.8 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - github.com/stretchr/testify v1.11.1 - github.com/valyala/fasthttp v1.66.0 + github.com/valyala/fasthttp v1.67.0 github.com/xlzd/gotp v0.1.0 - github.com/xtls/xray-core v1.250911.0 + github.com/xtls/xray-core v1.250911.1-0.20251015080723-b69a376aa1b6 go.uber.org/atomic v1.11.0 - golang.org/x/crypto v0.42.0 - golang.org/x/sys v0.36.0 - golang.org/x/text v0.29.0 - google.golang.org/grpc v1.75.1 + golang.org/x/crypto v0.43.0 + golang.org/x/sys v0.37.0 + golang.org/x/text v0.30.0 + google.golang.org/grpc v1.76.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.0 ) @@ -60,7 +59,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect + github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/miekg/dns v1.1.68 // indirect @@ -70,8 +69,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.54.0 // indirect - github.com/refraction-networking/utls v1.8.0 // indirect + github.com/quic-go/quic-go v0.55.0 // indirect + github.com/refraction-networking/utls v1.8.1 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagernet/sing v0.7.10 // indirect @@ -86,21 +85,20 @@ require ( github.com/valyala/fastjson v1.6.4 // indirect github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect - github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect + github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/mock v0.6.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/arch v0.21.0 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.44.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/sync v0.17.0 // indirect - golang.org/x/time v0.13.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect - google.golang.org/protobuf v1.36.9 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect + google.golang.org/protobuf v1.36.10 // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/go.sum b/go.sum index 9deb4a3d..48706c89 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,8 @@ github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIp github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= -github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U= -github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c= +github.com/gin-contrib/gzip v1.2.4 h1:yNz4EhPC2kHSZJD1oc1zwp7MLEhEZ3goQeGM3a1b6jU= +github.com/gin-contrib/gzip v1.2.4/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U= github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= @@ -95,8 +95,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg= -github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= @@ -126,10 +126,10 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE= -github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= +github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= +github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= @@ -168,8 +168,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU= -github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs= +github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac= +github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= @@ -178,10 +178,10 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= -github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU= -github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0= -github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno= -github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k= +github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 h1:nwobseOLLRtdbP6z7Z2aVI97u8ZptTgD1ofovhAKmeU= +github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ= +github.com/xtls/xray-core v1.250911.1-0.20251015080723-b69a376aa1b6 h1:gwgJxWb9OABUJAYxiS33nQzk3MRVjidzBnHBrzKnxOw= +github.com/xtls/xray-core v1.250911.1-0.20251015080723-b69a376aa1b6/go.mod h1:72ZU/srfutsNPmw9y8SCGRy0iccvshIRk8BNGR8D2Ik= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= @@ -204,14 +204,14 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw= -golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -220,26 +220,26 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/main.go b/main.go index 9360607d..21abcadf 100644 --- a/main.go +++ b/main.go @@ -337,6 +337,20 @@ func updateCert(publicKey string, privateKey string) { } else { fmt.Println("set certificate private key success") } + + err = settingService.SetSubCertFile(publicKey) + if err != nil { + fmt.Println("set certificate for subscription public key failed:", err) + } else { + fmt.Println("set certificate for subscription public key success") + } + + err = settingService.SetSubKeyFile(privateKey) + if err != nil { + fmt.Println("set certificate for subscription private key failed:", err) + } else { + fmt.Println("set certificate for subscription private key success") + } } else { fmt.Println("both public and private key should be entered.") } diff --git a/util/ldap/ldap.go b/util/ldap/ldap.go new file mode 100644 index 00000000..795d0e23 --- /dev/null +++ b/util/ldap/ldap.go @@ -0,0 +1,142 @@ +package ldaputil + +import ( + "crypto/tls" + "fmt" + + "github.com/go-ldap/ldap/v3" +) + +type Config struct { + Host string + Port int + UseTLS bool + BindDN string + Password string + BaseDN string + UserFilter string + UserAttr string + FlagField string + TruthyVals []string + Invert bool +} + +// FetchVlessFlags returns map[email]enabled +func FetchVlessFlags(cfg Config) (map[string]bool, error) { + addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) + var conn *ldap.Conn + var err error + if cfg.UseTLS { + conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) + } else { + conn, err = ldap.Dial("tcp", addr) + } + if err != nil { + return nil, err + } + defer conn.Close() + + if cfg.BindDN != "" { + if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil { + return nil, err + } + } + + if cfg.UserFilter == "" { + cfg.UserFilter = "(objectClass=person)" + } + if cfg.UserAttr == "" { + cfg.UserAttr = "mail" + } + // if field not set we fallback to legacy vless_enabled + if cfg.FlagField == "" { + cfg.FlagField = "vless_enabled" + } + + req := ldap.NewSearchRequest( + cfg.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + cfg.UserFilter, + []string{cfg.UserAttr, cfg.FlagField}, + nil, + ) + + res, err := conn.Search(req) + if err != nil { + return nil, err + } + + result := make(map[string]bool, len(res.Entries)) + for _, e := range res.Entries { + user := e.GetAttributeValue(cfg.UserAttr) + if user == "" { + continue + } + val := e.GetAttributeValue(cfg.FlagField) + enabled := false + for _, t := range cfg.TruthyVals { + if val == t { + enabled = true + break + } + } + if cfg.Invert { + enabled = !enabled + } + result[user] = enabled + } + return result, nil +} + +// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password. +func AuthenticateUser(cfg Config, username, password string) (bool, error) { + addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) + var conn *ldap.Conn + var err error + if cfg.UseTLS { + conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) + } else { + conn, err = ldap.Dial("tcp", addr) + } + if err != nil { + return false, err + } + defer conn.Close() + + // Optional initial bind for search + if cfg.BindDN != "" { + if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil { + return false, err + } + } + + if cfg.UserFilter == "" { + cfg.UserFilter = "(objectClass=person)" + } + if cfg.UserAttr == "" { + cfg.UserAttr = "uid" + } + + // Build filter to find specific user + filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username)) + req := ldap.NewSearchRequest( + cfg.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, + filter, + []string{"dn"}, + nil, + ) + res, err := conn.Search(req) + if err != nil { + return false, err + } + if len(res.Entries) == 0 { + return false, nil + } + userDN := res.Entries[0].DN + // Try to bind as the user + if err := conn.Bind(userDN, password); err != nil { + return false, nil + } + return true, nil +} diff --git a/web/entity/entity.go b/web/entity/entity.go index adb60972..42e2df85 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -74,7 +74,31 @@ type AllSetting struct { SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` // JSON subscription fragment configuration SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` // JSON subscription noise configuration SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` // JSON subscription mux configuration - SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` // JSON subscription routing rules + SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` + + // LDAP settings + LdapEnable bool `json:"ldapEnable" form:"ldapEnable"` + LdapHost string `json:"ldapHost" form:"ldapHost"` + LdapPort int `json:"ldapPort" form:"ldapPort"` + LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"` + LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"` + LdapPassword string `json:"ldapPassword" form:"ldapPassword"` + LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"` + LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"` + LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"` // e.g., mail or uid + LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"` + LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"` + // Generic flag configuration + LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"` + LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"` + LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"` + LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"` + LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"` + LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"` + LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB"` + LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"` + LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"` + // JSON subscription routing rules } // CheckValid validates all settings in the AllSetting struct, checking IP addresses, ports, SSL certificates, and other configuration values. diff --git a/web/service/inbound.go b/web/service/inbound.go index d4ab0fc5..bba4255b 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -1590,6 +1590,22 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo return !clientOldEnabled, needRestart, nil } +// SetClientEnableByEmail sets client enable state to desired value; returns (changed, needRestart, error) +func (s *InboundService) SetClientEnableByEmail(clientEmail string, enable bool) (bool, bool, error) { + current, err := s.checkIsEnabledByEmail(clientEmail) + if err != nil { + return false, false, err + } + if current == enable { + return false, false, nil + } + newEnabled, needRestart, err := s.ToggleClientEnableByEmail(clientEmail) + if err != nil { + return false, needRestart, err + } + return newEnabled == enable, needRestart, nil +} + func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) { _, inbound, err := s.GetClientInboundByEmail(clientEmail) if err != nil { diff --git a/web/service/server.go b/web/service/server.go index 45a76f86..a6470bc5 100644 --- a/web/service/server.go +++ b/web/service/server.go @@ -938,13 +938,26 @@ func (s *ServerService) ImportDB(file multipart.File) error { return common.NewErrorf("Error saving db: %v", err) } - // Check if we can init the db or not - if err = database.InitDB(tempPath); err != nil { - return common.NewErrorf("Error checking db: %v", err) + // Close temp file before opening via sqlite + if err = tempFile.Close(); err != nil { + return common.NewErrorf("Error closing temporary db file: %v", err) + } + tempFile = nil + + // Validate integrity (no migrations / side effects) + if err = database.ValidateSQLiteDB(tempPath); err != nil { + return common.NewErrorf("Invalid or corrupt db file: %v", err) } - // Stop Xray - s.StopXrayService() + // Stop Xray (ignore error but log) + if errStop := s.StopXrayService(); errStop != nil { + logger.Warningf("Failed to stop Xray before DB import: %v", errStop) + } + + // Close existing DB to release file locks (especially on Windows) + if errClose := database.CloseDB(); errClose != nil { + logger.Warningf("Failed to close existing DB before replacement: %v", errClose) + } // Backup the current database for fallback fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath()) @@ -979,7 +992,7 @@ func (s *ServerService) ImportDB(file multipart.File) error { return common.NewErrorf("Error moving db file: %v", err) } - // Migrate DB + // Open & migrate new DB if err = database.InitDB(config.GetDBPath()); err != nil { if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil { return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename) diff --git a/web/service/setting.go b/web/service/setting.go index 6b77f8f0..aa72cf00 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -73,6 +73,27 @@ var defaultValueMap = map[string]string{ "warp": "", "externalTrafficInformEnable": "false", "externalTrafficInformURI": "", + // LDAP defaults + "ldapEnable": "false", + "ldapHost": "", + "ldapPort": "389", + "ldapUseTLS": "false", + "ldapBindDN": "", + "ldapPassword": "", + "ldapBaseDN": "", + "ldapUserFilter": "(objectClass=person)", + "ldapUserAttr": "mail", + "ldapVlessField": "vless_enabled", + "ldapSyncCron": "@every 1m", + "ldapFlagField": "", + "ldapTruthyValues": "true,1,yes,on", + "ldapInvertFlag": "false", + "ldapInboundTags": "", + "ldapAutoCreate": "false", + "ldapAutoDelete": "false", + "ldapDefaultTotalGB": "0", + "ldapDefaultExpiryDays": "0", + "ldapDefaultLimitIP": "0", } // SettingService provides business logic for application settings management. @@ -473,10 +494,18 @@ func (s *SettingService) GetSubDomain() (string, error) { return s.getString("subDomain") } +func (s *SettingService) SetSubCertFile(subCertFile string) error { + return s.setString("subCertFile", subCertFile) +} + func (s *SettingService) GetSubCertFile() (string, error) { return s.getString("subCertFile") } +func (s *SettingService) SetSubKeyFile(subKeyFile string) error { + return s.setString("subKeyFile", subKeyFile) +} + func (s *SettingService) GetSubKeyFile() (string, error) { return s.getString("subKeyFile") } @@ -557,6 +586,87 @@ func (s *SettingService) GetIpLimitEnable() (bool, error) { return (accessLogPath != "none" && accessLogPath != ""), nil } +// LDAP exported getters +func (s *SettingService) GetLdapEnable() (bool, error) { + return s.getBool("ldapEnable") +} + +func (s *SettingService) GetLdapHost() (string, error) { + return s.getString("ldapHost") +} + +func (s *SettingService) GetLdapPort() (int, error) { + return s.getInt("ldapPort") +} + +func (s *SettingService) GetLdapUseTLS() (bool, error) { + return s.getBool("ldapUseTLS") +} + +func (s *SettingService) GetLdapBindDN() (string, error) { + return s.getString("ldapBindDN") +} + +func (s *SettingService) GetLdapPassword() (string, error) { + return s.getString("ldapPassword") +} + +func (s *SettingService) GetLdapBaseDN() (string, error) { + return s.getString("ldapBaseDN") +} + +func (s *SettingService) GetLdapUserFilter() (string, error) { + return s.getString("ldapUserFilter") +} + +func (s *SettingService) GetLdapUserAttr() (string, error) { + return s.getString("ldapUserAttr") +} + +func (s *SettingService) GetLdapVlessField() (string, error) { + return s.getString("ldapVlessField") +} + +func (s *SettingService) GetLdapSyncCron() (string, error) { + return s.getString("ldapSyncCron") +} + +func (s *SettingService) GetLdapFlagField() (string, error) { + return s.getString("ldapFlagField") +} + +func (s *SettingService) GetLdapTruthyValues() (string, error) { + return s.getString("ldapTruthyValues") +} + +func (s *SettingService) GetLdapInvertFlag() (bool, error) { + return s.getBool("ldapInvertFlag") +} + +func (s *SettingService) GetLdapInboundTags() (string, error) { + return s.getString("ldapInboundTags") +} + +func (s *SettingService) GetLdapAutoCreate() (bool, error) { + return s.getBool("ldapAutoCreate") +} + +func (s *SettingService) GetLdapAutoDelete() (bool, error) { + return s.getBool("ldapAutoDelete") +} + +func (s *SettingService) GetLdapDefaultTotalGB() (int, error) { + return s.getInt("ldapDefaultTotalGB") +} + +func (s *SettingService) GetLdapDefaultExpiryDays() (int, error) { + return s.getInt("ldapDefaultExpiryDays") +} + +func (s *SettingService) GetLdapDefaultLimitIP() (int, error) { + return s.getInt("ldapDefaultLimitIP") +} + func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error { if err := allSetting.CheckValid(); err != nil { return err diff --git a/web/service/user.go b/web/service/user.go index f42c3cf8..1bde69f6 100644 --- a/web/service/user.go +++ b/web/service/user.go @@ -7,7 +7,7 @@ import ( "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/crypto" - + ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap" "github.com/xlzd/gotp" "gorm.io/gorm" ) @@ -49,8 +49,37 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode return nil } + // If LDAP enabled and local password check fails, attempt LDAP auth if !crypto.CheckPasswordHash(user.Password, password) { - return nil + ldapEnabled, _ := s.settingService.GetLdapEnable() + if !ldapEnabled { + return nil + } + + host, _ := s.settingService.GetLdapHost() + port, _ := s.settingService.GetLdapPort() + useTLS, _ := s.settingService.GetLdapUseTLS() + bindDN, _ := s.settingService.GetLdapBindDN() + ldapPass, _ := s.settingService.GetLdapPassword() + baseDN, _ := s.settingService.GetLdapBaseDN() + userFilter, _ := s.settingService.GetLdapUserFilter() + userAttr, _ := s.settingService.GetLdapUserAttr() + + cfg := ldaputil.Config{ + Host: host, + Port: port, + UseTLS: useTLS, + BindDN: bindDN, + Password: ldapPass, + BaseDN: baseDN, + UserFilter: userFilter, + UserAttr: userAttr, + } + ok, err := ldaputil.AuthenticateUser(cfg, username, password) + if err != nil || !ok { + return nil + } + // On successful LDAP auth, continue 2FA checks below } twoFactorEnable, err := s.settingService.GetTwoFactorEnable()