From d1f8ab86d15dcdfb9f39793dcb255278cab0ea6f Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 2 Oct 2026 11:53:16 +0200 Subject: [PATCH] unifi: list networks and set the DNS their DHCP hands out Which DNS server the home network's DHCP hands out could be changed only in the controller's own interface or by hand against its API (novox/hq issue 198). --- modules/unifi/client.ts | 24 +++++++++++++++++ modules/unifi/tools/index.ts | 51 ++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/modules/unifi/client.ts b/modules/unifi/client.ts index fda068c..89afa7a 100644 --- a/modules/unifi/client.ts +++ b/modules/unifi/client.ts @@ -23,6 +23,19 @@ export interface UnifiPortForward { site_id?: string; } +export interface UnifiNetwork { + _id: string; + name: string; + purpose: string; + ip_subnet?: string; + dhcpd_enabled?: boolean; + dhcpd_dns_enabled?: boolean; + dhcpd_dns_1?: string; + dhcpd_dns_2?: string; + dhcpd_dns_3?: string; + dhcpd_dns_4?: string; +} + export interface UnifiDevice { _id: string; name: string; @@ -232,6 +245,17 @@ export class UnifiApiClient { await this.request("DELETE", `/api/s/${this.site}/rest/portforward/${id}`); } + // --- Networks --- + + async listNetworks(): Promise { + return this.request("GET", `/api/s/${this.site}/rest/networkconf`); + } + + async updateNetwork(id: string, fields: Partial): Promise { + const result = await this.request("PUT", `/api/s/${this.site}/rest/networkconf/${id}`, fields); + return result[0]; + } + // --- Devices --- async listDevices(): Promise { diff --git a/modules/unifi/tools/index.ts b/modules/unifi/tools/index.ts index da26751..d890999 100644 --- a/modules/unifi/tools/index.ts +++ b/modules/unifi/tools/index.ts @@ -1,10 +1,23 @@ // unifi's tools — its own code (novox/hq ADR 0039), importing unifi's own client. They return // structured data; the mesh serves them through the sdk's tool harness. unifi is tools-only (no // events entrypoint): the controller does not push lifecycle events the mesh consumes, so this -// module reads its resources — port forwards, devices, clients — and exposes them, and stops there. +// module reads its resources — port forwards, networks, devices, clients — and exposes them, and stops there. import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; -import { UnifiApiClient, type UnifiPortForward } from "../client.js"; +import { UnifiApiClient, type UnifiNetwork, type UnifiPortForward } from "../client.js"; + +function summarizeNetwork(n: UnifiNetwork): Record { + const dns = [n.dhcpd_dns_1, n.dhcpd_dns_2, n.dhcpd_dns_3, n.dhcpd_dns_4].filter((s): s is string => !!s); + return { + id: n._id, + name: n.name, + purpose: n.purpose, + subnet: n.ip_subnet ?? null, + dhcp: n.dhcpd_enabled ?? null, + // What DHCP hands out as DNS: the listed servers when set, otherwise the gateway itself. + dhcp_dns: n.dhcpd_dns_enabled ? dns : "the gateway", + }; +} function summarizePortForward(r: UnifiPortForward): Record { return { @@ -88,6 +101,40 @@ export function getUnifiTools(unifi: UnifiApiClient): ToolDefinition[] { return { deleted: true, id: String(args.id) }; }, }, + { + name: "unifi_list_networks", + description: "List the networks the UniFi controller manages, with the DNS servers each one's DHCP hands out.", + input: {}, + run: async () => { + const nets = await unifi.listNetworks(); + return { count: nets.length, networks: nets.map(summarizeNetwork) }; + }, + }, + { + name: "unifi_set_network_dns", + description: + "Set the DNS servers a network's DHCP hands out to its devices (see unifi_list_networks for ids). " + + "Up to four addresses, comma-separated; \"gateway\" hands out the gateway itself. Devices pick it up when they renew.", + input: { + id: { type: "string", description: "the network id" }, + dns: { type: "string", description: "comma-separated DNS server addresses, or \"gateway\"" }, + }, + run: async (args) => { + const asked = String(args.dns ?? "").trim(); + if (!asked) return { updated: false, reason: "say dns: addresses, or \"gateway\"" }; + const servers = asked === "gateway" ? [] : asked.split(",").map((s) => s.trim()).filter(Boolean); + if (servers.length > 4) return { updated: false, reason: "DHCP hands out at most four DNS servers" }; + const fields: Partial = { + dhcpd_dns_enabled: servers.length > 0, + dhcpd_dns_1: servers[0] ?? "", + dhcpd_dns_2: servers[1] ?? "", + dhcpd_dns_3: servers[2] ?? "", + dhcpd_dns_4: servers[3] ?? "", + }; + const net = await unifi.updateNetwork(String(args.id), fields); + return { updated: summarizeNetwork(net) }; + }, + }, { name: "unifi_list_devices", description: "List the network devices (APs, switches, gateways) the UniFi controller manages.",