luci-app-unbound: add LuCI for forward stub and auth zone clauses 2010/head
authorEric Luehrsen <ericluehrsen@gmail.com>
Thu, 26 Jul 2018 06:05:33 +0000 (02:05 -0400)
committerEric Luehrsen <ericluehrsen@gmail.com>
Mon, 30 Jul 2018 03:20:35 +0000 (23:20 -0400)
With growing interest, DNS over TLS can be setup in Unbounds foward-zone:
clause. New section 'zone' is available for forward-, stub-, and auth-
zone cluses. This LuCI application will show the 'zone' section and
permit changing 'enabled' and 'fallback' options. Detailed changes to
'zone' secitons will need to use the Edit:UCI tab (text editor).

Signed-off-by: Eric Luehrsen <ericluehrsen@gmail.com>
applications/luci-app-unbound/luasrc/controller/unbound.lua
applications/luci-app-unbound/luasrc/model/cbi/unbound/configure.lua
applications/luci-app-unbound/luasrc/model/cbi/unbound/extended.lua
applications/luci-app-unbound/luasrc/model/cbi/unbound/manual.lua
applications/luci-app-unbound/luasrc/model/cbi/unbound/server.lua
applications/luci-app-unbound/luasrc/model/cbi/unbound/uciedit.lua [new file with mode: 0644]
applications/luci-app-unbound/luasrc/model/cbi/unbound/zones.lua [new file with mode: 0644]

index 730ca724a4316d75a42956b4651add2c540c1f00..ea3d26b9197ed51dc9e3f66452f1fbb49bc02171 100644 (file)
 -- Copyright 2008 Steven Barth <steven@midlink.org>
 -- Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
--- Copyright 2017 Eric Luehrsen <ericluehrsen@hotmail.com>
+-- Copyright 2017 Eric Luehrsen <ericluehrsen@gmail.com>
 -- Licensed to the public under the Apache License 2.0.
 
 module("luci.controller.unbound", package.seeall)
 
 
 function index()
-  local ucl = luci.model.uci.cursor()
-  local valexp = ucl:get_first("unbound", "unbound", "extended_luci")
-  local valman = ucl:get_first("unbound", "unbound", "manual_conf")
+    local fs = require "nixio.fs"
+    local ucl = luci.model.uci.cursor()
+    local valman = ucl:get_first("unbound", "unbound", "manual_conf")
 
 
-  if not nixio.fs.access("/etc/config/unbound") then
-    return
-  end
+    if not fs.access("/etc/config/unbound") then
+        return
+    end
 
 
-  if valexp == "1" then
     -- Expanded View
-    entry({"admin", "services", "unbound"}, firstchild(), _("Recursive DNS")).dependent = false
+    entry({"admin", "services", "unbound"},
+        firstchild(), _("Recursive DNS")).dependent = false
 
     -- UCI Tab(s)
-    entry({"admin", "services", "unbound", "configure"}, cbi("unbound/configure"), _("Settings"), 10)
+    entry({"admin", "services", "unbound", "configure"},
+        cbi("unbound/configure"), _("Unbound"), 10)
+
+
+    if (valman == "0") then
+        entry({"admin", "services", "unbound", "zones"},
+            cbi("unbound/zones"), _("Zones"), 15)
+    end
+
 
     -- Status Tab(s)
-    entry({"admin", "services", "unbound", "status"}, firstchild(), _("Status"), 20)
-    entry({"admin", "services", "unbound", "status", "syslog"}, call("QuerySysLog"), _("Log"), 50).leaf = true
+    entry({"admin", "services", "unbound", "status"},
+        firstchild(), _("Status"), 20)
+
+    entry({"admin", "services", "unbound", "status", "syslog"},
+        call("QuerySysLog"), _("Log"), 50).leaf = true
+
+
+    if fs.access("/usr/sbin/unbound-control") then
+        -- Require unbound-control to execute
+        entry({"admin", "services", "unbound", "status", "statistics"},
+            call("QueryStatistics"), _("Statistics"), 10).leaf = true
 
+        entry({"admin", "services", "unbound", "status", "localdata"},
+            call("QueryLocalData"), _("Local Data"), 20).leaf = true
 
-    if nixio.fs.access("/usr/sbin/unbound-control") then
-      -- Require unbound-control to execute
-      entry({"admin", "services", "unbound", "status", "statistics"}, call("QueryStatistics"), _("Statistics"), 10).leaf = true
-      entry({"admin", "services", "unbound", "status", "localdata"}, call("QueryLocalData"), _("Local Data"), 20).leaf = true
-      entry({"admin", "services", "unbound", "status", "localzone"}, call("QueryLocalZone"), _("Local Zones"), 30).leaf = true
+        entry({"admin", "services", "unbound", "status", "localzone"},
+            call("QueryLocalZone"), _("Local Zones"), 30).leaf = true
 
     else
-      entry({"admin", "services", "unbound", "status", "statistics"}, call("ShowEmpty"), _("Statistics"), 10).leaf = true
+        entry({"admin", "services", "unbound", "status", "statistics"},
+            call("ShowEmpty"), _("Statistics"), 10).leaf = true
     end
 
 
     -- Raw File Tab(s)
-    entry({"admin", "services", "unbound", "files"}, firstchild(), _("Files"), 30)
+    entry({"admin", "services", "unbound", "files"},
+        firstchild(), _("Files"), 30)
 
 
-    if valman ~= "1" then
-      entry({"admin", "services", "unbound", "files", "base"}, call("ShowUnboundConf"), _("UCI: Unbound"), 10).leaf = true
+    if (valman == "0") then
+        entry({"admin", "services", "unbound", "files", "uci"},
+            form("unbound/uciedit"), _("Edit: UCI"), 5).leaf = true
+
+        entry({"admin", "services", "unbound", "files", "base"},
+            call("ShowUnboundConf"), _("Show: Unbound"), 10).leaf = true
+
     else
-      entry({"admin", "services", "unbound", "files", "base"}, form("unbound/manual"), _("Edit: Unbound"), 10).leaf = true
+        entry({"admin", "services", "unbound", "files", "base"},
+            form("unbound/manual"), _("Edit: Unbound"), 10).leaf = true
     end
 
 
-    entry({"admin", "services", "unbound", "files", "server"}, form("unbound/server"), _("Edit: Server"), 20).leaf = true
-    entry({"admin", "services", "unbound", "files", "extended"}, form("unbound/extended"), _("Edit: Extended"), 30).leaf = true
+    entry({"admin", "services", "unbound", "files", "server"},
+        form("unbound/server"), _("Edit: Server"), 20).leaf = true
 
+    entry({"admin", "services", "unbound", "files", "extended"},
+        form("unbound/extended"), _("Edit: Extended"), 30).leaf = true
 
