Files
mesh-catalog/modules/systemd-resolved/cmd/resolver-tools/main.go
T
jochen d53571213c
mesh/merge-gate fail: builds new: modules/systemd-resolved, sent nowhere; no bus step; a manifest the change touches fails the module check: modules/system…
mesh/repo-check fail: its merge-check.sh failed: long-running resources without health: 70
mesh/delivery superseded: a newer head of the same pull request
systemd-resolved: a machine's own resolver, routing a VPN's domains by link (hq ADR 0247)
Holds node-resolver and provides split-dns at the machine's reach, for a
machine whose VPN client pushes resolvers of its own. It writes the
resolver file naming the machine's private address, gives resolved the
mesh's resolvers as the default route, and serves routes, route and unroute
on the mesh and, over a root-only socket, on the machine. Its guard keeps an
outside write of the file for the module that handles it and puts the
module's file back: at once when taken, after 90 s otherwise, so a write
nothing declared to handle is still raised by the node-engine.
2026-10-07 21:28:11 +02:00

98 lines
3.7 KiB
Go

// systemd-resolved's tools bundle (novox/hq ADR 0247): the node-resolver seat's verbs, and the module's
// guard.
//
// Started with no arguments it is served by the node's runtime as the operator account, over MCP on stdio
// through the Go SDK (ADR 0188, ADR 0193): `routes`, `route` and `unroute` on the mesh, for the operator
// to read and correct. Started as `resolver-tools guard` it is the module's long-running process, as root:
// it keeps the machine's resolver file the module's own and serves the same verbs on the machine, to the
// modules there (guard.go). stdout is the MCP channel; everything else is said on stderr.
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// Seat is the role this module holds.
const Seat = "node-resolver"
func main() {
if len(os.Args) > 1 {
if os.Args[1] != "guard" || len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: resolver-tools [guard]")
os.Exit(2)
}
if err := guard(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
return
}
if err := stdio.Serve("", tools(ThisResolver(), History)); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func guard() error {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()
g := NewGuard()
if err := os.MkdirAll(g.Dir, 0o755); err != nil {
return err
}
g.writeHistory()
go g.Run(ctx)
return g.Serve(ctx, Socket)
}
func str(description string) map[string]any {
return map[string]any{"type": "string", "description": description}
}
func arg(a map[string]any, k string) string {
v, _ := a[k].(string)
return v
}
// verb is one of the seat's verbs: listed as `<seat>.<verb>`, so the runtime serves it on the seat's
// subject, as <node>/node-resolver.<verb>.
func verb(name, description string, input map[string]any, run func(a map[string]any) (any, error)) stdio.Tool {
return stdio.Tool{Name: Seat + "." + name, Description: description, Input: input, Run: run}
}
func tools(r *Resolver, history string) []stdio.Tool {
ctx := context.Background()
return []stdio.Tool{
verb("routes", "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, newest first: when, who wrote it as far as the file says, whether a "+
"module took it, and when the module's own file stood again. (r)",
nil, func(map[string]any) (any, error) {
routes, err := r.Routes(ctx)
if err != nil {
return nil, err
}
return map[string]any{"mesh": routes.Mesh, "links": routes.Links, "outside_writes": ReadHistory(history)}, nil
}),
verb("route", "Send these domains, and every name under them, to these servers over this link, and only "+
"them: the link never answers other 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. (a)",
map[string]any{"link": str("the network link the servers are reached over, by name"),
"domains": str("the domains to route there, separated by spaces or commas"),
"servers": str("the servers' addresses, separated by spaces or commas")},
func(a map[string]any) (any, error) {
return r.Route(ctx, arg(a, "link"), Split(a["domains"]), Split(a["servers"]))
}),
verb("unroute", "Take one link's route away: its domains go to the mesh's resolvers again. Nothing changes "+
"when the link has none. (a)",
map[string]any{"link": str("the network link, by name")},
func(a map[string]any) (any, error) { return r.Unroute(ctx, arg(a, "link")) }),
}
}