9 Commits 36b2a58675 ... 094ea9faaa

Tác giả SHA1 Thông báo Ngày
  MHSanaei 094ea9faaa tun: dual MTU, gateway, DNS, auto routing 20 giờ trước cách đây
  MHSanaei eb16cca551 Add ipsBlocked to Freedom 20 giờ trước cách đây
  MHSanaei aef0503f8f Bump Go version and update dependencies 20 giờ trước cách đây
  MHSanaei 86304226a9 mKCP transport: Add cwndMultiplier 20 giờ trước cách đây
  MHSanaei 6d0e7ec495 reset button for auth password 21 giờ trước cách đây
  MHSanaei 04b4fb4384 finalmask 21 giờ trước cách đây
  MHSanaei ae5ad505d0 add hysteria inbound 22 giờ trước cách đây
  MHSanaei c188056f64 Centralize session options and adjust cookies 1 ngày trước cách đây
  MHSanaei 0a424a9f16 Use vnext/users structure for VLESS outbound 1 ngày trước cách đây
45 tập tin đã thay đổi với 3850 bổ sung2168 xóa
  1. 5 3
      database/model/model.go
  2. 17 17
      go.mod
  3. 32 33
      go.sum
  4. 63 10
      sub/subJsonService.go
  5. 67 1
      sub/subService.go
  6. 3 1
      web/assets/js/model/dbinbound.js
  7. 343 271
      web/assets/js/model/inbound.js
  8. 27 19
      web/assets/js/model/outbound.js
  9. 11 8
      web/assets/js/util/index.js
  10. 0 6
      web/controller/index.go
  11. 2 2
      web/html/component/aClientTable.html
  12. 245 161
      web/html/form/client.html
  13. 17 21
      web/html/form/inbound.html
  14. 711 295
      web/html/form/outbound.html
  15. 32 0
      web/html/form/protocol/hysteria.html
  16. 70 44
      web/html/form/protocol/shadowsocks.html
  17. 37 8
      web/html/form/protocol/socks.html
  18. 33 30
      web/html/form/protocol/trojan.html
  19. 88 38
      web/html/form/protocol/tun.html
  20. 1 1
      web/html/form/protocol/vless.html
  21. 4 3
      web/html/form/protocol/vmess.html
  22. 11 5
      web/html/form/protocol/wireguard.html
  23. 2 4
      web/html/form/reality_settings.html
  24. 48 12
      web/html/form/stream/external_proxy.html
  25. 249 81
      web/html/form/stream/stream_finalmask.html
  26. 14 10
      web/html/form/stream/stream_grpc.html
  27. 36 8
      web/html/form/stream/stream_httpupgrade.html
  28. 115 0
      web/html/form/stream/stream_hysteria.html
  29. 43 29
      web/html/form/stream/stream_kcp.html
  30. 35 28
      web/html/form/stream/stream_settings.html
  31. 115 71
      web/html/form/stream/stream_sockopt.html
  32. 90 20
      web/html/form/stream/stream_tcp.html
  33. 34 8
      web/html/form/stream/stream_ws.html
  34. 210 148
      web/html/form/stream/stream_xhttp.html
  35. 12 9
      web/html/form/tls_settings.html
  36. 331 152
      web/html/inbounds.html
  37. 70 34
      web/html/modals/client_bulk_modal.html
  38. 11 5
      web/html/modals/client_modal.html
  39. 448 483
      web/html/modals/inbound_info_modal.html
  40. 134 65
      web/html/modals/inbound_modal.html
  41. 19 3
      web/service/inbound.go
  42. 2 2
      web/service/xray.go
  43. 0 12
      web/session/session.go
  44. 8 7
      web/web.go
  45. 5 0
      xray/api.go

+ 5 - 3
database/model/model.go

@@ -21,6 +21,7 @@ const (
 	Shadowsocks Protocol = "shadowsocks"
 	Shadowsocks Protocol = "shadowsocks"
 	Mixed       Protocol = "mixed"
 	Mixed       Protocol = "mixed"
 	WireGuard   Protocol = "wireguard"
 	WireGuard   Protocol = "wireguard"
+	Hysteria    Protocol = "hysteria"
 )
 )
 
 
 // User represents a user account in the 3x-ui panel.
 // User represents a user account in the 3x-ui panel.
@@ -118,10 +119,11 @@ type CustomGeoResource struct {
 
 
 // Client represents a client configuration for Xray inbounds with traffic limits and settings.
 // Client represents a client configuration for Xray inbounds with traffic limits and settings.
 type Client struct {
 type Client struct {
-	ID         string `json:"id"`                           // Unique client identifier
+	ID         string `json:"id,omitempty"`                 // Unique client identifier
 	Security   string `json:"security"`                     // Security method (e.g., "auto", "aes-128-gcm")
 	Security   string `json:"security"`                     // Security method (e.g., "auto", "aes-128-gcm")
-	Password   string `json:"password"`                     // Client password
-	Flow       string `json:"flow"`                         // Flow control (XTLS)
+	Password   string `json:"password,omitempty"`           // Client password
+	Flow       string `json:"flow,omitempty"`               // Flow control (XTLS)
+	Auth       string `json:"auth,omitempty"`               // Auth password (Hysteria)
 	Email      string `json:"email"`                        // Client email identifier
 	Email      string `json:"email"`                        // Client email identifier
 	LimitIP    int    `json:"limitIp"`                      // IP limit for this client
 	LimitIP    int    `json:"limitIp"`                      // IP limit for this client
 	TotalGB    int64  `json:"totalGB" form:"totalGB"`       // Total traffic limit in GB
 	TotalGB    int64  `json:"totalGB" form:"totalGB"`       // Total traffic limit in GB

+ 17 - 17
go.mod

@@ -1,6 +1,6 @@
 module github.com/mhsanaei/3x-ui/v2
 module github.com/mhsanaei/3x-ui/v2
 
 
-go 1.26.1
+go 1.26.2
 
 
 require (
 require (
 	github.com/gin-contrib/gzip v1.2.6
 	github.com/gin-contrib/gzip v1.2.6
@@ -12,20 +12,20 @@ require (
 	github.com/google/uuid v1.6.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/websocket v1.5.3
 	github.com/gorilla/websocket v1.5.3
 	github.com/joho/godotenv v1.5.1
 	github.com/joho/godotenv v1.5.1
-	github.com/mymmrac/telego v1.7.0
+	github.com/mymmrac/telego v1.8.0
 	github.com/nicksnyder/go-i18n/v2 v2.6.1
 	github.com/nicksnyder/go-i18n/v2 v2.6.1
 	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
 	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.0
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/shirou/gopsutil/v4 v4.26.3
 	github.com/shirou/gopsutil/v4 v4.26.3
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
-	github.com/valyala/fasthttp v1.69.0
+	github.com/valyala/fasthttp v1.70.0
 	github.com/xlzd/gotp v0.1.0
 	github.com/xlzd/gotp v0.1.0
 	github.com/xtls/xray-core v1.260327.0
 	github.com/xtls/xray-core v1.260327.0
 	go.uber.org/atomic v1.11.0
 	go.uber.org/atomic v1.11.0
-	golang.org/x/crypto v0.49.0
-	golang.org/x/sys v0.42.0
-	golang.org/x/text v0.35.0
+	golang.org/x/crypto v0.50.0
+	golang.org/x/sys v0.43.0
+	golang.org/x/text v0.36.0
 	google.golang.org/grpc v1.80.0
 	google.golang.org/grpc v1.80.0
 	google.golang.org/protobuf v1.36.11
 	google.golang.org/protobuf v1.36.11
 	gorm.io/driver/sqlite v1.6.0
 	gorm.io/driver/sqlite v1.6.0
@@ -62,18 +62,18 @@ require (
 	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // 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.38 // indirect
+	github.com/mattn/go-isatty v0.0.21 // indirect
+	github.com/mattn/go-sqlite3 v1.14.42 // indirect
 	github.com/miekg/dns v1.1.72 // indirect
 	github.com/miekg/dns v1.1.72 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
-	github.com/pires/go-proxyproto v0.11.0 // indirect
+	github.com/pires/go-proxyproto v0.12.0 // indirect
 	github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // 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/qpack v0.6.0 // indirect
 	github.com/quic-go/quic-go v0.59.0 // indirect
 	github.com/quic-go/quic-go v0.59.0 // indirect
 	github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect
 	github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
-	github.com/sagernet/sing v0.8.4 // indirect
+	github.com/sagernet/sing v0.8.8 // indirect
 	github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
 	github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
 	github.com/tklauser/go-sysconf v0.3.16 // indirect
 	github.com/tklauser/go-sysconf v0.3.16 // indirect
 	github.com/tklauser/numcpus v0.11.0 // indirect
 	github.com/tklauser/numcpus v0.11.0 // indirect
@@ -85,18 +85,18 @@ require (
 	github.com/vishvananda/netns v0.0.5 // indirect
 	github.com/vishvananda/netns v0.0.5 // indirect
 	github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect
 	github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect
 	github.com/yusufpapurcu/wmi v1.2.4 // 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.5.1 // indirect
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
-	golang.org/x/arch v0.25.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/arch v0.26.0 // indirect
+	golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
+	golang.org/x/mod v0.35.0 // indirect
+	golang.org/x/net v0.53.0 // indirect
 	golang.org/x/sync v0.20.0 // indirect
 	golang.org/x/sync v0.20.0 // indirect
 	golang.org/x/time v0.15.0 // indirect
 	golang.org/x/time v0.15.0 // indirect
-	golang.org/x/tools v0.43.0 // indirect
+	golang.org/x/tools v0.44.0 // indirect
 	golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
 	golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
 	gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
 	gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
 	lukechampine.com/blake3 v1.4.1 // indirect
 	lukechampine.com/blake3 v1.4.1 // indirect
 )
 )

+ 32 - 33
go.sum

@@ -119,10 +119,10 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
 github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
 github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
 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-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4=
-github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
+github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
+github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
+github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
 github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
 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/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -130,8 +130,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/mymmrac/telego v1.7.0 h1:yRO/l00tFGG4nY66ufUKb4ARqv7qx9+LsjQv/b0NEyo=
-github.com/mymmrac/telego v1.7.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
+github.com/mymmrac/telego v1.8.0 h1:EvIprWo9Cn0MHgumvvqNXPAXO1yJj3pu2cdCCeDxbow=
+github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
 github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
 github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
 github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
 github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@@ -140,8 +140,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 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 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.0/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/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
+github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
@@ -156,8 +156,8 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 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.8 h1:1dRlGJ3wm4d2nwjKI1R/dr/7GKDKgUvXyD4OAWlQyt8=
+github.com/sagernet/sing v0.8.8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
 github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
 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/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 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
@@ -185,8 +185,8 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
 github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
 github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
-github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
+github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=
+github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=
 github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
 github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
 github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
 github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
 github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
 github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@@ -203,8 +203,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
 github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
 github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
 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.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro=
+go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
 go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
 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/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 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
@@ -225,40 +225,39 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
 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/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
-golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
-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/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
-golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4=
+golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
+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/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
+golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
+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/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
+golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
 golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
 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/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
+golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
 golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
 golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
 golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
 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.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
 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/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
 gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
 gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
 gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
 gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
 google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
 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.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

+ 63 - 10
sub/subJsonService.go

@@ -194,6 +194,8 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
 			newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client))
 			newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client))
 		case "trojan", "shadowsocks":
 		case "trojan", "shadowsocks":
 			newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
 			newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
+		case "hysteria":
+			newOutbounds = append(newOutbounds, s.genHy(inbound, newStream, client))
 		}
 		}
 
 
 		newOutbounds = append(newOutbounds, s.defaultOutbounds...)
 		newOutbounds = append(newOutbounds, s.defaultOutbounds...)
@@ -322,22 +324,30 @@ func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_ut
 		outbound.Mux = json_util.RawMessage(s.mux)
 		outbound.Mux = json_util.RawMessage(s.mux)
 	}
 	}
 	outbound.StreamSettings = streamSettings
 	outbound.StreamSettings = streamSettings
-	settings := make(map[string]any)
-	settings["address"] = inbound.Listen
-	settings["port"] = inbound.Port
-	settings["id"] = client.ID
-	if client.Flow != "" {
-		settings["flow"] = client.Flow
-	}
 
 
 	// Add encryption for VLESS outbound from inbound settings
 	// Add encryption for VLESS outbound from inbound settings
 	var inboundSettings map[string]any
 	var inboundSettings map[string]any
 	json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
 	json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
-	if encryption, ok := inboundSettings["encryption"].(string); ok {
-		settings["encryption"] = encryption
+	encryption, _ := inboundSettings["encryption"].(string)
+
+	user := map[string]any{
+		"id":         client.ID,
+		"level":      8,
+		"encryption": encryption,
+	}
+	if client.Flow != "" {
+		user["flow"] = client.Flow
+	}
+
+	vnext := map[string]any{
+		"address": inbound.Listen,
+		"port":    inbound.Port,
+		"users":   []any{user},
 	}
 	}
 
 
-	outbound.Settings = settings
+	outbound.Settings = map[string]any{
+		"vnext": []any{vnext},
+	}
 	result, _ := json.MarshalIndent(outbound, "", "  ")
 	result, _ := json.MarshalIndent(outbound, "", "  ")
 	return result
 	return result
 }
 }
@@ -381,6 +391,49 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
 	return result
 	return result
 }
 }
 
 
+func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, client model.Client) json_util.RawMessage {
+	outbound := Outbound{}
+
+	outbound.Protocol = string(inbound.Protocol)
+	outbound.Tag = "proxy"
+
+	if s.mux != "" {
+		outbound.Mux = json_util.RawMessage(s.mux)
+	}
+
+	var settings, stream map[string]any
+	json.Unmarshal([]byte(inbound.Settings), &settings)
+	version, _ := settings["version"].(float64)
+	outbound.Settings = map[string]any{
+		"version": int(version),
+		"address": inbound.Listen,
+		"port":    inbound.Port,
+	}
+
+	json.Unmarshal([]byte(inbound.StreamSettings), &stream)
+	hyStream := stream["hysteriaSettings"].(map[string]any)
+	outHyStream := map[string]any{
+		"version": int(version),
+		"auth":    client.Auth,
+	}
+	if udpIdleTimeout, ok := hyStream["udpIdleTimeout"].(float64); ok {
+		outHyStream["udpIdleTimeout"] = int(udpIdleTimeout)
+	}
+	newStream["hysteriaSettings"] = outHyStream
+
+	if finalmask, ok := hyStream["finalmask"].(map[string]any); ok {
+		newStream["finalmask"] = finalmask
+	}
+
+	newStream["network"] = "hysteria"
+	newStream["security"] = "tls"
+
+	outbound.StreamSettings, _ = json.MarshalIndent(newStream, "", "  ")
+
+	result, _ := json.MarshalIndent(outbound, "", "  ")
+	return result
+}
+
 type Outbound struct {
 type Outbound struct {
 	Protocol       string               `json:"protocol"`
 	Protocol       string               `json:"protocol"`
 	Tag            string               `json:"tag"`
 	Tag            string               `json:"tag"`

+ 67 - 1
sub/subService.go

@@ -120,7 +120,7 @@ func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error)
 		FROM inbounds,
 		FROM inbounds,
 			JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client 
 			JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client 
 		WHERE
 		WHERE
-			protocol in ('vmess','vless','trojan','shadowsocks')
+			protocol in ('vmess','vless','trojan','shadowsocks','hysteria')
 			AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?
 			AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?
 	)`, subId, true).Find(&inbounds).Error
 	)`, subId, true).Find(&inbounds).Error
 	if err != nil {
 	if err != nil {
@@ -171,6 +171,8 @@ func (s *SubService) getLink(inbound *model.Inbound, email string) string {
 		return s.genTrojanLink(inbound, email)
 		return s.genTrojanLink(inbound, email)
 	case "shadowsocks":
 	case "shadowsocks":
 		return s.genShadowsocksLink(inbound, email)
 		return s.genShadowsocksLink(inbound, email)
+	case "hysteria":
+		return s.genHysteriaLink(inbound, email)
 	}
 	}
 	return ""
 	return ""
 }
 }
@@ -885,6 +887,70 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
 	return url.String()
 	return url.String()
 }
 }
 
 
+func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) string {
+	address := s.address
+	if inbound.Protocol != model.Hysteria {
+		return ""
+	}
+	var stream map[string]interface{}
+	json.Unmarshal([]byte(inbound.StreamSettings), &stream)
+	clients, _ := s.inboundService.GetClients(inbound)
+	clientIndex := -1
+	for i, client := range clients {
+		if client.Email == email {
+			clientIndex = i
+			break
+		}
+	}
+	auth := clients[clientIndex].Auth
+	port := inbound.Port
+	params := make(map[string]string)
+
+	params["security"] = "tls"
+	tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
+	alpns, _ := tlsSetting["alpn"].([]interface{})
+	var alpn []string
+	for _, a := range alpns {
+		alpn = append(alpn, a.(string))
+	}
+	if len(alpn) > 0 {
+		params["alpn"] = strings.Join(alpn, ",")
+	}
+	if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
+		params["sni"], _ = sniValue.(string)
+	}
+
+	tlsSettings, _ := searchKey(tlsSetting, "settings")
+	if tlsSetting != nil {
+		if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
+			params["fp"], _ = fpValue.(string)
+		}
+		if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
+			if insecure.(bool) {
+				params["insecure"] = "1"
+			}
+		}
+	}
+
+	var settings map[string]interface{}
+	json.Unmarshal([]byte(inbound.Settings), &settings)
+	version, _ := settings["version"].(float64)
+	protocol := "hysteria2"
+	if int(version) == 1 {
+		protocol = "hysteria"
+	}
+
+	link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, address, port)
+	url, _ := url.Parse(link)
+	q := url.Query()
+	for k, v := range params {
+		q.Add(k, v)
+	}
+	url.RawQuery = q.Encode()
+	url.Fragment = s.genRemark(inbound, email, "")
+	return url.String()
+}
+
 func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
 func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
 	separationChar := string(s.remarkModel[0])
 	separationChar := string(s.remarkModel[0])
 	orderChars := s.remarkModel[1:]
 	orderChars := s.remarkModel[1:]

+ 3 - 1
web/assets/js/model/dbinbound.js

@@ -125,7 +125,7 @@ class DBInbound {
             sniffing: sniffing,
             sniffing: sniffing,
             clientStats: this.clientStats,
             clientStats: this.clientStats,
         };
         };
-        
+
         this._cachedInbound = Inbound.fromJson(config);
         this._cachedInbound = Inbound.fromJson(config);
         return this._cachedInbound;
         return this._cachedInbound;
     }
     }
@@ -147,6 +147,7 @@ class DBInbound {
             case Protocols.VMESS:
             case Protocols.VMESS:
             case Protocols.VLESS:
             case Protocols.VLESS:
             case Protocols.TROJAN:
             case Protocols.TROJAN:
+            case Protocols.HYSTERIA:
                 return true;
                 return true;
             case Protocols.SHADOWSOCKS:
             case Protocols.SHADOWSOCKS:
                 return this.toInbound().isSSMultiUser;
                 return this.toInbound().isSSMultiUser;
@@ -161,6 +162,7 @@ class DBInbound {
             case Protocols.VLESS:
             case Protocols.VLESS:
             case Protocols.TROJAN:
             case Protocols.TROJAN:
             case Protocols.SHADOWSOCKS:
             case Protocols.SHADOWSOCKS:
+            case Protocols.HYSTERIA:
                 return true;
                 return true;
             default:
             default:
                 return false;
                 return false;

+ 343 - 271
web/assets/js/model/inbound.js

@@ -8,6 +8,7 @@ const Protocols = {
     HTTP: 'http',
     HTTP: 'http',
     WIREGUARD: 'wireguard',
     WIREGUARD: 'wireguard',
     TUN: 'tun',
     TUN: 'tun',
+    HYSTERIA: 'hysteria',
 };
 };
 
 
 const SSMethods = {
 const SSMethods = {
@@ -322,18 +323,16 @@ class KcpStreamSettings extends XrayCommonClass {
         tti = 20,
         tti = 20,
         uplinkCapacity = 5,
         uplinkCapacity = 5,
         downlinkCapacity = 20,
         downlinkCapacity = 20,
-        congestion = false,
-        readBufferSize = 1,
-        writeBufferSize = 1,
+        cwndMultiplier = 0,
+        maxSendingWindow = 0,
     ) {
     ) {
         super();
         super();
         this.mtu = mtu;
         this.mtu = mtu;
         this.tti = tti;
         this.tti = tti;
         this.upCap = uplinkCapacity;
         this.upCap = uplinkCapacity;
         this.downCap = downlinkCapacity;
         this.downCap = downlinkCapacity;
-        this.congestion = congestion;
-        this.readBuffer = readBufferSize;
-        this.writeBuffer = writeBufferSize;
+        this.cwndMultiplier = cwndMultiplier;
+        this.maxSendingWindow = maxSendingWindow;
     }
     }
 
 
     static fromJson(json = {}) {
     static fromJson(json = {}) {
@@ -342,9 +341,8 @@ class KcpStreamSettings extends XrayCommonClass {
             json.tti,
             json.tti,
             json.uplinkCapacity,
             json.uplinkCapacity,
             json.downlinkCapacity,
             json.downlinkCapacity,
-            json.congestion,
-            json.readBufferSize,
-            json.writeBufferSize,
+            json.cwndMultiplier,
+            json.maxSendingWindow,
         );
         );
     }
     }
 
 
@@ -354,9 +352,8 @@ class KcpStreamSettings extends XrayCommonClass {
             tti: this.tti,
             tti: this.tti,
             uplinkCapacity: this.upCap,
             uplinkCapacity: this.upCap,
             downlinkCapacity: this.downCap,
             downlinkCapacity: this.downCap,
-            congestion: this.congestion,
-            readBufferSize: this.readBuffer,
-            writeBufferSize: this.writeBuffer,
+            cwndMultiplier: this.cwndMultiplier,
+            maxSendingWindow: this.maxSendingWindow,
         };
         };
     }
     }
 }
 }
@@ -589,6 +586,106 @@ class xHTTPStreamSettings extends XrayCommonClass {
     }
     }
 }
 }
 
 
+class HysteriaStreamSettings extends XrayCommonClass {
+    constructor(
+        protocol,
+        version = 2,
+        auth = '',
+        udpIdleTimeout = 60,
+        masquerade,
+    ) {
+        super(protocol);
+        this.version = version;
+        this.auth = auth;
+        this.udpIdleTimeout = udpIdleTimeout;
+        this.masquerade = masquerade;
+    }
+
+    static fromJson(json = {}) {
+        return new HysteriaStreamSettings(
+            json.protocol,
+            json.version ?? 2,
+            json.auth ?? '',
+            json.udpIdleTimeout ?? 60,
+            json.masquerade ? HysteriaMasquerade.fromJson(json.masquerade) : undefined,
+        );
+    }
+
+    toJson() {
+        return {
+            protocol: this.protocol,
+            version: this.version,
+            auth: this.auth,
+            udpIdleTimeout: this.udpIdleTimeout,
+            masquerade: this.masqueradeSwitch ? this.masquerade.toJson() : undefined,
+        };
+    }
+
+    get masqueradeSwitch() {
+        return this.masquerade != undefined;
+    }
+
+    set masqueradeSwitch(value) {
+        this.masquerade = value ? new HysteriaMasquerade() : undefined;
+    }
+};
+
+class HysteriaMasquerade extends XrayCommonClass {
+    constructor(
+        type = 'proxy',
+        dir = '',
+        url = '',
+        rewriteHost = false,
+        insecure = false,
+        content = '',
+        headers = [],
+        statusCode = 0,
+    ) {
+        super();
+        this.type = type;
+        this.dir = dir;
+        this.url = url;
+        this.rewriteHost = rewriteHost;
+        this.insecure = insecure;
+        this.content = content;
+        this.headers = headers;
+        this.statusCode = statusCode;
+    }
+
+    addHeader(name, value) {
+        this.headers.push({ name: name, value: value });
+    }
+
+    removeHeader(index) {
+        this.headers.splice(index, 1);
+    }
+
+    static fromJson(json = {}) {
+        return new HysteriaMasquerade(
+            json.type,
+            json.dir,
+            json.url,
+            json.rewriteHost,
+            json.insecure,
+            json.content,
+            XrayCommonClass.toHeaders(json.headers),
+            json.statusCode,
+        );
+    }
+
+    toJson() {
+        return {
+            type: this.type,
+            dir: this.dir,
+            url: this.url,
+            rewriteHost: this.rewriteHost,
+            insecure: this.insecure,
+            content: this.content,
+            headers: XrayCommonClass.toV2Headers(this.headers, false),
+            statusCode: this.statusCode,
+        };
+    }
+};
 class TlsStreamSettings extends XrayCommonClass {
 class TlsStreamSettings extends XrayCommonClass {
     constructor(
     constructor(
         serverName = '',
         serverName = '',
@@ -987,6 +1084,12 @@ class UdpMask extends XrayCommonClass {
             case 'header-wechat':
             case 'header-wechat':
             case 'header-wireguard':
             case 'header-wireguard':
                 return {};
                 return {};
+            case 'header-custom':
+                return { client: [], server: [] };
+            case 'noise':
+                return { reset: 0, noise: [] };
+            case 'sudoku':
+                return { ascii: '', customTable: '', customTables: [], paddingMin: 0, paddingMax: 0 };
             default:
             default:
                 return settings;
                 return settings;
         }
         }
@@ -1021,7 +1124,6 @@ class FinalMaskStreamSettings extends XrayCommonClass {
         return {
         return {
             udp: this.udp.map(udp => udp.toJson())
             udp: this.udp.map(udp => udp.toJson())
         };
         };
-
     }
     }
 }
 }
 
 
@@ -1037,6 +1139,7 @@ class StreamSettings extends XrayCommonClass {
         grpcSettings = new GrpcStreamSettings(),
         grpcSettings = new GrpcStreamSettings(),
         httpupgradeSettings = new HTTPUpgradeStreamSettings(),
         httpupgradeSettings = new HTTPUpgradeStreamSettings(),
         xhttpSettings = new xHTTPStreamSettings(),
         xhttpSettings = new xHTTPStreamSettings(),
+        hysteriaSettings = new HysteriaStreamSettings(),
         finalmask = new FinalMaskStreamSettings(),
         finalmask = new FinalMaskStreamSettings(),
         sockopt = undefined,
         sockopt = undefined,
     ) {
     ) {
@@ -1052,6 +1155,7 @@ class StreamSettings extends XrayCommonClass {
         this.grpc = grpcSettings;
         this.grpc = grpcSettings;
         this.httpupgrade = httpupgradeSettings;
         this.httpupgrade = httpupgradeSettings;
         this.xhttp = xhttpSettings;
         this.xhttp = xhttpSettings;
+        this.hysteria = hysteriaSettings;
         this.finalmask = finalmask;
         this.finalmask = finalmask;
         this.sockopt = sockopt;
         this.sockopt = sockopt;
     }
     }
@@ -1116,6 +1220,7 @@ class StreamSettings extends XrayCommonClass {
             GrpcStreamSettings.fromJson(json.grpcSettings),
             GrpcStreamSettings.fromJson(json.grpcSettings),
             HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
             HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
             xHTTPStreamSettings.fromJson(json.xhttpSettings),
             xHTTPStreamSettings.fromJson(json.xhttpSettings),
+            HysteriaStreamSettings.fromJson(json.hysteriaSettings),
             FinalMaskStreamSettings.fromJson(json.finalmask),
             FinalMaskStreamSettings.fromJson(json.finalmask),
             SockoptStreamSettings.fromJson(json.sockopt),
             SockoptStreamSettings.fromJson(json.sockopt),
         );
         );
@@ -1135,6 +1240,7 @@ class StreamSettings extends XrayCommonClass {
             grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
             grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
             httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
             httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
             xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
             xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
+            hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
             finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,
             finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,
             sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
             sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
         };
         };
@@ -1201,6 +1307,7 @@ class Inbound extends XrayCommonClass {
             case Protocols.VLESS: return this.settings.vlesses;
             case Protocols.VLESS: return this.settings.vlesses;
             case Protocols.TROJAN: return this.settings.trojans;
             case Protocols.TROJAN: return this.settings.trojans;
             case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null;
             case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null;
+            case Protocols.HYSTERIA: return this.settings.hysterias;
             default: return null;
             default: return null;
         }
         }
     }
     }
@@ -1212,9 +1319,14 @@ class Inbound extends XrayCommonClass {
     set protocol(protocol) {
     set protocol(protocol) {
         this._protocol = protocol;
         this._protocol = protocol;
         this.settings = Inbound.Settings.getSettings(protocol);
         this.settings = Inbound.Settings.getSettings(protocol);
+        this.stream = new StreamSettings();
         if (protocol === Protocols.TROJAN) {
         if (protocol === Protocols.TROJAN) {
             this.tls = false;
             this.tls = false;
         }
         }
+        if (protocol === Protocols.HYSTERIA) {
+            this.stream.network = 'hysteria';
+            this.stream.security = 'tls';
+        }
     }
     }
 
 
     get network() {
     get network() {
@@ -1316,6 +1428,7 @@ class Inbound extends XrayCommonClass {
     }
     }
 
 
     canEnableTls() {
     canEnableTls() {
+        if (this.protocol === Protocols.HYSTERIA) return true;
         if (![Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol)) return false;
         if (![Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol)) return false;
         return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.network);
         return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.network);
     }
     }
@@ -1342,7 +1455,7 @@ class Inbound extends XrayCommonClass {
     }
     }
 
 
     canEnableStream() {
     canEnableStream() {
-        return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
+        return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA].includes(this.protocol);
     }
     }
 
 
     reset() {
     reset() {
@@ -1689,6 +1802,26 @@ class Inbound extends XrayCommonClass {
         return url.toString();
         return url.toString();
     }
     }
 
 
+    genHysteriaLink(address = '', port = this.port, remark = '', clientAuth) {
+        const protocol = this.settings.version == 2 ? "hysteria2" : "hysteria";
+        const link = `${protocol}://${clientAuth}@${address}:${port}`;
+
+        const params = new Map();
+        params.set("security", "tls");
+        if (this.stream.tls.settings.fingerprint?.length > 0) params.set("fp", this.stream.tls.settings.fingerprint);
+        if (this.stream.tls.alpn?.length > 0) params.set("alpn", this.stream.tls.alpn);
+        if (this.stream.tls.settings.allowInsecure) params.set("insecure", "1");
+        if (this.stream.tls.settings.echConfigList?.length > 0) params.set("ech", this.stream.tls.settings.echConfigList.join(','));
+        if (this.stream.tls.sni?.length > 0) params.set("sni", this.stream.tls.sni);
+
+        const url = new URL(link);
+        for (const [key, value] of params) {
+            url.searchParams.set(key, value);
+        }
+        url.hash = encodeURIComponent(remark);
+        return url.toString();
+    }
+
     getWireguardLink(address, port, remark, peerId) {
     getWireguardLink(address, port, remark, peerId) {
         let txt = `[Interface]\n`
         let txt = `[Interface]\n`
         txt += `PrivateKey = ${this.settings.peers[peerId].privateKey}\n`
         txt += `PrivateKey = ${this.settings.peers[peerId].privateKey}\n`
@@ -1721,6 +1854,8 @@ class Inbound extends XrayCommonClass {
                 return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : '');
                 return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : '');
             case Protocols.TROJAN:
             case Protocols.TROJAN:
                 return this.genTrojanLink(address, port, forceTls, remark, client.password);
                 return this.genTrojanLink(address, port, forceTls, remark, client.password);
+            case Protocols.HYSTERIA:
+                return this.genHysteriaLink(address, port, remark, client.auth.length > 0 ? client.auth : this.stream.hysteria.auth);
             default: return '';
             default: return '';
         }
         }
     }
     }
@@ -1827,6 +1962,7 @@ Inbound.Settings = class extends XrayCommonClass {
             case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
             case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
             case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
             case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
             case Protocols.TUN: return new Inbound.TunSettings(protocol);
             case Protocols.TUN: return new Inbound.TunSettings(protocol);
+            case Protocols.HYSTERIA: return new Inbound.HysteriaSettings(protocol);
             default: return null;
             default: return null;
         }
         }
     }
     }
@@ -1842,6 +1978,7 @@ Inbound.Settings = class extends XrayCommonClass {
             case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
             case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
             case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
             case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
             case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
             case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
+            case Protocols.HYSTERIA: return Inbound.HysteriaSettings.fromJson(json);
             default: return null;
             default: return null;
         }
         }
     }
     }
@@ -1851,49 +1988,9 @@ Inbound.Settings = class extends XrayCommonClass {
     }
     }
 };
 };
 
 
