mirror of https://github.com/tailscale/tailscale/
cmd/tailscale/cli: add ping subcommand
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>pull/668/merge
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