From f6a1b149df3ff08408a0ad0b2d41f2a7a85200cd Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 19 Jun 2025 10:29:32 -0700 Subject: [PATCH] lol Change-Id: Idcc360abdcc723fcf5ccef9d539056c68b7aa2b2 Signed-off-by: Brad Fitzpatrick --- cmd/tinyderpclient/tinyderpclient.go | 39 +++++ derp/derp.go | 19 +++ derp/derp_server.go | 15 +- derp/derp_server_default.go | 2 +- derp/derphttp/derphttp_client.go | 110 +----------- derp/derphttp/derphttp_server.go | 10 +- hostinfo/hostinfo.go | 2 - net/dnscache/dnscache.go | 27 --- net/netmon/state.go | 11 +- net/netns/netns.go | 2 +- net/tlsdial/tlsdial.go | 56 +------ syncs/locked.go | 32 ---- types/logger/rusage.go | 2 +- util/eventbus/debug.go | 4 - util/eventbus/debughttp.go | 240 --------------------------- util/eventbus/debughttp_off.go | 20 --- 16 files changed, 76 insertions(+), 515 deletions(-) create mode 100644 cmd/tinyderpclient/tinyderpclient.go delete mode 100644 syncs/locked.go delete mode 100644 util/eventbus/debughttp.go delete mode 100644 util/eventbus/debughttp_off.go diff --git a/cmd/tinyderpclient/tinyderpclient.go b/cmd/tinyderpclient/tinyderpclient.go new file mode 100644 index 000000000..746a216ff --- /dev/null +++ b/cmd/tinyderpclient/tinyderpclient.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "maps" + "net/http" + "slices" + + "tailscale.com/derp/derphttp" + "tailscale.com/net/netmon" + "tailscale.com/tailcfg" + "tailscale.com/types/key" +) + +func main() { + dm := &tailcfg.DERPMap{} + res, err := http.Get("https://controlplane.tailscale.com/derpmap/default") + if err != nil { + log.Fatalf("fetching DERPMap: %v", err) + } + defer res.Body.Close() + if err := json.NewDecoder(res.Body).Decode(dm); err != nil { + log.Fatalf("decoding DERPMap: %v", err) + } + + region := slices.Sorted(maps.Keys(dm.Regions))[0] + + netMon := netmon.NewStatic() + rc := derphttp.NewRegionClient(key.NewNode(), log.Printf, netMon, func() *tailcfg.DERPRegion { + return dm.Regions[region] + }) + defer rc.Close() + + if err := rc.Connect(context.Background()); err != nil { + log.Fatalf("rc.Connect: %v", err) + } +} diff --git a/derp/derp.go b/derp/derp.go index 24c1ca65c..f628580ce 100644 --- a/derp/derp.go +++ b/derp/derp.go @@ -270,3 +270,22 @@ type Conn interface { SetReadDeadline(time.Time) error SetWriteDeadline(time.Time) error } + +type serverInfo struct { + Version int `json:"version,omitempty"` + + TokenBucketBytesPerSecond int `json:",omitempty"` + TokenBucketBytesBurst int `json:",omitempty"` +} + +// IdealNodeHeader is the HTTP request header sent on DERP HTTP client requests +// to indicate that they're connecting to their ideal (Region.Nodes[0]) node. +// The HTTP header value is the name of the node they wish they were connected +// to. This is an optional header. +const IdealNodeHeader = "Ideal-Node" + +// FastStartHeader is the header (with value "1") that signals to the HTTP +// server that the DERP HTTP client does not want the HTTP 101 response +// headers and it will begin writing & reading the DERP protocol immediately +// following its HTTP request. +const FastStartHeader = "Derp-Fast-Start" diff --git a/derp/derp_server.go b/derp/derp_server.go index bd67e7eec..1c9c10129 100644 --- a/derp/derp_server.go +++ b/derp/derp_server.go @@ -1,6 +1,8 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause +//go:build !ts_omit_derpserver + package derp // TODO(crawshaw): with predefined serverKey in clients and HMAC on packets we could skip TLS @@ -60,12 +62,6 @@ import ( // verbosely log whenever DERP drops a packet. var verboseDropKeys = map[key.NodePublic]bool{} -// IdealNodeHeader is the HTTP request header sent on DERP HTTP client requests -// to indicate that they're connecting to their ideal (Region.Nodes[0]) node. -// The HTTP header value is the name of the node they wish they were connected -// to. This is an optional header. -const IdealNodeHeader = "Ideal-Node" - // IdealNodeContextKey is the context key used to pass the IdealNodeHeader value // from the HTTP handler to the DERP server's Accept method. var IdealNodeContextKey = ctxkey.New[string]("ideal-node", "") @@ -1505,13 +1501,6 @@ func (s *Server) noteClientActivity(c *sclient) { dup.sendHistory = append(dup.sendHistory, c) } -type serverInfo struct { - Version int `json:"version,omitempty"` - - TokenBucketBytesPerSecond int `json:",omitempty"` - TokenBucketBytesBurst int `json:",omitempty"` -} - func (s *Server) sendServerInfo(bw *lazyBufioWriter, clientKey key.NodePublic) error { msg, err := json.Marshal(serverInfo{Version: ProtocolVersion}) if err != nil { diff --git a/derp/derp_server_default.go b/derp/derp_server_default.go index 014cfffd6..0d517b186 100644 --- a/derp/derp_server_default.go +++ b/derp/derp_server_default.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -//go:build !linux || android +//go:build !ts_omit_derpserver && (!linux || android) package derp diff --git a/derp/derphttp/derphttp_client.go b/derp/derphttp/derphttp_client.go index 7385f0ad1..1a9fdd8af 100644 --- a/derp/derphttp/derphttp_client.go +++ b/derp/derphttp/derphttp_client.go @@ -32,14 +32,10 @@ import ( "tailscale.com/derp" "tailscale.com/derp/derpconst" "tailscale.com/envknob" - "tailscale.com/health" - "tailscale.com/net/dnscache" "tailscale.com/net/netmon" - "tailscale.com/net/netns" "tailscale.com/net/netx" "tailscale.com/net/sockstats" "tailscale.com/net/tlsdial" - "tailscale.com/net/tshttpproxy" "tailscale.com/syncs" "tailscale.com/tailcfg" "tailscale.com/tstime" @@ -54,11 +50,9 @@ import ( // Send/Recv will completely re-establish the connection (unless Close // has been called). type Client struct { - TLSConfig *tls.Config // optional; nil means default - HealthTracker *health.Tracker // optional; used if non-nil only - DNSCache *dnscache.Resolver // optional; nil means no caching - MeshKey key.DERPMesh // optional; for trusted clients - IsProber bool // optional; for probers to optional declare themselves as such + TLSConfig *tls.Config // optional; nil means default + MeshKey key.DERPMesh // optional; for trusted clients + IsProber bool // optional; for probers to optional declare themselves as such // WatchConnectionChanges is whether the client wishes to subscribe to // notifications about clients connecting & disconnecting. @@ -522,7 +516,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien // just to get routed into the server's HTTP Handler so it // can Hijack the request, but we signal with a special header // that we don't want to deal with its HTTP response. - req.Header.Set(fastStartHeader, "1") // suppresses the server's HTTP response + req.Header.Set(derp.FastStartHeader, "1") // suppresses the server's HTTP response if err := req.Write(brw); err != nil { return nil, 0, err } @@ -599,20 +593,8 @@ func (c *Client) dialURL(ctx context.Context) (net.Conn, error) { return c.dialer(ctx, "tcp", net.JoinHostPort(host, urlPort(c.url))) } hostOrIP := host - dialer := netns.NewDialer(c.logf, c.netMon) - - if c.DNSCache != nil { - ip, _, _, err := c.DNSCache.LookupIP(ctx, host) - if err == nil { - hostOrIP = ip.String() - } - if err != nil && netns.IsSOCKSDialer(dialer) { - // Return an error if we're not using a dial - // proxy that can do DNS lookups for us. - return nil, err - } - } + var dialer net.Dialer tcpConn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(hostOrIP, urlPort(c.url))) if err != nil { return nil, fmt.Errorf("dial of %v: %v", host, err) @@ -647,7 +629,7 @@ func (c *Client) dialRegion(ctx context.Context, reg *tailcfg.DERPRegion) (net.C } func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn { - tlsConf := tlsdial.Config(c.HealthTracker, c.TLSConfig) + tlsConf := tlsdial.Config(nil, c.TLSConfig) if node != nil { if node.InsecureForTests { tlsConf.InsecureSkipVerify = true @@ -699,7 +681,8 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl } func (c *Client) dialContext(ctx context.Context, proto, addr string) (net.Conn, error) { - return netns.NewDialer(c.logf, c.netMon).DialContext(ctx, proto, addr) + var d net.Dialer + return d.DialContext(ctx, proto, addr) } // shouldDialProto reports whether an explicitly provided IPv4 or IPv6 @@ -723,18 +706,6 @@ const dialNodeTimeout = 1500 * time.Millisecond // TODO(bradfitz): longer if no options remain perhaps? ... Or longer // overall but have dialRegion start overlapping races? func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, error) { - // First see if we need to use an HTTP proxy. - proxyReq := &http.Request{ - Method: "GET", // doesn't really matter - URL: &url.URL{ - Scheme: "https", - Host: c.tlsServerName(n), - Path: "/", // unused - }, - } - if proxyURL, err := tshttpproxy.ProxyFromEnvironment(proxyReq); err == nil && proxyURL != nil { - return c.dialNodeUsingProxy(ctx, n, proxyURL) - } type res struct { c net.Conn @@ -827,71 +798,6 @@ func firstStr(a, b string) string { return b } -// dialNodeUsingProxy connects to n using a CONNECT to the HTTP(s) proxy in proxyURL. -func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, proxyURL *url.URL) (_ net.Conn, err error) { - pu := proxyURL - var proxyConn net.Conn - if pu.Scheme == "https" { - var d tls.Dialer - proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "443"))) - } else { - var d net.Dialer - proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "80"))) - } - defer func() { - if err != nil && proxyConn != nil { - // In a goroutine in case it's a *tls.Conn (that can block on Close) - // TODO(bradfitz): track the underlying tcp.Conn and just close that instead. - go proxyConn.Close() - } - }() - if err != nil { - return nil, err - } - - done := make(chan struct{}) - defer close(done) - go func() { - select { - case <-done: - return - case <-ctx.Done(): - proxyConn.Close() - } - }() - - target := net.JoinHostPort(n.HostName, "443") - - var authHeader string - if v, err := tshttpproxy.GetAuthHeader(pu); err != nil { - c.logf("derphttp: error getting proxy auth header for %v: %v", proxyURL, err) - } else if v != "" { - authHeader = fmt.Sprintf("Proxy-Authorization: %s\r\n", v) - } - - if _, err := fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n%s\r\n", target, target, authHeader); err != nil { - if ctx.Err() != nil { - return nil, ctx.Err() - } - return nil, err - } - - br := bufio.NewReader(proxyConn) - res, err := http.ReadResponse(br, nil) - if err != nil { - if ctx.Err() != nil { - return nil, ctx.Err() - } - c.logf("derphttp: CONNECT dial to %s: %v", target, err) - return nil, err - } - c.logf("derphttp: CONNECT dial to %s: %v", target, res.Status) - if res.StatusCode != 200 { - return nil, fmt.Errorf("invalid response status from HTTP proxy %s on CONNECT to %s: %v", pu, target, res.Status) - } - return proxyConn, nil -} - func (c *Client) Send(dstKey key.NodePublic, b []byte) error { client, _, err := c.connect(c.newContext(), "derphttp.Client.Send") if err != nil { diff --git a/derp/derphttp/derphttp_server.go b/derp/derphttp/derphttp_server.go index 50aba774a..ea2b9ea8c 100644 --- a/derp/derphttp/derphttp_server.go +++ b/derp/derphttp/derphttp_server.go @@ -1,6 +1,8 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause +//go:build !ts_omit_derpserver + package derphttp import ( @@ -12,12 +14,6 @@ import ( "tailscale.com/derp" ) -// fastStartHeader is the header (with value "1") that signals to the HTTP -// server that the DERP HTTP client does not want the HTTP 101 response -// headers and it will begin writing & reading the DERP protocol immediately -// following its HTTP request. -const fastStartHeader = "Derp-Fast-Start" - // Handler returns an http.Handler to be mounted at /derp, serving s. func Handler(s *derp.Server) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -42,7 +38,7 @@ func Handler(s *derp.Server) http.Handler { return } - fastStart := r.Header.Get(fastStartHeader) == "1" + fastStart := r.Header.Get(derp.FastStartHeader) == "1" h, ok := w.(http.Hijacker) if !ok { diff --git a/hostinfo/hostinfo.go b/hostinfo/hostinfo.go index 3e8f2f994..aef8c9dcd 100644 --- a/hostinfo/hostinfo.go +++ b/hostinfo/hostinfo.go @@ -24,7 +24,6 @@ import ( "tailscale.com/types/lazy" "tailscale.com/types/opt" "tailscale.com/types/ptr" - "tailscale.com/util/cloudenv" "tailscale.com/util/dnsname" "tailscale.com/util/lineiter" "tailscale.com/version" @@ -63,7 +62,6 @@ func New() *tailcfg.Hostinfo { GoVersion: runtime.Version(), Machine: condCall(unameMachine), DeviceModel: deviceModelCached(), - Cloud: string(cloudenv.Get()), NoLogsNoSupport: envknob.NoLogsNoSupport(), AllowsUpdate: envknob.AllowsRemoteUpdate(), } diff --git a/net/dnscache/dnscache.go b/net/dnscache/dnscache.go index d60e92f0b..3974b1320 100644 --- a/net/dnscache/dnscache.go +++ b/net/dnscache/dnscache.go @@ -21,7 +21,6 @@ import ( "tailscale.com/envknob" "tailscale.com/net/netx" "tailscale.com/types/logger" - "tailscale.com/util/cloudenv" "tailscale.com/util/singleflight" "tailscale.com/util/slicesx" "tailscale.com/util/testenv" @@ -135,26 +134,6 @@ func (r *Resolver) dlogf(format string, args ...any) { } } -// cloudHostResolver returns a Resolver for the current cloud hosting environment. -// It currently only supports Google Cloud. -func (r *Resolver) cloudHostResolver() (v *net.Resolver, ok bool) { - switch runtime.GOOS { - case "android", "ios", "darwin": - return nil, false - } - ip := cloudenv.Get().ResolverIP() - if ip == "" { - return nil, false - } - return &net.Resolver{ - PreferGo: true, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - var d net.Dialer - return d.DialContext(ctx, network, net.JoinHostPort(ip, "53")) - }, - }, true -} - func (r *Resolver) ttl() time.Duration { if r.TTL > 0 { return r.TTL @@ -296,12 +275,6 @@ func (r *Resolver) lookupIP(ctx context.Context, host string) (ip, ip6 netip.Add } else { ips, err = r.fwd().LookupNetIP(lookupCtx, "ip", host) } - if err != nil || len(ips) == 0 { - if resolver, ok := r.cloudHostResolver(); ok { - r.dlogf("resolving %q via cloud resolver", host) - ips, err = resolver.LookupNetIP(lookupCtx, "ip", host) - } - } if (err != nil || len(ips) == 0) && r.LookupIPFallback != nil { lookupCtx, lookupCancel := context.WithTimeout(ctx, 30*time.Second) defer lookupCancel() diff --git a/net/netmon/state.go b/net/netmon/state.go index bd0960768..82d941fa0 100644 --- a/net/netmon/state.go +++ b/net/netmon/state.go @@ -7,7 +7,6 @@ import ( "bytes" "fmt" "net" - "net/http" "net/netip" "runtime" "slices" @@ -18,7 +17,6 @@ import ( "tailscale.com/hostinfo" "tailscale.com/net/netaddr" "tailscale.com/net/tsaddr" - "tailscale.com/net/tshttpproxy" "tailscale.com/util/mak" ) @@ -154,7 +152,7 @@ func (i Interface) Addrs() ([]net.Addr, error) { if i.AltAddrs != nil { return i.AltAddrs, nil } - return i.Interface.Addrs() + return nil, nil } // ForeachInterfaceAddress is a wrapper for GetList, then @@ -502,13 +500,6 @@ func getState(optTSInterfaceName string) (*State, error) { } if s.AnyInterfaceUp() { - req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil) - if err != nil { - return nil, err - } - if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil { - s.HTTPProxy = u.String() - } if getPAC != nil { s.PAC = getPAC() } diff --git a/net/netns/netns.go b/net/netns/netns.go index a473506fa..742e9167a 100644 --- a/net/netns/netns.go +++ b/net/netns/netns.go @@ -90,7 +90,7 @@ func FromDialer(logf logger.Logf, netMon *netmon.Monitor, d *net.Dialer) Dialer if disabled.Load() { return d } - d.Control = control(logf, netMon) + //d.Control = control(logf, netMon) if wrapDialer != nil { return wrapDialer(d) } diff --git a/net/tlsdial/tlsdial.go b/net/tlsdial/tlsdial.go index 80f3bfc06..770900b24 100644 --- a/net/tlsdial/tlsdial.go +++ b/net/tlsdial/tlsdial.go @@ -28,10 +28,8 @@ import ( "tailscale.com/derp/derpconst" "tailscale.com/envknob" - "tailscale.com/health" "tailscale.com/hostinfo" "tailscale.com/net/bakedroots" - "tailscale.com/net/tlsdial/blockblame" ) var counterFallbackOK int32 // atomic @@ -49,16 +47,6 @@ var debug = envknob.RegisterBool("TS_DEBUG_TLS_DIAL") // Headscale, etc. var tlsdialWarningPrinted sync.Map // map[string]bool -var mitmBlockWarnable = health.Register(&health.Warnable{ - Code: "blockblame-mitm-detected", - Title: "Network may be blocking Tailscale", - Text: func(args health.Args) string { - return fmt.Sprintf("Network equipment from %q may be blocking Tailscale traffic on this network. Connect to another network, or contact your network administrator for assistance.", args["manufacturer"]) - }, - Severity: health.SeverityMedium, - ImpactsConnectivity: true, -}) - // Config returns a tls.Config for connecting to a server that // uses system roots for validation but, if those fail, also tries // the baked-in LetsEncrypt roots as a fallback validation method. @@ -66,7 +54,7 @@ var mitmBlockWarnable = health.Register(&health.Warnable{ // If base is non-nil, it's cloned as the base config before // being configured and returned. // If ht is non-nil, it's used to report health errors. -func Config(ht *health.Tracker, base *tls.Config) *tls.Config { +func Config(ht any, base *tls.Config) *tls.Config { var conf *tls.Config if base == nil { conf = new(tls.Config) @@ -109,48 +97,6 @@ func Config(ht *health.Tracker, base *tls.Config) *tls.Config { return nil } - // Perform some health checks on this certificate before we do - // any verification. - var cert *x509.Certificate - var selfSignedIssuer string - if certs := cs.PeerCertificates; len(certs) > 0 { - cert = certs[0] - if certIsSelfSigned(cert) { - selfSignedIssuer = cert.Issuer.String() - } - } - if ht != nil { - defer func() { - if retErr != nil && cert != nil { - // Is it a MITM SSL certificate from a well-known network appliance manufacturer? - // Show a dedicated warning. - m, ok := blockblame.VerifyCertificate(cert) - if ok { - log.Printf("tlsdial: server cert seen while dialing %q looks like %q equipment (could be blocking Tailscale)", dialedHost, m.Name) - ht.SetUnhealthy(mitmBlockWarnable, health.Args{"manufacturer": m.Name}) - } else { - ht.SetHealthy(mitmBlockWarnable) - } - } else { - ht.SetHealthy(mitmBlockWarnable) - } - if retErr != nil && selfSignedIssuer != "" { - // Self-signed certs are never valid. - // - // TODO(bradfitz): plumb down the selfSignedIssuer as a - // structured health warning argument. - ht.SetTLSConnectionError(cs.ServerName, fmt.Errorf("likely intercepted connection; certificate is self-signed by %v", selfSignedIssuer)) - } else { - // Ensure we clear any error state for this ServerName. - ht.SetTLSConnectionError(cs.ServerName, nil) - if selfSignedIssuer != "" { - // Log the self-signed issuer, but don't treat it as an error. - log.Printf("tlsdial: warning: server cert for %q passed x509 validation but is self-signed by %q", dialedHost, selfSignedIssuer) - } - } - }() - } - // First try doing x509 verification with the system's // root CA pool. opts := x509.VerifyOptions{ diff --git a/syncs/locked.go b/syncs/locked.go deleted file mode 100644 index d2048665d..000000000 --- a/syncs/locked.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package syncs - -import ( - "sync" -) - -// AssertLocked panics if m is not locked. -func AssertLocked(m *sync.Mutex) { - if m.TryLock() { - m.Unlock() - panic("mutex is not locked") - } -} - -// AssertRLocked panics if rw is not locked for reading or writing. -func AssertRLocked(rw *sync.RWMutex) { - if rw.TryLock() { - rw.Unlock() - panic("mutex is not locked") - } -} - -// AssertWLocked panics if rw is not locked for writing. -func AssertWLocked(rw *sync.RWMutex) { - if rw.TryRLock() { - rw.RUnlock() - panic("mutex is not rlocked") - } -} diff --git a/types/logger/rusage.go b/types/logger/rusage.go index 3943636d6..7b728ee0b 100644 --- a/types/logger/rusage.go +++ b/types/logger/rusage.go @@ -15,7 +15,7 @@ func RusagePrefixLog(logf Logf) Logf { return func(f string, argv ...any) { var m runtime.MemStats runtime.ReadMemStats(&m) - goMem := float64(m.HeapInuse+m.StackInuse) / (1 << 20) + goMem := float64(m.HeapInuse) / (1 << 20) maxRSS := rusageMaxRSS() pf := fmt.Sprintf("%.1fM/%.1fM %s", goMem, maxRSS, f) logf(pf, argv...) diff --git a/util/eventbus/debug.go b/util/eventbus/debug.go index b6264f82f..107dd0280 100644 --- a/util/eventbus/debug.go +++ b/util/eventbus/debug.go @@ -10,8 +10,6 @@ import ( "slices" "sync" "sync/atomic" - - "tailscale.com/tsweb" ) // A Debugger offers access to a bus's privileged introspection and @@ -137,8 +135,6 @@ func (d *Debugger) SubscribeTypes(client *Client) []reflect.Type { return client.subscribeTypes() } -func (d *Debugger) RegisterHTTP(td *tsweb.DebugHandler) { registerHTTPDebugger(d, td) } - // A hook collects hook functions that can be run as a group. type hook[T any] struct { sync.Mutex diff --git a/util/eventbus/debughttp.go b/util/eventbus/debughttp.go deleted file mode 100644 index a94eaa9cf..000000000 --- a/util/eventbus/debughttp.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -//go:build !ios && !android - -package eventbus - -import ( - "bytes" - "cmp" - "embed" - "fmt" - "html/template" - "io" - "io/fs" - "log" - "net/http" - "path/filepath" - "reflect" - "slices" - "strings" - "sync" - - "github.com/coder/websocket" - "tailscale.com/tsweb" -) - -type httpDebugger struct { - *Debugger -} - -func registerHTTPDebugger(d *Debugger, td *tsweb.DebugHandler) { - dh := httpDebugger{d} - td.Handle("bus", "Event bus", dh) - td.HandleSilent("bus/monitor", http.HandlerFunc(dh.serveMonitor)) - td.HandleSilent("bus/style.css", serveStatic("style.css")) - td.HandleSilent("bus/htmx.min.js", serveStatic("htmx.min.js.gz")) - td.HandleSilent("bus/htmx-websocket.min.js", serveStatic("htmx-websocket.min.js.gz")) -} - -//go:embed assets/*.html -var templatesSrc embed.FS - -var templates = sync.OnceValue(func() *template.Template { - d, err := fs.Sub(templatesSrc, "assets") - if err != nil { - panic(fmt.Errorf("getting eventbus debughttp templates subdir: %w", err)) - } - ret := template.New("").Funcs(map[string]any{ - "prettyPrintStruct": prettyPrintStruct, - }) - return template.Must(ret.ParseFS(d, "*")) -}) - -//go:generate go run fetch-htmx.go - -//go:embed assets/*.css assets/*.min.js.gz -var static embed.FS - -func serveStatic(name string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case strings.HasSuffix(name, ".css"): - w.Header().Set("Content-Type", "text/css") - case strings.HasSuffix(name, ".min.js.gz"): - w.Header().Set("Content-Type", "text/javascript") - w.Header().Set("Content-Encoding", "gzip") - case strings.HasSuffix(name, ".js"): - w.Header().Set("Content-Type", "text/javascript") - default: - http.Error(w, "not found", http.StatusNotFound) - return - } - - f, err := static.Open(filepath.Join("assets", name)) - if err != nil { - http.Error(w, fmt.Sprintf("opening asset: %v", err), http.StatusInternalServerError) - return - } - defer f.Close() - if _, err := io.Copy(w, f); err != nil { - http.Error(w, fmt.Sprintf("serving asset: %v", err), http.StatusInternalServerError) - return - } - }) -} - -func render(w http.ResponseWriter, name string, data any) { - err := templates().ExecuteTemplate(w, name+".html", data) - if err != nil { - err := fmt.Errorf("rendering template: %v", err) - log.Print(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func (h httpDebugger) ServeHTTP(w http.ResponseWriter, r *http.Request) { - type clientInfo struct { - *Client - Publish []reflect.Type - Subscribe []reflect.Type - } - type typeInfo struct { - reflect.Type - Publish []*Client - Subscribe []*Client - } - type info struct { - *Debugger - Clients map[string]*clientInfo - Types map[string]*typeInfo - } - - data := info{ - Debugger: h.Debugger, - Clients: map[string]*clientInfo{}, - Types: map[string]*typeInfo{}, - } - - getTypeInfo := func(t reflect.Type) *typeInfo { - if data.Types[t.Name()] == nil { - data.Types[t.Name()] = &typeInfo{ - Type: t, - } - } - return data.Types[t.Name()] - } - - for _, c := range h.Clients() { - ci := &clientInfo{ - Client: c, - Publish: h.PublishTypes(c), - Subscribe: h.SubscribeTypes(c), - } - slices.SortFunc(ci.Publish, func(a, b reflect.Type) int { return cmp.Compare(a.Name(), b.Name()) }) - slices.SortFunc(ci.Subscribe, func(a, b reflect.Type) int { return cmp.Compare(a.Name(), b.Name()) }) - data.Clients[c.Name()] = ci - - for _, t := range ci.Publish { - ti := getTypeInfo(t) - ti.Publish = append(ti.Publish, c) - } - for _, t := range ci.Subscribe { - ti := getTypeInfo(t) - ti.Subscribe = append(ti.Subscribe, c) - } - } - - render(w, "main", data) -} - -func (h httpDebugger) serveMonitor(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Upgrade") == "websocket" { - h.serveMonitorStream(w, r) - return - } - - render(w, "monitor", nil) -} - -func (h httpDebugger) serveMonitorStream(w http.ResponseWriter, r *http.Request) { - conn, err := websocket.Accept(w, r, nil) - if err != nil { - return - } - defer conn.CloseNow() - wsCtx := conn.CloseRead(r.Context()) - - mon := h.WatchBus() - defer mon.Close() - - i := 0 - for { - select { - case <-r.Context().Done(): - return - case <-wsCtx.Done(): - return - case <-mon.Done(): - return - case event := <-mon.Events(): - msg, err := conn.Writer(r.Context(), websocket.MessageText) - if err != nil { - return - } - data := map[string]any{ - "Count": i, - "Type": reflect.TypeOf(event.Event), - "Event": event, - } - i++ - if err := templates().ExecuteTemplate(msg, "event.html", data); err != nil { - log.Println(err) - return - } - if err := msg.Close(); err != nil { - return - } - } - } -} - -func prettyPrintStruct(t reflect.Type) string { - if t.Kind() != reflect.Struct { - return t.String() - } - var rec func(io.Writer, int, reflect.Type) - rec = func(out io.Writer, indent int, t reflect.Type) { - ind := strings.Repeat(" ", indent) - fmt.Fprintf(out, "%s", t.String()) - fs := collectFields(t) - if len(fs) > 0 { - io.WriteString(out, " {\n") - for _, f := range fs { - fmt.Fprintf(out, "%s %s ", ind, f.Name) - if f.Type.Kind() == reflect.Struct { - rec(out, indent+1, f.Type) - } else { - fmt.Fprint(out, f.Type) - } - io.WriteString(out, "\n") - } - fmt.Fprintf(out, "%s}", ind) - } - } - - var ret bytes.Buffer - rec(&ret, 0, t) - return ret.String() -} - -func collectFields(t reflect.Type) (ret []reflect.StructField) { - for _, f := range reflect.VisibleFields(t) { - if !f.IsExported() { - continue - } - ret = append(ret, f) - } - return ret -} diff --git a/util/eventbus/debughttp_off.go b/util/eventbus/debughttp_off.go deleted file mode 100644 index 85330579c..000000000 --- a/util/eventbus/debughttp_off.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -//go:build ios || android - -package eventbus - -import "tailscale.com/tsweb" - -func registerHTTPDebugger(d *Debugger, td *tsweb.DebugHandler) { - // The event bus debugging UI uses html/template, which uses - // reflection for method lookups. This forces the compiler to - // retain a lot more code and information to make dynamic method - // dispatch work, which is unacceptable bloat for the iOS build. - // We also disable it on Android while we're at it, as nobody - // is debugging Tailscale internals on Android. - // - // TODO: https://github.com/tailscale/tailscale/issues/15297 to - // bring the debug UI back to iOS somehow. -}