// 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 `.`, so the runtime serves it on the seat's // subject, as /node-resolver.. 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")) }), } }