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

@ -37,7 +37,7 @@ while [ "$#" -gt 1 ]; do
--extra-small)
shift
ldflags="$ldflags -w -s"
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion"
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan"
;;
--box)
shift

View File

@ -156,7 +156,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd from tailscale.com/util/zstdframe
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
github.com/kortschak/wol from tailscale.com/ipn/ipnlocal
github.com/kortschak/wol from tailscale.com/feature/wakeonlan
github.com/mailru/easyjson/buffer from github.com/mailru/easyjson/jwriter
💣 github.com/mailru/easyjson/jlexer from github.com/go-openapi/swag
github.com/mailru/easyjson/jwriter from github.com/go-openapi/swag
@ -801,6 +801,9 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/drive from tailscale.com/client/tailscale+
tailscale.com/envknob from tailscale.com/client/tailscale+
tailscale.com/envknob/featureknob from tailscale.com/client/web+
tailscale.com/feature from tailscale.com/feature/wakeonlan
tailscale.com/feature/condregister from tailscale.com/tsnet
tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
tailscale.com/hostinfo from tailscale.com/client/web+

View File

@ -127,7 +127,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd from tailscale.com/util/zstdframe
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
github.com/kortschak/wol from tailscale.com/ipn/ipnlocal
github.com/kortschak/wol from tailscale.com/feature/wakeonlan
LD github.com/kr/fs from github.com/pkg/sftp
L github.com/mdlayher/genetlink from tailscale.com/net/tstun
L 💣 github.com/mdlayher/netlink from github.com/google/nftables+
@ -259,6 +259,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/drive/driveimpl/shared from tailscale.com/drive/driveimpl+
tailscale.com/envknob from tailscale.com/client/tailscale+
tailscale.com/envknob/featureknob from tailscale.com/client/web+
tailscale.com/feature from tailscale.com/feature/wakeonlan
tailscale.com/feature/condregister from tailscale.com/cmd/tailscaled
tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
tailscale.com/hostinfo from tailscale.com/client/web+

View File

