Compare commits
1 Commits
main
...
bradfitz/t
Author | SHA1 | Date |
---|---|---|
![]() |
6438ad54b1 |
|
@ -19,6 +19,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptrace"
|
"net/http/httptrace"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -558,6 +559,15 @@ func (lc *LocalClient) SetDNS(ctx context.Context, name, value string) error {
|
||||||
//
|
//
|
||||||
// The ctx is only used for the duration of the call, not the lifetime of the net.Conn.
|
// The ctx is only used for the duration of the call, not the lifetime of the net.Conn.
|
||||||
func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
|
func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
|
||||||
|
return lc.dialViaLocalAPI(ctx, host, fmt.Sprint(port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialTCPNamedPort is like DialTCP but takes a named port rather than an integer.
|
||||||
|
func (lc *LocalClient) DialTCPNamedPort(ctx context.Context, host, portName string) (net.Conn, error) {
|
||||||
|
return lc.dialViaLocalAPI(ctx, host, portName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LocalClient) dialViaLocalAPI(ctx context.Context, host, port string) (net.Conn, error) {
|
||||||
connCh := make(chan net.Conn, 1)
|
connCh := make(chan net.Conn, 1)
|
||||||
trace := httptrace.ClientTrace{
|
trace := httptrace.ClientTrace{
|
||||||
GotConn: func(info httptrace.GotConnInfo) {
|
GotConn: func(info httptrace.GotConnInfo) {
|
||||||
|
@ -573,7 +583,7 @@ func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (n
|
||||||
"Upgrade": []string{"ts-dial"},
|
"Upgrade": []string{"ts-dial"},
|
||||||
"Connection": []string{"upgrade"},
|
"Connection": []string{"upgrade"},
|
||||||
"Dial-Host": []string{host},
|
"Dial-Host": []string{host},
|
||||||
"Dial-Port": []string{fmt.Sprint(port)},
|
"Dial-Port": []string{port},
|
||||||
}
|
}
|
||||||
res, err := lc.DoLocalRequest(req)
|
res, err := lc.DoLocalRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -605,6 +615,59 @@ func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (n
|
||||||
return netutil.NewAltReadWriteCloserConn(rwc, switchedConn), nil
|
return netutil.NewAltReadWriteCloserConn(rwc, switchedConn), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListenNewRandomPortName...
|
||||||
|
func (lc *LocalClient) ListenNewRandomPortName(ctx context.Context) (portName string, accept func(context.Context) (net.Conn, error), err error) {
|
||||||
|
connCh := make(chan net.Conn, 1)
|
||||||
|
portNameCh := make(chan string, 1)
|
||||||
|
trace := httptrace.ClientTrace{
|
||||||
|
GotConn: func(info httptrace.GotConnInfo) {
|
||||||
|
connCh <- info.Conn
|
||||||
|
},
|
||||||
|
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
|
||||||
|
portNameCh <- fmt.Sprintf("Got %v, %v", code, header)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx = httptrace.WithClientTrace(ctx, &trace)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", "http://local-tailscaled.sock/localapi/v0/open-bidi-pipe", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
req.Header = http.Header{
|
||||||
|
"Upgrade": []string{"ts-open-bidi-pipe"},
|
||||||
|
"Connection": []string{"upgrade"},
|
||||||
|
}
|
||||||
|
|
||||||
|
doErrc := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
res, err := lc.DoLocalRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
doErrc <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = res
|
||||||
|
}()
|
||||||
|
|
||||||
|
accept = func(ctx context.Context) (net.Conn, error) {
|
||||||
|
panic("TODO")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case name := <-portNameCh:
|
||||||
|
return name, accept, nil
|
||||||
|
case err := <-doErrc:
|
||||||
|
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)
|
||||||
|
// }
|
||||||
|
panic("TODO")
|
||||||
|
}
|
||||||
|
|
||||||
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
|
// 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.
|
// It is intended to be used with netcheck to see availability of DERPs.
|
||||||
func (lc *LocalClient) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
func (lc *LocalClient) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ import (
|
||||||
|
|
||||||
var Stderr io.Writer = os.Stderr
|
var Stderr io.Writer = os.Stderr
|
||||||
var Stdout io.Writer = os.Stdout
|
var Stdout io.Writer = os.Stdout
|
||||||
|
var Stdin io.Reader = os.Stdin
|
||||||
|
|
||||||
func printf(format string, a ...any) {
|
func printf(format string, a ...any) {
|
||||||
fmt.Fprintf(Stdout, format, a...)
|
fmt.Fprintf(Stdout, format, a...)
|
||||||
|
|
|
@ -7,46 +7,65 @@ package cli
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ncCmd = &ffcli.Command{
|
var ncCmd = &ffcli.Command{
|
||||||
Name: "nc",
|
Name: "nc",
|
||||||
ShortUsage: "nc <hostname-or-IP> <port>",
|
ShortUsage: "nc <hostname-or-IP> <port>\n nc -l",
|
||||||
ShortHelp: "Connect to a port on a host, connected to stdin/stdout",
|
ShortHelp: "Connect to a port on a host, connected to stdin/stdout",
|
||||||
Exec: runNC,
|
Exec: runNC,
|
||||||
|
FlagSet: func() *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet("nc", flag.ExitOnError)
|
||||||
|
fs.BoolVar(&ncArgs.listen, "l", false, "whether to listen for incoming connections (\"Tailpipe\")")
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
}
|
||||||
|
|
||||||
|
var ncArgs struct {
|
||||||
|
listen bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func runNC(ctx context.Context, args []string) error {
|
func runNC(ctx context.Context, args []string) error {
|
||||||
st, err := localClient.Status(ctx)
|
if ncArgs.listen {
|
||||||
if err != nil {
|
if len(args) != 0 {
|
||||||
return fixTailscaledConnectError(err)
|
return errors.New("no arguments supported with -l")
|
||||||
}
|
}
|
||||||
description, ok := isRunningOrStarting(st)
|
return runNCListen(ctx)
|
||||||
if !ok {
|
|
||||||
printf("%s\n", description)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(args) != 2 {
|
if len(args) != 2 {
|
||||||
return errors.New("usage: nc <hostname-or-IP> <port>")
|
return errors.New("usage: nc <hostname-or-IP> <port>")
|
||||||
}
|
}
|
||||||
|
|
||||||
hostOrIP, portStr := args[0], args[1]
|
if _, err := checkRunning(ctx); err != nil {
|
||||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
return err
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid port number %q", portStr)
|
|
||||||
}
|
}
|
||||||
|
hostOrIP, portStr := args[0], args[1]
|
||||||
|
|
||||||
// TODO(bradfitz): also add UDP too, via flag?
|
var c net.Conn
|
||||||
c, err := localClient.DialTCP(ctx, hostOrIP, uint16(port))
|
var err error
|
||||||
|
if strings.HasPrefix(portStr, "tailpipe-") {
|
||||||
|
c, err = localClient.DialTCPNamedPort(ctx, hostOrIP, portStr)
|
||||||
|
} else {
|
||||||
|
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid port number %q", portStr)
|
||||||
|
}
|
||||||
|
// TODO(bradfitz): also add UDP too, via flag?
|
||||||
|
c, err = localClient.DialTCP(ctx, hostOrIP, uint16(port))
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Dial(%q, %v): %w", hostOrIP, port, err)
|
return fmt.Errorf("Dial(%q, %v): %w", hostOrIP, portStr, err)
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
errc := make(chan error, 1)
|
errc := make(chan error, 1)
|
||||||
|
@ -60,3 +79,43 @@ func runNC(ctx context.Context, args []string) error {
|
||||||
}()
|
}()
|
||||||
return <-errc
|
return <-errc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkRunning(ctx context.Context) (*ipnstate.Status, error) {
|
||||||
|
st, err := localClient.Status(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fixTailscaledConnectError(err)
|
||||||
|
}
|
||||||
|
description, ok := isRunningOrStarting(st)
|
||||||
|
if !ok {
|
||||||
|
printf("%s\n", description)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return st, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// runNCLIsten opens a tailpipe.
|
||||||
|
func runNCListen(ctx context.Context) error {
|
||||||
|
st, err := checkRunning(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
portName, accept, err := localClient.ListenNewRandomPortName(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(Stderr, "Port opened. Connect with: nc %v %v\n", st.Self.Addrs[0], portName)
|
||||||
|
c, err := accept(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
errc := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(Stdout, c)
|
||||||
|
errc <- err
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(c, Stdin)
|
||||||
|
errc <- err
|
||||||
|
}()
|
||||||
|
return <-errc
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ipnlocal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptrace"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/net/netutil"
|
||||||
|
"tailscale.com/net/tsaddr"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PipeDialPeerAPIURL ....
|
||||||
|
func (b *LocalBackend) PipeDialPeerAPIURL(ip netip.Addr) (peerAPIURL string, err error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
nm := b.netMap
|
||||||
|
if b.state != ipn.Running || nm == nil {
|
||||||
|
return "", errors.New("not connected to the tailnet")
|
||||||
|
}
|
||||||
|
ipa := netip.PrefixFrom(ip, ip.BitLen())
|
||||||
|
for _, p := range nm.Peers {
|
||||||
|
if slices.Contains(p.Addresses, ipa) {
|
||||||
|
if p.User == nm.User ||
|
||||||
|
len(p.Addresses) > 0 && b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.CapabilityTailpipeTarget) {
|
||||||
|
peerAPI := peerAPIBase(b.netMap, p)
|
||||||
|
if peerAPI == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("invalid target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("target not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) DialTailpipe(ctx context.Context, tailscaleIPStr, portName string) (net.Conn, error) {
|
||||||
|
ip, err := netip.ParseAddr(tailscaleIPStr)
|
||||||
|
if err != nil || !tsaddr.IsTailscaleIP(ip) {
|
||||||
|
return nil, fmt.Errorf("host must be a Tailscale IP for now, not %q", tailscaleIPStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
hc := b.dialer.PeerAPIHTTPClient()
|
||||||
|
|
||||||
|
peerAPIBase, err := b.PipeDialPeerAPIURL(ip)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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", peerAPIBase+"/localapi/v0/connect-to-open-tailpipe", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header = http.Header{
|
||||||
|
"Upgrade": []string{"tailpipe"},
|
||||||
|
"Connection": []string{"upgrade"},
|
||||||
|
"Port-Name": []string{portName},
|
||||||
|
}
|
||||||
|
res, err := hc.Do(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
|
||||||
|
}
|
|
@ -67,6 +67,7 @@ var handler = map[string]localAPIHandler{
|
||||||
"login-interactive": (*Handler).serveLoginInteractive,
|
"login-interactive": (*Handler).serveLoginInteractive,
|
||||||
"logout": (*Handler).serveLogout,
|
"logout": (*Handler).serveLogout,
|
||||||
"metrics": (*Handler).serveMetrics,
|
"metrics": (*Handler).serveMetrics,
|
||||||
|
"open-bidi-pipe": (*Handler).serveOpenBidiPipe,
|
||||||
"ping": (*Handler).servePing,
|
"ping": (*Handler).servePing,
|
||||||
"prefs": (*Handler).servePrefs,
|
"prefs": (*Handler).servePrefs,
|
||||||
"profile": (*Handler).serveProfile,
|
"profile": (*Handler).serveProfile,
|
||||||
|
@ -764,8 +765,16 @@ func (h *Handler) serveDial(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := net.JoinHostPort(hostStr, portStr)
|
dial := func() (net.Conn, error) {
|
||||||
outConn, err := h.b.Dialer().UserDial(r.Context(), "tcp", addr)
|
if strings.HasPrefix(portStr, "tailpipe-") {
|
||||||
|
// hostStr is expected to be a Tailscale IP at this point.
|
||||||
|
return h.b.DialTailpipe(r.Context(), hostStr, portStr)
|
||||||
|
}
|
||||||
|
addr := net.JoinHostPort(hostStr, portStr)
|
||||||
|
return h.b.Dialer().UserDial(r.Context(), "tcp", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
outConn, err := dial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "dial failure: "+err.Error(), http.StatusBadGateway)
|
http.Error(w, "dial failure: "+err.Error(), http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
|
@ -933,6 +942,29 @@ func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write(j)
|
w.Write(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveOpenBidiPipe(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := r.FormValue("name")
|
||||||
|
if name != "" && !h.PermitWrite {
|
||||||
|
http.Error(w, "naming a bidi pipe requires write access", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Foo", "bar")
|
||||||
|
w.WriteHeader(http.StatusProcessing) // informational code 102
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
w.Header().Set("Foo", "baz")
|
||||||
|
w.WriteHeader(http.StatusProcessing) // informational code 102
|
||||||
|
|
||||||
|
w.Header()["Foo"] = nil
|
||||||
|
io.WriteString(w, "the body")
|
||||||
|
}
|
||||||
|
|
||||||
func defBool(a string, def bool) bool {
|
func defBool(a string, def bool) bool {
|
||||||
if a == "" {
|
if a == "" {
|
||||||
return def
|
return def
|
||||||
|
|
|
@ -1647,6 +1647,8 @@ const (
|
||||||
CapabilityDebugPeer = "https://tailscale.com/cap/debug-peer"
|
CapabilityDebugPeer = "https://tailscale.com/cap/debug-peer"
|
||||||
// CapabilityWakeOnLAN grants the ability to send a Wake-On-LAN packet.
|
// CapabilityWakeOnLAN grants the ability to send a Wake-On-LAN packet.
|
||||||
CapabilityWakeOnLAN = "https://tailscale.com/cap/wake-on-lan"
|
CapabilityWakeOnLAN = "https://tailscale.com/cap/wake-on-lan"
|
||||||
|
|
||||||
|
CapabilityTailpipeTarget = "https://tailscale.com/cap/tailpipe-target"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetDNSRequest is a request to add a DNS record.
|
// SetDNSRequest is a request to add a DNS record.
|
||||||
|
|
Loading…
Reference in New Issue