package operations import ( "fmt" "net" "os" "os/exec" "regexp" "runtime" "strconv" "strings" "time" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "service-operation/types" ) type PingOperation struct { timeout time.Duration } func NewPingOperation(timeout time.Duration) *PingOperation { return &PingOperation{timeout: timeout} } func (p *PingOperation) Execute(host string, count int) (*types.OperationResult, error) { // Validate host/IP if host == "" { return nil, fmt.Errorf("host cannot be empty") } // Always try system ping first for better reliability result, err := p.executeSystemPing(host, count) if err == nil && result.PacketsRecv > 0 { return result, nil } // If system ping fails, try raw ICMP as fallback fmt.Printf("System ping failed (%v), trying raw ICMP\n", err) rawResult, rawErr := p.executeRawICMP(host, count) if rawErr == nil && rawResult.PacketsRecv > 0 { return rawResult, nil } // If both fail, return the system ping result with error info if result != nil { return result, nil } return nil, fmt.Errorf("both system ping and raw ICMP failed: system_err=%v, raw_err=%v", err, rawErr) } func (p *PingOperation) executeSystemPing(host string, count int) (*types.OperationResult, error) { result := &types.OperationResult{ Type: types.OperationPing, Host: host, PacketsSent: count, StartTime: time.Now(), } // Resolve host to get IP address for better details ips, err := net.LookupIP(host) var resolvedIP string if err == nil && len(ips) > 0 { resolvedIP = ips[0].String() } // Build ping command based on OS var cmd *exec.Cmd timeoutSeconds := int(p.timeout.Seconds()) if timeoutSeconds < 1 { timeoutSeconds = 10 // Minimum 10 seconds timeout } switch runtime.GOOS { case "linux": // Linux ping: -c count -W timeout_in_seconds cmd = exec.Command("ping", "-c", fmt.Sprintf("%d", count), "-W", fmt.Sprintf("%d", timeoutSeconds), host) case "darwin": // macOS ping: -c count -W timeout_in_milliseconds cmd = exec.Command("ping", "-c", fmt.Sprintf("%d", count), "-W", fmt.Sprintf("%d", timeoutSeconds*1000), host) case "windows": // Windows ping: -n count -w timeout_in_milliseconds cmd = exec.Command("ping", "-n", fmt.Sprintf("%d", count), "-w", fmt.Sprintf("%d", timeoutSeconds*1000), host) default: // Default to Linux-style cmd = exec.Command("ping", "-c", fmt.Sprintf("%d", count), "-W", fmt.Sprintf("%d", timeoutSeconds), host) } // Set command timeout slightly longer than ping timeout cmdTimeout := time.Duration(timeoutSeconds+5) * time.Second done := make(chan error, 1) var output []byte var cmdErr error go func() { output, cmdErr = cmd.Output() done <- cmdErr }() select { case err := <-done: result.EndTime = time.Now() if err != nil { // Even if command fails, try to parse partial output result.PacketLoss = 100.0 if len(output) > 0 { p.parseSystemPingOutput(result, string(output), count) } // Set detailed error information result.Error = p.createDetailedErrorMessage(err.Error(), host, resolvedIP) return result, nil // Don't return error, return result with failure info } // Parse successful output if len(output) > 0 { p.parseSystemPingOutput(result, string(output), count) } // Create detailed success message if result.Success { result.Details = p.createDetailedSuccessMessage(result, host, resolvedIP) } return result, nil case <-time.After(cmdTimeout): // Command timed out if cmd.Process != nil { cmd.Process.Kill() } result.EndTime = time.Now() result.PacketLoss = 100.0 result.Error = fmt.Sprintf("Ping request timed out after %v", cmdTimeout) result.Details = p.createDetailedErrorMessage("timeout", host, resolvedIP) return result, fmt.Errorf("ping command timed out after %v", cmdTimeout) } } func (p *PingOperation) createDetailedSuccessMessage(result *types.OperationResult, host, resolvedIP string) string { var details strings.Builder // Success indicator with basic info details.WriteString(fmt.Sprintf("🟢 PING SUCCESS - %d/%d packets received", result.PacketsRecv, result.PacketsSent)) // Host and IP information if resolvedIP != "" && resolvedIP != host { details.WriteString(fmt.Sprintf(" | Host: %s (%s)", host, resolvedIP)) } else { details.WriteString(fmt.Sprintf(" | Host: %s", host)) } // Timing information if result.AvgRTT > 0 { details.WriteString(fmt.Sprintf(" | Avg RTT: %.2fms", float64(result.AvgRTT.Nanoseconds())/1000000)) } if result.MinRTT > 0 && result.MaxRTT > 0 { details.WriteString(fmt.Sprintf(" | Range: %.2f-%.2fms", float64(result.MinRTT.Nanoseconds())/1000000, float64(result.MaxRTT.Nanoseconds())/1000000)) } // Packet loss information if result.PacketLoss > 0 { details.WriteString(fmt.Sprintf(" | Packet Loss: %.1f%%", result.PacketLoss)) } else { details.WriteString(" | No packet loss") } return details.String() } func (p *PingOperation) createDetailedErrorMessage(errorMsg, host, resolvedIP string) string { var details strings.Builder errorLower := strings.ToLower(errorMsg) if strings.Contains(errorLower, "timeout") { details.WriteString("⏱️ PING TIMEOUT - Host did not respond within timeout period") } else if strings.Contains(errorLower, "unreachable") { details.WriteString("🚫 HOST UNREACHABLE - Network path to host is blocked") } else if strings.Contains(errorLower, "no route") { details.WriteString("🛤️ NO ROUTE - No network route to destination") } else if strings.Contains(errorLower, "name resolution") || strings.Contains(errorLower, "unknown host") { details.WriteString("🔍 DNS RESOLUTION FAILED - Unable to resolve hostname") } else if strings.Contains(errorLower, "permission denied") { details.WriteString("🔐 PERMISSION DENIED - Insufficient privileges for ICMP") } else { details.WriteString("❌ PING FAILED - Connection error") } // Add host information if resolvedIP != "" && resolvedIP != host { details.WriteString(fmt.Sprintf(" | Target: %s (%s)", host, resolvedIP)) } else { details.WriteString(fmt.Sprintf(" | Target: %s", host)) } // Add specific error details if errorMsg != "" { details.WriteString(fmt.Sprintf(" | Error: %s", p.getShortErrorMessage(errorMsg))) } return details.String() } func (p *PingOperation) getShortErrorMessage(errorMessage string) string { if errorMessage == "" { return "Unknown error" } errorLower := strings.ToLower(errorMessage) if strings.Contains(errorLower, "timeout") { return "Request timeout" } else if strings.Contains(errorLower, "unreachable") { return "Host unreachable" } else if strings.Contains(errorLower, "no route") { return "No route to host" } else if strings.Contains(errorLower, "name resolution") || strings.Contains(errorLower, "unknown host") { return "DNS resolution failed" } else if strings.Contains(errorLower, "permission denied") { return "Permission denied" } else if strings.Contains(errorLower, "network is down") { return "Network is down" } // For other errors, take first 50 characters and clean it up shortMsg := errorMessage if len(shortMsg) > 50 { shortMsg = shortMsg[:50] + "..." } return shortMsg } func (p *PingOperation) parseSystemPingOutput(result *types.OperationResult, output string, expectedCount int) { // Extract packet loss lossRegex := regexp.MustCompile(`(\d+(?:\.\d+)?)%.*packet.*loss`) if matches := lossRegex.FindStringSubmatch(output); len(matches) > 1 { if loss, err := strconv.ParseFloat(matches[1], 64); err == nil { result.PacketLoss = loss } } // Extract timing information (Linux/macOS format) timingRegex := regexp.MustCompile(`rtt min/avg/max/(?:mdev|stddev) = ([\d.]+)/([\d.]+)/([\d.]+)/([\d.]+) ms`) if matches := timingRegex.FindStringSubmatch(output); len(matches) > 4 { if min, err := strconv.ParseFloat(matches[1], 64); err == nil { result.MinRTT = time.Duration(min * float64(time.Millisecond)) } if avg, err := strconv.ParseFloat(matches[2], 64); err == nil { result.AvgRTT = time.Duration(avg * float64(time.Millisecond)) result.ResponseTime = result.AvgRTT } if max, err := strconv.ParseFloat(matches[3], 64); err == nil { result.MaxRTT = time.Duration(max * float64(time.Millisecond)) } } // Extract individual ping times and count successful pings timeRegex := regexp.MustCompile(`time[<=]([\d.]+) ?ms`) timeMatches := timeRegex.FindAllStringSubmatch(output, -1) result.PacketsRecv = len(timeMatches) for _, match := range timeMatches { if len(match) > 1 { if t, err := strconv.ParseFloat(match[1], 64); err == nil { rtt := time.Duration(t * float64(time.Millisecond)) result.RTTs = append(result.RTTs, rtt) // Update min/max if not set by summary stats if result.MinRTT == 0 || rtt < result.MinRTT { result.MinRTT = rtt } if result.MaxRTT == 0 || rtt > result.MaxRTT { result.MaxRTT = rtt } } } } // Calculate average RTT if not set by summary stats if result.AvgRTT == 0 && len(result.RTTs) > 0 { var total time.Duration for _, rtt := range result.RTTs { total += rtt } result.AvgRTT = total / time.Duration(len(result.RTTs)) result.ResponseTime = result.AvgRTT } // Recalculate packet loss if we have individual pings if expectedCount > 0 { result.PacketLoss = float64(expectedCount-result.PacketsRecv) / float64(expectedCount) * 100 } // Mark as successful if we got any responses if result.PacketsRecv > 0 { result.Success = true } // Handle Windows output format if Linux/macOS parsing didn't work if result.PacketsRecv == 0 && strings.Contains(strings.ToLower(output), "reply from") { p.parseWindowsPingOutput(result, output, expectedCount) } } func (p *PingOperation) parseWindowsPingOutput(result *types.OperationResult, output string, expectedCount int) { // Windows: "Reply from 1.1.1.1: bytes=32 time=14ms TTL=56" timeRegex := regexp.MustCompile(`time[<=]([\d.]+)ms`) timeMatches := timeRegex.FindAllStringSubmatch(output, -1) result.PacketsRecv = len(timeMatches) for _, match := range timeMatches { if len(match) > 1 { if t, err := strconv.ParseFloat(match[1], 64); err == nil { rtt := time.Duration(t * float64(time.Millisecond)) result.RTTs = append(result.RTTs, rtt) if result.MinRTT == 0 || rtt < result.MinRTT { result.MinRTT = rtt } if result.MaxRTT == 0 || rtt > result.MaxRTT { result.MaxRTT = rtt } } } } if len(result.RTTs) > 0 { var total time.Duration for _, rtt := range result.RTTs { total += rtt } result.AvgRTT = total / time.Duration(len(result.RTTs)) result.ResponseTime = result.AvgRTT result.Success = true } if expectedCount > 0 { result.PacketLoss = float64(expectedCount-result.PacketsRecv) / float64(expectedCount) * 100 } } func (p *PingOperation) executeRawICMP(host string, count int) (*types.OperationResult, error) { dst, err := net.ResolveIPAddr("ip4", host) if err != nil { return nil, fmt.Errorf("failed to resolve host %s: %v", host, err) } // Try to create ICMP connection with better error handling conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") if err != nil { return nil, fmt.Errorf("failed to create ICMP connection (requires root/admin privileges): %v", err) } defer conn.Close() result := &types.OperationResult{ Type: types.OperationPing, Host: host, PacketsSent: count, StartTime: time.Now(), } var totalRTT time.Duration var minRTT, maxRTT time.Duration pid := os.Getpid() & 0xffff for i := 0; i < count; i++ { message := &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: pid, Seq: i + 1, Data: []byte(fmt.Sprintf("Hello, World! %d", i)), }, } data, err := message.Marshal(nil) if err != nil { continue } start := time.Now() _, err = conn.WriteTo(data, dst) if err != nil { continue } // Set read deadline err = conn.SetReadDeadline(time.Now().Add(p.timeout)) if err != nil { continue } reply := make([]byte, 1500) _, peer, err := conn.ReadFrom(reply) if err != nil { continue } rtt := time.Since(start) // Parse the reply with better error handling - fix the type conversion rm, err := icmp.ParseMessage(int(ipv4.ICMPTypeEchoReply), reply) if err != nil { continue } switch rm.Type { case ipv4.ICMPTypeEchoReply: if peer.String() == dst.String() { echoReply, ok := rm.Body.(*icmp.Echo) if ok && echoReply.ID == pid { result.PacketsRecv++ totalRTT += rtt if result.PacketsRecv == 1 || rtt < minRTT { minRTT = rtt } if result.PacketsRecv == 1 || rtt > maxRTT { maxRTT = rtt } result.RTTs = append(result.RTTs, rtt) } } } // Sleep between pings except for the last one if i < count-1 { time.Sleep(1 * time.Second) } } result.EndTime = time.Now() result.PacketLoss = float64(count-result.PacketsRecv) / float64(count) * 100 if result.PacketsRecv > 0 { result.MinRTT = minRTT result.MaxRTT = maxRTT result.AvgRTT = totalRTT / time.Duration(result.PacketsRecv) result.ResponseTime = result.AvgRTT result.Success = true // Create detailed success message for raw ICMP result.Details = p.createDetailedSuccessMessage(result, host, dst.String()) } else { result.Error = "No ICMP echo replies received" result.Details = p.createDetailedErrorMessage("no replies", host, dst.String()) } return result, nil }