@ -35,6 +35,7 @@ import (
"tailscale.com/control/controlclient"
"tailscale.com/drive/driveimpl"
"tailscale.com/envknob"
_ "tailscale.com/feature/condregister"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/conffile"

View File

@ -0,0 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The condregister package registers all conditional features guarded
// by build tags. It is one central package that callers can empty import
// to ensure all conditional features are registered.
package condregister

View File

@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_wakeonlan
package condregister
import _ "tailscale.com/feature/wakeonlan"

15
feature/feature.go Normal file
View File

@ -0,0 +1,15 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package feature tracks which features are linked into the binary.
package feature
var in = map[string]bool{}
// Register notes that the named feature is linked into the binary.
func Register(name string) {
if _, ok := in[name]; ok {
panic("duplicate feature registration for " + name)
}
in[name] = true
}

View File

@ -0,0 +1,243 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package wakeonlan registers the Wake-on-LAN feature.
package wakeonlan
import (
"encoding/json"
"log"
"net"
"net/http"
"runtime"
"sort"
"strings"
"unicode"
"github.com/kortschak/wol"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
)
func init() {
feature.Register("wakeonlan")
ipnlocal.RegisterC2N("POST /wol", handleC2NWoL)
ipnlocal.RegisterPeerAPIHandler("/v0/wol", handlePeerAPIWakeOnLAN)
hostinfo.RegisterHostinfoNewHook(func(h *tailcfg.Hostinfo) {
h.WoLMACs = getWoLMACs()
})
}
func handleC2NWoL(b *ipnlocal.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.NetMon().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)
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}
func canWakeOnLAN(h ipnlocal.PeerAPIHandler) bool {
if h.Peer().UnsignedPeerAPIOnly() {
return false
}
return h.IsSelfUntagged() || h.PeerCaps().HasCapability(tailcfg.PeerCapabilityWakeOnLAN)
}
var metricWakeOnLANCalls = clientmetric.NewCounter("peerapi_wol")
func handlePeerAPIWakeOnLAN(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) {
metricWakeOnLANCalls.Add(1)
if !canWakeOnLAN(h) {
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.LocalBackend().NetMon().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)
writeJSON(w, res)
}
// TODO(bradfitz): this is all too simplistic and static. It needs to run
// continuously in response to netmon events (USB ethernet adapters might get
// plugged in) and look for the media type/status/etc. Right now on macOS it
// still detects a half dozen "up" en0, en1, en2, en3 etc interfaces that don't
// have any media. We should only report the one that's actually connected.
// But it works for now (2023-10-05) for fleshing out the rest.
var wakeMAC = envknob.RegisterString("TS_WAKE_MAC") // mac address, "false" or "auto". for https://github.com/tailscale/tailscale/issues/306
// getWoLMACs returns up to 10 MAC address of the local machine to send
// wake-on-LAN packets to in order to wake it up. The returned MACs are in
// lowercase hex colon-separated form ("xx:xx:xx:xx:xx:xx").
//
// If TS_WAKE_MAC=auto, it tries to automatically find the MACs based on the OS
// type and interface properties. (TODO(bradfitz): incomplete) If TS_WAKE_MAC is
// set to a MAC address, that sole MAC address is returned.
func getWoLMACs() (macs []string) {
switch runtime.GOOS {
case "ios", "android":
return nil
}
if s := wakeMAC(); s != "" {
switch s {
case "auto":
ifs, _ := net.Interfaces()
for _, iface := range ifs {
if iface.Flags&net.FlagLoopback != 0 {
continue
}
if iface.Flags&net.FlagBroadcast == 0 ||
iface.Flags&net.FlagRunning == 0 ||
iface.Flags&net.FlagUp == 0 {
continue
}
if keepMAC(iface.Name, iface.HardwareAddr) {
macs = append(macs, iface.HardwareAddr.String())
}
if len(macs) == 10 {
break
}
}
return macs
case "false", "off": // fast path before ParseMAC error
return nil
}
mac, err := net.ParseMAC(s)
if err != nil {
log.Printf("invalid MAC %q", s)
return nil
}
return []string{mac.String()}
}
return nil
}
var ignoreWakeOUI = map[[3]byte]bool{
{0x00, 0x15, 0x5d}: true, // Hyper-V
{0x00, 0x50, 0x56}: true, // VMware
{0x00, 0x1c, 0x14}: true, // VMware
{0x00, 0x05, 0x69}: true, // VMware
{0x00, 0x0c, 0x29}: true, // VMware
{0x00, 0x1c, 0x42}: true, // Parallels
{0x08, 0x00, 0x27}: true, // VirtualBox
{0x00, 0x21, 0xf6}: true, // VirtualBox
{0x00, 0x14, 0x4f}: true, // VirtualBox
{0x00, 0x0f, 0x4b}: true, // VirtualBox
{0x52, 0x54, 0x00}: true, // VirtualBox/Vagrant
}
func keepMAC(ifName string, mac []byte) bool {
if len(mac) != 6 {
return false
}
base := strings.TrimRightFunc(ifName, unicode.IsNumber)
switch runtime.GOOS {
case "darwin":
switch base {
case "llw", "awdl", "utun", "bridge", "lo", "gif", "stf", "anpi", "ap":
return false
}
}
if mac[0] == 0x02 && mac[1] == 0x42 {
// Docker container.
return false
}
oui := [3]byte{mac[0], mac[1], mac[2]}
if ignoreWakeOUI[oui] {
return false
}
return true
}

View File

