From 4a56cbd3107249f6c716338282cceab11bbcca15 Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 21 Aug 2025 14:15:15 +0200 Subject: [PATCH] Fix Wake-on-LAN: Implement custom WOL with OS-specific broadcast support --- go.mod | 2 +- internal/wol/enable_broadcast_unix.go | 20 +++ internal/wol/enable_broadcast_windows.go | 21 +++ internal/wol/wol.go | 220 +++++++++++++++++------ 4 files changed, 209 insertions(+), 54 deletions(-) create mode 100644 internal/wol/enable_broadcast_unix.go create mode 100644 internal/wol/enable_broadcast_windows.go diff --git a/go.mod b/go.mod index 09a1214..cbfe4c6 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/gin-gonic/gin v1.9.1 + golang.org/x/sys v0.9.0 modernc.org/sqlite v1.28.0 ) @@ -33,7 +34,6 @@ require ( golang.org/x/crypto v0.9.0 // indirect golang.org/x/mod v0.8.0 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.9.0 // indirect golang.org/x/text v0.9.0 // indirect golang.org/x/tools v0.6.0 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/internal/wol/enable_broadcast_unix.go b/internal/wol/enable_broadcast_unix.go new file mode 100644 index 0000000..8361119 --- /dev/null +++ b/internal/wol/enable_broadcast_unix.go @@ -0,0 +1,20 @@ +//go:build !windows + +package wol + +import ( + "net" + "syscall" +) + +func enableBroadcast(conn *net.UDPConn, debug bool) error { + raw, err := conn.SyscallConn() + if err != nil { + return err + } + var setErr error + raw.Control(func(fd uintptr) { + setErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1) + }) + return setErr +} diff --git a/internal/wol/enable_broadcast_windows.go b/internal/wol/enable_broadcast_windows.go new file mode 100644 index 0000000..fb60f48 --- /dev/null +++ b/internal/wol/enable_broadcast_windows.go @@ -0,0 +1,21 @@ +//go:build windows + +package wol + +import ( + "net" + + "golang.org/x/sys/windows" +) + +func enableBroadcast(conn *net.UDPConn, debug bool) error { + raw, err := conn.SyscallConn() + if err != nil { + return err + } + var setErr error + raw.Control(func(fd uintptr) { + setErr = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_BROADCAST, 1) + }) + return setErr +} diff --git a/internal/wol/wol.go b/internal/wol/wol.go index 25d5c25..42ee1f0 100644 --- a/internal/wol/wol.go +++ b/internal/wol/wol.go @@ -2,95 +2,209 @@ package wol import ( "encoding/hex" + "fmt" "log" "net" + "os" + "strconv" "strings" ) -// Service ist der Wake-on-LAN Service -type Service struct{} +// Service kapselt WOL-Einstellungen und -Verhalten +type Service struct { + broadcastAddr string + port int + debug bool +} -// NewService erstellt einen neuen Wake-on-LAN Service +// NewService erstellt einen neuen Wake-on-LAN Service und liest Konfiguration +// Konfiguration per Env: +// - WOL_BROADCAST: explizite Broadcast-Adresse (z. B. 192.168.0.255). Wenn leer, werden Interface-Broadcasts verwendet +// - WOL_PORT: Ziel-UDP-Port (Standard 9) +// - WOL_DEBUG: bei "1", "true" (case-insensitive) werden Debug-Logs ausgegeben func NewService() *Service { - return &Service{} + bcast := strings.TrimSpace(os.Getenv("WOL_BROADCAST")) + port := 9 + if v := strings.TrimSpace(os.Getenv("WOL_PORT")); v != "" { + if p, err := strconv.Atoi(v); err == nil && p > 0 && p <= 65535 { + port = p + } + } + debug := false + if v := strings.ToLower(strings.TrimSpace(os.Getenv("WOL_DEBUG"))); v == "1" || v == "true" || v == "yes" { + debug = true + } + return &Service{broadcastAddr: bcast, port: port, debug: debug} } // WakePC sendet ein Wake-on-LAN Paket an die angegebene MAC-Adresse func (s *Service) WakePC(mac string) error { - // MAC-Adresse normalisieren - normalizedMAC := strings.ReplaceAll(mac, ":", "") - normalizedMAC = strings.ReplaceAll(normalizedMAC, "-", "") + cleanMAC := strings.ReplaceAll(strings.ReplaceAll(mac, ":", ""), "-", "") + if s.debug { + log.Printf("WOL: preparing magic packet for MAC %s (clean %s)", mac, cleanMAC) + } - log.Printf("Versende Wake-on-LAN Paket an MAC: %s", mac) - - // Magic Packet erstellen - magicPacket, err := s.createMagicPacket(normalizedMAC) + magicPacket, err := s.createMagicPacket(cleanMAC) if err != nil { log.Printf("Fehler beim Erstellen des Magic Packets: %v", err) return err } - - // UDP-Paket senden - err = s.sendMagicPacket(magicPacket) - if err != nil { - log.Printf("Fehler beim Senden des Wake-on-LAN Pakets: %v", err) - return err + if s.debug { + preview := 12 + if len(magicPacket) < preview { + preview = len(magicPacket) + } + log.Printf("WOL: packet size=%d preview=%s...", len(magicPacket), strings.ToUpper(hex.EncodeToString(magicPacket[:preview]))) } - log.Printf("Wake-on-LAN Paket erfolgreich an %s gesendet", mac) + if s.broadcastAddr != "" { + // Explizite Broadcast-Adresse verwenden + addr := fmt.Sprintf("%s:%d", s.broadcastAddr, s.port) + if s.debug { + log.Printf("WOL: sending to explicit broadcast %s", addr) + } + return s.sendViaUDP(magicPacket, nil, s.broadcastAddr) + } + + // Über alle aktiven Interfaces senden (IPv4 Broadcast) + broadcasts := s.collectInterfaceBroadcasts() + if len(broadcasts) == 0 { + // Fallback: Limited Broadcast + if s.debug { + log.Printf("WOL: no interface broadcasts found, falling back to 255.255.255.255") + } + return s.sendViaUDP(magicPacket, nil, "255.255.255.255") + } + + var firstErr error + var sent int + for _, it := range broadcasts { + laddr := &net.UDPAddr{IP: it.localIP, Port: 0} + if s.debug { + log.Printf("WOL: sending from %s to %s:%d", laddr.IP.String(), it.broadcast.String(), s.port) + } + err := s.sendViaUDP(magicPacket, laddr, it.broadcast.String()) + if err != nil { + if firstErr == nil { + firstErr = err + } + if s.debug { + log.Printf("WOL: send failed on iface %s → %s: %v", laddr.IP.String(), it.broadcast.String(), err) + } + continue + } + sent++ + } + if sent == 0 && firstErr != nil { + return firstErr + } + if s.debug { + log.Printf("WOL: successfully sent on %d interface(s)", sent) + } return nil } -// createMagicPacket erstellt ein Wake-on-LAN Magic Packet -func (s *Service) createMagicPacket(mac string) ([]byte, error) { - // MAC-Adresse in Bytes konvertieren - macBytes, err := hex.DecodeString(mac) +// helper zum Senden über UDP4 (optional gebunden an LocalAddr) +func (s *Service) sendViaUDP(packet []byte, laddr *net.UDPAddr, bcast string) error { + raddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", bcast, s.port)) if err != nil { - return nil, err + return err } - - // Magic Packet: 6 Bytes 0xFF gefolgt von 16 Wiederholungen der MAC-Adresse - magicPacket := make([]byte, 102) - - // Erste 6 Bytes mit 0xFF füllen - for i := 0; i < 6; i++ { - magicPacket[i] = 0xFF - } - - // MAC-Adresse 16 mal wiederholen - for i := 6; i < 102; i += 6 { - copy(magicPacket[i:i+6], macBytes) - } - - return magicPacket, nil -} - -// sendMagicPacket sendet das Magic Packet über UDP -func (s *Service) sendMagicPacket(magicPacket []byte) error { - // UDP-Verbindung erstellen - conn, err := net.Dial("udp", "255.255.255.255:9") + conn, err := net.DialUDP("udp4", laddr, raddr) if err != nil { return err } defer conn.Close() - // Paket senden - _, err = conn.Write(magicPacket) + // Broadcast explizit aktivieren (OS-spezifisch) + if err := enableBroadcast(conn, s.debug); err != nil { + if s.debug { + log.Printf("WOL: enable broadcast failed: %v", err) + } + } + + // Schreiben + n, err := conn.Write(packet) + if s.debug { + log.Printf("WOL: wrote %d bytes to %s", n, raddr.String()) + } return err } +// createMagicPacket erstellt ein Wake-on-LAN Magic Packet +func (s *Service) createMagicPacket(mac string) ([]byte, error) { + macBytes, err := hex.DecodeString(mac) + if err != nil { + return nil, err + } + if len(macBytes) != 6 { + return nil, fmt.Errorf("ungültige MAC-Länge: %d", len(macBytes)) + } + packet := make([]byte, 102) + for i := 0; i < 6; i++ { + packet[i] = 0xFF + } + for i := 6; i < 102; i += 6 { + copy(packet[i:i+6], macBytes) + } + return packet, nil +} + +// ifaceBroadcast repräsentiert eine Broadcast-Adresse plus lokale IP +type ifaceBroadcast struct { + localIP net.IP + broadcast net.IP +} + +// collectInterfaceBroadcasts ermittelt Broadcast-Adressen für alle aktiven IPv4-Interfaces +func (s *Service) collectInterfaceBroadcasts() []ifaceBroadcast { + var result []ifaceBroadcast + ifs, err := net.Interfaces() + if err != nil { + if s.debug { + log.Printf("WOL: failed to list interfaces: %v", err) + } + return result + } + for _, iface := range ifs { + if (iface.Flags&net.FlagUp) == 0 || (iface.Flags&net.FlagLoopback) != 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, a := range addrs { + ipNet, ok := a.(*net.IPNet) + if !ok || ipNet.IP == nil { + continue + } + ip4 := ipNet.IP.To4() + if ip4 == nil { + continue + } + mask := ipNet.Mask + if len(mask) != 4 { + continue + } + bcast := net.IPv4( + ip4[0]|^mask[0], + ip4[1]|^mask[1], + ip4[2]|^mask[2], + ip4[3]|^mask[3], + ) + result = append(result, ifaceBroadcast{localIP: ip4, broadcast: bcast}) + } + } + return result +} + // ValidateMAC prüft, ob die MAC-Adresse gültig ist func ValidateMAC(mac string) bool { - // MAC-Adresse bereinigen - cleanMAC := strings.ReplaceAll(mac, ":", "") - cleanMAC = strings.ReplaceAll(cleanMAC, "-", "") - - // Länge prüfen (12 Hex-Zeichen) + cleanMAC := strings.ReplaceAll(strings.ReplaceAll(mac, ":", ""), "-", "") if len(cleanMAC) != 12 { return false } - - // Prüfen, ob alle Zeichen gültige Hexadezimalziffern sind _, err := hex.DecodeString(cleanMAC) return err == nil }