Merge pull request #1434 from bbx-winner/feat-dial-test

feat: support ping/dns/traceroute for dial test
This commit is contained in:
debugtalk
2022-08-15 20:54:08 +08:00
committed by GitHub
10 changed files with 775 additions and 0 deletions

96
hrp/cmd/dial.go Normal file
View File

@@ -0,0 +1,96 @@
package cmd
import (
"runtime"
"time"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/dial"
)
var (
pingOptions dial.PingOptions
dnsOptions dial.DnsOptions
traceRouteOptions dial.TraceRouteOptions
)
var pingCmd = &cobra.Command{
Use: "ping $url",
Short: "run integrated ping command",
Args: cobra.ExactArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel)
},
RunE: func(cmd *cobra.Command, args []string) error {
return dial.DoPing(&pingOptions, args)
},
}
var dnsCmd = &cobra.Command{
Use: "dns $url",
Short: "DNS resolution for different source and record types",
Args: cobra.ExactArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel)
},
RunE: func(cmd *cobra.Command, args []string) error {
if dnsOptions.DnsSourceType != dial.DnsSourceTypeLocal && dnsOptions.DnsServer != "" {
log.Warn().Msg("DNS server not supported for non-local DNS source, ignored")
}
if dnsOptions.DnsSourceType == dial.DnsSourceTypeHttp && dnsOptions.DnsRecordType == dial.DnsRecordTypeCNAME {
log.Warn().Msg("CNAME record not supported for http DNS source, using default record type(A)")
}
return dial.DoDns(&dnsOptions, args)
},
}
var traceRouteCmd = &cobra.Command{
Use: "traceroute $url",
Short: "run integrated traceroute command",
Args: cobra.ExactArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel)
},
RunE: func(cmd *cobra.Command, args []string) error {
if runtime.GOOS == "windows" {
log.Info().Msg("using default probe number (3) on Windows")
}
return dial.DoTraceRoute(&traceRouteOptions, args)
},
}
var curlCmd = &cobra.Command{
Use: "curl $url",
Short: "run integrated curl command",
Args: cobra.MinimumNArgs(1),
DisableFlagParsing: true,
PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel)
},
RunE: func(cmd *cobra.Command, args []string) error {
return dial.DoCurl(args)
},
}
func init() {
rootCmd.AddCommand(pingCmd)
pingCmd.Flags().IntVarP(&pingOptions.Count, "count", "c", 10, "Stop after sending (and receiving) N packets")
pingCmd.Flags().DurationVarP(&pingOptions.Timeout, "timeout", "t", 20*time.Second, "Ping exits after N seconds")
pingCmd.Flags().DurationVarP(&pingOptions.Interval, "interval", "i", 1*time.Second, "Wait N seconds between sending each packet")
pingCmd.Flags().BoolVar(&pingOptions.SaveTests, "save-tests", false, "Save ping result as json")
rootCmd.AddCommand(dnsCmd)
dnsCmd.Flags().IntVar(&dnsOptions.DnsSourceType, "dns-source", 0, "DNS source type\n0: local DNS\n1: http DNS\n2: google DNS")
dnsCmd.Flags().IntVar(&dnsOptions.DnsRecordType, "dns-record", 1, "DNS record type\n1: A\n28: AAAA\n5: CNAME")
dnsCmd.Flags().StringVar(&dnsOptions.DnsServer, "dns-server", "", "DNS server, only available for local DNS source")
dnsCmd.Flags().BoolVar(&dnsOptions.SaveTests, "save-tests", false, "Save DNS resolution result as json")
rootCmd.AddCommand(traceRouteCmd)
traceRouteCmd.Flags().IntVarP(&traceRouteOptions.MaxTTL, "max-hops", "m", 30, "Set the max number of hops (max TTL to be reached)")
traceRouteCmd.Flags().IntVarP(&traceRouteOptions.Queries, "queries", "q", 1, "Set the number of probes per each hop")
traceRouteCmd.Flags().BoolVar(&traceRouteOptions.SaveTests, "save-tests", false, "Save traceroute result as json")
rootCmd.AddCommand(curlCmd)
}

