mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-07 13:24:21 +00:00
d97bd8643e
Replace the upstream 9seconds/mtg sidecar with the dolonet/mtg-multi fork so a single MTProto inbound can serve many per-user secrets. Each panel client is now one named FakeTLS secret in the fork's [secrets] section: clients are first-class (attach/detach, limits, expiry, per-client tg:// links) exactly like every other protocol, mirroring the WireGuard multi-client model. Per-client traffic and online status come from the fork's /stats JSON API (its Prometheus output has no per-user label), fed into the existing email-keyed client_traffics accumulator; an optional throttle caps concurrent connections. A one-time seeder converts each legacy single-secret inbound into a one-client inbound. The fork ships only linux/darwin amd64/arm64 binaries but is pure Go, so provisioning builds it from source for every supported platform (release.yml, DockerInit.sh) while keeping the panel-expected mtg-<os>-<arch> filename and the 'run' verb, so process.go is untouched. Also fixes a pre-existing update.sh gap that never renamed the mtg binary for armv6/armv7 updates.
66 lines
1.8 KiB
Go
66 lines
1.8 KiB
Go
package mtproto
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
// serverPort extracts the loopback port a httptest server bound to, so
|
|
// scrapeStats can rebuild the same http://127.0.0.1:<port>/stats URL.
|
|
func serverPort(t *testing.T, srv *httptest.Server) int {
|
|
t.Helper()
|
|
u, err := url.Parse(srv.URL)
|
|
if err != nil {
|
|
t.Fatalf("parse url: %v", err)
|
|
}
|
|
port, err := strconv.Atoi(u.Port())
|
|
if err != nil {
|
|
t.Fatalf("parse port: %v", err)
|
|
}
|
|
return port
|
|
}
|
|
|
|
func TestScrapeStats(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/stats" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_, _ = io.WriteString(w, `{"started_at":"2026-01-01T00:00:00Z","total_connections":2,`+
|
|
`"users":{`+
|
|
`"alice":{"connections":2,"bytes_in":100,"bytes_out":200,"last_seen":"2026-01-01T00:01:00Z"},`+
|
|
`"bob":{"connections":0,"bytes_in":5,"bytes_out":7,"last_seen":null}}}`)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
users, ok := scrapeStats(serverPort(t, srv))
|
|
if !ok {
|
|
t.Fatal("scrapeStats should succeed against a valid /stats endpoint")
|
|
}
|
|
if len(users) != 2 {
|
|
t.Fatalf("expected 2 users, got %d: %+v", len(users), users)
|
|
}
|
|
if users["alice"].BytesIn != 100 || users["alice"].BytesOut != 200 || users["alice"].Connections != 2 {
|
|
t.Fatalf("alice stats parsed wrong: %+v", users["alice"])
|
|
}
|
|
if users["bob"].Connections != 0 || users["bob"].BytesIn != 5 {
|
|
t.Fatalf("bob stats parsed wrong: %+v", users["bob"])
|
|
}
|
|
}
|
|
|
|
func TestScrapeStatsUnreachable(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.NotFound(w, r)
|
|
}))
|
|
port := serverPort(t, srv)
|
|
srv.Close()
|
|
|
|
if _, ok := scrapeStats(port); ok {
|
|
t.Fatal("scrapeStats must report ok=false when the endpoint is unreachable")
|
|
}
|
|
}
|