You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/ipn/ipnlocal/serve.go

565 lines
15 KiB
Go

// 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"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/netip"
"net/url"
"os"
"path"
pathpkg "path"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/exp/slices"
"tailscale.com/ipn"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netutil"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/mak"
"tailscale.com/util/strs"
)
// serveHTTPContextKey is the context.Value key for a *serveHTTPContext.
type serveHTTPContextKey struct{}
type serveHTTPContext struct {
SrcAddr netip.AddrPort
DestPort uint16
}
// serveListener is the state of host-level net.Listen for a specific (Tailscale IP, serve port)
// combination. If there are two TailscaleIPs (v4 and v6) and three ports being served,
// then there will be six of these active and looping in their Run method.
//
// This is not used in userspace-networking mode.
//
// Most serve traffic is intercepted by netstack. This exists purely for connections
// from the machine itself, as that goes via the kernel, so we need to be in the
// kernel's listening/routing tables.
type serveListener struct {
b *LocalBackend
ap netip.AddrPort
ctx context.Context // valid while listener is desired
cancel context.CancelFunc // for ctx, to close listener
logf logger.Logf
bo *backoff.Backoff // for retrying failed Listen calls
closeListener syncs.AtomicValue[func() error] // Listener's Close method, if any
}
func (b *LocalBackend) newServeListener(ctx context.Context, ap netip.AddrPort, logf logger.Logf) *serveListener {
ctx, cancel := context.WithCancel(ctx)
return &serveListener{
b: b,
ap: ap,
ctx: ctx,
cancel: cancel,
logf: logf,
bo: backoff.NewBackoff("serve-listener", logf, 30*time.Second),
}
}
// Close cancels the context and closes the listener, if any.
func (s *serveListener) Close() error {
s.cancel()
if close, ok := s.closeListener.LoadOk(); ok {
s.closeListener.Store(nil)
close()
}
return nil
}
// Run starts a net.Listen for the serveListener's address and port.
// If unable to listen, it retries with exponential backoff.
// Listen is retried until the context is canceled.
func (s *serveListener) Run() {
for {
ln, err := net.Listen("tcp", s.ap.String())
if err != nil {
if s.shouldWarnAboutListenError(err) {
s.logf("serve failed to listen on %v, backing off: %v", s.ap, err)
}
s.bo.BackOff(s.ctx, err)
continue
}
s.closeListener.Store(ln.Close)
s.logf("serve listening on %v", s.ap)
err = s.handleServeListenersAccept(ln)
if s.ctx.Err() != nil {
// context canceled, we're done
return
}
if err != nil {
s.logf("serve listener accept error, retrying: %v", err)
}
}
}
func (s *serveListener) shouldWarnAboutListenError(err error) bool {
if !s.b.e.GetLinkMonitor().InterfaceState().HasIP(s.ap.Addr()) {
// Machine likely doesn't have IPv6 enabled (or the IP is still being
// assigned). No need to warn. Notably, WSL2 (Issue 6303).
return false
}
// TODO(bradfitz): check errors.Is(err, syscall.EADDRNOTAVAIL) etc? Let's
// see what happens in practice.
return true
}
// handleServeListenersAccept accepts connections for the Listener.
// Calls incoming handler in a new goroutine for each accepted connection.
func (s *serveListener) handleServeListenersAccept(ln net.Listener) error {
for {
conn, err := ln.Accept()
if err != nil {
return err
}
srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort()
getConn := func() (net.Conn, bool) { return conn, true }
sendRST := func() {
s.b.logf("serve RST for %v", srcAddr)
conn.Close()
}
go s.b.HandleInterceptedTCPConn(s.ap.Port(), srcAddr, getConn, sendRST)
}
}
// updateServeTCPPortNetMapAddrListenersLocked starts a net.Listen for configured
// Serve ports on all the node's addresses.
// Existing Listeners are closed if port no longer in incoming ports list.
//
// b.mu must be held.
func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint16) {
// close existing listeners where port
// is no longer in incoming ports list
for ap, sl := range b.serveListeners {
if !slices.Contains(ports, ap.Port()) {
b.logf("closing listener %v", ap)
sl.Close()
delete(b.serveListeners, ap)
}
}
nm := b.netMap
if nm == nil {
b.logf("netMap is nil")
return
}
if nm.SelfNode == nil {
b.logf("netMap SelfNode is nil")
return
}
for _, a := range nm.Addresses {
for _, p := range ports {
addrPort := netip.AddrPortFrom(a.Addr(), p)
if _, ok := b.serveListeners[addrPort]; ok {
continue // already listening
}
sl := b.newServeListener(context.Background(), addrPort, b.logf)
mak.Set(&b.serveListeners, addrPort, sl)
go sl.Run()
}
}
}
// SetServeConfig establishes or replaces the current serve config.
func (b *LocalBackend) SetServeConfig(config *ipn.ServeConfig) error {
b.mu.Lock()
defer b.mu.Unlock()
nm := b.netMap
if nm == nil {
return errors.New("netMap is nil")
}
if nm.SelfNode == nil {
return errors.New("netMap SelfNode is nil")
}
profileID := b.pm.CurrentProfile().ID
confKey := ipn.ServeConfigKey(profileID)
var bs []byte
if config != nil {
j, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("encoding serve config: %w", err)
}
bs = j
}
if err := b.store.WriteState(confKey, bs); err != nil {
return fmt.Errorf("writing ServeConfig to StateStore: %w", err)
}
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
return nil
}
// ServeConfig provides a view of the current serve mappings.
// If serving is not configured, the returned view is not Valid.
func (b *LocalBackend) ServeConfig() ipn.ServeConfigView {
b.mu.Lock()
defer b.mu.Unlock()
return b.serveConfig
}
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ipn.HostPort, srcAddr netip.AddrPort, getConn func() (net.Conn, bool), sendRST func()) {
b.mu.Lock()
sc := b.serveConfig
b.mu.Unlock()
if !sc.Valid() {
b.logf("localbackend: got ingress conn w/o serveConfig; rejecting")
sendRST()
return
}
if !sc.AllowIngress().Get(target) {
b.logf("localbackend: got ingress conn for unconfigured %q; rejecting", target)
sendRST()
return
}
_, port, err := net.SplitHostPort(string(target))
if err != nil {
b.logf("localbackend: got ingress conn for bad target %q; rejecting", target)
sendRST()
return
}
port16, err := strconv.ParseUint(port, 10, 16)
if err != nil {
b.logf("localbackend: got ingress conn for bad target %q; rejecting", target)
sendRST()
return
}
// TODO(bradfitz): pass ingressPeer etc in context to HandleInterceptedTCPConn,
// extend serveHTTPContext or similar.
b.HandleInterceptedTCPConn(uint16(port16), srcAddr, getConn, sendRST)
}
func (b *LocalBackend) HandleInterceptedTCPConn(dport uint16, srcAddr netip.AddrPort, getConn func() (net.Conn, bool), sendRST func()) {
b.mu.Lock()
sc := b.serveConfig
b.mu.Unlock()
if !sc.Valid() {
b.logf("[unexpected] localbackend: got TCP conn w/o serveConfig; from %v to port %v", srcAddr, dport)
sendRST()
return
}
tcph, ok := sc.TCP().GetOk(dport)
if !ok {
b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr)
sendRST()
return
}
if tcph.HTTPS() {
conn, ok := getConn()
if !ok {
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
return
}
hs := &http.Server{
TLSConfig: &tls.Config{
GetCertificate: b.getTLSServeCertForPort(dport),
},
Handler: http.HandlerFunc(b.serveWebHandler),
BaseContext: func(_ net.Listener) context.Context {
return context.WithValue(context.Background(), serveHTTPContextKey{}, &serveHTTPContext{
SrcAddr: srcAddr,
DestPort: dport,
})
},
}
hs.ServeTLS(netutil.NewOneConnListener(conn, nil), "", "")
return
}
if backDst := tcph.TCPForward(); backDst != "" {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
cancel()
if err != nil {
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
sendRST()
return
}
conn, ok := getConn()
if !ok {
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
backConn.Close()
return
}
defer conn.Close()
defer backConn.Close()
if sni := tcph.TerminateTLS(); sni != "" {
conn = tls.Server(conn, &tls.Config{
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
pair, err := b.GetCertPEM(ctx, sni)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
if err != nil {
return nil, err
}
return &cert, nil
},
})
}
// TODO(bradfitz): do the RegisterIPPortIdentity and
// UnregisterIPPortIdentity stuff that netstack does
errc := make(chan error, 1)
go func() {
_, err := io.Copy(backConn, conn)
errc <- err
}()
go func() {
_, err := io.Copy(conn, backConn)
errc <- err
}()
<-errc
return
}
b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr)
sendRST()
}
func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView, at string, ok bool) {
var z ipn.HTTPHandlerView // zero value
if r.TLS == nil {
return z, "", false
}
sctx, ok := r.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext)
if !ok {
b.logf("[unexpected] localbackend: no serveHTTPContext in request")
return z, "", false
}
wsc, ok := b.webServerConfig(r.TLS.ServerName, sctx.DestPort)
if !ok {
return z, "", false
}
if h, ok := wsc.Handlers().GetOk(r.URL.Path); ok {
return h, r.URL.Path, true
}
path := path.Clean(r.URL.Path)
for {
withSlash := path + "/"
if h, ok := wsc.Handlers().GetOk(withSlash); ok {
return h, withSlash, true
}
if h, ok := wsc.Handlers().GetOk(path); ok {
return h, path, true
}
if path == "/" {
return z, "", false
}
path = pathpkg.Dir(path)
}
}
func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
h, mountPoint, ok := b.getServeHandler(r)
if !ok {
http.NotFound(w, r)
return
}
if s := h.Text(); s != "" {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
io.WriteString(w, s)
return
}
if v := h.Path(); v != "" {
b.serveFileOrDirectory(w, r, v, mountPoint)
return
}
if v := h.Proxy(); v != "" {
// TODO(bradfitz): this is a lot of setup per HTTP request. We should
// build the whole http.Handler with all the muxing and child handlers
// only on start/config change. But this works for now (2022-11-09).
targetURL, insecure := expandProxyArg(v)
u, err := url.Parse(targetURL)
if err != nil {
http.Error(w, "bad proxy config", http.StatusInternalServerError)
return
}
rp := httputil.NewSingleHostReverseProxy(u)
rp.Transport = &http.Transport{
DialContext: b.dialer.SystemDial,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
}
rp.ServeHTTP(w, r)
return
}
http.Error(w, "empty handler", 500)
}
func (b *LocalBackend) serveFileOrDirectory(w http.ResponseWriter, r *http.Request, fileOrDir, mountPoint string) {
fi, err := os.Stat(fileOrDir)
if err != nil {
if os.IsNotExist(err) {
http.NotFound(w, r)
return
}
http.Error(w, err.Error(), 500)
return
}
if fi.Mode().IsRegular() {
if mountPoint != r.URL.Path {
http.NotFound(w, r)
return
}
f, err := os.Open(fileOrDir)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer f.Close()
http.ServeContent(w, r, path.Base(mountPoint), fi.ModTime(), f)
return
}
if !fi.IsDir() {
http.Error(w, "not a file or directory", 500)
return
}
if len(r.URL.Path) < len(mountPoint) && r.URL.Path+"/" == mountPoint {
http.Redirect(w, r, mountPoint, http.StatusFound)
return
}
var fs http.Handler = http.FileServer(http.Dir(fileOrDir))
if mountPoint != "/" {
fs = http.StripPrefix(strings.TrimSuffix(mountPoint, "/"), fs)
}
fs.ServeHTTP(&fixLocationHeaderResponseWriter{
ResponseWriter: w,
mountPoint: mountPoint,
}, r)
}
// fixLocationHeaderResponseWriter is an http.ResponseWriter wrapper that, upon
// flushing HTTP headers, prefixes any Location header with the mount point.
type fixLocationHeaderResponseWriter struct {
http.ResponseWriter
mountPoint string
fixOnce sync.Once // guards call to fix
}
func (w *fixLocationHeaderResponseWriter) fix() {
h := w.ResponseWriter.Header()
if v := h.Get("Location"); v != "" {
h.Set("Location", w.mountPoint+v)
}
}
func (w *fixLocationHeaderResponseWriter) WriteHeader(code int) {
w.fixOnce.Do(w.fix)
w.ResponseWriter.WriteHeader(code)
}
func (w *fixLocationHeaderResponseWriter) Write(p []byte) (int, error) {
w.fixOnce.Do(w.fix)
return w.ResponseWriter.Write(p)
}
// expandProxyArg returns a URL from s, where s can be of form:
//
// * port number ("8080")
// * host:port ("localhost:8080")
// * full URL ("http://localhost:8080", in which case it's returned unchanged)
// * insecure TLS ("https+insecure://127.0.0.1:4430")
func expandProxyArg(s string) (targetURL string, insecureSkipVerify bool) {
if s == "" {
return "", false
}
if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") {
return s, false
}
if rest, ok := strs.CutPrefix(s, "https+insecure://"); ok {
return "https://" + rest, true
}
if allNumeric(s) {
return "http://127.0.0.1:" + s, false
}
return "http://" + s, false
}
func allNumeric(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return s != ""
}
func (b *LocalBackend) webServerConfig(sniName string, port uint16) (c ipn.WebServerConfigView, ok bool) {
key := ipn.HostPort(fmt.Sprintf("%s:%v", sniName, port))
b.mu.Lock()
defer b.mu.Unlock()
if !b.serveConfig.Valid() {
return c, false
}
return b.serveConfig.Web().GetOk(key)
}
func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
_, ok := b.webServerConfig(hi.ServerName, port)
if !ok {
return nil, errors.New("no webserver configured for name/port")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
pair, err := b.GetCertPEM(ctx, hi.ServerName)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
if err != nil {
return nil, err
}
return &cert, nil
}
}