fix(api-token): let a node-sync master reach every node endpoint it calls

Invariant: every request runtime.Remote sends to a node is accepted under
the node-sync scope, except /server/updatePanel, which #6201 withholds on
purpose.

The allowlist was written on 2026-08-15 and three Remote calls arrived
after it without being added: /clients/activeInbounds (#6164, same day),
/inbounds/:id/subSortIndex (#6179) and /clients/bulkResetTraffic
(4210a50c). A master enrolled with a node-sync token or mTLS got 403 on
each: the subscription sort order and multi-client traffic resets never
reached the node, and online attribution silently fell back to email-only
because FetchTrafficSnapshot logs that failure at debug level.

TestMasterNodeContract drives every exported Remote method through the
production router, once per enrollment scope, fails on any 401/403 the
node returns even when Remote swallows it, and fails when a Remote method
has no cell. It replaces TestNodeSyncScopeAllowlistMatchesRemoteInventory,
a hand-copied duplicate of the allowlist that never read Remote and so
missed all three; its updatePanel rule stays in
TestNodeSyncScopeUsesFullPathPatterns. It also supersedes
TestMasterPushWithAdminTokenAppliesClients (its UpdateInbound cell); that
file's removal landed in dae91276 by a staging slip.
This commit is contained in:
MHSanaei
2026-10-05 13:31:02 +02:00
parent dae91276aa
commit 5819a01cdc
3 changed files with 385 additions and 34 deletions
-34
View File
@@ -7,7 +7,6 @@ import (
"net/http/cookiejar"
"net/http/httptest"
"path/filepath"
"reflect"
"testing"
"github.com/gin-contrib/sessions"
@@ -138,39 +137,6 @@ func TestCheckAPIAuth_AcceptsVerifiedClientCert(t *testing.T) {
}
}
func TestNodeSyncScopeAllowlistMatchesRemoteInventory(t *testing.T) {
expected := map[string]map[string]struct{}{
"/server/status": {http.MethodGet: {}},
"/inbounds/list": {http.MethodGet: {}},
"/inbounds/add": {http.MethodPost: {}},
"/inbounds/del/:id": {http.MethodPost: {}},
"/inbounds/update/:id": {http.MethodPost: {}},
"/clients/add": {http.MethodPost: {}},
"/clients/del/:email": {http.MethodPost: {}},
"/clients/:email/detach": {http.MethodPost: {}},
"/clients/update/:email": {http.MethodPost: {}},
"/server/restartXrayService": {http.MethodPost: {}},
"/server/getWebCertFiles": {http.MethodGet: {}},
"/server/descendants": {http.MethodGet: {}},
"/clients/resetTraffic/:email": {http.MethodPost: {}},
"/inbounds/resetAllTraffics": {http.MethodPost: {}},
"/inbounds/:id/resetTraffic": {http.MethodPost: {}},
"/clients/onlinesByGuid": {http.MethodPost: {}},
"/clients/onlines": {http.MethodPost: {}},
"/clients/lastOnline": {http.MethodPost: {}},
"/inbounds/pushClientTraffics": {http.MethodPost: {}},
"/server/clientIps": {http.MethodGet: {}, http.MethodPost: {}},
"/clients/clientIpsByGuid": {http.MethodPost: {}},
"/hosts/list": {http.MethodGet: {}},
}
if !reflect.DeepEqual(nodeSyncScopeAllow, expected) {
t.Fatalf("node-sync allowlist drift:\n got: %#v\nwant: %#v", nodeSyncScopeAllow, expected)
}
if _, ok := nodeSyncScopeAllow["/server/updatePanel"]; ok {
t.Fatal("node-sync must not include /server/updatePanel")
}
}
func TestNodeSyncScopeUsesFullPathPatterns(t *testing.T) {
engine, _ := newAPIAuthTestEngine(t)
cases := []struct {