api_traffic_test.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. package xray
  2. import (
  3. "context"
  4. "net"
  5. "testing"
  6. statsService "github.com/xtls/xray-core/app/stats/command"
  7. "google.golang.org/grpc"
  8. )
  9. // fakeStatsServer serves a scripted sequence of QueryStats responses, one per
  10. // call, so the delta bookkeeping in GetTraffic can be driven exactly.
  11. type fakeStatsServer struct {
  12. statsService.UnimplementedStatsServiceServer
  13. rounds [][]*statsService.Stat
  14. calls int
  15. }
  16. func (f *fakeStatsServer) QueryStats(context.Context, *statsService.QueryStatsRequest) (*statsService.QueryStatsResponse, error) {
  17. round := f.calls
  18. f.calls++
  19. if round >= len(f.rounds) {
  20. round = len(f.rounds) - 1
  21. }
  22. return &statsService.QueryStatsResponse{Stat: f.rounds[round]}, nil
  23. }
  24. func stat(name string, value int64) *statsService.Stat {
  25. return &statsService.Stat{Name: name, Value: value}
  26. }
  27. // startFakeStats runs the scripted stats service and returns an XrayAPI wired
  28. // to it.
  29. func startFakeStats(t *testing.T, rounds [][]*statsService.Stat) *XrayAPI {
  30. t.Helper()
  31. lis, err := net.Listen("tcp", "127.0.0.1:0")
  32. if err != nil {
  33. t.Fatal(err)
  34. }
  35. srv := grpc.NewServer()
  36. statsService.RegisterStatsServiceServer(srv, &fakeStatsServer{rounds: rounds})
  37. go func() { _ = srv.Serve(lis) }()
  38. t.Cleanup(srv.Stop)
  39. api := &XrayAPI{}
  40. if err := api.Init(lis.Addr().(*net.TCPAddr).Port); err != nil {
  41. t.Fatalf("api init: %v", err)
  42. }
  43. t.Cleanup(api.Close)
  44. return api
  45. }
  46. func clientTrafficByEmail(t *testing.T, traffics []*ClientTraffic) map[string]*ClientTraffic {
  47. t.Helper()
  48. byEmail := make(map[string]*ClientTraffic, len(traffics))
  49. for _, ct := range traffics {
  50. byEmail[ct.Email] = ct
  51. }
  52. return byEmail
  53. }
  54. // TestGetTrafficFirstPollIsBaselineOnly pins the one case the skip protects:
  55. // the panel may attach to counters that already hold traffic it cannot
  56. // attribute, so the first poll of a client only records baselines.
  57. func TestGetTrafficFirstPollIsBaselineOnly(t *testing.T) {
  58. api := startFakeStats(t, [][]*statsService.Stat{
  59. {stat("user>>>alice>>>traffic>>>uplink", 5000)},
  60. })
  61. _, clients, err := api.GetTraffic()
  62. if err != nil {
  63. t.Fatalf("GetTraffic: %v", err)
  64. }
  65. if len(clients) != 0 {
  66. t.Fatalf("first poll reported %+v, want no traffic (baseline only)", clients[0])
  67. }
  68. if got := api.StatsLastValues["user>>>alice>>>traffic>>>uplink"]; got != 5000 {
  69. t.Fatalf("baseline = %d, want 5000", got)
  70. }
  71. }
  72. // TestGetTrafficCountsNewStatFromZero is the regression for a new client's
  73. // traffic being dropped: xray creates a counter on first use, so a name that
  74. // appears after the baseline poll starts at zero and every byte in it is new.
  75. func TestGetTrafficCountsNewStatFromZero(t *testing.T) {
  76. api := startFakeStats(t, [][]*statsService.Stat{
  77. {stat("user>>>alice>>>traffic>>>uplink", 100)},
  78. {
  79. stat("user>>>alice>>>traffic>>>uplink", 180),
  80. stat("user>>>bob>>>traffic>>>uplink", 4096),
  81. stat("user>>>bob>>>traffic>>>downlink", 8192),
  82. },
  83. })
  84. if _, _, err := api.GetTraffic(); err != nil {
  85. t.Fatalf("GetTraffic (baseline): %v", err)
  86. }
  87. _, clients, err := api.GetTraffic()
  88. if err != nil {
  89. t.Fatalf("GetTraffic: %v", err)
  90. }
  91. byEmail := clientTrafficByEmail(t, clients)
  92. bob, ok := byEmail["bob"]
  93. if !ok {
  94. t.Fatal("a client whose counter appeared after the baseline poll reported no traffic")
  95. }
  96. if bob.Up != 4096 || bob.Down != 8192 {
  97. t.Fatalf("bob = up %d / down %d, want 4096 / 8192", bob.Up, bob.Down)
  98. }
  99. alice, ok := byEmail["alice"]
  100. if !ok {
  101. t.Fatal("alice reported no traffic")
  102. }
  103. if alice.Up != 80 {
  104. t.Fatalf("alice up = %d, want the delta 80", alice.Up)
  105. }
  106. }
  107. // TestGetTrafficCountsAfterCounterReset covers a core restart: the counters
  108. // start over at zero, so a value below the recorded baseline is all new
  109. // traffic rather than something to drop.
  110. func TestGetTrafficCountsAfterCounterReset(t *testing.T) {
  111. api := startFakeStats(t, [][]*statsService.Stat{
  112. {stat("user>>>alice>>>traffic>>>uplink", 100)},
  113. {stat("user>>>alice>>>traffic>>>uplink", 900)},
  114. {stat("user>>>alice>>>traffic>>>uplink", 250)},
  115. })
  116. if _, _, err := api.GetTraffic(); err != nil {
  117. t.Fatalf("GetTraffic (baseline): %v", err)
  118. }
  119. if _, _, err := api.GetTraffic(); err != nil {
  120. t.Fatalf("GetTraffic (delta): %v", err)
  121. }
  122. _, clients, err := api.GetTraffic()
  123. if err != nil {
  124. t.Fatalf("GetTraffic (after reset): %v", err)
  125. }
  126. alice, ok := clientTrafficByEmail(t, clients)["alice"]
  127. if !ok {
  128. t.Fatal("traffic after a counter reset was dropped entirely")
  129. }
  130. if alice.Up != 250 {
  131. t.Fatalf("alice up = %d, want 250 counted from zero", alice.Up)
  132. }
  133. }
  134. // TestGetTrafficSkipsAPIInboundAndPrunes checks the tag-level parsing: the api
  135. // inbound is the panel's own gRPC channel and is never a user-facing inbound,
  136. // and baselines for vanished stats are dropped once they outgrow the live set.
  137. func TestGetTrafficSkipsAPIInboundAndPrunes(t *testing.T) {
  138. api := startFakeStats(t, [][]*statsService.Stat{
  139. {
  140. stat("inbound>>>api>>>traffic>>>uplink", 10),
  141. stat("inbound>>>in-443>>>traffic>>>uplink", 10),
  142. stat("inbound>>>gone-1>>>traffic>>>uplink", 10),
  143. stat("inbound>>>gone-2>>>traffic>>>uplink", 10),
  144. stat("inbound>>>gone-3>>>traffic>>>uplink", 10),
  145. stat("inbound>>>gone-4>>>traffic>>>uplink", 10),
  146. stat("inbound>>>gone-5>>>traffic>>>uplink", 10),
  147. },
  148. {
  149. stat("inbound>>>api>>>traffic>>>uplink", 99),
  150. stat("inbound>>>in-443>>>traffic>>>uplink", 60),
  151. stat("inbound>>>in-443>>>traffic>>>downlink", 70),
  152. },
  153. })
  154. if _, _, err := api.GetTraffic(); err != nil {
  155. t.Fatalf("GetTraffic (baseline): %v", err)
  156. }
  157. tags, _, err := api.GetTraffic()
  158. if err != nil {
  159. t.Fatalf("GetTraffic: %v", err)
  160. }
  161. if len(tags) != 1 {
  162. t.Fatalf("got %d tag traffics, want only the non-api inbound: %+v", len(tags), tags)
  163. }
  164. if tags[0].Tag != "in-443" || !tags[0].IsInbound || tags[0].IsOutbound {
  165. t.Fatalf("tag traffic = %+v, want inbound in-443", tags[0])
  166. }
  167. if tags[0].Up != 50 || tags[0].Down != 70 {
  168. t.Fatalf("in-443 = up %d / down %d, want 50 / 70", tags[0].Up, tags[0].Down)
  169. }
  170. if _, stale := api.StatsLastValues["inbound>>>gone-1>>>traffic>>>uplink"]; stale {
  171. t.Fatal("baselines for stats that no longer exist were not pruned")
  172. }
  173. }