fix(amneziawg): keep port forwards per inbound and off future relay ports

Attach and BulkAttach copied a client record's forwardedPorts onto the
target inbound. A client a node reports keeps the forwards the node
chose, so attaching it to one of the master's AmneziaWG inbounds opened
listeners of the node's choosing on the master, among them the
loopback relay port a later AmneziaWG or TUIC inbound derives from its
id; that inbound's traffic then went to the forward and Xray failed to
bind.

A forward is set per inbound and is no longer carried by an attach, and
creating an AmneziaWG or TUIC inbound is refused while a peer forward
already holds its relay port.
This commit is contained in:
MHSanaei
2026-11-01 01:22:51 +01:00
parent 6dbd219a14
commit 30f17ef5a3
5 changed files with 134 additions and 2 deletions
@@ -157,3 +157,63 @@ func TestAttachRefusesDefaultRouteNextToHeldAddress(t *testing.T) {
}
}
}
// A forward is a listener on the inbound's host; carried over from a node-reported
// client, it let the node pick ports on the master, a future relay port among them.
func TestAttachDoesNotCarryForwardedPorts(t *testing.T) {
attaches := map[string]func(*ClientService, *InboundService, string, int) error{
"Attach": func(s *ClientService, inboundSvc *InboundService, email string, ibId int) error {
_, err := s.AttachByEmail(inboundSvc, email, []int{ibId})
return err
},
"BulkAttach": func(s *ClientService, inboundSvc *InboundService, email string, ibId int) error {
result, _, err := s.BulkAttach(inboundSvc, []string{email}, []int{ibId})
if err == nil && len(result.Errors) > 0 {
err = errors.New(strings.Join(result.Errors, "; "))
}
return err
},
}
for name, attach := range attaches {
t.Run(name, func(t *testing.T) {
setupConflictDB(t)
node := 1
seedInboundConflict(t, "own", "0.0.0.0", 51830, model.AmneziaWG, ``, awgRelayWindowSettings(t, "own"))
seedInboundConflictNode(t, "node", "0.0.0.0", 44301, model.AmneziaWG, ``, `{"clients":[`+
`{"email":"forwarder","subId":"s-fwd","enable":true,"publicKey":"QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI=","allowedIPs":["10.9.1.2/32"],"forwardedPorts":"8022"}]}`, &node)
s := &ClientService{}
inboundSvc := &InboundService{}
var own model.Inbound
for _, tag := range []string{"node", "own"} {
own = model.Inbound{}
if err := database.GetDB().Where("tag = ?", tag).First(&own).Error; err != nil {
t.Fatalf("read seeded %s row: %v", tag, err)
}
clients, err := inboundSvc.GetClients(&own)
if err != nil {
t.Fatalf("GetClients(%s): %v", tag, err)
}
if err := s.SyncInbound(nil, own.Id, clients); err != nil {
t.Fatalf("SyncInbound(%s): %v", tag, err)
}
}
if err := attach(s, inboundSvc, "forwarder", own.Id); err != nil {
t.Fatalf("attach: %v", err)
}
if err := database.GetDB().First(&own, own.Id).Error; err != nil {
t.Fatalf("reread own inbound: %v", err)
}
clients, err := inboundSvc.GetClients(&own)
if err != nil {
t.Fatalf("GetClients(own): %v", err)
}
for _, c := range clients {
if c.Email == "forwarder" && c.ForwardedPorts != "" {
t.Fatalf("attach carried forwardedPorts %q onto the master's inbound", c.ForwardedPorts)
}
}
})
}
}