mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
ac3fc12077
* fix(ports): refuse an inbound on a port an AmneziaWG peer forwards checkForwardedPortsConflict only ever ran from the AmneziaWG save path, and only in one direction: an AmneziaWG client's forwardedPorts were checked against the ports other inbounds already hold, while the reverse -- an ordinary inbound saved onto a port some peer forwards -- had no guard at all. The forward listener binds that port on every interface in both directions (amneziawgnet/portfwd.go's attachTCP/attachUDP), so the two listeners want the same socket: the loser either leaves the peer's forward silently dead or fails the inbound's listen. checkPortConflictTx now resolves that owner the same way the relay-slot checks do -- same host, peers derived from the stored settings with the shared InstanceFromInbound -- and names the peer in the refusal. Sitting inside checkPortConflictTx covers both the save and the enable path added in #6549. TestAddInboundRefusesAPortAnAmneziaWGPeerForwards fails without this -- watched red, the create is allowed -- and its node-row companion pins the scoping that keeps a node row legal on a locally forwarded port. * fix(ports): name only a peer that binds as the owner of a forwarded port The owner lookup read instance.Peers and ForwardedPortsInclude directly, so a peer the forward supervisor skips (no email, or no address the tunnel routes to) was reported as holding a port nothing binds -- refusing a create that is legal with a message naming a row whose own port is its WireGuard one. It also repeated the candidate's listen address as the forward's location, though the forward binds :port on every interface. Share the supervisor's own gate through amneziawgnet.ForwardedPortOwner, report the wildcard bind, and propagate a failed owner query instead of reading it as "no conflict", matching the sibling checks in the same file. * style(ports): keep the forwarded-key doc block within the 2-line cap The reworded desiredPortForwardKeys doc ran to three lines, against the rule this repo sets for committed Go comments.
136 lines
4.8 KiB
Go
136 lines
4.8 KiB
Go
package service
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
// checkForwardedPortsConflict only ran from the AmneziaWG save path, so an
|
|
// ordinary inbound could take a port a peer forwards on every interface.
|
|
func TestAddInboundRefusesAPortAnAmneziaWGPeerForwards(t *testing.T) {
|
|
const forwarded = 8443
|
|
cases := []struct {
|
|
name string
|
|
port int
|
|
wantErr bool
|
|
}{
|
|
{"the forwarded port", forwarded, true},
|
|
{"a free port", forwarded + 1, false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
setupConflictDB(t)
|
|
seedInboundConflict(t, "awg-forward", "0.0.0.0", 51820, model.AmneziaWG, ``,
|
|
awgRelayWindowSettingsWithForward(t, "awg-forward", "8443"))
|
|
|
|
_, _, err := (&InboundService{}).AddInbound(&model.Inbound{
|
|
Tag: "user-inbound", Enable: true, Listen: "0.0.0.0", Port: tc.port,
|
|
Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`, Settings: `{"clients":[]}`,
|
|
})
|
|
if !tc.wantErr {
|
|
if err != nil {
|
|
t.Fatalf("port %d is free; the create must be allowed: %v", tc.port, err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
t.Fatalf("port %d is forwarded by a peer of another inbound; the create must be refused", tc.port)
|
|
}
|
|
if !strings.Contains(err.Error(), "awg-forward@relay-window") {
|
|
t.Fatalf("the refusal must name the peer holding the port, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A peer the forward supervisor opens no listener for holds no port: it has no
|
|
// email, or no address the tunnel can route to, and Reconcile skips it either way.
|
|
func TestAddInboundAllowsAPortNoPeerCanActuallyForward(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
settings func(t *testing.T) string
|
|
}{
|
|
{
|
|
name: "a peer with no email",
|
|
settings: func(t *testing.T) string {
|
|
t.Helper()
|
|
return replaceFirst(t, awgRelayWindowSettingsWithForward(t, "awg-forward", "8443"),
|
|
`"email":"awg-forward@relay-window"`, `"email":""`)
|
|
},
|
|
},
|
|
{
|
|
name: "an IPv6-only peer on a row without IPv6",
|
|
settings: func(t *testing.T) string {
|
|
t.Helper()
|
|
return replaceFirst(t, awgRelayWindowSettingsWithForward(t, "awg-forward", "8443"),
|
|
`"allowedIPs":["10.8.1.2/32"]`, `"allowedIPs":["fd00::2/128"]`)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
setupConflictDB(t)
|
|
seedInboundConflict(t, "awg-forward", "0.0.0.0", 51820, model.AmneziaWG, ``, tc.settings(t))
|
|
|
|
if _, _, err := (&InboundService{}).AddInbound(&model.Inbound{
|
|
Tag: "user-inbound", Enable: true, Listen: "0.0.0.0", Port: 8443,
|
|
Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`, Settings: `{"clients":[]}`,
|
|
}); err != nil {
|
|
t.Fatalf("nothing binds 8443 for this peer; the create must be allowed: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The refusal has to point at where the socket really is: the forward listens on
|
|
// every interface, so repeating the candidate's requested address asserts a lie.
|
|
func TestForwardedPortRefusalNamesTheWildcardBind(t *testing.T) {
|
|
setupConflictDB(t)
|
|
seedInboundConflict(t, "awg-forward", "0.0.0.0", 51820, model.AmneziaWG, ``,
|
|
awgRelayWindowSettingsWithForward(t, "awg-forward", "8443"))
|
|
|
|
_, _, err := (&InboundService{}).AddInbound(&model.Inbound{
|
|
Tag: "user-inbound", Enable: true, Listen: "10.0.0.5", Port: 8443,
|
|
Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`, Settings: `{"clients":[]}`,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("the port is forwarded on every interface, including 10.0.0.5; the create must be refused")
|
|
}
|
|
if !strings.Contains(err.Error(), " on * by its client ") {
|
|
t.Fatalf("the refusal must place the forward on every interface, got %v", err)
|
|
}
|
|
}
|
|
|
|
func replaceFirst(t *testing.T, s, old, new string) string {
|
|
t.Helper()
|
|
if !strings.Contains(s, old) {
|
|
t.Fatalf("fixture no longer contains %s", old)
|
|
}
|
|
return strings.Replace(s, old, new, 1)
|
|
}
|
|
|
|
// The forward listener runs where the AmneziaWG row runs, so a node row sharing
|
|
// a local peer's port stays legal -- the scoping every other guard here uses.
|
|
func TestAddInboundAllowsANodeRowOnALocallyForwardedPort(t *testing.T) {
|
|
setupConflictDB(t)
|
|
seedInboundConflict(t, "awg-forward", "0.0.0.0", 51820, model.AmneziaWG, ``,
|
|
awgRelayWindowSettingsWithForward(t, "awg-forward", "8443"))
|
|
|
|
node := &model.Node{Name: "n1", Address: "127.0.0.1", Port: 2096, Scheme: "https", Enable: true, Status: "online"}
|
|
if err := database.GetDB().Create(node).Error; err != nil {
|
|
t.Fatalf("seed node: %v", err)
|
|
}
|
|
if _, _, err := (&InboundService{}).AddInbound(&model.Inbound{
|
|
Tag: "node-inbound", Enable: true, Listen: "0.0.0.0", Port: 8443,
|
|
Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`, Settings: `{"clients":[]}`,
|
|
NodeID: &node.Id,
|
|
}); err != nil {
|
|
t.Fatalf("a node row does not bind here; the create must be allowed: %v", err)
|
|
}
|
|
}
|