For example: $ tailscale ping -h USAGE ping <hostname-or-IP> FLAGS -c 10 max number of pings to send -stop-once-direct true stop once a direct path is established -verbose false verbose output $ tailscale ping mon.ts.tailscale.com pong from monitoring (100.88.178.64) via DERP(sfo) in 65ms pong from monitoring (100.88.178.64) via DERP(sfo) in 252ms pong from monitoring (100.88.178.64) via [2604:a880:2:d1::36:d001]:41641 in 33ms Fixes #661 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>main
parent
d65e2632ab
commit
84dc891843
@ -0,0 +1,130 @@ |
||||
// 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/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.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 |
||||
timeout time.Duration |
||||
} |
||||
|
||||
func runPing(ctx context.Context, args []string) error { |
||||
c, bc, ctx, cancel := connect(ctx) |
||||
defer cancel() |
||||
|
||||
if len(args) != 1 { |
||||
return errors.New("usage: ping <hostname-or-IP>") |
||||
} |
||||
hostOrIP := args[0] |
||||
var ip string |
||||
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 { |
||||
ip = addrs[0] |
||||
} |
||||
if pingArgs.verbose && ip != hostOrIP { |
||||
log.Printf("lookup %q => %q", hostOrIP, ip) |
||||
} |
||||
|
||||
ch := 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 { |
||||
ch <- pr |
||||
} |
||||
}) |
||||
go pump(ctx, bc, c) |
||||
|
||||
n := 0 |
||||
anyPong := false |
||||
for { |
||||
n++ |
||||
bc.Ping(ip) |
||||
timer := time.NewTimer(pingArgs.timeout) |
||||
select { |
||||
case <-timer.C: |
||||
fmt.Printf("timeout waiting for ping reply\n") |
||||
case pr := <-ch: |
||||
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) |
||||
} |
||||
anyPong = true |
||||
fmt.Printf("pong from %s (%s) via %v in %v\n", pr.NodeName, pr.NodeIP, via, latency) |
||||
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") |
||||
} |
||||
return nil |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue