Browse Source

ipn/ipnlocal: add Wake-on-LAN function to peerapi

No CLI support yet. Just the curl'able version if you know the peerapi
port. (like via a TSMP ping)

Updates #306

Change-Id: I0662ba6530f7ab58d0ddb24e3664167fcd1c4bcf
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick 3 years ago
parent
commit
c88506caa6
5 changed files with 73 additions and 2 deletions
  1. 1 0
      cmd/tailscaled/depaware.txt
  2. 1 0
      go.mod
  3. 2 0
      go.sum
  4. 67 2
      ipn/ipnlocal/peerapi.go
  5. 2 0
      tailcfg/tailcfg.go

+ 1 - 0
cmd/tailscaled/depaware.txt

@@ -82,6 +82,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/smallzstd
         github.com/klauspost/compress/zstd/internal/xxhash           from github.com/klauspost/compress/zstd
+        github.com/kortschak/wol                                     from tailscale.com/ipn/ipnlocal
   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/jsimonetti/rtnetlink+

+ 1 - 0
go.mod

@@ -167,6 +167,7 @@ require (
 	github.com/kevinburke/ssh_config v1.1.0 // indirect
 	github.com/kisielk/errcheck v1.6.0 // indirect
 	github.com/kisielk/gotool v1.0.0 // indirect
+	github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
 	github.com/kr/fs v0.1.0 // indirect
 	github.com/kr/pretty v0.3.0 // indirect
 	github.com/kr/text v0.2.0 // indirect

+ 2 - 0
go.sum

@@ -680,6 +680,8 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
+github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=

+ 67 - 2
ipn/ipnlocal/peerapi.go

@@ -29,6 +29,7 @@ import (
 	"unicode"
 	"unicode/utf8"
 
+	"github.com/kortschak/wol"
 	"golang.org/x/net/dns/dnsmessage"
 	"inet.af/netaddr"
 	"tailscale.com/client/tailscale/apitype"
@@ -563,6 +564,9 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	case "/v0/dnsfwd":
 		h.handleServeDNSFwd(w, r)
 		return
+	case "/v0/wol":
+		h.handleWakeOnLAN(w, r)
+		return
 	}
 	who := h.peerUser.DisplayName
 	fmt.Fprintf(w, `<html>
@@ -646,6 +650,11 @@ func (h *peerAPIHandler) canDebug() bool {
 	return h.isSelf || h.peerHasCap(tailcfg.CapabilityDebugPeer)
 }
 
+// canWakeOnLAN reports whether h can send a Wake-on-LAN packet from this node.
+func (h *peerAPIHandler) canWakeOnLAN() bool {
+	return h.isSelf || h.peerHasCap(tailcfg.CapabilityWakeOnLAN)
+}
+
 func (h *peerAPIHandler) peerHasCap(wantCap string) bool {
 	for _, hasCap := range h.ps.b.PeerCaps(h.remoteAddr.IP()) {
 		if hasCap == wantCap {
@@ -836,8 +845,8 @@ func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Reque
 }
 
 func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Request) {
-	if !h.isSelf {
-		http.Error(w, "not owner", http.StatusForbidden)
+	if !h.canDebug() {
+		http.Error(w, "denied; no debug access", http.StatusForbidden)
 		return
 	}
 	dh := health.DebugHandler("dnsfwd")
@@ -848,6 +857,62 @@ 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?
+	st, err := interfaces.GetState()
+	if err != 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.IP().IsLoopback() || ip.IP().Is6() {
+				continue
+			}
+			ipa := ip.IP().IPAddr()
+			local := &net.UDPAddr{
+				IP:   ipa.IP,
+				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

+ 2 - 0
tailcfg/tailcfg.go

@@ -1595,6 +1595,8 @@ const (
 	// CapabilityDebugPeer grants the ability for a peer to read this node's
 	// goroutines, metrics, magicsock internal state, etc.
 	CapabilityDebugPeer = "https://tailscale.com/cap/debug-peer"
+	// CapabilityWakeOnLAN grants the ability to send a Wake-On-LAN packet.
+	CapabilityWakeOnLAN = "https://tailscale.com/cap/wake-on-lan"
 )
 
 // SetDNSRequest is a request to add a DNS record.