filtering: a per-node 'expose' setting overrides a listen's source (ADR 0051)

listens.from was a manifest constant — one value for every node a module runs
on. Now a per-node setting overrides it: {"expose": {"5432": "anywhere"}} makes
postgres public on the machine it is set for while it stays from:mesh elsewhere,
and the firewall (ADR 0050) is computed from the effective source. Exposure()
validates it — a port the module does not listen on, or a source that is not
mesh/anywhere/machine, is refused rather than reaching nothing; UnusedSettings
knows 'expose' is a real destination. Tested: default mesh, setting opens it to
anywhere, bad settings refused.
This commit is contained in:
2026-09-04 21:12:33 +02:00
parent 931ca6f01e
commit b306c74467
4 changed files with 131 additions and 7 deletions
+13 -2
View File
@@ -138,8 +138,19 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
}
// 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.
rules, err := r.Filtering(with.Generators, with.Ports)
// would write a rule set that closed every other module on the machine. Each module's per-node
// exposure settings override its listens' source first (novox/hq ADR 0051).
exposure := map[string]map[int]string{}
for _, m := range r.Modules {
e, err := Exposure(m, with.Settings[m.Module])
if err != nil {
return nil, err
}
if e != nil {
exposure[m.Module] = e
}
}
rules, err := r.Filtering(with.Generators, with.Ports, exposure)
if err != nil {
return nil, err
}
+67 -3
View File
@@ -3,6 +3,7 @@ package catalogue
import (
"fmt"
"sort"
"strconv"
"strings"
)
@@ -31,7 +32,7 @@ type Rule struct {
// 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(computed map[string]Generator, ports map[string]map[int]int) ([]Rule, error) {
func (r Resolution) Filtering(computed map[string]Generator, ports map[string]map[int]int, exposure map[string]map[int]string) ([]Rule, error) {
// 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.
@@ -70,10 +71,17 @@ func (r Resolution) Filtering(computed map[string]Generator, ports map[string]ma
if at, known := ports[m.Module][l.Port]; known {
port = at
}
at := opening{port: port, protocol: l.At(), from: l.From}
// The source, with any per-node exposure setting applied. The override names the port
// the module declares, so it travels with that port to wherever the machine publishes
// it (novox/hq ADR 0051): the same module is internal on one node and public on another.
from := l.From
if override, set := exposure[m.Module][l.Port]; set {
from = override
}
at := opening{port: port, protocol: l.At(), from: from}
rule, seen := found[at]
if !seen {
rule = &Rule{Port: port, Protocol: l.At(), From: l.From}
rule = &Rule{Port: port, Protocol: l.At(), From: from}
found[at] = rule
}
rule.Because = append(rule.Because, m.Module)
@@ -100,6 +108,62 @@ func (r Resolution) Filtering(computed map[string]Generator, ports map[string]ma
return widest(out), nil
}
// ExposeSetting is the settings key that overrides a listen's source per node (novox/hq ADR 0051):
//
// {"expose": {"5432": "anywhere"}}
//
// makes the module's port 5432 open to the public on the node this is set for, whatever the
// manifest's default. It keys on the port the module declares — the mesh maps that to where the
// machine publishes, and the override travels with it.
const ExposeSetting = "expose"
// Exposure reads a module's per-node exposure overrides from its settings: declared-port → source.
//
// It refuses an override for a port the module does not listen on, or to a source that is not a
// real one — an exposure setting that reaches no port, or names a source nothing enforces, is the
// "reads as a restriction and is none" fault this whole mechanism exists to prevent (novox/hq
// ADR 0048/0050). A module with no `expose` setting yields nothing and keeps its manifest defaults.
func Exposure(m Manifest, layers []Layer) (map[int]string, error) {
listened := make(map[int]bool, len(m.Listens))
for _, l := range m.Listens {
listened[l.Port] = true
}
out := map[int]string{}
for _, layer := range layers {
raw, ok := layer.Values[ExposeSetting]
if !ok {
continue
}
entries, ok := raw.(map[string]any)
if !ok {
return nil, fmt.Errorf("%s: %s is a { port: source } map, and %q set it to something else",
m.Module, ExposeSetting, layer.From)
}
for portText, value := range entries {
port, err := strconv.Atoi(portText)
if err != nil {
return nil, fmt.Errorf("%s exposes %q, which is not a port", m.Module, portText)
}
if !listened[port] {
return nil, fmt.Errorf(
"%s exposes port %d, which it does not listen on — the setting reaches nothing",
m.Module, port)
}
source, ok := value.(string)
if !ok || (source != FromMesh && source != FromEverywhere && source != FromMachine) {
return nil, fmt.Errorf("%s exposes port %d as %v; it is %q, %q or %q",
m.Module, port, value, FromMesh, FromEverywhere, FromMachine)
}
out[port] = source
}
}
if len(out) == 0 {
return nil, nil
}
return out, nil
}
// 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:
+46 -2
View File
@@ -369,7 +369,7 @@ func TestAMachineOffTheNetworkIsBoundToItselfByAnAddressThatWorks(t *testing.T)
// mustFilter is the rule set, refusing to continue if it could not be computed.
func mustFilter(t *testing.T, r Resolution, computed map[string]Generator) []Rule {
t.Helper()
rules, err := r.Filtering(computed, nil)
rules, err := r.Filtering(computed, nil, nil)
if err != nil {
t.Fatalf("no rule set could be computed: %v", err)
}
@@ -440,7 +440,7 @@ func TestAComputedModulesOwnListensAreNotLost(t *testing.T) {
func TestAGeneratorThatCannotSayRefusesTheRuleSet(t *testing.T) {
_, err := Resolution{Node: "anchor",
Modules: []Manifest{{Module: "networking", Computed: "mesh-network"}},
}.Filtering(map[string]Generator{"mesh-network": cannotSay{}}, nil)
}.Filtering(map[string]Generator{"mesh-network": cannotSay{}}, nil, nil)
if err == nil {
t.Fatal("a machine whose open ports could not be computed was given a rule set anyway")
}
@@ -578,3 +578,47 @@ func named(r Resolution) []string {
}
return out
}
// A port's source is a per-node setting, not a manifest constant: the same module is internal on
// one machine and public on another (novox/hq ADR 0051). postgres listens from the mesh by default;
// a setting on one node exposes it to anywhere, and the rule set follows.
func TestExposureSettingOverridesAListensSource(t *testing.T) {
postgres := Manifest{Module: "postgres", Version: "1",
Listens: []Listening{{Port: 5432, From: FromMesh, Why: "the database"}}}
base, err := Resolution{Node: "ace", Modules: []Manifest{postgres}}.Filtering(nil, nil, nil)
if err != nil {
t.Fatalf("filtering: %v", err)
}
if len(base) != 1 || base[0].From != FromMesh {
t.Fatalf("without a setting, the default source is the mesh: %+v", base)
}
expose := map[string]map[int]string{"postgres": {5432: FromEverywhere}}
exposed, err := Resolution{Node: "novox", Modules: []Manifest{postgres}}.Filtering(nil, nil, expose)
if err != nil {
t.Fatalf("filtering: %v", err)
}
if len(exposed) != 1 || exposed[0].From != FromEverywhere {
t.Fatalf("the setting did not open the port to anywhere: %+v", exposed)
}
}
// Exposure refuses a setting that names a port the module does not listen on, or a source that is
// not a real one — a setting reaching nothing is worse than none (novox/hq ADR 0048/0051).
func TestExposureRefusesAPortNotListenedOnAndABadSource(t *testing.T) {
postgres := Manifest{Module: "postgres", Listens: []Listening{{Port: 5432, From: FromMesh}}}
layer := func(port, source string) []Layer {
return []Layer{{From: "node", Values: map[string]any{ExposeSetting: map[string]any{port: source}}}}
}
if _, err := Exposure(postgres, layer("5432", FromEverywhere)); err != nil {
t.Fatalf("a valid exposure was refused: %v", err)
}
if _, err := Exposure(postgres, layer("6379", FromEverywhere)); err == nil {
t.Error("a port the module does not listen on was accepted")
}
if _, err := Exposure(postgres, layer("5432", "everyone")); err == nil {
t.Error("a source that is not mesh/anywhere/machine was accepted")
}
}
+5
View File
@@ -171,6 +171,11 @@ func UnusedSettings(m Manifest, layers []Layer) []string {
var unused []string
for _, layer := range layers {
for key := range layer.Values {
// `expose` is a real destination for a module that listens: it overrides a port's
// source (novox/hq ADR 0051), validated in Exposure, so it is not stray here.
if key == ExposeSetting && len(m.Listens) > 0 {
continue
}
unused = append(unused, fmt.Sprintf(
"%s sets %q, and %s has no file or contribution to merge it into",
layer.From, key, m.Module))