mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-12 15:46:06 +00:00
fc625d8f66
Inbound.Port carried a unique gorm tag before multi-node existed; the tag was removed from the model long ago, but AutoMigrate never drops a constraint, so SQLite databases created in that era still physically enforce global port uniqueness. On such installs the node-scoped port conflict logic is correct yet the raw insert fails - manual "Deploy to node" saves and setRemoteTraffic's central-inbound adoption both die with "UNIQUE constraint failed: inbounds.port" when two nodes use the same port. Add a SQLite-only migration that drops an explicit unique index on port directly, and rebuilds the table (create from the current model, copy shared columns, swap) when the constraint is inline, since SQLite can't ALTER it away. Fresh databases and Postgres are untouched. Closes #5894
77 lines
2.8 KiB
Go
77 lines
2.8 KiB
Go
package database
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const legacyInboundsDDL = "CREATE TABLE `inbounds` (`id` integer PRIMARY KEY AUTOINCREMENT,`user_id` integer,`up` integer,`down` integer,`total` integer,`remark` text,`enable` numeric,`expiry_time` integer,`listen` text,`port` integer UNIQUE,`protocol` text,`settings` text,`stream_settings` text,`tag` text UNIQUE,`sniffing` text)"
|
|
|
|
func openLegacyPortUniqueDB(t *testing.T) string {
|
|
t.Helper()
|
|
dbPath := filepath.Join(t.TempDir(), "x-ui.db")
|
|
legacy, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open legacy db: %v", err)
|
|
}
|
|
if err := legacy.Exec(legacyInboundsDDL).Error; err != nil {
|
|
t.Fatalf("create legacy inbounds: %v", err)
|
|
}
|
|
if err := legacy.Exec(
|
|
`INSERT INTO inbounds (user_id, remark, enable, port, protocol, settings, stream_settings, tag, sniffing)
|
|
VALUES (1, 'preexisting', 1, 80, 'vless', '{"clients":[]}', '{}', 'in-80-tcp', '{}')`,
|
|
).Error; err != nil {
|
|
t.Fatalf("seed legacy inbound: %v", err)
|
|
}
|
|
sqlDB, err := legacy.DB()
|
|
if err != nil {
|
|
t.Fatalf("legacy db handle: %v", err)
|
|
}
|
|
if err := sqlDB.Close(); err != nil {
|
|
t.Fatalf("close legacy db: %v", err)
|
|
}
|
|
return dbPath
|
|
}
|
|
|
|
func TestDropLegacyInboundPortUnique(t *testing.T) {
|
|
dbPath := openLegacyPortUniqueDB(t)
|
|
|
|
if err := InitDB(dbPath); err != nil {
|
|
t.Fatalf("InitDB over legacy schema: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = CloseDB() })
|
|
|
|
var preexisting model.Inbound
|
|
if err := GetDB().Where("tag = ?", "in-80-tcp").First(&preexisting).Error; err != nil {
|
|
t.Fatalf("preexisting inbound lost in rebuild: %v", err)
|
|
}
|
|
if preexisting.Remark != "preexisting" || preexisting.Port != 80 {
|
|
t.Fatalf("preexisting inbound corrupted: remark=%q port=%d", preexisting.Remark, preexisting.Port)
|
|
}
|
|
|
|
nodeA, nodeB := 1, 2
|
|
samePortA := &model.Inbound{UserId: 1, Tag: "node-a-80", Enable: true, Port: 80, Protocol: model.VLESS, Remark: "a", Settings: `{"clients":[]}`, NodeID: &nodeA}
|
|
samePortB := &model.Inbound{UserId: 1, Tag: "node-b-80", Enable: true, Port: 80, Protocol: model.VLESS, Remark: "b", Settings: `{"clients":[]}`, NodeID: &nodeB}
|
|
if err := GetDB().Create(samePortA).Error; err != nil {
|
|
t.Fatalf("create node-a inbound on port 80: %v", err)
|
|
}
|
|
if err := GetDB().Create(samePortB).Error; err != nil {
|
|
t.Fatalf("create node-b inbound on port 80: %v", err)
|
|
}
|
|
|
|
var dupTag model.Inbound
|
|
dupErr := GetDB().Create(&model.Inbound{UserId: 1, Tag: "in-80-tcp", Enable: true, Port: 90, Protocol: model.VLESS, Settings: `{"clients":[]}`}).Error
|
|
if dupErr == nil {
|
|
t.Fatalf("duplicate tag insert succeeded, want unique violation; row=%v", dupTag)
|
|
}
|
|
|
|
if err := dropLegacyInboundPortUnique(); err != nil {
|
|
t.Fatalf("second dropLegacyInboundPortUnique run: %v", err)
|
|
}
|
|
}
|