mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-11 13:18:53 +00:00
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:

committed by
Brad Fitzpatrick

parent
3fb8a1f6bf
commit
1562a6f2f2
@@ -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.
|
||||
|
@@ -20,13 +20,11 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kortschak/wol"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"tailscale.com/drive"
|
||||
@@ -226,6 +224,23 @@ type peerAPIHandler struct {
|
||||
peerUser tailcfg.UserProfile // profile of peerNode
|
||||
}
|
||||
|
||||
// PeerAPIHandler is the interface implemented by [peerAPIHandler] and needed by
|
||||
// module features registered via tailscale.com/feature/*.
|
||||
type PeerAPIHandler interface {
|
||||
Peer() tailcfg.NodeView
|
||||
PeerCaps() tailcfg.PeerCapMap
|
||||
Self() tailcfg.NodeView
|
||||
LocalBackend() *LocalBackend
|
||||
IsSelfUntagged() bool // whether the peer is untagged and the same as this user
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) IsSelfUntagged() bool {
|
||||
return !h.selfNode.IsTagged() && !h.peerNode.IsTagged() && h.isSelf
|
||||
}
|
||||
func (h *peerAPIHandler) Peer() tailcfg.NodeView { return h.peerNode }
|
||||
func (h *peerAPIHandler) Self() tailcfg.NodeView { return h.selfNode }
|
||||
func (h *peerAPIHandler) LocalBackend() *LocalBackend { return h.ps.b }
|
||||
|
||||
func (h *peerAPIHandler) logf(format string, a ...any) {
|
||||
h.ps.b.logf("peerapi: "+format, a...)
|
||||
}
|
||||
@@ -302,6 +317,20 @@ func peerAPIRequestShouldGetSecurityHeaders(r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RegisterPeerAPIHandler registers a PeerAPI handler.
|
||||
//
|
||||
// The path should be of the form "/v0/foo".
|
||||
//
|
||||
// It panics if the path is already registered.
|
||||
func RegisterPeerAPIHandler(path string, f func(PeerAPIHandler, http.ResponseWriter, *http.Request)) {
|
||||
if _, ok := peerAPIHandlers[path]; ok {
|
||||
panic(fmt.Sprintf("duplicate PeerAPI handler %q", path))
|
||||
}
|
||||
peerAPIHandlers[path] = f
|
||||
}
|
||||
|
||||
var peerAPIHandlers = map[string]func(PeerAPIHandler, http.ResponseWriter, *http.Request){} // by URL.Path
|
||||
|
||||
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.validatePeerAPIRequest(r); err != nil {
|
||||
metricInvalidRequests.Add(1)
|
||||
@@ -346,10 +375,6 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case "/v0/dnsfwd":
|
||||
h.handleServeDNSFwd(w, r)
|
||||
return
|
||||
case "/v0/wol":
|
||||
metricWakeOnLANCalls.Add(1)
|
||||
h.handleWakeOnLAN(w, r)
|
||||
return
|
||||
case "/v0/interfaces":
|
||||
h.handleServeInterfaces(w, r)
|
||||
return
|
||||
@@ -364,6 +389,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleServeIngress(w, r)
|
||||
return
|
||||
}
|
||||
if ph, ok := peerAPIHandlers[r.URL.Path]; ok {
|
||||
ph(h, w, r)
|
||||
return
|
||||
}
|
||||
who := h.peerUser.DisplayName
|
||||
fmt.Fprintf(w, `<html>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -624,14 +653,6 @@ func (h *peerAPIHandler) canDebug() bool {
|
||||
return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityDebugPeer)
|
||||
}
|
||||
|
||||
// canWakeOnLAN reports whether h can send a Wake-on-LAN packet from this node.
|
||||
func (h *peerAPIHandler) canWakeOnLAN() bool {
|
||||
if h.peerNode.UnsignedPeerAPIOnly() {
|
||||
return false
|
||||
}
|
||||
return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityWakeOnLAN)
|
||||
}
|
||||
|
||||
var allowSelfIngress = envknob.RegisterBool("TS_ALLOW_SELF_INGRESS")
|
||||
|
||||
// canIngress reports whether h can send ingress requests to this node.
|
||||
@@ -640,10 +661,10 @@ func (h *peerAPIHandler) canIngress() bool {
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
|
||||
return h.peerCaps().HasCapability(wantCap)
|
||||
return h.PeerCaps().HasCapability(wantCap)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) peerCaps() tailcfg.PeerCapMap {
|
||||
func (h *peerAPIHandler) PeerCaps() tailcfg.PeerCapMap {
|
||||
return h.ps.b.PeerCaps(h.remoteAddr.Addr())
|
||||
}
|
||||
|
||||
@@ -817,61 +838,6 @@ func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Reques
|
||||
dh.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleWakeOnLAN(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.canWakeOnLAN() {
|
||||
http.Error(w, "no WoL access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "bad method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
macStr := r.FormValue("mac")
|
||||
if macStr == "" {
|
||||
http.Error(w, "missing 'mac' param", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mac, err := net.ParseMAC(macStr)
|
||||
if err != nil {
|
||||
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
|
||||
st := h.ps.b.sys.NetMon.Get().InterfaceState()
|
||||
if st == nil {
|
||||
http.Error(w, "failed to get interfaces state", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var res struct {
|
||||
SentTo []string
|
||||
Errors []string
|
||||
}
|
||||
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)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) replyToDNSQueries() bool {
|
||||
if h.isSelf {
|
||||
// If the peer is owned by the same user, just allow it
|
||||
@@ -1150,7 +1116,7 @@ func (h *peerAPIHandler) handleServeDrive(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
capsMap := h.peerCaps()
|
||||
capsMap := h.PeerCaps()
|
||||
driveCaps, ok := capsMap[tailcfg.PeerCapabilityTaildrive]
|
||||
if !ok {
|
||||
h.logf("taildrive: not permitted")
|
||||
@@ -1274,8 +1240,7 @@ var (
|
||||
metricInvalidRequests = clientmetric.NewCounter("peerapi_invalid_requests")
|
||||
|
||||
// Non-debug PeerAPI endpoints.
|
||||
metricPutCalls = clientmetric.NewCounter("peerapi_put")
|
||||
metricDNSCalls = clientmetric.NewCounter("peerapi_dns")
|
||||
metricWakeOnLANCalls = clientmetric.NewCounter("peerapi_wol")
|
||||
metricIngressCalls = clientmetric.NewCounter("peerapi_ingress")
|
||||
metricPutCalls = clientmetric.NewCounter("peerapi_put")
|
||||
metricDNSCalls = clientmetric.NewCounter("peerapi_dns")
|
||||
metricIngressCalls = clientmetric.NewCounter("peerapi_ingress")
|
||||
)
|
||||
|
Reference in New Issue
Block a user