ipn/ipnstate,tailcfg: define IsRouter for PeerStatus and Node
Add consistent definitions and tests so that watchers of the IPN bus can keep track of routers when listening for NotifyInitialStatus and NotifyPeerChanges. Updates #17366 Updates tailscale/corp#33033 Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
@@ -360,6 +360,24 @@ func (ps *PeerStatus) HasCap(cap tailcfg.NodeCapability) bool {
|
||||
return ps.CapMap.Contains(cap)
|
||||
}
|
||||
|
||||
// IsRouter reports whether ps describes a router:
|
||||
// a node that routes addresses besides its own.
|
||||
// Examples: an exit node, a subnet router, an app connector, etc.
|
||||
// It is the analogue of [tailcfg.Node.IsRouter].
|
||||
func (ps *PeerStatus) IsRouter() bool {
|
||||
// TODO(sfllaw): Keep this aligned with dbx.Node.IsSubnetRouter.
|
||||
if ps.AllowedIPs == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range ps.AllowedIPs.All() {
|
||||
if !r.IsSingleIP() || !slices.Contains(ps.TailscaleIPs, r.Addr()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsTagged reports whether ps is tagged.
|
||||
func (ps *PeerStatus) IsTagged() bool {
|
||||
return ps.Tags != nil && ps.Tags.Len() > 0
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnstate_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
func TestPeerStatusIsRouter(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
status ipnstate.PeerStatus
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
status: ipnstate.PeerStatus{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
status: ipnstate.PeerStatus{
|
||||
TailscaleIPs: []netip.Addr{
|
||||
netip.MustParseAddr("100.64.0.1"),
|
||||
},
|
||||
AllowedIPs: new(views.SliceOf([]netip.Prefix{})),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "plain-ipv4",
|
||||
status: ipnstate.PeerStatus{
|
||||
TailscaleIPs: []netip.Addr{
|
||||
netip.MustParseAddr("100.64.0.1"),
|
||||
},
|
||||
AllowedIPs: new(views.SliceOf([]netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.1/32"),
|
||||
})),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "plain-ipv6",
|
||||
status: ipnstate.PeerStatus{
|
||||
TailscaleIPs: []netip.Addr{
|
||||
netip.MustParseAddr("fd7a:115c:a1e0::1"),
|
||||
},
|
||||
AllowedIPs: new(views.SliceOf([]netip.Prefix{
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||
})),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "plain-ipv4-ipv6",
|
||||
status: ipnstate.PeerStatus{
|
||||
TailscaleIPs: []netip.Addr{
|
||||
netip.MustParseAddr("100.64.0.1"),
|
||||
netip.MustParseAddr("fd7a:115c:a1e0::1"),
|
||||
},
|
||||
AllowedIPs: new(views.SliceOf([]netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.1/32"),
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||
})),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "exit-node-ipv4",
|
||||
status: ipnstate.PeerStatus{
|
||||
TailscaleIPs: []netip.Addr{
|
||||
netip.MustParseAddr("100.64.0.1"),
|
||||
},
|
||||
AllowedIPs: new(views.SliceOf([]netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.1/32"),
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
})),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exit-node-ipv6",
|
||||
status: ipnstate.PeerStatus{
|
||||
TailscaleIPs: []netip.Addr{
|
||||
netip.MustParseAddr("fd7a:115c:a1e0::1"),
|
||||
},
|
||||
AllowedIPs: new(views.SliceOf([]netip.Prefix{
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
})),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exit-node-ipv4-ipv6",
|
||||
status: ipnstate.PeerStatus{
|
||||
TailscaleIPs: []netip.Addr{
|
||||
netip.MustParseAddr("100.64.0.1"),
|
||||
netip.MustParseAddr("fd7a:115c:a1e0::1"),
|
||||
},
|
||||
AllowedIPs: new(views.SliceOf([]netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.1/32"),
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
})),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "subnet-router-ipv4",
|
||||
status: ipnstate.PeerStatus{
|
||||
TailscaleIPs: []netip.Addr{
|
||||
netip.MustParseAddr("100.64.0.1"),
|
||||
},
|
||||
AllowedIPs: new(views.SliceOf([]netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.1/32"),
|
||||
netip.MustParsePrefix("192.0.2.0/24"),
|
||||
})),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "subnet-router-ipv6",
|
||||
status: ipnstate.PeerStatus{
|
||||
TailscaleIPs: []netip.Addr{
|
||||
netip.MustParseAddr("fd7a:115c:a1e0::1"),
|
||||
},
|
||||
AllowedIPs: new(views.SliceOf([]netip.Prefix{
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||
netip.MustParsePrefix("2001:db8::/32"),
|
||||
})),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "subnet-router-ipv4-ipv6",
|
||||
status: ipnstate.PeerStatus{
|
||||
TailscaleIPs: []netip.Addr{
|
||||
netip.MustParseAddr("100.64.0.1"),
|
||||
netip.MustParseAddr("fd7a:115c:a1e0::1"),
|
||||
},
|
||||
AllowedIPs: new(views.SliceOf([]netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.1/32"),
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||
netip.MustParsePrefix("192.0.2.0/24"),
|
||||
netip.MustParsePrefix("2001:db8::/32"),
|
||||
})),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.status.IsRouter(); got != tc.want {
|
||||
t.Errorf("got %t, want %t", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user