module(..., package.seeall) -- Load libraries modelfunctions = require("modelfunctions") fs = require("acf.fs") format = require("acf.format") validator = require("acf.validator") -- Set variables local configfile = "/etc/dnsmasq.conf" local processname = "dnsmasq" local packagename = "dnsmasq" local leasefile = "/var/lib/misc/dnsmasq.leases" local dnsfilterfile = "/etc/dnsmasq-dnsfilter.conf" -- ################################################################################ -- LOCAL FUNCTIONS local function update_file (file, search_name, value_in) if not file or not search_name or search_name == "" then return file, false end -- Since we clear out the value, this prevents us from changing parent's value local value = value_in if type(value_in) == "table" then value = {} for i,val in ipairs(value_in) do value[#value + 1] = val end end local new_conf_file = {} local skip_lines = {} for l in string.gmatch(file, "([^\n]*)\n?") do if string.find ( l, "\\%s*$" ) then skip_lines[#skip_lines+1] = string.match(l, "^(.*)\\%s*$") l = nil else if #skip_lines then skip_lines[#skip_lines+1] = l l = table.concat(skip_lines, " ") end -- check if comment line if not string.find ( l, "^%s*#" ) then -- find name local a = string.match ( l, "^%s*([^=]*%S)%s*=" ) if a and (search_name == a) then -- Figure out the value local b = string.match ( l, '=%s*(.*%S)%s*$' ) or "" -- no need to remove comments from end of line, they -- should not be there, see 'man dnsmasq' -- We found the name, change the value if not value then l = nil elseif type(value) == "string" then if value ~= b then l = search_name.."="..value end value = nil else local temp = l l = nil for i,val in ipairs(value) do if val == b then l = temp table.remove(value, i) break end end end skip_lines = {} -- replacing line end end if #skip_lines > 0 then for i,line in ipairs(skip_lines) do new_conf_file[#new_conf_file + 1] = line end skip_lines = {} l = nil end end new_conf_file[#new_conf_file + 1] = l end if value then -- we didn't find the searchname, add it now if type(value) == "string" then new_conf_file[#new_conf_file + 1] = search_name.."="..value else for i,val in ipairs(value) do new_conf_file[#new_conf_file + 1] = search_name.."="..val end end end file = table.concat(new_conf_file, '\n') return file, true end -- Parse string for name=value pairs, returned in a table local function parse_file (file) if not file or file == "" then return nil end local opts = nil local skip_lines = {} for l in string.gmatch(file, "([^\n]*)\n?") do if string.find ( l, "\\%s*$" ) then skip_lines[#skip_lines+1] = string.match(l, "^(.*)\\%s*$") else if #skip_lines then skip_lines[#skip_lines+1] = l l = table.concat(skip_lines, " ") skip_lines = {} end -- check if comment line if not string.find ( l, "^%s*#" ) then -- find name local a = string.match ( l, "^%s*([^=]*%S)%s*=" ) if a then -- Figure out the value local b = string.match ( l, '=%s*(.*%S)%s*$' ) or "" -- no need to remove comments from end of line, they -- should not be there, see 'man dnsmasq' if not (opts) then opts = {} end if not opts[a] then opts[a] = {b} else table.insert(opts[a], b) end end end end end return opts end local function validateconfig(self, config) local success = true function testlist(param, test, errtxt) if #param.value > 0 then for i,val in ipairs(param.value) do if test(val) then param.errtxt = errtxt success = false break end end end end function testtext(param, test, errtxt) if test(param.value) then param.errtxt = errtxt success = false end end local interfaces = {} local interfacescontroller = self:new("alpine-baselayout/interfaces") local ints = interfacescontroller.model.get_interfaces() interfacescontroller:destroy() for i,v in ipairs(ints.value) do interfaces[v] = i end testlist(config.value.domain, function(v) return string.find(v, "%s") end, "Cannot contain spaces") testlist(config.value.interface, function(v) return not interfaces[v] end, "Invalid interface") testlist(config.value.listen_address, function(v) return not validator.is_ipv4(v) end, "Invalid IP Address") testlist(config.value.dhcp_range, function(v) return string.find(v, "%s") end, "Cannot contain spaces") testlist(config.value.no_dhcp_interface, function(v) return not interfaces[v] end, "Invalid interface") testlist(config.value.dhcp_host, function(v) return string.find(v, "%s") end, "Cannot contain spaces") testlist(config.value.dhcp_option, function(v) return string.find(v, "%s") end, "Cannot contain spaces") testlist(config.value.mx_host, function(v) return string.find(v, "%s") end, "Cannot contain spaces") if config.value.dns_filtering.value then -- validate ip address only if dns filtering is enabled testtext(config.value.dns_default_address, function(v) return not validator.is_ipv4(v) end, "Invalid IP Address") end return success, config end local function dns_filtering_is_enabled(conf) -- Returns true if dnsfilterfile is included in configfile. -- conf - config variable from parse_file() local conf_files = conf["conf-file"] or {} for i,conf_file in ipairs(conf_files) do if conf_file == dnsfilterfile then return true end end return false end local function set_dns_filtering(file, enable) -- Returns new table of conf["conf-file"]. -- Includes existing conf-files and adds or removes dnsfilterfile. -- file - config data -- enabled - boolean for dns filtering local conf = parse_file(file) or {} local enabled = dns_filtering_is_enabled(conf) local conf_files = conf["conf-file"] or {} if enabled ~= enable then -- state changed, we have to modify conf_files table if enable then -- add dnsfilterfile to conf-files table.insert(conf_files, dnsfilterfile) else -- remove dnsfilterfile from conf-files for i,conf_file in ipairs(conf_files) do if conf_file == dnsfilterfile then table.remove(conf_files, i) break end end end end return conf_files end local function update_dns_whitelist(servers, default_address) -- Updates dnsfilterfile -- servers - value from web form as table local file = fs.read_file(dnsfilterfile) or "" local serverlines = {} for i,server in ipairs(servers or {}) do local modified = "/" .. server .. "/#" table.insert(serverlines, modified) end file = update_file(file, "server", serverlines) -- update address to point to default_address file = update_file(file, "address", "/#/" .. default_address) fs.write_file(dnsfilterfile, file) return true end local function parse_whitelist(file) -- Parses dnsfilterfile and returns table of servers and -- default address. -- Format should be in: -- server=/whitelisted.com/# -- server=/another.com/# -- address=/#/192.168.1.1 local servers = {} local parsed = parse_file(file) or {} local serverlines = parsed["server"] or {} for i,value in ipairs(serverlines) do -- remove '/' and '/#' from value local server = string.match(value, "^/%s*([^=]*%S)%s*/#") table.insert(servers, server) end -- parse default address local default_address = "" local addresses = parsed["address"] or {} if #addresses > 0 then -- get first address, anyway there shouldn't be more than 1? default_address = string.match(addresses[1], "^/#/(.*%S)") end return servers, default_address end -- ################################################################################ -- PUBLIC FUNCTIONS function get_startstop(self, clientdata) return modelfunctions.get_startstop(processname) end function startstop_service(self, startstop, action) return modelfunctions.startstop_service(startstop, action) end function getstatus() return modelfunctions.getstatus(processname, packagename, "DNS Masq Status") end function getconfig() local conf = parse_file(fs.read_file(configfile) or "") or {} local dns_filtering = {} dns_filtering.enabled = dns_filtering_is_enabled(conf) dns_filtering.whitelist, dns_filtering.default_address = parse_whitelist(fs.read_file(dnsfilterfile) or "") local output = {} output.domain = cfe({ type="list", value=conf.domain or {}, label="Local Domain", descr="List of internal domain(s) for your LAN 'domain[,address_range]'. Address range can be a single IP address, a range specified by 'IP,IP', or IP/netmask." }) output.interface = cfe({ type="list", value=conf.interface or {}, label="Interface", descr="List of interfaces to listen on." }) output.listen_address = cfe({ type="list", value=conf["listen-address"] or {}, label="List of IP addresses to listen on" }) output.dhcp_range = cfe ({ type="list", value=conf["dhcp-range"] or {}, label="Range of DHCP IPs", descr="List of Start,End,Netmask,Time in seconds/minutes(m)/hours(h) ie. 169.254.0.10,169.254.0.100,255.255.255.0,12h" }) output.no_dhcp_interface = cfe({ type="list", value=conf["no-dhcp-interface"] or {}, label="No DHCP Interface", descr="List of interfaces which should have DNS but not DHCP." }) output.dhcp_host = cfe({ type="list", value=conf["dhcp-host"] or {}, label="DHCP Host Parameter", descr="List of per host parameters for the DHCP server. See dnsmasq documentation." }) output.dhcp_option = cfe({ type="list", value=conf["dhcp-option"] or {}, label="DHCP Option", descr="List of different or extra options to DHCP clients. See dnsmasq documentation." }) output.mx_host = cfe({ type="list", value=conf["mx-host"] or {}, label="MX Record", descr="List of MX records 'mx_name,hostname'." }) output.dns_filtering = cfe({ type="boolean", value=dns_filtering.enabled or false, label="DNS filtering enabled", descr="State of DNS filtering." }) output.dns_whitelist = cfe({ type="list", value=dns_filtering.whitelist or {}, label="DNS whitelist", descr="DNS whitelist for filtering. Type one allowed domain per one line." }) output.dns_default_address = cfe({ type="text", value=dns_filtering.default_address or "", label="Blocked page server address", descr="All DNS requests which are not on the white list, will be redirected to this IP address. This address should have a web server running that shows an address blocked page. Example: 192.168.1.1" }) return cfe({ type="group", value=output, label="DNS Masq Config" }) end function setconfig(self, config) local success, config = validateconfig(self, config) if success then local file = fs.read_file(configfile) or "" file = update_file(file,"domain",config.value.domain.value) file = update_file(file,"interface",config.value.interface.value) file = update_file(file,"listen-address",config.value.listen_address.value) file = update_file(file,"dhcp-range",config.value.dhcp_range.value) file = update_file(file,"no-dhcp-interface",config.value.no_dhcp_interface.value) file = update_file(file,"dhcp-host",config.value.dhcp_host.value) file = update_file(file,"dhcp-option",config.value.dhcp_option.value) file = update_file(file,"mx-host",config.value.mx_host.value) -- dns filtering local conf_file_value = set_dns_filtering(file, config.value.dns_filtering.value) file = update_file(file,"conf-file",conf_file_value) fs.write_file(configfile, file) else config.errtxt = "Failed to set config" end -- update dns filter file update_dns_whitelist(config.value.dns_whitelist.value, config.value.dns_default_address.value) return config end function getconfigfile() -- FIXME Validate return modelfunctions.getfiledetails(configfile) end function setconfigfile(self, filedetails) -- FIXME Validate return modelfunctions.setfiledetails(self, filedetails, {configfile}) end function getleases() return modelfunctions.getfiledetails(leasefile) end