mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-11 21:27:31 +00:00
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:

committed by
Brad Fitzpatrick

parent
b647977b33
commit
fc12cbfcd3
@@ -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
|
||||
|
Reference in New Issue
Block a user