diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index 9fcc8aaf2..af46f896e 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -1094,29 +1094,6 @@ func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) er return nil } -// StreamServe returns an io.ReadCloser that streams serve/Funnel -// connections made to the provided HostPort. -// -// If Serve and Funnel were not already enabled for the HostPort in the ServeConfig, -// the backend enables it for the duration of the context's lifespan and -// then turns it back off once the context is closed. If either are already enabled, -// then they remain that way but logs are still streamed -func (lc *LocalClient) StreamServe(ctx context.Context, hp ipn.ServeStreamRequest) (io.ReadCloser, error) { - req, err := http.NewRequestWithContext(ctx, "POST", "http://"+apitype.LocalAPIHost+"/localapi/v0/stream-serve", jsonBody(hp)) - if err != nil { - return nil, err - } - res, err := lc.doLocalRequestNiceError(req) - if err != nil { - return nil, err - } - if res.StatusCode != 200 { - res.Body.Close() - return nil, errors.New(res.Status) - } - return res.Body, nil -} - // GetServeConfig return the current serve config. // // If the serve config is empty, it returns (nil, nil). diff --git a/cmd/tailscale/cli/serve.go b/cmd/tailscale/cli/serve.go index 39c0e106e..ab1a16913 100644 --- a/cmd/tailscale/cli/serve.go +++ b/cmd/tailscale/cli/serve.go @@ -149,7 +149,6 @@ type localServeClient interface { QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error) IncrementCounter(ctx context.Context, name string, delta int) error - StreamServe(ctx context.Context, req ipn.ServeStreamRequest) (io.ReadCloser, error) // TODO: testing :) } // serveEnv is the environment the serve command runs within. All I/O should be diff --git a/cmd/tailscale/cli/serve_dev.go b/cmd/tailscale/cli/serve_dev.go index c2c94cc4a..30aa43119 100644 --- a/cmd/tailscale/cli/serve_dev.go +++ b/cmd/tailscale/cli/serve_dev.go @@ -5,9 +5,10 @@ package cli import ( "context" + "encoding/json" + "errors" "flag" "fmt" - "io" "log" "os" "os/signal" @@ -29,15 +30,15 @@ var infoMap = map[string]commandInfo{ "serve": { ShortHelp: "Serve content and local servers on your tailnet", LongHelp: strings.Join([]string{ - "Serve lets you share a local server securely within your tailnet.", - "To share a local server on the internet, use \"tailscale funnel\"", + `Serve lets you share a local server securely within your tailnet.`, + `To share a local server on the internet, use "tailscale funnel"`, }, "\n"), }, "funnel": { ShortHelp: "Serve content and local servers on the internet", LongHelp: strings.Join([]string{ - "Funnel lets you share a local server on the internet using Tailscale.", - "To share only within your tailnet, use \"tailscale serve\"", + `Funnel lets you share a local server on the internet using Tailscale.`, + `To share only within your tailnet, use "tailscale serve"`, }, "\n"), }, } @@ -134,14 +135,76 @@ func (e *serveEnv) runServeDev(funnel bool) execFunc { } func (e *serveEnv) streamServe(ctx context.Context, req ipn.ServeStreamRequest) error { - stream, err := e.lc.StreamServe(ctx, req) + watcher, err := e.lc.WatchIPNBus(ctx, ipn.NotifyInitialState|ipn.NotifyServeRequest) if err != nil { return err } - defer stream.Close() + defer watcher.Close() + n, err := watcher.Next() + if err != nil { + return err + } + if n.SessionID == "" { + return errors.New("missing session id") + } + sc, err := e.lc.GetServeConfig(ctx) + if err != nil { + return fmt.Errorf("error getting serve config: %w", err) + } + if sc == nil { + sc = &ipn.ServeConfig{} + } + setHandler(sc, req, n.SessionID) + err = e.lc.SetServeConfig(ctx, sc) + if err != nil { + return fmt.Errorf("error setting serve config: %w", err) + } - fmt.Fprintf(os.Stderr, "Serve started on \"https://%s\".\n", strings.TrimSuffix(string(req.HostPort), ":443")) - fmt.Fprintf(os.Stderr, "Press Ctrl-C to stop.\n\n") - _, err = io.Copy(os.Stdout, stream) - return err + fmt.Fprintf(os.Stderr, "Funnel started on \"https://%s\".\n", strings.TrimSuffix(string(req.HostPort), ":443")) + fmt.Fprintf(os.Stderr, "Press Ctrl-C to stop Funnel.\n\n") + + for { + n, err := watcher.Next() + if err != nil { + return fmt.Errorf("error calling next: %w", err) + } + if n.FunnelRequestLog == nil { + continue + } + bts, _ := json.Marshal(n.FunnelRequestLog) + fmt.Printf("%s\n", bts) + } +} + +func setHandler(sc *ipn.ServeConfig, req ipn.ServeStreamRequest, sessionID string) { + if sc.Foreground == nil { + sc.Foreground = make(map[string]*ipn.ServeConfig) + } + if sc.Foreground[sessionID] == nil { + sc.Foreground[sessionID] = &ipn.ServeConfig{} + } + if sc.Foreground[sessionID].TCP == nil { + sc.Foreground[sessionID].TCP = make(map[uint16]*ipn.TCPPortHandler) + } + if _, ok := sc.Foreground[sessionID].TCP[443]; !ok { + sc.Foreground[sessionID].TCP[443] = &ipn.TCPPortHandler{HTTPS: true} + } + if sc.Foreground[sessionID].Web == nil { + sc.Foreground[sessionID].Web = make(map[ipn.HostPort]*ipn.WebServerConfig) + } + wsc, ok := sc.Foreground[sessionID].Web[req.HostPort] + if !ok { + wsc = &ipn.WebServerConfig{} + sc.Foreground[sessionID].Web[req.HostPort] = wsc + } + if wsc.Handlers == nil { + wsc.Handlers = make(map[string]*ipn.HTTPHandler) + } + wsc.Handlers[req.MountPoint] = &ipn.HTTPHandler{ + Proxy: req.Source, + } + if sc.AllowFunnel == nil { + sc.AllowFunnel = make(map[ipn.HostPort]bool) + } + sc.AllowFunnel[req.HostPort] = true } diff --git a/ipn/backend.go b/ipn/backend.go index 8da7e6a5c..b33415157 100644 --- a/ipn/backend.go +++ b/ipn/backend.go @@ -66,6 +66,8 @@ const ( NotifyInitialNetMap // if set, the first Notify message (sent immediately) will contain the current NetMap NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out + + NotifyServeRequest // if set, Serve requests will be sent to the watcher ) // Notify is a communication from a backend (e.g. tailscaled) to a frontend @@ -122,6 +124,10 @@ type Notify struct { ClientVersion *tailcfg.ClientVersion `json:",omitempty"` // type is mirrored in xcode/Shared/IPN.swift + + // FunnelRequestLog is a notification that a request + // has been sent via the serve config. + FunnelRequestLog *FunnelRequestLog `json:",omitempty"` } func (n Notify) String() string { diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 90718fb8d..5f30caa92 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -76,6 +76,12 @@ func (src *ServeConfig) Clone() *ServeConfig { } } dst.AllowFunnel = maps.Clone(src.AllowFunnel) + if dst.Foreground != nil { + dst.Foreground = map[string]*ServeConfig{} + for k, v := range src.Foreground { + dst.Foreground[k] = v.Clone() + } + } return dst } @@ -84,6 +90,7 @@ var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct { TCP map[uint16]*TCPPortHandler Web map[HostPort]*WebServerConfig AllowFunnel map[HostPort]bool + Foreground map[string]*ServeConfig }{}) // Clone makes a deep copy of TCPPortHandler. diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index 0e22544dd..cd89fa151 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -177,11 +177,18 @@ func (v ServeConfigView) AllowFunnel() views.Map[HostPort, bool] { return views.MapOf(v.ж.AllowFunnel) } +func (v ServeConfigView) Foreground() views.MapFn[string, *ServeConfig, ServeConfigView] { + return views.MapFnOf(v.ж.Foreground, func(t *ServeConfig) ServeConfigView { + return t.View() + }) +} + // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _ServeConfigViewNeedsRegeneration = ServeConfig(struct { TCP map[uint16]*TCPPortHandler Web map[HostPort]*WebServerConfig AllowFunnel map[HostPort]bool + Foreground map[string]*ServeConfig }{}) // View returns a readonly view of TCPPortHandler. diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index b516b12c0..08adf8aae 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -246,9 +246,6 @@ type LocalBackend struct { serveListeners map[netip.AddrPort]*serveListener // addrPort => serveListener serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *httputil.ReverseProxy - // serveStreamers is a map for those running Funnel in the foreground - // and streaming incoming requests. - serveStreamers map[uint16]map[uint32]func(ipn.FunnelRequestLog) // serve port => map of stream loggers (key is UUID) // statusLock must be held before calling statusChanged.Wait() or // statusChanged.Broadcast(). @@ -2014,6 +2011,16 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa go b.pollRequestEngineStatus(ctx) } + if mask&ipn.NotifyServeRequest != 0 { + defer func() { + sc := b.ServeConfig().AsStruct() + if sc != nil { + delete(sc.Foreground, sessionID) + b.SetServeConfig(sc) // TODO(marwan-at-work): check err + } + }() + } + for { select { case <-ctx.Done(): @@ -2344,7 +2351,7 @@ func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) { } else { filtered := tsaddr.FilterPrefixesCopy(p.AdvertiseRoutes(), tsaddr.IsViaPrefix) b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(filtered)) - b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(p) + b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(p, true) } } @@ -4046,7 +4053,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { netns.SetBindToInterfaceByRoute(hasCapability(nm, tailcfg.CapabilityBindToInterfaceByRoute)) netns.SetDisableBindConnToInterface(hasCapability(nm, tailcfg.CapabilityDebugDisableBindConnToInterface)) - b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs()) + b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs(), true) if nm == nil { b.nodeByAddr = nil return @@ -4093,7 +4100,7 @@ func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) { } } -func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) { +func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView, changed bool) { if b.netMap == nil || !b.netMap.SelfNode.Valid() || !prefs.Valid() || b.pm.CurrentProfile().ID == "" { // We're not logged in, so we don't have a profile. // Don't try to load the serve config. @@ -4101,6 +4108,9 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) { b.serveConfig = ipn.ServeConfigView{} return } + if !changed { + return + } confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID) // TODO(maisem,bradfitz): prevent reading the config from disk // if the profile has not changed. @@ -4127,14 +4137,14 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) { // the ports that tailscaled should handle as a function of b.netMap and b.prefs. // // b.mu must be held. -func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.PrefsView) { +func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.PrefsView, changed bool) { handlePorts := make([]uint16, 0, 4) if prefs.Valid() && prefs.RunSSH() && envknob.CanSSHD() { handlePorts = append(handlePorts, 22) } - b.reloadServeConfigLocked(prefs) + b.reloadServeConfigLocked(prefs, changed) if b.serveConfig.Valid() { servePorts := make([]uint16, 0, 3) b.serveConfig.TCP().Range(func(port uint16, _ ipn.TCPPortHandlerView) bool { @@ -4879,7 +4889,7 @@ func (b *LocalBackend) SetDevStateStore(key, value string) error { b.mu.Lock() defer b.mu.Unlock() - b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs()) + b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs(), true) return nil } diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index 8778548c1..11cb499f9 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -23,13 +23,14 @@ import ( "sync" "time" - "github.com/google/uuid" + "go4.org/mem" "tailscale.com/ipn" "tailscale.com/logtail/backoff" "tailscale.com/net/netutil" "tailscale.com/syncs" "tailscale.com/tailcfg" "tailscale.com/types/logger" + "tailscale.com/types/views" "tailscale.com/util/mak" "tailscale.com/version" ) @@ -236,17 +237,21 @@ func (b *LocalBackend) SetServeConfig(config *ipn.ServeConfig) error { var bs []byte if config != nil { - j, err := json.Marshal(config) + // TODO(marwan): either strip Clone+StripForeground here (which means we need to double check lastServeConfJSON is unaffected) + // OR: strip foreground on backend start ups. + var err error + bs, err = json.Marshal(config) if err != nil { return fmt.Errorf("encoding serve config: %w", err) } - bs = j + b.serveConfig = config.View() + b.lastServeConfJSON = mem.B(bs) } if err := b.store.WriteState(confKey, bs); err != nil { return fmt.Errorf("writing ServeConfig to StateStore: %w", err) } - b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs()) + b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs(), false) return nil } @@ -258,147 +263,7 @@ func (b *LocalBackend) ServeConfig() ipn.ServeConfigView { return b.serveConfig } -// StreamServe opens a stream to write any incoming connections made -// to the given HostPort out to the listening io.Writer. -// -// If Serve and Funnel were not already enabled for the HostPort in the ServeConfig, -// the backend enables it for the duration of the context's lifespan and -// then turns it back off once the context is closed. If either are already enabled, -// then they remain that way but logs are still streamed -func (b *LocalBackend) StreamServe(ctx context.Context, w io.Writer, req ipn.ServeStreamRequest) (err error) { - f, ok := w.(http.Flusher) - if !ok { - return errors.New("writer not a flusher") - } - f.Flush() - - port, err := req.HostPort.Port() - if err != nil { - return err - } - - // Turn on Funnel for the given HostPort. - sc := b.ServeConfig().AsStruct() - if sc == nil { - sc = &ipn.ServeConfig{} - } - setHandler(sc, req) - if err := b.SetServeConfig(sc); err != nil { - return fmt.Errorf("errro setting serve config: %w", err) - } - // Defer turning off Funnel once stream ends. - defer func() { - sc := b.ServeConfig().AsStruct() - deleteHandler(sc, req, port) - err = errors.Join(err, b.SetServeConfig(sc)) - }() - - var writeErrs []error - writeToStream := func(log ipn.FunnelRequestLog) { - jsonLog, err := json.Marshal(log) - if err != nil { - writeErrs = append(writeErrs, err) - return - } - if _, err := fmt.Fprintf(w, "%s\n", jsonLog); err != nil { - writeErrs = append(writeErrs, err) - return - } - f.Flush() - } - - // Hook up connections stream. - b.mu.Lock() - mak.NonNilMapForJSON(&b.serveStreamers) - if b.serveStreamers[port] == nil { - b.serveStreamers[port] = make(map[uint32]func(ipn.FunnelRequestLog)) - } - id := uuid.New().ID() - b.serveStreamers[port][id] = writeToStream - b.mu.Unlock() - - // Clean up streamer when done. - defer func() { - b.mu.Lock() - delete(b.serveStreamers[port], id) - b.mu.Unlock() - }() - - select { - case <-ctx.Done(): - // Triggered by foreground `tailscale funnel` process - // (the streamer) getting closed, or by turning off Tailscale. - } - - return errors.Join(writeErrs...) -} - -func setHandler(sc *ipn.ServeConfig, req ipn.ServeStreamRequest) { - if sc.TCP == nil { - sc.TCP = make(map[uint16]*ipn.TCPPortHandler) - } - if _, ok := sc.TCP[443]; !ok { - sc.TCP[443] = &ipn.TCPPortHandler{ - HTTPS: true, - } - } - if sc.Web == nil { - sc.Web = make(map[ipn.HostPort]*ipn.WebServerConfig) - } - wsc, ok := sc.Web[req.HostPort] - if !ok { - wsc = &ipn.WebServerConfig{} - sc.Web[req.HostPort] = wsc - } - if wsc.Handlers == nil { - wsc.Handlers = make(map[string]*ipn.HTTPHandler) - } - wsc.Handlers[req.MountPoint] = &ipn.HTTPHandler{ - Proxy: req.Source, - } - if req.Funnel { - if sc.AllowFunnel == nil { - sc.AllowFunnel = make(map[ipn.HostPort]bool) - } - sc.AllowFunnel[req.HostPort] = true - } -} - -func deleteHandler(sc *ipn.ServeConfig, req ipn.ServeStreamRequest, port uint16) { - delete(sc.AllowFunnel, req.HostPort) - if sc.TCP != nil { - delete(sc.TCP, port) - } - if sc.Web == nil { - return - } - if sc.Web[req.HostPort] == nil { - return - } - wsc, ok := sc.Web[req.HostPort] - if !ok { - return - } - if wsc.Handlers == nil { - return - } - if _, ok := wsc.Handlers[req.MountPoint]; !ok { - return - } - delete(wsc.Handlers, req.MountPoint) - if len(wsc.Handlers) == 0 { - delete(sc.Web, req.HostPort) - } -} - func (b *LocalBackend) maybeLogServeConnection(destPort uint16, srcAddr netip.AddrPort) { - b.mu.Lock() - streamers := b.serveStreamers[destPort] - b.mu.Unlock() - if len(streamers) == 0 { - return - } - var log ipn.FunnelRequestLog log.SrcAddr = srcAddr log.Time = b.clock.Now() @@ -413,9 +278,9 @@ func (b *LocalBackend) maybeLogServeConnection(destPort uint16, srcAddr netip.Ad } } - for _, stream := range streamers { - stream(log) - } + b.send(ipn.Notify{ + FunnelRequestLog: &log, + }) } func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) { @@ -487,83 +352,93 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort) return nil } - 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) + f := func(tcpCfg views.MapFn[uint16, *ipn.TCPPortHandler, ipn.TCPPortHandlerView], sessionID string) (handler func(net.Conn) error) { + tcph, ok := tcpCfg.GetOk(dport) + if !ok { + b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr) + return nil + } + + if tcph.HTTPS() || tcph.HTTP() { + hs := &http.Server{ + Handler: http.HandlerFunc(b.serveWebHandler), + BaseContext: func(_ net.Listener) context.Context { + return context.WithValue(context.Background(), serveHTTPContextKey{}, &serveHTTPContext{ + SrcAddr: srcAddr, + DestPort: dport, + }) + }, + } + if tcph.HTTPS() { + hs.TLSConfig = &tls.Config{ + GetCertificate: b.getTLSServeCertForPort(dport), + } + return func(c net.Conn) error { + return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "") + } + } + + return func(c net.Conn) error { + return hs.Serve(netutil.NewOneConnListener(c, nil)) + } + } + + if backDst := tcph.TCPForward(); backDst != "" { + return func(conn net.Conn) error { + defer conn.Close() + b.maybeLogServeConnection(dport, srcAddr) + 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) + return nil + } + 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, false) + 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 + }() + return <-errc + } + } + + b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr) return nil } - - if tcph.HTTPS() || tcph.HTTP() { - hs := &http.Server{ - Handler: http.HandlerFunc(b.serveWebHandler), - BaseContext: func(_ net.Listener) context.Context { - return context.WithValue(context.Background(), serveHTTPContextKey{}, &serveHTTPContext{ - SrcAddr: srcAddr, - DestPort: dport, - }) - }, - } - if tcph.HTTPS() { - hs.TLSConfig = &tls.Config{ - GetCertificate: b.getTLSServeCertForPort(dport), - } - return func(c net.Conn) error { - return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "") - } - } - - return func(c net.Conn) error { - return hs.Serve(netutil.NewOneConnListener(c, nil)) - } + sc.Foreground().Range(func(k string, v ipn.ServeConfigView) (cont bool) { + handler = f(v.TCP(), k) + return handler == nil + }) + if handler != nil { + return handler } - - if backDst := tcph.TCPForward(); backDst != "" { - return func(conn net.Conn) error { - defer conn.Close() - b.maybeLogServeConnection(dport, srcAddr) - 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) - return nil - } - 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, false) - 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 - }() - return <-errc - } - } - - b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr) - return nil + return f(sc.TCP(), "") } func getServeHTTPContext(r *http.Request) (c *serveHTTPContext, ok bool) { @@ -827,6 +702,13 @@ func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebS if !b.serveConfig.Valid() { return c, false } + b.serveConfig.Foreground().Range(func(k string, v ipn.ServeConfigView) (cont bool) { + c, ok = v.Web().GetOk(key) + return !ok + }) + if ok { + return c, ok + } return b.serveConfig.Web().GetOk(key) } diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 8b0cd8f54..678e91458 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -97,7 +97,6 @@ var handler = map[string]localAPIHandler{ "set-expiry-sooner": (*Handler).serveSetExpirySooner, "start": (*Handler).serveStart, "status": (*Handler).serveStatus, - "stream-serve": (*Handler).serveStreamServe, "tka/init": (*Handler).serveTKAInit, "tka/log": (*Handler).serveTKALog, "tka/modify": (*Handler).serveTKAModify, @@ -854,35 +853,6 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) { } } -// serveStreamServe handles foreground serve and funnel streams. This is -// currently in development per https://github.com/tailscale/tailscale/issues/8489 -func (h *Handler) serveStreamServe(w http.ResponseWriter, r *http.Request) { - if !envknob.UseWIPCode() { - http.Error(w, "stream serve not yet available", http.StatusNotImplemented) - return - } - if !h.PermitWrite { - // Write permission required because we modify the ServeConfig. - http.Error(w, "serve stream denied", http.StatusForbidden) - return - } - if r.Method != "POST" { - http.Error(w, "POST required", http.StatusMethodNotAllowed) - return - } - var req ipn.ServeStreamRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeErrorJSON(w, fmt.Errorf("decoding HostPort: %w", err)) - return - } - w.Header().Set("Content-Type", "application/json") - if err := h.b.StreamServe(r.Context(), w, req); err != nil { - writeErrorJSON(w, fmt.Errorf("streaming serve: %w", err)) - return - } - w.WriteHeader(http.StatusOK) -} - func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) { if !h.PermitRead { http.Error(w, "IP forwarding check access denied", http.StatusForbidden) diff --git a/ipn/serve.go b/ipn/serve.go index 11df99726..a3c708ed9 100644 --- a/ipn/serve.go +++ b/ipn/serve.go @@ -37,6 +37,11 @@ type ServeConfig struct { // AllowFunnel is the set of SNI:port values for which funnel // traffic is allowed, from trusted ingress peers. AllowFunnel map[HostPort]bool `json:",omitempty"` + + // Foreground is a map of an IPN Bus session id to a + // foreground serve config. Note that only TCP and Web + // are used inside the Foreground map. + Foreground map[string]*ServeConfig `json:",omitempty"` } // HostPort is an SNI name and port number, joined by a colon.