mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-29 06:31:58 +00:00
fix(clients): refuse a ":" email on a MASQUE inbound
MASQUE logs a user in with the email as the Basic-auth user name, so xray-core refuses the whole inbound (MASQUE: invalid "email") over one email holding ":", and with it the Xray start. The panel accepts such an email for every other protocol, so attaching or creating that client on a MASQUE inbound took every inbound down on the next restart. syncInboundClients is the write every create, attach, bulk, import and inbound-edit path shares inside its transaction, before the runtime push, so the per-protocol check there now refuses the email for MASQUE next to the existing TUIC identity check, loading the protocol once.
This commit is contained in:
1 parent
27e45f3039
commit
c4b9b3f9c7
2 files changed
+65
-4
No files matched your search
@@ -105,7 +105,7 @@ func (s *ClientService) syncInboundClients(tx *gorm.DB, inboundId int, clients [
|
|||||||
tx = database.GetDB()
|
tx = database.GetDB()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.validateTuicIdentities(tx, inboundId, clients, detachEmails, prune); err != nil {
|
if err := s.validateProtocolClients(tx, inboundId, clients, detachEmails, prune); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,14 +416,34 @@ func (s *ClientService) ListForInboundBySubId(tx *gorm.DB, inboundId int, subId
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ClientService) validateTuicIdentities(tx *gorm.DB, inboundID int, changed []model.Client, detached []string, prune bool) error {
|
// validateProtocolClients runs inside the write every path linking clients to an inbound
|
||||||
|
// shares, so a client the protocol's core config would refuse never reaches it.
|
||||||
|
func (s *ClientService) validateProtocolClients(tx *gorm.DB, inboundID int, changed []model.Client, detached []string, prune bool) error {
|
||||||
var inbound model.Inbound
|
var inbound model.Inbound
|
||||||
if err := tx.Select("protocol").Where("id = ?", inboundID).Take(&inbound).Error; err != nil {
|
if err := tx.Select("protocol").Where("id = ?", inboundID).Take(&inbound).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if inbound.Protocol != model.TUIC {
|
switch inbound.Protocol {
|
||||||
return nil
|
case model.TUIC:
|
||||||
|
return s.validateTuicIdentities(tx, inboundID, changed, detached, prune)
|
||||||
|
case model.MASQUE:
|
||||||
|
return validateMasqueEmails(changed)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MASQUE sends the email as the Basic-auth user, so xray-core refuses the whole inbound,
|
||||||
|
// and with it the Xray start, over one email holding ":".
|
||||||
|
func validateMasqueEmails(clients []model.Client) error {
|
||||||
|
for _, client := range clients {
|
||||||
|
if strings.Contains(client.Email, ":") {
|
||||||
|
return fmt.Errorf("MASQUE: client email %q cannot contain \":\"", client.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) validateTuicIdentities(tx *gorm.DB, inboundID int, changed []model.Client, detached []string, prune bool) error {
|
||||||
candidates := append([]model.Client(nil), changed...)
|
candidates := append([]model.Client(nil), changed...)
|
||||||
if !prune {
|
if !prune {
|
||||||
current, err := s.ListForInbound(tx, inboundID)
|
current, err := s.ListForInbound(tx, inboundID)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||||
@@ -57,6 +58,46 @@ func TestGetXrayConfig_EmitsMasqueUserPasswords(t *testing.T) {
|
|||||||
t.Fatal("inbound in-8443-masque not found in the generated config")
|
t.Fatal("inbound in-8443-masque not found in the generated config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MASQUE logs users in over Basic auth, so xray-core refuses the whole inbound over one
|
||||||
|
// email holding ":"; the panel allows that email elsewhere and must refuse it here.
|
||||||
|
func TestCreateClient_RefusesColonEmailOnMasque(t *testing.T) {
|
||||||
|
setupConflictDB(t)
|
||||||
|
in := &model.Inbound{
|
||||||
|
Tag: "in-8443-masque", Enable: true, Listen: "127.0.0.1", Port: 8443, Protocol: model.MASQUE,
|
||||||
|
Settings: `{"clients":[{"email":"ivy@example.test","password":"masque-pass-ivy","enable":true}],"address":["10.14.0.1/24"]}`,
|
||||||
|
StreamSettings: masqueTestStream,
|
||||||
|
}
|
||||||
|
if _, _, err := (&InboundService{}).AddInbound(in); err != nil {
|
||||||
|
t.Fatalf("AddInbound: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := (&ClientService{}).Create(&InboundService{}, &ClientCreatePayload{
|
||||||
|
Client: model.Client{Email: "tg:42", Enable: true},
|
||||||
|
InboundIds: []int{in.Id},
|
||||||
|
})
|
||||||
|
if want := `MASQUE: client email "tg:42" cannot contain ":"`; err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
t.Fatalf("Create err = %v, want one containing %q", err, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := (&XrayService{}).GetXrayConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetXrayConfig: %v", err)
|
||||||
|
}
|
||||||
|
for i := range cfg.InboundConfigs {
|
||||||
|
if cfg.InboundConfigs[i].Tag != in.Tag {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
raw, _ := json.Marshal(cfg.InboundConfigs[i])
|
||||||
|
var emitted map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &emitted); err != nil {
|
||||||
|
t.Fatalf("decode emitted inbound: %v", err)
|
||||||
|
}
|
||||||
|
assertXrayAccepts(t, "the emitted MASQUE inbound", buildGoldenInbound(t, emitted))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("inbound %s not found in the generated config", in.Tag)
|
||||||
|
}
|
||||||
|
|
||||||
// A client added to a MASQUE inbound without a password could never authenticate,
|
// A client added to a MASQUE inbound without a password could never authenticate,
|
||||||
// and xray-core refuses the whole inbound over one empty pass.
|
// and xray-core refuses the whole inbound over one empty pass.
|
||||||
func TestFillProtocolDefaults_MintsMasquePassword(t *testing.T) {
|
func TestFillProtocolDefaults_MintsMasquePassword(t *testing.T) {
|
||||||
|
|||||||
Reference in new issue
Block a user