-Inbound.VmessSettings = class extends Inbound.Settings {
-    constructor(protocol,
-        vmesses = [new Inbound.VmessSettings.VMESS()]) {
-        super(protocol);
-        this.vmesses = vmesses;
-    }
-
-    indexOfVmessById(id) {
-        return this.vmesses.findIndex(VMESS => VMESS.id === id);
-    }
-
-    addVmess(VMESS) {
-        if (this.indexOfVmessById(VMESS.id) >= 0) {
-            return false;
-        }
-        this.vmesses.push(VMESS);
-    }
-
-    delVmess(VMESS) {
-        const i = this.indexOfVmessById(VMESS.id);
-        if (i >= 0) {
-            this.vmesses.splice(i, 1);
-        }
-    }
-
-    static fromJson(json = {}) {
-        return new Inbound.VmessSettings(
-            Protocols.VMESS,
-            json.clients.map(client => Inbound.VmessSettings.VMESS.fromJson(client)),
-        );
-    }
-
-    toJson() {
-        return {
-            clients: Inbound.VmessSettings.toJsonArray(this.vmesses),
-        };
-    }
-};
-
-Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
+/** Shared user-quota fields and UI helpers for multi-user protocol clients. */
+Inbound.ClientBase = class extends XrayCommonClass {
     constructor(
     constructor(
-        id = RandomUtil.randomUUID(),
-        security = USERS_SECURITY.AUTO,
         email = RandomUtil.randomLowerAndNum(8),
         email = RandomUtil.randomLowerAndNum(8),
         limitIp = 0,
         limitIp = 0,
         totalGB = 0,
         totalGB = 0,
@@ -1904,11 +2001,9 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
         comment = '',
         comment = '',
         reset = 0,
         reset = 0,
         created_at = undefined,
         created_at = undefined,
-        updated_at = undefined
+        updated_at = undefined,
     ) {
     ) {
         super();
         super();
-        this.id = id;
-        this.security = security;
         this.email = email;
         this.email = email;
         this.limitIp = limitIp;
         this.limitIp = limitIp;
         this.totalGB = totalGB;
         this.totalGB = totalGB;
@@ -1922,10 +2017,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
         this.updated_at = updated_at;
         this.updated_at = updated_at;
     }
     }
 
 
-    static fromJson(json = {}) {
-        return new Inbound.VmessSettings.VMESS(
-            json.id,
-            json.security,
+    static commonArgsFromJson(json = {}) {
+        return [
             json.email,
             json.email,
             json.limitIp,
             json.limitIp,
             json.totalGB,
             json.totalGB,
@@ -1937,10 +2030,27 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
             json.reset,
             json.reset,
             json.created_at,
             json.created_at,
             json.updated_at,
             json.updated_at,
-        );
+        ];
+    }
+
+    _clientBaseToJson() {
+        return {
+            email: this.email,
+            limitIp: this.limitIp,
+            totalGB: this.totalGB,
+            expiryTime: this.expiryTime,
+            enable: this.enable,
+            tgId: this.tgId,
+            subId: this.subId,
+            comment: this.comment,
+            reset: this.reset,
+            created_at: this.created_at,
+            updated_at: this.updated_at,
+        };
     }
     }
+
     get _expiryTime() {
     get _expiryTime() {
-        if (this.expiryTime === 0 || this.expiryTime === "") {
+        if (this.expiryTime === 0 || this.expiryTime === '') {
             return null;
             return null;
         }
         }
         if (this.expiryTime < 0) {
         if (this.expiryTime < 0) {
@@ -1950,12 +2060,13 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
     }
     }
 
 
     set _expiryTime(t) {
     set _expiryTime(t) {
-        if (t == null || t === "") {
+        if (t == null || t === '') {
             this.expiryTime = 0;
             this.expiryTime = 0;
         } else {
         } else {
             this.expiryTime = t.valueOf();
             this.expiryTime = t.valueOf();
         }
         }
     }
     }
+
     get _totalGB() {
     get _totalGB() {
         return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
         return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
     }
     }
@@ -1963,7 +2074,73 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
     set _totalGB(gb) {
     set _totalGB(gb) {
         this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
         this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
     }
     }
+};
+
+Inbound.VmessSettings = class extends Inbound.Settings {
+    constructor(protocol,
+        vmesses = [new Inbound.VmessSettings.VMESS()]) {
+        super(protocol);
+        this.vmesses = vmesses;
+    }
+
+    indexOfVmessById(id) {
+        return this.vmesses.findIndex(VMESS => VMESS.id === id);
+    }
+
+    addVmess(VMESS) {
+        if (this.indexOfVmessById(VMESS.id) >= 0) {
+            return false;
+        }
+        this.vmesses.push(VMESS);
+    }
+
+    delVmess(VMESS) {
+        const i = this.indexOfVmessById(VMESS.id);
+        if (i >= 0) {
+            this.vmesses.splice(i, 1);
+        }
+    }
 
 
+    static fromJson(json = {}) {
+        return new Inbound.VmessSettings(
+            Protocols.VMESS,
+            (json.clients || []).map(client => Inbound.VmessSettings.VMESS.fromJson(client)),
+        );
+    }
+
+    toJson() {
+        return {
+            clients: Inbound.VmessSettings.toJsonArray(this.vmesses),
+        };
+    }
+};
+
+Inbound.VmessSettings.VMESS = class extends Inbound.ClientBase {
+    constructor(
+        id = RandomUtil.randomUUID(),
+        security = USERS_SECURITY.AUTO,
+        email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at,
+    ) {
+        super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
+        this.id = id;
+        this.security = security;
+    }
+
+    static fromJson(json = {}) {
+        return new Inbound.VmessSettings.VMESS(
+            json.id,
+            json.security,
+            ...Inbound.ClientBase.commonArgsFromJson(json),
+        );
+    }
+
+    toJson() {
+        return {
+            id: this.id,
+            security: this.security,
+            ...this._clientBaseToJson(),
+        };
+    }
 };
 };
 
 
 Inbound.VLESSSettings = class extends Inbound.Settings {
 Inbound.VLESSSettings = class extends Inbound.Settings {
@@ -2041,85 +2218,36 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
 
 
         return json;
         return json;
     }
     }
-
-
 };
 };
 
 
-Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
+Inbound.VLESSSettings.VLESS = class extends Inbound.ClientBase {
     constructor(
     constructor(
         id = RandomUtil.randomUUID(),
         id = RandomUtil.randomUUID(),
         flow = '',
         flow = '',
-        email = RandomUtil.randomLowerAndNum(8),
-        limitIp = 0,
-        totalGB = 0,
-        expiryTime = 0,
-        enable = true,
-        tgId = '',
-        subId = RandomUtil.randomLowerAndNum(16),
-        comment = '',
-        reset = 0,
-        created_at = undefined,
-        updated_at = undefined
+        email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at,
     ) {
     ) {
-        super();
+        super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
         this.id = id;
         this.id = id;
         this.flow = flow;
         this.flow = flow;
-        this.email = email;
-        this.limitIp = limitIp;
-        this.totalGB = totalGB;
-        this.expiryTime = expiryTime;
-        this.enable = enable;
-        this.tgId = tgId;
-        this.subId = subId;
-        this.comment = comment;
-        this.reset = reset;
-        this.created_at = created_at;
-        this.updated_at = updated_at;
     }
     }
 
 
     static fromJson(json = {}) {
     static fromJson(json = {}) {
         return new Inbound.VLESSSettings.VLESS(
         return new Inbound.VLESSSettings.VLESS(
             json.id,
             json.id,
             json.flow,
             json.flow,
-            json.email,
-            json.limitIp,
-            json.totalGB,
-            json.expiryTime,
-            json.enable,
-            json.tgId,
-            json.subId,
-            json.comment,
-            json.reset,
-            json.created_at,
-            json.updated_at,
+            ...Inbound.ClientBase.commonArgsFromJson(json),
         );
         );
     }
     }
 
 
-    get _expiryTime() {
-        if (this.expiryTime === 0 || this.expiryTime === "") {
-            return null;
-        }
-        if (this.expiryTime < 0) {
-            return this.expiryTime / -86400000;
-        }
-        return moment(this.expiryTime);
-    }
-
-    set _expiryTime(t) {
-        if (t == null || t === "") {
-            this.expiryTime = 0;
-        } else {
-            this.expiryTime = t.valueOf();
-        }
-    }
-    get _totalGB() {
-        return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
-    }
-
-    set _totalGB(gb) {
-        this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
+    toJson() {
+        return {
+            id: this.id,
+            flow: this.flow,
+            ...this._clientBaseToJson(),
+        };
     }
     }
 };
 };
+
 Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
 Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
     constructor(name = "", alpn = '', path = '', dest = '', xver = 0) {
     constructor(name = "", alpn = '', path = '', dest = '', xver = 0) {
         super();
         super();
@@ -2179,7 +2307,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
     static fromJson(json = {}) {
     static fromJson(json = {}) {
         return new Inbound.TrojanSettings(
         return new Inbound.TrojanSettings(
             Protocols.TROJAN,
             Protocols.TROJAN,
-            json.clients.map(client => Inbound.TrojanSettings.Trojan.fromJson(client)),
+            (json.clients || []).map(client => Inbound.TrojanSettings.Trojan.fromJson(client)),
             Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),);
             Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),);
     }
     }
 
 
@@ -2191,95 +2319,28 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
     }
     }
 };
 };
 
 
-Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
+Inbound.TrojanSettings.Trojan = class extends Inbound.ClientBase {
     constructor(
     constructor(
         password = RandomUtil.randomSeq(10),
         password = RandomUtil.randomSeq(10),
-        email = RandomUtil.randomLowerAndNum(8),
-        limitIp = 0,
-        totalGB = 0,
-        expiryTime = 0,
-        enable = true,
-        tgId = '',
-        subId = RandomUtil.randomLowerAndNum(16),
-        comment = '',
-        reset = 0,
-        created_at = undefined,
-        updated_at = undefined
+        email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at,
     ) {
     ) {
-        super();
+        super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
         this.password = password;
         this.password = password;
-        this.email = email;
-        this.limitIp = limitIp;
-        this.totalGB = totalGB;
-        this.expiryTime = expiryTime;
-        this.enable = enable;
-        this.tgId = tgId;
-        this.subId = subId;
-        this.comment = comment;
-        this.reset = reset;
-        this.created_at = created_at;
-        this.updated_at = updated_at;
     }
     }
 
 
     toJson() {
     toJson() {
         return {
         return {
             password: this.password,
             password: this.password,
-            email: this.email,
-            limitIp: this.limitIp,
-            totalGB: this.totalGB,
-            expiryTime: this.expiryTime,
-            enable: this.enable,
-            tgId: this.tgId,
-            subId: this.subId,
-            comment: this.comment,
-            reset: this.reset,
-            created_at: this.created_at,
-            updated_at: this.updated_at,
+            ...this._clientBaseToJson(),
         };
         };
     }
     }
 
 
     static fromJson(json = {}) {
     static fromJson(json = {}) {
         return new Inbound.TrojanSettings.Trojan(
         return new Inbound.TrojanSettings.Trojan(
             json.password,
             json.password,
-            json.email,
-            json.limitIp,
-            json.totalGB,
-            json.expiryTime,
-            json.enable,
-            json.tgId,
-            json.subId,
-            json.comment,
-            json.reset,
-            json.created_at,
-            json.updated_at,
+            ...Inbound.ClientBase.commonArgsFromJson(json),
         );
         );
     }
     }
-
-    get _expiryTime() {
-        if (this.expiryTime === 0 || this.expiryTime === "") {
-            return null;
-        }
-        if (this.expiryTime < 0) {
-            return this.expiryTime / -86400000;
-        }
-        return moment(this.expiryTime);
-    }
-
-    set _expiryTime(t) {
-        if (t == null || t === "") {
-            this.expiryTime = 0;
-        } else {
-            this.expiryTime = t.valueOf();
-        }
-    }
-    get _totalGB() {
-        return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
-    }
-
-    set _totalGB(gb) {
-        this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
-    }
-
 };
 };
 
 
 Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
 Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
@@ -2343,7 +2404,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
             json.method,
             json.method,
             json.password,
             json.password,
             json.network,
             json.network,
-            json.clients.map(client => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)),
+            (json.clients || []).map(client => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)),
             json.ivCheck,
             json.ivCheck,
         );
         );
     }
     }
@@ -2359,53 +2420,22 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
     }
     }
 };
 };
 
 
-Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
+Inbound.ShadowsocksSettings.Shadowsocks = class extends Inbound.ClientBase {
     constructor(
     constructor(
         method = '',
         method = '',
         password = RandomUtil.randomShadowsocksPassword(),
         password = RandomUtil.randomShadowsocksPassword(),
-        email = RandomUtil.randomLowerAndNum(8),
-        limitIp = 0,
-        totalGB = 0,
-        expiryTime = 0,
-        enable = true,
-        tgId = '',
-        subId = RandomUtil.randomLowerAndNum(16),
-        comment = '',
-        reset = 0,
-        created_at = undefined,
-        updated_at = undefined
+        email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at,
     ) {
     ) {
-        super();
+        super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
         this.method = method;
         this.method = method;
         this.password = password;
         this.password = password;
-        this.email = email;
-        this.limitIp = limitIp;
-        this.totalGB = totalGB;
-        this.expiryTime = expiryTime;
-        this.enable = enable;
-        this.tgId = tgId;
-        this.subId = subId;
-        this.comment = comment;
-        this.reset = reset;
-        this.created_at = created_at;
-        this.updated_at = updated_at;
     }
     }
 
 
     toJson() {
     toJson() {
         return {
         return {
             method: this.method,
             method: this.method,
             password: this.password,
             password: this.password,
-            email: this.email,
-            limitIp: this.limitIp,
-            totalGB: this.totalGB,
-            expiryTime: this.expiryTime,
-            enable: this.enable,
-            tgId: this.tgId,
-            subId: this.subId,
-            comment: this.comment,
-            reset: this.reset,
-            created_at: this.created_at,
-            updated_at: this.updated_at,
+            ...this._clientBaseToJson(),
         };
         };
     }
     }
 
 
@@ -2413,45 +2443,56 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
         return new Inbound.ShadowsocksSettings.Shadowsocks(
         return new Inbound.ShadowsocksSettings.Shadowsocks(
             json.method,
             json.method,
             json.password,
             json.password,
-            json.email,
-            json.limitIp,
-            json.totalGB,
-            json.expiryTime,
-            json.enable,
-            json.tgId,
-            json.subId,
-            json.comment,
-            json.reset,
-            json.created_at,
-            json.updated_at,
+            ...Inbound.ClientBase.commonArgsFromJson(json),
         );
         );
     }
     }
+};
 
 
-    get _expiryTime() {
-        if (this.expiryTime === 0 || this.expiryTime === "") {
-            return null;
-        }
-        if (this.expiryTime < 0) {
-            return this.expiryTime / -86400000;
-        }
-        return moment(this.expiryTime);
+Inbound.HysteriaSettings = class extends Inbound.Settings {
+    constructor(protocol, version = 2, hysterias = [new Inbound.HysteriaSettings.Hysteria()]) {
+        super(protocol);
+        this.version = version;
+        this.hysterias = hysterias;
     }
     }
 
 
-    set _expiryTime(t) {
-        if (t == null || t === "") {
-            this.expiryTime = 0;
-        } else {
-            this.expiryTime = t.valueOf();
-        }
+    static fromJson(json = {}) {
+        return new Inbound.HysteriaSettings(
+            Protocols.HYSTERIA,
+            json.version ?? 2,
+            (json.clients || []).map(client => Inbound.HysteriaSettings.Hysteria.fromJson(client)),
+        );
     }
     }
-    get _totalGB() {
-        return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
+
+    toJson() {
+        return {
+            version: this.version,
+            clients: Inbound.HysteriaSettings.toJsonArray(this.hysterias),
+        };
     }
     }
+};
 
 
-    set _totalGB(gb) {
-        this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
+Inbound.HysteriaSettings.Hysteria = class extends Inbound.ClientBase {
+    constructor(
+        auth = RandomUtil.randomSeq(10),
+        email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at,
+    ) {
+        super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
+        this.auth = auth;
     }
     }
 
 
+    toJson() {
+        return {
+            auth: this.auth,
+            ...this._clientBaseToJson(),
+        };
+    }
+
+    static fromJson(json = {}) {
+        return new Inbound.HysteriaSettings.Hysteria(
+            json.auth,
+            ...Inbound.ClientBase.commonArgsFromJson(json),
+        );
+    }
 };
 };
 
 
 Inbound.TunnelSettings = class extends Inbound.Settings {
 Inbound.TunnelSettings = class extends Inbound.Settings {
@@ -2682,29 +2723,60 @@ Inbound.TunSettings = class extends Inbound.Settings {
     constructor(
     constructor(
         protocol,
         protocol,
         name = 'xray0',
         name = 'xray0',
-        mtu = 1500,
-        userLevel = 0
+        mtu = [1500, 1280],
+        gateway = [],
+        dns = [],
+        userLevel = 0,
+        autoSystemRoutingTable = [],
+        autoOutboundsInterface = 'auto'
     ) {
     ) {
         super(protocol);
         super(protocol);
         this.name = name;
         this.name = name;
-        this.mtu = mtu;
+        this.mtu = this._normalizeMtu(mtu);
+        this.gateway = Array.isArray(gateway) ? gateway : [];
+        this.dns = Array.isArray(dns) ? dns : [];
         this.userLevel = userLevel;
         this.userLevel = userLevel;
+        this.autoSystemRoutingTable = Array.isArray(autoSystemRoutingTable) ? autoSystemRoutingTable : [];
+        this.autoOutboundsInterface = autoOutboundsInterface;
+    }
+
+    _normalizeMtu(mtu) {
+        if (!Array.isArray(mtu)) {
+            const single = Number(mtu) || 1500;
+            return [single, single];
+        }
+        if (mtu.length === 0) {
+            return [1500, 1280];
+        }
+        if (mtu.length === 1) {
+            const single = Number(mtu[0]) || 1500;
+            return [single, single];
+        }
+        return [Number(mtu[0]) || 1500, Number(mtu[1]) || 1280];
     }
     }
 
 
     static fromJson(json = {}) {
     static fromJson(json = {}) {
         return new Inbound.TunSettings(
         return new Inbound.TunSettings(
             Protocols.TUN,
             Protocols.TUN,
             json.name ?? 'xray0',
             json.name ?? 'xray0',
-            json.mtu ?? json.MTU ?? 1500,
-            json.userLevel ?? 0
+            json.mtu ?? json.MTU ?? [1500, 1280],
+            json.gateway ?? json.Gateway ?? [],
+            json.dns ?? json.DNS ?? [],
+            json.userLevel ?? 0,
+            json.autoSystemRoutingTable ?? [],
+            Object.prototype.hasOwnProperty.call(json, 'autoOutboundsInterface') ? json.autoOutboundsInterface : 'auto'
         );
         );
     }
     }
 
 
     toJson() {
     toJson() {
         return {
         return {
             name: this.name || 'xray0',
             name: this.name || 'xray0',
-            mtu: this.mtu || 1500,
+            mtu: this._normalizeMtu(this.mtu),
+            gateway: this.gateway,
+            dns: this.dns,
             userLevel: this.userLevel || 0,
             userLevel: this.userLevel || 0,
+            autoSystemRoutingTable: this.autoSystemRoutingTable,
+            autoOutboundsInterface: this.autoOutboundsInterface,
         };
         };
     }
     }
-};
+};

+ 27 - 19
web/assets/js/model/outbound.js

@@ -169,18 +169,16 @@ class KcpStreamSettings extends CommonClass {
         tti = 20,
         tti = 20,
         uplinkCapacity = 5,
         uplinkCapacity = 5,
         downlinkCapacity = 20,
         downlinkCapacity = 20,
-        congestion = false,
-        readBufferSize = 1,
-        writeBufferSize = 1,
+        cwndMultiplier = 0,
+        maxSendingWindow = 0,
     ) {
     ) {
         super();
         super();
         this.mtu = mtu;
         this.mtu = mtu;
         this.tti = tti;
         this.tti = tti;
         this.upCap = uplinkCapacity;
         this.upCap = uplinkCapacity;
         this.downCap = downlinkCapacity;
         this.downCap = downlinkCapacity;
-        this.congestion = congestion;
-        this.readBuffer = readBufferSize;
-        this.writeBuffer = writeBufferSize;
+        this.cwndMultiplier = cwndMultiplier;
+        this.maxSendingWindow = maxSendingWindow;
     }
     }
 
 
     static fromJson(json = {}) {
     static fromJson(json = {}) {
@@ -189,9 +187,8 @@ class KcpStreamSettings extends CommonClass {
             json.tti,
             json.tti,
             json.uplinkCapacity,
             json.uplinkCapacity,
             json.downlinkCapacity,
             json.downlinkCapacity,
-            json.congestion,
-            json.readBufferSize,
-            json.writeBufferSize,
+            json.cwndMultiplier,
+            json.maxSendingWindow,
         );
         );
     }
     }
 
 
@@ -201,9 +198,8 @@ class KcpStreamSettings extends CommonClass {
             tti: this.tti,
             tti: this.tti,
             uplinkCapacity: this.upCap,
             uplinkCapacity: this.upCap,
             downlinkCapacity: this.downCap,
             downlinkCapacity: this.downCap,
-            congestion: this.congestion,
-            readBufferSize: this.readBuffer,
-            writeBufferSize: this.writeBuffer,
+            cwndMultiplier: this.cwndMultiplier,
+            maxSendingWindow: this.maxSendingWindow,
         };
         };
     }
     }
 }
 }
@@ -579,6 +575,8 @@ class UdpMask extends CommonClass {
             case 'header-dns':
             case 'header-dns':
             case 'xdns':
             case 'xdns':
                 return { domain: settings.domain || '' };
                 return { domain: settings.domain || '' };
+            case 'xicmp':
+                return { ip: settings.ip || '', id: settings.id ?? 0 };
             case 'mkcp-original':
             case 'mkcp-original':
             case 'header-dtls':
             case 'header-dtls':
             case 'header-srtp':
             case 'header-srtp':
@@ -586,6 +584,12 @@ class UdpMask extends CommonClass {
             case 'header-wechat':
             case 'header-wechat':
             case 'header-wireguard':
             case 'header-wireguard':
                 return {}; // No settings needed
                 return {}; // No settings needed
+            case 'header-custom':
+                return { client: [], server: [] };
+            case 'noise':
+                return { reset: 0, noise: [] };
+            case 'sudoku':
+                return { ascii: '', customTable: '', customTables: [], paddingMin: 0, paddingMax: 0 };
             default:
             default:
                 return settings;
                 return settings;
         }
         }
@@ -782,8 +786,8 @@ class Outbound extends CommonClass {
     }
     }
 
 
     canEnableTls() {
     canEnableTls() {
-        if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol)) return false;
-        if (this.protocol === Protocols.Hysteria) return this.stream.network === 'hysteria';
+        if (this.protocol === Protocols.Hysteria) return true;
+        if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
         return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network);
         return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network);
     }
     }
 
 
@@ -1133,13 +1137,15 @@ Outbound.FreedomSettings = class extends CommonClass {
         domainStrategy = '',
         domainStrategy = '',
         redirect = '',
         redirect = '',
         fragment = {},
         fragment = {},
-        noises = []
+        noises = [],
+        ipsBlocked = [],
     ) {
     ) {
         super();
         super();
         this.domainStrategy = domainStrategy;
         this.domainStrategy = domainStrategy;
         this.redirect = redirect;
         this.redirect = redirect;
-        this.fragment = fragment;
-        this.noises = noises;
+        this.fragment = fragment || {};
+        this.noises = Array.isArray(noises) ? noises : [];
+        this.ipsBlocked = Array.isArray(ipsBlocked) ? ipsBlocked : [];
     }
     }
 
 
     addNoise() {
     addNoise() {
@@ -1154,8 +1160,9 @@ Outbound.FreedomSettings = class extends CommonClass {
         return new Outbound.FreedomSettings(
         return new Outbound.FreedomSettings(
             json.domainStrategy,
             json.domainStrategy,
             json.redirect,
             json.redirect,
-            json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : undefined,
-            json.noises ? json.noises.map(noise => Outbound.FreedomSettings.Noise.fromJson(noise)) : undefined,
+            json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : {},
+            json.noises ? json.noises.map(noise => Outbound.FreedomSettings.Noise.fromJson(noise)) : [],
+            json.ipsBlocked || [],
         );
         );
     }
     }
 
 
@@ -1165,6 +1172,7 @@ Outbound.FreedomSettings = class extends CommonClass {
             redirect: ObjectUtil.isEmpty(this.redirect) ? undefined : this.redirect,
             redirect: ObjectUtil.isEmpty(this.redirect) ? undefined : this.redirect,
             fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
             fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
             noises: this.noises.length === 0 ? undefined : Outbound.FreedomSettings.Noise.toJsonArray(this.noises),
             noises: this.noises.length === 0 ? undefined : Outbound.FreedomSettings.Noise.toJsonArray(this.noises),
+            ipsBlocked: this.ipsBlocked.length === 0 ? undefined : this.ipsBlocked,
         };
         };
     }
     }
 };
 };

+ 11 - 8
web/assets/js/util/index.js

@@ -651,10 +651,13 @@ class CookieManager {
     }
     }
 
 
     static setCookie(cname, cvalue, exdays) {
     static setCookie(cname, cvalue, exdays) {
-        const d = new Date();
-        d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
-        let expires = 'expires=' + d.toUTCString();
-        document.cookie = cname + '=' + encodeURIComponent(cvalue) + ';' + expires + ';path=/';
+        let expires = '';
+        if (exdays) {
+            const d = new Date();
+            d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
+            expires = 'expires=' + d.toUTCString() + ';';
+        }
+        document.cookie = cname + '=' + encodeURIComponent(cvalue) + ';' + expires + 'path=/';
     }
     }
 }
 }
 
 
@@ -813,13 +816,13 @@ class LanguageManager {
                 });
                 });
 
 
                 if (LanguageManager.isSupportLanguage(lang)) {
                 if (LanguageManager.isSupportLanguage(lang)) {
-                    CookieManager.setCookie("lang", lang, 150);
+                    CookieManager.setCookie("lang", lang);
                 } else {
                 } else {
-                    CookieManager.setCookie("lang", "en-US", 150);
+                    CookieManager.setCookie("lang", "en-US");
                     window.location.reload();
                     window.location.reload();
                 }
                 }
             } else {
             } else {
-                CookieManager.setCookie("lang", "en-US", 150);
+                CookieManager.setCookie("lang", "en-US");
                 window.location.reload();
                 window.location.reload();
             }
             }
         }
         }
@@ -832,7 +835,7 @@ class LanguageManager {
             language = "en-US";
             language = "en-US";
         }
         }
 
 
-        CookieManager.setCookie("lang", language, 150);
+        CookieManager.setCookie("lang", language);
         window.location.reload();
         window.location.reload();
     }
     }
 
 

+ 0 - 6
web/controller/index.go

@@ -95,12 +95,6 @@ func (a *IndexController) login(c *gin.Context) {
 	logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, getRemoteIp(c))
 	logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, getRemoteIp(c))
 	a.tgbot.UserLoginNotify(safeUser, ``, getRemoteIp(c), timeStr, 1)
 	a.tgbot.UserLoginNotify(safeUser, ``, getRemoteIp(c), timeStr, 1)
 
 
-	sessionMaxAge, err := a.settingService.GetSessionMaxAge()
-	if err != nil {
-		logger.Warning("Unable to get session's max age from DB")
-	}
-
-	session.SetMaxAge(c, sessionMaxAge*60)
 	session.SetLoginUser(c, user)
 	session.SetLoginUser(c, user)
 	if err := sessions.Default(c).Save(); err != nil {
 	if err := sessions.Default(c).Save(); err != nil {
 		logger.Warning("Unable to save session: ", err)
 		logger.Warning("Unable to save session: ", err)

+ 2 - 2
web/html/component/aClientTable.html

@@ -30,7 +30,7 @@
   </a-tooltip>
   </a-tooltip>
 </template>
 </template>
 <template slot="enable" slot-scope="text, client, index">
 <template slot="enable" slot-scope="text, client, index">
-  <a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
+  <a-switch v-model="client.enable" @change="switchEnableClient(record.id, client, $event)"></a-switch>
 </template>
 </template>
 <template slot="online" slot-scope="text, client, index">
 <template slot="online" slot-scope="text, client, index">
   <a-popover :overlay-class-name="themeSwitcher.currentTheme">
   <a-popover :overlay-class-name="themeSwitcher.currentTheme">
@@ -165,7 +165,7 @@
         <span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span>
         <span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span>
       </a-menu-item>
       </a-menu-item>
       <a-menu-item>
       <a-menu-item>
-        <a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id,client)"></a-switch>
+        <a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id, client, $event)"></a-switch>
         {{ i18n "enable"}}
         {{ i18n "enable"}}
       </a-menu-item>
       </a-menu-item>
     </a-menu>
     </a-menu>

+ 245 - 161
web/html/form/client.html

@@ -1,172 +1,256 @@
 {{define "form/client"}}
 {{define "form/client"}}
-<a-form layout="horizontal" v-if="client" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-    <a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
-        <a-switch v-model="client.enable"></a-switch>
-    </a-form-item>
-    <a-form-item>
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
-                </template>
-                {{ i18n "pages.inbounds.email" }}
-                <a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
-            </a-tooltip>
+<a-form
+  layout="horizontal"
+  v-if="client"
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
+  <a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
+    <a-switch v-model="client.enable"></a-switch>
+  </a-form-item>
+  <a-form-item>
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
         </template>
         </template>
-        <a-input v-model.trim="client.email"></a-input>
-    </a-form-item>
-    <a-form-item v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    <span>{{ i18n "reset" }}</span>
-                </template>
-                {{ i18n "password" }}
-                <a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
-                <a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
-            </a-tooltip>
+        {{ i18n "pages.inbounds.email" }}
+        <a-icon
+          type="sync"
+          @click="client.email = RandomUtil.randomLowerAndNum(9)"
+        ></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input v-model.trim="client.email"></a-input>
+  </a-form-item>
+  <a-form-item
+    v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS"
+  >
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "reset" }}</span>
         </template>
         </template>
-        <a-input v-model.trim="client.password"></a-input>
-    </a-form-item>
-    <a-form-item v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    <span>{{ i18n "reset" }}</span>
-                </template>
-                ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon>
-            </a-tooltip>
+        {{ i18n "password" }}
+        <a-icon
+          v-if="inbound.protocol === Protocols.SHADOWSOCKS"
+          @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)"
+          type="sync"
+        ></a-icon>
+        <a-icon
+          v-if="inbound.protocol === Protocols.TROJAN"
+          @click="client.password = RandomUtil.randomSeq(10)"
+          type="sync"
+        >
+        </a-icon>
+      </a-tooltip>
+    </template>
+    <a-input v-model.trim="client.password"></a-input>
+  </a-form-item>
+  <a-form-item v-if="inbound.protocol === Protocols.HYSTERIA">
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "reset" }}</span>
         </template>
         </template>
-        <a-input v-model.trim="client.id"></a-input>
-    </a-form-item>
-    <a-form-item v-if="inbound.protocol === Protocols.VMESS" label='{{ i18n "security" }}'>
-        <a-select v-model="client.security" :dropdown-class-name="themeSwitcher.currentTheme">
-            <a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
-        </a-select>
-    </a-form-item>
-    <a-form-item v-if="client.email && app.subSettings?.enable">
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
-                </template>
-                Subscription
-                <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
-            </a-tooltip>
+        Auth Password
+        <a-icon
+          @click="client.auth = RandomUtil.randomSeq(10)"
+          type="sync"
+        ></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input v-model.trim="client.auth"></a-input>
+  </a-form-item>
+  <a-form-item
+    v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS"
+  >
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "reset" }}</span>
         </template>
         </template>
-        <a-input v-model.trim="client.subId"></a-input>
-    </a-form-item>
-    <a-form-item v-if="client.email && app.tgBotEnable">
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    <span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
-                </template>
-                Telegram ChatID
-                <a-icon type="question-circle"></a-icon>
-            </a-tooltip>
+        ID
+        <a-icon
+          @click="client.id = RandomUtil.randomUUID()"
+          type="sync"
+        ></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input v-model.trim="client.id"></a-input>
+  </a-form-item>
+  <a-form-item
+    v-if="inbound.protocol === Protocols.VMESS"
+    label='{{ i18n "security" }}'
+  >
+    <a-select
+      v-model="client.security"
+      :dropdown-class-name="themeSwitcher.currentTheme"
+    >
+      <a-select-option v-for="key in USERS_SECURITY" :value="key"
+        >[[ key ]]</a-select-option
+      >
+    </a-select>
+  </a-form-item>
+  <a-form-item v-if="client.email && app.subSettings?.enable">
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
         </template>
         </template>
-        <a-input-number :style="{ width: '50%' }" v-model.number="client.tgId" min="0"></a-input-number>
-    </a-form-item>
-    <a-form-item v-if="client.email" label='{{ i18n "comment" }}'>
-        <a-input v-model.trim="client.comment"></a-input>
-    </a-form-item>
-    <a-form-item v-if="app.ipLimitEnable">
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    <span>{{ i18n "pages.inbounds.IPLimitDesc"}}</span>
-                </template>
-                    <span>{{ i18n "pages.inbounds.IPLimit"}} </span>
-                <a-icon type="question-circle"></a-icon>
-            </a-tooltip>
+        Subscription
+        <a-icon
+          @click="client.subId = RandomUtil.randomLowerAndNum(16)"
+          type="sync"
+        ></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input v-model.trim="client.subId"></a-input>
+  </a-form-item>
+  <a-form-item v-if="client.email && app.tgBotEnable">
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
         </template>
         </template>
-        <a-input-number v-model.number="client.limitIp" min="0"></a-input-number>
-    </a-form-item>
-    <a-form-item v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit">
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    <span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
-                </template>
-                    <span>{{ i18n "pages.inbounds.IPLimitlog" }} </span>
-                <a-icon type="question-circle"></a-icon>
-            </a-tooltip>
+        Telegram ChatID
+        <a-icon type="question-circle"></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input-number
+      :style="{ width: '50%' }"
+      v-model.number="client.tgId"
+      min="0"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item v-if="client.email" label='{{ i18n "comment" }}'>
+    <a-input v-model.trim="client.comment"></a-input>
+  </a-form-item>
+  <a-form-item v-if="app.ipLimitEnable">
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "pages.inbounds.IPLimitDesc"}}</span>
         </template>
         </template>
-        <a-tooltip>
-            <template slot="title">
-                <span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
-            </template>
-            <span :style="{ color: '#FF4D4F' }">
-                <a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
-            </span>
-        </a-tooltip>
-        <a-form layout="block">
-            <a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email)" placeholder="Click To Get IPs"
-                :auto-size="{ minRows: 5, maxRows: 10 }">
-            </a-textarea>
-        </a-form>
-    </a-form-item>
-    <a-form-item v-if="inbound.canEnableTlsFlow()" label='Flow'>
-        <a-select v-model="client.flow" :dropdown-class-name="themeSwitcher.currentTheme">
-            <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
-            <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
-        </a-select>
-    </a-form-item>
-    <a-form-item>
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
-                </template>
-                {{ i18n "pages.inbounds.totalFlow" }}
-                <a-icon type="question-circle"></a-icon>
-            </a-tooltip>
+        <span>{{ i18n "pages.inbounds.IPLimit"}} </span>
+        <a-icon type="question-circle"></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input-number v-model.number="client.limitIp" min="0"></a-input-number>
+  </a-form-item>
+  <a-form-item
+    v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit"
+  >
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
         </template>
         </template>
-        <a-input-number v-model.number="client._totalGB" :min="0"></a-input-number>
-    </a-form-item>
-    <a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'>
-        <a-tag :color="ColorUtils.clientUsageColor(clientStats, app.trafficDiff)">
-            [[ SizeFormatter.sizeFormat(clientStats.up) ]] /
-            [[ SizeFormatter.sizeFormat(clientStats.down) ]]
-            ([[ SizeFormatter.sizeFormat(clientStats.up + clientStats.down) ]])
-        </a-tag>
-        <a-tooltip>
-            <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
-            <a-icon type="retweet"
-                @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
-                v-if="client.email.length > 0"></a-icon>
-        </a-tooltip>
-    </a-form-item>
-    <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
-        <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
-    </a-form-item>
-    <a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
-        <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
-    </a-form-item>
-    <a-form-item v-else>
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</template>
-                {{ i18n "pages.inbounds.expireDate" }}
-                <a-icon type="question-circle"></a-icon>
-            </a-tooltip>
+        <span>{{ i18n "pages.inbounds.IPLimitlog" }} </span>
+        <a-icon type="question-circle"></a-icon>
+      </a-tooltip>
+    </template>
+    <a-tooltip>
+      <template slot="title">
+        <span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
+      </template>
+      <span :style="{ color: '#FF4D4F' }">
+        <a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
+      </span>
+    </a-tooltip>
+    <a-form layout="block">
+      <a-textarea
+        id="clientIPs"
+        readonly
+        @click="getDBClientIps(client.email)"
+        placeholder="Click To Get IPs"
+        :auto-size="{ minRows: 5, maxRows: 10 }"
+      >
+      </a-textarea>
+    </a-form>
+  </a-form-item>
+  <a-form-item v-if="inbound.canEnableTlsFlow()" label="Flow">
+    <a-select
+      v-model="client.flow"
+      :dropdown-class-name="themeSwitcher.currentTheme"
+    >
+      <a-select-option value selected>{{ i18n "none" }}</a-select-option>
+      <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key"
+        >[[ key ]]</a-select-option
+      >
+    </a-select>
+  </a-form-item>
+  <a-form-item>
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
         </template>
         </template>
