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>
pull/4539/head
Brad Fitzpatrick 2 years ago committed by Brad Fitzpatrick
parent 3f7cc3563f
commit c88506caa6

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

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

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

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

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

Loading…
Cancel
Save