fix(sub): resolve a blank subId to no subscription

A subId is the only selector of a subscription's inbounds and external
links, so a blank one matched every client stored without a subId, as
clients embedded through inbounds/add or inbounds/import are. The
Telegram bot authorised a client by email and then looked its links up
by that blank subId, handing a bound user the working links of every
other subId-less client.

A blank subId now resolves to nothing in the subscription service, and
the bot builds a subId-less client's own links by email, as the panel's
client page does.
This commit is contained in:
MHSanaei
2026-11-01 00:57:32 +01:00
parent 9a95153f0e
commit 1c748adadf
5 changed files with 70 additions and 1 deletions
@@ -232,6 +232,11 @@ func (t *Tgbot) clientSubLinks(email, subURL string) ([]string, error) {
if err != nil || client == nil {
return nil, errors.New("client not found")
}
// A client stored without a subId has no subscription to look up; build
// its own links by email, as the panel's client page does.
if strings.TrimSpace(client.SubID) == "" {
return t.inboundService.GetAllClientLinks(u.Hostname(), email)
}
return t.inboundService.GetSubLinks(u.Hostname(), client.SubID)
}
@@ -87,3 +87,38 @@ func TestIndividualLinksDoNotNeedAResolvableHost(t *testing.T) {
t.Fatalf("bot sent %q, want the client's vless link", got)
}
}
// A client stored without a subId has no subscription; the blank value must not
// be looked up as one, or it selects every other client stored without a subId.
func TestIndividualLinksOfBlankSubIdClientExposeNoOtherClient(t *testing.T) {
sent := recordingBotServer(t)
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
service.RegisterSubLinkProvider(sub.NewLinkProvider())
const ownerUUID = "11111111-2222-4333-8444-555555555555"
const otherUUID = "99999999-2222-4333-8444-555555555555"
inboundSvc := service.InboundService{}
_, _, err := inboundSvc.AddInbound(&model.Inbound{
UserId: 1, Tag: "in-443", Enable: true, Listen: "203.0.113.5", Port: 443,
Protocol: model.VLESS, Remark: "in", StreamSettings: `{"network":"tcp","security":"none"}`,
Settings: `{"clients":[{"id":"` + ownerUUID + `","email":"` + ownerMail + `","tgId":4242,"enable":true},` +
`{"id":"` + otherUUID + `","email":"other@x","enable":true}],"decryption":"none"}`,
})
if err != nil {
t.Fatalf("AddInbound: %v", err)
}
origRunning := isRunning
t.Cleanup(func() { isRunning = origRunning })
isRunning = true
tapClientLinks(t, &Tgbot{}, ownerTgID, "client_individual_links "+ownerMail)
got := strings.Join(sent(), "\n")
if !strings.Contains(got, "vless://"+ownerUUID+"@203.0.113.5:443") {
t.Errorf("bot sent %q, want the client's own vless link", got)
}
if strings.Contains(got, otherUUID) {
t.Errorf("bot sent another client's credentials to %s: %q", ownerMail, got)
}
}