ipn, ipn/ipnlocal: add Foreground field for ServeConfig

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>
pull/9285/head
Marwan Sulaiman 1 year ago committed by Marwan Sulaiman
parent 96094cc07e
commit 50990f8931

@ -1094,29 +1094,6 @@ func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) er
return nil 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. // GetServeConfig return the current serve config.
// //
// If the serve config is empty, it returns (nil, nil). // If the serve config is empty, it returns (nil, nil).

@ -149,7 +149,6 @@ type localServeClient interface {
QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error)
WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error)
IncrementCounter(ctx context.Context, name string, delta int) 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 // serveEnv is the environment the serve command runs within. All I/O should be

@ -5,9 +5,9 @@ package cli
import ( import (
"context" "context"
"errors"
"flag" "flag"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"os/signal" "os/signal"
@ -16,6 +16,7 @@ import (
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/util/mak"
) )
type execFunc func(ctx context.Context, args []string) error type execFunc func(ctx context.Context, args []string) error
@ -30,14 +31,14 @@ var infoMap = map[string]commandInfo{
ShortHelp: "Serve content and local servers on your tailnet", ShortHelp: "Serve content and local servers on your tailnet",
LongHelp: strings.Join([]string{ LongHelp: strings.Join([]string{
"Serve lets you share a local server securely within your tailnet.", "Serve lets you share a local server securely within your tailnet.",
"To share a local server on the internet, use \"tailscale funnel\"", `To share a local server on the internet, use "tailscale funnel"`,
}, "\n"), }, "\n"),
}, },
"funnel": { "funnel": {
ShortHelp: "Serve content and local servers on the internet", ShortHelp: "Serve content and local servers on the internet",
LongHelp: strings.Join([]string{ LongHelp: strings.Join([]string{
"Funnel lets you share a local server on the internet using Tailscale.", "Funnel lets you share a local server on the internet using Tailscale.",
"To share only within your tailnet, use \"tailscale serve\"", `To share only within your tailnet, use "tailscale serve"`,
}, "\n"), }, "\n"),
}, },
} }
@ -134,14 +135,56 @@ func (e *serveEnv) runServeDev(funnel bool) execFunc {
} }
func (e *serveEnv) streamServe(ctx context.Context, req ipn.ServeStreamRequest) error { 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)
if err != nil { if err != nil {
return err return err
} }
defer stream.Close() defer watcher.Close()
n, err := watcher.Next()
if err != nil {
return err
}
sessionID := n.SessionID
if sessionID == "" {
return errors.New("missing SessionID")
}
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, 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")
for {
_, err = watcher.Next()
if err != nil {
if errors.Is(err, context.Canceled) {
return nil
}
return err
}
}
}
fmt.Fprintf(os.Stderr, "Serve started on \"https://%s\".\n", strings.TrimSuffix(string(req.HostPort), ":443")) // setHandler modifies sc to add a Foreground config (described by req) with the given sessionID.
fmt.Fprintf(os.Stderr, "Press Ctrl-C to stop.\n\n") func setHandler(sc *ipn.ServeConfig, req ipn.ServeStreamRequest, sessionID string) {
_, err = io.Copy(os.Stdout, stream) fconf := &ipn.ServeConfig{}
return err mak.Set(&sc.Foreground, sessionID, fconf)
mak.Set(&fconf.TCP, 443, &ipn.TCPPortHandler{HTTPS: true})
wsc := &ipn.WebServerConfig{}
mak.Set(&fconf.Web, req.HostPort, wsc)
mak.Set(&wsc.Handlers, req.MountPoint, &ipn.HTTPHandler{
Proxy: req.Source,
})
mak.Set(&fconf.AllowFunnel, req.HostPort, true)
} }

