// 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" "net/http" "net/http/httptrace" "net/netip" "net/url" "os" "strings" "time" "tailscale.com/derp/derphttp" "tailscale.com/envknob" "tailscale.com/ipn" "tailscale.com/net/interfaces" "tailscale.com/net/portmapper" "tailscale.com/net/tshttpproxy" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/wgengine/monitor" ) 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 link 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 := monitor.New(log.Printf) if err != nil { return err } defer mon.Close() mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) { if !changed { log.Printf("Link monitor fired; no change") return } log.Printf("Link 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, getRegion) c2 := derphttp.NewRegionClient(priv2, log.Printf, 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 { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() portmapper.VerboseLogs = true switch envknob.String("TS_DEBUG_PORTMAP_TYPE") { case "": case "pmp": portmapper.DisablePCP = true portmapper.DisableUPnP = true case "pcp": portmapper.DisablePMP = true portmapper.DisableUPnP = true case "upnp": portmapper.DisablePCP = true portmapper.DisablePMP = true default: log.Fatalf("TS_DEBUG_PORTMAP_TYPE must be one of pmp,pcp,upnp") } done := make(chan bool, 1) var c *portmapper.Client logf := log.Printf c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), func() { logf("portmapping changed.") logf("have mapping: %v", c.HaveMapping()) if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok { logf("cb: mapping: %v", ext) select { case done <- true: default: } return } logf("cb: no mapping") }) linkMon, err := monitor.New(logger.WithPrefix(logf, "monitor: ")) if err != nil { return err } gatewayAndSelfIP := func() (gw, self netip.Addr, ok bool) { if v := os.Getenv("TS_DEBUG_GW_SELF"); strings.Contains(v, "/") { i := strings.Index(v, "/") gw = netip.MustParseAddr(v[:i]) self = netip.MustParseAddr(v[i+1:]) return gw, self, true } return linkMon.GatewayAndSelfIP() } c.SetGatewayLookupFunc(gatewayAndSelfIP) gw, selfIP, ok := gatewayAndSelfIP() if !ok { logf("no gateway or self IP; %v", linkMon.InterfaceState()) return nil } logf("gw=%v; self=%v", gw, selfIP) uc, err := net.ListenPacket("udp", "0.0.0.0:0") if err != nil { return err } defer uc.Close() c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port)) res, err := c.Probe(ctx) if err != nil { return fmt.Errorf("Probe: %v", err) } logf("Probe: %+v", res) if !res.PCP && !res.PMP && !res.UPnP { logf("no portmapping services available") return nil } if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok { logf("mapping: %v", ext) } else { logf("no mapping") } select { case <-done: return nil case <-ctx.Done(): return ctx.Err() } }