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 }