-        <a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
-            :dropdown-class-name="themeSwitcher.currentTheme" v-model="client._expiryTime"></a-date-picker>
-        <a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
-                            value="client._expiryTime" v-model="client._expiryTime"></a-persian-datepicker>
-        <a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
-    </a-form-item>
-    <a-form-item v-if="client.expiryTime != 0">
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">{{ i18n "pages.client.renewDesc" }}</template>
-                {{ i18n "pages.client.renew" }}
-                <a-icon type="question-circle"></a-icon>
-            </a-tooltip>
-        </template>
-        <a-input-number v-model.number="client.reset" :min="0"></a-input-number>
-    </a-form-item>
+        {{ i18n "pages.inbounds.totalFlow" }}
+        <a-icon type="question-circle"></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input-number v-model.number="client._totalGB" :min="0"></a-input-number>
+  </a-form-item>
+  <a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'>
+    <a-tag :color="ColorUtils.clientUsageColor(clientStats, app.trafficDiff)">
+      [[ SizeFormatter.sizeFormat(clientStats.up) ]] / [[
+      SizeFormatter.sizeFormat(clientStats.down) ]] ([[
+      SizeFormatter.sizeFormat(clientStats.up + clientStats.down) ]])
+    </a-tag>
+    <a-tooltip>
+      <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
+      <a-icon
+        type="retweet"
+        @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
+        v-if="client.email.length > 0"
+      ></a-icon>
+    </a-tooltip>
+  </a-form-item>
+  <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
+    <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
+  </a-form-item>
+  <a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
+    <a-input-number
+      v-model.number="delayedExpireDays"
+      :min="0"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item v-else>
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title"
+          >{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</template
+        >
+        {{ i18n "pages.inbounds.expireDate" }}
+        <a-icon type="question-circle"></a-icon>
+      </a-tooltip>
+    </template>
+    <a-date-picker
+      v-if="datepicker == 'gregorian'"
+      :show-time="{ format: 'HH:mm:ss' }"
+      format="YYYY-MM-DD HH:mm:ss"
+      :dropdown-class-name="themeSwitcher.currentTheme"
+      v-model="client._expiryTime"
+    ></a-date-picker>
+    <a-persian-datepicker
+      v-else
+      placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
+      value="client._expiryTime"
+      v-model="client._expiryTime"
+    ></a-persian-datepicker>
+    <a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
+  </a-form-item>
+  <a-form-item v-if="client.expiryTime != 0">
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">{{ i18n "pages.client.renewDesc" }}</template>
+        {{ i18n "pages.client.renew" }}
+        <a-icon type="question-circle"></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input-number v-model.number="client.reset" :min="0"></a-input-number>
+  </a-form-item>
 </a-form>
 </a-form>
-{{end}}
+{{end}}

+ 17 - 21
web/html/form/inbound.html

@@ -1,7 +1,6 @@
 {{define "form/inbound"}}
 {{define "form/inbound"}}
 <!-- base -->
 <!-- base -->
-<a-form :colon="false" :label-col="{ md: {span:8} }"
-    :wrapper-col="{ md: {span:14} }">
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
     <a-form-item label='{{ i18n "enable" }}'>
     <a-form-item label='{{ i18n "enable" }}'>
         <a-switch v-model="dbInbound.enable"></a-switch>
         <a-switch v-model="dbInbound.enable"></a-switch>
     </a-form-item>
     </a-form-item>
@@ -10,8 +9,7 @@
     </a-form-item>
     </a-form-item>
 
 
     <a-form-item label='{{ i18n "protocol" }}'>
     <a-form-item label='{{ i18n "protocol" }}'>
-        <a-select v-model="inbound.protocol" :disabled="isEdit"
-            :dropdown-class-name="themeSwitcher.currentTheme">
+        <a-select v-model="inbound.protocol" :disabled="isEdit" :dropdown-class-name="themeSwitcher.currentTheme">
             <a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p
             <a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p
                 ]]</a-select-option>
                 ]]</a-select-option>
         </a-select>
         </a-select>
@@ -31,8 +29,7 @@
     </a-form-item>
     </a-form-item>
 
 
     <a-form-item label='{{ i18n "pages.inbounds.port" }}'>
     <a-form-item label='{{ i18n "pages.inbounds.port" }}'>
-        <a-input-number v-model.number="inbound.port" :min="1"
-            :max="65535"></a-input-number>
+        <a-input-number v-model.number="inbound.port" :min="1" :max="65535"></a-input-number>
     </a-form-item>
     </a-form-item>
 
 
     <a-form-item>
     <a-form-item>
@@ -45,8 +42,7 @@
                 <a-icon type="question-circle"></a-icon>
                 <a-icon type="question-circle"></a-icon>
             </a-tooltip>
             </a-tooltip>
         </template>
         </template>
-        <a-input-number v-model.number="dbInbound.totalGB"
-            :min="0"></a-input-number>
+        <a-input-number v-model.number="dbInbound.totalGB" :min="0"></a-input-number>
     </a-form-item>
     </a-form-item>
 
 
     <a-form-item>
     <a-form-item>
@@ -55,10 +51,8 @@
                 <template slot="title">
                 <template slot="title">
                     <span>{{ i18n "pages.inbounds.periodicTrafficResetDesc"
                     <span>{{ i18n "pages.inbounds.periodicTrafficResetDesc"
                         }}</span>
                         }}</span>
-                    <br
-                        v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
-                    <span
-                        v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
+                    <br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
+                    <span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
                         <strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
                         <strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
                         <span>[[
                         <span>[[
                             IntlUtil.formatDate(dbInbound.lastTrafficResetTime)
                             IntlUtil.formatDate(dbInbound.lastTrafficResetTime)
@@ -69,12 +63,12 @@
                 <a-icon type="question-circle"></a-icon>
                 <a-icon type="question-circle"></a-icon>
             </a-tooltip>
             </a-tooltip>
         </template>
         </template>
-        <a-select v-model="dbInbound.trafficReset"
-            :dropdown-class-name="themeSwitcher.currentTheme">
+        <a-select v-model="dbInbound.trafficReset" :dropdown-class-name="themeSwitcher.currentTheme">
             <a-select-option value="never">{{ i18n
             <a-select-option value="never">{{ i18n
                 "pages.inbounds.periodicTrafficReset.never" }}</a-select-option>
                 "pages.inbounds.periodicTrafficReset.never" }}</a-select-option>
             <a-select-option value="hourly">{{ i18n
             <a-select-option value="hourly">{{ i18n
-                "pages.inbounds.periodicTrafficReset.hourly" }}</a-select-option>
+                "pages.inbounds.periodicTrafficReset.hourly"
+                }}</a-select-option>
             <a-select-option value="daily">{{ i18n
             <a-select-option value="daily">{{ i18n
                 "pages.inbounds.periodicTrafficReset.daily" }}</a-select-option>
                 "pages.inbounds.periodicTrafficReset.daily" }}</a-select-option>
             <a-select-option value="weekly">{{ i18n
             <a-select-option value="weekly">{{ i18n
@@ -97,13 +91,10 @@
                 <a-icon type="question-circle"></a-icon>
                 <a-icon type="question-circle"></a-icon>
             </a-tooltip>
             </a-tooltip>
         </template>
         </template>
-        <a-date-picker :style="{ width: '100%' }"
-            v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
-            format="YYYY-MM-DD HH:mm:ss"
-            :dropdown-class-name="themeSwitcher.currentTheme"
+        <a-date-picker :style="{ width: '100%' }" v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
+            format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
             v-model="dbInbound._expiryTime"></a-date-picker>
             v-model="dbInbound._expiryTime"></a-date-picker>
-        <a-persian-datepicker v-else
-            placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
+        <a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
             value="dbInbound._expiryTime" v-model="dbInbound._expiryTime">
             value="dbInbound._expiryTime" v-model="dbInbound._expiryTime">
         </a-persian-datepicker>
         </a-persian-datepicker>
     </a-form-item>
     </a-form-item>
@@ -154,6 +145,11 @@
     {{template "form/tun"}}
     {{template "form/tun"}}
 </template>
 </template>
 
 
+<!-- hysteria -->
+<template v-if="inbound.protocol === Protocols.HYSTERIA">
+    {{template "form/hysteria"}}
+</template>
+
 <!-- stream settings -->
 <!-- stream settings -->
 <template v-if="inbound.canEnableStream()">
 <template v-if="inbound.canEnableStream()">
     {{template "form/streamSettings"}}
     {{template "form/streamSettings"}}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 711 - 295
web/html/form/outbound.html


+ 32 - 0
web/html/form/protocol/hysteria.html

@@ -0,0 +1,32 @@
+{{define "form/hysteria"}}
+<a-collapse activeKey="0"
+    v-for="(client, index) in inbound.settings.hysterias.slice(0,1)"
+    v-if="!isEdit">
+    <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
+        {{template "form/client"}}
+    </a-collapse-panel>
+</a-collapse>
+<a-collapse v-else>
+    <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' +
+        inbound.settings.hysterias.length">
+        <table width="100%">
+            <tr class="client-table-header">
+                <th>{{ i18n "pages.inbounds.email" }}</th>
+                <th>Auth</th>
+            </tr>
+            <tr v-for="(client, index) in inbound.settings.hysterias"
+                :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
+                <td>[[ client.email ]]</td>
+                <td>[[ client.auth ]]</td>
+            </tr>
+        </table>
+    </a-collapse-panel>
+</a-collapse>
+<a-form :colon="false" :label-col="{ md: {span:8} }"
+    :wrapper-col="{ md: {span:14} }">
+    <a-form-item :label="'{{ i18n "pages.inbounds.stream.tcp.version" }}'">
+        <a-input-number v-model.number="inbound.settings.version" :min="2"
+            :max="2" disabled></a-input-number>
+    </a-form-item>
+</a-form>
+{{end}}

+ 70 - 44
web/html/form/protocol/shadowsocks.html

@@ -1,50 +1,76 @@
 {{define "form/shadowsocks"}}
 {{define "form/shadowsocks"}}
 <template v-if="inbound.isSSMultiUser">
 <template v-if="inbound.isSSMultiUser">
-    <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">  
-        <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
-            {{template "form/client"}}
-        </a-collapse-panel>
-    </a-collapse>
-    <a-collapse v-else>
-        <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.shadowsockses.length">
-            <table width="100%">
-                <tr class="client-table-header">
-                    <th>{{ i18n "pages.inbounds.email" }}</th>
-                    <th>Password</th>
-                </tr>
-                <tr v-for="(client, index) in inbound.settings.shadowsockses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
-                    <td>[[ client.email ]]</td>
-                    <td>[[ client.password ]]</td>
-                </tr>
-            </table>
-        </a-collapse-panel>
-    </a-collapse>
+  <a-collapse
+    activeKey="0"
+    v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)"
+    v-if="!isEdit"
+  >
+    <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
+      {{template "form/client"}}
+    </a-collapse-panel>
+  </a-collapse>
+  <a-collapse v-else>
+    <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.shadowsockses.length">
+      <table width="100%">
+        <tr class="client-table-header">
+          <th>{{ i18n "pages.inbounds.email" }}</th>
+          <th>Password</th>
+        </tr>
+        <tr
+          v-for="(client, index) in inbound.settings.shadowsockses"
+          :class="index % 2 == 1 ? ' client-table-odd-row' : ''"
+        >
+          <td>[[ client.email ]]</td>
+          <td>[[ client.password ]]</td>
+        </tr>
+      </table>
+    </a-collapse-panel>
+  </a-collapse>
 </template>
 </template>
-<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-    <a-form-item label='{{ i18n "encryption" }}'>
-        <a-select v-model="inbound.settings.method" @change="SSMethodChange" :dropdown-class-name="themeSwitcher.currentTheme">
-            <a-select-option v-for="(method,method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
-        </a-select>
-    </a-form-item>
-    <a-form-item v-if="inbound.isSS2022">
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    <span>{{ i18n "reset" }}</span>
-                </template> Password <a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
-            </a-tooltip>
+<a-form
+  :colon=" false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
+  <a-form-item label='{{ i18n "encryption" }}'>
+    <a-select
+      v-model="inbound.settings.method"
+      @change="SSMethodChange"
+      :dropdown-class-name="themeSwitcher.currentTheme"
+    >
+      <a-select-option v-for="(method,method_name) in SSMethods" :value="method"
+        >[[ method_name ]]</a-select-option
+      >
+    </a-select>
+  </a-form-item>
+  <a-form-item v-if="inbound.isSS2022">
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "reset" }}</span>
         </template>
         </template>
-        <a-input v-model.trim="inbound.settings.password"></a-input>
-    </a-form-item>
-    <a-form-item label='{{ i18n "pages.inbounds.network" }}'>
-        <a-select v-model="inbound.settings.network" :style="{ width: '100px' }" :dropdown-class-name="themeSwitcher.currentTheme">
-            <a-select-option value="tcp,udp">TCP,UDP</a-select-option>
-            <a-select-option value="tcp">TCP</a-select-option>
-            <a-select-option value="udp">UDP</a-select-option>
-        </a-select>
-    </a-form-item>
-    <a-form-item label='ivCheck'>
-        <a-switch v-model="inbound.settings.ivCheck"></a-switch>
-    </a-form-item>
+        Password
+        <a-icon
+          @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)"
+          type="sync"
+        ></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input v-model.trim="inbound.settings.password"></a-input>
+  </a-form-item>
+  <a-form-item label='{{ i18n "pages.inbounds.network" }}'>
+    <a-select
+      v-model="inbound.settings.network"
+      :style="{ width: '100px' }"
+      :dropdown-class-name="themeSwitcher.currentTheme"
+    >
+      <a-select-option value="tcp,udp">TCP,UDP</a-select-option>
+      <a-select-option value="tcp">TCP</a-select-option>
+      <a-select-option value="udp">UDP</a-select-option>
+    </a-select>
+  </a-form-item>
+  <a-form-item label="ivCheck">
+    <a-switch v-model="inbound.settings.ivCheck"></a-switch>
+  </a-form-item>
 </a-form>
 </a-form>
 {{end}}
 {{end}}

+ 37 - 8
web/html/form/protocol/socks.html

@@ -1,5 +1,9 @@
 {{define "form/mixed"}}
 {{define "form/mixed"}}
-<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
   <a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
   <a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
     <a-switch v-model="inbound.settings.udp"></a-switch>
     <a-switch v-model="inbound.settings.udp"></a-switch>
   </a-form-item>
   </a-form-item>
@@ -7,7 +11,10 @@
     <a-input v-model.trim="inbound.settings.ip"></a-input>
     <a-input v-model.trim="inbound.settings.ip"></a-input>
   </a-form-item>
   </a-form-item>
   <a-form-item label='{{ i18n "password" }}'>
   <a-form-item label='{{ i18n "password" }}'>
-    <a-switch :checked="inbound.settings.auth === 'password'" @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
+    <a-switch
+      :checked="inbound.settings.auth === 'password'"
+      @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"
+    ></a-switch>
   </a-form-item>
   </a-form-item>
   <template v-if="inbound.settings.auth === 'password'">
   <template v-if="inbound.settings.auth === 'password'">
     <table :style="{ width: '100%', textAlign: 'center', margin: '1rem 0' }">
     <table :style="{ width: '100%', textAlign: 'center', margin: '1rem 0' }">
@@ -15,17 +22,39 @@
         <td width="45%">{{ i18n "username" }}</td>
         <td width="45%">{{ i18n "username" }}</td>
         <td width="45%">{{ i18n "password" }}</td>
         <td width="45%">{{ i18n "password" }}</td>
         <td>
         <td>
-          <a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"></a-button>
+          <a-button
+            icon="plus"
+            size="small"
+            @click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"
+          ></a-button>
         </td>
         </td>
       </tr>
       </tr>
     </table>
     </table>
-    <a-input-group compact v-for="(account, index) in inbound.settings.accounts" :style="{ marginBottom: '10px' }">
-      <a-input :style="{ width: '50%' }" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
-        <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
+    <a-input-group
+      compact
+      v-for="(account, index) in inbound.settings.accounts"
+      :style="{ marginBottom: '10px' }"
+    >
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="account.user"
+        placeholder='{{ i18n "username" }}'
+      >
+        <template slot="addonBefore" :style="{ margin: '0' }"
+          >[[ index+1 ]]</template
+        >
       </a-input>
       </a-input>
-      <a-input :style="{ width: '50%' }" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="account.pass"
+        placeholder='{{ i18n "password" }}'
+      >
         <template slot="addonAfter">
         <template slot="addonAfter">
-          <a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button>
+          <a-button
+            icon="minus"
+            size="small"
+            @click="inbound.settings.delAccount(index)"
+          ></a-button>
         </template>
         </template>
       </a-input>
       </a-input>
     </a-input-group>
     </a-input-group>

+ 33 - 30
web/html/form/protocol/trojan.html

@@ -11,40 +11,43 @@
         <th>{{ i18n "pages.inbounds.email" }}</th>
         <th>{{ i18n "pages.inbounds.email" }}</th>
         <th>Password</th>
         <th>Password</th>
       </tr>
       </tr>
-      <tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
+      <tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? ' client-table-odd-row' : ''">
         <td>[[ client.email ]]</td>
         <td>[[ client.email ]]</td>
         <td>[[ client.password ]]</td>
         <td>[[ client.password ]]</td>
       </tr>
       </tr>
     </table>
     </table>
   </a-collapse-panel>
   </a-collapse-panel>
 </a-collapse>
 </a-collapse>
-<template v-if="inbound.isTcp">
-  <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-    <a-form-item label="Fallbacks">
-      <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
-    </a-form-item>
-  </a-form>
+<template v-if=" inbound.isTcp">
+    <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+      <a-form-item label="Fallbacks">
+        <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
+      </a-form-item>
+    </a-form>
 
 
-  <!-- trojan fallbacks -->
-  <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-    <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delFallback(index)" :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
-    </a-divider>
-    <a-form-item label='SNI'>
-      <a-input v-model="fallback.name"></a-input>
-    </a-form-item>
-    <a-form-item label='ALPN'>
-      <a-input v-model="fallback.alpn"></a-input>
-    </a-form-item>
-    <a-form-item label='Path'>
-      <a-input v-model="fallback.path"></a-input>
-    </a-form-item>
-    <a-form-item label='Dest'>
-      <a-input v-model="fallback.dest"></a-input>
-    </a-form-item>
-    <a-form-item label='xVer'>
-      <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
-    </a-form-item>
-  </a-form>
-  <a-divider style="margin:5px 0;"></a-divider>
-</template>
-{{end}}
+    <!-- trojan fallbacks -->
+    <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
+      :wrapper-col="{ md: {span:14} }">
+      <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
+          @click="() => inbound.settings.delFallback(index)"
+          :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
+      </a-divider>
+      <a-form-item label='SNI'>
+        <a-input v-model="fallback.name"></a-input>
+      </a-form-item>
+      <a-form-item label='ALPN'>
+        <a-input v-model="fallback.alpn"></a-input>
+      </a-form-item>
+      <a-form-item label='Path'>
+        <a-input v-model="fallback.path"></a-input>
+      </a-form-item>
+      <a-form-item label='Dest'>
+        <a-input v-model="fallback.dest"></a-input>
+      </a-form-item>
+      <a-form-item label='xVer'>
+        <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
+      </a-form-item>
+    </a-form>
+    <a-divider style="margin:5px 0;"></a-divider>
+    </template>
+    {{end}}

+ 88 - 38
web/html/form/protocol/tun.html

@@ -1,44 +1,94 @@
 {{define "form/tun"}}
 {{define "form/tun"}}
-<a-form :colon="false" :label-col="{ md: {span:8} }"
-    :wrapper-col="{ md: {span:14} }">
-    <a-form-item>
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    <span>{{ i18n "pages.xray.tun.nameDesc" }}</span>
-                </template>
-                Interface Name
-                <a-icon type="question-circle"></a-icon>
-            </a-tooltip>
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
+  <a-form-item>
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "pages.xray.tun.nameDesc" }}</span>
         </template>
         </template>
-        <a-input v-model.trim="inbound.settings.name"
-            placeholder="xray0"></a-input>
-    </a-form-item>
-    <a-form-item>
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    <span>{{ i18n "pages.xray.tun.mtuDesc" }}</span>
-                </template>
-                MTU
-                <a-icon type="question-circle"></a-icon>
-            </a-tooltip>
+        Interface Name
+        <a-icon type="question-circle"></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input v-model.trim="inbound.settings.name" placeholder="xray0"></a-input>
+  </a-form-item>
+  <a-form-item>
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "pages.xray.tun.mtuDesc" }}</span>
         </template>
         </template>
-        <a-input-number v-model.number="inbound.settings.mtu" :min="1"
-            :max="9000" placeholder="1500"></a-input-number>
-    </a-form-item>
-    <a-form-item>
-        <template slot="label">
-            <a-tooltip>
-                <template slot="title">
-                    <span>{{ i18n "pages.xray.tun.userLevelDesc" }}</span>
-                </template>
-                {{ i18n "pages.xray.tun.userLevel" }}
-                <a-icon type="question-circle"></a-icon>
-            </a-tooltip>
+        MTU IPv4
+        <a-icon type="question-circle"></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input-number
+      v-model.number="inbound.settings.mtu[0]"
+      :min="1"
+      :max="9000"
+      placeholder="1500"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item label="MTU IPv6">
+    <a-input-number
+      v-model.number="inbound.settings.mtu[1]"
+      :min="1"
+      :max="9000"
+      placeholder="1280"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item label="Gateway">
+    <a-select
+      mode="tags"
+      v-model="inbound.settings.gateway"
+      :style="{ width: '100%' }"
+      :token-separators="[',']"
+      placeholder="IPv4/IPv6 gateway"
+    ></a-select>
+  </a-form-item>
+  <a-form-item label="DNS">
+    <a-select
+      mode="tags"
+      v-model="inbound.settings.dns"
+      :style="{ width: '100%' }"
+      :token-separators="[',']"
+      placeholder="DNS servers"
+    ></a-select>
+  </a-form-item>
+  <a-form-item>
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "pages.xray.tun.userLevelDesc" }}</span>
         </template>
         </template>
-        <a-input-number v-model.number="inbound.settings.userLevel" :min="0"
-            placeholder="0"></a-input-number>
-    </a-form-item>
+        {{ i18n "pages.xray.tun.userLevel" }}
+        <a-icon type="question-circle"></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input-number
+      v-model.number="inbound.settings.userLevel"
+      :min="0"
+      placeholder="0"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item label="Auto Routing Table">
+    <a-select
+      mode="tags"
+      v-model="inbound.settings.autoSystemRoutingTable"
+      :style="{ width: '100%' }"
+      :token-separators="[',']"
+      placeholder="e.g. vpn, proxy"
+    ></a-select>
+  </a-form-item>
+  <a-form-item label="Auto Outbounds">
+    <a-input
+      v-model.trim="inbound.settings.autoOutboundsInterface"
+      placeholder="auto"
+    ></a-input>
+  </a-form-item>
 </a-form>
 </a-form>
 {{end}}
 {{end}}

+ 1 - 1
web/html/form/protocol/vless.html

@@ -120,4 +120,4 @@
       </a-form>
       </a-form>
       <a-divider :style="{ margin: '5px 0' }"></a-divider>
       <a-divider :style="{ margin: '5px 0' }"></a-divider>
     </template>
     </template>
-{{end}}
+    {{end}}

+ 4 - 3
web/html/form/protocol/vmess.html

@@ -1,5 +1,5 @@
 {{define "form/vmess"}}
 {{define "form/vmess"}}
-<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">    
+<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
     <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
     <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
         {{template "form/client"}}
         {{template "form/client"}}
     </a-collapse-panel>
     </a-collapse-panel>
@@ -12,7 +12,8 @@
                 <th>ID</th>
                 <th>ID</th>
                 <th>{{ i18n "security" }}</th>
                 <th>{{ i18n "security" }}</th>
             </tr>
             </tr>
-            <tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
+            <tr v-for="(client, index) in inbound.settings.vmesses" 
+            :class="index % 2 == 1 ? ' client-table-odd-row' : ''">
                 <td>[[ client.email ]]</td>
                 <td>[[ client.email ]]</td>
                 <td>[[ client.id ]]</td>
                 <td>[[ client.id ]]</td>
                 <td>[[ client.security ]]</td>
                 <td>[[ client.security ]]</td>
@@ -20,4 +21,4 @@
         </table>
         </table>
     </a-collapse-panel>
     </a-collapse-panel>
 </a-collapse>
 </a-collapse>
-{{end}}
+{{end}}

+ 11 - 5
web/html/form/protocol/wireguard.html

@@ -7,7 +7,8 @@
           <span>{{ i18n "reset" }}</span>
           <span>{{ i18n "reset" }}</span>
         </template>
         </template>
         {{ i18n "pages.xray.wireguard.secretKey" }}
         {{ i18n "pages.xray.wireguard.secretKey" }}
-        <a-icon type="sync" @click="[inbound.settings.pubKey, inbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())"></a-icon>
+        <a-icon type="sync"
+          @click="[inbound.settings.pubKey, inbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())"></a-icon>
       </a-tooltip>
       </a-tooltip>
     </template>
     </template>
     <a-input v-model.trim="inbound.settings.secretKey"></a-input>
     <a-input v-model.trim="inbound.settings.secretKey"></a-input>
@@ -24,8 +25,11 @@
   <a-form-item label="Peers">
   <a-form-item label="Peers">
     <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addPeer()"></a-button>
     <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addPeer()"></a-button>
   </a-form-item>
   </a-form-item>
-  <a-form v-for="(peer, index) in inbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-    <a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon v-if="inbound.settings.peers.length>1" type="delete" @click="() => inbound.settings.delPeer(index)" :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
+  <a-form v-for="(peer, index) in inbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }"
+    :wrapper-col="{ md: {span:14} }">
+    <a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon v-if="inbound.settings.peers.length>1"
+        type="delete" @click="() => inbound.settings.delPeer(index)"
+        :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
     </a-divider>
     </a-divider>
     <a-form-item>
     <a-form-item>
       <template slot="label">
       <template slot="label">
@@ -34,7 +38,8 @@
             <span>{{ i18n "reset" }}</span>
             <span>{{ i18n "reset" }}</span>
           </template>
           </template>
           {{ i18n "pages.xray.wireguard.secretKey" }}
           {{ i18n "pages.xray.wireguard.secretKey" }}
-          <a-icon @click="[peer.publicKey, peer.privateKey] = Object.values(Wireguard.generateKeypair())" type="sync"></a-icon>
+          <a-icon @click="[peer.publicKey, peer.privateKey] = Object.values(Wireguard.generateKeypair())"
+            type="sync"></a-icon>
         </a-tooltip>
         </a-tooltip>
       </template>
       </template>
       <a-input v-model.trim="peer.privateKey"></a-input>
       <a-input v-model.trim="peer.privateKey"></a-input>
@@ -64,7 +69,8 @@
       </template>
       </template>
       <template v-for="(aip, index) in peer.allowedIPs" :style="{ marginBottom: '10px' }">
       <template v-for="(aip, index) in peer.allowedIPs" :style="{ marginBottom: '10px' }">
         <a-input v-model.trim="peer.allowedIPs[index]">
         <a-input v-model.trim="peer.allowedIPs[index]">
-          <a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)"></a-button>
+          <a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small"
+            @click="peer.allowedIPs.splice(index, 1)"></a-button>
         </a-input>
         </a-input>
       </template>
       </template>
     </a-form-item>
     </a-form-item>

+ 2 - 4
web/html/form/reality_settings.html

@@ -17,8 +17,7 @@
             <a-tooltip>
             <a-tooltip>
                 <template slot="title">
                 <template slot="title">
                     <span>{{ i18n "reset" }}</span>
                     <span>{{ i18n "reset" }}</span>
-                </template> Target <a-icon @click="randomizeRealityTarget()"
-                    type="sync"></a-icon>
+                </template> Target <a-icon @click="randomizeRealityTarget()" type="sync"></a-icon>
             </a-tooltip>
             </a-tooltip>
         </template>
         </template>
         <a-input v-model.trim="inbound.stream.reality.target"></a-input>
         <a-input v-model.trim="inbound.stream.reality.target"></a-input>
@@ -28,8 +27,7 @@
             <a-tooltip>
             <a-tooltip>
                 <template slot="title">
                 <template slot="title">
                     <span>{{ i18n "reset" }}</span>
                     <span>{{ i18n "reset" }}</span>
-                </template> SNI <a-icon @click="randomizeRealityTarget()"
-                    type="sync"></a-icon>
+                </template> SNI <a-icon @click="randomizeRealityTarget()" type="sync"></a-icon>
             </a-tooltip>
             </a-tooltip>
         </template>
         </template>
         <a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
         <a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>

+ 48 - 12
web/html/form/stream/external_proxy.html

@@ -1,31 +1,67 @@
 {{define "form/externalProxy"}}
 {{define "form/externalProxy"}}
-<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
   <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
   <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
   <a-form-item label="External Proxy">
   <a-form-item label="External Proxy">
     <a-switch v-model="externalProxy"></a-switch>
     <a-switch v-model="externalProxy"></a-switch>
-    <a-button icon="plus" v-if="externalProxy" type="primary" :style="{ marginLeft: '10px' }" size="small"
-      @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
+    <a-button
+      icon="plus"
+      v-if="externalProxy"
+      type="primary"
+      :style="{ marginLeft: '10px' }"
+      size="small"
+      @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"
+    ></a-button>
   </a-form-item>
   </a-form-item>
-  <a-input-group :style="{ margin: '8px 0' }" compact v-for="(row, index) in inbound.stream.externalProxy">
+  <a-input-group
+    :style="{ margin: '8px 0' }"
+    compact
+    v-for="(row, index) in inbound.stream.externalProxy"
+  >
     <template>
     <template>
       <a-tooltip title="Force TLS">
       <a-tooltip title="Force TLS">
-        <a-select v-model="row.forceTls" :style="{ width: '20%', margin: '0px' }"
-          :dropdown-class-name="themeSwitcher.currentTheme">
-          <a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
+        <a-select
+          v-model="row.forceTls"
+          :style="{ width: '20%', margin: '0px' }"
+          :dropdown-class-name="themeSwitcher.currentTheme"
+        >
+          <a-select-option value="same"
+            >{{ i18n "pages.inbounds.same" }}</a-select-option
+          >
           <a-select-option value="none">{{ i18n "none" }}</a-select-option>
           <a-select-option value="none">{{ i18n "none" }}</a-select-option>
           <a-select-option value="tls">TLS</a-select-option>
           <a-select-option value="tls">TLS</a-select-option>
         </a-select>
         </a-select>
       </a-tooltip>
       </a-tooltip>
     </template>
     </template>
-    <a-input :style="{ width: '30%' }" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
+    <a-input
+      :style="{ width: '30%' }"
+      v-model.trim="row.dest"
+      placeholder='{{ i18n "host" }}'
+    ></a-input>
     <a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
     <a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
-      <a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65535"></a-input-number>
+      <a-input-number
+        :style="{ width: '15%' }"
+        v-model.number="row.port"
+        min="1"
+        max="65535"
+      ></a-input-number>
     </a-tooltip>
     </a-tooltip>
-    <a-input :style="{ width: '30%', top: '0' }" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
+    <a-input
+      :style="{ width: '30%', top: '0' }"
+      v-model.trim="row.remark"
+      placeholder='{{ i18n "remark" }}'
+    >
       <template slot="addonAfter">
       <template slot="addonAfter">
-        <a-button icon="minus" size="small" @click="inbound.stream.externalProxy.splice(index, 1)"></a-button>
+        <a-button
+          icon="minus"
+          size="small"
+          @click="inbound.stream.externalProxy.splice(index, 1)"
+        ></a-button>
       </template>
       </template>
     </a-input>
     </a-input>
   </a-input-group>
   </a-input-group>
 </a-form>
 </a-form>
-{{end}}
+{{end}}

+ 249 - 81
web/html/form/stream/stream_finalmask.html

@@ -1,84 +1,252 @@
 {{define "form/streamFinalMask"}}
 {{define "form/streamFinalMask"}}
