diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index c57b58895..692d1c92a 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -1091,6 +1091,17 @@ func (lc *LocalClient) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, er return getServeConfigFromJSON(body) } +// GetMemoryServeConfig return the current serve config. +// +// If the serve config is empty, it returns (nil, nil). +func (lc *LocalClient) GetMemoryServeConfig(ctx context.Context) (*ipn.ServeConfig, error) { + body, err := lc.send(ctx, "GET", "/localapi/v0/serve-config?memory=true", 200, nil) + if err != nil { + return nil, fmt.Errorf("getting serve config: %w", err) + } + return getServeConfigFromJSON(body) +} + func getServeConfigFromJSON(body []byte) (sc *ipn.ServeConfig, err error) { if err := json.Unmarshal(body, &sc); err != nil { return nil, err diff --git a/cmd/tailscale/cli/funnel.go b/cmd/tailscale/cli/funnel.go index ff66adc4d..8ce146669 100644 --- a/cmd/tailscale/cli/funnel.go +++ b/cmd/tailscale/cli/funnel.go @@ -44,7 +44,7 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command { ShortHelp: "Turn on/off Funnel service", ShortUsage: strings.Join([]string{ "funnel {on|off}", - "funnel status [--json]", + "funnel status [--json] [--memory]", }, "\n "), LongHelp: strings.Join([]string{ "Funnel allows you to publish a 'tailscale serve'", @@ -62,6 +62,7 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command { ShortHelp: "show current serve/funnel status", FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) { fs.BoolVar(&e.json, "json", false, "output JSON") + fs.BoolVar(&e.memory, "memory", false, "in memory config") }), UsageFunc: usageFunc, }, diff --git a/cmd/tailscale/cli/serve.go b/cmd/tailscale/cli/serve.go index 7b4d38691..8c466471a 100644 --- a/cmd/tailscale/cli/serve.go +++ b/cmd/tailscale/cli/serve.go @@ -131,6 +131,7 @@ func (e *serveEnv) newFlags(name string, setup func(fs *flag.FlagSet)) *flag.Fla type localServeClient interface { StatusWithoutPeers(context.Context) (*ipnstate.Status, error) GetServeConfig(context.Context) (*ipn.ServeConfig, error) + GetMemoryServeConfig(context.Context) (*ipn.ServeConfig, error) SetServeConfig(context.Context, *ipn.ServeConfig) error QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error) @@ -146,7 +147,8 @@ type localServeClient interface { // It also contains the flags, as registered with newServeCommand. type serveEnv struct { // flags - json bool // output JSON (status only for now) + json bool // output JSON (status only for now) + memory bool // output memory (status only for now) lc localServeClient // localClient interface, specific to serve @@ -626,7 +628,13 @@ func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error { // - tailscale status // - tailscale status --json func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error { - sc, err := e.lc.GetServeConfig(ctx) + var sc *ipn.ServeConfig + var err error + if e.memory { + sc, err = e.lc.GetMemoryServeConfig(ctx) + } else { + sc, err = e.lc.GetServeConfig(ctx) + } if err != nil { return err } diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 5377705bb..a09e2e511 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -80,6 +80,7 @@ func (src *ServeConfig) Clone() *ServeConfig { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct { + InMemory bool TCP map[uint16]*TCPPortHandler Web map[HostPort]*WebServerConfig AllowFunnel map[HostPort]bool diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index 8a04c2c32..9cb66edcf 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -159,6 +159,8 @@ func (v *ServeConfigView) UnmarshalJSON(b []byte) error { return nil } +func (v ServeConfigView) InMemory() bool { return v.ж.InMemory } + func (v ServeConfigView) TCP() views.MapFn[uint16, *TCPPortHandler, TCPPortHandlerView] { return views.MapFnOf(v.ж.TCP, func(t *TCPPortHandler) TCPPortHandlerView { return t.View() @@ -177,6 +179,7 @@ func (v ServeConfigView) AllowFunnel() views.Map[HostPort, bool] { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _ServeConfigViewNeedsRegeneration = ServeConfig(struct { + InMemory bool TCP map[uint16]*TCPPortHandler Web map[HostPort]*WebServerConfig AllowFunnel map[HostPort]bool diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index caac3977c..cd57305f5 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -242,6 +242,7 @@ type LocalBackend struct { // ServeConfig fields. (also guarded by mu) lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig serveConfig ipn.ServeConfigView // or !Valid if none + memServeConfig ipn.ServeConfigView // or !Valid if none serveListeners map[netip.AddrPort]*serveListener // addrPort => serveListener serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *httputil.ReverseProxy @@ -2329,6 +2330,7 @@ func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) { b.setTCPPortsIntercepted(nil) b.lastServeConfJSON = mem.B(nil) b.serveConfig = ipn.ServeConfigView{} + b.memServeConfig = ipn.ServeConfigView{} } else { filtered := tsaddr.FilterPrefixesCopy(p.AdvertiseRoutes(), tsaddr.IsViaPrefix) b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(filtered)) @@ -2686,7 +2688,7 @@ func (b *LocalBackend) checkExitNodePrefsLocked(p *ipn.Prefs) error { } func (b *LocalBackend) checkFunnelEnabledLocked(p *ipn.Prefs) error { - if p.ShieldsUp && b.serveConfig.IsFunnelOn() { + if p.ShieldsUp && (b.serveConfig.IsFunnelOn() || b.memServeConfig.IsFunnelOn()) { return errors.New("Cannot enable shields-up when Funnel is enabled.") } return nil @@ -2765,7 +2767,8 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) { // doesn't affect security or correctness. And we also don't expect people to // modify their ServeConfig in raw mode. func (b *LocalBackend) wantIngressLocked() bool { - return b.serveConfig.Valid() && b.serveConfig.AllowFunnel().Len() > 0 + return b.serveConfig.Valid() && (b.serveConfig.AllowFunnel().Len() > 0) || + b.memServeConfig.Valid() && (b.memServeConfig.AllowFunnel().Len() > 0) } // setPrefsLockedOnEntry requires b.mu be held to call it, but it @@ -4073,6 +4076,7 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) { // Don't try to load the serve config. b.lastServeConfJSON = mem.B(nil) b.serveConfig = ipn.ServeConfigView{} + // b.memServeConfig = ipn.ServeConfigView{} should we do this? return } confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID) @@ -4082,6 +4086,7 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) { if err != nil { b.lastServeConfJSON = mem.B(nil) b.serveConfig = ipn.ServeConfigView{} + // b.memServeConfig = ipn.ServeConfigView{} should we do this? return } if b.lastServeConfJSON.Equal(mem.B(confj)) { @@ -4092,6 +4097,7 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) { if err := json.Unmarshal(confj, &conf); err != nil { b.logf("invalid ServeConfig %q in StateStore: %v", confKey, err) b.serveConfig = ipn.ServeConfigView{} + // b.memServeConfig = ipn.ServeConfigView{} should we do this? return } b.serveConfig = conf.View() @@ -4109,9 +4115,13 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn. } b.reloadServeConfigLocked(prefs) - if b.serveConfig.Valid() { + + setServeProxy := func(sc ipn.ServeConfigView) { + if !sc.Valid() { + return + } servePorts := make([]uint16, 0, 3) - b.serveConfig.TCP().Range(func(port uint16, _ ipn.TCPPortHandlerView) bool { + sc.TCP().Range(func(port uint16, _ ipn.TCPPortHandlerView) bool { if port > 0 { servePorts = append(servePorts, uint16(port)) } @@ -4126,6 +4136,9 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn. b.updateServeTCPPortNetMapAddrListenersLocked(servePorts) } } + setServeProxy(b.serveConfig) + setServeProxy(b.memServeConfig) + // Kick off a Hostinfo update to control if WireIngress changed. if wire := b.wantIngressLocked(); b.hostinfo != nil && b.hostinfo.WireIngress != wire { b.logf("Hostinfo.WireIngress changed to %v", wire) @@ -4140,35 +4153,39 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn. // backend specified in serveConfig. It expects serveConfig to be valid and // up-to-date, so should be called after reloadServeConfigLocked. func (b *LocalBackend) setServeProxyHandlersLocked() { - if !b.serveConfig.Valid() { - return - } var backends map[string]bool - b.serveConfig.Web().Range(func(_ ipn.HostPort, conf ipn.WebServerConfigView) (cont bool) { - conf.Handlers().Range(func(_ string, h ipn.HTTPHandlerView) (cont bool) { - backend := h.Proxy() - if backend == "" { - // Only create proxy handlers for servers with a proxy backend. - return true - } - mak.Set(&backends, backend, true) - if _, ok := b.serveProxyHandlers.Load(backend); ok { - return true - } + f := func(sc ipn.ServeConfigView) { + if !sc.Valid() { + return + } + sc.Web().Range(func(_ ipn.HostPort, conf ipn.WebServerConfigView) (cont bool) { + conf.Handlers().Range(func(_ string, h ipn.HTTPHandlerView) (cont bool) { + backend := h.Proxy() + if backend == "" { + // Only create proxy handlers for servers with a proxy backend. + return true + } + mak.Set(&backends, backend, true) + if _, ok := b.serveProxyHandlers.Load(backend); ok { + return true + } - b.logf("serve: creating a new proxy handler for %s", backend) - p, err := b.proxyHandlerForBackend(backend) - if err != nil { - // The backend endpoint (h.Proxy) should have been validated by expandProxyTarget - // in the CLI, so just log the error here. - b.logf("[unexpected] could not create proxy for %v: %s", backend, err) + b.logf("serve: creating a new proxy handler for %s", backend) + p, err := b.proxyHandlerForBackend(backend) + if err != nil { + // The backend endpoint (h.Proxy) should have been validated by expandProxyTarget + // in the CLI, so just log the error here. + b.logf("[unexpected] could not create proxy for %v: %s", backend, err) + return true + } + b.serveProxyHandlers.Store(backend, p) return true - } - b.serveProxyHandlers.Store(backend, p) + }) return true }) - return true - }) + } + f(b.serveConfig) + f(b.memServeConfig) // Clean up handlers for proxy backends that are no longer present // in configuration. @@ -4937,7 +4954,8 @@ func (b *LocalBackend) resetForProfileChangeLockedOnEntry() error { } b.lastServeConfJSON = mem.B(nil) b.serveConfig = ipn.ServeConfigView{} - b.enterStateLockedOnEntry(ipn.NoState) // Reset state. + b.memServeConfig = ipn.ServeConfigView{} // is this needed? + b.enterStateLockedOnEntry(ipn.NoState) // Reset state. health.SetLocalLogConfigHealth(nil) return b.Start(ipn.Options{}) } diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index de9de77ce..8a0972eaf 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -231,19 +231,24 @@ func (b *LocalBackend) SetServeConfig(config *ipn.ServeConfig) error { if !nm.SelfNode.Valid() { 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) + if !config.InMemory { + 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 } - bs = j - } - if err := b.store.WriteState(confKey, bs); err != nil { - return fmt.Errorf("writing ServeConfig to StateStore: %w", err) + if err := b.store.WriteState(confKey, bs); err != nil { + return fmt.Errorf("writing ServeConfig to StateStore: %w", err) + } + } else { + b.memServeConfig = config.View() } b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs()) @@ -252,9 +257,12 @@ func (b *LocalBackend) SetServeConfig(config *ipn.ServeConfig) error { // 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 { +func (b *LocalBackend) ServeConfig(inMemory bool) ipn.ServeConfigView { b.mu.Lock() defer b.mu.Unlock() + if inMemory { + return b.memServeConfig + } return b.serveConfig } @@ -278,9 +286,9 @@ func (b *LocalBackend) StreamServe(ctx context.Context, w io.Writer, req ipn.Ser } // Turn on Funnel for the given HostPort. - sc := b.ServeConfig().AsStruct() + sc := b.ServeConfig(true).AsStruct() if sc == nil { - sc = &ipn.ServeConfig{} + sc = &ipn.ServeConfig{InMemory: true} } setHandler(sc, req) if err := b.SetServeConfig(sc); err != nil { @@ -288,7 +296,7 @@ func (b *LocalBackend) StreamServe(ctx context.Context, w io.Writer, req ipn.Ser } // Defer turning off Funnel once stream ends. defer func() { - sc := b.ServeConfig().AsStruct() + sc := b.ServeConfig(true).AsStruct() deleteHandler(sc, req, port) err = errors.Join(err, b.SetServeConfig(sc)) }() @@ -419,58 +427,63 @@ func (b *LocalBackend) maybeLogServeConnection(destPort uint16, srcAddr netip.Ad func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) { b.mu.Lock() sc := b.serveConfig + msc := b.memServeConfig b.mu.Unlock() - if !sc.Valid() { - b.logf("localbackend: got ingress conn w/o serveConfig; rejecting") - sendRST() - return - } - - if !sc.AllowFunnel().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 - } - dport := uint16(port16) - if b.getTCPHandlerForFunnelFlow != nil { - handler := b.getTCPHandlerForFunnelFlow(srcAddr, dport) - if handler != nil { - c, ok := getConnOrReset() - if !ok { - b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport) - return - } - handler(c) + f := func(sc ipn.ServeConfigView) { + if !sc.Valid() { + b.logf("localbackend: got ingress conn w/o serveConfig; rejecting") + sendRST() return } + + if !sc.AllowFunnel().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 + } + dport := uint16(port16) + if b.getTCPHandlerForFunnelFlow != nil { + handler := b.getTCPHandlerForFunnelFlow(srcAddr, dport) + if handler != nil { + c, ok := getConnOrReset() + if !ok { + b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport) + return + } + handler(c) + return + } + } + // TODO(bradfitz): pass ingressPeer etc in context to tcpHandlerForServe, + // extend serveHTTPContext or similar. + handler := b.tcpHandlerForServe(dport, srcAddr) + if handler == nil { + sendRST() + return + } + c, ok := getConnOrReset() + if !ok { + b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport) + return + } + handler(c) } - // TODO(bradfitz): pass ingressPeer etc in context to tcpHandlerForServe, - // extend serveHTTPContext or similar. - handler := b.tcpHandlerForServe(dport, srcAddr) - if handler == nil { - sendRST() - return - } - c, ok := getConnOrReset() - if !ok { - b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport) - return - } - handler(c) + f(sc) + f(msc) } // tcpHandlerForServe returns a handler for a TCP connection to be served via @@ -478,90 +491,100 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort) (handler func(net.Conn) error) { b.mu.Lock() sc := b.serveConfig + msc := b.memServeConfig b.mu.Unlock() - if !sc.Valid() { - b.logf("[unexpected] localbackend: got TCP conn w/o serveConfig; from %v to port %v", srcAddr, dport) - 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(sc ipn.ServeConfigView) (handler func(net.Conn) error) { + if !sc.Valid() { + // TODO: should log only if both configs are invalid + b.logf("[unexpected] localbackend: got TCP conn w/o serveConfig; from %v to port %v", srcAddr, dport) + return nil } - if tcph.HTTPS() { - hs.TLSConfig = &tls.Config{ - GetCertificate: b.getTLSServeCertForPort(dport), + + tcph, ok := sc.TCP().GetOk(dport) + if !ok { + // TODO: should log only if both configs are not 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.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 + }, + }) + } + + // 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 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 - } + if h := f(sc); h != nil { + return h } - - b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr) - return nil + return f(msc) } func getServeHTTPContext(r *http.Request) (c *serveHTTPContext, ok bool) { @@ -825,7 +848,11 @@ func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebS if !b.serveConfig.Valid() { return c, false } - return b.serveConfig.Web().GetOk(key) + wc, ok := b.serveConfig.Web().GetOk(key) + if ok { + return wc, ok + } + return b.memServeConfig.Web().GetOk(key) } func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 44ec4dc43..2dcb9df88 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -835,7 +835,7 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) { return } w.Header().Set("Content-Type", "application/json") - config := h.b.ServeConfig() + config := h.b.ServeConfig(r.FormValue("memory") == "true") json.NewEncoder(w).Encode(config) case "POST": if !h.PermitWrite { diff --git a/ipn/serve.go b/ipn/serve.go index 3b6034fa9..98f232b49 100644 --- a/ipn/serve.go +++ b/ipn/serve.go @@ -26,6 +26,11 @@ func ServeConfigKey(profileID ProfileID) StateKey { // ServeConfig is the JSON type stored in the StateStore for // StateKey "_serve/$PROFILE_ID" as returned by ServeConfigKey. type ServeConfig struct { + // InMemory indicates whether this config + // is persisted in the local store or is + // an in memory config + InMemory bool + // TCP are the list of TCP port numbers that tailscaled should handle for // the Tailscale IP addresses. (not subnet routers, etc) TCP map[uint16]*TCPPortHandler `json:",omitempty"`