72
hrp/internal/dial/curl.go Normal file
View File

@@ -0,0 +1,72 @@
package dial
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
const (
normalResult = "STDOUT"
errorResult = "STDERR"
failedResult = "FAILED"
)
type CurlResult struct {
Result string `json:"result"`
ErrorMsg string `json:"errorMsg"`
ResultType string `json:"resultType"`
}
func DoCurl(args []string) (err error) {
var saveTests bool
for i, arg := range args {
if arg == "--save-tests" {
args = append(args[:i], args[i+1:]...)
saveTests = true
}
}
var curlResult CurlResult
defer func() {
if saveTests {
dir, _ := os.Getwd()
curlResultName := fmt.Sprintf("curl_result_%v.json", time.Now().Format("20060102150405"))
curlResultPath := filepath.Join(dir, curlResultName)
err = builtin.Dump2JSON(curlResult, curlResultPath)
if err != nil {
log.Error().Err(err).Msg("save dns resolution result failed")
}
}
}()
cmd := exec.Command("curl", args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
if err != nil {
log.Error().Err(err).Msgf("fail to run curl command")
curlResult.ErrorMsg = err.Error()
curlResult.Result = stderr.String()
curlResult.ResultType = errorResult
return
}
if stdout.String() != "" {
fmt.Printf(stdout.String())
curlResult.Result = stdout.String()
curlResult.ResultType = normalResult
} else if stderr.String() != "" {
fmt.Printf(stderr.String())
curlResult.ErrorMsg = stderr.String()
curlResult.ResultType = errorResult
}
return
}

251
hrp/internal/dial/dns.go Normal file
View File

@@ -0,0 +1,251 @@
package dial
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/miekg/dns"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
const (
httpDnsUrl = "https://dig.bdurl.net/q"
googleDnsUrl = "https://dns.google/resolve"
)
const (
DnsSourceTypeLocal = iota
DnsSourceTypeHttp
DnsSourceTypeGoogle
)
const (
DnsRecordTypeA = 1
DnsRecordTypeAAAA = 28
DnsRecordTypeCNAME = 5
)
var dnsHttpClient = &http.Client{
Timeout: 5 * time.Minute,
}
type DnsOptions struct {
DnsSourceType int
DnsRecordType int
DnsServer string
SaveTests bool
}
type DnsResult struct {
DnsList []string `json:"dnsList"`
DnsSource int `json:"dnsType"`
DnsRecordType int `json:"dnsRecordType"`
DnsServer string `json:"dnsServer,omitempty"`
Ttl int `json:"ttl"`
Suc bool `json:"suc"`
ErrMsg string `json:"errMsg"`
}
type googleDnsResp struct {
Answer []googleDnsAnswer `json:"Answer"`
}
type httpDnsResp struct {
Ips []string `json:"ips"`
Ttl int `json:"ttl"`
}
type googleDnsAnswer struct {
Name string `json:"name"`
Type int `json:"type"`
TTL int `json:"TTL"`
Data string `json:"data"`
}
func ParseIP(s string) (net.IP, int) {
ip := net.ParseIP(s)
if ip == nil {
return nil, 0
}
for i := 0; i < len(s); i++ {
switch s[i] {
case '.':
return ip, 4
case ':':
return ip, 6
}
}
return nil, 0
}
func localDns(src string, dnsRecordType int, dnsServer string) (dnsResult DnsResult, err error) {
dnsResult.DnsSource = DnsSourceTypeLocal
dnsResult.DnsRecordType = dnsRecordType
if dnsServer == "" {
config, _ := dns.ClientConfigFromFile("/etc/resolv.conf")
dnsServer = config.Servers[0]
} else {
dnsResult.DnsServer = dnsServer
}
_, ipType := ParseIP(dnsServer)
if ipType == 4 {
dnsServer += ":53"
}
c := dns.Client{
Timeout: 5 * time.Second,
}
m := dns.Msg{}
m.SetQuestion(src+".", uint16(dnsRecordType))
r, _, err := c.Exchange(&m, dnsServer)
if err != nil {
return
}
for _, ans := range r.Answer {
switch dnsRecordType {
case DnsRecordTypeA:
record, isType := ans.(*dns.A)
if isType {
dnsResult.Ttl = int(record.Hdr.Ttl)
dnsResult.DnsList = append(dnsResult.DnsList, record.A.String())
}
case DnsRecordTypeAAAA:
record, isType := ans.(*dns.AAAA)
if isType {
dnsResult.Ttl = int(record.Hdr.Ttl)
dnsResult.DnsList = append(dnsResult.DnsList, record.AAAA.String())
}
case DnsRecordTypeCNAME:
record, isType := ans.(*dns.CNAME)
if isType {
dnsResult.Ttl = int(record.Hdr.Ttl)
dnsResult.DnsList = append(dnsResult.DnsList, record.Target)
}
}
}
return
}
func httpDns(url string, dnsRecordType int) (dnsResult DnsResult, err error) {
target := httpDnsUrl + "?host=" + url
if dnsRecordType == DnsRecordTypeAAAA {
target += "&aid=13&f=2"
}
resp, err := dnsHttpClient.Get(target)
dnsResult.DnsSource = DnsSourceTypeHttp
dnsResult.DnsRecordType = dnsRecordType
if err != nil {
return
}
defer resp.Body.Close()
var buf []byte
buf, err = ioutil.ReadAll(resp.Body)
if err != nil {
return
}
var result httpDnsResp
err = json.Unmarshal(buf, &result)
if err != nil {
return
}
dnsResult.DnsList = result.Ips
dnsResult.Ttl = result.Ttl
return
}
func googleDns(url string, dnsRecordType int) (dnsResult DnsResult, err error) {
resp, err := dnsHttpClient.Get(googleDnsUrl + "?name=" + url + "&type=" + strconv.Itoa(dnsRecordType))
dnsResult.DnsSource = DnsSourceTypeGoogle
dnsResult.DnsRecordType = dnsRecordType
if err != nil {
return
}
defer resp.Body.Close()
var buf []byte
buf, err = ioutil.ReadAll(resp.Body)
if err != nil {
return
}
var result googleDnsResp
err = json.Unmarshal(buf, &result)
if err != nil {
return
}
if len(result.Answer) == 0 {
return
}
for _, answer := range result.Answer {
if answer.Type == dnsRecordType {
dnsResult.Ttl = answer.TTL
dnsResult.DnsList = append(dnsResult.DnsList, answer.Data)
}
}
return
}
func DoDns(dnsOptions *DnsOptions, args []string) (err error) {
if len(args) != 1 {
return errors.New("there should be one argument")
}
var dnsResult DnsResult
defer func() {
if dnsOptions.SaveTests {
dir, _ := os.Getwd()
dnsResultName := fmt.Sprintf("dns_result_%v.json", time.Now().Format("20060102150405"))
dnsResultPath := filepath.Join(dir, dnsResultName)
err = builtin.Dump2JSON(dnsResult, dnsResultPath)
if err != nil {
log.Error().Err(err).Msg("save dns resolution result failed")
}
}
}()
dnsTarget := args[0]
parsedURL, err := url.Parse(dnsTarget)
if err == nil && parsedURL.Host != "" {
log.Info().Msgf("parse input url %v and extract host %v", dnsTarget, parsedURL.Host)
dnsTarget = strings.Split(parsedURL.Host, ":")[0]
}
log.Info().Msgf("resolve DNS for %v", dnsTarget)
dnsRecordType := dnsOptions.DnsRecordType
dnsServer := dnsOptions.DnsServer
switch dnsOptions.DnsSourceType {
case DnsSourceTypeLocal:
dnsResult, err = localDns(dnsTarget, dnsRecordType, dnsServer)
case DnsSourceTypeHttp:
dnsResult, err = httpDns(dnsTarget, dnsRecordType)
case DnsSourceTypeGoogle:
dnsResult, err = googleDns(dnsTarget, dnsRecordType)
}
if err != nil {
dnsResult.Suc = false
dnsResult.ErrMsg = err.Error()
log.Error().Err(err).Msgf("fail to do DNS for %s", dnsTarget)
} else {
dnsResult.Suc = true
dnsResult.ErrMsg = ""
fmt.Printf("\nDNS resolution done, result IP list: %v\n", dnsResult.DnsList)
}
return
}

116
hrp/internal/dial/ping.go Normal file
View File

@@ -0,0 +1,116 @@
package dial
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/go-ping/ping"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
type PingOptions struct {
Count int
Timeout time.Duration
Interval time.Duration
SaveTests bool
}
type PingResult struct {
Suc bool `json:"suc"`
ErrMsg string `json:"errMsg"`
Ip string `json:"ip"`
AvgCost int `json:"avgCost"`
MaxCost int `json:"maxCost"`
MinCost int `json:"minCost"`
Lost int `json:"lost"`
PingCount int `json:"pingCount"`
PacketSize int `json:"packetSize"`
ReceivePacketCount int `json:"receivePacketCount"`
SendPacketCount int `json:"sendPacketCount"`
SuccessCount int `json:"successCount"`
DebugLog string `json:"debugLog"`
}
func DoPing(pingOptions *PingOptions, args []string) (err error) {
if len(args) != 1 {
return errors.New("there should be one argument")
}
var pingResult PingResult
defer func() {
if pingOptions.SaveTests {
dir, _ := os.Getwd()
pingResultName := fmt.Sprintf("ping_result_%v.json", time.Now().Format("20060102150405"))
pingResultPath := filepath.Join(dir, pingResultName)
err = builtin.Dump2JSON(pingResult, pingResultPath)
if err != nil {
log.Error().Err(err).Msg("save ping result failed")
}
}
}()
pingTarget := args[0]
parsedURL, err := url.Parse(pingTarget)
if err == nil && parsedURL.Host != "" {
log.Info().Msgf("parse input url %v and extract host %v", pingTarget, parsedURL.Host)
pingTarget = strings.Split(parsedURL.Host, ":")[0]
}
log.Info().Msgf("ping host %v", pingTarget)
pinger, err := ping.NewPinger(pingTarget)
if err != nil {
log.Error().Err(err).Msgf("fail to get pinger for %s", pingTarget)
pingResult.Suc = false
pingResult.ErrMsg = err.Error()
pingResult.DebugLog = err.Error()
return
}
pinger.Count = pingOptions.Count
pinger.Timeout = pingOptions.Timeout
pinger.Interval = pingOptions.Interval
pinger.OnRecv = func(pkt *ping.Packet) {
pingResult.DebugLog += fmt.Sprintf("%d bytes from %s: icmp_seq=%d time=%v\n",
pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt)
}
pinger.OnFinish = func(stats *ping.Statistics) {
pingResult.DebugLog += fmt.Sprintf("\n--- %s ping statistics ---\n", stats.Addr)
pingResult.DebugLog += fmt.Sprintf("%d packets transmitted, %d packets received, %v%% packet loss\n",
stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss)
pingResult.DebugLog += fmt.Sprintf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n",
stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt)
}
pingResult.DebugLog += fmt.Sprintf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr())
err = pinger.Run() // blocks until finished
if err != nil {
log.Error().Err(err).Msgf("fail to run ping for %s", parsedURL)
pingResult.Suc = false
pingResult.ErrMsg = err.Error()
pingResult.DebugLog = err.Error()
return
}
fmt.Print(pingResult.DebugLog)
stats := pinger.Statistics() // get send/receive/rtt stats
pingResult.Ip = pinger.IPAddr().String()
pingResult.AvgCost = int(stats.AvgRtt / time.Millisecond)
pingResult.MaxCost = int(stats.MaxRtt / time.Millisecond)
pingResult.MinCost = int(stats.MinRtt / time.Millisecond)
pingResult.Lost = int(stats.PacketLoss)
pingResult.PingCount = pingOptions.Count
pingResult.PacketSize = pinger.Size
pingResult.ReceivePacketCount = stats.PacketsRecv
pingResult.SendPacketCount = stats.PacketsSent
pingResult.SuccessCount = stats.PacketsRecv
pingResult.Suc = true
pingResult.ErrMsg = ""
return
}

