inbound: check transport in port conflict, allow tcp and udp on same port (#4169)

the panel rejected configurations like vless reality on tcp/443 and
hysteria2 on udp/443 even though those are independent sockets in
linux. the old checkPortExist looked only at port + listen.

inboundTransports now classifies each inbound by L4 transport:
hysteria/hysteria2/wireguard are udp; streamSettings.network=kcp is
udp; shadowsocks reads settings.network ("tcp"/"udp"/"tcp,udp");
mixed (socks/http) adds udp when settings.udp is true; everything
else is tcp. checkPortConflict pulls every row on the same port and
only flags a conflict when transport masks overlap. the listen-
overlap rule (specific addr vs any-addr on the same port) is kept.

inbounds.tag has a unique DB constraint and the controller derives
tags from port ("inbound-443"). without disambiguation a second
inbound on the same port would still hit a unique-constraint error.
generateInboundTag keeps the historical "inbound-<port>" shape when
the base tag is free, so existing routing rules survive the upgrade
unchanged, and appends "-tcp"/"-udp" only when the base is already
taken.

closes #4103.
This commit is contained in:
pwnnex
2026-05-06 12:41:21 +03:00
committed by GitHub
parent 47163c1418
commit 6a483fa987
3 changed files with 614 additions and 36 deletions
+17 -36
View File
@@ -104,36 +104,6 @@ func (s *InboundService) GetInboundsByTrafficReset(period string) ([]*model.Inbo
return inbounds, nil
}
func (s *InboundService) checkPortExist(listen string, port int, ignoreId int) (bool, error) {
db := database.GetDB()
if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" {
db = db.Model(model.Inbound{}).Where("port = ?", port)
} else {
db = db.Model(model.Inbound{}).
Where("port = ?", port).
Where(
db.Model(model.Inbound{}).Where(
"listen = ?", listen,
).Or(
"listen = \"\"",
).Or(
"listen = \"0.0.0.0\"",
).Or(
"listen = \"::\"",
).Or(
"listen = \"::0\""))
}
if ignoreId > 0 {
db = db.Where("id != ?", ignoreId)
}
var count int64
err := db.Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, error) {
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
@@ -221,7 +191,7 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
// 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) {
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0)
exist, err := s.checkPortConflict(inbound, 0)
if err != nil {
return inbound, false, err
}
@@ -229,6 +199,16 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
return inbound, false, common.NewError("Port already exists:", inbound.Port)
}
// pick a tag that won't collide with an existing row. for the common
// case this is the same "inbound-<port>" string the controller already
// set; only when this port already has another inbound on a different
// transport (now possible after the transport-aware port check) does
// this disambiguate with a -tcp/-udp suffix. see #4103.
inbound.Tag, err = s.generateInboundTag(inbound, 0)
if err != nil {
return inbound, false, err
}
existEmail, err := s.checkEmailExistForInbound(inbound)
if err != nil {
return inbound, false, err
@@ -462,7 +442,7 @@ func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) {
// It validates changes, updates the database, and syncs with the running Xray instance.
// Returns the updated inbound, whether Xray needs restart, and any error.
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id)
exist, err := s.checkPortConflict(inbound, inbound.Id)
if err != nil {
return inbound, false, err
}
@@ -565,10 +545,11 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.Settings = inbound.Settings
oldInbound.StreamSettings = inbound.StreamSettings
oldInbound.Sniffing = inbound.Sniffing
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
} else {
oldInbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
// regenerate tag with collision-aware logic. for this row we pass
// inbound.Id as ignoreId so it doesn't see its own old tag in the db.
oldInbound.Tag, err = s.generateInboundTag(inbound, inbound.Id)
if err != nil {
return inbound, false, err
}
needRestart := false