mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 12:39:25 +09:00
mapper: trim the Hostinfo sent to peers and to the node itself
As on Tailscale SaaS, the node's own entry keeps Hostname, RequestTags, RoutableIPs and Services; peers get Hostname, OS, Services without peerapi-dns-proxy, SSH host keys and AppConnector, and resend on those.
This commit is contained in:
1 parent
833ff6b053
commit
464dba33e1
9 files changed
+76
-209
No files matched your search
@@ -114,6 +114,8 @@ func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder {
|
||||
tailnode.MachineAuthorized = true
|
||||
tailnode.Expired = false
|
||||
|
||||
tailnode.Hostinfo = types.SelfHostinfo(tailnode.Hostinfo)
|
||||
|
||||
b.resp.Node = tailnode
|
||||
|
||||
return b
|
||||
@@ -319,6 +321,7 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
|
||||
// cap stays off the peer view, leaving CapMap empty for most
|
||||
// peers. [policyv2.PeerCapMap] encodes those conditions.
|
||||
tn.CapMap = policyv2.PeerCapMap(peer, peerCapMaps[peer.ID()])
|
||||
tn.Hostinfo = types.PeerHostinfo(tn.Hostinfo)
|
||||
|
||||
tailPeers = append(tailPeers, tn)
|
||||
}
|
||||
|
||||
@@ -1774,6 +1774,7 @@ func (b *MapResponseBuilder) buildTailPeersCapMapsReference(peers views.Slice[ty
|
||||
// cap stays off the peer view, leaving CapMap empty for most
|
||||
// peers. [policyv2.PeerCapMap] encodes those conditions.
|
||||
tn.CapMap = policyv2.PeerCapMap(peer, allCapMaps[peer.ID()])
|
||||
tn.Hostinfo = types.PeerHostinfo(tn.Hostinfo)
|
||||
|
||||
tailPeers = append(tailPeers, tn)
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ func TestIssuesRoutes(t *testing.T) {
|
||||
servertest.WithUser(user))
|
||||
|
||||
exit.WaitForPeers(t, 1, 10*time.Second)
|
||||
advertiseRoutes(t, exit, obs, tsaddr.ExitRoutes())
|
||||
advertiseRoutes(t, exit, tsaddr.ExitRoutes())
|
||||
|
||||
_, c, err := srv.State().SetApprovedRoutes(
|
||||
findNodeID(t, srv, "selfexit-node"), tsaddr.ExitRoutes())
|
||||
@@ -274,7 +274,7 @@ func TestIssuesRoutes(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
srv.App.Change(c)
|
||||
|
||||
advertiseRoutes(t, exit, obs, tsaddr.ExitRoutes())
|
||||
advertiseRoutes(t, exit, tsaddr.ExitRoutes())
|
||||
|
||||
obs.WaitForCondition(t, "peer offers exit node", 10*time.Second,
|
||||
peerOffersExit("preexit-node"))
|
||||
@@ -306,7 +306,7 @@ func TestIssuesRoutes(t *testing.T) {
|
||||
servertest.WithUser(user))
|
||||
|
||||
exit.WaitForPeers(t, 1, 10*time.Second)
|
||||
advertiseRoutes(t, exit, obs, tsaddr.ExitRoutes())
|
||||
advertiseRoutes(t, exit, tsaddr.ExitRoutes())
|
||||
|
||||
obs.WaitForCondition(t, "peer offers exit node", 10*time.Second,
|
||||
peerOffersExit("advexit-node"))
|
||||
@@ -323,7 +323,7 @@ func TestIssuesRoutes(t *testing.T) {
|
||||
|
||||
c1 := servertest.NewClient(t, srv, "histore-node1",
|
||||
servertest.WithUser(user))
|
||||
c2 := servertest.NewClient(t, srv, "histore-node2",
|
||||
servertest.NewClient(t, srv, "histore-node2",
|
||||
servertest.WithUser(user))
|
||||
|
||||
c1.WaitForPeers(t, 1, 10*time.Second)
|
||||
@@ -339,19 +339,7 @@ func TestIssuesRoutes(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_ = c1.Direct().SendUpdate(ctx)
|
||||
|
||||
c2.WaitForCondition(t, "route in peer hostinfo", 10*time.Second,
|
||||
func(nm *netmap.NetworkMap) bool {
|
||||
for _, p := range nm.Peers {
|
||||
hi := p.Hostinfo()
|
||||
if hi.Valid() && hi.Hostname() == "histore-node1" {
|
||||
return hi.RoutableIPs().Len() > 0
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
require.NoError(t, c1.Direct().SendUpdate(ctx))
|
||||
|
||||
nodeID := findNodeID(t, srv, "histore-node1")
|
||||
nv, ok := srv.State().GetNodeByID(nodeID)
|
||||
@@ -1165,9 +1153,9 @@ func findNodeID(tb testing.TB, srv *servertest.TestServer, hostname string) type
|
||||
return 0
|
||||
}
|
||||
|
||||
// advertiseRoutes pushes routes in c's Hostinfo and waits until obs sees
|
||||
// them, so the server has stored the announcement.
|
||||
func advertiseRoutes(tb testing.TB, c, obs *servertest.TestClient, routes []netip.Prefix) {
|
||||
// advertiseRoutes pushes routes in c's Hostinfo. The server stores the
|
||||
// announcement before it answers.
|
||||
func advertiseRoutes(tb testing.TB, c *servertest.TestClient, routes []netip.Prefix) {
|
||||
tb.Helper()
|
||||
|
||||
c.Direct().SetHostinfo(&tailcfg.Hostinfo{
|
||||
@@ -1179,14 +1167,7 @@ func advertiseRoutes(tb testing.TB, c, obs *servertest.TestClient, routes []neti
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_ = c.Direct().SendUpdate(ctx)
|
||||
|
||||
obs.WaitForCondition(tb, "routes in peer hostinfo", 10*time.Second,
|
||||
func(nm *netmap.NetworkMap) bool {
|
||||
p, ok := peerByHostname(nm, c.Name)
|
||||
|
||||
return ok && p.Hostinfo().RoutableIPs().Len() == len(routes)
|
||||
})
|
||||
require.NoError(tb, c.Direct().SendUpdate(ctx))
|
||||
}
|
||||
|
||||
// selfOffersExit mirrors how tailscaled computes Self.ExitNodeOption.
|
||||
|
||||
@@ -82,18 +82,6 @@ var mapStreamSkipReasons = map[string]string{
|
||||
"map-delivery/S3.6/bystander/peers": sameUserPeers,
|
||||
"map-delivery/S3.6/laptop/peers": sameUserPeers,
|
||||
|
||||
"map-delivery/S2.0/appc/self-hostinfo-keys": selfHostinfoWhole,
|
||||
"map-delivery/S2.0/bystander/self-hostinfo-keys": selfHostinfoWhole,
|
||||
"map-delivery/S2.0/sshd/self-hostinfo-keys": selfHostinfoWhole,
|
||||
|
||||
"map-delivery/S2.0/appc/peer-hostinfo-keys": peerHostinfoWhole,
|
||||
"map-delivery/S2.0/bystander/peer-hostinfo-keys": peerHostinfoWhole,
|
||||
"map-delivery/S2.0/sshd/peer-hostinfo-keys": peerHostinfoWhole,
|
||||
|
||||
"map-delivery/S2.0/appc/peer-hostinfo": peerServicesWhole,
|
||||
"map-delivery/S2.0/bystander/peer-hostinfo": peerServicesWhole,
|
||||
"map-delivery/S2.0/sshd/peer-hostinfo": peerServicesWhole,
|
||||
|
||||
"hidden-peers/S9/observer/removed": removalSentToAll,
|
||||
"hidden-peers/S10/observer/removed": removalSentToAll,
|
||||
"hidden-peers/S13/observer/removed": removalSentToAll,
|
||||
@@ -141,9 +129,6 @@ const (
|
||||
restartEndpointsKept = "RESTART_ENDPOINTS_KEPT: headscale keeps a restarted node's last endpoints at its peers until it reports new ones; SaaS sends peers the node without endpoints until then"
|
||||
reconnectAnnounced = "RECONNECT_ANNOUNCED: headscale tells peers a node is online after a stream gap of a few seconds; SaaS sends them nothing"
|
||||
routerReconnectRecompute = "ROUTER_RECONNECT_RECOMPUTE: headscale sends every node a policy response when a subnet router's stream comes back after a few seconds; SaaS sends nothing"
|
||||
selfHostinfoWhole = "SELF_HOSTINFO_WHOLE: headscale sends a node its whole reported Hostinfo; SaaS sends only Hostname, RequestTags and Services"
|
||||
peerHostinfoWhole = "PEER_HOSTINFO_WHOLE: headscale sends peers a node's whole reported Hostinfo; SaaS sends only Hostname, OS, Services, the SSH host keys and AppConnector"
|
||||
peerServicesWhole = "PEER_SERVICES_WHOLE: headscale sends peers every service a node reports; SaaS leaves out peerapi-dns-proxy"
|
||||
removalSentToAll = "REMOVAL_SENT_TO_ALL: headscale tells every connected node that a deleted node is gone; SaaS tells only the nodes that had it as a peer"
|
||||
taggedDeviceUser = "TAGGED_DEVICE_USER: SaaS shows a tagged device's user as the login that made its key; headscale shows tagged-devices"
|
||||
)
|
||||
|
||||
@@ -60,54 +60,6 @@ func TestRoutes(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("advertised_routes_in_hostinfo", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := servertest.NewServer(t)
|
||||
user := srv.CreateUser(t, "advroute-user")
|
||||
|
||||
routePrefix := netip.MustParsePrefix("192.168.1.0/24")
|
||||
|
||||
c1 := servertest.NewClient(t, srv, "advroute-node1",
|
||||
servertest.WithUser(user))
|
||||
c2 := servertest.NewClient(t, srv, "advroute-node2",
|
||||
servertest.WithUser(user))
|
||||
|
||||
c1.WaitForPeers(t, 1, 10*time.Second)
|
||||
|
||||
// Update hostinfo with advertised routes.
|
||||
c1.Direct().SetHostinfo(&tailcfg.Hostinfo{
|
||||
BackendLogID: "servertest-advroute-node1",
|
||||
Hostname: "advroute-node1",
|
||||
RoutableIPs: []netip.Prefix{routePrefix},
|
||||
})
|
||||
|
||||
// Send a non-streaming update to push the new [tailcfg.Hostinfo].
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_ = c1.Direct().SendUpdate(ctx)
|
||||
|
||||
// The observer should eventually see the advertised routes
|
||||
// in the peer's [tailcfg.Hostinfo].
|
||||
c2.WaitForCondition(t, "advertised route in hostinfo",
|
||||
15*time.Second,
|
||||
func(nm *netmap.NetworkMap) bool {
|
||||
for _, p := range nm.Peers {
|
||||
hi := p.Hostinfo()
|
||||
if hi.Valid() && hi.Hostname() == "advroute-node1" {
|
||||
for i := range hi.RoutableIPs().Len() {
|
||||
if hi.RoutableIPs().At(i) == routePrefix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("route_advertise_and_approve", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -134,22 +86,8 @@ func TestRoutes(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_ = c1.Direct().SendUpdate(ctx)
|
||||
|
||||
// Wait for the server to process the [tailcfg.Hostinfo] update
|
||||
// by waiting for observer to see the advertised route.
|
||||
c2.WaitForCondition(t, "hostinfo update propagated",
|
||||
10*time.Second,
|
||||
func(nm *netmap.NetworkMap) bool {
|
||||
for _, p := range nm.Peers {
|
||||
hi := p.Hostinfo()
|
||||
if hi.Valid() && hi.Hostname() == "fullrt-advertiser" {
|
||||
return hi.RoutableIPs().Len() > 0
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
// The server stores the announcement before it answers.
|
||||
require.NoError(t, c1.Direct().SendUpdate(ctx))
|
||||
|
||||
// Step 2: Approve the route on the server.
|
||||
nodeID := findNodeID(t, srv, "fullrt-advertiser")
|
||||
|
||||
@@ -34,7 +34,7 @@ type mapRequestDelta struct {
|
||||
hostinfoChanged bool
|
||||
|
||||
// peerHostinfoChanged reports whether a Hostinfo field that peers read
|
||||
// changed (see [peerHostinfo]). Only that forces a whole-node resend.
|
||||
// changed (see [peerHostinfoEqual]). Only that forces a whole-node resend.
|
||||
peerHostinfoChanged bool
|
||||
|
||||
// dnsMetadataChanged reports whether a Hostinfo field feeding the
|
||||
@@ -153,58 +153,18 @@ func hostinfoEqual(oldHI, newHI *tailcfg.Hostinfo) bool {
|
||||
return oldCopy.Equal(&newCopy)
|
||||
}
|
||||
|
||||
// peerHostinfoEqual reports whether a and b agree on the Hostinfo fields
|
||||
// another node's client reads from a peer: what tailscale status shows, PeerAPI
|
||||
// services, SSH known hosts, exit-node location, app-connector eligibility, and
|
||||
// the routes this server feeds into policy. Everything else is stored but never
|
||||
// fanned out, and a peer's NetInfo is never read at all.
|
||||
// peerHostinfoEqual reports whether a and b agree on what peers are sent of
|
||||
// them ([types.PeerHostinfo]) and on the routes this server feeds into policy.
|
||||
// Everything else is stored but never fanned out.
|
||||
func peerHostinfoEqual(a, b tailcfg.HostinfoView) bool {
|
||||
if !a.Valid() || !b.Valid() {
|
||||
return a.Valid() == b.Valid()
|
||||
}
|
||||
|
||||
return a.Hostname() == b.Hostname() &&
|
||||
a.OS() == b.OS() &&
|
||||
servicesEqual(a.Services(), b.Services()) &&
|
||||
views.SliceEqual(a.SSH_HostKeys(), b.SSH_HostKeys()) &&
|
||||
locationEqual(a.Location(), b.Location()) &&
|
||||
a.AppConnector() == b.AppConnector() &&
|
||||
return types.PeerHostinfo(a).Equal(types.PeerHostinfo(b)) &&
|
||||
views.SliceEqual(a.RoutableIPs(), b.RoutableIPs())
|
||||
}
|
||||
|
||||
// servicesEqual compares field by field, as [tailcfg.Service] is
|
||||
// incomparable.
|
||||
func servicesEqual(a, b views.Slice[tailcfg.Service]) bool {
|
||||
if a.Len() != b.Len() {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a.Len() {
|
||||
x, y := a.At(i), b.At(i)
|
||||
if x.Proto != y.Proto || x.Port != y.Port || x.Description != y.Description {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// locationEqual compares field by field, as [tailcfg.LocationView] has no
|
||||
// Equal.
|
||||
func locationEqual(a, b tailcfg.LocationView) bool {
|
||||
if !a.Valid() || !b.Valid() {
|
||||
return a.Valid() == b.Valid()
|
||||
}
|
||||
|
||||
return a.Country() == b.Country() &&
|
||||
a.CountryCode() == b.CountryCode() &&
|
||||
a.City() == b.City() &&
|
||||
a.CityCode() == b.CityCode() &&
|
||||
a.Latitude() == b.Latitude() &&
|
||||
a.Longitude() == b.Longitude() &&
|
||||
a.Priority() == b.Priority()
|
||||
}
|
||||
|
||||
// netInfoEqualIgnoringDERP compares two NetInfo values via
|
||||
// [tailcfg.NetInfo.BasicallyEqual] after zeroing PreferredDERP on both, so
|
||||
// that DERP-only changes do not appear here (they are tracked separately).
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -886,7 +885,13 @@ func TestPeerHostinfoEqual(t *testing.T) {
|
||||
want: false,
|
||||
},
|
||||
{name: "ssh host keys", mutate: func(hi *tailcfg.Hostinfo) { hi.SSH_HostKeys = []string{"ssh-ed25519 AAAA"} }, want: false},
|
||||
{name: "location", mutate: func(hi *tailcfg.Hostinfo) { hi.Location = &tailcfg.Location{Priority: 5} }, want: false},
|
||||
{name: "location", mutate: func(hi *tailcfg.Hostinfo) { hi.Location = &tailcfg.Location{Priority: 5} }, want: true},
|
||||
{
|
||||
name: "dns proxy service",
|
||||
mutate: func(hi *tailcfg.Hostinfo) { hi.Services = []tailcfg.Service{{Proto: tailcfg.PeerAPIDNS, Port: 1}} },
|
||||
want: true,
|
||||
},
|
||||
{name: "app connector false", mutate: func(hi *tailcfg.Hostinfo) { hi.AppConnector = opt.NewBool(false) }, want: true},
|
||||
{name: "app connector", mutate: func(hi *tailcfg.Hostinfo) { hi.AppConnector = opt.NewBool(true) }, want: false},
|
||||
{
|
||||
name: "routes",
|
||||
@@ -907,54 +912,6 @@ func TestPeerHostinfoEqual(t *testing.T) {
|
||||
require.False(t, peerHostinfoEqual(tailcfg.HostinfoView{}, base().View()))
|
||||
}
|
||||
|
||||
// TestPeerHostinfoEqualSeesEveryField fails when tailscale adds a field to
|
||||
// [tailcfg.Service] or [tailcfg.Location] that servicesEqual or locationEqual
|
||||
// does not compare.
|
||||
func TestPeerHostinfoEqualSeesEveryField(t *testing.T) {
|
||||
set := func(t *testing.T, f reflect.Value) {
|
||||
t.Helper()
|
||||
|
||||
switch {
|
||||
case f.Kind() == reflect.String:
|
||||
f.SetString("x")
|
||||
case f.CanInt():
|
||||
f.SetInt(1)
|
||||
case f.CanUint():
|
||||
f.SetUint(1)
|
||||
case f.CanFloat():
|
||||
f.SetFloat(1)
|
||||
default:
|
||||
t.Fatalf("field kind %s not handled; compare it", f.Kind())
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range reflect.VisibleFields(reflect.TypeFor[tailcfg.Service]()) {
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Run("Service."+field.Name, func(t *testing.T) {
|
||||
var svc tailcfg.Service
|
||||
set(t, reflect.ValueOf(&svc).Elem().FieldByIndex(field.Index))
|
||||
|
||||
a := &tailcfg.Hostinfo{Services: []tailcfg.Service{{}}}
|
||||
b := &tailcfg.Hostinfo{Services: []tailcfg.Service{svc}}
|
||||
assert.False(t, peerHostinfoEqual(a.View(), b.View()))
|
||||
})
|
||||
}
|
||||
|
||||
for _, field := range reflect.VisibleFields(reflect.TypeFor[tailcfg.Location]()) {
|
||||
t.Run("Location."+field.Name, func(t *testing.T) {
|
||||
var loc tailcfg.Location
|
||||
set(t, reflect.ValueOf(&loc).Elem().FieldByIndex(field.Index))
|
||||
|
||||
a := &tailcfg.Hostinfo{Location: &tailcfg.Location{}}
|
||||
b := &tailcfg.Hostinfo{Location: &loc}
|
||||
assert.False(t, peerHostinfoEqual(a.View(), b.View()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetInfoEqualIgnoringDERP(t *testing.T) {
|
||||
t.Run("both nil", func(t *testing.T) {
|
||||
require.True(t, netInfoEqualIgnoringDERP(nil, nil))
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailcfg/nodecap"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/set"
|
||||
@@ -1453,3 +1454,49 @@ func (nv NodeView) TailNode(
|
||||
|
||||
return &tNode, nil
|
||||
}
|
||||
|
||||
// SelfHostinfo is the part of a node's reported Hostinfo that its own map
|
||||
// entry carries, as on Tailscale SaaS.
|
||||
func SelfHostinfo(hi tailcfg.HostinfoView) tailcfg.HostinfoView {
|
||||
if !hi.Valid() {
|
||||
return hi
|
||||
}
|
||||
|
||||
return (&tailcfg.Hostinfo{
|
||||
Hostname: hi.Hostname(),
|
||||
RequestTags: hi.RequestTags().AsSlice(),
|
||||
RoutableIPs: hi.RoutableIPs().AsSlice(),
|
||||
Services: hi.Services().AsSlice(),
|
||||
}).View()
|
||||
}
|
||||
|
||||
// PeerHostinfo is the part of a node's reported Hostinfo that its peers are
|
||||
// sent, as on Tailscale SaaS. Clients find a peer's DNS proxy by its
|
||||
// capability version, so the service announcing it is left out. Empty
|
||||
// fields stay nil, so two results compare equal by content.
|
||||
func PeerHostinfo(hi tailcfg.HostinfoView) tailcfg.HostinfoView {
|
||||
if !hi.Valid() {
|
||||
return hi
|
||||
}
|
||||
|
||||
out := &tailcfg.Hostinfo{
|
||||
Hostname: hi.Hostname(),
|
||||
OS: hi.OS(),
|
||||
}
|
||||
|
||||
for _, s := range hi.Services().All() {
|
||||
if s.Proto != tailcfg.PeerAPIDNS {
|
||||
out.Services = append(out.Services, s)
|
||||
}
|
||||
}
|
||||
|
||||
if hi.SSH_HostKeys().Len() > 0 {
|
||||
out.SSH_HostKeys = hi.SSH_HostKeys().AsSlice()
|
||||
}
|
||||
|
||||
if hi.AppConnector().EqualBool(true) {
|
||||
out.AppConnector = opt.NewBool(true)
|
||||
}
|
||||
|
||||
return out.View()
|
||||
}
|
||||
@@ -817,14 +817,9 @@ func assertValidNetmap(t *testing.T, client TailscaleClient) {
|
||||
|
||||
assert.Truef(c, peer.Hostinfo().Valid(), "peer (%s) of %q does not have Hostinfo", peer.ComputedName(), client.Hostname())
|
||||
|
||||
// Peers get peerapi4 and peerapi6, not peerapi-dns-proxy.
|
||||
if hi := peer.Hostinfo(); hi.Valid() {
|
||||
assert.LessOrEqualf(c, 3, peer.Hostinfo().Services().Len(), "peer (%s) of %q does not have enough services, got: %v", peer.ComputedName(), client.Hostname(), peer.Hostinfo().Services())
|
||||
|
||||
// Netinfo is not always set
|
||||
// assert.Truef(c, hi.NetInfo().Valid(), "peer (%s) of %q does not have NetInfo", peer.ComputedName(), client.Hostname())
|
||||
if ni := hi.NetInfo(); ni.Valid() {
|
||||
assert.NotEqualf(c, 0, ni.PreferredDERP(), "peer (%s) has no home DERP in %q's netmap, got: %s", peer.ComputedName(), client.Hostname(), peer.Hostinfo().NetInfo().PreferredDERP())
|
||||
}
|
||||
assert.LessOrEqualf(c, 2, peer.Hostinfo().Services().Len(), "peer (%s) of %q does not have enough services, got: %v", peer.ComputedName(), client.Hostname(), peer.Hostinfo().Services())
|
||||
}
|
||||
|
||||
assert.NotEmptyf(c, peer.Endpoints(), "peer (%s) of %q does not have any endpoints", peer.ComputedName(), client.Hostname())
|
||||
|
||||
Reference in new issue
Block a user