mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 16:22:03 +01:00 
			
		
		
		
	This change adds a v6conn to the pinger to enable sending pings to v6 addrs. Updates #7826 Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
		
			
				
	
	
		
			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()
 | |
| 	}
 | |
| }
 |