--[[ @object-name: dns @object-desc: dns --]] local utils = require "oui.utils" local cjson = require "cjson" local fs = require "oui.fs" local uci = require "uci" local M = {} --[[ @method-name: get_info @method-desc: get DNS information @out array protos supported encryption protocols [DoT; DoH; DNSCrypt; oDoH] @out number dnscrypt_version DNS encrypted version @out array serverlist DoH,DNSCrypt,oDoH list of supported servers @out string serverlist.name name @out string serverlist.proto protocol @out bool serverlist.ipv6 IPv6 @out bool serverlist.dnssec dnssec @out bool serverlist.nolog no log @out bool serverlist.nofilter does not support filtering @out number err_code error code: [-1: failed to get server list] @out string err_msg error message @in-example: {"jsonrpc":"2.0","method":"call","params":["","dns","get_info",{}] } @out-example: {"id":null,"jsonrpc":"2.0","result":{"protos":["DoT","DoH","DNSCrypt","oDoH"],"serverlist":[{"nolog":true,"proto":"DoH","name":"ahadns-doh-ny-ipv6","nofilter":false,"ipv6":true,"dnssec":true},{"nolog":true,"proto":"DNSCrypt","name":"ffmuc.net","nofilter":true,"ipv6":false,"dnssec":true},{"nolog":true,"proto":"DNSCrypt","name":"techsaviours.org-dnscrypt","nofilter":true,"ipv6":false,"dnssec":true}]}} --]] function M.get_info() local protos = {"DoT", "DNSCrypt"} local serverlist = {} if fs.access("/usr/share/dnscrypt-proxy/dnscrypt-resolvers.csv") then for l in io.lines("/usr/share/dnscrypt-proxy/dnscrypt-resolvers.csv") do local fields = {} for field in l:gmatch("[^,]+") do fields[#fields + 1] = field end if fields[1] ~= "Name" then serverlist[#serverlist + 1] = { name = fields[1], proto = "DNSCrypt", } end end return { protos = protos, dnscrypt_version = 1, serverlist = serverlist } end protos[#protos + 1] = "DoH" protos[#protos + 1] = "oDoH" local function fetch_server() local p = ngx.pipe.spawn( {"/usr/sbin/dnscrypt-proxy", "-config", "/etc/dnscrypt-proxy2/dnscrypt-proxy.toml", "-list-all", "-json"}, { stdout_read_timeout = 5000 } ) local data = p:stdout_read_all() local ok, data = pcall(cjson.decode, data) if ok then return data end end local data = fetch_server() if not data then for _, name in ipairs({"public-resolvers.md", "relays.md", "odoh-servers.md"}) do os.execute("touch /etc/dnscrypt-proxy2/" .. name) end ngx.log(ngx.ERR, "dnscrypt-proxy2: fetch failed, use old") end data = fetch_server() if not data then return { err_code = -1, err_msg = "fetch servers list failed" } end for _, s in ipairs(data) do serverlist[#serverlist + 1] = { name = s.name, proto = s.proto:match("%S+"), ipv6 = s.ipv6, dnssec = s.dnssec, nolog = s.nolog, nofilter = s.nofilter } end return { protos = protos, dnscrypt_version = 2, serverlist = serverlist } end --[[ @method-name: get_config @method-desc: get the current configuration @out bool force_dns whether to cover all clients @out bool rebind_protection whether to enable rebinding attack protection @out string mode mode [auto; secure; manual; proxy] @out string proto protocol @out string ?nextdns_id when dot is selected, if there is this field, it means NextDns, otherwise it means Cloudflare @out array ?server represents a custom dns server list or selected encrypted server list or proxy address or DNS server address in automatic mode @in-example: {"jsonrpc":"2.0","method":"call","params":["","dns","get_config",{}] } @out-example: {"id":null,"jsonrpc":"2.0","result":{"force_dns":true,"proxy_server":"fvz-anyone","proxy_serverlist":["adguard-dns-family-ns1","adguard-dns-family-ns2"],"rebind_protection":"1"}} --]] function M.get_config() local c = uci.cursor() local res = {} res.force_dns = not not c:get("firewall", "dns_over_lan") res.rebind_protection = c:get("dhcp", "@dnsmasq[0]", "rebind_protection") ~= "0" local mode = c:get("gl-dns", "@dns[0]", "mode") or "auto" local proto = c:get("gl-dns", "@dns[0]", "proto") local ipv6_en = c:get("glipv6", "globals", "enabled") if mode == "manual" or mode == "proxy" then res.server = c:get("dhcp", "@dnsmasq[0]", "server") or {} elseif mode == "secure" then if proto == "DoT" then local nextdns_id = c:get("stubby", "@resolver[0]", "tls_auth_name") or "" if nextdns_id ~= "cloudflare-dns.com" then if nextdns_id == "dns.nextdns.io" then res.nextdns_id = "" else res.nextdns_id = nextdns_id:match("[^%.]+") end end else res.server = {} if not fs.access("/usr/share/dnscrypt-proxy/dnscrypt-resolvers.csv") then for l in io.lines("/etc/dnscrypt-proxy2/dnscrypt-proxy.toml") do if l:match("^server_names = ") then local server = l:gsub("server_names = ", ""):gsub("'", "\"") local ok, server = pcall(cjson.decode, server) if ok then res.server = server end break end end else c:foreach("dnscrypt-proxy", "dnscrypt-proxy", function(s) res.server[#res.server + 1] = s.resolver end) end end else res.server = {} local server = {} local resolvfile = c:get("dhcp", "@dnsmasq[0]", "resolvfile") or "" if fs.access(resolvfile) then local nameservers for l in io.lines(resolvfile) do local iface = l:match("# Interface (%S+)") if iface then server[iface] = {} nameservers = server[iface] else local nameserver = l:match("nameserver (%S+)") if nameserver and nameservers then nameservers[#nameservers + 1] = nameserver else nameservers = nil end end end for name, nameservers in pairs(server) do if #nameservers > 0 then if name:match("modem") then if name:match("6$") then name = "modem6" else name = "modem" end end res.server[#res.server + 1] = name .. " " .. table.concat(nameservers, ",") end end end local function parse_vpn_resolv(name) local path = "/tmp/resolv.conf." .. name local nameservers = {} local nameservers6 = {} if fs.access(path) then for l in io.lines(path) do local nameserver = l:match("nameserver (%S+)") if nameserver then if nameserver:match(":") then nameservers6[#nameservers6 + 1] = nameserver else nameservers[#nameservers + 1] = nameserver end end end end if #nameservers > 0 then res.server[#res.server + 1] = name .. " " .. table.concat(nameservers, ",") end if #nameservers6 > 0 and ipv6_en == "1" then res.server[#res.server + 1] = name .. "6 " .. table.concat(nameservers6, ",") end end parse_vpn_resolv("ovpn") parse_vpn_resolv("wg") end res.mode = mode res.proto = proto return res end local function set_dns_overall(c, enable) if enable then local lan_ip = c:get("network", "lan", "ipaddr") or "192.168.8.1" local guest_ip = c:get("network", "guest", "ipaddr") or "192.168.9.1" c:set("firewall", "dns_over_lan", "redirect") c:set("firewall", "dns_over_lan", "src", "lan") c:set("firewall", "dns_over_lan", "src_dport", "53") c:set("firewall", "dns_over_lan", "dest", "lan") c:set("firewall", "dns_over_lan", "dest_ip", lan_ip) c:set("firewall", "dns_over_lan", "target", "DNAT") c:set("firewall", "dns_over_guest", "redirect") c:set("firewall", "dns_over_guest", "src", "guest") c:set("firewall", "dns_over_guest", "src_dport", "53") c:set("firewall", "dns_over_guest", "dest", "guest") c:set("firewall", "dns_over_guest", "dest_ip", guest_ip) c:set("firewall", "dns_over_guest", "target", "DNAT") else c:delete("firewall", "dns_over_lan") c:delete("firewall", "dns_over_guest") end c:commit("firewall") ngx.pipe.spawn({"/etc/init.d/firewall", "reload"}) end --[[ @method-name: set_config @method-desc: modify the configuration file @in bool force_dns whether to cover all clients @in bool rebind_protection whether to enable rebinding attack protection @in string mode mode [auto; secure; manual; proxy] @in string ?proto protocol @in string ?nextdns_id when dot is selected, if there is this field, it means NextDns, otherwise it means Cloudflare @in array ?server represents a custom dns server list or selected encrypted server list or proxy address or DNS server address in automatic mode @out number err_code error code: [-1: DNS is occupied; -2: parameter error] @out string err_msg error message @in-example: {"method":"call", "params":["", "dns", "set_config", {"type":"auto", "rebind_protection": true}]} @out-example: {"id":null,"jsonrpc":"2.0","result":[]} --]] function M.set_config(params) local c = uci.cursor() local mode = params.mode local proto = params.proto local server = params.server or {} local agh_enable = c:get("adguardhome", "config", "enabled") == "1" if mode and agh_enable then return { err_code = -1, err_msg = "Dns Busy. Occupied by adguardhome" } end local dnscrypt_version = 2 if mode then c:delete("dhcp", "@dnsmasq[0]", "server") c:delete("dhcp", "@dnsmasq[0]", "noresolv") c:delete("dhcp", "@dnsmasq[0]", "boguspriv") c:delete("dhcp", "@dnsmasq[0]", "cachesize ") c:set("stubby", "global", "enabled", 0) if fs.access("/usr/share/dnscrypt-proxy/dnscrypt-resolvers.csv") then dnscrypt_version = 1 c:set("dnscrypt-proxy", "@global[0]", "enable", 0) end if mode == "manual" or mode == "proxy" then c:set("dhcp", "@dnsmasq[0]", "noresolv", "1") c:set("dhcp", "@dnsmasq[0]", "server", server) elseif mode == "secure" then if proto == "DoT" then c:set("stubby", "global", "enabled", 1) c:foreach("stubby", "resolver", function(s) c:delete("stubby", s[".name"]) end) local servers = { {"2606:4700:4700::1111", "cloudflare-dns.com", 853}, {"2606:4700:4700::1001", "cloudflare-dns.com", 853}, {"1.1.1.1", "cloudflare-dns.com", 853}, {"1.0.0.1", "cloudflare-dns.com", 853} } local id = params.nextdns_id if id then local addr_v6_tail = "0" local auth_name if id ~= "" then addr_v6_tail = id:sub(1, 2) .. ":" .. id:sub(3) else auth_name = "dns.nextdns.io" end servers = { {"45.90.28.0", auth_name or id .. ".dns1.nextdns.io", ""}, {"2a07:a8c0::" .. addr_v6_tail, auth_name or id .. ".dns1.nextdns.io", ""}, {"45.90.30.0", auth_name or id .. ".dns2.nextdns.io", ""}, {"2a07:a8c1::" .. addr_v6_tail, auth_name or id .. ".dns2.nextdns.io", ""} } end for _, server in ipairs(servers) do local sid = c:add("stubby", "resolver") local addr, name, port = server[1], server[2], server[3] c:set("stubby", sid, "address", addr) c:set("stubby", sid, "tls_port", port) c:set("stubby", sid, "tls_auth_name", name) end else if dnscrypt_version == 1 then c:set("dnscrypt-proxy", "@global[0]", "enable", 1) c:foreach("dnscrypt-proxy", "dnscrypt-proxy", function(s) c:delete("dnscrypt-proxy", s[".name"]) end) for _, name in ipairs(server) do local sid = c:add("dnscrypt-proxy", "dnscrypt-proxy") c:set("dnscrypt-proxy", sid, "address", "127.0.0.1") c:set("dnscrypt-proxy", sid, "port", 5453) c:set("dnscrypt-proxy", sid, "resolver", name) end else if not fs.access("/etc/dnscrypt-proxy2/dnscrypt-proxy.toml.bak") then os.execute("cp /etc/dnscrypt-proxy2/dnscrypt-proxy.toml /etc/dnscrypt-proxy2/dnscrypt-proxy.toml.bak") end local lines = {} for l in io.lines("/etc/dnscrypt-proxy2/dnscrypt-proxy.toml") do if l:match("^server_names = ") or l:match("^# server_names = ") then lines[#lines + 1] = "server_names = " .. cjson.encode(server):gsub("\"", "'") elseif l:match("^listen_addresses = ") then lines[#lines + 1] = "listen_addresses = ['127.0.0.1:5453']" elseif l:match("routes =") then if proto == "oDoH" then lines[#lines + 1] = "routes = [{ server_name='*', via=['odohrelay-crypto-sx', 'odohrelay-ibksturm', 'odohrelay-koki-ams', 'oodohrelay-koki-se', 'odohrelay-surf']}]" else lines[#lines + 1] = "# routes = [" end else if #lines + 1 > 688 and #lines + 1 < 701 then l = string.gsub(l, "#", "") if proto ~= "oDoH" then l = "#" .. l end end if #lines + 1 > 857 then l = string.gsub(l, "#", "") if proto == "oDoH" then l = "#" .. l end end lines[#lines + 1] = l end end utils.writefile("/etc/dnscrypt-proxy2/dnscrypt-proxy.toml", table.concat(lines, "\n")) fs.sync() -- Disable because dnscrypt-proxy's block_undelegated already blocks RFC 1918 private addresses and RFC 6761 top level domains c:set("dhcp", "@dnsmasq[0]", "boguspriv", "1") -- Disable dnsmasq cache because we don't want to cache twice and the dnscrypt-proxy cache is superior c:set("dhcp", "@dnsmasq[0]", "cachesize", "1") end end c:set("dhcp", "@dnsmasq[0]", "server", { "127.0.0.1#5453" }) -- Ignore ISP's DNS by not reading upstream servers from /etc/resolv.conf c:set("dhcp", "@dnsmasq[0]", "noresolv", "1") -- Ensures that /etc/resolv.conf directs local system processes to use dnsmasq and hence secure c:set("dhcp", "@dnsmasq[0]", "localuse", "1") else mode = "auto" proto = "" end c:set("gl-dns", "@dns[0]", "mode", mode) c:set("gl-dns", "@dns[0]", "proto", proto or "") end c:set("dhcp", "@dnsmasq[0]", "rebind_protection", params.rebind_protection and 1 or 0) c:commit("dhcp") c:commit("gl-dns") c:commit("stubby") if dnscrypt_version == 1 then c:commit("dnscrypt-proxy") end if mode then ngx.pipe.spawn({"/etc/init.d/stubby", "stop"}):wait() ngx.pipe.spawn({"/etc/init.d/dnscrypt-proxy", "stop"}):wait() ngx.pipe.spawn({"/etc/init.d/stubby", "start"}):wait() ngx.pipe.spawn({"/etc/init.d/dnscrypt-proxy", "start"}):wait() end ngx.pipe.spawn({"/etc/init.d/dnsmasq", "restart"}):wait() set_dns_overall(c, params.force_dns) fs.sync() end --[[ @method-name: get_host @method-desc: get host file content @out string content file content @in-example: {"jsonrpc":"2.0","method":"call","params":["","dns","get_host",{}] } @out-example: {"id":null,"jsonrpc":"2.0","result":{"content": "185.199.108.154 github.githubassets.com"}} --]] function M.get_host() local content = utils.readfile("/etc/hosts") return { content = content } end --[[ @method-name: set_host @method-desc: modify host file content @in string content file content @in-example: {"jsonrpc":"2.0","method":"call","params":["","dns","set_host",{"content": "185.199.108.154 github.githubassets.com"}] } @out-example: {"id":null,"jsonrpc":"2.0","result": null} --]] function M.set_host(params) local content = params.content if content:sub(#content) ~= "\n" then content = content .. "\n" end utils.writefile("/etc/hosts", content or "") fs.sync() ngx.pipe.spawn({"/etc/init.d/dnsmasq", "restart"}) end return M