Merge pull request 'The mesh says what filters a converged machine: filters kept per node, shown, named by status, previewed with fates (hq ADR 0168)' (#211) from feat/one-thing-filters-a-converged-machine into main
This commit was merged in pull request #211.
This commit is contained in:
@@ -95,11 +95,22 @@ func reportsReaching(t *testing.T, open *stores, reachable []link.Reach, held ..
|
|||||||
// filter is not sent to one that has not. The anchor reports one, as a real host does; this
|
// filter is not sent to one that has not. The anchor reports one, as a real host does; this
|
||||||
// fixture lacked it from 2026-09-28 and nothing ran the test (issue 177).
|
// fixture lacked it from 2026-09-28 and nothing ran the test (issue 177).
|
||||||
Outward: []string{"eth0"},
|
Outward: []string{"eth0"},
|
||||||
|
// And what filters it (ADR 0168): its front end, the runtime's own, and a chain a
|
||||||
|
// predecessor left in the runtime's user chain.
|
||||||
|
Filters: anchorFilters,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// anchorFilters is what the adopted anchor says filters it: ufw's chains, the runtime's, and a
|
||||||
|
// predecessor's chain the mesh did not write.
|
||||||
|
var anchorFilters = []link.Filter{
|
||||||
|
{Where: "table ip filter, chain ufw-reject-input", Owner: "found-firewall", Refuses: "reject"},
|
||||||
|
{Where: "table ip filter, chain DOCKER", Owner: "runtime", Refuses: `iifname != "docker0" oifname "docker0" drop`},
|
||||||
|
{Where: "table ip filter, chain DOCKER-USER", Owner: "other", Refuses: `iifname "eth0" tcp dport 6000 drop`},
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
heldContainer = link.Held{ID: "hello-web.server", Module: "hello-web", Kind: "container",
|
heldContainer = link.Held{ID: "hello-web.server", Module: "hello-web", Kind: "container",
|
||||||
Target: "hello-web", Since: time.Now()}
|
Target: "hello-web", Since: time.Now()}
|
||||||
@@ -264,6 +275,20 @@ func TestConvergingPreviewsThenChangesAndAdoptingKeepsWhatWasTaken(t *testing.T)
|
|||||||
if strings.Contains(preview, "15672") {
|
if strings.Contains(preview, "15672") {
|
||||||
t.Errorf("a loopback listener is in the preview:\n%s", preview)
|
t.Errorf("a loopback listener is in the preview:\n%s", preview)
|
||||||
}
|
}
|
||||||
|
// What filters the machine now, and the fate of each (novox/hq ADR 0168): the predecessor's
|
||||||
|
// chain is named as not the mesh's and left, so the reader knows before the flip.
|
||||||
|
for _, want := range []string{
|
||||||
|
"table ip filter, chain DOCKER-USER",
|
||||||
|
"NOT THE MESH'S; left in force",
|
||||||
|
`iifname "eth0" tcp dport 6000 drop`,
|
||||||
|
"table ip filter, chain ufw-reject-input",
|
||||||
|
"the found firewall's; retired with it",
|
||||||
|
"the container runtime's own; left",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(preview, want) {
|
||||||
|
t.Errorf("the preview does not say %q:\n%s", want, preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, line := range strings.Split(preview, "\n") {
|
for _, line := range strings.Split(preview, "\n") {
|
||||||
if strings.Contains(line, "5000") && !strings.Contains(line, "WILL CLOSE") {
|
if strings.Contains(line, "5000") && !strings.Contains(line, "WILL CLOSE") {
|
||||||
t.Errorf("an undeclared published port is not said to close: %s", line)
|
t.Errorf("an undeclared published port is not said to close: %s", line)
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func showMode(ctx context.Context, inv *inventory.Inventory, node inventory.Node
|
|||||||
if said, err := inv.AdoptionOf(ctx, node.Name); err == nil && len(said.Strays) > 0 {
|
if said, err := inv.AdoptionOf(ctx, node.Name); err == nil && len(said.Strays) > 0 {
|
||||||
showStrays(said.Strays)
|
showStrays(said.Strays)
|
||||||
}
|
}
|
||||||
|
// And what filters it, truthfully (novox/hq ADR 0168): the mesh alone, or not.
|
||||||
|
if filtering, err := inv.FilteringOf(ctx, node.Name); err == nil {
|
||||||
|
showFiltering(filtering, false)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fmt.Printf(" mode adopted since %s\n",
|
fmt.Printf(" mode adopted since %s\n",
|
||||||
@@ -72,10 +76,63 @@ func showMode(ctx context.Context, inv *inventory.Inventory, node inventory.Node
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
showStrays(said.Strays)
|
showStrays(said.Strays)
|
||||||
|
if filtering, err := inv.FilteringOf(ctx, node.Name); err == nil {
|
||||||
|
showFiltering(filtering, true)
|
||||||
|
}
|
||||||
fmt.Printf(" as of %s\n", said.At.Local().Format(time.DateTime))
|
fmt.Printf(" as of %s\n", said.At.Local().Format(time.DateTime))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// showFiltering says what filters a machine, with owners (novox/hq ADR 0168), and for a converged
|
||||||
|
// machine the state of the firewall it was found with. A machine that has not said is not said to
|
||||||
|
// be filtered by anything.
|
||||||
|
func showFiltering(f inventory.Filtering, adopted bool) {
|
||||||
|
if len(f.Filters) == 0 && f.FoundFirewall == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fw := f.FoundFirewall; fw != nil && !adopted {
|
||||||
|
switch {
|
||||||
|
case fw.Active:
|
||||||
|
fmt.Printf(" found firewall %s is ACTIVE on this converged machine; the next apply retires it again\n", fw.Kind)
|
||||||
|
case fw.RetiredBy == inventory.FilterMesh || fw.RetiredBy == "mesh":
|
||||||
|
fmt.Printf(" found firewall %s, retired by the mesh; its configuration stays on disk\n", fw.Kind)
|
||||||
|
case fw.RetiredBy != "":
|
||||||
|
fmt.Printf(" found firewall %s, found inactive — not by the mesh\n", fw.Kind)
|
||||||
|
default:
|
||||||
|
fmt.Printf(" found firewall %s, inactive\n", fw.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(f.Filters) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if f.Alone() {
|
||||||
|
fmt.Printf(" filtered by the mesh alone (%s)\n", filterSummary(f.Filters))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" filtered by NOT the mesh alone: %d rule set(s) the mesh did not write refuse traffic here\n", len(f.Others()))
|
||||||
|
for _, x := range f.Filters {
|
||||||
|
if x.Owner == inventory.FilterOther || x.Owner == inventory.FilterFoundFirewall {
|
||||||
|
fmt.Printf(" %-17s %s — %s: %s\n", "", x.Where, x.Owner, x.Refuses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf(" %-17s and its own: %s\n", "", filterSummary(f.Filters))
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterSummary counts a machine's filters by owner: "mesh 2, runtime 3, ban 1".
|
||||||
|
func filterSummary(filters []inventory.Filter) string {
|
||||||
|
counts := map[string]int{}
|
||||||
|
for _, x := range filters {
|
||||||
|
counts[x.Owner]++
|
||||||
|
}
|
||||||
|
var parts []string
|
||||||
|
for _, owner := range []string{inventory.FilterMesh, inventory.FilterRuntime, inventory.FilterBan, inventory.FilterFoundFirewall, inventory.FilterOther} {
|
||||||
|
if n := counts[owner]; n > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s %d", owner, n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
// showStrays says what a machine runs that the mesh neither wrote nor holds (ADR 0163).
|
// showStrays says what a machine runs that the mesh neither wrote nor holds (ADR 0163).
|
||||||
func showStrays(strays []inventory.Stray) {
|
func showStrays(strays []inventory.Stray) {
|
||||||
if len(strays) == 0 {
|
if len(strays) == 0 {
|
||||||
@@ -661,7 +718,11 @@ func converge(ctx context.Context, open *stores, node string, yes bool, digest s
|
|||||||
}
|
}
|
||||||
derived := derivedFilter{rules: rules, foundation: with.Foundation, mesh: with.Mesh,
|
derived := derivedFilter{rules: rules, foundation: with.Foundation, mesh: with.Mesh,
|
||||||
outward: plan.PublicDomain != "", outwardLinks: with.OutwardLinks}
|
outward: plan.PublicDomain != "", outwardLinks: with.OutwardLinks}
|
||||||
preview, saw := previewOf(node, reported, derived, plan, taken, filter, runs[filter])
|
filtering, err := inv.FilteringOf(ctx, node)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
preview, saw := previewOf(node, reported, filtering, derived, plan, taken, filter, runs[filter])
|
||||||
preview += "\n\n preview " + saw
|
preview += "\n\n preview " + saw
|
||||||
if !yes {
|
if !yes {
|
||||||
return preview + fmt.Sprintf("\n\nNothing has changed. Run `converge %s --yes %s` to do "+
|
return preview + fmt.Sprintf("\n\nNothing has changed. Run `converge %s --yes %s` to do "+
|
||||||
@@ -731,7 +792,7 @@ func converge(ctx context.Context, open *stores, node string, yes bool, digest s
|
|||||||
// previewOf is what converging a node will change, before it changes it, and a short digest of
|
// previewOf is what converging a node will change, before it changes it, and a short digest of
|
||||||
// what it said: every reachable thing and its fate, the modules the flip takes and the filter. The
|
// what it said: every reachable thing and its fate, the modules the flip takes and the filter. The
|
||||||
// digest is what the flip is asked to act on, so it changes whenever any of those would.
|
// digest is what the flip is asked to act on, so it changes whenever any of those would.
|
||||||
func previewOf(node string, reported inventory.Adoption, derived derivedFilter,
|
func previewOf(node string, reported inventory.Adoption, filtering inventory.Filtering, derived derivedFilter,
|
||||||
plan catalogue.Resolution, taken []string, filter string, filterAssigned bool) (string, string) {
|
plan catalogue.Resolution, taken []string, filter string, filterAssigned bool) (string, string) {
|
||||||
var said []string
|
var said []string
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
@@ -823,6 +884,30 @@ func previewOf(node string, reported inventory.Adoption, derived derivedFilter,
|
|||||||
fmt.Fprintf(&b, " the found firewall (%s) is disabled, never flushed: its configuration stays on disk\n", fw)
|
fmt.Fprintf(&b, " the found firewall (%s) is disabled, never flushed: its configuration stays on disk\n", fw)
|
||||||
}
|
}
|
||||||
said = append(said, fmt.Sprintf("filter %s assigned=%t firewall=%s", filter, filterAssigned, fw))
|
said = append(said, fmt.Sprintf("filter %s assigned=%t firewall=%s", filter, filterAssigned, fw))
|
||||||
|
// What filters the machine now, and the fate of each (novox/hq ADR 0168): the found firewall
|
||||||
|
// retired, the runtime's own and bans left, and what the mesh did not write left and named —
|
||||||
|
// so the reader knows before the flip that the machine will not be filtered by the mesh alone.
|
||||||
|
if len(filtering.Filters) > 0 {
|
||||||
|
b.WriteString("\n what filters the machine now, and what the flip does to each:\n")
|
||||||
|
for _, x := range filtering.Filters {
|
||||||
|
fate := "left: " + x.Owner + "'s"
|
||||||
|
switch x.Owner {
|
||||||
|
case inventory.FilterMesh:
|
||||||
|
fate = "the mesh's guard; replaced by its filter"
|
||||||
|
case inventory.FilterFoundFirewall:
|
||||||
|
fate = "the found firewall's; retired with it"
|
||||||
|
case inventory.FilterRuntime:
|
||||||
|
fate = "the container runtime's own; left"
|
||||||
|
case inventory.FilterBan:
|
||||||
|
fate = "a ban list; left"
|
||||||
|
case inventory.FilterOther:
|
||||||
|
fate = "NOT THE MESH'S; left in force — the machine is not filtered by the mesh alone until you remove it"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, " %-50s %s\n", x.Where, fate)
|
||||||
|
fmt.Fprintf(&b, " %-50s %s\n", "", x.Refuses)
|
||||||
|
said = append(said, "filter "+x.Owner+" "+x.Where)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Sorted: the same account, reported in another order, is the same preview.
|
// Sorted: the same account, reported in another order, is the same preview.
|
||||||
sort.Strings(said)
|
sort.Strings(said)
|
||||||
sum := sha256.Sum256([]byte(strings.Join(said, "\n")))
|
sum := sha256.Sum256([]byte(strings.Join(said, "\n")))
|
||||||
|
|||||||
@@ -607,6 +607,10 @@ type answers struct {
|
|||||||
// a consequence of the refusals above: a node that does not resolve is not on the network, and
|
// a consequence of the refusals above: a node that does not resolve is not on the network, and
|
||||||
// a mesh whose hub is that node has no hub.
|
// a mesh whose hub is that node has no hub.
|
||||||
network string
|
network string
|
||||||
|
// filtered is every converged machine that is not filtered by the mesh alone (novox/hq ADR
|
||||||
|
// 0168): what filters it beyond the mesh's own, the runtime's plumbing and bans, by name — a
|
||||||
|
// predecessor's chain, a found firewall in force again. Such a machine is not "all well".
|
||||||
|
filtered map[string]inventory.Filtering
|
||||||
// untaken is, per machine, each assigned module whose resources the machine is holding as it
|
// untaken is, per machine, each assigned module whose resources the machine is holding as it
|
||||||
// found them, and how many — a module that was assigned, sent, and is running none of what it
|
// found them, and how many — a module that was assigned, sent, and is running none of what it
|
||||||
// declares because nothing has taken it (novox/hq ADR 0100, 04-ISSUES/125).
|
// declares because nothing has taken it (novox/hq ADR 0100, 04-ISSUES/125).
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -66,6 +67,21 @@ type meshStatus struct {
|
|||||||
// **A document without this said an outage was a well mesh.** Read from what each machine
|
// **A document without this said an outage was a well mesh.** Read from what each machine
|
||||||
// reported, so it is the machine's account and not the mesh's take-time listing.
|
// reported, so it is the machine's account and not the mesh's take-time listing.
|
||||||
Untaken []machineUntaken `json:"untaken,omitempty"`
|
Untaken []machineUntaken `json:"untaken,omitempty"`
|
||||||
|
// Filtered is every converged machine that is not filtered by the mesh alone (novox/hq ADR
|
||||||
|
// 0168), one entry per rule set the mesh did not write — the found firewall in force again,
|
||||||
|
// or a chain nobody speaks for. Absent when every converged machine is filtered by the mesh
|
||||||
|
// alone. A document without this called a machine well while a predecessor's chain refused
|
||||||
|
// what the mesh declared open.
|
||||||
|
Filtered []machineFiltered `json:"filtered,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// machineFiltered is one rule set on a converged machine that the mesh did not write and that
|
||||||
|
// refuses traffic: where it is, whose the host reads it as, and what it refuses.
|
||||||
|
type machineFiltered struct {
|
||||||
|
Node string `json:"node"`
|
||||||
|
Where string `json:"where"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Refuses string `json:"refuses"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// machineUntaken is one module a machine is holding rather than running, and how many resources of
|
// machineUntaken is one module a machine is holding rather than running, and how many resources of
|
||||||
@@ -173,6 +189,21 @@ func statusAsJSON(asked answers) ([]byte, error) {
|
|||||||
machineUntaken{Node: name, Module: m, Held: asked.untaken[name][m]})
|
machineUntaken{Node: name, Module: m, Held: asked.untaken[name][m]})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
filteredNodes := make([]string, 0, len(asked.filtered))
|
||||||
|
for name := range asked.filtered {
|
||||||
|
filteredNodes = append(filteredNodes, name)
|
||||||
|
}
|
||||||
|
sort.Strings(filteredNodes)
|
||||||
|
for _, name := range filteredNodes {
|
||||||
|
f := asked.filtered[name]
|
||||||
|
if fw := f.FoundFirewall; fw != nil && fw.Active {
|
||||||
|
out.Filtered = append(out.Filtered, machineFiltered{Node: name, Where: "the found firewall",
|
||||||
|
Owner: inventory.FilterFoundFirewall, Refuses: fw.Kind + " is in force again"})
|
||||||
|
}
|
||||||
|
for _, x := range f.Others() {
|
||||||
|
out.Filtered = append(out.Filtered, machineFiltered{Node: name, Where: x.Where, Owner: x.Owner, Refuses: x.Refuses})
|
||||||
|
}
|
||||||
|
}
|
||||||
for name := range asked.refused {
|
for name := range asked.refused {
|
||||||
out.Unresolved = append(out.Unresolved, machineUnresolved{
|
out.Unresolved = append(out.Unresolved, machineUnresolved{
|
||||||
Node: name, Problem: asked.refused[name]})
|
Node: name, Problem: asked.refused[name]})
|
||||||
|
|||||||
@@ -167,3 +167,46 @@ func TestAMachineFailingTheSameWayIsSaidToBeStuck(t *testing.T) {
|
|||||||
t.Fatalf("one failure is not stuck: %v", once)
|
t.Fatalf("one failure is not stuck: %v", once)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A converged machine something other than the mesh filters is named, per rule set, and is not
|
||||||
|
// well (novox/hq ADR 0168); one filtered by the mesh alone is not in the list.
|
||||||
|
func TestAMachineNotFilteredByTheMeshAloneIsNamedAndNotWell(t *testing.T) {
|
||||||
|
alone := inventory.Filtering{Filters: []inventory.Filter{
|
||||||
|
{Where: "table inet mesh, chain input", Owner: inventory.FilterMesh, Refuses: "policy drop"},
|
||||||
|
{Where: "table ip filter, chain DOCKER", Owner: inventory.FilterRuntime, Refuses: "drop"},
|
||||||
|
{Where: "table ip filter, chain f2b-sshd", Owner: inventory.FilterBan, Refuses: "ip saddr 192.0.2.1 reject"},
|
||||||
|
}}
|
||||||
|
if !alone.Alone() {
|
||||||
|
t.Fatal("the mesh's own, the runtime's and a ban are not the mesh alone")
|
||||||
|
}
|
||||||
|
notAlone := inventory.Filtering{
|
||||||
|
Filters: append(alone.Filters, inventory.Filter{Where: "chain HAL-MESH-ONLY (iptables-legacy)",
|
||||||
|
Owner: inventory.FilterOther, Refuses: `-A HAL-MESH-ONLY -m comment --comment "not public" -j DROP`}),
|
||||||
|
FoundFirewall: &inventory.FoundFirewall{Kind: "ufw", Active: true},
|
||||||
|
}
|
||||||
|
asked := answers{nodes: []inventory.Node{{Name: "home-server"}, {Name: "laptop"}},
|
||||||
|
filtered: map[string]inventory.Filtering{"home-server": notAlone}}
|
||||||
|
if asked.well() {
|
||||||
|
t.Fatal("a machine not filtered by the mesh alone reads as well")
|
||||||
|
}
|
||||||
|
body, err := statusAsJSON(asked)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var parsed struct {
|
||||||
|
Filtered []map[string]string `json:"filtered"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(parsed.Filtered) != 2 {
|
||||||
|
t.Fatalf("filtered: %v", parsed.Filtered)
|
||||||
|
}
|
||||||
|
if parsed.Filtered[0]["node"] != "home-server" || parsed.Filtered[0]["owner"] != inventory.FilterFoundFirewall ||
|
||||||
|
parsed.Filtered[1]["where"] != "chain HAL-MESH-ONLY (iptables-legacy)" || parsed.Filtered[1]["owner"] != inventory.FilterOther {
|
||||||
|
t.Fatalf("filtered: %v", parsed.Filtered)
|
||||||
|
}
|
||||||
|
if body, _ := statusAsJSON(answers{nodes: asked.nodes}); strings.Contains(string(body), `"filtered"`) {
|
||||||
|
t.Fatal("a mesh filtered by itself alone carries a filtered list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -227,6 +227,28 @@ func printStatus(asked answers) error {
|
|||||||
" not readable from a commit; that needs a version the host reports as ordered\n\n")
|
" not readable from a commit; that needs a version the host reports as ordered\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(asked.filtered) > 0 {
|
||||||
|
// A converged machine is filtered by the mesh alone, and the mesh says truthfully which
|
||||||
|
// (novox/hq ADR 0168). One that is not — a predecessor's chain still refusing, a found
|
||||||
|
// firewall in force again — is said here, and is not well.
|
||||||
|
machines := make([]string, 0, len(asked.filtered))
|
||||||
|
for name := range asked.filtered {
|
||||||
|
machines = append(machines, name)
|
||||||
|
}
|
||||||
|
sort.Strings(machines)
|
||||||
|
fmt.Printf("%d converged machine(s) are not filtered by the mesh alone:\n", len(machines))
|
||||||
|
for _, name := range machines {
|
||||||
|
f := asked.filtered[name]
|
||||||
|
if fw := f.FoundFirewall; fw != nil && fw.Active {
|
||||||
|
fmt.Printf(" %-12s the found firewall (%s) is in force again; the next apply retires it\n", name, fw.Kind)
|
||||||
|
}
|
||||||
|
for _, x := range f.Others() {
|
||||||
|
fmt.Printf(" %-12s %s (%s): %s\n", name, x.Where, x.Owner, x.Refuses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("\n the mesh wrote none of these and removes none; `node show <node>` lists every filter with its owner\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
if len(asked.untaken) > 0 {
|
if len(asked.untaken) > 0 {
|
||||||
// **Before the adopted line, and it breaks "all well".** An adopted machine is a state
|
// **Before the adopted line, and it breaks "all well".** An adopted machine is a state
|
||||||
// somebody chose and can leave alone; a module assigned to one and never taken is work
|
// somebody chose and can leave alone; a module assigned to one and never taken is work
|
||||||
@@ -357,6 +379,13 @@ func theThreeQuestions(ctx context.Context, open *stores) (answers, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return answers{}, err
|
return answers{}, err
|
||||||
}
|
}
|
||||||
|
// And which converged machines something other than the mesh filters (novox/hq ADR 0168), as
|
||||||
|
// each last reported — the account that was missing when a predecessor's chain refused what the
|
||||||
|
// mesh declared open for eleven hours (04-ISSUES/144, 145).
|
||||||
|
out.filtered, err = filteredMachines(ctx, inv, out.nodes)
|
||||||
|
if err != nil {
|
||||||
|
return answers{}, err
|
||||||
|
}
|
||||||
out.plans, err = inv.RecentPlans(ctx, 5)
|
out.plans, err = inv.RecentPlans(ctx, 5)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return answers{}, err
|
return answers{}, err
|
||||||
@@ -406,6 +435,30 @@ func theThreeQuestions(ctx context.Context, open *stores) (answers, error) {
|
|||||||
//
|
//
|
||||||
// A machine that reports no holds contributes nothing, so a converged mesh answers an empty map and
|
// A machine that reports no holds contributes nothing, so a converged mesh answers an empty map and
|
||||||
// the caller prints nothing.
|
// the caller prints nothing.
|
||||||
|
// filteredMachines is every converged machine not filtered by the mesh alone, with what it last said
|
||||||
|
// filters it (novox/hq ADR 0168). An adopted machine keeps its found firewall by design and is not
|
||||||
|
// counted; a machine that has not said is not said to be filtered by anything.
|
||||||
|
func filteredMachines(ctx context.Context, inv *inventory.Inventory, nodes []inventory.Node) (
|
||||||
|
map[string]inventory.Filtering, error) {
|
||||||
|
out := map[string]inventory.Filtering{}
|
||||||
|
for _, n := range nodes {
|
||||||
|
if n.Adopted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f, err := inv.FilteringOf(ctx, n.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("what filters %s cannot be read: %w", n.Name, err)
|
||||||
|
}
|
||||||
|
if len(f.Filters) == 0 && f.FoundFirewall == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !f.Alone() {
|
||||||
|
out[n.Name] = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func untakenModules(ctx context.Context, inv *inventory.Inventory, nodes []inventory.Node) (
|
func untakenModules(ctx context.Context, inv *inventory.Inventory, nodes []inventory.Node) (
|
||||||
map[string]map[string]int, error) {
|
map[string]map[string]int, error) {
|
||||||
|
|
||||||
@@ -442,7 +495,8 @@ func untakenModules(ctx context.Context, inv *inventory.Inventory, nodes []inven
|
|||||||
// read as success for the whole of the edge cut-over outage (novox/hq 04-ISSUES/125).
|
// read as success for the whole of the edge cut-over outage (novox/hq 04-ISSUES/125).
|
||||||
func (a answers) well() bool {
|
func (a answers) well() bool {
|
||||||
return len(a.wrong) == 0 && len(a.quiet) == 0 && len(a.behind) == 0 &&
|
return len(a.wrong) == 0 && len(a.quiet) == 0 && len(a.behind) == 0 &&
|
||||||
len(a.waiting) == 0 && len(a.refused) == 0 && a.network == "" && len(a.untaken) == 0
|
len(a.waiting) == 0 && len(a.refused) == 0 && a.network == "" && len(a.untaken) == 0 &&
|
||||||
|
len(a.filtered) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// hostSplit is which machines report which host version, for every version more than one machine
|
// hostSplit is which machines report which host version, for every version more than one machine
|
||||||
|
|||||||
@@ -178,6 +178,103 @@ type Stray struct {
|
|||||||
Detail string `json:"detail,omitempty"`
|
Detail string `json:"detail,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A Filter is one place on a machine that refuses traffic, with its owner (novox/hq ADR 0168).
|
||||||
|
type Filter struct {
|
||||||
|
Where string `json:"where"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Refuses string `json:"refuses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owners of a filter, as the host names them (ADR 0168).
|
||||||
|
const (
|
||||||
|
FilterMesh = "mesh"
|
||||||
|
FilterFoundFirewall = "found-firewall"
|
||||||
|
FilterRuntime = "runtime"
|
||||||
|
FilterBan = "ban"
|
||||||
|
FilterOther = "other"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FoundFirewall is the state of a converged machine's found firewall (ADR 0168): in force now or
|
||||||
|
// not, and how it came to be inactive.
|
||||||
|
type FoundFirewall struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Active bool `json:"active"`
|
||||||
|
RetiredBy string `json:"retired_by,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtering is what a machine last said filters it (ADR 0168).
|
||||||
|
type Filtering struct {
|
||||||
|
Filters []Filter
|
||||||
|
FoundFirewall *FoundFirewall
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alone is whether the machine is filtered by the mesh alone: nothing in its list but the mesh's
|
||||||
|
// own, the runtime's plumbing and bans, and no found firewall in force.
|
||||||
|
func (f Filtering) Alone() bool {
|
||||||
|
for _, x := range f.Filters {
|
||||||
|
if x.Owner == FilterOther || x.Owner == FilterFoundFirewall {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f.FoundFirewall == nil || !f.FoundFirewall.Active
|
||||||
|
}
|
||||||
|
|
||||||
|
// Others is every filter that is neither the mesh's, the runtime's nor a ban.
|
||||||
|
func (f Filtering) Others() []Filter {
|
||||||
|
var out []Filter
|
||||||
|
for _, x := range f.Filters {
|
||||||
|
if x.Owner == FilterOther || x.Owner == FilterFoundFirewall {
|
||||||
|
out = append(out, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordFiltering keeps what a machine last said filters it, replacing what was there (ADR 0168).
|
||||||
|
func (i *Inventory) RecordFiltering(ctx context.Context, nodeID string, filters []Filter, found *FoundFirewall) error {
|
||||||
|
raw, err := json.Marshal(nonNil(filters))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var foundRaw any
|
||||||
|
if found != nil {
|
||||||
|
b, err := json.Marshal(found)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
foundRaw = string(b)
|
||||||
|
}
|
||||||
|
_, err = i.store.Pool().Exec(ctx,
|
||||||
|
`update node set filters = $2, found_firewall = $3 where id = $1`, nodeID, raw, foundRaw)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilteringOf is what a machine last said filters it; empty for a machine that never said.
|
||||||
|
func (i *Inventory) FilteringOf(ctx context.Context, name string) (Filtering, error) {
|
||||||
|
var filtersRaw, foundRaw []byte
|
||||||
|
err := i.store.Pool().QueryRow(ctx,
|
||||||
|
`select filters, found_firewall from node where name = $1`, name).Scan(&filtersRaw, &foundRaw)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return Filtering{}, fmt.Errorf("%w: %s", ErrNoSuchNode, name)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return Filtering{}, err
|
||||||
|
}
|
||||||
|
var out Filtering
|
||||||
|
if len(filtersRaw) > 0 {
|
||||||
|
if err := json.Unmarshal(filtersRaw, &out.Filters); err != nil {
|
||||||
|
return Filtering{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(foundRaw) > 0 {
|
||||||
|
out.FoundFirewall = &FoundFirewall{}
|
||||||
|
if err := json.Unmarshal(foundRaw, out.FoundFirewall); err != nil {
|
||||||
|
return Filtering{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Reach is one thing reachable on an adopted node: a listening socket or a published port.
|
// Reach is one thing reachable on an adopted node: a listening socket or a published port.
|
||||||
type Reach struct {
|
type Reach struct {
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- What filters a machine, with owners, as the host reports it with every apply (novox/hq ADR 0168):
|
||||||
|
-- every table and chain that refuses traffic — the mesh's, the found firewall's, the runtime's own,
|
||||||
|
-- a ban, or other — so the mesh says truthfully what filters a converged machine and names what it
|
||||||
|
-- did not write. And the state of the firewall a converged machine was found with: in force now or
|
||||||
|
-- not, and who retired it.
|
||||||
|
alter table node add column filters jsonb;
|
||||||
|
alter table node add column found_firewall jsonb;
|
||||||
@@ -305,6 +305,23 @@ func (e Enrolment) Heard(ctx context.Context, report Report) (news bool, err err
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// What filters the machine, and the state of its found firewall (novox/hq ADR 0168), whenever
|
||||||
|
// it says — every apply of a host that knows how, adopted or converged; never cleared by a
|
||||||
|
// report that carries none, which is every bare word that the node is there.
|
||||||
|
if len(report.Filters) > 0 || report.FoundFirewall != nil {
|
||||||
|
filters := make([]inventory.Filter, 0, len(report.Filters))
|
||||||
|
for _, f := range report.Filters {
|
||||||
|
filters = append(filters, inventory.Filter{Where: f.Where, Owner: f.Owner, Refuses: f.Refuses})
|
||||||
|
}
|
||||||
|
var found *inventory.FoundFirewall
|
||||||
|
if report.FoundFirewall != nil {
|
||||||
|
found = &inventory.FoundFirewall{Kind: report.FoundFirewall.Kind, Active: report.FoundFirewall.Active,
|
||||||
|
RetiredBy: report.FoundFirewall.RetiredBy}
|
||||||
|
}
|
||||||
|
if err := e.Inventory.RecordFiltering(ctx, node.ID, filters, found); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
// Which of its links face outside (novox/hq ADR 0140), whenever it says so. Recorded on every
|
// Which of its links face outside (novox/hq ADR 0140), whenever it says so. Recorded on every
|
||||||
// report that carries it, adopted or converged, because the filter the mesh composes is written
|
// report that carries it, adopted or converged, because the filter the mesh composes is written
|
||||||
// around it — and never cleared by a report that carries none, which is every bare word that the
|
// around it — and never cleared by a report that carries none, which is every bare word that the
|
||||||
|
|||||||
@@ -218,3 +218,45 @@ func TestWhatAnAdoptedNodeHoldsIsKeptAndAnAliveWordDoesNotWipeIt(t *testing.T) {
|
|||||||
t.Fatalf("a report from an adopted node holding nothing did not empty held: %+v", got)
|
t.Fatalf("a report from an adopted node holding nothing did not empty held: %+v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What filters a machine, and the state of its found firewall, are kept from every report that
|
||||||
|
// carries them and never cleared by one that does not (novox/hq ADR 0168).
|
||||||
|
func TestWhatFiltersAMachineIsKeptFromItsReport(t *testing.T) {
|
||||||
|
inv, _, _ := heardFrom(t, link.Report{
|
||||||
|
Node: "home-server", Applied: []string{"a"},
|
||||||
|
Filters: []link.Filter{
|
||||||
|
{Where: "table inet mesh, chain forward", Owner: "mesh", Refuses: "policy drop"},
|
||||||
|
{Where: "chain HAL-MESH-ONLY (iptables-legacy)", Owner: "other", Refuses: "-j DROP"},
|
||||||
|
},
|
||||||
|
FoundFirewall: &link.FoundFirewall{Kind: "ufw", Active: false, RetiredBy: "found-inactive"},
|
||||||
|
})
|
||||||
|
ctx := context.Background()
|
||||||
|
f, err := inv.FilteringOf(ctx, "home-server")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(f.Filters) != 2 || f.Filters[1].Owner != inventory.FilterOther || f.Alone() {
|
||||||
|
t.Fatalf("recorded %+v", f)
|
||||||
|
}
|
||||||
|
if f.FoundFirewall == nil || f.FoundFirewall.RetiredBy != "found-inactive" || f.FoundFirewall.Active {
|
||||||
|
t.Fatalf("the found firewall's state: %+v", f.FoundFirewall)
|
||||||
|
}
|
||||||
|
if len(f.Others()) != 1 || f.Others()[0].Where != "chain HAL-MESH-ONLY (iptables-legacy)" {
|
||||||
|
t.Fatalf("others: %+v", f.Others())
|
||||||
|
}
|
||||||
|
// A bare word that the node is there clears nothing.
|
||||||
|
if _, err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{Node: "home-server"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if again, _ := inv.FilteringOf(ctx, "home-server"); len(again.Filters) != 2 {
|
||||||
|
t.Fatalf("a bare report cleared what filters the machine: %+v", again)
|
||||||
|
}
|
||||||
|
// The next full report replaces it: the chain removed by hand is gone from the record.
|
||||||
|
if _, err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{Node: "home-server", Applied: []string{"a"},
|
||||||
|
Filters: []link.Filter{{Where: "table inet mesh, chain forward", Owner: "mesh", Refuses: "policy drop"}}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if again, _ := inv.FilteringOf(ctx, "home-server"); len(again.Filters) != 1 || !again.Alone() {
|
||||||
|
t.Fatalf("the next report did not replace what filters the machine: %+v", again)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -197,6 +197,15 @@ type Report struct {
|
|||||||
// Strays is what runs on the machine that the mesh neither wrote nor holds (ADR 0163).
|
// Strays is what runs on the machine that the mesh neither wrote nor holds (ADR 0163).
|
||||||
Strays []Stray `json:"strays,omitempty"`
|
Strays []Stray `json:"strays,omitempty"`
|
||||||
|
|
||||||
|
// Filters is what filters the machine now: every table and chain that refuses traffic, with
|
||||||
|
// its owner — the mesh's, the found firewall's, the container runtime's own, a ban, or other
|
||||||
|
// (novox/hq ADR 0168). Every machine reports it, adopted or converged; absent from a host older
|
||||||
|
// than this.
|
||||||
|
Filters []Filter `json:"filters,omitempty"`
|
||||||
|
// FoundFirewall is the state of the firewall a converged machine was found with: in force now
|
||||||
|
// or not, and how it came to be inactive — the mesh disabled it, or it was found so (ADR 0168).
|
||||||
|
FoundFirewall *FoundFirewall `json:"found_firewall,omitempty"`
|
||||||
|
|
||||||
// Profile is what the machine can do, detected again by this apply (novox/hq ADR 0161): the
|
// Profile is what the machine can do, detected again by this apply (novox/hq ADR 0161): the
|
||||||
// same shape enrolment sends, so a machine that gained or lost a capability — switched its
|
// same shape enrolment sends, so a machine that gained or lost a capability — switched its
|
||||||
// network manager — is known at its next push and not at its next enrolment. Absent from a host
|
// network manager — is known at its next push and not at its next enrolment. Absent from a host
|
||||||
@@ -278,6 +287,21 @@ type Held struct {
|
|||||||
Facts map[string]any `json:"facts,omitempty"`
|
Facts map[string]any `json:"facts,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A Filter is one place on a machine that refuses traffic, with its owner (novox/hq ADR 0168):
|
||||||
|
// the host's own shape, carried as data.
|
||||||
|
type Filter struct {
|
||||||
|
Where string `json:"where"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Refuses string `json:"refuses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FoundFirewall is the state of a converged machine's found firewall (ADR 0168).
|
||||||
|
type FoundFirewall struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Active bool `json:"active"`
|
||||||
|
RetiredBy string `json:"retired_by,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// A Stray is a container a machine runs that the mesh neither wrote nor holds (ADR 0163).
|
// A Stray is a container a machine runs that the mesh neither wrote nor holds (ADR 0163).
|
||||||
type Stray struct {
|
type Stray struct {
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
|
|||||||
Reference in New Issue
Block a user