-<a-divider :style="{ margin: '5px 0 0' }"></a-divider>
-<a-form :colon="false" :label-col="{ md: {span:8} }"
-    :wrapper-col="{ md: {span:14} }">
-    <a-form-item label="UDP Masks">
-        <a-button icon="plus" type="primary" size="small"
-            @click="inbound.stream.addUdpMask(inbound.stream.network === 'kcp' ? 'mkcp-aes128gcm' : 'xdns')"></a-button>
-    </a-form-item>
-    <template
-        v-if="inbound.stream.finalmask.udp && inbound.stream.finalmask.udp.length > 0">
-        <a-form v-for="(mask, index) in inbound.stream.finalmask.udp"
-            :key="index" :colon="false"
-            :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-            <a-divider :style="{ margin: '0' }"> UDP Mask [[ index + 1 ]]
-                <a-icon type="delete"
-                    @click="() => inbound.stream.delUdpMask(index)"
-                    :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
-            </a-divider>
-            <a-form-item label='Type'>
-                <a-select v-model="mask.type"
-                    @change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
-                    :dropdown-class-name="themeSwitcher.currentTheme">
-                    <!-- mKCP-specific masks -->
-                    <a-select-option v-if="inbound.stream.network === 'kcp'"
-                        value="mkcp-aes128gcm">
-                        mKCP AES-128-GCM</a-select-option>
-                    <a-select-option v-if="inbound.stream.network === 'kcp'"
-                        value="header-dns">
-                        Header DNS</a-select-option>
-                    <a-select-option v-if="inbound.stream.network === 'kcp'"
-                        value="header-dtls">
-                        Header DTLS 1.2</a-select-option>
-                    <a-select-option v-if="inbound.stream.network === 'kcp'"
-                        value="header-srtp">
-                        Header SRTP</a-select-option>
-                    <a-select-option v-if="inbound.stream.network === 'kcp'"
-                        value="header-utp">
-                        Header uTP</a-select-option>
-                    <a-select-option v-if="inbound.stream.network === 'kcp'"
-                        value="header-wechat">
-                        Header WeChat Video</a-select-option>
-                    <a-select-option v-if="inbound.stream.network === 'kcp'"
-                        value="header-wireguard">
-                        Header WireGuard</a-select-option>
-                    <a-select-option v-if="inbound.stream.network === 'kcp'"
-                        value="mkcp-original">
-                        mKCP Original</a-select-option>
-                    <a-select-option v-if="inbound.stream.network === 'kcp'"
-                        value="xicmp">
-                        xICMP (Experimental)</a-select-option>
-                    <!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP/KCP -->
-                    <a-select-option
-                        v-if="['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(inbound.stream.network)"
-                        value="xdns">
-                        xDNS (Experimental)</a-select-option>
-                </a-select>
-            </a-form-item>
-            <!-- Settings for password-based masks -->
-            <a-form-item label='Password'
-                v-if="['mkcp-aes128gcm'].includes(mask.type)">
-                <a-input v-model.trim="mask.settings.password"
-                    placeholder="Obfuscation password"></a-input>
-            </a-form-item>
-            <!-- Settings for domain-based masks -->
-            <a-form-item label='Domain'
-                v-if="['header-dns', 'xdns'].includes(mask.type)">
-                <a-input v-model.trim="mask.settings.domain"
-                    placeholder="e.g., www.example.com"></a-input>
-            </a-form-item>
-            <!-- Settings for xICMP -->
-            <a-form-item label='IP'
-                v-if="mask.type === 'xicmp'">
-                <a-input v-model.trim="mask.settings.ip"
-                    placeholder="e.g., 1.1.1.1"></a-input>
-            </a-form-item>
-            <a-form-item label='ID'
-                v-if="mask.type === 'xicmp'">
-                <a-input-number v-model.number="mask.settings.id"
-                    :min="0" :max="65535"></a-input-number>
-            </a-form-item>
-        </a-form>
-    </template>
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+  v-if="inbound.protocol == Protocols.HYSTERIA || inbound.stream.network == 'kcp'"
+>
+  <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
+  <a-form-item label="UDP Masks">
+    <a-button
+      icon="plus"
+      type="primary"
+      size="small"
+      @click="inbound.stream.addUdpMask(inbound.protocol === Protocols.HYSTERIA ? 'salamander' : 'mkcp-aes128gcm')"
+    ></a-button>
+  </a-form-item>
+  <template
+    v-if="inbound.stream.finalmask.udp && inbound.stream.finalmask.udp.length > 0"
+  >
+    <a-form
+      v-for="(mask, index) in inbound.stream.finalmask.udp"
+      :key="index"
+      :colon="false"
+      :label-col="{ md: {span:8} }"
+      :wrapper-col="{ md: {span:14} }"
+    >
+      <a-divider :style="{ margin: '0' }">
+        UDP Mask [[ index + 1 ]]
+        <a-icon
+          type="delete"
+          @click="() => inbound.stream.delUdpMask(index)"
+          :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
+        ></a-icon>
+      </a-divider>
+      <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
+        <a-select
+          v-model="mask.type"
+          @change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
+          :dropdown-class-name="themeSwitcher.currentTheme"
+        >
+          <template v-if="inbound.protocol === Protocols.HYSTERIA">
+            <a-select-option value="salamander"
+              >Salamander (Hysteria2)</a-select-option
+            >
+          </template>
+          <template v-else>
+            <a-select-option value="mkcp-aes128gcm"
+              >mKCP AES-128-GCM</a-select-option
+            >
+            <a-select-option value="header-dns">Header DNS</a-select-option>
+            <a-select-option value="header-dtls"
+              >Header DTLS 1.2</a-select-option
+            >
+            <a-select-option value="header-srtp">Header SRTP</a-select-option>
+            <a-select-option value="header-utp">Header uTP</a-select-option>
+            <a-select-option value="header-wechat"
+              >Header WeChat Video</a-select-option
+            >
+            <a-select-option value="header-wireguard"
+              >Header WireGuard</a-select-option
+            >
+            <a-select-option value="mkcp-original"
+              >mKCP Original</a-select-option
+            >
+            <a-select-option value="xdns">xDNS</a-select-option>
+            <a-select-option value="xicmp">xICMP</a-select-option>
+            <a-select-option value="header-custom"
+              >Header Custom</a-select-option
+            >
+            <a-select-option value="noise">Noise</a-select-option>
+            <a-select-option value="sudoku">Sudoku</a-select-option>
+          </template>
+        </a-select>
+      </a-form-item>
+      <a-form-item
+        label="Password"
+        v-if="['mkcp-aes128gcm', 'salamander', 'sudoku'].includes(mask.type)"
+      >
+        <a-input
+          v-model.trim="mask.settings.password"
+          placeholder="Obfuscation password"
+        ></a-input>
+      </a-form-item>
+      <a-form-item
+        label="Domain"
+        v-if="['header-dns', 'xdns'].includes(mask.type)"
+      >
+        <a-input
+          v-model.trim="mask.settings.domain"
+          placeholder="e.g., www.example.com"
+        ></a-input>
+      </a-form-item>
+      <template v-if="mask.type === 'header-custom'">
+        <a-form-item label="Client">
+          <a-icon
+            type="plus"
+            type="primary"
+            size="small"
+            @click="mask.settings.client.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"
+          />
+        </a-form-item>
+        <template v-for="(c, index) in mask.settings.client" :key="index">
+          <a-divider :style="{ margin: '0' }">
+            Client [[ index + 1 ]]
+            <a-icon
+              type="delete"
+              @click="() => mask.settings.client.splice(index, 1)"
+              :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
+            ></a-icon>
+          </a-divider>
+          <a-form-item label="Rand">
+            <a-input-number v-model.number="c.rand" />
+          </a-form-item>
+          <a-form-item label="Rand Range">
+            <a-input v-model.trim="c.randRange" placeholder="0-255" />
+          </a-form-item>
+          <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
+            <a-select
+              v-model="c.type"
+              :dropdown-class-name="themeSwitcher.currentTheme"
+            >
+              <a-select-option value="array">Array</a-select-option>
+              <a-select-option value="str">String</a-select-option>
+              <a-select-option value="hex">Hex</a-select-option>
+              <a-select-option value="base64">Base64</a-select-option>
+            </a-select>
+          </a-form-item>
+          <a-form-item label="Packet">
+            <a-input v-model.trim="c.packet" placeholder="binary data" />
+          </a-form-item>
+        </template>
+        <a-divider :style="{ margin: '0' }"></a-divider>
+        <a-form-item label="Server">
+          <a-icon
+            type="plus"
+            type="primary"
+            size="small"
+            @click="mask.settings.server.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"
+          />
+        </a-form-item>
+        <template v-for="(s, index) in mask.settings.server" :key="index">
+          <a-divider :style="{ margin: '0' }">
+            Server [[ index + 1 ]]
+            <a-icon
+              type="delete"
+              @click="() => mask.settings.server.splice(index, 1)"
+              :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
+            ></a-icon>
+          </a-divider>
+          <a-form-item label="Rand">
+            <a-input-number v-model.number="s.rand" />
+          </a-form-item>
+          <a-form-item label="Rand Range">
+            <a-input v-model.trim="s.randRange" placeholder="0-255" />
+          </a-form-item>
+          <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
+            <a-select
+              v-model="s.type"
+              :dropdown-class-name="themeSwitcher.currentTheme"
+            >
+              <a-select-option value="array">Array</a-select-option>
+              <a-select-option value="str">String</a-select-option>
+              <a-select-option value="hex">Hex</a-select-option>
+              <a-select-option value="base64">Base64</a-select-option>
+            </a-select>
+          </a-form-item>
+          <a-form-item label="Packet">
+            <a-input v-model.trim="s.packet" placeholder="binary data" />
+          </a-form-item>
+        </template>
+      </template>
+      <template v-if="mask.type === 'noise'">
+        <a-form-item label="Reset">
+          <a-input-number v-model.number="mask.settings.reset" :min="0" />
+        </a-form-item>
+        <a-form-item label="Noise">
+          <a-icon
+            type="plus"
+            type="primary"
+            size="small"
+            @click="mask.settings.noise.push({rand: '1-8192', randRange: '0-255', type: 'array', packet: '', delay: ''})"
+          />
+        </a-form-item>
+        <template v-for="(n, index) in mask.settings.noise" :key="index">
+          <a-divider :style="{ margin: '0' }">
+            Noise [[ index + 1 ]]
+            <a-icon
+              type="delete"
+              @click="() => mask.settings.noise.splice(index, 1)"
+              :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
+            ></a-icon>
+          </a-divider>
+          <a-form-item label="Rand">
+            <a-input v-model.trim="n.rand" placeholder="1-8192" />
+          </a-form-item>
+          <a-form-item label="Rand Range">
+            <a-input v-model.trim="n.randRange" placeholder="0-255" />
+          </a-form-item>
+          <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
+            <a-select
+              v-model="n.type"
+              :dropdown-class-name="themeSwitcher.currentTheme"
+            >
+              <a-select-option value="array">Array</a-select-option>
+              <a-select-option value="str">String</a-select-option>
+              <a-select-option value="hex">Hex</a-select-option>
+              <a-select-option value="base64">Base64</a-select-option>
+            </a-select>
+          </a-form-item>
+          <a-form-item label="Packet">
+            <a-input v-model.trim="n.packet" placeholder="binary data" />
+          </a-form-item>
+          <a-form-item label="Delay">
+            <a-input v-model.trim="n.delay" placeholder="10-20" />
+          </a-form-item>
+        </template>
+      </template>
+      <template v-if="mask.type === 'sudoku'">
+        <a-form-item label="ASCII">
+          <a-input v-model.trim="mask.settings.ascii" placeholder="ASCII" />
+        </a-form-item>
+        <a-form-item label="Custom Table">
+          <a-input
+            v-model.trim="mask.settings.customTable"
+            placeholder="Custom Table"
+          />
+        </a-form-item>
+        <a-form-item label="Custom Tables">
+          <a-input
+            v-model.trim="mask.settings.customTables"
+            placeholder="Custom Tables"
+          />
+        </a-form-item>
+        <a-form-item label="Padding Min">
+          <a-input-number v-model.number="mask.settings.paddingMin" :min="0" />
+        </a-form-item>
+        <a-form-item label="Padding Max">
+          <a-input-number v-model.number="mask.settings.paddingMax" :min="0" />
+        </a-form-item>
+      </template>
+      <template v-if="mask.type === 'xicmp'">
+        <a-form-item label="IP">
+          <a-input v-model.trim="mask.settings.ip" placeholder="0.0.0.0" />
+        </a-form-item>
+        <a-form-item label="ID">
+          <a-input-number v-model.number="mask.settings.id" :min="0" />
+        </a-form-item>
+      </template>
+    </a-form>
+  </template>
 </a-form>
 </a-form>
 {{end}}
 {{end}}

+ 14 - 10
web/html/form/stream/stream_grpc.html

@@ -1,13 +1,17 @@
 {{define "form/streamGRPC"}}
 {{define "form/streamGRPC"}}
-<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-    <a-form-item label="Service Name">
-        <a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
-    </a-form-item>
-    <a-form-item label="Authority">
-        <a-input v-model.trim="inbound.stream.grpc.authority"></a-input>
-    </a-form-item>
-    <a-form-item label="Multi Mode">
-        <a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
-    </a-form-item>
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
+  <a-form-item label="Service Name">
+    <a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
+  </a-form-item>
+  <a-form-item label="Authority">
+    <a-input v-model.trim="inbound.stream.grpc.authority"></a-input>
+  </a-form-item>
+  <a-form-item label="Multi Mode">
+    <a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
+  </a-form-item>
 </a-form>
 </a-form>
 {{end}}
 {{end}}

+ 36 - 8
web/html/form/stream/stream_httpupgrade.html

@@ -1,7 +1,13 @@
 {{define "form/streamHTTPUpgrade"}}
 {{define "form/streamHTTPUpgrade"}}
-<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
   <a-form-item label="Proxy Protocol">
   <a-form-item label="Proxy Protocol">
-    <a-switch v-model="inbound.stream.httpupgrade.acceptProxyProtocol"></a-switch>
+    <a-switch
+      v-model="inbound.stream.httpupgrade.acceptProxyProtocol"
+    ></a-switch>
   </a-form-item>
   </a-form-item>
   <a-form-item label='{{ i18n "host" }}'>
   <a-form-item label='{{ i18n "host" }}'>
     <a-input v-model.trim="inbound.stream.httpupgrade.host"></a-input>
     <a-input v-model.trim="inbound.stream.httpupgrade.host"></a-input>
@@ -10,15 +16,37 @@
     <a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
     <a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
   </a-form-item>
   </a-form-item>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
-    <a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('', '')"></a-button>
+    <a-button
+      icon="plus"
+      size="small"
+      @click="inbound.stream.httpupgrade.addHeader('', '')"
+    ></a-button>
   </a-form-item>
   </a-form-item>
   <a-form-item :wrapper-col="{span:24}">
   <a-form-item :wrapper-col="{span:24}">
-    <a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers">
-      <a-input :style="{ width: '50%' }" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
-        <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
+    <a-input-group
+      compact
+      v-for="(header, index) in inbound.stream.httpupgrade.headers"
+    >
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="header.name"
+        placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
+      >
+        <template slot="addonBefore" :style="{ margin: '0' }"
+          >[[ index+1 ]]</template
+        >
       </a-input>
       </a-input>
-      <a-input :style="{ width: '50%' }" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
-        <a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.httpupgrade.removeHeader(index)"></a-button>
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="header.value"
+        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
+      >
+        <a-button
+          icon="minus"
+          slot="addonAfter"
+          size="small"
+          @click="inbound.stream.httpupgrade.removeHeader(index)"
+        ></a-button>
       </a-input>
       </a-input>
     </a-input-group>
     </a-input-group>
   </a-form-item>
   </a-form-item>

+ 115 - 0
web/html/form/stream/stream_hysteria.html

@@ -0,0 +1,115 @@
+{{define "form/streamHysteria"}}
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
+  <a-form-item>
+    <template slot="label">
+      <a-tooltip>
+        <template slot="title">
+          <span>{{ i18n "reset" }}</span>
+        </template>
+        Auth Password
+        <a-icon
+          @click="inbound.stream.hysteria.auth = RandomUtil.randomSeq(10)"
+          type="sync"
+        ></a-icon>
+      </a-tooltip>
+    </template>
+    <a-input v-model.trim="inbound.stream.hysteria.auth"></a-input>
+  </a-form-item>
+  <a-form-item label="UDP Idle Timeout">
+    <a-input-number
+      v-model.number="inbound.stream.hysteria.udpIdleTimeout"
+      :min="0"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item label="Masquerade">
+    <a-switch v-model="inbound.stream.hysteria.masqueradeSwitch"></a-switch>
+  </a-form-item>
+  <template v-if="inbound.stream.hysteria.masqueradeSwitch">
+    <a-divider :style="{ margin: '5px 0 0' }">Masquerade</a-divider>
+    <a-form-item label="Type">
+      <a-select
+        v-model="inbound.stream.hysteria.masquerade.type"
+        :dropdown-class-name="themeSwitcher.currentTheme"
+      >
+        <a-select-option value="file">File</a-select-option>
+        <a-select-option value="proxy">Proxy</a-select-option>
+        <a-select-option value="string">String</a-select-option>
+      </a-select>
+    </a-form-item>
+    <a-form-item
+      label="Dir"
+      v-if="inbound.stream.hysteria.masquerade.type === 'file'"
+    >
+      <a-input v-model.trim="inbound.stream.hysteria.masquerade.dir"></a-input>
+    </a-form-item>
+    <template v-if="inbound.stream.hysteria.masquerade.type === 'proxy'">
+      <a-form-item label="URL">
+        <a-input
+          v-model.trim="inbound.stream.hysteria.masquerade.url"
+        ></a-input>
+      </a-form-item>
+      <a-form-item label="Rewrite Host">
+        <a-switch
+          v-model="inbound.stream.hysteria.masquerade.rewriteHost"
+        ></a-switch>
+      </a-form-item>
+      <a-form-item label="Insecure">
+        <a-switch
+          v-model="inbound.stream.hysteria.masquerade.insecure"
+        ></a-switch>
+      </a-form-item>
+    </template>
+    <template v-if="inbound.stream.hysteria.masquerade.type === 'string'">
+      <a-form-item label="Content">
+        <a-input
+          v-model.trim="inbound.stream.hysteria.masquerade.content"
+        ></a-input>
+      </a-form-item>
+      <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
+        <a-button
+          size="small"
+          @click="inbound.stream.hysteria.masquerade.addHeader('', '')"
+          >+</a-button
+        >
+      </a-form-item>
+      <a-form-item :wrapper-col="{span:24}">
+        <a-input-group
+          compact
+          v-for="(header, index) in inbound.stream.hysteria.masquerade.headers"
+        >
+          <a-input
+            style="width: 50%"
+            v-model.trim="header.name"
+            placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
+          >
+            <template slot="addonBefore" style="margin: 0"
+              >[[ index+1 ]]</template
+            >
+          </a-input>
+          <a-input
+            style="width: 50%"
+            v-model.trim="header.value"
+            placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
+          >
+            <a-button
+              slot="addonAfter"
+              size="small"
+              @click="inbound.stream.hysteria.masquerade.removeHeader(index)"
+              >-</a-button
+            >
+          </a-input>
+        </a-input-group>
+      </a-form-item>
+      <a-form-item label="Status Code">
+        <a-input-number
+          v-model.number="inbound.stream.hysteria.masquerade.statusCode"
+        ></a-input-number>
+      </a-form-item>
+    </template>
+  </template>
+</a-form>
+{{end}}

+ 43 - 29
web/html/form/stream/stream_kcp.html

@@ -1,32 +1,46 @@
 {{define "form/streamKCP"}}
 {{define "form/streamKCP"}}
-<a-form :colon="false" :label-col="{ md: {span:8} }"
-    :wrapper-col="{ md: {span:14} }">
-    <a-form-item label='MTU'>
-        <a-input-number v-model.number="inbound.stream.kcp.mtu" :min="576"
-            :max="1460"></a-input-number>
-    </a-form-item>
-    <a-form-item label='TTI (ms)'>
-        <a-input-number v-model.number="inbound.stream.kcp.tti" :min="10"
-            :max="100"></a-input-number>
-    </a-form-item>
-    <a-form-item label='Uplink (MB/s)'>
-        <a-input-number v-model.number="inbound.stream.kcp.upCap"
-            :min="0"></a-input-number>
-    </a-form-item>
-    <a-form-item label='Downlink (MB/s)'>
-        <a-input-number v-model.number="inbound.stream.kcp.downCap"
-            :min="0"></a-input-number>
-    </a-form-item>
-    <a-form-item label='Congestion'>
-        <a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
-    </a-form-item>
-    <a-form-item label='Read Buffer (MB)'>
-        <a-input-number v-model.number="inbound.stream.kcp.readBuffer"
-            :min="0"></a-input-number>
-    </a-form-item>
-    <a-form-item label='Write Buffer (MB)'>
-        <a-input-number v-model.number="inbound.stream.kcp.writeBuffer"
-            :min="0"></a-input-number>
-    </a-form-item>
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
+  <a-form-item label="MTU">
+    <a-input-number
+      v-model.number="inbound.stream.kcp.mtu"
+      :min="576"
+      :max="1460"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item label="TTI (ms)">
+    <a-input-number
+      v-model.number="inbound.stream.kcp.tti"
+      :min="10"
+      :max="100"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item label="Uplink (MB/s)">
+    <a-input-number
+      v-model.number="inbound.stream.kcp.upCap"
+      :min="0"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item label="Downlink (MB/s)">
+    <a-input-number
+      v-model.number="inbound.stream.kcp.downCap"
+      :min="0"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item label="CWND Multiplier">
+    <a-input-number
+      v-model.number="inbound.stream.kcp.cwndMultiplier"
+      :min="0"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item label="Max Sending Window">
+    <a-input-number
+      v-model.number="inbound.stream.kcp.maxSendingWindow"
+      :min="0"
+    ></a-input-number>
+  </a-form-item>
 </a-form>
 </a-form>
 {{end}}
 {{end}}

+ 35 - 28
web/html/form/stream/stream_settings.html

@@ -1,59 +1,66 @@
 {{define "form/streamSettings"}}
 {{define "form/streamSettings"}}
 <!-- select stream network -->
 <!-- select stream network -->
-<a-form :colon="false" :label-col="{ md: {span:8} }"
-    :wrapper-col="{ md: {span:14} }">
-    <a-form-item label='{{ i18n "transmission" }}'>
-        <a-select v-model="inbound.stream.network" :style="{ width: '75%' }"
-            @change="streamNetworkChange"
-            :dropdown-class-name="themeSwitcher.currentTheme">
-            <a-select-option value="tcp">TCP (RAW)</a-select-option>
-            <a-select-option value="kcp">mKCP</a-select-option>
-            <a-select-option value="ws">WebSocket</a-select-option>
-            <a-select-option value="grpc">gRPC</a-select-option>
-            <a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
-            <a-select-option value="xhttp">XHTTP</a-select-option>
-        </a-select>
-    </a-form-item>
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+  v-if="inbound.protocol != Protocols.HYSTERIA"
+>
+  <a-form-item label='{{ i18n "transmission" }}'>
+    <a-select
+      v-model="inbound.stream.network"
+      :style="{ width: '75%' }"
+      @change="streamNetworkChange"
+      :dropdown-class-name="themeSwitcher.currentTheme"
+    >
+      <a-select-option value="tcp">TCP (RAW)</a-select-option>
+      <a-select-option value="kcp">mKCP</a-select-option>
+      <a-select-option value="ws">WebSocket</a-select-option>
+      <a-select-option value="grpc">gRPC</a-select-option>
+      <a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
+      <a-select-option value="xhttp">XHTTP</a-select-option>
+    </a-select>
+  </a-form-item>
 </a-form>
 </a-form>
 
 
 <!-- tcp -->
 <!-- tcp -->
 <template v-if="inbound.stream.network === 'tcp'">
 <template v-if="inbound.stream.network === 'tcp'">
-    {{template "form/streamTCP"}}
+  {{template "form/streamTCP"}}
 </template>
 </template>
 
 
 <!-- kcp -->
 <!-- kcp -->
 <template v-if="inbound.stream.network === 'kcp'">
 <template v-if="inbound.stream.network === 'kcp'">
-    {{template "form/streamKCP"}}
+  {{template "form/streamKCP"}}
 </template>
 </template>
 
 
 <!-- ws -->
 <!-- ws -->
 <template v-if="inbound.stream.network === 'ws'">
 <template v-if="inbound.stream.network === 'ws'">
-    {{template "form/streamWS"}}
+  {{template "form/streamWS"}}
 </template>
 </template>
 
 
 <!-- grpc -->
 <!-- grpc -->
 <template v-if="inbound.stream.network === 'grpc'">
 <template v-if="inbound.stream.network === 'grpc'">
-    {{template "form/streamGRPC"}}
+  {{template "form/streamGRPC"}}
+</template>
+
+<!-- hysteria -->
+<template v-if="inbound.stream.network === 'hysteria'">
+  {{template "form/streamHysteria"}}
 </template>
 </template>
 
 
 <!-- httpupgrade -->
 <!-- httpupgrade -->
 <template v-if="inbound.stream.network === 'httpupgrade'">
 <template v-if="inbound.stream.network === 'httpupgrade'">
-    {{template "form/streamHTTPUpgrade"}}
+  {{template "form/streamHTTPUpgrade"}}
 </template>
 </template>
 
 
 <!-- xhttp -->
 <!-- xhttp -->
 <template v-if="inbound.stream.network === 'xhttp'">
 <template v-if="inbound.stream.network === 'xhttp'">
-    {{template "form/streamXHTTP"}}
+  {{template "form/streamXHTTP"}}
 </template>
 </template>
 
 
 <!-- sockopt -->
 <!-- sockopt -->
-<template>
-    {{template "form/streamSockopt"}}
-</template>
+<template> {{template "form/streamSockopt"}} </template>
 
 
-<!-- finalmask - only for TCP, WS, HTTPUpgrade, XHTTP, mKCP -->
-<template
-    v-if="['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(inbound.stream.network)">
-    {{template "form/streamFinalMask"}}
-</template>
+<!-- finalmask -->
+<template> {{template "form/streamFinalMask"}} </template>
 {{end}}
 {{end}}

+ 115 - 71
web/html/form/stream/stream_sockopt.html

@@ -1,75 +1,119 @@
 {{define "form/streamSockopt"}}
 {{define "form/streamSockopt"}}
 <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
 <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
-<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-    <a-form-item label="Sockopt">
-        <a-switch v-model="inbound.stream.sockoptSwitch"></a-switch>
-    </a-form-item>
-    <template v-if="inbound.stream.sockoptSwitch">
-        <a-form-item label="Route Mark">
-            <a-input-number v-model.number="inbound.stream.sockopt.mark" :min="0"></a-input-number>
-        </a-form-item>
-        <a-form-item label="TCP Keep Alive Interval">
-            <a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
-        </a-form-item>
-        <a-form-item label="TCP Keep Alive Idle">
-            <a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number>
-        </a-form-item>
-        <a-form-item label="TCP Max Seg">
-            <a-input-number v-model.number="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number>
-        </a-form-item>
-        <a-form-item label="TCP User Timeout">
-            <a-input-number v-model.number="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number>
-        </a-form-item>
-        <a-form-item label="TCP Window Clamp">
-            <a-input-number v-model.number="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number>
-        </a-form-item>
-        <a-form-item label="Proxy Protocol">
-            <a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
-        </a-form-item>
-        <a-form-item label="TCP Fast Open">
-            <a-switch v-model.trim="inbound.stream.sockopt.tcpFastOpen"></a-switch>
-        </a-form-item>
-        <a-form-item label="Multipath TCP">
-            <a-switch v-model.trim="inbound.stream.sockopt.tcpMptcp"></a-switch>
-        </a-form-item>
-        <a-form-item label="Penetrate">
-            <a-switch v-model.trim="inbound.stream.sockopt.penetrate"></a-switch>
-        </a-form-item>
-        <a-form-item label="V6 Only">
-            <a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch>
-        </a-form-item>
-        <a-form-item label='Domain Strategy'>
-            <a-select v-model="inbound.stream.sockopt.domainStrategy" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
-              <a-select-option v-for="key in DOMAIN_STRATEGY_OPTION" :value="key">[[ key ]]</a-select-option>
-            </a-select>
-        </a-form-item>
-        <a-form-item label='TCP Congestion'>
-            <a-select v-model="inbound.stream.sockopt.tcpcongestion" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
-              <a-select-option v-for="key in TCP_CONGESTION_OPTION" :value="key">[[ key ]]</a-select-option>
-            </a-select>
-        </a-form-item>
-        <a-form-item label="TProxy">
-            <a-select v-model="inbound.stream.sockopt.tproxy" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
-                <a-select-option value="off">Off</a-select-option>
-                <a-select-option value="redirect">Redirect</a-select-option>
-                <a-select-option value="tproxy">TProxy</a-select-option>
-            </a-select>
-        </a-form-item>
-        <a-form-item label="Dialer Proxy">
-            <a-input v-model="inbound.stream.sockopt.dialerProxy"></a-input>
-        </a-form-item>
-        <a-form-item label="Interface Name">
-            <a-input v-model="inbound.stream.sockopt.interfaceName"></a-input>
-        </a-form-item>
-        <a-form-item label="Trusted X-Forwarded-For">
-            <a-select mode="tags" v-model="inbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
-                :dropdown-class-name="themeSwitcher.currentTheme">
-                <a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
-                <a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
-                <a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
-                <a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
-            </a-select>
-        </a-form-item>
-    </template>
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
+  <a-form-item label="Sockopt">
+    <a-switch v-model="inbound.stream.sockoptSwitch"></a-switch>
+  </a-form-item>
+  <template v-if="inbound.stream.sockoptSwitch">
+    <a-form-item label="Route Mark">
+      <a-input-number
+        v-model.number="inbound.stream.sockopt.mark"
+        :min="0"
+      ></a-input-number>
+    </a-form-item>
+    <a-form-item label="TCP Keep Alive Interval">
+      <a-input-number
+        v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval"
+        :min="0"
+      ></a-input-number>
+    </a-form-item>
+    <a-form-item label="TCP Keep Alive Idle">
+      <a-input-number
+        v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle"
+        :min="0"
+      ></a-input-number>
+    </a-form-item>
+    <a-form-item label="TCP Max Seg">
+      <a-input-number
+        v-model.number="inbound.stream.sockopt.tcpMaxSeg"
+        :min="0"
+      ></a-input-number>
+    </a-form-item>
+    <a-form-item label="TCP User Timeout">
+      <a-input-number
+        v-model.number="inbound.stream.sockopt.tcpUserTimeout"
+        :min="0"
+      ></a-input-number>
+    </a-form-item>
+    <a-form-item label="TCP Window Clamp">
+      <a-input-number
+        v-model.number="inbound.stream.sockopt.tcpWindowClamp"
+        :min="0"
+      ></a-input-number>
+    </a-form-item>
+    <a-form-item label="Proxy Protocol">
+      <a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
+    </a-form-item>
+    <a-form-item label="TCP Fast Open">
+      <a-switch v-model.trim="inbound.stream.sockopt.tcpFastOpen"></a-switch>
+    </a-form-item>
+    <a-form-item label="Multipath TCP">
+      <a-switch v-model.trim="inbound.stream.sockopt.tcpMptcp"></a-switch>
+    </a-form-item>
+    <a-form-item label="Penetrate">
+      <a-switch v-model.trim="inbound.stream.sockopt.penetrate"></a-switch>
+    </a-form-item>
+    <a-form-item label="V6 Only">
+      <a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch>
+    </a-form-item>
+    <a-form-item label="Domain Strategy">
+      <a-select
+        v-model="inbound.stream.sockopt.domainStrategy"
+        :style="{ width: '50%' }"
+        :dropdown-class-name="themeSwitcher.currentTheme"
+      >
+        <a-select-option v-for="key in DOMAIN_STRATEGY_OPTION" :value="key"
+          >[[ key ]]</a-select-option
+        >
+      </a-select>
+    </a-form-item>
+    <a-form-item label="TCP Congestion">
+      <a-select
+        v-model="inbound.stream.sockopt.tcpcongestion"
+        :style="{ width: '50%' }"
+        :dropdown-class-name="themeSwitcher.currentTheme"
+      >
+        <a-select-option v-for="key in TCP_CONGESTION_OPTION" :value="key"
+          >[[ key ]]</a-select-option
+        >
+      </a-select>
+    </a-form-item>
+    <a-form-item label="TProxy">
+      <a-select
+        v-model="inbound.stream.sockopt.tproxy"
+        :style="{ width: '50%' }"
+        :dropdown-class-name="themeSwitcher.currentTheme"
+      >
+        <a-select-option value="off">Off</a-select-option>
+        <a-select-option value="redirect">Redirect</a-select-option>
+        <a-select-option value="tproxy">TProxy</a-select-option>
+      </a-select>
+    </a-form-item>
+    <a-form-item label="Dialer Proxy">
+      <a-input v-model="inbound.stream.sockopt.dialerProxy"></a-input>
+    </a-form-item>
+    <a-form-item label="Interface Name">
+      <a-input v-model="inbound.stream.sockopt.interfaceName"></a-input>
+    </a-form-item>
+    <a-form-item label="Trusted X-Forwarded-For">
+      <a-select
+        mode="tags"
+        v-model="inbound.stream.sockopt.trustedXForwardedFor"
+        :style="{ width: '100%' }"
+        :dropdown-class-name="themeSwitcher.currentTheme"
+      >
+        <a-select-option value="CF-Connecting-IP"
+          >CF-Connecting-IP</a-select-option
+        >
+        <a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
+        <a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
+        <a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
+      </a-select>
+    </a-form-item>
+  </template>
 </a-form>
 </a-form>
 {{end}}
 {{end}}

+ 90 - 20
web/html/form/stream/stream_tcp.html

@@ -1,17 +1,31 @@
 {{define "form/streamTCP"}}
 {{define "form/streamTCP"}}
 <!-- tcp type -->
 <!-- tcp type -->
-<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
   <a-form-item label="Proxy Protocol" v-if="inbound.canEnableTls()">
   <a-form-item label="Proxy Protocol" v-if="inbound.canEnableTls()">
     <a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
     <a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
   </a-form-item>
   </a-form-item>
   <a-form-item label='HTTP {{ i18n "camouflage" }}'>
   <a-form-item label='HTTP {{ i18n "camouflage" }}'>
-    <a-switch :checked="inbound.stream.tcp.type === 'http'" @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
+    <a-switch
+      :checked="inbound.stream.tcp.type === 'http'"
+      @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"
+    ></a-switch>
   </a-form-item>
   </a-form-item>
 </a-form>
 </a-form>
 
 
-<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+<a-form
+  v-if="inbound.stream.tcp.type === 'http'"
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
   <!-- tcp request -->
   <!-- tcp request -->
-  <a-divider :style="{ margin: '0' }">{{ i18n "pages.inbounds.stream.general.request" }}</a-divider>
+  <a-divider :style="{ margin: '0' }"
+    >{{ i18n "pages.inbounds.stream.general.request" }}</a-divider
+  >
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
     <a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
     <a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
   </a-form-item>
   </a-form-item>
@@ -19,31 +33,66 @@
     <a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
     <a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
   </a-form-item>
   </a-form-item>
   <a-form-item>
   <a-form-item>
-    <template slot="label">{{ i18n "pages.inbounds.stream.tcp.path" }}
-      <a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addPath('/')"></a-button>
+    <template slot="label"
+      >{{ i18n "pages.inbounds.stream.tcp.path" }}
+      <a-button
+        icon="plus"
+        size="small"
+        @click="inbound.stream.tcp.request.addPath('/')"
+      ></a-button>
     </template>
     </template>
     <template v-for="(path, index) in inbound.stream.tcp.request.path">
     <template v-for="(path, index) in inbound.stream.tcp.request.path">
       <a-input v-model.trim="inbound.stream.tcp.request.path[index]">
       <a-input v-model.trim="inbound.stream.tcp.request.path[index]">
-        <a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)" v-if="inbound.stream.tcp.request.path.length>1"></a-button>
+        <a-button
+          icon="minus"
+          size="small"
+          slot="addonAfter"
+          @click="inbound.stream.tcp.request.removePath(index)"
+          v-if="inbound.stream.tcp.request.path.length>1"
+        ></a-button>
       </a-input>
       </a-input>
     </template>
     </template>
   </a-form-item>
   </a-form-item>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
-    <a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')"></a-button>
+    <a-button
+      icon="plus"
+      size="small"
+      @click="inbound.stream.tcp.request.addHeader('Host', '')"
+    ></a-button>
   </a-form-item>
   </a-form-item>
   <a-form-item :wrapper-col="{span:24}">
   <a-form-item :wrapper-col="{span:24}">
-    <a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
-      <a-input :style="{ width: '50%' }" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
-        <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
+    <a-input-group
+      compact
+      v-for="(header, index) in inbound.stream.tcp.request.headers"
+    >
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="header.name"
+        placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'
+      >
+        <template slot="addonBefore" :style="{ margin: '0' }"
+          >[[ index+1 ]]</template
+        >
       </a-input>
       </a-input>
-      <a-input :style="{ width: '50%' }" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
-        <a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.tcp.request.removeHeader(index)"></a-button>
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="header.value"
+        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
+      >
+        <a-button
+          icon="minus"
+          slot="addonAfter"
+          size="small"
+          @click="inbound.stream.tcp.request.removeHeader(index)"
+        ></a-button>
       </a-input>
       </a-input>
     </a-input-group>
     </a-input-group>
   </a-form-item>
   </a-form-item>
 
 
   <!-- tcp response -->
   <!-- tcp response -->
-  <a-divider :style="{ margin: '0' }">{{ i18n "pages.inbounds.stream.general.response" }}</a-divider>
+  <a-divider :style="{ margin: '0' }"
+    >{{ i18n "pages.inbounds.stream.general.response" }}</a-divider
+  >
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
     <a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
     <a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
   </a-form-item>
   </a-form-item>
@@ -54,16 +103,37 @@
     <a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
     <a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
   </a-form-item>
   </a-form-item>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
-    <a-button icon="plus" size="small" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"></a-button>
+    <a-button
+      icon="plus"
+      size="small"
+      @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"
+    ></a-button>
   </a-form-item>
   </a-form-item>
   <a-form-item :wrapper-col="{span:24}">
   <a-form-item :wrapper-col="{span:24}">
-    <a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
-      <a-input :style="{ width: '50%' }" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
-        <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
+    <a-input-group
+      compact
+      v-for="(header, index) in inbound.stream.tcp.response.headers"
+    >
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="header.name"
+        placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'
+      >
+        <template slot="addonBefore" :style="{ margin: '0' }"
+          >[[ index+1 ]]</template
+        >
       </a-input>
       </a-input>
-      <a-input :style="{ width: '50%' }" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="header.value"
+        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
+      >
         <template slot="addonAfter">
         <template slot="addonAfter">
-          <a-button icon="minus" size="small" @click="inbound.stream.tcp.response.removeHeader(index)"></a-button>
+          <a-button
+            icon="minus"
+            size="small"
+            @click="inbound.stream.tcp.response.removeHeader(index)"
+          ></a-button>
         </template>
         </template>
       </a-input>
       </a-input>
     </a-input-group>
     </a-input-group>

+ 34 - 8
web/html/form/stream/stream_ws.html

