Files
3x-ui/internal/web/service/inbound_amneziawg_test.go
T
Kuzz007 d3da7abdf0 fix: port the PR #6105 review-round fixes into this fork's own AmneziaWG code
Same 8 findings fixed on upstream-pr/amneziawg, ported here since this
fork's internal/amneziawg + related web/service files predate that PR
branch's own fix-up commits:

1. hostRulesFingerprint now folds in a peer's IPv4 whenever
   ForwardedPorts is set, not only when RouteThroughXray is on, so a
   re-IP forces the bounce needed to move the DNAT rule too.
2. ValidateConfigValue (new, params.go) rejects control characters in
   server/client keys, email and I1 at save time; sanitizeConfigValue
   strips them defensively at .conf-render time.
3. checkForwardedPortsConflict now scopes to node_id IS NULL and takes
   a pre-loaded portConflictContext (loadPortConflictContext), so a
   port used only on another node isn't a false collision and an
   inbound with N clients costs one query instead of N.
4. PostDown commands are now best-effort (appendOrTrue) so an external
   firewall flush can't abort the rest of the teardown chain.
5. The "ip rule list | grep -q" existence check now uses
   grep -c >/dev/null, avoiding a pipefail/SIGPIPE false negative that
   could re-add a duplicate rule.
6. route_egress.go's stale "always present, no opt-in" comment
   corrected to describe the real RouteThroughXray-gated behavior.
   (This fork's genAmneziaWGLink already emits vpn://, and there's no
   upstream-facing docs page here, so neither needed the PR branch's
   Finding 6 docs/link-format changes.)
7. install.sh: Arch's ndppd install uses pacman -Sy, not -Syu, matching
   every other pacman call in the script; should_install_amneziawg
   short-circuits to yes when awg is already installed, so `x-ui
   update` doesn't re-prompt -- this fork's own opt-out-by-default
   philosophy for should_install_amneziawg is unchanged, only the
   redundant-reprompt behavior is fixed.
8. CollectTraffic checks pointer identity before writing back a
   traffic-counter baseline, so a concurrent restart's freshly-reset
   (empty) baseline can't be clobbered by stale pre-restart counters.
   sweepOrphansLocked no longer permanently disables itself on a
   transient os.ReadDir failure.

go build/vet/test and frontend typecheck/lint/build/vitest all pass.
2026-07-29 01:21:03 +03:00

131 lines
4.6 KiB
Go

