@@ -281,13 +281,13 @@ If you want to use routing to WARP before v2.1.0 follow steps as below:
2. Select `IP Limit Management`.
3. Choose the appropriate options based on your needs.
- - make sure you have access.log on your Xray Configuration
+ - make sure you have ./access.log on your Xray Configuration after v2.1.3 we have an option for it
```sh
"log": {
- "loglevel": "warning",
"access": "./access.log",
- "error": "./error.log"
+ "dnsLog": false,
+ "loglevel": "warning"
},
```
diff --git a/config/version b/config/version
index 8f9174b4..abae0d9a 100644
--- a/config/version
+++ b/config/version
@@ -1 +1 @@
-2.1.2
\ No newline at end of file
+2.1.3
\ No newline at end of file
diff --git a/database/db.go b/database/db.go
index 8bd0fb49..c75953f0 100644
--- a/database/db.go
+++ b/database/db.go
@@ -21,6 +21,7 @@ var db *gorm.DB
var initializers = []func() error{
initUser,
initInbound,
+ initOutbound,
initSetting,
initInboundClientIps,
initClientTraffic,
@@ -51,6 +52,10 @@ func initInbound() error {
return db.AutoMigrate(&model.Inbound{})
}
+func initOutbound() error {
+ return db.AutoMigrate(&model.OutboundTraffics{})
+}
+
func initSetting() error {
return db.AutoMigrate(&model.Setting{})
}
diff --git a/database/model/model.go b/database/model/model.go
index e2d54436..32ab255f 100644
--- a/database/model/model.go
+++ b/database/model/model.go
@@ -44,6 +44,15 @@ type Inbound struct {
Tag string `json:"tag" form:"tag" gorm:"unique"`
Sniffing string `json:"sniffing" form:"sniffing"`
}
+
+type OutboundTraffics struct {
+ Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
+ Tag string `json:"tag" form:"tag" gorm:"unique"`
+ Up int64 `json:"up" form:"up" gorm:"default:0"`
+ Down int64 `json:"down" form:"down" gorm:"default:0"`
+ Total int64 `json:"total" form:"total" gorm:"default:0"`
+}
+
type InboundClientIps struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
diff --git a/go.mod b/go.mod
index d46ed1fe..722df287 100644
--- a/go.mod
+++ b/go.mod
@@ -7,23 +7,23 @@ require (
github.com/gin-contrib/gzip v0.0.6
github.com/gin-gonic/gin v1.9.1
github.com/goccy/go-json v0.10.2
- github.com/mymmrac/telego v0.28.0
- github.com/nicksnyder/go-i18n/v2 v2.3.0
+ github.com/mymmrac/telego v0.29.1
+ github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.1.1
github.com/robfig/cron/v3 v3.0.1
- github.com/shirou/gopsutil/v3 v3.23.12
- github.com/valyala/fasthttp v1.51.0
+ github.com/shirou/gopsutil/v3 v3.24.1
+ github.com/valyala/fasthttp v1.52.0
github.com/xtls/xray-core v1.8.7
go.uber.org/atomic v1.11.0
golang.org/x/text v0.14.0
- google.golang.org/grpc v1.61.0
- gorm.io/driver/sqlite v1.5.4
- gorm.io/gorm v1.25.6
+ google.golang.org/grpc v1.61.1
+ gorm.io/driver/sqlite v1.5.5
+ gorm.io/gorm v1.25.7
)
require (
- github.com/andybalholm/brotli v1.0.6 // indirect
+ github.com/andybalholm/brotli v1.1.0 // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
@@ -45,10 +45,11 @@ require (
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
+ github.com/grbit/go-json v0.11.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/klauspost/compress v1.17.4 // indirect
+ github.com/klauspost/compress v1.17.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
@@ -75,6 +76,7 @@ require (
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fastjson v1.6.4 // indirect
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 // indirect
@@ -82,16 +84,16 @@ require (
go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.6.0 // indirect
- golang.org/x/crypto v0.18.0 // indirect
+ golang.org/x/crypto v0.19.0 // indirect
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
golang.org/x/mod v0.14.0 // indirect
- golang.org/x/net v0.20.0 // indirect
- golang.org/x/sys v0.16.0 // indirect
+ golang.org/x/net v0.21.0 // indirect
+ golang.org/x/sys v0.17.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.16.1 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b // indirect
diff --git a/go.sum b/go.sum
index 8b6465d7..0c5f04a0 100644
--- a/go.sum
+++ b/go.sum
@@ -12,8 +12,8 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE=
github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns=
-github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
-github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
@@ -124,6 +124,8 @@ github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTj
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
+github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
@@ -136,8 +138,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
-github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
+github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@@ -175,12 +177,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/mymmrac/telego v0.28.0 h1:DNXaYISeZw1J9oB81vCNdskLow8gCRRUJxufqLuH3XE=
-github.com/mymmrac/telego v0.28.0/go.mod h1:oRperySNzJq8dRTl24+uBF1Uy7tlQGIjid/JQtHDsZg=
+github.com/mymmrac/telego v0.29.1 h1:nsNnK0mS18OL+unoDjDI6BVfafJBbT8Wtj7rCzEWoM8=
+github.com/mymmrac/telego v0.29.1/go.mod h1:ZLD1+L2TQRr97NPOCoN1V2w8y9kmFov33OfZ3qT8cF4=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
-github.com/nicksnyder/go-i18n/v2 v2.3.0 h1:2NPsCsNFCVd7i+Su0xYsBrIhS3bE2XMv5gNTft2O+PQ=
-github.com/nicksnyder/go-i18n/v2 v2.3.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
+github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
+github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
@@ -230,8 +232,8 @@ github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJ
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
-github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
-github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
+github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI=
+github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
@@ -288,8 +290,10 @@ 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.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
-github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
+github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
+github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
+github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
+github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 h1:tkMT5pTye+1NlKIXETU78NXw0fyjnaNHmJyyLyzw8+U=
@@ -319,8 +323,8 @@ golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
-golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
@@ -338,8 +342,8 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -369,9 +373,9 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -407,14 +411,14 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
-google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
+google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
+google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
@@ -435,10 +439,10 @@ gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
-gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
-gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
-gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
+gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
+gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
+gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b h1:yqkg3pTifuKukuWanp8spDsL4irJkHF5WI0J47hU87o=
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
diff --git a/logger/logger.go b/logger/logger.go
index a1386b05..ca047cbc 100644
--- a/logger/logger.go
+++ b/logger/logger.go
@@ -65,6 +65,16 @@ func Infof(format string, args ...interface{}) {
addToBuffer("INFO", fmt.Sprintf(format, args...))
}
+func Notice(args ...interface{}) {
+ logger.Notice(args...)
+ addToBuffer("NOTICE", fmt.Sprint(args...))
+}
+
+func Noticef(format string, args ...interface{}) {
+ logger.Noticef(format, args...)
+ addToBuffer("NOTICE", fmt.Sprintf(format, args...))
+}
+
func Warning(args ...interface{}) {
logger.Warning(args...)
addToBuffer("WARNING", fmt.Sprint(args...))
diff --git a/web/assets/css/custom.css b/web/assets/css/custom.css
index ca7d23b1..c41bcc16 100644
--- a/web/assets/css/custom.css
+++ b/web/assets/css/custom.css
@@ -1050,12 +1050,17 @@ li.ant-select-dropdown-menu-item:empty:after {
color: rgba(255, 255, 255, 0.25);
}
+.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(
+ :last-child
+ ),
+.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not(
+ :last-child
+ ),
+.ant-input-group.ant-input-group-compact
+ > .ant-input:not(:first-child):not(:last-child),
+.ant-input-number-handler,
.ant-input-number-handler-wrap {
- border-radius: 0;
-}
-
-.ant-input-number-handler {
- border-radius: 0;
+ border-radius: 0;
}
.ant-input-number {
@@ -1089,7 +1094,8 @@ li.ant-select-dropdown-menu-item:empty:after {
> td,
.ant-table-thead
> tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)
- > td {
+ > td,
+.ant-calendar-time-picker-select li:hover {
background-color: rgb(232 244 242);
}
@@ -1121,3 +1127,11 @@ li.ant-select-dropdown-menu-item:empty:after {
.ant-input-group-addon:not(:first-child):not(:last-child), .ant-input-group-wrap:not(:first-child):not(:last-child), .ant-input-group>.ant-input:not(:first-child):not(:last-child) {
border-radius: 0rem 1rem 1rem 0rem;
}
+
+.ant-tag {
+ margin-right: 6px;
+}
+
+b, strong {
+ font-weight: 500;
+}
diff --git a/web/assets/js/langs.js b/web/assets/js/langs.js
index 59f0696f..42fa49ff 100644
--- a/web/assets/js/langs.js
+++ b/web/assets/js/langs.js
@@ -29,6 +29,11 @@ const supportLangs = [
value: 'es-ES',
icon: '🇪🇸',
},
+ {
+ name: 'Indonesian',
+ value: 'id-ID',
+ icon: '🇮🇩',
+ },
];
function getLang() {
diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js
index 42fb50a1..6a52563d 100644
--- a/web/assets/js/model/outbound.js
+++ b/web/assets/js/model/outbound.js
@@ -418,7 +418,7 @@ class Outbound extends CommonClass {
}
canEnableTls() {
- if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
+ if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.stream.network);
}
@@ -861,13 +861,13 @@ Outbound.SocksSettings = class extends CommonClass {
}
static fromJson(json={}) {
- servers = json.servers;
+ let servers = json.servers;
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
return new Outbound.SocksSettings(
servers[0].address,
servers[0].port,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
- ObjectUtil.isArrEmpty(servers[0].pass) ? '' : servers[0].users[0].pass,
+ ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,
);
}
@@ -891,13 +891,13 @@ Outbound.HttpSettings = class extends CommonClass {
}
static fromJson(json={}) {
- servers = json.servers;
+ let servers = json.servers;
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
return new Outbound.HttpSettings(
servers[0].address,
servers[0].port,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
- ObjectUtil.isArrEmpty(servers[0].pass) ? '' : servers[0].users[0].pass,
+ ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,
);
}
@@ -914,8 +914,8 @@ Outbound.HttpSettings = class extends CommonClass {
Outbound.WireguardSettings = class extends CommonClass {
constructor(
- mtu=1420, secretKey=Wireguard.generateKeypair().privateKey,
- address=[''], workers=2, domainStrategy='ForceIPv6v4', reserved='',
+ mtu=1420, secretKey='',
+ address=[''], workers=2, domainStrategy='', reserved='',
peers=[new Outbound.WireguardSettings.Peer()], kernelMode=false) {
super();
this.mtu = mtu;
@@ -965,7 +965,7 @@ Outbound.WireguardSettings = class extends CommonClass {
};
Outbound.WireguardSettings.Peer = class extends CommonClass {
- constructor(publicKey=Wireguard.generateKeypair().publicKey, psk='', allowedIPs=['0.0.0.0/0','::/0'], endpoint='', keepAlive=0) {
+ constructor(publicKey='', psk='', allowedIPs=['0.0.0.0/0','::/0'], endpoint='', keepAlive=0) {
super();
this.publicKey = publicKey;
this.psk = psk;
diff --git a/web/assets/js/sw.js b/web/assets/js/sw.js
new file mode 100644
index 00000000..46cf61c4
--- /dev/null
+++ b/web/assets/js/sw.js
@@ -0,0 +1,39 @@
+var self = this;
+
+var filesToCache = [
+ '/'
+];
+
+self.addEventListener('install', function (e) {
+ e.waitUntil(
+ caches.open('3xPanel').then(function (cache) {
+ return cache.addAll(filesToCache);
+ })
+ );
+});
+self.addEventListener('activate', function (event) {
+ event.waitUntil(
+ caches.keys().then(function (cacheNames) {
+ return Promise.all(
+ cacheNames.filter(function (cacheName) {
+ }).map(function (cacheName) {
+ return caches.delete(cacheName);
+ })
+ );
+ })
+ );
+});
+self.addEventListener('fetch', function (event) {
+ event.respondWith(
+ caches.open('mysite-dynamic').then(function (cache) {
+ return cache.match(event.request).then(function (response) {
+ return response || fetch(event.request).then(function (response) {
+ cache.put(event.request, response.clone());
+ return response;
+ });
+ });
+ })
+ );
+});
+
+console.clear();
\ No newline at end of file
diff --git a/web/assets/manifest.json b/web/assets/manifest.json
new file mode 100644
index 00000000..04c63c1d
--- /dev/null
+++ b/web/assets/manifest.json
@@ -0,0 +1,41 @@
+{
+ "name": "3x-UI Panel",
+ "short_name": "3xPanel",
+ "description": "3x-ui panel converted to PWA",
+ "start_url": "/",
+ "background_color": "#F4F4F4",
+ "display": "fullscreen",
+ "theme_color": "#293343",
+ "icons": [
+ {
+ "src": "./assets/icons/16.png",
+ "sizes": "16x16",
+ "type": "image/png"
+ },
+ {
+ "src": "./assets/icons/24.png",
+ "sizes": "24x24",
+ "type": "image/png"
+ },
+ {
+ "src": "./assets/icons/32.png",
+ "sizes": "32x32",
+ "type": "image/png"
+ },
+ {
+ "src": "./assets/icons/64.png",
+ "sizes": "64x64",
+ "type": "image/png"
+ },
+ {
+ "src": "./assets/icons/192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "./assets/icons/512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ]
+}
diff --git a/web/assets/persian-datepicker/persian-datepicker.min.css b/web/assets/persian-datepicker/persian-datepicker.min.css
index bbbef736..9e99974a 100644
--- a/web/assets/persian-datepicker/persian-datepicker.min.css
+++ b/web/assets/persian-datepicker/persian-datepicker.min.css
@@ -1 +1,454 @@
-jdp-overlay{height:0;width:0}jdp-container{-moz-animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimation;-webkit-animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimation;animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimation;background:#fff;border-radius:4px;box-shadow:0 1px 6px rgba(0,0,0,.12),0 1px 4px rgba(0,0,0,.24);direction:rtl;display:none;max-width:307.875px;min-width:307.875px;overflow:hidden;padding:.5rem 0;position:absolute;-ms-touch-action:manipulation;touch-action:manipulation;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}jdp-container,jdp-container *,jdp-container :after,jdp-container :before{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}jdp-container .jdp-icon-minus,jdp-container .jdp-icon-plus{border:1px solid #e6e6e6;border-radius:4px;cursor:pointer;display:inline-block;flex:none;overflow:hidden;text-align:center;text-decoration:none;vertical-align:middle}jdp-container .jdp-icon-minus svg,jdp-container .jdp-icon-plus svg{height:1.5rem;padding:.25rem;vertical-align:middle;width:1.5rem}jdp-container .jdp-icon-minus.not-in-range,jdp-container .jdp-icon-plus.not-in-range{cursor:not-allowed}jdp-container .jdp-icon-minus.not-in-range svg,jdp-container .jdp-icon-plus.not-in-range svg{opacity:.3}jdp-container .jdp-months,jdp-container .jdp-years{fill:rgba(0,0,0,.9);color:rgba(0,0,0,.9);display:-webkit-inline-box;display:-webkit-flex;display:-ms-inline-flexbox;display:inline-flex;font-size:120%;margin:0 2.5%}jdp-container .jdp-months{width:50%}jdp-container .jdp-years{width:40%}jdp-container .jdp-month,jdp-container .jdp-month input,jdp-container .jdp-month select,jdp-container .jdp-time,jdp-container .jdp-time input,jdp-container .jdp-time select,jdp-container .jdp-year,jdp-container .jdp-year input,jdp-container .jdp-year select{background:#fff;border:none;border-radius:0;color:inherit;display:inline-block;font-family:inherit;font-size:inherit;font-weight:300;height:auto;line-height:inherit;margin:0;outline:0;padding:0;text-align:center;vertical-align:initial;width:100%}jdp-container .jdp-month input:active,jdp-container .jdp-month input:focus,jdp-container .jdp-month select:active,jdp-container .jdp-month select:focus,jdp-container .jdp-month:active,jdp-container .jdp-month:focus,jdp-container .jdp-time input:active,jdp-container .jdp-time input:focus,jdp-container .jdp-time select:active,jdp-container .jdp-time select:focus,jdp-container .jdp-time:active,jdp-container .jdp-time:focus,jdp-container .jdp-year input:active,jdp-container .jdp-year input:focus,jdp-container .jdp-year select:active,jdp-container .jdp-year select:focus,jdp-container .jdp-year:active,jdp-container .jdp-year:focus{outline:0}jdp-container .jdp-month input option,jdp-container .jdp-month option,jdp-container .jdp-month select option,jdp-container .jdp-time input option,jdp-container .jdp-time option,jdp-container .jdp-time select option,jdp-container .jdp-year input option,jdp-container .jdp-year option,jdp-container .jdp-year select option{font-size:95%;min-height:1.3rem;outline:0;padding:0}jdp-container .jdp-month:hover,jdp-container .jdp-time:hover,jdp-container .jdp-year:hover{filter:brightness(.9)}jdp-container .jdp-month input,jdp-container .jdp-time input,jdp-container .jdp-year input{-webkit-appearance:none;-moz-appearance:textfield;cursor:text}jdp-container .jdp-month input::-webkit-inner-spin-button,jdp-container .jdp-month input::-webkit-outer-spin-button,jdp-container .jdp-time input::-webkit-inner-spin-button,jdp-container .jdp-time input::-webkit-outer-spin-button,jdp-container .jdp-year input::-webkit-inner-spin-button,jdp-container .jdp-year input::-webkit-outer-spin-button{-webkit-appearance:none}jdp-container .jdp-month select,jdp-container .jdp-time select,jdp-container .jdp-year select{-webkit-appearance:menulist;-moz-appearance:menulist;cursor:pointer;position:relative}jdp-container .jdp-days{-ms-flex-pack:justify;display:inline-block;display:-ms-flexbox;display:flex;flex-wrap:wrap;-ms-flex-wrap:wrap;justify-content:space-around;outline:0;padding:.5rem 0 0;text-align:left;width:100%}jdp-container .jdp-day,jdp-container .jdp-day-name{background:0 0;border:1px solid transparent;color:rgba(0,0,0,.9);display:inline-block;flex-basis:14.2857143%;font-weight:400;height:31px;justify-content:center;line-height:31px;margin:2px 0;position:relative;text-align:center;width:14.2857143%}jdp-container .jdp-day-name.today,jdp-container .jdp-day.today{border-color:rgba(0,0,0,.3)}.dark jdp-container .jdp-day-name.selected,.dark jdp-container .jdp-day.selected,jdp-container .jdp-day-name.selected,jdp-container .jdp-day.selected{background-color:#008771!important;color:#fff!important;opacity:1!important}.dark jdp-container .jdp-day-name.holly-day,.dark jdp-container .jdp-day-name.last-week,.dark jdp-container .jdp-day.holly-day,.dark jdp-container .jdp-day.last-week,jdp-container .jdp-day-name.holly-day,jdp-container .jdp-day-name.last-week,jdp-container .jdp-day.holly-day,jdp-container .jdp-day.last-week{color:#f44336}.dark jdp-container .jdp-day.not-in-month,jdp-container .jdp-day.not-in-month{opacity:.4}jdp-container .jdp-day.disabled-day{cursor:not-allowed;opacity:.15}jdp-container .jdp-day:not(.disabled-day){border-radius:4px;cursor:pointer;transition:.1s linear}jdp-container .jdp-day:not(.disabled-day):hover{background:rgba(0,0,0,.1);transform:scale(1.15);z-index:1}jdp-container .jdp-day-name{background-color:rgba(0,0,0,.1);border-radius:0;cursor:default;font-size:90%;font-weight:900}jdp-container .jdp-footer{-ms-flex-pack:justify;display:inline-block;display:-ms-flexbox;display:flex;flex-wrap:nowrap;-ms-flex-wrap:nowrap;justify-content:space-between;outline:0;padding:.5rem .5rem 0;width:100%}jdp-container .jdp-btn-close,jdp-container .jdp-btn-empty,jdp-container .jdp-btn-today{background:#008771;border-radius:5px;color:#fff;cursor:pointer;display:inline-block;font-size:90%;font-weight:400;padding:.3em .6em;text-align:center}jdp-container .jdp-btn-close.disabled-btn,jdp-container .jdp-btn-empty.disabled-btn,jdp-container .jdp-btn-today.disabled-btn{cursor:not-allowed;opacity:.2}@media only screen and (max-width:481px){jdp-overlay{backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);background-color:rgba(0,0,0,.3);display:none;height:100%;left:0;position:fixed;top:0;width:100%}jdp-container{-moz-animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimationMobile;-webkit-animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimationMobile;animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimationMobile;border-radius:4px 4px 0 0;bottom:0!important;left:50%!important;max-width:100%;min-width:280px;top:unset!important;transform:translateX(-50%)!important;width:100%}jdp-container .jdp-footer{margin:.5rem 0}jdp-container .jdp-btn-close,jdp-container .jdp-btn-empty,jdp-container .jdp-btn-today{font-size:100%;padding:.5em .8em}jdp-container .jdp-btn-today~.jdp-btn-empty{margin-left:auto;margin-right:1em}}jdp-container .jdp-time-container{display:flex;padding:0}jdp-container .jdp-time-container .jdp-time{flex:auto;margin:0 .5rem;position:relative}jdp-container .jdp-time-container .jdp-time select{background:rgba(0,0,0,.03);border-radius:5px;padding:.5rem 1rem .5rem 7px}jdp-container .jdp-time-container .jdp-time:after{content:":";font-size:1.5rem;height:100%;position:absolute;right:-.7rem;top:50%;transform:translateY(-50%)}jdp-container .jdp-time-container .jdp-time:first-child:after{display:none}jdp-container .jdp-time-container.jdp-only-time .jdp-time select{font-size:1.5rem;padding:.8rem 1rem .8rem 7px}jdp-container .jdp-time-container.jdp-only-time .jdp-time:after{font-size:2.3rem;position:absolute;right:-.8rem}@-webkit-keyframes jdpOpenAnimation{0%{opacity:0;transform:scale(.8)}to{opacity:1;transform:scale(1)}}@keyframes jdpOpenAnimation{0%{opacity:0;transform:scale(.8)}to{opacity:1;transform:scale(1)}}@-webkit-keyframes jdpOpenAnimationMobile{0%{bottom:-10%;opacity:0}to{bottom:0;opacity:1}}@keyframes jdpOpenAnimationMobile{0%{margin-bottom:-20%;opacity:0}to{margin-bottom:0;opacity:1}}.dark jdp-overlay{background-color:#181f2c}.dark jdp-container{background:#181f2c;border-color:#2c3950;box-shadow:0 1px 6px rgba(255,255,255,.12),0 1px 4px rgba(255,255,255,.24);color:#fff}.dark jdp-container .jdp-icon-minus,.dark jdp-container .jdp-icon-plus{border:1px solid #ccc}.dark jdp-container .jdp-months,.dark jdp-container .jdp-years{fill:rgba(255,255,255,0.9);color:rgba(255,255,255,.9)}.dark jdp-container .jdp-month,.dark jdp-container .jdp-month input,.dark jdp-container .jdp-month select,.dark jdp-container .jdp-time,.dark jdp-container .jdp-time input,.dark jdp-container .jdp-time select,.dark jdp-container .jdp-year,.dark jdp-container .jdp-year input,.dark jdp-container .jdp-year select{background:#222d42;color:#fff}.dark jdp-container .jdp-day,.dark jdp-container .jdp-day-name{border:1px solid transparent;color:rgba(255,255,255,.9)}.dark jdp-container .jdp-day-name.today,.dark jdp-container .jdp-day.today{border-color:rgba(255,255,255,.3)}.dark jdp-container .jdp-day.disabled-day{opacity:.15}.dark jdp-container .jdp-day:not(.disabled-day):hover{background:rgba(255,255,255,.1);transform:scale(1.15);z-index:1}.dark jdp-container .jdp-day-name{background-color:#222d42}.dark jdp-container .jdp-footer{background:#181f2c}.dark jdp-container .jdp-btn-close,.dark jdp-container .jdp-btn-empty,.dark jdp-container .jdp-btn-today{background:#008771;color:#fff}.dark jdp-container .jdp-btn-close.disabled-btn,.dark jdp-container .jdp-btn-empty.disabled-btn,.dark jdp-container .jdp-btn-today.disabled-btn{opacity:.2}
\ No newline at end of file
+jdp-overlay {
+ height: 0;
+ width: 0;
+}
+jdp-container {
+ -moz-animation: 0.3s cubic-bezier(0.23, 1, 0.32, 1) jdpOpenAnimation;
+ -webkit-animation: 0.3s cubic-bezier(0.23, 1, 0.32, 1) jdpOpenAnimation;
+ animation: 0.3s cubic-bezier(0.23, 1, 0.32, 1) jdpOpenAnimation;
+ background: #fff;
+ border-radius: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,.15);
+ direction: rtl;
+ display: none;
+ width: 280px;
+ overflow: hidden;
+ padding: 0.5rem 0;
+ position: absolute;
+ -ms-touch-action: manipulation;
+ touch-action: manipulation;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transform-origin: bottom;
+}
+jdp-container,
+jdp-container *,
+jdp-container :after,
+jdp-container :before {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+jdp-container .jdp-icon-minus,
+jdp-container .jdp-icon-plus {
+ border: 1px solid rgb(232 244 242);
+ border-radius: 6px;
+ cursor: pointer;
+ display: flex;
+ flex: none;
+ overflow: hidden;
+ text-align: center;
+ text-decoration: none;
+ vertical-align: middle;
+ transition: all 0.2s;
+ width: 24px;
+ height: 24px;
+ align-items: center;
+ justify-content: center;
+}
+
+jdp-container .jdp-icon-minus:hover,
+jdp-container .jdp-icon-plus:hover {
+ background-color: rgb(232 244 242);
+}
+
+jdp-container .jdp-icon-minus svg,
+jdp-container .jdp-icon-plus svg {
+ height: 1.5rem;
+ padding: 0.25rem;
+ vertical-align: middle;
+ width: 1.5rem;
+}
+jdp-container .jdp-icon-minus.not-in-range,
+jdp-container .jdp-icon-plus.not-in-range {
+ cursor: not-allowed;
+}
+jdp-container .jdp-icon-minus.not-in-range svg,
+jdp-container .jdp-icon-plus.not-in-range svg {
+ opacity: 0.3;
+}
+jdp-container .jdp-months,
+jdp-container .jdp-years {
+ fill: rgba(0, 0, 0, 0.9);
+ color: rgba(0, 0, 0, 0.9);
+ display: -webkit-inline-box;
+ display: -webkit-flex;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ font-size: 120%;
+ margin: 0 2.5%;
+}
+jdp-container .jdp-months {
+ width: 40%;
+ margin-right: 1rem;
+}
+jdp-container .jdp-years {
+ width: 40%;
+ margin: 0.5rem 1.2rem 0.8rem 0;
+}
+jdp-container .jdp-month,
+jdp-container .jdp-month input,
+jdp-container .jdp-month select,
+jdp-container .jdp-time,
+jdp-container .jdp-time input,
+jdp-container .jdp-time select,
+jdp-container .jdp-year,
+jdp-container .jdp-year input,
+jdp-container .jdp-year select {
+ background: #fff;
+ border: none;
+ border-radius: 0;
+ color: inherit;
+ display: inline-block;
+ font-family: inherit;
+ font-size: inherit;
+ font-weight: 300;
+ height: auto;
+ line-height: inherit;
+ margin: 0;
+ outline: 0;
+ padding: 0;
+ text-align: center;
+ vertical-align: initial;
+ width: 100%;
+ font-feature-settings: "ss01";
+}
+jdp-container .jdp-month input:active,
+jdp-container .jdp-month input:focus,
+jdp-container .jdp-month select:active,
+jdp-container .jdp-month select:focus,
+jdp-container .jdp-month:active,
+jdp-container .jdp-month:focus,
+jdp-container .jdp-time input:active,
+jdp-container .jdp-time input:focus,
+jdp-container .jdp-time select:active,
+jdp-container .jdp-time select:focus,
+jdp-container .jdp-time:active,
+jdp-container .jdp-time:focus,
+jdp-container .jdp-year input:active,
+jdp-container .jdp-year input:focus,
+jdp-container .jdp-year select:active,
+jdp-container .jdp-year select:focus,
+jdp-container .jdp-year:active,
+jdp-container .jdp-year:focus {
+ outline: 0;
+}
+jdp-container .jdp-month input option,
+jdp-container .jdp-month option,
+jdp-container .jdp-month select option,
+jdp-container .jdp-time input option,
+jdp-container .jdp-time option,
+jdp-container .jdp-time select option,
+jdp-container .jdp-year input option,
+jdp-container .jdp-year option,
+jdp-container .jdp-year select option {
+ font-size: 95%;
+ min-height: 1.3rem;
+ outline: 0;
+ padding: 0;
+}
+jdp-container .jdp-month input,
+jdp-container .jdp-time input,
+jdp-container .jdp-year input {
+ -webkit-appearance: none;
+ -moz-appearance: textfield;
+ cursor: text;
+}
+jdp-container .jdp-month input::-webkit-inner-spin-button,
+jdp-container .jdp-month input::-webkit-outer-spin-button,
+jdp-container .jdp-time input::-webkit-inner-spin-button,
+jdp-container .jdp-time input::-webkit-outer-spin-button,
+jdp-container .jdp-year input::-webkit-inner-spin-button,
+jdp-container .jdp-year input::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+}
+jdp-container .jdp-month select,
+jdp-container .jdp-time select,
+jdp-container .jdp-year select {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ cursor: pointer;
+ appearance: none;
+ position: relative;
+}
+jdp-container .jdp-days {
+ -ms-flex-pack: justify;
+ display: inline-block;
+ display: -ms-flexbox;
+ display: flex;
+ flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ justify-content: space-around;
+ outline: 0;
+ padding: 8px 12px;
+ text-align: left;
+ width: 100%;
+ border-top: 1px solid #e8e8e8;
+}
+jdp-container .jdp-day,
+jdp-container .jdp-day-name {
+ background: 0 0;
+ border: 1px solid transparent;
+ color: rgba(0,0,0,.65);
+ display: block;
+ font-weight: 400;
+ height: 24px;
+ justify-content: center;
+ line-height: 22px;
+ margin: 2px 6px;
+ position: relative;
+ text-align: center;
+ width: 24px;
+ font-feature-settings: "ss01";
+}
+jdp-container .jdp-day-name.today,
+jdp-container .jdp-day.today {
+ border-color: #008771;
+ color: #008771;
+ font-weight: 700;
+}
+.dark jdp-container .jdp-day-name.selected,
+.dark jdp-container .jdp-day.selected,
+jdp-container .jdp-day-name.selected,
+jdp-container .jdp-day.selected {
+ background-color: #008771 !important;
+ color: #fff !important;
+ opacity: 1 !important;
+}
+.dark jdp-container .jdp-day-name.holly-day,
+.dark jdp-container .jdp-day-name.last-week,
+.dark jdp-container .jdp-day.holly-day,
+.dark jdp-container .jdp-day.last-week,
+jdp-container .jdp-day-name.holly-day,
+jdp-container .jdp-day-name.last-week,
+jdp-container .jdp-day.holly-day,
+jdp-container .jdp-day.last-week {
+ color: #f44336;
+}
+.dark jdp-container .jdp-day.not-in-month,
+jdp-container .jdp-day.not-in-month {
+ opacity: 0.4;
+}
+jdp-container .jdp-day.disabled-day {
+ cursor: not-allowed;
+ opacity: 0.15;
+}
+jdp-container .jdp-day:not(.disabled-day) {
+ border-radius: 6px;
+ cursor: pointer;
+ transition: 0.1s linear;
+}
+jdp-container .jdp-day:not(.disabled-day):hover {
+ background: rgb(232 244 242);
+}
+jdp-container .jdp-day-name {
+ background-color: rgb(0 0 0 / 0%);
+ border-radius: 6px;
+ cursor: default;
+}
+jdp-container .jdp-footer {
+ -ms-flex-pack: justify;
+ display: inline-block;
+ display: -ms-flexbox;
+ display: flex;
+ flex-wrap: nowrap;
+ -ms-flex-wrap: nowrap;
+ justify-content: space-between;
+ outline: 0;
+ padding: 6px 12px 0;
+ width: 100%;
+ border-top: 1px solid #e8e8e8;
+}
+jdp-container .jdp-btn-close,
+jdp-container .jdp-btn-empty,
+jdp-container .jdp-btn-today {
+ background: #00877000;
+ border-radius: 5px;
+ color: #008771;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 90%;
+ font-weight: 400;
+ padding: 0.3em 0.6em;
+ text-align: center;
+}
+jdp-container .jdp-btn-close.disabled-btn,
+jdp-container .jdp-btn-empty.disabled-btn,
+jdp-container .jdp-btn-today.disabled-btn {
+ cursor: not-allowed;
+ opacity: 0.2;
+}
+jdp-container .jdp-time-container {
+ display: flex;
+ padding: 6px 12px 12px 12px;
+}
+jdp-container .jdp-time-container .jdp-time {
+ flex: auto;
+ margin: 0 0.5rem;
+ position: relative;
+}
+jdp-container .jdp-time-container .jdp-time select {
+ border: 1px solid rgb(232 244 242);
+ border-radius: 6px;
+ appearance: none;
+ transition: all 0.2s;
+}
+
+jdp-container .jdp-time-container .jdp-time select:hover {
+ background-color: rgb(232 244 242);
+}
+
+jdp-container .jdp-time-container .jdp-time:after {
+ content: ":";
+ font-size: 1.5rem;
+ height: 100%;
+ position: absolute;
+ right: -0.7rem;
+ transform: translateY(-50%);
+}
+jdp-container .jdp-time-container .jdp-time:first-child:after {
+ display: none;
+}
+jdp-container .jdp-time-container.jdp-only-time .jdp-time select {
+ font-size: 1.5rem;
+ padding: 0.8rem 1rem 0.8rem 7px;
+}
+jdp-container .jdp-time-container.jdp-only-time .jdp-time:after {
+ font-size: 2.3rem;
+ position: absolute;
+ right: -0.8rem;
+}
+@-webkit-keyframes jdpOpenAnimation {
+ 0% {
+ transform: scaleY(.8);
+ transform-origin: 0% 0%;
+ opacity: 0
+ }
+
+ to {
+ transform: scaleY(1);
+ transform-origin: 0% 0%;
+ opacity: 1
+ }
+}
+@keyframes jdpOpenAnimation {
+ 0% {
+ transform: scaleY(.8);
+ transform-origin: 0% 0%;
+ opacity: 0
+ }
+
+ to {
+ transform: scaleY(1);
+ transform-origin: 0% 0%;
+ opacity: 1
+ }
+}
+@-webkit-keyframes jdpOpenAnimationMobile {
+ 0% {
+ bottom: -10%;
+ opacity: 0;
+ }
+ to {
+ bottom: 0;
+ opacity: 1;
+ }
+}
+@keyframes jdpOpenAnimationMobile {
+ 0% {
+ margin-bottom: -20%;
+ opacity: 0;
+ }
+ to {
+ margin-bottom: 0;
+ opacity: 1;
+ }
+}
+
+.dark jdp-container .jdp-days {
+ border-color: #313f5a;
+}
+
+.dark jdp-overlay {
+ background-color: #181f2c;
+}
+.dark jdp-container {
+ background: #101828;
+ border-color: #2c3950;
+ box-shadow: 0 2px 8px rgba(0,0,0,.15);
+ color: #fff;
+}
+.dark jdp-container .jdp-icon-minus,
+.dark jdp-container .jdp-icon-plus {
+ border-color: #313f5a;
+}
+
+.dark jdp-container .jdp-icon-minus:hover,
+.dark jdp-container .jdp-icon-plus:hover {
+ background-color: #313f5a;
+}
+
+.dark jdp-container .jdp-months,
+.dark jdp-container .jdp-years {
+ fill: rgba(255, 255, 255, 0.9);
+ color: rgba(255, 255, 255, 0.9);
+}
+.dark jdp-container .jdp-month,
+.dark jdp-container .jdp-month input,
+.dark jdp-container .jdp-month select,
+.dark jdp-container .jdp-time,
+.dark jdp-container .jdp-time input,
+.dark jdp-container .jdp-time select,
+.dark jdp-container .jdp-year,
+.dark jdp-container .jdp-year input,
+.dark jdp-container .jdp-year select {
+ background: #101828;
+ color: rgb(255 255 255 / 65%);
+}
+.dark jdp-container .jdp-day,
+.dark jdp-container .jdp-day-name {
+ border: 1px solid transparent;
+ color: rgba(255, 255, 255, 0.65);
+}
+.dark jdp-container .jdp-day-name.today,
+.dark jdp-container .jdp-day.today {
+ border-color: #008771;
+}
+.dark jdp-container .jdp-day.disabled-day {
+ opacity: 0.15;
+}
+.dark jdp-container .jdp-day:not(.disabled-day):hover {
+ background-color: #313f5a;
+ color: #fff;
+}
+.dark jdp-container .jdp-footer {
+ border-color: #313f5a;
+}
+.dark jdp-container .jdp-btn-close,
+.dark jdp-container .jdp-btn-empty,
+.dark jdp-container .jdp-btn-today {
+ color: rgb(255 255 255 / 65%);
+}
+
+.dark jdp-container .jdp-btn-close:hover,
+.dark jdp-container .jdp-btn-empty:hover,
+.dark jdp-container .jdp-btn-today:hover {
+ color: rgb(255, 255, 255);
+}
+
+.dark jdp-container .jdp-btn-close.disabled-btn,
+.dark jdp-container .jdp-btn-empty.disabled-btn,
+.dark jdp-container .jdp-btn-today.disabled-btn {
+ opacity: 0.2;
+}
+
+.dark jdp-container .jdp-time-container .jdp-time select:hover {
+ background-color: #313f5a;
+ color: #fff;
+}
+
+.dark jdp-container .jdp-time-container .jdp-time select {
+ border: 1px solid rgb(49 63 90);
+}
diff --git a/web/controller/xray_setting.go b/web/controller/xray_setting.go
index 09e9115f..28f55b54 100644
--- a/web/controller/xray_setting.go
+++ b/web/controller/xray_setting.go
@@ -10,6 +10,7 @@ type XraySettingController struct {
XraySettingService service.XraySettingService
SettingService service.SettingService
InboundService service.InboundService
+ OutboundService service.OutboundService
XrayService service.XrayService
}
@@ -27,6 +28,8 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
g.GET("/getXrayResult", a.getXrayResult)
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
g.POST("/warp/:action", a.warp)
+ g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
+ g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
}
func (a *XraySettingController) getXraySetting(c *gin.Context) {
@@ -84,3 +87,22 @@ func (a *XraySettingController) warp(c *gin.Context) {
jsonObj(c, resp, err)
}
+
+func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
+ outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
+ if err != nil {
+ jsonMsg(c, "Error getting traffics", err)
+ return
+ }
+ jsonObj(c, outboundsTraffic, nil)
+}
+
+func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
+ tag := c.PostForm("tag")
+ err := a.OutboundService.ResetOutboundTraffic(tag)
+ if err != nil {
+ jsonMsg(c, "Error in reset outbound traffics", err)
+ return
+ }
+ jsonObj(c, "", nil)
+}
diff --git a/web/html/common/head.html b/web/html/common/head.html
index 4fa2ea8a..e20cdc24 100644
--- a/web/html/common/head.html
+++ b/web/html/common/head.html
@@ -9,6 +9,16 @@
+
+
+
@@ -36,15 +44,15 @@
- CPU: [[ cpuCoreFormat(status.cpuCores) ]]
- Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]
+ CPU: [[ cpuCoreFormat(status.cpuCores) ]]
+ Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]
- {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
+ {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@@ -56,7 +64,7 @@
:stroke-color="status.swap.color"
:percent="status.swap.percent">
- Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
+ Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@@ -64,7 +72,7 @@
:stroke-color="status.disk.color"
:percent="status.disk.percent">
- {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
+ {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@@ -75,25 +83,25 @@
-
+
- 3X-UI v{{ .cur_ver }}
- Xray v[[ status.xray.version ]]
- @panel3xui
+ 3X-UI:
+ v{{ .cur_ver }}
+ @Panel3xui
-
+
- {{ i18n "menu.link" }}:
- {{ i18n "pages.index.logs" }}
- {{ i18n "pages.index.config" }}
- {{ i18n "pages.index.backup" }}
+ {{ i18n "pages.index.operationHours" }}:
+ Xray [[ formatSecond(status.appStats.uptime) ]]
+ OS [[ formatSecond(status.uptime) ]]
-
+
- {{ i18n "pages.index.xrayStatus" }}:
- [[ status.xray.state ]]
+ {{ i18n "pages.index.xrayStatus" }}:
+ [[ status.xray.state ]]
+
An error occurred while running Xray
@@ -106,137 +114,143 @@
{{ i18n "pages.index.stopXray" }}
{{ i18n "pages.index.restartXray" }}
- {{ i18n "pages.index.xraySwitch" }}
+ v[[ status.xray.version ]]
-
+
- {{ i18n "pages.index.operationHours" }}:
- Xray
- [[ formatSecond(status.appStats.uptime) ]]
- OS
- [[ formatSecond(status.uptime) ]]
+ {{ i18n "menu.link" }}:
+ {{ i18n "pages.index.logs" }}
+ {{ i18n "pages.index.config" }}
+ {{ i18n "pages.index.backup" }}
-
+
- {{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
+ {{ i18n "pages.index.systemLoad" }}:
+
+ [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
{{ i18n "pages.index.systemLoadDesc" }}
-
+
-
+
- {{ i18n "usage"}}:
- RAM [[ sizeFormat(status.appStats.mem) ]] -
- Threads [[ status.appStats.threads ]]
-
+ {{ i18n "usage"}}:
+
+ RAM [[ sizeFormat(status.appStats.mem) ]]
+
+
+ Threads [[ status.appStats.threads ]]
+
-
+
-
- IPv4:
+
+ IPv4
[[ status.publicIP.ipv4 ]]
-
-
-
-
- IPv6:
+
+
+
+
+ IPv6
[[ status.publicIP.ipv6 ]]
-
+
-
+
-
- TCP: [[ status.tcpCount ]]
+
+ TCP: [[ status.tcpCount ]]
{{ i18n "pages.index.connectionTcpCountDesc" }}
-
+
-
- UDP: [[ status.udpCount ]]
+
+ UDP: [[ status.udpCount ]]
{{ i18n "pages.index.connectionUdpCountDesc" }}
-
+
-
+
-
- [[ sizeFormat(status.netIO.up) ]]/s
+
+
+ Up: [[ sizeFormat(status.netIO.up) ]]/s
{{ i18n "pages.index.upSpeed" }}
-
+
-
- [[ sizeFormat(status.netIO.down) ]]/s
+
+
+ Down: [[ sizeFormat(status.netIO.down) ]]/s
{{ i18n "pages.index.downSpeed" }}
-
+
-
+
-
- [[ sizeFormat(status.netTraffic.sent) ]]
+
+
{{ i18n "pages.index.totalSent" }}
-
-
+ Out: [[ sizeFormat(status.netTraffic.sent) ]]
+
-
- [[ sizeFormat(status.netTraffic.recv) ]]
+
+
{{ i18n "pages.index.totalReceive" }}
-
-
+ In: [[ sizeFormat(status.netTraffic.recv) ]]
+
@@ -256,7 +270,7 @@
>
+ style="margin-right: 10px" @click="switchV2rayVersion(version)">
[[ version ]]
@@ -440,8 +454,8 @@
loading: false,
show(logs) {
this.visible = true;
- this.logs = logs;
- this.formattedLogs = logs.length > 0 ? this.formatLogs(logs) : "No Record...";
+ this.logs = logs || [];
+ this.formattedLogs = this.logs.length > 0 ? this.formatLogs(this.logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = '';
diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html
index 0acbbfec..533553c5 100644
--- a/web/html/xui/settings.html
+++ b/web/html/xui/settings.html
@@ -76,15 +76,15 @@
-
-
-
+
+
+
{{ i18n "pages.settings.save" }}
{{ i18n "pages.settings.restartPanel" }}
-
+
diff --git a/web/html/xui/warp_modal.html b/web/html/xui/warp_modal.html
index 4e6dd4fa..38310a69 100644
--- a/web/html/xui/warp_modal.html
+++ b/web/html/xui/warp_modal.html
@@ -140,7 +140,7 @@
mtu: 1420,
secretKey: warpModal.warpData.private_key,
address: Object.values(config.interface.addresses),
- domainStrategy: 'ForceIPv6v4',
+ domainStrategy: 'ForceIP',
peers: [{
publicKey: peer.public_key,
endpoint: peer.endpoint.host,
diff --git a/web/html/xui/xray.html b/web/html/xui/xray.html
index d6f0c0f8..871631e0 100644
--- a/web/html/xui/xray.html
+++ b/web/html/xui/xray.html
@@ -147,6 +147,40 @@
+
+
+
+
+
+
+
+ [[ s ]]
+
+
+
+
+
+
+
+
+
+
+
+ [[ s ]]
+
+
+
+
@@ -293,6 +327,14 @@
[[ rule.outboundTag ]]
+
+
+
+ Balancer Tag: [[ rule.balancerTag ]]
+
+ [[ rule.balancerTag ]]
+
+
- {{ i18n "pages.xray.outbound.addOutbound" }}
- WARP
+
+
+ {{ i18n
+ "pages.xray.outbound.addOutbound" }}
+ WARP
+
+
+
+
+
+
{{ i18n "edit" }}
+
+
+ {{ i18n "pages.inbounds.resetTraffic"}}
+
+
{{ i18n "delete"}}
@@ -378,6 +434,9 @@
reality
+
+ [[ findOutboundTraffic(outbound) ]]
+
@@ -408,6 +467,41 @@
+
+ {{ i18n "pages.xray.balancer.addBalancer"}}
+
+
+ [[ index+1 ]]
+
+ e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;">
+
+
+
+ {{ i18n "edit" }}
+
+
+
+ {{ i18n "delete"}}
+
+
+
+
+
+
+ Random
+ Round Robin
+
+
+ [[ sel ]]
+
+
+
@@ -430,6 +524,7 @@
{{template "ruleModal"}}
{{template "outModal"}}
{{template "reverseModal"}}
+{{template "balancerModal"}}
{{template "warpModal"}}
+{{end}}
\ No newline at end of file
diff --git a/web/html/xui/xray_rule_modal.html b/web/html/xui/xray_rule_modal.html
index 9ed9e06a..07cc3217 100644
--- a/web/html/xui/xray_rule_modal.html
+++ b/web/html/xui/xray_rule_modal.html
@@ -107,6 +107,19 @@
[[ tag ]]
+
+
+
+
+ {{ i18n "pages.xray.balancer.balancerDesc" }}
+
+ Balancer Tag
+
+
+
+ [[ tag ]]
+
+
@@ -133,11 +146,12 @@
protocol: [],
attrs: [],
outboundTag: "",
+ balancerTag: "",
},
inboundTags: [],
outboundTags: [],
users: [],
- balancerTag: [],
+ balancerTags: [],
ok() {
newRule = ruleModal.getResult();
ObjectUtil.execute(ruleModal.confirm, newRule);
@@ -160,6 +174,7 @@
this.rule.protocol = rule.protocol;
this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : [];
this.rule.outboundTag = rule.outboundTag;
+ this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : ""
} else {
this.rule = {
domainMatcher: "",
@@ -174,6 +189,7 @@
protocol: [],
attrs: [],
outboundTag: "",
+ balancerTag: "",
}
}
this.isEdit = isEdit;
@@ -186,6 +202,10 @@
}
if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
}
+
+ if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
+ this.balancerTags = app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)
+ }
},
close() {
ruleModal.visible = false;
@@ -211,6 +231,7 @@
rule.protocol = value.protocol;
rule.attrs = Object.fromEntries(value.attrs);
rule.outboundTag = value.outboundTag;
+ rule.balancerTag = value.balancerTag;
for (const [key, value] of Object.entries(rule)) {
if (
diff --git a/web/job/check_client_ip_job.go b/web/job/check_client_ip_job.go
index b393e68d..51a09db8 100644
--- a/web/job/check_client_ip_job.go
+++ b/web/job/check_client_ip_job.go
@@ -1,7 +1,9 @@
package job
import (
+ "bufio"
"encoding/json"
+ "io"
"log"
"os"
"os/exec"
@@ -23,7 +25,6 @@ type CheckClientIpJob struct {
var job *CheckClientIpJob
var ipFiles = []string{
xray.GetIPLimitLogPath(),
- xray.GetIPLimitPrevLogPath(),
xray.GetIPLimitBannedLogPath(),
xray.GetIPLimitBannedPrevLogPath(),
xray.GetAccessPersistentLogPath(),
@@ -49,6 +50,37 @@ func (j *CheckClientIpJob) Run() {
j.checkFail2BanInstalled()
j.processLogFile()
}
+
+ if !j.hasLimitIp() && xray.GetAccessLogPath() == "./access.log" {
+ go j.clearLogTime()
+ }
+}
+
+func (j *CheckClientIpJob) clearLogTime() {
+ for {
+ time.Sleep(time.Hour)
+ j.clearAccessLog()
+ }
+}
+
+func (j *CheckClientIpJob) clearAccessLog() {
+ accessLogPath := xray.GetAccessLogPath()
+ logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
+ j.checkError(err)
+ defer logAccessP.Close()
+
+ // reopen the access log file for reading
+ file, err := os.Open(accessLogPath)
+ j.checkError(err)
+ defer file.Close()
+
+ // copy access log content to persistent file
+ _, err = io.Copy(logAccessP, file)
+ j.checkError(err)
+
+ // clean access log
+ err = os.Truncate(accessLogPath, 0)
+ j.checkError(err)
}
func (j *CheckClientIpJob) hasLimitIp() bool {
@@ -86,30 +118,40 @@ func (j *CheckClientIpJob) checkFail2BanInstalled() {
err := exec.Command(cmd, args...).Run()
if err != nil {
- logger.Warning("fail2ban is not installed. IP limiting may not work properly.")
+ logger.Error("fail2ban is not installed. IP limiting may not work properly.")
}
}
func (j *CheckClientIpJob) processLogFile() {
accessLogPath := xray.GetAccessLogPath()
- if accessLogPath == "" {
- logger.Warning("access.log doesn't exist in your config.json")
+
+ if accessLogPath == "none" {
+ logger.Error("Access log is set to 'none' check your Xray Configs")
return
}
- data, err := os.ReadFile(accessLogPath)
- InboundClientIps := make(map[string][]string)
- j.checkError(err)
+ if accessLogPath == "" {
+ logger.Error("Access log doesn't exist in your Xray Configs")
+ return
+ }
- lines := strings.Split(string(data), "\n")
- for _, line := range lines {
- ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
+ file, err := os.Open(accessLogPath)
+ j.checkError(err)
+ defer file.Close()
+
+ InboundClientIps := make(map[string][]string)
+
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ ipRegx, _ := regexp.Compile(`(\d+\.\d+\.\d+\.\d+).* accepted`)
emailRegx, _ := regexp.Compile(`email:.+`)
- matchesIp := ipRegx.FindString(line)
- if len(matchesIp) > 0 {
- ip := string(matchesIp)
- if ip == "127.0.0.1" || ip == "1.1.1.1" {
+ matches := ipRegx.FindStringSubmatch(line)
+ if len(matches) > 1 {
+ ip := matches[1]
+ if ip == "127.0.0.1" {
continue
}
@@ -124,13 +166,14 @@ func (j *CheckClientIpJob) processLogFile() {
continue
}
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
-
} else {
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
}
}
}
+ j.checkError(scanner.Err())
+
shouldCleanLog := false
for clientEmail, ips := range InboundClientIps {
@@ -141,27 +184,13 @@ func (j *CheckClientIpJob) processLogFile() {
} else {
shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
}
-
}
// added delay before cleaning logs to reduce chance of logging IP that already has been banned
time.Sleep(time.Second * 2)
if shouldCleanLog {
- // copy access log to persistent file
- logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
- j.checkError(err)
- input, err := os.ReadFile(accessLogPath)
- j.checkError(err)
- if _, err := logAccessP.Write(input); err != nil {
- j.checkError(err)
- }
- defer logAccessP.Close()
-
- // clean access log
- if err := os.Truncate(xray.GetAccessLogPath(), 0); err != nil {
- j.checkError(err)
- }
+ j.clearAccessLog()
}
}
diff --git a/web/job/clear_logs_job.go b/web/job/clear_logs_job.go
index 5ceb5a75..c6312006 100644
--- a/web/job/clear_logs_job.go
+++ b/web/job/clear_logs_job.go
@@ -15,8 +15,8 @@ func NewClearLogsJob() *ClearLogsJob {
// Here Run is an interface method of the Job interface
func (j *ClearLogsJob) Run() {
logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()}
- logFilesPrev := []string{xray.GetIPLimitPrevLogPath(), xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()}
-
+ logFilesPrev := []string{xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()}
+
// clear old previous logs
for i := 0; i < len(logFilesPrev); i++ {
if err := os.Truncate(logFilesPrev[i], 0); err != nil {
@@ -26,25 +26,26 @@ func (j *ClearLogsJob) Run() {
// clear log files and copy to previous logs
for i := 0; i < len(logFiles); i++ {
-
- // copy to previous logs
- logFilePrev, err := os.OpenFile(logFilesPrev[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
- if err != nil {
- logger.Warning("clear logs job err:", err)
+ if i > 0 {
+ // copy to previous logs
+ logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
+ if err != nil {
+ logger.Warning("clear logs job err:", err)
+ }
+
+ logFile, err := os.ReadFile(logFiles[i])
+ if err != nil {
+ logger.Warning("clear logs job err:", err)
+ }
+
+ _, err = logFilePrev.Write(logFile)
+ if err != nil {
+ logger.Warning("clear logs job err:", err)
+ }
+ defer logFilePrev.Close()
}
- logFile, err := os.ReadFile(logFiles[i])
- if err != nil {
- logger.Warning("clear logs job err:", err)
- }
-
- _, err = logFilePrev.Write(logFile)
- if err != nil {
- logger.Warning("clear logs job err:", err)
- }
- defer logFilePrev.Close()
-
- err = os.Truncate(logFiles[i], 0)
+ err := os.Truncate(logFiles[i], 0)
if err != nil {
logger.Warning("clear logs job err:", err)
}
diff --git a/web/job/xray_traffic_job.go b/web/job/xray_traffic_job.go
index 158930a4..c0de4428 100644
--- a/web/job/xray_traffic_job.go
+++ b/web/job/xray_traffic_job.go
@@ -6,8 +6,9 @@ import (
)
type XrayTrafficJob struct {
- xrayService service.XrayService
- inboundService service.InboundService
+ xrayService service.XrayService
+ inboundService service.InboundService
+ outboundService service.OutboundService
}
func NewXrayTrafficJob() *XrayTrafficJob {
@@ -24,11 +25,15 @@ func (j *XrayTrafficJob) Run() {
logger.Warning("get xray traffic failed:", err)
return
}
- err, needRestart := j.inboundService.AddTraffic(traffics, clientTraffics)
+ err, needRestart0 := j.inboundService.AddTraffic(traffics, clientTraffics)
if err != nil {
- logger.Warning("add traffic failed:", err)
+ logger.Warning("add inbound traffic failed:", err)
}
- if needRestart {
+ err, needRestart1 := j.outboundService.AddTraffic(traffics, clientTraffics)
+ if err != nil {
+ logger.Warning("add outbound traffic failed:", err)
+ }
+ if needRestart0 || needRestart1 {
j.xrayService.SetToNeedRestart()
}
diff --git a/web/service/config.json b/web/service/config.json
index 82f7dddf..6cf6c3a6 100644
--- a/web/service/config.json
+++ b/web/service/config.json
@@ -1,7 +1,8 @@
{
"log": {
- "loglevel": "warning",
- "error": "./error.log"
+ "access": "none",
+ "dnsLog": false,
+ "loglevel": "warning"
},
"api": {
"tag": "api",
@@ -43,7 +44,9 @@
},
"system": {
"statsInboundDownlink": true,
- "statsInboundUplink": true
+ "statsInboundUplink": true,
+ "statsOutboundDownlink": true,
+ "statsOutboundUplink": true
}
},
"routing": {
diff --git a/web/service/inbound.go b/web/service/inbound.go
index f3445101..291c0dee 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -682,7 +682,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
return needRestart, tx.Save(oldInbound).Error
}
-func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
+func (s *InboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
var err error
db := database.GetDB()
tx := db.Begin()
@@ -694,7 +694,7 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
tx.Commit()
}
}()
- err = s.addInboundTraffic(tx, inboundTraffics)
+ err = s.addInboundTraffic(tx, traffics)
if err != nil {
return err, false
}
diff --git a/web/service/outbound.go b/web/service/outbound.go
new file mode 100644
index 00000000..244d7a14
--- /dev/null
+++ b/web/service/outbound.go
@@ -0,0 +1,102 @@
+package service
+
+import (
+ "x-ui/database"
+ "x-ui/database/model"
+ "x-ui/logger"
+ "x-ui/xray"
+
+ "gorm.io/gorm"
+)
+
+type OutboundService struct {
+ xrayApi xray.XrayAPI
+}
+
+func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
+ var err error
+ db := database.GetDB()
+ tx := db.Begin()
+
+ defer func() {
+ if err != nil {
+ tx.Rollback()
+ } else {
+ tx.Commit()
+ }
+ }()
+
+ err = s.addOutboundTraffic(tx, traffics)
+ if err != nil {
+ return err, false
+ }
+
+ return nil, false
+}
+
+func (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
+ if len(traffics) == 0 {
+ return nil
+ }
+
+ var err error
+
+ for _, traffic := range traffics {
+ if traffic.IsOutbound {
+
+ var outbound model.OutboundTraffics
+
+ err = tx.Model(&model.OutboundTraffics{}).Where("tag = ?", traffic.Tag).
+ FirstOrCreate(&outbound).Error
+ if err != nil {
+ return err
+ }
+
+ outbound.Tag = traffic.Tag
+ outbound.Up = outbound.Up + traffic.Up
+ outbound.Down = outbound.Down + traffic.Down
+ outbound.Total = outbound.Up + outbound.Down
+
+ err = tx.Save(&outbound).Error
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (s *OutboundService) GetOutboundsTraffic() ([]*model.OutboundTraffics, error) {
+ db := database.GetDB()
+ var traffics []*model.OutboundTraffics
+
+ err := db.Model(model.OutboundTraffics{}).Find(&traffics).Error
+ if err != nil {
+ logger.Warning(err)
+ return nil, err
+ }
+
+ return traffics, nil
+}
+
+func (s *OutboundService) ResetOutboundTraffic(tag string) error {
+ db := database.GetDB()
+
+ whereText := "tag "
+ if tag == "-alltags-" {
+ whereText += " <> ?"
+ } else {
+ whereText += " = ?"
+ }
+
+ result := db.Model(model.OutboundTraffics{}).
+ Where(whereText, tag).
+ Updates(map[string]interface{}{"up": 0, "down": 0, "total": 0})
+
+ err := result.Error
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/web/service/tgbot.go b/web/service/tgbot.go
index fdb5d312..87445a9a 100644
--- a/web/service/tgbot.go
+++ b/web/service/tgbot.go
@@ -6,9 +6,9 @@ import (
"net"
"net/url"
"os"
+ "slices"
"strconv"
"strings"
- "slices"
"time"
"x-ui/config"
"x-ui/database"
@@ -115,14 +115,19 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
}
func (t *Tgbot) NewBot(token string, proxyUrl string) (*telego.Bot, error) {
- if proxyUrl == "" || !strings.HasPrefix(proxyUrl, "socks5://") {
- logger.Warning("invalid socks5 url, start with default")
+ if proxyUrl == "" {
+ // No proxy URL provided, use default instance
+ return telego.NewBot(token)
+ }
+
+ if !strings.HasPrefix(proxyUrl, "socks5://") {
+ logger.Warning("Invalid socks5 URL, starting with default")
return telego.NewBot(token)
}
_, err := url.Parse(proxyUrl)
if err != nil {
- logger.Warning("cant parse proxy url, use default instance for tgbot:", err)
+ logger.Warning("Can't parse proxy URL, using default instance for tgbot:", err)
return telego.NewBot(token)
}
@@ -197,9 +202,13 @@ func (t *Tgbot) OnReceive() {
}, th.AnyCallbackQueryWithMessage())
botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) {
- if message.UserShared != nil {
+ if message.UsersShared != nil {
if checkAdmin(message.From.ID) {
- err := t.inboundService.SetClientTelegramUserID(message.UserShared.RequestID, strconv.FormatInt(message.UserShared.UserID, 10))
+ userIDsStr := ""
+ for _, userID := range message.UsersShared.UserIDs {
+ userIDsStr += strconv.FormatInt(userID, 10) + " "
+ }
+ err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userIDsStr)
output := ""
if err != nil {
output += t.I18nBot("tgbot.messages.selectUserFailed")
@@ -260,7 +269,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
msg += t.I18nBot("tgbot.commands.unknown")
}
- if msg != ""{
+ if msg != "" {
if onlyMessage {
t.SendMsgToTgbot(chatId, msg)
return
@@ -272,7 +281,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
- chatId := callbackQuery.Message.Chat.ID
+ chatId := callbackQuery.Message.GetChat().ID
if isAdmin {
// get query from hash storage
@@ -291,22 +300,22 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
t.searchClient(chatId, email)
case "client_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clientRefreshSuccess", "Email=="+email))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "client_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "ips_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.IpRefreshSuccess", "Email=="+email))
- t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())
case "ips_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
- t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())
case "tgid_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.TGIdRefreshSuccess", "Email=="+email))
- t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
+ t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())
case "tgid_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
- t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
+ t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())
case "reset_traffic":
inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow(
@@ -316,13 +325,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic_c "+email)),
),
)
- t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
+ t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
case "reset_traffic_c":
err := t.inboundService.ResetClientTrafficByEmail(email)
if err == nil {
t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetTrafficSuccess", "Email=="+email))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
}
@@ -346,7 +355,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 40")),
),
tu.InlineKeyboardRow(
- tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")),
+ tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 50")),
tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")),
tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 80")),
),
@@ -356,7 +365,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("200 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 200")),
),
)
- t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
+ t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
case "limit_traffic_c":
if len(dataArray) == 3 {
limitTraffic, err := strconv.Atoi(dataArray[2])
@@ -365,13 +374,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if err == nil {
t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.setTrafficLimitSuccess", "Email=="+email))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
return
}
}
}
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "limit_traffic_in":
if len(dataArray) >= 3 {
oldInputNumber, err := strconv.Atoi(dataArray[2])
@@ -427,12 +436,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
),
)
- t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
+ t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
return
}
}
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "reset_exp":
inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow(
@@ -459,7 +468,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 365")),
),
)
- t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
+ t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
case "reset_exp_c":
if len(dataArray) == 3 {
days, err := strconv.Atoi(dataArray[2])
@@ -494,13 +503,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if err == nil {
t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.expireResetSuccess", "Email=="+email))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
return
}
}
}
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "reset_exp_in":
if len(dataArray) >= 3 {
oldInputNumber, err := strconv.Atoi(dataArray[2])
@@ -556,12 +565,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
),
)
- t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
+ t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
return
}
}
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "ip_limit":
inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow(
@@ -590,7 +599,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("10").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 10")),
),
)
- t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
+ t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
case "ip_limit_c":
if len(dataArray) == 3 {
count, err := strconv.Atoi(dataArray[2])
@@ -599,13 +608,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if err == nil {
t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetIpSuccess", "Email=="+email, "Count=="+strconv.Itoa(count)))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
return
}
}
}
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "ip_limit_in":
if len(dataArray) >= 3 {
oldInputNumber, err := strconv.Atoi(dataArray[2])
@@ -661,12 +670,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
),
)
- t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
+ t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
return
}
}
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "clear_ips":
inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow(
@@ -676,12 +685,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmClearIps")).WithCallbackData(t.encodeQuery("clear_ips_c "+email)),
),
)
- t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
+ t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
case "clear_ips_c":
err := t.inboundService.ClearClientIps(email)
if err == nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clearIpSuccess", "Email=="+email))
- t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
}
@@ -700,7 +709,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmRemoveTGUser")).WithCallbackData(t.encodeQuery("tgid_remove_c "+email)),
),
)
- t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
+ t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
case "tgid_remove_c":
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil || traffic == nil {
@@ -710,7 +719,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
err = t.inboundService.SetClientTelegramUserID(traffic.Id, "")
if err == nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.removedTGUserSuccess", "Email=="+email))
- t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
+ t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
}
@@ -723,7 +732,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmToggle")).WithCallbackData(t.encodeQuery("toggle_enable_c "+email)),
),
)
- t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
+ t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
case "toggle_enable_c":
enabled, err := t.inboundService.ToggleClientEnableByEmail(email)
if err == nil {
@@ -733,7 +742,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.disableSuccess", "Email=="+email))
}
- t.searchClient(chatId, email, callbackQuery.Message.MessageID)
+ t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
}
@@ -769,7 +778,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
t.onlineClients(chatId)
case "onlines_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
- t.onlineClients(chatId, callbackQuery.Message.MessageID)
+ t.onlineClients(chatId, callbackQuery.Message.GetMessageID())
case "commands":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands"))
@@ -1022,7 +1031,7 @@ func (t *Tgbot) getInboundUsages() string {
}
func (t *Tgbot) clientInfoMsg(traffic *xray.ClientTraffic, printEnabled bool, printOnline bool, printActive bool,
- printDate bool, printTraffic bool, printRefreshed bool) string {
+ printDate bool, printTraffic bool, printRefreshed bool) string {
now := time.Now().Unix()
expiryTime := ""
@@ -1210,13 +1219,13 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
} else {
t.SendMsgToTgbot(chatId, output, inlineKeyboard)
- requestUser := telego.KeyboardButtonRequestUser{
+ requestUser := telego.KeyboardButtonRequestUsers{
RequestID: int32(traffic.Id),
UserIsBot: new(bool),
}
keyboard := tu.Keyboard(
tu.KeyboardRow(
- tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUser(&requestUser),
+ tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUsers(&requestUser),
),
tu.KeyboardRow(
tu.KeyboardButton(t.I18nBot("tgbot.buttons.closeKeyboard")),
@@ -1380,7 +1389,6 @@ func (t *Tgbot) getExhausted(chatId int64) {
output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))
output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC))
-
if exhaustedCC > 0 {
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients"))
@@ -1490,7 +1498,6 @@ func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(onlinesCount))
keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh"))))
-
if onlinesCount > 0 {
var buttons []telego.InlineKeyboardButton
@@ -1565,30 +1572,44 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
file, err := os.Open(xray.GetIPLimitBannedPrevLogPath())
if err == nil {
- document := tu.Document(
- tu.ID(chatId),
- tu.File(file),
- )
- _, err = bot.SendDocument(document)
- if err != nil {
- logger.Error("Error in uploading backup: ", err)
+ // Check if the file is non-empty before attempting to upload
+ fileInfo, _ := file.Stat()
+ if fileInfo.Size() > 0 {
+ document := tu.Document(
+ tu.ID(chatId),
+ tu.File(file),
+ )
+ _, err = bot.SendDocument(document)
+ if err != nil {
+ logger.Error("Error in uploading IPLimitBannedPrevLog: ", err)
+ }
+ } else {
+ logger.Warning("IPLimitBannedPrevLog file is empty, not uploading.")
}
+ file.Close()
} else {
- logger.Error("Error in opening db file for backup: ", err)
+ logger.Error("Error in opening IPLimitBannedPrevLog file for backup: ", err)
}
file, err = os.Open(xray.GetIPLimitBannedLogPath())
if err == nil {
- document := tu.Document(
- tu.ID(chatId),
- tu.File(file),
- )
- _, err = bot.SendDocument(document)
- if err != nil {
- logger.Error("Error in uploading config.json: ", err)
+ // Check if the file is non-empty before attempting to upload
+ fileInfo, _ := file.Stat()
+ if fileInfo.Size() > 0 {
+ document := tu.Document(
+ tu.ID(chatId),
+ tu.File(file),
+ )
+ _, err = bot.SendDocument(document)
+ if err != nil {
+ logger.Error("Error in uploading IPLimitBannedLog: ", err)
+ }
+ } else {
+ logger.Warning("IPLimitBannedLog file is empty, not uploading.")
}
+ file.Close()
} else {
- logger.Error("Error in opening config.json file for backup: ", err)
+ logger.Error("Error in opening IPLimitBannedLog file for backup: ", err)
}
}
diff --git a/web/service/xray.go b/web/service/xray.go
index 82d1cc3f..7cd1612c 100644
--- a/web/service/xray.go
+++ b/web/service/xray.go
@@ -95,7 +95,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if !clientTraffic.Enable {
clients = RemoveIndex(clients, index-indexDecrease)
indexDecrease++
- logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
+ logger.Info("Remove Inbound User ", c["email"], " due the expire or traffic limit")
}
diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml
index 7e0f26c5..157baf8c 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -76,7 +76,7 @@
"title" = "Overview"
"memory" = "RAM"
"hard" = "Disk"
-"xrayStatus" = "Status"
+"xrayStatus" = "Xray"
"stopXray" = "Stop"
"restartXray" = "Restart"
"xraySwitch" = "Version"
@@ -309,8 +309,8 @@
"restart" = "Restart Xray"
"basicTemplate" = "Basics"
"advancedTemplate" = "Advanced"
-"generalConfigs" = "General Strategy"
-"generalConfigsDesc" = "These options will determine general strategy adjustments."
+"generalConfigs" = "General"
+"generalConfigsDesc" = "These options will determine general adjustments."
"blockConfigs" = "Protection Shield"
"blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites."
"blockCountryConfigs" = "Block Country"
@@ -388,10 +388,15 @@
"Inbounds" = "Inbounds"
"InboundsDesc" = "Accepting the specific clients."
"Outbounds" = "Outbounds"
+"Balancers" = "Balancers"
"OutboundsDesc" = "Set the outgoing traffic pathway."
"Routings" = "Routing Rules"
"RoutingsDesc" = "The priority of each rule is important!"
"completeTemplate" = "All"
+"logLevel" = "Log Level"
+"logLevelDesc" = "The log level for error logs, indicating the information that needs to be recorded."
+"accessLog" = "Access Log"
+"accessLogDesc" = "The file path for the access log. The special value 'none' disabled access logs"
[pages.xray.rules]
"first" = "First"
@@ -402,6 +407,7 @@
"dest" = "Destination"
"inbound" = "Inbound"
"outbound" = "Outbound"
+"balancer" = "Balancer"
"info" = "Info"
"add" = "Add Rule"
"edit" = "Edit Rule"
@@ -422,6 +428,15 @@
"portal" = "Portal"
"intercon" = "Interconnection"
+[pages.xray.balancer]
+"addBalancer" = "Add Balancer"
+"editBalancer" = "Edit Balancer"
+"balancerStrategy" = "Strategy"
+"balancerSelectors" = "Selectors"
+"tag" = "Tag"
+"tagDesc" = "Unique Tag"
+"balancerDesc" = "It is not possible to use balancerTag and outboundTag at the same time. If used at the same time, only outboundTag will work."
+
[pages.xray.wireguard]
"secretKey" = "Secret Key"
"publicKey" = "Public Key"
@@ -452,7 +467,7 @@
"wentWrong" = "❌ Something went wrong!"
"noIpRecord" = "❗ No IP Record!"
"noInbounds" = "❗ No inbound found!"
-"unlimited" = "♾ Unlimited"
+"unlimited" = "♾ Unlimited(Reset)"
"add" = "Add"
"month" = "Month"
"months" = "Months"
diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml
index ac9de9a4..b0d54d87 100644
--- a/web/translation/translate.es_ES.toml
+++ b/web/translation/translate.es_ES.toml
@@ -20,7 +20,7 @@
"check" = "Verificar"
"indefinite" = "Indefinido"
"unlimited" = "Ilimitado"
-"none" = "Ninguno"
+"none" = "None"
"qrCode" = "Código QR"
"info" = "Más Información"
"edit" = "Editar"
@@ -76,7 +76,7 @@
"title" = "Estado del Sistema"
"memory" = "Memoria"
"hard" = "Disco Duro"
-"xrayStatus" = "Estado de"
+"xrayStatus" = "Xray"
"stopXray" = "Detener"
"restartXray" = "Reiniciar"
"xraySwitch" = "Versión"
@@ -388,10 +388,15 @@
"Inbounds" = "Entrante"
"InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
"Outbounds" = "Salidas"
+"Balancers" = "Equilibradores"
"OutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor."
"Routings" = "Reglas de enrutamiento"
"RoutingsDesc" = "¡La prioridad de cada regla es importante!"
"completeTemplate" = "Todos"
+"logLevel" = "Nivel de registro"
+"logLevelDesc" = "El nivel de registro para registros de errores, que indica la información que debe registrarse."
+"accessLog" = "Registro de acceso"
+"accessLogDesc" = "La ruta del archivo para el registro de acceso. El valor especial 'ninguno' deshabilita los registros de acceso"
[pages.xray.rules]
"first" = "Primero"
@@ -402,6 +407,7 @@
"dest" = "Destino"
"inbound" = "Entrante"
"outbound" = "saliente"
+"balancer" = "Balancín"
"info" = "Información"
"add" = "Agregar regla"
"edit" = "Editar regla"
@@ -422,6 +428,15 @@
"portal" = "portal"
"intercon" = "Interconexión"
+[pages.xray.balancer]
+"addBalancer" = "Agregar equilibrador"
+"editBalancer" = "Editar balanceador"
+"balancerStrategy" = "Estrategia"
+"balancerSelectors" = "Selectores"
+"tag" = "Etiqueta"
+"tagDesc" = "etiqueta única"
+"balancerDesc" = "No es posible utilizar balancerTag y outboundTag al mismo tiempo. Si se utilizan al mismo tiempo, sólo funcionará outboundTag."
+
[pages.xray.wireguard]
"secretKey" = "Llave secreta"
"publicKey" = "Llave pública"
diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml
index 11b66788..5be08677 100644
--- a/web/translation/translate.fa_IR.toml
+++ b/web/translation/translate.fa_IR.toml
@@ -76,7 +76,7 @@
"title" = "نمای کلی"
"memory" = "RAM"
"hard" = "Disk"
-"xrayStatus" = "وضعیتایکسری"
+"xrayStatus" = "ایکسری"
"stopXray" = "توقف"
"restartXray" = "شروعمجدد"
"xraySwitch" = "نسخه"
@@ -388,10 +388,15 @@
"Inbounds" = "ورودیها"
"InboundsDesc" = "پذیرش کلاینت خاص"
"Outbounds" = "خروجیها"
+"Balancers" = "بالانسرها"
"OutboundsDesc" = "مسیر ترافیک خروجی را تنظیم کنید"
"Routings" = "قوانین مسیریابی"
"RoutingsDesc" = "اولویت هر قانون مهم است"
"completeTemplate" = "کامل"
+"logLevel" = "سطح گزارش"
+"logLevelDesc" = "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند."
+"accessLog" = "مسیر گزارش"
+"accessLogDesc" = "مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارشهای دسترسی را غیرفعال میکند."
[pages.xray.rules]
"first" = "اولین"
@@ -402,6 +407,7 @@
"dest" = "مقصد"
"inbound" = "ورودی"
"outbound" = "خروجی"
+"balancer" = "بالانسر"
"info" = "اطلاعات"
"add" = "افزودن قانون"
"edit" = "ویرایش قانون"
@@ -422,6 +428,15 @@
"portal" = "پورتال"
"intercon" = "اتصال میانی"
+[pages.xray.balancer]
+"addBalancer" = "افزودن بالانسر"
+"editBalancer" = "ویرایش بالانسر"
+"balancerStrategy" = "استراتژی"
+"balancerSelectors" = "انتخابگرها"
+"tag" = "برچسب"
+"tagDesc" = "برچسب یگانه"
+"balancerDesc" = "امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد."
+
[pages.xray.wireguard]
"secretKey" = "کلید شخصی"
"publicKey" = "کلید عمومی"
@@ -452,7 +467,7 @@
"wentWrong" = "❌ مشکلی رخ داده است!"
"noIpRecord" = "❗ رکورد IP یافت نشد!"
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
-"unlimited" = "♾ نامحدود"
+"unlimited" = "♾ - نامحدود(ریست)"
"add" = "اضافه کردن"
"month" = "ماه"
"months" = "ماهها"
diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml
new file mode 100644
index 00000000..ad108b05
--- /dev/null
+++ b/web/translation/translate.id_ID.toml
@@ -0,0 +1,591 @@
+"username" = "Nama Pengguna"
+"password" = "Kata Sandi"
+"login" = "Masuk"
+"confirm" = "Konfirmasi"
+"cancel" = "Batal"
+"close" = "Tutup"
+"copy" = "Salin"
+"copied" = "Tersalin"
+"download" = "Unduh"
+"remark" = "Catatan"
+"enable" = "Aktifkan"
+"protocol" = "Protokol"
+"search" = "Cari"
+"filter" = "Filter"
+"loading" = "Memuat..."
+"second" = "Detik"
+"minute" = "Menit"
+"hour" = "Jam"
+"day" = "Hari"
+"check" = "Centang"
+"indefinite" = "Tak Terbatas"
+"unlimited" = "Tanpa Batas"
+"none" = "None"
+"qrCode" = "Kode QR"
+"info" = "Informasi Lebih Lanjut"
+"edit" = "Edit"
+"delete" = "Hapus"
+"reset" = "Reset"
+"copySuccess" = "Berhasil Disalin"
+"sure" = "Yakin"
+"encryption" = "Enkripsi"
+"transmission" = "Transmisi"
+"host" = "Host"
+"path" = "Jalur"
+"camouflage" = "Obfuscation"
+"status" = "Status"
+"enabled" = "Aktif"
+"disabled" = "Nonaktif"
+"depleted" = "Habis"
+"depletingSoon" = "Akan Habis"
+"offline" = "Offline"
+"online" = "Online"
+"domainName" = "Nama Domain"
+"monitor" = "IP Pemantauan"
+"certificate" = "Sertifikat"
+"fail" = "Gagal"
+"success" = "Berhasil"
+"getVersion" = "Dapatkan Versi"
+"install" = "Instal"
+"clients" = "Klien"
+"usage" = "Penggunaan"
+"secretToken" = "Token Rahasia"
+"remained" = "Tersisa"
+"security" = "Keamanan"
+
+[menu]
+"dashboard" = "Ikhtisar"
+"inbounds" = "Masuk"
+"settings" = "Pengaturan Panel"
+"xray" = "Konfigurasi Xray"
+"logout" = "Keluar"
+"link" = "Kelola"
+
+[pages.login]
+"title" = "Selamat Datang"
+"loginAgain" = "Sesi Anda telah berakhir, harap masuk kembali"
+
+[pages.login.toasts]
+"invalidFormData" = "Format data input tidak valid."
+"emptyUsername" = "Nama Pengguna diperlukan"
+"emptyPassword" = "Kata Sandi diperlukan"
+"wrongUsernameOrPassword" = "Nama pengguna atau kata sandi tidak valid."
+"successLogin" = "Login berhasil"
+
+[pages.index]
+"title" = "Ikhtisar"
+"memory" = "RAM"
+"hard" = "Disk"
+"xrayStatus" = "Xray"
+"stopXray" = "Stop"
+"restartXray" = "Restart"
+"xraySwitch" = "Versi"
+"xraySwitchClick" = "Pilih versi yang ingin Anda pindah."
+"xraySwitchClickDesk" = "Pilih dengan hati-hati, karena versi yang lebih lama mungkin tidak kompatibel dengan konfigurasi saat ini."
+"operationHours" = "Waktu Aktif"
+"systemLoad" = "Beban Sistem"
+"systemLoadDesc" = "Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir"
+"connectionTcpCountDesc" = "Total koneksi TCP di seluruh sistem"
+"connectionUdpCountDesc" = "Total koneksi UDP di seluruh sistem"
+"connectionCount" = "Statistik Koneksi"
+"upSpeed" = "Kecepatan unggah keseluruhan di seluruh sistem"
+"downSpeed" = "Kecepatan unduh keseluruhan di seluruh sistem"
+"totalSent" = "Total data terkirim di seluruh sistem sejak startup OS"
+"totalReceive" = "Total data diterima di seluruh sistem sejak startup OS"
+"xraySwitchVersionDialog" = "Ganti Versi Xray"
+"xraySwitchVersionDialogDesc" = "Apakah Anda yakin ingin mengubah versi Xray menjadi"
+"dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini"
+"logs" = "Log"
+"config" = "Konfigurasi"
+"backup" = "Cadangan & Pulihkan"
+"backupTitle" = "Cadangan & Pulihkan Database"
+"backupDescription" = "Disarankan untuk membuat cadangan sebelum memulihkan database."
+"exportDatabase" = "Cadangkan"
+"importDatabase" = "Pulihkan"
+
+[pages.inbounds]
+"title" = "Masuk"
+"totalDownUp" = "Total Terkirim/Diterima"
+"totalUsage" = "Penggunaan Total"
+"inboundCount" = "Total Masuk"
+"operate" = "Menu"
+"enable" = "Aktifkan"
+"remark" = "Catatan"
+"protocol" = "Protokol"
+"port" = "Port"
+"traffic" = "Traffic"
+"details" = "Rincian"
+"transportConfig" = "Transport"
+"expireDate" = "Durasi"
+"resetTraffic" = "Reset Traffic"
+"addInbound" = "Tambahkan Masuk"
+"generalActions" = "Tindakan Umum"
+"create" = "Buat"
+"update" = "Perbarui"
+"modifyInbound" = "Ubah Masuk"
+"deleteInbound" = "Hapus Masuk"
+"deleteInboundContent" = "Apakah Anda yakin ingin menghapus masuk?"
+"deleteClient" = "Hapus Klien"
+"deleteClientContent" = "Apakah Anda yakin ingin menghapus klien?"
+"resetTrafficContent" = "Apakah Anda yakin ingin mereset traffic?"
+"copyLink" = "Salin URL"
+"address" = "Alamat"
+"network" = "Jaringan"
+"destinationPort" = "Port Tujuan"
+"targetAddress" = "Alamat Target"
+"monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP"
+"meansNoLimit" = " = Unlimited. (unit: GB)"
+"totalFlow" = "Total Aliran"
+"leaveBlankToNeverExpire" = "Biarkan kosong untuk tidak pernah kedaluwarsa"
+"noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default"
+"certificatePath" = "Path Berkas"
+"certificateContent" = "Konten Berkas"
+"publicKeyPath" = "Path Kunci Publik"
+"publicKeyContent" = "Konten Kunci Publik"
+"keyPath" = "Path Kunci Privat"
+"keyContent" = "Konten Kunci Privat"
+"clickOnQRcode" = "Klik pada Kode QR untuk Menyalin"
+"client" = "Klien"
+"export" = "Ekspor Semua URL"
+"clone" = "Duplikat"
+"cloneInbound" = "Duplikat"
+"cloneInboundContent" = "Semua pengaturan masuk ini, kecuali Port, Listening IP, dan Klien, akan diterapkan pada duplikat."
+"cloneInboundOk" = "Duplikat"
+"resetAllTraffic" = "Reset Semua Traffic Masuk"
+"resetAllTrafficTitle" = "Reset Semua Traffic Masuk"
+"resetAllTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua masuk?"
+"resetInboundClientTraffics" = "Reset Traffic Klien Masuk"
+"resetInboundClientTrafficTitle" = "Reset Traffic Klien Masuk"
+"resetInboundClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic klien masuk ini?"
+"resetAllClientTraffics" = "Reset Traffic Semua Klien"
+"resetAllClientTrafficTitle" = "Reset Traffic Semua Klien"
+"resetAllClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua klien?"
+"delDepletedClients" = "Hapus Klien Habis"
+"delDepletedClientsTitle" = "Hapus Klien Habis"
+"delDepletedClientsContent" = "Apakah Anda yakin ingin menghapus semua klien yang habis?"
+"email" = "Email"
+"emailDesc" = "Harap berikan alamat email yang unik."
+"IPLimit" = "Batas IP"
+"IPLimitDesc" = "Menonaktifkan masuk jika jumlah melebihi nilai yang ditetapkan. (0 = nonaktif)"
+"IPLimitlog" = "Log IP"
+"IPLimitlogDesc" = "Log histori IP. (untuk mengaktifkan masuk setelah menonaktifkan, hapus log)"
+"IPLimitlogclear" = "Hapus Log"
+"setDefaultCert" = "Atur Sertifikat dari Panel"
+"xtlsDesc" = "Xray harus versi 1.7.5"
+"realityDesc" = "Xray harus versi 1.8.0+"
+"telegramDesc" = "Harap berikan ID Telegram atau obrolan tanpa menggunakan '@'. (dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)"
+"subscriptionDesc" = "Untuk menemukan URL langganan Anda, buka 'Rincian'. Selain itu, Anda dapat menggunakan nama yang sama untuk beberapa klien."
+"info" = "Info"
+"same" = "Sama"
+"inboundData" = "Data Masuk"
+"exportInbound" = "Ekspor Masuk"
+"import" = "Impor"
+"importInbound" = "Impor Masuk"
+
+[pages.client]
+"add" = "Tambah Klien"
+"edit" = "Edit Klien"
+"submitAdd" = "Tambah Klien"
+"submitEdit" = "Simpan Perubahan"
+"clientCount" = "Jumlah Klien"
+"bulk" = "Tambahkan Massal"
+"method" = "Metode"
+"first" = "Pertama"
+"last" = "Terakhir"
+"prefix" = "Awalan"
+"postfix" = "Akhiran"
+"delayedStart" = "Mulai saat Penggunaan Awal"
+"expireDays" = "Durasi"
+"days" = "Hari"
+"renew" = "Perpanjang Otomatis"
+"renewDesc" = "Perpanjangan otomatis setelah kedaluwarsa. (0 = nonaktif)(unit: hari)"
+
+[pages.inbounds.toasts]
+"obtain" = "Dapatkan"
+
+[pages.inbounds.stream.general]
+"request" = "Permintaan"
+"response" = "Respons"
+"name" = "Nama"
+"value" = "Nilai"
+
+[pages.inbounds.stream.tcp]
+"version" = "Versi"
+"method" = "Metode"
+"path" = "Path"
+"status" = "Status"
+"statusDescription" = "Deskripsi Status"
+"requestHeader" = "Header Permintaan"
+"responseHeader" = "Header Respons"
+
+[pages.inbounds.stream.quic]
+"encryption" = "Enkripsi"
+
+[pages.settings]
+"title" = "Pengaturan Panel"
+"save" = "Simpan"
+"infoDesc" = "Setiap perubahan yang dibuat di sini perlu disimpan. Harap restart panel untuk menerapkan perubahan."
+"restartPanel" = "Restart Panel"
+"restartPanelDesc" = "Apakah Anda yakin ingin merestart panel? Jika Anda tidak dapat mengakses panel setelah merestart, lihat info log panel di server."
+"actions" = "Tindakan"
+"resetDefaultConfig" = "Reset ke Default"
+"panelSettings" = "Umum"
+"securitySettings" = "Otentikasi"
+"TGBotSettings" = "Bot Telegram"
+"panelListeningIP" = "IP Pendengar"
+"panelListeningIPDesc" = "Alamat IP untuk panel web. (biarkan kosong untuk mendengarkan semua IP)"
+"panelListeningDomain" = "Domain Pendengar"
+"panelListeningDomainDesc" = "Nama domain untuk panel web. (biarkan kosong untuk mendengarkan semua domain dan IP)"
+"panelPort" = "Port Pendengar"
+"panelPortDesc" = "Nomor port untuk panel web. (harus menjadi port yang tidak digunakan)"
+"publicKeyPath" = "Path Kunci Publik"
+"publicKeyPathDesc" = "Path berkas kunci publik untuk panel web. (dimulai dengan ‘/‘)"
+"privateKeyPath" = "Path Kunci Privat"
+"privateKeyPathDesc" = "Path berkas kunci privat untuk panel web. (dimulai dengan ‘/‘)"
+"panelUrlPath" = "URI Path"
+"panelUrlPathDesc" = "URI path untuk panel web. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)"
+"pageSize" = "Ukuran Halaman"
+"pageSizeDesc" = "Tentukan ukuran halaman untuk tabel masuk. (0 = nonaktif)"
+"remarkModel" = "Model Catatan & Karakter Pemisah"
+"datepicker" = "Jenis Kalender"
+"datepickerPlaceholder" = "Pilih tanggal"
+"datepickerDescription" = "Tugas terjadwal akan berjalan berdasarkan kalender ini."
+"sampleRemark" = "Contoh Catatan"
+"oldUsername" = "Username Saat Ini"
+"currentPassword" = "Kata Sandi Saat Ini"
+"newUsername" = "Username Baru"
+"newPassword" = "Kata Sandi Baru"
+"telegramBotEnable" = "Aktifkan Bot Telegram"
+"telegramBotEnableDesc" = "Mengaktifkan bot Telegram."
+"telegramToken" = "Token Telegram"
+"telegramTokenDesc" = "Token bot Telegram yang diperoleh dari '@BotFather'."
+"telegramProxy" = "Proxy SOCKS"
+"telegramProxyDesc" = "Mengaktifkan proxy SOCKS5 untuk terhubung ke Telegram. (sesuaikan pengaturan sesuai panduan)"
+"telegramChatId" = "ID Obrolan Admin"
+"telegramChatIdDesc" = "ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)"
+"telegramNotifyTime" = "Waktu Notifikasi"
+"telegramNotifyTimeDesc" = "Waktu notifikasi bot Telegram yang diatur untuk laporan berkala. (gunakan format waktu crontab)"
+"tgNotifyBackup" = "Cadangan Database"
+"tgNotifyBackupDesc" = "Kirim berkas cadangan database dengan laporan."
+"tgNotifyLogin" = "Notifikasi Login"
+"tgNotifyLoginDesc" = "Dapatkan notifikasi tentang username, alamat IP, dan waktu setiap kali seseorang mencoba masuk ke panel web Anda."
+"sessionMaxAge" = "Durasi Sesi"
+"sessionMaxAgeDesc" = "Durasi di mana Anda dapat tetap masuk. (unit: menit)"
+"expireTimeDiff" = "Notifikasi Tanggal Kedaluwarsa"
+"expireTimeDiffDesc" = "Dapatkan notifikasi tentang tanggal kedaluwarsa saat mencapai ambang batas ini. (unit: hari)"
+"trafficDiff" = "Notifikasi Batas Traffic"
+"trafficDiffDesc" = "Dapatkan notifikasi tentang batas traffic saat mencapai ambang batas ini. (unit: GB)"
+"tgNotifyCpu" = "Notifikasi Beban CPU"
+"tgNotifyCpuDesc" = "Dapatkan notifikasi jika beban CPU melebihi ambang batas ini. (unit: %)"
+"timeZone" = "Zone Waktu"
+"timeZoneDesc" = "Tugas terjadwal akan berjalan berdasarkan zona waktu ini."
+"subSettings" = "Langganan"
+"subEnable" = "Aktifkan Layanan Langganan"
+"subEnableDesc" = "Mengaktifkan layanan langganan."
+"subListen" = "IP Pendengar"
+"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)"
+"subPort" = "Port Pendengar"
+"subPortDesc" = "Nomor port untuk layanan langganan. (harus menjadi port yang tidak digunakan)"
+"subCertPath" = "Path Kunci Publik"
+"subCertPathDesc" = "Path berkas kunci publik untuk layanan langganan. (dimulai dengan ‘/‘)"
+"subKeyPath" = "Path Kunci Privat"
+"subKeyPathDesc" = "Path berkas kunci privat untuk layanan langganan. (dimulai dengan ‘/‘)"
+"subPath" = "URI Path"
+"subPathDesc" = "URI path untuk layanan langganan. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)"
+"subDomain" = "Domain Pendengar"
+"subDomainDesc" = "Nama domain untuk layanan langganan. (biarkan kosong untuk mendengarkan semua domain dan IP)"
+"subUpdates" = "Interval Pembaruan"
+"subUpdatesDesc" = "Interval pembaruan URL langganan dalam aplikasi klien. (unit: jam)"
+"subEncrypt" = "Encode"
+"subEncryptDesc" = "Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64."
+"subShowInfo" = "Tampilkan Info Penggunaan"
+"subShowInfoDesc" = "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien."
+"subURI" = "URI Proxy Terbalik"
+"subURIDesc" = "URI path URL langganan untuk penggunaan di belakang proxy."
+
+[pages.xray]
+"title" = "Konfigurasi Xray"
+"save" = "Simpan"
+"restart" = "Restart Xray"
+"basicTemplate" = "Dasar"
+"advancedTemplate" = "Lanjutan"
+"generalConfigs" = "Strategi Umum"
+"generalConfigsDesc" = "Opsi ini akan menentukan penyesuaian strategi umum."
+"blockConfigs" = "Pelindung"
+"blockConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan protokol dan situs web yang diminta."
+"blockCountryConfigs" = "Blokir Negara"
+"blockCountryConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan negara yang diminta."
+"directCountryConfigs" = "Langsung ke Negara"
+"directCountryConfigsDesc" = "Opsi ini akan langsung meneruskan lalu lintas berdasarkan negara yang diminta."
+"ipv4Configs" = "Pengalihan IPv4"
+"ipv4ConfigsDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui IPv4."
+"warpConfigs" = "Pengalihan WARP"
+"warpConfigsDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui WARP."
+"Template" = "Template Konfigurasi Xray Lanjutan"
+"TemplateDesc" = "File konfigurasi Xray akhir akan dibuat berdasarkan template ini."
+"FreedomStrategy" = "Strategi Protokol Freedom"
+"FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom."
+"RoutingStrategy" = "Strategi Pengalihan Keseluruhan"
+"RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan."
+"Torrent" = "Blokir Protokol BitTorrent"
+"TorrentDesc" = "Memblokir protokol BitTorrent."
+"PrivateIp" = "Blokir Koneksi ke IP Pribadi"
+"PrivateIpDesc" = "Memblokir pembentukan koneksi ke rentang IP pribadi."
+"Ads" = "Blokir Iklan"
+"AdsDesc" = "Memblokir situs web periklanan."
+"Family" = "Proteksi Keluarga"
+"FamilyDesc" = "Memblokir konten dewasa dan situs web berbahaya."
+"Security" = "Pelindung Keamanan"
+"SecurityDesc" = "Memblokir situs web malware, phishing, dan penambang kripto."
+"Speedtest" = "Blokir Speedtest"
+"SpeedtestDesc" = "Memblokir pembentukan koneksi ke situs web speedtest."
+"IRIp" = "Blokir Koneksi ke IP Iran"
+"IRIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Iran."
+"IRDomain" = "Blokir Koneksi ke Domain Iran"
+"IRDomainDesc" = "Memblokir pembentukan koneksi ke domain Iran."
+"ChinaIp" = "Blokir Koneksi ke IP China"
+"ChinaIpDesc" = "Memblokir pembentukan koneksi ke rentang IP China."
+"ChinaDomain" = "Blokir Koneksi ke Domain China"
+"ChinaDomainDesc" = "Memblokir pembentukan koneksi ke domain China."
+"RussiaIp" = "Blokir Koneksi ke IP Rusia"
+"RussiaIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Rusia."
+"RussiaDomain" = "Blokir Koneksi ke Domain Rusia"
+"RussiaDomainDesc" = "Memblokir pembentukan koneksi ke domain Rusia."
+"VNIp" = "Blokir Koneksi ke IP Vietnam"
+"VNIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Vietnam."
+"VNDomain" = "Blokir Koneksi ke Domain Vietnam"
+"VNDomainDesc" = "Memblokir pembentukan koneksi ke domain Vietnam."
+"DirectIRIp" = "Koneksi Langsung ke IP Iran"
+"DirectIRIpDesc" = "Membentuk koneksi langsung ke rentang IP Iran."
+"DirectIRDomain" = "Koneksi Langsung ke Domain Iran"
+"DirectIRDomainDesc" = "Membentuk koneksi langsung ke domain Iran."
+"DirectChinaIp" = "Koneksi Langsung ke IP China"
+"DirectChinaIpDesc" = "Membentuk koneksi langsung ke rentang IP China."
+"DirectChinaDomain" = "Koneksi Langsung ke Domain China"
+"DirectChinaDomainDesc" = "Membentuk koneksi langsung ke domain China."
+"DirectRussiaIp" = "Koneksi Langsung ke IP Rusia"
+"DirectRussiaIpDesc" = "Membentuk koneksi langsung ke rentang IP Rusia."
+"DirectRussiaDomain" = "Koneksi Langsung ke Domain Rusia"
+"DirectRussiaDomainDesc" = "Membentuk koneksi langsung ke domain Rusia."
+"DirectVNIp" = "Koneksi Langsung ke IP Vietnam"
+"DirectVNIpDesc" = "Membentuk koneksi langsung ke rentang IP Vietnam."
+"DirectVNDomain" = "Koneksi Langsung ke Domain Vietnam"
+"DirectVNDomainDesc" = "Membentuk koneksi langsung ke domain Vietnam."
+"GoogleIPv4" = "Google"
+"GoogleIPv4Desc" = "Rute lalu lintas ke Google melalui IPv4."
+"NetflixIPv4" = "Netflix"
+"NetflixIPv4Desc" = "Rute lalu lintas ke Netflix melalui IPv4."
+"GoogleWARP" = "Google"
+"GoogleWARPDesc" = "Tambahkan pengalihan untuk Google melalui WARP."
+"OpenAIWARP" = "ChatGPT"
+"OpenAIWARPDesc" = "Rute lalu lintas ke ChatGPT melalui WARP."
+"NetflixWARP" = "Netflix"
+"NetflixWARPDesc" = "Rute lalu lintas ke Netflix melalui WARP."
+"SpotifyWARP" = "Spotify"
+"SpotifyWARPDesc" = "Rute lalu lintas ke Spotify melalui WARP."
+"IRWARP" = "Domain Iran"
+"IRWARPDesc" = "Rute lalu lintas ke domain Iran melalui WARP."
+"Inbounds" = "Masuk"
+"InboundsDesc" = "Menerima klien tertentu."
+"Outbounds" = "Keluar"
+"Balancers" = "Penyeimbang"
+"OutboundsDesc" = "Atur jalur lalu lintas keluar."
+"Routings" = "Aturan Pengalihan"
+"RoutingsDesc" = "Prioritas setiap aturan penting!"
+"completeTemplate" = "Semua"
+"logLevel" = "Tingkat Log"
+"logLevelDesc" = "Tingkat log untuk log kesalahan, menunjukkan informasi yang perlu dicatat."
+"accessLog" = "Log Akses"
+"accessLogDesc" = "Jalur file untuk log akses. Nilai khusus 'tidak ada' menonaktifkan log akses"
+
+[pages.xray.rules]
+"first" = "Pertama"
+"last" = "Terakhir"
+"up" = "Naik"
+"down" = "Turun"
+"source" = "Sumber"
+"dest" = "Tujuan"
+"inbound" = "Masuk"
+"outbound" = "Keluar"
+"balancer" = "Pengimbang"
+"info" = "Info"
+"add" = "Tambahkan Aturan"
+"edit" = "Edit Aturan"
+"useComma" = "Item yang dipisahkan koma"
+
+[pages.xray.outbound]
+"addOutbound" = "Tambahkan Keluar"
+"addReverse" = "Tambahkan Revers"
+"editOutbound" = "Edit Keluar"
+"editReverse" = "Edit Revers"
+"tag" = "Tag"
+"tagDesc" = "Tag Unik"
+"address" = "Alamat"
+"reverse" = "Revers"
+"domain" = "Domain"
+"type" = "Tipe"
+"bridge" = "Jembatan"
+"portal" = "Portal"
+"intercon" = "Interkoneksi"
+
+[pages.xray.balancer]
+"addBalancer" = "Tambahkan Penyeimbang"
+"editBalancer" = "Sunting Penyeimbang"
+"balancerStrategy" = "Strategi"
+"balancerSelectors" = "Penyeleksi"
+"tag" = "Menandai"
+"tagDesc" = "Label Unik"
+"balancerDesc" = "BalancerTag dan outboundTag tidak dapat digunakan secara bersamaan. Jika digunakan secara bersamaan, hanya outboundTag yang akan berfungsi."
+
+[pages.xray.wireguard]
+"secretKey" = "Kunci Rahasia"
+"publicKey" = "Kunci Publik"
+"allowedIPs" = "IP yang Diizinkan"
+"endpoint" = "Titik Akhir"
+"psk" = "Kunci Pra-Bagi"
+"domainStrategy" = "Strategi Domain"
+
+[pages.settings.security]
+"admin" = "Admin"
+"secret" = "Token Rahasia"
+"loginSecurity" = "Login Aman"
+"loginSecurityDesc" = "Menambahkan lapisan otentikasi tambahan untuk memberikan keamanan lebih."
+"secretToken" = "Token Rahasia"
+"secretTokenDesc" = "Simpan token ini dengan aman di tempat yang aman. Token ini diperlukan untuk login dan tidak dapat dipulihkan."
+
+[pages.settings.toasts]
+"modifySettings" = "Ubah Pengaturan"
+"getSettings" = "Dapatkan Pengaturan"
+"modifyUser" = "Ubah Admin"
+"originalUserPassIncorrect" = "Username atau password saat ini tidak valid"
+"userPassMustBeNotEmpty" = "Username dan password baru tidak boleh kosong"
+
+[tgbot]
+"keyboardClosed" = "❌ Papan ketik kustom ditutup!"
+"noResult" = "❗ Tidak ada hasil!"
+"noQuery" = "❌ Permintaan tidak ditemukan! Harap gunakan perintah lagi!"
+"wentWrong" = "❌ Ada yang salah!"
+"noIpRecord" = "❗ Tidak ada Catatan IP!"
+"noInbounds" = "❗ Tidak ada masuk ditemukan!"
+"unlimited" = "♾ Tak terbatas"
+"add" = "Tambah"
+"month" = "Bulan"
+"months" = "Bulan"
+"day" = "Hari"
+"days" = "Hari"
+"hours" = "Jam"
+"unknown" = "Tidak diketahui"
+"inbounds" = "Masuk"
+"clients" = "Klien"
+"offline" = "🔴 Offline"
+"online" = "🟢 Online"
+
+[tgbot.commands]
+"unknown" = "❗ Perintah tidak dikenal."
+"pleaseChoose" = "👇 Harap pilih:\r\n"
+"help" = "🤖 Selamat datang di bot ini! Ini dirancang untuk menyediakan data tertentu dari panel web dan memungkinkan Anda melakukan modifikasi sesuai kebutuhan.\r\n\r\n"
+"start" = "👋 Halo {{ .Firstname }}.\r\n"
+"welcome" = "🤖 Selamat datang di {{.Hostname }} bot managemen.\r\n"
+"status" = "✅ Bot dalam keadaan baik!"
+"usage" = "❗ Harap berikan teks untuk mencari!"
+"getID" = "🆔 ID Anda:{{.ID }}
"
+"helpAdminCommands" = "Untuk mencari email klien:\r\n/usage [Email]
\r\n\r\nUntuk mencari masuk (dengan statistik klien):\r\n/inbound [Remark]
"
+"helpClientCommands" = "Untuk mencari statistik, gunakan perintah berikut:\r\n\r\n/usage [Email]
"
+
+[tgbot.messages]
+"cpuThreshold" = "🔴 Beban CPU {{ .Percent }}% melebihi batas {{ .Threshold }}%"
+"selectUserFailed" = "❌ Kesalahan dalam pemilihan pengguna!"
+"userSaved" = "✅ Pengguna Telegram tersimpan."
+"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n"
+"loginFailed" = "❗️ Gagal masuk ke panel.\r\n"
+"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n"
+"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n"
+"hostname" = "💻 Host: {{ .Hostname }}\r\n"
+"version" = "🚀 Versi 3X-UI: {{ .Version }}\r\n"
+"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
+"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
+"ip" = "🌐 IP: {{ .IP }}\r\n"
+"ips" = "🔢 IP:\r\n{{ .IPs }}\r\n"
+"serverUpTime" = "⏳ Waktu Aktif: {{ .UpTime }} {{ .Unit }}\r\n"
+"serverLoad" = "📈 Beban Sistem: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
+"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n"
+"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
+"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
+"traffic" = "🚦 Lalu Lintas: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
+"xrayStatus" = "ℹ️ Status: {{ .State }}\r\n"
+"username" = "👤 Nama Pengguna: {{ .Username }}\r\n"
+"time" = "⏰ Waktu: {{ .Time }}\r\n"
+"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
+"port" = "🔌 Port: {{ .Port }}\r\n"
+"expire" = "📅 Tanggal Kadaluarsa: {{ .Time }}\r\n"
+"expireIn" = "📅 Kadaluarsa Dalam: {{ .Time }}\r\n"
+"active" = "💡 Aktif: {{ .Enable }}\r\n"
+"enabled" = "🚨 Diaktifkan: {{ .Enable }}\r\n"
+"online" = "🌐 Status Koneksi: {{ .Status }}\r\n"
+"email" = "📧 Email: {{ .Email }}\r\n"
+"upload" = "🔼 Unggah: ↑{{ .Upload }}\r\n"
+"download" = "🔽 Unduh: ↓{{ .Download }}\r\n"
+"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
+"TGUser" = "👤 Pengguna Telegram: {{ .TelegramID }}\r\n"
+"exhaustedMsg" = "🚨 Habis {{ .Type }}:\r\n"
+"exhaustedCount" = "🚨 Jumlah Habis {{ .Type }}:\r\n"
+"onlinesCount" = "🌐 Klien Online: {{ .Count }}\r\n"
+"disabled" = "🛑 Dinonaktifkan: {{ .Disabled }}\r\n"
+"depleteSoon" = "🔜 Habis Sebentar: {{ .Deplete }}\r\n\r\n"
+"backupTime" = "🗄 Waktu Backup: {{ .Time }}\r\n"
+"refreshedOn" = "\r\n📋🔄 Diperbarui Pada: {{ .Time }}\r\n\r\n"
+"yes" = "✅ Ya"
+"no" = "❌ Tidak"
+
+[tgbot.buttons]
+"closeKeyboard" = "❌ Tutup Papan Ketik"
+"cancel" = "❌ Batal"
+"cancelReset" = "❌ Batal Reset"
+"cancelIpLimit" = "❌ Batal Batas IP"
+"confirmResetTraffic" = "✅ Konfirmasi Reset Lalu Lintas?"
+"confirmClearIps" = "✅ Konfirmasi Hapus IPs?"
+"confirmRemoveTGUser" = "✅ Konfirmasi Hapus Pengguna Telegram?"
+"confirmToggle" = "✅ Konfirmasi Aktifkan/Nonaktifkan Pengguna?"
+"dbBackup" = "Dapatkan Cadangan DB"
+"serverUsage" = "Penggunaan Server"
+"getInbounds" = "Dapatkan Inbounds"
+"depleteSoon" = "Habis Sebentar"
+"clientUsage" = "Dapatkan Penggunaan"
+"onlines" = "Klien Online"
+"commands" = "Perintah"
+"refresh" = "🔄 Perbarui"
+"clearIPs" = "❌ Hapus IPs"
+"removeTGUser" = "❌ Hapus Pengguna Telegram"
+"selectTGUser" = "👤 Pilih Pengguna Telegram"
+"selectOneTGUser" = "👤 Pilih Pengguna Telegram:"
+"resetTraffic" = "📈 Reset Lalu Lintas"
+"resetExpire" = "📅 Ubah Tanggal Kadaluarsa"
+"ipLog" = "🔢 Log IP"
+"ipLimit" = "🔢 Batas IP"
+"setTGUser" = "👤 Set Pengguna Telegram"
+"toggle" = "🔘 Aktifkan / Nonaktifkan"
+"custom" = "🔢 Kustom"
+"confirmNumber" = "✅ Konfirmasi: {{ .Num }}"
+"confirmNumberAdd" = "✅ Konfirmasi menambahkan: {{ .Num }}"
+"limitTraffic" = "🚧 Batas Lalu Lintas"
+"getBanLogs" = "Dapatkan Log Pemblokiran"
+
+[tgbot.answers]
+"successfulOperation" = "✅ Operasi berhasil!"
+"errorOperation" = "❗ Kesalahan dalam operasi."
+"getInboundsFailed" = "❌ Gagal mendapatkan inbounds."
+"canceled" = "❌ {{ .Email }}: Operasi dibatalkan."
+"clientRefreshSuccess" = "✅ {{ .Email }}: Klien diperbarui dengan berhasil."
+"IpRefreshSuccess" = "✅ {{ .Email }}: IP diperbarui dengan berhasil."
+"TGIdRefreshSuccess" = "✅ {{ .Email }}: Pengguna Telegram Klien diperbarui dengan berhasil."
+"resetTrafficSuccess" = "✅ {{ .Email }}: Lalu lintas direset dengan berhasil."
+"setTrafficLimitSuccess" = "✅ {{ .Email }}: Batas lalu lintas disimpan dengan berhasil."
+"expireResetSuccess" = "✅ {{ .Email }}: Hari kadaluarsa direset dengan berhasil."
+"resetIpSuccess" = "✅ {{ .Email }}: Batas IP {{ .Count }} disimpan dengan berhasil."
+"clearIpSuccess" = "✅ {{ .Email }}: IP dihapus dengan berhasil."
+"getIpLog" = "✅ {{ .Email }}: Dapatkan Log IP."
+"getUserInfo" = "✅ {{ .Email }}: Dapatkan Info Pengguna Telegram."
+"removedTGUserSuccess" = "✅ {{ .Email }}: Pengguna Telegram dihapus dengan berhasil."
+"enableSuccess" = "✅ {{ .Email }}: Diaktifkan dengan berhasil."
+"disableSuccess" = "✅ {{ .Email }}: Dinonaktifkan dengan berhasil."
+"askToAddUserId" = "Konfigurasi Anda tidak ditemukan!\r\nSilakan minta admin Anda untuk menggunakan ID Telegram Anda dalam konfigurasi Anda.\r\n\r\nID Pengguna Anda: {{ .TgUserID }}
"
diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml
index 2e663e4c..8badec04 100644
--- a/web/translation/translate.ru_RU.toml
+++ b/web/translation/translate.ru_RU.toml
@@ -76,7 +76,7 @@
"title" = "Статус системы"
"memory" = "Память"
"hard" = "Жесткий диск"
-"xrayStatus" = "Статус"
+"xrayStatus" = "Xray"
"stopXray" = "Остановить"
"restartXray" = "Перезапустить"
"xraySwitch" = "Версия"
@@ -388,10 +388,15 @@
"Inbounds" = "Входящие"
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных пользователей"
"Outbounds" = "Исходящие"
+"Balancers" = "Балансиры"
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера"
"Routings" = "Правила маршрутизации"
"RoutingsDesc" = "Важен приоритет каждого правила!"
"completeTemplate" = "Все"
+"logLevel" = "Уровень журнала"
+"logLevelDesc" = "Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать."
+"accessLog" = "Журнал доступа"
+"accessLogDesc" = "Путь к файлу журнала доступа. Специальное значение «none» отключило журналы доступа."
[pages.xray.rules]
"first" = "Первый"
@@ -402,6 +407,7 @@
"dest" = "Пункт назначения"
"inbound" = "Входящий"
"outbound" = "Исходящий"
+"balancer" = "балансир"
"info" = "Информация"
"add" = "Добавить правило"
"edit" = "Редактировать правило"
@@ -422,6 +428,15 @@
"portal" = "Портал"
"intercon" = "Соединение"
+[pages.xray.balancer]
+"addBalancer" = "Добавить балансир"
+"editBalancer" = "Редактировать балансир"
+"balancerStrategy" = "Стратегия"
+"balancerSelectors" = "Селекторы"
+"tag" = "Тег"
+"tagDesc" = "уникальный тег"
+"balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag."
+
[pages.xray.wireguard]
"secretKey" = "Секретный ключ"
"publicKey" = "Открытый ключ"
diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml
index 4c40adca..ad987cf3 100644
--- a/web/translation/translate.vi_VN.toml
+++ b/web/translation/translate.vi_VN.toml
@@ -20,7 +20,7 @@
"check" = "Kiểm tra"
"indefinite" = "Không xác định"
"unlimited" = "Không giới hạn"
-"none" = "Không có"
+"none" = "None"
"qrCode" = "Mã QR"
"info" = "Thông tin thêm"
"edit" = "Chỉnh sửa"
@@ -76,7 +76,7 @@
"title" = "Trạng thái hệ thống"
"memory" = "Ram"
"hard" = "Dung lượng"
-"xrayStatus" = "Trạng thái Xray"
+"xrayStatus" = "Xray"
"stopXray" = "Dừng lại"
"restartXray" = "Khởi động lại"
"xraySwitch" = "Phiên bản"
@@ -388,10 +388,15 @@
"Inbounds" = "Đầu vào"
"InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể."
"Outbounds" = "Đầu ra"
+"Balancers" = "Cân bằng"
"OutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này."
"Routings" = "Quy tắc định tuyến"
"RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!"
"completeTemplate" = "All"
+"logLevel" = "Mức đăng nhập"
+"logLevelDesc" = "Cấp độ nhật ký cho nhật ký lỗi, cho biết thông tin cần được ghi lại."
+"accessLog" = "Nhật ký truy cập"
+"accessLogDesc" = "Đường dẫn tệp cho nhật ký truy cập. Nhật ký truy cập bị vô hiệu hóa có giá trị đặc biệt 'không'"
[pages.xray.rules]
"first" = "Đầu tiên"
@@ -402,6 +407,7 @@
"dest" = "Đích"
"inbound" = "Vào"
"outbound" = "Ra"
+"balancer" = "Cân bằng"
"info" = "Thông tin"
"add" = "Thêm quy tắc"
"edit" = "Chỉnh sửa quy tắc"
@@ -422,6 +428,15 @@
"portal" = "Cổng thông tin"
"intercon" = "Kết nối"
+[pages.xray.balancer]
+"addBalancer" = "Thêm cân bằng"
+"editBalancer" = "Chỉnh sửa cân bằng"
+"balancerStrategy" = "Chiến lược"
+"balancerSelectors" = "Bộ chọn"
+"tag" = "Thẻ"
+"tagDesc" = "thẻ duy nhất"
+"balancerDesc" = "Không thể sử dụng balancerTag và outboundTag cùng một lúc. Nếu sử dụng cùng lúc thì chỉ outboundTag mới hoạt động."
+
[pages.xray.wireguard]
"secretKey" = "Khoá bí mật"
"publicKey" = "Khóa công khai"
diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml
index a8cd3aff..7a10e65d 100644
--- a/web/translation/translate.zh_Hans.toml
+++ b/web/translation/translate.zh_Hans.toml
@@ -76,7 +76,7 @@
"title" = "系统状态"
"memory" = "内存"
"hard" = "硬盘"
-"xrayStatus" = "状态"
+"xrayStatus" = "Xray"
"stopXray" = "停止"
"restartXray" = "重启"
"xraySwitch" = "版本"
@@ -388,10 +388,15 @@
"Inbounds" = "入站"
"InboundsDesc" = "更改配置模板接受特殊客户端"
"Outbounds" = "出站"
+"Balancers" = "平衡器"
"OutboundsDesc" = "更改配置模板定义此服务器的传出方式"
"Routings" = "路由规则"
"RoutingsDesc" = "每条规则的优先级都很重要"
"completeTemplate" = "全部"
+"logLevel" = "日志级别"
+"logLevelDesc" = "错误日志的日志级别,表示需要记录的信息。"
+"accessLog" = "访问日志"
+"accessLogDesc" = "访问日志的文件路径。 特殊值“none”禁用访问日志"
[pages.xray.rules]
"first" = "第一个"
@@ -402,6 +407,7 @@
"dest" = "目的地"
"inbound" = "入站"
"outbound" = "出站"
+"balancer" = "平衡器"
"info" = "信息"
"add" = "添加规则"
"edit" = "编辑规则"
@@ -422,6 +428,15 @@
"portal" = "门户"
"intercon" = "互连"
+[pages.xray.balancer]
+"addBalancer" = "添加平衡器"
+"editBalancer" = "编辑平衡器"
+"balancerStrategy" = "战略"
+"balancerSelectors" = "选择器"
+"tag" = "标签"
+"tagDesc" = "唯一标记"
+"balancerDesc" = "不能同时使用balancerTag和outboundTag。 如果同时使用,则只有outboundTag起作用。"
+
[pages.xray.wireguard]
"secretKey" = "密钥"
"publicKey" = "公钥"
diff --git a/x-ui.sh b/x-ui.sh
index ffb20f06..518b9232 100644
--- a/x-ui.sh
+++ b/x-ui.sh
@@ -150,6 +150,12 @@ custom_version() {
eval $install_command
}
+# Function to handle the deletion of the script file
+delete_script() {
+ rm "$0" # Remove the script file itself
+ exit 1
+}
+
uninstall() {
confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n"
if [[ $? != 0 ]]; then
@@ -167,12 +173,13 @@ uninstall() {
rm /usr/local/x-ui/ -rf
echo ""
- echo -e "Uninstalled Successfully, If you want to remove this script, then after exiting the script run ${green}rm /usr/bin/x-ui -f${plain} to delete it."
+ echo -e "Uninstalled Successfully.\n"
+ echo "If you need to install this panel again, you can use below command:"
+ echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)${plain}"
echo ""
-
- if [[ $# == 0 ]]; then
- before_show_menu
- fi
+ # Trap the SIGTERM signal
+ trap delete_script SIGTERM
+ delete_script
}
reset_user() {
@@ -483,6 +490,33 @@ show_xray_status() {
fi
}
+firewall_menu() {
+ echo -e "${green}\t1.${plain} Install Firewall & open ports"
+ echo -e "${green}\t2.${plain} Allowed List"
+ echo -e "${green}\t3.${plain} Delete Ports from List"
+ echo -e "${green}\t4.${plain} Disable Firewall"
+ echo -e "${green}\t0.${plain} Back to Main Menu"
+ read -p "Choose an option: " choice
+ case "$choice" in
+ 0)
+ show_menu
+ ;;
+ 1)
+ open_ports
+ ;;
+ 2)
+ sudo ufw status
+ ;;
+ 3)
+ delete_ports
+ ;;
+ 4)
+ sudo ufw disable
+ ;;
+ *) echo "Invalid choice" ;;
+ esac
+}
+
open_ports() {
if ! command -v ufw &>/dev/null; then
echo "ufw firewall is not installed. Installing now..."
@@ -535,6 +569,37 @@ open_ports() {
ufw status | grep $ports
}
+delete_ports() {
+ # Prompt the user to enter the ports they want to delete
+ read -p "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports
+
+ # Check if the input is valid
+ if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
+ echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2
+ exit 1
+ fi
+
+ # Delete the specified ports using ufw
+ IFS=',' read -ra PORT_LIST <<<"$ports"
+ for port in "${PORT_LIST[@]}"; do
+ if [[ $port == *-* ]]; then
+ # Split the range into start and end ports
+ start_port=$(echo $port | cut -d'-' -f1)
+ end_port=$(echo $port | cut -d'-' -f2)
+ # Loop through the range and delete each port
+ for ((i = start_port; i <= end_port; i++)); do
+ ufw delete allow $i
+ done
+ else
+ ufw delete allow "$port"
+ fi
+ done
+
+ # Confirm that the ports are deleted
+ echo "Deleted the specified ports:"
+ ufw status | grep $ports
+}
+
update_geo() {
local defaultBinFolder="/usr/local/x-ui/bin"
read -p "Please enter x-ui bin folder path. Leave blank for default. (Default: '${defaultBinFolder}')" binFolder
@@ -1124,10 +1189,10 @@ show_menu() {
${green}17.${plain} Cloudflare SSL Certificate
${green}18.${plain} IP Limit Management
${green}19.${plain} WARP Management
+ ${green}20.${plain} Firewall Management
————————————————
- ${green}20.${plain} Enable BBR
- ${green}21.${plain} Update Geo Files
- ${green}22.${plain} Active Firewall and open ports
+ ${green}21.${plain} Enable BBR
+ ${green}22.${plain} Update Geo Files
${green}23.${plain} Speedtest by Ookla
"
show_status
@@ -1195,13 +1260,13 @@ show_menu() {
warp_cloudflare
;;
20)
- enable_bbr
+ firewall_menu
;;
21)
- update_geo
+ enable_bbr
;;
22)
- open_ports
+ update_geo
;;
23)
run_speedtest
diff --git a/xray/api.go b/xray/api.go
index 36b19875..1ce5afa1 100644
--- a/xray/api.go
+++ b/xray/api.go
@@ -213,6 +213,7 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
continue
}
isInbound := matchs[1] == "inbound"
+ isOutbound := matchs[1] == "outbound"
tag := matchs[2]
isDown := matchs[3] == "downlink"
if tag == "api" {
@@ -221,8 +222,9 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
traffic, ok := tagTrafficMap[tag]
if !ok {
traffic = &Traffic{
- IsInbound: isInbound,
- Tag: tag,
+ IsInbound: isInbound,
+ IsOutbound: isOutbound,
+ Tag: tag,
}
tagTrafficMap[tag] = traffic
traffics = append(traffics, traffic)
diff --git a/xray/log_writer.go b/xray/log_writer.go
index 5fc6b3d1..53358ca2 100644
--- a/xray/log_writer.go
+++ b/xray/log_writer.go
@@ -31,7 +31,7 @@ func (lw *LogWriter) Write(m []byte) (n int, err error) {
// Find level in []
startIndex := strings.Index(messageBody, "[")
endIndex := strings.Index(messageBody, "]")
- if startIndex != -1 && endIndex != -1 {
+ if startIndex != -1 && endIndex != -1 && startIndex < endIndex {
level := strings.TrimSpace(messageBody[startIndex+1 : endIndex])
msgBody := "XRAY: " + strings.TrimSpace(messageBody[endIndex+1:])
diff --git a/xray/process.go b/xray/process.go
index 093cf69d..e37a0649 100644
--- a/xray/process.go
+++ b/xray/process.go
@@ -41,10 +41,6 @@ func GetIPLimitLogPath() string {
return config.GetLogFolder() + "/3xipl.log"
}
-func GetIPLimitPrevLogPath() string {
- return config.GetLogFolder() + "/3xipl.prev.log"
-}
-
func GetIPLimitBannedLogPath() string {
return config.GetLogFolder() + "/3xipl-banned.log"
}
diff --git a/xray/traffic.go b/xray/traffic.go
index a1ef5186..7b907bae 100644
--- a/xray/traffic.go
+++ b/xray/traffic.go
@@ -1,8 +1,9 @@
package xray
type Traffic struct {
- IsInbound bool
- Tag string
- Up int64
- Down int64
+ IsInbound bool
+ IsOutbound bool
+ Tag string
+ Up int64
+ Down int64
}