@ -11,6 +11,7 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"maps"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -241,8 +242,9 @@ type LocalBackend struct {
componentLogUntil map[string]componentLogState componentLogUntil map[string]componentLogState
// ServeConfig fields. (also guarded by mu) // ServeConfig fields. (also guarded by mu)
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
serveConfig ipn.ServeConfigView // or !Valid if none serveConfig ipn.ServeConfigView // or !Valid if none
activeWatchSessions set.Set[string] // of WatchIPN SessionID
serveListeners map[netip.AddrPort]*serveListener // addrPort => serveListener serveListeners map[netip.AddrPort]*serveListener // addrPort => serveListener
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *httputil.ReverseProxy serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *httputil.ReverseProxy
@ -301,23 +303,24 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
clock := tstime.StdClock{} clock := tstime.StdClock{}
b := &LocalBackend{ b := &LocalBackend{
ctx: ctx, ctx: ctx,
ctxCancel: cancel, ctxCancel: cancel,
logf: logf, logf: logf,
keyLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now), keyLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now), statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
sys: sys, sys: sys,
e: e, e: e,
dialer: dialer, dialer: dialer,
store: store, store: store,
pm: pm, pm: pm,
backendLogID: logID, backendLogID: logID,
state: ipn.NoState, state: ipn.NoState,
portpoll: portpoll, portpoll: portpoll,
em: newExpiryManager(logf), em: newExpiryManager(logf),
gotPortPollRes: make(chan struct{}), gotPortPollRes: make(chan struct{}),
loginFlags: loginFlags, loginFlags: loginFlags,
clock: clock, clock: clock,
activeWatchSessions: make(set.Set[string]),
} }
netMon := sys.NetMon.Get() netMon := sys.NetMon.Get()
@ -1956,6 +1959,7 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
var ini *ipn.Notify var ini *ipn.Notify
b.mu.Lock() b.mu.Lock()
b.activeWatchSessions.Add(sessionID)
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap
if mask&initialBits != 0 { if mask&initialBits != 0 {
@ -1981,6 +1985,7 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
defer func() { defer func() {
b.mu.Lock() b.mu.Lock()
delete(b.notifyWatchers, handle) delete(b.notifyWatchers, handle)
delete(b.activeWatchSessions, sessionID)
b.mu.Unlock() b.mu.Unlock()
}() }()
@ -2011,7 +2016,9 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
go b.pollRequestEngineStatus(ctx) go b.pollRequestEngineStatus(ctx)
} }
defer b.DeleteForegroundSession(sessionID) // TODO(marwan-at-work): check err // TODO(marwan-at-work): check err
// TODO(marwan-at-work): streaming background logs?
defer b.DeleteForegroundSession(sessionID)
for { for {
select { select {
@ -2776,7 +2783,7 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
// doesn't affect security or correctness. And we also don't expect people to // doesn't affect security or correctness. And we also don't expect people to
// modify their ServeConfig in raw mode. // modify their ServeConfig in raw mode.
func (b *LocalBackend) wantIngressLocked() bool { func (b *LocalBackend) wantIngressLocked() bool {
return b.serveConfig.Valid() && b.serveConfig.AllowFunnel().Len() > 0 return b.serveConfig.Valid() && b.serveConfig.HasAllowFunnel()
} }
// setPrefsLockedOnEntry requires b.mu be held to call it, but it // setPrefsLockedOnEntry requires b.mu be held to call it, but it
@ -4092,6 +4099,10 @@ func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) {
} }
} }
// reloadServeConfigLocked reloads the serve config from the store or resets the
// serve config to nil if not logged in. The "changed" parameter, when false, instructs
// the method to only run the reset-logic and not reload the store from memory to ensure
// foreground sessions are not removed if they are not saved on disk.
func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) { func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
if b.netMap == nil || !b.netMap.SelfNode.Valid() || !prefs.Valid() || b.pm.CurrentProfile().ID == "" { 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. // We're not logged in, so we don't have a profile.
@ -4100,6 +4111,7 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
b.serveConfig = ipn.ServeConfigView{} b.serveConfig = ipn.ServeConfigView{}
return return
} }
confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID) confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID)
// TODO(maisem,bradfitz): prevent reading the config from disk // TODO(maisem,bradfitz): prevent reading the config from disk
// if the profile has not changed. // if the profile has not changed.
@ -4119,6 +4131,12 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
b.serveConfig = ipn.ServeConfigView{} b.serveConfig = ipn.ServeConfigView{}
return return
} }
// remove inactive sessions
maps.DeleteFunc(conf.Foreground, func(s string, sc *ipn.ServeConfig) bool {
return !b.activeWatchSessions.Contains(s)
})
b.serveConfig = conf.View() b.serveConfig = conf.View()
} }
@ -4136,7 +4154,7 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
b.reloadServeConfigLocked(prefs) b.reloadServeConfigLocked(prefs)
if b.serveConfig.Valid() { if b.serveConfig.Valid() {
servePorts := make([]uint16, 0, 3) servePorts := make([]uint16, 0, 3)
b.serveConfig.TCP().Range(func(port uint16, _ ipn.TCPPortHandlerView) bool { b.serveConfig.RangeOverTCPs(func(port uint16, _ ipn.TCPPortHandlerView) bool {
if port > 0 { if port > 0 {
servePorts = append(servePorts, uint16(port)) servePorts = append(servePorts, uint16(port))
} }
@ -4169,7 +4187,7 @@ func (b *LocalBackend) setServeProxyHandlersLocked() {
return return
} }
var backends map[string]bool var backends map[string]bool
b.serveConfig.Web().Range(func(_ ipn.HostPort, conf ipn.WebServerConfigView) (cont bool) { b.serveConfig.RangeOverWebs(func(_ ipn.HostPort, conf ipn.WebServerConfigView) (cont bool) {
conf.Handlers().Range(func(_ string, h ipn.HTTPHandlerView) (cont bool) { conf.Handlers().Range(func(_ string, h ipn.HTTPHandlerView) (cont bool) {
backend := h.Proxy() backend := h.Proxy()
if backend == "" { if backend == "" {

@ -26,6 +26,7 @@ import (
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/logid" "tailscale.com/types/logid"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/util/set"
"tailscale.com/wgengine" "tailscale.com/wgengine"
"tailscale.com/wgengine/filter" "tailscale.com/wgengine/filter"
"tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wgcfg"
@ -843,6 +844,9 @@ var _ legacyBackend = (*LocalBackend)(nil)
func TestWatchNotificationsCallbacks(t *testing.T) { func TestWatchNotificationsCallbacks(t *testing.T) {
b := new(LocalBackend) b := new(LocalBackend)
// activeWatchSessions is typically set in NewLocalBackend
// so WatchNotifications expects it to be non-empty.
b.activeWatchSessions = make(set.Set[string])
n := new(ipn.Notify) n := new(ipn.Notify)
b.WatchNotifications(context.Background(), 0, func() { b.WatchNotifications(context.Background(), 0, func() {
b.mu.Lock() b.mu.Lock()

@ -274,111 +274,6 @@ func (b *LocalBackend) DeleteForegroundSession(sessionID string) error {
return b.setServeConfigLocked(sc) return b.setServeConfigLocked(sc)
} }
// 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
//
// TODO(marwan-at-work): this whole endpoint will be
// deleted in a follow up PR in favor of WatchIPNBus
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))
}()
select {
case <-ctx.Done():
// Triggered by foreground `tailscale funnel` process
// (the streamer) getting closed, or by turning off Tailscale.
}
return nil
}
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) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) { func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
b.mu.Lock() b.mu.Lock()
sc := b.serveConfig sc := b.serveConfig
@ -390,7 +285,7 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target
return return
} }
if !sc.AllowFunnel().Get(target) { if !sc.HasFunnelForTarget(target) {
b.logf("localbackend: got ingress conn for unconfigured %q; rejecting", target) b.logf("localbackend: got ingress conn for unconfigured %q; rejecting", target)
sendRST() sendRST()
return return
@ -448,7 +343,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
return nil return nil
} }
tcph, ok := sc.TCP().GetOk(dport) tcph, ok := sc.FindTCP(dport)
if !ok { if !ok {
b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr) b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr)
return nil return nil
@ -643,6 +538,8 @@ func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) {
r.Out.Header.Set("Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers") r.Out.Header.Set("Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers")
} }
// serveWebHandler is an http.HandlerFunc that maps incoming requests to the
// correct *http.
func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) { func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
h, mountPoint, ok := b.getServeHandler(r) h, mountPoint, ok := b.getServeHandler(r)
if !ok { if !ok {
@ -784,7 +681,7 @@ func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebS
if !b.serveConfig.Valid() { if !b.serveConfig.Valid() {
return c, false return c, false
} }
return b.serveConfig.Web().GetOk(key) return b.serveConfig.FindWeb(key)
} }
func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {

@ -97,7 +97,6 @@ var handler = map[string]localAPIHandler{
"set-expiry-sooner": (*Handler).serveSetExpirySooner, "set-expiry-sooner": (*Handler).serveSetExpirySooner,
"start": (*Handler).serveStart, "start": (*Handler).serveStart,
"status": (*Handler).serveStatus, "status": (*Handler).serveStatus,
"stream-serve": (*Handler).serveStreamServe,
"tka/init": (*Handler).serveTKAInit, "tka/init": (*Handler).serveTKAInit,
"tka/log": (*Handler).serveTKALog, "tka/log": (*Handler).serveTKALog,
"tka/modify": (*Handler).serveTKAModify, "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) { func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead { if !h.PermitRead {
http.Error(w, "IP forwarding check access denied", http.StatusForbidden) http.Error(w, "IP forwarding check access denied", http.StatusForbidden)

@ -37,12 +37,12 @@ type ServeConfig struct {
// traffic is allowed, from trusted ingress peers. // traffic is allowed, from trusted ingress peers.
AllowFunnel map[HostPort]bool `json:",omitempty"` AllowFunnel map[HostPort]bool `json:",omitempty"`
// Foreground is a map of an IPN Bus session id to a // Foreground is a map of an IPN Bus session ID to an alternate foreground
// foreground serve config. Note that only TCP and Web // serve config that's valid for the life of that WatchIPNBus session ID.
// are used inside the Foreground map. // This. This allows the config to specify ephemeral configs that are
// // used in the CLI's foreground mode to ensure ungraceful shutdowns
// TODO(marwan-at-work): this is not currently // of either the client or the LocalBackend does not expose ports
// used. Remove the TODO in the follow up PR. // that users are not aware of.
Foreground map[string]*ServeConfig `json:",omitempty"` Foreground map[string]*ServeConfig `json:",omitempty"`
} }
@ -320,3 +320,102 @@ func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error {
} }
return deny(portsStr) return deny(portsStr)
} }
// RangeOverTCPs ranges over both background and foreground TCPs.
// If the returned bool from the given f is false, then this function stops
// iterating immediately and does not check other foreground configs.
func (v ServeConfigView) RangeOverTCPs(f func(port uint16, _ TCPPortHandlerView) bool) {
parentCont := true
v.TCP().Range(func(k uint16, v TCPPortHandlerView) (cont bool) {
parentCont = f(k, v)
return parentCont
})
v.Foreground().Range(func(k string, v ServeConfigView) (cont bool) {
if !parentCont {
return false
}
v.TCP().Range(func(k uint16, v TCPPortHandlerView) (cont bool) {
parentCont = f(k, v)
return parentCont
})
return parentCont
})
}
// RangeOverWebs ranges over both background and foreground Webs.
// If the returned bool from the given f is false, then this function stops
// iterating immediately and does not check other foreground configs.
func (v ServeConfigView) RangeOverWebs(f func(_ HostPort, conf WebServerConfigView) bool) {
parentCont := true
v.Web().Range(func(k HostPort, v WebServerConfigView) (cont bool) {
parentCont = f(k, v)
return parentCont
})
v.Foreground().Range(func(k string, v ServeConfigView) (cont bool) {
if !parentCont {
return false
}
v.Web().Range(func(k HostPort, v WebServerConfigView) (cont bool) {
parentCont = f(k, v)
return parentCont
})
return parentCont
})
}
// FindTCP returns the first TCP that matches with the given port. It
// prefers a foreground match first followed by a background search if none
// existed.
func (v ServeConfigView) FindTCP(port uint16) (res TCPPortHandlerView, ok bool) {
v.Foreground().Range(func(_ string, v ServeConfigView) (cont bool) {
res, ok = v.TCP().GetOk(port)
return !ok
})
if ok {
return res, ok
}
return v.TCP().GetOk(port)
}
// FindWeb returns the first Web that matches with the given HostPort. It
// prefers a foreground match first followed by a background search if none
// existed.
func (v ServeConfigView) FindWeb(hp HostPort) (res WebServerConfigView, ok bool) {
v.Foreground().Range(func(_ string, v ServeConfigView) (cont bool) {
res, ok = v.Web().GetOk(hp)
return !ok
})
if ok {
return res, ok
}
return v.Web().GetOk(hp)
}
// HasAllowFunnel returns whether this config has at least one AllowFunnel
// set in the background or foreground configs.
func (v ServeConfigView) HasAllowFunnel() bool {
return v.AllowFunnel().Len() > 0 || func() bool {
var exists bool
v.Foreground().Range(func(k string, v ServeConfigView) (cont bool) {
exists = v.AllowFunnel().Len() > 0
return !exists
})
return exists
}()
}
// FindFunnel reports whether target exists in in either the background AllowFunnel
// or any of the foreground configs.
func (v ServeConfigView) HasFunnelForTarget(target HostPort) bool {
if v.AllowFunnel().Get(target) {
return true
}
var exists bool
v.Foreground().Range(func(_ string, v ServeConfigView) (cont bool) {
if exists = v.AllowFunnel().Get(target); exists {
return false
}
return true
})
return exists
}

Loading…
Cancel
Save