client.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. package bwtest
  2. import (
  3. "bufio"
  4. "context"
  5. crand "crypto/rand"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "math/rand"
  10. "net"
  11. "sync"
  12. "sync/atomic"
  13. "time"
  14. )
  15. var (
  16. ErrTestAlreadyRunning = errors.New("a bandwidth test is already in progress")
  17. ErrServerRejected = errors.New("remote server rejected bandwidth test request")
  18. )
  19. // BwTestClient manages executing bandwidth tests against a remote BwTest server
  20. type BwTestClient struct {
  21. mu sync.RWMutex
  22. running bool
  23. currentCancel context.CancelFunc
  24. status TestStatus
  25. samples []*LiveSample
  26. subscribers map[chan LiveSample]struct{}
  27. sampleCount uint64
  28. jitterSum, peakTx, peakRx float64
  29. }
  30. // NewClient creates a new BwTestClient instance
  31. func NewClient() *BwTestClient {
  32. return &BwTestClient{
  33. subscribers: make(map[chan LiveSample]struct{}),
  34. }
  35. }
  36. // GetStatus returns the current or most recent test state
  37. func (c *BwTestClient) GetStatus() TestStatus {
  38. c.mu.RLock()
  39. defer c.mu.RUnlock()
  40. st := c.status
  41. if len(c.samples) > 0 {
  42. st.CurrentSample = c.samples[len(c.samples)-1]
  43. st.RecentSamples = append([]*LiveSample(nil), c.samples...)
  44. }
  45. return st
  46. }
  47. // Subscribe gives each observer its own bounded queue. Slow observers never
  48. // block the data path; they can resynchronize using GetStatus.
  49. func (c *BwTestClient) Subscribe() (<-chan LiveSample, func()) {
  50. c.mu.Lock()
  51. ch := make(chan LiveSample, 16)
  52. c.subscribers[ch] = struct{}{}
  53. c.mu.Unlock()
  54. var once sync.Once
  55. return ch, func() { once.Do(func() { c.mu.Lock(); delete(c.subscribers, ch); close(ch); c.mu.Unlock() }) }
  56. }
  57. func (c *BwTestClient) recordSampleLocked(sample *LiveSample) {
  58. c.sampleCount++
  59. c.jitterSum += sample.JitterMs
  60. c.peakTx = max(c.peakTx, sample.TxMbps)
  61. c.peakRx = max(c.peakRx, sample.RxMbps)
  62. c.samples = append(c.samples, sample)
  63. if len(c.samples) > 200 {
  64. c.samples = c.samples[len(c.samples)-200:]
  65. }
  66. c.status.ElapsedSec = sample.ElapsedSec
  67. c.status.CurrentSample = sample
  68. for ch := range c.subscribers {
  69. select {
  70. case ch <- *sample:
  71. default:
  72. }
  73. }
  74. }
  75. // Stop terminates any currently active bandwidth test
  76. func (c *BwTestClient) Stop() {
  77. c.mu.Lock()
  78. defer c.mu.Unlock()
  79. if c.running && c.currentCancel != nil {
  80. c.currentCancel()
  81. }
  82. }
  83. // Run executes a bandwidth test asynchronously
  84. func (c *BwTestClient) Run(ctx context.Context, req SessionRequest) error {
  85. c.mu.Lock()
  86. if c.running {
  87. c.mu.Unlock()
  88. return ErrTestAlreadyRunning
  89. }
  90. if req.Target == "" {
  91. req.Target = "127.0.0.1"
  92. }
  93. if req.Port <= 0 {
  94. req.Port = 2000
  95. }
  96. if req.Protocol != "tcp" {
  97. req.Protocol = "udp"
  98. }
  99. if req.Direction != "tx" && req.Direction != "rx" && req.Direction != "both" {
  100. req.Direction = "both"
  101. }
  102. if req.PacketSize <= 0 {
  103. req.PacketSize = 1472
  104. }
  105. if req.PacketSize < 64 || req.PacketSize > 65507 {
  106. c.mu.Unlock()
  107. return fmt.Errorf("packet size must be between 64 and 65507 bytes")
  108. }
  109. if req.Streams <= 0 {
  110. req.Streams = 1
  111. }
  112. if req.Streams > 8 {
  113. req.Streams = 8
  114. }
  115. if req.Duration < 0 {
  116. req.Duration = 10
  117. }
  118. testCtx, cancel := context.WithCancel(context.Background())
  119. c.currentCancel = cancel
  120. c.running = true
  121. c.samples = make([]*LiveSample, 0, 200)
  122. c.sampleCount, c.jitterSum, c.peakTx, c.peakRx = 0, 0, 0, 0
  123. sessionID := fmt.Sprintf("bw-%d-%d", time.Now().Unix(), rand.Intn(10000))
  124. c.status = TestStatus{
  125. Running: true,
  126. SessionID: sessionID,
  127. Target: req.Target,
  128. Port: req.Port,
  129. Protocol: req.Protocol,
  130. Direction: req.Direction,
  131. BandwidthLimit: req.BandwidthLimit,
  132. PacketSize: req.PacketSize,
  133. Streams: req.Streams,
  134. Duration: req.Duration,
  135. StartTime: time.Now(),
  136. }
  137. c.mu.Unlock()
  138. go c.executeTest(testCtx, cancel, req, sessionID)
  139. return nil
  140. }
  141. func (c *BwTestClient) executeTest(ctx context.Context, cancel context.CancelFunc, req SessionRequest, sessionID string) {
  142. defer func() {
  143. cancel()
  144. c.mu.Lock()
  145. c.running = false
  146. c.status.Running = false
  147. c.finalizeSummary()
  148. c.mu.Unlock()
  149. }()
  150. targetAddr := fmt.Sprintf("%s:%d", req.Target, req.Port)
  151. dialer := net.Dialer{Timeout: 5 * time.Second}
  152. controlConn, err := dialer.DialContext(ctx, "tcp", targetAddr)
  153. if err != nil {
  154. c.mu.Lock()
  155. c.status.Error = fmt.Sprintf("Failed to connect to control port %s: %v", targetAddr, err)
  156. c.mu.Unlock()
  157. return
  158. }
  159. defer controlConn.Close()
  160. stopClose := context.AfterFunc(ctx, func() { controlConn.Close() })
  161. defer stopClose()
  162. _ = controlConn.SetDeadline(time.Now().Add(5 * time.Second))
  163. // If UDP, bind a local UDP socket
  164. var udpConn *net.UDPConn
  165. var clientUDPPort int
  166. if req.Protocol == "udp" {
  167. local := controlConn.LocalAddr().(*net.TCPAddr)
  168. uaddr := &net.UDPAddr{IP: local.IP, Zone: local.Zone}
  169. var err error
  170. if err == nil {
  171. udpConn, err = net.ListenUDP("udp", uaddr)
  172. }
  173. if err != nil {
  174. c.mu.Lock()
  175. c.status.Error = fmt.Sprintf("Failed to create local UDP socket: %v", err)
  176. c.mu.Unlock()
  177. return
  178. }
  179. defer udpConn.Close()
  180. _ = udpConn.SetReadBuffer(8 * 1024 * 1024)
  181. if la, ok := udpConn.LocalAddr().(*net.UDPAddr); ok {
  182. clientUDPPort = la.Port
  183. }
  184. }
  185. if req.Protocol == "udp" {
  186. size, err := udpPayloadSize(req.PacketSize, controlConn.RemoteAddr().(*net.TCPAddr).IP)
  187. if err != nil {
  188. c.mu.Lock()
  189. c.status.Error = err.Error()
  190. c.mu.Unlock()
  191. return
  192. }
  193. req.PacketSize = size
  194. }
  195. // Send handshake request
  196. hs := SessionHandshake{
  197. Type: "request",
  198. SessionID: sessionID,
  199. Protocol: req.Protocol,
  200. Direction: req.Direction,
  201. BandwidthLimit: req.BandwidthLimit,
  202. PacketSize: req.PacketSize,
  203. Streams: req.Streams,
  204. Duration: req.Duration,
  205. UDPPort: clientUDPPort,
  206. }
  207. if req.Protocol == "tcp" {
  208. hs.DataFraming = tcpFramingVersion
  209. } else {
  210. hs.UDPFeedback = udpFeedbackVersion
  211. }
  212. b, _ := json.Marshal(hs)
  213. if _, err := controlConn.Write(append(b, '\n')); err != nil {
  214. c.mu.Lock()
  215. c.status.Error = fmt.Sprintf("Handshake send failed: %v", err)
  216. c.mu.Unlock()
  217. return
  218. }
  219. // Read accept response
  220. reader := bufio.NewReader(controlConn)
  221. line, err := reader.ReadBytes('\n')
  222. if err != nil {
  223. c.mu.Lock()
  224. c.status.Error = fmt.Sprintf("Server response error: %v", err)
  225. c.mu.Unlock()
  226. return
  227. }
  228. var acceptResp SessionHandshake
  229. if err := json.Unmarshal(line, &acceptResp); err != nil || acceptResp.Type != "accept" {
  230. c.mu.Lock()
  231. c.status.Error = fmt.Sprintf("Server rejected test: %s", acceptResp.Message)
  232. c.mu.Unlock()
  233. return
  234. }
  235. if req.Protocol == "tcp" && acceptResp.DataFraming != tcpFramingVersion {
  236. c.mu.Lock()
  237. c.status.Error = "Server does not support framed TCP bandwidth tests; upgrade both peers"
  238. c.mu.Unlock()
  239. return
  240. }
  241. if req.Protocol == "udp" {
  242. if req.BandwidthLimit <= 0 && acceptResp.UDPFeedback != udpFeedbackVersion {
  243. c.mu.Lock()
  244. c.status.Error = "Automatic UDP tests require receiver-feedback support on both peers"
  245. c.mu.Unlock()
  246. return
  247. }
  248. if acceptResp.PacketSize >= 64 {
  249. req.PacketSize = min(req.PacketSize, acceptResp.PacketSize)
  250. }
  251. c.mu.Lock()
  252. c.status.PacketSize = req.PacketSize
  253. c.mu.Unlock()
  254. }
  255. _ = controlConn.SetDeadline(time.Time{})
  256. tcpWriter := &tcpFrameWriter{conn: controlConn}
  257. serverUDPPort := acceptResp.UDPPort
  258. if serverUDPPort <= 0 {
  259. serverUDPPort = req.Port
  260. }
  261. // Statistics counters
  262. var (
  263. localTxBytes atomic.Int64
  264. localTxPackets atomic.Int64
  265. localRxBytes atomic.Int64
  266. localRxPackets atomic.Int64
  267. localLostPackets atomic.Int64
  268. serverRxBytes atomic.Int64
  269. serverRxPackets atomic.Int64
  270. serverLostPackets atomic.Int64
  271. curJitterMs atomic.Uint64 // float64 in bits
  272. jitterMu sync.Mutex
  273. jitterCalc JitterCalculator
  274. rxTracker udpTracker
  275. )
  276. sessionHash := HashSessionID(sessionID)
  277. udpPacing := newUDPPacer(req.BandwidthLimit)
  278. var serverUDPAddr *net.UDPAddr
  279. // One reader owns the buffered connection, including bytes prefetched
  280. // during the handshake. It dispatches data frames and feedback reports.
  281. go func() {
  282. buf := make([]byte, tcpFrameMax)
  283. for {
  284. var line []byte
  285. if req.Protocol == "tcp" {
  286. kind, payload, err := readTCPFrame(reader, buf)
  287. if err != nil {
  288. return
  289. }
  290. if kind == tcpFrameData {
  291. localRxBytes.Add(int64(len(payload)))
  292. localRxPackets.Add(1)
  293. continue
  294. }
  295. line = payload
  296. } else {
  297. var err error
  298. line, err = reader.ReadBytes('\n')
  299. if err != nil {
  300. return
  301. }
  302. }
  303. var rep SessionHandshake
  304. if err := json.Unmarshal(line, &rep); err == nil && rep.Type == "report" {
  305. serverRxBytes.Store(rep.RxBytes)
  306. serverRxPackets.Store(rep.RxPackets)
  307. serverLostPackets.Store(rep.LostPackets)
  308. udpPacing.observe(rep.RxBytes, rep.RxPackets, rep.LostPackets, time.Now())
  309. }
  310. }
  311. }()
  312. var wg sync.WaitGroup
  313. if req.Protocol == "udp" {
  314. defer func() {
  315. c.mu.Lock()
  316. defer c.mu.Unlock()
  317. if c.status.Error != "" || time.Since(c.status.StartTime) < time.Second {
  318. return
  319. }
  320. if req.Direction != "tx" && localRxPackets.Load() == 0 {
  321. c.status.Error = "No UDP data received; check UDP connectivity and firewall rules"
  322. } else if req.Direction != "rx" && serverRxPackets.Load() == 0 && serverRxBytes.Load() == 0 {
  323. c.status.Error = "No UDP delivery confirmed by the receiver; check UDP connectivity and firewall rules"
  324. }
  325. }()
  326. }
  327. // UDP Data path
  328. if req.Protocol == "udp" {
  329. serverUDPAddr, err = net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", req.Target, serverUDPPort))
  330. if err != nil {
  331. c.mu.Lock()
  332. c.status.Error = fmt.Sprintf("Failed to resolve server UDP address: %v", err)
  333. c.mu.Unlock()
  334. return
  335. }
  336. // RX-only also sends a probe, so the server learns the real UDP tuple.
  337. if _, err := udpConn.WriteToUDP(udpProbe(sessionHash), serverUDPAddr); err != nil {
  338. c.mu.Lock()
  339. c.status.Error = err.Error()
  340. c.mu.Unlock()
  341. return
  342. }
  343. // Transmitter (TX or Both)
  344. if req.Direction == "tx" || req.Direction == "both" {
  345. for s := 0; s < req.Streams; s++ {
  346. wg.Add(1)
  347. streamID := uint8(s)
  348. go func() {
  349. defer wg.Done()
  350. buf := make([]byte, req.PacketSize)
  351. if _, err := crand.Read(buf); err != nil {
  352. return
  353. }
  354. var seq uint64
  355. hdr := PacketHeader{
  356. Magic: MagicBytes,
  357. Version: ProtoVer,
  358. PacketType: PacketTypeData,
  359. StreamID: streamID,
  360. SessionHash: sessionHash,
  361. PayloadLength: uint32(req.PacketSize - HeaderSize),
  362. }
  363. for {
  364. select {
  365. case <-ctx.Done():
  366. return
  367. default:
  368. }
  369. if !udpPacing.wait(ctx, req.PacketSize) {
  370. return
  371. }
  372. seq++
  373. hdr.Sequence = seq
  374. hdr.TimestampNano = time.Now().UnixNano()
  375. EncodeHeader(buf, hdr)
  376. n, err := udpConn.WriteToUDP(buf, serverUDPAddr)
  377. if err == nil {
  378. localTxBytes.Add(int64(n))
  379. localTxPackets.Add(1)
  380. }
  381. }
  382. }()
  383. }
  384. }
  385. // Receiver (RX or Both)
  386. if req.Direction == "rx" || req.Direction == "both" {
  387. wg.Add(1)
  388. go func() {
  389. defer wg.Done()
  390. rxBuf := make([]byte, 65535)
  391. for {
  392. select {
  393. case <-ctx.Done():
  394. return
  395. default:
  396. }
  397. n, _, err := udpConn.ReadFromUDP(rxBuf)
  398. if err != nil {
  399. select {
  400. case <-ctx.Done():
  401. return
  402. default:
  403. time.Sleep(10 * time.Millisecond)
  404. continue
  405. }
  406. }
  407. if n < HeaderSize {
  408. continue
  409. }
  410. hdr, err := DecodeHeader(rxBuf[:n])
  411. if err != nil || hdr.SessionHash != sessionHash || hdr.Version != ProtoVer || hdr.PacketType != PacketTypeData || int(hdr.PayloadLength) != n-HeaderSize {
  412. continue
  413. }
  414. accepted, lost := rxTracker.accept(hdr.StreamID, hdr.Sequence)
  415. localLostPackets.Store(lost)
  416. if !accepted {
  417. continue
  418. }
  419. nowNano := time.Now().UnixNano()
  420. localRxBytes.Add(int64(n))
  421. localRxPackets.Add(1)
  422. // Jitter calculation
  423. jitterMu.Lock()
  424. j := jitterCalc.Update(hdr.TimestampNano, nowNano)
  425. jitterMu.Unlock()
  426. curJitterMs.Store(uint64(j * 1000)) // ms * 1000
  427. }
  428. }()
  429. }
  430. } else if req.Protocol == "tcp" {
  431. // TCP Data path
  432. if req.Direction == "tx" || req.Direction == "both" {
  433. wg.Add(1)
  434. go func() {
  435. defer wg.Done()
  436. chunk := make([]byte, 64*1024)
  437. if _, err := crand.Read(chunk); err != nil {
  438. return
  439. }
  440. limiter := NewRateLimiter(req.BandwidthLimit)
  441. for {
  442. select {
  443. case <-ctx.Done():
  444. return
  445. default:
  446. }
  447. if limiter != nil {
  448. if !limiter.WaitContext(ctx, len(chunk)) {
  449. return
  450. }
  451. }
  452. n, err := tcpWriter.write(tcpFrameData, chunk)
  453. if err != nil {
  454. return
  455. }
  456. localTxBytes.Add(int64(n))
  457. localTxPackets.Add(1)
  458. }
  459. }()
  460. }
  461. }
  462. // Telemetry sampling loop (every 500ms)
  463. sampleTicker := time.NewTicker(500 * time.Millisecond)
  464. defer sampleTicker.Stop()
  465. var lastTxBytes, lastRxBytes, lastOfferedBytes int64
  466. lastTime := time.Now()
  467. startTime := lastTime
  468. timeoutChan := make(<-chan time.Time)
  469. if req.Duration > 0 {
  470. timer := time.NewTimer(time.Duration(req.Duration) * time.Second)
  471. defer timer.Stop()
  472. timeoutChan = timer.C
  473. }
  474. for {
  475. select {
  476. case <-ctx.Done():
  477. if req.Protocol == "tcp" {
  478. return
  479. }
  480. // Notify remote server to stop
  481. stopMsg := SessionHandshake{Type: "stop", SessionID: sessionID}
  482. sb, _ := json.Marshal(stopMsg)
  483. _, _ = controlConn.Write(append(sb, '\n'))
  484. return
  485. case <-timeoutChan:
  486. if req.Protocol == "tcp" {
  487. return
  488. }
  489. stopMsg := SessionHandshake{Type: "stop", SessionID: sessionID}
  490. sb, _ := json.Marshal(stopMsg)
  491. _, _ = controlConn.Write(append(sb, '\n'))
  492. return
  493. case now := <-sampleTicker.C:
  494. intervalSec := now.Sub(lastTime).Seconds()
  495. if intervalSec <= 0 {
  496. intervalSec = 0.5
  497. }
  498. lastTime = now
  499. elapsedSec := now.Sub(startTime).Seconds()
  500. var curTxBytes, curRxBytes int64
  501. var curLostPackets int64
  502. if req.Direction != "rx" {
  503. curTxBytes = serverRxBytes.Load()
  504. curLostPackets += serverLostPackets.Load()
  505. }
  506. if req.Direction != "tx" {
  507. curRxBytes = localRxBytes.Load()
  508. curLostPackets += localLostPackets.Load()
  509. }
  510. offered := localTxBytes.Load()
  511. offeredMbps := float64((offered-lastOfferedBytes)*8) / intervalSec / 1e6
  512. lastOfferedBytes = offered
  513. if req.Protocol == "udp" && req.Direction != "tx" {
  514. rep := SessionHandshake{Type: "report", SessionID: sessionID, RxBytes: localRxBytes.Load(), RxPackets: localRxPackets.Load(), LostPackets: localLostPackets.Load()}
  515. data, _ := json.Marshal(rep)
  516. _ = controlConn.SetWriteDeadline(time.Now().Add(2 * time.Second))
  517. if _, err := controlConn.Write(append(data, '\n')); err != nil {
  518. c.mu.Lock()
  519. c.status.Error = fmt.Sprintf("Receiver feedback failed: %v", err)
  520. c.mu.Unlock()
  521. return
  522. }
  523. // Retry discovery while no reverse packet has arrived.
  524. if localRxPackets.Load() == 0 {
  525. _, _ = udpConn.WriteToUDP(udpProbe(sessionHash), serverUDPAddr)
  526. }
  527. }
  528. deltaTx := curTxBytes - lastTxBytes
  529. deltaRx := curRxBytes - lastRxBytes
  530. lastTxBytes = curTxBytes
  531. lastRxBytes = curRxBytes
  532. txBps := int64(float64(deltaTx*8) / intervalSec)
  533. rxBps := int64(float64(deltaRx*8) / intervalSec)
  534. txMbps := float64(txBps) / 1e6
  535. rxMbps := float64(rxBps) / 1e6
  536. // Total packets & loss %
  537. totalExpectedPkts := int64(0)
  538. if req.Direction != "tx" {
  539. totalExpectedPkts += localRxPackets.Load() + localLostPackets.Load()
  540. }
  541. if req.Direction != "rx" {
  542. totalExpectedPkts += serverRxPackets.Load() + serverLostPackets.Load()
  543. }
  544. var lossPct float64
  545. if totalExpectedPkts > 0 {
  546. lossPct = (float64(curLostPackets) / float64(totalExpectedPkts)) * 100.0
  547. if lossPct > 100.0 {
  548. lossPct = 100.0
  549. }
  550. }
  551. jitter := float64(curJitterMs.Load()) / 1000.0
  552. sample := &LiveSample{
  553. PacketSize: req.PacketSize,
  554. OfferedTxMbps: offeredMbps, OfferedTxBytes: offered,
  555. Timestamp: now,
  556. ElapsedSec: elapsedSec,
  557. TxBps: txBps,
  558. RxBps: rxBps,
  559. TxMbps: txMbps,
  560. RxMbps: rxMbps,
  561. TxPackets: serverRxPackets.Load(),
  562. RxPackets: localRxPackets.Load(),
  563. TxBytes: curTxBytes,
  564. RxBytes: curRxBytes,
  565. LostPackets: curLostPackets,
  566. LossPct: lossPct,
  567. JitterMs: jitter,
  568. }
  569. c.mu.Lock()
  570. c.recordSampleLocked(sample)
  571. c.mu.Unlock()
  572. }
  573. }
  574. }
  575. func (c *BwTestClient) finalizeSummary() {
  576. if len(c.samples) == 0 {
  577. return
  578. }
  579. last := c.samples[len(c.samples)-1]
  580. duration := last.ElapsedSec
  581. var avgTx, avgRx, avgLoss, avgJitter float64
  582. if duration > 0 {
  583. avgTx = float64(last.TxBytes) * 8 / duration / 1e6
  584. avgRx = float64(last.RxBytes) * 8 / duration / 1e6
  585. }
  586. expected := last.TxPackets + last.RxPackets + last.LostPackets
  587. if expected > 0 {
  588. avgLoss = float64(last.LostPackets) * 100 / float64(expected)
  589. }
  590. if c.sampleCount > 0 {
  591. avgJitter = c.jitterSum / float64(c.sampleCount)
  592. }
  593. c.status.Summary = &TestSummary{
  594. DurationSec: duration,
  595. StartTime: c.status.StartTime,
  596. EndTime: time.Now(),
  597. TotalTxBytes: last.TxBytes,
  598. TotalRxBytes: last.RxBytes,
  599. AvgTxMbps: avgTx,
  600. AvgRxMbps: avgRx,
  601. PeakTxMbps: c.peakTx,
  602. PeakRxMbps: c.peakRx,
  603. TotalTxPackets: last.TxPackets,
  604. TotalRxPackets: last.RxPackets,
  605. TotalLostPackets: last.LostPackets,
  606. AvgLossPct: avgLoss,
  607. AvgJitterMs: avgJitter,
  608. }
  609. }