feat(mtproto): per-client ad-tags, management-API auth, and record secret sync

Catch the panel up to the mtg-multi README (v1.14.0):

- Each client can now carry its own 32-hex advertising tag overriding the
  inbound-level one. The tag lives on the client (settings JSON is the
  source of truth, clients.ad_tag is the UI projection), is rendered into
  the fork's [secret-ad-tags] section for active secrets only (mtg rejects
  a config whose override names an unknown secret), is pushed per entry
  through PUT /secrets, and is part of the reload fingerprint so a tag
  edit hot-applies without dropping connections.
- The loopback management API can replace the whole secret set, so every
  mtg process now gets a random per-process api-token; the manager sends
  it as a bearer token on PUT /secrets and GET /stats and reuses it across
  config rewrites, because mtg reads the token only at startup.
- Malformed tags are rejected at every save path and additionally dropped
  in InstanceFromInbound: one bad tag would otherwise fail the whole
  generated config and take every client of the inbound down with it.
- SyncInbound never copied a re-keyed mtproto secret into the canonical
  clients table, so the clients page and subscription links kept serving
  the old secret, which mtg then rejects. It is now guarded-copied like
  the other credentials.
This commit is contained in:
MHSanaei
2026-07-07 12:00:43 +02:00
parent 659f0f404c
commit 43500a5470
33 changed files with 361 additions and 54 deletions
+16
View File
@@ -456,6 +456,18 @@ func mtprotoSecretMiddle(secret string) string {
return mtprotoRandomMiddle()
}
// ValidMtprotoAdTag reports whether a Telegram advertising tag from
// @MTProxybot is well-formed: exactly 16 bytes as 32 hex characters. mtg
// refuses to start (or rejects a live update) on a malformed tag, so every
// write path validates before the tag can reach a generated config.
func ValidMtprotoAdTag(tag string) bool {
if len(tag) != 32 {
return false
}
_, err := hex.DecodeString(tag)
return err == nil
}
// StripMtprotoInboundSecret removes the vestigial inbound-level `secret` from an
// mtproto inbound's settings JSON. MTProto is multi-client: every secret lives on
// a client, and mtg's [secrets] config plus every share link read only the
@@ -666,6 +678,7 @@ type Client struct {
PreSharedKey string `json:"preSharedKey,omitempty"`
KeepAlive int `json:"keepAlive,omitempty"`
Secret string `json:"secret,omitempty" example:"ee1234567890abcdef1234567890abcd7777772e636c6f7564666c6172652e636f6d"`
AdTag string `json:"adTag,omitempty" example:"0123456789abcdef0123456789abcdef"`
Email string `json:"email"` // Client email identifier
LimitIP int `json:"limitIp"` // IP limit for this client
TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
@@ -696,6 +709,7 @@ type ClientRecord struct {
PreSharedKey string `json:"preSharedKey" gorm:"column:wg_pre_shared_key"`
KeepAlive int `json:"keepAlive" gorm:"column:wg_keep_alive;default:0"`
Secret string `json:"secret" gorm:"column:secret"`
AdTag string `json:"adTag" gorm:"column:ad_tag;default:''"`
LimitIP int `json:"limitIp" gorm:"column:limit_ip"`
TotalGB int64 `json:"totalGB" gorm:"column:total_gb"`
ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time"`
@@ -880,6 +894,7 @@ func (c *Client) ToRecord() *ClientRecord {
PreSharedKey: c.PreSharedKey,
KeepAlive: c.KeepAlive,
Secret: c.Secret,
AdTag: c.AdTag,
}
if c.Reverse != nil {
if b, err := json.Marshal(c.Reverse); err == nil {
@@ -932,6 +947,7 @@ func (r *ClientRecord) ToClient() *Client {
PreSharedKey: r.PreSharedKey,
KeepAlive: r.KeepAlive,
Secret: r.Secret,
AdTag: r.AdTag,
}
if r.Reverse != "" {
var rev ClientReverse