mirror of
https://github.com/alireza0/x-ui.git
synced 2026-08-04 14:46:25 +00:00
Separate outbounds
This commit is contained in:
parent
dc546308e9
commit
c0bbfc49a5
31 changed files with 1562 additions and 262 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 != "" {
|
||||
|
|
|
|||
40
go.mod
40
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
|
||||
|
|
|
|||
53
go.sum
53
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=
|
||||
|
|
|
|||
5
main.go
5
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!")
|
||||
}
|
||||
|
||||
|
|
|
|||
64
web/assets/js/model/dboutbound.js
Normal file
64
web/assets/js/model/dboutbound.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
118
web/controller/outbound.go
Normal file
118
web/controller/outbound.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@
|
|||
<span><strong>{{ i18n "menu.dashboard"}}</strong></span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}xui/inbounds">
|
||||
<a-icon type="user"></a-icon>
|
||||
<a-icon type="cloud-download"></a-icon>
|
||||
<span><strong>{{ i18n "menu.inbounds"}}</strong></span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}xui/outbounds">
|
||||
<a-icon type="cloud-upload"></a-icon>
|
||||
<span><strong>{{ i18n "menu.outbounds"}}</strong></span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}xui/settings">
|
||||
<a-icon type="setting"></a-icon>
|
||||
<span><strong>{{ i18n "menu.settings"}}</strong></span>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
{{define "form/outbound"}}
|
||||
<!-- base -->
|
||||
<a-tabs :active-key="outModal.activeKey" style="padding: 0; background-color: transparent;" @change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
|
||||
<a-tab-pane key="1" tab="Form">
|
||||
<a-tabs :animated="false" class="outbound-detail-inner-tabs" style="padding: 1rem;">
|
||||
<a-tab-pane key="ob-basic" tab="{{ i18n "pages.xray.outbound.settings" }}">
|
||||
<a-tabs :animated="false" class="outbound-detail-tabs" :active-key="outModal.activeKey" style="padding: 1rem;"
|
||||
@change="(activeKey) => outModal.onTabChange(activeKey)">
|
||||
<a-tab-pane key="ob-settings" tab="{{ i18n "pages.xray.outbound.settings" }}">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="outbound.protocol" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
|
|
@ -1080,14 +1078,20 @@
|
|||
</template>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="JSON" force-render="true">
|
||||
<a-tab-pane key="ob-json" tab='{{ i18n "pages.xray.outbound.advanced" }}' force-render="true">
|
||||
<a-divider>{{ i18n "pages.xray.outbound.linkConverter" }}</a-divider>
|
||||
<a-form-item style="margin: 10px 0">
|
||||
Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss:// hysteria2://"></a-input>
|
||||
<a-button @click="convertLink" type="primary"><a-icon type="form"></a-icon></a-button>
|
||||
<a-input-search
|
||||
v-model.trim="outModal.link"
|
||||
style="margin-right: 5px;"
|
||||
@search="convertLink"
|
||||
@pressEnter="convertLink"
|
||||
placeholder="vmess:// | vless:// | trojan:// | ss:// | hysteria2://">
|
||||
<a-button slot="enterButton" type="primary"><a-icon type="sync"></a-icon></a-button>
|
||||
</a-input-search>
|
||||
</a-form-item>
|
||||
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea>
|
||||
<a-divider>{{ i18n "pages.xray.outbound.jsonFreeEditor" }}</a-divider>
|
||||
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
{{define "outModal"}}
|
||||
{{define "outboundModal"}}
|
||||
<a-modal id="out-modal" v-model="outModal.visible" :title="outModal.title" @ok="outModal.ok"
|
||||
:confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:ok-button-props="{ props: { disabled: !outModal.isValid } }" style="overflow: hidden;" width="800px" body-style="padding: 0;"
|
||||
:ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
{{template "form/outbound"}}
|
||||
{{template "form/outbound"}}
|
||||
</a-modal>
|
||||
<script>
|
||||
const outboundCmOptions = {
|
||||
lineNumbers: true,
|
||||
mode: "application/json",
|
||||
lint: true,
|
||||
theme: "xq",
|
||||
lineWrapping: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
};
|
||||
|
||||
const outModal = {
|
||||
title: '',
|
||||
|
|
@ -20,23 +29,32 @@
|
|||
cm: null,
|
||||
duplicateTag: false,
|
||||
isValid: true,
|
||||
activeKey: '1',
|
||||
activeKey: 'ob-settings',
|
||||
tags: [],
|
||||
ok() {
|
||||
ObjectUtil.execute(outModal.confirm, outModal.outbound.toJson());
|
||||
},
|
||||
show({ title='', okText='{{ i18n "confirm" }}', outbound, confirm=(outbound)=>{}, isEdit=false, tags=[] }) {
|
||||
onTabChange(activeKey) {
|
||||
this.activeKey = activeKey;
|
||||
if (activeKey === 'ob-json') {
|
||||
setTimeout(() => this.initJsonEditor(), 0);
|
||||
} else {
|
||||
this.destroyJsonEditor();
|
||||
}
|
||||
},
|
||||
show({ title='', okText='{{ i18n "confirm" }}', outbound, confirm=(outbound)=>{}, isEdit=false, tags=[] }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.jsonMode = false;
|
||||
this.link = '';
|
||||
this.activeKey = '1';
|
||||
this.destroyJsonEditor();
|
||||
this.activeKey = 'ob-settings';
|
||||
this.visible = true;
|
||||
this.outbound = isEdit ? Outbound.fromJson(outbound) : new Outbound();
|
||||
this.isEdit = isEdit;
|
||||
this.tags = tags;
|
||||
this.check()
|
||||
this.check();
|
||||
},
|
||||
close() {
|
||||
outModal.visible = false;
|
||||
|
|
@ -45,8 +63,8 @@
|
|||
loading(loading=true) {
|
||||
outModal.confirmLoading = loading;
|
||||
},
|
||||
check(){
|
||||
if(outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)){
|
||||
check() {
|
||||
if (outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)) {
|
||||
this.duplicateTag = true;
|
||||
this.isValid = false;
|
||||
} else {
|
||||
|
|
@ -54,29 +72,32 @@
|
|||
this.isValid = true;
|
||||
}
|
||||
},
|
||||
initJsonEditor() {
|
||||
const textAreaObj = document.getElementById('outboundJson');
|
||||
if (!textAreaObj) return;
|
||||
this.destroyJsonEditor();
|
||||
textAreaObj.value = JSON.stringify(this.outbound.toJson(), null, 2);
|
||||
this.cm = CodeMirror.fromTextArea(textAreaObj, outboundCmOptions);
|
||||
this.cm.on('change', editor => {
|
||||
const value = editor.getValue();
|
||||
if (this.isJsonString(value)) {
|
||||
this.outbound = Outbound.fromJson(JSON.parse(value));
|
||||
this.check();
|
||||
}
|
||||
});
|
||||
},
|
||||
destroyJsonEditor() {
|
||||
if (this.cm != null) {
|
||||
this.cm.toTextArea();
|
||||
this.cm = null;
|
||||
}
|
||||
},
|
||||
toggleJson(jsonTab) {
|
||||
textAreaObj = document.getElementById('outboundJson');
|
||||
if(jsonTab){
|
||||
if(this.cm != null) {
|
||||
this.cm.toTextArea();
|
||||
this.cm=null;
|
||||
}
|
||||
textAreaObj.value = JSON.stringify(this.outbound.toJson(), null, 2);
|
||||
this.cm = CodeMirror.fromTextArea(textAreaObj, app.cmOptions);
|
||||
this.cm.on('change',editor => {
|
||||
value = editor.getValue();
|
||||
if(this.isJsonString(value)){
|
||||
this.outbound = Outbound.fromJson(JSON.parse(value));
|
||||
this.check();
|
||||
}
|
||||
});
|
||||
this.activeKey = '2';
|
||||
if (jsonTab) {
|
||||
this.activeKey = 'ob-json';
|
||||
setTimeout(() => this.initJsonEditor(), 0);
|
||||
} else {
|
||||
if(this.cm != null) {
|
||||
this.cm.toTextArea();
|
||||
this.cm=null;
|
||||
}
|
||||
this.activeKey = '1';
|
||||
this.destroyJsonEditor();
|
||||
}
|
||||
},
|
||||
isJsonString(str) {
|
||||
|
|
@ -107,13 +128,13 @@
|
|||
canEnableTls() {
|
||||
return this.outModal.outbound.canEnableTls();
|
||||
},
|
||||
convertLink(){
|
||||
newOutbound = Outbound.fromLink(outModal.link);
|
||||
if(newOutbound){
|
||||
convertLink() {
|
||||
const newOutbound = Outbound.fromLink(outModal.link);
|
||||
if (newOutbound) {
|
||||
this.outModal.outbound = newOutbound;
|
||||
this.outModal.toggleJson(true);
|
||||
this.outModal.check();
|
||||
this.$message.success('Link imported successfully...');
|
||||
this.$message.success('Link imported successfully...');
|
||||
outModal.link = '';
|
||||
} else {
|
||||
this.$message.error('Wrong Link!');
|
||||
|
|
@ -122,6 +143,5 @@
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
498
web/html/xui/outbounds.html
Normal file
498
web/html/xui/outbounds.html
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
|
||||
<script src="{{ .base_path }}assets/base64/base64.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/codemirror.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/lint/lint.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/lint/javascript-lint.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/hint/javascript-hint.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/fold/foldcode.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ant-card-body {
|
||||
padding: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-col-sm-24 {
|
||||
margin: 0.5rem -2rem 0.5rem 2rem;
|
||||
}
|
||||
.online-animation .ant-badge-status-dot {
|
||||
animation: 1.2s ease infinite normal none running onlineAnimation;
|
||||
}
|
||||
@keyframes onlineAnimation {
|
||||
0%, 50%, 100% { transform: scale(1); opacity: 1; }
|
||||
10% { transform: scale(1.5); opacity: .2; }
|
||||
}
|
||||
.info-large-tag {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
description='{{ i18n "secAlertSsl" }}'
|
||||
show-icon closable>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
<strong>{{ i18n "pages.outbounds.totalDownUp" }}:</strong>
|
||||
<a-tag color="blue">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
<strong>{{ i18n "pages.outbounds.totalUsage" }}:</strong>
|
||||
<a-tag color="blue">[[ sizeFormat(total.up + total.down) ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
<strong>{{ i18n "pages.outbounds.outboundCount" }}:</strong>
|
||||
<a-tag color="blue">[[ dbOutbounds.length ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
|
||||
<strong>{{ i18n "online" }}:</strong>
|
||||
<a-tag color="blue">[[ onlineOutbounds.length ]]</a-tag>
|
||||
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-for="tag in onlineOutbounds" style="margin: 0;">[[ tag ]]</p>
|
||||
</template>
|
||||
<a-tag style="margin:0; padding: 0 2px;" color="green" v-if="onlineOutbounds.length">[[ onlineOutbounds.length ]]</a-tag>
|
||||
</a-popover>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable>
|
||||
<div slot="title">
|
||||
<a-row>
|
||||
<a-col :xs="12" :sm="12" :lg="12">
|
||||
<a-button type="primary" icon="plus" @click="addOutbound()">
|
||||
<template v-if="!isMobile">{{ i18n "pages.xray.outbound.addOutbound" }}</template>
|
||||
</a-button>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button type="primary" icon="menu">
|
||||
<template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
|
||||
</a-button>
|
||||
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="resetTraffics">
|
||||
<a-icon type="reload"></a-icon>
|
||||
{{ i18n "pages.outbounds.resetAllTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="warp">
|
||||
<a-icon type="cloud"></a-icon>
|
||||
WARP
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="12" :lg="12" style="text-align: right;">
|
||||
<a-select v-model="refreshInterval"
|
||||
v-if="isRefreshEnabled"
|
||||
style="width: 70px;"
|
||||
@change="changeRefreshInterval" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
|
||||
</a-select>
|
||||
<a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
|
||||
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div :style="isMobile ? '' : 'display: flex; align-items: center; justify-content: flex-start;'">
|
||||
<a-tooltip :title='`{{ i18n "filter" }}`'>
|
||||
<a-switch v-model="enableFilter"
|
||||
:style="isMobile ? 'margin-bottom: .5rem; display: flex;' : 'margin-right: .5rem;'"
|
||||
@change="toggleFilter">
|
||||
<a-icon slot="checkedChildren" type="search"></a-icon>
|
||||
<a-icon slot="unCheckedChildren" type="filter"></a-icon>
|
||||
</a-switch>
|
||||
</a-tooltip>
|
||||
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px" :size="isMobile ? 'small' : ''"></a-input>
|
||||
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterOutbounds" button-style="solid" :size="isMobile ? 'small' : ''">
|
||||
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<a-back-top></a-back-top>
|
||||
<a-table :columns="isMobile ? mobileColumns : columns" :row-key="r => r.id"
|
||||
:data-source="searchedOutbounds"
|
||||
:scroll="isMobile ? {} : { x: 800 }"
|
||||
:pagination="pagination(searchedOutbounds)"
|
||||
style="margin-top: 10px;">
|
||||
<template slot="actions" slot-scope="text, row, index">
|
||||
<a-tooltip v-if="index > 0">
|
||||
<template slot="title">{{ i18n "pages.xray.rules.first" }}</template>
|
||||
<a-icon style="font-size: 24px; cursor: pointer;" class="normal-icon" type="vertical-align-top" @click="setFirstOutbound(row.id)"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.outbounds.edit" }}</template>
|
||||
<a-icon style="font-size: 24px; cursor: pointer;" class="normal-icon" type="edit" @click="editOutbound(row)"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.outbounds.resetTraffic" }}</template>
|
||||
<a-popconfirm @confirm="resetTraffic(row.id, false)"
|
||||
title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
|
||||
:overlay-class-name="themeSwitcher.currentTheme"
|
||||
ok-text='{{ i18n "reset"}}'
|
||||
cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: #3c89e8' : 'color: blue'"></a-icon>
|
||||
<a-icon style="font-size: 24px; cursor: pointer;" class="normal-icon" type="retweet"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title"><span style="color: #FF4D4F">{{ i18n "delete" }}</span></template>
|
||||
<a-popconfirm @confirm="deleteOutbound(row.id, false)"
|
||||
title='{{ i18n "pages.outbounds.deleteConfirm" }}'
|
||||
:overlay-class-name="themeSwitcher.currentTheme"
|
||||
ok-text='{{ i18n "delete"}}'
|
||||
ok-type="danger"
|
||||
cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" style="color: #e04141"></a-icon>
|
||||
<a-icon style="font-size: 24px; cursor: pointer;" class="delete-icon" type="delete"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template slot="online" slot-scope="text, row">
|
||||
<a-tag v-if="isOutboundOnline(row.tag)" color="green">{{ i18n "online" }}</a-tag>
|
||||
<a-tag v-else>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
<template slot="tag" slot-scope="text, row">
|
||||
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="title">
|
||||
<template v-if="isOutboundOnline(row.tag)">{{ i18n "online" }}</template>
|
||||
<template v-else>{{ i18n "offline" }}</template>
|
||||
</template>
|
||||
<a-badge
|
||||
:class="isOutboundOnline(row.tag) ? 'online-animation' : ''"
|
||||
:color="isOutboundOnline(row.tag) ? 'green' : (themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc')">
|
||||
</a-badge>
|
||||
</a-tooltip>
|
||||
[[ row.tag ]]
|
||||
</template>
|
||||
<template slot="protocol" slot-scope="text, row">
|
||||
<a-tag color="purple">[[ row.protocol ]]</a-tag>
|
||||
<template v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(row.protocol)">
|
||||
<a-tag color="blue">[[ row.toOutbound().stream.network ]]</a-tag>
|
||||
<a-tag v-if="row.toOutbound().stream.isTls" color="green">tls</a-tag>
|
||||
<a-tag v-if="row.toOutbound().stream.isReality" color="green">reality</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="address" slot-scope="text, row">
|
||||
<p style="margin: 0 5px;" v-for="addr in findOutboundAddress(row.toOutbound().toJson())">[[ addr ]]</p>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, row">
|
||||
<a-tag color="blue">↑[[ sizeFormat(row.up) ]] / ↓[[ sizeFormat(row.down) ]]</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</transition>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
<script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/dboutbound.js?{{ .cur_ver }}"></script>
|
||||
<script>
|
||||
const columns = [
|
||||
{ title: "ID", align: 'center', width: 30, dataIndex: 'id' },
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', align: 'right', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "online" }}', align: 'center', width: 60, scopedSlots: { customRender: 'online' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', align: 'left', width: 100, scopedSlots: { customRender: 'tag' } },
|
||||
{ title: '{{ i18n "protocol"}}', align: 'center', width: 120, scopedSlots: { customRender: 'protocol' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.address"}}', width: 150, align: 'center', scopedSlots: { customRender: 'address' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', width: 140, scopedSlots: { customRender: 'traffic' } },
|
||||
];
|
||||
const mobileColumns = [
|
||||
{ title: "ID", align: 'center', width: 30, dataIndex: 'id' },
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', align: 'center', width: 110, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', align: 'center', scopedSlots: { customRender: 'tag' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||
];
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
themeSwitcher,
|
||||
spinning: false,
|
||||
isMobile: window.innerWidth <= 768,
|
||||
dbOutbounds: [],
|
||||
searchedOutbounds: [],
|
||||
onlineOutbounds: [],
|
||||
searchKey: '',
|
||||
enableFilter: false,
|
||||
filterBy: '',
|
||||
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true",
|
||||
refreshing: false,
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
showAlert: false,
|
||||
pageSize: 0,
|
||||
},
|
||||
methods: {
|
||||
loading(spinning = true) { this.spinning = spinning; },
|
||||
async getDBOutbounds() {
|
||||
this.refreshing = true;
|
||||
const msg = await HttpUtil.post('/xui/outbound/list');
|
||||
if (!msg.success) {
|
||||
this.refreshing = false;
|
||||
return;
|
||||
}
|
||||
await this.getOnlineOutbounds();
|
||||
this.setOutbounds(msg.obj);
|
||||
setTimeout(() => { this.refreshing = false; }, 500);
|
||||
},
|
||||
async getOnlineOutbounds() {
|
||||
const msg = await HttpUtil.post('/xui/outbound/onlines');
|
||||
if (msg.success) {
|
||||
this.onlineOutbounds = msg.obj != null ? msg.obj : [];
|
||||
}
|
||||
},
|
||||
setOutbounds(list) {
|
||||
this.dbOutbounds = [];
|
||||
this.searchedOutbounds = [];
|
||||
for (const item of list) {
|
||||
const dbOutbound = new DBOutbound(item);
|
||||
this.dbOutbounds.push(dbOutbound);
|
||||
this.searchedOutbounds.push(dbOutbound);
|
||||
}
|
||||
this.filterOutbounds();
|
||||
},
|
||||
isOutboundOnline(tag) {
|
||||
return this.onlineOutbounds.includes(tag);
|
||||
},
|
||||
applyOutboundFilter() {
|
||||
let list = this.dbOutbounds.slice();
|
||||
if (this.enableFilter && this.filterBy === 'online') {
|
||||
list = list.filter(o => this.isOutboundOnline(o.tag));
|
||||
}
|
||||
if (this.searchKey) {
|
||||
const key = this.searchKey.toLowerCase();
|
||||
list = list.filter(o =>
|
||||
o.tag.toLowerCase().includes(key) ||
|
||||
o.protocol.toLowerCase().includes(key)
|
||||
);
|
||||
}
|
||||
this.searchedOutbounds = list;
|
||||
},
|
||||
filterOutbounds() {
|
||||
this.applyOutboundFilter();
|
||||
},
|
||||
toggleFilter() {
|
||||
if (!this.enableFilter) {
|
||||
this.filterBy = '';
|
||||
}
|
||||
this.filterOutbounds();
|
||||
},
|
||||
findOutboundAddress(o) {
|
||||
let serverObj = null;
|
||||
switch (o.protocol) {
|
||||
case Protocols.VMess:
|
||||
case Protocols.VLESS:
|
||||
if (o.settings?.address && o.settings?.port) {
|
||||
return [o.settings.address + ':' + o.settings.port];
|
||||
}
|
||||
break;
|
||||
case Protocols.HTTP:
|
||||
case Protocols.Socks:
|
||||
case Protocols.Shadowsocks:
|
||||
case Protocols.Trojan:
|
||||
serverObj = o.settings?.servers;
|
||||
break;
|
||||
case Protocols.DNS:
|
||||
return [o.settings?.address + ':' + o.settings?.port];
|
||||
case Protocols.Wireguard:
|
||||
return o.settings?.peers?.map(peer => peer.endpoint) ?? [];
|
||||
}
|
||||
return serverObj ? serverObj.map(obj => obj.address + ':' + obj.port) : [];
|
||||
},
|
||||
allTags(excludeId) {
|
||||
return this.dbOutbounds.filter(o => o.id !== excludeId).map(o => o.tag);
|
||||
},
|
||||
addOutbound() {
|
||||
outModal.show({
|
||||
title: '{{ i18n "pages.xray.outbound.addOutbound"}}',
|
||||
okText: '{{ i18n "pages.xray.outbound.addOutbound" }}',
|
||||
confirm: async (outbound) => {
|
||||
outModal.loading();
|
||||
const db = new DBOutbound();
|
||||
const data = DBOutbound.payloadFromOutbound(Outbound.fromJson(outbound), db);
|
||||
await this.submit('/xui/outbound/add', data, outModal);
|
||||
},
|
||||
isEdit: false,
|
||||
tags: this.allTags(-1),
|
||||
});
|
||||
},
|
||||
editOutbound(dbOutbound) {
|
||||
outModal.show({
|
||||
title: '{{ i18n "pages.xray.outbound.editOutbound"}} ' + dbOutbound.tag,
|
||||
outbound: dbOutbound.toOutbound().toJson(),
|
||||
confirm: async (outbound) => {
|
||||
outModal.loading();
|
||||
const data = DBOutbound.payloadFromOutbound(Outbound.fromJson(outbound), dbOutbound);
|
||||
await this.submit(`/xui/outbound/update/${dbOutbound.id}`, data, outModal);
|
||||
},
|
||||
isEdit: true,
|
||||
tags: this.allTags(dbOutbound.id),
|
||||
});
|
||||
},
|
||||
deleteOutbound(id, confirmation = true) {
|
||||
if (confirmation) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.outbounds.delete" }}',
|
||||
content: '{{ i18n "pages.outbounds.deleteConfirm" }}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit(`/xui/outbound/del/${id}`),
|
||||
});
|
||||
} else {
|
||||
this.submit(`/xui/outbound/del/${id}`);
|
||||
}
|
||||
},
|
||||
setFirstOutbound(id) {
|
||||
this.submit(`/xui/outbound/setFirst/${id}`);
|
||||
},
|
||||
resetTraffic(id, confirmation = true) {
|
||||
if (confirmation) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.outbounds.resetTraffic" }}',
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
onOk: () => this.submit(`/xui/outbound/${id}/resetTraffic`),
|
||||
});
|
||||
} else {
|
||||
this.submit(`/xui/outbound/${id}/resetTraffic`);
|
||||
}
|
||||
},
|
||||
resetAllTraffics() {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.outbounds.resetAllTraffic" }}',
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
onOk: () => this.submit('/xui/outbound/resetAllTraffics'),
|
||||
});
|
||||
},
|
||||
generalActions({ key }) {
|
||||
switch (key) {
|
||||
case 'resetTraffics':
|
||||
this.resetAllTraffics();
|
||||
break;
|
||||
case 'warp':
|
||||
warpModal.show();
|
||||
break;
|
||||
}
|
||||
},
|
||||
async submit(url, data, modal) {
|
||||
const msg = await HttpUtil.postWithModal(url, data, modal);
|
||||
if (msg.success) {
|
||||
await this.getDBOutbounds();
|
||||
}
|
||||
},
|
||||
async startDataRefreshLoop() {
|
||||
while (this.isRefreshEnabled) {
|
||||
try {
|
||||
await this.getDBOutbounds();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
await PromiseUtil.sleep(this.refreshInterval);
|
||||
}
|
||||
},
|
||||
toggleRefresh() {
|
||||
localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
|
||||
if (this.isRefreshEnabled) {
|
||||
this.startDataRefreshLoop();
|
||||
}
|
||||
},
|
||||
changeRefreshInterval() {
|
||||
localStorage.setItem("refreshInterval", this.refreshInterval);
|
||||
},
|
||||
async manualRefresh() {
|
||||
if (!this.refreshing) {
|
||||
this.loading(true);
|
||||
await this.getDBOutbounds();
|
||||
this.loading(false);
|
||||
}
|
||||
},
|
||||
pagination(obj) {
|
||||
if (this.pageSize > 0 && obj.length > this.pageSize) {
|
||||
const sizeOptions = [];
|
||||
for (let i = this.pageSize; i <= obj.length; i += this.pageSize) {
|
||||
sizeOptions.push(i.toString());
|
||||
}
|
||||
sizeOptions.push(obj.length.toString());
|
||||
return {
|
||||
showSizeChanger: true,
|
||||
size: 'small',
|
||||
position: 'bottom',
|
||||
pageSize: this.pageSize,
|
||||
pageSizeOptions: sizeOptions,
|
||||
};
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onResize() {
|
||||
this.isMobile = window.innerWidth <= 768;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchKey: debounce(function () {
|
||||
this.applyOutboundFilter();
|
||||
}, 500),
|
||||
},
|
||||
computed: {
|
||||
total() {
|
||||
let up = 0, down = 0;
|
||||
this.dbOutbounds.forEach(o => { up += o.up; down += o.down; });
|
||||
return { up, down };
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (window.location.protocol !== "https:") {
|
||||
this.showAlert = true;
|
||||
}
|
||||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
this.loading();
|
||||
if (this.isRefreshEnabled) {
|
||||
this.startDataRefreshLoop();
|
||||
} else {
|
||||
this.getDBOutbounds();
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{{template "outboundModal"}}
|
||||
{{template "warpModal"}}
|
||||
</body>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -513,50 +513,6 @@
|
|||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-outbound" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
|
||||
<a-button type="primary" icon="plus" @click="addOutbound()" style="margin-bottom: 10px;">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
||||
<a-button type="primary" icon="cloud" @click="showWarp()" style="margin-bottom: 10px;">WARP</a-button>
|
||||
<a-table :columns="outboundColumns" bordered
|
||||
:row-key="r => r.key"
|
||||
:data-source="outboundData"
|
||||
:scroll="isMobile ? {} : { x: 200 }"
|
||||
:pagination="false"
|
||||
:indent-size="0"
|
||||
:style="isMobile ? 'padding: 5px 5px' : 'margin-right: 1px;'">
|
||||
<template slot="action" slot-scope="text, outbound, index">
|
||||
[[ index+1 ]]
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item v-if="index>0" @click="setFirstOutbound(index)">
|
||||
<a-icon type="vertical-align-top"></a-icon>
|
||||
{{ i18n "pages.xray.rules.first"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="editOutbound(index)">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteOutbound(index)">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="address" slot-scope="text, outbound, index">
|
||||
<p style="margin: 0 5px;" v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
|
||||
</template>
|
||||
<template slot="protocol" slot-scope="text, outbound, index">
|
||||
<a-tag style="margin:0;" color="purple">[[ outbound.protocol ]]</a-tag>
|
||||
<template v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||
<a-tag style="margin:0;" color="blue">[[ outbound.streamSettings.network ]]</a-tag>
|
||||
<a-tag style="margin:0;" v-if="outbound.streamSettings.security=='tls'" color="green">tls</a-tag>
|
||||
<a-tag style="margin:0;" v-if="outbound.streamSettings.security=='reality'" color="green">reality</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-balancer" tab='{{ i18n "pages.xray.Balancers"}}' style="padding-top: 20px;" force-render="true">
|
||||
<a-button type="primary" icon="plus" @click="addBalancer()" style="margin-bottom: 10px;">{{ i18n "pages.xray.balancer.addBalancer"}}</a-button>
|
||||
<a-table :columns="balancerColumns" bordered v-if="balancersData.length>0"
|
||||
|
|
@ -743,7 +699,6 @@
|
|||
<a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" style="margin: 10px 0;" :size="isMobile ? 'small' : ''">
|
||||
<a-radio-button value="xraySetting">{{ i18n "pages.xray.completeTemplate"}}</a-radio-button>
|
||||
<a-radio-button value="inboundSettings">{{ i18n "pages.xray.Inbounds" }}</a-radio-button>
|
||||
<a-radio-button value="outboundSettings">{{ i18n "pages.xray.Outbounds" }}</a-radio-button>
|
||||
<a-radio-button value="routingRuleSettings">{{ i18n "pages.xray.Routings" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<textarea style="position:absolute; left: -800px;" id="xraySetting"></textarea>
|
||||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
381
web/service/outbound.go
Normal file
381
web/service/outbound.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" = "افزودن بالانسر"
|
||||
|
|
|
|||
|
|
@ -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" = "Добавить балансир"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" = "添加平衡器"
|
||||
|
|
|
|||
28
xray/api.go
28
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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
46
xray/outbound.go
Normal file
46
xray/outbound.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue