mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-12 05:37:32 +00:00
tailcfg: add Node.UnsignedPeerAPIOnly to let server mark node as peerapi-only
capver 48 Change-Id: I20b2fa81d61ef8cc8a84e5f2afeefb68832bd904 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:

committed by
Brad Fitzpatrick

parent
3367136d9e
commit
e55ae53169
@@ -1303,6 +1303,14 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
|
||||
localNetsB.AddPrefix(p)
|
||||
}
|
||||
packetFilter = netMap.PacketFilter
|
||||
|
||||
if packetFilterPermitsUnlockedNodes(netMap.Peers, packetFilter) {
|
||||
err := errors.New("server sent invalid packet filter permitting traffic to unlocked nodes; rejecting all packets for safety")
|
||||
health.SetValidUnsignedNodes(err)
|
||||
packetFilter = nil
|
||||
} else {
|
||||
health.SetValidUnsignedNodes(nil)
|
||||
}
|
||||
}
|
||||
if prefs.Valid() {
|
||||
ar := prefs.AdvertiseRoutes()
|
||||
@@ -1375,6 +1383,45 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
|
||||
}
|
||||
}
|
||||
|
||||
// packetFilterPermitsUnlockedNodes reports any peer in peers with the
|
||||
// UnsignedPeerAPIOnly bool set true has any of its allowed IPs in the packet
|
||||
// filter.
|
||||
//
|
||||
// If this reports true, the packet filter is invalid (the server is either broken
|
||||
// or malicious) and should be ignored for safety.
|
||||
func packetFilterPermitsUnlockedNodes(peers []*tailcfg.Node, packetFilter []filter.Match) bool {
|
||||
var b netipx.IPSetBuilder
|
||||
var numUnlocked int
|
||||
for _, p := range peers {
|
||||
if !p.UnsignedPeerAPIOnly {
|
||||
continue
|
||||
}
|
||||
numUnlocked++
|
||||
for _, a := range p.AllowedIPs { // not only addresses!
|
||||
b.AddPrefix(a)
|
||||
}
|
||||
}
|
||||
if numUnlocked == 0 {
|
||||
return false
|
||||
}
|
||||
s, err := b.IPSet()
|
||||
if err != nil {
|
||||
// Shouldn't happen, but if it does, fail closed.
|
||||
return true
|
||||
}
|
||||
for _, m := range packetFilter {
|
||||
for _, r := range m.Srcs {
|
||||
if !s.OverlapsPrefix(r) {
|
||||
continue
|
||||
}
|
||||
if len(m.Dsts) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setFilter(f *filter.Filter) {
|
||||
b.filterAtomic.Store(f)
|
||||
b.e.SetFilter(f)
|
||||
|
@@ -22,6 +22,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
@@ -638,3 +639,109 @@ func TestInternalAndExternalInterfaces(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketFilterPermitsUnlockedNodes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
peers []*tailcfg.Node
|
||||
filter []filter.Match
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no-unsigned",
|
||||
peers: []*tailcfg.Node{
|
||||
{ID: 1},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unsigned-good",
|
||||
peers: []*tailcfg.Node{
|
||||
{ID: 1, UnsignedPeerAPIOnly: true},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unsigned-bad",
|
||||
peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 1,
|
||||
UnsignedPeerAPIOnly: true,
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.0/32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: []filter.Match{
|
||||
{
|
||||
Srcs: []netip.Prefix{netip.MustParsePrefix("100.64.0.0/32")},
|
||||
Dsts: []filter.NetPortRange{
|
||||
{
|
||||
Net: netip.MustParsePrefix("100.99.0.0/32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unsigned-bad-src-is-superset",
|
||||
peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 1,
|
||||
UnsignedPeerAPIOnly: true,
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.0/32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: []filter.Match{
|
||||
{
|
||||
Srcs: []netip.Prefix{netip.MustParsePrefix("100.64.0.0/24")},
|
||||
Dsts: []filter.NetPortRange{
|
||||
{
|
||||
Net: netip.MustParsePrefix("100.99.0.0/32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unsigned-okay-because-no-dsts",
|
||||
peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 1,
|
||||
UnsignedPeerAPIOnly: true,
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.0/32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: []filter.Match{
|
||||
{
|
||||
Srcs: []netip.Prefix{netip.MustParsePrefix("100.64.0.0/32")},
|
||||
Caps: []filter.CapMatch{
|
||||
{
|
||||
Dst: netip.MustParsePrefix("100.99.0.0/32"),
|
||||
Cap: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := packetFilterPermitsUnlockedNodes(tt.peers, tt.filter); got != tt.want {
|
||||
t.Errorf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// TODO(tom): RPC retry/backoff was broken and has been removed. Fix?
|
||||
@@ -38,7 +39,7 @@ type tkaState struct {
|
||||
}
|
||||
|
||||
// tkaFilterNetmapLocked checks the signatures on each node key, dropping
|
||||
// nodes from the netmap who's signature does not verify.
|
||||
// nodes from the netmap whose signature does not verify.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
@@ -49,27 +50,33 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
return // TKA not enabled.
|
||||
}
|
||||
|
||||
toDelete := make(map[int]struct{}, len(nm.Peers))
|
||||
var toDelete map[int]bool // peer index => true
|
||||
for i, p := range nm.Peers {
|
||||
if p.UnsignedPeerAPIOnly {
|
||||
// Not subject to TKA.
|
||||
continue
|
||||
}
|
||||
if len(p.KeySignature) == 0 {
|
||||
b.logf("Network lock is dropping peer %v(%v) due to missing signature", p.ID, p.StableID)
|
||||
toDelete[i] = struct{}{}
|
||||
mak.Set(&toDelete, i, true)
|
||||
} else {
|
||||
if err := b.tka.authority.NodeKeyAuthorized(p.Key, p.KeySignature); err != nil {
|
||||
b.logf("Network lock is dropping peer %v(%v) due to failed signature check: %v", p.ID, p.StableID, err)
|
||||
toDelete[i] = struct{}{}
|
||||
mak.Set(&toDelete, i, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nm.Peers is ordered, so deletion must be order-preserving.
|
||||
peers := make([]*tailcfg.Node, 0, len(nm.Peers))
|
||||
for i, p := range nm.Peers {
|
||||
if _, delete := toDelete[i]; !delete {
|
||||
peers = append(peers, p)
|
||||
if len(toDelete) > 0 {
|
||||
peers := make([]*tailcfg.Node, 0, len(nm.Peers))
|
||||
for i, p := range nm.Peers {
|
||||
if !toDelete[i] {
|
||||
peers = append(peers, p)
|
||||
}
|
||||
}
|
||||
nm.Peers = peers
|
||||
}
|
||||
nm.Peers = peers
|
||||
}
|
||||
|
||||
// tkaSyncIfNeeded examines TKA info reported from the control plane,
|
||||
|
Reference in New Issue
Block a user