server.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. package bwtest
  2. import (
  3. "bufio"
  4. "context"
  5. crand "crypto/rand"
  6. "encoding/json"
  7. "fmt"
  8. "log"
  9. "math/rand"
  10. "net"
  11. "sync"
  12. "sync/atomic"
  13. "time"
  14. )
  15. // ServerSession tracks an active incoming bandwidth test session on the server
  16. type serverSession struct {
  17. id string
  18. sessionHash uint64
  19. clientTCP net.Conn
  20. tcpWriter *tcpFrameWriter
  21. clientAddr net.IP
  22. clientPort int
  23. peerReady chan struct{}
  24. peerOnce sync.Once
  25. udpPacing *udpPacer
  26. rxTracker udpTracker
  27. handshake SessionHandshake
  28. cancel context.CancelFunc
  29. ctx context.Context
  30. // Statistics
  31. rxBytes atomic.Int64
  32. rxPackets atomic.Int64
  33. txBytes atomic.Int64
  34. txPackets atomic.Int64
  35. lostPackets atomic.Int64
  36. jitterMs atomic.Uint64 // float64 bits
  37. mu sync.Mutex
  38. jitter JitterCalculator
  39. }
  40. // BwTestServer represents the local Bandwidth Test server
  41. type BwTestServer struct {
  42. config ServerConfig
  43. mu sync.RWMutex
  44. tcpListener net.Listener
  45. udpConn *net.UDPConn
  46. running bool
  47. cancel context.CancelFunc
  48. sessions map[string]*serverSession
  49. sessMu sync.RWMutex
  50. totalServed atomic.Int64
  51. totalBytes atomic.Int64
  52. }
  53. // NewServer creates a new BwTestServer instance
  54. func NewServer(cfg ServerConfig) *BwTestServer {
  55. if cfg.Port <= 0 {
  56. cfg.Port = 2000
  57. }
  58. if cfg.ListenAddr == "" {
  59. cfg.ListenAddr = "0.0.0.0"
  60. }
  61. return &BwTestServer{
  62. config: cfg,
  63. sessions: make(map[string]*serverSession),
  64. }
  65. }
  66. // Start launches the TCP control listener and UDP data listener
  67. func (s *BwTestServer) Start() error {
  68. s.mu.Lock()
  69. defer s.mu.Unlock()
  70. if s.running {
  71. return nil
  72. }
  73. ctx, cancel := context.WithCancel(context.Background())
  74. s.cancel = cancel
  75. tcpAddr := fmt.Sprintf("%s:%d", s.config.ListenAddr, s.config.Port)
  76. ln, err := net.Listen("tcp", tcpAddr)
  77. if err != nil {
  78. cancel()
  79. return fmt.Errorf("failed to bind BwTest TCP listener on %s: %w", tcpAddr, err)
  80. }
  81. s.tcpListener = ln
  82. udpAddr, err := net.ResolveUDPAddr("udp", tcpAddr)
  83. if err != nil {
  84. ln.Close()
  85. cancel()
  86. return fmt.Errorf("failed to resolve BwTest UDP address: %w", err)
  87. }
  88. uconn, err := net.ListenUDP("udp", udpAddr)
  89. if err != nil {
  90. ln.Close()
  91. cancel()
  92. return fmt.Errorf("failed to bind BwTest UDP listener on %s: %w", tcpAddr, err)
  93. }
  94. _ = uconn.SetReadBuffer(8 * 1024 * 1024) // 8MB OS UDP socket buffer
  95. s.udpConn = uconn
  96. s.running = true
  97. go s.acceptLoop(ctx)
  98. go s.udpReceiverLoop(ctx)
  99. log.Printf("[BwTestServer] Started Bandwidth Test Server listening on %s (TCP & UDP)", tcpAddr)
  100. return nil
  101. }
  102. // Stop terminates the server and all active sessions
  103. func (s *BwTestServer) Stop() {
  104. s.mu.Lock()
  105. if !s.running {
  106. s.mu.Unlock()
  107. return
  108. }
  109. s.running = false
  110. if s.cancel != nil {
  111. s.cancel()
  112. }
  113. if s.tcpListener != nil {
  114. _ = s.tcpListener.Close()
  115. }
  116. if s.udpConn != nil {
  117. _ = s.udpConn.Close()
  118. }
  119. s.mu.Unlock()
  120. s.sessMu.Lock()
  121. for _, sess := range s.sessions {
  122. if sess.cancel != nil {
  123. sess.cancel()
  124. }
  125. if sess.clientTCP != nil {
  126. _ = sess.clientTCP.Close()
  127. }
  128. }
  129. s.sessions = make(map[string]*serverSession)
  130. s.sessMu.Unlock()
  131. log.Printf("[BwTestServer] Bandwidth Test Server stopped")
  132. }
  133. // Status returns current operational metrics
  134. func (s *BwTestServer) Status() ServerStatus {
  135. s.mu.RLock()
  136. defer s.mu.RUnlock()
  137. s.sessMu.RLock()
  138. activeCount := len(s.sessions)
  139. s.sessMu.RUnlock()
  140. return ServerStatus{
  141. Enabled: s.config.Enabled,
  142. Port: s.config.Port,
  143. Running: s.running,
  144. ActiveSessions: activeCount,
  145. TotalSessionsServed: s.totalServed.Load(),
  146. TotalBytesProcessed: s.totalBytes.Load(),
  147. }
  148. }
  149. // SetPort updates the port and restarts the server if currently running
  150. func (s *BwTestServer) SetPort(port int) error {
  151. s.mu.Lock()
  152. wasRunning := s.running
  153. s.config.Port = port
  154. s.mu.Unlock()
  155. if wasRunning {
  156. s.Stop()
  157. return s.Start()
  158. }
  159. return nil
  160. }
  161. func (s *BwTestServer) acceptLoop(ctx context.Context) {
  162. for {
  163. conn, err := s.tcpListener.Accept()
  164. if err != nil {
  165. select {
  166. case <-ctx.Done():
  167. return
  168. default:
  169. time.Sleep(50 * time.Millisecond)
  170. continue
  171. }
  172. }
  173. go s.handleClientControl(ctx, conn)
  174. }
  175. }
  176. func (s *BwTestServer) handleClientControl(serverCtx context.Context, conn net.Conn) {
  177. defer conn.Close()
  178. _ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
  179. remoteTCPAddr, _ := conn.RemoteAddr().(*net.TCPAddr)
  180. clientIP := net.IPv4(127, 0, 0, 1)
  181. if remoteTCPAddr != nil {
  182. clientIP = remoteTCPAddr.IP
  183. }
  184. reader := bufio.NewReader(conn)
  185. line, err := reader.ReadBytes('\n')
  186. if err != nil {
  187. return
  188. }
  189. var req SessionHandshake
  190. if err := json.Unmarshal(line, &req); err != nil {
  191. resp := SessionHandshake{Type: "reject", Message: "Invalid handshake JSON"}
  192. b, _ := json.Marshal(resp)
  193. _, _ = conn.Write(append(b, '\n'))
  194. return
  195. }
  196. if req.Type != "request" {
  197. return
  198. }
  199. if req.Protocol == "tcp" && req.DataFraming != tcpFramingVersion {
  200. b, _ := json.Marshal(SessionHandshake{Type: "reject", Message: "TCP test requires framed protocol; upgrade both peers"})
  201. _, _ = conn.Write(append(b, '\n'))
  202. return
  203. }
  204. if req.Protocol == "udp" && req.BandwidthLimit <= 0 && req.UDPFeedback != udpFeedbackVersion {
  205. b, _ := json.Marshal(SessionHandshake{Type: "reject", Message: "Automatic UDP requires receiver feedback; upgrade both peers"})
  206. _, _ = conn.Write(append(b, '\n'))
  207. return
  208. }
  209. if req.Protocol == "udp" {
  210. if req.PacketSize <= 0 {
  211. req.PacketSize = 1472
  212. }
  213. size, err := udpPayloadSize(req.PacketSize, clientIP)
  214. if err != nil {
  215. return
  216. }
  217. req.PacketSize = size
  218. }
  219. _ = conn.SetReadDeadline(time.Time{})
  220. // Session parameter sanitization
  221. if req.SessionID == "" {
  222. req.SessionID = fmt.Sprintf("bw-%d-%d", time.Now().UnixNano(), rand.Intn(10000))
  223. }
  224. if req.PacketSize <= 0 {
  225. req.PacketSize = 1472
  226. }
  227. if req.PacketSize < 64 {
  228. req.PacketSize = 64
  229. }
  230. if req.PacketSize > 65507 {
  231. req.PacketSize = 65507
  232. }
  233. if req.Streams <= 0 {
  234. req.Streams = 1
  235. }
  236. if req.Streams > 8 {
  237. req.Streams = 8
  238. }
  239. if req.Duration < 0 {
  240. req.Duration = 10
  241. }
  242. var sessCtx context.Context
  243. var sessCancel context.CancelFunc
  244. if req.Duration > 0 {
  245. sessCtx, sessCancel = context.WithTimeout(serverCtx, time.Duration(req.Duration+2)*time.Second)
  246. } else {
  247. sessCtx, sessCancel = context.WithCancel(serverCtx)
  248. }
  249. defer sessCancel()
  250. stopClose := context.AfterFunc(sessCtx, func() { conn.Close() })
  251. defer stopClose()
  252. sess := &serverSession{
  253. id: req.SessionID,
  254. sessionHash: HashSessionID(req.SessionID),
  255. clientTCP: conn,
  256. tcpWriter: &tcpFrameWriter{conn: conn},
  257. clientAddr: clientIP,
  258. clientPort: req.UDPPort,
  259. peerReady: make(chan struct{}),
  260. udpPacing: newUDPPacer(req.BandwidthLimit),
  261. handshake: req,
  262. ctx: sessCtx,
  263. cancel: sessCancel,
  264. }
  265. s.sessMu.Lock()
  266. s.sessions[sess.id] = sess
  267. s.sessMu.Unlock()
  268. s.totalServed.Add(1)
  269. defer func() {
  270. s.sessMu.Lock()
  271. delete(s.sessions, sess.id)
  272. s.sessMu.Unlock()
  273. }()
  274. // Respond with accept
  275. s.mu.RLock()
  276. listenPort := s.config.Port
  277. s.mu.RUnlock()
  278. acceptResp := SessionHandshake{
  279. Type: "accept",
  280. SessionID: sess.id,
  281. UDPPort: listenPort,
  282. Protocol: req.Protocol,
  283. Direction: req.Direction,
  284. Message: "Ready",
  285. }
  286. if req.Protocol == "tcp" {
  287. acceptResp.DataFraming = tcpFramingVersion
  288. } else {
  289. acceptResp.UDPFeedback = udpFeedbackVersion
  290. acceptResp.PacketSize = req.PacketSize
  291. }
  292. acceptBytes, _ := json.Marshal(acceptResp)
  293. if _, err := conn.Write(append(acceptBytes, '\n')); err != nil {
  294. return
  295. }
  296. // Launch worker routines based on direction and protocol
  297. if req.Protocol == "udp" {
  298. // If client is receiving (server transmitting)
  299. if req.Direction == "rx" || req.Direction == "both" {
  300. go s.runServerUDPTx(sessCtx, sess)
  301. }
  302. } else if req.Protocol == "tcp" {
  303. if req.Direction == "rx" || req.Direction == "both" {
  304. go s.runServerTCPTx(sessCtx, sess)
  305. }
  306. }
  307. // Read control messages / reports from client until termination
  308. go s.reportLoop(sessCtx, sess)
  309. frameBuf := make([]byte, tcpFrameMax)
  310. for {
  311. var line []byte
  312. if req.Protocol == "tcp" {
  313. kind, payload, err := readTCPFrame(reader, frameBuf)
  314. if err != nil {
  315. break
  316. }
  317. if kind == tcpFrameData {
  318. sess.rxBytes.Add(int64(len(payload)))
  319. sess.rxPackets.Add(1)
  320. s.totalBytes.Add(int64(len(payload)))
  321. continue
  322. }
  323. line = payload
  324. } else {
  325. var err error
  326. line, err = reader.ReadBytes('\n')
  327. if err != nil {
  328. break
  329. }
  330. }
  331. var msg SessionHandshake
  332. if err := json.Unmarshal(line, &msg); err == nil {
  333. if msg.Type == "stop" {
  334. break
  335. }
  336. if msg.Type == "report" && msg.SessionID == sess.id {
  337. sess.udpPacing.observe(msg.RxBytes, msg.RxPackets, msg.LostPackets, time.Now())
  338. }
  339. }
  340. }
  341. }
  342. // udpReceiverLoop processes incoming UDP test packets across all active sessions
  343. func (s *BwTestServer) udpReceiverLoop(ctx context.Context) {
  344. buf := make([]byte, 65535)
  345. for {
  346. select {
  347. case <-ctx.Done():
  348. return
  349. default:
  350. }
  351. n, remoteAddr, err := s.udpConn.ReadFromUDP(buf)
  352. if err != nil {
  353. select {
  354. case <-ctx.Done():
  355. return
  356. default:
  357. time.Sleep(10 * time.Millisecond)
  358. continue
  359. }
  360. }
  361. if n < HeaderSize {
  362. continue
  363. }
  364. hdr, err := DecodeHeader(buf[:n])
  365. if err != nil {
  366. continue
  367. }
  368. // Find matching session
  369. s.sessMu.RLock()
  370. var matched *serverSession
  371. for _, sess := range s.sessions {
  372. if sess.sessionHash == hdr.SessionHash {
  373. matched = sess
  374. break
  375. }
  376. }
  377. s.sessMu.RUnlock()
  378. if matched == nil {
  379. continue
  380. }
  381. // Learn the actual source port even for RX-only clients behind NAT.
  382. // Restrict discovery to the host that owns this TCP control session.
  383. if !remoteAddr.IP.Equal(matched.clientAddr) {
  384. continue
  385. }
  386. if hdr.Version != ProtoVer {
  387. continue
  388. }
  389. if hdr.PacketType == udpProbeType {
  390. matched.mu.Lock()
  391. matched.clientPort = remoteAddr.Port
  392. matched.mu.Unlock()
  393. matched.peerOnce.Do(func() { close(matched.peerReady) })
  394. continue
  395. }
  396. if hdr.PacketType != PacketTypeData || int(hdr.PayloadLength) != n-HeaderSize {
  397. continue
  398. }
  399. accepted, lost := matched.rxTracker.accept(hdr.StreamID, hdr.Sequence)
  400. matched.lostPackets.Store(lost)
  401. if !accepted {
  402. continue
  403. }
  404. matched.mu.Lock()
  405. matched.clientPort = remoteAddr.Port
  406. matched.mu.Unlock()
  407. matched.peerOnce.Do(func() { close(matched.peerReady) })
  408. nowNano := time.Now().UnixNano()
  409. matched.rxBytes.Add(int64(n))
  410. matched.rxPackets.Add(1)
  411. s.totalBytes.Add(int64(n))
  412. // Jitter calculation
  413. matched.mu.Lock()
  414. curJitter := matched.jitter.Update(hdr.TimestampNano, nowNano)
  415. matched.mu.Unlock()
  416. _ = curJitter
  417. }
  418. }
  419. // runServerUDPTx sends UDP packets from server to client (when client requested RX or Both)
  420. func (s *BwTestServer) runServerUDPTx(ctx context.Context, sess *serverSession) {
  421. if sess.handshake.UDPFeedback == udpFeedbackVersion {
  422. select {
  423. case <-sess.peerReady:
  424. case <-ctx.Done():
  425. return
  426. }
  427. }
  428. sess.mu.Lock()
  429. port := sess.clientPort
  430. sess.mu.Unlock()
  431. if port <= 0 {
  432. return
  433. }
  434. dstUDPAddr := &net.UDPAddr{IP: sess.clientAddr, Port: port}
  435. pktSize := sess.handshake.PacketSize
  436. if pktSize < HeaderSize {
  437. pktSize = HeaderSize
  438. }
  439. buf := make([]byte, pktSize)
  440. if _, err := crand.Read(buf); err != nil {
  441. return
  442. }
  443. var seq uint64
  444. hdr := PacketHeader{
  445. Magic: MagicBytes,
  446. Version: ProtoVer,
  447. PacketType: PacketTypeData,
  448. StreamID: 0,
  449. Flags: 0,
  450. SessionHash: sess.sessionHash,
  451. PayloadLength: uint32(pktSize - HeaderSize),
  452. }
  453. for {
  454. select {
  455. case <-ctx.Done():
  456. return
  457. default:
  458. }
  459. if !sess.udpPacing.wait(ctx, pktSize) {
  460. return
  461. }
  462. seq++
  463. hdr.Sequence = seq
  464. hdr.TimestampNano = time.Now().UnixNano()
  465. EncodeHeader(buf, hdr)
  466. n, err := s.udpConn.WriteToUDP(buf, dstUDPAddr)
  467. if err == nil {
  468. sess.txBytes.Add(int64(n))
  469. sess.txPackets.Add(1)
  470. s.totalBytes.Add(int64(n))
  471. }
  472. }
  473. }
  474. // runServerTCPTx generates continuous TCP stream data to client
  475. func (s *BwTestServer) runServerTCPTx(ctx context.Context, sess *serverSession) {
  476. chunk := make([]byte, 64*1024)
  477. if _, err := crand.Read(chunk); err != nil {
  478. return
  479. }
  480. limiter := NewRateLimiter(sess.handshake.BandwidthLimit)
  481. for {
  482. select {
  483. case <-ctx.Done():
  484. return
  485. default:
  486. }
  487. if limiter != nil {
  488. if !limiter.WaitContext(ctx, len(chunk)) {
  489. return
  490. }
  491. }
  492. n, err := sess.tcpWriter.write(tcpFrameData, chunk)
  493. if err != nil {
  494. return
  495. }
  496. sess.txBytes.Add(int64(n))
  497. sess.txPackets.Add(1)
  498. s.totalBytes.Add(int64(n))
  499. }
  500. }
  501. // reportLoop periodically sends feedback reports back to the client over TCP
  502. func (s *BwTestServer) reportLoop(ctx context.Context, sess *serverSession) {
  503. ticker := time.NewTicker(300 * time.Millisecond)
  504. defer ticker.Stop()
  505. for {
  506. select {
  507. case <-ctx.Done():
  508. return
  509. case <-ticker.C:
  510. report := SessionHandshake{
  511. Type: "report",
  512. SessionID: sess.id,
  513. RxBytes: sess.rxBytes.Load(),
  514. RxPackets: sess.rxPackets.Load(),
  515. LostPackets: sess.lostPackets.Load(),
  516. }
  517. b, err := json.Marshal(report)
  518. if err == nil {
  519. if sess.handshake.Protocol == "tcp" {
  520. _, _ = sess.tcpWriter.write(tcpFrameControl, b)
  521. } else {
  522. _, _ = sess.clientTCP.Write(append(b, '\n'))
  523. }
  524. }
  525. }
  526. }
  527. }