fix(inbound): always create in AddInbound instead of overwriting a row whose id was posted

The add controller binds the inbound model's id form field and never clears
it, and AddInbound persisted with GORM Save, which updates in place when the
primary key is non-zero. A client that reused an existing id (for instance by
duplicating an inbound fetched from /get and changing the port) silently
overwrote that stored row instead of creating a new inbound. Zero the id at
the top of AddInbound, matching how it already zeroes the client-stat ids.
This commit is contained in:
MHSanaei
2026-07-14 23:42:44 +02:00
parent e4ef8a54d4
commit 091dbc0c6e
2 changed files with 37 additions and 0 deletions
+1
View File
@@ -719,6 +719,7 @@ func (s *InboundService) normalizeMtprotoXrayPort(inbound *model.Inbound, oldSet
// then saves the inbound to the database and optionally adds it to the running Xray instance.
// Returns the created inbound, whether Xray needs restart, and any error.
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
inbound.Id = 0
// Normalize streamSettings based on protocol
s.normalizeStreamSettings(inbound)
if err := validateFinalMaskRealityCombo(inbound.StreamSettings); err != nil {
@@ -91,6 +91,42 @@ func TestAddInbound_RejectsFinalMaskRealityCombo(t *testing.T) {
}
}
// AddInbound must always create a new row. The add controller binds the model's
// `id` form field and never clears it, so a client that reuses an existing id
// (e.g. duplicating an inbound fetched from /get) must not silently overwrite
// that stored row via GORM Save's upsert-on-primary-key behavior.
func TestAddInbound_IgnoresBoundIdAndCreatesNewRow(t *testing.T) {
setupConflictDB(t)
svc := &InboundService{}
first := &model.Inbound{Tag: "in-45100-tcp", Enable: true, Listen: "0.0.0.0", Port: 45100, Protocol: model.VLESS, Settings: `{"clients":[]}`}
created, _, err := svc.AddInbound(first)
if err != nil {
t.Fatalf("AddInbound first: %v", err)
}
second := &model.Inbound{Id: created.Id, Tag: "in-45101-tcp", Enable: true, Listen: "0.0.0.0", Port: 45101, Protocol: model.VLESS, Settings: `{"clients":[]}`}
if _, _, err := svc.AddInbound(second); err != nil {
t.Fatalf("AddInbound second: %v", err)
}
var count int64
if err := database.GetDB().Model(&model.Inbound{}).Count(&count).Error; err != nil {
t.Fatalf("count: %v", err)
}
if count != 2 {
t.Fatalf("expected 2 inbound rows, got %d: a bound id overwrote the first row instead of creating a new one", count)
}
var reloaded model.Inbound
if err := database.GetDB().First(&reloaded, created.Id).Error; err != nil {
t.Fatalf("reload first: %v", err)
}
if reloaded.Port != 45100 {
t.Fatalf("first inbound port = %d, want 45100 (the second add overwrote it)", reloaded.Port)
}
}
// end-to-end: same guard on the update path, on a row that was valid before
// the edit — the rejected StreamSettings must not overwrite the stored row.
func TestUpdateInbound_RejectsFinalMaskRealityCombo(t *testing.T) {