feature/*: make Wake-on-LAN conditional, start supporting modular features

This pulls out the Wake-on-LAN (WoL) code out into its own package
(feature/wakeonlan) that registers itself with various new hooks
around tailscaled.

Then a new build tag (ts_omit_wakeonlan) causes the package to not
even be linked in the binary.

Ohter new packages include:

   * feature: to just record which features are loaded. Future:
     dependencies between features.
   * feature/condregister: the package with all the build tags
     that tailscaled, tsnet, and the Tailscale Xcode project
     extension can empty (underscore) import to load features
     as a function of the defined build tags.

Future commits will move of our "ts_omit_foo" build tags into this
style.

Updates #12614

Change-Id: I9c5378dafb1113b62b816aabef02714db3fc9c4a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-01-22 11:56:36 -08:00
committed by Brad Fitzpatrick
parent 3fb8a1f6bf
commit 1562a6f2f2
18 changed files with 355 additions and 242 deletions

View File

@@ -10,19 +10,16 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"time"
"github.com/kortschak/wol"
"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/ipn"
@@ -66,9 +63,6 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
req("GET /update"): handleC2NUpdateGet,
req("POST /update"): handleC2NUpdatePost,
// Wake-on-LAN.
req("POST /wol"): handleC2NWoL,
// Device posture.
req("GET /posture/identity"): handleC2NPostureIdentityGet,
@@ -82,6 +76,18 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
req("GET /vip-services"): handleC2NVIPServicesGet,
}
// RegisterC2N registers a new c2n handler for the given pattern.
//
// A pattern is like "GET /foo" (specific to an HTTP method) or "/foo" (all
// methods). It panics if the pattern is already registered.
func RegisterC2N(pattern string, h func(*LocalBackend, http.ResponseWriter, *http.Request)) {
k := req(pattern)
if _, ok := c2nHandlers[k]; ok {
panic(fmt.Sprintf("c2n: duplicate handler for %q", pattern))
}
c2nHandlers[k] = h
}
type c2nHandler func(*LocalBackend, http.ResponseWriter, *http.Request)
type methodAndPath struct {
@@ -503,55 +509,6 @@ func regularFileExists(path string) bool {
return err == nil && fi.Mode().IsRegular()
}
func handleC2NWoL(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
r.ParseForm()
var macs []net.HardwareAddr
for _, macStr := range r.Form["mac"] {
mac, err := net.ParseMAC(macStr)
if err != nil {
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
return
}
macs = append(macs, mac)
}
var res struct {
SentTo []string
Errors []string
}
st := b.sys.NetMon.Get().InterfaceState()
if st == nil {
res.Errors = append(res.Errors, "no interface state")
writeJSON(w, &res)
return
}
var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
for _, mac := range macs {
for ifName, ips := range st.InterfaceIPs {
for _, ip := range ips {
if ip.Addr().IsLoopback() || ip.Addr().Is6() {
continue
}
local := &net.UDPAddr{
IP: ip.Addr().AsSlice(),
Port: 0,
}
remote := &net.UDPAddr{
IP: net.IPv4bcast,
Port: 0,
}
if err := wol.Wake(mac, password, local, remote); err != nil {
res.Errors = append(res.Errors, err.Error())
} else {
res.SentTo = append(res.SentTo, ifName)
}
break // one per interface is enough
}
}
}
sort.Strings(res.SentTo)
writeJSON(w, &res)
}
// handleC2NTLSCertStatus returns info about the last TLS certificate issued for the
// provided domain. This can be called by the controlplane to clean up DNS TXT
// records when they're no longer needed by LetsEncrypt.