mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-08 19:27:14 +00:00
d3da7abdf0
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.
131 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|