Files
crossbar/internal/config/identity.go
T
kyle 8018f67031 Wake: a target may list several broadcast addresses
Config keeps broadcast (one) and adds broadcasts (a list); exactly one must
be present. Wake.Addresses() returns Broadcast then Broadcasts; checkWake
errors on both-set, neither-or-empty, and non-host:port entries. Waker sends
to every address in order, logging/past a failure so one dead address does
not stop the others, and returns false only when none could be sent.

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
2026-09-25 13:45:39 -07:00

102 lines
3.1 KiB
Go

package config
import (
"fmt"
"net"
"time"
)
// Wake is the magic-wake pattern sent to a host to rouse it: its MAC, the
// broadcast address(s) to aim at, and how long to wait for the answer. A host
// that roams between networks names several, so Broadcast (one) and Broadcasts
// (a list) are alternatives: exactly one must be set.
type Wake struct {
MAC string `toml:"mac"`
Broadcast string `toml:"broadcast"`
Broadcasts []string `toml:"broadcasts"`
Wait Duration `toml:"wait"`
}
// Addresses is Broadcast (when set) followed by Broadcasts: the ordered list to
// send wake packets to, never empty for a parsed config.
func (w *Wake) Addresses() []string {
addrs := make([]string, 0, 1+len(w.Broadcasts))
if w.Broadcast != "" {
addrs = append(addrs, w.Broadcast)
}
return append(addrs, w.Broadcasts...)
}
const (
DefaultWakeWait = 45 * time.Second
MinWakeWait = 5 * time.Second
)
// identityMode reports whether s is a recognized identity backend.
func identityMode(s string) bool {
return s == "off" || s == "tailscale" || s == "header"
}
// checkWake validates and defaults the magic-wake pattern of each host that has
// one.
func (c *Config) checkWake() *Error {
for name := range c.Hosts {
h := c.Hosts[name]
w := h.Wake
if w == nil {
continue
}
wakeField := fmt.Sprintf("hosts.%s.wake", name)
mac, err := net.ParseMAC(w.MAC)
if err != nil || len(mac) != 6 {
return &Error{Field: wakeField + ".mac", Msg: "must be a MAC address"}
}
broadcastSet := w.Broadcast != ""
broadcastsSet := len(w.Broadcasts) > 0
switch {
case broadcastSet && broadcastsSet:
return &Error{Field: wakeField + ".broadcasts", Msg: "choose broadcast or broadcasts, not both"}
case !broadcastSet && !broadcastsSet:
return &Error{Field: wakeField + ".broadcast", Msg: "must be a non-empty host:port"}
default:
for _, a := range w.Broadcasts {
if _, _, err := net.SplitHostPort(a); err != nil || a == "" {
return &Error{Field: wakeField + ".broadcasts", Msg: "must be a non-empty host:port"}
}
}
}
if w.Wait.Duration == 0 {
w.Wait.Duration = DefaultWakeWait
} else if w.Wait.Duration < MinWakeWait {
return &Error{Field: wakeField + ".wait", Msg: "must be at least 5s"}
}
h.Wake = w
c.Hosts[name] = h
}
return nil
}
// checkIdentity rejects an unrecognized identity backend.
func (c *Config) checkIdentity() *Error {
if !identityMode(c.Identity) {
return &Error{Field: "identity", Msg: `must be "off", "tailscale", or "header"`}
}
return nil
}
// checkPeers enforces the peers/identity contract for one route: peers may only
// be set with an identity backend on, and may not be an empty list.
func checkPeers(name string, peers []string, peersDefined, identityDefined bool, identity string) *Error {
peersField := fmt.Sprintf("routes.%s.peers", name)
switch {
case len(peers) > 0 && identityDefined && identity == "off":
return &Error{Field: peersField, Msg: "peers need identity = tailscale or header"}
case len(peers) == 0 && peersDefined && identityDefined && identity != "off":
return &Error{Field: peersField, Msg: "empty peers list"}
}
return nil
}