351 lines
7.4 KiB
Go
351 lines
7.4 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ping
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/net/icmp"
|
|
"golang.org/x/net/ipv4"
|
|
"golang.org/x/net/ipv6"
|
|
"tailscale.com/tstest"
|
|
"tailscale.com/util/mak"
|
|
)
|
|
|
|
var (
|
|
localhost = &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)}
|
|
)
|
|
|
|
func TestPinger(t *testing.T) {
|
|
clock := &tstest.Clock{}
|
|
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
|
|
p, closeP := mockPinger(t, clock)
|
|
defer closeP()
|
|
|
|
bodyData := []byte("data goes here")
|
|
|
|
// Start a ping in the background
|
|
r := make(chan time.Duration, 1)
|
|
go func() {
|
|
dur, err := p.Send(ctx, localhost, bodyData)
|
|
if err != nil {
|
|
t.Errorf("p.Send: %v", err)
|
|
r <- 0
|
|
} else {
|
|
r <- dur
|
|
}
|
|
}()
|
|
|
|
p.waitOutstanding(t, ctx, 1)
|
|
|
|
// Fake a response from ourself
|
|
fakeResponse := mustMarshal(t, &icmp.Message{
|
|
Type: ipv4.ICMPTypeEchoReply,
|
|
Code: ipv4.ICMPTypeEchoReply.Protocol(),
|
|
Body: &icmp.Echo{
|
|
ID: 1234,
|
|
Seq: 1,
|
|
Data: bodyData,
|
|
},
|
|
})
|
|
|
|
const fakeDuration = 100 * time.Millisecond
|
|
p.handleResponse(fakeResponse, clock.Now().Add(fakeDuration), v4Type)
|
|
|
|
select {
|
|
case dur := <-r:
|
|
want := fakeDuration
|
|
if dur != want {
|
|
t.Errorf("wanted ping response time = %d; got %d", want, dur)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("did not get response by timeout")
|
|
}
|
|
}
|
|
|
|
func TestV6Pinger(t *testing.T) {
|
|
if c, err := net.ListenPacket("udp6", "::1"); err != nil {
|
|
// skip test if we can't use IPv6.
|
|
t.Skipf("IPv6 not supported: %s", err)
|
|
} else {
|
|
c.Close()
|
|
}
|
|
|
|
clock := &tstest.Clock{}
|
|
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
|
|
p, closeP := mockPinger(t, clock)
|
|
defer closeP()
|
|
|
|
bodyData := []byte("data goes here")
|
|
|
|
// Start a ping in the background
|
|
r := make(chan time.Duration, 1)
|
|
go func() {
|
|
dur, err := p.Send(ctx, &net.IPAddr{IP: net.ParseIP("::")}, bodyData)
|
|
if err != nil {
|
|
t.Errorf("p.Send: %v", err)
|
|
r <- 0
|
|
} else {
|
|
r <- dur
|
|
}
|
|
}()
|
|
|
|
p.waitOutstanding(t, ctx, 1)
|
|
|
|
// Fake a response from ourself
|
|
fakeResponse := mustMarshal(t, &icmp.Message{
|
|
Type: ipv6.ICMPTypeEchoReply,
|
|
Code: ipv6.ICMPTypeEchoReply.Protocol(),
|
|
Body: &icmp.Echo{
|
|
ID: 1234,
|
|
Seq: 1,
|
|
Data: bodyData,
|
|
},
|
|
})
|
|
|
|
const fakeDuration = 100 * time.Millisecond
|
|
p.handleResponse(fakeResponse, clock.Now().Add(fakeDuration), v6Type)
|
|
|
|
select {
|
|
case dur := <-r:
|
|
want := fakeDuration
|
|
if dur != want {
|
|
t.Errorf("wanted ping response time = %d; got %d", want, dur)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("did not get response by timeout")
|
|
}
|
|
}
|
|
|
|
func TestPingerTimeout(t *testing.T) {
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
|
|
clock := &tstest.Clock{}
|
|
p, closeP := mockPinger(t, clock)
|
|
defer closeP()
|
|
|
|
// Send a ping in the background
|
|
r := make(chan error, 1)
|
|
go func() {
|
|
_, err := p.Send(ctx, localhost, []byte("data goes here"))
|
|
r <- err
|
|
}()
|
|
|
|
// Wait until we're blocking
|
|
p.waitOutstanding(t, ctx, 1)
|
|
|
|
// Close everything down
|
|
p.cleanupOutstanding()
|
|
|
|
// Should have got an error from the ping
|
|
err := <-r
|
|
if !errors.Is(err, net.ErrClosed) {
|
|
t.Errorf("wanted errors.Is(err, net.ErrClosed); got=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestPingerMismatch(t *testing.T) {
|
|
clock := &tstest.Clock{}
|
|
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithTimeout(ctx, 1*time.Second) // intentionally short
|
|
defer cancel()
|
|
|
|
p, closeP := mockPinger(t, clock)
|
|
defer closeP()
|
|
|
|
bodyData := []byte("data goes here")
|
|
|
|
// Start a ping in the background
|
|
r := make(chan time.Duration, 1)
|
|
go func() {
|
|
dur, err := p.Send(ctx, localhost, bodyData)
|
|
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Errorf("p.Send: %v", err)
|
|
r <- 0
|
|
} else {
|
|
r <- dur
|
|
}
|
|
}()
|
|
|
|
p.waitOutstanding(t, ctx, 1)
|
|
|
|
// "Receive" a bunch of intentionally malformed packets that should not
|
|
// result in the Send call above returning
|
|
badPackets := []struct {
|
|
name string
|
|
pkt *icmp.Message
|
|
}{
|
|
{
|
|
name: "wrong type",
|
|
pkt: &icmp.Message{
|
|
Type: ipv4.ICMPTypeDestinationUnreachable,
|
|
Code: 0,
|
|
Body: &icmp.DstUnreach{},
|
|
},
|
|
},
|
|
{
|
|
name: "wrong id",
|
|
pkt: &icmp.Message{
|
|
Type: ipv4.ICMPTypeEchoReply,
|
|
Code: 0,
|
|
Body: &icmp.Echo{
|
|
ID: 9999,
|
|
Seq: 1,
|
|
Data: bodyData,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "wrong seq",
|
|
pkt: &icmp.Message{
|
|
Type: ipv4.ICMPTypeEchoReply,
|
|
Code: 0,
|
|
Body: &icmp.Echo{
|
|
ID: 1234,
|
|
Seq: 5,
|
|
Data: bodyData,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "bad body",
|
|
pkt: &icmp.Message{
|
|
Type: ipv4.ICMPTypeEchoReply,
|
|
Code: 0,
|
|
Body: &icmp.Echo{
|
|
ID: 1234,
|
|
Seq: 1,
|
|
|
|
// Intentionally missing first byte
|
|
Data: bodyData[1:],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const fakeDuration = 100 * time.Millisecond
|
|
tm := clock.Now().Add(fakeDuration)
|
|
|
|
for _, tt := range badPackets {
|
|
fakeResponse := mustMarshal(t, tt.pkt)
|
|
p.handleResponse(fakeResponse, tm, v4Type)
|
|
}
|
|
|
|
// Also "receive" a packet that does not unmarshal as an ICMP packet
|
|
p.handleResponse([]byte("foo"), tm, v4Type)
|
|
|
|
select {
|
|
case <-r:
|
|
t.Fatal("wanted timeout")
|
|
case <-ctx.Done():
|
|
t.Logf("test correctly timed out")
|
|
}
|
|
}
|
|
|
|
// udpingPacketConn will convert potentially ICMP destination addrs to UDP
|
|
// destination addrs in WriteTo so that a test that is intending to send ICMP
|
|
// traffic will instead send UDP traffic, without the higher level Pinger being
|
|
// aware of this difference.
|
|
type udpingPacketConn struct {
|
|
net.PacketConn
|
|
// destPort will be configured by the test to be the peer expected to respond to a ping.
|
|
destPort uint16
|
|
}
|
|
|
|
func (u *udpingPacketConn) WriteTo(body []byte, dest net.Addr) (int, error) {
|
|
switch d := dest.(type) {
|
|
case *net.IPAddr:
|
|
udpAddr := &net.UDPAddr{
|
|
IP: d.IP,
|
|
Port: int(u.destPort),
|
|
Zone: d.Zone,
|
|
}
|
|
return u.PacketConn.WriteTo(body, udpAddr)
|
|
}
|
|
return 0, fmt.Errorf("unimplemented udpingPacketConn for %T", dest)
|
|
}
|
|
|
|
func mockPinger(t *testing.T, clock *tstest.Clock) (*Pinger, func()) {
|
|
p := New(context.Background(), t.Logf, nil)
|
|
p.timeNow = clock.Now
|
|
p.Verbose = true
|
|
p.id = 1234
|
|
|
|
// In tests, we use UDP so that we can test without being root; this
|
|
// doesn't matter because we mock out the ICMP reply below to be a real
|
|
// ICMP echo reply packet.
|
|
conn4, err := net.ListenPacket("udp4", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("net.ListenPacket: %v", err)
|
|
}
|
|
|
|
conn6, err := net.ListenPacket("udp6", "[::]:0")
|
|
if err != nil {
|
|
t.Fatalf("net.ListenPacket: %v", err)
|
|
}
|
|
|
|
conn4 = &udpingPacketConn{
|
|
destPort: 12345,
|
|
PacketConn: conn4,
|
|
}
|
|
conn6 = &udpingPacketConn{
|
|
PacketConn: conn6,
|
|
destPort: 12345,
|
|
}
|
|
|
|
mak.Set(&p.conns, v4Type, conn4)
|
|
mak.Set(&p.conns, v6Type, conn6)
|
|
done := func() {
|
|
if err := p.Close(); err != nil {
|
|
t.Errorf("error on close: %v", err)
|
|
}
|
|
}
|
|
return p, done
|
|
}
|
|
|
|
func mustMarshal(t *testing.T, m *icmp.Message) []byte {
|
|
t.Helper()
|
|
|
|
b, err := m.Marshal(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (p *Pinger) waitOutstanding(t *testing.T, ctx context.Context, count int) {
|
|
// This is a bit janky, but... we busy-loop to wait for the Send call
|
|
// to write to our map so we know that a response will be handled.
|
|
var haveMapEntry bool
|
|
for !haveMapEntry {
|
|
time.Sleep(10 * time.Millisecond)
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Error("no entry in ping map before timeout")
|
|
return
|
|
default:
|
|
}
|
|
|
|
p.mu.Lock()
|
|
haveMapEntry = len(p.pings) == count
|
|
p.mu.Unlock()
|
|
}
|
|
}
|