mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +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:
parent
b647977b33
commit
fc12cbfcd3
@ -19,6 +19,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
@ -31,6 +32,7 @@ import (
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
@ -419,6 +421,60 @@ func SetDNS(ctx context.Context, name, value string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DialTCP connects to the host's port via Tailscale.
|
||||
//
|
||||
// The host may be a base DNS name (resolved from the netmap inside
|
||||
// tailscaled), a FQDN, or an IP address.
|
||||
//
|
||||
// The ctx is only used for the duration of the call, not the lifetime of the net.Conn.
|
||||
func DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
|
||||
connCh := make(chan net.Conn, 1)
|
||||
trace := httptrace.ClientTrace{
|
||||
GotConn: func(info httptrace.GotConnInfo) {
|
||||
connCh <- info.Conn
|
||||
},
|
||||
}
|
||||
ctx = httptrace.WithClientTrace(ctx, &trace)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "http://local-tailscaled.sock/localapi/v0/dial", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = http.Header{
|
||||
"Upgrade": []string{"ts-dial"},
|
||||
"Connection": []string{"upgrade"},
|
||||
"Dial-Host": []string{host},
|
||||
"Dial-Port": []string{fmt.Sprint(port)},
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != http.StatusSwitchingProtocols {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("unexpected HTTP response: %s, %s", res.Status, body)
|
||||
}
|
||||
// From here on, the underlying net.Conn is ours to use, but there
|
||||
// is still a read buffer attached to it within resp.Body. So, we
|
||||
// must direct I/O through resp.Body, but we can still use the
|
||||
// underlying net.Conn for stuff like deadlines.
|
||||
var switchedConn net.Conn
|
||||
select {
|
||||
case switchedConn = <-connCh:
|
||||
default:
|
||||
}
|
||||
if switchedConn == nil {
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("httptrace didn't provide a connection")
|
||||
}
|
||||
rwc, ok := res.Body.(io.ReadWriteCloser)
|
||||
if !ok {
|
||||
res.Body.Close()
|
||||
return nil, errors.New("http Transport did not provide a writable body")
|
||||
}
|
||||
return netutil.NewAltReadWriteCloserConn(rwc, switchedConn), nil
|
||||
}
|
||||
|
||||
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
|
||||
// It is intended to be used with netcheck to see availability of DERPs.
|
||||
func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||
|
@ -170,6 +170,7 @@ change in the future.
|
||||
ipCmd,
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
ncCmd,
|
||||
versionCmd,
|
||||
webCmd,
|
||||
fileCmd,
|
||||
|
@ -56,6 +56,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user