@@ -1,5 +1,9 @@
 {{define "form/streamWS"}}
 {{define "form/streamWS"}}
-<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
   <a-form-item label="Proxy Protocol">
   <a-form-item label="Proxy Protocol">
     <a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
     <a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
   </a-form-item>
   </a-form-item>
@@ -9,19 +13,41 @@
   <a-form-item label='{{ i18n "path" }}'>
   <a-form-item label='{{ i18n "path" }}'>
     <a-input v-model.trim="inbound.stream.ws.path"></a-input>
     <a-input v-model.trim="inbound.stream.ws.path"></a-input>
   </a-form-item>
   </a-form-item>
-  <a-form-item label='Heartbeat Period'>
-    <a-input-number v-model.number="inbound.stream.ws.heartbeatPeriod" :min="0"></a-input-number>
+  <a-form-item label="Heartbeat Period">
+    <a-input-number
+      v-model.number="inbound.stream.ws.heartbeatPeriod"
+      :min="0"
+    ></a-input-number>
   </a-form-item>
   </a-form-item>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
-    <a-button icon="plus" size="small" @click="inbound.stream.ws.addHeader('', '')"></a-button>
+    <a-button
+      icon="plus"
+      size="small"
+      @click="inbound.stream.ws.addHeader('', '')"
+    ></a-button>
   </a-form-item>
   </a-form-item>
   <a-form-item :wrapper-col="{span:24}">
   <a-form-item :wrapper-col="{span:24}">
     <a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
     <a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
-      <a-input :style="{ width: '50%' }" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
-        <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="header.name"
+        placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
+      >
+        <template slot="addonBefore" :style="{ margin: '0' }"
+          >[[ index+1 ]]</template
+        >
       </a-input>
       </a-input>
-      <a-input :style="{ width: '50%' }" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
-        <a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.ws.removeHeader(index)"></a-button>
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="header.value"
+        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
+      >
+        <a-button
+          icon="minus"
+          slot="addonAfter"
+          size="small"
+          @click="inbound.stream.ws.removeHeader(index)"
+        ></a-button>
       </a-input>
       </a-input>
     </a-input-group>
     </a-input-group>
   </a-form-item>
   </a-form-item>

+ 210 - 148
web/html/form/stream/stream_xhttp.html

@@ -1,150 +1,212 @@
 {{define "form/streamXHTTP"}}
 {{define "form/streamXHTTP"}}
-<a-form :colon="false" :label-col="{ md: {span:8} }"
-    :wrapper-col="{ md: {span:14} }">
-    <a-form-item label='{{ i18n "host" }}'>
-        <a-input v-model.trim="inbound.stream.xhttp.host"></a-input>
-    </a-form-item>
-    <a-form-item label='{{ i18n "path" }}'>
-        <a-input v-model.trim="inbound.stream.xhttp.path"></a-input>
-    </a-form-item>
-    <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
-        <a-button icon="plus" size="small"
-            @click="inbound.stream.xhttp.addHeader('', '')"></a-button>
-    </a-form-item>
-    <a-form-item :wrapper-col="{span:24}">
-        <a-input-group compact
-            v-for="(header, index) in inbound.stream.xhttp.headers">
-            <a-input :style="{ width: '50%' }" v-model.trim="header.name"
-                placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
-                <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1
-                    ]]</template>
-            </a-input>
-            <a-input :style="{ width: '50%' }" v-model.trim="header.value"
-                placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
-                <a-button icon="minus" slot="addonAfter" size="small"
-                    @click="inbound.stream.xhttp.removeHeader(index)"></a-button>
-            </a-input>
-        </a-input-group>
-    </a-form-item>
-    <a-form-item label='Mode'>
-        <a-select v-model="inbound.stream.xhttp.mode" :style="{ width: '50%' }"
-            :dropdown-class-name="themeSwitcher.currentTheme">
-            <a-select-option v-for="key in MODE_OPTION" :value="key">[[ key
-                ]]</a-select-option>
-        </a-select>
-    </a-form-item>
-    <a-form-item label="Max Buffered Upload"
-        v-if="inbound.stream.xhttp.mode === 'packet-up'">
-        <a-input-number
-            v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
-    </a-form-item>
-    <a-form-item label="Max Upload Size (Byte)"
-        v-if="inbound.stream.xhttp.mode === 'packet-up'">
-        <a-input
-            v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
-    </a-form-item>
-    <a-form-item label="Stream-Up Server"
-        v-if="inbound.stream.xhttp.mode === 'stream-up'">
-        <a-input
-            v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
-    </a-form-item>
-    <a-form-item label="Padding Bytes">
-        <a-input v-model.trim="inbound.stream.xhttp.xPaddingBytes"></a-input>
-    </a-form-item>
-    <a-form-item label="Padding Obfs Mode">
-        <a-switch v-model="inbound.stream.xhttp.xPaddingObfsMode"></a-switch>
-    </a-form-item>
-    <template v-if="inbound.stream.xhttp.xPaddingObfsMode">
-        <a-form-item label="Padding Key">
-            <a-input v-model.trim="inbound.stream.xhttp.xPaddingKey"
-                placeholder="x_padding"></a-input>
-        </a-form-item>
-        <a-form-item label="Padding Header">
-            <a-input v-model.trim="inbound.stream.xhttp.xPaddingHeader"
-                placeholder="X-Padding"></a-input>
-        </a-form-item>
-        <a-form-item label="Padding Placement">
-            <a-select v-model="inbound.stream.xhttp.xPaddingPlacement"
-                :dropdown-class-name="themeSwitcher.currentTheme">
-                <a-select-option value>Default (queryInHeader)</a-select-option>
-                <a-select-option
-                    value="queryInHeader">queryInHeader</a-select-option>
-                <a-select-option value="header">header</a-select-option>
-                <a-select-option value="cookie">cookie</a-select-option>
-                <a-select-option value="query">query</a-select-option>
-            </a-select>
-        </a-form-item>
-        <a-form-item label="Padding Method">
-            <a-select v-model="inbound.stream.xhttp.xPaddingMethod"
-                :dropdown-class-name="themeSwitcher.currentTheme">
-                <a-select-option value>Default (repeat-x)</a-select-option>
-                <a-select-option value="repeat-x">repeat-x</a-select-option>
-                <a-select-option value="tokenish">tokenish</a-select-option>
-            </a-select>
-        </a-form-item>
-    </template>
-    <a-form-item label="Uplink HTTP Method">
-        <a-select v-model="inbound.stream.xhttp.uplinkHTTPMethod"
-            :dropdown-class-name="themeSwitcher.currentTheme">
-            <a-select-option value>Default (POST)</a-select-option>
-            <a-select-option value="POST">POST</a-select-option>
-            <a-select-option value="PUT">PUT</a-select-option>
-            <a-select-option value="GET">GET (packet-up only)</a-select-option>
-        </a-select>
-    </a-form-item>
-    <a-form-item label="Session Placement">
-        <a-select v-model="inbound.stream.xhttp.sessionPlacement"
-            :dropdown-class-name="themeSwitcher.currentTheme">
-            <a-select-option value>Default (path)</a-select-option>
-            <a-select-option value="path">path</a-select-option>
-            <a-select-option value="header">header</a-select-option>
-            <a-select-option value="cookie">cookie</a-select-option>
-            <a-select-option value="query">query</a-select-option>
-        </a-select>
-    </a-form-item>
-    <a-form-item label="Session Key"
-        v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'">
-        <a-input v-model.trim="inbound.stream.xhttp.sessionKey"
-            placeholder="x_session"></a-input>
-    </a-form-item>
-    <a-form-item label="Sequence Placement">
-        <a-select v-model="inbound.stream.xhttp.seqPlacement"
-            :dropdown-class-name="themeSwitcher.currentTheme">
-            <a-select-option value>Default (path)</a-select-option>
-            <a-select-option value="path">path</a-select-option>
-            <a-select-option value="header">header</a-select-option>
-            <a-select-option value="cookie">cookie</a-select-option>
-            <a-select-option value="query">query</a-select-option>
-        </a-select>
-    </a-form-item>
-    <a-form-item label="Sequence Key"
-        v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'">
-        <a-input v-model.trim="inbound.stream.xhttp.seqKey"
-            placeholder="x_seq"></a-input>
-    </a-form-item>
-    <a-form-item label="Uplink Data Placement"
-        v-if="inbound.stream.xhttp.mode === 'packet-up'">
-        <a-select v-model="inbound.stream.xhttp.uplinkDataPlacement"
-            :dropdown-class-name="themeSwitcher.currentTheme">
-            <a-select-option value>Default (body)</a-select-option>
-            <a-select-option value="body">body</a-select-option>
-            <a-select-option value="header">header</a-select-option>
-            <a-select-option value="cookie">cookie</a-select-option>
-            <a-select-option value="query">query</a-select-option>
-        </a-select>
-    </a-form-item>
-    <a-form-item label="Uplink Data Key"
-        v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
-        <a-input v-model.trim="inbound.stream.xhttp.uplinkDataKey"
-            placeholder="x_data"></a-input>
-    </a-form-item>
-    <a-form-item label="Uplink Chunk Size"
-        v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
-        <a-input-number v-model.number="inbound.stream.xhttp.uplinkChunkSize"
-            :min="0" placeholder="0 (unlimited)"></a-input-number>
-    </a-form-item>
-    <a-form-item label="No SSE Header">
-        <a-switch v-model="inbound.stream.xhttp.noSSEHeader"></a-switch>
-    </a-form-item>
+<a-form
+  :colon="false"
+  :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }"
+>
+  <a-form-item label='{{ i18n "host" }}'>
+    <a-input v-model.trim="inbound.stream.xhttp.host"></a-input>
+  </a-form-item>
+  <a-form-item label='{{ i18n "path" }}'>
+    <a-input v-model.trim="inbound.stream.xhttp.path"></a-input>
+  </a-form-item>
+  <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
+    <a-button
+      icon="plus"
+      size="small"
+      @click="inbound.stream.xhttp.addHeader('', '')"
+    ></a-button>
+  </a-form-item>
+  <a-form-item :wrapper-col="{span:24}">
+    <a-input-group
+      compact
+      v-for="(header, index) in inbound.stream.xhttp.headers"
+    >
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="header.name"
+        placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
+      >
+        <template slot="addonBefore" :style="{ margin: '0' }"
+          >[[ index+1 ]]</template
+        >
+      </a-input>
+      <a-input
+        :style="{ width: '50%' }"
+        v-model.trim="header.value"
+        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
+      >
+        <a-button
+          icon="minus"
+          slot="addonAfter"
+          size="small"
+          @click="inbound.stream.xhttp.removeHeader(index)"
+        ></a-button>
+      </a-input>
+    </a-input-group>
+  </a-form-item>
+  <a-form-item label="Mode">
+    <a-select
+      v-model="inbound.stream.xhttp.mode"
+      :style="{ width: '50%' }"
+      :dropdown-class-name="themeSwitcher.currentTheme"
+    >
+      <a-select-option v-for="key in MODE_OPTION" :value="key"
+        >[[ key ]]</a-select-option
+      >
+    </a-select>
+  </a-form-item>
+  <a-form-item
+    label="Max Buffered Upload"
+    v-if="inbound.stream.xhttp.mode === 'packet-up'"
+  >
+    <a-input-number
+      v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item
+    label="Max Upload Size (Byte)"
+    v-if="inbound.stream.xhttp.mode === 'packet-up'"
+  >
+    <a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
+  </a-form-item>
+  <a-form-item
+    label="Stream-Up Server"
+    v-if="inbound.stream.xhttp.mode === 'stream-up'"
+  >
+    <a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
+  </a-form-item>
+  <a-form-item label="Padding Bytes">
+    <a-input v-model.trim="inbound.stream.xhttp.xPaddingBytes"></a-input>
+  </a-form-item>
+  <a-form-item label="Padding Obfs Mode">
+    <a-switch v-model="inbound.stream.xhttp.xPaddingObfsMode"></a-switch>
+  </a-form-item>
+  <template v-if="inbound.stream.xhttp.xPaddingObfsMode">
+    <a-form-item label="Padding Key">
+      <a-input
+        v-model.trim="inbound.stream.xhttp.xPaddingKey"
+        placeholder="x_padding"
+      ></a-input>
+    </a-form-item>
+    <a-form-item label="Padding Header">
+      <a-input
+        v-model.trim="inbound.stream.xhttp.xPaddingHeader"
+        placeholder="X-Padding"
+      ></a-input>
+    </a-form-item>
+    <a-form-item label="Padding Placement">
+      <a-select
+        v-model="inbound.stream.xhttp.xPaddingPlacement"
+        :dropdown-class-name="themeSwitcher.currentTheme"
+      >
+        <a-select-option value>Default (queryInHeader)</a-select-option>
+        <a-select-option value="queryInHeader">queryInHeader</a-select-option>
+        <a-select-option value="header">header</a-select-option>
+        <a-select-option value="cookie">cookie</a-select-option>
+        <a-select-option value="query">query</a-select-option>
+      </a-select>
+    </a-form-item>
+    <a-form-item label="Padding Method">
+      <a-select
+        v-model="inbound.stream.xhttp.xPaddingMethod"
+        :dropdown-class-name="themeSwitcher.currentTheme"
+      >
+        <a-select-option value>Default (repeat-x)</a-select-option>
+        <a-select-option value="repeat-x">repeat-x</a-select-option>
+        <a-select-option value="tokenish">tokenish</a-select-option>
+      </a-select>
+    </a-form-item>
+  </template>
+  <a-form-item label="Uplink HTTP Method">
+    <a-select
+      v-model="inbound.stream.xhttp.uplinkHTTPMethod"
+      :dropdown-class-name="themeSwitcher.currentTheme"
+    >
+      <a-select-option value>Default (POST)</a-select-option>
+      <a-select-option value="POST">POST</a-select-option>
+      <a-select-option value="PUT">PUT</a-select-option>
+      <a-select-option value="GET">GET (packet-up only)</a-select-option>
+    </a-select>
+  </a-form-item>
+  <a-form-item label="Session Placement">
+    <a-select
+      v-model="inbound.stream.xhttp.sessionPlacement"
+      :dropdown-class-name="themeSwitcher.currentTheme"
+    >
+      <a-select-option value>Default (path)</a-select-option>
+      <a-select-option value="path">path</a-select-option>
+      <a-select-option value="header">header</a-select-option>
+      <a-select-option value="cookie">cookie</a-select-option>
+      <a-select-option value="query">query</a-select-option>
+    </a-select>
+  </a-form-item>
+  <a-form-item
+    label="Session Key"
+    v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'"
+  >
+    <a-input
+      v-model.trim="inbound.stream.xhttp.sessionKey"
+      placeholder="x_session"
+    ></a-input>
+  </a-form-item>
+  <a-form-item label="Sequence Placement">
+    <a-select
+      v-model="inbound.stream.xhttp.seqPlacement"
+      :dropdown-class-name="themeSwitcher.currentTheme"
+    >
+      <a-select-option value>Default (path)</a-select-option>
+      <a-select-option value="path">path</a-select-option>
+      <a-select-option value="header">header</a-select-option>
+      <a-select-option value="cookie">cookie</a-select-option>
+      <a-select-option value="query">query</a-select-option>
+    </a-select>
+  </a-form-item>
+  <a-form-item
+    label="Sequence Key"
+    v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'"
+  >
+    <a-input
+      v-model.trim="inbound.stream.xhttp.seqKey"
+      placeholder="x_seq"
+    ></a-input>
+  </a-form-item>
+  <a-form-item
+    label="Uplink Data Placement"
+    v-if="inbound.stream.xhttp.mode === 'packet-up'"
+  >
+    <a-select
+      v-model="inbound.stream.xhttp.uplinkDataPlacement"
+      :dropdown-class-name="themeSwitcher.currentTheme"
+    >
+      <a-select-option value>Default (body)</a-select-option>
+      <a-select-option value="body">body</a-select-option>
+      <a-select-option value="header">header</a-select-option>
+      <a-select-option value="cookie">cookie</a-select-option>
+      <a-select-option value="query">query</a-select-option>
+    </a-select>
+  </a-form-item>
+  <a-form-item
+    label="Uplink Data Key"
+    v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'"
+  >
+    <a-input
+      v-model.trim="inbound.stream.xhttp.uplinkDataKey"
+      placeholder="x_data"
+    ></a-input>
+  </a-form-item>
+  <a-form-item
+    label="Uplink Chunk Size"
+    v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'"
+  >
+    <a-input-number
+      v-model.number="inbound.stream.xhttp.uplinkChunkSize"
+      :min="0"
+      placeholder="0 (unlimited)"
+    ></a-input-number>
+  </a-form-item>
+  <a-form-item label="No SSE Header">
+    <a-switch v-model="inbound.stream.xhttp.noSSEHeader"></a-switch>
+  </a-form-item>
 </a-form>
 </a-form>
-{{end}}
+{{end}}

+ 12 - 9
web/html/form/tls_settings.html

@@ -2,15 +2,18 @@
 <!-- tls enable -->
 <!-- tls enable -->
 <a-form v-if="inbound.canEnableTls()" :colon="false"
 <a-form v-if="inbound.canEnableTls()" :colon="false"
   :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-  <a-divider :style="{ margin: '3px 0' }"></a-divider>
-  <a-form-item label='{{ i18n "security" }}'>
-    <a-radio-group v-model="inbound.stream.security" button-style="solid">
-      <a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
-      <a-radio-button v-if="inbound.canEnableReality()"
-        value="reality">Reality</a-radio-button>
-      <a-radio-button value="tls">TLS</a-radio-button>
-    </a-radio-group>
-  </a-form-item>
+  <template v-if="inbound.protocol !== Protocols.HYSTERIA">
+    <a-divider :style="{ margin: '3px 0' }"></a-divider>
+    <a-form-item label='{{ i18n "security" }}'>
+      <a-radio-group v-model="inbound.stream.security" button-style="solid">
+        <a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
+        <a-radio-button v-if="inbound.canEnableReality()"
+          value="reality">Reality</a-radio-button>
+        <a-radio-button value="tls">TLS</a-radio-button>
+      </a-radio-group>
+    </a-form-item>
+  </template>
+  <template v-else><a-divider style="margin:0;">TLS</a-divider></template>
 
 
   <!-- tls settings -->
   <!-- tls settings -->
   <template v-if="inbound.stream.isTls">
   <template v-if="inbound.stream.isTls">

+ 331 - 152
web/html/inbounds.html

@@ -2,14 +2,18 @@
 {{ template "page/head_end" .}}
 {{ template "page/head_end" .}}
 
 
 {{ template "page/body_start" .}}
 {{ template "page/body_start" .}}
-<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' inbounds-page'">
+<a-layout id="app" v-cloak
+  :class="themeSwitcher.currentTheme + ' inbounds-page'">
   <a-sidebar></a-sidebar>
   <a-sidebar></a-sidebar>
   <a-layout id="content-layout">
   <a-layout id="content-layout">
     <a-layout-content>
     <a-layout-content>
-      <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}' size="large">
+      <a-spin :spinning="loadingStates.spinning" :delay="500"
+        tip='{{ i18n "loading"}}' size="large">
         <transition name="list" appear>
         <transition name="list" appear>
-          <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
-            message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
+          <a-alert type="error" v-if="showAlert && loadingStates.fetched"
+            :style="{ marginBottom: '10px' }"
+            message='{{ i18n "secAlertTitle" }}' color="red"
+            description='{{ i18n "secAlertSsl" }}' show-icon closable>
           </a-alert>
           </a-alert>
         </transition>
         </transition>
         <transition name="list" appear>
         <transition name="list" appear>
@@ -21,7 +25,8 @@
               <a-card size="small" :style="{ padding: '16px' }" hoverable>
               <a-card size="small" :style="{ padding: '16px' }" hoverable>
                 <a-row>
                 <a-row>
                   <a-col :sm="12" :md="5">
                   <a-col :sm="12" :md="5">
-                    <a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}'
+                    <a-custom-statistic
+                      title='{{ i18n "pages.inbounds.totalDownUp" }}'
                       :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
                       :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
                       <template #prefix>
                       <template #prefix>
                         <a-icon type="swap"></a-icon>
                         <a-icon type="swap"></a-icon>
@@ -29,7 +34,8 @@
                     </a-custom-statistic>
                     </a-custom-statistic>
                   </a-col>
                   </a-col>
                   <a-col :sm="12" :md="5">
                   <a-col :sm="12" :md="5">
-                    <a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}'
+                    <a-custom-statistic
+                      title='{{ i18n "pages.inbounds.totalUsage" }}'
                       :value="SizeFormatter.sizeFormat(total.up + total.down)"
                       :value="SizeFormatter.sizeFormat(total.up + total.down)"
                       :style="{ marginTop: isMobile ? '10px' : 0 }">
                       :style="{ marginTop: isMobile ? '10px' : 0 }">
                       <template #prefix>
                       <template #prefix>
@@ -38,15 +44,19 @@
                     </a-custom-statistic>
                     </a-custom-statistic>
                   </a-col>
                   </a-col>
                   <a-col :sm="12" :md="5">
                   <a-col :sm="12" :md="5">
-                    <a-custom-statistic title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}'
-                      :value="SizeFormatter.sizeFormat(total.allTime)" :style="{ marginTop: isMobile ? '10px' : 0 }">
+                    <a-custom-statistic
+                      title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}'
+                      :value="SizeFormatter.sizeFormat(total.allTime)"
+                      :style="{ marginTop: isMobile ? '10px' : 0 }">
                       <template #prefix>
                       <template #prefix>
                         <a-icon type="history"></a-icon>
                         <a-icon type="history"></a-icon>
                       </template>
                       </template>
                     </a-custom-statistic>
                     </a-custom-statistic>
                   </a-col>
                   </a-col>
                   <a-col :sm="12" :md="5">
                   <a-col :sm="12" :md="5">
-                    <a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length"
+                    <a-custom-statistic
+                      title='{{ i18n "pages.inbounds.inboundCount" }}'
+                      :value="dbInbounds.length"
                       :style="{ marginTop: isMobile ? '10px' : 0 }">
                       :style="{ marginTop: isMobile ? '10px' : 0 }">
                       <template #prefix>
                       <template #prefix>
                         <a-icon type="bars"></a-icon>
                         <a-icon type="bars"></a-icon>
@@ -60,33 +70,50 @@
                         <a-space direction="horizontal">
                         <a-space direction="horizontal">
                           <a-icon type="team"></a-icon>
                           <a-icon type="team"></a-icon>
                           <div>
                           <div>
-                            <a-back-top :target="() => document.getElementById('content-layout')"
+                            <a-back-top
+                              :target="() => document.getElementById('content-layout')"
                               visibility-height="200"></a-back-top>
                               visibility-height="200"></a-back-top>
                             <a-tag color="green">[[ total.clients ]]</a-tag>
                             <a-tag color="green">[[ total.clients ]]</a-tag>
-                            <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                            <a-popover title='{{ i18n "disabled" }}'
+                              :overlay-class-name="themeSwitcher.currentTheme">
                               <template slot="content">
                               <template slot="content">
-                                <div v-for="clientEmail in total.deactive"><span>[[ clientEmail ]]</span></div>
+                                <div
+                                  v-for="clientEmail in total.deactive"><span>[[
+                                    clientEmail ]]</span></div>
                               </template>
                               </template>
-                              <a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
+                              <a-tag v-if="total.deactive.length">[[
+                                total.deactive.length ]]</a-tag>
                             </a-popover>
                             </a-popover>
-                            <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                            <a-popover title='{{ i18n "depleted" }}'
+                              :overlay-class-name="themeSwitcher.currentTheme">
                               <template slot="content">
                               <template slot="content">
-                                <div v-for="clientEmail in total.depleted"><span>[[ clientEmail ]]</span></div>
+                                <div
+                                  v-for="clientEmail in total.depleted"><span>[[
+                                    clientEmail ]]</span></div>
                               </template>
                               </template>
-                              <a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
+                              <a-tag color="red" v-if="total.depleted.length">[[
+                                total.depleted.length ]]</a-tag>
                             </a-popover>
                             </a-popover>
                             <a-popover title='{{ i18n "depletingSoon" }}'
                             <a-popover title='{{ i18n "depletingSoon" }}'
                               :overlay-class-name="themeSwitcher.currentTheme">
                               :overlay-class-name="themeSwitcher.currentTheme">
                               <template slot="content">
                               <template slot="content">
-                                <div v-for="clientEmail in total.expiring"><span>[[ clientEmail ]]</span></div>
+                                <div
+                                  v-for="clientEmail in total.expiring"><span>[[
+                                    clientEmail ]]</span></div>
                               </template>
                               </template>
-                              <a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
+                              <a-tag color="orange"
+                                v-if="total.expiring.length">[[
+                                total.expiring.length ]]</a-tag>
                             </a-popover>
                             </a-popover>
-                            <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                            <a-popover title='{{ i18n "online" }}'
+                              :overlay-class-name="themeSwitcher.currentTheme">
                               <template slot="content">
                               <template slot="content">
-                                <div v-for="clientEmail in onlineClients"><span>[[ clientEmail ]]</span></div>
+                                <div
+                                  v-for="clientEmail in onlineClients"><span>[[
+                                    clientEmail ]]</span></div>
                               </template>
                               </template>
-                              <a-tag color="blue" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag>
+                              <a-tag color="blue" v-if="onlineClients.length">[[
+                                onlineClients.length ]]</a-tag>
                             </a-popover>
                             </a-popover>
                           </div>
                           </div>
                         </a-space>
                         </a-space>
@@ -100,14 +127,18 @@
               <a-card hoverable>
               <a-card hoverable>
                 <template #title>
                 <template #title>
                   <a-space direction="horizontal">
                   <a-space direction="horizontal">
-                    <a-button type="primary" icon="plus" @click="openAddInbound">
-                      <template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template>
+                    <a-button type="primary" icon="plus"
+                      @click="openAddInbound">
+                      <template v-if="!isMobile">{{ i18n
+                        "pages.inbounds.addInbound" }}</template>
                     </a-button>
                     </a-button>
                     <a-dropdown :trigger="['click']">
                     <a-dropdown :trigger="['click']">
                       <a-button type="primary" icon="menu">
                       <a-button type="primary" icon="menu">
-                        <template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
+                        <template v-if="!isMobile">{{ i18n
+                          "pages.inbounds.generalActions" }}</template>
                       </a-button>
                       </a-button>
-                      <a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
+                      <a-menu slot="overlay" @click="a => generalActions(a)"
+                        :theme="themeSwitcher.currentTheme">
                         <a-menu-item key="import">
                         <a-menu-item key="import">
                           <a-icon type="import"></a-icon>
                           <a-icon type="import"></a-icon>
                           {{ i18n "pages.inbounds.importInbound" }}
                           {{ i18n "pages.inbounds.importInbound" }}
@@ -118,7 +149,8 @@
                         </a-menu-item>
                         </a-menu-item>
                         <a-menu-item key="subs" v-if="subSettings.enable">
                         <a-menu-item key="subs" v-if="subSettings.enable">
                           <a-icon type="export"></a-icon>
                           <a-icon type="export"></a-icon>
-                          {{ i18n "pages.inbounds.export" }} - {{ i18n "pages.settings.subSettings" }}
+                          {{ i18n "pages.inbounds.export" }} - {{ i18n
+                          "pages.settings.subSettings" }}
                         </a-menu-item>
                         </a-menu-item>
                         <a-menu-item key="resetInbounds">
                         <a-menu-item key="resetInbounds">
                           <a-icon type="reload"></a-icon>
                           <a-icon type="reload"></a-icon>
@@ -128,7 +160,8 @@
                           <a-icon type="file-done"></a-icon>
                           <a-icon type="file-done"></a-icon>
                           {{ i18n "pages.inbounds.resetAllClientTraffics" }}
                           {{ i18n "pages.inbounds.resetAllClientTraffics" }}
                         </a-menu-item>
                         </a-menu-item>
-                        <a-menu-item key="delDepletedClients" :style="{ color: '#FF4D4F' }">
+                        <a-menu-item key="delDepletedClients"
+                          :style="{ color: '#FF4D4F' }">
                           <a-icon type="rest"></a-icon>
                           <a-icon type="rest"></a-icon>
                           {{ i18n "pages.inbounds.delDepletedClients" }}
                           {{ i18n "pages.inbounds.delDepletedClients" }}
                         </a-menu-item>
                         </a-menu-item>
@@ -138,20 +171,28 @@
                 </template>
                 </template>
                 <template #extra>
                 <template #extra>
                   <a-button-group>
                   <a-button-group>
-                    <a-button icon="sync" @click="manualRefresh" :loading="refreshing"></a-button>
-                    <a-popover placement="bottomRight" trigger="click" :overlay-class-name="themeSwitcher.currentTheme">
+                    <a-button icon="sync" @click="manualRefresh"
+                      :loading="refreshing"></a-button>
+                    <a-popover placement="bottomRight" trigger="click"
+                      :overlay-class-name="themeSwitcher.currentTheme">
                       <template #title>
                       <template #title>
                         <div class="ant-custom-popover-title">
                         <div class="ant-custom-popover-title">
-                          <a-switch v-model="isRefreshEnabled" @change="toggleRefresh" size="small"></a-switch>
+                          <a-switch v-model="isRefreshEnabled"
+                            @change="toggleRefresh" size="small"></a-switch>
                           <span>{{ i18n "pages.inbounds.autoRefresh" }}</span>
                           <span>{{ i18n "pages.inbounds.autoRefresh" }}</span>
                         </div>
                         </div>
                       </template>
                       </template>
                       <template #content>
                       <template #content>
                         <a-space direction="vertical">
                         <a-space direction="vertical">
-                          <span>{{ i18n "pages.inbounds.autoRefreshInterval" }}</span>
-                          <a-select v-model="refreshInterval" :disabled="!isRefreshEnabled" :style="{ width: '100%' }"
-                            @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>
+                          <span>{{ i18n "pages.inbounds.autoRefreshInterval"
+                            }}</span>
+                          <a-select v-model="refreshInterval"
+                            :disabled="!isRefreshEnabled"
+                            :style="{ width: '100%' }"
+                            @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-select>
                         </a-space>
                         </a-space>
                       </template>
                       </template>
@@ -160,27 +201,38 @@
                   </a-button-group>
                   </a-button-group>
                 </template>
                 </template>
                 <a-space direction="vertical">
                 <a-space direction="vertical">
-                  <div :style="isMobile ? {} : { display: 'flex', alignItems: 'center', justifyContent: 'flex-start' }">
+                  <div
+                    :style="isMobile ? {} : { display: 'flex', alignItems: 'center', justifyContent: 'flex-start' }">
                     <a-switch v-model="enableFilter"
                     <a-switch v-model="enableFilter"
                       :style="isMobile ? { marginBottom: '.5rem', display: 'flex' } : { marginRight: '.5rem' }"
                       :style="isMobile ? { marginBottom: '.5rem', display: 'flex' } : { marginRight: '.5rem' }"
                       @change="toggleFilter">
                       @change="toggleFilter">
                       <a-icon slot="checkedChildren" type="search"></a-icon>
                       <a-icon slot="checkedChildren" type="search"></a-icon>
                       <a-icon slot="unCheckedChildren" type="filter"></a-icon>
                       <a-icon slot="unCheckedChildren" type="filter"></a-icon>
                     </a-switch>
                     </a-switch>
-                    <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus
-                      :style="{ maxWidth: '300px' }" :size="isMobile ? 'small' : ''"></a-input>
-                    <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid"
+                    <a-input v-if="!enableFilter" v-model.lazy="searchKey"
+                      placeholder='{{ i18n "search" }}' autofocus
+                      :style="{ maxWidth: '300px' }"
+                      :size="isMobile ? 'small' : ''"></a-input>
+                    <a-radio-group v-if="enableFilter" v-model="filterBy"
+                      @change="filterInbounds" button-style="solid"
                       :size="isMobile ? 'small' : ''">
                       :size="isMobile ? 'small' : ''">
-                      <a-radio-button value="">{{ i18n "none" }}</a-radio-button>
-                      <a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
-                      <a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
-                      <a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
-                      <a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
+                      <a-radio-button value>{{ i18n "none" }}</a-radio-button>
+                      <a-radio-button value="deactive">{{ i18n "disabled"
+                        }}</a-radio-button>
+                      <a-radio-button value="depleted">{{ i18n "depleted"
+                        }}</a-radio-button>
+                      <a-radio-button value="expiring">{{ i18n "depletingSoon"
+                        }}</a-radio-button>
+                      <a-radio-button value="online">{{ i18n "online"
+                        }}</a-radio-button>
                     </a-radio-group>
                     </a-radio-group>
                   </div>
                   </div>
-                  <a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id"
-                    :data-source="searchedInbounds" :scroll="isMobile ? {} : { x: 1000 }"
-                    :pagination=pagination(searchedInbounds) :expand-icon-as-cell="false" :expand-row-by-click="false"
+                  <a-table :columns="isMobile ? mobileColumns : columns"
+                    :row-key="dbInbound => dbInbound.id"
+                    :data-source="searchedInbounds"
+                    :scroll="isMobile ? {} : { x: 1000 }"
+                    :pagination=pagination(searchedInbounds)
+                    :expand-icon-as-cell="false" :expand-row-by-click="false"
                     :expand-icon-column-index="0" :indent-size="0"
                     :expand-icon-column-index="0" :indent-size="0"
                     :row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
                     :row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
                     :style="{ marginTop: '10px' }"
                     :style="{ marginTop: '10px' }"
@@ -189,7 +241,8 @@
                       <a-dropdown :trigger="['click']">
                       <a-dropdown :trigger="['click']">
                         <a-icon @click="e => e.preventDefault()" type="more"
                         <a-icon @click="e => e.preventDefault()" type="more"
                           :style="{ fontSize: '20px', textDecoration: 'solid' }"></a-icon>
                           :style="{ fontSize: '20px', textDecoration: 'solid' }"></a-icon>
-                        <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)"
+                        <a-menu slot="overlay"
+                          @click="a => clickAction(a, dbInbound)"
                           :theme="themeSwitcher.currentTheme">
                           :theme="themeSwitcher.currentTheme">
                           <a-menu-item key="edit">
                           <a-menu-item key="edit">
                             <a-icon type="edit"></a-icon>
                             <a-icon type="edit"></a-icon>
@@ -211,7 +264,8 @@
                             </a-menu-item>
                             </a-menu-item>
                             <a-menu-item key="resetClients">
                             <a-menu-item key="resetClients">
                               <a-icon type="file-done"></a-icon>
                               <a-icon type="file-done"></a-icon>
-                              {{ i18n "pages.inbounds.resetInboundClientTraffics"}}
+                              {{ i18n
+                              "pages.inbounds.resetInboundClientTraffics"}}
                             </a-menu-item>
                             </a-menu-item>
                             <a-menu-item key="export">
                             <a-menu-item key="export">
                               <a-icon type="export"></a-icon>
                               <a-icon type="export"></a-icon>
@@ -219,9 +273,11 @@
                             </a-menu-item>
                             </a-menu-item>
                             <a-menu-item key="subs" v-if="subSettings.enable">
                             <a-menu-item key="subs" v-if="subSettings.enable">
                               <a-icon type="export"></a-icon>
                               <a-icon type="export"></a-icon>
