| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- package xray
- import (
- "context"
- "net"
- "testing"
- statsService "github.com/xtls/xray-core/app/stats/command"
- "google.golang.org/grpc"
- )
- // fakeStatsServer serves a scripted sequence of QueryStats responses, one per
- // call, so the delta bookkeeping in GetTraffic can be driven exactly.
- type fakeStatsServer struct {
- statsService.UnimplementedStatsServiceServer
- rounds [][]*statsService.Stat
- calls int
- }
- func (f *fakeStatsServer) QueryStats(context.Context, *statsService.QueryStatsRequest) (*statsService.QueryStatsResponse, error) {
- round := f.calls
- f.calls++
- if round >= len(f.rounds) {
- round = len(f.rounds) - 1
- }
- return &statsService.QueryStatsResponse{Stat: f.rounds[round]}, nil
- }
- func stat(name string, value int64) *statsService.Stat {
- return &statsService.Stat{Name: name, Value: value}
- }
- // startFakeStats runs the scripted stats service and returns an XrayAPI wired
- // to it.
- func startFakeStats(t *testing.T, rounds [][]*statsService.Stat) *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})
- go func() { _ = srv.Serve(lis) }()
- t.Cleanup(srv.Stop)
- api := &XrayAPI{}
- if err := api.Init(lis.Addr().(*net.TCPAddr).Port); err != nil {
- t.Fatalf("api init: %v", err)
- }
- t.Cleanup(api.Close)
- return api
- }
- func clientTrafficByEmail(t *testing.T, traffics []*ClientTraffic) map[string]*ClientTraffic {
- t.Helper()
- byEmail := make(map[string]*ClientTraffic, len(traffics))
- for _, ct := range traffics {
- byEmail[ct.Email] = ct
- }
- return byEmail
- }
- // TestGetTrafficFirstPollIsBaselineOnly pins the one case the skip protects:
- // 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{
- {stat("user>>>alice>>>traffic>>>uplink", 5000)},
- })
- _, clients, err := api.GetTraffic()
- if err != nil {
- t.Fatalf("GetTraffic: %v", err)
- }
- if len(clients) != 0 {
- t.Fatalf("first poll reported %+v, want no traffic (baseline only)", clients[0])
- }
- if got := api.StatsLastValues["user>>>alice>>>traffic>>>uplink"]; got != 5000 {
- t.Fatalf("baseline = %d, want 5000", got)
- }
- }
- // TestGetTrafficCountsNewStatFromZero is the regression for a new client's
- // 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{
- {stat("user>>>alice>>>traffic>>>uplink", 100)},
- {
- stat("user>>>alice>>>traffic>>>uplink", 180),
- stat("user>>>bob>>>traffic>>>uplink", 4096),
- stat("user>>>bob>>>traffic>>>downlink", 8192),
- },
- })
- if _, _, err := api.GetTraffic(); err != nil {
- t.Fatalf("GetTraffic (baseline): %v", err)
- }
- _, clients, err := api.GetTraffic()
- if err != nil {
- t.Fatalf("GetTraffic: %v", err)
- }
- byEmail := clientTrafficByEmail(t, clients)
- bob, ok := byEmail["bob"]
- if !ok {
- t.Fatal("a client whose counter appeared after the baseline poll reported no traffic")
- }
- if bob.Up != 4096 || bob.Down != 8192 {
- t.Fatalf("bob = up %d / down %d, want 4096 / 8192", bob.Up, bob.Down)
- }
- alice, ok := byEmail["alice"]
- if !ok {
- t.Fatal("alice reported no traffic")
- }
- if alice.Up != 80 {
- t.Fatalf("alice up = %d, want the delta 80", alice.Up)
- }
- }
- // TestGetTrafficCountsAfterCounterReset covers a core restart: the counters
- // 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{
- {stat("user>>>alice>>>traffic>>>uplink", 100)},
- {stat("user>>>alice>>>traffic>>>uplink", 900)},
- {stat("user>>>alice>>>traffic>>>uplink", 250)},
- })
- if _, _, err := api.GetTraffic(); err != nil {
- t.Fatalf("GetTraffic (baseline): %v", err)
- }
- if _, _, err := api.GetTraffic(); err != nil {
- t.Fatalf("GetTraffic (delta): %v", err)
- }
- _, clients, err := api.GetTraffic()
- if err != nil {
- t.Fatalf("GetTraffic (after reset): %v", err)
- }
- alice, ok := clientTrafficByEmail(t, clients)["alice"]
- if !ok {
- t.Fatal("traffic after a counter reset was dropped entirely")
- }
- if alice.Up != 250 {
- t.Fatalf("alice up = %d, want 250 counted from zero", alice.Up)
- }
- }
- // TestGetTrafficSkipsAPIInboundAndPrunes checks the tag-level parsing: the api
- // 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{
- {
- stat("inbound>>>api>>>traffic>>>uplink", 10),
- stat("inbound>>>in-443>>>traffic>>>uplink", 10),
- stat("inbound>>>gone-1>>>traffic>>>uplink", 10),
- stat("inbound>>>gone-2>>>traffic>>>uplink", 10),
- stat("inbound>>>gone-3>>>traffic>>>uplink", 10),
- stat("inbound>>>gone-4>>>traffic>>>uplink", 10),
- stat("inbound>>>gone-5>>>traffic>>>uplink", 10),
- },
- {
- stat("inbound>>>api>>>traffic>>>uplink", 99),
- stat("inbound>>>in-443>>>traffic>>>uplink", 60),
- stat("inbound>>>in-443>>>traffic>>>downlink", 70),
- },
- })
- if _, _, err := api.GetTraffic(); err != nil {
- t.Fatalf("GetTraffic (baseline): %v", err)
- }
- tags, _, err := api.GetTraffic()
- if err != nil {
- t.Fatalf("GetTraffic: %v", err)
- }
- if len(tags) != 1 {
- t.Fatalf("got %d tag traffics, want only the non-api inbound: %+v", len(tags), tags)
- }
- if tags[0].Tag != "in-443" || !tags[0].IsInbound || tags[0].IsOutbound {
- t.Fatalf("tag traffic = %+v, want inbound in-443", tags[0])
- }
- if tags[0].Up != 50 || tags[0].Down != 70 {
- t.Fatalf("in-443 = up %d / down %d, want 50 / 70", tags[0].Up, tags[0].Down)
- }
- if _, stale := api.StatsLastValues["inbound>>>gone-1>>>traffic>>>uplink"]; stale {
- t.Fatal("baselines for stats that no longer exist were not pruned")
- }
- }
|