Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76ac3c99bd | ||
|
|
fe5988c536 | ||
|
|
ed5d467d90 | ||
|
|
228d0226dd | ||
|
|
6215ff0760 | ||
|
|
54812306be | ||
|
|
ce9e20fbbc |
@@ -309,7 +309,7 @@ func converge(ctx context.Context, open *stores, node string, yes bool, digest s
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
derived := derivedFilter{rules: rules, foundation: with.Foundation, mesh: with.Mesh,
|
derived := derivedFilter{rules: rules, foundation: with.Foundation, mesh: with.Mesh,
|
||||||
outward: plan.PublicDomain != ""}
|
outward: plan.PublicDomain != "", outwardLinks: with.OutwardLinks}
|
||||||
preview, saw := previewOf(node, reported, derived, plan, taken, filter, runs[filter])
|
preview, saw := previewOf(node, reported, derived, plan, taken, filter, runs[filter])
|
||||||
preview += "\n\n preview " + saw
|
preview += "\n\n preview " + saw
|
||||||
if !yes {
|
if !yes {
|
||||||
@@ -414,6 +414,18 @@ func previewOf(node string, reported inventory.Adoption, derived derivedFilter,
|
|||||||
b.WriteString(" not previewed: traffic the machine routes that is not a published port " +
|
b.WriteString(" not previewed: traffic the machine routes that is not a published port " +
|
||||||
"(a tunnel, NAT in the found firewall) — the derived filter drops it unless a module " +
|
"(a tunnel, NAT in the found firewall) — the derived filter drops it unless a module " +
|
||||||
"declares it\n")
|
"declares it\n")
|
||||||
|
// Which links the filter constrains, said rather than left to the sentence above (novox/hq ADR
|
||||||
|
// 0140). Everything arriving anywhere else is this machine's own guest and keeps working — which
|
||||||
|
// is what a reader most wants to know, because the previous shape of this filter cut a machine's
|
||||||
|
// guests off at the flip without saying so, and that is how this was found.
|
||||||
|
if len(derived.outwardLinks) > 0 {
|
||||||
|
b.WriteString(fmt.Sprintf(" it filters what arrives on: %s, and on the private network "+
|
||||||
|
"— everything its own guests send keeps working\n",
|
||||||
|
strings.Join(derived.outwardLinks, ", ")))
|
||||||
|
} else {
|
||||||
|
b.WriteString(" it has reported no link facing outside, so no filter can be composed " +
|
||||||
|
"for it — the flip is refused until it reports one\n")
|
||||||
|
}
|
||||||
|
|
||||||
isTaken := map[string]bool{}
|
isTaken := map[string]bool{}
|
||||||
for _, m := range taken {
|
for _, m := range taken {
|
||||||
@@ -473,6 +485,10 @@ type derivedFilter struct {
|
|||||||
// mesh is every address on the private network; outward says the machine faces outside.
|
// mesh is every address on the private network; outward says the machine faces outside.
|
||||||
mesh []string
|
mesh []string
|
||||||
outward bool
|
outward bool
|
||||||
|
// outwardLinks is the links this machine reported as facing outside it (novox/hq ADR 0140).
|
||||||
|
// The filter constrains what arrives on them; everything arriving elsewhere is this machine's
|
||||||
|
// own guest and is not filtered.
|
||||||
|
outwardLinks []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// closesOutside is what a narrowing from everywhere to the private network is called: it closes.
|
// closesOutside is what a narrowing from everywhere to the private network is called: it closes.
|
||||||
@@ -498,6 +514,12 @@ func (d derivedFilter) fate(r inventory.Reach) string {
|
|||||||
return "stays open — the mesh's own, from anywhere"
|
return "stays open — the mesh's own, from anywhere"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// This machine's own guests ask it for an address and for names, and those two arrive here
|
||||||
|
// (novox/hq ADR 0140). Admitted by the link they arrive on, so a listener bound anywhere but an
|
||||||
|
// outward link keeps answering them.
|
||||||
|
if (r.Protocol == "udp" && (r.Port == 53 || r.Port == 67)) || (r.Protocol == "tcp" && r.Port == 53) {
|
||||||
|
return "stays open — this machine's own guests asking it for an address and for names"
|
||||||
|
}
|
||||||
for _, rule := range d.rules {
|
for _, rule := range d.rules {
|
||||||
if rule.Port != r.Port || rule.Protocol != r.Protocol {
|
if rule.Port != r.Port || rule.Protocol != r.Protocol {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -148,6 +148,9 @@ func usage() {
|
|||||||
node public-domain <name> the domain it composes its routed names under
|
node public-domain <name> the domain it composes its routed names under
|
||||||
node public-domain <name> <d> ...set it to d
|
node public-domain <name> <d> ...set it to d
|
||||||
node public-domain <name> --clear ...it faces the outside no longer
|
node public-domain <name> --clear ...it faces the outside no longer
|
||||||
|
node networks <name> the networks it routes for what it hosts
|
||||||
|
node networks <name> <cidr>... ...set them; its filter forwards these too
|
||||||
|
node networks <name> --clear ...only the container runtime's own
|
||||||
token issue --node <name> a one-time right to join, for an existing record
|
token issue --node <name> a one-time right to join, for an existing record
|
||||||
token issue --new <name> create the record and issue for it
|
token issue --new <name> create the record and issue for it
|
||||||
token issue ... --adopted ...for a machine in use, which joins adopted
|
token issue ... --adopted ...for a machine in use, which joins adopted
|
||||||
|
|||||||
@@ -66,6 +66,18 @@ func nodeCommand(ctx context.Context, args []string) error {
|
|||||||
// because the damage is already done by the time it prints.
|
// because the damage is already done by the time it prints.
|
||||||
return publicDomain(ctx, inv, args[1:])
|
return publicDomain(ctx, inv, args[1:])
|
||||||
|
|
||||||
|
case "networks":
|
||||||
|
// Removed by novox/hq ADR 0140, which superseded the record that added it. The filter no
|
||||||
|
// longer names any network: it constrains what arrives from outside the machine and says
|
||||||
|
// nothing about what did not, so there is no list to keep. Answered rather than met with
|
||||||
|
// "unknown command", because this was the documented way to stop a flip cutting a machine's
|
||||||
|
// containers off and somebody will reasonably still type it.
|
||||||
|
return errors.New("`node networks` is gone (novox/hq ADR 0140). The filter constrains what " +
|
||||||
|
"arrives from outside this machine and says nothing about traffic that did not, so no " +
|
||||||
|
"network is named anywhere and nothing needs to be said to keep a machine's own " +
|
||||||
|
"containers reaching outward. The machine reports which of its links face outside; see " +
|
||||||
|
"`node show <name>`")
|
||||||
|
|
||||||
case "account":
|
case "account":
|
||||||
// The operator's login on this machine (novox/hq to-be 29): what a home-scoped file is
|
// The operator's login on this machine (novox/hq to-be 29): what a home-scoped file is
|
||||||
// owned by and which account `ssh <node>` uses. Reports with no argument; sets with one;
|
// owned by and which account `ssh <node>` uses. Reports with no argument; sets with one;
|
||||||
|
|||||||
@@ -640,13 +640,19 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return catalogue.Rendering{}, inventory.Node{}, err
|
return catalogue.Rendering{}, inventory.Node{}, err
|
||||||
}
|
}
|
||||||
|
// Which of this machine's links face outside, which is what the derived filter is written
|
||||||
|
// around (novox/hq ADR 0140). Reported by the machine, never set.
|
||||||
|
outwardLinks, err := inv.OutwardLinksOf(ctx, node)
|
||||||
|
if err != nil {
|
||||||
|
return catalogue.Rendering{}, inventory.Node{}, err
|
||||||
|
}
|
||||||
return catalogue.Rendering{
|
return catalogue.Rendering{
|
||||||
BusMembership: memberships[node],
|
BusMembership: memberships[node],
|
||||||
Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports,
|
Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports,
|
||||||
Certificate: certificate, Authority: authority, Mesh: private, Names: names,
|
Certificate: certificate, Authority: authority, Mesh: private, Names: names,
|
||||||
Machines: machines,
|
Machines: machines,
|
||||||
Suffix: overlay.Suffix(), MeshRange: meshRange, Accounts: accounts, Foundation: foundation,
|
Suffix: overlay.Suffix(), MeshRange: meshRange, TunnelInterface: overlay.Interface, Accounts: accounts, Foundation: foundation,
|
||||||
Kept: kept, Adopted: record.Adopted,
|
Kept: kept, Adopted: record.Adopted, OutwardLinks: outwardLinks,
|
||||||
Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, Built: built,
|
Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, Built: built,
|
||||||
BusUsers: busUsers,
|
BusUsers: busUsers,
|
||||||
}, record, nil
|
}, record, nil
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ func anchorRendering(adopted bool) Rendering {
|
|||||||
Values: map[string]any{ExposeSetting: map[string]any{"5000": FromEverywhere}}}}},
|
Values: map[string]any{ExposeSetting: map[string]any{"5000": FromEverywhere}}}}},
|
||||||
Mesh: []string{"10.42.0.1"},
|
Mesh: []string{"10.42.0.1"},
|
||||||
Foundation: []int{5671},
|
Foundation: []int{5671},
|
||||||
Adopted: adopted,
|
// What the machine reported faces outside, which every rule in the filter is written
|
||||||
|
// around (novox/hq ADR 0140).
|
||||||
|
OutwardLinks: []string{"eth0"},
|
||||||
|
TunnelInterface: "mesh0",
|
||||||
|
Adopted: adopted,
|
||||||
// Genesis takes the foundation's modules.
|
// Genesis takes the foundation's modules.
|
||||||
Taken: map[string]bool{"postgres": true, "lavinmq": true},
|
Taken: map[string]bool{"postgres": true, "lavinmq": true},
|
||||||
}
|
}
|
||||||
@@ -575,11 +579,13 @@ func TestAGivenMachineSideReachesTheFilterTheOpeningAndTheConsumer(t *testing.T)
|
|||||||
}
|
}
|
||||||
r := Resolution{Node: "anchor", Modules: []Manifest{forge}}
|
r := Resolution{Node: "anchor", Modules: []Manifest{forge}}
|
||||||
with := Rendering{
|
with := Rendering{
|
||||||
Ports: map[string]map[int]int{"forge": portsAsThePlanWould(forge, given)},
|
Ports: map[string]map[int]int{"forge": portsAsThePlanWould(forge, given)},
|
||||||
Given: map[string]map[int]int{"forge": given},
|
Given: map[string]map[int]int{"forge": given},
|
||||||
Mesh: []string{"10.77.0.1"},
|
Mesh: []string{"10.77.0.1"},
|
||||||
Adopted: true,
|
Adopted: true,
|
||||||
Taken: map[string]bool{"forge": true},
|
OutwardLinks: []string{"eth0"},
|
||||||
|
TunnelInterface: "mesh0",
|
||||||
|
Taken: map[string]bool{"forge": true},
|
||||||
}
|
}
|
||||||
|
|
||||||
// What the runtime is handed: the machine's own port on the outside, the container's within.
|
// What the runtime is handed: the machine's own port on the outside, the container's within.
|
||||||
@@ -660,9 +666,11 @@ func TestALongFormPortIsOpenedWhereTheManifestPublishesIt(t *testing.T) {
|
|||||||
forge := aForge()
|
forge := aForge()
|
||||||
r := Resolution{Node: "anchor", Modules: []Manifest{forge}}
|
r := Resolution{Node: "anchor", Modules: []Manifest{forge}}
|
||||||
composed, err := r.Compose(Rendering{
|
composed, err := r.Compose(Rendering{
|
||||||
Ports: map[string]map[int]int{"forge": portsAsThePlanWould(forge, nil)},
|
Ports: map[string]map[int]int{"forge": portsAsThePlanWould(forge, nil)},
|
||||||
Mesh: []string{"10.77.0.1"},
|
Mesh: []string{"10.77.0.1"},
|
||||||
Adopted: true,
|
Adopted: true,
|
||||||
|
OutwardLinks: []string{"eth0"},
|
||||||
|
TunnelInterface: "mesh0",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -153,6 +153,26 @@ func (b *Build) problems(module string) []string {
|
|||||||
"%s: %q is a bundle and says no language, so nothing can choose a compiler "+
|
"%s: %q is a bundle and says no language, so nothing can choose a compiler "+
|
||||||
"for it", module, a.Name))
|
"for it", module, a.Name))
|
||||||
}
|
}
|
||||||
|
// **A system, for a language that compiles to a binary** (novox/hq ADR 0142). A binary
|
||||||
|
// is pinned to one operating system at link time so a host refuses to touch a machine
|
||||||
|
// it was not built for (novox/hq ADR 0005); an artifact that says nothing would be
|
||||||
|
// compiled for whatever the build machine happened to be, which reads as portable and
|
||||||
|
// is not.
|
||||||
|
if compiled := compilesToABinary(a.Language); compiled && strings.TrimSpace(a.System) == "" {
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"%s: %q is compiled to a binary and says no system, so it would be built for "+
|
||||||
|
"whatever the build machine happens to be. Declare one artifact per "+
|
||||||
|
"system: %s", module, a.Name, spokenSystems()))
|
||||||
|
} else if !compiled && strings.TrimSpace(a.System) != "" {
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"%s: %q names the system %q and is written in %q, which compiles to code that "+
|
||||||
|
"runs anywhere — a system that decides nothing reads as though it did",
|
||||||
|
module, a.Name, a.System, a.Language))
|
||||||
|
} else if compiled && !knownSystem(a.System) {
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"%s: %q is built for %q, and a system is %s",
|
||||||
|
module, a.Name, a.System, spokenSystems()))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if a.From == "" {
|
if a.From == "" {
|
||||||
problems = append(problems, fmt.Sprintf(
|
problems = append(problems, fmt.Sprintf(
|
||||||
@@ -193,3 +213,42 @@ func oneOrOther(n int) string {
|
|||||||
}
|
}
|
||||||
return "them"
|
return "them"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Systems the mesh builds binaries for, which is the set a host may be pinned to (novox/hq ADR 0005).
|
||||||
|
//
|
||||||
|
// **A closed list, and the host's own, not the compiler's.** These are not the values a Go toolchain
|
||||||
|
// would call an operating system — the difference between two of them is a C library, not a kernel.
|
||||||
|
// They are what a machine reports itself to be and what a host is linked to refuse, so the list that
|
||||||
|
// matters is the one the host understands.
|
||||||
|
var systems = []string{"alpine", "android", "arch"}
|
||||||
|
|
||||||
|
// knownSystem is whether the mesh builds for it.
|
||||||
|
func knownSystem(system string) bool {
|
||||||
|
want := strings.ToLower(strings.TrimSpace(system))
|
||||||
|
for _, s := range systems {
|
||||||
|
if s == want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// spokenSystems is the list as a refusal says it, so a reader is one edit from right.
|
||||||
|
func spokenSystems() string {
|
||||||
|
return strings.Join(systems, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// compilesToABinary is whether this language's bundle is a binary for one operating system rather
|
||||||
|
// than code that runs wherever its interpreter does.
|
||||||
|
//
|
||||||
|
// **Asked of the language, not of the artifact.** A module says what it is written in; what that
|
||||||
|
// implies is the mesh's to know, exactly as the compiler is (novox/hq ADR 0142). Asking the artifact
|
||||||
|
// would let two artifacts in one language disagree about whether they are portable.
|
||||||
|
func compilesToABinary(language string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(language)) {
|
||||||
|
case "go":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// bundleFor is a manifest whose one artifact is a bundle in the given language and system.
|
||||||
|
func bundleFor(language, system string) Manifest {
|
||||||
|
return Manifest{Module: "a-component", Build: &Build{Artifacts: []Artifact{
|
||||||
|
{Name: "binary", Kind: ArtifactBundle, Language: language, System: system},
|
||||||
|
}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func problemsOf(t *testing.T, m Manifest) string {
|
||||||
|
t.Helper()
|
||||||
|
return strings.Join(m.Build.problems(m.Module), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// **A language that compiles to a binary must say which system.**
|
||||||
|
//
|
||||||
|
// A binary is pinned to one operating system at link time, so a host refuses to touch a machine it
|
||||||
|
// was not built for. An artifact that says nothing would be compiled for whatever the build machine
|
||||||
|
// happened to be — which reads as portable and is not, and is the fault this check exists for.
|
||||||
|
func TestABinaryMustSayWhichSystemItIsFor(t *testing.T) {
|
||||||
|
got := problemsOf(t, bundleFor("go", ""))
|
||||||
|
if !strings.Contains(got, "says no system") {
|
||||||
|
t.Fatalf("a compiled bundle with no system was accepted:\n%s", got)
|
||||||
|
}
|
||||||
|
// And the refusal names what it could have said, so a reader is one edit from right.
|
||||||
|
for _, system := range []string{"alpine", "android", "arch"} {
|
||||||
|
if !strings.Contains(got, system) {
|
||||||
|
t.Fatalf("the refusal does not name %q as a choice:\n%s", system, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestABinaryThatNamesASystemIsAccepted(t *testing.T) {
|
||||||
|
if got := problemsOf(t, bundleFor("go", "arch")); got != "" {
|
||||||
|
t.Fatalf("a compiled bundle naming a system was refused:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A system the mesh does not build for is refused where it is written. These are the host's own
|
||||||
|
// names, not a compiler's: the difference between two of them is a C library rather than a kernel,
|
||||||
|
// so a value that looks like an operating system to a toolchain is still wrong here.
|
||||||
|
func TestASystemTheMeshDoesNotBuildForIsRefused(t *testing.T) {
|
||||||
|
for _, wrong := range []string{"linux", "debian", "darwin"} {
|
||||||
|
got := problemsOf(t, bundleFor("go", wrong))
|
||||||
|
if !strings.Contains(got, "and a system is") {
|
||||||
|
t.Fatalf("%q was accepted as a system:\n%s", wrong, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **And a language that runs anywhere must not name one.** A system that decides nothing reads as
|
||||||
|
// though it did, which is the same fault as a restriction that restricts nothing (novox/hq ADR 0045).
|
||||||
|
func TestAPortableBundleMayNotNameASystem(t *testing.T) {
|
||||||
|
got := problemsOf(t, bundleFor("typescript", "arch"))
|
||||||
|
if !strings.Contains(got, "runs anywhere") {
|
||||||
|
t.Fatalf("a portable bundle was allowed to name a system:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPortableBundleNamingNoSystemIsAccepted(t *testing.T) {
|
||||||
|
if got := problemsOf(t, bundleFor("typescript", "")); got != "" {
|
||||||
|
t.Fatalf("an ordinary bundle was refused:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One component, one artifact per system: the shape the mesh's own binaries are declared in, and the
|
||||||
|
// reason the target is the artifact's rather than the recipe's.
|
||||||
|
func TestOneArtifactPerSystemIsAccepted(t *testing.T) {
|
||||||
|
m := Manifest{Module: "the-host", Build: &Build{Artifacts: []Artifact{
|
||||||
|
{Name: "arch", Kind: ArtifactBundle, Language: "go", System: "arch"},
|
||||||
|
{Name: "alpine", Kind: ArtifactBundle, Language: "go", System: "alpine"},
|
||||||
|
{Name: "android", Kind: ArtifactBundle, Language: "go", System: "android"},
|
||||||
|
}}}
|
||||||
|
if got := problemsOf(t, m); got != "" {
|
||||||
|
t.Fatalf("one artifact per system was refused:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,6 +124,16 @@ type Rendering struct {
|
|||||||
// nothing on this node keeps them, or the mesh has no operator key.
|
// nothing on this node keeps them, or the mesh has no operator key.
|
||||||
Kept *KeptExport
|
Kept *KeptExport
|
||||||
|
|
||||||
|
// OutwardLinks is the links this machine reported as facing outside it, which the filter is
|
||||||
|
// written around (novox/hq ADR 0140). Empty means the machine has not said, and the mesh
|
||||||
|
// composes no filter for it rather than writing a rule around a link with no name.
|
||||||
|
OutwardLinks []string
|
||||||
|
|
||||||
|
// TunnelInterface is the interface the mesh's private network runs on, named here rather than
|
||||||
|
// imported because the overlay package rests on this one. Traffic arriving on it is the mesh's,
|
||||||
|
// not this machine's own guest, so the filter admits it only by a rule.
|
||||||
|
TunnelInterface string
|
||||||
|
|
||||||
// Foundation is the ports the mesh itself needs reachable on every machine, which no module
|
// Foundation is the ports the mesh itself needs reachable on every machine, which no module
|
||||||
// declares because the foundation is not a module (novox/hq 04-ISSUES/051 and 052). The broker
|
// declares because the foundation is not a module (novox/hq 04-ISSUES/051 and 052). The broker
|
||||||
// is the one that matters: a machine dials it to enrol, and a firewall derived only from
|
// is the one that matters: a machine dials it to enrol, and a firewall derived only from
|
||||||
@@ -345,7 +355,21 @@ func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
filtering := AsNftables(rules, with.Mesh, r.PublicDomain != "", with.Foundation)
|
// **A machine that has not said which links face outside is sent no filter** (novox/hq ADR
|
||||||
|
// 0140). The whole chain is written around those links: with none, the rule that lets this
|
||||||
|
// machine's own guests keep working would name an empty set, which nftables refuses, and a rule
|
||||||
|
// set that does not load is a machine filtering nothing while its unit reports success. Refused
|
||||||
|
// here, where a person reads it, rather than on the machine — and the machine keeps the filter
|
||||||
|
// it already has.
|
||||||
|
if filters := r.filtersHere(); filters != "" && len(with.OutwardLinks) == 0 {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%s cannot be sent a filter: it has not reported which of its links face outside, and "+
|
||||||
|
"every rule in the chain is written around them. It reports that on each apply; "+
|
||||||
|
"`node show %s` says whether it has. Until then %s is not sent, and the machine "+
|
||||||
|
"keeps the filter it has", r.Node, r.Node, filters)
|
||||||
|
}
|
||||||
|
filtering := AsNftables(rules, with.Mesh, r.PublicDomain != "", with.Foundation,
|
||||||
|
with.OutwardLinks, with.TunnelInterface)
|
||||||
|
|
||||||
var out []map[string]any
|
var out []map[string]any
|
||||||
for _, m := range r.Modules {
|
for _, m := range r.Modules {
|
||||||
@@ -852,6 +876,17 @@ func mapping(written string) (outer, inner int, address string, ok bool) {
|
|||||||
return outer, inner, strings.Join(parts[:len(parts)-2], ":"), true
|
return outer, inner, strings.Join(parts[:len(parts)-2], ":"), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filtersHere is the module on this node that loads the machine's packet filter, or empty when none
|
||||||
|
// does. Named rather than counted: a refusal that says which module is one step from acted on.
|
||||||
|
func (r Resolution) filtersHere() string {
|
||||||
|
for _, m := range r.Modules {
|
||||||
|
if m.Filtering != nil {
|
||||||
|
return m.Module
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// Rules is the rule set this node's filter is derived from: every module's listens, what was
|
// Rules is the rule set this node's filter is derived from: every module's listens, what was
|
||||||
// computed for this machine, and each module's per-node exposure. The same answer whether the node
|
// computed for this machine, and each module's per-node exposure. The same answer whether the node
|
||||||
// is adopted or converged — the one loads it as a filter, the other declares it as openings.
|
// is adopted or converged — the one loads it as a filter, the other declares it as openings.
|
||||||
|
|||||||
@@ -230,7 +230,23 @@ const SSHPort = 22
|
|||||||
// It is a floor for the same reason ssh is. A machine nobody can reach is a machine nobody can
|
// It is a floor for the same reason ssh is. A machine nobody can reach is a machine nobody can
|
||||||
// repair; a machine the mesh cannot reach is a machine the mesh cannot manage. Neither is a thing
|
// repair; a machine the mesh cannot reach is a machine the mesh cannot manage. Neither is a thing
|
||||||
// any module asks for, and neither may be derived away.
|
// any module asks for, and neither may be derived away.
|
||||||
func AsNftables(rules []Rule, mesh []string, outward bool, foundation []int) string {
|
func AsNftables(rules []Rule, mesh []string, outward bool, foundation []int,
|
||||||
|
outwardLinks []string, tunnel string) string {
|
||||||
|
// The links that are not this machine's own: the ones facing outside, and the mesh's tunnel.
|
||||||
|
// Traffic arriving on any of them is admitted only by a rule below; traffic arriving anywhere
|
||||||
|
// else is this machine's own guest and is not something the mesh has a position on.
|
||||||
|
//
|
||||||
|
// The tunnel is named here deliberately. Treating it as "not outside" would make a port nothing
|
||||||
|
// declares reachable from every machine in the mesh, which is the derivation abandoned.
|
||||||
|
quoted := make([]string, 0, len(outwardLinks)+1)
|
||||||
|
for _, link := range outwardLinks {
|
||||||
|
quoted = append(quoted, fmt.Sprintf("%q", link))
|
||||||
|
}
|
||||||
|
if tunnel != "" {
|
||||||
|
quoted = append(quoted, fmt.Sprintf("%q", tunnel))
|
||||||
|
}
|
||||||
|
inward := strings.Join(quoted, ", ")
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("# Computed by the mesh from what is assigned to this node.\n")
|
b.WriteString("# Computed by the mesh from what is assigned to this node.\n")
|
||||||
b.WriteString("# Edits are lost on the next declaration; change a module's listens instead.\n\n")
|
b.WriteString("# Edits are lost on the next declaration; change a module's listens instead.\n\n")
|
||||||
@@ -252,6 +268,22 @@ func AsNftables(rules []Rule, mesh []string, outward bool, foundation []int) str
|
|||||||
b.WriteString("\t\ticmp type echo-request accept\n")
|
b.WriteString("\t\ticmp type echo-request accept\n")
|
||||||
b.WriteString("\t\ticmpv6 type { echo-request, nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert } accept\n")
|
b.WriteString("\t\ticmpv6 type { echo-request, nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert } accept\n")
|
||||||
|
|
||||||
|
// **What this machine's own guests must be able to ask it** (novox/hq ADR 0140). A guest gets
|
||||||
|
// its address and its names from this machine, over the link it is on, and those two questions
|
||||||
|
// arrive at the input chain like any other. Denied, the guest never gets an address and never
|
||||||
|
// resolves a name — which is not "a closed port" but a network that does not work at all, and it
|
||||||
|
// is this machine's own guest asking.
|
||||||
|
//
|
||||||
|
// Asked for by the link it arrives on rather than by the address it comes from, for the reason
|
||||||
|
// the forward chain below no longer names an address: a range describes one machine and goes
|
||||||
|
// stale in silence. Anything arriving from outside, or over the tunnel, is not a guest of this
|
||||||
|
// machine and asks through a port somebody declared, like everything else.
|
||||||
|
if len(inward) > 0 {
|
||||||
|
b.WriteString("\t\t# this machine's own guests asking it for an address and for names\n")
|
||||||
|
b.WriteString(fmt.Sprintf("\t\tiifname != { %s } udp dport { 53, 67 } accept\n", inward))
|
||||||
|
b.WriteString(fmt.Sprintf("\t\tiifname != { %s } tcp dport 53 accept\n", inward))
|
||||||
|
}
|
||||||
|
|
||||||
// **ssh, always, and not because a module asked.**
|
// **ssh, always, and not because a module asked.**
|
||||||
//
|
//
|
||||||
// Every other line in this chain is derived from what is assigned here, which is the whole
|
// Every other line in this chain is derived from what is assigned here, which is the whole
|
||||||
@@ -361,19 +393,36 @@ func AsNftables(rules []Rule, mesh []string, outward bool, foundation []int) str
|
|||||||
// about the ports most worth protecting. Rehearsed on three machines: loading these rules
|
// about the ports most worth protecting. Rehearsed on three machines: loading these rules
|
||||||
// refused a port on the host and left a published container port reachable (novox/hq issue 047).
|
// refused a port on the host and left a published container port reachable (novox/hq issue 047).
|
||||||
//
|
//
|
||||||
// The way through is the one the system being replaced already used: deny by default here, and
|
// **What it constrains is traffic arriving from OUTSIDE this machine, and nothing else**
|
||||||
// then explicitly allow the runtime's own networks, so containers keep working while everything
|
// (novox/hq ADR 0140).
|
||||||
// else has to be asked for.
|
//
|
||||||
|
// It used to deny everything here and then allow the machine's own containers back by naming
|
||||||
|
// the address ranges they sit on — two ranges fixed in this file and the rest recorded per
|
||||||
|
// machine. Every way of keeping that list correct failed. A constant describes one machine. A
|
||||||
|
// recorded range goes stale in silence and cannot tell a network the mesh made from one a
|
||||||
|
// predecessor left behind. Generating it from the modules would have put half this rule set on
|
||||||
|
// the machine.
|
||||||
|
//
|
||||||
|
// The list should not exist, because the mesh has no position on a container reaching outward:
|
||||||
|
// that is not a port opened to anybody. So traffic that did not arrive from outside is accepted
|
||||||
|
// in one line, and what did arrive from outside is allowed only where a rule below admits it.
|
||||||
|
//
|
||||||
|
// The tunnel is not "not outside". Accepting everything off it would make a port nothing
|
||||||
|
// declares reachable from any machine in the mesh, which is the derivation abandoned — so it is
|
||||||
|
// named here beside the outward links, and traffic arriving on it meets the rules below like
|
||||||
|
// anything else.
|
||||||
b.WriteString("\tchain forward {\n")
|
b.WriteString("\tchain forward {\n")
|
||||||
b.WriteString("\t\ttype filter hook forward priority filter; policy drop;\n")
|
b.WriteString("\t\ttype filter hook forward priority filter; policy drop;\n")
|
||||||
b.WriteString("\t\tct state established,related accept\n")
|
b.WriteString("\t\tct state established,related accept\n")
|
||||||
b.WriteString("\t\tct state invalid drop\n")
|
b.WriteString("\t\tct state invalid drop\n")
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
// What the container runtime created. Without these, denying by default stops every container
|
// Only when there is a link to name. An empty set is a line nftables refuses, and a rule set
|
||||||
// on the machine — which is exactly the failure the absent chain was avoiding, avoided properly.
|
// that does not load is a machine filtering nothing while its unit reports success — so the
|
||||||
for _, network := range runtimeNetworks {
|
// chain denies rather than renders nonsense. Composing a declaration for a machine that has
|
||||||
b.WriteString(fmt.Sprintf("\t\t# %s\n", network.why))
|
// named none is refused upstream, so this is a floor and not a path anything travels.
|
||||||
b.WriteString(fmt.Sprintf("\t\tip saddr %s accept\n", network.cidr))
|
if inward != "" {
|
||||||
|
b.WriteString("\t\t# this machine's own guests reaching outward: not a port opened to anybody\n")
|
||||||
|
b.WriteString(fmt.Sprintf("\t\tiifname != { %s } accept\n", inward))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rules) > 0 {
|
if len(rules) > 0 {
|
||||||
@@ -430,18 +479,6 @@ func AsNftables(rules []Rule, mesh []string, outward bool, foundation []int) str
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// runtimeNetworks are the container runtime's own networks, which must keep working when the
|
|
||||||
// forward chain denies by default.
|
|
||||||
//
|
|
||||||
// Taken from what the system being replaced allows, which has been carrying this machine's traffic
|
|
||||||
// for months: the runtime's bridge range and the range its compose files are given. A machine whose
|
|
||||||
// runtime is configured with something else needs this to say so — which is a thing the mesh cannot
|
|
||||||
// derive and a reason this list is named here rather than computed.
|
|
||||||
var runtimeNetworks = []struct{ cidr, why string }{
|
|
||||||
{"172.16.0.0/12", "the container runtime's bridge networks"},
|
|
||||||
{"192.168.128.0/17", "the networks its compose files are given"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// byFamily splits addresses into the two nftables understands separately.
|
// byFamily splits addresses into the two nftables understands separately.
|
||||||
//
|
//
|
||||||
// `ip saddr` and `ip6 saddr` are different matches, and one set holding both families is a syntax
|
// `ip saddr` and `ip6 saddr` are different matches, and one set holding both families is a syntax
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func TestTheBrokersPortIsOpenedThoughNoModuleDeclaresIt(t *testing.T) {
|
|||||||
// A machine on the private network, with one ordinary module rule, and nothing that mentions
|
// A machine on the private network, with one ordinary module rule, and nothing that mentions
|
||||||
// the broker — which is every machine.
|
// the broker — which is every machine.
|
||||||
rules := []Rule{{Port: 8080, From: FromMesh, Because: []string{"some-module"}}}
|
rules := []Rule{{Port: 8080, From: FromMesh, Because: []string{"some-module"}}}
|
||||||
out := AsNftables(rules, []string{"10.42.0.1"}, false, []int{brokerPort})
|
out := AsNftables(rules, []string{"10.42.0.1"}, false, []int{brokerPort}, nil, "mesh0")
|
||||||
|
|
||||||
if !strings.Contains(out, "tcp dport 5671 accept") {
|
if !strings.Contains(out, "tcp dport 5671 accept") {
|
||||||
t.Fatalf("the broker's port is not opened, so no machine could enrol:\n%s", out)
|
t.Fatalf("the broker's port is not opened, so no machine could enrol:\n%s", out)
|
||||||
@@ -48,7 +48,7 @@ func TestTheBrokersPortIsOpenedThoughNoModuleDeclaresIt(t *testing.T) {
|
|||||||
// And a mesh that was never told about a broker still gets a ruleset, rather than an empty one or
|
// And a mesh that was never told about a broker still gets a ruleset, rather than an empty one or
|
||||||
// a panic. A control plane in that state cannot issue tokens either, which is where it surfaces.
|
// a panic. A control plane in that state cannot issue tokens either, which is where it surfaces.
|
||||||
func TestNoBrokerMeansNoFoundationRuleRatherThanNoRuleset(t *testing.T) {
|
func TestNoBrokerMeansNoFoundationRuleRatherThanNoRuleset(t *testing.T) {
|
||||||
out := AsNftables(nil, []string{"10.42.0.1"}, false, nil)
|
out := AsNftables(nil, []string{"10.42.0.1"}, false, nil, nil, "mesh0")
|
||||||
if !strings.Contains(out, "table inet mesh") {
|
if !strings.Contains(out, "table inet mesh") {
|
||||||
t.Fatalf("no ruleset at all:\n%s", out)
|
t.Fatalf("no ruleset at all:\n%s", out)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func TestTwoModulesWantingOnePortAreBothNamed(t *testing.T) {
|
|||||||
t.Fatalf("a module that wanted this port open is not named: %+v", rules[0])
|
t.Fatalf("a module that wanted this port open is not named: %+v", rules[0])
|
||||||
}
|
}
|
||||||
// The consequence, which is the reason this matters: removing web must not read as closing 443.
|
// The consequence, which is the reason this matters: removing web must not read as closing 443.
|
||||||
nft := AsNftables(rules, nil, false, nil)
|
nft := AsNftables(rules, nil, false, nil, nil, "mesh0")
|
||||||
if !strings.Contains(nft, "web") || !strings.Contains(nft, "board") {
|
if !strings.Contains(nft, "web") || !strings.Contains(nft, "board") {
|
||||||
t.Fatalf("the rendered rule set does not name both sources:\n%s", nft)
|
t.Fatalf("the rendered rule set does not name both sources:\n%s", nft)
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ func TestAPortOpenToEveryoneIsNotAlsoRestrictedToTheMesh(t *testing.T) {
|
|||||||
func TestWhatNoModuleDeclaredIsClosed(t *testing.T) {
|
func TestWhatNoModuleDeclaredIsClosed(t *testing.T) {
|
||||||
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
||||||
{Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}},
|
{Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}},
|
||||||
}}, nil), []string{"198.51.100.2"}, false, nil)
|
}}, nil), []string{"198.51.100.2"}, false, nil, nil, "mesh0")
|
||||||
// Naming the chain, not just the policy: the forward chain drops too, and an assertion on
|
// Naming the chain, not just the policy: the forward chain drops too, and an assertion on
|
||||||
// "policy drop" alone passes while the input chain accepts everything. It did, once, here.
|
// "policy drop" alone passes while the input chain accepts everything. It did, once, here.
|
||||||
if !strings.Contains(nft, "type filter hook input priority filter; policy drop;") {
|
if !strings.Contains(nft, "type filter hook input priority filter; policy drop;") {
|
||||||
@@ -134,7 +134,7 @@ func TestWhatNoModuleDeclaredIsClosed(t *testing.T) {
|
|||||||
// `flush ruleset` would do the first and not the second: it empties every table on the machine,
|
// `flush ruleset` would do the first and not the second: it empties every table on the machine,
|
||||||
// including the ones the container runtime writes for its bridges.
|
// including the ones the container runtime writes for its bridges.
|
||||||
func TestReloadingReplacesOnlyTheMeshsOwnRules(t *testing.T) {
|
func TestReloadingReplacesOnlyTheMeshsOwnRules(t *testing.T) {
|
||||||
nft := AsNftables(nil, nil, false, nil)
|
nft := AsNftables(nil, nil, false, nil, nil, "mesh0")
|
||||||
if strings.Contains(nft, "flush ruleset") {
|
if strings.Contains(nft, "flush ruleset") {
|
||||||
t.Fatalf("loading the rule set empties every table on the machine:\n%s", nft)
|
t.Fatalf("loading the rule set empties every table on the machine:\n%s", nft)
|
||||||
}
|
}
|
||||||
@@ -160,22 +160,122 @@ func TestReloadingReplacesOnlyTheMeshsOwnRules(t *testing.T) {
|
|||||||
// So the chain exists and denies by default, and the runtime's own networks are allowed explicitly
|
// So the chain exists and denies by default, and the runtime's own networks are allowed explicitly
|
||||||
// — which is how the system being replaced has been doing it on these machines for months.
|
// — which is how the system being replaced has been doing it on these machines for months.
|
||||||
func TestWhatIsForwardedIsGovernedToo(t *testing.T) {
|
func TestWhatIsForwardedIsGovernedToo(t *testing.T) {
|
||||||
nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil)
|
nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, nil, "mesh0")
|
||||||
if !strings.Contains(nft, "hook forward priority filter; policy drop") {
|
if !strings.Contains(nft, "hook forward priority filter; policy drop") {
|
||||||
t.Fatalf("forwarded traffic is not governed, so container ports are open:\n%s", nft)
|
t.Fatalf("forwarded traffic is not governed, so container ports are open:\n%s", nft)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// And containers keep working, which is the whole reason the chain was left out before.
|
// And this machine's own guests keep working, which is the whole reason the chain was left out
|
||||||
func TestTheRuntimesOwnNetworksKeepWorking(t *testing.T) {
|
// before — by not being mentioned (novox/hq ADR 0140).
|
||||||
nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil)
|
//
|
||||||
for _, network := range []string{"172.16.0.0/12", "192.168.128.0/17"} {
|
// It used to be done by naming the address ranges they sit on: two fixed here and the rest recorded
|
||||||
if !strings.Contains(nft, "ip saddr "+network+" accept") {
|
// per machine. That list broke a workstation's containers at a flip and could not be made correct,
|
||||||
t.Fatalf("%s is not allowed, so denying by default stops every container:\n%s", network, nft)
|
// because a range describes one machine and cannot tell a network the mesh made from one a
|
||||||
|
// predecessor left behind. What replaced it is a single line about the links traffic arrives on.
|
||||||
|
func TestThisMachinesOwnGuestsKeepWorkingWithoutBeingNamed(t *testing.T) {
|
||||||
|
nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, []string{"eth0"}, "mesh0")
|
||||||
|
if !strings.Contains(nft, `iifname != { "eth0", "mesh0" } accept`) {
|
||||||
|
t.Fatalf("what did not arrive from outside is not accepted, so this machine's own guests "+
|
||||||
|
"reach nothing:\n%s", nft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No address of a machine's own networks appears anywhere in a rendered filter.
|
||||||
|
//
|
||||||
|
// This is the assertion that fails against the previous behaviour, and it is why it is written on
|
||||||
|
// the text rather than on an outcome: the two ranges were a constant in this file, so nothing but
|
||||||
|
// reading the output catches one creeping back in.
|
||||||
|
func TestNoNetworkOfTheMachinesOwnIsNamed(t *testing.T) {
|
||||||
|
nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, []string{"eth0"}, "mesh0")
|
||||||
|
for _, gone := range []string{"172.16.0.0/12", "192.168.128.0/17", "saddr 192.168", "saddr 172."} {
|
||||||
|
if strings.Contains(nft, gone) {
|
||||||
|
t.Fatalf("%q is named, and a range describes one machine and goes stale in silence:\n%s",
|
||||||
|
gone, nft)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **The tunnel is constrained, not treated as inside.**
|
||||||
|
//
|
||||||
|
// Accepting everything arriving over the private network would make a port nothing declares
|
||||||
|
// reachable from every machine in the mesh — the derivation abandoned, and a rule that reads as a
|
||||||
|
// restriction while restricting nothing. So the tunnel is named beside the outward links, and
|
||||||
|
// traffic arriving on it meets the declared rules like anything else.
|
||||||
|
func TestTheTunnelIsConstrainedLikeAnOutwardLink(t *testing.T) {
|
||||||
|
nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, []string{"eth0"}, "mesh0")
|
||||||
|
line := `iifname != { "eth0", "mesh0" } accept`
|
||||||
|
if !strings.Contains(nft, line) {
|
||||||
|
t.Fatalf("the tunnel is not constrained, so an undeclared port is reachable from any "+
|
||||||
|
"machine in the mesh:\n%s", nft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A machine with two links facing outside has both constrained. Asserted on the one line, because a
|
||||||
|
// rule covering one and not the other would leave a machine filtering half of what reaches it.
|
||||||
|
func TestEveryOutwardLinkIsConstrained(t *testing.T) {
|
||||||
|
nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, []string{"eth0", "wlan0"}, "mesh0")
|
||||||
|
if !strings.Contains(nft, `iifname != { "eth0", "wlan0", "mesh0" } accept`) {
|
||||||
|
t.Fatalf("not every outward link is constrained:\n%s", nft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A guest asks its host for an address and for names, and those two arrive at the input chain. Asked
|
||||||
|
// for by the link they arrive on, so a resolver bound anywhere but an outward link keeps answering.
|
||||||
|
func TestGuestsMayAskTheirHostForAnAddressAndNames(t *testing.T) {
|
||||||
|
nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, []string{"eth0"}, "mesh0")
|
||||||
|
for _, want := range []string{
|
||||||
|
`iifname != { "eth0", "mesh0" } udp dport { 53, 67 } accept`,
|
||||||
|
`iifname != { "eth0", "mesh0" } tcp dport 53 accept`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(nft, want) {
|
||||||
|
t.Fatalf("a guest cannot ask its host for an address or a name, which is not a closed "+
|
||||||
|
"port but a network that does not work:\n%s", nft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With no link named at all the chain denies rather than rendering an empty set, which nftables
|
||||||
|
// refuses — and a rule set that does not load is a machine filtering nothing while its unit reports
|
||||||
|
// success. Composing a declaration for such a machine is refused upstream; this is the floor.
|
||||||
|
func TestNoLinkNamedRendersNoCatchAllRatherThanAnEmptySet(t *testing.T) {
|
||||||
|
nft := AsNftables(nil, []string{"198.51.100.2"}, false, nil, nil, "")
|
||||||
|
if strings.Contains(nft, "{ }") || strings.Contains(nft, "iifname != {}") {
|
||||||
|
t.Fatalf("an empty set is rendered, which nftables refuses:\n%s", nft)
|
||||||
|
}
|
||||||
|
if !strings.Contains(nft, "hook forward priority filter; policy drop") {
|
||||||
|
t.Fatalf("the forward chain does not deny:\n%s", nft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A machine that has not said which links face outside is sent no filter, and the refusal names the
|
||||||
|
// module that would have loaded it so the reader knows what is being withheld.
|
||||||
|
func TestAMachineThatNamedNoOutwardLinkIsSentNoFilter(t *testing.T) {
|
||||||
|
r := Resolution{Node: "anchor", Modules: []Manifest{
|
||||||
|
{Module: "nftables", Filtering: &Filtering{Into: "/etc/mesh/filter.nft"}},
|
||||||
|
{Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}},
|
||||||
|
}}
|
||||||
|
_, err := r.Declaration(Rendering{Mesh: []string{"198.51.100.2"}, TunnelInterface: "mesh0"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("a machine that named no outward link was sent a filter written around none")
|
||||||
|
}
|
||||||
|
for _, want := range []string{"anchor", "nftables", "face outside"} {
|
||||||
|
if !strings.Contains(err.Error(), want) {
|
||||||
|
t.Fatalf("the refusal does not say %q: %v", want, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And a machine that names none but loads no filter is not refused: there is nothing to write.
|
||||||
|
func TestAMachineWithNoFilterModuleIsNotRefused(t *testing.T) {
|
||||||
|
r := Resolution{Node: "anchor", Modules: []Manifest{
|
||||||
|
{Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}},
|
||||||
|
}}
|
||||||
|
if _, err := r.Declaration(Rendering{Mesh: []string{"198.51.100.2"}}); err != nil {
|
||||||
|
t.Fatalf("a machine that loads no filter was refused one: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A published port is matched by what the client asked for, not by where the packet ends up.
|
// A published port is matched by what the client asked for, not by where the packet ends up.
|
||||||
//
|
//
|
||||||
// The runtime rewrites the destination before this chain sees it, so a rule naming the published
|
// The runtime rewrites the destination before this chain sees it, so a rule naming the published
|
||||||
@@ -183,7 +283,7 @@ func TestTheRuntimesOwnNetworksKeepWorking(t *testing.T) {
|
|||||||
func TestAPublishedPortIsMatchedByWhatWasAskedFor(t *testing.T) {
|
func TestAPublishedPortIsMatchedByWhatWasAskedFor(t *testing.T) {
|
||||||
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
||||||
{Module: "web", Listens: []Listening{{Port: 8080, From: FromEverywhere}}},
|
{Module: "web", Listens: []Listening{{Port: 8080, From: FromEverywhere}}},
|
||||||
}}, nil), []string{"198.51.100.2"}, false, nil)
|
}}, nil), []string{"198.51.100.2"}, false, nil, nil, "mesh0")
|
||||||
if !strings.Contains(nft, "ct original proto-dst 8080 accept") {
|
if !strings.Contains(nft, "ct original proto-dst 8080 accept") {
|
||||||
t.Fatalf("the forwarded rule does not match the port a client asked for:\n%s", nft)
|
t.Fatalf("the forwarded rule does not match the port a client asked for:\n%s", nft)
|
||||||
}
|
}
|
||||||
@@ -193,7 +293,7 @@ func TestAPublishedPortIsMatchedByWhatWasAskedFor(t *testing.T) {
|
|||||||
func TestAMeshScopedPortIsMeshScopedWhenForwarded(t *testing.T) {
|
func TestAMeshScopedPortIsMeshScopedWhenForwarded(t *testing.T) {
|
||||||
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
||||||
{Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}},
|
{Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}},
|
||||||
}}, nil), []string{"198.51.100.2"}, false, nil)
|
}}, nil), []string{"198.51.100.2"}, false, nil, nil, "mesh0")
|
||||||
if !strings.Contains(nft, "ip saddr { 198.51.100.2 } ct original proto-dst 5432 accept") {
|
if !strings.Contains(nft, "ip saddr { 198.51.100.2 } ct original proto-dst 5432 accept") {
|
||||||
t.Fatalf("a mesh-only port is reachable from anywhere once forwarded:\n%s", nft)
|
t.Fatalf("a mesh-only port is reachable from anywhere once forwarded:\n%s", nft)
|
||||||
}
|
}
|
||||||
@@ -203,7 +303,7 @@ func TestAMeshScopedPortIsMeshScopedWhenForwarded(t *testing.T) {
|
|||||||
func TestFromTheMeshIsTheNodesTheMeshKnows(t *testing.T) {
|
func TestFromTheMeshIsTheNodesTheMeshKnows(t *testing.T) {
|
||||||
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
||||||
{Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}},
|
{Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}},
|
||||||
}}, nil), []string{"198.51.100.2", "198.51.100.3"}, false, nil)
|
}}, nil), []string{"198.51.100.2", "198.51.100.3"}, false, nil, nil, "mesh0")
|
||||||
if !strings.Contains(nft, "ip saddr { 198.51.100.2, 198.51.100.3 } tcp dport 5432 accept") {
|
if !strings.Contains(nft, "ip saddr { 198.51.100.2, 198.51.100.3 } tcp dport 5432 accept") {
|
||||||
t.Fatalf("a mesh-scoped port was not restricted to the mesh's addresses:\n%s", nft)
|
t.Fatalf("a mesh-scoped port was not restricted to the mesh's addresses:\n%s", nft)
|
||||||
}
|
}
|
||||||
@@ -213,7 +313,7 @@ func TestFromTheMeshIsTheNodesTheMeshKnows(t *testing.T) {
|
|||||||
func TestAMeshPortOnANodeWithNoMeshIsClosedAndSaysSo(t *testing.T) {
|
func TestAMeshPortOnANodeWithNoMeshIsClosedAndSaysSo(t *testing.T) {
|
||||||
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
||||||
{Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}},
|
{Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}},
|
||||||
}}, nil), nil, false, nil)
|
}}, nil), nil, false, nil, nil, "mesh0")
|
||||||
if strings.Contains(nft, "dport 5432 accept") {
|
if strings.Contains(nft, "dport 5432 accept") {
|
||||||
t.Fatalf("a port meant for the mesh was opened to everything:\n%s", nft)
|
t.Fatalf("a port meant for the mesh was opened to everything:\n%s", nft)
|
||||||
}
|
}
|
||||||
@@ -226,7 +326,7 @@ func TestAMeshPortOnANodeWithNoMeshIsClosedAndSaysSo(t *testing.T) {
|
|||||||
func TestAMachineScopedPortIsNotOpened(t *testing.T) {
|
func TestAMachineScopedPortIsNotOpened(t *testing.T) {
|
||||||
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
||||||
{Module: "cache", Listens: []Listening{{Port: 6379, From: FromMachine}}},
|
{Module: "cache", Listens: []Listening{{Port: 6379, From: FromMachine}}},
|
||||||
}}, nil), []string{"198.51.100.2"}, false, nil)
|
}}, nil), []string{"198.51.100.2"}, false, nil, nil, "mesh0")
|
||||||
if strings.Contains(nft, "dport 6379 accept") {
|
if strings.Contains(nft, "dport 6379 accept") {
|
||||||
t.Fatalf("a port for this machine only was opened to the network:\n%s", nft)
|
t.Fatalf("a port for this machine only was opened to the network:\n%s", nft)
|
||||||
}
|
}
|
||||||
@@ -238,7 +338,8 @@ func TestTheModuleAskingForTheRuleSetGetsEveryModulesPorts(t *testing.T) {
|
|||||||
{Module: "firewall", Filtering: &Filtering{Into: "/etc/mesh/filter.nft"}},
|
{Module: "firewall", Filtering: &Filtering{Into: "/etc/mesh/filter.nft"}},
|
||||||
{Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}},
|
{Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}},
|
||||||
}}
|
}}
|
||||||
out, err := r.Declaration(Rendering{Mesh: []string{"198.51.100.2"}})
|
out, err := r.Declaration(Rendering{Mesh: []string{"198.51.100.2"},
|
||||||
|
OutwardLinks: []string{"eth0"}, TunnelInterface: "mesh0"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("declaration: %v", err)
|
t.Fatalf("declaration: %v", err)
|
||||||
}
|
}
|
||||||
@@ -268,7 +369,7 @@ func TestAskingForTheRuleSetWithNowhereToPutItIsRefused(t *testing.T) {
|
|||||||
func TestAMeshOnBothAddressFamiliesRendersBoth(t *testing.T) {
|
func TestAMeshOnBothAddressFamiliesRendersBoth(t *testing.T) {
|
||||||
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
nft := AsNftables(mustFilter(t, Resolution{Modules: []Manifest{
|
||||||
{Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}},
|
{Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}},
|
||||||
}}, nil), []string{"198.51.100.2", "2001:db8::2"}, false, nil)
|
}}, nil), []string{"198.51.100.2", "2001:db8::2"}, false, nil, nil, "mesh0")
|
||||||
if !strings.Contains(nft, "ip saddr { 198.51.100.2 } tcp dport 5432 accept") {
|
if !strings.Contains(nft, "ip saddr { 198.51.100.2 } tcp dport 5432 accept") {
|
||||||
t.Fatalf("the machines with v4 addresses were dropped:\n%s", nft)
|
t.Fatalf("the machines with v4 addresses were dropped:\n%s", nft)
|
||||||
}
|
}
|
||||||
@@ -296,7 +397,8 @@ func TestWhatTheMeshComputesIsAppliedBeforeWhatTheModuleDeclared(t *testing.T) {
|
|||||||
"restart-on": []any{"filtering"}},
|
"restart-on": []any{"filtering"}},
|
||||||
},
|
},
|
||||||
}}}
|
}}}
|
||||||
out, err := r.Declaration(Rendering{Mesh: []string{"198.51.100.2"}})
|
out, err := r.Declaration(Rendering{Mesh: []string{"198.51.100.2"},
|
||||||
|
OutwardLinks: []string{"eth0"}, TunnelInterface: "mesh0"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("declaration: %v", err)
|
t.Fatalf("declaration: %v", err)
|
||||||
}
|
}
|
||||||
@@ -672,7 +774,7 @@ func TestExposureRefusesAPortNotListenedOnAndABadSource(t *testing.T) {
|
|||||||
// loading the rules lives on conntrack until it drops, and then the machine is reached from a
|
// loading the rules lives on conntrack until it drops, and then the machine is reached from a
|
||||||
// rescue console (novox/hq issue 047).
|
// rescue console (novox/hq issue 047).
|
||||||
func TestSSHIsOpenFromTheMeshEvenWhenNothingIsAssigned(t *testing.T) {
|
func TestSSHIsOpenFromTheMeshEvenWhenNothingIsAssigned(t *testing.T) {
|
||||||
nft := AsNftables(nil, []string{"198.51.100.2", "198.51.100.3"}, false, nil)
|
nft := AsNftables(nil, []string{"198.51.100.2", "198.51.100.3"}, false, nil, nil, "mesh0")
|
||||||
if !strings.Contains(nft, "ip saddr { 198.51.100.2, 198.51.100.3 } tcp dport 22 accept") {
|
if !strings.Contains(nft, "ip saddr { 198.51.100.2, 198.51.100.3 } tcp dport 22 accept") {
|
||||||
t.Fatalf("ssh is not open to the mesh, so a machine can lock everyone out:\n%s", nft)
|
t.Fatalf("ssh is not open to the mesh, so a machine can lock everyone out:\n%s", nft)
|
||||||
}
|
}
|
||||||
@@ -685,7 +787,7 @@ func TestSSHIsOpenFromTheMeshEvenWhenNothingIsAssigned(t *testing.T) {
|
|||||||
// And from outside as well, on a machine that faces outward — because that is the way in when the
|
// And from outside as well, on a machine that faces outward — because that is the way in when the
|
||||||
// private network is the thing that broke.
|
// private network is the thing that broke.
|
||||||
func TestSSHIsOpenFromOutsideOnAMachineThatFacesIt(t *testing.T) {
|
func TestSSHIsOpenFromOutsideOnAMachineThatFacesIt(t *testing.T) {
|
||||||
nft := AsNftables(nil, []string{"198.51.100.2"}, true, nil)
|
nft := AsNftables(nil, []string{"198.51.100.2"}, true, nil, nil, "mesh0")
|
||||||
if !strings.Contains(nft, "\t\ttcp dport 22 accept") {
|
if !strings.Contains(nft, "\t\ttcp dport 22 accept") {
|
||||||
t.Fatalf("a machine reachable from outside does not answer ssh there:\n%s", nft)
|
t.Fatalf("a machine reachable from outside does not answer ssh there:\n%s", nft)
|
||||||
}
|
}
|
||||||
@@ -697,7 +799,7 @@ func TestSSHIsOpenFromOutsideOnAMachineThatFacesIt(t *testing.T) {
|
|||||||
// to narrow the rule to, so narrowing it shuts the port entirely — on the first machine anybody
|
// to narrow the rule to, so narrowing it shuts the port entirely — on the first machine anybody
|
||||||
// adopts, reached over the network, closed by the act of adopting it.
|
// adopts, reached over the network, closed by the act of adopting it.
|
||||||
func TestSSHIsNeverLeftWithoutARule(t *testing.T) {
|
func TestSSHIsNeverLeftWithoutARule(t *testing.T) {
|
||||||
nft := AsNftables(nil, nil, false, nil)
|
nft := AsNftables(nil, nil, false, nil, nil, "mesh0")
|
||||||
if !strings.Contains(nft, "tcp dport 22 accept") {
|
if !strings.Contains(nft, "tcp dport 22 accept") {
|
||||||
t.Fatalf("a machine with no mesh addresses has no ssh rule, so adopting it locks it:\n%s", nft)
|
t.Fatalf("a machine with no mesh addresses has no ssh rule, so adopting it locks it:\n%s", nft)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -571,6 +571,21 @@ type Artifact struct {
|
|||||||
// image built from this same module's own repository, the same as every other artifact.
|
// image built from this same module's own repository, the same as every other artifact.
|
||||||
Context *ArtifactContext `json:"context,omitempty"`
|
Context *ArtifactContext `json:"context,omitempty"`
|
||||||
|
|
||||||
|
// System is the operating system this artifact is compiled for, for a bundle whose output is a
|
||||||
|
// binary rather than portable code (novox/hq ADR 0142).
|
||||||
|
//
|
||||||
|
// **Named by the artifact, not by the recipe.** A toolchain deliberately accepts nothing from
|
||||||
|
// the module — anything a module could override there it would be writing a Dockerfile to
|
||||||
|
// override — and yet a compiled binary is per operating system, pinned at link time so a host
|
||||||
|
// refuses to touch a machine it was not built for (novox/hq ADR 0005). The way out is that the
|
||||||
|
// target is a property of the artifact: one artifact declared per system, one build each, and
|
||||||
|
// the recipe stays the mesh's.
|
||||||
|
//
|
||||||
|
// Empty for a bundle whose output runs anywhere, which is every interpreted language, and for
|
||||||
|
// every other kind. A bundle in a language that compiles to a binary must say one, because
|
||||||
|
// "compiled for whatever the build machine happened to be" is the fault this exists to prevent.
|
||||||
|
System string `json:"system,omitempty"`
|
||||||
|
|
||||||
// Language is what this module's code is written in, for a bundle.
|
// Language is what this module's code is written in, for a bundle.
|
||||||
//
|
//
|
||||||
// **Declared, never guessed.** Inferring it from what files happen to be present makes a
|
// **Declared, never guessed.** Inferring it from what files happen to be present makes a
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ func TestPrintRehearsalRuleset(t *testing.T) {
|
|||||||
rules := mustFilter(t, Resolution{Modules: []Manifest{
|
rules := mustFilter(t, Resolution{Modules: []Manifest{
|
||||||
{Module: "pub", Listens: []Listening{{Port: 8099, From: FromMesh, Why: "the thing it serves"}}},
|
{Module: "pub", Listens: []Listening{{Port: 8099, From: FromMesh, Why: "the thing it serves"}}},
|
||||||
}}, nil)
|
}}, nil)
|
||||||
t.Log("\n" + AsNftables(rules, []string{"192.0.2.20"}, true, nil))
|
t.Log("\n" + AsNftables(rules, []string{"192.0.2.20"}, true, nil, nil, "mesh0"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- The networks a machine routes for what it hosts, beyond the container runtime's own defaults.
|
||||||
|
--
|
||||||
|
-- novox/hq ADR 0137. The derived packet filter denies forwarding by default and then allows the
|
||||||
|
-- container runtime's two default pools, named in the controller's code with a comment saying that
|
||||||
|
-- a machine configured otherwise "needs this to say so" — and no way to say it. So the filter was
|
||||||
|
-- correct only on a machine whose runtime used the defaults, and silently wrong on any other.
|
||||||
|
--
|
||||||
|
-- Measured on 2026-09-28: flipping a workstation to the derived filter cut egress for five of its
|
||||||
|
-- container networks and for every network its test beds create, because those are allocated from
|
||||||
|
-- ranges the two defaults do not cover. Nothing reported a fault; the containers simply could not
|
||||||
|
-- reach anything.
|
||||||
|
--
|
||||||
|
-- A node-level fact, beside the node's public domain and for the same reason: it is a property of
|
||||||
|
-- the machine, not of whichever module happens to load the filter today. Swapping that module must
|
||||||
|
-- not lose it.
|
||||||
|
--
|
||||||
|
-- Null for a machine that routes nothing but the runtime's defaults, which is the ordinary case and
|
||||||
|
-- what every machine held before this column existed.
|
||||||
|
alter table node add column routed_networks jsonb;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- Which of a machine's links face outside it, replacing the networks it was told to say it routes.
|
||||||
|
--
|
||||||
|
-- novox/hq ADR 0140, superseding 0137 and 0139. The derived filter blocked everything passing
|
||||||
|
-- through a machine and then allowed the machine's own containers back by naming the address ranges
|
||||||
|
-- they sit on: two ranges fixed in the controller's source, the rest recorded by 0043's column.
|
||||||
|
--
|
||||||
|
-- Every route to a correct list fails. A constant describes one machine. A recorded range goes stale
|
||||||
|
-- in silence, and cannot tell a network the mesh made from one a predecessor left behind — measured
|
||||||
|
-- on the control-node, where six ranges fall outside the constants and two of the six belong to
|
||||||
|
-- services the mesh does not run. Generating the list from the modules put half the rule set on the
|
||||||
|
-- machine.
|
||||||
|
--
|
||||||
|
-- The list should not exist, because the mesh has no position on a container reaching outward: that
|
||||||
|
-- is not a port opened to anybody. The filter constrains what arrives from OUTSIDE the machine and
|
||||||
|
-- says nothing about what did not, which needs one fact instead of a list — which links "outside"
|
||||||
|
-- arrives on.
|
||||||
|
--
|
||||||
|
-- Reported by the machine on every apply, never recorded by hand, so it cannot go stale. Null for a
|
||||||
|
-- machine that has not reported yet; the mesh composes no filter for such a machine and leaves the
|
||||||
|
-- one it has, because a rule written around a link with no name is a rule set that does not load.
|
||||||
|
alter table node add column outward_links jsonb;
|
||||||
|
|
||||||
|
-- What 0043 recorded is not migrated into it. The ranges answered a question that no longer exists,
|
||||||
|
-- and every machine that named one keeps working without it: the traffic those ranges allowed is now
|
||||||
|
-- allowed by not having arrived from outside.
|
||||||
|
alter table node drop column routed_networks;
|
||||||
@@ -518,6 +518,61 @@ func (i *Inventory) PublicDomainOf(ctx context.Context, name string) (string, er
|
|||||||
return *domain, nil
|
return *domain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RecordOutwardLinks keeps the links a machine reported as facing outside it.
|
||||||
|
//
|
||||||
|
// A reported fact, not a setting (novox/hq ADR 0140). It replaces the networks a machine used to be
|
||||||
|
// told to say it routes: the filter blocked everything passing through and then allowed the machine's
|
||||||
|
// own containers back by naming their address ranges, and every way of keeping that list correct
|
||||||
|
// failed — a constant describes one machine, and a recorded range goes stale in silence. The filter
|
||||||
|
// now constrains what arrives from outside and says nothing about what did not, and the one thing it
|
||||||
|
// needs is which links "outside" arrives on. The machine reads that from its own routing table on
|
||||||
|
// every apply, so it cannot go stale and nobody types it.
|
||||||
|
//
|
||||||
|
// An empty list clears it, which is what a machine with no route off itself reports. The mesh then
|
||||||
|
// composes no filter for that machine at all.
|
||||||
|
func (i *Inventory) RecordOutwardLinks(ctx context.Context, id string, links []string) error {
|
||||||
|
var kept []string
|
||||||
|
for _, name := range links {
|
||||||
|
if name = strings.TrimSpace(name); name != "" {
|
||||||
|
kept = append(kept, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(kept) == 0 {
|
||||||
|
_, err := i.store.Pool().Exec(ctx,
|
||||||
|
`update node set outward_links = null where id = $1`, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(kept)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = i.store.Pool().Exec(ctx,
|
||||||
|
`update node set outward_links = $2 where id = $1`, id, string(body))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutwardLinksOf is the links a machine reported as facing outside it, empty when it has reported
|
||||||
|
// none — which is a machine the mesh composes no filter for.
|
||||||
|
func (i *Inventory) OutwardLinksOf(ctx context.Context, name string) ([]string, error) {
|
||||||
|
var body []byte
|
||||||
|
err := i.store.Pool().QueryRow(ctx,
|
||||||
|
`select outward_links from node where name = $1`, name).Scan(&body)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrNoSuchNode, name)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(body) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var links []string
|
||||||
|
if err := json.Unmarshal(body, &links); err != nil {
|
||||||
|
return nil, fmt.Errorf("the outward links recorded for %s are not a list: %w", name, err)
|
||||||
|
}
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RecordOverlayKey keeps the public half a node generated.
|
// RecordOverlayKey keeps the public half a node generated.
|
||||||
func (i *Inventory) RecordOverlayKey(ctx context.Context, node, key string) error {
|
func (i *Inventory) RecordOverlayKey(ctx context.Context, node, key string) error {
|
||||||
if strings.TrimSpace(key) == "" {
|
if strings.TrimSpace(key) == "" {
|
||||||
|
|||||||
@@ -301,6 +301,17 @@ func (e Enrolment) Heard(ctx context.Context, report Report) (news bool, err err
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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
|
||||||
|
// around it — and never cleared by a report that carries none, which is every bare word that the
|
||||||
|
// node is there. A machine whose routing table it could not read reports nothing rather than
|
||||||
|
// guessing, and keeps whatever it last said; a machine with genuinely no route off itself is one
|
||||||
|
// the mesh composes no filter for at all.
|
||||||
|
if len(report.Outward) > 0 {
|
||||||
|
if err := e.Inventory.RecordOutwardLinks(ctx, node.ID, report.Outward); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
// What it says about the tunnel it carried (novox/hq ADR 0105), whenever it says it.
|
// What it says about the tunnel it carried (novox/hq ADR 0105), whenever it says it.
|
||||||
if report.Tunnel != nil {
|
if report.Tunnel != nil {
|
||||||
if err := e.Inventory.RecordCarriedTunnel(ctx, node.ID, inventory.Carried{
|
if err := e.Inventory.RecordCarriedTunnel(ctx, node.ID, inventory.Carried{
|
||||||
|
|||||||
@@ -170,6 +170,20 @@ type Report struct {
|
|||||||
// Firewall is the firewall found on the machine — "ufw" or "none" — and empty on a node that
|
// Firewall is the firewall found on the machine — "ufw" or "none" — and empty on a node that
|
||||||
// was never asked, which is every converged one.
|
// was never asked, which is every converged one.
|
||||||
Firewall string `json:"firewall,omitempty"`
|
Firewall string `json:"firewall,omitempty"`
|
||||||
|
|
||||||
|
// Outward is the links on this machine that face outside it — the ones carrying a default route
|
||||||
|
// (novox/hq ADR 0140). Every node reports it, adopted or converged, because the filter the mesh
|
||||||
|
// composes for it is written around these and nothing else.
|
||||||
|
//
|
||||||
|
// **It replaces a list of addresses.** The filter used to block everything passing through the
|
||||||
|
// machine and then allow the machine's own containers back by naming the ranges they sit on. A
|
||||||
|
// range describes one machine and goes stale in silence; the link carrying the default route is
|
||||||
|
// read afresh on every report and does not change when a module is added or removed.
|
||||||
|
//
|
||||||
|
// Empty means the machine has not said. The mesh composes no filter for such a machine and
|
||||||
|
// leaves the one it has: a rule written around a link with no name is a rule set that does not
|
||||||
|
// load, and that is a machine filtering nothing while its unit reports success.
|
||||||
|
Outward []string `json:"outward,omitempty"`
|
||||||
// Reachable is what can be reached on the machine now: every listening socket and every
|
// Reachable is what can be reached on the machine now: every listening socket and every
|
||||||
// published container port. Only an adopted node reports it; it is what converging previews.
|
// published container port. Only an adopted node reports it; it is what converging previews.
|
||||||
Reachable []Reach `json:"reachable,omitempty"`
|
Reachable []Reach `json:"reachable,omitempty"`
|
||||||
|
|||||||
@@ -346,7 +346,8 @@ func (s *Server) reported(ctx context.Context, m Control) {
|
|||||||
// whenever it arrives, which is the behaviour the mesh has had all along.
|
// whenever it arrives, which is the behaviour the mesh has had all along.
|
||||||
func staleAgainst(report Report) string {
|
func staleAgainst(report Report) string {
|
||||||
if report.Rekey != nil || report.Tunnel != nil || len(report.Held) > 0 ||
|
if report.Rekey != nil || report.Tunnel != nil || len(report.Held) > 0 ||
|
||||||
report.Firewall != "" || len(report.Reachable) > 0 || len(report.Carried) > 0 {
|
report.Firewall != "" || len(report.Reachable) > 0 || len(report.Carried) > 0 ||
|
||||||
|
len(report.Outward) > 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return report.Declared
|
return report.Declared
|
||||||
|
|||||||
Reference in New Issue
Block a user