-    if nixio.fs.access("/var/lib/unbound/unbound_dhcp.conf") then
-      entry({"admin", "services", "unbound", "files", "dhcp"}, call("ShowDHCPConf"), _("Include: DHCP"), 40).leaf = true
+
+    if fs.access("/var/lib/unbound/dhcp.conf") then
+        entry({"admin", "services", "unbound", "files", "dhcp"},
+            call("ShowDHCPConf"), _("Show: DHCP"), 40).leaf = true
     end
 
 
-    if nixio.fs.access("/var/lib/unbound/adb_list.overall") then
-      entry({"admin", "services", "unbound", "files", "adblock"}, call("ShowAdblock"), _("Include: Adblock"), 50).leaf = true
+    if fs.access("/var/lib/unbound/adb_list.overall") then
+        entry({"admin", "services", "unbound", "files", "adblock"},
+            call("ShowAdblock"), _("Show: Adblock"), 50).leaf = true
     end
-
-  else
-    -- Simple View to UCI only
-    entry({"admin", "services", "unbound"}, cbi("unbound/configure"), _("Recursive DNS")).dependent = false
-  end
 end
 
 
 function ShowEmpty()
-  local lclhead = "Unbound Control"
-  local lcldesc = luci.i18n.translate("This could display more statistics with the unbound-control package.")
-  luci.template.render("unbound/show-empty", {heading = lclhead, description = lcldesc})
+    local lclhead = "Unbound Control"
+    local lcldesc = luci.i18n.translate(
+        "This could display more statistics with the unbound-control package.")
+
+    luci.template.render("unbound/show-empty",
+        {heading = lclhead, description = lcldesc})
 end
 
 
 function QuerySysLog()
-  local lclhead = "System Log"
-  local lcldata = luci.util.exec("logread | grep -i unbound")
-  local lcldesc = luci.i18n.translate("This shows syslog filtered for events involving Unbound.")
-  luci.template.render("unbound/show-textbox", {heading = lclhead, description = lcldesc, content = lcldata})
+    local lcldata = luci.util.exec("logread | grep -i unbound")
+    local lcldesc = luci.i18n.translate(
+        "This shows syslog filtered for events involving Unbound.")
+
+    luci.template.render("unbound/show-textbox",
+        {heading = "", description = lcldesc, content = lcldata})
 end
 
 
 function QueryStatistics()
-  local lclhead = "Unbound Control Stats"
-  local lcldata = luci.util.exec("unbound-control -c /var/lib/unbound/unbound.conf stats_noreset")
-  local lcldesc = luci.i18n.translate("This shows some performance statistics tracked by Unbound.")
-  luci.template.render("unbound/show-textbox", {heading = lclhead, description = lcldesc, content = lcldata})
+    local lcldata = luci.util.exec(
+        "unbound-control -c /var/lib/unbound/unbound.conf stats_noreset")
+
+    local lcldesc = luci.i18n.translate(
+        "This shows Unbound self reported performance statistics.")
+
+    luci.template.render("unbound/show-textbox",
+        {heading = "", description = lcldesc, content = lcldata})
 end
 
 
 function QueryLocalData()
-  local lclhead = "Unbound Control Local Data"
-  local lcldata = luci.util.exec("unbound-control -c /var/lib/unbound/unbound.conf list_local_data")
-  local lcldesc = luci.i18n.translate("This shows local host records that shortcut recursion.")
-  luci.template.render("unbound/show-textbox", {heading = lclhead, description = lcldesc, content = lcldata})
+    local lcldata = luci.util.exec(
+        "unbound-control -c /var/lib/unbound/unbound.conf list_local_data")
+
+    local lcldesc = luci.i18n.translate(
+        "This shows Unbound 'local-data:' entries from default, .conf, or control.")
+
+    luci.template.render("unbound/show-textbox",
+        {heading = "", description = lcldesc, content = lcldata})
 end
 
 
 function QueryLocalZone()
-  local lclhead = "Unbound Control Local Zones"
-  local lcldata = luci.util.exec("unbound-control -c /var/lib/unbound/unbound.conf list_local_zones")
-  local lcldesc = luci.i18n.translate("This shows local zone definitions that affect recursion routing or processing. ")
-  luci.template.render("unbound/show-textbox", {heading = lclhead, description = lcldesc, content = lcldata})
+    local lcldata = luci.util.exec(
+        "unbound-control -c /var/lib/unbound/unbound.conf list_local_zones")
+
+    local lcldesc = luci.i18n.translate(
+        "This shows Unbound 'local-zone:' entries from default, .conf, or control.")
+
+    luci.template.render("unbound/show-textbox",
+        {heading = "", description = lcldesc, content = lcldata})
 end
 
 
 function ShowUnboundConf()
-  local unboundfile = "/var/lib/unbound/unbound.conf"
-  local lclhead = "Unbound Conf"
-  local lcldata = nixio.fs.readfile(unboundfile)
-  local lcldesc = luci.i18n.translate("This shows configuration generated by UCI:")
-  lcldesc = lcldesc .. " (" .. unboundfile .. ")"
-  luci.template.render("unbound/show-textbox", {heading = lclhead, description = lcldesc, content = lcldata})
+    local unboundfile = "/var/lib/unbound/unbound.conf"
+    local lcldata = nixio.fs.readfile(unboundfile)
+    local lcldesc = luci.i18n.translate(
+        "This shows '" .. unboundfile .. "' generated from UCI configuration.")
+
+    luci.template.render("unbound/show-textbox",
+        {heading = "", description = lcldesc, content = lcldata})
 end
 
 
 function ShowDHCPConf()
-  local dhcpfile = "/var/lib/unbound/unbound_dhcp.conf"
-  local lclhead = "DHCP Conf"
-  local lcldata = nixio.fs.readfile(dhcpfile)
-  local lcldesc = luci.i18n.translate("This shows LAN hosts added by DHCP hook scripts:")
-  lcldesc = lcldesc .. " (" .. dhcpfile .. ")"
-  luci.template.render("unbound/show-textbox", {heading = lclhead, description = lcldesc, content = lcldata})
+    local dhcpfile = "/var/lib/unbound/dhcp.conf"
+    local lcldata = nixio.fs.readfile(dhcpfile)
+    local lcldesc = luci.i18n.translate(
+        "This shows '" .. dhcpfile .. "' list of hosts from DHCP hook scripts.")
+
+    luci.template.render("unbound/show-textbox",
+        {heading = "", description = lcldesc, content = lcldata})
 end
 
 
 function ShowAdblock()
