summaryrefslogtreecommitdiffhomepage
path: root/ipn
diff options
context:
space:
mode:
Diffstat (limited to 'ipn')
-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
7 files changed, 140 insertions, 253 deletions
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.