diff --git a/net/dnsdist/Makefile b/net/dnsdist/Makefile index 2688eae1c3dc2..edc3c62e1d0d0 100644 --- a/net/dnsdist/Makefile +++ b/net/dnsdist/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=dnsdist PKG_VERSION:=1.9.8 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.bz2 PKG_SOURCE_URL:=https://downloads.powerdns.com/releases/ @@ -35,7 +35,11 @@ define Package/dnsdist/Default +libatomic \ +libcap \ +libstdcpp \ - @HAS_LUAJIT_ARCH +luajit + +libubox-lua \ + +libubus-lua \ + +libuci-lua \ + @HAS_LUAJIT_ARCH +luajit \ + +luafilesystem URL:=https://dnsdist.org/ VARIANT:=$(1) PROVIDES:=dnsdist @@ -62,11 +66,24 @@ define Package/dnsdist/install/Default $(INSTALL_DIR) $(1)/etc/dnsdist.conf.d $(INSTALL_CONF) ./files/dnsdist.conf $(1)/etc/dnsdist.conf $(INSTALL_DIR) $(1)/etc/config - $(INSTALL_CONF) ./files/dnsdist.config $(1)/etc/config/dnsdist + $(INSTALL_CONF) ./files/sample.uci.conf $(1)/etc/config/dnsdist $(INSTALL_DIR) $(1)/etc/init.d $(INSTALL_BIN) ./files/dnsdist.init $(1)/etc/init.d/dnsdist $(INSTALL_DIR) $(1)/usr/bin $(INSTALL_BIN) $(PKG_BUILD_DIR)/dnsdist $(1)/usr/bin/ + $(INSTALL_DIR) $(1)/usr/share/acl.d + $(INSTALL_DATA) ./files/dnsdist_acl.json $(1)/usr/share/acl.d + $(INSTALL_DIR) $(1)/usr/share/lua/dnsdist + $(INSTALL_DIR) $(1)/usr/share/dnsdist + $(CP) ./files/common.lua $(1)/usr/share/lua/dnsdist/ + $(CP) ./files/configuration.lua $(1)/usr/share/lua/dnsdist/ + $(CP) ./files/local-domains.lua $(1)/usr/share/lua/dnsdist/ + $(CP) ./files/os.lua $(1)/usr/share/lua/dnsdist/ + $(CP) ./files/dnsdist-odhcpd.lua $(1)/usr/share/lua/dnsdist/ + $(CP) ./files/start.lua $(1)/usr/share/lua/dnsdist/ + $(CP) ./files/sample.uci.conf $(1)/usr/share/dnsdist/sample.uci.conf + $(CP) ./files/dnsdist.config $(1)/usr/share/dnsdist/simple.uci.conf + $(INSTALL_BIN) ./files/diag.sh $(1)/usr/share/dnsdist/diag.sh endef define Package/dnsdist diff --git a/net/dnsdist/files/common.lua b/net/dnsdist/files/common.lua new file mode 100644 index 0000000000000..531841aba9118 --- /dev/null +++ b/net/dnsdist/files/common.lua @@ -0,0 +1,55 @@ +local M = {} + +M.poolName = '' +M.interfaceTagName = 'itf' + +local maintenanceHookRegistrations = {} +local configurationParsedHookRegistrations = {} +local configurationDoneHookRegistrations = {} + +local string_find = string.find + +function M.addrIsIPv6(addr) + return string_find(addr, ':') +end + +local function insertIntoTableIfNotExists(tab, value) + for _, v in ipairs(tab) do + if v == value then + return + end + end + table.insert(tab, value) +end + +function M.registerMaintenanceHook(callback) + insertIntoTableIfNotExists(maintenanceHookRegistrations, callback) +end + +function M.registerConfigurationParsedHook(callback) + insertIntoTableIfNotExists(configurationParsedHookRegistrations, callback) +end + +function M.registerConfigurationDoneHook(callback) + insertIntoTableIfNotExists(configurationDoneHookRegistrations, callback) +end + +function M.runMaintenanceHooks() + for _, callback in ipairs(maintenanceHookRegistrations) do + callback() + end +end + +function M.runConfigurationParsedHooks(config, cursor) + for _, callback in ipairs(configurationParsedHookRegistrations) do + callback(config, cursor) + end +end + +function M.runConfigurationDoneHooks(config) + for _, callback in ipairs(configurationDoneHookRegistrations) do + callback(config) + end +end + +return M diff --git a/net/dnsdist/files/configuration.lua b/net/dnsdist/files/configuration.lua new file mode 100644 index 0000000000000..851cc3c06a4d6 --- /dev/null +++ b/net/dnsdist/files/configuration.lua @@ -0,0 +1,827 @@ +local M = {} + +local uci = require 'uci' + +local localDomains = require 'dnsdist/local-domains' +local common = require 'dnsdist/common' +local os = require 'dnsdist/os' + +local configurationCheckInterval = 0 +local configurationCheckInterfacePatterns = {} +local maxPSS = 0 + +function M.getGeneralUCIOption(cursor, key, default) + local value, err = cursor:get('dnsdist', 'general', key) + if err == nil and value ~= nil then + return value + else + return default + end +end + +function M.parseDNSRecordType(recordType) + local idx = string.find(recordType, 'TYPE') + if idx == 1 then + recordType = string.sub(recordType, 5, -1) + return tonumber(recordType) + else + if DNSQType[recordType] ~= nil then + return DNSQType[recordType] + end + end + return nil +end + +local function getTLSMaterial(config) + local cert = config['tls_cert'] + local key = config['tls_key'] + local skip = false + + if config['tls_cert_is_password_protected'] == '1' and config['tls_cert_password'] ~= nil then + key = '' + if os.fileExists(cert) then + cert = newTLSCertificate(config['tls_cert'], { password = config['tls_cert_password'] }) + else + skip = true + end + else + if not os.fileExists(cert) or not os.fileExists(key) then + skip = true + end + end + return cert, key, skip +end + +local function buildSentinelDomainNames() + local sentinelDomainNames = {} + + local cursor = uci.cursor() + cursor:foreach('dnsdist', 'domainlist', function (entry) + local sentinelDomainName = entry['.name'] + local domainList = {} + for _, v in pairs(entry['entry']) do + local name, itf = string.match(v, '([^%s]+)%s([^%s]+)') + if domainList[itf] == nil then + domainList[itf] = {} + end + table.insert(domainList[itf], name) + end + local itfRules = {} + for itfName, config in pairs(domainList) do + local rules = { suffixes = nil, exact = nil } + for _, name in pairs(config) do + if string.sub(name, 1, 1) == '*' then + if rules['suffixes'] == nil then + rules['suffixes'] = newSuffixMatchNode() + end + rules['suffixes']:add(string.sub(name, 3, -1)) + else + if rules['exact'] == nil then + rules['exact'] = newDNSNameSet() + end + rules['exact']:add(newDNSName(name)) + end + end + if rules['suffixes'] == nil and rules['exact'] ~= nil then + itfRules[itfName] = QNameSetRule(rules['exact']) + elseif rules['suffixes'] ~= nil and rules['exact'] == nil then + itfRules[itfName] = SuffixMatchNodeRule(rules['suffixes']) + elseif rules['suffixes'] ~= nil and rules['exact'] ~= nil then + itfRules[itfName] = OrRule({QNameSetRule(rules['exact']), SuffixMatchNodeRule(rules['suffixes'])}) + end + end + sentinelDomainNames[sentinelDomainName] = itfRules + end) + return sentinelDomainNames +end + +local sentinelDomainsTagRules = {} +local function getRuleForSentinelDomain(sentinelDomainsListName, sentinelDomainsNameRule) + local rule = sentinelDomainsTagRules[sentinelDomainsListName] + if rule == nil then + local tagName = 'stnl-' .. sentinelDomainsListName + addAction(sentinelDomainsNameRule, SetTagAction(tagName, 'true')) + rule = TagRule(tagName) + sentinelDomainsTagRules[sentinelDomainsListName] = rule + end + return rule +end + +function M.isInterfaceEnabled(includePatterns, excludePatterns, interfaceName) + if #includePatterns > 0 then + local enabled = false + for _, pattern in ipairs(includePatterns) do + if string.match(interfaceName, pattern) ~= nil then + enabled = true + break + end + end + if not enabled then + return false + end + end + + if #excludePatterns > 0 then + for _, pattern in ipairs(excludePatterns) do + if string.match(interfaceName, pattern) ~= nil then + return false + end + end + end + + return true +end + +function M.isBridge() + local cursor = uci.cursor() + local value, err = cursor:get("network", "wan", "type") + if err == nil and value == "bridge" then + return true + else + return false + end +end + +function M.enabled(config) + if config['enabled'] == '0' then + return false + end + return true +end + +function M.apply(config) + if not M.enabled(config) then + return + end + + -- logging + if config['verbose_mode'] == 1 then + setVerbose(true) + if config['verbose_log_destination'] ~= nil and #config['verbose_log_destination'] > 0 then + vinfolog('Directing verbose-level log messages to '..config['verbose_log_destination']) + setVerboseLogDestination(config['verbose_log_destination']) + end + end + + -- cache + if config['domain_cache_size'] ~= nil and tonumber(config['domain_cache_size']) > 0 then + + setCacheCleaningDelay(tonumber(config['domain_cleanup_interval'])) + + local cacheOptions = { maxTTL = tonumber(config['domain_ttl_cap']) } + local pc = newPacketCache(tonumber(config['domain_cache_size']), cacheOptions) + + getPool(common.poolName):setCache(pc) + if config['auto_upgraded_backends_pool'] ~= nil then + getPool(config['auto_upgraded_backends_pool']):setCache(pc) + end + else + -- make sure that the upgraded backend pool is always created, even when the cache is disabled + if config['auto_upgraded_backends_pool'] ~= nil then + getPool(config['auto_upgraded_backends_pool']) + end + end + + -- web server + if config['api-port'] ~= nil then + webserver("127.0.0.1:"..tonumber(config['api-port'])) + setWebserverConfig({apiRequiresAuthentication=false, acl="127.0.0.0/8"}) + end + + -- we need to retain the CAP_NET_RAW capability to be able to use a + -- specific source interface for our backends + local capabilitiesToRetain = {} + local serversCount = 0 + for _, v in pairs(config['servers']) do + serversCount = serversCount + 1 + end + local serverIdx = 0 + for _, v in pairs(config['servers']) do + if serverIdx >= config['max_upstream_resolvers'] then + warnlog('Skipping upstream resolver '..v['address']..' because we already have '..config['max_upstream_resolvers']..' resolvers') + else + serverIdx = serverIdx + 1 + if v['source'] ~= nil then + table.insert(capabilitiesToRetain, 'CAP_NET_RAW') + end + -- if we have only one upstream resolver, mark it UP as we have no other + -- choice anyway + if serversCount == 1 then + infolog("Marking the only downstream server as 'UP'") + v['healthCheckMode'] = 'up' + end + newServer(v) + end + end + + if #capabilitiesToRetain > 0 then + addCapabilitiesToRetain(capabilitiesToRetain) + end + + -- network interfaces + local interfaces = getListOfNetworkInterfaces() + + local localDomainsDests = {} + local allAddresses = {} + + local ACL = { + '127.0.0.0/8', + '::1/128', + } + + local sentinelRules = buildSentinelDomainNames() + local interfaceRules = {} + for _, itf in ipairs(interfaces) do + if M.isInterfaceEnabled(config['interfaces-include'], config['interfaces-exclude'], itf) then + local conf = config['interfaces'][itf] + if conf == nil then + conf = config['interfaces']['default_interface'] + end + + if conf ~= nil then + if conf['enabled'] == '1' then + local interfaceDests = newNMG() + local mainAddr = nil + local additionalAddrs = {} + local itfAddrs = {} + local v4Addrs = {} + local v6Addrs = {} + local addresses = getListOfAddressesOfNetworkInterface(itf) + + for _,v in ipairs(getListOfRangesOfNetworkInterface(itf)) do + table.insert(ACL, v) + end + + for _, addr in ipairs(addresses) do + if common.addrIsIPv6(addr) and string.sub(addr, 1, 1) ~= '[' then + addr = '['..addr..']' + if mainAddr == nil then + mainAddr = addr + else + table.insert(additionalAddrs, addr) + end + table.insert(v6Addrs, addr) + else + if mainAddr == nil then + mainAddr = addr + else + table.insert(additionalAddrs, addr) + end + table.insert(v4Addrs, addr) + end + table.insert(itfAddrs, addr) + table.insert(allAddresses, addr) + + if conf['do53'] == '1' then + local parameters = { + maxConcurrentTCPConnections = tonumber(config['concurrent_incoming_connections_per_device']) + } + addLocal(addr..':'..config['do53_port'], parameters) + end + + interfaceDests:addMask(addr) + end + + if interfaceDests:size() > 0 then + local itfNMGRule = NetmaskGroupRule(interfaceDests, false) + addAction(itfNMGRule, SetTagAction(common.interfaceTagName, itf)) + interfaceRules[itf] = TagRule(common.interfaceTagName, itf) + end + + if conf['local_resolution'] == '1' then + table.insert(localDomainsDests, itf) + end + + if conf['dot'] == '1' and mainAddr ~= nil then + -- we set maxConcurrentTCPConnections here but see also + -- setMaxTCPConnectionsPerClient() below for TCP/DoT + local dotParameters = { + ignoreTLSConfigurationErrors = true, + maxConcurrentTCPConnections = tonumber(config['concurrent_incoming_connections_per_device']), + numberOfStoredSessions = 0, + minTLSVersion = config['tls_min_version'], + ciphers = config['tls_ciphers_incoming'], + ciphersTLS13 = config['tls_ciphers13_incoming'] + } + if next(additionalAddrs) ~= nil then + dotParameters['additionalAddresses'] = {} + for _, v in ipairs(additionalAddrs) do + table.insert(dotParameters['additionalAddresses'], v..':'..config['dot_port']) + end + end + + local cert, key, skip = getTLSMaterial(config) + if not skip then + addTLSLocal(mainAddr..':'..config['dot_port'], cert, key, dotParameters) + else + warnlog('Not listening for DoT queries on '..mainAddr..':'..config['dot_port']..' because the certificate or key is missing') + end + end + + if conf['doh'] == '1' and mainAddr ~= nil then + local dohParameters = { + ignoreTLSConfigurationErrors = true, + maxConcurrentTCPConnections = tonumber(config['concurrent_incoming_connections_per_device']), + numberOfStoredSessions = 0, + internalPipeBufferSize = 0, + minTLSVersion = config['tls_min_version'], + ciphers = config['tls_ciphers_incoming'], + ciphersTLS13 = config['tls_ciphers13_incoming'] + } + if next(additionalAddrs) ~= nil then + dohParameters['additionalAddresses'] = {} + for _, v in ipairs(additionalAddrs) do + table.insert(dohParameters['additionalAddresses'], v..':'..config['doh_port']) + end + end + + local cert, key, skip = getTLSMaterial(config) + if not skip then + addDOHLocal(mainAddr..':'..config['doh_port'], cert, key, '/dns-query', dohParameters) + else + warnlog('Not listening for DoH queries on '..mainAddr..':'..config['doh_port']..' because the certificate or key is missing') + end + end + + if interfaceDests:size() > 0 and (conf['sentinel_domains'] ~= nil or conf['advertise'] == '1') then + -- sentinel domains + if conf['sentinel_domains'] ~= nil then + local sentinelItfs = sentinelRules[conf['sentinel_domains']] + for sentinelItf, sentinelDomainsRule in pairs(sentinelItfs) do + local addresses = getListOfAddressesOfNetworkInterface(sentinelItf) + local addressesList = {} + for _, addr in ipairs(addresses) do + if common.addrIsIPv6(addr) then + if string.find(addr, '%%') == nil then + if string.sub(addr, 1, 1) ~= '[' then + addr = '['..addr..']' + end + table.insert(addressesList, addr) + end + else + table.insert(addressesList, addr) + end + end + if #addressesList > 0 then + local sentinelDomainsTagRule = getRuleForSentinelDomain(conf['sentinel_domains'], sentinelDomainsRule) + addAction(AndRule{interfaceRules[itf], sentinelDomainsTagRule}, SpoofAction(addressesList, { ttl=config['sentinel_domains_ttl'] })) + end + end + end + + -- Advertise DoT /DoH via SVCB + if conf['advertise'] == '1' then + local namedResolver = config['advertise_for_domain_name'] + local targetName = '_dns.resolver.arpa.' + if namedResolver ~= nil and #namedResolver > 0 then + targetName = namedResolver + namedResolver = '_dns.'..namedResolver + end + local svc = {} + if conf['dot'] == '1' then + table.insert(svc, newSVCRecordParameters(1, targetName, { mandatory={"port"}, alpn={ "dot" }, noDefaultAlpn=true, port=config['dot_port'], ipv4hint=v4Addrs, ipv6hint=v6Addrs })) + end + if conf['doh'] == '1' then + table.insert(svc, newSVCRecordParameters(2, targetName, { mandatory={"port"}, alpn={ "h2" }, port=config['doh_port'], ipv4hint=v4Addrs, ipv6hint=v6Addrs, key7='/dns-query' })) + end + if #svc > 0 then + local nameRule = nil + if namedResolver ~= nil and #namedResolver > 0 then + nameRule = OrRule{QNameRule('_dns.resolver.arpa.'), QNameRule(namedResolver)} + else + nameRule = QNameRule('_dns.resolver.arpa.') + end + addAction(AndRule{QTypeRule(DNSQType.SVCB), interfaceRules[itf], nameRule}, SpoofSVCAction(svc)) + if config['advertise_for_domain_name'] ~= nil and #config['advertise_for_domain_name'] > 0 then + -- basically an automatic sentinel domain rule + addAction(AndRule{interfaceRules[itf], QNameRule(config['advertise_for_domain_name'])}, SpoofAction(itfAddrs, { ttl=config['sentinel_domains_ttl'] })) + end + -- reply with NODATA (NXDOMAIN would deny all types at that name and below, including SVC) for other types + -- but only for _dns.resolver.arpa., the advertised name might have other types + addAction(AndRule{interfaceRules[itf], QNameRule('_dns.resolver.arpa.')}, NegativeAndSOAAction(false, '_dns.resolver.arpa.', 3600, 'fake.resolver.arpa.', 'fake.resolver.arpa.', 1, 1800, 900, 604800, 86400)) + end + end + end + end + end + end + end + + -- allow queries from all subnets on chosen interfaces + setACL(ACL) + + -- tuning + if config['concurrent_incoming_connections_per_device'] ~= nil then + setMaxTCPConnectionsPerClient(tonumber(config['concurrent_incoming_connections_per_device'])) + end + if config['max_idle_doh_connections_per_downstream'] ~= nil then + setMaxIdleDoHConnectionsPerDownstream(tonumber(config['max_idle_doh_connections_per_downstream'])) + end + if config['max_idle_tcp_connections_per_downstream'] ~= nil then + setMaxCachedTCPConnectionsPerDownstream(tonumber(config['max_idle_tcp_connections_per_downstream'])) + end + + -- local domains interfaces + localDomains.destinations = localDomainsDests + for suffix in string.gmatch(config['local_domains_suffix'], '([^%s]+)') do + table.insert(localDomains.lanSuffixes, suffix) + end + localDomains.ttl = tonumber(config['local_domains_ttl']) + + -- watch for configuration changes + configurationCheckInterval = config['configuration-check-interval'] + + if configurationCheckInterval > 0 then + configurationCheckInterfacePatterns['lan-interfaces-include'] = config['interfaces-include'] + configurationCheckInterfacePatterns['lan-interfaces-exclude'] = config['interfaces-exclude'] + configurationCheckInterfacePatterns['wan-interfaces-include'] = config['wan-interfaces-include'] + end + + -- watch for maximum memory usage + maxPSS = config['max_pss'] + + -- route queries to a DoT / DoH backend, if available, and to the default pool otherwise + if config['auto_upgraded_backends_pool'] ~= nil then + addAction(PoolAvailableRule(config['auto_upgraded_backends_pool']), ContinueAction(PoolAction(config['auto_upgraded_backends_pool']))) + end + + common.registerMaintenanceHook(M.maintenance) +end + +function M.loadFromUCI() + local config = {} + local cursor = uci.cursor() + + -- Load these values even if dnsdist is not enabled, + -- we need them to know how often to check if that changed + config['configuration-check-interval'] = tonumber(M.getGeneralUCIOption(cursor, 'configuration_check_interval', 60)) + + config['enabled'] = M.getGeneralUCIOption(cursor, 'enabled', '0') + if config['enabled'] == '0' then + return config + end + + config['do53_port'] = M.getGeneralUCIOption(cursor, 'do53_port', 53) + config['dot_port'] = M.getGeneralUCIOption(cursor, 'dot_port', 853) + config['doh_port'] = M.getGeneralUCIOption(cursor, 'doh_port', 443) + config['tls_cert'] = M.getGeneralUCIOption(cursor, 'tls_cert', '') + config['tls_key'] = M.getGeneralUCIOption(cursor, 'tls_key', '') + config['tls_cert_is_password_protected'] = M.getGeneralUCIOption(cursor, 'tls_cert_is_password_protected', '0') + config['tls_ciphers_incoming'] = M.getGeneralUCIOption(cursor, 'tls_ciphers_incoming', '') + config['tls_ciphers13_incoming'] = M.getGeneralUCIOption(cursor, 'tls_ciphers13_incoming', '') + config['tls_ciphers_outgoing'] = M.getGeneralUCIOption(cursor, 'tls_ciphers_outgoing', '') + config['tls_ciphers13_outgoing'] = M.getGeneralUCIOption(cursor, 'tls_ciphers13_outgoing', '') + config['concurrent_incoming_connections_per_device'] = tonumber(M.getGeneralUCIOption(cursor, 'concurrent_incoming_connections_per_device', '10')) + config['default_check_interval'] = tonumber(M.getGeneralUCIOption(cursor, 'default_check_interval', '5')) + config['default_check_timeout'] = tonumber(M.getGeneralUCIOption(cursor, 'default_check_timeout', '1000')) + config['default_max_check_failures'] = tonumber(M.getGeneralUCIOption(cursor, 'default_max_check_failures', '2')) + config['default_max_upstream_concurrent_tcp_connections'] = tonumber(M.getGeneralUCIOption(cursor, 'default_max_upstream_concurrent_tcp_connections', '0')) + config['max_idle_tcp_connections_per_downstream'] = tonumber(M.getGeneralUCIOption(cursor, 'max_idle_tcp_connections_per_downstream', '2')) + config['max_idle_doh_connections_per_downstream'] = tonumber(M.getGeneralUCIOption(cursor, 'max_idle_doh_connections_per_downstream', '2')) + config['outgoing_udp_sockets_per_downstream'] = tonumber(M.getGeneralUCIOption(cursor, 'outgoing_udp_sockets_per_downstream', '100')) + config['max_upstream_resolvers'] = tonumber(M.getGeneralUCIOption(cursor, 'max_upstream_resolvers', '5')) + + -- lazy health-checks + config['health_checks_sample_size'] = tonumber(M.getGeneralUCIOption(cursor, 'health_checks_sample_size', 100)) + config['health_checks_min_sample_count'] = tonumber(M.getGeneralUCIOption(cursor, 'health_checks_min_sample_count', 10)) + config['health_checks_threshold'] = tonumber(M.getGeneralUCIOption(cursor, 'health_checks_threshold', 20)) + config['health_checks_failed_interval']= tonumber(M.getGeneralUCIOption(cursor, 'health_checks_failed_interval', 30)) + config['health_checks_mode']= M.getGeneralUCIOption(cursor, 'health_checks_mode', 'TimeoutOrServFail') + config['health_checks_exponential_backoff']= tonumber(M.getGeneralUCIOption(cursor, 'health_checks_exponential_backoff', 1)) + config['health_checks_max_backoff']= tonumber(M.getGeneralUCIOption(cursor, 'health_checks_max_backoff', 3600)) + + -- pss + config['max_pss'] = tonumber(M.getGeneralUCIOption(cursor, 'max_pss', 0)) + + -- logging + config['verbose_mode'] = tonumber(M.getGeneralUCIOption(cursor, 'verbose_mode', 0)) + config['verbose_log_destination'] = M.getGeneralUCIOption(cursor, 'verbose_log_destination', '') + + -- enable verbose logging very early if necessary + -- we'll apply verbose_log_destination after startup + if config['verbose_mode'] == 1 then + setVerbose(true) + end + + -- cache + config['domain_cache_size'] = M.getGeneralUCIOption(cursor, 'domain_cache_size', '100') + config['domain_ttl_cap'] = M.getGeneralUCIOption(cursor, 'domain_ttl_cap', '600') + config['domain_cleanup_interval'] = M.getGeneralUCIOption(cursor, 'domain_cleanup_interval', '60') + + -- DoT / DoH advertisement + config['advertise_for_domain_name'] = M.getGeneralUCIOption(cursor, 'advertise_for_domain_name', '') + + -- auto upgrade of discovered servers + config['auto_upgrade_discovered_backends'] = M.getGeneralUCIOption(cursor, 'auto_upgrade_discovered_backends', '0') + config['keep_auto_upgraded_backends'] = M.getGeneralUCIOption(cursor, 'keep_auto_upgraded_backends', '1') + config['auto_upgraded_backends_pool'] = M.getGeneralUCIOption(cursor, 'auto_upgraded_backends_pool', 'upgraded-to-dox') + + config['local_domains_suffix'] = M.getGeneralUCIOption(cursor, 'local_domains_suffix', 'lan') + config['local_domains_ttl'] = tonumber(M.getGeneralUCIOption(cursor, 'local_domains_ttl', '1')) + config['sentinel_domains_ttl'] = tonumber(M.getGeneralUCIOption(cursor, 'sentinel_domains_ttl', '60')) + + -- web server + config['api-port'] = M.getGeneralUCIOption(cursor, 'web_server_port', '9080') + + config['servers'] = {} + cursor:foreach('dnsdist', 'server', function (entry) + local server = { + healthCheckMode = 'lazy', + lazyHealthCheckSampleSize = config['health_checks_sample_size'], + lazyHealthCheckMinSampleCount = config['health_checks_min_sample_count'], + lazyHealthCheckThreshold = config['health_checks_threshold'], + lazyHealthCheckFailedInterval = config['health_checks_failed_interval'], + lazyHealthCheckMode = config['health_checks_mode'], + lazyHealthCheckUseExponentialBackOff = (config['health_checks_exponential_backoff'] == 1), + lazyHealthCheckMaxBackOff = config['health_checks_max_backoff'], + lazyHealthCheckWhenUpgraded = true + } + local invalid = false + server['name'] = entry['.name'] + if entry['adn'] ~= nil then + server['subjectAltName'] = entry['adn'] + end + local type = entry['type'] + if type == 'doh' or type == 'dot' then + server['tls'] = 'openssl' + end + if entry['port'] ~= nil then + server['address'] = entry['addr']..':'..entry['port'] + else + server['address'] = entry['addr']..':53' + end + if entry['upstreamWANInterface'] ~= nil and #entry['upstreamWANInterface'] > 0 then + local itfAddrs = getListOfAddressesOfNetworkInterface(entry['upstreamWANInterface']) + if #itfAddrs > 0 then + infolog('Setting WAN interface affinity for upstream resolver '..server['name']..' to '..entry['upstreamWANInterface']) + server['source'] = entry['upstreamWANInterface'] + else + errlog('Discarding upstream resolver '..server['name']..' because the requested interface affinity ('..entry['upstreamWANInterface']..') cannot be satisfied') + invalid = true + end + end + if entry['path'] then + server['dohPath'] = entry['path'] + end + if entry['validate'] == '0' then + server['validateCertificates'] = false + end + if entry['maxInFlight'] ~= nil then + server['maxInFlight'] = entry['maxInFlight'] + end + if entry['maxConcurrentTCPConnections'] ~= nil then + server['maxConcurrentTCPConnections'] = entry['maxConcurrentTCPConnections'] + else + server['maxConcurrentTCPConnections'] = config['default_max_upstream_concurrent_tcp_connections'] + end + if entry['maxCheckFailures'] ~= nil then + server['maxCheckFailures'] = entry['maxCheckFailures'] + else + server['maxCheckFailures'] = config['default_max_check_failures'] + end + if entry['checkInterval'] ~= nil then + server['checkInterval'] = entry['checkInterval'] + else + server['checkInterval'] = config['default_check_interval'] + end + if entry['checkTimeout'] ~= nil then + server['checkTimeout'] = entry['checkTimeout'] + else + server['checkTimeout'] = config['default_check_timeout'] + end + if config['tls_ciphers_outgoing'] ~= nil and config['tls_ciphers_outgoing'] then + server['ciphers'] = config['tls_ciphers_outgoing'] + end + if config['tls_ciphers13_outgoing'] ~= nil and config['tls_ciphers13_outgoing'] then + server['ciphers13'] = config['tls_ciphers13_outgoing'] + end + if config['outgoing_udp_sockets_per_downstream'] ~= nil then + server['sockets'] = tonumber(config['outgoing_udp_sockets_per_downstream']) + end + if entry['autoUpgrade'] ~= nil then + if entry['autoUpgrade'] == true or entry['autoUpgrade'] == '1' then + entry['autoUpgrade'] = true + elseif entry['autoUpgrade'] == '0' then + entry['autoUpgrade'] = false + end + server['autoUpgrade'] = entry['autoUpgrade'] + if entry['autoUpgradeInterval'] ~= nil then + server['autoUpgradeInterval'] = tonumber(entry['autoUpgradeInterval']) + end + if entry['autoUpgradeKeep'] ~= nil then + server['autoUpgradeKeep'] = entry['autoUpgradeKeep'] + end + if entry['autoUpgradePool'] ~= nil then + server['autoUpgradePool'] = entry['autoUpgradePool'] + end + if entry['autoUpgradeDoHKey'] ~= nil then + server['autoUpgradeDoHKey'] = tonumber(entry['autoUpgradeDoHKey']) + end + end + + if not invalid then + config['servers'][server['name']] = server + end + end) + + if next(config['servers']) == nil then + -- no servers found in the configuration, let's see if we learned something from DHCP + local resolvConfFile = '/tmp/resolv.conf.d/resolv.conf.auto' + if not os.fileExists(resolvConfFile) then + resolvConfFile = '/tmp/resolv.conf.auto' + end + + if os.fileExists(resolvConfFile) then + vinfolog("Reading upstream resolvers from "..resolvConfFile..":") + vinfolog(string.format('[[%q]]', io.open(resolvConfFile):read('a*'))) + else + vinfolog("Not reading upstream resolvers from "..resolvConfFile.." as the file does not exist") + end + + local resolvers = getResolvers(resolvConfFile) + local numresolvers = 0 + for _, resolver in ipairs(resolvers) do + local server = { + healthCheckMode = 'lazy', + lazyHealthCheckSampleSize = config['health_checks_sample_size'], + lazyHealthCheckMinSampleCount = config['health_checks_min_sample_count'], + lazyHealthCheckThreshold = config['health_checks_threshold'], + lazyHealthCheckFailedInterval = config['health_checks_failed_interval'], + lazyHealthCheckMode = config['health_checks_mode'], + lazyHealthCheckUseExponentialBackOff = (config['health_checks_exponential_backoff'] == 1), + lazyHealthCheckMaxBackOff = config['health_checks_max_backoff'], + lazyHealthCheckWhenUpgraded = true + } + server['address'] = resolver + if config['auto_upgrade_discovered_backends'] == '1' then + server['autoUpgrade'] = true + if config['keep_auto_upgraded_backends'] == '1' then + server['autoUpgradeKeep'] = true + end + if config['auto_upgraded_backends_pool'] ~= nil then + server['autoUpgradePool'] = config['auto_upgraded_backends_pool'] + end + -- these settings will be inherited in case of DoT/DoH upgrade + if config['tls_ciphers_outgoing'] ~= nil and config['tls_ciphers_outgoing'] then + server['ciphers'] = config['tls_ciphers_outgoing'] + end + if config['tls_ciphers13_outgoing'] ~= nil and config['tls_ciphers13_outgoing'] then + server['ciphers13'] = config['tls_ciphers13_outgoing'] + end + end + if config['default_max_upstream_concurrent_tcp_connections'] ~= nil then + server['maxConcurrentTCPConnections'] = config['default_max_upstream_concurrent_tcp_connections'] + end + if config['default_max_check_failures'] ~= nil then + server['maxCheckFailures'] = config['default_max_check_failures'] + end + if config['default_check_interval'] ~= nil then + server['checkInterval'] = config['default_check_interval'] + end + if config['default_check_timeout'] ~= nil then + server['checkTimeout'] = config['default_check_timeout'] + end + if config['outgoing_udp_sockets_per_downstream'] ~= nil then + server['sockets'] = tonumber(config['outgoing_udp_sockets_per_downstream']) + end + + config['servers'][resolver] = server + numresolvers = numresolvers + 1 + end + + infolog(string.format("Read %s, learned %s upstream resolvers", resolvConfFile, numresolvers)) + end + + config['interfaces'] = {} + cursor:foreach('dnsdist', 'interface', function (itf) + local conf = {} + local name = itf['name'] + if name ~= nil then + conf['enabled'] = itf['enabled'] + conf['do53'] = itf['do53'] + conf['dot'] = itf['dot'] + conf['doh'] = itf['doh'] + conf['advertise'] = itf['advertise'] + conf['policy_engine'] = itf['policy_engine'] + conf['local_resolution'] = itf['local_resolution'] + conf['sentinel_domains'] = itf['sentinel_domains'] + config['interfaces'][name] = conf + end + end) + + config['interfaces-include'] = {} + config['interfaces-exclude'] = {} + for pattern in string.gmatch(M.getGeneralUCIOption(cursor, 'network_interface_include', ''), '([^%s]+)') do + table.insert(config['interfaces-include'], pattern) + end + for pattern in string.gmatch(M.getGeneralUCIOption(cursor, 'network_interface_exclude', ''), '([^%s]+)') do + table.insert(config['interfaces-exclude'], pattern) + end + config['wan-interfaces-include'] = {} + for pattern in string.gmatch(M.getGeneralUCIOption(cursor, 'network_interface_wan_include', ''), '([^%s]+)') do + table.insert(config['wan-interfaces-include'], pattern) + end + + collectgarbage() + common.runConfigurationParsedHooks(config, cursor) + collectgarbage() + + return config +end + +local nextConfigurationCheck = 0 +local lastConfigurationModificationTime = 0 +local lastNetworkConfiguration = nil + +local function isInterfaceWatched(interfaceName) + if #configurationCheckInterfacePatterns['wan-interfaces-include'] > 0 then + for _, pattern in ipairs(configurationCheckInterfacePatterns['wan-interfaces-include']) do + if string.match(interfaceName, pattern) ~= nil then + return true + end + end + end + + return M.isInterfaceEnabled(configurationCheckInterfacePatterns['lan-interfaces-include'], configurationCheckInterfacePatterns['lan-interfaces-exclude'], interfaceName) +end + +local function configurationChanged() + local mtime = os.getFileModificationTime('/etc/config/dnsdist') + if mtime ~= 0 then + if lastConfigurationModificationTime ~= 0 and lastConfigurationModificationTime ~= mtime then + return true + end + lastConfigurationModificationTime = mtime + end + + local networkConfiguration = {} + local interfaces = getListOfNetworkInterfaces() + for _, itf in ipairs(interfaces) do + if isInterfaceWatched(itf) then + local addresses = getListOfAddressesOfNetworkInterface(itf) + networkConfiguration[itf] = addresses + end + end + + if lastNetworkConfiguration ~= nil then + for itf, addresses in pairs(lastNetworkConfiguration) do + local newAddresses = networkConfiguration[itf] + -- if the interface had no addresses, we do not care + if #addresses > 0 then + if newAddresses == nil or #newAddresses ~= #addresses then + infolog('Addresses changed on interface '..itf) + return true + end + local found = false + -- we have the same number of addresses, so if all the existing ones + -- are still there we should be fine + for _, addr in ipairs(addresses) do + for _, newAddr in ipairs(newAddresses) do + if addr == newAddr then + found = true + end + end + if not found then + infolog('Addresses changed on interface '..itf) + return true + end + end + elseif newAddresses ~= nil and #newAddresses > 0 then + infolog('Addresses changed on interface '..itf) + return true + end + end + -- now make sure that we do not have a new interface with + -- addresses + for itf, addresses in pairs(networkConfiguration) do + local oldAddresses = lastNetworkConfiguration[itf] + -- if the interface has no addresses, we do not care + if oldAddresses == nil and #addresses > 0 then + infolog('New interface with addresses: '..itf) + return true + end + end + end + + lastNetworkConfiguration = networkConfiguration + return false +end + +function M.maintenance() + if configurationCheckInterval > 0 and os.time() >= nextConfigurationCheck then + if configurationChanged() then + warnlog("Configuration has been modified, exiting") + os.exit(1) + end + if maxPSS > 0 then + local currentPSS = os.getPSS() + if currentPSS > maxPSS then + warnlog("The maximum PSS value has been reached ("..currentPSS.." / "..maxPSS.."), exiting") + os.exit(1) + end + end + nextConfigurationCheck = os.time() + configurationCheckInterval + end +end + +return M diff --git a/net/dnsdist/files/diag.sh b/net/dnsdist/files/diag.sh new file mode 100755 index 0000000000000..e886014cc7be2 --- /dev/null +++ b/net/dnsdist/files/diag.sh @@ -0,0 +1,38 @@ +#!/bin/sh +DIR=$(mktemp -d) + +[ -d $DIR ] || exit 1 + +cd $DIR + +logread > $DIR/logread.txt +wget -q -O dnsdist.statistics.txt http://127.0.0.1:9080/api/v1/servers/localhost +PIDS=$(pidof dnsdist) + +echo "dnsdist pids: $PIDS" > dnsdist.pids.txt +cp /etc/dnsdist.conf dnsdist.conf.txt +cp /etc/config/dnsdist dnsdist.uci.txt + +ps > ps.txt + +dmesg > dmesg.txt + +netstat -rn > netstat-rn.txt + +netstat -pan > netstat-pan.txt + +for pid in $(pidof dnsdist) +do + ls -al /proc/$pid/fd > dnsdist.pid.$pid.fd.txt + for f in limits maps smaps smaps_rollup stat statm status + do + [ -e /proc/$pid/$f ] && cat /proc/$pid/$f > dnsdist.pid.$pid.$f.txt + done +done + +TARF=$DIR/dnsdist.diagnostics.$(date +%s).tar + +tar -cf $TARF *.txt +rm -f *.txt + +echo Diagnostics stored as $TARF diff --git a/net/dnsdist/files/dnsdist-odhcpd.lua b/net/dnsdist/files/dnsdist-odhcpd.lua new file mode 100644 index 0000000000000..ff2766763a5dd --- /dev/null +++ b/net/dnsdist/files/dnsdist-odhcpd.lua @@ -0,0 +1,190 @@ +if not submitToMainThread then + -- stub for testing outside of dnsdist + function submitToMainThread(cmd, data) + print("cmd",cmd) + for k,v in pairs(data) do + print(k,v) + end + end +end + +-- # ubus call dhcp ipv4leases +-- { +-- "device": { +-- "br-lan": { +-- "leases": [ +-- { +-- "mac": "dca632cd93a6", +-- "hostname": "archimedes", +-- "accept-reconf-nonce": false, +-- "reqopts": "1,2,6,12,15,26,28,121,3,33,40,41,42,119,249,252,17", +-- "flags": [ +-- "bound" +-- ], +-- "address": "192.168.52.124", +-- "valid": 43160 +-- } +-- ] +-- } +-- } +-- } + +-- # ubus call dhcp ipv6leases +-- { +-- "device": { +-- "br-lan": { +-- "leases": [ +-- { +-- "duid": "00030001dca632c0d5f0", +-- "iaid": 0, +-- "hostname": "", +-- "accept-reconf": false, +-- "assigned": 2589, +-- "flags": [ +-- "bound" +-- ], +-- "ipv6-addr": [ +-- { +-- "address": "fdc0:b385:de66::a1d", +-- "preferred-lifetime": -1, +-- "valid-lifetime": -1 +-- } +-- ], +-- "valid": 56 +-- } +-- ] +-- } +-- } +-- } + + +local ubus = require 'ubus' -- opkg install libubus-lua +local uloop = require 'uloop' -- opkg install libubox-lua + +uloop.init() + +local conn = ubus.connect() +if not conn then + error("Failed to connect to ubusd") +end + +local byName4 = {} -- key: hostname. value: {ip=.., expire=..} - expire is a timestamp, not a TTL +local byIP4 = {} -- key: IP. value: hostname as string. + +local byName6 = {} -- key: hostname. value: {ip=.., expire=..} - expire is a timestamp, not a TTL +local byIP6 = {} -- key: IP. value: hostname as string. + +local leasetime = 12*3600 -- TODO: fetch from uci + +local now = os.time() + +local function send(cmd, name, ip, proto) + submitToMainThread(cmd, {ip=ip, name=name, proto=proto}) +end + +local function delEntry(name, ip, proto, byName, byIP) + byName[name] = nil + byIP[ip] = nil + send('del', name, ip, proto) +end + +local function setEntry(name, ip, ttl, proto, byName, byIP) + now = os.time() + if byName[name] and byName[name].ip == ip then + -- just update the expiry + byName[name].expire = now + ttl + return + end + + if byName[name] then + -- the name exists, but with the wrong IP + delEntry(name, byName[name].ip, proto, byName, byIP) + end + + if byIP[ip] then + -- the IP exists, but with the wrong name + delEntry(byIP[ip], ip, proto, byName, byIP) + end + + -- we are ready to register the entry + + byName[name] = { ip=ip, expire=now+ttl} + byIP[ip] = name + send('add', name, ip, proto) +end + +local function handleEvent(msg, name) + -- { "dhcp.ack": {"mac":"dc:a6:32:cd:93:a6","ip":"192.168.52.124","name":"archimedes","interface":"br-lan"} } + -- odhcpd only sends v4 events + if name == 'dhcp.ack' and #msg.name > 0 then + setEntry(msg.name, msg.ip, leasetime, 'v4', byName4, byIP4) + end +end + +local function _getLeases(cmd, proto) + local status = conn:call("dhcp", cmd, {}) + + for _,intfv in pairs(status.device) do + for _,lease in ipairs(intfv.leases) do + if #lease.hostname > 0 + then + if proto == 'v4' then + setEntry(lease.hostname, lease.address, lease.valid, proto, byName4, byIP4) + elseif proto == 'v6' and lease['ipv6-addr'] ~= nil then + setEntry(lease.hostname, lease['ipv6-addr'][1].address, lease.valid, proto, byName6, byIP6) + end + end + end + end +end + +local function getLeases() + _getLeases("ipv4leases", 'v4') + _getLeases("ipv6leases", 'v6') +end + +local function _expireLeases(proto, byName, byIP) + now = os.time() + + for k,v in pairs(byName) do + if v.expire < now then + delEntry(k, v.ip, proto, byName, byIP) + end + end +end + +local function expireLeases() + _expireLeases('v4', byName4, byIP4) + _expireLeases('v6', byName6, byIP6) +end + +local interval = 60*1000 -- milliseconds + +local timer +local function maintenance() + -- print("maint") + getLeases() + expireLeases() + timer:set(interval) + collectgarbage() + collectgarbage() +end +timer = uloop.timer(maintenance) +timer:set(interval) + +local sub = { + notify = function(msg,name) + handleEvent(msg, name) + end, +} + +conn:subscribe("dhcp", sub) + +getLeases() + +uloop.run() + +-- Close connection +conn:close() +collectgarbage() +collectgarbage() diff --git a/net/dnsdist/files/dnsdist.conf b/net/dnsdist/files/dnsdist.conf index e69de29bb2d1d..dd43415a0cfa4 100644 --- a/net/dnsdist/files/dnsdist.conf +++ b/net/dnsdist/files/dnsdist.conf @@ -0,0 +1 @@ +dofile('/usr/share/lua/dnsdist/start.lua') diff --git a/net/dnsdist/files/dnsdist.config b/net/dnsdist/files/dnsdist.config index be0c93b1c2162..905fd132c9dc3 100644 --- a/net/dnsdist/files/dnsdist.config +++ b/net/dnsdist/files/dnsdist.config @@ -1,4 +1,149 @@ config 'dnsdist' 'general' - option enabled '0' - option user 'root' - option group 'root' + option 'enabled' '0' + # Number of entries in the domain cache + option 'domain_cache_size' '100' + # Cap the TTL of cached records to the supplied number of seconds. Default is 600 (10 minutes), minimum is 1, useful range is up to 86400. + # Setting this value to the highest possible value for a TTL, 2^32-1, means that TTLs will not be capped. + option 'domain_ttl_cap' '600' + # Set how often the cache is scanned to expunge expired entries, in seconds. Default is 60s (every minute), minimum value is 0, useful range is up to 86400 + option 'domain_cleanup_interval' '60' + # The TTL used for local domain responses. The values allowed by the DNS protocol range from 1 to 2^31-1 + option 'local_domains_ttl' '1' + # The DNS suffix, or list of suffixes, identifying local domain names + option 'local_domains_suffix' 'lan' + # The TTL used for sentinel domains responses + option 'sentinel_domains_ttl' '60' + # Number of concurrent connections per client device. 0 means unlimited, the maximum value is 2^64-1. + option 'concurrent_incoming_connections_per_device' '10' + # The DNS engine will consider to accept queries on interfaces whose names match this pattern, + # or list of patterns. When a list of patterns is provided, interfaces matching any pattern will + # be considered. If this value is left empty, the DNS engine will consider all interfaces. + # The interface names used are the device names as seen by the kernel, 'physical' and 'virtual' interface names, + # not the ones from the netifd configuration, also called 'logical' interface names. + # The default is the 'br' pattern as OpenWrt defaults to one bridge for local interfaces. + option 'network_interface_include' 'br' + # The DNS engine will NOT consider to accept queries on interfaces whose names match this pattern + # or list of patterns. When a list of patterns is provided, interfaces matching any pattern will + # not be considered. If this value is left empty, no interface will be excluded. + # The interface names used are the device names as seen by the kernel, 'physical' and 'virtual' interface names, + # not the ones from the netifd configuration, also called 'logical' interface names. + # The default is to exclude interfaces matching the 'wan' and 'eth0.2' patterns, + # as by default on most routers the wan interface is the second VLAN on the single network interface (eth0), + # but it is sometimes named 'wan' on container hosts. + option 'network_interface_exclude' 'wan eth0.2' + # Pattern or list of patterns identifying WAN interfaces. If 'configuration_check_interval' is + # set, only changes to the interfaces matching these patterns, plus the ones matching + # 'network_interface_include' but not 'network_interface_exclude', will be considered + # configuration changes. When a list of patterns is provided, matching any pattern is + # enough to be considered. If this value is left empty, only the interfaces matching + # 'network_interface_include' but not 'network_interface_exclude' will be considered. + # The interface names used are the device names as seen by the kernel, 'physical' and 'virtual' interface names, + # not the ones from the netifd configuration, also called 'logical' interface names. + # Refer to 'network_interface_exclude' for the default value. + option 'network_interface_wan_include' 'wan' + # Port on which to accept Do53 queries on (UDP and TCP) + option 'do53_port' '53' + # Port on which to accept DoT queries on + option 'dot_port' '853' + # Port on which to accept DoH queries on + option 'doh_port' '443' + # Interval between health-check queries to an upstream resolver, in seconds + option 'default_check_interval' '5' + # Health check timeout to an upstream resolver, in milliseconds + option 'default_check_timeout' '1000' + # Number of failed consecutive health-checks to mark an upstream resolver down + option 'default_max_check_failures' '2' + # Whether upstream resolvers learned via the system should be checked for DoT/DoH support + option 'auto_upgrade_discovered_backends' '1' + # Whether the Do53 version of auto-upgraded resolver should be kept as fallback + option 'keep_auto_upgraded_backends' '1' + # In which internal pool should upgraded resolver be placed. The pool will be created + # if it does not exist. If this value is left empty upgraded resolvers are placed into + # the 'upgraded-to-dox' pool + option 'auto_upgraded_backends_pool' 'upgraded-to-dox' + # How often should the configuration be checked for changes, in seconds. 0 means disabled + option 'configuration_check_interval' '60' + # Domain name for DoT / DoH advertisement, if any. The default is to advertise only for the opportunistic '_dns.resolver.arpa.' name. + option 'advertise_for_domain_name' '' + # User to switch to, after the configuration has been set up. Default is to run as user 'root' + option 'user' 'root' + # Group to switch to, after the configuration has been set up. Default is to run as group 'root' + option 'group' 'root' + # Whether to enable 'verbose' logging ('1') or not ('0') + option 'verbose_mode' '0' + # Log verbose-level messages to this file instead of syslog and stdout (the default) + option 'verbose_log_destination' '' + # Maximum number of TCP connections to an upstream resolver. 0 means no limit + option 'default_max_upstream_concurrent_tcp_connections' '0' + # Maximum number of cached idle TCP connections to an upstream resolver. Keeping more connections improves latency but uses memory and increases the load on the upstream resolver + option 'max_idle_tcp_connections_per_downstream' '2' + # Maximum number of cached idle DoH connections to an upstream resolver. Keeping more connections improves latency but uses memory and increases the load on the upstream resolver + option 'max_idle_doh_connections_per_downstream' '2' + # Number of outgoing UDP sockets to an upstream resolver. Higher values reduce the risk of a successful UDP spoofing attempt, but increase the memory usage. The minimum value is 1, and a sane maximum value is 32767, as higher values might require too many sockets. The default is 100 + option 'outgoing_udp_sockets_per_downstream' '100' + # Maximum number of resolvers that can be used at the same time, either from the configuration + # or learned via the system. Additional resolvers, if any, will be skipped. + # Minimum value is 0, maximum is 100, default is 5. + option 'max_upstream_resolvers' '5' + # The maximum size of the sample of queries to record and consider for the + # health-checking mechanism. Higher values use slightly more memory. + # Minimum is 1, maximum is 10000, default is 100. + option 'health_checks_sample_size' '100' + # The minimum amount of regular queries that should have been recorded before the + # health-check threshold can be applied. Minimum is 1, maximum is equal to 'health_checks_sample_size', + # default is 10. + option 'health_checks_min_sample_count' '10' + # The threshold, as a percentage, of queries that should fail for the health-check + # to be triggered. The default is 20 which means 20% of the last 'health_checks_sample_size' queries + # should fail for a health-check to be triggered. Minimum is 1, maximum is 100. + option 'health_checks_threshold' '20' + # The interval, in seconds, between health-check queries. Note that when 'health_checks_exponential_backoff' is set to 1, + # the interval doubles between every queries. These queries are only sent when a threshold of failing regular queries has + # been reached, and until the backend is available again. Minimum is 1, maximum is 86400, default is 30 seconds. + option 'health_checks_failed_interval' '30' + # This setting controls which responses are considered a failure: 'TimeoutOnly' means that only timeout and I/O errors of + # regular queries will be considered for the health-check threshold, while 'TimeoutOrServFail' will also consider 'Server Failure' + # answers. Default is 'TimeoutOrServFail'. + option 'health_checks_mode' 'TimeoutOrServFail' + # Whether the health-check should use an exponential back-off instead of a fixed value, between health-check probes. If set to '0', + # after a backend has been moved to the 'down' state health-check probes are sent every 'health_checks_failed_interval' seconds. + # When set to '1', the delay between each probe starts at 'health_checks_failed_interval' seconds and doubles between every probe, + # capped at 'health_checks_max_backoff' seconds. Default is '1'. + option 'health_checks_exponential_backoff' '1' + # This value, in seconds, caps the time between two health-check queries when 'health_checks_exponential_backoff' is set to '1'. + # The default is 3600 which means that at most one hour will pass between two health-check queries. Minimum is 1, maximum is 86400. + option 'health_checks_max_backoff' '3600' + # The maximum PSS ("proportional set size") value acceptable for the dnsdist process, in kilobytes. The current PSS will be retrieved every 'configuration_check_interval' seconds and, if the specified threshold has been reached, the process will automatically restart to prevent consuming too much memory. The default is 0 which means that this check is disabled. + option 'max_pss' '0' + +# sentinel domains +config domainlist sentinel_domains + # Exact matching of the name will be done unless it starts with '*.' in which case suffix matching will be done + # IPv4 addresses configured on the configured interface will be returned to A queries, IPv6 to AAAA queries. + # The interface names used are the device names as seen by the kernel, 'physical' and 'virtual' interface names, + # not the ones from the netifd configuration, also called 'logical' interface names. + list entry 'router br-lan' + list entry '*.router br-lan' + +# configuration for a given network interfaces. +# The configuration of the special name 'default_interface' will be applied +# to all interfaces that do not have a specific configuration entry, provided +# that they match 'network_interface_include' and do not match 'network_interface_exclude'. +# The interface names used are the device names as seen by the kernel, 'physical' and 'virtual' interface names, +# not the ones from the netifd configuration, also called 'logical' interface names. +config interface + option name 'default_interface' + # Whether dnsdist will listen on this interface + option enabled 1 + # Whether dnsdist will accept Do53 queries, UDP and TCP, on this interface + option do53 1 + # Whether dnsdist will accept DoT queries on this interface + option dot 0 + # Whether dnsdist will accept DoH queries on this interface + option doh 0 + # Whether local domain resolution will be enabled for queries received on this interface + option local_resolution 1 + # Whether sentinel domain resolution will be enabled for queries received on this interface + option sentinel_domains 'sentinel_domains' + # Whether dnsdist will advertise DoT and/or DoH support, if available, in response to DDR queries received on this interface + option advertise 1 diff --git a/net/dnsdist/files/dnsdist.init b/net/dnsdist/files/dnsdist.init index b9d4e0058c1fc..e65eb63c51b8a 100644 --- a/net/dnsdist/files/dnsdist.init +++ b/net/dnsdist/files/dnsdist.init @@ -21,6 +21,6 @@ start_service() { [ "$user" != root ] && procd_append_param command -u "$user" [ "$group" != root ] && procd_append_param command -g "$group" procd_set_param file /etc/dnsdist.conf - procd_set_param respawn + procd_set_param respawn 60 0 20 procd_close_instance } diff --git a/net/dnsdist/files/dnsdist_acl.json b/net/dnsdist/files/dnsdist_acl.json new file mode 100644 index 0000000000000..02d941f6f8c48 --- /dev/null +++ b/net/dnsdist/files/dnsdist_acl.json @@ -0,0 +1,9 @@ +{ + "user": "dnsdist", + "access": { + "dhcp": { + "methods": [ "ipv4leases", "ipv6leases" ] + } + }, + "subscribe": [ "dhcp" ] +} diff --git a/net/dnsdist/files/local-domains.lua b/net/dnsdist/files/local-domains.lua new file mode 100644 index 0000000000000..40bc8dc1c6dd8 --- /dev/null +++ b/net/dnsdist/files/local-domains.lua @@ -0,0 +1,122 @@ +local M = {} + +M.lanSuffixes = { 'lan' } +M.ttl = 1 +M.destinations = nil + +local ffi = require 'ffi' +local C = ffi.C + +local common = require 'dnsdist/common' + +local byName4 = {} +local byName6 = {} +local dhcpScriptPath = '/usr/share/lua/dnsdist/dnsdist-odhcpd.lua' + +local qname_ret_ptr = ffi.new("char *[1]") +local qname_ret_ptr_param = ffi.cast("const char **", qname_ret_ptr) +local qname_ret_size = ffi.new("size_t[1]") +local qname_ret_size_param = ffi.cast("size_t*", qname_ret_size) + +local ffi_copy = ffi.copy +local ffi_string = ffi.string +local string_sub = string.sub +local string_byte = string.byte +local table_insert = table.insert + +local lanSuffixesMatchNode = nil + +local function lanaction(dq) + if lanSuffixesMatchNode == nil then + return DNSAction.None + end + -- warnlog(string.format("checking type and name, dq=%s, dq.qtype=%s", dq, dq.qtype)) + local qtype = C.dnsdist_ffi_dnsquestion_get_qtype(dq) + local byName + + if qtype == DNSQType.A then + byName = byName4 + elseif qtype == DNSQType.AAAA then + byName = byName6 + else + -- queries for non-address types get an empty NOERROR + C.dnsdist_ffi_dnsquestion_set_rcode(dq, DNSRCode.NOERROR) + return DNSAction.Spoof + end + + C.dnsdist_ffi_dnsquestion_get_qname_raw(dq, qname_ret_ptr_param, qname_ret_size_param) + + local qname = ffi_string(qname_ret_ptr[0], qname_ret_size[0]) + qname = newDNSNameFromRaw(qname) + local suffix = lanSuffixesMatchNode:getBestMatch(qname) + if suffix == nil then + return DNSAction.None + end + if qname == suffix then + -- queries for one of the exact suffixes (i.e `lan.`) get NOERROR + C.dnsdist_ffi_dnsquestion_set_rcode(dq, DNSRCode.NOERROR) + return DNSAction.Spoof + end + qname = qname:makeRelative(suffix) + + local ip = byName[qname:toStringNoDot()] + if ip then + local buf = ffi.new("char[?]", #ip + 1) + ffi_copy(buf, ip) + C.dnsdist_ffi_dnsquestion_set_result(dq, buf, #ip) + C.dnsdist_ffi_dnsquestion_set_max_returned_ttl(dq, M.ttl) + return DNSAction.Spoof + else + C.dnsdist_ffi_dnsquestion_set_rcode(dq, DNSRCode.NXDOMAIN) + return DNSAction.Nxdomain + end +end + +function threadmessage(cmd, data) + local name=data.name + local ip=data.ip + local proto=data.proto + local byName + if proto == 'v4' then + byName = byName4 + elseif proto == 'v6' then + byName = byName6 + else + return + end + if name and ip + then + if cmd == 'add' + then + byName[name] = ip + elseif cmd == 'del' + then + byName[name] = nil + else + warnlog(string.format("got unknown command '%s' from odhcpd thread", cmd)) + end + end + -- for k,v in pairs(msg) do warnlog(k..'/'..v) end +end + +function M.run(_) + if M.destinations == nil or #M.destinations == 0 or M.lanSuffixes == nil or #M.lanSuffixes == 0 then + return + end + + lanSuffixesMatchNode = newSuffixMatchNode() + lanSuffixesMatchNode:add(M.lanSuffixes) + + local script = io.open(dhcpScriptPath) + if script == nil then + return + end + newThread(script:read("*a")) + itfRules = {} + for _, itf in ipairs(M.destinations) do + table.insert(itfRules, TagRule(common.interfaceTagName, itf)) + end + addAction(AndRule{OrRule(itfRules), SuffixMatchNodeRule(lanSuffixesMatchNode)}, LuaFFIAction(lanaction)) +end + +return M diff --git a/net/dnsdist/files/os.lua b/net/dnsdist/files/os.lua new file mode 100644 index 0000000000000..bdf10eb99099b --- /dev/null +++ b/net/dnsdist/files/os.lua @@ -0,0 +1,147 @@ +local M = {} + +local ffi = require 'ffi' +local lfs = require 'lfs' +local C = ffi.C + +ffi.cdef[[ +typedef struct timespec { + long tv_sec; /* seconds */ + long tv_nsec; /* nanoseconds */ +} timespec_t; + +int clock_gettime(int clk_id, timespec_t *tp); +typedef unsigned int socklen_t; +const char *inet_ntop(int af, const void *restrict src, + char *restrict dst, socklen_t size); +int inet_pton(int af, const char *restrict src, void *restrict dst); + +void _exit(int status); + +unsigned int sleep(unsigned int seconds); +]] + +local IS_MAC = (ffi.os == "OSX") +local IS_LINUX = (ffi.os == "Linux") + +local CLOCK_MONOTONIC +local AF_INET +local AF_INET6 +if IS_LINUX then + CLOCK_MONOTONIC = 1 + AF_INET = 2 + AF_INET6 = 10 +elseif IS_MAC then + CLOCK_MONOTONIC = 6 + AF_INET = 2 + AF_INET6 = 10 +else + errlog('OS not supported: '..ffi.os) +end + +local tv = ffi.new("struct timespec[?]", 1) + +function M.getTimeUsec() + C.clock_gettime(CLOCK_MONOTONIC, tv) + return tv[0].tv_sec * 1000000 + tv[0].tv_nsec / 1000 +end + +local pton_dst = ffi.new("char[?]", 16) +local inet_buffer = ffi.new("char[?]", 256) + +function M.convertIPv4ToBinary(str) + C.inet_pton(AF_INET, str, pton_dst) + return ffi.string(pton_dst, 4) +end + +function M.convertIPv6ToBinary(str) + C.inet_pton(AF_INET6, str, pton_dst) + return ffi.string(pton_dst, 16) +end + +function M.binaryIPv4ToString(addr) + local str = C.inet_ntop(2, addr, inet_buffer, 256) + return str ~= nil and ffi.string(str) or '' +end + +function M.binaryIPv6ToString(addr) + local str = C.inet_ntop(10, addr, inet_buffer, 256) + return str ~= nil and ffi.string(str) or '' +end + +function M.getCommandReturnCode(command) + return os.execute(command) +end + +function M.getCommandOutput(command) + local desc = io.popen(command) + if desc == nil then + return nil + end + desc:flush() + local output = desc:read('*all') + desc:close() + return output +end + +function M.getFileContent(file) + local desc = io.open(file, "rb") + if desc == nil then + return nil + end + local content = desc:read("*all") + desc:close() + return content +end + +function M.getPSS() + local smaps_rollup_file = '/proc/self/smaps_rollup' + local pss_pattern = 'Pss:' + local kb_pattern = ' Kb' + if not M.fileExists(smaps_rollup_file) then + return 0 + end + for line in io.lines(smaps_rollup_file) do + if string.sub(line, 1, #pss_pattern) == pss_pattern then + local remaining = string.sub(line, #pss_pattern + 1, -(#kb_pattern + 1)) + local value = tonumber(remaining) + if value then + return value + else + return 0 + end + end + end + return 0 +end + +function M.getFileModificationTime(path) + local stats = lfs.attributes(path) + if stats ~= nil then + return stats.modification + end + return 0 +end + +function M.fileExists(path) + local stats = lfs.attributes(path) + if stats ~= nil then + return true + end + return false +end + +function M.exit(code) + C._exit(code) +end + +local os_time = os.time +function M.time() + return os_time() +end + +function M.sleep(seconds) + return C.sleep(seconds) +end + +return M diff --git a/net/dnsdist/files/sample.uci.conf b/net/dnsdist/files/sample.uci.conf new file mode 100644 index 0000000000000..60b1dae6ffbe2 --- /dev/null +++ b/net/dnsdist/files/sample.uci.conf @@ -0,0 +1,228 @@ +config dnsdist general + option 'enabled' '1' + # private key to use for incoming DoT and DoH + option 'tls_key' '/etc/dnsdist.key' + # certificate to use for incoming DoT and DoH + option 'tls_cert' '/etc/dnsdist.pem' + # the fingerprint of the certificate, so we know we need to reload it + # Example: '37:4F:E4:F7:F2:23:C8:2A:25:F6:43:80:52:42:E2:C9:50:2F:AD:16' + option 'tls_cert_fingerprint' '' + # minimum TLS version to accept ('tls1.0', 'tls1.1', 'tls1.2', 'tls1.3') + option 'tls_min_version' 'tls1.2' + # TLS ciphers used for incoming connections, for TLS < 1.3 (if this value is empty the OpenSSL defaults will be used) + option 'tls_ciphers_incoming' 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305' + # TLS ciphers used for incoming connections, for TLS 1.3 (if this value is empty the OpenSSL defaults will be used) + option 'tls_ciphers13_incoming' '' + # TLS ciphers used for outgoing connections, for TLS < 1.3 (if this value is empty the OpenSSL defaults will be used) + option 'tls_ciphers_outgoing' 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305' + # TLS ciphers used for outgoing connections, for TLS 1.3 (if this value is empty the OpenSSL defaults will be used) + option 'tls_ciphers13_outgoing' '' + # Number of entries in the domain cache. 0 means caching is disabled, the maximum value is 2^32-1 entries + # but is likely limited by the amount of RAM available. + option 'domain_cache_size' '100' + # Cap the TTL of cached records to the supplied number of seconds. Default is 600 (10 minutes), minimum is 1, useful range is up to 86400. + # Setting this value to the highest possible value for a TTL, 2^32-1, means that TTLs will not be capped. + option 'domain_ttl_cap' '600' + # Set how often the cache is scanned to expunge expired entries, in seconds. Default is 60s (every minute), minimum value is 0, useful range is up to 86400 + option 'domain_cleanup_interval' '60' + # The TTL used for local domain responses. The values allowed by the DNS protocol range from 1 to 2^31-1 + option 'local_domains_ttl' '1' + # The DNS suffix, or list of suffixes, identifying local domain names + option 'local_domains_suffix' 'lan' + # The TTL used for sentinel domains responses + option 'sentinel_domains_ttl' '60' + # Number of concurrent connections per client device. 0 means unlimited, the maximum value is 2^64-1. + option 'concurrent_incoming_connections_per_device' '10' + # The DNS engine will consider to accept queries on interfaces whose names match this pattern, + # or list of patterns. When a list of patterns is provided, interfaces matching any pattern will + # be considered. If this value is left empty, the DNS engine will consider all interfaces. + # The interface names used are the device names as seen by the kernel, 'physical' and 'virtual' interface names, + # not the ones from the netifd configuration, also called 'logical' interface names. + # The default is the 'br' pattern as OpenWrt defaults to one bridge for local interfaces. + option 'network_interface_include' 'br' + # The DNS engine will NOT consider to accept queries on interfaces whose names match this pattern + # or list of patterns. When a list of patterns is provided, interfaces matching any pattern will + # not be considered. If this value is left empty, no interface will be excluded. + # The interface names used are the device names as seen by the kernel, 'physical' and 'virtual' interface names, + # not the ones from the netifd configuration, also called 'logical' interface names. + # The default is to exclude interfaces matching the 'wan' and 'eth0.2' patterns, + # as by default on most routers the wan interface is the second VLAN on the single network interface (eth0), + # but it is sometimes named 'wan' on container hosts. + option 'network_interface_exclude' 'wan eth0.2' + # Pattern or list of patterns identifying WAN interfaces. If 'configuration_check_interval' is + # set, only changes to the interfaces matching these patterns, plus the ones matching + # 'network_interface_include' but not 'network_interface_exclude', will be considered + # configuration changes. When a list of patterns is provided, matching any pattern is + # enough to be considered. If this value is left empty, only the interfaces matching + # 'network_interface_include' but not 'network_interface_exclude' will be considered. + # The interface names used are the device names as seen by the kernel, 'physical' and 'virtual' interface names, + # not the ones from the netifd configuration, also called 'logical' interface names. + # Refer to 'network_interface_exclude' for the default value. + option 'network_interface_wan_include' 'wan eth0.2' + # Port on which to accept API queries + option 'web_server_port' '9080' + # Port on which to accept Do53 queries on (UDP and TCP) + option 'do53_port' '53' + # Port on which to accept DoT queries on + option 'dot_port' '853' + # Port on which to accept DoH queries on + option 'doh_port' '443' + # Interval between health-check queries to an upstream resolver, in seconds + option 'default_check_interval' '5' + # Health check timeout to an upstream resolver, in milliseconds + option 'default_check_timeout' '1000' + # Number of failed consecutive health-checks to mark an upstream resolver down + option 'default_max_check_failures' '2' + # Whether upstream resolvers learned via the system should be checked for DoT/DoH support + option 'auto_upgrade_discovered_backends' '0' + # Whether the Do53 version of auto-upgraded resolver should be kept as fallback + option 'keep_auto_upgraded_backends' '1' + # In which internal pool should upgraded resolver be placed. The pool will be created + # if it does not exist. If this value is left empty upgraded resolvers are placed into + # the 'upgraded-to-dox' pool + option 'auto_upgraded_backends_pool' 'upgraded-to-dox' + # How often should the configuration be checked for changes, in seconds. 0 means disabled + option 'configuration_check_interval' '60' + # Domain name for DoT / DoH advertisement, if any. The default is to advertise only for the opportunistic '_dns.resolver.arpa.' name. + option 'advertise_for_domain_name' '' + # User to switch to, after the configuration has been set up. Default is to run as user 'root' + option 'user' 'dnsdist' + # Group to switch to, after the configuration has been set up. Default is to run as group 'root' + option 'group' 'dnsdist' + # Whether to enable 'verbose' logging ('1') or not ('0') + option 'verbose_mode' '0' + # Log verbose-level messages to this file instead of syslog and stdout (the default) + option 'verbose_log_destination' '' + # Maximum number of TCP connections to an upstream resolver. 0 means no limit + option 'default_max_upstream_concurrent_tcp_connections' '0' + # Maximum number of cached idle TCP connections to an upstream resolver. Keeping more connections improves latency but uses memory and increases the load on the upstream resolver + option 'max_idle_tcp_connections_per_downstream' '2' + # Maximum number of cached idle DoH connections to an upstream resolver. Keeping more connections improves latency but uses memory and increases the load on the upstream resolver + option 'max_idle_doh_connections_per_downstream' '2' + # Number of outgoing UDP sockets to an upstream resolver. Higher values reduce the risk of a successful UDP spoofing attempt, but increase the memory usage. The minimum value is 1, and a sane maximum value is 32767, as higher values might require too many sockets. The default is 100 + option 'outgoing_udp_sockets_per_downstream' '100' + # Maximum number of resolvers that can be used at the same time, either from the configuration + # or learned via the system. Additional resolvers, if any, will be skipped. + # Minimum value is 0, maximum is 100, default is 5. + option 'max_upstream_resolvers' '5' + # The maximum size of the sample of queries to record and consider for the + # health-checking mechanism. Higher values use slightly more memory. + # Minimum is 1, maximum is 10000, default is 100. + option 'health_checks_sample_size' '100' + # The minimum amount of regular queries that should have been recorded before the + # health-check threshold can be applied. Minimum is 1, maximum is equal to 'health_checks_sample_size', + # default is 10. + option 'health_checks_min_sample_count' '10' + # The threshold, as a percentage, of queries that should fail for the health-check + # to be triggered. The default is 20 which means 20% of the last 'health_checks_sample_size' queries + # should fail for a health-check to be triggered. Minimum is 1, maximum is 100. + option 'health_checks_threshold' '20' + # The interval, in seconds, between health-check queries. Note that when 'health_checks_exponential_backoff' is set to 1, + # the interval doubles between every queries. These queries are only sent when a threshold of failing regular queries has + # been reached, and until the backend is available again. Minimum is 1, maximum is 86400, default is 30 seconds. + option 'health_checks_failed_interval' '30' + # This setting controls which responses are considered a failure: 'TimeoutOnly' means that only timeout and I/O errors of + # regular queries will be considered for the health-check threshold, while 'TimeoutOrServFail' will also consider 'Server Failure' + # answers. Default is 'TimeoutOrServFail'. + option 'health_checks_mode' 'TimeoutOrServFail' + # Whether the health-check should use an exponential back-off instead of a fixed value, between health-check probes. If set to '0', + # after a backend has been moved to the 'down' state health-check probes are sent every 'health_checks_failed_interval' seconds. + # When set to '1', the delay between each probe starts at 'health_checks_failed_interval' seconds and doubles between every probe, + # capped at 'health_checks_max_backoff' seconds. Default is '1'. + option 'health_checks_exponential_backoff' '1' + # This value, in seconds, caps the time between two health-check queries when 'health_checks_exponential_backoff' is set to '1'. + # The default is 3600 which means that at most one hour will pass between two health-check queries. Minimum is 1, maximum is 86400. + option 'health_checks_max_backoff' '3600' + # The maximum PSS ("proportional set size") value acceptable for the dnsdist process, in kilobytes. The current PSS will be retrieved every 'configuration_check_interval' seconds and, if the specified threshold has been reached, the process will automatically restart to prevent consuming too much memory. The default is 0 which means that this check is disabled. + option 'max_pss' '0' + +# sentinel domains +config domainlist sentinel_domains + # Exact matching of the name will be done unless it starts with '*.' in which case suffix matching will be done + # IPv4 addresses configured on the configured interface will be returned to A queries, IPv6 to AAAA queries. + # The interface names used are the device names as seen by the kernel, 'physical' and 'virtual' interface names, + # not the ones from the netifd configuration, also called 'logical' interface names. + list entry 'router br-lan' + list entry '*.router br-lan' + +# configuration for a given network interfaces. +# The configuration of the special name 'default_interface' will be applied +# to all interfaces that do not have a specific configuration entry, provided +# that they match 'network_interface_include' and do not match 'network_interface_exclude'. +# The interface names used are the device names as seen by the kernel, 'physical' and 'virtual' interface names, +# not the ones from the netifd configuration, also called 'logical' interface names. +config interface + option name 'default_interface' + # Whether dnsdist will listen on this interface + option enabled 1 + # Whether dnsdist will accept Do53 queries, UDP and TCP, on this interface + option do53 1 + # Whether dnsdist will accept DoT queries on this interface + option dot 1 + # Whether dnsdist will accept DoH queries on this interface + option doh 1 + # Whether local domain resolution will be enabled for queries received on this interface + option local_resolution 1 + # Whether sentinel domain resolution will be enabled for queries received on this interface + option sentinel_domains 'sentinel_domains' + # Whether dnsdist will advertise DoT and/or DoH support, if available, in response to DDR queries received on this interface + option advertise 0 + +# Configuration of resolvers +# If no resolver is defined, dnsdist will try to learn +# the addresses of resolvers from /tmp/resolv.conf.auto +#config server quad9doh +# For DoT and DoH servers, the name to use as Server Name Indication +# in the TLS handshake, and against which the certificate presented +# by the server should be validated +# option adn 'dns.quad9.net' +# Type of server: do53, dot or doh +# option type 'doh' +# IP address of the server +# option addr '9.9.9.9' +# Port of the server (default is 53) +# option port 443 +# For DoH servers, the HTTP path to use +# option path '/dns-query' +# Maximum value of concurrent queries to send to this server (TCP and DoT). +# Possible values are 1 (default) to 65535. +# Higher values yield better performance, but only if the server supports it. +# option maxInFlight 1 +# Whether to validate the TLS certificate provided by the server, for +# DoT and DoH ones. Default is true. +# option validate 1 +# How many consecutive health-checks have to fail before this server is +# no longer considered working. The default and minimum value is 1, the +# maximum value is 255 +# option maxCheckFailures 1 +# Frequency of health-check queries, in seconds. The default and minimum value +# is 1, the maximum value is 65535 +# option checkInterval 1 +# Timeout of health-check queries, in milliseconds. Default is 1000 which is one +# second. Supported values are from 1 to 65535. +# option checkTimeout 1000 +# If set, this server will be contacted using this network interface only. +# If this condition cannot be satisfied because the interface is not valid, +# this server will not be used. +# The interface names used are the device names as seen by the kernel, 'physical' and 'virtual' interface names, +# not the ones from the netifd configuration, also called 'logical' interface names. +# option upstreamWANInterface 'wlan0' +# The maximum number of concurrent TCP connections that should be opened +# to this server. The default is 0, which means unlimited. The maximum value +# is 2^32-1. +# option maxConcurrentTCPConnections 0 +# Whether to use the Discovery of Designated Resolvers mechanism to check if +# this server can be upgrade to DoT or DoH +# option autoUpgrade 0 +# If auto-upgrade is enabled, how often should we check if an upgrade is possible, in seconds. +# Default is 3600 which is once every hour. The minimum value is 1 second, the maximum is 2^32-1. +# option autoUpgradeInterval 3600 +# Whether to keep the Do53 version of this backend as a fall-back after a +# successful upgrade to DoT or DoH. Default is false. +# option autoUpgradeKeep false +# The pool to place this server into after a succesfull upgrade. The pool will be +# created if it does not exist +# option autoUpgradePool '' +# The code of the DoH option to use in Discovery of Designated Resolvers exchanges. +# Default is 7, which is what all servers should use. Possible values are 0 to 65535. +# option autoUpgradeDoHKey '7' diff --git a/net/dnsdist/files/start.lua b/net/dnsdist/files/start.lua new file mode 100644 index 0000000000000..638e18f7fca5a --- /dev/null +++ b/net/dnsdist/files/start.lua @@ -0,0 +1,78 @@ +local useUCI = true + +includeDirectory('/etc/dnsdist.conf.d/') + +collectgarbage() +local common = require 'dnsdist/common' +local localDomains = require 'dnsdist/local-domains' +local dnsdistOs = require 'dnsdist/os' +common.registerConfigurationDoneHook(localDomains.run) +collectgarbage() +collectgarbage() +collectgarbage("setpause", 100) + +-- fixed configuration options +setMaxUDPOutstanding(50) +setMaxTCPClientThreads(1) +setOutgoingDoHWorkerThreads(1) +setRingBuffersSize(300, 1) +setRingBuffersOptions({recordResponses=false}) +setMaxTCPQueuedConnections(10) +setOutgoingTLSSessionsCacheMaxTicketsPerBackend(5) +setTCPInternalPipeBufferSize(0) +setMaxTCPQueuedConnections(100) +setRandomizedIdsOverUDP(true) +setRandomizedOutgoingSockets(true) + +if useUCI then + local status, _ = pcall(require, 'uci') + if status then + local configuration = require 'dnsdist/configuration' + local config = configuration.loadFromUCI() + collectgarbage() + local loggedonce = false + + while not configuration.enabled(config) or configuration.isBridge() do + local waitTime = tonumber(config['configuration-check-interval']) + if not configuration.enabled(config) then + if not loggedonce then + errlog('Not starting up yet - we are disabled in the configuration') + end + vinfolog('Currently disabled by configuration, next check in '..waitTime..' seconds') + else + if not loggedonce then + errlog('Not starting up yet - the router is in bridge mode') + end + vinfolog('The router is currently in bridge mode, next check in '..waitTime..' seconds') + end + + loggedonce = true + + local ret = dnsdistOs.sleep(tonumber(waitTime)) + if ret > 0 then + -- sleep was interrupted + errlog('Sleep was interrupted - exiting') + os.exit(1) + end + config = configuration.loadFromUCI() + collectgarbage() + collectgarbage() + end + + configuration.apply(config) + + common.runConfigurationDoneHooks(config) + + function maintenance() + common.runMaintenanceHooks() + end + + config = nil + collectgarbage() + collectgarbage() + return + else + errlog('Loading of the configuration from UCI requested but UCI support is not available') + return + end +end