summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarwan Sulaiman <marwan@tailscale.com>2023-09-05 13:51:52 -0400
committerMarwan Sulaiman <marwan@tailscale.com>2023-09-05 13:51:52 -0400
commitce16658ac0e032e9524bac8f5fc926b11f82a7a7 (patch)
tree7ccf2dd7c97b1d5996c4ca709818edc529b34f81
parenta4aa6507fa3afa586dc3cbbf85f2cae852622b9e (diff)
downloadtailscale-marwan/postmem.tar.xz
tailscale-marwan/postmem.zip
ipn, ipn/ipnlocal: add Foreground field for ServeConfigmarwan/postmem
This PR adds a new field to the serve config that can be used to identify which serves are in "foreground mode" and then can also be used to ensure they do not get persisted to disk so that if Tailscaled gets ungracefully shutdown, the reloaded ServeConfig will not have those ports opened. Updates #8489 Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
-rw-r--r--client/tailscale/localclient.go23
-rw-r--r--cmd/tailscale/cli/serve.go1
-rw-r--r--cmd/tailscale/cli/serve_dev.go85
-rw-r--r--ipn/backend.go6
-rw-r--r--ipn/ipn_clone.go7
-rw-r--r--ipn/ipn_view.go7
-rw-r--r--ipn/ipnlocal/local.go28
-rw-r--r--ipn/ipnlocal/serve.go310
-rw-r--r--ipn/localapi/localapi.go30
-rw-r--r--ipn/serve.go5
10 files changed, 214 insertions, 288 deletions
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, "Funnel started on \"https://%s\".\n", strings.TrimSuffix(string(req.HostPort), ":443"))
+ fmt.Fprintf(os.Stderr, "Press Ctrl-C to stop Funnel.\n\n")
- 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
+ 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)
- 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,
- })
- },
+ 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() {
- hs.TLSConfig = &tls.Config{
- GetCertificate: b.getTLSServeCertForPort(dport),
+
+ 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.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
+ return hs.Serve(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
+ },
+ })
+ }
- 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
}
-
- // 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
+ b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr)
+ return 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
+ }
+ 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.