diff --git a/database/db.go b/database/db.go
index 4a6e9115..7ac49d91 100644
--- a/database/db.go
+++ b/database/db.go
@@ -39,18 +39,6 @@ func initUser() error {
return nil
}
-func initInbound() error {
- return db.AutoMigrate(&model.Inbound{})
-}
-
-func initSetting() error {
- return db.AutoMigrate(&model.Setting{})
-}
-
-func initClientTraffic() error {
- return db.AutoMigrate(&xray.ClientTraffic{})
-}
-
func InitDB(dbPath string) error {
dir := path.Dir(dbPath)
err := os.MkdirAll(dir, fs.ModeDir)
@@ -78,16 +66,12 @@ func InitDB(dbPath string) error {
if err != nil {
return err
}
- err = initInbound()
- if err != nil {
- return err
- }
- err = initSetting()
- if err != nil {
- return err
- }
-
- err = initClientTraffic()
+ err = db.AutoMigrate(
+ &model.Inbound{},
+ &model.Outbound{},
+ &model.Setting{},
+ &xray.ClientTraffic{},
+ )
if err != nil {
return err
}
diff --git a/database/model/model.go b/database/model/model.go
index 275d419b..2ab550f8 100644
--- a/database/model/model.go
+++ b/database/model/model.go
@@ -46,6 +46,47 @@ type Inbound struct {
Sniffing string `json:"sniffing" form:"sniffing"`
}
+type Outbound struct {
+ Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
+ Up int64 `json:"up" form:"up"`
+ Down int64 `json:"down" form:"down"`
+ Sort int `json:"sort" form:"sort"`
+ SendThrough string `json:"sendThrough" form:"sendThrough"`
+ Protocol string `json:"protocol" form:"protocol"`
+ Settings string `json:"settings" form:"settings"`
+ Tag string `json:"tag" form:"tag" gorm:"unique"`
+ StreamSettings string `json:"streamSettings" form:"streamSettings"`
+ ProxySettings string `json:"proxySettings" form:"proxySettings"`
+ Mux string `json:"mux" form:"mux"`
+ TargetStrategy string `json:"targetStrategy" form:"targetStrategy"`
+}
+
+func (o *Outbound) GenXrayOutboundConfig() *xray.OutboundConfig {
+ cfg := &xray.OutboundConfig{
+ Protocol: o.Protocol,
+ Tag: o.Tag,
+ TargetStrategy: o.TargetStrategy,
+ }
+ if o.SendThrough != "" {
+ cfg.SendThrough = json_util.RawMessage(fmt.Sprintf("\"%s\"", o.SendThrough))
+ }
+ if len(o.Settings) > 0 {
+ cfg.Settings = json_util.RawMessage(o.Settings)
+ } else {
+ cfg.Settings = json_util.RawMessage("{}")
+ }
+ if len(o.StreamSettings) > 0 {
+ cfg.StreamSettings = json_util.RawMessage(o.StreamSettings)
+ }
+ if len(o.ProxySettings) > 0 {
+ cfg.ProxySettings = json_util.RawMessage(o.ProxySettings)
+ }
+ if len(o.Mux) > 0 {
+ cfg.Mux = json_util.RawMessage(o.Mux)
+ }
+ return cfg
+}
+
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
listen := i.Listen
if listen != "" {
diff --git a/go.mod b/go.mod
index 8fbc5bb7..0d82f2e2 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/alireza0/x-ui
-go 1.26.1
+go 1.26.3
require (
github.com/gin-contrib/gzip v1.2.6
@@ -10,13 +10,13 @@ require (
github.com/goccy/go-json v0.10.6
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
- github.com/pelletier/go-toml/v2 v2.3.0
+ github.com/pelletier/go-toml/v2 v2.3.1
github.com/robfig/cron/v3 v3.0.1
- github.com/shirou/gopsutil/v4 v4.26.3
+ github.com/shirou/gopsutil/v4 v4.26.4
github.com/xtls/xray-core v1.260327.0
go.uber.org/atomic v1.11.0
- golang.org/x/text v0.35.0
- google.golang.org/grpc v1.80.0
+ golang.org/x/text v0.37.0
+ google.golang.org/grpc v1.81.1
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)
@@ -25,10 +25,10 @@ require (
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect
- github.com/bytedance/sonic v1.15.0 // indirect
+ github.com/bytedance/sonic v1.15.1 // indirect
github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
- github.com/cloudwego/base64x v0.1.6 // indirect
+ github.com/cloudwego/base64x v0.1.7 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect
@@ -50,38 +50,38 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mattn/go-sqlite3 v1.14.40 // indirect
+ github.com/mattn/go-isatty v0.0.22 // indirect
+ github.com/mattn/go-sqlite3 v1.14.44 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.11.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
- github.com/quic-go/quic-go v0.59.0 // indirect
+ github.com/quic-go/quic-go v0.59.1 // indirect
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
- github.com/sagernet/sing v0.8.4 // indirect
+ github.com/sagernet/sing v0.8.10 // indirect
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
- github.com/tklauser/go-sysconf v0.3.16 // indirect
- github.com/tklauser/numcpus v0.11.0 // indirect
+ github.com/tklauser/go-sysconf v0.4.0 // indirect
+ github.com/tklauser/numcpus v0.12.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
- go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
+ go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
- golang.org/x/arch v0.25.0 // indirect
- golang.org/x/crypto v0.49.0 // indirect
+ golang.org/x/arch v0.27.0 // indirect
+ golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
- golang.org/x/mod v0.34.0 // indirect
- golang.org/x/net v0.52.0 // indirect
+ golang.org/x/mod v0.36.0 // indirect
+ golang.org/x/net v0.54.0 // indirect
golang.org/x/sync v0.20.0 // indirect
- golang.org/x/sys v0.42.0 // indirect
+ golang.org/x/sys v0.44.0 // indirect
golang.org/x/time v0.15.0 // indirect
- golang.org/x/tools v0.43.0 // indirect
+ golang.org/x/tools v0.45.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
diff --git a/go.sum b/go.sum
index c121b4dd..c2e33b5d 100644
--- a/go.sum
+++ b/go.sum
@@ -8,6 +8,8 @@ github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
+github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
+github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -16,6 +18,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
+github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
+github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -97,8 +101,12 @@ github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRt
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
+github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-sqlite3 v1.14.40 h1:f7+saIsbq4EF86mUqe0uiecQOJYMOdfi5uATADmUG94=
github.com/mattn/go-sqlite3 v1.14.40/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
+github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
+github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -114,6 +122,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
+github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -124,6 +134,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
+github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
+github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
@@ -132,10 +144,14 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.8.4 h1:Fj+jlY3F8vhcRfz/G/P3Dwcs5wqnmyNPT7u1RVVmjFI=
github.com/sagernet/sing v0.8.4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
+github.com/sagernet/sing v0.8.10 h1:V5VZffy8rm4dtBVKIpKa8vibRR2SiJprtu/10DFUalU=
+github.com/sagernet/sing v0.8.10/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
+github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
+github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -149,8 +165,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
+github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
+github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
+github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
+github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
@@ -169,18 +189,25 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
+go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
+go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
+go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
+go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
+go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
+go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
@@ -191,14 +218,28 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
+golang.org/x/arch v0.27.0 h1:0WNVcR8u9yFz8j5FvdHpgwNp3FS5U4guYdzHwEiGjoU=
+golang.org/x/arch v0.27.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
+golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
+golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
+golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
+golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
+golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
+golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
+golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
+golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
+golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
+golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
+golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -209,12 +250,22 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
+golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
+golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
+golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
+golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
+golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
@@ -225,6 +276,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
+google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
+google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/main.go b/main.go
index df47dbc7..ed400d40 100644
--- a/main.go
+++ b/main.go
@@ -47,6 +47,9 @@ func runWebServer() {
log.Fatal(err)
}
+ outboundService := service.OutboundService{}
+ outboundService.MigrateDB()
+
var server *web.Server
server = web.NewServer()
@@ -364,6 +367,7 @@ func getPanelURI() {
func migrateDb() {
inboundService := service.InboundService{}
+ outboundService := service.OutboundService{}
err := database.InitDB(config.GetDBPath())
if err != nil {
@@ -371,6 +375,7 @@ func migrateDb() {
}
fmt.Println("Start migrating database...")
inboundService.MigrateDB()
+ outboundService.MigrateDB()
fmt.Println("Migration done!")
}
diff --git a/web/assets/js/model/dboutbound.js b/web/assets/js/model/dboutbound.js
new file mode 100644
index 00000000..fa87e15e
--- /dev/null
+++ b/web/assets/js/model/dboutbound.js
@@ -0,0 +1,64 @@
+class DBOutbound {
+
+ constructor(data) {
+ this.id = 0;
+ this.up = 0;
+ this.down = 0;
+ this.sort = 0;
+ this.sendThrough = "";
+ this.protocol = "";
+ this.settings = "";
+ this.tag = "";
+ this.streamSettings = "";
+ this.proxySettings = "";
+ this.mux = "";
+ this.targetStrategy = "";
+ if (data == null) {
+ return;
+ }
+ ObjectUtil.cloneProps(this, data);
+ }
+
+ toOutbound() {
+ const config = {
+ protocol: this.protocol,
+ tag: this.tag,
+ };
+ if (!ObjectUtil.isEmpty(this.sendThrough)) {
+ config.sendThrough = this.sendThrough;
+ }
+ if (!ObjectUtil.isEmpty(this.settings)) {
+ config.settings = JSON.parse(this.settings);
+ }
+ if (!ObjectUtil.isEmpty(this.streamSettings)) {
+ config.streamSettings = JSON.parse(this.streamSettings);
+ }
+ if (!ObjectUtil.isEmpty(this.proxySettings)) {
+ config.proxySettings = JSON.parse(this.proxySettings);
+ }
+ if (!ObjectUtil.isEmpty(this.mux)) {
+ config.mux = JSON.parse(this.mux);
+ }
+ if (!ObjectUtil.isEmpty(this.targetStrategy)) {
+ config.targetStrategy = this.targetStrategy;
+ }
+ return Outbound.fromJson(config);
+ }
+
+ static payloadFromOutbound(outbound, db) {
+ const json = outbound.toJson();
+ const data = {
+ up: db.up,
+ down: db.down,
+ sendThrough: json.sendThrough || "",
+ protocol: json.protocol,
+ tag: json.tag || "",
+ targetStrategy: json.targetStrategy || "",
+ settings: JSON.stringify(json.settings ?? {}, null, 2),
+ streamSettings: json.streamSettings ? JSON.stringify(json.streamSettings, null, 2) : "",
+ proxySettings: json.proxySettings ? JSON.stringify(json.proxySettings, null, 2) : "",
+ mux: json.mux ? JSON.stringify(json.mux, null, 2) : "",
+ };
+ return data;
+ }
+}
diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js
index fd27b374..f3fab299 100644
--- a/web/assets/js/model/outbound.js
+++ b/web/assets/js/model/outbound.js
@@ -1237,7 +1237,7 @@ class Outbound extends CommonClass {
}
static fromLink(link) {
- data = link.split('://');
+ var data = link.split('://');
if (data.length != 2) return null;
switch (data[0].toLowerCase()) {
case Protocols.VMess:
diff --git a/web/controller/outbound.go b/web/controller/outbound.go
new file mode 100644
index 00000000..bffa9f6d
--- /dev/null
+++ b/web/controller/outbound.go
@@ -0,0 +1,118 @@
+package controller
+
+import (
+ "strconv"
+
+ "github.com/alireza0/x-ui/database/model"
+ "github.com/alireza0/x-ui/web/service"
+ "github.com/gin-gonic/gin"
+)
+
+type OutboundController struct {
+ outboundService service.OutboundService
+ xrayService service.XrayService
+}
+
+func NewOutboundController(g *gin.RouterGroup) *OutboundController {
+ a := &OutboundController{}
+ a.initRouter(g)
+ return a
+}
+
+func (a *OutboundController) initRouter(g *gin.RouterGroup) {
+ g = g.Group("/outbound")
+
+ g.POST("/list", a.getOutbounds)
+ g.POST("/add", a.addOutbound)
+ g.POST("/del/:id", a.delOutbound)
+ g.POST("/update/:id", a.updateOutbound)
+ g.POST("/setFirst/:id", a.setFirstOutbound)
+ g.POST("/:id/resetTraffic", a.resetTraffic)
+ g.POST("/resetAllTraffics", a.resetAllTraffics)
+ g.POST("/onlines", a.onlines)
+}
+
+func (a *OutboundController) getOutbounds(c *gin.Context) {
+ outbounds, err := a.outboundService.GetAllOutbounds()
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.outbounds.toasts.obtain"), err)
+ return
+ }
+ jsonObj(c, outbounds, nil)
+}
+
+func (a *OutboundController) addOutbound(c *gin.Context) {
+ outbound := &model.Outbound{}
+ err := c.ShouldBind(outbound)
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.outbounds.create"), err)
+ return
+ }
+ outbound, needRestart, err := a.outboundService.AddOutbound(outbound)
+ jsonMsgObj(c, I18nWeb(c, "pages.outbounds.create"), outbound, err)
+ if err == nil && needRestart {
+ a.xrayService.SetToNeedRestart()
+ }
+}
+
+func (a *OutboundController) delOutbound(c *gin.Context) {
+ id, err := strconv.Atoi(c.Param("id"))
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.outbounds.delete"), err)
+ return
+ }
+ needRestart, err := a.outboundService.DelOutbound(id)
+ jsonMsg(c, I18nWeb(c, "pages.outbounds.delete"), err)
+ if err == nil && needRestart {
+ a.xrayService.SetToNeedRestart()
+ }
+}
+
+func (a *OutboundController) updateOutbound(c *gin.Context) {
+ id, err := strconv.Atoi(c.Param("id"))
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.outbounds.update"), err)
+ return
+ }
+ outbound := &model.Outbound{Id: id}
+ err = c.ShouldBind(outbound)
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.outbounds.update"), err)
+ return
+ }
+ outbound, needRestart, err := a.outboundService.UpdateOutbound(outbound)
+ jsonMsgObj(c, I18nWeb(c, "pages.outbounds.update"), outbound, err)
+ if err == nil && needRestart {
+ a.xrayService.SetToNeedRestart()
+ }
+}
+
+func (a *OutboundController) setFirstOutbound(c *gin.Context) {
+ id, err := strconv.Atoi(c.Param("id"))
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.outbounds.update"), err)
+ return
+ }
+ err = a.outboundService.SetFirstOutbound(id)
+ if err == nil {
+ a.xrayService.SetToNeedRestart()
+ }
+ jsonMsg(c, I18nWeb(c, "pages.outbounds.update"), err)
+}
+
+func (a *OutboundController) resetTraffic(c *gin.Context) {
+ id, err := strconv.Atoi(c.Param("id"))
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.outbounds.resetTraffic"), err)
+ return
+ }
+ jsonMsg(c, I18nWeb(c, "pages.outbounds.resetTraffic"), a.outboundService.ResetTraffic(id))
+}
+
+func (a *OutboundController) resetAllTraffics(c *gin.Context) {
+ jsonMsg(c, I18nWeb(c, "pages.outbounds.resetAllTraffic"), a.outboundService.ResetAllTraffics())
+}
+
+func (a *OutboundController) onlines(c *gin.Context) {
+ jsonObj(c, a.outboundService.GetOnlineOutbounds(), nil)
+}
diff --git a/web/controller/xray_setting.go b/web/controller/xray_setting.go
index 53f19d08..c0fb5cd7 100644
--- a/web/controller/xray_setting.go
+++ b/web/controller/xray_setting.go
@@ -12,6 +12,7 @@ type XraySettingController struct {
XraySettingService service.XraySettingService
SettingService service.SettingService
InboundService service.InboundService
+ OutboundService service.OutboundService
XrayService service.XrayService
WarpService service.WarpService
}
@@ -47,10 +48,26 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
if err != nil {
clientReverseTags = "[]"
}
+ outboundTags, err := a.OutboundService.GetOutboundTags()
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
+ return
+ }
+ outboundReverseTags, err := a.OutboundService.GetOutboundReverseTags()
+ if err != nil {
+ outboundReverseTags = "[]"
+ }
+ outboundSummaries, err := a.OutboundService.GetOutboundSummariesJSON()
+ if err != nil {
+ outboundSummaries = "[]"
+ }
xrayResponse := map[string]any{
- "xraySetting": json.RawMessage(xraySetting),
- "inboundTags": json.RawMessage(inboundTags),
- "clientReverseTags": json.RawMessage(clientReverseTags),
+ "xraySetting": json.RawMessage(xraySetting),
+ "inboundTags": json.RawMessage(inboundTags),
+ "clientReverseTags": json.RawMessage(clientReverseTags),
+ "outboundTags": json.RawMessage(outboundTags),
+ "outboundReverseTags": json.RawMessage(outboundReverseTags),
+ "outbounds": json.RawMessage(outboundSummaries),
}
result, err := json.Marshal(xrayResponse)
if err != nil {
diff --git a/web/controller/xui.go b/web/controller/xui.go
index 70c1dbf4..01df0bbb 100644
--- a/web/controller/xui.go
+++ b/web/controller/xui.go
@@ -8,6 +8,7 @@ type XUIController struct {
BaseController
inboundController *InboundController
+ outboundController *OutboundController
settingController *SettingController
xraySettingController *XraySettingController
}
@@ -24,10 +25,12 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index)
g.GET("/inbounds", a.inbounds)
+ g.GET("/outbounds", a.outbounds)
g.GET("/settings", a.settings)
g.GET("/xray", a.xraySettings)
a.inboundController = NewInboundController(g)
+ a.outboundController = NewOutboundController(g)
a.settingController = NewSettingController(g)
a.xraySettingController = NewXraySettingController(g)
}
@@ -40,6 +43,10 @@ func (a *XUIController) inbounds(c *gin.Context) {
html(c, "inbounds.html", "pages.inbounds.title", nil)
}
+func (a *XUIController) outbounds(c *gin.Context) {
+ html(c, "outbounds.html", "pages.outbounds.title", nil)
+}
+
func (a *XUIController) settings(c *gin.Context) {
html(c, "settings.html", "pages.settings.title", nil)
}
diff --git a/web/html/xui/common_sider.html b/web/html/xui/common_sider.html
index 4921c54d..558717ef 100644
--- a/web/html/xui/common_sider.html
+++ b/web/html/xui/common_sider.html
@@ -4,9 +4,13 @@
{{ i18n "menu.dashboard"}}
-
+
{{ i18n "menu.inbounds"}}
+
+
+ {{ i18n "menu.outbounds"}}
+
{{ i18n "menu.settings"}}
diff --git a/web/html/xui/form/outbound.html b/web/html/xui/form/outbound.html
index ff964529..6a1c98c4 100644
--- a/web/html/xui/form/outbound.html
+++ b/web/html/xui/form/outbound.html
@@ -1,9 +1,7 @@
{{define "form/outbound"}}
-
- {outModal.toggleJson(activeKey == '2'); }">
-
-
-
+ outModal.onTabChange(activeKey)">
+
@@ -1080,14 +1078,20 @@
-
-
-
+
+ {{ i18n "pages.xray.outbound.linkConverter" }}
- Link:
-
+
+
+
-
+ {{ i18n "pages.xray.outbound.jsonFreeEditor" }}
+
{{end}}
diff --git a/web/html/xui/xray_outbound_modal.html b/web/html/xui/outbound_modal.html
similarity index 61%
rename from web/html/xui/xray_outbound_modal.html
rename to web/html/xui/outbound_modal.html
index f3ea5fe2..82445bb0 100644
--- a/web/html/xui/xray_outbound_modal.html
+++ b/web/html/xui/outbound_modal.html
@@ -1,11 +1,20 @@
-{{define "outModal"}}
+{{define "outboundModal"}}
- {{template "form/outbound"}}
+ {{template "form/outbound"}}
{{end}}
diff --git a/web/html/xui/outbounds.html b/web/html/xui/outbounds.html
new file mode 100644
index 00000000..a41e446c
--- /dev/null
+++ b/web/html/xui/outbounds.html
@@ -0,0 +1,498 @@
+
+
+{{template "head" .}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ template "commonSider" . }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n "pages.outbounds.totalDownUp" }}:
+ [[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]
+
+
+ {{ i18n "pages.outbounds.totalUsage" }}:
+ [[ sizeFormat(total.up + total.down) ]]
+
+
+ {{ i18n "pages.outbounds.outboundCount" }}:
+ [[ dbOutbounds.length ]]
+
+
+
+ {{ i18n "online" }}:
+ [[ onlineOutbounds.length ]]
+
+
+ [[ tag ]]
+
+ [[ onlineOutbounds.length ]]
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n "pages.xray.outbound.addOutbound" }}
+
+
+
+ {{ i18n "pages.inbounds.generalActions" }}
+
+ generalActions(a)" :theme="themeSwitcher.currentTheme">
+
+
+ {{ i18n "pages.outbounds.resetAllTraffic" }}
+
+
+
+ WARP
+
+
+
+
+
+
+ [[ key ]]s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n "none" }}
+ {{ i18n "online" }}
+
+
+
+
+
+
+ {{ i18n "pages.xray.rules.first" }}
+
+
+
+ {{ i18n "pages.outbounds.edit" }}
+
+
+
+ {{ i18n "pages.outbounds.resetTraffic" }}
+
+
+
+
+
+
+ {{ i18n "delete" }}
+
+
+
+
+
+
+
+ {{ i18n "online" }}
+ {{ i18n "offline" }}
+
+
+
+
+ {{ i18n "online" }}
+ {{ i18n "offline" }}
+
+
+
+
+ [[ row.tag ]]
+
+
+ [[ row.protocol ]]
+
+ [[ row.toOutbound().stream.network ]]
+ tls
+ reality
+
+
+
+ [[ addr ]]
+
+
+ ↑[[ sizeFormat(row.up) ]] / ↓[[ sizeFormat(row.down) ]]
+
+
+
+
+
+
+
+
+{{template "js" .}}
+{{template "component/themeSwitcher" .}}
+
+
+
+{{template "outboundModal"}}
+{{template "warpModal"}}
+
+
diff --git a/web/html/xui/warp_modal.html b/web/html/xui/warp_modal.html
index f2c4292a..db2b87d2 100644
--- a/web/html/xui/warp_modal.html
+++ b/web/html/xui/warp_modal.html
@@ -213,20 +213,36 @@ new Vue({
this.delOutbound();
}
},
- addOutbound(){
- app.templateSettings.outbounds.push(warpModal.warpOutbound.toJson());
- app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
- warpModal.close();
+ async addOutbound(){
+ warpModal.loading(true);
+ const db = new DBOutbound();
+ const data = DBOutbound.payloadFromOutbound(warpModal.warpOutbound, db);
+ const msg = await HttpUtil.post('/xui/outbound/add', data);
+ warpModal.loading(false);
+ if (msg.success) {
+ await app.getDBOutbounds();
+ warpModal.close();
+ }
},
- resetOutbound(){
- app.templateSettings.outbounds[this.warpOutboundIndex] = warpModal.warpOutbound.toJson();
- app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
- warpModal.close();
+ async resetOutbound(){
+ const row = app.dbOutbounds[this.warpOutboundIndex];
+ if (!row) return;
+ warpModal.loading(true);
+ const data = DBOutbound.payloadFromOutbound(warpModal.warpOutbound, row);
+ const msg = await HttpUtil.post(`/xui/outbound/update/${row.id}`, data);
+ warpModal.loading(false);
+ if (msg.success) {
+ await app.getDBOutbounds();
+ warpModal.close();
+ }
},
- delOutbound(){
- if (this.warpOutboundIndex != -1){
- app.templateSettings.outbounds.splice(this.warpOutboundIndex,1);
- app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);
+ async delOutbound(){
+ const row = app.dbOutbounds[this.warpOutboundIndex];
+ if (row) {
+ warpModal.loading(true);
+ await HttpUtil.post(`/xui/outbound/del/${row.id}`);
+ warpModal.loading(false);
+ await app.getDBOutbounds();
}
warpModal.close();
}
@@ -234,7 +250,7 @@ new Vue({
computed: {
warpOutboundIndex: {
get: function() {
- return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag == 'warp') : -1;
+ return app.dbOutbounds ? app.dbOutbounds.findIndex((o) => o.tag == 'warp') : -1;
}
}
}
diff --git a/web/html/xui/xray.html b/web/html/xui/xray.html
index e6f4cb52..17286693 100644
--- a/web/html/xui/xray.html
+++ b/web/html/xui/xray.html
@@ -513,50 +513,6 @@
-
- {{ i18n "pages.xray.outbound.addOutbound" }}
- WARP
-
-
- [[ index+1 ]]
-
- e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;">
-
-
-
- {{ i18n "pages.xray.rules.first"}}
-
-
-
- {{ i18n "edit" }}
-
-
-
- {{ i18n "delete"}}
-
-
-
-
-
-
- [[ addr ]]
-
-
- [[ outbound.protocol ]]
-
- [[ outbound.streamSettings.network ]]
- tls
- reality
-
-
-
-
{{ i18n "pages.xray.balancer.addBalancer"}}
{{ i18n "pages.xray.completeTemplate"}}
{{ i18n "pages.xray.Inbounds" }}
- {{ i18n "pages.xray.Outbounds" }}
{{ i18n "pages.xray.Routings" }}
@@ -758,7 +713,6 @@
{{template "component/themeSwitcher" .}}
{{template "component/setting"}}
{{template "ruleModal"}}
-{{template "outModal"}}
{{template "balancerModal"}}
{{template "dnsModal"}}
{{template "fakednsModal"}}
@@ -837,6 +791,9 @@
xraySetting: '',
inboundTags: [],
clientReverseTags: [],
+ outboundTags: [],
+ outboundReverseTags: [],
+ loadedOutbounds: [],
saveBtnDisable: true,
restartResult: '',
showAlert: false,
@@ -955,6 +912,9 @@
this.xraySetting = xs;
this.inboundTags = result.inboundTags;
this.clientReverseTags = result.clientReverseTags;
+ this.outboundTags = result.outboundTags || [];
+ this.outboundReverseTags = result.outboundReverseTags || [];
+ this.loadedOutbounds = result.outbounds || [];
this.saveBtnDisable = true;
}
},
@@ -995,18 +955,6 @@
if(pageKey == 'tpl-advanced') this.changeCode();
if(pageKey == 'tpl-balancer') this.changeObsCode();
},
- syncRulesWithOutbound(tag, setting) {
- const newTemplateSettings = this.templateSettings;
- const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);
- const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag);
- if (!haveRules && outboundIndex > 0) {
- newTemplateSettings.outbounds.splice(outboundIndex);
- }
- if (haveRules && outboundIndex < 0) {
- newTemplateSettings.outbounds.push(setting);
- }
- this.templateSettings = newTemplateSettings;
- },
templateRuleGetter(routeSettings) {
const { property, outboundTag } = routeSettings;
let result = [];
@@ -1127,46 +1075,6 @@
}
return serverObj ? serverObj.map(obj => obj.address + ':' + obj.port) : null;
},
- addOutbound(){
- outModal.show({
- title: '{{ i18n "pages.xray.outbound.addOutbound"}}',
- okText: '{{ i18n "pages.xray.outbound.addOutbound" }}',
- confirm: (outbound) => {
- outModal.loading();
- if(outbound.tag.length > 0){
- this.templateSettings.outbounds.push(outbound);
- this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);
- }
- outModal.close();
- },
- isEdit: false,
- tags: this.templateSettings.outbounds.map(obj => obj.tag)
- });
- },
- editOutbound(index){
- outModal.show({
- title: '{{ i18n "pages.xray.outbound.editOutbound"}} ' + (index+1),
- outbound: app.templateSettings.outbounds[index],
- confirm: (outbound) => {
- outModal.loading();
- this.templateSettings.outbounds[index] = outbound;
- this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);
- outModal.close();
- },
- isEdit: true,
- tags: this.outboundData.filter((o) => o.key != index ).map(obj => obj.tag)
- });
- },
- deleteOutbound(index){
- outbounds = this.templateSettings.outbounds;
- outbounds.splice(index,1);
- this.outboundSettings = JSON.stringify(outbounds);
- },
- setFirstOutbound(index){
- outbounds = this.templateSettings.outbounds;
- outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
- this.outboundSettings = JSON.stringify(outbounds);
- },
addBalancer() {
balancerModal.show({
title: '{{ i18n "pages.xray.balancer.addBalancer"}}',
@@ -1438,9 +1346,6 @@
rules.splice(index,1);
this.routingRuleSettings = JSON.stringify(rules);
},
- showWarp(){
- warpModal.show();
- }
},
async mounted() {
if (window.location.protocol !== "https:") {
@@ -1529,25 +1434,6 @@
this.templateSettings = newTemplateSettings;
},
},
- outboundSettings: {
- get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
- set: function (newValue) {
- newTemplateSettings = this.templateSettings;
- newTemplateSettings.outbounds = JSON.parse(newValue);
- this.templateSettings = newTemplateSettings;
- },
- },
- outboundData: {
- get: function () {
- data = []
- if (this.templateSettings != null) {
- this.templateSettings.outbounds.forEach((o, index) => {
- data.push({'key': index, ...o});
- });
- }
- return data;
- },
- },
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {
@@ -1618,23 +1504,21 @@
burstObservatoryEnable: function () { return this.templateSettings != null && this.templateSettings.burstObservatory != undefined },
freedomStrategy: {
get: function () {
- if (!this.templateSettings) return "AsIs";
- freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && o.tag == "direct");
- if (!freedomOutbound) return "AsIs";
- if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs";
+ const freedomOutbound = this.loadedOutbounds.find((o) => o.protocol === "freedom" && o.tag == "direct");
+ if (!freedomOutbound || !freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs";
return freedomOutbound.settings.domainStrategy;
},
set: function (newValue) {
- newTemplateSettings = this.templateSettings;
- freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && o.tag == "direct");
- if(freedomOutboundIndex == -1){
- newTemplateSettings.outbounds.push({protocol: "freedom", tag: "direct", settings: { "domainStrategy": newValue }});
- } else if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {
- newTemplateSettings.outbounds[freedomOutboundIndex].settings = { "domainStrategy": newValue };
- } else {
- newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
- }
- this.templateSettings = newTemplateSettings;
+ const freedomOutbound = this.loadedOutbounds.find((o) => o.protocol === "freedom" && o.tag == "direct");
+ if (!freedomOutbound) return;
+ const settings = freedomOutbound.settings || {};
+ settings.domainStrategy = newValue;
+ const data = {
+ protocol: freedomOutbound.protocol,
+ tag: freedomOutbound.tag,
+ settings: JSON.stringify(settings, null, 2),
+ };
+ HttpUtil.post(`/xui/outbound/update/${freedomOutbound.id}`, data).then(() => this.getXraySetting());
}
},
routingStrategy: {
@@ -1678,7 +1562,6 @@
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "direct", property: "ip", data: newValue });
- this.syncRulesWithOutbound("direct", this.directSettings);
}
},
directDomains: {
@@ -1687,7 +1570,6 @@
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "direct", property: "domain", data: newValue });
- this.syncRulesWithOutbound("direct", this.directSettings);
}
},
ipv4Domains: {
@@ -1696,7 +1578,6 @@
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue });
- this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
}
},
warpDomains: {
@@ -1736,7 +1617,7 @@
},
WarpExist: {
get: function() {
- return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag == "warp")>=0 : false;
+ return this.outboundTags ? this.outboundTags.includes("warp") : false;
},
},
enableDNS: {
@@ -1850,8 +1731,7 @@
}
},
geodataOutboundTags: function () {
- if (!this.templateSettings) return [];
- return this.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map((o) => o.tag);
+ return this.outboundTags || [];
},
geodataAssets: {
get: function () {
diff --git a/web/html/xui/xray_balancer_modal.html b/web/html/xui/xray_balancer_modal.html
index 1bfd7db2..51e816cb 100644
--- a/web/html/xui/xray_balancer_modal.html
+++ b/web/html/xui/xray_balancer_modal.html
@@ -82,8 +82,8 @@
};
}
this.balancerTags = balancerTags.filter((tag) => tag != balancer.tag);
- this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
- this.outboundTags.push(...app.clientReverseTags);
+ this.outboundTags = [...(app.outboundTags || [])];
+ if (app.clientReverseTags) this.outboundTags.push(...app.clientReverseTags);
this.isEdit = isEdit;
this.check();
this.checkSelector();
diff --git a/web/html/xui/xray_rule_modal.html b/web/html/xui/xray_rule_modal.html
index 30c49fce..15444830 100644
--- a/web/html/xui/xray_rule_modal.html
+++ b/web/html/xui/xray_rule_modal.html
@@ -195,11 +195,13 @@
this.isEdit = isEdit;
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
this.inboundTags.push(...app.inboundTags);
- app.templateSettings.outbounds.filter(o => o.protocol === Protocols.VLESS && o.settings.reverse?.tag).forEach(o => {
- this.inboundTags.push(o.settings.reverse.tag);
- });
+ if (app.outboundReverseTags) {
+ app.outboundReverseTags.forEach(tag => {
+ if (tag && !this.inboundTags.includes(tag)) this.inboundTags.push(tag);
+ });
+ }
if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)
- this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
+ this.outboundTags = ["", ...(app.outboundTags || [])];
if (app.clientReverseTags) {
app.clientReverseTags.forEach(tag => {
if (tag && !this.outboundTags.includes(tag)) {
diff --git a/web/job/xray_traffic_job.go b/web/job/xray_traffic_job.go
index acb2e0f0..65bd1106 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 {
@@ -26,7 +27,10 @@ func (j *XrayTrafficJob) Run() {
}
err, needRestart := j.inboundService.AddTraffic(traffics, clientTraffics)
if err != nil {
- logger.Warning("add traffic failed:", err)
+ logger.Warning("add inbound traffic failed:", err)
+ }
+ if err := j.outboundService.AddTraffic(traffics); err != nil {
+ logger.Warning("add outbound traffic failed:", err)
}
if needRestart {
j.xrayService.SetToNeedRestart()
diff --git a/web/service/outbound.go b/web/service/outbound.go
new file mode 100644
index 00000000..5dce6408
--- /dev/null
+++ b/web/service/outbound.go
@@ -0,0 +1,381 @@
+package service
+
+import (
+ "encoding/json"
+
+ "github.com/alireza0/x-ui/database"
+ "github.com/alireza0/x-ui/database/model"
+ "github.com/alireza0/x-ui/logger"
+ "github.com/alireza0/x-ui/util/common"
+ "github.com/alireza0/x-ui/xray"
+
+ "gorm.io/gorm"
+)
+
+type OutboundService struct {
+ xrayApi xray.XrayAPI
+ settingService SettingService
+}
+
+func (s *OutboundService) GetAllOutbounds() ([]*model.Outbound, error) {
+ db := database.GetDB()
+ var outbounds []*model.Outbound
+ err := db.Model(model.Outbound{}).Order("sort asc, id asc").Find(&outbounds).Error
+ if err != nil && err != gorm.ErrRecordNotFound {
+ return nil, err
+ }
+ return outbounds, nil
+}
+
+func (s *OutboundService) GetOutbound(id int) (*model.Outbound, error) {
+ db := database.GetDB()
+ outbound := &model.Outbound{}
+ err := db.Model(model.Outbound{}).First(outbound, id).Error
+ if err != nil {
+ return nil, err
+ }
+ return outbound, nil
+}
+
+func (s *OutboundService) checkTagExist(tag string, ignoreId int) (bool, error) {
+ db := database.GetDB().Model(model.Outbound{}).Where("tag = ?", tag)
+ if ignoreId > 0 {
+ db = db.Where("id != ?", ignoreId)
+ }
+ var count int64
+ err := db.Count(&count).Error
+ if err != nil {
+ return false, err
+ }
+ return count > 0, nil
+}
+
+func (s *OutboundService) AddOutbound(outbound *model.Outbound) (*model.Outbound, bool, error) {
+ exist, err := s.checkTagExist(outbound.Tag, 0)
+ if err != nil {
+ return outbound, false, err
+ }
+ if exist {
+ return outbound, false, common.NewError("Tag already exists:", outbound.Tag)
+ }
+
+ db := database.GetDB()
+ var maxSort int
+ db.Model(model.Outbound{}).Select("COALESCE(MAX(sort), -1)").Scan(&maxSort)
+ outbound.Sort = maxSort + 1
+
+ err = db.Save(outbound).Error
+ if err != nil {
+ return outbound, false, err
+ }
+
+ needRestart := false
+ if p != nil && p.IsRunning() {
+ s.xrayApi.Init(p.GetAPIPort())
+ outboundJson, err1 := json.MarshalIndent(outbound.GenXrayOutboundConfig(), "", " ")
+ if err1 != nil {
+ logger.Debug("Unable to marshal outbound config:", err1)
+ } else {
+ err1 = s.xrayApi.AddOutbound(outboundJson)
+ if err1 == nil {
+ logger.Debug("New outbound added by api:", outbound.Tag)
+ } else {
+ logger.Debug("Unable to add outbound by api:", err1)
+ needRestart = true
+ }
+ }
+ s.xrayApi.Close()
+ }
+
+ return outbound, needRestart, nil
+}
+
+func (s *OutboundService) DelOutbound(id int) (bool, error) {
+ db := database.GetDB()
+ outbound, err := s.GetOutbound(id)
+ if err != nil {
+ return false, err
+ }
+
+ needRestart := false
+ if p != nil && p.IsRunning() {
+ s.xrayApi.Init(p.GetAPIPort())
+ err1 := s.xrayApi.DelOutbound(outbound.Tag)
+ if err1 == nil {
+ logger.Debug("Outbound deleted by api:", outbound.Tag)
+ } else {
+ logger.Debug("Unable to delete outbound by api:", err1)
+ needRestart = true
+ }
+ s.xrayApi.Close()
+ }
+
+ return needRestart, db.Delete(model.Outbound{}, id).Error
+}
+
+func (s *OutboundService) UpdateOutbound(outbound *model.Outbound) (*model.Outbound, bool, error) {
+ exist, err := s.checkTagExist(outbound.Tag, outbound.Id)
+ if err != nil {
+ return outbound, false, err
+ }
+ if exist {
+ return outbound, false, common.NewError("Tag already exists:", outbound.Tag)
+ }
+
+ oldOutbound, err := s.GetOutbound(outbound.Id)
+ if err != nil {
+ return outbound, false, err
+ }
+
+ oldTag := oldOutbound.Tag
+ oldOutbound.SendThrough = outbound.SendThrough
+ oldOutbound.Protocol = outbound.Protocol
+ oldOutbound.Settings = outbound.Settings
+ oldOutbound.Tag = outbound.Tag
+ oldOutbound.StreamSettings = outbound.StreamSettings
+ oldOutbound.ProxySettings = outbound.ProxySettings
+ oldOutbound.Mux = outbound.Mux
+ oldOutbound.TargetStrategy = outbound.TargetStrategy
+
+ needRestart := false
+ if p != nil && p.IsRunning() {
+ s.xrayApi.Init(p.GetAPIPort())
+ if s.xrayApi.DelOutbound(oldTag) == nil {
+ logger.Debug("Old outbound deleted by api:", oldTag)
+ }
+ outboundJson, err2 := json.MarshalIndent(oldOutbound.GenXrayOutboundConfig(), "", " ")
+ if err2 != nil {
+ logger.Debug("Unable to marshal updated outbound config:", err2)
+ needRestart = true
+ } else {
+ err2 = s.xrayApi.AddOutbound(outboundJson)
+ if err2 == nil {
+ logger.Debug("Updated outbound added by api:", oldOutbound.Tag)
+ } else {
+ logger.Debug("Unable to update outbound by api:", err2)
+ needRestart = true
+ }
+ }
+ s.xrayApi.Close()
+ }
+
+ db := database.GetDB()
+ return outbound, needRestart, db.Save(oldOutbound).Error
+}
+
+func (s *OutboundService) SetFirstOutbound(id int) error {
+ db := database.GetDB()
+ outbound, err := s.GetOutbound(id)
+ if err != nil {
+ return err
+ }
+ return db.Transaction(func(tx *gorm.DB) error {
+ if err := tx.Model(model.Outbound{}).Where("sort < ?", outbound.Sort).
+ Update("sort", gorm.Expr("sort + 1")).Error; err != nil {
+ return err
+ }
+ return tx.Model(model.Outbound{}).Where("id = ?", id).Update("sort", 0).Error
+ })
+}
+
+func (s *OutboundService) ResetTraffic(id int) error {
+ db := database.GetDB()
+ return db.Model(model.Outbound{}).Where("id = ?", id).
+ Updates(map[string]interface{}{"up": 0, "down": 0}).Error
+}
+
+func (s *OutboundService) ResetAllTraffics() error {
+ db := database.GetDB()
+ return db.Model(model.Outbound{}).Where("1 = 1").
+ Updates(map[string]interface{}{"up": 0, "down": 0}).Error
+}
+
+func (s *OutboundService) GetOutboundSummariesJSON() (string, error) {
+ outbounds, err := s.GetAllOutbounds()
+ if err != nil {
+ return "", err
+ }
+ summaries := make([]map[string]interface{}, 0, len(outbounds))
+ for _, o := range outbounds {
+ var settings interface{}
+ if len(o.Settings) > 0 {
+ json.Unmarshal([]byte(o.Settings), &settings)
+ }
+ summaries = append(summaries, map[string]interface{}{
+ "id": o.Id,
+ "tag": o.Tag,
+ "protocol": o.Protocol,
+ "settings": settings,
+ })
+ }
+ result, _ := json.Marshal(summaries)
+ return string(result), nil
+}
+
+func (s *OutboundService) GetOutboundReverseTags() (string, error) {
+ outbounds, err := s.GetAllOutbounds()
+ if err != nil {
+ return "", err
+ }
+ var tags []string
+ for _, o := range outbounds {
+ if o.Protocol != "vless" {
+ continue
+ }
+ var settings map[string]interface{}
+ if json.Unmarshal([]byte(o.Settings), &settings) != nil {
+ continue
+ }
+ reverse, ok := settings["reverse"].(map[string]interface{})
+ if !ok {
+ continue
+ }
+ if tag, ok := reverse["tag"].(string); ok && tag != "" {
+ tags = append(tags, tag)
+ }
+ }
+ result, _ := json.Marshal(tags)
+ return string(result), nil
+}
+
+func (s *OutboundService) GetOutboundTags() (string, error) {
+ db := database.GetDB()
+ var tags []string
+ err := db.Model(model.Outbound{}).Select("tag").Order("sort asc, id asc").Find(&tags).Error
+ if err != nil && err != gorm.ErrRecordNotFound {
+ return "", err
+ }
+ result, _ := json.Marshal(tags)
+ return string(result), nil
+}
+
+func (s *OutboundService) AddTraffic(traffics []*xray.Traffic) error {
+ hasOutboundTraffic := false
+ for _, traffic := range traffics {
+ if !traffic.IsInbound {
+ hasOutboundTraffic = true
+ break
+ }
+ }
+ if !hasOutboundTraffic {
+ if p != nil {
+ p.SetOnlineOutbounds(nil)
+ }
+ return nil
+ }
+
+ var onlineOutbounds []string
+ db := database.GetDB()
+ for _, traffic := range traffics {
+ if traffic.IsInbound {
+ continue
+ }
+ err := db.Model(&model.Outbound{}).Where("tag = ?", traffic.Tag).
+ Updates(map[string]interface{}{
+ "up": gorm.Expr("up + ?", traffic.Up),
+ "down": gorm.Expr("down + ?", traffic.Down),
+ }).Error
+ if err != nil {
+ return err
+ }
+ if traffic.Up+traffic.Down > 0 {
+ onlineOutbounds = append(onlineOutbounds, traffic.Tag)
+ }
+ }
+ if p != nil {
+ p.SetOnlineOutbounds(onlineOutbounds)
+ }
+ return nil
+}
+
+func (s *OutboundService) GetOnlineOutbounds() []string {
+ if p == nil {
+ return nil
+ }
+ return p.GetOnlineOutbounds()
+}
+
+func outboundFromMap(raw map[string]interface{}, sort int) *model.Outbound {
+ o := &model.Outbound{Sort: sort}
+ if v, ok := raw["sendThrough"].(string); ok {
+ o.SendThrough = v
+ }
+ if v, ok := raw["protocol"].(string); ok {
+ o.Protocol = v
+ }
+ if v, ok := raw["tag"].(string); ok {
+ o.Tag = v
+ }
+ if v, ok := raw["targetStrategy"].(string); ok {
+ o.TargetStrategy = v
+ }
+ if v, ok := raw["settings"]; ok && v != nil {
+ b, _ := json.Marshal(v)
+ o.Settings = string(b)
+ }
+ if v, ok := raw["streamSettings"]; ok && v != nil {
+ b, _ := json.Marshal(v)
+ o.StreamSettings = string(b)
+ }
+ if v, ok := raw["proxySettings"]; ok && v != nil {
+ b, _ := json.Marshal(v)
+ o.ProxySettings = string(b)
+ }
+ if v, ok := raw["mux"]; ok && v != nil {
+ b, _ := json.Marshal(v)
+ o.Mux = string(b)
+ }
+ return o
+}
+
+func (s *OutboundService) MigrateDB() {
+ db := database.GetDB()
+ var count int64
+ db.Model(&model.Outbound{}).Count(&count)
+ if count > 0 {
+ return
+ }
+
+ templateConfig, err := s.settingService.GetXrayConfigTemplate()
+ if err != nil {
+ logger.Warning("outbound migration: get xray template failed:", err)
+ return
+ }
+
+ var cfg map[string]interface{}
+ if err := json.Unmarshal([]byte(templateConfig), &cfg); err != nil {
+ logger.Warning("outbound migration: parse template failed:", err)
+ return
+ }
+
+ rawOutbounds, ok := cfg["outbounds"].([]interface{})
+ if !ok || len(rawOutbounds) == 0 {
+ return
+ }
+
+ for i, item := range rawOutbounds {
+ raw, ok := item.(map[string]interface{})
+ if !ok {
+ continue
+ }
+ outbound := outboundFromMap(raw, i)
+ if outbound.Tag == "" || outbound.Protocol == "" {
+ continue
+ }
+ if err := db.Create(outbound).Error; err != nil {
+ logger.Warning("outbound migration: create failed:", err)
+ }
+ }
+
+ cfg["outbounds"] = []interface{}{}
+ newTemplate, err := json.MarshalIndent(cfg, "", " ")
+ if err != nil {
+ logger.Warning("outbound migration: marshal template failed:", err)
+ return
+ }
+ if err := s.settingService.saveSetting("xrayTemplateConfig", string(newTemplate)); err != nil {
+ logger.Warning("outbound migration: save template failed:", err)
+ return
+ }
+ logger.Info("Migrated", len(rawOutbounds), "outbound(s) from xray settings to database")
+}
diff --git a/web/service/server.go b/web/service/server.go
index bee5d633..6ad6889d 100644
--- a/web/service/server.go
+++ b/web/service/server.go
@@ -90,8 +90,9 @@ type Release struct {
}
type ServerService struct {
- xrayService XrayService
- inboundService InboundService
+ xrayService XrayService
+ inboundService InboundService
+ outboundService OutboundService
}
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
@@ -580,6 +581,7 @@ func (s *ServerService) ImportDB(file multipart.File) error {
return common.NewErrorf("Error migrating db: %v", err)
}
s.inboundService.MigrateDB()
+ s.outboundService.MigrateDB()
// Start Xray
err = s.RestartXrayService()
diff --git a/web/service/xray.go b/web/service/xray.go
index 71008159..d242c7bc 100644
--- a/web/service/xray.go
+++ b/web/service/xray.go
@@ -20,9 +20,10 @@ var (
)
type XrayService struct {
- inboundService InboundService
- settingService SettingService
- xrayAPI xray.XrayAPI
+ inboundService InboundService
+ outboundService OutboundService
+ settingService SettingService
+ xrayAPI xray.XrayAPI
}
func (s *XrayService) IsXrayRunning() bool {
@@ -182,6 +183,17 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
inboundConfig := inbound.GenXrayInboundConfig()
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
}
+
+ xrayConfig.OutboundConfigs = []xray.OutboundConfig{}
+
+ outbounds, err := s.outboundService.GetAllOutbounds()
+ if err != nil {
+ return nil, err
+ }
+ for _, outbound := range outbounds {
+ outboundConfig := outbound.GenXrayOutboundConfig()
+ xrayConfig.OutboundConfigs = append(xrayConfig.OutboundConfigs, *outboundConfig)
+ }
return xrayConfig, nil
}
diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml
index 495d460e..63ebde19 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -56,6 +56,7 @@
[menu]
"dashboard" = "Overview"
"inbounds" = "Inbounds"
+"outbounds" = "Outbounds"
"settings" = "Panel Settings"
"xray" = "Xray Configs"
"logout" = "Log Out"
@@ -179,6 +180,22 @@
"import" = "Import"
"importInbound" = "Import an Inbound"
+[pages.outbounds]
+"title" = "Outbounds"
+"totalDownUp" = "Total Sent/Received"
+"totalUsage" = "Total Usage"
+"outboundCount" = "Total Outbounds"
+"create" = "Create"
+"edit" = "Edit Outbound"
+"update" = "Update"
+"delete" = "Delete Outbound"
+"deleteConfirm" = "Are you sure you want to delete this outbound?"
+"resetTraffic" = "Reset Traffic"
+"resetAllTraffic" = "Reset All Outbound Traffic"
+
+[pages.outbounds.toasts]
+"obtain" = "Failed to load outbounds"
+
[pages.client]
"add" = "Add Client"
"edit" = "Edit Client"
@@ -616,6 +633,9 @@
"reverseTagDesc" = "The tag of the reverse proxy inbound that this VLESS client uses."
"reverseTagPlaceholder" = "reverse-inbound-tag"
"sendThrough" = "Send Through"
+"advanced" = "Advanced"
+"linkConverter" = "Link converter"
+"jsonFreeEditor" = "JSON free editor"
[pages.xray.balancer]
"addBalancer" = "Add Balancer"
diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml
index f98f200e..3dafa929 100644
--- a/web/translation/translate.fa_IR.toml
+++ b/web/translation/translate.fa_IR.toml
@@ -56,6 +56,7 @@
[menu]
"dashboard" = "نمای کلی"
"inbounds" = "ورودیها"
+"outbounds" = "خروجیها"
"settings" = "تنظیمات پنل"
"xray" = "پیکربندی ایکسری"
"logout" = "خروج"
@@ -179,6 +180,22 @@
"import" = "افزودن"
"importInbound" = "افزودن یک ورودی"
+[pages.outbounds]
+"title" = "خروجیها"
+"totalDownUp" = "مجموع ارسال/دریافت"
+"totalUsage" = "مجموع مصرف"
+"outboundCount" = "تعداد خروجیها"
+"create" = "ایجاد"
+"edit" = "ویرایش خروجی"
+"update" = "بهروزرسانی"
+"delete" = "حذف خروجی"
+"deleteConfirm" = "آیا از حذف این خروجی مطمئن هستید؟"
+"resetTraffic" = "ریست ترافیک"
+"resetAllTraffic" = "ریست ترافیک همه خروجیها"
+
+[pages.outbounds.toasts]
+"obtain" = "دریافت خروجیها ناموفق بود"
+
[pages.client]
"add" = "کاربر جدید"
"edit" = "ویرایش کاربر"
@@ -616,6 +633,9 @@
"reverseTagDesc" = "برچسب inbound پروکسی معکوسی که این کلاینت VLESS به آن متصل میشود."
"reverseTagPlaceholder" = "reverse-inbound-tag"
"sendThrough" = "ارسال با"
+"advanced" = "پیشرفته"
+"linkConverter" = "تبدیل لینک"
+"jsonFreeEditor" = "ویرایشگر آزاد JSON"
[pages.xray.balancer]
"addBalancer" = "افزودن بالانسر"
diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml
index c88984fb..7b5d3ab4 100644
--- a/web/translation/translate.ru_RU.toml
+++ b/web/translation/translate.ru_RU.toml
@@ -56,6 +56,7 @@
[menu]
"dashboard" = "Обзор"
"inbounds" = "Подключения"
+"outbounds" = "Исходящие"
"settings" = "Настройки"
"xray" = "Xray"
"logout" = "Выйти"
@@ -179,6 +180,22 @@
"import" = "Импортировать"
"importInbound" = "Импортировать входящее сообщение"
+[pages.outbounds]
+"title" = "Исходящие"
+"totalDownUp" = "Всего отправлено/получено"
+"totalUsage" = "Общий трафик"
+"outboundCount" = "Всего исходящих"
+"create" = "Создать"
+"edit" = "Редактировать исходящий"
+"update" = "Обновить"
+"delete" = "Удалить исходящий"
+"deleteConfirm" = "Удалить этот исходящий?"
+"resetTraffic" = "Сбросить трафик"
+"resetAllTraffic" = "Сбросить весь трафик исходящих"
+
+[pages.outbounds.toasts]
+"obtain" = "Не удалось загрузить исходящие"
+
[pages.client]
"add" = "Добавить клиента"
"edit" = "Редактировать клиента"
@@ -616,6 +633,9 @@
"reverseTagDesc" = "Тег inbound обратного прокси, который использует этот VLESS-клиент."
"reverseTagPlaceholder" = "reverse-inbound-tag"
"sendThrough" = "Отправить через"
+"advanced" = "Дополнительно"
+"linkConverter" = "Конвертер ссылок"
+"jsonFreeEditor" = "Свободный редактор JSON"
[pages.xray.balancer]
"addBalancer" = "Добавить балансир"
diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml
index e6b0ca0a..7adda319 100644
--- a/web/translation/translate.vi_VN.toml
+++ b/web/translation/translate.vi_VN.toml
@@ -56,6 +56,7 @@
[menu]
"dashboard" = "Tổng quan"
"inbounds" = "Đầu Vào khách hàng"
+"outbounds" = "Đầu Ra"
"settings" = "Cài đặt X-UI"
"xray" = "Cài đặt Xray"
"logout" = "Đăng xuất"
@@ -179,6 +180,22 @@
"import" = "Nhập"
"importInbound" = "Nhập hàng gửi về"
+[pages.outbounds]
+"title" = "Outbounds"
+"totalDownUp" = "Tổng Gửi/Nhận"
+"totalUsage" = "Tổng sử dụng"
+"outboundCount" = "Tổng Outbound"
+"create" = "Tạo"
+"edit" = "Sửa Outbound"
+"update" = "Cập nhật"
+"delete" = "Xóa Outbound"
+"deleteConfirm" = "Bạn có chắc muốn xóa outbound này?"
+"resetTraffic" = "Đặt lại lưu lượng"
+"resetAllTraffic" = "Đặt lại tất cả lưu lượng outbound"
+
+[pages.outbounds.toasts]
+"obtain" = "Không thể tải outbounds"
+
[pages.client]
"add" = "Thêm máy khách"
"edit" = "Chỉnh sửa Máy khách"
@@ -616,6 +633,9 @@
"reverseTagDesc" = "Thẻ inbound reverse proxy mà client VLESS này sử dụng."
"reverseTagPlaceholder" = "reverse-inbound-tag"
"sendThrough" = "Gửi qua"
+"advanced" = "Nâng cao"
+"linkConverter" = "Chuyển đổi liên kết"
+"jsonFreeEditor" = "Trình soạn JSON tự do"
[pages.xray.balancer]
"addBalancer" = "Thêm cân bằng"
diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml
index 476c937a..a8b7a56e 100644
--- a/web/translation/translate.zh_Hans.toml
+++ b/web/translation/translate.zh_Hans.toml
@@ -56,6 +56,7 @@
[menu]
"dashboard" = "概述"
"inbounds" = "入站列表"
+"outbounds" = "出站列表"
"settings" = "面板设置"
"xray" = "Xray"
"logout" = "退出登录"
@@ -179,6 +180,22 @@
"import"="导入"
"importInbound" = "导入入站数据"
+[pages.outbounds]
+"title" = "出站"
+"totalDownUp" = "总发送/接收"
+"totalUsage" = "总用量"
+"outboundCount" = "出站总数"
+"create" = "创建"
+"edit" = "编辑出站"
+"update" = "更新"
+"delete" = "删除出站"
+"deleteConfirm" = "确定要删除此出站吗?"
+"resetTraffic" = "重置流量"
+"resetAllTraffic" = "重置所有出站流量"
+
+[pages.outbounds.toasts]
+"obtain" = "获取出站失败"
+
[pages.client]
"add" = "添加客户端"
"edit" = "编辑客户"
@@ -616,6 +633,9 @@
"reverseTagDesc" = "此 VLESS 客户端使用的反向代理 inbound 标签。"
"reverseTagPlaceholder" = "reverse-inbound-tag"
"sendThrough" = "发送通过"
+"advanced" = "高级"
+"linkConverter" = "链接转换"
+"jsonFreeEditor" = "JSON 自由编辑"
[pages.xray.balancer]
"addBalancer" = "添加平衡器"
diff --git a/xray/api.go b/xray/api.go
index 2ee2fc2a..416db8aa 100644
--- a/xray/api.go
+++ b/xray/api.go
@@ -87,6 +87,34 @@ func (x *XrayAPI) DelInbound(tag string) error {
return err
}
+func (x *XrayAPI) AddOutbound(outbound []byte) error {
+ client := *x.HandlerServiceClient
+
+ conf := new(conf.OutboundDetourConfig)
+ err := json.Unmarshal(outbound, conf)
+ if err != nil {
+ logger.Debug("Failed to unmarshal outbound:", err)
+ return err
+ }
+ config, err := conf.Build()
+ if err != nil {
+ logger.Debug("Failed to build outbound:", err)
+ return err
+ }
+ outboundConfig := command.AddOutboundRequest{Outbound: config}
+
+ _, err = client.AddOutbound(context.Background(), &outboundConfig)
+ return err
+}
+
+func (x *XrayAPI) DelOutbound(tag string) error {
+ client := *x.HandlerServiceClient
+ _, err := client.RemoveOutbound(context.Background(), &command.RemoveOutboundRequest{
+ Tag: tag,
+ })
+ return err
+}
+
func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]interface{}) error {
var account *serial.TypedMessage
switch Protocol {
diff --git a/xray/config.go b/xray/config.go
index 01c8bc48..9d5cdffd 100644
--- a/xray/config.go
+++ b/xray/config.go
@@ -11,7 +11,7 @@ type Config struct {
RouterConfig json_util.RawMessage `json:"routing"`
DNSConfig json_util.RawMessage `json:"dns"`
InboundConfigs []InboundConfig `json:"inbounds"`
- OutboundConfigs json_util.RawMessage `json:"outbounds"`
+ OutboundConfigs []OutboundConfig `json:"outbounds"`
Transport json_util.RawMessage `json:"transport"`
Policy json_util.RawMessage `json:"policy"`
API json_util.RawMessage `json:"api"`
@@ -41,9 +41,14 @@ func (c *Config) Equals(other *Config) bool {
if !bytes.Equal(c.DNSConfig, other.DNSConfig) {
return false
}
- if !bytes.Equal(c.OutboundConfigs, other.OutboundConfigs) {
+ if len(c.OutboundConfigs) != len(other.OutboundConfigs) {
return false
}
+ for i, outbound := range c.OutboundConfigs {
+ if !outbound.Equals(&other.OutboundConfigs[i]) {
+ return false
+ }
+ }
if !bytes.Equal(c.Transport, other.Transport) {
return false
}
diff --git a/xray/outbound.go b/xray/outbound.go
new file mode 100644
index 00000000..eeb87759
--- /dev/null
+++ b/xray/outbound.go
@@ -0,0 +1,46 @@
+package xray
+
+import (
+ "bytes"
+
+ "github.com/alireza0/x-ui/util/json_util"
+)
+
+type OutboundConfig struct {
+ SendThrough json_util.RawMessage `json:"sendThrough,omitempty"`
+ Protocol string `json:"protocol"`
+ Settings json_util.RawMessage `json:"settings"`
+ Tag string `json:"tag"`
+ StreamSettings json_util.RawMessage `json:"streamSettings,omitempty"`
+ ProxySettings json_util.RawMessage `json:"proxySettings,omitempty"`
+ Mux json_util.RawMessage `json:"mux,omitempty"`
+ TargetStrategy string `json:"targetStrategy,omitempty"`
+}
+
+func (c *OutboundConfig) Equals(other *OutboundConfig) bool {
+ if !bytes.Equal(c.SendThrough, other.SendThrough) {
+ return false
+ }
+ if c.Protocol != other.Protocol {
+ return false
+ }
+ if !bytes.Equal(c.Settings, other.Settings) {
+ return false
+ }
+ if c.Tag != other.Tag {
+ return false
+ }
+ if !bytes.Equal(c.StreamSettings, other.StreamSettings) {
+ return false
+ }
+ if !bytes.Equal(c.ProxySettings, other.ProxySettings) {
+ return false
+ }
+ if !bytes.Equal(c.Mux, other.Mux) {
+ return false
+ }
+ if c.TargetStrategy != other.TargetStrategy {
+ return false
+ }
+ return true
+}
diff --git a/xray/process.go b/xray/process.go
index 0e89f514..2e6bf685 100644
--- a/xray/process.go
+++ b/xray/process.go
@@ -57,7 +57,8 @@ type process struct {
version string
apiPort int
- onlineClients []string
+ onlineClients []string
+ onlineOutbounds []string
config *Config
logWriter *LogWriter
@@ -115,6 +116,14 @@ func (p *Process) SetOnlineClients(users []string) {
p.onlineClients = users
}
+func (p *Process) GetOnlineOutbounds() []string {
+ return p.onlineOutbounds
+}
+
+func (p *Process) SetOnlineOutbounds(tags []string) {
+ p.onlineOutbounds = tags
+}
+
func (p *Process) GetUptime() uint64 {
return uint64(time.Since(p.startTime).Seconds())
}