tailscale/cmd/tailscaled/debug.go
Mihai Parparita 7330aa593e all: avoid repeated default interface lookups
On some platforms (notably macOS and iOS) we look up the default
interface to bind outgoing connections to. This is both duplicated
work and results in logspam when the default interface is not available
(i.e. when a phone has no connectivity, we log an error and thus cause
more things that we will try to upload and fail).

Fixed by passing around a netmon.Monitor to more places, so that we can
use its cached interface state.

Fixes #7850
Updates #7621

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-04-20 15:46:01 -07:00

228 lines
6.3 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build go1.19
package main
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/http/httptrace"
"net/url"
"os"
"time"
"tailscale.com/derp/derphttp"
"tailscale.com/ipn"
"tailscale.com/net/interfaces"
"tailscale.com/net/netmon"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
var debugArgs struct {
ifconfig bool // print network state once and exit
monitor bool
getURL string
derpCheck string
portmap bool
}
var debugModeFunc = debugMode // so it can be addressable
func debugMode(args []string) error {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run network monitor forever. Precludes all other options.")
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
if err := fs.Parse(args); err != nil {
return err
}
if len(fs.Args()) > 0 {
return errors.New("unknown non-flag debug subcommand arguments")
}
ctx := context.Background()
if debugArgs.derpCheck != "" {
return checkDerp(ctx, debugArgs.derpCheck)
}
if debugArgs.ifconfig {
return runMonitor(ctx, false)
}
if debugArgs.monitor {
return runMonitor(ctx, true)
}
if debugArgs.portmap {
return debugPortmap(ctx)
}
if debugArgs.getURL != "" {
return getURL(ctx, debugArgs.getURL)
}
return errors.New("only --monitor is available at the moment")
}
func runMonitor(ctx context.Context, loop bool) error {
dump := func(st *interfaces.State) {
j, _ := json.MarshalIndent(st, "", " ")
os.Stderr.Write(j)
}
mon, err := netmon.New(log.Printf)
if err != nil {
return err
}
defer mon.Close()
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
if !changed {
log.Printf("Network monitor fired; no change")
return
}
log.Printf("Network monitor fired. New state:")
dump(st)
})
if loop {
log.Printf("Starting link change monitor; initial state:")
}
dump(mon.InterfaceState())
if !loop {
return nil
}
mon.Start()
log.Printf("Started link change monitor; waiting...")
select {}
}
func getURL(ctx context.Context, urlStr string) error {
if urlStr == "login" {
urlStr = "https://login.tailscale.com"
}
log.SetOutput(os.Stdout)
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
})
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
if err != nil {
return fmt.Errorf("http.NewRequestWithContext: %v", err)
}
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
if err != nil {
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
}
log.Printf("proxy: %v", proxyURL)
tr := &http.Transport{
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
ProxyConnectHeader: http.Header{},
DisableKeepAlives: true,
}
if proxyURL != nil {
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
if err == nil && auth != "" {
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
}
log.Printf("tshttpproxy.GetAuthHeader(%v) got: auth of %d bytes, err=%v", proxyURL, len(auth), err)
const truncLen = 20
if len(auth) > truncLen {
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
}
if auth != "" {
// We used log.Printf above (for timestamps).
// Use fmt.Printf here instead just to appease
// a security scanner, despite log.Printf only
// going to stdout.
fmt.Printf("... Proxy-Authorization = %q\n", auth)
}
}
res, err := tr.RoundTrip(req)
if err != nil {
return fmt.Errorf("Transport.RoundTrip: %v", err)
}
defer res.Body.Close()
return res.Write(os.Stdout)
}
func checkDerp(ctx context.Context, derpRegion string) (err error) {
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
if err != nil {
return fmt.Errorf("create derp map request: %w", err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fetch derp map failed: %w", err)
}
defer res.Body.Close()
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return fmt.Errorf("fetch derp map failed: %w", err)
}
if res.StatusCode != 200 {
return fmt.Errorf("fetch derp map: %v: %s", res.Status, b)
}
var dmap tailcfg.DERPMap
if err = json.Unmarshal(b, &dmap); err != nil {
return fmt.Errorf("fetch DERP map: %w", err)
}
getRegion := func() *tailcfg.DERPRegion {
for _, r := range dmap.Regions {
if r.RegionCode == derpRegion {
return r
}
}
for _, r := range dmap.Regions {
log.Printf("Known region: %q", r.RegionCode)
}
log.Fatalf("unknown region %q", derpRegion)
panic("unreachable")
}
priv1 := key.NewNode()
priv2 := key.NewNode()
c1 := derphttp.NewRegionClient(priv1, log.Printf, nil, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, nil, getRegion)
defer func() {
if err != nil {
c1.Close()
c2.Close()
}
}()
c2.NotePreferred(true) // just to open it
m, err := c2.Recv()
log.Printf("c2 got %T, %v", m, err)
t0 := time.Now()
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
return err
}
fmt.Println(time.Since(t0))
m, err = c2.Recv()
log.Printf("c2 got %T, %v", m, err)
if err != nil {
return err
}
log.Printf("ok")
return err
}
func debugPortmap(ctx context.Context) error {
return fmt.Errorf("this flag has been deprecated in favour of 'tailscale debug portmap'")
}