@ -6,18 +6,20 @@ package ping
import (
import (
"context"
"context"
"errors"
"errors"
"fmt"
"net"
"net"
"testing"
"testing"
"time"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"tailscale.com/tstest"
"tailscale.com/tstest"
"tailscale.com/util/mak"
)
)
var (
var (
localhost = & net . IPAddr { IP : net . IPv4 ( 127 , 0 , 0 , 1 ) }
localhost = & net . IPAddr { IP : net . IPv4 ( 127 , 0 , 0 , 1 ) }
localhostUDP = & net . UDPAddr { IP : net . IPv4 ( 127 , 0 , 0 , 1 ) , Port : 12345 }
)
)
func TestPinger ( t * testing . T ) {
func TestPinger ( t * testing . T ) {
@ -35,7 +37,7 @@ func TestPinger(t *testing.T) {
// Start a ping in the background
// Start a ping in the background
r := make ( chan time . Duration , 1 )
r := make ( chan time . Duration , 1 )
go func ( ) {
go func ( ) {
dur , err := p . Send ( ctx , localhost UDP , bodyData )
dur , err := p . Send ( ctx , localhost , bodyData )
if err != nil {
if err != nil {
t . Errorf ( "p.Send: %v" , err )
t . Errorf ( "p.Send: %v" , err )
r <- 0
r <- 0
@ -49,7 +51,7 @@ func TestPinger(t *testing.T) {
// Fake a response from ourself
// Fake a response from ourself
fakeResponse := mustMarshal ( t , & icmp . Message {
fakeResponse := mustMarshal ( t , & icmp . Message {
Type : ipv4 . ICMPTypeEchoReply ,
Type : ipv4 . ICMPTypeEchoReply ,
Code : 0 ,
Code : ipv4 . ICMPTypeEchoReply . Protocol ( ) ,
Body : & icmp . Echo {
Body : & icmp . Echo {
ID : 1234 ,
ID : 1234 ,
Seq : 1 ,
Seq : 1 ,
@ -58,7 +60,65 @@ func TestPinger(t *testing.T) {
} )
} )
const fakeDuration = 100 * time . Millisecond
const fakeDuration = 100 * time . Millisecond
p . handleResponse ( fakeResponse , localhost , clock . Now ( ) . Add ( fakeDuration ) )
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 {
select {
case dur := <- r :
case dur := <- r :
@ -83,7 +143,7 @@ func TestPingerTimeout(t *testing.T) {
// Send a ping in the background
// Send a ping in the background
r := make ( chan error , 1 )
r := make ( chan error , 1 )
go func ( ) {
go func ( ) {
_ , err := p . Send ( ctx , localhost UDP , [ ] byte ( "data goes here" ) )
_ , err := p . Send ( ctx , localhost , [ ] byte ( "data goes here" ) )
r <- err
r <- err
} ( )
} ( )
@ -115,7 +175,7 @@ func TestPingerMismatch(t *testing.T) {
// Start a ping in the background
// Start a ping in the background
r := make ( chan time . Duration , 1 )
r := make ( chan time . Duration , 1 )
go func ( ) {
go func ( ) {
dur , err := p . Send ( ctx , localhost UDP , bodyData )
dur , err := p . Send ( ctx , localhost , bodyData )
if err != nil && ! errors . Is ( err , context . DeadlineExceeded ) {
if err != nil && ! errors . Is ( err , context . DeadlineExceeded ) {
t . Errorf ( "p.Send: %v" , err )
t . Errorf ( "p.Send: %v" , err )
r <- 0
r <- 0
@ -185,11 +245,11 @@ func TestPingerMismatch(t *testing.T) {
for _ , tt := range badPackets {
for _ , tt := range badPackets {
fakeResponse := mustMarshal ( t , tt . pkt )
fakeResponse := mustMarshal ( t , tt . pkt )
p . handleResponse ( fakeResponse , localhost, tm )
p . handleResponse ( fakeResponse , tm, v4Type )
}
}
// Also "receive" a packet that does not unmarshal as an ICMP packet
// Also "receive" a packet that does not unmarshal as an ICMP packet
p . handleResponse ( [ ] byte ( "foo" ) , localhost, tm )
p . handleResponse ( [ ] byte ( "foo" ) , tm, v4Type )
select {
select {
case <- r :
case <- r :
@ -199,23 +259,59 @@ func TestPingerMismatch(t *testing.T) {
}
}
}
}
// 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 ( ) ) {
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
// 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
// doesn't matter because we mock out the ICMP reply below to be a real
// ICMP echo reply packet.
// ICMP echo reply packet.
conn , err := net . ListenPacket ( "udp4" , "127.0.0.1:0" )
conn 4 , err := net . ListenPacket ( "udp4" , "127.0.0.1:0" )
if err != nil {
if err != nil {
t . Fatalf ( "net.ListenPacket: %v" , err )
t . Fatalf ( "net.ListenPacket: %v" , err )
}
}
p := & Pinger {
conn6 , err := net . ListenPacket ( "udp6" , "[::]:0" )
c : conn ,
if err != nil {
Logf : t . Logf ,
t . Fatalf ( "net.ListenPacket: %v" , err )
Verbose : true ,
timeNow : clock . Now ,
id : 1234 ,
pings : make ( map [ uint16 ] outstanding ) ,
}
}
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 ( ) {
done := func ( ) {
if err := p . Close ( ) ; err != nil {
if err := p . Close ( ) ; err != nil {
t . Errorf ( "error on close: %v" , err )
t . Errorf ( "error on close: %v" , err )