package serveractions import ( "context" "fmt" "net" "os" "os/exec" "regexp" "strings" "time" "golang.org/x/crypto/ssh" ) var Debug bool func LogDebug(format string, v ...interface{}) { if Debug { fmt.Fprintf(os.Stderr, "[DEBUG] "+format+"\n", v...) } } func WakeServer(name string, servers []Server) { for _, s := range servers { if s.Name == name { fmt.Printf("Sending Wake-on-LAN packet to %s (%s)...\n", s.Name, s.Mac) err := sendMagicPacket(s.Mac, s.IP) if err != nil { fmt.Printf("Error sending packet from local network: %v\n", err) } else { fmt.Println("Local WOL packet sent successfully.") } // Try to wake via proxy if there's another online server in the same subnet LogDebug("Checking for online servers in the same subnet to use as a proxy...") for _, other := range servers { if other.Name != s.Name && other.IP != "" && strings.HasPrefix(other.IP, s.IP[:strings.LastIndex(s.IP, ".")]) { // Only use as proxy if we have credentials if other.SSHUser != "" && other.SSHPass != "" { if PingHost(other.IP) { fmt.Printf("Attempting to wake %s via proxy %s...\n", s.Name, other.Name) err := WakeServerRemote(other, s.Mac) if err != nil { LogDebug("Failed to wake via proxy %s: %v", other.Name, err) } else { fmt.Printf("Wake signal sent via proxy %s.\n", other.Name) return } } } } } return } } fmt.Printf("Server '%s' not found in configuration.\n", name) } func WakeServerRemote(proxy Server, targetMac string) error { // Send magic packet from the proxy server using a simple shell command // We use python or similar if available, or just a simple bash command that constructs the packet // constructing the packet in bash: // echo -e $(printf 'f%.0s' {1..12}; printf "$(echo $MAC | sed 's/://g')%.0s" {1..16}) | xxd -r -p | nc -w1 -u -b 255.255.255.255 9 // A simpler way if wakeonlan is installed: wakeonlan $MAC // But let's assume we might need to construct it or use a common tool. // We'll try common tools first. cmd := fmt.Sprintf("wakeonlan %s || ether-wake %s || (printf '%%b' \"$(printf '\\\\xff\\\\xff\\\\xff\\\\xff\\\\xff\\\\xff'; for i in {1..16}; do printf '%s' | sed 's/\\([0-9a-fA-F]\\{2\\}\\)/\\\\x\\1/g'; done)\" | nc -w1 -u -b 255.255.255.255 9)", targetMac, targetMac, targetMac) return executeSSHCommand(proxy, cmd) } func WakeAllServers(servers []Server) { fmt.Println("Waking all servers...") for _, s := range servers { WakeServer(s.Name, servers) } } func CheckServersStatus(servers []Server) { fmt.Println("Checking server status...") for _, s := range servers { if s.IP == "" { fmt.Printf(" - %-20s [SKIPPED] (No IP configured)\n", s.Name) continue } online := PingHost(s.IP) status := "Offline" if online { status = "Online" } fmt.Printf(" - %-20s [%s]\n", s.Name, status) } } func PingHost(host string) bool { LogDebug("Pinging host: %s", host) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "ping", "-c", "1", "-W", "1", host) err := cmd.Run() if err != nil { LogDebug("Ping failed for %s: %v", host, err) } else { LogDebug("Ping successful for %s", host) } return err == nil } // Creates and sends a magic packet to the specified MAC address. // Supports cross-VLAN by sending to both general and subnet-directed broadcast. func sendMagicPacket(macAddr string, ip string) error { hwAddr, err := net.ParseMAC(macAddr) if err != nil { return fmt.Errorf("invalid mac address '%s': %w", macAddr, err) } magicPacket := make([]byte, 102) for i := 0; i < 6; i++ { magicPacket[i] = 0xFF } for i := 1; i <= 16; i++ { copy(magicPacket[i*6:], hwAddr) } // List of addresses to try sending the WOL packet to // List of addresses to try sending the WOL packet to broadcastAddresses := []string{"255.255.255.255"} // If a valid IP is provided, calculate the directed broadcast address (assuming /24 subnet) if ip != "" { ipParts := strings.Split(ip, ".") if len(ipParts) == 4 { directedBroadcast := fmt.Sprintf("%s.%s.%s.255", ipParts[0], ipParts[1], ipParts[2]) if directedBroadcast != "255.255.255.255" { broadcastAddresses = append(broadcastAddresses, directedBroadcast) } broadcastAddresses = append(broadcastAddresses, ip) } } ports := []int{7, 9} for _, addr := range broadcastAddresses { for _, port := range ports { fullAddr := fmt.Sprintf("%s:%d", addr, port) LogDebug("Sending magic packet to %s", fullAddr) for i := 0; i < 3; i++ { conn, err := net.Dial("udp", fullAddr) if err == nil { conn.Write(magicPacket) conn.Close() } time.Sleep(10 * time.Millisecond) } } } return nil } func ShutdownServer(name string, servers []Server) { for _, s := range servers { if s.Name == name { fmt.Printf("Attempting to shutdown %s...\n", s.Name) err := executeSSHCommand(s, "sudo shutdown -h now") if err != nil { fmt.Printf("Error shutting down %s: %v\n", s.Name, err) } else { fmt.Printf("%s is shutting down.\n", s.Name) } return } } fmt.Printf("Server '%s' not found in configuration.\n", name) } func RebootServer(name string, servers []Server) { for _, s := range servers { if s.Name == name { fmt.Printf("Attempting to reboot %s...\n", s.Name) err := executeSSHCommand(s, "sudo reboot") if err != nil { fmt.Printf("Error rebooting %s: %v\n", s.Name, err) } else { fmt.Printf("%s is rebooting.\n", s.Name) } return } } fmt.Printf("Server '%s' not found in configuration.\n", name) } func GetSSHClient(server Server) (*ssh.Client, error) { config := &ssh.ClientConfig{ User: server.SSHUser, Auth: []ssh.AuthMethod{ // Explicitly using ONLY Password and Keyboard-Interactive to avoid // trying local SSH keys which might trigger "Too many attempts" ssh.Password(server.SSHPass), ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) { answers := make([]string, len(questions)) for i := range questions { answers[i] = server.SSHPass } return answers, nil }), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } return ssh.Dial("tcp", server.IP+":22", config) } func executeSSHCommand(server Server, command string) error { client, err := GetSSHClient(server) if err != nil { return fmt.Errorf("failed to connect: %w", err) } defer client.Close() LogDebug("SSH connection established to %s", server.IP) session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session: %w", err) } defer session.Close() LogDebug("Executing SSH command: %s", command) output, err := session.CombinedOutput(command) if err != nil { LogDebug("SSH command failed: %v, Output: %s", err, string(output)) return fmt.Errorf("failed to run command: %w", err) } LogDebug("SSH command output: %s", string(output)) return nil } func GetMacAddress(ip string) (string, error) { // Ping the host to ensure it's in the ARP table PingHost(ip) LogDebug("Looking for MAC address for IP: %s", ip) // Run arp -a to get the ARP table ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "arp", "-a") output, err := cmd.CombinedOutput() if err != nil { LogDebug("arp -a failed: %v", err) return "", fmt.Errorf("error running arp: %v", err) } LogDebug("arp output received (%d bytes)", len(output)) // Regex to find the MAC address for the given IP // Matches: (IP) at MAC or variations lines := strings.Split(string(output), "\n") for _, line := range lines { if strings.Contains(line, "("+ip+")") || strings.Contains(line, " "+ip+" ") { LogDebug("Found matching line in arp table: %s", line) // Extract MAC address - usually looking for 6 pairs of hex digits separated by : or - re := regexp.MustCompile(`([0-9a-fA-F]{1,2}[:\-]){5}[0-9a-fA-F]{1,2}`) mac := re.FindString(line) if mac != "" { LogDebug("Extracted MAC: %s", mac) return mac, nil } } } LogDebug("No MAC address found in arp table for IP %s", ip) return "", fmt.Errorf("MAC address not found for IP %s", ip) } func GetMacAddressSSH(ip, user, pass string) (string, error) { if user == "" || pass == "" { return "", fmt.Errorf("SSH credentials required for remote MAC lookup") } LogDebug("Attempting to get MAC address via SSH for %s@%s", user, ip) server := Server{ IP: ip, SSHUser: user, SSHPass: pass, } // Try common commands to get MAC on Linux/Unix // ip link, ifconfig, etc. cmd := "cat /sys/class/net/$(ip route get 8.8.8.8 | awk '{print $5}')/address 2>/dev/null || ifconfig | grep -E 'ether|HWaddr' | awk '{print $2}' | head -n 1" config := &ssh.ClientConfig{ User: server.SSHUser, Auth: []ssh.AuthMethod{ ssh.Password(server.SSHPass), ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) { answers := make([]string, len(questions)) for i := range questions { answers[i] = server.SSHPass } return answers, nil }), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 5 * time.Second, } client, err := ssh.Dial("tcp", server.IP+":22", config) if err != nil { return "", fmt.Errorf("SSH dial failed: %w", err) } defer client.Close() session, err := client.NewSession() if err != nil { return "", fmt.Errorf("failed to create session: %w", err) } defer session.Close() output, err := session.CombinedOutput(cmd) if err != nil { return "", fmt.Errorf("SSH command failed: %w", err) } mac := strings.TrimSpace(string(output)) // Validate MAC format re := regexp.MustCompile(`([0-9a-fA-F]{1,2}[:\-]){5}[0-9a-fA-F]{1,2}`) mac = re.FindString(mac) if mac == "" { return "", fmt.Errorf("could not extract MAC address from SSH output") } LogDebug("Successfully retrieved MAC via SSH: %s", mac) return mac, nil }