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"}} - - - - - + + @@ -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 ]] + + + [[ onlineOutbounds.length ]] + + + + + + + +
+ + + + + + + + + + + + + {{ i18n "pages.outbounds.resetAllTraffic" }} + + + + WARP + + + + + + + [[ key ]]s + + + + + +
+
+ + + + + + + + + {{ i18n "none" }} + {{ i18n "online" }} + +
+ + + + + + + + + +
+
+
+
+
+
+{{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 - - - - - - {{ 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()) }