@ -32,11 +32,19 @@ import (
var started = time.Now()
var newHooks []func(*tailcfg.Hostinfo)
// RegisterHostinfoNewHook registers a callback to be called on a non-nil
// [tailcfg.Hostinfo] before it is returned by [New].
func RegisterHostinfoNewHook(f func(*tailcfg.Hostinfo)) {
newHooks = append(newHooks, f)
}
// New returns a partially populated Hostinfo for the current host.
func New() *tailcfg.Hostinfo {
hostname, _ := os.Hostname()
hostname = dnsname.FirstLabel(hostname)
return &tailcfg.Hostinfo{
hi := &tailcfg.Hostinfo{
IPNVersion: version.Long(),
Hostname: hostname,
App: appTypeCached(),
@ -57,8 +65,11 @@ func New() *tailcfg.Hostinfo {
Cloud: string(cloudenv.Get()),
NoLogsNoSupport: envknob.NoLogsNoSupport(),
AllowsUpdate: envknob.AllowsRemoteUpdate(),
WoLMACs: getWoLMACs(),
}
for _, f := range newHooks {
f(hi)
}
return hi
}
// non-nil on some platforms

View File

@ -1,106 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package hostinfo
import (
"log"
"net"
"runtime"
"strings"
"unicode"
"tailscale.com/envknob"
)
// TODO(bradfitz): this is all too simplistic and static. It needs to run
// continuously in response to netmon events (USB ethernet adapaters might get
// plugged in) and look for the media type/status/etc. Right now on macOS it
// still detects a half dozen "up" en0, en1, en2, en3 etc interfaces that don't
// have any media. We should only report the one that's actually connected.
// But it works for now (2023-10-05) for fleshing out the rest.
var wakeMAC = envknob.RegisterString("TS_WAKE_MAC") // mac address, "false" or "auto". for https://github.com/tailscale/tailscale/issues/306
// getWoLMACs returns up to 10 MAC address of the local machine to send
// wake-on-LAN packets to in order to wake it up. The returned MACs are in
// lowercase hex colon-separated form ("xx:xx:xx:xx:xx:xx").
//
// If TS_WAKE_MAC=auto, it tries to automatically find the MACs based on the OS
// type and interface properties. (TODO(bradfitz): incomplete) If TS_WAKE_MAC is
// set to a MAC address, that sole MAC address is returned.
func getWoLMACs() (macs []string) {
switch runtime.GOOS {
case "ios", "android":
return nil
}
if s := wakeMAC(); s != "" {
switch s {
case "auto":
ifs, _ := net.Interfaces()
for _, iface := range ifs {
if iface.Flags&net.FlagLoopback != 0 {
continue
}
if iface.Flags&net.FlagBroadcast == 0 ||
iface.Flags&net.FlagRunning == 0 ||
iface.Flags&net.FlagUp == 0 {
continue
}
if keepMAC(iface.Name, iface.HardwareAddr) {
macs = append(macs, iface.HardwareAddr.String())
}
if len(macs) == 10 {
break
}
}
return macs
case "false", "off": // fast path before ParseMAC error
return nil
}
mac, err := net.ParseMAC(s)
if err != nil {
log.Printf("invalid MAC %q", s)
return nil
}
return []string{mac.String()}
}
return nil
}
var ignoreWakeOUI = map[[3]byte]bool{
{0x00, 0x15, 0x5d}: true, // Hyper-V
{0x00, 0x50, 0x56}: true, // VMware
{0x00, 0x1c, 0x14}: true, // VMware
{0x00, 0x05, 0x69}: true, // VMware
{0x00, 0x0c, 0x29}: true, // VMware
{0x00, 0x1c, 0x42}: true, // Parallels
{0x08, 0x00, 0x27}: true, // VirtualBox
{0x00, 0x21, 0xf6}: true, // VirtualBox
{0x00, 0x14, 0x4f}: true, // VirtualBox
{0x00, 0x0f, 0x4b}: true, // VirtualBox
{0x52, 0x54, 0x00}: true, // VirtualBox/Vagrant
}
func keepMAC(ifName string, mac []byte) bool {
if len(mac) != 6 {
return false
}
base := strings.TrimRightFunc(ifName, unicode.IsNumber)
switch runtime.GOOS {
case "darwin":
switch base {
case "llw", "awdl", "utun", "bridge", "lo", "gif", "stf", "anpi", "ap":
return false
}
}
if mac[0] == 0x02 && mac[1] == 0x42 {
// Docker container.
return false
}
oui := [3]byte{mac[0], mac[1], mac[2]}
if ignoreWakeOUI[oui] {
return false
}
return true
}

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.

View File

@ -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")
)

View File

@ -29,6 +29,7 @@ import (
"tailscale.com/client/tailscale"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
_ "tailscale.com/feature/condregister"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"

View File

@ -17,6 +17,7 @@ import (
_ "tailscale.com/derp/derphttp"
_ "tailscale.com/drive/driveimpl"
_ "tailscale.com/envknob"
_ "tailscale.com/feature/condregister"
_ "tailscale.com/health"
_ "tailscale.com/hostinfo"
_ "tailscale.com/ipn"

View File

@ -17,6 +17,7 @@ import (
_ "tailscale.com/derp/derphttp"
_ "tailscale.com/drive/driveimpl"
_ "tailscale.com/envknob"
_ "tailscale.com/feature/condregister"
_ "tailscale.com/health"
_ "tailscale.com/hostinfo"
_ "tailscale.com/ipn"

View File

@ -17,6 +17,7 @@ import (
_ "tailscale.com/derp/derphttp"
_ "tailscale.com/drive/driveimpl"
_ "tailscale.com/envknob"
_ "tailscale.com/feature/condregister"
_ "tailscale.com/health"
_ "tailscale.com/hostinfo"
_ "tailscale.com/ipn"

View File

@ -17,6 +17,7 @@ import (
_ "tailscale.com/derp/derphttp"
_ "tailscale.com/drive/driveimpl"
_ "tailscale.com/envknob"
_ "tailscale.com/feature/condregister"
_ "tailscale.com/health"
_ "tailscale.com/hostinfo"
_ "tailscale.com/ipn"

View File

@ -24,6 +24,7 @@ import (
_ "tailscale.com/derp/derphttp"
_ "tailscale.com/drive/driveimpl"
_ "tailscale.com/envknob"
_ "tailscale.com/feature/condregister"
_ "tailscale.com/health"
_ "tailscale.com/hostinfo"
_ "tailscale.com/ipn"