mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-31 07:27:13 +00:00
feat(mtproto): adopt dolonet/mtg-multi and make MTProto inbounds multi-client
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.
This commit is contained in:
@@ -1,99 +1,65 @@
|
||||
package mtproto
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestParseMetricLineBraceBoundary pins the contract of the brace-position
|
||||
// guard in parseMetricLine (manager.go:425 -> `if end < brace`).
|
||||
//
|
||||
// Once a '{' is found at index `brace`, the matching '}' must appear AFTER it.
|
||||
// A '}' that precedes the '{', or a '{' with no closing '}' at all
|
||||
// (strings.IndexByte returns -1, which is < brace), is a malformed line and
|
||||
// must yield an error rather than slicing past the brace.
|
||||
func TestParseMetricLineBraceBoundary(t *testing.T) {
|
||||
t.Run("closing brace before opening brace is malformed", func(t *testing.T) {
|
||||
// '}' at index 8 comes before '{' at index 16: end < brace must hold,
|
||||
// so this is rejected. Mutating `<` to `>`/`>=` would accept it.
|
||||
_, _, _, err := parseMetricLine(`mtg_x_a}_b{direction="x"} 5`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for '}' appearing before '{'")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("opening brace with no closing brace is malformed", func(t *testing.T) {
|
||||
// No '}' at all -> end == -1, which is < brace. Must error.
|
||||
// If the guard were dropped/inverted the code would slice line[brace+1:-1]
|
||||
// and panic; asserting a clean error keeps that contract.
|
||||
_, _, _, err := parseMetricLine(`mtg_traffic{direction="x" 5`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for '{' without a closing '}'")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("well-formed braces are accepted", func(t *testing.T) {
|
||||
// '{' at index 11, '}' at index 25: end > brace, so the guard must NOT
|
||||
// fire and parsing must succeed. Guards against a mutant that always errors.
|
||||
name, labels, val, err := parseMetricLine(`mtg_traffic{direction="up"} 42`)
|
||||
if err != nil {
|
||||
t.Fatalf("well-formed line should parse: %v", err)
|
||||
}
|
||||
if name != "mtg_traffic" {
|
||||
t.Fatalf("name=%q", name)
|
||||
}
|
||||
if labels["direction"] != "up" {
|
||||
t.Fatalf("labels=%v", labels)
|
||||
}
|
||||
if val != 42 {
|
||||
t.Fatalf("val=%v", val)
|
||||
}
|
||||
})
|
||||
// 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
|
||||
}
|
||||
|
||||
// TestParseMetricLineLabelEqualsBoundary pins the contract of the '=' guard in
|
||||
// the per-label loop (manager.go:430 -> `if eq < 0`).
|
||||
//
|
||||
// - eq < 0 (no '=' in the segment): the segment is skipped, no label added.
|
||||
// - eq == 0 (segment begins with '='): the key is empty but the pair is STILL
|
||||
// parsed, producing labels[""] = value. The boundary is `< 0`, not `<= 0`.
|
||||
func TestParseMetricLineLabelEqualsBoundary(t *testing.T) {
|
||||
t.Run("label segment without '=' is skipped, not fatal", func(t *testing.T) {
|
||||
// "novalue" has no '=' (eq == -1) and must be skipped. A real key=val
|
||||
// segment in the same line must still be parsed. Mutating `< 0` to `> 0`
|
||||
// would take kv[:eq] with eq=-1 and panic; mutating away the skip would
|
||||
// also corrupt parsing.
|
||||
name, labels, val, err := parseMetricLine(`mtg_traffic{novalue,direction="down"} 9`)
|
||||
if err != nil {
|
||||
t.Fatalf("line with a value-less label should still parse: %v", err)
|
||||
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
|
||||
}
|
||||
if name != "mtg_traffic" {
|
||||
t.Fatalf("name=%q", name)
|
||||
}
|
||||
if _, present := labels["novalue"]; present {
|
||||
t.Fatalf("value-less segment must not create a label: %v", labels)
|
||||
}
|
||||
if labels["direction"] != "down" {
|
||||
t.Fatalf("real label must still be parsed: %v", labels)
|
||||
}
|
||||
if val != 9 {
|
||||
t.Fatalf("val=%v", val)
|
||||
}
|
||||
})
|
||||
_, _ = 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()
|
||||
|
||||
t.Run("label segment beginning with '=' is parsed as empty key", func(t *testing.T) {
|
||||
// "=onlyvalue": eq == 0. Since the guard is `< 0`, this is NOT skipped:
|
||||
// it yields labels[""] = "onlyvalue". A mutant changing `< 0` to `<= 0`
|
||||
// would skip it, losing the empty-key entry.
|
||||
_, labels, _, err := parseMetricLine(`mtg_traffic{=onlyvalue} 1`)
|
||||
if err != nil {
|
||||
t.Fatalf("segment with empty key should still parse: %v", err)
|
||||
}
|
||||
v, present := labels[""]
|
||||
if !present {
|
||||
t.Fatalf("eq==0 segment must produce an empty-key label: %v", labels)
|
||||
}
|
||||
if v != "onlyvalue" {
|
||||
t.Fatalf("empty-key label value=%q", v)
|
||||
}
|
||||
})
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user