Fix Wake-on-LAN: Implement custom WOL with OS-specific broadcast support
This commit is contained in:
2
go.mod
2
go.mod
@ -4,6 +4,7 @@ go 1.21
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
golang.org/x/sys v0.9.0
|
||||||
modernc.org/sqlite v1.28.0
|
modernc.org/sqlite v1.28.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,7 +34,6 @@ require (
|
|||||||
golang.org/x/crypto v0.9.0 // indirect
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
golang.org/x/mod v0.8.0 // indirect
|
||||||
golang.org/x/net v0.10.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/text v0.9.0 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
|
|||||||
20
internal/wol/enable_broadcast_unix.go
Normal file
20
internal/wol/enable_broadcast_unix.go
Normal file
@ -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
|
||||||
|
}
|
||||||
21
internal/wol/enable_broadcast_windows.go
Normal file
21
internal/wol/enable_broadcast_windows.go
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -2,95 +2,209 @@ package wol
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service ist der Wake-on-LAN Service
|
// Service kapselt WOL-Einstellungen und -Verhalten
|
||||||
type Service struct{}
|
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 {
|
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
|
// WakePC sendet ein Wake-on-LAN Paket an die angegebene MAC-Adresse
|
||||||
func (s *Service) WakePC(mac string) error {
|
func (s *Service) WakePC(mac string) error {
|
||||||
// MAC-Adresse normalisieren
|
cleanMAC := strings.ReplaceAll(strings.ReplaceAll(mac, ":", ""), "-", "")
|
||||||
normalizedMAC := strings.ReplaceAll(mac, ":", "")
|
if s.debug {
|
||||||
normalizedMAC = strings.ReplaceAll(normalizedMAC, "-", "")
|
log.Printf("WOL: preparing magic packet for MAC %s (clean %s)", mac, cleanMAC)
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("Versende Wake-on-LAN Paket an MAC: %s", mac)
|
magicPacket, err := s.createMagicPacket(cleanMAC)
|
||||||
|
|
||||||
// Magic Packet erstellen
|
|
||||||
magicPacket, err := s.createMagicPacket(normalizedMAC)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Fehler beim Erstellen des Magic Packets: %v", err)
|
log.Printf("Fehler beim Erstellen des Magic Packets: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if s.debug {
|
||||||
// UDP-Paket senden
|
preview := 12
|
||||||
err = s.sendMagicPacket(magicPacket)
|
if len(magicPacket) < preview {
|
||||||
if err != nil {
|
preview = len(magicPacket)
|
||||||
log.Printf("Fehler beim Senden des Wake-on-LAN Pakets: %v", err)
|
}
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createMagicPacket erstellt ein Wake-on-LAN Magic Packet
|
// helper zum Senden über UDP4 (optional gebunden an LocalAddr)
|
||||||
func (s *Service) createMagicPacket(mac string) ([]byte, error) {
|
func (s *Service) sendViaUDP(packet []byte, laddr *net.UDPAddr, bcast string) error {
|
||||||
// MAC-Adresse in Bytes konvertieren
|
raddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", bcast, s.port))
|
||||||
macBytes, err := hex.DecodeString(mac)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
conn, err := net.DialUDP("udp4", laddr, raddr)
|
||||||
// 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")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
// Paket senden
|
// Broadcast explizit aktivieren (OS-spezifisch)
|
||||||
_, err = conn.Write(magicPacket)
|
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
|
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
|
// ValidateMAC prüft, ob die MAC-Adresse gültig ist
|
||||||
func ValidateMAC(mac string) bool {
|
func ValidateMAC(mac string) bool {
|
||||||
// MAC-Adresse bereinigen
|
cleanMAC := strings.ReplaceAll(strings.ReplaceAll(mac, ":", ""), "-", "")
|
||||||
cleanMAC := strings.ReplaceAll(mac, ":", "")
|
|
||||||
cleanMAC = strings.ReplaceAll(cleanMAC, "-", "")
|
|
||||||
|
|
||||||
// Länge prüfen (12 Hex-Zeichen)
|
|
||||||
if len(cleanMAC) != 12 {
|
if len(cleanMAC) != 12 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfen, ob alle Zeichen gültige Hexadezimalziffern sind
|
|
||||||
_, err := hex.DecodeString(cleanMAC)
|
_, err := hex.DecodeString(cleanMAC)
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user