-  local adblockfile = "/var/lib/unbound/adb_list.overall"
-  local lclhead = "Adblock Conf"
-  local lcldata, lcldesc
-
-
-  if nixio.fs.stat(adblockfile).size > 262144 then
-    lcldesc = luci.i18n.translate("Adblock domain list is too large for LuCI:")
-    lcldesc = lcldesc .. " (" .. adblockfile .. ")"
-    luci.template.render("unbound/show-empty", {heading = lclhead, description = lcldesc})
-
-  else
-    lcldata = nixio.fs.readfile(adblockfile)
-    lcldesc = luci.i18n.translate("This shows blocked domains provided by Adblock scripts:")
-    lcldesc = lcldesc .. " (" .. adblockfile .. ")"
-    luci.template.render("unbound/show-textbox", {heading = lclhead, description = lcldesc, content = lcldata})
-  end
+    local fs = require "nixio.fs"
+    local tp = require "luci.template"
+    local tr = require "luci.i18n"
+    local adblockfile = "/var/lib/unbound/adb_list.overall"
+    local lcldata, lcldesc
+
+
+    if fs.stat(adblockfile).size > 262144 then
+        lcldesc = tr.translate(
+            "Adblock domain list '" .. adblockfile .. "' is too large for LuCI.")
+
+        tp.render("unbound/show-empty",
+            {heading = "", description = lcldesc})
+
+    else
+        lcldata = fs.readfile(adblockfile)
+        lcldesc = tr.translate(
+            "This shows '" .. adblockfile .. "' list of adblock domains." )
+
+        tp.render("unbound/show-textbox",
+            {heading = "", description = lcldesc, content = lcldata})
+    end
 end
 
index 256bbb83925acb776caa35a08175326a51c444ae..f665a2c9da16b8252817cf38e1806d3642518eca 100644 (file)
@@ -1,5 +1,5 @@
 -- Copyright 2008 Steven Barth <steven@midlink.org>
--- Copyright 2016 Eric Luehrsen <ericluehrsen@hotmail.com>
+-- Copyright 2016 Eric Luehrsen <ericluehrsen@gmail.com>
 -- Copyright 2016 Dan Luedtke <mail@danrl.com>
 -- Licensed to the public under the Apache License 2.0.
 
@@ -9,307 +9,312 @@ local rlh, rpv, vld, nvd, eds, prt, tlm
 local ctl, dlk, dom, dty, lfq, wfq, exa
 local dp6, d64, pfx, qry, qrs
 local pro, tgr, rsc, rsn, ag2, stt
-local rpn, din, dfw, ath
+local rpn, din, ath
+
+local ut = require "luci.util"
+local sy = require "luci.sys"
+local ht = require "luci.http"
+local ds = require "luci.dispatcher"
 local ucl = luci.model.uci.cursor()
 local valman = ucl:get_first("unbound", "unbound", "manual_conf")
 
 m1 = Map("unbound")
+s1 = m1:section(TypedSection, "unbound", translate("DNS Resolver"),
+    translatef("Unbound <a href=\"%s\" target=\"_blank\">(NLnet Labs)</a>"
+    .. " is a validating, recursive, and caching DNS resolver.",
+    "https://www.unbound.net/"))
 
-s1 = m1:section(TypedSection, "unbound")
 s1.addremove = false
 s1.anonymous = true
 
 --LuCI, Unbound, or Not
-s1:tab("basic", translate("Basic"),
-  translatef("<h3>Unbound Basic Settings</h3>\n"
-  .. "<a href=\"%s\" target=\"_blank\">Unbound (link)</a>"
-  .. " is a validating, recursive, and caching DNS resolver. "
-  .. "UCI documentation can be found on "
-  .. "<a href=\"%s\" target=\"_blank\">github (link)</a>.",
-  "https://www.unbound.net/",
-  "https://github.com/openwrt/packages/blob/master/net/unbound/files/README.md"))
-
-
-if valman ~= "1" then
-  -- Not in manual configuration mode; show UCI
-  s1:tab("advanced", translate("Advanced"),
-    translatef("<h3>Unbound Advanced Settings</h3>\n"
-    .. "Domain manipulation, lookup protection, and workarounds for "
-    .. "<a href=\"%s\" target=\"_blank\">Unbound </a>"
-    .. " DNS resolver.", "https://www.unbound.net/"))
-
-  s1:tab("DHCP", translate("DHCP"),
-    translatef("<h3>Unbound DHCP Settings</h3>\n"
-    .. "Link your DHCP server to "
-    .. "<a href=\"%s\" target=\"_blank\">Unbound </a>"
-    .. " DNS resolver.", "https://www.unbound.net/ "))
-
-  s1:tab("resource", translate("Resource"),
-    translatef("<h3>Unbound Resource Settings</h3>\n"
-    .. "Memory and protocol setttings for "
-    .. "<a href=\"%s\" target=\"_blank\">Unbound </a>"
-    .. " DNS resolver.", "https://www.unbound.net/"))
-end
+s1:tab("basic", translate("Basic"))
 
 
-s1:tab("trigger", translate("Trigger"),
-    translatef("<h3>Unbound Event Trigger Settings</h3>\n"
-    .. "Start, reload, and save RFC5011 DNSKEY records for "
-    .. "<a href=\"%s\" target=\"_blank\">Unbound </a>"
-    .. " DNS resolver.", "https://www.unbound.net/"))
+if (valman == "0") then
+    -- Not in manual configuration mode; show UCI
+    s1:tab("advanced", translate("Advanced"))
+    s1:tab("DHCP", translate("DHCP"))
+    s1:tab("resource", translate("Resource"))
+end
 
 
 --Basic Tab, unconditional pieces
 ena = s1:taboption("basic", Flag, "enabled", translate("Enable Unbound:"),
-  translate("Enable the initialization scripts for Unbound"))
+    translate("Enable the initialization scripts for Unbound"))
 ena.rmempty = false
 
 mcf = s1:taboption("basic", Flag, "manual_conf", translate("Manual Conf:"),
-  translate("Skip UCI and use /etc/unbound/unbound.conf"))
+    translate("Skip UCI and use /etc/unbound/unbound.conf"))
 mcf.rmempty = false
 
