From 1f99f889e161f2c6330861b82616e8230956a7b4 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 6 Apr 2021 13:38:47 -0700 Subject: [PATCH] ipn/{ipnlocal,localapi}: add localapi handler to dial/proxy file PUTs Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/depaware.txt | 5 +- ipn/ipnlocal/local.go | 14 +++++ ipn/ipnlocal/peerapi_macios_ext.go | 50 ++++++++++++++---- ipn/localapi/localapi.go | 84 ++++++++++++++++++++++++++++-- 4 files changed, 138 insertions(+), 15 deletions(-) diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index bd1490d40..e8a5c024b 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -163,7 +163,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+ golang.org/x/net/bpf from github.com/mdlayher/netlink+ golang.org/x/net/dns/dnsmessage from net+ - golang.org/x/net/http/httpguts from net/http + golang.org/x/net/http/httpguts from net/http+ golang.org/x/net/http/httpproxy from net/http golang.org/x/net/http2/hpack from net/http golang.org/x/net/idna from golang.org/x/net/http/httpguts+ @@ -247,7 +247,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de net from crypto/tls+ net/http from expvar+ net/http/httptrace from github.com/tcnksm/go-httpstat+ - net/http/internal from net/http + net/http/httputil from tailscale.com/ipn/localapi + net/http/internal from net/http+ net/http/pprof from tailscale.com/cmd/tailscaled net/textproto from golang.org/x/net/http/httpguts+ net/url from crypto/x509+ diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index b747cef3d..7f05d7dc7 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -19,6 +19,7 @@ import ( "strings" "sync" "sync/atomic" + "syscall" "time" "inet.af/netaddr" @@ -2155,3 +2156,16 @@ func (b *LocalBackend) CheckIPForwarding() error { } return nil } + +// peerDialControlFunc is non-nil on platforms that require a way to +// bind to dial out to other peers. +var peerDialControlFunc func(*LocalBackend) func(network, address string, c syscall.RawConn) error + +// PeerDialControlFunc returns a net.Dialer.Control func (possibly nil) to use to +// dial other Tailscale peers from the current environment. +func (b *LocalBackend) PeerDialControlFunc() func(network, address string, c syscall.RawConn) error { + if peerDialControlFunc != nil { + return peerDialControlFunc(b) + } + return nil +} diff --git a/ipn/ipnlocal/peerapi_macios_ext.go b/ipn/ipnlocal/peerapi_macios_ext.go index a75e18eed..4915d5581 100644 --- a/ipn/ipnlocal/peerapi_macios_ext.go +++ b/ipn/ipnlocal/peerapi_macios_ext.go @@ -7,6 +7,7 @@ package ipnlocal import ( + "errors" "fmt" "log" "net" @@ -20,6 +21,7 @@ import ( func init() { initListenConfig = initListenConfigNetworkExtension + peerDialControlFunc = peerDialControlFuncNetworkExtension } // initListenConfigNetworkExtension configures nc for listening on IP @@ -33,16 +35,7 @@ func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *i nc.Control = func(network, address string, c syscall.RawConn) error { var sockErr error err := c.Control(func(fd uintptr) { - - v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6 - proto := unix.IPPROTO_IP - opt := unix.IP_BOUND_IF - if v6 { - proto = unix.IPPROTO_IPV6 - opt = unix.IPV6_BOUND_IF - } - - sockErr = unix.SetsockoptInt(int(fd), proto, opt, tunIf.Index) + sockErr = bindIf(fd, network, address, tunIf.Index) log.Printf("peerapi: bind(%q, %q) on index %v = %v", network, address, tunIf.Index, sockErr) }) if err != nil { @@ -52,3 +45,40 @@ func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *i } return nil } + +func bindIf(fd uintptr, network, address string, ifIndex int) error { + v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6 + proto := unix.IPPROTO_IP + opt := unix.IP_BOUND_IF + if v6 { + proto = unix.IPPROTO_IPV6 + opt = unix.IPV6_BOUND_IF + } + return unix.SetsockoptInt(int(fd), proto, opt, ifIndex) +} + +func peerDialControlFuncNetworkExtension(b *LocalBackend) func(network, address string, c syscall.RawConn) error { + b.mu.Lock() + defer b.mu.Unlock() + st := b.prevIfState + pas := b.peerAPIServer + index := -1 + if st != nil && pas != nil && pas.tunName != "" { + if tunIf, ok := st.Interface[pas.tunName]; ok { + index = tunIf.Index + } + } + return func(network, address string, c syscall.RawConn) error { + if index == -1 { + return errors.New("failed to find TUN interface to bind to") + } + var sockErr error + err := c.Control(func(fd uintptr) { + sockErr = bindIf(fd, network, address, index) + }) + if err != nil { + return err + } + return sockErr + } +} diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 9a6240608..2478ee8b5 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -11,12 +11,15 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" + "net/http/httputil" "net/url" "reflect" "runtime" "strconv" "strings" + "sync" "time" "inet.af/netaddr" @@ -73,6 +76,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.serveFiles(w, r) return } + if strings.HasPrefix(r.URL.Path, "/localapi/v0/file-put/") { + h.serveFilePut(w, r) + return + } switch r.URL.Path { case "/localapi/v0/whois": h.serveWhoIs(w, r) @@ -243,14 +250,85 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) { http.Error(w, "want GET to list targets", 400) return } - wfs, err := h.b.FileTargets() + fts, err := h.b.FileTargets() if err != nil { http.Error(w, err.Error(), 500) return } - makeNonNil(&wfs) + makeNonNil(&fts) w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(wfs) + json.NewEncoder(w).Encode(fts) +} + +func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) { + if !h.PermitWrite { + http.Error(w, "file access denied", http.StatusForbidden) + return + } + if r.Method != "PUT" { + http.Error(w, "want PUT to put file", 400) + return + } + fts, err := h.b.FileTargets() + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + upath := strings.TrimPrefix(r.URL.EscapedPath(), "/localapi/v0/file-put/") + slash := strings.Index(upath, "/") + if slash == -1 { + http.Error(w, "bogus URL", 400) + return + } + stableID, filenameEscaped := tailcfg.StableNodeID(upath[:slash]), upath[slash+1:] + + var ft *ipnlocal.FileTarget + for _, x := range fts { + if x.Node.StableID == stableID { + ft = x + break + } + } + if ft == nil { + http.Error(w, "node not found", 404) + return + } + dstURL, err := url.Parse(ft.PeerAPIURL) + if err != nil { + http.Error(w, "bogus peer URL", 500) + return + } + outReq, err := http.NewRequestWithContext(r.Context(), "PUT", "http://peer/v0/put/"+filenameEscaped, r.Body) + if err != nil { + http.Error(w, "bogus outreq", 500) + return + } + outReq.ContentLength = r.ContentLength + + rp := httputil.NewSingleHostReverseProxy(dstURL) + rp.Transport = getDialPeerTransport(h.b) + rp.ServeHTTP(w, outReq) +} + +var dialPeerTransportOnce struct { + sync.Once + v *http.Transport +} + +func getDialPeerTransport(b *ipnlocal.LocalBackend) *http.Transport { + dialPeerTransportOnce.Do(func() { + t := http.DefaultTransport.(*http.Transport).Clone() + t.Dial = nil //lint:ignore SA1019 yes I know I'm setting it to nil defensively + dialer := net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + Control: b.PeerDialControlFunc(), + } + t.DialContext = dialer.DialContext + dialPeerTransportOnce.v = t + }) + return dialPeerTransportOnce.v } func defBool(a string, def bool) bool {