@ -6,9 +6,12 @@ package ipnlocal
import (
"context"
"errors"
"fmt"
"net/netip"
"testing"
"time"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/netmap"
@ -17,7 +20,7 @@ import (
)
func TestNodeBackendReadiness ( t * testing . T ) {
nb := newNodeBackend ( t . Context ( ) , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) )
nb := newNodeBackend ( t . Context ( ) , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) , nil )
// The node backend is not ready until [nodeBackend.ready] is called,
// and [nodeBackend.Wait] should fail with [context.DeadlineExceeded].
@ -48,7 +51,7 @@ func TestNodeBackendReadiness(t *testing.T) {
}
func TestNodeBackendShutdown ( t * testing . T ) {
nb := newNodeBackend ( t . Context ( ) , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) )
nb := newNodeBackend ( t . Context ( ) , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) , nil )
shutdownCause := errors . New ( "test shutdown" )
@ -86,7 +89,7 @@ func TestNodeBackendShutdown(t *testing.T) {
}
func TestNodeBackendReadyAfterShutdown ( t * testing . T ) {
nb := newNodeBackend ( t . Context ( ) , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) )
nb := newNodeBackend ( t . Context ( ) , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) , nil )
shutdownCause := errors . New ( "test shutdown" )
nb . shutdown ( shutdownCause )
@ -98,7 +101,7 @@ func TestNodeBackendReadyAfterShutdown(t *testing.T) {
func TestNodeBackendParentContextCancellation ( t * testing . T ) {
ctx , cancelCtx := context . WithCancel ( context . Background ( ) )
nb := newNodeBackend ( ctx , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) )
nb := newNodeBackend ( ctx , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) , nil )
cancelCtx ( )
@ -115,7 +118,7 @@ func TestNodeBackendParentContextCancellation(t *testing.T) {
}
func TestNodeBackendConcurrentReadyAndShutdown ( t * testing . T ) {
nb := newNodeBackend ( t . Context ( ) , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) )
nb := newNodeBackend ( t . Context ( ) , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) , nil )
// Calling [nodeBackend.ready] and [nodeBackend.shutdown] concurrently
// should not cause issues, and [nodeBackend.Wait] should unblock,
@ -127,6 +130,17 @@ func TestNodeBackendConcurrentReadyAndShutdown(t *testing.T) {
}
func TestNodeBackendReachability ( t * testing . T ) {
addrs := [ ] netip . Prefix { netip . MustParsePrefix ( "100.64.0.1/32" ) }
defaults := func ( n tailcfg . Node ) tailcfg . Node {
if n . ID == 0 {
n . ID = 1234
}
if n . Name == "" {
n . Name = "exit-node.example.ts.net"
}
return n
}
for _ , tc := range [ ] struct {
name string
@ -139,54 +153,191 @@ func TestNodeBackendReachability(t *testing.T) {
// peer node.
cap bool
// Peer defines the peer node.
peer tailcfg . Node
// Ping sets how the peer node responds to pings:
// pingTimedOut: peer is unreachable
// pingSuccess: peer responds to pings
// pingLocalhost: peer is the same as the self node
ping mockPinger
want bool
} {
{
name : "disabled/nil" ,
cap : false ,
peer : defaults ( tailcfg . Node {
Online : nil ,
} ) ,
want : false ,
} ,
{
name : "disabled/offline" ,
cap : false ,
peer : tailcfg . Node {
peer : defaults ( tailcfg . Node {
Online : ptr . To ( false ) ,
} ,
} ) ,
want : false ,
} ,
{
name : "disabled/online" ,
cap : false ,
peer : tailcfg . Node {
peer : defaults ( tailcfg . Node {
Online : ptr . To ( true ) ,
} ,
} ) ,
want : true ,
} ,
{
name : "enabled/no_ip" ,
cap : true ,
ping : pingTimedOut ,
peer : defaults ( tailcfg . Node {
Online : ptr . To ( false ) ,
Addresses : nil ,
} ) ,
want : false ,
} ,
{
name : "enabled/offline" ,
cap : true ,
peer : tailcfg . Node {
Online : ptr . To ( false ) ,
} ,
peer : defaults ( tailcfg . Node {
Online : ptr . To ( false ) ,
Addresses : addrs ,
} ) ,
ping : pingTimedOut ,
want : false ,
} ,
{
name : "enabled/offline_but_pingable" ,
cap : true ,
peer : defaults ( tailcfg . Node {
Online : ptr . To ( false ) ,
Addresses : addrs ,
} ) ,
ping : pingSuccess ,
want : true ,
} ,
{
name : "enabled/online" ,
cap : true ,
peer : tailcfg . Node {
Online : ptr . To ( true ) ,
} ,
peer : defaults ( tailcfg . Node {
Online : ptr . To ( true ) ,
Addresses : addrs ,
} ) ,
ping : pingSuccess ,
want : true ,
} ,
{
name : "enabled/online_but_unpingable" ,
cap : true ,
peer : defaults ( tailcfg . Node {
Online : ptr . To ( true ) ,
Addresses : addrs ,
} ) ,
ping : pingTimedOut ,
want : false ,
} ,
{
name : "enabled/offline_localhost" ,
cap : true ,
peer : defaults ( tailcfg . Node {
Online : ptr . To ( false ) ,
Addresses : addrs ,
} ) ,
ping : pingLocalhost ,
want : true ,
} ,
{
name : "enabled/online_localhost" ,
cap : true ,
peer : defaults ( tailcfg . Node {
Online : ptr . To ( true ) ,
Addresses : addrs ,
} ) ,
ping : pingLocalhost ,
want : true ,
} ,
{
name : "enabled/offline_but_cancelled" ,
cap : true ,
peer : defaults ( tailcfg . Node {
Online : ptr . To ( false ) ,
Addresses : addrs ,
} ) ,
ping : pingCancelled ,
want : false ,
} ,
{
name : "enabled/online_but_cancelled" ,
cap : true ,
peer : defaults ( tailcfg . Node {
Online : ptr . To ( true ) ,
Addresses : addrs ,
} ) ,
ping : pingCancelled ,
want : false ,
} ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
nb := newNodeBackend ( t . Context ( ) , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) )
ctx := t . Context ( )
nb := newNodeBackend ( ctx , tstest . WhileTestRunningLogger ( t ) , eventbus . New ( ) , mockPinger ( tc . ping ) )
nb . netMap = & netmap . NetworkMap { }
if tc . cap {
nb . netMap . AllCaps . Make ( )
nb . netMap . AllCaps . Add ( tailcfg . NodeAttrClientSideReachability )
}
got := nb . PeerIsReachable ( t . Context ( ) , tc . peer . View ( ) )
if tc . ping == pingCancelled {
c , cancel := context . WithCancelCause ( ctx )
ctx = c
cancel ( fmt . Errorf ( "subtest: %q" , tc . name ) )
}
got := nb . PeerIsReachable ( ctx , tc . peer . View ( ) )
if got != tc . want {
t . Errorf ( "got %v, want %v" , got , tc . want )
}
} )
}
}
type mockPinger int
const (
pingTimedOut mockPinger = iota
pingSuccess
pingLocalhost
pingCancelled
)
func ( p mockPinger ) Ping ( ctx context . Context , ip netip . Addr , pingType tailcfg . PingType , size int ) ( * ipnstate . PingResult , error ) {
select {
case <- ctx . Done ( ) :
return nil , ctx . Err ( )
default :
}
res := & ipnstate . PingResult {
IP : ip . String ( ) ,
NodeIP : ip . String ( ) ,
}
switch p {
case pingTimedOut :
ctx , cancel := context . WithTimeout ( ctx , 0 )
defer cancel ( )
<- ctx . Done ( )
res . Err = ctx . Err ( ) . Error ( )
return res , nil
case pingLocalhost :
res . Err = fmt . Sprintf ( "%v is local Tailscale IP" , ip )
res . IsLocalIP = true
case pingSuccess :
res . LatencySeconds = 1
default :
panic ( fmt . Sprintf ( "unknown mockPinger %v" , p ) )
}
return res , nil
}