summaryrefslogtreecommitdiffhomepage
path: root/ipn
diff options
context:
space:
mode:
Diffstat (limited to 'ipn')
-rw-r--r--ipn/ipn_clone.go1
-rw-r--r--ipn/ipn_view.go3
-rw-r--r--ipn/ipnlocal/local.go76
-rw-r--r--ipn/ipnlocal/serve.go285
-rw-r--r--ipn/localapi/localapi.go2
-rw-r--r--ipn/serve.go5
6 files changed, 213 insertions, 159 deletions
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
- }
+ 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
- }
+ 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)
+ _, 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
}
- 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)
}
- // 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
- }
+ 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() || 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,
- })
- },
+ 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() {
- 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
+ // 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
}
- 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
}
- b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr)
- return nil
+ if h := f(sc); h != nil {
+ return h
+ }
+ 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"`