mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-22 06:32:04 +03:00
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.
This commit is contained in:
@@ -368,6 +368,13 @@ func (s *ClientService) addInboundClient(inboundSvc *InboundService, data *model
|
||||
}
|
||||
}
|
||||
|
||||
var portCtx portConflictContext
|
||||
if oldInbound.Protocol == model.AmneziaWG {
|
||||
portCtx, err = inboundSvc.loadPortConflictContext()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
for _, client := range clients {
|
||||
if strings.TrimSpace(client.Email) == "" {
|
||||
return false, common.NewError("client email is required")
|
||||
@@ -402,9 +409,7 @@ func (s *ClientService) addInboundClient(inboundSvc *InboundService, data *model
|
||||
}
|
||||
}
|
||||
if oldInbound.Protocol == model.AmneziaWG {
|
||||
if hit, err := inboundSvc.checkForwardedPortsConflict(client.ForwardedPorts); err != nil {
|
||||
return false, err
|
||||
} else if hit != "" {
|
||||
if hit := inboundSvc.checkForwardedPortsConflict(portCtx, client.ForwardedPorts); hit != "" {
|
||||
return false, common.NewError("amneziawg: forwardedPorts collides with", hit)
|
||||
}
|
||||
}
|
||||
@@ -660,9 +665,11 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
||||
}
|
||||
}
|
||||
if oldInbound.Protocol == model.AmneziaWG {
|
||||
if hit, err := inboundSvc.checkForwardedPortsConflict(clients[0].ForwardedPorts); err != nil {
|
||||
portCtx, err := inboundSvc.loadPortConflictContext()
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if hit != "" {
|
||||
}
|
||||
if hit := inboundSvc.checkForwardedPortsConflict(portCtx, clients[0].ForwardedPorts); hit != "" {
|
||||
return false, common.NewError("amneziawg: forwardedPorts collides with", hit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,13 +200,33 @@ func (s *InboundService) normalizeAmneziaWGSettings(inbound *model.Inbound) erro
|
||||
if err := amneziawg.ValidateInterfaceName(parsed.Server.IPv6ExternalInterface); err != nil {
|
||||
return fmt.Errorf("amneziawg: ipv6ExternalInterface: %w", err)
|
||||
}
|
||||
if err := amneziawg.ValidateConfigValue("privateKey", parsed.Server.PrivateKey); err != nil {
|
||||
return fmt.Errorf("amneziawg: %w", err)
|
||||
}
|
||||
if err := amneziawg.ValidateConfigValue("publicKey", parsed.Server.PublicKey); err != nil {
|
||||
return fmt.Errorf("amneziawg: %w", err)
|
||||
}
|
||||
if err := amneziawg.ValidateConfigValue("i1", parsed.Server.I1); err != nil {
|
||||
return fmt.Errorf("amneziawg: %w", err)
|
||||
}
|
||||
|
||||
portCtx, err := s.loadPortConflictContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range parsed.Clients {
|
||||
if hit, err := s.checkForwardedPortsConflict(c.ForwardedPorts); err != nil {
|
||||
return err
|
||||
} else if hit != "" {
|
||||
if hit := s.checkForwardedPortsConflict(portCtx, c.ForwardedPorts); hit != "" {
|
||||
return fmt.Errorf("amneziawg: client %q forwardedPorts collides with %s", c.Email, hit)
|
||||
}
|
||||
if err := amneziawg.ValidateConfigValue("email", c.Email); err != nil {
|
||||
return fmt.Errorf("amneziawg: %w", err)
|
||||
}
|
||||
if err := amneziawg.ValidateConfigValue("publicKey", c.PublicKey); err != nil {
|
||||
return fmt.Errorf("amneziawg: client %q: %w", c.Email, err)
|
||||
}
|
||||
if err := amneziawg.ValidateConfigValue("preSharedKey", c.PreSharedKey); err != nil {
|
||||
return fmt.Errorf("amneziawg: client %q: %w", c.Email, err)
|
||||
}
|
||||
}
|
||||
|
||||
bs, err := json.MarshalIndent(parsed, "", " ")
|
||||
@@ -217,25 +237,47 @@ func (s *InboundService) normalizeAmneziaWGSettings(inbound *model.Inbound) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// portConflictContext caches the state checkForwardedPortsConflict needs —
|
||||
// the panel's own port and this host's enabled inbound ports — so validating
|
||||
// N clients in one save (normalizeAmneziaWGSettings, or a bulk client add)
|
||||
// costs one query total instead of N. Load it once with
|
||||
// loadPortConflictContext and pass it to every checkForwardedPortsConflict
|
||||
// call in that batch.
|
||||
type portConflictContext struct {
|
||||
webPort int
|
||||
inbounds []*model.Inbound
|
||||
}
|
||||
|
||||
// loadPortConflictContext loads the panel's own port and every enabled
|
||||
// inbound hosted on THIS panel (node_id IS NULL) — an inbound hosted on a
|
||||
// different node listens on that node's own host, never this one, so it can
|
||||
// never collide with a DNAT rule this process installs.
|
||||
func (s *InboundService) loadPortConflictContext() (portConflictContext, error) {
|
||||
var ctx portConflictContext
|
||||
if webPort, err := (&SettingService{}).GetPort(); err == nil {
|
||||
ctx.webPort = webPort
|
||||
}
|
||||
err := database.GetDB().Model(model.Inbound{}).
|
||||
Where("enable = ? AND node_id IS NULL", true).
|
||||
Find(&ctx.inbounds).Error
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// checkForwardedPortsConflict reports whether a client's ForwardedPorts spec
|
||||
// covers the panel's own web port or any enabled inbound's own listen port.
|
||||
// portForwardLines has no destination restriction and nothing else checks
|
||||
// this, so a collision here would silently DNAT traffic meant for the panel
|
||||
// or another protocol straight to the tunnel client instead. Returns a
|
||||
// human-readable description of the first collision found, or "" when there
|
||||
// is none.
|
||||
func (s *InboundService) checkForwardedPortsConflict(forwardedPorts string) (string, error) {
|
||||
// covers the panel's own web port or any of this host's own enabled inbound
|
||||
// listen ports. portForwardLines has no destination restriction and nothing
|
||||
// else checks this, so a collision here would silently DNAT traffic meant
|
||||
// for the panel or another protocol straight to the tunnel client instead.
|
||||
// Returns a human-readable description of the first collision found, or ""
|
||||
// when there is none.
|
||||
func (s *InboundService) checkForwardedPortsConflict(ctx portConflictContext, forwardedPorts string) string {
|
||||
if forwardedPorts == "" {
|
||||
return "", nil
|
||||
return ""
|
||||
}
|
||||
if webPort, err := (&SettingService{}).GetPort(); err == nil && amneziawg.ForwardedPortsInclude(forwardedPorts, webPort) {
|
||||
return fmt.Sprintf("the panel's own port (%d)", webPort), nil
|
||||
if ctx.webPort > 0 && amneziawg.ForwardedPortsInclude(forwardedPorts, ctx.webPort) {
|
||||
return fmt.Sprintf("the panel's own port (%d)", ctx.webPort)
|
||||
}
|
||||
var inbounds []*model.Inbound
|
||||
if err := database.GetDB().Model(model.Inbound{}).Where("enable = ?", true).Find(&inbounds).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, ib := range inbounds {
|
||||
for _, ib := range ctx.inbounds {
|
||||
if !amneziawg.ForwardedPortsInclude(forwardedPorts, ib.Port) {
|
||||
continue
|
||||
}
|
||||
@@ -243,7 +285,7 @@ func (s *InboundService) checkForwardedPortsConflict(forwardedPorts string) (str
|
||||
if name == "" {
|
||||
name = ib.Tag
|
||||
}
|
||||
return fmt.Sprintf("inbound '%s' (#%d, port %d)", name, ib.Id, ib.Port), nil
|
||||
return fmt.Sprintf("inbound '%s' (#%d, port %d)", name, ib.Id, ib.Port)
|
||||
}
|
||||
return "", nil
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -11,21 +11,25 @@ import (
|
||||
func TestCheckForwardedPortsConflict_EmptySpecNoConflict(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
svc := &InboundService{}
|
||||
hit, err := svc.checkForwardedPortsConflict("")
|
||||
if err != nil || hit != "" {
|
||||
t.Fatalf("an empty spec must never conflict; got hit=%q err=%v", hit, err)
|
||||
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, err := svc.checkForwardedPortsConflict("2053")
|
||||
if err != nil {
|
||||
t.Fatalf("checkForwardedPortsConflict: %v", err)
|
||||
}
|
||||
hit := svc.checkForwardedPortsConflict(ctx, "2053")
|
||||
if !strings.Contains(hit, "panel") {
|
||||
t.Fatalf("expected a collision naming the panel's own port, got %q", hit)
|
||||
}
|
||||
@@ -36,10 +40,11 @@ func TestCheckForwardedPortsConflict_CollidesWithEnabledInboundPort(t *testing.T
|
||||
seedInboundConflict(t, "vless-8080", "0.0.0.0", 8080, model.VLESS, `{"network":"tcp"}`, `{}`)
|
||||
|
||||
svc := &InboundService{}
|
||||
hit, err := svc.checkForwardedPortsConflict("8000-8100")
|
||||
ctx, err := svc.loadPortConflictContext()
|
||||
if err != nil {
|
||||
t.Fatalf("checkForwardedPortsConflict: %v", err)
|
||||
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)
|
||||
}
|
||||
@@ -53,9 +58,12 @@ func TestCheckForwardedPortsConflict_IgnoresDisabledInboundPort(t *testing.T) {
|
||||
}
|
||||
|
||||
svc := &InboundService{}
|
||||
hit, err := svc.checkForwardedPortsConflict("8080")
|
||||
if err != nil || hit != "" {
|
||||
t.Fatalf("a disabled inbound's port must not be reserved; got hit=%q err=%v", hit, err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +72,32 @@ func TestCheckForwardedPortsConflict_NoCollisionWhenPortsDontOverlap(t *testing.
|
||||
seedInboundConflict(t, "vless-8080", "0.0.0.0", 8080, model.VLESS, `{"network":"tcp"}`, `{}`)
|
||||
|
||||
svc := &InboundService{}
|
||||
hit, err := svc.checkForwardedPortsConflict("9000-9100")
|
||||
if err != nil || hit != "" {
|
||||
t.Fatalf("unrelated ports must not conflict; got hit=%q err=%v", hit, err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user