package service
import (
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
func TestCheckForwardedPortsConflict_EmptySpecNoConflict(t *testing.T) {
setupConflictDB(t)
svc := &InboundService{}
ctx, err := svc.loadPortConflictContext()
if err != nil {
t.Fatalf("loadPortConflictContext: %v", err)
}
if hit := svc.checkForwardedPortsConflict(ctx, ""); hit != "" {
t.Fatalf("an empty spec must never conflict; got hit=%q", hit)
}
}
func TestCheckForwardedPortsConflict_CollidesWithPanelPort(t *testing.T) {
setupConflictDB(t)
svc := &InboundService{}
ctx, err := svc.loadPortConflictContext()
if err != nil {
t.Fatalf("loadPortConflictContext: %v", err)
}
// getString falls back to defaultValueMap's "webPort": "2053" on a fresh
// DB with no explicit setting row.
hit := svc.checkForwardedPortsConflict(ctx, "2053")
if !strings.Contains(hit, "panel") {
t.Fatalf("expected a collision naming the panel's own port, got %q", hit)
}
}
func TestCheckForwardedPortsConflict_CollidesWithEnabledInboundPort(t *testing.T) {
setupConflictDB(t)
seedInboundConflict(t, "vless-8080", "0.0.0.0", 8080, model.VLESS, `{"network":"tcp"}`, `{}`)
svc := &InboundService{}
ctx, err := svc.loadPortConflictContext()
if err != nil {
t.Fatalf("loadPortConflictContext: %v", err)
}
hit := svc.checkForwardedPortsConflict(ctx, "8000-8100")
if !strings.Contains(hit, "vless-8080") {
t.Fatalf("expected a collision naming the colliding inbound, got %q", hit)
}
}
func TestCheckForwardedPortsConflict_IgnoresDisabledInboundPort(t *testing.T) {
setupConflictDB(t)
disabled := &model.Inbound{Tag: "vless-8080-off", Enable: false, Listen: "0.0.0.0", Port: 8080, Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`}
if err := database.GetDB().Create(disabled).Error; err != nil {
t.Fatalf("seed disabled inbound: %v", err)
}
svc := &InboundService{}
ctx, err := svc.loadPortConflictContext()
if err != nil {
t.Fatalf("loadPortConflictContext: %v", err)
}
if hit := svc.checkForwardedPortsConflict(ctx, "8080"); hit != "" {
t.Fatalf("a disabled inbound's port must not be reserved; got hit=%q", hit)
}
}
func TestCheckForwardedPortsConflict_NoCollisionWhenPortsDontOverlap(t *testing.T) {
setupConflictDB(t)
seedInboundConflict(t, "vless-8080", "0.0.0.0", 8080, model.VLESS, `{"network":"tcp"}`, `{}`)
svc := &InboundService{}
ctx, err := svc.loadPortConflictContext()
if err != nil {
t.Fatalf("loadPortConflictContext: %v", err)
}
if hit := svc.checkForwardedPortsConflict(ctx, "9000-9100"); hit != "" {
t.Fatalf("unrelated ports must not conflict; got hit=%q", hit)
}
}
// A port-forward spec matching a port used only by an inbound hosted on a
// DIFFERENT node must not conflict: that inbound's DNAT/listen socket lives
// on the node's own host, never on this panel's, so there is nothing here
// for the forwarded port to actually collide with. Mirrors
// TestCheckPortConflict_NodeScope's own reasoning for the general port-
// conflict check.
func TestCheckForwardedPortsConflict_IgnoresPortOnDifferentNode(t *testing.T) {
setupConflictDB(t)
seedInboundConflictNode(t, "node1-8080", "0.0.0.0", 8080, model.VLESS, `{"network":"tcp"}`, `{}`, new(1))
svc := &InboundService{}
ctx, err := svc.loadPortConflictContext()
if err != nil {
t.Fatalf("loadPortConflictContext: %v", err)
}
if hit := svc.checkForwardedPortsConflict(ctx, "8080"); hit != "" {
t.Fatalf("a port used only on a different node must not conflict; got hit=%q", hit)
}
}
// inboundAmneziaWGServer is pure (no DB), so it needs neither setupConflictDB
// nor CGO/sqlite -- it can run in any Go environment.
func TestInboundAmneziaWGServer_RedactsPrivateKey(t *testing.T) {
settings := `{"server":{"privateKey":"super-secret","publicKey":"pub","mtu":1420},"clients":[]}`
got := inboundAmneziaWGServer(string(model.AmneziaWG), settings)
if got == nil {
t.Fatal("expected a non-nil server block")
}
if got.PrivateKey != "" {
t.Fatalf("PrivateKey must be redacted, got %q", got.PrivateKey)
}
if got.PublicKey != "pub" || got.MTU != 1420 {
t.Fatalf("non-secret fields must still come through unchanged, got %+v", got)
}
}
func TestInboundAmneziaWGServer_NonAmneziaWGReturnsNil(t *testing.T) {
if got := inboundAmneziaWGServer(string(model.VLESS), `{"server":{"privateKey":"x"}}`); got != nil {
t.Fatalf("a non-AmneziaWG protocol must return nil, got %+v", got)
}
}
func TestInboundAmneziaWGServer_MissingServerBlockReturnsNil(t *testing.T) {
if got := inboundAmneziaWGServer(string(model.AmneziaWG), `{"clients":[]}`); got != nil {
t.Fatalf("settings with no server block must return nil, got %+v", got)
}
}