--- /dev/null
+// SPDX-License-Identifier: GPL-2.0-or-later
+// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
+'use strict';
+
+function mdns_data(model)
+{
+ return model.ubus.call("umdns", "browse", { array: true, address: false });
+}
+
+function refresh_timer(model)
+{
+ model.ubus.call("umdns", "update");
+
+ model.mdns.refresh_count++;
+ if (model.mdns.refresh_count < 3)
+ model.mdns.timer.set(500);
+
+ if (model.mdns.refresh_count < 2)
+ return;
+
+ let data = mdns_data(model);
+ for (let service_name, service_data in data) {
+ for (let host_name, host_data in service_data) {
+ let interface = host_data.iface;
+ if (!interface)
+ continue;
+ if (host_data.host)
+ continue;
+
+ let question = host_name + "." + service_name + ".local";
+ model.ubus.call("umdns", "query", { question, interface });
+ }
+ }
+}
+
+function refresh_start(model)
+{
+ model.mdns.refresh_count = 0;
+ model.mdns.timer ??= model.uloop.timer(500, () => refresh_timer(model));
+}
+
+function strip_local(name)
+{
+ if (substr(name, -6) == ".local")
+ name = substr(name, 0, -6);
+
+ return name;
+}
+
+function get_hosts(model)
+{
+ let data = model.ubus.call("umdns", "hosts", { array: true });
+ let ret = {};
+
+ for (let name, val in data) {
+ name = strip_local(name);
+ ret[lc(name)] = val;
+ }
+
+ return ret;
+}
+
+function get_host_names(model)
+{
+ let data = model.ubus.call("umdns", "hosts", { array: true });
+ let ret = {};
+
+ for (let name, val in data) {
+ name = strip_local(name);
+ ret[lc(name)] = name;
+ }
+
+ return ret;
+}
+
+function get_host_servicenames(model)
+{
+ let hosts = {};
+ let data = mdns_data(model);
+ for (let service_name, service_data in data) {
+ for (let host_name, host_data in service_data) {
+ let name = strip_local(host_data.host);
+
+ hosts[lc(name)] ??= [];
+ if (index(hosts[lc(name)], host_name) < 0)
+ push(hosts[lc(name)], host_name);
+ }
+ }
+ return hosts;
+}
+
+function get_service_hosts(model, name)
+{
+ name = lc(name);
+
+ let data = mdns_data(model);
+ for (let cur_name, hosts in data)
+ if (lc(cur_name) == name)
+ return hosts;
+}
+
+function get_host_services(model)
+{
+ let hosts = {};
+ let data = mdns_data(model);
+ for (let service_name, service_data in data) {
+ for (let host_name, host_data in service_data) {
+ host_data.name = host_name;
+ host_name = lc(strip_local(host_data.host));
+ hosts[host_name] ??= {};
+ hosts[host_name][service_name] = host_data;
+ }
+ }
+ return hosts;
+}
+
+function host_info(host)
+{
+ let ret = {};
+ if (host.ipv4)
+ ret.IPv4 = host.ipv4;
+ if (host.ipv6)
+ ret.IPv6 = host.ipv6;
+ return ret;
+}
+
+const host_arg = {
+ name: "host",
+ help: "host name",
+ type: "enum",
+ ignore_case: true,
+ value: () => values(get_host_names(model)),
+};
+
+const service_arg = {
+ name: "service",
+ help: "service name",
+ type: "enum",
+ ignore_case: true,
+ value: () => keys(mdns_data(model)),
+};
+
+function add_field(ret, name, val)
+{
+ if (val == null)
+ return;
+ if (type(ret) == "array")
+ push(ret, [ name, val ]);
+ else
+ ret[name] = val;
+}
+
+function service_info(data)
+{
+ let info = [];
+
+ add_field(info, "Name", data.name);
+ add_field(info, "Interface", data.iface);
+ add_field(info, "Port", data.port);
+ add_field(info, "Text", data.txt);
+
+ return info;
+}
+
+const MDNS = {
+ refresh: {
+ help: "Refresh service list by sending queries",
+ call: function(ctx, argv, named) {
+ refresh_start(model);
+ return ctx.ok("Querying hosts");
+ }
+ },
+ service: {
+ help: "Show service info",
+ args: [ service_arg ],
+ call: function (ctx, argv, named) {
+ let name = argv[0];
+ if (name != null) {
+ let data = get_service_hosts(model, name);
+ if (!data)
+ return ctx.not_found("Service not found: %s", name);
+
+ let ret = {};
+ for (let name, host in data) {
+ if (!host.host)
+ continue;
+ let host_name = strip_local(host.host);
+ host.name = name;
+ ret["Host " + host_name] = service_info(host);
+ }
+
+ return ctx.multi_table("Service " + name, ret);
+ }
+
+ let data = mdns_data(model);
+ let services = {};
+ for (let service_name, service_data in data) {
+ let hosts = [];
+ for (let name, host in service_data)
+ if (host.host)
+ push(hosts, `${name}(${strip_local(host.host)})`);
+ if (length(hosts))
+ services[service_name] = sort(hosts);
+ }
+
+ return ctx.table("Services", services);
+ }
+ },
+ host: {
+ help: "Host information",
+ args: [ host_arg ],
+ call: function (ctx, argv, named) {
+ let hosts_svc = get_host_services(model);
+ let hosts = get_hosts(model);
+ let host = argv[0];
+
+ if (host == null) {
+ let host_names = get_host_names(model);
+ let ret = {};
+
+ for (let lc_name, name in host_names) {
+ let data = hosts[lc_name];
+ if (!data)
+ continue;
+
+ let title = "Host " + name;
+ ret[title] = host_info(data);
+ let svc = hosts_svc[lc_name];
+ if (svc)
+ ret[title].services = keys(svc);
+ }
+ return ctx.multi_table("Hosts", ret);
+ }
+
+ let lc_host = lc(host);
+ let data = hosts[lc_host];
+ if (!data)
+ return ctx.not_found("Host not found: " + host);
+
+ let ret = {};
+ ret.Info = host_info(data);
+
+ for (let service_name, sdata in hosts_svc[lc_host])
+ ret["Service " + service_name] = service_info(sdata);
+
+ return ctx.multi_table("Host " + host, ret);
+ }
+ },
+};
+
+const Root = {
+ mdns: {
+ help: "Browse mdns hosts",
+ select_node: "MDNS",
+ select: function(ctx, argv) {
+ try {
+ refresh_start(model);
+ } catch (e) {
+ ctx.model.exception(e);
+ }
+ return true;
+ },
+ }
+};
+
+model.add_nodes({ Root, MDNS });
+model.mdns = {};