A machine with a VPN client that writes /etc/resolv.conf needs its domains routed to the VPN's servers while every other name still goes to the mesh's resolvers. node-resolver's holder does that on the machine, owns the resolver file there, and serves routes, route and unroute. The uplink's holder steps back from the file only where the resolver is held; every other machine composes as before.
125 lines
5.6 KiB
Go
125 lines
5.6 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
|
|
}
|
|
|
|
// rendersResolverFile says whether a module asks the mesh to render the machine's resolver file.
|
|
func rendersResolverFile(m Manifest) bool {
|
|
for _, f := range m.Facts {
|
|
if !f.Home && f.Path == ResolverFile {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ResolverFileOwner is the module that writes the machine's resolver file among the modules on one
|
|
// machine (novox/hq ADR 0247): the holder of node-resolver when one renders it, else nobody is named here
|
|
// and the file is whoever's it always was — the uplink holder's (ADR 0223).
|
|
func ResolverFileOwner(modules []Manifest) string {
|
|
for _, m := range modules {
|
|
if holdsSeat(m, ResolverSeat) && rendersResolverFile(m) {
|
|
return m.Module
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// stepsBack is the module as it composes on a machine whose resolver file is the node-resolver holder's:
|
|
// **the uplink holder's rendering of that file is left out**, and nothing else of it changes. Only the
|
|
// uplink's holder steps back — any other module rendering the file beside the resolver's is still two
|
|
// owners of one path, and is refused as before.
|
|
func stepsBack(m Manifest, modules []Manifest) Manifest {
|
|
if !holdsSeat(m, "node-uplink") || !rendersResolverFile(m) {
|
|
return m
|
|
}
|
|
owner := ResolverFileOwner(modules)
|
|
if owner == "" || owner == m.Module {
|
|
return m
|
|
}
|
|
facts := make(map[string]RosterFile, len(m.Facts))
|
|
for name, f := range m.Facts {
|
|
if !f.Home && f.Path == ResolverFile {
|
|
continue
|
|
}
|
|
facts[name] = f
|
|
}
|
|
m.Facts = facts
|
|
return m
|
|
}
|
|
|
|
// steppedBack is every module of one machine as it composes there (stepsBack, each).
|
|
func steppedBack(modules []Manifest) []Manifest {
|
|
if ResolverFileOwner(modules) == "" {
|
|
return modules
|
|
}
|
|
out := make([]Manifest, len(modules))
|
|
for i, m := range modules {
|
|
out[i] = stepsBack(m, modules)
|
|
}
|
|
return out
|
|
}
|