The filter keeps the openings of a module left out of the declaration that still runs (issue 1966, recovery) #218

Merged
mesh-tools merged 2 commits from fix/1966-filter-keeps-openings into main 2026-11-09 09:49:15 +00:00
2 changed files with 88 additions and 36 deletions
Showing only changes of commit 17a4ac620c - Show all commits
+62 -36
View File
@@ -1248,47 +1248,19 @@ func resolutionFiltersHere(r mcat.Resolution) string {
// is adopted or converged — the one loads it as a filter, the other declares it as openings.
func Rules(r mcat.Resolution, with mcat.Rendering) ([]mcat.Rule, error) {
// A module whose settings cannot compose is left out of the declaration (novox/hq ADR 0163,
// rule 6), and its settings with it: its per-node exposure is not read, and its listens open
// as its manifest says them. It is not left out of the filter: what it runs is left running,
// so a port it serves stays open (issue 1966).
// rule 6), and not out of the filter: what it runs is left running, so a port it serves stays
// open (issue 1966). Its per-node exposure and reaches still narrow it (ADR 0046); where they
// cannot be read for it, its listens open no wider than the mesh, never at the manifest's
// default, which could reopen a port an operator had narrowed.
left := LeftOut(r, with.Settings, with.Adopted)
exposure := map[string]map[int]string{}
for _, m := range r.Modules {
if _, isLeft := left[m.Module]; isLeft {
continue
}
e, err := Exposure(m, with.Settings[m.Module])
e, err := exposureOf(m, with.Settings[m.Module])
if err != nil {
return nil, err
}
// And how far each endpoint reaches, which says the same thing to the filter and more
// besides (novox/hq ADR 0138). Folded in here rather than beside: the filter has one
// question — from where — and a reach answers it, so giving it two inputs would let them
// disagree. Reaches refuses a port that both name, so this cannot silently prefer one.
reaches, err := Reaches(m, with.Settings[m.Module])
if err != nil {
return nil, err
}
// **Only for an endpoint the proxy does not serve.** A routed endpoint's port is how the
// proxy reaches it and nothing else (ADR 0045), so `public` there asks for a public name and
// says nothing about the port — opening it to the world as well would undo the arrangement
// the proxy exists for, and would silently reopen a port an operator had narrowed.
//
// Found by trying to express a real module: one whose routed name must be public and whose
// machine-side port must not be. Under one value for both, there was no way to say it.
routed := RoutedPorts(m)
for port, reach := range reaches {
if routed[port] {
continue
if _, isLeft := left[m.Module]; !isLeft {
return nil, err
}
source, ok := FilterSource(reach)
if !ok {
return nil, fmt.Errorf("%s: %q is not a reach the filter can read", m.Module, reach)
}
if e == nil {
e = map[int]string{}
}
e[port] = source
e = noWiderThanTheMesh(m)
}
if e != nil {
exposure[m.Module] = e
@@ -1297,6 +1269,60 @@ func Rules(r mcat.Resolution, with mcat.Rendering) ([]mcat.Rule, error) {
return Filtering(r, with.Generators, with.Ports, exposure)
}
// noWiderThanTheMesh is the exposure of a module left out whose own cannot be read: each of its
// listens from wider than the mesh narrowed to the mesh, a narrower one kept.
func noWiderThanTheMesh(m mcat.Manifest) map[int]string {
var e map[int]string
for _, l := range m.Listens {
if l.From == mcat.FromLocalNetwork || l.From == mcat.FromEverywhere {
if e == nil {
e = map[int]string{}
}
e[l.Port] = mcat.FromMesh
}
}
return e
}
// exposureOf is a module's per-node exposure, declared port to source: its expose setting, and
// the reaches of its endpoints the proxy does not serve.
func exposureOf(m mcat.Manifest, layers []mcat.Layer) (map[int]string, error) {
e, err := Exposure(m, layers)
if err != nil {
return nil, err
}
// And how far each endpoint reaches, which says the same thing to the filter and more
// besides (novox/hq ADR 0138). Folded in here rather than beside: the filter has one
// question — from where — and a reach answers it, so giving it two inputs would let them
// disagree. Reaches refuses a port that both name, so this cannot silently prefer one.
reaches, err := Reaches(m, layers)
if err != nil {
return nil, err
}
// **Only for an endpoint the proxy does not serve.** A routed endpoint's port is how the
// proxy reaches it and nothing else (ADR 0045), so `public` there asks for a public name and
// says nothing about the port — opening it to the world as well would undo the arrangement
// the proxy exists for, and would silently reopen a port an operator had narrowed.
//
// Found by trying to express a real module: one whose routed name must be public and whose
// machine-side port must not be. Under one value for both, there was no way to say it.
routed := RoutedPorts(m)
for port, reach := range reaches {
if routed[port] {
continue
}
source, ok := FilterSource(reach)
if !ok {
return nil, fmt.Errorf("%s: %q is not a reach the filter can read", m.Module, reach)
}
if e == nil {
e = map[int]string{}
}
e[port] = source
}
return e, nil
}
// provisionsAs is the account that reads what the mesh writes for this provider: the one secret
// per consumer it must open to set that consumer's password (novox/hq issue 225).
//
@@ -87,6 +87,32 @@ func TestTheModuleAskingForTheRuleSetGetsEveryModulesPorts(t *testing.T) {
if !strings.Contains(content, "dport 5100 accept") || !strings.Contains(content, "dport 443 accept") {
t.Fatalf("the filter closes the port of a module left out of the declaration, which still runs:\n%s", content)
}
// Its per-node narrowing still holds (ADR 0046), and an exposure that cannot be read for it opens it no wider than
// the mesh, never at the manifest's default (review round 1).
for _, row := range []struct {
expose map[string]any
from string
}{
{map[string]any{"5100": mcat.FromMachine}, mcat.FromMachine},
{map[string]any{"9999": mcat.FromMachine}, mcat.FromMesh}, // a port it does not listen on: refused
} {
layer := with.Settings["distribution"][0]
layer.Placement = map[string]any{mcat.PortsSetting: map[string]any{"5000": 5100}, mcat.ExposeSetting: row.expose}
with.Settings = mcat.SettingsBy{"distribution": {layer}}
rules, err := Rules(r, with)
if err != nil {
t.Fatalf("expose %v: %v", row.expose, err)
}
var from []string
for _, rule := range rules {
if rule.Port == 5100 {
from = append(from, rule.From)
}
}
if len(from) != 1 || from[0] != row.from {
t.Errorf("expose %v: the left-out module's 5100 opens from %v, want %s", row.expose, from, row.from)
}
}
}
// The host does not sort, so the order the mesh writes is the order a machine applies.