summaryrefslogtreecommitdiffhomepage
path: root/cmd/tailscaled/cli/ping.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/tailscaled/cli/ping.go')
-rw-r--r--cmd/tailscaled/cli/ping.go179
1 files changed, 179 insertions, 0 deletions
diff --git a/cmd/tailscaled/cli/ping.go b/cmd/tailscaled/cli/ping.go
new file mode 100644
index 000000000..25470aa69
--- /dev/null
+++ b/cmd/tailscaled/cli/ping.go
@@ -0,0 +1,179 @@
+// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cli
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "log"
+ "net"
+ "strings"
+ "time"
+
+ "github.com/peterbourgon/ff/v2/ffcli"
+ "tailscale.com/client/tailscale"
+ "tailscale.com/ipn"
+ "tailscale.com/ipn/ipnstate"
+)
+
+var pingCmd = &ffcli.Command{
+ Name: "ping",
+ ShortUsage: "ping <hostname-or-IP>",
+ ShortHelp: "Ping a host at the Tailscale layer, see how it routed",
+ LongHelp: strings.TrimSpace(`
+
+The 'tailscale ping' command pings a peer node at the Tailscale layer
+and reports which route it took for each response. The first ping or
+so will likely go over DERP (Tailscale's TCP relay protocol) while NAT
+traversal finds a direct path through.
+
+If 'tailscale ping' works but a normal ping does not, that means one
+side's operating system firewall is blocking packets; 'tailscale ping'
+does not inject packets into either side's TUN devices.
+
+By default, 'tailscale ping' stops after 10 pings or once a direct
+(non-DERP) path has been established, whichever comes first.
+
+The provided hostname must resolve to or be a Tailscale IP
+(e.g. 100.x.y.z) or a subnet IP advertised by a Tailscale
+relay node.
+
+`),
+ Exec: runPing,
+ FlagSet: (func() *flag.FlagSet {
+ fs := flag.NewFlagSet("ping", flag.ExitOnError)
+ fs.BoolVar(&pingArgs.verbose, "verbose", false, "verbose output")
+ fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established")
+ fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through IP + wireguard, but not involving host OS stack)")
+ fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
+ fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
+ return fs
+ })(),
+}
+
+var pingArgs struct {
+ num int
+ untilDirect bool
+ verbose bool
+ tsmp bool
+ timeout time.Duration
+}
+
+func runPing(ctx context.Context, args []string) error {
+ c, bc, ctx, cancel := connect(ctx)
+ defer cancel()
+
+ if len(args) != 1 || args[0] == "" {
+ return errors.New("usage: ping <hostname-or-IP>")
+ }
+ var ip string
+ prc := make(chan *ipnstate.PingResult, 1)
+ bc.SetNotifyCallback(func(n ipn.Notify) {
+ if n.ErrMessage != nil {
+ log.Fatal(*n.ErrMessage)
+ }
+ if pr := n.PingResult; pr != nil && pr.IP == ip {
+ prc <- pr
+ }
+ })
+ pumpErr := make(chan error, 1)
+ go func() { pumpErr <- pump(ctx, bc, c) }()
+
+ hostOrIP := args[0]
+ ip, err := tailscaleIPFromArg(ctx, hostOrIP)
+ if err != nil {
+ return err
+ }
+
+ if pingArgs.verbose && ip != hostOrIP {
+ log.Printf("lookup %q => %q", hostOrIP, ip)
+ }
+
+ n := 0
+ anyPong := false
+ for {
+ n++
+ bc.Ping(ip, pingArgs.tsmp)
+ timer := time.NewTimer(pingArgs.timeout)
+ select {
+ case <-timer.C:
+ fmt.Printf("timeout waiting for ping reply\n")
+ case err := <-pumpErr:
+ return err
+ case pr := <-prc:
+ timer.Stop()
+ if pr.Err != "" {
+ return errors.New(pr.Err)
+ }
+ latency := time.Duration(pr.LatencySeconds * float64(time.Second)).Round(time.Millisecond)
+ via := pr.Endpoint
+ if pr.DERPRegionID != 0 {
+ via = fmt.Sprintf("DERP(%s)", pr.DERPRegionCode)
+ }
+ if pingArgs.tsmp {
+ // TODO(bradfitz): populate the rest of ipnstate.PingResult for TSMP queries?
+ // For now just say it came via TSMP.
+ via = "TSMP"
+ }
+ anyPong = true
+ extra := ""
+ if pr.PeerAPIPort != 0 {
+ extra = fmt.Sprintf(", %d", pr.PeerAPIPort)
+ }
+ fmt.Printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
+ if pingArgs.tsmp {
+ return nil
+ }
+ if pr.Endpoint != "" && pingArgs.untilDirect {
+ return nil
+ }
+ time.Sleep(time.Second)
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ if n == pingArgs.num {
+ if !anyPong {
+ return errors.New("no reply")
+ }
+ if pingArgs.untilDirect {
+ return errors.New("direct connection not established")
+ }
+ return nil
+ }
+ }
+}
+
+func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, err error) {
+ // If the argument is an IP address, use it directly without any resolution.
+ if net.ParseIP(hostOrIP) != nil {
+ return hostOrIP, nil
+ }
+
+ // Otherwise, try to resolve it first from the network peer list.
+ st, err := tailscale.Status(ctx)
+ if err != nil {
+ return "", err
+ }
+ for _, ps := range st.Peer {
+ if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
+ if len(ps.TailscaleIPs) == 0 {
+ return "", errors.New("node found but lacks an IP")
+ }
+ return ps.TailscaleIPs[0].String(), nil
+ }
+ }
+
+ // Finally, use DNS.
+ var res net.Resolver
+ if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
+ return "", fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
+ } else if len(addrs) == 0 {
+ return "", fmt.Errorf("no IPs found for %q", hostOrIP)
+ } else {
+ return addrs[0], nil
+ }
+}