client/tailscale, cmd/tailscale, localapi: add 'tailscale nc'

This adds a "tailscale nc" command that acts a bit like "nc", but
dials out via tailscaled via localapi.

This is a step towards a "tailscale ssh", as we'll use "tailscale nc"
as a ProxyCommand for in some cases (notably in userspace mode).

But this is also just useful for debugging & scripting.

Updates #3802

RELNOTE=tailscale nc

Change-Id: Ia5c37af2d51dd0259d5833d80264d3ad5f68446a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2022-03-24 09:04:01 -07:00
committed by Brad Fitzpatrick
parent b647977b33
commit fc12cbfcd3
4 changed files with 119 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
@@ -26,6 +27,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
@@ -124,6 +126,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveDebug(w, r)
case "/localapi/v0/set-expiry-sooner":
h.serveSetExpirySooner(w, r)
case "/localapi/v0/dial":
h.serveDial(w, r)
case "/":
io.WriteString(w, "tailscaled\n")
default:
@@ -542,6 +546,63 @@ func (h *Handler) serveSetExpirySooner(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "done\n")
}
func (h *Handler) serveDial(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
}
const upgradeProto = "ts-dial"
if !strings.Contains(r.Header.Get("Connection"), "upgrade") ||
r.Header.Get("Upgrade") != upgradeProto {
http.Error(w, "bad ts-dial upgrade", http.StatusBadRequest)
return
}
hostStr, portStr := r.Header.Get("Dial-Host"), r.Header.Get("Dial-Port")
if hostStr == "" || portStr == "" {
http.Error(w, "missing Dial-Host or Dial-Port header", http.StatusBadRequest)
return
}
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "make request over HTTP/1", http.StatusBadRequest)
return
}
addr := net.JoinHostPort(hostStr, portStr)
outConn, err := h.b.Dialer().UserDial(r.Context(), "tcp", addr)
if err != nil {
http.Error(w, "dial failure: "+err.Error(), http.StatusBadGateway)
return
}
defer outConn.Close()
w.Header().Set("Upgrade", upgradeProto)
w.Header().Set("Connection", "upgrade")
w.WriteHeader(http.StatusSwitchingProtocols)
reqConn, brw, err := hijacker.Hijack()
if err != nil {
h.logf("localapi dial Hijack error: %v", err)
return
}
defer reqConn.Close()
if err := brw.Flush(); err != nil {
return
}
reqConn = netutil.NewDrainBufConn(reqConn, brw.Reader)
errc := make(chan error, 1)
go func() {
_, err := io.Copy(reqConn, outConn)
errc <- err
}()
go func() {
_, err := io.Copy(outConn, reqConn)
errc <- err
}()
<-errc
}
func defBool(a string, def bool) bool {
if a == "" {
return def