-                              {{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}
+                              {{ i18n "pages.inbounds.export"}} - {{ i18n
+                              "pages.settings.subSettings" }}
                             </a-menu-item>
                             </a-menu-item>
-                            <a-menu-item key="delDepletedClients" :style="{ color: '#FF4D4F' }">
+                            <a-menu-item key="delDepletedClients"
+                              :style="{ color: '#FF4D4F' }">
                               <a-icon type="rest"></a-icon>
                               <a-icon type="rest"></a-icon>
                               {{ i18n "pages.inbounds.delDepletedClients" }}
                               {{ i18n "pages.inbounds.delDepletedClients" }}
                             </a-menu-item>
                             </a-menu-item>
@@ -237,10 +293,12 @@
                             {{ i18n "pages.inbounds.exportInbound" }}
                             {{ i18n "pages.inbounds.exportInbound" }}
                           </a-menu-item>
                           </a-menu-item>
                           <a-menu-item key="resetTraffic">
                           <a-menu-item key="resetTraffic">
-                            <a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
+                            <a-icon type="retweet"></a-icon> {{ i18n
+                            "pages.inbounds.resetTraffic" }}
                           </a-menu-item>
                           </a-menu-item>
                           <a-menu-item key="clone">
                           <a-menu-item key="clone">
-                            <a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
+                            <a-icon type="block"></a-icon> {{ i18n
+                            "pages.inbounds.clone"}}
                           </a-menu-item>
                           </a-menu-item>
                           <a-menu-item key="delete">
                           <a-menu-item key="delete">
                             <span :style="{ color: '#FF4D4F' }">
                             <span :style="{ color: '#FF4D4F' }">
@@ -256,26 +314,38 @@
                       </a-dropdown>
                       </a-dropdown>
                     </template>
                     </template>
                     <template slot="protocol" slot-scope="text, dbInbound">
                     <template slot="protocol" slot-scope="text, dbInbound">
-                      <a-tag :style="{ margin: '0' }" color="purple">[[ dbInbound.protocol ]]</a-tag>
-                      <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
-                        <a-tag :style="{ margin: '0' }" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
-                        <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls"
+                      <a-tag :style="{ margin: '0' }" color="purple">[[
+                        dbInbound.protocol ]]</a-tag>
+                      <template
+                        v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
+                        <a-tag :style="{ margin: '0' }" color="green">[[
+                          dbInbound.toInbound().stream.network ]]</a-tag>
+                        <a-tag :style="{ margin: '0' }"
+                          v-if="dbInbound.toInbound().stream.isTls"
                           color="blue">TLS</a-tag>
                           color="blue">TLS</a-tag>
-                        <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality"
+                        <a-tag :style="{ margin: '0' }"
+                          v-if="dbInbound.toInbound().stream.isReality"
                           color="blue">Reality</a-tag>
                           color="blue">Reality</a-tag>
                       </template>
                       </template>
                     </template>
                     </template>
                     <template slot="clients" slot-scope="text, dbInbound">
                     <template slot="clients" slot-scope="text, dbInbound">
                       <template v-if="clientCount[dbInbound.id]">
                       <template v-if="clientCount[dbInbound.id]">
-                        <a-tag :style="{ margin: '0' }" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
-                        <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                        <a-tag :style="{ margin: '0' }" color="green">[[
+                          clientCount[dbInbound.id].clients ]]</a-tag>
+                        <a-popover title='{{ i18n "disabled" }}'
+                          :overlay-class-name="themeSwitcher.currentTheme">
                           <template slot="content">
                           <template slot="content">
-                            <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail"
+                            <div
+                              v-for="clientEmail in clientCount[dbInbound.id].deactive"
+                              :key="clientEmail"
                               class="client-popup-item">
                               class="client-popup-item">
                               <span>[[ clientEmail ]]</span>
                               <span>[[ clientEmail ]]</span>
-                              <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
+                              <a-tooltip
+                                :overlay-class-name="themeSwitcher.currentTheme">
                                 <template #title>
                                 <template #title>
-                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
+                                  [[
+                                  clientCount[dbInbound.id].comments.get(clientEmail)
+                                  ]]
                                 </template>
                                 </template>
                                 <a-icon type="message"
                                 <a-icon type="message"
                                   v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                   v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
@@ -286,84 +356,113 @@
                             v-if="clientCount[dbInbound.id].deactive.length">[[
                             v-if="clientCount[dbInbound.id].deactive.length">[[
                             clientCount[dbInbound.id].deactive.length ]]</a-tag>
                             clientCount[dbInbound.id].deactive.length ]]</a-tag>
                         </a-popover>
                         </a-popover>
-                        <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                        <a-popover title='{{ i18n "depleted" }}'
+                          :overlay-class-name="themeSwitcher.currentTheme">
                           <template slot="content">
                           <template slot="content">
-                            <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail"
+                            <div
+                              v-for="clientEmail in clientCount[dbInbound.id].depleted"
+                              :key="clientEmail"
                               class="client-popup-item">
                               class="client-popup-item">
                               <span>[[ clientEmail ]]</span>
                               <span>[[ clientEmail ]]</span>
-                              <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
+                              <a-tooltip
+                                :overlay-class-name="themeSwitcher.currentTheme">
                                 <template #title>
                                 <template #title>
-                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
+                                  [[
+                                  clientCount[dbInbound.id].comments.get(clientEmail)
+                                  ]]
                                 </template>
                                 </template>
                                 <a-icon type="message"
                                 <a-icon type="message"
                                   v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                   v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                               </a-tooltip>
                               </a-tooltip>
                             </div>
                             </div>
                           </template>
                           </template>
-                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red"
+                          <a-tag :style="{ margin: '0', padding: '0 2px' }"
+                            color="red"
                             v-if="clientCount[dbInbound.id].depleted.length">[[
                             v-if="clientCount[dbInbound.id].depleted.length">[[
                             clientCount[dbInbound.id].depleted.length ]]</a-tag>
                             clientCount[dbInbound.id].depleted.length ]]</a-tag>
                         </a-popover>
                         </a-popover>
-                        <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                        <a-popover title='{{ i18n "depletingSoon" }}'
+                          :overlay-class-name="themeSwitcher.currentTheme">
                           <template slot="content">
                           <template slot="content">
-                            <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail"
+                            <div
+                              v-for="clientEmail in clientCount[dbInbound.id].expiring"
+                              :key="clientEmail"
                               class="client-popup-item">
                               class="client-popup-item">
                               <span>[[ clientEmail ]]</span>
                               <span>[[ clientEmail ]]</span>
-                              <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
+                              <a-tooltip
+                                :overlay-class-name="themeSwitcher.currentTheme">
                                 <template #title>
                                 <template #title>
-                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
+                                  [[
+                                  clientCount[dbInbound.id].comments.get(clientEmail)
+                                  ]]
                                 </template>
                                 </template>
                                 <a-icon type="message"
                                 <a-icon type="message"
                                   v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                   v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                               </a-tooltip>
                               </a-tooltip>
                             </div>
                             </div>
                           </template>
                           </template>
-                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange"
+                          <a-tag :style="{ margin: '0', padding: '0 2px' }"
+                            color="orange"
                             v-if="clientCount[dbInbound.id].expiring.length">[[
                             v-if="clientCount[dbInbound.id].expiring.length">[[
                             clientCount[dbInbound.id].expiring.length ]]</a-tag>
                             clientCount[dbInbound.id].expiring.length ]]</a-tag>
                         </a-popover>
                         </a-popover>
-                        <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                        <a-popover title='{{ i18n "online" }}'
+                          :overlay-class-name="themeSwitcher.currentTheme">
                           <template slot="content">
                           <template slot="content">
-                            <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail"
+                            <div
+                              v-for="clientEmail in clientCount[dbInbound.id].online"
+                              :key="clientEmail"
                               class="client-popup-item">
                               class="client-popup-item">
                               <span>[[ clientEmail ]]</span>
                               <span>[[ clientEmail ]]</span>
-                              <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
+                              <a-tooltip
+                                :overlay-class-name="themeSwitcher.currentTheme">
                                 <template #title>
                                 <template #title>
-                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
+                                  [[
+                                  clientCount[dbInbound.id].comments.get(clientEmail)
+                                  ]]
                                 </template>
                                 </template>
                                 <a-icon type="message"
                                 <a-icon type="message"
                                   v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                   v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                               </a-tooltip>
                               </a-tooltip>
                             </div>
                             </div>
                           </template>
                           </template>
-                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="blue"
-                            v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length
+                          <a-tag :style="{ margin: '0', padding: '0 2px' }"
+                            color="blue"
+                            v-if="clientCount[dbInbound.id].online.length">[[
+                            clientCount[dbInbound.id].online.length
                             ]]</a-tag>
                             ]]</a-tag>
                         </a-popover>
                         </a-popover>
                       </template>
                       </template>
                     </template>
                     </template>
                     <template slot="traffic" slot-scope="text, dbInbound">
                     <template slot="traffic" slot-scope="text, dbInbound">
-                      <a-popover :overlay-class-name="themeSwitcher.currentTheme">
+                      <a-popover
+                        :overlay-class-name="themeSwitcher.currentTheme">
                         <template slot="content">
                         <template slot="content">
                           <table cellpadding="2" width="100%">
                           <table cellpadding="2" width="100%">
                             <tr>
                             <tr>
-                              <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td>
-                              <td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td>
+                              <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up)
+                                ]]</td>
+                              <td>↓[[ SizeFormatter.sizeFormat(dbInbound.down)
+                                ]]</td>
                             </tr>
                             </tr>
-                            <tr v-if="dbInbound.total > 0 &&  dbInbound.up + dbInbound.down < dbInbound.total">
+                            <tr
+                              v-if="dbInbound.total > 0 &&  dbInbound.up + dbInbound.down < dbInbound.total">
                               <td>{{ i18n "remained" }}</td>
                               <td>{{ i18n "remained" }}</td>
-                              <td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
+                              <td>[[ SizeFormatter.sizeFormat(dbInbound.total -
+                                dbInbound.up - dbInbound.down) ]]</td>
                             </tr>
                             </tr>
                           </table>
                           </table>
                         </template>
                         </template>
                         <a-tag
                         <a-tag
                           :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
                           :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
-                          [[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /
+                          [[ SizeFormatter.sizeFormat(dbInbound.up +
+                          dbInbound.down) ]] /
                           <template v-if="dbInbound.total > 0">
                           <template v-if="dbInbound.total > 0">
                             [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
                             [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
                           </template>
                           </template>
                           <template v-else>
                           <template v-else>
-                            <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
+                            <svg height="10px" width="14px"
+                              viewBox="0 0 640 512" fill="currentColor">
                               <path
                               <path
                                 d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
                                 d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
                                 fill="currentColor"></path>
                                 fill="currentColor"></path>
@@ -372,25 +471,30 @@
                         </a-tag>
                         </a-tag>
                       </a-popover>
                       </a-popover>
                     </template>
                     </template>
-                    <template slot="allTimeInbound" slot-scope="text, dbInbound">
-                      <a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0) ]]</a-tag>
+                    <template slot="allTimeInbound"
+                      slot-scope="text, dbInbound">
+                      <a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0)
+                        ]]</a-tag>
                     </template>
                     </template>
                     <template slot="enable" slot-scope="text, dbInbound">
                     <template slot="enable" slot-scope="text, dbInbound">
                       <a-switch v-model="dbInbound.enable"
                       <a-switch v-model="dbInbound.enable"
                         @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
                         @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
                     </template>
                     </template>
                     <template slot="expiryTime" slot-scope="text, dbInbound">
                     <template slot="expiryTime" slot-scope="text, dbInbound">
-                      <a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
+                      <a-popover v-if="dbInbound.expiryTime > 0"
+                        :overlay-class-name="themeSwitcher.currentTheme">
                         <template slot="content">
                         <template slot="content">
                           [[ IntlUtil.formatDate(dbInbound.expiryTime) ]]
                           [[ IntlUtil.formatDate(dbInbound.expiryTime) ]]
                         </template>
                         </template>
                         <a-tag :style="{ minWidth: '50px' }"
                         <a-tag :style="{ minWidth: '50px' }"
                           :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
                           :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
-                          [[ IntlUtil.formatRelativeTime(dbInbound.expiryTime) ]]
+                          [[ IntlUtil.formatRelativeTime(dbInbound.expiryTime)
+                          ]]
                         </a-tag>
                         </a-tag>
                       </a-popover>
                       </a-popover>
                       <a-tag v-else color="purple" class="infinite-tag">
                       <a-tag v-else color="purple" class="infinite-tag">
-                        <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
+                        <svg height="10px" width="14px" viewBox="0 0 640 512"
+                          fill="currentColor">
                           <path
                           <path
                             d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
                             d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
                             fill="currentColor"></path>
                             fill="currentColor"></path>
@@ -398,21 +502,28 @@
                       </a-tag>
                       </a-tag>
                     </template>
                     </template>
                     <template slot="info" slot-scope="text, dbInbound">
                     <template slot="info" slot-scope="text, dbInbound">
-                      <a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme"
+                      <a-popover placement="bottomRight"
+                        :overlay-class-name="themeSwitcher.currentTheme"
                         trigger="click">
                         trigger="click">
                         <template slot="content">
                         <template slot="content">
                           <table cellpadding="2">
                           <table cellpadding="2">
                             <tr>
                             <tr>
                               <td>{{ i18n "pages.inbounds.protocol" }}</td>
                               <td>{{ i18n "pages.inbounds.protocol" }}</td>
                               <td>
                               <td>
-                                <a-tag :style="{ margin: '0' }" color="purple">[[ dbInbound.protocol ]]</a-tag>
+                                <a-tag :style="{ margin: '0' }"
+                                  color="purple">[[ dbInbound.protocol
+                                  ]]</a-tag>
                                 <template
                                 <template
                                   v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
                                   v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
-                                  <a-tag :style="{ margin: '0' }" color="blue">[[ dbInbound.toInbound().stream.network
+                                  <a-tag :style="{ margin: '0' }"
+                                    color="blue">[[
+                                    dbInbound.toInbound().stream.network
                                     ]]</a-tag>
                                     ]]</a-tag>
-                                  <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls"
+                                  <a-tag :style="{ margin: '0' }"
+                                    v-if="dbInbound.toInbound().stream.isTls"
                                     color="green">tls</a-tag>
                                     color="green">tls</a-tag>
-                                  <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality"
+                                  <a-tag :style="{ margin: '0' }"
+                                    v-if="dbInbound.toInbound().stream.isReality"
                                     color="green">reality</a-tag>
                                     color="green">reality</a-tag>
                                 </template>
                                 </template>
                               </td>
                               </td>
@@ -424,111 +535,156 @@
                             <tr v-if="clientCount[dbInbound.id]">
                             <tr v-if="clientCount[dbInbound.id]">
                               <td>{{ i18n "clients" }}</td>
                               <td>{{ i18n "clients" }}</td>
                               <td>
                               <td>
-                                <a-tag :style="{ margin: '0' }" color="blue">[[ clientCount[dbInbound.id].clients
+                                <a-tag :style="{ margin: '0' }" color="blue">[[
+                                  clientCount[dbInbound.id].clients
                                   ]]</a-tag>
                                   ]]</a-tag>
                                 <a-popover title='{{ i18n "disabled" }}'
                                 <a-popover title='{{ i18n "disabled" }}'
                                   :overlay-class-name="themeSwitcher.currentTheme">
                                   :overlay-class-name="themeSwitcher.currentTheme">
                                   <template slot="content">
                                   <template slot="content">
-                                    <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail"
+                                    <div
+                                      v-for="clientEmail in clientCount[dbInbound.id].deactive"
+                                      :key="clientEmail"
                                       class="client-popup-item">
                                       class="client-popup-item">
                                       <span>[[ clientEmail ]]</span>
                                       <span>[[ clientEmail ]]</span>
-                                      <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
+                                      <a-tooltip
+                                        :overlay-class-name="themeSwitcher.currentTheme">
                                         <template #title>
                                         <template #title>
-                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
+                                          [[
+                                          clientCount[dbInbound.id].comments.get(clientEmail)
+                                          ]]
                                         </template>
                                         </template>
                                         <a-icon type="message"
                                         <a-icon type="message"
                                           v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                           v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                       </a-tooltip>
                                       </a-tooltip>
                                     </div>
                                     </div>
                                   </template>
                                   </template>
-                                  <a-tag :style="{ margin: '0', padding: '0 2px' }"
+                                  <a-tag
+                                    :style="{ margin: '0', padding: '0 2px' }"
                                     v-if="clientCount[dbInbound.id].deactive.length">[[
                                     v-if="clientCount[dbInbound.id].deactive.length">[[
-                                    clientCount[dbInbound.id].deactive.length ]]</a-tag>
+                                    clientCount[dbInbound.id].deactive.length
+                                    ]]</a-tag>
                                 </a-popover>
                                 </a-popover>
                                 <a-popover title='{{ i18n "depleted" }}'
                                 <a-popover title='{{ i18n "depleted" }}'
                                   :overlay-class-name="themeSwitcher.currentTheme">
                                   :overlay-class-name="themeSwitcher.currentTheme">
                                   <template slot="content">
                                   <template slot="content">
-                                    <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail"
+                                    <div
+                                      v-for="clientEmail in clientCount[dbInbound.id].depleted"
+                                      :key="clientEmail"
                                       class="client-popup-item">
                                       class="client-popup-item">
                                       <span>[[ clientEmail ]]</span>
                                       <span>[[ clientEmail ]]</span>
-                                      <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
+                                      <a-tooltip
+                                        :overlay-class-name="themeSwitcher.currentTheme">
                                         <template #title>
                                         <template #title>
-                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
+                                          [[
+                                          clientCount[dbInbound.id].comments.get(clientEmail)
+                                          ]]
                                         </template>
                                         </template>
                                         <a-icon type="message"
                                         <a-icon type="message"
                                           v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                           v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                       </a-tooltip>
                                       </a-tooltip>
                                     </div>
                                     </div>
                                   </template>
                                   </template>
-                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red"
+                                  <a-tag
+                                    :style="{ margin: '0', padding: '0 2px' }"
+                                    color="red"
                                     v-if="clientCount[dbInbound.id].depleted.length">[[
                                     v-if="clientCount[dbInbound.id].depleted.length">[[
-                                    clientCount[dbInbound.id].depleted.length ]]</a-tag>
+                                    clientCount[dbInbound.id].depleted.length
+                                    ]]</a-tag>
                                 </a-popover>
                                 </a-popover>
                                 <a-popover title='{{ i18n "depletingSoon" }}'
                                 <a-popover title='{{ i18n "depletingSoon" }}'
                                   :overlay-class-name="themeSwitcher.currentTheme">
                                   :overlay-class-name="themeSwitcher.currentTheme">
                                   <template slot="content">
                                   <template slot="content">
-                                    <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail"
+                                    <div
+                                      v-for="clientEmail in clientCount[dbInbound.id].expiring"
+                                      :key="clientEmail"
                                       class="client-popup-item">
                                       class="client-popup-item">
                                       <span>[[ clientEmail ]]</span>
                                       <span>[[ clientEmail ]]</span>
-                                      <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
+                                      <a-tooltip
+                                        :overlay-class-name="themeSwitcher.currentTheme">
                                         <template #title>
                                         <template #title>
-                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
+                                          [[
+                                          clientCount[dbInbound.id].comments.get(clientEmail)
+                                          ]]
                                         </template>
                                         </template>
                                         <a-icon type="message"
                                         <a-icon type="message"
                                           v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                           v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                       </a-tooltip>
                                       </a-tooltip>
                                     </div>
                                     </div>
                                   </template>
                                   </template>
-                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange"
+                                  <a-tag
+                                    :style="{ margin: '0', padding: '0 2px' }"
+                                    color="orange"
                                     v-if="clientCount[dbInbound.id].expiring.length">[[
                                     v-if="clientCount[dbInbound.id].expiring.length">[[
-                                    clientCount[dbInbound.id].expiring.length ]]</a-tag>
+                                    clientCount[dbInbound.id].expiring.length
+                                    ]]</a-tag>
                                 </a-popover>
                                 </a-popover>
-                                <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                                <a-popover title='{{ i18n "online" }}'
+                                  :overlay-class-name="themeSwitcher.currentTheme">
                                   <template slot="content">
                                   <template slot="content">
-                                    <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail"
+                                    <div
+                                      v-for="clientEmail in clientCount[dbInbound.id].online"
+                                      :key="clientEmail"
                                       class="client-popup-item">
                                       class="client-popup-item">
                                       <span>[[ clientEmail ]]</span>
                                       <span>[[ clientEmail ]]</span>
-                                      <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
+                                      <a-tooltip
+                                        :overlay-class-name="themeSwitcher.currentTheme">
                                         <template #title>
                                         <template #title>
-                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
+                                          [[
+                                          clientCount[dbInbound.id].comments.get(clientEmail)
+                                          ]]
                                         </template>
                                         </template>
                                         <a-icon type="message"
                                         <a-icon type="message"
                                           v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                           v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                       </a-tooltip>
                                       </a-tooltip>
                                     </div>
                                     </div>
                                   </template>
                                   </template>
-                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="green"
+                                  <a-tag
+                                    :style="{ margin: '0', padding: '0 2px' }"
+                                    color="green"
                                     v-if="clientCount[dbInbound.id].online.length">[[
                                     v-if="clientCount[dbInbound.id].online.length">[[
-                                    clientCount[dbInbound.id].online.length ]]</a-tag>
+                                    clientCount[dbInbound.id].online.length
+                                    ]]</a-tag>
                                 </a-popover>
                                 </a-popover>
                               </td>
                               </td>
                             </tr>
                             </tr>
                             <tr>
                             <tr>
                               <td>{{ i18n "pages.inbounds.traffic" }}</td>
                               <td>{{ i18n "pages.inbounds.traffic" }}</td>
                               <td>
                               <td>
-                                <a-popover :overlay-class-name="themeSwitcher.currentTheme">
+                                <a-popover
+                                  :overlay-class-name="themeSwitcher.currentTheme">
                                   <template slot="content">
                                   <template slot="content">
                                     <table cellpadding="2" width="100%">
                                     <table cellpadding="2" width="100%">
                                       <tr>
                                       <tr>
-                                        <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td>
-                                        <td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td>
+                                        <td>↑[[
+                                          SizeFormatter.sizeFormat(dbInbound.up)
+                                          ]]</td>
+                                        <td>↓[[
+                                          SizeFormatter.sizeFormat(dbInbound.down)
+                                          ]]</td>
                                       </tr>
                                       </tr>
                                       <tr
                                       <tr
                                         v-if="dbInbound.total > 0 &&  dbInbound.up + dbInbound.down < dbInbound.total">
                                         v-if="dbInbound.total > 0 &&  dbInbound.up + dbInbound.down < dbInbound.total">
                                         <td>{{ i18n "remained" }}</td>
                                         <td>{{ i18n "remained" }}</td>
-                                        <td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down)
+                                        <td>[[
+                                          SizeFormatter.sizeFormat(dbInbound.total
+                                          - dbInbound.up - dbInbound.down)
                                           ]]</td>
                                           ]]</td>
                                       </tr>
                                       </tr>
                                     </table>
                                     </table>
                                   </template>
                                   </template>
                                   <a-tag
                                   <a-tag
                                     :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
                                     :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
-                                    [[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /
+                                    [[ SizeFormatter.sizeFormat(dbInbound.up +
+                                    dbInbound.down) ]] /
                                     <template v-if="dbInbound.total > 0">
                                     <template v-if="dbInbound.total > 0">
-                                      [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
+                                      [[
+                                      SizeFormatter.sizeFormat(dbInbound.total)
+                                      ]]
                                     </template>
                                     </template>
                                     <template v-else>
                                     <template v-else>
-                                      <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
+                                      <svg height="10px" width="14px"
+                                        viewBox="0 0 640 512"
+                                        fill="currentColor">
                                         <path
                                         <path
                                           d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
                                           d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
                                           fill="currentColor"></path>
                                           fill="currentColor"></path>
@@ -541,12 +697,17 @@
                             <tr>
                             <tr>
                               <td>{{ i18n "pages.inbounds.expireDate" }}</td>
                               <td>{{ i18n "pages.inbounds.expireDate" }}</td>
                               <td>
                               <td>
-                                <a-tag :style="{ minWidth: '50px', textAlign: 'center' }"
-                                  v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'">
-                                  [[ IntlUtil.formatDate(dbInbound.expiryTime) ]]
+                                <a-tag
+                                  :style="{ minWidth: '50px', textAlign: 'center' }"
+                                  v-if="dbInbound.expiryTime > 0"
+                                  :color="dbInbound.isExpiry? 'red': 'blue'">
+                                  [[ IntlUtil.formatDate(dbInbound.expiryTime)
+                                  ]]
                                 </a-tag>
                                 </a-tag>
-                                <a-tag v-else :style="{ textAlign: 'center' }" color="purple" class="infinite-tag">
-                                  <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
+                                <a-tag v-else :style="{ textAlign: 'center' }"
+                                  color="purple" class="infinite-tag">
+                                  <svg height="10px" width="14px"
+                                    viewBox="0 0 640 512" fill="currentColor">
                                     <path
                                     <path
                                       d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
                                       d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
                                       fill="currentColor"></path>
                                       fill="currentColor"></path>
@@ -555,25 +716,32 @@
                               </td>
                               </td>
                             </tr>
                             </tr>
                             <tr>
                             <tr>
-                              <td>{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}</td>
+                              <td>{{ i18n
+                                "pages.inbounds.periodicTrafficResetTitle"
+                                }}</td>
                               <td>
                               <td>
-                                <a-tag color="blue">[[ dbInbound.trafficReset ]]</a-tag>
+                                <a-tag color="blue">[[ dbInbound.trafficReset
+                                  ]]</a-tag>
                               </td>
                               </td>
                             </tr>
                             </tr>
                           </table>
                           </table>
                         </template>
                         </template>
                         <a-badge>
                         <a-badge>
-                          <a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle"
+                          <a-icon v-if="!dbInbound.enable" slot="count"
+                            type="pause-circle"
                             :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
                             :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
-                          <a-button shape="round" size="small" :style="{ fontSize: '14px', padding: '0 10px' }">
+                          <a-button shape="round" size="small"
+                            :style="{ fontSize: '14px', padding: '0 10px' }">
                             <a-icon type="info"></a-icon>
                             <a-icon type="info"></a-icon>
                           </a-button>
                           </a-button>
                         </a-badge>
                         </a-badge>
                       </a-popover>
                       </a-popover>
                     </template>
                     </template>
                     <template slot="expandedRowRender" slot-scope="record">
                     <template slot="expandedRowRender" slot-scope="record">
-                      <a-table :row-key="client => client.id" :columns="isMobile ? innerMobileColumns : innerColumns"
-                        :data-source="getInboundClients(record)" :pagination=pagination(getInboundClients(record))
+                      <a-table :row-key="client => client.id"
+                        :columns="isMobile ? innerMobileColumns : innerColumns"
+                        :data-source="getInboundClients(record)"
+                        :pagination=pagination(getInboundClients(record))
                         :style="{ margin: `-10px ${isMobile ? '2px' : '22px'} -11px` }">
                         :style="{ margin: `-10px ${isMobile ? '2px' : '22px'} -11px` }">
                         {{template "component/aClientTable"}}
                         {{template "component/aClientTable"}}
                       </a-table>
                       </a-table>
@@ -589,11 +757,15 @@
   </a-layout>
   </a-layout>
 </a-layout>
 </a-layout>
 {{template "page/body_scripts" .}}
 {{template "page/body_scripts" .}}
-<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
+<script
+  src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
 <script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
 <script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/js/model/reality_targets.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
+<script
+  src="{{ .base_path }}assets/js/model/reality_targets.js?{{ .cur_ver }}"></script>
+<script
+  src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
+<script
+  src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
 {{template "component/aSidebar" .}}
 {{template "component/aSidebar" .}}
 {{template "component/aThemeSwitch" .}}
 {{template "component/aThemeSwitch" .}}
 {{template "component/aCustomStatistic" .}}
 {{template "component/aCustomStatistic" .}}
@@ -1247,6 +1419,7 @@
         switch (protocol) {
         switch (protocol) {
           case Protocols.TROJAN: return client.password;
           case Protocols.TROJAN: return client.password;
           case Protocols.SHADOWSOCKS: return client.email;
           case Protocols.SHADOWSOCKS: return client.email;
+          case Protocols.HYSTERIA: return client.auth;
           default: return client.id;
           default: return client.id;
         }
         }
       },
       },
@@ -1322,19 +1495,25 @@
 
 
         this.submit(`/panel/api/inbounds/update/${dbInboundId}`, formData);
         this.submit(`/panel/api/inbounds/update/${dbInboundId}`, formData);
       },
       },
-      async switchEnableClient(dbInboundId, client) {
-        this.loading()
-        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-        if (!dbInbound) return;
-        inbound = dbInbound.toInbound();
-        clients = inbound && inbound.clients ? inbound.clients : null;
-        if (!clients || !Array.isArray(clients)) return;
-        index = this.findIndexOfClient(dbInbound.protocol, clients, client);
-        if (index < 0 || !clients[index]) return;
-        clients[index].enable = !clients[index].enable;
-        clientId = this.getClientId(dbInbound.protocol, clients[index]);
-        await this.updateClient(clients[index], dbInboundId, clientId);
-        this.loading(false);
+      async switchEnableClient(dbInboundId, client, state) {
+        this.loading();
+        try {
+          dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+          if (!dbInbound) return;
+
+          inbound = dbInbound.toInbound();
+          clients = inbound && inbound.clients ? inbound.clients : null;
+          if (!clients || !Array.isArray(clients)) return;
+
+          index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+          if (index < 0 || !clients[index]) return;
+
+          clients[index].enable = typeof state === 'boolean' ? state : !!client.enable;
+          clientId = this.getClientId(dbInbound.protocol, clients[index]);
+          await this.updateClient(clients[index], dbInboundId, clientId);
+        } finally {
+          this.loading(false);
+        }
       },
       },
       async submit(url, data, modal) {
       async submit(url, data, modal) {
         const msg = await HttpUtil.postWithModal(url, data, modal);
         const msg = await HttpUtil.postWithModal(url, data, modal);

+ 70 - 34
web/html/modals/client_bulk_modal.html

@@ -1,52 +1,75 @@
 {{define "modals/clientsBulkModal"}}
 {{define "modals/clientsBulkModal"}}
-<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title"
-    @ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
-    :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
-    <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible"
+    :title="clientsBulkModal.title"
+    @ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading"
+    :closable="true" :mask-closable="false"
+    :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'
+    :class="themeSwitcher.currentTheme">
+    <a-form :colon="false" :label-col="{ md: {span:8} }"
+        :wrapper-col="{ md: {span:14} }">
         <a-form-item label='{{ i18n "pages.client.method" }}'>
         <a-form-item label='{{ i18n "pages.client.method" }}'>
             <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid"
             <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid"
                 :dropdown-class-name="themeSwitcher.currentTheme">
                 :dropdown-class-name="themeSwitcher.currentTheme">
                 <a-select-option :value="0">Random</a-select-option>
                 <a-select-option :value="0">Random</a-select-option>
                 <a-select-option :value="1">Random+Prefix</a-select-option>
                 <a-select-option :value="1">Random+Prefix</a-select-option>
                 <a-select-option :value="2">Random+Prefix+Num</a-select-option>
                 <a-select-option :value="2">Random+Prefix+Num</a-select-option>
-                <a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
+                <a-select-option
+                    :value="3">Random+Prefix+Num+Postfix</a-select-option>
                 <a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
                 <a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
             </a-select>
             </a-select>
         </a-form-item>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.first" }}' v-if="clientsBulkModal.emailMethod>1">
-            <a-input-number v-model.number="clientsBulkModal.firstNum" :min="1"></a-input-number>
+        <a-form-item label='{{ i18n "pages.client.first" }}'
+            v-if="clientsBulkModal.emailMethod>1">
+            <a-input-number v-model.number="clientsBulkModal.firstNum"
+                :min="1"></a-input-number>
         </a-form-item>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.last" }}' v-if="clientsBulkModal.emailMethod>1">
-            <a-input-number v-model.number="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
+        <a-form-item label='{{ i18n "pages.client.last" }}'
+            v-if="clientsBulkModal.emailMethod>1">
+            <a-input-number v-model.number="clientsBulkModal.lastNum"
+                :min="clientsBulkModal.firstNum"></a-input-number>
         </a-form-item>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0">
+        <a-form-item label='{{ i18n "pages.client.prefix" }}'
+            v-if="clientsBulkModal.emailMethod>0">
             <a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input>
             <a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input>
         </a-form-item>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.postfix" }}' v-if="clientsBulkModal.emailMethod>2">
+        <a-form-item label='{{ i18n "pages.client.postfix" }}'
+            v-if="clientsBulkModal.emailMethod>2">
             <a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
             <a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
         </a-form-item>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
-            <a-input-number v-model.number="clientsBulkModal.quantity" :min="1" :max="500"></a-input-number>
+        <a-form-item label='{{ i18n "pages.client.clientCount" }}'
+            v-if="clientsBulkModal.emailMethod < 2">
+            <a-input-number v-model.number="clientsBulkModal.quantity" :min="1"
+                :max="500"></a-input-number>
         </a-form-item>
         </a-form-item>
-        <a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS">
-            <a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme">
-                <a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
+        <a-form-item label='{{ i18n "security" }}'
+            v-if="inbound.protocol === Protocols.VMESS">
+            <a-select v-model="clientsBulkModal.security"
+                :dropdown-class-name="themeSwitcher.currentTheme">
+                <a-select-option v-for="key in USERS_SECURITY" :value="key">[[
+                    key ]]</a-select-option>
             </a-select>
             </a-select>
         </a-form-item>
         </a-form-item>
-        <a-form-item label='Flow' v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
-            <a-select v-model="clientsBulkModal.flow" :dropdown-class-name="themeSwitcher.currentTheme">
-                <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
-                <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
+        <a-form-item label='Flow'
+            v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
+            <a-select v-model="clientsBulkModal.flow"
+                :dropdown-class-name="themeSwitcher.currentTheme">
+                <a-select-option value selected>{{ i18n "none"
+                    }}</a-select-option>
+                <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[
+                    key ]]</a-select-option>
             </a-select>
             </a-select>
         </a-form-item>
         </a-form-item>
         <a-form-item v-if="app.subSettings?.enable">
         <a-form-item v-if="app.subSettings?.enable">
             <template slot="label">
             <template slot="label">
                 <a-tooltip>
                 <a-tooltip>
                     <template slot="title">
                     <template slot="title">
-                        <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
+                        <span>{{ i18n "pages.inbounds.subscriptionDesc"
+                            }}</span>
                     </template>
                     </template>
                     Subscription
                     Subscription
-                    <a-icon @click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
+                    <a-icon
+                        @click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)"
+                        type="sync"></a-icon>
                 </a-tooltip>
                 </a-tooltip>
             </template>
             </template>
             <a-input v-model.trim="clientsBulkModal.subId"></a-input>
             <a-input v-model.trim="clientsBulkModal.subId"></a-input>
@@ -61,7 +84,8 @@
                     <a-icon type="question-circle"></a-icon>
                     <a-icon type="question-circle"></a-icon>
                 </a-tooltip>
                 </a-tooltip>
             </template>
             </template>
-            <a-input-number :style="{ width: '50%' }" v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
+            <a-input-number :style="{ width: '50%' }"
+                v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
         </a-form-item>
         </a-form-item>
         <a-form-item v-if="app.ipLimitEnable">
         <a-form-item v-if="app.ipLimitEnable">
             <template slot="label">
             <template slot="label">
@@ -73,7 +97,8 @@
                     <a-icon type="question-circle"></a-icon>
                     <a-icon type="question-circle"></a-icon>
                 </a-tooltip>
                 </a-tooltip>
             </template>
             </template>
