feat(machine): add ACLFilter if ACL's are enabled.

This commit change the default behaviour and remove the notion of namespaces between the hosts. It allows all namespaces to be only filtered by the ACLs. This behavior is closer to tailsnet.
This commit is contained in:
Adrien Raffin 2022-02-03 19:53:11 +01:00 committed by Adrien Raffin-Caboisse
parent 9b7d657cbe
commit e482dfeed4
No known key found for this signature in database
GPG Key ID: 7FB60532DEBEAD6A
3 changed files with 183 additions and 43 deletions

View File

@ -119,14 +119,21 @@ func (machine Machine) isExpired() bool {
return time.Now().UTC().After(*machine.Expiry) return time.Now().UTC().After(*machine.Expiry)
} }
// Our Pineapple fork of Headscale ignores namespaces when dealing with peers func containsAddresses(inputs []string, addrs MachineAddresses) bool {
// and instead passes ALL peers across all namespaces to each client. Access between clients for _, addr := range addrs.ToStringSlice() {
// is then enforced with ACL policies. if containsString(inputs, addr) {
func (h *Headscale) getAllPeers(machine *Machine) (Machines, error) { return true
}
}
return false
}
// getFilteredByACLPeerss should return the list of peers authorized to be accessed from machine.
func (h *Headscale) getFilteredByACLPeers(machine *Machine) (Machines, error) {
log.Trace(). log.Trace().
Caller(). Caller().
Str("machine", machine.Name). Str("machine", machine.Name).
Msg("Finding all peers") Msg("Finding peers filtered by ACLs")
machines := Machines{} machines := Machines{}
if err := h.db.Preload("Namespace").Where("machine_key <> ? AND registered", if err := h.db.Preload("Namespace").Where("machine_key <> ? AND registered",
@ -135,15 +142,50 @@ func (h *Headscale) getAllPeers(machine *Machine) (Machines, error) {
return Machines{}, err return Machines{}, err
} }
mMachines := make(map[uint64]Machine)
sort.Slice(machines, func(i, j int) bool { return machines[i].ID < machines[j].ID }) // Aclfilter peers here. We are itering through machines in all namespaces and search through the computed aclRules
// for match between rule SrcIPs and DstPorts. If the rule is a match we allow the machine to be viewable.
// FIXME: On official control plane if a rule allow user A to talk to user B but NO rule allows user B to talk to
// user A. The behaviour is the following
//
// On official tailscale control plane:
// on first `tailscale status`` on node A we can see node B. The `tailscale status` command on node B doesn't show node A
// We can successfully establish a communication from A to B. When it's done, if we run the `tailscale status` command
// on node B again we can now see node A. It's not possible to establish a communication from node B to node A.
// On this implementation of the feature
// on any `tailscale status` command on node A we can see node B. The `tailscale status` command on node B DOES show A.
//
// I couldn't find a way to not clutter the output of `tailscale status` with all nodes that we could be talking to.
// In order to do this we would need to be able to identify that node A want to talk to node B but that Node B doesn't know
// how to talk to node A and then add the peering resource.
for _, m := range machines {
for _, rule := range h.aclRules {
var dst []string
for _, d := range rule.DstPorts {
dst = append(dst, d.IP)
}
if (containsAddresses(rule.SrcIPs, machine.IPAddresses) && (containsAddresses(dst, m.IPAddresses) || containsString(dst, "*"))) ||
(containsAddresses(rule.SrcIPs, m.IPAddresses) && containsAddresses(dst, machine.IPAddresses)) {
mMachines[m.ID] = m
}
}
}
var authorizedMachines Machines
for _, m := range mMachines {
authorizedMachines = append(authorizedMachines, m)
}
sort.Slice(authorizedMachines, func(i, j int) bool { return authorizedMachines[i].ID < authorizedMachines[j].ID })
log.Trace(). log.Trace().
Caller(). Caller().
Str("machine", machine.Name). Str("machine", machine.Name).
Msgf("Found all machines: %s", machines.String()) Msgf("Found some machines: %s", machines.String())
return machines, nil return authorizedMachines, nil
} }
func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) { func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
@ -233,47 +275,52 @@ func (h *Headscale) getSharedTo(machine *Machine) (Machines, error) {
} }
func (h *Headscale) getPeers(machine *Machine) (Machines, error) { func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
// direct, err := h.getDirectPeers(machine) var peers Machines
// if err != nil { var err error
// log.Error(). // If ACLs rules are defined, filter visible host list with the ACLs
// Caller(). // else use the classic namespace scope
// Err(err). if h.aclPolicy != nil {
// Msg("Cannot fetch peers") peers, err = h.getFilteredByACLPeers(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
// return Machines{}, err return Machines{}, err
// } }
} else {
direct, err := h.getDirectPeers(machine)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
// shared, err := h.getShared(machine) return Machines{}, err
// if err != nil { }
// log.Error().
// Caller().
// Err(err).
// Msg("Cannot fetch peers")
// return Machines{}, err shared, err := h.getShared(machine)
// } if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
// sharedTo, err := h.getSharedTo(machine) return Machines{}, err
// if err != nil { }
// log.Error().
// Caller().
// Err(err).
// Msg("Cannot fetch peers")
// return Machines{}, err sharedTo, err := h.getSharedTo(machine)
// } if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
// peers := append(direct, shared...) return Machines{}, err
// peers = append(peers, sharedTo...) }
peers = append(direct, shared...)
peers, err := h.getAllPeers(machine) peers = append(peers, sharedTo...)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot fetch peers")
return Machines{}, err
} }
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID }) sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })

