summaryrefslogtreecommitdiffhomepage
path: root/cmd/tailscaled/cli/ping.go
blob: 25470aa69820d25fd792e3186d78a0e43d06d7c3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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
	}
}