Which module owns /etc/hosts or /etc/resolv.conf on a machine was answered only by reading every manifest, and a module not holding node-hostname could declare /etc/hosts where nobody held it. hq ADR 0287 makes the paths part of the seat's definition: node-hostname names /etc/hosts and /etc/hostname, node-uplink names /etc/resolv.conf stepping back for node-resolver, node-resolver names /etc/resolv.conf. CanHold refuses a holder that does not declare every path (registration and handover), registration refuses a module declaring a seat's path while claiming none of the seats naming it, a module's own seat names paths under the same rules (node only, literal, not named by another seat), composition refuses a generated resource on a seat's path, the uplink's step-back is read from the seat instead of a special case, and `seats` shows each seat's paths and who owns each on each machine.
123 lines
5.5 KiB
Go
123 lines
5.5 KiB
Go
package catalogue
|
|
|
|
// The machine's own resolver (novox/hq ADR 0247).
|
|
//
|
|
// **Most machines have none.** Every machine lists the mesh's resolvers in /etc/resolv.conf and nothing
|
|
// else, and the module holding its uplink writes that file (ADR 0223). A machine with a VPN client that
|
|
// pushes its own resolvers for its own domains needs a third answer: those domains to the VPN's servers,
|
|
// over the VPN's link, and every other name to the mesh's resolvers as before. One file cannot list both
|
|
// sets of servers — musl takes the first reply, glibc the first server's "no such name" — so the domains
|
|
// are routed by a resolver on the machine itself: the `node-resolver` seat's holder.
|
|
//
|
|
// **Where it is held, it owns the resolver file.** The file then names the machine's own resolver, which
|
|
// routes; the uplink's holder steps back from writing it on that machine, by the rule below and not by
|
|
// two modules writing one path. There are two uplink holders (NetworkManager's and systemd-networkd's),
|
|
// and the resolver is its own module so it is written once, not once in each.
|
|
//
|
|
// **It knows nothing of any VPN.** Its verbs route a set of domains to a set of servers over one link,
|
|
// list what is routed and remove a route. A module wrapping a VPN client that writes /etc/resolv.conf
|
|
// itself carries its own adapter and calls those verbs; a VPN that tells systemd-resolved its link's DNS
|
|
// itself needs none.
|
|
|
|
// ResolverSeat is the machine's own resolver (novox/hq ADR 0247).
|
|
const ResolverSeat = "node-resolver"
|
|
|
|
// ResolverFile is the machine's resolver file: the uplink holder's, or the node-resolver holder's where
|
|
// one is held.
|
|
const ResolverFile = "/etc/resolv.conf"
|
|
|
|
// resolverVerbs is the contract every holder of node-resolver serves (novox/hq ADR 0247): what is routed
|
|
// where, route a set of domains, and take a route away. The same verbs are served on the machine itself to
|
|
// the modules there, so what a VPN pushed never crosses the bus to be routed; on the mesh they are the
|
|
// operator's way to read and correct the same.
|
|
func resolverVerbs() []Verb {
|
|
return []Verb{
|
|
{Name: "routes", Description: "What this machine's own resolver sends where: the mesh's resolvers, " +
|
|
"which answer every name not routed elsewhere, and each link given servers of its own with the " +
|
|
"domains routed to them. Also the resolver file's outside writes it kept, newest first: when, who " +
|
|
"wrote it as far as the file says, whether a module took it, and when the resolver's own file was " +
|
|
"put back.",
|
|
Input: schema(map[string]string{}, nil),
|
|
Replaces: []string{"resolvectl status", "resolvectl dns", "resolvectl domain"}},
|
|
{Name: "route", Description: "Send these domains, and every name under them, to these servers over " +
|
|
"this link — and only them: the link is never the machine's default route for names, and the " +
|
|
"mesh's own domain is refused. Replaces whatever the link was given before. A link that goes away " +
|
|
"takes its route with it.",
|
|
Input: schema(map[string]string{
|
|
"link": "the network link the servers are reached over, by name (a VPN's tunnel interface)",
|
|
"domains": "the domains to route there, separated by spaces or commas",
|
|
"servers": "the servers' addresses, separated by spaces or commas",
|
|
}, []string{"link", "domains", "servers"}),
|
|
Replaces: []string{"resolvectl dns", "resolvectl domain", "resolvectl default-route"}},
|
|
{Name: "unroute", Description: "Take one link's route away: its domains go back to the mesh's " +
|
|
"resolvers. Nothing changes when the link has none.",
|
|
Input: schema(map[string]string{"link": "the network link, by name"}, []string{"link"}),
|
|
Replaces: []string{"resolvectl revert"}},
|
|
}
|
|
}
|
|
|
|
// holdsSeat says whether a module claims a seat, by its current name.
|
|
func holdsSeat(m Manifest, seat string) bool {
|
|
for _, c := range m.Claims {
|
|
if canonicalSeat(c.Name) == seat {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// stepsBack is the module as it composes among the modules of one machine (novox/hq ADR 0287, ADR 0247):
|
|
// **where it holds a seat that names a path stepping back for another seat, and that other seat's holder
|
|
// is on the machine and declares the path, its own declaration of the path is left out**, and nothing else
|
|
// of it changes. The stepping back is the seat's definition — node-uplink stepping back from the resolver
|
|
// file for node-resolver — so any other module declaring the path beside the owner's is still two owners
|
|
// of one path, and is refused as before.
|
|
func stepsBack(m Manifest, modules []Manifest) Manifest {
|
|
drop := map[string]bool{}
|
|
for _, c := range m.Claims {
|
|
seat, known := SeatNamed(c.Name)
|
|
if !known {
|
|
continue
|
|
}
|
|
for _, p := range seat.Paths {
|
|
if p.StepsBackFor == "" {
|
|
continue
|
|
}
|
|
for _, o := range modules {
|
|
if o.Module != m.Module && holdsSeat(o, p.StepsBackFor) && declaresPath(o, p.Path) {
|
|
drop[p.Path] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(drop) == 0 {
|
|
return m
|
|
}
|
|
facts := make(map[string]RosterFile, len(m.Facts))
|
|
for name, f := range m.Facts {
|
|
if !f.Home && drop[f.Path] {
|
|
continue
|
|
}
|
|
facts[name] = f
|
|
}
|
|
m.Facts = facts
|
|
var resources []map[string]any
|
|
for _, r := range m.Resources {
|
|
if p, ok := r["path"].(string); ok && drop[p] {
|
|
continue
|
|
}
|
|
resources = append(resources, r)
|
|
}
|
|
m.Resources = resources
|
|
return m
|
|
}
|
|
|
|
// steppedBack is every module of one machine as it composes there (stepsBack, each).
|
|
func steppedBack(modules []Manifest) []Manifest {
|
|
out := make([]Manifest, len(modules))
|
|
for i, m := range modules {
|
|
out[i] = stepsBack(m, modules)
|
|
}
|
|
return out
|
|
}
|