Compare commits

...

1 Commits

Author SHA1 Message Date
Marwan Sulaiman
df49cb24d5 ipn, ipn/ipnlocal: add an in memory serve config
This PR adds a parallel in-memory ServeConfig so that foreground
funnels are guaranteed to go away in case of unexpected shutdown

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-08-24 15:38:54 +01:00
9 changed files with 242 additions and 168 deletions

View File

@@ -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

View File

@@ -44,7 +44,7 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
ShortHelp: "Turn on/off Funnel service",
ShortUsage: strings.Join([]string{
"funnel <serve-port> {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,
},

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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{})
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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"`