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 <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2022-04-26 12:29:41 -07:00
committed by Brad Fitzpatrick
parent 3f7cc3563f
commit c88506caa6
5 changed files with 73 additions and 2 deletions

View File

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