5d5b6c99a7
Fix Wake-on-LAN: set SO_BROADCAST on UDP socket, send 3 magic packets, expose broadcast_addr and wake timeout fields in UI, add Test Wake endpoint, surface send errors in job status, bump default wake timeout to 180s.
100 lines
2.2 KiB
Go
100 lines
2.2 KiB
Go
package wol
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"regexp"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
var macRegex = regexp.MustCompile(`^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$`)
|
|
|
|
func ParseMAC(s string) ([6]byte, error) {
|
|
s = strings.ReplaceAll(s, "-", ":")
|
|
s = strings.ToLower(s)
|
|
if !macRegex.MatchString(s) {
|
|
return [6]byte{}, fmt.Errorf("invalid MAC address: %s", s)
|
|
}
|
|
parts := strings.Split(s, ":")
|
|
var mac [6]byte
|
|
for i := 0; i < 6; i++ {
|
|
var b int
|
|
if _, err := fmt.Sscanf(parts[i], "%x", &b); err != nil {
|
|
return [6]byte{}, fmt.Errorf("invalid MAC address: %s", s)
|
|
}
|
|
mac[i] = byte(b)
|
|
}
|
|
return mac, nil
|
|
}
|
|
|
|
func FormatMAC(mac [6]byte) string {
|
|
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
|
|
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
|
|
}
|
|
|
|
func BuildMagicPacket(mac [6]byte) []byte {
|
|
packet := make([]byte, 6+16*6)
|
|
for i := 0; i < 6; i++ {
|
|
packet[i] = 0xFF
|
|
}
|
|
for i := 0; i < 16; i++ {
|
|
offset := 6 + i*6
|
|
copy(packet[offset:offset+6], mac[:])
|
|
}
|
|
return packet
|
|
}
|
|
|
|
func Send(mac [6]byte, broadcastAddr string) error {
|
|
packet := BuildMagicPacket(mac)
|
|
|
|
udpAddr := &net.UDPAddr{
|
|
IP: net.IPv4bcast,
|
|
Port: 9,
|
|
}
|
|
if broadcastAddr != "" {
|
|
udpAddr.IP = net.ParseIP(broadcastAddr)
|
|
if udpAddr.IP == nil {
|
|
return fmt.Errorf("invalid broadcast address: %s", broadcastAddr)
|
|
}
|
|
}
|
|
|
|
conn, err := net.DialUDP("udp4", nil, udpAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("creating UDP connection: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
sc, err := conn.SyscallConn()
|
|
if err != nil {
|
|
return fmt.Errorf("getting syscall conn: %w", err)
|
|
}
|
|
var setErr error
|
|
if err := sc.Control(func(fd uintptr) {
|
|
setErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1)
|
|
}); err != nil {
|
|
return fmt.Errorf("calling control: %w", err)
|
|
}
|
|
if setErr != nil {
|
|
return fmt.Errorf("enabling broadcast: %w", setErr)
|
|
}
|
|
if err := conn.SetWriteDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
|
return fmt.Errorf("setting write deadline: %w", err)
|
|
}
|
|
|
|
for i := 0; i < 3; i++ {
|
|
n, err := conn.Write(packet)
|
|
if err != nil {
|
|
return fmt.Errorf("sending magic packet: %w", err)
|
|
}
|
|
if n != len(packet) {
|
|
return fmt.Errorf("incomplete write: sent %d/%d bytes", n, len(packet))
|
|
}
|
|
if i < 2 {
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
}
|
|
return nil
|
|
}
|