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 }