ipn,magicsock: deny peer capabilities to unsigned peers (#20561)

Unsigned peers aren't covered by tailnet lock, so they must never hold peer capabilities even if the packet filter grants them. This change extends the check for unsigned-peers to ensure full coverage in capabilities.

Fixes tailscale/corp#45116

Change-Id: I918af24f0b9855e55921cbdad109cc68e745e125

Signed-off-by: Mike Jensen <mikej@tailscale.com>
This commit is contained in:
Mike Jensen
2026-07-22 08:27:15 -06:00
committed by GitHub
parent 1d82c1b3d0
commit 0eb38dc2e5
6 changed files with 162 additions and 0 deletions
+5
View File
@@ -2965,6 +2965,11 @@ func nodeHasCap(filt *filter.Filter, src, dst tailcfg.NodeView, cap tailcfg.Peer
!dst.Valid() {
return false
}
if src.UnsignedPeerAPIOnly() {
// Unsigned peers aren't covered by tailnet lock and must never hold
// peer capabilities such as relay allocation/target.
return false
}
for _, srcPrefix := range src.Addresses().All() {
if !srcPrefix.IsSingleIP() {
continue
+21
View File
@@ -3821,6 +3821,9 @@ func Test_nodeHasCap(t *testing.T) {
nodeDOnlyIPv6 := nodeCOnlyIPv4.Clone()
nodeDOnlyIPv6.Addresses[0] = netip.MustParsePrefix("::2/128")
nodeCUnsigned := nodeCOnlyIPv4.Clone()
nodeCUnsigned.UnsignedPeerAPIOnly = true
tests := []struct {
name string
filt *filter.Filter
@@ -3937,6 +3940,24 @@ func Test_nodeHasCap(t *testing.T) {
cap: tailcfg.PeerCapabilityRelayTarget,
want: false,
},
{
name: "unsigned-src",
filt: filter.New([]filtertype.Match{
{
Srcs: []netip.Prefix{netip.MustParsePrefix("2.2.2.2/32")},
Caps: []filtertype.CapMatch{
{
Dst: netip.MustParsePrefix("1.1.1.1/32"),
Cap: tailcfg.PeerCapabilityRelayTarget,
},
},
},
}, nil, nil, nil, nil, nil),
src: nodeCUnsigned.View(),
dst: nodeAOnlyIPv4.View(),
cap: tailcfg.PeerCapabilityRelayTarget,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {