client.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  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, time.Now())
  309. }
  310. }
  311. }()
  312. var wg sync.WaitGroup
  313. // UDP Data path
  314. if req.Protocol == "udp" {
  315. serverUDPAddr, err = net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", req.Target, serverUDPPort))
  316. if err != nil {
  317. c.mu.Lock()
  318. c.status.Error = fmt.Sprintf("Failed to resolve server UDP address: %v", err)
  319. c.mu.Unlock()
  320. return
  321. }
  322. // RX-only also sends a probe, so the server learns the real UDP tuple.
  323. if _, err := udpConn.WriteToUDP(udpProbe(sessionHash), serverUDPAddr); err != nil {
  324. c.mu.Lock()
  325. c.status.Error = err.Error()
  326. c.mu.Unlock()
  327. return
  328. }
  329. // Transmitter (TX or Both)
  330. if req.Direction == "tx" || req.Direction == "both" {
  331. for s := 0; s < req.Streams; s++ {
  332. wg.Add(1)
  333. streamID := uint8(s)
  334. go func() {
  335. defer wg.Done()
  336. buf := make([]byte, req.PacketSize)
  337. if _, err := crand.Read(buf); err != nil {
  338. return
  339. }
  340. var seq uint64
  341. hdr := PacketHeader{
  342. Magic: MagicBytes,
  343. Version: ProtoVer,
  344. PacketType: PacketTypeData,
  345. StreamID: streamID,
  346. SessionHash: sessionHash,
  347. PayloadLength: uint32(req.PacketSize - HeaderSize),
  348. }
  349. for {
  350. select {
  351. case <-ctx.Done():
  352. return
  353. default:
  354. }
  355. if !udpPacing.wait(ctx, req.PacketSize) {
  356. return
  357. }
  358. seq++
  359. hdr.Sequence = seq
  360. hdr.TimestampNano = time.Now().UnixNano()
  361. EncodeHeader(buf, hdr)
  362. n, err := udpConn.WriteToUDP(buf, serverUDPAddr)
  363. if err == nil {
  364. localTxBytes.Add(int64(n))
  365. localTxPackets.Add(1)
  366. }
  367. }
  368. }()
  369. }
  370. }
  371. // Receiver (RX or Both)
  372. if req.Direction == "rx" || req.Direction == "both" {
  373. wg.Add(1)
  374. go func() {
  375. defer wg.Done()
  376. rxBuf := make([]byte, 65535)
  377. for {
  378. select {
  379. case <-ctx.Done():
  380. return
  381. default:
  382. }
  383. n, _, err := udpConn.ReadFromUDP(rxBuf)
  384. if err != nil {
  385. select {
  386. case <-ctx.Done():
  387. return
  388. default:
  389. time.Sleep(10 * time.Millisecond)
  390. continue
  391. }
  392. }
  393. if n < HeaderSize {
  394. continue
  395. }
  396. hdr, err := DecodeHeader(rxBuf[:n])
  397. if err != nil || hdr.SessionHash != sessionHash || hdr.Version != ProtoVer || hdr.PacketType != PacketTypeData || int(hdr.PayloadLength) != n-HeaderSize {
  398. continue
  399. }
  400. accepted, lost := rxTracker.accept(hdr.StreamID, hdr.Sequence)
  401. localLostPackets.Store(lost)
  402. if !accepted {
  403. continue
  404. }
  405. nowNano := time.Now().UnixNano()
  406. localRxBytes.Add(int64(n))
  407. localRxPackets.Add(1)
  408. // Jitter calculation
  409. jitterMu.Lock()
  410. j := jitterCalc.Update(hdr.TimestampNano, nowNano)
  411. jitterMu.Unlock()
  412. curJitterMs.Store(uint64(j * 1000)) // ms * 1000
  413. }
  414. }()
  415. }
  416. } else if req.Protocol == "tcp" {
  417. // TCP Data path
  418. if req.Direction == "tx" || req.Direction == "both" {
  419. wg.Add(1)
  420. go func() {
  421. defer wg.Done()
  422. chunk := make([]byte, 64*1024)
  423. if _, err := crand.Read(chunk); err != nil {
  424. return
  425. }
  426. limiter := NewRateLimiter(req.BandwidthLimit)
  427. for {
  428. select {
  429. case <-ctx.Done():
  430. return
  431. default:
  432. }
  433. if limiter != nil {
  434. if !limiter.WaitContext(ctx, len(chunk)) {
  435. return
  436. }
  437. }
  438. n, err := tcpWriter.write(tcpFrameData, chunk)
  439. if err != nil {
  440. return
  441. }
  442. localTxBytes.Add(int64(n))
  443. localTxPackets.Add(1)
  444. }
  445. }()
  446. }
  447. }
  448. // Telemetry sampling loop (every 500ms)
  449. sampleTicker := time.NewTicker(500 * time.Millisecond)
  450. defer sampleTicker.Stop()
  451. var lastTxBytes, lastRxBytes, lastOfferedBytes int64
  452. lastTime := time.Now()
  453. startTime := lastTime
  454. timeoutChan := make(<-chan time.Time)
  455. if req.Duration > 0 {
  456. timer := time.NewTimer(time.Duration(req.Duration) * time.Second)
  457. defer timer.Stop()
  458. timeoutChan = timer.C
  459. }
  460. for {
  461. select {
  462. case <-ctx.Done():
  463. if req.Protocol == "tcp" {
  464. return
  465. }
  466. // Notify remote server to stop
  467. stopMsg := SessionHandshake{Type: "stop", SessionID: sessionID}
  468. sb, _ := json.Marshal(stopMsg)
  469. _, _ = controlConn.Write(append(sb, '\n'))
  470. return
  471. case <-timeoutChan:
  472. if req.Protocol == "tcp" {
  473. return
  474. }
  475. stopMsg := SessionHandshake{Type: "stop", SessionID: sessionID}
  476. sb, _ := json.Marshal(stopMsg)
  477. _, _ = controlConn.Write(append(sb, '\n'))
  478. return
  479. case now := <-sampleTicker.C:
  480. intervalSec := now.Sub(lastTime).Seconds()
  481. if intervalSec <= 0 {
  482. intervalSec = 0.5
  483. }
  484. lastTime = now
  485. elapsedSec := now.Sub(startTime).Seconds()
  486. var curTxBytes, curRxBytes int64
  487. var curLostPackets int64
  488. if req.Direction != "rx" {
  489. curTxBytes = serverRxBytes.Load()
  490. curLostPackets += serverLostPackets.Load()
  491. }
  492. if req.Direction != "tx" {
  493. curRxBytes = localRxBytes.Load()
  494. curLostPackets += localLostPackets.Load()
  495. }
  496. offered := localTxBytes.Load()
  497. offeredMbps := float64((offered-lastOfferedBytes)*8) / intervalSec / 1e6
  498. lastOfferedBytes = offered
  499. if req.Protocol == "udp" && req.Direction != "tx" {
  500. rep := SessionHandshake{Type: "report", SessionID: sessionID, RxBytes: localRxBytes.Load(), RxPackets: localRxPackets.Load(), LostPackets: localLostPackets.Load()}
  501. data, _ := json.Marshal(rep)
  502. _ = controlConn.SetWriteDeadline(time.Now().Add(2 * time.Second))
  503. if _, err := controlConn.Write(append(data, '\n')); err != nil {
  504. c.mu.Lock()
  505. c.status.Error = fmt.Sprintf("Receiver feedback failed: %v", err)
  506. c.mu.Unlock()
  507. return
  508. }
  509. // Retry discovery while no reverse packet has arrived.
  510. if localRxPackets.Load() == 0 {
  511. _, _ = udpConn.WriteToUDP(udpProbe(sessionHash), serverUDPAddr)
  512. }
  513. }
  514. deltaTx := curTxBytes - lastTxBytes
  515. deltaRx := curRxBytes - lastRxBytes
  516. lastTxBytes = curTxBytes
  517. lastRxBytes = curRxBytes
  518. txBps := int64(float64(deltaTx*8) / intervalSec)
  519. rxBps := int64(float64(deltaRx*8) / intervalSec)
  520. txMbps := float64(txBps) / 1e6
  521. rxMbps := float64(rxBps) / 1e6
  522. // Total packets & loss %
  523. totalExpectedPkts := int64(0)
  524. if req.Direction != "tx" {
  525. totalExpectedPkts += localRxPackets.Load() + localLostPackets.Load()
  526. }
  527. if req.Direction != "rx" {
  528. totalExpectedPkts += serverRxPackets.Load() + serverLostPackets.Load()
  529. }
  530. var lossPct float64
  531. if totalExpectedPkts > 0 {
  532. lossPct = (float64(curLostPackets) / float64(totalExpectedPkts)) * 100.0
  533. if lossPct > 100.0 {
  534. lossPct = 100.0
  535. }
  536. }
  537. jitter := float64(curJitterMs.Load()) / 1000.0
  538. sample := &LiveSample{
  539. PacketSize: req.PacketSize,
  540. OfferedTxMbps: offeredMbps, OfferedTxBytes: offered,
  541. Timestamp: now,
  542. ElapsedSec: elapsedSec,
  543. TxBps: txBps,
  544. RxBps: rxBps,
  545. TxMbps: txMbps,
  546. RxMbps: rxMbps,
  547. TxPackets: serverRxPackets.Load(),
  548. RxPackets: localRxPackets.Load(),
  549. TxBytes: curTxBytes,
  550. RxBytes: curRxBytes,
  551. LostPackets: curLostPackets,
  552. LossPct: lossPct,
  553. JitterMs: jitter,
  554. }
  555. c.mu.Lock()
  556. c.recordSampleLocked(sample)
  557. c.mu.Unlock()
  558. }
  559. }
  560. }
  561. func (c *BwTestClient) finalizeSummary() {
  562. if len(c.samples) == 0 {
  563. return
  564. }
  565. last := c.samples[len(c.samples)-1]
  566. duration := last.ElapsedSec
  567. var avgTx, avgRx, avgLoss, avgJitter float64
  568. if duration > 0 {
  569. avgTx = float64(last.TxBytes) * 8 / duration / 1e6
  570. avgRx = float64(last.RxBytes) * 8 / duration / 1e6
  571. }
  572. expected := last.TxPackets + last.RxPackets + last.LostPackets
  573. if expected > 0 {
  574. avgLoss = float64(last.LostPackets) * 100 / float64(expected)
  575. }
  576. if c.sampleCount > 0 {
  577. avgJitter = c.jitterSum / float64(c.sampleCount)
  578. }
  579. c.status.Summary = &TestSummary{
  580. DurationSec: duration,
  581. StartTime: c.status.StartTime,
  582. EndTime: time.Now(),
  583. TotalTxBytes: last.TxBytes,
  584. TotalRxBytes: last.RxBytes,
  585. AvgTxMbps: avgTx,
  586. AvgRxMbps: avgRx,
  587. PeakTxMbps: c.peakTx,
  588. PeakRxMbps: c.peakRx,
  589. TotalTxPackets: last.TxPackets,
  590. TotalRxPackets: last.RxPackets,
  591. TotalLostPackets: last.LostPackets,
  592. AvgLossPct: avgLoss,
  593. AvgJitterMs: avgJitter,
  594. }
  595. }