summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAnton Tolchanov <anton@tailscale.com>2025-09-25 14:19:15 +0100
committerAnton Tolchanov <anton@tailscale.com>2025-09-25 14:19:15 +0100
commit00088aea1e30a0833c4875f84f1e75d1ad777357 (patch)
treefac1ecd118621206330ecc6581e813ba5726b02f
parent99f0c03e9ba1b84007e6eb4ad05256ef9854da66 (diff)
downloadtailscale-knyar/sshcap.tar.xz
tailscale-knyar/sshcap.zip
WIP expiring capsknyar/sshcap
-rw-r--r--control/controlclient/map.go1
-rw-r--r--ipn/ipnlocal/c2n_test.go3
-rw-r--r--ipn/ipnlocal/expiry.go86
-rw-r--r--ipn/ipnlocal/expiry_test.go27
-rw-r--r--ipn/ipnlocal/local.go1
-rw-r--r--tailcfg/tailcfg.go33
-rw-r--r--tailcfg/tailcfg_clone.go36
-rw-r--r--tailcfg/tailcfg_view.go84
-rw-r--r--tsnet/packet_filter_test.go144
9 files changed, 342 insertions, 73 deletions
diff --git a/control/controlclient/map.go b/control/controlclient/map.go
index 22cea5aca..935e5b414 100644
--- a/control/controlclient/map.go
+++ b/control/controlclient/map.go
@@ -173,6 +173,7 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
if DevKnob.StripCaps() {
resp.Node.Capabilities = nil
resp.Node.CapMap = nil
+ resp.Node.ExtraCapMap = nil
}
// If the server is old and is still sending us Capabilities instead of
// CapMap, convert it to CapMap early so the rest of the client code can
diff --git a/ipn/ipnlocal/c2n_test.go b/ipn/ipnlocal/c2n_test.go
index 75a57dee5..9586549c3 100644
--- a/ipn/ipnlocal/c2n_test.go
+++ b/ipn/ipnlocal/c2n_test.go
@@ -268,6 +268,8 @@ func TestRedactNetmapPrivateKeys(t *testing.T) {
f(tailcfg.DisplayMessage{}, "Severity"): false,
f(tailcfg.DisplayMessage{}, "Text"): false,
f(tailcfg.DisplayMessage{}, "Title"): false,
+ f(tailcfg.ExtraCapMapValue{}, "Expiry"): false,
+ f(tailcfg.ExtraCapMapValue{}, "Value"): false,
f(tailcfg.FilterRule{}, "CapGrant"): false,
f(tailcfg.FilterRule{}, "DstPorts"): false,
f(tailcfg.FilterRule{}, "IPProto"): false,
@@ -353,6 +355,7 @@ func TestRedactNetmapPrivateKeys(t *testing.T) {
f(tailcfg.Node{}, "DiscoKey"): false,
f(tailcfg.Node{}, "Endpoints"): false,
f(tailcfg.Node{}, "ExitNodeDNSResolvers"): false,
+ f(tailcfg.Node{}, "ExtraCapMap"): false,
f(tailcfg.Node{}, "Expired"): false,
f(tailcfg.Node{}, "HomeDERP"): false,
f(tailcfg.Node{}, "Hostinfo"): false,
diff --git a/ipn/ipnlocal/expiry.go b/ipn/ipnlocal/expiry.go
index 849e28610..961c9ff4c 100644
--- a/ipn/ipnlocal/expiry.go
+++ b/ipn/ipnlocal/expiry.go
@@ -153,6 +153,39 @@ func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow ti
}
}
+func (em *expiryManager) expireNodeCaps(netmap *netmap.NetworkMap, localNow time.Time) {
+ controlNow := localNow.Add(em.clockDelta.Load())
+ if controlNow.Before(flagExpiredPeersEpoch) {
+ em.logf("netmap: expireNodeCaps: [unexpected] delta-adjusted current time is before hardcoded epoch; skipping")
+ return
+ }
+ expireCaps := func(n *tailcfg.Node) (changed bool) {
+ if len(n.ExtraCapMap) == 0 {
+ return false
+ }
+ for capName, cap := range n.ExtraCapMap {
+ if !cap.Expiry.IsZero() && cap.Expiry.Before(controlNow) {
+ delete(n.ExtraCapMap, capName)
+ changed = true
+ }
+ }
+ return changed
+ }
+ if netmap.SelfNode.Valid() {
+ // TODO(anton): don't clone if there's nothing to change.
+ self := netmap.SelfNode.AsStruct()
+ if expireCaps(self) {
+ netmap.SelfNode = self.View()
+ }
+ }
+ for i, peer := range netmap.Peers {
+ p := peer.AsStruct()
+ if expireCaps(p) {
+ netmap.Peers[i] = p.View()
+ }
+ }
+}
+
// nextPeerExpiry returns the time that the next node in the netmap expires
// (including the self node), based on their KeyExpiry. It skips nodes that are
// already marked as Expired. If there are no nodes expiring in the future,
@@ -174,43 +207,42 @@ func (em *expiryManager) nextPeerExpiry(nm *netmap.NetworkMap, localNow time.Tim
}
var nextExpiry time.Time // zero if none
- for _, peer := range nm.Peers {
- if peer.KeyExpiry().IsZero() {
- continue // tagged node
- } else if peer.Expired() {
- // Peer already expired; Expired is set by the
- // flagExpiredPeers function, above.
- continue
- } else if peer.KeyExpiry().Before(controlNow) {
- // This peer already expired, and peer.Expired
- // isn't set for some reason. Skip this node.
- continue
+ update := func(expiry time.Time) {
+ if expiry.IsZero() {
+ return
}
-
// nextExpiry being zero is a sentinel that we haven't yet set
// an expiry; otherwise, only update if this node's expiry is
// sooner than the currently-stored one (since we want the
// soonest-occurring expiry time).
- if nextExpiry.IsZero() || peer.KeyExpiry().Before(nextExpiry) {
- nextExpiry = peer.KeyExpiry()
+ if nextExpiry.IsZero() || expiry.Before(nextExpiry) {
+ nextExpiry = expiry
+ }
+ }
+ handleNode := func(n tailcfg.NodeView) {
+ if n.KeyExpiry().IsZero() {
+ // tagged node
+ } else if n.Expired() {
+ // Already expired; Expired is set by the
+ // flagExpiredPeers function, above.
+ } else if n.KeyExpiry().Before(controlNow) {
+ // Already expired, but Expired
+ // isn't set for some reason. Skip it.
+ } else {
+ update(n.KeyExpiry())
+ }
+ // Also handle expiring caps.
+ for _, c := range n.ExtraCapMap().All() {
+ update(c.Expiry())
}
}
+ for _, peer := range nm.Peers {
+ handleNode(peer)
+ }
// Ensure that we also fire this timer if our own node key expires.
if nm.SelfNode.Valid() {
- selfExpiry := nm.SelfNode.KeyExpiry()
-
- if selfExpiry.IsZero() {
- // No expiry for self node
- } else if selfExpiry.Before(controlNow) {
- // Self node already expired; we don't want to return a
- // time in the past, so skip this.
- } else if nextExpiry.IsZero() || selfExpiry.Before(nextExpiry) {
- // Self node expires after now, but before the soonest
- // peer in the netmap; update our next expiry to this
- // time.
- nextExpiry = selfExpiry
- }
+ handleNode(nm.SelfNode)
}
// As an additional defense in depth, never return a time that is
diff --git a/ipn/ipnlocal/expiry_test.go b/ipn/ipnlocal/expiry_test.go
index 2c646ca72..d65788a28 100644
--- a/ipn/ipnlocal/expiry_test.go
+++ b/ipn/ipnlocal/expiry_test.go
@@ -15,6 +15,7 @@ import (
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/util/eventbus/eventbustest"
+ "tailscale.com/util/mak"
)
func TestFlagExpiredPeers(t *testing.T) {
@@ -238,6 +239,32 @@ func TestNextPeerExpiry(t *testing.T) {
},
want: noExpiry,
},
+ {
+ name: "self_attribute",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInMoreFuture, func(n *tailcfg.Node) {
+ mak.Set(&n.ExtraCapMap, "foo", tailcfg.ExtraCapMapValue{Expiry: timeInMoreFuture})
+ }),
+ }),
+ SelfNode: n(2, "self", noExpiry, func(n *tailcfg.Node) {
+ mak.Set(&n.ExtraCapMap, "foo", tailcfg.ExtraCapMapValue{Expiry: timeInFuture})
+ }).View(),
+ },
+ want: timeInFuture,
+ },
+ {
+ name: "peer_attribute",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInMoreFuture, func(n *tailcfg.Node) {
+ mak.Set(&n.ExtraCapMap, "foo", tailcfg.ExtraCapMapValue{Expiry: timeInFuture})
+ }),
+ }),
+ SelfNode: n(2, "self", timeInMoreFuture).View(),
+ },
+ want: timeInFuture,
+ },
}
for _, tt := range tests {
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index ce42ae75a..9f154912f 100644
--- a/ipn/ipnlocal/local.go
+++ b/ipn/ipnlocal/local.go
@@ -1575,6 +1575,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
if st.NetMap != nil {
now := b.clock.Now()
b.em.flagExpiredPeers(st.NetMap, now)
+ b.em.expireNodeCaps(st.NetMap, now)
// Always stop the existing netmap timer if we have a netmap;
// it's possible that we have no nodes expiring, so we should
diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go
index 8f16f5e27..d8e030d21 100644
--- a/tailcfg/tailcfg.go
+++ b/tailcfg/tailcfg.go
@@ -5,7 +5,7 @@
// the node and the coordination server.
package tailcfg
-//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService --clonefunc
+//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService,ExtraCapMapValue --clonefunc
import (
"bytes"
@@ -442,6 +442,8 @@ type Node struct {
// for a particular task vs other peers that could also be chosen.
CapMap NodeCapMap `json:",omitempty"`
+ ExtraCapMap ExtraCapMap `json:",omitempty"`
+
// UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA
// restrictions. However, in exchange for that privilege, it does not get
// network access. It can only access this node's peerapi, which may not let
@@ -522,7 +524,16 @@ func (v NodeView) HasCap(cap NodeCapability) bool {
// HasCap reports whether the node has the given capability.
// It is safe to call on a nil Node.
func (v *Node) HasCap(cap NodeCapability) bool {
- return v != nil && v.CapMap.Contains(cap)
+ if v == nil {
+ return false
+ }
+ if v.CapMap.Contains(cap) {
+ return true
+ }
+ if v.ExtraCapMap.Contains(cap) {
+ return true
+ }
+ return false
}
// DisplayName returns the user-facing name for a node which should
@@ -1548,6 +1559,23 @@ const (
PeerCapabilityTsIDP PeerCapability = "tailscale.com/cap/tsidp"
)
+type ExtraCapMap map[NodeCapability]ExtraCapMapValue
+
+type ExtraCapMapValue struct {
+ Expiry time.Time
+ Value []RawMessage
+}
+
+func (c ExtraCapMap) Contains(cap NodeCapability) bool {
+ _, ok := c[cap]
+ return ok
+}
+func (c ExtraCapMap) Equal(c2 ExtraCapMap) bool {
+ return maps.EqualFunc(c, c2, func(v1, v2 ExtraCapMapValue) bool {
+ return v1.Expiry.Equal(v2.Expiry) && slices.Equal(v1.Value, v2.Value)
+ })
+}
+
// NodeCapMap is a map of capabilities to their optional values. It is valid for
// a capability to have no values (nil slice); such capabilities can be tested
// for by using the [NodeCapMap.Contains] method.
@@ -2348,6 +2376,7 @@ func (n *Node) Equal(n2 *Node) bool {
n.MachineAuthorized == n2.MachineAuthorized &&
slices.Equal(n.Capabilities, n2.Capabilities) &&
n.CapMap.Equal(n2.CapMap) &&
+ n.ExtraCapMap.Equal(n2.ExtraCapMap) &&
n.ComputedName == n2.ComputedName &&
n.computedHostIfDifferent == n2.computedHostIfDifferent &&
n.ComputedNameWithHost == n2.ComputedNameWithHost &&
diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go
index c073c9e09..a926cead5 100644
--- a/tailcfg/tailcfg_clone.go
+++ b/tailcfg/tailcfg_clone.go
@@ -65,6 +65,12 @@ func (src *Node) Clone() *Node {
dst.CapMap[k] = append([]RawMessage{}, src.CapMap[k]...)
}
}
+ if dst.ExtraCapMap != nil {
+ dst.ExtraCapMap = map[NodeCapability]ExtraCapMapValue{}
+ for k, v := range src.ExtraCapMap {
+ dst.ExtraCapMap[k] = *(v.Clone())
+ }
+ }
if dst.SelfNodeV4MasqAddrForThisPeer != nil {
dst.SelfNodeV4MasqAddrForThisPeer = ptr.To(*src.SelfNodeV4MasqAddrForThisPeer)
}
@@ -111,6 +117,7 @@ var _NodeCloneNeedsRegeneration = Node(struct {
MachineAuthorized bool
Capabilities []NodeCapability
CapMap NodeCapMap
+ ExtraCapMap ExtraCapMap
UnsignedPeerAPIOnly bool
ComputedName string
computedHostIfDifferent string
@@ -652,9 +659,27 @@ var _VIPServiceCloneNeedsRegeneration = VIPService(struct {
Active bool
}{})
+// Clone makes a deep copy of ExtraCapMapValue.
+// The result aliases no memory with the original.
+func (src *ExtraCapMapValue) Clone() *ExtraCapMapValue {
+ if src == nil {
+ return nil
+ }
+ dst := new(ExtraCapMapValue)
+ *dst = *src
+ dst.Value = append(src.Value[:0:0], src.Value...)
+ return dst
+}
+
+// A compilation failure here means this code must be regenerated, with the command at the top of this file.
+var _ExtraCapMapValueCloneNeedsRegeneration = ExtraCapMapValue(struct {
+ Expiry time.Time
+ Value []RawMessage
+}{})
+
// Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
-// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService.
+// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService,ExtraCapMapValue.
func Clone(dst, src any) bool {
switch src := src.(type) {
case *User:
@@ -837,6 +862,15 @@ func Clone(dst, src any) bool {
*dst = src.Clone()
return true
}
+ case *ExtraCapMapValue:
+ switch dst := dst.(type) {
+ case *ExtraCapMapValue:
+ *dst = *src.Clone()
+ return true
+ case **ExtraCapMapValue:
+ *dst = src.Clone()
+ return true
+ }
}
return false
}
diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go
index 46c6da7dc..044b0319a 100644
--- a/tailcfg/tailcfg_view.go
+++ b/tailcfg/tailcfg_view.go
@@ -21,7 +21,7 @@ import (
"tailscale.com/types/views"
)
-//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService
+//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService,ExtraCapMapValue
// View returns a read-only view of User.
func (p *User) View() UserView {
@@ -302,6 +302,11 @@ func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.Slic
func (v NodeView) CapMap() views.MapSlice[NodeCapability, RawMessage] {
return views.MapSliceOf(v.ж.CapMap)
}
+func (v NodeView) ExtraCapMap() views.MapFn[NodeCapability, ExtraCapMapValue, ExtraCapMapValueView] {
+ return views.MapFnOf(v.ж.ExtraCapMap, func(t ExtraCapMapValue) ExtraCapMapValueView {
+ return t.View()
+ })
+}
// UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA
// restrictions. However, in exchange for that privilege, it does not get
@@ -404,6 +409,7 @@ var _NodeViewNeedsRegeneration = Node(struct {
MachineAuthorized bool
Capabilities []NodeCapability
CapMap NodeCapMap
+ ExtraCapMap ExtraCapMap
UnsignedPeerAPIOnly bool
ComputedName string
computedHostIfDifferent string
@@ -2606,3 +2612,79 @@ var _VIPServiceViewNeedsRegeneration = VIPService(struct {
Ports []ProtoPortRange
Active bool
}{})
+
+// View returns a read-only view of ExtraCapMapValue.
+func (p *ExtraCapMapValue) View() ExtraCapMapValueView {
+ return ExtraCapMapValueView{ж: p}
+}
+
+// ExtraCapMapValueView provides a read-only view over ExtraCapMapValue.
+//
+// Its methods should only be called if `Valid()` returns true.
+type ExtraCapMapValueView struct {
+ // ж is the underlying mutable value, named with a hard-to-type
+ // character that looks pointy like a pointer.
+ // It is named distinctively to make you think of how dangerous it is to escape
+ // to callers. You must not let callers be able to mutate it.
+ ж *ExtraCapMapValue
+}
+
+// Valid reports whether v's underlying value is non-nil.
+func (v ExtraCapMapValueView) Valid() bool { return v.ж != nil }
+
+// AsStruct returns a clone of the underlying value which aliases no memory with
+// the original.
+func (v ExtraCapMapValueView) AsStruct() *ExtraCapMapValue {
+ if v.ж == nil {
+ return nil
+ }
+ return v.ж.Clone()
+}
+
+// MarshalJSON implements [jsonv1.Marshaler].
+func (v ExtraCapMapValueView) MarshalJSON() ([]byte, error) {
+ return jsonv1.Marshal(v.ж)
+}
+
+// MarshalJSONTo implements [jsonv2.MarshalerTo].
+func (v ExtraCapMapValueView) MarshalJSONTo(enc *jsontext.Encoder) error {
+ return jsonv2.MarshalEncode(enc, v.ж)
+}
+
+// UnmarshalJSON implements [jsonv1.Unmarshaler].
+func (v *ExtraCapMapValueView) UnmarshalJSON(b []byte) error {
+ if v.ж != nil {
+ return errors.New("already initialized")
+ }
+ if len(b) == 0 {
+ return nil
+ }
+ var x ExtraCapMapValue
+ if err := jsonv1.Unmarshal(b, &x); err != nil {
+ return err
+ }
+ v.ж = &x
+ return nil
+}
+
+// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
+func (v *ExtraCapMapValueView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
+ if v.ж != nil {
+ return errors.New("already initialized")
+ }
+ var x ExtraCapMapValue
+ if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
+ return err
+ }
+ v.ж = &x
+ return nil
+}
+
+func (v ExtraCapMapValueView) Expiry() time.Time { return v.ж.Expiry }
+func (v ExtraCapMapValueView) Value() views.Slice[RawMessage] { return views.SliceOf(v.ж.Value) }
+
+// A compilation failure here means this code must be regenerated, with the command at the top of this file.
+var _ExtraCapMapValueViewNeedsRegeneration = ExtraCapMapValue(struct {
+ Expiry time.Time
+ Value []RawMessage
+}{})
diff --git a/tsnet/packet_filter_test.go b/tsnet/packet_filter_test.go
index 462234222..f227fd14c 100644
--- a/tsnet/packet_filter_test.go
+++ b/tsnet/packet_filter_test.go
@@ -8,6 +8,7 @@ import (
"fmt"
"net/netip"
"testing"
+ "testing/synctest"
"time"
"tailscale.com/ipn"
@@ -180,56 +181,83 @@ func TestPacketFilterFromNetmap(t *testing.T) {
{src: "2.2.2.2", dst: "1.1.1.2", port: 22, want: filter.Drop}, // different dst
},
},
+ {
+ name: "capmap_based_peers_with_expiry",
+ mapResponse: &tailcfg.MapResponse{
+ Node: &tailcfg.Node{
+ Addresses: []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")},
+ },
+ Peers: []*tailcfg.Node{{
+ ID: 2,
+ Name: "foo",
+ Key: key,
+ Addresses: []netip.Prefix{netip.MustParsePrefix("2.2.2.2/32")},
+ ExtraCapMap: tailcfg.ExtraCapMap{"X": {
+ Expiry: time.Now().Add(1 * time.Minute),
+ }},
+ }},
+ PacketFilter: []tailcfg.FilterRule{{
+ SrcIPs: []string{"cap:X"},
+ DstPorts: []tailcfg.NetPortRange{{
+ IP: "1.1.1.1/32",
+ Ports: tailcfg.PortRange{
+ First: 22,
+ Last: 22,
+ },
+ }},
+ IPProto: []int{int(ipproto.TCP)},
+ }},
+ },
+ waitTest: func(nm *netmap.NetworkMap) bool {
+ return len(nm.Peers) > 0
+ },
+ checks: []check{
+ {src: "2.2.2.2", dst: "1.1.1.1", port: 22, want: filter.Accept},
+ {src: "2.2.2.2", dst: "1.1.1.1", port: 23, want: filter.Drop}, // different port
+ {src: "3.3.3.3", dst: "1.1.1.1", port: 22, want: filter.Drop}, // different src
+ {src: "2.2.2.2", dst: "1.1.1.2", port: 22, want: filter.Drop}, // different dst
+ },
+ incrementalMapResponse: &tailcfg.MapResponse{
+ PeersChanged: []*tailcfg.Node{{
+ ID: 2,
+ Name: "foo",
+ Key: key,
+ Addresses: []netip.Prefix{netip.MustParsePrefix("2.2.2.3/32")},
+ }},
+ },
+ incrementalWaitTest: func(nm *netmap.NetworkMap) bool {
+ time.Sleep(time.Minute)
+ // Wait until the peer's extra cap expires.
+ if len(nm.Peers) == 0 {
+ return false
+ }
+ peer := nm.Peers[0]
+ if peer.Addresses().AsSlice()[0].Addr() != netip.MustParseAddr("2.2.2.3") {
+ return false
+ }
+ return true
+ },
+ },
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
- ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
- defer cancel()
-
- controlURL, c := startControl(t)
- s, _, pubKey := startServer(t, ctx, controlURL, "node")
-
- if test.waitTest(s.lb.NetMap()) {
- t.Fatal("waitTest already passes before sending initial netmap: this will be flaky")
- }
-
- if !c.AddRawMapResponse(pubKey, test.mapResponse) {
- t.Fatalf("could not send map response to %s", pubKey)
- }
-
- if err := waitFor(t, ctx, s, test.waitTest); err != nil {
- t.Fatalf("waitFor: %s", err)
- }
-
- pf := s.lb.GetFilterForTest()
-
- for _, check := range test.checks {
- got := pf.Check(netip.MustParseAddr(check.src), netip.MustParseAddr(check.dst), check.port, ipproto.TCP)
-
- want := check.want
- if test.incrementalMapResponse != nil {
- want = filter.Drop
- }
- if got != want {
- t.Errorf("check %s -> %s:%d, got: %s, want: %s", check.src, check.dst, check.port, got, want)
- }
- }
+ synctest.Test(t, func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
+ defer cancel()
- if test.incrementalMapResponse != nil {
- if test.incrementalWaitTest == nil {
- t.Fatal("incrementalWaitTest must be set if incrementalMapResponse is set")
- }
+ controlURL, c := startControl(t)
+ s, _, pubKey := startServer(t, ctx, controlURL, "node")
- if test.incrementalWaitTest(s.lb.NetMap()) {
- t.Fatal("incrementalWaitTest already passes before sending incremental netmap: this will be flaky")
+ if test.waitTest(s.lb.NetMap()) {
+ t.Fatal("waitTest already passes before sending initial netmap: this will be flaky")
}
- if !c.AddRawMapResponse(pubKey, test.incrementalMapResponse) {
+ if !c.AddRawMapResponse(pubKey, test.mapResponse) {
t.Fatalf("could not send map response to %s", pubKey)
}
- if err := waitFor(t, ctx, s, test.incrementalWaitTest); err != nil {
+ if err := waitFor(t, ctx, s, test.waitTest); err != nil {
t.Fatalf("waitFor: %s", err)
}
@@ -237,12 +265,44 @@ func TestPacketFilterFromNetmap(t *testing.T) {
for _, check := range test.checks {
got := pf.Check(netip.MustParseAddr(check.src), netip.MustParseAddr(check.dst), check.port, ipproto.TCP)
- if got != check.want {
- t.Errorf("check %s -> %s:%d, got: %s, want: %s", check.src, check.dst, check.port, got, check.want)
+
+ want := check.want
+ if test.incrementalMapResponse != nil {
+ want = filter.Drop
+ }
+ if got != want {
+ t.Errorf("check %s -> %s:%d, got: %s, want: %s", check.src, check.dst, check.port, got, want)
+ }
+ }
+
+ if test.incrementalMapResponse != nil {
+ if test.incrementalWaitTest == nil {
+ t.Fatal("incrementalWaitTest must be set if incrementalMapResponse is set")
+ }
+
+ if test.incrementalWaitTest(s.lb.NetMap()) {
+ t.Fatal("incrementalWaitTest already passes before sending incremental netmap: this will be flaky")
+ }
+
+ if !c.AddRawMapResponse(pubKey, test.incrementalMapResponse) {
+ t.Fatalf("could not send map response to %s", pubKey)
+ }
+
+ if err := waitFor(t, ctx, s, test.incrementalWaitTest); err != nil {
+ t.Fatalf("waitFor: %s", err)
+ }
+
+ pf := s.lb.GetFilterForTest()
+
+ for _, check := range test.checks {
+ got := pf.Check(netip.MustParseAddr(check.src), netip.MustParseAddr(check.dst), check.port, ipproto.TCP)
+ if got != check.want {
+ t.Errorf("check %s -> %s:%d, got: %s, want: %s", check.src, check.dst, check.port, got, check.want)
+ }
}
}
- }
+ })
})
}
}