-lci = s1:taboption("basic", Flag, "extended_luci", translate("Extended Tabs:"),
-  translate("See detailed tabs for statistics, debug, and manual configuration"))
-lci.rmempty = false
-
-
-if valman ~= "1" then
-  -- Not in manual configuration mode; show UCI
-  --Basic Tab
-  lsv = s1:taboption("basic", Flag, "localservice", translate("Local Service:"),
-    translate("Accept queries only from local subnets"))
-  lsv.rmempty = false
-
-  vld = s1:taboption("basic", Flag, "validator", translate("Enable DNSSEC:"),
-    translate("Enable the DNSSEC validator module"))
-  vld.rmempty = false
-
-  nvd = s1:taboption("basic", Flag, "validator_ntp", translate("DNSSEC NTP Fix:"),
-    translate("Break the loop where DNSSEC needs NTP and NTP needs DNS"))
-  nvd.rmempty = false
-  nvd:depends({ validator = true })
-
-  d64 = s1:taboption("basic", Flag, "dns64", translate("Enable DNS64:"),
-    translate("Enable the DNS64 module"))
-  d64.rmempty = false
-
-  pfx = s1:taboption("basic", Value, "dns64_prefix", translate("DNS64 Prefix:"),
-    translate("Prefix for generated DNS64 addresses"))
-  pfx.datatype = "ip6addr"
-  pfx.placeholder = "64:ff9b::/96"
-  pfx.optional = true
-  pfx:depends({ dns64 = true })
-
-  prt = s1:taboption("basic", Value, "listen_port", translate("Listening Port:"),
-    translate("Choose Unbounds listening port"))
-  prt.datatype = "port"
-  prt.rmempty = false
-
-  --Avanced Tab
-  din = s1:taboption("advanced", DynamicList, "domain_insecure",
-    translate("Domain Insecure:"),
-    translate("List domains to bypass checks of DNSSEC"))
-  din:depends({ validator = true })
-
-  dfw = s1:taboption("advanced", DynamicList, "domain_forward",
-    translate("Domain Forward:"),
-    translate("List domains to simply forward to stub resolvers in /tmp/resolve.auto"))
-
-  rlh = s1:taboption("advanced", Flag, "rebind_localhost", translate("Filter Localhost Rebind:"),
-    translate("Protect against upstream response of 127.0.0.0/8"))
-  rlh.rmempty = false
-
-  rpv = s1:taboption("advanced", ListValue, "rebind_protection", translate("Filter Private Rebind:"),
-    translate("Protect against upstream responses within local subnets"))
-  rpv:value("0", translate("No Filter"))
-  rpv:value("1", translate("Filter RFC1918/4193"))
-  rpv:value("2", translate("Filter Entire Subnet"))
-  rpv.rmempty = false
-
-  rpn = s1:taboption("advanced", Value, "rebind_interface", translate("Rebind Network Filter:"),
-    translate("Network subnets to filter from upstream responses"))
-  rpn.template = "cbi/network_netlist"
-  rpn.widget = "checkbox"
-  rpn.rmempty = true
-  rpn.cast = "string"
-  rpn.nocreate = true
-  rpn:depends({ rebind_protection = 2 })
-  rpn:depends({ rebind_protection = 3 })
-
-  --DHCP Tab
-  dlk = s1:taboption("DHCP", ListValue, "dhcp_link", translate("DHCP Link:"),
-    translate("Link to supported programs to load DHCP into DNS"))
-  dlk:value("none", translate("No Link"))
-  dlk:value("dnsmasq", "dnsmasq")
-  dlk:value("odhcpd", "odhcpd")
-  dlk.rmempty = false
-
-  dp6 = s1:taboption("DHCP", Flag, "dhcp4_slaac6", translate("DHCPv4 to SLAAC:"),
-    translate("Use DHCPv4 MAC to discover IP6 hosts SLAAC (EUI64)"))
-  dp6.rmempty = false
-  dp6:depends({ dhcp_link = "odhcpd" })
-
-  dom = s1:taboption("DHCP", Value, "domain", translate("Local Domain:"),
-    translate("Domain suffix for this router and DHCP clients"))
-  dom.placeholder = "lan"
-  dom:depends({ dhcp_link = "none" })
-  dom:depends({ dhcp_link = "odhcpd" })
-
-  dty = s1:taboption("DHCP", ListValue, "domain_type", translate("Local Domain Type:"),
-    translate("How to treat queries of this local domain"))
-  dty:value("deny", translate("Ignored"))
-  dty:value("refuse", translate("Refused"))
-  dty:value("static", translate("Only Local"))
-  dty:value("transparent", translate("Also Forwarded"))
-  dty:depends({ dhcp_link = "none" })
-  dty:depends({ dhcp_link = "odhcpd" })
-
-  lfq = s1:taboption("DHCP", ListValue, "add_local_fqdn", translate("LAN DNS:"),
-    translate("How to enter the LAN or local network router in DNS"))
-  lfq:value("0", translate("No Entry"))
-  lfq:value("1", translate("Hostname, Primary Address"))
-  lfq:value("2", translate("Hostname, All Addresses"))
-  lfq:value("3", translate("Host FQDN, All Addresses"))
-  lfq:value("4", translate("Interface FQDN, All Addresses"))
-  lfq:depends({ dhcp_link = "none" })
-  lfq:depends({ dhcp_link = "odhcpd" })
-
-  wfq = s1:taboption("DHCP", ListValue, "add_wan_fqdn", translate("WAN DNS:"),
-    translate("Override the WAN side router entry in DNS"))
-  wfq:value("0", translate("Use Upstream"))
-  wfq:value("1", translate("Hostname, Primary Address"))
-  wfq:value("2", translate("Hostname, All Addresses"))
-  wfq:value("3", translate("Host FQDN, All Addresses"))
-  wfq:value("4", translate("Interface FQDN, All Addresses"))
-  wfq:depends({ dhcp_link = "none" })
-  wfq:depends({ dhcp_link = "odhcpd" })
-
-  exa = s1:taboption("DHCP", ListValue, "add_extra_dns", translate("Extra DNS:"),
-    translate("Use extra DNS entries found in /etc/config/dhcp"))
-  exa:value("0", translate("Ignore"))
-  exa:value("1", translate("Include Network/Hostnames"))
-  exa:value("2", translate("Advanced MX/SRV RR"))
-  exa:value("3", translate("Advanced CNAME RR"))
-  exa:depends({ dhcp_link = "none" })
-  exa:depends({ dhcp_link = "odhcpd" })
-
-  --TODO: dnsmasq needs to not reference resolve-file and get off port 53.
-
-  --Resource Tuning Tab
-  ctl = s1:taboption("resource", ListValue, "unbound_control", translate("Unbound Control App:"),
-    translate("Enable access for unbound-control"))
-  ctl.rmempty = false
-  ctl:value("0", translate("No Remote Control"))
-  ctl:value("1", translate("Local Host, No Encryption"))
-  ctl:value("2", translate("Local Host, Encrypted"))
-  ctl:value("3", translate("Local Subnet, Encrypted"))
-  ctl:value("4", translate("Local Subnet, Static Encryption"))
-
-  pro = s1:taboption("resource", ListValue, "protocol", translate("Recursion Protocol:"),
-    translate("Chose the protocol recursion queries leave on"))
-  pro:value("default", translate("Default"))
-  pro:value("ip4_only", translate("IP4 Only"))
-  pro:value("ip6_only", translate("IP6 Only"))
-  pro:value("ip6_prefer", translate("IP6 Preferred"))
-  pro:value("mixed", translate("IP4 and IP6"))
-  pro.rmempty = false
-
-  rsc = s1:taboption("resource", ListValue, "resource", translate("Memory Resource:"),
-    translate("Use menu System/Processes to observe any memory growth"))
-  rsc:value("default", translate("Default"))
-  rsc:value("tiny", translate("Tiny"))
-  rsc:value("small", translate("Small"))
-  rsc:value("medium", translate("Medium"))
-  rsc:value("large", translate("Large"))
-  rsc.rmempty = false
-
-  rsn = s1:taboption("resource", ListValue, "recursion", translate("Recursion Strength:"),
-    translate("Recursion activity affects memory growth and CPU load"))
-  rsn:value("default", translate("Default"))
-  rsn:value("passive", translate("Passive"))
-  rsn:value("aggressive", translate("Aggressive"))
-  rsn.rmempty = false
-
-  qry = s1:taboption("resource", Flag, "query_minimize", translate("Query Minimize:"),
-    translate("Break down query components for limited added privacy"))
-  qry.rmempty = false
-  qry:depends({ recursion = "passive" })
-  qry:depends({ recursion = "aggressive" })
-
-  qrs = s1:taboption("resource", Flag, "query_min_strict", translate("Strict Minimize:"),
-    translate("Strict version of 'query minimize' but it can break DNS"))
-  qrs.rmempty = false
-  qrs:depends({ query_minimize = true })
-
-  ath = s1:taboption("resource", Flag, "prefetch_root", translate("Prefetch Root:"),
-    translate("Obtain complete root zone files and install in auth-zone: clause"))
-  ath.rmempty = false
-
-  eds = s1:taboption("resource", Value, "edns_size", translate("EDNS Size:"),
-    translate("Limit extended DNS packet size"))
-  eds.datatype = "and(uinteger,min(512),max(4096))"
-  eds.rmempty = false
-
-  tlm = s1:taboption("resource", Value, "ttl_min", translate("TTL Minimum:"),
-    translate("Prevent excessively short cache periods"))
-  tlm.datatype = "and(uinteger,min(0),max(600))"
-  tlm.rmempty = false
-
-  stt = s1:taboption("resource", Flag, "extended_stats", translate("Extended Statistics:"),
-    translate("Extended statistics are printed from unbound-control"))
-  stt.rmempty = false
-end
-
-
---Trigger Tab, always unconditional
-ag2 = s1:taboption("trigger", Value, "root_age", translate("Root DSKEY Age:"),
-    translate("Limit days between RFC5011 copies to reduce flash writes"))
-ag2.datatype = "and(uinteger,min(1),max(99))"
-ag2:value("3", "3")
-ag2:value("9", "9 ("..translate("default")..")")
-ag2:value("12", "12")
-ag2:value("24", "24")
-ag2:value("99", "99 ("..translate("never")..")")
 
