mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-12 17:05:01 +03:00
fix(finalmask): refuse a salamander password xray-core cannot open
xray-core needs a salamander (and gecko) password of at least 4 bytes (smPSKMinLen), but checks it only when a socket opens: xray run -test reports Configuration OK. On an inbound the listener then fails and Xray does not start at all, taking every other inbound down; on a client the dial fails. The panel saved any password, and the finalmask editor starts a new salamander mask with an empty one. maskcompat.CheckSalamanderPasswords applies the core's byte rule, pinned against the core's own ListenPacket, and every path that stores a finalmask refuses a short password: inbound add and update, hosts, the subscription's global finalmask, and the inbounds and outbounds of the Xray template. The editor flags it inline as it is typed.
This commit is contained in:
@@ -229,6 +229,9 @@ func (s *HostService) AddHostGroup(req *entity.HostGroup) ([]*model.Host, error)
|
||||
if groupId == "" {
|
||||
groupId = random.NumLower(16)
|
||||
}
|
||||
if err := validateFinalMaskJSONSalamander(req.FinalMask); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
created := buildHostRows(groupId, req)
|
||||
|
||||
err := database.GetDB().Transaction(func(tx *gorm.DB) error {
|
||||
@@ -247,6 +250,9 @@ func (s *HostService) AddHostGroup(req *entity.HostGroup) ([]*model.Host, error)
|
||||
}
|
||||
|
||||
func (s *HostService) UpdateHostGroup(groupId string, req *entity.HostGroup) ([]*model.Host, error) {
|
||||
if err := validateFinalMaskJSONSalamander(req.FinalMask); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
created := buildHostRows(groupId, req)
|
||||
|
||||
err := database.GetDB().Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
@@ -995,6 +995,25 @@ func validateFinalMaskXmcProfiles(streamSettings string) error {
|
||||
return common.NewError("XMC finalmask requires at least one complete Minecraft profile — each needs a username (3-16 of A-Z a-z 0-9 _), a UUID, and both texture fields from Mojang's session server (XTLS/Xray-core#6487). Complete the profiles or remove the XMC mask.")
|
||||
}
|
||||
|
||||
// validateFinalMaskSalamander refuses at save a salamander password the core loads and then refuses
|
||||
// when the listener opens, which stops every other inbound with it.
|
||||
func validateFinalMaskSalamander(streamSettings string) error {
|
||||
var stream map[string]any
|
||||
if json.Unmarshal([]byte(streamSettings), &stream) != nil {
|
||||
return nil
|
||||
}
|
||||
return maskcompat.CheckSalamanderPasswords(stream["finalmask"])
|
||||
}
|
||||
|
||||
// validateFinalMaskJSONSalamander checks a stored whole finalmask: a host's, or the subscription's.
|
||||
func validateFinalMaskJSONSalamander(finalmask string) error {
|
||||
var fm map[string]any
|
||||
if json.Unmarshal([]byte(finalmask), &fm) != nil {
|
||||
return nil
|
||||
}
|
||||
return maskcompat.CheckSalamanderPasswords(fm)
|
||||
}
|
||||
|
||||
// normalizeMtprotoSecret rebuilds every mtproto client's FakeTLS secret so it is
|
||||
// always valid before the row is persisted, and drops the vestigial inbound-level
|
||||
// secret and adTag: MTProto is multi-client, so mtg and every share link read
|
||||
@@ -1156,6 +1175,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||
if err := validateFinalMaskXmcProfiles(inbound.StreamSettings); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
if err := validateFinalMaskSalamander(inbound.StreamSettings); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
s.normalizeMtprotoSecret(inbound)
|
||||
if err := s.normalizeMtprotoXrayPort(inbound, ""); err != nil {
|
||||
return inbound, false, err
|
||||
@@ -1798,6 +1820,9 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
if err := validateFinalMaskXmcProfiles(inbound.StreamSettings); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
if err := validateFinalMaskSalamander(inbound.StreamSettings); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
s.normalizeMtprotoSecret(inbound)
|
||||
|
||||
oldInbound, err := s.GetInbound(inbound.Id)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
|
||||
)
|
||||
|
||||
func hysteriaSalamanderStream(password string) string {
|
||||
return `{"network":"hysteria","security":"tls","hysteriaSettings":{"version":2},
|
||||
"tlsSettings":{"alpn":["h3"],"certificates":[{"certificateFile":"/etc/ssl/h.crt","keyFile":"/etc/ssl/h.key"}]},
|
||||
"finalmask":{"udp":[{"type":"salamander","settings":{"password":"` + password + `"}}]}}`
|
||||
}
|
||||
|
||||
func hysteriaInbound(port int, stream string) *model.Inbound {
|
||||
return &model.Inbound{
|
||||
Enable: true, Listen: "127.0.0.1", Port: port, Protocol: model.Hysteria,
|
||||
Settings: `{"version":2,"clients":[{"email":"sal@example.test","auth":"a","enable":true}]}`,
|
||||
StreamSettings: stream,
|
||||
}
|
||||
}
|
||||
|
||||
// xray-core loads a salamander mask whose password is under 4 bytes and refuses it only when the
|
||||
// socket opens: on an inbound that stops every listener, so each save path must refuse it.
|
||||
func TestSavePathsRefuseShortSalamanderPassword(t *testing.T) {
|
||||
const want = "salamander finalmask password must be at least 4 bytes"
|
||||
tests := []struct {
|
||||
name string
|
||||
save func(t *testing.T) error
|
||||
}{
|
||||
{"add inbound", func(t *testing.T) error {
|
||||
_, _, err := (&InboundService{}).AddInbound(hysteriaInbound(18443, hysteriaSalamanderStream("")))
|
||||
return err
|
||||
}},
|
||||
{"update inbound", func(t *testing.T) error {
|
||||
in, _, err := (&InboundService{}).AddInbound(hysteriaInbound(18444, hysteriaSalamanderStream("long-enough")))
|
||||
if err != nil {
|
||||
t.Fatalf("AddInbound: %v", err)
|
||||
}
|
||||
in.StreamSettings = hysteriaSalamanderStream("abc")
|
||||
_, _, err = (&InboundService{}).UpdateInbound(in)
|
||||
return err
|
||||
}},
|
||||
{"add host", func(t *testing.T) error {
|
||||
ib := mkInbound(t, 18445, model.VLESS, `{"clients":[]}`)
|
||||
_, err := (&HostService{}).AddHostGroup(&entity.HostGroup{
|
||||
InboundIds: []int{ib.Id}, Remark: "h", Port: 443,
|
||||
FinalMask: `{"udp":[{"type":"salamander","settings":{"password":"€"}}]}`,
|
||||
})
|
||||
return err
|
||||
}},
|
||||
{"subscription JSON finalmask", func(t *testing.T) error {
|
||||
s := &SettingService{}
|
||||
all, err := s.GetAllSetting()
|
||||
if err != nil {
|
||||
t.Fatalf("GetAllSetting: %v", err)
|
||||
}
|
||||
all.SubJsonFinalMask = `{"udp":[{"type":"salamander","settings":{"password":"ab"}}]}`
|
||||
return s.UpdateAllSetting(all, SecretClears{})
|
||||
}},
|
||||
{"xray template inbound", func(t *testing.T) error {
|
||||
return (&XraySettingService{}).SaveXraySetting(`{"inbounds":[{"tag":"hy","port":18446,"protocol":"hysteria",
|
||||
"settings":{"version":2,"clients":[]},"streamSettings":` + hysteriaSalamanderStream("") + `}],
|
||||
"outbounds":[{"protocol":"freedom","tag":"direct"}]}`)
|
||||
}},
|
||||
{"xray template outbound", func(t *testing.T) error {
|
||||
return (&XraySettingService{}).SaveXraySetting(`{"outbounds":[{"protocol":"freedom","tag":"direct"},
|
||||
{"protocol":"hysteria","tag":"hy-out","settings":{"version":2,"address":"hy.example.com","port":443},
|
||||
"streamSettings":{"network":"hysteria","security":"tls","hysteriaSettings":{"version":2,"auth":"a"},
|
||||
"finalmask":{"udp":[{"type":"salamander","settings":{"password":"abc"}}]}}}]}`)
|
||||
}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
if err := tc.save(t); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("save err = %v, want one containing %q", err, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1665,6 +1665,9 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting, clears
|
||||
if err := validateSubJsonDnsSetting(allSetting); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFinalMaskJSONSalamander(allSetting.SubJsonFinalMask); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := allSetting.CheckValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -53,6 +53,11 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
|
||||
if err != nil {
|
||||
return common.NewError("xray template config invalid:", err)
|
||||
}
|
||||
for _, inbound := range xrayConfig.InboundConfigs {
|
||||
if err := validateFinalMaskSalamander(string(inbound.StreamSettings)); err != nil {
|
||||
return common.NewError("xray template inbound \""+inbound.Tag+"\":", err)
|
||||
}
|
||||
}
|
||||
if len(xrayConfig.OutboundConfigs) > 0 {
|
||||
var outbounds []json.RawMessage
|
||||
if err := json.Unmarshal(xrayConfig.OutboundConfigs, &outbounds); err != nil {
|
||||
@@ -77,6 +82,14 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
|
||||
}
|
||||
continue
|
||||
}
|
||||
var probe struct {
|
||||
Tag string `json:"tag"`
|
||||
StreamSettings json.RawMessage `json:"streamSettings"`
|
||||
}
|
||||
_ = json.Unmarshal(outbound, &probe)
|
||||
if err := validateFinalMaskSalamander(string(probe.StreamSettings)); err != nil {
|
||||
return common.NewError("xray template outbound \""+probe.Tag+"\":", err)
|
||||
}
|
||||
if err := xray.ValidateOutboundConfig(outbound); err != nil {
|
||||
if shouldSkipLegacyUnencryptedOutboundRejection(coreVersion, err) {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user