-            <a-input-number v-model.number="clientsBulkModal.limitIp" min="0"></a-input-number>
+            <a-input-number v-model.number="clientsBulkModal.limitIp"
+                min="0"></a-input-number>
         </a-form-item>
         </a-form-item>
         <a-form-item>
         <a-form-item>
             <template slot="label">
             <template slot="label">
@@ -85,29 +110,38 @@
                     <a-icon type="question-circle"></a-icon>
                     <a-icon type="question-circle"></a-icon>
                 </a-tooltip>
                 </a-tooltip>
             </template>
             </template>
-            <a-input-number v-model.number="clientsBulkModal.totalGB" :min="0"></a-input-number>
+            <a-input-number v-model.number="clientsBulkModal.totalGB"
+                :min="0"></a-input-number>
         </a-form-item>
         </a-form-item>
         <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
         <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
-            <a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
+            <a-switch v-model="clientsBulkModal.delayedStart"
+                @click="clientsBulkModal.expiryTime=0"></a-switch>
         </a-form-item>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
-            <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
+        <a-form-item label='{{ i18n "pages.client.expireDays" }}'
+            v-if="clientsBulkModal.delayedStart">
+            <a-input-number v-model.number="delayedExpireDays"
+                :min="0"></a-input-number>
         </a-form-item>
         </a-form-item>
         <a-form-item v-else>
         <a-form-item v-else>
             <template slot="label">
             <template slot="label">
                 <a-tooltip>
                 <a-tooltip>
                     <template slot="title">
                     <template slot="title">
-                        <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
+                        <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire"
+                            }}</span>
                     </template>
                     </template>
                     {{ i18n "pages.inbounds.expireDate" }}
                     {{ i18n "pages.inbounds.expireDate" }}
                     <a-icon type="question-circle"></a-icon>
                     <a-icon type="question-circle"></a-icon>
                 </a-tooltip>
                 </a-tooltip>
             </template>
             </template>
-            <a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
-                format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
+            <a-date-picker v-if="datepicker == 'gregorian'"
+                :show-time="{ format: 'HH:mm:ss' }"
+                format="YYYY-MM-DD HH:mm:ss"
+                :dropdown-class-name="themeSwitcher.currentTheme"
                 v-model="clientsBulkModal.expiryTime"></a-date-picker>
                 v-model="clientsBulkModal.expiryTime"></a-date-picker>
-            <a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
-                value="clientsBulkModal.expiryTime" v-model="clientsBulkModal.expiryTime">
+            <a-persian-datepicker v-else
+                placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
+                value="clientsBulkModal.expiryTime"
+                v-model="clientsBulkModal.expiryTime">
             </a-persian-datepicker>
             </a-persian-datepicker>
         </a-form-item>
         </a-form-item>
         <a-form-item v-if="clientsBulkModal.expiryTime != 0">
         <a-form-item v-if="clientsBulkModal.expiryTime != 0">
@@ -120,7 +154,8 @@
                     <a-icon type="question-circle"></a-icon>
                     <a-icon type="question-circle"></a-icon>
                 </a-tooltip>
                 </a-tooltip>
             </template>
             </template>
-            <a-input-number v-model.number="clientsBulkModal.reset" :min="0"></a-input-number>
+            <a-input-number v-model.number="clientsBulkModal.reset"
+                :min="0"></a-input-number>
         </a-form-item>
         </a-form-item>
     </a-form>
     </a-form>
 </a-modal>
 </a-modal>
@@ -214,6 +249,7 @@
                 case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
                 case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
                 case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
                 case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
                 case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method);
                 case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method);
+                case Protocols.HYSTERIA: return new Inbound.HysteriaSettings.Hysteria();
                 default: return null;
                 default: return null;
             }
             }
         },
         },

+ 11 - 5
web/html/modals/client_modal.html

@@ -1,10 +1,14 @@
 {{define "modals/clientsModal"}}
 {{define "modals/clientsModal"}}
-<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
-         :confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
-         :class="themeSwitcher.currentTheme"
-         :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
+<a-modal id="client-modal" v-model="clientModal.visible"
+    :title="clientModal.title" @ok="clientModal.ok"
+    :confirm-loading="clientModal.confirmLoading" :closable="true"
+    :mask-closable="false"
+    :class="themeSwitcher.currentTheme"
+    :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
     <template v-if="isEdit">
     <template v-if="isEdit">
-        <a-tag v-if="isExpiry || isTrafficExhausted" color="red" :style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account is (Expired|Traffic Ended) And Disabled</a-tag>
+        <a-tag v-if="isExpiry || isTrafficExhausted" color="red"
+            :style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account
+            is (Expired|Traffic Ended) And Disabled</a-tag>
     </template>
     </template>
     {{template "form/client"}}
     {{template "form/client"}}
 </a-modal>
 </a-modal>
@@ -56,6 +60,7 @@
             switch (protocol) {
             switch (protocol) {
                 case Protocols.TROJAN: return client.password;
                 case Protocols.TROJAN: return client.password;
                 case Protocols.SHADOWSOCKS: return client.email;
                 case Protocols.SHADOWSOCKS: return client.email;
+                case Protocols.HYSTERIA: return client.auth;
                 default: return client.id;
                 default: return client.id;
             }
             }
         },
         },
@@ -65,6 +70,7 @@
                 case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
                 case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
                 case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
                 case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
                 case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method)));
                 case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method)));
+                case Protocols.HYSTERIA: return clients.push(new Inbound.HysteriaSettings.Hysteria());
                 default: return null;
                 default: return null;
             }
             }
         },
         },

+ 448 - 483
web/html/modals/inbound_info_modal.html

@@ -1,8 +1,6 @@
 {{define "modals/inboundInfoModal"}}
 {{define "modals/inboundInfoModal"}}
-<a-modal id="inbound-info-modal" v-model="infoModal.visible"
-  title='{{ i18n "pages.inbounds.details"}}' :closable="true"
-  :mask-closable="true" :footer="null" width="600px"
-  :class="themeSwitcher.currentTheme">
+<a-modal id="inbound-info-modal" v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' :closable="true"
+  :mask-closable="true" :footer="null" width="600px" :class="themeSwitcher.currentTheme">
   <a-row>
   <a-row>
     <a-col :xs="24" :md="12">
     <a-col :xs="24" :md="12">
       <table>
       <table>
@@ -29,8 +27,7 @@
       </table>
       </table>
     </a-col>
     </a-col>
     <a-col :xs="24" :md="12">
     <a-col :xs="24" :md="12">
-      <template
-        v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
+      <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
         <table>
         <table>
           <tr>
           <tr>
             <td>{{ i18n "transmission" }}</td>
             <td>{{ i18n "transmission" }}</td>
@@ -38,8 +35,7 @@
               <a-tag color="green">[[ inbound.network ]]</a-tag>
               <a-tag color="green">[[ inbound.network ]]</a-tag>
             </td>
             </td>
           </tr>
           </tr>
-          <template
-            v-if="inbound.isTcp || inbound.isWs || inbound.isHttpupgrade || inbound.isXHTTP">
+          <template v-if="inbound.isTcp || inbound.isWs || inbound.isHttpupgrade || inbound.isXHTTP">
             <tr>
             <tr>
               <td>{{ i18n "host" }}</td>
               <td>{{ i18n "host" }}</td>
               <td v-if="inbound.host">
               <td v-if="inbound.host">
@@ -51,13 +47,13 @@
                 <a-tag color="orange">{{ i18n "none" }}</a-tag>
                 <a-tag color="orange">{{ i18n "none" }}</a-tag>
               </td>
               </td>
             </tr>
             </tr>
-          </tr>
-          <tr>
-            <td>{{ i18n "path" }}</td>
-            <td v-if="inbound.path">
-              <a-tooltip :title="[[ inbound.path ]]">
-                <a-tag class="info-large-tag">[[ inbound.path ]]</a-tag>
-              </a-tooltip>
+            </tr>
+            <tr>
+              <td>{{ i18n "path" }}</td>
+              <td v-if="inbound.path">
+                <a-tooltip :title="[[ inbound.path ]]">
+                  <a-tag class="info-large-tag">[[ inbound.path ]]</a-tag>
+                </a-tooltip>
               <td v-else>
               <td v-else>
                 <a-tag color="orange">{{ i18n "none" }}</a-tag>
                 <a-tag color="orange">{{ i18n "none" }}</a-tag>
               </td>
               </td>
@@ -79,483 +75,452 @@
                   <a-tag class="info-large-tag">[[ inbound.serviceName
                   <a-tag class="info-large-tag">[[ inbound.serviceName
                     ]]</a-tag>
                     ]]</a-tag>
                 </a-tooltip>
                 </a-tooltip>
-                <tr>
-                  <td>grpc multiMode</td>
-                  <td>
-                    <a-tag>[[ inbound.stream.grpc.multiMode ]]</a-tag>
-                  </td>
-                </tr>
-              </template>
-            </table>
+            <tr>
+              <td>grpc multiMode</td>
+              <td>
+                <a-tag>[[ inbound.stream.grpc.multiMode ]]</a-tag>
+              </td>
+            </tr>
           </template>
           </template>
-        </a-col>
-        <template v-if="dbInbound.hasLink()">
-          {{ i18n "security" }}
-          <a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[
-            inbound.stream.security ]]</a-tag>
-          <br />
-          <td>Authentication</td>
-          <a-tag v-if="inbound.settings.selectedAuth" color="green">[[
-            inbound.settings.selectedAuth ? inbound.settings.selectedAuth : ''
-            ]]</a-tag>
-          <a-tag v-else color="red">{{ i18n "none" }}</a-tag>
-          <br />
-          {{ i18n "encryption" }}
-          <a-tag class="info-large-tag"
-            :color="inbound.settings.encryption ? 'green' : 'red'">[[
-            inbound.settings.encryption ? inbound.settings.encryption : ''
-            ]]</a-tag>
-          <a-tooltip title='{{ i18n "copy" }}'>
-            <a-button size="small" icon="snippets"
-              @click="copy(inbound.settings.encryption)"></a-button>
+        </table>
+      </template>
+    </a-col>
+    <template v-if="dbInbound.hasLink()">
+      {{ i18n "security" }}
+      <a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[
+        inbound.stream.security ]]</a-tag>
+      <br />
+      <td>Authentication</td>
+      <a-tag v-if="inbound.settings.selectedAuth" color="green">[[
+        inbound.settings.selectedAuth ? inbound.settings.selectedAuth : ''
+        ]]</a-tag>
+      <a-tag v-else color="red">{{ i18n "none" }}</a-tag>
+      <br />
+      {{ i18n "encryption" }}
+      <a-tag class="info-large-tag" :color="inbound.settings.encryption ? 'green' : 'red'">[[
+        inbound.settings.encryption ? inbound.settings.encryption : ''
+        ]]</a-tag>
+      <a-tooltip title='{{ i18n "copy" }}'>
+        <a-button size="small" icon="snippets" @click="copy(inbound.settings.encryption)"></a-button>
+      </a-tooltip>
+      <br />
+      <template v-if="inbound.stream.security != 'none'">
+        {{ i18n "domainName" }}
+        <a-tag v-if="inbound.serverName" color="green">[[ inbound.serverName
+          ? inbound.serverName : '' ]]</a-tag>
+        <a-tag v-else color="orange">{{ i18n "none" }}</a-tag>
+      </template>
+    </template>
+    <table v-if="dbInbound.isSS" :style="{ marginBottom: '10px', width: '100%' }">
+      <tr>
+        <td>{{ i18n "encryption" }}</td>
+        <td>
+          <a-tag color="green">[[ inbound.settings.method ]]</a-tag>
+        </td>
+      </tr>
+      <tr v-if="inbound.isSS2022">
+        <td>{{ i18n "password" }}</td>
+        <td>
+          <a-tooltip :title="[[ inbound.settings.password  ]]">
+            <a-tag class="info-large-tag">[[ inbound.settings.password
+              ]]</a-tag>
           </a-tooltip>
           </a-tooltip>
-          <br />
-          <template v-if="inbound.stream.security != 'none'">
-            {{ i18n "domainName" }}
-            <a-tag v-if="inbound.serverName" color="green">[[ inbound.serverName
-              ? inbound.serverName : '' ]]</a-tag>
-            <a-tag v-else color="orange">{{ i18n "none" }}</a-tag>
-          </template>
-        </template>
-        <table v-if="dbInbound.isSS"
-          :style="{ marginBottom: '10px', width: '100%' }">
+        </td>
+      </tr>
+      <tr>
+        <td>{{ i18n "pages.inbounds.network" }}</td>
+        <td>
+          <a-tag color="green">[[ inbound.settings.network ]]</a-tag>
+        </td>
+      </tr>
+    </table>
+    <template v-if="infoModal.clientSettings">
+      <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
+      <table :style="{ marginBottom: '10px' }">
+        <tr>
+          <td>{{ i18n "pages.inbounds.email" }}</td>
+          <td v-if="infoModal.clientSettings.email">
+            <a-tag color="green">[[ infoModal.clientSettings.email
+              ]]</a-tag>
+          </td>
+          <td v-else>
+            <a-tag color="red">{{ i18n "none" }}</a-tag>
+          </td>
+        </tr>
+        <tr v-if="infoModal.clientSettings.id">
+          <td>ID</td>
+          <td>
+            <a-tag>[[ infoModal.clientSettings.id ]]</a-tag>
+          </td>
+        </tr>
+        <tr v-if="dbInbound.isVMess">
+          <td>{{ i18n "security" }}</td>
+          <td>
+            <a-tag>[[ infoModal.clientSettings.security ]]</a-tag>
+          </td>
+        </tr>
+        <tr v-if="infoModal.inbound.canEnableTlsFlow()">
+          <td>Flow</td>
+          <td v-if="infoModal.clientSettings.flow">
+            <a-tag>[[ infoModal.clientSettings.flow ]]</a-tag>
+          </td>
+          <td v-else>
+            <a-tag color="orange">{{ i18n "none" }}</a-tag>
+          </td>
+        </tr>
+        <tr v-if="infoModal.clientSettings.password">
+          <td>{{ i18n "password" }}</td>
+          <td>
+            <a-tooltip :title="[[ infoModal.clientSettings.password  ]]">
+              <a-tag class="info-large-tag">[[
+                infoModal.clientSettings.password ]]</a-tag>
+            </a-tooltip>
+          </td>
+        </tr>
+        <tr>
+          <td>{{ i18n "status" }}</td>
+          <td>
+            <a-tag v-if="isDepleted" color="red">{{ i18n "depleted"
+              }}</a-tag>
+            <a-tag v-else-if="isEnable" color="green">{{ i18n "enabled"
+              }}</a-tag>
+            <a-tag v-else>{{ i18n "disabled" }}</a-tag>
+          </td>
+        </tr>
+        <tr v-if="infoModal.clientStats">
+          <td>{{ i18n "usage" }}</td>
+          <td>
+            <a-tag color="green">[[
+              SizeFormatter.sizeFormat(infoModal.clientStats.up +
+              infoModal.clientStats.down) ]]</a-tag>
+            <a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up)
+              ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down)
+              ]] ↓</a-tag>
+          </td>
+        </tr>
+        <tr>
+          <td>{{ i18n "pages.inbounds.createdAt" }}</td>
+          <td>
+            <template v-if="infoModal.clientSettings && infoModal.clientSettings.created_at">
+              <a-tag>[[
+                IntlUtil.formatDate(infoModal.clientSettings.created_at)
+                ]]</a-tag>
+            </template>
+            <template v-else>
+              <a-tag>-</a-tag>
+            </template>
+          </td>
+        </tr>
+        <tr>
+          <td>{{ i18n "pages.inbounds.updatedAt" }}</td>
+          <td>
+            <template v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at">
+              <a-tag>[[
+                IntlUtil.formatDate(infoModal.clientSettings.updated_at)
+                ]]</a-tag>
+            </template>
+            <template v-else>
+              <a-tag>-</a-tag>
+            </template>
+          </td>
+        </tr>
+        <tr>
+          <td>{{ i18n "lastOnline" }}</td>
+          <td>
+            <a-tag>[[ app.formatLastOnline(infoModal.clientSettings &&
+              infoModal.clientSettings.email ?
+              infoModal.clientSettings.email : '') ]]</a-tag>
+          </td>
+        </tr>
+        <tr v-if="infoModal.clientSettings.comment">
+          <td>{{ i18n "comment" }}</td>
+          <td>
+            <a-tooltip :title="[[ infoModal.clientSettings.comment  ]]">
+              <a-tag class="info-large-tag">[[
+                infoModal.clientSettings.comment ]]</a-tag>
+            </a-tooltip>
+          </td>
+        </tr>
+        <tr v-if="app.ipLimitEnable">
+          <td>{{ i18n "pages.inbounds.IPLimit" }}</td>
+          <td>
+            <a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>
+          </td>
+        </tr>
+        <tr v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
+          <td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
+          <td>
+            <div style="max-height: 150px; overflow-y: auto; text-align: left;">
+              <div v-if="infoModal.clientIpsArray && infoModal.clientIpsArray.length > 0">
+                <a-tag v-for="(ipInfo, idx) in infoModal.clientIpsArray" :key="idx" color="blue"
+                  style="margin: 2px 0; display: block; font-family: monospace; font-size: 11px;">
+                  [[ formatIpInfo(ipInfo) ]]
+                </a-tag>
+              </div>
+              <a-tag v-else>[[ infoModal.clientIps || 'No IP Record'
+                ]]</a-tag>
+            </div>
+            <div style="margin-top: 5px;">
+              <a-icon type="sync" :spin="refreshing" @click="refreshIPs" :style="{ margin: '0 5px' }"></a-icon>
+              <a-tooltip>
+                <template slot="title">
+                  <span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
+                </template>
+                <a-icon type="delete" @click="clearClientIps"></a-icon>
+              </a-tooltip>
+            </div>
+          </td>
+        </tr>
+      </table>
+      <table :style="{ display: 'inline-table', marginBlock: '10px', width: '100%', textAlign: 'center' }">
+        <tr>
+          <th>{{ i18n "remained" }}</th>
+          <th>{{ i18n "pages.inbounds.totalFlow" }}</th>
+          <th>{{ i18n "pages.inbounds.expireDate" }}</th>
+        </tr>
+        <tr>
+          <td>
+            <a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0"
+              :color="statsColor(infoModal.clientStats)"> [[ getRemStats()
+              ]] </a-tag>
+          </td>
+          <td>
+            <a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[
+              SizeFormatter.sizeFormat(infoModal.clientSettings.totalGB) ]]
+            </a-tag>
+            <a-tag v-else color="purple" class="infinite-tag">
+              <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
+                <path
+                  d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
+                  fill="currentColor"></path>
+              </svg>
+            </a-tag>
+          </td>
+          <td>
+            <template v-if="infoModal.clientSettings.expiryTime > 0">
+              <a-tag
+                :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
+                [[ IntlUtil.formatDate(infoModal.clientSettings.expiryTime)
+                ]]
+              </a-tag>
+            </template>
+            <a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="green">[[
+              infoModal.clientSettings.expiryTime /
+              -86400000 ]] {{ i18n "pages.client.days" }}
+            </a-tag>
+            <a-tag v-else color="purple" class="infinite-tag">
+              <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
+                <path
+                  d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
+                  fill="currentColor"></path>
+              </svg>
+            </a-tag>
+          </td>
+        </tr>
+      </table>
+      <template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
+        <a-divider>Subscription URL</a-divider>
+        <tr-info-row class="tr-info-row">
+          <tr-info-title class="tr-info-title">
+            <a-tag color="purple">Subscription Link</a-tag>
+            <a-tooltip title='{{ i18n "copy" }}'>
+              <a-button size="small" icon="snippets" @click="copy(infoModal.subLink)"></a-button>
+            </a-tooltip>
+          </tr-info-title>
+          <a :href="[[ infoModal.subLink ]]" target="_blank">[[
+            infoModal.subLink ]]</a>
+        </tr-info-row>
+        <tr-info-row class="tr-info-row" v-if="app.subSettings.subJsonEnable">
+          <tr-info-title class="tr-info-title">
+            <a-tag color="purple">Json Link</a-tag>
+            <a-tooltip title='{{ i18n "copy" }}'>
+              <a-button size="small" icon="snippets" @click="copy(infoModal.subJsonLink)"></a-button>
+            </a-tooltip>
+          </tr-info-title>
+          <a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[
+            infoModal.subJsonLink ]]</a>
+        </tr-info-row>
+      </template>
+      <template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
+        <a-divider>Telegram ChatID</a-divider>
+        <tr-info-row class="tr-info-row">
+          <tr-info-title class="tr-info-title">
+            <a-tag color="blue">[[ infoModal.clientSettings.tgId ]]</a-tag>
+            <a-tooltip title='{{ i18n "copy" }}'>
+              <a-button size="small" icon="snippets" @click="copy(infoModal.clientSettings.tgId)"></a-button>
+            </a-tooltip>
+          </tr-info-title>
+        </tr-info-row>
+      </template>
+      <template v-if="dbInbound.hasLink()">
+        <a-divider>URL</a-divider>
+        <tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
+          <tr-info-title class="tr-info-title">
+            <a-tag class="tr-info-tag" color="green">[[ link.remark
+              ]]</a-tag>
+            <a-tooltip title='{{ i18n "copy" }}'>
+              <a-button :style="{ minWidth: '24px' }" size="small" icon="snippets" @click="copy(link.link)"></a-button>
+            </a-tooltip>
+          </tr-info-title>
+          <code>[[ link.link ]]</code>
+        </tr-info-row>
+      </template>
+    </template>
+    <template v-else>
+      <template v-if="dbInbound.isSS && !inbound.isSSMultiUser">
+        <a-divider>URL</a-divider>
+        <tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
+          <tr-info-title class="tr-info-title">
+            <a-tag class="tr-info-tag" color="green">[[ link.remark
+              ]]</a-tag>
+            <a-tooltip title='{{ i18n "copy" }}'>
+              <a-button :style="{ minWidth: '24px' }" size="small" icon="snippets" @click="copy(link.link)"></a-button>
+            </a-tooltip>
+          </tr-info-title>
+          <code>[[ link.link ]]</code>
+        </tr-info-row>
+      </template>
+      <table v-if="inbound.protocol == Protocols.TUNNEL" class="tr-info-table">
+        <tr>
+          <th>{{ i18n "pages.inbounds.targetAddress" }}</th>
+          <th>{{ i18n "pages.inbounds.destinationPort" }}</th>
+          <th>{{ i18n "pages.inbounds.network" }}</th>
+          <th>FollowRedirect</th>
+        </tr>
+        <tr>
+          <td>
+            <a-tag color="green">[[ inbound.settings.address ]]</a-tag>
+          </td>
+          <td>
+            <a-tag color="green">[[ inbound.settings.port ]]</a-tag>
+          </td>
+          <td>
+            <a-tag color="green">[[ inbound.settings.network ]]</a-tag>
+          </td>
+          <td>
+            <a-tag color="green">[[ inbound.settings.followRedirect
+              ]]</a-tag>
+          </td>
+        </tr>
+      </table>
+      <table v-if="dbInbound.isMixed" class="tr-info-table">
+        <tr>
+          <th>{{ i18n "password" }} Auth</th>
+          <th>{{ i18n "pages.inbounds.enable" }} udp</th>
+          <th>IP</th>
+        </tr>
+        <tr>
+          <td>
+            <a-tag color="green">[[ inbound.settings.auth ]]</a-tag>
+          </td>
+          <td>
+            <a-tag color="green">[[ inbound.settings.udp]]</a-tag>
+          </td>
+          <td>
+            <a-tag color="green">[[ inbound.settings.ip ]]</a-tag>
+          </td>
+        </tr>
+        <template v-if="inbound.settings.auth == 'password'">
           <tr>
           <tr>
-            <td>{{ i18n "encryption" }}</td>
+            <td></td>
+            <td>{{ i18n "username" }}</td>
+            <td>{{ i18n "password" }}</td>
+          </tr>
+          <tr v-for="account,index in inbound.settings.accounts">
+            <td>[[ index ]]</td>
             <td>
             <td>
-              <a-tag color="green">[[ inbound.settings.method ]]</a-tag>
+              <a-tag color="green">[[ account.user ]]</a-tag>
             </td>
             </td>
-          </tr>
-          <tr v-if="inbound.isSS2022">
-            <td>{{ i18n "password" }}</td>
             <td>
             <td>
-              <a-tooltip :title="[[ inbound.settings.password  ]]">
-                <a-tag class="info-large-tag">[[ inbound.settings.password
-                  ]]</a-tag>
-              </a-tooltip>
+              <a-tag color="green">[[ account.pass ]]</a-tag>
             </td>
             </td>
           </tr>
           </tr>
+        </template>
+      </table>
+      <table v-if="dbInbound.isHTTP" class="tr-info-table">
+        <tr>
+          <th></th>
+          <th>{{ i18n "username" }}</th>
+          <th>{{ i18n "password" }}</th>
+        </tr>
+        <tr v-for="account,index in inbound.settings.accounts">
+          <td>[[ index ]]</td>
+          <td>
+            <a-tag color="green">[[ account.user ]]</a-tag>
+          </td>
+          <td>
+            <a-tag color="green">[[ account.pass ]]</a-tag>
+          </td>
+        </tr>
+      </table>
+      <table v-if="dbInbound.isWireguard" class="tr-info-table">
+        <tr class="client-table-odd-row">
+          <td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
+          <td>[[ inbound.settings.secretKey ]]</td>
+        </tr>
+        <tr>
+          <td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
+          <td>[[ inbound.settings.pubKey ]]</td>
+        </tr>
+        <tr class="client-table-odd-row">
+          <td>MTU</td>
+          <td>[[ inbound.settings.mtu ]]</td>
+        </tr>
+        <tr>
+          <td>No Kernel Tun</td>
+          <td>[[ inbound.settings.noKernelTun ]]</td>
+        </tr>
+        <template v-for="(peer, index) in inbound.settings.peers">
           <tr>
           <tr>
-            <td>{{ i18n "pages.inbounds.network" }}</td>
-            <td>
-              <a-tag color="green">[[ inbound.settings.network ]]</a-tag>
+            <td colspan="2">
+              <a-divider>Peer [[ index + 1 ]]</a-divider>
             </td>
             </td>
           </tr>
           </tr>
-        </table>
-        <template v-if="infoModal.clientSettings">
-          <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
-          <table :style="{ marginBottom: '10px' }">
-            <tr>
-              <td>{{ i18n "pages.inbounds.email" }}</td>
-              <td v-if="infoModal.clientSettings.email">
-                <a-tag color="green">[[ infoModal.clientSettings.email
-                  ]]</a-tag>
-              </td>
-              <td v-else>
-                <a-tag color="red">{{ i18n "none" }}</a-tag>
-              </td>
-            </tr>
-            <tr v-if="infoModal.clientSettings.id">
-              <td>ID</td>
-              <td>
-                <a-tag>[[ infoModal.clientSettings.id ]]</a-tag>
-              </td>
-            </tr>
-            <tr v-if="dbInbound.isVMess">
-              <td>{{ i18n "security" }}</td>
-              <td>
-                <a-tag>[[ infoModal.clientSettings.security ]]</a-tag>
-              </td>
-            </tr>
-            <tr v-if="infoModal.inbound.canEnableTlsFlow()">
-              <td>Flow</td>
-              <td v-if="infoModal.clientSettings.flow">
-                <a-tag>[[ infoModal.clientSettings.flow ]]</a-tag>
-              </td>
-              <td v-else>
-                <a-tag color="orange">{{ i18n "none" }}</a-tag>
-              </td>
-            </tr>
-            <tr v-if="infoModal.clientSettings.password">
-              <td>{{ i18n "password" }}</td>
-              <td>
-                <a-tooltip :title="[[ infoModal.clientSettings.password  ]]">
-                  <a-tag class="info-large-tag">[[
-                    infoModal.clientSettings.password ]]</a-tag>
-                </a-tooltip>
-              </td>
-            </tr>
-            <tr>
-              <td>{{ i18n "status" }}</td>
-              <td>
-                <a-tag v-if="isDepleted" color="red">{{ i18n "depleted"
-                  }}</a-tag>
-                <a-tag v-else-if="isEnable" color="green">{{ i18n "enabled"
-                  }}</a-tag>
-                <a-tag v-else>{{ i18n "disabled" }}</a-tag>
-              </td>
-            </tr>
-            <tr v-if="infoModal.clientStats">
-              <td>{{ i18n "usage" }}</td>
-              <td>
-                <a-tag color="green">[[
-                  SizeFormatter.sizeFormat(infoModal.clientStats.up +
-                  infoModal.clientStats.down) ]]</a-tag>
-                <a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up)
-                  ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down)
-                  ]] ↓</a-tag>
-              </td>
-            </tr>
-            <tr>
-              <td>{{ i18n "pages.inbounds.createdAt" }}</td>
-              <td>
-                <template
-                  v-if="infoModal.clientSettings && infoModal.clientSettings.created_at">
-                  <a-tag>[[
-                    IntlUtil.formatDate(infoModal.clientSettings.created_at)
-                    ]]</a-tag>
-                </template>
-                <template v-else>
-                  <a-tag>-</a-tag>
-                </template>
-              </td>
-            </tr>
-            <tr>
-              <td>{{ i18n "pages.inbounds.updatedAt" }}</td>
-              <td>
-                <template
-                  v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at">
-                  <a-tag>[[
-                    IntlUtil.formatDate(infoModal.clientSettings.updated_at)
-                    ]]</a-tag>
-                </template>
-                <template v-else>
-                  <a-tag>-</a-tag>
-                </template>
-              </td>
-            </tr>
-            <tr>
-              <td>{{ i18n "lastOnline" }}</td>
-              <td>
-                <a-tag>[[ app.formatLastOnline(infoModal.clientSettings &&
-                  infoModal.clientSettings.email ?
-                  infoModal.clientSettings.email : '') ]]</a-tag>
-              </td>
-            </tr>
-            <tr v-if="infoModal.clientSettings.comment">
-              <td>{{ i18n "comment" }}</td>
-              <td>
-                <a-tooltip :title="[[ infoModal.clientSettings.comment  ]]">
-                  <a-tag class="info-large-tag">[[
-                    infoModal.clientSettings.comment ]]</a-tag>
-                </a-tooltip>
-              </td>
-            </tr>
-            <tr v-if="app.ipLimitEnable">
-              <td>{{ i18n "pages.inbounds.IPLimit" }}</td>
-              <td>
-                <a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>
-              </td>
-            </tr>
-            <tr
-              v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
-              <td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
-              <td>
-                <div
-                  style="max-height: 150px; overflow-y: auto; text-align: left;">
-                  <div
-                    v-if="infoModal.clientIpsArray && infoModal.clientIpsArray.length > 0">
-                    <a-tag
-                      v-for="(ipInfo, idx) in infoModal.clientIpsArray"
-                      :key="idx"
-                      color="blue"
-                      style="margin: 2px 0; display: block; font-family: monospace; font-size: 11px;">
-                      [[ formatIpInfo(ipInfo) ]]
-                    </a-tag>
-                  </div>
-                  <a-tag v-else>[[ infoModal.clientIps || 'No IP Record'
-                    ]]</a-tag>
-                </div>
-                <div style="margin-top: 5px;">
-                  <a-icon type="sync" :spin="refreshing" @click="refreshIPs"
-                    :style="{ margin: '0 5px' }"></a-icon>
-                  <a-tooltip>
-                    <template slot="title">
-                      <span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
-                    </template>
-                    <a-icon type="delete" @click="clearClientIps"></a-icon>
+          <tr class="client-table-odd-row">
+            <td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
+            <td>[[ peer.privateKey ]]</td>
+          </tr>
+          <tr>
+            <td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
+            <td>[[ peer.publicKey ]]</td>
+          </tr>
+          <tr class="client-table-odd-row">
+            <td>{{ i18n "pages.xray.wireguard.psk" }}</td>
+            <td>[[ peer.psk ]]</td>
+          </tr>
+          <tr>
+            <td>{{ i18n "pages.xray.wireguard.allowedIPs" }}</td>
+            <td>[[ peer.allowedIPs.join(",") ]]</td>
+          </tr>
+          <tr class="client-table-odd-row">
+            <td>Keep Alive</td>
+            <td>[[ peer.keepAlive ]]</td>
+          </tr>
+          <tr>
+            <td colspan="2">
+              <tr-info-row class="tr-info-row">
+                <tr-info-title class="tr-info-title">
+                  <a-tag color="blue">Config</a-tag>
+                  <a-tooltip title='{{ i18n "copy" }}'>
+                    <a-button :style="{ minWidth: '24px' }" size="small" icon="snippets"
+                      @click="copy(infoModal.links[index])"></a-button>
                   </a-tooltip>
                   </a-tooltip>
+                  <a-tooltip title='{{ i18n "download" }}'>
+                    <a-button :style="{ minWidth: '24px' }" size="small" icon="download"
+                      @click="FileManager.downloadTextFile(infoModal.links[index], `peer-${index + 1}.conf`)"></a-button>
+                  </a-tooltip>
+                </tr-info-title>
+                <div v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)"
+                  :style="{ borderRadius: '1rem', padding: '0.5rem' }" class="client-table-odd-row">
                 </div>
                 </div>