-tgr = s1:taboption("trigger", Value, "trigger_interface", translate("Trigger Networks:"),
-    translate("Networks that may trigger Unbound to reload (avoid wan6)"))
-tgr.template = "cbi/network_netlist"
-tgr.widget = "checkbox"
-tgr.rmempty = true
-tgr.cast = "string"
-tgr.nocreate = true
+if (valman == "0") then
+    -- Not in manual configuration mode; show UCI
+    --Basic Tab
+    lsv = s1:taboption("basic", Flag, "localservice",
+        translate("Local Service:"),
+        translate("Accept queries only from local subnets"))
+    lsv.rmempty = false
+
+    vld = s1:taboption("basic", Flag, "validator",
+        translate("Enable DNSSEC:"),
+        translate("Enable the DNSSEC validator module"))
+    vld.rmempty = false
+
+    nvd = s1:taboption("basic", Flag, "validator_ntp",
+        translate("DNSSEC NTP Fix:"),
+        translate("Break the loop where DNSSEC needs NTP and NTP needs DNS"))
+    nvd.rmempty = false
+    nvd:depends({ validator = true })
+
+    prt = s1:taboption("basic", Value, "listen_port",
+        translate("Listening Port:"),
+        translate("Choose Unbounds listening port"))
+    prt.datatype = "port"
+    prt.rmempty = false
+
+    --Avanced Tab
+    rlh = s1:taboption("advanced", Flag, "rebind_localhost",
+        translate("Filter Localhost Rebind:"),
+        translate("Protect against upstream response of 127.0.0.0/8"))
+    rlh.rmempty = false
+
+    rpv = s1:taboption("advanced", ListValue, "rebind_protection",
+        translate("Filter Private Rebind:"),
+        translate("Protect against upstream responses within local subnets"))
+    rpv:value("0", translate("No Filter"))
+    rpv:value("1", translate("Filter RFC1918/4193"))
+    rpv:value("2", translate("Filter Entire Subnet"))
+    rpv.rmempty = false
+
+    d64 = s1:taboption("advanced", Flag, "dns64", translate("Enable DNS64:"),
+        translate("Enable the DNS64 module"))
+    d64.rmempty = false
+
+    pfx = s1:taboption("advanced", Value, "dns64_prefix",
+        translate("DNS64 Prefix:"),
+        translate("Prefix for generated DNS64 addresses"))
+    pfx.datatype = "ip6addr"
+    pfx.placeholder = "64:ff9b::/96"
+    pfx.optional = true
+    pfx:depends({ dns64 = true })
+
+    din = s1:taboption("advanced", DynamicList, "domain_insecure",
+        translate("Domain Insecure:"),
+        translate("List domains to bypass checks of DNSSEC"))
+    din:depends({ validator = true })
+
+    ag2 = s1:taboption("advanced", Value, "root_age",
+        translate("Root DSKEY Age:"),
+        translate("Limit days between RFC5011 copies to reduce flash writes"))
+    ag2.datatype = "and(uinteger,min(1),max(99))"
+    ag2:value("3", "3")
+    ag2:value("9", "9 ("..translate("default")..")")
+    ag2:value("12", "12")
+    ag2:value("24", "24")
+    ag2:value("99", "99 ("..translate("never")..")")
+
+    tgr = s1:taboption("advanced", Value, "trigger_interface",
+        translate("Trigger Networks:"),
+        translate("Networks that may trigger Unbound to reload (avoid wan6)"))
+    tgr.template = "cbi/network_netlist"
+    tgr.widget = "checkbox"
+    tgr.rmempty = true
+    tgr.cast = "string"
+    tgr.nocreate = true
+
+    --DHCP Tab
+    dlk = s1:taboption("DHCP", ListValue, "dhcp_link",
+        translate("DHCP Link:"),
+        translate("Link to supported programs to load DHCP into DNS"))
+    dlk:value("none", translate("No Link"))
+    dlk:value("dnsmasq", "dnsmasq")
+    dlk:value("odhcpd", "odhcpd")
+    dlk.rmempty = false
+
+    dp6 = s1:taboption("DHCP", Flag, "dhcp4_slaac6",
+        translate("DHCPv4 to SLAAC:"),
+        translate("Use DHCPv4 MAC to discover IP6 hosts SLAAC (EUI64)"))
+    dp6.rmempty = false
+    dp6:depends({ dhcp_link = "odhcpd" })
+
+    dom = s1:taboption("DHCP", Value, "domain",
+        translate("Local Domain:"),
+        translate("Domain suffix for this router and DHCP clients"))
+    dom.placeholder = "lan"
+    dom:depends({ dhcp_link = "none" })
+    dom:depends({ dhcp_link = "odhcpd" })
+
+    dty = s1:taboption("DHCP", ListValue, "domain_type",
+        translate("Local Domain Type:"),
+        translate("How to treat queries of this local domain"))
+    dty:value("deny", translate("Denied (nxdomain)"))
+    dty:value("refuse", translate("Refused"))
+    dty:value("static", translate("Static (local only)"))
+    dty:value("transparent", translate("Transparent (local/global)"))
+    dty:depends({ dhcp_link = "none" })
+    dty:depends({ dhcp_link = "odhcpd" })
+
+    lfq = s1:taboption("DHCP", ListValue, "add_local_fqdn",
+        translate("LAN DNS:"),
+        translate("How to enter the LAN or local network router in DNS"))
+    lfq:value("0", translate("No Entry"))
+    lfq:value("1", translate("Hostname, Primary Address"))
+    lfq:value("2", translate("Hostname, All Addresses"))
+    lfq:value("3", translate("Host FQDN, All Addresses"))
+    lfq:value("4", translate("Interface FQDN, All Addresses"))
+    lfq:depends({ dhcp_link = "none" })
+    lfq:depends({ dhcp_link = "odhcpd" })
+
+    wfq = s1:taboption("DHCP", ListValue, "add_wan_fqdn",
+        translate("WAN DNS:"),
+        translate("Override the WAN side router entry in DNS"))
+    wfq:value("0", translate("Use Upstream"))
+    wfq:value("1", translate("Hostname, Primary Address"))
+    wfq:value("2", translate("Hostname, All Addresses"))
+    wfq:value("3", translate("Host FQDN, All Addresses"))
+    wfq:value("4", translate("Interface FQDN, All Addresses"))
+    wfq:depends({ dhcp_link = "none" })
+    wfq:depends({ dhcp_link = "odhcpd" })
+
+    exa = s1:taboption("DHCP", ListValue, "add_extra_dns",
+        translate("Extra DNS:"),
+        translate("Use extra DNS entries found in /etc/config/dhcp"))
+    exa:value("0", translate("Ignore"))
+    exa:value("1", translate("Host Records"))
+    exa:value("2", translate("Host/MX/SRV RR"))
+    exa:value("3", translate("Host/MX/SRV/CNAME RR"))
+    exa:depends({ dhcp_link = "none" })
+    exa:depends({ dhcp_link = "odhcpd" })
+
+    --TODO: dnsmasq needs to not reference resolve-file and get off port 53.
+
+    --Resource Tuning Tab
+    ctl = s1:taboption("resource", ListValue, "unbound_control",
+        translate("Unbound Control App:"),
+        translate("Enable access for unbound-control"))
+    ctl.rmempty = false
+    ctl:value("0", translate("No Remote Control"))
+    ctl:value("1", translate("Local Host, No Encryption"))
+    ctl:value("2", translate("Local Host, Encrypted"))
+    ctl:value("3", translate("Local Subnet, Encrypted"))
+    ctl:value("4", translate("Local Subnet, Static Encryption"))
+
+    pro = s1:taboption("resource", ListValue, "protocol",
+        translate("Recursion Protocol:"),
+        translate("Chose the protocol recursion queries leave on"))
+    pro:value("default", translate("Default"))
+    pro:value("ip4_only", translate("IP4 Only"))
+    pro:value("ip6_only", translate("IP6 Only"))
+    pro:value("ip6_prefer", translate("IP6 Preferred"))
+    pro:value("mixed", translate("IP4 and IP6"))
+    pro.rmempty = false
+
+    rsc = s1:taboption("resource", ListValue, "resource",
+        translate("Memory Resource:"),
+        translate("Use menu System/Processes to observe any memory growth"))
+    rsc:value("default", translate("Default"))
+    rsc:value("tiny", translate("Tiny"))
+    rsc:value("small", translate("Small"))
+    rsc:value("medium", translate("Medium"))
+    rsc:value("large", translate("Large"))
+    rsc.rmempty = false
+
+    rsn = s1:taboption("resource", ListValue, "recursion",
+        translate("Recursion Strength:"),
+        translate("Recursion activity affects memory growth and CPU load"))
+    rsn:value("default", translate("Default"))
+    rsn:value("passive", translate("Passive"))
+    rsn:value("aggressive", translate("Aggressive"))
+    rsn.rmempty = false
+
+    qry = s1:taboption("resource", Flag, "query_minimize",
+        translate("Query Minimize:"),
+        translate("Break down query components for limited added privacy"))
+    qry.rmempty = false
+    qry:depends({ recursion = "passive" })
+    qry:depends({ recursion = "aggressive" })
+
+    qrs = s1:taboption("resource", Flag, "query_min_strict",
+        translate("Strict Minimize:"),
+        translate("Strict version of 'query minimize' but it can break DNS"))
+    qrs.rmempty = false
+    qrs:depends({ query_minimize = true })
+
+    eds = s1:taboption("resource", Value, "edns_size",
+        translate("EDNS Size:"),
+        translate("Limit extended DNS packet size"))
+    eds.datatype = "and(uinteger,min(512),max(4096))"
+    eds.rmempty = false
+
+    tlm = s1:taboption("resource", Value, "ttl_min",
+        translate("TTL Minimum:"),
+        translate("Prevent excessively short cache periods"))
+    tlm.datatype = "and(uinteger,min(0),max(600))"
+    tlm.rmempty = false
+
+    stt = s1:taboption("resource", Flag, "extended_stats",
+        translate("Extended Statistics:"),
+        translate("Extended statistics are printed from unbound-control"))
+    stt.rmempty = false
+
+else
+    ag2 = s1:taboption("basic", Value, "root_age",
+        translate("Root DSKEY Age:"),
+        translate("Limit days between RFC5011 copies to reduce flash writes"))
+    ag2.datatype = "and(uinteger,min(1),max(99))"
+    ag2:value("3", "3")
+    ag2:value("9", "9 ("..translate("default")..")")
+    ag2:value("12", "12")
+    ag2:value("24", "24")
+    ag2:value("99", "99 ("..translate("never")..")")
+
+    tgr = s1:taboption("basic", Value, "trigger_interface",
+        translate("Trigger Networks:"),
+        translate("Networks that may trigger Unbound to reload (avoid wan6)"))
+    tgr.template = "cbi/network_netlist"
+    tgr.widget = "checkbox"
+    tgr.rmempty = true
+    tgr.cast = "string"
+    tgr.nocreate = true
+end
 
 
 function ena.cfgvalue(self, section)
