fix(runtime): reset node inbound traffic by node-side id (#6717)

* fix(runtime): reset node inbound traffic by node-side id

Remote.ResetInboundTraffic posted to the master's inbound id (ib.Id),
while every other node-side inbound call resolves the id assigned by the
node from the tag. The reset therefore hit an unrelated inbound or failed
silently (warning only), so the panel reported success either way.

Resolve the id through resolveRemoteID(ctx, ib.Tag) and fail before
posting when the tag cannot be resolved.

Fixes #6713

* fix(job): list the inbound on the periodic-reset fake nodes

Remote.ResetInboundTraffic now resolves the node-side id from the tag via
panel/api/inbounds/list before posting resetTraffic. The fake nodes in
periodic_traffic_reset_nodes_test.go answered that list with no obj, so the
tag never resolved, no reset reached a node, and
TestPeriodicResetReachesInboundNodesConcurrently failed (green on main, red
after merging the node-side-id fix). Each fake node now lists the inbound it
hosts, so the test again measures concurrent resets against a node shaped
like a real one.

---------

Co-authored-by: Кот <kot@zeroclaw.local>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Chester Fishmans
2026-10-03 03:59:11 +05:00
committed by GitHub
parent 3168c87c67
commit 98db0710c9
3 changed files with 64 additions and 5 deletions
+5 -1
View File
@@ -713,7 +713,11 @@ func (r *Remote) ResetAllTraffics(ctx context.Context) error {
}
func (r *Remote) ResetInboundTraffic(ctx context.Context, ib *model.Inbound) error {
_, err := r.do(ctx, http.MethodPost, fmt.Sprintf("panel/api/inbounds/%d/resetTraffic", ib.Id), nil)
id, err := r.resolveRemoteID(ctx, ib.Tag)
if err != nil {
return fmt.Errorf("remote ResetInboundTraffic: resolve tag %q: %w", ib.Tag, err)
}
_, err = r.do(ctx, http.MethodPost, fmt.Sprintf("panel/api/inbounds/%d/resetTraffic", id), nil)
return err
}
+49
View File
@@ -7,6 +7,8 @@ import (
"net/http/httptest"
"slices"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
// The master replays a node's reset backlog through the node's bulk endpoint.
@@ -31,3 +33,50 @@ func TestRemoteResetClientTrafficsPostsEmailsToBulkEndpoint(t *testing.T) {
t.Fatalf("node got %s %v, want /panel/api/clients/bulkResetTraffic [a@x b@x]", path, body.Emails)
}
}
// A central inbound id need not match the node's id, so the reset must target
// the node-side id resolved from the tag, never ib.Id.
func TestRemoteResetInboundTrafficUsesNodeInboundID(t *testing.T) {
var method, path string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
method, path = req.Method, req.URL.Path
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"msg":"ok"}`))
}))
t.Cleanup(srv.Close)
r := NewRemote(nodeForPlainServer(t, srv, "verify", "tok"), nil)
r.cacheSet("n1-in-443", 7)
ib := &model.Inbound{Id: 42, Tag: "n1-in-443"}
if err := r.ResetInboundTraffic(context.Background(), ib); err != nil {
t.Fatalf("ResetInboundTraffic: %v", err)
}
if method != http.MethodPost || path != "/panel/api/inbounds/7/resetTraffic" {
t.Fatalf("node got %s %s, want POST /panel/api/inbounds/7/resetTraffic", method, path)
}
}
// An unresolvable tag must fail before posting, so a reset never lands on an
// unrelated node inbound that happens to share the central id.
func TestRemoteResetInboundTrafficUnknownTagErrors(t *testing.T) {
var resetPosted bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
if req.URL.Path == "/panel/api/inbounds/list" {
_, _ = w.Write([]byte(`{"success":true,"msg":"ok","obj":[]}`))
return
}
resetPosted = true
_, _ = w.Write([]byte(`{"success":true,"msg":"ok"}`))
}))
t.Cleanup(srv.Close)
r := NewRemote(nodeForPlainServer(t, srv, "verify", "tok"), nil)
ib := &model.Inbound{Id: 42, Tag: "n1-in-443"}
if err := r.ResetInboundTraffic(context.Background(), ib); err == nil {
t.Fatal("ResetInboundTraffic error = nil, want unknown-tag error")
}
if resetPosted {
t.Fatal("reset request posted to node despite an unresolved tag")
}
}