diff --git a/README.md b/README.md index cc34cdf..e84e6db 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,32 @@ # Dappnode Bind Core Package -🌐 This package acts as the DNS proxy for the Dappnode Packages. It is required to access the services provided by the Dappnode Packages from the host machine connected to the Dappnode. +🌐 This package acts as the DNS proxy for the Dappnode Packages. When connected to DAppNode, all DNS requests from your device are first sent to this package. It resolves DAppNode-specific domains locally and forwards all other DNS queries to the configured upstream DNSCrypt resolvers, ensuring secure and private name resolution for both DAppNode services and general internet browsing. + +## Environment Variables + +### PUBLIC_RESOLVERS_OVERRIDE + +The `PUBLIC_RESOLVERS_OVERRIDE` environment variable allows you to customize the DNSCrypt resolvers used by the package. By default, the package uses a curated list of reliable public resolvers: +- scaleway-fr +- commons-host +- dnscrypt.me +- cloudflare + +You can override these defaults by providing a comma-separated list of resolver names. For example: +``` +PUBLIC_RESOLVERS_OVERRIDE=quad9,adguard-dns,google +``` + +#### DNSCrypt Features + +The DNS proxy in this package uses DNSCrypt-proxy with the following security features enabled: + +- **DNSCrypt and DNS-over-HTTPS (DoH)**: Supports both DNSCrypt and DoH protocols for encrypted DNS queries +- **No-logging Policy**: Only uses resolvers that pledge not to log user queries +- **No-filter Policy**: Uses resolvers that don't enforce their own blocklists +- **IPv4 Support**: Compatible with IPv4 networks +- **TCP/UDP Support**: Uses UDP by default for better performance, with TCP fallback available + +The proxy is configured to handle up to 250 simultaneous client connections and listens on all interfaces (0.0.0.0:53). + +For a full list of available public resolvers that can be used with `PUBLIC_RESOLVERS_OVERRIDE`, visit: https://dnscrypt.info/public-servers diff --git a/bind/Dockerfile b/bind/Dockerfile index 717b387..ba6bbfe 100644 --- a/bind/Dockerfile +++ b/bind/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21.5-alpine3.18 AS builder +FROM golang:1.24.1-alpine3.21 AS builder ARG UPSTREAM_VERSION @@ -20,7 +20,7 @@ RUN cd ${SRC_DIR} && \ CGO_ENABLED=0 go build -mod vendor -ldflags="-s -w" -o /usr/local/bin/dnscrypt-proxy && \ chmod +x /usr/local/bin/dnscrypt-proxy -FROM alpine:3.18 +FROM alpine:3.21 WORKDIR /app diff --git a/bind/custom/config/dnscrypt-proxy.toml b/bind/custom/config/dnscrypt-proxy.toml index 9fe59a4..f4d0cdb 100644 --- a/bind/custom/config/dnscrypt-proxy.toml +++ b/bind/custom/config/dnscrypt-proxy.toml @@ -21,17 +21,23 @@ ## Servers from the "public-resolvers" source (see down below) can ## be viewed here: https://dnscrypt.info/public-servers ## -## If this line is commented, all registered servers matching the require_* filters -## will be used. +## The proxy will automatically pick working servers from this list. +## Note that the require_* filters do NOT apply when using this setting. +## +## By default, this list is empty and all registered servers matching the +## require_* filters will be used instead. ## -## The proxy will automatically pick the fastest, working servers from the list. ## Remove the leading # first to enable this; lines starting with # are ignored. server_names = ['scaleway-fr', 'commons-host', 'dnscrypt.me', 'cloudflare'] ## List of local addresses and ports to listen to. Can be IPv4 and/or IPv6. -## Note: When using systemd socket activation, choose an empty set (i.e. [] ). +## Example with both IPv4 and IPv6: +## listen_addresses = ['127.0.0.1:53', '[::1]:53'] +## +## To listen to all IPv4 addresses, use `listen_addresses = ['0.0.0.0:53']` +## To listen to all IPv4+IPv6 addresses, use `listen_addresses = ['[::]:53']` listen_addresses = ['0.0.0.0:53'] @@ -49,7 +55,7 @@ max_clients = 250 # user_name = 'nobody' -## Require servers (from static + remote sources) to satisfy specific properties +## Require servers (from remote sources) to satisfy specific properties # Use servers reachable over IPv4 ipv4_servers = true @@ -63,6 +69,9 @@ dnscrypt_servers = true # Use servers implementing the DNS-over-HTTPS protocol doh_servers = true +# Use servers implementing the Oblivious DoH protocol +odoh_servers = false + ## Require servers defined by remote sources to satisfy specific properties @@ -72,9 +81,12 @@ require_dnssec = false # Server must not log user queries (declarative) require_nolog = true -# Server must not enforce its own blacklist (for parental control, ads blocking...) +# Server must not enforce its own blocklist (for parental control, ads blocking...) require_nofilter = true +# Server names to avoid even if they match all criteria +disabled_server_names = [] + ## Always use TCP to connect to upstream servers. ## This can be useful if you need to route everything through Tor. @@ -85,54 +97,111 @@ require_nofilter = true force_tcp = false +## Enable *experimental* support for HTTP/3 (DoH3, HTTP over QUIC) +## Note that, like DNSCrypt but unlike other HTTP versions, this uses +## UDP and (usually) port 443 instead of TCP. + +http3 = false + + ## SOCKS proxy ## Uncomment the following line to route all TCP connections to a local Tor node ## Tor doesn't support UDP, so set `force_tcp` to `true` as well. -# proxy = "socks5://127.0.0.1:9050" +# proxy = 'socks5://127.0.0.1:9050' ## HTTP/HTTPS proxy ## Only for DoH servers -# http_proxy = "http://127.0.0.1:8888" +# http_proxy = 'http://127.0.0.1:8888' -## How long a DNS query will wait for a response, in milliseconds +## How long a DNS query will wait for a response, in milliseconds. +## If you have a network with *a lot* of latency, you may need to +## increase this. Startup may be slower if you do so. +## Don't increase it too much. 10000 is the highest reasonable value. +## A timeout below 5000 is not recommended. timeout = 10000 -## Keepalive for HTTP (HTTPS, HTTP/2) queries, in seconds +## Keepalive for HTTP (HTTPS, HTTP/2, HTTP/3) queries, in seconds keepalive = 30 -## Load-balancing strategy: 'p2' (default), 'ph', 'fastest' or 'random' +## Add EDNS-client-subnet information to outgoing queries +## +## Multiple networks can be listed; they will be randomly chosen. +## These networks don't have to match your actual networks. + +# edns_client_subnet = ['0.0.0.0/0', '2001:db8::/32'] + + +## Response for blocked queries. Options are `refused`, `hinfo` (default) or +## an IP response. To give an IP response, use the format `a:,aaaa:`. +## Using the `hinfo` option means that some responses will be lies. +## Unfortunately, the `hinfo` option appears to be required for Android 8+ + +# blocked_query_response = 'refused' + + +## Load-balancing strategy: 'p2' (default), 'ph', 'p', 'first' or 'random' +## Randomly choose 1 of the fastest 2, half, n, 1 or all live servers by latency. +## The response quality still depends on the server itself. # lb_strategy = 'p2' +## Set to `true` to constantly try to estimate the latency of all the resolvers +## and adjust the load-balancing parameters accordingly, or to `false` to disable. +## Default is `true` that makes 'p2' `lb_strategy` work well. + +# lb_estimator = true + ## Log level (0-6, default: 2 - 0 is very verbose, 6 only contains fatal errors) # log_level = 2 -## log file for the application +## Log file for the application, as an alternative to sending logs to +## the standard system logging service (syslog/Windows event log). +## +## This file is different from other log files, and will not be +## automatically rotated by the application. # log_file = 'dnscrypt-proxy.log' +## When using a log file, only keep logs from the most recent launch. + +# log_file_latest = true + + ## Use the system logger (syslog on Unix, Event Log on Windows) # use_syslog = true +## The maximum concurrency to reload certificates from the resolvers. +## Default is 10. + +# cert_refresh_concurrency = 10 + + ## Delay, in minutes, after which certificates are reloaded cert_refresh_delay = 240 +## Initially don't check DNSCrypt server certificates for expiration, and +## only start checking them after a first successful connection to a resolver. +## This can be useful on routers with no battery-backed clock. + +# cert_ignore_timestamp = false + + ## DNSCrypt: Create a new, unique key for every single DNS query ## This may improve privacy but can also have a significant impact on CPU usage ## Only enable if you don't have a lot of network load @@ -145,41 +214,75 @@ cert_refresh_delay = 240 # tls_disable_session_tickets = false -## DoH: Use a specific cipher suite instead of the server preference +## DoH: Use TLS 1.2 and specific cipher suite instead of the server preference ## 49199 = TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ## 49195 = TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ## 52392 = TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 ## 52393 = TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 ## ## On non-Intel CPUs such as MIPS routers and ARM systems (Android, Raspberry Pi...), -## the following suite improves performance. +## uncommenting the following line may improve performance. ## This may also help on Intel CPUs running 32-bit operating systems. +## However, this can cause issues fetching sources or connecting to some HTTP servers, +## and should not be set on regular CPUs. ## -## Keep tls_cipher_suite empty if you have issues fetching sources or -## connecting to some DoH servers. Google and Cloudflare are fine with it. +## Keep tls_cipher_suite undefined to let the app automatically choose secure parameters. # tls_cipher_suite = [52392, 49199] -## Fallback resolver -## This is a normal, non-encrypted DNS resolver, that will be only used -## for one-shot queries when retrieving the initial resolvers list, and -## only if the system DNS configuration doesn't work. -## No user application queries will ever be leaked through this resolver, -## and it will not be used after IP addresses of resolvers URLs have been found. -## It will never be used if lists have already been cached, and if stamps -## don't include host names without IP addresses. -## It will not be used if the configured system DNS works. -## A resolver supporting DNSSEC is recommended. This may become mandatory. +## Log TLS key material to a file, for debugging purposes only. +## This file will contain the TLS master key, which can be used to decrypt +## all TLS traffic to/from DoH servers. +## Never ever enable except for debugging purposes with a tool such as mitmproxy. + +# tls_key_log_file = '/tmp/keylog.txt' + + +## Bootstrap resolvers +## +## These are normal, non-encrypted DNS resolvers, that will be only used +## for one-shot queries when retrieving the initial resolvers list and if +## the system DNS configuration doesn't work. +## +## No user queries will ever be leaked through these resolvers, and they will +## not be used after IP addresses of DoH resolvers have been found (if you are +## using DoH). +## +## They will never be used if lists have already been cached, and if the stamps +## of the configured servers already include IP addresses (which is the case for +## most of DoH servers, and for all DNSCrypt servers and relays). +## +## They will not be used if the configured system DNS works, or after the +## proxy already has at least one usable secure resolver. +## +## Resolvers supporting DNSSEC are recommended, and, if you are using +## DoH, bootstrap resolvers should ideally be operated by a different entity +## than the DoH servers you will be using, especially if you have IPv6 enabled. +## +## People in China may want to use 114.114.114.114:53 here. +## Other popular options include 8.8.8.8, 9.9.9.9 and 1.1.1.1. +## +## If more than one resolver is specified, they will be tried in sequence. ## -## People in China may need to use 114.114.114.114:53 here. -## Other popular options include 8.8.8.8 and 1.1.1.1. +## TL;DR: put valid standard resolver addresses here. Your actual queries will +## not be sent there. If you're using DNSCrypt or Anonymized DNS and your +## lists are up to date, these resolvers will not even be used. -bootstrap_resolvers = ['1.1.1.1:53'] +bootstrap_resolvers = ['9.9.9.11:53', '8.8.8.8:53'] -## Never let dnscrypt-proxy try to use the system DNS settings; -## unconditionally use the fallback resolver. +## When internal DNS resolution is required, for example to retrieve +## the resolvers list: +## +## - queries will be sent to dnscrypt-proxy itself, if it is already +## running with active servers (*) +## - or else, queries will be sent to fallback servers +## - finally, if `ignore_system_dns` is `false`, queries will be sent +## to the system DNS +## +## (*) this is incompatible with systemd sockets. +## `listen_addrs` must not be empty. ignore_system_dns = true @@ -188,10 +291,22 @@ ignore_system_dns = true ## initializing the proxy. ## Useful if the proxy is automatically started at boot, and network ## connectivity is not guaranteed to be immediately available. -## Use 0 to disable. +## Use 0 to not test for connectivity at all (not recommended), +## and -1 to wait as much as possible. netprobe_timeout = 60 +## Address and port to try initializing a connection to, just to check +## if the network is up. It can be any address and any port, even if +## there is nothing answering these on the other side. Just don't use +## a local address, as the goal is to check for Internet connectivity. +## On Windows, a datagram with a single, nul byte will be sent, only +## when the system starts. +## On other operating systems, the connection will be initialized +## but nothing will be sent at all. + +netprobe_address = '9.9.9.9:53' + ## Offline mode - Do not use any remote encrypted servers. ## The proxy will remain fully functional to respond to queries that @@ -200,9 +315,19 @@ netprobe_timeout = 60 # offline_mode = false +## Additional data to attach to outgoing queries. +## These strings will be added as TXT records to queries. +## Do not use, except on servers explicitly asking for extra data +## to be present. +## encrypted-dns-server can be configured to use this for access control +## in the [access_control] section + +# query_meta = ['key1:value1', 'key2:value2', 'token:MySecretToken'] + + ## Automatic log files rotation -# Maximum log files size in MB +# Maximum log files size in MB - Set to 0 for unlimited. log_files_max_size = 10 # How long to keep backup files, in days @@ -217,23 +342,43 @@ log_files_max_backups = 1 # Filters # ######################### +## Note: if you are using dnsmasq, disable the `dnssec` option in dnsmasq if you +## configure dnscrypt-proxy to do any kind of filtering (including the filters +## below and blocklists). +## You can still choose resolvers that do DNSSEC validation. + + ## Immediately respond to IPv6-related queries with an empty response ## This makes things faster when there is no IPv6 connectivity, but can ## also cause reliability issues with some stub resolvers. -## Do not enable if you added a validating resolver such as dnsmasq in front -## of the proxy. block_ipv6 = false +## Immediately respond to A and AAAA queries for host names without a domain name +## This also prevents "dotless domain names" from being resolved upstream. + +block_unqualified = true + + +## Immediately respond to queries for local zones instead of leaking them to +## upstream resolvers (always causing errors or timeouts). + +block_undelegated = true + + +## TTL for synthetic responses sent when a request has been blocked (due to +## IPv6 or blocklists). + +reject_ttl = 10 + + ################################################################################## # Route queries for specific domains to a dedicated set of servers # ################################################################################## -## Example map entries (one entry per line): -## example.com 9.9.9.9 -## example.net 9.9.9.9,8.8.8.8,1.1.1.1 +## See the `example-forwarding-rules.txt` file for an example forwarding_rules = 'forwarding-rules.txt' @@ -246,13 +391,18 @@ forwarding_rules = 'forwarding-rules.txt' ## Cloaking returns a predefined address for a specific name. ## In addition to acting as a HOSTS file, it can also return the IP address ## of a different name. It will also do CNAME flattening. +## If 'cloak_ptr' is set, then PTR (reverse lookups) are enabled +## for cloaking rules that do not contain wild cards. ## -## Example map entries (one entry per line) -## example.com 10.1.1.1 -## www.google.com forcesafesearch.google.com +## See the `example-cloaking-rules.txt` file for an example cloaking_rules = 'cloaking-rules.txt' +## TTL used when serving entries in cloaking-rules.txt + +# cloak_ttl = 600 +# cloak_ptr = false + ########################### @@ -266,12 +416,12 @@ cache = true ## Cache size -cache_size = 512 +cache_size = 4096 ## Minimum TTL for cached entries -cache_min_ttl = 600 +cache_min_ttl = 2400 ## Maximum TTL for cached entries @@ -290,6 +440,53 @@ cache_neg_max_ttl = 600 +######################################## +# Captive portal handling # +######################################## + +[captive_portals] + +## A file that contains a set of names used by operating systems to +## check for connectivity and captive portals, along with hard-coded +## IP addresses to return. + +# map_file = 'example-captive-portals.txt' + + + +################################## +# Local DoH server # +################################## + +[local_doh] + +## dnscrypt-proxy can act as a local DoH server. By doing so, web browsers +## requiring a direct connection to a DoH server in order to enable some +## features will enable these, without bypassing your DNS proxy. + +## Addresses that the local DoH server should listen to + +# listen_addresses = ['127.0.0.1:3000'] + + +## Path of the DoH URL. This is not a file, but the part after the hostname +## in the URL. By convention, `/dns-query` is frequently chosen. +## For each `listen_address` the complete URL to access the server will be: +## `https://` (ex: `https://127.0.0.1/dns-query`) + +# path = '/dns-query' + + +## Certificate file and key - Note that the certificate has to be trusted. +## Can be generated using the following command: +## openssl req -x509 -nodes -newkey rsa:2048 -days 5000 -sha256 -keyout localhost.pem -out localhost.pem +## See the documentation (wiki) for more information. + +# cert_file = 'localhost.pem' +# cert_key_file = 'localhost.pem' + + + ############################### # Query logging # ############################### @@ -298,19 +495,20 @@ cache_neg_max_ttl = 600 [query_log] - ## Path to the query log file (absolute, or relative to the same directory as the executable file) +## Path to the query log file (absolute, or relative to the same directory as the config file) +## Can be set to /dev/stdout in order to log to the standard output. - # file = 'query.log' +# file = 'query.log' - ## Query log format (currently supported: tsv and ltsv) +## Query log format (currently supported: tsv and ltsv) - format = 'tsv' +format = 'tsv' - ## Do not log these query types, to reduce verbosity. Keep empty to log everything. +## Do not log these query types, to reduce verbosity. Keep empty to log everything. - # ignored_qtypes = ['DNSKEY', 'NS'] +# ignored_qtypes = ['DNSKEY', 'NS'] @@ -324,22 +522,22 @@ cache_neg_max_ttl = 600 [nx_log] - ## Path to the query log file (absolute, or relative to the same directory as the executable file) +## Path to the query log file (absolute, or relative to the same directory as the config file) - # file = 'nx.log' +# file = 'nx.log' - ## Query log format (currently supported: tsv and ltsv) +## Query log format (currently supported: tsv and ltsv) - format = 'tsv' +format = 'tsv' ###################################################### -# Pattern-based blocking (blacklists) # +# Pattern-based blocking (blocklists) # ###################################################### -## Blacklists are made of one pattern per line. Example of valid patterns: +## Blocklists are made of one pattern per line. Example of valid patterns: ## ## example.com ## =example.com @@ -348,81 +546,108 @@ cache_neg_max_ttl = 600 ## ads*.example.* ## ads*.example[0-9]*.com ## -## Example blacklist files can be found at https://download.dnscrypt.info/blacklists/ -## A script to build blacklists from public feeds can be found in the -## `utils/generate-domains-blacklists` directory of the dnscrypt-proxy source code. +## Example blocklist files can be found at https://download.dnscrypt.info/blocklists/ +## A script to build blocklists from public feeds can be found in the +## `utils/generate-domains-blocklists` directory of the dnscrypt-proxy source code. -[blacklist] +[blocked_names] - ## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file) +## Path to the file of blocking rules (absolute, or relative to the same directory as the config file) - # blacklist_file = 'blacklist.txt' +# blocked_names_file = 'blocked-names.txt' - ## Optional path to a file logging blocked queries +## Optional path to a file logging blocked queries - # log_file = 'blocked.log' +# log_file = 'blocked-names.log' - ## Optional log format: tsv or ltsv (default: tsv) +## Optional log format: tsv or ltsv (default: tsv) - # log_format = 'tsv' +# log_format = 'tsv' ########################################################### -# Pattern-based IP blocking (IP blacklists) # +# Pattern-based IP blocking (IP blocklists) # ########################################################### -## IP blacklists are made of one pattern per line. Example of valid patterns: +## IP blocklists are made of one pattern per line. Example of valid patterns: ## ## 127.* ## fe80:abcd:* ## 192.168.1.4 -[ip_blacklist] +[blocked_ips] - ## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file) +## Path to the file of blocking rules (absolute, or relative to the same directory as the config file) - # blacklist_file = 'ip-blacklist.txt' +# blocked_ips_file = 'blocked-ips.txt' - ## Optional path to a file logging blocked queries +## Optional path to a file logging blocked queries - # log_file = 'ip-blocked.log' +# log_file = 'blocked-ips.log' - ## Optional log format: tsv or ltsv (default: tsv) +## Optional log format: tsv or ltsv (default: tsv) - # log_format = 'tsv' +# log_format = 'tsv' ###################################################### -# Pattern-based whitelisting (blacklists bypass) # +# Pattern-based allow lists (blocklists bypass) # ###################################################### -## Whitelists support the same patterns as blacklists -## If a name matches a whitelist entry, the corresponding session +## Allowlists support the same patterns as blocklists +## If a name matches an allowlist entry, the corresponding session ## will bypass names and IP filters. ## ## Time-based rules are also supported to make some websites only accessible at specific times of the day. -[whitelist] +[allowed_names] + +## Path to the file of allow list rules (absolute, or relative to the same directory as the config file) + +# allowed_names_file = 'allowed-names.txt' + + +## Optional path to a file logging allowed queries + +# log_file = 'allowed-names.log' + + +## Optional log format: tsv or ltsv (default: tsv) + +# log_format = 'tsv' + + + +######################################################### +# Pattern-based allowed IPs lists (blocklists bypass) # +######################################################### + +## Allowed IP lists support the same patterns as IP blocklists +## If an IP response matches an allowed entry, the corresponding session +## will bypass IP filters. +## +## Time-based rules are also supported to make some websites only accessible at specific times of the day. - ## Path to the file of whitelisting rules (absolute, or relative to the same directory as the executable file) +[allowed_ips] - # whitelist_file = 'whitelist.txt' +## Path to the file of allowed ip rules (absolute, or relative to the same directory as the config file) +# allowed_ips_file = 'allowed-ips.txt' - ## Optional path to a file logging whitelisted queries - # log_file = 'whitelisted.log' +## Optional path to a file logging allowed queries +# log_file = 'allowed-ips.log' - ## Optional log format: tsv or ltsv (default: tsv) +## Optional log format: tsv or ltsv (default: tsv) - # log_format = 'tsv' +# log_format = 'tsv' @@ -431,34 +656,33 @@ cache_neg_max_ttl = 600 ########################################## ## One or more weekly schedules can be defined here. -## Patterns in the name-based blocklist can optionally be followed with @schedule_name +## Patterns in the name-based blocked_names file can optionally be followed with @schedule_name ## to apply the pattern 'schedule_name' only when it matches a time range of that schedule. ## -## For example, the following rule in a blacklist file: +## For example, the following rule in a blocklist file: ## *.youtube.* @time-to-sleep -## would block access to YouTube only during the days, and period of the days -## define by the 'time-to-sleep' schedule. +## would block access to YouTube during the times defined by the 'time-to-sleep' schedule. ## ## {after='21:00', before= '7:00'} matches 0:00-7:00 and 21:00-0:00 ## {after= '9:00', before='18:00'} matches 9:00-18:00 [schedules] - # [schedules.'time-to-sleep'] - # mon = [{after='21:00', before='7:00'}] - # tue = [{after='21:00', before='7:00'}] - # wed = [{after='21:00', before='7:00'}] - # thu = [{after='21:00', before='7:00'}] - # fri = [{after='23:00', before='7:00'}] - # sat = [{after='23:00', before='7:00'}] - # sun = [{after='21:00', before='7:00'}] + # [schedules.time-to-sleep] + # mon = [{after='21:00', before='7:00'}] + # tue = [{after='21:00', before='7:00'}] + # wed = [{after='21:00', before='7:00'}] + # thu = [{after='21:00', before='7:00'}] + # fri = [{after='23:00', before='7:00'}] + # sat = [{after='23:00', before='7:00'}] + # sun = [{after='21:00', before='7:00'}] - # [schedules.'work'] - # mon = [{after='9:00', before='18:00'}] - # tue = [{after='9:00', before='18:00'}] - # wed = [{after='9:00', before='18:00'}] - # thu = [{after='9:00', before='18:00'}] - # fri = [{after='9:00', before='17:00'}] + # [schedules.work] + # mon = [{after='9:00', before='18:00'}] + # tue = [{after='9:00', before='18:00'}] + # wed = [{after='9:00', before='18:00'}] + # thu = [{after='9:00', before='18:00'}] + # fri = [{after='9:00', before='17:00'}] @@ -478,43 +702,209 @@ cache_neg_max_ttl = 600 ## must include the prefixes. ## ## If the `urls` property is missing, cache files and valid signatures -## must be already present; This doesn't prevent these cache files from +## must already be present. This doesn't prevent these cache files from ## expiring after `refresh_delay` hours. +## `refreshed_delay` must be in the [24..168] interval. +## The minimum delay of 24 hours (1 day) avoids unnecessary requests to servers. +## The maximum delay of 168 hours (1 week) ensures cache freshness. [sources] - ## An example of a remote source from https://github.com/DNSCrypt/dnscrypt-resolvers + ### An example of a remote source from https://github.com/DNSCrypt/dnscrypt-resolvers + + [sources.public-resolvers] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md'] + cache_file = 'public-resolvers.md' + minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + refresh_delay = 73 + prefix = '' + + ### Anonymized DNS relays + + [sources.relays] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/relays.md'] + cache_file = 'relays.md' + minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + refresh_delay = 73 + prefix = '' + + ### ODoH (Oblivious DoH) servers and relays + + # [sources.odoh-servers] + # urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-servers.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-servers.md'] + # cache_file = 'odoh-servers.md' + # minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + # refresh_delay = 73 + # prefix = '' + # [sources.odoh-relays] + # urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-relays.md'] + # cache_file = 'odoh-relays.md' + # minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + # refresh_delay = 73 + # prefix = '' + + ### Quad9 - [sources.'public-resolvers'] - urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v2/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md'] - cache_file = 'public-resolvers.md' - minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' - refresh_delay = 72 - prefix = '' + # [sources.quad9-resolvers] + # urls = ['https://www.quad9.net/quad9-resolvers.md'] + # minisign_key = 'RWQBphd2+f6eiAqBsvDZEBXBGHQBJfeG6G+wJPPKxCZMoEQYpmoysKUN' + # cache_file = 'quad9-resolvers.md' + # prefix = 'quad9-' - ## Quad9 over DNSCrypt - https://quad9.net/ + ### Another example source, with resolvers censoring some websites not appropriate for children + ### This is a subset of the `public-resolvers` list, so enabling both is useless. - # [sources.quad9-resolvers] - # urls = ["https://www.quad9.net/quad9-resolvers.md"] - # minisign_key = "RWQBphd2+f6eiAqBsvDZEBXBGHQBJfeG6G+wJPPKxCZMoEQYpmoysKUN" - # cache_file = "quad9-resolvers.md" - # refresh_delay = 72 - # prefix = "quad9-" + # [sources.parental-control] + # urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/parental-control.md', 'https://download.dnscrypt.info/resolvers-list/v3/parental-control.md'] + # cache_file = 'parental-control.md' + # minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + + ### dnscry.pt servers - See https://www.dnscry.pt + + # [sources.dnscry-pt-resolvers] + # urls = ["https://www.dnscry.pt/resolvers.md"] + # minisign_key = "RWQM31Nwkqh01x88SvrBL8djp1NH56Rb4mKLHz16K7qsXgEomnDv6ziQ" + # cache_file = "dnscry.pt-resolvers.md" + # refresh_delay = 73 + # prefix = "dnscry.pt-" + + +######################################### +# Servers with known bugs # +######################################### + +[broken_implementations] + +## Cisco servers currently cannot handle queries larger than 1472 bytes, and don't +## truncate responses larger than questions as expected by the DNSCrypt protocol. +## This prevents large responses from being received over UDP and over relays. +## +## Older versions of the `dnsdist` server software had a bug with queries larger +## than 1500 bytes. This is fixed since `dnsdist` version 1.5.0, but +## some server may still run an outdated version. +## +## The list below enables workarounds to make non-relayed usage more reliable +## until the servers are fixed. + +fragments_blocked = ['cisco', 'cisco-ipv6', 'cisco-familyshield', 'cisco-familyshield-ipv6', 'cisco-sandbox', 'cleanbrowsing-adult', 'cleanbrowsing-adult-ipv6', 'cleanbrowsing-family', 'cleanbrowsing-family-ipv6', 'cleanbrowsing-security', 'cleanbrowsing-security-ipv6'] + + + +################################################################# +# Certificate-based client authentication for DoH # +################################################################# + +## Use a X509 certificate to authenticate yourself when connecting to DoH servers. +## This is only useful if you are operating your own, private DoH server(s). +## 'creds' maps servers to certificates, and supports multiple entries. +## If you are not using the standard root CA, an optional "root_ca" +## property set to the path to a root CRT file can be added to a server entry. + +[doh_client_x509_auth] + +# creds = [ +# { server_name='*', client_cert='client.crt', client_key='client.key' } +# ] + + + +################################ +# Anonymized DNS # +################################ + +[anonymized_dns] + +## Routes are indirect ways to reach DNSCrypt servers. +## +## A route maps a server name ("server_name") to one or more relays that will be +## used to connect to that server. +## +## A relay can be specified as a DNS Stamp (either a relay stamp, or a +## DNSCrypt stamp) or a server name. +## +## The following example routes "example-server-1" via `anon-example-1` or `anon-example-2`, +## and "example-server-2" via the relay whose relay DNS stamp is +## "sdns://gRIxMzcuNzQuMjIzLjIzNDo0NDM". +## +## !!! THESE ARE JUST EXAMPLES !!! +## +## Review the list of available relays from the "relays.md" file, and, for each +## server you want to use, define the relays you want connections to go through. +## +## Carefully choose relays and servers so that they are run by different entities. +## +## "server_name" can also be set to "*" to define a default route, for all servers: +## { server_name='*', via=['anon-example-1', 'anon-example-2'] } +## +## If a route is ["*"], the proxy automatically picks a relay on a distinct network. +## { server_name='*', via=['*'] } is also an option, but is likely to be suboptimal. +## +## Manual selection is always recommended over automatic selection, so that you can +## select (relay,server) pairs that work well and fit your own criteria (close by or +## in different countries, operated by different entities, on distinct ISPs...) + +# routes = [ +# { server_name='example-server-1', via=['anon-example-1', 'anon-example-2'] }, +# { server_name='example-server-2', via=['sdns://gRIxMzcuNzQuMjIzLjIzNDo0NDM'] } +# ] + + +## Skip resolvers incompatible with anonymization instead of using them directly + +skip_incompatible = false + + +## If public server certificates for a non-conformant server cannot be +## retrieved via a relay, try getting them directly. Actual queries +## will then always go through relays. + +# direct_cert_fallback = false + + + +############################### +# DNS64 # +############################### + +## DNS64 is a mechanism for synthesizing AAAA records from A records. +## It is used with an IPv6/IPv4 translator to enable client-server +## communication between an IPv6-only client and an IPv4-only server, +## without requiring any changes to either the IPv6 or the IPv4 node, +## for the class of applications that work through NATs. +## +## There are two options to synthesize such records: +## Option 1: Using a set of static IPv6 prefixes; +## Option 2: By discovering the IPv6 prefix from DNS64-enabled resolver. +## +## If both options are configured - only static prefixes are used. +## (Ref. RFC6147, RFC6052, RFC7050) +## +## Do not enable unless you know what DNS64 is and why you need it, or else +## you won't be able to connect to anything at all. + +[dns64] + +## Static prefix(es) as Pref64::/n CIDRs + +# prefix = ['64:ff9b::/96'] + +## DNS64-enabled resolver(s) to discover Pref64::/n CIDRs +## These resolvers are used to query for Well-Known IPv4-only Name (WKN) "ipv4only.arpa." to discover only. +## Set with your ISP's resolvers in case of custom prefixes (other than Well-Known Prefix 64:ff9b::/96). +## IMPORTANT: Default resolvers listed below support Well-Known Prefix 64:ff9b::/96 only. - ## Another example source, with resolvers censoring some websites not appropriate for children - ## This is a subset of the `public-resolvers` list, so enabling both is useless +# resolver = ['[2606:4700:4700::64]:53', '[2001:4860:4860::64]:53'] - # [sources.'parental-control'] - # urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v2/parental-control.md', 'https://download.dnscrypt.info/resolvers-list/v2/parental-control.md'] - # cache_file = 'parental-control.md' - # minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' +######################################## +# Static entries # +######################################## ## Optional, local, static list of additional servers ## Mostly useful for testing your own servers. [static] - # [static.'google'] - # stamp = 'sdns://AgUAAAAAAAAAAAAOZG5zLmdvb2dsZS5jb20NL2V4cGVyaW1lbnRhbA' \ No newline at end of file + # [static.myserver] + # stamp = 'sdns://AQcAAAAAAAAAAAAQMi5kbnNjcnlwdC1jZXJ0Lg' \ No newline at end of file diff --git a/bind/custom/src/common.go b/bind/custom/src/common.go index 05e7491..c6d18ac 100644 --- a/bind/custom/src/common.go +++ b/bind/custom/src/common.go @@ -173,3 +173,5 @@ func ReadTextFile(filename string) (string, error) { bin = bytes.TrimPrefix(bin, []byte{0xef, 0xbb, 0xbf}) return string(bin), nil } + +func isDigit(b byte) bool { return b >= '0' && b <= '9' } \ No newline at end of file diff --git a/bind/custom/src/config.go b/bind/custom/src/config.go index 8e0e4a9..82ee81b 100644 --- a/bind/custom/src/config.go +++ b/bind/custom/src/config.go @@ -42,6 +42,7 @@ type Config struct { Timeout int `toml:"timeout"` KeepAlive int `toml:"keepalive"` Proxy string `toml:"proxy"` + CertRefreshConcurrency int `toml:"cert_refresh_concurrency"` CertRefreshDelay int `toml:"cert_refresh_delay"` CertIgnoreTimestamp bool `toml:"cert_ignore_timestamp"` EphemeralKeys bool `toml:"dnscrypt_ephemeral_keys"` @@ -117,6 +118,7 @@ func newConfig() Config { LocalDoH: LocalDoHConfig{Path: "/dns-query"}, Timeout: 5000, KeepAlive: 5, + CertRefreshConcurrency: 10, CertRefreshDelay: 240, HTTP3: false, CertIgnoreTimestamp: false, @@ -261,7 +263,7 @@ type ServerSummary struct { IPv6 bool `json:"ipv6"` Addrs []string `json:"addrs,omitempty"` Ports []int `json:"ports"` - DNSSEC bool `json:"dnssec"` + DNSSEC *bool `json:"dnssec,omitempty"` NoLog bool `json:"nolog"` NoFilter bool `json:"nofilter"` Description string `json:"description,omitempty"` @@ -292,6 +294,7 @@ type ConfigFlags struct { Resolve *string List *bool ListAll *bool + IncludeRelays *bool JSONOutput *bool Check *bool ConfigFile *string @@ -325,6 +328,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error { *flags.ConfigFile, ) } + WarnIfMaybeWritableByOtherUsers(foundConfigFile) config := newConfig() md, err := toml.DecodeFile(foundConfigFile, &config) if err != nil { @@ -438,6 +442,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error { if config.ForceTCP { proxy.mainProto = "tcp" } + proxy.certRefreshConcurrency = Max(1, config.CertRefreshConcurrency) proxy.certRefreshDelay = time.Duration(Max(60, config.CertRefreshDelay)) * time.Minute proxy.certRefreshDelayAfterFailure = time.Duration(10 * time.Second) proxy.certIgnoreTimestamp = config.CertIgnoreTimestamp @@ -741,11 +746,11 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error { return err } if len(proxy.registeredServers) == 0 { - return errors.New("No servers configured") + return errors.New("None of the servers listed in the server_names list was found in the configured sources.") } } if *flags.List || *flags.ListAll { - if err := config.printRegisteredServers(proxy, *flags.JSONOutput); err != nil { + if err := config.printRegisteredServers(proxy, *flags.JSONOutput, *flags.IncludeRelays); err != nil { return err } os.Exit(0) @@ -781,8 +786,47 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error { return nil } -func (config *Config) printRegisteredServers(proxy *Proxy, jsonOutput bool) error { +func (config *Config) printRegisteredServers(proxy *Proxy, jsonOutput bool, includeRelays bool) error { var summary []ServerSummary + if includeRelays { + for _, registeredRelay := range proxy.registeredRelays { + addrStr, port := registeredRelay.stamp.ServerAddrStr, stamps.DefaultPort + var hostAddr string + hostAddr, port = ExtractHostAndPort(addrStr, port) + addrs := make([]string, 0) + if (registeredRelay.stamp.Proto == stamps.StampProtoTypeDoH || registeredRelay.stamp.Proto == stamps.StampProtoTypeODoHTarget) && + len(registeredRelay.stamp.ProviderName) > 0 { + providerName := registeredRelay.stamp.ProviderName + var host string + host, port = ExtractHostAndPort(providerName, port) + addrs = append(addrs, host) + } + if len(addrStr) > 0 { + addrs = append(addrs, hostAddr) + } + nolog := true + nofilter := true + if registeredRelay.stamp.Proto == stamps.StampProtoTypeODoHRelay { + nolog = registeredRelay.stamp.Props&stamps.ServerInformalPropertyNoLog != 0 + } + serverSummary := ServerSummary{ + Name: registeredRelay.name, + Proto: registeredRelay.stamp.Proto.String(), + IPv6: strings.HasPrefix(addrStr, "["), + Ports: []int{port}, + Addrs: addrs, + NoLog: nolog, + NoFilter: nofilter, + Description: registeredRelay.description, + Stamp: registeredRelay.stamp.String(), + } + if jsonOutput { + summary = append(summary, serverSummary) + } else { + fmt.Println(serverSummary.Name) + } + } + } for _, registeredServer := range proxy.registeredServers { addrStr, port := registeredServer.stamp.ServerAddrStr, stamps.DefaultPort var hostAddr string @@ -798,13 +842,14 @@ func (config *Config) printRegisteredServers(proxy *Proxy, jsonOutput bool) erro if len(addrStr) > 0 { addrs = append(addrs, hostAddr) } + dnssec := registeredServer.stamp.Props&stamps.ServerInformalPropertyDNSSEC != 0 serverSummary := ServerSummary{ Name: registeredServer.name, Proto: registeredServer.stamp.Proto.String(), IPv6: strings.HasPrefix(addrStr, "["), Ports: []int{port}, Addrs: addrs, - DNSSEC: registeredServer.stamp.Props&stamps.ServerInformalPropertyDNSSEC != 0, + DNSSEC: &dnssec, NoLog: registeredServer.stamp.Props&stamps.ServerInformalPropertyNoLog != 0, NoFilter: registeredServer.stamp.Props&stamps.ServerInformalPropertyNoFilter != 0, Description: registeredServer.description, @@ -898,7 +943,7 @@ func (config *Config) loadSource(proxy *Proxy, cfgSourceName string, cfgSource * if cfgSource.RefreshDelay <= 0 { cfgSource.RefreshDelay = 72 } - cfgSource.RefreshDelay = Min(168, Max(24, cfgSource.RefreshDelay)) + cfgSource.RefreshDelay = Min(169, Max(25, cfgSource.RefreshDelay)) source, err := NewSource( cfgSourceName, proxy.xTransport, @@ -955,4 +1000,4 @@ func isIPAndPort(addrStr string) error { return fmt.Errorf("Port does not parse '%s' [%v]", addrStr, err) } return nil -} +} \ No newline at end of file diff --git a/bind/custom/src/plugin_forward.go b/bind/custom/src/plugin_forward.go index 0b5e505..27d790f 100644 --- a/bind/custom/src/plugin_forward.go +++ b/bind/custom/src/plugin_forward.go @@ -7,18 +7,34 @@ import ( "strings" "github.com/jedisct1/dlog" + "github.com/lifenjoiner/dhcpdns" "github.com/miekg/dns" ) +type SearchSequenceItemType int + +const ( + Explicit SearchSequenceItemType = iota + Bootstrap + DHCP +) + +type SearchSequenceItem struct { + typ SearchSequenceItemType + servers []string +} + type PluginForwardEntry struct { domain string - servers []string + sequence []SearchSequenceItem fallback string } type PluginForward struct { - forwardMap []PluginForwardEntry - fallbackTTL uint32 + forwardMap []PluginForwardEntry + fallbackTTL uint32 + bootstrapResolvers []string + dhcpdns []*dhcpdns.Detector } func (plugin *PluginForward) Name() string { @@ -31,11 +47,17 @@ func (plugin *PluginForward) Description() string { func (plugin *PluginForward) Init(proxy *Proxy) error { dlog.Noticef("Loading the set of forwarding rules from [%s]", proxy.forwardFile) + + if proxy.xTransport != nil { + plugin.bootstrapResolvers = proxy.xTransport.bootstrapResolvers + } + lines, err := ReadTextFile(proxy.forwardFile) plugin.fallbackTTL = proxy.ForwardFallbackTTL if err != nil { return err } + requiresDHCP := false for lineNo, line := range strings.Split(lines, "\n") { line = TrimAndStripInlineComments(line) if len(line) == 0 { @@ -43,6 +65,10 @@ func (plugin *PluginForward) Init(proxy *Proxy) error { } domain, serversStr, fallback, ok := StringThreeFields(line) + domain = strings.TrimPrefix(domain, "*.") + if strings.Contains(domain, "*") { + ok = false + } if !ok { domain, serversStr, ok = StringTwoFields(line) if !ok { @@ -53,23 +79,77 @@ func (plugin *PluginForward) Init(proxy *Proxy) error { } } domain = strings.ToLower(domain) - var servers []string + var sequence []SearchSequenceItem for _, server := range strings.Split(serversStr, ",") { server = strings.TrimSpace(server) - if net.ParseIP(server) != nil { - server = fmt.Sprintf("%s:%d", server, 53) + switch server { + case "$BOOTSTRAP": + if len(plugin.bootstrapResolvers) == 0 { + return fmt.Errorf( + "Syntax error for a forwarding rule at line %d. No bootstrap resolvers available", + 1+lineNo, + ) + } + if len(sequence) > 0 && sequence[len(sequence)-1].typ == Bootstrap { + // Ignore repetitions + } else { + sequence = append(sequence, SearchSequenceItem{typ: Bootstrap}) + dlog.Infof("Forwarding [%s] to the bootstrap servers", domain) + } + case "$DHCP": + if len(sequence) > 0 && sequence[len(sequence)-1].typ == DHCP { + // Ignore repetitions + } else { + sequence = append(sequence, SearchSequenceItem{typ: DHCP}) + dlog.Infof("Forwarding [%s] to the DHCP servers", domain) + } + requiresDHCP = true + default: + if strings.HasPrefix(server, "$") { + dlog.Criticalf("Unknown keyword [%s] at line %d", server, 1+lineNo) + continue + } + if server, err = normalizeIPAndOptionalPort(server, "53"); err != nil { + dlog.Criticalf("Syntax error for a forwarding rule at line %d: %s", 1+lineNo, err) + continue + } + idxServers := -1 + for i, item := range sequence { + if item.typ == Explicit { + idxServers = i + } + } + if idxServers == -1 { + sequence = append(sequence, SearchSequenceItem{typ: Explicit, servers: []string{server}}) + } else { + sequence[idxServers].servers = append(sequence[idxServers].servers, server) + } + dlog.Infof("Forwarding [%s] to [%s]", domain, server) } - servers = append(servers, server) - } - if len(servers) == 0 { - continue } plugin.forwardMap = append(plugin.forwardMap, PluginForwardEntry{ domain: domain, - servers: servers, + sequence: sequence, fallback: fallback, }) } + if requiresDHCP { + if len(proxy.userName) > 0 { + dlog.Warn("DHCP/DNS detection may not work when 'user_name' is set or when starting as a non-root user") + } + if proxy.SourceIPv6 { + dlog.Notice("Starting a DHCP/DNS detector for IPv6") + d6 := &dhcpdns.Detector{RemoteIPPort: "[2001:DB8::53]:80"} + go d6.Serve(9, 10) + plugin.dhcpdns = append(plugin.dhcpdns, d6) + } + if proxy.SourceIPv4 { + dlog.Notice("Starting a DHCP/DNS detector for IPv4") + d4 := &dhcpdns.Detector{RemoteIPPort: "192.0.2.53:80"} + go d4.Serve(9, 10) + plugin.dhcpdns = append(plugin.dhcpdns, d4) + } + } return nil } @@ -84,68 +164,143 @@ func (plugin *PluginForward) Reload() error { func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) error { qName := pluginsState.qName qNameLen := len(qName) - var servers []string + var sequence []SearchSequenceItem var fallback string for _, candidate := range plugin.forwardMap { candidateLen := len(candidate.domain) if candidateLen > qNameLen { continue } - if qName[qNameLen-candidateLen:] == candidate.domain && - (candidateLen == qNameLen || (qName[qNameLen-candidateLen-1] == '.')) { - servers = candidate.servers + if (qName[qNameLen-candidateLen:] == candidate.domain && + (candidateLen == qNameLen || (qName[qNameLen-candidateLen-1] == '.'))) || + (candidate.domain == ".") { + sequence = candidate.sequence fallback = candidate.fallback break } } - if len(servers) == 0 { + if len(sequence) == 0 { return nil } + var err error + var respMsg *dns.Msg + tries := 4 hasFallback := len(fallback) > 0 - server := servers[rand.Intn(len(servers))] - pluginsState.serverName = server - client := dns.Client{Net: pluginsState.serverProto, Timeout: pluginsState.timeout} - respMsg, _, err := client.Exchange(msg, server) - dlog.Noticef("Question: %s", msg.Question[0].String()) + for _, item := range sequence { + var server string + switch item.typ { + case Explicit: + server = item.servers[rand.Intn(len(item.servers))] + case Bootstrap: + server = plugin.bootstrapResolvers[rand.Intn(len(plugin.bootstrapResolvers))] + case DHCP: + const maxInconsistency = 9 + for _, dhcpdns := range plugin.dhcpdns { + inconsistency, ip, dhcpDNS, err := dhcpdns.Status() + if err != nil && ip != "" && inconsistency > maxInconsistency { + dlog.Infof("No response from the DHCP server while resolving [%s]", qName) + continue + } + if len(dhcpDNS) > 0 { + server = net.JoinHostPort(dhcpDNS[rand.Intn(len(dhcpDNS))].String(), "53") + break + } + } + if len(server) == 0 { + dlog.Infof("DHCP didn't provide any DNS server to forward [%s]", qName) + continue + } + } + pluginsState.serverName = server + if len(server) == 0 { + continue + } - if err != nil && !hasFallback { - return nil - } else if hasFallback && (err != nil || respMsg == nil || respMsg.Opcode == dns.OpcodeQuery && respMsg.Rcode != dns.RcodeSuccess) { - dlog.Noticef("Generating fallback response, fallback: %s", fallback) - synth := EmptyResponseFromMessage(msg) - synth.Rcode = dns.RcodeSuccess - synth.Answer = []dns.RR{} - parsedIP := ParseIP(fallback) - if parsedIP != nil { - rr := new(dns.A) - rr.Hdr = dns.RR_Header{Name: msg.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: plugin.fallbackTTL} - rr.A = parsedIP - synth.Answer = append(synth.Answer, rr) + if tries == 0 { + break + } + tries-- + dlog.Debugf("Forwarding [%s] to [%s]", qName, server) + client := dns.Client{Net: pluginsState.serverProto, Timeout: pluginsState.timeout} + respMsg, _, err = client.Exchange(msg, server) + if err != nil && !hasFallback { + return nil + } else if hasFallback && (err != nil || respMsg == nil || respMsg.Opcode == dns.OpcodeQuery && respMsg.Rcode != dns.RcodeSuccess) { + dlog.Noticef("Generating fallback response, fallback: %s", fallback) + synth := EmptyResponseFromMessage(msg) + synth.Rcode = dns.RcodeSuccess + synth.Answer = []dns.RR{} + parsedIP := ParseIP(fallback) + if parsedIP != nil { + rr := new(dns.A) + rr.Hdr = dns.RR_Header{Name: msg.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: plugin.fallbackTTL} + rr.A = parsedIP + synth.Answer = append(synth.Answer, rr) + } else { + rr := new(dns.CNAME) + rr.Hdr = dns.RR_Header{Name: msg.Question[0].Name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: plugin.fallbackTTL} + rr.Target = dns.Fqdn(fallback) + synth.Answer = append(synth.Answer, rr) + } + respMsg = synth } else { - rr := new(dns.CNAME) - rr.Hdr = dns.RR_Header{Name: msg.Question[0].Name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: plugin.fallbackTTL} - rr.Target = dns.Fqdn(fallback) - synth.Answer = append(synth.Answer, rr) + if respMsg.Truncated { + client.Net = "tcp" + respMsg, _, err = client.Exchange(msg, server) + if err != nil { + continue + } + } + if len(sequence) > 0 { + switch respMsg.Rcode { + case dns.RcodeNameError, dns.RcodeRefused, dns.RcodeNotAuth: + continue + } + } + if edns0 := respMsg.IsEdns0(); edns0 == nil || !edns0.Do() { + respMsg.AuthenticatedData = false + } } + respMsg.Id = msg.Id + pluginsState.synthResponse = respMsg + pluginsState.action = PluginsActionSynth + pluginsState.returnCode = PluginsReturnCodeForward + return nil + } + return err +} - respMsg = synth - } else { - if respMsg.Truncated { - client.Net = "tcp" - respMsg, _, err = client.Exchange(msg, server) +func normalizeIPAndOptionalPort(addr string, defaultPort string) (string, error) { + var host, port string + var err error + + if strings.HasPrefix(addr, "[") { + if !strings.Contains(addr, "]:") { + if addr[len(addr)-1] != ']' { + return "", fmt.Errorf("invalid IPv6 format: missing closing ']'") + } + host = addr[1 : len(addr)-1] + port = defaultPort + } else { + host, port, err = net.SplitHostPort(addr) if err != nil { - return err + return "", err } } - if edns0 := respMsg.IsEdns0(); edns0 == nil || !edns0.Do() { - respMsg.AuthenticatedData = false + } else { + host, port, err = net.SplitHostPort(addr) + if err != nil { + host = addr + port = defaultPort } } - - respMsg.Id = msg.Id - pluginsState.synthResponse = respMsg - pluginsState.action = PluginsActionSynth - pluginsState.returnCode = PluginsReturnCodeForward - return nil -} + ip := net.ParseIP(host) + if ip == nil { + return "", fmt.Errorf("invalid IP address: [%s]", host) + } + if ip.To4() != nil { + return fmt.Sprintf("%s:%s", ip.String(), port), nil + } + return fmt.Sprintf("[%s]:%s", ip.String(), port), nil +} \ No newline at end of file diff --git a/bind/custom/src/proxy.go b/bind/custom/src/proxy.go index 69c4f59..18a5e83 100644 --- a/bind/custom/src/proxy.go +++ b/bind/custom/src/proxy.go @@ -74,6 +74,7 @@ type Proxy struct { certRefreshDelayAfterFailure time.Duration timeout time.Duration certRefreshDelay time.Duration + certRefreshConcurrency int cacheSize int logMaxBackups int logMaxAge int @@ -118,11 +119,18 @@ func (proxy *Proxy) registerLocalDoHListener(listener *net.TCPListener) { } func (proxy *Proxy) addDNSListener(listenAddrStr string) { - listenUDPAddr, err := net.ResolveUDPAddr("udp", listenAddrStr) + udp := "udp" + tcp := "tcp" + isIPv4 := isDigit(listenAddrStr[0]) + if isIPv4 { + udp = "udp4" + tcp = "tcp4" + } + listenUDPAddr, err := net.ResolveUDPAddr(udp, listenAddrStr) if err != nil { dlog.Fatal(err) } - listenTCPAddr, err := net.ResolveTCPAddr("tcp", listenAddrStr) + listenTCPAddr, err := net.ResolveTCPAddr(tcp, listenAddrStr) if err != nil { dlog.Fatal(err) } @@ -141,11 +149,11 @@ func (proxy *Proxy) addDNSListener(listenAddrStr string) { // if 'userName' is set and we are the parent process if !proxy.child { // parent - listenerUDP, err := net.ListenUDP("udp", listenUDPAddr) + listenerUDP, err := net.ListenUDP(udp, listenUDPAddr) if err != nil { dlog.Fatal(err) } - listenerTCP, err := net.ListenTCP("tcp", listenTCPAddr) + listenerTCP, err := net.ListenTCP(tcp, listenTCPAddr) if err != nil { dlog.Fatal(err) } @@ -186,7 +194,12 @@ func (proxy *Proxy) addDNSListener(listenAddrStr string) { } func (proxy *Proxy) addLocalDoHListener(listenAddrStr string) { - listenTCPAddr, err := net.ResolveTCPAddr("tcp", listenAddrStr) + network := "tcp" + isIPv4 := isDigit(listenAddrStr[0]) + if isIPv4 { + network = "tcp4" + } + listenTCPAddr, err := net.ResolveTCPAddr(network, listenAddrStr) if err != nil { dlog.Fatal(err) } @@ -202,7 +215,7 @@ func (proxy *Proxy) addLocalDoHListener(listenAddrStr string) { // if 'userName' is set and we are the parent process if !proxy.child { // parent - listenerTCP, err := net.ListenTCP("tcp", listenTCPAddr) + listenerTCP, err := net.ListenTCP(network, listenTCPAddr) if err != nil { dlog.Fatal(err) } @@ -442,7 +455,13 @@ func (proxy *Proxy) udpListenerFromAddr(listenAddr *net.UDPAddr) error { if err != nil { return err } - clientPc, err := listenConfig.ListenPacket(context.Background(), "udp", listenAddr.String()) + listenAddrStr := listenAddr.String() + network := "udp" + isIPv4 := isDigit(listenAddrStr[0]) + if isIPv4 { + network = "udp4" + } + clientPc, err := listenConfig.ListenPacket(context.Background(), network, listenAddrStr) if err != nil { return err } @@ -456,7 +475,13 @@ func (proxy *Proxy) tcpListenerFromAddr(listenAddr *net.TCPAddr) error { if err != nil { return err } - acceptPc, err := listenConfig.Listen(context.Background(), "tcp", listenAddr.String()) + listenAddrStr := listenAddr.String() + network := "tcp" + isIPv4 := isDigit(listenAddrStr[0]) + if isIPv4 { + network = "tcp4" + } + acceptPc, err := listenConfig.Listen(context.Background(), network, listenAddrStr) if err != nil { return err } @@ -470,7 +495,13 @@ func (proxy *Proxy) localDoHListenerFromAddr(listenAddr *net.TCPAddr) error { if err != nil { return err } - acceptPc, err := listenConfig.Listen(context.Background(), "tcp", listenAddr.String()) + listenAddrStr := listenAddr.String() + network := "tcp" + isIPv4 := isDigit(listenAddrStr[0]) + if isIPv4 { + network = "tcp4" + } + acceptPc, err := listenConfig.Listen(context.Background(), network, listenAddrStr) if err != nil { return err } @@ -518,7 +549,7 @@ func (proxy *Proxy) exchangeWithUDPServer( var pc net.Conn proxyDialer := proxy.xTransport.proxyDialer if proxyDialer == nil { - pc, err = net.DialUDP("udp", nil, upstreamAddr) + pc, err = net.DialTimeout("udp", upstreamAddr.String(), serverInfo.Timeout) } else { pc, err = (*proxyDialer).Dial("udp", upstreamAddr.String()) } @@ -561,7 +592,7 @@ func (proxy *Proxy) exchangeWithTCPServer( var pc net.Conn proxyDialer := proxy.xTransport.proxyDialer if proxyDialer == nil { - pc, err = net.DialTCP("tcp", nil, upstreamAddr) + pc, err = net.DialTimeout("tcp", upstreamAddr.String(), serverInfo.Timeout) } else { pc, err = (*proxyDialer).Dial("tcp", upstreamAddr.String()) } @@ -877,4 +908,4 @@ func NewProxy() *Proxy { return &Proxy{ serversInfo: NewServersInfo(), } -} +} \ No newline at end of file diff --git a/bind/entrypoint.sh b/bind/entrypoint.sh index ba83c19..4555342 100755 --- a/bind/entrypoint.sh +++ b/bind/entrypoint.sh @@ -1,5 +1,13 @@ #!/bin/sh +if [ -n "$PUBLIC_RESOLVERS_OVERRIDE" ]; then + # Convert comma-separated server names into a TOML array format with single quotes + SERVER_LIST=$(echo "$PUBLIC_RESOLVERS_OVERRIDE" | sed "s/[[:space:]]*,[[:space:]]*/\', \'/g") + SERVER_LIST="['${SERVER_LIST}']" + + sed -i "s#^server_names = .*#server_names = ${SERVER_LIST}#" dnscrypt-proxy.toml +fi + # Start DNS server in background right away /app/dnscrypt-proxy & diff --git a/docker-compose.yml b/docker-compose.yml index 99ac410..65f1e6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,13 +5,15 @@ services: build: context: bind args: - UPSTREAM_VERSION: 2.1.5 + UPSTREAM_VERSION: 2.1.8 restart: always networks: dncore_network: ipv4_address: 172.33.1.2 aliases: - bind.dappnode + environment: + - PUBLIC_RESOLVERS_OVERRIDE logging: driver: journald image: "bind.dnp.dappnode.eth:0.3.0"