-  return luci.sys.init.enabled("unbound") and self.enabled or self.disabled
+    return sy.init.enabled("unbound") and self.enabled or self.disabled
 end
 
 
 function ena.write(self, section, value)
-  if value == "1" then
-    luci.sys.init.enable("unbound")
-    luci.sys.call("/etc/init.d/unbound start >/dev/null")
-  else
-    luci.sys.call("/etc/init.d/unbound stop >/dev/null")
-    luci.sys.init.disable("unbound")
-  end
-
-  return Flag.write(self, section, value)
+    if (value == "1") then
+        sy.init.enable("unbound")
+        sy.call("/etc/init.d/unbound start >/dev/null 2>&1")
+
+    else
+        sy.call("/etc/init.d/unbound stop >/dev/null 2>&1")
+        sy.init.disable("unbound")
+    end
+
+
+    return Flag.write(self, section, value)
 end
 
 
-function m1.on_apply(self)
-  function ena.validate(self, value)
-    if value ~= "0" then
-      luci.sys.call("/etc/init.d/unbound restart >/dev/null 2>&1")
+function m1.on_commit(self)
+    if sy.init.enabled("unbound") then
+        -- Restart Unbound with configuration
+        sy.call("/etc/init.d/unbound restart >/dev/null 2>&1")
+
     else
