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:
Kuzz007
2026-07-29 01:21:03 +03:00
parent a903ca6793
commit d3da7abdf0
9 changed files with 319 additions and 72 deletions
+62 -20
View File
@@ -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 ""
}