mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-29 15:23:45 +00:00
lol
Change-Id: Idcc360abdcc723fcf5ccef9d539056c68b7aa2b2 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
583f740c0b
commit
f6a1b149df
39
cmd/tinyderpclient/tinyderpclient.go
Normal file
39
cmd/tinyderpclient/tinyderpclient.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
19
derp/derp.go
19
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"
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
@ -55,8 +51,6 @@ import (
|
||||
// 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
|
||||
|
||||
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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(),
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -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...)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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.
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user