fix(inbound): enforce node-eligibility server-side, not just in the UI

Investigated multi-node interaction with AmneziaWG: the master's own
reconcile (DesiredAmneziaWGInstances) and Xray config generation
(injectAmneziawgEgress, the GenXrayInboundConfig protocol skip) all
correctly filter on NodeID IS NULL, so a node-assigned AmneziaWG (or
MTProto) inbound would never be managed by the master. But nothing
stopped one from being created that way: NODE_ELIGIBLE_PROTOCOLS
(frontend/src/pages/inbounds/form/InboundFormModal.tsx) only hides the
node picker client-side -- a direct API call could set nodeId on an
AmneziaWG inbound, which every node then reconciles as an ordinary
local inbound (nodes run the identical binary, full cron suite
included), leaving it running unmanaged and untracked by the master's
own AmneziaWG bookkeeping.

Added isNodeEligibleProtocol (inbound_protocol.go), mirroring the
frontend's allowlist, and enforced it in both AddInbound (the actually
exploitable path -- nodeId comes straight from the request) and
UpdateInbound (defense in depth; NodeID is already restored from the
stored row there before this check, so it mainly guards against a
protocol change on an existing node-hosted inbound).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kuzz007
2026-07-26 11:42:24 +03:00
parent ba1a33f307
commit 1ed9cd8ea1
3 changed files with 48 additions and 0 deletions
@@ -88,3 +88,21 @@ func TestInboundCanHostFallbacks_StaysTcpOnly(t *testing.T) {
t.Errorf("inboundCanHostFallbacks(nil) = true, want false")
}
}
// Mirrors NODE_ELIGIBLE_PROTOCOLS in
// frontend/src/pages/inbounds/form/InboundFormModal.tsx -- keep both lists
// in sync if a protocol's node-eligibility ever changes.
func TestIsNodeEligibleProtocol(t *testing.T) {
eligible := []model.Protocol{model.VLESS, model.VMESS, model.Trojan, model.Shadowsocks, model.Hysteria, model.WireGuard}
for _, p := range eligible {
if !isNodeEligibleProtocol(p) {
t.Errorf("isNodeEligibleProtocol(%q) = false, want true", p)
}
}
ineligible := []model.Protocol{model.MTProto, model.AmneziaWG, model.Mixed, model.HTTP, model.Tunnel}
for _, p := range ineligible {
if isNodeEligibleProtocol(p) {
t.Errorf("isNodeEligibleProtocol(%q) = true, want false", p)
}
}
}