diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index b88eda4d8..998536d0d 100644 --- a/cmd/tailscaled/depaware.txt +++ b/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+ diff --git a/go.mod b/go.mod index a527e5161..7c367d245 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index f633a591b..3a25433fa 100644 --- a/go.sum +++ b/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= diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 09e953fb7..3337a8b6c 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/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, ` @@ -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 diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 8c54c7ab8..c555397f2 100644 --- a/tailcfg/tailcfg.go +++ b/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.