mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
2a7342baa9
* feat: add inbound share address strategy Allow node-managed inbounds to choose whether exported share links use the node address, routable listen address, or a custom endpoint. Preserve locally configured share address fields during remote node traffic sync. Refs #5161 Refs #4891 * fix: preserve inbound share address settings Forward share address fields to remote nodes, keep existing values when older update payloads omit them, align localhost handling between frontend and subscriptions, and preserve share address settings when cloning inbounds. * fix: keep share address strategy out of subscriptions Limit the new share address strategy to direct exported share links and QR codes. Restore subscription address resolution to the existing panel-owned behavior and update the UI help text accordingly. * fix: address share address review feedback * fix: validate custom share address * fix --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
172 lines
4.7 KiB
Go
172 lines
4.7 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
|
|
)
|
|
|
|
// #4983: a synced inbound's OriginNodeGuid must point at the panel that
|
|
// physically hosts it. A node's own local inbound (empty origin in its
|
|
// snapshot) is attributed to the node's own GUID; an inbound the node forwards
|
|
// from its own sub-node (non-empty origin) keeps that deeper GUID across the
|
|
// hop — so a chained Node1->Node2->Node3 attributes Node3's inbounds to Node3.
|
|
func TestSetRemoteTraffic_AttributesOriginNodeGuid(t *testing.T) {
|
|
setupConflictDB(t)
|
|
db := database.GetDB()
|
|
|
|
const nodeID = 1
|
|
if err := db.Create(&model.Node{
|
|
Id: nodeID,
|
|
Name: "node2",
|
|
Address: "10.0.0.2",
|
|
Port: 2053,
|
|
ApiToken: "t",
|
|
Guid: "node2-guid",
|
|
}).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
|
|
snap := &runtime.TrafficSnapshot{
|
|
Inbounds: []*model.Inbound{
|
|
{ // node2's own local inbound — reports no origin
|
|
Tag: "in-443-tcp",
|
|
Enable: true,
|
|
Port: 443,
|
|
Protocol: model.VLESS,
|
|
Settings: `{"clients":[]}`,
|
|
},
|
|
{ // forwarded from node2's sub-node (node3) — carries node3's guid
|
|
Tag: "in-8443-tcp",
|
|
Enable: true,
|
|
Port: 8443,
|
|
Protocol: model.VLESS,
|
|
Settings: `{"clients":[]}`,
|
|
OriginNodeGuid: "node3-guid",
|
|
},
|
|
},
|
|
}
|
|
|
|
svc := InboundService{}
|
|
if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false); err != nil {
|
|
t.Fatalf("setRemoteTrafficLocked: %v", err)
|
|
}
|
|
|
|
origin := func(tag string) string {
|
|
var ib model.Inbound
|
|
if err := db.Where("tag = ?", tag).First(&ib).Error; err != nil {
|
|
t.Fatalf("load inbound %q: %v", tag, err)
|
|
}
|
|
return ib.OriginNodeGuid
|
|
}
|
|
|
|
if og := origin("in-443-tcp"); og != "node2-guid" {
|
|
t.Fatalf("local inbound origin = %q, want node2-guid (the node's own GUID)", og)
|
|
}
|
|
if og := origin("in-8443-tcp"); og != "node3-guid" {
|
|
t.Fatalf("forwarded inbound origin = %q, want node3-guid (kept across the hop)", og)
|
|
}
|
|
}
|
|
|
|
func TestSetRemoteTraffic_PreservesLocalShareAddressStrategy(t *testing.T) {
|
|
setupConflictDB(t)
|
|
db := database.GetDB()
|
|
|
|
const nodeID = 1
|
|
if err := db.Create(&model.Node{
|
|
Id: nodeID,
|
|
Name: "node2",
|
|
Address: "10.0.0.2",
|
|
Port: 2053,
|
|
ApiToken: "t",
|
|
Guid: "node2-guid",
|
|
}).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
|
|
nodeIDPtr := nodeID
|
|
if err := db.Create(&model.Inbound{
|
|
UserId: 1,
|
|
NodeID: &nodeIDPtr,
|
|
Tag: "remote-in",
|
|
Enable: true,
|
|
Port: 443,
|
|
Protocol: model.VLESS,
|
|
Settings: `{"clients":[]}`,
|
|
ShareAddrStrategy: "custom",
|
|
ShareAddr: "edge.example.com",
|
|
}).Error; err != nil {
|
|
t.Fatalf("create central inbound: %v", err)
|
|
}
|
|
|
|
snap := &runtime.TrafficSnapshot{
|
|
Inbounds: []*model.Inbound{{
|
|
Tag: "remote-in",
|
|
Enable: true,
|
|
Port: 8443,
|
|
Protocol: model.VLESS,
|
|
Settings: `{"clients":[]}`,
|
|
}},
|
|
}
|
|
|
|
svc := InboundService{}
|
|
if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false); err != nil {
|
|
t.Fatalf("setRemoteTrafficLocked: %v", err)
|
|
}
|
|
|
|
var ib model.Inbound
|
|
if err := db.Where("tag = ?", "remote-in").First(&ib).Error; err != nil {
|
|
t.Fatalf("load inbound: %v", err)
|
|
}
|
|
if ib.ShareAddrStrategy != "custom" || ib.ShareAddr != "edge.example.com" {
|
|
t.Fatalf("share address fields were overwritten: strategy=%q addr=%q", ib.ShareAddrStrategy, ib.ShareAddr)
|
|
}
|
|
if ib.Port != 8443 {
|
|
t.Fatalf("sync should still update regular remote fields; port = %d, want 8443", ib.Port)
|
|
}
|
|
}
|
|
|
|
func TestSetRemoteTraffic_DefaultsShareAddressFieldsForNewCentralInbound(t *testing.T) {
|
|
setupConflictDB(t)
|
|
db := database.GetDB()
|
|
|
|
const nodeID = 1
|
|
if err := db.Create(&model.Node{
|
|
Id: nodeID,
|
|
Name: "node2",
|
|
Address: "10.0.0.2",
|
|
Port: 2053,
|
|
ApiToken: "t",
|
|
Guid: "node2-guid",
|
|
}).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
|
|
snap := &runtime.TrafficSnapshot{
|
|
Inbounds: []*model.Inbound{{
|
|
Tag: "remote-in",
|
|
Enable: true,
|
|
Port: 8443,
|
|
Protocol: model.VLESS,
|
|
Settings: `{"clients":[]}`,
|
|
ShareAddrStrategy: "custom",
|
|
ShareAddr: "remote.example.com",
|
|
}},
|
|
}
|
|
|
|
svc := InboundService{}
|
|
if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false); err != nil {
|
|
t.Fatalf("setRemoteTrafficLocked: %v", err)
|
|
}
|
|
|
|
var ib model.Inbound
|
|
if err := db.Where("tag = ?", "remote-in").First(&ib).Error; err != nil {
|
|
t.Fatalf("load inbound: %v", err)
|
|
}
|
|
if ib.ShareAddrStrategy != "node" || ib.ShareAddr != "" {
|
|
t.Fatalf("new central inbound share fields = (%q, %q), want (node, empty)", ib.ShareAddrStrategy, ib.ShareAddr)
|
|
}
|
|
}
|