diff options
Diffstat (limited to 'ipn/ipnlocal/tailpipe.go')
| -rw-r--r-- | ipn/ipnlocal/tailpipe.go | 104 |
1 files changed, 104 insertions, 0 deletions
diff --git a/ipn/ipnlocal/tailpipe.go b/ipn/ipnlocal/tailpipe.go new file mode 100644 index 000000000..876f9a0e5 --- /dev/null +++ b/ipn/ipnlocal/tailpipe.go @@ -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 +} |
