tailscale/net/ping/ping.go

255 lines
5.6 KiB
Go
Raw Normal View History

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package ping allows sending ICMP echo requests to a host in order to
// determine network latency.
package ping
import (
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"log"
"net"
"sync"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
)
type response struct {
t time.Time
err error
}
type outstanding struct {
ch chan response
data []byte
}
// Pinger represents a set of ICMP echo requests to be sent at a single time.
//
// A new instance should be created for each concurrent set of ping requests;
// this type should not be reused.
type Pinger struct {
c net.PacketConn
Logf logger.Logf
Verbose bool
timeNow func() time.Time
id uint16 // uint16 per RFC 792
wg sync.WaitGroup
// Following fields protected by mu
mu sync.Mutex
seq uint16 // uint16 per RFC 792
pings map[uint16]outstanding
}
// New creates a new Pinger. The Context provided will be used to create
// network listeners, and to set an absolute deadline (if any) on the net.Conn
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
func New(ctx context.Context, logf logger.Logf, netMon *netmon.Monitor) (*Pinger, error) {
p, err := newUnstarted(ctx, logf, netMon)
if err != nil {
return nil, err
}
// Start by setting the deadline from the context; note that this
// applies to all future I/O, so we only need to do it once.
deadline, ok := ctx.Deadline()
if ok {
if err := p.c.SetReadDeadline(deadline); err != nil {
return nil, err
}
}
p.wg.Add(1)
go p.run(ctx)
return p, nil
}
func newUnstarted(ctx context.Context, logf logger.Logf, netMon *netmon.Monitor) (*Pinger, error) {
var id [2]byte
_, err := rand.Read(id[:])
if err != nil {
return nil, err
}
conn, err := netns.Listener(logf, netMon).ListenPacket(ctx, "ip4:icmp", "0.0.0.0")
if err != nil {
return nil, err
}
return &Pinger{
c: conn,
Logf: logf,
timeNow: time.Now,
id: binary.LittleEndian.Uint16(id[:]),
pings: make(map[uint16]outstanding),
}, nil
}
func (p *Pinger) logf(format string, a ...any) {
if p.Logf != nil {
p.Logf(format, a...)
} else {
log.Printf(format, a...)
}
}
func (p *Pinger) vlogf(format string, a ...any) {
if p.Verbose {
p.logf(format, a...)
}
}
func (p *Pinger) Close() error {
err := p.c.Close()
p.wg.Wait()
return err
}
func (p *Pinger) run(ctx context.Context) {
defer p.wg.Done()
buf := make([]byte, 1500)
loop:
for {
select {
case <-ctx.Done():
break loop
default:
}
n, addr, err := p.c.ReadFrom(buf)
if err != nil {
// Ignore temporary errors; everything else is fatal
if netErr, ok := err.(net.Error); !ok || !netErr.Temporary() {
break
}
continue
}
p.handleResponse(buf[:n], addr, p.timeNow())
}
p.cleanupOutstanding()
}
func (p *Pinger) cleanupOutstanding() {
// Complete outstanding requests
p.mu.Lock()
defer p.mu.Unlock()
for _, o := range p.pings {
o.ch <- response{err: net.ErrClosed}
}
}
func (p *Pinger) handleResponse(buf []byte, addr net.Addr, now time.Time) {
const ProtocolICMP = 1
m, err := icmp.ParseMessage(ProtocolICMP, buf)
if err != nil {
p.vlogf("handleResponse: invalid packet: %v", err)
return
}
if m.Type != ipv4.ICMPTypeEchoReply {
p.vlogf("handleResponse: wanted m.Type=%d; got %d", ipv4.ICMPTypeEchoReply, m.Type)
return
}
resp, ok := m.Body.(*icmp.Echo)
if !ok || resp == nil {
p.vlogf("handleResponse: wanted body=*icmp.Echo; got %v", m.Body)
return
}
// We assume we sent this if the ID in the response is ours.
if uint16(resp.ID) != p.id {
p.vlogf("handleResponse: wanted ID=%d; got %d", p.id, resp.ID)
return
}
// Search for existing running echo request
var o outstanding
p.mu.Lock()
if o, ok = p.pings[uint16(resp.Seq)]; ok {
// Ensure that the data matches before we delete from our map,
// so a future correct packet will be handled correctly.
if bytes.Equal(resp.Data, o.data) {
delete(p.pings, uint16(resp.Seq))
} else {
p.vlogf("handleResponse: got response for Seq %d with mismatched data", resp.Seq)
ok = false
}
} else {
p.vlogf("handleResponse: got response for unknown Seq %d", resp.Seq)
}
p.mu.Unlock()
if ok {
o.ch <- response{t: now}
}
}
// Send sends an ICMP Echo Request packet to the destination, waits for a
// response, and returns the duration between when the request was sent and
// when the reply was received.
//
// If provided, "data" is sent with the packet and is compared upon receiving a
// reply.
func (p *Pinger) Send(ctx context.Context, dest net.Addr, data []byte) (time.Duration, error) {
// Use sequential sequence numbers on the assumption that we will not
// wrap around when using a single Pinger instance
p.mu.Lock()
p.seq++
seq := p.seq
p.mu.Unlock()
m := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: int(p.id),
Seq: int(seq),
Data: data,
},
}
b, err := m.Marshal(nil)
if err != nil {
return 0, err
}
// Register our response before sending since we could otherwise race a
// quick reply.
ch := make(chan response, 1)
p.mu.Lock()
p.pings[seq] = outstanding{ch: ch, data: data}
p.mu.Unlock()
start := p.timeNow()
n, err := p.c.WriteTo(b, dest)
if err != nil {
return 0, err
} else if n != len(b) {
return 0, fmt.Errorf("conn.WriteTo: got %v; want %v", n, len(b))
}
select {
case resp := <-ch:
if resp.err != nil {
return 0, resp.err
}
return resp.t.Sub(start), nil
case <-ctx.Done():
return 0, ctx.Err()
}
}