-      luci.sys.call("/etc/init.d/unbound stop >/dev/null 2>&1")
+        sy.call("/etc/init.d/unbound stop >/dev/null 2>&1")
     end
-  end
+end
 
 
-  -- Restart Unbound with configuration and reload the page (some options hide)
-  luci.http.redirect(luci.dispatcher.build_url("admin", "services", "unbound"))
+function m1.on_apply(self)
+    -- reload the page because some options hide
+    ht.redirect(ds.build_url("admin", "services", "unbound", "configure"))
 end
 
 
index 67d2ec6c6bcef63f4e17ac8a7cbadc0d7d9c3e41..6c5e8c23ef63b5acc73d5c14b282361012c447e7 100644 (file)
@@ -1,28 +1,31 @@
--- Copyright 2016 Eric Luehrsen <ericluehrsen@hotmail.com>
+-- Copyright 2016 Eric Luehrsen <ericluehrsen@gmail.com>
 -- Licensed to the public under the Apache License 2.0.
 
 local m4, s4, frm
 local filename = "/etc/unbound/unbound_ext.conf"
-local description =  translatef("Here you may edit 'forward:' and 'remote-control:' in an extended 'include:'")
-description = description .. " (" .. filename .. ")"
+local fs = require "nixio.fs"
+local ut = require "luci.util"
 
 m4 = SimpleForm("editing", nil)
 m4:append(Template("unbound/css-editing"))
 m4.submit = translate("Save")
 m4.reset = false
-s4 = m4:section(SimpleSection, "Unbound Extended Conf", description)
+s4 = m4:section(SimpleSection, "",
+    translatef(
+    "Edit clauses such as 'forward-zone:' for 'include: " .. filename .. "'"))
+
 frm = s4:option(TextValue, "data")
 frm.datatype = "string"
 frm.rows = 20
 
 
 function frm.cfgvalue()
-  return nixio.fs.readfile(filename) or ""
+    return fs.readfile(filename) or ""
 end
 
 
 function frm.write(self, section, data)
-  return nixio.fs.writefile(filename, luci.util.trim(data:gsub("\r\n", "\n")))
+    return fs.writefile(filename, ut.trim(data:gsub("\r\n", "\n")))
 end
 
 
index 5cfb9c32c1111a8bdd570cf4585c11062cf28e93..317c23fda0ce21e29fd7580772d1485dc548b16b 100644 (file)
@@ -1,28 +1,31 @@
--- Copyright 2016 Eric Luehrsen <ericluehrsen@hotmail.com>
+-- Copyright 2016 Eric Luehrsen <ericluehrsen@gmail.com>
 -- Licensed to the public under the Apache License 2.0.
 
 local m2, s2, frm
 local filename = "/etc/unbound/unbound.conf"
-local description =  translatef("Here you may edit raw 'unbound.conf' when you don't use UCI:")
-description = description .. " (" .. filename .. ")"
+local fs = require "nixio.fs"
+local ut = require "luci.util"
 
 m2 = SimpleForm("editing", nil)
 m2:append(Template("unbound/css-editing"))
 m2.submit = translate("Save")
 m2.reset = false
-s2 = m2:section(SimpleSection, "Unbound Conf", description)
+s2 = m2:section(SimpleSection, "",
+    translatef(
+    "Edit '" .. filename .. "' when you do not use UCI."))
+
 frm = s2:option(TextValue, "data")
 frm.datatype = "string"
 frm.rows = 20
 
 
 function frm.cfgvalue()
-  return nixio.fs.readfile(filename) or ""
+    return fs.readfile(filename) or ""
 end
 
 
 function frm.write(self, section, data)
-  return nixio.fs.writefile(filename, luci.util.trim(data:gsub("\r\n", "\n")))
+    return fs.writefile(filename, ut.trim(data:gsub("\r\n", "\n")))
 end
 
 
index d0ac407847b3276db5bd373d154017d6036517c6..5cef2a67b013a80789a8ac674d350e843f0a99b9 100644 (file)
@@ -1,28 +1,31 @@
--- Copyright 2016 Eric Luehrsen <ericluehrsen@hotmail.com>
+-- Copyright 2016 Eric Luehrsen <ericluehrsen@gmail.com>
 -- Licensed to the public under the Apache License 2.0.
 
 local m3, s3, frm
 local filename = "/etc/unbound/unbound_srv.conf"
-local description =  translatef("Here you may edit the 'server:' clause in an internal 'include:'")
-description = description .. " (" .. filename .. ")"
+local fs = require "nixio.fs"
+local ut = require "luci.util"
 
 m3 = SimpleForm("editing", nil)
 m3:append(Template("unbound/css-editing"))
 m3.submit = translate("Save")
 m3.reset = false
-s3 = m3:section(SimpleSection, "Unbound Server Conf", description)
+s3 = m3:section(SimpleSection, "",
+    translatef(
+    "Edit 'server:' clause options for 'include: " .. filename .. "'"))
+
 frm = s3:option(TextValue, "data")
 frm.datatype = "string"
 frm.rows = 20
 
 
 function frm.cfgvalue()
-  return nixio.fs.readfile(filename) or ""
+    return fs.readfile(filename) or ""
 end
 
 
 function frm.write(self, section, data)
-  return nixio.fs.writefile(filename, luci.util.trim(data:gsub("\r\n", "\n")))
+    return fs.writefile(filename, ut.trim(data:gsub("\r\n", "\n")))
 end
 
 
