a route composes its internal-network alias too, not only its public name
Every cutover done on novox tonight (drive, files, files-api, git, keycloak, umami) dropped the <label>.<node>.internal alias HAL always paired with the public hostname — found only when the operator tested it by hand. Not a security boundary (a predecessor proxy served both as a convenience, reaching a service over the VPN without a public TLS round trip, not as access control), so restoring it is composing the same convenience the same way the public name already is: <label> joined to the node's own private address (r.At), independently of whether a public domain exists to join the other half to. composeName's signature changes (publicDomain, internalDomain) but its shape does not — additive, label-gated, apex-aware, exactly mirroring the public half it already did. A contribution the mesh writes both names into is the entire fix; route-adapter and route-proxy pick up internal- name whenever they're updated to serve it, not before, so this alone changes nothing about what is live on any node yet.
This commit is contained in:
@@ -911,7 +911,7 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s contributing to %s: %w", m.Module, to, err)
|
return nil, fmt.Errorf("%s contributing to %s: %w", m.Module, to, err)
|
||||||
}
|
}
|
||||||
composeName(values, r.PublicDomain)
|
composeName(values, r.PublicDomain, r.At)
|
||||||
out[to] = append(out[to], Contribution{From: m.Module, Values: values})
|
out[to] = append(out[to], Contribution{From: m.Module, Values: values})
|
||||||
}
|
}
|
||||||
// Several contributions to one requirement (ADR 0094's sibling for `contributes`): an
|
// Several contributions to one requirement (ADR 0094's sibling for `contributes`): an
|
||||||
@@ -925,7 +925,7 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s contributing %s to %s: %w", m.Module, local, to, err)
|
return nil, fmt.Errorf("%s contributing %s to %s: %w", m.Module, local, to, err)
|
||||||
}
|
}
|
||||||
composeName(values, r.PublicDomain)
|
composeName(values, r.PublicDomain, r.At)
|
||||||
out[to] = append(out[to], Contribution{From: m.Module, Values: values})
|
out[to] = append(out[to], Contribution{From: m.Module, Values: values})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -933,23 +933,31 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// composeName joins a contribution's label with a node's public domain, in place (novox/hq ADR
|
// composeName joins a contribution's label with a node's public domain, and separately with its
|
||||||
// 0056).
|
// private one, in place (novox/hq ADR 0056).
|
||||||
//
|
//
|
||||||
// **The whole of what the mesh does with a route's name: join two given strings.** A contribution
|
// **The whole of what the mesh does with a route's name: join two given strings — twice.** A
|
||||||
// carries a `label` — the subdomain its operator chose — and the node carries its public domain;
|
// contribution carries a `label` — the subdomain its operator chose — and the node carries its
|
||||||
// the granted name is `<label>.<public-domain>` and the mesh interprets neither half. It runs on
|
// public domain and its own private-network address; the granted names are `<label>.<public-domain>`
|
||||||
// any contribution carrying a label, not only a route's, because the mesh does not know what a
|
// and `<label>.<internal-domain>`, and the mesh interprets none of the halves. It runs on any
|
||||||
|
// contribution carrying a label, not only a route's, because the mesh does not know what a
|
||||||
// provision means — a name it can compose from parts it was given is the point, whatever the
|
// provision means — a name it can compose from parts it was given is the point, whatever the
|
||||||
// provision is called.
|
// provision is called.
|
||||||
//
|
//
|
||||||
|
// **The internal name is not a security boundary.** A predecessor proxy that answered both a
|
||||||
|
// public and a private-network hostname for the same route did so as a convenience — reaching a
|
||||||
|
// service over the VPN without a public TLS round trip — not as an access control, and composing
|
||||||
|
// the same alias here restores that convenience rather than adding one. A route with no internal
|
||||||
|
// domain to compose against (a node not on the private network) gets no internal name, the same as
|
||||||
|
// it gets no public one with no public domain.
|
||||||
|
//
|
||||||
// **Additive, so an unmigrated catalogue still works.** A contribution that already carries a full
|
// **Additive, so an unmigrated catalogue still works.** A contribution that already carries a full
|
||||||
// `name` and no `label` is left exactly as it is: the catalogue can migrate module by module while
|
// `name` and no `label` is left exactly as it is: the catalogue can migrate module by module while
|
||||||
// the running mesh keeps serving the full names it has. And a labelled contribution on a node with
|
// the running mesh keeps serving the full names it has. And a labelled contribution on a node with
|
||||||
// no public domain composes nothing — there is nothing to join it to — which reads downstream as a
|
// no public domain composes nothing — there is nothing to join it to — which reads downstream as a
|
||||||
// route that named no host, the same as it would have before this existed.
|
// route that named no host, the same as it would have before this existed.
|
||||||
func composeName(values map[string]any, publicDomain string) {
|
func composeName(values map[string]any, publicDomain, internalDomain string) {
|
||||||
if values == nil || publicDomain == "" {
|
if values == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, already := values["name"]; already {
|
if _, already := values["name"]; already {
|
||||||
@@ -962,14 +970,25 @@ func composeName(values map[string]any, publicDomain string) {
|
|||||||
if !ok || strings.TrimSpace(label) == "" {
|
if !ok || strings.TrimSpace(label) == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(label) == "@" {
|
trimmed := strings.TrimSpace(label)
|
||||||
// The apex: a module served at the bare public domain, no subdomain — the zone-file
|
if trimmed == "@" {
|
||||||
// convention `@`. Composes to the domain itself, so a node's own site is a label like any
|
// The apex: a module served at the bare domain, no subdomain — the zone-file convention
|
||||||
// other rather than the one route that must still carry a full name.
|
// `@`. Composes to the domain itself, so a node's own site is a label like any other rather
|
||||||
values["name"] = publicDomain
|
// than the one route that must still carry a full name.
|
||||||
|
if publicDomain != "" {
|
||||||
|
values["name"] = publicDomain
|
||||||
|
}
|
||||||
|
if internalDomain != "" {
|
||||||
|
values["internal-name"] = internalDomain
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
values["name"] = strings.TrimSpace(label) + "." + publicDomain
|
if publicDomain != "" {
|
||||||
|
values["name"] = trimmed + "." + publicDomain
|
||||||
|
}
|
||||||
|
if internalDomain != "" {
|
||||||
|
values["internal-name"] = trimmed + "." + internalDomain
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// receivedFile is the file a provider is given its consumers' contributions in.
|
// receivedFile is the file a provider is given its consumers' contributions in.
|
||||||
|
|||||||
@@ -132,6 +132,72 @@ func TestALabelWithNoPublicDomainComposesNothing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withPrivateAddress is a workstation on the private network, at the given internal name — the
|
||||||
|
// same fact a route's own consumers already receive as `${bound:...:at}`.
|
||||||
|
func withPrivateAddress(at string) Node {
|
||||||
|
n := workstation()
|
||||||
|
n.At = at
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestALabelComposesWithTheNodesPrivateAddressToo(t *testing.T) {
|
||||||
|
// A predecessor proxy answered a route on both a public and a private-network hostname for the
|
||||||
|
// same convenience the mesh restores here: reaching a service over the VPN without a public TLS
|
||||||
|
// round trip. Composed independently of the public name, from the node's own `At`.
|
||||||
|
got, err := Resolve(shelf(proxy(), labelled("board", "git", 8080)),
|
||||||
|
[]string{"board"}, withPrivateAddress("anchor.internal"), World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
given := received(t, mustDeclare(t, got))
|
||||||
|
if given[0].Values["internal-name"] != "git.anchor.internal" {
|
||||||
|
t.Fatalf("the label did not compose with the private address: %v", given[0].Values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThePublicAndInternalNamesComposeIndependently(t *testing.T) {
|
||||||
|
// A node with both a public domain and a private address gets both names from one label; a
|
||||||
|
// node with only one of the two gets only the matching one — neither composition depends on
|
||||||
|
// the other being possible.
|
||||||
|
both := withPrivateAddress("anchor.internal")
|
||||||
|
both.PublicDomain = "example.tld"
|
||||||
|
got, err := Resolve(shelf(proxy(), labelled("board", "git", 8080)),
|
||||||
|
[]string{"board"}, both, World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
given := received(t, mustDeclare(t, got))
|
||||||
|
if given[0].Values["name"] != "git.example.tld" {
|
||||||
|
t.Fatalf("the public name did not compose alongside the internal one: %v", given[0].Values)
|
||||||
|
}
|
||||||
|
if given[0].Values["internal-name"] != "git.anchor.internal" {
|
||||||
|
t.Fatalf("the internal name did not compose alongside the public one: %v", given[0].Values)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicOnly, err := Resolve(shelf(proxy(), labelled("board", "git", 8080)),
|
||||||
|
[]string{"board"}, withDomain("example.tld"), World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
givenPublicOnly := received(t, mustDeclare(t, publicOnly))
|
||||||
|
if _, has := givenPublicOnly[0].Values["internal-name"]; has {
|
||||||
|
t.Fatalf("an internal name was composed with no private address to compose it from: %v",
|
||||||
|
givenPublicOnly[0].Values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTheApexLabelComposesToTheBarePrivateAddress(t *testing.T) {
|
||||||
|
got, err := Resolve(shelf(proxy(), labelled("board", "@", 4000)),
|
||||||
|
[]string{"board"}, withPrivateAddress("anchor.internal"), World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
given := received(t, mustDeclare(t, got))
|
||||||
|
if given[0].Values["internal-name"] != "anchor.internal" {
|
||||||
|
t.Fatalf("the apex label did not compose to the bare private address: %v", given[0].Values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestARoutedNameResolvesToTheServingNode(t *testing.T) {
|
func TestARoutedNameResolvesToTheServingNode(t *testing.T) {
|
||||||
// novox/hq ADR 0066 propagate: a granted route name is published into internal resolution,
|
// novox/hq ADR 0066 propagate: a granted route name is published into internal resolution,
|
||||||
// mapped to the node that serves it, alongside the `<node>.internal` names — so every
|
// mapped to the node that serves it, alongside the `<node>.internal` names — so every
|
||||||
|
|||||||
Reference in New Issue
Block a user