| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- // 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()
- }
- }
|