diff --git a/applications/luci-app-unbound/luasrc/model/cbi/unbound/uciedit.lua b/applications/luci-app-unbound/luasrc/model/cbi/unbound/uciedit.lua
new file mode 100644 (file)
index 0000000..3aef189
--- /dev/null
@@ -0,0 +1,37 @@
+-- Copyright 2016 Eric Luehrsen <ericluehrsen@gmail.com>
+-- Licensed to the public under the Apache License 2.0.
+
+local m6, s6, frm
+local filename = "/etc/config/unbound"
+local fs = require "nixio.fs"
+local ut = require "luci.util"
+
+m6 = SimpleForm("editing", nil)
+m6:append(Template("unbound/css-editing"))
+m6.submit = translate("Save")
+m6.reset = false
+s6 = m6:section(SimpleSection, "",
+    translatef("Edit '" .. filename .. "' "
+    .. "and help can be found in OpenWrt "
+    .. "<a href=\"%s\" target=\"_blank\">Guides</a> "
+    .. "and <a href=\"%s\" target=\"_blank\">Github</a>.",
+    "https://openwrt.org/docs/guide-user/services/dns/unbound",
+    "https://github.com/openwrt/packages/blob/master/net/unbound/files/README.md"))
+
+frm = s6:option(TextValue, "data")
+frm.datatype = "string"
+frm.rows = 20
+
+
+function frm.cfgvalue()
+    return fs.readfile(filename) or ""
+end
+
+
+function frm.write(self, section, data)
+    return fs.writefile(filename, ut.trim(data:gsub("\r\n", "\n")))
+end
+
+
+return m6
+
diff --git a/applications/luci-app-unbound/luasrc/model/cbi/unbound/zones.lua b/applications/luci-app-unbound/luasrc/model/cbi/unbound/zones.lua
new file mode 100644 (file)
index 0000000..bbc0e23
--- /dev/null
@@ -0,0 +1,207 @@
+-- Copyright 2017 Eric Luehrsen <ericluehrsen@gmail.com>
+-- Licensed to the public under the Apache License 2.0.
+
+local m5, s5
+local ztype, zones, servers, fallback, enabled
+
+local fs = require "nixio.fs"
+local ut = require "luci.util"
+local sy = require "luci.sys"
+local resolvfile = "/tmp/resolv.conf.auto"
+
+m5 = Map("unbound")
+s5 = m5:section(TypedSection, "zone", "Zones",
+    translatef("This shows extended zones and more details can be "
+    .. "changed in Files tab and <a href=\"%s\">Edit:UCI</a> subtab.",
+    "/cgi-bin/luci/admin/services/unbound/files" ))
+
+s5.addremove = false
+s5.anonymous = true
+s5.sortable = true
+s5.template = "cbi/tblsection"
+
+ztype = s5:option(DummyValue, "DummyType", translate("Type"))
+ztype.rawhtml = true
+
+zones = s5:option(DummyValue, "DummyZones", translate("Zones"))
+zones.rawhtml = true
+
+servers = s5:option(DummyValue, "DummyServers", translate("Servers"))
+servers.rawhtml = true
+
+fallback = s5:option(Flag, "fallback", translate("Fallback"))
+fallback.rmempty = false
+
+enabled = s5:option(Flag, "enabled", translate("Enable"))
+enabled.rmempty = false
+
+
+function ztype.cfgvalue(self, s)
+    -- Format a meaninful tile for the Zone Type column
+    local itxt = self.map:get(s, "zone_type")
+    local itls = self.map:get(s, "tls_upstream")
+
+
+    if itxt and itxt:match("forward") then
+        if itls and (itls == "1") then
+            return translate("Forward TLS")
+
+        else
+            return translate("Forward")
+        end
+
+    elseif itxt and itxt:match("stub") then
+        return translate("Recurse")
+
+    elseif itxt and itxt:match("auth") then
+        return translate("AXFR")
+
+    else
+        return translate("Error")
+    end
+end
+
+
+function zones.cfgvalue(self, s)
+    -- Format a meaninful sentence for the Zones viewed column
+    local xtxt, otxt
+    local itxt = self.map:get(s, "zone_name")
+    local itype = self.map:get(s, "zone_type")
+
+
+    for xtxt in ut.imatch(itxt) do
+        if (xtxt == ".") then
+            -- zone_name lists
+            xtxt = translate("(root)")
+        end
+
+
+        if otxt and (#otxt > 0) then
+            otxt = otxt .. ", <var>%s</var>" % xtxt
+
+        else
+            otxt = "<var>%s</var>" % xtxt
+        end
+    end
+
+
+    if itype and itype:match("forward") then
+        -- from zone_type create a readable hint for the action
+        otxt = translate("accept upstream results for ") .. otxt
+
+    elseif itype and itype:match("stub") then
+        otxt = translate("select recursion for ") .. otxt
+
+    elseif itype and itype:match("auth") then
+        otxt = translate("prefetch zone files for ") .. otxt
+
+    else
+        otxt = translate("unknown action for ") .. otxt
+    end
+
+
+    if otxt and (#otxt > 0) then
+        return otxt
+
+    else
+        return "(empty)"
+    end
+end
+
+
+function servers.cfgvalue(self, s)
+    -- Format a meaninful sentence for the Servers (and URL) column
+    local xtxt, otxt, rtxt, found
+    local itxt = self.map:get(s, "server")
+    local iurl = self.map:get(s, "url_dir")
+    local itype = self.map:get(s, "zone_type")
+    local itls = self.map:get(s, "tls_upstream")
+    local iidx = self.map:get(s, "tls_index")
+    local irslv = self.map:get(s, "resolv_conf")
+
+
+    for xtxt in ut.imatch(itxt) do
+        if otxt and (#otxt > 0) then
+            -- bundle and make pretty the server list
+            otxt = otxt .. ", <var>%s</var>" % xtxt
+
+        else
+            otxt = "<var>%s</var>" % xtxt
+        end
+    end
+
+
+    if otxt and (#otxt > 0)
+    and itls and (itls == "1")
+    and iidx and (#iidx > 0) then
+        -- show TLS certificate name index if provided
+        otxt = translatef("use nameservers by <var>%s</var> at ", iidx) .. otxt
+
+    elseif otxt and (#otxt > 0) then
+        otxt = translate("use nameservers ") .. otxt
+    end
+
+
+    if iurl and (#iurl > 0) and itype and itype:match("auth") then
+        if otxt and (#otxt > 0) then
+            -- include optional URL filed for auth-zone: type
+            otxt = otxt .. translatef(", and try <var>%s</var>", iurl)
+
+        else
+            otxt = translatef("download from <var>%s</var>", iurl)
+        end
+    end
+
+
+    if irslv and (irslv == "1") and itype and itype:match("forward") then
+        for xtxt in ut.imatch(fs.readfile(resolvfile)) do
+            if xtxt:match("nameserver") then
+                found = true
+
+            elseif (found == true) then
+                if rtxt and (#rtxt > 0) then
+                    -- fetch name servers from resolv.conf
+                    rtxt = rtxt .. ", <var>%s</var>" % xtxt
+
+                else
+                    rtxt = "<var>%s</var>" % xtxt
+                end
+
+
+                found = false
+            end
+        end
+
+
+        if otxt and (#otxt > 0) and rtxt and (#rtxt > 0) then
+            otxt = otxt
+                .. translatef(", and <var>%s</var> entries ", resolvfile) .. rtxt
+
+        elseif rtxt and (#rtxt > 0) then
+            otxt = translatef("use <var>%s</var> nameservers ", resolvfile) .. rtxt
+        end
+    end
+
+
+    if otxt and (#otxt > 0) then
+        return otxt
+
+    else
+        return "(empty)"
+    end
+end
+
+
+function m5.on_commit(self)
+    if sy.init.enabled("unbound") then
+        -- Restart Unbound with configuration
+        sy.call("/etc/init.d/unbound restart >/dev/null 2>&1")
+
+    else
+        sy.call("/etc/init.d/unbound stop >/dev/null 2>&1")
+    end
+end
+
+
+return m5
+