manager.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. package tools
  2. import (
  3. "bufio"
  4. "context"
  5. "fmt"
  6. "net"
  7. "os/exec"
  8. "regexp"
  9. "strconv"
  10. "strings"
  11. "time"
  12. )
  13. var (
  14. validHostRegex = regexp.MustCompile(`^[a-zA-Z0-9.:_-]+$`)
  15. pingReplyRegex = regexp.MustCompile(`(\d+)\s+bytes from\s+([^:]+):\s+icmp_seq=(\d+)\s+ttl=(\d+)\s+time=([\d.]+)\s+ms`)
  16. pingTimeoutRegex = regexp.MustCompile(`(?:no answer yet for icmp_seq=|Request timeout for icmp_seq\s+)(\d+)`)
  17. pingUnreachRegex = regexp.MustCompile(`From\s+([^\s:]+)\s+icmp_seq=(\d+)\s+(.*)`)
  18. pingStatRegex = regexp.MustCompile(`(\d+)\s+packets transmitted,\s+(\d+)\s+(?:packets\s+)?received,\s+([\d.]+)%\s+packet loss`)
  19. pingRttRegex = regexp.MustCompile(`rtt min/avg/max/mdev = ([\d.]+)/([\d.]+)/([\d.]+)/([\d.]+)\s+ms`)
  20. mtrHopRegex = regexp.MustCompile(`^\s*(\d+)\.\|--\s+(\S+)\s+([\d.]+)%?\s+(\d+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)`)
  21. )
  22. type Manager struct{}
  23. func NewManager() *Manager {
  24. return &Manager{}
  25. }
  26. func sanitizeTarget(target string) (string, error) {
  27. t := strings.TrimSpace(target)
  28. if t == "" {
  29. return "", fmt.Errorf("target host or IP is required")
  30. }
  31. if len(t) > 255 || !validHostRegex.MatchString(t) {
  32. return "", fmt.Errorf("invalid target host format")
  33. }
  34. return t, nil
  35. }
  36. // RunPing executes ICMP ping with custom parameters
  37. func (m *Manager) RunPing(ctx context.Context, req PingRequest) (*PingResponse, error) {
  38. target, err := sanitizeTarget(req.Target)
  39. if err != nil {
  40. return nil, err
  41. }
  42. count := req.Count
  43. if count <= 0 {
  44. count = 4
  45. }
  46. if count > 30 {
  47. count = 30
  48. }
  49. size := req.Size
  50. if size <= 0 {
  51. size = 56
  52. }
  53. if size > 65500 {
  54. size = 65500
  55. }
  56. timeout := req.Timeout
  57. if timeout <= 0 {
  58. timeout = 5
  59. }
  60. if timeout > 30 {
  61. timeout = 30
  62. }
  63. args := []string{
  64. "-c", strconv.Itoa(count),
  65. "-s", strconv.Itoa(size),
  66. "-W", strconv.Itoa(timeout),
  67. }
  68. if req.Interface != "" && validHostRegex.MatchString(req.Interface) {
  69. args = append(args, "-I", req.Interface)
  70. }
  71. if req.DoNotFrag {
  72. args = append(args, "-M", "do")
  73. }
  74. args = append(args, target)
  75. cmd := exec.CommandContext(ctx, "ping", args...)
  76. outBytes, _ := cmd.CombinedOutput()
  77. outStr := string(outBytes)
  78. resp := &PingResponse{
  79. Target: target,
  80. PacketsSent: count,
  81. Replies: []PingReply{},
  82. RawOutput: outStr,
  83. Status: "timeout",
  84. }
  85. // Parse individual ping replies
  86. matches := pingReplyRegex.FindAllStringSubmatch(outStr, -1)
  87. for _, match := range matches {
  88. if len(match) >= 6 {
  89. bytes, _ := strconv.Atoi(match[1])
  90. from := match[2]
  91. seq, _ := strconv.Atoi(match[3])
  92. ttl, _ := strconv.Atoi(match[4])
  93. timeMs, _ := strconv.ParseFloat(match[5], 64)
  94. resp.Replies = append(resp.Replies, PingReply{
  95. Seq: seq,
  96. Bytes: bytes,
  97. From: from,
  98. TTL: ttl,
  99. TimeMs: timeMs,
  100. })
  101. }
  102. }
  103. // Parse overall stats
  104. if statMatch := pingStatRegex.FindStringSubmatch(outStr); len(statMatch) >= 4 {
  105. resp.PacketsSent, _ = strconv.Atoi(statMatch[1])
  106. resp.PacketsRecv, _ = strconv.Atoi(statMatch[2])
  107. resp.PacketLossPct, _ = strconv.ParseFloat(statMatch[3], 64)
  108. }
  109. // Parse RTT min/avg/max/mdev
  110. if rttMatch := pingRttRegex.FindStringSubmatch(outStr); len(rttMatch) >= 5 {
  111. resp.MinRTT, _ = strconv.ParseFloat(rttMatch[1], 64)
  112. resp.AvgRTT, _ = strconv.ParseFloat(rttMatch[2], 64)
  113. resp.MaxRTT, _ = strconv.ParseFloat(rttMatch[3], 64)
  114. resp.MdevRTT, _ = strconv.ParseFloat(rttMatch[4], 64)
  115. }
  116. if resp.PacketsRecv == resp.PacketsSent && resp.PacketsSent > 0 {
  117. resp.Status = "ok"
  118. } else if resp.PacketsRecv > 0 {
  119. resp.Status = "partial"
  120. } else {
  121. resp.Status = "timeout"
  122. }
  123. return resp, nil
  124. }
  125. // StreamPing executes ICMP ping and streams real-time events line-by-line
  126. func (m *Manager) StreamPing(ctx context.Context, req PingRequest, onEvent func(PingStreamEvent)) error {
  127. target, err := sanitizeTarget(req.Target)
  128. if err != nil {
  129. return err
  130. }
  131. size := req.Size
  132. if size <= 0 {
  133. size = 56
  134. }
  135. if size > 65500 {
  136. size = 65500
  137. }
  138. timeout := req.Timeout
  139. if timeout <= 0 {
  140. timeout = 3
  141. }
  142. if timeout > 30 {
  143. timeout = 30
  144. }
  145. args := []string{
  146. "-O", // report outstanding replies/timeouts in real time
  147. "-s", strconv.Itoa(size),
  148. "-W", strconv.Itoa(timeout),
  149. }
  150. if req.Count > 0 {
  151. args = append(args, "-c", strconv.Itoa(req.Count))
  152. }
  153. if req.Interface != "" && validHostRegex.MatchString(req.Interface) {
  154. args = append(args, "-I", req.Interface)
  155. }
  156. if req.DoNotFrag {
  157. args = append(args, "-M", "do")
  158. }
  159. args = append(args, target)
  160. cmd := exec.CommandContext(ctx, "ping", args...)
  161. stdout, err := cmd.StdoutPipe()
  162. if err != nil {
  163. return err
  164. }
  165. cmd.Stderr = cmd.Stdout
  166. if err := cmd.Start(); err != nil {
  167. return err
  168. }
  169. packetsSent := 0
  170. packetsRecv := 0
  171. var minRTT, maxRTT, sumRTT float64
  172. scanner := bufio.NewScanner(stdout)
  173. for scanner.Scan() {
  174. line := scanner.Text()
  175. trimmed := strings.TrimSpace(line)
  176. if trimmed == "" {
  177. continue
  178. }
  179. if strings.HasPrefix(trimmed, "PING ") {
  180. onEvent(PingStreamEvent{
  181. Type: "header",
  182. Raw: line,
  183. })
  184. continue
  185. }
  186. // Match reply
  187. if match := pingReplyRegex.FindStringSubmatch(trimmed); len(match) >= 6 {
  188. bytes, _ := strconv.Atoi(match[1])
  189. from := match[2]
  190. seq, _ := strconv.Atoi(match[3])
  191. ttl, _ := strconv.Atoi(match[4])
  192. timeMs, _ := strconv.ParseFloat(match[5], 64)
  193. packetsSent++
  194. packetsRecv++
  195. sumRTT += timeMs
  196. if packetsRecv == 1 || timeMs < minRTT {
  197. minRTT = timeMs
  198. }
  199. if timeMs > maxRTT {
  200. maxRTT = timeMs
  201. }
  202. avgRTT := sumRTT / float64(packetsRecv)
  203. lossPct := float64(packetsSent-packetsRecv) * 100.0 / float64(packetsSent)
  204. onEvent(PingStreamEvent{
  205. Type: "reply",
  206. Seq: seq,
  207. Bytes: bytes,
  208. From: from,
  209. TTL: ttl,
  210. TimeMs: timeMs,
  211. PacketsSent: packetsSent,
  212. PacketsRecv: packetsRecv,
  213. PacketLossPct: lossPct,
  214. MinRTT: minRTT,
  215. AvgRTT: avgRTT,
  216. MaxRTT: maxRTT,
  217. Raw: line,
  218. })
  219. continue
  220. }
  221. // Match timeout (no answer yet for icmp_seq=X)
  222. if match := pingTimeoutRegex.FindStringSubmatch(trimmed); len(match) >= 2 {
  223. seq, _ := strconv.Atoi(match[1])
  224. packetsSent++
  225. lossPct := float64(packetsSent-packetsRecv) * 100.0 / float64(packetsSent)
  226. avgRTT := 0.0
  227. if packetsRecv > 0 {
  228. avgRTT = sumRTT / float64(packetsRecv)
  229. }
  230. onEvent(PingStreamEvent{
  231. Type: "timeout",
  232. Seq: seq,
  233. PacketsSent: packetsSent,
  234. PacketsRecv: packetsRecv,
  235. PacketLossPct: lossPct,
  236. MinRTT: minRTT,
  237. AvgRTT: avgRTT,
  238. MaxRTT: maxRTT,
  239. Message: "Request timed out",
  240. Raw: line,
  241. })
  242. continue
  243. }
  244. // Match ICMP unreach error
  245. if match := pingUnreachRegex.FindStringSubmatch(trimmed); len(match) >= 4 {
  246. from := match[1]
  247. seq, _ := strconv.Atoi(match[2])
  248. msg := match[3]
  249. packetsSent++
  250. lossPct := float64(packetsSent-packetsRecv) * 100.0 / float64(packetsSent)
  251. avgRTT := 0.0
  252. if packetsRecv > 0 {
  253. avgRTT = sumRTT / float64(packetsRecv)
  254. }
  255. onEvent(PingStreamEvent{
  256. Type: "unreach",
  257. Seq: seq,
  258. From: from,
  259. PacketsSent: packetsSent,
  260. PacketsRecv: packetsRecv,
  261. PacketLossPct: lossPct,
  262. MinRTT: minRTT,
  263. AvgRTT: avgRTT,
  264. MaxRTT: maxRTT,
  265. Message: msg,
  266. Raw: line,
  267. })
  268. continue
  269. }
  270. // Match overall stats summary
  271. if match := pingStatRegex.FindStringSubmatch(trimmed); len(match) >= 4 {
  272. sSent, _ := strconv.Atoi(match[1])
  273. sRecv, _ := strconv.Atoi(match[2])
  274. sLoss, _ := strconv.ParseFloat(match[3], 64)
  275. packetsSent = sSent
  276. packetsRecv = sRecv
  277. avgRTT := 0.0
  278. if packetsRecv > 0 {
  279. avgRTT = sumRTT / float64(packetsRecv)
  280. }
  281. onEvent(PingStreamEvent{
  282. Type: "stats",
  283. PacketsSent: packetsSent,
  284. PacketsRecv: packetsRecv,
  285. PacketLossPct: sLoss,
  286. MinRTT: minRTT,
  287. AvgRTT: avgRTT,
  288. MaxRTT: maxRTT,
  289. Raw: line,
  290. })
  291. continue
  292. }
  293. // Match RTT min/avg/max/mdev
  294. if match := pingRttRegex.FindStringSubmatch(trimmed); len(match) >= 5 {
  295. sMin, _ := strconv.ParseFloat(match[1], 64)
  296. sAvg, _ := strconv.ParseFloat(match[2], 64)
  297. sMax, _ := strconv.ParseFloat(match[3], 64)
  298. sMdev, _ := strconv.ParseFloat(match[4], 64)
  299. lossPct := 0.0
  300. if packetsSent > 0 {
  301. lossPct = float64(packetsSent-packetsRecv) * 100.0 / float64(packetsSent)
  302. }
  303. onEvent(PingStreamEvent{
  304. Type: "stats",
  305. PacketsSent: packetsSent,
  306. PacketsRecv: packetsRecv,
  307. PacketLossPct: lossPct,
  308. MinRTT: sMin,
  309. AvgRTT: sAvg,
  310. MaxRTT: sMax,
  311. MdevRTT: sMdev,
  312. Raw: line,
  313. })
  314. continue
  315. }
  316. // Generic line
  317. onEvent(PingStreamEvent{
  318. Type: "raw",
  319. Raw: line,
  320. })
  321. }
  322. _ = cmd.Wait()
  323. lossPct := 0.0
  324. if packetsSent > 0 {
  325. lossPct = float64(packetsSent-packetsRecv) * 100.0 / float64(packetsSent)
  326. }
  327. avgRTT := 0.0
  328. if packetsRecv > 0 {
  329. avgRTT = sumRTT / float64(packetsRecv)
  330. }
  331. onEvent(PingStreamEvent{
  332. Type: "done",
  333. PacketsSent: packetsSent,
  334. PacketsRecv: packetsRecv,
  335. PacketLossPct: lossPct,
  336. MinRTT: minRTT,
  337. AvgRTT: avgRTT,
  338. MaxRTT: maxRTT,
  339. Raw: "Ping finished",
  340. })
  341. return nil
  342. }
  343. // RunTraceroute traces the route to a target
  344. func (m *Manager) RunTraceroute(ctx context.Context, req TracerouteRequest) (*TracerouteResponse, error) {
  345. target, err := sanitizeTarget(req.Target)
  346. if err != nil {
  347. return nil, err
  348. }
  349. maxHops := req.MaxHops
  350. if maxHops <= 0 {
  351. maxHops = 15
  352. }
  353. if maxHops > 30 {
  354. maxHops = 30
  355. }
  356. args := []string{
  357. "-m", strconv.Itoa(maxHops),
  358. "-q", "2",
  359. "-w", "1",
  360. }
  361. if !req.Resolve {
  362. args = append(args, "-n")
  363. }
  364. if req.Protocol == "icmp" {
  365. args = append(args, "-I")
  366. }
  367. if req.Interface != "" && validHostRegex.MatchString(req.Interface) {
  368. args = append(args, "-i", req.Interface)
  369. }
  370. args = append(args, target)
  371. cmd := exec.CommandContext(ctx, "traceroute", args...)
  372. outBytes, _ := cmd.CombinedOutput()
  373. outStr := string(outBytes)
  374. resp := &TracerouteResponse{
  375. Target: target,
  376. Hops: []TracerouteHop{},
  377. RawOutput: outStr,
  378. }
  379. lines := strings.Split(outStr, "\n")
  380. for i, line := range lines {
  381. if i == 0 {
  382. continue // skip header line
  383. }
  384. trimmed := strings.TrimSpace(line)
  385. if trimmed == "" {
  386. continue
  387. }
  388. fields := strings.Fields(trimmed)
  389. if len(fields) < 2 {
  390. continue
  391. }
  392. hopNum, err := strconv.Atoi(fields[0])
  393. if err != nil {
  394. continue
  395. }
  396. if fields[1] == "*" {
  397. resp.Hops = append(resp.Hops, TracerouteHop{
  398. Hop: hopNum,
  399. Host: "*",
  400. IP: "*",
  401. TimesMs: []string{"*"},
  402. Status: "timeout",
  403. })
  404. } else {
  405. hostOrIP := fields[1]
  406. ip := hostOrIP
  407. if len(fields) >= 3 && strings.HasPrefix(fields[2], "(") && strings.HasSuffix(fields[2], ")") {
  408. ip = strings.Trim(fields[2], "()")
  409. }
  410. times := make([]string, 0)
  411. for _, f := range fields[2:] {
  412. if f != "ms" && !strings.HasPrefix(f, "(") {
  413. times = append(times, f)
  414. }
  415. }
  416. resp.Hops = append(resp.Hops, TracerouteHop{
  417. Hop: hopNum,
  418. Host: hostOrIP,
  419. IP: ip,
  420. TimesMs: times,
  421. Status: "ok",
  422. })
  423. }
  424. }
  425. return resp, nil
  426. }
  427. // StreamTraceroute executes traceroute and streams hop events line-by-line
  428. func (m *Manager) StreamTraceroute(ctx context.Context, req TracerouteRequest, onEvent func(TracerouteStreamEvent)) error {
  429. target, err := sanitizeTarget(req.Target)
  430. if err != nil {
  431. return err
  432. }
  433. maxHops := req.MaxHops
  434. if maxHops <= 0 {
  435. maxHops = 15
  436. }
  437. if maxHops > 30 {
  438. maxHops = 30
  439. }
  440. args := []string{
  441. "-m", strconv.Itoa(maxHops),
  442. "-q", "2",
  443. "-w", "1",
  444. }
  445. if !req.Resolve {
  446. args = append(args, "-n")
  447. }
  448. if req.Protocol == "icmp" {
  449. args = append(args, "-I")
  450. }
  451. if req.Interface != "" && validHostRegex.MatchString(req.Interface) {
  452. args = append(args, "-i", req.Interface)
  453. }
  454. args = append(args, target)
  455. cmd := exec.CommandContext(ctx, "traceroute", args...)
  456. stdout, err := cmd.StdoutPipe()
  457. if err != nil {
  458. return err
  459. }
  460. cmd.Stderr = cmd.Stdout
  461. if err := cmd.Start(); err != nil {
  462. return err
  463. }
  464. hopsCount := 0
  465. scanner := bufio.NewScanner(stdout)
  466. isHeader := true
  467. for scanner.Scan() {
  468. line := scanner.Text()
  469. trimmed := strings.TrimSpace(line)
  470. if trimmed == "" {
  471. continue
  472. }
  473. if isHeader {
  474. isHeader = false
  475. onEvent(TracerouteStreamEvent{
  476. Type: "header",
  477. Raw: line,
  478. })
  479. continue
  480. }
  481. fields := strings.Fields(trimmed)
  482. if len(fields) < 2 {
  483. continue
  484. }
  485. hopNum, err := strconv.Atoi(fields[0])
  486. if err != nil {
  487. continue
  488. }
  489. hopsCount++
  490. var hop TracerouteHop
  491. if fields[1] == "*" {
  492. hop = TracerouteHop{
  493. Hop: hopNum,
  494. Host: "*",
  495. IP: "*",
  496. TimesMs: []string{"*"},
  497. Status: "timeout",
  498. }
  499. } else {
  500. hostOrIP := fields[1]
  501. ip := hostOrIP
  502. if len(fields) >= 3 && strings.HasPrefix(fields[2], "(") && strings.HasSuffix(fields[2], ")") {
  503. ip = strings.Trim(fields[2], "()")
  504. }
  505. times := make([]string, 0)
  506. for _, f := range fields[2:] {
  507. if f != "ms" && !strings.HasPrefix(f, "(") {
  508. times = append(times, f)
  509. }
  510. }
  511. hop = TracerouteHop{
  512. Hop: hopNum,
  513. Host: hostOrIP,
  514. IP: ip,
  515. TimesMs: times,
  516. Status: "ok",
  517. }
  518. }
  519. onEvent(TracerouteStreamEvent{
  520. Type: "hop",
  521. Hop: &hop,
  522. HopsCount: hopsCount,
  523. Raw: line,
  524. })
  525. }
  526. _ = cmd.Wait()
  527. onEvent(TracerouteStreamEvent{
  528. Type: "done",
  529. HopsCount: hopsCount,
  530. Raw: "Traceroute finished",
  531. })
  532. return nil
  533. }
  534. // RunDNSLookup performs DNS resolution via dig
  535. func (m *Manager) RunDNSLookup(ctx context.Context, req DNSRequest) (*DNSResponse, error) {
  536. domain, err := sanitizeTarget(req.Domain)
  537. if err != nil {
  538. return nil, err
  539. }
  540. qType := strings.ToUpper(strings.TrimSpace(req.Type))
  541. if qType == "" {
  542. qType = "A"
  543. }
  544. args := []string{
  545. "+noall", "+answer", "+stats",
  546. domain,
  547. qType,
  548. }
  549. if req.Server != "" {
  550. srv, sErr := sanitizeTarget(req.Server)
  551. if sErr == nil {
  552. args = append(args, "@"+srv)
  553. }
  554. }
  555. cmd := exec.CommandContext(ctx, "dig", args...)
  556. outBytes, _ := cmd.CombinedOutput()
  557. outStr := string(outBytes)
  558. resp := &DNSResponse{
  559. Domain: domain,
  560. Type: qType,
  561. Server: req.Server,
  562. Records: []DNSRecord{},
  563. RawOutput: outStr,
  564. }
  565. lines := strings.Split(outStr, "\n")
  566. for _, line := range lines {
  567. l := strings.TrimSpace(line)
  568. if l == "" {
  569. continue
  570. }
  571. if strings.HasPrefix(l, ";; Query time:") {
  572. parts := strings.Fields(l)
  573. if len(parts) >= 4 {
  574. resp.QueryTimeMs, _ = strconv.Atoi(parts[2])
  575. }
  576. continue
  577. }
  578. if strings.HasPrefix(l, ";; SERVER:") {
  579. parts := strings.Fields(l)
  580. if len(parts) >= 3 {
  581. resp.Server = parts[2]
  582. }
  583. continue
  584. }
  585. if strings.HasPrefix(l, ";") {
  586. continue
  587. }
  588. fields := strings.Fields(l)
  589. if len(fields) >= 5 {
  590. ttl, _ := strconv.Atoi(fields[1])
  591. resp.Records = append(resp.Records, DNSRecord{
  592. Name: fields[0],
  593. TTL: ttl,
  594. Class: fields[2],
  595. Type: fields[3],
  596. Value: strings.Join(fields[4:], " "),
  597. })
  598. }
  599. }
  600. return resp, nil
  601. }
  602. // RunPortProbe tests TCP port connectivity and measures latency
  603. func (m *Manager) RunPortProbe(ctx context.Context, req PortProbeRequest) (*PortProbeResponse, error) {
  604. target, err := sanitizeTarget(req.Target)
  605. if err != nil {
  606. return nil, err
  607. }
  608. if req.Port <= 0 || req.Port > 65535 {
  609. return nil, fmt.Errorf("invalid port number: must be between 1 and 65535")
  610. }
  611. timeoutSec := req.Timeout
  612. if timeoutSec <= 0 {
  613. timeoutSec = 3
  614. }
  615. if timeoutSec > 10 {
  616. timeoutSec = 10
  617. }
  618. serviceMap := map[int]string{
  619. 21: "FTP", 22: "SSH", 23: "Telnet", 25: "SMTP", 53: "DNS",
  620. 80: "HTTP", 110: "POP3", 143: "IMAP", 179: "BGP", 443: "HTTPS",
  621. 587: "Submission", 993: "IMAPS", 995: "POP3S", 1194: "OpenVPN",
  622. 3389: "RDP", 5060: "SIP", 8080: "HTTP-Alt", 8291: "Winbox (MikroTik)",
  623. 8443: "HTTPS-Alt", 8006: "Proxmox VE",
  624. }
  625. addr := net.JoinHostPort(target, strconv.Itoa(req.Port))
  626. start := time.Now()
  627. d := net.Dialer{Timeout: time.Duration(timeoutSec) * time.Second}
  628. conn, dialErr := d.DialContext(ctx, "tcp", addr)
  629. elapsed := float64(time.Since(start).Microseconds()) / 1000.0
  630. resp := &PortProbeResponse{
  631. Target: target,
  632. Port: req.Port,
  633. LatencyMs: elapsed,
  634. Service: serviceMap[req.Port],
  635. }
  636. if dialErr != nil {
  637. resp.Open = false
  638. resp.Error = dialErr.Error()
  639. resp.RawOutput = fmt.Sprintf("Failed to connect to %s: %v", addr, dialErr)
  640. } else {
  641. _ = conn.Close()
  642. resp.Open = true
  643. resp.RawOutput = fmt.Sprintf("Successfully connected to %s (%s) in %.2f ms (TCP handshake OK)", addr, resp.Service, elapsed)
  644. }
  645. return resp, nil
  646. }
  647. // RunMTR executes MTR (My Traceroute) report
  648. func (m *Manager) RunMTR(ctx context.Context, req MTRRequest) (*MTRResponse, error) {
  649. target, err := sanitizeTarget(req.Target)
  650. if err != nil {
  651. return nil, err
  652. }
  653. count := req.Count
  654. if count <= 0 {
  655. count = 3
  656. }
  657. if count > 10 {
  658. count = 10
  659. }
  660. cmd := exec.CommandContext(ctx, "mtr", "-n", "-r", "-c", strconv.Itoa(count), target)
  661. outBytes, _ := cmd.CombinedOutput()
  662. outStr := string(outBytes)
  663. resp := &MTRResponse{
  664. Target: target,
  665. Hops: []MTRHop{},
  666. RawOutput: outStr,
  667. }
  668. lines := strings.Split(outStr, "\n")
  669. for _, line := range lines {
  670. matches := mtrHopRegex.FindStringSubmatch(line)
  671. if len(matches) >= 10 {
  672. hopNum, _ := strconv.Atoi(matches[1])
  673. host := matches[2]
  674. loss, _ := strconv.ParseFloat(matches[3], 64)
  675. sent, _ := strconv.Atoi(matches[4])
  676. last, _ := strconv.ParseFloat(matches[5], 64)
  677. avg, _ := strconv.ParseFloat(matches[6], 64)
  678. best, _ := strconv.ParseFloat(matches[7], 64)
  679. wrst, _ := strconv.ParseFloat(matches[8], 64)
  680. stdev, _ := strconv.ParseFloat(matches[9], 64)
  681. resp.Hops = append(resp.Hops, MTRHop{
  682. Hop: hopNum,
  683. Host: host,
  684. LossPct: loss,
  685. Sent: sent,
  686. LastMs: last,
  687. AvgMs: avg,
  688. BestMs: best,
  689. WrstMs: wrst,
  690. StDevMs: stdev,
  691. })
  692. }
  693. }
  694. return resp, nil
  695. }