View File

@@ -0,0 +1,20 @@
package dial
type TraceRouteOptions struct {
MaxTTL int
Queries int
SaveTests bool
}
type TraceRouteResult struct {
IP string `json:"ip"`
Details []TraceRouteResultNode `json:"details"`
Suc bool `json:"suc"`
ErrMsg string `json:"errMsg"`
}
type TraceRouteResultNode struct {
Id int `json:"id"`
Ip string `json:"ip"`
Time string `json:"time"`
}

View File

@@ -0,0 +1,106 @@
//go:build darwin || linux
// +build darwin linux
package dial
import (
"bufio"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
var (
regexIPAddr = regexp.MustCompile(`([\d.]+)`)
regexElapsedTime = regexp.MustCompile(`(\d+\.\d+)`)
regexTraceroutePass = regexp.MustCompile(fmt.Sprintf(`(\d+)[\s*]+(\S+)\s+\(%s\)\s+%s\s+ms`, regexIPAddr, regexElapsedTime))
regexTracerouteFailure = regexp.MustCompile(`(\d+)[\s*]+$`)
)
func DoTraceRoute(traceRouteOptions *TraceRouteOptions, args []string) (err error) {
if len(args) != 1 {
return errors.New("there should be one argument")
}
var traceRouteResult TraceRouteResult
defer func() {
if traceRouteOptions.SaveTests {
dir, _ := os.Getwd()
traceRouteResultName := fmt.Sprintf("traceroute_result_%v.json", time.Now().Format("20060102150405"))
traceRouteResultPath := filepath.Join(dir, traceRouteResultName)
err = builtin.Dump2JSON(traceRouteResult, traceRouteResultPath)
if err != nil {
log.Error().Err(err).Msg("save traceroute result failed")
}
}
}()
traceRouteTarget := args[0]
parsedURL, err := url.Parse(traceRouteTarget)
if err == nil && parsedURL.Host != "" {
log.Info().Msgf("parse input url %v and extract host %v", traceRouteTarget, parsedURL.Host)
traceRouteTarget = strings.Split(parsedURL.Host, ":")[0]
}
cmd := exec.Command("traceroute", "-m", strconv.Itoa(traceRouteOptions.MaxTTL),
"-q", strconv.Itoa(traceRouteOptions.Queries), traceRouteTarget)
stdout, _ := cmd.StdoutPipe()
startT := time.Now()
defer func() {
log.Info().Msgf("for target %s, traceroute costs %v", traceRouteTarget, time.Since(startT))
}()
log.Info().Msgf("start to traceroute %v", traceRouteTarget)
err = cmd.Start()
if err != nil {
traceRouteResult.Suc = false
traceRouteResult.ErrMsg = "execute traceroute failed"
log.Error().Err(err).Msg("start command failed")
return
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
hopLine := scanner.Text()
fmt.Println(hopLine)
failureLine := regexTracerouteFailure.FindStringSubmatch(hopLine)
if len(failureLine) == 2 {
hopID, _ := strconv.Atoi(failureLine[1])
traceRouteResult.Details = append(traceRouteResult.Details, TraceRouteResultNode{
Id: hopID,
})
continue
}
passLine := regexTraceroutePass.FindStringSubmatch(hopLine)
if len(passLine) == 5 {
hopID, _ := strconv.Atoi(passLine[1])
traceRouteResult.Details = append(traceRouteResult.Details, TraceRouteResultNode{
Id: hopID,
Ip: passLine[3],
Time: passLine[4],
})
traceRouteResult.Suc = true
}
}
hopCount := len(traceRouteResult.Details)
traceRouteResult.IP = traceRouteResult.Details[hopCount-1].Ip
err = cmd.Wait()
if err != nil {
traceRouteResult.Suc = false
traceRouteResult.ErrMsg = "wait traceroute finish failed"
log.Error().Err(err).Msg("wait command failed")
return
}
return
}

View File

@@ -0,0 +1,105 @@
//go:build windows
// +build windows
package dial
import (
"bufio"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
var (
regexTracertPass = regexp.MustCompile(`(\d+)[\s*<]+(\d+)\s+ms`)
regexTracertFailure = regexp.MustCompile(`(\d+)[\s*]+Request timed out`)
)
func DoTraceRoute(traceRouteOptions *TraceRouteOptions, args []string) (err error) {
if len(args) != 1 {
return errors.New("there should be one argument")
}
var traceRouteResult TraceRouteResult
defer func() {
if traceRouteOptions.SaveTests {
dir, _ := os.Getwd()
traceRouteResultName := fmt.Sprintf("traceroute_result_%v.json", time.Now().Format("20060102150405"))
traceRouteResultPath := filepath.Join(dir, traceRouteResultName)
err = builtin.Dump2JSON(traceRouteResult, traceRouteResultPath)
if err != nil {
log.Error().Err(err).Msg("save traceroute result failed")
}
}
}()
traceRouteTarget := args[0]
parsedURL, err := url.Parse(traceRouteTarget)
if err == nil && parsedURL.Host != "" {
log.Info().Msgf("parse input url %v and extract host %v", traceRouteTarget, parsedURL.Host)
traceRouteTarget = strings.Split(parsedURL.Host, ":")[0]
}
cmd := exec.Command("tracert", "-h", strconv.Itoa(traceRouteOptions.MaxTTL), traceRouteTarget)
stdout, _ := cmd.StdoutPipe()
startT := time.Now()
defer func() {
log.Info().Msgf("for target %s, traceroute costs %v", traceRouteTarget, time.Since(startT))
}()
log.Info().Msgf("start to traceroute %v", traceRouteTarget)
err = cmd.Start()
if err != nil {
traceRouteResult.Suc = false
traceRouteResult.ErrMsg = "execute traceroute failed"
log.Error().Err(err).Msg("start command failed")
return
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
hopLine := scanner.Text()
fmt.Println(hopLine)
failureLine := regexTracertFailure.FindStringSubmatch(hopLine)
if len(failureLine) == 2 {
hopID, _ := strconv.Atoi(failureLine[1])
traceRouteResult.Details = append(traceRouteResult.Details, TraceRouteResultNode{
Id: hopID,
})
continue
}
passLine := regexTracertPass.FindStringSubmatch(hopLine)
if len(passLine) == 3 {
hopID, _ := strconv.Atoi(passLine[1])
fields := strings.Fields(hopLine)
hopIP := strings.Trim(fields[len(fields)-1], "[]")
traceRouteResult.Details = append(traceRouteResult.Details, TraceRouteResultNode{
Id: hopID,
Ip: hopIP,
Time: passLine[2],
})
traceRouteResult.Suc = true
}
}
hopCount := len(traceRouteResult.Details)
traceRouteResult.IP = traceRouteResult.Details[hopCount-1].Ip
err = cmd.Wait()
if err != nil {
traceRouteResult.Suc = false
traceRouteResult.ErrMsg = "wait traceroute finish failed"
log.Error().Err(err).Msg("wait command failed")
return
}
return
}