mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-04 13:53:37 +09:00
@@ -46,6 +46,7 @@ keys remain all-access.
|
|||||||
- Improve systemd service file hardening [#3341](https://github.com/juanfont/headscale/pull/3341)
|
- Improve systemd service file hardening [#3341](https://github.com/juanfont/headscale/pull/3341)
|
||||||
- The peer map is keyed by node ID and reused for writes that cannot change peer visibility, so a routine map request no longer rebuilds it. Adds `headscale_nodestore_snapshot_builds_total` [#3417](https://github.com/juanfont/headscale/issues/3417) [#3450](https://github.com/juanfont/headscale/pull/3450)
|
- The peer map is keyed by node ID and reused for writes that cannot change peer visibility, so a routine map request no longer rebuilds it. Adds `headscale_nodestore_snapshot_builds_total` [#3417](https://github.com/juanfont/headscale/issues/3417) [#3450](https://github.com/juanfont/headscale/pull/3450)
|
||||||
- Headscale now requires Go 1.27 to build
|
- Headscale now requires Go 1.27 to build
|
||||||
|
- Fix exit node not offered to viewers whose only matching rule is a `via` grant; peer visibility now comes from the peer map alone [#3409](https://github.com/juanfont/headscale/pull/3409)
|
||||||
|
|
||||||
## 0.29.4 (unreleased)
|
## 0.29.4 (unreleased)
|
||||||
|
|
||||||
|
|||||||
@@ -249,42 +249,32 @@ func (b *MapResponseBuilder) WithPeerChanges(peers views.Slice[types.NodeView])
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildTailPeers converts [views.Slice] of [types.NodeView] to a slice of [tailcfg.Node]
|
// buildTailPeers converts [views.Slice] of [types.NodeView] to a sorted slice of
|
||||||
// with policy filtering and sorting.
|
// [tailcfg.Node]. The peers come from the NodeStore peer map, which already
|
||||||
|
// decided visibility; only each peer's routes are filtered by policy here.
|
||||||
func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) ([]*tailcfg.Node, error) {
|
func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) ([]*tailcfg.Node, error) {
|
||||||
node, ok := b.mapper.state.GetNodeByID(b.nodeID)
|
node, ok := b.mapper.state.GetNodeByID(b.nodeID)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrNodeNotFoundMapper
|
return nil, ErrNodeNotFoundMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get unreduced matchers for peer relationship determination.
|
// [State.RoutesForPeer] needs the unreduced matchers: every rule where the
|
||||||
// [State.MatchersForNode] returns unreduced matchers that include all rules where the
|
// node is source or destination, not only the [State.FilterForNode] rules
|
||||||
// node could be either source or destination. This is different from
|
// where it is destination.
|
||||||
// [State.FilterForNode] which returns reduced rules for packet filtering (only rules
|
|
||||||
// where node is destination).
|
|
||||||
matchers, err := b.mapper.state.MatchersForNode(node)
|
matchers, err := b.mapper.state.MatchersForNode(node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are filter rules present, see if there are any nodes that cannot
|
|
||||||
// access each-other at all and remove them from the peers.
|
|
||||||
var changedViews views.Slice[types.NodeView]
|
|
||||||
if len(matchers) > 0 {
|
|
||||||
changedViews = policy.ReduceNodes(node, peers, matchers)
|
|
||||||
} else {
|
|
||||||
changedViews = peers
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot the per-node policy CapMap once per peer-list build
|
// Snapshot the per-node policy CapMap once per peer-list build
|
||||||
// instead of locking the policy manager per peer. The per-call
|
// instead of locking the policy manager per peer. The per-call
|
||||||
// path used to take pm.mu N times for an N-peer response.
|
// path used to take pm.mu N times for an N-peer response.
|
||||||
allCapMaps := b.mapper.state.NodeCapMaps()
|
allCapMaps := b.mapper.state.NodeCapMaps()
|
||||||
|
|
||||||
// Build tail nodes with per-peer via-aware route function.
|
// Build tail nodes with per-peer via-aware route function.
|
||||||
tailPeers := make([]*tailcfg.Node, 0, changedViews.Len())
|
tailPeers := make([]*tailcfg.Node, 0, peers.Len())
|
||||||
|
|
||||||
for _, peer := range changedViews.All() {
|
for _, peer := range peers.All() {
|
||||||
// Pass the peer's policy CapMap as selfPolicyCaps so per-peer
|
// Pass the peer's policy CapMap as selfPolicyCaps so per-peer
|
||||||
// address-shape rules (today: disable-ipv4) apply consistently
|
// address-shape rules (today: disable-ipv4) apply consistently
|
||||||
// in the viewer's netmap. The CapMap merge into tn.CapMap is
|
// in the viewer's netmap. The CapMap merge into tn.CapMap is
|
||||||
|
|||||||
+11
-67
@@ -14,7 +14,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/policy"
|
|
||||||
"github.com/juanfont/headscale/hscontrol/state"
|
"github.com/juanfont/headscale/hscontrol/state"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/types/change"
|
"github.com/juanfont/headscale/hscontrol/types/change"
|
||||||
@@ -412,7 +411,7 @@ func (m *mapper) buildFromChange(
|
|||||||
} else {
|
} else {
|
||||||
if len(resp.PeersChanged) > 0 {
|
if len(resp.PeersChanged) > 0 {
|
||||||
peers := m.state.ListPeers(nodeID, resp.PeersChanged...)
|
peers := m.state.ListPeers(nodeID, resp.PeersChanged...)
|
||||||
builder.WithUserProfiles(m.filterVisibleNodes(nodeID, peers))
|
builder.WithUserProfiles(peers)
|
||||||
builder.WithPeerChanges(peers)
|
builder.WithPeerChanges(peers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,48 +432,9 @@ func (m *mapper) buildFromChange(
|
|||||||
return builder.Build()
|
return builder.Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
// visiblePeerIDs returns the set of peer node IDs the recipient may see under
|
// filterVisiblePeerPatches drops peer-change patches whose target is not in
|
||||||
// the current policy. It is the single visibility decision shared by the
|
// the recipient's NodeStore peer map, the same set
|
||||||
// incremental peer-change and user-profile paths, computed from the same live
|
// [MapResponseBuilder.buildTailPeers] is fed from. Without it, online/offline,
|
||||||
// per-node matchers and [policy.ReduceNodes] filter that
|
|
||||||
// [MapResponseBuilder.buildTailPeers] applies to full peer objects, so the
|
|
||||||
// paths cannot drift. The recipient's adjacency from the NodeStore
|
|
||||||
// ([NodeStore.ListPeers]) is the authority for which peers exist for it; the
|
|
||||||
// live matchers only narrow that set further, matching buildTailPeers.
|
|
||||||
//
|
|
||||||
// ok is false when the node or its matchers cannot be resolved; callers must
|
|
||||||
// then fail closed (emit nothing) rather than risk leaking forbidden peers.
|
|
||||||
func (m *mapper) visiblePeerIDs(nodeID types.NodeID) (map[tailcfg.NodeID]struct{}, bool) {
|
|
||||||
node, ok := m.state.GetNodeByID(nodeID)
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
matchers, err := m.state.MatchersForNode(node)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
peers := m.state.ListPeers(nodeID)
|
|
||||||
|
|
||||||
// No matchers means no policy restrictions, so every peer is visible —
|
|
||||||
// the same default buildTailPeers applies.
|
|
||||||
if len(matchers) > 0 {
|
|
||||||
peers = policy.ReduceNodes(node, peers, matchers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key by tailcfg.NodeID so the peer-patch path can look up by patch.NodeID
|
|
||||||
// directly, avoiding an unchecked int64->uint64 conversion.
|
|
||||||
visible := make(map[tailcfg.NodeID]struct{}, peers.Len())
|
|
||||||
for _, peer := range peers.All() {
|
|
||||||
visible[peer.ID().NodeID()] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return visible, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterVisiblePeerPatches drops peer-change patches whose target peer the
|
|
||||||
// recipient cannot see under the ACL policy. Without it, online/offline,
|
|
||||||
// endpoint, and key-expiry patches disclose the existence, presence, and
|
// endpoint, and key-expiry patches disclose the existence, presence, and
|
||||||
// addresses of peers the recipient's policy forbids it from accessing.
|
// addresses of peers the recipient's policy forbids it from accessing.
|
||||||
func (m *mapper) filterVisiblePeerPatches(
|
func (m *mapper) filterVisiblePeerPatches(
|
||||||
@@ -485,10 +445,13 @@ func (m *mapper) filterVisiblePeerPatches(
|
|||||||
return patches
|
return patches
|
||||||
}
|
}
|
||||||
|
|
||||||
visible, ok := m.visiblePeerIDs(nodeID)
|
// Key by tailcfg.NodeID so patches are looked up by patch.NodeID
|
||||||
if !ok {
|
// directly, avoiding an unchecked int64->uint64 conversion.
|
||||||
// Fail closed: if visibility cannot be resolved, send no patches.
|
peers := m.state.ListPeers(nodeID)
|
||||||
return nil
|
|
||||||
|
visible := make(map[tailcfg.NodeID]struct{}, peers.Len())
|
||||||
|
for _, peer := range peers.All() {
|
||||||
|
visible[peer.ID().NodeID()] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filterByVisible(visible, patches, func(p *tailcfg.PeerChange) tailcfg.NodeID {
|
return filterByVisible(visible, patches, func(p *tailcfg.PeerChange) tailcfg.NodeID {
|
||||||
@@ -496,25 +459,6 @@ func (m *mapper) filterVisiblePeerPatches(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterVisibleNodes restricts a peer slice to the nodes the recipient can see
|
|
||||||
// under the ACL policy. It guards UserProfiles on the incremental PeersChanged
|
|
||||||
// path, which receives an unfiltered node slice and would otherwise leak the
|
|
||||||
// identities of users whose nodes the recipient cannot access.
|
|
||||||
func (m *mapper) filterVisibleNodes(
|
|
||||||
nodeID types.NodeID,
|
|
||||||
peers views.Slice[types.NodeView],
|
|
||||||
) views.Slice[types.NodeView] {
|
|
||||||
visible, ok := m.visiblePeerIDs(nodeID)
|
|
||||||
if !ok {
|
|
||||||
// Fail closed: emit no peer user profiles rather than risk a leak.
|
|
||||||
return views.SliceOf([]types.NodeView{})
|
|
||||||
}
|
|
||||||
|
|
||||||
return views.SliceOf(filterByVisible(visible, peers.AsSlice(), func(p types.NodeView) tailcfg.NodeID {
|
|
||||||
return p.ID().NodeID()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterByVisible keeps only the items whose key resolves to a NodeID present
|
// filterByVisible keeps only the items whose key resolves to a NodeID present
|
||||||
// in the visible set, preserving input order.
|
// in the visible set, preserving input order.
|
||||||
func filterByVisible[T any](
|
func filterByVisible[T any](
|
||||||
|
|||||||
@@ -203,8 +203,8 @@ func TestNextDNSCapMapRendering(t *testing.T) {
|
|||||||
|
|
||||||
// TestBuildFromChangeFiltersPeerPatchesByVisibility proves that incremental
|
// TestBuildFromChangeFiltersPeerPatchesByVisibility proves that incremental
|
||||||
// peer-change patches (online/offline, endpoint, key-expiry) are restricted to
|
// peer-change patches (online/offline, endpoint, key-expiry) are restricted to
|
||||||
// the recipient's ACL-visible peer set, the same way buildTailPeers filters
|
// the recipient's ACL-visible peer set, the same NodeStore peer map
|
||||||
// full peer objects via policy.ReduceNodes. Without it, a node receives the
|
// buildTailPeers is fed from. Without it, a node receives the
|
||||||
// existence, presence, and addresses of peers its policy forbids accessing.
|
// existence, presence, and addresses of peers its policy forbids accessing.
|
||||||
func TestBuildFromChangeFiltersPeerPatchesByVisibility(t *testing.T) {
|
func TestBuildFromChangeFiltersPeerPatchesByVisibility(t *testing.T) {
|
||||||
tmp := t.TempDir()
|
tmp := t.TempDir()
|
||||||
@@ -361,11 +361,10 @@ func TestBuildFromChangeFiltersUserProfilesByVisibility(t *testing.T) {
|
|||||||
// full-map path under every policy shape, and a cross-user UserProfile must not
|
// full-map path under every policy shape, and a cross-user UserProfile must not
|
||||||
// leak. If a future refactor lets one path drift from another, this fails.
|
// leak. If a future refactor lets one path drift from another, this fails.
|
||||||
//
|
//
|
||||||
// It pins two behaviours the scattered per-path filters get wrong today and the
|
// It pins two behaviours of the snapshot peer map every path reads: deny-all
|
||||||
// consolidation onto the snapshot peer map must fix: deny-all (empty matchers)
|
// (empty matchers) must hide every peer on the incremental path rather than
|
||||||
// must hide every peer on the incremental path rather than fall open to "no
|
// fall open to "no matchers => all visible", and per-node policies
|
||||||
// matchers => all visible", and per-node policies (autogroup:self) must agree
|
// (autogroup:self) must agree across paths.
|
||||||
// across paths.
|
|
||||||
func TestBuildFromChangeVisibilityMatchesFullMap(t *testing.T) {
|
func TestBuildFromChangeVisibilityMatchesFullMap(t *testing.T) {
|
||||||
tmp := t.TempDir()
|
tmp := t.TempDir()
|
||||||
p4 := netip.MustParsePrefix("100.64.0.0/10")
|
p4 := netip.MustParsePrefix("100.64.0.0/10")
|
||||||
@@ -650,12 +649,11 @@ func TestGenerateDNSConfigNilHostinfoNoPanic(t *testing.T) {
|
|||||||
}, "generateDNSConfig must not panic when a node has nil Hostinfo")
|
}, "generateDNSConfig must not panic when a node has nil Hostinfo")
|
||||||
}
|
}
|
||||||
|
|
||||||
// policyShapes covers the paths that decide how the mapper filters peers: a
|
// policyShapes covers the paths that decide which peers the mapper sends: a
|
||||||
// global filter with matchers, a per-node (autogroup:self) filter, a policy
|
// global filter with matchers, a per-node (autogroup:self) filter, a policy
|
||||||
// that leaves every node with zero matchers, and no rules at all. The
|
// that leaves every node with zero matchers, and no rules at all. The
|
||||||
// zero-matcher shape is the interesting one, because
|
// zero-matcher shape is the interesting one: it must hide every peer, not
|
||||||
// [MapResponseBuilder.buildTailPeers] skips [policy.ReduceNodes] there and
|
// fall open to "no matchers => all visible".
|
||||||
// emits its input as given.
|
|
||||||
var policyShapes = []struct {
|
var policyShapes = []struct {
|
||||||
name string
|
name string
|
||||||
policy string
|
policy string
|
||||||
|
|||||||
@@ -8,30 +8,8 @@ import (
|
|||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"tailscale.com/types/views"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReduceNodes returns the list of peers authorized to be accessed from a given node.
|
|
||||||
func ReduceNodes(
|
|
||||||
node types.NodeView,
|
|
||||||
nodes views.Slice[types.NodeView],
|
|
||||||
matchers []matcher.Match,
|
|
||||||
) views.Slice[types.NodeView] {
|
|
||||||
var result []types.NodeView
|
|
||||||
|
|
||||||
for _, peer := range nodes.All() {
|
|
||||||
if peer.ID() == node.ID() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.CanAccess(matchers, peer) || peer.CanAccess(matchers, node) {
|
|
||||||
result = append(result, peer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return views.SliceOf(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReduceRoutes returns a reduced list of routes for a given node that it can access.
|
// ReduceRoutes returns a reduced list of routes for a given node that it can access.
|
||||||
func ReduceRoutes(
|
func ReduceRoutes(
|
||||||
node types.NodeView,
|
node types.NodeView,
|
||||||
|
|||||||
@@ -24,836 +24,7 @@ var p = func(prefStr string) netip.Prefix {
|
|||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReduceNodes(t *testing.T) {
|
func TestBuildPeerMapFromPolicy(t *testing.T) {
|
||||||
type args struct {
|
|
||||||
nodes types.Nodes
|
|
||||||
rules []tailcfg.FilterRule
|
|
||||||
node *types.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want types.Nodes
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "all hosts can talk to each other",
|
|
||||||
args: args{
|
|
||||||
nodes: types.Nodes{ // list of all nodes in the database
|
|
||||||
&types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
User: &types.User{Name: "mickael"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{
|
|
||||||
{
|
|
||||||
SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "*"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{ // current nodes
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: types.Nodes{
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
User: &types.User{Name: "mickael"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "One host can talk to another, but not all hosts",
|
|
||||||
args: args{
|
|
||||||
nodes: types.Nodes{ // list of all nodes in the database
|
|
||||||
&types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
User: &types.User{Name: "mickael"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
||||||
{
|
|
||||||
SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "100.64.0.2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{ // current nodes
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: types.Nodes{
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "host cannot directly talk to destination, but return path is authorized",
|
|
||||||
args: args{
|
|
||||||
nodes: types.Nodes{ // list of all nodes in the database
|
|
||||||
&types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
User: &types.User{Name: "mickael"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
||||||
{
|
|
||||||
SrcIPs: []string{"100.64.0.3"},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "100.64.0.2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{ // current nodes
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: types.Nodes{
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
User: &types.User{Name: "mickael"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rules allows all hosts to reach one destination",
|
|
||||||
args: args{
|
|
||||||
nodes: types.Nodes{ // list of all nodes in the database
|
|
||||||
&types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
User: &types.User{Name: "mickael"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
||||||
{
|
|
||||||
SrcIPs: []string{"*"},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "100.64.0.2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{ // current nodes
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: types.Nodes{
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rules allows all hosts to reach one destination, destination can reach all hosts",
|
|
||||||
args: args{
|
|
||||||
nodes: types.Nodes{ // list of all nodes in the database
|
|
||||||
&types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
User: &types.User{Name: "mickael"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
||||||
{
|
|
||||||
SrcIPs: []string{"*"},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "100.64.0.2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{ // current nodes
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: types.Nodes{
|
|
||||||
&types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
User: &types.User{Name: "mickael"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rule allows all hosts to reach all destinations",
|
|
||||||
args: args{
|
|
||||||
nodes: types.Nodes{ // list of all nodes in the database
|
|
||||||
&types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
User: &types.User{Name: "mickael"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
||||||
{
|
|
||||||
SrcIPs: []string{"*"},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "*"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{ // current nodes
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: types.Nodes{
|
|
||||||
&types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
User: &types.User{Name: "mickael"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "without rule all communications are forbidden",
|
|
||||||
args: args{
|
|
||||||
nodes: types.Nodes{ // list of all nodes in the database
|
|
||||||
&types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
User: &types.User{Name: "joe"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
User: &types.User{Name: "mickael"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
||||||
},
|
|
||||||
node: &types.Node{ // current nodes
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
User: &types.User{Name: "marc"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Investigating 699
|
|
||||||
// Found some nodes: [ts-head-8w6paa ts-unstable-lys2ib ts-head-upcrmb ts-unstable-rlwpvr] nodes=ts-head-8w6paa
|
|
||||||
// ACL rules generated ACL=[{"DstPorts":[{"Bits":null,"IP":"*","Ports":{"First":0,"Last":65535}}],"SrcIPs":["fd7a:115c:a1e0::3","100.64.0.3","fd7a:115c:a1e0::4","100.64.0.4"]}]
|
|
||||||
// ACL Cache Map={"100.64.0.3":{"*":{}},"100.64.0.4":{"*":{}},"fd7a:115c:a1e0::3":{"*":{}},"fd7a:115c:a1e0::4":{"*":{}}}
|
|
||||||
name: "issue-699-broken-star",
|
|
||||||
args: args{
|
|
||||||
nodes: types.Nodes{ //
|
|
||||||
&types.Node{
|
|
||||||
ID: 1,
|
|
||||||
Hostname: "ts-head-upcrmb",
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
IPv6: ap("fd7a:115c:a1e0::3"),
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
Hostname: "ts-unstable-rlwpvr",
|
|
||||||
IPv4: ap("100.64.0.4"),
|
|
||||||
IPv6: ap("fd7a:115c:a1e0::4"),
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 3,
|
|
||||||
Hostname: "ts-head-8w6paa",
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
IPv6: ap("fd7a:115c:a1e0::1"),
|
|
||||||
User: &types.User{Name: "user2"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 4,
|
|
||||||
Hostname: "ts-unstable-lys2ib",
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
IPv6: ap("fd7a:115c:a1e0::2"),
|
|
||||||
User: &types.User{Name: "user2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
||||||
{
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{
|
|
||||||
IP: "*",
|
|
||||||
Ports: tailcfg.PortRange{First: 0, Last: 65535},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
SrcIPs: []string{
|
|
||||||
"fd7a:115c:a1e0::3", "100.64.0.3",
|
|
||||||
"fd7a:115c:a1e0::4", "100.64.0.4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{ // current nodes
|
|
||||||
ID: 3,
|
|
||||||
Hostname: "ts-head-8w6paa",
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
IPv6: ap("fd7a:115c:a1e0::1"),
|
|
||||||
User: &types.User{Name: "user2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: types.Nodes{
|
|
||||||
&types.Node{
|
|
||||||
ID: 1,
|
|
||||||
Hostname: "ts-head-upcrmb",
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
IPv6: ap("fd7a:115c:a1e0::3"),
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
&types.Node{
|
|
||||||
ID: 2,
|
|
||||||
Hostname: "ts-unstable-rlwpvr",
|
|
||||||
IPv4: ap("100.64.0.4"),
|
|
||||||
IPv6: ap("fd7a:115c:a1e0::4"),
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "failing-edge-case-during-p3-refactor",
|
|
||||||
args: args{
|
|
||||||
nodes: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "peer1",
|
|
||||||
User: &types.User{Name: "mini"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
Hostname: "peer2",
|
|
||||||
User: &types.User{Name: "peer2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{
|
|
||||||
{
|
|
||||||
SrcIPs: []string{"100.64.0.1/32"},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "::/0", Ports: tailcfg.PortRangeAny},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{
|
|
||||||
ID: 0,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "mini",
|
|
||||||
User: &types.User{Name: "mini"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
Hostname: "peer2",
|
|
||||||
User: &types.User{Name: "peer2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "p4-host-in-netmap-user2-dest-bug",
|
|
||||||
args: args{
|
|
||||||
nodes: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "user1-2",
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 0,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "user1-1",
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.4"),
|
|
||||||
Hostname: "user2-2",
|
|
||||||
User: &types.User{Name: "user2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{
|
|
||||||
{
|
|
||||||
SrcIPs: []string{
|
|
||||||
"100.64.0.3/32",
|
|
||||||
"100.64.0.4/32",
|
|
||||||
"fd7a:115c:a1e0::3/128",
|
|
||||||
"fd7a:115c:a1e0::4/128",
|
|
||||||
},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SrcIPs: []string{
|
|
||||||
"100.64.0.1/32",
|
|
||||||
"100.64.0.2/32",
|
|
||||||
"fd7a:115c:a1e0::1/128",
|
|
||||||
"fd7a:115c:a1e0::2/128",
|
|
||||||
},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
Hostname: "user-2-1",
|
|
||||||
User: &types.User{Name: "user2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "user1-2",
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 0,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "user1-1",
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.4"),
|
|
||||||
Hostname: "user2-2",
|
|
||||||
User: &types.User{Name: "user2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "p4-host-in-netmap-user1-dest-bug",
|
|
||||||
args: args{
|
|
||||||
nodes: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "user1-2",
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
Hostname: "user-2-1",
|
|
||||||
User: &types.User{Name: "user2"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.4"),
|
|
||||||
Hostname: "user2-2",
|
|
||||||
User: &types.User{Name: "user2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{
|
|
||||||
{
|
|
||||||
SrcIPs: []string{
|
|
||||||
"100.64.0.1/32",
|
|
||||||
"100.64.0.2/32",
|
|
||||||
"fd7a:115c:a1e0::1/128",
|
|
||||||
"fd7a:115c:a1e0::2/128",
|
|
||||||
},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "100.64.0.1/32", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "100.64.0.2/32", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "fd7a:115c:a1e0::2/128", Ports: tailcfg.PortRangeAny},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SrcIPs: []string{
|
|
||||||
"100.64.0.1/32",
|
|
||||||
"100.64.0.2/32",
|
|
||||||
"fd7a:115c:a1e0::1/128",
|
|
||||||
"fd7a:115c:a1e0::2/128",
|
|
||||||
},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
|
|
||||||
{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{
|
|
||||||
ID: 0,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "user1-1",
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "user1-2",
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.3"),
|
|
||||||
Hostname: "user-2-1",
|
|
||||||
User: &types.User{Name: "user2"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 3,
|
|
||||||
IPv4: ap("100.64.0.4"),
|
|
||||||
Hostname: "user2-2",
|
|
||||||
User: &types.User{Name: "user2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "subnet-router-with-only-route",
|
|
||||||
args: args{
|
|
||||||
nodes: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "user1",
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "router",
|
|
||||||
User: &types.User{Name: "router"},
|
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
|
|
||||||
},
|
|
||||||
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{
|
|
||||||
{
|
|
||||||
SrcIPs: []string{
|
|
||||||
"100.64.0.1/32",
|
|
||||||
},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "user1",
|
|
||||||
User: &types.User{Name: "user1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "router",
|
|
||||||
User: &types.User{Name: "router"},
|
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
|
|
||||||
},
|
|
||||||
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "subnet-router-with-only-route-smaller-mask-2181",
|
|
||||||
args: args{
|
|
||||||
nodes: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "router",
|
|
||||||
User: &types.User{Name: "router"},
|
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
|
|
||||||
},
|
|
||||||
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "node",
|
|
||||||
User: &types.User{Name: "node"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{
|
|
||||||
{
|
|
||||||
SrcIPs: []string{
|
|
||||||
"100.64.0.2/32",
|
|
||||||
},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "10.99.0.2/32", Ports: tailcfg.PortRangeAny},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "router",
|
|
||||||
User: &types.User{Name: "router"},
|
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
|
|
||||||
},
|
|
||||||
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "node",
|
|
||||||
User: &types.User{Name: "node"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node-to-subnet-router-with-only-route-smaller-mask-2181",
|
|
||||||
args: args{
|
|
||||||
nodes: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "router",
|
|
||||||
User: &types.User{Name: "router"},
|
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
|
|
||||||
},
|
|
||||||
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "node",
|
|
||||||
User: &types.User{Name: "node"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{
|
|
||||||
{
|
|
||||||
SrcIPs: []string{
|
|
||||||
"100.64.0.2/32",
|
|
||||||
},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "10.99.0.2/32", Ports: tailcfg.PortRangeAny},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "node",
|
|
||||||
User: &types.User{Name: "node"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "router",
|
|
||||||
User: &types.User{Name: "router"},
|
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
|
|
||||||
},
|
|
||||||
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Subnet-to-subnet: routers must see each other when ACL
|
|
||||||
// uses only subnet CIDRs. Issue #3157.
|
|
||||||
{
|
|
||||||
name: "subnet-to-subnet-routers-see-each-other-3157",
|
|
||||||
args: args{
|
|
||||||
nodes: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "router-a",
|
|
||||||
User: &types.User{Name: "router-a"},
|
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.88.8.0/24")},
|
|
||||||
},
|
|
||||||
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.88.8.0/24")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "router-b",
|
|
||||||
User: &types.User{Name: "router-b"},
|
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.9.0/24")},
|
|
||||||
},
|
|
||||||
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.9.0/24")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: []tailcfg.FilterRule{
|
|
||||||
{
|
|
||||||
SrcIPs: []string{"10.88.8.0/24"},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{IP: "10.99.9.0/24", Ports: tailcfg.PortRangeAny},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
node: &types.Node{
|
|
||||||
ID: 1,
|
|
||||||
IPv4: ap("100.64.0.1"),
|
|
||||||
Hostname: "router-a",
|
|
||||||
User: &types.User{Name: "router-a"},
|
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.88.8.0/24")},
|
|
||||||
},
|
|
||||||
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.88.8.0/24")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []*types.Node{
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
IPv4: ap("100.64.0.2"),
|
|
||||||
Hostname: "router-b",
|
|
||||||
User: &types.User{Name: "router-b"},
|
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.9.0/24")},
|
|
||||||
},
|
|
||||||
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.99.9.0/24")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
matchers := matcher.MatchesFromFilterRules(tt.args.rules)
|
|
||||||
gotViews := ReduceNodes(
|
|
||||||
tt.args.node.View(),
|
|
||||||
tt.args.nodes.ViewSlice(),
|
|
||||||
matchers,
|
|
||||||
)
|
|
||||||
// Convert views back to nodes for comparison in tests
|
|
||||||
var got types.Nodes
|
|
||||||
for _, v := range gotViews.All() {
|
|
||||||
got = append(got, v.AsStruct())
|
|
||||||
}
|
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
|
|
||||||
t.Errorf("ReduceNodes() unexpected result (-want +got):\n%s", diff)
|
|
||||||
t.Log("Matchers: ")
|
|
||||||
|
|
||||||
for _, m := range matchers {
|
|
||||||
t.Log("\t+", m.DebugString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReduceNodesFromPolicy(t *testing.T) {
|
|
||||||
n := func(id types.NodeID, ip, hostname, username string, routess ...string) *types.Node {
|
n := func(id types.NodeID, ip, hostname, username string, routess ...string) *types.Node {
|
||||||
routes := make([]netip.Prefix, 0, len(routess))
|
routes := make([]netip.Prefix, 0, len(routess))
|
||||||
for _, route := range routess {
|
for _, route := range routess {
|
||||||
@@ -1107,19 +278,13 @@ func TestReduceNodesFromPolicy(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, matchers, tt.wantMatchers)
|
assert.Len(t, matchers, tt.wantMatchers)
|
||||||
|
|
||||||
gotViews := ReduceNodes(
|
var want []types.NodeID
|
||||||
tt.node.View(),
|
for _, n := range tt.want {
|
||||||
tt.nodes.ViewSlice(),
|
want = append(want, n.ID)
|
||||||
matchers,
|
|
||||||
)
|
|
||||||
// Convert views back to nodes for comparison in tests
|
|
||||||
var got types.Nodes
|
|
||||||
for _, v := range gotViews.All() {
|
|
||||||
got = append(got, v.AsStruct())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
|
got := pm.BuildPeerMap(tt.nodes.ViewSlice())[tt.node.ID]
|
||||||
t.Errorf("TestReduceNodesFromPolicy() unexpected result (-want +got):\n%s", diff)
|
if !assert.ElementsMatch(t, want, got) {
|
||||||
t.Log("Matchers: ")
|
t.Log("Matchers: ")
|
||||||
|
|
||||||
for _, m := range matchers {
|
for _, m := range matchers {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
// - TestRoutesCompat: validates filter rule compilation (compileFilterRulesForNode
|
// - TestRoutesCompat: validates filter rule compilation (compileFilterRulesForNode
|
||||||
// + ReduceFilterRules) against golden file captures.
|
// + ReduceFilterRules) against golden file captures.
|
||||||
//
|
//
|
||||||
// - TestRoutesCompatPeerVisibility: validates peer visibility (CanAccess /
|
// - TestRoutesCompatPeerVisibility: validates peer visibility (CanAccess)
|
||||||
// ReduceNodes) for the subnet-to-subnet scenarios (f10–f15). These tests
|
// for the subnet-to-subnet scenarios (f10–f15). These tests
|
||||||
// derive expected peer relationships from the golden file captures: if
|
// derive expected peer relationships from the golden file captures: if
|
||||||
// Tailscale SaaS delivers filter rules to a node, then the subnet routers
|
// Tailscale SaaS delivers filter rules to a node, then the subnet routers
|
||||||
// referenced in those rules must be visible as peers. This exercises the
|
// referenced in those rules must be visible as peers. This exercises the
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/juanfont/headscale/hscontrol/types/change"
|
"github.com/juanfont/headscale/hscontrol/types/change"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"tailscale.com/net/tsaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
)
|
)
|
||||||
@@ -300,6 +301,100 @@ func TestIssuesRoutes(t *testing.T) {
|
|||||||
assert.Contains(t, announced, route,
|
assert.Contains(t, announced, route,
|
||||||
"server should store the advertised route as announced")
|
"server should store the advertised route as announced")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// A grant steering autogroup:internet via a tagged exit node must
|
||||||
|
// offer the exit node to viewers whose only matching rule is the via
|
||||||
|
// grant. The via rule is compiled onto the via-tagged node, so only
|
||||||
|
// the exit node's matchers authorise the pair.
|
||||||
|
t.Run("via_exit_node_offered_to_members", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
srv := servertest.NewServer(t)
|
||||||
|
adminUser := srv.CreateUser(t, "via3408-admin")
|
||||||
|
memberUser := srv.CreateUser(t, "via3408-member")
|
||||||
|
|
||||||
|
changed, err := srv.State().SetPolicy([]byte(`{
|
||||||
|
"tagOwners": {"tag:exit": ["via3408-admin@"]},
|
||||||
|
"autoApprovers": {"exitNode": ["tag:exit"]},
|
||||||
|
"groups": {"group:admins": ["via3408-admin@"]},
|
||||||
|
"grants": [
|
||||||
|
{
|
||||||
|
"src": ["group:admins"],
|
||||||
|
"dst": ["*"],
|
||||||
|
"ip": ["*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": ["autogroup:member"],
|
||||||
|
"dst": ["autogroup:internet"],
|
||||||
|
"via": ["tag:exit"],
|
||||||
|
"ip": ["*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
changes, err := srv.State().ReloadPolicy()
|
||||||
|
require.NoError(t, err)
|
||||||
|
srv.App.Change(changes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
adminNode := servertest.NewClient(t, srv, "via3408-admin-node",
|
||||||
|
servertest.WithUser(adminUser))
|
||||||
|
memberNode := servertest.NewClient(t, srv, "via3408-member-node",
|
||||||
|
servertest.WithUser(memberUser))
|
||||||
|
exitNode := servertest.NewClient(t, srv, "via3408-exit",
|
||||||
|
servertest.WithUser(adminUser),
|
||||||
|
servertest.WithTags("tag:exit"))
|
||||||
|
|
||||||
|
exitNode.Direct().SetHostinfo(&tailcfg.Hostinfo{
|
||||||
|
BackendLogID: "servertest-via3408-exit",
|
||||||
|
Hostname: "via3408-exit",
|
||||||
|
RoutableIPs: tsaddr.ExitRoutes(),
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
require.NoError(t, exitNode.Direct().SendUpdate(ctx))
|
||||||
|
|
||||||
|
exitID := findNodeID(t, srv, "via3408-exit")
|
||||||
|
_, routeChange, err := srv.State().SetApprovedRoutes(
|
||||||
|
exitID, tsaddr.ExitRoutes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
srv.App.Change(routeChange)
|
||||||
|
|
||||||
|
seesExitNode := func(nm *netmap.NetworkMap) bool {
|
||||||
|
for _, p := range nm.Peers {
|
||||||
|
hi := p.Hostinfo()
|
||||||
|
if !hi.Valid() || hi.Hostname() != "via3408-exit" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var v4, v6 bool
|
||||||
|
|
||||||
|
for i := range p.AllowedIPs().Len() {
|
||||||
|
switch p.AllowedIPs().At(i) {
|
||||||
|
case netip.MustParsePrefix("0.0.0.0/0"):
|
||||||
|
v4 = true
|
||||||
|
case netip.MustParsePrefix("::/0"):
|
||||||
|
v6 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v4 && v6
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
memberNode.WaitForCondition(t,
|
||||||
|
"member sees the tag:exit exit node with exit routes in AllowedIPs",
|
||||||
|
15*time.Second, seesExitNode)
|
||||||
|
adminNode.WaitForCondition(t,
|
||||||
|
"admin sees the tag:exit exit node with exit routes in AllowedIPs",
|
||||||
|
15*time.Second, seesExitNode)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestIssuesIPAllocation tests IP address allocation correctness.
|
// TestIssuesIPAllocation tests IP address allocation correctness.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ var viaCompatTests = []struct {
|
|||||||
{"via-grant-v33", "single via grant + HA primary election"},
|
{"via-grant-v33", "single via grant + HA primary election"},
|
||||||
{"via-grant-v35", "via grant with unadvertised destination"},
|
{"via-grant-v35", "via grant with unadvertised destination"},
|
||||||
{"via-grant-v36", "full complex: peer connectivity + crossed subnet + crossed exit"},
|
{"via-grant-v36", "full complex: peer connectivity + crossed subnet + crossed exit"},
|
||||||
|
{"via-grant-v52", "members reach the internet only via tag:exit, admins reach everything"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestViaGrantMapCompat loads golden captures from Tailscale SaaS and
|
// TestViaGrantMapCompat loads golden captures from Tailscale SaaS and
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ func Test_NodeCanAccess(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// With a unidirectional ACL (src=A→dst=B), the dst
|
// With a unidirectional ACL (src=A→dst=B), the dst
|
||||||
// router cannot access the src router. Bidirectional
|
// router cannot access the src router. Bidirectional
|
||||||
// peer visibility comes from [policy.ReduceNodes] checking
|
// peer visibility comes from BuildPeerMap checking
|
||||||
// both A.CanAccess(B) || B.CanAccess(A).
|
// both A.CanAccess(B) || B.CanAccess(A).
|
||||||
name: "subnet-to-subnet-unidirectional-dst-cannot-access-src-3157",
|
name: "subnet-to-subnet-unidirectional-dst-cannot-access-src-3157",
|
||||||
node1: Node{
|
node1: Node{
|
||||||
|
|||||||
Reference in New Issue
Block a user