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)
|
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.
|
// IsTagged reports whether ps is tagged.
|
||||||
func (ps *PeerStatus) IsTagged() bool {
|
func (ps *PeerStatus) IsTagged() bool {
|
||||||
return ps.Tags != nil && ps.Tags.Len() > 0
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -593,6 +593,18 @@ func (n *Node) DisplayNames(forOwner bool) (name, hostIfDifferent string) {
|
|||||||
return n.ComputedName, ""
|
return n.ComputedName, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsRouter reports whether n is a router: it routes addresses besides its own.
|
||||||
|
// Examples: an exit node, a subnet router, an app connector, etc.
|
||||||
|
func (n *Node) IsRouter() bool {
|
||||||
|
// TODO(sfllaw): Keep this aligned with dbx.Node.IsSubnetRouter.
|
||||||
|
for _, r := range n.AllowedIPs {
|
||||||
|
if !slices.Contains(n.Addresses, r) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// IsTagged reports whether the node has any tags.
|
// IsTagged reports whether the node has any tags.
|
||||||
func (n *Node) IsTagged() bool {
|
func (n *Node) IsTagged() bool {
|
||||||
return len(n.Tags) > 0
|
return len(n.Tags) > 0
|
||||||
@@ -603,6 +615,10 @@ func (n *Node) SharerOrUser() UserID {
|
|||||||
return cmp.Or(n.Sharer, n.User)
|
return cmp.Or(n.Sharer, n.User)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsRouter reports whether n is a router: it routes addresses besides its own.
|
||||||
|
// Examples: an exit node, a subnet router, an app connector, etc.
|
||||||
|
func (n NodeView) IsRouter() bool { return n.ж.IsRouter() }
|
||||||
|
|
||||||
// IsTagged reports whether the node has any tags.
|
// IsTagged reports whether the node has any tags.
|
||||||
func (n NodeView) IsTagged() bool { return n.ж.IsTagged() }
|
func (n NodeView) IsTagged() bool { return n.ж.IsTagged() }
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ package tailcfg_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
. "tailscale.com/tailcfg"
|
. "tailscale.com/tailcfg"
|
||||||
"tailscale.com/tstest/deptest"
|
"tailscale.com/tstest/deptest"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
@@ -614,6 +617,251 @@ func TestNodeEqual(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nodeIsRouterCases = []struct {
|
||||||
|
name string
|
||||||
|
node Node
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
node: Node{},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too-few-allowedips",
|
||||||
|
node: Node{
|
||||||
|
Addresses: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
},
|
||||||
|
AllowedIPs: []netip.Prefix{},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicates",
|
||||||
|
node: Node{
|
||||||
|
Addresses: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
},
|
||||||
|
AllowedIPs: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plain-ipv4",
|
||||||
|
node: Node{
|
||||||
|
Addresses: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
},
|
||||||
|
AllowedIPs: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plain-ipv6",
|
||||||
|
node: Node{
|
||||||
|
Addresses: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
},
|
||||||
|
AllowedIPs: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plain-ipv4-ipv6",
|
||||||
|
node: Node{
|
||||||
|
Addresses: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
},
|
||||||
|
AllowedIPs: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exit-node-ipv4",
|
||||||
|
node: Node{
|
||||||
|
Addresses: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
},
|
||||||
|
AllowedIPs: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exit-node-ipv6",
|
||||||
|
node: Node{
|
||||||
|
Addresses: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
},
|
||||||
|
AllowedIPs: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
netip.MustParsePrefix("::/0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exit-node-ipv4-ipv6",
|
||||||
|
node: Node{
|
||||||
|
Addresses: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
},
|
||||||
|
AllowedIPs: []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",
|
||||||
|
node: Node{
|
||||||
|
Addresses: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
},
|
||||||
|
AllowedIPs: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
netip.MustParsePrefix("192.0.2.0/24"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subnet-router-ipv6",
|
||||||
|
node: Node{
|
||||||
|
Addresses: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
},
|
||||||
|
AllowedIPs: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
netip.MustParsePrefix("2001:db8::/32"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subnet-router-ipv4-ipv6",
|
||||||
|
node: Node{
|
||||||
|
Addresses: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("100.64.0.1/32"),
|
||||||
|
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||||
|
},
|
||||||
|
AllowedIPs: []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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeIsRouter(t *testing.T) {
|
||||||
|
for _, tc := range nodeIsRouterCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := tc.node.IsRouter(); got != tc.want {
|
||||||
|
t.Errorf("node: got %t, want %t", got, tc.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
nv := tc.node.View()
|
||||||
|
if got := nv.IsRouter(); got != tc.want {
|
||||||
|
t.Errorf("view: got %t, want %t", got, tc.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that [ipnstate.PeerStatus.IsRouter] matches.
|
||||||
|
ps := peerStatusFromNode(nv)
|
||||||
|
if got := ps.IsRouter(); got != tc.want {
|
||||||
|
t.Errorf("peer status: got %t, want %t", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzNodeIsRouter(f *testing.F) {
|
||||||
|
encodePrefixes := func(f *testing.F, prefixes ...netip.Prefix) string {
|
||||||
|
f.Helper()
|
||||||
|
out := make([]string, len(prefixes))
|
||||||
|
for i, p := range prefixes {
|
||||||
|
out[i] = p.String()
|
||||||
|
}
|
||||||
|
return strings.Join(out, " ")
|
||||||
|
}
|
||||||
|
decodePrefixes := func(t *testing.T, prefixes string) []netip.Prefix {
|
||||||
|
t.Helper()
|
||||||
|
var out []netip.Prefix
|
||||||
|
for _, p := range strings.Fields(prefixes) {
|
||||||
|
pfx, err := netip.ParsePrefix(p)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("skipping %q: %v", prefixes, err)
|
||||||
|
t.Skipf("%q: %v", prefixes, err)
|
||||||
|
}
|
||||||
|
out = append(out, pfx)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range nodeIsRouterCases {
|
||||||
|
addresses := encodePrefixes(f, tc.node.Addresses...)
|
||||||
|
allowedIPs := encodePrefixes(f, tc.node.AllowedIPs...)
|
||||||
|
f.Logf("addresses=%q allowedIPs=%q", addresses, allowedIPs)
|
||||||
|
f.Add(addresses, allowedIPs)
|
||||||
|
}
|
||||||
|
f.Fuzz(func(t *testing.T, addresses, allowedIPs string) {
|
||||||
|
n := Node{
|
||||||
|
Addresses: decodePrefixes(t, addresses),
|
||||||
|
AllowedIPs: decodePrefixes(t, allowedIPs),
|
||||||
|
}
|
||||||
|
ps := peerStatusFromNode(n.View())
|
||||||
|
t.Logf("%v %v", n.Addresses, n.AllowedIPs)
|
||||||
|
|
||||||
|
if len(n.Addresses) != len(ps.TailscaleIPs) ||
|
||||||
|
len(n.AllowedIPs) != ps.AllowedIPs.Len() {
|
||||||
|
t.Skip("n and ps are not equivalent")
|
||||||
|
}
|
||||||
|
|
||||||
|
gotN := n.IsRouter()
|
||||||
|
gotPS := ps.IsRouter()
|
||||||
|
if gotN != gotPS {
|
||||||
|
t.Errorf("mismatched node %t, peer status %t; addresses=%q allowedIPs=%q",
|
||||||
|
gotN, gotPS, addresses, allowedIPs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func peerStatusFromNode(n NodeView) *ipnstate.PeerStatus {
|
||||||
|
ps := &ipnstate.PeerStatus{
|
||||||
|
ID: n.StableID(),
|
||||||
|
NodeID: n.ID(),
|
||||||
|
PublicKey: n.Key(),
|
||||||
|
DNSName: n.Name(),
|
||||||
|
}
|
||||||
|
for _, p := range n.Addresses().All() {
|
||||||
|
if p.IsSingleIP() {
|
||||||
|
ps.TailscaleIPs = append(ps.TailscaleIPs, p.Addr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ps.AllowedIPs = new(n.AllowedIPs())
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
func TestNetInfoFields(t *testing.T) {
|
func TestNetInfoFields(t *testing.T) {
|
||||||
handled := []string{
|
handled := []string{
|
||||||
"MappingVariesByDestIP",
|
"MappingVariesByDestIP",
|
||||||
|
|||||||
Reference in New Issue
Block a user