A machine's filtering is computed from what it was assigned

A rule nobody derives is a rule somebody keeps in step by hand, and five HAL
manifests carry a `scope:` key that reads as a restriction and restricts
nothing. Both halves are closed here.

Manifests are parsed strictly. An unknown key is refused, which is the
discipline the host's declaration parser has always had; `scope:` survived
because nothing rejected it.

A module says what it listens on and who may reach it, and saying from where is
required — a rule with no source is open, and must say so rather than appear to
restrict something. The mesh gathers every assigned module's ports, widens
where two overlap, names every module that wanted each one, and renders one
nftables file per node. What no module declared is closed.

Three things it deliberately does not do: it writes no forward policy, because
what a machine routes is the container runtime's business and dropping there
stops every container on the node; it never flushes the whole ruleset, only
its own table; and it carries no command to load itself, because the link may
not carry an action. A service declares `restart-on` the file instead, which is
the shape that rule leaves.

Also fixes a fault the lab found: certificateFor asked where every node is
without the catalogue, so nothing resolved, every machine looked like it was on
no private network, and every certificate the mesh was asked for was refused
with a reason that was not true. Asking that question without the catalogue is
now refused rather than answered wrongly.
This commit is contained in:
2026-08-31 00:25:12 +02:00
parent 646609c1b2
commit 48735171eb
5 changed files with 577 additions and 3 deletions
+49 -2
View File
@@ -1211,6 +1211,14 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
func whereEveryoneIs(ctx context.Context, inv *inventory.Inventory,
shelf map[string]catalogue.Manifest) (map[string]string, error) {
if shelf == nil {
// Refused rather than answered. Being on the private network is a conclusion about what a
// node resolves to, so with no catalogue nothing resolves and the honest answer is
// "nobody" — which is wrong, indistinguishable from a mesh with no overlay, and refused
// every certificate the mesh was asked for while saying the machine was on no network.
return nil, errors.New(
"asked where everyone is without the catalogue, which cannot be answered")
}
places, err := inv.Overlays(ctx)
if err != nil {
return nil, err
@@ -1296,9 +1304,39 @@ func declarationWith(ctx context.Context, inv *inventory.Inventory, node string,
break
}
// And who else is on the private network, which is what a rule saying "from the mesh"
// resolves to. Every node's address, including this one's: a machine reaching itself by its
// own overlay address rather than by loopback is ordinary, and leaving it out would filter
// the node's own traffic to itself with no rule naming why.
private, err := onThePrivateNetwork(ctx, inv)
if err != nil {
return nil, err
}
return plan.Declaration(catalogue.Rendering{
Settings: settings, Generators: gens, Grants: grants, Needed: needed,
Certificate: certificate, Authority: authority})
Certificate: certificate, Authority: authority, Mesh: private})
}
// onThePrivateNetwork is every node's address on the overlay, sorted.
//
// A node with no address is left out rather than rendered as an empty source: an empty entry in a
// source set is a syntax error in the rule file, and a rule file that does not load leaves the
// node filtering whatever it was filtering before -- the one outcome worse than a wrong rule,
// because nothing reports it.
func onThePrivateNetwork(ctx context.Context, inv *inventory.Inventory) ([]string, error) {
places, err := inv.Overlays(ctx)
if err != nil {
return nil, err
}
var out []string
for _, p := range places {
if strings.TrimSpace(p.Address) != "" {
out = append(out, p.Address)
}
}
sort.Strings(out)
return out, nil
}
// certificateFor is what the mesh certifies about one machine's internal name.
@@ -1332,7 +1370,16 @@ func certificateFor(ctx context.Context, inv *inventory.Inventory, node string)
// The name it is certified for. Only a machine on the private network has one — a certificate
// for a name nothing resolves is a certificate nothing can check.
where, err := whereEveryoneIs(ctx, inv, nil)
//
// With the catalogue, not without it. Being on the private network is a conclusion about what
// a node resolves to, so a nil shelf resolves nothing and every machine looks like it is on no
// network — which refused every certificate the mesh was asked for, and said the machine was
// not on a network it plainly was.
shelf, err := inv.Catalogue(ctx)
if err != nil {
return "", "", err
}
where, err := whereEveryoneIs(ctx, inv, shelf)
if err != nil {
return "", "", err
}
+15
View File
@@ -53,6 +53,11 @@ type Rendering struct {
// name the module gave it.
Needed map[string]map[string]string
// Mesh is every node's address on the private network, which is what a rule saying "from the
// mesh" resolves to. Passed in for the same reason grants are: who else is on the network is
// a fact about the mesh, and resolution answers questions about one machine.
Mesh []string
Settings SettingsBy
Generators map[string]Generator
// Grants are the credentials this node must create, for the provisions it offers. Passed in
@@ -80,9 +85,19 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
return nil, err
}
// Once, from every module's listens -- not per module. A module receiving only its own ports
// would write a rule set that closed every other module on the machine.
filtering := AsNftables(r.Filtering(), with.Mesh)
var out []map[string]any
for _, m := range r.Modules {
resources := m.Resources
if f := m.Filtering; f != nil {
resources = append(append([]map[string]any{}, resources...), map[string]any{
"id": FilteringID(), "type": "file", "path": f.Into,
"content": filtering, "mode": "0600",
})
}
if c := m.Certificate; c != nil {
if with.Certificate == "" {
// Asked for and not issued. Refused rather than skipped: a module that serves TLS
+192
View File
@@ -0,0 +1,192 @@
package catalogue
import (
"fmt"
"sort"
"strings"
)
// A Rule is one opening in a node's filter, and what caused it.
//
// **A rule names its source** ([ADR 0007](novox/hq)). Not decoration: the fault this whole
// mechanism exists to prevent is a rule that appears to restrict something and restricts nothing,
// and the first question anybody asks of a generated rule set is *why is this port open* — which
// has no answer unless the derivation carries it through.
type Rule struct {
Port int
Protocol string
// From is who may reach it, as the manifest said: mesh, anywhere, or machine.
From string
// Because is every module that wanted this port open, sorted. More than one is ordinary --
// two modules can want :443 -- and losing the others would make removing one module look
// like it should have closed a port that must stay open.
Because []string
// Why is what those modules said the port is for, in the same order.
Why []string
}
// Filtering is the whole rule set for the node this resolved for.
//
// **Derived from what is assigned here, and from the overlay's shape** — a node's open ports are
// a consequence of what runs on it, not a second list kept in step by hand. Nothing else opens a
// port: **what is not declared is closed**, which is the property that makes the derivation worth
// having rather than merely tidy.
func (r Resolution) Filtering() []Rule {
// Keyed by what actually distinguishes an opening. Two modules wanting :443 from the mesh is
// one rule with two sources; one wanting it from the mesh and another from anywhere is two,
// and they are collapsed below -- deliberately, and only in the widening direction.
type opening struct {
port int
protocol string
from string
}
found := map[opening]*Rule{}
for _, m := range r.Modules {
for _, l := range m.Listens {
at := opening{port: l.Port, protocol: l.At(), from: l.From}
rule, seen := found[at]
if !seen {
rule = &Rule{Port: l.Port, Protocol: l.At(), From: l.From}
found[at] = rule
}
rule.Because = append(rule.Because, m.Module)
rule.Why = append(rule.Why, l.Why)
}
}
out := make([]Rule, 0, len(found))
for _, rule := range found {
out = append(out, *rule)
}
// By port, then protocol, then source. A rule set that reorders itself between two runs of an
// unchanged mesh is a diff nobody can read, and every consumer of this compares one to the
// last.
sort.Slice(out, func(a, b int) bool {
if out[a].Port != out[b].Port {
return out[a].Port < out[b].Port
}
if out[a].Protocol != out[b].Protocol {
return out[a].Protocol < out[b].Protocol
}
return out[a].From < out[b].From
})
return widest(out)
}
// widest drops a rule that another already covers.
//
// A port open to anywhere is not additionally restricted by a second rule opening it to the mesh:
// keeping both would render two lines where the narrower one restricts nothing, which is exactly
// the appearance-of-restriction this mechanism exists to remove. The narrower rule's sources are
// carried onto the wider one, so the module that wanted it is still named.
func widest(rules []Rule) []Rule {
reach := map[string]int{FromMachine: 0, FromMesh: 1, FromEverywhere: 2}
var out []Rule
for _, rule := range rules {
covered := false
for i := range out {
if out[i].Port != rule.Port || out[i].Protocol != rule.Protocol {
continue
}
if reach[out[i].From] >= reach[rule.From] {
out[i].Because = append(out[i].Because, rule.Because...)
out[i].Why = append(out[i].Why, rule.Why...)
covered = true
break
}
// The other way round: this one is wider, so it replaces what is there and takes its
// sources with it.
rule.Because = append(rule.Because, out[i].Because...)
rule.Why = append(rule.Why, out[i].Why...)
out = append(out[:i], out[i+1:]...)
break
}
if !covered {
out = append(out, rule)
}
}
return out
}
// AsNftables renders a rule set as a complete nftables configuration.
//
// Complete rather than a fragment: `nft -f` on this file replaces the table entirely, so what the
// node ends up filtering is what the mesh computed and nothing left over from before. A fragment
// appended to whatever was there would make the derivation advisory, and an advisory firewall is
// the unenforced rule again.
//
// `mesh` is rendered as the addresses of the nodes that are actually on the private network, not
// as a subnet. The mesh knows every address; a subnet is a guess that stays wrong quietly, and
// the set shrinks when a node leaves without anybody editing anything.
func AsNftables(rules []Rule, mesh []string) string {
var b strings.Builder
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")
// Replacing this table and nothing else, every time it is loaded. Declared as an empty table
// first so the delete cannot fail on a machine that has never loaded one — the alternative
// idiom, `flush ruleset`, empties every table on the machine, including the ones the container
// runtime writes for its bridges. Reloading the mesh's rules must not stop every container.
b.WriteString("table inet mesh {}\ndelete table inet mesh\n\n")
b.WriteString("table inet mesh {\n")
b.WriteString("\tchain input {\n")
// Dropping by default is the whole point, and the three lines under it are what keep dropping
// by default from meaning "unreachable": a reply to something this node started, its own
// loopback, and the diagnostic that tells an operator the machine is up and refusing rather
// than gone.
b.WriteString("\t\ttype filter hook input priority filter; policy drop;\n")
b.WriteString("\t\tct state established,related accept\n")
b.WriteString("\t\tct state invalid drop\n")
b.WriteString("\t\tiif lo 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")
if len(rules) > 0 {
b.WriteString("\n")
}
for _, rule := range rules {
for i, module := range rule.Because {
why := ""
if i < len(rule.Why) && rule.Why[i] != "" {
why = " — " + rule.Why[i]
}
b.WriteString(fmt.Sprintf("\t\t# %s%s\n", module, why))
}
switch rule.From {
case FromMachine:
// Nothing. A port bound for something else on the same machine is reached over
// loopback, which is already accepted above -- and rendering a rule for it would open
// it to the network, which is the opposite of what the manifest asked for.
b.WriteString(fmt.Sprintf("\t\t# %s/%d is this machine only; loopback already covers it\n",
rule.Protocol, rule.Port))
case FromMesh:
if len(mesh) == 0 {
// Said, and impossible to render. Left as a comment rather than silently widened
// to anywhere or silently dropped: one of those opens a port nobody asked to
// open, and the other closes one somebody did.
b.WriteString(fmt.Sprintf(
"\t\t# %s/%d wanted the mesh, and this node knows no mesh addresses; closed\n",
rule.Protocol, rule.Port))
break
}
b.WriteString(fmt.Sprintf("\t\tip saddr { %s } %s dport %d accept\n",
strings.Join(mesh, ", "), rule.Protocol, rule.Port))
case FromEverywhere:
b.WriteString(fmt.Sprintf("\t\t%s dport %d accept\n", rule.Protocol, rule.Port))
}
}
b.WriteString("\t}\n")
// Nothing this node sends is filtered, and it is stated rather than left to nftables' default
// so that reading this file answers the question instead of requiring the reader to know it.
b.WriteString("\tchain output {\n\t\ttype filter hook output priority filter; policy accept;\n\t}\n")
// **No forward chain, deliberately.** What this machine forwards is the container runtime's
// business — docker writes its own rules for the bridges it creates, and a second table
// hooking forward is consulted as well as those, so a drop here drops container traffic that
// docker explicitly allowed. Every container on the node stops, including the control plane.
//
// The mesh has no knowledge with which to compute a forwarding policy: nothing in a manifest
// says what a machine routes. Writing one anyway would be a rule nothing derives, which is the
// fault this whole mechanism exists to remove.
b.WriteString("}\n")
return b.String()
}
+221
View File
@@ -0,0 +1,221 @@
package catalogue
import (
"strings"
"testing"
)
// A manifest key nothing reads is worse than a key that is refused: five HAL manifests carried
// `scope:`, which read as a restriction and restricted nothing (novox/hq 04-ISSUES/003). The host's
// declaration parser has always refused unknown keys; manifests never did.
func TestAManifestKeyNothingReadsIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"web","scope":"mesh"}`))
if err == nil {
t.Fatal("a manifest with a key nothing reads was accepted, which is how scope: survived")
}
if !strings.Contains(err.Error(), "scope") {
t.Fatalf("refused without naming the key, which leaves the author guessing: %v", err)
}
}
// The same discipline must not refuse a manifest that is merely ordinary.
func TestAnOrdinaryManifestIsStillAccepted(t *testing.T) {
m, err := ParseManifest([]byte(
`{"module":"web","listens":[{"port":443,"from":"anywhere","why":"the site"}]}`))
if err != nil {
t.Fatalf("an ordinary manifest was refused: %v", err)
}
if len(m.Listens) != 1 || m.Listens[0].At() != "tcp" {
t.Fatalf("listens did not survive parsing: %+v", m.Listens)
}
}
// A rule with no source is open, and must say so rather than appear to restrict something.
func TestAPortWithNoSourceIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"web","listens":[{"port":443}]}`))
if err == nil {
t.Fatal("a port that never said who may reach it was accepted; it reads as a restriction")
}
if !strings.Contains(err.Error(), "from where") {
t.Fatalf("refused without saying what is missing: %v", err)
}
}
func TestASourceNobodyDefinedIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"web","listens":[{"port":443,"from":"lan"}]}`))
if err == nil {
t.Fatal("a source the mesh cannot render was accepted, and would be silently dropped")
}
}
// The derivation is over the whole node, not one module.
func TestTheRuleSetIsEveryAssignedModulesPorts(t *testing.T) {
r := Resolution{Modules: []Manifest{
{Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}},
{Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}},
}}
rules := r.Filtering()
if len(rules) != 2 {
t.Fatalf("a node's rule set lost a module's ports: %+v", rules)
}
if rules[0].Port != 443 || rules[1].Port != 5432 {
t.Fatalf("rules are not in a stable order, so every diff is unreadable: %+v", rules)
}
}
// Every rule names what caused it, and both causes survive.
func TestTwoModulesWantingOnePortAreBothNamed(t *testing.T) {
r := Resolution{Modules: []Manifest{
{Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere, Why: "the site"}}},
{Module: "board", Listens: []Listening{{Port: 443, From: FromEverywhere, Why: "the board"}}},
}}
rules := r.Filtering()
if len(rules) != 1 {
t.Fatalf("one port became %d rules", len(rules))
}
if len(rules[0].Because) != 2 {
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.
nft := AsNftables(rules, nil)
if !strings.Contains(nft, "web") || !strings.Contains(nft, "board") {
t.Fatalf("the rendered rule set does not name both sources:\n%s", nft)
}
}
// Widening, and only widening.
func TestAPortOpenToEveryoneIsNotAlsoRestrictedToTheMesh(t *testing.T) {
r := Resolution{Modules: []Manifest{
{Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}},
{Module: "board", Listens: []Listening{{Port: 443, From: FromMesh}}},
}}
rules := r.Filtering()
if len(rules) != 1 {
t.Fatalf("the same port was rendered twice, once restricting nothing: %+v", rules)
}
if rules[0].From != FromEverywhere {
t.Fatalf("the narrower rule won, which closes a port a module asked to open: %+v", rules[0])
}
if len(rules[0].Because) != 2 {
t.Fatalf("the covered module was dropped along with its rule: %+v", rules[0])
}
}
// What is not declared is closed.
func TestWhatNoModuleDeclaredIsClosed(t *testing.T) {
nft := AsNftables((Resolution{Modules: []Manifest{
{Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}},
}}).Filtering(), []string{"198.51.100.2"})
// 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.
if !strings.Contains(nft, "type filter hook input priority filter; policy drop;") {
t.Fatalf("the input chain does not drop by default, so nothing is filtered:\n%s", nft)
}
if strings.Contains(nft, "dport 22") {
t.Fatalf("a port no module declared was opened:\n%s", nft)
}
// Dropping by default must not mean unreachable: the node's own outbound link to the broker
// is the one connection the whole mesh runs on, and it is a reply to something it started.
if !strings.Contains(nft, "ct state established,related accept") {
t.Fatalf("a node applying this would lose its own link to the mesh:\n%s", nft)
}
if !strings.Contains(nft, "iif lo accept") {
t.Fatalf("loopback is filtered, which breaks everything on the machine:\n%s", nft)
}
}
// Loading it twice must be the same as loading it once, and must not disturb anything else.
//
// `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.
func TestReloadingReplacesOnlyTheMeshsOwnRules(t *testing.T) {
nft := AsNftables(nil, nil)
if strings.Contains(nft, "flush ruleset") {
t.Fatalf("loading the rule set empties every table on the machine:\n%s", nft)
}
if !strings.Contains(nft, "delete table inet mesh") {
t.Fatalf("loading it a second time would add to what is there rather than replace it:\n%s", nft)
}
// The delete must not fail on a machine that has never loaded one, or the service does not
// start and the machine filters nothing while reporting a configuration error.
if !strings.Contains(nft, "table inet mesh {}\ndelete table inet mesh") {
t.Fatalf("the delete has nothing to delete on a machine loading this for the first time:\n%s", nft)
}
}
// The rule set governs what reaches this machine, and nothing else.
//
// A forward policy would be consulted alongside the container runtime's own rules, so dropping
// there stops container traffic the runtime allowed -- every container on the node, control plane
// included. And nothing in a manifest says what a machine routes, so there is nothing to derive it
// from: a forwarding rule here would be exactly the undeclared rule this mechanism removes.
func TestTheRuleSetDoesNotDecideWhatTheMachineForwards(t *testing.T) {
nft := AsNftables(nil, []string{"198.51.100.2"})
if strings.Contains(nft, "hook forward") {
t.Fatalf("the rule set filters forwarding, which stops every container on the node:\n%s", nft)
}
}
// "From the mesh" is the addresses the mesh actually has.
func TestFromTheMeshIsTheNodesTheMeshKnows(t *testing.T) {
nft := AsNftables((Resolution{Modules: []Manifest{
{Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}},
}}).Filtering(), []string{"198.51.100.2", "198.51.100.3"})
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)
}
}
// The case that must not be widened silently.
func TestAMeshPortOnANodeWithNoMeshIsClosedAndSaysSo(t *testing.T) {
nft := AsNftables((Resolution{Modules: []Manifest{
{Module: "store", Listens: []Listening{{Port: 5432, From: FromMesh}}},
}}).Filtering(), nil)
if strings.Contains(nft, "dport 5432 accept") {
t.Fatalf("a port meant for the mesh was opened to everything:\n%s", nft)
}
if !strings.Contains(nft, "closed") {
t.Fatalf("it was closed silently, which is a fault nobody can find:\n%s", nft)
}
}
// A port bound for something else on the same machine must not reach the network.
func TestAMachineScopedPortIsNotOpened(t *testing.T) {
nft := AsNftables((Resolution{Modules: []Manifest{
{Module: "cache", Listens: []Listening{{Port: 6379, From: FromMachine}}},
}}).Filtering(), []string{"198.51.100.2"})
if strings.Contains(nft, "dport 6379 accept") {
t.Fatalf("a port for this machine only was opened to the network:\n%s", nft)
}
}
// The rule set is the machine's, not the asking module's.
func TestTheModuleAskingForTheRuleSetGetsEveryModulesPorts(t *testing.T) {
r := Resolution{Modules: []Manifest{
{Module: "firewall", Filtering: &Filtering{Into: "/etc/mesh/filter.nft"}},
{Module: "web", Listens: []Listening{{Port: 443, From: FromEverywhere}}},
}}
out, err := r.Declaration(Rendering{Mesh: []string{"198.51.100.2"}})
if err != nil {
t.Fatalf("declaration: %v", err)
}
var content string
for _, res := range out {
if res["path"] == "/etc/mesh/filter.nft" {
content, _ = res["content"].(string)
}
}
if content == "" {
t.Fatal("the module that asked for the rule set did not receive one")
}
if !strings.Contains(content, "dport 443 accept") {
t.Fatalf("the rule set holds only the asking module's ports:\n%s", content)
}
}
func TestAskingForTheRuleSetWithNowhereToPutItIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"firewall","filtering":{"into":""}}`))
if err == nil {
t.Fatal("a module asked for the rule set and named no path; it would receive nothing")
}
}
+100 -1
View File
@@ -6,6 +6,7 @@
package catalogue
import (
"bytes"
"encoding/json"
"fmt"
"regexp"
@@ -212,6 +213,24 @@ type Manifest struct {
// module, in a file anybody can read, for ever.
Needs map[string]string `json:"needs,omitempty"`
// Listens is what this module accepts connections on, and from where.
//
// **A rule names its source** ([ADR 0007](novox/hq)). A port with no source is open to
// everything that can reach the machine, and saying so is the difference between a manifest
// that restricts something and one that appears to — which is the fault
// [04-ISSUES/003](novox/hq) records, where five manifests carried a `scope:` nothing read.
//
// **Derived, not kept in step by hand.** A machine's open ports are a consequence of what runs
// on it; the mesh gathers these and hands the whole set to whatever enforces them.
Listens []Listening `json:"listens,omitempty"`
// Filtering is where this module wants the node's whole computed rule set written.
//
// One module per node asks for it, and what it receives is derived from every module's
// `listens` rather than from its own — a firewall is a property of the machine, and a module
// that could only see its own ports would write a rule set that closed everything else.
Filtering *Filtering `json:"filtering,omitempty"`
// Certificate is where this module wants a certificate for its machine's name inside the
// mesh, and where the key that goes with it can be found.
//
@@ -274,6 +293,46 @@ const (
ArtifactUpstream = "upstream"
)
// Listening is one port a module accepts connections on.
type Listening struct {
Port int `json:"port"`
// Protocol is "tcp" or "udp". Absent means tcp, which is what almost everything is — and a
// field that had to be written every time would be written wrongly some of the time.
Protocol string `json:"protocol,omitempty"`
// From is who may reach it. Required, because a rule with no source is open and must say so
// rather than appear to restrict something.
From string `json:"from"`
// Why this port is open, for somebody reading a generated rule set and wondering.
Why string `json:"why,omitempty"`
}
// Where a listening port may be reached from.
const (
// FromMesh is any machine on the private network. What almost everything wants.
FromMesh = "mesh"
// FromEverywhere is the public internet. Deliberately spelled out: a port open to everything
// should be legible as such in the manifest, not the consequence of an omission.
FromEverywhere = "anywhere"
// FromMachine is this machine only — a port bound for something else on the same host.
FromMachine = "machine"
)
// At is this port's protocol, with the default applied.
func (l Listening) At() string {
if l.Protocol == "" {
return "tcp"
}
return l.Protocol
}
// Filtering says where a module wants the computed rule set.
type Filtering struct {
// Into is the path to write it to. Whatever loads it is this module's own business — an
// action beside this field, ordinarily — because how a machine enforces rules is a fact about
// the machine and the mesh has no business knowing it.
Into string `json:"into"`
}
// Certificate says where a module wants what the mesh issued for its machine.
type Certificate struct {
// Into is where the certificate is written.
@@ -287,6 +346,10 @@ type Certificate struct {
func CertificateID() string { return "certificate" }
func AuthorityID() string { return "certificate-authority" }
// FilteringID names the computed rule set, so it is the same resource across every declaration
// and a change to it is an update rather than an addition beside the old one.
func FilteringID() string { return "filtering" }
// NeedID is the resource identity of the file a module's own secret lands in.
func NeedID(name string) string { return "needs-" + name }
@@ -331,7 +394,16 @@ func ReceivedID(requirement string) string { return "received-" + requirement }
// them in one pass or in four.
func ParseManifest(raw []byte) (Manifest, error) {
var m Manifest
if err := json.Unmarshal(raw, &m); err != nil {
// Strictly. **An unknown key is refused**, which is the discipline the host's declaration
// parser has and manifests lacked (novox/hq 04-ISSUES/003): a `scope:` key survived in five
// manifests, read by nothing, making them appear to restrict a port and restrict nothing.
//
// "An unenforced rule is indistinguishable from a wrong one, and costs more, because people
// believe it" — and a silently-accepted key is worse than unenforced, because a reviewer
// checking whether something is restricted will find that it is, and be wrong.
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&m); err != nil {
return Manifest{}, fmt.Errorf("this is not a module manifest: %w", err)
}
@@ -429,6 +501,33 @@ func ParseManifest(raw []byte) (Manifest, error) {
"%s binds %q and does not require it", m.Module, to))
}
}
if f := m.Filtering; f != nil && strings.TrimSpace(f.Into) == "" {
problems = append(problems, fmt.Sprintf(
"%s asks for the computed rule set and does not say where to put it", m.Module))
}
for _, l := range m.Listens {
if l.Port < 1 || l.Port > 65535 {
problems = append(problems, fmt.Sprintf(
"%s listens on port %d, which is not a port", m.Module, l.Port))
}
switch l.From {
case FromMesh, FromEverywhere, FromMachine:
case "":
// The fault this field exists to prevent. A rule with no source is open, and a
// manifest that omitted it would read as a restriction and be none.
problems = append(problems, fmt.Sprintf(
"%s listens on %d and does not say from where; it is %q, %q or %q",
m.Module, l.Port, FromMesh, FromEverywhere, FromMachine))
default:
problems = append(problems, fmt.Sprintf(
"%s listens on %d from %q; it is %q, %q or %q",
m.Module, l.Port, l.From, FromMesh, FromEverywhere, FromMachine))
}
if p := l.At(); p != "tcp" && p != "udp" {
problems = append(problems, fmt.Sprintf(
"%s listens on %d over %q, which is tcp or udp", m.Module, l.Port, p))
}
}
if c := m.Certificate; c != nil {
if !strings.HasPrefix(c.Into, "/") {
problems = append(problems, fmt.Sprintf(