-              </td>
-            </tr>
-          </table>
-          <table
-            :style="{ display: 'inline-table', marginBlock: '10px', width: '100%', textAlign: 'center' }">
-            <tr>
-              <th>{{ i18n "remained" }}</th>
-              <th>{{ i18n "pages.inbounds.totalFlow" }}</th>
-              <th>{{ i18n "pages.inbounds.expireDate" }}</th>
-            </tr>
-            <tr>
-              <td>
-                <a-tag
-                  v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0"
-                  :color="statsColor(infoModal.clientStats)"> [[ getRemStats()
-                  ]] </a-tag>
-              </td>
-              <td>
-                <a-tag v-if="infoModal.clientSettings.totalGB > 0"
-                  :color="statsColor(infoModal.clientStats)"> [[
-                  SizeFormatter.sizeFormat(infoModal.clientSettings.totalGB) ]]
-                </a-tag>
-                <a-tag v-else color="purple" class="infinite-tag">
-                  <svg height="10px" width="14px" viewBox="0 0 640 512"
-                    fill="currentColor">
-                    <path
-                      d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
-                      fill="currentColor"></path>
-                  </svg>
-                </a-tag>
-              </td>
-              <td>
-                <template v-if="infoModal.clientSettings.expiryTime > 0">
-                  <a-tag
-                    :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
-                    [[ IntlUtil.formatDate(infoModal.clientSettings.expiryTime)
-                    ]]
-                  </a-tag>
-                </template>
-                <a-tag v-else-if="infoModal.clientSettings.expiryTime < 0"
-                  color="green">[[ infoModal.clientSettings.expiryTime /
-                  -86400000 ]] {{ i18n "pages.client.days" }}
-                </a-tag>
-                <a-tag v-else color="purple" class="infinite-tag">
-                  <svg height="10px" width="14px" viewBox="0 0 640 512"
-                    fill="currentColor">
-                    <path
-                      d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
-                      fill="currentColor"></path>
-                  </svg>
-                </a-tag>
-              </td>
-            </tr>
-          </table>
-          <template
-            v-if="app.subSettings.enable && infoModal.clientSettings.subId">
-            <a-divider>Subscription URL</a-divider>
-            <tr-info-row class="tr-info-row">
-              <tr-info-title class="tr-info-title">
-                <a-tag color="purple">Subscription Link</a-tag>
-                <a-tooltip title='{{ i18n "copy" }}'>
-                  <a-button size="small" icon="snippets"
-                    @click="copy(infoModal.subLink)"></a-button>
-                </a-tooltip>
-              </tr-info-title>
-              <a :href="[[ infoModal.subLink ]]" target="_blank">[[
-                infoModal.subLink ]]</a>
-            </tr-info-row>
-            <tr-info-row class="tr-info-row"
-              v-if="app.subSettings.subJsonEnable">
-              <tr-info-title class="tr-info-title">
-                <a-tag color="purple">Json Link</a-tag>
-                <a-tooltip title='{{ i18n "copy" }}'>
-                  <a-button size="small" icon="snippets"
-                    @click="copy(infoModal.subJsonLink)"></a-button>
-                </a-tooltip>
-              </tr-info-title>
-              <a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[
-                infoModal.subJsonLink ]]</a>
-            </tr-info-row>
-          </template>
-          <template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
-            <a-divider>Telegram ChatID</a-divider>
-            <tr-info-row class="tr-info-row">
-              <tr-info-title class="tr-info-title">
-                <a-tag color="blue">[[ infoModal.clientSettings.tgId ]]</a-tag>
-                <a-tooltip title='{{ i18n "copy" }}'>
-                  <a-button size="small" icon="snippets"
-                    @click="copy(infoModal.clientSettings.tgId)"></a-button>
-                </a-tooltip>
-              </tr-info-title>
-            </tr-info-row>
-          </template>
-          <template v-if="dbInbound.hasLink()">
-            <a-divider>URL</a-divider>
-            <tr-info-row v-for="(link,index) in infoModal.links"
-              class="tr-info-row">
-              <tr-info-title class="tr-info-title">
-                <a-tag class="tr-info-tag" color="green">[[ link.remark
-                  ]]</a-tag>
-                <a-tooltip title='{{ i18n "copy" }}'>
-                  <a-button :style="{ minWidth: '24px' }" size="small"
-                    icon="snippets" @click="copy(link.link)"></a-button>
-                </a-tooltip>
-              </tr-info-title>
-              <code>[[ link.link ]]</code>
-            </tr-info-row>
-          </template>
-        </template>
-        <template v-else>
-          <template v-if="dbInbound.isSS && !inbound.isSSMultiUser">
-            <a-divider>URL</a-divider>
-            <tr-info-row v-for="(link,index) in infoModal.links"
-              class="tr-info-row">
-              <tr-info-title class="tr-info-title">
-                <a-tag class="tr-info-tag" color="green">[[ link.remark
-                  ]]</a-tag>
-                <a-tooltip title='{{ i18n "copy" }}'>
-                  <a-button :style="{ minWidth: '24px' }" size="small"
-                    icon="snippets" @click="copy(link.link)"></a-button>
-                </a-tooltip>
-              </tr-info-title>
-              <code>[[ link.link ]]</code>
-            </tr-info-row>
-          </template>
-          <table v-if="inbound.protocol == Protocols.TUNNEL"
-            class="tr-info-table">
-            <tr>
-              <th>{{ i18n "pages.inbounds.targetAddress" }}</th>
-              <th>{{ i18n "pages.inbounds.destinationPort" }}</th>
-              <th>{{ i18n "pages.inbounds.network" }}</th>
-              <th>FollowRedirect</th>
-            </tr>
-            <tr>
-              <td>
-                <a-tag color="green">[[ inbound.settings.address ]]</a-tag>
-              </td>
-              <td>
-                <a-tag color="green">[[ inbound.settings.port ]]</a-tag>
-              </td>
-              <td>
-                <a-tag color="green">[[ inbound.settings.network ]]</a-tag>
-              </td>
-              <td>
-                <a-tag color="green">[[ inbound.settings.followRedirect
-                  ]]</a-tag>
-              </td>
-            </tr>
-          </table>
-          <table v-if="dbInbound.isMixed" class="tr-info-table">
-            <tr>
-              <th>{{ i18n "password" }} Auth</th>
-              <th>{{ i18n "pages.inbounds.enable" }} udp</th>
-              <th>IP</th>
-            </tr>
-            <tr>
-              <td>
-                <a-tag color="green">[[ inbound.settings.auth ]]</a-tag>
-              </td>
-              <td>
-                <a-tag color="green">[[ inbound.settings.udp]]</a-tag>
-              </td>
-              <td>
-                <a-tag color="green">[[ inbound.settings.ip ]]</a-tag>
-              </td>
-            </tr>
-            <template v-if="inbound.settings.auth == 'password'">
-              <tr>
-                <td></td>
-                <td>{{ i18n "username" }}</td>
-                <td>{{ i18n "password" }}</td>
-              </tr>
-              <tr v-for="account,index in inbound.settings.accounts">
-                <td>[[ index ]]</td>
-                <td>
-                  <a-tag color="green">[[ account.user ]]</a-tag>
-                </td>
-                <td>
-                  <a-tag color="green">[[ account.pass ]]</a-tag>
-                </td>
-              </tr>
-            </template>
-          </table>
-          <table v-if="dbInbound.isHTTP" class="tr-info-table">
-            <tr>
-              <th></th>
-              <th>{{ i18n "username" }}</th>
-              <th>{{ i18n "password" }}</th>
-            </tr>
-            <tr v-for="account,index in inbound.settings.accounts">
-              <td>[[ index ]]</td>
-              <td>
-                <a-tag color="green">[[ account.user ]]</a-tag>
-              </td>
-              <td>
-                <a-tag color="green">[[ account.pass ]]</a-tag>
-              </td>
-            </tr>
-          </table>
-          <table v-if="dbInbound.isWireguard" class="tr-info-table">
-            <tr class="client-table-odd-row">
-              <td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
-              <td>[[ inbound.settings.secretKey ]]</td>
-            </tr>
-            <tr>
-              <td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
-              <td>[[ inbound.settings.pubKey ]]</td>
-            </tr>
-            <tr class="client-table-odd-row">
-              <td>MTU</td>
-              <td>[[ inbound.settings.mtu ]]</td>
-            </tr>
-            <tr>
-              <td>No Kernel Tun</td>
-              <td>[[ inbound.settings.noKernelTun ]]</td>
-            </tr>
-            <template v-for="(peer, index) in inbound.settings.peers">
-              <tr>
-                <td colspan="2">
-                  <a-divider>Peer [[ index + 1 ]]</a-divider>
-                </td>
-              </tr>
-              <tr class="client-table-odd-row">
-                <td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
-                <td>[[ peer.privateKey ]]</td>
-              </tr>
-              <tr>
-                <td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
-                <td>[[ peer.publicKey ]]</td>
-              </tr>
-              <tr class="client-table-odd-row">
-                <td>{{ i18n "pages.xray.wireguard.psk" }}</td>
-                <td>[[ peer.psk ]]</td>
-              </tr>
-              <tr>
-                <td>{{ i18n "pages.xray.wireguard.allowedIPs" }}</td>
-                <td>[[ peer.allowedIPs.join(",") ]]</td>
-              </tr>
-              <tr class="client-table-odd-row">
-                <td>Keep Alive</td>
-                <td>[[ peer.keepAlive ]]</td>
-              </tr>
-              <tr>
-                <td colspan="2">
-                  <tr-info-row class="tr-info-row">
-                    <tr-info-title class="tr-info-title">
-                      <a-tag color="blue">Config</a-tag>
-                      <a-tooltip title='{{ i18n "copy" }}'>
-                        <a-button :style="{ minWidth: '24px' }" size="small"
-                          icon="snippets"
-                          @click="copy(infoModal.links[index])"></a-button>
-                      </a-tooltip>
-                      <a-tooltip title='{{ i18n "download" }}'>
-                        <a-button :style="{ minWidth: '24px' }" size="small"
-                          icon="download"
-                          @click="FileManager.downloadTextFile(infoModal.links[index], `peer-${index + 1}.conf`)"></a-button>
-                      </a-tooltip>
-                    </tr-info-title>
-                    <div
-                      v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)"
-                      :style="{ borderRadius: '1rem', padding: '0.5rem' }"
-                      class="client-table-odd-row">
-                    </div>
-                  </tr-info-row>
-                </td>
-              </tr>
-            </table>
-          </template>
-        </template>
-      </a-modal>
-      <script>
+              </tr-info-row>
+            </td>
+          </tr>
+      </table>
+    </template>
+    </template>
+</a-modal>
+<script>
   function refreshIPs(email) {
   function refreshIPs(email) {
     return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
     return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
       if (!msg.success) {
       if (!msg.success) {
@@ -654,9 +619,9 @@
 
 
       if (
       if (
         [
         [
-          Protocols.VMESS, 
+          Protocols.VMESS,
           Protocols.VLESS,
           Protocols.VLESS,
-          Protocols.TROJAN, 
+          Protocols.TROJAN,
           Protocols.SHADOWSOCKS
           Protocols.SHADOWSOCKS
         ].includes(this.inbound.protocol)
         ].includes(this.inbound.protocol)
       ) {
       ) {
@@ -797,9 +762,9 @@
             this.infoModal.clientIps = 'No IP Record';
             this.infoModal.clientIps = 'No IP Record';
             this.infoModal.clientIpsArray = [];
             this.infoModal.clientIpsArray = [];
           })
           })
-          .catch(() => {});
+          .catch(() => { });
       },
       },
     },
     },
   });
   });
 </script>
 </script>
-      {{end}}
+{{end}}

+ 134 - 65
web/html/modals/inbound_modal.html

@@ -5,10 +5,9 @@
     {{template "form/inbound"}}
     {{template "form/inbound"}}
 </a-modal>
 </a-modal>
 <script>
 <script>
-
     // Make inModal globally available to ensure it works with any base path
     // Make inModal globally available to ensure it works with any base path
-    const inModal = window.inModal = {
-        title: '',
+    const inModal = (window.inModal = {
+        title: "",
         visible: false,
         visible: false,
         confirmLoading: false,
         confirmLoading: false,
         okText: '{{ i18n "sure" }}',
         okText: '{{ i18n "sure" }}',
@@ -19,7 +18,14 @@
         ok() {
         ok() {
             ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
             ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
         },
         },
-        show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => { }, isEdit = false }) {
+        show({
+            title = "",
+            okText = '{{ i18n "sure" }}',
+            inbound = null,
+            dbInbound = null,
+            confirm = (inbound, dbInbound) => { },
+            isEdit = false,
+        }) {
             this.title = title;
             this.title = title;
             this.okText = okText;
             this.okText = okText;
             if (inbound) {
             if (inbound) {
@@ -30,7 +36,11 @@
             // Always ensure testseed is initialized for VLESS protocol (even if vision flow is not set yet)
             // Always ensure testseed is initialized for VLESS protocol (even if vision flow is not set yet)
             // This ensures Vue reactivity works properly
             // This ensures Vue reactivity works properly
             if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) {
             if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) {
-                if (!this.inbound.settings.testseed || !Array.isArray(this.inbound.settings.testseed) || this.inbound.settings.testseed.length < 4) {
+                if (
+                    !this.inbound.settings.testseed ||
+                    !Array.isArray(this.inbound.settings.testseed) ||
+                    this.inbound.settings.testseed.length < 4
+                ) {
                     // Create a new array to ensure Vue reactivity
                     // Create a new array to ensure Vue reactivity
                     this.inbound.settings.testseed = [900, 500, 900, 256].slice();
                     this.inbound.settings.testseed = [900, 500, 900, 256].slice();
                 }
                 }
@@ -56,7 +66,10 @@
             // Use inModal.inbound explicitly to ensure correct context
             // Use inModal.inbound explicitly to ensure correct context
             if (!inModal.inbound || !inModal.inbound.settings) return;
             if (!inModal.inbound || !inModal.inbound.settings) return;
             // Ensure testseed is initialized
             // Ensure testseed is initialized
-            if (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed)) {
+            if (
+                !inModal.inbound.settings.testseed ||
+                !Array.isArray(inModal.inbound.settings.testseed)
+            ) {
                 inModal.inbound.settings.testseed = [900, 500, 900, 256];
                 inModal.inbound.settings.testseed = [900, 500, 900, 256];
             }
             }
             // Ensure array has enough elements
             // Ensure array has enough elements
@@ -70,26 +83,35 @@
             // Use inModal.inbound explicitly to ensure correct context
             // Use inModal.inbound explicitly to ensure correct context
             if (!inModal.inbound || !inModal.inbound.settings) return;
             if (!inModal.inbound || !inModal.inbound.settings) return;
             // Ensure testseed is initialized
             // Ensure testseed is initialized
-            if (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed) || inModal.inbound.settings.testseed.length < 4) {
+            if (
+                !inModal.inbound.settings.testseed ||
+                !Array.isArray(inModal.inbound.settings.testseed) ||
+                inModal.inbound.settings.testseed.length < 4
+            ) {
                 inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
                 inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
             }
             }
             // Create new array with random values
             // Create new array with random values
-            inModal.inbound.settings.testseed = [Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000)];
+            inModal.inbound.settings.testseed = [
+                Math.floor(Math.random() * 1000),
+                Math.floor(Math.random() * 1000),
+                Math.floor(Math.random() * 1000),
+                Math.floor(Math.random() * 1000),
+            ];
         },
         },
         resetTestseed() {
         resetTestseed() {
             // Use inModal.inbound explicitly to ensure correct context
             // Use inModal.inbound explicitly to ensure correct context
             if (!inModal.inbound || !inModal.inbound.settings) return;
             if (!inModal.inbound || !inModal.inbound.settings) return;
             // Reset testseed to default values
             // Reset testseed to default values
             inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
             inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
-        }
-    };
+        },
+    });
 
 
     // Store Vue instance globally to ensure methods are always accessible
     // Store Vue instance globally to ensure methods are always accessible
     let inboundModalVueInstance = null;
     let inboundModalVueInstance = null;
-    
+
     inboundModalVueInstance = new Vue({
     inboundModalVueInstance = new Vue({
-        delimiters: ['[[', ']]'],
-        el: '#inbound-modal',
+        delimiters: ["[[", "]]"],
+        el: "#inbound-modal",
         data: {
         data: {
             inModal: inModal,
             inModal: inModal,
             delayedStart: false,
             delayedStart: false,
@@ -103,13 +125,19 @@
                 return inModal.isEdit;
                 return inModal.isEdit;
             },
             },
             get client() {
             get client() {
-                return inModal.inbound && inModal.inbound.clients && inModal.inbound.clients.length > 0 ? inModal.inbound.clients[0] : null;
+                return inModal.inbound &&
+                    inModal.inbound.clients &&
+                    inModal.inbound.clients.length > 0
+                    ? inModal.inbound.clients[0]
+                    : null;
             },
             },
             get datepicker() {
             get datepicker() {
                 return app.datepicker;
                 return app.datepicker;
             },
             },
             get delayedExpireDays() {
             get delayedExpireDays() {
-                return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
+                return this.client && this.client.expiryTime < 0
+                    ? this.client.expiryTime / -86400000
+                    : 0;
             },
             },
             set delayedExpireDays(days) {
             set delayedExpireDays(days) {
                 this.client.expiryTime = -86400000 * days;
                 this.client.expiryTime = -86400000 * days;
@@ -119,72 +147,103 @@
             },
             },
             set externalProxy(value) {
             set externalProxy(value) {
                 if (value) {
                 if (value) {
-                    inModal.inbound.stream.externalProxy = [{
-                        forceTls: "same",
-                        dest: window.location.hostname,
-                        port: inModal.inbound.port,
-                        remark: ""
-                    }];
+                    inModal.inbound.stream.externalProxy = [
+                        {
+                            forceTls: "same",
+                            dest: window.location.hostname,
+                            port: inModal.inbound.port,
+                            remark: "",
+                        },
+                    ];
                 } else {
                 } else {
                     inModal.inbound.stream.externalProxy = [];
                     inModal.inbound.stream.externalProxy = [];
                 }
                 }
-            }
+            },
         },
         },
         watch: {
         watch: {
-            'inModal.inbound.stream.security'(newVal, oldVal) {
+            "inModal.inbound.stream.security"(newVal, oldVal) {
                 // Clear flow when security changes from reality/tls to none
                 // Clear flow when security changes from reality/tls to none
-                if (inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {
-                    inModal.inbound.settings.vlesses.forEach(client => {
+                if (
+                    inModal.inbound.protocol == Protocols.VLESS &&
+                    !inModal.inbound.canEnableTlsFlow()
+                ) {
+                    inModal.inbound.settings.vlesses.forEach((client) => {
                         client.flow = "";
                         client.flow = "";
                     });
                     });
                 }
                 }
             },
             },
             // Ensure testseed is always initialized when vision flow is enabled
             // Ensure testseed is always initialized when vision flow is enabled
-            'inModal.inbound.settings.vlesses': {
+            "inModal.inbound.settings.vlesses": {
                 handler() {
                 handler() {
-                    if (inModal.inbound.protocol === Protocols.VLESS && inModal.inbound.settings && inModal.inbound.settings.vlesses) {
-                        const hasVisionFlow = inModal.inbound.settings.vlesses.some(c => c.flow === 'xtls-rprx-vision' || c.flow === 'xtls-rprx-vision-udp443');
-                        if (hasVisionFlow && (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed) || inModal.inbound.settings.testseed.length < 4)) {
+                    if (
+                        inModal.inbound.protocol === Protocols.VLESS &&
+                        inModal.inbound.settings &&
+                        inModal.inbound.settings.vlesses
+                    ) {
+                        const hasVisionFlow = inModal.inbound.settings.vlesses.some(
+                            (c) =>
+                                c.flow === "xtls-rprx-vision" ||
+                                c.flow === "xtls-rprx-vision-udp443",
+                        );
+                        if (
+                            hasVisionFlow &&
+                            (!inModal.inbound.settings.testseed ||
+                                !Array.isArray(inModal.inbound.settings.testseed) ||
+                                inModal.inbound.settings.testseed.length < 4)
+                        ) {
                             inModal.inbound.settings.testseed = [900, 500, 900, 256];
                             inModal.inbound.settings.testseed = [900, 500, 900, 256];
                         }
                         }
                     }
                     }
                 },
                 },
-                deep: true
-            }
+                deep: true,
+            },
         },
         },
         methods: {
         methods: {
             streamNetworkChange() {
             streamNetworkChange() {
                 if (!inModal.inbound.canEnableTls()) {
                 if (!inModal.inbound.canEnableTls()) {
-                    this.inModal.inbound.stream.security = 'none';
+                    this.inModal.inbound.stream.security = "none";
                 }
                 }
                 if (!inModal.inbound.canEnableReality()) {
                 if (!inModal.inbound.canEnableReality()) {
                     this.inModal.inbound.reality = false;
                     this.inModal.inbound.reality = false;
                 }
                 }
-                if (this.inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {
-                    this.inModal.inbound.settings.vlesses.forEach(client => {
+                if (
+                    this.inModal.inbound.protocol == Protocols.VLESS &&
+                    !inModal.inbound.canEnableTlsFlow()
+                ) {
+                    this.inModal.inbound.settings.vlesses.forEach((client) => {
                         client.flow = "";
                         client.flow = "";
                     });
                     });
                 }
                 }
+                if (inModal.inbound.stream.network != "kcp") {
+                    inModal.inbound.stream.finalmask.udp = [];
+                }
             },
             },
             SSMethodChange() {
             SSMethodChange() {
-                this.inModal.inbound.settings.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
+                this.inModal.inbound.settings.password =
+                    RandomUtil.randomShadowsocksPassword(
+                        this.inModal.inbound.settings.method,
+                    );
 
 
                 if (this.inModal.inbound.isSSMultiUser) {
                 if (this.inModal.inbound.isSSMultiUser) {
                     if (this.inModal.inbound.settings.shadowsockses.length == 0) {
                     if (this.inModal.inbound.settings.shadowsockses.length == 0) {
-                        this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
+                        this.inModal.inbound.settings.shadowsockses = [
+                            new Inbound.ShadowsocksSettings.Shadowsocks(),
+                        ];
                     }
                     }
                     if (!this.inModal.inbound.isSS2022) {
                     if (!this.inModal.inbound.isSS2022) {
-                        this.inModal.inbound.settings.shadowsockses.forEach(client => {
+                        this.inModal.inbound.settings.shadowsockses.forEach((client) => {
                             client.method = this.inModal.inbound.settings.method;
                             client.method = this.inModal.inbound.settings.method;
-                        })
+                        });
                     } else {
                     } else {
-                        this.inModal.inbound.settings.shadowsockses.forEach(client => {
+                        this.inModal.inbound.settings.shadowsockses.forEach((client) => {
                             client.method = "";
                             client.method = "";
-                        })
+                        });
                     }
                     }
-                    this.inModal.inbound.settings.shadowsockses.forEach(client => {
-                        client.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
-                    })
+                    this.inModal.inbound.settings.shadowsockses.forEach((client) => {
+                        client.password = RandomUtil.randomShadowsocksPassword(
+                            this.inModal.inbound.settings.method,
+                        );
+                    });
                 } else {
                 } else {
                     if (this.inModal.inbound.settings.shadowsockses.length > 0) {
                     if (this.inModal.inbound.settings.shadowsockses.length > 0) {
                         this.inModal.inbound.settings.shadowsockses = [];
                         this.inModal.inbound.settings.shadowsockses = [];
@@ -197,7 +256,7 @@
             },
             },
             async getNewX25519Cert() {
             async getNewX25519Cert() {
                 inModal.loading(true);
                 inModal.loading(true);
-                const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
+                const msg = await HttpUtil.get("/panel/api/server/getNewX25519Cert");
                 inModal.loading(false);
                 inModal.loading(false);
                 if (!msg.success) {
                 if (!msg.success) {
                     return;
                     return;
@@ -206,12 +265,12 @@
                 inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
                 inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
             },
             },
             clearX25519Cert() {
             clearX25519Cert() {
-                this.inbound.stream.reality.privateKey = '';
-                this.inbound.stream.reality.settings.publicKey = '';
+                this.inbound.stream.reality.privateKey = "";
+                this.inbound.stream.reality.settings.publicKey = "";
             },
             },
             async getNewmldsa65() {
             async getNewmldsa65() {
                 inModal.loading(true);
                 inModal.loading(true);
-                const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
+                const msg = await HttpUtil.get("/panel/api/server/getNewmldsa65");
                 inModal.loading(false);
                 inModal.loading(false);
                 if (!msg.success) {
                 if (!msg.success) {
                     return;
                     return;
@@ -220,11 +279,11 @@
                 inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
                 inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
             },
             },
             clearMldsa65() {
             clearMldsa65() {
-                this.inbound.stream.reality.mldsa65Seed = '';
-                this.inbound.stream.reality.settings.mldsa65Verify = '';
+                this.inbound.stream.reality.mldsa65Seed = "";
+                this.inbound.stream.reality.settings.mldsa65Verify = "";
             },
             },
             randomizeRealityTarget() {
             randomizeRealityTarget() {
-                if (typeof getRandomRealityTarget !== 'undefined') {
+                if (typeof getRandomRealityTarget !== "undefined") {
                     const randomTarget = getRandomRealityTarget();
                     const randomTarget = getRandomRealityTarget();
                     this.inbound.stream.reality.target = randomTarget.target;
                     this.inbound.stream.reality.target = randomTarget.target;
                     this.inbound.stream.reality.serverNames = randomTarget.sni;
                     this.inbound.stream.reality.serverNames = randomTarget.sni;
@@ -232,21 +291,24 @@
             },
             },
             async getNewEchCert() {
             async getNewEchCert() {
                 inModal.loading(true);
                 inModal.loading(true);
-                const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });
+                const msg = await HttpUtil.post("/panel/api/server/getNewEchCert", {
+                    sni: inModal.inbound.stream.tls.sni,
+                });
                 inModal.loading(false);
                 inModal.loading(false);
                 if (!msg.success) {
                 if (!msg.success) {
                     return;
                     return;
                 }
                 }
                 inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
                 inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
-                inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;
+                inModal.inbound.stream.tls.settings.echConfigList =
+                    msg.obj.echConfigList;
             },
             },
             clearEchCert() {
             clearEchCert() {
-                this.inbound.stream.tls.echServerKeys = '';
-                this.inbound.stream.tls.settings.echConfigList = '';
+                this.inbound.stream.tls.echServerKeys = "";
+                this.inbound.stream.tls.settings.echConfigList = "";
             },
             },
             async getNewVlessEnc() {
             async getNewVlessEnc() {
                 inModal.loading(true);
                 inModal.loading(true);
-                const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
+                const msg = await HttpUtil.get("/panel/api/server/getNewVlessEnc");
                 inModal.loading(false);
                 inModal.loading(false);
 
 
                 if (!msg.success) {
                 if (!msg.success) {
@@ -255,7 +317,7 @@
 
 
                 const auths = msg.obj.auths || [];
                 const auths = msg.obj.auths || [];
                 const selected = inModal.inbound.settings.selectedAuth;
                 const selected = inModal.inbound.settings.selectedAuth;
-                const block = auths.find(a => a.label === selected);
+                const block = auths.find((a) => a.label === selected);
 
 
                 if (!block) {
                 if (!block) {
                     console.error("No auth block for", selected);
                     console.error("No auth block for", selected);
@@ -266,15 +328,18 @@
                 inModal.inbound.settings.encryption = block.encryption;
                 inModal.inbound.settings.encryption = block.encryption;
             },
             },
             clearVlessEnc() {
             clearVlessEnc() {
-                this.inbound.settings.decryption = 'none';
-                this.inbound.settings.encryption = 'none';
+                this.inbound.settings.decryption = "none";
+                this.inbound.settings.encryption = "none";
                 this.inbound.settings.selectedAuth = undefined;
                 this.inbound.settings.selectedAuth = undefined;
             },
             },
             // Vision Seed methods - must be in Vue methods for proper binding
             // Vision Seed methods - must be in Vue methods for proper binding
             updateTestseed(index, value) {
             updateTestseed(index, value) {
                 // Ensure testseed is initialized
                 // Ensure testseed is initialized
-                if (!this.inbound.settings.testseed || !Array.isArray(this.inbound.settings.testseed)) {
-                    this.$set(this.inbound.settings, 'testseed', [900, 500, 900, 256]);
+                if (
+                    !this.inbound.settings.testseed ||
+                    !Array.isArray(this.inbound.settings.testseed)
+                ) {
+                    this.$set(this.inbound.settings, "testseed", [900, 500, 900, 256]);
                 }
                 }
                 // Ensure array has enough elements
                 // Ensure array has enough elements
                 while (this.inbound.settings.testseed.length <= index) {
                 while (this.inbound.settings.testseed.length <= index) {
@@ -285,15 +350,19 @@
             },
             },
             setRandomTestseed() {
             setRandomTestseed() {
                 // Create new array with random values and use Vue.set for reactivity
                 // Create new array with random values and use Vue.set for reactivity
-                const newSeed = [Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000)];
-                this.$set(this.inbound.settings, 'testseed', newSeed);
+                const newSeed = [
+                    Math.floor(Math.random() * 1000),
+                    Math.floor(Math.random() * 1000),
+                    Math.floor(Math.random() * 1000),
+                    Math.floor(Math.random() * 1000),
+                ];
+                this.$set(this.inbound.settings, "testseed", newSeed);
             },
             },
             resetTestseed() {
             resetTestseed() {
                 // Reset testseed to default values using Vue.set for reactivity
                 // Reset testseed to default values using Vue.set for reactivity
-                this.$set(this.inbound.settings, 'testseed', [900, 500, 900, 256]);
-            }
+                this.$set(this.inbound.settings, "testseed", [900, 500, 900, 256]);
+            },
         },
         },
     });
     });
-
 </script>
 </script>
 {{end}}
 {{end}}

+ 19 - 3
web/service/inbound.go

@@ -270,6 +270,10 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
 			if client.Email == "" {
 			if client.Email == "" {
 				return inbound, false, common.NewError("empty client ID")
 				return inbound, false, common.NewError("empty client ID")
 			}
 			}
+		case "hysteria":
+			if client.Auth == "" {
+				return inbound, false, common.NewError("empty client ID")
+			}
 		default:
 		default:
 			if client.ID == "" {
 			if client.ID == "" {
 				return inbound, false, common.NewError("empty client ID")
 				return inbound, false, common.NewError("empty client ID")
@@ -606,6 +610,10 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
 			if client.Email == "" {
 			if client.Email == "" {
 				return false, common.NewError("empty client ID")
 				return false, common.NewError("empty client ID")
 			}
 			}
+		case "hysteria":
+			if client.Auth == "" {
+				return false, common.NewError("empty client ID")
+			}
 		default:
 		default:
 			if client.ID == "" {
 			if client.ID == "" {
 				return false, common.NewError("empty client ID")
 				return false, common.NewError("empty client ID")
@@ -655,6 +663,7 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
 				err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{
 				err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{
 					"email":    client.Email,
 					"email":    client.Email,
 					"id":       client.ID,
 					"id":       client.ID,
+					"auth":     client.Auth,
 					"security": client.Security,
 					"security": client.Security,
 					"flow":     client.Flow,
 					"flow":     client.Flow,
 					"password": client.Password,
 					"password": client.Password,
@@ -690,11 +699,13 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
 
 
 	email := ""
 	email := ""
 	client_key := "id"
 	client_key := "id"
-	if oldInbound.Protocol == "trojan" {
+	switch oldInbound.Protocol {
+	case "trojan":
 		client_key = "password"
 		client_key = "password"
-	}
-	if oldInbound.Protocol == "shadowsocks" {
+	case "shadowsocks":
 		client_key = "email"
 		client_key = "email"
+	case "hysteria":
+		client_key = "auth"
 	}
 	}
 
 
 	interfaceClients := settings["clients"].([]any)
 	interfaceClients := settings["clients"].([]any)
@@ -801,6 +812,9 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
 		case "shadowsocks":
 		case "shadowsocks":
 			oldClientId = oldClient.Email
 			oldClientId = oldClient.Email
 			newClientId = clients[0].Email
 			newClientId = clients[0].Email
+		case "hysteria":
+			oldClientId = oldClient.Auth
+			newClientId = clients[0].Auth
 		default:
 		default:
 			oldClientId = oldClient.ID
 			oldClientId = oldClient.ID
 			newClientId = clients[0].ID
 			newClientId = clients[0].ID
@@ -921,6 +935,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
 				"id":       clients[0].ID,
 				"id":       clients[0].ID,
 				"security": clients[0].Security,
 				"security": clients[0].Security,
 				"flow":     clients[0].Flow,
 				"flow":     clients[0].Flow,
+				"auth":     clients[0].Auth,
 				"password": clients[0].Password,
 				"password": clients[0].Password,
 				"cipher":   cipher,
 				"cipher":   cipher,
 			})
 			})
@@ -1813,6 +1828,7 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, e
 				err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]any{
 				err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]any{
 					"email":    client.Email,
 					"email":    client.Email,
 					"id":       client.ID,
 					"id":       client.ID,
+					"auth":     client.Auth,
 					"security": client.Security,
 					"security": client.Security,
 					"flow":     client.Flow,
 					"flow":     client.Flow,
 					"password": client.Password,
 					"password": client.Password,

+ 2 - 2
web/service/xray.go

@@ -148,10 +148,10 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
 
 
 				// clear client config for additional parameters
 				// clear client config for additional parameters
 				for key := range c {
 				for key := range c {
-					if key != "email" && key != "id" && key != "password" && key != "flow" && key != "method" {
+					if key != "email" && key != "id" && key != "password" && key != "flow" && key != "method" && key != "auth" {
 						delete(c, key)
 						delete(c, key)
 					}
 					}
-					if c["flow"] == "xtls-rprx-vision-udp443" {
+					if flow, ok := c["flow"].(string); ok && flow == "xtls-rprx-vision-udp443" {
 						c["flow"] = "xtls-rprx-vision"
 						c["flow"] = "xtls-rprx-vision"
 					}
 					}
 				}
 				}

+ 0 - 12
web/session/session.go

@@ -31,18 +31,6 @@ func SetLoginUser(c *gin.Context, user *model.User) {
 	s.Set(loginUserKey, *user)
 	s.Set(loginUserKey, *user)
 }
 }
 
 
-// SetMaxAge configures the session cookie maximum age in seconds.
-// This controls how long the session remains valid before requiring re-authentication.
-func SetMaxAge(c *gin.Context, maxAge int) {
-	s := sessions.Default(c)
-	s.Options(sessions.Options{
-		Path:     defaultPath,
-		MaxAge:   maxAge,
-		HttpOnly: true,
-		SameSite: http.SameSiteLaxMode,
-	})
-}
-
 // GetLoginUser retrieves the authenticated user from the session.
 // GetLoginUser retrieves the authenticated user from the session.
 // Returns nil if no user is logged in or if the session data is invalid.
 // Returns nil if no user is logged in or if the session data is invalid.
 func GetLoginUser(c *gin.Context) *model.User {
 func GetLoginUser(c *gin.Context) *model.User {

+ 8 - 7
web/web.go

@@ -207,14 +207,15 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 
 
 	store := cookie.NewStore(secret)
 	store := cookie.NewStore(secret)
 	// Configure default session cookie options, including expiration (MaxAge)
 	// Configure default session cookie options, including expiration (MaxAge)
-	if sessionMaxAge, err := s.settingService.GetSessionMaxAge(); err == nil {
-		store.Options(sessions.Options{
-			Path:     "/",
-			MaxAge:   sessionMaxAge * 60, // minutes -> seconds
-			HttpOnly: true,
-			SameSite: http.SameSiteLaxMode,
-		})
+	sessionOptions := sessions.Options{
+		Path:     basePath,
+		HttpOnly: true,
+		SameSite: http.SameSiteLaxMode,
 	}
 	}
+	if sessionMaxAge, err := s.settingService.GetSessionMaxAge(); err == nil && sessionMaxAge > 0 {
+		sessionOptions.MaxAge = sessionMaxAge * 60 // minutes -> seconds
+	}
+	store.Options(sessionOptions)
 	engine.Use(sessions.Sessions("3x-ui", store))
 	engine.Use(sessions.Sessions("3x-ui", store))
 	engine.Use(func(c *gin.Context) {
 	engine.Use(func(c *gin.Context) {
 		c.Set("base_path", basePath)
 		c.Set("base_path", basePath)

+ 5 - 0
xray/api.go

@@ -19,6 +19,7 @@ import (
 	"github.com/xtls/xray-core/common/protocol"
 	"github.com/xtls/xray-core/common/protocol"
 	"github.com/xtls/xray-core/common/serial"
 	"github.com/xtls/xray-core/common/serial"
 	"github.com/xtls/xray-core/infra/conf"
 	"github.com/xtls/xray-core/infra/conf"
+	hysteriaAccount "github.com/xtls/xray-core/proxy/hysteria/account"
 	"github.com/xtls/xray-core/proxy/shadowsocks"
 	"github.com/xtls/xray-core/proxy/shadowsocks"
 	"github.com/xtls/xray-core/proxy/shadowsocks_2022"
 	"github.com/xtls/xray-core/proxy/shadowsocks_2022"
 	"github.com/xtls/xray-core/proxy/trojan"
 	"github.com/xtls/xray-core/proxy/trojan"
@@ -167,6 +168,10 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an
 				Email: user["email"].(string),
 				Email: user["email"].(string),
 			})
 			})
 		}
 		}
+	case "hysteria":
+		account = serial.ToTypedMessage(&hysteriaAccount.Account{
+			Auth: user["auth"].(string),
+		})
 	default:
 	default:
 		return nil
 		return nil
 	}
 	}

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác