精简P2: 剥离 xray-core API 客户端: StatsService 改 protowire 线缆级实现(QueryStats/GetUsersStats 字段号按核心 proto 复刻, agent 流量上报与本地流量任务不受影响); Handler/Routing 变更类 RPC 无法脱离核心类型化配置构建, 统一降级 ErrHotApplyUnavailable(调用方既有兜底: 热更新->重启核心, balancer 视图报未运行, 节点桥探测放弃, IP 限连临时断开仅记日志); ValidateOutboundConfig 改纯 JSON 实现, 精确复刻核心 v26.7.11 未加密 vless/trojan 公网禁令(含私有 IP/域名匹配表与原文报错, 且同核心一样只查扁平 address 形式); 删除 buildUserAccount 及 SS/WG/e2e 旧测试, 流量增量测试改线缆级假服务器并新增 stock protobuf 服务器互操作测试

This commit is contained in:
Muse
2026-10-20 11:03:45 +00:00
parent c56a02c948
commit cb8981158f
8 changed files with 762 additions and 1647 deletions
+52 -15
View File
@@ -5,41 +5,78 @@ import (
"net"
"testing"
statsService "github.com/xtls/xray-core/app/stats/command"
"google.golang.org/grpc"
"google.golang.org/protobuf/encoding/protowire"
)
// fakeStatsServer serves a scripted sequence of QueryStats responses, one per
// call, so the delta bookkeeping in GetTraffic can be driven exactly.
// call, so the delta bookkeeping in GetTraffic can be driven exactly. It
// speaks the stats service's wire format directly: this build has no typed
// protobuf client, so the fake encodes responses with protowire and the
// server runs with the client's raw codec.
type fakeStatsServer struct {
statsService.UnimplementedStatsServiceServer
rounds [][]*statsService.Stat
rounds [][]statEntry
calls int
}
func (f *fakeStatsServer) QueryStats(context.Context, *statsService.QueryStatsRequest) (*statsService.QueryStatsResponse, error) {
func (f *fakeStatsServer) queryStats() rawBytes {
round := f.calls
f.calls++
if round >= len(f.rounds) {
round = len(f.rounds) - 1
}
return &statsService.QueryStatsResponse{Stat: f.rounds[round]}, nil
return encodeQueryStatsResponse(f.rounds[round])
}
func stat(name string, value int64) *statsService.Stat {
return &statsService.Stat{Name: name, Value: value}
// encodeQueryStatsResponse builds a QueryStatsResponse message
// (stat = 1, repeated Stat{name = 1, value = 2}).
func encodeQueryStatsResponse(stats []statEntry) rawBytes {
var out []byte
for _, st := range stats {
var statMsg []byte
statMsg = protowire.AppendTag(statMsg, 1, protowire.BytesType)
statMsg = protowire.AppendString(statMsg, st.Name)
statMsg = protowire.AppendTag(statMsg, 2, protowire.VarintType)
statMsg = protowire.AppendVarint(statMsg, uint64(st.Value))
out = protowire.AppendTag(out, 1, protowire.BytesType)
out = protowire.AppendBytes(out, statMsg)
}
return rawBytes(out)
}
func stat(name string, value int64) statEntry {
return statEntry{Name: name, Value: value}
}
type statsServiceIface interface{}
// startFakeStats runs the scripted stats service and returns an XrayAPI wired
// to it.
func startFakeStats(t *testing.T, rounds [][]*statsService.Stat) *XrayAPI {
func startFakeStats(t *testing.T, rounds [][]statEntry) *XrayAPI {
t.Helper()
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
srv := grpc.NewServer()
statsService.RegisterStatsServiceServer(srv, &fakeStatsServer{rounds: rounds})
fake := &fakeStatsServer{rounds: rounds}
srv := grpc.NewServer(grpc.ForceServerCodec(rawCodec{}))
srv.RegisterService(&grpc.ServiceDesc{
ServiceName: "xray.app.stats.command.StatsService",
HandlerType: (*statsServiceIface)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "QueryStats",
Handler: func(_ any, _ context.Context, dec func(any) error, _ grpc.UnaryServerInterceptor) (any, error) {
var req rawBytes
if err := dec(&req); err != nil {
return nil, err
}
return fake.queryStats(), nil
},
},
},
}, fake)
go func() { _ = srv.Serve(lis) }()
t.Cleanup(srv.Stop)
@@ -64,7 +101,7 @@ func clientTrafficByEmail(t *testing.T, traffics []*ClientTraffic) map[string]*C
// the panel may attach to counters that already hold traffic it cannot
// attribute, so the first poll of a client only records baselines.
func TestGetTrafficFirstPollIsBaselineOnly(t *testing.T) {
api := startFakeStats(t, [][]*statsService.Stat{
api := startFakeStats(t, [][]statEntry{
{stat("user>>>alice>>>traffic>>>uplink", 5000)},
})
@@ -84,7 +121,7 @@ func TestGetTrafficFirstPollIsBaselineOnly(t *testing.T) {
// traffic being dropped: xray creates a counter on first use, so a name that
// appears after the baseline poll starts at zero and every byte in it is new.
func TestGetTrafficCountsNewStatFromZero(t *testing.T) {
api := startFakeStats(t, [][]*statsService.Stat{
api := startFakeStats(t, [][]statEntry{
{stat("user>>>alice>>>traffic>>>uplink", 100)},
{
stat("user>>>alice>>>traffic>>>uplink", 180),
@@ -122,7 +159,7 @@ func TestGetTrafficCountsNewStatFromZero(t *testing.T) {
// start over at zero, so a value below the recorded baseline is all new
// traffic rather than something to drop.
func TestGetTrafficCountsAfterCounterReset(t *testing.T) {
api := startFakeStats(t, [][]*statsService.Stat{
api := startFakeStats(t, [][]statEntry{
{stat("user>>>alice>>>traffic>>>uplink", 100)},
{stat("user>>>alice>>>traffic>>>uplink", 900)},
{stat("user>>>alice>>>traffic>>>uplink", 250)},
@@ -152,7 +189,7 @@ func TestGetTrafficCountsAfterCounterReset(t *testing.T) {
// inbound is the panel's own gRPC channel and is never a user-facing inbound,
// and baselines for vanished stats are dropped once they outgrow the live set.
func TestGetTrafficSkipsAPIInboundAndPrunes(t *testing.T) {
api := startFakeStats(t, [][]*statsService.Stat{
api := startFakeStats(t, [][]statEntry{
{
stat("inbound>>>api>>>traffic>>>uplink", 10),
stat("inbound>>>in-443>>>traffic>>>uplink", 10),