View File

@ -1,6 +1,7 @@
package headscale package headscale
import ( import (
"fmt"
"strconv" "strconv"
"time" "time"
@ -154,6 +155,89 @@ func (s *Suite) TestGetDirectPeers(c *check.C) {
c.Assert(peersOfMachine0[8].Name, check.Equals, "testmachine10") c.Assert(peersOfMachine0[8].Name, check.Equals, "testmachine10")
} }
func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
type base struct {
namespace *Namespace
key *PreAuthKey
}
var stor []base
for _, name := range []string{"test", "admin"} {
namespace, err := app.CreateNamespace(name)
c.Assert(err, check.IsNil)
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
c.Assert(err, check.IsNil)
stor = append(stor, base{namespace, pak})
}
_, err := app.GetMachineByID(0)
c.Assert(err, check.NotNil)
for index := 0; index <= 10; index++ {
machine := Machine{
ID: uint64(index),
MachineKey: "foo" + strconv.Itoa(index),
NodeKey: "bar" + strconv.Itoa(index),
DiscoKey: "faa" + strconv.Itoa(index),
IPAddress: fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1)),
Name: "testmachine" + strconv.Itoa(index),
NamespaceID: stor[index%2].namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(stor[index%2].key.ID),
}
app.db.Save(&machine)
}
app.aclPolicy = &ACLPolicy{
Groups: map[string][]string{
"group:test": {"admin"},
},
Hosts: map[string]netaddr.IPPrefix{},
TagOwners: map[string][]string{},
ACLs: []ACL{
{Action: "accept", Users: []string{"admin"}, Ports: []string{"*:*"}},
{Action: "accept", Users: []string{"test"}, Ports: []string{"test:*"}},
},
Tests: []ACLTest{},
}
rules, err := app.generateACLRules()
c.Assert(err, check.IsNil)
app.aclRules = rules
adminMachine, err := app.GetMachineByID(1)
c.Logf("Machine(%v), namespace: %v", adminMachine.Name, adminMachine.Namespace)
c.Assert(err, check.IsNil)
testMachine, err := app.GetMachineByID(2)
c.Logf("Machine(%v), namespace: %v", testMachine.Name, testMachine.Namespace)
c.Assert(err, check.IsNil)
_, err = testMachine.GetHostInfo()
c.Assert(err, check.IsNil)
peersOfTestMachine, err := app.getFilteredByACLPeers(testMachine)
c.Assert(err, check.IsNil)
peersOfAdminMachine, err := app.getFilteredByACLPeers(adminMachine)
c.Assert(err, check.IsNil)
c.Log(peersOfTestMachine)
c.Assert(len(peersOfTestMachine), check.Equals, 4)
c.Assert(peersOfTestMachine[0].Name, check.Equals, "testmachine4")
c.Assert(peersOfTestMachine[1].Name, check.Equals, "testmachine6")
c.Assert(peersOfTestMachine[3].Name, check.Equals, "testmachine10")
c.Log(peersOfAdminMachine)
c.Assert(len(peersOfAdminMachine), check.Equals, 9)
c.Assert(peersOfAdminMachine[0].Name, check.Equals, "testmachine2")
c.Assert(peersOfAdminMachine[2].Name, check.Equals, "testmachine4")
c.Assert(peersOfAdminMachine[5].Name, check.Equals, "testmachine7")
}
func (s *Suite) TestExpireMachine(c *check.C) { func (s *Suite) TestExpireMachine(c *check.C) {
namespace, err := app.CreateNamespace("test") namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)

View File

@ -212,6 +212,15 @@ func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) {
return ips, nil return ips, nil
} }
func containsString(ss []string, s string) bool {
for _, v := range ss {
if v == s {
return true
}
}
return false
}
func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool { func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool {
for _, v := range ips { for _, v := range ips {
if v == ip { if v == ip {